test: expand nation command differential matrix
This commit is contained in:
@@ -1229,10 +1229,15 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
const hasNationChange = (resolution.patches?.cities ?? []).some((patch) =>
|
const hasNationChange = (resolution.patches?.cities ?? []).some((patch) =>
|
||||||
Object.prototype.hasOwnProperty.call(patch.patch ?? {}, 'nationId')
|
Object.prototype.hasOwnProperty.call(patch.patch ?? {}, 'nationId')
|
||||||
);
|
);
|
||||||
// 레거시 건국 계열은 도시의 nation만 바꾸고 supply/front를
|
// 레거시 건국 계열과 무작위 수도 이전은 명령이 직접 지정한
|
||||||
// 즉시 재계산하지 않는다. 다음 월 처리 전까지 그 상태를 보존한다.
|
// front 값만 바꾸고 전체 전선을 즉시 재계산하지 않는다.
|
||||||
const preservesFoundingFrontState = ['che_건국', 'cr_건국', 'che_무작위건국'].includes(actionKey);
|
const preservesImmediateFrontState = [
|
||||||
if (hasNationChange && !preservesFoundingFrontState) {
|
'che_건국',
|
||||||
|
'cr_건국',
|
||||||
|
'che_무작위건국',
|
||||||
|
'che_무작위수도이전',
|
||||||
|
].includes(actionKey);
|
||||||
|
if (hasNationChange && !preservesImmediateFrontState) {
|
||||||
const worldView = worldOverlay?.view ?? worldRef;
|
const worldView = worldOverlay?.view ?? worldRef;
|
||||||
if (worldView && options.map) {
|
if (worldView && options.map) {
|
||||||
const frontPatches = buildFrontStatePatches({
|
const frontPatches = buildFrontStatePatches({
|
||||||
|
|||||||
@@ -36,6 +36,15 @@ export interface RandomMoveCapitalResolveContext<
|
|||||||
|
|
||||||
const ACTION_NAME = '무작위 수도 이전';
|
const ACTION_NAME = '무작위 수도 이전';
|
||||||
|
|
||||||
|
type InclusiveRandomGenerator = GeneralActionResolveContext['rng'] & {
|
||||||
|
nextIntInclusive?: (maxInclusive: number) => number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const legacyChoiceIndex = (rng: GeneralActionResolveContext['rng'], length: number): number => {
|
||||||
|
const inclusive = rng as InclusiveRandomGenerator;
|
||||||
|
return inclusive.nextIntInclusive ? inclusive.nextIntInclusive(length - 1) : rng.nextInt(0, length);
|
||||||
|
};
|
||||||
|
|
||||||
export class ActionDefinition<
|
export class ActionDefinition<
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||||
> implements GeneralActionDefinition<
|
> implements GeneralActionDefinition<
|
||||||
@@ -89,7 +98,8 @@ export class ActionDefinition<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const destCity = neutralCandidateCities[rng.nextInt(0, neutralCandidateCities.length)]!;
|
// 레거시 RandUtil::choice()는 후보가 하나뿐이어도 nextInt(0)을 소비한다.
|
||||||
|
const destCity = neutralCandidateCities[legacyChoiceIndex(rng, neutralCandidateCities.length)]!;
|
||||||
const oldCityId = nation.capitalCityId;
|
const oldCityId = nation.capitalCityId;
|
||||||
const generalName = general.name;
|
const generalName = general.name;
|
||||||
const nationName = nation.name;
|
const nationName = nation.name;
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ export const projectCoreDatabaseSnapshot = (rows: {
|
|||||||
power: readNumber(meta, 'power'),
|
power: readNumber(meta, 'power'),
|
||||||
war: readNumber(meta, 'war'),
|
war: readNumber(meta, 'war'),
|
||||||
diplomacyLimit: readNumber(meta, 'surlimit'),
|
diplomacyLimit: readNumber(meta, 'surlimit'),
|
||||||
|
capitalRevision: readNumber(meta, 'capset'),
|
||||||
meta,
|
meta,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -245,6 +245,7 @@ const buildGeneral = (row: Record<string, unknown>, turnTime: Date): TurnGeneral
|
|||||||
const buildNation = (row: Record<string, unknown>, generals: TurnGeneral[]): Nation => {
|
const buildNation = (row: Record<string, unknown>, generals: TurnGeneral[]): Nation => {
|
||||||
const id = readNumber(row, 'id');
|
const id = readNumber(row, 'id');
|
||||||
const meta = asRecord(row.meta);
|
const meta = asRecord(row.meta);
|
||||||
|
const turnLastByOfficerLevel = asRecord(row.turnLastByOfficerLevel);
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name: readString(row, 'name', `국가${id}`),
|
name: readString(row, 'name', `국가${id}`),
|
||||||
@@ -258,10 +259,17 @@ const buildNation = (row: Record<string, unknown>, generals: TurnGeneral[]): Nat
|
|||||||
typeCode: readString(row, 'typeCode', 'che_중립'),
|
typeCode: readString(row, 'typeCode', 'che_중립'),
|
||||||
meta: {
|
meta: {
|
||||||
...meta,
|
...meta,
|
||||||
|
...Object.fromEntries(
|
||||||
|
Object.entries(turnLastByOfficerLevel).map(([officerLevel, lastTurn]) => [
|
||||||
|
`turn_last_${officerLevel}`,
|
||||||
|
lastTurn,
|
||||||
|
])
|
||||||
|
),
|
||||||
tech: readNumber(row, 'tech', readNumber(meta, 'tech')),
|
tech: readNumber(row, 'tech', readNumber(meta, 'tech')),
|
||||||
gennum: readNumber(row, 'generalCount', readNumber(meta, 'gennum')),
|
gennum: readNumber(row, 'generalCount', readNumber(meta, 'gennum')),
|
||||||
war: readNumber(row, 'war', readNumber(meta, 'war')),
|
war: readNumber(row, 'war', readNumber(meta, 'war')),
|
||||||
surlimit: readNumber(row, 'diplomacyLimit', readNumber(meta, 'surlimit')),
|
surlimit: readNumber(row, 'diplomacyLimit', readNumber(meta, 'surlimit')),
|
||||||
|
capset: readNumber(row, 'capitalRevision', readNumber(meta, 'capset')),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -276,7 +284,16 @@ const buildWorldInput = (
|
|||||||
const month = readNumber(referenceBefore.world, 'month', request.setup?.world?.month ?? 1);
|
const month = readNumber(referenceBefore.world, 'month', request.setup?.world?.month ?? 1);
|
||||||
const turnTime = new Date(`${String(year).padStart(4, '0')}-${String(month).padStart(2, '0')}-01T00:00:00.000Z`);
|
const turnTime = new Date(`${String(year).padStart(4, '0')}-${String(month).padStart(2, '0')}-01T00:00:00.000Z`);
|
||||||
const generals = referenceBefore.generals.map((row) => buildGeneral(row, turnTime));
|
const generals = referenceBefore.generals.map((row) => buildGeneral(row, turnTime));
|
||||||
const nations = referenceBefore.nations.map((row) => buildNation(row, generals));
|
const fixtureNations = new Map((request.setup?.nations ?? []).map((row) => [readNumber(row, 'id'), row] as const));
|
||||||
|
const nations = referenceBefore.nations.map((row) =>
|
||||||
|
buildNation(
|
||||||
|
{
|
||||||
|
...row,
|
||||||
|
turnLastByOfficerLevel: fixtureNations.get(readNumber(row, 'id'))?.turnLastByOfficerLevel,
|
||||||
|
},
|
||||||
|
generals
|
||||||
|
)
|
||||||
|
);
|
||||||
const observedCityRows = new Map(referenceBefore.cities.map((row) => [readNumber(row, 'id'), row] as const));
|
const observedCityRows = new Map(referenceBefore.cities.map((row) => [readNumber(row, 'id'), row] as const));
|
||||||
const randomFoundingCandidateCityIds = request.setup?.randomFoundingCandidateCityIds
|
const randomFoundingCandidateCityIds = request.setup?.randomFoundingCandidateCityIds
|
||||||
? new Set(request.setup.randomFoundingCandidateCityIds)
|
? new Set(request.setup.randomFoundingCandidateCityIds)
|
||||||
@@ -511,6 +528,7 @@ const projectWorld = (
|
|||||||
power: nation.power,
|
power: nation.power,
|
||||||
war: readNumber(nation.meta, 'war'),
|
war: readNumber(nation.meta, 'war'),
|
||||||
diplomacyLimit: readNumber(nation.meta, 'surlimit'),
|
diplomacyLimit: readNumber(nation.meta, 'surlimit'),
|
||||||
|
capitalRevision: readNumber(nation.meta, 'capset'),
|
||||||
meta: nation.meta,
|
meta: nation.meta,
|
||||||
})),
|
})),
|
||||||
diplomacy: world
|
diplomacy: world
|
||||||
|
|||||||
@@ -59,7 +59,21 @@ const general = (id: number, nationId: number, cityId: number, officerLevel: num
|
|||||||
meta: {},
|
meta: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const buildRequest = (action: string, args?: Record<string, unknown>): TurnCommandFixtureRequest => ({
|
interface FixturePatches {
|
||||||
|
world?: Partial<NonNullable<NonNullable<TurnCommandFixtureRequest['setup']>['world']>>;
|
||||||
|
generals?: Record<number, Record<string, unknown>>;
|
||||||
|
nations?: Record<number, Record<string, unknown>>;
|
||||||
|
cities?: Record<number, Record<string, unknown>>;
|
||||||
|
troops?: Array<Record<string, unknown>>;
|
||||||
|
diplomacy?: Record<string, Record<string, unknown>>;
|
||||||
|
randomFoundingCandidateCityIds?: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildRequest = (
|
||||||
|
action: string,
|
||||||
|
args?: Record<string, unknown>,
|
||||||
|
fixturePatches: FixturePatches = {}
|
||||||
|
): TurnCommandFixtureRequest => ({
|
||||||
kind: 'nation',
|
kind: 'nation',
|
||||||
actorGeneralId: 1,
|
actorGeneralId: 1,
|
||||||
action,
|
action,
|
||||||
@@ -71,6 +85,7 @@ const buildRequest = (action: string, args?: Record<string, unknown>): TurnComma
|
|||||||
year: 190,
|
year: 190,
|
||||||
month: 1,
|
month: 1,
|
||||||
hiddenSeed: 'turn-command-nation-matrix-v1',
|
hiddenSeed: 'turn-command-nation-matrix-v1',
|
||||||
|
...fixturePatches.world,
|
||||||
},
|
},
|
||||||
nations: [
|
nations: [
|
||||||
{
|
{
|
||||||
@@ -86,6 +101,7 @@ const buildRequest = (action: string, args?: Record<string, unknown>): TurnComma
|
|||||||
diplomacyLimit: 0,
|
diplomacyLimit: 0,
|
||||||
generalCount: 2,
|
generalCount: 2,
|
||||||
meta: { can_국호변경: 1, can_국기변경: 1, surlimit: 0 },
|
meta: { can_국호변경: 1, can_국기변경: 1, surlimit: 0 },
|
||||||
|
...fixturePatches.nations?.[1],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
@@ -100,6 +116,7 @@ const buildRequest = (action: string, args?: Record<string, unknown>): TurnComma
|
|||||||
diplomacyLimit: 0,
|
diplomacyLimit: 0,
|
||||||
generalCount: 1,
|
generalCount: 1,
|
||||||
meta: { surlimit: 0 },
|
meta: { surlimit: 0 },
|
||||||
|
...fixturePatches.nations?.[2],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
cities: [
|
cities: [
|
||||||
@@ -118,6 +135,7 @@ const buildRequest = (action: string, args?: Record<string, unknown>): TurnComma
|
|||||||
term: 0,
|
term: 0,
|
||||||
trust: 80,
|
trust: 80,
|
||||||
trade: 100,
|
trade: 100,
|
||||||
|
...fixturePatches.cities?.[3],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 70,
|
id: 70,
|
||||||
@@ -134,12 +152,35 @@ const buildRequest = (action: string, args?: Record<string, unknown>): TurnComma
|
|||||||
term: 0,
|
term: 0,
|
||||||
trust: 80,
|
trust: 80,
|
||||||
trade: 100,
|
trade: 100,
|
||||||
|
...fixturePatches.cities?.[70],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
generals: [general(1, 1, 3, 12), general(2, 2, 70, 12), general(3, 1, 3, 1)],
|
generals: [
|
||||||
|
{ ...general(1, 1, 3, 12), ...fixturePatches.generals?.[1] },
|
||||||
|
{ ...general(2, 2, 70, 12), ...fixturePatches.generals?.[2] },
|
||||||
|
{ ...general(3, 1, 3, 1), ...fixturePatches.generals?.[3] },
|
||||||
|
],
|
||||||
|
...(fixturePatches.troops ? { troops: fixturePatches.troops } : {}),
|
||||||
|
...(fixturePatches.randomFoundingCandidateCityIds
|
||||||
|
? { randomFoundingCandidateCityIds: fixturePatches.randomFoundingCandidateCityIds }
|
||||||
|
: {}),
|
||||||
diplomacy: [
|
diplomacy: [
|
||||||
{ fromNationId: 1, toNationId: 2, state: 3, term: 0, dead: 0 },
|
{
|
||||||
{ fromNationId: 2, toNationId: 1, state: 3, term: 0, dead: 0 },
|
fromNationId: 1,
|
||||||
|
toNationId: 2,
|
||||||
|
state: 3,
|
||||||
|
term: 0,
|
||||||
|
dead: 0,
|
||||||
|
...fixturePatches.diplomacy?.['1:2'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fromNationId: 2,
|
||||||
|
toNationId: 1,
|
||||||
|
state: 3,
|
||||||
|
term: 0,
|
||||||
|
dead: 0,
|
||||||
|
...fixturePatches.diplomacy?.['2:1'],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
observe: {
|
observe: {
|
||||||
@@ -151,7 +192,7 @@ const buildRequest = (action: string, args?: Record<string, unknown>): TurnComma
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const cases: Array<[string, Record<string, unknown> | undefined]> = [
|
const cases: Array<[string, Record<string, unknown> | undefined, FixturePatches?]> = [
|
||||||
['휴식', undefined],
|
['휴식', undefined],
|
||||||
['che_포상', { isGold: true, amount: 100, destGeneralID: 3 }],
|
['che_포상', { isGold: true, amount: 100, destGeneralID: 3 }],
|
||||||
['che_선전포고', { destNationID: 2 }],
|
['che_선전포고', { destNationID: 2 }],
|
||||||
@@ -160,13 +201,94 @@ const cases: Array<[string, Record<string, unknown> | undefined]> = [
|
|||||||
['che_몰수', { isGold: true, amount: 100, destGeneralID: 3 }],
|
['che_몰수', { isGold: true, amount: 100, destGeneralID: 3 }],
|
||||||
['che_물자원조', { destNationID: 2, amountList: [100, 200] }],
|
['che_물자원조', { destNationID: 2, amountList: [100, 200] }],
|
||||||
['che_불가침제의', { destNationID: 2, year: 191, month: 1 }],
|
['che_불가침제의', { destNationID: 2, year: 191, month: 1 }],
|
||||||
|
[
|
||||||
|
'che_부대탈퇴지시',
|
||||||
|
{ destGeneralID: 3 },
|
||||||
|
{
|
||||||
|
generals: { 1: { troopId: 1 }, 3: { troopId: 1 } },
|
||||||
|
troops: [{ id: 1, nationId: 1, name: '조조군' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
['che_발령', { destGeneralID: 3, destCityID: 70 }, { cities: { 70: { nationId: 1 } } }],
|
||||||
|
['che_종전제의', { destNationID: 2 }, { diplomacy: { '1:2': { state: 0 }, '2:1': { state: 0 } } }],
|
||||||
|
['che_불가침파기제의', { destNationID: 2 }, { diplomacy: { '1:2': { state: 7 }, '2:1': { state: 7 } } }],
|
||||||
|
['cr_인구이동', { destCityID: 70, amount: 1000 }, { cities: { 70: { nationId: 1, supplyState: 1 } } }],
|
||||||
|
[
|
||||||
|
'che_천도',
|
||||||
|
{ destCityID: 70 },
|
||||||
|
{
|
||||||
|
nations: {
|
||||||
|
1: {
|
||||||
|
capitalRevision: 0,
|
||||||
|
turnLastByOfficerLevel: {
|
||||||
|
12: {
|
||||||
|
command: '천도',
|
||||||
|
arg: { destCityID: 70 },
|
||||||
|
term: 2,
|
||||||
|
seq: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cities: { 70: { nationId: 1, supplyState: 1 } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'che_증축',
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
nations: {
|
||||||
|
1: {
|
||||||
|
capitalRevision: 0,
|
||||||
|
turnLastByOfficerLevel: {
|
||||||
|
12: { command: '증축', arg: {}, term: 5, seq: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cities: { 3: { level: 7 } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'che_감축',
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
nations: {
|
||||||
|
1: {
|
||||||
|
capitalRevision: 0,
|
||||||
|
turnLastByOfficerLevel: {
|
||||||
|
12: { command: '감축', arg: {}, term: 5, seq: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cities: { 3: { level: 9 } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'che_무작위수도이전',
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
world: { year: 181 },
|
||||||
|
nations: {
|
||||||
|
1: {
|
||||||
|
meta: {
|
||||||
|
can_무작위수도이전: 1,
|
||||||
|
},
|
||||||
|
turnLastByOfficerLevel: {
|
||||||
|
12: { command: '무작위 수도 이전', arg: {}, term: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cities: { 70: { nationId: 0, supplyState: 0 } },
|
||||||
|
randomFoundingCandidateCityIds: [70],
|
||||||
|
},
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
integration('nation command success matrix', () => {
|
integration('nation command success matrix', () => {
|
||||||
it.each(cases)(
|
it.each(cases)(
|
||||||
'%s matches the legacy state delta and command RNG',
|
'%s matches the legacy state delta and command RNG',
|
||||||
async (action, args) => {
|
async (action, args, fixturePatches) => {
|
||||||
const request = buildRequest(action, args);
|
const request = buildRequest(action, args, fixturePatches);
|
||||||
const reference = runReferenceTurnCommandTraceRequest(
|
const reference = runReferenceTurnCommandTraceRequest(
|
||||||
workspaceRoot!,
|
workspaceRoot!,
|
||||||
request as unknown as Record<string, unknown>
|
request as unknown as Record<string, unknown>
|
||||||
|
|||||||
Reference in New Issue
Block a user