This commit is contained in:
2026-01-11 10:35:12 +00:00
parent 373ddd863d
commit 511aade81c
9 changed files with 127 additions and 25 deletions
@@ -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 === '순간이동') {
@@ -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';
@@ -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"
@@ -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<GeneralTurnCommandKey, GeneralTurnCommandImporter
che_거병: async () => 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'),
+2 -2
View File
@@ -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], '국가 정보가 없습니다.');
+7 -7
View File
@@ -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();
+30 -1
View File
@@ -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<number, number> => {
const result: Record<number, number> = {};
const visited = new Set<number>();
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;
};
@@ -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,
@@ -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');
});
});
});