diff --git a/packages/logic/src/actions/turn/general/che_NPC능동.ts b/packages/logic/src/actions/turn/general/che_NPC능동.ts index d65079f..f77b798 100644 --- a/packages/logic/src/actions/turn/general/che_NPC능동.ts +++ b/packages/logic/src/actions/turn/general/che_NPC능동.ts @@ -1,6 +1,6 @@ import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js'; import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js'; -import { mustBeNPC, reqGeneralGold, unknownOrDeny, existsDestCity } from '@sammo-ts/logic/constraints/presets.js'; +import { mustBeNPC, existsDestCity } from '@sammo-ts/logic/constraints/presets.js'; import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; import type { GeneralActionOutcome, @@ -104,7 +104,7 @@ export class ActionDefinition< return null; } - buildConstraints(ctx: ConstraintContext, args: NPCSelfArgs): Constraint[] { + buildConstraints(_ctx: ConstraintContext, args: NPCSelfArgs): Constraint[] { const constraints = [mustBeNPC()]; if (args.optionText === '순간이동') { diff --git a/packages/logic/src/actions/turn/general/che_강행.ts b/packages/logic/src/actions/turn/general/che_강행.ts index 1d5e99b..7039aec 100644 --- a/packages/logic/src/actions/turn/general/che_강행.ts +++ b/packages/logic/src/actions/turn/general/che_강행.ts @@ -5,7 +5,7 @@ import { nearCity, reqGeneralGold, reqGeneralRice, - unknownOrDeny, + existsDestCity, } from '@sammo-ts/logic/constraints/presets.js'; import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; diff --git a/packages/logic/src/actions/turn/general/che_귀환.ts b/packages/logic/src/actions/turn/general/che_귀환.ts index 6827b36..c2390c5 100644 --- a/packages/logic/src/actions/turn/general/che_귀환.ts +++ b/packages/logic/src/actions/turn/general/che_귀환.ts @@ -1,12 +1,12 @@ -import type { City, General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js'; +import type { City, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js'; import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js'; import { - allow, + // allow, notBeNeutral, notWanderingNation, notCapital, readMetaNumberFromUnknown, - unknownOrDeny, + // unknownOrDeny, } from '@sammo-ts/logic/constraints/presets.js'; import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; import type { @@ -22,7 +22,7 @@ import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js' import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js'; import type { GeneralTurnCommandSpec } from './index.js'; -export interface ReturnArgs {} +export interface ReturnArgs { } const ACTION_NAME = '귀환'; const ACTION_KEY = 'che_귀환'; @@ -100,7 +100,7 @@ export class ActionResolver< } const josaRo = JosaUtil.pick(destCityName, '로'); - const date = 'XX:XX'; // Placeholder for date, actual date is handled by log format usually, but legacy includes it explicitly? + // const date = 'XX:XX'; // Legacy: <1>$date // LogFormat.MONTH handles date prefix usually. LogFormat.HM handles HH:MM. // Legacy output: "StartCity로 귀환했습니다. 10:00" diff --git a/packages/logic/src/actions/turn/general/index.ts b/packages/logic/src/actions/turn/general/index.ts index aa79ab6..2900289 100644 --- a/packages/logic/src/actions/turn/general/index.ts +++ b/packages/logic/src/actions/turn/general/index.ts @@ -3,7 +3,7 @@ import type { TurnCommandModule, TurnCommandSpecBase } from '@sammo-ts/logic/act export const GENERAL_TURN_COMMAND_KEYS = [ 'che_거병', 'che_임관', - 'che_랜덤임관', + // 'che_랜덤임관', 'che_귀환', 'che_등용수락', 'che_건국', @@ -60,7 +60,7 @@ const defaultImporters: Record import('./che_거병.js'), che_임관: async () => import('./che_임관.js'), che_등용수락: () => import('./che_등용수락.js'), - che_랜덤임관: () => import('./che_랜덤임관.js'), + // che_랜덤임관: () => import('./che_랜덤임관.js'), che_귀환: async () => import('./che_귀환.js'), che_건국: async () => import('./che_건국.js'), che_훈련: async () => import('./che_훈련.js'), diff --git a/packages/logic/src/constraints/city.ts b/packages/logic/src/constraints/city.ts index b1fc946..0473a60 100644 --- a/packages/logic/src/constraints/city.ts +++ b/packages/logic/src/constraints/city.ts @@ -541,11 +541,11 @@ export const notCapital = (checkCurrentCity = false): Constraint => ({ } } // Need nation to know capital - reqs.push({ kind: 'nation', id: ctx.nationId }); + reqs.push({ kind: 'nation', id: ctx.nationId ?? 0 }); return reqs; }, test: (ctx, view) => { - const nationReq: RequirementKey = { kind: 'nation', id: ctx.nationId }; + const nationReq: RequirementKey = { kind: 'nation', id: ctx.nationId ?? 0 }; if (!view.has(nationReq)) return unknownOrDeny(ctx, [nationReq], '국가 정보가 없습니다.'); const nation = view.get(nationReq) as Nation | null; if (!nation) return unknownOrDeny(ctx, [nationReq], '국가 정보가 없습니다.'); diff --git a/packages/logic/src/constraints/general.ts b/packages/logic/src/constraints/general.ts index 9a34496..2697795 100644 --- a/packages/logic/src/constraints/general.ts +++ b/packages/logic/src/constraints/general.ts @@ -181,11 +181,11 @@ export const existsDestGeneral = (): Constraint => ({ requires: (ctx) => resolveDestGeneralId(ctx) !== undefined ? [ - { - kind: 'destGeneral', - id: resolveDestGeneralId(ctx) ?? 0, - }, - ] + { + kind: 'destGeneral', + id: resolveDestGeneralId(ctx) ?? 0, + }, + ] : [], test: (ctx, view) => { const destGeneralId = resolveDestGeneralId(ctx); @@ -292,8 +292,8 @@ export const mustBeNPC = (): Constraint => ({ if (!general) { return unknownOrDeny(ctx, [req], '장수 정보가 없습니다.'); } - // Assuming npcState >= 2 means NPC. Need to verify exact logic if possible, - // but typically 0=human, 1=?, 2=NPC. + // Assuming npcState >= 2 means NPC. Need to verify exact logic if possible, + // but typically 0=human, 1=?, 2=NPC. // Legacy: $general->getNPC() where 0:User, 1:Virtual User(unused?), 2:NPC ... if (general.npcState >= 2) { return allow(); diff --git a/packages/logic/src/world/distance.ts b/packages/logic/src/world/distance.ts index b39becf..85600f9 100644 --- a/packages/logic/src/world/distance.ts +++ b/packages/logic/src/world/distance.ts @@ -10,7 +10,7 @@ export const getCityDistance = (map: MapDefinition, startCityId: number, endCity while (queue.length > 0) { const [currentId, dist] = queue.shift()!; - const cityDef = map.cities.find(c => c.id === currentId); + const cityDef = map.cities.find((c) => c.id === currentId); if (!cityDef) continue; for (const neighborId of cityDef.connections) { @@ -26,3 +26,32 @@ export const getCityDistance = (map: MapDefinition, startCityId: number, endCity return Infinity; }; + +export const searchDistance = (map: MapDefinition, startCityId: number, range: number): Record => { + const result: Record = {}; + const visited = new Set(); + const queue: [number, number][] = [[startCityId, 0]]; + + visited.add(startCityId); + result[startCityId] = 0; + + while (queue.length > 0) { + const [currentId, dist] = queue.shift()!; + + if (dist >= range) continue; + + const cityDef = map.cities.find((c) => c.id === currentId); + if (!cityDef) continue; + + for (const neighborId of cityDef.connections) { + if (!visited.has(neighborId)) { + visited.add(neighborId); + const newDist = dist + 1; + result[neighborId] = newDist; + queue.push([neighborId, newDist]); + } + } + } + + return result; +}; diff --git a/packages/logic/test/scenarios/general/che_NPC능동.test.ts b/packages/logic/test/scenarios/general/che_NPC능동.test.ts index 05062ca..e0b67ac 100644 --- a/packages/logic/test/scenarios/general/che_NPC능동.test.ts +++ b/packages/logic/test/scenarios/general/che_NPC능동.test.ts @@ -111,8 +111,8 @@ describe('che_NPC능동', () => { const runner = new TestGameRunner(world, 200, 1); // Add NPC General manually to snapshot - const cityId = MINIMAL_MAP.cities[0].id; - const destCityId = MINIMAL_MAP.cities[1].id; + const cityId = MINIMAL_MAP.cities[0]!.id; + const destCityId = MINIMAL_MAP.cities[1]!.id; const general: General = { id: 1, @@ -144,7 +144,7 @@ describe('che_NPC능동', () => { }; world.snapshot.generals.push(general); - const outcome = await runner.runTurn([ + await runner.runTurn([ { generalId: general.id, commandKey: 'che_NPC능동', @@ -166,8 +166,8 @@ describe('che_NPC능동', () => { }); const world = new InMemoryWorld(bootstrapResult.snapshot); - const cityId = MINIMAL_MAP.cities[0].id; - const destCityId = MINIMAL_MAP.cities[1].id; + const cityId = MINIMAL_MAP.cities[0]!.id; + const destCityId = MINIMAL_MAP.cities[1]!.id; const general: General = { id: 1, diff --git a/packages/logic/test/world/distance.test.ts b/packages/logic/test/world/distance.test.ts new file mode 100644 index 0000000..2a79dec --- /dev/null +++ b/packages/logic/test/world/distance.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from 'vitest'; +import { getCityDistance, searchDistance } from '@sammo-ts/logic/world/distance.js'; +import type { MapDefinition } from '@sammo-ts/logic/world/types.js'; + +describe('World Distance', () => { + const mockMap: MapDefinition = { + id: 'test_map', + name: 'Test Map', + cities: [ + { id: 1, connections: [2, 3] }, + { id: 2, connections: [1, 4] }, + { id: 3, connections: [1, 5] }, + { id: 4, connections: [2, 6] }, + { id: 5, connections: [3, 7] }, + { id: 6, connections: [4] }, + { id: 7, connections: [5] }, + // Isolated city + { id: 8, connections: [] }, + ] as any[], + }; + + describe('getCityDistance', () => { + it('should return 0 for same city', () => { + expect(getCityDistance(mockMap, 1, 1)).to.equal(0); + }); + + it('should return 1 for adjacent cities', () => { + expect(getCityDistance(mockMap, 1, 2)).to.equal(1); + expect(getCityDistance(mockMap, 1, 3)).to.equal(1); + }); + + it('should return correct distance for distant cities', () => { + expect(getCityDistance(mockMap, 1, 4)).to.equal(2); // 1-2-4 + expect(getCityDistance(mockMap, 1, 6)).to.equal(3); // 1-2-4-6 + }); + + it('should return Infinity for unreachable cities', () => { + expect(getCityDistance(mockMap, 1, 8)).to.equal(Infinity); + }); + }); + + describe('searchDistance', () => { + it('should include start city with distance 0', () => { + const result = searchDistance(mockMap, 1, 0); + expect(result).to.deep.equal({ 1: 0 }); + }); + + it('should find cities within range 1', () => { + const result = searchDistance(mockMap, 1, 1); + expect(result).to.deep.equal({ + 1: 0, + 2: 1, + 3: 1, + }); + }); + + it('should find cities within range 2', () => { + const result = searchDistance(mockMap, 1, 2); + expect(result).to.deep.equal({ + 1: 0, + 2: 1, + 3: 1, + 4: 2, + 5: 2, + }); + }); + + it('should not include unreachable cities', () => { + const result = searchDistance(mockMap, 1, 10); + expect(result).not.to.have.property('8'); + }); + }); +});