こんにちは、zawato(@zawato7)です!
今回は、PythonでオセロのGUIを実装してみたので、コードを解説したいと思います。
PythonでオセロのGUIを実装したいと考えている方は、是非参考にしてみてください。
- 実装結果
- 実装内容
- プログラムの詳細
- _init_(self, root)
- create_board(self)
- initialize_board(self)
- place_piece(self, row, col, color)
- handle_click(self, event)
- is_valid_move(self, row, col, color)
- check_direction(self, row, col, direction, color)
- flip_pieces(self, row, col)
- flip_in_direction(self, row, col, direction)
- highlight_valid_moves(self)
- has_valid_moves(self, color)
- pass_turn(self)
- end_game(self)
- create_sidebar(self)
- update_turn_display(self)
- update_score(self)
- count_pieces(self)
- おわりに
実装結果
今回、実装したオセロの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)
コンストラクタで、ゲームの初期化を行います。root
はTkinter
のウィンドウオブジェクトです。
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を実装してみました。
実行してみた感想や改善点などがありましたら、是非コメントしてください!
コメント