def test_cant_buy_more_than_max_card(): players = [SimplePlayer(Color.RED), SimplePlayer(Color.BLUE)] state = State(players) with pytest.raises(ValueError): # not enough money apply_action( state, Action(players[0].color, ActionType.BUY_DEVELOPMENT_CARD, None)) player_deck_replenish(state, players[0].color, SHEEP, 26) player_deck_replenish(state, players[0].color, WHEAT, 26) player_deck_replenish(state, players[0].color, ORE, 26) for i in range(25): apply_action( state, Action(players[0].color, ActionType.BUY_DEVELOPMENT_CARD, None)) # assert must have all victory points assert player_num_dev_cards(state, players[0].color) == 25 assert get_dev_cards_in_hand(state, players[0].color, VICTORY_POINT) == 5 with pytest.raises(ValueError): # not enough cards in bank apply_action( state, Action(players[0].color, ActionType.BUY_DEVELOPMENT_CARD, None)) assert player_num_resource_cards(state, players[0].color) == 3
def robber_possibilities(state, color) -> List[Action]: actions = [] for (coordinate, tile) in state.board.map.land_tiles.items(): if coordinate == state.board.robber_coordinate: continue # ignore. must move robber. # each tile can yield a (move-but-cant-steal) action or # several (move-and-steal-from-x) actions. to_steal_from = set() # set of player_indexs for _, node_id in tile.nodes.items(): building = state.board.buildings.get(node_id, None) if building is not None: candidate_color = building[0] if (player_num_resource_cards(state, candidate_color) >= 1 and color != candidate_color # can't play yourself ): to_steal_from.add(candidate_color) if len(to_steal_from) == 0: actions.append( Action(color, ActionType.MOVE_ROBBER, (coordinate, None, None))) else: for enemy_color in to_steal_from: actions.append( Action(color, ActionType.MOVE_ROBBER, (coordinate, enemy_color, None))) return actions
def test_end_turn_goes_to_next_player(fake_roll_dice): fake_roll_dice.return_value = (1, 2) # not a 7 players = [SimplePlayer(Color.RED), SimplePlayer(Color.BLUE)] game = Game(players) actions = [] while not any( a.action_type == ActionType.ROLL for a in game.state.playable_actions ): actions.append(game.play_tick()) p0_color = game.state.colors[0] p1_color = game.state.colors[1] assert ( game.state.current_prompt == ActionPrompt.PLAY_TURN and game.state.current_color() == p0_color ) assert game.state.playable_actions == [Action(p0_color, ActionType.ROLL, None)] game.execute(Action(p0_color, ActionType.ROLL, None)) assert game.state.current_prompt == ActionPrompt.PLAY_TURN assert game.state.current_color() == p0_color assert player_has_rolled(game.state, p0_color) assert Action(p0_color, ActionType.END_TURN, None) in game.state.playable_actions game.execute(Action(p0_color, ActionType.END_TURN, None)) assert game.state.current_prompt == ActionPrompt.PLAY_TURN assert game.state.current_color() == p1_color assert not player_has_rolled(game.state, p0_color) assert not player_has_rolled(game.state, p1_color) assert game.state.playable_actions == [Action(p1_color, ActionType.ROLL, None)]
def action_from_json(data): color = Color[data[0]] action_type = ActionType[data[1]] if action_type == ActionType.BUILD_ROAD: action = Action(color, action_type, tuple(data[2])) elif action_type == ActionType.MARITIME_TRADE: value = tuple(data[2]) action = Action(color, action_type, value) else: action = Action(color, action_type, data[2]) return action
def normalize_action(action): normalized = action if normalized.action_type == ActionType.ROLL: return Action(action.color, action.action_type, None) elif normalized.action_type == ActionType.MOVE_ROBBER: return Action(action.color, action.action_type, action.value[0]) elif normalized.action_type == ActionType.BUILD_ROAD: return Action(action.color, action.action_type, tuple(sorted(action.value))) elif normalized.action_type == ActionType.BUY_DEVELOPMENT_CARD: return Action(action.color, action.action_type, None) elif normalized.action_type == ActionType.DISCARD: return Action(action.color, action.action_type, None) return normalized
def test_play_monopoly_player_steals_cards(): player_to_act = SimplePlayer(Color.RED) player_to_steal_from_1 = SimplePlayer(Color.BLUE) player_to_steal_from_2 = SimplePlayer(Color.ORANGE) players = [player_to_act, player_to_steal_from_1, player_to_steal_from_2] state = State(players) player_deck_replenish(state, player_to_act.color, MONOPOLY) player_deck_replenish(state, player_to_steal_from_1.color, ORE, 3) player_deck_replenish(state, player_to_steal_from_1.color, WHEAT, 1) player_deck_replenish(state, player_to_steal_from_2.color, ORE, 2) player_deck_replenish(state, player_to_steal_from_2.color, WHEAT, 1) action_to_execute = Action(player_to_act.color, ActionType.PLAY_MONOPOLY, ORE) apply_action(state, action_to_execute) assert player_num_resource_cards(state, player_to_act.color, ORE) == 5 assert player_num_resource_cards(state, player_to_steal_from_1.color, ORE) == 0 assert player_num_resource_cards(state, player_to_steal_from_1.color, WHEAT) == 1 assert player_num_resource_cards(state, player_to_steal_from_2.color, ORE) == 0 assert player_num_resource_cards(state, player_to_steal_from_2.color, WHEAT) == 1
def maritime_trade_possibilities(state, color) -> List[Action]: trade_offers = set() # Get lowest rate per resource port_resources = set(state.board.get_player_port_resources(color)) rates: Dict[FastResource, int] = { WOOD: 4, BRICK: 4, SHEEP: 4, WHEAT: 4, ORE: 4 } if None in port_resources: rates = {WOOD: 3, BRICK: 3, SHEEP: 3, WHEAT: 3, ORE: 3} for resource in port_resources: if resource != None: rates[resource] = 2 # For resource in hand for resource in RESOURCES: amount = player_num_resource_cards(state, color, resource) if amount >= rates[resource]: resource_out: List[Any] = [resource] * rates[resource] resource_out += [None] * (4 - rates[resource]) for j_resource in RESOURCES: if (resource != j_resource and freqdeck_count( state.resource_freqdeck, j_resource) > 0): trade_offer = tuple(resource_out + [j_resource]) trade_offers.add(trade_offer) return list( map(lambda t: Action(color, ActionType.MARITIME_TRADE, t), trade_offers))
def test_defeating_your_own_largest_army_doesnt_give_more_vps(): # Arrange: Buy all dev cards players = [SimplePlayer(Color.RED), SimplePlayer(Color.BLUE)] state = State(players) player_deck_replenish(state, players[0].color, SHEEP, 26) player_deck_replenish(state, players[0].color, WHEAT, 26) player_deck_replenish(state, players[0].color, ORE, 26) for i in range(25): apply_action( state, Action(players[0].color, ActionType.BUY_DEVELOPMENT_CARD, None)) assert get_largest_army(state) == (None, None) assert get_actual_victory_points(state, Color.RED) == 5 # Act - Assert play_dev_card(state, Color.RED, KNIGHT) play_dev_card(state, Color.RED, KNIGHT) play_dev_card(state, Color.RED, KNIGHT) assert get_largest_army(state) == (Color.RED, 3) assert get_actual_victory_points(state, Color.RED) == 7 # Act - Assert play_dev_card(state, Color.RED, KNIGHT) assert get_largest_army(state) == (Color.RED, 4) assert get_actual_victory_points(state, Color.RED) == 7
def test_play_monopoly_no_monopoly_card(): players = [SimplePlayer(Color.RED), SimplePlayer(Color.BLUE)] game = Game(players) action_to_execute = Action(players[0].color, ActionType.PLAY_MONOPOLY, ORE) with pytest.raises(ValueError): # no monopoly game.execute(action_to_execute)
def test_sequence(): players = [ SimplePlayer(Color.RED), SimplePlayer(Color.BLUE), SimplePlayer(Color.WHITE), SimplePlayer(Color.ORANGE), ] state = State(players) p0_color = state.colors[0] assert state.current_prompt == ActionPrompt.BUILD_INITIAL_SETTLEMENT assert Action(p0_color, ActionType.BUILD_SETTLEMENT, 0) in state.playable_actions assert Action(p0_color, ActionType.BUILD_SETTLEMENT, 50) in state.playable_actions apply_action(state, state.playable_actions[0])
def test_play_year_of_plenty_no_year_of_plenty_card(): players = [SimplePlayer(Color.RED), SimplePlayer(Color.BLUE)] game = Game(players) action_to_execute = Action( players[0].color, ActionType.PLAY_YEAR_OF_PLENTY, [ORE, WHEAT] ) with pytest.raises(ValueError): # no year of plenty card game.execute(action_to_execute)
def test_execute_action_on_copies_doesnt_conflict(): players = [ SimplePlayer(Color.RED), SimplePlayer(Color.BLUE), SimplePlayer(Color.WHITE), SimplePlayer(Color.ORANGE), ] game = Game(players) p0_color = game.state.colors[0] game.execute(Action(p0_color, ActionType.BUILD_SETTLEMENT, 0)) action = Action(p0_color, ActionType.BUILD_ROAD, (0, 1)) game_copy = game.copy() game_copy.execute(action) game_copy = game.copy() game_copy.execute(action) game.execute(action)
def initial_road_possibilities(state, color) -> List[Action]: # Must be connected to last settlement last_settlement_node_id = state.buildings_by_color[color][ BuildingType.SETTLEMENT][-1] buildable_edges = filter( lambda edge: last_settlement_node_id in edge, state.board.buildable_edges(color), ) return [ Action(color, ActionType.BUILD_ROAD, edge) for edge in buildable_edges ]
def test_moving_robber_steals_correctly(): players = [SimplePlayer(Color.RED), SimplePlayer(Color.BLUE)] state = State(players) player_deck_replenish(state, players[1].color, WHEAT, 1) state.board.build_settlement(Color.BLUE, 3, initial_build_phase=True) action = Action(players[0].color, ActionType.MOVE_ROBBER, ((2, 0, -2), None, None)) apply_action(state, action) assert player_num_resource_cards(state, players[0].color) == 0 assert player_num_resource_cards(state, players[1].color) == 1 action = Action( players[0].color, ActionType.MOVE_ROBBER, ((0, 0, 0), players[1].color, WHEAT), ) apply_action(state, action) assert player_num_resource_cards(state, players[0].color) == 1 assert player_num_resource_cards(state, players[1].color) == 0
def road_building_possibilities(state, color) -> List[Action]: key = player_key(state, color) has_money = player_resource_freqdeck_contains(state, color, ROAD_COST_FREQDECK) has_roads_available = state.player_state[f"{key}_ROADS_AVAILABLE"] > 0 if has_money and has_roads_available: buildable_edges = state.board.buildable_edges(color) return [ Action(color, ActionType.BUILD_ROAD, edge) for edge in buildable_edges ] else: return []
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 test_play_year_of_plenty_not_enough_resources(): players = [SimplePlayer(Color.RED), SimplePlayer(Color.BLUE)] player_to_act = players[0] game = Game(players) game.state.resource_freqdeck = [0, 0, 0, 0, 0] player_deck_replenish(game.state, player_to_act.color, YEAR_OF_PLENTY) action_to_execute = Action( player_to_act.color, ActionType.PLAY_YEAR_OF_PLENTY, [ORE, WHEAT], ) with pytest.raises(ValueError): # not enough cards in bank game.execute(action_to_execute)
def settlement_possibilities(state, color, initial_build_phase=False) -> List[Action]: if initial_build_phase: buildable_node_ids = state.board.buildable_node_ids( color, initial_build_phase=True) return [ Action(color, ActionType.BUILD_SETTLEMENT, node_id) for node_id in buildable_node_ids ] else: key = player_key(state, color) has_money = player_resource_freqdeck_contains( state, color, SETTLEMENT_COST_FREQDECK) has_settlements_available = ( state.player_state[f"{key}_SETTLEMENTS_AVAILABLE"] > 0) if has_money and has_settlements_available: buildable_node_ids = state.board.buildable_node_ids(color) return [ Action(color, ActionType.BUILD_SETTLEMENT, node_id) for node_id in buildable_node_ids ] else: return []
def test_can_only_play_one_dev_card_per_turn(): players = [ SimplePlayer(Color.RED), SimplePlayer(Color.BLUE), SimplePlayer(Color.WHITE), SimplePlayer(Color.ORANGE), ] state = State(players) player_deck_replenish(state, players[0].color, YEAR_OF_PLENTY, 2) action = Action(players[0].color, ActionType.PLAY_YEAR_OF_PLENTY, 2 * [BRICK]) apply_action(state, action) with pytest.raises(ValueError): # shouldnt be able to play two dev cards apply_action(state, action)
def test_trade_execution(): players = [ SimplePlayer(Color.RED), SimplePlayer(Color.BLUE), SimplePlayer(Color.WHITE), SimplePlayer(Color.ORANGE), ] state = State(players) player_deck_replenish(state, players[0].color, BRICK, 4) trade_offer = tuple([BRICK] * 4 + [ORE]) action = Action(players[0].color, ActionType.MARITIME_TRADE, trade_offer) apply_action(state, action) assert player_num_resource_cards(state, players[0].color) == 1 assert sum(state.resource_freqdeck) == 19 * 5 + 4 - 1
def test_buying_road_is_payed_for(): players = [SimplePlayer(Color.RED), SimplePlayer(Color.BLUE)] state = State(players) state.is_initial_build_phase = False state.board.build_settlement(players[0].color, 3, True) action = Action(players[0].color, ActionType.BUILD_ROAD, (3, 4)) player_freqdeck_add( state, players[0].color, freqdeck_from_listdeck([WOOD, BRICK]), ) apply_action(state, action) assert player_num_resource_cards(state, players[0].color, WOOD) == 0 assert player_num_resource_cards(state, players[0].color, BRICK) == 0
def test_discard_config(fake_roll_dice): fake_roll_dice.return_value = (1, 6) players = [SimplePlayer(Color.RED), SimplePlayer(Color.BLUE)] game = Game(players, discard_limit=10) while not any( a.action_type == ActionType.ROLL for a in game.state.playable_actions ): game.play_tick() until_nine = 9 - player_num_resource_cards(game.state, players[1].color) player_deck_replenish(game.state, players[1].color, WHEAT, until_nine) assert player_num_resource_cards(game.state, players[1].color) == 9 game.play_tick() # should be p0 rolling. assert game.state.playable_actions != [ Action(players[1].color, ActionType.DISCARD, None) ]
def generate_playable_actions(state) -> List[Action]: action_prompt = state.current_prompt color = state.current_color() if action_prompt == ActionPrompt.BUILD_INITIAL_SETTLEMENT: return settlement_possibilities(state, color, True) elif action_prompt == ActionPrompt.BUILD_INITIAL_ROAD: return initial_road_possibilities(state, color) elif action_prompt == ActionPrompt.MOVE_ROBBER: return robber_possibilities(state, color) elif action_prompt == ActionPrompt.PLAY_TURN: if state.is_road_building: actions = road_building_possibilities(state, color) elif not player_has_rolled(state, color): actions = [Action(color, ActionType.ROLL, None)] if player_can_play_dev(state, color, "KNIGHT"): actions.append(Action(color, ActionType.PLAY_KNIGHT_CARD, None)) else: actions = [Action(color, ActionType.END_TURN, None)] actions.extend(road_building_possibilities(state, color)) actions.extend(settlement_possibilities(state, color)) actions.extend(city_possibilities(state, color)) can_buy_dev_card = (player_can_afford_dev_card(state, color) and len(state.development_listdeck) > 0) if can_buy_dev_card: actions.append( Action(color, ActionType.BUY_DEVELOPMENT_CARD, None)) # Play Dev Cards if player_can_play_dev(state, color, "YEAR_OF_PLENTY"): actions.extend( year_of_plenty_possibilities(color, state.resource_freqdeck)) if player_can_play_dev(state, color, "MONOPOLY"): actions.extend(monopoly_possibilities(color)) if player_can_play_dev(state, color, "KNIGHT"): actions.append(Action(color, ActionType.PLAY_KNIGHT_CARD, None)) if (player_can_play_dev(state, color, "ROAD_BUILDING") and len(road_building_possibilities(state, color)) > 0): actions.append( Action(color, ActionType.PLAY_ROAD_BUILDING, None)) # Trade actions.extend(maritime_trade_possibilities(state, color)) return actions elif action_prompt == ActionPrompt.DISCARD: return discard_possibilities(color) else: raise RuntimeError("Unknown ActionPrompt")
def year_of_plenty_possibilities(color, freqdeck: List[int]) -> List[Action]: options = set() for i, first_card in enumerate(RESOURCES): for j in range(i, len(RESOURCES)): second_card = RESOURCES[j] # doing it this way to not repeat to_draw = freqdeck_from_listdeck([first_card, second_card]) if freqdeck_contains(freqdeck, to_draw): options.add((first_card, second_card)) else: # try allowing player select 1 card only. if freqdeck_can_draw(freqdeck, 1, first_card): options.add((first_card, )) if freqdeck_can_draw(freqdeck, 1, second_card): options.add((second_card, )) return list( map( lambda cards: Action(color, ActionType.PLAY_YEAR_OF_PLENTY, tuple(cards)), options, ))
def test_play_year_of_plenty_gives_player_resources(): players = [SimplePlayer(Color.RED), SimplePlayer(Color.BLUE)] state = State(players) player_to_act = players[0] player_deck_replenish(state, player_to_act.color, YEAR_OF_PLENTY, 1) action_to_execute = Action(player_to_act.color, ActionType.PLAY_YEAR_OF_PLENTY, [ORE, WHEAT]) apply_action(state, action_to_execute) for card_type in RESOURCES: if card_type == ORE or card_type == WHEAT: assert player_num_resource_cards(state, player_to_act.color, card_type) == 1 assert freqdeck_count(state.resource_freqdeck, card_type) == 18 else: assert player_num_resource_cards(state, player_to_act.color, card_type) == 0 assert freqdeck_count(state.resource_freqdeck, card_type) == 19 assert get_dev_cards_in_hand(state, player_to_act.color, YEAR_OF_PLENTY) == 0
def monopoly_possibilities(color) -> List[Action]: return [ Action(color, ActionType.PLAY_MONOPOLY, card) for card in RESOURCES ]
def execute_spectrum(game, action): """Returns [(game_copy, proba), ...] tuples for result of given action. Result probas should add up to 1. Does not modify self""" deterministic_actions = set([ ActionType.END_TURN, ActionType.BUILD_SETTLEMENT, ActionType.BUILD_ROAD, ActionType.BUILD_CITY, ActionType.PLAY_KNIGHT_CARD, ActionType.PLAY_YEAR_OF_PLENTY, ActionType.PLAY_ROAD_BUILDING, ActionType.MARITIME_TRADE, ActionType. DISCARD, # for simplicity... ok if reality is slightly different ActionType. PLAY_MONOPOLY, # for simplicity... we assume good card-counting and bank is visible... ]) if action.action_type in deterministic_actions: copy = game.copy() copy.execute(action, validate_action=False) return [(copy, 1)] elif action.action_type == ActionType.BUY_DEVELOPMENT_CARD: results = [] for card in DEVELOPMENT_CARDS: option_action = Action(action.color, action.action_type, card) option_game = game.copy() try: option_game.execute(option_action, validate_action=False) except Exception: # ignore exceptions, since player might imagine impossible outcomes. # ignoring means the value function of this node will be flattened, # to the one before. pass results.append((option_game, starting_devcard_proba(card))) return results elif action.action_type == ActionType.ROLL: results = [] for roll in range(2, 13): outcome = (roll // 2, math.ceil(roll / 2)) option_action = Action(action.color, action.action_type, outcome) option_game = game.copy() option_game.execute(option_action, validate_action=False) results.append((option_game, number_probability(roll))) return results elif action.action_type == ActionType.MOVE_ROBBER: (coordinate, robbed_color, _) = action.value if robbed_color is None: # no one to steal, then deterministic copy = game.copy() copy.execute(action, validate_action=False) return [(copy, 1)] else: results = [] for card in RESOURCES: option_action = Action( action.color, action.action_type, (coordinate, robbed_color, card), ) option_game = game.copy() try: option_game.execute(option_action, validate_action=False) except Exception: # ignore exceptions, since player might imagine impossible outcomes. # ignoring means the value function of this node will be flattened, # to the one before. pass results.append((option_game, 1 / 5.0)) return results else: raise RuntimeError("Unknown ActionType " + str(action.action_type))
def discard_possibilities(color) -> List[Action]: return [Action(color, ActionType.DISCARD, None)]