def test_read(self): pos = StandardPosition((0, 0)) self.assertEqual(str(pos), 'a1') pos = StandardPosition((0, 7)) self.assertEqual(str(pos), 'a8') pos = StandardPosition((7, 7)) self.assertEqual(str(pos), 'h8') pos = StandardPosition((7, 0)) self.assertEqual(str(pos), 'h1')
def test_write(self): pos = StandardPosition.from_str('a1') self.assertEqual(tuple(pos), (0, 0)) pos = StandardPosition.from_str('a8') self.assertEqual(tuple(pos), (0, 7)) pos = StandardPosition.from_str('h8') self.assertEqual(tuple(pos), (7, 7)) pos = StandardPosition.from_str('h1') self.assertEqual(tuple(pos), (7, 0))
def generate_moves(str_moves): for str_move in str_moves: source = StandardPosition.from_str(str_move[0:2]) destination = StandardPosition.from_str(str_move[2:4]) promotion_char = str_move[4:] if promotion_char: yield StandardMove(source=source, destination=destination, promotion=from_str(promotion_char, initialized=False)) else: yield StandardMove(source=source, destination=destination)
def from_str(cls, move_str: str): if len(move_str) == 4: return cls(StandardPosition.from_str(move_str[0:2]), StandardPosition.from_str(move_str[2:4])) elif len(move_str) == 5: return cls(StandardPosition.from_str(move_str[0:2]), StandardPosition.from_str(move_str[2:4]), from_str(move_str[4], initialized=False)) else: raise ValueError( 'Primitive move is described as 4-5 length string (without or with promotion)' )
def test_read_fenstring(self): board = StandardBoard() board.put_piece(piece=King(White), position=StandardPosition.from_str('e1')) self.assertEqual(board.get_fen(), '8/8/8/8/8/8/8/4K3') board.put_piece(piece=King(Black), position=StandardPosition.from_str('e8')) self.assertEqual(board.get_fen(), '4k3/8/8/8/8/8/8/4K3') board.put_piece(piece=Queen(White), position=StandardPosition.from_str('d1')) board.put_piece(piece=Queen(Black), position=StandardPosition.from_str('d8')) self.assertEqual(board.get_fen(), '3qk3/8/8/8/8/8/8/3QK3') board.put_piece(piece=Rook(White), position=StandardPosition.from_str('a1')) board.put_piece(piece=Rook(White), position=StandardPosition.from_str('h1')) board.put_piece(piece=Rook(Black), position=StandardPosition.from_str('a8')) board.put_piece(piece=Rook(Black), position=StandardPosition.from_str('h8')) self.assertEqual(board.get_fen(), 'r2qk2r/8/8/8/8/8/8/R2QK2R')
def test_write_above_standard(self): pos = StandardPosition.from_str('z100') self.assertEqual(tuple(pos), (25, 99)) pos = StandardPosition.from_str('ba100') self.assertEqual(tuple(pos), (26, 99)) pos = StandardPosition.from_str('bz100') self.assertEqual(tuple(pos), (51, 99)) pos = StandardPosition.from_str('ca100') self.assertEqual(tuple(pos), (52, 99)) pos = StandardPosition.from_str('baa100') self.assertEqual(tuple(pos), (676, 99))
def test_read_above_standard(self): pos = StandardPosition((25, 99)) self.assertEqual(str(pos), 'z100') # 'b1 means the same as ab1 just like decimals - 001 == 1' pos = StandardPosition((26, 99)) self.assertEqual(str(pos), 'ba100') pos = StandardPosition((51, 99)) self.assertEqual(str(pos), 'bz100') pos = StandardPosition((52, 99)) self.assertEqual(str(pos), 'ca100') pos = StandardPosition((676, 99)) self.assertEqual(str(pos), 'baa100')
def set_fen(self, board_fen: str): # TODO: validate input string """ Sets board state from FEN :param board_fen: string, min 15 letters (ranks separated by slash) """ if self.files != 8 or self.ranks != 8: # FEN is not supported on other-sized board than 8x8 raise NotImplemented pieces_tmp: Dict['StandardPosition', 'Piece'] = {} rank_counter = self.ranks - 1 for rank in board_fen.split('/'): file_counter = 0 for piece in rank: if piece not in digits: position_object = StandardPosition( (file_counter, rank_counter)) piece_object = from_str(piece) pieces_tmp.update({position_object: piece_object}) file_counter += 1 else: for i in range(int(piece)): file_counter += 1 rank_counter -= 1 self.__pieces = pieces_tmp
def test_init_move(): a_pos = StandardPosition((0, 0)) b_pos = StandardPosition((0, 1)) move = StandardMove(source=a_pos, destination=b_pos) assert move.source == a_pos assert move.destination == b_pos assert move.promotion is None move = StandardMove(source=a_pos, destination=b_pos, promotion=Queen) assert move.source == a_pos assert move.destination == b_pos assert move.promotion == Queen with pytest.raises(ValueError, message="Source and destination should be not the same field"): StandardMove(source=StandardPosition((0, 0)), destination=StandardPosition((0, 0)))
def assert_move(self, move: 'StandardMove'): """ verify if given move is valid in current game state, proper exception is raised when needed """ source, destination = move.source, move.destination piece = self.board.get_piece(source) if not piece: raise NoPiece("Any piece on %s, you need to move pieces, not an air." % source) # check if requested destination field is in available fields generated by game logic available_dest = self.standard_moves(source) | self.standard_captures(source) | self.special_moves(source) if destination not in available_dest: raise NotAValidMove("%s is not a proper move for a %s %s\npositions available for that piece: %s" % ( move, piece.side, piece.name, ', '.join({str(pos) for pos in available_dest}))) # create test board for real validations test_board = deepcopy(self.board) test_piece = test_board.remove_piece(source) # TODO: test special moves test_board.put_piece(test_piece, destination) if isinstance(test_piece, Pawn): # TODO: here is an example of above TODO if destination == self.en_passant: test_board.remove_piece(StandardPosition((destination.file, source.rank))) # test on the copied board if move not causing any self-check king_pos, king = test_board.find_pieces(requested_piece=King(self.on_move))[0] if king_pos in self.attacked_fields_by_sides(set(self.sides) - {piece.side}, test_board): raise CausesCheck("{move} move causes {side} {name} ({pos}) check delivered by: [{atck}]".format( move=move, side=king.side, name=king.name, pos=king_pos, atck=', '.join( ["%s: %s" % (position, '%s %s' % (piece.side, piece.name)) for position, piece in self.who_can_step_here(king_pos, test_board).items()] ) )) # simple validation when promotion was declared for promoted pawn if isinstance(piece, Pawn) and destination.rank in (7, 0): if not move.promotion: raise NotAValidPromotion("Proper promotion are required when promoting a pawn")
def standard_moves(self, position: 'StandardPosition', board: StandardBoard = None) -> Set['StandardPosition']: """ return set of available moves """ if not board: board = self.board piece = board.get_piece(position) if not piece: raise NoPiece('Any piece on %s' % position) new_positions = set() for m_desc in piece.movement.move: for vector in self.__transform_vector(m_desc.vector, m_desc.any_direction, piece.side): if m_desc.distance is infinity: loop = itertools.count(1) else: loop = range(1, m_desc.distance + 1) for distance in loop: new_position = StandardPosition( (position[0] + int(vector[0] * distance), position[1] + int(vector[1] * distance)) ) if not board.validate_position(new_position): break new_piece = board.get_piece(new_position) if new_piece is not None: break new_positions.add(new_position) return new_positions
def test_white_pawn_capture_available(self): piece = Pawn(White) pos = StandardPosition.from_str('b4') self.game.board.put_piece(piece, pos) self.game.board.put_piece(Pawn(Black), StandardPosition.from_str('a5')) self.game.board.put_piece(Pawn(Black), StandardPosition.from_str('b5')) self.game.board.put_piece(Pawn(Black), StandardPosition.from_str('c5')) self.game.board.put_piece(Pawn(Black), StandardPosition.from_str('a4')) self.game.board.put_piece(Pawn(Black), StandardPosition.from_str('c4')) self.game.board.put_piece(Pawn(Black), StandardPosition.from_str('a3')) self.game.board.put_piece(Pawn(Black), StandardPosition.from_str('b3')) self.game.board.put_piece(Pawn(Black), StandardPosition.from_str('c3')) self.assertEqual(self._get_available_move_strings(pos), set()) self.assertEqual(self._get_available_capture_strings(pos), {'a5', 'c5'})
def special_moves(self, position: 'StandardPosition', board: StandardBoard = None) -> Set['StandardPosition']: """ return set of available special moves (castling and en passant) """ # TODO: replace for some more convenient solution if not board: board = self.board piece = board.get_piece(position) if not piece: raise NoPiece('Any piece on %s' % position) new_positions = set() if isinstance(piece, Pawn): new_position = None if piece == Pawn(Black) and position.rank == 6 and not board.get_piece( StandardPosition((position.file, 5))): new_position = StandardPosition( (position.file, position.rank - 2) ) elif piece == Pawn(White) and position.rank == 1 and not board.get_piece( StandardPosition((position.file, 2))): new_position = StandardPosition( (position.file, position.rank + 2) ) if new_position and board.validate_position(new_position): new_piece = board.get_piece(new_position) if new_piece is None: new_positions.add(new_position) elif isinstance(piece, King) and self.__castling and position.file == 4: attacked_fields = self.attacked_fields_by_sides(set(self.sides) - {self.on_move}) if King(self.on_move) in self.__castling: pos1 = StandardPosition((position.file + 1, position.rank)) pos2 = StandardPosition((position.file + 2, position.rank)) if not board.get_piece(pos1) and not board.get_piece(pos2) and not {pos1, pos2} & attacked_fields: new_positions.add(pos2) if Queen(self.on_move) in self.__castling: pos1 = StandardPosition((position.file - 1, position.rank)) pos2 = StandardPosition((position.file - 2, position.rank)) pos3 = StandardPosition((position.file - 2, position.rank)) if not board.get_piece(pos1) and not board.get_piece(pos2) and not board.get_piece(pos3) \ and not {pos1, pos2} & attacked_fields: new_positions.add(pos2) return new_positions
def test_init_move_from_uci(): move = StandardMove.from_str('e2e4') assert move.source == StandardPosition((4, 1)) assert move.destination == StandardPosition((4, 3)) assert move.promotion is None move = StandardMove.from_str('a7a8q') assert move.source == StandardPosition((0, 6)) assert move.destination == StandardPosition((0, 7)) assert move.promotion == Queen # cases based on wrong length bad_moves = ['a', 'e2', 'e2e', 'e2e4qq', 'sadnhjfegesj'] # edge cases, depended on proper UCI syntax and position ranges bad_moves += ['eeee', 'e234', '4123f', 'qqqqq'] for move_str in bad_moves: with pytest.raises(ValueError, message='Not a proper UCI format should be declined'): StandardMove.from_str(move_str) with pytest.raises(ValueError, message="Source and destination should be not the same field"): StandardMove.from_str('e2e2')
def move(self, move: 'StandardMove') -> Optional['Piece']: """ try to execute given move, return captured piece if not fails """ self.assert_move(move) source, destination = move.source, move.destination moved_piece = self.board.get_piece(position=source) if moved_piece.side != self.on_move: raise WrongMoveOrder("You are trying to move %s when %s are on move" % (moved_piece.side, self.on_move)) # move are accepted by the game logic, all below concerns game state (counters, history, proper move execution) self.save_history() moved_piece = self.board.remove_piece(position=source) taken_piece = self.board.put_piece(piece=moved_piece, position=destination) if isinstance(moved_piece, Pawn): if destination == self.en_passant: # check if en passant move was involved taken_piece = self.board.remove_piece(StandardPosition((destination.file, source.rank))) # check if pawn was pushed by two fields, set needed en passant position if so if abs(source.rank - destination.rank) == 2: self.__en_passant = StandardPosition( (source.file, int((source.rank + destination.rank) / 2)) ) else: # clear en passant position on any other pawn-move self.__en_passant = None self.__half_moves_since_pawn_moved = 0 if destination.rank in (7, 0): # handle promotion self.board.put_piece(move.promotion(self.on_move), destination) else: # clear en passant position on any other piece-move if self.__en_passant: self.__en_passant = None self.__half_moves_since_pawn_moved += 1 # simple and ugly check for castling execution if isinstance(moved_piece, King) and abs(source.file - destination.file) == 2: rank = source.rank if move.destination.file == 6: # king-castle moved_rook = self.board.remove_piece(position=StandardPosition((7, rank))) self.board.put_piece(moved_rook, StandardPosition((5, rank))) elif destination.file == 2: # queen-castle moved_rook = self.board.remove_piece(position=StandardPosition((0, rank))) self.board.put_piece(moved_rook, StandardPosition((3, rank))) if not taken_piece: self.__half_moves_since_capture += 1 else: self.__half_moves_since_capture = 0 self.__pocket[self.on_move].append(taken_piece) # put taken piece to the side pocket! self.__update_castling_info(source, destination) self.__position_occurence[hash(self.board)] += 1 self.moves_history.append(move) self.__half_moves += 1 return taken_piece
def test_board_get_put_remove_piece(self): board = StandardBoard() old_piece = board.put_piece(piece=Rook(White), position=StandardPosition.from_str('e4')) self.assertEqual(old_piece, None) print(board.get_piece(StandardPosition.from_str('e3'))) print(Rook(White)) self.assertNotEqual(board.get_piece(StandardPosition.from_str('e3')), Rook(White)) self.assertEqual(board.get_piece(StandardPosition.from_str('e4')), Rook(White)) old_piece = board.put_piece(piece=Queen(Black), position=StandardPosition.from_str('e4')) self.assertEqual(old_piece, Rook(White)) self.assertEqual(board.get_piece(StandardPosition.from_str('e4')), Queen(Black)) old_piece = board.remove_piece(StandardPosition.from_str('e4')) self.assertEqual(board.get_piece(StandardPosition.from_str('e4')), None) self.assertEqual(old_piece, Queen(Black))
def standard_captures(self, position: 'StandardPosition', board: StandardBoard = None) -> Set['StandardPosition']: """ return set of available captures """ if not board: board = self.board piece = board.get_piece(position) if not piece: raise NoPiece('Any piece on %s' % position) new_positions = set() if isinstance(piece, Pawn) and self.en_passant and self.en_passant in self.attacked_fields(position, board): # TODO: move to "special captures" or something new_positions.add(self.en_passant) for c_desc in piece.movement.capture: for vector in self.__transform_vector(c_desc.vector, c_desc.any_direction, piece.side): if c_desc.distance is infinity: loop = itertools.count(1) else: loop = range(1, c_desc.distance + 1) for distance in loop: new_position = StandardPosition( (position[0] + int(vector[0] * distance), position[1] + int(vector[1] * distance)) ) if not board.validate_position(new_position): break new_piece = board.get_piece(new_position) if not new_piece: continue if new_piece and new_piece.side != piece.side: new_positions.add(new_position) if c_desc.capture_break: break elif new_piece and new_piece.side == piece.side: break return new_positions
def __update_castling_info(self, source, destination): """ simply removes castling ability if given move do so """ if self.__castling: if StandardPosition.from_str('e1') in (source,): self.__castling = self.__castling - {King(White), Queen(White)} elif StandardPosition.from_str('a1') in (source, destination): self.__castling = self.__castling - {Queen(White)} elif StandardPosition.from_str('h1') in (source, destination): self.__castling = self.__castling - {King(White)} if StandardPosition.from_str('e8') in (source,): self.__castling = self.__castling - {King(Black), Queen(Black)} elif StandardPosition.from_str('a8') in (source, destination): self.__castling = self.__castling - {Queen(Black)} elif StandardPosition.from_str('h8') in (source, destination): self.__castling = self.__castling - {King(Black)}
def attacked_fields(self, position: 'StandardPosition', board: StandardBoard = None) -> Set['StandardPosition']: """ return set of attacked positions which as coming from given position (piece on that position) """ if not board: board = self.board piece = board.get_piece(position) if not piece: raise NoPiece('Any piece on %s' % position) new_positions = set() for c_desc in piece.movement.capture: for vector in self.__transform_vector(c_desc.vector, c_desc.any_direction, piece.side): if c_desc.distance is infinity: loop = itertools.count(1) else: loop = range(1, c_desc.distance + 1) for distance in loop: new_position = StandardPosition( (position[0] + int(vector[0] * distance), position[1] + int(vector[1] * distance)) ) if not board.validate_position(new_position): break new_piece = board.get_piece(new_position) if not new_piece: new_positions.add(new_position) elif new_piece and new_piece.side != piece.side: new_positions.add(new_position) break elif new_piece and new_piece.side == piece.side: break return new_positions
def test_write_fenstring(self): board = StandardBoard() board.set_fen('8/8/8/8/8/8/8/4K3') self.assertEqual( board.get_piece(position=StandardPosition.from_str('e1')), King(White)) self.assertEqual( board.get_piece(position=StandardPosition.from_str('e8')), None) board.set_fen('4k3/8/8/8/8/8/8/4K3') self.assertEqual( board.get_piece(position=StandardPosition.from_str('e1')), King(White)) self.assertEqual( board.get_piece(position=StandardPosition.from_str('e8')), King(Black)) self.assertNotEqual( board.get_piece(position=StandardPosition.from_str('e8')), King(White)) self.assertNotEqual( board.get_piece(position=StandardPosition.from_str('e8')), Rook(Black))
def move_from_str(self, move_str: str): return StandardMove( StandardPosition.from_str(move_str[:2]), StandardPosition.from_str(move_str[2:]) )
def moves_from_str(self, moves_str): for move_str in moves_str: yield StandardMove( StandardPosition.from_str(move_str[:2]), StandardPosition.from_str(move_str[-2:]) )
while True: move_str = input("Move: ") if move_str == "board": print(board_rendererer.normal(game.board)) continue elif move_str == "back": i = int(input("How many moves do you want to rollback? ")) try: game.variant.load_history(i) except IndexError: print("Given value are above of the length of moves history") continue print(board_rendererer.normal(game.board)) continue try: source = StandardPosition.from_str(move_str[0:2]) destination = StandardPosition.from_str(move_str[2:4]) promotion_char = move_str[4:] except ValueError as err: print("bad syntax (%s)" % err) continue if not game.board.validate_position(source) or not game.board.validate_position(destination): print("You give position above actual board range (%dx%d)" % game.board.size) continue if promotion_char: move = StandardMove(source=source, destination=destination, promotion=from_str(promotion_char, initialized=False)) else: move = StandardMove(source=source, destination=destination) try:
def init_board_state(self): """ Set board start position for classic chess variant """ self.board.put_piece(piece=Rook(White), position=StandardPosition((0, 0))) self.board.put_piece(piece=Rook(White), position=StandardPosition((7, 0))) self.board.put_piece(piece=Knight(White), position=StandardPosition((1, 0))) self.board.put_piece(piece=Knight(White), position=StandardPosition((6, 0))) self.board.put_piece(piece=Bishop(White), position=StandardPosition((2, 0))) self.board.put_piece(piece=Bishop(White), position=StandardPosition((5, 0))) self.board.put_piece(piece=Queen(White), position=StandardPosition((3, 0))) self.board.put_piece(piece=King(White), position=StandardPosition((4, 0))) for i in range(8): self.board.put_piece(piece=Pawn(White), position=StandardPosition((i, 1))) for i in range(8): self.board.put_piece(piece=Pawn(Black), position=StandardPosition((i, 6))) self.board.put_piece(piece=Rook(Black), position=StandardPosition((0, 7))) self.board.put_piece(piece=Rook(Black), position=StandardPosition((7, 7))) self.board.put_piece(piece=Knight(Black), position=StandardPosition((1, 7))) self.board.put_piece(piece=Knight(Black), position=StandardPosition((6, 7))) self.board.put_piece(piece=Bishop(Black), position=StandardPosition((2, 7))) self.board.put_piece(piece=Bishop(Black), position=StandardPosition((5, 7))) self.board.put_piece(piece=Queen(Black), position=StandardPosition((3, 7))) self.board.put_piece(piece=King(Black), position=StandardPosition((4, 7))) return self.board.get_fen()
def test_white_pawn_one_move(self): piece = Pawn(White) pos = StandardPosition.from_str('b3') self.game.board.put_piece(piece, pos) self.assertEqual(self._get_available_move_strings(pos), {'b4'}) self.assertEqual(self._get_available_capture_strings(pos), set())