Created
February 12, 2022 02:57
-
-
Save thomashikaru/820a7cd2bc6d55a7ffc1c3775ba397fd to your computer and use it in GitHub Desktop.
This file contains 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 pygame | |
import numpy as np | |
import itertools | |
import sys | |
import networkx as nx | |
import collections | |
from pygame import gfxdraw | |
# Game constants | |
BOARD_BROWN = (199, 105, 42) | |
BOARD_WIDTH = 1000 | |
BOARD_BORDER = 75 | |
STONE_RADIUS = 22 | |
WHITE = (255, 255, 255) | |
BLACK = (0, 0, 0) | |
TURN_POS = (BOARD_BORDER, 20) | |
SCORE_POS = (BOARD_BORDER, BOARD_WIDTH - BOARD_BORDER + 30) | |
DOT_RADIUS = 4 | |
def make_grid(size): | |
"""Return list of (start_point, end_point pairs) defining gridlines | |
Args: | |
size (int): size of grid | |
Returns: | |
Tuple[List[Tuple[float, float]]]: start and end points for gridlines | |
""" | |
start_points, end_points = [], [] | |
# vertical start points (constant y) | |
xs = np.linspace(BOARD_BORDER, BOARD_WIDTH - BOARD_BORDER, size) | |
ys = np.full((size), BOARD_BORDER) | |
start_points += list(zip(xs, ys)) | |
# horizontal start points (constant x) | |
xs = np.full((size), BOARD_BORDER) | |
ys = np.linspace(BOARD_BORDER, BOARD_WIDTH - BOARD_BORDER, size) | |
start_points += list(zip(xs, ys)) | |
# vertical end points (constant y) | |
xs = np.linspace(BOARD_BORDER, BOARD_WIDTH - BOARD_BORDER, size) | |
ys = np.full((size), BOARD_WIDTH - BOARD_BORDER) | |
end_points += list(zip(xs, ys)) | |
# horizontal end points (constant x) | |
xs = np.full((size), BOARD_WIDTH - BOARD_BORDER) | |
ys = np.linspace(BOARD_BORDER, BOARD_WIDTH - BOARD_BORDER, size) | |
end_points += list(zip(xs, ys)) | |
return (start_points, end_points) | |
def xy_to_colrow(x, y, size): | |
"""Convert x,y coordinates to column and row number | |
Args: | |
x (float): x position | |
y (float): y position | |
size (int): size of grid | |
Returns: | |
Tuple[int, int]: column and row numbers of intersection | |
""" | |
inc = (BOARD_WIDTH - 2 * BOARD_BORDER) / (size - 1) | |
x_dist = x - BOARD_BORDER | |
y_dist = y - BOARD_BORDER | |
col = int(round(x_dist / inc)) | |
row = int(round(y_dist / inc)) | |
return col, row | |
def colrow_to_xy(col, row, size): | |
"""Convert column and row numbers to x,y coordinates | |
Args: | |
col (int): column number (horizontal position) | |
row (int): row number (vertical position) | |
size (int): size of grid | |
Returns: | |
Tuple[float, float]: x,y coordinates of intersection | |
""" | |
inc = (BOARD_WIDTH - 2 * BOARD_BORDER) / (size - 1) | |
x = int(BOARD_BORDER + col * inc) | |
y = int(BOARD_BORDER + row * inc) | |
return x, y | |
def has_no_liberties(board, group): | |
"""Check if a stone group has any liberties on a given board. | |
Args: | |
board (object): game board (size * size matrix) | |
group (List[Tuple[int, int]]): list of (col,row) pairs defining a stone group | |
Returns: | |
[boolean]: True if group has any liberties, False otherwise | |
""" | |
for x, y in group: | |
if x > 0 and board[x - 1, y] == 0: | |
return False | |
if y > 0 and board[x, y - 1] == 0: | |
return False | |
if x < board.shape[0] - 1 and board[x + 1, y] == 0: | |
return False | |
if y < board.shape[0] - 1 and board[x, y + 1] == 0: | |
return False | |
return True | |
def get_stone_groups(board, color): | |
"""Get stone groups of a given color on a given board | |
Args: | |
board (object): game board (size * size matrix) | |
color (str): name of color to get groups for | |
Returns: | |
List[List[Tuple[int, int]]]: list of list of (col, row) pairs, each defining a group | |
""" | |
size = board.shape[0] | |
color_code = 1 if color == "black" else 2 | |
xs, ys = np.where(board == color_code) | |
graph = nx.grid_graph(dim=[size, size]) | |
stones = set(zip(xs, ys)) | |
all_spaces = set(itertools.product(range(size), range(size))) | |
stones_to_remove = all_spaces - stones | |
graph.remove_nodes_from(stones_to_remove) | |
return nx.connected_components(graph) | |
def is_valid_move(col, row, board): | |
"""Check if placing a stone at (col, row) is valid on board | |
Args: | |
col (int): column number | |
row (int): row number | |
board (object): board grid (size * size matrix) | |
Returns: | |
boolean: True if move is valid, False otherewise | |
""" | |
# TODO: check for ko situation (infinite back and forth) | |
if col < 0 or col >= board.shape[0]: | |
return False | |
if row < 0 or row >= board.shape[0]: | |
return False | |
return board[col, row] == 0 | |
class Game: | |
def __init__(self, size): | |
self.board = np.zeros((size, size)) | |
self.size = size | |
self.black_turn = True | |
self.prisoners = collections.defaultdict(int) | |
self.start_points, self.end_points = make_grid(self.size) | |
def init_pygame(self): | |
pygame.init() | |
screen = pygame.display.set_mode((BOARD_WIDTH, BOARD_WIDTH)) | |
self.screen = screen | |
self.ZOINK = pygame.mixer.Sound("wav/zoink.wav") | |
self.CLICK = pygame.mixer.Sound("wav/click.wav") | |
self.font = pygame.font.SysFont("arial", 30) | |
def clear_screen(self): | |
# fill board and add gridlines | |
self.screen.fill(BOARD_BROWN) | |
for start_point, end_point in zip(self.start_points, self.end_points): | |
pygame.draw.line(self.screen, BLACK, start_point, end_point) | |
# add guide dots | |
guide_dots = [3, self.size // 2, self.size - 4] | |
for col, row in itertools.product(guide_dots, guide_dots): | |
x, y = colrow_to_xy(col, row, self.size) | |
gfxdraw.aacircle(self.screen, x, y, DOT_RADIUS, BLACK) | |
gfxdraw.filled_circle(self.screen, x, y, DOT_RADIUS, BLACK) | |
pygame.display.flip() | |
def pass_move(self): | |
self.black_turn = not self.black_turn | |
self.draw() | |
def handle_click(self): | |
# get board position | |
x, y = pygame.mouse.get_pos() | |
col, row = xy_to_colrow(x, y, self.size) | |
if not is_valid_move(col, row, self.board): | |
self.ZOINK.play() | |
return | |
# update board array | |
self.board[col, row] = 1 if self.black_turn else 2 | |
# get stone groups for black and white | |
self_color = "black" if self.black_turn else "white" | |
other_color = "white" if self.black_turn else "black" | |
# handle captures | |
capture_happened = False | |
for group in list(get_stone_groups(self.board, other_color)): | |
if has_no_liberties(self.board, group): | |
capture_happened = True | |
for i, j in group: | |
self.board[i, j] = 0 | |
self.prisoners[self_color] += len(group) | |
# handle special case of invalid stone placement | |
# this must be done separately because we need to know if capture resulted | |
if not capture_happened: | |
group = None | |
for group in get_stone_groups(self.board, self_color): | |
if (col, row) in group: | |
break | |
if has_no_liberties(self.board, group): | |
self.ZOINK.play() | |
self.board[col, row] = 0 | |
return | |
# change turns and draw screen | |
self.CLICK.play() | |
self.black_turn = not self.black_turn | |
self.draw() | |
def draw(self): | |
# draw stones - filled circle and antialiased ring | |
self.clear_screen() | |
for col, row in zip(*np.where(self.board == 1)): | |
x, y = colrow_to_xy(col, row, self.size) | |
gfxdraw.aacircle(self.screen, x, y, STONE_RADIUS, BLACK) | |
gfxdraw.filled_circle(self.screen, x, y, STONE_RADIUS, BLACK) | |
for col, row in zip(*np.where(self.board == 2)): | |
x, y = colrow_to_xy(col, row, self.size) | |
gfxdraw.aacircle(self.screen, x, y, STONE_RADIUS, WHITE) | |
gfxdraw.filled_circle(self.screen, x, y, STONE_RADIUS, WHITE) | |
# text for score and turn info | |
score_msg = ( | |
f"Black's Prisoners: {self.prisoners['black']}" | |
+ f" White's Prisoners: {self.prisoners['white']}" | |
) | |
txt = self.font.render(score_msg, True, BLACK) | |
self.screen.blit(txt, SCORE_POS) | |
turn_msg = ( | |
f"{'Black' if self.black_turn else 'White'} to move. " | |
+ "Click to place stone, press P to pass." | |
) | |
txt = self.font.render(turn_msg, True, BLACK) | |
self.screen.blit(txt, TURN_POS) | |
pygame.display.flip() | |
def update(self): | |
# TODO: undo button | |
events = pygame.event.get() | |
for event in events: | |
if event.type == pygame.MOUSEBUTTONUP: | |
self.handle_click() | |
if event.type == pygame.QUIT: | |
sys.exit() | |
if event.type == pygame.KEYUP: | |
if event.key == pygame.K_p: | |
self.pass_move() | |
if __name__ == "__main__": | |
g = Game(size=19) | |
g.init_pygame() | |
g.clear_screen() | |
g.draw() | |
while True: | |
g.update() | |
pygame.time.wait(100) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment