このメモでやること

拙作のパズルゲーム trisblocks には、自分で書いた AI にプレイさせるための仕組みがあります。 ゲームが1フレームごとに盤面を渡してくれるので、こちらは「どう動かすか」を返すだけです。

ただ、はじめて触るときは「そもそも何をどこに書けばいいのか」で止まりがちです。 そこで今回は、動くところまでだけをやります。30段ほどのファイルを1つ作って、ゲーム画面から起動する、それだけです。

盤面を読んで賢く積む話は次回にまわします。今回のゴールは、画面の中のミノが、自分の書いたコードで動いたという一点だけです。


用意するもの

ブラウザだけです。インストールも、アカウントも、開発環境も要りません。

  • メモ帳など、文字が打てるもの
  • Chrome や Edge などのブラウザ

AI は HTML ファイル1つとして書きます。書けたら、ゲームの画面からそのファイルを選ぶだけで動きます。


ステップ1 : ファイルを作る

メモ帳を開いて、下の中身をまるごと貼り付けてください。 そして ファイル名を my-ai.html にして、デスクトップなど分かりやすい場所に保存します。

※ メモ帳で保存するときは「ファイルの種類」を すべてのファイル、「文字コード」を UTF-8 にしてください。

    <!DOCTYPE html>
    <meta charset="utf-8">
    <title>わたしのAI</title>
    <p id="log">まだ動いていません</p>

    <script>
    const CH = new BroadcastChannel('tb-ai');   // ゲームとの通話回線
    let lastPiece = -1;

    CH.onmessage = (ev) => {
      const m = ev.data;
      if (!m || m.from !== 'game') return;

      // 「そこに居る?」と聞かれたら返事をする
      if (m.type === 'ping') {
        CH.postMessage({ from: 'ai', type: 'pong' });
        return;
      }
      if (m.type !== 'state') return;

      // 盤面は毎フレーム届く。 ミノが変わった時だけ考える
      if (m.me.pieces === lastPiece) return;
      lastPiece = m.me.pieces;

      // ここが AI の中身。 いまは「左右どちらかへ1マス寄って、落とす」だけ
      const dir = Math.random() < 0.5 ? 'left' : 'right';
      CH.postMessage({ from: 'ai', act: [[dir], ['hard']] });

      document.getElementById('log').textContent =
        m.me.pieces + ' 個目 / ' + m.me.lines + ' ライン消した';
    };

    CH.postMessage({ from: 'ai', type: 'hello' });   // 起動したことを知らせる
    </script>

やっていることは3つだけです。

  • 回線をつなぐBroadcastChannel('tb-ai') が、ゲームのタブとの通話回線です
  • 話を聞くonmessage に、ゲームから盤面 (state) が毎フレーム届きます
  • 返事をするpostMessage で、押したいボタンを送り返します

ステップ2 : ゲーム画面から動かす

ここからはブラウザの操作だけです。

① trisblocks を開く

trisblocks を開いて、タイトル画面を出します。

② メニューから AI DEVELOP を選ぶ

DEVELOPAI DEVELOP と進みます。キャラクター選択は出ません。

③ 「ファイルをえらぶ」で my-ai.html を指定する

いちばん上の あなたの AI のすぐ下に ファイルをえらぶ があります。これを押して、保存した my-ai.html を選びます。

選んだ瞬間にランプが緑になり、「えらんだファイルの AI が動いています」と出れば成功です。 ファイルはゲームの中の隔離された枠で動くので、どこに置いてあっても構いませんし、アップロードも要りません。 書き直したときは、選び直すだけで新しい中身に入れ替わります。

④ 対戦相手を選んで「はじめる」

同じ画面の 対戦相手 から、戦う相手を選びます。CPU EASY / NORMAL / HARD のほか、 投稿された AI が並んでいることもあります。迷ったら CPU NORMAL です。 ランプが緑になっていれば はじめる が押せるので、押すと試合が始まります。

ミノが勝手に左右に散らばって落ちていけば成功です。

ただし、この AI は盤面を一切見ていません。 でたらめに寄せて落とすだけなので、10〜20個ほど置いたところでゲームオーバーになります。 それで正常です。 ライン消しは、ほとんど起きません。


ファイルを直接開いても動きません

my-ai.html をダブルクリックして開いても、それだけでは何も起きません。 BroadcastChannel同じオリジンのページ同士にしか届かないため、手元のファイル (file:///C:/…) と ゲーム (https://traias.com/…) は、同じ名前のチャンネルを開いていても別々の回線になってしまいます。

かならず AI DEVELOP の「ファイルをえらぶ」 から指定してください。 ゲームの中の隔離された枠で動かすので、この問題自体が起きません。


届いているデータを覗く

どういうデータが届いているのか見たい時は、my-ai.html に1段足すのがいちばん手軽です。

    console.log(m);        // state をそのまま開発者ツールへ出す

ブラウザで F12 を押して Console タブを開くと、盤面・NEXT・HOLD・火力の中身が毎フレーム流れていきます。


ついでに : キャラクターを指定する

そのままだと、盤面の背景は「前にゲームで選んだキャラクター」になります。 AI 側から指名することもできます。 さきほどのサンプルの、pong を返している行に char を足すだけです。

    CH.postMessage({ from: 'ai', type: 'pong', char: 'c3' });

使える id は、ゲームから届く hello の中に一覧で入っています。

    if (m.type === 'hello') {
      console.log(m.meta.chars);
      // → [{id:'c1', name:'セレスト'}, {id:'c2', name:'ロゼリア'}, ...]
    }

試合の途中で変えたくなったら、いつでも { from: 'ai', type: 'char', id: 'c2' } を送れます。 次の試合から切り替わります。 指定しなかった場合は、ゲーム側で前に選んだものがそのまま使われます。


うまくいかないとき

症状見るところ
ランプが赤いまま変わらないファイルをえらぶ で選び直してみてください。my-ai.html を直接ブラウザで開いてもつながりません
何も起きない・エラーが出るfrom: 'ai' を書き忘れていないか。これが無いとゲーム側に届きません
文字化けする保存時の文字コードが UTF-8 になっているか
ミノが1個も動かないact の中身が二重の配列 ([['hard']]) になっているか

ブラウザの F12 を押すと開発者ツールが出ます。Console タブに赤い文字が出ていれば、それが原因です。


ここまでのまとめ

  • AI は HTML ファイル1つ。AI DEVELOP の ファイルをえらぶ で指定するだけ
  • ゲームとは BroadcastChannel('tb-ai') でつながる
  • ping には pong を返す … これで「動いている」と認識されます
  • 盤面は毎フレーム届く。me.pieces が変わった時だけ考えれば十分です
  • 返す中身は { from: 'ai', act: [['left'], ['hard']] } の形

次回は、届いた盤面を実際に読んで、穴を作らない置き場所を選ぶところまでやります。 この30段のうち、書き替えるのは「ここが AI の中身」の2段だけです。