def receive_damage(self, amount: int, own_board: PlayerBoard, poisonous: Optional[bool] = False, defer_damage_trigger: Optional[bool] = False): """Receive amount of damage which can be poisonous Arguments: amount {int} -- Amount of damage to receive poisonous {bool} -- Whether the damage is poisonous own_board {PlayerBoard} -- The board belonging to the minion taking damage Returns tuple: int -- Current health of the attacked minion (will be negative if its dead, can be used for overkill amount) """ if amount <= 0: return self.health if self.divine_shield: self.divine_shield = False own_board.divine_shield_popped(self) else: # Minion took damage self.health -= amount if self.health <= 0 or poisonous: self.dead = True self.dead_by_poison = True if poisonous else False if not defer_damage_trigger: self.on_receive_damage(own_board) else: own_board.deferred_damage_triggers.append( self.on_receive_damage) return self.health
def test_deathwing(initialized_game): initialized_game.player_board[0] = PlayerBoard(0, HeroType.DEATHWING, 1, 1, [HarvestGolem()]) initialized_game.player_board[1] = PlayerBoard(0, None, 1, 1, [PunchingBag(attack=10)], enemy_is_deathwing=True) initialized_game.start_of_game() initialized_game.single_round() assert initialized_game.player_board[0].minions[0].attack == 4 initialized_game.player_board[0] = PlayerBoard(0, HeroType.DEATHWING, 1, 1, [HarvestGolem()], enemy_is_deathwing=True) initialized_game.player_board[1] = PlayerBoard(0, HeroType.DEATHWING, 1, 1, [PunchingBag(attack=10)], enemy_is_deathwing=True) initialized_game.start_of_game() initialized_game.single_round() assert initialized_game.player_board[0].minions[0].attack == 6
def update_bonus_attack(self, own_board: PlayerBoard, opposing_board: PlayerBoard): murlocs_current = own_board.count_minion_type(MinionType.Murloc) + opposing_board.count_minion_type(MinionType.Murloc) - 1 # Don't count itself murlocs_change = murlocs_current - self.number_murlocs bonus = murlocs_change * 2 if self.golden else murlocs_change self.attack += bonus self.number_murlocs = murlocs_current
def trigger(self, minion: Minion, own_board: PlayerBoard, opposing_board: PlayerBoard): number_rats = minion.last_attack logging.debug( f"Rat pack deathrattle triggered, creating {number_rats} rats") for rat in range(number_rats): own_board.add_minion(Rat(golden=minion.golden), position=minion.position)
def trigger_reborn(self, own_board: PlayerBoard): """If the minion has reborn and has not triggered, trigger it""" if self.reborn and not self.reborn_triggered: reborn_at = self.find_deathrattle_reborn_position() self.__init__(reborn_triggered=True, token=True, attacked=self.attacked, golden=self.golden, position=reborn_at) own_board.add_minion(new_minion=self, position=reborn_at)
def test_yshaarj(initialized_game): initialized_game.player_board[0] = PlayerBoard(0, HeroType.YSHAARJ_ACTIVATED, 1, 1, []) initialized_game.player_board[1] = PlayerBoard(0, None, 1, 1, []) attacker_board = initialized_game.player_board[0] defender_board = initialized_game.player_board[1] initialized_game.start_of_game() initialized_game.single_round() assert attacker_board.minions[0].rank == 1
def trigger(self, minion: Minion, own_board: PlayerBoard, opposing_board: PlayerBoard, macaw_trigger: Optional[bool] = False): triggers = 2 if minion.golden else 1 for _ in range(triggers): for opposing_minion in opposing_board.get_living_minions(): opposing_minion.receive_damage(1, opposing_board) for friendly_minion in own_board.get_living_minions(): friendly_minion.receive_damage(1, own_board)
def on_receive_damage(self, own_board: PlayerBoard): from utils.minion_utils import get_minions disallowed_demons = [ "Amalgam", "Fiery Imp", "Imp", "Imp Mama", "Voidwalker" ] select_demons = lambda x: (MinionType.Demon in x.types) and ( x.name not in disallowed_demons) demons = get_minions(select_demons) for _ in range(2 if self.golden else 1): demon = demons[random.randint(0, len(demons) - 1)](taunt=True) own_board.add_minion(demon, self.position + 1, True)
def test_kurtrus_ashfallen(initialized_game): initialized_game.player_board[0] = PlayerBoard( 0, HeroType.KURTRUS_ASHFALLEN_ACTIVATED, 1, 1, [AcolyteOfCThun()]) initialized_game.player_board[1] = PlayerBoard(0, None, 1, 1, [PunchingBag(attack=10)]) initialized_game.start_of_game() initialized_game.single_round() acolyte = initialized_game.player_board[0].minions[0] # Reborn minions get the +2/+2 buff assert acolyte.attack == 4 assert acolyte.health == 3
def test_greybough(initialized_game): initialized_game.player_board[0] = PlayerBoard(0, HeroType.GREYBOUGH, 1, 1, [HarvestGolem()]) initialized_game.player_board[1] = PlayerBoard(0, None, 1, 1, [PunchingBag(attack=10)]) initialized_game.start_of_game() initialized_game.single_round() golem_token = initialized_game.player_board[0].minions[0] assert golem_token.attack == 3 assert golem_token.health == 3 assert golem_token.taunt
def check_deaths(self, attacking_player_board: PlayerBoard, defending_player_board: PlayerBoard): """Check deaths on both sides Arguments: attacking_player_board {PlayerBoard} -- Player board of attacking player defending_player_board {PlayerBoard} -- Player board of defending player """ for minion in attacking_player_board.get_minions(): if minion.check_death(attacking_player_board, defending_player_board): self.kill(minion, attacking_player_board, defending_player_board, minion_defending_player=False) return for minion in defending_player_board.get_minions(): if minion.check_death(defending_player_board, attacking_player_board): self.kill(minion, attacking_player_board, defending_player_board, minion_defending_player=True) return
def trigger(self, minion: Minion, own_board: PlayerBoard, opposing_board: PlayerBoard): logging.debug(f"Spawn of NZoth deathrattle triggered") bonus = 2 if minion.golden else 1 for other_minion in own_board.get_minions(): other_minion.add_attack(bonus) other_minion.add_defense(bonus)
def trigger(self, minion: Minion, own_board: PlayerBoard, opposing_board: PlayerBoard, macaw_trigger: Optional[bool] = False): # Filter out un purchaseable elementals like curator token, water droplet, and elementals above current tech level not_allowed_elementals = ["Amalgam", "Water Droplet", "Gentle Djinni"] isSpawnable = lambda x: (MinionType.Elemental in x.types) and ( x.name not in not_allowed_elementals) elementals_to_summon = 2 if minion.golden else 1 # Golden genie summons two distinct random elementals Deathrattle.summon_random_minions(minion, own_board, elementals_to_summon, isSpawnable, macaw_trigger) for _ in range(elementals_to_summon): own_board.call_triggers(TriggerType.ON_CARD_PUT_IN_HAND, own_board)
def trigger(self, minion: Minion, own_board: PlayerBoard, opposing_board: PlayerBoard, macaw_trigger: Optional[bool] = False): bonus = 2 if minion.golden else 1 for other_minion in own_board.get_living_minions(): other_minion.add_stats(bonus, bonus)
def attack(self, attacking_minion: Minion, defending_minion: Minion, attacking_board: PlayerBoard, defending_board: PlayerBoard): """Let one minion attack the other Arguments: attacking_minion {Minion} -- Minion that attacks defending_minion {Minion} -- Minion that is attacked """ # Pre-attack triggers attacking_board.call_triggers(TriggerType.ON_FRIENDLY_BEFORE_ATTACK, attacking_minion, attacking_board, defending_minion, defending_board) defending_board.call_triggers(TriggerType.ON_FRIENDLY_ATTACKED, defending_minion, defending_board, attacking_board) # If minion is killed by pyro spawn trigger, attack is cancelled if defending_minion.dead: attacking_minion.on_attack_after(attacking_board, defending_board) return # Deal attack damage if attacking_minion.cleave: # Minions hit with cleave should take damage all at once, but triggers resolve after neighbors = defending_board.get_minions_neighbors(defending_minion) defenders = [neighbors[0], defending_minion, neighbors[1]] for minion in [x for x in defenders if x]: self.deal_attack_damage(minion, defending_board, attacking_minion, attacking_board, True) self.deal_attack_damage(attacking_minion, attacking_board, defending_minion, defending_board) else: self.deal_attack_damage(defending_minion, defending_board, attacking_minion, attacking_board) self.deal_attack_damage(attacking_minion, attacking_board, defending_minion, defending_board) # Damage triggers from cleave and herald of flame should be resolved after attack and overkill trigger resolution for trigger in defending_board.deferred_damage_triggers: trigger(defending_board) defending_board.deferred_damage_triggers.clear() # Post attack triggers (macaw) attacking_minion.on_attack_after(attacking_board, defending_board)
def on_friendly_removal_after(self, other_minion: Minion, friendly_board: PlayerBoard, enemy_board: PlayerBoard): """After a friendly Demon dies, deal 3 damage to a random enemy minion. """ triggers = 2 if self.golden else 1 if MinionType.Demon in other_minion.types: for _ in range(triggers): target = enemy_board.random_minion() if target: target.receive_damage(3, enemy_board)
def trigger(self, minion: Minion, own_board: PlayerBoard, opposing_board: PlayerBoard, macaw_trigger: Optional[bool] = False): for _ in range(2 if minion.golden else 1): other_minion = own_board.random_minion() if other_minion: other_minion.add_stats(0, minion.max_health)
def set_aside_dead_minions(self, own_board: PlayerBoard, enemy_board: PlayerBoard): """Remove any dead minions. Return the List of minions to process any deathrattles and reborn triggers. Arguments: board {PlayerBoard} -- Player board to check for dead minions """ dead_minions = own_board.select_dead() left_neighors = [] for minion in dead_minions: left_neighors.append(minion.left_neighbor) # Dead minions care about their left neighbor for deathrattle/reborn positioning for index, minion in enumerate(dead_minions): own_board.remove_minion(minion, enemy_board) minion.left_neighbor = left_neighors[index] return dead_minions
def resolve_extra_attacks(self, attacking_player_board: PlayerBoard, defending_player_board: PlayerBoard): """Resolve any 'attacks immediately' minions and the consequences of those attacks Arguments: attacking_player_board {PlayerBoard} -- Player board of attacking player defending_player_board {PlayerBoard} -- Player board of defending player """ for attacker in attacking_player_board.get_immediate_attack_minions(): attacker.immediate_attack_pending = False defender = defending_player_board.select_defending_minion() if defender: self.attack(attacker, defender, attacking_player_board, defending_player_board) self.check_deaths(attacking_player_board, defending_player_board) else: return
def trigger(self, minion: Minion, own_board: PlayerBoard, opposing_board: PlayerBoard, macaw_trigger: Optional[bool] = False): number_bombs = 2 if minion.golden else 1 for _ in range(number_bombs): opposing_minion = opposing_board.random_minion() if opposing_minion: opposing_minion.receive_damage(amount=4, own_board=opposing_board)
def kill(self, minion: Minion, minion_board: PlayerBoard, opposing_board: PlayerBoard, minion_defending_player: bool): """Kill a minion off and update board using deathrattles and other triggers Arguments: minion {Minion} -- Minion that will die minion_board {PlayerBoard} -- Player board belonging to the minion opposing_board {PlayerBoard} -- Board opposing of the minion that dies minion_defending_player {bool} -- Whether the minion died is on the defending side for trigger orders """ # TODO: Baron if minion_defending_player: opposing_board.remove_minion(minion) else: minion_board.remove_minion(minion) for deathrattle in minion.deathrattles: if minion_defending_player: deathrattle.trigger(minion, opposing_board, minion_board) else: deathrattle.trigger(minion, minion_board, opposing_board) self.check_deaths(minion_board, opposing_board)
def trigger(self, minion: Minion, own_board: PlayerBoard, opposing_board: PlayerBoard): target_minion = own_board.random_minion() if target_minion: target_minion.add_attack(minion.last_attack) logging.debug( f"Fiendish Servant deathrattle triggers onto {target_minion.minion_string()}" ) else: logging.debug( "Fiendish Servant deathrattle triggers but there are no targets left" )
def test_illidan_stormrage(initialized_game): initialized_game.player_board[0] = PlayerBoard(0, HeroType.ILLIDAN_STORMRAGE, 1, 1, [AcolyteOfCThun()]) initialized_game.player_board[1] = PlayerBoard( 0, None, 1, 1, [HarvestGolem(), PunchingBag(taunt=True)]) attacker_board = initialized_game.player_board[0] defender_board = initialized_game.player_board[1] # NOTE: Illidan player bonus attacks trigger before combat, even though opponent has more minons initialized_game.start_of_game() acolyte = attacker_board.minions[0] punching_bag = defender_board.minions[1] assert acolyte.attack == 4 assert punching_bag.health == 96 # Deathrattles and pirate attacks should resolve punching_bag = PunchingBag(attack=1) attacker_board.set_minions([Scallywag(), Alleycat()]) defender_board.set_minions([punching_bag]) initialized_game.start_of_game() assert punching_bag.health == 92 # Test windfury and attack order scallywag = Scallywag() punching_bag = PunchingBag() attacker_board.set_minions([scallywag, CracklingCyclone()]) defender_board.set_minions([punching_bag]) initialized_game.start_of_game() assert punching_bag.health == 84 first_attacker = attacker_board.select_attacking_minion() assert first_attacker == scallywag # Single minion only attacks once single_minion = PunchingBag() attacker_board.set_minions([single_minion]) defender_board.set_minions([PunchingBag()]) initialized_game.start_of_game() assert single_minion.attack == 2
def trigger(self, minion: Minion, own_board: PlayerBoard, opposing_board: PlayerBoard): if minion.golden: logging.debug( "Kaboom Bot (golden) deathrattle triggered, dealing 4 damage twice" ) else: logging.debug("Kaboom Bot deathrattle triggered, dealing 4 damage") number_bombs = 2 if minion.golden else 1 for bomb in range(number_bombs): # TODO: Should kill the unit before throwing a second bomb opposing_minion = opposing_board.random_minion() opposing_minion.receive_damage(amount=4, poisonous=False)
def trigger(self, minion: Minion, own_board: PlayerBoard, opposing_board: PlayerBoard, macaw_trigger: Optional[bool] = False): iterations = 2 if minion.golden else 1 for _ in range(iterations): unshielded_minions = [ minion for minion in own_board.get_living_minions() if not minion.divine_shield ] if len(unshielded_minions) > 0: unshielded_minions[random.randint(0, len(unshielded_minions) - 1)].divine_shield = True
def on_overkill(self, friendly_board: PlayerBoard, defending_minion: Minion, enemy_board: PlayerBoard): overkill_amount = defending_minion.health * -1 neighbors = [ x for x in enemy_board.get_minions_neighbors(defending_minion) if x ] num_neighbors = len(neighbors) if num_neighbors > 0: if self.golden: for i in range(num_neighbors): neighbors[i].receive_damage(overkill_amount, enemy_board) else: index = random.randint(0, num_neighbors - 1) neighbors[index].receive_damage(overkill_amount, enemy_board)
def determine_board_deathrattle_multiplier(self, own_board: PlayerBoard, count_self: bool): barons = [ minion for minion in own_board.minions if minion.name == "Baron Rivendare" ] if count_self: barons += [self] if any(baron.golden for baron in barons): multiplier = 3 elif len(barons) > 0: multiplier = 2 else: multiplier = 1 own_board.deathrattle_multiplier = multiplier
def test_tonytwotusk(initialized_game): non_golden_pirate = DreadAdmiralEliza() bag = PunchingBag(attack=10) initialized_game.player_board[0] = PlayerBoard( 0, HeroType.GREYBOUGH, 1, 1, [ ReplicatingMenace(), HarvestGolem(), non_golden_pirate, TonyTwoTusk() ]) attacker_board = initialized_game.player_board[0] defender_board = initialized_game.player_board[1] defender_board.set_minions([bag]) initialized_game.start_of_game() for _ in range(5): initialized_game.single_round() assert non_golden_pirate.golden assert non_golden_pirate.health == non_golden_pirate.base_health * 2 initialized_game.single_round() initialized_game.single_round() assert non_golden_pirate.attack == 16 assert non_golden_pirate.health == 6
import logging from utils.profile import Profile from game.game_instance import GameInstance from game.simulation import Simulation from game.player_board import PlayerBoard from minions.rank_1 import DragonspawnLieutenant, FiendishServant, RedWhelp, RighteousProtector, Mecharoo player_board_0 = PlayerBoard(player_id=0, hero=None, life_total=12, rank=4, minions=[ FiendishServant(), DragonspawnLieutenant(), DragonspawnLieutenant(), RedWhelp(), RedWhelp() ]) player_board_1 = PlayerBoard(player_id=1, hero=None, life_total=12, rank=4, minions=[ DragonspawnLieutenant(), Mecharoo(), FiendishServant(), DragonspawnLieutenant(), FiendishServant(),
def simulate_game_from_log(logPath): logreader = LogReader(logPath) turns = 0 turn_results = {} while True: board_state = logreader.watch_log_file_for_combat_state() if not board_state: break player_board_0 = PlayerBoard( player_id=0, hero=board_state.friendlyHero, life_total=board_state.friendlyPlayerHealth, rank=board_state.friendlyTechLevel, minions=board_state.friendlyBoard, enemy_is_deathwing=board_state.enemyHero is HeroType.DEATHWING) player_board_1 = PlayerBoard( player_id=1, hero=board_state.enemyHero, life_total=board_state.enemyPlayerHealth, rank=board_state.enemyTechLevel, minions=board_state.enemyBoard, enemy_is_deathwing=board_state.friendlyHero is HeroType.DEATHWING) try: single_threaded = False games = 10_000 game_state = (player_board_0, player_board_1) pickled_state = pickle.dumps(game_state) if single_threaded: results = [] for _ in range(games): results.append(Simulator.Simulate(pickled_state)) else: pool = Pool() results = pool.map(Simulator.Simulate, repeat(pickled_state, games)) pool.close() pool.join() counter = Counter(results) results = sorted(counter.items(), key=lambda x: x[0]) wins, losses, ties, enemy_lethal, friendly_lethal = 0.0, 0.0, 0.0, 0.0, 0.0 for result in results: damage = result[0] game_count = result[1] if damage > 0: wins += game_count if damage > player_board_1.life_total: enemy_lethal += game_count elif damage < 0: losses += game_count if (damage * -1) > player_board_0.life_total: friendly_lethal += game_count else: ties += game_count turn_results[turns] = [ 100 * enemy_lethal / games, 100 * wins / games, 100 * ties / games, 100 * losses / games, 100 * friendly_lethal / games ] turns += 1 except Exception as e: print(f"Game:{logPath}, turn:{turns}, error:{e}") turn_results[turns] = [0, 0, 0, 0, 0] turns += 1 return turn_results