From 5bca24cd561bfcf53f057b2d5a136a14470469b0 Mon Sep 17 00:00:00 2001 From: shinoi2 Date: Fri, 8 Dec 2023 09:06:58 +0800 Subject: [PATCH 1/5] code format --- fireplace/dsl/selector.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/fireplace/dsl/selector.py b/fireplace/dsl/selector.py index c1f1e9f3b..f43404a4a 100644 --- a/fireplace/dsl/selector.py +++ b/fireplace/dsl/selector.py @@ -27,6 +27,7 @@ class Selector: Set operations preserve ordering (necessary for cards like Echo of Medivh, where ordering matters) """ + def eval(self, entities: List[BaseEntity], source: BaseEntity) -> List[BaseEntity]: return entities @@ -534,14 +535,14 @@ def CONTROLLED_BY(selector): NEUTRAL = AttrValue(GameTag.CLASS) == CardClass.NEUTRAL -LEFTMOST_FIELD = FuncSelector(lambda entities, source: - source.game.player1.field[:1] + source.game.player2.field[:1]) -RIGTHMOST_FIELD = FuncSelector(lambda entities, source: - source.game.player1.field[-1:] + source.game.player2.field[-1:]) -LEFTMOST_HAND = FuncSelector(lambda entities, source: - source.game.player1.hand[:1] + source.game.player2.hand[-1:]) -RIGTHMOST_HAND = FuncSelector(lambda entities, source: - source.game.player1.hand[:1] + source.game.player2.hand[-1:]) +LEFTMOST_FIELD = FuncSelector( + lambda entities, source: source.game.player1.field[:1] + source.game.player2.field[:1]) +RIGTHMOST_FIELD = FuncSelector( + lambda entities, source: source.game.player1.field[-1:] + source.game.player2.field[-1:]) +LEFTMOST_HAND = FuncSelector( + lambda entities, source: source.game.player1.hand[:1] + source.game.player2.hand[-1:]) +RIGTHMOST_HAND = FuncSelector( + lambda entities, source: source.game.player1.hand[:1] + source.game.player2.hand[-1:]) OUTERMOST_HAND = LEFTMOST_HAND + RIGTHMOST_HAND CARDS_PLAYED_THIS_GAME = FuncSelector( From 73fddd61b02e3e5ab97ba3972c51a0b4ff3455de Mon Sep 17 00:00:00 2001 From: shinoi2 Date: Wed, 13 Dec 2023 10:12:43 +0800 Subject: [PATCH 2/5] Fix bugs: * Weapon has multi-events bug * Refresh buff bug * Summon hero first and then summon heropower --- fireplace/card.py | 8 ++++---- fireplace/cards/brawl/pick_your_fate.py | 4 ++-- fireplace/cards/kobolds/neutral_common.py | 2 +- fireplace/entity.py | 2 +- fireplace/player.py | 13 ++++++++++--- tests/test_kobolds.py | 12 ++++++++++++ 6 files changed, 30 insertions(+), 11 deletions(-) diff --git a/fireplace/card.py b/fireplace/card.py index e8207506c..cebac3f96 100644 --- a/fireplace/card.py +++ b/fireplace/card.py @@ -259,7 +259,7 @@ def events(self): return self.data.scripts.Hand.events if self.zone == Zone.DECK: return self.data.scripts.Deck.events - return self.base_events + self._events + return self.base_events + list(self._events) @property def cost(self): @@ -338,7 +338,7 @@ def discard(self): self.zone = Zone.GRAVEYARD if old_zone == Zone.HAND: actions = self.get_actions("discard") - self.game.trigger(self, actions, event_args=None) + self.game.cheat_action(self, actions) def draw(self): if len(self.controller.hand) >= self.controller.max_hand_size: @@ -352,7 +352,7 @@ def draw(self): if self.game.step > Step.BEGIN_MULLIGAN: # Proc the draw script, but only if we are past mulligan actions = self.get_actions("draw") - self.game.trigger(self, actions, event_args=None) + self.game.cheat_action(self, actions) def heal(self, target, amount): return self.game.cheat_action(self, [actions.Heal(target, amount)]) @@ -1089,7 +1089,7 @@ def _set_zone(self, zone): if zone == Zone.PLAY: if self.controller.weapon: self.log("Destroying old weapon %r", self.controller.weapon) - self.game.trigger(self, [actions.Destroy(self.controller.weapon)], event_args=None) + self.controller.weapon.destroy() self.controller.weapon = self elif self.zone == Zone.PLAY: self.controller.weapon = None diff --git a/fireplace/cards/brawl/pick_your_fate.py b/fireplace/cards/brawl/pick_your_fate.py index 2ea1f901d..1fdd56cb9 100644 --- a/fireplace/cards/brawl/pick_your_fate.py +++ b/fireplace/cards/brawl/pick_your_fate.py @@ -210,7 +210,7 @@ class TB_PickYourFate_9: class TB_PickYourFate_9_Ench: - update = Refresh(FRIENDLY_MINIONS + DEATHRATTLE, "TB_PickYourFate_9_EnchMinion") + update = Refresh(FRIENDLY_MINIONS + DEATHRATTLE, buff="TB_PickYourFate_9_EnchMinion") TB_PickYourFate_9_EnchMinion = buff(+1, +1) @@ -222,7 +222,7 @@ class TB_PickYourFate_10: class TB_PickYourFate_10_Ench: - update = Refresh(FRIENDLY_MINIONS + BATTLECRY, "TB_PickYourFate_10_EnchMinion") + update = Refresh(FRIENDLY_MINIONS + BATTLECRY, buff="TB_PickYourFate_10_EnchMinion") TB_PickYourFate_10_EnchMinion = buff(+1, +1) diff --git a/fireplace/cards/kobolds/neutral_common.py b/fireplace/cards/kobolds/neutral_common.py index b2a647663..c5f6cebfc 100644 --- a/fireplace/cards/kobolds/neutral_common.py +++ b/fireplace/cards/kobolds/neutral_common.py @@ -41,7 +41,7 @@ class LOOT_134e: class LOOT_136: """Sneaky Devil""" # Stealth Your other minions have +1 Attack. - update = Refresh(FRIENDLY_MINIONS - SELF, "LOOT_136e") + update = Refresh(FRIENDLY_MINIONS - SELF, buff="LOOT_136e") LOOT_136e = buff(atk=1) diff --git a/fireplace/entity.py b/fireplace/entity.py index f918003c1..c05075c09 100644 --- a/fireplace/entity.py +++ b/fireplace/entity.py @@ -34,7 +34,7 @@ def is_card(self): @property def events(self): - return self.base_events + self._events + return self.base_events + list(self._events) @property def update_scripts(self): diff --git a/fireplace/player.py b/fireplace/player.py index 3dbb6e6d4..3484ba58a 100644 --- a/fireplace/player.py +++ b/fireplace/player.py @@ -177,6 +177,7 @@ def card(self, id, source=None, parent=None, zone=Zone.SETASIDE): def prepare_for_game(self): self.summon(self.starting_hero) + # self.game.trigger(self, [Summon(self, self.starting_hero)], event_args=None) self.starting_hero = self.hero for id in self.starting_deck: card = self.card(id, zone=Zone.DECK) @@ -190,8 +191,14 @@ def prepare_for_game(self): # Draw initial hand (but not any more than what we have in the deck) hand_size = min(len(self.deck), self.start_hand_size) # Quest cards are automatically included in the player's mulligan as the left-most card - quests = [card for card in self.deck if card.data.quest] - starting_hand = quests + random.sample(self.deck, hand_size - len(quests)) + quests = [] + exclude_quests = [] + for card in self.deck: + if card.data.quest: + quests.append(card) + else: + exclude_quests.append(card) + starting_hand = quests + random.sample(exclude_quests, hand_size - len(quests)) # It's faster to move cards directly to the hand instead of drawing for card in starting_hand: card.zone = Zone.HAND @@ -298,6 +305,6 @@ def summon(self, card): Puts \a card in the PLAY zone """ if isinstance(card, str): - card = self.card(card, zone=Zone.PLAY) + card = self.card(card) self.game.cheat_action(self, [Summon(self, card)]) return card diff --git a/tests/test_kobolds.py b/tests/test_kobolds.py index 589725d2e..d482c3c16 100644 --- a/tests/test_kobolds.py +++ b/tests/test_kobolds.py @@ -118,3 +118,15 @@ def test_crushing_walls(): game.player2.give("LOOT_522").play() assert len(game.player1.field) == 1 assert game.player1.field[0].id == CHICKEN + + +def test_dragon_soul(): + game = prepare_game() + game.player1.give("LOOT_209").play() + for _ in range(3): + game.player1.give(MOONFIRE).play(target=game.player2.hero) + assert len(game.player1.field) == 1 + game.skip_turn() + for _ in range(3): + game.player1.give(MOONFIRE).play(target=game.player2.hero) + assert len(game.player1.field) == 2 From 04bbbd6da60544f7f67cf2036bbbf4aed2c58db0 Mon Sep 17 00:00:00 2001 From: shinoi2 Date: Wed, 13 Dec 2023 11:08:02 +0800 Subject: [PATCH 3/5] Fix bugs * https://github.com/jleclanche/fireplace/issues/221 * https://github.com/jleclanche/fireplace/issues/226 --- fireplace/actions.py | 3 --- fireplace/card.py | 4 ++++ fireplace/dsl/copy.py | 5 ++++- tests/test_classic.py | 45 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/fireplace/actions.py b/fireplace/actions.py index df61ff829..591b9a0a2 100644 --- a/fireplace/actions.py +++ b/fireplace/actions.py @@ -1277,7 +1277,6 @@ class Silence(TargetedAction): def do(self, source, target): log.info("Silencing %r", self) self.broadcast(source, EventListener.ON, target) - old_health = target.health target.clear_buffs() for attr in target.silenceable_attributes: if getattr(target, attr): @@ -1286,8 +1285,6 @@ def do(self, source, target): # Wipe the event listeners target._events = [] target.silenced = True - if target.health < old_health: - target.damage = max(target.damage - (old_health - target.health), 0) class Summon(TargetedAction): diff --git a/fireplace/card.py b/fireplace/card.py index cebac3f96..d8b923a74 100644 --- a/fireplace/card.py +++ b/fireplace/card.py @@ -1041,9 +1041,13 @@ def _set_zone(self, zone): # Can happen if a Destroy is queued after a bounce, for example self.logger.warning("Trying to remove %r which is already gone", self) return + old_health = self.owner.health self.owner.buffs.remove(self) if self in self.game.active_aura_buffs: self.game.active_aura_buffs.remove(self) + if self.owner.health < old_health: + self.owner.damage = max(self.owner.damage - (old_health - self.owner.health), 0) + super()._set_zone(zone) def apply(self, target): diff --git a/fireplace/dsl/copy.py b/fireplace/dsl/copy.py index 4ff506cb6..7a2167d7b 100644 --- a/fireplace/dsl/copy.py +++ b/fireplace/dsl/copy.py @@ -51,5 +51,8 @@ def copy(self, source, entity): ret.damage = entity.damage for buff in entity.buffs: # Recreate the buff stack - entity.buff(ret, buff.id) + new_buff = buff.source.buff(ret, buff.id) + if buff in source.game.active_aura_buffs: + new_buff.tick = buff.tick + source.game.active_aura_buffs.append(new_buff) return ret diff --git a/tests/test_classic.py b/tests/test_classic.py index 91111f2de..09386ddfd 100644 --- a/tests/test_classic.py +++ b/tests/test_classic.py @@ -3747,3 +3747,48 @@ def test_ysera_awakens(): assert game.player1.hero.health == game.player2.hero.health == 30 - 5 assert len(game.board) == 1 assert ysera.health == 12 + + +def test_mirror_entity_aura(): + # https://github.com/jleclanche/fireplace/issues/221 + game = prepare_game() + game.end_turn() + game.player2.give("CS2_222").play() # Stormwind Champion + game.end_turn() + + mirror = game.player1.give("EX1_294") + mirror.play() + game.end_turn() + + # Mirror entity copies the exact nature of the card when it hits the field. + blademaster = game.player2.give("CS2_181") + blademaster.play() + assert len(game.player1.field) == 1 + assert len(game.player2.field) == 2 + assert game.player1.field[0].health == 4 + assert game.player1.field[0].max_health == 7 + assert game.player2.field[1].health == 4 + assert game.player2.field[1].max_health == 8 + + +def test_stormwind_champion_heal(): + # https://github.com/jleclanche/fireplace/issues/226 + game = prepare_game() + + goldshire = game.player1.summon(GOLDSHIRE_FOOTMAN) + assert goldshire.atk == 1 + assert goldshire.health == 2 + stormwind = game.player1.give("CS2_222") + stormwind.play() + assert goldshire.atk == 2 + assert goldshire.health == 3 + + game.player1.give(MOONFIRE).play(target=goldshire) + assert goldshire.atk == 2 + assert goldshire.health == 2 + game.end_turn() + + # Destroy with Fireball + game.player2.give(FIREBALL).play(target=stormwind) + assert goldshire.atk == 1 + assert goldshire.health == 2 From f9247452ad905cfb3f39607c66b3e4749733f190 Mon Sep 17 00:00:00 2001 From: shinoi2 Date: Wed, 13 Dec 2023 11:23:30 +0800 Subject: [PATCH 4/5] stash --- fireplace/card.py | 8 +++-- tests/test_classic.py | 74 +++++++++++++++++++++---------------------- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/fireplace/card.py b/fireplace/card.py index d8b923a74..323d54917 100644 --- a/fireplace/card.py +++ b/fireplace/card.py @@ -1041,12 +1041,14 @@ def _set_zone(self, zone): # Can happen if a Destroy is queued after a bounce, for example self.logger.warning("Trying to remove %r which is already gone", self) return - old_health = self.owner.health + if hasattr(self.owner, "health"): + old_health = self.owner.health self.owner.buffs.remove(self) if self in self.game.active_aura_buffs: self.game.active_aura_buffs.remove(self) - if self.owner.health < old_health: - self.owner.damage = max(self.owner.damage - (old_health - self.owner.health), 0) + if hasattr(self.owner, "health"): + if self.owner.health < old_health: + self.owner.damage = max(self.owner.damage - (old_health - self.owner.health), 0) super()._set_zone(zone) diff --git a/tests/test_classic.py b/tests/test_classic.py index 09386ddfd..06f734bb1 100644 --- a/tests/test_classic.py +++ b/tests/test_classic.py @@ -3751,44 +3751,44 @@ def test_ysera_awakens(): def test_mirror_entity_aura(): # https://github.com/jleclanche/fireplace/issues/221 - game = prepare_game() - game.end_turn() - game.player2.give("CS2_222").play() # Stormwind Champion - game.end_turn() - - mirror = game.player1.give("EX1_294") - mirror.play() - game.end_turn() - - # Mirror entity copies the exact nature of the card when it hits the field. - blademaster = game.player2.give("CS2_181") - blademaster.play() - assert len(game.player1.field) == 1 - assert len(game.player2.field) == 2 - assert game.player1.field[0].health == 4 - assert game.player1.field[0].max_health == 7 - assert game.player2.field[1].health == 4 - assert game.player2.field[1].max_health == 8 + game = prepare_game() + game.end_turn() + game.player2.give("CS2_222").play() # Stormwind Champion + game.end_turn() + + mirror = game.player1.give("EX1_294") + mirror.play() + game.end_turn() + + # Mirror entity copies the exact nature of the card when it hits the field. + blademaster = game.player2.give("CS2_181") + blademaster.play() + assert len(game.player1.field) == 1 + assert len(game.player2.field) == 2 + assert game.player1.field[0].health == 4 + assert game.player1.field[0].max_health == 7 + assert game.player2.field[1].health == 4 + assert game.player2.field[1].max_health == 8 def test_stormwind_champion_heal(): # https://github.com/jleclanche/fireplace/issues/226 - game = prepare_game() - - goldshire = game.player1.summon(GOLDSHIRE_FOOTMAN) - assert goldshire.atk == 1 - assert goldshire.health == 2 - stormwind = game.player1.give("CS2_222") - stormwind.play() - assert goldshire.atk == 2 - assert goldshire.health == 3 - - game.player1.give(MOONFIRE).play(target=goldshire) - assert goldshire.atk == 2 - assert goldshire.health == 2 - game.end_turn() - - # Destroy with Fireball - game.player2.give(FIREBALL).play(target=stormwind) - assert goldshire.atk == 1 - assert goldshire.health == 2 + game = prepare_game() + + goldshire = game.player1.summon(GOLDSHIRE_FOOTMAN) + assert goldshire.atk == 1 + assert goldshire.health == 2 + stormwind = game.player1.give("CS2_222") + stormwind.play() + assert goldshire.atk == 2 + assert goldshire.health == 3 + + game.player1.give(MOONFIRE).play(target=goldshire) + assert goldshire.atk == 2 + assert goldshire.health == 2 + game.end_turn() + + # Destroy with Fireball + game.player2.give(FIREBALL).play(target=stormwind) + assert goldshire.atk == 1 + assert goldshire.health == 2 From 4f6ebff3a8e2b93786abd3c887a2f273a30aadd6 Mon Sep 17 00:00:00 2001 From: shinoi2 Date: Wed, 13 Dec 2023 11:39:40 +0800 Subject: [PATCH 5/5] fix test_angry_chicken failed --- tests/test_classic.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_classic.py b/tests/test_classic.py index 06f734bb1..d14249517 100644 --- a/tests/test_classic.py +++ b/tests/test_classic.py @@ -237,11 +237,13 @@ def test_angry_chicken(): assert chicken.enrage assert not chicken.enraged assert chicken.atk == chicken.health == 2 + game.skip_turn() game.player1.give(MOONFIRE).play(target=chicken) assert chicken.enraged assert chicken.atk == 1 + 1 + 5 assert chicken.health == 1 - stormwind.destroy() + game.player1.give(FIREBALL).play(target=stormwind) + assert len(game.player1.field) == 1 assert chicken.atk == chicken.health == 1 assert not chicken.enraged