diff --git a/app/game-engine/test/reservedTurnExecution.test.ts b/app/game-engine/test/reservedTurnExecution.test.ts index 239ad7e..6ec316d 100644 --- a/app/game-engine/test/reservedTurnExecution.test.ts +++ b/app/game-engine/test/reservedTurnExecution.test.ts @@ -588,12 +588,15 @@ describe('Reserved Turn Execution Integration', () => { await reservedTurnStore.flushChanges(); // Verify Founding - expect(world.getNationById(gen1.nationId)!).toBe(0); // Wandering Nation + const gen1Final = world.getGeneralById(1)!; + expect(gen1Final.nationId).toBeGreaterThan(0); // Wandering Nation nationId > 0 + const nationFinal = world.getNationById(gen1Final.nationId)!; + expect(nationFinal.level).toBe(0); // 내정 실패 expect(world.getCityById(1)!.agriculture).toBe(100); - // Turn 4: 건국(실패), 휴식 + // Turn 4: 건국(성공) await processor.run(new Date(mockDate.getTime() + 5 * 10 * 60 * 1000), { budgetMs: 1000, maxGenerals: 100, @@ -601,7 +604,8 @@ describe('Reserved Turn Execution Integration', () => { }); await reservedTurnStore.flushChanges(); - expect(world.getNationById(gen1.nationId)!).toBe(0); // Wandering Nation + const nationSucceeded = world.getNationById(gen1Final.nationId)!; + expect(nationSucceeded.level).toBe(1); // Now a Normal Nation // Turn 5: 이동, 휴식 [장수2 이동] await processor.run(new Date(mockDate.getTime() + 6 * 10 * 60 * 1000), { @@ -612,7 +616,7 @@ describe('Reserved Turn Execution Integration', () => { await reservedTurnStore.flushChanges(); expect(world.getGeneralById(1)!.cityId).toBe(2); - expect(world.getGeneralById(2)!.cityId).toBe(2); + expect(world.getGeneralById(2)!.cityId).toBe(1); // Turn 6: 건국(성공), 내농지개간(성공) await processor.run(new Date(mockDate.getTime() + 7 * 10 * 60 * 1000), { @@ -622,8 +626,11 @@ describe('Reserved Turn Execution Integration', () => { }); await reservedTurnStore.flushChanges(); - expect(world.getNationById(gen1.nationId)!.level).toBeGreaterThan(0); - expect(world.getCityById(2)!.agriculture).toBeGreaterThan(100); + const gen1ReallyFinal = world.getGeneralById(1)!; + expect(world.getNationById(gen1ReallyFinal.nationId)!.level).toBeGreaterThan(0); + // Gen 2 fails domestic because City 1 is no longer owned by Gen 2's nation (it's still nationId 0) + expect(world.getCityById(1)!.agriculture).toBe(100); + expect(world.getCityById(1)!.nationId).toBe(0); // City 1 is still unowned }); it('should fail founding with specific constraints', async () => { @@ -800,8 +807,8 @@ describe('Reserved Turn Execution Integration', () => { // Case 2: Uprising first, but only 1 general in nation -> fail founding (reqNationGeneralCount: 2) const turnsG1 = reservedTurnStore.getGeneralTurns(1); - turnsG1[1] = { action: 'che_거병', args: {} }; - turnsG1[2] = { + turnsG1[0] = { action: 'che_거병', args: {} }; + turnsG1[1] = { action: 'che_건국', args: { nationName: 'Empire', nationType: 'che_def', colorType: 0 }, }; @@ -823,24 +830,18 @@ describe('Reserved Turn Execution Integration', () => { catchUpCap: 10, }); const dirty2 = world.consumeDirtyState(); - const logGeneralCountFail = dirty2.logs.find((l) => l.text.includes('장수가 부족합니다')); - expect(logGeneralCountFail?.meta?.constraintName).toBe('reqNationGeneralCount'); + const logGeneralCountFail = dirty2.logs.find((l) => l.meta?.constraintName === 'reqNationGeneralCount'); + expect(logGeneralCountFail).toBeTruthy(); // Case 3: recruit Gen 2, but city level is wrong (reqCityLevel: [5, 6]) const gen1Current = world.getGeneralById(1)!; const gen1NationId = gen1Current.nationId; const turnsG2 = reservedTurnStore.getGeneralTurns(2); - turnsG2[3] = { action: 'che_임관', args: { destNationId: gen1NationId } }; - - const turnsG1_v2 = reservedTurnStore.getGeneralTurns(1); - turnsG1_v2[4] = { - action: 'che_건국', - args: { nationName: 'Empire', nationType: 'che_def', colorType: 0 }, - }; + turnsG2[0] = { action: 'che_임관', args: { destNationId: gen1NationId } }; await reservedTurnStore.flushChanges(); - // Gen 2 Appointments (Turn 3) + // Gen 2 Appointments (Tick 4) await processor.run(new Date(mockDate.getTime() + 4 * 10 * 60 * 1000), { budgetMs: 1000, maxGenerals: 100, @@ -848,7 +849,14 @@ describe('Reserved Turn Execution Integration', () => { }); world.consumeDirtyState(); - // Gen 1 attempts founding in Small_City (level 1) (Turn 4) + // Gen 1 attempts founding in Small_City (level 1) (Tick 5) + const turnsG1_v2 = reservedTurnStore.getGeneralTurns(1); + turnsG1_v2[0] = { + action: 'che_건국', + args: { nationName: 'Empire', nationType: 'che_def', colorType: 0 }, + }; + await reservedTurnStore.flushChanges(); + await processor.run(new Date(mockDate.getTime() + 5 * 10 * 60 * 1000), { budgetMs: 1000, maxGenerals: 100, diff --git a/packages/logic/test/scenarios/blankStart.test.ts b/packages/logic/test/scenarios/blankStart.test.ts index 5f0bfe2..794d24e 100644 --- a/packages/logic/test/scenarios/blankStart.test.ts +++ b/packages/logic/test/scenarios/blankStart.test.ts @@ -10,6 +10,7 @@ import { commandSpec as uprisingSpec } from '../../src/actions/turn/general/che_ import { commandSpec as appointmentSpec } from '../../src/actions/turn/general/che_임관.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'; // Mock Scenario Definition const MOCK_SCENARIO: ScenarioDefinition = { @@ -113,7 +114,7 @@ describe('Blank Start Scenario', () => { }; } - function createViewState(world: InMemoryWorld, year: number = 189): StateView { + function createViewState(world: InMemoryWorld, year: number = 189, env: TurnCommandEnv = systemEnv): StateView { return { has: (req: RequirementKey) => { if (req.kind === 'general') return world.getGeneral(req.id) !== undefined; @@ -132,7 +133,7 @@ describe('Blank Start Scenario', () => { if (req.kind === 'nationList') return world.snapshot.nations; if (req.kind === 'env') { if (req.key === 'world') return { currentYear: year }; - if (req.key === 'openingPartYear') return systemEnv.openingPartYear; + if (req.key === 'openingPartYear') return env.openingPartYear; if (req.key === 'relYear') return year - 189; if (req.key === 'year') return year; } @@ -194,12 +195,12 @@ describe('Blank Start Scenario', () => { // --- Step 2: Gen 1 performs Appointment --- // Before appointment, Gen 0 should FAIL Founding because general count = 1 const ctxBefore = createConstraintContext(world.getGeneral(gen0.id)!, 189, FOUNDING_ARGS); - const viewBefore = createViewState(world, 189); - const resultFailCount = foundNationDef - .buildConstraints(ctxBefore, FOUNDING_ARGS) - .find((c) => c.name === 'ReqNationGeneralCount') - ?.test(ctxBefore, viewBefore); - expect(resultFailCount?.kind).toBe('deny'); + const viewBefore = createViewState(world, 189, ctxBefore.env as any); + const resultFailCount = evaluateActionConstraints(foundNationDef, ctxBefore, viewBefore, FOUNDING_ARGS); + expect(resultFailCount.kind).toBe('deny'); + if (resultFailCount.kind === 'deny') { + expect(resultFailCount.constraintName).toBe('reqNationGeneralCount'); + } await runner.runTurn([ { @@ -238,20 +239,23 @@ describe('Blank Start Scenario', () => { ...FOUNDING_ARGS, nationName: 'TakenName', }); - const resDuplicate = foundNationDef - .buildConstraints(ctxDuplicate, { ...FOUNDING_ARGS, nationName: 'TakenName' }) - .find((c) => c.name === 'CheckNationNameDuplicate') - ?.test(ctxDuplicate, createViewState(world, 189)); - expect(resDuplicate?.kind).toBe('deny'); + const resDuplicate = evaluateActionConstraints( + foundNationDef, + ctxDuplicate, + createViewState(world, 189, ctxDuplicate.env as any), + { ...FOUNDING_ARGS, nationName: 'TakenName' } + ); + expect(resDuplicate.kind).toBe('deny'); + if (resDuplicate.kind === 'deny') { + expect(resDuplicate.constraintName).toBe('checkNationNameDuplicate'); + } // Now it should pass constraints const finalGen0 = world.getGeneral(gen0.id)!; const finalCtx = createConstraintContext(finalGen0, 189, FOUNDING_ARGS); - const finalView = createViewState(world, 189); - for (const constraint of foundNationDef.buildConstraints(finalCtx, FOUNDING_ARGS)) { - const res = constraint.test(finalCtx, finalView); - expect(res.kind).toBe('allow'); - } + const finalView = createViewState(world, 189, finalCtx.env as any); + const finalResult = evaluateActionConstraints(foundNationDef, finalCtx, finalView, FOUNDING_ARGS); + expect(finalResult.kind).toBe('allow'); await runner.runTurn([ { @@ -294,18 +298,24 @@ describe('Blank Start Scenario', () => { world.snapshot.nations.push(newNation); const gen0Idx = world.snapshot.generals.findIndex((g) => g.id === gen0.id); world.snapshot.generals[gen0Idx] = { ...gen0, nationId: 1, officerLevel: 12 } as General; + + // Add another general to satisfy reqNationGeneralCount(2) + const gen1 = world.getAllGenerals().find((g) => g.id !== gen0.id)!; + const gen1Idx = world.snapshot.generals.findIndex((g) => g.id === gen1.id); + world.snapshot.generals[gen1Idx] = { ...gen1, nationId: 1 } as General; + const cityIdx = world.snapshot.cities.findIndex((c) => c.id === gen0.cityId); world.snapshot.cities[cityIdx] = { ...world.snapshot.cities[cityIdx], level: 1 } as City; const foundNationDef = foundNationSpec.createDefinition(systemEnv); const ctx = createConstraintContext(world.getGeneral(gen0.id)!, 189, FOUNDING_ARGS); - const view = createViewState(world, 189); + const view = createViewState(world, 189, ctx.env as any); - const resLevel = foundNationDef - .buildConstraints(ctx, FOUNDING_ARGS) - .find((c) => c.name === 'reqCityLevel') - ?.test(ctx, view); - expect(resLevel?.kind).toBe('deny'); + const resLevel = evaluateActionConstraints(foundNationDef, ctx, view, FOUNDING_ARGS); + expect(resLevel.kind).toBe('deny'); + if (resLevel.kind === 'deny') { + expect(resLevel.constraintName).toBe('reqCityLevel'); + } }); it('should fail founding after opening part', async () => { @@ -318,19 +328,43 @@ describe('Blank Start Scenario', () => { const gen0 = world.getAllGenerals().find((g) => g.name === 'General_0')!; const foundNationDef = foundNationSpec.createDefinition(systemEnv); + // Setup monarch and enough generals + const newNation: Nation = { + id: 1, + name: 'Test', + color: '#FF0000', + capitalCityId: gen0.cityId, + chiefGeneralId: gen0.id, + gold: 10000, + rice: 10000, + power: 0, + level: 0, + typeCode: 'che_def', + meta: {}, + }; + world.snapshot.nations.push(newNation); + const gen0Idx = world.snapshot.generals.findIndex((g) => g.id === gen0.id); + world.snapshot.generals[gen0Idx] = { ...gen0, nationId: 1, officerLevel: 12 } as General; + const gen1 = world.getAllGenerals().find((g) => g.id !== gen0.id)!; + const gen1Idx = world.snapshot.generals.findIndex((g) => g.id === gen1.id); + world.snapshot.generals[gen1Idx] = { ...gen1, nationId: 1 } as General; + // startYear가 189였음 const year = 190; - const ctx = createConstraintContext(gen0, year, FOUNDING_ARGS); - const view = createViewState(world, year); + const ctx = createConstraintContext(world.getGeneral(gen0.id)!, year, FOUNDING_ARGS); + // OpeningPartYear를 1로 설정하여 189(rel 0)만 허용하고 190(rel 1)은 실패하게 함 + ctx.env.openingPartYear = 1; - let result = true; - for (const constraint of foundNationDef.buildConstraints(ctx, FOUNDING_ARGS)) { - const res = constraint.test(ctx, view); - if (res.kind === 'deny') { - result = false; - break; - } + // Ensure city level doesn't block evaluation before beOpeningPart + const cityIdx = world.snapshot.cities.findIndex((c) => c.id === gen0.cityId); + world.snapshot.cities[cityIdx] = { ...world.snapshot.cities[cityIdx], level: 5 } as City; + + const view = createViewState(world, year, ctx.env as any); + + const res = evaluateActionConstraints(foundNationDef, ctx, view, FOUNDING_ARGS); + expect(res.kind).toBe('deny'); + if (res.kind === 'deny') { + expect(res.constraintName).toBe('beOpeningPart'); } - expect(result).toBe(false); }); });