From 38a8d33242d455efd33bc27823a3cb7b62a49a08 Mon Sep 17 00:00:00 2001 From: Hide_D Date: Wed, 7 Jan 2026 17:17:04 +0000 Subject: [PATCH] refactor: update test setup, introduce system environment for commands, and streamline frontend build configuration. --- app/game-api/package.json | 8 + app/game-api/src/battleSim/processor.ts | 10 +- app/game-api/src/context.ts | 2 + app/game-api/src/index.ts | 6 + app/game-api/src/turns/commandTable.ts | 2 +- app/game-api/tsconfig.json | 43 +++- app/game-engine/tsconfig.json | 21 +- app/gateway-api/package.json | 8 + app/gateway-api/tsconfig.json | 25 +- app/gateway-frontend/package.json | 2 + app/gateway-frontend/src/utils/gameTrpc.ts | 2 +- app/gateway-frontend/tsconfig.json | 83 ++++--- app/gateway-frontend/vite.config.d.ts | 2 - app/gateway-frontend/vite.config.js | 16 -- .../logic/test/scenarios/blankStart.test.ts | 120 ++++++---- .../logic/test/scenarios/diplomacy.test.ts | 102 ++++++--- .../logic/test/scenarios/domestic.test.ts | 196 ++++++++-------- packages/logic/test/scenarios/troops.test.ts | 213 ++++++------------ packages/logic/test/testEnv.ts | 60 ++--- packages/logic/tsconfig.json | 19 +- pnpm-lock.yaml | 6 + tsdown.config.ts | 4 +- 22 files changed, 518 insertions(+), 432 deletions(-) delete mode 100644 app/gateway-frontend/vite.config.d.ts delete mode 100644 app/gateway-frontend/vite.config.js diff --git a/app/game-api/package.json b/app/game-api/package.json index 2c07748..56d92c5 100644 --- a/app/game-api/package.json +++ b/app/game-api/package.json @@ -3,6 +3,14 @@ "private": true, "version": "0.0.0", "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, "scripts": { "build": "tsdown -c ../../tsdown.config.ts -F @sammo-ts/game-api", "dev": "tsdown -c ../../tsdown.config.ts -F @sammo-ts/game-api --watch", diff --git a/app/game-api/src/battleSim/processor.ts b/app/game-api/src/battleSim/processor.ts index 1354e14..c6d98a9 100644 --- a/app/game-api/src/battleSim/processor.ts +++ b/app/game-api/src/battleSim/processor.ts @@ -217,7 +217,7 @@ const resolveCityRiceConsumption = (options: { year: number; startYear: number; }): number => { - const cityReport = options.battle.reports.find((report) => report.type === 'city'); + const cityReport = options.battle.reports.find((report: any) => report.type === 'city'); if (!cityReport) { return 0; } @@ -225,7 +225,7 @@ const resolveCityRiceConsumption = (options: { return 0; } - const crewType = options.unitSet.crewTypes?.find((item) => item.id === options.castleCrewTypeId); + const crewType = options.unitSet.crewTypes?.find((item: any) => item.id === options.castleCrewTypeId); const riceCoef = crewType?.rice ?? 1; const tech = Number(options.defenderNation.meta.tech ?? 0); const trainAtmos = resolveCityTrainAtmos(options.year, options.startYear); @@ -333,7 +333,7 @@ export const processBattleSimJob = (payload: BattleSimJobPayload): BattleSimResu }); lastBattle = outcome; - const attackerReport = outcome.reports.find((report) => report.type === 'general' && report.isAttacker); + const attackerReport = outcome.reports.find((report: any) => report.type === 'general' && report.isAttacker); const killed = attackerReport?.killed ?? 0; const dead = attackerReport?.dead ?? 0; @@ -370,7 +370,7 @@ export const processBattleSimJob = (payload: BattleSimJobPayload): BattleSimResu defenderAvgRice += (defenderRiceInit - defenderRiceAfter + cityRice) * weight; const attackerActivated = outcome.metrics?.attackerActivatedSkills ?? {}; - for (const [skillName, value] of Object.entries(attackerActivated)) { + for (const [skillName, value] of Object.entries(attackerActivated) as [string, number][]) { attackerSkills[skillName] = (attackerSkills[skillName] ?? 0) + value * weight; } @@ -380,7 +380,7 @@ export const processBattleSimJob = (payload: BattleSimJobPayload): BattleSimResu defendersSkills.push({}); } const bucket = defendersSkills[defIdx]!; - for (const [skillName, value] of Object.entries(defenderActivated[defIdx]!)) { + for (const [skillName, value] of Object.entries(defenderActivated[defIdx]!) as [string, number][]) { bucket[skillName] = (bucket[skillName] ?? 0) + value * weight; } } diff --git a/app/game-api/src/context.ts b/app/game-api/src/context.ts index dcf6929..ad7bf77 100644 --- a/app/game-api/src/context.ts +++ b/app/game-api/src/context.ts @@ -35,6 +35,8 @@ export type NationRow = GamePrisma.NationGetPayload>; export type TroopRow = GamePrisma.TroopGetPayload>; export type JsonValue = GamePrisma.JsonValue; +export type JsonObject = GamePrisma.JsonObject; +export type JsonArray = GamePrisma.JsonArray; export type InputJsonValue = GamePrisma.InputJsonValue; export type DatabaseClient = InfraDatabaseClient; diff --git a/app/game-api/src/index.ts b/app/game-api/src/index.ts index 3e744fc..bec4876 100644 --- a/app/game-api/src/index.ts +++ b/app/game-api/src/index.ts @@ -22,6 +22,12 @@ export * from './battleSim/inMemoryTransport.js'; export * from './battleSim/keys.js'; export * from './battleSim/worker.js'; +// Types for TRPC consumer +export type { MessageView } from './messages/store.js'; +export type { TurnCommandTable } from './turns/commandTable.js'; +export type { ReservedTurnView } from './turns/reservedTurns.js'; +export type { JsonObject, JsonArray } from './context.js'; + const isMain = (): boolean => { if (!process.argv[1]) { return false; diff --git a/app/game-api/src/turns/commandTable.ts b/app/game-api/src/turns/commandTable.ts index 50a29b2..4ee230d 100644 --- a/app/game-api/src/turns/commandTable.ts +++ b/app/game-api/src/turns/commandTable.ts @@ -328,7 +328,7 @@ const evaluateAvailability = ( reason: result.reason, }; } - const missingKinds = new Set(result.missing.map((req) => req.kind)); + const missingKinds = new Set(result.missing.map((req: any) => req.kind)); const inputOnlyMissing = missingKinds.size === 0 ? reqArg : Array.from(missingKinds).every((kind) => INPUT_REQUIREMENT_KINDS.has(kind)); if (inputOnlyMissing) { diff --git a/app/game-api/tsconfig.json b/app/game-api/tsconfig.json index 783dd31..cdab0d8 100644 --- a/app/game-api/tsconfig.json +++ b/app/game-api/tsconfig.json @@ -3,20 +3,41 @@ "compilerOptions": { "outDir": "dist", "composite": true, + "rootDir": "src", "baseUrl": ".", "paths": { - "@sammo-ts/common": ["../../packages/common/src/index.ts"], - "@sammo-ts/common/*": ["../../packages/common/src/*"], - "@sammo-ts/infra": ["../../packages/infra/src/index.ts"], - "@sammo-ts/infra/*": ["../../packages/infra/src/*"], - "@sammo-ts/logic": ["../../packages/logic/src/index.ts"], - "@sammo-ts/logic/*": ["../../packages/logic/src/*"] + "@sammo-ts/common": [ + "../../packages/common/src/index.ts" + ], + "@sammo-ts/common/*": [ + "../../packages/common/src/*" + ], + "@sammo-ts/infra": [ + "../../packages/infra/src/index.ts" + ], + "@sammo-ts/infra/*": [ + "../../packages/infra/src/*" + ], + "@sammo-ts/logic": [ + "../../packages/logic/src/index.ts" + ], + "@sammo-ts/logic/*": [ + "../../packages/logic/src/*" + ] } }, - "include": ["src", "test", "*.ts"], + "include": [ + "src" + ], "references": [ - { "path": "../../packages/common" }, - { "path": "../../packages/infra" }, - { "path": "../../packages/logic" } + { + "path": "../../packages/common" + }, + { + "path": "../../packages/infra" + }, + { + "path": "../../packages/logic" + } ] -} +} \ No newline at end of file diff --git a/app/game-engine/tsconfig.json b/app/game-engine/tsconfig.json index 86aaf28..9e979cd 100644 --- a/app/game-engine/tsconfig.json +++ b/app/game-engine/tsconfig.json @@ -2,12 +2,21 @@ "extends": "../../tsconfig.paths.json", "compilerOptions": { "outDir": "dist", - "composite": true + "composite": true, + "rootDir": "src" }, - "include": ["src", "test", "*.ts"], + "include": [ + "src" + ], "references": [ - { "path": "../../packages/common" }, - { "path": "../../packages/infra" }, - { "path": "../../packages/logic" } + { + "path": "../../packages/common" + }, + { + "path": "../../packages/infra" + }, + { + "path": "../../packages/logic" + } ] -} +} \ No newline at end of file diff --git a/app/gateway-api/package.json b/app/gateway-api/package.json index 0f1f919..30cbf0e 100644 --- a/app/gateway-api/package.json +++ b/app/gateway-api/package.json @@ -3,6 +3,14 @@ "private": true, "version": "0.0.0", "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, "scripts": { "build": "tsdown -c ../../tsdown.config.ts -F @sammo-ts/gateway-api", "dev": "tsdown -c ../../tsdown.config.ts -F @sammo-ts/gateway-api --watch", diff --git a/app/gateway-api/tsconfig.json b/app/gateway-api/tsconfig.json index 17a1b4c..46edbbd 100644 --- a/app/gateway-api/tsconfig.json +++ b/app/gateway-api/tsconfig.json @@ -2,13 +2,24 @@ "extends": "../../tsconfig.paths.json", "compilerOptions": { "outDir": "dist", - "composite": true + "composite": true, + "rootDir": "src" }, - "include": ["src", "test", "*.ts"], + "include": [ + "src" + ], "references": [ - { "path": "../../packages/common" }, - { "path": "../../packages/infra" }, - { "path": "../../packages/logic" }, - { "path": "../../app/game-engine" } + { + "path": "../../packages/common" + }, + { + "path": "../../packages/infra" + }, + { + "path": "../../packages/logic" + }, + { + "path": "../../app/game-engine" + } ] -} +} \ No newline at end of file diff --git a/app/gateway-frontend/package.json b/app/gateway-frontend/package.json index 9ec6aff..4c81cdf 100644 --- a/app/gateway-frontend/package.json +++ b/app/gateway-frontend/package.json @@ -14,6 +14,8 @@ "dependencies": { "@sammo-ts/common": "workspace:*", "@sammo-ts/game-api": "workspace:*", + "@sammo-ts/gateway-api": "workspace:*", + "@sammo-ts/logic": "workspace:*", "@trpc/client": "^11.8.1", "@trpc/server": "^11.8.1", "@vueuse/core": "^13.9.0", diff --git a/app/gateway-frontend/src/utils/gameTrpc.ts b/app/gateway-frontend/src/utils/gameTrpc.ts index 01ed1ba..b7c8f16 100644 --- a/app/gateway-frontend/src/utils/gameTrpc.ts +++ b/app/gateway-frontend/src/utils/gameTrpc.ts @@ -1,5 +1,5 @@ import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'; -import type { appRouter } from '../../../game-api/src/router'; +import type { appRouter } from '@sammo-ts/game-api'; export type GameRouter = typeof appRouter; diff --git a/app/gateway-frontend/tsconfig.json b/app/gateway-frontend/tsconfig.json index ca69c65..1dfea04 100644 --- a/app/gateway-frontend/tsconfig.json +++ b/app/gateway-frontend/tsconfig.json @@ -4,48 +4,75 @@ "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", - "lib": ["ESNext", "DOM", "DOM.Iterable"], + "lib": [ + "ESNext", + "DOM", + "DOM.Iterable" + ], "skipLibCheck": true, - - /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "preserve", - - /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "baseUrl": ".", "paths": { - "@/*": ["./src/*"], - "@sammo-ts/common": ["../../packages/common/src/index.ts"], - "@sammo-ts/common/*": ["../../packages/common/src/*"], - "@sammo-ts/infra": ["../../packages/infra/src/index.ts"], - "@sammo-ts/infra/*": ["../../packages/infra/src/*"], - "@sammo-ts/logic": ["../../packages/logic/src/index.ts"], - "@sammo-ts/logic/*": ["../../packages/logic/src/*"], - "@sammo-ts/game-engine": ["../../app/game-engine/src/index.ts"], - "@sammo-ts/game-engine/*": ["../../app/game-engine/src/*"], - "@sammo-ts/game-api": ["../../app/game-api/src/index.ts"], - "@sammo-ts/game-api/*": ["../../app/game-api/src/*"], - "@sammo-ts/gateway-api": ["../../app/gateway-api/src/index.ts"], - "@sammo-ts/gateway-api/*": ["../../app/gateway-api/src/*"] + "@/*": [ + "./src/*" + ], + "@sammo-ts/common": [ + "../../packages/common/src/index.ts" + ], + "@sammo-ts/common/*": [ + "../../packages/common/src/*" + ], + "@sammo-ts/infra": [ + "../../packages/infra/src/index.ts" + ], + "@sammo-ts/infra/*": [ + "../../packages/infra/src/*" + ], + "@sammo-ts/logic": [ + "../../packages/logic/src/index.ts" + ], + "@sammo-ts/logic/*": [ + "../../packages/logic/src/*" + ], + "@sammo-ts/game-engine": [ + "../../app/game-engine/src/index.ts" + ], + "@sammo-ts/game-engine/*": [ + "../../app/game-engine/src/*" + ], + "@sammo-ts/game-api": [ + "../../app/game-api/src/index.ts" + ], + "@sammo-ts/game-api/*": [ + "../../app/game-api/src/*" + ], + "@sammo-ts/gateway-api": [ + "../../app/gateway-api/src/index.ts" + ], + "@sammo-ts/gateway-api/*": [ + "../../app/gateway-api/src/*" + ] } }, - "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "test/**/*.ts", "*.ts"], + "include": [ + "src/**/*.ts", + "src/**/*.d.ts", + "src/**/*.tsx", + "src/**/*.vue", + "test/**/*.ts" + ], "references": [ - { "path": "./tsconfig.node.json" }, - { "path": "../../packages/common" }, - { "path": "../../packages/infra" }, - { "path": "../../packages/logic" }, - { "path": "../../app/game-engine" }, - { "path": "../../app/game-api" }, - { "path": "../../app/gateway-api" } + { + "path": "./tsconfig.node.json" + } ] -} +} \ No newline at end of file diff --git a/app/gateway-frontend/vite.config.d.ts b/app/gateway-frontend/vite.config.d.ts deleted file mode 100644 index 2c646ae..0000000 --- a/app/gateway-frontend/vite.config.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare const _default: import('vite').UserConfig; -export default _default; diff --git a/app/gateway-frontend/vite.config.js b/app/gateway-frontend/vite.config.js deleted file mode 100644 index 8e28ec9..0000000 --- a/app/gateway-frontend/vite.config.js +++ /dev/null @@ -1,16 +0,0 @@ -import { defineConfig } from 'vite'; -import vue from '@vitejs/plugin-vue'; -import tailwindcss from '@tailwindcss/vite'; -import path from 'path'; -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [vue(), tailwindcss()], - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - }, - }, - server: { - port: 3000, - }, -}); diff --git a/packages/logic/test/scenarios/blankStart.test.ts b/packages/logic/test/scenarios/blankStart.test.ts index fc3eb41..cd543d5 100644 --- a/packages/logic/test/scenarios/blankStart.test.ts +++ b/packages/logic/test/scenarios/blankStart.test.ts @@ -5,19 +5,15 @@ import { MINIMAL_MAP } from '../fixtures/minimalMap.js'; import { InMemoryWorld, TestGameRunner } from '../testEnv.js'; import { buildScenarioBootstrap } from '../../src/world/bootstrap.js'; import type { ScenarioDefinition, ScenarioGeneral } from '../../src/scenario/types.js'; -import type { MapDefinition } from '../../src/world/types.js'; -import type { General, Nation } from '../../src/domain/entities.js'; +import type { Nation } from '../../src/domain/entities.js'; import { commandSpec as foundNationSpec } from '../../src/actions/turn/general/che_건국.js'; import type { TurnCommandEnv } from '../../src/actions/turn/commandEnv.js'; // Mock Scenario Definition const MOCK_SCENARIO: ScenarioDefinition = { - id: 'test_scenario', title: 'Test Scenario', - template: 'test', startYear: 189, - beginYear: 189, - endYear: 250, + life: null, fiction: 0, history: [], ignoreDefaultEvents: false, @@ -30,41 +26,44 @@ const MOCK_SCENARIO: ScenarioDefinition = { events: [], initialEvents: [], config: { + stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 50, npcMin: 10, chiefMin: 70 }, + iconPath: '', + map: {}, + const: {}, environment: { mapName: 'minimal_map', unitSet: 'test_set', - startYear: 189, }, - options: {}, }, }; // Mock General Data -const MOCK_GENERALS: ScenarioGeneral[] = Array.from({ length: 10 }, (_, i) => ({ - name: `General_${i}`, - nation: null, // neutral - city: MINIMAL_MAP.cities[i % MINIMAL_MAP.cities.length].name, - npc: 0, - p_name: `General_${i}`, // Using p_name as personality/picture placeholder if needed by types - uniqueName: `General_${i}`, - officerLevel: 0, - birthYear: 160, - deathYear: 220, - strength: 70 + i, - intelligence: 70 + i, - leadership: 70 + i, - charm: 70 + i, // Note: entities.ts checks stats, ensuring keys match - personality: null, - special: null, - specialWar: null, - affinity: 0, - picture: null, - horse: null, - weapon: null, - book: null, - item: null, - text: null, -})); +const MOCK_GENERALS: ScenarioGeneral[] = Array.from({ length: 10 }, (_, i) => { + const cityDef = MINIMAL_MAP.cities[i % MINIMAL_MAP.cities.length]; + if (!cityDef) throw new Error('City definition missing'); + + return { + name: `General_${i}`, + nation: null, // neutral + city: cityDef.name, + officerLevel: 0, + birthYear: 160, + deathYear: 220, + strength: 70 + i, + intelligence: 70 + i, + leadership: 70 + i, + personality: null, + special: null, + specialWar: null, + affinity: 0, + picture: null, + horse: null, + weapon: null, + book: null, + item: null, + text: null, + }; +}); // We need to inject generals into the scenario object for bootstrap const scenarioWithGenerals = produce(MOCK_SCENARIO, draft => { @@ -95,18 +94,37 @@ describe('Blank Start Scenario', () => { expect(targetGeneral).toBeDefined(); if (!targetGeneral) return; - expect(targetGeneral.nationId).toBe(0); // Should be neutral (nation 0 commonly used for neutral in sammo, or checking bootstrap logic) - // bootstrap puts neutral in nation 0 if includeNeutralNation is true. + expect(targetGeneral.nationId).toBe(0); // 3. Command: Found Nation - const env: TurnCommandEnv = { - general: targetGeneral, - date: new Date(189, 0, 1), + const systemEnv: TurnCommandEnv = { + develCost: 50, + trainDelta: 5, + atmosDelta: 5, + maxTrainByCommand: 100, + maxAtmosByCommand: 100, + sabotageDefaultProb: 0.5, + sabotageProbCoefByStat: 0.1, + sabotageDefenceCoefByGeneralCount: 0.1, + sabotageDamageMin: 10, + sabotageDamageMax: 20, + openingPartYear: 200, + maxGeneral: 10, + defaultNpcGold: 1000, + defaultNpcRice: 1000, + defaultCrewTypeId: 1, + defaultSpecialDomestic: null, + defaultSpecialWar: null, + initialNationGenLimit: 10, + maxTechLevel: 10, + baseGold: 1000, + baseRice: 1000, + maxResourceActionAmount: 1000 }; - const foundNationDef = foundNationSpec.createDefinition(env); + + const foundNationDef = foundNationSpec.createDefinition(systemEnv); // Execute Turn - // We simulate the turn runner processing this command await runner.runTurn([ { generalId: targetGeneral.id, @@ -117,9 +135,6 @@ describe('Blank Start Scenario', () => { ]); // 4. Simulate Turn Processing (Daemon Logic Mock) - // Since logic/actions/engine doesn't auto-create nation, we simulate the "Post-Turn Phase" - // that scans for the 'founding' flag. - const generalAfter = world.getGeneral(targetGeneral.id); expect(generalAfter?.meta?.founding).toBe(true); @@ -132,8 +147,8 @@ describe('Blank Start Scenario', () => { color: '#FF0000', capitalCityId: generalAfter.cityId, chiefGeneralId: generalAfter.id, - gold: 1000, - rice: 1000, + gold: 10000, + rice: 10000, power: 0, level: 1, typeCode: 'che_def', @@ -141,15 +156,22 @@ describe('Blank Start Scenario', () => { }; // Apply updates manually to world - // In a real test we'd add methods to InMemoryWorld to do this cleanly - (world as any).snapshot.nations.push(newNation); + world.snapshot.nations.push(newNation); const city = world.getCity(generalAfter.cityId); if (city) { - (world as any).updateCity({ ...city, nationId: newNationId }); + // Manually update nationId in city + const cityIdx = world.snapshot.cities.findIndex(c => c.id === city.id); + const cityToUpdate = world.snapshot.cities[cityIdx]; + if (cityToUpdate) cityToUpdate.nationId = newNationId; } - (world as any).updateGeneral({ ...generalAfter, nationId: newNationId, meta: { ...generalAfter.meta, founding: false } }); + const genIdx = world.snapshot.generals.findIndex(g => g.id === generalAfter.id); + const genToUpdate = world.snapshot.generals[genIdx]; + if (genToUpdate) { + genToUpdate.nationId = newNationId; + genToUpdate.meta = { ...generalAfter.meta, founding: false }; + } } // 5. Verify diff --git a/packages/logic/test/scenarios/diplomacy.test.ts b/packages/logic/test/scenarios/diplomacy.test.ts index 141624f..d8bfaa0 100644 --- a/packages/logic/test/scenarios/diplomacy.test.ts +++ b/packages/logic/test/scenarios/diplomacy.test.ts @@ -7,7 +7,7 @@ import type { WorldSnapshot } from '../../src/world/types.js'; import { commandSpec as declareWarSpec } from '../../src/actions/turn/nation/che_선전포고.js'; import { commandSpec as deploySpec } from '../../src/actions/turn/general/che_출병.js'; import type { TurnCommandEnv } from '../../src/actions/turn/commandEnv.js'; -import { processDiplomacyMonth, DIPLOMACY_STATE } from '../../src/diplomacy/index.js'; +import { processDiplomacyMonth, DIPLOMACY_STATE, type DiplomacyEntry } from '../../src/diplomacy/index.js'; import { buildWarConfig, buildWarAftermathConfig } from '../../src/actions/turn/actionContextHelpers.js'; describe('Diplomacy Scenario', () => { @@ -18,30 +18,30 @@ describe('Diplomacy Scenario', () => { const mockNationA: Nation = { id: NATION_A_ID, name: 'Nation A', color: '#FF0000', capitalCityId: 1, chiefGeneralId: 1, - gold: 10000, rice: 10000, power: 0, level: 5, typeCode: 'test', meta: { tech: 1000 } + gold: 10000, rice: 10000, power: 0, level: 5, typeCode: 'test', meta: {} }; const mockNationB: Nation = { id: NATION_B_ID, name: 'Nation B', color: '#0000FF', capitalCityId: 2, chiefGeneralId: 2, - gold: 10000, rice: 10000, power: 0, level: 5, typeCode: 'test', meta: { tech: 1000 } + gold: 10000, rice: 10000, power: 0, level: 5, typeCode: 'test', meta: {} }; - // Two adjacent cities - const cityADef = MINIMAL_MAP.cities[0]; // A - const cityBDef = MINIMAL_MAP.cities[1]; // B + const cityADef = MINIMAL_MAP.cities[0]; + const cityBDef = MINIMAL_MAP.cities[1]; + if (!cityADef || !cityBDef) throw new Error('City definition missing'); const mockCityA: City = { - id: 1, name: cityADef.name, nationId: NATION_A_ID, level: 1, region: 1, state: 0, + id: 1, name: cityADef.name, nationId: NATION_A_ID, level: 1, state: 0, population: 10000, populationMax: 10000, agriculture: 1000, agricultureMax: 1000, commerce: 1000, commerceMax: 1000, security: 1000, securityMax: 1000, defence: 1000, defenceMax: 1000, wall: 1000, wallMax: 1000, - supplyState: 1, frontState: 0, trust: 1000, trade: 1000, meta: {} + supplyState: 1, frontState: 0, meta: {} }; const mockCityB: City = { - id: 2, name: cityBDef.name, nationId: NATION_B_ID, level: 1, region: 1, state: 0, + id: 2, name: cityBDef.name, nationId: NATION_B_ID, level: 1, state: 0, population: 10000, populationMax: 10000, agriculture: 1000, agricultureMax: 1000, commerce: 1000, commerceMax: 1000, security: 1000, securityMax: 1000, defence: 1000, defenceMax: 1000, wall: 1000, wallMax: 1000, - supplyState: 1, frontState: 0, trust: 1000, trade: 1000, meta: {} + supplyState: 1, frontState: 0, meta: {} }; const mockGeneralA: General = { @@ -75,15 +75,34 @@ describe('Diplomacy Scenario', () => { const world = new InMemoryWorld(snapshot); const runner = new TestGameRunner(world, 200, 1); - const env: TurnCommandEnv = { - general: mockGeneralA, - date: new Date(200, 0, 1), - unitSet: snapshot.unitSet, - map: snapshot.map + + const systemEnv: TurnCommandEnv = { + develCost: 50, + trainDelta: 5, + atmosDelta: 5, + maxTrainByCommand: 100, + maxAtmosByCommand: 100, + sabotageDefaultProb: 0.5, + sabotageProbCoefByStat: 0.1, + sabotageDefenceCoefByGeneralCount: 0.1, + sabotageDamageMin: 10, + sabotageDamageMax: 20, + openingPartYear: 200, + maxGeneral: 10, + defaultNpcGold: 1000, + defaultNpcRice: 1000, + defaultCrewTypeId: 1, + defaultSpecialDomestic: null, + defaultSpecialWar: null, + initialNationGenLimit: 10, + maxTechLevel: 10, + baseGold: 1000, + baseRice: 1000, + maxResourceActionAmount: 1000 }; // 2. Declare War - const declareWarDef = declareWarSpec.createDefinition(env); + const declareWarDef = declareWarSpec.createDefinition(systemEnv); await runner.runTurn([{ generalId: 1, commandKey: 'che_선전포고', @@ -99,12 +118,11 @@ describe('Diplomacy Scenario', () => { const warConfig = buildWarConfig(snapshot.scenarioConfig, snapshot.unitSet!); const aftermathConfig = buildWarAftermathConfig(snapshot.scenarioConfig, warConfig.castleCrewTypeId); - // Manual WarTimeContext const warTime = { year: 200, month: 1, - season: 0, // Spring - turn: 1 // mock value + season: 0, + turn: 1 }; const seedBase = 'test_seed'; @@ -112,8 +130,6 @@ describe('Diplomacy Scenario', () => { const destCity = world.getCity(destCityId)!; const destNation = world.getNation(destCity.nationId); - // Update warTime based on runner's date if needed, but static is fine for test step - // Actually let's use runner's date for 'realism' in simulation step 5 warTime.year = runner.currentDate.getFullYear(); warTime.month = runner.currentDate.getMonth() + 1; warTime.season = Math.floor(runner.currentDate.getMonth() / 3); @@ -124,13 +140,18 @@ describe('Diplomacy Scenario', () => { warConfig, aftermathConfig, time: { ...warTime }, - seedBase + seedBase, + map: world.snapshot.map, + unitSet: world.snapshot.unitSet, + cities: world.getAllCities(), + nations: world.getAllNations(), + generals: world.getAllGenerals(), + diplomacy: world.snapshot.diplomacy }; }; - // 3. Try to Deploy (Should Fail/Crash if blocked, or return empty effects) - // With corrected context, this call should PROCEED to check logic. - const deployDef = deploySpec.createDefinition(env); + // 3. Try to Deploy + const deployDef = deploySpec.createDefinition(systemEnv); await runner.runTurn([{ generalId: 1, commandKey: 'che_출병', @@ -139,17 +160,30 @@ describe('Diplomacy Scenario', () => { context: buildDispatchContext(2) }]); - // Assert failure: No troops created (snapshot.troops still empty) expect(world.snapshot.troops.length).toBe(0); // 4. Simulate State Transition for (let i = 0; i < 24; i++) { - world.snapshot.diplomacy = processDiplomacyMonth( - world.snapshot.diplomacy, - new Map() - ); - runner.currentDate.setMonth(runner.currentDate.getMonth() + 1); // Advance runner time too + const entries: DiplomacyEntry[] = world.snapshot.diplomacy.map(s => ({ + fromNationId: s.fromNationId, + toNationId: s.toNationId, + state: s.state, + term: s.durationMonths, + dead: 0, + meta: {} + })); + + const updatedEntries = processDiplomacyMonth(entries, new Map()); + + world.snapshot.diplomacy = updatedEntries.map(e => ({ + fromNationId: e.fromNationId, + toNationId: e.toNationId, + state: e.state, + durationMonths: e.term + })); + + runner.currentDate.setMonth(runner.currentDate.getMonth() + 1); } const diplomacyAB_After = world.getDiplomacy(NATION_A_ID, NATION_B_ID); @@ -166,11 +200,5 @@ describe('Diplomacy Scenario', () => { const generalAfter = world.getGeneral(1)!; console.log(`General Exp: ${mockGeneralA.experience} -> ${generalAfter.experience}`); - - // Final assertion: expect experience to have changed (battle happened) or at least no crash - // Note: if battle outcome is stalemate/loss, exp might minimally change or change a lot. - // And if 'unable to find path' even in WAR (due to some other reason), exp won't change. - // But map is connected. - // We just verified critical path execution. }); }); diff --git a/packages/logic/test/scenarios/domestic.test.ts b/packages/logic/test/scenarios/domestic.test.ts index c5591f5..5036615 100644 --- a/packages/logic/test/scenarios/domestic.test.ts +++ b/packages/logic/test/scenarios/domestic.test.ts @@ -5,94 +5,40 @@ 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 developAgricultureSpec } from '../../src/actions/turn/general/che_농지개간.js'; -import { commandSpec as commercialSpec } from '../../src/actions/turn/general/che_상업투자.js'; import type { TurnCommandEnv } from '../../src/actions/turn/commandEnv.js'; describe('Domestic Affairs Scenario', () => { it('should increase agriculture when executing "Farming" command', async () => { - // 1. Setup World with existing nation/city/general - const NATION_ID = 1; - const CITY_ID = 1; - const GENERAL_ID = 1; - + // 1. Setup World const mockNation: Nation = { - id: NATION_ID, - name: 'TestNation', - color: '#0000FF', - capitalCityId: CITY_ID, - chiefGeneralId: GENERAL_ID, - gold: 10000, - rice: 10000, - power: 0, - level: 1, - typeCode: 'test', - meta: { tech: 1000 } + id: 1, name: 'Test Nation', color: '#FF0000', capitalCityId: 1, chiefGeneralId: 1, + gold: 10000, rice: 10000, power: 0, level: 5, typeCode: 'test', meta: {} }; - const cityDef = MINIMAL_MAP.cities.find(c => c.id === CITY_ID)!; + const cityDef = MINIMAL_MAP.cities[0]; + if (!cityDef) throw new Error('City definition missing'); const mockCity: City = { - id: CITY_ID, - name: cityDef.name, - nationId: NATION_ID, - level: cityDef.level, - region: cityDef.region, - state: 0, - population: 10000, - populationMax: cityDef.max.population, - agriculture: 500, - agricultureMax: cityDef.max.agriculture, - commerce: 500, - commerceMax: cityDef.max.commerce, - security: 500, - securityMax: cityDef.max.security, - defence: 200, - defenceMax: cityDef.max.defence, - wall: 200, - wallMax: cityDef.max.wall, - supplyState: 1, - frontState: 0, - trust: 50, - trade: 100, - meta: {} + id: 1, name: cityDef.name, nationId: 1, level: 1, state: 0, + population: 10000, populationMax: 10000, agriculture: 500, agricultureMax: 1000, + commerce: 500, commerceMax: 1000, security: 500, securityMax: 1000, + defence: 500, defenceMax: 1000, wall: 500, wallMax: 1000, + supplyState: 1, frontState: 0, meta: {} }; const mockGeneral: General = { - id: GENERAL_ID, - name: 'Governor A', - nationId: NATION_ID, - cityId: CITY_ID, - troopId: 0, - npcState: 0, - experience: 100, - dedication: 100, - officerLevel: 5, - gold: 1000, - rice: 1000, - crew: 5000, - crewTypeId: 1, - train: 80, - atmos: 80, - injury: 0, - age: 30, - stats: { - leadership: 80, - strength: 80, - intelligence: 80, // High int implies better political results usually - }, - role: { - personality: null, - specialDomestic: null, - specialWar: null, - items: { horse: null, weapon: null, book: null, item: null } - }, - triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} }, - meta: {} + id: 1, name: 'Domestic Officer', nationId: 1, cityId: 1, troopId: 0, npcState: 0, + experience: 100, dedication: 100, officerLevel: 5, gold: 1000, rice: 1000, crew: 0, crewTypeId: 0, + train: 100, atmos: 100, injury: 0, age: 30, + stats: { leadership: 50, strength: 50, intelligence: 100 }, + role: { personality: null, specialDomestic: null, specialWar: null, items: { horse: null, weapon: null, book: null, item: null } }, + triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} }, meta: {} }; const snapshot: WorldSnapshot = { scenarioConfig: { environment: { mapName: 'minimal_map', unitSet: 'default' }, options: {} } as any, scenarioMeta: { title: 'Test', startYear: 200, life: 0, fiction: 0, history: [], ignoreDefaultEvents: false }, map: MINIMAL_MAP, + unitSet: { id: 'default', name: 'default' }, nations: [mockNation], cities: [mockCity], generals: [mockGeneral], @@ -105,35 +51,95 @@ describe('Domestic Affairs Scenario', () => { const world = new InMemoryWorld(snapshot); const runner = new TestGameRunner(world, 200, 1); - // 2. Command: Develop Agriculture - const env: TurnCommandEnv = { - general: mockGeneral, - date: new Date(200, 0, 1), + // 2. Prepare Command + const systemEnv: TurnCommandEnv = { + develCost: 50, + trainDelta: 5, + atmosDelta: 5, + maxTrainByCommand: 100, + maxAtmosByCommand: 100, + sabotageDefaultProb: 0.5, + sabotageProbCoefByStat: 0.1, + sabotageDefenceCoefByGeneralCount: 0.1, + sabotageDamageMin: 10, + sabotageDamageMax: 20, + openingPartYear: 200, + maxGeneral: 10, + defaultNpcGold: 1000, + defaultNpcRice: 1000, + defaultCrewTypeId: 1, + defaultSpecialDomestic: null, + defaultSpecialWar: null, + initialNationGenLimit: 10, + maxTechLevel: 10, + baseGold: 1000, + baseRice: 1000, + maxResourceActionAmount: 1000 }; - const farmingDef = developAgricultureSpec.createDefinition(env); - const initialAgri = world.getCity(CITY_ID)!.agriculture; + const farmingDef = developAgricultureSpec.createDefinition(systemEnv); - // Execute Turn - await runner.runTurn([ - { - generalId: GENERAL_ID, - commandKey: 'che_농지개간', - resolver: farmingDef, - args: {} - } - ]); + // 3. Execute + await runner.runTurn([{ + generalId: 1, + commandKey: 'che_농지개간', + resolver: farmingDef, + args: {} + }]); - // 3. Verify - const cityAfter = world.getCity(CITY_ID)!; - expect(cityAfter.agriculture).toBeGreaterThan(initialAgri); - - // Verify upper bound if applicable (handled by constraints usually, but good to check it changed) - console.log(`Agriculture: ${initialAgri} -> ${cityAfter.agriculture}`); + // 4. Verify + const updatedCity = world.getCity(1)!; + console.log(`Agriculture: 500 -> ${updatedCity.agriculture}`); + expect(updatedCity.agriculture).toBeGreaterThan(500); }); - it('should NOT increase if city is fully developed or insufficient conditions', async () => { - // Setup similar to above but with max agriculture - // Implementation skipped for brevity in this step, focusing on success case first + it('should not increase agriculture when city is already maxed', async () => { + // Setup with max agriculture + const mockNation: Nation = { + id: 1, name: 'Test Nation', color: '#FF0000', capitalCityId: 1, chiefGeneralId: 1, + gold: 10000, rice: 10000, power: 0, level: 5, typeCode: 'test', meta: {} + }; + const mockCity: City = { + id: 1, name: 'City A', nationId: 1, level: 1, state: 0, + population: 10000, populationMax: 10000, agriculture: 1000, agricultureMax: 1000, + commerce: 500, commerceMax: 1000, security: 500, securityMax: 1000, + defence: 500, defenceMax: 1000, wall: 500, wallMax: 1000, + supplyState: 1, frontState: 0, meta: {} + }; + const mockGeneral: General = { + id: 1, name: 'Domestic Officer', nationId: 1, cityId: 1, troopId: 0, npcState: 0, + experience: 100, dedication: 100, officerLevel: 5, gold: 1000, rice: 1000, crew: 0, crewTypeId: 0, + train: 100, atmos: 100, injury: 0, age: 30, + stats: { leadership: 50, strength: 50, intelligence: 100 }, + role: { personality: null, specialDomestic: null, specialWar: null, items: { horse: null, weapon: null, book: null, item: null } }, + triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} }, meta: {} + }; + + const snapshot: WorldSnapshot = { + scenarioConfig: { environment: { mapName: 'minimal_map', unitSet: 'default' }, options: {} } as any, + map: MINIMAL_MAP, + nations: [mockNation], + cities: [mockCity], + generals: [mockGeneral], + troops: [], + diplomacy: [], + events: [], + initialEvents: [] + }; + const world = new InMemoryWorld(snapshot); + const runner = new TestGameRunner(world, 200, 1); + + const systemEnv: TurnCommandEnv = { develCost: 50 } as any; + const farmingDef = developAgricultureSpec.createDefinition(systemEnv); + + await runner.runTurn([{ + generalId: 1, + commandKey: 'che_농지개간', + resolver: farmingDef, + args: {} + }]); + + const updatedCity = world.getCity(1)!; + expect(updatedCity.agriculture).toBe(1000); }); }); diff --git a/packages/logic/test/scenarios/troops.test.ts b/packages/logic/test/scenarios/troops.test.ts index 093c8ec..24a880e 100644 --- a/packages/logic/test/scenarios/troops.test.ts +++ b/packages/logic/test/scenarios/troops.test.ts @@ -4,125 +4,49 @@ 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 draftSpec } from '../../src/actions/turn/general/che_징병.js'; +import { commandSpec as recruitSpec } from '../../src/actions/turn/general/che_징병.js'; import { commandSpec as trainSpec } from '../../src/actions/turn/general/che_훈련.js'; -import { commandSpec as moraleSpec } 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'; describe('Troop Management Scenario', () => { it('should successfully draft troops, then train and boost morale', async () => { - // 1. Setup - const NATION_ID = 1; - const CITY_ID = 1; - const GENERAL_ID = 1; - + // 1. Setup World const mockNation: Nation = { - id: NATION_ID, - name: 'TroopNation', - color: '#00FF00', - capitalCityId: CITY_ID, - chiefGeneralId: GENERAL_ID, - gold: 50000, - rice: 50000, - power: 0, - level: 3, - typeCode: 'test', - meta: { tech: 5000 } // High tech + id: 1, name: 'Militaristic Nation', color: '#FF0000', capitalCityId: 1, chiefGeneralId: 1, + gold: 50000, rice: 50000, power: 0, level: 5, typeCode: 'test', meta: {} }; - const cityDef = MINIMAL_MAP.cities.find(c => c.id === CITY_ID)!; + const cityDef = MINIMAL_MAP.cities[0]; + if (!cityDef) throw new Error('City definition missing'); const mockCity: City = { - id: CITY_ID, - name: cityDef.name, - nationId: NATION_ID, - level: cityDef.level, - region: cityDef.region, - state: 0, - population: 50000, // Increased to meet minimum drafting population (30000+) - populationMax: cityDef.max.population, - agriculture: 1000, - agricultureMax: cityDef.max.agriculture, - commerce: 1000, - commerceMax: cityDef.max.commerce, - security: 1000, - securityMax: cityDef.max.security, - defence: 500, - defenceMax: cityDef.max.defence, - wall: 500, - wallMax: cityDef.max.wall, - supplyState: 1, - frontState: 0, - trust: 100, - trade: 100, - meta: {} + id: 1, name: cityDef.name, nationId: 1, level: 1, state: 0, + population: 50000, populationMax: 50000, agriculture: 500, agricultureMax: 1000, + commerce: 500, commerceMax: 1000, security: 500, securityMax: 1000, + defence: 500, defenceMax: 1000, wall: 500, wallMax: 1000, + supplyState: 1, frontState: 0, meta: {} }; - // General starts with 0 troops but high leadership const mockGeneral: General = { - id: GENERAL_ID, - name: 'Commander T', - nationId: NATION_ID, - cityId: CITY_ID, - troopId: 0, - npcState: 0, - experience: 100, - dedication: 100, - officerLevel: 5, - gold: 3000, - rice: 3000, - crew: 0, - crewTypeId: 1, // Basic infantry (assuming 1 is valid) - train: 10, - atmos: 10, - injury: 0, - age: 25, - stats: { - leadership: 95, - strength: 80, - intelligence: 80, - }, - role: { - personality: null, - specialDomestic: null, - specialWar: null, - items: { horse: null, weapon: null, book: null, item: null } - }, - triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} }, - meta: {} - }; - - // Simple UnitSet for test - const unitSet = { - id: 'default', - name: 'Default', - crewTypes: [ - { - id: 1, - armType: 1, - name: '보병', - attack: 10, - defence: 10, - speed: 10, - avoid: 0, - magicCoef: 0, - cost: 10, - rice: 1, - requirements: [], - attackCoef: {}, - defenceCoef: {}, - info: [], - initSkillTrigger: [], - phaseSkillTrigger: [], - iActionList: [] - } - ] + id: 1, name: 'General Lee', nationId: 1, cityId: 1, troopId: 0, npcState: 0, + experience: 100, dedication: 100, officerLevel: 5, gold: 1000, rice: 1000, crew: 0, crewTypeId: 1, + train: 10, atmos: 10, injury: 0, age: 30, + stats: { leadership: 100, strength: 100, intelligence: 50 }, + role: { personality: null, specialDomestic: null, specialWar: null, items: { horse: null, weapon: null, book: null, item: null } }, + triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} }, meta: {} }; const snapshot: WorldSnapshot = { scenarioConfig: { environment: { mapName: 'minimal_map', unitSet: 'default' }, options: {} } as any, scenarioMeta: { title: 'Test', startYear: 200, life: 0, fiction: 0, history: [], ignoreDefaultEvents: false }, map: MINIMAL_MAP, - unitSet: unitSet as any, + unitSet: { + id: 'default', + name: 'default', + crewTypes: [ + { id: 1, name: 'Infantry', armType: 1, cost: 10, requirements: [] } + ] + } as any, nations: [mockNation], cities: [mockCity], generals: [mockGeneral], @@ -133,65 +57,70 @@ describe('Troop Management Scenario', () => { }; const world = new InMemoryWorld(snapshot); - const runner = new TestGameRunner(world, 201, 1); - const env: TurnCommandEnv = { - general: mockGeneral, - date: new Date(201, 0, 1), - unitSet: unitSet as any + const runner = new TestGameRunner(world, 200, 1); + + const systemEnv: TurnCommandEnv = { + develCost: 50, + trainDelta: 35, + atmosDelta: 35, + maxTrainByCommand: 100, + maxAtmosByCommand: 100, + sabotageDefaultProb: 0.5, + sabotageProbCoefByStat: 0.1, + sabotageDefenceCoefByGeneralCount: 0.1, + sabotageDamageMin: 10, + sabotageDamageMax: 20, + openingPartYear: 200, + maxGeneral: 10, + defaultNpcGold: 1000, + defaultNpcRice: 1000, + defaultCrewTypeId: 1, + defaultSpecialDomestic: null, + defaultSpecialWar: null, + initialNationGenLimit: 10, + maxTechLevel: 10, + baseGold: 1000, + baseRice: 1000, + maxResourceActionAmount: 1000 }; - const draftDef = draftSpec.createDefinition(env); - const trainDef = trainSpec.createDefinition(env); - const moraleDef = moraleSpec.createDefinition(env); - - // Step 1: Draft command (args usually amount=0 means max or specific amount) - // Let's assume drafting 1000 troops. - // We need to check draft command args. `che_징병.ts` implementation details? - // Assuming { amount: 1000 } or similar. - // Let's check `che_징병.ts` args type if possible later, but standard is `amount: number`. - + // 2. Draft Troops + const recruitDef = recruitSpec.createDefinition(systemEnv); await runner.runTurn([{ - generalId: GENERAL_ID, + generalId: 1, commandKey: 'che_징병', - resolver: draftDef, - args: { - amount: 1000, - crewType: 1 // Must specify crewType - } + resolver: recruitDef, + args: { crewType: 1, amount: 1000 } }]); - let general = world.getGeneral(GENERAL_ID)!; - expect(general.crew).toBeGreaterThan(0); - console.log(`Drafted: ${general.crew}`); - const draftedCrew = general.crew; + const generalAfterDraft = world.getGeneral(1)!; + console.log(`Drafted: ${generalAfterDraft.crew}`); + expect(generalAfterDraft.crew).toBe(1000); - // Step 2: Update env with new general state (important for constraints/logic that depends on current state) - env.general = general; - - // Step 3: Train command + // 3. Train + const trainDef = trainSpec.createDefinition(systemEnv); await runner.runTurn([{ - generalId: GENERAL_ID, + generalId: 1, commandKey: 'che_훈련', resolver: trainDef, args: {} }]); - general = world.getGeneral(GENERAL_ID)!; - expect(general.train).toBeGreaterThan(10); - console.log(`Train: 10 -> ${general.train}`); + const generalAfterTrain = world.getGeneral(1)!; + console.log(`Train: 10 -> ${generalAfterTrain.train}`); + expect(generalAfterTrain.train).toBeGreaterThan(10); - env.general = general; - - // Step 4: Morale boost + // 4. Boost Morale + const atmosDef = atmosSpec.createDefinition(systemEnv); await runner.runTurn([{ - generalId: GENERAL_ID, + generalId: 1, commandKey: 'che_사기진작', - resolver: moraleDef, + resolver: atmosDef, args: {} }]); - general = world.getGeneral(GENERAL_ID)!; - expect(general.atmos).toBeGreaterThan(10); - console.log(`Atmos: 10 -> ${general.atmos}`); + const generalAfterAtmos = world.getGeneral(1)!; + console.log(`Atmos: 10 -> ${generalAfterAtmos.atmos}`); + expect(generalAfterAtmos.atmos).toBeGreaterThan(10); }); }); diff --git a/packages/logic/test/testEnv.ts b/packages/logic/test/testEnv.ts index 5f799be..e925dbe 100644 --- a/packages/logic/test/testEnv.ts +++ b/packages/logic/test/testEnv.ts @@ -1,15 +1,16 @@ -import { produceWithPatches, enablePatches } from 'immer'; -import type { City, General, Nation, GeneralId, NationId, CityId, GeneralTriggerState } from '../src/domain/entities.js'; +import { enablePatches } from 'immer'; +import type { City, General, Nation, GeneralId, NationId, CityId } from '../src/domain/entities.js'; import type { WorldSnapshot } from '../src/world/types.js'; import { type GeneralActionResolution, resolveGeneralAction, type GeneralActionResolver } from '../src/actions/engine.js'; import type { TurnSchedule } from '../src/turn/calendar.js'; import { type DiplomacyEntry, applyDiplomacyPatch, buildDefaultDiplomacy } from '../src/diplomacy/index.js'; +import type { ScenarioDiplomacy } from '../src/scenario/types.js'; enablePatches(); export class InMemoryWorld { - private snapshot: WorldSnapshot; + public snapshot: WorldSnapshot; constructor(initialSnapshot: WorldSnapshot) { this.snapshot = initialSnapshot; @@ -89,8 +90,19 @@ export class InMemoryWorld { if (effect.type === 'diplomacy:patch') { const srcId = effect.srcNationId; const destId = effect.destNationId; - const existing = this.getDiplomacy(srcId, destId) ?? buildDefaultDiplomacy(srcId, destId); - const patched = applyDiplomacyPatch(existing, effect.patch); + const existing = this.getDiplomacy(srcId, destId); + + // Convert ScenarioDiplomacy to DiplomacyEntry for applying patch + const entry: DiplomacyEntry = existing ? { + fromNationId: existing.fromNationId, + toNationId: existing.toNationId, + state: existing.state, + term: existing.durationMonths, + dead: 0, + meta: {} + } : buildDefaultDiplomacy(srcId, destId); + + const patched = applyDiplomacyPatch(entry, effect.patch); this.updateDiplomacy(patched); } } @@ -120,23 +132,30 @@ export class InMemoryWorld { } } - getDiplomacy(srcId: number, destId: number): DiplomacyEntry | undefined { + getDiplomacy(srcId: number, destId: number): ScenarioDiplomacy | undefined { return this.snapshot.diplomacy.find(d => d.fromNationId === srcId && d.toNationId === destId); } updateDiplomacy(entry: DiplomacyEntry) { + const scenarioEntry: ScenarioDiplomacy = { + fromNationId: entry.fromNationId, + toNationId: entry.toNationId, + state: entry.state, + durationMonths: entry.term + }; + const idx = this.snapshot.diplomacy.findIndex(d => d.fromNationId === entry.fromNationId && d.toNationId === entry.toNationId); if (idx >= 0) { - this.snapshot.diplomacy[idx] = entry; + this.snapshot.diplomacy[idx] = scenarioEntry; } else { - this.snapshot.diplomacy.push(entry); + this.snapshot.diplomacy.push(scenarioEntry); } } } export interface TestCommand { generalId: GeneralId; - commandKey: string; // Not used directly here, but for clarity + commandKey: string; resolver: GeneralActionResolver; args: unknown; context?: Record; @@ -151,11 +170,10 @@ export class TestGameRunner { this.currentDate = new Date(startYear, startMonth - 1); } - // A simplified run turn method async runTurn(commands: TestCommand[]) { const schedule: TurnSchedule = { entries: [ - { startMinute: 0, tickMinutes: 60 } // simplified: 1 hour ticks for whole day + { startMinute: 0, tickMinutes: 60 } ] }; @@ -164,9 +182,10 @@ export class TestGameRunner { if (!general) continue; const city = this.world.getCity(general.cityId); + if (!city) throw new Error(`General ${general.id} is in non-existent city ${general.cityId}`); + const nation = general.nationId ? this.world.getNation(general.nationId) : null; - // In test env, we might not have full context, but engine needs basic entities const inputContext = { general, city, @@ -174,23 +193,13 @@ export class TestGameRunner { rng: { real: () => Math.random(), int: (min: number, max: number) => Math.floor(Math.random() * (max - min)) + min, - nextInt: (min: number, max: number) => Math.floor(Math.random() * (max - min)) + min, // Added nextInt + nextInt: (min: number, max: number) => Math.floor(Math.random() * (max - min)) + min, next: () => Math.random() - } as any, // Mock RNG + } as any, - // Extended context for specific commands (Recruit etc.) - map: this.world.snapshot.map, - unitSet: this.world.snapshot.unitSet, - cities: this.world.getAllCities(), - nations: this.world.getAllNations(), - currentYear: this.currentDate.getFullYear(), - startYear: this.world.snapshot.scenarioMeta?.startYear, - // GeneralActionContext base requirements year: this.currentDate.getFullYear(), month: this.currentDate.getMonth() + 1, season: Math.floor(this.currentDate.getMonth() / 3), - diplomacy: this.world.snapshot.diplomacy, - generals: this.world.getAllGenerals(), ...cmd.context }; @@ -201,7 +210,7 @@ export class TestGameRunner { const resolution = resolveGeneralAction( cmd.resolver, - inputContext, + inputContext as any, scheduleContext, cmd.args ); @@ -209,7 +218,6 @@ export class TestGameRunner { await this.world.applyResolution(resolution); } - // Advance time this.currentDate.setMonth(this.currentDate.getMonth() + 1); } } diff --git a/packages/logic/tsconfig.json b/packages/logic/tsconfig.json index 85cce3a..7aceea6 100644 --- a/packages/logic/tsconfig.json +++ b/packages/logic/tsconfig.json @@ -7,7 +7,9 @@ "module": "NodeNext", "moduleResolution": "NodeNext", "target": "ES2023", - "lib": ["ES2024"], + "lib": [ + "ES2024" + ], "verbatimModuleSyntax": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, @@ -16,8 +18,15 @@ "noFallthroughCasesInSwitch": true, "useUnknownInCatchVariables": true, "noUnusedLocals": true, - "noUnusedParameters": true + "noUnusedParameters": true, + "rootDir": "src" }, - "include": ["src", "test", "*.ts"], - "references": [{ "path": "../common" }] -} + "include": [ + "src" + ], + "references": [ + { + "path": "../common" + } + ] +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9501b33..9c788ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -203,6 +203,12 @@ importers: '@sammo-ts/game-api': specifier: workspace:* version: link:../game-api + '@sammo-ts/gateway-api': + specifier: workspace:* + version: link:../gateway-api + '@sammo-ts/logic': + specifier: workspace:* + version: link:../../packages/logic '@trpc/client': specifier: ^11.8.1 version: 11.8.1(@trpc/server@11.8.1(typescript@5.9.3))(typescript@5.9.3) diff --git a/tsdown.config.ts b/tsdown.config.ts index b72576b..8063c9a 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -8,7 +8,9 @@ export default defineConfig({ entry: 'src/index.ts', format: 'es', outDir: 'dist', - dts: true, + dts: { + build: true + }, sourcemap: true, target: 'node22', platform: 'node',