96 lines
4.7 KiB
JavaScript
96 lines
4.7 KiB
JavaScript
// 測試:RoomManager.restart(再來一局)+ 離線緩衝重連(rejoin)+ ack/去重
|
||
// 跑法:先啟動 server(node 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, '再次 restart(playing)→ 被拒');
|
||
|
||
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); }); |