건국 관련 추가 구현
This commit is contained in:
@@ -1,6 +1,14 @@
|
||||
import type { GeneralTriggerState, TriggerValue } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import { beNeutral } from '@sammo-ts/logic/constraints/presets.js';
|
||||
import {
|
||||
beMonarch,
|
||||
beWanderingNation,
|
||||
reqNationGeneralCount,
|
||||
beOpeningPart,
|
||||
beNeutralCity,
|
||||
reqCityLevel,
|
||||
checkNationNameDuplicate,
|
||||
} from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||
import { LogCategory, LogFormat } from '@sammo-ts/logic/logging/types.js';
|
||||
@@ -8,7 +16,11 @@ import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'
|
||||
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
|
||||
export interface FoundingArgs {}
|
||||
export interface FoundingArgs {
|
||||
nationName: string;
|
||||
nationType: string;
|
||||
colorType: number;
|
||||
}
|
||||
|
||||
const ACTION_NAME = '건국';
|
||||
|
||||
@@ -18,18 +30,31 @@ export class ActionDefinition<
|
||||
public readonly key = 'che_건국';
|
||||
public readonly name = ACTION_NAME;
|
||||
|
||||
parseArgs(_raw: unknown): FoundingArgs | null {
|
||||
void _raw;
|
||||
return {};
|
||||
parseArgs(raw: unknown): FoundingArgs | null {
|
||||
if (typeof raw !== 'object' || raw === null) return null;
|
||||
const { nationName, nationType, colorType } = raw as any;
|
||||
if (typeof nationName !== 'string' || !nationName) return null;
|
||||
if (typeof nationType !== 'string' || !nationType) return null;
|
||||
if (typeof colorType !== 'number') return null;
|
||||
|
||||
return { nationName, nationType, colorType };
|
||||
}
|
||||
|
||||
buildConstraints(_ctx: ConstraintContext, _args: FoundingArgs): Constraint[] {
|
||||
return [beNeutral()];
|
||||
buildConstraints(_ctx: ConstraintContext, args: FoundingArgs): Constraint[] {
|
||||
return [
|
||||
beOpeningPart(),
|
||||
beMonarch(),
|
||||
beWanderingNation(),
|
||||
reqNationGeneralCount(2),
|
||||
beNeutralCity(),
|
||||
reqCityLevel([5, 6]), // 소, 중 도시
|
||||
checkNationNameDuplicate(args.nationName),
|
||||
];
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: GeneralActionResolveContext<TriggerState>,
|
||||
_args: FoundingArgs
|
||||
args: FoundingArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
const general = context.general;
|
||||
|
||||
@@ -37,9 +62,10 @@ export class ActionDefinition<
|
||||
general.meta = {
|
||||
...(general.meta as object),
|
||||
founding: true as TriggerValue,
|
||||
foundingArgs: args as unknown as TriggerValue, // Cast to TriggerValue to solve type mismatch
|
||||
};
|
||||
|
||||
context.addLog(`${ACTION_NAME}을 준비했습니다.`, {
|
||||
context.addLog(`${args.nationName} 건국을 준비했습니다.`, {
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
});
|
||||
@@ -54,7 +80,11 @@ export const actionContextBuilder = defaultActionContextBuilder;
|
||||
export const commandSpec: GeneralTurnCommandSpec = {
|
||||
key: 'che_건국',
|
||||
category: '전략',
|
||||
reqArg: false,
|
||||
args: {},
|
||||
reqArg: true,
|
||||
args: {
|
||||
nationName: 'string',
|
||||
nationType: 'string',
|
||||
colorType: 'number',
|
||||
},
|
||||
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { City, General } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { City, General, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { MapDefinition } from '@sammo-ts/logic/world/types.js';
|
||||
import {
|
||||
allow,
|
||||
@@ -368,53 +368,6 @@ export const notSameDestCity = (): Constraint => ({
|
||||
},
|
||||
});
|
||||
|
||||
const buildMapIndex = (map: MapDefinition): Map<number, number[]> => {
|
||||
const index = new Map<number, number[]>();
|
||||
for (const city of map.cities) {
|
||||
index.set(city.id, Array.from(city.connections ?? []));
|
||||
}
|
||||
return index;
|
||||
};
|
||||
|
||||
const hasRouteToDest = (
|
||||
mapIndex: Map<number, number[]>,
|
||||
allowedCityIds: Set<number>,
|
||||
fromCityId: number,
|
||||
toCityId: number
|
||||
): boolean => {
|
||||
if (fromCityId === toCityId) {
|
||||
return true;
|
||||
}
|
||||
if (!allowedCityIds.has(toCityId)) {
|
||||
return false;
|
||||
}
|
||||
const queue: number[] = [fromCityId];
|
||||
const visited = new Set<number>();
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift();
|
||||
if (current === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (visited.has(current)) {
|
||||
continue;
|
||||
}
|
||||
visited.add(current);
|
||||
const neighbors = mapIndex.get(current) ?? [];
|
||||
for (const next of neighbors) {
|
||||
if (!allowedCityIds.has(next)) {
|
||||
continue;
|
||||
}
|
||||
if (next === toCityId) {
|
||||
return true;
|
||||
}
|
||||
if (!visited.has(next)) {
|
||||
queue.push(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const hasRouteWithEnemy = (): Constraint => ({
|
||||
name: 'HasRouteWithEnemy',
|
||||
requires: (ctx) => {
|
||||
@@ -477,15 +430,53 @@ export const hasRouteWithEnemy = (): Constraint => ({
|
||||
if (!allowedCityIds.has(destCity.id)) {
|
||||
return { kind: 'deny', reason: '경로에 도달할 방법이 없습니다.' };
|
||||
}
|
||||
|
||||
const mapIndex = buildMapIndex(map);
|
||||
if (!mapIndex.has(general.cityId)) {
|
||||
return unknownOrDeny(ctx, [], '경로 정보가 없습니다.');
|
||||
}
|
||||
|
||||
if (!hasRouteToDest(mapIndex, allowedCityIds, general.cityId, destCity.id)) {
|
||||
return { kind: 'deny', reason: '경로에 도달할 방법이 없습니다.' };
|
||||
}
|
||||
return allow();
|
||||
},
|
||||
});
|
||||
|
||||
export const beNeutralCity = (): Constraint => ({
|
||||
name: 'BeNeutralCity',
|
||||
requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []),
|
||||
test: (ctx, view) => {
|
||||
const city = readCity(view, ctx.cityId);
|
||||
if (!city) {
|
||||
if (ctx.cityId === undefined) {
|
||||
return unknownOrDeny(ctx, [], '도시 정보가 없습니다.');
|
||||
}
|
||||
const req: RequirementKey = { kind: 'city', id: ctx.cityId };
|
||||
return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.');
|
||||
}
|
||||
if (city.nationId === 0) {
|
||||
return allow();
|
||||
}
|
||||
const general = readGeneral(ctx, view);
|
||||
if (general && city.nationId === general.nationId) {
|
||||
const nationReq: RequirementKey = { kind: 'nation', id: general.nationId };
|
||||
const nation = view.get(nationReq) as Nation | null;
|
||||
if (nation && nation.level === 0) {
|
||||
// 방랑군 본인 도시면 건국 가능
|
||||
return allow();
|
||||
}
|
||||
}
|
||||
return { kind: 'deny', reason: '공백지가 아닙니다.' };
|
||||
},
|
||||
});
|
||||
|
||||
export const reqCityLevel = (levels: number[]): Constraint => ({
|
||||
name: 'ReqCityLevel',
|
||||
requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []),
|
||||
test: (ctx, view) => {
|
||||
const city = readCity(view, ctx.cityId);
|
||||
if (!city) {
|
||||
if (ctx.cityId === undefined) {
|
||||
return unknownOrDeny(ctx, [], '도시 정보가 없습니다.');
|
||||
}
|
||||
const req: RequirementKey = { kind: 'city', id: ctx.cityId };
|
||||
return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.');
|
||||
}
|
||||
if (levels.includes(city.level)) {
|
||||
return allow();
|
||||
}
|
||||
return { kind: 'deny', reason: '규모가 맞지 않습니다.' };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -59,6 +59,25 @@ export const beChief = (): Constraint => ({
|
||||
},
|
||||
});
|
||||
|
||||
export const beMonarch = (): Constraint => ({
|
||||
name: 'BeMonarch',
|
||||
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }],
|
||||
test: (ctx, view) => {
|
||||
const req: RequirementKey = { kind: 'general', id: ctx.actorId };
|
||||
if (!view.has(req)) {
|
||||
return unknownOrDeny(ctx, [req], '장수 정보가 없습니다.');
|
||||
}
|
||||
const general = view.get(req) as General | null;
|
||||
if (!general) {
|
||||
return unknownOrDeny(ctx, [req], '장수 정보가 없습니다.');
|
||||
}
|
||||
if (general.officerLevel === 12) {
|
||||
return allow();
|
||||
}
|
||||
return { kind: 'deny', reason: '군주가 아닙니다.' };
|
||||
},
|
||||
});
|
||||
|
||||
export const reqGeneralGold = (
|
||||
getRequiredGold: (ctx: ConstraintContext, view: StateView) => number,
|
||||
requirements: RequirementKey[] = []
|
||||
|
||||
@@ -17,3 +17,28 @@ export const notOpeningPart = (relYear: number, openingPartYear: number): Constr
|
||||
return { kind: 'deny', reason: '초반 제한 중에는 불가능합니다.' };
|
||||
},
|
||||
});
|
||||
|
||||
export const beOpeningPart = (): Constraint => ({
|
||||
name: 'BeOpeningPart',
|
||||
requires: () => [
|
||||
{ kind: 'env', key: 'world' },
|
||||
{ kind: 'env', key: 'openingPartYear' },
|
||||
],
|
||||
test: (_ctx, view) => {
|
||||
const world = view.get({ kind: 'env', key: 'world' }) as { currentYear: number } | null;
|
||||
const openingPartYear = view.get({ kind: 'env', key: 'openingPartYear' }) as number | null;
|
||||
if (!world || openingPartYear === null) {
|
||||
return {
|
||||
kind: 'unknown',
|
||||
missing: [
|
||||
{ kind: 'env', key: 'world' },
|
||||
{ kind: 'env', key: 'openingPartYear' },
|
||||
],
|
||||
};
|
||||
}
|
||||
if (world.currentYear < openingPartYear) {
|
||||
return allow();
|
||||
}
|
||||
return { kind: 'deny', reason: '초반이 지났습니다.' };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -21,6 +21,25 @@ export const notWanderingNation = (): Constraint => ({
|
||||
},
|
||||
});
|
||||
|
||||
export const beWanderingNation = (): Constraint => ({
|
||||
name: 'BeWanderingNation',
|
||||
requires: (ctx) => (ctx.nationId !== undefined ? [{ kind: 'nation', id: ctx.nationId }] : []),
|
||||
test: (ctx, view) => {
|
||||
const nation = readNation(view, ctx.nationId);
|
||||
if (!nation) {
|
||||
if (ctx.nationId === undefined) {
|
||||
return unknownOrDeny(ctx, [], '국가 정보가 없습니다.');
|
||||
}
|
||||
const req: RequirementKey = { kind: 'nation', id: ctx.nationId };
|
||||
return unknownOrDeny(ctx, [req], '국가 정보가 없습니다.');
|
||||
}
|
||||
if (nation.level === 0) {
|
||||
return allow();
|
||||
}
|
||||
return { kind: 'deny', reason: '방랑군이 아닙니다.' };
|
||||
},
|
||||
});
|
||||
|
||||
export const availableStrategicCommand = (allowTurnCnt = 0): Constraint => ({
|
||||
name: 'AvailableStrategicCommand',
|
||||
requires: (ctx) => {
|
||||
@@ -196,3 +215,60 @@ export const differentDestNation = (): Constraint => ({
|
||||
return allow();
|
||||
},
|
||||
});
|
||||
|
||||
export const reqNationGeneralCount = (min: number): Constraint => ({
|
||||
name: 'ReqNationGeneralCount',
|
||||
requires: (ctx) => {
|
||||
const reqs: RequirementKey[] = [{ kind: 'generalList' }];
|
||||
if (ctx.nationId !== undefined) {
|
||||
reqs.push({ kind: 'nation', id: ctx.nationId });
|
||||
} else {
|
||||
reqs.push({ kind: 'general', id: ctx.actorId });
|
||||
}
|
||||
return reqs;
|
||||
},
|
||||
test: (ctx, view) => {
|
||||
const listReq: RequirementKey = { kind: 'generalList' };
|
||||
if (!view.has(listReq)) {
|
||||
return unknownOrDeny(ctx, [listReq], '장수가 없습니다.');
|
||||
}
|
||||
const generals = view.get(listReq) as General[] | null;
|
||||
if (!generals) {
|
||||
return unknownOrDeny(ctx, [listReq], '장수가 없습니다.');
|
||||
}
|
||||
|
||||
let baseNationId = ctx.nationId;
|
||||
if (baseNationId === undefined) {
|
||||
const general = readGeneral(ctx, view);
|
||||
if (!general) {
|
||||
const req: RequirementKey = {
|
||||
kind: 'general',
|
||||
id: ctx.actorId,
|
||||
};
|
||||
return unknownOrDeny(ctx, [req], '장수가 없습니다.');
|
||||
}
|
||||
baseNationId = general.nationId;
|
||||
}
|
||||
|
||||
const count = generals.filter((g) => g.nationId === baseNationId).length;
|
||||
if (count >= min) {
|
||||
return allow();
|
||||
}
|
||||
return { kind: 'deny', reason: `국가 소속 장수가 부족합니다. (필요: ${min}, 현재: ${count})` };
|
||||
},
|
||||
});
|
||||
|
||||
export const checkNationNameDuplicate = (name: string): Constraint => ({
|
||||
name: 'CheckNationNameDuplicate',
|
||||
requires: () => [{ kind: 'nationList' }],
|
||||
test: (_ctx, view) => {
|
||||
const nations = view.get({ kind: 'nationList' }) as Nation[] | null;
|
||||
if (!nations) {
|
||||
return { kind: 'unknown', missing: [{ kind: 'nationList' }] };
|
||||
}
|
||||
if (nations.some((n) => n.name === name)) {
|
||||
return { kind: 'deny', reason: '이미 존재하는 국가 이름입니다.' };
|
||||
}
|
||||
return allow();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ export type RequirementKey =
|
||||
| { kind: 'destNation'; id: number }
|
||||
| { kind: 'diplomacy'; srcNationId: number; destNationId: number }
|
||||
| { kind: 'diplomacyList' }
|
||||
| { kind: 'nationList' }
|
||||
| { kind: 'arg'; key: string }
|
||||
| { kind: 'env'; key: string };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user