from itertools import product from unicodedata import lookup from copy import deepcopy def cross(A, B): return tuple(a+b for a in A for b in B) pieces = ["pawn", "knight", "bishop", "rook", "queen", "king"] colors = ["white", "black"] ranks = "87654321" files = "abcdefgh" squares = cross(files, ranks) pawn_ranks = {"white" : '2', "black" : '7' } home_ranks = {"white" : '1', "black" : '8' } init_positions = {"pawn" : cross(files, pawn_ranks.values()), "rook" : cross("ah", home_ranks.values()), "knight" : cross("bg", home_ranks.values()), "bishop" : cross("cf", home_ranks.values()), "queen" : cross("d", home_ranks.values()), "king" : cross("e", home_ranks.values()) } AN_names = {'R' : "rook", 'N' : "knight", 'B' : "bishop", 'Q' : "queen", 'K' : "king"} def move(sq, v): return chr(ord(sq[0]) + v[0]) + str(int(sq[1]) + v[1]) def moves(sq, dirs): targets = [move(sq, dir) for dir in dirs] return [sq for sq in targets if sq in squares] def invert(color): if color == "white": return "black" return "white" class Empty: def __repr__(self): return str(vars(self)) def parse_AN(move): def suffix_in(l, dval=False, i=1): nonlocal move x = move[-i:] if x in list(l): move = move[:-i] return x return dval queenside = ["{}-{}-{}".format(c,c,c) for c in "0O"] kingside = ["{}-{}".format(c,c) for c in "0O"] r = Empty() r.kingside = r.queenside = False if move in queenside: r.queenside = True return r if move in kingside: r.kingside = True return r r.move = move r.mate = suffix_in('#') == '#' r.check = suffix_in('+') == '+' r.promotion = suffix_in(AN_names, None) if r.promotion: r.promotion = AN_names[r.promotion] _ = suffix_in('=') r.dest = suffix_in(squares, i=2) r.capture = suffix_in("xX") != False r.rank = suffix_in(ranks, None) r.file = suffix_in(files, None) r.piece = suffix_in(AN_names, 'pawn') if len(move) > 0 or (r.promotion and r.piece != "pawn") or not r.dest: return False if r.piece != "pawn": r.piece = AN_names[r.piece] return r up, down = (0, 1), (0, -1) left, right = (-1, 0), (1, 0) rook_dirs = (up, down, left, right) upper_left, upper_right = (-1, 1), (1, 1) lower_left, lower_right = (-1, -1), (1, -1) bishop_dirs = (upper_left, upper_right, lower_left, lower_right) class Piece: def __init__(self, color=None, piece=None): self.color = color self.piece = piece def __repr__(self): if None in (self.piece, self.color): return "" name = self.color.upper() + " CHESS " + self.piece.upper() return lookup(name) def __str__(self): return repr(self) def __eq__(self, other): if other == None: return None in (self.color, self.piece) if not isinstance(other, Piece): return NotImplemented return self.piece == other.piece and self.color == other.color class Game: def __init__(self): self.index = 0 self.make_board() self.moves = [] self.AN_moves = [] self.history = [] self.turn = "white" self.can_castle = {"white" : {"queen" : True, "king" : True}, "black" : {"queen" : True, "king" : True} } self.history.append(deepcopy(self)) def __repr__(self): # Unicode board representation r = "" for rank in ranks: r += rank + " |" for file in files: sq = file+rank r += " " if self.is_empty(sq): r += " " else: r += repr(self.board[sq]) r += "\n" r += " +" + "-"*16 + "\n" r += " "*4 + " ".join(list(files)) return r def make_board(self): self.board = dict((sq, Piece()) for sq in squares) # Add pieces for piece, positions in init_positions.items(): for sq in positions: self.board[sq].piece = piece # Add colors for sq in cross(files, "12"): self.board[sq].color = "white" for sq in cross(files, "78"): self.board[sq].color = "black" # Castling markers self.board["a1"].side = "queen" self.board["a8"].side = "queen" self.board["h1"].side = "king" self.board["h8"].side = "king" def rebase(self): self.moves = self.moves[:self.index+1] self.history = self.history[:self.index+1] def timetravel(self, i): if i >= len(self.history) or i < 0: return False new = self.history[i] self.board = new.board self.turn = new.turn self.can_castle = new.can_castle self.index = i def prev(self): return self.timetravel(self.index-1) def next(self): return self.timetravel(self.index+1) def last(self): return self.timetravel(len(self.history)-1) def first(self): return self.timetravel(0) def is_empty(self, sq): return self.board[sq] == None def is_check(self, color=None): if color == None: color = self.turn sq = self.occupying(Piece(color, "king"))[0] return self.is_attacked(color, sq) def is_mate(self): return self.is_check() and self.is_stall() def is_stall(self): for move in self.all_legal_moves(): new = deepcopy(self) if self.board[move[0]].piece == "pawn" and move[1][1]==home_ranks[invert(self.turn)]: move += ("queen",) if new.move(*move): return False return True def endofgame(self): return self.is_stall() or self.is_mate() def occupying(self, piece): return [sq for sq in squares if self.board[sq] == piece] def castle(self, side): color = self.turn rank = home_ranks[self.turn] if side not in ("queen", "king"): return False if not self.can_castle[color][side]: return False if side == "queen": check_if_empty = cross("bcd", rank) check_if_attacked = cross("cde", rank) rook_move = "a"+rank, "d"+rank king_move = "e"+rank, "c"+rank elif side == "king": check_if_empty = cross("fg", rank) check_if_attacked = check_if_empty rook_move = "h"+rank, "f"+rank king_move = "e"+rank, "g"+rank are_empty = all(map(self.is_empty, check_if_empty)) not_attacked = not any(self.is_attacked(color, sq) for sq in check_if_attacked) if not (are_empty and not_attacked): return False self.rebase() self.board[king_move[1]] = self.board[king_move[0]] self.board[rook_move[1]] = self.board[rook_move[0]] self.board[king_move[0]] = Piece() self.board[rook_move[0]] = Piece() self.can_castle[color]["queen"] = False self.can_castle[color]["king"] = False self.moves.append(king_move) self.turn = invert(self.turn) self.history.append(deepcopy(self)) self.index += 1 return True def move(self, source, target, promotion=None): new = deepcopy(self) if not new.is_legal(source, target, promotion): return False board = new.board moved = board[source] eaten = board[target] if promotion == None: promotion = moved else: promotion = Piece(new.turn, promotion) if moved.piece == "pawn" and target in moves(source, bishop_dirs) and eaten==None: # En passant a, b = move(target, up), move(target, down) board[a] = Piece() board[b] = Piece() # Castling if moved.piece == "rook": new.can_castle[new.turn][moved.side] = False if eaten.piece == "rook": new.can_castle[eaten.color][eaten.side] = False if moved.piece == "king": new.can_castle[new.turn]["queen"] = False new.can_castle[new.turn]["king"] = False board[source] = Piece() board[target] = promotion if new.is_check(self.turn): return False self.rebase() self.moves.append((source, target)) self.board = new.board self.can_castle = new.can_castle self.turn = invert(moved.color) self.history.append(deepcopy(self)) self.index += 1 return True def is_legal(self, source, target, promotion): piece = self.board[source].piece color = self.board[source].color pawn_on_last_rank = piece == "pawn" and \ home_ranks[invert(color)] == target[1] if promotion != None and not pawn_on_last_rank: return False if promotion == None and pawn_on_last_rank: return False if promotion in ("king", "pawn"): return False return color == self.turn and target in self.possible_moves(source) def is_attacked(self, color, sq): enemy_color = invert(color) attacked = [] for piece in pieces: occupied = self.occupying(Piece(enemy_color, piece)) attacked += sum(list(map(self.attacks, occupied)), []) return sq in attacked def all_legal_moves(self): color = self.turn sqrs = [] for piece in pieces: piece = Piece(color, piece) sqrs += self.occupying(piece) r = [] for sq in sqrs: mvs = self.possible_moves(sq) r += [(sq, m) for m in mvs] return r def possible_moves(self, sq): board = self.board piece = board[sq].piece color = board[sq].color def can_eat(target): eaten = board[target] return eaten != None and eaten.color != color is_empty = self.is_empty targets = [t for t in self.attacks(sq) if can_eat(t) or is_empty(t)] if piece == "pawn": r = [] dir, back = up, down if color == "black": dir, back = down, up frwd = move(sq, dir) jump = move(frwd, dir) if frwd in squares and is_empty(frwd): r.append(frwd) is_on_pawn_rank = pawn_ranks[color] == sq[1] if is_on_pawn_rank and is_empty(jump): r.append(jump) for t in targets: a, b = move(t, dir), move(t, back) en_passant = can_eat(b) and self.index>0 and\ self.moves[self.index-1] == (a,b) if can_eat(t) or en_passant: r.append(t) return r elif piece == "king": return [sq for sq in targets if not self.is_attacked(color, sq)] else: return targets def attacks(self, sq): if sq not in squares: return [] board = self.board piece = board[sq].piece color = board[sq].color def possible_line(dir): target = move(sq, dir) while target in board and self.is_empty(target): yield target target = move(target, dir) if target in board: yield target def possible_lines(dirs): return sum(list(map(list, map(possible_line, dirs))), []) rook = possible_lines(rook_dirs) bishop = possible_lines(bishop_dirs) if piece == "pawn": if color == "white": dirs = upper_left, upper_right else: dirs = lower_left, lower_right return moves(sq, dirs) elif piece == "knight": nums = [2, -2, 1, -1] dirs = [(a, b) for a,b in product(nums, nums) if abs(a) != abs(b)] return moves(sq, dirs) elif piece == "rook": return rook elif piece == "bishop": return bishop elif piece == "queen": return rook + bishop elif piece == "king": dirs = rook_dirs + bishop_dirs return moves(sq, dirs) return [] def validate_move(self, source, dest, promotion): new = deepcopy(self) valid = new.move(source, dest, promotion) new.turn = self.turn return valid, new def AN_move(self, move): AN = move info = parse_AN(move) if info == False: return False if info.queenside: return self.castle("queen") if info.kingside: return self.castle("king") piece = info.piece sqrs = self.occupying(Piece(self.turn, piece)) promotion = info.promotion dest = info.dest if info.file != None: sqrs = [sq for sq in sqrs if sq[0]==info.file] if info.rank != None: sqrs = [sq for sq in sqrs if sq[1]==info.rank] moves = [] for sq in sqrs: valid, new = self.validate_move(sq, dest, promotion) check = new.is_check()==info.check and new.is_mate()==info.mate if valid and check: moves.append((sq, dest, promotion)) if len(moves) != 1: return False move = moves[0] self.move(*move) self.AN_moves.append(AN) return True 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)