refactor: tighten backend and logic boundaries

This commit is contained in:
2026-08-15 06:46:52 +00:00
parent 7b02ff155f
commit 35d8824b95
86 changed files with 820 additions and 1872 deletions
+1 -46
View File
@@ -236,30 +236,6 @@ export const remainCityCapacity = (key: keyof City, label: string): Constraint =
},
});
export const remainCityCapacityByMax = (key: keyof City, maxKey: keyof City, label: string): Constraint => ({
name: 'remainCityCapacityByMax',
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], '도시 정보가 없습니다.');
}
const current = city[key];
const max = city[maxKey];
if (typeof current !== 'number' || typeof max !== 'number') {
return unknownOrDeny(ctx, [], '도시 정보가 없습니다.');
}
if (current < max) {
return allow();
}
return { kind: 'deny', reason: `${label}이 충분합니다.` };
},
});
export const reqCityCapacity = (key: keyof City, label: string, required: number | string): Constraint => ({
name: 'reqCityCapacity',
requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []),
@@ -506,7 +482,7 @@ export const hasRouteWithEnemy = (): Constraint => ({
},
});
export const beNeutralCity = (): Constraint => ({
export const neutralCity = (): Constraint => ({
name: 'beNeutralCity',
requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []),
test: (ctx, view) => {
@@ -534,8 +510,6 @@ export const beNeutralCity = (): Constraint => ({
},
});
export const neutralCity = (): Constraint => beNeutralCity();
export const constructableCity = (): Constraint => ({
name: 'constructableCity',
requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []),
@@ -558,25 +532,6 @@ export const constructableCity = (): Constraint => ({
},
});
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: '규모가 맞지 않습니다.' };
},
});
export const nearCity = (maxDistance: number): Constraint => ({
name: 'nearCity',
requires: (ctx) => {
+1 -9
View File
@@ -1,4 +1,4 @@
import type { Constraint, ConstraintContext, ConstraintResult, RequirementKey, StateView } from './types.js';
import type { Constraint, ConstraintContext, ConstraintResult, StateView } from './types.js';
export const evaluateConstraints = (
constraints: Constraint[],
@@ -24,14 +24,6 @@ export const evaluateConstraints = (
return { kind: 'allow' };
};
export const collectRequirements = (constraints: Constraint[], ctx: ConstraintContext): RequirementKey[] => {
const keys: RequirementKey[] = [];
for (const constraint of constraints) {
keys.push(...constraint.requires(ctx));
}
return keys;
};
export interface ActionWithConstraints {
buildConstraints(ctx: ConstraintContext, args: unknown): Constraint[];
}
-98
View File
@@ -89,25 +89,6 @@ 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 beLord = (): Constraint => ({
name: 'beLord',
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }],
@@ -508,47 +489,6 @@ export const existsDestGeneral = (): Constraint => ({
},
});
export const destGeneralInDestNation = (): Constraint => ({
name: 'destGeneralInDestNation',
requires: (ctx) => {
const reqs: RequirementKey[] = [];
const destGeneralId = resolveDestGeneralId(ctx);
if (destGeneralId !== undefined) {
reqs.push({ kind: 'destGeneral', id: destGeneralId });
}
const destNationId = resolveDestNationId(ctx);
if (destNationId !== undefined) {
reqs.push({ kind: 'destNation', id: destNationId });
}
return reqs;
},
test: (ctx, view) => {
const destGeneral = readDestGeneral(ctx, view);
if (!destGeneral) {
const destGeneralId = resolveDestGeneralId(ctx);
if (destGeneralId === undefined) {
return unknownOrDeny(ctx, [], '장수 정보가 없습니다.');
}
const req: RequirementKey = {
kind: 'destGeneral',
id: destGeneralId,
};
return unknownOrDeny(ctx, [req], '장수 정보가 없습니다.');
}
const destNationId = resolveDestNationId(ctx);
if (destNationId === undefined) {
return unknownOrDeny(ctx, [], '국가 정보가 없습니다.');
}
if (destGeneral.nationId !== destNationId) {
return {
kind: 'deny',
reason: '제의 장수가 국가 소속이 아닙니다.',
};
}
return allow();
},
});
export const friendlyDestGeneral = (): Constraint => ({
name: 'friendlyDestGeneral',
requires: (ctx) => {
@@ -603,29 +543,6 @@ export const mustBeNPC = (): Constraint => ({
},
});
export const notSameDestNation = (): Constraint => ({
name: 'notSameDestNation',
requires: (ctx) => {
const reqs: RequirementKey[] = [];
const destNationId = resolveDestNationId(ctx);
if (destNationId !== undefined) {
reqs.push({ kind: 'destNation', id: destNationId });
}
return reqs;
},
test: (ctx, _view) => {
const destNationId = resolveDestNationId(ctx);
if (destNationId === undefined) {
return unknownOrDeny(ctx, [], '목표 국가가 없습니다.');
}
if (ctx.nationId === destNationId) {
return { kind: 'deny', reason: '이미 소속된 국가입니다.' };
}
return allow();
},
});
export const notLord = (): Constraint => ({
name: 'notLord',
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }],
@@ -640,18 +557,3 @@ export const notLord = (): Constraint => ({
return { kind: 'deny', reason: '군주는 불가능합니다.' };
},
});
export const notChief = (): Constraint => ({
name: 'notChief',
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }],
test: (ctx, view) => {
const req: RequirementKey = { kind: 'general', id: ctx.actorId };
const general = view.get(req) as General | null;
if (!general) return unknownOrDeny(ctx, [req], '장수 정보가 없습니다.');
if (general.officerLevel <= 4) {
return allow();
}
return { kind: 'deny', reason: '수뇌입니다.' };
},
});
-3
View File
@@ -7,9 +7,6 @@ export const denyWithReason = (reason: string): Constraint => ({
test: () => ({ kind: 'deny', reason }),
});
// TODO: 점진 이전을 위해 유지. 신규 코드에서는 denyWithReason을 사용한다.
export const alwaysFail = denyWithReason;
export const notOpeningPart = (relYear: number, openingPartYear: number): Constraint => ({
name: 'notOpeningPart',
requires: () => [],
-19
View File
@@ -48,25 +48,6 @@ 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 wanderingNation = (): Constraint => ({
name: 'wanderingNation',
requires: (ctx) => (ctx.nationId !== undefined ? [{ kind: 'nation', id: ctx.nationId }] : []),