开发了一个每天随机对战的宝可梦游戏

简介: 本文分享《Daily Red Pokémon》浏览器游戏的工程实践:基于 Next.js/React/TS 实现每日固定队伍生成、Gen 3 战斗模拟(@pkmn/sim)、Web Worker 隔离计算、动作日志恢复对局,以及修复生产环境序列化故障。聚焦可复现性、状态边界与轻量部署。(239字)

本文分享一个浏览器游戏项目的实现与取舍:如何生成每天一致的队伍、接入 Gen 3 战斗模拟器、用动作日志恢复对局,以及处理一个只在生产构建中出现的序列化问题。技术栈以 Next.js、React、TypeScript 和 Web Worker 为主。

开发 Daily Red Pokemon 时,我最先确定的是游戏范围:每天给玩家六只 Pokémon,让他挑战一次 Red。没有练级流程,没有大地图,也不要求先注册账号。

这个范围听起来很小,但做成可反复使用的作品,需要回答不少工程问题:刷新后能否继续?跳过动画会不会改变结果?AI 评估招式时会不会消耗真正战斗的随机数?本地能运行,压缩上线后是否还能完成一个回合?

这是一个独立的 Pokémon 同人学习项目,不代表 Pokémon 权利方的官方发行或授权。下文结合代码节选和历史故障记录,说明实现方式及其适用边界。

在这里插入图片描述

下面按几个实际影响架构的决定来复盘。玩法只保留理解设计所需要的部分,重点是这些决定为什么值得做,以及它们带来了什么限制。

一、先把玩法收窄,再确定数据边界

Daily Red Pokemon 是回合制单打:双方各六只,等级统一为 100;玩家选择首发、出招或换人,直到产生胜负或平局。Red 的阵容固定为 Pikachu、Espeon、Snorlax、Venusaur、Charizard 和 Blastoise,Pikachu 首发,其余出场可以因换人而变化。

玩家侧从 30 种已配置的 Pokémon 中抽取六种,不重复。招式、性格、能力值、特性和携带道具有预设,不是把所有世代、所有招式组合都交给随机生成。

这个限制同时服务玩法和开发:

  • 玩家面对的是“用现有队伍解决问题”,而不是不断刷新到理想阵容。
  • 每套配置能单独检查,遇到错误可以定位到具体数据。
  • 测试覆盖有明确边界,不必一开始就处理无限的配招组合。

每天一次计入记录的挑战,负责提供共同话题;独立练习负责给玩家试错空间。这两种需求需要在状态模型中分开,否则“练习一下”也可能意外消耗正式机会。

我的经验是,先写清楚什么会被计分、什么必须保持一致,比先做更多动画更能决定项目是否容易维护。

二、技术栈:页面、战斗计算和部署分别负责什么

项目使用现有 Web 工程承载游戏,核心部分如下。这里列的是实际参与游戏链路的技术,不是根目录所有依赖。

层次 使用的技术 具体职责
网站与页面 Next.js 15、React 19 页面、玩法说明、元数据、交互组件
类型与数据 TypeScript、JSON 动作协议、队伍配置、战斗快照与事件类型
规则计算 @pkmn/sim@0.10.11 Pokémon Showdown 的 Gen 3 战斗模拟
计算隔离 浏览器 Web Worker 初始化对局、执行动作、恢复操作序列
界面与音频 CSS、Web Audio API 战场、菜单、血条、动画和程序合成音效
本地持久化 localStorage 正式对局日志、战绩与用户设置
网站部署 OpenNext、Cloudflare Workers、Wrangler 构建适配、静态资源和网站请求

浏览器 Web Worker 与 Cloudflare Worker 是两件事:前者在玩家设备里计算对战,后者承载网站部署。当前游戏没有把每次出招发到 Cloudflare 做服务端裁决。

页面也不必全部等待战斗引擎。玩法说明可以作为页面内容提供,浏览器在开始或恢复战斗时创建计算线程。这样做能把入口页面与重一些的模拟器分开,但不意味着引擎下载没有成本;首次开战仍需要关注弱网体验。

我把代码分成三条主要链路:

页面与交互:DailyRedGame → useBattle → BattleStage / BattleMenus
战斗计算:BattleClient → battle.worker → BattleSession → @pkmn/sim
本地记录:useBattle → journal / storage → localStorage

页面不用知道模拟器内部对象怎么组织;模拟器也不用知道血条用哪种 CSS 动画。两侧通过动作、快照和事件通信。

三、规则引擎:接入成熟模拟器,自己实现每日玩法

项目早期有过自写伤害和状态逻辑,后来改为固定版本的 @pkmn/sim。原因是规则组合比一条伤害公式复杂得多:招式命中、能力、道具、异常状态、换人、PP 和回合结束效果会互相影响。

以 Gen 3 为例,招式类型决定物理或特殊类别。Shadow Ball 是物理,Crunch 和 Dragon Claw 是特殊。如果界面沿用后续世代的分类,即使引擎算对了,玩家也会基于错误信息做决策。

因此,Daily Red Pokemon 的招式说明与计算都以 Gen 3 数据为依据。我保留自己的每日选队、对手策略和表现层,让成熟模拟器负责基础规则。

规则适配中有两处明确选择:

// 节选自模拟器适配层:数据世代和战斗格式要保持一致。
const dex = Dex.mod("gen3");

const battle = new Battle({
   
  formatid: toID("gen3customgame"),
  seed: battleSeed,
  p1: {
    name: "Player", team: playerSets },
  p2: {
    name: "RED", team: redSets },
});

这里的 battleSeed 和队伍对象由适配层生成,上面只是接入位置的精简示例。输入层还要检查等级、招式合法性、EV、IV、性格与道具,不能把“用了成熟库”等同于“业务配置一定正确”。

接入模拟器也不等于运行 FireRed ROM。当前实现使用浏览器中的规则模拟,不加载 ROM,也不承诺与原版卡带逐位一致。

四、每日队伍:可复现比“看起来随机”更重要

如果直接在页面加载时使用 Math.random(),刷新就可能换队,也无法让同一天的玩家比较结果。这里需要的是由日期确定的伪随机序列。

当前实现把本地日期和数据版本组合成种子,再用固定的伪随机数发生器做 Fisher–Yates 洗牌,最后取前六个。核心代码很短:

// 依赖 seed.ts 中固定的 hashString32 和 shuffledCopy 实现。
const DATA_VERSION = 2;

function dailySeed(dateKey: string): number {
   
  return hashString32(`daily-red-pokemon:${
     dateKey}:v${
     DATA_VERSION}`) || 1;
}

const seed = dailySeed(dateKey);
const teamIds = shuffledCopy(playerPool, seed)
  .slice(0, 6)
  .map(pokemon => pokemon.id);

相同日期能得到相同队伍,前提是候选池内容及顺序、哈希、洗牌算法和版本都一致。只保留同一个数字种子,却悄悄调整候选池顺序,仍可能改变当天队伍。

我把重置时间设为玩家当地午夜,让“每天一次”贴近日常使用。代价是不同时区并非同时换题,所以页面显示挑战日期和 NEXT TEAM 倒计时,方便对照。

另外,数据种子版本与存档结构版本解决的是不同问题。前者参与选队,后者用于识别日志格式;当前都有版本标识,但并不会自动迁移所有旧数据。规则或数据升级时,旧动作日志是否还能复算,仍然需要单独处理。

这套本地日期方案适合轻量日常挑战。如果以后要做可信排行榜,就需要服务器确定挑战日期、验证操作和结算结果,不能直接把本地战绩当作可信成绩。

五、Web Worker 与动作日志:让刷新成为恢复流程

我希望玩家中途关闭页面后还能继续,因此存档不只记录“还剩多少 HP”,而是保存开局配置和已经提交的动作。

只存 HP 会丢失很多上下文:剩余 PP、状态、道具是否消耗、强化阶段、随机数进度等。配置加动作序列可以让模拟器重新走到相同位置,前提是相关规则和算法保持兼容。

下面是日志的核心结构节选:

type Action =
  | {
    kind: "move"; slot: number }
  | {
    kind: "switch"; slot: number }
  | {
    kind: "forfeit" };

interface Journal {
   
  config: BattleConfig; // 日期、种子、六个 ID、首发、模式和版本
  actions: Action[];
  updatedAt: string;
  outcome?: "won" | "lost" | "tied";
  turns?: number;
}

Worker 收到初始化请求后,创建 BattleSession 并依次重放 actions。战斗运行时,每次请求带递增 ID,主线程通过 ID 找到对应 Promise;线程错误和超时也要反馈给界面,不能让按钮一直显示加载中。

这里真正需要仔细处理的是失败顺序。当前流程会先把新动作加入日志,再请求 Worker;如果动作执行失败,就恢复旧动作列表、写回日志并销毁当前客户端,供后续从已保存日志重新初始化。

读取 localStorage 时,也不能只写一个 JSON.parse() 就继续执行。当前代码会检查版本、日期格式、六个有效且不重复的 Pokémon ID、首发范围和动作结构;具体动作在当前局面是否可执行,由模拟器再检查。

还有几个容易漏掉的边界:

  • 重复结算: 用挑战日期阻止常规刷新、恢复流程重复累计战绩。
  • 多标签页: 监听存储变化,并在行动前比对日志;检测到旧状态就要求恢复。它能减少误覆盖,但不是数据库级的并发事务。
  • 存储失败: 页面显示警告,让玩家知道本次进度可能无法恢复。
  • 跨日续局: 午夜产生新队伍,但已保存的旧局保留原日期;先结束旧局,再开始新挑战。

记录当前只在同一浏览器中保存,也没有完整历史对局归档。对于一个小项目,这降低了账号和后端复杂度;同时也意味着换设备不会自动同步,恢复还要承担重放日志的成本。

六、把战斗结果、动画和 AI 评估分开

实现交互时,一个关键决定是同时保留两种状态:state 表示引擎已计算出的局面,frame 表示玩家此刻看到的画面。

例如,一次操作可能依次产生出招、伤害、回复和濒死事件。引擎可以先算完,界面再逐个播放。Normal、Fast、Instant 只是播放策略;跳过动画时直接显示最终状态,不能重新调用伤害计算。

玩家动作 → Worker 计算 → 最终局面 + 事件序列
                              ├─ 正常速度:逐事件显示
                              ├─ 快速模式:缩短事件间隔
                              └─ 跳过动画:显示最终局面

这样,战斗录像可以重用事件播放器;重新挑战则新建对局,让玩家再次提交动作。两者看起来相似,底层含义却不同。

入口 实现上的含义 是否改变正式战绩
正式每日挑战 使用当天配置,保存日志并结算 是
随机练习 使用独立练习种子 否
Replay today's challenge 用当天队伍新建练习对局 否
Watch battle replay 播放已产生的事件 否

这张表也是划分测试用例的依据:重新挑战应创建独立对局,观看录像应只读取已有事件,两者都不应再次写入正式战绩。按钮文案则要明确区分“重新做选择”和“重看过去”。

对手策略也要遵守同样的隔离原则。Red 的决策在提交玩家本次动作之前生成;评估在战斗副本上完成,计算伤害时保存并恢复副本的 PRNG 状态,避免评估过程推进正式战斗的随机序列。

当前策略是可解释的启发式评分,考虑伤害、命中率、击杀、回复和换人收益,不是训练出来的模型,也不是完整的最优搜索。对这个规模的游戏,先保证行为可追踪和可复现,比追求一个难以解释的对手更有价值。

七、一次真实踩坑:开发正常,生产压缩后战斗失败

项目开发记录里最值得保留的故障,发生在生产构建后的一个回合中。初始战场能加载,开发环境和普通 Node 测试也正常,但执行动作时界面出现:

getMoveRequestData is not a function

最终定位到战斗副本的序列化。固定版本的模拟器在 State.toRef 中使用 obj.constructor.name 识别对象,反序列化则匹配 Pokemon、Side、Battle 等固定字符串。生产压缩改变类名后,恢复出来的对象引用不完整,后面调用实例方法才出错。

这解释了为什么“首页加载成功”和“构建成功”都没能发现问题:只有真正推进回合、触发 AI 的副本评估,才走到这段逻辑。

修复放在单独的 cloneBattle.ts 适配层:复制前恢复序列化依赖的九类构造器名称,再调用 Battle.fromJSON(battle.toJSON())。没有关闭整个网站的压缩,也没有直接编辑安装目录里的依赖文件。

这是一项针对 @pkmn/sim@0.10.11 的兼容处理,会修改相关构造器的 name,不适合推广成通用做法。升级依赖时,需要重新检查上游是否仍依赖类名。

回归用例也不能只断言“没有抛错”。项目中的测试会主动改名复现原始失败,再验证适配后的副本能够推进回合,同时原战斗保持不变。

这次问题让我把生产验收的最小单位改成了一个完成的回合:按钮可点、请求有响应、日志有推进、画面更新且可以恢复,才算这条链路走通。Worker 把异常转成界面错误后,浏览器控制台甚至可能很安静,所以还要检查页面实际状态。

八、把界面可读性和生产部署一起纳入验收

工程正确不等于玩家能看懂。经典战场采用交叉布局:右上的对手,对应左上的血条;左下的我方,对应右下的血条。单纯还原位置,很容易让新玩家看错。

后续调整增加了 YOUR POKÉMON、RED · OPPONENT 和指向箭头,让角色与血条用同一事件里的 side 响应伤害和回复。动画开关与减少动态效果偏好也应该得到尊重。上面的开发截图就是这轮调整留下的记录。

音频也有类似问题。浏览器要求先获得用户交互,音乐才能启动;声音初始化失败不应该阻止战斗加载。于是音频作为独立能力处理,游戏不依赖它成功才能继续。

玩法、每日刷新和保存方式放在页面正文中,帮助玩家在初始化战斗前理解规则。这部分说明与战斗线程分离,避免引擎尚未就绪时,用户连基本操作和保存限制都无法了解。

部署使用 OpenNext 适配 Cloudflare Workers,静态资源单独提供。要区分网站服务器侧 Worker 与浏览器计算线程,线上要验证的也是完整链路:引擎资源是否加载、第一回合能否完成、刷新后能否恢复、结束后是否只计一次结果。

以下是我从项目现有验收记录中保留的检查重点;它们不代表所有设备和浏览器都已覆盖。

检查层 代表性问题
数据与规则 配招是否合法?Gen 3 类别、道具与状态是否正确?
可复现性 同配置同动作能否恢复?AI 评估是否影响原局?
保存与流程 刷新是否重复计分?练习是否污染正式结果?
交互与呈现 动画跳过是否一致?血条归属是否清楚?窄屏能否操作?
生产构建 压缩后能否执行回合、续局和结算?

历史验收里已有自动对战、移动端布局和本地生产构建记录,但没有据此得出人类胜率、低端设备性能或全浏览器兼容结论。对于下一轮开发,测量首次开战加载成本、补真机覆盖、评估旧存档迁移,比单纯增加装饰效果更值得优先投入。

九、这个项目带来的几条开发体会

Daily Red Pokemon 对我最大的提醒,是一个小玩法也需要明确的状态边界。什么决定今天的队伍,什么决定战斗结果,什么只是视觉反馈,什么允许重试——这些问题越早说清,后面的实现越少互相牵扯。

如果重新安排开发顺序,我仍会先完成可复现的一局,再接动作日志与恢复,然后做菜单、动画和声音。规则库负责已有规则,自己的代码聚焦每日玩法和用户体验;验证时同时保留静态检查与生产回合检查。

下一步是否增加账号与服务端校验,应由跨设备同步和成绩可信度的需求决定。本地存档降低了后端成本,但无法提供可信排行榜;若要扩展这两项能力,就需要重新设计日期来源、操作验证和结算流程。

相关文章
|
2天前
|
云安全 人工智能 运维
当攻击全面Agent化,你的安全防御能否跟上AI Speed?
多起客户侧AK泄露事件应急的真实观察
1199 0
|
25天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
9576 32
|
11天前
|
存储 人工智能 并行计算
大模型本地部署终端选型方法论:以 Qwen3.8-27B 为例的四档分层完整流程
本文提出一套大模型本地部署终端选型方法论:定约束、定档位、定框架、定参数四步决策法,配合入门、主力、质量、无损四档分层模型。以 Qwen3.8-27B 实测数据为例,逐环节解读显存、带宽、存储、散热、系统、预算等要素,给出面向不同预算的优选方案、决策自查清单与市场观察框架。文末前瞻 AI 笔记本的 CPU+GPU 与统一内存两条路线,论证四步决策法在新品类上的延续性。
|
9天前
|
人工智能 JSON 自然语言处理
2026 年 Jev 决策模型深度拆解:原理解读、实战测评与保姆级落地教程
有一款特殊AI模型在开发者圈子刷屏,它摒弃传统大模型擅长的对话聊天能力,专注做高速结构化决策,它就是TypeSafe AI推出的Jev模型。该模型由ChatGPT共同发明人Diogo Almeida主导研发,定位为**System One Model(系统一模型)**,对标人类大脑快速直觉判断的思维模式,在响应延迟、调用成本、结构化输出稳定性上相比传统生成式大模型有着巨大差异。本文会完整拆解Jev底层原理、三大核心原语能力、适用业务场景,同时提供可直接运行的curl、Python代码示例,并且结合多组实测数据,客观分析模型优势与能力边界,帮助普通开发者和AI应用从业者快速上手落地。
779 1
|
6天前
|
人工智能 JSON 自然语言处理
2026 年 Jev 决策模型全解析:核心原理拆解,性能实战测评与落地部署教程
有一款特殊AI模型在开发者圈子刷屏,它摒弃传统大模型擅长的对话聊天能力,专注做高速结构化决策,它就是TypeSafe AI推出的Jev模型。该模型由ChatGPT共同发明人Diogo Almeida主导研发,定位为System One Model(系统一模型),对标人类大脑快速直觉判断的思维模式,在响应延迟、调用成本、结构化输出稳定性上相比传统生成式大模型有着巨大差异。本文会完整拆解Jev底层原理、三大核心原语能力、适用业务场景,同时提供可直接运行的curl、Python代码示例,并且结合多组实测数据,客观分析模型优势与能力边界,帮助普通开发者和AI应用从业者快速上手落地。
515 0
|
23天前
|
人工智能 测试技术 API
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
Jev是TypeSafe AI推出的“系统一模型”,不生成文本,专做毫秒级结构化决策:Choice(多选)、Score(打分)、Noul(是非概率)。响应快193倍、成本低444倍,适合工单路由、内容审核、测试定级等高频判断场景。
2598 4
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
|
7天前
|
存储 人工智能 自然语言处理
千问办公官网入口链接在哪?2026年最新QwenWork测评、千问办公怎么样?一文看懂
千问办公(QwenWork)是阿里云推出的AI智能办公平台,提供网页端和阿里云官网双入口。支持PPT/Word/Excel生成、多模态处理、网页全栈搭建、钉钉深度集成等6大核心能力。个人版免费可用,高级版198元/月;企业版198元/人/月。新用户注册即赠2000积分。阿里千问办公QwenWork官网:https://t.aliyun.com/U/0VCTGt 阿里AI工作平台,一句话完成数据分析、PPT 生成、视频剪辑、网页搭建等复杂任务
1002 0
|
16天前
|
Windows
【保姆级】.NET Framework 3.5离线安装教程(2026最新实测)
.NET Framework 3.5是微软经典运行库,专为兼容老软件、老游戏设计。Win7自带,Win10/11需手动启用(在线或离线安装)。它与4.x系列并存不冲突,缺它会导致“找不到组件”报错。本文详解两种安装方式及常见问题解决,小白也能轻松上手。(239字)
2187 0

热门文章

最新文章