Prepare game updates and single-process PM2 deployment

This commit is contained in:
2026-10-14 19:13:36 +09:00
parent 4f24129e29
commit 22792bc2b4
50 changed files with 1546 additions and 197 deletions
+16 -3
View File
@@ -6,12 +6,13 @@ import ts from 'typescript';
import { GAME_CHECKPOINT_KEY, GAME_ROOM_KEY } from '../constants/recording.js';
import { HOME_TELEPORT_APPEARANCE } from '../constants/home.js';
import { createGameApi, ApiClientError } from '../src/client/api.js';
import { createGlobalSettingsCache } from '../src/client/global-settings.js';
import { emptyPlayerStats } from '../src/shared/p2p-types.js';
const source = readFileSync('src/client/main.ts', 'utf8').replace(/^import .*;\r?\n/gm, '');
const compiled = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.None } }).outputText;
const flush = async () => { for (let i = 0; i < 8; i++) await new Promise(r => setImmediate(r)); };
async function setup(kind: 'expired' | 'orphan' | 'rematch-wait') {
async function setup(kind: 'expired' | 'orphan' | 'rematch-wait' | 'result') {
const mem = new Map<string, string>(), calls: string[] = [], polls = new Map<number, () => void>(), launched: string[] = [];
const active = { matchId: 'm', seq: 1, mode: 'friend', roomId: 'r' };
const session: any = { gameSessionId: 's', token: 't'.repeat(43), stats: emptyPlayerStats(), lastAppliedSeq: kind === 'orphan' ? 0 : 1, ...(kind === 'orphan' ? { activeMatch: active } : {}) };
@@ -36,8 +37,10 @@ async function setup(kind: 'expired' | 'orphan' | 'rematch-wait') {
const nodes = new Map<string, any>();
const el = (id: string) => { if (!nodes.has(id)) nodes.set(id, { classList: { toggle() {}, add() {}, remove() {} }, addEventListener() {}, focus() {} }); return nodes.get(id); };
const ctx: any = { Map, Date, Promise, JSON, URL, URLSearchParams, Number, crypto, ApiClientError, GAME_CHECKPOINT_KEY, GAME_ROOM_KEY, GUEST_TOKEN_KEY: 'guest', ROOM_POLL_MS: 1000, ACTIONS: {}, ACTION_IDS: [],
document: { getElementById: el, body: { dataset: {} }, querySelectorAll: () => [] }, location: { origin: 'http://local', search: '' }, sessionStorage: storage, localStorage: { getItem: () => null, setItem() {} }, navigator: {}, fighterArt: () => '',
window: { addEventListener() {} }, document: { addEventListener() {}, getElementById: el, body: { dataset: {} }, querySelectorAll: () => [] }, location: { origin: 'http://local', search: '' }, sessionStorage: storage, localStorage: { getItem: () => null, setItem() {} }, navigator: {}, fighterArt: () => '',
GAME_NAME: '쎄쎄쎄:기공대전', HOME_TELEPORT_APPEARANCE, createGameApi: () => createGameApi(storage, send),
createGlobalSettingsCache: () => createGlobalSettingsCache(storage, send),
createAppFooter: () => ({ setConnection() {}, openRules() {} }),
createClientAnalytics: () => ({ ready() {}, screen() {}, gameFinished() {}, gameStarted() {}, error() {}, click() {} }),
createResultReporter: (api: any) => ({ destroy() {}, resumePending: async () => {}, submit: api.record }),
createMatchView: () => ({ update() {}, destroy() {} }), createPeer: () => ({}),
@@ -45,7 +48,7 @@ async function setup(kind: 'expired' | 'orphan' | 'rematch-wait') {
createMatchController: (config: any) => { launched.push(config.activeMatch.matchId); return { subscribe() {}, start: async () => {}, view: () => ({ matchId: config.activeMatch.matchId }), destroy() {} }; },
setTimeout(fn: () => void) { polls.set(++pollId, fn); return pollId; }, clearTimeout(id: number) { polls.delete(id); },
};
vm.createContext(ctx); vm.runInContext(compiled + '\nglobalThis.review={leave,boot,inspect:()=>({ready,currentScreen,currentRoom})};', ctx); await flush();
vm.createContext(ctx); vm.runInContext(compiled + '\nglobalThis.review={leave,boot,inspect:()=>({ready,currentScreen,currentRoom,currentMatch})};', ctx); await flush();
return { ctx, calls, polls, room, mem, launched, open: () => open };
}
test('already-expired room does not trap a restored result on exit', async () => {
@@ -61,3 +64,13 @@ test('refresh restores HTTP rematch consent polling and starts an advanced round
h.room.round = 2; h.room.currentMatchId = 'new-match'; h.room.rematch = [false, false];
[...h.polls.values()][0](); await flush(); assert.deepEqual(h.launched, ['new-match']);
});
test('a result monitors opponent departure without requesting a rematch', async () => {
const h = await setup('result');
assert.equal(h.polls.size, 1);
assert.equal(h.ctx.review.inspect().currentMatch.opponentLeft, false);
h.room.status = 'closed';
[...h.polls.values()][0](); await flush();
assert.equal(h.ctx.review.inspect().currentMatch.opponentLeft, true);
assert.equal(h.calls.some(path => path.endsWith('/rematch')), false);
});