最近在做 AI 辅助游戏开发方向的灰度验证时,我抛给 DeepSeek 一个颇有分量的任务:不给任何具名代码,只提供《杀戮尖塔》的核心规则描述,让它从零复刻一个可运行的简化原型。一次小范围灰测的结果出乎意料——整套卡牌战斗循环、能量机制、随机地图生成思路都被完整输出,运行起来像模像样。这篇文章就把这次“灰测炸场”的完整过程整理出来,从概念拆解到 Prompt 设计,再到 Python 代码实现和排错清单,既能当 AI 编程实验记录看,也能当作一次游戏系统设计实战入门。
1. 灰测与复刻:先弄清楚这次实验在做什么
1.1 什么是灰测
“灰测”在不同语境下有不同含义。在游戏行业,它通常指“灰度测试”,即在一个小范围用户群体中提前开放版本,收集数据和反馈,确认稳定后再全量放出。在软件测试领域,它也可能指“灰盒测试”,介于黑盒和白盒之间,既关注输入输出,也关注内部逻辑。
在本文的场景里,“灰测”更接近灰度测试的扩展用法:先用一个受控的小任务来验证 AI 模型、Prompt 策略和开发流程是否可靠。DeepSeek 的能力并不需要靠“能不能写排序算法”来证明,直接给它一个中等复杂度的真实项目,才能看出它在任务拆解、数据结构设计、边界条件处理上的真实水平。
复刻《杀戮尖塔》就是一个很好的灰测用例。它规则清晰,却包含多个系统:卡牌、敌人、回合、地图、事件、遗物。既有状态机,又有随机策略,难度适中,适合观察 AI 的工程化生成能力。
1.2 为什么选择《杀戮尖塔》作为复刻对象
《杀戮尖塔》(Slay the Spire)是一款 Roguelike 卡牌游戏,玩家在爬塔过程中不断打怪、选卡、获得遗物,最终挑战 Boss。它的核心规则可以拆成几个彼此独立又相互依赖的模块:
- 回合制战斗:玩家每回合获得能量,用手牌攻击、防御或释放技能。
- 卡牌系统:卡牌有费用、伤害、护甲、抽牌、能量回复等属性。
- 敌人意图系统:敌人头顶会显示下回合行动,玩家需要据此决策。
- Roguelike 地图:每一层是若干条路线,节点包括战斗、事件、商店、宝箱等。
- 遗物与事件:遗物提供全局被动增益,事件提供风险抉择。
这些系统具备了做一个完整游戏原型所需的大部分要素,又不像大型 RPG 那样动辄几万行代码,非常适合用来评估 AI 在短时间内的产出质量。
1.3 读完这篇文章你能掌握什么
本文不打算只展示“AI 好厉害”的结果,而是把 AI 辅助复刻的完整方法论拆给你看。
你会掌握:
- DeepSeek 的 API 接入方式和本地部署思路。
- 把复杂游戏拆解成任务的 Prompt 设计方法。
- 一套基于 Python 的简化版《杀戮尖塔》战斗系统代码。
- 从 Demo 扩展到地图、事件、存档模块的设计方案。
- AI 生成代码时的高频坑点和排查思路。
2. 环境准备:DeepSeek 接入与工程目录
2.1 DeepSeek 的三种接入方式
在开始复刻之前,先要把 DeepSeek 接入到开发环境里。目前常见的方式大致有三种:
- 官方 API 调用:通过 HTTP 请求访问 DeepSeek 开放平台,适合脚本、后端服务和自动化流程,也是本文重点演示的方式。
- 本地部署开源模型:DeepSeek 系列开源模型支持本地部署,使用 vLLM、Ollama 等推理框架加载模型权重后,通过兼容接口对外提供服务。适合对数据隐私要求较高的场景,也适合像 Jetson Orin 这类边缘设备上的离线推理实验。
- 第三方工具集成:将 DeepSeek 接入到 VS Code、Codex CLI、企业微信机器人等工具中,让 AI 在编码和办公场景里直接工作。
这三种方式各有适用场景。本文的复刻实验用 API 方式就够了。如果你的环境不允许调用外部 API,也可以选择本地部署,只要把代码里的请求地址改成本地推理服务的地址即可。
2.2 DeepSeek API 最小调用示例
在写完整项目之前,先验证 API 是否连通。以 Python 为例,使用 requests 库发起一个最简单的对话请求:
import requests url = "https://api.deepseek.com/chat/completions" headers = { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" } payload = { "model": "deepseek-chat", "messages": [ {"role": "system", "content": "你是一名资深游戏架构师和Python开发工程师。"}, {"role": "user", "content": "请用一句话说明杀戮尖塔的核心玩法。"} ], "temperature": 0.7 } response = requests.post(url, headers=headers, json=payload, timeout=60) print(response.json()["choices"][0]["message"]["content"])需要注意,模型名称、API 地址和鉴权方式可能随官方文档更新而变化。实际使用时,以 DeepSeek 开放平台当前最新文档为准。API_KEY 不要直接硬编码到代码里,推荐放到环境变量或者本地配置文件中。
你如果要在命令行里快速验证,也可以使用 curl:
curl https://api.deepseek.com/chat/completions \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "deepseek-chat", "messages": [ {"role": "system", "content": "你是一名Python开发工程师。"}, {"role": "user", "content": "用Python写一个计算卡牌游戏回合数的函数"} ] }'2.3 项目目录结构
整个复刻 Demo 采用纯 Python 标准库实现,不依赖第三方游戏引擎,目录结构如下:
slay-the-spire-demo/ ├── main.py # 入口,启动游戏 ├── card_models.py # 卡牌数据模型 ├── battle.py # 战斗系统 ├── map_generator.py # Roguelike 地图生成 ├── events.py # 事件与遗物配置 └── save_manager.py # 存档管理Python 版本推荐使用 3.10 及以上,因为示例里用到了 dataclass 和类型注解。如果你的 Python 版本较低,大部分代码仍然可用,但需要手动调整类型注解写法。
3. 任务拆解与 Prompt 设计:让 AI 按你的思路写代码
3.1 先拆任务,再让 AI 动手
直接对 AI 说“帮我复刻杀戮尖塔”,得到的往往是一堆泛泛的方案。正确做法是先拆成可执行的子任务:数据模型、战斗循环、地图生成等。每个子任务再对应一次独立的 Prompt 会话,这样 AI 的输出会更有针对性,也更容易排查问题。
《杀戮尖塔》的简化任务树可以拆成这样:
- 设计卡牌数据模型:名称、费用、伤害、护甲、特殊效果。
- 设计角色数据模型:生命值、能量、手牌、抽牌堆、弃牌堆、格挡值。
- 实现战斗回合循环:抽牌 → 玩家操作 → 敌人行动 → 判定胜负。
- 实现敌人行为:意图系统、攻击计算。
- 实现地图生成:节点路线、随机事件。
- 实现存档:把当前状态保存为 JSON 文件。
3.2 系统 Prompt 的写法
好的系统 Prompt 能显著提升 AI 输出质量。在这次灰测中,我给 DeepSeek 设置了一个“游戏架构师 + Python 工程师”的角色,并给出了明确的输出约束:
你是一名资深的游戏系统架构师,同时也是经验丰富的 Python 开发者。 你的任务是帮助我实现一个简化版的《杀戮尖塔》卡牌战斗原型。 要求: 1. 代码使用 Python 标准库,不依赖第三方库。 2. 数据模型优先使用 dataclass。 3. 每个函数必须有注释,说明输入、输出和边界情况。 4. 卡牌效果目前只支持伤害、护甲和抽牌,不要实现过于复杂的机制。 5. 输出代码前,先用三句话说明你的设计方案。这个 Prompt 的关键点在于:限定了技术栈,明确了输出结构,缩小了功能范围。AI 生成的内容不会因为想得太复杂而收不住。
3.3 多轮迭代:把报错信息回喂给 AI
AI 生成代码很少一次通过,尤其是涉及状态管理的战斗系统。遇到报错时,不要急着自己改,把完整的错误信息回传给 AI,并附加上下文:
这是 battle.py 中的回合循环代码: [贴入代码] 运行时抛出以下异常: [贴入完整 traceback 信息] 请分析异常根因,并给出修复后的完整代码。这种方式能有效让 AI 定位问题。但要注意,AI 有可能反复给出同一套无效修复方案。遇到这种情况,你需要自己介入,暂停 AI 的“建议循环”,手动检查状态变量的更新顺序,再引导 AI 继续。
4. 核心实战:用 Python 实现简化版卡牌战斗
这一节我们直接进入代码实现。先把最核心的战斗系统跑通,再扩展地图和事件。
4.1 卡牌与角色数据模型
文件:card_models.py
from dataclasses import dataclass, field from typing import List @dataclass class Card: name: str cost: int = 1 damage: int = 0 block: int = 0 draw: int = 0 def description(self) -> str: parts = [] if self.damage > 0: parts.append(f"造成 {self.damage} 点伤害") if self.block > 0: parts.append(f"获得 {self.block} 点格挡") if self.draw > 0: parts.append(f"抽 {self.draw} 张牌") return ",".join(parts) @dataclass class Character: name: str hp: int max_hp: int energy: int = 3 block: int = 0 hand: List[Card] = field(default_factory=list) draw_pile: List[Card] = field(default_factory=list) discard_pile: List[Card] = field(default_factory=list) def take_damage(self, amount: int) -> None: remaining = amount - self.block self.block = max(0, self.block - amount) if remaining > 0: self.hp -= remaining if self.hp < 0: self.hp = 0Character 类的 take_damage 方法实现了“先扣格挡,再扣血”的规则。这个顺序在卡牌战斗里非常重要,如果先扣血再扣格挡,会出现护甲形同虚设的 bug。
文件:battle.py
import random from card_models import Card, Character def create_default_deck() -> list: deck = [] for _ in range(5): deck.append(Card(name="打击", cost=1, damage=6)) for _ in range(5): deck.append(Card(name="防御", cost=1, block=6)) return deck def draw_cards(player: Character, count: int) -> None: for _ in range(count): if not player.draw_pile: if not player.discard_pile: break player.draw_pile = player.discard_pile player.discard_pile = [] random.shuffle(player.draw_pile) player.hand.append(player.draw_pile.pop()) def discard_hand(player: Character) -> None: player.discard_pile.extend(player.hand) player.hand.clear() def play_card(player: Character, enemy: Character, card_index: int) -> bool: if card_index < 0 or card_index >= len(player.hand): print("无效的卡牌编号") return False card = player.hand[card_index] if player.energy < card.cost: print(f"能量不足,{card.name} 需要 {card.cost} 点能量") return False player.energy -= card.cost if card.damage > 0: enemy.take_damage(card.damage) print(f"你使用【{card.name}】,对敌人造成 {card.damage} 点伤害") if card.block > 0: player.block += card.block print(f"你使用【{card.name}】,获得 {card.block} 点格挡") if card.draw > 0: draw_cards(player, card.draw) print(f"你使用【{card.name}】,抽 {card.draw} 张牌") player.discard_pile.append(player.hand.pop(card_index)) return True def enemy_turn(player: Character, enemy: Character, attack_value: int) -> None: print(f"\n敌人的回合,意图攻击 {attack_value} 点伤害") enemy_intent = attack_value player.take_damage(enemy_intent) if player.block > 0: print(f"格挡吸收了伤害,当前格挡值为 {player.block}") print(f"玩家剩余生命值:{player.hp}")这里的 draw_cards 函数处理了抽牌堆耗尽的情况。当抽牌堆为空且弃牌堆有牌时,把弃牌堆洗入抽牌堆,这也是标准卡牌游戏的逻辑。
4.2 主战斗循环
继续编写 battle.py 中的战斗入口函数:
def start_battle(player: Character, enemy: Character) -> None: round_number = 1 while player.hp > 0 and enemy.hp > 0: print(f"\n======== 第 {round_number} 回合 ========") print(f"玩家 HP: {player.hp}/{player.max_hp} 格挡: {player.block} 能量: {player.energy}") print(f"敌人 HP: {enemy.hp} 意图攻击: 6") player.block = 0 player.energy = 3 draw_cards(player, 5) # 玩家操作阶段 while True: print("\n当前手牌:") for i, card in enumerate(player.hand): print(f"[{i}] {card.name}(费用 {card.cost}):{card.description()}") print(f"剩余能量:{player.energy}") choice = input("输入卡牌编号出牌,输入 e 结束回合:").strip() if choice.lower() == "e": break if not choice.isdigit(): print("请输入数字编号") continue play_card(player, enemy, int(choice)) if enemy.hp <= 0: break if enemy.hp <= 0: print("敌人已被击败!") break discard_hand(player) # 敌人回合,这里固定攻击 6,后续可以扩展为意图系统 enemy_turn(player, enemy, 6) round_number += 1 if player.hp <= 0: print("你被击败了……") else: print("战斗胜利!")文件:main.py
from battle import start_battle, create_default_deck from card_models import Character def main(): player = Character(name="灰测者", hp=80, max_hp=80) player.draw_pile = create_default_deck() enemy = Character(name="史莱姆", hp=50, max_hp=50) start_battle(player, enemy) if __name__ == "__main__": main()4.3 运行与验证
在命令行执行:
python main.py预期输出大致如下:
======== 第 1 回合 ======== 玩家 HP: 80/80 格挡: 0 能量: 3 敌人 HP: 50 意图攻击: 6 当前手牌: [0] 打击(费用 1):造成 6 点伤害 [1] 防御(费用 1):获得 6 点格挡 [2] 打击(费用 1):造成 6 点伤害 [3] 打击(费用 1):造成 6 点伤害 [4] 防御(费用 1):获得 6 点格挡 剩余能量:3你可以按数字键出牌,按 e 结束回合。整个 Demo 已经具备完整的战斗闭环。
4.4 结果说明与代码审查
从这个 Demo 可以看出,AI 生成的代码在结构上有几个值得肯定的地方:
- 数据模型独立,便于扩展卡牌种类。
- 抽牌堆、弃牌堆、手牌分离,符合真实卡牌游戏设计。
- 回合状态清晰,玩家操作和敌人行动没有耦合。
但也要注意,这个版本还存在一些明显的边界问题:
- 没有处理抽牌堆为空且弃牌堆也为空时,手牌数量不足 5 张的情况。
- 敌人意图是写死的,没有实现随机机制。
- 没有胜利后的结算和奖励选择。
这些问题也正是下一步扩展的方向。你完全可以把这个代码段拿给 DeepSeek,让它在此基础上补全功能。
5. 从 Demo 走向完整复刻:地图、事件与存档
战斗系统跑通之后,离“杀戮尖塔”还差很多。接下来逐一扩展。
5.1 地图生成模块
文件:map_generator.py
《杀戮尖塔》的地图特点是分层的节点路线,玩家从底层走到顶层。简化版本可以这样实现:
import random def generate_map(rows: int = 7, branch: int = 3) -> list: """ 生成一个分层地图结构。 每一层是若干节点,节点类型包含战斗、事件、商店、宝箱。 """ node_types = ["战斗", "战斗", "事件", "商店", "宝箱"] game_map = [] for row in range(rows): nodes = [] for _ in range(branch): nodes.append({ "row": row, "type": random.choice(node_types), "visited": False }) game_map.append(nodes) return game_map这个生成器没有做层与层之间的路径连接,实际项目中还需要实现“从上一层的某节点出发,下一步只能走到相邻下一层节点”的路由规则。你可以在扩展时把相邻关系表示为每个节点的 children 列表。
5.2 事件与遗物体系
事件和遗物是 Roguelike 游戏的内容填充器。事件可以做成一个简单的配置表:
文件:events.py
import random EVENTS = [ { "id": "ghost_hut", "name": "鬼魂小屋", "desc": "一个幽灵提出给你 80 点生命值,但从所有卡牌中移除一张。", "choices": [ {"text": "接受", "hp_gain": 80, "remove_card": True}, {"text": "离开", "hp_gain": 0} ] }, { "id": "bonfire", "name": "篝火", "desc": "你在一堆篝火旁休息。", "choices": [ {"text": "休息", "hp_gain": 20}, {"text": "锻造卡牌", "upgrade_card": True} ] } ] def gen_random_event(): return random.choice(EVENTS)遗物可以作为一种被动能力挂在角色身上,例如“每回合抽牌数量 +1”。这个机制在数据模型上只需要加字段即可。
5.3 存档方案
Roguelike 游戏必须支持半途退出。JSON 是当前最合适的存档格式:
import json from card_models import Character, Card def to_dict(player: Character, game_map: list) -> dict: return { "hp": player.hp, "max_hp": player.max_hp, "energy": player.energy, "block": player.block, "deck": [c.name for c in player.draw_pile], "map": game_map } def save_game(player: Character, game_map: list, path: str = "save.json") -> None: with open(path, "w", encoding="utf-8") as f: json.dump(to_dict(player, game_map), f, ensure_ascii=False, indent=2)存档时需要注意,卡牌是对象,不能直接序列化。这里用卡牌名称代替,读档时再根据名称从卡牌配置表里恢复对象。这种设计在真实项目中很常见,避免对象引用导致的序列化问题。
6. 常见问题与排查思路
在复刻过程中,最容易踩到下面几个问题。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| API 请求超时 | 网络不稳定或模型推理时间过长 | 增加 timeout 参数,使用流式输出观察进度 |
| AI 生成的代码运行报错 | 变量名不一致或状态更新顺序错误 | 把完整报错信息回传给 AI,要求定位行号 |
| 抽牌堆越界 | 没有处理抽牌堆和弃牌堆为空的情况 | 检查 draw_cards 中的边界条件 |
| 战斗回合无限循环 | 双方 HP 都没有发生变化 | 检查伤害计算逻辑,确认卡牌费用能正常扣除 |
| 存档文件乱码 | 使用了不兼容的编码格式 | 写入时使用 encoding="utf-8" |
| AI 输出内容超出预期范围 | 系统 Prompt 约束不够具体 | 明确限定功能范围,禁止多实现无关机制 |
特别提醒:如果你的 AI 生成代码中出现了“打不开图片素材”“找不到音频文件”这类问题,通常是因为 AI 在输出时假设了额外的目录结构。这时候需要人工介入,把项目的真实目录结构告诉 AI,再重新生成。
7. 最佳实践与工程建议
7.1 提示词工程层面的建议
不要用含糊的描述。把需求拆成“输入-处理-输出”三段式:
- 输入:当前角色的属性、手牌、剩余能量。
- 处理:选择卡牌后的状态变化规则。
- 输出:更新后的角色状态、日志打印。
每次对话只让 AI 完成一个小目标,例如“只实现伤害问题,不碰格挡”。这样可以显著降低代码出错的概率。
7.2 代码质量控制
AI 生成的代码需要人工 review,重点检查三部分:
- 数据流是否完整:状态变量是否在回合开始时正确重置。
- 边界条件是否覆盖:手牌 0 张、抽牌堆空、敌方 HP 为 0。
- 可扩展性是否够好:卡牌效果写死还是可以通过配置扩展。
如果 AI 把大量逻辑堆在一个函数里,不要犹豫,要求它拆成多个小函数。卡牌游戏的状态管理一旦耦合过深,后期加新机制会非常痛苦。
7.3 版权与合规边界
复刻《杀戮尖塔》是技术学习行为。如果你要做公开发布或商业化版本,必须警惕版权问题。
本文所有示例不包含《杀戮尖塔》的原始美术资源、音乐资源和具体文案,只实现玩法规则层面的简化版本。如果你打算使用原版素材,需要提前确认授权情况,否则即使代码全部自研,美术和音频素材也可能带来法律风险。
另外,使用 DeepSeek API 时,要注意密钥安全,不要提交到公共仓库;生产环境调用要遵守平台的使用条款,必要时咨询所在公司的合规意见。
7.4 从“AI 生成”到“工程落地”的完整流程
实际项目中,不建议把 AI 生成的代码直接搬进生产环境。推荐流程是:
- 让 AI 生成初版原型,验证玩法是否可行。
- 人工整理代码结构和命名规范。
- 编写单元测试,覆盖回合循环和卡牌边界。
- 在测试环境跑完整对局,记录数据。
- 通过灰度测试逐步扩大试用范围,再上正式环境。
AI 的价值在于把“从零到 60 分”的时间大幅缩短,而“从 60 分到 90 分”仍然需要人的架构能力和工程经验。
8. 总结与后续学习路线
这次灰测实验的核心收获不是“DeepSeek 能复刻杀戮尖塔”,而是一套可以复用的 AI 辅助开发方法论:拆分任务、设计 Prompt、多轮迭代、人工审查。你拿这套方法去复刻扫雷、斗地主、自走棋 Demo,流程完全一致。
如果你想继续深入,可以有这样几个方向:
- 给战斗系统加入意图系统,让敌人行为不可预测但又有规律。
- 把卡牌效果改为 JSON 配置驱动,让策划不用改代码就能加新卡。
- 给游戏补充 UI,用 Pygame 或 Web 前端替换当前的黑框命令行界面。
- 编写自动化测试脚本,模拟一万次对局验证数值平衡。
动手试一次,比看十篇教程都有用。下次给 AI 一个完整游戏规则时,记得先从最小可玩循环开始,而不是一上来就让它输出一个“完整游戏”。