217 lines
8.1 KiB
JavaScript
217 lines
8.1 KiB
JavaScript
// 引擎單元測試(用 Node 內建 assert,無外部依賴)
|
||
// 使用:node scripts/test_engine.js
|
||
const assert = require('assert');
|
||
const GameEngine = require('../server/game/engine.js');
|
||
|
||
const results = [];
|
||
function test(name, fn) {
|
||
try { fn(); results.push(['ok', name]); }
|
||
catch (e) { results.push(['fail', name, e.message]); }
|
||
}
|
||
|
||
// 1) 發牌與牌組
|
||
test('init 4 人:每人 5 張、目標牌正確、round=1', () => {
|
||
const e = new GameEngine();
|
||
const st = e.init(['p1', 'p2', 'p3', 'p4']);
|
||
assert.strictEqual(st.players.length, 4);
|
||
for (const p of st.players) assert.strictEqual(p.handSize, 5);
|
||
assert.ok(['Q', 'K', 'A'].includes(st.targetCard));
|
||
assert.strictEqual(st.roundNumber, 1);
|
||
assert.ok(st.currentPlayerId);
|
||
});
|
||
|
||
test('init 8 人:每人 5 張', () => {
|
||
const e = new GameEngine();
|
||
const st = e.init(Array.from({ length: 8 }, (_, i) => 'p' + i));
|
||
assert.strictEqual(st.players.length, 8);
|
||
for (const p of st.players) assert.strictEqual(p.handSize, 5);
|
||
});
|
||
|
||
test('牌組總張數 = 人數 x5(2/3/5/8)', () => {
|
||
for (const n of [2, 3, 5, 8]) {
|
||
const e = new GameEngine();
|
||
const deck = e.createDeck(n);
|
||
assert.strictEqual(deck.length, n * 5, `n=${n}`);
|
||
}
|
||
});
|
||
|
||
// 2) 出牌驗證
|
||
test('出牌:0 張 → 錯誤', () => {
|
||
const e = new GameEngine(); e.init(['a', 'b']);
|
||
assert.ok(e.playCard(e.currentPlayerId, []).error);
|
||
});
|
||
test('出牌:4 張 → 錯誤', () => {
|
||
const e = new GameEngine(); e.init(['a', 'b']);
|
||
assert.ok(e.playCard(e.currentPlayerId, [0, 1, 2, 3]).error);
|
||
});
|
||
test('出牌:索引超出範圍 → 錯誤', () => {
|
||
const e = new GameEngine(); e.init(['a', 'b']);
|
||
assert.ok(e.playCard(e.currentPlayerId, [99]).error);
|
||
});
|
||
test('出牌:重複索引 → 錯誤', () => {
|
||
const e = new GameEngine(); e.init(['a', 'b']);
|
||
assert.ok(e.playCard(e.currentPlayerId, [0, 0]).error);
|
||
});
|
||
test('出牌:非你的回合 → 錯誤', () => {
|
||
const e = new GameEngine(); e.init(['a', 'b']);
|
||
const other = Array.from(e.players.keys()).find((k) => k !== e.currentPlayerId);
|
||
assert.ok(e.playCard(other, [0]).error);
|
||
});
|
||
test('出牌:合法出 1-3 張並移除、換下一位', () => {
|
||
const e = new GameEngine(); e.init(['a', 'b', 'c']);
|
||
const pid = e.currentPlayerId;
|
||
const before = e.getPlayerHand(pid).length;
|
||
const r = e.playCard(pid, [0]);
|
||
assert.strictEqual(r.error, undefined);
|
||
assert.strictEqual(e.getPlayerHand(pid).length, before - 1);
|
||
assert.strictEqual(r.nextPlayerId, e.currentPlayerId);
|
||
});
|
||
|
||
// 3) 質疑:整組判定
|
||
test('質疑:吹牛組(含非目標牌)→ 質疑成功,出牌者開槍', () => {
|
||
const e = new GameEngine(); e.init(['a', 'b', 'c']);
|
||
e.targetCard = 'A';
|
||
e.getPlayer('a').hand = ['A', 'K', 'JOKER'];
|
||
e.getPlayer('b').hand = ['A', 'A', 'A'];
|
||
e.getPlayer('c').hand = ['A', 'A'];
|
||
e.currentPlayerId = 'a';
|
||
const r = e.playCard('a', [0, 1]); // A, K
|
||
assert.strictEqual(r.error, undefined);
|
||
const c = e.challenge('b');
|
||
assert.strictEqual(c.isBluff, true);
|
||
assert.strictEqual(c.shooter, 'a');
|
||
});
|
||
|
||
test('質疑:真牌組(全目標牌)→ 質疑失敗,質疑者開槍', () => {
|
||
const e = new GameEngine(); e.init(['a', 'b', 'c']);
|
||
e.targetCard = 'A';
|
||
e.getPlayer('a').hand = ['A', 'Q', 'Q'];
|
||
e.getPlayer('b').hand = ['A', 'A', 'A'];
|
||
e.getPlayer('c').hand = ['A', 'A'];
|
||
e.currentPlayerId = 'a';
|
||
e.playCard('a', [0]); // A
|
||
const c = e.challenge('b');
|
||
assert.strictEqual(c.isBluff, false);
|
||
assert.strictEqual(c.shooter, 'b');
|
||
});
|
||
|
||
test('質疑:Joker 視為萬能牌 → 質疑失敗', () => {
|
||
// 用 3 人局:a 出 JOKER 後仍有玩家( c )持牌,b 才能主動質疑
|
||
const e = new GameEngine(); e.init(['a', 'b', 'c']);
|
||
e.targetCard = 'K';
|
||
e.getPlayer('a').hand = ['JOKER', 'Q'];
|
||
e.getPlayer('b').hand = ['K', 'K'];
|
||
e.getPlayer('c').hand = ['Q', 'Q', 'Q'];
|
||
e.currentPlayerId = 'a';
|
||
e.playCard('a', [0]); // JOKER(宣稱 K)
|
||
const c = e.challenge('b');
|
||
assert.strictEqual(c.isBluff, false); // JOKER 是萬能牌 → 質疑失敗
|
||
});
|
||
|
||
// 3.5) 資訊邊界:公開狀態不洩漏實際牌值
|
||
test('資訊邊界:出牌後 gameState 只含張數、不含真實牌值', () => {
|
||
const e = new GameEngine(); e.init(['a', 'b', 'c']);
|
||
const pid = e.currentPlayerId;
|
||
e.playCard(pid, [0]);
|
||
const st = e.getGameState();
|
||
for (const g of st.playedGroups) {
|
||
assert.strictEqual(typeof g.cardCount, 'number');
|
||
assert.strictEqual(g.cards, undefined, 'playedGroups 不應洩漏 cards');
|
||
}
|
||
assert.ok(!Array.isArray(st.playedCards) || st.playedCards.length === 0, 'playedCards 不應含牌值');
|
||
});
|
||
test('質疑時才揭露真實牌值(challengedGroup)', () => {
|
||
const e = new GameEngine(); e.init(['a', 'b', 'c']);
|
||
e.targetCard = 'A';
|
||
e.currentPlayerId = 'a';
|
||
e.playCard('a', [0]);
|
||
const lastGroup = e.playGroups[e.playGroups.length - 1];
|
||
const before = [...lastGroup.cards]; // 引擎內部可讀
|
||
const st = e.getGameState();
|
||
// 公開狀態沒有牌值
|
||
for (const g of st.playedGroups) assert.strictEqual(g.cards, undefined);
|
||
const r = e.challenge('b');
|
||
assert.ok(Array.isArray(r.challengedGroup) && r.challengedGroup.length === before.length,
|
||
'質疑結果才揭露牌值');
|
||
});
|
||
|
||
// 4) 系統質疑
|
||
test('系統質疑:輪到某玩家且其他人都沒牌 → 自動打出並質疑', () => {
|
||
const e = new GameEngine(); e.init(['a', 'b', 'c']);
|
||
e.targetCard = 'A';
|
||
e.getPlayer('a').hand = ['K', 'K'];
|
||
e.getPlayer('b').hand = ['A'];
|
||
e.getPlayer('c').hand = [];
|
||
e.currentPlayerId = 'b';
|
||
const r = e.playCard('b', [0]); // b 出 A,手牌清空
|
||
assert.ok(r.systemChallenge, '應觸發系統質疑');
|
||
assert.strictEqual(r.systemChallenge.playerId, 'a');
|
||
assert.strictEqual(r.systemChallenge.isBluff, true); // K 不是 A
|
||
assert.strictEqual(r.systemChallenge.shooter, 'a');
|
||
});
|
||
|
||
test('系統質疑:若剩餘全為真牌 → 無人開槍並重開一手', () => {
|
||
const e = new GameEngine(); e.init(['a', 'b', 'c']);
|
||
e.targetCard = 'A';
|
||
e.getPlayer('a').hand = ['A', 'A'];
|
||
e.getPlayer('b').hand = ['A'];
|
||
e.getPlayer('c').hand = [];
|
||
e.currentPlayerId = 'b';
|
||
const r = e.playCard('b', [0]);
|
||
assert.ok(r.systemChallenge);
|
||
assert.strictEqual(r.systemChallenge.isBluff, false);
|
||
assert.strictEqual(r.systemChallenge.shooter, null);
|
||
// 重開一手後 a 仍有 5 張
|
||
assert.strictEqual(e.getPlayerHand('a').length, 5);
|
||
});
|
||
|
||
// 5) 重開一手與死亡
|
||
test('質疑後重開一手:存活者各 5 張、死掉者 0 張', () => {
|
||
const e = new GameEngine(); e.init(['a', 'b', 'c']);
|
||
e.targetCard = 'A';
|
||
e.getPlayer('a').hand = ['A', 'K', 'JOKER'];
|
||
e.getPlayer('b').hand = ['A', 'A', 'A'];
|
||
e.getPlayer('c').hand = ['A', 'A'];
|
||
e.currentPlayerId = 'a';
|
||
e.playCard('a', [0, 1]); // A, K 吹牛
|
||
// 讓 a 必死
|
||
e.getPlayer('a').revolver = { chambers: [true, false, false, false, false, false], currentChamber: 0 };
|
||
const c = e.challenge('b');
|
||
assert.strictEqual(c.shooter, 'a');
|
||
assert.strictEqual(e.getPlayer('a').alive, false);
|
||
assert.strictEqual(e.getPlayerHand('a').length, 0);
|
||
assert.strictEqual(e.getPlayerHand('b').length, 5);
|
||
assert.ok(e.roundNumber >= 2);
|
||
});
|
||
|
||
// 6) 完整模擬:必定會結束
|
||
test('完整隨機模擬可正常結束,存活者 <= 1', () => {
|
||
const e = new GameEngine();
|
||
e.init(['p1', 'p2', 'p3', 'p4']);
|
||
let guard = 0;
|
||
while (!e.isGameOver && guard < 3000) {
|
||
guard++;
|
||
const pid = e.currentPlayerId;
|
||
if (!pid) break;
|
||
const hand = e.getPlayerHand(pid);
|
||
if (hand.length === 0) break;
|
||
const n = 1 + Math.floor(Math.random() * Math.min(3, hand.length));
|
||
const idx = Array.from({ length: n }, (_, i) => i);
|
||
e.playCard(pid, idx);
|
||
if (Math.random() < 0.35 && e.playGroups.length > 0) {
|
||
e.challenge(e.currentPlayerId);
|
||
}
|
||
}
|
||
assert.ok(e.isGameOver || guard >= 3000 === false, '遊戲應結束(無死結)');
|
||
assert.ok(e.getAlivePlayers().length <= 1, '最多一位存活');
|
||
assert.ok(e.roundNumber > 0);
|
||
});
|
||
|
||
// 印出結果
|
||
let failed = 0;
|
||
for (const [status, name, msg] of results) {
|
||
if (status === 'ok') console.log(' PASS ' + name);
|
||
else { failed++; console.log(' FAIL ' + name + ' -> ' + msg); }
|
||
}
|
||
console.log(`\n${results.length - failed}/${results.length} 通過`);
|
||
process.exit(failed ? 1 : 0); |