feat: 여러 커맨드의 제약 조건 개선 및 새로운 제약 조건 추가

This commit is contained in:
2026-02-06 18:26:21 +00:00
parent e863c41c07
commit d7cb8c77e3
20 changed files with 230 additions and 42 deletions
@@ -5,7 +5,6 @@ import {
nearCity,
reqGeneralGold,
reqGeneralRice,
existsDestCity,
} from '@sammo-ts/logic/constraints/presets.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
@@ -173,7 +172,6 @@ export class ActionDefinition<
buildConstraints(ctx: ConstraintContext, _args: ForcedMoveArgs): Constraint[] {
return [
existsDestCity(),
notSameDestCity(),
nearCity(3),
reqGeneralGold((_c, _v) => {
@@ -1,6 +1,6 @@
import type { GeneralTriggerState, Nation } 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 { allowJoinAction, beNeutral, beOpeningPart, noPenalty } 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 {
@@ -33,7 +33,7 @@ export class ActionDefinition<
}
buildConstraints(_ctx: ConstraintContext, _args: UprisingArgs): Constraint[] {
return [beNeutral()];
return [beNeutral(), beOpeningPart(), allowJoinAction(), noPenalty('noFoundNation')];
}
resolve(
@@ -1,6 +1,12 @@
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import { occupiedCity, reqGeneralGold, reqGeneralRice, suppliedCity } from '@sammo-ts/logic/constraints/presets.js';
import {
occupiedCity,
reqCityTrader,
reqGeneralGold,
reqGeneralRice,
suppliedCity,
} 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 { LogFormat } from '@sammo-ts/logic/logging/types.js';
@@ -39,11 +45,11 @@ export class ActionDefinition<
}
buildMinConstraints(_ctx: ConstraintContext, _args: TradeArgs): Constraint[] {
return [occupiedCity(), suppliedCity()];
return [reqCityTrader(), occupiedCity({ allowNeutral: true }), suppliedCity()];
}
buildConstraints(_ctx: ConstraintContext, args: TradeArgs): Constraint[] {
const constraints: Constraint[] = [occupiedCity(), suppliedCity()];
const constraints: Constraint[] = [reqCityTrader(), occupiedCity({ allowNeutral: true }), suppliedCity()];
if (args.buyRice) {
constraints.push(reqGeneralGold(() => 1));
} else {
@@ -5,6 +5,7 @@ import {
notWanderingNation,
occupiedCity,
reqGeneralGold,
reqGeneralRice,
suppliedCity,
} from '@sammo-ts/logic/constraints/presets.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
@@ -52,7 +53,14 @@ export class ActionDefinition<
buildConstraints(_ctx: ConstraintContext, _args: TechResearchArgs): Constraint[] {
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.costGold ?? 0;
return [notBeNeutral(), notWanderingNation(), occupiedCity(), suppliedCity(), reqGeneralGold(getRequiredGold)];
return [
notBeNeutral(),
notWanderingNation(),
occupiedCity(),
suppliedCity(),
reqGeneralGold(getRequiredGold),
reqGeneralRice(() => 0),
];
}
resolve(
@@ -15,8 +15,8 @@ const ACTION_NAME = '내정 특기 초기화';
const hasSpecial = (value: string | null | undefined): boolean =>
value !== null && value !== undefined && value !== 'None';
const reqDomesticSpecial = (): Constraint => ({
name: 'ReqGeneralDomesticSpecial',
const reqGeneralValue = (): Constraint => ({
name: 'reqGeneralValue',
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }],
test: (ctx, view) => {
const general = readGeneral(ctx, view);
@@ -42,8 +42,12 @@ export class ActionDefinition<
return {};
}
buildMinConstraints(_ctx: ConstraintContext, _args: ResetSpecialDomesticArgs): Constraint[] {
return [reqGeneralValue()];
}
buildConstraints(_ctx: ConstraintContext, _args: ResetSpecialDomesticArgs): Constraint[] {
return [reqDomesticSpecial()];
return [reqGeneralValue()];
}
resolve(
@@ -62,8 +62,8 @@ const pickByWeight = <T extends string>(rng: DrillContext['rng'], weights: Recor
return last ? last[0] : first[0];
};
const reqGeneralStat = (key: 'train' | 'atmos', label: string, minValue: number): Constraint => ({
name: `ReqGeneral${label}`,
const reqGeneralValue = (key: 'train' | 'atmos', label: string, minValue: number): Constraint => ({
name: 'reqGeneralValue',
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }],
test: (ctx, view) => {
const general = readGeneral(ctx, view);
@@ -103,8 +103,8 @@ export class ActionDefinition<
return [
notBeNeutral(),
reqGeneralCrew(),
reqGeneralStat('train', '훈련', trainLow),
reqGeneralStat('atmos', '사기', atmosLow),
reqGeneralValue('train', '훈련', trainLow),
reqGeneralValue('atmos', '사기', atmosLow),
reqGeneralGold(getRequiredGold),
reqGeneralRice(getRequiredRice),
];
@@ -4,8 +4,10 @@ import {
notBeNeutral,
notWanderingNation,
occupiedCity,
reqGeneralAtmosMargin,
reqGeneralCrew,
reqGeneralGold,
reqGeneralRice,
} 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';
@@ -49,7 +51,19 @@ export class ActionDefinition<
buildConstraints(_ctx: ConstraintContext, _args: BoostMoraleArgs): Constraint[] {
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.costGold ?? 0;
return [notBeNeutral(), notWanderingNation(), occupiedCity(), reqGeneralCrew(), reqGeneralGold(getRequiredGold)];
const maxAtmos =
this.env.maxAtmosByCommand && this.env.maxAtmosByCommand > 0
? this.env.maxAtmosByCommand
: DEFAULT_MAX_ATMOS;
return [
notBeNeutral(),
notWanderingNation(),
occupiedCity(),
reqGeneralCrew(),
reqGeneralGold(getRequiredGold),
reqGeneralRice(() => 0),
reqGeneralAtmosMargin(maxAtmos),
];
}
resolve(
@@ -52,6 +52,12 @@ export class ActionDefinition<
return parseArgsWithSchema(ARGS_SCHEMA, raw);
}
buildMinConstraints(_ctx: ConstraintContext, _args: DexTransferArgs): Constraint[] {
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.develCost ?? 0;
const getRequiredRice = (_context: ConstraintContext, _view: StateView): number => this.env.develCost ?? 0;
return [notBeNeutral(), occupiedCity(), reqGeneralGold(getRequiredGold), reqGeneralRice(getRequiredRice)];
}
buildConstraints(_ctx: ConstraintContext, _args: DexTransferArgs): Constraint[] {
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.develCost ?? 0;
const getRequiredRice = (_context: ConstraintContext, _view: StateView): number => this.env.develCost ?? 0;
@@ -1,6 +1,5 @@
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
import { notBeNeutral, reqGeneralGold } from '@sammo-ts/logic/constraints/presets.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
@@ -34,8 +33,7 @@ export class ActionDefinition<
}
buildConstraints(_ctx: ConstraintContext, _args: RecoveryArgs): Constraint[] {
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.costGold ?? 0;
return [notBeNeutral(), reqGeneralGold(getRequiredGold)];
return [];
}
resolve(
@@ -22,8 +22,8 @@ const ACTION_KEY = 'che_은퇴';
const REQ_AGE = 60;
const reqAge = (): Constraint => ({
name: 'ReqAge',
const reqGeneralValue = (): Constraint => ({
name: 'reqGeneralValue',
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }],
test: (ctx, view) => {
const generalKey: RequirementKey = { kind: 'general', id: ctx.actorId };
@@ -118,7 +118,7 @@ export class ActionDefinition<
}
buildConstraints(_ctx: ConstraintContext, _args: RetireArgs): Constraint[] {
return [reqAge()];
return [reqGeneralValue()];
}
resolve(context: GeneralActionResolveContext<TriggerState>, args: RetireArgs): GeneralActionOutcome<TriggerState> {
@@ -16,8 +16,8 @@ const ACTION_NAME = '전투 특기 초기화';
const hasSpecial = (value: string | null | undefined): boolean =>
value !== null && value !== undefined && value !== 'None';
const reqWarSpecial = (): Constraint => ({
name: 'ReqGeneralWarSpecial',
const reqGeneralValue = (): Constraint => ({
name: 'reqGeneralValue',
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }],
test: (ctx, view) => {
const general = readGeneral(ctx, view);
@@ -43,8 +43,12 @@ export class ActionDefinition<
return {};
}
buildMinConstraints(_ctx: ConstraintContext, _args: ResetSpecialWarArgs): Constraint[] {
return [reqGeneralValue()];
}
buildConstraints(_ctx: ConstraintContext, _args: ResetSpecialWarArgs): Constraint[] {
return [reqWarSpecial()];
return [reqGeneralValue()];
}
resolve(
@@ -4,7 +4,8 @@ import {
notBeNeutral,
notWanderingNation,
occupiedCity,
remainCityCapacityByMax,
remainCityCapacity,
reqGeneralGold,
reqGeneralRice,
suppliedCity,
} from '@sammo-ts/logic/constraints/presets.js';
@@ -76,7 +77,8 @@ export class ActionDefinition<
notWanderingNation(),
occupiedCity(),
suppliedCity(),
remainCityCapacityByMax('population', 'populationMax', '인구'),
reqGeneralGold(() => 0),
remainCityCapacity('population', '인구'),
reqGeneralRice(getRequiredRice),
];
}
@@ -6,6 +6,7 @@ import {
notBeNeutral,
notWanderingNation,
occupiedCity,
reqGeneralGold,
reqGeneralRice,
suppliedCity,
} from '@sammo-ts/logic/constraints/presets.js';
@@ -134,6 +135,7 @@ export class ActionDefinition<
notWanderingNation(),
occupiedCity(),
suppliedCity(),
reqGeneralGold(() => 0, requirements),
reqGeneralRice(getRiceCost, requirements),
remainCityTrust(),
];
@@ -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 { notBeNeutral } from '@sammo-ts/logic/constraints/presets.js';
import { notBeNeutral, notLord } from '@sammo-ts/logic/constraints/presets.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
@@ -106,7 +106,7 @@ export class ActionDefinition<
}
buildConstraints(_ctx: ConstraintContext, _args: ResignArgs): Constraint[] {
return [notBeNeutral()];
return [notBeNeutral(), notLord()];
}
resolve(context: GeneralActionResolveContext<TriggerState>, args: ResignArgs): GeneralActionOutcome<TriggerState> {
@@ -2,7 +2,6 @@ import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import {
notBeNeutral,
notWanderingNation,
occupiedCity,
reqGeneralGold,
reqGeneralRice,
@@ -107,9 +106,9 @@ export class ActionDefinition<
buildConstraints(_ctx: ConstraintContext, args: DonateArgs): Constraint[] {
if (args.isGold) {
return [notBeNeutral(), notWanderingNation(), occupiedCity(), suppliedCity(), reqGeneralGold(() => args.amount)];
return [notBeNeutral(), occupiedCity(), suppliedCity(), reqGeneralGold(() => args.amount)];
}
return [notBeNeutral(), notWanderingNation(), occupiedCity(), suppliedCity(), reqGeneralRice(() => args.amount)];
return [notBeNeutral(), occupiedCity(), suppliedCity(), reqGeneralRice(() => args.amount)];
}
resolve(context: GeneralActionResolveContext<TriggerState>, args: DonateArgs): GeneralActionOutcome<TriggerState> {
@@ -10,7 +10,6 @@ import type {
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import {
disallowDiplomacyBetweenStatus,
existsDestCity,
notBeNeutral,
notNeutralDestCity,
notOccupiedDestCity,
@@ -393,7 +392,6 @@ export class ActionDefinition<
notBeNeutral(),
occupiedCity(),
suppliedCity(),
existsDestCity(),
notOccupiedDestCity(),
notNeutralDestCity(),
reqGeneralGold(() => gold),
@@ -1,6 +1,12 @@
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
import { notBeNeutral, notWanderingNation, occupiedCity, reqGeneralCrew, reqGeneralGold } from '@sammo-ts/logic/constraints/presets.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import {
notBeNeutral,
notWanderingNation,
occupiedCity,
reqGeneralCrew,
reqGeneralTrainMargin,
} 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 type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
@@ -42,8 +48,11 @@ export class ActionDefinition<
}
buildConstraints(_ctx: ConstraintContext, _args: TrainingArgs): Constraint[] {
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.costGold ?? 0;
return [notBeNeutral(), notWanderingNation(), occupiedCity(), reqGeneralCrew(), reqGeneralGold(getRequiredGold)];
const maxTrain =
this.env.maxTrainByCommand && this.env.maxTrainByCommand > 0
? this.env.maxTrainByCommand
: DEFAULT_MAX_TRAIN;
return [notBeNeutral(), notWanderingNation(), occupiedCity(), reqGeneralCrew(), reqGeneralTrainMargin(maxTrain)];
}
resolve(
@@ -24,8 +24,8 @@ export interface EventResearchConfig {
category?: string;
}
const requireNationAux = (auxKey: string, actionName: string): Constraint => ({
name: 'requireNationAux',
const reqNationAuxValue = (auxKey: string, actionName: string): Constraint => ({
name: 'reqNationAuxValue',
requires: (ctx) => (ctx.nationId !== undefined ? [{ kind: 'nation', id: ctx.nationId }] : []),
test: (ctx: ConstraintContext, view: StateView) => {
if (ctx.nationId === undefined) {
@@ -69,13 +69,25 @@ export const createEventResearchCommand = (config: EventResearchConfig): {
return {};
}
buildMinConstraints(_ctx: ConstraintContext, _args: Record<string, never>): Constraint[] {
void _ctx;
void _args;
return [
occupiedCity(),
beChief(),
reqNationAuxValue(config.auxKey, ACTION_NAME),
reqNationGold(() => this.env.baseGold + COST),
reqNationRice(() => this.env.baseRice + COST),
];
}
buildConstraints(_ctx: ConstraintContext, _args: Record<string, never>): Constraint[] {
void _ctx;
void _args;
return [
occupiedCity(),
beChief(),
requireNationAux(config.auxKey, ACTION_NAME),
reqNationAuxValue(config.auxKey, ACTION_NAME),
reqNationGold(() => this.env.baseGold + COST),
reqNationRice(() => this.env.baseRice + COST),
];
+36
View File
@@ -109,6 +109,42 @@ export const suppliedCity = (): Constraint => ({
},
});
export const reqCityTrader = (): Constraint => ({
name: 'reqCityTrader',
requires: (ctx) => {
const reqs: RequirementKey[] = [{ kind: 'general', id: ctx.actorId }];
if (ctx.cityId !== undefined) {
reqs.push({ kind: 'city', id: ctx.cityId });
}
return reqs;
},
test: (ctx, view) => {
const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId };
if (!view.has(generalReq)) {
return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.');
}
const general = view.get(generalReq) as General | null;
if (!general) {
return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.');
}
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], '도시 정보가 없습니다.');
}
const trade = readMetaNumberFromUnknown(city.meta, 'trade');
if (trade !== null || general.npcState >= 2) {
return allow();
}
return { kind: 'deny', reason: '도시에 상인이 없습니다.' };
},
});
export const suppliedDestCity = (): Constraint => ({
name: 'suppliedDestCity',
requires: (ctx) =>
+92
View File
@@ -145,6 +145,98 @@ export const reqGeneralCrew = (): Constraint => ({
},
});
export const reqGeneralTrainMargin = (maxTrain: number): Constraint => ({
name: 'reqGeneralTrainMargin',
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }],
test: (ctx, view) => {
const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId };
if (!view.has(generalReq)) {
return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.');
}
const general = view.get(generalReq) as General | null;
if (!general) {
return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.');
}
if (general.train < maxTrain) {
return allow();
}
return { kind: 'deny', reason: '병사들은 이미 정예병사들입니다.' };
},
});
export const reqGeneralAtmosMargin = (maxAtmos: number): Constraint => ({
name: 'reqGeneralAtmosMargin',
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }],
test: (ctx, view) => {
const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId };
if (!view.has(generalReq)) {
return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.');
}
const general = view.get(generalReq) as General | null;
if (!general) {
return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.');
}
if (general.atmos < maxAtmos) {
return allow();
}
return { kind: 'deny', reason: '이미 사기는 하늘을 찌를듯 합니다.' };
},
});
export const allowJoinAction = (): Constraint => ({
name: 'allowJoinAction',
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }],
test: (ctx, view) => {
const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId };
if (!view.has(generalReq)) {
return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.');
}
const general = view.get(generalReq) as General | null;
if (!general) {
return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.');
}
const makelimit = typeof general.meta.makelimit === 'number' ? general.meta.makelimit : 0;
if (makelimit === 0) {
return allow();
}
const joinActionLimit = typeof ctx.env.joinActionLimit === 'number' ? ctx.env.joinActionLimit : 12;
return { kind: 'deny', reason: `재야가 된지 ${joinActionLimit}턴이 지나야 합니다.` };
},
});
export const noPenalty = (penaltyKey: string): Constraint => ({
name: 'noPenalty',
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }],
test: (ctx, view) => {
const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId };
if (!view.has(generalReq)) {
return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.');
}
const general = view.get(generalReq) as General | null;
if (!general) {
return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.');
}
const penalty = general.meta.penalty;
if (!penalty || typeof penalty !== 'object' || Array.isArray(penalty)) {
return allow();
}
const penaltyMap = penalty as Record<string, unknown>;
if (!Object.prototype.hasOwnProperty.call(penaltyMap, penaltyKey)) {
return allow();
}
const reason = penaltyMap[penaltyKey];
return {
kind: 'deny',
reason: `징계 사유: ${typeof reason === 'string' ? reason : String(reason)}`,
};
},
});
export const reqGeneralCrewMargin = (
getCrewTypeId: (ctx: ConstraintContext, view: StateView) => number | null,
requirements: RequirementKey[] = []