장수 55종과 수뇌 35종의 상태, 로그, 메시지, 예약 턴 비교를 닫습니다. 실제 시나리오 프로필과 대표 PostgreSQL 수명주기, 즉시 외교와 출병 회귀를 추가하고 발견된 Ref 로그 및 생성 장수 저장 차이를 교정합니다.
253 lines
8.2 KiB
TypeScript
253 lines
8.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import type { MapDefinition, UnitSetDefinition } from '@sammo-ts/logic';
|
|
import { InMemoryTurnWorld } from '@sammo-ts/game-engine/turn/inMemoryWorld.js';
|
|
|
|
import type { CanonicalTurnSnapshot } from '../src/turn-differential/canonical.js';
|
|
import {
|
|
buildCoreTurnCommandWorldInput,
|
|
runCoreTurnCommandTrace,
|
|
type TurnCommandFixtureRequest,
|
|
} from '../src/turn-differential/coreCommandTrace.js';
|
|
|
|
const cityStats = {
|
|
population: 1_000,
|
|
agriculture: 100,
|
|
commerce: 100,
|
|
security: 100,
|
|
defence: 100,
|
|
wall: 100,
|
|
};
|
|
|
|
const map: MapDefinition = {
|
|
id: 'clock-projection-test',
|
|
name: 'clock projection test',
|
|
cities: [
|
|
{
|
|
id: 1,
|
|
name: '테스트시',
|
|
level: 5,
|
|
region: 1,
|
|
position: { x: 0, y: 0 },
|
|
connections: [],
|
|
initial: cityStats,
|
|
max: cityStats,
|
|
},
|
|
],
|
|
};
|
|
|
|
const unitSet: UnitSetDefinition = {
|
|
id: 'clock-projection-test',
|
|
name: 'clock projection test',
|
|
defaultCrewTypeId: 1100,
|
|
crewTypes: [],
|
|
};
|
|
|
|
const referenceBefore: CanonicalTurnSnapshot = {
|
|
schemaVersion: 1,
|
|
engine: 'ref',
|
|
world: {
|
|
year: 185,
|
|
month: 1,
|
|
tickMinutes: 60,
|
|
lastTurnTick: 24_229_750_000,
|
|
// Ref snapshots use MySQL's timezone-less microsecond representation.
|
|
turnTime: '2026-08-22 14:02:55.000000',
|
|
gameNow: '2026-08-22 14:02:55.000000',
|
|
},
|
|
generals: [
|
|
{
|
|
id: 1,
|
|
name: '장수',
|
|
nationId: 0,
|
|
cityId: 1,
|
|
troopId: 0,
|
|
officerLevel: 0,
|
|
turnTick: 24_245_250_000,
|
|
turnTime: '2026-08-22 14:28:45.000000',
|
|
},
|
|
],
|
|
rankData: [],
|
|
cities: [{ id: 1, name: '테스트시', nationId: 0, level: 5 }],
|
|
nations: [],
|
|
troops: [],
|
|
diplomacy: [],
|
|
generalTurns: [],
|
|
nationTurns: [],
|
|
logs: [],
|
|
messages: [],
|
|
watermarks: { logId: 0, historyLogId: 0, messageId: 0 },
|
|
};
|
|
|
|
describe('turn command fixture GameClock projection', () => {
|
|
it('preserves Ref absolute dates when materializing persisted ticks', () => {
|
|
const request: TurnCommandFixtureRequest = {
|
|
kind: 'general',
|
|
actorGeneralId: 1,
|
|
action: '휴식',
|
|
};
|
|
const input = buildCoreTurnCommandWorldInput(request, referenceBefore, unitSet, map);
|
|
const world = new InMemoryTurnWorld(input.state, input.snapshot, {
|
|
schedule: { entries: [{ startMinute: 0, tickMinutes: 60 }] },
|
|
});
|
|
|
|
expect(world.getState().lastTurnTime.toISOString()).toBe('2026-08-22T14:02:55.000Z');
|
|
expect(world.getGeneralById(1)?.turnTime.toISOString()).toBe('2026-08-22T14:28:45.000Z');
|
|
});
|
|
|
|
it('injects Ref gameNow instead of the later actor turn time into command messages', async () => {
|
|
const commandSnapshot: CanonicalTurnSnapshot = {
|
|
...referenceBefore,
|
|
world: {
|
|
...referenceBefore.world,
|
|
initYear: 180,
|
|
initMonth: 1,
|
|
develCost: 100,
|
|
gameNow: '2026-08-22 14:02:55.123456',
|
|
},
|
|
generals: [
|
|
{
|
|
...referenceBefore.generals[0],
|
|
nationId: 1,
|
|
cityId: 3,
|
|
officerLevel: 12,
|
|
gold: 100_000,
|
|
rice: 100_000,
|
|
},
|
|
{
|
|
...referenceBefore.generals[0],
|
|
id: 2,
|
|
name: '수신자',
|
|
nationId: 2,
|
|
cityId: 70,
|
|
officerLevel: 1,
|
|
},
|
|
],
|
|
cities: [
|
|
{
|
|
id: 3,
|
|
name: '아국도시',
|
|
nationId: 1,
|
|
level: 5,
|
|
population: 100_000,
|
|
populationMax: 200_000,
|
|
agriculture: 1_000,
|
|
commerce: 1_000,
|
|
security: 1_000,
|
|
defence: 1_000,
|
|
wall: 1_000,
|
|
supplyState: 1,
|
|
frontState: 0,
|
|
state: 0,
|
|
trust: 80,
|
|
trade: 100,
|
|
},
|
|
{
|
|
id: 70,
|
|
name: '타국도시',
|
|
nationId: 2,
|
|
level: 5,
|
|
population: 100_000,
|
|
populationMax: 200_000,
|
|
agriculture: 1_000,
|
|
commerce: 1_000,
|
|
security: 1_000,
|
|
defence: 1_000,
|
|
wall: 1_000,
|
|
supplyState: 1,
|
|
frontState: 0,
|
|
state: 0,
|
|
trust: 80,
|
|
trade: 100,
|
|
},
|
|
],
|
|
nations: [
|
|
{
|
|
id: 1,
|
|
name: '아국',
|
|
color: '#111111',
|
|
capitalCityId: 3,
|
|
gold: 1_000_000,
|
|
rice: 1_000_000,
|
|
power: 1_000,
|
|
level: 1,
|
|
typeCode: 'che_중립',
|
|
},
|
|
{
|
|
id: 2,
|
|
name: '타국',
|
|
color: '#222222',
|
|
capitalCityId: 70,
|
|
gold: 1_000_000,
|
|
rice: 1_000_000,
|
|
power: 1_000,
|
|
level: 1,
|
|
typeCode: 'che_중립',
|
|
},
|
|
],
|
|
};
|
|
const request: TurnCommandFixtureRequest = {
|
|
kind: 'general',
|
|
actorGeneralId: 1,
|
|
action: 'che_등용',
|
|
args: { destGeneralID: 2 },
|
|
setup: {
|
|
isolateWorld: true,
|
|
world: { startYear: 180, year: 185, month: 1, freezeClock: true },
|
|
},
|
|
observe: { generalIds: [1, 2], cityIds: [3, 70], nationIds: [1, 2], messageAfterId: 0 },
|
|
};
|
|
|
|
const trace = await runCoreTurnCommandTrace(request, commandSnapshot);
|
|
|
|
expect(trace.after.world.gameNow).toBe('2026-08-22 14:02:55.123456');
|
|
expect(trace.after.messages.map((message) => message.createdAt)).toEqual(['2026-08-22T14:02:55.123Z']);
|
|
});
|
|
|
|
it('keeps a stale nation gennum visible instead of masking an update omission with live membership', async () => {
|
|
const snapshot: CanonicalTurnSnapshot = {
|
|
...referenceBefore,
|
|
generals: Array.from({ length: 4 }, (_, index) => ({
|
|
...referenceBefore.generals[0],
|
|
id: index + 1,
|
|
name: `장수${index + 1}`,
|
|
nationId: 1,
|
|
cityId: 1,
|
|
officerLevel: index === 0 ? 12 : 1,
|
|
gold: 1_000,
|
|
rice: 1_000,
|
|
})),
|
|
cities: [{ ...referenceBefore.cities[0], nationId: 1 }],
|
|
nations: [
|
|
{
|
|
id: 1,
|
|
name: '위',
|
|
color: '#111111',
|
|
capitalCityId: 1,
|
|
gold: 1_000,
|
|
rice: 1_000,
|
|
power: 1_000,
|
|
level: 1,
|
|
typeCode: 'che_중립',
|
|
// Simulate a command that created three members but omitted
|
|
// the denormalized nation counter update.
|
|
generalCount: 1,
|
|
},
|
|
],
|
|
};
|
|
|
|
const trace = await runCoreTurnCommandTrace(
|
|
{
|
|
kind: 'general',
|
|
actorGeneralId: 1,
|
|
action: '휴식',
|
|
observe: { allGenerals: true, allNations: true },
|
|
},
|
|
snapshot
|
|
);
|
|
|
|
expect(trace.before.generals.filter((general) => general.nationId === 1)).toHaveLength(4);
|
|
expect(trace.before.nations).toEqual([expect.objectContaining({ id: 1, generalCount: 1 })]);
|
|
expect(trace.after.nations).toEqual([expect.objectContaining({ id: 1, generalCount: 1 })]);
|
|
});
|
|
});
|