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

前回書いたバグ修正部分も以下の質問で改善させました。

ダイアログでWhiteを選ぶと、白が先手番になります。白は後手番のままにしたいので修正をお願いします。

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)  # ダイアログを常にトップに保持

自分で作成していないと見落とすパターンですね。1文字だけなので対象部分を手動で直しました。

あとは、プレイ中には影響がないですが、以下のことも直しました。

結果のダイアログが2回出ます。1回にしたいです

コードに表示される結果のダイアログが2回表示される問題は、check_game_end メソッドがコンピュータのターンとプレイヤーのターンの両方で呼び出されるために発生しています。ゲームが終了したかどうかを確認した後にゲーム終了のダイアログを表示する show_game_end メソッドが2回呼び出されるため、ダイアログが2回表示されます。

この問題を解決するためには、check_game_end メソッドがゲーム終了条件を満たしているかどうかを確認した後、show_game_end メソッドを呼び出す前にゲームが既に終了しているかどうかを確認する必要があります。

以下の方法でこれを実現できます:ゲームの状態(進行中または終了)を追跡する新しいインスタンス変数を追加します。
ゲームが終了した場合、この変数を更新してゲーム終了を示します。
check_game_end メソッドが呼び出されるたびに、この変数をチェックしてゲームが既に終了している場合は show_game_end メソッドを呼び出さないようにします。

class Reversi:
    def __init__(self, root):
        # 他のインスタンス変数と同様に、game_over変数をFalseで初期化します
        self.game_over = False
        # ... (他のコードは変更なし)

    def check_game_end(self):
        # ... (他のコードは変更なし)

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

    def show_game_end(self):
        # このメソッドが呼び出されたときにゲームが既に終了している場合は、何もしません
        if self.game_over:
            return

        # ... (他のコードは変更なし)

実際には、上記だけだと再戦するときにフラグの初期化が行われないので、start_game関数にもself.game_over =Falseを追記しています。

そんなこんなで、修正した内容は以下の通り。

import tkinter as tk
import random
from tkinter import messagebox

class MonteCarlo:
    
    BOARD_SIZE = 8
    
    DIRECTIONS = [(0, 1), (1, 0), (0, -1), (-1, 0), (1, 1), (-1, -1), (1, -1), (-1, 1)]
    
    CELL_WEIGHTS = [
        [120, -20, 20, 5, 5, 20, -20, 120],
        [-20, -40, -5, -5, -5, -5, -40, -20],
        [ 20,  -5, 15, 3, 3, 15,  -5,  20],
        [  5,  -5,  3, 3, 3,  3,  -5,   5],
        [  5,  -5,  3, 3, 3,  3,  -5,   5],
        [ 20,  -5, 15, 3, 3, 15,  -5,  20],
        [-20, -40, -5, -5, -5, -5, -40, -20],
        [120, -20, 20, 5, 5, 20, -20, 120]
    ]
    
    def __init__(self, board, player, depth):
        self.board = board
        self.player = player
        self.depth = depth

    def get_possible_moves(self, board, player):
        return [(x, y) for x in range(self.BOARD_SIZE) for y in range(self.BOARD_SIZE) if self.place_piece(x, y, player, board, actual=False)]


    def place_piece(self, x, y, player, board=None, actual=False):
        
        if board is None:
            board = self.board

        if x < 0 or x >= self.BOARD_SIZE or y < 0 or y >= self.BOARD_SIZE or board[x][y] != 0:
            return False 

        directions = self.DIRECTIONS  
        to_flip = []  

        for dx, dy in directions:  
            i, j, flips = x + dx, y + dy, []  
            while 0 <= i < self.BOARD_SIZE and 0 <= j < self.BOARD_SIZE and board[i][j] == -player:  
                flips.append((i, j))  
                i += dx  
                j += dy
            if 0 <= i < self.BOARD_SIZE and 0 <= j < self.BOARD_SIZE and board[i][j] == player and flips:  
                to_flip.extend(flips)  

        if not to_flip:
            return False  

        if actual:  
            board[x][y] = player  
            for i, j in to_flip:  
                board[i][j] = player
        return True

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

        for move in self.get_possible_moves(self.board, self.player):
            total_score = 0
            for _ in range(num_simulations):
                total_score += self.playout(move, self.depth)
            average_score = total_score / num_simulations

            if average_score > max_score:
                max_score = average_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(x, y, self.player, board=simulated_board, 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(opponent_move[0], opponent_move[1], opponent, board=simulated_board, actual=True)
            return -self.playout(opponent_move, depth - 1)
        else:
            return self.evaluate(simulated_board)  # 対戦相手が行動できない場合は現在のボードの評価を返します

    def evaluate(self, board):
        return sum(board[x][y] * self.CELL_WEIGHTS[x][y] for x in range(self.BOARD_SIZE) for y in range(self.BOARD_SIZE))

class Reversi:
    BOARD_SIZE = 8

    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(self.BOARD_SIZE)] for _ in range(self.BOARD_SIZE)]
        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(self.BOARD_SIZE):
            for j in range(self.BOARD_SIZE):
                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, 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()
            self.move_count += 1  # 手数を増やす
            self.player *= -1  # プレイヤーを切り替え
            self.draw_board()  # ボードを再描画
        else:  
            self.player *= -1  # 有効な手がない場合、プレイヤーを切り替え
        self.check_game_end()  # ゲーム終了条件をチェック

    def computer_move_random(self):
        # コンピュータの手番
        if self.player == -1:  # コンピュータの手番の場合
            # 有効な手を探す
            valid_moves = [(x, y) for x in range(self.BOARD_SIZE) for y in range(self.BOARD_SIZE) 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 >= self.BOARD_SIZE or y < 0 or y >= self.BOARD_SIZE 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 < self.BOARD_SIZE and 0 <= j < self.BOARD_SIZE and self.board[i][j] == -player:  # 異なる色の石を見つけるまで探索
                flips.append((i, j))  # ひっくり返される石をリストに追加
                i += dx  # 探索座標を更新
                j += dy
            if 0 <= i < self.BOARD_SIZE and 0 <= j < self.BOARD_SIZE 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(self.BOARD_SIZE) for y in range(self.BOARD_SIZE) if self.place_piece(x, y, self.player, False)]  # 現在のプレイヤーの可能な手
        opponent_moves = [(x, y) for x in range(self.BOARD_SIZE) for y in range(self.BOARD_SIZE) 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()  # イベントループを開始

他に認知しているバグとしては、プレイヤーが石を置いたのがすぐに画面に反映されず、CPUの思考中に固まるため一気に2手進むように見えるというのがあります。
当面は見た目の問題より、「オセロ」に勝つことですので、プレイが止まってしまわない限りは放置していきます。

次回の「その5」では、改めて「探索関数」「評価関数」を考えてみようと思います。
アイディアやらプロンプトを考えては試す必要があるので、ちょっと時間かかるかも?

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