第3章 プロトタイプの作り方/4 本目 / 全 5/約30分

コンピュータの手を作る

こより

相手がでたらめだと読む意味がないよ。たまに間違えるくらいが、いちばん遊べる相手になる。

案内役 こより
この術でできるようになること

相手が自分で手を選ぶ

なぜ要るか

ここまでのゲームで、相手はどう札を選んでいるか知っていますか。でたらめです。

作られたコードを見ると、こうなっています。

var cpuCard = cpuHand[Math.floor(Math.random() * cpuHand.length)];
var cpuTarget = TARGETS[Math.floor(Math.random() * TARGETS.length)];

この2行は、手札から1枚と的から1つを、それぞれくじ引きで決めています。Math.random() がでたらめな数を作る仕組みで、その数を使って並びの中から1つ抜き出しているだけです。

札もランダム、置く的もランダム。何も考えていません。

これで遊べはします。でも企画で決めた面白さは、こうでした。

どの的を捨てて、どの的を取るかを決める。

相手がでたらめなら、捨てる判断に意味がありません。 どこを守っても、相手はそこを見ていないからです。芯が成立していないわけです。だからここを作ります。

そしてこれは、AIに任せると一番差が出る作業でもあります。「相手の思考」は自分で書くと大変ですが、頼めば作ってくれます。

AIプロンプト

この術の作業をAIへ任せるときは、これをそのまま貼ってください。

index.html の相手の手を、ランダムではなく考えて選ぶようにして。相手が出せる手は「どの札を、どの的に置くか」の組み合わせ全部。まずその一覧を作る関数と、1つの手に点数を付ける関数に分けて。点数は、その手を打った後にリードできる的の数を第一に、無駄な上積みを減らす方向で。強すぎて勝てなくなるのは困るので、たまに読み負ける程度にして。何をどう変えたか、選び方の考えも説明して。

出てきたら3回遊んで、強すぎたら「相手が強すぎるので、わざと間違える確率を上げて。」と頼み直してください。

「強い」ではなく「勝てるけど油断できない」

先に方針を決めます。ここを決めないと失敗します。

相手を強くしすぎると、遊んでもらえません。 全部読み切る相手には勝てません。勝てないゲームは2回目が起きません。

狙うのはこの状態です。

相手の強さ 遊んだ感じ
でたらめ 読む意味がない。すぐ飽きる
たまに間違える 読めば勝てる。読み負けると悔しい
完璧に読む 何をしても勝てない。やめる

真ん中を狙います。わざと間違える確率を入れるのがコツです。これは自分で書くと思いつきにくい部分なので、頼むときに明示します。

やってみる

  1. 考え方まで含めて頼む

    cd my-first-game
    claude -p "index.html の相手の手を、ランダムではなく考えて選ぶようにして。相手が出せる手は「どの札を、どの的に置くか」の組み合わせ全部。まずその一覧を作る関数と、1つの手に点数を付ける関数に分けて。点数は、その手を打った後にリードできる的の数を第一に、無駄な上積みを減らす方向で。強すぎて勝てなくなるのは困るので、たまに読み負ける程度にして。何をどう変えたか、選び方の考えも説明して。"
    

    長く見えますが、頼んでいるのは3つだけです。手の一覧を作る/点数を付ける/たまに間違える。 この分け方を人間が決めるのが要点で、中身の書き方はAIが知っています。

    プロンプトに出てくる「関数」は、ひとまとまりの処理に名前を付けたものです。名前で呼び出せるので、あとから中身だけを差し替えられます。役割ごとに分けて頼むと、直したい場所だけを直せるようになります。

    「強すぎて勝てなくなるのは困る」を必ず入れます。 入れないと、最善手だけを出す相手になります。

  2. どう作られたか確認する

    出てきたコードを見てみます。

    きれいに3つに分かれて出てきました。まず打てる手を全部並べる関数です。

    function legalMoves(hand) {
      var moves = [];
      hand.forEach(function (cardId) {
        TARGETS.forEach(function (t) { moves.push({ card: cardId, target: t.id }); });
      });
      return moves;
    }
    

    やっているのは総当たりの組み立てです。手札の札を1枚ずつ取り出し、それぞれに的3つ分の置き方を作って、moves という並びへ積んでいきます。

    手札3枚 × 的3つで 9通り。1手目の選択肢がこれだけあります。

    次に、1つの手に点数を付ける関数です。

    function scoreMove(move) {
      // 置いたと仮定して、リードできる的の数 → 上積みの無駄 → 弱い札を先に、で選ぶ
      var lead = 0;
      var waste = 0;
      TARGETS.forEach(function (t) {
        var theirs = sumOf(board[t.id].theirs) + (t.id === move.target ? card(move.card).power : 0);
        var mine = sumOf(board[t.id].mine);
        if (theirs > mine) { lead++; waste += theirs - mine; }
      });
      return lead * 1000 - waste * 10 - card(move.card).power;
    }
    

    言葉を2つ押さえてください。「リードできる的」は、その的の合計でコンピュータ側が自分を上回っている的です。「無駄な上積み」は、上回るのに必要な分より大きく出してしまった差です。差が開くほど、他の的に回せたはずの力を捨てたことになります。

    この3行の重みが、相手の性格そのものです。 リードできる的の数が最優先(×1000)、無駄な上積みは減点(×10)、同じなら弱い札から使う。「1点差で勝てばいい」という考え方が、数字で書かれています。

    最後に、選ぶ関数です。

    function chooseCpuMove() {
      var moves = legalMoves(cpuHand);
      if (Math.random() < MISTAKE_RATE) return moves[Math.floor(Math.random() * moves.length)];
      var best = moves[0];
      for (var i = 1; i < moves.length; i++) if (scoreMove(moves[i]) > scoreMove(best)) best = moves[i];
      return best;
    }
    

    上から3段の流れです。打てる手を全部並べる、決めた確率でくじ引きに逃げる、そうでなければ点数がいちばん高い手を選ぶ。この「たまに逃げる」1行が、勝てる相手にしている部分です。

    MISTAKE_RATE という名前が付いています。「わざと間違える確率」です。頼んだ意図が、そのまま名前になっています。

  3. 遊んで強さを確かめる

    open index.html
    

    3回遊んでみてください。判定はこれだけです。

    • 一度も勝てない → 強すぎます
    • 必ず勝てる → 弱すぎます
    • 勝ったり負けたりする → ちょうどいい
  4. 強さを調整する

    強すぎたら、こう頼みます。

    claude -p "相手が強すぎるので、わざと間違える確率を上げて。"
    

    数字1つの調整なので、何度でも試せます。 ここは自分の感覚を信じて決めてください。

  5. 保存する

    git add .
    git commit -m "相手が考えて札を選ぶようにした"
    git push
    

実例: CNPトレカでも同じ順番でした

僕のカードゲームでも、CPU戦を先に作りました。

https://tcg.kenty.app

こちらは扱うカードが多いので、相手の思考はもっと複雑です。それでも考え方は同じで、「勝てるけど油断できない」を狙って調整しています。

そして、CPUの思考を作ると副産物があります。動作確認が1人でできるようになります。 対人戦だと確認のたびに2画面を開く必要がありますが、CPU戦なら1人で何度でも回せます。

作る順番としても、CPUが先で正解でした。

合法手 — ルールに従った手だけを作る

いま作った legalMoves() には名前が付いています。合法手(ごうほうしゅ)です。

ルールに従って、いま実際に打てる手の一覧」のこと。将棋でもカードゲームでも、コンピュータの思考はまずここから始まります。

今回のゲームでは、合法手はきれいに9通り(3枚×3的)でした。どの組み合わせを選んでもルール違反になりません。 だから一覧を作るのは掛け算1回で済みました。

でも本格的なゲームではそうなりません。 状況によって「出せる手」が変わります。

僕のCNPトレカアプリだと、こういう制限があります。

制限
コストが足りない コスト8のカードは、レイキが8ないと出せない
色が合わない 緑5のカードは、緑のレイキが5必要
対象がいない 「相手のユニットを移動させる」効果は、相手にユニットがいないと使えない
使えるタイミングでない 【メイン】と書かれた効果は、そのタイミング以外では使えない

表に出てくるレイキは、CNPトレカでカードを出すときに使う資源です。どんなに強いカードを持っていても、支払う資源が足りなければ、その手は最初から候補になりません。

つまり、手札に5枚あっても、いま実際に出せるのは2枚だけという状況が普通に起きます。合法手が、盤面ごとに変わるわけです。

三つの的の側は、これと比べると拍子抜けするほど単純です。出せない札も、置けない的もありません。 だから合法手は手札の枚数×的の数だけで、1手目は3枚×3つの9通り、6章で山を7枚・4枚配りに増やしても4枚×3つの12通りです。手が進むと手札が減るので、候補も6通り、3通りと小さくなります。数は違っても、打てる手を全部並べて点数を付けて選ぶという総当たりの考え方は同じです。

なぜ合法手が重要か

コンピュータの思考は、合法手の中から選ぶという形でしか作れません。順番はこうです。

  1. いまの盤面から、合法手を全部並べる
  2. その中から、いちばん良さそうな手を選ぶ

ここを分けずに「良さそうな手を選ぶ」だけを作ると、ルール違反の手を出します。 コストが足りないカードを出そうとして止まる、対象がいないのに効果を使おうとして落ちる、といった壊れ方をします。

しかもこの壊れ方は厄介です。たまにしか起きません。 その条件が揃った時だけ落ちるので、原因を掴むのに時間がかかります。

だから、合法手を作る部分を独立させて先に作ります。 これができていれば、思考の側は「並んだ候補から選ぶ」だけになって単純になります。

覚えておいてください

ルールベースのゲームでは、合法手の考え方が核になります。 カードゲーム、将棋、ボードゲーム。どれも同じです。

そして仕事のシステムでも同じ形が出てきます。「いまこの申請は承認できる状態か」「この在庫で受注できるか」。先に「できることの一覧」を確定させて、その中から選ぶ。 構造はまったく同じです。

いまは掛け算1回で足りますが、分けておいたことが後で効きます。 6章で山を7枚に増やしても、legalMoves() は1行も変えずに12通りを返します。条件が増えたときも、直すのはこの関数の中だけで済みます。

つまずきどころ

強さの調整が終わらない

遊んで7割くらい勝てるところで止めてください。完璧なバランスは6章で扱います。ここは「読み合いが成立している」ところまでで十分です。

相手の思考が難しくて読めない

読めなくて構いません。遊んだ感じで判定します。 ただし「何をしているか1行で説明して」と聞いておくと、あとで直すときに楽です。

ランダムのままでいい気がする

面白さの1行を読み返してください。「どの的を捨てるか」が入っているなら、捨てた的を相手が突いてこないと成立しません。 ここは作る必要があります。相手の出方が関係ないゲームなら、ランダムのままで構いません。企画が判断してくれます。

Windows の場合

claude -p の使い方は同じです。

Codex で進める場合

codex exec "index.html の相手の手を、考えて選ぶようにして。強すぎないように、たまに間違える確率も入れて。"

思考の実装は見た目の話ではないので、コスパに優れる Codex 向きの作業です。

次の一歩

1つの的をわざと空けたまま遊んでみてください。 2つの的に全部つぎ込む打ち方です。

相手がその空いた的を1枚で取りに来たら、思考が働いています。放置したら、点数の付け方を見直す合図です。 次の術では、動かなくなったときの調べ方をやります!

ここまでの三つの的

この講座は、見本ゲーム「三つの的」を術ごとにすこしずつ育てながら進みます。下にあるのはこの術を終えた時点の実物です。そのまま遊べます。

別の画面でこのゲームを開く

術の一覧へ戻る

メルマガ(無料)忖度なしのAI事情を、メールでお届けします。ご登録の方に、プレゼントをお渡ししています