Last active
June 11, 2025 17:49
-
-
Save shiracamus/4aedce10cf50066ad0e93fae0ef91fbf to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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