Add sabotage injury boundary parity
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
|
||||
현재 ref MariaDB와 core memory를 잇는 공통 runner, 성공 경로 55개,
|
||||
실행 중 확률 실패 9개, full constraint fallback 7개와 모략 확률 clamp
|
||||
8개, 모략 결과값 경계 5개가 구현됐다.
|
||||
8개, 모략 결과값 경계 5개, 부상 경계 3개가 구현됐다.
|
||||
실패 9개는 내정 critical
|
||||
`주민선정/정착장려/상업투자/기술연구/물자조달`과 모략
|
||||
`화계/선동/파괴/탈취`이며 RNG 전체 trace, semantic state delta와 실패
|
||||
@@ -19,9 +19,11 @@
|
||||
0.5 경계에서 성공 판정 RNG의 무소비 또는 `nextBits(1)` 소비와 전체
|
||||
state delta를 비교한다. 결과값 5개는 화계 농업·상업, 선동 치안·민심,
|
||||
파괴 수비·성벽의 0 바닥과 탈취의 대상 국가 자원 상한·미보급 도시 최종
|
||||
저장 상태를 비교한다. 나머지 명령별 제약 실패·값 경계·alternative와
|
||||
전체 core PostgreSQL 재조회가 완료 기준을 통과하기 전까지 55개 명령
|
||||
전체의 동적 호환 상태를 `확인`으로 올리지 않는다.
|
||||
저장 상태를 비교한다. 부상 3개는 화계·선동·파괴의 대상 장수별 판정,
|
||||
부상도 80 상한, 병력·훈련·사기 0.98 정수 저장과 대상 장수 로그를
|
||||
비교한다. 나머지 명령별 제약 실패·값 경계·alternative와 전체 core
|
||||
PostgreSQL 재조회가 완료 기준을 통과하기 전까지 55개 명령 전체의 동적
|
||||
호환 상태를 `확인`으로 올리지 않는다.
|
||||
|
||||
## 결정 요약
|
||||
|
||||
@@ -596,6 +598,11 @@ patch로 좁혔다. 미보급 탈취는 레거시가 자원 감소 update 뒤
|
||||
`destCity.agri/comm`을 다시 저장하므로 관찰 가능한 최종 자원은 변하지
|
||||
않는다. 버그로 보이지만 호환 계약으로 보존하고 `state=32`만 남긴다.
|
||||
|
||||
부상 경계 fixture에서는 화계의 `계략로 인해` 조사를 레거시
|
||||
`계략으로 인해`로 수정하고, 선동·파괴에 누락된 대상 장수 부상 로그를
|
||||
추가했다. 병력·훈련·사기의 0.98 감소는 ref 정수 DB 저장처럼 내림이 아닌
|
||||
반올림을 적용한다.
|
||||
|
||||
## 55개 명령 coverage manifest
|
||||
|
||||
`manifest.json`은 PHP command inventory와 TS registry의 합집합을 기준으로
|
||||
|
||||
@@ -192,6 +192,11 @@ agitation and destruction city values, the supplied-nation resource cap for
|
||||
seizure, and the legacy final state of unsupplied seizure. They also remove
|
||||
destruction's unintended front recalculation and preserve the legacy
|
||||
unsupplied-seizure overwrite that leaves city resources unchanged.
|
||||
Three sabotage injury-boundary cases cover per-defender rolls, the injury cap
|
||||
of 80, integer persistence after multiplying crew/train/atmosphere by 0.98,
|
||||
and the target-general injury log for fire attack, agitation and destruction.
|
||||
They restore the legacy Korean particle in the log, add the two missing logs,
|
||||
and round rather than floor the integer fields.
|
||||
This is not yet a claim that every command-specific
|
||||
constraint, clamp, alternative and persistence boundary has been dynamically
|
||||
compared.
|
||||
|
||||
@@ -136,6 +136,10 @@ export class ActionResolver<
|
||||
consumeSuccessfulStrategyItem(this.pipeline, context);
|
||||
for (const injured of result.injuredGenerals) {
|
||||
effects.push(createGeneralPatchEffect(injured.patch, injured.id));
|
||||
ctx.addLog('<M>계략</>으로 인해 <R>부상</>을 당했습니다.', {
|
||||
generalId: injured.id,
|
||||
format: LogFormat.MONTH,
|
||||
});
|
||||
}
|
||||
|
||||
return { effects };
|
||||
|
||||
@@ -117,6 +117,10 @@ export class ActionResolver<
|
||||
consumeSuccessfulStrategyItem(this.pipeline, context);
|
||||
for (const injured of result.injuredGenerals) {
|
||||
effects.push(createGeneralPatchEffect(injured.patch, injured.id));
|
||||
ctx.addLog('<M>계략</>으로 인해 <R>부상</>을 당했습니다.', {
|
||||
generalId: injured.id,
|
||||
format: LogFormat.MONTH,
|
||||
});
|
||||
}
|
||||
|
||||
return { effects };
|
||||
|
||||
@@ -212,9 +212,9 @@ export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralT
|
||||
id: defender.id,
|
||||
patch: {
|
||||
injury: clamp(defender.injury + injuryAmount, 0, INJURY_MAX),
|
||||
crew: Math.floor(defender.crew * 0.98),
|
||||
atmos: Math.floor(defender.atmos * 0.98),
|
||||
train: Math.floor(defender.train * 0.98),
|
||||
crew: Math.round(defender.crew * 0.98),
|
||||
atmos: Math.round(defender.atmos * 0.98),
|
||||
train: Math.round(defender.train * 0.98),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -386,7 +386,7 @@ export class ActionResolver<
|
||||
for (const injured of result.injuredGenerals) {
|
||||
// 타겟 장수는 Draft가 아니므로 Effect 반환
|
||||
effects.push(createGeneralPatchEffect(injured.patch, injured.id));
|
||||
context.addLog(`<M>${ACTION_KEY}</>로 인해 <R>부상</>을 당했습니다.`, {
|
||||
context.addLog('<M>계략</>으로 인해 <R>부상</>을 당했습니다.', {
|
||||
generalId: injured.id,
|
||||
format: LogFormat.MONTH,
|
||||
});
|
||||
|
||||
@@ -649,6 +649,74 @@ integration('general sabotage value boundary matrix', () => {
|
||||
);
|
||||
});
|
||||
|
||||
type SabotageInjuryBoundaryCase = {
|
||||
action: 'che_화계' | 'che_선동' | 'che_파괴';
|
||||
stat: 'leadership' | 'strength' | 'intelligence';
|
||||
hiddenSeed: string;
|
||||
};
|
||||
|
||||
const sabotageInjuryBoundaryCases: SabotageInjuryBoundaryCase[] = [
|
||||
{ action: 'che_화계', stat: 'intelligence', hiddenSeed: 'general-injury-4' },
|
||||
{ action: 'che_선동', stat: 'leadership', hiddenSeed: 'general-injury-13' },
|
||||
{ action: 'che_파괴', stat: 'strength', hiddenSeed: 'general-injury-4' },
|
||||
];
|
||||
|
||||
const injuryLogTexts = (logs: Array<Record<string, unknown>>): string[] =>
|
||||
logs
|
||||
.map((entry) => entry.text)
|
||||
.filter((text): text is string => typeof text === 'string' && text.includes('부상</>을 당했습니다.'));
|
||||
|
||||
const legacyInjuryLogBody = (text: string): string => text.replace(/^<C>●<\/>\d+월:/, '');
|
||||
|
||||
integration('general sabotage injury boundary matrix', () => {
|
||||
it.each(sabotageInjuryBoundaryCases)(
|
||||
'$action matches legacy injury cap, integer persistence, and log',
|
||||
async ({ action, stat, hiddenSeed }) => {
|
||||
const request = buildRequest(
|
||||
action,
|
||||
{ destCityID: 70 },
|
||||
{ [stat]: 100 },
|
||||
{
|
||||
generals: {
|
||||
2: {
|
||||
[stat]: 10,
|
||||
injury: 79,
|
||||
crew: 101,
|
||||
atmos: 51,
|
||||
train: 51,
|
||||
},
|
||||
},
|
||||
cities: { 70: { security: 0, securityMax: 2_000 } },
|
||||
}
|
||||
);
|
||||
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(injuryLogTexts(reference.after.logs)).toHaveLength(1);
|
||||
expect(injuryLogTexts(core.after.logs)).toEqual(
|
||||
injuryLogTexts(reference.after.logs).map(legacyInjuryLogBody)
|
||||
);
|
||||
expect(core.rng).toEqual(reference.rng);
|
||||
expect(
|
||||
compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
|
||||
ignoredPathPatterns: ignoredLifecyclePaths,
|
||||
})
|
||||
).toEqual([]);
|
||||
},
|
||||
120_000
|
||||
);
|
||||
});
|
||||
|
||||
type GeneralConstraintCase = {
|
||||
name: string;
|
||||
action: string;
|
||||
|
||||
Reference in New Issue
Block a user