6幕・3客席のカードゲームを自作し、シミュレーションで数値を調整した記録
に公開
予算1→6の6幕・3客席のカードゲームを作り、「ただの算数」と言われた v0.1 から v0.3 まで、CPU同士の対戦で測って直した記録です。出せない幕 51%→0%、基本値の割合 80%→62% などの数字つきでまとめます。
ブラウザで遊べるカードゲームを自作し、数値の調整をシミュレーションで行いました。ルールは 12枚のデッキ、6幕、予算は 1→6、3つの客席のうち2つを取れば勝ち、というものです。最初の版は「ただの算数」と言われて不採用になり、v0.2、v0.3 と作り直しました。
- v0.1 は、全員の個性を同じ大きさに揃えたのが失敗の原因だった
- v0.2 で、強さではなく「種類」の違う特技を全員に持たせた
- v0.3 は、CPU同士1,500戦を測った結果に合わせて数値を直した
- 測定後は、出せない幕が 第1幕 51%→0%、最終拍手に占める基本値の割合が 80%→62% になった
何を作りたかったか
作ったのは、推しのメンバーをセンターに置き、他のメンバーのカードも組み合わせて、全体で良いライブを作るカードゲームです。MARVEL SNAP のように、少ない幕数で複数の場所(このゲームでは客席)の取り合いをする形を参考にしました。そこで決めたルールの骨格がこれです。
- デッキは12枚(人物6枚と演出6枚)
- 6幕。第n幕の予算は n で、持ち越しなし
- 客席は3つ。2つ取った側が勝ち
- 人に強弱を付けない。コストと拍手は「役割(開幕〜大トリ)」が決め、誰をどの役割に置くかは編成で選ぶ
最後の点は、キャラクターに強弱を付けると「強い人が人気」という見え方になってしまうのを避けたかったからです。
v0.1:ただの算数、と言われた
v0.1 を実際に遊んでもらった評価は厳しいものでした。「使い方が分からない」「勝ちだと思ったら CPU の勝ち」「キャラそれぞれに特殊効果がない」「ただの算数」「読み合いがない」。
このうち「勝ちだと思ったら CPU の勝ち」は、調べたら判定自体は正しく、900戦で再計算した結果と一致しました。原因は2つありました。
- 第6幕のカードを公開してから0.4秒で結果へ自動遷移し、逆転が見えていなかった。第6幕の前にリードしていた側の38%が、最終幕で逆転負けしており、客席は平均 2.27/3 が入れ替わっていた
- カードに表示した数字に、客席の効果が入っていなかった。5,400か所のうち4,195か所で、カードの数字の合計が客席の数字と合わなかった
画面の数字が合わなければ、勝ちだと思ったのに負けた、と見えるのは当然です。これは数字の表示の問題で、ルールの問題ではありませんでした。
一方、「ただの算数」への答えは、ルール側にありました。人に強弱を付けないために個性を全員同じ大きさ(得意な客席で +2)に揃えたことが原因です。同じ大きさの足し算では、考える余地がありません。私は、これを「再提案しない」案として記録に残しました。
もう1つ、v0.1 の測定では、スターターデッキ(初期デッキ)の偏りも見つかりました。両者が同じ貪欲な CPU(その幕でいちばん得になる手を選ぶ CPU)で各300戦を回すと、「熱狂」ユニットが初見・交流の2デッキに25〜33%しか勝てませんでした。演出カードのペンライトをアンコールへ入れ替えた後は、対初見39%、対交流50%に収まりました。
v0.2:強さではなく、種類の違う特技
v0.2 では、メンバーごとに種類の違う特技を持たせました。強さを変えず、効果の「型」を変える方針です。数値は web/js/data.js の SKILL_VALUES に置いています。
export const SKILL_VALUES = {
m1: { normal: { self: 4 }, center: { self: 7 } },
m3: { normal: { self: 1 }, center: { self: 2 } },
m5: { normal: { others: 2, later: 1 }, center: { others: 3, later: 2 } },
m6: { normal: { enemy: 3 }, center: { enemy: 5 } },
m9: { normal: { draw: 1, self: 1 }, center: { draw: 2, self: 1 } },
// ほかのメンバーも同じ形(normal=通常 / center=センター版)
};
特技の例を挙げます。m1 は「今幕に相手がこの客席へ出していれば自分に加算」で相手を読む特技、m5 は味方への加算をする「ダンスリーダー」型、m6 は相手の最大のカードを減らす「視線泥棒」型、m9 は山札から引く「おねだり」型です。9人で9種類、センターに置くと強化版になります。
特技のほかに、相手への干渉と読み合い、8種類の客席から対戦ごとに3つを選び幕ごとに1つずつ公開する仕組み、勝負宣言と「降りる」、性格を持つライバル(強気・慎重・きまぐれ)も足しました。
v0.2 の測定(CPU同士2,700戦)は、予算の使用率 90%、最終幕の逆転負け 29%(v0.1 は38%)、スターター間の勝率 37〜63% でした。ただし、人が遊んだ時の面白さと偏りは未検証、と記録しています。
v0.3:1,500戦を測って、数値を直す
v0.2 を CPU 同士で1,500戦測ると、次の問題が数字で見えました。
- 出せるカードが1枚も無い幕が、第1幕で 51%、第2幕で 24%
- 最終拍手の 80% が基本値(役割で決まる数字)で、特技の影響が小さい
- 1戦で出るカードは 11.5枚(盤面の24枠のうち)で、盤面がすかすか
- 考える CPU 対 無作為に出す CPU の勝率は 77%
測定は scripts/sim/metrics.mjs で行います。「出せない幕」を数えているのは、各幕の手札に、その幕の予算で出せるカードがあるかを確かめる部分です。
if (!m.sides[side].hand.some(c => c.cost <= m.round)) noPlay[m.round]++;
基本値の割合は、盤面のカード全部について、基本値の合計を最終拍手の合計で割って出しています。
for (const c of m.board[side][v]) {
baseSum += c.base; totalSum += c.base + c.bonus;
}
// 最終拍手に占める基本値の割合 = baseSum / totalSum
比べる相手の「無作為」は、出せる手の中から無作為に選ぶだけの CPU(randomPlays)です。考える側が無作為に何割勝つかは、「選択に意味があるか」を測る物差しになります。スターターの総当たりと、第6幕の前にリードしていた側の逆転率は scripts/sim/balance.mjs が測ります。
v0.3 での直しは4つです。
- 初期手札を4枚にし、センターと開幕のカードを必ず入れる(出せない幕を減らす)
- 基本値を下げ、特技を大きくする(基本値の割合を下げる)
- 演出を軽くして、軽いカードを増やす(盤面を埋める)
- 同じユニットの人物が同じ客席に2人いれば各+1、3人で各+2(箱推しの編成に意味を持たせる)
役割の数値は ROLES に、連携は SYNERGY に置いています。
export const ROLES = [
{ id: 'opener', name: '開幕', cost: 1, base: 1 },
{ id: 'link', name: 'つなぎ', cost: 2, base: 2 },
{ id: 'hype', name: '盛り上げ', cost: 3, base: 3 },
{ id: 'spot', name: '見せ場', cost: 4, base: 4 },
{ id: 'center', name: 'センター', cost: 5, base: 5 },
{ id: 'finale', name: '大トリ', cost: 6, base: 8 },
];
export const SYNERGY = [{ min: 3, bonus: 2 }, { min: 2, bonus: 1 }];
数値を調整するのは data.js のこのブロック(ROLES、SKILL_VALUES、STAGE_VALUES、STAGES、SYNERGY、INITIAL_HAND)だけです。演出カードの数字は、効果文にも入っているので、STAGE_VALUES と文面の両方を直します。
実装は別のエージェントに任せ、数値の調整は私が行いました。たとえば、ルミナスというユニットの開幕とつなぎを入れ替え、独壇場のセンター版を +6 から +5 に下げています。
測定結果
v0.3 にして、同じ測定をもう一度行いました。
| 項目 | v0.2 | v0.3 |
|---|---|---|
| 出せない幕(第1幕) | 51% | 0% |
| 出せない幕(第2幕) | 24% | 4% |
| 最終拍手に占める基本値 | 80% | 62% |
| 1戦で出るカード | 11.5枚 | 14.1枚 |
| 考える側 対 無作為 | 77% | 81% |
| スターター間の勝率 | 37〜63% | 41〜57% |
| 最終幕の逆転負け | 29% | 26% |
(v0.2 の「スターター間」「逆転負け」は2,700戦の測定の値で、他の行は1,500戦の測定の値です。)
あらかじめ決めた目標は、出せない幕が第1幕 5%未満・第2幕 10%未満、基本値の割合 50〜65%、スターター間 40〜60%、逆転負け 20〜35%、そして「1戦で出るカードが16枚以上」でした。16枚だけは未達です。予算の使用率が 95% で、枚数は予算で決まってしまうからです。
残った課題
- 16枚という目標は、予算が枚数を決めるため、今のルールでは届かない
- 「拍手が最大・最小のカード」の判定に連携分を含めるかは、未決
- CPU の読み(煽り 46%、サプライズ 28%)は、まだ調整していない
- カードプールは9人のまま
- 人が遊んだときの面白さ、偏りは測っていない。測れたのは CPU 同士の数字です
もう1つ、実装で踏んだ罠を書いておきます。既存の乱数のシャッフル関数が、randrange(i) で範囲が1つ足りない実装でした。流用せず、正しい Fisher-Yates で書き直しています。また、出せない手札は disabled にせず unplayable というクラスにしました。タップして特技の説明を読めるようにするためです。
まとめ
「ただの算数」と言われた原因は、強弱をなくすために個性を同じ大きさにしたことでした。強さではなく種類を変えること、そして、数字の表示を実際の計算と一致させることで直しました。数値の調整は、決めた目標に対して、CPU同士で何百戦も測ってから動かすと、感覚ではなく数字で進められます。
カードの見せ方の試作は 紙のカードにできない表現の記事 にまとめています。