ゲームバランスの調整方法|戦闘を1万回自動で戦わせて、勝率を合わせるコード

ゲームのバランス調整は、同じ戦いをプログラムで1万回戦わせて、勝率を数えれば、遊ばなくても進められます。この記事のコードは、ターン制の1対1の戦いを自動で繰り返し、敵のHPを変えながら、目標の勝率に合う値を探します。
コードはNodeで動かして確かめました。数字はすべて、このコードを実行して数えた値です。ゲームのバランスの正解は、ゲームごとに違うので、ここで扱うのは、調整のしかたです。
この記事で分かること
- 戦闘を自動で繰り返して、勝率・ターン数を数えるコード全体
- 敵のHPと勝率の関係(実測)
- 何回戦えば、勝率が安定するか(100回・1,000回・1万回の比較)
- 目標の勝率になる敵のHPを、9回の探索で見つける方法
- どの数字を動かすと、勝率が大きく変わるか
- 確かめられていないこと
ゲームの企画を決める手順は、ゲームの企画書をAIで作る方法にあります。この記事は、企画で決めた数字を、実際に試して調整する段階の内容です。
この記事の目次
バランス調整を数える3段

戦いのルールは、次のとおりです。プレイヤーが先に攻撃し、そのあと敵が攻撃します。攻撃は、命中するかどうかで分かれ、命中したら、攻撃力の80〜120%のダメージです。会心が出ると、ダメージは2倍になります。HPが0になった側が負けです。
コード全体
次のコードを、sim.jsという名前で保存して、「node sim.js」で実行します。ブラウザでの実行は、確かめていません。
// ターン制の1対1バトルを、何度も自動で戦わせて、勝率を数える
// 実行: node sim.js
// 同じ乱数の並びを再現できる、シード付きの乱数(mulberry32)
function makeRng(seed) {
let a = seed >>> 0;
return () => {
a = (a + 0x6D2B79F5) >>> 0;
let t = a;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
// 1回の戦い。攻撃は、命中・ばらつき(±20%)・会心(2倍)で決まる
function attack(atk, acc, crit, rnd) {
if (rnd() >= acc) return 0;
let dmg = atk * (0.8 + rnd() * 0.4);
if (rnd() < crit) dmg *= 2;
return Math.round(dmg);
}
function battle(p, e, rnd) {
let ph = p.hp, eh = e.hp, turns = 0;
while (ph > 0 && eh > 0 && turns < 200) {
turns++;
eh -= attack(p.atk, p.acc, p.crit, rnd); // プレイヤーが先
if (eh <= 0) break;
ph -= attack(e.atk, e.acc, e.crit, rnd);
}
return { win: eh <= 0, turns, left: Math.max(ph, 0) };
}
function simulate(p, e, n, seed) {
const rnd = makeRng(seed);
let wins = 0, turns = 0, left = 0;
for (let i = 0; i < n; i++) {
const r = battle(p, e, rnd);
if (r.win) { wins++; left += r.left; }
turns += r.turns;
}
return { winRate: wins / n, avgTurns: turns / n, avgLeftWhenWin: wins ? left / wins : 0 };
}
// 目標の勝率になる、敵のHPを探す(二分探索。敵が強いほど勝率が下がる前提)
function tuneEnemyHp(p, e, target, n, seed, lo = 10, hi = 400) {
let steps = 0;
while (hi - lo > 1) {
const mid = Math.floor((lo + hi) / 2);
const wr = simulate(p, { ...e, hp: mid }, n, seed).winRate;
steps++;
if (wr > target) lo = mid; else hi = mid;
}
return { hp: lo, steps };
}
if (typeof module !== 'undefined') module.exports = { makeRng, battle, simulate, tuneEnemyHp };
if (typeof require !== 'undefined' && require.main === module) {
const player = { hp: 100, atk: 12, acc: 0.9, crit: 0.1 };
const enemy = { hp: 80, atk: 9, acc: 0.85, crit: 0.05 };
console.log('敵のHPごとの結果(1万回)');
for (const hp of [40, 60, 80, 100, 120, 140]) {
const r = simulate(player, { ...enemy, hp }, 10000, 1);
console.log(hp, '勝率', (r.winRate * 100).toFixed(1) + '%', '平均ターン', r.avgTurns.toFixed(1), '勝ったときの残りHP', r.avgLeftWhenWin.toFixed(1));
}
const hard = { ...enemy, hp: 140 }; // 勝率が7割前後になる敵
console.log('試行回数ごとの、勝率のばらつき(敵HP140・シードを20通りに変えて、最小〜最大)');
for (const n of [100, 1000, 10000]) {
const v = [];
for (let s = 1; s <= 20; s++) v.push(simulate(player, hard, n, s * 7919).winRate);
console.log(n, '回:', (Math.min(...v) * 100).toFixed(1) + '〜' + (Math.max(...v) * 100).toFixed(1) + '%', '幅', ((Math.max(...v) - Math.min(...v)) * 100).toFixed(1) + '点');
}
console.log('目標の勝率になる敵のHPを探す(1万回ずつ)');
for (const target of [0.9, 0.7, 0.5]) {
const t0 = Date.now();
const r = tuneEnemyHp(player, enemy, target, 10000, 1);
const check = simulate(player, { ...enemy, hp: r.hp }, 10000, 99);
console.log('目標', target * 100 + '%', '→ 敵HP', r.hp, '(探索', r.steps, '回・', Date.now() - t0, 'ms)別のシードで確認した勝率', (check.winRate * 100).toFixed(1) + '%');
}
console.log('パラメータを1つずつ10%上げたときの勝率の変化(敵HP140のとき)');
const base = simulate(player, hard, 10000, 1).winRate;
console.log('もとの勝率', (base * 100).toFixed(1) + '%');
for (const [name, p2, e2] of [['プレイヤー攻撃+10%', { ...player, atk: player.atk * 1.1 }, hard], ['プレイヤーHP+10%', { ...player, hp: player.hp * 1.1 }, hard], ['プレイヤー会心率+10%(相対)', { ...player, crit: player.crit * 1.1 }, hard], ['敵の攻撃+10%', player, { ...hard, atk: hard.atk * 1.1 }], ['敵のHP+10%', player, { ...hard, hp: hard.hp * 1.1 }]]) {
const r = simulate(p2, e2, 10000, 1).winRate;
console.log(name, ((r - base) * 100).toFixed(1) + '点');
}
}
乱数は、シード付きのものを使いました。同じシードなら、毎回同じ結果になるので、数字を変えたときの違いを、乱数のぶれと区別して比べられます。
敵のHPと勝率
プレイヤーはHP100・攻撃12・命中90%・会心10%、敵は攻撃9・命中85%・会心5%として、敵のHPを変えながら、1万回ずつ戦わせました。

敵のHPが100までは、勝率は98%以上でした。120で90.0%、140で69.6%と、100を超えると急に下がります。勝ったときの残りHPも、敵のHP40で平均75.6、140で18.3と、大きく減りました。敵のHPを10〜20上げるだけで、体感がかなり変わる範囲があります。
何回戦えば、勝率が安定するか
戦いには乱数が入るので、少ない回数では、勝率がぶれます。敵のHP140(勝率が約7割)で、乱数の並びを20通りに変えて、勝率の最小と最大を比べました。

100回では58.0〜74.0%と、16点も動きました。1,000回で6.0点、1万回で1.5点です。数十回の試し遊びで「勝率7割くらい」と感じても、本当は6割にも8割にもなり得ます。この点が、人が遊んで調整する場合の弱いところです。
目標の勝率に合わせる
敵が強いほど勝率が下がります。この性質を使って、目標の勝率になる敵のHPを、二分探索で探しました。範囲の真ん中を試して、勝率が目標より高ければ、HPを上げる側へ、低ければ下げる側へ絞ります。
- 目標90%:敵HP119(8回の探索)。別のシードで確認すると90.7%
- 目標70%:敵HP139(9回の探索)。確認すると70.3%
- 目標50%:敵HP153(9回の探索)。確認すると51.2%
探索は、どれも約50〜60ミリ秒で終わりました。確認の勝率が目標から最大1.2点ずれているのは、乱数のぶれです。
どの数字を動かすと、勝率が変わるか
敵HP140(勝率69.6%)のときに、数字を1つずつ10%上げて、勝率の増減を比べました。同じシードで比べているので、違いは、数字の変更によるものです。

勝率に大きく効いたのは、敵のHPと攻撃、プレイヤーのHPと攻撃でした。プレイヤーの会心率は、10%(0.10から0.11へ)の上げ幅が小さく、1.6点しか動きませんでした。「会心率を少し上げる強化」は、この設定では、効果が小さいという結果です。この結果は、この戦闘ルールでのものです。ルールが違えば、順番も変わります。
AIにシミュレーションを書かせるときの確認点
このようなコードは、AIに書かせても作れます。出てきたコードで、次の3点を確かめると、数字を信じてよいかが分かります。
- 乱数にシードがあるか。ないと、実行のたびに数字が変わり、比べられません
- 試行回数が十分か。この記事の測定では、勝率が約7割のとき、1万回で1.5点の幅でした
- 比べる実行で、同じシードを使っているか
AIに、このコードを書かせた場合の出来は、確かめていません。
確かめられていないこと
- 実際のプレイヤーの体感との一致勝率が7割になる設定を、人が遊んで「ちょうどよい」と感じるかは、確かめていません。
- 回復・防御・スキルなど、行動の選択がある戦闘(このコードは、毎ターン攻撃だけです)
- 複数の敵や、パーティーでの戦い
- プレイヤーの上手さによる勝率の違い
- ブラウザでの実行と、その速さ
確かめていないものは、動かして確かめてから、別の記事で扱います。
うまくいかないとき
| 症状 | 確認すること |
|---|---|
| 実行するたびに勝率が変わる | シードを固定しているかを確かめます。固定していても、試行回数が少ないと、設定を変えたときのぶれが残ります |
| 勝率が100%か0%ばかり | 敵のHPの範囲が広すぎます。この記事の設定では、100〜150のあたりが変わり目でした |
| 探索が止まらない | 敵が強いほど勝率が下がる前提が崩れていないかを確かめます。ルールを変えると、勝率が単調に下がらない場合があります |
| 数字を変えても勝率が動かない | 勝率が、100%や0%に近すぎないかを見ます。変わり目の近くで比べます |
関連記事ゲームの企画書をAIで作る方法【2026年】テンプレートと仕様書への変え方
よくある疑問
勝率は、何%にすればよいですか
ゲームの狙い次第で、この記事では決められません。コードは、目標の勝率に合う敵のHPを探す道具として使います。
ブラウザで動かせますか
コードに、ブラウザ専用の命令は入っていませんが、ブラウザでは確かめていません。この記事の結果は、Nodeで実行したものです。
プレイヤーの選択が入るゲームは、どうしますか
選択の方針を、プログラムで決めて試す形が考えられます。この記事では、確かめていません。
この記事のまとめ
この記事のまとめ
- 戦闘をコードにして1万回戦わせると、遊ばなくても勝率を数えられる
- 100回では勝率が16点もぶれた。1万回で1.5点だった
- 目標の勝率になる敵のHPは、二分探索で9回の探索で見つかった
- 10%の変更が勝率に効く大きさは、数字ごとに違う。会心率は1.6点だった
- 人が遊んだときの体感との一致は、確かめていない












