【Pygame】オセロゲームを作成する方法

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 x BOARD_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.turnBLACKに設定しています。

ボードの描画

以下の部分は、オセロゲームのボードを描画するためのメソッドです。

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_SIZEy * 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メソッドを呼び出して、その位置に石を描画します。
    • xyは石の位置を示し、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はクラスのインスタンスを指し、xyは石を置く位置の座標を示します。
  • colorは石の色(黒または白)を示します。
  • pygame.draw.circleは、円を描画するためのPygameの関数です。
  • screenは描画先の画面を示します。
  • colorは円の色を指定します(ここでは石の色)。
  • (x * GRID_SIZE + GRID_SIZE // 2, y * GRID_SIZE + GRID_SIZE // 2):
    • 円の中心座標を計算します。
    • x * GRID_SIZEy * 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.turnBLACKの場合、opponentWHITEになります。その逆も同様です。
  • 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:
    • 自分の石に到達した場合、その方向に有効な手があると判断し、validTrueに設定してループを終了します。

マス目が全て石で埋まっているかの判定

マス目が全て石で埋まっているかの判定を行います。(全て埋まっていたらゲームを終了させるため)

    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.turnBLACKの場合、opponentWHITEになります。その逆も同様です。
  • 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::
    • runningTrueである限り、ゲームループを実行します。
  • for event in pygame.event.get()::
    • Pygameのイベントキューからイベントを取得し、ループで処理します。
  • if event.type == pygame.QUIT::
    • ウィンドウの閉じるボタンが押された場合、runningFalseに設定してループを終了します。
  • elif event.type == pygame.MOUSEBUTTONDOWN::
    • マウスボタンが押された場合の処理を行います。
    • x, y = event.pos:
    • マウスのクリック位置を取得します。
    • x //= GRID_SIZEy //= 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】ミニマックス法で敵CPUを強化したオセロゲームを作成
Pygameとミニマックス法で敵CPUを強化したオセロゲームを作成する方法をソースコード付きで詳しく解説します。
【Pygame】アルファ・ベータ法で敵CPUを強化したオセロゲームを作成
Pygameとアルファ・ベータ法で敵CPUを強化したオセロゲームを作成する方法をソースコード付きで詳しく解説します。

関連ページ

Pygameの使い方については以下ページで解説しています。

【Pygame超入門】使い方とサンプルゲームを解説
Pygameで2Dゲームを簡単に制作する方法を入門者向けに解説します。

Python全般については以下ページで解説しています。

【Python超入門】使い方とサンプル集
Pythonの使い方について、基礎文法から応用例まで入門者向けに解説します。

コメント