ゲームのCPUを「ちょうどよく弱く」する方法
制作ノート / カイテイソナーのCPU開発記
- 強いCPUより、弱いCPUのほうが難しい
- まず「一番強いCPU」を作る
- 失敗した弱くし方:わざと外させる
- 採用した方法:ねらいの鋭さを鈍らせる
- ソナーの扱いで大失敗した話
- 高速艇の逃げ方で無限ループになった話
- 400戦して勝率を測った
- まとめ:弱さの設計は「気持ち」の設計
強いCPUより、弱いCPUのほうが難しい
カイテイソナーという、海の中に隠れた相手の艦隊をソナーで探して撃ち合うゲームを作りました。 もともとは2人でオンライン対戦するゲームだったのですが、「ひとりのときも遊びたい」という声があり、CPU対戦を足すことにしました。
作りはじめる前は、簡単な仕事だと思っていました。ところが実際にやってみると、 強いCPUを作るのは思ったより簡単で、ちょうどよく弱いCPUを作るのが圧倒的に難しいと分かりました。 この記事は、その試行錯誤の記録です。
まず「一番強いCPU」を作る
弱いCPUを作りたいのに、最初にやったのは「最強のCPU」を作ることでした。理由は単純で、 強さの上限が分からないと、どのくらい弱めればいいのかも決められないからです。
カイテイソナーの盤面は格子状になっていて、相手はそこに何隻かの艦を隠しています。 最強のCPUの中身は、ざっくり言うと次のようなものです。
- 確率マップ(各マスに艦がいる可能性を数値にしたもの):残っている艦の「置ける並べ方」を全部数え上げて、どのマスが怪しいかを1マスずつ点数にする
- 命中したら追撃:当たったマスの上下左右を優先して狙う
- ソナーの反応を反映:ソナーを撃つと周囲に何隻いるかが分かるので、その情報を確率マップに反映する
- 潜航でかわす:狙われていそうなときは潜って避ける
- 高速艇を逃がす:位置がバレた高速艇は移動させる
これを組んだ結果、私自身がまったく勝てないCPUができました。ここが出発点です。
失敗した弱くし方:わざと外させる
さて、ここから弱くしていきます。最初に思いついたのは、いちばん素朴な方法でした。 「一定の確率で、わざと見当違いのマスを撃たせる」というものです。
これは、遊んでみるとすぐに違和感がありました。 直前まで的確に追い詰めてきたCPUが、突然まったく関係ない隅っこを撃つ。 人間なら絶対にしない打ち方なので、「弱い」ではなく「バグっている」ように見えるのです。
弱いCPUに求められているのは「ミスをすること」ではなく、 「勝てそうで勝てない、惜しい判断をすること」だと気づきました。
採用した方法:ねらいの鋭さを鈍らせる
最終的に採用したのは、確率マップはそのまま使い、そこから撃つマスを選ぶときの「鋭さ」だけを変える方法です。
確率マップができた時点で、各マスには「ここに艦がいそうな度合い」の点数がついています。 最強のCPUは、この中から必ず一番点数の高いマスを選びます。 弱いCPUは、点数の高いマスほど選ばれやすいけれど、2番目・3番目のマスも選ばれることがあるようにしました。
調整のつまみは aim という値ひとつです。この値が大きいほど、より確実に最善手を選びます。
| やさしい | aim = 0.15/点数の差をほとんど無視して、広く散らして撃つ |
|---|---|
| ふつう | aim = 0.80/有望なマスを優先するが、時々ずれる |
| チャレンジ | aim = 9e9/実質的に必ず最善手を選ぶ |
この方式のいいところは、弱いCPUの打ち方も「筋が通って見える」ことです。 やさしいCPUも、まったくの見当違いではなく「まあ、そこを狙う気持ちは分かる」という場所を撃ちます。 負けたときに理不尽さがなく、勝ったときにも「運が良かっただけ」と感じにくい。
ソナーの扱いで大失敗した話
ここからが、この開発で一番ハマった場所です。
カイテイソナーには「ソナー」という行動があります。3×3の範囲を調べて、その中に敵が何隻いるかが分かる、という仕組みです。 当然、この情報は確率マップに反映すべきです。私は素直にこう考えました。
「反応があったマスの周辺は、点数を上げればいい」
これを実装して、実際に対戦してみたところ、おかしなことが起きました。 一番強いはずの「チャレンジ」が、「ふつう」より弱くなってしまったのです。
原因を突き止めるのに時間がかかりましたが、答えは算数でした。
考えるべきは、絶対数ではなく密度です。 盤面全体で見たとき、1マスあたりに艦がいる割合はあらかじめ決まっています。 これと比べたとき、3×3の9マスに反応が1隻というのは、実は「平均より薄い」。 密度で言えば「このあたりは手薄だ」という情報になっていたのです。
それなのに私は「反応があった=怪しい」と単純に加点していました。 結果として、薄い場所を優先して撃つCPUができあがっていたわけです。 チャレンジはソナーを積極的に使うので、使うほど不利になっていました。
正しくは、こう計算しなければいけませんでした。
(反応した隻数 - すでに分かっている隻数)÷ その範囲の未攻撃マス数 ÷ 盤面全体の密度
つまり「盤面の平均と比べて濃いか薄いか」を出して、濃ければ加点、薄ければ減点する。 これに直したら、チャレンジは期待どおりの強さになりました。
情報を足したのに弱くなる、というのは直感に反します。 「情報を反映する」と「情報を正しく反映する」はまったく別物だと痛感した出来事でした。
高速艇の逃げ方で無限ループになった話
もうひとつ、地味にハマったのが高速艇の扱いです。 高速艇は位置がバレたら逃げられる艦なのですが、最初の実装では 「これまでに撃たれた履歴を見て、危なそうなら逃げる」としていました。
すると、CPUが延々と逃げ続けるようになりました。 履歴は消えないので、一度撃たれた記録が永久に「危険」と判定され続けるのです。 逃げるだけで自分のターンを使い切ってしまい、攻撃をしないCPUが完成しました。
直し方は単純で、「前回逃げたあとに撃たれた攻撃」だけを見るようにしました。 逃げたら過去はリセットする、ということです。
400戦して勝率を測った
「たぶんちょうどいい強さになった」で終わらせると危ないので、 CPU同士を自動で戦わせるテスト用のスクリプトを書いて、各段階400戦ずつ回して勝率を測りました。
| やさしい 🐟 | CPU側の勝率 47%(ほぼ五分。慣れていない人でも勝てる) |
|---|---|
| ふつう 🐬 | CPU側の勝率 65%(何回か遊べば勝てる) |
| チャレンジ 🦈 | CPU側の勝率 82%(作った本人がほぼ勝てない) |
あわせて、無限ループで止まってしまうケースが0件であることも確認しました。 先ほどの高速艇の件があったので、ここは念入りに見ています。
まとめ:弱さの設計は「気持ち」の設計
この開発を通して分かったのは、CPUの強さ調整というのは、 アルゴリズムの問題であると同時に「遊ぶ人がどう感じるか」の問題だということです。
- ランダムに外させると「弱い」ではなく「壊れている」に見える
- 弱いCPUにも、判断の筋が通っている必要がある
- 情報を足せば強くなるとは限らない。正しく足さないと逆効果になる
- 体感に頼らず、実際に何百戦か回して数字で確かめたほうがいい
実際に対戦してみる
この記事に出てきた3段階のCPUは、実際に遊べます。登録もインストールも不要です。 まずは「やさしい」から試してみてください。