feat(sim): 전투 규칙을 멀티 몬스터로 (타겟 선택·각자 의도·전체 처치 승리)

This commit is contained in:
2026-06-10 00:52:17 +09:00
parent b14b614d94
commit 4ef3d1811d
2 changed files with 87 additions and 51 deletions

View File

@@ -1,7 +1,7 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
mulberry32, applyDamage, chooseAction, simulateCombat, runBatch,
mulberry32, applyDamage, chooseAction, chooseTarget, simulateCombat, runBatch,
} from './sim-balance.mjs';
test('applyDamage: 방어 우선 차감 후 hp', () => {
@@ -23,34 +23,46 @@ const CARDS = {
Bash: { name: '강타', cost: 2, kind: 'Attack', damage: 10 },
};
test('chooseAction: 치사 가능하면 공격 선택', () => {
const idx = chooseAction(['Strike', 'Defend'], CARDS, 3, 5, 0, { kind: 'Attack', value: 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: 치사 불가 + 적 공격 의도면 방어 선택', () => {
const idx = chooseAction(['Strike', 'Defend'], CARDS, 3, 40, 0, { kind: 'Attack', value: 10 });
assert.equal(idx, 1);
});
test('chooseAction: 적 방어 의도면 공격 우선', () => {
const idx = chooseAction(['Defend', 'Strike'], CARDS, 3, 40, 0, { kind: 'Defend', value: 8 });
assert.equal(idx, 1);
});
test('chooseAction: 사용 가능 카드 없으면 -1', () => {
const idx = chooseAction(['Bash'], CARDS, 1, 40, 0, { kind: 'Attack', value: 10 });
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'],
enemy: {
name: '슬라임', maxHp: 45, intents: [
{ kind: 'Attack', value: 10 }, { kind: 'Attack', value: 6 }, { kind: 'Defend', value: 8 },
],
},
monsters: [
{ name: '주황버섯', maxHp: 16, intents: [{ kind: 'Attack', value: 5 }, { kind: 'Defend', value: 4 }] },
{ name: '파란버섯', maxHp: 12, intents: [{ kind: 'Attack', value: 8 }] },
],
};
test('simulateCombat: 결정적 결과(동일 시드)', () => {
@@ -61,12 +73,23 @@ test('simulateCombat: 결정적 결과(동일 시드)', () => {
assert.ok(r1.turns >= 1);
});
test('simulateCombat: 약한 적이면 대체로 승리', () => {
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('runBatch: 집계 필드·승률 범위', () => {
const r = runBatch(100, 1);
assert.equal(r.N, 100);