구현
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.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 { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||||
import type {
|
import type {
|
||||||
GeneralActionOutcome,
|
GeneralActionOutcome,
|
||||||
@@ -104,7 +104,7 @@ export class ActionDefinition<
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildConstraints(ctx: ConstraintContext, args: NPCSelfArgs): Constraint[] {
|
buildConstraints(_ctx: ConstraintContext, args: NPCSelfArgs): Constraint[] {
|
||||||
const constraints = [mustBeNPC()];
|
const constraints = [mustBeNPC()];
|
||||||
|
|
||||||
if (args.optionText === '순간이동') {
|
if (args.optionText === '순간이동') {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
nearCity,
|
nearCity,
|
||||||
reqGeneralGold,
|
reqGeneralGold,
|
||||||
reqGeneralRice,
|
reqGeneralRice,
|
||||||
unknownOrDeny,
|
|
||||||
existsDestCity,
|
existsDestCity,
|
||||||
} from '@sammo-ts/logic/constraints/presets.js';
|
} from '@sammo-ts/logic/constraints/presets.js';
|
||||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.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 type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||||
import {
|
import {
|
||||||
allow,
|
// allow,
|
||||||
notBeNeutral,
|
notBeNeutral,
|
||||||
notWanderingNation,
|
notWanderingNation,
|
||||||
notCapital,
|
notCapital,
|
||||||
readMetaNumberFromUnknown,
|
readMetaNumberFromUnknown,
|
||||||
unknownOrDeny,
|
// unknownOrDeny,
|
||||||
} from '@sammo-ts/logic/constraints/presets.js';
|
} from '@sammo-ts/logic/constraints/presets.js';
|
||||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||||
import type {
|
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 { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||||
import type { GeneralTurnCommandSpec } from './index.js';
|
import type { GeneralTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export interface ReturnArgs {}
|
export interface ReturnArgs { }
|
||||||
|
|
||||||
const ACTION_NAME = '귀환';
|
const ACTION_NAME = '귀환';
|
||||||
const ACTION_KEY = 'che_귀환';
|
const ACTION_KEY = 'che_귀환';
|
||||||
@@ -100,7 +100,7 @@ export class ActionResolver<
|
|||||||
}
|
}
|
||||||
|
|
||||||
const josaRo = JosaUtil.pick(destCityName, '로');
|
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</>
|
// Legacy: <1>$date</>
|
||||||
// LogFormat.MONTH handles date prefix usually. LogFormat.HM handles HH:MM.
|
// LogFormat.MONTH handles date prefix usually. LogFormat.HM handles HH:MM.
|
||||||
// Legacy output: "StartCity로 귀환했습니다. 10:00"
|
// Legacy output: "StartCity로 귀환했습니다. 10:00"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { TurnCommandModule, TurnCommandSpecBase } from '@sammo-ts/logic/act
|
|||||||
export const GENERAL_TURN_COMMAND_KEYS = [
|
export const GENERAL_TURN_COMMAND_KEYS = [
|
||||||
'che_거병',
|
'che_거병',
|
||||||
'che_임관',
|
'che_임관',
|
||||||
'che_랜덤임관',
|
// '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_임관: async () => import('./che_임관.js'),
|
che_임관: async () => import('./che_임관.js'),
|
||||||
che_등용수락: () => 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'),
|
che_건국: async () => import('./che_건국.js'),
|
||||||
che_훈련: async () => import('./che_훈련.js'),
|
che_훈련: async () => import('./che_훈련.js'),
|
||||||
|
|||||||
@@ -541,11 +541,11 @@ export const notCapital = (checkCurrentCity = false): Constraint => ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Need nation to know capital
|
// Need nation to know capital
|
||||||
reqs.push({ kind: 'nation', id: ctx.nationId });
|
reqs.push({ kind: 'nation', id: ctx.nationId ?? 0 });
|
||||||
return reqs;
|
return reqs;
|
||||||
},
|
},
|
||||||
test: (ctx, view) => {
|
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], '국가 정보가 없습니다.');
|
if (!view.has(nationReq)) return unknownOrDeny(ctx, [nationReq], '국가 정보가 없습니다.');
|
||||||
const nation = view.get(nationReq) as Nation | null;
|
const nation = view.get(nationReq) as Nation | null;
|
||||||
if (!nation) return unknownOrDeny(ctx, [nationReq], '국가 정보가 없습니다.');
|
if (!nation) return unknownOrDeny(ctx, [nationReq], '국가 정보가 없습니다.');
|
||||||
|
|||||||
@@ -181,11 +181,11 @@ export const existsDestGeneral = (): Constraint => ({
|
|||||||
requires: (ctx) =>
|
requires: (ctx) =>
|
||||||
resolveDestGeneralId(ctx) !== undefined
|
resolveDestGeneralId(ctx) !== undefined
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
kind: 'destGeneral',
|
kind: 'destGeneral',
|
||||||
id: resolveDestGeneralId(ctx) ?? 0,
|
id: resolveDestGeneralId(ctx) ?? 0,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
test: (ctx, view) => {
|
test: (ctx, view) => {
|
||||||
const destGeneralId = resolveDestGeneralId(ctx);
|
const destGeneralId = resolveDestGeneralId(ctx);
|
||||||
@@ -292,8 +292,8 @@ export const mustBeNPC = (): Constraint => ({
|
|||||||
if (!general) {
|
if (!general) {
|
||||||
return unknownOrDeny(ctx, [req], '장수 정보가 없습니다.');
|
return unknownOrDeny(ctx, [req], '장수 정보가 없습니다.');
|
||||||
}
|
}
|
||||||
// Assuming npcState >= 2 means NPC. Need to verify exact logic if possible,
|
// Assuming npcState >= 2 means NPC. Need to verify exact logic if possible,
|
||||||
// but typically 0=human, 1=?, 2=NPC.
|
// but typically 0=human, 1=?, 2=NPC.
|
||||||
// Legacy: $general->getNPC() where 0:User, 1:Virtual User(unused?), 2:NPC ...
|
// Legacy: $general->getNPC() where 0:User, 1:Virtual User(unused?), 2:NPC ...
|
||||||
if (general.npcState >= 2) {
|
if (general.npcState >= 2) {
|
||||||
return allow();
|
return allow();
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const getCityDistance = (map: MapDefinition, startCityId: number, endCity
|
|||||||
while (queue.length > 0) {
|
while (queue.length > 0) {
|
||||||
const [currentId, dist] = queue.shift()!;
|
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;
|
if (!cityDef) continue;
|
||||||
|
|
||||||
for (const neighborId of cityDef.connections) {
|
for (const neighborId of cityDef.connections) {
|
||||||
@@ -26,3 +26,32 @@ export const getCityDistance = (map: MapDefinition, startCityId: number, endCity
|
|||||||
|
|
||||||
return Infinity;
|
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);
|
const runner = new TestGameRunner(world, 200, 1);
|
||||||
|
|
||||||
// Add NPC General manually to snapshot
|
// Add NPC General manually to snapshot
|
||||||
const cityId = MINIMAL_MAP.cities[0].id;
|
const cityId = MINIMAL_MAP.cities[0]!.id;
|
||||||
const destCityId = MINIMAL_MAP.cities[1].id;
|
const destCityId = MINIMAL_MAP.cities[1]!.id;
|
||||||
|
|
||||||
const general: General = {
|
const general: General = {
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -144,7 +144,7 @@ describe('che_NPC능동', () => {
|
|||||||
};
|
};
|
||||||
world.snapshot.generals.push(general);
|
world.snapshot.generals.push(general);
|
||||||
|
|
||||||
const outcome = await runner.runTurn([
|
await runner.runTurn([
|
||||||
{
|
{
|
||||||
generalId: general.id,
|
generalId: general.id,
|
||||||
commandKey: 'che_NPC능동',
|
commandKey: 'che_NPC능동',
|
||||||
@@ -166,8 +166,8 @@ describe('che_NPC능동', () => {
|
|||||||
});
|
});
|
||||||
const world = new InMemoryWorld(bootstrapResult.snapshot);
|
const world = new InMemoryWorld(bootstrapResult.snapshot);
|
||||||
|
|
||||||
const cityId = MINIMAL_MAP.cities[0].id;
|
const cityId = MINIMAL_MAP.cities[0]!.id;
|
||||||
const destCityId = MINIMAL_MAP.cities[1].id;
|
const destCityId = MINIMAL_MAP.cities[1]!.id;
|
||||||
|
|
||||||
const general: General = {
|
const general: General = {
|
||||||
id: 1,
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user