Initial commit

This commit is contained in:
maths81
2026-06-11 22:13:55 +08:00
commit 96a6a99963
13 changed files with 1358 additions and 0 deletions

127
test/game.test.js Normal file
View File

@@ -0,0 +1,127 @@
import test from "node:test";
import assert from "node:assert/strict";
import { GameStore } from "../src/game.js";
const setup = () => {
const store = new GameStore({ random: () => 0.42 });
const first = store.createRoom("甲");
const second = store.joinRoom(first.room.id, "乙");
const third = store.joinRoom(first.room.id, "丙");
store.start(first.room.id, first.playerId);
return { store, room: first.room, ids: [first.playerId, second.playerId, third.playerId] };
};
test("deals two hidden cards and four cards to every player", () => {
const { room } = setup();
assert.equal(room.hidden.length, 2);
assert.deepEqual(room.players.map((player) => player.hand.length), [4, 4, 4]);
assert.equal([...room.hidden, ...room.players.flatMap((player) => player.hand)].length, 14);
});
test("a room view never reveals another player's hand or hidden cards", () => {
const { store, room, ids } = setup();
const view = store.view(room, ids[0]);
assert.equal(view.hidden, null);
assert.ok(Array.isArray(view.players[0].hand));
assert.equal(view.players[1].hand, undefined);
assert.equal(view.players[2].hand, undefined);
});
test("one-card request logs both requested values but hides the transferred value", () => {
const { store, room, ids } = setup();
room.players[1].hand = [2, 3, 4, 5];
store.requestCards(room.id, ids[0], { targetId: ids[1], values: [2, 3], mode: "one" });
assert.match(room.logs.at(-1).message, /“取一张”请求/);
assert.match(room.logs.at(-1).message, /数字 2 或 3/);
const result = store.answerRequest(room.id, ids[1], { value: 3 });
assert.equal(room.players[0].hand.at(-1) >= 2, true);
assert.equal(room.players[1].hand.includes(3), false);
assert.match(room.logs.at(-1).message, /交出了 1 张牌/);
assert.doesNotMatch(room.logs.at(-1).message, /数字 3/);
assert.equal(result.privateEvents.length, 2);
});
test("all-card request can be declined unless target holds both values", () => {
const { store, room, ids } = setup();
room.players[1].hand = [2, 2, 4, 5];
store.requestCards(room.id, ids[0], { targetId: ids[1], values: [2, 3], mode: "all" });
assert.match(room.logs.at(-1).message, /“取同号全部”请求/);
assert.match(room.logs.at(-1).message, /数字 2 或 3/);
assert.doesNotThrow(() => store.answerRequest(room.id, ids[1], { value: null }));
});
test("a player cannot request the same unordered pair on consecutive personal turns", () => {
const { store, room, ids } = setup();
room.players.forEach((player) => {
player.hand = [4, 4, 5, 5];
});
store.requestCards(room.id, ids[0], { targetId: ids[1], values: [2, 3], mode: "one" });
store.answerRequest(room.id, ids[1], { value: null });
store.requestCards(room.id, ids[1], { targetId: ids[2], values: [2, 4], mode: "one" });
store.answerRequest(room.id, ids[2], { value: 4 });
store.requestCards(room.id, ids[2], { targetId: ids[0], values: [3, 5], mode: "one" });
store.answerRequest(room.id, ids[0], { value: 5 });
assert.throws(
() => store.requestCards(room.id, ids[0], { targetId: ids[2], values: [3, 2], mode: "all" }),
/不能连续两个回合请求相同的两种牌/
);
assert.doesNotThrow(() =>
store.requestCards(room.id, ids[0], { targetId: ids[2], values: [2, 4], mode: "all" })
);
});
test("correct guess scores and finishes game", () => {
const { store, room, ids } = setup();
store.proposeGuess(room.id, ids[0], room.hidden);
store.answerGuess(room.id, ids[1], { choice: "agree", guesserId: ids[0] });
const result = store.answerGuess(room.id, ids[2], { choice: "agree", guesserId: ids[0] });
assert.equal(result.correct, true);
assert.equal(room.phase, "finished");
assert.equal(room.players[0].score, 3);
assert.equal(room.players[1].score, 1);
assert.equal(room.players[2].score, 1);
});
test("a later player can agree with a counter guess", () => {
const { store, room, ids } = setup();
const wrongGuess = room.hidden[0] === 2 && room.hidden[1] === 2 ? [2, 3] : [2, 2];
store.proposeGuess(room.id, ids[0], wrongGuess);
store.answerGuess(room.id, ids[1], { choice: "counter", values: room.hidden });
const result = store.answerGuess(room.id, ids[2], { choice: "agree", guesserId: ids[1] });
assert.equal(result.correct, true);
assert.equal(room.players[0].score, -1);
assert.equal(room.players[1].score, 3);
assert.equal(room.players[2].score, 1);
});
test("the only abandoning player must agree or counter on a second action", () => {
const { store, room, ids } = setup();
store.proposeGuess(room.id, ids[0], room.hidden);
store.answerGuess(room.id, ids[1], { choice: "abandon" });
const firstRound = store.answerGuess(room.id, ids[2], { choice: "agree", guesserId: ids[0] });
assert.equal(firstRound.resolved, false);
assert.equal(room.pending.forcedPlayerId, ids[1]);
assert.throws(
() => store.answerGuess(room.id, ids[1], { choice: "abandon" }),
/必须选择认同或另猜/
);
const result = store.answerGuess(room.id, ids[1], { choice: "agree", guesserId: ids[0] });
assert.equal(result.correct, true);
assert.equal(room.players[1].score, 1);
});
test("if both later players abandon, a wrong initiator is the only player eliminated", () => {
const { store, room, ids } = setup();
const wrongGuess = room.hidden[0] === 2 && room.hidden[1] === 2 ? [2, 3] : [2, 2];
store.proposeGuess(room.id, ids[0], wrongGuess);
store.answerGuess(room.id, ids[1], { choice: "abandon" });
const result = store.answerGuess(room.id, ids[2], { choice: "abandon" });
assert.equal(result.correct, false);
assert.equal(room.players[0].eliminated, true);
assert.equal(room.players[1].eliminated, false);
assert.equal(room.players[2].eliminated, false);
assert.equal(room.currentPlayerId, undefined);
assert.equal(store.view(room, ids[1]).currentPlayerId, ids[1]);
});