news 2026/8/31 1:18:28

用 coding-agent 驱动放置游戏:状态机与桌面应用实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 coding-agent 驱动放置游戏:状态机与桌面应用实现

在 Show HN 上出现了一个很有意思的题目:idle desktop incremental game driven by coding-agent。把“放置类增量游戏”和“coding-agent”放在一起,初看像是一个脑洞,细想却很合理:放置游戏的核心是挂机时资源自动增长,coding-agent 的工作方式恰好也是“无人值守持续产出”。给它一个任务清单,它在后台分析代码、修改文件、运行测试、反复调试,最终产出提交或 PR。下面按工程实现思路把这个创意拆成可落地方案:先梳理放置游戏与 coding-agent 的映射关系,再确定桌面端技术选型,实现一个最小可运行原型,讨论如何把真实 coding-agent 接进游戏循环,最后给出排错、数值平衡和安全建议。

这个原型适合三类读者:想用游戏化方式观察 AI 编程代理工作的人,想练习 Electron + React + 状态机架构的开发者,以及研究放置游戏数值系统和事件驱动设计的工程师。原始 Show HN 项目没有公开完整实现细节,所以这里的代码是一种可以自行复现的典型实现路径,不是对原项目的搬运。

1. 先想清楚:放置游戏和 coding-agent 为什么能放进同一个循环

动手写代码之前,最关键的一步不是选框架,而是确认两个领域在机制上真的能对齐。如果只是把“agent 在工作”的动画贴在界面上,玩法是空的;真正让游戏成立的是数值循环,这个循环必须由 coding-agent 的工作流驱动。

1.1 放置游戏的核心机制是“挂机收益”,不是“点击”

放置游戏,也叫增量游戏,代表作是 Cookie Clicker 一类。玩家的基本操作很快退居次要位置,核心体验变成:离开一段时间后回来,看到资源涨了一大截,然后决定把资源花在哪里。

这个循环通常由三个部分组成:

  1. 基础产率:单位时间自动产生多少资源。
  2. 升级项:把累计资源换成更高产率,形成指数增长。
  3. 随机事件或阶段性门槛:让玩家在某个时间点必须做出选择。

把这套逻辑套到 coding-agent 上,资源不是“饼干”,而是“开发进度”或“代币”;产率不是“每秒饼干数”,而是“agent 每秒能推进多少任务完成度”;升级项是“更快的模型档位”“更多的并行 agent”“更高的测试通过率”。映射关系一旦成立,整个玩法就自然浮现了。

1.2 coding-agent 的工作流本身就是一条事件流

真实世界里,一个 coding-agent 的工作过程并不是持续匀速产生代码,而是一条离散事件链:

任务输入 -> 代码分析 -> 编辑文件 -> 运行测试 -> 修复失败 -> 再次测试 -> 提交完成

这些事件天然带有不确定性:测试可能失败,依赖可能缺失,重构可能引入新问题。对放置游戏来说,这种不确定性恰恰是数值系统需要的东西。没有随机性,游戏就只是单纯的数字累加;有失败概率,玩家才会为了“降低失败率”去购买升级项。

所以这里的关键设计判断是:把 coding-agent 建模成一个状态机,而不是一个简单的收益函数。状态机的每一次状态迁移,都是游戏进度的一次产出或扣减;状态迁移的随机概率,就是游戏里需要被玩家“优化”的变量。

1.3 用一张映射表把两个领域对齐

在实现之前,先把两套概念逐项对齐,避免后面写代码时东改西改:

放置游戏要素coding-agent 映射示例数值
基础产率agent 每秒推进的任务完成度0.5 进度点/秒
主资源开发代币 tokens累计后购买升级
升级项模型档位 / 并行数 / 工具链速度提升 20%
随机事件测试失败、调试失败编辑阶段 20% 失败
中期目标完成一次大型重构任务累计进度达到阈值
重置机制重建仓库、引入新任务主题解锁更高倍率

游戏循环用一句话概括就是:任务投入给 agent,agent 按状态机产出事件,事件折算成进度和代币,代币购买升级,升级反过来提高 agent 的产出效率和成功率。后面所有代码都是围绕这条主线展开的。

2. 技术选型:桌面壳、游戏循环和 agent 接口怎么组合

这个项目有两个技术约束必须同时满足:一是要能做成桌面应用,二是要能方便地跟 coding-agent 交互。先决定桌面壳,再决定 agent 的接入方式,最后才是 React 组件怎么写。

2.1 桌面壳选择:不要一开始就上重方案

桌面端有三种常见选择,各有取舍。

方案安装包体积开发效率子进程能力适合场景
Electron较大,约 100MB 起步高,前端技术栈直接复用主进程天然支持 Node API,spawn 子进程方便快速原型、依赖 Node 生态
Tauri较小,几 MB 到十几 MB中,前端 + Rust 两层需要通过 Rust command 包装追求体积和内存占用
纯 Web最小最高没有直接子进程能力只验证玩法,不做桌面壳

这个原型推荐 Electron。原因很简单:真实 coding-agent 几乎都以 CLI 进程形态存在,Electron 主进程可以直接用 Node 的child_process启动和管理子进程,不需要额外封装一层本地服务。Tauri 能做得更轻,但每一步进程调用都要跨 Rust 边界,原型阶段会多出不少噪音。等玩法确认后,再考虑用 Tauri 重写外壳。

2.2 游戏循环不能只依赖 setInterval

很多新手在写“每秒增长”时直接从setInterval(() => tokens += 1, 1000)开始,这在真实桌面应用里会有三个隐患:

  1. 系统进入省电模式后,setInterval可能被节流,时间基准不可靠。
  2. 窗口切后台再切回来,interval 可能补发多个回调,造成数值突跳。
  3. React 重新渲染和游戏状态更新耦合在一起,界面一卡,游戏逻辑也跟着卡。

稳妥的做法是:固定每 1000ms 触发一次 tick,但在 tick 内部用真实时间差dt计算收益,同时给dt设一个上限,防止离线或卡顿后一次性追算太多。

let lastTickAt = Date.now(); setInterval(() => { const now = Date.now(); const dt = Math.min((now - lastTickAt) / 1000, 5); lastTickAt = now; tickGame(dt); }, 1000);

Math.min(..., 5)的意思是:即使程序卡了 30 秒,也只按 5 秒计算收益。放置游戏需要“挂机收益”,但不需要“卡顿补偿”,后者会把数值曲线打乱,也让离线恢复行为变得难以测试。

2.3 coding-agent 接入的三种方式

根据开发阶段不同,agent 接口可以分成三种实现方式,彼此之间可以切换。

接入方式优点缺点适合阶段
模拟状态机确定性高、零成本、可反复测试不产生真实代码,效果是假的原型、数值调节、自动化测试
CLI 子进程真实、直接复用现有 agent 工具输出格式不稳定、资源不可控、耗时不可控接入真实 agent
HTTP / 流式 API可控性强、能追踪 token 消耗需要 key、有网络依赖线上玩法或服务端结算

原型阶段先写模拟状态机,把游戏循环、升级、存档全部跑通;下一步再替换成 CLI 子进程。两套接口要抽象成同一个事件类型,否则后面切换时 React 层和数值层都要推倒重写。

2.4 环境准备与依赖清单

开发环境按以下版本准备:Node.js 20 LTS 或更高,npm 10 或 pnpm 9,Git 2.30 以上。如果后续要切 Tauri,才需要安装 Rust 工具链。

node -v npm -v git --version

创建项目并安装基础依赖:

npm create vite@latest idle-agent -- --template react-ts cd idle-agent npm install npm install -D electron electron-builder npm install zustand

这里选 Vite 的react-ts模板是因为它默认支持 TypeScript,状态管理用 zustand,体积小,而且能方便地在 React 组件外部读写状态。Electron 主要负责主进程、子进程管理和窗口创建,渲染进程继续用 Web 技术。

3. 实现最小可运行原型:模拟 agent 驱动放置循环

原型阶段的目标不是做完整游戏,而是用一个最简闭环证明“coding-agent -> 事件流 -> 数值增长 -> 升级 -> 再产出”是通的。先定义类型,再写 tick,再写状态机、升级和存档,最后接上 React 渲染。

3.1 先定义 GameState、AgentState 和 UpgradeDef

类型先行能避免后面大量返工。尤其是 agent 的phase字段,它决定了整个状态机的节点集合,后期加阶段也要从这里改起。

// src/game/types.ts export type AgentPhase = | 'idle' | 'analyzing' | 'editing' | 'running-tests' | 'debugging' | 'pr-ready' | 'failed'; export interface AgentState { id: string; name: string; phase: AgentPhase; currentTask: string; progressInTask: number; // 0 到 100 tasksDone: number; tasksFailed: number; } export interface PlayerState { tokens: number; totalProgress: number; agentLevel: number; speedMultiplier: number; agentCount: number; } export interface GameState { player: PlayerState; agents: AgentState[]; events: AgentEvent[]; } export interface AgentEvent { type: 'task-started' | 'task-complete' | 'task-failed' | 'test-failed'; task: string; ts: number; } export type UpgradeEffect = | { kind: 'speed'; multiplier: number } | { kind: 'agent-count'; add: number } | { kind: 'success-rate'; bonus: number }; export interface UpgradeDef { id: string; name: string; description: string; baseCost: number; costGrowth: number; maxLevel?: number; effect: UpgradeEffect; }

progressInTask是 agent 当前阶段的完成度,范围 0 到 100。它和totalProgress不是一个东西:前者是单任务阶段进度,后者是玩家累计收益。混在一起会导致升级效果和任务完成判定互相干扰。

3.2 用 tick 函数驱动每秒生产

生产公式决定数值增长曲线,先写一个足够简单但可扩展的版本:

// src/game/ticks.ts import type { AgentState, PlayerState } from './types'; import { advanceAgent } from './agentSim'; export function computeProduction(player: PlayerState): number { const basePerAgent = 0.5; const levelMultiplier = 1 + player.agentLevel * 0.25; const totalMultiplier = levelMultiplier * player.speedMultiplier; return player.agentCount * basePerAgent * totalMultiplier; } export function tickGame( player: PlayerState, agents: AgentState[], dt: number, events: AgentEvent[] ): void { const production = computeProduction(player); player.tokens += production * dt; player.totalProgress += production * dt; for (const agent of agents) { advanceAgent(agent, dt, Math.random, events); } }

computeProduction返回值是“每秒代币数”,乘以dt就是这一帧的增量。agentLevelspeedMultiplier分开保存,是为了区分两种不同的升级来源:等级来自重置或里程碑,倍率来自可重复购买的升级项。数值设计上,它们分别代表“长周期成长”和“短周期消费”。

3.3 用状态机模拟一个 coding-agent 的工作过程

模拟器是这段代码的重心。它不写真实代码,但必须模拟出 coding-agent 的行为节奏和失败概率。

// src/game/agentSim.ts import type { AgentPhase, AgentState } from './types'; const PHASE_SECONDS: Record<AgentPhase, number> = { idle: 3, analyzing: 8, editing: 14, 'running-tests': 6, debugging: 10, 'pr-ready': 2, failed: 4, }; const SAMPLE_TASKS = [ 'fix: 修正缓存键过期策略', 'feat: 增加 JSON Schema 校验', 'refactor: 拆分订单模块', 'test: 补充支付回调测试', ]; export function advanceAgent( agent: AgentState, dt: number, rng: () => number, events: AgentEvent[] ): void { const phaseSeconds = PHASE_SECONDS[agent.phase]; agent.progressInTask += (dt / phaseSeconds) * 100; if (agent.progressInTask < 100) return; agent.progressInTask = 0; switch (agent.phase) { case 'idle': agent.phase = 'analyzing'; agent.currentTask = SAMPLE_TASKS[Math.floor(rng() * SAMPLE_TASKS.length)]; events.push({ type: 'task-started', task: agent.currentTask, ts: Date.now() }); break; case 'analyzing': agent.phase = 'editing'; break; case 'editing': { const roll = rng(); if (roll < 0.2) { agent.phase = 'debugging'; events.push({ type: 'test-failed', task: agent.currentTask, ts: Date.now() }); } else { agent.phase = 'running-tests'; } break; } case 'running-tests': agent.phase = 'pr-ready'; agent.tasksDone += 1; events.push({ type: 'task-complete', task: agent.currentTask, ts: Date.now() }); break; case 'debugging': { const roll = rng(); if (roll < 0.75) { agent.phase = 'editing'; } else { agent.phase = 'failed'; agent.tasksFailed += 1; events.push({ type: 'task-failed', task: agent.currentTask, ts: Date.now() }); } break; } case 'pr-ready': case 'failed': agent.phase = 'idle'; break; } }

这个状态机里有两个重要的参数:编辑阶段 20% 概率进入调试,调试阶段 75% 概率修复成功。这两个概率直接决定玩家的“挫败感”和“升级价值”。如果不再买任何升级,长期来看约有一半的循环会带着一次失败事件,这对放置游戏来说节奏偏紧,但作为初始参数可以接受,后面靠升级项去调节。

这里把rng作为参数传入,而不是直接调用Math.random,是为了测试时能注入固定随机数,让状态迁移可以被断言。真实项目里,状态机逻辑越确定,越容易写单元测试。

3.4 升级项:成本函数和效果叠加

升级系统是放置游戏的消费出口。成本公式采用最常见的指数增长:

// src/game/upgrades.ts import type { UpgradeDef } from './types'; export const UPGRADES: UpgradeDef[] = [ { id: 'speed-1', name: '更快的分析器', description: 'agent 每秒产出提升 20%', baseCost: 15, costGrowth: 1.6, effect: { kind: 'speed', multiplier: 1.2 }, }, { id: 'agent-2', name: '第二个 agent', description: '解锁并行 agent,agent 数量 +1', baseCost: 120, costGrowth: 4.5, maxLevel: 2, effect: { kind: 'agent-count', add: 1 }, }, { id: 'lucky-1', name: '测试修复包', description: '调试阶段失败率降低 5%', baseCost: 80, costGrowth: 2.2, effect: { kind: 'success-rate', bonus: 0.05 }, }, ]; export function costOf(upgrade: UpgradeDef, currentLevel: number): number { return Math.floor(upgrade.baseCost * Math.pow(upgrade.costGrowth, currentLevel)); }

costGrowth决定同一升级项第 N 次购买的价格。1.6 表示价格每买一次乘 1.6,这个系数越大,玩家买几轮后就不得不去换更长线的目标。agent-2costGrowth设为 4.5 且maxLevel: 2,因为并行 agent 是质变型升级,不应该允许无限堆叠,否则数值会失控。

升级效果建议设计成叠加乘数而不是替代原有值。比如速度升级是speedMultiplier *= 1.2,新价格基于当前等级,这样既有指数成本,又有指数收益,玩家会有“再过一会儿就能买”的期待感。

3.5 存档:版本号、序列化和自动保存

放置游戏最怕丢存档。存档系统至少要包含三件事:版本号、序列化格式、读取时的数据校验。

// src/game/save.ts import type { GameState } from './types'; const SAVE_KEY = 'idle-agent-save'; const SAVE_VERSION = 1; export function saveGame(state: GameState): void { const payload = { version: SAVE_VERSION, savedAt: Date.now(), state, }; localStorage.setItem(SAVE_KEY, JSON.stringify(payload)); } export function loadGame(): GameState | null { const raw = localStorage.getItem(SAVE_KEY); if (!raw) return null; try { const payload = JSON.parse(raw); if (payload.version !== SAVE_VERSION) { // 版本不匹配时先尝试迁移,原型阶段简单处理为丢弃 return null; } return sanitizeGameState(payload.state); } catch { return null; } } function sanitizeGameState(state: unknown): GameState | null { if (!state || typeof state !== 'object') return null; const s = state as Partial<GameState>; if (typeof s.player?.tokens !== 'number') return null; if (!Array.isArray(s.agents)) return null; return state as GameState; }

localStorage在 Electron 渲染进程里可用,原型阶段足够。生产环境建议改成主进程写文件,用electron-store或者自己走 IPC 保存到用户数据目录,这样存档位置更稳定,也方便做多份备份。

自动保存频率建议 15 秒一次,不要每秒写。写频率过高会频繁触发磁盘 IO,桌面端体验拉胯。

3.6 UI 层:把状态渲染成上下三块面板

界面不需要花哨,三个区域足够:顶部资源数字,中间 agent 状态和升级按钮,底部滚动日志。状态管理用 zustand,因为 zustand 允许在 React 组件外部更新状态,游戏 tick 循环和组件渲染可以解耦。

// src/App.tsx import { useEffect } from 'react'; import { create } from 'zustand'; import { tickGame } from './game/ticks'; import { UPGRADES, costOf } from './game/upgrades'; import { saveGame, loadGame } from
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/31 1:17:55

macOS菜单栏收件箱:让tmux中Claude Code/Codex任务状态一目了然

如果你最近在 macOS 上用 Claude Code 或 Codex CLI 跑过稍大一点的开发任务&#xff0c;大概率会遇到同一个问题&#xff1a;agent 已经接管终端&#xff0c;任务短则几十秒&#xff0c;长则十几分钟&#xff0c;你不可能一直把窗口钉在屏幕上。切去写文档、看代码、查资料&am…

作者头像 李华
网站建设 2026/8/31 1:16:57

Flume 多维数据源采集实战:数据库、日志与埋点的统一接入之道

Flume 多维数据源采集实战&#xff1a;数据库、日志与埋点的统一接入之道1. Flume 架构概述与多维数据源接入意义Apache Flume 是一个高可用、高可靠、分布式的海量日志采集、聚合和传输的系统&#xff0c;专为日志收集中设计。在企业级数据中台建设过程中&#xff0c;通常需要…

作者头像 李华
网站建设 2026/8/31 1:04:40

发现chat上传文档有数量限制,-文心一言虽然可以上传,但是给出的文档没有给出具体对应参考文献。-ds比较好,可以上传,且会对应参考文献格式-比较准,gb7714-2025比较准-但是有偏差-需要调整

通过调用&#xff1a;ds&#xff0c;文心一言&#xff0c;chat——发现chat上传文档有数量限制&#xff0c;无法上传全部文档-文心一言虽然可以上传&#xff0c;但是给出的文档没有给出具体对应参考文献。-ds相对来说比较好&#xff0c;可以上传&#xff0c;且会对应参考文献&a…

作者头像 李华
网站建设 2026/8/31 1:01:35

Flume HTTPSource 与 HTTP Sink 实践:构建实时数据接收网关与推送端点

Flume HTTPSource 与 HTTP Sink 实践&#xff1a;构建实时数据接收网关与推送端点 Flume HTTPSource 与 HTTP Sink 概述 Apache Flume 是一个分布式、可靠、可扩展的服务&#xff0c;用于高效地收集、聚合和移动大量日志数据。在实时数据处理场景中&#xff0c;Flume 的 HTTPSo…

作者头像 李华
网站建设 2026/8/30 23:57:58

BlueNRG-2低功耗模式GPIO端口保持配置与调试指南

前阵子调一个用BlueNRG-2做的低功耗门磁&#xff0c;遇到了一个让我连续加了两天班的问题&#xff1a;设备在正常运行的时候一切正常&#xff0c;但只要进入低功耗模式&#xff0c;本来应该保持低电平的传感器供电引脚就会飘到接近电源电压&#xff0c;外设被提前唤醒&#xff…

作者头像 李华
网站建设 2026/8/30 23:57:49

Postman不是接口测试工具?Roblox怀旧邮差游戏与开发拆解

先说一个容易踩的坑。你在搜索引擎里输入 postman&#xff0c;前几页大概率不是游戏&#xff0c;而是那个做接口调试的 Postman 工具。满屏都是 Postman 下载、Postman 汉化、Postman 接口测试教程&#xff0c;甚至还有“Postman 打不开”“Postman 忘记密码”这类问题。我这次…

作者头像 李华