Game AI — LLM Vision + TAS 自动通关系统设计
LLM 看屏幕 · 模拟操控 · TAS 仿真训练 · MBE 闭环进化
创建时间: 2026-02-07 状态: 设计阶段
1. 核心思路
不走传统 RL(训练 CNN + 策略网络),而是:
截屏 → LLM Vision 理解画面 → LLM 推理决策 → 模拟键鼠执行 → 观察结果 → 循环
为什么这条路可行?
| 传统 RL 方案 | LLM Vision 方案 |
|---|---|
| 需要百万帧训练 | 零样本就能理解画面 |
| 每个游戏重新训练 CNN | 同一个 LLM 通吃所有游戏 |
| 奖励函数难设计 | LLM 自己理解"通关"是什么意思 |
| 无法利用攻略知识 | 直接注入攻略文本到 Prompt |
| 决策不可解释 | LLM 输出思考过程,完全可解释 |
| 泛化能力差 | 换游戏只需换 Prompt |
TAS 仿真的优势:
| 能力 | 说明 |
|---|---|
| 存档/读档 | 失败时回滚到检查点,不必从头开始 |
| 帧步进 | AI 可以"暂停时间"慢慢思考,不需要实时反应 |
| 内存读取 | 直接读 RAM 获取精确状态(血量、坐标、分数) |
| 输入录制 | 记录操作序列,可回放、分析、分享 |
| 加速/减速 | 训练时 10x 加速,验证时正常速度 |
| 确定性复现 | 同一存档 + 同一输入 = 完全相同结果 |
2. 系统架构
╔═══════════════════════════════════════════════════════════════════════╗
║ Game AI TAS System ║
║ ║
║ ┌─────────────────────────────────────────────────────────────────┐ ║
║ │ 主控循环 (Agent Loop) │ ║
║ │ │ ║
║ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │ ║
║ │ │ ① 感知层 │──►│ ② 决策层 │──►│ ③ 执行层 │──►│ ④ 反馈层 │ │ ║
║ │ │ Screen │ │ Brain │ │ Hands │ │ Memory │ │ ║
║ │ └──────────┘ └──────────┘ └──────────┘ └────┬────┘ │ ║
║ │ ▲ │ │ ║
║ │ └───────────────────────────────────────────────┘ │ ║
║ └─────────────────────────────────────────────────────────────────┘ ║
║ │ ║
║ ┌─────────┴─────────┐ ║
║ │ ⑤ MBE 闭环层 │ ║
║ │ (按需调用) │ ║
║ │ │ ║
║ │ · 策略专家知识库 │ ║
║ │ · Eval 表现评估 │ ║
║ │ · HOPE 策略优化 │ ║
║ │ · 训练数据积累 │ ║
║ └───────────────────┘ ║
║ ║
║ ┌───────────────────────────────────────────────────────────────┐ ║
║ │ ⑥ TAS 仿真环境 │ ║
║ │ BizHawk / RetroArch / mGBA / FCEUX / DeSmuME ... │ ║
║ │ 存档管理 · 帧步进 · RAM 读取 · 输入注入 · 加速 │ ║
║ └───────────────────────────────────────────────────────────────┘ ║
╚═══════════════════════════════════════════════════════════════════════╝
3. 六大模块详细设计
3.1 ① 感知层 (Perception) — "AI 的眼睛"
双通道感知:视觉 + 内存
┌─────────────────────────────────────────┐
│ 感知层 │
│ │
│ 通道 A: LLM Vision (通用, 跨游戏) │
│ ┌──────────────────────────────────┐ │
│ │ 截屏 → 缩放/裁剪 → Base64 │ │
│ │ → 发送给 LLM Vision API │ │
│ │ → 返回结构化场景描述 │ │
│ └──────────────────────────────────┘ │
│ │
│ 通道 B: RAM 直读 (精确, TAS 专用) │
│ ┌──────────────────────────────────┐ │
│ │ 读取模拟器内存地址 │ │
│ │ → 解析: HP, 坐标, 分数, 状态 │ │
│ │ → 返回结构化数值数据 │ │
│ └──────────────────────────────────┘ │
│ │
│ 融合输出: GameState 对象 │
│ {scene, entities, player, metrics...} │
└─────────────────────────────────────────┘
通道 A — LLM Vision 感知(核心):
class VisionPerception:
"""用 LLM 视觉能力理解游戏画面"""
def __init__(self, llm_client, game_profile):
self.llm = llm_client # GPT-4o / Claude 3.5 / Gemini
self.game = game_profile # 游戏特定的提示词模板
self.frame_buffer = [] # 最近 N 帧缓存 (用于理解动态)
async def perceive(self, screenshot: bytes) -> GameState:
"""截屏 → 结构化游戏状态"""
prompt = f"""你是 {self.game.name} 的游戏分析专家。
分析这张游戏截图,返回 JSON:
{{
"scene_type": "战斗/探索/菜单/过场/死亡",
"player": {{
"position": "屏幕位置描述",
"health": "血量状态",
"state": "站立/跑动/跳跃/攻击/受伤"
}},
"enemies": [
{{"type": "类型", "position": "位置", "threat_level": "高/中/低"}}
],
"environment": {{
"obstacles": ["描述"],
"items": ["描述"],
"paths": ["可行进方向"]
}},
"ui_info": {{
"score": null,
"lives": null,
"level": null,
"special_meter": null
}},
"situation_summary": "一句话总结当前局面",
"urgency": "高/中/低"
}}"""
response = await self.llm.vision_query(
image=screenshot,
prompt=prompt,
max_tokens=500
)
return GameState.from_dict(json.loads(response))
通道 B — RAM 直读(TAS 专用,精确补充):
class RAMPerception:
"""直接读取模拟器 RAM 获取精确数值"""
# 每个游戏需要一份内存地址映射表
# 例: 超级马里奥兄弟 (NES)
MEMORY_MAP = {
"super_mario_bros": {
"player_x": 0x0086, # 玩家 X 坐标
"player_y": 0x00CE, # 玩家 Y 坐标
"player_state": 0x000E, # 状态 (0=小, 1=大, 2=火球)
"lives": 0x075A, # 生命数
"coins": 0x075E, # 金币数
"world": 0x075F, # 当前世界
"level": 0x0760, # 当前关卡
"score": 0x07DD, # 分数 (BCD)
"time": 0x07F8, # 剩余时间
"enemy_drawn": 0x000F, # 敌人是否在画面上
}
}
def __init__(self, emulator_api, game_id):
self.emu = emulator_api
self.mem_map = self.MEMORY_MAP.get(game_id, {})
def read_state(self) -> dict:
"""读取所有已映射的 RAM 值"""
state = {}
for name, addr in self.mem_map.items():
state[name] = self.emu.read_memory(addr)
return state
双通道融合:
class FusedPerception:
"""融合 LLM Vision + RAM 数据"""
async def perceive(self, screenshot, emulator) -> GameState:
# 两路并行
vision_task = self.vision.perceive(screenshot)
ram_state = self.ram.read_state()
game_state = await vision_task
# RAM 数据修正/补充 LLM 的理解
# LLM 可能估错血量, RAM 给出精确值
if "lives" in ram_state:
game_state.player.lives = ram_state["lives"]
if "player_x" in ram_state:
game_state.player.exact_position = (
ram_state["player_x"], ram_state["player_y"]
)
if "score" in ram_state:
game_state.ui_info.score = ram_state["score"]
return game_state
3.2 ② 决策层 (Brain) — "AI 的大脑"
LLM 推理决策,支持多级思考:
┌───────────────────────────────────────────────────────┐
│ 决策层 │
│ │
│ 输入: │
│ ├── 当前 GameState (来自感知层) │
│ ├── 最近 N 步行动历史 + 结果 │
│ ├── 游戏攻略知识 (来自 MBE 知识库, 按需) │
│ └── 当前目标/子目标 │
│ │
│ 思考模式: │
│ ┌──────────────────────────────────────────────┐ │
│ │ Level 1: 反射式 (简单场景, 低延迟) │ │
│ │ "前面有坑 → 跳" │ │
│ │ "敌人接近 → 攻击" │ │
│ │ 延迟: ~200ms, 适用: 80% 场景 │ │
│ ├──────────────────────────────────────────────┤ │
│ │ Level 2: 策略式 (复杂场景, 中延迟) │ │
│ │ "Boss 战第二阶段 → 分析攻击模式 → 选择策略" │ │
│ │ 延迟: ~1-2s, 适用: 15% 场景 │ │
│ ├──────────────────────────────────────────────┤ │
│ │ Level 3: 规划式 (卡关/迷宫, 高延迟+存档) │ │
│ │ "迷宫地图推理 → 路径规划 → 分步执行" │ │
│ │ 延迟: ~5-10s, 适用: 5% 场景 │ │
│ │ → 此时 TAS 暂停帧步进, 让 AI 慢慢想 │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ 输出: ActionPlan │
│ {actions: [...], confidence, reasoning} │
└───────────────────────────────────────────────────────┘
class GameBrain:
"""LLM 驱动的游戏决策引擎"""
def __init__(self, llm_client, game_profile, mbe_client=None):
self.llm = llm_client
self.game = game_profile
self.mbe = mbe_client # 可选, 调用 MBE 获取攻略知识
self.action_history = deque(maxlen=20)
self.current_goal = None
self.sub_goals = []
async def decide(self, state: GameState) -> ActionPlan:
"""根据当前状态决定下一步行动"""
# 1. 判断思考级别
think_level = self._assess_complexity(state)
# 2. 如果需要攻略知识, 调用 MBE
knowledge = ""
if think_level >= 2 and self.mbe:
knowledge = await self._query_mbe_strategy(state)
# 3. 构建决策 Prompt
prompt = self._build_decision_prompt(state, knowledge, think_level)
# 4. LLM 推理
response = await self.llm.query(
prompt=prompt,
temperature=0.3 if think_level == 1 else 0.7,
max_tokens=300 if think_level == 1 else 800
)
# 5. 解析行动计划
plan = ActionPlan.parse(response)
self.action_history.append((state.summary, plan))
return plan
def _build_decision_prompt(self, state, knowledge, level):
return f"""你是 {self.game.name} 的游戏高手 AI。
## 当前状态
{state.to_prompt()}
## 最近行动历史
{self._format_history()}
## 当前目标
{self.current_goal or "通关当前关卡"}
{f"## 攻略参考 (来自 MBE 知识库)" + chr(10) + knowledge if knowledge else ""}
## 可用操作
{self.game.action_space_description}
## 请决策
返回 JSON:
{{
"reasoning": "简短思考过程 (1-2句话)",
"actions": [
{{"button": "按键名", "duration_frames": 帧数, "delay_frames": 延迟帧数}}
],
"confidence": 0.0-1.0,
"need_savestate": true/false // 是否需要先存档 (不确定时存档)
}}"""
async def _query_mbe_strategy(self, state: GameState) -> str:
"""调用 MBE 查询攻略知识"""
query = f"{self.game.name} {state.situation_summary}"
# 调用 MBE 专家系统 API
response = await self.mbe.query_expert(
expert_id=f"game_strategy_{self.game.id}",
question=query
)
return response.answer if response else ""
3.3 ③ 执行层 (Hands) — "AI 的手"
class ActionExecutor:
"""操作执行器 — 支持 TAS 模拟器和真实键鼠两种模式"""
def __init__(self, mode="tas", emulator=None):
self.mode = mode
if mode == "tas":
self.backend = TASExecutor(emulator)
elif mode == "realtime":
self.backend = RealtimeExecutor()
async def execute(self, plan: ActionPlan):
"""执行行动计划"""
# 存档 (如果 AI 觉得不确定)
if plan.need_savestate:
await self.backend.save_state()
for action in plan.actions:
await self.backend.press(
button=action.button,
duration=action.duration_frames,
delay=action.delay_frames
)
# 等待动作生效
await self.backend.advance_frames(plan.total_frames)
class TASExecutor:
"""TAS 模拟器执行器"""
def __init__(self, emulator):
self.emu = emulator # BizHawk / RetroArch API
async def press(self, button, duration_frames, delay_frames):
"""精确到帧的按键输入"""
if delay_frames > 0:
await self.emu.advance_frames(delay_frames)
# 按下
self.emu.set_input(button, True)
await self.emu.advance_frames(duration_frames)
# 松开
self.emu.set_input(button, False)
async def save_state(self):
self.emu.save_state(slot="auto")
async def load_state(self, slot="auto"):
self.emu.load_state(slot=slot)
async def advance_frames(self, n):
"""步进 N 帧"""
self.emu.frame_advance(n)
async def get_screenshot(self) -> bytes:
"""获取当前帧截图"""
return self.emu.screenshot()
class RealtimeExecutor:
"""真实键鼠执行器 (实操模式)"""
def __init__(self):
import pydirectinput
self.input = pydirectinput
async def press(self, button, duration_frames, delay_frames):
"""模拟键盘按键 (帧转换为时间, 假设 60fps)"""
frame_time = 1.0 / 60
if delay_frames > 0:
await asyncio.sleep(delay_frames * frame_time)
self.input.keyDown(button)
await asyncio.sleep(duration_frames * frame_time)
self.input.keyUp(button)
3.4 ④ 反馈层 (Memory) — "AI 的记忆"
class GameMemory:
"""游戏记忆 — 记录经验, 识别模式, 避免重复错误"""
def __init__(self):
self.episode_log = [] # 当前回合的完整记录
self.death_patterns = [] # 死亡模式库
self.success_patterns = [] # 成功模式库
self.area_knowledge = {} # 区域知识图谱
self.boss_patterns = {} # Boss 行为模式
self.stuck_detector = StuckDetector()
def record_step(self, state, action, result_state):
"""记录每一步"""
step = StepRecord(
state=state,
action=action,
result=result_state,
reward=self._compute_reward(state, result_state),
timestamp=time.time()
)
self.episode_log.append(step)
# 检测卡关
if self.stuck_detector.is_stuck(self.episode_log):
return StepFeedback(stuck=True, suggestion="尝试不同策略")
return StepFeedback(stuck=False)
def on_death(self, state, action):
"""死亡回调 — 记录死亡模式"""
pattern = DeathPattern(
scene=state.scene_type,
situation=state.situation_summary,
last_actions=[s.action for s in self.episode_log[-5:]],
cause=state.death_cause
)
self.death_patterns.append(pattern)
def on_checkpoint(self, state):
"""通关/到达检查点 — 记录成功模式"""
# 提取从上个检查点到这里的操作序列
success_trace = self.episode_log[self.last_checkpoint_idx:]
self.success_patterns.append(SuccessPattern(
area=state.current_area,
actions_trace=success_trace,
total_steps=len(success_trace)
))
def get_context_for_decision(self, state) -> str:
"""为决策层提供记忆上下文"""
context_parts = []
# 1. 这个区域之前死过吗?
relevant_deaths = [d for d in self.death_patterns
if d.scene == state.scene_type]
if relevant_deaths:
context_parts.append(
f"⚠️ 注意: 在类似场景死过 {len(relevant_deaths)} 次, "
f"死因: {relevant_deaths[-1].cause}"
)
# 2. 这个区域有成功经验吗?
relevant_success = self.area_knowledge.get(state.current_area)
if relevant_success:
context_parts.append(
f"✅ 经验: 上次通过此区域用了 {relevant_success.strategy}"
)
# 3. Boss 模式识别
if state.scene_type == "boss_fight":
boss_id = state.boss_identifier
if boss_id in self.boss_patterns:
context_parts.append(
f"🎯 Boss 攻击模式: {self.boss_patterns[boss_id]}"
)
return "\n".join(context_parts)
def _compute_reward(self, before, after) -> float:
"""计算奖励信号"""
reward = 0.0
# 进度推进 (+)
if after.progress > before.progress:
reward += (after.progress - before.progress) * 10
# 分数增加 (+)
if after.score > before.score:
reward += (after.score - before.score) * 0.01
# 生命减少 (-)
if after.lives < before.lives:
reward -= 5.0
# 血量减少 (-)
if after.player.health < before.player.health:
reward -= 2.0
# 死亡 (--)
if after.is_dead:
reward -= 20.0
# 通关 (++)
if after.level_completed:
reward += 100.0
return reward
3.5 ⑤ MBE 闭环层 — "需要时调用"
不是把 MBE 嵌进来,而是在关键节点调用 MBE 的服务:
┌──────────────────────────────────────────────────────────┐
│ MBE 调用点 │
│ │
│ ① 策略知识查询 │
│ ├── 卡关时 → 查询 MBE 攻略专家知识库 │
│ ├── Boss 战 → 查询 Boss 弱点/攻略 │
│ └── 新游戏 → 加载 MBE 中该游戏的策略知识 │
│ │
│ ② 表现评估 (Eval) │
│ ├── 每关结束 → 调用 MBE Eval 评估通关质量 │
│ │ (用时、死亡次数、得分、操作精度) │
│ ├── 对比历史 → 调用 MBE 趋势分析看是否进步 │
│ └── 门禁检查 → 策略更新后必须通过 Eval 才能"上线" │
│ │
│ ③ 策略进化 (HOPE + Training) │
│ ├── 收集死亡模式 → 存入 MBE 回归用例库 │
│ ├── 成功策略 → 存入 MBE 训练数据 │
│ ├── HOPE 学习 → 哪些策略对哪些场景更有效 │
│ └── Prompt 优化 → 基于 HOPE 反馈微调决策 Prompt │
│ │
│ ④ A/B 测试 │
│ ├── 新策略 Prompt vs 旧策略 Prompt │
│ ├── 不同 LLM (GPT-4o vs Claude) 对比 │
│ └── 不同感知频率/精度对比 │
│ │
│ ⑤ 监控告警 │
│ ├── 胜率突然下降 → 告警 │
│ ├── 同一位置反复死亡 > 10 次 → 告警 + 自动切换策略 │
│ └── LLM API 延迟飙升 → 降级到纯 RAM 感知模式 │
└──────────────────────────────────────────────────────────┘
class MBEIntegration:
"""MBE 按需调用集成"""
def __init__(self, mbe_base_url="http://localhost:8000"):
self.base_url = mbe_base_url
# ---- 策略知识查询 ----
async def query_strategy(self, game_id, situation) -> str:
"""卡关时查询 MBE 攻略知识库"""
resp = await httpx.post(f"{self.base_url}/api/consult", json={
"question": f"游戏 {game_id} 中遇到 {situation},应该怎么做?",
"expert_id": f"game_{game_id}",
})
return resp.json().get("answer", "")
# ---- 表现评估 ----
async def evaluate_run(self, game_id, run_stats: dict):
"""评估一次通关/闯关的表现"""
resp = await httpx.post(
f"{self.base_url}/api/evaluation/expert",
json={
"expert_id": f"game_agent_{game_id}",
"eval_type": "game_run",
"report_json": run_stats,
"overall_score": run_stats.get("score", 0),
}
)
return resp.json()
# ---- 回归用例 (死亡模式) ----
async def report_failure(self, game_id, death_pattern: dict):
"""上报死亡模式到 MBE 回归用例库"""
await httpx.post(
f"{self.base_url}/api/evaluation/regression/"
f"game_agent_{game_id}/report",
json=death_pattern
)
# ---- A/B 测试 ----
async def create_strategy_test(self, game_id, v1_prompt, v2_prompt):
"""创建策略 A/B 测试"""
await httpx.post(
f"{self.base_url}/api/evaluation/ab-test/create",
params={
"expert_id": f"game_agent_{game_id}",
"control_version": "v1",
"treatment_version": "v2",
}
)
3.6 ⑥ TAS 仿真环境层
支持的模拟器和游戏平台:
| 模拟器 | 平台 | 脚本接口 | 推荐程度 |
|---|---|---|---|
| BizHawk | NES/SNES/GBA/N64/Genesis... | Lua API | ⭐⭐⭐ 首选 |
| RetroArch | 多平台 | Python (libretro) | ⭐⭐⭐ |
| FCEUX | NES | Lua API | ⭐⭐ |
| mGBA | GBA | Lua/Python | ⭐⭐ |
| DeSmuME | NDS | Lua | ⭐⭐ |
| Dolphin | GameCube/Wii | Python (scripting) | ⭐ |
BizHawk 接口封装:
class BizHawkBridge:
"""BizHawk 模拟器桥接 (通过 Socket 通信)"""
def __init__(self, host="localhost", port=9999):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((host, port))
def read_memory(self, addr: int, size: int = 1) -> int:
"""读取 RAM 内存"""
self._send(f"READ {addr} {size}")
return int(self._recv())
def write_memory(self, addr: int, value: int):
"""写入 RAM (调试用)"""
self._send(f"WRITE {addr} {value}")
def set_input(self, button: str, pressed: bool):
"""设置按键状态"""
self._send(f"INPUT {button} {'1' if pressed else '0'}")
def frame_advance(self, n: int = 1):
"""步进 N 帧"""
self._send(f"ADVANCE {n}")
def save_state(self, slot: str = "auto"):
"""存档"""
self._send(f"SAVESTATE {slot}")
def load_state(self, slot: str = "auto"):
"""读档"""
self._send(f"LOADSTATE {slot}")
def screenshot(self) -> bytes:
"""截屏 (返回 PNG bytes)"""
self._send("SCREENSHOT")
size = int(self._recv())
return self._recv_bytes(size)
def get_frame_count(self) -> int:
"""当前帧数"""
self._send("FRAMECOUNT")
return int(self._recv())
def set_speed(self, multiplier: float):
"""设置模拟速度 (1.0=正常, 0=最快)"""
self._send(f"SPEED {multiplier}")
4. 主控循环
class GameAIAgent:
"""游戏 AI 主控循环"""
def __init__(self, config: GameConfig):
# 核心组件
self.perception = FusedPerception(config)
self.brain = GameBrain(config)
self.executor = ActionExecutor(config)
self.memory = GameMemory()
# MBE 集成 (可选)
self.mbe = MBEIntegration(config.mbe_url) if config.use_mbe else None
# 运行状态
self.running = False
self.total_deaths = 0
self.current_level = None
self.run_stats = RunStats()
async def run(self, max_episodes=100):
"""主循环"""
self.running = True
for episode in range(max_episodes):
logger.info(f"=== Episode {episode + 1} ===")
await self._run_episode()
# 每个 episode 结束后, 评估表现
if self.mbe:
await self.mbe.evaluate_run(
self.game.id, self.run_stats.to_dict()
)
# 重置
self.run_stats.reset()
async def _run_episode(self):
"""单个回合"""
step = 0
while self.running:
# ① 感知
screenshot = await self.executor.backend.get_screenshot()
state = await self.perception.perceive(screenshot)
# 检测是否通关
if state.level_completed:
logger.info(f"🎉 通关! Level {state.current_level}")
self.memory.on_checkpoint(state)
break
# 检测是否死亡
if state.is_dead:
self.total_deaths += 1
logger.info(f"💀 死亡 #{self.total_deaths}: {state.death_cause}")
self.memory.on_death(state, last_action)
# 上报 MBE 回归库
if self.mbe:
await self.mbe.report_failure(self.game.id, {
"scene": state.scene_type,
"cause": state.death_cause,
"step": step
})
# 读档
await self.executor.backend.load_state()
continue
# ② 获取记忆上下文
memory_context = self.memory.get_context_for_decision(state)
self.brain.inject_context(memory_context)
# ③ 决策
plan = await self.brain.decide(state)
last_action = plan
# 低置信度时查询 MBE 攻略
if plan.confidence < 0.3 and self.mbe:
strategy = await self.mbe.query_strategy(
self.game.id, state.situation_summary
)
if strategy:
plan = await self.brain.decide_with_hint(state, strategy)
# ④ 执行
await self.executor.execute(plan)
# ⑤ 记录
new_screenshot = await self.executor.backend.get_screenshot()
new_state = await self.perception.perceive(new_screenshot)
feedback = self.memory.record_step(state, plan, new_state)
# 卡关检测
if feedback.stuck:
logger.warning("🔄 检测到卡关, 尝试新策略")
self.brain.switch_strategy()
if self.mbe:
# 查 MBE 有没有别人的攻略
hint = await self.mbe.query_strategy(
self.game.id, f"卡在 {state.situation_summary}"
)
self.brain.inject_hint(hint)
step += 1
self.run_stats.update(state, plan, new_state)
# 控制感知频率 (不需要每帧都调 LLM)
# 简单场景: 每 10 帧感知一次
# 复杂场景: 每 3 帧
# Boss 战: 每帧
skip = self._get_perception_interval(state)
if skip > 0:
await self.executor.backend.advance_frames(skip)
def _get_perception_interval(self, state: GameState) -> int:
"""根据场景复杂度调整感知频率"""
if state.urgency == "高":
return 0 # 每帧
elif state.urgency == "中":
return 3 # 每 3 帧
else:
return 10 # 每 10 帧
5. 游戏配置文件 (Game Profile)
每个游戏一个配置文件,零代码切换游戏:
# profiles/super_mario_bros.yaml
game:
id: super_mario_bros
name: "超级马里奥兄弟"
platform: nes
rom: "roms/Super Mario Bros (JU).nes"
emulator:
type: bizhawk
core: NesHawk
# 按键映射
action_space:
- { name: "right", button: "Right", description: "向右走" }
- { name: "left", button: "Left", description: "向左走" }
- { name: "jump", button: "A", description: "跳跃" }
- { name: "run", button: "B", description: "加速跑" }
- { name: "down", button: "Down", description: "蹲下/进水管" }
- { name: "up", button: "Up", description: "向上看" }
- { name: "run_jump", button: "A+B+Right", description: "加速跳" }
# RAM 内存地址映射
memory_map:
player_x: 0x0086
player_y: 0x00CE
player_state: 0x000E
lives: 0x075A
coins: 0x075E
world: 0x075F
level: 0x0760
score: 0x07DD
time: 0x07F8
# 游戏特定知识 (注入 Prompt)
game_knowledge: |
- 马里奥可以通过跳上敌人头顶消灭它们 (乌龟、板栗仔)
- 问号砖块可以撞出金币或蘑菇
- 大蘑菇让马里奥变大, 火花让马里奥能发射火球
- 星星让马里奥无敌一段时间
- 关底旗杆: 跳得越高分数越高
- 1-2、4-2 有隐藏水管可以跳关
# 奖励设计
rewards:
progress_forward: 1.0 # 每向右推进 1 像素
coin_collected: 5.0 # 每个金币
enemy_killed: 10.0 # 每消灭一个敌人
powerup_collected: 20.0 # 获得道具
level_complete: 100.0 # 通关
death: -50.0 # 死亡
time_penalty: -0.1 # 每帧时间惩罚 (鼓励速通)
# 感知配置
perception:
vision_interval_easy: 10 # 简单场景每 10 帧看一次
vision_interval_hard: 2 # 困难场景每 2 帧看一次
ram_interval: 1 # RAM 每帧都读
# MBE 集成
mbe:
enabled: true
expert_id: game_strategy_mario
knowledge_base: "mario_walkthrough"
eval_metrics:
- total_score
- deaths
- completion_time
- levels_cleared
6. 训练流程
╔═══════════════════════════════════════════════════════════════╗
║ 训练流程 (渐进式) ║
║ ║
║ Stage 1: 零样本探索 (Zero-shot) ║
║ ┌──────────────────────────────────────────┐ ║
║ │ · 只用 LLM Vision + 基础游戏知识 │ ║
║ │ · TAS 帧步进模式, 不限思考时间 │ ║
║ │ · 收集: 死亡模式、卡关位置、成功片段 │ ║
║ │ · 产出: 初始经验库 │ ║
║ └──────────────────────────────────────────┘ ║
║ ↓ ║
║ Stage 2: 经验回放 (Experience Replay) ║
║ ┌──────────────────────────────────────────┐ ║
║ │ · 分析 Stage 1 的死亡/卡关模式 │ ║
║ │ · 注入攻略知识到 MBE 知识库 │ ║
║ │ · 优化决策 Prompt (加入经验规则) │ ║
║ │ · 读档重跑困难段落, 尝试不同策略 │ ║
║ │ · 产出: 优化后的决策 Prompt │ ║
║ └──────────────────────────────────────────┘ ║
║ ↓ ║
║ Stage 3: 策略 A/B 测试 ║
║ ┌──────────────────────────────────────────┐ ║
║ │ · 多个策略变体同时跑同一关 │ ║
║ │ · MBE A/B 测试框架对比表现 │ ║
║ │ · 自动选择最优策略 │ ║
║ │ · 产出: 最优策略 Prompt + 配置 │ ║
║ └──────────────────────────────────────────┘ ║
║ ↓ ║
║ Stage 4: 速度优化 (Speed Run) ║
║ ┌──────────────────────────────────────────┐ ║
║ │ · 能通关后, 优化通关速度 │ ║
║ │ · 减少感知频率, 提高操作精度 │ ║
║ │ · 记录最优操作序列 (TAS input log) │ ║
║ │ · 产出: 可回放的 TAS 通关录像 │ ║
║ └──────────────────────────────────────────┘ ║
║ ↓ ║
║ Stage 5: 实时挑战 (Real-time) ║
║ ┌──────────────────────────────────────────┐ ║
║ │ · 关闭帧步进, 正常速度运行 │ ║
║ │ · 用积累的经验减少 LLM 调用频率 │ ║
║ │ · 简单场景用缓存策略, 复杂场景调 LLM │ ║
║ │ · 产出: 真正的"实时打游戏" AI │ ║
║ └──────────────────────────────────────────┘ ║
╚═══════════════════════════════════════════════════════════════╝
7. 目录结构
game-ai/
├── README.md
├── requirements.txt
├── config.yaml # 全局配置
│
├── profiles/ # 游戏配置
│ ├── super_mario_bros.yaml
│ ├── zelda.yaml
│ ├── megaman.yaml
│ └── ...
│
├── src/
│ ├── __init__.py
│ ├── agent.py # GameAIAgent 主控循环
│ │
│ ├── perception/
│ │ ├── __init__.py
│ │ ├── vision.py # LLM Vision 感知
│ │ ├── ram.py # RAM 直读
│ │ └── fusion.py # 双通道融合
│ │
│ ├── brain/
│ │ ├── __init__.py
│ │ ├── decision.py # LLM 决策引擎
│ │ ├── goals.py # 目标管理
│ │ └── prompts/ # 决策 Prompt 模板
│ │ ├── base.jinja2
│ │ ├── combat.jinja2
│ │ └── exploration.jinja2
│ │
│ ├── execution/
│ │ ├── __init__.py
│ │ ├── executor.py # 操作执行器
│ │ ├── tas.py # TAS 模拟器后端
│ │ └── realtime.py # 真实键鼠后端
│ │
│ ├── memory/
│ │ ├── __init__.py
│ │ ├── game_memory.py # 游戏记忆
│ │ ├── death_patterns.py # 死亡模式库
│ │ └── stuck_detector.py # 卡关检测
│ │
│ ├── emulators/
│ │ ├── __init__.py
│ │ ├── bizhawk.py # BizHawk 桥接
│ │ ├── retroarch.py # RetroArch 桥接
│ │ └── base.py # 模拟器基类
│ │
│ ├── mbe/
│ │ ├── __init__.py
│ │ └── integration.py # MBE 调用封装
│ │
│ └── utils/
│ ├── __init__.py
│ ├── screenshot.py # 截屏工具
│ └── models.py # 数据模型
│
├── scripts/
│ ├── play.py # 启动游戏 AI
│ ├── train.py # 训练模式
│ ├── replay.py # 回放 TAS 录像
│ └── setup_bizhawk.py # 模拟器配置向导
│
├── roms/ # (gitignore) ROM 文件
├── savestates/ # 存档
├── recordings/ # 操作录像
└── logs/ # 运行日志
8. 技术选型
| 组件 | 选型 | 原因 |
|---|---|---|
| LLM Vision | GPT-4o / Claude 3.5 Sonnet | 图像理解能力最强; 支持 JSON 输出 |
| 快速 LLM | GPT-4o-mini / Claude Haiku | Level 1 反射决策用, 低延迟低成本 |
| TAS 模拟器 | BizHawk 2.9+ | 最全平台支持; Lua API 成熟; TAS 社区首选 |
| 模拟器通信 | Socket / HTTP | BizHawk Lua ↔ Python Socket 桥接 |
| 键鼠模拟 | pydirectinput | Windows 下最可靠; 支持 DirectInput 游戏 |
| 截屏 | mss | 比 PIL 快 10x; 支持多显示器 |
| 异步框架 | asyncio + httpx | 感知/决策/执行流水线并行 |
| 数据存储 | SQLite (本地) | 轻量; 存死亡模式/操作记录/统计 |
| MBE 调用 | httpx → MBE REST API | 按需调用, 松耦合 |
成本估算 (以超级马里奥为例)
| 项目 | 估算 |
|---|---|
| 每帧 Vision 调用 (GPT-4o) | ~$0.005 |
| 每帧决策调用 (GPT-4o-mini) | ~$0.0005 |
| 每 10 帧感知一次 | 通关 1 关约 3000 帧 = 300 次调用 |
| 单关 Vision 成本 | ~$1.5 |
| 单关决策成本 | ~$0.15 |
| 训练通关全 8 关 | ~$50-100 (含反复重试) |
| 速通优化后 (缓存策略) | 成本降 80% |
9. 快速启动计划
| 阶段 | 时间 | 内容 | 产出 |
|---|---|---|---|
| MVP | 1 周 | BizHawk 桥接 + Vision 感知 + 简单决策 + 马里奥 1-1 | 能看到 AI 在马里奥 1-1 里移动和跳跃 |
| v0.2 | 2 周 | 记忆系统 + 死亡回溯 + 存档管理 | 能通过 1-1 |
| v0.3 | 3 周 | MBE 集成 + 攻略知识库 + A/B 测试 | 能通过 1-1 到 1-4 |
| v0.4 | 4 周 | 多策略 + 速通优化 + 实时模式 | 全关通关 + TAS 录像输出 |
| v1.0 | 6 周 | 多游戏支持 + 配置化 + 完整文档 | 换个游戏配置就能玩 |
10. 首选验证游戏
| 游戏 | 平台 | 难度 | 推荐理由 |
|---|---|---|---|
| 超级马里奥兄弟 | NES | ⭐⭐ | 经典; 状态简单; RAM 映射完善; TAS 社区资源丰富 |
| 洛克人 2 | NES | ⭐⭐⭐ | Boss 模式识别; 策略选择 (关卡顺序) |
| 塞尔达传说 | NES | ⭐⭐⭐ | 探索+战斗+解谜; 测试规划能力 |
| 口袋妖怪红 | GBA | ⭐⭐ | 回合制; LLM 有充分思考时间; 策略丰富 |
| 俄罗斯方块 | NES | ⭐ | 最简单的验证; 纯反应+规划 |
建议从"超级马里奥兄弟 1-1"开始 — 最经典、资源最多、复杂度适中。
附录: BizHawk Lua 端脚本
-- bizhawk_bridge.lua
-- 在 BizHawk 中运行, 与 Python 通过 Socket 通信
local socket = require("socket")
local server = socket.tcp()
server:bind("localhost", 9999)
server:listen(1)
server:settimeout(0) -- 非阻塞
local client = nil
while true do
-- 接受连接
if not client then
client = server:accept()
if client then
client:settimeout(0)
console.log("Python connected!")
end
end
-- 处理命令
if client then
local line, err = client:receive()
if line then
local parts = {}
for word in line:gmatch("%S+") do
table.insert(parts, word)
end
local cmd = parts[1]
if cmd == "READ" then
local addr = tonumber(parts[2])
local val = memory.read_u8(addr)
client:send(tostring(val) .. "\n")
elseif cmd == "INPUT" then
local button = parts[2]
local pressed = parts[3] == "1"
joypad.set({[button] = pressed}, 1)
client:send("OK\n")
elseif cmd == "ADVANCE" then
local frames = tonumber(parts[2]) or 1
for i = 1, frames do
emu.frameadvance()
end
client:send("OK\n")
elseif cmd == "SAVESTATE" then
local slot = tonumber(parts[2]) or 0
savestate.saveslot(slot)
client:send("OK\n")
elseif cmd == "LOADSTATE" then
local slot = tonumber(parts[2]) or 0
savestate.loadslot(slot)
client:send("OK\n")
elseif cmd == "SCREENSHOT" then
local path = "screenshot_temp.png"
client.screenshot(path)
local f = io.open(path, "rb")
local data = f:read("*a")
f:close()
client:send(tostring(#data) .. "\n")
client:send(data)
elseif cmd == "FRAMECOUNT" then
client:send(tostring(emu.framecount()) .. "\n")
elseif cmd == "SPEED" then
local speed = tonumber(parts[2]) or 100
client.setspeed(speed)
client:send("OK\n")
end
elseif err == "closed" then
client = nil
console.log("Python disconnected")
end
end
emu.frameadvance()
end