import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mulberry32, applyDamage, chooseAction, chooseTarget, simulateCombat, runBatch, } from './sim-balance.mjs'; test('applyDamage: 방어 우선 차감 후 hp', () => { assert.deepEqual(applyDamage(80, 0, 10), { hp: 70, block: 0 }); assert.deepEqual(applyDamage(80, 5, 10), { hp: 75, block: 0 }); assert.deepEqual(applyDamage(80, 12, 10), { hp: 80, block: 2 }); assert.deepEqual(applyDamage(3, 0, 10), { hp: 0, block: 0 }); }); test('mulberry32: 동일 시드 동일 수열', () => { const a = mulberry32(1), b = mulberry32(1); assert.equal(a(), b()); assert.equal(a(), b()); }); const CARDS = { Strike: { name: '타격', cost: 1, kind: 'Attack', damage: 6 }, Defend: { name: '방어', cost: 1, kind: 'Skill', block: 5 }, Bash: { name: '강타', cost: 2, kind: 'Attack', damage: 10 }, }; test('chooseAction: 공격을 스킬보다 먼저 선택', () => { const idx = chooseAction(['Defend', 'Strike'], CARDS, 3); assert.equal(idx, 1); // Strike }); test('chooseAction: 공격 없으면 스킬 선택', () => { const idx = chooseAction(['Defend'], CARDS, 3); assert.equal(idx, 0); }); test('chooseAction: 사용 가능 카드 없으면 -1', () => { const idx = chooseAction(['Bash'], CARDS, 1); assert.equal(idx, -1); }); test('chooseTarget: 이번 타격으로 처치 가능한 최소 체력 우선', () => { const mob = [ { hp: 20, block: 0, alive: true }, { hp: 5, block: 0, alive: true }, { hp: 8, block: 0, alive: true }, ]; assert.equal(chooseTarget(mob, 6), mob[1]); // 5<=6 처치 가능, 최소 }); test('chooseTarget: 처치 불가면 유효체력 최소 선택', () => { const mob = [ { hp: 20, block: 0, alive: true }, { hp: 12, block: 5, alive: true }, { hp: 14, block: 0, alive: true }, ]; assert.equal(chooseTarget(mob, 6), mob[2]); // 유효 14 < 17 < 20 }); const DATA = { cards: CARDS, starterDeck: ['Strike', 'Strike', 'Strike', 'Strike', 'Strike', 'Defend', 'Defend', 'Defend', 'Defend', 'Bash'], monsters: [ { name: '주황버섯', maxHp: 16, intents: [{ kind: 'Attack', value: 5 }, { kind: 'Defend', value: 4 }] }, { name: '파란버섯', maxHp: 12, intents: [{ kind: 'Attack', value: 8 }] }, ], }; test('simulateCombat: 결정적 결과(동일 시드)', () => { const r1 = simulateCombat(DATA, mulberry32(1)); const r2 = simulateCombat(DATA, mulberry32(1)); assert.deepEqual(r1, r2); assert.equal(typeof r1.win, 'boolean'); assert.ok(r1.turns >= 1); }); test('simulateCombat: 모든 몬스터 처치 시 승리', () => { let wins = 0; for (let i = 0; i < 50; i++) if (simulateCombat(DATA, mulberry32(i + 1)).win) wins++; assert.ok(wins >= 40, `예상 승리 다수, 실제 ${wins}/50`); }); test('simulateCombat: 강한 다수 적이면 패배 가능', () => { const hard = { cards: CARDS, starterDeck: DATA.starterDeck, monsters: Array.from({ length: 4 }, () => ({ name: '슬라임', maxHp: 60, intents: [{ kind: 'Attack', value: 12 }] })), }; let losses = 0; for (let i = 0; i < 30; i++) if (!simulateCombat(hard, mulberry32(i + 1)).win) losses++; 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); assert.ok(r.winRate >= 0 && r.winRate <= 1); assert.ok(r.avgTurns > 0); assert.ok(r.cardStats.Strike.plays > 0); }); test('runBatch: 동일 시드 동일 결과', () => { assert.deepEqual(runBatch(100, 7), runBatch(100, 7)); }); test('simulateCombat: 복합 카드(공격+방어) 블록이 적 공격을 흡수', () => { const data = { cards: { Combo: { name: '콤보', cost: 1, kind: 'Attack', damage: 1, block: 3 } }, starterDeck: ['Combo', 'Combo', 'Combo', 'Combo', 'Combo'], monsters: [{ name: '적', maxHp: 9999, intents: [{ kind: 'Attack', value: 9 }] }], }; const r = simulateCombat(data, mulberry32(1)); // 매 턴 3장(에너지3) → 블록 9 = 적 공격 9 전부 흡수 → 무피해로 MAX_TURNS 도달(draw), HP 유지. // 블록 미적용이면 매턴 -9로 사망(win=false, draw 아님). assert.equal(r.draw, true); assert.equal(r.playerHpRemaining, 80); });