320 lines
14 KiB
Python
320 lines
14 KiB
Python
import random
|
|
|
|
class Game:
|
|
def __init__(self, player1, player2):
|
|
|
|
# Randomize who goes first when the board is created
|
|
if random.SystemRandom().randint(0, 1):
|
|
self.challengers = {'white': player1, 'black': player2}
|
|
else:
|
|
self.challengers = {'white': player2, 'black': player1}
|
|
|
|
# White goes first, so base whose turn it is off of that
|
|
self.white_turn = True
|
|
|
|
self.board = None
|
|
self.reset_board()
|
|
|
|
# The point of chess revolves around the king's position
|
|
# Due to this, we're going to use the king's position a lot, so lets save this variable
|
|
self.w_king_pos = (0, 4)
|
|
self.b_king_pos = (7, 4)
|
|
|
|
# Now, there's a move called En Passant: https://en.wikipedia.org/wiki/En_passant
|
|
# This can be done, if the piece being "taken" is in a specific position....and was just moved
|
|
# The latter is why this is important here, lets save a variable for the last pawn moved
|
|
self.last_pawn_moved = None
|
|
|
|
# The other special case we can do is castling, if the rook and king have not been moved yet this can be done
|
|
self.can_castle = {'white':{
|
|
(0, 0): True,
|
|
(0, 7): True},
|
|
'black': {
|
|
(7, 0): True,
|
|
(7, 7): True}}
|
|
|
|
|
|
def reset_board(self):
|
|
# Lets face the board with white on the bottom, black on top
|
|
# Chess notation is {letter}{number} which a 2D array doesn't support
|
|
# So we're just going to create this based on a normal number array
|
|
# However, we're going to flip it to make the row part of notation easier
|
|
self.board = [['WR', 'WN', 'WB', 'WQ', 'WK', 'WW', 'WN', 'WR'],
|
|
['WP', 'WP', 'WP', 'WP', 'WP', 'WP', 'WP', 'WP'],
|
|
['', '', '', '', '', '', '', ''],
|
|
['', '', '', '', '', '', '', ''],
|
|
['', '', '', '', '', '', '', ''],
|
|
['', '', '', '', '', '', '', ''],
|
|
['BP', 'BP', 'BP', 'BP', 'BP', 'BP', 'BP', 'BP'],
|
|
['BR', 'BN', 'BB', 'BQ', 'BK', 'BB', 'BN', 'BR']]
|
|
|
|
# We want to send a different message if it's not this players turn
|
|
# So lets split up 'can_play' and the checks for that actual move
|
|
def can_player(self, player):
|
|
if self.white_turn:
|
|
return player == self.challengers.get('white')
|
|
elif not self.white_turn:
|
|
return player == self.challengers.get('black')
|
|
|
|
def move(self, piece, pos):
|
|
# First lets transform the position
|
|
pos = (pos[1] - 1, ord(pos[0].upper()) - 65)
|
|
|
|
# Now lets transform the piece to what it will be on the board
|
|
piece_map = {'knight':'N',
|
|
'king': 'K',
|
|
'bishop': 'B',
|
|
'queen': 'Q',
|
|
'rook': 'R',
|
|
'pawn': 'P'}
|
|
|
|
piece_color = 'W' if self.white_turn else 'B'
|
|
piece = "{}{}".format(piece_color, piece_map.get(piece))
|
|
|
|
# Lets check for a piece that matches the provided one
|
|
for x, row in enumerate(self.board):
|
|
for y, board_piece in enumerate(row):
|
|
if piece == board_piece:
|
|
#TODO: Handle when multiple pieces of the same type can move to the same position
|
|
if self.valid_move((x, y), pos):
|
|
self._move(piece, (x, y), pos)
|
|
return
|
|
|
|
# Our internal method for actually moving the piece
|
|
def _move(self, piece, pos, new_pos):
|
|
# Set our last pawn moved to the new position if a pawn was moved
|
|
# Otherwise it needs to be None
|
|
if 'P' in piece:
|
|
self.last_pawn_moved = new_pos
|
|
else:
|
|
self.last_pawn_moved = None
|
|
|
|
# If the king has been moved, the player cannot castle anymore
|
|
if 'K' in piece:
|
|
if self.white_turn:
|
|
colour = 'white'
|
|
self.w_king_pos = new_pos
|
|
else:
|
|
colour = 'black'
|
|
self.b_king_pos = new_pos
|
|
|
|
for x in self.can_castle[colour]:
|
|
self.can_castle[colour][x] = False
|
|
|
|
# If the rook has been moved (depending on the side it was on)
|
|
# Then that side cannot be moved anymore
|
|
# Now, according to the rules, the following example is valid:
|
|
# - Promote a pawn to a rook, in the queen's corner
|
|
# - This rook has now not been moved, therefore it can be castled
|
|
# So we need to be a little lenient here
|
|
# This is why our castling "on or off" is based on position, so we can simply use that
|
|
if 'R' in piece:
|
|
colour = 'white' if self.white_turn else 'black'
|
|
try:
|
|
self.can_castle[colour][pos] = False
|
|
except KeyError:
|
|
pass
|
|
|
|
# Now lets do the actual 'moving' of pieces
|
|
# There's nothing special that happens we need to keep track of, when taking an enemy piece
|
|
# So lets just overwrite it, that's it
|
|
# TODO: The one special case to the above, En Passant
|
|
self.board[new_pos[0]][new_pos[1]] = piece
|
|
self.board[pos[0]][pos[1]] = ''
|
|
|
|
# Next couple methods are going to be used for convenience in order to check some things
|
|
# The idea of how to check for a "Check" is:
|
|
# - Get king position
|
|
# - Get all pieces on the other team
|
|
# - Check if their move, to ours, is a valid move
|
|
# To do this we need the following convenience methods
|
|
|
|
def check(self):
|
|
# To check for a check, what we should do is loop through the board
|
|
# Then check if it's the the current players turn's piece, and compare to moving to the king's position
|
|
for x, row in enumerate(self.board):
|
|
for y, piece in enumerate(row):
|
|
if self.white_turn and re.search('B.', piece) and self.valid_move((x, y), self.b_king_pos):
|
|
return True
|
|
elif not self.white_turn and 'W' in piece and self.valid_move((x, y), self.w_king_pos):
|
|
return True
|
|
|
|
def checkmate(self):
|
|
# We don't care about our check position, as this doesn't matter to us
|
|
# We can be in chekcmate if we have no other pieces, or are in check
|
|
# So calling check first is not something this method needs to worry about
|
|
king_pos = self.w_king_pos if self.white_turn else self.b_king_pos
|
|
|
|
# Lets do this dynamicaly, this is our range of movement (-1, 0, 1) for a king
|
|
move_range = range(-1, 2)
|
|
# Loop through the horizontal movements
|
|
for x in move_range:
|
|
# Loop through the vertical movements
|
|
for y in move_range:
|
|
# If we hit any position that we can move to, then we are not in checkmate
|
|
if self._valid_king_move(king_pos, (x, y)):
|
|
return False
|
|
|
|
def valid_move(self, pos, new_pos):
|
|
# Lets make sure a valid position was given, if not then we obviously can't move that piece (it don't exist brah)
|
|
try:
|
|
piece = self.board[pos[0]][pos[1]]
|
|
except IndexError:
|
|
return False
|
|
try:
|
|
new_piece = self.board[new_pos[0]][new_pos[1]]
|
|
except IndexError:
|
|
return False
|
|
|
|
# First and easiest check, make sure this is their piece
|
|
if self.white_turn and 'W' not in piece:
|
|
return False
|
|
elif not self.white_turn and not re.search('B.', piece):
|
|
return False
|
|
|
|
# Another easy check, no pieces can move onto their own piece
|
|
# This will also inadvertantly check if new_pos == pos
|
|
if self.white_turn and 'W' in new_piece:
|
|
return False
|
|
elif not self.white_turn and re.search('B.', new_piece):
|
|
return False
|
|
|
|
# Now lets check based on the type of piece it is, if they can move to the new position
|
|
# If the piece is a pawn
|
|
if 'P' in piece:
|
|
return self._valid_pawn_move(pos, new_pos, piece, new_piece)
|
|
# If the piece is a rook
|
|
elif 'R' in piece:
|
|
return self._valid_rook_move(pos, new_pos)
|
|
# If the piece is a Bishop (since there are "B" for black pieces, we need to check the second position)
|
|
# We're not going to use splicing here, in case this is a blank spot
|
|
elif re.search('(W|B)B', piece):
|
|
return self._valid_bishop_move(pos, new_pos)
|
|
# If the piece was a knight
|
|
elif 'N' in piece:
|
|
return self._valid_knight_move(pos, new_pos)
|
|
# If the piece was the queen
|
|
elif 'Q' in piece:
|
|
# The queen can move like a rook, or bishop, so return true if either of these are met
|
|
return any((self._valid_rook_move(pos, new_pos), self._valid_bishop_move(pos, new_pos)))
|
|
# If the piece was the king
|
|
elif 'K' in piece:
|
|
return self._valid_king_move(pos, new_pos)
|
|
# Otherwise this isn't a real piece, of course you can't move it
|
|
else:
|
|
return False
|
|
|
|
def _valid_king_move(self, pos, new_pos):
|
|
# The movement is simple, can move in any direction but only once
|
|
# However, we need to ensure this wouldn't be put us into check
|
|
x_movement = abs(new_pos[1] - pos[1])
|
|
y_movement = abs(new_pos[0] - pos[0])
|
|
if x_movement > 1 or y_movement > 1:
|
|
return False
|
|
|
|
# Now we can check for the check
|
|
for x, row in enumerate(self.board):
|
|
for y, piece in enumerate(row):
|
|
if self.white_turn and re.search('B.', piece) and self.valid_pos((x, y), new_pos):
|
|
return False
|
|
elif not self.white_turn and 'W' in piece and self.valid_pos((x, y), new_pos):
|
|
return False
|
|
|
|
# If this wouldn't cause check, then it's valid
|
|
return True
|
|
|
|
def _valid_knight_move(self, pos, new_pos):
|
|
# This is pretty simple, the knight can skip over pieces
|
|
# So we need to just check if the L shape it can make
|
|
x_movement = abs(new_pos[1] - pos[1])
|
|
y_movement = abs(new_pos[0] - pos[0])
|
|
|
|
if x_movement == 2:
|
|
return y_movement == 1
|
|
elif y_movement == 2:
|
|
return x_movement == 1
|
|
|
|
def _valid_bishop_move(self, pos, new_pos):
|
|
# We can only move in diagonals, easiest way to check this:
|
|
# Make sure we're moving the same amount in both directions
|
|
if new_pos[0] - pos[0] != new_pos[1] - new_pos[1]:
|
|
return False
|
|
# We also cannot jump over other pieces, so lets check this as well
|
|
increment_x = 1 if new_pos[0] > pos[0] else -1
|
|
increment_y = 1 if new_pos[1] > pos[1] else -1
|
|
|
|
temp_pos = pos
|
|
while temp_pos != new_pos:
|
|
if self.board[temp_pos[0]][temp_pos[1]] != '':
|
|
return False
|
|
temp_pos[0] += increment_x
|
|
temp_pos[1] += increment_y
|
|
|
|
return True
|
|
|
|
def _valid_pawn_move(self, pos, new_pos, piece, new_piece):
|
|
num_paces = new_pos[0] - pos[0] if self.white_turn else pos[0] - new_pos[0]
|
|
|
|
# The pawn has a lot of odd limitations compared to the rest of the pieces
|
|
# The easiest way to check this is going to be to check the limitations
|
|
|
|
# Lets first check if we're moving straight forward
|
|
if new_pos[1] == pos[1]:
|
|
# Easiest check if we're moving straight forward, we cannot take another piece by this method, period
|
|
if new_piece != '':
|
|
return False
|
|
|
|
# Now let's check if it's outside the range of what can possibly be moved in a straight line (2 paces)
|
|
if num_paces not in [1, 2]:
|
|
return False
|
|
|
|
# Now check if we're moving twice
|
|
if num_paces == 2:
|
|
# If we are moving twice, we have to be on home row
|
|
if (self.white_turn and pos[0] != 1) or (not self.white_turn and pos[0] != 6):
|
|
return False
|
|
|
|
# We cannot hop over a piece, make sure there's nothing in between us
|
|
if (self.white_turn and self.board[2][new_pos[1]] != '') or (not self.white_turn and self.board[5][new_pos[1]] != ''):
|
|
return False
|
|
# If these checks are not met, then our move is valid
|
|
else:
|
|
# Now lets check if we are moving diagonally one column first
|
|
# Since if we're not moving in a straight line, that's the only other possiblity
|
|
|
|
# Since we based num_paces earlier, off of whether or not this is a white/black piece
|
|
# We can only need to check here if it's moving 'forward' 1 pace (== 1)
|
|
if num_paces != 1:
|
|
return False
|
|
if abs(new_pos[1] - pos[1]) != 1:
|
|
return False
|
|
|
|
# En Passant is going to be a bit more complicated, so for now lets just check if there's an enemy piece where we're moving
|
|
if self.white_turn:
|
|
if not re.search('B.', new_piece):
|
|
# TODO: Check En Passant: https://en.wikipedia.org/wiki/En_passant
|
|
return False
|
|
else:
|
|
if 'W' not in new_piece:
|
|
# TODO: Check En Passant: https://en.wikipedia.org/wiki/En_passant
|
|
return False
|
|
# If we have passed all these checks, then this is a valid move
|
|
return True
|
|
|
|
def _valid_rook_move(self, pos, new_pos):
|
|
# We can only move in straight lines, so we require at least one of these below to not be hit
|
|
if pos[0] != new_pos[0] and pos[1] != new_pos[1]:
|
|
return False
|
|
# Next we need to check if there are any pieces in between the piece and the new position
|
|
# To do this, loop through the range between the current and new positions
|
|
if pos[0] == new_pos[0]:
|
|
increment_var = 1 if new_pos[1] > pos[1] else -1
|
|
for i in range(pos[1], new_pos[1]):
|
|
if self.board[pos[0]][i] != '':
|
|
return False
|
|
else:
|
|
increment_var = 1 if new_pos[0] > pos[0] else -1
|
|
for i in range(pos[0], new_pos[0]):
|
|
if self.board[i][pos[1]] != '':
|
|
return False
|
|
return True
|