
前回は、エディタのウインドで何千回も戦わせて、勝率や残りHPの分布を見られるようにしました。最終回の今回は、アセットを書きかえずに数字だけを変えて比べる「上書き」と、ボスのHPを一気に比べる機能を足します。#15で手さぐりで決めたボスのHPを勝率で振り返り、カードの選び方(考え方)でも結果が大きく変わることを確かめて、バランスの決め方をまとめてみたいと思います!
- シリーズの目次:デッキ構築カードゲームの作り方
- 前回:番外編②エディタのウインドで何千回も戦わせよう
今回作るもの
- 上書き:ボスのHP・敵の攻撃(%)・休んで回復するHP。アセットはそのまま
- ボスのHPを40〜120で比べて、勝率を棒グラフに
- 報酬で取ったカードごとの勝率
- 考え方を3つ(ふつう・攻め重視・守り重視)にして比べる
| スクリプト | 直すところ |
|---|---|
| SimSetup | 上書きの数字を持つ |
| SimEnemy・SimBattle・SimRun | 上書きした数字で戦う |
| SimBasicPolicy | 回復・防御のしきい値をプロパティにする |
| SimAggressivePolicy・SimDefensivePolicy | 新しく作る(攻め重視・守り重視) |
| BattleSimulatorWindow | 上書きの欄、考え方、比べる機能、報酬の表 |
上書きできるようにする
数字を変えて試すたびにEnemyDataのアセットを書きかえると、元の数字に戻し忘れることがあります。そこで、シミュレーターの中だけで数字を差しかえる「上書き」を作ります。
SimSetupに上書きの数字を足す
public int startHp = 0; // 0以外なら、その HP から始める
// ★番外編③ アセットを書きかえずに、数字だけ上書きして試す
public int bossHp = 0; // 0以外なら、ボス戦の敵の最大 HP をこの値にする
public int enemyAttackPercent = 100; // 敵の攻撃を何%にするか
- 休んで回復するHPは、もともとある
restHealをそのまま上書きします
SimEnemyとSimBattleで使う
SimEnemyは、最大HPを外から受け取れるようにします。
// ★番外編③ 最大 HP を外から渡せるようにした(上書きするため)
public SimEnemy(EnemyData data, int slot, int maxHp)
{
Data = data;
Fighter = new SimFighter(data.enemyName, maxHp);
Slot = slot;
}
SimBattleは、上書きの条件(SimSetup)も受け取るようにして、ボス戦の敵のHPと、敵の攻撃を変えます。
private SimEnemy[] summoned = new SimEnemy[2]; // 手下の場所(EnemyParty と同じ2か所)
private const int MaxTurns = 200; // 終わらない戦いを止めるための上限
private int attackPercent; // ★番外編③ 敵の攻撃を何%にするか
// ★番外編③ 上書きの条件(setup)も受け取るようにした
public SimBattle(SimFighter player, SimDeck deck, BattleData battle, SimSetup setup)
{
Player = player;
Deck = deck;
attackPercent = setup.enemyAttackPercent;
foreach (EnemyData data in battle.enemies)
{
int maxHp = battle.isBoss && setup.bossHp > 0 ? setup.bossHp : data.maxHp;
Enemies.Add(new SimEnemy(data, -1, maxHp));
}
}
// ★番外編③ 敵の次の攻撃(上書きの割合をかけたもの。弱った状態は入れない)
public int AttackOf(SimEnemy enemy)
{
return enemy.NextAction.attack * attackPercent / 100;
}
- ボスの判定は、#15で作ったBattleDataの
isBossを使います - 敵の攻撃は
AttackOfで、割合をかけた数字にまとめます。考え方(SimBasicPolicy)も同じメソッドを使うので、「上書きした攻撃が来る」と分かって守れます
敵のターンで攻撃するところと、手下を呼ぶところも直します。
if (action.attack > 0)
{
Player.TakeDamage(enemy.Fighter.UseWeaken(AttackOf(enemy))); // ★番外編③ 割合をかけた攻撃
summoned[i] = new SimEnemy(data, i, data.maxHp); // ★番外編③
SimRunでは、SimBattleを作るところでsetupも渡します。
SimBattle battle = new SimBattle(player, deck, setup.battles[i], setup); // ★番外編③ setup も渡す
上書きがすべて「そのまま」(ボスのHP 0・攻撃100%・回復10)のときは、前回までと同じ結果になります。シード1・4で、ゲームと照らし合わせた結果(29ターン・残りHP 12/30ターン・残りHP 5)が変わらないことを確かめておきましょう。
考え方を3つにする
カードの選び方を変えると、同じデッキでも勝率が変わるはずです。ふつうの考え方の「どこで回復するか」「どれぐらいの攻撃なら守るか」を、子クラスで変えられるようにします。
SimBasicPolicyの上の方に、2つのプロパティを足します。
// ★番外編③ 考え方を変えられるように、数字をプロパティにした(子クラスで上書きする)
protected virtual int HealBelowHp => 25; // HP がこれ以下なら回復する
protected virtual int DefendMargin => 5; // 来る攻撃が、今の防御よりこれ以上多ければ守る
Pickの中の数字を、このプロパティに置きかえます。来る攻撃も、上書きした攻撃(AttackOf)で考えるようにします。
if (player.Hp <= HealBelowHp) // ★番外編③
int attack = battle.AttackOf(enemy); // ★番外編③ 上書きした攻撃で考える
incoming += enemy.Fighter.Weakened ? attack / 2 : attack;
}
if (incoming - player.Block >= DefendMargin) // ★番外編③
if (battle.AttackOf(enemy) > battle.AttackOf(best)) // ★番外編③
- 最後のものは、
Strongest(攻撃の一番大きい敵)の中です
virtual:子クラスで中身を上書きしてもいい、という印です(番外編①のSimPolicyで使ったabstractは「子クラスで必ず書く」、virtualは「書いてもいい」)
Simulationフォルダに、2つの考え方を作ります。
// 攻め重視の考え方:HP がかなり減るまで回復せず、大きな攻撃が来るときだけ守る
public class SimAggressivePolicy : SimBasicPolicy
{
protected override int HealBelowHp => 15;
protected override int DefendMargin => 12;
}
// 守り重視の考え方:早めに回復して、少しでも攻撃が来るなら守る
public class SimDefensivePolicy : SimBasicPolicy
{
protected override int HealBelowHp => 35;
protected override int DefendMargin => 1;
}
- 選び方の流れは同じで、数字だけがちがう考え方を、2行の
overrideだけで作れます
ウインドに足す
BattleSimulatorWindowに足していきます。
項目
private int startHp = 0; // 始める HP(0なら満タン)
// ★番外編③ 考え方と上書き
private int policyIndex = 0; // 0:ふつう 1:攻め重視 2:守り重視
private int bossHp = 0; // 0ならデータのまま
private int enemyAttackPercent = 100;
private int restHeal = 10;
private List<Vector2Int> bossSweep = new List<Vector2Int>(); // ボスの HP ごとの勝率(x:HP、y:勝った回数)
Vector2Intは、整数2つの組です。ここでは「ボスのHP」と「勝った回数」の組に使っています
欄とボタン
OnGUIの、始めるHPの下に足します。
startHp = EditorGUILayout.IntSlider("始める HP(0で満タン)", startHp, 0, 60);
policyIndex = EditorGUILayout.Popup("考え方", policyIndex, new[] { "ふつう", "攻め重視", "守り重視" }); // ★番外編③
// ★番外編③ アセットを書きかえずに、数字だけ上書きする
EditorGUILayout.Space();
EditorGUILayout.LabelField("上書き(アセットは変わりません)", EditorStyles.boldLabel);
bossHp = EditorGUILayout.IntSlider("ボスの HP(0でデータのまま)", bossHp, 0, 200);
enemyAttackPercent = EditorGUILayout.IntSlider("敵の攻撃(%)", enemyAttackPercent, 50, 150);
restHeal = EditorGUILayout.IntSlider("休んで回復する HP", restHeal, 0, 30);
「実行」ボタンの下に、比べるボタンと、そのグラフを足します。
if (GUILayout.Button("ボスの HP を 40〜120 で比べる")) // ★番外編③
{
SweepBossHp();
}
DrawBossSweep(); // ★番外編③
負けた戦いの下に、報酬の表を足します。
DrawLostBattles();
DrawRewardStats(); // ★番外編③
実行で、条件と考え方を使う
Runの中の、始める戦い・始めるHPを入れていた2行と、new SimBasicPolicy()を直します。
ApplyConditions(setup); // ★番外編③ 条件と上書きをまとめて入れる
results.Clear();
SimPolicy policy = CreatePolicy(); // ★番外編③
DrawSummaryの上に、メソッドを足します。
// ★番外編③ ウインドで選んだ条件と上書きを、setup に入れる
private void ApplyConditions(SimSetup setup)
{
setup.startBattle = Mathf.Min(startBattle, setup.battles.Count - 1);
setup.startHp = startHp;
setup.bossHp = bossHp;
setup.enemyAttackPercent = enemyAttackPercent;
setup.restHeal = restHeal;
}
// ★番外編③ 選んだ考え方を作る
private SimPolicy CreatePolicy()
{
switch (policyIndex)
{
case 1:
return new SimAggressivePolicy();
case 2:
return new SimDefensivePolicy();
default:
return new SimBasicPolicy();
}
}
// ★番外編③ ボスの HP だけを 40〜120 で変えて、それぞれ runs 回戦わせる
private void SweepBossHp()
{
SimSetup setup = SimMenu.ReadScene();
if (setup == null)
{
return;
}
ApplyConditions(setup);
SimPolicy policy = CreatePolicy();
bossSweep.Clear();
for (int hp = 40; hp <= 120; hp += 8)
{
setup.bossHp = hp;
int wins = 0;
for (int i = 0; i < runs; i++)
{
if (SimRun.Play(setup, firstSeed + i, policy).won)
{
wins++;
}
}
bossSweep.Add(new Vector2Int(hp, wins));
}
}
// ★番外編③ ボスの HP ごとの勝率を、棒グラフにする
private void DrawBossSweep()
{
if (bossSweep.Count == 0)
{
return;
}
EditorGUILayout.Space();
EditorGUILayout.LabelField("ボスの HP ごとの勝率", EditorStyles.boldLabel);
foreach (Vector2Int item in bossSweep)
{
float rate = (float)item.y / runs;
Rect row = EditorGUILayout.GetControlRect();
Rect barArea = new Rect(row.x + 75f, row.y + 2f, row.width - 140f, row.height - 4f);
EditorGUI.LabelField(new Rect(row.x, row.y, 70f, row.height), $"HP {item.x}");
EditorGUI.DrawRect(barArea, new Color(0f, 0f, 0f, 0.15f));
EditorGUI.DrawRect(new Rect(barArea.x, barArea.y, barArea.width * rate, barArea.height), barColor);
EditorGUI.LabelField(new Rect(barArea.xMax + 5f, row.y, 60f, row.height), $"{rate * 100f:F1}%");
}
}
SweepBossHp:ボスのHPを40から8ずつ増やしながら、それぞれruns回ずつ戦わせる(1000回なら全部で11000回)- グラフは、前回の残りHPの棒グラフと同じ描き方です。棒の長さは勝率そのもの(100%で満タン)
「比べる」ボタンは、回数の11倍戦わせます。1000回なら約2秒ですが、回数を10000にすると11万回になり、20秒ぐらいエディタが止まります。まずは1000回で試して、気になるところだけ回数を増やしましょう。
ExportCsvの上に、報酬の表を足します。
// ★番外編③ 報酬で取ったカードごとに、それを取った回の勝率を出す
private void DrawRewardStats()
{
Dictionary<string, int> taken = new Dictionary<string, int>(); // 取った回数
Dictionary<string, int> won = new Dictionary<string, int>(); // そのうち勝った回数
foreach (SimResult result in results)
{
foreach (string card in new HashSet<string>(result.rewards)) // 同じ回で2枚取っても1回と数える
{
taken[card] = taken.TryGetValue(card, out int t) ? t + 1 : 1;
if (result.won)
{
won[card] = won.TryGetValue(card, out int w) ? w + 1 : 1;
}
}
}
if (taken.Count == 0)
{
return;
}
EditorGUILayout.Space();
EditorGUILayout.LabelField("報酬で取ったカードと、その回の勝率", EditorStyles.boldLabel);
List<string> cards = new List<string>(taken.Keys);
cards.Sort((a, b) => taken[b].CompareTo(taken[a])); // 取った回数の多い順
foreach (string card in cards)
{
int wins = won.TryGetValue(card, out int w) ? w : 0;
EditorGUILayout.LabelField(card, $"{wins * 100f / taken[card]:F1}%({taken[card]}回)");
}
}
Dictionary<string, int>:カードの名前ごとに、回数を数える入れ物ですHashSet:同じものを1つにまとめる入れ物。同じ回で同じカードを2枚取っても、1回と数えますSort((a, b) => …):取った回数の多い順に並べかえます

#15のボスを、勝率で振り返ろう
条件は「1戦目から通し・最初のデッキ・ふつうの考え方・1000回ずつ」です。ボスのHPを40〜120で比べるを押すと、約2秒で11000回戦って、HPごとの勝率が出ます。

| ボスのHP | 40 | 56 | 64(今) | 72 | 80 | 96 | 120(最初) | ||
|---|---|---|---|---|---|---|---|---|---|
| 勝率 | 99.0% | 96.3% | ==orange | 92.9%== | 86.0% | 79.0% | 61.9% | ==orange | 36.4%== |
- 最初のHP 120では、勝率は約36%。しかもこれは、攻撃の数字を今の弱めた数字にしたままの結果です。#15の最初は攻撃も強かったので、実際はもっと低かったはずです
- #15では、自分で遊んでみて「勝てない」と感じ、何度か戦わせて120 → 80 → 70 → 64と下げました。シミュレーターで見ると、その判断は数字でも合っていたことが分かります
- HPを8上げるごとに、勝率は5〜9%ずつ下がっていきます。「ボスのHPを1割上げると、勝率がこれぐらい下がる」という感覚がつかめると、調整がぐっと楽になります
正解はありませんが、目安を決めておくと迷いません。
- 自動の考え方(ふつう)は、人より下手なことが多い。人はもう少し勝てる
- 1戦目から通して、ふつうの考え方で90%前後なら「ちゃんと遊べば、ほぼ勝てる」
- 最後のボスで少しドキドキしてほしいので、ボス戦の残りHPの分布が「0〜9」にも少し出るぐらい(前回の棒グラフ)が、ぎりぎりの手応えになります
どの報酬が負けにつながる?
前回、「ボス戦だけならほぼ勝てるのに、通すと7%負ける」ことが分かりました。報酬の表で、取ったカードごとの勝率を見てみます。

- ふでばこの壁(96.2%)・色えんぴつセット(97.4%)を取った回は、勝ちやすい
- えんぴつをけずる(88.7%)・大そうじ(89.4%)・クリップ(88.2%)は、全体(92.9%)より低い
- 付せんメモは36.4%(33回)と、とても低い
ここで大事なのは、「このカードを取ったから負けた」とは、まだ言えないことです。
- 付せんメモは、報酬の優先順に入っていないので、3枚の中に優先順のカードが1枚も無いときにだけ取っています。つまり「いいカードが出なかった回」に取っている(コンパスや修正テープも同じ理由で取っていますが、回数が少ないので、100%という数字はあまり当てになりません)
- 大そうじは、ふつうの考え方が一度も使わないカードです。取っても、山札の中で引いたときに手札を1枚ふさぐだけになります
- 回数が少ない(33回など)ものは、ぶれも大きい
それでも、「考え方が使わないカードは、デッキの重りになる」ことは分かります。デッキ構築ゲームで「取らない」という選択肢があるのは、このためです。
考え方を変えると?
同じ条件(1戦目から通し・最初のデッキ・1000回)で、考え方だけを変えてみました。
| 考え方 | 回復するHP | 守る目安 | 勝率 | |
|---|---|---|---|---|
| ふつう | 25以下 | 攻撃が防御より5以上多い | 92.9% | |
| 攻め重視 | 15以下 | 攻撃が防御より12以上多い | ==orange | 58.2%== |
| 守り重視 | 35以下 | 攻撃が防御より1以上多い | 93.3% |

- 攻め重視は、勝率が大きく下がりました。ボス戦で403回も負けています。6〜11ぐらいの攻撃を防御なしで受け続けて、少しずつHPがけずられていくためです。勝てた回もターン数は平均22.4と短いのですが、残りHPは平均10.8(ふつうは18.2)と、ぎりぎりです
- 守り重視は、ふつうとほとんど変わりません。予告を見て守るゲームなので、「守るべきときに守る」だけで十分ということですね
- つまり、このゲームは「予告を見て守る」ことが勝ち負けを大きく分ける作りになっています。ねらいどおりのバランスです
スライム攻め重視で58%しか勝てないなら、ガンガン攻める人は苦労しそうですね・・・



それがこのゲームの個性なの!もし「攻める遊び方も楽しくしたい」なら、攻撃のカードを強くして、もう一度比べればいいんだよ
動かして確認!
番外編③の動作確認
- 上書きがすべて「そのまま」なら、シード1・4の結果が前回と同じ(ゲームとも同じ)
- ボスのHPを上書きして実行すると、勝率が変わる。EnemyDataのアセットのMax Hpは変わらない
- 「ボスのHPを40〜120で比べる」で、11本の棒グラフが出る。HPが高いほど勝率が下がる
- 報酬の表に、取ったカードと勝率が、取った回数の多い順に並ぶ
- 考え方を「攻め重視」にすると、勝率が大きく下がる



カードや敵の数字を変えたら、また全部比べ直しですか・・・?



そう!でも1000回が0.2秒だから、変えるたびにボタンを押すだけだよ。ゲームを直したら、まずシミュレーターを回すクセをつけよう
番外編のまとめ
| 回 | 作ったもの | 分かったこと |
|---|---|---|
| ① | ルールだけを写したシミュレーター | 同じシードで、ゲームとまったく同じ結果になる |
| ② | エディタのウインド | 1000回が0.2秒。ボス戦だけより、通しの方が負けやすい |
| ③ | 上書き・比べる・考え方 | #15のボスの調整は数字でも合っていた。守ることが勝ち負けを分ける |
次にやってみること
- カードの数字も上書きできるようにする。ダメージの割合などを足せば、カードの強さも比べられます
- 報酬の優先順を変えた考え方を作る。どのカードを取るのが一番強いかを、勝率で探せます
- 新しい敵やカードを足したら、まず1000回回す。作るたびに勝率を見ておくと、強すぎ・弱すぎにすぐ気づけます
あとがき
番外編3回、おつかれさまでした!画面なしで戦いを回すシミュレーターを作り、ゲームと結果が一致することを確かめてから、ウインドで何千回も戦わせ、数字を変えて比べられるようにしました。「遊んでみてなんとなく」だったバランス調整が、勝率という数字で話せるようになると、自信を持ってゲームを仕上げられます。ぜひ、自分で足したカードや敵でも試してみてください!
コメント