2 Commits

Author SHA1 Message Date
b4a4560678 Merge pull request 'fix: 버리기-뽑기 카드 미선언 prop 추가 (no such field 크래시)' (#95) from fix/discard-draw-props into main 2026-06-29 17:51:50 +09:00
8f8f17bd8f fix(deck): 버리기-뽑기 카드의 미선언 prop 추가 (no such field 크래시)
`DiscardPostDraw`·`DiscardDrawPerPick`가 hand.mjs(BeginDiscardSelection·
AutoDiscardHand·FinishDiscardSelection·SelectDiscardSlot)에서 대입되는데
gen-slaydeck.mjs prop 목록에 선언되지 않아 런타임 "cannot set DiscardPostDraw,
no such field"로 BeginDiscardSelection이 중단됨. 그 직전 DiscardSelectRemaining/
Total이 이미 set되어 게임이 버리기-선택 모드에 갇혀, 이후 모든 카드(공중제비 포함)
클릭이 버리기 픽으로 처리됨("2장 버린다 표시 + 동작 없음").

근본 원인: 형제 prop DiscardPostShiv·DiscardShivPerPick는 선언됐으나
draw 변형 2개가 누락. 선언만 추가(병렬 패턴).

신규 가드 tools/verify/cbprops.mjs: cb의 `self.X =` 대입 ↔ 선언 prop 대조,
미선언 = no such field 후보 검출. 수정 전 2개 누락→후 0.
검증: cbprops 0·cbgap GAP 0·테스트 93/93. 산출물(codeblock) 재생성 포함.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 02:44:06 +09:00
3 changed files with 50 additions and 0 deletions

View File

@@ -932,6 +932,20 @@
"Attributes": [],
"Name": "DiscardShivPerPick"
},
{
"Type": "number",
"DefaultValue": "0",
"SyncDirection": 0,
"Attributes": [],
"Name": "DiscardPostDraw"
},
{
"Type": "number",
"DefaultValue": "0",
"SyncDirection": 0,
"Attributes": [],
"Name": "DiscardDrawPerPick"
},
{
"Type": "boolean",
"DefaultValue": "false",

View File

@@ -155,6 +155,8 @@ function writeCodeblocks() {
prop('number', 'DiscardSelectTotal', '0'),
prop('number', 'DiscardPostShiv', '0'),
prop('number', 'DiscardShivPerPick', '0'),
prop('number', 'DiscardPostDraw', '0'),
prop('number', 'DiscardDrawPerPick', '0'),
prop('boolean', 'RetainSelectActive', 'false'),
prop('boolean', 'ReserveSelectActive', 'false'),
prop('number', 'NextTurnBlock', '0'),

34
tools/verify/cbprops.mjs Normal file
View File

@@ -0,0 +1,34 @@
// cb/*.mjs의 `self.<Field> = ` 대입 ↔ gen-slaydeck.mjs 선언 prop 대조.
// 미선언 prop에 대입하면 MSW 런타임 "cannot set X, no such field" → 그 후보를 정적 검출.
// 사용: node tools/verify/cbprops.mjs
import { readFileSync, readdirSync } from 'node:fs';
const orch = readFileSync('tools/deck/gen-slaydeck.mjs', 'utf8');
const declared = new Set();
for (const m of orch.matchAll(/prop\(\s*'[^']*'\s*,\s*'([^']+)'/g)) declared.add(m[1]);
// MSW 빌트인/설정으로 대입 가능한 self 필드(프롭 아님) — 오탐 제외 화이트리스트.
const BUILTIN = new Set(['Entity']);
const dir = 'tools/deck/cb';
const files = readdirSync(dir).filter((f) => f.endsWith('.mjs'));
const assigns = new Map(); // name -> Set(files)
for (const f of files) {
const src = readFileSync(`${dir}/${f}`, 'utf8');
// self.Name = (단, == / ~= / .Y= / [..]= 는 제외)
for (const m of src.matchAll(/self\.([A-Za-z_]\w*)\s*=(?!=)/g)) {
const name = m[1];
if (!assigns.has(name)) assigns.set(name, new Set());
assigns.get(name).add(f);
}
}
const missing = [...assigns.keys()]
.filter((n) => !declared.has(n) && !BUILTIN.has(n))
.sort();
console.log(`선언 prop: ${declared.size} | 대입된 self.X distinct: ${assigns.size}`);
console.log(`미선언 대입 (no such field 후보): ${missing.length}`);
for (const n of missing) console.log(` - ${n} [${[...assigns.get(n)].join(', ')}]`);
console.log(missing.length ? 'RESULT: MISSING PROPS ABOVE' : 'RESULT: 모든 self 대입이 선언됨 ✓');
process.exit(missing.length ? 1 : 0);