{“content”:”---\nname: pokemon-red-windows-automation\ndescription: Pokemon Red PyBoy automation on Windows PC via SSH\n---\n\n# Pokemon Red Windows 自动化 (PyBoy)\n\n## 背景\n在 Windows PC 上运行 PyBoy 模拟器,通过 HTTP API 控制 Pokemon Red。\n\n## 环境\n- PC: Windows, Python 3.11, PyBoy 2.7.0\n- ROM: Pokemon Red (MD5: 2b2a3d3d0e387b1e15ba6a8d4a47d0e5)\n- 远程控制: SSH from Termux (Hermes)\n\n## 已知问题\n\n### 问题 1: 服务器无法启动 ✅ 已解决\n- 症状:服务器进程立即退出,端口 8080 从未监听\n- 原因pokemon_live.pywindow=None(Python None),但 PyBoy 要求的不是 Python None,而是字符串 \"null\"\n- 错误信息KeyError: 'Unknown window type: None. Use \"SDL2\", \"OpenGL\", \"GLFW\", or \"null\"'\n- 解决:必须用 window=\"null\"(字符串),不是 window=None\n- 验证window=\"null\" 时截图正常(160×144 JPEG,约 1-2KB),frame>0 后有实际画面\n\n### 问题 2: 服务器频繁崩溃(已大幅改善)\n- 之前:8-10 次按键后就崩溃\n- 原因:PyBoy tick 和 button_press/release 操作没有完全原子化,且 /state /summary 端点也没有锁保护\n- 修复(2026-05-05):\n 1. 整个 button_press → tick → button_release 序列在锁内完成\n 2. button_release()finally 块中,确保异常时也释放\n 3. /state/summary 端点也加锁\n 4. tick() 使用 render=True 确保截图 buffer 正确\n 5. 每action最多 3 帧(原 10 帧),减少单次阻塞时间\n- 效果:30 次连续快速按键全部成功(30/30)\n- 剩余问题:约 50+ 次操作后 PyBoy 仍可能崩溃(疑似 C 扩展内部内存问题)\n\n### PyBoy 崩溃根因(C 扩展层问题)\n- 崩溃不是 Python 内存泄漏,而是 C 扩展(Cython + SDL2)内部状态累积\n- 每次 button_press → tick → button_release 都在 C 层分配 GPU buffer(sprite tiles、palette RAM)\n- 约 50 次操作后,C 层的某个 handle/resource 耗尽 → 整个进程崩溃\n- Python 层的 pyboy_lock 无法解决 C 层问题——锁只能保证 Python 线程安全\n- PyBoy import 在 SSH 会话中会卡住(SDL2 初始化在 headless 环境挂起),这是已知行为\n\n### PyBoy 崩溃时的现象\n- 服务器进程消失,端口变为 TIME_WAIT\n- curl 返回 “connection refused”\n- 无法通过代码恢复,需要重启 schtasks 任务\n\n### 注意:PyBoy import 会在 SSH 会话中挂起\n- 直接 ssh ... \"python -c 'from pyboy import PyBoy; ...'\" 会卡住(SDL2 在 headless 环境初始化挂起)\n- 用 cmd /c \"set PYTHONUNBUFFERED=1&&python ...\" 或通过 HTTP API 调用更可靠\n- 启动服务器用 schtasks,不要在 SSH 命令行直接运行\n\n### PyBoy window 类型参考\n| 值 | 行为 |\n|---|---|\n| \"null\" (字符串) | ✅ 正确!无头模式,可截图,屏幕 buffer 正常渲染 |\n| \"headless\" | 未知,可能等同于 \"null\" |\n| \"SDL2\" | 有窗口,需要 GUI 环境 |\n| \"OpenGL\" | OpenGL 窗口 |\n| \"GLFW\" | GLFW 窗口 |\n| \"dummy\" | 未知 |\n| None (Python) | ❌ 导致 KeyError,服务器无法启动 |\n\n## 工作目录\nPC: D:\\\n服务器: D:\\pokemon_live.py\nROM: D:\\Games\\Pokemon\\Pokemon - Red Version (USA, Europe) (SGB Enhanced).gb\nvenv Python: C:\\Users\\Sozo\\pokemon-agent\\venv\\Scripts\\python.exe\nschtasks 任务: PokemonLive\n\n## 长期稳定方案\n\n### 根因\n崩溃是 C 扩展(Cython/SDL2)内部状态累积,Python 层锁无法根治。\n\n### 方案对比\n\n| 方案 | 原理 | 难度 | 推荐度 |\n|------|------|------|--------|\n| Save State 检查点 | 每 N 次操作保存状态,崩溃后从检查点恢复 | ⭐ 简单 | ⭐⭐⭐⭐ |\n| Subprocess per Action | 每次操作开独立进程,操作完即销毁 | ⭐⭐ 中等 | ⭐⭐⭐⭐⭐ |\n| 定期重启 + 检查点 | 每 30 次操作重启 + 恢复检查点 | ⭐⭐ 简单 | ⭐⭐⭐⭐ |\n\n### PyBoy Save State API(已确认可用)\npython\n# save_state() → bytes(约 64KB)\nstate = pb.save_state()\nwith open('D:\\\\pokemon_checkpoint.sav', 'wb') as f:\n f.write(state)\n\n# load_state(bytes)\nwith open('D:\\\\pokemon_checkpoint.sav', 'rb') as f:\n pb.load_state(f.read())\n\n\n### 推荐:定期重启 + 检查点\n- 每 30 次操作自动保存检查点 + 重启 PyBoy 实例\n- /save_checkpoint + /load_checkpoint 端点\n- 崩溃后从检查点恢复,游戏进度得到保护\n\n### 更彻底:Subprocess per Action\n每次操作在独立进程执行 → 崩溃完全隔离;每次操作后保存检查点,下次从检查点恢复。\n\n## 验证状态\n- ✅ PyBoy 初始化成功(“Ready! Frame: 0”)\n- ✅ save_state()load_state() 方法存在\n- ✅ pokemon_live.py checkpoint 版本已上传到 PC\n- ⏳ 检查点端点功能待验证(服务器启动后 SSH 会话会卡住,需通过 curl 验证)\n\n## 验证步骤(服务器启动后)\nbash\n# 通过 schtasks 启动\nssh sozo@100.83.112.84 \"cmd /c schtasks /Run /I /TN PokemonLive\"\nsleep 10\n\n# 验证检查点端点\ncurl -s http://localhost:8080/checkpoint_info # 应该是 {\"exists\": false}\ncurl -s http://localhost:8080/save_checkpoint # 保存检查点\ncurl -s http://localhost:8080/checkpoint_info # 应该是 {\"exists\": true, ...}\n\n# 执行几次操作后验证 action_count\nfor i in $(seq 1 5); do\n curl -s -X POST -H \"Content-Type: application/json\" \\\n -d '{\"button\":\"a\",\"frames\":1}' http://localhost:8080/action\ndone\ncurl -s http://localhost:8080/summary # 应该有 actions=N\n\n\n## 常用命令\n```bash\n# 通过 schtasks 在用户会话中启动服务器\nssh sozo@100.83.112.84 “cmd /c \“schtasks /Run /I /TN PokemonLive\""\n\n# 检查服务器状态\ncurl -s http://localhost:8080/health\ncurl -s http://localhost:8080/summary\ncurl -s http://localhost:8080/state\n\n# 按 A 键(POST 方式)\ncurl -s -X POST -H “Content-Type: application/json” \\n -d ’{“button”:“a”,“frames”:5}’ \\n http://localhost:8080/action\n\n# 获取截图\ncurl -s -o ~/screenshot.jpg http://localhost:8080/frame\n\n# 杀掉服务器进程\nssh sozo@100.83.112.84 “powershell -Command Stop-Process -Id (Get-NetTCPConnection -LocalPort 8080).OwningProcess -Force”\n\n# 验证 PyBoy 初始化(快速测试)\nssh sozo@100.83.112.84 “cmd /c \“set PYTHONUNBUFFERED=1&&C:\Users\Sozo\pokemon-agent\venv\Scripts\python.exe -c \\\“from pyboy import PyBoy; pb = PyBoy(r’D:\Games\Pokemon\Pokemon - Red Version (USA, Europe) (SGB Enhanced).gb’, window=‘null’); print(‘OK frame=’, pb.frame_count)\\\”\""\n\n## 文件\n- pokemon_live.py: HTTP 服务器(port 8080),健壮版 + 检查点版本\n - 包含 pyboy_lock 保护所有 PyBoy 访问\n - button_release()finally 中确保释放\n - 压力测试:30/30 次按键成功\n - 新增检查点端点/save_checkpoint/load_checkpoint/checkpoint_info\n - 新增 action_count:追踪操作次数\n - 新增自动检查点:每 25 次操作自动保存到 D:\\pokemon_checkpoint.sav\n - pokemon_live_checkpoint.py(本地路径:/data/data/com.termux/files/home/pokemon_live_checkpoint.py)\n”}