import chessconf from random import choice, choices from copy import deepcopy from subprocess import Popen, PIPE move_struct = tuple[tuple[int, int, str], tuple[int, int, str]] | tuple[tuple[int, int, str], tuple[int, int, str], tuple[int, int, str]] | tuple[tuple[int, int, str], tuple[int, int, str], tuple[int, int, str], tuple[int, int, str]] board_struct = tuple[list[str], list[str], list[str], list[str], list[str], list[str], list[str], list[str]] ICON = { 'K': chessconf.WHITECOLOR + chessconf.WHITE_KING_ICON + '\x1b[0m', 'Q': chessconf.WHITECOLOR + chessconf.WHITE_QUEEN_ICON + '\x1b[0m', 'R': chessconf.WHITECOLOR + chessconf.WHITE_ROOK_ICON + '\x1b[0m', 'B': chessconf.WHITECOLOR + chessconf.WHITE_BISHOP_ICON + '\x1b[0m', 'N': chessconf.WHITECOLOR + chessconf.WHITE_KNIGHT_ICON + '\x1b[0m', 'P': chessconf.WHITECOLOR + chessconf.WHITE_PAWN_ICON + '\x1b[0m', '.': chessconf.SPACECOLOR + chessconf.SPACE_ICON + '\x1b[0m', 'k': chessconf.BLACKCOLOR + chessconf.BLACK_KING_ICON + '\x1b[0m', 'q': chessconf.BLACKCOLOR + chessconf.BLACK_QUEEN_ICON + '\x1b[0m', 'r': chessconf.BLACKCOLOR + chessconf.BLACK_ROOK_ICON + '\x1b[0m', 'b': chessconf.BLACKCOLOR + chessconf.BLACK_BISHOP_ICON + '\x1b[0m', 'n': chessconf.BLACKCOLOR + chessconf.BLACK_KNIGHT_ICON + '\x1b[0m', 'p': chessconf.BLACKCOLOR + chessconf.BLACK_PAWN_ICON + '\x1b[0m'} INT_TO_ALPH = {0: 'a', 1: 'b', 2: 'c', 3: 'd', 4: 'e', 5: 'f', 6: 'g', 7: 'h'} ALPH_TO_INT = {'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4, 'f': 5, 'g': 6, 'h': 7} INT_TO_NUM = {0: '8', 1: '7', 2: '6', 3: '5', 4: '4', 5: '3', 6: '2', 7: '1'} NUM_TO_INT = {'8': 0, '7': 1, '6': 2, '5': 3, '4': 4, '3': 5, '2': 6, '1': 7} COLORWHITE = {'R': 'R', 'N': 'N', 'B': 'B', 'K': 'K', 'Q': 'Q', 'P': 'P', 'r': 'R', 'n': 'N', 'b': 'B', 'k': 'K', 'q': 'Q', 'p': 'P'} COLORBLACK = {'R': 'r', 'N': 'n', 'B': 'b', 'K': 'k', 'Q': 'q', 'P': 'p', 'r': 'r', 'n': 'n', 'b': 'b', 'k': 'k', 'q': 'q', 'p': 'p'} WHITEPIECES, BLACKPIECES = {'R', 'N', 'B', 'Q', 'K', 'P'}, {'r', 'n', 'b', 'q', 'k', 'p'} def render_hex(hex_val: int) -> None: if not isinstance(hex_val, int): raise ValueError('Input must be a string or an integer.') if hex_val > 0xFFFFFFFFFFFFFFFF: raise ValueError('Input hexadecimal value is too large, must be 64 bits or less.') binary_value = bin(hex_val)[2:].zfill(64) print('\n', end = '') for i in range(0, 64, 8): print(*['\x1b[47m' + bit + '\x1b[0m' if bit == '1' else bit for bit in binary_value[i:i+8]]) def invert(b): return b ^ 0xFFFFFFFFFFFFFFFF class Board(): def __init__(self, white: bool = True) -> None: self.pos: board_struct = ( ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'r'], ['p', 'p', 'p', 'p', 'p', 'p', 'p', 'p'], ['.', '.', '.', '.', '.', '.', '.', '.'], ['.', '.', '.', '.', '.', '.', '.', '.'], ['.', '.', '.', '.', '.', '.', '.', '.'], ['.', '.', '.', '.', '.', '.', '.', '.'], ['P', 'P', 'P', 'P', 'P', 'P', 'P', 'P'], ['R', 'N', 'B', 'Q', 'K', 'B', 'N', 'R']) self.player_color, self.white = True, white self.hist, self.pos_hist = [], [deepcopy(self.pos)] self.half_move_ctr, self.fifty_move_ctr = 0, 0 self.w_short_castling, self.w_long_castling, self.b_short_castling, self.b_long_castling = True, True, True, True self.ep_sqr = None self.board_turned = False self.asked_draw = False self.bb = { 'p': 0x00FF000000000000, 'r': 0x8100000000000000, 'n': 0x4200000000000000, 'b': 0x2400000000000000, 'q': 0x1000000000000000, 'k': 0x0800000000000000, 'P': 0x000000000000FF00, 'R': 0x0000000000000081, 'N': 0x0000000000000042, 'B': 0x0000000000000024, 'Q': 0x0000000000000010, 'K': 0x0000000000000008} def bb_black(self): return self.bb['p'] | self.bb['r'] | self.bb['n'] | self.bb['b'] | self.bb['q'] | self.bb['k'] def bb_white(self): return self.bb['P'] | self.bb['R'] | self.bb['N'] | self.bb['B'] | self.bb['Q'] | self.bb['K'] def bb(self): return self.bb_white() | self.bb_black() def _bb_rook_moves(self, white: bool): bm, enemypieces = (self.bb_R, self.bb_black()) if white else (self.bb_r, self.bb_white()) render_hex(bm) render_hex(bm << 8) def render(self, white: bool) -> None: if (white and not self.board_turned) or (not white and self.board_turned): if self.board_turned: print('\nBoard turned! Use /turnboard to turn it back.') print(f''' ┌───┬───┬───┬───┬───┬───┬───┬───┐ 8 │ {' │ '.join((ICON[self.pos[0][0]], ICON[self.pos[0][1]], ICON[self.pos[0][2]], ICON[self.pos[0][3]], ICON[self.pos[0][4]], ICON[self.pos[0][5]], ICON[self.pos[0][6]], ICON[self.pos[0][7]]))} │ ├───┼───┼───┼───┼───┼───┼───┼───┤ 7 │ {' │ '.join((ICON[self.pos[1][0]], ICON[self.pos[1][1]], ICON[self.pos[1][2]], ICON[self.pos[1][3]], ICON[self.pos[1][4]], ICON[self.pos[1][5]], ICON[self.pos[1][6]], ICON[self.pos[1][7]]))} │ ├───┼───┼───┼───┼───┼───┼───┼───┤ 6 │ {' │ '.join((ICON[self.pos[2][0]], ICON[self.pos[2][1]], ICON[self.pos[2][2]], ICON[self.pos[2][3]], ICON[self.pos[2][4]], ICON[self.pos[2][5]], ICON[self.pos[2][6]], ICON[self.pos[2][7]]))} │ ├───┼───┼───┼───┼───┼───┼───┼───┤ 5 │ {' │ '.join((ICON[self.pos[3][0]], ICON[self.pos[3][1]], ICON[self.pos[3][2]], ICON[self.pos[3][3]], ICON[self.pos[3][4]], ICON[self.pos[3][5]], ICON[self.pos[3][6]], ICON[self.pos[3][7]]))} │ ├───┼───┼───┼───┼───┼───┼───┼───┤ 4 │ {' │ '.join((ICON[self.pos[4][0]], ICON[self.pos[4][1]], ICON[self.pos[4][2]], ICON[self.pos[4][3]], ICON[self.pos[4][4]], ICON[self.pos[4][5]], ICON[self.pos[4][6]], ICON[self.pos[4][7]]))} │ ├───┼───┼───┼───┼───┼───┼───┼───┤ 3 │ {' │ '.join((ICON[self.pos[5][0]], ICON[self.pos[5][1]], ICON[self.pos[5][2]], ICON[self.pos[5][3]], ICON[self.pos[5][4]], ICON[self.pos[5][5]], ICON[self.pos[5][6]], ICON[self.pos[5][7]]))} │ ├───┼───┼───┼───┼───┼───┼───┼───┤ 2 │ {' │ '.join((ICON[self.pos[6][0]], ICON[self.pos[6][1]], ICON[self.pos[6][2]], ICON[self.pos[6][3]], ICON[self.pos[6][4]], ICON[self.pos[6][5]], ICON[self.pos[6][6]], ICON[self.pos[6][7]]))} │ ├───┼───┼───┼───┼───┼───┼───┼───┤ 1 │ {' │ '.join((ICON[self.pos[7][0]], ICON[self.pos[7][1]], ICON[self.pos[7][2]], ICON[self.pos[7][3]], ICON[self.pos[7][4]], ICON[self.pos[7][5]], ICON[self.pos[7][6]], ICON[self.pos[7][7]]))} │ └───┴───┴───┴───┴───┴───┴───┴───┘ a b c d e f g h''') else: if self.board_turned: print('\nBoard turned! Use /turnboard to turn it back.') print(f''' ┌───┬───┬───┬───┬───┬───┬───┬───┐ 1 │ {' │ '.join((ICON[self.pos[7][7]], ICON[self.pos[7][6]], ICON[self.pos[7][5]], ICON[self.pos[7][4]], ICON[self.pos[7][3]], ICON[self.pos[7][2]], ICON[self.pos[7][1]], ICON[self.pos[7][0]]))} │ ├───┼───┼───┼───┼───┼───┼───┼───┤ 2 │ {' │ '.join((ICON[self.pos[6][7]], ICON[self.pos[6][6]], ICON[self.pos[6][5]], ICON[self.pos[6][4]], ICON[self.pos[6][3]], ICON[self.pos[6][2]], ICON[self.pos[6][1]], ICON[self.pos[6][0]]))} │ ├───┼───┼───┼───┼───┼───┼───┼───┤ 3 │ {' │ '.join((ICON[self.pos[5][7]], ICON[self.pos[5][6]], ICON[self.pos[5][5]], ICON[self.pos[5][4]], ICON[self.pos[5][3]], ICON[self.pos[5][2]], ICON[self.pos[5][1]], ICON[self.pos[5][0]]))} │ ├───┼───┼───┼───┼───┼───┼───┼───┤ 4 │ {' │ '.join((ICON[self.pos[4][7]], ICON[self.pos[4][6]], ICON[self.pos[4][5]], ICON[self.pos[4][4]], ICON[self.pos[4][3]], ICON[self.pos[4][2]], ICON[self.pos[4][1]], ICON[self.pos[4][0]]))} │ ├───┼───┼───┼───┼───┼───┼───┼───┤ 5 │ {' │ '.join((ICON[self.pos[3][7]], ICON[self.pos[3][6]], ICON[self.pos[3][5]], ICON[self.pos[3][4]], ICON[self.pos[3][3]], ICON[self.pos[3][2]], ICON[self.pos[3][1]], ICON[self.pos[3][0]]))} │ ├───┼───┼───┼───┼───┼───┼───┼───┤ 6 │ {' │ '.join((ICON[self.pos[2][7]], ICON[self.pos[2][6]], ICON[self.pos[2][5]], ICON[self.pos[2][4]], ICON[self.pos[2][3]], ICON[self.pos[2][2]], ICON[self.pos[2][1]], ICON[self.pos[2][0]]))} │ ├───┼───┼───┼───┼───┼───┼───┼───┤ 7 │ {' │ '.join((ICON[self.pos[1][7]], ICON[self.pos[1][6]], ICON[self.pos[1][5]], ICON[self.pos[1][4]], ICON[self.pos[1][3]], ICON[self.pos[1][2]], ICON[self.pos[1][1]], ICON[self.pos[1][0]]))} │ ├───┼───┼───┼───┼───┼───┼───┼───┤ 8 │ {' │ '.join((ICON[self.pos[0][7]], ICON[self.pos[0][6]], ICON[self.pos[0][5]], ICON[self.pos[0][4]], ICON[self.pos[0][3]], ICON[self.pos[0][2]], ICON[self.pos[0][1]], ICON[self.pos[0][0]]))} │ └───┴───┴───┴───┴───┴───┴───┴───┘ h g f e d c b a''') def _rook_moves(self, y: int, x: int, white: bool) -> list: enemypieces, rook = (BLACKPIECES, 'R') if white else (WHITEPIECES, 'r') moves = [] directions = ((1, 0), (0, 1), (-1, 0), (0, -1)) for dy, dx in directions: newy, newx = y + dy, x + dx while 0 <= newy <= 7 and 0 <= newx <= 7: if self.pos[newy][newx] == '.': moves.append(((y, x, '.'), (newy, newx, rook))) elif self.pos[newy][newx] in enemypieces: moves.append(((y, x, '.'), (newy, newx, rook))) break else: break newy, newx = newy + dy, newx + dx return moves def _knight_moves(self, y: int, x: int, white: bool) -> list: ownpieces, knight = (WHITEPIECES, 'N') if white else (BLACKPIECES, 'n') moves = [] directions = ((1, 2), (1, -2), (2, 1), (2, -1), (-1, 2), (-1, -2), (-2, 1), (-2, -1)) for dy, dx in directions: newy, newx = y + dy, x + dx if 0 <= newy <= 7 and 0 <= newx <= 7 and self.pos[newy][newx] not in ownpieces: moves.append(((y, x, '.'), (newy, newx, knight))) return moves def _bishop_moves(self, y: int, x: int, white: bool) -> list: enemypieces, bishop = (BLACKPIECES, 'B') if white else (WHITEPIECES, 'b') moves = [] directions = ((1, 1), (1, -1), (-1, 1), (-1, -1)) for dy, dx in directions: newy, newx = y + dy, x + dx while 0 <= newx <= 7 and 0 <= newy <= 7: if self.pos[newy][newx] == '.': moves.append(((y, x, '.'), (newy, newx, bishop))) elif self.pos[newy][newx] in enemypieces: moves.append(((y, x, '.'), (newy, newx, bishop))) break else: break newy, newx = newy + dy, newx + dx return moves def _queen_moves(self, y: int, x: int, white: bool) -> list: enemypieces, queen = (BLACKPIECES, 'Q') if white else (WHITEPIECES, 'q') moves = [] directions = ((1, 1), (1, -1), (-1, 1), (-1, -1), (1, 0), (0, 1), (-1, 0), (0, -1)) for dy, dx in directions: newy, newx = y + dy, x + dx while 0 <= newx <= 7 and 0 <= newy <= 7: if self.pos[newy][newx] == '.': moves.append(((y, x, '.'), (newy, newx, queen))) elif self.pos[newy][newx] in enemypieces: moves.append(((y, x, '.'), (newy, newx, queen))) break else: break newy, newx = newy + dy, newx + dx return moves def _king_moves(self, y: int, x: int, white: bool) -> list: ownpieces, king = (WHITEPIECES, 'K') if white else (BLACKPIECES, 'k') moves = [] directions = ((1, 1), (1, -1), (-1, 1), (-1, -1), (1, 0), (0, 1), (-1, 0), (0, -1)) for dy, dx in directions: newy, newx = y + dy, x + dx if 0 <= newy <= 7 and 0 <= newx <= 7 and self.pos[newy][newx] not in ownpieces: moves.append(((y, x, '.'), (newy, newx, king))) return moves def _castling_moves(self, white: bool) -> list: shortcastling, longcastling, colorpiece, castlingrow = (self.w_short_castling, self.w_long_castling, COLORWHITE, 7) if white else (self.b_short_castling, self.b_long_castling, COLORBLACK, 0) moves = [] if shortcastling and self.pos[castlingrow][5] == '.' and self.pos[castlingrow][6] == '.': moves.append(((castlingrow, 4, '.'), (castlingrow, 5, colorpiece['R']), (castlingrow, 6, colorpiece['K']), (castlingrow, 7, '.'))) if longcastling and self.pos[castlingrow][1] == '.' and self.pos[castlingrow][2] == '.' and self.pos[castlingrow][3] == '.': moves.append(((castlingrow, 0, '.'), (castlingrow, 2, colorpiece['K']), (castlingrow, 3, colorpiece['R']), (castlingrow, 4, '.'))) return moves def _pawn_moves(self, y: int, x: int, white: bool) -> list: startingrow, forwardstep, promotionrow = (6, -1, 1) if white else (1, 1, 6) colorpiece, enemypieces = (COLORWHITE, BLACKPIECES) if white else (COLORBLACK, WHITEPIECES) moves = [] if 0 <= y + forwardstep <= 7 and self.pos[y + forwardstep][x] == '.': if y == promotionrow: for i in (colorpiece['Q'], colorpiece['R'], colorpiece['N'], colorpiece['B']): moves.append(((y, x, '.'), (y + forwardstep, x, i))) else: moves.append(((y, x, '.'), (y + forwardstep, x, colorpiece['P']))) if y == startingrow and self.pos[y + forwardstep * 2][x] == '.': moves.append(((y, x, '.'), (y + forwardstep * 2, x, colorpiece['P']))) for dx in (1, -1): newx = x + dx if 0 <= y + forwardstep <= 7 and 0 <= newx <= 7 and self.pos[y + forwardstep][newx] in enemypieces: if y == promotionrow: for i in (colorpiece['Q'], colorpiece['R'], colorpiece['N'], colorpiece['B']): moves.append(((y, x, '.'), (y + forwardstep, newx, i))) else: # taking enemy piece moves.append(((y, x, '.'), (y + forwardstep, newx, colorpiece['P']))) if [y + forwardstep, newx] == self.ep_sqr: # en passant moves.append(((y, x, '.'), (y + forwardstep, newx, colorpiece['P']), (y, newx, '.'))) return moves def all_moves(self, white: bool) -> tuple: colorpiece = COLORWHITE if white else COLORBLACK move_generators = {colorpiece['P']: self._pawn_moves, colorpiece['R']: self._rook_moves, colorpiece['N']: self._knight_moves, colorpiece['B']: self._bishop_moves, colorpiece['Q']: self._queen_moves, colorpiece['K']: self._king_moves} moves = [] for y, row in enumerate(self.pos): for x, piece in enumerate(row): if piece in move_generators: moves.extend(move_generators[piece](y, x, white)) moves.extend(self._castling_moves(white)) return tuple(moves) def reach(self, white: bool) -> tuple: reach = (([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], [])) colorpiece, forwardstep, startingrow, enemypieces = (COLORWHITE, -1, 6, BLACKPIECES) if white else (COLORBLACK, 1, 1, WHITEPIECES) shortcastling, longcastling, castlingrow = (self.w_short_castling, self.w_long_castling, 7) if white else (self.b_short_castling, self.b_long_castling, 0) for y, row in enumerate(self.pos): for x, piece in enumerate(row): if piece == colorpiece['R']: directions = ((1, 0), (0, 1), (-1, 0), (0, -1)) for dy, dx in directions: newy, newx = y + dy, x + dx while 0 <= newy <= 7 and 0 <= newx <= 7: if self.pos[newy][newx] != '.': reach[newy][newx].append((y, x, piece)) break reach[newy][newx].append((y, x, piece)) newy, newx = newy + dy, newx + dx elif piece == colorpiece['N']: directions = ((1, 2), (1, -2), (2, 1), (2, -1), (-1, 2), (-1, -2), (-2, 1), (-2, -1)) for dy, dx in directions: newy, newx = y + dy, x + dx if 0 <= newy <= 7 and 0 <= newx <= 7: reach[newy][newx].append((y, x, piece)) elif piece == colorpiece['B']: directions = ((1, 1), (1, -1), (-1, 1), (-1, -1)) for dy, dx in directions: newy, newx = y + dy, x + dx while 0 <= newx <= 7 and 0 <= newy <= 7: if self.pos[newy][newx] != '.': reach[newy][newx].append((y, x, piece)) break reach[newy][newx].append((y, x, piece)) newy, newx = newy + dy, newx + dx elif piece == colorpiece['Q']: directions = ((1, 1), (1, -1), (-1, 1), (-1, -1), (1, 0), (0, 1), (-1, 0), (0, -1)) for dy, dx in directions: newy, newx = y + dy, x + dx while 0 <= newx <= 7 and 0 <= newy <= 7: if self.pos[newy][newx] != '.': reach[newy][newx].append((y, x, piece)) break reach[newy][newx].append((y, x, piece)) newy, newx = newy + dy, newx + dx elif piece == colorpiece['K']: directions = ((1, 1), (1, -1), (-1, 1), (-1, -1), (1, 0), (0, 1), (-1, 0), (0, -1)) for dy, dx in directions: newy, newx = y + dy, x + dx if 0 <= newy <= 7 and 0 <= newx <= 7: reach[newy][newx].append((y, x, piece)) if shortcastling and self.pos[castlingrow][5] == '.' and self.pos[castlingrow][6] == '.': reach[castlingrow][6].append((y, x, piece)) if longcastling and self.pos[castlingrow][2] == '.' and self.pos[castlingrow][3] == '.': reach[castlingrow][2].append((y, x, piece)) elif piece == colorpiece['P']: if self.pos[y + forwardstep][x] == '.': reach[y + forwardstep][x].append((y, x, piece)) if y == startingrow and self.pos[y + (forwardstep * 2)][x] == '.': reach[y + (forwardstep * 2)][x].append((y, x, piece)) for dx in (1, -1): newx = x + dx if 0 <= y + forwardstep <= 7 and 0 <= newx <= 7 and (self.pos[y + forwardstep][newx] in enemypieces or self.ep_sqr == (y + forwardstep, newx)): reach[y + forwardstep][newx].append((y, x, piece)) return reach def control(self, white: bool) -> tuple: control = (([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], [])) colorpiece, colorenemypiece, forwardstep = (COLORWHITE, COLORBLACK, -1) if white else (COLORBLACK, COLORWHITE, 1) for y, row in enumerate(self.pos): for x, piece in enumerate(row): if piece == colorpiece['R']: directions = ((1, 0), (0, 1), (-1, 0), (0, -1)) for dy, dx in directions: newy, newx = y + dy, x + dx while 0 <= newy <= 7 and 0 <= newx <= 7: if self.pos[newy][newx] not in {'.', colorpiece['R'], colorpiece['Q']}: control[newy][newx].append((y, x, piece)) break control[newy][newx].append((y, x, piece)) newy, newx = newy + dy, newx + dx elif piece == colorpiece['N']: directions = ((1, 2), (1, -2), (2, 1), (2, -1), (-1, 2), (-1, -2), (-2, 1), (-2, -1)) for dy, dx in directions: newy, newx = y + dy, x + dx if 0 <= newy <= 7 and 0 <= newx <= 7: control[newy][newx].append((y, x, piece)) elif piece == colorpiece['B']: directions = ((1, 1), (1, -1), (-1, 1), (-1, -1)) for dy, dx in directions: newy, newx = y + dy, x + dx while 0 <= newx <= 7 and 0 <= newy <= 7: if self.pos[newy][newx] not in {'.', colorpiece['Q']}: control[newy][newx].append((y, x, piece)) if self.pos[newy][newx] == colorpiece['P'] and dy == forwardstep: control[newy + dy][newx + dx].append((y, x, piece)) break control[newy][newx].append((y, x, piece)) newy, newx = newy + dy, newx + dx elif piece == colorpiece['Q']: directions = ((1, 0), (0, 1), (-1, 0), (0, -1)) for dy, dx in directions: newy, newx = y + dy, x + dx while 0 <= newx <= 7 and 0 <= newy <= 7: if self.pos[newy][newx] not in {'.', colorpiece['R'], colorpiece['Q']}: control[newy][newx].append((y, x, piece)) break control[newy][newx].append((y, x, piece)) newy, newx = newy + dy, newx + dx directions = ((1, 1), (1, -1), (-1, 1), (-1, -1)) for dy, dx in directions: newy, newx = y + dy, x + dx while 0 <= newx <= 7 and 0 <= newy <= 7: if self.pos[newy][newx] not in {'.', colorpiece['B'], colorpiece['Q']}: control[newy][newx].append((y, x, piece)) if self.pos[newy][newx] == colorpiece['P'] and dy == forwardstep: control[newy + dy][newx + dx].append((y, x, piece)) break control[newy][newx].append((y, x, piece)) newy, newx = newy + dy, newx + dx elif piece == colorpiece['K']: directions = ((1, 1), (1, -1), (-1, 1), (-1, -1), (1, 0), (0, 1), (-1, 0), (0, -1)) for dy, dx in directions: newy, newx = y + dy, x + dx if 0 <= newy <= 7 and 0 <= newx <= 7: control[newy][newx].append((y, x, piece)) elif piece == colorpiece['P']: for dx in (1, -1): newx = x + dx if 0 <= y + forwardstep <= 7 and 0 <= newx <= 7: control[y + forwardstep][newx].append((y, x, piece)) if self.ep_sqr == (y + forwardstep, newx) and self.pos[y][newx] == colorenemypiece['P']: control[y][newx].append((y, x, piece)) return control def legal_moves(self, white: bool) -> tuple: enemycontrol = self.control(False) if white else self.control(True) colorpiece = COLORWHITE if white else COLORBLACK shortcastling, longcastling, arookmoved, kingmoved, hrookmoved = ((self.w_short_castling, self.w_long_castling, self.whitearookmoved, self.whitekingmoved, self.whitehrookmoved) if white else (self.b_short_castling, self.b_long_castling, self.whitearookmoved, self.whitekingmoved, self.whitehrookmoved)) moves = [] for move in self.all_moves(white): if move == ((7, 4, '.'), (7, 5, colorpiece['R']), (7, 6, colorpiece['K']), (7, 7, '.')) and (enemycontrol[7][5] or enemycontrol[7][4] or kingmoved or hrookmoved or not shortcastling): continue elif move == ((7, 0, '.'), (7, 2, colorpiece['K']), (7, 3, colorpiece['R']), (7, 4, '.')) and (enemycontrol[7][3] or enemycontrol[7][4] or kingmoved or arookmoved or not longcastling): continue self.do(move) if any(self.pos[singlemove[0]][singlemove[1]] == colorpiece['K'] for enemymove in self.all_moves(not white) for singlemove in enemymove): self.undo(); continue self.undo(); moves.append(move) return tuple(moves) def _fifty_move_rule(self) -> None: if self.fifty_move_ctr == 100: userinput = input('\n50 moves have been played without capture or pawnpush. Do you want to force a draw? [Y/n] > ') if userinput in {'', 'yes', 'y', 'Y'}: self.render(self.player_color) print('\nDraw due to fifty move rule!\n'); raise SystemExit def _move_rep(self) -> None: if self.pos_hist.count(self.pos_hist[-1]) == 3: self.render(self.player_color) print('\nDRAW DUE TO MOVEREPITITION!\n'); raise SystemExit def check_pos(self) -> None: for white in {True, False}: if not len(self.legal_moves(not white)): self.render(self.player_color) if self._is_checkmate(white): print('\nWHITE WON BY CHECKMATE!\n') if white else print('\nBLACK WON BY CHECKMATE!\n'); raise SystemExit print('\nSTALEMATE!\n'); raise SystemExit self._fifty_move_rule(); self._move_rep() def do(self, move: move_struct) -> None: save = [] for singlemove in move: save.append((self.pos[singlemove[0]][singlemove[1]], singlemove[0], singlemove[1], singlemove[2])) self.pos[singlemove[0]][singlemove[1]] = singlemove[2] self.hist.append(tuple(save)) def undo(self) -> None: lastmove = self.hist.pop() for singlemove in lastmove: self.pos[singlemove[1]][singlemove[2]] = singlemove[0] def make_move(self, move: move_struct, white: bool) -> None: self.asked_draw, self.ep_sqr = False, None colorpiece, forwardstep = (COLORWHITE, -1) if white else (COLORBLACK, 1) if move[1][2] == colorpiece['P'] and move[1][0] == move[0][0] + (2 * forwardstep): # EP square self.ep_sqr = (move[0][0] + forwardstep, move[0][1]) save = [] pawn_move, capture = False, 0 for singlemove in move: save.append((self.pos[singlemove[0]][singlemove[1]], singlemove[0], singlemove[1], singlemove[2])) if singlemove[2] == colorpiece['P']: # checks if pawnmove to reset 50-move-rule pawn_move = True capture = capture + 1 if singlemove[2] == '.' else capture - 1 # capture if capture not 0 if singlemove[0:2] == (7, 0): self.w_long_castling = False elif singlemove[0:2] == (7, 4): self.w_short_castling, self.w_long_castling = False, False elif singlemove[0:2] == (7, 7): self.w_short_castling = False elif singlemove[0:2] == (0, 0): self.b_long_castling = False elif singlemove[0:2] == (0, 4): self.b_short_castling, self.b_long_castling = False, False elif singlemove[0:2] == (0, 7): self.b_short_castling = False self.pos[singlemove[0]][singlemove[1]] = singlemove[2] self.fifty_move_ctr = 0 if pawn_move or capture else self.fifty_move_ctr + 1 self.half_move_ctr += 1 self.hist.append(tuple(save)) self.pos_hist.append(deepcopy(self.pos)) self.check_pos() def _is_check(self, white: bool, move: move_struct = None) -> bool: colorenemypiece = COLORBLACK if white else COLORWHITE if not move: if any(self.pos[singlemove[0]][singlemove[1]] == colorenemypiece['K'] for move in self.all_moves(white) for singlemove in move): return True return False self.do(move) if any(self.pos[singlemove[0]][singlemove[1]] == colorenemypiece['K'] for move in self.all_moves(white) for singlemove in move): self.undo(); return True self.undo(); return False def _is_checkmate(self, white: bool, move: move_struct = None) -> bool: colorenemypiece = COLORBLACK if white else COLORWHITE if not move: if not self.legal_moves(not white): if not any(self.pos[singlemove[0]][singlemove[1]] == colorenemypiece['K'] for move in self.all_moves(white) for singlemove in move): return False return True self.do(move) if not self.legal_moves(not white): if not any(self.pos[singlemove[0]][singlemove[1]] == colorenemypiece['K'] for move in self.all_moves(white) for singlemove in move): self.undo(); return False self.undo(); return True def _is_capture(self, move: move_struct) -> bool: capture = 0 for singlemove in move: capture = capture + 1 if singlemove[2] == '.' else capture - 1 if not capture: return True return False def set_fen(self, fen_str: str) -> bool | None: try: board_str, color_str, castling_str, ep_str, fifty_move_str, full_move_str = fen_str.strip().split() except ValueError: return False tmp_pos = [] board_lst = board_str.split('/') if len(board_lst) != 8: return False for row_str in board_lst: tmp_row = [] for char in row_str: if char == '.' or char in WHITEPIECES or char in BLACKPIECES: tmp_row.append(char) elif char in {'1', '2', '3', '4', '5', '6', '7', '8'}: tmp_row.extend(['.'] * int(char)) else: return False tmp_pos.append(tmp_row) if len(tmp_pos) != 8: return False self.pos = tuple(tmp_pos) self.white = color_str == 'w' if not self.white and color_str != 'b': return False if castling_str != '-' and set(castling_str) <= {'K', 'Q', 'k', 'q'} or sorted(castling_str) != list(castling_str): return False self.w_short_castling = 'K' in castling_str self.w_long_castling = 'Q' in castling_str self.b_short_castling = 'k' in castling_str self.b_long_castling = 'q' in castling_str if ep_str == '-': self.ep_sqr = None elif len(ep_str) == 2 and ep_str[0] in ALPH_TO_INT and ep_str[1] in NUM_TO_INT and NUM_TO_INT[ep_str[1]] in {2, 5}: self.ep_sqr = (NUM_TO_INT[ep_str[1]], ALPH_TO_INT[ep_str[0]]) else: return False if fifty_move_str.isdigit(): self.fifty_move_ctr = int(fifty_move_str) if full_move_str.isdigit(): self.half_move_ctr = int(full_move_str) * 2 + (1 if color_str == 'b' else 0) self.check_pos(); self.start_game(self.white) def get_fen(self, white: bool) -> str: row_strs = [] for row in self.pos: row_str = '' space_ctr = 0 for piece in row: if piece == '.': space_ctr += 1 elif piece in WHITEPIECES or piece in BLACKPIECES: if space_ctr: row_str += str(space_ctr) row_str += piece space_ctr = 0 if space_ctr: row_str += str(space_ctr) row_strs.append(row_str) board_str = '/'.join(row_strs) color_str = 'w' if white else 'b' castling_str = ('K' if self.w_short_castling else '') + ('Q' if self.w_long_castling else '') + ('k' if self.b_short_castling else '') + ('q' if self.b_long_castling else '') castling_str = '-' if not castling_str else castling_str ep_str = '-' if self.ep_sqr == None else INT_TO_ALPH[self.ep_sqr[1]] + INT_TO_NUM[self.ep_sqr[0]] fifty_move_str = str(self.fifty_move_ctr) full_move_str = str(self.half_move_ctr // 2) fen_str = ' '.join([board_str, color_str, castling_str, ep_str, fifty_move_str, full_move_str]) return fen_str def _game_controls(self, userinput: tuple, white: bool) -> bool | None: if userinput in {'/m', '/menu', '?', 'h', 'help', '/h', '/help', '-h', '-help', '--help'}: print((''' /m, /menu > gamecontrols, opens this menu /q, /quit > quit game /s, /save > save game to FEN /t, /turnboard > turn board position /p, /printboard > reprint board /d, /draw > offer draw /r, /resign > resign game /f, /forcedraw > force draw if 50 move rule applies /i, /import > import game from FEN /m, /manual > how to play''')); return True elif userinput in {'/q', '/quit'}: print(f'\nFEN-notation > {self.get_fen(white)}\n'); raise SystemExit elif userinput in {'/s', '/save'}: print('\nFEN-notation >', self.get_fen(white)); return True elif userinput in {'/t', '/turnboard'}: self.board_turned = not self.board_turned self.render(self.player_color); return True elif userinput in {'/p', '/printboard'}: self.render(self.player_color); return True elif userinput in {'/d', '/draw'}: if not self.asked_draw: self.asked_draw = True if (white and self.eval_pos() < 0) or (not white and self.eval_pos() > 0): print('\nDRAW\n'); raise SystemExit elif self.eval_pos() == 0: if choices([True, False], weights = [1, 5], k = 1)[0]: print('\nDRAW\n'); raise SystemExit print('\nNO DRAW'); return True print('\nNO DRAW'); return True print('\nAlready asked for draw this move'); return True elif userinput in {'/r', '/resign'}: print('\nBLACK WON BY RESIGNATION\n') if white else print('\nWHITE WON BY RESIGNATION\n'); raise SystemExit elif userinput in {'/f', '/forcedraw'}: if self.fifty_move_ctr > 99: print('\nWhite forced DRAW\n') if white else print('\nBlack forced DRAW\n'); raise SystemExit print('\n50-move-rule does not apply'); return True elif userinput in {'/i', '/import'}: if not self.set_fen(input('\nPaste FEN-position here > ')): print('!INVALID FEN!'); return True return False def find_piece(self, item: str) -> tuple: for y, row in enumerate(self.pos): for x, piece in enumerate(row): if piece == item: return (y, x) def parse(self, userinput: str, white: bool) -> tuple: colorpiece = COLORWHITE if white else COLORBLACK shortcastling, longcastling, castlingrow = (self.w_short_castling, self.w_long_castling, 7) if white else (self.b_short_castling, self.b_long_castling, 0) if self._game_controls(userinput, white): userinput = input('\nEnter your move in algebraic notation > ') return self.parse(userinput, white) userinput = userinput.strip() check, checkmate, capture, promotion = False, False, False, '' if userinput: for postfix, check_code in chessconf.POSTFIX_MAP.items(): if userinput.endswith(postfix): check, checkmate = (False, True) if check_code else (True, False) userinput = userinput[:-len(postfix)]; break if userinput: if userinput in chessconf.CASTLING_MAP: if chessconf.CASTLING_MAP[userinput]: usermove = ((castlingrow, 4, '.'), (castlingrow, 5, 'R'), (castlingrow, 6, 'K'), (castlingrow, 7, '.')) else: usermove = ((castlingrow, 0, '.'), (castlingrow, 2, 'K'), (castlingrow, 3, 'R'), (castlingrow, 4, '.')) if (parsed := self._validate(usermove, white, check, checkmate, capture, promotion)): return parsed promotion = None for postfix, promotion_code in chessconf.PROMOTION_MAP.items(): if userinput.endswith(postfix): promotion, userinput = colorpiece[promotion_code], userinput[:-len(postfix)]; break if len(userinput) >= 2 and userinput[-2] in ALPH_TO_INT and userinput[-1] in NUM_TO_INT: target, userinput = (NUM_TO_INT[userinput[-1]], ALPH_TO_INT[userinput[-2]]), userinput[:-2] y, x, attackingpiece = None, None, colorpiece['P'] if userinput: abbr_piece = False for i in WHITEPIECES: if userinput.startswith(i): attackingpiece, abbr_piece, userinput = colorpiece[i], True, userinput[1:]; break if not abbr_piece: for prefix, piece_code in chessconf.PIECE_MAP.items(): if userinput.startswith(prefix): attackingpiece, userinput = colorpiece[piece_code], userinput[len(prefix):]; break if userinput: for postfix, action_code in chessconf.ACTION_MAP.items(): if userinput.endswith(postfix): capture, userinput = True if action_code == 'x' else False, userinput[:-len(postfix)]; break if userinput: att_coords = False if userinput[-1] in NUM_TO_INT: y, att_coords, userinput = NUM_TO_INT[userinput[-1]], True, userinput[:-1] if userinput: if userinput[-1] in ALPH_TO_INT: x, att_coords, userinput = ALPH_TO_INT[userinput[-1]], True, userinput[:-1] if y: attackingpiece = self.pos[y][x] if att_coords: if userinput in {' ', ' on '}: userinput = '' if not userinput: matchingattackers = [attacker for attacker in self.reach(white)[target[0]][target[1]] if attacker[2] == attackingpiece] if y != None and x != None: attackers = [attacker for attacker in matchingattackers if attacker[1] == x] elif y != None: attackers = [attacker for attacker in matchingattackers if attacker[0] == y] elif x != None: attackers = [attacker for attacker in matchingattackers if attacker[1] == x] else: attackers = matchingattackers if len(attackers) == 1: attacker = attackers[0] if promotion: if attackingpiece == colorpiece['P']: usermove = ((attacker[0], attacker[1], '.'), (target[0], target[1], promotion)) check = self._validate(usermove, white, check, checkmate, capture, promotion) if check: return check else: if attacker[2] == colorpiece['K'] and shortcastling and target == (castlingrow, 6): usermove = ((castlingrow, 4, '.'), (castlingrow, 5, colorpiece['R']), (castlingrow, 6, colorpiece['K']), (castlingrow, 7, '.')) elif attacker[2] == colorpiece['K'] and longcastling and target == (castlingrow, 2): usermove = ((castlingrow, 0, '.'), (castlingrow, 2, colorpiece['K']), (castlingrow, 3, colorpiece['R']), (castlingrow, 4, '.')) elif attacker[2] == colorpiece['P'] and self.ep_sqr == (target[0], target[1]): usermove = ((attacker[0], attacker[1], '.'), (target[0], target[1], attacker[2]), (attacker[0], target[1], '.')) else: usermove = ((attacker[0], attacker[1], '.'), (target[0], target[1], attacker[2])) if (validation := self._validate(usermove, white, check, checkmate, capture, promotion)): return validation elif len(attackers) > 1: userinput = input('\nPlease specify which piece to move.\n\nEnter your move in algebraic notation > ') return self.parse(userinput, white) print(('\n!INVALID INPUT! Read the manual for more information.')) return self.parse(input('\nEnter your move in algebraic notation > '), white) def _validate(self, usermove: move_struct, white: bool, check: bool = False, checkmate: bool = False, capture: bool = False, promotion: str = '') -> tuple | None: enemycontrol = self.control(not white) colorpiece = COLORWHITE if white else COLORBLACK shortcastling, longcastling = (((7, 4, '.'), (7, 5, 'R'), (7, 6, 'K'), (7, 7, '.')), ((7, 0, '.'), (7, 2, 'K'), (7, 3, 'R'), (7, 4, '.'))) if white else (((0, 4, '.'), (0, 5, 'r'), (0, 6, 'k'), (0, 7, '.')), ((0, 0, '.'), (0, 2, 'k'), (0, 3, 'r'), (0, 4, '.'))) if usermove in self.legal_moves(white): notcheck, notcheckmate, notcapture = check and not self._is_check(white, usermove), checkmate and not self._is_checkmate(white, usermove), capture and not self._is_capture(usermove) if notcheck or notcheckmate or notcapture: if notcheck and notcapture: userinput = input('\nMove is neither a check nor a capture. Do you want to proceed? [Y/n] > ') elif notcheckmate and notcapture: userinput = input('\nMove is neither a checkmate nor a capture. Do you want to proceed? [Y/n] > ') elif notcheck: userinput = input('\nMove is not a check. Do you want to proceed? [Y/n] > ') elif notcheckmate: userinput = input('\nMove is not a checkmate. Do you want to proceed? [Y/n] > ') elif notcapture: userinput = input('\nMove is not a capture. Do you want to proceed? [Y/n] > ') if userinput in {'y', 'yes', '1', '', 'Y'}: return usermove userinput = input('\nEnter your move in algebraic notation > ') return self.parse(userinput, white) return usermove if usermove in self.all_moves(white): if any(singlemove[2] == colorpiece['K'] for singlemove in usermove): if usermove in (shortcastling, longcastling) and enemycontrol[usermove[0][0]][4]: print(enemycontrol[usermove[0][0]][4]) print('\nKing cannot castle if in check.') elif (usermove == shortcastling and not enemycontrol[usermove[0][0]][6]) or (usermove == longcastling and not enemycontrol[usermove[0][0]][2]): print('\nKing cannot castle through enemy controlled square.') else: print('\nSquare controlled by enemy.') else: y, x = self.find_piece(colorpiece['K']) if enemycontrol[y][x]: print('\nKing is in check.') else: print('\nPiece is pinned to king.') userinput = input('\nEnter your move in algebraic notation > ') return self.parse(userinput, white) def eval_pos(self) -> float: value = 0 for y, row in enumerate(self.pos): for x, piece in enumerate(row): if piece != '.': value += chessconf.PIECEVALUE[piece] + chessconf.POSTABLES[piece][y][x] return value def minimax(self, depth: int, ismax: bool, alpha: float | None = -float('inf'), beta: float | None = float('inf')) -> float: if depth == 0: return self.eval_pos() if ismax: bestvalue = float('-inf') for move in self.all_moves(True): self.do(move) minimaxvalue = self.minimax(depth - 1, not ismax, alpha, beta) self.undo() if minimaxvalue > bestvalue: bestvalue = minimaxvalue alpha = max(alpha, bestvalue) if alpha >= beta: break return bestvalue else: bestvalue = float('inf') for move in self.all_moves(False): self.do(move) minimaxvalue = self.minimax(depth - 1, not ismax, alpha, beta) self.undo() if minimaxvalue < bestvalue: bestvalue = minimaxvalue beta = min(beta, bestvalue) if alpha >= beta: break return bestvalue def best_moves(self, white: bool, depth: int | None = chessconf.minimax_depth) -> tuple: if white: bestvalue, bestmovelist = float('-inf'), [] for move in self.legal_moves(True): self.do(move) minimaxvalue = self.minimax(depth - 1, not white) self.undo() if minimaxvalue == bestvalue: bestmovelist.append(move) elif minimaxvalue > bestvalue: bestvalue, bestmovelist = minimaxvalue, [move] legalbestmoves = [move for move in bestmovelist if move in self.legal_moves(True)] return tuple(legalbestmoves) else: bestvalue, bestmovelist = float('inf'), [] for move in self.legal_moves(False): self.do(move) minimaxvalue = self.minimax(depth - 1, not white) self.undo() if minimaxvalue == bestvalue: bestmovelist.append(move) elif minimaxvalue < bestvalue: bestvalue, bestmovelist = minimaxvalue, [move] legalbestmoves = [move for move in bestmovelist if move in self.legal_moves(False)] return tuple(legalbestmoves) def stockfish_move(self, white: bool, depth: int | None = chessconf.stockfish_depth) -> str | None: stockfish = Popen(chessconf.STOCKFISH_PATH, stdin = PIPE, stdout = PIPE, text = True) stockfish.stdin.write(f'position fen {self.get_fen(white)}\ngo depth {depth}\n'); stockfish.stdin.flush() while True: output = stockfish.stdout.readline().strip() if 'bestmove' in output: move = output.split(' ')[1]; stockfish.stdin.close(); stockfish.terminate() return None if move == '(none)' else move def start_game(self, white: bool | None = None) -> None: while True: userinput = input('\nDo you want to play against stockfish or own minimax algorithm? [M/s] > ') if userinput in {'', 'm', 'minimax', '1', 'M', 'Minimax', 'MINIMAX'}: while True: userinput = input('\nChoose a difficulty level from 1 (easy) to 5 (hard) > ') if userinput.isdigit() and int(userinput) in range(1, 6): def enemymove(): return choice(self.best_moves(not self.player_color, int(userinput))) if not self._game_controls(userinput, self.player_color): def enemymove(): return choice(self.best_moves(not self.player_color)) break elif userinput in {'s', 'stockfish', '2', 'S', 'Stockfish', 'STOCKFISH'}: while True: userinput = input('\nChoose a difficulty level from 1 (easy) to 10 (hard) > ') if userinput.isdigit() and int(userinput) in range(1, 11): def enemymove(): return self.parse(self.stockfish_move(not self.player_color, int(userinput)), not self.player_color) if not self._game_controls(userinput, self.player_color): def enemymove(): return self.parse(self.stockfish_move(not self.player_color), not self.player_color) break if not self._game_controls(userinput, white): break if white == None: while True: colorchoice = input('\nWould you like to play with white or black? Press ENTER for random color. [w/b] > ') if colorchoice in {'', 'r', 'random', 'R', 'Random', 'RANDOM'}: randchoice = choice([True, False]) self.player_color, self.white = randchoice, randchoice elif colorchoice in {'w', 'white', '1', 'W', 'White', 'WHITE'}: self.player_color, self.white = True, True elif colorchoice in {'b', 'black', '2', 'B', 'Black', 'BLACK'}: self.player_color, self.white = False, False if not self._game_controls(colorchoice, white): break if self.white: self.render(self.player_color) userinput = input('\nEnter your move in algebraic notation > ') usermove = self.parse(userinput, self.player_color) self.make_move(usermove, self.player_color) while True: self.make_move(enemymove(), not self.player_color) self.render(self.player_color) userinput = input('\nEnter your move in algebraic notation > ') usermove = self.parse(userinput, self.player_color) self.make_move(usermove, self.player_color) if __name__ == '__main__': board: Board = Board() render_hex(invert(board.bb())) render_hex(board.bb()) board.start_game()