0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PythonとPygameでオセロを作る!【二人対戦 & AI対戦 (ランダム / Minimax) 実装】

Posted at

はじめに

PythonとPygameを使って、オセロ(リバーシ)を作成しました!
スクリーンショット 2025-01-30 17.20.03.png

このプロジェクトでは、
二人対戦モード
AI対戦モード (ランダムAI / Minimax AI)
を実装し、対戦可能にしました。

ゲームの流れやコードのポイントを解説するので、ぜひ自作ゲーム開発の参考にしてください!

使用技術

  • Python 3.x
  • Pygame (GUIライブラリ)
  • NumPy (盤面管理)
  • Minimax アルゴリズム (AI対戦用)

実装する機能

  1. オセロの基本ルール
    • 8×8 のボード
    • 石の配置、反転、合法手の判定
    • ゲーム終了判定、勝者判定
  2. Pygameを使ったGUI
    • 盤面の描画
    • クリック操作で石を置く
    • 置ける場所のハイライト
    • スコア表示
  3. AI対戦
    • ランダムAI: 可能な手からランダムに選択
    • Minimax AI: 1手先を読んで最適な手を選択
  4. ゲームモードの選択
    • 二人対戦 (Human vs Human)
    • AI対戦 (Human vs AI: ランダム or Minimax)

プログラムのポイント

1. 盤面の初期化

self.board = np.zeros((BOARD_SIZE, BOARD_SIZE), dtype=int)
self.board[3, 3], self.board[4, 4] = WHITE, WHITE
self.board[3, 4], self.board[4, 3] = BLACK, BLACK
  • 8×8の盤面を NumPy の zeros() で作成
  • 中央4マスに白黒の初期配置を設定

2. 合法手の判定

def is_valid_move(self, row, col):
    if self.board[row, col] != EMPTY:
        return False
    for dr, dc in DIRECTIONS:
        if self._can_flip(row, col, dr, dc):
            return True
    return False
  • 指定の座標に石が置けるかをチェック
  • 8方向に相手の石があり、挟めるか確認

3. AIの手を決める(ランダム or Minimax)

def ai_move(self):
    if self.ai_player != self.current_player:
        return
    valid_moves = self.get_valid_moves()
    if not valid_moves:
        return
    move = random.choice(valid_moves) if self.ai_type == AI_RANDOM else self.minimax_move()
    self.place_piece(*move)
  • AIのターンなら get_valid_moves() で合法手を取得
  • AI_RANDOM はランダムな手を選択
  • AI_MINIMAX は最適な手を選択

4. Minimaxによる最適手の選択

def minimax_move(self):
    valid_moves = self.get_valid_moves()
    best_move = max(valid_moves, key=lambda move: self.simulate_move(move), default=random.choice(valid_moves))
    return best_move
  • simulate_move() で各手を試し、最も石が増える手を選択

プログラム全体

import pygame
import numpy as np
import random

# 定数の定義
BOARD_SIZE = 8
CELL_SIZE = 60
INFO_WIDTH = 150  # スコア表示用のスペース
SCREEN_WIDTH = BOARD_SIZE * CELL_SIZE + INFO_WIDTH
SCREEN_HEIGHT = BOARD_SIZE * CELL_SIZE

EMPTY, BLACK, WHITE = 0, 1, 2
DIRECTIONS = [(-1, -1), (-1, 0), (-1, 1),
              (0, -1),         (0, 1),
              (1, -1), (1, 0), (1, 1)]

# 色の定義
GREEN = (34, 139, 34)
WHITE_COLOR = (255, 255, 255)
BLACK_COLOR = (0, 0, 0)
HIGHLIGHT_COLOR = (200, 200, 200, 128)  # 半透明のハイライト
TEXT_COLOR = (255, 255, 255)  # スコアの文字色
BG_COLOR = (50, 50, 50)  # スコア表示の背景色

# AIの種類
AI_NONE = 0
AI_RANDOM = 1
AI_MINIMAX = 2

class OthelloGame:
    def __init__(self, ai_type=AI_NONE):
        self.board = np.zeros((BOARD_SIZE, BOARD_SIZE), dtype=int)
        self.board[3, 3], self.board[4, 4] = WHITE, WHITE
        self.board[3, 4], self.board[4, 3] = BLACK, BLACK
        self.current_player = BLACK
        self.running = True
        self.ai_type = ai_type  # AIの種類
        self.ai_player = WHITE if ai_type != AI_NONE else None  # AIは白

    def is_valid_move(self, row, col):
        if self.board[row, col] != EMPTY:
            return False
        for dr, dc in DIRECTIONS:
            if self._can_flip(row, col, dr, dc):
                return True
        return False

    def _can_flip(self, row, col, dr, dc):
        opponent = WHITE if self.current_player == BLACK else BLACK
        r, c = row + dr, col + dc
        flipped = False

        while 0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE and self.board[r, c] == opponent:
            r += dr
            c += dc
            flipped = True
        
        if flipped and 0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE and self.board[r, c] == self.current_player:
            return True
        return False

    def place_piece(self, row, col):
        if not self.is_valid_move(row, col):
            return False
        self.board[row, col] = self.current_player
        for dr, dc in DIRECTIONS:
            self._flip_pieces(row, col, dr, dc)
        self.current_player = WHITE if self.current_player == BLACK else BLACK
        return True

    def _flip_pieces(self, row, col, dr, dc):
        opponent = WHITE if self.current_player == BLACK else BLACK
        r, c = row + dr, col + dc
        pieces_to_flip = []

        while 0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE and self.board[r, c] == opponent:
            pieces_to_flip.append((r, c))
            r += dr
            c += dc
        
        if 0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE and self.board[r, c] == self.current_player:
            for r, c in pieces_to_flip:
                self.board[r, c] = self.current_player

    def has_valid_moves(self):
        return any(self.is_valid_move(r, c) for r in range(BOARD_SIZE) for c in range(BOARD_SIZE))

    def get_valid_moves(self):
        return [(r, c) for r in range(BOARD_SIZE) for c in range(BOARD_SIZE) if self.is_valid_move(r, c)]

    def check_game_over(self):
        if not self.has_valid_moves():
            self.current_player = WHITE if self.current_player == BLACK else BLACK
            if not self.has_valid_moves():
                self.running = False

    def get_winner(self):
        black_score, white_score = np.sum(self.board == BLACK), np.sum(self.board == WHITE)
        return "Black Wins!" if black_score > white_score else "White Wins!" if white_score > black_score else "It's a Draw!"

    def get_scores(self):
        return np.sum(self.board == BLACK), np.sum(self.board == WHITE)

    def ai_move(self):
        if self.ai_player != self.current_player:
            return

        valid_moves = self.get_valid_moves()
        if not valid_moves:
            return

        if self.ai_type == AI_RANDOM:
            move = random.choice(valid_moves)
        elif self.ai_type == AI_MINIMAX:
            move = self.minimax_move()
        else:
            return

        self.place_piece(*move)

    def minimax_move(self):
        """簡易Minimax: 1手先を読んで石が増える手を選択"""
        valid_moves = self.get_valid_moves()
        best_move = max(valid_moves, key=lambda move: self.simulate_move(move), default=random.choice(valid_moves))
        return best_move

    def simulate_move(self, move):
        temp_game = OthelloGame()
        temp_game.board = np.copy(self.board)
        temp_game.current_player = self.current_player
        temp_game.place_piece(*move)
        return np.sum(temp_game.board == self.current_player)

def draw_board(screen, game, font):
    screen.fill(GREEN)

    for i in range(1, BOARD_SIZE):
        pygame.draw.line(screen, BLACK_COLOR, (i * CELL_SIZE, 0), (i * CELL_SIZE, SCREEN_HEIGHT), 2)
        pygame.draw.line(screen, BLACK_COLOR, (0, i * CELL_SIZE), (SCREEN_HEIGHT, i * CELL_SIZE), 2)

    for r in range(BOARD_SIZE):
        for c in range(BOARD_SIZE):
            if game.board[r, c] == BLACK:
                pygame.draw.circle(screen, BLACK_COLOR, (c * CELL_SIZE + CELL_SIZE // 2, r * CELL_SIZE + CELL_SIZE // 2), CELL_SIZE // 2 - 5)
            elif game.board[r, c] == WHITE:
                pygame.draw.circle(screen, WHITE_COLOR, (c * CELL_SIZE + CELL_SIZE // 2, r * CELL_SIZE + CELL_SIZE // 2), CELL_SIZE // 2 - 5)

    pygame.draw.rect(screen, BG_COLOR, (BOARD_SIZE * CELL_SIZE, 0, INFO_WIDTH, SCREEN_HEIGHT))

    black_score, white_score = game.get_scores()
    screen.blit(font.render(f"Black: {black_score}", True, TEXT_COLOR), (BOARD_SIZE * CELL_SIZE + 20, 50))
    screen.blit(font.render(f"White: {white_score}", True, TEXT_COLOR), (BOARD_SIZE * CELL_SIZE + 20, 100))

def main():
    pygame.init()
    screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
    pygame.display.set_caption("オセロ - モード選択")
    font = pygame.font.Font(None, 36)
    
    mode = int(input("モード選択 (0: 二人対戦, 1: ランダムAI, 2: Minimax AI) → "))
    game = OthelloGame(ai_type=mode)

    while game.running:
        game.ai_move()
        draw_board(screen, game, font)
        pygame.display.flip()

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                game.running = False
            elif event.type == pygame.MOUSEBUTTONDOWN:
                row, col = pygame.mouse.get_pos()[1] // CELL_SIZE, pygame.mouse.get_pos()[0] // CELL_SIZE
                if game.place_piece(row, col):
                    game.check_game_over()

    print(game.get_winner())
    pygame.quit()

if __name__ == "__main__":
    main()

遊び方

  1. プログラムを実行
  2. モードを選択(ターミナル上)
    • 0 → 二人対戦
    • 1 → ランダムAIと対戦
    • 2 → Minimax AIと対戦
  3. クリックで石を置く
  4. 勝敗が決まるとコンソールに結果を表示

拡張案

AIの強化

  • Minimax の探索深度を増やしてより強くする
  • α-β枝刈りを導入

ゲームのUI改良

  • 石を置ける場所をハイライトする
  • スコアをリアルタイムで画面に表示

オンライン対戦

  • WebSocket を使ってネット対戦機能を追加

まとめ

PythonとPygameを使って、オセロを作成し、二人対戦やAI対戦を実装しました。

Pygameの基礎を学びつつ、ゲームAIの導入まで実践できるので、ぜひ試してみてください!

次のステップとして AIの強化やオンライン対戦 も面白いですね。

ぜひ自作ゲームを発展させてみましょう!🎉

0
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?