尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Vibe Coding实战:用自然语言驱动AI开发怀旧网游挂机模拟器

Vibe Coding实战:用自然语言驱动AI开发怀旧网游挂机模拟器 vibe coding 这个词最近频繁出现在开发者社区里很多人以为它只是“让 AI 全自动写代码”但真正上手 13 天后会发现它背后其实是另一种开发节奏。这篇博客记录的就是一个 13 天的个人项目用 vibe coding 的方式做了一个名为“QQ华夏挂机版”的怀旧网游挂机模拟小 Demo用来纪念 18 年前的网游时光。这个项目不包含原版游戏任何素材只通过文字、数据、日志还原“自动打怪、升级、掉装备”的玩法核心目标是跑通一条从自然语言需求到可运行代码的完整链路。如果你对 vibe coding 感兴趣又不想一上来就写复杂的 Web 应用这个项目是非常合适的练手对象。它会涉及角色数据建模、自动战斗循环、经验成长曲线、存档机制、异常恢复和日志输出。每个部分都能用 Python 标准库实现不需要额外安装第三方依赖因此无论你是在 Windows、macOS 还是 Linux 上学习跟着文章把脚本跑起来只需要十几分钟。做完这个项目之后你会理解 vibe coding 的边界AI 能生成大量代码但需求拆解、结果验证、边界处理、数值平衡和存档兼容仍然需要人来把握。下面先讲清楚 vibe coding 的工作方式再展开这个挂机 Demo 的开发全过程。1. vibe coding 到底是什么它为什么适合这个怀旧项目1.1 vibe coding 不是“让 AI 替你干活”而是“用语言驱动开发节奏”vibe coding 可以理解成一种开发方式开发者用自然语言描述需求、现象和期望结果AI 模型生成代码开发者负责验证、运行、发现问题和继续迭代。与传统编程不同的是你不再逐行敲完所有逻辑而是把程序拆成一个个“能说清楚”的片段再用提示词让 AI 补全。“vibe”这个词强调的是保持在开发心流里不要因为等待 AI 生成或反复改提示词而丢掉上下文。你可以把它看作一种人机结对编程AI 负责快速起草你负责判断方向。比如你告诉 AI“写一个玩家对战怪物的函数怪物攻击力高于玩家防御时会造成伤害伤害至少为 1”AI 会给出类似max(1, monster.attack - player.defense random.randint(-1, 2))的代码。你不需要从零敲出每一行但你需要能判断这个公式是否符合预期。这种模式对开发者的要求不是降低而是换了一个方向。你不需要疯狂拼写语法但你必须能读懂代码、能跑测试、能描述清楚问题。尤其是在排错时如果把一段没有上下文的报错信息丢给 AI它很难给出准确修复。真正有效的做法是先定位问题发生在哪个函数、输入是什么、希望输出什么再把代码片段和错误日志一起交给 AI。1.2 这类开发方式适合什么项目不适合什么项目vibe coding 不是万能钥匙。根据这 13 天的实践我认为它非常适合以下场景工具脚本和自动化脚本数据处理、文件整理、定时任务。原型和 Demo快速验证交互逻辑、算法流程、页面布局。学习项目用 AI 生成代码并阅读是最快熟悉一门语言的方式之一。模拟器和小游戏状态清晰逻辑边界完整方便用函数表达。不适合的场景也需要提前说明。涉及真实资金交易、高并发、用户隐私、安全权限控制的系统不能只靠 vibe coding 生成代码后直接上线。金融计算中的金额精度、防止并发超卖、权限绕过等问题都需要人工专门审查和充分测试。AI 可以帮你生成初稿但“安全”和“合规”不能靠 vibe coding 的“感觉”来保证。另外个人学习项目要注意版权和合规。这次的项目只保留了“挂机玩法”的文字逻辑不包含原版游戏的美术、音乐、角色名和数值体系。如果你想做一个怀旧主题小游戏建议只写玩法框架不要直接搬运原游戏的资源或真实商业数据。1.3 为什么“QQ华夏挂机版”是一个合适的 vibe coding 练习选择“挂机版”作为练习项目是因为它的逻辑链路非常适合 AI 辅助开发状态变化清楚玩家有等级、经验、生命、攻击、防御、金币、背包。循环边界完整每回合玩家打怪、怪物反击、结算经验与掉落。结果可视化容易控制台日志就是最简单的游戏界面。迭代空间充足先跑通战斗再补存档再调数值平衡每一步都能独立验收。传统开发模式里你可能会花很多时间在 UI 和交互上。挂机 Demo 则不同核心是状态在时间上的推进。AI 很容易理解“每 1 秒打一只怪怪物死了给玩家经验经验满了升级”这样一句需求并且能生成相当完整的代码。这个项目的另一个优点是失败成本低。即使生成的代码有 bug最多只是控制台输出不对或存档文件损坏不会影响任何线上服务。你可以放心地反复试验提示词观察 AI 在不同描述方式下的输出差异从而建立一套适合自己的 vibe coding 工作流。2. 13天开发节奏从自然语言需求到第一个可运行版本2.1 先拆需求再写提示词不要直接让 AI“做一个游戏”“做一个 QQ 华夏挂机版”这句话听起来完整但对 AI 来说信息量远远不够。AI 没有自动理解需求上下文的功能它只能根据提示词中的行为描述生成代码。如果一句话需求直接丢过去你得到的很可能是结构混乱、参数残缺、边界缺失的代码甚至是一个只打印欢迎语的空壳。所以第一步是需求拆解。我把它拆成了六个可以独立验收的模块模块核心功能验收标准角色创建选择姓名、职业初始化属性启动时能生成一个角色实例自动战斗每回合玩家攻击怪物、怪物反击能在控制台看到双方伤害数值经验成长击杀怪物获得经验经验满升级升级后属性提升生命恢复掉落背包怪物掉落金币和物品背包列表增长且有容量限制存档读档保存角色状态到 JSON 文件重启程序后能恢复进度运行控制支持指定回合数和间隔时间用户可以用命令行参数控制流程拆完模块后每个模块都可以单独写提示词。比如“创建一个 Player 数据类包含 name、job、level、exp、hp、max_hp、attack、defense、gold、bag 字段并写一个 gain_exp 方法用于增加经验。”这比“做一个游戏”清晰得多AI 也不容易跑偏。2.2 第一天到第三天把“挂机”翻译成数据结构前三天我没有让 AI 写完整项目而是先编写游戏的数据模型。这个阶段最重要的任务是回答几个问题玩家需要保存哪些属性怪物和玩家战斗时哪些属性参与伤害计算经验升级曲线是线性还是递增存档文件需要包含哪些字段数据结构一旦定下来后续功能都是围绕它展开的。比如玩家数据使用 Python 的dataclass每个字段都有明确含义。怪物数据使用字典列表每个怪物包含名称、生命、攻击、防御、经验、金币和掉落物。这样做的好处是AI 生成战斗函数时可以直接操作这些字段不需要反复解析字符串。数据结构表如下对象字段说明Playername, job, level, exp基础身份与成长进度Playerhp, max_hp, attack, defense战斗属性Playergold, bag经济与背包状态Monstername, hp, attack, defense怪物战斗属性Monsterexp, gold, drop击杀结算信息存档version, saved_at, player保存版本、时间、玩家状态把数据模型固定下来之后后面所有提示词都可以引用“Player 对象”和“Monster 字典”AI 生成代码的方向会稳定很多。这里也推荐把字段名统一成英文避免在代码里混用中文属性名。存档文件里的中文可以通过ensure_asciiFalse正常显示但字段名尽量使用英文。2.3 第四天到第七天跑通第一个战斗循环有了数据模型第四天开始让 AI 生成战斗函数。我给 AI 的提示词类似下面这样请用 Python 写一个函数输入为玩家对象和怪物字典输出为战斗日志字典。 规则 1. 玩家命中率为 85%命中后伤害为 player.attack 加上 -2 到 3 的随机数再减去怪物 defense最低伤害为 1。 2. 如果怪物未死亡怪物对玩家造成伤害伤害为怪物 attack 减去玩家 defense 加上 -1 到 2 的随机数最低为 1。 3. 怪物生命小于等于 0 时玩家获得经验、金币和掉落物。 4. 函数打印清晰的战斗日志。运行第一版后发现一个问题角色升级时生命值没有恢复到最大值导致连续战斗时角色越来越虚弱。原因是 AI 生成的升级逻辑只增加了max_hp没有把hp同步设置成新的max_hp。这属于非常典型的状态更新遗漏AI 只关注“加属性”忽略了“补状态”。把报错和期望发给 AI 后它很快补上了self.hp self.max_hp。这个阶段最重要的事情是建立“运行后必须看日志”的习惯。挂机程序本身没有界面日志就是唯一的反馈渠道。如果日志没有把当前回合的状态打印清楚很难凭感觉判断数值是否合理。2.4 第八天到第十三天迭代、测试、存档和平衡第八天之后主体逻辑已经能跑。接下来的迭代重点是增加存档和读档功能避免每次启动都从零开始。调整伤害公式让战士、法师、刺客三个职业有差异。修复背包无限增长的问题加入容量限制。增加--load参数允许从存档继续挂机。测试连续升级时while循环是否会卡住。这个阶段最大的感受是AI 修 bug 的能力依赖于你提供的上下文。如果你只把Traceback粘贴过去AI 经常只能猜测问题方向。更好的做法是同时提供运行 python idle_huaxia.py --rounds 20 后第 8 回合出现 AttributeError: Player object has no attribute take_damage。 代码片段 class Player: ... 我需要调用 player.take_damage(5) 来扣血但没有这个方法。 请给 Player 类补充 take_damage 方法并保证生命值最低为 0同时更新调用处。这样 AI 能快速定位问题。13 天中大量时间不是花在“让 AI 生成代码”而是花在“解释清楚期望行为、验证输出、补充边界条件”上。这个过程持续到最后一天最终的小项目虽然没有复杂 UI但核心玩法已经完整。3. 从零实现最小可运行 Demo3.1 环境准备为了让这个项目可复现我把它做成了一个单文件 Python 脚本只依赖标准库。环境要求非常简单环境项推荐配置说明Python3.10 或更高使用了dataclass和类型注解操作系统Windows / macOS / Linux命令行输出需要支持 UTF-8第三方依赖无只用标准库argparse、json、random、time、pathlib编辑器VS Code / PyCharm / 任意文本编辑器配合 AI 插件体验更好不需要创建虚拟环境也不需要pip install。直接复制代码保存为idle_huaxia.py即可运行。3.2 目录结构虽然脚本是单文件但推荐在项目目录下保留提示词和存档目录方便后续维护idle-huaxia-demo/ ├── idle_huaxia.py # 主脚本 ├── prompts/ # 存放每次给 AI 的提示词 │ ├── 01-data-model.txt │ ├── 02-battle-loop.txt │ └── 03-save-load.txt ├── data/ │ └── save.json # 自动生成的存档文件 └── README.md # 项目说明prompts目录是整个 vibe coding 项目最有价值的部分。每次让 AI 生成功能之前先把需求写进该目录运行过程如果发现问题再新增一个带有错误日志的修复提示词。这样即使换了电脑你也能通过提示词文件还原整个开发过程。3.3 完整代码单文件版下面是一个最小可运行版本。它包含了角色创建、自动战斗、经验升级、背包掉落、存档读档和命令行控制。这段代码可以作为你继续扩展的起点。 idle_huaxia.py 怀旧网游挂机玩法的最小控制台 Demo。 仅用于学习 Python 和 vibe coding 工作流不包含任何原版游戏素材。 import argparse import json import random import time from dataclasses import dataclass, field, asdict from pathlib import Path DEFAULT_CONFIG { interval: 1.0, hit_rate: 0.85, bag_limit: 20, level_factor: 0.2, } MONSTERS [ {name: 野狗, hp: 25, attack: 6, defense: 2, exp: 12, gold: 3, drop: 布甲}, {name: 山贼, hp: 45, attack: 9, defense: 4, exp: 20, gold: 8, drop: 短刀}, {name: 石像, hp: 80, attack: 12, defense: 6, exp: 35, gold: 15, drop: 护心镜}, ] dataclass class Player: name: str job: str 战士 level: int 1 exp: int 0 hp: int 100 max_hp: int 100 attack: int 15 defense: int 5 gold: int 0 bag: list field(default_factorylist) property def exp_needed(self) - int: return int(50 * self.level * (1 DEFAULT_CONFIG[level_factor] * self.level)) def gain_exp(self, amount: int) - None: self.exp amount while self.exp self.exp_needed: self.exp - self.exp_needed self.level_up() def level_up(self) - None: self.level 1 self.max_hp 10 self.hp self.max_hp self.attack 2 self.defense 1 print(f[升级] 恭喜升到 {self.level} 级生命和攻击提升。) def take_damage(self, amount: int) - None: self.hp max(0, self.hp - amount) def player_attack(player: Player, monster: dict) - int: if random.random() DEFAULT_CONFIG[hit_rate]: return 0 damage max(1, player.attack random.randint(-2, 3) - monster[defense]) monster[hp] max(0, monster[hp] - damage) return damage def monster_attack(player: Player, monster: dict) - int: if monster[hp] 0: return 0 damage max(1, monster[attack] - player.defense random.randint(-1, 2)) player.take_damage(damage) return damage def battle(player: Player, monster: dict) - None: pd player_attack(player, monster) md monster_attack(player, monster) print(f[战斗] 你对 {monster[name]} 造成 {pd} 点伤害它反击了 {md} 点伤害。) if monster[hp] 0: player.gain_exp(monster[exp]) player.gold monster[gold] player.bag.append(monster[drop]) print(f[胜利] 击杀 {monster[name]}获得经验 {monster[exp]}、金币 {monster[gold]}、物品 {monster[drop]}。) def save_game(player: Player, save_path: str data/save.json) - None: payload { version: 1, saved_at: int(time.time()), player: asdict(player), } path Path(save_path) path.parent.mkdir(exist_okTrue) tmp_path path.with_suffix(.json.tmp) tmp_path.write_text(json.dumps(payload, ensure_asciiFalse, indent2), encodingutf-8) tmp_path.replace(path) def load_game(save_path: str data/save.json): path Path(save_path) if not path.exists(): return None payload json.loads(path.read_text(encodingutf-8)) if payload.get(version) ! 1: raise ValueError(存档版本不兼容) return Player(**payload[player]) def run(player: Player, rounds: int 10, interval: float 1.0) - None: monster_pool MONSTERS for idx in range(1, rounds 1): if player.hp 0: print([结束] 角色倒下挂机流程自动停止。) return monster dict(monster_pool[(idx - 1) % len(monster_pool)]) battle(player, monster) print(f[状态] 第 {idx} 回合等级 {player.level}HP {player.hp}/{player.max_hp}经验 {player.exp}/{player.exp_needed}金币 {player.gold}。) if len(player.bag) DEFAULT_CONFIG[bag_limit]: print([提示] 背包已满本轮掉落的低阶物品被自动出售。) player.gold 2 player.bag player.bag[-DEFAULT_CONFIG[bag_limit]:] time.sleep(interval) def create_player(name: str, job: str) - Player: job_stats { 战士: {hp: 100, attack: 15, defense: 6}, 法师: {hp: 70, attack: 20, defense: 3}, 刺客: {hp: 80, attack: 18, defense: 4}, } base job_stats.get(job, job_stats[战士]) return Player(namename, jobjob, hpbase[hp], max_hpbase[hp], attackbase[attack], defensebase[defense]) def main(): parser argparse.ArgumentParser(description怀旧网游挂机玩法 Demo) parser.add_argument(--name, default清风) parser.add_argument(--job, default战士, choices[战士, 法师, 刺客]) parser.add_argument(--rounds, typeint, default10) parser.add_argument(--interval, typefloat, default1.0) parser.add_argument(--load, actionstore_true, help加载已有存档) args parser.parse_args() if args.load: player load_game() or create_player(args.name, args.job) else: player create_player(args.name, args.job) print(f开始挂机玩家 {player.name}{player.job}计划 {args.rounds} 回合。) run(player, roundsargs.rounds, intervalargs.interval) save_game(player) print(挂机结束进度已保存到 data/save.json。) if __name__ __main__: main()这段代码最关键的地方是gain_exp中的while循环。如果经验一次性超过多级所需AI 很可能只写if判断导致只升一级。使用while可以连续升级直到剩余经验不足以继续升级为止。take_damage方法则保证了生命值不会变成负数角色死亡后挂机主循环会停下来。3.4 如何运行在项目目录下执行python idle_huaxia.py --name 清风 --job 战士 --rounds 10 --interval 1预期输出类似开始挂机玩家 清风战士计划 10 回合。 [战斗] 你对 野狗 造成 13 点伤害它反击了 2 点伤害。 [胜利] 击杀 野狗获得经验 12、金币 3、物品 布甲。 [状态] 第 1 回合等级 1HP 98/100经验 12/60金币 3。 ... 挂机结束进度已保存到 data/save.json。执行结束后打开data/save.json可以看到玩家状态已经持久化{ version: 1, saved_at: 1790000000, player: { name: 清风, job: 战士, level: 1, exp: 12, hp: 98, max_hp: 100, attack: 15, defense: 6, gold: 3, bag: [布甲] } }如果要继续挂机可以执行python idle_huaxia.py --load --rounds 20 --interval 1程序读取存档后从已有状态继续推进。这里要注意如果存档文件版本字段不是 1会抛出ValueError。给存档加版本号是 vibe coding 项目里很值得保留的好习惯它让将来的数据迁移成为可能。4. 关键代码拆解AI 生成之后人工该检查什么4.1 随机数、命中率和伤害公式战斗逻辑是最容易让 AI “编得很像但实际不平衡”的部分。原始的伤害公式如果写成damage player.attack - monster[defense]那么低攻击角色打高防御怪物时会出现零伤害挂机永远推不下去。加入随机数和下限保护后公式变得稳了damage max(1, player.attack random.randint(-2, 3) - monster[defense])这里三个关键参数值得单独检查参数作用调大影响调小影响hit_rate控制玩家命中概率战斗更稳定会出现大量 Miss推进变慢随机浮动范围模拟伤害波动输出方差大战斗容易变成固定数值最小伤害下限保证角色始终能造成伤害低攻职业也能通关数值可能不真实但不会卡死在实际项目中建议把命中率、经验系数等配置从代码中抽出来放到DEFAULT_CONFIG字典里统一维护。这样 AI 在后续修改时不需要改动多个函数也能避免“这里改了伤害公式那里还在用旧参数”的问题。4.2 升级经验与属性成长经验成长曲线是挂机游戏里最影响体验的部分。如果经验需求固定不变游戏会越来越无聊如果需求指数增长过快又会让人看不到提升空间。我使用的公式是int(50 * self.level * (1 DEFAULT_CONFIG[level_factor] * self.level))也就是每升一级下一级所需经验会随等级增加而增加。level_factor控制增长速度。当前值是 0.21 级升级需要 60 经验5 级升级需要大约 100 经验10 级升级需要大约 150 经验。你可以根据挂机时长调整这个系数。有一个容易出错的点gain_exp里一定要用while而不是if。因为高级怪物给的经验可能超过当前升级所需的好几倍。假设角色 1 级时需要 60 经验一只怪物给了 150 经验if版本只会升到 2 级剩余 90 经验会消失while版本会连续升到 3 级剩余经验继续保留。这个边界在 AI 第一版代码里往往会被忽略。4.3 存档和异常恢复存档功能看似简单但 AI 生成的第一版可能直接这样写path.write_text(json.dumps(payload), encodingutf-8)这种写法有一个隐患如果程序在写入过程中被强制终止存档文件可能只写了一半下一次读档直接JSONDecodeError。更好的方案是“先写临时文件再原子替换”tmp_path path.with_suffix(.json.tmp) tmp_path.write_text(json.dumps(payload, ensure_asciiFalse, indent2), encodingutf-8) tmp_path.replace(path)tmp_path.replace(path)在同一个目录内替换文件时是原子操作读取方只会看到旧文件或新文件不会看到写了一半的文件。这个技巧在许多生产级系统里都有应用放在一个小挂机 Demo 里也很值得练习。读档时还要注意版本兼容if payload.get(version) ! 1: raise ValueError(存档版本不兼容)如果未来你给角色增加了新字段旧存档没有这个字段直接传入Player(**payload[player])会报TypeError。加上版本号后你可以写一个“旧版本迁移”函数把旧存档升级成新结构。这是长期维护 vibe coding 项目的关键细节。4.4 日志与可观测性挂机程序没有界面日志就是唯一的观察窗口。建议至少输出以下信息当前回合数。玩家对怪物造成的伤害。怪物反击造成的伤害。击杀后的经验、金币、物品收益。玩家当前等级、生命、经验、金币状态。输出格式可以统一为[回合] [战斗] [胜利] [升级] [提示] [结束]这类前缀。如果你想把日志采集起来做分析可以加上时间戳import datetime timestamp datetime.datetime.now().strftime(%Y-%m-%d %H:%M:%S) print(f[{timestamp}][战斗] ...)有了时间戳后日志就能用于定位“某个阶段为什么挂机变慢”“背包什么时候开始满”等问题。一个 mini 项目也可以提前养成可观测性的习惯。5. 常见问题排查挂机脚本为什么不按预期工作5.1 五个常见现象与处理表Vibe coding 项目里大多数问题不是 AI 不会写而是生成代码后在特定输入下触发了边界 bug。下面是这个项目最常见的五类问题以及对应的排查建议问题现象常见原因检查方式处理建议模块找不到或脚本无法启动运行目录不对或文件被保存为错误名称执行pwd查看当前目录执行python idle_huaxia.py看报错确认脚本在工作目录下不要直接双击运行控制台中文乱码Windows 终端默认编码不是 UTF-8查看终端代码页或检查 Python 报错信息Windows 下先执行set PYTHONIOENCODINGutf-8再运行挂机跑了几轮就停止角色生命值降为零主循环检测到hp 0查看日志中[结束] 角色倒下之前的状态提高职业初始防御或加入自动回血逻辑角色升级后没有恢复生命升级时只增加max_hp没有同步hp查看升级日志前后的 HP 状态在level_up中把hp设置为新的max_hp存档读出来报 JSON 错误存档文件被中断写入或手动编辑导致格式损坏打开data/save.json查看内容使用临时文件原子替换开发时备份存档除了以上问题还有一种很隐蔽的情况AI 生成的exp_needed使用了level_factor但你在测试时把DEFAULT_CONFIG中的值改掉了导致旧存档读取后升级经验变化。建议每次都从同一份配置启动或者把配置一并写入存档。5.2 从日志倒推问题遇到 bug 时不要直接看代码猜先看日志。这个项目的日志已经包含回合状态排查顺序可以这样安排查看最后一条日志决定问题发生在哪一阶段。如果是战斗未推进检查上一回合玩家和怪物的生命值。如果是升级异常检查经验变化和升级后的属性。如果是存档问题先打开 JSON 文件确认字段是否完整。仍然无法定位就把日志、关键函数代码、输入参数一起发给 AI并要求它给出修改后的完整
返回列表