Pygameでオセロゲームを作成する方法をソースコード付きで詳しく解説します。
【1】オセロゲーム(敵CPUなし)
Pygameでオセロゲームを作成する方法を解説します。
まず、オセロゲームの動作を理解するために、敵CPUなし(白側と黒側は人が操作)のものから紹介します。
【1-1】サンプルコード①
【1-2】サンプルコード①の解説
上記コードの各部分について解説します。
1. インポートと定数の定義
import pygame
import sys
# 定数の定義
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GREEN = (0, 128, 0)
YELLOW = (255, 155, 0)
SIZE = 600
BOARD_SIZE = 12
GRID_SIZE = SIZE // BOARD_SIZE
- pygameとsysモジュールをインポートします。
- BLACK、WHITE、GREEN、YELLOWは色をRGB形式で定義しています。
- SIZEは画面サイズ、BOARD_SIZEはボードのマス目の数です。
- GRID_SIZEは各マスのサイズで、「画面サイズ」を「ボードのマス目の数」で割ることで計算しています。
2. Pygameの初期化とウィンドウ設定
pygame.init()
screen = pygame.display.set_mode((SIZE, SIZE))
pygame.display.set_caption("オセロゲーム")
- pygame.init()でPygameを初期化します。
- pygame.display.set_modeでウィンドウサイズを設定し、pygame.display.set_captionでウィンドウのタイトルを設定します。
Othelloクラスの定義
class Othello:
def __init__(self):
self.board = [[None] * BOARD_SIZE for _ in range(BOARD_SIZE)]
mid = BOARD_SIZE // 2
self.board[mid - 1][mid - 1] = WHITE
self.board[mid - 1][mid] = BLACK
self.board[mid][mid - 1] = BLACK
self.board[mid][mid] = WHITE
self.turn = BLACK
- Othelloクラスを定義し、初期化メソッドでボードを初期化します。つまり、ゲームの初期状態を設定し、ゲームを開始する準備を整えています。
self.board
は、オセロのゲームボードを表す2次元リストです。BOARD_SIZE
はボードのサイズ(例えば、8×8や12×12)を示します。BOARD_SIZE
xBOARD_SIZE
のボードを作成し、すべてのマスをNone
で初期化しています。None
は、そのマスにまだ石が置かれていないことを示します。mid
はボードの中央の位置を計算しています。BOARD_SIZE
を2で割り、中央のインデックスを求めます。例えば、BOARD_SIZE
が8の場合、mid
は4になります。- オセロゲームの初期配置(中央の4つのマスに、白と黒の石を交互に配置)をしています。
self.board[mid - 1][mid - 1] = WHITE
: 左上に白の石を配置。self.board[mid - 1][mid] = BLACK
: 右上に黒の石を配置。self.board[mid][mid - 1] = BLACK
: 左下に黒の石を配置。self.board[mid][mid] = WHITE
: 右下に白の石を配置。
- self.turnは現在のターンを示し、ゲームは黒の石から始まるため、
self.turn
をBLACK
に設定しています。
ボードの描画
以下の部分は、オセロゲームのボードを描画するためのメソッドです。
def draw_board(self):
screen.fill(GREEN)
for x in range(BOARD_SIZE):
for y in range(BOARD_SIZE):
rect = pygame.Rect(x * GRID_SIZE, y * GRID_SIZE, GRID_SIZE, GRID_SIZE)
pygame.draw.rect(screen, BLACK, rect, 1)
if self.board[x][y] is not None:
self.draw_stone(x, y, self.board[x][y])
screen.fill(GREEN)
は、画面全体を緑色で塗りつぶします。これにより、オセロボードの背景色が緑色になります。- 二重のforループを使用して、ボードの各マスを描画します。
for x in range(BOARD_SIZE)
: ボードの横方向のマスをループします。for y in range(BOARD_SIZE)
: ボードの縦方向のマスをループします。
pygame.Rect(x * GRID_SIZE, y * GRID_SIZE, GRID_SIZE, GRID_SIZE)
:- 各マスの位置とサイズを定義するための矩形(rect)を作成します。
x * GRID_SIZE
とy * GRID_SIZE
は、マスの左上隅の座標を計算します。GRID_SIZE
は、各マスの幅と高さを設定します。
pygame.draw.rect(screen, BLACK, rect, 1)
:- 定義した矩形を黒色で描画します。
- 最後の引数
1
は、矩形の線の太さを示します(ここでは1ピクセル)。
if self.board[x][y] is not None
:- ボードの現在のマスに石が置かれているかどうかをチェックします。
None
でない場合、そのマスには石が置かれています。
self.draw_stone(x, y, self.board[x][y])
:- 石が置かれている場合、
draw_stone
メソッドを呼び出して、その位置に石を描画します。 x
とy
は石の位置を示し、self.board[x][y]
は石の色(黒または白)を示します。
- 石が置かれている場合、
石の描画
以下は、オセロゲームのボード上に石を描画するためのメソッドです。
指定された位置に指定された色の石を描画します。
def draw_stone(self, x, y, color):
pygame.draw.circle(screen, color, (x * GRID_SIZE + GRID_SIZE // 2, y * GRID_SIZE + GRID_SIZE // 2), GRID_SIZE // 2 - 4)
draw_stone
は、オセロの石を描画するためのメソッドです。self
はクラスのインスタンスを指し、x
とy
は石を置く位置の座標を示します。color
は石の色(黒または白)を示します。pygame.draw.circle
は、円を描画するためのPygameの関数です。screen
は描画先の画面を示します。color
は円の色を指定します(ここでは石の色)。(x * GRID_SIZE + GRID_SIZE // 2, y * GRID_SIZE + GRID_SIZE // 2)
:- 円の中心座標を計算します。
x * GRID_SIZE
とy * GRID_SIZE
は、マスの左上隅の座標を示します。+ GRID_SIZE // 2
は、マスの中心に円を配置するためのオフセットです。
GRID_SIZE // 2 - 4
:- 円の半径を設定します。
GRID_SIZE // 2
はマスの半分のサイズを示し、- 4
は円がマスに収まるように少し小さくしています。
有効な手のチェック
このコードは、指定された位置に石を置くことが有効な手かどうかをチェックするメソッドです。
各方向に対して相手の石があるかを確認し、自分の石で挟めるかをチェックします。
def is_valid_move(self, x, y):
if self.board[x][y] is not None:
return False
opponent = WHITE if self.turn == BLACK else BLACK
valid = False
for dx, dy in [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]:
nx, ny = x + dx, y + dy
if 0 <= nx < BOARD_SIZE and 0 <= ny < BOARD_SIZE and self.board[nx][ny] == opponent:
while 0 <= nx < BOARD_SIZE and 0 <= ny < BOARD_SIZE:
nx += dx
ny += dy
if not (0 <= nx < BOARD_SIZE and 0 <= ny < BOARD_SIZE):
break
if self.board[nx][ny] is None:
break
if self.board[nx][ny] == self.turn:
valid = True
break
return valid
self.board[x][y]
がNone
でない場合、そのマスには既に石が置かれているため、有効な手ではありません。この場合、False
を返してメソッドを終了します。opponent = WHITE if self.turn == BLACK else BLACK
で現在のターンが黒なら相手は白、現在のターンが白なら相手は黒と設定します。self.turn
がBLACK
の場合、opponent
はWHITE
になります。その逆も同様です。valid
は有効な手かどうかを示すフラグです。初期値はFalse
に設定します。for dx, dy in [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
:- 8方向(左上、上、右上、左、右、左下、下、右下)の移動を表すペアをループします。
nx, ny = x + dx, y + dy
:- 現在の位置から1マス移動した位置を計算します。
if 0 <= nx < BOARD_SIZE and 0 <= ny < BOARD_SIZE and self.board[nx][ny] == opponent
:- 移動先がボードの範囲内であり、かつ相手の石が置かれているかをチェックします。
while 0 <= nx < BOARD_SIZE and 0 <= ny < BOARD_SIZE
:- ボードの範囲内である限り、さらにその方向に移動を続けます。
nx += dx, ny += dy
:- さらに1マスその方向に移動します。
if not (0 <= nx < BOARD_SIZE and 0 <= ny < BOARD_SIZE)
:- ボードの範囲外に出た場合、ループを終了します。
if self.board[nx][ny] is None
:- 空のマスに到達した場合、ループを終了します。
if self.board[nx][ny] == self.turn
:- 自分の石に到達した場合、その方向に有効な手があると判断し、
valid
をTrue
に設定してループを終了します。
- 自分の石に到達した場合、その方向に有効な手があると判断し、
マス目が全て石で埋まっているかの判定
マス目が全て石で埋まっているかの判定を行います。(全て埋まっていたらゲームを終了させるため)
def is_board_full(self):
for row in self.board:
if None in row:
return False
return True
- for row in self.board: は、ボードの各行を順番に取り出してループ処理を行います。self.boardは、オセロのボードを表す2次元リストです。
- if None in row: は、現在の行にNone(空のマス)が含まれているかどうかをチェックします。Noneが含まれている場合、その行にはまだ石が置かれていないマスがあることを意味します。
- return False は、空のマスが見つかった場合にメソッドを終了し、Falseを返します。つまり、ボードが全て埋まっていないことを示します。どの行にもNoneが含まれていない場合、return True が実行されます。これは、ボードが全て埋まっていることを示します。
石の反転
flip_stonesメソッドは指定された位置に石を置いた後、挟んだ相手の石を自分の色に反転させます。
def flip_stones(self, x, y):
opponent = WHITE if self.turn == BLACK else BLACK
for dx, dy in [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]:
pieces_to_flip = []
nx, ny = x + dx, y + dy
while 0 <= nx < BOARD_SIZE and 0 <= ny < BOARD_SIZE and self.board[nx][ny] == opponent:
pieces_to_flip.append((nx, ny))
nx += dx
ny += dy
if 0 <= nx < BOARD_SIZE and 0 <= ny < BOARD_SIZE and self.board[nx][ny] == self.turn:
for px, py in pieces_to_flip:
self.board[px][py] = self.turn
- 「opponent = WHITE if self.turn == BLACK else BLACK」では、現在のターンが黒なら相手は白、現在のターンが白なら相手は黒と設定します。
self.turn
がBLACK
の場合、opponent
はWHITE
になります。その逆も同様です。 for dx, dy in [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
:- 8方向(左上、上、右上、左、右、左下、下、右下)の移動を表すペアをループします。
pieces_to_flip = []
:- 反転させる石の位置を記録するリストを初期化します。
nx, ny = x + dx, y + dy
:- 現在の位置から1マス移動した位置を計算します。
while 0 <= nx < BOARD_SIZE and 0 <= ny < BOARD_SIZE and self.board[nx][ny] == opponent
:- ボードの範囲内であり、かつ相手の石が置かれている限り、その方向に移動を続けます。
pieces_to_flip.append((nx, ny))
:- 相手の石の位置をリストに追加します。
nx += dx, ny += dy
:- さらに1マスその方向に移動します。
if 0 <= nx < BOARD_SIZE and 0 <= ny < BOARD_SIZE and self.board[nx][ny] == self.turn
:- 自分の石に到達した場合、その方向にある相手の石をすべて反転させます。
for px, py in pieces_to_flip
:- 反転させる石の位置をループします。
self.board[px][py] = self.turn
:- 反転させる石を自分の色に変更します。
次の一手のチェック
has_valid_moveメソッドは次の一手が存在するかをチェックします。
def has_valid_move(self):
for x in range(BOARD_SIZE):
for y in range(BOARD_SIZE):
if self.is_valid_move(x, y):
return True
return False
- 二重のforループを使用して、ボードの全てのマスをチェックします。
for x in range(BOARD_SIZE)
: ボードの横方向のマスをループします。for y in range(BOARD_SIZE)
: ボードの縦方向のマスをループします。
self.is_valid_move(x, y)
:- 現在のマス(
x
,y
)が有効な手かどうかをチェックします。 is_valid_move
メソッドは、指定された位置に石を置くことがルール上有効かどうかを判断します。
- 現在のマス(
if self.is_valid_move(x, y): return True
:- 有効な手が見つかった場合、
True
を返してメソッドを終了します。
- 有効な手が見つかった場合、
- 「return False」は、すべてのマスをチェックしても有効な手が見つからなかった場合に
False
を返します。
ゲーム終了の判定
game_endメソッドはゲーム終了時に石の数を比較して勝敗を判定します。
def game_end(self):
black_count = sum(row.count(BLACK) for row in self.board)
white_count = sum(row.count(WHITE) for row in self.board)
if black_count > white_count:
return "Winner black"
elif white_count > black_count:
return "Winner white"
else:
return "Draw"
black_count = sum(row.count(BLACK) for row in self.board)
:- ボード上のすべての行をループし、各行に含まれる黒の石(
BLACK
)の数をカウントします。 sum(row.count(BLACK) for row in self.board)
は、各行の黒の石の数を合計します。
- ボード上のすべての行をループし、各行に含まれる黒の石(
white_count = sum(row.count(WHITE) for row in self.board)
:- ボード上のすべての行をループし、各行に含まれる白の石(
WHITE
)の数をカウントします。 sum(row.count(WHITE) for row in self.board)
は、各行の白の石の数を合計します。
- ボード上のすべての行をループし、各行に含まれる白の石(
if black_count > white_count:
:- 黒の石の数が白の石の数より多い場合、「黒側の勝利」を返します。
elif white_count > black_count:
:- 白の石の数が黒の石の数より多い場合、「白側の勝利」を返します。
else:
:- 黒と白の石の数が同じ場合、「引き分け」を返します。
次の一手の反映
next_moveメソッドは、次の一手を反映し、石を置いて反転させ、ターンを交代します。
def next_move(self, x, y):
if self.is_board_full():
result = self.game_end()
self.display_result(result)
elif self.is_valid_move(x, y):
self.board[x][y] = self.turn
self.flip_stones(x, y)
self.turn = WHITE if self.turn == BLACK else BLACK
if not self.has_valid_move() or self.is_board_full():
self.turn = WHITE if self.turn == BLACK else BLACK
if not self.has_valid_move():
result = self.game_end()
self.display_result(result)
- ボードが満杯かどうかのチェック
- if self.is_board_full(): は、ボードが全て埋まっているかどうかをチェックします。
- self.is_board_full() メソッドが True を返す場合、ボードが満杯であることを意味します。
- ゲーム終了の処理
- result = self.game_end() は、ゲームの終了を判定し、結果を取得します。
- self.display_result(result) は、取得した結果を画面に表示します。
- 有効な手のチェック
- elif self.is_valid_move(x, y): は、指定された位置 (x, y) に有効な手があるかどうかをチェックします。
- self.is_valid_move(x, y) メソッドが True を返す場合、その位置に石を置くことができます。
- 石を置く処理
- self.board[x][y] = self.turn は、現在のプレイヤーの石を指定された位置に置きます。
- self.flip_stones(x, y) は、置いた石によって挟まれた相手の石をひっくり返します。
- ターンの交代
- self.turn = WHITE if self.turn == BLACK else BLACK は、現在のプレイヤーのターンを交代します。
- 現在のターンが BLACK であれば WHITE に、WHITE であれば BLACK に変更します。
- 再度有効な手のチェック
- if not self.has_valid_move() or self.is_board_full(): は、次のプレイヤーが有効な手を持っていないか、ボードが全て埋まっているかをチェックします。
- self.has_valid_move() メソッドが False を返す場合、次のプレイヤーには有効な手がありません。
- ターンの再交代
- self.turn = WHITE if self.turn == BLACK else BLACK は、再度ターンを交代します。これにより、もう一方のプレイヤーに手番が戻ります。
- ゲーム終了の再チェック
- if not self.has_valid_move(): は、再度有効な手がないかどうかをチェックします。もしどちらのプレイヤーも有効な手を持っていない場合、ゲームを終了させます。
- result = self.game_end() は、ゲームの終了を判定し、結果を取得します。
- self.display_result(result) は、取得した結果を画面に表示します。
リザルト画面の表示
以下はゲームの結果を画面に表示するためのメソッドです。
def display_result(self, result):
font = pygame.font.Font(None, 74)
text = font.render(result, True, YELLOW)
text_rect = text.get_rect(center=(SIZE // 2, SIZE // 2))
screen.blit(text, text_rect)
pygame.display.flip()
pygame.time.wait(10000)
pygame.quit()
sys.exit()
- フォントの設定
- font = pygame.font.Font(None, 74) は、表示するテキストのフォントとサイズを設定します。ここでは、デフォルトのフォントを使用し、サイズを74に設定しています。
- テキストのレンダリング
- text = font.render(result, True, YELLOW) は、指定された結果のテキストをレンダリングします。result は表示する文字列で、True はアンチエイリアス(文字の滑らかさ)を有効にし、YELLOW はテキストの色を指定しています。
- テキストの位置設定
- text_rect = text.get_rect(center=(SIZE // 2, SIZE // 2)) は、テキストの位置を画面の中央に設定します。SIZE // 2 は画面の幅と高さの半分を意味し、これによりテキストが画面の中央に配置されます。
- テキストの描画
- screen.blit(text, text_rect) は、レンダリングされたテキストを画面に描画します。text_rect はテキストの位置とサイズを指定します。
- 画面の更新
- pygame.display.flip() は、画面を更新して描画内容を表示します。
- 待機時間
- pygame.time.wait(10000) は、10秒間(10000ミリ秒)待機します。この間、結果が画面に表示され続けます。
- Pygameの終了
- pygame.quit() は、Pygameを終了します。
- プログラムの終了
- sys.exit() は、プログラムを終了します。
メインループ
main関数ではゲームのメインループを実行します。
そして、メインループの中でユーザーの入力を処理し、ボードを更新して描画します。
def main():
game = Othello()
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.MOUSEBUTTONDOWN:
x, y = event.pos
x //= GRID_SIZE
y //= GRID_SIZE
game.next_move(x, y)
game.draw_board()
pygame.display.flip()
pygame.quit()
sys.exit()
if __name__ == "__main__":
main()
game = Othello()
:- オセロゲームのインスタンスを作成します。
Othello
クラスのコンストラクタが呼び出され、ゲームボードが初期化されます。
running = True
:- ゲームループを制御するためのフラグを初期化します。
while running:
:running
がTrue
である限り、ゲームループを実行します。
for event in pygame.event.get():
:- Pygameのイベントキューからイベントを取得し、ループで処理します。
if event.type == pygame.QUIT:
:- ウィンドウの閉じるボタンが押された場合、
running
をFalse
に設定してループを終了します。
- ウィンドウの閉じるボタンが押された場合、
elif event.type == pygame.MOUSEBUTTONDOWN:
:- マウスボタンが押された場合の処理を行います。
x, y = event.pos
:- マウスのクリック位置を取得します。
x //= GRID_SIZE
とy //= GRID_SIZE
:- クリック位置をグリッドの座標に変換します。
game.next_move(x, y)
:- クリックされた位置に石を置く処理を行います。
game.draw_board()
:- ゲームボードを描画します。
pygame.display.flip()
:- 画面を更新して描画内容を表示します。
pygame.quit()
:- Pygameを終了します。
sys.exit()
:- プログラムを終了します。
if __name__ == "__main__":
:- このスクリプトが直接実行された場合にのみ、
main
関数を呼び出します。 - 他のスクリプトからインポートされた場合には実行されません。
- このスクリプトが直接実行された場合にのみ、
【2】オセロゲーム(敵CPUあり)
次に、白側を手動ではなく敵CPU(AI)します。
今回は、単純に白のターンで敵CPUが有効な手からランダムに次手を選ぶようにします。
【2-1】サンプルコード②
【2-2】サンプルコード②の解説
サンプルコード①との違いは、白色(CPU)側の処理を行うメソッドcpu_move
を追加した点です。その部分を詳しく解説します。
def cpu_move(self):
# 有効な手をリストアップ
valid_moves = [(x, y) for x in range(BOARD_SIZE) for y in range(BOARD_SIZE) if self.is_valid_move(x, y)]
# 有効な手がある場合
if valid_moves:
# ランダムに手を選択
x, y = random.choice(valid_moves)
# 選んだ手を実行
self.next_move(x, y)
1. 有効な手のリストを作成
valid_moves = [(x, y) for x in range(BOARD_SIZE) for y in range(BOARD_SIZE) if self.is_valid_move(x, y)]
この部分では、ボード上のすべての位置をチェックし、白が置ける有効な手をリストアップしています。
is_valid_move
関数は、その位置に石を置けるかどうかを判定します。
2. 手を選択
if valid_moves:
x, y = random.choice(valid_moves)
有効な手が存在する場合、その中からランダムに一つを選びます。
ここでは random.choice
を使ってランダムに選んでいます。
3. 手を実行
self.next_move(x, y)
選んだ手を実行します。next_move
関数は、指定された位置に石を置き、必要な石をひっくり返し、ターンを切り替えます。
【3】CPUを強くしたい場合
CPU側を強くしたい場合、ランダムではなく勝率の高い次手を選択するアルゴリズムを実装する必要があります。
代表的なアルゴリズムとしては、以下のものがあります。
- アルゴリズムの例
- ミニマックスアルゴリズム:ゲームツリーを探索し、最適な手を選ぶ手法。
- アルファベータ剪定:ミニマックスアルゴリズムの効率を上げるための手法。
- モンテカルロ木探索:シミュレーションを行い、最も有望な手を選ぶ手法。
これらのアルゴリズムを敵CPUに実装する方法については以下ページで解説しています。
関連ページ
Pygameの使い方については以下ページで解説しています。
Python全般については以下ページで解説しています。
コメント