Skip to content

Instantly share code, notes, and snippets.

@shiracamus
Last active June 11, 2025 17:49
Show Gist options
  • Save shiracamus/4aedce10cf50066ad0e93fae0ef91fbf to your computer and use it in GitHub Desktop.
Save shiracamus/4aedce10cf50066ad0e93fae0ef91fbf to your computer and use it in GitHub Desktop.
import random
import pygame
from abc import ABC, abstractmethod
from typing import Generator
PLACE = tuple[int, int] # (x, y)
COLOR = tuple[int, int, int] # (Red, Green, Blue)
class Color:
"""色 (Red, Gree, Blue)"""
GREEN = (0, 255, 0)
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
class Disc:
"""駒"""
NONE: "Disc"
BLACK: "Disc"
WHITE: "Disc"
RADIUS = 25 # 半径
def __init__(self, color: COLOR, opponent: COLOR) -> None:
self.color = color
self.opponent = opponent
def is_opponent(self, target: "Disc") -> bool:
return target.color == self.opponent
def draw(self, x: int, y: int) -> None:
"""座標(x, y)に自駒を描く"""
pygame.draw.circle(window, self.color, (x, y), self.RADIUS)
Disc.NONE = Disc(Color.GREEN, Color.GREEN)
Disc.BLACK = Disc(Color.BLACK, Color.WHITE)
Disc.WHITE = Disc(Color.WHITE, Color.BLACK)
class Cell:
"""盤のマス(盤には直截Discを置くのでインスタンス化せずに描画処理のみ)"""
GAP = 10
WIDTH = HEIGHT = Disc.RADIUS * 2 + GAP
@classmethod
def draw(cls, x: int, y: int, disc: Disc) -> None:
"""座標(x, y)のマスとdiscを描く"""
rect = pygame.Rect(x * cls.WIDTH, y * cls.HEIGHT, cls.WIDTH, cls.HEIGHT)
pygame.draw.rect(window, Color.BLACK, rect, 1)
disc.draw(*rect.center)
class Board:
"""盤"""
SIZE = 8
RANGE = range(SIZE)
DIRECTIONS = ((-1, -1), (-1, 0), (-1, 1),
(0, -1), (0, 1),
(1, -1), (1, 0), (1, 1)) # 8方向
def __init__(self) -> None:
self.cells = [[Disc.NONE for _ in self.RANGE] for _ in self.RANGE]
upper = left = self.SIZE // 2 - 1
lower = right = self.SIZE // 2
self.cells[upper][left] = self.cells[lower][right] = Disc.BLACK
self.cells[upper][right] = self.cells[lower][left] = Disc.WHITE
def reversible_places(self, x: int, y: int, disc: Disc) -> Generator[PLACE, None, None]:
"""座標(x, y)にdiscを置くと挟んで反転できる相手駒の座標を枚挙する"""
if self.cells[y][x] != Disc.NONE:
return
for dx, dy in self.DIRECTIONS:
nx, ny = x + dx, y + dy
places: list[PLACE] = []
while nx in self.RANGE and ny in self.RANGE:
if not self.cells[ny][nx].is_opponent(disc):
if self.cells[ny][nx] == disc:
yield from places
break
places.append((nx, ny))
nx += dx
ny += dy
def valid_places(self, disc: Disc) -> Generator[PLACE, None, None]:
"""discを置ける座標(x, y)を枚挙する"""
return ((x, y)
for y in self.RANGE
for x in self.RANGE
if any(self.reversible_places(x, y, disc)))
def move(self, x: int, y: int, disc: Disc) -> bool:
"""座標(x, y)にdiscを置けるなら挟んだ相手駒を反転してTrueを返す"""
reversible_places = list(self.reversible_places(x, y, disc))
if not reversible_places:
return False
self.cells[y][x] = disc
for rx, ry in reversible_places:
self.cells[ry][rx] = disc
return True
def count(self, disc: Disc) -> int:
"""盤上のdiscの個数を返す"""
return sum(row.count(disc) for row in self.cells)
def draw(self) -> None:
"""盤の状態を描く"""
window.fill(Color.GREEN)
for y, row in enumerate(self.cells):
for x, disc in enumerate(row):
Cell.draw(x, y, disc)
pygame.display.update()
class Player(ABC):
"""プレイヤー"""
def __init__(self, disc: Disc) -> None:
self.disc = disc
def count(self, board: Board) -> int:
"""board上の自駒の数を返す"""
return board.count(self.disc)
@abstractmethod
def play(self, board: Board) -> bool:
"""board上に自駒を置いてTrueを返す、置けないならFalseを返す"""
pass
class Human(Player):
"""人がマウスで操作するプレイヤー"""
def play(self, board: Board) -> bool:
"""board上に自駒を置いてTrueを返す、置けないならFalseを返す"""
if not any(board.valid_places(self.disc)):
return False
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
raise KeyboardInterrupt("QUIT")
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
mx, my = pygame.mouse.get_pos()
x, y = mx // Cell.WIDTH, my // Cell.HEIGHT
if board.move(x, y, self.disc):
return True
class Computer(Player):
"""コンピュータがランダムに場所を選択するプレイヤー"""
THINKING_TIME = 1000 # ミリ秒
def play(self, board: Board) -> bool:
"""board上に自駒を置いてTrueを返す、置けないならFalseを返す"""
valid_places = list(board.valid_places(self.disc))
if not valid_places:
return False
pygame.time.delay(self.THINKING_TIME) # 考えたふり
x, y = random.choice(valid_places)
return board.move(x, y, self.disc) # 駒を置く
class Othello:
"""オセロゲーム"""
WIDTH = Cell.WIDTH * Board.SIZE
HEIGHT = Cell.HEIGHT * Board.SIZE
def __init__(self, black: Player, white: Player) -> None:
self.black = black
self.white = white
def play(self) -> None:
"""遊ぶ"""
player, opponent = self.black, self.white
self.board = board = Board()
while True:
board.draw()
if not player.play(board):
player, opponent = opponent, player # パス、交代
if not player.play(board):
return # 両者ともパス、ゲーム終了
player, opponent = opponent, player # 交代
def show_result(self) -> None:
"""対戦結果を表示する"""
black_count = self.black.count(self.board)
white_count = self.white.count(self.board)
if black_count > white_count:
text = "Black win !!!"
elif black_count < white_count:
text = "Black lose..."
else:
text = "Draw..."
text += f" {black_count} vs {white_count}"
font = pygame.font.SysFont("arial", 36)
surface = font.render(text, True, Color.BLACK, Color.WHITE)
rect = surface.get_rect(center=(self.WIDTH // 2, self.HEIGHT // 2 - 60))
window.fill(Color.GREEN)
window.blit(surface, rect)
pygame.display.update()
def wait_restart(self) -> None:
"""restartボタンを表示し、ボタンが押されるまで待つ"""
button = pygame.Rect(self.WIDTH // 2 - 80, self.HEIGHT // 2 + 20, 160, 50)
pygame.draw.rect(window, Color.WHITE, button)
pygame.draw.rect(window, Color.BLACK, button, 2)
font = pygame.font.SysFont("arial", 28)
surface = font.render("restart", True, Color.BLACK)
rect = surface.get_rect(center=button.center)
window.blit(surface, rect)
pygame.display.update()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
raise KeyboardInterrupt("QUIT")
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
if button.collidepoint(event.pos):
return
pygame.init()
pygame.display.set_caption("オセロ")
window = pygame.display.set_mode((Othello.WIDTH, Othello.HEIGHT))
try:
while True:
othello = Othello(Human(Disc.BLACK), Computer(Disc.WHITE))
othello.play()
othello.show_result()
othello.wait_restart()
except KeyboardInterrupt: # QUIT
pass
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment