def get_player_expandable_nodes(game: Game, color: Color): node_sets = game.state.board.find_connected_components(color) enemy_colors = [ enemy_color for enemy_color in game.state.colors if enemy_color != color ] enemy_node_ids = set() for enemy_color in enemy_colors: enemy_node_ids.update( get_player_buildings(game.state, enemy_color, BuildingType.SETTLEMENT)) enemy_node_ids.update( get_player_buildings(game.state, enemy_color, BuildingType.CITY)) expandable_node_ids = [ node_id for node_set in node_sets for node_id in node_set if node_id not in enemy_node_ids # not plowed ] # not exactly "buildable_node_ids" b.c. we could expand from non-buildable nodes return expandable_node_ids
def prune_robber_actions(current_color, game, actions): """Eliminate all but the most impactful tile""" enemy_color = next(filter(lambda c: c != current_color, game.state.colors)) enemy_owned_tiles = set() for node_id in get_player_buildings(game.state, enemy_color, BuildingType.SETTLEMENT): enemy_owned_tiles.update(game.state.board.map.adjacent_tiles[node_id]) for node_id in get_player_buildings(game.state, enemy_color, BuildingType.CITY): enemy_owned_tiles.update(game.state.board.map.adjacent_tiles[node_id]) robber_moves = set( filter( lambda a: a.action_type == ActionType.MOVE_ROBBER and game.state. board.map.tiles[a.value[0]] in enemy_owned_tiles, actions, )) production_features = build_production_features(True) def impact(action): game_copy = game.copy() game_copy.execute(action) our_production_sample = production_features(game_copy, current_color) enemy_production_sample = production_features(game_copy, current_color) production = value_production(our_production_sample, "P0") enemy_production = value_production(enemy_production_sample, "P1") return enemy_production - production most_impactful_robber_action = max( robber_moves, key=impact) # most production and variety producing actions = filter( # lambda a: a.action_type != action_type or a == most_impactful_robber_action, lambda a: a.action_type != ActionType.MOVE_ROBBER or a in robber_moves, actions, ) return actions
def after(self, game: Game): winner = game.winning_color() if winner is None: return # throw away data for color in game.state.colors: cities = len( get_player_buildings(game.state, color, BuildingType.CITY)) settlements = len( get_player_buildings(game.state, color, BuildingType.SETTLEMENT)) longest = get_longest_road_color(game.state) == color largest = get_largest_army(game.state)[0] == color devvps = get_dev_cards_in_hand(game.state, color, VICTORY_POINT) self.cities[color] += cities self.settlements[color] += settlements self.longest[color] += longest self.largest[color] += largest self.devvps[color] += devvps self.num_games += 1
def test_play_many_games(): for _ in range(10): # play 10 games players = [ RandomPlayer(Color.RED), RandomPlayer(Color.BLUE), RandomPlayer(Color.WHITE), RandomPlayer(Color.ORANGE), ] game = Game(players) game.play() # Assert everything looks good for color in game.state.colors: cities = len( get_player_buildings(game.state, color, BuildingType.CITY)) settlements = len( get_player_buildings(game.state, color, BuildingType.SETTLEMENT)) longest = get_longest_road_color(game.state) == color largest = get_largest_army(game.state)[0] == color devvps = get_dev_cards_in_hand(game.state, color, VICTORY_POINT) assert (settlements + 2 * cities + 2 * longest + 2 * largest + devvps) == get_actual_victory_points(game.state, color)
def city_possibilities(state, color) -> List[Action]: key = player_key(state, color) has_money = player_resource_freqdeck_contains(state, color, CITY_COST_FREQDECK) has_cities_available = state.player_state[f"{key}_CITIES_AVAILABLE"] > 0 if has_money and has_cities_available: return [ Action(color, ActionType.BUILD_CITY, node_id) for node_id in get_player_buildings( state, color, BuildingType.SETTLEMENT) ] else: return []
def production_features(game: Game, p0_color: Color): # P0_WHEAT_PRODUCTION, P0_ORE_PRODUCTION, ..., P1_WHEAT_PRODUCTION, ... features = {} board = game.state.board robbed_nodes = set( board.map.tiles[board.robber_coordinate].nodes.values()) for resource in RESOURCES: for i, color in iter_players(game.state.colors, p0_color): production = 0 for node_id in get_player_buildings(game.state, color, BuildingType.SETTLEMENT): if consider_robber and node_id in robbed_nodes: continue production += get_node_production(game.state.board, node_id, resource) for node_id in get_player_buildings(game.state, color, BuildingType.CITY): if consider_robber and node_id in robbed_nodes: continue production += 2 * get_node_production( game.state.board, node_id, resource) features[f"{prefix}P{i}_{resource}_PRODUCTION"] = production return features
def expansion_features(game: Game, p0_color: Color): global STATIC_GRAPH MAX_EXPANSION_DISTANCE = 3 # exclusive features = {} # For each connected component node, bfs_edges (skipping enemy edges and nodes nodes) empty_edges = set(get_edges(game.state.board.map.land_nodes)) for i, color in iter_players(game.state.colors, p0_color): empty_edges.difference_update( get_player_buildings(game.state, color, BuildingType.ROAD)) searchable_subgraph = STATIC_GRAPH.edge_subgraph(empty_edges) board_buildable_node_ids = game.state.board.buildable_node_ids( p0_color, True ) # this should be the same for all players. TODO: Can maintain internally (instead of re-compute). for i, color in iter_players(game.state.colors, p0_color): expandable_node_ids = get_player_expandable_nodes(game, color) def skip_blocked_by_enemy(neighbor_ids): for node_id in neighbor_ids: node_color = game.state.board.get_node_color(node_id) if node_color is None or node_color == color: yield node_id # not owned by enemy, can explore # owned_edges = get_player_buildings(state, color, BuildingType.ROAD) dis_res_prod = { distance: {k: 0 for k in RESOURCES} for distance in range(MAX_EXPANSION_DISTANCE) } for node_id in expandable_node_ids: if node_id in board_buildable_node_ids: # node itself is buildable for resource in RESOURCES: production = get_node_production(game.state.board, node_id, resource) dis_res_prod[0][resource] = max(production, dis_res_prod[0][resource]) if node_id not in searchable_subgraph.nodes(): continue # must be internal node, no need to explore bfs_iteration = nx.bfs_edges( searchable_subgraph, node_id, depth_limit=MAX_EXPANSION_DISTANCE - 1, sort_neighbors=skip_blocked_by_enemy, ) paths = {node_id: []} for edge in bfs_iteration: a, b = edge path_until_now = paths[a] distance = len(path_until_now) + 1 paths[b] = paths[a] + [b] if b not in board_buildable_node_ids: continue # means we can get to node b, at distance=d, starting from path[0] for resource in RESOURCES: production = get_node_production(game.state.board, b, resource) dis_res_prod[distance][resource] = max( production, dis_res_prod[distance][resource]) for distance, res_prod in dis_res_prod.items(): for resource, prod in res_prod.items(): features[f"P{i}_{resource}_AT_DISTANCE_{int(distance)}"] = prod return features
def get_owned_or_buildable(game, color, board_buildable): return frozenset( get_player_buildings(game.state, color, BuildingType.SETTLEMENT) + get_player_buildings(game.state, color, BuildingType.CITY) + board_buildable)
def create_board_tensor(game: Game, p0_color: Color): """Creates a tensor of shape (WIDTH=21, HEIGHT=11, CHANNELS). 1 x n hot-encoded planes (2 and 1s for city/settlements). 1 x n planes for the roads built by each player. 5 tile resources planes, one per resource. 1 robber plane (to note nodes blocked by robber). 6 port planes (one for each resource and one for the 3:1 ports) Example: - To see WHEAT plane: tf.transpose(board_tensor[:,:,3]) """ # add 4 hot-encoded color multiplier planes (nodes), and 4 edge planes. 8 planes color_multiplier_planes = [] node_map, edge_map = get_node_and_edge_maps() for _, color in iter_players(tuple(game.state.colors), p0_color): node_plane = tf.zeros((WIDTH, HEIGHT)) edge_plane = tf.zeros((WIDTH, HEIGHT)) indices = [] updates = [] for node_id in get_player_buildings(game.state, color, BuildingType.SETTLEMENT): indices.append(node_map[node_id]) updates.append(1) for node_id in get_player_buildings(game.state, color, BuildingType.CITY): indices.append(node_map[node_id]) updates.append(2) if len(indices) > 0: node_plane = tf.tensor_scatter_nd_update(node_plane, indices, updates) indices = [] updates = [] for edge in get_player_buildings(game.state, color, BuildingType.ROAD): indices.append(edge_map[edge]) updates.append(1) if len(indices) > 0: edge_plane = tf.tensor_scatter_nd_update(edge_plane, indices, updates) color_multiplier_planes.append(node_plane) color_multiplier_planes.append(edge_plane) color_multiplier_planes = tf.stack(color_multiplier_planes, axis=2) # axis=channels # add 5 node-resource probas, add color edges resource_proba_planes = tf.zeros((WIDTH, HEIGHT, 5)) resources = [i for i in RESOURCES] tile_map = get_tile_coordinate_map() for (coordinate, tile) in game.state.board.map.land_tiles.items(): if tile.resource is None: continue # there is already a 3x5 zeros matrix there (everything started as a 0!). # Tile looks like: # [0.33, 0, 0.33, 0, 0.33] # [ 0, 0, 0, 0, 0] # [0.33, 0, 0.33, 0, 0.33] proba = 0 if tile.number is None else number_probability(tile.number) (y, x) = tile_map[coordinate] # returns values in (row, column) math def channel_idx = resources.index(tile.resource) indices = [[x + i, y + j, channel_idx] for j in range(3) for i in range(5)] updates = ( [proba, 0, proba, 0, proba] + [0, 0, 0, 0, 0] + [proba, 0, proba, 0, proba] ) resource_proba_planes = tf.tensor_scatter_nd_add( resource_proba_planes, indices, updates ) # add 1 robber channel robber_plane = tf.zeros((WIDTH, HEIGHT, 1)) (y, x) = tile_map[game.state.board.robber_coordinate] indices = [[x + i, y + j, 0] for j in range(3) for i in range(5)] updates = [1, 0, 1, 0, 1] + [0, 0, 0, 0, 0] + [1, 0, 1, 0, 1] robber_plane = tf.tensor_scatter_nd_add(robber_plane, indices, updates) # Q: Would this be simpler as boolean features for each player? # add 6 port channels (5 resources + 1 for 3:1 ports) # for each port, take index and take node_id coordinates port_planes = tf.zeros((WIDTH, HEIGHT, 6)) for resource, node_ids in game.state.board.map.port_nodes.items(): channel_idx = 5 if resource is None else resources.index(resource) indices = [] updates = [] for node_id in node_ids: (x, y) = node_map[node_id] indices.append([x, y, channel_idx]) updates.append(1) port_planes = tf.tensor_scatter_nd_add(port_planes, indices, updates) return tf.concat( [color_multiplier_planes, resource_proba_planes, robber_plane, port_planes], axis=2, )