109 lines
4.6 KiB
JavaScript
109 lines
4.6 KiB
JavaScript
// 多人端對端整合測試:連真實伺服器,驗證「多張出牌 → 下家挑戰 → 手牌更新」流程。
|
||
// 使用:先啟動 server(node server/index.js),再跑 node scripts/test_socket_e2e.js
|
||
const { io } = require('../client/node_modules/socket.io-client');
|
||
|
||
const URL = 'http://localhost:3000';
|
||
const clients = {};
|
||
|
||
function connect(name) {
|
||
return new Promise((resolve, reject) => {
|
||
const s = io(URL, { transports: ['websocket'], reconnection: false, timeout: 8000 });
|
||
s._hand = null;
|
||
s._name = name;
|
||
s._lastError = null;
|
||
s.on('connect', () => { clients[s.id] = s; resolve(s); });
|
||
s.on('connect_error', (e) => reject(new Error(name + ' connect_error: ' + e.message)));
|
||
s.on('yourHand', (h) => { s._hand = h || []; });
|
||
s.on('error', (d) => { s._lastError = d && d.message; });
|
||
});
|
||
}
|
||
|
||
function once(socket, ev, timeout = 8000) {
|
||
return new Promise((resolve, reject) => {
|
||
const t = setTimeout(() => reject(new Error(`${socket._name} wait ${ev} timeout`)), timeout);
|
||
socket.once(ev, (d) => { clearTimeout(t); resolve(d); });
|
||
});
|
||
}
|
||
|
||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||
let failures = 0;
|
||
function ok(cond, msg) {
|
||
if (cond) console.log(' PASS ' + msg);
|
||
else { failures++; console.log(' FAIL ' + msg); }
|
||
}
|
||
|
||
(async () => {
|
||
console.log('[1] 三玩家連線+房間');
|
||
const a = await connect('A');
|
||
const b = await connect('B');
|
||
const c = await connect('C');
|
||
|
||
const roomCreated = once(a, 'roomCreated');
|
||
a.emit('createRoom');
|
||
const room = await roomCreated;
|
||
const roomId = room.id;
|
||
ok(roomId, '建立房間 roomId=' + roomId);
|
||
|
||
b.emit('joinRoom', { roomId });
|
||
await once(a, 'playerJoined');
|
||
c.emit('joinRoom', { roomId });
|
||
await once(a, 'playerJoined');
|
||
ok(true, 'B、C 加入');
|
||
|
||
console.log('[2] 開始遊戲 + 私密手牌');
|
||
const started = [once(a, 'gameStarted'), once(b, 'gameStarted'), once(c, 'gameStarted')];
|
||
a.emit('startGame', { roomId });
|
||
const [gsA] = await Promise.all(started);
|
||
ok(gsA.currentPlayerId && ['Q','K','A'].includes(gsA.targetCard), 'gameStarted 狀態正確');
|
||
ok(gsA.players.length === 3, '3 位玩家');
|
||
await sleep(300);
|
||
ok(a._hand.length === 5 && b._hand.length === 5 && c._hand.length === 5,
|
||
'每位玩家收到 5 張私密手牌(yourHand)');
|
||
|
||
console.log('[3] 目前玩家多張出牌');
|
||
const cur = clients[gsA.currentPlayerId];
|
||
ok(!!cur, '定位目前玩家');
|
||
const playedEvt = once(a, 'cardPlayed');
|
||
cur.emit('playCard', { roomId, cardIndices: [0, 1] });
|
||
const played = await playedEvt;
|
||
// 資訊邊界:cardPlayed 只公開張數,不洩漏實際牌值
|
||
ok(played.playedGroup && played.playedGroup.cardCount === 2, '出牌 2 張並廣播(只含張數)');
|
||
ok(!('cards' in (played.playedGroup || {})), 'cardPlayed 不洩漏實際牌值');
|
||
const currentNow = played.gameState.currentPlayerId;
|
||
ok(currentNow !== cur.id, '換下一位');
|
||
await sleep(300);
|
||
ok(cur._hand.length === 3, `出牌者剩 3 張 (yourHand 同步,實際=${cur._hand.length})`);
|
||
|
||
console.log('[4] 非我的回合出牌 → 被拒絕');
|
||
const notTurn = [a, b, c].find((s) => s.id !== cur.id && s.id !== currentNow);
|
||
ok(!!notTurn, '找到非回合玩家');
|
||
notTurn._lastError = null;
|
||
notTurn.emit('playCard', { roomId, cardIndices: [0] });
|
||
await sleep(300);
|
||
ok(!!notTurn._lastError, '非回合者被拒絕(' + (notTurn._lastError || 'none') + ')');
|
||
|
||
console.log('[5] 目前玩家(下家)質疑 → 收到結果與新狀態');
|
||
const currentSocket = clients[currentNow];
|
||
const challengedEvt = once(a, 'challenged');
|
||
currentSocket.emit('challenge', { roomId });
|
||
const ch = await challengedEvt;
|
||
ok(ch.result && typeof ch.result.isBluff === 'boolean', '質疑結果帶 isBluff=' + ch.result.isBluff);
|
||
ok(ch.result.shooter, '有開槍者(shooter=' + ch.result.shooter + ')');
|
||
ok(ch.gameState.roundNumber >= 2, '質疑後進入下一局(round=' + ch.gameState.roundNumber + ')');
|
||
await sleep(300);
|
||
ok(ch.gameState.players.every((p) => {
|
||
const sh = clients[p.id];
|
||
return !sh || (sh._hand.length === p.handSize);
|
||
}), 'yourHand 與公開狀態一致(' + ch.gameState.players.map((p) => p.handSize).join(',') + ')');
|
||
|
||
const aliveCount = ch.gameState.players.filter((p) => p.alive).length;
|
||
console.log(`[6] 結果快照:第 ${ch.gameState.roundNumber} 局,存活 ${aliveCount}/3,isBluff=${ch.result.isBluff}`);
|
||
|
||
[a, b, c].forEach((s) => s.close());
|
||
await sleep(300);
|
||
console.log(failures === 0 ? '\n整合測試全部通過' : `\n有 ${failures} 項失敗`);
|
||
process.exit(failures ? 1 : 0);
|
||
})().catch((e) => {
|
||
console.error('[FATAL]', e.message);
|
||
process.exit(1);
|
||
}); |