Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The Witchwood all cards, without tests #508

Merged
merged 11 commits into from
Dec 27, 2023
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
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):
shinoi2 marked this conversation as resolved.
Show resolved Hide resolved
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):
shinoi2 marked this conversation as resolved.
Show resolved Hide resolved
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