def test_zobrist_hashing(self): game = CCGame(width=5) hasher = CCZobristHash(game) game_2 = CCGame(width=5) self.assertEqual(hasher.get_hash(game), hasher.get_hash(game_2)) game.move(2, 0, CCMovement.LS) self.assertFalse(hasher.get_hash(game) == hasher.get_hash(game_2))
def test_alpha_beta_prunning(self): """assert that using alpha-beta prunning doesn't alter the choice of movements""" game = CCGame(width=5, player_row_span=3) strat_ab = MinMaxStrategy(steps=1, alpha_beta_pruning=True) strat_no_ab = MinMaxStrategy(steps=1, alpha_beta_pruning=False) N_STEPS = 10 for _ in range(0, N_STEPS): m_ab = strat_ab.select_move(game, game.player_turn) m_no_ab = strat_no_ab.select_move(game, game.player_turn) self.assertEqual(m_ab, m_no_ab) game.apply_move_sequence(m_ab)
def test_player_1_wins(self): game = CCGame(width=5, player_row_span=3) game.board = TEST_BOARD_STRATEGY_PLAYER_1_WINS_IN_TWO strategy = MinMaxStrategy(alpha_beta_pruning=False) move, score = strategy._select_move(game, 1, 0, -100000, 100000) self.assertTrue(score > 1000) game.apply_move_sequence(move) game.rotate_turn() move, score = strategy._select_move(game, 1, 0, -100000, 100000) self.assertEqual(100000, score) game.apply_move_sequence(move) self.assertEqual(1, game.state())
def test_player_1_wins(self): game = CCGame(width=5, player_row_span=3) game.board = TEST_BOARD_STRATEGY_PLAYER_1_WINS_IN_TWO strategy = OnlyMaxStrategy(steps=1, player=1, heuristic=CombinedVerticalAdvance()) move, score = strategy._select_move(game, 0) self.assertEqual(50000, score) game.apply_move_sequence(move) game.rotate_turn() move, score = strategy._select_move(game, 0) self.assertEqual(100000, score) game.apply_move_sequence(move) self.assertEqual(1, game.state())
def _available_moves(game: CCGame, row: int, column: int, player: int, previous_moves: list = [], previous_positions: list = []) -> List[CCMove]: """ Returns a dict a list of movements that the player can make, considering the piece found at 'row' and 'column' """ moves: List[CCMove] = [] jumping = game.player_can_only_jump def undo_movement(): if game.player_turn != player: game.rotate_turn() game.undo_last_move() if not jumping and game.player_can_only_jump: # reset jumping state game.player_can_only_jump = False for movement in CCMovement: if game.can_move(row, column, movement): turn = game.move(row, column, movement) if (game.moved_to_row[-1], game.moved_to_column[-1]) in previous_positions: # we already passed through this state, avoid # infinite recursion undo_movement() continue previous_positions.append( (game.moved_to_row[-1], game.moved_to_column[-1])) previous_moves.append(movement) moves.append(CCMove([*previous_positions], [*previous_moves])) if turn == player: # turn hasn't rotated -> current piece can still jump more moves += (CCReasoner._available_moves( game, game.moved_to_row[-1], game.moved_to_column[-1], player, previous_moves, previous_positions)) previous_positions.pop() previous_moves.pop() undo_movement() return moves
def test_player_2_wins_in_one(self): game = CCGame(width=5, player_row_span=3) game.board = TEST_BOARD_STRATEGY_PLAYER_2_WINS_IN_ONE strategy = MinMaxStrategy(steps=0) game.rotate_turn() move, score = strategy._select_move(game, 2, 0, -100000, 100000) game.apply_move_sequence(move) self.assertEqual(100000, score) self.assertEqual(2, game.state())
def move(self, game: CCGame, player: int): valid_move = False while not valid_move: while not (self.gui.first_click and self.gui.second_click): self.gui.update() if not self.movement_is_valid( self.gui.first_click[0], self.gui.first_click[1], self.gui.second_click[0], self.gui.second_click[1], game, player): print('Invalid move, try again') self.gui.reset_user_input() continue # check if movement is valid! else: game._do_move(self.gui.first_click[0], self.gui.first_click[1], self.gui.second_click[0], self.gui.second_click[1]) valid_move = True self.gui.reset_user_input()
def test_transposition_table(self): """assert that using a transposition table doesn't alter the choice of movements""" game = CCGame(width=5, player_row_span=3) strat_tt = MinMaxStrategy(steps=1, alpha_beta_pruning=False, transposition_table=True) strat_no_tt = MinMaxStrategy(steps=1, alpha_beta_pruning=False, transposition_table=False) N_STEPS = 10 for _ in range(0, N_STEPS): m_tt = strat_tt.select_move(game, game.player_turn) h_tt = strat_tt.heuristic.value(game, game.player_turn) h_no_tt = strat_no_tt.heuristic.value(game, game.player_turn) self.assertAlmostEqual(h_tt, h_no_tt, 2) game.apply_move_sequence(m_tt)
def test_combined_heuristic(self): game_1 = CCGame(width=5) game_2 = CCGame(width=5) game_1.board = TEST_BOARD_VA_2_1 game_2.board = TEST_BOARD_VA_2_2 heuristic = CombinedVerticalAdvance() self.assertTrue(heuristic.value(game_1, 2) < heuristic.value(game_2, 2))
def test_combined_vertical_advance_player_1(self): game_1 = CCGame(width=5) game_2 = CCGame(width=5) game_1.board = TEST_BOARD_VA_1_1 game_2.board = TEST_BOARD_VA_1_2 heuristic = CombinedVerticalAdvance() self.assertTrue(heuristic.value(game_1, 1) < heuristic.value(game_2, 1))
def test_available_moves(self): game = CCGame(width=5) moves = CCReasoner.available_moves(game, 1) self.assertEqual(moves, [ CCMove([(1, 0), (3, 0)], [CCMovement.LS]), CCMove([(1, 0), (3, 2)], [CCMovement.RS]), CCMove([(1, 1), (3, 1)], [CCMovement.LS]), CCMove([(1, 1), (3, 3)], [CCMovement.RS]), CCMove([(2, 0), (3, 0)], [CCMovement.LS]), CCMove([(2, 0), (3, 1)], [CCMovement.RS]), CCMove([(2, 1), (3, 1)], [CCMovement.LS]), CCMove([(2, 1), (3, 2)], [CCMovement.RS]), CCMove([(2, 2), (3, 2)], [CCMovement.LS]), CCMove([(2, 2), (3, 3)], [CCMovement.RS]) ])
def compete(weights_1: list, weights_2: list): """ Plays a game between two AIs with different weights and return the result of the game. The second AI is in disadvantage (has a pure greedy lookahead). Return the number of turns it took for player 1 to beat player 2, or MAX_TURNS if player 1 didn't manage to beat player 2. """ print('{} versus greedy {}'.format(weights_1, weights_2)) heuristic_1 = OptimizedCombinedHeuristic(weights_1) heuristic_2 = OptimizedCombinedHeuristic(weights_2) game = CCGame(width=GAME_WIDTH, player_row_span=PLAYER_ROW_SPAN, visitors=[heuristic_1, heuristic_2]) strategy_1 = MinMaxStrategy(steps=LOOK_AHEAD, pre_sort_moves=True, transposition_table=True, heuristic=heuristic_1) strategy_2 = OnlyMaxStrategy(player=2, steps=0, transposition_table=True, heuristic=heuristic_2) turns = 0 last_boards: Set[CCGame] = set() board_repeats = 0 while (game.state() == 0 and turns < MAX_TURNS): strategy = (strategy_1 if game.player_turn == 1 else strategy_2) move = strategy.select_move(game, game.player_turn) game.apply_move_sequence(move) if game in last_boards: board_repeats += 1 if board_repeats == 3: # infinite loop (tie) print('Board repeats - tie') return MAX_TURNS last_boards.add(deepcopy(game)) turns += 1 state = game.state() if state == 1: print(f'Won in {turns} turns.') return min(MAX_TURNS, turns) else: print(f'Failed to beat player 2, game state: {state}') return MAX_TURNS
def test_available_moves_depth_2(self): game = CCGame(width=5) game.move(2, 0, CCMovement.RS) game.rotate_turn() moves = CCReasoner.available_moves(game, 1) self.assertTrue( CCMove([(0, 0), (2, 0), (4, 2)], [CCMovement.LS, CCMovement.RS]) in moves) self.assertTrue( CCMove([(1, 0), (3, 2), (3, 0)], [CCMovement.RS, CCMovement.L]) in moves) self.assertTrue( CCMove([(2, 2), (2, 0), (4, 2)], [CCMovement.L, CCMovement.RS]) in moves)
def test_inv_squared_sum_center_line(self): game = CCGame(width=5) game.board = TEST_BOARD_CENTER_LINE heuristic = InvSquaredSumCenterLine() self.assertTrue(heuristic.value(game, 2) < heuristic.value(game, 1))
def test_use_only_max_end_game(self): game = CCGame(width=5, player_row_span=2) game.board = TEST_BOARD_END_GAME strat = MinMaxStrategy(steps=0, alpha_beta_pruning=False) self.assertTrue(strat._use_only_max(game))
def test_use_only_max_false(self): game = CCGame(width=5, player_row_span=3) game.board = TEST_BOARD_VA_1_1 strat = MinMaxStrategy(steps=0, alpha_beta_pruning=False) self.assertFalse(strat._use_only_max(game))
def test_available_moves_player_2(self): game = CCGame(width=5) moves_1 = CCReasoner.available_moves(game, 1) game.rotate_turn() moves_2 = CCReasoner.available_moves(game, 2) self.assertEqual(len(moves_1), len(moves_2))
def _select_move(self, game: CCGame, depth: int) -> Tuple[CCMove, float]: """ Returns: tuple - position 0: best movet that can be done by the player at this level. - position 1: best heuristic value that can be achieved at this level if following best move. """ best_move, best_score = (None, -100000.0) if self.use_transposition_table and self.hasher: # transposition table business logic position_hash = self.hasher.get_hash(game) best_move, best_score, cached_depth = self.transposition_table.get( position_hash, (None, -100000.0, -1)) if best_move and cached_depth == depth: return (best_move, best_score) moves = self.available_moves(game, self.player) if game.player_turn != self.player: raise AssertionError(""" Player turn hasn't been rotated properly - this is likely a software bug """) for move in moves: if not best_move: best_move = move game.apply_move_sequence(move) # doesn't matter what the other does game.rotate_turn() # check if game has already ended if game.state() == 1: # player 1 wins # prefer winning in as few steps as possible curr_score = (100000 / (depth + 1) if self.player == 1 else -100000) elif game.state() == 2: # player 2 wins # prefer winning in as few steps as possible curr_score = (-100000 if self.player == 1 else 100000 / (depth + 1)) else: if depth == self.steps: curr_score = self.heuristic.value(game, self.player) else: curr_score = self._select_move(game, depth + 1)[1] # keep the best move that can be done at this level if (curr_score > best_score): best_score = curr_score best_move = move # undo movement for _ in range(0, len(move.directions)): game.undo_last_move() if best_move: if self.hasher: # save into transposition table self.transposition_table[position_hash] = (best_move, best_score, depth) return (best_move, best_score) else: raise AssertionError(""" No possible movements available, this must be a software bug """)
def play(board_size: int, player_row_span: int): random.seed(1) # (these weights were found running different experiments with the # weight_search.py script) oc_heuristic = OptimizedCombinedHeuristic(weights=[0.01, 0.44, 0.55]) game = CCGame(width=board_size, player_row_span=player_row_span, visitors=[oc_heuristic]) manual_players = set([2]) ai_players = { 1: MinMaxStrategy(steps=1, pre_sort_moves=True, transposition_table=True, heuristic=oc_heuristic) } start = time.time() player_turn = 1 turns = 0 gui = PygameGUI(game) manual_player = ManualPlayer(gui) ai_players_perf: Dict[int, List[float]] = {1: [], 2: []} while (game.state() == 0): print(f'Turn: {player_turn}') assert game.player_turn == player_turn gui.update() if player_turn not in manual_players: strategy = ai_players[player_turn] start = time.time() move = strategy.select_move(game, player_turn) end = time.time() ai_players_perf[player_turn].append(end - start) print(f'Move sequence: {move}') print(f'Turn {turns}') print(('Performance: ' f'{stats.describe(ai_players_perf[player_turn])}')) print(f'Heuristic values: {oc_heuristic.value(game, 1)} - ' f'{oc_heuristic.value(game, 2)}') game.apply_move_sequence(move) else: print("It's manual's player turn!") manual_player.move(game, player_turn) if game.player_turn == player_turn: game.rotate_turn() player_turn = game.player_turn turns += 1 print('..........................') print(f'PLAYER {game.state()} WINS after {turns} turns') end = time.time() print(end - start)
def _select_move(self, game: CCGame, player: int, depth: int, alpha: float, beta: float) -> Tuple[CCMove, float]: """ Returns: tuple - position 0: best movem that can be done by the player at this level. - position 1: best heuristic value that can be achieved at this level if following best move. Heuristic is negative for player 2 and position for player 1. """ if self.hasher: # transposition table business logic position_hash = self.hasher.get_hash(game) tt = (self.transposition_table_1 if player == 1 else self.transposition_table_2) best_move, best_score, cached_depth = (tt.get( position_hash, (None, -100000.0, -1))) if best_move and cached_depth == depth: return (best_move, best_score) moves = self.available_moves(game, player) if game.player_turn != player: raise AssertionError(""" Player turn hasn't been rotated properly - this is likely a software bug """) moves_queue = PriorityQueue() # type: ignore for move in moves: priority = 1 positions = move.board_positions if self.pre_sort_moves: advance = (positions[-1][0] - positions[0][0] if player == 1 else positions[0][0] - positions[-1][0]) if (self.extra_prunning and advance <= 0 and depth >= 3): # prune movements down the tree which don't bring any # extra advance continue # otherwise sort movements by vertical advance to maximize # alpha-beta pruning priority = -advance moves_queue.put(PrioritizedCCMove(priority, move)) best_move = None maximizing = depth % 2 == 0 best_score = -100000.0 if maximizing else 100000.0 while not moves_queue.empty(): move = moves_queue.get().move if not best_move: best_move = move game.apply_move_sequence(move) # check if game has already ended if game.state() == 1: # player 1 wins # prefer winning in as few steps as possible curr_score = (100000 / (depth + 1) if player == 1 else -100000) if not maximizing: curr_score = -curr_score elif game.state() == 2: # player 2 wins # prefer winning in as few steps as possible curr_score = (-100000 if player == 1 else 100000 / (depth + 1)) if not maximizing: curr_score = -curr_score else: if depth == self.steps * 2: # maximizing # approximate the score of the game by # subtracting heuristics curr_score = ( self.heuristic.value(game, player) - self.heuristic.value(game, 2 if player == 1 else 1)) else: curr_score = self._select_move(game, 2 if player == 1 else 1, depth + 1, alpha, beta)[1] # keep the best move that can be done at this level if ((maximizing and curr_score > best_score) or (not maximizing and curr_score < best_score)): best_score = curr_score best_move = move # undo movement if game.player_turn != player: game.rotate_turn() for _ in range(0, len(move.directions)): game.undo_last_move() # perform alpha-beta pruning if self.alpha_beta_pruning: if maximizing: alpha = max(alpha, best_score) else: beta = min(beta, best_score) if beta <= alpha: # alpha/beta pruning break if best_move: if self.hasher: # save into transposition table tt[position_hash] = (best_move, best_score, depth) return (best_move, best_score) else: raise AssertionError(""" No possible movements available, this must be a software bug """)
def test_combined_vertical_advance_symmetry(self): game = CCGame(width=5) heuristic = CombinedVerticalAdvance() self.assertEqual(heuristic.value(game, 1), heuristic.value(game, 2))
def test_optimized_combined_heuristic(self): """test that the optimized, stateful heuristic provides the same values as the non-optimized one""" heuristic = CombinedHeuristic() optimized_heuristic = OptimizedCombinedHeuristic() game_1 = CCGame(width=5, visitors=[optimized_heuristic]) def heuristics_agree(): for player in [1, 2]: self.assertAlmostEqual(heuristic.value(game_1, player), optimized_heuristic.value(game_1, player), 2) game_1.move(2, 1, CCMovement.LS) heuristics_agree() game_1.move(7, 1, CCMovement.RN) heuristics_agree() game_1.undo_last_move() game_1.rotate_turn() heuristics_agree() game_1.undo_last_move() heuristics_agree()
def test_inv_squared_sum_dest_corner(self): game = CCGame(width=5) game.board = TEST_BOARD_SQUARED_SUM heuristic = InvSquaredSumDestCorner() self.assertTrue(heuristic.value(game, 1) < heuristic.value(game, 2))
def inv_squared_sum_dest_corner_zero(self): game = CCGame(width=5) game.board = TEST_BOARD_SQUARED_SUM_ZERO heuristic = InvSquaredSumDestCorner() self.assertEqual(heuristic.value(game, 1), 0) self.assertEqual(heuristic.value(game, 2), 0)