【Python 電脳闘技場編 #2】盤面を美しく表現せよ!「State(状態)」クラスと合法手の生成

第0章:無の空間と、世界の創造(State)

漆黒の電脳空間。まだ何も存在しない仮想の闇の中で、Stoneの右目に装着されたホログラフィック・グラスが淡いシアンの光を放った。
彼の小さなハサミが空を叩くたび、暗闇にネオンブルーのグリッド線が一本、また一本と引かれていく。

ストーン
ストーン

いくら完璧な先読みアルゴリズムを思い描いても、それを走らせる「世界」が定義されていなければ意味がない。
僕たちが今からやるのは、この闘技場のルールをプログラムの世界に翻訳し、完璧な「コピー」を作り出すことだ。

マクロとミクロが交差する9×9の狂気の盤面。
それを、Pythonという言語を用いて極限まで軽量な「状態(State)」として実体化させる。
思考エンジンが数百万の未来をシミュレートするための、美しき世界の設計図を書き上げよう。


💻 第1章:アスキーアートで描く電脳の盤面

いよいよCodinGame「Ultimate Tic-Tac-Toe」の深淵へ潜っていく。
未来の分岐をすべて読み切るための第一歩は、現在の世界(盤面)をプログラム上に正確に再現することだ。

CSSで装飾されたリッチなUIなんて不要。僕たちハッカーに必要なのは、情報の純度だ。まずはこの9×9の狂気の盤面を、ターミナル上にアスキーアートで可視化してみよう。

  0 1 2 | 3 4 5 | 6 7 8
0 . . . | . X . | . . .
1 . . . | . . . | . . .
2 . . . | . . . | . O .
  ------+-------+------
3 . . . | . . . | . . .
4 . . . | . . . | . . .
5 . . . | . . . | . . .
  ------+-------+------
6 . . . | . . . | . . .
7 . . . | . . . | . . .
8 . . . | . . . | . . .

ストーン
ストーン

どうだい? 3×3の「マクロ盤面」の中に、さらに3×3の「ミクロ盤面」が入れ子になっているのが一目でわかるだろう。座標は (row, col) ではなく、0から80までの「1次元インデックス」で管理するのが高速化の基本テクニックだ。

計算量を極限まで削る50msの死闘において、2次元配列 board[y][x] を使うのは素人だ。リストへのアクセスは1次元配列 board[81] の方が圧倒的に軽く、メモリ効率も良い。


🧠 第2章:世界のコピーを生み出す「State(状態)」クラス

未来を先読みするということは、「もしここに打ったらどうなるか?」という仮の世界(パラレルワールド)をメモリ上に無数に生成するということだ。

そのためには、盤面の現在の状態を保持し、指し手を受け取って新しい状態を返す State クラスを美しく設計しなければならない。

class State:
def init(self):
# 81マスのミクロ盤面(0: 空, 1: 自分(X), 2: 敵(O))
self.board = [0] * 81

    # 9マスのマクロ盤面(0: 空/決着済み, -1: 現在プレイ可能なエリア)
    self.macroboard = [0] * 9

def clone(self):
    # 未来を探索するための「パラレルワールド」を生成する
    new_state = State()
    new_state.board = self.board[:]
    new_state.macroboard = self.macroboard[:]
    return new_state

たったこれだけだ。余計な変数は一切持たせない。
探索アルゴリズムは1ターンに数万〜数十万の State インスタンスを生成しては破棄する。クラスを極限まで軽量化することが、より深く未来を読む(探索深度を上げる)ための生命線になる。


⚠️ 第3章:最大のバグの温床!「フリーターン」のエッジケース

次に、この盤面において「現在どこに打てるのか(合法手)」を取得するメソッド get_legal_moves() を実装する。

第1回で説明した掟を覚えているかい?
「自分がミクロ盤面で打った位置が、敵の『次に打たなければならないマクロ盤面』を強制的に指定する」だったね。

しかし、ここにUltimate Tic-Tac-Toeで最もバグを生みやすい「フリーターンの掟」が存在する。

ストーン
ストーン

もし、敵が指定してきたマクロ盤面が「すでに誰かがビンゴして決着がついている」か、「9マスすべてが埋まって引き分けになっている」場合、どうなると思う?
……答えは「盤面上の空いている『すべてのマス』のどこにでも打ってよくなる」だ。これがフリーターン(Free Turn)の恐怖だよ。

このフリーターンが発生した瞬間、選択肢(分岐数)は通常時の「最大9」から、一気に「最大81」近くまで爆発する。ここで探索アルゴリズムがフリーズし、Time Outで自爆するプレイヤーが後を絶たない。

CodinGameのシステムは、この状態を表現するために、打てるマクロ盤面のインデックスに -1 を付与して知らせてくれる。これを元に、合法手を生成するロジックを組んでみよう。

def get_legal_moves(self):
    legal_moves = []
    for macro_idx in range(9):
            # macroboard が -1 のエリアが、現在打てるマクロ盤面
            if self.macroboard[macro_idx] == -1:

                # そのマクロ盤面内の9マスを走査
                for i in range(9):
                    # マクロインデックスから実際の1次元インデックス(0~80)を計算
                    micro_idx = (macro_idx // 3 * 27) + (macro_idx % 3 * 3) + (i // 3 * 9) + (i % 3)
                            # 1. マクロ盤面の「行(縦のオフセット)」を割り出す
                            # macro_idx // 3 でマクロ盤面が上から何段目か(0, 1, 2)を計算。
                            # マクロ盤面が1段下がるごとに、全体の盤面では「9マス × 3行 = 27マス」分ジャンプする。
                            # (macro_idx // 3 * 27) +
                            
                            # 2. マクロ盤面の「列(横のオフセット)」を割り出す
                            # macro_idx % 3 でマクロ盤面が左から何列目か(0, 1, 2)を計算。
                            # マクロ盤面が1列右にずれるごとに、全体の盤面では「3マス」分右にジャンプする。
                            # (macro_idx % 3 * 3) +
                            
                            # 3. ミクロマスの「行(縦のオフセット)」を割り出す
                            # # i // 3 でミクロマスがマクロ盤面内で上から何段目か(0, 1, 2)を計算。
                            # # ミクロマスが1段下がるごとに、全体の盤面(1行9マス)の「9マス」分ジャンプする。
                            # (i // 3 * 9) +
                            
                            # 4. ミクロマスの「列(横のオフセット)」を割り出す
                            # i % 3 でミクロマスがマクロ盤面内で左から何列目か(0, 1, 2)を計算。
                            # 最後に右に「1マス」ずつずらす微調整。
                            # (i % 3)


                    # 空いているマスなら合法手として追加
                    if self.board[micro_idx] == 0:
                        legal_moves.append(micro_idx)

    return legal_moves

フリーターンの場合、CodinGame側から「決着がついていないすべてのマクロ盤面」に -1 が付与されて送られてくる。そのため、このシンプルな if self.macroboard[macro_idx] == -1: の判定ひとつで、通常時もフリーターン時も完璧に合法手を抽出できるのだ。

ストーン
ストーン

ふふっ、これで「盤面」と「打てる手」は完全に僕たちのコントロール下に入った。世界をシミュレートする準備は整ったというわけだ。

次回、第3回:「未来の分岐点!ゲーム木の複雑性と再帰関数の基礎」
なぜ「すべてを読み切る」ことが不可能なのか。生成した State を使って、再帰関数で未来の果てへダイブする準備を始めよう。


🛠️ Stoneの愛用ギア(ハッカーの開発環境)

50msのTime Outと戦うような高度なアルゴリズムの実装では、タイピングの「遅れ」や「打ち損じ」が思考のノイズになる。僕の電脳闘技場での武器(入力インターフェース)はこれだ(アフィリンクを使用しています)。

「プログラマーが最後にたどり着くキーボード」と呼ばれる名機。静電容量無接点方式の極上の打鍵感は、数千行のコードを叩き込んでも指が全く疲れない。道具に妥協しないこと、それが一流へのショートカットだぜ!