Merge branch 'main' into feature/ingame-info-menus
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
|
||||
현재 ref MariaDB와 core memory를 잇는 공통 runner, 성공 경로 55개,
|
||||
실행 중 확률 실패 9개, full constraint fallback 7개와 모략 확률 clamp
|
||||
8개가 구현됐다.
|
||||
8개, 모략 결과값 경계 5개가 구현됐다.
|
||||
실패 9개는 내정 critical
|
||||
`주민선정/정착장려/상업투자/기술연구/물자조달`과 모략
|
||||
`화계/선동/파괴/탈취`이며 RNG 전체 trace, semantic state delta와 실패
|
||||
@@ -17,9 +17,11 @@
|
||||
금·쌀 부족과 민심 상한을 대표하며 휴식 fallback과 RNG/state delta를
|
||||
비교한다. clamp 8개는 `화계/선동/파괴/탈취` 각각의 계산 확률 0과
|
||||
0.5 경계에서 성공 판정 RNG의 무소비 또는 `nextBits(1)` 소비와 전체
|
||||
state delta를 비교한다. 나머지 명령별 제약 실패·값 경계·alternative와 전체 core
|
||||
PostgreSQL 재조회가 완료 기준을 통과하기 전까지 55개 명령 전체의 동적
|
||||
호환 상태를 `확인`으로 올리지 않는다.
|
||||
state delta를 비교한다. 결과값 5개는 화계 농업·상업, 선동 치안·민심,
|
||||
파괴 수비·성벽의 0 바닥과 탈취의 대상 국가 자원 상한·미보급 도시 최종
|
||||
저장 상태를 비교한다. 나머지 명령별 제약 실패·값 경계·alternative와
|
||||
전체 core PostgreSQL 재조회가 완료 기준을 통과하기 전까지 55개 명령
|
||||
전체의 동적 호환 상태를 `확인`으로 올리지 않는다.
|
||||
|
||||
## 결정 요약
|
||||
|
||||
@@ -588,6 +590,12 @@ fixture가 명시한 필드만 비교하면 예상하지 못한 side effect를
|
||||
`city.trust FLOAT` 재조회 정밀도 차이도 발견해 부분 patch와 6자리
|
||||
유효숫자 저장 경계로 바로잡았다.
|
||||
|
||||
결과값 경계 fixture에서는 파괴의 전체 city spread도 선동과 같이
|
||||
불필요한 front 재계산을 일으키는 문제를 발견해 수비·성벽·state 부분
|
||||
patch로 좁혔다. 미보급 탈취는 레거시가 자원 감소 update 뒤 원본
|
||||
`destCity.agri/comm`을 다시 저장하므로 관찰 가능한 최종 자원은 변하지
|
||||
않는다. 버그로 보이지만 호환 계약으로 보존하고 `state=32`만 남긴다.
|
||||
|
||||
## 55개 명령 coverage manifest
|
||||
|
||||
`manifest.json`은 PHP command inventory와 TS registry의 합집합을 기준으로
|
||||
|
||||
@@ -187,6 +187,11 @@ fire attack, agitation, destruction and seizure. The zero boundary skips the
|
||||
success RNG primitive, while exactly 0.5 consumes `nextBits(1)`; the complete
|
||||
RNG trace and semantic delta match. These cases also fixed fire-attack city
|
||||
state persistence and agitation front/trust persistence differences.
|
||||
Five sabotage value-boundary cases cover zero floors for fire-attack,
|
||||
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.
|
||||
This is not yet a claim that every command-specific
|
||||
constraint, clamp, alternative and persistence boundary has been dynamically
|
||||
compared.
|
||||
|
||||
@@ -140,14 +140,12 @@ export class ActionResolver<
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const commDmg = stolenGold / 12;
|
||||
const agriDmg = stolenRice / 12;
|
||||
|
||||
// 레거시는 미보급 도시 자원을 먼저 감소시키지만 같은 명령 끝의
|
||||
// 원본 destCity 전체 저장이 이를 덮어쓴다. 관찰 가능한 최종
|
||||
// 상태는 자원 변화 없이 state 32만 남는다.
|
||||
effects.push(
|
||||
createCityPatchEffect(
|
||||
{
|
||||
commerce: Math.round(Math.max(0, destCity.commerce - commDmg)),
|
||||
agriculture: Math.round(Math.max(0, destCity.agriculture - agriDmg)),
|
||||
state: 32,
|
||||
},
|
||||
args.destCityId
|
||||
|
||||
@@ -106,7 +106,6 @@ export class ActionResolver<
|
||||
effects.push(
|
||||
createCityPatchEffect(
|
||||
{
|
||||
...destCity,
|
||||
defence: newDef,
|
||||
wall: newWall,
|
||||
state: 32, // Legacy sabotage state
|
||||
|
||||
@@ -554,6 +554,101 @@ integration('general sabotage probability clamp matrix', () => {
|
||||
);
|
||||
});
|
||||
|
||||
type SabotageValueBoundaryCase = {
|
||||
name: string;
|
||||
action: 'che_화계' | 'che_선동' | 'che_파괴' | 'che_탈취';
|
||||
stat: 'leadership' | 'strength' | 'intelligence';
|
||||
fixturePatches: FixturePatches;
|
||||
};
|
||||
|
||||
const sabotageValueBoundaryCases: SabotageValueBoundaryCase[] = [
|
||||
{
|
||||
name: 'fire attack does not reduce agriculture or commerce below zero',
|
||||
action: 'che_화계',
|
||||
stat: 'intelligence',
|
||||
fixturePatches: { cities: { 70: { agriculture: 1, commerce: 1 } } },
|
||||
},
|
||||
{
|
||||
name: 'agitation does not reduce security or trust below zero',
|
||||
action: 'che_선동',
|
||||
stat: 'leadership',
|
||||
fixturePatches: { cities: { 70: { security: 1, trust: 1 } } },
|
||||
},
|
||||
{
|
||||
name: 'destruction does not reduce defence or wall below zero',
|
||||
action: 'che_파괴',
|
||||
stat: 'strength',
|
||||
fixturePatches: { cities: { 70: { defence: 1, wall: 1 } } },
|
||||
},
|
||||
{
|
||||
name: 'seizure does not take more than supplied nation resources',
|
||||
action: 'che_탈취',
|
||||
stat: 'strength',
|
||||
fixturePatches: { nations: { 2: { gold: 1, rice: 1 } } },
|
||||
},
|
||||
{
|
||||
name: 'seizure does not reduce unsupplied city resources below zero',
|
||||
action: 'che_탈취',
|
||||
stat: 'strength',
|
||||
fixturePatches: {
|
||||
cities: { 70: { agriculture: 1, commerce: 1, supplyState: 0 } },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const hasSuccessfulSabotageLog = (logs: Array<Record<string, unknown>>): boolean =>
|
||||
logs.some((entry) => typeof entry.text === 'string' && entry.text.includes('성공했습니다.'));
|
||||
|
||||
integration('general sabotage value boundary matrix', () => {
|
||||
it.each(sabotageValueBoundaryCases)(
|
||||
'$name matches the legacy clamped state delta',
|
||||
async ({ action, stat, fixturePatches }) => {
|
||||
const request = buildRequest(
|
||||
action,
|
||||
{ destCityID: 70 },
|
||||
{ [stat]: 100 },
|
||||
{
|
||||
...fixturePatches,
|
||||
generals: {
|
||||
...fixturePatches.generals,
|
||||
2: { ...fixturePatches.generals?.[2], [stat]: 10 },
|
||||
},
|
||||
cities: {
|
||||
...fixturePatches.cities,
|
||||
70: {
|
||||
...fixturePatches.cities?.[70],
|
||||
security: 0,
|
||||
securityMax: 2_000,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
request.setup!.world!.hiddenSeed = 'general-value-0';
|
||||
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(hasSuccessfulSabotageLog(reference.after.logs)).toBe(true);
|
||||
expect(hasSuccessfulSabotageLog(core.after.logs)).toBe(true);
|
||||
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