Last active
February 7, 2025 09:24
-
-
Save shiracamus/79f2c74ede6e065478a43e4753742566 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 numpy as np | |
import pygame | |
from abc import ABC, abstractmethod | |
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 Piece: | |
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=None): | |
if cells is None: | |
cells = np.zeros((self.SIZE, self.SIZE), dtype=int) | |
cells[3, 3], cells[4, 4] = Piece.WHITE, Piece.WHITE | |
cells[3, 4], cells[4, 3] = Piece.BLACK, Piece.BLACK | |
self._cells = cells | |
def copy(self): | |
"""コピーしたOthelloBoardを返す""" | |
return OthelloBoard(np.copy(self._cells)) | |
def score(self, piece): | |
"""pieceの数を返す""" | |
return np.sum(self._cells == piece) | |
def __getitem__(self, pos): | |
"""pos位置の駒を返す、pos位置が不正ならPiece.INVALIDを返す""" | |
row, col = pos | |
if 0 <= row < self.SIZE and 0 <= col < self.SIZE: | |
return self._cells[row, col] | |
else: | |
return Piece.INVALID | |
def movable_places(self, piece): | |
"""pieceを置ける位置を枚挙する""" | |
for row in range(self.SIZE): | |
for col in range(self.SIZE): | |
if any(self._flipable_places(piece, row, col)): | |
yield row, col | |
def _flipable_places(self, piece, row, col): | |
"""pieceをrow,colに置いたときに挟んだ相手駒の位置(row,col)を枚挙する""" | |
if self[row, col] == Piece.EMPTY: | |
opponent = Piece.OPPONENT[piece] | |
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] == piece: | |
yield from places | |
def move(self, piece, row, col): | |
"""pieceを位置(row,col)に置けるなら相手駒をひっくり返してTrueを返す""" | |
flip_places = list(self._flipable_places(piece, row, col)) | |
if not flip_places: | |
return False | |
self._cells[row, col] = piece | |
for r, c in flip_places: | |
self._cells[r, c] = piece | |
return True | |
def draw(self, screen): | |
"""描画する""" | |
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] == Piece.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] == Piece.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): | |
"""表示する""" | |
for cells in self._cells: | |
print(*[Piece.CHAR[piece] for piece in cells]) | |
class Player(ABC): | |
def __init__(self, piece): | |
self._piece = piece | |
self._name = "Black" if piece == Piece.BLACK else "White" | |
def __str__(self): | |
return self._name | |
def can_play(self, board): | |
"""board上に自駒を置けるならTrueを返す""" | |
return any(board.movable_places(self._piece)) | |
def play(self, board): | |
"""パスするかboardに自駒を置き、相手の番になるならTrueを返す""" | |
if not self.can_play(board): | |
print(f"{self}: pass") | |
return True | |
return board.move(self._piece, *self._select_place(board)) | |
@abstractmethod | |
def _select_place(self, board): | |
"""board上の駒を置く位置を選んで返す""" | |
pass | |
class Human(Player): | |
def _select_place(self, board): | |
"""マウスクリックで駒の置く位置を選択""" | |
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): | |
"""board上の駒の置ける位置からランダムに選択""" | |
return random.choice(list(board.movable_places(self._piece))) | |
class MinmaxAI(Player): | |
def _select_place(self, board): | |
"""簡易Minimax: 1手先を読んで一番石が増える位置を選択""" | |
def simulate_move(place): | |
temp_board = board.copy() | |
temp_board.move(self._piece, *place) | |
return temp_board.score(self._piece) | |
return max(board.movable_places(self._piece), key=simulate_move) | |
# AIの種類 | |
AI = { | |
"0": Human(Piece.WHITE), | |
"1": RandomAI(Piece.WHITE), | |
"2": MinmaxAI(Piece.WHITE), | |
} | |
class InfoBoard: | |
WIDTH = 200 | |
@classmethod | |
def draw(cls, screen, font, player, black_score, white_score): | |
"""描画する""" | |
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): | |
self._board = OthelloBoard() | |
self._player = black_player | |
self._turn = {black_player: white_player, white_player: black_player} | |
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(Piece.BLACK), | |
self._board.score(Piece.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({Piece.CHAR[Piece.BLACK]}):{black_score},", | |
f"White({Piece.CHAR[Piece.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(): | |
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(Piece.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