From 350f7b983de77de4d64a0c374b9faed8156a77f8 Mon Sep 17 00:00:00 2001 From: Mateus Cechetto Date: Tue, 11 Aug 2026 10:29:12 -0300 Subject: [PATCH] feat: use whitelist for sideboard cards that shuffle cards into the deck --- hearthstone/entities.py | 18 ++++++++++------- hearthstone/utils/__init__.py | 8 ++++++++ tests/test_entities.py | 38 ++++++++++++++++++++++++++++++++++- 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/hearthstone/entities.py b/hearthstone/entities.py index 0e58dff..6575c69 100644 --- a/hearthstone/entities.py +++ b/hearthstone/entities.py @@ -1,6 +1,8 @@ from typing import Dict, Iterable, Iterator, List, Optional, Tuple, Union, cast -from hearthstone.utils import MAESTRA_DISGUISE_DBF_ID, get_original_card_id +from hearthstone.utils import ( + MAESTRA_DISGUISE_DBF_ID, START_OF_GAME_SIDEBOARD_CARDS, get_original_card_id +) from .enums import CardSet, CardType, GameTag, State, Step, Zone from .typedefs import GameTagsDict @@ -220,9 +222,9 @@ def initial_deck(self) -> Iterator["Card"]: continue # Allow CREATOR=1 because of monster hunt decks. - # Cards shuffled into the deck at the start of the game by a card with a - # sideboard (eg. Commander Beatrix) are part of the deck list the player - # submitted, so they belong here too. + # Cards shuffled into the deck at the start of the game by one of the cards in + # START_OF_GAME_SIDEBOARD_CARDS (eg. Commander Beatrix) are part of the deck + # list the player submitted, so they belong here too. # Everything else is likely a false positive. if entity.initial_creator > 1 and not entity.is_sideboard_deck_card: continue @@ -337,8 +339,9 @@ def can_be_in_deck(self) -> bool: def _is_sideboard_deck_card(self, creator_id: int) -> bool: """ - Whether this card was shuffled into the deck during setup by a card that has a - sideboard (eg. Commander Beatrix, who adds copies of her sideboard card). + Whether this card was shuffled into the deck during setup by one of the cards in + START_OF_GAME_SIDEBOARD_CARDS (eg. Commander Beatrix, who adds copies of her + sideboard card). Those copies are created after the initial deck is dumped and carry a creator, but their identity is picked during deckbuilding and they are part of the deck list the @@ -352,7 +355,8 @@ def _is_sideboard_deck_card(self, creator_id: int) -> bool: return False creator = self.game.find_entity_by_id(creator_id) - return bool(creator and creator.tags.get(GameTag.MAX_SIDEBOARD_CARDS, 0)) + creator_card_id = getattr(creator, "initial_card_id", None) + return creator_card_id in START_OF_GAME_SIDEBOARD_CARDS @property def is_sideboard_deck_card(self) -> bool: diff --git a/hearthstone/utils/__init__.py b/hearthstone/utils/__init__.py index 0efec74..8aaa378 100644 --- a/hearthstone/utils/__init__.py +++ b/hearthstone/utils/__init__.py @@ -600,6 +600,14 @@ def get_copied_card_id_by_format(card_id, format_type): MAESTRA_DISGUISE_DBF_ID = 64674 +# Cards whose sideboard is shuffled into the deck at the start of the game. Those copies +# are picked during deckbuilding, so they are part of the deck list the player submitted - +# unlike the sideboards of eg. E.T.C., Band Manager, which are only created once the card +# is played. +START_OF_GAME_SIDEBOARD_CARDS = [ + "JAIL_397", # Commander Beatrix +] + if __name__ == "__main__": from enum import IntEnum diff --git a/tests/test_entities.py b/tests/test_entities.py index a187a8a..484c987 100644 --- a/tests/test_entities.py +++ b/tests/test_entities.py @@ -166,7 +166,8 @@ def test_initial_deck_with_sideboard_cards_shuffled_in_at_start_of_game( ): # Commander Beatrix shuffles 10 copies of her sideboard card into the deck # during CREATE_GAME, before the game is set up. The card was picked during - # deckbuilding, so the copies are part of the submitted deck list. + # deckbuilding, so the copies are part of the submitted deck list. She reveals + # herself first, which is what lets us recognise her as the creator. beatrix = Card(5, None) beatrix.tags.update({ GameTag.ZONE: Zone.DECK, @@ -203,6 +204,41 @@ def test_initial_deck_with_sideboard_cards_shuffled_in_at_start_of_game( assert list(player.initial_deck) == [beatrix, copy] assert player.known_starting_deck_list == ["JAIL_397", "CS2_231"] + def test_initial_deck_with_unlisted_sideboard_card_creating_during_setup( + self, game, player + ): + # Only the sideboards we know join the deck at the start of the game count. Having + # a sideboard is not enough on its own: a card that creates something in the deck + # during setup for any other reason must not have it counted as a deck card, which + # is why START_OF_GAME_SIDEBOARD_CARDS is an explicit list. + etc = Card(5, None) + etc.tags.update({ + GameTag.ZONE: Zone.DECK, + GameTag.CONTROLLER: player.player_id, + }) + game.register_entity(etc) + etc.reveal("ETC_080", { + GameTag.CARDTYPE: CardType.MINION, + GameTag.MAX_SIDEBOARD_CARDS: 3, + }) + + generated = Card(6, None) + generated.tags.update({ + GameTag.ZONE: Zone.DECK, + GameTag.CONTROLLER: player.player_id, + }) + game.register_entity(generated) + generated.tag_change(GameTag.DISPLAYED_CREATOR, etc.id) + generated.reveal("CS2_231", { + GameTag.CARDTYPE: CardType.MINION, + GameTag.CREATOR: etc.id, + GameTag.DISPLAYED_CREATOR: etc.id, + }) + + assert not generated.is_original_entity + assert generated.initial_card_id is None + assert list(player.initial_deck) == [etc] + def test_initial_deck_with_sideboard_cards_created_after_setup(self, game, player): # E.T.C., Band Manager also has a sideboard, but its cards are created once it is # played, long after setup. Those are not deck cards.