iOSのボードゲームアプリに、四目並べ「4つ ならべ」を足しました。ルールの部分を Claude Code に作らせたところ、「つよい」CPUが強すぎて、相手がほとんど勝てない状態になりました。強さを勘で決めずに、自動対戦の勝率で決めた手順を書きます。
作りたかったもの
四目並べは、7列6段の盤に上から駒を落として、縦・横・斜めのどれかに4つ並べたら勝ちのゲームです。今回作ったのは画面ではありません。ルールと手の計算だけを持つ部分です。画面と切り離してあるので、遊ぶ前にテストで動きを確かめられます。
中身は4つに分けました。盤、状態、エンジン、CPUです。
盤は、7×6のマスと、勝ち筋が69本あることを持ちます。69本は、4つ並ぶ置き方を数えた数です。横は1段につき4通りで6段ぶんの24通り、縦は1列につき3通りで7列ぶんの21通り、斜めは右上がりと右下がりで24通りあり、合わせて69通りになります。勝ち負けの判定は、この69本を見るだけで済みます。
状態は、どこに何が置かれたかを持ちます。列に駒を落とす、まだ置ける列はどれか、最後に置いた場所はどこか、の3つです。エンジンは、駒を落として勝ちと引き分けを判定し、置けない列に落とそうとしたらエラーを出します。CPUは、次の一手を決めます。
次の図は、その4つがどうつながっているかを表しています。
flowchart LR A["盤(7×6・勝ち筋69本)"] --> B["状態(どこに何が置かれたか)"] B --> C["エンジン(落とす・勝ち負けの判定)"] C --> D["CPU(やさしい/つよい)"] D -- "選んだ列" --> C
AIへの頼み方
作業は Claude Code に頼みました。私がやったのは、作る前に「どうなったら完成か」を決めることです。
決めた条件は6つです。縦・横・斜めの4方向で勝ちを判定できること。満杯の列に落としたらエラーになること。盤が埋まったら引き分けになること。勝負がついたあとは置けないこと。やさしいCPUが、勝てる手を見つけ、相手の勝ちを止められること。そして、つよいCPUの「先読みの深さ」を、根拠つきで決めてあること。
最後の1つを条件に入れたのが、今回いちばん効きました。ただ「強いCPUを作って」と頼むと、どれくらい強いのかが誰にも分からないまま残るからです。ユニットテスト(部品ごとの動きを自動で確かめる小さなテスト)も、同じタイミングで書かせました。あとからテストを足すより、条件を先に渡したほうが、何を確かめたいかがそのままテストになります。
CPUは2種類あります。やさしいCPUは、自分が勝てる手があれば置き、相手が次に勝つ手があれば止め、どちらもなければ残った列からランダムに選びます。
つよいCPUはミニマックスという考え方を使います。相手もいちばん良い手を指してくる前提で、先まで読むやり方です。ただし最後まで読むと時間がかかります。そこで、決めた手数まで読んだらそこで止めます。止めた時点の盤を点数にして、いちばん点の高い手を選びます。点数は「並びかけ」、つまり4つ並ぶ途中まで来ている形が、どれだけできているかで付けます。この「何手先まで読むか」が、先読みの深さです。
つまずいたところ
できあがったつよいCPUは、想定より強くなりました。深さを2以上にすると、相手が勝てるのは1%以下でした。100局やって1局勝てるかどうかです。これでは手加減の選択肢としては使えません。
やっかいなのは、この「強すぎる」が、テストでは不合格にならない点です。勝ち負けの判定や反則の検出は、正しいか正しくないかがはっきりしています。CPUの強さはそうではありません。深さをいくつにするかは、コードが間違っているかどうかの話ではなく、遊ぶ人が勝てる見込みを持てるかどうかの話だからです。テストを何件足しても、この線引きは出てきません。
もう1つ、四目並べとは別のところでつまずきました。全部のテストを走らせたら、以前作ったマインスイーパー側のテストが1件落ちました。「もういっかい」で最初からやり直せるかを見るテストです。これは乱数(毎回ちがう数を出す仕組み)に結果が左右される種類の失敗です。マインスイーパーを作っていた前のタスクでも、同じ形で落ちていました。
どう判断したか
深さは、勘ではなく実測で決めることにしました。CPU同士で自動対戦させて、相手がどれくらい勝てるかを数えるやり方です。自動対戦なので、相手も人ではなくCPUです。深さ1から4まで、それぞれ400局ずつ戦わせました。合わせて1600局で、かかった時間は約6分です。
結果、深さ1では相手の勝ちが17.25%、深さ2以上では1%以下でした。採用したのは深さ1です。5〜6局に1局は勝てるくらいが、「つよい」の手加減として残せると考えました。
深さ1にしても、やさしいCPUと同じにはなりません。やさしいCPUは、勝てる手と止める手がないときはランダムに選びます。つよいCPUは、そういうときも全部の列を点数で比べ、4つ並びに近づく手を選びます。運任せの部分がない、という違いです。
数字はコードのコメントに書き残しました。あとから「なぜ2ではなく1なのか」と聞かれたときに、答えられるようにするためです。コメントに理由を書いておくと、次に触るのが私でもAIでも、同じ判断をやり直さずに済みます。
計測そのものも、テストの形で残しました。ただし、ふだんのテストでは動かない指定を付けてあります。Swift のテストには .disabled という指定があり、付けたテストは実行されません。6分かかるものを毎回動かすと、テスト全体が重くなるからです。深さの値を変えたくなったときは、この指定を一時的に外せば、同じ条件で測り直せます。
マインスイーパーの失敗は、今回は直しませんでした。四目並べのコードに触れていない部分で、原因も別(乱数の扱い)だからです。ここで一緒に直すと、どちらの変更でテストが通るようになったのかが分からなくなります。この記事の時点では落ちたままで、別のタスクとして残してあります。
次の図は、深さを決めるまでの流れです。
flowchart TD A["深さ1〜4を用意する"] --> B["深さごとに400局を自動対戦"] B --> C["相手が勝った割合を数える"] C -- "深さ2〜4:1%以下(強すぎる)" --> D["採用しない"] C -- "深さ1:17.25%" --> E["深さ1を採用し、数字をコメントに残す"]
できたもの
ユニットテストは全部で138件になりました。今回増えたのは28件で、内訳はエンジンが15件、CPUが13件です。CPUの13件のうち1件が、さきほどの計測用で、ふだんは動きません。
iPhone 16 のシミュレータ(Mac の上で iPhone の動きを再現する仕組み)で走らせ、四目並べのテストはすべて通りました。落ちたのは、さきほどのマインスイーパーの1件だけです。
数字で言えば、つよいCPUを相手にしたとき、相手の勝ちは17.25%です。手加減の具合が、感想ではなく数字で説明できる状態になりました。深さを変えたくなっても、同じ測り方でやり直せます。
まとめ
- CPUの強さのように、正解がひとつに決まらないものは、テストでは合否を出せません。代わりに、自動対戦のような測り方を先に決めておくと判断できます。
- AIに作らせるときは、「強いCPU」ではなく「深さの根拠をコメントに残す」まで完了の条件に入れると、あとから理由をたどれます。
- 時間のかかる計測は、ふだん動かないテストとして残しておくと、条件を変えて測り直すときにそのまま使えます。
- 別の場所で落ちたテストは、同じ作業で一緒に直さず、切り離して残します。
次は、この四目並べを画面から遊べるようにします。
参考にした資料
この記事は、筆者の開発記録をもとにAI(Claude)が下書きし、筆者が確認して公開しています。