From 4d8fa0f40f5face10a50f657c85d5ddfa1c0f7fa Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 30 Jun 2026 17:11:11 +0900 Subject: [PATCH] =?UTF-8?q?docs(rules):=20=EC=B9=B4=EB=93=9C=20kind?= =?UTF-8?q?=E2=86=94=ED=9A=A8=EA=B3=BC=20=EA=B7=9C=EC=B9=99(=C2=A79)+?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=EB=8F=84=EA=B5=AC=20+=20codex=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=20=EB=B3=B4=EA=B0=95=20(=ED=98=91=EC=97=85=EC=9E=90?= =?UTF-8?q?=20=ED=95=98=EB=84=A4=EC=8A=A4=20=EB=B0=98=EC=98=81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이번 세션에서 발견·수정한 하네스 학습을 저장소(공유 매개)에 반영해 협업자(codex 등)도 적용받게 한다. 메모리는 로컬이라 공유 안 됨. - RULES.md §9 신설 (카드 kind ↔ 효과 일치): ResolveCardDrop 라우팅 (Attack=몬스터드롭/Skill·Power=스윕/Status=unplayable)·Power 분기가 damage/aoe 무시 → 데미지=Attack, block/유틸=Skill, 지속효과=Power. 안 맞으면 사용불가/死카드(아이언 바디·분노 사고). - tools/verify/cardkinds.mjs 신설: kind↔효과 위반(Attack-무데미지/ Power-무효과/미지원 kind) 정적 검출(이상 0=exit 0). 현재 main 147장 0, Defend=Attack·Rage=Power 위반은 2건 검출 확인. - docs/codex-working-rules.md 6~9 추가: ⑥ main 머지 충돌 시 머지 전체 revert 금지(소스 충돌만 해소·산출물 재생성 — #98/#99가 #96 날린 사고) ⑦ 카드 kind 일치+cardkinds 검증 ⑧ 변경 후 검증 스위트 ⑨ RULES.md 권위. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01UUvHKjrt8jqLzDeCsRRGmj --- RULES.md | 9 ++++++++ docs/codex-working-rules.md | 4 ++++ tools/verify/cardkinds.mjs | 42 +++++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 tools/verify/cardkinds.mjs diff --git a/RULES.md b/RULES.md index 1a4cb02..9ea2832 100644 --- a/RULES.md +++ b/RULES.md @@ -94,3 +94,12 @@ grep -c "CalcPlayerAttack" RootDesk/MyDesk/SlayDeckController.codeblock - cb(`tools/deck/cb/*.mjs`)의 Lua 지역변수는 **의미가 드러나는 이름**으로 작성한다(`e`→`entity`, `n`→`count`, `m`→`monster`, `lp`→`localPlayer`, `s`→`soulPoints`, `tr`→`transform`). `a`/`b`/`c` 같은 무의미 단일문자 변수는 금지. - 단, 순수 반복 인덱스 `i`/`j`/`r`/`c`는 관용상 허용한다. - 새 cb 메서드를 작성하거나 기존 메서드를 손댈 때 이 규칙을 적용한다(대규모 일괄 개명은 별도 작업으로). + +## 9. 카드 데이터 규칙 (kind ↔ 효과 일치) + +새 카드를 추가/수정할 때 `data/cards.json`의 `kind`는 카드의 효과·사용 메커니즘과 **반드시 일치**해야 한다. 안 맞으면 카드가 **사용 불가**거나 **재생 시 아무 효과 없는 死카드**가 된다(런타임 에러도 안 나고 sim 테스트도 못 잡음 — 정적 검증 필수). + +- **`ResolveCardDrop` 사용 라우팅이 kind별로 다름**: `Attack`=몬스터 위에 드롭(`FindMonsterAtTouch>0` 필요)·`Skill`/`Power`=위로 스윕(`ui.y>-180`)·`Status`=unplayable. → **block·디버프·드로우 등 유틸만 있고 데미지가 없는 카드를 `Attack`으로 두면 위로 스윕으로 사용할 수 없다**(2026-06-30 아이언 바디 사고: block만 있는 방어카드가 Attack이라 전사 시작덱 4장이 먹통 → Skill로 수정). +- **`PlayCard`의 `Power` 분기는 PlayerPowers 등록만 하고 `damage`/`aoe`를 무시**한다. → 데미지 카드=`Attack`, 방어/유틸=`Skill`, 지속효과=`Power`(단 `powerEffect` 또는 지속/온플레이 power 필드 — `turnStart*`·`dex`·`thorns`·`intangible`·`attackPoison`·`drawDamage`·`shivX`·`cardPlayed*` 등 — 이 있어야 함). Power인데 power 효과 필드가 없으면 死카드(2026-06-30 분노 사고: `damage:4/aoe`만 있어 Power 분기서 무시됨 → kind Power→Attack으로 기능화). +- 새 효과 필드는 `docs/card-effect-fields.md` 사전에 등록하고 Lua(`tools/deck/cb/*.mjs`) + JS 미러(`tools/balance/sim-balance.mjs`) **양쪽에 핸들러 구현**(§6). 한쪽만 있으면 게임↔시뮬 드리프트. +- **검증: `node tools/verify/cardkinds.mjs`** — kind↔효과 위반(Attack-무데미지 / Power-무효과 / 미지원 kind)을 정적 검출(이상 0 = exit 0). 카드 추가/수정 후 반드시 실행. (관련 가드: 미선언 `self.X` = `cbprops.mjs`, UI 경로 = `cbgap.mjs`, 이중구현 = `sim-balance.test.mjs`.) diff --git a/docs/codex-working-rules.md b/docs/codex-working-rules.md index 9d383b4..4c4fdde 100644 --- a/docs/codex-working-rules.md +++ b/docs/codex-working-rules.md @@ -5,3 +5,7 @@ 3. 전직 구조를 바꿀 때는 실제 직업명만 사용한다. 임의의 내부 분류명이나 새 직업명을 사용자-facing 구조에 추가하지 않는다. 4. 대량 치환 전에 수정 대상 파일과 범위를 먼저 확인하고, 원본 문자열이 깨진 상태면 치환 작업을 진행하지 않는다. 5. 생성기 파일을 크게 수정할 때는 `node --check`와 생성기 실행으로 문법을 먼저 검증한 뒤 산출물을 갱신한다. +6. 작업 브랜치에 `main`을 머지했다가 충돌·문제가 나도 **그 머지 커밋을 통째로 `git revert`하지 않는다** — main에 먼저 들어간 타인 작업이 collateral로 사라진다(2026-06-30 `#98/#99`가 `#96`의 버그수정 11개를 이렇게 전부 날림). 소스 충돌만 해소하고 산출물(codeblock 등)은 재생성한다. (RULES §4) +7. 카드 `kind`는 효과와 일치시킨다 — 데미지 카드=`Attack`, block·유틸만 있으면=`Skill`, 지속효과=`Power`(`powerEffect` 또는 power 필드 필수). 안 맞으면 사용 불가/死카드가 된다(2026-06-30 아이언 바디=Attack인데 block만, 분노=Power인데 damage만 → 둘 다 먹통). 카드 추가/수정 후 `node tools/verify/cardkinds.mjs`로 검증(이상 0 = exit 0). (RULES §9) +8. 카드/cb 변경 후 검증 스위트를 돌린다: `node tools/verify/cardkinds.mjs`(kind↔효과)·`cbprops.mjs`(미선언 `self.X` 필드)·`cbgap.mjs`(UI 경로) + `node --test tools/balance/sim-balance.test.mjs`(이중구현 미러). 새 효과 필드는 Lua(`cb/*.mjs`)와 JS 미러(`tools/balance/sim-balance.mjs`) **양쪽**에 구현(한쪽만 = 게임↔시뮬 드리프트). (RULES §6) +9. 하네스 규칙의 권위는 `RULES.md`다 — 작업 전 RULES.md(§1 산출물 읽기/수정 금지·§4 git/PR·§6 이중구현 동기화·§9 카드 kind)를 읽고 따른다. diff --git a/tools/verify/cardkinds.mjs b/tools/verify/cardkinds.mjs new file mode 100644 index 0000000..232da53 --- /dev/null +++ b/tools/verify/cardkinds.mjs @@ -0,0 +1,42 @@ +// 카드 kind ↔ 효과 정합성 정적 검사 (협업자/codex가 카드 추가 후 실행). +// 배경(2026-06-30): kind가 효과와 안 맞으면 카드가 사용불가/死카드가 된다. +// - ResolveCardDrop 라우팅: Attack=몬스터 위 드롭(FindMonsterAtTouch>0 필요) / Skill·Power=위로 스윕 / Status=unplayable. +// → block·유틸만 있고 데미지 없는 카드를 Attack으로 두면 위로 스윕으로 못 쓴다(아이언 바디 사고). +// - PlayCard의 Power 분기는 PlayerPowers 등록만 하고 damage/aoe를 무시한다. +// → Power인데 powerEffect도 power필드도 없으면 재생 시 아무 효과 없는 死카드(분노 사고). +// 사용: node tools/verify/cardkinds.mjs (이상 0 → exit 0, 있으면 목록 + exit 1) +import { readFileSync } from 'node:fs'; + +const cards = JSON.parse(readFileSync('data/cards.json', 'utf8')).cards; + +// Power 카드를 실제로 기능하게 하는 필드(powerEffect 지속효과 + 온플레이/지속 power 필드). +// damage/aoe/block 같은 Attack/Skill 전용 필드는 Power 분기서 무시되므로 제외. +const POWER_FIELDS = [ + 'powerEffect', 'strength', 'dex', 'thorns', 'intangible', + 'turnStartShiv', 'turnStartDraw', 'turnStartDiscard', + 'shivDamageBonus', 'firstShivDamageBonus', 'shivRetain', 'shivAoe', + 'attackPoison', 'drawDamage', 'drawPoison', 'attackDamageVsWeakMultiplier', + 'cardPlayedBlock', 'cardPlayedDamage', 'cardPlayedRandomDamage', + 'extraPoisonTicks', 'poisonApplicationBurstEvery', 'poisonApplicationBurstDamage', + 'skillSlyOnPlay', 'endTurnDexLoss', +]; +const VALID_KINDS = ['Attack', 'Skill', 'Power', 'Status']; + +const issues = []; +for (const [id, c] of Object.entries(cards)) { + if (!VALID_KINDS.includes(c.kind)) { + issues.push(`${id}(${c.name}): 미지원 kind="${c.kind}"`); + continue; + } + if (c.kind === 'Attack' && c.damage == null && c.xDamagePerEnergy == null) { + issues.push(`${id}(${c.name}): kind=Attack인데 damage 없음 → 몬스터 드롭 라우팅 불가(방어/유틸이면 kind=Skill)`); + } + if (c.kind === 'Power' && !POWER_FIELDS.some((f) => c[f] != null)) { + issues.push(`${id}(${c.name}): kind=Power인데 power효과 없음(死카드) → damage/aoe는 Power 분기서 무시, kind 재검토`); + } +} + +console.log(`카드 ${Object.keys(cards).length}장 kind↔효과 정합성: 이상 ${issues.length}`); +for (const i of issues) console.log(' ⚠️ ' + i); +console.log(issues.length ? 'RESULT: 정합성 위반 (위 카드 kind 수정 필요)' : 'RESULT: 모든 카드 kind↔효과 일치 ✓'); +process.exit(issues.length ? 1 : 0); -- 2.49.1