feat: 대형 테스트 맵의 인구 수치 증가 및 NPC 시뮬레이션에 대한 새로운 성장 검증 로직 추가
This commit is contained in:
+18
-18
@@ -12,7 +12,7 @@ export const LARGE_TEST_MAP: MapDefinition = {
|
||||
position: { x: 10, y: 10 },
|
||||
connections: [2, 3],
|
||||
max: {
|
||||
population: 20000,
|
||||
population: 300000,
|
||||
agriculture: 2000,
|
||||
commerce: 2000,
|
||||
security: 2000,
|
||||
@@ -20,7 +20,7 @@ export const LARGE_TEST_MAP: MapDefinition = {
|
||||
wall: 1000,
|
||||
},
|
||||
initial: {
|
||||
population: 10000,
|
||||
population: 150000,
|
||||
agriculture: 600,
|
||||
commerce: 600,
|
||||
security: 600,
|
||||
@@ -36,7 +36,7 @@ export const LARGE_TEST_MAP: MapDefinition = {
|
||||
position: { x: 20, y: 10 },
|
||||
connections: [1, 4],
|
||||
max: {
|
||||
population: 24000,
|
||||
population: 400000,
|
||||
agriculture: 2200,
|
||||
commerce: 2200,
|
||||
security: 2200,
|
||||
@@ -44,7 +44,7 @@ export const LARGE_TEST_MAP: MapDefinition = {
|
||||
wall: 1100,
|
||||
},
|
||||
initial: {
|
||||
population: 12000,
|
||||
population: 200000,
|
||||
agriculture: 660,
|
||||
commerce: 660,
|
||||
security: 660,
|
||||
@@ -60,7 +60,7 @@ export const LARGE_TEST_MAP: MapDefinition = {
|
||||
position: { x: 10, y: 20 },
|
||||
connections: [1, 5],
|
||||
max: {
|
||||
population: 30000,
|
||||
population: 500000,
|
||||
agriculture: 2500,
|
||||
commerce: 2500,
|
||||
security: 2500,
|
||||
@@ -68,7 +68,7 @@ export const LARGE_TEST_MAP: MapDefinition = {
|
||||
wall: 1200,
|
||||
},
|
||||
initial: {
|
||||
population: 15000,
|
||||
population: 250000,
|
||||
agriculture: 1200,
|
||||
commerce: 1200,
|
||||
security: 1200,
|
||||
@@ -84,7 +84,7 @@ export const LARGE_TEST_MAP: MapDefinition = {
|
||||
position: { x: 30, y: 10 },
|
||||
connections: [2, 5],
|
||||
max: {
|
||||
population: 32000,
|
||||
population: 600000,
|
||||
agriculture: 2600,
|
||||
commerce: 2600,
|
||||
security: 2600,
|
||||
@@ -92,7 +92,7 @@ export const LARGE_TEST_MAP: MapDefinition = {
|
||||
wall: 1300,
|
||||
},
|
||||
initial: {
|
||||
population: 16000,
|
||||
population: 300000,
|
||||
agriculture: 1300,
|
||||
commerce: 1300,
|
||||
security: 1300,
|
||||
@@ -108,7 +108,7 @@ export const LARGE_TEST_MAP: MapDefinition = {
|
||||
position: { x: 20, y: 20 },
|
||||
connections: [3, 4, 6],
|
||||
max: {
|
||||
population: 20000,
|
||||
population: 300000,
|
||||
agriculture: 2000,
|
||||
commerce: 2000,
|
||||
security: 2000,
|
||||
@@ -116,7 +116,7 @@ export const LARGE_TEST_MAP: MapDefinition = {
|
||||
wall: 1000,
|
||||
},
|
||||
initial: {
|
||||
population: 10000,
|
||||
population: 150000,
|
||||
agriculture: 600,
|
||||
commerce: 600,
|
||||
security: 600,
|
||||
@@ -132,7 +132,7 @@ export const LARGE_TEST_MAP: MapDefinition = {
|
||||
position: { x: 30, y: 20 },
|
||||
connections: [5, 7],
|
||||
max: {
|
||||
population: 24000,
|
||||
population: 400000,
|
||||
agriculture: 2200,
|
||||
commerce: 2200,
|
||||
security: 2200,
|
||||
@@ -140,7 +140,7 @@ export const LARGE_TEST_MAP: MapDefinition = {
|
||||
wall: 1100,
|
||||
},
|
||||
initial: {
|
||||
population: 12000,
|
||||
population: 200000,
|
||||
agriculture: 660,
|
||||
commerce: 660,
|
||||
security: 660,
|
||||
@@ -156,7 +156,7 @@ export const LARGE_TEST_MAP: MapDefinition = {
|
||||
position: { x: 40, y: 20 },
|
||||
connections: [6, 8],
|
||||
max: {
|
||||
population: 30000,
|
||||
population: 500000,
|
||||
agriculture: 2500,
|
||||
commerce: 2500,
|
||||
security: 2500,
|
||||
@@ -164,7 +164,7 @@ export const LARGE_TEST_MAP: MapDefinition = {
|
||||
wall: 1200,
|
||||
},
|
||||
initial: {
|
||||
population: 15000,
|
||||
population: 250000,
|
||||
agriculture: 1200,
|
||||
commerce: 1200,
|
||||
security: 1200,
|
||||
@@ -180,7 +180,7 @@ export const LARGE_TEST_MAP: MapDefinition = {
|
||||
position: { x: 50, y: 20 },
|
||||
connections: [7, 9],
|
||||
max: {
|
||||
population: 32000,
|
||||
population: 600000,
|
||||
agriculture: 2600,
|
||||
commerce: 2600,
|
||||
security: 2600,
|
||||
@@ -188,7 +188,7 @@ export const LARGE_TEST_MAP: MapDefinition = {
|
||||
wall: 1300,
|
||||
},
|
||||
initial: {
|
||||
population: 16000,
|
||||
population: 300000,
|
||||
agriculture: 1300,
|
||||
commerce: 1300,
|
||||
security: 1300,
|
||||
@@ -204,7 +204,7 @@ export const LARGE_TEST_MAP: MapDefinition = {
|
||||
position: { x: 60, y: 20 },
|
||||
connections: [8],
|
||||
max: {
|
||||
population: 20000,
|
||||
population: 300000,
|
||||
agriculture: 2000,
|
||||
commerce: 2000,
|
||||
security: 2000,
|
||||
@@ -212,7 +212,7 @@ export const LARGE_TEST_MAP: MapDefinition = {
|
||||
wall: 1000,
|
||||
},
|
||||
initial: {
|
||||
population: 10000,
|
||||
population: 150000,
|
||||
agriculture: 1000,
|
||||
commerce: 1000,
|
||||
security: 1000,
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { LogEntryDraft, TurnSchedule, UnitSetDefinition } from '@sammo-ts/logic';
|
||||
import { LogCategory } from '@sammo-ts/logic';
|
||||
import type {
|
||||
ConstraintContext,
|
||||
LogEntryDraft,
|
||||
RequirementKey,
|
||||
StateView,
|
||||
TurnSchedule,
|
||||
UnitSetDefinition,
|
||||
} from '@sammo-ts/logic';
|
||||
import { DEFAULT_TURN_COMMAND_PROFILE, LogCategory, evaluateConstraints } from '@sammo-ts/logic';
|
||||
import type { TurnGeneral, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
|
||||
import type { GeneralAiDebugState } from '../src/turn/ai/generalAi.js';
|
||||
import { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js';
|
||||
@@ -8,10 +15,13 @@ import { joinYearMonth } from '../src/turn/ai/aiUtils.js';
|
||||
import { InMemoryReservedTurnStore } from '../src/turn/reservedTurnStore.js';
|
||||
import { createReservedTurnHandler } from '../src/turn/reservedTurnHandler.js';
|
||||
import { InMemoryTurnProcessor } from '../src/turn/inMemoryTurnProcessor.js';
|
||||
import { GeneralAI } from '../src/turn/ai/generalAi.js';
|
||||
import { do징병 } from '../src/turn/ai/generalAiGeneralActions.js';
|
||||
import { createIncomeHandler } from '../src/turn/incomeHandler.js';
|
||||
import { createNpcTaxHandler } from '../src/turn/npcTaxHandler.js';
|
||||
import { createFrontStateHandler } from '../src/turn/frontStateHandler.js';
|
||||
import { composeCalendarHandlers } from '../src/turn/calendarHandlers.js';
|
||||
import { buildCommandEnv, buildReservedTurnDefinitions } from '../src/turn/reservedTurnCommands.js';
|
||||
import { LARGE_TEST_MAP, buildLargeTestCities } from './fixtures/largeTestMap.js';
|
||||
|
||||
const mockDate = new Date('0179-08-01T00:00:00Z');
|
||||
@@ -120,6 +130,19 @@ describe('NPC 대형 시뮬레이션', () => {
|
||||
}
|
||||
|
||||
const cities = buildLargeTestCities();
|
||||
const initialCityStats = new Map(
|
||||
cities.map((city) => [
|
||||
city.id,
|
||||
{
|
||||
population: city.population,
|
||||
agriculture: city.agriculture,
|
||||
commerce: city.commerce,
|
||||
security: city.security,
|
||||
defence: city.defence,
|
||||
wall: city.wall,
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
const unitSet: UnitSetDefinition = {
|
||||
id: 'test_unit_set',
|
||||
@@ -399,6 +422,63 @@ describe('NPC 대형 시뮬레이션', () => {
|
||||
expect(afterTotal).toBeGreaterThan(beforeTotal);
|
||||
};
|
||||
|
||||
const assertDomesticGrowthBy = (year: number, month: number) => {
|
||||
const citiesNow = world.listCities().filter((city) => city.nationId > 0);
|
||||
for (const city of citiesNow) {
|
||||
const baseline = initialCityStats.get(city.id);
|
||||
if (!baseline) {
|
||||
continue;
|
||||
}
|
||||
const delta = {
|
||||
population: city.population - baseline.population,
|
||||
agriculture: city.agriculture - baseline.agriculture,
|
||||
commerce: city.commerce - baseline.commerce,
|
||||
security: city.security - baseline.security,
|
||||
defence: city.defence - baseline.defence,
|
||||
wall: city.wall - baseline.wall,
|
||||
};
|
||||
console.log('[DEBUG] domestic delta', {
|
||||
year,
|
||||
month,
|
||||
cityId: city.id,
|
||||
nationId: city.nationId,
|
||||
delta,
|
||||
});
|
||||
|
||||
const failures: string[] = [];
|
||||
if (city.population <= baseline.population) failures.push('population');
|
||||
if (city.agriculture <= baseline.agriculture) failures.push('agriculture');
|
||||
if (city.commerce <= baseline.commerce) failures.push('commerce');
|
||||
if (city.security <= baseline.security) failures.push('security');
|
||||
if (city.defence <= baseline.defence) failures.push('defence');
|
||||
if (city.wall <= baseline.wall) failures.push('wall');
|
||||
if (failures.length > 0) {
|
||||
console.log('[DEBUG] domestic not grown', {
|
||||
year,
|
||||
month,
|
||||
cityId: city.id,
|
||||
nationId: city.nationId,
|
||||
failures,
|
||||
baseline,
|
||||
current: {
|
||||
population: city.population,
|
||||
agriculture: city.agriculture,
|
||||
commerce: city.commerce,
|
||||
security: city.security,
|
||||
defence: city.defence,
|
||||
wall: city.wall,
|
||||
},
|
||||
});
|
||||
}
|
||||
expect(city.population).toBeGreaterThan(baseline.population);
|
||||
expect(city.agriculture).toBeGreaterThan(baseline.agriculture);
|
||||
expect(city.commerce).toBeGreaterThan(baseline.commerce);
|
||||
expect(city.security).toBeGreaterThan(baseline.security);
|
||||
expect(city.defence).toBeGreaterThan(baseline.defence);
|
||||
expect(city.wall).toBeGreaterThan(baseline.wall);
|
||||
}
|
||||
};
|
||||
|
||||
const targetChecks = new Map<string, () => void>([
|
||||
['179-09', () => assertUprisingCount(1)],
|
||||
['179-10', () => assertUprisingCount(2)],
|
||||
@@ -411,11 +491,122 @@ describe('NPC 대형 시뮬레이션', () => {
|
||||
['180-07', () => assertSmallMediumCitiesFounded()],
|
||||
['180-11', () => assertCityTrust(90)],
|
||||
['181-01', () => assertNationGeneralCount(10)],
|
||||
['182-01', () => assertDomesticGrowthBy(182, 1)],
|
||||
['182-10', () => assertNationRecruitCount(5)],
|
||||
]);
|
||||
|
||||
const toKey = (year: number, month: number) => `${year}-${String(month).padStart(2, '0')}`;
|
||||
|
||||
const debugRecruitConstraints = async (generalId: number) => {
|
||||
const general = world.getGeneralById(generalId);
|
||||
if (!general) {
|
||||
console.log('[DEBUG] no general for recruit check', generalId);
|
||||
return;
|
||||
}
|
||||
const city = world.getCityById(general.cityId);
|
||||
const nation = general.nationId > 0 ? world.getNationById(general.nationId) : null;
|
||||
const env = buildCommandEnv(snapshot.scenarioConfig, snapshot.unitSet);
|
||||
const { general: generalDefinitions, nation: nationDefinitions } = await buildReservedTurnDefinitions({
|
||||
env,
|
||||
commandProfile: DEFAULT_TURN_COMMAND_PROFILE,
|
||||
defaultActionKey: '휴식',
|
||||
});
|
||||
const definition = generalDefinitions.get('che_징병');
|
||||
if (!definition) {
|
||||
console.log('[DEBUG] no che_징병 definition');
|
||||
return;
|
||||
}
|
||||
const args = {
|
||||
crewType: snapshot.unitSet?.defaultCrewTypeId ?? 1100,
|
||||
amount: general.stats.leadership * 100,
|
||||
};
|
||||
const parsedArgs = definition.parseArgs(args);
|
||||
if (!parsedArgs) {
|
||||
console.log('[DEBUG] che_징병 args invalid', args);
|
||||
return;
|
||||
}
|
||||
|
||||
const state = world.getState();
|
||||
const startYear = snapshot.scenarioMeta?.startYear ?? 0;
|
||||
const constraintEnv = {
|
||||
currentYear: state.currentYear,
|
||||
currentMonth: state.currentMonth,
|
||||
year: state.currentYear,
|
||||
month: state.currentMonth,
|
||||
startYear,
|
||||
relYear: state.currentYear - startYear,
|
||||
openingPartYear: env.openingPartYear,
|
||||
minAvailableRecruitPop: env.minAvailableRecruitPop,
|
||||
cities: world.listCities(),
|
||||
nations: world.listNations(),
|
||||
map: LARGE_TEST_MAP,
|
||||
unitSet: snapshot.unitSet,
|
||||
};
|
||||
|
||||
const ctx: ConstraintContext = {
|
||||
actorId: general.id,
|
||||
cityId: general.cityId,
|
||||
nationId: general.nationId,
|
||||
args: parsedArgs as Record<string, unknown>,
|
||||
env: constraintEnv,
|
||||
mode: 'full',
|
||||
};
|
||||
|
||||
const view: StateView = {
|
||||
has: (req: RequirementKey) => {
|
||||
if (req.kind === 'general') return world.getGeneralById(req.id) !== null;
|
||||
if (req.kind === 'city') return world.getCityById(req.id) !== null;
|
||||
if (req.kind === 'nation') return world.getNationById(req.id) !== null;
|
||||
if (req.kind === 'generalList') return true;
|
||||
if (req.kind === 'nationList') return true;
|
||||
if (req.kind === 'env') return true;
|
||||
return false;
|
||||
},
|
||||
get: (req: RequirementKey) => {
|
||||
if (req.kind === 'general') return world.getGeneralById(req.id);
|
||||
if (req.kind === 'city') return world.getCityById(req.id);
|
||||
if (req.kind === 'nation') return world.getNationById(req.id);
|
||||
if (req.kind === 'generalList') return world.listGenerals();
|
||||
if (req.kind === 'nationList') return world.listNations();
|
||||
if (req.kind === 'env') return constraintEnv[req.key] ?? null;
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
const constraints = definition.buildConstraints(ctx, parsedArgs as never);
|
||||
const result = evaluateConstraints(constraints, ctx, view);
|
||||
console.log('[DEBUG] che_징병 constraint result:', result);
|
||||
for (const constraint of constraints) {
|
||||
const single = evaluateConstraints([constraint], ctx, view);
|
||||
if (single.kind !== 'allow') {
|
||||
console.log('[DEBUG] constraint blocked:', constraint.name, single);
|
||||
}
|
||||
}
|
||||
|
||||
const generalFallback = generalDefinitions.get('휴식')!;
|
||||
const nationFallback = nationDefinitions.get('휴식')!;
|
||||
const ai = new GeneralAI({
|
||||
general,
|
||||
city: city ?? undefined,
|
||||
nation,
|
||||
world: state,
|
||||
worldRef: world,
|
||||
reservedTurnProvider: reservedTurnStore,
|
||||
scenarioConfig: snapshot.scenarioConfig,
|
||||
scenarioMeta: snapshot.scenarioMeta,
|
||||
map: LARGE_TEST_MAP,
|
||||
unitSet: snapshot.unitSet,
|
||||
commandEnv: env,
|
||||
generalDefinitions,
|
||||
nationDefinitions,
|
||||
generalFallback,
|
||||
nationFallback,
|
||||
});
|
||||
console.log('[DEBUG] can 징병:', ai.generalPolicy.can('징병'));
|
||||
console.log('[DEBUG] priority includes 징병:', ai.generalPolicy.priority.includes('징병'));
|
||||
console.log('[DEBUG] do징병 candidate:', do징병(ai));
|
||||
};
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
await runOneMonth();
|
||||
@@ -434,6 +625,10 @@ describe('NPC 대형 시뮬레이션', () => {
|
||||
if (lastAiTrace?.aiState) {
|
||||
console.log('[DEBUG] last aiState:', lastAiTrace.aiState);
|
||||
}
|
||||
const debugGeneralId = lastAiTrace?.generalId ?? world.listGenerals()[0]?.id;
|
||||
if (debugGeneralId) {
|
||||
await debugRecruitConstraints(debugGeneralId);
|
||||
}
|
||||
dumpTraceSummary('NPC 대형 시뮬레이션 실패');
|
||||
const sampleNation = world.listNations().find((nation) => nation.level >= 1 && nation.capitalCityId);
|
||||
if (sampleNation) {
|
||||
|
||||
Reference in New Issue
Block a user