summaryrefslogtreecommitdiff
path: root/chess.py
diff options
context:
space:
mode:
Diffstat (limited to 'chess.py')
-rw-r--r--chess.py1033
1 files changed, 1033 insertions, 0 deletions
diff --git a/chess.py b/chess.py
new file mode 100644
index 0000000..64ba1af
--- /dev/null
+++ b/chess.py
@@ -0,0 +1,1033 @@
+import chessconf
+import random
+import copy
+import subprocess
+import typing
+from collections.abc import Generator
+
+mv_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: typing.Final[dict[str, str]] = {
+ '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: typing.Final[dict[int, str]] = {0: 'a', 1: 'b', 2: 'c', 3: 'd', 4: 'e', 5: 'f', 6: 'g', 7: 'h'}
+ALPH_TO_INT: typing.Final[dict[str, int]] = {'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4, 'f': 5, 'g': 6, 'h': 7}
+INT_TO_NUM: typing.Final[dict[int, str]] = {0: '8', 1: '7', 2: '6', 3: '5', 4: '4', 5: '3', 6: '2', 7: '1'}
+NUM_TO_INT: typing.Final[dict[str, int]] = {'8': 0, '7': 1, '6': 2, '5': 3, '4': 4, '3': 5, '2': 6, '1': 7}
+
+COLOR_WHITE: typing.Final[dict[str, str]] = {'R': 'R', 'N': 'N', 'B': 'B', 'K': 'K', 'Q': 'Q', 'P': 'P', 'r': 'R', 'n': 'N', 'b': 'B', 'k': 'K', 'q': 'Q', 'p': 'P'}
+COLOR_BLACK: typing.Final[dict[str, str]] = {'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: typing.Final[set[str]] = {'R', 'N', 'B', 'Q', 'K', 'P'}
+BLACKPIECES: typing.Final[set[str]] = {'r', 'n', 'b', 'q', 'k', 'p'}
+
+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.white: bool = white
+ self.bot: bool | None = None
+
+ self.end_game: bool = False
+ self.last_move_capture: bool = False
+
+ self.log: list = []
+ self.pos_log: list = [copy.deepcopy(self.pos)]
+
+ self.half_mv_ctr: int = 0
+ self.fifty_mv_ctr: int = 0
+
+ self.ep_sqr: tuple | None = None
+
+ self.board_turned: bool = False
+ self.asked_draw: bool = False
+
+ self.w_short_castling: bool = True
+ self.w_long_castling: bool = True
+ self.b_short_castling: bool = True
+ self.b_long_castling: bool = True
+
+ 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_mvs(self, y: int, x: int, white: bool) -> Generator[mv_struct]:
+ enemypieces = BLACKPIECES if white else WHITEPIECES
+ rook = 'R' if white else 'r'
+
+ directions = ((1, 0), (0, 1), (-1, 0), (0, -1))
+ for dy, dx in directions:
+ newy, newx = y + dy, x + dx
+ while newy in range(8) and newx in range(8):
+ if self.pos[newy][newx] == '.':
+ yield ((y, x, '.'), (newy, newx, rook))
+ elif self.pos[newy][newx] in enemypieces:
+ yield ((y, x, '.'), (newy, newx, rook))
+ break
+ else:
+ break
+ newy, newx = newy + dy, newx + dx
+
+ def _knight_mvs(self, y: int, x: int, white: bool) -> Generator[mv_struct]:
+ ownpieces, knight = (WHITEPIECES, 'N') if white else (BLACKPIECES, '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 newy in range(8) and newx in range(8) and self.pos[newy][newx] not in ownpieces:
+ yield ((y, x, '.'), (newy, newx, knight))
+
+ def _bishop_mvs(self, y: int, x: int, white: bool) -> Generator[mv_struct]:
+ enemypieces, bishop = (BLACKPIECES, 'B') if white else (WHITEPIECES, 'b')
+
+ directions = ((1, 1), (1, -1), (-1, 1), (-1, -1))
+ for dy, dx in directions:
+ newy, newx = y + dy, x + dx
+ while newx in range(8) and newy in range(8):
+ piece = self.pos[newy][newx]
+ if piece == '.':
+ yield ((y, x, '.'), (newy, newx, bishop))
+ elif piece in enemypieces:
+ yield ((y, x, '.'), (newy, newx, bishop))
+ break
+ else:
+ break
+ newy, newx = newy + dy, newx + dx
+
+ def _queen_mvs(self, y: int, x: int, white: bool) -> Generator[mv_struct]:
+ enemypieces, queen = (BLACKPIECES, 'Q') if white else (WHITEPIECES, '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 newx in range(8) and newy in range(8):
+ piece = self.pos[newy][newx]
+ if piece == '.':
+ yield ((y, x, '.'), (newy, newx, queen))
+ elif piece in enemypieces:
+ yield ((y, x, '.'), (newy, newx, queen))
+ break
+ else:
+ break
+ newy, newx = newy + dy, newx + dx
+
+ def _king_mvs(self, y: int, x: int, white: bool) -> Generator[mv_struct]:
+ ownpieces, king = (WHITEPIECES, 'K') if white else (BLACKPIECES, '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 newy in range(8) and newx in range(8) and self.pos[newy][newx] not in ownpieces:
+ yield ((y, x, '.'), (newy, newx, king))
+
+ def _castling_mvs(self, white: bool) -> Generator[mv_struct]:
+ short_castling, long_castling, color_piece, castling_row = (self.w_short_castling, self.w_long_castling, COLOR_WHITE, 7) if white else (self.b_short_castling, self.b_long_castling, COLOR_BLACK, 0)
+
+ if short_castling and self.pos[castling_row][5] == '.' and self.pos[castling_row][6] == '.':
+ yield ((castling_row, 4, '.'), (castling_row, 5, color_piece['R']), (castling_row, 6, color_piece['K']), (castling_row, 7, '.'))
+ if long_castling and self.pos[castling_row][1] == '.' and self.pos[castling_row][2] == '.' and self.pos[castling_row][3] == '.':
+ yield ((castling_row, 0, '.'), (castling_row, 2, color_piece['K']), (castling_row, 3, color_piece['R']), (castling_row, 4, '.'))
+
+ def _pawn_mvs(self, y: int, x: int, white: bool) -> Generator[mv_struct]:
+
+ startingrow = 6 if white else 1
+ forwardstep = -1 if white else 1
+ promotionrow = 1 if white else 6
+
+ color_piece = COLOR_WHITE if white else COLOR_BLACK
+ enemypieces = BLACKPIECES if white else WHITEPIECES
+
+ newy = y + forwardstep
+
+ if newy in range(8) and self.pos[newy][x] == '.':
+ if y == promotionrow:
+ for promotion in {color_piece['Q'], color_piece['R'], color_piece['N'], color_piece['B']}:
+ yield ((y, x, '.'), (newy, x, promotion))
+ else:
+ yield ((y, x, '.'), (newy, x, color_piece['P']))
+ if y == startingrow and self.pos[newy + forwardstep][x] == '.':
+ yield ((y, x, '.'), (newy + forwardstep, x, color_piece['P']))
+ for dx in (1, -1):
+ newx = x + dx
+ if newy in range(8) and newx in range(8) and self.pos[newy][newx] in enemypieces:
+ if y == promotionrow:
+ for i in (color_piece['Q'], color_piece['R'], color_piece['N'], color_piece['B']):
+ yield ((y, x, '.'), (newy, newx, i))
+ else: # taking enemy piece
+ yield ((y, x, '.'), (newy, newx, color_piece['P']))
+ if [newy, newx] == self.ep_sqr: # en passant
+ yield ((y, x, '.'), (newy, newx, color_piece['P']), (y, newx, '.'))
+
+ def all_mvs(self, white: bool) -> Generator[mv_struct]:
+ color_piece = COLOR_WHITE if white else COLOR_BLACK
+ mv_generators = {color_piece['P']: self._pawn_mvs, color_piece['R']: self._rook_mvs, color_piece['N']: self._knight_mvs, color_piece['B']: self._bishop_mvs, color_piece['Q']: self._queen_mvs, color_piece['K']: self._king_mvs}
+ for y, row in enumerate(self.pos):
+ for x, piece in enumerate(row):
+ if generator := mv_generators.get(piece):
+ yield from generator(y, x, white)
+ yield from self._castling_mvs(white)
+
+ def reach(self, white: bool) -> tuple:
+ reach = (([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []))
+ color_piece, forwardstep, startingrow, enemypieces = (COLOR_WHITE, -1, 6, BLACKPIECES) if white else (COLOR_BLACK, 1, 1, WHITEPIECES)
+ short_castling, long_castling, castling_row = (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 == color_piece['R']:
+ directions = ((1, 0), (0, 1), (-1, 0), (0, -1))
+ for dy, dx in directions:
+ newy, newx = y + dy, x + dx
+ while newy in range(8) and newx in range(8):
+ 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 == color_piece['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 newy in range(8) and newx in range(8):
+ reach[newy][newx].append((y, x, piece))
+
+ elif piece == color_piece['B']:
+ directions = ((1, 1), (1, -1), (-1, 1), (-1, -1))
+ for dy, dx in directions:
+ newy, newx = y + dy, x + dx
+ while newx in range(8) and newy in range(8):
+ 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 == color_piece['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 newx in range(8) and newy in range(8):
+ 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 == color_piece['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 newy in range(8) and newx in range(8):
+ reach[newy][newx].append((y, x, piece))
+
+ if short_castling and self.pos[castling_row][5] == '.' and self.pos[castling_row][6] == '.':
+ reach[castling_row][6].append((castling_row, 4, piece))
+ if long_castling and self.pos[castling_row][2] == '.' and self.pos[castling_row][3] == '.':
+ reach[castling_row][2].append((castling_row, 4, piece))
+
+ elif piece == color_piece['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 y + forwardstep in range(8) and newx in range(8) 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 = (([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []), ([], [], [], [], [], [], [], []))
+ color_piece, colorenemypiece, forwardstep = (COLOR_WHITE, COLOR_BLACK, -1) if white else (COLOR_BLACK, COLOR_WHITE, 1)
+ for y, row in enumerate(self.pos):
+ for x, piece in enumerate(row):
+
+ if piece == color_piece['R']:
+ directions = ((1, 0), (0, 1), (-1, 0), (0, -1))
+ for dy, dx in directions:
+ newy, newx = y + dy, x + dx
+ while newy in range(8) and newx in range(8):
+ if self.pos[newy][newx] not in {'.', color_piece['R'], color_piece['Q']}:
+ control[newy][newx].append((y, x, piece))
+ break
+ control[newy][newx].append((y, x, piece))
+ newy, newx = newy + dy, newx + dx
+
+ elif piece == color_piece['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 newy in range(8) and newx in range(8):
+ control[newy][newx].append((y, x, piece))
+
+ elif piece == color_piece['B']:
+ directions = ((1, 1), (1, -1), (-1, 1), (-1, -1))
+ for dy, dx in directions:
+ newy, newx = y + dy, x + dx
+ while newx in range(8) and newy in range(8):
+ if self.pos[newy][newx] not in {'.', color_piece['Q']}:
+ control[newy][newx].append((y, x, piece))
+ if self.pos[newy][newx] == color_piece['P'] and dy == forwardstep:
+ control[newy][newx].append((y, x, piece))
+ break
+ control[newy][newx].append((y, x, piece))
+ newy, newx = newy + dy, newx + dx
+
+ elif piece == color_piece['Q']:
+ directions = ((1, 0), (0, 1), (-1, 0), (0, -1))
+ for dy, dx in directions:
+ newy, newx = y + dy, x + dx
+ while newx in range(8) and newy in range(8):
+ if self.pos[newy][newx] not in {'.', color_piece['R'], color_piece['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 newx in range(8) and newy in range(8):
+ if self.pos[newy][newx] not in {'.', color_piece['B'], color_piece['Q']}:
+ control[newy][newx].append((y, x, piece))
+ if self.pos[newy][newx] == color_piece['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 == color_piece['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 newy in range(8) and newx in range(8):
+ control[newy][newx].append((y, x, piece))
+
+ elif piece == color_piece['P']:
+ for dx in (1, -1):
+ newx = x + dx
+ if y + forwardstep in range(8) and newx in range(8):
+ 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_mvs(self, white: bool) -> Generator[mv_struct]:
+ enemycontrol = self.control(not white)
+ color_piece = COLOR_WHITE if white else COLOR_BLACK
+ short_castling, long_castling = ((self.w_short_castling, self.w_long_castling) if white else (self.b_short_castling, self.b_long_castling))
+
+ for mv in self.all_mvs(white):
+ if mv == ((7, 4, '.'), (7, 5, color_piece['R']), (7, 6, color_piece['K']), (7, 7, '.')) and (enemycontrol[7][5] or enemycontrol[7][4] or not short_castling):
+ continue
+ elif mv == ((7, 0, '.'), (7, 2, color_piece['K']), (7, 3, color_piece['R']), (7, 4, '.')) and (enemycontrol[7][3] or enemycontrol[7][4] or not long_castling):
+ continue
+ self.do(mv)
+ if any(self.pos[single_mv[0]][single_mv[1]] == color_piece['K'] for enemy_mv in self.all_mvs(not white) for single_mv in enemy_mv):
+ self.undo()
+ continue
+ self.undo()
+ yield mv
+
+ def _fifty_mv_rule(self) -> None:
+ if self.fifty_mv_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.white if self.bot is not None else not self.bot)
+ print('\nDraw due to fifty move rule!\n')
+ raise SystemExit
+
+ def _mv_rep(self) -> None:
+ if self.pos_log.count(self.pos_log[-1]) == 3:
+ self.render(self.white if self.bot is not None else not self.bot)
+ print('\nDRAW DUE TO MOVE REPITITION!\n')
+ raise SystemExit
+
+ def check_pos(self) -> None:
+ for white in {True, False}:
+ if not [*self.legal_mvs(not white)]:
+ self.render(self.white if self.bot is not None else not self.bot)
+ 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_mv_rule(); self._mv_rep()
+
+ def do(self, mv: mv_struct) -> None:
+ save = []
+ for single_mv in mv:
+ save.append((self.pos[single_mv[0]][single_mv[1]], single_mv[0], single_mv[1], single_mv[2]))
+ self.pos[single_mv[0]][single_mv[1]] = single_mv[2]
+ self.log.append(tuple(save))
+
+ def undo(self) -> None:
+ lastmv = self.log.pop()
+ for single_mv in lastmv:
+ self.pos[single_mv[1]][single_mv[2]] = single_mv[0]
+
+ def make_mv(self, mv: mv_struct, white: bool) -> None:
+ self.asked_draw, self.ep_sqr = False, None
+ color_piece, forwardstep = (COLOR_WHITE, -1) if white else (COLOR_BLACK, 1)
+ self.last_move_capture = self._is_capture(mv)
+
+ if mv[1][2] == color_piece['P'] and mv[1][0] == mv[0][0] + (2 * forwardstep): # EP square
+ self.ep_sqr = (mv[0][0] + forwardstep, mv[0][1])
+
+ save = []
+ pawn_mv = False
+
+ for single_mv in mv:
+ save.append((self.pos[single_mv[0]][single_mv[1]], single_mv[0], single_mv[1], single_mv[2]))
+ if single_mv[2] == color_piece['P']: # checks if pawnmove to reset 50-mv-rule
+ pawn_mv = True
+
+ match single_mv[:2]:
+ case (7, 0):
+ self.w_long_castling = False
+ case (7, 4):
+ self.w_short_castling, self.w_long_castling = False, False
+ case (7, 7):
+ self.w_short_castling = False
+ case (0, 0):
+ self.b_long_castling = False
+ case (0, 4):
+ self.b_short_castling, self.b_long_castling = False, False
+ case (0, 7):
+ self.b_short_castling = False
+
+ self.pos[single_mv[0]][single_mv[1]] = single_mv[2]
+
+ self.fifty_mv_ctr = 0 if pawn_mv or self.last_move_capture else self.fifty_mv_ctr + 1
+ self.half_mv_ctr += 1
+ self.log.append(tuple(save))
+ self.pos_log.append(copy.deepcopy(self.pos))
+ self.end_game = self.is_end_game()
+ self.check_pos()
+
+ def _is_check(self, white: bool, mv: mv_struct | None = None) -> bool:
+ colorenemypiece = COLOR_BLACK if white else COLOR_WHITE
+ if not mv:
+ if any(self.pos[single_mv[0]][single_mv[1]] == colorenemypiece['K'] for mv in self.all_mvs(white) for single_mv in mv):
+ return True
+ return False
+ self.do(mv)
+ if any(self.pos[single_mv[0]][single_mv[1]] == colorenemypiece['K'] for mv in self.all_mvs(white) for single_mv in mv):
+ self.undo()
+ return True
+ self.undo()
+ return False
+
+ def _is_checkmate(self, white: bool, mv: mv_struct | None = None) -> bool:
+ colorenemypiece = COLOR_BLACK if white else COLOR_WHITE
+ if not mv:
+ if not [*self.legal_mvs(not white)]:
+ if not any(self.pos[single_mv[0]][single_mv[1]] == colorenemypiece['K'] for mv in self.all_mvs(white) for single_mv in mv):
+ return False
+ return True
+ self.do(mv)
+ if not [*self.legal_mvs(not white)]:
+ if not any(self.pos[single_mv[0]][single_mv[1]] == colorenemypiece['K'] for mv in self.all_mvs(white) for single_mv in mv):
+ self.undo()
+ return False
+ self.undo()
+ return True
+
+ def _is_capture(self, mv: mv_struct) -> bool:
+ capture = 0
+ for single_mv in mv:
+ capture += (1 if single_mv[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_mv_str, full_mv_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 | 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_mv_str.isdigit():
+ self.fifty_mv_ctr = int(fifty_mv_str)
+
+ if full_mv_str.isdigit():
+ self.half_mv_ctr = int(full_mv_str) * 2 + (1 if color_str == 'b' else 0)
+
+ self.check_pos(); return True
+
+ 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_mv_str = str(self.fifty_mv_ctr)
+
+ full_mv_str = str(self.half_mv_ctr // 2)
+
+ fen_str = ' '.join([board_str, color_str, castling_str, ep_str, fifty_mv_str, full_mv_str])
+ return fen_str
+
+ def _game_controls(self, userinput: tuple, white: bool = None) -> 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
+
+Read the manual for more information'''))
+ 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.white if self.bot is not None else not self.bot)
+ return True
+
+ elif userinput in {'/p', '/printboard'}:
+ self.render(self.white if self.bot is not None else not self.bot)
+ 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 random.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 mv')
+ 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_mv_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:
+ color_piece = COLOR_WHITE if white else COLOR_BLACK
+ short_castling, long_castling, castling_row = (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_or_checkmate = ''
+ capture = False
+ promotion = ''
+
+ if userinput:
+ for check_or_checkmate_option, check_code in chessconf.CHECK_OR_CHECKMATE_MAP.items():
+ if userinput.endswith(check_or_checkmate_option):
+ check_or_checkmate = check_code
+ userinput = userinput[:-len(check_or_checkmate_option)]
+ break
+
+ if userinput:
+ if userinput in chessconf.CASTLING_MAP:
+ if chessconf.CASTLING_MAP[userinput]:
+ user_mv = ((castling_row, 4, '.'), (castling_row, 5, 'R'), (castling_row, 6, 'K'), (castling_row, 7, '.'))
+ else:
+ user_mv = ((castling_row, 0, '.'), (castling_row, 2, 'K'), (castling_row, 3, 'R'), (castling_row, 4, '.'))
+ if (parsed := self._validate(user_mv, white, check_or_checkmate, capture, promotion)):
+ return parsed
+
+ promotion = None
+
+ for promotion_option, promotion_code in chessconf.PROMOTION_MAP.items():
+ if userinput.endswith(promotion_option):
+ promotion = color_piece[promotion_code]
+ userinput = userinput[:-len(promotion_option)]
+ break
+
+ if len(userinput) >= 2 and userinput[-2] in ALPH_TO_INT and userinput[-1] in NUM_TO_INT:
+ to_y = NUM_TO_INT[userinput[-1]]
+ to_x = ALPH_TO_INT[userinput[-2]]
+ userinput = userinput[:-2]
+
+ from_y = None
+ from_x = None
+ from_piece = color_piece['P']
+
+ if userinput:
+ abbr_piece = False
+ for abbr in WHITEPIECES: # need capitalized piece abbreviations
+ if userinput.startswith(abbr):
+ from_piece = color_piece[abbr]
+ abbr_piece = True
+ userinput = userinput[1:]
+ break
+
+ if not abbr_piece:
+ for piece_option, piece_code in chessconf.PIECE_MAP.items():
+ if userinput.startswith(piece_option):
+ from_piece = color_piece[piece_code]
+ userinput = userinput[len(piece_option):]
+ break
+
+ if userinput:
+ for action_option, action_code in chessconf.ACTION_MAP.items():
+ if userinput.endswith(action_option):
+ capture = True if action_code == 'x' else False
+ userinput = userinput[:-len(action_option)]
+ break
+
+ if userinput:
+ from_coords = False
+ if userinput[-1] in NUM_TO_INT:
+ from_y = NUM_TO_INT[userinput[-1]]
+ from_coords = True
+ userinput = userinput[:-1]
+
+ if userinput:
+ if userinput[-1] in ALPH_TO_INT:
+ from_x = ALPH_TO_INT[userinput[-1]]
+ from_coords = True
+ userinput = userinput[:-1]
+ if from_y is not None:
+ from_piece = self.pos[from_y][from_x]
+
+ if from_coords:
+ if userinput in {' ', ' on '}:
+ userinput = ''
+
+ if not userinput:
+ matchingattackers = [attacker for attacker in self.reach(white)[to_y][to_x] if attacker[2] == from_piece]
+ if from_y is not None and from_x is not None:
+ attackers = [attacker for attacker in matchingattackers if attacker[0] == from_y and attacker[1] == from_x]
+ elif from_y is not None:
+ attackers = [attacker for attacker in matchingattackers if attacker[0] == from_y]
+ elif from_x is not None:
+ attackers = [attacker for attacker in matchingattackers if attacker[1] == from_x]
+ else:
+ attackers = matchingattackers
+
+ if len(attackers) == 1:
+ from_y = attackers[0][0]
+ from_x = attackers[0][1]
+
+ if promotion:
+ if from_piece == color_piece['P']:
+ user_mv = ((from_y, from_x, '.'), (to_y, to_x, promotion))
+ validation = self._validate(user_mv, white, check_or_checkmate, capture, promotion)
+ if validation:
+ return validation
+ else:
+ if from_piece == color_piece['K'] and short_castling and to_y == castling_row and to_x == 6:
+ user_mv = ((castling_row, 4, '.'), (castling_row, 5, color_piece['R']), (castling_row, 6, color_piece['K']), (castling_row, 7, '.'))
+ elif from_piece == color_piece['K'] and long_castling and to_y == castling_row and to_x == 2:
+ user_mv = ((castling_row, 0, '.'), (castling_row, 2, color_piece['K']), (castling_row, 3, color_piece['R']), (castling_row, 4, '.'))
+ elif from_piece == color_piece['P'] and self.ep_sqr == (to_y, to_x):
+ user_mv = ((from_y, from_x, '.'), (to_y, to_x, from_piece), (from_y, to_x, '.'))
+ else:
+ user_mv = ((from_y, from_x, '.'), (to_y, to_x, from_piece))
+ if validation := self._validate(user_mv, white, check_or_checkmate, capture, promotion):
+ return validation
+ elif len(attackers) > 1:
+ userinput = input('''
+Please specify which piece to move.
+
+Enter 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, user_mv: mv_struct, white: bool, check_or_checkmate: str = '', capture: bool = False, promotion: str = '') -> tuple | None:
+ enemycontrol = self.control(not white)
+ color_piece = COLOR_WHITE if white else COLOR_BLACK
+ short_castling = ((7, 4, '.'), (7, 5, 'R'), (7, 6, 'K'), (7, 7, '.')) if white else ((0, 4, '.'), (0, 5, 'r'), (0, 6, 'k'), (0, 7, '.'))
+ long_castling = ((7, 0, '.'), (7, 2, 'K'), (7, 3, 'R'), (7, 4, '.')) if white else ((0, 0, '.'), (0, 2, 'k'), (0, 3, 'r'), (0, 4, '.'))
+
+ if user_mv in self.legal_mvs(white):
+ notcheck = check_or_checkmate == '+' and not self._is_check(white, user_mv)
+ notcheckmate = check_or_checkmate == '#' and not self._is_checkmate(white, user_mv)
+ notcapture = capture and not self._is_capture(user_mv)
+
+ 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 user_mv
+ userinput = input('\nEnter your move in algebraic notation > ')
+ return self.parse(userinput, white)
+ return user_mv
+
+ if user_mv in self.all_mvs(white):
+ if any(single_mv[2] == color_piece['K'] for single_mv in user_mv):
+ if user_mv in (short_castling, long_castling) and enemycontrol[user_mv[0][0]][4]:
+ print(enemycontrol[user_mv[0][0]][4])
+ print('\nKing cannot castle if in check.')
+ elif (user_mv == short_castling and not enemycontrol[user_mv[0][0]][6]) or (user_mv == long_castling and not enemycontrol[user_mv[0][0]][2]):
+ print('\nKing cannot castle through enemy controlled square.')
+ else:
+ print('\nSquare controlled by enemy.')
+ else:
+ y, x = self.find_piece(color_piece['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 is_end_game(self) -> bool:
+ white_queen_ctr = 0
+ white_minor_pieces_ctr = 0
+ black_queen_ctr = 0
+ black_minor_pieces_ctr = 0
+ other_pieces = False
+ for row in self.pos:
+ for piece in row:
+ if piece in {'N', 'B'}:
+ white_minor_pieces_ctr += 1
+ if piece == 'Q':
+ white_queen_ctr += 1
+ else:
+ other_pieces = True
+ if piece in {'n', 'b'}:
+ black_minor_pieces_ctr += 1
+ if piece == 'q':
+ black_queen_ctr += 1
+ else:
+ other_pieces = True
+
+ if not white_queen_ctr and not black_queen_ctr:
+ return True
+ if white_queen_ctr == 1 and black_queen_ctr == 1:
+ if white_minor_pieces_ctr <= 1 and black_minor_pieces_ctr <= 1 and not other_pieces:
+ return True
+ return False
+
+ def eval_pos(self) -> float:
+ value = 0
+ for y, row in enumerate(self.pos):
+ for x, piece in enumerate(row):
+ if piece != '.':
+ if piece in {'K', 'k'}:
+ if self.end_game:
+ value += chessconf.PIECEVALUE[piece] + chessconf.POSTABLES[piece + '_end'][y][x]
+ else:
+ value += chessconf.PIECEVALUE[piece] + chessconf.POSTABLES[piece + '_middle'][y][x]
+ else:
+ 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 and not self.last_move_capture:
+ return self.eval_pos()
+
+ if ismax:
+ bestvalue = float('-inf')
+ for mv in self.all_mvs(True):
+ self.do(mv)
+ minimaxvalue = self.minimax(depth - 1, not ismax, alpha, beta)
+ self.undo()
+ if minimaxvalue > bestvalue:
+ bestvalue = minimaxvalue
+ if bestvalue > alpha:
+ alpha = bestvalue
+ if alpha >= beta:
+ break
+ return bestvalue
+ else:
+ bestvalue = float('inf')
+ for mv in self.all_mvs(False):
+ self.do(mv)
+ minimaxvalue = self.minimax(depth - 1, not ismax, alpha, beta)
+ self.undo()
+ if minimaxvalue < bestvalue:
+ bestvalue = minimaxvalue
+ if bestvalue < beta:
+ beta = bestvalue
+ if alpha >= beta:
+ break
+ return bestvalue
+
+ def best_mvs(self, white: bool, depth: int = chessconf.minimax_depth) -> tuple:
+ if white:
+ bestvalue, best_mv_list = float('-inf'), []
+ for mv in self.legal_mvs(True):
+ self.do(mv)
+ minimaxvalue = self.minimax(depth - 1, not white)
+ self.undo()
+ if minimaxvalue == bestvalue:
+ best_mv_list.append(mv)
+ elif minimaxvalue > bestvalue:
+ bestvalue, best_mv_list = minimaxvalue, [mv]
+ legalbest_mvs = [mv for mv in best_mv_list if mv in self.legal_mvs(True)]
+ return tuple(legalbest_mvs)
+ else:
+ bestvalue, best_mv_list = float('inf'), []
+ for mv in self.legal_mvs(False):
+ self.do(mv)
+ minimaxvalue = self.minimax(depth - 1, not white)
+ self.undo()
+ if minimaxvalue == bestvalue:
+ best_mv_list.append(mv)
+ elif minimaxvalue < bestvalue:
+ bestvalue, best_mv_list = minimaxvalue, [mv]
+ legalbest_mvs = [mv for mv in best_mv_list if mv in self.legal_mvs(False)]
+ return tuple(legalbest_mvs)
+
+ def stockfish_mv(self, white: bool, depth: int = chessconf.stockfish_depth) -> str | None:
+ stockfish = subprocess.Popen(chessconf.STOCKFISH_PATH, stdin=subprocess.PIPE, stdout=subprocess.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:
+ mv = output.split(' ')[1]
+ stockfish.stdin.close()
+ stockfish.terminate()
+ return None if mv == '(none)' else mv
+
+ def select_game(self) -> typing.Callable | typing.Callable | typing.Callable | False:
+ i = 0
+ while i < 3:
+ userinput = input('''
+(1) > play against minimax
+(2) > play against stockfish
+(3) > pass the board to a friend
+
+Choose a game mode > ''')
+ if userinput in {'1', 'one', '(1)', 'ONE', '', 'm', 'minimax', 'M', 'Minimax', 'MINIMAX'}:
+ return self.play_minimax
+ elif userinput in {'2', 'two', '(2)', 'TWO', 's', 'stockfish', 'S', 'Stockfish', 'STOCKFISH'}:
+ return self.play_stockfish
+ elif userinput in {'3', 'three', '(3)', 'THREE', 'p', 'pass', 'P', 'Pass', 'PASS', 'f', 'friend', 'F', 'FRIEND'}:
+ return self.pass_and_play
+ if not self._game_controls(userinput):
+ i += 1
+ return False
+
+ def select_color(self) -> bool:
+ i = 0
+ while i < 3:
+ userinput = input('''
+(1) > play white
+(2) > play black
+(3) > play random color
+
+Choose a color > ''')
+ if userinput in {'1', 'one', '(1)', 'ONE', 'w', 'white', 'W', 'White', 'WHITE'}:
+ return True
+ elif userinput in {'2', 'two', '(2)', 'TWO', 'b', 'black', 'B', 'Black', 'BLACK'}:
+ return False
+ elif userinput in {'3', 'three', '(3)', 'THREE', '', 'r', 'random', 'R', 'Random', 'RANDOM'}:
+ return random.choice(True, False)
+ if not self._game_controls(userinput):
+ i += 1
+ return random.choice(True, False)
+
+ def pass_and_play(self, white: bool = True) -> None:
+ while True:
+ self.render(white)
+ userinput = input('\nEnter your move in algebraic notation > ')
+ user_mv = self.parse(userinput, white)
+ self.make_mv(user_mv, white)
+ white = not white
+
+ def play_minimax(self, white: bool = True, depth: int = chessconf.minimax_depth) -> None:
+ self.bot = True
+ if white:
+ self.bot = False
+ self.render(True)
+ userinput = input('\nEnter your move in algebraic notation > ')
+ user_mv = self.parse(userinput, True)
+ self.make_mv(user_mv, True)
+ while True:
+ enemy_mv = random.choice(self.best_mvs(not white, depth))
+ self.make_mv(enemy_mv, not white)
+ self.render(white)
+ userinput = input('\nEnter your move in algebraic notation > ')
+ user_mv = self.parse(userinput, white)
+ self.make_mv(user_mv, white)
+
+ def play_stockfish(self, white: bool = True, depth: int = chessconf.stockfish_depth) -> None:
+ self.bot = True
+ if white:
+ self.bot = False
+ self.render(True)
+ userinput = input('\nEnter your move in algebraic notation > ')
+ user_mv = self.parse(userinput, True)
+ self.make_mv(user_mv, True)
+ while True:
+ enemy_return = self.stockfish_mv(not white, depth)
+ enemy_mv = self.parse(enemy_return, not white)
+ self.make_mv(enemy_mv, not white)
+ self.render(white)
+ userinput = input('\nEnter your move in algebraic notation > ')
+ user_mv = self.parse(userinput, white)
+ self.make_mv(user_mv, white)
+
+if __name__ == '__main__':
+ board = Board()
+ game = board.select_game()
+ color = board.select_color()
+ game(color) \ No newline at end of file