가오픈 장수 턴의 오픈 전 적체 방지

This commit is contained in:
2026-09-03 00:36:56 +00:00
parent b1984669d4
commit 97a8b1daef
7 changed files with 101 additions and 6 deletions
@@ -80,6 +80,27 @@ describe('generic join deterministic contracts', () => {
]);
});
it('moves an inherited turn phase past the runnable opening boundary', () => {
const rng = {
nextRangeInt(min: number) {
return min;
},
};
const openingGameAt = new Date('2026-07-30T10:00:00.000Z');
const preopenCursor = new Date(openingGameAt.getTime() - 30 * 60_000);
const turnTime = resolveJoinTurnTime(
rng,
{ tickSeconds: 60 } as Parameters<typeof resolveJoinTurnTime>[1],
openingGameAt,
preopenCursor,
0
);
expect(turnTime.getTime()).toBeGreaterThan(openingGameAt.getTime());
expect((turnTime.getTime() - preopenCursor.getTime()) % 60_000).toBe(0);
});
it('uses the HiDCHe product name without the legacy PHP runtime label', () => {
expect(JOIN_WELCOME_MESSAGE).toBe('삼국지 모의전투 HiDCHe의 세계에 오신 것을 환영합니다 ^o^');
expect(JOIN_WELCOME_MESSAGE).not.toContain('PHP');
@@ -174,6 +174,26 @@ describe('runtime clock shift', () => {
expect(world.getGameClockState().wallAnchor).toEqual(resumedAt);
});
it('keeps runnable general scheduling at the future opening anchor during PREOPEN', () => {
const gameBase = new Date('2026-07-30T10:00:00.000Z');
const openAt = new Date('2026-09-02T23:30:00.000Z');
const world = buildWorld({
clockBaseTime: gameBase,
clockTick: 0,
clockMode: 'realtime',
clockWallAnchor: openAt,
lastTurnTick: 0,
});
const preopenAt = new Date('2026-09-02T23:03:00.000Z');
expect(world.getGameNow(preopenAt).getTime()).toBeLessThan(gameBase.getTime());
expect(world.getRunnableGameNow(preopenAt)).toEqual(gameBase);
expect(world.getRunnableGameNow(openAt)).toEqual(gameBase);
expect(world.getRunnableGameNow(new Date(openAt.getTime() + 60_000))).toEqual(
new Date(gameBase.getTime() + 60_000)
);
});
it.each([
[5, 6],
[10, 3],
@@ -4,7 +4,11 @@ import { GAME_TICKS_PER_TURN } from '@sammo-ts/common';
import { parseScenarioGeneralPoolCandidate } from '@sammo-ts/logic';
import { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js';
import { reserveSelectionPool, resolveSelectionPoolUserIcon } from '../src/turn/selectPoolService.js';
import {
buildInitialTurnTime,
reserveSelectionPool,
resolveSelectionPoolUserIcon,
} from '../src/turn/selectPoolService.js';
import type { TurnGeneralPoolEntry, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
interface TestPoolRow {
@@ -28,6 +32,30 @@ interface PoolWhere {
const acceptedAt = new Date('0200-05-01T00:00:00.000Z');
describe('selection-pool initial turn scheduling', () => {
it('does not inherit a turntime base from before the opening boundary', () => {
const openingGameAt = new Date('0200-01-01T00:00:00.000Z');
const preopenGameAt = new Date(openingGameAt.getTime() - 30 * 60_000);
const rng = {
nextRangeInt(min: number) {
return min;
},
};
const turnTime = buildInitialTurnTime(
rng,
{
tickSeconds: 300,
meta: { turntime: preopenGameAt.toISOString() },
} as unknown as Parameters<typeof buildInitialTurnTime>[1],
preopenGameAt,
openingGameAt
);
expect(turnTime.getTime()).toBeGreaterThanOrEqual(openingGameAt.getTime());
});
});
const buildRows = (): TestPoolRow[] =>
Array.from({ length: 29 }, (_, index) => {
const uniqueName = `P${String(index + 1).padStart(2, '0')}`;