From de238294391d0fbf695373891dee607131715fa2 Mon Sep 17 00:00:00 2001 From: gahusb Date: Wed, 10 Jun 2026 00:57:53 +0900 Subject: [PATCH] =?UTF-8?q?fix(sim):=20=EB=B9=88=20=EC=9D=B8=EC=B9=B4?= =?UTF-8?q?=EC=9A=B4=ED=84=B0=20=EC=A6=89=EC=8B=9C=20=EC=8A=B9=EB=A6=AC?= =?UTF-8?q?=C2=B7=ED=83=80=EA=B2=9F=20=ED=83=80=EC=9D=B4=EB=B8=8C=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=ED=81=AC=20=EA=B2=B0=EC=A0=95=EC=84=B1=C2=B7=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=C2=B7draw/empty=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/balance/sim-balance.mjs | 7 +++++-- tools/balance/sim-balance.test.mjs | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/tools/balance/sim-balance.mjs b/tools/balance/sim-balance.mjs index 69a2573..b5a244f 100644 --- a/tools/balance/sim-balance.mjs +++ b/tools/balance/sim-balance.mjs @@ -52,6 +52,8 @@ export function loadData() { return { cards: cardsData.cards, starterDeck: cardsData.starterDeck, monsters }; } +// 주의: 인게임은 플레이어가 카드를 직접 선택한다. 이 chooseAction은 밸런스 추정용 자동 플레이 휴리스틱일 뿐 +// 이며, Lua에 대응 AI가 없다(동기화 대상은 데미지/방어/의도/승패 규칙이지 플레이어 선택이 아님). // 손패에서 낼 카드 인덱스(-1=종료). 공격 우선, 없으면 스킬. export function chooseAction(hand, cards, energy) { const entries = hand.map((id, i) => ({ id, i })).filter((x) => cards[x.id].cost <= energy); @@ -70,7 +72,7 @@ export function chooseTarget(aliveMonsters, plannedDamage) { const eff = (m) => m.hp + m.block; const killable = aliveMonsters.filter((m) => eff(m) <= plannedDamage); const pool = killable.length ? killable : aliveMonsters; - return pool.slice().sort((a, b) => eff(a) - eff(b))[0]; + return pool.slice().sort((a, b) => eff(a) - eff(b) || pool.indexOf(a) - pool.indexOf(b))[0]; } function bump(s, cost, dmg, blk) { @@ -83,6 +85,7 @@ function bump(s, cost, dmg, blk) { // 반환: { win, turns, playerHpRemaining, draw? } export function simulateCombat(data, rng, stats) { const { cards, starterDeck, monsters } = data; + if (monsters.length === 0) return { win: true, turns: 0, playerHpRemaining: PLAYER_HP }; let drawPile = shuffle(starterDeck, rng); let discard = []; let hand = []; @@ -128,7 +131,7 @@ export function simulateCombat(data, rng, stats) { discard.push(...hand); hand = []; for (const m of mob) { if (!m.alive) continue; - m.block = 0; + m.block = 0; // 매 턴 초기화 (이전 턴 블록 미이월) const it = m.intents[m.intentIdx]; if (it) { if (it.kind === 'Attack') { const r = applyDamage(pHp, pBlock, it.value); pHp = r.hp; pBlock = r.block; } diff --git a/tools/balance/sim-balance.test.mjs b/tools/balance/sim-balance.test.mjs index ed6f220..1866805 100644 --- a/tools/balance/sim-balance.test.mjs +++ b/tools/balance/sim-balance.test.mjs @@ -90,6 +90,23 @@ test('simulateCombat: 강한 다수 적이면 패배 가능', () => { assert.ok(losses >= 1, `강한 적엔 패배가 나와야 함, 실제 패 ${losses}/30`); }); +test('simulateCombat: 턴 상한 초과 시 draw 반환', () => { + const immortal = { + cards: { Defend: { name: '방어', cost: 1, kind: 'Skill', block: 5 } }, + starterDeck: Array(10).fill('Defend'), + monsters: [{ name: '불사', maxHp: 9999, intents: [{ kind: 'Attack', value: 1 }] }], + }; + const r = simulateCombat(immortal, mulberry32(1)); + assert.equal(r.draw, true); + assert.equal(r.win, false); +}); + +test('simulateCombat: 몬스터 없으면 즉시 승리', () => { + const r = simulateCombat({ cards: {}, starterDeck: [], monsters: [] }, mulberry32(1)); + assert.equal(r.win, true); + assert.equal(r.turns, 0); +}); + test('runBatch: 집계 필드·승률 범위', () => { const r = runBatch(100, 1); assert.equal(r.N, 100);