fix: match scenario 2601 monthly seed progression
This commit is contained in:
@@ -191,6 +191,10 @@ describe('Blank Start Scenario', () => {
|
||||
const newNation = world.getNation(newNationId)!;
|
||||
expect(newNation.chiefGeneralId).toBe(gen0.id);
|
||||
expect(newNation.level).toBe(0); // Wandering Nation
|
||||
// Ref che_거병 does not install a nation-specific NPC policy. Leaving
|
||||
// this key absent preserves AutorunNationPolicy's 50,000 population
|
||||
// floor instead of silently overriding it with zero.
|
||||
expect(newNation.meta).not.toHaveProperty('npc_nation_policy');
|
||||
|
||||
// --- Step 2: Gen 1 performs Appointment ---
|
||||
// Before appointment, Gen 0 should FAIL Founding because general count = 1
|
||||
|
||||
@@ -6,6 +6,8 @@ import type { MapDefinition } from '../../../src/world/types.js';
|
||||
import type { TurnCommandEnv } from '../../../src/actions/turn/commandEnv.js';
|
||||
import type { ConstraintContext, RequirementKey, StateView } from '../../../src/constraints/types.js';
|
||||
import { evaluateActionConstraints } from '../../../src/constraints/evaluate.js';
|
||||
import { createRefOrderedActionStack } from '../../../src/actionModules/bundle.js';
|
||||
import type { GeneralActionModule } from '../../../src/actionModules/general.js';
|
||||
|
||||
const MOCK_SCENARIO_BASE = {
|
||||
title: 'Test',
|
||||
@@ -146,7 +148,7 @@ function createViewState(world: InMemoryWorld, year: number = 200, env: TurnComm
|
||||
}
|
||||
|
||||
describe('che_귀환', () => {
|
||||
it('should return to capital if normal officer', async () => {
|
||||
it('should return to capital and apply legacy experience modifiers', async () => {
|
||||
const bootstrapResult = buildScenarioBootstrap({
|
||||
scenario: MOCK_SCENARIO_BASE,
|
||||
map: LINEAR_MAP,
|
||||
@@ -203,7 +205,23 @@ describe('che_귀환', () => {
|
||||
generalId: general.id,
|
||||
commandKey: 'che_귀환',
|
||||
resolver: (await import('../../../src/actions/turn/general/che_귀환.js')).commandSpec.createDefinition(
|
||||
{} as any
|
||||
{
|
||||
...systemEnv,
|
||||
generalActionModules: createRefOrderedActionStack<GeneralActionModule>({
|
||||
nation: {
|
||||
onCalcStat: (_context, statName, value) =>
|
||||
statName === 'experience' && typeof value === 'number' ? value * 1.1 : value,
|
||||
},
|
||||
officer: {},
|
||||
domestic: {},
|
||||
war: {},
|
||||
personality: {},
|
||||
crewType: null,
|
||||
inheritance: {},
|
||||
scenario: null,
|
||||
items: [],
|
||||
}),
|
||||
}
|
||||
),
|
||||
args: {},
|
||||
},
|
||||
@@ -211,7 +229,7 @@ describe('che_귀환', () => {
|
||||
|
||||
const updated = world.getGeneral(general.id);
|
||||
expect(updated?.cityId).toBe(101); // Capital
|
||||
expect(updated?.experience).toBe(70);
|
||||
expect(updated?.experience).toBe(77);
|
||||
expect(updated?.dedication).toBe(100);
|
||||
expect(updated?.meta.leadership_exp).toBe(1);
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@ import { MINIMAL_MAP } from '../fixtures/minimalMap.js';
|
||||
import { InMemoryWorld, TestGameRunner } from '../testEnv.js';
|
||||
import type { City, General, Nation } from '../../src/domain/entities.js';
|
||||
import type { WorldSnapshot } from '../../src/world/types.js';
|
||||
import { commandSpec as procureSpec } from '../../src/actions/turn/general/che_물자조달.js';
|
||||
import {
|
||||
commandSpec as procureSpec,
|
||||
roundLegacyAccumulatedInteger,
|
||||
} from '../../src/actions/turn/general/che_물자조달.js';
|
||||
import { commandSpec as donateSpec } from '../../src/actions/turn/general/che_헌납.js';
|
||||
import { commandSpec as moveSpec } from '../../src/actions/turn/general/che_이동.js';
|
||||
import { commandSpec as wanderSpec } from '../../src/actions/turn/general/che_방랑.js';
|
||||
@@ -24,8 +27,65 @@ import {
|
||||
loadItemModules,
|
||||
} from '../../src/items/index.js';
|
||||
import { createRefOrderedActionStack } from '../../src/actionModules/bundle.js';
|
||||
import type { GeneralActionModule } from '../../src/actionModules/general.js';
|
||||
import {
|
||||
normalizeLegacyGeneratedDex,
|
||||
resolveLegacySpecialityAge,
|
||||
} from '../../src/actions/turn/general/che_인재탐색.js';
|
||||
import {
|
||||
addLegacyStoredTech,
|
||||
readLegacyStoredTech,
|
||||
toLegacyStoredTech,
|
||||
} from '../../src/actions/turn/general/che_기술연구.js';
|
||||
import {
|
||||
readLegacyCityTrust,
|
||||
storeLegacyCityTrust,
|
||||
} from '../../src/actions/turn/general/legacyCityTrust.js';
|
||||
import { roundLegacyRecruitCost } from '../../src/actions/turn/general/che_징병.js';
|
||||
|
||||
describe('General Commands New Scenario', () => {
|
||||
it('truncates generated NPC dex like GeneralBuilder integer arguments', () => {
|
||||
expect(normalizeLegacyGeneratedDex([36.5, 7.9, 7.1, 7.99, 0.75])).toEqual([36, 7, 7, 7, 0]);
|
||||
});
|
||||
|
||||
it('rounds the accumulated procurement experience like a MariaDB INT assignment', () => {
|
||||
const delta = (45 * 0.7) / 3;
|
||||
expect(delta).toBe(10.499999999999998);
|
||||
expect(Math.round(delta)).toBe(10);
|
||||
expect(roundLegacyAccumulatedInteger(4554, delta)).toBe(4565);
|
||||
});
|
||||
|
||||
it('persists generated NPC speciality ages from the legacy creation date', () => {
|
||||
expect(resolveLegacySpecialityAge(80, 22, 12)).toBe(27);
|
||||
expect(resolveLegacySpecialityAge(80, 22, 6)).toBe(32);
|
||||
expect(resolveLegacySpecialityAge(80, 24, 12)).toBe(29);
|
||||
});
|
||||
|
||||
it('stores technology as binary32 without per-update decimal quantization', () => {
|
||||
const value = 433.51797;
|
||||
expect(toLegacyStoredTech(value)).toBe(Math.fround(value));
|
||||
expect(toLegacyStoredTech(value)).not.toBe(Number(Math.fround(value).toPrecision(6)));
|
||||
expect(readLegacyStoredTech(624.0966796875)).toBe(624.097);
|
||||
expect(addLegacyStoredTech(624.0966796875, 22.9)).toBe(Math.fround(624.097 + 22.9));
|
||||
});
|
||||
|
||||
it('separates MariaDB FLOAT trust storage from its six-digit PHP read value', () => {
|
||||
const stored = storeLegacyCityTrust(88.306755);
|
||||
|
||||
expect(stored).toBe(Math.fround(88.306755));
|
||||
expect(stored).not.toBe(readLegacyCityTrust(stored));
|
||||
expect(readLegacyCityTrust(stored)).toBe(88.3068);
|
||||
expect(readLegacyCityTrust(storeLegacyCityTrust(readLegacyCityTrust(stored) + 10))).toBe(98.3068);
|
||||
});
|
||||
|
||||
it('rounds recruitment cost across the PHP half boundary', () => {
|
||||
const cavalryCost = (11 * 1.15 * 7000) / 100;
|
||||
|
||||
expect(cavalryCost).toBe(885.4999999999999);
|
||||
expect(Math.round(cavalryCost)).toBe(885);
|
||||
expect(roundLegacyRecruitCost(cavalryCost)).toBe(886);
|
||||
});
|
||||
|
||||
// 1. Setup Environment
|
||||
const systemEnv: TurnCommandEnv = {
|
||||
develCost: 100,
|
||||
@@ -168,7 +228,26 @@ describe('General Commands New Scenario', () => {
|
||||
const runner = new TestGameRunner(world, 200, 1);
|
||||
|
||||
// 1. Procure
|
||||
const procureDef = procureSpec.createDefinition(systemEnv);
|
||||
const procureDef = procureSpec.createDefinition({
|
||||
...systemEnv,
|
||||
generalActionModules: (() => {
|
||||
const noOp = {};
|
||||
return createRefOrderedActionStack({
|
||||
nation: noOp,
|
||||
officer: noOp,
|
||||
domestic: noOp,
|
||||
war: noOp,
|
||||
personality: {
|
||||
eventHandlers: {},
|
||||
onCalcStat: (_context, statName, value) => (statName === 'experience' ? value * 1.1 : value),
|
||||
} satisfies GeneralActionModule,
|
||||
crewType: null,
|
||||
inheritance: noOp,
|
||||
scenario: null,
|
||||
items: [],
|
||||
});
|
||||
})(),
|
||||
});
|
||||
await runner.runTurn([
|
||||
{
|
||||
generalId: 1,
|
||||
@@ -191,8 +270,12 @@ describe('General Commands New Scenario', () => {
|
||||
]);
|
||||
|
||||
const n1_after_procure = world.getNation(1)!;
|
||||
const g1_after_procure = world.getGeneral(1)!;
|
||||
// Nation gains gold
|
||||
expect(n1_after_procure.gold).toBeGreaterThan(10000);
|
||||
// Ref's addExperience/addDedication route rewards through onCalcStat.
|
||||
expect(g1_after_procure.experience).toBe(183);
|
||||
expect(g1_after_procure.dedication).toBe(208);
|
||||
|
||||
// 2. Donate
|
||||
const donateDef = donateSpec.createDefinition(systemEnv);
|
||||
|
||||
@@ -7,8 +7,18 @@ import { commandSpec as recruitSpec } from '../../src/actions/turn/general/che_
|
||||
import { commandSpec as trainSpec } from '../../src/actions/turn/general/che_훈련.js';
|
||||
import { commandSpec as atmosSpec } from '../../src/actions/turn/general/che_사기진작.js';
|
||||
import type { TurnCommandEnv } from '../../src/actions/turn/commandEnv.js';
|
||||
import { createRefOrderedActionStack } from '../../src/actionModules/bundle.js';
|
||||
import type { GeneralActionModule } from '../../src/actionModules/general.js';
|
||||
import { applyLegacyInjury, finalizeLegacyStat } from '../../src/actions/turn/general/legacyGeneralStat.js';
|
||||
|
||||
describe('Troop Management Scenario', () => {
|
||||
it('applies injury before action stat modifiers and floors like Ref General::getLeadership()', () => {
|
||||
const injured = applyLegacyInjury(62, 3);
|
||||
expect(injured).toBeCloseTo(60.14, 12);
|
||||
expect(finalizeLegacyStat(injured * 2)).toBe(120);
|
||||
expect(Math.round(((120 * 100) / 6076) * 30)).toBe(59);
|
||||
});
|
||||
|
||||
it('should successfully draft troops, then train and boost morale', async () => {
|
||||
// 1. Setup World
|
||||
const mockNation: Nation = {
|
||||
@@ -130,6 +140,29 @@ describe('Troop Management Scenario', () => {
|
||||
baseGold: 1000,
|
||||
baseRice: 1000,
|
||||
maxResourceActionAmount: 1000,
|
||||
...(snapshot.unitSet ? { unitSet: snapshot.unitSet } : {}),
|
||||
generalActionModules: (() => {
|
||||
const noOp = {};
|
||||
return createRefOrderedActionStack({
|
||||
nation: noOp,
|
||||
officer: noOp,
|
||||
domestic: noOp,
|
||||
war: noOp,
|
||||
personality: {
|
||||
eventHandlers: {},
|
||||
onCalcStat: (_context, statName, value) => {
|
||||
if (typeof value !== 'number') return value;
|
||||
if (statName === 'experience') return value * 0.9;
|
||||
if (statName === 'addDex') return value * 0.5;
|
||||
return value;
|
||||
},
|
||||
} satisfies GeneralActionModule,
|
||||
crewType: null,
|
||||
inheritance: noOp,
|
||||
scenario: null,
|
||||
items: [],
|
||||
});
|
||||
})(),
|
||||
};
|
||||
|
||||
// 2. Draft Troops
|
||||
@@ -145,8 +178,20 @@ describe('Troop Management Scenario', () => {
|
||||
|
||||
const generalAfterDraft = world.getGeneral(1)!;
|
||||
expect(generalAfterDraft.crew).toBe(1000);
|
||||
// Ref's General::addExperience() applies personality/item stat modules
|
||||
// after rounding the crew-based base reward. Dedication is routed
|
||||
// through the same pipeline but remains unchanged in this fixture.
|
||||
expect(generalAfterDraft.experience).toBe(109);
|
||||
expect(generalAfterDraft.dedication).toBe(110);
|
||||
// General::addDex(): 보병(armType 1)은 징병 인원 / 100만큼 숙련도가 오른다.
|
||||
expect(generalAfterDraft.meta.dex1).toBe(10);
|
||||
expect(generalAfterDraft.meta.dex1).toBe(5);
|
||||
// Ref che_징병 stores aux.armType and its AI keeps that category on
|
||||
// later recruitment decisions.
|
||||
expect(generalAfterDraft.meta.armType).toBe(1);
|
||||
// 훈련/사기진작의 실제 증가분과 addDex 파이프라인을 관찰할 여유를 만든다.
|
||||
world.snapshot.generals = world.snapshot.generals.map((general) =>
|
||||
general.id === generalAfterDraft.id ? { ...general, train: 80, atmos: 80 } : general
|
||||
);
|
||||
|
||||
// 3. Train
|
||||
const trainDef = trainSpec.createDefinition(systemEnv);
|
||||
@@ -162,6 +207,10 @@ describe('Troop Management Scenario', () => {
|
||||
const generalAfterTrain = world.getGeneral(1)!;
|
||||
// 레거시 훈련식: round(통솔 * 100 * trainDelta / 병력), 상한까지 적용.
|
||||
expect(generalAfterTrain.train).toBe(100);
|
||||
// General::addExperience()/addDedication()/addDex()와 동일하게 모듈 보정을 거친다.
|
||||
expect(generalAfterTrain.experience).toBe(199);
|
||||
expect(generalAfterTrain.dedication).toBe(180);
|
||||
expect(generalAfterTrain.meta.dex1).toBe(15);
|
||||
|
||||
// 4. Boost Morale
|
||||
const atmosDef = atmosSpec.createDefinition(systemEnv);
|
||||
@@ -177,5 +226,8 @@ describe('Troop Management Scenario', () => {
|
||||
const generalAfterAtmos = world.getGeneral(1)!;
|
||||
// 레거시 사기진작식: round(통솔 * 100 / 병력 * atmosDelta), 명령 상한까지 적용.
|
||||
expect(generalAfterAtmos.atmos).toBe(100);
|
||||
expect(generalAfterAtmos.experience).toBe(289);
|
||||
expect(generalAfterAtmos.dedication).toBe(250);
|
||||
expect(generalAfterAtmos.meta.dex1).toBe(25);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user