Skip to content

Commit

Permalink
Merge pull request #4 from play-boardgames-together/dragons
Browse files Browse the repository at this point in the history
Descent of Dragons
  • Loading branch information
shinoi2 authored Apr 3, 2024
2 parents eab329e + 19cf0d7 commit c9be06e
Show file tree
Hide file tree
Showing 82 changed files with 2,984 additions and 435,188 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fireplace/cards/CardDefs.xml filter=lfs diff=lfs merge=lfs -text
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ jobs:

steps:
- uses: actions/checkout@v3
with:
lfs: 'true'
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ A Hearthstone simulator and implementation, written in Python.

## Cards Implementation

Now updated to [Patch 15.6.2.36393](https://hearthstone.wiki.gg/wiki/Patch_15.6.2.36393)
Now updated to [Patch 16.6.0.43246](https://hearthstone.wiki.gg/wiki/Patch_16.6.0.43246)
* **100%** Basic (142 of 142 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)
* **100%** The Grand Tournament (132 of 132 cards)
* **100%** Hero Skins (27 of 27 cards)
* **100%** Hero Skins (30 of 30 cards)
* **100%** The League of Explorers (45 of 45 cards)
* **100%** Whispers of the Old Gods (134 of 134 cards)
* **100%** One Night in Karazhan (45 of 45 cards)
Expand All @@ -30,8 +30,9 @@ Now updated to [Patch 15.6.2.36393](https://hearthstone.wiki.gg/wiki/Patch_15.6.
* **100%** Rastakhan's Rumble (135 of 135 cards)
* **100%** Rise of Shadows (136 of 136 cards)
* **99%** Saviours of Uldum (134 of 135 cards)
* **100%** Descent of Dragons (1 of 1 card)
* **100%** Wild Event (23 of 23 cards)
* **100%** Descent of Dragons (140 of 140 cards)
* **100%** Galakrond's Awakening (35 of 35 cards)
* **100%** Ashes of Outlands (1 of 1 card)

Not Implemented
* Zephrys the Great (ULD_003)
Expand Down
42 changes: 33 additions & 9 deletions fireplace/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1274,8 +1274,16 @@ class SpendMana(TargetedAction):
AMOUNT = IntArg()

def do(self, source, target, amount):
target.used_mana = max(target.used_mana + amount, 0)
log.info("%s pays %i mana", target, amount)
_amount = amount
if target.temp_mana:
# Coin, Innervate etc
used_temp = min(target.temp_mana, amount)
_amount -= used_temp
target.temp_mana -= used_temp
target.used_mana = max(target.used_mana + _amount, 0)
source.game.manager.targeted_action(self, source, target, amount)
self.broadcast(source, EventListener.AFTER, target, amount)


class SetMana(TargetedAction):
Expand Down Expand Up @@ -1314,6 +1322,7 @@ def do(self, source, target, cards):
card.zone = Zone.HAND
ret.append(card)
source.game.manager.targeted_action(self, source, target, card)
self.broadcast(source, EventListener.AFTER, target, card)
return ret


Expand All @@ -1332,9 +1341,9 @@ def do(self, source, target, amount):
return 0


class HitAndExcessDamageToHero(TargetedAction):
class HitExcessDamage(TargetedAction):
"""
Hit character targets by \a amount and excess damage to their hero.
Hit character targets by \a amount and excess damage to other.
"""
TARGET = ActionArg()
AMOUNT = IntArg()
Expand All @@ -1344,13 +1353,12 @@ def do(self, source, target, amount):
if amount:
source.game.manager.targeted_action(self, source, target, amount)
if target.health >= amount:
return source.game.queue_actions(source, [Predamage(target, amount)])[0][0]
source.game.queue_actions(source, [Predamage(target, amount)])
return 0
else:
excess_amount = amount - target.health
return source.game.queue_actions(source, [
Predamage(target, amount),
Predamage(target.controller.hero, excess_amount)
])[0]
source.game.queue_actions(source, [Predamage(target, amount)])
return excess_amount
return 0


Expand Down Expand Up @@ -1565,6 +1573,8 @@ class Silence(TargetedAction):

def do(self, source, target):
log.info("Silencing %r", self)
if target.type != CardType.MINION:
return
self.broadcast(source, EventListener.ON, target)
target.clear_buffs()
for attr in target.silenceable_attributes:
Expand Down Expand Up @@ -2038,7 +2048,7 @@ def do(self, source, target, card, amount=1):
source.game.manager.targeted_action(self, source, target, card, amount)
if target.progress >= target.progress_total:
source.game.trigger(target, target.get_actions("reward"), event_args=None)
if target.data.quest:
if target.data.quest or target.data.sidequest:
target.zone = Zone.GRAVEYARD


Expand Down Expand Up @@ -2082,3 +2092,17 @@ def do(self, source, target):
source.game.queue_actions(source, [CastSpell(target)])
else:
source.game.queue_actions(source, [Summon(source.controller, target)])


class Invoke(TargetedAction):
def do(self, source, player):
source.game.manager.targeted_action(self, source, player)
player.invoke_counter += 1
galakrond = player.galakrond
if not galakrond:
return
source.game.queue_actions(source, [
Reveal(galakrond),
PlayHeroPower(galakrond.data.hero_power, None),
AddProgress(galakrond, source)
])
115 changes: 104 additions & 11 deletions fireplace/card.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,14 @@ def Card(id):
CardType.WEAPON: Weapon,
CardType.HERO_POWER: HeroPower,
}[data.type]
if subclass is Spell and data.secret:
subclass = Secret
if subclass is Spell and data.quest:
subclass = Quest
if subclass is Spell:
if data.secret:
subclass = Secret
elif data.quest:
subclass = Quest
elif data.sidequest:
subclass = SideQuest

return subclass(data)


Expand Down Expand Up @@ -83,7 +87,7 @@ def __repr__(self):

def __eq__(self, other):
if isinstance(other, BaseCard):
return self.id.__eq__(other.id)
return self.entity_id.__eq__(other.entity_id)
elif isinstance(other, str):
return self.id.__eq__(other)
return super().__eq__(other)
Expand Down Expand Up @@ -266,6 +270,7 @@ class PlayableCard(BaseCard, Entity, TargetableByAuras):
keep_buff = boolean_property("keep_buff")
echo = boolean_property("echo")
has_overkill = boolean_property("has_overkill")
has_discover = boolean_property("has_discover")

def __init__(self, data):
self.cant_play = False
Expand Down Expand Up @@ -426,6 +431,13 @@ def is_playable(self):
if self.requirements[PlayReq.REQ_NUM_MINION_SLOTS] > self.controller.minion_slots:
return False

if PlayReq.REQ_BOARD_NOT_COMPLETELY_FULL in self.requirements:
if (
self.controller.minion_slots == 0 and
self.controller.opponent.minion_slots == 0
):
return False

min_enemy_minions = self.requirements.get(PlayReq.REQ_MINIMUM_ENEMY_MINIONS, 0)
if len(self.controller.opponent.field) < min_enemy_minions:
return False
Expand Down Expand Up @@ -608,7 +620,7 @@ def requires_target(self):
return bool(self.play_targets)
req = self.requirements.get(PlayReq.REQ_TARGET_IF_AVAILABLE_AND_HAS_OVERLOADED_MANA)
if req is not None:
if self.controller.overloaded > 0:
if self.controller.overloaded > 0 or self.controller.overload_locked > 0:
return bool(self.play_targets)
req = self.requirements.get(PlayReq.REQ_TARGET_IF_AVAILABLE_AND_DRAWN_THIS_TURN)
if req is not None:
Expand Down Expand Up @@ -641,6 +653,10 @@ def requires_target(self):
if req is not None:
if self.controller.field.filter(mark_of_evil=True):
return bool(self.play_targets)
req = self.requirements.get(PlayReq.REQ_STEADY_SHOT)
if req is not None:
if self.steady_shot_can_target:
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 Expand Up @@ -762,6 +778,7 @@ def hit(self, amount):
class Character(LiveEntity):
health_attribute = "health"
cant_attack = boolean_property("cant_attack")
cant_be_frozen = boolean_property("cant_be_frozen")
cant_be_targeted_by_opponents = boolean_property("cant_be_targeted_by_opponents")
cant_be_targeted_by_abilities = boolean_property("cant_be_targeted_by_abilities")
cant_be_targeted_by_hero_powers = boolean_property("cant_be_targeted_by_hero_powers")
Expand All @@ -776,7 +793,7 @@ class Character(LiveEntity):
stealthed = boolean_property("stealthed")

def __init__(self, data):
self.frozen = False
self._frozen = False
self.attack_target = None
self.num_attacks = 0
self.race = Race.INVALID
Expand Down Expand Up @@ -822,6 +839,18 @@ def attack_targets(self):

return (taunts or targets).filter(attackable=True)

@property
def frozen(self):
if self.cant_be_frozen:
self._frozen = False
return self._frozen

@frozen.setter
def frozen(self, value):
if self.cant_be_frozen:
value = False
self._frozen = value

def can_attack(self, target=None):
if self.controller.choice:
return False
Expand Down Expand Up @@ -898,6 +927,8 @@ def set_current_health(self, amount):


class Hero(Character):
galakrond_hero_card = boolean_property("galakrond_hero_card")

def __init__(self, data):
self.armor = 0
self.power = None
Expand Down Expand Up @@ -1040,18 +1071,29 @@ def ignore_scripts(self):
return self.silenced

@property
def adjacent_minions(self):
def left_minion(self):
assert self.zone is Zone.PLAY, self.zone
ret = CardList()
index = self.zone_position - 1
left = self.controller.field[:index]
right = self.controller.field[index + 1:]
if left:
ret.append(left[-1])
return ret

@property
def right_minion(self):
assert self.zone is Zone.PLAY, self.zone
ret = CardList()
index = self.zone_position - 1
right = self.controller.field[index + 1:]
if right:
ret.append(right[0])
return ret

@property
def adjacent_minions(self):
return self.left_minion + self.right_minion

@property
def attackable(self):
if self.stealthed:
Expand Down Expand Up @@ -1222,7 +1264,7 @@ def _set_zone(self, value):

def is_summonable(self):
# secrets are all unique
if self.controller.secrets.contains(self):
if self.controller.secrets.contains(self.id):
return False
if len(self.controller.secrets) >= self.game.MAX_SECRETS_ON_PLAY:
return False
Expand All @@ -1246,8 +1288,9 @@ def is_summonable(self):

def _set_zone(self, value):
if value == Zone.PLAY:
# Move secrets to the SECRET Zone when played
value = Zone.SECRET
if self.zone == Zone.SECRET:
self.controller.secrets.remove(self)
if value == Zone.SECRET:
self.controller.secrets.insert(0, self)
super()._set_zone(value)
Expand All @@ -1260,6 +1303,44 @@ def events(self):
return ret


class SideQuest(Spell):
spelltype = enums.SpellType.SIDEQUEST

@property
def zone_position(self):
if self.zone == Zone.SECRET:
return self.controller.secrets.index(self) + 1
return super().zone_position

def dump_hidden(self):
if self.zone == Zone.SECRET:
return self.dump()
return super().dump_hidden()

def is_summonable(self):
if self.controller.secrets.contains(self.id):
return False
if len(self.controller.secrets) >= self.game.MAX_SECRETS_ON_PLAY:
return False
return super().is_summonable()

def _set_zone(self, value):
if value == Zone.PLAY:
value = Zone.SECRET
if self.zone == Zone.SECRET:
self.controller.secrets.remove(self)
if value == Zone.SECRET:
self.controller.secrets.append(self)
super()._set_zone(value)

@property
def events(self):
ret = super().events
if self.zone == Zone.SECRET:
ret += self.data.scripts.sidequest
return ret


class Enchantment(BaseCard):
atk = int_property("atk")
cost = int_property("cost")
Expand Down Expand Up @@ -1381,6 +1462,7 @@ class HeroPower(PlayableCard):
heropower_disabled = int_property("heropower_disabled")
passive_hero_power = boolean_property("passive_hero_power")
playable_zone = Zone.PLAY
steady_shot_can_target = boolean_property("steady_shot_can_target")

def __init__(self, data):
super().__init__(data)
Expand All @@ -1402,6 +1484,17 @@ def exhausted(self):
return self.activations_this_turn >= (
1 + self.additional_activations + self.additional_activations_this_turn)

@property
def events(self):
if self.heropower_disabled:
return []
return super().events

@property
def update_scripts(self):
if not self.heropower_disabled:
yield from super().update_scripts

def _set_zone(self, value):
if value == Zone.PLAY:
if self.controller.hero.power:
Expand Down
Loading

0 comments on commit c9be06e

Please sign in to comment.