Skip to content

Instantly share code, notes, and snippets.

@elesiuta
Last active February 11, 2024 20:01
Show Gist options
  • Save elesiuta/a17d93feb3d29971c3b5abbde69d53f3 to your computer and use it in GitHub Desktop.
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)
#!/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())
@elesiuta
Copy link
Author

elesiuta commented Sep 17, 2023

Chess, but each square also follows the rules of Conway's Game of Life.

  • Only your own pieces count as neighbours when deciding births/deaths.
  • For births, an empty square must have 3 neighbours for 2 consecutive turns.
  • For deaths, a piece must have <2 or >3 neighbours for 3 consecutive turns.

This is a simple proof of concept I made with python and curses.

You can play with a friend locally or over a socket connection.

You can also run it in your browser with replit but it's pretty janky, it's better to run in your own terminal.

@elesiuta
Copy link
Author

@danthedaniel
Copy link

Pretty cool!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment