merge: add general command failure parity
This commit is contained in:
@@ -2,14 +2,18 @@
|
|||||||
|
|
||||||
## 상태
|
## 상태
|
||||||
|
|
||||||
- 문서 상태: 구현 전 승인 가능한 설계
|
- 문서 상태: 단계적 구현 중
|
||||||
- 비교 기준: `ref/sam`의 `ng_compare` 브랜치
|
- 비교 기준: `ref/sam`의 `ng_compare` 브랜치
|
||||||
- 대상: 휴식과 `cr_건국`을 포함한 일반 장수 예약 명령 55개
|
- 대상: 휴식과 `cr_건국`을 포함한 일반 장수 예약 명령 55개
|
||||||
- 범위: 명령 결과, RNG 소비, 로그, 예약 턴 lifecycle, DB 영속화
|
- 범위: 명령 결과, RNG 소비, 로그, 예약 턴 lifecycle, DB 영속화
|
||||||
|
|
||||||
이 문서는 테스트 구현 자체가 아니다. 아래의 파일, 실행기, 격리 스택과
|
현재 ref MariaDB와 core memory를 잇는 공통 runner, 성공 경로 55개,
|
||||||
fixture가 구현되고 완료 기준을 통과하기 전까지 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;
|
4. live sortie also passes the battle trace comparison;
|
||||||
5. any ignored path is documented in the case evidence.
|
5. any ignored path is documented in the case evidence.
|
||||||
|
|
||||||
As of 2026-07-26, 54 general matrix cases, 35 nation matrix cases, declaration
|
As of 2026-07-26, 54 general matrix cases, 35 reserved nation matrix cases,
|
||||||
and live sortie pass this boundary. Live sortie is the remaining general
|
declaration and live sortie pass this boundary. Live sortie is the remaining
|
||||||
command key and covers battle entry, conquest, defeated-general neutralization
|
general command key and covers battle entry, conquest, defeated-general
|
||||||
and last-city nation collapse. Thus all 55 general command keys have a
|
neutralization and last-city nation collapse. Thus all 55 general command keys
|
||||||
completed success-path comparison. This is 91 executable comparison cases;
|
have a completed success-path comparison. The three instant diplomatic nation
|
||||||
it is not yet a claim that failure/boundary paths or all 38 nation commands
|
responses pass a separate reference trace and core resolver/API boundary.
|
||||||
have been dynamically compared.
|
|
||||||
|
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
|
The fixture runner also reports whether the requested legacy command reached
|
||||||
its completed execution path. For multi-turn commands this is derived from the
|
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;
|
const ded = (score * 1.0) / 3;
|
||||||
|
|
||||||
// 7. Update General
|
// 7. Update General
|
||||||
const nextExp = general.experience + Math.trunc(exp);
|
// 레거시는 부동소수점 증가분을 INT column에 저장할 때 반올림한다.
|
||||||
const nextDed = general.dedication + Math.trunc(ded);
|
const nextExp = general.experience + Math.round(exp);
|
||||||
|
const nextDed = general.dedication + Math.round(ded);
|
||||||
|
|
||||||
let appliedScore = score;
|
let appliedScore = score;
|
||||||
if (context.city && [1, 3].includes(context.city.frontState)) {
|
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;
|
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 => ({
|
const remainCityTrust = (): Constraint => ({
|
||||||
name: 'remainCityTrust',
|
name: 'remainCityTrust',
|
||||||
requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []),
|
requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []),
|
||||||
@@ -106,7 +110,7 @@ export class ActionDefinition<
|
|||||||
const trustDelta = result.score / 10;
|
const trustDelta = result.score / 10;
|
||||||
context.city.meta = {
|
context.city.meta = {
|
||||||
...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.rice = Math.max(0, context.general.rice - result.costGold);
|
||||||
context.general.experience += result.exp;
|
context.general.experience += result.exp;
|
||||||
|
|||||||
@@ -381,3 +381,106 @@ integration('general command success matrix', () => {
|
|||||||
120_000
|
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