feat: 새로운 제약 조건 추가 및 기존 제약 조건 개선

This commit is contained in:
2026-02-06 19:06:38 +00:00
parent 15730feafe
commit a30bc93d10
6 changed files with 165 additions and 138 deletions
@@ -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<TriggerState, ReduceCityArgs, ReduceCityResolveContext<TriggerState>> {
@@ -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', '더이상 감축할 수 없습니다.'),
];
}
@@ -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<TriggerState, MaterialAidArgs, MaterialAidResolveContext<TriggerState>> {
@@ -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, '상대국이 외교제한중입니다.'),
];
}
@@ -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<TriggerState, ExpandCityArgs, ExpandCityResolveContext<TriggerState>> {
@@ -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' };
},
},
];
}
@@ -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: '평시에만 가능합니다.',
}),
];
@@ -92,6 +92,11 @@ export const disallowDiplomacyBetweenStatus = (disallowList: Record<number, stri
},
});
export const disallowDiplomacyStatus = (disallowList: Record<number, string>): Constraint => ({
...disallowDiplomacyBetweenStatus(disallowList),
name: 'disallowDiplomacyStatus',
});
export const allowDiplomacyBetweenStatus = (allowList: number[], reason: string): Constraint => ({
name: 'allowDiplomacyBetweenStatus',
requires: (ctx) => {
+2
View File
@@ -310,6 +310,8 @@ const ALWAYS_FAIL_TS_ALIASES = new Set([
'reqfuturetreatyterm',
'reqvalidstrategiccommandtype',
'hasroutetodestcity',
'requirecapitalcity',
'reqaidwithinlimit',
]);
const canonicalizeConstraintName = (side, normalizedName) => {