feat: 대형 테스트 맵의 인구 수치 증가 및 NPC 시뮬레이션에 대한 새로운 성장 검증 로직 추가

This commit is contained in:
2026-01-24 11:52:35 +00:00
parent 2ea8e11f50
commit 45bab33fc2
2 changed files with 215 additions and 20 deletions
+18 -18
View File
@@ -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) {