Files

96 lines
4.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 測試:RoomManager.restart(再來一局)+ 離線緩衝重連(rejoin)+ ack/去重
// 跑法:先啟動 servernode server/index.js)再跑本檔:node scripts/test_reconnect.js
const { io } = require('../client/node_modules/socket.io-client');
const RoomManager = require('../server/game/room');
const URL = 'http://localhost:3000';
let failures = 0;
function ok(cond, msg) {
if (cond) console.log(' PASS ' + msg);
else { failures++; console.log(' FAIL ' + msg); }
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
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._err = null;
s.on('connect', () => 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._err = d && d.message; });
});
}
function once(socket, ev, timeout = 8000) {
return new Promise((resolve) => {
const t = setTimeout(() => resolve(undefined), timeout);
socket.once(ev, (d) => { clearTimeout(t); resolve(d); });
});
}
function emitAck(s, ev, payload) {
return new Promise((resolve) => { s.emit(ev, payload, (res) => resolve(res)); });
}
(async () => {
console.log('[A] RoomManager.restart(再來一局)');
const rm = new RoomManager();
const room = rm.createRoom('p1');
rm.joinRoom(room.id, 'p2');
const gs = rm.startGame(room.id).gameState;
ok(gs && gs.roundNumber === 1, '初始開局 round=1');
room.engine.isGameOver = true;
rm.syncStatus(room.id);
ok(room.status === 'ended', 'syncStatus: 自然分出勝負 → 房間 ended');
const rr = rm.restart(room.id);
ok(rr.success && rr.gameState.roundNumber === 1, 'restart 成功並 round=1');
ok(!!rr.gameState.players && rr.gameState.players.length === 2, 'restart 後仍是 2 人');
ok(rm.restart(room.id).error, '再次 restartplaying)→ 被拒');
console.log('[B] 重連(離線緩衝)');
const a = await connect('A');
const b = await connect('B');
const created = await emitAck(a, 'createRoom', { requestId: 1 });
ok(created.ok && created.room && created.room.id, 'createRoom ack ok, room=' + (created.room && created.room.id));
const roomId = created.room.id;
const joined = await emitAck(b, 'joinRoom', { roomId, requestId: 2 });
ok(joined.ok, 'joinRoom ack ok');
const st = await emitAck(a, 'startGame', { roomId, requestId: 3 });
ok(st.ok, 'startGame ack ok');
const dup = await emitAck(a, 'startGame', { roomId, requestId: 3 });
ok(!!dup.error, '相同 requestId 重送 → 去重擋下');
const bId = b.id;
const disconnEvt = once(a, 'playerDisconnected');
b.close();
const pd = await disconnEvt;
ok(pd && pd.reconnecting === true, 'B 離線 → 顯示「等待重連」而非立刻結束');
const c = await connect('C');
const resyncP = once(c, 'resync');
const rejEvt = once(a, 'playerRejoined');
const rej = await emitAck(c, 'rejoin', { roomId, playerId: bId, requestId: 4 });
ok(rej.ok, 'rejoin ack ok');
const res = await resyncP;
ok(res && res.gameState && res.gameState.isGameOver === false, '重連後收到 resync 完整狀態');
await rejEvt;
await sleep(300);
ok((c._hand || []).length === 5, '重連者拿回 5 張私密手牌');
const bad = await emitAck(c, 'rejoin', { roomId, playerId: 'nonexistent', requestId: 5 });
ok(!!bad.error, '不存在的座位 → rejoin 被拒');
const roomsRes = await fetch('http://localhost:3000/api/rooms').then((r) => r.json());
const roomNow = roomsRes.find((x) => x.id === roomId);
ok(roomNow && roomNow.players === 2, 'B 座位仍在(未離場,players=2');
// rejoined seat must still be able to ACT (engine resolves seatId) and still receive hand broadcasts
const curTurn = (res && res.gameState && res.gameState.currentPlayerId) || a.id;
const actor = (curTurn === a.id) ? a : c;
const upd = once(actor, 'yourHand');
const ackAct = await emitAck(actor, 'playCard', { roomId, cardIndices: [0], requestId: 100 });
ok(ackAct.ok, 'playCard after rejoin ok via seatId' + (ackAct.error ? ' -> ' + ackAct.error : ''));
if (actor === c) { await upd; ok(Array.isArray(c._hand) && c._hand.length === 4, 'hand update after rejoin reaches rejoined seat (broadcastHands)'); }
else { ok(Array.isArray(a._hand) && a._hand.length === 4, 'original seat hand updated after rejoin'); }
a.close(); c.close();
await sleep(200);
console.log(failures === 0 ? '\n重連/再來一局測試全部通過' : `\n有 ${failures} 項失敗`);
process.exit(failures ? 1 : 0);
})().catch((e) => { console.error('[FATAL]', e.message); process.exit(1); });