Last active
February 11, 2024 20:01
-
-
Save elesiuta/a17d93feb3d29971c3b5abbde69d53f3 to your computer and use it in GitHub Desktop.
Conway's Game of Chess (moved to https://github.com/elesiuta/conways-game-of-chess)
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
#!/usr/bin/env python3 | |
# Conway's Game of Chess | |
# Copyright (C) 2023 Eric Lesiuta | |
import argparse | |
import atexit | |
import curses | |
import hashlib | |
import os | |
import pickle | |
import textwrap | |
import time | |
import socket | |
import sys | |
def exit_handler(engine, engine_state, conn, *args) -> None: | |
"""clean up in the event of an exception and atexit functions aren't called""" | |
type_, value, traceback = args | |
print(type_, value, traceback, file=sys.stderr) | |
print(" ".join(engine.recent_moves_str)) | |
with open(engine.args.save, "wb") as f: | |
pickle.dump(engine_state, f) | |
if conn: | |
conn.close() | |
def start_cli() -> int: | |
# cli | |
parser = argparse.ArgumentParser(description="Conway's game of chess") | |
parser.epilog = textwrap.dedent(""" | |
Conway's game of chess is a chess variant where the pieces can reproduce and die. | |
Legend: White birth queue ┐ | |
White: P R P N P B P Q P B P N P R <─┘ | |
┌──────────────────────────────────────┐ | |
│ # <─ White birth COUNTER on empty w │ | |
│ squares, born from queue on ^ │ | |
│ next turn after reaching 2 │ │ | |
│ │ │ | |
│ INDICATOR that white has exactly ┘ │ | |
│ 3 nearby pieces, birth counter │ | |
│ will increment at the start of the │ | |
│ next turn, black birth counter and │ | |
│ indicator are below and separate │ | |
│ │ | |
│ # ♔ <─ Piece symbol o │ | |
│ ^ ^ │ | |
│ └ Death COUNTER on occupied │ │ | |
│ squares, dies after reaching 3 │ │ | |
│ │ │ | |
│ INDICATOR that the piece has > 3 ┘ │ | |
│ nearby pieces (overpopulation), │ | |
│ or < 2 nearby pieces │ | |
│ (underpopulation) and will die │ | |
│ │ | |
│ # <─ Black COUNTER & INDICATOR ──> l │ | |
└──────────────────────────────────────┘ | |
The INDICATORs are updated immediately when the conditions are met. | |
The COUNTERs are incremented only at the start of the respective player's turn. | |
Births and deaths also only occur at the start of the respective player's turn. | |
If the conditions for a birth or death counter are no longer met, (as shown by the indicators), the counter resets. | |
Opponent pieces are not counted as nearby pieces for the birth/death population criteria. | |
On birth, pieces are taken from the birth queue (circular) and placed on the board in order of rank then file. | |
Placement starts from rank 1 for white and rank 8 for black, with both filling the board from left to right. | |
The game ends when the king is captured or perishes due to over/underpopulation. | |
""") | |
parser.formatter_class = argparse.RawDescriptionHelpFormatter | |
parser.add_argument("--flip", action="store_true", help="flip the board") | |
parser.add_argument("--save", action="store", metavar="FILE", default="conway_chess.pickle", help="save file location") | |
parser.add_argument("--load", action="store", metavar="FILE", help="load a save file") | |
parser.add_argument("--host", nargs=3, metavar=("HOST", "PORT", "COLOR"), help="host a game") | |
parser.add_argument("--join", nargs=2, metavar=("HOST", "PORT"), help="join a game") | |
parser.add_argument("--ascii", action="store_true", help="use ascii characters for pieces") | |
parser.add_argument("--light", action="store_true", help="flip unicode piece colors for light terminals") | |
args = parser.parse_args() | |
# print instructions before playing | |
print(parser.epilog) | |
_ = input("Press enter to play") | |
# networking | |
conn, my_colour = None, None | |
if args.host and args.join: | |
print("You can only host or join a game, not both", file=sys.stderr) | |
return 1 | |
if args.host: | |
if args.host[2] not in ("white", "black"): | |
print("You can only host as white or black", file=sys.stderr) | |
return 1 | |
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
# s = ssl._create_unverified_context().wrap_socket(s, server_side=True) | |
s.bind((args.host[0], int(args.host[1]))) | |
s.listen() | |
conn, addr = s.accept() | |
conn.sendall(args.host[2].encode()) | |
my_colour = args.host[2] | |
atexit.register(conn.close) | |
elif args.join: | |
conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
# conn = ssl._create_unverified_context().wrap_socket(conn, server_side=False) | |
conn.connect((args.join[0], int(args.join[1]))) | |
host_colour = conn.recv(5).decode() | |
if host_colour == "white": | |
my_colour = "black" | |
else: | |
my_colour = "white" | |
atexit.register(conn.close) | |
# engine initialization | |
engine = Engine(args) | |
engine_state = [pickle.dumps(engine)] | |
engine_state_redo = [] | |
if args.load: | |
engine_state = pickle.load(open(args.load, "rb")) | |
for i in range(len(engine_state)): | |
engine_state[i] = pickle.loads(engine_state[i]) | |
engine_state[i].args = args | |
engine_state[i] = pickle.dumps(engine_state[i]) | |
engine = pickle.loads(engine_state[-1]) | |
atexit.register(lambda: print(" ".join(engine.recent_moves_str))) | |
atexit.register(lambda: pickle.dump(engine_state, open(args.save, "wb"))) | |
sys.excepthook = lambda *args: exit_handler(engine, engine_state, conn, *args) | |
try: | |
from stockfish import Stockfish | |
stockfish = Stockfish() | |
except: | |
stockfish = None | |
# check terminal size | |
columns, lines = os.get_terminal_size() | |
assert engine.height <= lines, f"Terminal height ({lines}) is too short by {engine.height - lines} lines" | |
assert engine.width <= columns, f"Terminal width ({columns}) is too narrow by {engine.width - columns} columns" | |
# main curses loop | |
for err_count in reversed(range(30)): | |
try: | |
return curses.wrapper(main_loop, engine, engine_state, engine_state_redo, stockfish, conn, my_colour) | |
except curses.error as e: | |
print("CURSES ERROR: %s" % e, file=sys.stderr) | |
print("try resizing your terminal, game will quit in %s seconds" % (err_count + 1), file=sys.stderr) | |
time.sleep(1) | |
except Exception as e: | |
print("ERROR: %s" % e, file=sys.stderr) | |
return 1 | |
return 1 | |
def main_loop(stdscr, engine: "Engine", engine_state: list[bytes], engine_state_redo: list[bytes], stockfish, conn, my_colour) -> int: | |
"""main loop for the curses implementation of the game""" | |
curses.cbreak() | |
curses.noecho() | |
while True: | |
# refresh screen | |
current_display = engine.display(my_colour) | |
stdscr.clear() | |
curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE) | |
curses.init_pair(2, curses.COLOR_BLUE, curses.COLOR_WHITE) | |
curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_BLUE) | |
curses.init_pair(4, curses.COLOR_YELLOW, curses.COLOR_WHITE) | |
curses.init_pair(5, curses.COLOR_YELLOW, curses.COLOR_BLUE) | |
curses.init_pair(6, curses.COLOR_GREEN, curses.COLOR_BLACK) | |
curses.init_pair(7, curses.COLOR_YELLOW, curses.COLOR_BLACK) | |
curses.init_pair(8, curses.COLOR_RED, curses.COLOR_BLACK) | |
curses.init_pair(9, curses.COLOR_CYAN, curses.COLOR_BLACK) | |
stdscr.attrset(curses.color_pair(0)) | |
y = 0 | |
for line in current_display: | |
for x, char in enumerate(line): | |
# pieces | |
if char in "RNBQKP" and y > 4 and y < engine.height - 2: | |
if engine.use_unicode: | |
char = engine.unicode_replacements[char] | |
else: | |
if char == "K": | |
stdscr.attrset(curses.color_pair(4)) | |
else: | |
stdscr.attrset(curses.color_pair(2)) | |
elif char in "rnbqkp" and y > 4 and y < engine.height - 2: | |
if engine.use_unicode: | |
char = engine.unicode_replacements[char] | |
else: | |
if char == "k": | |
stdscr.attrset(curses.color_pair(5)) | |
else: | |
stdscr.attrset(curses.color_pair(3)) | |
# indicators | |
elif char in "wl" and y > 4 and y < engine.height - 2: | |
stdscr.attrset(curses.color_pair(9)) | |
elif char in "ou" and y > 4 and y < engine.height - 2: | |
stdscr.attrset(curses.color_pair(7)) | |
# death counters | |
elif char == "0" and x > 1 and x < engine.width - 1 and y > 4 and y < engine.height - 2 and (y - 5) % 4 == 2: | |
stdscr.attrset(curses.color_pair(7)) | |
elif char == "1" and x > 1 and x < engine.width - 1 and y > 4 and y < engine.height - 2 and (y - 5) % 4 == 2: | |
stdscr.attrset(curses.color_pair(7)) | |
elif char == "2" and x > 1 and x < engine.width - 1 and y > 4 and y < engine.height - 2 and (y - 5) % 4 == 2: | |
stdscr.attrset(curses.color_pair(7)) | |
elif char == "3" and x > 1 and x < engine.width - 1 and y > 4 and y < engine.height - 2 and (y - 5) % 4 == 2: | |
stdscr.attrset(curses.color_pair(8)) | |
# birth counters | |
elif char == "0" and x > 1 and x < engine.width - 1 and y > 4 and y < engine.height - 2 and (y - 5) % 4 != 2: | |
stdscr.attrset(curses.color_pair(9)) | |
elif char == "1" and x > 1 and x < engine.width - 1 and y > 4 and y < engine.height - 2 and (y - 5) % 4 != 2: | |
stdscr.attrset(curses.color_pair(9)) | |
elif char == "2" and x > 1 and x < engine.width - 1 and y > 4 and y < engine.height - 2 and (y - 5) % 4 != 2: | |
stdscr.attrset(curses.color_pair(6)) | |
# board | |
else: | |
stdscr.attrset(curses.color_pair(0)) | |
stdscr.addstr(y, x, char) | |
y += 1 | |
stdscr.move(*engine.get_cursor()) | |
stdscr.refresh() | |
# check for key press, sync with network player if connected, and update engine state | |
key = None | |
if conn: | |
if my_colour == engine.current_turn: | |
ch: int = stdscr.getch() | |
ch = engine.flip_cursor_y(ch, curses.KEY_UP, curses.KEY_DOWN) | |
conn.sendall(ch.to_bytes(2, "big") + hashlib.sha256(pickle.dumps(engine.recent_moves_str)).digest()[-2:]) | |
else: | |
msg = conn.recv(4) | |
ch = int.from_bytes(msg[:2], "big") | |
hash_lsb = msg[2:] | |
assert hash_lsb == hashlib.sha256(pickle.dumps(engine.recent_moves_str)).digest()[-2:], f"client and server are out of sync" | |
else: | |
ch: int = stdscr.getch() | |
ch = engine.flip_cursor_y(ch, curses.KEY_UP, curses.KEY_DOWN) | |
if ch == ord("\n") or ch == ord(" "): | |
key = "enter" | |
elif ch == ord("s"): | |
key = "stockfish" | |
elif ch == curses.KEY_UP: | |
key = "up" | |
elif ch == curses.KEY_DOWN: | |
key = "down" | |
elif ch == curses.KEY_LEFT: | |
key = "left" | |
elif ch == curses.KEY_RIGHT: | |
key = "right" | |
elif ch == curses.KEY_BACKSPACE or ch == ord("u"): | |
if len(engine_state) >= 2: | |
engine_state_redo.append(engine_state.pop()) | |
engine = pickle.loads(engine_state[-1]) | |
continue | |
elif ch == ord("r"): | |
if engine_state_redo: | |
engine_state.append(engine_state_redo.pop()) | |
engine = pickle.loads(engine_state[-1]) | |
continue | |
elif ch == 27 or ch == ord("q"): | |
key = "esc" | |
return 0 | |
else: | |
key = "other" | |
if engine.update_state(key, stockfish): | |
engine_state.append(pickle.dumps(engine)) | |
engine_state_redo = [] | |
class Engine: | |
def __init__(self, args) -> None: | |
self.args = args | |
self.board = Board(self.args) | |
# tick all the pieces for the first turn | |
for piece in self.board.get_pieces(): | |
piece.tick(self.board.get_surrounding_pieces(piece), "white", True) | |
self.cursor_row = 0 | |
self.cursor_col = 0 | |
self.height = len(self.board.display()) + 5 | |
self.width = len(self.board.display()[0]) | |
self.white_birth_queue = ["P", "R", "P", "N", "P", "B", "P", "Q", "P", "B", "P", "N", "P", "R"] | |
self.black_birth_queue = ["P", "R", "P", "N", "P", "B", "P", "Q", "P", "B", "P", "N", "P", "R"] | |
self.selected_piece = None | |
self.current_turn = "white" | |
self.col_labels = ["a", "b", "c", "d", "e", "f", "g", "h"] | |
self.recent_moves = [] | |
self.recent_moves_str = [] | |
self.game_over_message = None | |
self.use_unicode = not self.args.ascii | |
self.unicode_pieces = "♟♜♞♝♛♚♙♖♘♗♕♔" | |
self.ascii_pieces = "PRNBQKprnbqk" | |
if self.args.light: | |
self.ascii_pieces = "prnbqkPRNBQK" | |
self.unicode_replacements = dict(zip(self.ascii_pieces, self.unicode_pieces)) | |
assert self.height == len(self.display(None)) | |
assert self.width == len(self.display(None)[0]) | |
def get_cursor(self) -> tuple[int, int]: | |
"""get the position of the cursor in terms of display row and column""" | |
if self.args.flip: | |
real_row = (7 - self.cursor_row) * 4 + 7 | |
else: | |
real_row = self.cursor_row * 4 + 7 | |
real_col = self.cursor_col * 6 + 4 | |
return real_row, real_col | |
def flip_cursor_y(self, ch: int, key_up: int, key_down: int) -> int: | |
"""flip the key press for up and down""" | |
if self.args.flip: | |
if ch == key_up: | |
return key_down | |
elif ch == key_down: | |
return key_up | |
return ch | |
def display(self, my_colour) -> list: | |
board = self.board.display() | |
if self.game_over_message is not None: | |
header = f"Game over: {self.game_over_message}".center(self.width, " ") | |
else: | |
header = f"Current turn: {self.current_turn}{' (your turn)' if my_colour == self.current_turn else ''}".center(self.width, " ") | |
board.insert(0, list(header)) | |
if self.selected_piece is None: | |
header_2 = "Selected: None".center(self.width, " ") | |
else: | |
header_2 = f"Selected: {self.selected_piece}{self.col_labels[self.selected_piece.col]}{self.selected_piece.row + 1}".center(self.width, " ") | |
board.insert(1, list(header_2)) | |
header_3 = f"Recent moves: {' | '.join(self.recent_moves_str[-3:])}".center(self.width, " ") | |
board.insert(2, list(header_3)) | |
if self.use_unicode: | |
white_queue = f"White: {' '.join([self.unicode_replacements[piece] for piece in self.white_birth_queue])}".center(self.width, " ") | |
black_queue = f"Black: {' '.join([self.unicode_replacements[piece.lower()] for piece in self.black_birth_queue])}".center(self.width, " ") | |
else: | |
white_queue = f"White: {' '.join(self.white_birth_queue)}".center(self.width, " ") | |
black_queue = f"Black: {' '.join(self.black_birth_queue)}".center(self.width, " ") | |
if self.args.flip: | |
board.insert(3, list(black_queue)) | |
board.append(list(white_queue)) | |
else: | |
board.insert(3, list(white_queue)) | |
board.append(list(black_queue)) | |
return board | |
def move_is_valid(self, source, dest, stockfish) -> bool: | |
source_row = source.row + 1 | |
source_col = self.col_labels[source.col] | |
dest_row = dest.row + 1 | |
dest_col = self.col_labels[dest.col] | |
move_str = f"{source}{source_col}{source_row}->{dest}{dest_col}{dest_row}" | |
if source.move_is_valid(dest): | |
if stockfish is not None: | |
try: | |
stockfish.set_fen_position(self.board.get_fen_position(self.current_turn)) | |
if stockfish.is_move_correct(f"{source_col}{source_row}{dest_col}{dest_row}"): | |
self.recent_moves.append((source, dest)) | |
self.recent_moves_str.append(move_str) | |
return True | |
else: | |
return False | |
except: | |
self.recent_moves.append((source, dest)) | |
self.recent_moves_str.append(move_str) | |
return True | |
else: | |
self.recent_moves.append((source, dest)) | |
self.recent_moves_str.append(move_str) | |
return True | |
else: | |
return False | |
def update_state(self, key, stockfish) -> bool: | |
"""returns whether there was a state change""" | |
# cursor | |
if key: | |
if key == "up": | |
self.cursor_row = (self.cursor_row - 1) % 8 | |
elif key == "down": | |
self.cursor_row = (self.cursor_row + 1) % 8 | |
elif key == "left": | |
self.cursor_col = (self.cursor_col - 1) % 8 | |
elif key == "right": | |
self.cursor_col = (self.cursor_col + 1) % 8 | |
elif key == "stockfish": | |
try: | |
if stockfish is not None: | |
stockfish.set_fen_position(self.board.get_fen_position(self.current_turn)) | |
move = stockfish.get_best_move() | |
if move is not None: | |
self.selected_piece = self.board.get_piece(int(move[1]) - 1, ord(move[0]) - ord("a")) | |
self.cursor_row = int(move[3]) - 1 | |
self.cursor_col = ord(move[2]) - ord("a") | |
key = "enter" | |
except: | |
pass | |
if key == "enter": | |
if self.selected_piece is None: | |
# select a piece | |
if self.board.get_piece(self.cursor_row, self.cursor_col).side == self.current_turn: | |
self.selected_piece = self.board.get_piece(self.cursor_row, self.cursor_col) | |
elif self.move_is_valid(self.selected_piece, self.board.get_piece(self.cursor_row, self.cursor_col), stockfish): | |
# move the selected piece to the cursor | |
try: | |
self.board.move_piece(self.selected_piece, self.board.get_piece(self.cursor_row, self.cursor_col)) | |
except Exception as e: | |
self.game_over_message = str(e) | |
return False | |
self.selected_piece = None | |
self.current_turn = "black" if self.current_turn == "white" else "white" | |
# tick all the pieces at the start of the next turn | |
for piece in self.board.get_pieces(): | |
piece.tick(self.board.get_surrounding_pieces(piece), self.current_turn, True) | |
# check if any pieces need to be born | |
if self.current_turn == "white": | |
for i in range(8): | |
for j in range(8): | |
piece = self.board.get_piece(i, j) | |
if piece.side == "empty": | |
if piece.birth_counter_white == 3: | |
next_piece = self.white_birth_queue.pop(0) | |
self.board.set_new_piece(i, j, next_piece, "white") | |
self.white_birth_queue.append(next_piece) | |
elif self.current_turn == "black": | |
for i in reversed(range(8)): | |
for j in range(8): | |
piece = self.board.get_piece(i, j) | |
if piece.side == "empty": | |
if piece.birth_counter_black == 3: | |
next_piece = self.black_birth_queue.pop(0) | |
self.board.set_new_piece(i, j, next_piece, "black") | |
self.black_birth_queue.append(next_piece) | |
# check if any pieces need to die | |
for piece in self.board.get_pieces(): | |
if piece.death_counter == 4: | |
try: | |
self.board.kill_piece(piece, self.current_turn) | |
except Exception as e: | |
self.game_over_message = str(e) | |
return False | |
# recalculate nearby pieces for indicators | |
for piece in self.board.get_pieces(): | |
piece.tick(self.board.get_surrounding_pieces(piece), self.current_turn, False) | |
return True | |
if key == "other": | |
self.selected_piece = None | |
return False | |
class Board: | |
def __init__(self, args) -> None: | |
self.args = args | |
self.board: list[list[Piece]] = [[Empty() for x in range(8)] for y in range(8)] | |
self.board[0] = [ | |
Rook("white"), | |
Knight("white"), | |
Bishop("white"), | |
Queen("white"), | |
King("white"), | |
Bishop("white"), | |
Knight("white"), | |
Rook("white") | |
] | |
self.board[1] = [Pawn("white") for x in range(8)] | |
self.board[6] = [Pawn("black") for x in range(8)] | |
self.board[7] = [ | |
Rook("black"), | |
Knight("black"), | |
Bishop("black"), | |
Queen("black"), | |
King("black"), | |
Bishop("black"), | |
Knight("black"), | |
Rook("black") | |
] | |
self.piece_width = 5 | |
self.piece_height = 3 | |
for i in range(8): | |
for j in range(8): | |
self.board[i][j].set_position(i, j) | |
def get_fen_position(self, current_turn: str) -> str: | |
# return a string in Forsyth-Edwards Notation (FEN) | |
fen = "" | |
for row in reversed(self.board): | |
empty_spaces = 0 | |
for piece in row: | |
if piece.side == "empty": | |
empty_spaces += 1 | |
else: | |
if empty_spaces > 0: | |
fen += str(empty_spaces) | |
empty_spaces = 0 | |
fen += str(piece) | |
if empty_spaces > 0: | |
fen += str(empty_spaces) | |
fen += "/" | |
fen = fen[:-1] | |
fen += " " + current_turn[0] + " - - 0 1" | |
return fen | |
def get_piece(self, row: int, col: int) -> "Piece": | |
return self.board[row][col] | |
def get_pieces(self) -> list["Piece"]: | |
pieces = [] | |
for row in self.board: | |
for piece in row: | |
pieces.append(piece) | |
return pieces | |
def get_surrounding_pieces(self, piece: "Piece") -> list["Piece"]: | |
surrounding_pieces = [] | |
for i in range(-1, 2): | |
for j in range(-1, 2): | |
if i == 0 and j == 0: | |
continue | |
elif 0 <= piece.row + i < 8 and 0 <= piece.col + j < 8: | |
surrounding_pieces.append(self.board[piece.row + i][piece.col + j]) | |
return surrounding_pieces | |
def set_new_piece(self, row: int, col: int, piece: str, side: str) -> None: | |
if piece == "P": | |
self.board[row][col] = Pawn(side) | |
elif piece == "R": | |
self.board[row][col] = Rook(side) | |
elif piece == "N": | |
self.board[row][col] = Knight(side) | |
elif piece == "B": | |
self.board[row][col] = Bishop(side) | |
elif piece == "Q": | |
self.board[row][col] = Queen(side) | |
elif piece == "K": | |
self.board[row][col] = King(side) | |
else: | |
raise ValueError("invalid piece") | |
self.board[row][col].set_position(row, col) | |
def kill_piece(self, piece: "Piece", turn: str) -> None: | |
"""piece died due to over/under population""" | |
if piece.side == turn: | |
row = piece.row | |
col = piece.col | |
self.board[row][col].perish(conway=True) | |
self.board[row][col] = Empty() | |
self.board[row][col].set_position(row, col) | |
def display(self) -> list: | |
"""get a version of the board suitable for printing to the ui""" | |
# use ascii art to create a grid between the pieces | |
WIDTH = self.piece_width | |
HEIGHT = self.piece_height | |
middle = ["─"] * WIDTH + ["┬"] | |
board = [["┌"] + middle * 7 + ["─"] * WIDTH + ["┐"]] | |
for row in reversed(self.board) if self.args.flip else self.board: | |
board += [["│"], ["│"], ["│"]] | |
for piece in row: | |
board[-3] += piece.display()[0] + ["│"] | |
board[-2] += piece.display()[1] + ["│"] | |
board[-1] += piece.display()[2] + ["│"] | |
middle = ["─"] * WIDTH + ["┼"] | |
board += [["├"] + middle * 7 + ["─"] * WIDTH + ["┤"]] | |
_ = board.pop() | |
middle = ["─"] * WIDTH + ["┴"] | |
board += [["└"] + middle * 7 + ["─"] * WIDTH + ["┘"]] | |
# add the row and column numbers, NOTE: need to readjust if changing piece size | |
for i in range(len(board)): | |
if self.args.flip: | |
board[i] = [str(9 - ((i + 2) // 4)) if (i + 2) % 4 == 0 else " "] + board[i] + [str(9 - ((i + 2) // 4)) if (i + 2) % 4 == 0 else " "] | |
else: | |
board[i] = [str((i + 2) // 4) if (i + 2) % 4 == 0 else " "] + board[i] + [str((i + 2) // 4) if (i + 2) % 4 == 0 else " "] | |
board = [[" ", " ", " "] + list(" ".join(list("abcdefgh"))) + [" ", " ", " "]] + board | |
board += [[" ", " ", " "] + list(" ".join(list("abcdefgh"))) + [" ", " ", " "]] | |
return board | |
def move_piece(self, source: "Piece", dest: "Piece") -> bool: | |
"""moves piece and returns whether move is successful""" | |
if not source.move_is_valid(dest): | |
return False | |
# check if the move is a capture and move the piece | |
self.board[dest.row][dest.col].perish(conway=False) | |
self.board[dest.row][dest.col] = source | |
# replace the old position with an empty piece | |
self.board[source.row][source.col] = Empty() | |
self.board[source.row][source.col].set_position(source.row, source.col) | |
# update the position of the moved piece | |
source.set_position(dest.row, dest.col) | |
return True | |
class Piece: | |
def __init__(self, side) -> None: | |
"""common attributes (required by every chess piece)""" | |
self.side = side | |
self.row = -1 | |
self.col = -1 | |
self.death_counter = 0 | |
self.birth_counter_white = 0 | |
self.birth_counter_black = 0 | |
self.surrounding_white = 0 | |
self.surrounding_black = 0 | |
def __str__(self) -> str: | |
"""for displaying entities on the map ui""" | |
raise NotImplementedError() | |
def set_position(self, row: int, col: int) -> None: | |
"""set the position of the piece""" | |
self.row = row | |
self.col = col | |
def get_position(self) -> tuple[int, int]: | |
"""get the position of the piece""" | |
return self.row, self.col | |
def display(self) -> list[list[str]]: | |
"""get a 3x3 list of chars of the piece suitable for printing to the ui""" | |
white_reproduction = "w" if self.side == "empty" and self.surrounding_white == 3 else " " | |
black_reproduction = "l" if self.side == "empty" and self.surrounding_black == 3 else " " | |
over_under_population = " " | |
if self.side == "white": | |
if self.surrounding_white < 2: | |
over_under_population = "u" | |
elif self.surrounding_white > 3: | |
over_under_population = "o" | |
elif self.side == "black": | |
if self.surrounding_black < 2: | |
over_under_population = "u" | |
elif self.surrounding_black > 3: | |
over_under_population = "o" | |
chars_to_print = [ | |
[" " if white_reproduction == " " else str(self.birth_counter_white), " ", " ", " ", white_reproduction], | |
[" " if over_under_population == " " else str(self.death_counter), " ", str(self), " ", over_under_population], | |
[" " if black_reproduction == " " else str(self.birth_counter_black), " ", " ", " ", black_reproduction] | |
] | |
return chars_to_print | |
def move_is_valid(self, dest_piece: "Piece") -> bool: | |
"""check if the move is valid, TODO: check with chess logic, and add special moves""" | |
if dest_piece.side == self.side: | |
return False | |
elif dest_piece.side == "empty": | |
return True | |
else: | |
return True | |
def tick(self, surrounding_pieces: list["Piece"], current_turn: str, update_counters: bool) -> None: | |
"""perform next step in life cycle, only ticks for players pieces before their turn""" | |
self.surrounding_white = 0 | |
self.surrounding_black = 0 | |
for piece in surrounding_pieces: | |
if piece.side == "white": | |
self.surrounding_white += 1 | |
elif piece.side == "black": | |
self.surrounding_black += 1 | |
if not update_counters: | |
return | |
if self.side == "empty": | |
if current_turn == "white": | |
if self.surrounding_white == 3: | |
self.birth_counter_white += 1 | |
else: | |
self.birth_counter_white = 0 | |
if current_turn == "black": | |
if self.surrounding_black == 3: | |
self.birth_counter_black += 1 | |
else: | |
self.birth_counter_black = 0 | |
elif current_turn == self.side: | |
same_pieces = 0 | |
for piece in surrounding_pieces: | |
if piece.side == self.side: | |
same_pieces += 1 | |
if same_pieces < 2 or same_pieces > 3: | |
self.death_counter += 1 | |
else: | |
self.death_counter = 0 | |
def perish(self, conway: bool) -> None: | |
"""piece perished due to over/under population (conway=True) or capture (conway=False)""" | |
pass | |
class Empty(Piece): | |
def __init__(self) -> None: | |
"""empty space on the map""" | |
super().__init__("empty") | |
def __str__(self) -> str: | |
return " " | |
def move_is_valid(self, dest_piece: Piece) -> bool: | |
return False | |
class Pawn(Piece): | |
def __init__(self, side) -> None: | |
"""pawn chess piece""" | |
super().__init__(side) | |
def __str__(self) -> str: | |
return "P" if self.side == "white" else "p" | |
class Knight(Piece): | |
def __init__(self, side) -> None: | |
"""knight chess piece""" | |
super().__init__(side) | |
def __str__(self) -> str: | |
return "N" if self.side == "white" else "n" | |
class Bishop(Piece): | |
def __init__(self, side) -> None: | |
"""bishop chess piece""" | |
super().__init__(side) | |
def __str__(self) -> str: | |
return "B" if self.side == "white" else "b" | |
class Rook(Piece): | |
def __init__(self, side) -> None: | |
"""rook chess piece""" | |
super().__init__(side) | |
def __str__(self) -> str: | |
return "R" if self.side == "white" else "r" | |
class Queen(Piece): | |
def __init__(self, side) -> None: | |
"""queen chess piece""" | |
super().__init__(side) | |
def __str__(self) -> str: | |
return "Q" if self.side == "white" else "q" | |
class King(Piece): | |
def __init__(self, side) -> None: | |
"""king chess piece""" | |
super().__init__(side) | |
def __str__(self) -> str: | |
return "K" if self.side == "white" else "k" | |
def perish(self, conway) -> None: | |
winning_side = "white" if self.side == "black" else "black" | |
losing_side = "Black" if self.side == "black" else "White" | |
if conway: | |
raise Exception(f"{losing_side} king perished, {winning_side} wins!") | |
else: | |
raise Exception(f"{losing_side} king was captured, {winning_side} wins!") | |
if __name__ == "__main__": | |
sys.exit(start_cli()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Pretty cool!