之前做开发提效工具时,我对 coding-agent 的输出方式一直有个感觉:它在后台跑任务,但用户只能对着终端日志判断“它到底干了什么”。任务多了以后,进度是否正常、产出是否有效、瓶颈在哪里,全都不够直观。
有一个思路很有意思:把 coding-agent 的真实执行过程,抽象成一个桌面增量游戏(incremental game)的资源来源。它的产出就是游戏里的金币和经验,它完成文件编写、测试通过等事件,就是游戏里的成就与升级节点。这样开发过程和娱乐化监控被结合到了一起,既能让用户“看到” agent 在工作,也能用游戏化的方式反馈 agent 的执行状态。
这也就是本文想展开的项目主题:一个由 coding-agent 驱动的桌面空闲增量游戏。文章会从概念讲起,逐步拆解整体架构、环境搭建、核心代码实现、常见问题和工程化建议,最终给出一个可以直接本地运行的最小示例。
1. 背景与核心概念
1.1 什么是 idle desktop incremental game
idle game 在国内常被称为“放置类游戏”或“挂机游戏”。它的核心特点是:即使玩家不进行任何操作,游戏内的资源也会按照一定规则自动增长。比如常见的“每秒获得多少金币”,就是典型的 idle 机制。
incremental game 则更强调“增量”和“数值膨胀”。玩家通过一次一次的小操作,积累资源,再用资源购买加成,让效率呈指数级上升。很多挂机游戏其实同时具备 idle 和 incremental 两种属性,所以也常被统称为 idle incremental game。
当这类游戏运行在桌面端,就变成了一款桌面应用。它不需要浏览器,不需要复杂引擎,只要有一个能实时显示数值和状态的窗口,就能满足核心体验。由于游戏逻辑本身不复杂,这类项目也非常适合用来做技术练手,或者作为日常开发工具的辅助面板。
1.2 coding-agent 与游戏化的结合点
coding-agent 指的是能理解代码仓库、执行命令、编写文件、运行测试的智能编码代理。它通常通过终端输出日志,告诉我们它正在做什么,比如:
- 创建了某个文件
- 修改了某个函数
- 运行了测试
- 修复了一个编译错误
这些状态天然具备“游戏事件”的特征:事件发生、事件类型不同、频率不同、结果不同。如果把 agent 输出的一行行日志解析成结构化事件,再把事件映射成游戏的资源增长,就实现了“agent 驱动游戏”的效果。
举个例子:
[agent] file_written: src/api/user.py -> 金币 +20,经验 +25 [agent] test_passed: UserServiceTest -> 金币 +50,经验 +60玩家的“操作”不再是人去点击按钮,而是 coding-agent 在后台完成的真实任务。游戏变成了一个可视化的反馈面板。
1.3 为什么值得做成桌面应用
有人可能会问:web 页面也能展示这些信息,为什么一定要桌面应用?
桌面应用有几个实际优势:
- 可以和本地 coding-agent 直接通信,不需要中间服务器。
- 可以通过 stdin/stdout 或本地端口拿到 agent 的实时输出。
- 对于每天打开终端写代码的开发者来说,桌面上有一个常驻窗口更自然。
- 不依赖远程服务,数据安全性更高,agent 的日志不会传到外部平台。
当然,技术选型上我们也可以使用 Electron、Tauri,或者 Python 自带的 tkinter。本文为了降低环境成本,采用 Python + tkinter 实现桌面界面,重点讲清楚“agent -> 事件 -> 游戏状态 -> 渲染”这条链路。
2. 系统整体架构
2.1 数据流设计
整个系统的核心数据流可以拆成四层:
coding-agent 输出 ↓ 事件采集与解析层 ↓ 游戏状态引擎 ↓ 桌面界面渲染coding-agent 在运行时会持续输出日志。采集层通过子进程管道读取这些日志,根据配置好的规则解析成结构化事件。解析完成后,不要把事件直接塞进 UI,而是放进一个队列。游戏引擎的主循环定时从队列里取事件,更新金币、经验、等级等状态。界面层只需要定期读取状态并刷新显示。
这个设计的关键点是“解耦”。agent 的日志产生速度快,UI 的刷新频率有限,如果直接同步处理,很容易造成界面卡顿。队列可以在两者之间做缓冲。
2.2 模块划分
从代码结构上看,我建议把项目拆成这几个模块:
agent_bridge:负责启动 coding-agent 子进程、读取 stdout/stderr、解析事件。event_queue:负责暂存解析后的事件,避免生产端和消费端互相影响。game_engine:负责维护金币、经验、等级等游戏状态,对外提供“处理事件”和“被动产出增长”两个方法。desktop_app:负责创建桌面窗口,定时刷新界面。config:负责配置 agent 启动命令、事件关键词、数值增量等。
这样拆分以后,如果以后想接入真实 coding-agent,只需要修改agent_bridge的解析逻辑;如果想换掉界面库,只需要重写desktop_app。
2.3 安全与权限边界
这里必须强调一个原则:agent 是游戏的事件来源,但游戏不能反向控制 agent 执行危险操作。
也就是说,游戏层面最多只能启动 agent、读取 agent 输出、按事件增加数值,不应该直接操作文件、执行 git push、删除目录。实际开发中如果需要展示 agent 的产出文件列表,也要遵循最小权限原则。
同时,agent 输出的日志内容不能被当成代码直接执行。不管你从日志里解析出什么,都只当作字符串处理。这一点在后面的代码实现中也会体现。
3. 环境准备与工程结构
3.1 运行环境说明
本文示例以 Python 3.10 及以上版本为例,重点演示思路和核心代码。版本需要根据你的实际环境调整。
需要确认以下环境:
- Python 3.10+
- tkinter 模块可用
- 一个可以产生模拟日志的脚本,或者你可以直接使用
fake_agent.py代替真实 coding-agent
在 Windows 上,Python 官方安装包一般会自带 tkinter;在 macOS 或 Linux 上,可能需要额外安装python3-tk。如果运行时报ModuleNotFoundError: No module named 'tkinter',说明当前 Python 环境没有 tkinter,需要先安装。
3.2 项目目录结构
为了便于理解,我们使用一个比较清晰的项目结构:
idle-agent-game/ ├── config.json # 全局配置 ├── fake_agent.py # 模拟 coding-agent 的脚本 ├── agent_bridge.py # agent 事件采集与解析 ├── game_state.py # 游戏状态引擎 ├── desktop_app.py # 桌面界面 └── main.py # 启动入口这个结构很轻量,不需要复杂的构建工具。如果你想扩展成更大的项目,可以把每个模块放进独立目录,但当前演示已经够了。
3.3 配置说明
配置文件采用 JSON 格式,主要配置三项内容:
agent_command:要启动的 coding-agent 命令。event_rules:日志行中哪些关键词对应哪些事件。increments:每个事件对应的金币和经验增量。
这种方式的好处是:数值平衡和事件映射不需要写在代码里,改配置就能调。
4. 核心实现
4.1 创建项目结构
先创建项目目录:
mkdir idle-agent-game cd idle-agent-game然后在目录下创建config.json、fake_agent.py、agent_bridge.py、game_state.py、desktop_app.py、main.py这几个文件。
下面我们逐一实现。
4.2 配置文件 config.json
{ "agent_command": ["python", "fake_agent.py"], "event_rules": [ { "marker": "[agent] task_started", "event": "task_started" }, { "marker": "[agent] file_written", "event": "file_written" }, { "marker": "[agent] test_passed", "event": "test_passed" } ], "increments": { "task_started": { "gold": 5, "exp": 10 }, "file_written": { "gold": 20, "exp": 25 }, "test_passed": { "gold": 50, "exp": 60 } }, "passive_income_per_level": 1, "level_exp_base": 100 }这里的marker是日志里的关键标记。真实 coding-agent 不会完全输出这种格式,所以实际使用时需要根据工具的真实输出调整。比如某款 agent 输出File created: src/xxx.py,那你就可以把 marker 配成File created:,事件类型写成file_written。
4.3 模拟 coding-agent 的 fake_agent.py
在没有真实 coding-agent 的情况下,我们可以先用一个模拟脚本测试完整链路。它做的事情很简单:每隔几秒输出一行带标记的日志。
# 文件路径:fake_agent.py import time print("[agent] task_started: 开始处理登录模块", flush=True) time.sleep(2) print("[agent] file_written: src/auth.py", flush=True) time.sleep(3) print("[agent] file_written: src/config.py", flush=True) time.sleep(2) print("[agent] test_passed: test_auth.py::test_login", flush=True) time.sleep(2) print("[agent] task_started: 开始处理订单模块", flush=True) time.sleep(2) print("[agent] file_written: src/order.py", flush=True) time.sleep(3) print("[agent] test_passed: test_order.py::test_create_order", flush=True)注意这里的flush=True很关键。Python 的 print 默认走缓冲区,如果不用flush=True,子进程读取时可能出现日志延迟,看起来就像“卡住”了。
如果你有真实的 coding-agent,可以把config.json里的agent_command改成真实命令,后续代码逻辑不变。
4.4 事件采集与解析 agent_bridge.py
agent_bridge.py负责启动子进程,并且通过两个线程分别读取 stdout 和 stderr。
# 文件路径:agent_bridge.py import queue import subprocess import threading class AgentBridge: """负责启动 coding-agent 子进程,并解析输出事件。""" def __init__(self, agent_command, event_queue, config): self.agent_command = agent_command self.event_queue = event_queue self.config = config self.process = None def start(self): self.process = subprocess.Popen( self.agent_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1, encoding="utf-8", ) threading.Thread(target=self._read_stdout, daemon=True).start() threading.Thread(target=self._read_stderr, daemon=True).start() def stop(self): if self.process and self.process.poll() is None: self.process.terminate() def _read_stdout(self): if not self.process or not self.process.stdout: return for line in self.process.stdout: line = line.strip() if line: self._parse_line(line) def _read_stderr(self): if not self.process or not self.process.stderr: return for line in self.process.stderr: line = line.strip() if line: self._parse_line(line) def _parse_line(self, line): rules = self.config.get("event_rules", []) for rule in rules: marker = rule.get("marker", "") if marker in line: event = { "type": rule.get("event", "unknown"), "raw": line, } self.event_queue.put(event) return这里有几个设计点:
- 使用
text=True和encoding="utf-8",避免 Windows 下编码问题。 - stdout 和 stderr 都读,防止子进程错误日志溢出导致管道阻塞。
- 解析采用“包含”匹配,而不是精确匹配,因为 agent 日志通常是一个长字符串。
- 解析完成后只把事件放入队列,不在采集线程里更新 UI,避免线程安全问题。
4.5 游戏状态引擎 game_state.py
game_state.py负责维护游戏数值。它不关心事件从哪来,只提供两个核心方法:处理事件、处理被动收益。
# 文件路径:game_state.py class GameState: """游戏状态引擎:维护金币、经验、等级与被动收益。""" def __init__(self, config): self.config = config self.gold = 0 self.exp = 0 self.level = 1 self.total_events = 0 self.passive_income_per_second = 0 def apply_event(self, event): """应用一个 agent 事件。""" event_type = event.get("type", "unknown") increments = self.config["increments"].get(event_type, {}) self.gold += increments.get("gold", 0) self.exp += increments.get("exp", 0) self.total_events += 1 if self._check_level_up(): self.passive_income_per_second += self.config.get( "passive_income_per_level", 1 ) def update_passive_income(self, dt): """被动收益:即使没有事件,也能随着时间的推移产生金币。""" self.gold += self.passive_income_per_second * dt def _check_level_up(self): base_exp = self.config.get("level_exp_base", 100) need = base_exp * self.level if self.exp >= need: self.exp -= need self.level += 1 return True return False这个引擎看起来很简单,但它代表了增量游戏最核心的循环:
- 外部事件进入,资源增加。
- 经验达到阈值,等级提升。
- 等级提升以后,每秒被动金币增加。
- 即使 agent 暂时没有新事件,游戏也会继续产生收益。
这样的设计让“挂机体验”成立,也让玩家有持续成长的感受。
4.6 桌面界面 desktop_app.py
桌面界面使用 tkinter 实现。为了避免阻塞主线程,这里用after定时刷新,并从队列里批量取事件。
# 文件路径:desktop_app.py import queue import time import tkinter as tk from tkinter import ttk class GameApp: def __init__(self, root, game_state, event_queue): self.root = root self.game_state = game_state self.event_queue = event_queue self.root.title("Idle Agent Game") self.root.geometry("480x300") self.gold_var = tk.StringVar(value="金币: 0") self.exp_var = tk.StringVar(value="经验: 0") self.level_var = tk.StringVar(value="等级: 1") self.income_var = tk.StringVar(value="每秒收益: 0") self.event_var = tk.StringVar(value="最近事件: -") self._build_ui() self._last_frame_time = time.time() self._tick() def _build_ui(self): main_frame = ttk.Frame(self.root, padding=20) main_frame.pack(fill="both", expand=True) ttk.Label(main_frame, text="coding-agent 驱动桌面增量游戏", font=("", 16)).pack() ttk.Label(main_frame, textvariable=self.level_var, font=("", 14)).pack(anchor="w", pady=5) ttk.Label(main_frame, textvariable=self.gold_var, font=("", 14)).pack(anchor="w", pady=5) ttk.Label(main_frame, textvariable=self.exp_var, font=("", 14)).pack(anchor="w", pady=5) ttk.Label(main_frame, textvariable=self.income_var, font=("", 14)).pack(anchor="w", pady=5) ttk.Label(main_frame, textvariable=self.event_var, font=("", 12), foreground="#555").pack(anchor="w", pady=10) self.progress = ttk.Progressbar(main_frame, length=400, maximum=100) self.progress.pack(anchor="w", pady=10) def _tick(self): now = time.time() dt = now - self._last_frame_time self._last_frame_time = now self._drain_events() self.game_state.update_passive_income(dt) self._refresh() self.root.after(200, self._tick) def _drain_events(self): try: while True: event = self.event_queue.get_nowait() self.game_state.apply_event(event) self.event_var.set("最近事件: " + event.get("raw", event.get("type", ""))[:60]) except queue.Empty: pass def _refresh(self): state = self.game_state self.gold_var.set(f"金币: {state.gold:,.1f}") self.exp_var.set(f"经验: {state.exp:,.1f}") self.level_var.set(f"等级: {state.level}") self.income_var.set(f"每秒收益: {state.passive_income_per_second:,.1f}") base_exp = self.config_level_exp() progress_value = 0 if base_exp > 0: progress_value = min(100, state.exp / base_exp * 100) self.progress["value"] = progress_value def config_level_exp(self): level = self.game_state.level return self.game_state.config.get("level_exp_base", 100) * level这段代码有几个细节值得注意:
- 在
_drain_events里使用get_nowait()循环取出队列中的所有事件,避免事件积压。 - 刷新频率控制在 200 毫秒一次,也就是每秒大约 5 次,对这类游戏完全足够。
- 界面上的进度条表示“距离下一级还需要多少经验”,让升级目标更直观。
4.7 启动入口 main.py
main.py负责把前面几个模块组装起来。
# 文件路径:main.py import json import queue import tkinter as tk from agent_bridge import AgentBridge from desktop_app import GameApp from game_state import GameState def main(): with open("config.json", "r", encoding="utf-8") as f: config = json.load(f) event_queue = queue.Queue() game_state = GameState(config) bridge = AgentBridge(config["agent_command"], event_queue, config) bridge.start() root = tk.Tk() app = GameApp(root, game_state, event_queue) root.mainloop() bridge.stop() if __name__ == "__main__": main()4.8 运行与验证
在项目目录下执行:
python main.py如果一切正常,会出现一个窗口,并且数值会随着fake_agent.py输出日志而增长。窗口里会依次看到类似这样的变化:
等级: 1 金币: 5 经验: 10 每秒收益: 0 等级: 2 金币: 100 经验: 15 每秒收益: 1当经验达到level_exp_base * level时,等级自动提升,被动收益增加。这就是一个完整的“事件驱动增量游戏”闭环。
5. 常见问题与排查思路
在实际运行中,可能会遇到下面这些问题。我把最高频的问题整理成表格,方便排查。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
窗口打不开,提示ModuleNotFoundError: No module named 'tkinter' | Python 环境缺少 tkinter | 安装系统 tk 库,或更换为完整版 Python |
| 运行后一直没有任何数值变化 | agent 命令没启动,或者日志里没有匹配到 marker | 检查agent_command是否有效,检查日志格式 |
| 日志有输出,但很久才更新一次 | 子进程输出被缓冲 | 在 agent 脚本中给 print 加flush=True |
| 窗口标题正常显示,但 CPU 占用过高 | after刷新频率过高,或队列无限堆积 | 把刷新间隔从 100ms 调大到 200ms 或 500ms,并检查队列大小 |
| Windows 下出现编码报错 | 子进程默认编码不是 UTF-8 | 在Popen中明确指定encoding="utf-8"或者"gbk" |
| 接入真实 coding-agent 后事件无法识别 | 真实输出格式和配置 marker 不一致 | 先打印原始日志,再根据实际内容调整配置 |
如果接入真实 coding-agent,建议先写一个简单脚本,把 agent 的 stdout 原始日志保存到文件里,然后“喂”给解析器测试。这样可以把格式适配问题隔离开,避免在真实任务中反复试错。
6. 工程化与最佳实践
6.1 日志与可观测性
虽然这个项目是一个游戏,但它本质上在消费编码代理的日志,所以自己的日志也不能省。
推荐在采集模块里增加日志记录功能,至少记录这几类信息:
- 启动 agent 的命令是什么。
- 每一行原始日志是什么。
- 解析出了哪个事件。
- 事件队列是否出现了积压。
有了日志,不仅能排查问题,还能通过历史记录回放“游戏过程”。如果以后想做数据分析,日志就是最原始的数据来源。
6.2 不要让游戏阻塞 agent
一个很容易被忽略的点:如果事件队列没有消费机制,agent 输出速度极快时,队列可能会无限增长,最终导致内存占用过高。
建议在实现里做一层保护:
- 给事件队列设置最大长度。
- 如果队列已满,可以丢弃低优先级事件,或者直接把原始日志落盘。
- 在 UI 刷新逻辑中及时消费事件,但不要一次消费全部时做耗时计算。
对于这个演示项目,queue.Queue已经足够。生产环境如果担心内存,可以换成有界队列,比如queue.Queue(maxsize=1000)。
6.3 界面渲染性能
tkinter 的性能虽然不高,但对增量游戏这种简单文本展示来说完全够用。只是要注意:
- 不要在 UI 线程中执行耗时的字符串解析。
- 不要频繁创建新的 Label,控件最好只创建一次,之后只更新文本。
- 刷新频率控制在 5 到 10 帧即可,不需要追求 60 帧。
如果以后想让界面更精美,可以换 Electron、Tauri 或 Qt,但核心架构保持不变:agent 采集层、事件队列、游戏引擎、UI 层,这四层依然有效。
6.4 安全边界与最小权限
接入真实 coding-agent 后,安全性会变得非常重要。
- 游戏程序只负责读取日志,不应该支持“在游戏里输入命令并执行”的功能。
- coding-agent 如果需要在工作目录里修改文件,应该用独立目录,避免影响到生产仓库。
- 涉及 git 操作、数据库变更的任务,必须在 agent 配置里提前做好权限限制,而不是在游戏层临时放开。
- 如果从日志中解析文件路径,不要直接用它去读取任何文件。日志内容只能当字符串展示。
简单说,游戏关注的是“事件发生”,而不应该成为 agent 操作的另一个入口。
6.5 用配置驱动平衡
增量游戏非常依赖数值平衡。今天你觉得file_written加 20 金币合理,玩两天可能就觉得太快了。
所以强烈建议把所有数值都放到配置文件中,包括事件类型、事件增量、升级经验公式、被动收益倍率。开发阶段可以快速修改配置并重启验证,不用改代码。这样后续做“随机事件”“加成道具”时,也能保持一致的配置管理方式。
7. 扩展方向与下一步学习
目前这个项目已经跑通了最小闭环,但它还只相当于“玩具版本”。你可以继续从以下几个方向扩展。
第一个方向是接入真实 coding-agent。比如通过 JSON 流或事件回调的方式,把 agent 的进度、耗时、token 消耗等指标传给游戏层。这样游戏就不只是“金币模拟器”,还能同时充当开发监控面板。
第二个方向是增加更多游戏机制。比如:
- 用金币购买“自动化效率加成”
- 随机出现“代码评审事件”,通过奖励或惩罚影响数值
- 记录每日完成次数,形成简单成就系统
- 增加多个“项目场景”,每个场景对应不同的 agent 任务
第三个方向是引入持久化。当前程序的数值在关闭窗口后就会丢失。你可以用 SQLite 保存进度,下次启动时恢复,这就是标准的挂机游戏存档功能。也可以把 agent 的运行记录保存下来,用于后续统计。
第四个方向是提升可视化。tkinter 的界面偏朴素,如果你想做更美观的桌面界面,可以换到 PySide6、Tauri 或 Electron。核心事件处理逻辑可以不变,只需要重写 UI 层。
如果你对增量游戏本身不太熟,也可以先看一些经典设计:资源产出公式、升级阈值曲线、离线收益计算。这些和 agent 事件结合起来以后,会让整个项目更有深度。
回到最初的问题:coding-agent 在后台工作时,我们如何感知它的进展?用一款桌面增量游戏去承载这个过程,或许不是最严肃的答案,却是最容易让人坚持看下去的方案。接下来动手改一改config.json里的数值,或者把fake_agent.py换成你每天都在用的 coding-agent,很快就能看到不同效果。