Added doc strings to chess.py
parent
661e87715a
commit
f402ae36af
89
chess.py
89
chess.py
|
@ -3,6 +3,7 @@ from unicodedata import lookup
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
def cross(A, B):
|
def cross(A, B):
|
||||||
|
"""Return sum of of every element from A and B."""
|
||||||
return tuple(a+b for a in A for b in B)
|
return tuple(a+b for a in A for b in B)
|
||||||
|
|
||||||
pieces = ["pawn", "knight", "bishop", "rook", "queen", "king"]
|
pieces = ["pawn", "knight", "bishop", "rook", "queen", "king"]
|
||||||
|
@ -33,22 +34,29 @@ AN_names = {'R' : "rook",
|
||||||
'K' : "king"}
|
'K' : "king"}
|
||||||
|
|
||||||
def move(sq, v):
|
def move(sq, v):
|
||||||
|
"""Add vector v to square and return a square, even if not in board."""
|
||||||
return chr(ord(sq[0]) + v[0]) + str(int(sq[1]) + v[1])
|
return chr(ord(sq[0]) + v[0]) + str(int(sq[1]) + v[1])
|
||||||
|
|
||||||
def moves(sq, dirs):
|
def moves(sq, dirs):
|
||||||
|
"""Add multiple vectors to a square. Return squares that are in board."""
|
||||||
targets = [move(sq, dir) for dir in dirs]
|
targets = [move(sq, dir) for dir in dirs]
|
||||||
return [sq for sq in targets if sq in squares]
|
return [sq for sq in targets if sq in squares]
|
||||||
|
|
||||||
def invert(color):
|
def invert(color):
|
||||||
|
"""Return inverted color."""
|
||||||
if color == "white":
|
if color == "white":
|
||||||
return "black"
|
return "black"
|
||||||
return "white"
|
return "white"
|
||||||
|
|
||||||
class Empty:
|
class Empty:
|
||||||
|
"""Empty class to avoid unnecessary quotations."""
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return str(vars(self))
|
return str(vars(self))
|
||||||
|
|
||||||
def parse_AN(move):
|
def parse_AN(move):
|
||||||
|
"""Parse chess move writen in algebraic notation. Return relevant
|
||||||
|
information encapsulated within an Empty class, or False if parsing
|
||||||
|
fails."""
|
||||||
def suffix_in(l, dval=False, i=1):
|
def suffix_in(l, dval=False, i=1):
|
||||||
nonlocal move
|
nonlocal move
|
||||||
x = move[-i:]
|
x = move[-i:]
|
||||||
|
@ -87,6 +95,7 @@ def parse_AN(move):
|
||||||
r.piece = AN_names[r.piece]
|
r.piece = AN_names[r.piece]
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
# Names for vectors
|
||||||
up, down = (0, 1), (0, -1)
|
up, down = (0, 1), (0, -1)
|
||||||
left, right = (-1, 0), (1, 0)
|
left, right = (-1, 0), (1, 0)
|
||||||
rook_dirs = (up, down, left, right)
|
rook_dirs = (up, down, left, right)
|
||||||
|
@ -97,20 +106,27 @@ bishop_dirs = (upper_left, upper_right,
|
||||||
lower_left, lower_right)
|
lower_left, lower_right)
|
||||||
|
|
||||||
class Piece:
|
class Piece:
|
||||||
|
"""Contains piece color and piece type"""
|
||||||
def __init__(self, color=None, piece=None):
|
def __init__(self, color=None, piece=None):
|
||||||
|
"""Assign color and piece. If not specified they
|
||||||
|
default to None."""
|
||||||
self.color = color
|
self.color = color
|
||||||
self.piece = piece
|
self.piece = piece
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
"""Returns unicode representation of instance."""
|
||||||
if None in (self.piece, self.color):
|
if None in (self.piece, self.color):
|
||||||
return ""
|
return ""
|
||||||
name = self.color.upper() + " CHESS " + self.piece.upper()
|
name = self.color.upper() + " CHESS " + self.piece.upper()
|
||||||
return lookup(name)
|
return lookup(name)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
"""Same as repr."""
|
||||||
return repr(self)
|
return repr(self)
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
|
"""Ensures equality testing. If color or piece or both
|
||||||
|
equal None, instance equals None."""
|
||||||
if other == None:
|
if other == None:
|
||||||
return None in (self.color, self.piece)
|
return None in (self.color, self.piece)
|
||||||
if not isinstance(other, Piece):
|
if not isinstance(other, Piece):
|
||||||
|
@ -118,6 +134,16 @@ class Piece:
|
||||||
return self.piece == other.piece and self.color == other.color
|
return self.piece == other.piece and self.color == other.color
|
||||||
|
|
||||||
class Game:
|
class Game:
|
||||||
|
"""Chess game class.
|
||||||
|
Squares are represented by strings made of letters and integers e.g. 'h4'.
|
||||||
|
Board is represented by a dictionary whose keys are squares
|
||||||
|
and values instances of Piece class.
|
||||||
|
Moves are represented internally by tuples of squares;
|
||||||
|
a source and destination.
|
||||||
|
Moves are appended to moves stack.
|
||||||
|
Upon each move, the relevant attributes of the instance (the game state)
|
||||||
|
are appended to the history stack. This allows loading previous
|
||||||
|
game states to instance variables."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.index = 0
|
self.index = 0
|
||||||
self.make_board()
|
self.make_board()
|
||||||
|
@ -135,7 +161,7 @@ class Game:
|
||||||
self.turn))
|
self.turn))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
# Unicode board representation
|
"""Return unicode board representation."""
|
||||||
r = ""
|
r = ""
|
||||||
for rank in ranks:
|
for rank in ranks:
|
||||||
r += rank + " |"
|
r += rank + " |"
|
||||||
|
@ -152,6 +178,7 @@ class Game:
|
||||||
return r
|
return r
|
||||||
|
|
||||||
def make_board(self):
|
def make_board(self):
|
||||||
|
"""Create board and store it in self.board."""
|
||||||
self.board = dict((sq, Piece()) for sq in squares)
|
self.board = dict((sq, Piece()) for sq in squares)
|
||||||
# Add pieces
|
# Add pieces
|
||||||
for piece, positions in init_positions.items():
|
for piece, positions in init_positions.items():
|
||||||
|
@ -169,10 +196,15 @@ class Game:
|
||||||
self.board["h8"].side = "king"
|
self.board["h8"].side = "king"
|
||||||
|
|
||||||
def rebase(self):
|
def rebase(self):
|
||||||
|
"""Shorten moves and history stacks if move is made
|
||||||
|
while past game state is loaded into memory. Essentialy
|
||||||
|
overrides future game states."""
|
||||||
self.moves = self.moves[:self.index+1]
|
self.moves = self.moves[:self.index+1]
|
||||||
self.history = self.history[:self.index+1]
|
self.history = self.history[:self.index+1]
|
||||||
|
|
||||||
def timetravel(self, i):
|
def timetravel(self, i):
|
||||||
|
"""Load game state from past into instance variables.
|
||||||
|
Can be reversed since history and moves stack stay intact."""
|
||||||
if i >= len(self.history) or i < 0:
|
if i >= len(self.history) or i < 0:
|
||||||
return False
|
return False
|
||||||
c, board, turn = self.history[i]
|
c, board, turn = self.history[i]
|
||||||
|
@ -182,30 +214,39 @@ class Game:
|
||||||
self.index = i
|
self.index = i
|
||||||
|
|
||||||
def prev(self):
|
def prev(self):
|
||||||
|
"""Load game state from previous turn, if it exists."""
|
||||||
return self.timetravel(self.index-1)
|
return self.timetravel(self.index-1)
|
||||||
|
|
||||||
def next(self):
|
def next(self):
|
||||||
|
"""Load game state from next turn, if it exists."""
|
||||||
return self.timetravel(self.index+1)
|
return self.timetravel(self.index+1)
|
||||||
|
|
||||||
def last(self):
|
def last(self):
|
||||||
|
"""Load game state from last move."""
|
||||||
return self.timetravel(len(self.history)-1)
|
return self.timetravel(len(self.history)-1)
|
||||||
|
|
||||||
def first(self):
|
def first(self):
|
||||||
|
"""Load first recorded game state, an empty board."""
|
||||||
return self.timetravel(0)
|
return self.timetravel(0)
|
||||||
|
|
||||||
def is_empty(self, sq):
|
def is_empty(self, sq):
|
||||||
|
"""Return True if square is empty, else False."""
|
||||||
return self.board[sq] == None
|
return self.board[sq] == None
|
||||||
|
|
||||||
def is_check(self, color=None):
|
def is_check(self, color=None):
|
||||||
|
"""Return True if king of active player is in check else False."""
|
||||||
if color == None:
|
if color == None:
|
||||||
color = self.turn
|
color = self.turn
|
||||||
sq = self.occupying(Piece(color, "king"))[0]
|
sq = self.occupying(Piece(color, "king"))[0]
|
||||||
return self.is_attacked(color, sq)
|
return self.is_attacked(color, sq)
|
||||||
|
|
||||||
def is_mate(self):
|
def is_mate(self):
|
||||||
|
"""Return True if checkmate for active player."""
|
||||||
return self.is_check() and self.is_stall()
|
return self.is_check() and self.is_stall()
|
||||||
|
|
||||||
def is_stall(self):
|
def is_stall(self):
|
||||||
|
"""Return True if active player has no legal move. Whether
|
||||||
|
the king is in check is irrelevant."""
|
||||||
for move in self.all_legal_moves():
|
for move in self.all_legal_moves():
|
||||||
new = deepcopy(self)
|
new = deepcopy(self)
|
||||||
if self.board[move[0]].piece == "pawn" and move[1][1]==home_ranks[invert(self.turn)]:
|
if self.board[move[0]].piece == "pawn" and move[1][1]==home_ranks[invert(self.turn)]:
|
||||||
|
@ -215,12 +256,16 @@ class Game:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def endofgame(self):
|
def endofgame(self):
|
||||||
|
"""Return True if active player can't make a legal move."""
|
||||||
return self.is_stall() or self.is_mate()
|
return self.is_stall() or self.is_mate()
|
||||||
|
|
||||||
def occupying(self, piece):
|
def occupying(self, piece):
|
||||||
|
"""Return squares occupied by piece. Piece variable is an
|
||||||
|
instance of Piece class."""
|
||||||
return [sq for sq in squares if self.board[sq] == piece]
|
return [sq for sq in squares if self.board[sq] == piece]
|
||||||
|
|
||||||
def castle(self, side):
|
def castle(self, side):
|
||||||
|
"""Castle if possible, if not return False."""
|
||||||
color = self.turn
|
color = self.turn
|
||||||
rank = home_ranks[self.turn]
|
rank = home_ranks[self.turn]
|
||||||
|
|
||||||
|
@ -262,6 +307,9 @@ class Game:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def move(self, source, target, promotion=None):
|
def move(self, source, target, promotion=None):
|
||||||
|
"""Move piece from source to target if legal and promote it
|
||||||
|
to promotion if it's a pawn whos reached the last
|
||||||
|
rank. Return False if fails."""
|
||||||
new = deepcopy(self)
|
new = deepcopy(self)
|
||||||
if not new.is_legal(source, target, promotion):
|
if not new.is_legal(source, target, promotion):
|
||||||
return False
|
return False
|
||||||
|
@ -308,6 +356,9 @@ class Game:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def is_legal(self, source, target, promotion):
|
def is_legal(self, source, target, promotion):
|
||||||
|
"""Return True if move is partially legal. Does not
|
||||||
|
check whether the active players king is in check
|
||||||
|
after the move! See the validate_move method."""
|
||||||
piece = self.board[source].piece
|
piece = self.board[source].piece
|
||||||
color = self.board[source].color
|
color = self.board[source].color
|
||||||
pawn_on_last_rank = piece == "pawn" and \
|
pawn_on_last_rank = piece == "pawn" and \
|
||||||
|
@ -321,6 +372,7 @@ class Game:
|
||||||
return color == self.turn and target in self.possible_moves(source)
|
return color == self.turn and target in self.possible_moves(source)
|
||||||
|
|
||||||
def is_attacked(self, color, sq):
|
def is_attacked(self, color, sq):
|
||||||
|
"""Return True if square is attacked by enemy of opposite color."""
|
||||||
enemy_color = invert(color)
|
enemy_color = invert(color)
|
||||||
attacked = []
|
attacked = []
|
||||||
for piece in pieces:
|
for piece in pieces:
|
||||||
|
@ -329,6 +381,9 @@ class Game:
|
||||||
return sq in attacked
|
return sq in attacked
|
||||||
|
|
||||||
def all_legal_moves(self):
|
def all_legal_moves(self):
|
||||||
|
"""Return all possible partially legal moves by active
|
||||||
|
player. These moves can put the active player in check,
|
||||||
|
thus they are not completely legal. Castlings not included."""
|
||||||
color = self.turn
|
color = self.turn
|
||||||
sqrs = []
|
sqrs = []
|
||||||
for piece in pieces:
|
for piece in pieces:
|
||||||
|
@ -341,6 +396,9 @@ class Game:
|
||||||
return r
|
return r
|
||||||
|
|
||||||
def possible_moves(self, sq):
|
def possible_moves(self, sq):
|
||||||
|
"""Return all squares the piece from sq can move to.
|
||||||
|
Moving to these squares might put the player in check.
|
||||||
|
Castlings not included."""
|
||||||
board = self.board
|
board = self.board
|
||||||
piece = board[sq].piece
|
piece = board[sq].piece
|
||||||
color = board[sq].color
|
color = board[sq].color
|
||||||
|
@ -380,6 +438,8 @@ class Game:
|
||||||
return targets
|
return targets
|
||||||
|
|
||||||
def attacks(self, sq):
|
def attacks(self, sq):
|
||||||
|
"""Return all squares the piece on sq attacks. Return empty list
|
||||||
|
if square is empty."""
|
||||||
if sq not in squares:
|
if sq not in squares:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@ -424,12 +484,19 @@ class Game:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def validate_move(self, source, dest, promotion):
|
def validate_move(self, source, dest, promotion):
|
||||||
|
"""Create a copy of the game and try given move.
|
||||||
|
Return True if the move succeeds. This method
|
||||||
|
exists because is_legal does not test whether
|
||||||
|
the move puts the player in check."""
|
||||||
new = deepcopy(self)
|
new = deepcopy(self)
|
||||||
valid = new.move(source, dest, promotion)
|
valid = new.move(source, dest, promotion)
|
||||||
new.turn = self.turn
|
new.turn = self.turn
|
||||||
return valid, new
|
return valid, new
|
||||||
|
|
||||||
def AN_move(self, move):
|
def AN_move(self, move):
|
||||||
|
"""Move is given in algebraic notation. The method
|
||||||
|
moves the piece if the move is legal and unambiguous,
|
||||||
|
else it returns False."""
|
||||||
AN = move
|
AN = move
|
||||||
info = parse_AN(move)
|
info = parse_AN(move)
|
||||||
if info == False:
|
if info == False:
|
||||||
|
@ -462,23 +529,3 @@ class Game:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test():
|
|
||||||
game = Game()
|
|
||||||
move = ""
|
|
||||||
while not game.is_stall() and move!='q':
|
|
||||||
move = input(">> ")
|
|
||||||
if move == "prev":
|
|
||||||
game.prev()
|
|
||||||
print(game)
|
|
||||||
continue
|
|
||||||
elif move == "next":
|
|
||||||
game.next()
|
|
||||||
print(game)
|
|
||||||
continue
|
|
||||||
v = game.AN_move(move)
|
|
||||||
if not v:
|
|
||||||
print("Invalid")
|
|
||||||
continue
|
|
||||||
print(game)
|
|
||||||
print (game.history)
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue