def battle(self, clearing: Clearing, defender: Player) -> None: if self.has_trait(TRAIT_MARKSMAN): # Fortified MM is currently the only way for a piece to require two hits to be removed in battle # Marksman Vagabot is currently the only way for a player to deal hits in battle to a bot before the roll # To prevent messy logic in handling taking hits, we just deal two hits of damage in this case, which will # still functionally remove one building if defender.halves_damage(clearing): marksman_damage_result = defender.suffer_damage(clearing, 2, self, is_attacker=False) else: marksman_damage_result = defender.suffer_damage(clearing, 1, self, is_attacker=False) self.add_victory_points(marksman_damage_result.points_awarded + self.supplementary_score_for_removed_pieces_in_battle( defender, marksman_damage_result.removed_pieces, is_attacker=True)) random_rolls = (random.randint(0, 3), random.randint(0, 3)) # Defender allocates the rolls - high roll to attacker, low roll to defender, except in the case of Veterans roll_result = defender.allocate_rolls_as_defender(random_rolls) # Each battler caps their hits and adds their relevant bonus hits attacker_hits = (self.cap_rolled_hits(clearing, roll_result.attacker_roll) + self.get_bonus_hits(clearing, defender, is_attacker=True)) defender_hits = (defender.cap_rolled_hits(clearing, roll_result.defender_roll) + defender.get_bonus_hits(clearing, defender, is_attacker=False)) # Each battler removes their pieces and calculates how much VP the opponent should earn from the battle defender_damage_result = defender.suffer_damage(clearing, attacker_hits, self, is_attacker=False) attacker_damage_result = self.suffer_damage(clearing, defender_hits, defender, is_attacker=True) # Each battler scores their awarded VP, plus any bonus VP they deserve (such as Vagabot from removing warriors) self.add_victory_points(defender_damage_result.points_awarded + self.supplementary_score_for_removed_pieces_in_battle( defender, defender_damage_result.removed_pieces, is_attacker=True)) defender.add_victory_points(attacker_damage_result.points_awarded + defender.supplementary_score_for_removed_pieces_in_battle( self, attacker_damage_result.removed_pieces, is_attacker=False))
def explore_ruin(self, player: Player) -> None: if not self.ruin or not self.ruin.items: return item_gained = self.ruin.items[0] player.get_item(self.ruin.items[0]) self.ruin.items.remove(item_gained) if not self.ruin.items: self.ruin = None
def test_subtract_victory_points_below_0(self): mock_game = Mock() mock_piece_stock = Mock() player = Player(mock_game, Faction.MECHANICAL_MARQUISE_2_0, mock_piece_stock) player.victory_points = 1 player.add_victory_points(-2) self.assertEqual(player.victory_points, 0)
def test_add_victory_points(self): mock_game = Mock() mock_piece_stock = Mock() player = Player(mock_game, Faction.MECHANICAL_MARQUISE_2_0, mock_piece_stock) self.assertEqual(player.victory_points, 0) player.add_victory_points(2) self.assertEqual(player.victory_points, 2)
def test_get_item(self): mock_game = Mock() mock_piece_stock = Mock() player = Player(mock_game, Faction.MECHANICAL_MARQUISE_2_0, mock_piece_stock) self.assertEqual(player.crafted_items, []) item = ItemToken(Item.BAG) player.get_item(item) self.assertEqual(player.crafted_items, [item])
def can_move_piece(self, player: Player, piece: Piece, destination: Location, requires_rule: bool = False) -> bool: # We'll never look for rule in move that isn't from one clearing to another clearing if requires_rule and isinstance(destination, Clearing): if not player.does_rule_clearing( self) and not player.does_rule_clearing(destination): return False return super().can_move_piece(player, piece, destination, requires_rule)
def remove_pieces(self, player: Player, pieces: list[Piece]) -> list[Piece]: # Remove piece from this location removed_pieces = [] for piece in pieces: if piece.cannot_be_removed: piece.resolve_effects_on_attempting_to_remove_self() # For Vagabot 'Big Damage' else: self.piece_map(player).remove_piece(piece) removed_pieces.append(piece) # It's up to the owners of pieces that cannot be removed to override this method player.move_removed_pieces_into_supply(pieces, self) return removed_pieces
def battle(self, clearing: Clearing, defender: Player) -> None: random_rolls = (random.randint(0, 3), random.randint(0, 3)) # Defender allocates the rolls - high roll to attacker, low roll to defender, except in the case of Veterans roll_result = defender.allocate_rolls_as_defender(random_rolls) # Each battler caps their hits and adds their relevant bonus hits attacker_hits = ( self.cap_rolled_hits(clearing, roll_result.attacker_roll) + self.get_bonus_hits(clearing, defender, is_attacker=True)) defender_hits = ( defender.cap_rolled_hits(clearing, roll_result.defender_roll) + defender.get_bonus_hits(clearing, defender, is_attacker=False)) # Each battler removes their pieces and calculates how much VP the opponent should earn from the battle defender_damage_result = defender.suffer_damage(clearing, attacker_hits, self, is_attacker=False) attacker_damage_result = self.suffer_damage(clearing, defender_hits, defender, is_attacker=True) # Each battler scores their awarded VP, plus any bonus VP they deserve (such as Vagabot from removing warriors) self.add_victory_points( defender_damage_result.points_awarded + self.supplementary_score_for_removed_pieces_in_battle( defender, defender_damage_result.removed_pieces, is_attacker=True)) if self.has_trait(TRAIT_WAR_TAX): removed_buildings_and_tokens = [ piece for piece in defender_damage_result.removed_pieces if isinstance(piece, Building) or isinstance(piece, Token) ] defender.add_victory_points(len(removed_buildings_and_tokens) * -1) defender.add_victory_points( attacker_damage_result.points_awarded + defender.supplementary_score_for_removed_pieces_in_battle( self, attacker_damage_result.removed_pieces, is_attacker=False) )
def find_shortest_legal_paths_to_destination_clearing( self, player: Player, moving_piece: Piece, destination: Clearing, ignore_move: bool = False) -> list[list[Clearing]]: if not destination.can_move_piece_into(player, moving_piece): return [] # TODO: Test and clean clearing_paths: deque[list[Clearing]] = deque([[]]) all_shortest_paths: list[list[Clearing]] = [] while clearing_paths: current_path = clearing_paths.popleft() if not current_path: current_location = self else: current_location = current_path[-1] for adjacent_clearing in player.get_adjacent_clearings( current_location): # Don't double back on yourself - that adds to the path length uselessly if adjacent_clearing in current_path: continue # Skip impossible moves, unless ignore_move is True if not ignore_move and not current_location.can_move_piece( player, moving_piece, adjacent_clearing): continue next_path = [c for c in current_path] next_path.append(adjacent_clearing) if adjacent_clearing == destination: all_shortest_paths.append( [c for c in current_path if isinstance(c, Clearing)]) # Breadth-first-search: Once we find a path to the destination N clearings away, we know no path to # the destination is shorter than N, so don't add any more to the clearing_paths queue elif not all_shortest_paths: clearing_paths.append(next_path) return all_shortest_paths
def craft_item(self, item: Item, player: Player, score_points: int) -> None: item_token = self.get_item_if_available(item) if item_token: self.item_supply.remove(item_token) player.get_item(item_token) player.add_victory_points(score_points)
def remove_pieces_in_battle_to_supply(self, player: Player, pieces: list[Piece], is_attacker: bool = True) -> None: # Remove piece from this clearing for piece in pieces: self.piece_map(player).remove_piece(piece) player.move_removed_pieces_into_supply_from_battle(pieces, self, is_attacker)
def sort_clearings_by_ruled_by_self(clearings: list[Clearing], acting_player: Player, descending: bool = True) -> list[Clearing]: return sorted(clearings, key=lambda c: acting_player.does_rule_clearing(c), reverse=descending)