Files
core2026/packages/common/test/gameClock.test.ts
T

144 lines
5.1 KiB
TypeScript

import { describe, expect, it } from 'vitest';
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');
it('projects the fixed Ref turn tick and ignores wall time in manual mode', () => {
const clock = new GameClock({
baseTime,
tick: GAME_TICKS_PER_TURN,
mode: 'manual',
wallAnchor: new Date('2026-01-01T00:00:00.000Z'),
turnSeconds: 3_600,
});
expect(clock.ticksPerSecond).toBe(10_000);
expect(clock.nowTick(new Date('2126-01-01T00:00:00.000Z'))).toBe(GAME_TICKS_PER_TURN);
expect(clock.now(new Date('2126-01-01T00:00:00.000Z')).toISOString()).toBe('2042-01-01T01:00:00.000Z');
});
it('accepts a large forward wall jump without losing tick precision', () => {
const wallAnchor = new Date('2026-01-01T00:00:00.000Z');
const clock = new GameClock({
baseTime,
tick: 0,
mode: 'realtime',
wallAnchor,
turnSeconds: 7_200,
});
const jumped = new Date('2126-01-01T00:00:00.000Z');
const tick = clock.nowTick(jumped);
expect(Number.isSafeInteger(tick)).toBe(true);
expect(clock.tickToDate(tick).getTime() - baseTime.getTime()).toBe(jumped.getTime() - wallAnchor.getTime());
});
it('projects a negative tick before a future realtime anchor', () => {
const clock = new GameClock({
baseTime,
tick: 0,
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),
tick: 0,
mode: 'manual',
wallAnchor: new Date(0),
turnSeconds: 7_200,
});
const tick = MAX_SAFE_GAME_TICK - (MAX_SAFE_GAME_TICK % clock.ticksPerSecond);
const projected = clock.tickToDate(tick);
expect(Number.isNaN(projected.getTime())).toBe(false);
expect(clock.dateToTick(projected)).toBe(tick);
});
it('truncates sub-millisecond projections like Ref GameClock', () => {
const clock = new GameClock({
baseTime,
tick: 0,
mode: 'manual',
wallAnchor: baseTime,
turnSeconds: 600,
});
expect(clock.tickToDate(14_115_919).toISOString()).toBe('2042-01-01T00:03:55.265Z');
expect(clock.tickToDate(-1).toISOString()).toBe('2041-12-31T23:59:59.999Z');
});
});