Skip to content

Commit

Permalink
The Witchwood all cards, without tests (#508)
Browse files Browse the repository at this point in the history
* code format

* init witchwood

* WIP

* The Witchwood - Druid & Hunter

* update readme

* Raven Year

* Fix bugs:
* Weapon has multi-events bug
* Refresh buff bug
* Summon hero first and then summon heropower

* The Witchwood all cards, without tests

* update README.md

* Resolve conversation
  • Loading branch information
shinoi2 authored Dec 27, 2023
1 parent f39c128 commit 82b00c6
Show file tree
Hide file tree
Showing 53 changed files with 41,958 additions and 5,846 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ A Hearthstone simulator and implementation, written in Python.

## Cards Implementation

Now updated to [Patch 10.4.0.23576](https://hearthstone.fandom.com/wiki/Patch_10.4.0.23576)
Now updated to [Patch 11.4.0.25252](https://hearthstone.fandom.com/wiki/Patch_11.4.0.252526)
* **100%** Basic (142 of 142 cards)
* **100%** Classic (239 of 239 cards)
* **100%** Hall of Fame (10 of 10 cards)
Expand All @@ -24,7 +24,7 @@ Now updated to [Patch 10.4.0.23576](https://hearthstone.fandom.com/wiki/Patch_10
* **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)

## Requirements

Expand Down
73 changes: 62 additions & 11 deletions fireplace/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
BlockType, CardClass, CardType, GameTag, Mulligan, Race, PlayState, Step, Zone
)

from fireplace.dsl.selector import SELF

from .dsl import LazyNum, LazyValue, Selector
from .dsl.copy import Copy
from .dsl.random_picker import RandomBeast, RandomCollectible, RandomMinion
from .entity import Entity
from .exceptions import InvalidAction
Expand Down Expand Up @@ -448,6 +451,9 @@ def do(self, source, card, target, index, choose):
elif trigger_battlecry:
source.game.queue_actions(card, [Battlecry(battlecry_card, card.target)])

if card.echo:
source.game.queue_actions(card, [Give(player, Buff(Copy(SELF), "GIL_000"))])

# If the play action transforms the card (eg. Druid of the Claw), we
# have to broadcast the morph result as minion instead.
played_card = card.morphed or card
Expand All @@ -460,9 +466,9 @@ def do(self, source, card, target, index, choose):
player.cards_played_this_turn += 1
if card.type == CardType.MINION:
player.minions_played_this_turn += 1
if card.race == Race.TOTEM:
if Race.TOTEM in card.races:
card.controller.times_totem_summoned_this_game += 1
if card.race == Race.ELEMENTAL:
if Race.ELEMENTAL in card.races:
player.elemental_played_this_turn += 1
player.cards_played_this_game.append(card)
card.choose = None
Expand Down Expand Up @@ -798,7 +804,7 @@ def get_target_args(self, source, target):
def do(self, source, target, amount):
amount = target._hit(target.predamage)
target.predamage = 0
if source.type == CardType.MINION and source.stealthed:
if (source.type == CardType.MINION or source.type == CardType.HERO) and source.stealthed:
# TODO this should be an event listener of sorts
source.stealthed = False
if amount:
Expand All @@ -811,6 +817,7 @@ def do(self, source, target, amount):
if hasattr(source, "poisonous") and source.poisonous and (
target.type != CardType.HERO and source.type != CardType.WEAPON):
target.destroy()
target.damage_this_turn += amount
return amount


Expand Down Expand Up @@ -851,7 +858,7 @@ def get_target_args(self, source, target):
arg = _eval_card(source, arg)[0]
return [arg]

def do(self, source, card, target):
def do(self, source, card, target=None):
player = card.controller

if card.has_combo and player.combo:
Expand All @@ -861,6 +868,13 @@ def do(self, source, card, target):
log.info("Activating %r action targeting %r", card, target)
actions = card.get_actions("play")

if card.requires_target() and target is None:
if len(card.targets):
target = random.choice(card.targets)
else:
log.info("%s battlecry %s don't have a legal target", source, card)
return

source.target = target
source.game.main_power(source, actions, target)

Expand Down Expand Up @@ -1236,9 +1250,10 @@ class Reveal(TargetedAction):
"""

def do(self, source, target):
log.info("Revealing secret %r", target)
self.broadcast(source, EventListener.ON, target)
target.zone = Zone.GRAVEYARD
log.info("Revealing %r", target)
if target.zone == Zone.SECRET and target.data.secret:
self.broadcast(source, EventListener.ON, target)
target.zone = Zone.GRAVEYARD


class SetCurrentHealth(TargetedAction):
Expand All @@ -1252,6 +1267,7 @@ def do(self, source, target, amount):
log.info("Setting current health on %r to %i", target, amount)
maxhp = target.max_health
target.damage = max(0, maxhp - amount)
return target


class SetTag(TargetedAction):
Expand Down Expand Up @@ -1342,7 +1358,7 @@ def do(self, source, target, cards):
# card._summon_index = source_index + ((self.trigger_index + 1) % 2)
card._summon_index = self.get_summon_index(source_index)
card.zone = Zone.PLAY
if card.type == CardType.MINION and card.race == Race.TOTEM:
if card.type == CardType.MINION and Race.TOTEM in card.races:
card.controller.times_totem_summoned_this_game += 1
self.queue_broadcast(self, (source, EventListener.ON, target, card))
self.broadcast(source, EventListener.AFTER, target, card)
Expand Down Expand Up @@ -1570,7 +1586,7 @@ def do(self, source, target):
target.num_attacks -= 1


class SwapState(TargetedAction):
class SwapStateBuff(TargetedAction):
"""
Swap stats between two minions using \a buff.
"""
Expand All @@ -1593,7 +1609,7 @@ def do(self, source, target, other, buff):
buff2.apply(other)


class CopyState(TargetedAction):
class CopyStateBuff(TargetedAction):
"""
Copy target state, buff on self
"""
Expand Down Expand Up @@ -1744,7 +1760,6 @@ class GameStart(GameAction):
def do(self, source):
log.info("Game start")
self.broadcast(source, EventListener.ON)
self.broadcast(source, EventListener.AFTER)


class Adapt(TargetedAction):
Expand Down Expand Up @@ -1785,6 +1800,9 @@ def choose(self, card):


class AddProgress(TargetedAction):
"""
Add Progress for target, such as quest card and upgradeable card
"""
TARGET = ActionArg()
CARD = CardArg()
AMOUNT = IntArg()
Expand All @@ -1801,12 +1819,18 @@ def do(self, source, target, card, amount=1):


class ClearProgress(TargetedAction):
"""
Clear Progress for target
"""
def do(self, source, target):
log.info("%r clear progress", target)
target.clear_progress()


class GlimmerrootAction(TargetedAction):
"""
Curious Glimmerroot (UNG_035)
"""
def do(self, source, player):
self.player = player
self.source = source
Expand Down Expand Up @@ -1852,6 +1876,10 @@ def choose(self, card):


class CreateZombeast(TargetedAction):
"""
Build-A-Beast (ICC_828p)
Heropower of Deathstalker Rexxar
"""
def init(self, source):
hunter_beast_ids = RandomBeast(
card_class=CardClass.HUNTER,
Expand Down Expand Up @@ -1923,6 +1951,29 @@ def choose(self, card):


class LosesDivineShield(TargetedAction):
"""
Losses Divine Shield
"""
def do(self, source, target):
target.divine_shield = False
self.broadcast(source, EventListener.AFTER, target)


class Remove(TargetedAction):
"""
Remove character targets
"""
def do(self, source, target):
target.zone = Zone.REMOVEDFROMGAME


class Replay(TargetedAction):
"""
Cast it if it's spell, otherwise summon it (minion, weapon, hero).
Now only for Tess Greymane (GIL_598)
"""
def do(self, source, target):
if target.type == CardType.SPELL:
source.game.queue_actions(source, [CastSpell(target)])
else:
source.game.queue_actions(source, [Summon(source.controller, target)])
19 changes: 16 additions & 3 deletions fireplace/card.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ class PlayableCard(BaseCard, Entity, TargetableByAuras):
playable_zone = Zone.HAND
lifesteal = boolean_property("lifesteal")
keep_buff = boolean_property("keep_buff")
echo = boolean_property("echo")

def __init__(self, data):
self.cant_play = False
Expand All @@ -264,7 +265,7 @@ def events(self):
@property
def cost(self):
ret = 0
if self.zone == Zone.HAND:
if self.zone == Zone.HAND and self.game.turn > 0:
mod = self.data.scripts.cost_mod
if mod is not None:
r = mod.evaluate(self)
Expand Down Expand Up @@ -484,7 +485,7 @@ def requires_target(self):
if PlayReq.REQ_TARGET_IF_AVAILABLE in self.requirements:
return bool(self.play_targets)
if PlayReq.REQ_TARGET_IF_AVAILABLE_AND_DRAGON_IN_HAND in self.requirements:
if self.controller.hand.filter(race=Race.DRAGON):
if self.controller.hand.filter(races=Race.DRAGON):
return bool(self.play_targets)
req = self.requirements.get(PlayReq.REQ_TARGET_IF_AVAILABLE_AND_MINIMUM_FRIENDLY_MINIONS)
if req is not None:
Expand Down Expand Up @@ -534,6 +535,7 @@ def __init__(self, data):
self.predamage = 0
self.turns_in_play = 0
self.turn_killed = -1
self.damage_this_turn = 0

def _set_zone(self, zone):
if zone == Zone.GRAVEYARD and self.zone == Zone.PLAY:
Expand Down Expand Up @@ -617,6 +619,7 @@ class Character(LiveEntity):
ignore_taunt = boolean_property("ignore_taunt")
cannot_attack_heroes = boolean_property("cannot_attack_heroes")
unlimited_attacks = boolean_property("unlimited_attacks")
stealthed = boolean_property("stealthed")

def __init__(self, data):
self.frozen = False
Expand Down Expand Up @@ -688,6 +691,17 @@ def exhausted(self):
return True
return False

@property
def races(self):
if self.race == Race.ALL:
return [
Race.ELEMENTAL, Race.MECHANICAL,
Race.DEMON, Race.DRAGON,
Race.MURLOC, Race.BEAST,
Race.PIRATE, Race.TOTEM,
]
return [self.race]

@property
def should_exit_combat(self):
if self.attacking:
Expand Down Expand Up @@ -794,7 +808,6 @@ class Minion(Character):
charge = boolean_property("charge")
has_inspire = boolean_property("has_inspire")
spellpower = int_property("spellpower")
stealthed = boolean_property("stealthed")

silenceable_attributes = (
"always_wins_brawls", "aura", "cant_attack", "cant_be_targeted_by_abilities",
Expand Down
Loading

0 comments on commit 82b00c6

Please sign in to comment.