merge: 국가 사령턴 예약 수명주기 검증을 반영
This commit is contained in:
@@ -64,6 +64,8 @@ export interface TurnCommandFixtureRequest {
|
||||
cities?: Array<Record<string, unknown>>;
|
||||
troops?: Array<Record<string, unknown>>;
|
||||
diplomacy?: Array<Record<string, unknown>>;
|
||||
generalTurns?: Array<Record<string, unknown>>;
|
||||
nationTurns?: Array<Record<string, unknown>>;
|
||||
randomFoundingCandidateCityIds?: number[];
|
||||
generalCooldowns?: Array<GeneralCooldownSelector & { nextAvailableTurn: number }>;
|
||||
};
|
||||
@@ -741,18 +743,35 @@ const projectWorld = (
|
||||
};
|
||||
};
|
||||
|
||||
const emptyDatabaseClient = {
|
||||
const buildSnapshotReservedTurnDatabaseClient = (snapshot: CanonicalTurnSnapshot) => ({
|
||||
generalTurn: {
|
||||
findMany: async () => [],
|
||||
findMany: async () =>
|
||||
snapshot.generalTurns.map((turn, index) => ({
|
||||
id: index + 1,
|
||||
generalId: readNumber(turn, 'generalId'),
|
||||
turnIdx: readNumber(turn, 'turnIndex'),
|
||||
actionCode: readString(turn, 'action', '휴식'),
|
||||
arg: asRecord(turn.args),
|
||||
createdAt: new Date(0),
|
||||
})),
|
||||
deleteMany: async () => ({ count: 0 }),
|
||||
createMany: async () => ({ count: 0 }),
|
||||
},
|
||||
nationTurn: {
|
||||
findMany: async () => [],
|
||||
findMany: async () =>
|
||||
snapshot.nationTurns.map((turn, index) => ({
|
||||
id: index + 1,
|
||||
nationId: readNumber(turn, 'nationId'),
|
||||
officerLevel: readNumber(turn, 'officerLevel'),
|
||||
turnIdx: readNumber(turn, 'turnIndex'),
|
||||
actionCode: readString(turn, 'action', '휴식'),
|
||||
arg: asRecord(turn.args),
|
||||
createdAt: new Date(0),
|
||||
})),
|
||||
deleteMany: async () => ({ count: 0 }),
|
||||
createMany: async () => ({ count: 0 }),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const runCoreTurnCommandTrace = async (
|
||||
request: TurnCommandFixtureRequest,
|
||||
@@ -776,10 +795,13 @@ export const runCoreTurnCommandTrace = async (
|
||||
generalCooldowns: request.observe?.generalCooldowns ?? [],
|
||||
nationCooldowns: request.observe?.nationCooldowns ?? [],
|
||||
};
|
||||
const reservedTurns = new InMemoryReservedTurnStore(emptyDatabaseClient as never, {
|
||||
maxGeneralTurns: 30,
|
||||
maxNationTurns: 12,
|
||||
});
|
||||
const reservedTurns = new InMemoryReservedTurnStore(
|
||||
buildSnapshotReservedTurnDatabaseClient(referenceBefore) as never,
|
||||
{
|
||||
maxGeneralTurns: 30,
|
||||
maxNationTurns: 12,
|
||||
}
|
||||
);
|
||||
await reservedTurns.loadAll();
|
||||
const actor = snapshot.generals.find((general) => general.id === request.actorGeneralId);
|
||||
if (!actor) {
|
||||
@@ -851,7 +873,9 @@ export const runCoreTurnCommandTrace = async (
|
||||
},
|
||||
});
|
||||
world = new InMemoryTurnWorld(state, snapshot, {
|
||||
schedule: { entries: [{ startMinute: 0, tickMinutes: 10 }] },
|
||||
schedule: {
|
||||
entries: [{ startMinute: 0, tickMinutes: Math.max(1, Math.round(state.tickSeconds / 60)) }],
|
||||
},
|
||||
generalTurnHandler: handler,
|
||||
});
|
||||
const before = projectWorld(world, reservedTurns, [], [], selector);
|
||||
|
||||
@@ -212,6 +212,7 @@ integration('core ↔ legacy command-boundary differential', () => {
|
||||
const afterActor = reference.after.generals.find((general) => general.id === actorGeneralId);
|
||||
const coreBeforeActor = core.before.generals.find((general) => general.id === actorGeneralId);
|
||||
const coreAfterActor = core.after.generals.find((general) => general.id === actorGeneralId);
|
||||
const expectedTurnMinutes = Number(reference.before.world.tickMinutes);
|
||||
expect(
|
||||
reference.before.generalTurns.find(
|
||||
(turn) => turn.generalId === actorGeneralId && turn.turnIndex === 0
|
||||
@@ -223,10 +224,10 @@ integration('core ↔ legacy command-boundary differential', () => {
|
||||
)?.action
|
||||
).toBe('휴식');
|
||||
expect(timestampMillis(afterActor?.turnTime) - timestampMillis(beforeActor?.turnTime)).toBe(
|
||||
10 * 60_000
|
||||
expectedTurnMinutes * 60_000
|
||||
);
|
||||
expect(timestampMillis(coreAfterActor?.turnTime) - timestampMillis(coreBeforeActor?.turnTime)).toBe(
|
||||
10 * 60_000
|
||||
expectedTurnMinutes * 60_000
|
||||
);
|
||||
expect(timestampMillis(coreAfterActor?.turnTime)).toBe(timestampMillis(afterActor?.turnTime));
|
||||
expect(afterActor?.mySet).toBe(Math.min(9, Number(beforeActor?.mySet) + 3));
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { NATION_TURN_COMMAND_KEYS } from '@sammo-ts/logic';
|
||||
|
||||
import { compareTurnSnapshotDeltas } from '../src/turn-differential/compare.js';
|
||||
import { runCoreTurnCommandTrace, type TurnCommandFixtureRequest } from '../src/turn-differential/coreCommandTrace.js';
|
||||
import {
|
||||
@@ -27,6 +29,12 @@ const readNationMeta = (row: { meta?: unknown } | undefined): Record<string, unk
|
||||
const readGeneralMeta = readNationMeta;
|
||||
const NPC_SEIZURE_MESSAGE_TEXT = '몰수를 하다니... 이것이 윗사람이 할 짓이란 말입니까...';
|
||||
|
||||
const timestampMillis = (value: unknown): number => {
|
||||
const raw = String(value);
|
||||
const normalized = raw.includes('T') ? raw : `${raw.replace(' ', 'T').replace(/\.(\d{3})\d*$/, '.$1')}Z`;
|
||||
return new Date(normalized).getTime();
|
||||
};
|
||||
|
||||
const normalizeStoredLogText = (value: unknown): string =>
|
||||
String(value)
|
||||
.replace(/^(?:<C>●<\/>|<S>◆<\/>|<R>★<\/>)(?:(?:\d+년 )?\d+월:|\d+년:)?/, '')
|
||||
@@ -345,6 +353,15 @@ const buildRequest = (
|
||||
};
|
||||
}),
|
||||
],
|
||||
nationTurns: [
|
||||
{
|
||||
nationId: 1,
|
||||
officerLevel: 12,
|
||||
turnIndex: 1,
|
||||
action: 'che_국호변경',
|
||||
args: { nationName: '다음국' },
|
||||
},
|
||||
],
|
||||
},
|
||||
observe: {
|
||||
generalIds: [
|
||||
@@ -628,11 +645,18 @@ const cases: NationMatrixCase[] = [
|
||||
],
|
||||
];
|
||||
|
||||
describe('nation command differential coverage manifest', () => {
|
||||
it('keeps one successful Ref/Core case for every registered nation turn command', () => {
|
||||
expect(new Set(cases.map(([action]) => action))).toEqual(new Set(NATION_TURN_COMMAND_KEYS));
|
||||
});
|
||||
});
|
||||
|
||||
integration('nation command success matrix', () => {
|
||||
it.each(cases)(
|
||||
'%s matches the legacy state delta and command RNG',
|
||||
async (action, args, fixturePatches) => {
|
||||
const request = buildRequest(action, args, fixturePatches);
|
||||
request.includeLifecycle = true;
|
||||
const reference = runReferenceTurnCommandTraceRequest(
|
||||
workspaceRoot!,
|
||||
request as unknown as Record<string, unknown>
|
||||
@@ -667,6 +691,42 @@ integration('nation command success matrix', () => {
|
||||
expect(reference.execution.outcome).toMatchObject({ completed: true });
|
||||
expect(core.execution.outcome).not.toHaveProperty('blockedReason');
|
||||
expect(core.rng).toEqual(reference.rng);
|
||||
|
||||
const actorGeneralId = request.actorGeneralId;
|
||||
const actorNationId = 1;
|
||||
const actorOfficerLevel = 12;
|
||||
const referenceBeforeActor = reference.before.generals.find((general) => general.id === actorGeneralId);
|
||||
const referenceAfterActor = reference.after.generals.find((general) => general.id === actorGeneralId);
|
||||
const coreBeforeActor = core.before.generals.find((general) => general.id === actorGeneralId);
|
||||
const coreAfterActor = core.after.generals.find((general) => general.id === actorGeneralId);
|
||||
const expectedTurnMinutes = Number(reference.before.world.tickMinutes);
|
||||
const findActorNationTurn = (turns: Array<Record<string, unknown>>, turnIndex: number) =>
|
||||
turns.find(
|
||||
(turn) =>
|
||||
turn.nationId === actorNationId &&
|
||||
turn.officerLevel === actorOfficerLevel &&
|
||||
turn.turnIndex === turnIndex
|
||||
);
|
||||
|
||||
expect(findActorNationTurn(reference.before.nationTurns, 0)?.action).toBe(action);
|
||||
expect(findActorNationTurn(core.before.nationTurns, 0)?.action).toBe(action);
|
||||
expect(findActorNationTurn(reference.after.nationTurns, 0)).toMatchObject({
|
||||
action: 'che_국호변경',
|
||||
args: { nationName: '다음국' },
|
||||
});
|
||||
expect(findActorNationTurn(core.after.nationTurns, 0)).toMatchObject({
|
||||
action: 'che_국호변경',
|
||||
args: { nationName: '다음국' },
|
||||
});
|
||||
expect(
|
||||
timestampMillis(referenceAfterActor?.turnTime) - timestampMillis(referenceBeforeActor?.turnTime)
|
||||
).toBe(expectedTurnMinutes * 60_000);
|
||||
expect(timestampMillis(coreAfterActor?.turnTime) - timestampMillis(coreBeforeActor?.turnTime)).toBe(
|
||||
expectedTurnMinutes * 60_000
|
||||
);
|
||||
expect(timestampMillis(coreAfterActor?.turnTime)).toBe(timestampMillis(referenceAfterActor?.turnTime));
|
||||
expect(referenceAfterActor?.mySet).toBe(Math.min(9, Number(referenceBeforeActor?.mySet) + 3));
|
||||
expect(coreAfterActor?.mySet).toBe(referenceAfterActor?.mySet);
|
||||
expect(
|
||||
compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
|
||||
ignoredPathPatterns: ignoredLifecyclePaths,
|
||||
@@ -702,6 +762,31 @@ integration('nation command success matrix', () => {
|
||||
);
|
||||
});
|
||||
|
||||
integration('legacy nation lifecycle comparison guard', () => {
|
||||
it('does not alter command effects or RNG beyond the explicit queue and turn lifecycle', () => {
|
||||
const request = buildRequest('che_포상', { isGold: true, amount: 100, destGeneralID: 3 });
|
||||
const commandOnly = runReferenceTurnCommandTraceRequest(
|
||||
workspaceRoot!,
|
||||
request as unknown as Record<string, unknown>
|
||||
);
|
||||
const withLifecycle = runReferenceTurnCommandTraceRequest(workspaceRoot!, {
|
||||
...request,
|
||||
includeLifecycle: true,
|
||||
} as unknown as Record<string, unknown>);
|
||||
|
||||
expect(withLifecycle.rng).toEqual(commandOnly.rng);
|
||||
expect(
|
||||
compareTurnSnapshotDeltas(
|
||||
commandOnly.before,
|
||||
commandOnly.after,
|
||||
withLifecycle.before,
|
||||
withLifecycle.after,
|
||||
{ ignoredPathPatterns: ignoredLifecyclePaths }
|
||||
)
|
||||
).toEqual([]);
|
||||
}, 180_000);
|
||||
});
|
||||
|
||||
type NationActiveActionInheritanceCase = {
|
||||
name: string;
|
||||
request: NationMatrixCase;
|
||||
|
||||
Reference in New Issue
Block a user