166 lines
5.8 KiB
TypeScript
166 lines
5.8 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { test } from 'node:test';
|
|
import type {
|
|
GameEntry,
|
|
GameHost,
|
|
GameModule,
|
|
GameServerMessage,
|
|
PlatformGameSnapshot,
|
|
PlatformToGameEvent,
|
|
} from '../../YouleNexus/assets/framework/sdk/contracts/index.ts';
|
|
import {
|
|
assertGameEntry,
|
|
GameSessionHost,
|
|
} from '../../YouleNexus/assets/framework/sdk/runtime/game-session-host.ts';
|
|
|
|
function makeHost(): GameHost {
|
|
const snapshot: PlatformGameSnapshot = {
|
|
connection: { phase: 'connected' },
|
|
self: { playerId: 1, seat: 0 },
|
|
room: { roomcode: 'fixture', roomtype: [], stage: 0, needprepare: 0, infinite: 0 },
|
|
seats: [],
|
|
};
|
|
return {
|
|
seat: { toView: (seat) => seat, toServer: (seat) => seat },
|
|
getSnapshot: () => snapshot,
|
|
subscribe: () => () => {},
|
|
sendGameMessage: () => {},
|
|
execute: () => {},
|
|
};
|
|
}
|
|
|
|
function makeEntry(createModule: () => GameModule): GameEntry {
|
|
return {
|
|
key: 'fixture',
|
|
gameId: 41,
|
|
route: 'fixture-route',
|
|
resolveSeatCount: () => 4,
|
|
createModule,
|
|
};
|
|
}
|
|
|
|
function makeModule(log: string[], onRestore?: (deskinfo: unknown) => void): GameModule {
|
|
return {
|
|
attach: () => log.push('attach'),
|
|
handlePlatformEvent: (event) => log.push(`event:${event.type}`),
|
|
handleGameMessage: (message) => log.push(`message:${message.rpc}`),
|
|
restore: (deskinfo) => {
|
|
log.push('restore');
|
|
onRestore?.(deskinfo);
|
|
},
|
|
dispose: () => log.push('dispose'),
|
|
};
|
|
}
|
|
|
|
test('assertGameEntry rejects invalid identity, routes, seat counts, and module factories', () => {
|
|
const entry = makeEntry(() => makeModule([]));
|
|
|
|
assert.throws(() => assertGameEntry({ ...entry, key: '' }), /key/);
|
|
assert.throws(() => assertGameEntry({ ...entry, gameId: '' }), /gameId/);
|
|
for (const route of ['platform', 'agent', 'room']) {
|
|
assert.throws(() => assertGameEntry({ ...entry, route }), /reserved.*route/);
|
|
}
|
|
assert.throws(() => assertGameEntry({ ...entry, resolveSeatCount: () => 0 }), /seat count/);
|
|
assert.throws(() => assertGameEntry({ ...entry, resolveSeatCount: () => 2.5 }), /seat count/);
|
|
assert.throws(() => assertGameEntry({ ...entry, createModule: () => undefined as never }), /module/);
|
|
});
|
|
|
|
test('GameSessionHost delivers room events and deskinfo to one active module in order', () => {
|
|
const log: string[] = [];
|
|
const deskinfo = { turn: 7 };
|
|
let restoredValue: unknown;
|
|
const sessions = new GameSessionHost(makeEntry(() => makeModule(log, (value) => { restoredValue = value; })));
|
|
|
|
sessions.open(makeHost());
|
|
sessions.publish({ type: 'room.entered', roomtype: ['raw', { players: 4 }] });
|
|
sessions.restore(deskinfo);
|
|
sessions.close();
|
|
|
|
assert.deepEqual(log, ['attach', 'event:room.entered', 'restore', 'dispose']);
|
|
assert.equal(restoredValue, deskinfo);
|
|
assert.equal(sessions.state, 'idle');
|
|
});
|
|
|
|
test('GameSessionHost preserves raw platform events and game messages by identity', () => {
|
|
let receivedEvent: PlatformToGameEvent | undefined;
|
|
let receivedMessage: GameServerMessage | undefined;
|
|
const sessions = new GameSessionHost(makeEntry(() => ({
|
|
attach() {},
|
|
handlePlatformEvent(event) { receivedEvent = event; },
|
|
handleGameMessage(message) { receivedMessage = message; },
|
|
restore() {},
|
|
dispose() {},
|
|
})));
|
|
const rawRoomtype = ['raw', { players: 4 }];
|
|
const rawData = { cards: [3, 7] };
|
|
const message: GameServerMessage = { rpc: 'draw', data: rawData };
|
|
const event: PlatformToGameEvent = { type: 'room.entered', roomtype: rawRoomtype };
|
|
|
|
sessions.open(makeHost());
|
|
sessions.publish(event);
|
|
sessions.dispatchGameMessage('fixture-route', message);
|
|
|
|
assert.equal(receivedEvent, event);
|
|
assert.equal(receivedEvent?.roomtype, rawRoomtype);
|
|
assert.equal(receivedMessage, message);
|
|
assert.equal(receivedMessage?.data, rawData);
|
|
});
|
|
|
|
test('GameSessionHost rejects illegal state transitions and mismatched game routes', () => {
|
|
const log: string[] = [];
|
|
const sessions = new GameSessionHost(makeEntry(() => makeModule(log)));
|
|
const message: GameServerMessage = { rpc: 'draw', data: { card: 3 } };
|
|
|
|
assert.throws(() => sessions.publish({ type: 'room.dissolved' }), /active/);
|
|
assert.throws(() => sessions.restore({}), /active/);
|
|
sessions.open(makeHost());
|
|
assert.throws(() => sessions.open(makeHost()), /open/);
|
|
assert.throws(() => sessions.dispatchGameMessage('other-route', message), /route/);
|
|
sessions.dispatchGameMessage('fixture-route', message);
|
|
assert.deepEqual(log, ['attach', 'message:draw']);
|
|
});
|
|
|
|
test('GameSessionHost disposes a module exactly once and becomes terminal after dispose', () => {
|
|
const log: string[] = [];
|
|
const sessions = new GameSessionHost(makeEntry(() => makeModule(log)));
|
|
|
|
sessions.open(makeHost());
|
|
sessions.dispose();
|
|
sessions.dispose();
|
|
|
|
assert.deepEqual(log, ['attach', 'dispose']);
|
|
assert.equal(sessions.state, 'disposed');
|
|
assert.throws(() => sessions.open(makeHost()), /disposed/);
|
|
assert.throws(() => sessions.publish({ type: 'room.dissolved' }), /disposed/);
|
|
});
|
|
|
|
test('GameSessionHost creates a fresh module after close', () => {
|
|
const log: string[] = [];
|
|
const modules: GameModule[] = [];
|
|
const sessions = new GameSessionHost(makeEntry(() => {
|
|
const module = makeModule(log);
|
|
modules.push(module);
|
|
return module;
|
|
}));
|
|
|
|
sessions.open(makeHost());
|
|
sessions.close();
|
|
sessions.open(makeHost());
|
|
sessions.close();
|
|
|
|
assert.notEqual(modules[modules.length - 1], modules[modules.length - 2]);
|
|
assert.deepEqual(log, ['attach', 'dispose', 'attach', 'dispose']);
|
|
});
|
|
|
|
test('GameSessionHost returns to active after a restore handler throws', () => {
|
|
const log: string[] = [];
|
|
const sessions = new GameSessionHost(makeEntry(() => ({
|
|
...makeModule(log),
|
|
restore: () => { throw new Error('restore failed'); },
|
|
})));
|
|
|
|
sessions.open(makeHost());
|
|
assert.throws(() => sessions.restore({}), /restore failed/);
|
|
assert.equal(sessions.state, 'active');
|
|
});
|