新建项目

This commit is contained in:
2026-07-29 10:50:33 +08:00
parent bfd3841cbd
commit 4f69b859a8
49 changed files with 5565 additions and 0 deletions

340
tests/game-service.test.js Normal file
View File

@@ -0,0 +1,340 @@
const assert = require("assert");
const storage = {};
global.wx = {
getStorageSync(key) {
return storage[key];
},
setStorageSync(key, value) {
storage[key] = JSON.parse(JSON.stringify(value));
},
removeStorageSync(key) {
delete storage[key];
}
};
const service = require("../services/game-service");
function testLegacyRoomMigration() {
storage.zmc_rooms = {
123456: {
code: "123456",
name: "旧房间",
players: [{ id: "legacy-host", name: "旧玩家", ready: true }]
},
broken: "invalid"
};
service.ensureSeedData();
const room = service.getRoom("123456");
assert.strictEqual(room.schemaVersion, 2);
assert.strictEqual(room.status, "waiting");
assert.strictEqual(room.settings.mode, "infection");
assert.strictEqual(room.players[0].status, "ready");
assert.deepStrictEqual(room.events, []);
assert.strictEqual(storage.zmc_rooms.broken, undefined);
assert.ok(service.getRoom("731204"));
}
function testBrokenTopLevelStorage() {
storage.zmc_rooms = [];
assert.doesNotThrow(() => service.ensureSeedData());
assert.ok(service.getRoom("731204"));
}
function testMainFlowAndReplay() {
const joined = service.joinRoom("731204", "测试玩家");
assert.strictEqual(joined.players.length, 4);
const preparing = service.startGame("731204");
assert.strictEqual(preparing.status, "preparing");
assert.strictEqual(
preparing.players.filter((player) => player.role === "seeker").length,
preparing.settings.seekerCount
);
let room = service.beginPlaying("731204");
assert.strictEqual(room.status, "playing");
while (room.status === "playing") {
const target = room.players.find(
(player) => player.role === "hider" && player.status === "hiding"
);
const captureCode = service.getCaptureCode(room.code, target.id);
assert.strictEqual(captureCode.length, 4);
room = service.capture(room.code, target.id, captureCode);
}
assert.strictEqual(room.status, "finished");
assert.strictEqual(room.winner, "seekers");
const replay = service.resetRoom(room.code);
assert.strictEqual(replay.status, "waiting");
assert.ok(replay.players.every((player) => player.role === ""));
assert.ok(replay.players.every((player) => player.captures === 0));
assert.strictEqual(replay.resultSnapshot, null);
assert.strictEqual(replay.nextClueAt, null);
assert.strictEqual(replay.captureTokensUsed.length, 0);
}
function validPayload(overrides = {}) {
return {
name: "第二天测试房间",
mode: "classic",
durationMinutes: 30,
prepareMinutes: 3,
maxPlayers: 4,
seekerCount: 1,
clueIntervalMinutes: 5,
radiusMeters: 500,
skillsEnabled: true,
...overrides
};
}
function testCreateValidation() {
assert.throws(
() => service.createRoom(validPayload({ seekerCount: 4 })),
/初始寻找者必须少于房间人数上限/
);
assert.throws(
() => service.createRoom(validPayload({ durationMinutes: 10 })),
/游戏时长应为 1590 分钟/
);
assert.throws(
() => service.createRoom(validPayload({ name: " " })),
/请填写房间名称/
);
const room = service.createRoom(validPayload());
assert.strictEqual(room.name, "第二天测试房间");
assert.strictEqual(room.settings.seekerCount, 1);
}
function testLobbyPermissionsAndReadiness() {
const room = service.createRoom(validPayload({ maxPlayers: 6 }));
service.addDemoPlayers(room.code, 4);
assert.throws(
() => service.startGame(room.code, "not-the-host"),
/只有房主可以开始游戏/
);
const rooms = storage.zmc_rooms;
rooms[room.code].players[1].ready = false;
rooms[room.code].players[1].status = "idle";
storage.zmc_rooms = rooms;
assert.throws(() => service.startGame(room.code), /还有玩家尚未准备好/);
service.toggleReady(room.code, rooms[room.code].players[1].id);
const started = service.startGame(room.code);
assert.strictEqual(started.status, "preparing");
assert.throws(() => service.startGame(room.code), /不能重复开始/);
}
function testJoinStatusMessages() {
assert.throws(() => service.joinRoom("12", "玩家"), /六位房间码/);
assert.throws(() => service.joinRoom("999999", "玩家"), /没有找到/);
const room = service.createRoom(validPayload());
storage.zmc_rooms[room.code].status = "finished";
assert.throws(() => service.joinRoom(room.code, "玩家"), /已经结束/);
storage.zmc_rooms[room.code].status = "cancelled";
assert.throws(() => service.joinRoom(room.code, "玩家"), /房主取消/);
}
function prepareRoom(overrides = {}) {
const room = service.createRoom(validPayload({ maxPlayers: 6, ...overrides }));
service.addDemoPlayers(room.code, 4);
return room.code;
}
function testStateTransitionsAndDemoTiming() {
const code = prepareRoom();
assert.throws(() => service.beginPlaying(code), /不能进入追逃阶段/);
assert.throws(() => service.revealClue(code), /不能广播线索/);
assert.throws(() => service.useSkill(code, "radar"), /不能使用技能/);
const preparing = service.startGame(code);
assert.strictEqual(
preparing.phaseEndsAt - preparing.phaseStartedAt,
preparing.settings.prepareMinutes * 1000
);
const scheduledStartAt = preparing.phaseEndsAt;
const playing = service.beginPlaying(code);
assert.strictEqual(playing.phaseStartedAt, scheduledStartAt);
assert.strictEqual(playing.phaseEndsAt - playing.phaseStartedAt, 180000);
assert.strictEqual(playing.nextClueAt - playing.phaseStartedAt, 15000);
assert.throws(() => service.beginPlaying(code), /不能进入追逃阶段/);
const finished = service.finishGame(code, "hiders");
assert.strictEqual(finished.status, "finished");
assert.throws(() => service.finishGame(code), /不能结束游戏/);
}
function testConfiguredProductionTiming() {
const code = prepareRoom({
durationMinutes: 20,
prepareMinutes: 5,
clueIntervalMinutes: 8
});
storage.zmc_rooms[code].runtime.demoAccelerated = false;
const preparing = service.startGame(code);
assert.strictEqual(preparing.phaseEndsAt - preparing.phaseStartedAt, 5 * 60 * 1000);
const playing = service.beginPlaying(code);
assert.strictEqual(playing.phaseEndsAt - playing.phaseStartedAt, 20 * 60 * 1000);
assert.strictEqual(playing.nextClueAt - playing.phaseStartedAt, 8 * 60 * 1000);
}
function startPlayableRoom(overrides = {}) {
const code = prepareRoom(overrides);
const preparing = service.startGame(code);
return service.beginPlaying(preparing.code);
}
function testRoleCountsAndGameModes() {
let room = startPlayableRoom({ mode: "classic", maxPlayers: 8, seekerCount: 3 });
assert.strictEqual(room.players.length, 4);
assert.strictEqual(room.players.filter((player) => player.role === "seeker").length, 3);
let target = room.players.find((player) => player.role === "hider");
let token = service.getCaptureCode(room.code, target.id);
room = service.capture(room.code, target.id, token);
target = room.players.find((player) => player.id === target.id);
assert.strictEqual(target.role, "hider");
assert.strictEqual(target.status, "caught");
assert.strictEqual(room.status, "finished");
room = startPlayableRoom({ mode: "infection", seekerCount: 1 });
target = room.players.find((player) => player.role === "hider");
token = service.getCaptureCode(room.code, target.id);
room = service.capture(room.code, target.id, token);
target = room.players.find((player) => player.id === target.id);
assert.strictEqual(target.role, "seeker");
assert.strictEqual(target.status, "seeking");
}
function testPersistedSkills() {
let room = startPlayableRoom({ mode: "classic", skillsEnabled: true });
const seeker = room.players.find((player) => player.role === "seeker");
const hider = room.players.find((player) => player.role === "hider");
room = service.useSkill(room.code, "radar", seeker.id);
assert.strictEqual(
room.players.find((player) => player.id === seeker.id).skillsUsed.radar,
1
);
assert.throws(
() => service.useSkill(room.code, "radar", seeker.id),
/已经使用过/
);
room = service.useSkill(room.code, "silent", hider.id);
assert.strictEqual(
room.players.find((player) => player.id === hider.id).silentPending,
true
);
assert.ok(!room.events[0].message.includes(hider.name));
room = service.revealClue(room.code);
assert.strictEqual(
room.players.find((player) => player.id === hider.id).silentPending,
false
);
assert.match(room.events[0].message, /静默已生效/);
assert.throws(
() => service.useSkill(room.code, "radar", hider.id),
/当前身份不能使用/
);
room = startPlayableRoom({ skillsEnabled: false });
assert.throws(() => service.useSkill(room.code, "radar"), /未启用角色技能/);
}
function testCaptureTokenAndPermissions() {
const now = 1800000000000;
let room = startPlayableRoom({ mode: "classic" });
const seeker = room.players.find((player) => player.role === "seeker");
const target = room.players.find((player) => player.role === "hider");
const anotherHider = room.players.find(
(player) => player.role === "hider" && player.id !== target.id
);
const token = service.getCaptureCode(room.code, target.id, now);
assert.strictEqual(token.length, 4);
assert.notStrictEqual(service.getCaptureCode(room.code, anotherHider.id, now), token);
assert.notStrictEqual(service.getCaptureCode(room.code, target.id, now + 30000), token);
assert.throws(
() => service.capture(room.code, target.id, token, anotherHider.id, now),
/只有正在游戏的寻找者/
);
assert.throws(
() => service.capture(room.code, target.id, token, seeker.id, now + 30000),
/口令不正确或已经失效/
);
room = service.capture(room.code, target.id, token, seeker.id, now);
assert.strictEqual(room.players.find((player) => player.id === seeker.id).captures, 1);
assert.throws(
() => service.capture(room.code, target.id, token, seeker.id, now),
/口令已经使用过/
);
}
function testPauseResumeAndHostFinish() {
let room = startPlayableRoom({ mode: "classic" });
assert.throws(() => service.pauseGame(room.code, "not-host"), /只有房主/);
const phaseEndsAt = room.phaseEndsAt;
room = service.pauseGame(room.code);
assert.strictEqual(room.status, "paused");
assert.ok(room.pausedRemainingMs > 0);
assert.ok(room.pausedRemainingMs <= phaseEndsAt - room.pausedAt + 5);
assert.throws(() => service.pauseGame(room.code), /只有追逃阶段可以暂停/);
room = service.resumeGame(room.code);
assert.strictEqual(room.status, "playing");
assert.strictEqual(room.pausedRemainingMs, 0);
assert.throws(() => service.resumeGame(room.code), /没有暂停/);
assert.throws(
() => service.finishGame(room.code, "", "not-host", "host_ended"),
/只有房主可以提前结束/
);
room = service.finishGame(room.code, "", service.USER_ID, "host_ended");
assert.strictEqual(room.status, "finished");
assert.strictEqual(room.finishReason, "host_ended");
}
function testSafetyQuitAndStableResult() {
let room = startPlayableRoom({ mode: "classic", seekerCount: 1 });
const hiders = room.players.filter((player) => player.role === "hider");
room = service.quitGame(room.code, hiders[0].id);
assert.strictEqual(
room.players.find((player) => player.id === hiders[0].id).status,
"quit"
);
assert.strictEqual(room.status, "playing");
for (let index = 1; index < hiders.length; index += 1) {
room = service.quitGame(room.code, hiders[index].id);
}
assert.strictEqual(room.status, "finished");
assert.strictEqual(room.finishReason, "all_hiders_left");
assert.ok(room.resultSnapshot);
assert.strictEqual(room.resultSnapshot.players.length, room.players.length);
assert.deepStrictEqual(
room.resultSnapshot.players.map((player) => player.id),
service.getRoom(room.code).resultSnapshot.players.map((player) => player.id)
);
assert.throws(() => service.quitGame(room.code, hiders[0].id), /当前不能退出/);
}
testLegacyRoomMigration();
testBrokenTopLevelStorage();
testMainFlowAndReplay();
testCreateValidation();
testLobbyPermissionsAndReadiness();
testJoinStatusMessages();
testStateTransitionsAndDemoTiming();
testConfiguredProductionTiming();
testRoleCountsAndGameModes();
testPersistedSkills();
testCaptureTokenAndPermissions();
testPauseResumeAndHostFinish();
testSafetyQuitAndStableResult();
console.log("game-service tests passed");

View File

@@ -0,0 +1,28 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const pagesRoot = path.join(__dirname, "..", "pages");
const pageNames = fs.readdirSync(pagesRoot);
pageNames.forEach((pageName) => {
const pageRoot = path.join(pagesRoot, pageName);
const jsPath = path.join(pageRoot, `${pageName}.js`);
const wxmlPath = path.join(pageRoot, `${pageName}.wxml`);
if (!fs.existsSync(jsPath) || !fs.existsSync(wxmlPath)) return;
const js = fs.readFileSync(jsPath, "utf8");
const wxml = fs.readFileSync(wxmlPath, "utf8");
const handlers = [...wxml.matchAll(/(?:bind|catch)[a-z]+="([A-Za-z0-9_]+)"/g)]
.map((match) => match[1]);
handlers.forEach((handler) => {
assert.match(
js,
new RegExp(`\\n\\s{2}${handler}\\s*\\(`),
`${pageName}.wxml references missing handler ${handler}`
);
});
});
console.log("page contract tests passed");

View File

@@ -0,0 +1,38 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const projectRoot = path.join(__dirname, "..");
const appConfig = JSON.parse(fs.readFileSync(path.join(projectRoot, "app.json"), "utf8"));
const projectConfig = JSON.parse(
fs.readFileSync(path.join(projectRoot, "project.config.json"), "utf8")
);
assert.strictEqual(projectConfig.compileType, "miniprogram");
assert.ok(Array.isArray(appConfig.pages) && appConfig.pages.length > 0);
assert.strictEqual(new Set(appConfig.pages).size, appConfig.pages.length, "app pages must be unique");
appConfig.pages.forEach((pagePath) => {
["js", "json", "wxml", "wxss"].forEach((extension) => {
const file = path.join(projectRoot, `${pagePath}.${extension}`);
assert.ok(fs.existsSync(file), `missing page file: ${pagePath}.${extension}`);
});
});
[
"README.md",
"docs/PRD.md",
"docs/TECHNICAL_DESIGN.md",
"docs/ONE_WEEK_MVP_PLAN.md",
"docs/DEVICE_TEST_CHECKLIST.md",
"docs/DEMO_GUIDE.md",
"docs/TEST_REPORT.md",
"docs/RELEASE_NOTES.md"
].forEach((relativePath) => {
assert.ok(fs.existsSync(path.join(projectRoot, relativePath)), `missing deliverable: ${relativePath}`);
});
const appSource = fs.readFileSync(path.join(projectRoot, "app.js"), "utf8");
assert.match(appSource, /version:\s*"0\.1\.0-mvp"/);
console.log("release readiness tests passed");

17
tests/run-all.js Normal file
View File

@@ -0,0 +1,17 @@
const path = require("path");
const { spawnSync } = require("child_process");
const testFiles = [
"game-service.test.js",
"page-contract.test.js",
"release-readiness.test.js"
];
testFiles.forEach((testFile) => {
const result = spawnSync(process.execPath, [path.join(__dirname, testFile)], {
stdio: "inherit"
});
if (result.status !== 0) process.exit(result.status || 1);
});
console.log("all automated checks passed");