はじめに
PythonとPygameを使って、オセロ(リバーシ)を作成しました!
このプロジェクトでは、
✅ 二人対戦モード
✅ AI対戦モード (ランダムAI / Minimax AI)
を実装し、対戦可能にしました。
ゲームの流れやコードのポイントを解説するので、ぜひ自作ゲーム開発の参考にしてください!
使用技術
- Python 3.x
- Pygame (GUIライブラリ)
- NumPy (盤面管理)
- Minimax アルゴリズム (AI対戦用)
実装する機能
-
オセロの基本ルール
- 8×8 のボード
- 石の配置、反転、合法手の判定
- ゲーム終了判定、勝者判定
-
Pygameを使ったGUI
- 盤面の描画
- クリック操作で石を置く
- 置ける場所のハイライト
- スコア表示
-
AI対戦
- ランダムAI: 可能な手からランダムに選択
- Minimax AI: 1手先を読んで最適な手を選択
-
ゲームモードの選択
- 二人対戦 (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()
遊び方
- プログラムを実行
-
モードを選択(ターミナル上)
-
0
→ 二人対戦 -
1
→ ランダムAIと対戦 -
2
→ Minimax AIと対戦
-
- クリックで石を置く
- 勝敗が決まるとコンソールに結果を表示
拡張案
✅ AIの強化
- Minimax の探索深度を増やしてより強くする
- α-β枝刈りを導入
✅ ゲームのUI改良
- 石を置ける場所をハイライトする
- スコアをリアルタイムで画面に表示
✅ オンライン対戦
- WebSocket を使ってネット対戦機能を追加
まとめ
PythonとPygameを使って、オセロを作成し、二人対戦やAI対戦を実装しました。
Pygameの基礎を学びつつ、ゲームAIの導入まで実践できるので、ぜひ試してみてください!
次のステップとして AIの強化やオンライン対戦 も面白いですね。
ぜひ自作ゲームを発展させてみましょう!🎉