できるだけChatGPTだけで作るリバーシ(オセロ)ソフト その4 おまけ
前回書いたバグ修正部分も以下の質問で改善させました。
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文字だけなので対象部分を手動で直しました。
あとは、プレイ中には影響がないですが、以下のことも直しました。
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」では、改めて「探索関数」「評価関数」を考えてみようと思います。
アイディアやらプロンプトを考えては試す必要があるので、ちょっと時間かかるかも?