Skip to content

Instantly share code, notes, and snippets.

@shiracamus
Last active June 10, 2025 20:08
Show Gist options
  • Save shiracamus/79f2c74ede6e065478a43e4753742566 to your computer and use it in GitHub Desktop.
Save shiracamus/79f2c74ede6e065478a43e4753742566 to your computer and use it in GitHub Desktop.
import random
import numpy as np
import pygame
from abc import ABC, abstractmethod
from typing import Generator
from numpy.typing import NDArray
class Color:
GREEN = (34, 139, 34)
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
HIGHLIGHT = (200, 200, 200, 128) # 半透明のハイライト
TEXT = (255, 255, 255) # スコアの文字色
BG = (50, 50, 50) # スコア表示の背景色
class Disk:
EMPTY, BLACK, WHITE, INVALID = 0, 1, 2, 3
OPPONENT = {BLACK: WHITE, WHITE: BLACK}
CHAR = {EMPTY: ".", BLACK: "X", WHITE: "O"}
class Cell:
SIZE = 60
class OthelloBoard:
SIZE = 8
WIDTH = HEIGHT = SIZE * Cell.SIZE
DIRECTIONS = ((-1, -1), (-1, 0), (-1, 1),
(0, -1), (0, 1),
(1, -1), (1, 0), (1, 1))
def __init__(self, cells: NDArray[np.int_]|None = None) -> None:
if cells is None:
cells = np.zeros((self.SIZE, self.SIZE), dtype=int)
cells[3, 3] = cells[4, 4] = Disk.WHITE
cells[3, 4] = cells[4, 3] = Disk.BLACK
self._cells: NDArray[np.int_] = cells
def copy(self):
"""コピーしたOthelloBoardを返す"""
return OthelloBoard(np.copy(self._cells))
def score(self, disk: int) -> int:
"""diskの数を返す"""
return np.sum(self._cells == disk)
def __getitem__(self, pos: tuple[int, int]) -> int:
"""pos位置の駒を返す、pos位置が不正ならDisk.INVALIDを返す"""
row, col = pos
if 0 <= row < self.SIZE and 0 <= col < self.SIZE:
return self._cells[row, col]
else:
return Disk.INVALID
def available_places(self, disk: int) -> Generator[tuple[int, int], None, None]:
"""diskを置ける位置を枚挙する"""
for row in range(self.SIZE):
for col in range(self.SIZE):
if any(self._flipable_places(disk, row, col)):
yield row, col
def _flipable_places(self, disk: int, row: int, col: int) -> Generator[tuple[int, int], None, None]:
"""diskをrow,colに置いたときに挟んだ相手駒の位置(row,col)を枚挙する"""
if self[row, col] == Disk.EMPTY:
opponent = Disk.OPPONENT[disk]
for dr, dc in self.DIRECTIONS:
places = []
r, c = row + dr, col + dc
while self[r, c] == opponent:
places.append((r, c))
r, c = r + dr, c + dc
if self[r, c] == disk:
yield from places
def place(self, disk: int, row: int, col: int) -> bool:
"""diskを位置(row,col)に置けるなら相手駒をひっくり返してTrueを返す"""
flip_places = list(self._flipable_places(disk, row, col))
if not flip_places:
return False
self._cells[row, col] = disk
for r, c in flip_places:
self._cells[r, c] = disk
return True
def draw(self, screen) -> None:
"""描画する"""
for i in range(1, self.SIZE):
pygame.draw.line(screen, Color.BLACK,
(i * Cell.SIZE, 0),
(i * Cell.SIZE, self.HEIGHT), 2)
pygame.draw.line(screen, Color.BLACK,
(0, i * Cell.SIZE),
(self.WIDTH, i * Cell.SIZE), 2)
for r in range(self.SIZE):
for c in range(self.SIZE):
if self[r, c] == Disk.BLACK:
pygame.draw.circle(screen, Color.BLACK,
(c * Cell.SIZE + Cell.SIZE // 2,
r * Cell.SIZE + Cell.SIZE // 2),
Cell.SIZE // 2 - 5)
elif self[r, c] == Disk.WHITE:
pygame.draw.circle(screen, Color.WHITE,
(c * Cell.SIZE + Cell.SIZE // 2,
r * Cell.SIZE + Cell.SIZE // 2),
Cell.SIZE // 2 - 5)
def print(self) -> None:
"""表示する"""
for cells in self._cells:
print(*[Disk.CHAR[disk] for disk in cells])
class Player(ABC):
def __init__(self, disk: int) -> None:
self._disk: int = disk
self._name: str = "Black" if disk == Disk.BLACK else "White"
def __str__(self) -> str:
return self._name
def can_play(self, board: OthelloBoard) -> bool:
"""board上に自駒を置けるならTrueを返す"""
return any(board.available_places(self._disk))
def play(self, board: OthelloBoard) -> bool:
"""パスするかboardに自駒を置き、相手の番になるならTrueを返す"""
if not self.can_play(board):
print(f"{self}: pass")
return True
return board.place(self._disk, *self._select_place(board))
@abstractmethod
def _select_place(self, board: OthelloBoard) -> tuple[int, int]:
"""board上の駒を置く位置を選んで返す。置けない位置だった場合再度呼ばれる"""
pass
class Human(Player):
def _select_place(self, board: OthelloBoard) -> tuple[int, int]:
"""マウスクリックで駒を置く位置を選択"""
event = pygame.event.wait()
if event.type == pygame.QUIT:
raise KeyboardInterrupt()
if event.type == pygame.MOUSEBUTTONDOWN:
x, y = pygame.mouse.get_pos()
return y // Cell.SIZE, x // Cell.SIZE
return -1, -1 # 無効な位置を返して再選択させる
class RandomAI(Player):
def _select_place(self, board: OthelloBoard) -> tuple[int, int]:
"""board上の駒の置ける位置からランダムに選択"""
return random.choice(list(board.available_places(self._disk)))
class MinmaxAI(Player):
def _select_place(self, board: OthelloBoard) -> tuple[int, int]:
"""簡易Minimax: 1手先を読んで一番石が増える位置を選択"""
def simulate(place):
next_board = board.copy()
next_board.place(self._disk, *place)
return next_board.score(self._disk)
return max(board.available_places(self._disk), key=simulate)
# AIの種類
AI: dict[str, Player] = {
"0": Human(Disk.WHITE),
"1": RandomAI(Disk.WHITE),
"2": MinmaxAI(Disk.WHITE),
}
class InfoBoard:
WIDTH = 200
@classmethod
def draw(cls, screen, font, player, black_score, white_score) -> None:
"""描画する"""
othello_board_size = OthelloBoard.SIZE * Cell.SIZE
pygame.draw.rect(screen, Color.BG, (othello_board_size, 0,
cls.WIDTH, othello_board_size))
screen.blit(font.render(f"Player: {player}", True, Color.TEXT),
(OthelloBoard.SIZE * Cell.SIZE + 20, 50))
screen.blit(font.render(f"Black: {black_score}", True, Color.TEXT),
(OthelloBoard.SIZE * Cell.SIZE + 20, 100))
screen.blit(font.render(f"White: {white_score}", True, Color.TEXT),
(OthelloBoard.SIZE * Cell.SIZE + 20, 150))
class OthelloGame:
def __init__(self, black: Player, white: Player) -> None:
self._board: OthelloBoard = OthelloBoard()
self._player: Player = black
self._turn: dict[Player, Player] = {black: white, white: black}
def can_play(self):
"""どちらかのプレイヤーが駒を置けるならならTrueを返す"""
return (self._player.can_play(self._board) or
self._turn[self._player].can_play(self._board))
def play(self):
"""プレイヤーが駒を置く。パスするか駒を置いたらプレイヤー交代"""
if self._player.play(self._board):
self._player = self._turn[self._player]
def _scores(self):
"""黒駒と白駒の数を返す"""
return (self._board.score(Disk.BLACK),
self._board.score(Disk.WHITE))
def draw(self, screen, font):
"""描画する"""
screen.fill(Color.GREEN)
self._board.draw(screen)
InfoBoard.draw(screen, font, self._player, *self._scores())
def print_result(self):
"""勝敗結果を表示する"""
self._board.print()
black_score, white_score = self._scores()
print(f"Black({Disk.CHAR[Disk.BLACK]}):{black_score},",
f"White({Disk.CHAR[Disk.WHITE]}):{white_score}")
print("Black Wins!" if black_score > white_score else
"White Wins!" if white_score > black_score else
"It's a Draw!")
class Screen:
WIDTH = OthelloBoard.WIDTH + InfoBoard.WIDTH
HEIGHT = OthelloBoard.HEIGHT
def main() -> None:
mode = input("対戦相手選択 (0: 人, 1: ランダムAI, 2: Minimax AI) → ")
pygame.init()
pygame.display.set_caption("オセロ")
screen = pygame.display.set_mode((Screen.WIDTH, Screen.HEIGHT))
font = pygame.font.Font(None, 36)
try:
game = OthelloGame(Human(Disk.BLACK), AI.get(mode, AI["0"]))
while game.can_play():
game.play()
game.draw(screen, font)
pygame.display.flip()
game.print_result()
except KeyboardInterrupt:
pass
pygame.quit()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment