Skip to content

Commit

Permalink
Merge pull request #2 from play-boardgames-together/dalaran
Browse files Browse the repository at this point in the history
Rise of Shadows
  • Loading branch information
shinoi2 authored Mar 16, 2024
2 parents 336e0b4 + 2723259 commit a34fb6f
Show file tree
Hide file tree
Showing 50 changed files with 80,975 additions and 27,624 deletions.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ A Hearthstone simulator and implementation, written in Python.

## Cards Implementation

Now updated to [Patch 13.4.0.29349](https://hearthstone.fandom.com/wiki/Patch_13.4.0.29349)
Now updated to [Patch 14.6.0.31761](https://hearthstone.fandom.com/wiki/Patch_14.6.0.31761)
* **100%** Basic (142 of 142 cards)
* **100%** Classic (240 of 240 cards)
* **100%** Hall of Fame (13 of 13 cards)
* **100%** Classic (245 of 245 cards)
* **100%** Hall of Fame (24 of 24 cards)
* **100%** Curse of Naxxramas (30 of 30 cards)
* **100%** Goblins vs Gnomes (123 of 123 cards)
* **100%** Blackrock Mountain (31 of 31 cards)
Expand All @@ -24,9 +24,10 @@ Now updated to [Patch 13.4.0.29349](https://hearthstone.fandom.com/wiki/Patch_13
* **100%** Journey to Un'Goro (135 of 135 cards)
* **100%** Knights of the Frozen Throne (135 of 135 cards)
* **100%** Kobolds & Catacombs (135 of 135 cards)
* **100%** The Witchwood (135 of 135 cards)
* **100%** The Boomsday Project (135 of 135 cards)
* **100%** The Witchwood (129 of 129 cards)
* **100%** The Boomsday Project (136 of 136 cards)
* **100%** Rastakhan's Rumble (135 of 135 cards)
* **100%** Rise of Shadows (136 of 136 cards)

## Requirements

Expand Down
104 changes: 95 additions & 9 deletions fireplace/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
from collections import OrderedDict

from hearthstone.enums import (
BlockType, CardClass, CardType, GameTag, Mulligan, Race, PlayState, Step, Zone
BlockType, CardClass, CardType, GameTag, Mulligan, PlayState, Race, Step, Zone
)

from .enums import DISCARDED
from .cards import db
from .dsl import LazyNum, LazyValue, Selector
from .dsl.copy import Copy
from .dsl.random_picker import RandomBeast, RandomCollectible, RandomMinion
from .dsl.random_picker import RandomBeast, RandomCollectible, RandomMinion, RandomSpell
from .dsl.selector import SELF
from .entity import Entity
from .enums import DISCARDED
from .exceptions import InvalidAction
from .logging import log
from .utils import get_script_definition, random_class
Expand Down Expand Up @@ -142,9 +143,18 @@ def _broadcast(self, entity, source, at, *args):
for event in entity.events:
if event.at != at:
continue
if isinstance(event.trigger, self.__class__) and event.trigger.matches(entity, args):
if (
isinstance(event.trigger, self.__class__) and
event.trigger.matches(entity, source, args)
):
log.info("%r triggers off %r from %r", entity, self, source)
entity.trigger_event(source, event, args)
if (
entity.type == CardType.SPELL and
entity.data.secret and
entity.controller.extra_trigger_secret
):
entity.trigger_event(source, event, args)

def broadcast(self, source, at, *args):
source.game.action_start(BlockType.TRIGGER, source, 0, None)
Expand All @@ -171,7 +181,7 @@ def resolve_broadcasts(self):
def get_args(self, source):
return self._args

def matches(self, source, args):
def matches(self, entity, source, args):
for arg, match in zip(args, self._args):
if match is None:
# Allow matching Action(None, None, z) to Action(x, y, z)
Expand All @@ -185,9 +195,13 @@ def matches(self, source, args):
return False
else:
# this stuff is stupidslow
res = match.eval([arg], source)
res = match.eval([arg], entity)
if not res or res[0] is not arg:
return False
if hasattr(self, "source") and self.source:
res = self.source.eval([source], entity)
if not res or res[0] is not source:
return False
return True

def trigger_choice_callback(self):
Expand Down Expand Up @@ -1084,6 +1098,7 @@ def choose(self, card):
for action in self._callback:
self.source.game.trigger(
self.source, [action], [self.target, self.cards, card])
self.callback = self._callback
self.trigger_choice_callback()


Expand Down Expand Up @@ -1629,6 +1644,7 @@ def do(self, source, target, cards):
card.zone = Zone.DECK
target.shuffle_deck()
source.game.manager.targeted_action(self, source, target, card)
self.broadcast(source, EventListener.AFTER, target, card)


class Swap(TargetedAction):
Expand Down Expand Up @@ -1759,11 +1775,15 @@ def do(self, source, card, targets):
card.zone = Zone.PLAY
log.info("%s cast spell %s target %s", source, card, target)
source.game.manager.targeted_action(self, source, card, target)
source.game.queue_actions(source, [Battlecry(card, card.target)])
source.game.queue_actions(card, [Battlecry(card, card.target)])
while player.choice:
choice = random.choice(player.choice.cards)
log.info("Choosing card %r" % (choice))
player.choice.choose(choice)
while player.opponent.choice:
choice = random.choice(player.opponent.choice.cards)
log.info("Choosing card %r" % (choice))
player.opponent.choice.choose(choice)
player.choice = old_choice
source.game.queue_actions(source, [Deaths()])

Expand Down Expand Up @@ -2088,10 +2108,10 @@ class CreateZombeast(TargetedAction):
def init(self, source):
hunter_beast_ids = RandomBeast(
card_class=CardClass.HUNTER,
cost=list(range(0, 6))).find_cards(source)
cost=range(0, 6)).find_cards(source)
neutral_beast_ids = RandomBeast(
card_class=CardClass.NEUTRAL,
cost=list(range(0, 6))).find_cards(source)
cost=range(0, 6)).find_cards(source)
beast_ids = hunter_beast_ids + neutral_beast_ids
self.first_ids = []
self.second_ids = []
Expand Down Expand Up @@ -2193,3 +2213,69 @@ def do(self, source, target):
source.game.queue_actions(source, [CastSpell(target)])
else:
source.game.queue_actions(source, [Summon(source.controller, target)])


class SwampqueenHagathaAction(TargetedAction):
def init(self, source):
self.all_shaman_spells = RandomSpell(card_class=CardClass.SHAMAN).find_cards(source)
self.targeted_spells = []
self.non_targeted_spells = []
for id in self.all_shaman_spells:
if db[id].requirements:
self.targeted_spells.append(id)
else:
self.non_targeted_spells.append(id)

def do(self, source, player):
self.init(source)
self.player = player
self.source = source
self.min_count = 1
self.max_count = 1
self.choosed_cards = []
self.player.choice = self
self.do_step1()
source.game.manager.targeted_action(self, source, player)

def do_step1(self):
self.cards = [
self.player.card(id) for id in random.sample(self.all_shaman_spells, 3)]

def do_step2(self):
if self.cards[0] in self.targeted_spells:
self.cards = [
self.player.card(id) for id in random.sample(self.non_targeted_spells, 3)]
else:
self.cards = [
self.player.card(id) for id in random.sample(self.all_shaman_spells, 3)]

def done(self):
card1 = self.choosed_cards[0]
card2 = self.choosed_cards[1]

horror = self.player.card("DAL_431t")
horror.custom_card = True

def create_custom_card(horror):
horror.data.scripts.play = card1.data.scripts.play + card2.data.scripts.play
horror.requirements = card1.requirements | card2.requirements
horror.tags[GameTag.CARDTEXT_ENTITY_0] = card1.data.strings[GameTag.CARDTEXT]
horror.tags[GameTag.CARDTEXT_ENTITY_1] = card2.data.strings[GameTag.CARDTEXT]
horror.tags[GameTag.OVERLOAD] = card1.tags[GameTag.OVERLOAD] + \
card2.tags[GameTag.OVERLOAD]

horror.create_custom_card = create_custom_card
horror.create_custom_card(horror)
self.player.give(horror)

def choose(self, card):
if card not in self.cards:
raise InvalidAction("%r is not a valid choice (one of %r)" % (card, self.cards))
else:
self.choosed_cards.append(card)
if len(self.choosed_cards) == 1:
self.do_step2()
elif len(self.choosed_cards) == 2:
self.player.choice = None
self.done()
self.trigger_choice_callback()
67 changes: 63 additions & 4 deletions fireplace/card.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from hearthstone.enums import CardType, GameTag, MultiClassGroup, PlayReq, PlayState, \
Race, Rarity, Step, Zone
from hearthstone.utils import LACKEY_CARDS

from . import actions, cards, enums, rules
from .aura import TargetableByAuras
Expand Down Expand Up @@ -391,13 +392,15 @@ def is_playable(self):
if self.parent_card:
zone = self.parent_card.zone
playable_zone = self.parent_card.playable_zone
if not self.controller.can_pay_cost(self.parent_card):
return False
else:
zone = self.zone
playable_zone = self.playable_zone
if zone != playable_zone:
return False
if not self.controller.can_pay_cost(self):
return False

if not self.controller.can_pay_cost(self):
if zone != playable_zone:
return False

if self.must_choose_one:
Expand Down Expand Up @@ -438,6 +441,43 @@ def is_playable(self):
if len(self.controller.secrets) >= self.game.MAX_SECRETS_ON_PLAY:
return False

if PlayReq.REQ_MINION_SLOT_OR_MANA_CRYSTAL_SLOT in self.requirements:
if (
len(self.controller.game.board) >= self.game.MAX_MINIONS_ON_FIELD and
self.controller.max_mana >= self.controller.max_resources
):
return False

if PlayReq.REQ_MUST_PLAY_OTHER_CARD_FIRST in self.requirements:
if len(self.controller.cards_played_this_turn) == 0:
return False

if PlayReq.REQ_HAND_NOT_FULL in self.requirements:
if len(self.controller.hand) >= self.controller.max_hand_size:
return False

if PlayReq.REQ_CANNOT_PLAY_THIS in self.requirements:
return False

if PlayReq.REQ_FRIENDLY_MINIONS_OF_RACE_DIED_THIS_GAME in self.requirements:
race = self.requirements.get(PlayReq.REQ_FRIENDLY_MINIONS_OF_RACE_DIED_THIS_GAME, 0)
if not self.controller.graveyard.filter(type=CardType.MINION, race=race):
return False

if PlayReq.REQ_FRIENDLY_MINION_OF_RACE_DIED_THIS_TURN in self.requirements:
race = self.requirements.get(PlayReq.REQ_FRIENDLY_MINIONS_OF_RACE_DIED_THIS_GAME, 0)
if not self.controller.graveyard.filter(killed_this_turn=True, race=race):
return False

if PlayReq.REQ_FRIENDLY_MINION_OF_RACE_IN_HAND in self.requirements:
race = self.requirements.get(PlayReq.REQ_FRIENDLY_MINION_OF_RACE_IN_HAND, 0)
if not self.controller.hand.filter(races=race):
return False

if PlayReq.REQ_FRIENDLY_DEATHRATTLE_MINION_DIED_THIS_GAME in self.requirements:
if not self.controller.graveyard.filter(has_deathrattle=True):
return False

return self.is_summonable()

def play(self, target=None, index=None, choose=None):
Expand Down Expand Up @@ -562,10 +602,29 @@ def requires_target(self):
if req is not None:
if self not in self.controller.cards_drawn_this_turn:
return bool(self.play_targets)
req = self.requirements.get(PlayReq.REQ_DRAG_TO_PLAY_PRE29933)
req = self.requirements.get(
PlayReq.REQ_TARGET_IF_AVAILABLE_AND_ONLY_EVEN_COST_CARD_IN_DECK)
if req is not None:
if all(card.cost % 2 == 0 for card in self.controller.deck):
return bool(self.play_targets)
req = self.requirements.get(
PlayReq.REQ_TARGET_IF_AVAILABLE_AND_ONLY_ODD_COST_CARD_IN_DECK)
if req is not None:
if all(card.cost % 2 == 1 for card in self.controller.deck):
return bool(self.play_targets)
req = self.requirements.get(
PlayReq.REQ_TARGET_IF_AVAILABLE_AND_COST_5_OR_MORE_SPELL_IN_HAND)
if req is not None:
if self.controller.hand.filter(cost=range(5, 100)):
return bool(self.play_targets)
req = self.requirements.get(PlayReq.REQ_TARGET_IF_AVAILABLE_AND_MIN_MANA_CRYSTAL)
if req is not None:
if self.controller.max_mana >= req:
return bool(self.play_targets)
req = self.requirements.get(PlayReq.REQ_TARGET_IF_AVAILABLE_AND_FRIENDLY_LACKEY)
if req is not None:
if self.controller.filed.filter(id=LACKEY_CARDS):
return bool(self.play_targets)
# req = self.requirements.get(
# PlayReq.REQ_TARGET_IF_AVAILABLE_AND_PLAYER_HEALTH_CHANGED_THIS_TURN)
# if req is not None:
Expand Down
Loading

0 comments on commit a34fb6f

Please sign in to comment.