Conway's Game of Chess (moved to https://github.com/elesiuta/conways-game-of-chess)

17 min read Original article ↗
#!/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())