test: cover multi-turn nation strategy commands
This commit is contained in:
@@ -62,8 +62,9 @@ normalized to the core `*Id` spelling before command identity comparison.
|
|||||||
The matrix includes the four-call `전투태세` path, lifecycle and
|
The matrix includes the four-call `전투태세` path, lifecycle and
|
||||||
appointment commands, troop assembly, rebellion and all three founding
|
appointment commands, troop assembly, rebellion and all three founding
|
||||||
variants.
|
variants.
|
||||||
- `turnCommandNationMatrix.integration.test.ts`: 26 successful nation
|
- `turnCommandNationMatrix.integration.test.ts`: 32 successful nation
|
||||||
command paths, including all nine unit-research commands.
|
command paths, including all nine unit-research commands and the
|
||||||
|
multi-turn `필사즉생`, `허보`, and `초토화` paths.
|
||||||
- `turnCommandCoreReference.integration.test.ts`: declaration and live
|
- `turnCommandCoreReference.integration.test.ts`: declaration and live
|
||||||
sortie fixtures.
|
sortie fixtures.
|
||||||
|
|
||||||
@@ -168,11 +169,11 @@ Compatibility is established per case only when:
|
|||||||
4. live sortie also passes the battle trace comparison;
|
4. live sortie also passes the battle trace comparison;
|
||||||
5. any ignored path is documented in the case evidence.
|
5. any ignored path is documented in the case evidence.
|
||||||
|
|
||||||
As of 2026-07-26, 54 general matrix cases, 26 nation matrix cases, declaration
|
As of 2026-07-26, 54 general matrix cases, 32 nation matrix cases, declaration
|
||||||
and live sortie pass this boundary. Live sortie is the remaining general
|
and live sortie pass this boundary. Live sortie is the remaining general
|
||||||
command key and covers battle entry, conquest, defeated-general neutralization
|
command key and covers battle entry, conquest, defeated-general neutralization
|
||||||
and last-city nation collapse. Thus all 55 general command keys have a
|
and last-city nation collapse. Thus all 55 general command keys have a
|
||||||
completed success-path comparison. This is 82 executable comparison cases;
|
completed success-path comparison. This is 88 executable comparison cases;
|
||||||
it is not yet a claim that failure/boundary paths or all 38 nation commands
|
it is not yet a claim that failure/boundary paths or all 38 nation commands
|
||||||
have been dynamically compared.
|
have been dynamically compared.
|
||||||
|
|
||||||
|
|||||||
@@ -48,14 +48,23 @@ const DEFAULT_GLOBAL_DELAY = 9;
|
|||||||
const PRE_REQ_TURN = 1;
|
const PRE_REQ_TURN = 1;
|
||||||
const EXP_DED_GAIN = 5 * (PRE_REQ_TURN + 1);
|
const EXP_DED_GAIN = 5 * (PRE_REQ_TURN + 1);
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
const pickMoveCityId = (rng: GeneralActionResolveContext['rng'], destCityId: number, candidates: City[]): number => {
|
const pickMoveCityId = (rng: GeneralActionResolveContext['rng'], destCityId: number, candidates: City[]): number => {
|
||||||
if (candidates.length === 0) {
|
if (candidates.length === 0) {
|
||||||
return destCityId;
|
return destCityId;
|
||||||
}
|
}
|
||||||
let idx = rng.nextInt(0, candidates.length);
|
let idx = legacyChoiceIndex(rng, candidates.length);
|
||||||
let cityId = candidates[idx]?.id ?? destCityId;
|
let cityId = candidates[idx]?.id ?? destCityId;
|
||||||
if (cityId === destCityId && candidates.length > 1) {
|
if (cityId === destCityId) {
|
||||||
idx = rng.nextInt(0, candidates.length);
|
idx = legacyChoiceIndex(rng, candidates.length);
|
||||||
cityId = candidates[idx]?.id ?? destCityId;
|
cityId = candidates[idx]?.id ?? destCityId;
|
||||||
}
|
}
|
||||||
return cityId;
|
return cityId;
|
||||||
|
|||||||
@@ -93,8 +93,51 @@ export const disallowDiplomacyBetweenStatus = (disallowList: Record<number, stri
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const disallowDiplomacyStatus = (disallowList: Record<number, string>): Constraint => ({
|
export const disallowDiplomacyStatus = (disallowList: Record<number, string>): Constraint => ({
|
||||||
...disallowDiplomacyBetweenStatus(disallowList),
|
|
||||||
name: 'disallowDiplomacyStatus',
|
name: 'disallowDiplomacyStatus',
|
||||||
|
requires: (ctx) => [
|
||||||
|
...(ctx.nationId !== undefined ? ([{ kind: 'nation', id: ctx.nationId }] as RequirementKey[]) : []),
|
||||||
|
{ kind: 'diplomacyList' },
|
||||||
|
],
|
||||||
|
test: (ctx, view) => {
|
||||||
|
const general = readGeneral(ctx, view);
|
||||||
|
const baseNationId = ctx.nationId ?? general?.nationId;
|
||||||
|
if (baseNationId === undefined) {
|
||||||
|
return unknownOrDeny(ctx, [], '국가 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const req: RequirementKey = { kind: 'diplomacyList' };
|
||||||
|
if (!view.has(req)) {
|
||||||
|
return unknownOrDeny(ctx, [req], '외교 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const entries = view.get(req);
|
||||||
|
if (!Array.isArray(entries)) {
|
||||||
|
return unknownOrDeny(ctx, [req], '외교 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
for (const value of entries) {
|
||||||
|
if (!value || typeof value !== 'object') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const entry = value as {
|
||||||
|
fromNationId?: number;
|
||||||
|
srcNationId?: number;
|
||||||
|
me?: number;
|
||||||
|
state?: number;
|
||||||
|
stateCode?: number;
|
||||||
|
};
|
||||||
|
const fromNationId = entry.fromNationId ?? entry.srcNationId ?? entry.me;
|
||||||
|
if (fromNationId !== baseNationId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const state = entry.state ?? entry.stateCode;
|
||||||
|
if (state === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const reason = disallowList[state];
|
||||||
|
if (reason !== undefined) {
|
||||||
|
return { kind: 'deny', reason };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allow();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const allowDiplomacyBetweenStatus = (allowList: number[], reason: string): Constraint => ({
|
export const allowDiplomacyBetweenStatus = (allowList: number[], reason: string): Constraint => ({
|
||||||
|
|||||||
@@ -422,6 +422,37 @@ describe('Nation Missing Actions', () => {
|
|||||||
expect(result.kind).toBe('deny');
|
expect(result.kind).toBe('deny');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('che_초토화: blocks when the nation is at war with any nation', () => {
|
||||||
|
const general = buildGeneral(1, 1, 1);
|
||||||
|
const nation = buildNation(1);
|
||||||
|
const destNation = buildNation(2);
|
||||||
|
const city = buildCity(1, 1);
|
||||||
|
const destCity = buildCity(2, 2);
|
||||||
|
const view = new TestStateView();
|
||||||
|
view.set({ kind: 'general', id: general.id }, general);
|
||||||
|
view.set({ kind: 'city', id: city.id }, city);
|
||||||
|
view.set({ kind: 'nation', id: nation.id }, nation);
|
||||||
|
view.set({ kind: 'destCity', id: destCity.id }, destCity);
|
||||||
|
view.set({ kind: 'destNation', id: destNation.id }, destNation);
|
||||||
|
setupDiplomacy(view, nation.id, destNation.id, 3);
|
||||||
|
view.set({ kind: 'diplomacyList' }, [{ fromNationId: nation.id, toNationId: 3, state: 0, term: 12 }]);
|
||||||
|
|
||||||
|
const definition = new ScorchedEarthAction();
|
||||||
|
const args = { destCityId: destCity.id };
|
||||||
|
const ctx: ConstraintContext = {
|
||||||
|
actorId: general.id,
|
||||||
|
nationId: nation.id,
|
||||||
|
cityId: city.id,
|
||||||
|
destCityId: destCity.id,
|
||||||
|
destNationId: destNation.id,
|
||||||
|
args,
|
||||||
|
env: {},
|
||||||
|
mode: 'full',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(evaluateConstraints(definition.buildConstraints(ctx, args), ctx, view).kind).toBe('deny');
|
||||||
|
});
|
||||||
|
|
||||||
it('che_초토화: neutralizes city and increases nation resources', () => {
|
it('che_초토화: neutralizes city and increases nation resources', () => {
|
||||||
const general = buildGeneral(1, 1, 1);
|
const general = buildGeneral(1, 1, 1);
|
||||||
const nation = buildNation(1);
|
const nation = buildNation(1);
|
||||||
|
|||||||
@@ -290,7 +290,9 @@ const buildWorldInput = (
|
|||||||
buildNation(
|
buildNation(
|
||||||
{
|
{
|
||||||
...row,
|
...row,
|
||||||
turnLastByOfficerLevel: fixtureNations.get(readNumber(row, 'id'))?.turnLastByOfficerLevel,
|
turnLastByOfficerLevel:
|
||||||
|
fixtureNations.get(readNumber(row, 'id'))?.coreTurnLastByOfficerLevel ??
|
||||||
|
fixtureNations.get(readNumber(row, 'id'))?.turnLastByOfficerLevel,
|
||||||
},
|
},
|
||||||
generals
|
generals
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -316,6 +316,54 @@ const cases: NationMatrixCase[] = [
|
|||||||
{ diplomacy: { '1:2': { state: 1, term: 12 }, '2:1': { state: 1, term: 12 } } },
|
{ diplomacy: { '1:2': { state: 1, term: 12 }, '2:1': { state: 1, term: 12 } } },
|
||||||
],
|
],
|
||||||
['che_급습', { destNationID: 2 }, { diplomacy: { '1:2': { state: 1, term: 12 }, '2:1': { state: 1, term: 12 } } }],
|
['che_급습', { destNationID: 2 }, { diplomacy: { '1:2': { state: 1, term: 12 }, '2:1': { state: 1, term: 12 } } }],
|
||||||
|
[
|
||||||
|
'che_필사즉생',
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
nations: {
|
||||||
|
1: {
|
||||||
|
turnLastByOfficerLevel: {
|
||||||
|
12: { command: '필사즉생', arg: {}, term: 2 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
diplomacy: { '1:2': { state: 0 }, '2:1': { state: 0 } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'che_허보',
|
||||||
|
{ destCityID: 70 },
|
||||||
|
{
|
||||||
|
nations: {
|
||||||
|
1: {
|
||||||
|
turnLastByOfficerLevel: {
|
||||||
|
12: { command: '허보', arg: { destCityID: 70 }, term: 1 },
|
||||||
|
},
|
||||||
|
coreTurnLastByOfficerLevel: {
|
||||||
|
12: { command: '허보', arg: { destCityId: 70 }, term: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
diplomacy: { '1:2': { state: 0 }, '2:1': { state: 0 } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'che_초토화',
|
||||||
|
{ destCityID: 70 },
|
||||||
|
{
|
||||||
|
nations: {
|
||||||
|
1: {
|
||||||
|
turnLastByOfficerLevel: {
|
||||||
|
12: { command: '초토화', arg: { destCityID: 70 }, term: 2 },
|
||||||
|
},
|
||||||
|
coreTurnLastByOfficerLevel: {
|
||||||
|
12: { command: '초토화', arg: { destCityId: 70 }, term: 2 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cities: { 70: { nationId: 1, supplyState: 1 } },
|
||||||
|
},
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
integration('nation command success matrix', () => {
|
integration('nation command success matrix', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user