def test_init(self): cf = ConnectFour() self.assertEqual(cf.get_number_of_discs(), 0) self.assertEqual(cf.get_current_player(), 0) self.assertIsNone(cf.get_winner()) self.assertEqual(ConnectFour.width, 7) self.assertEqual(ConnectFour.height, 6)
def string_list_to_connect_four(string_list): symbol_to_player = {"X": 0, "O": 1} cf = ConnectFour() for y in range(ConnectFour.height): line = string_list[ConnectFour.height - 1 - y] for x in range(ConnectFour.width): symbol = line[x] try: cf.discs[x][y] = symbol_to_player[symbol] except KeyError: pass return cf
def count_unique_states_at_each_depth(db): heuristic_class = Heuristic100 state_to_node_prev_depth = {} state_to_node = {} depth = None for row in db.get_fringe(): state = row[0] depth = row[1] + 1 cf = ConnectFour.create_from_string(state) node = SearchNode(cf, heuristic_class) state_to_node_prev_depth[state] = node if depth is None: depth = 0 else: print("Fetched fringe from database. Depth={d} States={s}".format( d=depth - 1, s=len(state_to_node_prev_depth))) while depth <= 6: n_states = 0 if depth == 0: node = SearchNode(ConnectFour(), heuristic_class) state_to_node = {node.get_state(): node} else: for node in state_to_node_prev_depth.values(): for successor in node.get_successors(): n_states += 1 state = successor.get_state() state_to_node[state] = successor n_solved_states = 0 for (state, node) in state_to_node.items(): if abs(node.get_heuristic_value() ) >= Heuristic.heuristic_value_win_threshold: n_solved_states += 1 db.set_value_of_state(state, node.get_heuristic_value()) print( "Depth {d} States: {s:,} Unique states: {us:,} ({ss:,} solved) ({max:,} max)" .format(d=depth, s=n_states, us=len(state_to_node), ss=n_solved_states, max=7**depth)) unsolved_states = [ state for (state, node) in state_to_node.items() if abs(node.get_heuristic_value()) < Heuristic.heuristic_value_win_threshold ] db.add_fringe(unsolved_states, depth) state_to_node_prev_depth = state_to_node state_to_node = {} depth += 1
def test_play_game(self): # ....... # ....... # ..?.... # ..X.... # ..XOOO. # .OXXXO. positions = [(2, 0), (1, 0), (3, 0), (3, 1), (4, 0), (5, 0), (2, 1), (4, 1), (2, 2), (5, 1)] cf = ConnectFour() for i in range(len(positions)): (x, y) = positions[i] cf.place_disc(x) self.assertEqual(cf.get_number_of_discs(), i + 1) self.assertEqual(cf.discs[x][y], i % 2) self.assertEqual(cf.get_current_player(), (i + 1) % 2) self.assertIsNone(cf.get_winner()) cf.place_disc(2) self.assertEqual(cf.get_winner(), 0)
def print_database(db): cursor = db.execute_sql("select * from connectfour") i = 0 for (state, value, move) in cursor: # @UnusedVariable print("Row:", i, "State:", state, "Value:", value) cf = ConnectFour.create_from_string(state) print(cf.to_human_readable_string()) i += 1 if i == 1000: break
def do_alphabeta(db): initial_node = SearchNode(ConnectFour(), Heuristic100) max_depth = 6 for depth in range(1, max_depth + 1): #alphabeta = AlphaBeta(db_connection=db) alphabeta = AlphaBeta() (node, value) = alphabeta.alphabeta(initial_node, depth, True) print( "Depth: {d} Nodes created: {c} Nodes evaluated: {e} Best move value: {v}" .format(d=depth, c=alphabeta.n_created_nodes, e=alphabeta.n_evaluated_nodes, v=value)) print(node.cf.to_human_readable_string())
def play_game(db): cf = ConnectFour() while not cf.is_game_over(): move = get_next_move(cf) cf = ConnectFour(cf, move) print("Best move:", move) print("New game state:") print(cf.to_human_readable_string()) winner = cf.get_winner() if winner is not None: print(winner, "wins") else: print("It's a tie")
def get_successors(self): if self.cf.get_winner() is not None: return [] successors = [] for i in range(ConnectFour.width): try: successor_cf = ConnectFour(self.cf, i) successors.append( SearchNode(successor_cf, self.heuristic_class)) except ValueError: pass random.shuffle(successors) successors.sort(key=lambda x: x.get_heuristic_value(), reverse=self.cf.get_current_player() == 0) return successors
def test_create_from_string(self): # ....... # ....... # ..X.... # ..X.... # ..XOOO. # .OXXXO. cf_as_string = "01031006060701" cf = ConnectFour.create_from_string(cf_as_string) self.assertEqual(cf.get_number_of_discs(), 11) self.assertEqual(cf.discs[1][0], 1) self.assertEqual(cf.discs[2][3], 0) self.assertIsNone(cf.discs[3][2]) self.assertEqual(cf.get_current_player(), 1) self.assertEqual(cf.get_winner(), 0) self.assertEqual(cf.to_string(), cf_as_string)
def test_place_disc(self): cf = ConnectFour() for i in range(ConnectFour.height): # @UnusedVariable cf.place_disc(4) cf.place_disc(3) try: cf.place_disc(4) self.fail("Expected ValueError") except ValueError: pass try: cf.place_disc(-1) self.fail("Expected ValueError") except ValueError: pass try: cf.place_disc(ConnectFour.width) self.fail("Expected ValueError") except ValueError: pass
def get_heuristic_value(self): winner = self.cf.get_winner() if winner is not None: return self.get_heuristic_value_for_win(winner, 0) if self.cf.get_number_of_discs() == ConnectFour.width * ConnectFour.height: return 0 threats = self.get_threats() immediate_threats = [self.get_immediate_threats(x) for x in threats] current_player = self.cf.get_current_player() opponent = (current_player+1) % 2 # If current player has an immediate threat, current player will win in 1 move. if len(immediate_threats[current_player]) > 0: #print("Current player has an immediate threat. Will win in 1 move.") return self.get_heuristic_value_for_win(current_player, 1) # If opponent has more than one immediate threat, opponent will win in 2 moves. if len(immediate_threats[opponent]) > 1: #print("Opponent has more than 1 immediate threat and will win in 2 moves.") return self.get_heuristic_value_for_win(opponent, 2) # If opponent has exactly one immediate threat, place a disc in that column, # and get heuristic value for that state. if len(immediate_threats[opponent]) == 1: #print("Opponent has 1 immediate threat. Return the heuristic value of the " \ # + "state resulting from placing a disc in that column.") cf_successor = ConnectFour(self.cf) column_of_threat = next(iter(immediate_threats[opponent]))[0] cf_successor.place_disc(column_of_threat) return Heuristic100(cf_successor).get_heuristic_value() if all(len(x) == 0 for x in threats): # Neither player has any threats. # Player with the most central discs has advantage. return sum([ abs(ConnectFour.width//2 - x) * (-1 if self.cf.discs[x][y] == 0 else 1) for x in range(ConnectFour.width) for y in range(ConnectFour.height) if self.cf.discs[x][y] is not None ]) # Determine which player has advantage, based on zugzwang. # Which positions are playable for both players? # Take turns and place disc in those positions as long as possible. # After that, it's either: # - Player has to play a disc where opponent has an active threat (and loses) # - Player has to sacrifice own threat open_positions = self.get_open_positions() while True: if len(open_positions) == 0: return 100 * (len(threats[0]) - len(threats[1])) playable_positions = [] for player in [0, 1]: playable_positions.append({ (x,y) for (x,y) in open_positions if (y == 0 or (x,y-1) not in open_positions) \ and (x,y+1) not in threats[(player+1)%2] }) if len(playable_positions[current_player]) == 0: return 1000 * (-1 if current_player == 0 else 1) playable_positions_common = playable_positions[0] & playable_positions[1] if len(playable_positions_common) > 1: open_positions.remove(playable_positions_common.pop()) current_player = (current_player+1) % 2 continue # Current player has to sacrifice own threat. # Look for position that changes zugzwang. zugswang_position = next(( (x,y) for (x,y) in playable_positions[current_player] if (ConnectFour.height-y) % 2 == 0 ), None) if zugswang_position is not None: open_positions.remove(zugswang_position) current_player = (current_player+1) % 2 continue open_positions.remove(playable_positions[current_player].pop()) current_player = (current_player+1) % 2