できるだけChatGPTだけで作るリバーシ(オセロ)ソフト その2

前回からの続きで、修正を行っていきます。

現時点で以下の問題があります。
・起動時、ダイアログが前面に出ないため、先手後手の選択がしづらい。 ・石を置いても、CPUに切り替わらず、プレイヤーが石を置ける(ただし、色は切り替わっている)

ChatGPTが親切につけてくれた先手後手の選択ダイアログを機能させるのにちょっと面倒だったので、修正を持ち掛けます。

あと、質問して修正させるとかえって時間がかかるため、初期配置の石の色の部分だけは直接修正しました。これによって、ちゃんとプレイできるようになりました。

さらに、コメントを追記してもらうことで相当見やすくなりました。
ということで、ひとまず動かせるプログラムはここまで。

import tkinter as tk
import random
from tkinter import messagebox

class Reversi:
    def __init__(self, root):
        self.root = root
        self.root.resizable(False, False)  # メインウィンドウの最大化ボタンを無効化
        self.choose_color()  # ユーザーに色を選んでもらうダイアログを表示

    def choose_color(self):
        self.color_choice = tk.Toplevel(self.root)  # 新しいトップレベルウィンドウを作成
        self.color_choice.resizable(False, False)  # ダイアログの最大化ボタンを無効化
        # ラベルとボタンをダイアログに追加
        tk.Label(self.color_choice, text="Choose your color:").pack()
        tk.Button(self.color_choice, text="Black", command=lambda: self.start_game(1)).pack()
        tk.Button(self.color_choice, text="White", command=lambda: self.start_game(-1)).pack()
        self.color_choice.grab_set()  # ダイアログを前面に保持
        self.color_choice.attributes('-topmost', True)  # ダイアログを常にトップに保持

    def start_game(self, color):
        # ゲーム開始準備
        self.color_choice.destroy()  # 色選択ダイアログを閉じる
        self.canvas = tk.Canvas(self.root, width=400, height=400)  # キャンバスを作成
        self.canvas.pack()  # キャンバスをパック
        self.initialize_board()  # ボードを初期化
        self.player = color  # プレイヤーの色を設定
        # スコアラベルを作成
        self.score_label = tk.Label(self.root, text="Player 1: 2, Player 2: 2")
        self.score_label.pack()  # スコアラベルをパック
        self.canvas.bind("<Button-1>", self.click)  # クリックイベントをバインド
        self.draw_board()  # ボードを描画
        if self.player == -1:  # プレイヤーが白の場合、CPUのターンを開始
            self.computer_move()

    def initialize_board(self):
        # ボードを初期化: 8x8のグリッドで、中央に2つずつの黒と白の石を配置
        self.board = [[0 for _ in range(8)] for _ in range(8)]
        self.board[3][3] = 1
        self.board[4][4] = 1
        self.board[3][4] = -1
        self.board[4][3] = -1
        self.move_count = 0  # 手数を0にリセット

    def draw_board(self):
        # ボードと石を描画
        for i in range(8):
            for j in range(8):
                x, y = i * 50, j * 50
                self.canvas.create_rectangle(x, y, x + 50, y + 50, fill="green")  # グリーンのセルを描画
                if self.board[i][j] == 1:
                    self.canvas.create_oval(x + 5, y + 5, x + 45, y + 45, fill="black")  # 黒の石を描画
                elif self.board[i][j] == -1:
                    self.canvas.create_oval(x + 5, y + 5, x + 45, y + 45, fill="white")  # 白の石を描画
        self.update_score()  # スコアを更新


    def click(self, event):
        # ユーザーのクリックイベントハンドラ
        x, y = event.x // 50, event.y // 50  # クリックされたセルの座標を取得
        if self.place_piece(x, y, self.player, True):  # 石を置けるか確認
            self.move_count += 1  # 手数を増やす
            self.player *= -1  # プレイヤーを切り替え
            self.draw_board()  # ボードを再描画
            self.check_game_end()  # ゲーム終了条件をチェック
            self.computer_move()  # コンピュータの手番

    def computer_move(self):
        # コンピュータの手番
        if self.player == -1:  # コンピュータの手番の場合
            # 有効な手を探す
            valid_moves = [(x, y) for x in range(8) for y in range(8) if self.place_piece(x, y, self.player, False)]
            if valid_moves:  # 有効な手がある場合
                move = random.choice(valid_moves)  # ランダムな手を選ぶ
                self.place_piece(move[0], move[1], self.player, True)  # その手を選ぶ
                self.move_count += 1  # 手数を増やす
                self.player *= -1  # プレイヤーを切り替え
                self.draw_board()  # ボードを再描画
            else:  
                self.player *= -1  # 有効な手がない場合、プレイヤーを切り替え
            self.check_game_end()  # ゲーム終了条件をチェック

    def place_piece(self, x, y, player, actual=False):
        # 指定した座標に石を置くためのメソッド
        # x, y: 石を置く座標
        # player: 現在のプレイヤー (1 または -1)
        # actual: 実際に石を置くかどうかを示すフラグ
        if x < 0 or x >= 8 or y < 0 or y >= 8 or self.board[x][y] != 0:
            return False  # 無効な座標または既に石が置かれている場合はFalseを返す

        directions = [(0, 1), (1, 0), (0, -1), (-1, 0), (1, 1), (-1, -1), (1, -1), (-1, 1)]  # 探索する8方向
        to_flip = []  # ひっくり返される石のリスト

        for dx, dy in directions:  # 8方向について繰り返し
            i, j, flips = x + dx, y + dy, []  # 探索開始座標と、ひっくり返される石のリストを初期化
            while 0 <= i < 8 and 0 <= j < 8 and self.board[i][j] == -player:  # 異なる色の石を見つけるまで探索
                flips.append((i, j))  # ひっくり返される石をリストに追加
                i += dx  # 探索座標を更新
                j += dy
            if 0 <= i < 8 and 0 <= j < 8 and self.board[i][j] == player and flips:  # 探索終端がプレイヤーの石の場合
                to_flip.extend(flips)  # ひっくり返される石のリストに追加
        if not to_flip:
            return False  # ひっくり返される石がない場合はFalseを返す

        if actual:  # 実際に石を置く場合
            self.board[x][y] = player  # 指定した座標に石を置く
            for i, j in to_flip:  # ひっくり返される石を更新
                self.board[i][j] = player
        return True

    def update_score(self):
        # スコアを更新するメソッド
        scores = [sum(row.count(player) for row in self.board) for player in [1, -1]]  # 各プレイヤーのスコアを計算
        self.score_label.config(text=f"Player 1: {scores[0]}, Player 2: {scores[1]}")  # スコアラベルを更新

    def check_game_end(self):
        # ゲーム終了条件をチェックするメソッド
        player_moves = [(x, y) for x in range(8) for y in range(8) if self.place_piece(x, y, self.player, False)]  # 現在のプレイヤーの可能な手
        opponent_moves = [(x, y) for x in range(8) for y in range(8) if self.place_piece(x, y, -self.player, False)]  # 相手プレイヤーの可能な手

        if not player_moves and not opponent_moves or self.move_count >= 60:  # どちらも手がないか、60手以上進んだ場合
            self.show_game_end()  # ゲーム終了処理を呼び出し

    def show_game_end(self):
        # ゲーム終了時の処理を行うメソッド
        scores = [sum(row.count(player) for row in self.board) for player in [1, -1]]  # 各プレイヤーのスコアを計算
        winner = "Draw"  # 勝者の初期値を"Draw"に設定
        if scores[0] > scores[1]:  # プレイヤー1の勝利判定
            winner = "Player 1 wins"
        elif scores[1] > scores[0]:  # プレイヤー2の勝利判定
            winner = "Player 2 wins"

        # ゲーム終了のメッセージボックスを表示
        messagebox.showinfo("Game Over", f"{winner}! Final Score - Player 1: {scores[0]}, Player 2: {scores[1]}")
        self.canvas.destroy()  # キャンバスを破棄
        self.score_label.destroy()  # スコアラベルを破棄
        self.choose_color()  # 色選択ダイアログを再表示

root = tk.Tk()  # Tkinterのルートウィンドウを作成
reversi = Reversi(root)  # Reversiクラスのインスタンスを作成
root.mainloop()  # イベントループを開始


さて、次は探索関数、評価関数の作成となります。

コンピュータの手をモンテカルロ法で探索した結果でベストの手を打つように変更してください。
探索関数、評価関数はリファクタリングしたものでメンテしやすいようにしてください。
また、深度は引数で指定できるようにしてください。

多少ざっくりではありますが、上記の指示を出しました。
その結果として返ってきたのは、これ。

import random

class MonteCarlo:
    def __init__(self, board, player, depth):
        self.board = board
        self.player = player
        self.depth = depth

    def get_possible_moves(self, board, player):
        # 有効な手をリストとして取得する関数
        possible_moves = []
        for x in range(8):
            for y in range(8):
                if self.place_piece(board, x, y, player, actual=False):
                    possible_moves.append((x, y))
        return possible_moves

    def place_piece(self, board, x, y, player, actual=False):
        # (既存のplace_piece関数の内容をここにコピー)
        # ...
        return True

    def simulate(self):
        best_move = None
        max_score = -float('inf')

        for move in self.get_possible_moves(self.board, self.player):
            score = self.playout(move, self.depth)
            if score > max_score:
                max_score = score
                best_move = move

        return best_move

    def playout(self, move, depth):
        if depth == 0:
            return self.evaluate(self.board)

        simulated_board = [row[:] for row in self.board]
        x, y = move
        self.place_piece(simulated_board, x, y, self.player, actual=True)
        opponent = -self.player

        opponent_moves = self.get_possible_moves(simulated_board, opponent)
        if opponent_moves:
            opponent_move = random.choice(opponent_moves)
            self.place_piece(simulated_board, opponent_move[0], opponent_move[1], opponent, actual=True)

        return -self.playout(opponent_move, depth - 1)

    def evaluate(self, board):
        # 簡単な評価関数: ボード上の自身の石の数を返す
        return sum(row.count(self.player) for row in board)


# Reversiクラス内にMonteCarloクラスを使用するコードを追加します
class Reversi:
    # ... (他のメソッドは変更なし)

    def computer_move(self, depth=3):
        monte_carlo = MonteCarlo(self.board, self.player, depth)
        best_move = monte_carlo.simulate()
        if best_move:
            self.place_piece(best_move[0], best_move[1], self.player, actual=True)
            self.update_score()

今回は全探索やよく使われるミニマックス法ではなく、モンテカルロ木探索にしました。
評価関数はChatGPTが気を利かせて、石の数から算出してくれるようにしてくれました。

コードはこのままでは動かないので、中のコメントを見つつコードを追記していく必要があります。
指示を出してもよし、ここは手動で書いてもよし。これを見た方にお任せします。

正直、学習させてない状態でこれを使ってもランダムとほぼ何も変わりません。ただ単に、評価関数、探索関数を切り分けて後々変更しやすいようにするのが今回の目的であり、次回以降に役立つかなあと。

これで、今度こそベースになる素材がそろいました。

次回は、もうちょっとまともな評価関数を考えてみようと思います。

この記事が気に入ったらサポートをしてみませんか?