def __init__( self, players: Iterable[Player], seed: int = None, discard_limit: int = 7, vps_to_win: int = 10, catan_map: CatanMap = None, initialize: bool = True, ): """Creates a game (doesn't run it). Args: players (Iterable[Player]): list of players, should be at most 4. seed (int, optional): Random seed to use (for reproducing games). Defaults to None. discard_limit (int, optional): Discard limit to use. Defaults to 7. vps_to_win (int, optional): Victory Points needed to win. Defaults to 10. catan_map (CatanMap, optional): Map to use. Defaults to None. initialize (bool, optional): Whether to initialize. Defaults to True. """ if initialize: self.seed = seed or random.randrange(sys.maxsize) random.seed(self.seed) self.id = str(uuid.uuid4()) self.vps_to_win = vps_to_win self.state = State(players, catan_map, discard_limit=discard_limit)
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 test_building_settlement_gives_vp(): players = [SimplePlayer(Color.RED), SimplePlayer(Color.BLUE)] state = State(players) build_settlement(state, state.colors[0], 0, True) assert state.player_state["P0_VICTORY_POINTS"] == 1 assert state.player_state["P0_ACTUAL_VICTORY_POINTS"] == 1
def test_robber_possibilities(): red = SimplePlayer(Color.RED) blue = SimplePlayer(Color.BLUE) orange = SimplePlayer(Color.ORANGE) players = [red, blue, orange] state = State(players) # one for each resource tile (excluding desert) assert len(robber_possibilities(state, Color.RED)) == 18 # assert same number of possibilities, b.c. players have no cards. state.board.build_settlement(Color.BLUE, 3, initial_build_phase=True) state.board.build_settlement(Color.ORANGE, 0, initial_build_phase=True) assert len(robber_possibilities(state, Color.RED)) == 18 # assert same number of possibilities, b.c. only one player to rob in this tile player_deck_replenish(state, orange.color, WHEAT) assert len(robber_possibilities(state, Color.RED)) == 18 # now possibilites increase by 1 b.c. we have to decide to steal from blue or orange # Unless desert is (0,0,0)... in which case still at 18... player_deck_replenish(state, blue.color, WHEAT) possibilities = len(robber_possibilities(state, Color.RED)) assert possibilities == 19 or ( possibilities == 18 and state.board.map.land_tiles[(0, 0, 0)].resource is None)
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_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 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_playable_actions(): players = [SimplePlayer(Color.RED), SimplePlayer(Color.BLUE)] state = State(players) actions = generate_playable_actions(state) assert len(actions) == 54 assert actions[0].action_type == ActionType.BUILD_SETTLEMENT
def test_playable_cards(): player = SimplePlayer(Color.RED) state = State([player]) player_deck_replenish(state, Color.RED, "KNIGHT") player_clean_turn(state, Color.RED) assert player_can_play_dev(state, Color.RED, "KNIGHT")
def test_robber_possibilities_simple(): red = SimplePlayer(Color.RED) blue = SimplePlayer(Color.BLUE) orange = SimplePlayer(Color.ORANGE) players = [red, blue, orange] state = State(players) # one for each resource tile (excluding desert) assert len(robber_possibilities(state, Color.RED)) == 18
def test_building_city_gives_vp(): players = [SimplePlayer(Color.RED), SimplePlayer(Color.BLUE)] state = State(players) build_settlement(state, state.colors[0], 0, True) player_deck_replenish(state, state.colors[0], WHEAT, 2) player_deck_replenish(state, state.colors[0], ORE, 2) build_city(state, state.colors[0], 0) assert state.player_state["P0_VICTORY_POINTS"] == 2 assert state.player_state["P0_ACTUAL_VICTORY_POINTS"] == 2
def test_cant_steal_devcards(): # Arrange: Have RED buy 1 dev card (and have no resource cards) players = [SimplePlayer(Color.RED), SimplePlayer(Color.BLUE)] state = State(players) player_deck_replenish(state, Color.RED, WHEAT) player_deck_replenish(state, Color.RED, ORE) player_deck_replenish(state, Color.RED, SHEEP) buy_dev_card(state, Color.RED, KNIGHT) # Act: Attempt to steal a resource with pytest.raises(IndexError): # no resource cards in hand player_deck_random_draw(state, Color.RED)
def test_can_trade_with_port(): players = [SimplePlayer(Color.RED)] state = State(players) state.board.build_settlement(Color.RED, 26, initial_build_phase=True) port_tile = state.board.map.tiles[(3, -3, 0)] # port with node_id=25,26 resource_out = port_tile.resource or WHEAT # type: ignore num_out = 3 if port_tile.resource is None else 2 # type: ignore player_deck_replenish(state, Color.RED, resource_out, num_out) possibilities = maritime_trade_possibilities(state, Color.RED) assert len(possibilities) == 4
def test_4to1_maritime_trade_possibilities(): player = SimplePlayer(Color.RED) state = State([player]) possibilities = maritime_trade_possibilities(state, player.color) assert len(possibilities) == 0 player_deck_replenish(state, player.color, WHEAT, 4) possibilities = maritime_trade_possibilities(state, player.color) print(possibilities) assert len(possibilities) == 4 player_deck_replenish(state, player.color, BRICK, 4) possibilities = maritime_trade_possibilities(state, player.color) assert len(possibilities) == 8
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_road_possible_actions(): player = SimplePlayer(Color.RED) state = State([player]) assert len(road_building_possibilities( state, Color.RED)) == 0 # no money or place state.board.build_settlement(Color.RED, 3, initial_build_phase=True) assert len(road_building_possibilities(state, Color.RED)) == 0 # no money player_deck_replenish(state, player.color, WOOD) player_deck_replenish(state, player.color, BRICK) assert len(road_building_possibilities(state, Color.RED)) == 3 state.board.build_settlement(Color.RED, 1, initial_build_phase=True) assert len(road_building_possibilities(state, Color.RED)) == 6
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_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_city_playable_actions(): player = SimplePlayer(Color.RED) state = State([player]) assert len(city_possibilities(state, Color.RED)) == 0 # no money or place state.board.build_settlement(Color.RED, 3, initial_build_phase=True) build_settlement(state, player.color, 3, True) assert len(city_possibilities(state, Color.RED)) == 0 # no money player_deck_replenish(state, Color.RED, WHEAT, 2) player_deck_replenish(state, Color.RED, ORE, 3) assert len(city_possibilities(state, Color.RED)) == 1 state.board.build_settlement(Color.RED, 0, initial_build_phase=True) build_settlement(state, player.color, 0, True) assert len(city_possibilities(state, Color.RED)) == 2
def test_settlement_possible_actions(): player = SimplePlayer(Color.RED) state = State([player]) assert len(settlement_possibilities(state, Color.RED)) == 0 # no money or place state.board.build_settlement(Color.RED, 3, initial_build_phase=True) state.board.build_road(Color.RED, (3, 4)) state.board.build_road(Color.RED, (4, 5)) assert len(settlement_possibilities(state, Color.RED)) == 0 # no money player_freqdeck_add(state, player.color, SETTLEMENT_COST_FREQDECK) assert len(settlement_possibilities(state, Color.RED)) == 1 state.board.build_road(Color.RED, (5, 0)) assert len(settlement_possibilities(state, Color.RED)) == 2
def test_largest_army_calculation_when_no_one_has_three(): red = SimplePlayer(Color.RED) blue = SimplePlayer(Color.BLUE) white = SimplePlayer(Color.WHITE) state = State([red, blue, white]) player_deck_replenish(state, Color.RED, WHEAT, 2) player_deck_replenish(state, Color.RED, SHEEP, 2) player_deck_replenish(state, Color.RED, ORE, 2) player_deck_replenish(state, Color.BLUE, WHEAT, 1) player_deck_replenish(state, Color.BLUE, SHEEP, 1) player_deck_replenish(state, Color.BLUE, ORE, 1) buy_dev_card(state, Color.RED, KNIGHT) buy_dev_card(state, Color.RED, KNIGHT) buy_dev_card(state, Color.BLUE, KNIGHT) play_dev_card(state, Color.RED, KNIGHT) color, count = get_largest_army(state) assert color is None and count is None
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 test_largest_army_calculation_on_tie(): red = SimplePlayer(Color.RED) blue = SimplePlayer(Color.BLUE) white = SimplePlayer(Color.WHITE) state = State([red, blue, white]) player_deck_replenish(state, red.color, KNIGHT, 3) player_deck_replenish(state, blue.color, KNIGHT, 4) play_dev_card(state, Color.RED, KNIGHT) play_dev_card(state, Color.RED, KNIGHT) play_dev_card(state, Color.RED, KNIGHT) play_dev_card(state, Color.BLUE, KNIGHT) play_dev_card(state, Color.BLUE, KNIGHT) play_dev_card(state, Color.BLUE, KNIGHT) color, count = get_largest_army(state) assert color is Color.RED and count == 3 play_dev_card(state, Color.BLUE, KNIGHT) color, count = get_largest_army(state) assert color is Color.BLUE and count == 4
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
class Game: """ Initializes a map, decides player seating order, and exposes two main methods for executing the game (play and play_tick; to advance until completion or just by one decision by a player respectively). """ def __init__( self, players: Iterable[Player], seed: int = None, discard_limit: int = 7, vps_to_win: int = 10, catan_map: CatanMap = None, initialize: bool = True, ): """Creates a game (doesn't run it). Args: players (Iterable[Player]): list of players, should be at most 4. seed (int, optional): Random seed to use (for reproducing games). Defaults to None. discard_limit (int, optional): Discard limit to use. Defaults to 7. vps_to_win (int, optional): Victory Points needed to win. Defaults to 10. catan_map (CatanMap, optional): Map to use. Defaults to None. initialize (bool, optional): Whether to initialize. Defaults to True. """ if initialize: self.seed = seed or random.randrange(sys.maxsize) random.seed(self.seed) self.id = str(uuid.uuid4()) self.vps_to_win = vps_to_win self.state = State(players, catan_map, discard_limit=discard_limit) def play(self, accumulators=[], decide_fn=None): """Executes game until a player wins or exceeded TURNS_LIMIT. Args: accumulators (list[Accumulator], optional): list of Accumulator classes to use. Their .consume method will be called with every action, and their .finalize method will be called when the game ends (if it ends) Defaults to []. decide_fn (function, optional): Function to overwrite current player's decision with. Defaults to None. Returns: Color: winning color or None if game exceeded TURNS_LIMIT """ initial_game_state = self.copy() for accumulator in accumulators: accumulator.before(initial_game_state) while self.winning_color() is None and self.state.num_turns < TURNS_LIMIT: self.play_tick(decide_fn=decide_fn, accumulators=accumulators) final_game_state = self.copy() for accumulator in accumulators: accumulator.after(final_game_state) return self.winning_color() def play_tick(self, decide_fn=None, accumulators=[]): """Advances game by one ply (player decision). Args: decide_fn (function, optional): Function to overwrite current player's decision with. Defaults to None. Returns: Action: Final action (modified to be used as Log) """ player = self.state.current_player() actions = self.state.playable_actions action = ( decide_fn(player, self, actions) if decide_fn is not None else player.decide(self, actions) ) # Call accumulator.step here, because we want game_before_action, action if len(accumulators) > 0: game_snapshot = self.copy() for accumulator in accumulators: accumulator.step(game_snapshot, action) return self.execute(action) def execute(self, action: Action, validate_action: bool = True) -> Action: """Internal call that carries out decided action by player""" if validate_action and action not in self.state.playable_actions: raise ValueError( f"{action} not in playable actions: {self.state.playable_actions}" ) return apply_action(self.state, action) def winning_color(self) -> Union[Color, None]: """Gets winning color Returns: Union[Color, None]: Might be None if game truncated by TURNS_LIMIT """ result = None for color in self.state.colors: key = player_key(self.state, color) if ( self.state.player_state[f"{key}_ACTUAL_VICTORY_POINTS"] >= self.vps_to_win ): result = color return result def copy(self) -> "Game": """Creates a copy of this Game, that can be modified without repercusions on this one (useful for simulations). Returns: Game: Game copy. """ game_copy = Game([], None, None, initialize=False) game_copy.seed = self.seed game_copy.id = self.id game_copy.vps_to_win = self.vps_to_win game_copy.state = self.state.copy() return game_copy
def test_initial_placement_possibilities(): red = SimplePlayer(Color.RED) state = State([red]) assert len(settlement_possibilities(state, Color.RED, True)) == 54
def test_maritime_possibities_respect_bank_not_having_cards(): player = SimplePlayer(Color.RED) state = State([player]) player_deck_replenish(state, player.color, WHEAT) assert len(maritime_trade_possibilities(state, player.color)) == 0