Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 44 additions & 3 deletions hearthstone/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def __init__(self, id):
self.initial_creator = 0
self.initial_zone: Zone = Zone.INVALID
self.initial_zone_position = 0
self.is_initial_entity = False
self._initial_controller = 0

def __repr__(self):
Expand Down Expand Up @@ -119,6 +120,7 @@ def register_entity(self, entity: Entity) -> None:
if isinstance(entity, Player):
self.players.append(entity)
elif not self.setup_done:
entity.is_initial_entity = True
self.initial_entities.append(entity)

# Infer player class and card from "Maestra of the Masquerade" revealing herself
Expand Down Expand Up @@ -218,8 +220,11 @@ 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.
# Everything else is likely a false positive.
if entity.initial_creator > 1:
if entity.initial_creator > 1 and not entity.is_sideboard_deck_card:
continue

yield entity
Expand Down Expand Up @@ -330,6 +335,29 @@ def can_be_in_deck(self) -> bool:

return CardType(card_type).playable

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).

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
player submitted. That sets them apart from cards generated by effects such as
Azalina Soulsever's deck rule, whose identity is random and unknowable up front.
"""
if not self.is_initial_entity or self.initial_zone != Zone.DECK:
return False

if not creator_id or self.game is None:
return False

creator = self.game.find_entity_by_id(creator_id)
return bool(creator and creator.tags.get(GameTag.MAX_SIDEBOARD_CARDS, 0))

@property
def is_sideboard_deck_card(self) -> bool:
return self._is_sideboard_deck_card(self.initial_creator)

def _capture_initial_card_id(self, card_id: str, tags: GameTagsDict) -> None:
if self.initial_card_id:
# If we already know a previous card id, we do not want to change it.
Expand Down Expand Up @@ -372,12 +400,25 @@ def reveal(self, card_id: str, tags: GameTagsDict) -> None:
self.revealed = True
self.card_id = card_id

if (
created = (
tags.get(GameTag.CREATOR_DBID, 0) or
tags.get(GameTag.DISPLAYED_CREATOR, 0)
)

# The creator may only be known from an earlier tag change, so fall back to it.
creator_id = (
tags.get(GameTag.CREATOR, 0) or
tags.get(GameTag.DISPLAYED_CREATOR, 0) or
tags.get(GameTag.TRANSFORMED_FROM_CARD, 0)
self.initial_creator
)

if (
tags.get(GameTag.TRANSFORMED_FROM_CARD, 0) or
(created and not self._is_sideboard_deck_card(creator_id))
):
# Cards that are revealed with a creator most likely have been transformed.
# Sideboard cards shuffled into the deck at the start of the game are the
# exception: they are revealed with their real, original card id.
self.is_original_entity = False

self._capture_initial_card_id(card_id, tags)
Expand Down
75 changes: 75 additions & 0 deletions tests/test_entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,81 @@ def test_initial_deck_with_cards_generated_at_start_of_game(self, game, player):
assert generated.initial_creator == azalina.id
assert list(player.initial_deck) == [azalina]

def test_initial_deck_with_sideboard_cards_shuffled_in_at_start_of_game(
self, game, player
):
# 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.
beatrix = Card(5, None)
beatrix.tags.update({
GameTag.ZONE: Zone.DECK,
GameTag.CONTROLLER: player.player_id,
})
game.register_entity(beatrix)
beatrix.reveal("JAIL_397", {
GameTag.CARDTYPE: CardType.MINION,
GameTag.MAX_SIDEBOARD_CARDS: 1,
})

# The copies are created knowing nothing but their zone, and the creator only
# follows as a separate tag change.
copy = Card(6, None)
copy.tags.update({
GameTag.ZONE: Zone.DECK,
GameTag.CONTROLLER: player.player_id,
})
game.register_entity(copy)
copy.tag_change(GameTag.DISPLAYED_CREATOR, beatrix.id)

# Once drawn, the copy is revealed with its real card id - alongside the creator
# tags that would normally mark it as a generated card.
copy.reveal("CS2_231", {
GameTag.CARDTYPE: CardType.MINION,
GameTag.CREATOR: beatrix.id,
GameTag.DISPLAYED_CREATOR: beatrix.id,
GameTag.CREATOR_DBID: 126621,
})

assert copy.initial_creator == beatrix.id
assert copy.is_original_entity
assert copy.initial_card_id == "CS2_231"
assert list(player.initial_deck) == [beatrix, copy]
assert player.known_starting_deck_list == ["JAIL_397", "CS2_231"]

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.
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,
})

game.tag_change(GameTag.NEXT_STEP, Step.MAIN_ACTION)

band_card = Card(6, None)
band_card.tags.update({
GameTag.ZONE: Zone.DECK,
GameTag.CONTROLLER: player.player_id,
})
game.register_entity(band_card)
band_card.tag_change(GameTag.DISPLAYED_CREATOR, etc.id)
band_card.reveal("CS2_231", {
GameTag.CARDTYPE: CardType.MINION,
GameTag.CREATOR: etc.id,
GameTag.DISPLAYED_CREATOR: etc.id,
})

assert not band_card.is_original_entity
assert band_card.initial_card_id is None
assert list(player.initial_deck) == [etc]

def test_initial_deck_with_card_that_is_its_own_creator(self, game, player):
# Direhorn Hatchling tags itself as its own DISPLAYED_CREATOR when its Deathrattle
# shuffles a Direhorn Matriarch into the deck. It is still an original deck card.
Expand Down
Loading