🔰

誰でもできる~オセロCPUを強化編 二日目

に公開

CPUを強化する方法

オセロのCPUを強化するには、追加の技術が必要だ。その中でも、ミニマックスアルゴリズムが基本的なアプローチになるらしい。

ミニマックスアルゴリズムとは?

オセロCPUの基盤を作るアルゴリズムは、ミニマックスだ。これは、全ての可能な手を評価し、自分にとって最善の手を選ぶというものだ。

ミニマックスアルゴリズムは、現在の盤面から将来の手を予測し、相手の手を想定しながら、自分の最適な手を選ぶという方法だ。

盤面評価関数とは?

盤面評価関数は、オセロCPUがどの手を選ぶかを決定するための重要な要素だ。単に石の数だけでなく、コーナーや辺を占めることが勝利に繋がるため、その評価を含めることが大事だ。特に終盤では、石の配置を細かく評価することで、より強いCPUを作れるようになる。

評価関数では、石の数をはじめ、石の配置やボードの特定の場所に置かれた石が勝利にどれだけ貢献するかをスコア化する。例えば、コーナーを占めると一気に有利になるため、その評価を高く設定することが効果的だ。これにより、CPUは単に「多くの石を置く」ではなく、戦略的に「有利な位置に石を置く」ことを目指すようになる。
一手先二手先を計算の強さで強さのレベルを作成

最強レベルは三手先読み(負けました)

チャットGPT君の限界

オセロCPUを作る上で便利なツールとして、OpenAIのチャットGPTがある。確かに、コードの基本的な部分は簡単に書いてくれる。しかし、何度指示しても、コードの微調整や最適化には限界があり、修正が上手くいかないことが多い。意思の疎通がいまいち、疎遠である。

例えば、特定のアルゴリズムの調整や、評価関数の改善を試みる際に、思った通りの動作をしてくれないことが多く、何度も指示を出しても最適解にたどり着かない。コードの微調整や最適化を行うには、AIだけでは限界があると感じる場面が多かった。

VSコードGemini

そこで、私はVSコードのGeminiも追加で使用した。Geminiは、コードの修正に優れていて、アプリの開発がスムーズに進むようになった。特に、ミニマックスアルゴリズムの調整や、評価関数の改善において大きな助けとなった。GeminiはチャットGPTより、私が求めるように細かい部分まで最適化を施してくれるため、作業効率が格段に向上した。

以下今回のコード


import tkinter as tk
import random
import math
import copy # 盤面のディープコピーに使用

# 定数の設定
SIZE = 8
CELL_SIZE = 60

# 各マスの状態を定義(空、黒、白)
EMPTY, BLACK, WHITE = 0, 1, 2
DIRECTIONS = [(-1, -1), (-1, 0), (-1, 1),
              (0, -1),          (0, 1),
              (1, -1), (1, 0), (1, 1)]

# --- 追加:評価用の盤面重み付け ---
BOARD_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]
]

class Othello:
    def __init__(self, master):
        self.master = master
        self.master.title("Othello (レベル選択)") # タイトル変更

        # --- UI要素の作成 ---
        # 1. 開始設定フレーム (先攻/後攻、強さレベル)
        self.settings_frame = tk.Frame(master)
        self.settings_frame.pack()

        # 1a. 先攻/後攻選択フレーム
        self.start_frame = tk.Frame(self.settings_frame)
        self.start_frame.pack(pady=5)
        tk.Label(self.start_frame, text="対戦方法:").pack(side="left", padx=5)
        self.black_button = tk.Button(self.start_frame, text="黒(先攻)", command=lambda: self.start_game(BLACK))
        self.white_button = tk.Button(self.start_frame, text="白(後攻)", command=lambda: self.start_game(WHITE))
        self.black_button.pack(side="left", padx=2)
        self.white_button.pack(side="left", padx=2)

        # 1b. 強さレベル選択フレーム
        self.difficulty_frame = tk.Frame(self.settings_frame)
        self.difficulty_frame.pack(pady=5)
        tk.Label(self.difficulty_frame, text="コンピュータの強さ:").pack(side="left", padx=5)
        self.difficulty_var = tk.IntVar(value=2) # デフォルトレベル2
        levels = [("弱い(1)", 1), ("普通(2)", 2), ("強い(3)", 3)]
        for text, level in levels:
            rb = tk.Radiobutton(self.difficulty_frame, text=text, variable=self.difficulty_var, value=level)
            rb.pack(side="left", padx=2)

        # 2. 情報表示ラベル
        self.info_label = tk.Label(master, text="対戦方法と強さを選んで開始してください")
        self.info_label.pack()

        # 3. オセロ盤キャンバス
        self.canvas = tk.Canvas(master, width=SIZE*CELL_SIZE, height=SIZE*CELL_SIZE)
        self.canvas.pack()
        self.canvas.bind("<Button-1>", self.click)

        # 4. リスタートボタン (最初は非表示)
        self.restart_button = tk.Button(master, text="リスタート", command=self.reset_game)
        self.restart_button.pack()
        self.restart_button.pack_forget()

        # --- ゲーム状態変数 ---
        self.turn = None
        self.board = None
        self.game_active = False
        self.after_id = None
        self.player_color = None
        self.computer_color = None
        self.difficulty_level = None # 選択された強さレベル

    def start_game(self, player_color):
        # 選択された強さレベルを取得
        self.difficulty_level = self.difficulty_var.get()
        self.player_color = player_color
        self.computer_color = WHITE if player_color == BLACK else BLACK
        self.turn = BLACK
        self.game_active = True

        # 設定UIを非表示に
        self.settings_frame.pack_forget()

        self.reset_board()
        self.draw_board()
        self.update_info()

        # コンピュータが先攻の場合
        if self.turn == self.computer_color:
            self.info_label.config(text="コンピュータ思考中...")
            self.master.update()
            self.after_id = self.master.after(100, self.computer_move)

    def reset_board(self):
        self.board = [[EMPTY]*SIZE for _ in range(SIZE)]
        self.board[3][3], self.board[4][4] = WHITE, WHITE
        self.board[3][4], self.board[4][3] = BLACK, BLACK

    def reset_game(self):
        if self.after_id:
            self.master.after_cancel(self.after_id)
            self.after_id = None

        self.game_active = False
        self.restart_button.pack_forget()
        # 設定UIを再表示
        self.settings_frame.pack()
        self.info_label.config(text="対戦方法と強さを選んで開始してください")
        self.canvas.delete("all")
        # 変数初期化
        self.turn = None
        self.board = None
        self.player_color = None
        self.computer_color = None
        self.difficulty_level = None

    def draw_board(self):
        self.canvas.delete("all")
        for i in range(SIZE):
            for j in range(SIZE):
                x0, y0 = j*CELL_SIZE, i*CELL_SIZE
                x1, y1 = x0 + CELL_SIZE, y0 + CELL_SIZE
                self.canvas.create_rectangle(x0, y0, x1, y1, fill="green", outline="black")
                if self.board[i][j] == BLACK:
                    self.canvas.create_oval(x0+5, y0+5, x1-5, y1-5, fill="black")
                elif self.board[i][j] == WHITE:
                    self.canvas.create_oval(x0+5, y0+5, x1-5, y1-5, fill="white")

    def click(self, event):
        if not self.game_active or self.turn != self.player_color:
            return

        row, col = event.y // CELL_SIZE, event.x // CELL_SIZE

        stones_to_flip = self.get_stones_to_flip(row, col, self.player_color)
        if stones_to_flip is not None:
            self.make_move(row, col, self.player_color, stones_to_flip)
            self.draw_board()
            self.turn = self.computer_color
            self.update_info()

            self.info_label.config(text="コンピュータ思考中...")
            self.master.update()
            self.after_id = self.master.after(100, self.computer_move)
        else:
            player_can_move = any(self.get_stones_to_flip(i, j, self.player_color) is not None for i in range(SIZE) for j in range(SIZE))
            if not player_can_move:
                self.info_label.config(text="あなたはパスしました。コンピュータの番です。")
                self.turn = self.computer_color
                self.info_label.config(text="コンピュータ思考中...")
                self.master.update()
                self.after_id = self.master.after(1000, self.computer_move)

    def get_stones_to_flip(self, row, col, color):
        if not (0 <= row < SIZE and 0 <= col < SIZE and self.board[row][col] == EMPTY):
            return None
        opponent_color = 3 - color
        stones_to_flip_total = []
        for dr, dc in DIRECTIONS:
            r, c = row + dr, col + dc
            flipped_in_direction = []
            while 0 <= r < SIZE and 0 <= c < SIZE and self.board[r][c] == opponent_color:
                flipped_in_direction.append((r, c))
                r += dr
                c += dc
            if flipped_in_direction and 0 <= r < SIZE and 0 <= c < SIZE and self.board[r][c] == color:
                stones_to_flip_total.extend(flipped_in_direction)
        return stones_to_flip_total if stones_to_flip_total else None

    def make_move(self, row, col, color, stones_to_flip):
        self.board[row][col] = color
        for fr, fc in stones_to_flip:
            self.board[fr][fc] = color

    def evaluate(self, board_state):
        black_score = 0
        white_score = 0
        black_stones = 0
        white_stones = 0
        for r in range(SIZE):
            for c in range(SIZE):
                if board_state[r][c] == BLACK:
                    black_score += BOARD_WEIGHTS[r][c]
                    black_stones += 1
                elif board_state[r][c] == WHITE:
                    white_score += BOARD_WEIGHTS[r][c]
                    white_stones += 1
        stone_diff = black_stones - white_stones
        weight_diff = black_score - white_score
        final_score = stone_diff + weight_diff
        return final_score if self.computer_color == BLACK else -final_score

    def minimax(self, board_state, depth, is_maximizing_player, current_color):
        valid_moves = []
        for r in range(SIZE):
            for c in range(SIZE):
                stones = self.get_stones_to_flip_on_board(board_state, r, c, current_color)
                if stones is not None:
                    valid_moves.append(((r, c), stones))

        if depth == 0 or not valid_moves:
            return self.evaluate(board_state)

        opponent_color = 3 - current_color
        if is_maximizing_player:
            max_eval = -math.inf
            for move, stones in valid_moves:
                new_board = [row[:] for row in board_state]
                self.make_move_on_board(new_board, move[0], move[1], current_color, stones)
                eval_score = self.minimax(new_board, depth - 1, False, opponent_color)
                max_eval = max(max_eval, eval_score)
            return max_eval
        else:
            min_eval = math.inf
            for move, stones in valid_moves:
                new_board = [row[:] for row in board_state]
                self.make_move_on_board(new_board, move[0], move[1], current_color, stones)
                eval_score = self.minimax(new_board, depth - 1, True, opponent_color)
                min_eval = min(min_eval, eval_score)
            return min_eval

    def get_stones_to_flip_on_board(self, board_state, row, col, color):
        if not (0 <= row < SIZE and 0 <= col < SIZE and board_state[row][col] == EMPTY):
            return None
        opponent_color = 3 - color
        stones_to_flip_total = []
        for dr, dc in DIRECTIONS:
            r, c = row + dr, col + dc
            flipped_in_direction = []
            while 0 <= r < SIZE and 0 <= c < SIZE and board_state[r][c] == opponent_color:
                flipped_in_direction.append((r, c))
                r += dr
                c += dc
            if flipped_in_direction and 0 <= r < SIZE and 0 <= c < SIZE and board_state[r][c] == color:
                stones_to_flip_total.extend(flipped_in_direction)
        return stones_to_flip_total if stones_to_flip_total else None

    def make_move_on_board(self, board_state, row, col, color, stones_to_flip):
        board_state[row][col] = color
        for fr, fc in stones_to_flip:
            board_state[fr][fc] = color

    def computer_move(self):
        """コンピュータの手を決定して実行する(レベル別)"""
        if not self.game_active:
            return

        # 有効な手を探す
        valid_moves = []
        for r in range(SIZE):
            for c in range(SIZE):
                stones = self.get_stones_to_flip(r, c, self.computer_color)
                if stones is not None:
                    valid_moves.append(((r, c), stones))

        # 打てる手がない場合はパス
        if not valid_moves:
            self.info_label.config(text="コンピュータはパスしました。あなたの番です。")
            self.turn = self.player_color
            self.after_id = self.master.after(1000, self.update_info)
            return

        best_move = None

        # --- レベルに応じた思考 ---
        level = self.difficulty_level

        if level == 1: # レベル1: ランダム
            best_move = random.choice(valid_moves)

        else: # レベル2 or 3: ミニマックス
            best_value = -math.inf
            # レベルに応じて探索深度を設定
            search_depth = 1 if level == 2 else 3 # レベル2: 1手読み, レベル3: 3手読み (調整可能)

            for move, stones in valid_moves:
                board_copy = [row[:] for row in self.board]
                self.make_move_on_board(board_copy, move[0], move[1], self.computer_color, stones)
                move_value = self.minimax(board_copy, search_depth - 1, False, self.player_color)

                if move_value > best_value:
                    best_value = move_value
                    best_move = (move, stones)

            # もし何らかの理由で best_move が決まらなかった場合のフォールバック (ほぼ不要なはず)
            if best_move is None:
                print("警告: ミニマックスで最良手が見つからず、ランダムな手を選びます。")
                best_move = random.choice(valid_moves)

        # --- 最良手を実行 ---
        if best_move:
            move_coords, stones_to_flip = best_move
            self.make_move(move_coords[0], move_coords[1], self.computer_color, stones_to_flip)
            self.draw_board()
            self.turn = self.player_color
            self.update_info()
        else:
             # valid_moves があるのに best_move が None になることは通常ない
             print("エラー: 実行する手が見つかりません。")

        self.after_id = None

    def update_info(self):
        if not self.game_active:
             return

        black_count = sum(row.count(BLACK) for row in self.board)
        white_count = sum(row.count(WHITE) for row in self.board)
        empty_count = SIZE * SIZE - black_count - white_count

        current_player_can_move = any(self.get_stones_to_flip(i, j, self.turn) is not None for i in range(SIZE) for j in range(SIZE))
        opponent_color = 3 - self.turn
        opponent_can_move = any(self.get_stones_to_flip(i, j, opponent_color) is not None for i in range(SIZE) for j in range(SIZE))

        if empty_count == 0 or (not current_player_can_move and not opponent_can_move):
            self.game_active = False
            # ... (勝敗判定と表示は変更なし) ...
            if black_count > white_count: result = "黒の勝ち!"
            elif white_count > black_count: result = "白の勝ち!"
            else: result = "引き分け"
            if (self.player_color == BLACK and black_count > white_count) or \
               (self.player_color == WHITE and white_count > black_count): final_message = f"あなたの勝ち! ({result})"
            elif (self.player_color == BLACK and white_count > black_count) or \
                 (self.player_color == WHITE and black_count > white_count): final_message = f"コンピュータの勝ち ({result})"
            else: final_message = result
            self.info_label.config(text=f"ゲーム終了:黒 {black_count} - 白 {white_count}{final_message}")
            self.restart_button.pack()
            if self.after_id:
                self.master.after_cancel(self.after_id)
                self.after_id = None
        else:
            # パス処理
            if not current_player_can_move:
                pass_player = "あなた" if self.turn == self.player_color else "コンピュータ"
                self.info_label.config(text=f"{pass_player}はパスしました。相手の番です。")
                self.turn = opponent_color
                if self.turn == self.computer_color:
                    self.info_label.config(text="コンピュータ思考中...")
                    self.master.update()
                    self.after_id = self.master.after(1000, self.computer_move)
                else:
                    turn_text = "あなたの番です(黒)" if self.player_color == BLACK else "あなたの番です(白)"
                    self.info_label.config(text=f"{turn_text} (黒:{black_count} 白:{white_count})")
            else:
                # 通常の手番表示
                if self.turn == self.player_color:
                    turn_text = "あなたの番です(黒)" if self.player_color == BLACK else "あなたの番です(白)"
                else:
                    turn_text = "コンピューターの番です(白)" if self.computer_color == WHITE else "コンピューターの番です(黒)"
                self.info_label.config(text=f"{turn_text} (黒:{black_count} 白:{white_count})")

# アプリケーションの起動
if __name__ == "__main__":
    root = tk.Tk()
    game = Othello(root)
    root.mainloop()

Discussion