From a30bc93d10f1669f8399d7e6575b6f647799cba3 Mon Sep 17 00:00:00 2001 From: Hide_D Date: Fri, 6 Feb 2026 19:06:38 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=83=88=EB=A1=9C=EC=9A=B4=20=EC=A0=9C?= =?UTF-8?q?=EC=95=BD=20=EC=A1=B0=EA=B1=B4=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20=EA=B8=B0=EC=A1=B4=20=EC=A0=9C=EC=95=BD=20=EC=A1=B0=EA=B1=B4?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../logic/src/actions/turn/nation/che_감축.ts | 86 ++++++++++++++----- .../src/actions/turn/nation/che_물자원조.ts | 79 +++++++---------- .../logic/src/actions/turn/nation/che_증축.ts | 73 +++++++++++----- .../src/actions/turn/nation/che_초토화.ts | 58 +++---------- packages/logic/src/constraints/diplomacy.ts | 5 ++ tools/compare-command-constraints.mjs | 2 + 6 files changed, 165 insertions(+), 138 deletions(-) diff --git a/packages/logic/src/actions/turn/nation/che_감축.ts b/packages/logic/src/actions/turn/nation/che_감축.ts index 53a97b1..6da5d51 100644 --- a/packages/logic/src/actions/turn/nation/che_감축.ts +++ b/packages/logic/src/actions/turn/nation/che_감축.ts @@ -1,6 +1,6 @@ import type { City, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js'; import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js'; -import { beChief, occupiedCity, suppliedCity } from '@sammo-ts/logic/constraints/presets.js'; +import { beChief, notBeNeutral, occupiedCity, suppliedCity } from '@sammo-ts/logic/constraints/presets.js'; import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; import type { GeneralActionEffect, @@ -13,6 +13,7 @@ import { JosaUtil } from '@sammo-ts/common'; import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'; import type { NationTurnCommandSpec } from './index.js'; import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js'; +import type { MapDefinition } from '@sammo-ts/logic/world/types.js'; export interface ReduceCityArgs {} @@ -31,6 +32,64 @@ const DEFAULT_COST = 60000; const COST_COEF = 500; const MIN_POP = 30000; +const requireCapitalCity = (reason: string): Constraint => ({ + name: 'requireCapitalCity', + requires: (ctx) => (ctx.nationId !== undefined ? [{ kind: 'nation', id: ctx.nationId }] : []), + test: (ctx: ConstraintContext, view: StateView) => { + if (ctx.nationId === undefined) { + return { kind: 'deny', reason }; + } + const nation = view.get({ kind: 'nation', id: ctx.nationId }) as Nation | undefined; + if (!nation || !nation.capitalCityId) { + return { kind: 'deny', reason }; + } + return { kind: 'allow' }; + }, +}); + +const reqDestCityValue = ( + comp: '>' | '<' | '>=' | '<=', + required: number | 'origin', + reason: string +): Constraint => ({ + name: 'reqDestCityValue', + requires: (ctx) => + ctx.nationId !== undefined ? [{ kind: 'nation', id: ctx.nationId }, { kind: 'env', key: 'map' }] : [], + test: (ctx: ConstraintContext, view: StateView) => { + if (ctx.nationId === undefined) { + return { kind: 'deny', reason }; + } + const nation = view.get({ kind: 'nation', id: ctx.nationId }) as Nation | undefined; + if (!nation || !nation.capitalCityId) { + return { kind: 'deny', reason: '방랑상태에서는 불가능합니다.' }; + } + const city = view.get({ kind: 'city', id: nation.capitalCityId }) as City | undefined; + if (!city) { + return { kind: 'deny', reason: '수도 정보를 찾을 수 없습니다.' }; + } + const compareTarget = + required === 'origin' + ? ((view.get({ kind: 'env', key: 'map' }) as MapDefinition | undefined)?.cities.find( + (mapCity) => mapCity.id === nation.capitalCityId + )?.level ?? + 0) + : required; + const level = city.level; + const allow = + comp === '>' + ? level > compareTarget + : comp === '<' + ? level < compareTarget + : comp === '>=' + ? level >= compareTarget + : level <= compareTarget; + if (allow) { + return { kind: 'allow' }; + } + return { kind: 'deny', reason }; + }, +}); + export class ActionDefinition< TriggerState extends GeneralTriggerState = GeneralTriggerState, > implements GeneralActionDefinition> { @@ -44,7 +103,7 @@ export class ActionDefinition< } buildMinConstraints(_ctx: ConstraintContext, _args: ReduceCityArgs): Constraint[] { - return [occupiedCity(), beChief(), suppliedCity()]; + return [notBeNeutral()]; } private getRecoverAmount(): number { @@ -53,28 +112,13 @@ export class ActionDefinition< buildConstraints(_ctx: ConstraintContext, _args: ReduceCityArgs): Constraint[] { return [ + notBeNeutral(), occupiedCity(), beChief(), suppliedCity(), - { - name: 'reducibleCity', - requires: (ctx) => [ - { kind: 'nation', id: ctx.nationId! }, - { kind: 'city', id: 0 }, - ], - test: (ctx: ConstraintContext, view: StateView) => { - const nation = view.get({ kind: 'nation', id: ctx.nationId! }) as Nation | undefined; - const capitalCityId = nation?.capitalCityId; - - if (!capitalCityId) return { kind: 'deny', reason: '방랑상태에서는 불가능합니다.' }; - - const capitalCity = view.get({ kind: 'city', id: capitalCityId }) as City | undefined; - if (!capitalCity) return { kind: 'deny', reason: '수도 정보를 찾을 수 없습니다.' }; - if (capitalCity.level <= 4) return { kind: 'deny', reason: '더이상 감축할 수 없습니다.' }; - - return { kind: 'allow' }; - }, - }, + requireCapitalCity('방랑상태에서는 불가능합니다.'), + reqDestCityValue('>', 4, '더이상 감축할 수 없습니다.'), + reqDestCityValue('>', 'origin', '더이상 감축할 수 없습니다.'), ]; } diff --git a/packages/logic/src/actions/turn/nation/che_물자원조.ts b/packages/logic/src/actions/turn/nation/che_물자원조.ts index 91d5d93..3f6ccc4 100644 --- a/packages/logic/src/actions/turn/nation/che_물자원조.ts +++ b/packages/logic/src/actions/turn/nation/che_물자원조.ts @@ -6,6 +6,10 @@ import { suppliedCity, differentDestNation, existsDestNation, + reqDestNationValue, + reqNationGold, + reqNationRice, + reqNationValue, } from '@sammo-ts/logic/constraints/presets.js'; import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; import type { @@ -39,6 +43,25 @@ const ACTION_NAME = '원조'; const COEF_AID_AMOUNT = 10000; const POST_REQ_TURN = 12; +const reqAidWithinLimit = (goldAmount: number, riceAmount: number): Constraint => ({ + name: 'reqAidWithinLimit', + requires: (ctx) => (ctx.nationId !== undefined ? [{ kind: 'nation', id: ctx.nationId }] : []), + test: (ctx: ConstraintContext, view: StateView) => { + if (ctx.nationId === undefined) { + return { kind: 'deny', reason: '국가 정보가 없습니다.' }; + } + const nation = view.get({ kind: 'nation', id: ctx.nationId }) as Nation | undefined; + if (!nation) { + return { kind: 'deny', reason: '국가 정보가 없습니다.' }; + } + const limit = nation.level * COEF_AID_AMOUNT; + if (goldAmount > limit || riceAmount > limit) { + return { kind: 'deny', reason: '작위 제한량 이상은 보낼 수 없습니다.' }; + } + return { kind: 'allow' }; + }, +}); + export class ActionDefinition< TriggerState extends GeneralTriggerState = GeneralTriggerState, > implements GeneralActionDefinition> { @@ -52,22 +75,7 @@ export class ActionDefinition< } buildMinConstraints(_ctx: ConstraintContext, _args: MaterialAidArgs): Constraint[] { - return [ - occupiedCity(), - beChief(), - suppliedCity(), - { - name: 'nationSurlimit', - requires: (ctx) => [{ kind: 'nation', id: ctx.nationId! }], - test: (_ctx: ConstraintContext, view: StateView) => { - const nation = view.get({ kind: 'nation', id: _ctx.nationId! }) as Nation | undefined; - const surlimitRaw = nation?.meta.surlimit; - const surlimit = typeof surlimitRaw === 'number' ? surlimitRaw : 0; - if (surlimit > 0) return { kind: 'deny', reason: '외교제한중입니다.' }; - return { kind: 'allow' }; - }, - }, - ]; + return [occupiedCity(), beChief(), suppliedCity(), reqNationValue('surlimit', '외교제한', '==', 0, '외교제한중입니다.')]; } buildConstraints(_ctx: ConstraintContext, args: MaterialAidArgs): Constraint[] { @@ -78,40 +86,11 @@ export class ActionDefinition< suppliedCity(), existsDestNation(), differentDestNation(), - { - name: 'aidLimit', - requires: (ctx) => [{ kind: 'nation', id: ctx.nationId! }], - test: (ctx: ConstraintContext, view: StateView) => { - const nation = view.get({ kind: 'nation', id: ctx.nationId! }) as Nation | undefined; - const limit = (nation?.level ?? 1) * COEF_AID_AMOUNT; - if (goldAmount > limit || riceAmount > limit) { - return { kind: 'deny', reason: '작위 제한량 이상은 보낼 수 없습니다.' }; - } - return { kind: 'allow' }; - }, - }, - { - name: 'nationSurlimit', - requires: (ctx) => [{ kind: 'nation', id: ctx.nationId! }], - test: (_ctx: ConstraintContext, view: StateView) => { - const nation = view.get({ kind: 'nation', id: _ctx.nationId! }) as Nation | undefined; - const surlimitRaw = nation?.meta.surlimit; - const surlimit = typeof surlimitRaw === 'number' ? surlimitRaw : 0; - if (surlimit > 0) return { kind: 'deny', reason: '외교제한중입니다.' }; - return { kind: 'allow' }; - }, - }, - { - name: 'destNationSurlimit', - requires: () => [{ kind: 'nation', id: args.destNationId }], - test: (_ctx: ConstraintContext, view: StateView) => { - const destNation = view.get({ kind: 'nation', id: args.destNationId }) as Nation | undefined; - const surlimitRaw = destNation?.meta.surlimit; - const surlimit = typeof surlimitRaw === 'number' ? surlimitRaw : 0; - if (surlimit > 0) return { kind: 'deny', reason: '상대국이 외교제한중입니다.' }; - return { kind: 'allow' }; - }, - }, + reqAidWithinLimit(goldAmount, riceAmount), + reqNationGold(() => this.env.baseGold + (goldAmount > 0 ? 1 : 0)), + reqNationRice(() => this.env.baseRice + (riceAmount > 0 ? 1 : 0)), + reqNationValue('surlimit', '외교제한', '==', 0, '외교제한중입니다.'), + reqDestNationValue('surlimit', '외교제한', '==', 0, '상대국이 외교제한중입니다.'), ]; } diff --git a/packages/logic/src/actions/turn/nation/che_증축.ts b/packages/logic/src/actions/turn/nation/che_증축.ts index 15f6823..d7525de 100644 --- a/packages/logic/src/actions/turn/nation/che_증축.ts +++ b/packages/logic/src/actions/turn/nation/che_증축.ts @@ -2,6 +2,7 @@ import type { City, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/e import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js'; import { beChief, + notBeNeutral, occupiedCity, suppliedCity, reqNationGold, @@ -36,6 +37,52 @@ const WALL_INCREASE = 2000; const DEFAULT_COST = 60000; const COST_COEF = 500; +const requireCapitalCity = (reason: string): Constraint => ({ + name: 'requireCapitalCity', + requires: (ctx) => (ctx.nationId !== undefined ? [{ kind: 'nation', id: ctx.nationId }] : []), + test: (ctx: ConstraintContext, view: StateView) => { + if (ctx.nationId === undefined) { + return { kind: 'deny', reason }; + } + const nation = view.get({ kind: 'nation', id: ctx.nationId }) as Nation | undefined; + if (!nation || !nation.capitalCityId) { + return { kind: 'deny', reason }; + } + return { kind: 'allow' }; + }, +}); + +const reqDestCityValue = (comp: '>' | '<' | '>=' | '<=', required: number, reason: string): Constraint => ({ + name: 'reqDestCityValue', + requires: (ctx) => (ctx.nationId !== undefined ? [{ kind: 'nation', id: ctx.nationId }] : []), + test: (ctx: ConstraintContext, view: StateView) => { + if (ctx.nationId === undefined) { + return { kind: 'deny', reason }; + } + const nation = view.get({ kind: 'nation', id: ctx.nationId }) as Nation | undefined; + if (!nation || !nation.capitalCityId) { + return { kind: 'deny', reason: '방랑상태에서는 불가능합니다.' }; + } + const city = view.get({ kind: 'city', id: nation.capitalCityId }) as City | undefined; + if (!city) { + return { kind: 'deny', reason: '수도 정보를 찾을 수 없습니다.' }; + } + const level = city.level; + const allow = + comp === '>' + ? level > required + : comp === '<' + ? level < required + : comp === '>=' + ? level >= required + : level <= required; + if (allow) { + return { kind: 'allow' }; + } + return { kind: 'deny', reason }; + }, +}); + export class ActionDefinition< TriggerState extends GeneralTriggerState = GeneralTriggerState, > implements GeneralActionDefinition> { @@ -49,7 +96,7 @@ export class ActionDefinition< } buildMinConstraints(_ctx: ConstraintContext, _args: ExpandCityArgs): Constraint[] { - return [occupiedCity(), beChief(), suppliedCity()]; + return [notBeNeutral()]; } private getCost(): number { @@ -59,31 +106,15 @@ export class ActionDefinition< buildConstraints(_ctx: ConstraintContext, _args: ExpandCityArgs): Constraint[] { const cost = this.getCost(); return [ + notBeNeutral(), occupiedCity(), beChief(), suppliedCity(), + requireCapitalCity('방랑상태에서는 불가능합니다.'), + reqDestCityValue('>', 3, '수진, 진, 관문에서는 불가능합니다.'), + reqDestCityValue('<', 8, '더이상 증축할 수 없습니다.'), reqNationGold(() => this.env.baseGold + cost), reqNationRice(() => this.env.baseRice + cost), - { - name: 'expandableCity', - requires: (ctx) => [ - { kind: 'nation', id: ctx.nationId! }, - { kind: 'city', id: 0 }, - ], - test: (ctx: ConstraintContext, view: StateView) => { - const nation = view.get({ kind: 'nation', id: ctx.nationId! }) as Nation | undefined; - const capitalCityId = nation?.capitalCityId; - - if (!capitalCityId) return { kind: 'deny', reason: '방랑상태에서는 불가능합니다.' }; - - const capitalCity = view.get({ kind: 'city', id: capitalCityId }) as City | undefined; - if (!capitalCity) return { kind: 'deny', reason: '수도 정보를 찾을 수 없습니다.' }; - if (capitalCity.level <= 3) return { kind: 'deny', reason: '수진, 진, 관문에서는 불가능합니다.' }; - if (capitalCity.level >= 8) return { kind: 'deny', reason: '더이상 증축할 수 없습니다.' }; - - return { kind: 'allow' }; - }, - }, ]; } diff --git a/packages/logic/src/actions/turn/nation/che_초토화.ts b/packages/logic/src/actions/turn/nation/che_초토화.ts index c381834..efde0ad 100644 --- a/packages/logic/src/actions/turn/nation/che_초토화.ts +++ b/packages/logic/src/actions/turn/nation/che_초토화.ts @@ -1,14 +1,14 @@ import type { City, GeneralTriggerState, Nation, TriggerValue } from '@sammo-ts/logic/domain/entities.js'; -import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js'; +import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js'; import { beChief, - disallowDiplomacyBetweenStatus, + disallowDiplomacyStatus, occupiedCity, occupiedDestCity, + reqNationValue, suppliedCity, suppliedDestCity, } from '@sammo-ts/logic/constraints/presets.js'; -import { allow, unknownOrDeny } from '@sammo-ts/logic/constraints/helpers.js'; import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js'; import type { GeneralActionEffect, @@ -43,45 +43,6 @@ const ACTION_NAME = '초토화'; const PRE_REQ_TURN = 2; const POST_REQ_TURN = 24; -const requireNoDiplomacyLimit = (): Constraint => ({ - name: 'requireNoDiplomacyLimit', - requires: (ctx) => (ctx.nationId !== undefined ? [{ kind: 'nation', id: ctx.nationId }] : []), - test: (ctx: ConstraintContext, view: StateView) => { - const nationId = ctx.nationId; - if (nationId === undefined) { - return unknownOrDeny(ctx, [], '국가 정보가 없습니다.'); - } - const nation = view.get({ kind: 'nation', id: nationId }) as Nation | null; - if (!nation) { - return unknownOrDeny(ctx, [{ kind: 'nation', id: nationId }], '국가 정보가 없습니다.'); - } - const surlimit = typeof nation.meta?.surlimit === 'number' ? Number(nation.meta.surlimit) : 0; - if (surlimit > 0) { - return { kind: 'deny', reason: '외교제한 턴이 남아있습니다.' }; - } - return allow(); - }, -}); - -const notCapitalCity = (destCityId: number): Constraint => ({ - name: 'notCapitalCity', - requires: (ctx) => (ctx.nationId !== undefined ? [{ kind: 'nation', id: ctx.nationId }] : []), - test: (ctx: ConstraintContext, view: StateView) => { - const nationId = ctx.nationId; - if (nationId === undefined) { - return unknownOrDeny(ctx, [], '국가 정보가 없습니다.'); - } - const nation = view.get({ kind: 'nation', id: nationId }) as Nation | null; - if (!nation) { - return unknownOrDeny(ctx, [{ kind: 'nation', id: nationId }], '국가 정보가 없습니다.'); - } - if (nation.capitalCityId === destCityId) { - return { kind: 'deny', reason: '수도입니다.' }; - } - return allow(); - }, -}); - const calcReturnAmount = (destCity: City): number => { let amount = destCity.population / 5; const resourcePairs: Array<[number, number]> = [ @@ -117,7 +78,12 @@ export class ActionDefinition< } buildMinConstraints(_ctx: ConstraintContext, _args: ScorchedEarthArgs): Constraint[] { - return [occupiedCity(), beChief(), suppliedCity(), requireNoDiplomacyLimit()]; + return [ + occupiedCity(), + beChief(), + suppliedCity(), + reqNationValue('surlimit', '제한 턴', '==', 0, '외교제한 턴이 남아있습니다.'), + ]; } buildConstraints(_ctx: ConstraintContext, args: ScorchedEarthArgs): Constraint[] { @@ -128,9 +94,9 @@ export class ActionDefinition< beChief(), suppliedCity(), suppliedDestCity(), - notCapitalCity(args.destCityId), - requireNoDiplomacyLimit(), - disallowDiplomacyBetweenStatus({ + reqNationValue('capitalCityId', '수도', '!=', args.destCityId, '수도입니다.'), + reqNationValue('surlimit', '제한 턴', '==', 0, '외교제한 턴이 남아있습니다.'), + disallowDiplomacyStatus({ 0: '평시에만 가능합니다.', }), ]; diff --git a/packages/logic/src/constraints/diplomacy.ts b/packages/logic/src/constraints/diplomacy.ts index 199c6a4..09b0a79 100644 --- a/packages/logic/src/constraints/diplomacy.ts +++ b/packages/logic/src/constraints/diplomacy.ts @@ -92,6 +92,11 @@ export const disallowDiplomacyBetweenStatus = (disallowList: Record): Constraint => ({ + ...disallowDiplomacyBetweenStatus(disallowList), + name: 'disallowDiplomacyStatus', +}); + export const allowDiplomacyBetweenStatus = (allowList: number[], reason: string): Constraint => ({ name: 'allowDiplomacyBetweenStatus', requires: (ctx) => { diff --git a/tools/compare-command-constraints.mjs b/tools/compare-command-constraints.mjs index cc6f714..bea5132 100644 --- a/tools/compare-command-constraints.mjs +++ b/tools/compare-command-constraints.mjs @@ -310,6 +310,8 @@ const ALWAYS_FAIL_TS_ALIASES = new Set([ 'reqfuturetreatyterm', 'reqvalidstrategiccommandtype', 'hasroutetodestcity', + 'requirecapitalcity', + 'reqaidwithinlimit', ]); const canonicalizeConstraintName = (side, normalizedName) => {