I have reached a significant milestone since the start of my chess game project. I have implemented all basic functionality. There is no castling, en passant and pawn promotion yet but I have implemented checks and checkmate functionality.
I would like my code to be reviewed before going further in the project so that I can optimize the code beforehand.
I would appreciate any improvements and also bugs found.
Here is my code:
constants.py
WHITE = True
BLACK = False
RANK: dict[str, int] = {
"a": 0, "b": 1, "c": 2, "d": 3,
"e": 4, "f": 5, "g": 6, "h": 7
}
position.py
from constants import *
class Position:
def __init__(self, y: int, x: int) -> None:
self.y = y
self.x = x
def __add__(self, other):
return Position(self.y + other.y, self.x + other.x)
def __sub__(self, other):
return Position(self.y - other.y, self.x - other.x)
def __mul__(self, value: int):
return Position(self.y * value, self.x * value)
def __eq__(self, other) -> bool:
return self.y == other.y and self.x == other.x
def __repr__(self) -> str:
return f"(y: {self.y}, x: {self.x})"
def __str__(self) -> str:
flipped_rank = {v: k for k, v in RANK.items()}
return f"{flipped_rank[self.x]}{self.y + 1}"
def abs(self):
return Position(abs(self.y), abs(self.x))
support.py
from position import *
def is_same_color(*pieces: list[str]) -> bool:
for i in range(len(pieces) - 1):
if is_white(pieces[i]) == is_white(pieces[i + 1]):
return False
return True
def is_white(piece: str) -> bool:
return piece.isupper()
def is_black(piece: str) -> bool:
return piece.islower()
def is_king(piece: str) -> bool:
return piece.lower() == "k"
def is_queen(piece: str) -> bool:
return piece.lower() == "q"
def is_rook(piece: str) -> bool:
return piece.lower() == "r"
def is_knight(piece: str) -> bool:
return piece.lower() == "n"
def is_bishop(piece: str) -> bool:
return piece.lower() == "b"
def is_pawn(piece: str) -> bool:
return piece.lower() == "p"
def is_empty(piece: str) -> bool:
return piece == "."
def extract_move(move: str) -> tuple[Position, Position]:
try:
start_pos = Position(int(move[1]) - 1, RANK[move[0]])
end_pos = Position(int(move[3]) - 1, RANK[move[2]])
return start_pos, end_pos
except:
raise ValueError(f"Invalid position {move}")
def sign(x: int | float):
if x < 0:
return -1
elif x > 0:
return 1
elif x == 0:
return 0
main.py
from support import *
from copy import deepcopy
start_position = [
["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"]
]
class Board:
def __init__(self, board):
self.board = board
self.current_turn = WHITE
self.legal_moves = self.generate_legal_moves()
self.status = "RUNNING"
def play_move(self, move):
start_pos, end_pos = extract_move(move)
if move in self.legal_moves:
self[end_pos] = self[start_pos]
self[start_pos] = "."
self.current_turn = not self.current_turn
self.legal_moves = self.generate_legal_moves()
self.update_status()
else:
print(f"Invalid move {move}...")
def update_status(self):
if self.is_checkmate():
self.status = "GAMEOVER"
def play_moves(self, moves: str):
for move in moves.split():
print(self)
self.play_move(move)
def is_valid(self, move: str) -> bool:
start_pos, end_pos = extract_move(move)
largest = max(start_pos.y, start_pos.x, end_pos.y, end_pos.x)
smallest = min(start_pos.y, start_pos.x, end_pos.y, end_pos.x)
# Check if coordinates are out of bound
if smallest < 0 or largest > 7:
return False
if start_pos == end_pos:
return False
piece = self[start_pos]
if is_empty(piece):
return False
to_capture_piece = self[end_pos]
if not is_empty(to_capture_piece) and not is_same_color(to_capture_piece, piece):
return False
delta = end_pos - start_pos
if is_pawn(piece):
if abs(delta.y) == 1: # 1 step forward
if delta.x == 0 and is_empty(self[end_pos]): # No capture
return True
elif abs(delta.x) == 1 and not is_empty(self[end_pos]): # Capture
return True
if (abs(delta.y) == 2 and start_pos.y in (1, 6) and
is_empty(self[end_pos]) and is_empty(self[end_pos - Position(sign(delta.y), 0)])
): # 2 step forward
return True
elif is_bishop(piece):
if abs(delta.y) == abs(delta.x):
increment = Position(sign(delta.y), sign(delta.x))
for i in range(1, abs(delta.y)):
if not is_empty(self[start_pos + (increment * i)]):
return False
return True
elif is_rook(piece):
if delta.x == 0 or delta.y == 0:
increment = Position(sign(delta.y), sign(delta.x))
for i in range(1, max(abs(delta.y), abs(delta.x))):
if not is_empty(self[start_pos + (increment * i)]):
return False
return True
elif is_knight(piece):
if delta.abs() == Position(2, 1) or delta.abs() == Position(1, 2):
return True
elif is_queen(piece):
# Rook validation
if delta.x == 0 or delta.y == 0:
increment = Position(sign(delta.y), sign(delta.x))
for i in range(1, max(abs(delta.y), abs(delta.x))):
if not is_empty(self[start_pos + (increment * i)]):
return False
return True
# Bishop validation
if abs(delta.y) == abs(delta.x):
increment = Position(sign(delta.y), sign(delta.x))
for i in range(1, abs(delta.y)):
if not is_empty(self[start_pos + (increment * i)]):
return False
return True
elif is_king(piece):
if abs(delta.y) in (0, 1) and abs(delta.x) in (0, 1):
return True
return False
def is_check(self, move) -> bool:
new_game = deepcopy(self)
start_pos, end_pos = extract_move(move)
new_game[end_pos] = new_game[start_pos]
new_game[start_pos] = "."
king_pos = new_game.get_king_pos(new_game.current_turn)
for pos in new_game.get_all_pieces_pos()[not new_game.current_turn]:
if new_game.is_valid(str(pos) + str(king_pos)):
return True
return False
def is_checkmate(self) -> bool:
return len(self.legal_moves) == 0
def generate_legal_moves(self) -> list[str]:
legal_moves = []
candidate_moves = []
pieces_pos = self.get_all_pieces_pos()[self.current_turn]
# print([str(pos) for pos in pieces_pos])
for pos in pieces_pos:
piece = self[pos]
# print("In for pos in pieces_pos:", piece, pos)
if is_pawn(piece):
if is_white(piece):
deltas = [
Position(1, 0), Position(2, 0),
Position(1, 1), Position(1, -1)
]
else:
deltas = [
Position(-1, 0), Position(-2, 0),
Position(-1, 1), Position(-1, -1)
]
for delta in deltas:
try:
move = str(pos) + str(pos + delta)
candidate_moves.append(move)
except KeyError:
pass
elif is_knight(piece):
deltas = [
Position(2, 1), Position(1, 2),
Position(-2, 1), Position(1, -2),
Position(2, -1), Position(-1, 2),
Position(-2, -1), Position(-1, -2)
]
for delta in deltas:
try:
move = str(pos) + str(pos + delta)
candidate_moves.append(move)
except KeyError:
pass
elif is_bishop(piece):
deltas = [
Position(1, 1), Position(-1, -1),
Position(1, -1), Position(-1, 1)
]
for delta in deltas:
for i in range(1, 8):
try:
move = str(pos) + str(pos + delta * i)
candidate_moves.append(move)
except KeyError:
pass
elif is_rook(piece):
deltas = [
Position(1, 0), Position(0, 1),
Position(-1, 0), Position(0, -1)
]
for delta in deltas:
for i in range(1, 8):
try:
move = str(pos) + str(pos + delta * i)
candidate_moves.append(move)
except KeyError:
pass
elif is_king(piece):
deltas = [
Position(1, 0), Position(0, 1),
Position(-1, 0), Position(0, -1),
Position(1, 1), Position(-1, -1),
Position(1, -1), Position(-1, 1)
]
for delta in deltas:
try:
move = str(pos) + str(pos + delta)
candidate_moves.append(move)
except KeyError:
pass
elif is_queen(piece):
deltas = [
# Bishop
Position(1, 1), Position(-1, -1),
Position(1, -1), Position(-1, 1),
# Rook
Position(1, 0), Position(0, 1),
Position(-1, 0), Position(0, -1)
]
for delta in deltas:
for i in range(1, 8):
try:
move = str(pos) + str(pos + delta * i)
candidate_moves.append(move)
except KeyError:
pass
for move in candidate_moves:
try:
# print(move, self.is_valid(move), not self.is_check(move))
if self.is_valid(move) and not self.is_check(move):
legal_moves.append(move)
except ValueError:
pass
return legal_moves
def get_all_pieces_pos(self) -> dict[bool, list[Position]]:
pieces_pos = {WHITE: [], BLACK: []}
for y in range(8):
for x in range(8):
piece = self.board[y][x]
if not is_empty(piece):
pieces_pos[is_white(piece)].append(Position(y, x))
return pieces_pos
def get_king_pos(self, color: bool) -> Position:
for y in range(8):
for x in range(8):
if is_king(self.board[y][x]) and is_white(self.board[y][x]) == color:
return Position(y, x)
def __getitem__(self, pos: Position) -> str:
return self.board[pos.y][pos.x]
def __setitem__(self, pos: Position, piece: str) -> None:
self.board[pos.y][pos.x] = piece
def __repr__(self) -> str:
return "\n".join(
[" ".join(rank + [str(8 - i)]) for i, rank in enumerate(self.board[::-1])] +
[" ".join(RANK.keys())]
)
game = Board(start_position)
def play():
while True:
print(game)
if game.status == "GAMEOVER":
print(player, "wins!!")
break
player = "WHITE" if game.current_turn == WHITE else "BLACK"
# print([str(move) for move in game.generate_legal_moves()])
move = input(f"{player}, please enter your move:").lower().strip()
game.play_move(move)
play()
Thank you for your time!!