feat: 게임 시계 reconciliation 권위 기반 추가
This commit is contained in:
@@ -183,17 +183,43 @@ describe('runtime clock shift', () => {
|
||||
clockMode: 'realtime',
|
||||
clockWallAnchor: openAt,
|
||||
lastTurnTick: 0,
|
||||
clockPhase: 'PREOPEN',
|
||||
});
|
||||
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.promotePreopenAtOpening(openAt)).toBe(true);
|
||||
expect(world.getRunnableGameNow(new Date(openAt.getTime() + 60_000))).toEqual(
|
||||
new Date(gameBase.getTime() + 60_000)
|
||||
);
|
||||
});
|
||||
|
||||
it('promotes PREOPEN only at an opening tick-zero anchor', () => {
|
||||
const openAt = new Date('2026-09-02T23:30:00.000Z');
|
||||
const world = buildWorld({
|
||||
clockBaseTime: new Date('2026-07-30T10:00:00.000Z'),
|
||||
clockTick: 0,
|
||||
clockMode: 'realtime',
|
||||
clockWallAnchor: openAt,
|
||||
lastTurnTick: 0,
|
||||
clockPhase: 'PREOPEN',
|
||||
});
|
||||
|
||||
expect(world.promotePreopenAtOpening(new Date(openAt.getTime() - 1))).toBe(false);
|
||||
expect(world.promotePreopenAtOpening(openAt)).toBe(true);
|
||||
expect(world.getGameClockState()).toMatchObject({ phase: 'RUNNING', tick: 0 });
|
||||
});
|
||||
|
||||
it('rejects gameplay commits while the durable clock is suspended', async () => {
|
||||
const world = buildWorld({ clockPhase: 'SUSPENDED', clockMode: 'realtime' });
|
||||
|
||||
expect(() => world.advanceGameClockTo(new Date(), new Date())).toThrow(/SUSPENDED/);
|
||||
expect(() => world.executeGeneralTurn(world.listGenerals()[0]!)).toThrow(/SUSPENDED/);
|
||||
await expect(world.advanceMonth(new Date())).rejects.toThrow(/SUSPENDED/);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[5, 6],
|
||||
[10, 3],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { GameClock } from '@sammo-ts/common';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { calculateInitialTurnTick } from '../src/scenario/scenarioSeeder.js';
|
||||
import { calculateInitialTurnTick, resolveInitialClockPhase } from '../src/scenario/scenarioSeeder.js';
|
||||
|
||||
describe('scenario seeder general turn tick', () => {
|
||||
test('preserves Ref-compatible sub-millisecond RNG precision', () => {
|
||||
@@ -17,4 +17,31 @@ describe('scenario seeder general turn tick', () => {
|
||||
expect(calculateInitialTurnTick(clock, baseTick, 235_265_319)).toBe(baseTick + 14_115_919);
|
||||
expect(clock.dateToTick(new Date(now.getTime() + 235_265))).toBe(baseTick + 14_115_900);
|
||||
});
|
||||
|
||||
test('keeps formal opening at tick zero while PREOPEN projects signed ticks', () => {
|
||||
const seededAt = new Date('2030-01-01T01:00:00.000Z');
|
||||
const openAt = new Date('2030-01-01T02:00:00.000Z');
|
||||
const phase = resolveInitialClockPhase('realtime', seededAt, openAt);
|
||||
const clock = new GameClock({
|
||||
baseTime: new Date('0190-01-01T00:00:00.000Z'),
|
||||
tick: 0,
|
||||
mode: 'realtime',
|
||||
wallAnchor: openAt,
|
||||
turnSeconds: 600,
|
||||
phase,
|
||||
});
|
||||
|
||||
expect(phase).toBe('PREOPEN');
|
||||
expect(clock.nowTick(seededAt)).toBe(-6 * 36_000_000);
|
||||
expect(clock.nowTick(openAt)).toBe(0);
|
||||
expect(calculateInitialTurnTick(clock, 0, 0)).toBe(0);
|
||||
expect(calculateInitialTurnTick(clock, 0, 235_265_319)).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('uses explicit MANUAL and immediate RUNNING phases', () => {
|
||||
const now = new Date('2030-01-01T01:00:00.000Z');
|
||||
|
||||
expect(resolveInitialClockPhase('manual', now, new Date('2030-01-02T01:00:00.000Z'))).toBe('MANUAL');
|
||||
expect(resolveInitialClockPhase('realtime', now, now)).toBe('RUNNING');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user