feat: 게임 시계 reconciliation 권위 기반 추가
This commit is contained in:
@@ -1,6 +1,15 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { GAME_TICKS_PER_TURN, GameClock, MAX_SAFE_GAME_TICK } from '../src/time/GameClock.js';
|
||||
import {
|
||||
GAME_TICKS_PER_TURN,
|
||||
GameClock,
|
||||
MAX_SAFE_GAME_TICK,
|
||||
asGameTick,
|
||||
asObservedGameInstant,
|
||||
buildExactClockAlignmentPlan,
|
||||
createDeadline,
|
||||
scheduleNotBefore,
|
||||
} from '../src/time/GameClock.js';
|
||||
|
||||
describe('GameClock', () => {
|
||||
const baseTime = new Date('2042-01-01T00:00:00.000Z');
|
||||
@@ -42,12 +51,68 @@ describe('GameClock', () => {
|
||||
mode: 'realtime',
|
||||
wallAnchor: new Date('2026-01-01T01:00:00.000Z'),
|
||||
turnSeconds: 3_600,
|
||||
phase: 'PREOPEN',
|
||||
});
|
||||
|
||||
expect(clock.nowTick(new Date('2026-01-01T00:30:00.000Z'))).toBe(-GAME_TICKS_PER_TURN / 2);
|
||||
expect(clock.nowTick(new Date('2026-01-01T01:00:00.000Z'))).toBe(0);
|
||||
});
|
||||
|
||||
it('does not rewind a RUNNING realtime tick when wall time moves backward', () => {
|
||||
const clock = new GameClock({
|
||||
baseTime,
|
||||
tick: GAME_TICKS_PER_TURN,
|
||||
mode: 'realtime',
|
||||
wallAnchor: new Date('2026-01-01T01:00:00.000Z'),
|
||||
turnSeconds: 3_600,
|
||||
phase: 'RUNNING',
|
||||
revision: 7,
|
||||
});
|
||||
|
||||
expect(clock.nowTick(new Date('2026-01-01T00:59:55.000Z'))).toBe(GAME_TICKS_PER_TURN);
|
||||
expect(clock.revision).toBe(7);
|
||||
});
|
||||
|
||||
it('floors PREOPEN executable schedules at opening tick zero', () => {
|
||||
const observed = asObservedGameInstant(-GAME_TICKS_PER_TURN / 2);
|
||||
|
||||
expect(scheduleNotBefore(observed, 'PREOPEN')).toBe(0);
|
||||
expect(createDeadline(observed, asGameTick(GAME_TICKS_PER_TURN / 4), 'PREOPEN')).toBe(0);
|
||||
expect(scheduleNotBefore(observed, 'RUNNING')).toBe(observed);
|
||||
});
|
||||
|
||||
it('preserves a 65 minute 17.250 second sub-turn suspension remainder exactly', () => {
|
||||
const plan = buildExactClockAlignmentPlan({
|
||||
sourceRevision: 11,
|
||||
cutTick: 123_456,
|
||||
cutWall: new Date('2026-01-01T00:00:00.000Z'),
|
||||
resumeWall: new Date('2026-01-01T01:05:17.250Z'),
|
||||
ticksPerSecond: 10_000,
|
||||
});
|
||||
|
||||
expect(plan).toMatchObject({
|
||||
sourceRevision: 11,
|
||||
targetRevision: 12,
|
||||
gapTicks: 39_172_500,
|
||||
shiftTicks: 39_172_500,
|
||||
alignedTick: 39_295_956,
|
||||
});
|
||||
expect(plan.shiftTicks % GAME_TICKS_PER_TURN).toBe(3_172_500);
|
||||
});
|
||||
|
||||
it('aligns a 24 hour exact maintenance without catch-up', () => {
|
||||
const plan = buildExactClockAlignmentPlan({
|
||||
sourceRevision: 1,
|
||||
cutTick: 2 * GAME_TICKS_PER_TURN,
|
||||
cutWall: new Date('2026-01-01T00:00:00.000Z'),
|
||||
resumeWall: new Date('2026-01-02T00:00:00.000Z'),
|
||||
ticksPerSecond: 10_000,
|
||||
});
|
||||
|
||||
expect(plan.gapTicks).toBe(24 * GAME_TICKS_PER_TURN);
|
||||
expect(plan.alignedTick).toBe(26 * GAME_TICKS_PER_TURN);
|
||||
});
|
||||
|
||||
it('projects near the safe tick boundary without unsafe intermediate multiplication', () => {
|
||||
const clock = new GameClock({
|
||||
baseTime: new Date(0),
|
||||
|
||||
Reference in New Issue
Block a user