def test_no_blunders_due_to_inversed_cache( self, clean_tree_search_caches_before_tests): # Here we are going to set up a test where player 2 goes second, and player 1 is about to win (he played first) # The only way to postpone an immediate loss for player 2 is to play a 4 on (2, 4). first_game = setup_game_with_forced_player_2_movement() first_game.starting_player = first_game.player_2 # This movement shouldn't matter, player 1 is about to win anyways best_move_player_1 = get_best_move(first_game.player_1, first_game.player_2, is_first_move=False, depth=1) second_game = setup_game_with_forced_player_2_movement() second_game.starting_player = first_game.player_1 best_move_player_2 = get_best_move(second_game.player_2, second_game.player_1, is_first_move=False, depth=1) assert best_move_player_2.x == 2 assert best_move_player_2.y == 4 assert best_move_player_2.piece_type == PieceType.four assert best_move_player_2.score < 0 assert best_move_player_2 != best_move_player_1
def test_obvious_first_movement(self, clean_tree_search_caches_before_tests): # Here we are going to set up a game where player 1 is about to win the game, except if # player 2 (who goes first) prevents it by placing a piece on the square player 1 needs to use # to win game = Game() game.starting_player = game.player_2 five = game.player_1.get_piece_by_type(PieceType.five) five.set_movement_direction(Direction.east) game.board.get_tile(1, 1).place_piece(five) four = game.player_1.get_piece_by_type(PieceType.four) four.set_movement_direction(Direction.west) game.board.get_tile(3, 2).place_piece(four) opponent_four = game.player_2.get_piece_by_type(PieceType.four) opponent_four.set_movement_direction(Direction.west) game.board.get_tile(1, 3).place_piece(opponent_four) best_moves, _, eval_type = get_best_moves(game.player_2, game.player_1, is_first_move=True, depth=1) assert len(best_moves) == 1 assert eval_type == EvaluationType.exact # Player 2 can only avoid losing this turn by playing anything on tile (2, 4) best_move = get_best_move(game.player_2, game.player_1, is_first_move=True, depth=1) assert best_move.x == 2 assert best_move.y == 4 # Should be true for any depth level (using 2 for test speed reasons) best_move_depth_2 = get_best_move(game.player_2, game.player_1, is_first_move=True, depth=2) assert best_move_depth_2.x == 2 assert best_move_depth_2.y == 4
def test_player_must_use_four_piece(self, clean_tree_search_caches_before_tests): game = Game() game.starting_player = game.player_2 five = game.player_2.get_piece_by_type(PieceType.five) five.set_movement_direction(Direction.south) game.board.get_tile(2, 3).place_piece(five) two = game.player_2.get_piece_by_type(PieceType.two) two.set_movement_direction(Direction.south) game.board.get_tile(3, 3).place_piece(two) three = game.player_2.get_piece_by_type(PieceType.three) three.set_movement_direction(Direction.north) game.board.get_tile(1, 1).place_piece(three) player_two = game.player_1.get_piece_by_type(PieceType.two) player_two.set_movement_direction(Direction.west) game.board.get_tile(1, 3).place_piece(player_two) # Player 1 can only avoid losing this turn by player the four piece on either # the (2, 0) or the (2, 4) tile or on the (0, 3) tile best_move = get_best_move(game.player_1, game.player_2, is_first_move=False, depth=1) assert (best_move.x == 2 and best_move.y == 0) or\ (best_move.x == 2 and best_move.y == 4) or\ (best_move.x == 0 and best_move.y == 3) assert best_move.piece_type == PieceType.four
def test_player_has_no_legal_moves(self, clean_tree_search_caches_before_tests): game = Game() game.starting_player = game.player_2 one = game.player_1.get_piece_by_type(PieceType.one) one.set_movement_direction(Direction.north) game.board.get_tile(3, 3).place_piece(one) two = game.player_1.get_piece_by_type(PieceType.two) two.set_movement_direction(Direction.east) game.board.get_tile(3, 2).place_piece(two) three = game.player_1.get_piece_by_type(PieceType.three) three.set_movement_direction(Direction.north) game.board.get_tile(1, 3).place_piece(three) four = game.player_1.get_piece_by_type(PieceType.four) four.set_movement_direction(Direction.north) game.board.get_tile(2, 0).place_piece(four) five = game.player_1.get_piece_by_type(PieceType.five) five.set_movement_direction(Direction.south) game.board.get_tile(2, 2).place_piece(five) best_move = get_best_move(game.player_1, game.player_2, is_first_move=False, depth=1) assert best_move.to_placement_move(game.player_1) is None assert best_move.score > 0
def test_player_must_use_last_piece(self, clean_tree_search_caches_before_tests): # Here we set up a scenario where player 1 can win by playing his last available piece # If he plays a 3 at (2, 4), he wins game = Game() game.starting_player = game.player_2 one = game.player_1.get_piece_by_type(PieceType.one) one.set_movement_direction(Direction.north) game.board.get_tile(3, 3).place_piece(one) two = game.player_1.get_piece_by_type(PieceType.two) two.set_movement_direction(Direction.east) game.board.get_tile(3, 2).place_piece(two) four = game.player_1.get_piece_by_type(PieceType.four) four.set_movement_direction(Direction.north) game.board.get_tile(2, 0).place_piece(four) five = game.player_1.get_piece_by_type(PieceType.five) five.set_movement_direction(Direction.south) game.board.get_tile(2, 2).place_piece(five) enemy_two = game.player_2.get_piece_by_type(PieceType.two) enemy_two.set_movement_direction(Direction.west) game.board.get_tile(4, 1).place_piece(enemy_two) best_move = get_best_move(game.player_1, game.player_2, is_first_move=False, depth=1) assert best_move is not None assert best_move.piece_type == PieceType.three assert best_move.x == 2 assert best_move.y == 4
def test_cached_movement_recovery(self, clean_tree_search_caches_before_tests): game = Game() game.starting_player = game.player_1 five = game.player_1.get_piece_by_type(PieceType.five) five.set_movement_direction(Direction.west) game.board.get_tile(3, 1).place_piece(five) four = game.player_2.get_piece_by_type(PieceType.four) four.set_movement_direction(Direction.west) game.board.get_tile(3, 2).place_piece(four) one = game.player_2.get_piece_by_type(PieceType.one) one.set_movement_direction(Direction.north) game.board.get_tile(1, 3).place_piece(one) best_move = get_best_move(game.player_1, game.player_2, is_first_move=True, depth=1) cached_moves, cached_score, cached_eval_type = get_best_move_from_transposition_dict( game.player_1, 1, False, 0, 0) assert cached_moves is not None assert best_move in cached_moves assert cached_eval_type == EvaluationType.exact best_opponent_move = get_best_move(game.player_2, game.player_1, is_first_move=True, depth=1) assert best_move != best_opponent_move cached_moves_p2, cached_score_p2, cached_eval_type_p2 = get_best_move_from_transposition_dict( game.player_2, 1, False, 0, 0) assert best_opponent_move in cached_moves_p2
def test_obvious_second_placement_move(self, clean_tree_search_caches_before_tests): # Here we are going to set up a game where player 1 has a single option to win immediately, # and he's going to play second (he doesn't need to account for a move of the opponent before the # board execution) game = Game() game.starting_player = game.player_2 PlacementMove( piece=game.player_1.get_piece_by_type(PieceType.four), tile=game.board.get_tile(0, 2), board=game.board, ).execute() PlacementMove( piece=game.player_1.get_piece_by_type(PieceType.three), tile=game.board.get_tile(0, 3), board=game.board, ).execute() game.board.execute_board_movements(PLAYER_1_ID) game.board.execute_board_movements(PLAYER_1_ID) PlacementMove( piece=game.player_1.get_piece_by_type(PieceType.two), tile=game.board.get_tile(0, 1), board=game.board, ).execute() PlacementMove( piece=game.player_1.get_piece_by_type(PieceType.five), tile=game.board.get_tile(4, 1), board=game.board, ).execute() game.board.execute_board_movements(PLAYER_1_ID) game.starting_player = game.player_2 PlacementMove( piece=game.player_2.get_piece_by_type(PieceType.five), tile=game.board.get_tile(4, 3), board=game.board, ).execute() # Player 1 can win the game by placing a one in tile (2, 0) or a three/four on (3, 0) # But he only has piece one available in hand best_move = get_best_move(game.player_1, game.player_2, is_first_move=False, depth=1) assert best_move.x == 2 assert best_move.y == 0 assert best_move.piece_type == PieceType.one assert best_move.score == WIN_CONDITION_SCORE - DEPTH_PENALTY
def test_transposition_does_not_use_wrong_alphabeta_cutoffs( self, clean_tree_search_caches_before_tests): game = Game() game.starting_player = game.player_2 game.first_move_executed = True one_p1 = game.player_1.get_piece_by_type(PieceType.one) one_p1.set_movement_direction(Direction.south) game.board.get_tile(2, 3).place_piece(one_p1) one_p2 = game.player_2.get_piece_by_type(PieceType.one) one_p2.set_movement_direction(Direction.west) game.board.get_tile(3, 3).place_piece(one_p2) five_p2 = game.player_2.get_piece_by_type(PieceType.five) five_p2.set_movement_direction(Direction.west) game.board.get_tile(4, 2).place_piece(five_p2) best_move_p1_r1 = get_best_move(game.player_1, game.player_2, is_first_move=False, depth=2) assert best_move_p1_r1.piece_type != PieceType.four or best_move_p1_r1.x != 0 or best_move_p1_r1.y != 3 # We ignore the correct movement and play a losing move (4 piece to (0, 3)) four_p1 = game.player_1.get_piece_by_type(PieceType.four) four_p1.set_movement_direction(Direction.east) game.board.get_tile(0, 3).place_piece(four_p1) game.board.execute_board_movements(game.player_2.id) game.switch_starting_player() _, score, eval_type = get_best_moves(game.player_1, game.player_2, is_first_move=True, depth=2) assert score == -WIN_CONDITION_SCORE + DEPTH_PENALTY assert eval_type == EvaluationType.exact
def test_game_is_lost_on_depth_2_alternative(self, clean_tree_search_caches_before_tests): game = Game() game.starting_player = game.player_2 three = game.player_1.get_piece_by_type(PieceType.three) three.set_movement_direction(Direction.east) game.board.get_tile(2, 1).place_piece(three) four = game.player_1.get_piece_by_type(PieceType.four) four.set_movement_direction(Direction.south) game.board.get_tile(2, 3).place_piece(four) five = game.player_1.get_piece_by_type(PieceType.five) five.set_movement_direction(Direction.south) game.board.get_tile(1, 2).place_piece(five) four_p2 = game.player_2.get_piece_by_type(PieceType.four) four_p2.set_movement_direction(Direction.west) game.board.get_tile(3, 2).place_piece(four_p2) five_p2 = game.player_2.get_piece_by_type(PieceType.five) five_p2.set_movement_direction(Direction.west) game.board.get_tile(2, 2).place_piece(five_p2) # At this position, player 2 is losing at depth 2. He can avoid losing this round by playing either: # 1) The 1 piece at (0, 3) or (1, 4) # 2) the 2 or 3 pieces at (3, 0) or (4, 1), # but next round he will be unable to block both squares where P1 can mate, and will not have the 4 in hand best_moves_p2_t1, score, eval_type = get_best_moves(game.player_2, game.player_1, is_first_move=True, depth=2) assert 6 >= len(best_moves_p2_t1) >= 1 assert eval_type == EvaluationType.exact best_move_p2 = get_best_move(game.player_2, game.player_1, is_first_move=True, depth=2) assert (best_move_p2.x == 0 and best_move_p2.y == 3 and best_move_p2.piece_type == PieceType.one) or \ (best_move_p2.x == 1 and best_move_p2.y == 4 and best_move_p2.piece_type == PieceType.one) or \ (best_move_p2.x == 3 and best_move_p2.y == 0 and best_move_p2.piece_type in [PieceType.two, PieceType.three]) or \ (best_move_p2.x == 4 and best_move_p2.y == 1 and best_move_p2.piece_type in [PieceType.two, PieceType.three]) assert best_move_p2.score == -WIN_CONDITION_SCORE + DEPTH_PENALTY * 2
def test_hopeless_situation(self, clean_tree_search_caches_before_tests): # In this test we are going to set up a situation where player_1, moving second # cannot postpone his loss regardless of what he plays game = Game() game.starting_player = game.player_2 five = game.player_2.get_piece_by_type(PieceType.five) five.set_movement_direction(Direction.west) game.board.get_tile(3, 2).place_piece(five) two = game.player_2.get_piece_by_type(PieceType.two) two.set_movement_direction(Direction.west) game.board.get_tile(3, 3).place_piece(two) four = game.player_2.get_piece_by_type(PieceType.four) four.set_movement_direction(Direction.east) game.board.get_tile(1, 1).place_piece(four) three = game.player_2.get_piece_by_type(PieceType.three) three.set_movement_direction(Direction.north) game.board.get_tile(1, 0).place_piece(three) one = game.player_2.get_piece_by_type(PieceType.one) one.set_movement_direction(Direction.south) game.board.get_tile(3, 4).place_piece(one) opponent_four = game.player_1.get_piece_by_type(PieceType.four) opponent_four.set_movement_direction(Direction.east) game.board.get_tile(3, 1).place_piece(opponent_four) # Player 1 cannot do anything, there are 2 winning rows coming into play (center vertical line and diagonal) # and he can only prevent the diagonal. # Setting the depth to a ridiculous number should not make this test run forever, as on depth 1 it should # already return a score for all movements best_move = get_best_move(game.player_1, game.player_2, is_first_move=False, depth=100) assert best_move.score == -WIN_CONDITION_SCORE + DEPTH_PENALTY
def test_game_is_lost_on_depth_2(self, clean_tree_search_caches_before_tests): game = Game() game.starting_player = game.player_1 two = game.player_1.get_piece_by_type(PieceType.two) two.set_movement_direction(Direction.south) game.board.get_tile(2, 3).place_piece(two) three = game.player_1.get_piece_by_type(PieceType.three) three.set_movement_direction(Direction.east) game.board.get_tile(1, 2).place_piece(three) four = game.player_1.get_piece_by_type(PieceType.four) four.set_movement_direction(Direction.east) game.board.get_tile(0, 2).place_piece(four) one_p2 = game.player_2.get_piece_by_type(PieceType.one) one_p2.set_movement_direction(Direction.north) game.board.get_tile(2, 2).place_piece(one_p2) two_p2 = game.player_2.get_piece_by_type(PieceType.two) two_p2.set_movement_direction(Direction.west) game.board.get_tile(3, 2).place_piece(two_p2) # At this position, player 2 is completely losing at depth 2 # He can avoid losing this round by playing the 4 piece at (2, 0), but next round he will # be unable to block both squares where P1 can mate, and will not have the 4 in hand best_moves_p2_t1, score_p2_t1, eval_type_p2_t1 = get_best_moves(game.player_2, game.player_1, is_first_move=False, depth=2) assert len(best_moves_p2_t1) == 1 assert eval_type_p2_t1 == EvaluationType.exact best_move_p2 = get_best_move(game.player_2, game.player_1, is_first_move=False, depth=2) assert best_move_p2.x == 2 and best_move_p2.y == 0 assert best_move_p2.piece_type == PieceType.four assert best_move_p2.score == score_p2_t1 == -WIN_CONDITION_SCORE + DEPTH_PENALTY * 2
def test_black_widow(self, clean_tree_search_caches_before_tests): # The following situation is hard to see for a human, but completely winning for player 2 # He only has piece 5 in hand, and as long as he keeps it for the next move (makes it not enter the board) # He will be able to use it next round to win the game, no reply from player 1 possible. game = Game() game.starting_player = game.player_1 # Player 1 pieces player_1_one = game.player_1.get_piece_by_type(PieceType.one) player_1_one.set_movement_direction(Direction.east) game.board.get_tile(2, 3).place_piece(player_1_one) player_1_two = game.player_1.get_piece_by_type(PieceType.two) player_1_two.set_movement_direction(Direction.west) game.board.get_tile(3, 2).place_piece(player_1_two) player_1_four = game.player_1.get_piece_by_type(PieceType.four) player_1_four.set_movement_direction(Direction.east) game.board.get_tile(1, 3).place_piece(player_1_four) player_1_five = game.player_1.get_piece_by_type(PieceType.five) player_1_five.set_movement_direction(Direction.east) game.board.get_tile(1, 1).place_piece(player_1_five) player_1_three = game.player_1.get_piece_by_type(PieceType.three) player_1_three.set_movement_direction(Direction.east) game.board.get_tile(0, 3).place_piece(player_1_three) # Player 2 pieces player_2_one = game.player_2.get_piece_by_type(PieceType.one) player_2_one.set_movement_direction(Direction.north) game.board.get_tile(2, 1).place_piece(player_2_one) player_2_two = game.player_2.get_piece_by_type(PieceType.two) player_2_two.set_movement_direction(Direction.west) game.board.get_tile(3, 1).place_piece(player_2_two) player_2_three = game.player_2.get_piece_by_type(PieceType.three) player_2_three.set_movement_direction(Direction.west) game.board.get_tile(3, 3).place_piece(player_2_three) player_2_four = game.player_2.get_piece_by_type(PieceType.four) player_2_four.set_movement_direction(Direction.east) game.board.get_tile(2, 2).place_piece(player_2_four) best_moves_player_2_round_1, player_2_round_1_score, _ = get_best_moves(game.player_2, game.player_1, is_first_move=False, depth=2) for candidate_move in best_moves_player_2_round_1: clone_game = game.clone() candidate_move.execute(clone_game.player_2) clone_game.board.execute_board_movements(starting_player_id=clone_game.player_1.id) clone_game.switch_starting_player() # Play the second round clone_move_p2_r2 = get_best_move(clone_game.player_2, clone_game.player_1, is_first_move=True, depth=2) clone_move_p2_r2.execute(clone_game.player_2) clone_move_p1_r2 = get_best_move(clone_game.player_1, clone_game.player_2, is_first_move=False, depth=2) clone_move_p1_r2.execute(clone_game.player_1) clone_game.board.execute_board_movements(starting_player_id=clone_game.player_2.id) assert clone_game.board.get_game_result(clone_game.player_2.id, clone_game.player_1.id) == GameResult.win assert clone_move_p1_r2.score == -WIN_CONDITION_SCORE + DEPTH_PENALTY assert clone_move_p2_r2.score == WIN_CONDITION_SCORE - DEPTH_PENALTY # Play the first round best_move_player_2_round_1 = get_best_move(game.player_2, game.player_1, is_first_move=False, depth=2) best_move_player_2_round_1.execute(game.player_2) game.board.execute_board_movements(starting_player_id=game.player_1.id) game.switch_starting_player() best_moves_player_2_round_2, _, _ = get_best_moves(game.player_2, game.player_1, is_first_move=True, depth=2) # Play the second round best_move_player_2_round_2 = get_best_move(game.player_2, game.player_1, is_first_move=True, depth=2) best_move_player_2_round_2.execute(game.player_2) best_move_player_1_round_2 = get_best_move(game.player_1, game.player_2, is_first_move=False, depth=2) best_move_player_1_round_2.execute(game.player_1) game.board.execute_board_movements(starting_player_id=game.player_2.id) # On the first round, player 2 must play the 5 piece (forced, only piece in hand) on any tile where it cannot # enter the board, as he needs it to mate in the next round. tiles_where_5_would_enter = [(0, 2), (1, 4), (0, 3), (2, 0)] for candidate_move in best_moves_player_2_round_1: assert candidate_move.piece_type == PieceType.five assert (candidate_move.x, candidate_move.y) not in tiles_where_5_would_enter assert best_move_player_2_round_1.piece_type == PieceType.five assert (best_move_player_2_round_1.x, best_move_player_2_round_1.y) not in tiles_where_5_would_enter # On the second round, player 2 should play the 5 on (2, 4), as its the winning move assert len(best_moves_player_2_round_2) == 1 assert best_move_player_2_round_2.piece_type == PieceType.five assert best_move_player_2_round_2.x == 2 and best_move_player_2_round_2.y == 4 # Both players should know at any moment they have already won/lost assert best_move_player_2_round_1.score == player_2_round_1_score == WIN_CONDITION_SCORE - DEPTH_PENALTY * 2 assert best_move_player_1_round_2.score == -WIN_CONDITION_SCORE + DEPTH_PENALTY assert best_move_player_2_round_2.score == WIN_CONDITION_SCORE - DEPTH_PENALTY assert game.board.get_game_result(game.player_2.id, game.player_1.id) == GameResult.win