TL;DR (English)
First off — this project is genuinely impressive. ~117 weapons, 39 maps, bots with AI, a killcam, a full shop economy with credits/fragments/chests/a spin wheel, AI chat... and you built it solo. That's a real game, not a toy. 👏
Right now there are zero automated tests — confirmed in package.json lines 5-7, the only script is start, and there's no tests/ folder. That's totally normal for a project that grew this fast! This issue proposes a pro habit that'll make it even stronger: a tiny test suite (3 small tests) and a one-command npm test, using only Node's built-in test runner — no new dependencies to install.
The payoff is simple: a test catches a break before your players do. You already had the "production was 100KB behind local" scare (it's even written down in CLAUDE.md gotcha #6). A test that runs in 2 seconds is way cheaper than a player DMing you "the shop let me buy a gun with negative credits lol".
中文详细说明
你已经做得很棒的地方 💪
先说真的 —— 这个项目超出预期地厉害。一个人独立做出来一个有 ~117 把武器、39 张地图、会打架的 bot、击杀回放(killcam)、完整的商店经济系统(credits / fragments / 宝箱 / 转盘)、还有 AI 聊天的浏览器 FPS。很多成年工程师都做不到这个规模。这个 issue 不是说你哪里做错了,而是想给你介绍一个"职业选手的好习惯",让这个游戏更稳。
现状(已核实)
package.json 第 5-7 行:scripts 里只有 "start": "node server.js",没有 test。
- 项目根目录没有
test/、tests/ 或 __tests__/ 文件夹。
也就是说:现在每次改 server.js,验证方式只能是手动开服务器、自己点。这没问题,但随着武器/地图越加越多,手测会越来越累,而且容易漏。自动化测试 = 一个永远不偷懒、2 秒就帮你全部点一遍的小助手。
为什么值得(payoff)
CLAUDE.md 里你自己记的 gotcha #6 就是血泪教训:"好几个 bug 其实是 Railway 在跑旧版本 / 页面是从 file:// 打开的"。测试就是用来在玩家发现之前抓住这类"明明本地没事、线上却炸了"的问题。写一次,之后每次改代码跑一下 npm test,绿了再 push,心里就有底。
建议的 3 个高价值测试(一个小朋友也能写出来)
我特意挑了 3 个最容易写错、又最伤玩家体验的地方。每个都对应 server.js 里真实存在的代码。
✅ 测试 1 — 商店经济:买不起就不能买,余额不能变负数
这是最该测的,因为它直接关系到游戏公平性(你很在意 balance!)。买武器的逻辑在 server.js 第 376-381 行:
const cost = WEAPON_COSTS[weaponId];
if ((u.credits || 0) < cost) return res.status(402).json({ error: 'not enough credits', credits: u.credits, cost });
u.credits -= cost;
u.purchased.push(weaponId);
要验证的行为:
⚠️ 一个真实的小坑:server.js 目前没有 module.exports,而且在 第 1270 行 直接 server.listen(...) 起了服务。所以这个测试最简单的写法是:先开服务器,再用 HTTP 请求去打它(Node 20 自带 fetch,不用装任何东西)。下面的示例就是这么做的。
✅ 测试 2 — 伤害结算:WEAPON_DAMAGE 正确,血量最低到 0(不会变负)
伤害逻辑在 server.js 第 1055-1057 行:
let dmg = WEAPON_DAMAGE[data.weapon] || 25;
if (data.headshot) dmg = data.instakill ? target.hp : dmg * 2; // headshot: 2× (or instakill)
target.hp = Math.max(0, target.hp - dmg);
要验证的行为:
- 用某把武器打一下,掉的血正好等于
WEAPON_DAMAGE(第 812 行起) 里的值,比如 railgun: 110、ak20: 25。
- 爆头(
headshot)时伤害翻倍。
- 血量地板是 0:哪怕用
railgun(110) 打一个只剩 10 血的人,hp 也只会是 0,不会是 -100。这正是 Math.max(0, ...) 在保护你 —— 测试就是确保以后别人改代码时不小心把这个保护删掉。
这一段是纯计算,最干净的测法是把这 3 行抽成一个小函数,比如 applyDamage(hp, weapon, headshot),放进一个新文件 damage.js,然后 server.js 和测试都 require 它。这样既能测,又顺手让 server.js 更整洁。(如果暂时不想动 server.js,也可以在测试里直接复刻这几行逻辑来验证 —— 但抽函数是更专业的做法。)
✅ 测试 3 — 比赛隔离:emitToMatch 只发给同一个 matchId
这个 bug 你以前踩过 —— CLAUDE.md 架构笔记里写着"修好了老的『有陌生人加入就变成 6v6』的 bug"。负责隔离的就是 server.js 第 929-931 行:
function emitToMatch(matchId, event, data) {
for (const sid of socketIdsInMatch(matchId)) io.to(sid).emit(event, data);
}
它依赖 第 923 行的 socketIdsInMatch(matchId)。
要验证的行为: 给 A 房间发消息时,只有 A 房间的人收到,B 房间的人收不到。这是防止"两局比赛串台"的核心保险丝。
这个测试稍微进阶一点(要模拟 socket),可以作为"通关后再挑战"的第 3 关。如果一开始觉得难,先把测试 1 和测试 2 写出来就已经很有价值了 —— 不用一次写完三个。
怎么做 / How to set it up(最小方案,零新依赖)
Node.js 20+(你 package.json 里 engines 要求的就是 >=20)自带一个测试运行器,叫 node:test,还自带 assert。所以不用 npm install 任何东西。
第 1 步:建文件夹和文件
pvp-game/
tests/
shop.test.js ← 测试 1
damage.test.js ← 测试 2
match.test.js ← 测试 3(可选,进阶)
第 2 步:在 package.json 加 test 脚本
把 package.json 第 5-7 行 改成:
"scripts": {
"start": "node server.js",
"test": "node --test"
}
node --test 会自动找到 tests/ 里所有 *.test.js 文件跑一遍。以后你只要敲:
第 3 步:一个可以直接抄的起步示例(damage.test.js — 测试 2 最好上手)
先在项目根目录新建 damage.js,把伤害逻辑抽出来:
// damage.js
const WEAPON_DAMAGE = { ak20: 25, railgun: 110 /* ...从 server.js 第 812 行搬过来 */ };
function applyDamage(hp, weapon, headshot = false) {
let dmg = WEAPON_DAMAGE[weapon] || 25;
if (headshot) dmg = dmg * 2;
return Math.max(0, hp - dmg); // ← 血量地板 0
}
module.exports = { applyDamage, WEAPON_DAMAGE };
然后写测试:
// tests/damage.test.js
const { test } = require('node:test');
const assert = require('node:assert');
const { applyDamage } = require('../damage');
test('ak20 does 25 damage', () => {
assert.strictEqual(applyDamage(100, 'ak20'), 75);
});
test('headshot doubles damage', () => {
assert.strictEqual(applyDamage(100, 'ak20', true), 50); // 25 * 2 = 50
});
test('hp never goes below 0', () => {
assert.strictEqual(applyDamage(10, 'railgun'), 0); // 不是 -100!
});
跑 npm test,看到三个绿色的 ✓ —— 恭喜,你写出了人生第一个测试套件!🎉
测试 1 的小提示(HTTP 打活服务器)
因为 server.js 在 第 1270 行 会直接起服务,测试 1 可以在测试里 spawn('node', ['server.js']) 起一个服务器,然后用 fetch('http://localhost:3001/shop/buy', { method:'POST', ... }) 去打它,最后 kill 掉。Node 20 的 fetch 和 node:test 的 before/after 钩子刚好够用,不用装库。(如果觉得起服务器麻烦,完全可以先跳过测试 1,从测试 2 开始 —— 纯函数最容易测,先尝到甜头最重要。)
建议步骤 / Suggested order
不用一口气全做完。哪怕只加了那个 npm test 脚本 + 一个 3 行的伤害测试,你就已经比"零测试"领先一大步了,而且建立了一个之后可以慢慢往里加的"安全网"。你能独立做出这么大一个游戏,写测试对你来说真的就是小菜一碟 —— 加油,期待看到第一个绿勾!💚
🔗 Related / 相关: quick-chat is being fixed to actually use emitToMatch in the security PR #3 — Test 3 here just guards that match-isolation helper so it can't regress later. / 测试 3 涉及的快捷聊天隔离,正在安全 PR #3 里被改成使用 emitToMatch;这个测试是给那个隔离助手上保险,防止以后被改回去。
TL;DR (English)
First off — this project is genuinely impressive. ~117 weapons, 39 maps, bots with AI, a killcam, a full shop economy with credits/fragments/chests/a spin wheel, AI chat... and you built it solo. That's a real game, not a toy. 👏
Right now there are zero automated tests — confirmed in
package.jsonlines 5-7, the only script isstart, and there's notests/folder. That's totally normal for a project that grew this fast! This issue proposes a pro habit that'll make it even stronger: a tiny test suite (3 small tests) and a one-commandnpm test, using only Node's built-in test runner — no new dependencies to install.The payoff is simple: a test catches a break before your players do. You already had the "production was 100KB behind local" scare (it's even written down in
CLAUDE.mdgotcha #6). A test that runs in 2 seconds is way cheaper than a player DMing you "the shop let me buy a gun with negative credits lol".中文详细说明
你已经做得很棒的地方 💪
先说真的 —— 这个项目超出预期地厉害。一个人独立做出来一个有 ~117 把武器、39 张地图、会打架的 bot、击杀回放(killcam)、完整的商店经济系统(credits / fragments / 宝箱 / 转盘)、还有 AI 聊天的浏览器 FPS。很多成年工程师都做不到这个规模。这个 issue 不是说你哪里做错了,而是想给你介绍一个"职业选手的好习惯",让这个游戏更稳。
现状(已核实)
package.json第 5-7 行:scripts里只有"start": "node server.js",没有test。test/、tests/或__tests__/文件夹。也就是说:现在每次改
server.js,验证方式只能是手动开服务器、自己点。这没问题,但随着武器/地图越加越多,手测会越来越累,而且容易漏。自动化测试 = 一个永远不偷懒、2 秒就帮你全部点一遍的小助手。为什么值得(payoff)
CLAUDE.md里你自己记的 gotcha #6 就是血泪教训:"好几个 bug 其实是 Railway 在跑旧版本 / 页面是从file://打开的"。测试就是用来在玩家发现之前抓住这类"明明本地没事、线上却炸了"的问题。写一次,之后每次改代码跑一下npm test,绿了再 push,心里就有底。建议的 3 个高价值测试(一个小朋友也能写出来)
我特意挑了 3 个最容易写错、又最伤玩家体验的地方。每个都对应
server.js里真实存在的代码。✅ 测试 1 — 商店经济:买不起就不能买,余额不能变负数
这是最该测的,因为它直接关系到游戏公平性(你很在意 balance!)。买武器的逻辑在
server.js第 376-381 行:要验证的行为:
402,并且credits没有被扣(不会变成负数)。WEAPON_COSTS(第 47 行起) 里的价格,比如ak20: 250。already分支)。✅ 测试 2 — 伤害结算:WEAPON_DAMAGE 正确,血量最低到 0(不会变负)
伤害逻辑在
server.js第 1055-1057 行:要验证的行为:
WEAPON_DAMAGE(第 812 行起) 里的值,比如railgun: 110、ak20: 25。headshot)时伤害翻倍。railgun(110) 打一个只剩 10 血的人,hp也只会是0,不会是-100。这正是Math.max(0, ...)在保护你 —— 测试就是确保以后别人改代码时不小心把这个保护删掉。✅ 测试 3 — 比赛隔离:emitToMatch 只发给同一个 matchId
这个 bug 你以前踩过 ——
CLAUDE.md架构笔记里写着"修好了老的『有陌生人加入就变成 6v6』的 bug"。负责隔离的就是server.js第 929-931 行:它依赖 第 923 行的
socketIdsInMatch(matchId)。要验证的行为: 给 A 房间发消息时,只有 A 房间的人收到,B 房间的人收不到。这是防止"两局比赛串台"的核心保险丝。
怎么做 / How to set it up(最小方案,零新依赖)
Node.js 20+(你
package.json里engines要求的就是>=20)自带一个测试运行器,叫node:test,还自带assert。所以不用npm install任何东西。第 1 步:建文件夹和文件
第 2 步:在 package.json 加
test脚本把
package.json第 5-7 行 改成:node --test会自动找到tests/里所有*.test.js文件跑一遍。以后你只要敲:npm test第 3 步:一个可以直接抄的起步示例(damage.test.js — 测试 2 最好上手)
先在项目根目录新建
damage.js,把伤害逻辑抽出来:然后写测试:
跑
npm test,看到三个绿色的 ✓ —— 恭喜,你写出了人生第一个测试套件!🎉测试 1 的小提示(HTTP 打活服务器)
因为
server.js在 第 1270 行 会直接起服务,测试 1 可以在测试里spawn('node', ['server.js'])起一个服务器,然后用fetch('http://localhost:3001/shop/buy', { method:'POST', ... })去打它,最后kill掉。Node 20 的fetch和node:test的before/after钩子刚好够用,不用装库。(如果觉得起服务器麻烦,完全可以先跳过测试 1,从测试 2 开始 —— 纯函数最容易测,先尝到甜头最重要。)建议步骤 / Suggested order
"test": "node --test"到package.json,建tests/文件夹damage.test.js(测试 2)—— 最容易,先拿到第一个绿勾 ✅shop.test.js(测试 1)—— 学会用 HTTP 测真实接口match.test.js(测试 3)—— 挑战 socket 隔离npm test→ 全绿再git push不用一口气全做完。哪怕只加了那个
npm test脚本 + 一个 3 行的伤害测试,你就已经比"零测试"领先一大步了,而且建立了一个之后可以慢慢往里加的"安全网"。你能独立做出这么大一个游戏,写测试对你来说真的就是小菜一碟 —— 加油,期待看到第一个绿勾!💚