Add general command failure parity matrix
This commit is contained in:
@@ -2,14 +2,18 @@
|
||||
|
||||
## 상태
|
||||
|
||||
- 문서 상태: 구현 전 승인 가능한 설계
|
||||
- 문서 상태: 단계적 구현 중
|
||||
- 비교 기준: `ref/sam`의 `ng_compare` 브랜치
|
||||
- 대상: 휴식과 `cr_건국`을 포함한 일반 장수 예약 명령 55개
|
||||
- 범위: 명령 결과, RNG 소비, 로그, 예약 턴 lifecycle, DB 영속화
|
||||
|
||||
이 문서는 테스트 구현 자체가 아니다. 아래의 파일, 실행기, 격리 스택과
|
||||
fixture가 구현되고 완료 기준을 통과하기 전까지 55개 명령의 동적 호환 상태를
|
||||
`확인`으로 올리지 않는다.
|
||||
현재 ref MariaDB와 core memory를 잇는 공통 runner, 성공 경로 55개,
|
||||
실행 중 확률 실패 9개가 구현됐다. 실패 9개는 내정 critical
|
||||
`주민선정/정착장려/상업투자/기술연구/물자조달`과 모략
|
||||
`화계/선동/파괴/탈취`이며 RNG 전체 trace, semantic state delta와 실패
|
||||
로그 본문을 비교한다. 나머지 제약 실패·값 경계·alternative와 전체
|
||||
core PostgreSQL 재조회가 완료 기준을 통과하기 전까지 55개 명령 전체의
|
||||
동적 호환 상태를 `확인`으로 올리지 않는다.
|
||||
|
||||
## 결정 요약
|
||||
|
||||
|
||||
@@ -169,13 +169,18 @@ Compatibility is established per case only when:
|
||||
4. live sortie also passes the battle trace comparison;
|
||||
5. any ignored path is documented in the case evidence.
|
||||
|
||||
As of 2026-07-26, 54 general matrix cases, 35 nation matrix cases, declaration
|
||||
and live sortie pass this boundary. Live sortie is the remaining general
|
||||
command key and covers battle entry, conquest, defeated-general neutralization
|
||||
and last-city nation collapse. Thus all 55 general command keys have a
|
||||
completed success-path comparison. This is 91 executable comparison cases;
|
||||
it is not yet a claim that failure/boundary paths or all 38 nation commands
|
||||
have been dynamically compared.
|
||||
As of 2026-07-26, 54 general matrix cases, 35 reserved nation matrix cases,
|
||||
declaration and live sortie pass this boundary. Live sortie is the remaining
|
||||
general command key and covers battle entry, conquest, defeated-general
|
||||
neutralization and last-city nation collapse. Thus all 55 general command keys
|
||||
have a completed success-path comparison. The three instant diplomatic nation
|
||||
responses pass a separate reference trace and core resolver/API boundary.
|
||||
|
||||
Nine general in-action failure cases now also pass the common differential:
|
||||
five domestic critical failures and four sabotage failures. They compare the
|
||||
full command RNG trace, semantic state delta and the exact action-log body.
|
||||
This is not yet a claim that the remaining constraint, clamp, alternative and
|
||||
persistence boundaries have been dynamically compared.
|
||||
|
||||
The fixture runner also reports whether the requested legacy command reached
|
||||
its completed execution path. For multi-turn commands this is derived from the
|
||||
|
||||
@@ -100,8 +100,9 @@ export class ActionResolver<
|
||||
const ded = (score * 1.0) / 3;
|
||||
|
||||
// 7. Update General
|
||||
const nextExp = general.experience + Math.trunc(exp);
|
||||
const nextDed = general.dedication + Math.trunc(ded);
|
||||
// 레거시는 부동소수점 증가분을 INT column에 저장할 때 반올림한다.
|
||||
const nextExp = general.experience + Math.round(exp);
|
||||
const nextDed = general.dedication + Math.round(ded);
|
||||
|
||||
let appliedScore = score;
|
||||
if (context.city && [1, 3].includes(context.city.frontState)) {
|
||||
|
||||
@@ -46,6 +46,10 @@ const readTrust = (city: City): number => {
|
||||
return typeof trust === 'number' && Number.isFinite(trust) ? trust : DEFAULT_TRUST;
|
||||
};
|
||||
|
||||
// 레거시 city.trust는 MariaDB FLOAT이며 다음 명령에서 6자리 유효숫자로
|
||||
// 재조회된다. 같은 턴의 후속 명령도 그 저장 경계를 보도록 정규화한다.
|
||||
const toLegacyStoredTrust = (value: number): number => Number(value.toPrecision(6));
|
||||
|
||||
const remainCityTrust = (): Constraint => ({
|
||||
name: 'remainCityTrust',
|
||||
requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []),
|
||||
@@ -106,7 +110,7 @@ export class ActionDefinition<
|
||||
const trustDelta = result.score / 10;
|
||||
context.city.meta = {
|
||||
...context.city.meta,
|
||||
trust: clamp(readTrust(context.city) + trustDelta, 0, 100),
|
||||
trust: toLegacyStoredTrust(clamp(readTrust(context.city) + trustDelta, 0, 100)),
|
||||
};
|
||||
context.general.rice = Math.max(0, context.general.rice - result.costGold);
|
||||
context.general.experience += result.exp;
|
||||
|
||||
@@ -381,3 +381,106 @@ integration('general command success matrix', () => {
|
||||
120_000
|
||||
);
|
||||
});
|
||||
|
||||
type GeneralFailureCase = {
|
||||
action: string;
|
||||
args?: Record<string, unknown>;
|
||||
hiddenSeed: string;
|
||||
failureText: string;
|
||||
};
|
||||
|
||||
const failureCases: GeneralFailureCase[] = [
|
||||
{
|
||||
action: 'che_주민선정',
|
||||
hiddenSeed: 'general-failure-che_주민선정-0',
|
||||
failureText: "주민 선정을 <span class='ev_failed'>실패</span>",
|
||||
},
|
||||
{
|
||||
action: 'che_정착장려',
|
||||
hiddenSeed: 'general-failure-che_정착장려-0',
|
||||
failureText: "정착 장려를 <span class='ev_failed'>실패</span>",
|
||||
},
|
||||
{
|
||||
action: 'che_상업투자',
|
||||
hiddenSeed: 'general-failure-che_상업투자-2',
|
||||
failureText: "상업 투자를 <span class='ev_failed'>실패</span>",
|
||||
},
|
||||
{
|
||||
action: 'che_기술연구',
|
||||
hiddenSeed: 'general-failure-che_기술연구-0',
|
||||
failureText: "기술 연구를 <span class='ev_failed'>실패</span>",
|
||||
},
|
||||
{
|
||||
action: 'che_물자조달',
|
||||
hiddenSeed: 'general-failure-che_물자조달-1',
|
||||
failureText: "조달을 <span class='ev_failed'>실패</span>",
|
||||
},
|
||||
{
|
||||
action: 'che_화계',
|
||||
args: { destCityID: 70 },
|
||||
hiddenSeed: 'general-failure-che_화계-0',
|
||||
failureText: '화계가 실패했습니다.',
|
||||
},
|
||||
{
|
||||
action: 'che_선동',
|
||||
args: { destCityID: 70 },
|
||||
hiddenSeed: 'general-failure-che_선동-0',
|
||||
failureText: '선동이 실패했습니다.',
|
||||
},
|
||||
{
|
||||
action: 'che_파괴',
|
||||
args: { destCityID: 70 },
|
||||
hiddenSeed: 'general-failure-che_파괴-0',
|
||||
failureText: '파괴가 실패했습니다.',
|
||||
},
|
||||
{
|
||||
action: 'che_탈취',
|
||||
args: { destCityID: 70 },
|
||||
hiddenSeed: 'general-failure-che_탈취-0',
|
||||
failureText: '탈취가 실패했습니다.',
|
||||
},
|
||||
];
|
||||
|
||||
const failureLogTexts = (logs: Array<Record<string, unknown>>, failureText: string): string[] =>
|
||||
logs
|
||||
.map((entry) => entry.text)
|
||||
.filter((text): text is string => typeof text === 'string' && text.includes(failureText));
|
||||
|
||||
const legacyActionLogBody = (text: string): string => {
|
||||
const match = /^<C>●<\/>\d+월:(.*) <1>\d{2}:\d{2}<\/>$/.exec(text);
|
||||
return match?.[1] ?? text;
|
||||
};
|
||||
|
||||
integration('general command in-action failure matrix', () => {
|
||||
it.each(failureCases)(
|
||||
'$action matches legacy failure RNG, side effects, and failure log',
|
||||
async ({ action, args, hiddenSeed, failureText }) => {
|
||||
const request = buildRequest(action, args);
|
||||
request.setup!.world!.hiddenSeed = hiddenSeed;
|
||||
const reference = runReferenceTurnCommandTraceRequest(
|
||||
workspaceRoot!,
|
||||
request as unknown as Record<string, unknown>
|
||||
);
|
||||
const core = await runCoreTurnCommandTrace(request, reference.before);
|
||||
|
||||
expect(reference.execution.outcome).toMatchObject({ completed: true });
|
||||
expect(core.execution.outcome).toMatchObject({
|
||||
requestedAction: action,
|
||||
actionKey: action,
|
||||
usedFallback: false,
|
||||
});
|
||||
expect(core.execution.outcome).not.toHaveProperty('blockedReason');
|
||||
expect(failureLogTexts(reference.after.logs, failureText)).toHaveLength(1);
|
||||
expect(failureLogTexts(core.after.logs, failureText)).toEqual(
|
||||
failureLogTexts(reference.after.logs, failureText).map(legacyActionLogBody)
|
||||
);
|
||||
expect(core.rng).toEqual(reference.rng);
|
||||
expect(
|
||||
compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
|
||||
ignoredPathPatterns: ignoredLifecyclePaths,
|
||||
})
|
||||
).toEqual([]);
|
||||
},
|
||||
120_000
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user