プログラミング

PythonでオセロのGUIを実装してみた #Python #Tkinter

アイキャッチ_オセロGUI プログラミング

こんにちは、zawato(@zawato7)です!

今回は、PythonでオセロのGUIを実装してみたので、コードを解説したいと思います。

PythonでオセロのGUIを実装したいと考えている方は、是非参考にしてみてください。

実装結果

今回、実装したオセロのGUIは以下になります。

実装内容

GitHubにて、コードを公開してます。

環境

  • Python 3.11.7
  • Tkinter 8.6

プログラムの全体像

import tkinter as tk

class OthelloGame:
    def __init__(self, root):
        # rootオブジェクト
        self.root = root
        # ウィンドウのタイトル
        self.root.title("Othello Game")
        # 盤面サイズ
        self.board_size = 8
        # マス目の大きさ
        self.cell_size = 50
        # ボードの状態(黒、白、None)
        self.board = [[None for _ in range(self.board_size)] for _ in range(self.board_size)]
        # 先行を黒に指定
        self.turn = "black"
        self.create_sidebar()
        self.create_board()
        self.initialize_board()
        self.update_turn_display()
        self.update_score()
        self.highlight_valid_moves()

    def create_board(self):
        # Canvasの定義
        self.canvas = tk.Canvas(self.root, width=self.board_size * self.cell_size, height=self.board_size * self.cell_size)
        # グリッドの作成
        self.canvas.grid(row=0, column=0)
        # グリッドの設定
        for i in range(self.board_size):
            for j in range(self.board_size):
                x0 = i * self.cell_size
                y0 = j * self.cell_size
                x1 = x0 + self.cell_size
                y1 = y0 + self.cell_size
                self.canvas.create_rectangle(x0, y0, x1, y1, outline="black", fill="green")
                
        # クリックイベント(左ボタンクリック時に、handle_clickメソッドを呼び出す)
        self.canvas.bind("<Button-1>", self.handle_click)

    def initialize_board(self):
        # 初期盤面
        center = self.board_size // 2
        self.place_piece(center-1, center-1, "white")
        self.place_piece(center  , center  , "white")
        self.place_piece(center-1, center  , "black")
        self.place_piece(center  , center-1, "black")

    def place_piece(self, row, col, color):
        x0 = col     * self.cell_size + self.cell_size // 4
        y0 = row     * self.cell_size + self.cell_size // 4
        x1 = (col+1) * self.cell_size - self.cell_size // 4
        y1 = (row+1) * self.cell_size - self.cell_size // 4
        # マスの状態を更新
        self.board[row][col] = color
        # 駒を描画
        self.canvas.create_oval(x0, y0, x1, y1, fill=color)

    def handle_click(self, event):
        # クリック位置からマスを判定
        col = event.x // self.cell_size
        row = event.y // self.cell_size
        
        # クリック位置が正しくない場合は、無効
        if not (0 <= row < self.board_size and 0 <= col < self.board_size):
            return
        
        # 合法手かどうか判定し、
        if self.is_valid_move(row, col, self.turn):
            # 駒を置く
            self.place_piece(row, col, self.turn)
            # 駒をひっくり返す
            self.flip_pieces(row, col)
            # スコアの更新
            self.update_score()
            # 手番の更新
            self.turn = "white" if self.turn == "black" else "black"
            # 盤面を更新
            self.update_turn_display()
            # 駒を置けるマスをハイライト表示
            self.highlight_valid_moves()
            
            # 駒を置けるマスがなければ、パス
            if not self.has_valid_moves(self.turn):
                self.pass_turn()

    def is_valid_move(self, row, col, color):
        # 既に駒が置かれていれば、Falseを返す。
        if self.board[row][col] is not None:
            return False
        # 八方向(縦、横、斜め)
        directions = [(0, 1), (1, 0), (0, -1), (-1, 0), (1, 1), (-1, -1), (1, -1), (-1, 1)]
        # デフォルトをFalseに設定
        valid = False
        # 各方向のマスの状態を確認
        for direction in directions:
            if self.check_direction(row, col, direction, color):
                valid = True
        return valid

    def check_direction(self, row, col, direction, color):
        # 相手の駒の色を代入
        opponent_color = "white" if color == "black" else "black"
        # 指定の方向のマスを確認
        d_row, d_col = direction
        row += d_row
        col += d_col
        # 盤面外であれば、Falseを返す
        if not (0 <= row < self.board_size and 0 <= col < self.board_size):
            return False
        # 相手の駒がなければ、Falseを返す
        if self.board[row][col] != opponent_color:
            return False
        while 0 <= row < self.board_size and 0 <= col < self.board_size:
            # マスがNoneであれば、Falseを返す
            if self.board[row][col] is None:
                return False
            # 自分の駒があれば、Trueを返す
            if self.board[row][col] == color:
                return True
            row += d_row
            col += d_col
        return False

    def flip_pieces(self, row, col):
        directions = [(0, 1), (1, 0), (0, -1), (-1, 0), (1, 1), (-1, -1), (1, -1), (-1, 1)]
        for direction in directions:
            if self.check_direction(row, col, direction, self.turn):
                self.flip_in_direction(row, col, direction)

    def flip_in_direction(self, row, col, direction):
        opponent_color = "white" if self.turn == "black" else "black"
        d_row, d_col = direction
        row += d_row
        col += d_col
        while self.board[row][col] == opponent_color:
            # 相手の駒を自分の駒に置き換える
            self.place_piece(row, col, self.turn)
            row += d_row
            col += d_col

    def highlight_valid_moves(self):
        # 前の盤面でのハイライトを削除
        self.canvas.delete("highlight")
        has_moves = False
        for row in range(self.board_size):
            for col in range(self.board_size):
                if self.is_valid_move(row, col, self.turn):
                    has_moves = True
                    x0 = col * self.cell_size + self.cell_size // 2 - 5
                    y0 = row * self.cell_size + self.cell_size // 2 - 5
                    x1 = col * self.cell_size + self.cell_size // 2 + 5
                    y1 = row * self.cell_size + self.cell_size // 2 + 5
                    self.canvas.create_oval(x0, y0, x1, y1, fill="gray", tags="highlight")
        if not has_moves:
            print(f"{self.turn.capitalize()} has no valid moves")

    def has_valid_moves(self, color):
        for row in range(self.board_size):
            for col in range(self.board_size):
                if self.is_valid_move(row, col, color):
                    return True
        return False

    def pass_turn(self):
        # パスしたら次のプレイヤーに手番を渡す
        self.turn = "white" if self.turn == "black" else "black"
        self.update_turn_display()
        self.highlight_valid_moves()

        # 次のプレイヤーにも合法手がない場合、ゲームを終了する
        if not self.has_valid_moves(self.turn):
            self.end_game()

    def end_game(self):
        black_count, white_count = self.count_pieces()
        if black_count > white_count:
            winner = "黒の勝利!"
        elif white_count > black_count:
            winner = "白の勝利!"
        else:
            winner = "引き分け!"

        self.canvas.create_text(
            self.board_size * self.cell_size // 2,
            self.board_size * self.cell_size // 2,
            text=f"{winner}",
            font=("Helvetica", 36),
            fill="red"
        )

    def create_sidebar(self):
        self.sidebar = tk.Frame(self.root)
        self.sidebar.grid(row=0, column=1, sticky="ns")

        self.turn_label = tk.Label(self.sidebar, text="Turn: Black", font=("Helvetica", 14))
        self.turn_label.pack(pady=10)

        self.score_label = tk.Label(self.sidebar, text="", font=("Helvetica", 14))
        self.score_label.pack(pady=10)

    def update_turn_display(self):
        self.turn_label.config(text=f"Turn: {self.turn.capitalize()}")

    def update_score(self):
        black_count, white_count = self.count_pieces()
        self.score_label.config(text=f"Black: {black_count}  White: {white_count}")

    def count_pieces(self):
        black_count = 0
        white_count = 0
        for row in range(self.board_size):
            for col in range(self.board_size):
                if self.board[row][col] == "black":
                    black_count += 1
                elif self.board[row][col] == "white":
                    white_count += 1
        return black_count, white_count

if __name__ == "__main__":
    root = tk.Tk()
    game = OthelloGame(root)
    root.mainloop()

プログラムの詳細

ここからは、プログラムの処理について詳しく紹介していきます。

今回は、Tkinterと呼ばれるPythonでGUIを実装するためのツールキットを利用します。

import tkinter as tk

そして、OthelloGameというクラスを定義します。

class OthelloGame:

以下、各メソッドの解説になります。

_init_(self, root)

コンストラクタで、ゲームの初期化を行います。rootTkinterのウィンドウオブジェクトです。

  • board_size:ボードの大きさ(N×N)
  • cell_size:1マスのサイズ(ピクセル単位)
  • board:8×8の2次元リストで、各マスに置かれている駒の状態(”black”、”white”、またはNone)を保持
  • turn:手番の色(初期状態は黒)
  • pass_count:連続パスの回数を記録(2回連続でパスが発生したらゲーム終了)
def __init__(self, root):
    # rootオブジェクト
    self.root = root
    # ウィンドウのタイトル
    self.root.title("Othello Game")
    # 盤面サイズ
    self.board_size = 8
    # マス目の大きさ
    self.cell_size = 50
    # ボードの状態(黒、白、None)
    self.board = [[None for _ in range(self.board_size)] for _ in range(self.board_size)]
    # 先行を黒に指定
    self.turn = "black"
    self.create_sidebar()
    self.create_board()
    self.initialize_board()
    self.update_turn_display()
    self.update_score()
    self.highlight_valid_moves()

create_board(self)

ボードの描画を行います。Canvasウィジェットを使用して、8×8のグリッドを描きます。さらに、クリックイベントをボードにバインドし、クリックされた場所に駒を置くための処理を行います。

def create_board(self):
    # Canvasの定義
    self.canvas = tk.Canvas(self.root, width=self.board_size * self.cell_size, height=self.board_size * self.cell_size)
    # グリッドの作成
    self.canvas.grid(row=0, column=0)
    # グリッドの設定
    for i in range(self.board_size):
        for j in range(self.board_size):
            x0 = i * self.cell_size
            y0 = j * self.cell_size
            x1 = x0 + self.cell_size
            y1 = y0 + self.cell_size
            self.canvas.create_rectangle(x0, y0, x1, y1, outline="black", fill="green")

    # クリックイベント(左ボタンクリック時に、handle_clickメソッドを呼び出す)
    self.canvas.bind("<Button-1>", self.handle_click)

initialize_board(self)

オセロの初期状態として、ボードの中央4マスに白と黒の駒を配置します。

def initialize_board(self):
    # 初期盤面
    center = self.board_size // 2
    self.place_piece(center-1, center-1, "white")
    self.place_piece(center  , center  , "white")
    self.place_piece(center-1, center  , "black")
    self.place_piece(center  , center-1, "black")

place_piece(self, row, col, color)

指定された行と列に、指定された色の駒を置く関数です。Canvasに描画される円(駒)を作成し、内部データのboardにも駒の情報を保存します。

def place_piece(self, row, col, color):
    x0 = col     * self.cell_size + self.cell_size // 4
    y0 = row     * self.cell_size + self.cell_size // 4
    x1 = (col+1) * self.cell_size - self.cell_size // 4
    y1 = (row+1) * self.cell_size - self.cell_size // 4
    # マスの状態を更新
    self.board[row][col] = color
    # 駒を描画
    self.canvas.create_oval(x0, y0, x1, y1, fill=color)

handle_click(self, event)

マウスのクリックイベントが発生すると呼び出されます。

  • クリック位置(x, y)をボードの行と列に変換し、そのマスに駒が置けるかどうかを判定します。
  • is_valid_move() で合法手かどうかをチェックし、合法手であればその場所に駒を置き、駒をひっくり返します。
  • その後、手番を交代させ、ハイライトを更新します。
def handle_click(self, event):
    # クリック位置からマスを判定
    col = event.x // self.cell_size
    row = event.y // self.cell_size

    # クリック位置が正しくない場合は、無効
    if not (0 <= row < self.board_size and 0 <= col < self.board_size):
        return

    # 合法手かどうか判定し、
    if self.is_valid_move(row, col, self.turn):
        # 駒を置く
        self.place_piece(row, col, self.turn)
        # 駒をひっくり返す
        self.flip_pieces(row, col)
        # スコアの更新
        self.update_score()
        # 手番の更新
        self.turn = "white" if self.turn == "black" else "black"
        # 盤面を更新
        self.update_turn_display()
        # 駒を置けるマスをハイライト表示
        self.highlight_valid_moves()

        # 駒を置けるマスがなければ、パス
        if not self.has_valid_moves(self.turn):
            self.pass_turn()

is_valid_move(self, row, col, color)

指定された行・列が合法手かどうかをチェックします。周囲8方向(上下左右、斜め)の駒をチェックし、挟める相手の駒があるかを確認します。

周囲の駒が相手の駒で始まる必要があり、その後に自分の駒で囲むことができれば合法手です。

def is_valid_move(self, row, col, color):
    # 既に駒が置かれていれば、Falseを返す。
    if self.board[row][col] is not None:
        return False
    # 八方向(縦、横、斜め)
    directions = [(0, 1), (1, 0), (0, -1), (-1, 0), (1, 1), (-1, -1), (1, -1), (-1, 1)]
    # デフォルトをFalseに設定
    valid = False
    # 各方向のマスの状態を確認
    for direction in directions:
        if self.check_direction(row, col, direction, color):
            valid = True
    return valid

check_direction(self, row, col, direction, color)

is_valid_move()で使用する補助関数で、特定の方向に向かって駒を確認します。

相手の駒を挟んで自分の駒で終わっているかをチェックします。

def check_direction(self, row, col, direction, color):
    # 相手の駒の色を代入
    opponent_color = "white" if color == "black" else "black"
    # 指定の方向のマスを確認
    d_row, d_col = direction
    row += d_row
    col += d_col
    # 盤面外であれば、Falseを返す
    if not (0 <= row < self.board_size and 0 <= col < self.board_size):
        return False
    # 相手の駒がなければ、Falseを返す
    if self.board[row][col] != opponent_color:
        return False
    while 0 <= row < self.board_size and 0 <= col < self.board_size:
        # マスがNoneであれば、Falseを返す
        if self.board[row][col] is None:
            return False
        # 自分の駒があれば、Trueを返す
        if self.board[row][col] == color:
            return True
        row += d_row
        col += d_col
    return False

flip_pieces(self, row, col)

自分の駒で挟んでいる相手の駒をひっくり返す処理です。

各方向に対して、ひっくり返すことが可能であれば、flip_in_direction()関数を呼び出します。

def flip_pieces(self, row, col):
    directions = [(0, 1), (1, 0), (0, -1), (-1, 0), (1, 1), (-1, -1), (1, -1), (-1, 1)]
    for direction in directions:
        if self.check_direction(row, col, direction, self.turn):
            self.flip_in_direction(row, col, direction)

flip_in_direction(self, row, col, direction)

flip_pieces()で使用する補助関数で、特定の方向に向かって相手の駒をひっくり返します。

def flip_in_direction(self, row, col, direction):
    opponent_color = "white" if self.turn == "black" else "black"
    d_row, d_col = direction
    row += d_row
    col += d_col
    while self.board[row][col] == opponent_color:
        # 相手の駒を自分の駒に置き換える
        self.place_piece(row, col, self.turn)
        row += d_row
        col += d_col

highlight_valid_moves(self)

現在の手番で駒を置けるマスをハイライト表示します。

ハイライトは小さな灰色の円で示され、置ける場所を視覚的にわかりやすくしています。

def highlight_valid_moves(self):
    # 前の盤面でのハイライトを削除
    self.canvas.delete("highlight")
    has_moves = False
    for row in range(self.board_size):
        for col in range(self.board_size):
            if self.is_valid_move(row, col, self.turn):
                has_moves = True
                x0 = col * self.cell_size + self.cell_size // 2 - 5
                y0 = row * self.cell_size + self.cell_size // 2 - 5
                x1 = col * self.cell_size + self.cell_size // 2 + 5
                y1 = row * self.cell_size + self.cell_size // 2 + 5
                self.canvas.create_oval(x0, y0, x1, y1, fill="gray", tags="highlight")
    if not has_moves:
        print(f"{self.turn.capitalize()} has no valid moves")

has_valid_moves(self, color)

合法手があるかどうかを判定します。

def has_valid_moves(self, color):
    for row in range(self.board_size):
        for col in range(self.board_size):
            if self.is_valid_move(row, col, color):
                return True
    return False

pass_turn(self)

駒を置ける合法手がない場合にパスを行う関数です。

def pass_turn(self):
    # パスしたら次のプレイヤーに手番を渡す
    self.turn = "white" if self.turn == "black" else "black"
    self.update_turn_display()
    self.highlight_valid_moves()

    # 次のプレイヤーにも合法手がない場合、ゲームを終了する
    if not self.has_valid_moves(self.turn):
        self.end_game()

end_game(self)

ゲームが終了したときに、勝者を判定して画面に表示します。

黒と白の駒の数を比較し、多い方が勝者です。同点の場合は引き分けと表示されます。

def end_game(self):
    black_count, white_count = self.count_pieces()
    if black_count > white_count:
        winner = "黒の勝利!"
    elif white_count > black_count:
        winner = "白の勝利!"
    else:
        winner = "引き分け!"

    self.canvas.create_text(
        self.board_size * self.cell_size // 2,
        self.board_size * self.cell_size // 2,
        text=f"{winner}",
        font=("Helvetica", 36),
        fill="red"
    )

create_sidebar(self)

サイドバーを作成し、現在の手番とスコア(黒と白の駒の数)を表示するためのウィジェットを配置します。

def create_sidebar(self):
    self.sidebar = tk.Frame(self.root)
    self.sidebar.grid(row=0, column=1, sticky="ns")

    self.turn_label = tk.Label(self.sidebar, text="Turn: Black", font=("Helvetica", 14))
    self.turn_label.pack(pady=10)

    self.score_label = tk.Label(self.sidebar, text="", font=("Helvetica", 14))
    self.score_label.pack(pady=10)

update_turn_display(self)

現在の手番をサイドバーに表示します。

def update_turn_display(self):
    self.turn_label.config(text=f"Turn: {self.turn.capitalize()}")

update_score(self)

現在の黒と白の駒の数をカウントし、スコアを更新してサイドバーに表示します。

def update_score(self):
    black_count, white_count = self.count_pieces()
    self.score_label.config(text=f"Black: {black_count}  White: {white_count}")

count_pieces(self)

ボード上の黒と白の駒の数をカウントし、結果を返します。

def count_pieces(self):
    black_count = 0
    white_count = 0
    for row in range(self.board_size):
        for col in range(self.board_size):
            if self.board[row][col] == "black":
                black_count += 1
            elif self.board[row][col] == "white":
                white_count += 1
    return black_count, white_count

おわりに

今回は、PythonでオセロのGUIを実装してみました。

実行してみた感想や改善点などがありましたら、是非コメントしてください!

プロフィール
zawato

データサイエンティストとして3年の実務経験あり。
情報学修士卒。Python歴6年。
このブログでは、主にプログラミングやIT技術関連、エンジニア向けにちょっとした役立つ情報を発信しています。

zawatoをフォローする!
よかったらシェアしてね!
zawatoをフォローする!

コメント

タイトルとURLをコピーしました