PygameとMinimax法(ミニマックス法)で敵CPUを強化したオセロゲームを作成する方法をソースコード付きで詳しく解説します。
オセロゲームの敵CPUを強化
オセロゲームの敵CPU(白側)を強化するために、今回は「Minimax法」を用います。
Minimax法では、2人対戦型のゲームでN手先まで読みをする時、相手のターンでは自分にとって一番不利な手を選びます。
そして、自分のターンでは自分にとって一番有利な手を選びます。これをN手先まで繰り返し行い、最終的に自分のターンで最も有利になる手を選びます。
Minimax法のアルゴリズム
Minimax法のアルゴリズムを簡単に整理すると以下のとおりです。
| 手順 | 説明 |
|---|---|
| ①ゲームツリーの探索 | ゲームツリーとは、現在のボード状態から可能なすべての手を展開していったものです。各ノード(点)はボードの状態を表し、枝(線)は手の選択を表します。 |
| ②再帰的な探索 | ミニマックスアルゴリズムは、再帰的にゲームツリーを探索します。つまり、ある手を選んだ後、その次の手も同様に最適な手を探していきます。 |
| ③評価値の計算 | 各ノードでボードの状態を評価し、評価値を計算します。評価値は、白の石の数から黒の石の数を引いたものです。 |
相手のターンでは評価値が最小(Min)、自分のターンでは評価値が最大(Max)になるような手を選ぶことから、Minimax法と呼ばれています。これをオセローゲームに適用すると、敵CPU(白)とプレイヤー(黒)の処理は以下のようになります。
| 項目 | 説明 |
|---|---|
| ① 敵CPU(白)のターン | 白が有利になるように評価値を最大化しようとします。つまり、白の石の数が増えるような手を選びます。具体的には、白のターンで白が置けるすべての位置を試し、それぞれの手について評価値を計算します。その中で最も高い評価値を持つ手を選びます。 (例)白が置ける位置A、B、Cがあるとします。それぞれの位置に置いた場合の評価値を計算します。位置Aの評価値が5、位置Bの評価値が3、位置Cの評価値が7だとすると、白は評価値が最も高い位置Cを選びます。 |
| ② プレイヤー(黒)のターン | 黒が有利になるように評価値を最小化しようとします。つまり、白の石の数が減るような手を選びます。具体的には、黒が置けるすべての位置を試し、それぞれの手について評価値を計算します。その中で最も低い評価値を持つ手を選びます。 (例)黒が置ける位置D、E、Fがあるとします。それぞれの位置に置いた場合の評価値を計算します。位置Dの評価値が2、位置Eの評価値が4、位置Fの評価値が1だとすると、黒は評価値が最も低い位置Fを選びます。 |
評価値の計算
オセロゲームの場合、評価値としてよく用いられるのが数手先の「白の石数 – 黒の石数」です。例えば、深さが3なら敵CPU側(白)は3手先までのパターンを試し、3ターン後に「白の石数 – 黒の石数」が最も大きくなる次手を選びます。計算方法を整理すると以下のようになります。
| 計算手順 | 説明 |
|---|---|
| ① 3手先までのすべてのパターンを試す | 現在のボード状態から、白と黒が交互に手を打つ3手先までのすべての可能な手を探索します。 |
| ② 評価値の計算 | 各終端ノード(3手先のボード状態)で、白の石の数から黒の石の数を引いた評価値を計算します。 |
| ③ 最適な手の選択 | 白のターンでは、評価値(白の石数 – 黒の石数)を最大化する手を選びます。CPUは自分の手を最大化しようとしますが、人が最適な手を選ぶことを前提にしているため、最小化プレイヤーの処理が必要です。黒のターンでは、プレイヤーが最適な手を選ぶと仮定し、評価値(白の石数 – 黒の石数)を最小化する手を選びます。敵CPUとプレイヤーの両者が最適な手を選ぶと仮定して、敵CPUは一手を選びます。 |
サンプルコード
Minimax法を用いたオセロゲームのサンプルコードです。
サンプルコードのポイント解説
CPUが「もしここに置いたらどうなるか」を考える際、実際の盤面を直接書き換えるとゲームが進行してしまいます。そのため、シミュレーションを行う前に盤面を丸ごとバックアップしています。
# 盤面の状態を一時保存(バックアップ)
board_backup = copy.deepcopy(self.board)
# 試しに置いてみる
self.board[x][y] = WHITE
self.flip_stones(x, y) # 実際にひっくり返してシミュレーション
# 先読みを実行...
# 盤面を元の状態に完全に戻す
self.board = board_backup
盤面は「リストのリスト」の2次元配列です。通常のコピーでは中身のリストまで複製されないため、deepcopy を使って全データを別物として複製します。石を置くだけでなく、flip_stones を実行して相手の石をひっくり返した状態を作ることで、Minimax法の正しい評価値が求まります。
Minimax法の処理
def minimax(self, depth, is_maximizing):
# 探索の限界(3手先)に達したら、その時の盤面を評価する
if depth == 3 or not self.has_valid_move():
return self.evaluate_board()
if is_maximizing:
# CPU(白)の番:自分の利益を最大化するスコアを選ぶ
max_eval = float('-inf')
# ...(全マス探索)
return max_eval
else:
# プレイヤー(黒)の番:CPUの利益を最小化すると想定
min_eval = float('inf')
# ...(全マス探索)
return min_eval
minimax の中で minimax を呼び出すことで、1手、2手、3手……と深く潜っていきます。depth(深さ)の値を大きくすると「より深読み」を予測しますが、計算量が増えて動作が重くなります。
石が置けるかどうかの判定(is_valid_move)
オセロの根幹ルールである「相手の石を挟めるか」を全8方向についてis_valid_moveメソッドでチェックします。
# 8方向(上下左右、斜め)の移動量を定義
directions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
for dx, dy in directions:
nx, ny = x + dx, y + dy
# 隣が相手の石であることを確認
if 0 <= nx < BOARD_SIZE and 0 <= ny < BOARD_SIZE and self.board[nx][ny] == opponent:
while 0 <= nx < BOARD_SIZE and 0 <= ny < BOARD_SIZE:
nx += dx
ny += dy
# 自分の石にたどり着けば「挟めている」のでTrue
if self.board[nx][ny] == self.turn:
valid = True
break
1つのマスに対して8方向を順番に見ていき、1方向でも挟める条件を満たせば「有効な一手」となります。
石をひっくり返す処理(flip_stones)
石を置いた際、実際に挟まれた石を自分の色に変えます。
pieces_to_flip = [] # 相手の石を一時的に溜める
# ...(探索ループ)
if 0 <= nx < BOARD_SIZE and 0 <= ny < BOARD_SIZE and self.board[nx][ny] == self.turn:
for px, py in pieces_to_flip:
self.board[px][py] = self.turn # 確定した石をすべて自分の色に
探索の途中でいきなり色を変えるのではなく、自分の石が見つかって「挟めていることが確定」してから、リストに保存した相手の石を一気に塗り替えます。
### 座標計算とクリック判定
マウスでクリックした画面上の座標(ピクセル)を、盤面のインデックス(0〜7)に変換します。
```python
x, y = event.pos
x //= GRID_SIZE # 例:座標150px // 75px = インデックス2
y //= GRID_SIZE
GRID_SIZEは、画面幅(例:600)をマスの数(例:8)で割った値(例:75)です。整数の除算(//)で小数点を切り捨てることで、正確にどのマスがクリックされたかを算出します。
パスとゲーム終了の管理
オセロでは「置けない場合はパス」「両者置けなくなったら終了」というルールがあります。
# 片方が置けない場合
if not game.has_valid_move():
# 相手も置ける場所がないかチェック(完全終了の判定)
if not any(game.is_valid_move(x, y) for x in range(BOARD_SIZE) for y in range(BOARD_SIZE)):
print(game.game_end()) # 勝敗表示
break
game.turn = WHITE if game.turn == BLACK else BLACK # パス
any関数の活用で盤面全体をスキャンし、1つでも True があれば処理を続行します。全滅(どこにも置けない)状態を判定するのに非常に便利な関数です。
関連ページ
Pygameの使い方については以下ページで解説しています。

Python全般については以下ページで解説しています。



コメント