├── tests ├── __init__.py ├── agents │ ├── __init__.py │ ├── trade │ │ ├── __init__.py │ │ ├── test_helpers.py │ │ ├── test_case_mixin.py │ │ └── play_tests.py │ ├── trade_agent_tests.py │ └── testing_agents.py ├── card_tests │ └── __init__.py ├── replays │ ├── compact │ │ ├── example.rep │ │ ├── random_choice.rep │ │ ├── stonetusk_power.rep │ │ ├── card_tests │ │ │ ├── ArcaneMissilesWithSpellDamage.rep │ │ │ ├── BlessingOfWisdom.rep │ │ │ ├── DruidOfTheClaw.rep │ │ │ ├── SoulOfTheForest.rep │ │ │ ├── Shadowform.rep │ │ │ └── NobleSacrifice.rep │ │ └── stonetusk_innervate.rep │ ├── random_choice.hsreplay │ ├── example.hsreplay │ ├── card_tests │ │ ├── ArcaneMissilesWithSpellDamage.hsreplay │ │ ├── DruidOfTheClaw.hsreplay │ │ ├── BlessingOfWisdom.hsreplay │ │ ├── SoulOfTheForest.hsreplay │ │ ├── Shadowform.hsreplay │ │ └── NobleSacrifice.hsreplay │ └── stonetusk_power.hsreplay ├── needed_tests.txt ├── serialization_tests.py ├── testing_utils.py └── agent_tests.py ├── hearthbreaker ├── __init__.py ├── ui │ ├── __init__.py │ └── game_printer.py ├── tags │ ├── __init__.py │ ├── context.py │ ├── event.py │ └── card_source.py ├── agents │ ├── trade │ │ ├── __init__.py │ │ ├── util.py │ │ └── possible_play.py │ ├── __init__.py │ ├── agent_registry.py │ ├── basic_agents.py │ └── trade_agent.py ├── serialization │ ├── __init__.py │ └── serialization.py ├── cards │ ├── __init__.py │ ├── weapons │ │ ├── warlock.py │ │ ├── __init__.py │ │ ├── shaman.py │ │ ├── hunter.py │ │ ├── rogue.py │ │ ├── paladin.py │ │ └── warrior.py │ ├── heroes.py │ ├── spells │ │ ├── neutral.py │ │ └── __init__.py │ └── minions │ │ ├── paladin.py │ │ ├── mage.py │ │ ├── warrior.py │ │ └── priest.py ├── targeting.py ├── constants.py ├── proxies.py └── powers.py ├── jsonschema ├── __main__.py ├── __init__.py ├── COPYING ├── compat.py ├── cli.py ├── schemas │ ├── draft3.json │ └── draft4.json ├── _reflect.py └── _utils.py ├── .gitignore ├── setup.cfg ├── docs ├── hearthbreaker.replays.rst ├── hearthbreaker.game_objects.rst ├── hearthbreaker.agents.rst ├── card_death_speed.rst ├── hearthbreaker.rst ├── index.rst └── Makefile ├── .coveragerc ├── .travis.yml ├── zoo.hsdeck ├── example.hsdeck ├── patron.hsdeck ├── contributors.md ├── license ├── run_games.py └── replay.schema.json /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthbreaker/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthbreaker/ui/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/agents/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/card_tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthbreaker/tags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/agents/trade/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthbreaker/agents/trade/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthbreaker/serialization/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Daniel' 2 | -------------------------------------------------------------------------------- /jsonschema/__main__.py: -------------------------------------------------------------------------------- 1 | from jsonschema.cli import main 2 | main() 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .project 3 | .pydevproject 4 | .coverage 5 | htmlcov 6 | _build -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .git,docs,__init__.py,jsonschema 3 | max-line-length = 120 4 | ignore = F401, F403, F405 -------------------------------------------------------------------------------- /hearthbreaker/cards/__init__.py: -------------------------------------------------------------------------------- 1 | from hearthbreaker.cards.minions import * 2 | from hearthbreaker.cards.spells import * 3 | from hearthbreaker.cards.weapons import * -------------------------------------------------------------------------------- /docs/hearthbreaker.replays.rst: -------------------------------------------------------------------------------- 1 | Replay Module 2 | ------------- 3 | .. automodule:: hearthbreaker.replay 4 | :members: 5 | :undoc-members: 6 | :show-inheritance: 7 | -------------------------------------------------------------------------------- /docs/hearthbreaker.game_objects.rst: -------------------------------------------------------------------------------- 1 | Game Objects Module 2 | ------------------- 3 | 4 | .. automodule:: hearthbreaker.game_objects 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | exclude_lines = 3 | pragma: no cover 4 | pass$ 5 | 6 | [run] 7 | omit = 8 | */__init__.py 9 | */mock.py 10 | ui/* 11 | /opt/* 12 | */site-packages/* 13 | jsonschema/* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.3" 4 | - "3.4" 5 | - "pypy3" 6 | install: 7 | - pip install coveralls 8 | - pip install flake8 9 | script: 10 | - flake8 . 11 | - coverage run -m unittest discover -s tests -p *_tests.py 12 | after_success: 13 | coveralls 14 | sudo: false -------------------------------------------------------------------------------- /zoo.hsdeck: -------------------------------------------------------------------------------- 1 | 2 Shieldbearer 2 | 2 Flame Imp 3 | 2 Young Priestess 4 | 2 Dark Iron Dwarf 5 | 2 Dire Wolf Alpha 6 | 2 Voidwalker 7 | 2 Harvest Golem 8 | 2 Knife Juggler 9 | 2 Shattered Sun Cleric 10 | 2 Argent Squire 11 | 2 Doomguard 12 | 2 Soulfire 13 | 2 Defender of Argus 14 | 2 Abusive Sergeant 15 | 2 Nerubian Egg -------------------------------------------------------------------------------- /hearthbreaker/agents/__init__.py: -------------------------------------------------------------------------------- 1 | from hearthbreaker.agents.agent_registry import AgentRegistry as __ar__ 2 | from hearthbreaker.agents.basic_agents import RandomAgent 3 | from hearthbreaker.agents.trade_agent import TradeAgent 4 | 5 | registry = __ar__() 6 | 7 | registry.register("Random", RandomAgent) 8 | registry.register("Trade", TradeAgent) -------------------------------------------------------------------------------- /example.hsdeck: -------------------------------------------------------------------------------- 1 | 2 Goldshire Footman 2 | 2 Murloc Raider 3 | 2 Bloodfen Raptor 4 | 2 Frostwolf Grunt 5 | 2 River Crocolisk 6 | 2 Ironfur Grizzly 7 | 2 Magma Rager 8 | 2 Silverback Patriarch 9 | 2 Chillwind Yeti 10 | 2 Oasis Snapjaw 11 | 2 Sen'jin Shieldmasta 12 | 2 Booty Bay Bodyguard 13 | 2 Fen Creeper 14 | 2 Boulderfist Ogre 15 | 2 War Golem -------------------------------------------------------------------------------- /patron.hsdeck: -------------------------------------------------------------------------------- 1 | 1 Inner Rage 2 | 2 Execute 3 | 2 Whirlwind 4 | 2 Fiery War Axe 5 | 2 Battle Rage 6 | 2 Slam 7 | 2 Armorsmith 8 | 1 Cruel Taskmaster 9 | 2 Frothing Berserker 10 | 2 Warsong Commander 11 | 2 Death's Bite 12 | 2 Unstable Ghoul 13 | 2 Acolyte of Pain 14 | 1 Dread Corsair 15 | 2 Gnomish Inventor 16 | 2 Grim Patron 17 | 1 Emperor Thaurissan -------------------------------------------------------------------------------- /tests/replays/compact/example.rep: -------------------------------------------------------------------------------- 1 | deck( Jaina,Stonetusk Boar) 2 | deck(Malfurion,Naturalize) 3 | random() 4 | keep(0,2) 5 | keep(0,1,3) 6 | start() 7 | summon( 0,0) ;p1 8 | end( ) 9 | start() 10 | end( ) 11 | start() 12 | end() 13 | start() 14 | play(0,p1:0) ; p2 15 | end() 16 | start() 17 | summon (0 , 0) ; p1 18 | attack(p1:0,p2) ; p1 19 | end() 20 | start() 21 | concede() ; p2 22 | -------------------------------------------------------------------------------- /tests/replays/compact/random_choice.rep: -------------------------------------------------------------------------------- 1 | deck(Jaina,Demolisher) 2 | deck(Malfurion,Stonetusk Boar) 3 | random(0,0,0,0,0,0,0,0) 4 | keep(0,1,2) 5 | keep(0,1,2,3) 6 | start() 7 | random(0) 8 | end() 9 | start() 10 | random(0) 11 | end() 12 | start() 13 | random(0) 14 | end() 15 | start() 16 | random(0) 17 | summon(0, 0) 18 | end() 19 | start() 20 | random(0) 21 | summon(0, 0) 22 | end() 23 | start() 24 | random(0) 25 | end() 26 | random(p1:0) 27 | -------------------------------------------------------------------------------- /tests/replays/compact/stonetusk_power.rep: -------------------------------------------------------------------------------- 1 | deck(Jaina,Stonetusk Boar) 2 | deck(Malfurion,Power of the Wild) 3 | random() 4 | keep(0,1,2) 5 | keep(0,1,2,3) 6 | start() 7 | summon(0,0) 8 | attack(p1:0,p2) 9 | end() 10 | start() 11 | end() 12 | start() 13 | summon(0,0) 14 | attack(p1:0,p2) 15 | attack(p1:1,p2) 16 | end() 17 | start() 18 | play(0:1) 19 | end() 20 | start() 21 | end() 22 | start() 23 | play(0:0) 24 | end() 25 | start() 26 | concede() 27 | -------------------------------------------------------------------------------- /hearthbreaker/cards/weapons/warlock.py: -------------------------------------------------------------------------------- 1 | from hearthbreaker.cards.base import WeaponCard 2 | from hearthbreaker.constants import CHARACTER_CLASS, CARD_RARITY 3 | from hearthbreaker.game_objects import Weapon 4 | 5 | 6 | class BloodFury(WeaponCard): 7 | def __init__(self): 8 | super().__init__("Blood Fury", 3, CHARACTER_CLASS.WARLOCK, CARD_RARITY.RARE, False) 9 | 10 | def create_weapon(self, player): 11 | return Weapon(3, 8) 12 | -------------------------------------------------------------------------------- /tests/replays/compact/card_tests/ArcaneMissilesWithSpellDamage.rep: -------------------------------------------------------------------------------- 1 | deck(Jaina, Kobold Geomancer, Arcane Missiles) 2 | deck(Malfurion, Faerie Dragon) 3 | random(0, 0, 0, 0, 0, 0, 0, 0) 4 | keep(0,1,2) 5 | keep(0,1,2,3) 6 | start() 7 | random(0) 8 | end() 9 | start() 10 | random(0) 11 | end() 12 | start() 13 | random(0) 14 | summon(0, 0) 15 | end() 16 | start() 17 | random(0) 18 | summon(0, 0) 19 | end() 20 | start() 21 | random(0) 22 | play(0) 23 | random(1, 1, 1, 0) 24 | concede() 25 | end() 26 | -------------------------------------------------------------------------------- /tests/replays/compact/card_tests/BlessingOfWisdom.rep: -------------------------------------------------------------------------------- 1 | deck(Uther, Blessing of Wisdom) 2 | deck(Anduin, Stonetusk Boar, Stonetusk Boar, Ironbeak Owl) 3 | random() 4 | keep(0,1,2) 5 | keep(0,1,2,3) 6 | start() 7 | end() 8 | start() 9 | summon(0, 0) 10 | end() 11 | start() 12 | end() 13 | start() 14 | summon(0, 0) 15 | attack(p2:1, p1) 16 | end() 17 | start() 18 | play(0, p2:0) 19 | play(0, p2:1) 20 | end() 21 | start() 22 | summon(0, 2, p2:0) 23 | attack(p2:0, p1) 24 | attack(p2:1, p1) 25 | concede() 26 | end() 27 | -------------------------------------------------------------------------------- /docs/hearthbreaker.agents.rst: -------------------------------------------------------------------------------- 1 | hearthbreaker.agents package 2 | ============================ 3 | 4 | Submodules 5 | ---------- 6 | 7 | hearthbreaker.agents.basic_agents module 8 | ---------------------------------------- 9 | 10 | .. automodule:: hearthbreaker.agents.basic_agents 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | Module contents 16 | --------------- 17 | 18 | .. automodule:: hearthbreaker.agents 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | -------------------------------------------------------------------------------- /tests/replays/compact/card_tests/DruidOfTheClaw.rep: -------------------------------------------------------------------------------- 1 | deck(Malfurion, Druid of the Claw) 2 | deck(Malfurion, Druid of the Claw) 3 | random() 4 | keep(0,1,2) 5 | keep(0,1,2,3) 6 | start() 7 | end() 8 | start() 9 | end() 10 | start() 11 | end() 12 | start() 13 | end() 14 | start() 15 | end() 16 | start() 17 | end() 18 | start() 19 | end() 20 | start() 21 | end() 22 | start() 23 | summon(0:0,0) 24 | attack(p1:0,p2) 25 | end() 26 | start() 27 | summon(0:1,0) 28 | end() 29 | start() 30 | attack(p1:0,p2:0) 31 | concede() 32 | end() 33 | -------------------------------------------------------------------------------- /hearthbreaker/agents/agent_registry.py: -------------------------------------------------------------------------------- 1 | class AgentRegistry: 2 | def __init__(self): 3 | super().__init__() 4 | self.__registry = {} 5 | 6 | def register(self, name, agent_class): 7 | self.__registry[name] = agent_class 8 | 9 | def create_agent(self, name): 10 | if name in self.__registry: 11 | return self.__registry[name]() 12 | else: 13 | raise KeyError("{} is not in the agent registry".format(name)) 14 | 15 | def get_names(self): 16 | return [name for name in self.__registry.keys()] 17 | -------------------------------------------------------------------------------- /tests/replays/compact/card_tests/SoulOfTheForest.rep: -------------------------------------------------------------------------------- 1 | deck(Malfurion, Stonetusk Boar, Stonetusk Boar, Stonetusk Boar, Soul of the Forest) 2 | deck(Anduin, Stonetusk Boar, Ironbeak Owl) 3 | random() 4 | keep(0,1,2) 5 | keep(0,1,2,3) 6 | start() 7 | summon(0,0) 8 | end() 9 | start() 10 | summon(0,0) 11 | end() 12 | start() 13 | summon(0,0) 14 | end() 15 | start() 16 | summon(1,0) 17 | end() 18 | start() 19 | summon(0,0) 20 | end() 21 | start() 22 | summon(2,0) 23 | end() 24 | start() 25 | play(0) 26 | end() 27 | start() 28 | attack(p2:0,p1:0) 29 | summon(0,1, p1:1) 30 | attack(p2:0, p1:1) 31 | concede() 32 | end() 33 | -------------------------------------------------------------------------------- /docs/card_death_speed.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | Card Death Speed 3 | ================ 4 | 5 | The following table lists all cards in hearthstone, sorted by how long a deck consisting solely of this card will take to end the game. These tests assume the player will play the card optimally (i.e. may not play it on the first turn). They were re-run 30 times, and the lowest one taken for cards with random effects (e.g. Mad Bomber). 6 | 7 | In almost every case (except Hellfire and Pit Lord), this means the enemy hero is dead. 8 | 9 | .. csv-table:: Death Times 10 | :file: death_times.csv 11 | :header-rows: 1 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/replays/compact/card_tests/Shadowform.rep: -------------------------------------------------------------------------------- 1 | deck(Anduin, Shadowform) 2 | deck(Anduin, Stonetusk Boar) 3 | random() 4 | keep(0,1,2) 5 | keep(0,1,2,3) 6 | start() 7 | end() 8 | start() 9 | end() 10 | start() 11 | end() 12 | start() 13 | end() 14 | start() 15 | end() 16 | start() 17 | end() 18 | start() 19 | end() 20 | start() 21 | end() 22 | start() 23 | end() 24 | start() 25 | end() 26 | start() 27 | end() 28 | start() 29 | end() 30 | start() 31 | end() 32 | start() 33 | end() 34 | start() 35 | end() 36 | start() 37 | end() 38 | start() 39 | end() 40 | start() 41 | end() 42 | start() 43 | power(p1) 44 | play(0) 45 | power(p2) 46 | concede() 47 | end() 48 | -------------------------------------------------------------------------------- /tests/replays/compact/card_tests/NobleSacrifice.rep: -------------------------------------------------------------------------------- 1 | deck(Uther, Stonetusk Boar, Stonetusk Boar, Stonetusk Boar, Stonetusk Boar, Stonetusk Boar, Stonetusk Boar, Stonetusk Boar, Noble Sacrifice) 2 | deck(Anduin, Stonetusk Boar) 3 | random() 4 | keep(0,1,2) 5 | keep(0,1,2,3) 6 | start() 7 | summon(0, 0) 8 | end() 9 | start() 10 | end() 11 | start() 12 | summon(0, 0) 13 | summon(0, 0) 14 | end() 15 | start() 16 | end() 17 | start() 18 | summon(0, 0) 19 | summon(0, 0) 20 | summon(0, 0) 21 | end() 22 | start() 23 | end() 24 | start() 25 | summon(0, 0) 26 | end() 27 | start() 28 | end() 29 | start() 30 | play(0) 31 | end() 32 | start() 33 | summon(0, 0) 34 | attack(p2:0, p1) 35 | concede() 36 | end() 37 | -------------------------------------------------------------------------------- /hearthbreaker/cards/weapons/__init__.py: -------------------------------------------------------------------------------- 1 | from hearthbreaker.cards.weapons.hunter import ( 2 | EaglehornBow, 3 | GladiatorsLongbow, 4 | Glaivezooka, 5 | ) 6 | 7 | from hearthbreaker.cards.weapons.paladin import ( 8 | LightsJustice, 9 | SwordOfJustice, 10 | TruesilverChampion, 11 | Coghammer, 12 | ArgentLance, 13 | ) 14 | 15 | from hearthbreaker.cards.weapons.rogue import ( 16 | AssassinsBlade, 17 | PerditionsBlade, 18 | CogmastersWrench, 19 | ) 20 | 21 | from hearthbreaker.cards.weapons.shaman import ( 22 | Doomhammer, 23 | StormforgedAxe, 24 | Powermace, 25 | ) 26 | 27 | from hearthbreaker.cards.weapons.warrior import ( 28 | FieryWarAxe, 29 | ArcaniteReaper, 30 | Gorehowl, 31 | DeathsBite, 32 | OgreWarmaul, 33 | ) 34 | -------------------------------------------------------------------------------- /jsonschema/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | An implementation of JSON Schema for Python 3 | 4 | The main functionality is provided by the validator classes for each of the 5 | supported JSON Schema versions. 6 | 7 | Most commonly, :func:`validate` is the quickest way to simply validate a given 8 | instance under a schema, and will create a validator for you. 9 | 10 | """ 11 | 12 | from jsonschema.exceptions import ( 13 | ErrorTree, FormatError, RefResolutionError, SchemaError, ValidationError 14 | ) 15 | from jsonschema._format import ( 16 | FormatChecker, draft3_format_checker, draft4_format_checker, 17 | ) 18 | from jsonschema.validators import ( 19 | Draft3Validator, Draft4Validator, RefResolver, validate 20 | ) 21 | 22 | 23 | __version__ = "2.5.0-dev" 24 | 25 | 26 | # flake8: noqa 27 | -------------------------------------------------------------------------------- /tests/needed_tests.txt: -------------------------------------------------------------------------------- 1 | Various error conditions for replays should be tested 2 | No replay being saved involves one of player one's minions attacking or being attacked 3 | All replays always draw the card on top of the deck 4 | No replay with a minion with a battlecry is being tested 5 | No replay involving a mage or priest's hero power is being tested 6 | NO non targetted spells are being used in any replay 7 | No options are being saved in any replay 8 | Need much better observer test (and a much better observer) 9 | Wild growth should be used at 10 mana to get the excess mana card 10 | Once Sap or Frozen trap is a card, any spell cards which summon minions should be tested 11 | Nourish should be tested at 10 mana with the choosing mana option 12 | StackedDeck is never run to Fatigue 13 | PredictableAgent is never asked to choose an option -------------------------------------------------------------------------------- /docs/hearthbreaker.rst: -------------------------------------------------------------------------------- 1 | hearthbreaker package 2 | ===================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | :titlesonly: 9 | 10 | hearthbreaker.replays 11 | hearthbreaker.game_objects 12 | 13 | Submodules 14 | ---------- 15 | 16 | 17 | hearthbreaker.powers module 18 | --------------------------- 19 | 20 | .. automodule:: hearthbreaker.powers 21 | 22 | 23 | hearthbreaker.targeting module 24 | ------------------------------ 25 | 26 | .. automodule:: hearthbreaker.targeting 27 | 28 | Hearthbreaker Constants 29 | ----------------------- 30 | 31 | .. automodule:: hearthbreaker.constants 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | Module contents 37 | --------------- 38 | 39 | .. automodule:: hearthbreaker 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | -------------------------------------------------------------------------------- /contributors.md: -------------------------------------------------------------------------------- 1 | Contributors 2 | ============ 3 | 4 | The following people have contributed to the HearthBreaker project: 5 | 6 | * [Daniel Yule](https://github.com/danielyule) (Lead Developer) 7 | * [Markus Persson](https://github.com/Ragowit) (Card Implementations, Engine, Bugfixes) 8 | * [@randomflyingtaco](https://github.com/randomflyingtaco) (Card Implementations) 9 | * [Mike Harris](https://github.com/mharris717) (Trading Bot Implementation) 10 | * [Jade McGough] (https://github.com/zetsubo) (Documentation Fixes) 11 | * [Peter Kappler] (https://github.com/pkappler) (OS X Fixes) 12 | * [@littmus](https://github.com/littmus) (Bug fixes and additional testing) 13 | * [Andreas Källberg] (https://github.com/anka-213) (Bug Fixes and unit testing) 14 | 15 | Special thanks to Xinhuan for the insightful comments both here and on [Hearthhead](http://www.hearthhead.com/user=Xinhuan#comments) 16 | 17 | Members of the #hearthsim channel on freenode.net have been invaluable in finding edge cases and 18 | discussing strange rule applications 19 | -------------------------------------------------------------------------------- /hearthbreaker/tags/context.py: -------------------------------------------------------------------------------- 1 | from hearthbreaker.tags.base import Context 2 | 3 | 4 | class BattlecryContext(Context): 5 | def filter_targets(self, targets): 6 | return [target for target in targets if target.player is self.player or not target.stealth] 7 | 8 | def damage(self, amount, target): 9 | target.damage(amount) 10 | 11 | def heal(self, amount, target): 12 | if self.player.player.heal_does_damage: 13 | self.damage(amount, target) 14 | else: 15 | target.heal(amount) 16 | 17 | 18 | class SpellContext(Context): 19 | def filter_targets(self, targets): 20 | return [target for target in targets if target.player is self.player or not target.stealth] 21 | 22 | def damage(self, amount, target): 23 | target.damage((amount + self.player.spell_damage) * self.player.spell_multiplier) 24 | 25 | def heal(self, amount, target): 26 | if self.player.heal_does_damage: 27 | self.damage(amount, target) 28 | else: 29 | return amount * self.player.heal_multiplier 30 | -------------------------------------------------------------------------------- /jsonschema/COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Julian Berman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Daniel Yule 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. hsgame documentation master file, created by 2 | sphinx-quickstart on Sun Jun 1 15:59:59 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Hearthbreaker Documentation 7 | =========================== 8 | 9 | The purpose of this project is to create an open source Hearthstone simulator for the purposes of machine learning and 10 | data mining of Blizzard's . The end goal 11 | is to create a system implementing every card in Hearthstone, then simulate games of bots against bots to train 12 | them. The results from these games can be used to determine cards which work well together and cards which do not, and 13 | possibly discover new deck types. 14 | The goal is not to create a clone of Hearthstone which players can use to replace the game itself with. 15 | 16 | All cards prior to Goblins vs Gnomes have been implemented. GvG cards are in the process of being implemented 17 | 18 | Contents: 19 | 20 | .. toctree:: 21 | :titlesonly: 22 | 23 | hearthbreaker 24 | contributing 25 | 26 | 27 | 28 | Indices and tables 29 | ================== 30 | 31 | * :ref:`genindex` 32 | * :ref:`modindex` 33 | * :ref:`search` 34 | 35 | -------------------------------------------------------------------------------- /hearthbreaker/cards/weapons/shaman.py: -------------------------------------------------------------------------------- 1 | from hearthbreaker.cards.base import WeaponCard 2 | from hearthbreaker.constants import CHARACTER_CLASS, CARD_RARITY, MINION_TYPE 3 | from hearthbreaker.game_objects import Weapon 4 | from hearthbreaker.tags.action import Give 5 | from hearthbreaker.tags.base import Buff, Deathrattle 6 | from hearthbreaker.tags.condition import IsType 7 | from hearthbreaker.tags.status import Windfury, ChangeAttack, ChangeHealth 8 | from hearthbreaker.tags.selector import MinionSelector, RandomPicker 9 | 10 | 11 | class Doomhammer(WeaponCard): 12 | def __init__(self): 13 | super().__init__("Doomhammer", 5, CHARACTER_CLASS.SHAMAN, CARD_RARITY.EPIC, overload=2) 14 | 15 | def create_weapon(self, player): 16 | return Weapon(2, 8, buffs=[Buff(Windfury())]) 17 | 18 | 19 | class StormforgedAxe(WeaponCard): 20 | def __init__(self): 21 | super().__init__("Stormforged Axe", 2, CHARACTER_CLASS.SHAMAN, CARD_RARITY.COMMON, overload=1) 22 | 23 | def create_weapon(self, player): 24 | return Weapon(2, 3) 25 | 26 | 27 | class Powermace(WeaponCard): 28 | def __init__(self): 29 | super().__init__("Powermace", 3, CHARACTER_CLASS.SHAMAN, CARD_RARITY.RARE) 30 | 31 | def create_weapon(self, player): 32 | return Weapon(3, 2, deathrattle=Deathrattle(Give([Buff(ChangeHealth(2)), Buff(ChangeAttack(2))]), 33 | MinionSelector(IsType(MINION_TYPE.MECH), picker=RandomPicker()))) 34 | -------------------------------------------------------------------------------- /hearthbreaker/serialization/serialization.py: -------------------------------------------------------------------------------- 1 | import json 2 | from hearthbreaker.cards import FlameImp, LightsJustice, EyeForAnEye 3 | from hearthbreaker.engine import Game 4 | from tests.agents.testing_agents import CardTestingAgent 5 | from tests.testing_utils import generate_game_for 6 | 7 | 8 | def _save_object(o): 9 | return o.__to_json__() 10 | 11 | 12 | def _load_object(d): 13 | return Game.__from_json__(d) 14 | 15 | 16 | def serialize(game): 17 | """ 18 | Encode the given game instance as a JSON formatted string. This string can be used to re-construct the game exactly 19 | as it is now 20 | 21 | :param heartbreaker.game_objects.Game game: The game to serialize 22 | :rtype: string 23 | """ 24 | 25 | return json.dumps(game, default=_save_object, indent=2) 26 | 27 | 28 | def deserialize(json_string, agents): 29 | """ 30 | Decode the given game instance from a JSON formatted string. 31 | 32 | :param string json_string: The string representation of the game 33 | :rtype: :class:`hearthbreaker.engine.Game` 34 | """ 35 | d = json.loads(json_string) 36 | return Game.__from_json__(d, agents) 37 | 38 | 39 | if __name__ == "__main__": 40 | game = generate_game_for([LightsJustice, EyeForAnEye], FlameImp, CardTestingAgent, CardTestingAgent) 41 | for turn in range(0, 5): 42 | game.play_single_turn() 43 | 44 | print(serialize(game)) 45 | game2 = deserialize(serialize(game), [player.agent for player in game.players]) 46 | game2.start() 47 | -------------------------------------------------------------------------------- /tests/serialization_tests.py: -------------------------------------------------------------------------------- 1 | import json 2 | from hearthbreaker.engine import Game 3 | import tests.copy_tests 4 | 5 | 6 | class TestGameSerialization(tests.copy_tests.TestGameCopying): 7 | def setUp(self): 8 | def _save_object(o): 9 | return o.__to_json__() 10 | 11 | def serialization_copy(old_game): 12 | game_json = json.dumps(old_game, default=_save_object, indent=2) 13 | d = json.loads(game_json) 14 | game = Game.__from_json__(d, [player.agent for player in old_game.players]) 15 | game._has_turn_ended = old_game._has_turn_ended 16 | return game 17 | 18 | super().setUp() 19 | self._old_copy = Game.copy 20 | Game.copy = serialization_copy 21 | 22 | def tearDown(self): 23 | super().tearDown() 24 | Game.copy = self._old_copy 25 | 26 | 27 | class TestMinionSerialization(tests.copy_tests.TestMinionCopying): 28 | def setUp(self): 29 | def _save_object(o): 30 | return o.__to_json__() 31 | 32 | def serialization_copy(old_game): 33 | game_json = json.dumps(old_game, default=_save_object, indent=2) 34 | d = json.loads(game_json) 35 | game = Game.__from_json__(d, [player.agent for player in old_game.players]) 36 | game._has_turn_ended = old_game._has_turn_ended 37 | return game 38 | 39 | super().setUp() 40 | self._old_copy = Game.copy 41 | Game.copy = serialization_copy 42 | 43 | def tearDown(self): 44 | super().tearDown() 45 | Game.copy = self._old_copy 46 | -------------------------------------------------------------------------------- /jsonschema/compat.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import sys 3 | import operator 4 | 5 | try: 6 | from collections import MutableMapping, Sequence # noqa 7 | except ImportError: 8 | from collections.abc import MutableMapping, Sequence # noqa 9 | 10 | PY3 = sys.version_info[0] >= 3 11 | 12 | if PY3: 13 | zip = zip 14 | from io import StringIO 15 | from urllib.parse import ( 16 | unquote, urljoin, urlunsplit, SplitResult, urlsplit as _urlsplit 17 | ) 18 | from urllib.request import urlopen 19 | str_types = str, 20 | int_types = int, 21 | iteritems = operator.methodcaller("items") 22 | else: 23 | from itertools import izip as zip # noqa 24 | from StringIO import StringIO 25 | from urlparse import ( 26 | urljoin, urlunsplit, SplitResult, urlsplit as _urlsplit # noqa 27 | ) 28 | from urllib import unquote # noqa 29 | from urllib2 import urlopen # noqa 30 | str_types = basestring 31 | int_types = int, long 32 | iteritems = operator.methodcaller("iteritems") 33 | 34 | 35 | # On python < 3.3 fragments are not handled properly with unknown schemes 36 | def urlsplit(url): 37 | scheme, netloc, path, query, fragment = _urlsplit(url) 38 | if "#" in path: 39 | path, fragment = path.split("#", 1) 40 | return SplitResult(scheme, netloc, path, query, fragment) 41 | 42 | 43 | def urldefrag(url): 44 | if "#" in url: 45 | s, n, p, q, frag = urlsplit(url) 46 | defrag = urlunsplit((s, n, p, q, '')) 47 | else: 48 | defrag = url 49 | frag = '' 50 | return defrag, frag 51 | 52 | 53 | # flake8: noqa 54 | -------------------------------------------------------------------------------- /hearthbreaker/cards/weapons/hunter.py: -------------------------------------------------------------------------------- 1 | from hearthbreaker.cards.base import WeaponCard 2 | from hearthbreaker.game_objects import Weapon 3 | from hearthbreaker.tags.action import Give, IncreaseDurability 4 | from hearthbreaker.tags.condition import IsHero 5 | from hearthbreaker.tags.event import AttackCompleted, SecretRevealed, CharacterAttack 6 | from hearthbreaker.tags.selector import HeroSelector, MinionSelector, RandomPicker, WeaponSelector 7 | from hearthbreaker.constants import CHARACTER_CLASS, CARD_RARITY 8 | from hearthbreaker.tags.base import Effect, BuffUntil, Battlecry, ActionTag 9 | from hearthbreaker.tags.status import ChangeAttack, Immune 10 | 11 | 12 | class EaglehornBow(WeaponCard): 13 | def __init__(self): 14 | super().__init__("Eaglehorn Bow", 3, CHARACTER_CLASS.HUNTER, 15 | CARD_RARITY.RARE) 16 | 17 | def create_weapon(self, player): 18 | return Weapon(3, 2, effects=[Effect(SecretRevealed(), ActionTag(IncreaseDurability(), WeaponSelector()))]) 19 | 20 | 21 | class GladiatorsLongbow(WeaponCard): 22 | def __init__(self): 23 | super().__init__("Gladiator's Longbow", 7, CHARACTER_CLASS.HUNTER, 24 | CARD_RARITY.EPIC) 25 | 26 | def create_weapon(self, player): 27 | return Weapon(5, 2, effects=[Effect(CharacterAttack(IsHero()), 28 | ActionTag(Give(BuffUntil(Immune(), AttackCompleted())), HeroSelector()))]) 29 | 30 | 31 | class Glaivezooka(WeaponCard): 32 | def __init__(self): 33 | super().__init__("Glaivezooka", 2, CHARACTER_CLASS.HUNTER, CARD_RARITY.COMMON, 34 | battlecry=Battlecry(Give(ChangeAttack(1)), MinionSelector(None, picker=RandomPicker()))) 35 | 36 | def create_weapon(self, player): 37 | return Weapon(2, 2) 38 | -------------------------------------------------------------------------------- /tests/agents/trade_agent_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from hearthbreaker.agents.trade.possible_play import PossiblePlays 3 | from hearthbreaker.cards import Wisp, WarGolem, BloodfenRaptor, RiverCrocolisk, AbusiveSergeant, ArgentSquire 4 | from tests.agents.trade.test_helpers import TestHelpers 5 | from tests.agents.trade.test_case_mixin import TestCaseMixin 6 | 7 | 8 | class TestTradeAgent(TestCaseMixin, unittest.TestCase): 9 | def test_setup_smoke(self): 10 | game = TestHelpers().make_game() 11 | 12 | self.add_minions(game, 0, Wisp(), WarGolem()) 13 | self.add_minions(game, 1, BloodfenRaptor()) 14 | 15 | self.assertEqual(2, len(game.players[0].minions)) 16 | self.assertEqual(1, len(game.players[1].minions)) 17 | 18 | def test_basic_trade(self): 19 | game = TestHelpers().make_game() 20 | 21 | self.add_minions(game, 1, Wisp(), WarGolem()) 22 | self.add_minions(game, 0, BloodfenRaptor()) 23 | 24 | self.make_all_active(game) 25 | game.play_single_turn() 26 | 27 | self.assert_minions(game.players[1], "War Golem") 28 | self.assert_minions(game.players[0], "Bloodfen Raptor") 29 | 30 | def test_buff_target(self): 31 | game = TestHelpers().make_game() 32 | 33 | self.add_minions(game, 0, BloodfenRaptor(), RiverCrocolisk()) 34 | self.make_all_active(game) 35 | game.players[0].agent.player = game.players[0] 36 | self.add_minions(game, 0, AbusiveSergeant()) 37 | 38 | game.play_single_turn() 39 | 40 | def test_hero_power(self): 41 | game = self.make_game() 42 | cards = self.make_cards(game.current_player, ArgentSquire()) 43 | possible_plays = PossiblePlays(cards, 10, allow_hero_power=True) 44 | 45 | self.assertEqual(1, len(possible_plays.plays())) 46 | -------------------------------------------------------------------------------- /run_games.py: -------------------------------------------------------------------------------- 1 | import json 2 | from hearthbreaker.agents.basic_agents import RandomAgent 3 | from hearthbreaker.cards.heroes import hero_for_class 4 | from hearthbreaker.constants import CHARACTER_CLASS 5 | from hearthbreaker.engine import Game, Deck, card_lookup 6 | from hearthbreaker.cards import * 7 | import timeit 8 | 9 | 10 | def load_deck(filename): 11 | cards = [] 12 | character_class = CHARACTER_CLASS.MAGE 13 | 14 | with open(filename, "r") as deck_file: 15 | contents = deck_file.read() 16 | items = contents.splitlines() 17 | for line in items[0:]: 18 | parts = line.split(" ", 1) 19 | count = int(parts[0]) 20 | for i in range(0, count): 21 | card = card_lookup(parts[1]) 22 | if card.character_class != CHARACTER_CLASS.ALL: 23 | character_class = card.character_class 24 | cards.append(card) 25 | 26 | if len(cards) > 30: 27 | pass 28 | 29 | return Deck(cards, hero_for_class(character_class)) 30 | 31 | 32 | def do_stuff(): 33 | _count = 0 34 | 35 | def play_game(): 36 | nonlocal _count 37 | _count += 1 38 | new_game = game.copy() 39 | try: 40 | new_game.start() 41 | except Exception as e: 42 | print(json.dumps(new_game.__to_json__(), default=lambda o: o.__to_json__(), indent=1)) 43 | print(new_game._all_cards_played) 44 | raise e 45 | 46 | del new_game 47 | 48 | if _count % 1000 == 0: 49 | print("---- game #{} ----".format(_count)) 50 | 51 | deck1 = load_deck("mage.hsdeck") 52 | deck2 = load_deck("mage2.hsdeck") 53 | game = Game([deck1, deck2], [RandomAgent(), RandomAgent()]) 54 | 55 | print(timeit.timeit(play_game, 'gc.enable()', number=100000)) 56 | 57 | 58 | if __name__ == "__main__": 59 | do_stuff() 60 | -------------------------------------------------------------------------------- /hearthbreaker/cards/weapons/rogue.py: -------------------------------------------------------------------------------- 1 | from hearthbreaker.cards.base import WeaponCard 2 | from hearthbreaker.game_objects import Weapon 3 | from hearthbreaker.tags.action import Damage 4 | from hearthbreaker.tags.base import Battlecry, Buff 5 | from hearthbreaker.tags.condition import GreaterThan, IsType 6 | from hearthbreaker.tags.selector import CharacterSelector, UserPicker, Count, MinionSelector 7 | from hearthbreaker.tags.status import ChangeAttack 8 | from hearthbreaker.constants import CHARACTER_CLASS, CARD_RARITY, MINION_TYPE 9 | 10 | 11 | class WickedKnife(WeaponCard): 12 | def __init__(self): 13 | super().__init__("Wicked Knife", 1, CHARACTER_CLASS.ROGUE, CARD_RARITY.FREE, False) 14 | 15 | def create_weapon(self, player): 16 | return Weapon(1, 2) 17 | 18 | 19 | class AssassinsBlade(WeaponCard): 20 | def __init__(self): 21 | super().__init__("Assassin's Blade", 5, CHARACTER_CLASS.ROGUE, CARD_RARITY.COMMON) 22 | 23 | def create_weapon(self, player): 24 | return Weapon(3, 4) 25 | 26 | 27 | class PerditionsBlade(WeaponCard): 28 | def __init__(self): 29 | super().__init__("Perdition's Blade", 3, CHARACTER_CLASS.ROGUE, CARD_RARITY.RARE, 30 | battlecry=Battlecry(Damage(1), CharacterSelector(None, picker=UserPicker())), 31 | combo=Battlecry(Damage(2), CharacterSelector(None, picker=UserPicker()))) 32 | 33 | def create_weapon(self, player): 34 | return Weapon(2, 2) 35 | 36 | 37 | class CogmastersWrench(WeaponCard): 38 | def __init__(self): 39 | super().__init__("Cogmaster's Wrench", 3, CHARACTER_CLASS.ROGUE, CARD_RARITY.EPIC) 40 | 41 | def create_weapon(self, player): 42 | return Weapon(1, 3, buffs=[Buff(ChangeAttack(2), GreaterThan(Count(MinionSelector(IsType(MINION_TYPE.MECH))), 43 | value=0))]) 44 | -------------------------------------------------------------------------------- /tests/replays/random_choice.hsreplay: -------------------------------------------------------------------------------- 1 | { 2 | "moves": [ 3 | { 4 | "random": [ 5 | 0 6 | ], 7 | "name": "start" 8 | }, 9 | { 10 | "random": [], 11 | "name": "end" 12 | }, 13 | { 14 | "random": [ 15 | 0 16 | ], 17 | "name": "start" 18 | }, 19 | { 20 | "random": [], 21 | "name": "end" 22 | }, 23 | { 24 | "random": [ 25 | 0 26 | ], 27 | "name": "start" 28 | }, 29 | { 30 | "random": [], 31 | "name": "end" 32 | }, 33 | { 34 | "random": [ 35 | 0 36 | ], 37 | "name": "start" 38 | }, 39 | { 40 | "card": { 41 | "card_index": 0 42 | }, 43 | "random": [], 44 | "name": "play", 45 | "index": 0 46 | }, 47 | { 48 | "random": [], 49 | "name": "end" 50 | }, 51 | { 52 | "random": [ 53 | 0 54 | ], 55 | "name": "start" 56 | }, 57 | { 58 | "card": { 59 | "card_index": 0 60 | }, 61 | "random": [], 62 | "name": "play", 63 | "index": 0 64 | }, 65 | { 66 | "random": [], 67 | "name": "end" 68 | }, 69 | { 70 | "random": [ 71 | 0 72 | ], 73 | "name": "start" 74 | }, 75 | { 76 | "random": [ 77 | { 78 | "minion": 0, 79 | "player": "p1" 80 | } 81 | ], 82 | "name": "end" 83 | } 84 | ], 85 | "header": { 86 | "random": [ 87 | 0, 88 | 0, 89 | 0, 90 | 0, 91 | 0, 92 | 0, 93 | 0, 94 | 0 95 | ], 96 | "decks": [ 97 | { 98 | "hero": "Jaina", 99 | "cards": [ 100 | "Demolisher" 101 | ] 102 | }, 103 | { 104 | "hero": "Malfurion", 105 | "cards": [ 106 | "Stonetusk Boar" 107 | ] 108 | } 109 | ], 110 | "keep": [ 111 | [ 112 | 0, 113 | 1, 114 | 2 115 | ], 116 | [ 117 | 0, 118 | 1, 119 | 2, 120 | 3 121 | ] 122 | ] 123 | } 124 | } -------------------------------------------------------------------------------- /hearthbreaker/targeting.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | 4 | def find_spell_target(game, filter_function): 5 | targets = copy.copy(game.other_player.minions) 6 | targets.extend(game.current_player.minions) 7 | targets.append(game.other_player.hero) 8 | targets.append(game.current_player.hero) 9 | 10 | targets = [target for target in targets if filter_function(target)] 11 | return targets 12 | 13 | 14 | def find_enemy_spell_target(game, filter_function): 15 | targets = copy.copy(game.other_player.minions) 16 | targets.append(game.other_player.hero) 17 | 18 | targets = [target for target in targets if filter_function(target)] 19 | return targets 20 | 21 | 22 | def find_friendly_spell_target(game, filter_function): 23 | targets = copy.copy(game.current_player.minions) 24 | targets.append(game.current_player.hero) 25 | 26 | targets = [target for target in targets if filter_function(target)] 27 | return targets 28 | 29 | 30 | def find_minion_spell_target(game, filter_function): 31 | targets = copy.copy(game.other_player.minions) 32 | targets.extend(game.current_player.minions) 33 | 34 | targets = [target for target in targets if filter_function(target)] 35 | return targets 36 | 37 | 38 | def find_enemy_minion_spell_target(game, filter_function): 39 | targets = copy.copy(game.other_player.minions) 40 | 41 | targets = [target for target in targets if filter_function(target)] 42 | return targets 43 | 44 | 45 | def find_friendly_minion_spell_target(game, filter_function): 46 | targets = copy.copy(game.current_player.minions) 47 | 48 | targets = [target for target in targets if filter_function(target)] 49 | return targets 50 | 51 | 52 | def find_enemy_minion_battlecry_target(game, filter_function): 53 | targets = copy.copy(game.other_player.minions) 54 | 55 | targets = [target for target in targets if filter_function(target)] 56 | if len(targets) is 0: 57 | return None 58 | return targets 59 | 60 | 61 | def find_friendly_minion_battlecry_target(game, filter_function): 62 | targets = copy.copy(game.current_player.minions) 63 | 64 | targets = [target for target in targets if filter_function(target)] 65 | if len(targets) is 0: 66 | return None 67 | return targets 68 | -------------------------------------------------------------------------------- /tests/replays/example.hsreplay: -------------------------------------------------------------------------------- 1 | { 2 | "moves": [ 3 | { 4 | "random": [], 5 | "name": "start" 6 | }, 7 | { 8 | "card": { 9 | "card_index": 0 10 | }, 11 | "random": [], 12 | "name": "play", 13 | "index": 0 14 | }, 15 | { 16 | "random": [], 17 | "name": "end" 18 | }, 19 | { 20 | "random": [], 21 | "name": "start" 22 | }, 23 | { 24 | "random": [], 25 | "name": "end" 26 | }, 27 | { 28 | "random": [], 29 | "name": "start" 30 | }, 31 | { 32 | "random": [], 33 | "name": "end" 34 | }, 35 | { 36 | "random": [], 37 | "name": "start" 38 | }, 39 | { 40 | "card": { 41 | "card_index": 0 42 | }, 43 | "target": { 44 | "minion": 0, 45 | "player": "p1" 46 | }, 47 | "name": "play", 48 | "random": [] 49 | }, 50 | { 51 | "random": [], 52 | "name": "end" 53 | }, 54 | { 55 | "random": [], 56 | "name": "start" 57 | }, 58 | { 59 | "card": { 60 | "card_index": 0 61 | }, 62 | "random": [], 63 | "name": "play", 64 | "index": 0 65 | }, 66 | { 67 | "character": { 68 | "minion": 0, 69 | "player": "p1" 70 | }, 71 | "target": { 72 | "player": "p2" 73 | }, 74 | "name": "attack", 75 | "random": [] 76 | }, 77 | { 78 | "random": [], 79 | "name": "end" 80 | }, 81 | { 82 | "random": [], 83 | "name": "start" 84 | }, 85 | { 86 | "random": [], 87 | "name": "concede" 88 | } 89 | ], 90 | "header": { 91 | "random": [], 92 | "decks": [ 93 | { 94 | "hero": "Jaina", 95 | "cards": [ 96 | "Stonetusk Boar" 97 | ] 98 | }, 99 | { 100 | "hero": "Malfurion", 101 | "cards": [ 102 | "Naturalize" 103 | ] 104 | } 105 | ], 106 | "keep": [ 107 | [ 108 | 0, 109 | 2 110 | ], 111 | [ 112 | 0, 113 | 1, 114 | 3 115 | ] 116 | ] 117 | } 118 | } -------------------------------------------------------------------------------- /jsonschema/cli.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import argparse 3 | import json 4 | import sys 5 | 6 | from jsonschema._reflect import namedAny 7 | from jsonschema.validators import validator_for 8 | 9 | 10 | def _namedAnyWithDefault(name): 11 | if "." not in name: 12 | name = "jsonschema." + name 13 | return namedAny(name) 14 | 15 | 16 | def _json_file(path): 17 | with open(path) as file: 18 | return json.load(file) 19 | 20 | 21 | parser = argparse.ArgumentParser( 22 | description="JSON Schema Validation CLI", 23 | ) 24 | parser.add_argument( 25 | "-i", "--instance", 26 | action="append", 27 | dest="instances", 28 | type=_json_file, 29 | help="a path to a JSON instance to validate " 30 | "(may be specified multiple times)", 31 | ) 32 | parser.add_argument( 33 | "-F", "--error-format", 34 | default="{error.instance}: {error.message}\n", 35 | help="the format to use for each error output message, specified in " 36 | "a form suitable for passing to str.format, which will be called " 37 | "with 'error' for each error", 38 | ) 39 | parser.add_argument( 40 | "-V", "--validator", 41 | type=_namedAnyWithDefault, 42 | help="the fully qualified object name of a validator to use, or, for " 43 | "validators that are registered with jsonschema, simply the name " 44 | "of the class.", 45 | ) 46 | parser.add_argument( 47 | "schema", 48 | help="the JSON Schema to validate with", 49 | type=_json_file, 50 | ) 51 | 52 | 53 | def parse_args(args): 54 | arguments = vars(parser.parse_args(args=args or ["--help"])) 55 | if arguments["validator"] is None: 56 | arguments["validator"] = validator_for(arguments["schema"]) 57 | return arguments 58 | 59 | 60 | def main(args=sys.argv[1:]): 61 | sys.exit(run(arguments=parse_args(args=args))) 62 | 63 | 64 | def run(arguments, stdout=sys.stdout, stderr=sys.stderr): 65 | error_format = arguments["error_format"] 66 | validator = arguments["validator"](schema=arguments["schema"]) 67 | errored = False 68 | for instance in arguments["instances"] or (): 69 | for error in validator.iter_errors(instance): 70 | stderr.write(error_format.format(error=error)) 71 | errored = True 72 | return errored 73 | -------------------------------------------------------------------------------- /tests/replays/card_tests/ArcaneMissilesWithSpellDamage.hsreplay: -------------------------------------------------------------------------------- 1 | { 2 | "moves": [ 3 | { 4 | "random": [ 5 | 0 6 | ], 7 | "name": "start" 8 | }, 9 | { 10 | "random": [], 11 | "name": "end" 12 | }, 13 | { 14 | "random": [ 15 | 0 16 | ], 17 | "name": "start" 18 | }, 19 | { 20 | "random": [], 21 | "name": "end" 22 | }, 23 | { 24 | "random": [ 25 | 0 26 | ], 27 | "name": "start" 28 | }, 29 | { 30 | "card": { 31 | "card_index": 0 32 | }, 33 | "random": [], 34 | "name": "play", 35 | "index": 0 36 | }, 37 | { 38 | "random": [], 39 | "name": "end" 40 | }, 41 | { 42 | "random": [ 43 | 0 44 | ], 45 | "name": "start" 46 | }, 47 | { 48 | "card": { 49 | "card_index": 0 50 | }, 51 | "random": [], 52 | "name": "play", 53 | "index": 0 54 | }, 55 | { 56 | "random": [], 57 | "name": "end" 58 | }, 59 | { 60 | "random": [ 61 | 0 62 | ], 63 | "name": "start" 64 | }, 65 | { 66 | "card": { 67 | "card_index": 0 68 | }, 69 | "random": [ 70 | 1, 71 | 1, 72 | 1, 73 | 0 74 | ], 75 | "name": "play" 76 | }, 77 | { 78 | "random": [], 79 | "name": "concede" 80 | }, 81 | { 82 | "random": [], 83 | "name": "end" 84 | } 85 | ], 86 | "header": { 87 | "random": [ 88 | 0, 89 | 0, 90 | 0, 91 | 0, 92 | 0, 93 | 0, 94 | 0, 95 | 0 96 | ], 97 | "decks": [ 98 | { 99 | "hero": "Jaina", 100 | "cards": [ 101 | "Kobold Geomancer", 102 | "Arcane Missiles" 103 | ] 104 | }, 105 | { 106 | "hero": "Malfurion", 107 | "cards": [ 108 | "Faerie Dragon" 109 | ] 110 | } 111 | ], 112 | "keep": [ 113 | [ 114 | 0, 115 | 1, 116 | 2 117 | ], 118 | [ 119 | 0, 120 | 1, 121 | 2, 122 | 3 123 | ] 124 | ] 125 | } 126 | } -------------------------------------------------------------------------------- /tests/testing_utils.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import collections 3 | import sys 4 | from hearthbreaker.cards.heroes import hero_for_class 5 | from hearthbreaker.constants import CHARACTER_CLASS 6 | from hearthbreaker.engine import Game, Deck 7 | 8 | if sys.version_info.major is 3: 9 | if sys.version_info.minor <= 2: 10 | import mock # pragma: no cover 11 | else: 12 | from unittest import mock # pragma: no cover 13 | 14 | __all__ = ["mock", "StackedDeck", "generate_game_for"] 15 | 16 | 17 | class StackedDeck(Deck): 18 | def __init__(self, card_pattern, character_class): 19 | cards = [] 20 | while len(cards) + len(card_pattern) < 30: 21 | cards.extend(copy.deepcopy(card_pattern)) 22 | 23 | cards.extend(card_pattern[:30 - len(cards)]) 24 | hero = hero_for_class(character_class) 25 | super().__init__(cards, hero) 26 | 27 | def draw(self, random_func): 28 | for card_index in range(0, len(self.cards)): 29 | if not self.cards[card_index].drawn: 30 | self.cards[card_index].drawn = True 31 | self.left -= 1 32 | return self.cards[card_index] 33 | 34 | 35 | def generate_game_for(card1, card2, first_agent_type, second_agent_type, run_pre_game=True): 36 | if not isinstance(card1, collections.Sequence): 37 | card_set1 = [card1()] 38 | else: 39 | card_set1 = [card() for card in card1] 40 | class1 = CHARACTER_CLASS.MAGE 41 | for card in card_set1: 42 | if card.character_class != CHARACTER_CLASS.ALL: 43 | class1 = card.character_class 44 | break 45 | 46 | if not isinstance(card2, collections.Sequence): 47 | card_set2 = [card2()] 48 | else: 49 | card_set2 = [card() for card in card2] 50 | 51 | class2 = CHARACTER_CLASS.MAGE 52 | for card in card_set2: 53 | if card.character_class != CHARACTER_CLASS.ALL: 54 | class2 = card.character_class 55 | break 56 | 57 | deck1 = StackedDeck(card_set1, class1) 58 | deck2 = StackedDeck(card_set2, class2) 59 | game = Game([deck1, deck2], [first_agent_type(), second_agent_type()]) 60 | game.current_player = game.players[1] 61 | game.other_player = game.players[0] 62 | if run_pre_game: 63 | game.pre_game() 64 | return game 65 | -------------------------------------------------------------------------------- /tests/agents/trade/test_helpers.py: -------------------------------------------------------------------------------- 1 | from hearthbreaker.agents.basic_agents import RandomAgent 2 | from hearthbreaker.agents.trade_agent import TradeAgent 3 | from hearthbreaker.cards import WarGolem 4 | from hearthbreaker.cards.base import MinionCard 5 | import re 6 | from hearthbreaker.cards.spells.neutral import TheCoin 7 | from hearthbreaker.game_objects import Minion 8 | from tests.testing_utils import generate_game_for 9 | 10 | 11 | def t(self): 12 | return self.name 13 | 14 | 15 | Minion.try_name = t 16 | 17 | 18 | class TempCard(MinionCard): 19 | def __init__(self, base_attack=0, health=0, name="", taunt=False): 20 | self.base_attack = base_attack 21 | self.health = health 22 | self.name = name 23 | self.ref_name = name 24 | self.taunt = taunt 25 | self.mana = None 26 | self.minion_type = 0 27 | self.rarity = 0 28 | self._attached = False 29 | self.effects = [] 30 | self.buffs = [] 31 | self.auras = [] 32 | 33 | def create_minion(self, player): 34 | res = Minion(self.base_attack, self.health, taunt=self.taunt) 35 | res.name = self.name 36 | return res 37 | 38 | @staticmethod 39 | def make(s): 40 | taunt = False 41 | a, h = s.split("/") 42 | g = re.search("(\d+)t$", h) 43 | if g: 44 | taunt = True 45 | h = g.group(1) 46 | return TempCard(int(a), int(h), taunt=taunt, name=s) 47 | 48 | 49 | class TestHelpers: 50 | @staticmethod 51 | def fix_create_minion(classes=None): 52 | if not classes: 53 | classes = MinionCard.__subclasses__() 54 | for cls in classes: 55 | TestHelpers.fix_create_minion_single(cls) 56 | 57 | @staticmethod 58 | def fix_create_minion_single(cls): 59 | if isinstance(cls, TheCoin): 60 | return 61 | 62 | if not hasattr(cls, "create_minion_old"): 63 | old = cls.create_minion 64 | cls.create_minion_old = old 65 | 66 | def create_minion_named_gen(self, player): 67 | res = old(self, player) 68 | res.name = self.name 69 | return res 70 | 71 | cls.create_minion = create_minion_named_gen 72 | 73 | def list_copy(self, list): 74 | return [c for c in list] 75 | 76 | def make_game(self, before_draw_callback=None): 77 | return generate_game_for(WarGolem, WarGolem, TradeAgent, RandomAgent) 78 | -------------------------------------------------------------------------------- /hearthbreaker/constants.py: -------------------------------------------------------------------------------- 1 | class CARD_RARITY: 2 | FREE = 1 3 | COMMON = 2 4 | RARE = 3 5 | EPIC = 4 6 | LEGENDARY = 5 7 | 8 | __rarities = { 9 | "FREE": FREE, 10 | "COMMON": COMMON, 11 | "RARE": RARE, 12 | "EPIC": EPIC, 13 | "LEGENDARY": LEGENDARY, 14 | } 15 | 16 | @staticmethod 17 | def from_str(rarity_name): 18 | return CARD_RARITY.__rarities[rarity_name.upper()] 19 | 20 | @staticmethod 21 | def to_str(class_number): 22 | classes = dict(zip(CARD_RARITY.__rarities.values(), CARD_RARITY.__rarities.keys())) 23 | return classes[class_number].capitalize() 24 | 25 | 26 | class CHARACTER_CLASS: 27 | ALL = 0 28 | MAGE = 1 29 | HUNTER = 2 30 | SHAMAN = 3 31 | WARRIOR = 4 32 | DRUID = 5 33 | PRIEST = 6 34 | PALADIN = 7 35 | ROGUE = 8 36 | WARLOCK = 9 37 | LORD_JARAXXUS = 10 38 | DREAM = 11 39 | 40 | __classes = { 41 | "MAGE": MAGE, 42 | "HUNTER": HUNTER, 43 | "SHAMAN": SHAMAN, 44 | "WARRIOR": WARRIOR, 45 | "DRUID": DRUID, 46 | "PRIEST": PRIEST, 47 | "PALADIN": PALADIN, 48 | "ROGUE": ROGUE, 49 | "WARLOCK": WARLOCK, 50 | "LORD_JARAXXUS": LORD_JARAXXUS, 51 | "DREAM": DREAM, 52 | "": ALL, 53 | } 54 | 55 | @staticmethod 56 | def from_str(class_name): 57 | return CHARACTER_CLASS.__classes[class_name.upper()] 58 | 59 | @staticmethod 60 | def to_str(class_number): 61 | classes = dict(zip(CHARACTER_CLASS.__classes.values(), CHARACTER_CLASS.__classes.keys())) 62 | return classes[class_number].capitalize() 63 | 64 | 65 | class MINION_TYPE: 66 | ALL = -1 67 | NONE = 0 68 | BEAST = 1 69 | MURLOC = 2 70 | DRAGON = 3 71 | GIANT = 4 72 | DEMON = 5 73 | PIRATE = 6 74 | TOTEM = 7 75 | MECH = 8 76 | 77 | __types = { 78 | "": NONE, 79 | "BEAST": BEAST, 80 | "MURLOC": MURLOC, 81 | "DRAGON": DRAGON, 82 | "GIANT": GIANT, 83 | "DEMON": DEMON, 84 | "PIRATE": PIRATE, 85 | "TOTEM": TOTEM, 86 | "MECH": MECH, 87 | } 88 | 89 | @staticmethod 90 | def from_str(type_name): 91 | 92 | return MINION_TYPE.__types[type_name.upper()] 93 | 94 | @staticmethod 95 | def to_str(minion_number): 96 | types = dict(zip(MINION_TYPE.__types.values(), MINION_TYPE.__types.keys())) 97 | return types[minion_number].capitalize() 98 | -------------------------------------------------------------------------------- /hearthbreaker/agents/trade/util.py: -------------------------------------------------------------------------------- 1 | import random 2 | import collections 3 | import functools 4 | from hearthbreaker.game_objects import Hero 5 | 6 | 7 | class memoized(object): 8 | '''Decorator. Caches a function's return value each time it is called. 9 | If called later with the same arguments, the cached value is returned 10 | (not reevaluated). 11 | ''' 12 | def __init__(self, func): 13 | self.func = func 14 | self.cache = {} 15 | 16 | def __call__(self, *args): 17 | if not isinstance(args, collections.Hashable): 18 | # uncacheable. a list, for instance. 19 | # better to not cache than blow up. 20 | return self.func(*args) 21 | if args in self.cache: 22 | return self.cache[args] 23 | else: 24 | value = self.func(*args) 25 | self.cache[args] = value 26 | return value 27 | 28 | def __repr__(self): 29 | '''Return the function's docstring.''' 30 | return self.func.__doc__ 31 | 32 | def __get__(self, obj, objtype): 33 | '''Support instance methods.''' 34 | return functools.partial(self.__call__, obj) 35 | 36 | 37 | class Util: 38 | @staticmethod 39 | def reverse_sorted(list): 40 | res = sorted(list) 41 | res.reverse() 42 | return res 43 | 44 | @staticmethod 45 | def uniq_by_sorted(list): 46 | res = {} 47 | for obj in list: 48 | a = [c.name for c in obj] 49 | k = str.join("", sorted(a)) 50 | if not res.get(k): 51 | res[k] = obj 52 | 53 | return res.values() 54 | 55 | @staticmethod 56 | def rand_el(list): 57 | i = random.randint(0, len(list) - 1) 58 | return list[i] 59 | 60 | @staticmethod 61 | def rand_prefer_minion(targets): 62 | minions = [card for card in filter(lambda c: not isinstance(c, Hero), targets)] 63 | if len(minions) > 0: 64 | targets = minions 65 | return Util.rand_el(targets) 66 | 67 | @staticmethod 68 | def filter_out_one(arr, f): 69 | res = [obj for obj in filter(lambda x: not f(x), arr)] 70 | if len(res) + 1 != len(arr): 71 | s = "bad remove, list has {} elements, removed {}, {}" 72 | raise Exception(s.format(len(arr), len(arr) - len(res), arr)) 73 | return res 74 | 75 | @staticmethod 76 | def names(arr): 77 | res = [] 78 | for obj in arr: 79 | if hasattr(obj, "name"): 80 | res.append(obj.name) 81 | else: 82 | res.append("UNK") 83 | return res 84 | -------------------------------------------------------------------------------- /hearthbreaker/cards/weapons/paladin.py: -------------------------------------------------------------------------------- 1 | from hearthbreaker.cards.base import WeaponCard 2 | from hearthbreaker.game_objects import Weapon 3 | from hearthbreaker.tags.action import Give, DecreaseDurability, Heal, Joust, IncreaseDurability 4 | from hearthbreaker.tags.condition import IsHero 5 | from hearthbreaker.tags.event import MinionSummoned, CharacterAttack 6 | from hearthbreaker.tags.selector import TargetSelector, HeroSelector, MinionSelector, RandomPicker, WeaponSelector, \ 7 | SelfSelector 8 | from hearthbreaker.tags.base import Buff, Effect, Battlecry, ActionTag 9 | from hearthbreaker.tags.status import DivineShield, Taunt, ChangeAttack, ChangeHealth 10 | from hearthbreaker.constants import CHARACTER_CLASS, CARD_RARITY 11 | 12 | 13 | class LightsJustice(WeaponCard): 14 | def __init__(self): 15 | super().__init__("Light's Justice", 1, CHARACTER_CLASS.PALADIN, CARD_RARITY.FREE) 16 | 17 | def create_weapon(self, player): 18 | return Weapon(1, 4) 19 | 20 | 21 | class SwordOfJustice(WeaponCard): 22 | def __init__(self): 23 | super().__init__("Sword of Justice", 3, CHARACTER_CLASS.PALADIN, CARD_RARITY.EPIC) 24 | 25 | def create_weapon(self, player): 26 | return Weapon(1, 5, effects=[Effect(MinionSummoned(), ActionTag(Give([Buff(ChangeAttack(1)), 27 | Buff(ChangeHealth(1))]), 28 | TargetSelector())), 29 | Effect(MinionSummoned(), ActionTag(DecreaseDurability(), WeaponSelector()))]) 30 | 31 | 32 | class TruesilverChampion(WeaponCard): 33 | def __init__(self): 34 | super().__init__("Truesilver Champion", 4, CHARACTER_CLASS.PALADIN, CARD_RARITY.COMMON) 35 | 36 | def create_weapon(self, player): 37 | return Weapon(4, 2, effects=[Effect(CharacterAttack(IsHero()), ActionTag(Heal(2), HeroSelector()))]) 38 | 39 | 40 | class Coghammer(WeaponCard): 41 | def __init__(self): 42 | super().__init__("Coghammer", 3, CHARACTER_CLASS.PALADIN, CARD_RARITY.EPIC, 43 | battlecry=Battlecry(Give([Buff(DivineShield()), Buff(Taunt())]), 44 | MinionSelector(picker=RandomPicker()))) 45 | 46 | def create_weapon(self, player): 47 | return Weapon(2, 3) 48 | 49 | 50 | class ArgentLance(WeaponCard): 51 | def __init__(self): 52 | super().__init__("Argent Lance", 2, CHARACTER_CLASS.PALADIN, CARD_RARITY.RARE, 53 | battlecry=Battlecry(Joust(IncreaseDurability()), SelfSelector())) 54 | 55 | def create_weapon(self, player): 56 | return Weapon(2, 2) 57 | -------------------------------------------------------------------------------- /tests/replays/compact/stonetusk_innervate.rep: -------------------------------------------------------------------------------- 1 | deck(Jaina,Stonetusk Boar) 2 | deck(Malfurion,Naturalize) 3 | random(1,2,19,14,28,24,9,17) 4 | keep(0,1,2) 5 | keep(0,1,2,3) 6 | start() 7 | random(22) 8 | end() 9 | start() 10 | random(19) 11 | summon(0,0) 12 | play(3) 13 | summon(0,0) 14 | attack(p2:0,p1) 15 | attack(p2:1,p1) 16 | end() 17 | start() 18 | random(13) 19 | power() 20 | attack(p1,p2:0) 21 | end() 22 | start() 23 | random(6) 24 | power(p2:0) 25 | end() 26 | start() 27 | random(16) 28 | power() 29 | attack(p1,p2) 30 | end() 31 | start() 32 | random(22) 33 | power(p1) 34 | summon(0,0) 35 | attack(p2:0,p1) 36 | end() 37 | start() 38 | random(19) 39 | power() 40 | attack(p1,p2:0) 41 | end() 42 | start() 43 | random(1) 44 | power(p1) 45 | summon(0,0) 46 | summon(0,0) 47 | attack(p2:0,p1) 48 | attack(p2:1,p1) 49 | end() 50 | start() 51 | random(13) 52 | power() 53 | attack(p1,p2:0) 54 | random(13,1) 55 | play(0,p2:0) 56 | end() 57 | start() 58 | random(6) 59 | power(p1) 60 | summon(0,0) 61 | summon(0,0) 62 | summon(0,0) 63 | attack(p2:0,p1) 64 | attack(p2:1,p1) 65 | attack(p2:2,p1) 66 | end() 67 | start() 68 | random(2) 69 | power() 70 | attack(p1,p2:0) 71 | random(12,6) 72 | play(0,p2:0) 73 | random(1,1) 74 | play(0,p2:0) 75 | end() 76 | start() 77 | random(5) 78 | power(p1) 79 | summon(0,0) 80 | summon(0,0) 81 | summon(0,0) 82 | summon(0,0) 83 | attack(p2:0,p1) 84 | attack(p2:1,p1) 85 | attack(p2:2,p1) 86 | attack(p2:3,p1) 87 | end() 88 | start() 89 | random(14) 90 | power() 91 | attack(p1,p2:0) 92 | random(0,10) 93 | play(0,p2:0) 94 | random(7,1) 95 | play(0,p2:0) 96 | random(2,8) 97 | play(0,p2:0) 98 | end() 99 | start() 100 | random(6) 101 | power(p1) 102 | summon(0,0) 103 | summon(0,0) 104 | summon(0,0) 105 | summon(0,0) 106 | summon(0,0) 107 | attack(p2:0,p1) 108 | attack(p2:1,p1) 109 | attack(p2:2,p1) 110 | attack(p2:3,p1) 111 | attack(p2:4,p1) 112 | end() 113 | start() 114 | random(17) 115 | power() 116 | attack(p1,p2:0) 117 | random(0,3) 118 | play(0,p2:0) 119 | random(0,2) 120 | play(0,p2:0) 121 | random(0,0) 122 | play(0,p2:0) 123 | random(0) 124 | play(0,p2:0) 125 | end() 126 | start() 127 | power(p1) 128 | summon(0,0) 129 | summon(0,0) 130 | summon(0,0) 131 | summon(0,0) 132 | summon(0,0) 133 | summon(0,0) 134 | attack(p2:0,p1) 135 | attack(p2:1,p1) 136 | attack(p2:2,p1) 137 | attack(p2:3,p1) 138 | attack(p2:4,p1) 139 | attack(p2:5,p1) 140 | end() 141 | start() 142 | random(13) 143 | power() 144 | attack(p1,p2:0) 145 | play(0,p2:0) 146 | play(0,p2:0) 147 | end() 148 | start() 149 | power(p2:0) 150 | summon(0,0) 151 | summon(0,0) 152 | summon(0,0) 153 | summon(0,0) 154 | attack(p2:0,p1) 155 | attack(p2:1,p1) 156 | attack(p2:2,p1) 157 | attack(p2:3,p1) 158 | attack(p2:4,p1) 159 | attack(p2:5,p1) 160 | end() 161 | -------------------------------------------------------------------------------- /tests/agents/trade/test_case_mixin.py: -------------------------------------------------------------------------------- 1 | import random 2 | from tests.agents.trade.test_helpers import TestHelpers 3 | from hearthbreaker.agents.trade.trade import Trades 4 | 5 | 6 | class TestCaseMixin: 7 | def setUp(self): 8 | TestHelpers.fix_create_minion() 9 | random.seed(1857) 10 | 11 | def add_minions(self, game, player_index, *minions): 12 | player = game.players[player_index] 13 | for minion in minions: 14 | minion.player = player 15 | minion.use(player, game) 16 | 17 | def set_board(self, game, player_index, *minions): 18 | return self.add_minions(game, player_index, *minions) 19 | 20 | def make_all_active(self, game): 21 | for player in game.players: 22 | for minion in player.minions: 23 | minion.active = True 24 | minion.exhausted = False 25 | for card in player.hand: 26 | card.player = player 27 | 28 | def assert_minions(self, player, *names): 29 | actual = self.card_names(player.minions) 30 | self.assertEqual(sorted(actual), sorted(names)) 31 | 32 | def card_names(self, cards): 33 | return [m.try_name() for m in cards] 34 | 35 | def player_str(self, player): 36 | res = [] 37 | res.append("\nPlayer\n") 38 | res.append("Hand: ") 39 | res.append(self.card_names(player.hand)) 40 | res.append("\nDeck: ") 41 | res.append(self.card_names(player.deck.cards[0:5])) 42 | res.append("\n") 43 | 44 | res = [str(x) for x in res] 45 | 46 | return str.join("", res) 47 | 48 | def make_trades2(self, me, opp, game_callback=None): 49 | game = self.make_game() 50 | me = [m for m in map(lambda card: card.summon(game.current_player, game, len(game.current_player.minions)), me)] 51 | opp = [m for m in map(lambda card: card.summon(game.other_player, game, len(game.other_player.minions)), opp)] 52 | 53 | if game_callback: 54 | game_callback(game) 55 | 56 | trades = Trades(game.players[0], me, opp, game.players[1].hero) 57 | 58 | return [game, trades] 59 | 60 | def make_trades(self, me, opp): 61 | return self.make_trades2(me, opp)[1] 62 | 63 | def make_cards(self, player, *cards): 64 | cards = [c for c in cards] 65 | for c in cards: 66 | c.player = player 67 | return cards 68 | 69 | def make_game(self): 70 | return TestHelpers().make_game() 71 | 72 | def set_hand(self, game, player_index, *cards): 73 | cards = self.make_cards(game.players[player_index], *cards) 74 | game.players[player_index].hand = cards 75 | 76 | def set_mana(self, game, player_index, mana): 77 | player = game.players[player_index] 78 | player.mana = player.max_mana = mana 79 | -------------------------------------------------------------------------------- /tests/replays/stonetusk_power.hsreplay: -------------------------------------------------------------------------------- 1 | { 2 | "moves": [ 3 | { 4 | "random": [], 5 | "name": "start" 6 | }, 7 | { 8 | "card": { 9 | "card_index": 0 10 | }, 11 | "random": [], 12 | "name": "play", 13 | "index": 0 14 | }, 15 | { 16 | "character": { 17 | "minion": 0, 18 | "player": "p1" 19 | }, 20 | "target": { 21 | "player": "p2" 22 | }, 23 | "name": "attack", 24 | "random": [] 25 | }, 26 | { 27 | "random": [], 28 | "name": "end" 29 | }, 30 | { 31 | "random": [], 32 | "name": "start" 33 | }, 34 | { 35 | "random": [], 36 | "name": "end" 37 | }, 38 | { 39 | "random": [], 40 | "name": "start" 41 | }, 42 | { 43 | "card": { 44 | "card_index": 0 45 | }, 46 | "random": [], 47 | "name": "play", 48 | "index": 0 49 | }, 50 | { 51 | "character": { 52 | "minion": 0, 53 | "player": "p1" 54 | }, 55 | "target": { 56 | "player": "p2" 57 | }, 58 | "name": "attack", 59 | "random": [] 60 | }, 61 | { 62 | "character": { 63 | "minion": 1, 64 | "player": "p1" 65 | }, 66 | "target": { 67 | "player": "p2" 68 | }, 69 | "name": "attack", 70 | "random": [] 71 | }, 72 | { 73 | "random": [], 74 | "name": "end" 75 | }, 76 | { 77 | "random": [], 78 | "name": "start" 79 | }, 80 | { 81 | "card": { 82 | "option": 1, 83 | "card_index": 0 84 | }, 85 | "random": [], 86 | "name": "play" 87 | }, 88 | { 89 | "random": [], 90 | "name": "end" 91 | }, 92 | { 93 | "random": [], 94 | "name": "start" 95 | }, 96 | { 97 | "random": [], 98 | "name": "end" 99 | }, 100 | { 101 | "random": [], 102 | "name": "start" 103 | }, 104 | { 105 | "card": { 106 | "option": 0, 107 | "card_index": 0 108 | }, 109 | "random": [], 110 | "name": "play" 111 | }, 112 | { 113 | "random": [], 114 | "name": "end" 115 | }, 116 | { 117 | "random": [], 118 | "name": "start" 119 | }, 120 | { 121 | "random": [], 122 | "name": "concede" 123 | } 124 | ], 125 | "header": { 126 | "random": [], 127 | "decks": [ 128 | { 129 | "hero": "Jaina", 130 | "cards": [ 131 | "Stonetusk Boar" 132 | ] 133 | }, 134 | { 135 | "hero": "Malfurion", 136 | "cards": [ 137 | "Power of the Wild" 138 | ] 139 | } 140 | ], 141 | "keep": [ 142 | [ 143 | 0, 144 | 1, 145 | 2 146 | ], 147 | [ 148 | 0, 149 | 1, 150 | 2, 151 | 3 152 | ] 153 | ] 154 | } 155 | } -------------------------------------------------------------------------------- /hearthbreaker/cards/weapons/warrior.py: -------------------------------------------------------------------------------- 1 | from hearthbreaker.cards.base import WeaponCard 2 | from hearthbreaker.constants import CHARACTER_CLASS, CARD_RARITY 3 | from hearthbreaker.game_objects import Weapon 4 | from hearthbreaker.tags.action import Damage, IncreaseDurability, ChangeTarget, Give, IncreaseWeaponAttack 5 | from hearthbreaker.tags.base import Deathrattle, Effect, ActionTag, BuffUntil 6 | from hearthbreaker.tags.condition import NotCurrentTarget, OneIn, OpponentMinionCountIsGreaterThan, And, \ 7 | IsHero, TargetIsMinion 8 | from hearthbreaker.tags.event import CharacterAttack, AttackCompleted 9 | from hearthbreaker.tags.selector import MinionSelector, BothPlayer, HeroSelector, CharacterSelector, EnemyPlayer, \ 10 | RandomPicker, WeaponSelector 11 | from hearthbreaker.tags.status import ChangeAttack 12 | 13 | 14 | class FieryWarAxe(WeaponCard): 15 | def __init__(self): 16 | super().__init__("Fiery War Axe", 2, CHARACTER_CLASS.WARRIOR, CARD_RARITY.FREE) 17 | 18 | def create_weapon(self, player): 19 | return Weapon(3, 2) 20 | 21 | 22 | class ArcaniteReaper(WeaponCard): 23 | def __init__(self): 24 | super().__init__("Arcanite Reaper", 5, CHARACTER_CLASS.WARRIOR, CARD_RARITY.COMMON) 25 | 26 | def create_weapon(self, player): 27 | return Weapon(5, 2) 28 | 29 | 30 | class Gorehowl(WeaponCard): 31 | def __init__(self): 32 | super().__init__("Gorehowl", 7, CHARACTER_CLASS.WARRIOR, CARD_RARITY.EPIC) 33 | 34 | def create_weapon(self, player): 35 | return Weapon(7, 1, effects=[Effect(CharacterAttack(And(IsHero(), TargetIsMinion())), 36 | [ActionTag(IncreaseDurability(), WeaponSelector()), 37 | ActionTag(IncreaseWeaponAttack(-1), WeaponSelector()), 38 | ActionTag(Give(BuffUntil(ChangeAttack(1), AttackCompleted())), 39 | HeroSelector())])]) 40 | 41 | 42 | class HeavyAxe(WeaponCard): 43 | def __init__(self): 44 | super().__init__("Heavy Axe", 1, CHARACTER_CLASS.WARRIOR, CARD_RARITY.COMMON, False) 45 | 46 | def create_weapon(self, player): 47 | return Weapon(1, 3) 48 | 49 | 50 | class DeathsBite(WeaponCard): 51 | def __init__(self): 52 | super().__init__("Death's Bite", 4, CHARACTER_CLASS.WARRIOR, CARD_RARITY.COMMON) 53 | 54 | def create_weapon(self, player): 55 | return Weapon(4, 2, deathrattle=Deathrattle(Damage(1), MinionSelector(players=BothPlayer()))) 56 | 57 | 58 | class OgreWarmaul(WeaponCard): 59 | def __init__(self): 60 | super().__init__("Ogre Warmaul", 3, CHARACTER_CLASS.WARRIOR, CARD_RARITY.COMMON) 61 | 62 | def create_weapon(self, player): 63 | return Weapon(4, 2, effects=[Effect(CharacterAttack(IsHero()), 64 | ActionTag(ChangeTarget(CharacterSelector(NotCurrentTarget(), EnemyPlayer(), 65 | RandomPicker())), 66 | HeroSelector(), And(OneIn(2), OpponentMinionCountIsGreaterThan(0))))]) 67 | -------------------------------------------------------------------------------- /tests/replays/card_tests/DruidOfTheClaw.hsreplay: -------------------------------------------------------------------------------- 1 | { 2 | "moves": [ 3 | { 4 | "random": [], 5 | "name": "start" 6 | }, 7 | { 8 | "random": [], 9 | "name": "end" 10 | }, 11 | { 12 | "random": [], 13 | "name": "start" 14 | }, 15 | { 16 | "random": [], 17 | "name": "end" 18 | }, 19 | { 20 | "random": [], 21 | "name": "start" 22 | }, 23 | { 24 | "random": [], 25 | "name": "end" 26 | }, 27 | { 28 | "random": [], 29 | "name": "start" 30 | }, 31 | { 32 | "random": [], 33 | "name": "end" 34 | }, 35 | { 36 | "random": [], 37 | "name": "start" 38 | }, 39 | { 40 | "random": [], 41 | "name": "end" 42 | }, 43 | { 44 | "random": [], 45 | "name": "start" 46 | }, 47 | { 48 | "random": [], 49 | "name": "end" 50 | }, 51 | { 52 | "random": [], 53 | "name": "start" 54 | }, 55 | { 56 | "random": [], 57 | "name": "end" 58 | }, 59 | { 60 | "random": [], 61 | "name": "start" 62 | }, 63 | { 64 | "random": [], 65 | "name": "end" 66 | }, 67 | { 68 | "random": [], 69 | "name": "start" 70 | }, 71 | { 72 | "card": { 73 | "option": 0, 74 | "card_index": 0 75 | }, 76 | "random": [], 77 | "name": "play", 78 | "index": 0 79 | }, 80 | { 81 | "character": { 82 | "minion": 0, 83 | "player": "p1" 84 | }, 85 | "target": { 86 | "player": "p2" 87 | }, 88 | "name": "attack", 89 | "random": [] 90 | }, 91 | { 92 | "random": [], 93 | "name": "end" 94 | }, 95 | { 96 | "random": [], 97 | "name": "start" 98 | }, 99 | { 100 | "card": { 101 | "option": 1, 102 | "card_index": 0 103 | }, 104 | "random": [], 105 | "name": "play", 106 | "index": 0 107 | }, 108 | { 109 | "random": [], 110 | "name": "end" 111 | }, 112 | { 113 | "random": [], 114 | "name": "start" 115 | }, 116 | { 117 | "character": { 118 | "minion": 0, 119 | "player": "p1" 120 | }, 121 | "target": { 122 | "minion": 0, 123 | "player": "p2" 124 | }, 125 | "name": "attack", 126 | "random": [] 127 | }, 128 | { 129 | "random": [], 130 | "name": "concede" 131 | }, 132 | { 133 | "random": [], 134 | "name": "end" 135 | } 136 | ], 137 | "header": { 138 | "random": [], 139 | "decks": [ 140 | { 141 | "hero": "Malfurion", 142 | "cards": [ 143 | "Druid of the Claw" 144 | ] 145 | }, 146 | { 147 | "hero": "Malfurion", 148 | "cards": [ 149 | "Druid of the Claw" 150 | ] 151 | } 152 | ], 153 | "keep": [ 154 | [ 155 | 0, 156 | 1, 157 | 2 158 | ], 159 | [ 160 | 0, 161 | 1, 162 | 2, 163 | 3 164 | ] 165 | ] 166 | } 167 | } -------------------------------------------------------------------------------- /tests/replays/card_tests/BlessingOfWisdom.hsreplay: -------------------------------------------------------------------------------- 1 | { 2 | "moves": [ 3 | { 4 | "random": [], 5 | "name": "start" 6 | }, 7 | { 8 | "random": [], 9 | "name": "end" 10 | }, 11 | { 12 | "random": [], 13 | "name": "start" 14 | }, 15 | { 16 | "card": { 17 | "card_index": 0 18 | }, 19 | "random": [], 20 | "name": "play", 21 | "index": 0 22 | }, 23 | { 24 | "random": [], 25 | "name": "end" 26 | }, 27 | { 28 | "random": [], 29 | "name": "start" 30 | }, 31 | { 32 | "random": [], 33 | "name": "end" 34 | }, 35 | { 36 | "random": [], 37 | "name": "start" 38 | }, 39 | { 40 | "card": { 41 | "card_index": 0 42 | }, 43 | "random": [], 44 | "name": "play", 45 | "index": 0 46 | }, 47 | { 48 | "character": { 49 | "minion": 1, 50 | "player": "p2" 51 | }, 52 | "target": { 53 | "player": "p1" 54 | }, 55 | "name": "attack", 56 | "random": [] 57 | }, 58 | { 59 | "random": [], 60 | "name": "end" 61 | }, 62 | { 63 | "random": [], 64 | "name": "start" 65 | }, 66 | { 67 | "card": { 68 | "card_index": 0 69 | }, 70 | "target": { 71 | "minion": 0, 72 | "player": "p2" 73 | }, 74 | "name": "play", 75 | "random": [] 76 | }, 77 | { 78 | "card": { 79 | "card_index": 0 80 | }, 81 | "target": { 82 | "minion": 1, 83 | "player": "p2" 84 | }, 85 | "name": "play", 86 | "random": [] 87 | }, 88 | { 89 | "random": [], 90 | "name": "end" 91 | }, 92 | { 93 | "random": [], 94 | "name": "start" 95 | }, 96 | { 97 | "card": { 98 | "card_index": 0 99 | }, 100 | "target": { 101 | "minion": 0, 102 | "player": "p2" 103 | }, 104 | "name": "play", 105 | "index": 2, 106 | "random": [] 107 | }, 108 | { 109 | "character": { 110 | "minion": 0, 111 | "player": "p2" 112 | }, 113 | "target": { 114 | "player": "p1" 115 | }, 116 | "name": "attack", 117 | "random": [] 118 | }, 119 | { 120 | "character": { 121 | "minion": 1, 122 | "player": "p2" 123 | }, 124 | "target": { 125 | "player": "p1" 126 | }, 127 | "name": "attack", 128 | "random": [] 129 | }, 130 | { 131 | "random": [], 132 | "name": "concede" 133 | }, 134 | { 135 | "random": [], 136 | "name": "end" 137 | } 138 | ], 139 | "header": { 140 | "random": [], 141 | "decks": [ 142 | { 143 | "hero": "Uther", 144 | "cards": [ 145 | "Blessing of Wisdom" 146 | ] 147 | }, 148 | { 149 | "hero": "Anduin", 150 | "cards": [ 151 | "Stonetusk Boar", 152 | "Stonetusk Boar", 153 | "Ironbeak Owl" 154 | ] 155 | } 156 | ], 157 | "keep": [ 158 | [ 159 | 0, 160 | 1, 161 | 2 162 | ], 163 | [ 164 | 0, 165 | 1, 166 | 2, 167 | 3 168 | ] 169 | ] 170 | } 171 | } -------------------------------------------------------------------------------- /hearthbreaker/cards/heroes.py: -------------------------------------------------------------------------------- 1 | from hearthbreaker.cards.base import HeroCard 2 | from hearthbreaker.constants import CHARACTER_CLASS, MINION_TYPE 3 | from hearthbreaker.powers import MagePower, DruidPower, HunterPower, PaladinPower, PriestPower, RoguePower,\ 4 | ShamanPower, WarlockPower, WarriorPower, JaraxxusPower, DieInsect 5 | 6 | 7 | class Malfurion(HeroCard): 8 | def __init__(self): 9 | super().__init__("Malfurion Stormrage", CHARACTER_CLASS.DRUID, 30, DruidPower) 10 | 11 | 12 | class Rexxar(HeroCard): 13 | def __init__(self): 14 | super().__init__("Rexxar", CHARACTER_CLASS.HUNTER, 30, HunterPower) 15 | 16 | 17 | class Jaina(HeroCard): 18 | def __init__(self): 19 | super().__init__("Jaina Proudmoore", CHARACTER_CLASS.MAGE, 30, MagePower) 20 | 21 | 22 | class Uther(HeroCard): 23 | def __init__(self): 24 | super().__init__("Uther the Lightbringer", CHARACTER_CLASS.PALADIN, 30, PaladinPower) 25 | 26 | 27 | class Anduin(HeroCard): 28 | def __init__(self): 29 | super().__init__("Anduin Wrynn", CHARACTER_CLASS.PRIEST, 30, PriestPower) 30 | 31 | 32 | class Valeera(HeroCard): 33 | def __init__(self): 34 | super().__init__("Valeera Sanguinar", CHARACTER_CLASS.ROGUE, 30, RoguePower) 35 | 36 | 37 | class Thrall(HeroCard): 38 | def __init__(self): 39 | super().__init__("Thrall", CHARACTER_CLASS.SHAMAN, 30, ShamanPower) 40 | 41 | 42 | class Guldan(HeroCard): 43 | def __init__(self): 44 | super().__init__("Gul'dan", CHARACTER_CLASS.WARLOCK, 30, WarlockPower) 45 | 46 | 47 | class Garrosh(HeroCard): 48 | def __init__(self): 49 | super().__init__("Garrosh Hellscream", CHARACTER_CLASS.WARRIOR, 30, WarriorPower) 50 | 51 | 52 | class Jaraxxus(HeroCard): 53 | def __init__(self): 54 | super().__init__("Lord Jaraxxus", CHARACTER_CLASS.WARLOCK, 15, JaraxxusPower, MINION_TYPE.DEMON, 55 | ref_name="Lord Jarraxus (hero)") 56 | 57 | 58 | class Ragnaros(HeroCard): 59 | def __init__(self): 60 | super().__init__("Ragnaros the Firelord (hero)", CHARACTER_CLASS.ALL, 8, DieInsect) 61 | 62 | 63 | def hero_for_class(character_class): 64 | if character_class == CHARACTER_CLASS.DRUID: 65 | return Malfurion() 66 | elif character_class == CHARACTER_CLASS.HUNTER: 67 | return Rexxar() 68 | elif character_class == CHARACTER_CLASS.MAGE: 69 | return Jaina() 70 | elif character_class == CHARACTER_CLASS.PRIEST: 71 | return Anduin() 72 | elif character_class == CHARACTER_CLASS.PALADIN: 73 | return Uther() 74 | elif character_class == CHARACTER_CLASS.ROGUE: 75 | return Valeera() 76 | elif character_class == CHARACTER_CLASS.SHAMAN: 77 | return Thrall() 78 | elif character_class == CHARACTER_CLASS.WARLOCK: 79 | return Guldan() 80 | elif character_class == CHARACTER_CLASS.WARRIOR: 81 | return Garrosh() 82 | else: 83 | return Jaina() 84 | 85 | 86 | __hero_lookup = {"Jaina": Jaina, 87 | "Malfurion": Malfurion, 88 | "Rexxar": Rexxar, 89 | "Anduin": Anduin, 90 | "Uther": Uther, 91 | "Gul'dan": Guldan, 92 | "Valeera": Valeera, 93 | "Thrall": Thrall, 94 | "Garrosh": Garrosh, 95 | "Jaraxxus": Jaraxxus, 96 | "Ragnaros": Ragnaros, 97 | } 98 | 99 | 100 | def hero_from_name(name): 101 | return __hero_lookup[name]() 102 | -------------------------------------------------------------------------------- /tests/replays/card_tests/SoulOfTheForest.hsreplay: -------------------------------------------------------------------------------- 1 | { 2 | "moves": [ 3 | { 4 | "random": [], 5 | "name": "start" 6 | }, 7 | { 8 | "card": { 9 | "card_index": 0 10 | }, 11 | "random": [], 12 | "name": "play", 13 | "index": 0 14 | }, 15 | { 16 | "random": [], 17 | "name": "end" 18 | }, 19 | { 20 | "random": [], 21 | "name": "start" 22 | }, 23 | { 24 | "card": { 25 | "card_index": 0 26 | }, 27 | "random": [], 28 | "name": "play", 29 | "index": 0 30 | }, 31 | { 32 | "random": [], 33 | "name": "end" 34 | }, 35 | { 36 | "random": [], 37 | "name": "start" 38 | }, 39 | { 40 | "card": { 41 | "card_index": 0 42 | }, 43 | "random": [], 44 | "name": "play", 45 | "index": 0 46 | }, 47 | { 48 | "random": [], 49 | "name": "end" 50 | }, 51 | { 52 | "random": [], 53 | "name": "start" 54 | }, 55 | { 56 | "card": { 57 | "card_index": 1 58 | }, 59 | "random": [], 60 | "name": "play", 61 | "index": 0 62 | }, 63 | { 64 | "random": [], 65 | "name": "end" 66 | }, 67 | { 68 | "random": [], 69 | "name": "start" 70 | }, 71 | { 72 | "card": { 73 | "card_index": 0 74 | }, 75 | "random": [], 76 | "name": "play", 77 | "index": 0 78 | }, 79 | { 80 | "random": [], 81 | "name": "end" 82 | }, 83 | { 84 | "random": [], 85 | "name": "start" 86 | }, 87 | { 88 | "card": { 89 | "card_index": 2 90 | }, 91 | "random": [], 92 | "name": "play", 93 | "index": 0 94 | }, 95 | { 96 | "random": [], 97 | "name": "end" 98 | }, 99 | { 100 | "random": [], 101 | "name": "start" 102 | }, 103 | { 104 | "card": { 105 | "card_index": 0 106 | }, 107 | "random": [], 108 | "name": "play" 109 | }, 110 | { 111 | "random": [], 112 | "name": "end" 113 | }, 114 | { 115 | "random": [], 116 | "name": "start" 117 | }, 118 | { 119 | "character": { 120 | "minion": 0, 121 | "player": "p2" 122 | }, 123 | "target": { 124 | "minion": 0, 125 | "player": "p1" 126 | }, 127 | "name": "attack", 128 | "random": [] 129 | }, 130 | { 131 | "card": { 132 | "card_index": 0 133 | }, 134 | "target": { 135 | "minion": 1, 136 | "player": "p1" 137 | }, 138 | "name": "play", 139 | "index": 1, 140 | "random": [] 141 | }, 142 | { 143 | "character": { 144 | "minion": 0, 145 | "player": "p2" 146 | }, 147 | "target": { 148 | "minion": 1, 149 | "player": "p1" 150 | }, 151 | "name": "attack", 152 | "random": [] 153 | }, 154 | { 155 | "random": [], 156 | "name": "concede" 157 | }, 158 | { 159 | "random": [], 160 | "name": "end" 161 | } 162 | ], 163 | "header": { 164 | "random": [], 165 | "decks": [ 166 | { 167 | "hero": "Malfurion", 168 | "cards": [ 169 | "Stonetusk Boar", 170 | "Stonetusk Boar", 171 | "Stonetusk Boar", 172 | "Soul of the Forest" 173 | ] 174 | }, 175 | { 176 | "hero": "Anduin", 177 | "cards": [ 178 | "Stonetusk Boar", 179 | "Ironbeak Owl" 180 | ] 181 | } 182 | ], 183 | "keep": [ 184 | [ 185 | 0, 186 | 1, 187 | 2 188 | ], 189 | [ 190 | 0, 191 | 1, 192 | 2, 193 | 3 194 | ] 195 | ] 196 | } 197 | } -------------------------------------------------------------------------------- /tests/replays/card_tests/Shadowform.hsreplay: -------------------------------------------------------------------------------- 1 | { 2 | "moves": [ 3 | { 4 | "random": [], 5 | "name": "start" 6 | }, 7 | { 8 | "random": [], 9 | "name": "end" 10 | }, 11 | { 12 | "random": [], 13 | "name": "start" 14 | }, 15 | { 16 | "random": [], 17 | "name": "end" 18 | }, 19 | { 20 | "random": [], 21 | "name": "start" 22 | }, 23 | { 24 | "random": [], 25 | "name": "end" 26 | }, 27 | { 28 | "random": [], 29 | "name": "start" 30 | }, 31 | { 32 | "random": [], 33 | "name": "end" 34 | }, 35 | { 36 | "random": [], 37 | "name": "start" 38 | }, 39 | { 40 | "random": [], 41 | "name": "end" 42 | }, 43 | { 44 | "random": [], 45 | "name": "start" 46 | }, 47 | { 48 | "random": [], 49 | "name": "end" 50 | }, 51 | { 52 | "random": [], 53 | "name": "start" 54 | }, 55 | { 56 | "random": [], 57 | "name": "end" 58 | }, 59 | { 60 | "random": [], 61 | "name": "start" 62 | }, 63 | { 64 | "random": [], 65 | "name": "end" 66 | }, 67 | { 68 | "random": [], 69 | "name": "start" 70 | }, 71 | { 72 | "random": [], 73 | "name": "end" 74 | }, 75 | { 76 | "random": [], 77 | "name": "start" 78 | }, 79 | { 80 | "random": [], 81 | "name": "end" 82 | }, 83 | { 84 | "random": [], 85 | "name": "start" 86 | }, 87 | { 88 | "random": [], 89 | "name": "end" 90 | }, 91 | { 92 | "random": [], 93 | "name": "start" 94 | }, 95 | { 96 | "random": [], 97 | "name": "end" 98 | }, 99 | { 100 | "random": [], 101 | "name": "start" 102 | }, 103 | { 104 | "random": [], 105 | "name": "end" 106 | }, 107 | { 108 | "random": [], 109 | "name": "start" 110 | }, 111 | { 112 | "random": [], 113 | "name": "end" 114 | }, 115 | { 116 | "random": [], 117 | "name": "start" 118 | }, 119 | { 120 | "random": [], 121 | "name": "end" 122 | }, 123 | { 124 | "random": [], 125 | "name": "start" 126 | }, 127 | { 128 | "random": [], 129 | "name": "end" 130 | }, 131 | { 132 | "random": [], 133 | "name": "start" 134 | }, 135 | { 136 | "random": [], 137 | "name": "end" 138 | }, 139 | { 140 | "random": [], 141 | "name": "start" 142 | }, 143 | { 144 | "random": [], 145 | "name": "end" 146 | }, 147 | { 148 | "random": [], 149 | "name": "start" 150 | }, 151 | { 152 | "target": { 153 | "player": "p1" 154 | }, 155 | "random": [], 156 | "name": "power" 157 | }, 158 | { 159 | "card": { 160 | "card_index": 0 161 | }, 162 | "random": [], 163 | "name": "play" 164 | }, 165 | { 166 | "target": { 167 | "player": "p2" 168 | }, 169 | "random": [], 170 | "name": "power" 171 | }, 172 | { 173 | "random": [], 174 | "name": "concede" 175 | }, 176 | { 177 | "random": [], 178 | "name": "end" 179 | } 180 | ], 181 | "header": { 182 | "random": [], 183 | "decks": [ 184 | { 185 | "hero": "Anduin", 186 | "cards": [ 187 | "Shadowform" 188 | ] 189 | }, 190 | { 191 | "hero": "Anduin", 192 | "cards": [ 193 | "Stonetusk Boar" 194 | ] 195 | } 196 | ], 197 | "keep": [ 198 | [ 199 | 0, 200 | 1, 201 | 2 202 | ], 203 | [ 204 | 0, 205 | 1, 206 | 2, 207 | 3 208 | ] 209 | ] 210 | } 211 | } -------------------------------------------------------------------------------- /hearthbreaker/proxies.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class ProxyCharacter: 4 | def __init__(self, character_ref): 5 | if type(character_ref) is str: 6 | if character_ref.find(":") > -1: 7 | [self.player_ref, self.minion_ref] = character_ref.split(':') 8 | self.minion_ref = int(self.minion_ref) 9 | else: 10 | self.player_ref = character_ref 11 | self.minion_ref = None 12 | elif character_ref.is_hero(): 13 | if character_ref == character_ref.player.game.players[0].hero: 14 | self.player_ref = "p1" 15 | else: 16 | self.player_ref = "p2" 17 | self.minion_ref = None 18 | elif character_ref.is_minion(): 19 | if character_ref.player == character_ref.game.players[0]: 20 | self.player_ref = "p1" 21 | else: 22 | self.player_ref = "p2" 23 | self.minion_ref = character_ref.index 24 | 25 | def resolve(self, game): 26 | if self.player_ref == "p1": 27 | char = game.players[0].hero 28 | else: 29 | char = game.players[1].hero 30 | if self.minion_ref is not None: 31 | if self.minion_ref == -1: 32 | return None 33 | if self.player_ref == "p1": 34 | char = game.players[0].minions[self.minion_ref] 35 | else: 36 | char = game.players[1].minions[self.minion_ref] 37 | 38 | return char 39 | 40 | def __str__(self): 41 | if self.minion_ref is not None: 42 | return "{0}:{1}".format(self.player_ref, self.minion_ref) 43 | return self.player_ref 44 | 45 | def to_output(self): 46 | return str(self) 47 | 48 | def __to_json__(self): 49 | if self.minion_ref is not None: 50 | return { 51 | 'player': self.player_ref, 52 | 'minion': self.minion_ref 53 | } 54 | else: 55 | return { 56 | 'player': self.player_ref 57 | } 58 | 59 | @staticmethod 60 | def from_json(player, minion=None): 61 | rval = ProxyCharacter.__new__(ProxyCharacter) 62 | rval.player_ref = player 63 | rval.minion_ref = minion 64 | return rval 65 | 66 | 67 | class ProxyCard: 68 | def __init__(self, card_reference): 69 | self.option = None 70 | if isinstance(card_reference, str): 71 | if str.find(card_reference, ":") > -1: 72 | card_arr = str.split(card_reference, ":") 73 | self.card_ref = int(card_arr[0]) 74 | self.option = int(card_arr[1]) 75 | else: 76 | self.card_ref = int(card_reference) 77 | else: 78 | self.card_ref = card_reference 79 | self.targetable = False 80 | 81 | def set_option(self, option): 82 | self.option = option 83 | 84 | def resolve(self, game): 85 | if self.option is not None: 86 | game.current_player.agent.next_option = int(self.option) 87 | return game.current_player.hand[int(self.card_ref)] 88 | 89 | def __str__(self): 90 | if self.option is not None: 91 | return str(self.card_ref) + ':' + str(self.option) 92 | return str(self.card_ref) 93 | 94 | def to_output(self): 95 | return str(self) 96 | 97 | def __to_json__(self): 98 | if self.option is not None: 99 | return { 100 | 'card_index': self.card_ref, 101 | 'option': self.option 102 | } 103 | else: 104 | return { 105 | 'card_index': self.card_ref 106 | } 107 | 108 | @staticmethod 109 | def from_json(card_index, option=None): 110 | rval = ProxyCard(card_index) 111 | rval.set_option(option) 112 | return rval 113 | -------------------------------------------------------------------------------- /tests/replays/card_tests/NobleSacrifice.hsreplay: -------------------------------------------------------------------------------- 1 | { 2 | "moves": [ 3 | { 4 | "random": [], 5 | "name": "start" 6 | }, 7 | { 8 | "card": { 9 | "card_index": 0 10 | }, 11 | "random": [], 12 | "name": "play", 13 | "index": 0 14 | }, 15 | { 16 | "random": [], 17 | "name": "end" 18 | }, 19 | { 20 | "random": [], 21 | "name": "start" 22 | }, 23 | { 24 | "random": [], 25 | "name": "end" 26 | }, 27 | { 28 | "random": [], 29 | "name": "start" 30 | }, 31 | { 32 | "card": { 33 | "card_index": 0 34 | }, 35 | "random": [], 36 | "name": "play", 37 | "index": 0 38 | }, 39 | { 40 | "card": { 41 | "card_index": 0 42 | }, 43 | "random": [], 44 | "name": "play", 45 | "index": 0 46 | }, 47 | { 48 | "random": [], 49 | "name": "end" 50 | }, 51 | { 52 | "random": [], 53 | "name": "start" 54 | }, 55 | { 56 | "random": [], 57 | "name": "end" 58 | }, 59 | { 60 | "random": [], 61 | "name": "start" 62 | }, 63 | { 64 | "card": { 65 | "card_index": 0 66 | }, 67 | "random": [], 68 | "name": "play", 69 | "index": 0 70 | }, 71 | { 72 | "card": { 73 | "card_index": 0 74 | }, 75 | "random": [], 76 | "name": "play", 77 | "index": 0 78 | }, 79 | { 80 | "card": { 81 | "card_index": 0 82 | }, 83 | "random": [], 84 | "name": "play", 85 | "index": 0 86 | }, 87 | { 88 | "random": [], 89 | "name": "end" 90 | }, 91 | { 92 | "random": [], 93 | "name": "start" 94 | }, 95 | { 96 | "random": [], 97 | "name": "end" 98 | }, 99 | { 100 | "random": [], 101 | "name": "start" 102 | }, 103 | { 104 | "card": { 105 | "card_index": 0 106 | }, 107 | "random": [], 108 | "name": "play", 109 | "index": 0 110 | }, 111 | { 112 | "random": [], 113 | "name": "end" 114 | }, 115 | { 116 | "random": [], 117 | "name": "start" 118 | }, 119 | { 120 | "random": [], 121 | "name": "end" 122 | }, 123 | { 124 | "random": [], 125 | "name": "start" 126 | }, 127 | { 128 | "card": { 129 | "card_index": 0 130 | }, 131 | "random": [], 132 | "name": "play" 133 | }, 134 | { 135 | "random": [], 136 | "name": "end" 137 | }, 138 | { 139 | "random": [], 140 | "name": "start" 141 | }, 142 | { 143 | "card": { 144 | "card_index": 0 145 | }, 146 | "random": [], 147 | "name": "play", 148 | "index": 0 149 | }, 150 | { 151 | "character": { 152 | "minion": 0, 153 | "player": "p2" 154 | }, 155 | "target": { 156 | "player": "p1" 157 | }, 158 | "name": "attack", 159 | "random": [] 160 | }, 161 | { 162 | "random": [], 163 | "name": "concede" 164 | }, 165 | { 166 | "random": [], 167 | "name": "end" 168 | } 169 | ], 170 | "header": { 171 | "random": [], 172 | "decks": [ 173 | { 174 | "hero": "Uther", 175 | "cards": [ 176 | "Stonetusk Boar", 177 | "Stonetusk Boar", 178 | "Stonetusk Boar", 179 | "Stonetusk Boar", 180 | "Stonetusk Boar", 181 | "Stonetusk Boar", 182 | "Stonetusk Boar", 183 | "Noble Sacrifice" 184 | ] 185 | }, 186 | { 187 | "hero": "Anduin", 188 | "cards": [ 189 | "Stonetusk Boar" 190 | ] 191 | } 192 | ], 193 | "keep": [ 194 | [ 195 | 0, 196 | 1, 197 | 2 198 | ], 199 | [ 200 | 0, 201 | 1, 202 | 2, 203 | 3 204 | ] 205 | ] 206 | } 207 | } -------------------------------------------------------------------------------- /tests/agents/testing_agents.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from hearthbreaker.agents.basic_agents import DoNothingAgent 3 | 4 | 5 | class CardTestingAgent(DoNothingAgent): 6 | def __init__(self, play_on=1): 7 | super().__init__() 8 | 9 | self.player = None 10 | 11 | def do_turn(self, player): 12 | 13 | self.player = player 14 | while len(player.hand) > 0 and player.hand[0].can_use(player, player.game): 15 | player.game.play_card(player.hand[0]) 16 | 17 | 18 | class SelfSpellTestingAgent(CardTestingAgent): 19 | def __init__(self): 20 | super().__init__() 21 | 22 | def choose_target(self, targets): 23 | return self.player.game.current_player.hero 24 | 25 | 26 | class SelfMinionSpellTestingAgent(CardTestingAgent): 27 | def __init__(self): 28 | super().__init__() 29 | 30 | def choose_target(self, targets): 31 | return self.player.game.current_player.minions[0] 32 | 33 | 34 | class EnemySpellTestingAgent(CardTestingAgent): 35 | def __init__(self): 36 | super().__init__() 37 | 38 | def choose_target(self, targets): 39 | return self.player.game.other_player.hero 40 | 41 | 42 | class EnemyMinionSpellTestingAgent(CardTestingAgent): 43 | def __init__(self): 44 | super().__init__() 45 | 46 | def choose_target(self, targets): 47 | return self.player.game.other_player.minions[0] 48 | 49 | 50 | class OneCardPlayingAgent(DoNothingAgent): 51 | def __init__(self): 52 | super().__init__() 53 | 54 | def do_turn(self, player): 55 | if len(player.hand) > 0 and player.hand[0].can_use(player, player.game): 56 | if player.hand[0].name == "The Coin": 57 | player.game.play_card(player.hand[0]) 58 | player.game.play_card(player.hand[0]) 59 | 60 | 61 | class InspireTestingAgent(DoNothingAgent): 62 | def __init__(self): 63 | super().__init__() 64 | 65 | def do_turn(self, player): 66 | if player.hero.power.can_use() and len(player.minions) > 0: 67 | player.hero.power.use() 68 | 69 | if len(player.hand) > 0 and player.hand[0].can_use(player, player.game): 70 | if player.hand[0].name == "The Coin": 71 | player.game.play_card(player.hand[0]) 72 | player.game.play_card(player.hand[0]) 73 | 74 | 75 | class HeroPowerAndCardPlayingAgent(DoNothingAgent): 76 | def __init__(self): 77 | super().__init__() 78 | 79 | def do_turn(self, player): 80 | if player.hero.power.can_use(): 81 | player.hero.power.use() 82 | 83 | while len(player.hand) > 0 and player.hand[0].can_use(player, player.game): 84 | player.game.play_card(player.hand[0]) 85 | 86 | 87 | class MinionAttackingAgent(OneCardPlayingAgent): 88 | def do_turn(self, player): 89 | super().do_turn(player) 90 | for minion in copy.copy(player.minions): 91 | if minion.can_attack(): 92 | minion.attack() 93 | 94 | 95 | class WeaponTestingAgent(DoNothingAgent): 96 | def __init__(self): 97 | super().__init__() 98 | self.played_card = False 99 | 100 | def do_turn(self, player): 101 | if not self.played_card and player.hand[0].can_use(player, player.game): 102 | player.game.play_card(player.hand[0]) 103 | self.played_card = True 104 | 105 | if player.hero.can_attack(): 106 | player.hero.attack() 107 | 108 | 109 | class PlayAndAttackAgent(DoNothingAgent): 110 | def do_turn(self, player): 111 | 112 | while len(player.hand) > 0 and player.hand[0].can_use(player, player.game): 113 | player.game.play_card(player.hand[0]) 114 | 115 | while player.hero.can_attack(): 116 | player.hero.attack() 117 | 118 | done_something = True 119 | while done_something: 120 | done_something = False 121 | for minion in player.minions: 122 | if minion.can_attack(): 123 | done_something = True 124 | minion.attack() 125 | break 126 | -------------------------------------------------------------------------------- /hearthbreaker/cards/spells/neutral.py: -------------------------------------------------------------------------------- 1 | from hearthbreaker.cards.base import SpellCard 2 | from hearthbreaker.constants import CHARACTER_CLASS, CARD_RARITY 3 | from hearthbreaker.tags.base import BuffUntil, Buff 4 | from hearthbreaker.tags.event import TurnStarted 5 | from hearthbreaker.tags.status import Stealth, Taunt, Frozen 6 | import hearthbreaker.targeting 7 | 8 | 9 | class TheCoin(SpellCard): 10 | def __init__(self): 11 | super().__init__("The Coin", 0, CHARACTER_CLASS.ALL, CARD_RARITY.COMMON, False) 12 | 13 | def use(self, player, game): 14 | super().use(player, game) 15 | if player.mana < 10: 16 | player.mana += 1 17 | 18 | 19 | class ArmorPlating(SpellCard): 20 | def __init__(self): 21 | super().__init__("Armor Plating", 1, CHARACTER_CLASS.ALL, CARD_RARITY.COMMON, False, 22 | target_func=hearthbreaker.targeting.find_minion_spell_target) 23 | 24 | def use(self, player, game): 25 | super().use(player, game) 26 | self.target.increase_health(1) 27 | 28 | 29 | class EmergencyCoolant(SpellCard): 30 | def __init__(self): 31 | super().__init__("Emergency Coolant", 1, CHARACTER_CLASS.ALL, CARD_RARITY.COMMON, False, 32 | target_func=hearthbreaker.targeting.find_minion_spell_target) 33 | 34 | def use(self, player, game): 35 | super().use(player, game) 36 | self.target.add_buff(Buff(Frozen())) 37 | 38 | 39 | class FinickyCloakfield(SpellCard): 40 | def __init__(self): 41 | super().__init__("Finicky Cloakfield", 1, CHARACTER_CLASS.ALL, CARD_RARITY.COMMON, False, 42 | target_func=hearthbreaker.targeting.find_friendly_minion_spell_target) 43 | 44 | def use(self, player, game): 45 | super().use(player, game) 46 | self.target.add_buff(BuffUntil(Stealth(), TurnStarted())) 47 | 48 | 49 | class ReversingSwitch(SpellCard): 50 | def __init__(self): 51 | super().__init__("Reversing Switch", 1, CHARACTER_CLASS.ALL, CARD_RARITY.COMMON, False, 52 | target_func=hearthbreaker.targeting.find_minion_spell_target) 53 | 54 | def use(self, player, game): 55 | super().use(player, game) 56 | temp_attack = self.target.calculate_attack() 57 | temp_health = self.target.health 58 | if temp_attack == 0: 59 | self.target.die(None) 60 | else: 61 | self.target.set_attack_to(temp_health) 62 | self.target.set_health_to(temp_attack) 63 | 64 | 65 | class RustyHorn(SpellCard): 66 | def __init__(self): 67 | super().__init__("Rusty Horn", 1, CHARACTER_CLASS.ALL, CARD_RARITY.COMMON, False, 68 | target_func=hearthbreaker.targeting.find_minion_spell_target) 69 | 70 | def use(self, player, game): 71 | super().use(player, game) 72 | self.target.add_buff(Buff(Taunt())) 73 | 74 | 75 | class TimeRewinder(SpellCard): 76 | def __init__(self): 77 | super().__init__("Time Rewinder", 1, CHARACTER_CLASS.ALL, CARD_RARITY.COMMON, False, 78 | target_func=hearthbreaker.targeting.find_friendly_minion_spell_target) 79 | 80 | def use(self, player, game): 81 | super().use(player, game) 82 | self.target.bounce() 83 | 84 | 85 | class WhirlingBlades(SpellCard): 86 | def __init__(self): 87 | super().__init__("Whirling Blades", 1, CHARACTER_CLASS.ALL, CARD_RARITY.COMMON, False, 88 | target_func=hearthbreaker.targeting.find_minion_spell_target) 89 | 90 | def use(self, player, game): 91 | super().use(player, game) 92 | self.target.change_attack(1) 93 | 94 | 95 | spare_part_list = [ArmorPlating(), EmergencyCoolant(), FinickyCloakfield(), TimeRewinder(), ReversingSwitch(), 96 | RustyHorn(), WhirlingBlades()] 97 | 98 | 99 | class GallywixsCoin(SpellCard): 100 | def __init__(self): 101 | super().__init__("Gallywix's Coin", 0, CHARACTER_CLASS.ALL, CARD_RARITY.COMMON, False) 102 | 103 | def use(self, player, game): 104 | super().use(player, game) 105 | if player.mana < 10: 106 | player.mana += 1 107 | -------------------------------------------------------------------------------- /hearthbreaker/agents/basic_agents.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import copy 3 | 4 | import random 5 | from hearthbreaker.cards.base import Card 6 | 7 | 8 | class Agent(metaclass=abc.ABCMeta): 9 | 10 | @abc.abstractmethod 11 | def do_card_check(self, cards): 12 | pass 13 | 14 | @abc.abstractmethod 15 | def do_turn(self, player): 16 | pass 17 | 18 | @abc.abstractmethod 19 | def choose_target(self, targets): 20 | pass 21 | 22 | @abc.abstractmethod 23 | def choose_index(self, card, player): 24 | pass 25 | 26 | @abc.abstractmethod 27 | def choose_option(self, options, player): 28 | pass 29 | 30 | def filter_options(self, options, player): 31 | if isinstance(options[0], Card): 32 | return [option for option in options if option.can_choose(player)] 33 | return [option for option in options if option.card.can_choose(player)] 34 | 35 | 36 | class DoNothingAgent(Agent): 37 | def __init__(self): 38 | self.game = None 39 | 40 | def do_card_check(self, cards): 41 | return [True, True, True, True] 42 | 43 | def do_turn(self, player): 44 | pass 45 | 46 | def choose_target(self, targets): 47 | return targets[0] 48 | 49 | def choose_index(self, card, player): 50 | return 0 51 | 52 | def choose_option(self, options, player): 53 | return self.filter_options(options, player)[0] 54 | 55 | 56 | class PredictableAgent(Agent): 57 | def do_card_check(self, cards): 58 | return [True, True, True, True] 59 | 60 | def do_turn(self, player): 61 | done_something = True 62 | 63 | if player.hero.power.can_use(): 64 | player.hero.power.use() 65 | 66 | if player.hero.can_attack(): 67 | player.hero.attack() 68 | 69 | while done_something: 70 | done_something = False 71 | for card in player.hand: 72 | if card.can_use(player, player.game): 73 | player.game.play_card(card) 74 | done_something = True 75 | break 76 | 77 | for minion in copy.copy(player.minions): 78 | if minion.can_attack(): 79 | minion.attack() 80 | 81 | def choose_target(self, targets): 82 | return targets[0] 83 | 84 | def choose_index(self, card, player): 85 | return 0 86 | 87 | def choose_option(self, options, player): 88 | return self.filter_options(options, player)[0] 89 | 90 | 91 | class RandomAgent(DoNothingAgent): 92 | def __init__(self): 93 | super().__init__() 94 | 95 | def do_card_check(self, cards): 96 | return [True, True, True, True] 97 | 98 | def do_turn(self, player): 99 | while True: 100 | attack_minions = [minion for minion in filter(lambda minion: minion.can_attack(), player.minions)] 101 | if player.hero.can_attack(): 102 | attack_minions.append(player.hero) 103 | playable_cards = [card for card in filter(lambda card: card.can_use(player, player.game), player.hand)] 104 | if player.hero.power.can_use(): 105 | possible_actions = len(attack_minions) + len(playable_cards) + 1 106 | else: 107 | possible_actions = len(attack_minions) + len(playable_cards) 108 | if possible_actions > 0: 109 | action = random.randint(0, possible_actions - 1) 110 | if player.hero.power.can_use() and action == possible_actions - 1: 111 | player.hero.power.use() 112 | elif action < len(attack_minions): 113 | attack_minions[action].attack() 114 | else: 115 | player.game.play_card(playable_cards[action - len(attack_minions)]) 116 | else: 117 | return 118 | 119 | def choose_target(self, targets): 120 | return targets[random.randint(0, len(targets) - 1)] 121 | 122 | def choose_index(self, card, player): 123 | return random.randint(0, len(player.minions)) 124 | 125 | def choose_option(self, options, player): 126 | options = self.filter_options(options, player) 127 | return options[random.randint(0, len(options) - 1)] 128 | -------------------------------------------------------------------------------- /hearthbreaker/agents/trade_agent.py: -------------------------------------------------------------------------------- 1 | from hearthbreaker.agents.basic_agents import DoNothingAgent 2 | from hearthbreaker.agents.trade.possible_play import PlayMixin 3 | from hearthbreaker.agents.trade.trade import TradeMixin, AttackMixin 4 | from hearthbreaker.agents.trade.util import Util 5 | from hearthbreaker.tags.action import Damage 6 | from hearthbreaker.tags.status import ChangeAttack, ChangeHealth 7 | 8 | 9 | class BattlecryType: 10 | 11 | @staticmethod 12 | def target_type_for_card(card): 13 | res = None 14 | 15 | if hasattr(card, 'battlecry') and card.battlecry: 16 | for battlecry in card.battlecry: 17 | for action in battlecry.actions: 18 | # TODO add more battlecries here 19 | if isinstance(action, ChangeAttack) or isinstance(action, ChangeHealth): 20 | res = "Friendly" 21 | elif isinstance(action, Damage): 22 | res = "Enemy" 23 | return res 24 | 25 | 26 | class ChooseTargetMixin: 27 | def choose_target_enemy(self, all_targets): 28 | if len(all_targets) == 0: 29 | raise Exception("No targets") 30 | 31 | targets = self.prune_targets(all_targets, False) 32 | if len(targets) == 0: 33 | return Util.rand_el(all_targets) 34 | 35 | if not self.current_trade: 36 | return Util.rand_prefer_minion(targets) 37 | # raise Exception("No current trade") 38 | 39 | for target in targets: 40 | if self.current_trade.opp_minion == target: 41 | return target 42 | 43 | # raise Exception("Could not find target {}".format(target)) 44 | return Util.rand_prefer_minion(targets) 45 | 46 | def choose_target_friendly(self, targets): 47 | pruned = self.prune_targets(targets, True) 48 | if len(pruned) == 0: 49 | return Util.rand_el(targets) 50 | 51 | return Util.rand_el(pruned) 52 | 53 | def prune_targets(self, targets, get_friendly): 54 | res = [] 55 | for target in targets: 56 | is_friendly_minion = any(map(lambda c: c == target, self.player.minions)) 57 | is_friendly_hero = target == self.player.hero 58 | is_friendly = is_friendly_minion or is_friendly_hero 59 | 60 | if is_friendly == get_friendly: 61 | res.append(target) 62 | 63 | return res 64 | 65 | def has_friendly_targets(self, targets): 66 | return len(self.prune_targets(targets, True)) > 0 67 | 68 | def should_target_self(self, targets): 69 | cry_type = BattlecryType.target_type_for_card(self.last_card_played) 70 | 71 | if cry_type == "Friendly": 72 | return True 73 | elif cry_type == "Enemy": 74 | return False 75 | elif self.last_card_played.name == "Elven Archerzzz": 76 | return False 77 | elif self.has_friendly_targets(targets): 78 | return True 79 | else: 80 | return False 81 | 82 | def choose_target_inner(self, targets): 83 | if len(targets) == 0: 84 | return None 85 | 86 | if self.should_target_self(targets): 87 | return self.choose_target_friendly(targets) 88 | else: 89 | return self.choose_target_enemy(targets) 90 | 91 | def choose_target(self, targets): 92 | res = self.choose_target_inner(targets) 93 | # print("Target {}".format(res)) 94 | return res 95 | 96 | 97 | class NullCard: 98 | def __init__(self): 99 | self.name = "Null Card" 100 | 101 | def create_minion(self, player): 102 | return None 103 | 104 | 105 | class TradeAgent(TradeMixin, AttackMixin, PlayMixin, ChooseTargetMixin, DoNothingAgent): 106 | def __init__(self): 107 | super().__init__() 108 | self.current_trade = None 109 | self.last_card_played = NullCard() 110 | 111 | def do_turn(self, player): 112 | self.player = player 113 | self.play_cards(player) 114 | self.attack(player) 115 | 116 | if not player.game.game_ended: 117 | self.play_cards(player) 118 | 119 | def do_card_check(self, cards): 120 | return [True, True, True, True] 121 | 122 | def choose_index(self, card, player): 123 | return 0 124 | -------------------------------------------------------------------------------- /tests/agent_tests.py: -------------------------------------------------------------------------------- 1 | import random 2 | import unittest 3 | from hearthbreaker.agents.basic_agents import RandomAgent 4 | from hearthbreaker.cards import GoldshireFootman, MurlocRaider, BloodfenRaptor, FrostwolfGrunt, RiverCrocolisk, \ 5 | IronfurGrizzly, MagmaRager, SilverbackPatriarch, ChillwindYeti, SenjinShieldmasta, BootyBayBodyguard, \ 6 | FenCreeper, BoulderfistOgre, WarGolem, Shieldbearer, FlameImp, YoungPriestess, DarkIronDwarf, DireWolfAlpha, \ 7 | Voidwalker, HarvestGolem, KnifeJuggler, ShatteredSunCleric, ArgentSquire, Doomguard, Soulfire, DefenderOfArgus, \ 8 | AbusiveSergeant, NerubianEgg, KeeperOfTheGrove 9 | from hearthbreaker.cards.heroes import Guldan, Malfurion 10 | from hearthbreaker.engine import Game, Deck 11 | 12 | 13 | class TestAgents(unittest.TestCase): 14 | 15 | def setUp(self): 16 | random.seed(1857) 17 | 18 | def test_RandomAgent(self): 19 | deck1 = Deck([ 20 | GoldshireFootman(), 21 | GoldshireFootman(), 22 | MurlocRaider(), 23 | MurlocRaider(), 24 | BloodfenRaptor(), 25 | BloodfenRaptor(), 26 | FrostwolfGrunt(), 27 | FrostwolfGrunt(), 28 | RiverCrocolisk(), 29 | RiverCrocolisk(), 30 | IronfurGrizzly(), 31 | IronfurGrizzly(), 32 | MagmaRager(), 33 | MagmaRager(), 34 | SilverbackPatriarch(), 35 | SilverbackPatriarch(), 36 | ChillwindYeti(), 37 | ChillwindYeti(), 38 | KeeperOfTheGrove(), 39 | KeeperOfTheGrove(), 40 | SenjinShieldmasta(), 41 | SenjinShieldmasta(), 42 | BootyBayBodyguard(), 43 | BootyBayBodyguard(), 44 | FenCreeper(), 45 | FenCreeper(), 46 | BoulderfistOgre(), 47 | BoulderfistOgre(), 48 | WarGolem(), 49 | WarGolem(), 50 | ], Malfurion()) 51 | 52 | deck2 = Deck([ 53 | Shieldbearer(), 54 | Shieldbearer(), 55 | FlameImp(), 56 | FlameImp(), 57 | YoungPriestess(), 58 | YoungPriestess(), 59 | DarkIronDwarf(), 60 | DarkIronDwarf(), 61 | DireWolfAlpha(), 62 | DireWolfAlpha(), 63 | Voidwalker(), 64 | Voidwalker(), 65 | HarvestGolem(), 66 | HarvestGolem(), 67 | KnifeJuggler(), 68 | KnifeJuggler(), 69 | ShatteredSunCleric(), 70 | ShatteredSunCleric(), 71 | ArgentSquire(), 72 | ArgentSquire(), 73 | Doomguard(), 74 | Doomguard(), 75 | Soulfire(), 76 | Soulfire(), 77 | DefenderOfArgus(), 78 | DefenderOfArgus(), 79 | AbusiveSergeant(), 80 | AbusiveSergeant(), 81 | NerubianEgg(), 82 | NerubianEgg(), 83 | ], Guldan()) 84 | 85 | game = Game([deck1, deck2], [RandomAgent(), RandomAgent()]) 86 | game.pre_game() 87 | game.current_player = game.players[1] 88 | 89 | game.play_single_turn() 90 | 91 | self.assertEqual(0, len(game.current_player.minions)) 92 | 93 | game.play_single_turn() 94 | self.assertEqual(2, len(game.current_player.minions)) 95 | self.assertEqual(3, game.current_player.minions[1].health) 96 | self.assertEqual("Young Priestess", game.current_player.minions[0].card.name) 97 | 98 | game.play_single_turn() 99 | self.assertEqual(1, len(game.current_player.minions)) 100 | self.assertEqual("Frostwolf Grunt", game.current_player.minions[0].card.name) 101 | 102 | game.play_single_turn() 103 | self.assertEqual(0, len(game.other_player.minions)) 104 | self.assertEqual(28, game.other_player.hero.health) 105 | self.assertEqual(3, len(game.current_player.minions)) 106 | self.assertEqual("Dire Wolf Alpha", game.current_player.minions[2].card.name) 107 | 108 | for turn in range(0, 13): 109 | game.play_single_turn() 110 | self.assertFalse(game.game_ended) 111 | 112 | game.play_single_turn() 113 | 114 | self.assertEqual(0, game.current_player.hero.health) 115 | self.assertEqual(21, game.other_player.hero.health) 116 | 117 | self.assertTrue(game.game_ended) 118 | -------------------------------------------------------------------------------- /hearthbreaker/tags/event.py: -------------------------------------------------------------------------------- 1 | from hearthbreaker.tags.base import MinionEvent, PlayerEvent 2 | from hearthbreaker.tags.condition import MinionIsNotTarget, CardIsNotTarget 3 | from hearthbreaker.tags.selector import FriendlyPlayer 4 | 5 | 6 | class SpellCast(PlayerEvent): 7 | def __init__(self, condition=None, player=FriendlyPlayer()): 8 | super().__init__("spell_cast", condition, player) 9 | 10 | def bind(self, target, func): 11 | for player in self.player.get_players(target.player): 12 | self.__target__ = target 13 | self.__func__ = func 14 | player.bind("card_played", self.__action__) 15 | 16 | def unbind(self, target, func): 17 | for player in self.player.get_players(target.player): 18 | player.unbind("card_played", self.__action__) 19 | 20 | def __action__(self, card, index): 21 | if card.is_spell(): 22 | if self.condition: 23 | super().__action__(card, index) 24 | else: 25 | self.__func__(card, index) 26 | 27 | 28 | class CardPlayed(PlayerEvent): 29 | def __init__(self, condition=None, player=FriendlyPlayer()): 30 | super().__init__("card_played", condition, player) 31 | 32 | 33 | class CardUsed(PlayerEvent): 34 | def __init__(self, condition=CardIsNotTarget(), player=FriendlyPlayer()): 35 | super().__init__("card_used", condition, player) 36 | 37 | 38 | class CardDrawn(PlayerEvent): 39 | def __init__(self, condition=CardIsNotTarget(), player=FriendlyPlayer()): 40 | super().__init__("card_drawn", condition, player) 41 | 42 | 43 | class AfterAdded(PlayerEvent): 44 | def __init__(self, condition=MinionIsNotTarget(), player=FriendlyPlayer()): 45 | super().__init__("after_added", condition, player) 46 | 47 | 48 | class TurnEnded(PlayerEvent): 49 | def __init__(self, condition=None, player=FriendlyPlayer()): 50 | super().__init__("turn_ended", condition, player) 51 | 52 | 53 | class TurnStarted(PlayerEvent): 54 | def __init__(self, condition=None, player=FriendlyPlayer()): 55 | super().__init__("turn_started", condition, player) 56 | 57 | 58 | class MinionDied(PlayerEvent): 59 | def __init__(self, condition=None, player=FriendlyPlayer()): 60 | super().__init__("minion_died", condition, player) 61 | 62 | 63 | class MinionPlaced(PlayerEvent): 64 | def __init__(self, condition=MinionIsNotTarget(), player=FriendlyPlayer()): 65 | super().__init__("minion_placed", condition, player) 66 | 67 | 68 | class MinionSummoned(PlayerEvent): 69 | def __init__(self, condition=MinionIsNotTarget(), player=FriendlyPlayer()): 70 | super().__init__("minion_summoned", condition, player) 71 | 72 | 73 | class CharacterDamaged(PlayerEvent): 74 | def __init__(self, condition=None, player=FriendlyPlayer()): 75 | super().__init__("character_damaged", condition, player) 76 | 77 | 78 | class CharacterHealed(PlayerEvent): 79 | def __init__(self, condition=None, player=FriendlyPlayer()): 80 | super().__init__("character_healed", condition, player) 81 | 82 | 83 | class SecretRevealed(PlayerEvent): 84 | def __init__(self, condition=None, player=FriendlyPlayer()): 85 | super().__init__("secret_revealed", condition, player) 86 | 87 | 88 | class CharacterAttack(PlayerEvent): 89 | def __init__(self, condition=None, player=FriendlyPlayer()): 90 | super().__init__("character_attack", condition, player) 91 | 92 | 93 | class ArmorIncreased(PlayerEvent): 94 | def __init__(self, condition=None, player=FriendlyPlayer()): 95 | super().__init__("armor_increased", condition, player) 96 | 97 | 98 | class UsedPower(PlayerEvent): 99 | def __init__(self, condition=None, player=FriendlyPlayer()): 100 | super().__init__("used_power", condition, player) 101 | 102 | 103 | class CardDiscarded(PlayerEvent): 104 | def __init__(self, condition=None, player=FriendlyPlayer()): 105 | super().__init__("card_discarded", condition, player) 106 | 107 | 108 | class Attack(MinionEvent): 109 | def __init__(self, condition=None): 110 | super().__init__("attack", condition) 111 | 112 | 113 | class AttackCompleted(MinionEvent): 114 | def __init__(self): 115 | super().__init__("attack_completed") 116 | 117 | 118 | class DidDamage(MinionEvent): 119 | def __init__(self): 120 | super().__init__("did_damage") 121 | 122 | 123 | class WeaponDestroyed(MinionEvent): 124 | def __init__(self): 125 | super().__init__("weapon_destroyed") 126 | 127 | 128 | class Damaged(MinionEvent): 129 | def __init__(self): 130 | super().__init__("damaged") 131 | 132 | 133 | class Drawn(MinionEvent): 134 | def __init__(self): 135 | super().__init__("drawn") 136 | 137 | 138 | class SpellTargeted(MinionEvent): 139 | def __init__(self): 140 | super().__init__("spell_targeted") 141 | -------------------------------------------------------------------------------- /hearthbreaker/cards/spells/__init__.py: -------------------------------------------------------------------------------- 1 | from hearthbreaker.cards.spells.neutral import ( 2 | ArmorPlating, 3 | EmergencyCoolant, 4 | FinickyCloakfield, 5 | ReversingSwitch, 6 | RustyHorn, 7 | TimeRewinder, 8 | WhirlingBlades, 9 | TheCoin 10 | ) 11 | 12 | from hearthbreaker.cards.spells.druid import ( 13 | Innervate, 14 | Moonfire, 15 | Claw, 16 | Naturalize, 17 | Savagery, 18 | MarkOfTheWild, 19 | PowerOfTheWild, 20 | WildGrowth, 21 | Wrath, 22 | HealingTouch, 23 | MarkOfNature, 24 | SavageRoar, 25 | Bite, 26 | SoulOfTheForest, 27 | Swipe, 28 | Nourish, 29 | Starfall, 30 | ForceOfNature, 31 | Starfire, 32 | PoisonSeeds, 33 | DarkWispers, 34 | Recycle, 35 | TreeOfLife, 36 | AstralCommunion, 37 | ) 38 | 39 | from hearthbreaker.cards.spells.hunter import ( 40 | HuntersMark, 41 | ArcaneShot, 42 | BestialWrath, 43 | Flare, 44 | Tracking, 45 | ExplosiveTrap, 46 | FreezingTrap, 47 | Misdirection, 48 | Snipe, 49 | DeadlyShot, 50 | MultiShot, 51 | ExplosiveShot, 52 | KillCommand, 53 | UnleashTheHounds, 54 | AnimalCompanion, 55 | SnakeTrap, 56 | CallPet, 57 | CobraShot, 58 | FeignDeath, 59 | QuickShot, 60 | BearTrap, 61 | Powershot, 62 | ) 63 | 64 | from hearthbreaker.cards.spells.mage import ( 65 | ArcaneMissiles, 66 | IceLance, 67 | MirrorImage, 68 | ArcaneExplosion, 69 | Frostbolt, 70 | ArcaneIntellect, 71 | FrostNova, 72 | Counterspell, 73 | IceBarrier, 74 | IceBlock, 75 | MirrorEntity, 76 | Spellbender, 77 | Vaporize, 78 | ConeOfCold, 79 | Fireball, 80 | Polymorph, 81 | Blizzard, 82 | Flamestrike, 83 | Pyroblast, 84 | Duplicate, 85 | Flamecannon, 86 | EchoOfMedivh, 87 | UnstablePortal, 88 | DragonsBreath, 89 | ArcaneBlast, 90 | ) 91 | 92 | from hearthbreaker.cards.spells.paladin import ( 93 | AvengingWrath, 94 | BlessedChampion, 95 | BlessingOfKings, 96 | BlessingOfMight, 97 | BlessingOfWisdom, 98 | Consecration, 99 | DivineFavor, 100 | Equality, 101 | HammerOfWrath, 102 | HandOfProtection, 103 | HolyLight, 104 | HolyWrath, 105 | Humility, 106 | LayOnHands, 107 | EyeForAnEye, 108 | NobleSacrifice, 109 | Redemption, 110 | Repentance, 111 | Avenge, 112 | SealOfLight, 113 | MusterForBattle, 114 | SolemnVigil, 115 | ) 116 | 117 | from hearthbreaker.cards.spells.priest import ( 118 | CircleOfHealing, 119 | DivineSpirit, 120 | HolyFire, 121 | HolyNova, 122 | HolySmite, 123 | InnerFire, 124 | MassDispel, 125 | MindBlast, 126 | MindControl, 127 | MindVision, 128 | Mindgames, 129 | PowerWordShield, 130 | ShadowMadness, 131 | ShadowWordDeath, 132 | ShadowWordPain, 133 | Shadowform, 134 | Silence, 135 | Thoughtsteal, 136 | VelensChosen, 137 | Lightbomb, 138 | LightOfTheNaaru, 139 | Resurrect, 140 | ) 141 | 142 | from hearthbreaker.cards.spells.rogue import ( 143 | Assassinate, 144 | Backstab, 145 | Betrayal, 146 | BladeFlurry, 147 | ColdBlood, 148 | Conceal, 149 | DeadlyPoison, 150 | Eviscerate, 151 | FanOfKnives, 152 | Headcrack, 153 | Preparation, 154 | Sap, 155 | Shadowstep, 156 | Shiv, 157 | SinisterStrike, 158 | Sprint, 159 | Vanish, 160 | TinkersSharpswordOil, 161 | Sabotage, 162 | GangUp, 163 | ) 164 | 165 | from hearthbreaker.cards.spells.shaman import ( 166 | AncestralHealing, 167 | AncestralSpirit, 168 | Bloodlust, 169 | EarthShock, 170 | FarSight, 171 | FeralSpirit, 172 | ForkedLightning, 173 | FrostShock, 174 | Hex, 175 | LavaBurst, 176 | LightningBolt, 177 | LightningStorm, 178 | RockbiterWeapon, 179 | TotemicMight, 180 | Windfury, 181 | Reincarnate, 182 | Crackle, 183 | AncestorsCall, 184 | LavaShock, 185 | AncestralKnowledge, 186 | ) 187 | 188 | from hearthbreaker.cards.spells.warlock import ( 189 | MortalCoil, 190 | Hellfire, 191 | ShadowBolt, 192 | DrainLife, 193 | Soulfire, 194 | TwistingNether, 195 | Demonfire, 196 | SacrificialPact, 197 | SiphonSoul, 198 | SenseDemons, 199 | BaneOfDoom, 200 | Shadowflame, 201 | Corruption, 202 | PowerOverwhelming, 203 | Darkbomb, 204 | Demonheart, 205 | Implosion, 206 | Demonwrath, 207 | FistOfJaraxxus, 208 | ) 209 | 210 | from hearthbreaker.cards.spells.warrior import ( 211 | BattleRage, 212 | Brawl, 213 | Charge, 214 | Cleave, 215 | CommandingShout, 216 | Execute, 217 | HeroicStrike, 218 | InnerRage, 219 | MortalStrike, 220 | Rampage, 221 | ShieldBlock, 222 | ShieldSlam, 223 | Slam, 224 | Upgrade, 225 | Whirlwind, 226 | BouncingBlade, 227 | Crush, 228 | BurrowingMine, 229 | Revenge, 230 | ) 231 | -------------------------------------------------------------------------------- /tests/agents/trade/play_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from hearthbreaker.cards import ArgentSquire, DireWolfAlpha, HarvestGolem, BloodfenRaptor, MagmaRager, Wisp, Ysera 3 | from hearthbreaker.cards.spells.neutral import TheCoin 4 | from tests.agents.trade.test_helpers import TestHelpers 5 | from hearthbreaker.agents.trade.possible_play import PossiblePlays 6 | from tests.agents.trade.test_case_mixin import TestCaseMixin 7 | 8 | 9 | class TestTradeAgentPlayTests(TestCaseMixin, unittest.TestCase): 10 | def test_simple_plays(self): 11 | game = TestHelpers().make_game() 12 | 13 | self.set_hand(game, 0, ArgentSquire(), DireWolfAlpha(), HarvestGolem()) 14 | 15 | game.play_single_turn() 16 | 17 | self.assert_minions(game.players[0], "Argent Squire") 18 | 19 | game.play_single_turn() 20 | game.play_single_turn() 21 | 22 | self.assert_minions(game.players[0], "Argent Squire", "Dire Wolf Alpha") 23 | 24 | def test_will_play_biggest(self): 25 | game = TestHelpers().make_game() 26 | 27 | game.players[0].hand = self.make_cards(game.current_player, ArgentSquire(), ArgentSquire(), DireWolfAlpha()) 28 | game.players[0].mana = 1 29 | game.players[0].max_mana = 1 30 | 31 | game.play_single_turn() 32 | 33 | self.assert_minions(game.players[0], "Dire Wolf Alpha") 34 | 35 | def test_will_play_multiple(self): 36 | game = TestHelpers().make_game() 37 | 38 | game.players[0].hand = self.make_cards(game.current_player, ArgentSquire(), ArgentSquire(), ArgentSquire()) 39 | game.players[0].mana = 1 40 | game.players[0].max_mana = 1 41 | 42 | game.play_single_turn() 43 | 44 | self.assert_minions(game.players[0], "Argent Squire", "Argent Squire") 45 | 46 | def test_will_play_multiple_correct_order(self): 47 | game = TestHelpers().make_game() 48 | 49 | game.players[0].hand = self.make_cards(game.current_player, ArgentSquire(), ArgentSquire(), ArgentSquire(), 50 | HarvestGolem()) 51 | game.players[0].mana = 3 52 | game.players[0].max_mana = 3 53 | 54 | game.play_single_turn() 55 | 56 | self.assert_minions(game.players[0], "Harvest Golem", "Argent Squire") 57 | 58 | def test_will_use_entire_pool(self): 59 | game = TestHelpers().make_game() 60 | 61 | game.players[0].hand = self.make_cards(game.current_player, DireWolfAlpha(), DireWolfAlpha(), DireWolfAlpha(), 62 | HarvestGolem()) 63 | game.players[0].mana = 3 64 | game.players[0].max_mana = 3 65 | 66 | game.play_single_turn() 67 | 68 | self.assert_minions(game.players[0], "Dire Wolf Alpha", "Dire Wolf Alpha") 69 | 70 | def test_will_play_three_cards(self): 71 | game = TestHelpers().make_game() 72 | 73 | self.set_hand(game, 0, Wisp(), ArgentSquire(), DireWolfAlpha()) 74 | self.set_mana(game, 0, 3) 75 | 76 | game.play_single_turn() 77 | 78 | self.assert_minions(game.players[0], "Wisp", "Argent Squire", "Dire Wolf Alpha") 79 | 80 | 81 | class TestTradeAgentPlayCoinTests(TestCaseMixin, unittest.TestCase): 82 | def test_coin(self): 83 | game = self.make_game() 84 | cards = self.make_cards(game.current_player, ArgentSquire(), BloodfenRaptor(), TheCoin()) 85 | possible_plays = PossiblePlays(cards, 1) 86 | play = possible_plays.plays()[0] 87 | names = [c.name for c in play.cards] 88 | self.assertEqual(names, ["The Coin", "Bloodfen Raptor"]) 89 | 90 | def test_coin_save(self): 91 | game = self.make_game() 92 | cards = self.make_cards(game.current_player, ArgentSquire(), MagmaRager(), TheCoin()) 93 | possible_plays = PossiblePlays(cards, 1) 94 | play = possible_plays.plays()[0] 95 | names = [c.name for c in play.cards] 96 | self.assertEqual(names, ["Argent Squire"]) 97 | 98 | 99 | class TestTradeAgentHeroPowerTests(TestCaseMixin, unittest.TestCase): 100 | def test_will_use_hero_power_with_empty_hand(self): 101 | game = TestHelpers().make_game() 102 | 103 | self.set_hand(game, 0) 104 | self.set_mana(game, 0, 10) 105 | 106 | possible = PossiblePlays([], 10) 107 | play = possible.plays()[0] 108 | self.assertEqual(play.cards[0].name, "Hero Power") 109 | 110 | game.play_single_turn() 111 | self.assert_minions(game.players[0], "War Golem") 112 | 113 | def test_wont_kill_self_with_hero_power(self): 114 | game = TestHelpers().make_game() 115 | 116 | self.set_hand(game, 0) 117 | self.set_mana(game, 0, 2) 118 | game.players[0].hero.health = 1 119 | 120 | game.play_single_turn() 121 | self.assert_minions(game.players[0]) 122 | self.assertEqual(game.players[0].hero.health, 1) 123 | 124 | def test_will_hero_power_first_if_inevitable(self): 125 | game = self.make_game() 126 | cards = self.make_cards(game.current_player, DireWolfAlpha()) 127 | possible = PossiblePlays(cards, 10) 128 | play = possible.plays()[0] 129 | self.assertEqual(play.first_card().name, "Hero Power") 130 | 131 | def test_will_not_hero_power_if_not_inevitable(self): 132 | game = self.make_game() 133 | cards = self.make_cards(game.current_player, Ysera()) 134 | possible = PossiblePlays(cards, 10) 135 | play = possible.plays()[0] 136 | self.assertEqual(play.first_card().name, "Ysera") 137 | -------------------------------------------------------------------------------- /jsonschema/schemas/draft3.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-03/schema#", 3 | "dependencies": { 4 | "exclusiveMaximum": "maximum", 5 | "exclusiveMinimum": "minimum" 6 | }, 7 | "id": "http://json-schema.org/draft-03/schema#", 8 | "properties": { 9 | "$ref": { 10 | "format": "uri", 11 | "type": "string" 12 | }, 13 | "$schema": { 14 | "format": "uri", 15 | "type": "string" 16 | }, 17 | "additionalItems": { 18 | "default": {}, 19 | "type": [ 20 | { 21 | "$ref": "#" 22 | }, 23 | "boolean" 24 | ] 25 | }, 26 | "additionalProperties": { 27 | "default": {}, 28 | "type": [ 29 | { 30 | "$ref": "#" 31 | }, 32 | "boolean" 33 | ] 34 | }, 35 | "default": { 36 | "type": "any" 37 | }, 38 | "dependencies": { 39 | "additionalProperties": { 40 | "items": { 41 | "type": "string" 42 | }, 43 | "type": [ 44 | "string", 45 | "array", 46 | { 47 | "$ref": "#" 48 | } 49 | ] 50 | }, 51 | "default": {}, 52 | "type": [ 53 | "string", 54 | "array", 55 | "object" 56 | ] 57 | }, 58 | "description": { 59 | "type": "string" 60 | }, 61 | "disallow": { 62 | "items": { 63 | "type": [ 64 | "string", 65 | { 66 | "$ref": "#" 67 | } 68 | ] 69 | }, 70 | "type": [ 71 | "string", 72 | "array" 73 | ], 74 | "uniqueItems": true 75 | }, 76 | "divisibleBy": { 77 | "default": 1, 78 | "exclusiveMinimum": true, 79 | "minimum": 0, 80 | "type": "number" 81 | }, 82 | "enum": { 83 | "minItems": 1, 84 | "type": "array", 85 | "uniqueItems": true 86 | }, 87 | "exclusiveMaximum": { 88 | "default": false, 89 | "type": "boolean" 90 | }, 91 | "exclusiveMinimum": { 92 | "default": false, 93 | "type": "boolean" 94 | }, 95 | "extends": { 96 | "default": {}, 97 | "items": { 98 | "$ref": "#" 99 | }, 100 | "type": [ 101 | { 102 | "$ref": "#" 103 | }, 104 | "array" 105 | ] 106 | }, 107 | "format": { 108 | "type": "string" 109 | }, 110 | "id": { 111 | "format": "uri", 112 | "type": "string" 113 | }, 114 | "items": { 115 | "default": {}, 116 | "items": { 117 | "$ref": "#" 118 | }, 119 | "type": [ 120 | { 121 | "$ref": "#" 122 | }, 123 | "array" 124 | ] 125 | }, 126 | "maxDecimal": { 127 | "minimum": 0, 128 | "type": "number" 129 | }, 130 | "maxItems": { 131 | "minimum": 0, 132 | "type": "integer" 133 | }, 134 | "maxLength": { 135 | "type": "integer" 136 | }, 137 | "maximum": { 138 | "type": "number" 139 | }, 140 | "minItems": { 141 | "default": 0, 142 | "minimum": 0, 143 | "type": "integer" 144 | }, 145 | "minLength": { 146 | "default": 0, 147 | "minimum": 0, 148 | "type": "integer" 149 | }, 150 | "minimum": { 151 | "type": "number" 152 | }, 153 | "pattern": { 154 | "format": "regex", 155 | "type": "string" 156 | }, 157 | "patternProperties": { 158 | "additionalProperties": { 159 | "$ref": "#" 160 | }, 161 | "default": {}, 162 | "type": "object" 163 | }, 164 | "properties": { 165 | "additionalProperties": { 166 | "$ref": "#", 167 | "type": "object" 168 | }, 169 | "default": {}, 170 | "type": "object" 171 | }, 172 | "required": { 173 | "default": false, 174 | "type": "boolean" 175 | }, 176 | "title": { 177 | "type": "string" 178 | }, 179 | "type": { 180 | "default": "any", 181 | "items": { 182 | "type": [ 183 | "string", 184 | { 185 | "$ref": "#" 186 | } 187 | ] 188 | }, 189 | "type": [ 190 | "string", 191 | "array" 192 | ], 193 | "uniqueItems": true 194 | }, 195 | "uniqueItems": { 196 | "default": false, 197 | "type": "boolean" 198 | } 199 | }, 200 | "type": "object" 201 | } 202 | -------------------------------------------------------------------------------- /hearthbreaker/cards/minions/paladin.py: -------------------------------------------------------------------------------- 1 | from hearthbreaker.cards.base import MinionCard, WeaponCard 2 | from hearthbreaker.game_objects import Weapon, Minion 3 | from hearthbreaker.tags.action import Equip, Give, Heal, Damage, GiveAura 4 | from hearthbreaker.tags.base import Deathrattle, Battlecry, Effect, Buff, ActionTag, AuraUntil 5 | from hearthbreaker.tags.selector import PlayerSelector, MinionSelector, SelfSelector, EnemyPlayer, HeroSelector, \ 6 | BothPlayer, CardSelector 7 | from hearthbreaker.constants import CHARACTER_CLASS, CARD_RARITY, MINION_TYPE 8 | from hearthbreaker.tags.status import SetAttack, DivineShield, ChangeHealth, ChangeAttack, ManaChange 9 | from hearthbreaker.tags.condition import IsType, HasCardName, MinionHasDeathrattle 10 | from hearthbreaker.tags.event import MinionSummoned, MinionDied, CardPlayed 11 | 12 | 13 | class AldorPeacekeeper(MinionCard): 14 | def __init__(self): 15 | super().__init__("Aldor Peacekeeper", 3, CHARACTER_CLASS.PALADIN, CARD_RARITY.RARE, 16 | battlecry=Battlecry(Give(SetAttack(1)), MinionSelector(condition=None, players=EnemyPlayer()))) 17 | 18 | def create_minion(self, player): 19 | return Minion(3, 3) 20 | 21 | 22 | class ArgentProtector(MinionCard): 23 | def __init__(self): 24 | super().__init__("Argent Protector", 2, CHARACTER_CLASS.PALADIN, CARD_RARITY.COMMON, 25 | battlecry=Battlecry(Give(DivineShield()), MinionSelector())) 26 | 27 | def create_minion(self, player): 28 | return Minion(2, 2) 29 | 30 | 31 | class DefenderMinion(MinionCard): 32 | def __init__(self): 33 | super().__init__("Defender", 1, CHARACTER_CLASS.PALADIN, CARD_RARITY.COMMON) 34 | 35 | def create_minion(self, p): 36 | return Minion(2, 1) 37 | 38 | 39 | class GuardianOfKings(MinionCard): 40 | def __init__(self): 41 | super().__init__("Guardian of Kings", 7, CHARACTER_CLASS.PALADIN, CARD_RARITY.COMMON, 42 | battlecry=Battlecry(Heal(6), HeroSelector())) 43 | 44 | def create_minion(self, player): 45 | return Minion(5, 6) 46 | 47 | 48 | class Ashbringer(WeaponCard): 49 | def __init__(self): 50 | super().__init__("Ashbringer", 5, CHARACTER_CLASS.PALADIN, CARD_RARITY.LEGENDARY, False) 51 | 52 | def create_weapon(self, player): 53 | weapon = Weapon(5, 3) 54 | return weapon 55 | 56 | 57 | class TirionFordring(MinionCard): 58 | def __init__(self): 59 | super().__init__("Tirion Fordring", 8, CHARACTER_CLASS.PALADIN, CARD_RARITY.LEGENDARY) 60 | 61 | def create_minion(self, player): 62 | return Minion(6, 6, divine_shield=True, taunt=True, 63 | deathrattle=Deathrattle(Equip(Ashbringer()), PlayerSelector())) 64 | 65 | 66 | class CobaltGuardian(MinionCard): 67 | def __init__(self): 68 | super().__init__("Cobalt Guardian", 5, CHARACTER_CLASS.PALADIN, CARD_RARITY.RARE, minion_type=MINION_TYPE.MECH) 69 | 70 | def create_minion(self, player): 71 | return Minion(6, 3, effects=[Effect(MinionSummoned(IsType(MINION_TYPE.MECH)), ActionTag(Give(DivineShield()), 72 | SelfSelector()))]) 73 | 74 | 75 | class SilverHandRecruit(MinionCard): 76 | def __init__(self): 77 | super().__init__("Silver Hand Recruit", 1, CHARACTER_CLASS.PALADIN, CARD_RARITY.FREE, False) 78 | 79 | def create_minion(self, player): 80 | return Minion(1, 1) 81 | 82 | 83 | class ShieldedMinibot(MinionCard): 84 | def __init__(self): 85 | super().__init__("Shielded Minibot", 2, CHARACTER_CLASS.PALADIN, CARD_RARITY.COMMON, 86 | minion_type=MINION_TYPE.MECH) 87 | 88 | def create_minion(self, player): 89 | return Minion(2, 2, divine_shield=True) 90 | 91 | 92 | class Quartermaster(MinionCard): 93 | def __init__(self): 94 | super().__init__("Quartermaster", 5, CHARACTER_CLASS.PALADIN, CARD_RARITY.EPIC, 95 | battlecry=Battlecry(Give([Buff(ChangeAttack(2)), Buff(ChangeHealth(2))]), 96 | MinionSelector(HasCardName("Silver Hand Recruit")))) 97 | 98 | def create_minion(self, player): 99 | return Minion(2, 5) 100 | 101 | 102 | class ScarletPurifier(MinionCard): 103 | def __init__(self): 104 | super().__init__("Scarlet Purifier", 3, CHARACTER_CLASS.PALADIN, CARD_RARITY.RARE, 105 | battlecry=Battlecry(Damage(2), MinionSelector(MinionHasDeathrattle(), BothPlayer()))) 106 | 107 | def create_minion(self, player): 108 | return Minion(4, 3) 109 | 110 | 111 | class BolvarFordragon(MinionCard): 112 | def __init__(self): 113 | super().__init__("Bolvar Fordragon", 5, CHARACTER_CLASS.PALADIN, CARD_RARITY.LEGENDARY, 114 | effects=[Effect(MinionDied(), ActionTag(Give(ChangeAttack(1)), SelfSelector()))]) 115 | 116 | def create_minion(self, player): 117 | return Minion(1, 7) 118 | 119 | 120 | class DragonConsort(MinionCard): 121 | def __init__(self): 122 | super().__init__("Dragon Consort", 5, CHARACTER_CLASS.PALADIN, CARD_RARITY.RARE, 123 | minion_type=MINION_TYPE.DRAGON, 124 | battlecry=Battlecry(GiveAura([AuraUntil(ManaChange(-3), 125 | CardSelector(condition=IsType(MINION_TYPE.DRAGON)), 126 | CardPlayed(IsType(MINION_TYPE.DRAGON)), False)]), 127 | PlayerSelector())) 128 | 129 | def create_minion(self, player): 130 | return Minion(5, 5) 131 | -------------------------------------------------------------------------------- /jsonschema/_reflect.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: twisted.test.test_reflect -*- 2 | # Copyright (c) Twisted Matrix Laboratories. 3 | # See LICENSE for details. 4 | 5 | """ 6 | Standardized versions of various cool and/or strange things that you can do 7 | with Python's reflection capabilities. 8 | """ 9 | 10 | import sys 11 | 12 | from jsonschema.compat import PY3 13 | 14 | 15 | class _NoModuleFound(Exception): 16 | """ 17 | No module was found because none exists. 18 | """ 19 | 20 | 21 | 22 | class InvalidName(ValueError): 23 | """ 24 | The given name is not a dot-separated list of Python objects. 25 | """ 26 | 27 | 28 | 29 | class ModuleNotFound(InvalidName): 30 | """ 31 | The module associated with the given name doesn't exist and it can't be 32 | imported. 33 | """ 34 | 35 | 36 | 37 | class ObjectNotFound(InvalidName): 38 | """ 39 | The object associated with the given name doesn't exist and it can't be 40 | imported. 41 | """ 42 | 43 | 44 | 45 | if PY3: 46 | def reraise(exception, traceback): 47 | raise exception.with_traceback(traceback) 48 | else: 49 | exec("""def reraise(exception, traceback): 50 | raise exception.__class__, exception, traceback""") 51 | 52 | reraise.__doc__ = """ 53 | Re-raise an exception, with an optional traceback, in a way that is compatible 54 | with both Python 2 and Python 3. 55 | 56 | Note that on Python 3, re-raised exceptions will be mutated, with their 57 | C{__traceback__} attribute being set. 58 | 59 | @param exception: The exception instance. 60 | @param traceback: The traceback to use, or C{None} indicating a new traceback. 61 | """ 62 | 63 | 64 | def _importAndCheckStack(importName): 65 | """ 66 | Import the given name as a module, then walk the stack to determine whether 67 | the failure was the module not existing, or some code in the module (for 68 | example a dependent import) failing. This can be helpful to determine 69 | whether any actual application code was run. For example, to distiguish 70 | administrative error (entering the wrong module name), from programmer 71 | error (writing buggy code in a module that fails to import). 72 | 73 | @param importName: The name of the module to import. 74 | @type importName: C{str} 75 | @raise Exception: if something bad happens. This can be any type of 76 | exception, since nobody knows what loading some arbitrary code might 77 | do. 78 | @raise _NoModuleFound: if no module was found. 79 | """ 80 | try: 81 | return __import__(importName) 82 | except ImportError: 83 | excType, excValue, excTraceback = sys.exc_info() 84 | while excTraceback: 85 | execName = excTraceback.tb_frame.f_globals["__name__"] 86 | # in Python 2 execName is None when an ImportError is encountered, 87 | # where in Python 3 execName is equal to the importName. 88 | if execName is None or execName == importName: 89 | reraise(excValue, excTraceback) 90 | excTraceback = excTraceback.tb_next 91 | raise _NoModuleFound() 92 | 93 | 94 | 95 | def namedAny(name): 96 | """ 97 | Retrieve a Python object by its fully qualified name from the global Python 98 | module namespace. The first part of the name, that describes a module, 99 | will be discovered and imported. Each subsequent part of the name is 100 | treated as the name of an attribute of the object specified by all of the 101 | name which came before it. For example, the fully-qualified name of this 102 | object is 'twisted.python.reflect.namedAny'. 103 | 104 | @type name: L{str} 105 | @param name: The name of the object to return. 106 | 107 | @raise InvalidName: If the name is an empty string, starts or ends with 108 | a '.', or is otherwise syntactically incorrect. 109 | 110 | @raise ModuleNotFound: If the name is syntactically correct but the 111 | module it specifies cannot be imported because it does not appear to 112 | exist. 113 | 114 | @raise ObjectNotFound: If the name is syntactically correct, includes at 115 | least one '.', but the module it specifies cannot be imported because 116 | it does not appear to exist. 117 | 118 | @raise AttributeError: If an attribute of an object along the way cannot be 119 | accessed, or a module along the way is not found. 120 | 121 | @return: the Python object identified by 'name'. 122 | """ 123 | if not name: 124 | raise InvalidName('Empty module name') 125 | 126 | names = name.split('.') 127 | 128 | # if the name starts or ends with a '.' or contains '..', the __import__ 129 | # will raise an 'Empty module name' error. This will provide a better error 130 | # message. 131 | if '' in names: 132 | raise InvalidName( 133 | "name must be a string giving a '.'-separated list of Python " 134 | "identifiers, not %r" % (name,)) 135 | 136 | topLevelPackage = None 137 | moduleNames = names[:] 138 | while not topLevelPackage: 139 | if moduleNames: 140 | trialname = '.'.join(moduleNames) 141 | try: 142 | topLevelPackage = _importAndCheckStack(trialname) 143 | except _NoModuleFound: 144 | moduleNames.pop() 145 | else: 146 | if len(names) == 1: 147 | raise ModuleNotFound("No module named %r" % (name,)) 148 | else: 149 | raise ObjectNotFound('%r does not name an object' % (name,)) 150 | 151 | obj = topLevelPackage 152 | for n in names[1:]: 153 | obj = getattr(obj, n) 154 | 155 | return obj 156 | -------------------------------------------------------------------------------- /jsonschema/_utils.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import json 3 | import pkgutil 4 | import re 5 | 6 | from jsonschema.compat import str_types, MutableMapping, urlsplit 7 | 8 | 9 | class URIDict(MutableMapping): 10 | """ 11 | Dictionary which uses normalized URIs as keys. 12 | 13 | """ 14 | 15 | def normalize(self, uri): 16 | return urlsplit(uri).geturl() 17 | 18 | def __init__(self, *args, **kwargs): 19 | self.store = dict() 20 | self.store.update(*args, **kwargs) 21 | 22 | def __getitem__(self, uri): 23 | return self.store[self.normalize(uri)] 24 | 25 | def __setitem__(self, uri, value): 26 | self.store[self.normalize(uri)] = value 27 | 28 | def __delitem__(self, uri): 29 | del self.store[self.normalize(uri)] 30 | 31 | def __iter__(self): 32 | return iter(self.store) 33 | 34 | def __len__(self): 35 | return len(self.store) 36 | 37 | def __repr__(self): 38 | return repr(self.store) 39 | 40 | 41 | class Unset(object): 42 | """ 43 | An as-of-yet unset attribute or unprovided default parameter. 44 | 45 | """ 46 | 47 | def __repr__(self): 48 | return "" 49 | 50 | 51 | def load_schema(name): 52 | """ 53 | Load a schema from ./schemas/``name``.json and return it. 54 | 55 | """ 56 | 57 | data = pkgutil.get_data('jsonschema', "schemas/{0}.json".format(name)) 58 | return json.loads(data.decode("utf-8")) 59 | 60 | 61 | def indent(string, times=1): 62 | """ 63 | A dumb version of :func:`textwrap.indent` from Python 3.3. 64 | 65 | """ 66 | 67 | return "\n".join(" " * (4 * times) + line for line in string.splitlines()) 68 | 69 | 70 | def format_as_index(indices): 71 | """ 72 | Construct a single string containing indexing operations for the indices. 73 | 74 | For example, [1, 2, "foo"] -> [1][2]["foo"] 75 | 76 | :type indices: sequence 77 | 78 | """ 79 | 80 | if not indices: 81 | return "" 82 | return "[%s]" % "][".join(repr(index) for index in indices) 83 | 84 | 85 | def find_additional_properties(instance, schema): 86 | """ 87 | Return the set of additional properties for the given ``instance``. 88 | 89 | Weeds out properties that should have been validated by ``properties`` and 90 | / or ``patternProperties``. 91 | 92 | Assumes ``instance`` is dict-like already. 93 | 94 | """ 95 | 96 | properties = schema.get("properties", {}) 97 | patterns = "|".join(schema.get("patternProperties", {})) 98 | for property in instance: 99 | if property not in properties: 100 | if patterns and re.search(patterns, property): 101 | continue 102 | yield property 103 | 104 | 105 | def extras_msg(extras): 106 | """ 107 | Create an error message for extra items or properties. 108 | 109 | """ 110 | 111 | if len(extras) == 1: 112 | verb = "was" 113 | else: 114 | verb = "were" 115 | return ", ".join(repr(extra) for extra in extras), verb 116 | 117 | 118 | def types_msg(instance, types): 119 | """ 120 | Create an error message for a failure to match the given types. 121 | 122 | If the ``instance`` is an object and contains a ``name`` property, it will 123 | be considered to be a description of that object and used as its type. 124 | 125 | Otherwise the message is simply the reprs of the given ``types``. 126 | 127 | """ 128 | 129 | reprs = [] 130 | for type in types: 131 | try: 132 | reprs.append(repr(type["name"])) 133 | except Exception: 134 | reprs.append(repr(type)) 135 | return "%r is not of type %s" % (instance, ", ".join(reprs)) 136 | 137 | 138 | def flatten(suitable_for_isinstance): 139 | """ 140 | isinstance() can accept a bunch of really annoying different types: 141 | * a single type 142 | * a tuple of types 143 | * an arbitrary nested tree of tuples 144 | 145 | Return a flattened tuple of the given argument. 146 | 147 | """ 148 | 149 | types = set() 150 | 151 | if not isinstance(suitable_for_isinstance, tuple): 152 | suitable_for_isinstance = (suitable_for_isinstance,) 153 | for thing in suitable_for_isinstance: 154 | if isinstance(thing, tuple): 155 | types.update(flatten(thing)) 156 | else: 157 | types.add(thing) 158 | return tuple(types) 159 | 160 | 161 | def ensure_list(thing): 162 | """ 163 | Wrap ``thing`` in a list if it's a single str. 164 | 165 | Otherwise, return it unchanged. 166 | 167 | """ 168 | 169 | if isinstance(thing, str_types): 170 | return [thing] 171 | return thing 172 | 173 | 174 | def unbool(element, true=object(), false=object()): 175 | """ 176 | A hack to make True and 1 and False and 0 unique for ``uniq``. 177 | 178 | """ 179 | 180 | if element is True: 181 | return true 182 | elif element is False: 183 | return false 184 | return element 185 | 186 | 187 | def uniq(container): 188 | """ 189 | Check if all of a container's elements are unique. 190 | 191 | Successively tries first to rely that the elements are hashable, then 192 | falls back on them being sortable, and finally falls back on brute 193 | force. 194 | 195 | """ 196 | 197 | try: 198 | return len(set(unbool(i) for i in container)) == len(container) 199 | except TypeError: 200 | try: 201 | sort = sorted(unbool(i) for i in container) 202 | sliced = itertools.islice(sort, 1, None) 203 | for i, j in zip(sort, sliced): 204 | if i == j: 205 | return False 206 | except (NotImplementedError, TypeError): 207 | seen = [] 208 | for e in container: 209 | e = unbool(e) 210 | if e in seen: 211 | return False 212 | seen.append(e) 213 | return True 214 | -------------------------------------------------------------------------------- /hearthbreaker/powers.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | 3 | 4 | class Power: 5 | def __init__(self): 6 | self.hero = None 7 | self.used = False 8 | 9 | def can_use(self): 10 | return not self.used and self.hero.player.mana >= 2 11 | 12 | def use(self): 13 | if self.can_use(): 14 | self.hero.player.trigger("used_power") 15 | self.hero.player.mana -= 2 16 | self.used = True 17 | 18 | 19 | class DruidPower(Power): 20 | def use(self): 21 | super().use() 22 | self.hero.change_temp_attack(1) 23 | self.hero.increase_armor(1) 24 | 25 | 26 | class HunterPower(Power): 27 | def use(self): 28 | if self.hero.power_targets_minions: 29 | target = self.hero.find_power_target() 30 | super().use() 31 | target.damage(2 * self.hero.player.spell_multiplier, None) 32 | self.hero.player.game.check_delayed() 33 | else: 34 | super().use() 35 | self.hero.player.game.other_player.hero.damage(2 * self.hero.player.spell_multiplier, None) 36 | 37 | 38 | class MagePower(Power): 39 | def use(self): 40 | target = self.hero.find_power_target() 41 | super().use() 42 | target.damage(1 * self.hero.player.spell_multiplier, None) 43 | self.hero.player.game.check_delayed() 44 | 45 | 46 | class PriestPower(Power): 47 | def use(self): 48 | target = self.hero.find_power_target() 49 | super().use() 50 | if self.hero.player.heal_does_damage: 51 | target.damage(2 * self.hero.player.spell_multiplier, None) 52 | else: 53 | target.heal(2 * self.hero.player.heal_multiplier, None) 54 | 55 | def __str__(self): 56 | return "Lesser Heal" 57 | 58 | 59 | # Special power the priest can obtain via the card Shadowform 60 | class MindSpike(Power): 61 | def use(self): 62 | super().use() 63 | target = self.hero.find_power_target() 64 | target.damage(2 * self.hero.player.spell_multiplier, None) 65 | 66 | def __str__(self): 67 | return "Mind Spike" 68 | 69 | 70 | # Special power the priest can obtain via the card Shadowform 71 | class MindShatter(Power): 72 | def use(self): 73 | super().use() 74 | target = self.hero.find_power_target() 75 | target.damage(3 * self.hero.player.spell_multiplier, None) 76 | 77 | def __str__(self): 78 | return "Mind Shatter" 79 | 80 | 81 | class PaladinPower(Power): 82 | def use(self): 83 | super().use() 84 | from hearthbreaker.cards.minions.paladin import SilverHandRecruit 85 | 86 | recruit_card = SilverHandRecruit() 87 | recruit_card.summon(self.hero.player, self.hero.player.game, len(self.hero.player.minions)) 88 | 89 | 90 | class RoguePower(Power): 91 | def use(self): 92 | super().use() 93 | from hearthbreaker.cards.weapons.rogue import WickedKnife 94 | wicked_knife = WickedKnife() 95 | knife = wicked_knife.create_weapon(self.hero.player) 96 | knife.card = wicked_knife 97 | knife.equip(self.hero.player) 98 | 99 | 100 | class ShamanPower(Power): 101 | def __init__(self): 102 | self.healing_totem = False 103 | self.searing_totem = False 104 | self.stoneclaw_totem = False 105 | self.wrath_of_air_totem = False 106 | 107 | super().__init__() 108 | 109 | def can_use(self): 110 | self.healing_totem = False 111 | self.searing_totem = False 112 | self.stoneclaw_totem = False 113 | self.wrath_of_air_totem = False 114 | 115 | for minion in self.hero.player.minions: 116 | if minion.card.name == "Healing Totem": 117 | self.healing_totem = True 118 | elif minion.card.name == "Searing Totem": 119 | self.searing_totem = True 120 | elif minion.card.name == "Stoneclaw Totem": 121 | self.stoneclaw_totem = True 122 | elif minion.card.name == "Wrath of Air Totem": 123 | self.wrath_of_air_totem = True 124 | 125 | if self.healing_totem and self.searing_totem and self.stoneclaw_totem and self.wrath_of_air_totem: 126 | return False 127 | 128 | return super().can_use() 129 | 130 | def use(self): 131 | super().use() 132 | from hearthbreaker.cards.minions.shaman import HealingTotem, SearingTotem, StoneclawTotem, WrathOfAirTotem 133 | 134 | totems = [] 135 | if not self.healing_totem: 136 | totems.append(HealingTotem()) 137 | if not self.searing_totem: 138 | totems.append(SearingTotem()) 139 | if not self.stoneclaw_totem: 140 | totems.append(StoneclawTotem()) 141 | if not self.wrath_of_air_totem: 142 | totems.append(WrathOfAirTotem()) 143 | 144 | random_totem = self.hero.player.game.random_choice(totems) 145 | random_totem.summon(self.hero.player, self.hero.player.game, len(self.hero.player.minions)) 146 | 147 | 148 | class WarlockPower(Power): 149 | def use(self): 150 | super().use() 151 | self.hero.player.game.current_player.hero.damage(2 * self.hero.player.spell_multiplier, None) 152 | self.hero.player.game.current_player.draw() 153 | 154 | 155 | class JaraxxusPower(Power): 156 | def use(self): 157 | super().use() 158 | from hearthbreaker.cards.minions.warlock import Infernal 159 | 160 | infernal_card = Infernal() 161 | infernal_card.summon(self.hero.player, self.hero.player.game, len(self.hero.player.minions)) 162 | 163 | 164 | class DieInsect(Power): 165 | def use(self): 166 | super().use() 167 | targets = copy(self.hero.player.opponent.minions) 168 | targets.append(self.hero.player.opponent.hero) 169 | target = self.hero.player.game.random_choice(targets) 170 | target.damage(2 * self.hero.player.spell_multiplier, None) 171 | 172 | 173 | class WarriorPower(Power): 174 | def use(self): 175 | super().use() 176 | self.hero.increase_armor(2) 177 | -------------------------------------------------------------------------------- /hearthbreaker/tags/card_source.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from itertools import chain 3 | 4 | from hearthbreaker.tags.base import CardQuery, Player, Condition, Selector 5 | from hearthbreaker.tags.selector import FriendlyPlayer 6 | 7 | 8 | class CardSource(CardQuery, metaclass=abc.ABCMeta): 9 | 10 | def __init__(self, conditions): 11 | self.conditions = conditions 12 | 13 | def get_card(self, target, player, owner): 14 | card_list = self.get_list(target, player, owner) 15 | 16 | def check_condition(condition): 17 | return lambda c: condition.evaluate(target, c) 18 | 19 | for condition in self.conditions: 20 | card_list = filter(check_condition(condition), card_list) 21 | 22 | card_list = [card for card in card_list] 23 | card_len = len(card_list) 24 | if card_len == 1: 25 | return card_list[0] 26 | elif card_len == 0: 27 | return None 28 | else: 29 | return player.game.random_choice(card_list) 30 | 31 | @abc.abstractmethod 32 | def get_list(self, target, player, owner): 33 | pass 34 | 35 | 36 | class HandSource(CardSource): 37 | 38 | def __init__(self, player=FriendlyPlayer(), conditions=[]): 39 | super().__init__(conditions) 40 | self.player = player 41 | 42 | def get_list(self, target, player, owner): 43 | players = self.player.get_players(target) 44 | cards = [] 45 | for player in players: 46 | cards.extend(player.hand) 47 | return cards 48 | 49 | def __to_json__(self): 50 | return { 51 | 'name': 'hand', 52 | 'conditions': self.conditions, 53 | 'player': self.player 54 | } 55 | 56 | @staticmethod 57 | def __from_json__(name, player, conditions): 58 | return HandSource( 59 | Player.from_json(player), 60 | [Condition.from_json(**condition) for condition in conditions] 61 | ) 62 | 63 | 64 | class DeckSource(CardSource): 65 | def __init__(self, player=FriendlyPlayer(), conditions=[]): 66 | super().__init__(conditions) 67 | self.player = player 68 | 69 | def get_list(self, target, player, owner): 70 | players = self.player.get_players(target) 71 | if len(players) == 1: 72 | return filter(lambda c: not c.drawn, players[0].deck.cards) 73 | else: 74 | return filter(lambda c: not c.drawn, chain(players[0].deck.cards, players[1].deck.cards)) 75 | 76 | def __to_json__(self): 77 | return { 78 | 'name': 'deck', 79 | 'conditions': self.conditions, 80 | 'player': self.player 81 | } 82 | 83 | @staticmethod 84 | def __from_json__(name, player, conditions): 85 | return DeckSource( 86 | Player.from_json(player), 87 | [Condition.from_json(**condition) for condition in conditions] 88 | ) 89 | 90 | 91 | class ObjectSource(CardQuery): 92 | def __init__(self, selector): 93 | self.selector = selector 94 | 95 | def get_card(self, target, player, owner): 96 | objects = self.selector.choose_targets(owner, target) 97 | if len(objects) == 1: 98 | return objects[0].card 99 | elif len(objects) == 0: 100 | return None 101 | else: 102 | return player.game.random_choice(objects).card 103 | 104 | def __to_json__(self): 105 | return { 106 | 'name': 'object', 107 | 'selector': self.selector, 108 | } 109 | 110 | @staticmethod 111 | def __from_json__(name, selector): 112 | return ObjectSource(Selector.from_json(**selector)) 113 | 114 | 115 | class SpecificCard(CardQuery): 116 | def __init__(self, card): 117 | self.card = card 118 | 119 | def get_card(self, target, player, owner): 120 | from hearthbreaker.engine import card_lookup 121 | return card_lookup(self.card) 122 | 123 | def __to_json__(self): 124 | return self.card 125 | 126 | @staticmethod 127 | def __from_json__(card): 128 | 129 | return SpecificCard(card) 130 | 131 | 132 | class CardList(CardQuery): 133 | def __init__(self, list): 134 | self.list = list 135 | 136 | def get_card(self, target, player, owner): 137 | return player.game.random_choice(self.list) 138 | 139 | def __to_json__(self): 140 | return [card.name for card in self.list] 141 | 142 | @staticmethod 143 | def __from_json__(cards): 144 | from hearthbreaker.engine import card_lookup 145 | return CardList([card_lookup(card) for card in cards]) 146 | 147 | 148 | class CollectionSource(CardSource): 149 | def __init__(self, conditions): 150 | self.conditions = conditions 151 | 152 | def get_list(self, target, player, owner): 153 | from hearthbreaker.engine import get_cards 154 | return get_cards() 155 | 156 | def __to_json__(self): 157 | return { 158 | 'name': 'collection', 159 | 'conditions': self.conditions, 160 | } 161 | 162 | @staticmethod 163 | def __from_json__(name, conditions): 164 | return CollectionSource([Condition.from_json(**condition) for condition in conditions]) 165 | 166 | 167 | class LastCard(CardQuery): 168 | def __init__(self, player=FriendlyPlayer(),): 169 | super().__init__() 170 | self.player = player 171 | 172 | def get_card(self, target, player, owner): 173 | players = self.player.get_players(target) 174 | return players[0].game.last_card 175 | 176 | def __to_json__(self): 177 | return { 178 | 'name': 'last_card', 179 | 'player': self.player 180 | } 181 | 182 | @staticmethod 183 | def __from_json__(name, player): 184 | return LastCard( 185 | Player.from_json(player), 186 | ) 187 | 188 | 189 | class Same(CardQuery): 190 | def __init__(self): 191 | super().__init__() 192 | 193 | def get_card(self, target, player, owner): 194 | return player.game.selected_card 195 | 196 | def __to_json__(self): 197 | return { 198 | 'name': 'same', 199 | } 200 | 201 | @staticmethod 202 | def __from_json__(name): 203 | return Same() 204 | -------------------------------------------------------------------------------- /replay.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Schema for HearthBreaker Replay Files", 3 | "type": "object", 4 | "properties": { 5 | "header": { 6 | "type": "object", 7 | "properties": { 8 | "decks": { 9 | "type": "array", 10 | "items": { 11 | "type": "object", 12 | "properties": { 13 | "hero": { 14 | "type": "string", 15 | "enum": ["Jaina", "Malfurion", "Rexxar", "Anduin", "Uther", "Gul'dan", "Valeera", "Thrall", "Garrosh"] 16 | }, 17 | "cards": { 18 | "type": "array", 19 | "items": [{"type": "string"}], 20 | "maxItems": 30, 21 | "minItems": 1 22 | } 23 | }, 24 | "required": ["hero", "cards"] 25 | } 26 | }, 27 | "keep": { 28 | "type": "array", 29 | "items": { 30 | "type": "array", 31 | "items": { 32 | "type": "integer", 33 | "maximum": 3, 34 | "minimum": 0 35 | }, 36 | "uniqueItems": true, 37 | "minItems": 0, 38 | "maxItems": 4 39 | }, 40 | "minItems": 2, 41 | "maxItems": 2 42 | }, 43 | "random": { 44 | "type": "array", 45 | "items": { 46 | "type": "integer" 47 | } 48 | 49 | } 50 | }, 51 | "required": ["decks", "keep", "random"] 52 | }, 53 | "moves": { 54 | "type": "array", 55 | "items": { 56 | "oneOf": [ 57 | {"title": "Play", "$ref": "#/definitions/play"}, 58 | {"title": "Attack", "$ref": "#/definitions/attack"}, 59 | {"title": "Power", "$ref": "#/definitions/power"}, 60 | {"title": "Start", "$ref": "#/definitions/start"}, 61 | {"title": "End", "$ref": "#/definitions/end"}, 62 | {"title": "Concede", "$ref": "#/definitions/concede"} 63 | ] 64 | }, 65 | "additionalItems": false 66 | 67 | } 68 | }, 69 | "required": ["header", "moves"], 70 | "definitions": { 71 | "play": { 72 | "title": "Play", 73 | "type": "object", 74 | "properties": { 75 | "name": { 76 | "type": "string", 77 | "pattern": "^play$" 78 | }, 79 | "random": { 80 | "$ref": "#/definitions/random" 81 | }, 82 | "card": { 83 | "type": "object", 84 | "properties": { 85 | "card_index": { 86 | "type": "integer", 87 | "minumum": 0, 88 | "maximum": 9 89 | }, 90 | "option": { 91 | "type": "integer", 92 | "minimum": 0 93 | } 94 | }, 95 | "additionalProperties": false, 96 | "required": ["card_index"] 97 | }, 98 | "index": { 99 | "type": "integer", 100 | "minumum": 0, 101 | "maximum": 6 102 | }, 103 | "target": { 104 | "$ref": "#/definitions/characterRef" 105 | } 106 | }, 107 | "additionalProperties": false, 108 | "required": ["name", "card"] 109 | }, 110 | "attack": { 111 | "title": "Attack", 112 | "type": "object", 113 | "properties": { 114 | "name": { 115 | "type": "string", 116 | "pattern": "^attack$" 117 | }, 118 | "random": { 119 | "$ref": "#/definitions/random" 120 | }, 121 | "character": { 122 | "$ref": "#/definitions/characterRef" 123 | }, 124 | "target": { 125 | "$ref": "#/definitions/characterRef" 126 | } 127 | }, 128 | "additionalProperties": false, 129 | "required": ["name", "character", "target"] 130 | }, 131 | "power": { 132 | "name": "Power", 133 | "type": "object", 134 | "properties": { 135 | "name": { 136 | "type": "string", 137 | "pattern": "^power$" 138 | }, 139 | "random": { 140 | "$ref": "#/definitions/random" 141 | }, 142 | "target": { 143 | "$ref": "#/definitions/characterRef" 144 | } 145 | }, 146 | "additionalProperties": false, 147 | "required": ["name"] 148 | }, 149 | "start": { 150 | "title": "Start", 151 | "type": "object", 152 | "properties": { 153 | "name": { 154 | "type": "string", 155 | "pattern": "^start$" 156 | }, 157 | "random": { 158 | "$ref": "#/definitions/random" 159 | } 160 | }, 161 | "additionalProperties": false, 162 | "required": ["name"] 163 | }, 164 | "end": { 165 | "title": "End", 166 | "type": "object", 167 | "properties": { 168 | "name": { 169 | "type": "string", 170 | "pattern": "^end$" 171 | }, 172 | "random": { 173 | "$ref": "#/definitions/random" 174 | } 175 | }, 176 | "additionalProperties": false, 177 | "required": ["name"] 178 | }, 179 | "concede": { 180 | "title": "Concede", 181 | "type": "object", 182 | "properties": { 183 | "name": { 184 | "type": "string", 185 | "pattern": "^concede$" 186 | }, 187 | "random": { 188 | "$ref": "#/definitions/random" 189 | } 190 | }, 191 | "additionalProperties": false, 192 | "required": ["name"] 193 | }, 194 | "random": { 195 | "type": "array", 196 | "items": { 197 | "oneOf": [ 198 | { 199 | "$ref": "#/definitions/characterRef" 200 | }, 201 | { 202 | "type": "integer" 203 | } 204 | ] 205 | } 206 | }, 207 | "characterRef": { 208 | "title": "Character Reference", 209 | "type": "object", 210 | "properties": { 211 | "player": { 212 | "enum": [ 213 | "p1", 214 | "p2" 215 | ], 216 | "index": { 217 | "type": "integer", 218 | "minimum": 0, 219 | "maximum": 6 220 | } 221 | } 222 | }, 223 | "required": ["player"] 224 | } 225 | 226 | } 227 | } -------------------------------------------------------------------------------- /hearthbreaker/cards/minions/mage.py: -------------------------------------------------------------------------------- 1 | import hearthbreaker.cards 2 | from hearthbreaker.cards.base import MinionCard 3 | from hearthbreaker.constants import CHARACTER_CLASS, CARD_RARITY, MINION_TYPE 4 | from hearthbreaker.game_objects import Minion 5 | from hearthbreaker.tags.action import AddCard, Give, GiveAura, Damage 6 | from hearthbreaker.tags.base import Effect, Aura, Battlecry, AuraUntil, ActionTag 7 | from hearthbreaker.tags.condition import HasSecret, GreaterThan, IsType, Adjacent, IsSecret, IsSpell 8 | from hearthbreaker.tags.event import SpellCast, DidDamage, TurnEnded, CardPlayed, Drawn, CardUsed 9 | from hearthbreaker.tags.selector import SelfSelector, PlayerSelector, TargetSelector, \ 10 | CharacterSelector, EnemyPlayer, RandomPicker, MinionSelector, Count, BothPlayer, CardSelector 11 | from hearthbreaker.tags.status import ChangeAttack, ChangeHealth, Frozen, NoSpellTarget, ManaChange 12 | 13 | 14 | class ManaWyrm(MinionCard): 15 | def __init__(self): 16 | super().__init__("Mana Wyrm", 1, CHARACTER_CLASS.MAGE, CARD_RARITY.COMMON) 17 | 18 | def create_minion(self, player): 19 | return Minion(1, 3, effects=[Effect(SpellCast(), ActionTag(Give(ChangeAttack(1)), SelfSelector()))]) 20 | 21 | 22 | class SorcerersApprentice(MinionCard): 23 | def __init__(self): 24 | super().__init__("Sorcerer's Apprentice", 2, CHARACTER_CLASS.MAGE, CARD_RARITY.COMMON) 25 | 26 | def create_minion(self, player): 27 | return Minion(3, 2, auras=[Aura(ManaChange(-1), CardSelector(condition=IsSpell()))]) 28 | 29 | 30 | class KirinTorMage(MinionCard): 31 | def __init__(self): 32 | super().__init__("Kirin Tor Mage", 3, CHARACTER_CLASS.MAGE, CARD_RARITY.RARE, 33 | battlecry=Battlecry(GiveAura([AuraUntil(ManaChange(-100), CardSelector(condition=IsSecret()), 34 | CardPlayed(IsSecret()))]), PlayerSelector())) 35 | 36 | def create_minion(self, player): 37 | return Minion(4, 3) 38 | 39 | 40 | class EtherealArcanist(MinionCard): 41 | def __init__(self): 42 | super().__init__("Ethereal Arcanist", 4, CHARACTER_CLASS.MAGE, CARD_RARITY.RARE) 43 | 44 | def create_minion(self, player): 45 | return Minion(3, 3, effects=[Effect(TurnEnded(HasSecret()), ActionTag(Give(ChangeAttack(2)), SelfSelector())), 46 | Effect(TurnEnded(HasSecret()), ActionTag(Give(ChangeHealth(2)), SelfSelector()))]) 47 | 48 | 49 | class Sheep(MinionCard): 50 | def __init__(self): 51 | super().__init__("Sheep", 0, CHARACTER_CLASS.ALL, CARD_RARITY.COMMON, False, MINION_TYPE.BEAST) 52 | 53 | def create_minion(self, p): 54 | return Minion(1, 1) 55 | 56 | 57 | class WaterElemental(MinionCard): 58 | def __init__(self): 59 | super().__init__("Water Elemental", 4, CHARACTER_CLASS.MAGE, CARD_RARITY.COMMON) 60 | 61 | def create_minion(self, player): 62 | return Minion(3, 6, effects=[Effect(DidDamage(), ActionTag(Give(Frozen()), TargetSelector()))]) 63 | 64 | 65 | class ArchmageAntonidas(MinionCard): 66 | def __init__(self): 67 | super().__init__("Archmage Antonidas", 7, CHARACTER_CLASS.MAGE, CARD_RARITY.LEGENDARY) 68 | 69 | def create_minion(self, player): 70 | return Minion(5, 7, effects=[Effect(SpellCast(), ActionTag(AddCard(hearthbreaker.cards.Fireball()), 71 | PlayerSelector()))]) 72 | 73 | 74 | class Snowchugger(MinionCard): 75 | def __init__(self): 76 | super().__init__("Snowchugger", 2, CHARACTER_CLASS.MAGE, CARD_RARITY.COMMON, minion_type=MINION_TYPE.MECH) 77 | 78 | def create_minion(self, player): 79 | return Minion(2, 3, effects=[Effect(DidDamage(), ActionTag(Give(Frozen()), TargetSelector()))]) 80 | 81 | 82 | class SpellbenderMinion(MinionCard): 83 | def __init__(self): 84 | super().__init__("Spellbender", 0, CHARACTER_CLASS.MAGE, CARD_RARITY.EPIC, False, 85 | ref_name="Spellbender (minion)") 86 | 87 | def create_minion(self, p): 88 | return Minion(1, 3) 89 | 90 | 91 | class MirrorImageMinion(MinionCard): 92 | def __init__(self): 93 | super().__init__("Mirror Image", 0, CHARACTER_CLASS.MAGE, CARD_RARITY.COMMON, False, 94 | ref_name="Mirror Image (minion)") 95 | 96 | def create_minion(self, p): 97 | return Minion(0, 2, taunt=True) 98 | 99 | 100 | class GoblinBlastmage(MinionCard): 101 | def __init__(self): 102 | super().__init__("Goblin Blastmage", 4, CHARACTER_CLASS.MAGE, CARD_RARITY.RARE, 103 | battlecry=Battlecry(Damage(1), CharacterSelector(None, EnemyPlayer(), RandomPicker(4)), 104 | GreaterThan(Count(MinionSelector(IsType(MINION_TYPE.MECH))), value=0))) 105 | 106 | def create_minion(self, player): 107 | return Minion(5, 4) 108 | 109 | 110 | class SootSpewer(MinionCard): 111 | def __init__(self): 112 | super().__init__("Soot Spewer", 3, CHARACTER_CLASS.MAGE, CARD_RARITY.RARE, minion_type=MINION_TYPE.MECH) 113 | 114 | def create_minion(self, player): 115 | return Minion(3, 3, spell_damage=1) 116 | 117 | 118 | class WeeSpellstopper(MinionCard): 119 | def __init__(self): 120 | super().__init__("Wee Spellstopper", 4, CHARACTER_CLASS.MAGE, CARD_RARITY.EPIC) 121 | 122 | def create_minion(self, player): 123 | return Minion(2, 5, auras=[Aura(NoSpellTarget(), MinionSelector(Adjacent()))]) 124 | 125 | 126 | class FlameLeviathan(MinionCard): 127 | def __init__(self): 128 | super().__init__("Flame Leviathan", 7, CHARACTER_CLASS.MAGE, CARD_RARITY.LEGENDARY, 129 | minion_type=MINION_TYPE.MECH, 130 | effects=[Effect(Drawn(), ActionTag(Damage(2), CharacterSelector(None, BothPlayer())))]) 131 | 132 | def create_minion(self, player): 133 | return Minion(7, 7) 134 | 135 | 136 | class Flamewaker(MinionCard): 137 | def __init__(self): 138 | super().__init__("Flamewaker", 3, CHARACTER_CLASS.MAGE, CARD_RARITY.RARE) 139 | 140 | def create_minion(self, player): 141 | return Minion(2, 4, effects=[Effect(CardUsed(IsSpell()), 142 | ActionTag(Damage(1), 143 | CharacterSelector(None, EnemyPlayer(), RandomPicker(2))))]) 144 | -------------------------------------------------------------------------------- /jsonschema/schemas/draft4.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "default": {}, 4 | "definitions": { 5 | "positiveInteger": { 6 | "minimum": 0, 7 | "type": "integer" 8 | }, 9 | "positiveIntegerDefault0": { 10 | "allOf": [ 11 | { 12 | "$ref": "#/definitions/positiveInteger" 13 | }, 14 | { 15 | "default": 0 16 | } 17 | ] 18 | }, 19 | "schemaArray": { 20 | "items": { 21 | "$ref": "#" 22 | }, 23 | "minItems": 1, 24 | "type": "array" 25 | }, 26 | "simpleTypes": { 27 | "enum": [ 28 | "array", 29 | "boolean", 30 | "integer", 31 | "null", 32 | "number", 33 | "object", 34 | "string" 35 | ] 36 | }, 37 | "stringArray": { 38 | "items": { 39 | "type": "string" 40 | }, 41 | "minItems": 1, 42 | "type": "array", 43 | "uniqueItems": true 44 | } 45 | }, 46 | "dependencies": { 47 | "exclusiveMaximum": [ 48 | "maximum" 49 | ], 50 | "exclusiveMinimum": [ 51 | "minimum" 52 | ] 53 | }, 54 | "description": "Core schema meta-schema", 55 | "id": "http://json-schema.org/draft-04/schema#", 56 | "properties": { 57 | "$schema": { 58 | "format": "uri", 59 | "type": "string" 60 | }, 61 | "additionalItems": { 62 | "anyOf": [ 63 | { 64 | "type": "boolean" 65 | }, 66 | { 67 | "$ref": "#" 68 | } 69 | ], 70 | "default": {} 71 | }, 72 | "additionalProperties": { 73 | "anyOf": [ 74 | { 75 | "type": "boolean" 76 | }, 77 | { 78 | "$ref": "#" 79 | } 80 | ], 81 | "default": {} 82 | }, 83 | "allOf": { 84 | "$ref": "#/definitions/schemaArray" 85 | }, 86 | "anyOf": { 87 | "$ref": "#/definitions/schemaArray" 88 | }, 89 | "default": {}, 90 | "definitions": { 91 | "additionalProperties": { 92 | "$ref": "#" 93 | }, 94 | "default": {}, 95 | "type": "object" 96 | }, 97 | "dependencies": { 98 | "additionalProperties": { 99 | "anyOf": [ 100 | { 101 | "$ref": "#" 102 | }, 103 | { 104 | "$ref": "#/definitions/stringArray" 105 | } 106 | ] 107 | }, 108 | "type": "object" 109 | }, 110 | "description": { 111 | "type": "string" 112 | }, 113 | "enum": { 114 | "minItems": 1, 115 | "type": "array", 116 | "uniqueItems": true 117 | }, 118 | "exclusiveMaximum": { 119 | "default": false, 120 | "type": "boolean" 121 | }, 122 | "exclusiveMinimum": { 123 | "default": false, 124 | "type": "boolean" 125 | }, 126 | "format": { 127 | "type": "string" 128 | }, 129 | "id": { 130 | "format": "uri", 131 | "type": "string" 132 | }, 133 | "items": { 134 | "anyOf": [ 135 | { 136 | "$ref": "#" 137 | }, 138 | { 139 | "$ref": "#/definitions/schemaArray" 140 | } 141 | ], 142 | "default": {} 143 | }, 144 | "maxItems": { 145 | "$ref": "#/definitions/positiveInteger" 146 | }, 147 | "maxLength": { 148 | "$ref": "#/definitions/positiveInteger" 149 | }, 150 | "maxProperties": { 151 | "$ref": "#/definitions/positiveInteger" 152 | }, 153 | "maximum": { 154 | "type": "number" 155 | }, 156 | "minItems": { 157 | "$ref": "#/definitions/positiveIntegerDefault0" 158 | }, 159 | "minLength": { 160 | "$ref": "#/definitions/positiveIntegerDefault0" 161 | }, 162 | "minProperties": { 163 | "$ref": "#/definitions/positiveIntegerDefault0" 164 | }, 165 | "minimum": { 166 | "type": "number" 167 | }, 168 | "multipleOf": { 169 | "exclusiveMinimum": true, 170 | "minimum": 0, 171 | "type": "number" 172 | }, 173 | "not": { 174 | "$ref": "#" 175 | }, 176 | "oneOf": { 177 | "$ref": "#/definitions/schemaArray" 178 | }, 179 | "pattern": { 180 | "format": "regex", 181 | "type": "string" 182 | }, 183 | "patternProperties": { 184 | "additionalProperties": { 185 | "$ref": "#" 186 | }, 187 | "default": {}, 188 | "type": "object" 189 | }, 190 | "properties": { 191 | "additionalProperties": { 192 | "$ref": "#" 193 | }, 194 | "default": {}, 195 | "type": "object" 196 | }, 197 | "required": { 198 | "$ref": "#/definitions/stringArray" 199 | }, 200 | "title": { 201 | "type": "string" 202 | }, 203 | "type": { 204 | "anyOf": [ 205 | { 206 | "$ref": "#/definitions/simpleTypes" 207 | }, 208 | { 209 | "items": { 210 | "$ref": "#/definitions/simpleTypes" 211 | }, 212 | "minItems": 1, 213 | "type": "array", 214 | "uniqueItems": true 215 | } 216 | ] 217 | }, 218 | "uniqueItems": { 219 | "default": false, 220 | "type": "boolean" 221 | } 222 | }, 223 | "type": "object" 224 | } 225 | -------------------------------------------------------------------------------- /hearthbreaker/cards/minions/warrior.py: -------------------------------------------------------------------------------- 1 | from hearthbreaker.cards.base import MinionCard, WeaponCard 2 | from hearthbreaker.cards.spells.warrior import BurrowingMine 3 | from hearthbreaker.game_objects import Weapon, Minion 4 | from hearthbreaker.tags.action import IncreaseArmor, Damage, Give, Equip, AddCard 5 | from hearthbreaker.tags.base import Effect, Battlecry, Buff, Aura, ActionTag 6 | from hearthbreaker.tags.condition import AttackLessThanOrEqualTo, IsMinion, IsType, GreaterThan 7 | from hearthbreaker.tags.event import MinionPlaced, CharacterDamaged, ArmorIncreased, Damaged 8 | from hearthbreaker.tags.selector import BothPlayer, SelfSelector, TargetSelector, HeroSelector, MinionSelector, \ 9 | PlayerSelector, EnemyPlayer, UserPicker, Count, CardSelector 10 | from hearthbreaker.constants import CHARACTER_CLASS, CARD_RARITY, MINION_TYPE 11 | from hearthbreaker.tags.status import ChangeAttack, Charge, ChangeHealth 12 | 13 | 14 | class BattleAxe(WeaponCard): 15 | def __init__(self): 16 | super().__init__("Battle Axe", 1, CHARACTER_CLASS.WARRIOR, CARD_RARITY.COMMON, False) 17 | 18 | def create_weapon(self, player): 19 | return Weapon(2, 2) 20 | 21 | 22 | class ArathiWeaponsmith(MinionCard): 23 | def __init__(self): 24 | super().__init__("Arathi Weaponsmith", 4, CHARACTER_CLASS.WARRIOR, CARD_RARITY.COMMON, 25 | battlecry=Battlecry(Equip(BattleAxe()), PlayerSelector())) 26 | 27 | def create_minion(self, player): 28 | return Minion(3, 3) 29 | 30 | 31 | class Armorsmith(MinionCard): 32 | def __init__(self): 33 | super().__init__("Armorsmith", 2, CHARACTER_CLASS.WARRIOR, CARD_RARITY.RARE) 34 | 35 | def create_minion(self, player): 36 | return Minion(1, 4, effects=[Effect(CharacterDamaged(condition=IsMinion()), ActionTag(IncreaseArmor(), 37 | HeroSelector()))]) 38 | 39 | 40 | class CruelTaskmaster(MinionCard): 41 | def __init__(self): 42 | super().__init__("Cruel Taskmaster", 2, CHARACTER_CLASS.WARRIOR, CARD_RARITY.COMMON, 43 | battlecry=Battlecry([Damage(1), Give(ChangeAttack(2))], MinionSelector(players=BothPlayer(), 44 | picker=UserPicker()))) 45 | 46 | def create_minion(self, player): 47 | return Minion(2, 2) 48 | 49 | 50 | class FrothingBerserker(MinionCard): 51 | def __init__(self): 52 | super().__init__("Frothing Berserker", 3, CHARACTER_CLASS.WARRIOR, CARD_RARITY.RARE) 53 | 54 | def create_minion(self, player): 55 | return Minion(2, 4, effects=[Effect(CharacterDamaged(player=BothPlayer(), 56 | condition=IsMinion()), ActionTag(Give(ChangeAttack(1)), 57 | SelfSelector()))]) 58 | 59 | 60 | class GrommashHellscream(MinionCard): 61 | def __init__(self): 62 | super().__init__("Grommash Hellscream", 8, CHARACTER_CLASS.WARRIOR, CARD_RARITY.LEGENDARY) 63 | 64 | def create_minion(self, player): 65 | return Minion(4, 9, charge=True, enrage=[Aura(ChangeAttack(6), SelfSelector())]) 66 | 67 | 68 | class KorkronElite(MinionCard): 69 | def __init__(self): 70 | super().__init__("Kor'kron Elite", 4, CHARACTER_CLASS.WARRIOR, CARD_RARITY.COMMON) 71 | 72 | def create_minion(self, player): 73 | return Minion(4, 3, charge=True) 74 | 75 | 76 | class WarsongCommander(MinionCard): 77 | def __init__(self): 78 | super().__init__("Warsong Commander", 3, CHARACTER_CLASS.WARRIOR, CARD_RARITY.FREE) 79 | 80 | def create_minion(self, player): 81 | return Minion(2, 3, effects=[Effect(MinionPlaced(AttackLessThanOrEqualTo(3)), 82 | ActionTag(Give(Charge()), TargetSelector()))]) 83 | 84 | 85 | class Warbot(MinionCard): 86 | def __init__(self): 87 | super().__init__("Warbot", 1, CHARACTER_CLASS.WARRIOR, CARD_RARITY.COMMON, minion_type=MINION_TYPE.MECH) 88 | 89 | def create_minion(self, player): 90 | return Minion(1, 3, enrage=[Aura(ChangeAttack(1), SelfSelector())]) 91 | 92 | 93 | class Shieldmaiden(MinionCard): 94 | def __init__(self): 95 | super().__init__("Shieldmaiden", 6, CHARACTER_CLASS.WARRIOR, CARD_RARITY.RARE, 96 | battlecry=Battlecry(IncreaseArmor(5), HeroSelector())) 97 | 98 | def create_minion(self, player): 99 | return Minion(5, 5) 100 | 101 | 102 | class SiegeEngine(MinionCard): 103 | def __init__(self): 104 | super().__init__("Siege Engine", 5, CHARACTER_CLASS.WARRIOR, CARD_RARITY.RARE, minion_type=MINION_TYPE.MECH) 105 | 106 | def create_minion(self, player): 107 | return Minion(5, 5, effects=[Effect(ArmorIncreased(), ActionTag(Give(ChangeAttack(1)), SelfSelector()))]) 108 | 109 | 110 | class IronJuggernaut(MinionCard): 111 | def __init__(self): 112 | super().__init__("Iron Juggernaut", 6, CHARACTER_CLASS.WARRIOR, CARD_RARITY.LEGENDARY, 113 | minion_type=MINION_TYPE.MECH, 114 | battlecry=Battlecry(AddCard(BurrowingMine(), add_to_deck=True), PlayerSelector(EnemyPlayer()))) 115 | 116 | def create_minion(self, player): 117 | return Minion(6, 5) 118 | 119 | 120 | class ScrewjankClunker(MinionCard): 121 | def __init__(self): 122 | super().__init__("Screwjank Clunker", 4, CHARACTER_CLASS.WARRIOR, CARD_RARITY.RARE, 123 | minion_type=MINION_TYPE.MECH, 124 | battlecry=Battlecry(Give([Buff(ChangeHealth(2)), Buff(ChangeAttack(2))]), 125 | MinionSelector(IsType(MINION_TYPE.MECH), picker=UserPicker()))) 126 | 127 | def create_minion(self, player): 128 | return Minion(2, 5) 129 | 130 | 131 | class AxeFlinger(MinionCard): 132 | def __init__(self): 133 | super().__init__("Axe Flinger", 4, CHARACTER_CLASS.WARRIOR, CARD_RARITY.COMMON) 134 | 135 | def create_minion(self, player): 136 | return Minion(2, 5, effects=[Effect(Damaged(), ActionTag(Damage(2), HeroSelector(EnemyPlayer())))]) 137 | 138 | 139 | class AlexstraszasChampion(MinionCard): 140 | def __init__(self): 141 | super().__init__("Alexstrasza's Champion", 2, CHARACTER_CLASS.WARRIOR, CARD_RARITY.RARE, 142 | battlecry=(Battlecry(Give([Buff(ChangeAttack(1)), Buff(Charge())]), SelfSelector(), 143 | GreaterThan(Count(CardSelector(condition=IsType(MINION_TYPE.DRAGON))), 144 | value=0)))) 145 | 146 | def create_minion(self, player): 147 | return Minion(2, 3) 148 | -------------------------------------------------------------------------------- /hearthbreaker/agents/trade/possible_play.py: -------------------------------------------------------------------------------- 1 | from hearthbreaker.agents.trade.util import Util 2 | from functools import reduce 3 | 4 | 5 | class PossiblePlay: 6 | def __init__(self, cards, available_mana): 7 | if len(cards) == 0: 8 | raise Exception("PossiblePlay cards is empty") 9 | 10 | self.cards = cards 11 | self.available_mana = available_mana 12 | 13 | def card_mana(self): 14 | def eff_mana(card): 15 | if card.name == "The Coin": 16 | return -1 17 | else: 18 | return card.mana_cost() 19 | 20 | return reduce(lambda s, c: s + eff_mana(c), self.cards, 0) 21 | 22 | def sorted_mana(self): 23 | return Util.reverse_sorted(map(lambda c: c.mana_cost(), self.cards)) 24 | 25 | def wasted(self): 26 | return self.available_mana - self.card_mana() 27 | 28 | def value(self): 29 | res = self.card_mana() 30 | wasted = self.wasted() 31 | if wasted < 0: 32 | raise Exception("Too Much Mana") 33 | 34 | res += wasted * -100000000000 35 | 36 | factor = 100000000 37 | for card_mana in self.sorted_mana(): 38 | res += card_mana * factor 39 | factor = factor / 10 40 | 41 | if self.has_hero_power() and self.available_mana < 6: 42 | res -= 10000000000000000 43 | 44 | if any(map(lambda c: c.name == "The Coin", self.cards)): 45 | res -= 100 46 | 47 | return res 48 | 49 | def has_hero_power(self): 50 | for card in self.cards: 51 | if card.name == 'Hero Power': 52 | return True 53 | return False 54 | 55 | def first_card(self): 56 | if self.has_hero_power(): 57 | for card in self.cards: 58 | if card.name == 'Hero Power': 59 | return card 60 | raise Exception("bad") 61 | else: 62 | return self.cards[0] 63 | 64 | def __str__(self): 65 | names = [c.name for c in self.cards] 66 | s = str(names) 67 | return "{} {}".format(s, self.value()) 68 | 69 | 70 | class CoinPlays: 71 | def coin(self): 72 | cards = [c for c in filter(lambda c: c.name == 'The Coin', self.cards)] 73 | return cards[0] 74 | 75 | def raw_plays_with_coin(self): 76 | res = [] 77 | if self.has_coin(): 78 | coinPlays = self.after_coin().raw_plays() 79 | 80 | for play in coinPlays: 81 | cards = [self.coin()] + play 82 | res.append(cards) 83 | 84 | return res 85 | 86 | def raw_plays(self): 87 | res = [] 88 | for play in self.raw_plays_without_coin(): 89 | res.append(play) 90 | 91 | for play in self.raw_plays_with_coin(): 92 | res.append(play) 93 | 94 | return res 95 | 96 | def has_coin(self): 97 | return any(map(lambda c: c.name == "The Coin", self.cards)) 98 | 99 | def cards_without_coin(self): 100 | return Util.filter_out_one(self.cards, lambda c: c.name == "The Coin") 101 | 102 | def after_coin(self): 103 | return PossiblePlays(self.cards_without_coin(), self.mana + 1) 104 | 105 | def without_coin(self): 106 | return PossiblePlays(self.cards_without_coin(), self.mana) 107 | 108 | 109 | class HeroPowerCard: 110 | def __init__(self): 111 | self.mana = 2 112 | self.name = "Hero Power" 113 | self.player = None 114 | 115 | def can_use(self, player, game): 116 | return True 117 | 118 | def mana_cost(self): 119 | return 2 120 | 121 | 122 | class PossiblePlays(CoinPlays): 123 | def __init__(self, cards, mana, allow_hero_power=True): 124 | self.cards = cards 125 | self.mana = mana 126 | self.allow_hero_power = allow_hero_power 127 | 128 | def possible_is_pointless_coin(self, possible): 129 | if len(possible) != 1 or possible[0].name != "The Coin": 130 | return False 131 | 132 | cards_playable_after_coin = [card for card in filter(lambda c: c.mana - 1 == self.mana, self.cards)] 133 | return len(cards_playable_after_coin) == 0 134 | 135 | def raw_plays_without_coin(self): 136 | res = [] 137 | 138 | def valid_card(card): 139 | saved_mana = card.player.mana 140 | card.player.mana = self.mana 141 | usable = card.can_use(card.player, card.player.game) 142 | card.player.mana = saved_mana 143 | return usable 144 | 145 | possible = [card for card in 146 | filter(valid_card, self.cards)] 147 | 148 | if self.possible_is_pointless_coin(possible): 149 | possible = [] 150 | 151 | if self.mana >= 2 and self.allow_hero_power: 152 | possible.append(HeroPowerCard()) 153 | 154 | if len(possible) == 0: 155 | return [[]] 156 | 157 | for card in possible: 158 | rest = self.cards[0:99999] 159 | 160 | if card.name == 'Hero Power': 161 | f_plays = PossiblePlays(rest, 162 | self.mana - card.mana_cost(), 163 | allow_hero_power=False).raw_plays() 164 | else: 165 | rest.remove(card) 166 | f_plays = PossiblePlays(rest, 167 | self.mana - card.mana_cost(), 168 | allow_hero_power=self.allow_hero_power).raw_plays() 169 | 170 | for following_play in f_plays: 171 | combined = [card] + following_play 172 | res.append(combined) 173 | 174 | res = Util.uniq_by_sorted(res) 175 | 176 | return res 177 | 178 | def plays_inner(self): 179 | res = [PossiblePlay(raw, self.mana) for raw in self.raw_plays() if len(raw) > 0] 180 | res = sorted(res, key=PossiblePlay.value) 181 | res.reverse() 182 | 183 | return res 184 | 185 | def plays(self): 186 | return self.plays_inner() 187 | 188 | def __str__(self): 189 | res = [] 190 | for play in self.plays(): 191 | res.append(play.__str__()) 192 | return str.join("\n", res) 193 | 194 | 195 | class PlayMixin: 196 | def play_one_card(self, player): 197 | if len(player.minions) == 7: 198 | return 199 | if player.game.game_ended: 200 | return 201 | 202 | allow_hero_power = (not player.hero.power.used) and player.hero.health > 2 203 | plays = PossiblePlays(player.hand, player.mana, allow_hero_power=allow_hero_power).plays() 204 | 205 | if len(plays) > 0: 206 | play = plays[0] 207 | if len(play.cards) == 0: 208 | raise Exception("play has no cards") 209 | 210 | card = play.first_card() 211 | 212 | if card.name == 'Hero Power': 213 | player.hero.power.use() 214 | else: 215 | self.last_card_played = card 216 | player.game.play_card(card) 217 | 218 | return card 219 | 220 | def play_cards(self, player): 221 | card = self.play_one_card(player) 222 | if card: 223 | self.play_cards(player) 224 | -------------------------------------------------------------------------------- /hearthbreaker/cards/minions/priest.py: -------------------------------------------------------------------------------- 1 | from hearthbreaker.cards.base import MinionCard 2 | from hearthbreaker.game_objects import Minion 3 | from hearthbreaker.tags.action import Heal, Draw, Steal, Give, Damage, SwapStats 4 | from hearthbreaker.tags.base import Aura, Deathrattle, Effect, Battlecry, Buff, BuffUntil, ActionTag 5 | from hearthbreaker.tags.condition import IsMinion, AttackLessThanOrEqualTo, IsType, IsDamaged, GreaterThan 6 | from hearthbreaker.tags.event import TurnStarted, CharacterHealed, TurnEnded 7 | from hearthbreaker.tags.selector import PlayerSelector, MinionSelector, CharacterSelector, BothPlayer, \ 8 | EnemyPlayer, UserPicker, RandomPicker, CurrentPlayer, HeroSelector, SelfSelector, Count, CardSelector 9 | from hearthbreaker.constants import CHARACTER_CLASS, CARD_RARITY, MINION_TYPE 10 | from hearthbreaker.tags.status import ChangeHealth, HealAsDamage, AttackEqualsHealth, MultiplySpellDamage, \ 11 | MultiplyHealAmount, ChangeAttack 12 | 13 | 14 | class AuchenaiSoulpriest(MinionCard): 15 | def __init__(self): 16 | super().__init__("Auchenai Soulpriest", 4, CHARACTER_CLASS.PRIEST, CARD_RARITY.RARE) 17 | 18 | def create_minion(self, player): 19 | return Minion(3, 5, auras=[Aura(HealAsDamage(), PlayerSelector())]) 20 | 21 | 22 | class CabalShadowPriest(MinionCard): 23 | def __init__(self): 24 | super().__init__("Cabal Shadow Priest", 6, CHARACTER_CLASS.PRIEST, CARD_RARITY.EPIC, 25 | battlecry=Battlecry(Steal(), 26 | MinionSelector(AttackLessThanOrEqualTo(2), 27 | players=EnemyPlayer(), 28 | picker=UserPicker()))) 29 | 30 | def create_minion(self, player): 31 | return Minion(4, 5) 32 | 33 | 34 | class Lightspawn(MinionCard): 35 | def __init__(self): 36 | super().__init__("Lightspawn", 4, CHARACTER_CLASS.PRIEST, CARD_RARITY.COMMON) 37 | 38 | def create_minion(self, player): 39 | return Minion(0, 5, buffs=[Buff(AttackEqualsHealth())]) 40 | 41 | 42 | class Lightwell(MinionCard): 43 | def __init__(self): 44 | super().__init__("Lightwell", 2, CHARACTER_CLASS.PRIEST, CARD_RARITY.RARE) 45 | 46 | def create_minion(self, player): 47 | return Minion(0, 5, effects=[Effect(TurnStarted(), ActionTag(Heal(3), 48 | CharacterSelector(condition=IsDamaged(), 49 | picker=RandomPicker())))]) 50 | 51 | 52 | class NorthshireCleric(MinionCard): 53 | def __init__(self): 54 | super().__init__("Northshire Cleric", 1, CHARACTER_CLASS.PRIEST, 55 | CARD_RARITY.FREE) 56 | 57 | def create_minion(self, player): 58 | return Minion(1, 3, effects=[Effect(CharacterHealed(condition=IsMinion(), 59 | player=BothPlayer()), ActionTag(Draw(), PlayerSelector()))]) 60 | 61 | 62 | class ProphetVelen(MinionCard): 63 | def __init__(self): 64 | super().__init__("Prophet Velen", 7, CHARACTER_CLASS.PRIEST, CARD_RARITY.LEGENDARY) 65 | 66 | def create_minion(self, player): 67 | return Minion(7, 7, auras=[Aura(MultiplySpellDamage(2), PlayerSelector()), 68 | Aura(MultiplyHealAmount(2), PlayerSelector())]) 69 | 70 | 71 | class TempleEnforcer(MinionCard): 72 | def __init__(self): 73 | super().__init__("Temple Enforcer", 6, CHARACTER_CLASS.PRIEST, CARD_RARITY.COMMON, 74 | battlecry=Battlecry(Give(ChangeHealth(3)), MinionSelector(picker=UserPicker()))) 75 | 76 | def create_minion(self, player): 77 | return Minion(6, 6) 78 | 79 | 80 | class ShadowOfNothing(MinionCard): 81 | def __init__(self): 82 | super().__init__("Shadow of Nothing", 0, CHARACTER_CLASS.PRIEST, CARD_RARITY.EPIC, False) 83 | 84 | def create_minion(self, p): 85 | return Minion(0, 1) 86 | 87 | 88 | class DarkCultist(MinionCard): 89 | def __init__(self): 90 | super().__init__("Dark Cultist", 3, CHARACTER_CLASS.PRIEST, CARD_RARITY.COMMON) 91 | 92 | def create_minion(self, player): 93 | return Minion(3, 4, deathrattle=Deathrattle(Give(ChangeHealth(3)), MinionSelector(picker=RandomPicker()))) 94 | 95 | 96 | class Shrinkmeister(MinionCard): 97 | def __init__(self): 98 | super().__init__("Shrinkmeister", 2, CHARACTER_CLASS.PRIEST, CARD_RARITY.COMMON, 99 | battlecry=Battlecry(Give(BuffUntil(ChangeAttack(-2), TurnEnded(player=CurrentPlayer()))), 100 | MinionSelector(players=BothPlayer(), picker=UserPicker()))) 101 | 102 | def create_minion(self, player): 103 | return Minion(3, 2) 104 | 105 | 106 | class UpgradedRepairBot(MinionCard): 107 | def __init__(self): 108 | super().__init__("Upgraded Repair Bot", 5, CHARACTER_CLASS.PRIEST, CARD_RARITY.RARE, 109 | minion_type=MINION_TYPE.MECH, 110 | battlecry=Battlecry(Give(ChangeHealth(4)), MinionSelector(IsType(MINION_TYPE.MECH), 111 | picker=UserPicker()))) 112 | 113 | def create_minion(self, player): 114 | return Minion(5, 5) 115 | 116 | 117 | class Shadowbomber(MinionCard): 118 | def __init__(self): 119 | super().__init__("Shadowbomber", 1, CHARACTER_CLASS.PRIEST, CARD_RARITY.EPIC, 120 | battlecry=Battlecry(Damage(3), HeroSelector(players=BothPlayer()))) 121 | 122 | def create_minion(self, player): 123 | return Minion(2, 1) 124 | 125 | 126 | class Shadowboxer(MinionCard): 127 | def __init__(self): 128 | super().__init__("Shadowboxer", 2, CHARACTER_CLASS.PRIEST, CARD_RARITY.RARE, minion_type=MINION_TYPE.MECH) 129 | 130 | def create_minion(self, player): 131 | return Minion(2, 3, effects=[Effect(CharacterHealed(player=BothPlayer()), ActionTag(Damage(1), 132 | CharacterSelector(players=EnemyPlayer(), picker=RandomPicker(), 133 | condition=None)))]) 134 | 135 | 136 | class Voljin(MinionCard): 137 | def __init__(self): 138 | super().__init__("Vol'jin", 5, CHARACTER_CLASS.PRIEST, CARD_RARITY.LEGENDARY, 139 | battlecry=Battlecry(SwapStats("health", "health", True), MinionSelector(players=BothPlayer(), 140 | picker=UserPicker()))) 141 | 142 | def create_minion(self, player): 143 | return Minion(6, 2) 144 | 145 | 146 | class TwilightWhelp(MinionCard): 147 | def __init__(self): 148 | super().__init__("Twilight Whelp", 1, CHARACTER_CLASS.PRIEST, CARD_RARITY.COMMON, 149 | minion_type=MINION_TYPE.DRAGON, 150 | battlecry=(Battlecry(Give(Buff(ChangeHealth(2))), SelfSelector(), 151 | GreaterThan(Count(CardSelector(condition=IsType(MINION_TYPE.DRAGON))), 152 | value=0)))) 153 | 154 | def create_minion(self, player): 155 | return Minion(2, 1) 156 | -------------------------------------------------------------------------------- /hearthbreaker/ui/game_printer.py: -------------------------------------------------------------------------------- 1 | from hearthbreaker.constants import CHARACTER_CLASS 2 | import curses 3 | 4 | card_abbreviations = { 5 | 'Mark of the Wild': 'Mrk Wild', 6 | 'Power of the Wild': 'Pow Wild', 7 | 'Wild Growth': 'Wld Grth', 8 | 'Healing Touch': 'Hlng Tch', 9 | 'Mark of Nature': 'Mrk Ntr', 10 | 'Savage Roar': 'Svg Roar', 11 | 'Soul of the Forest': 'Sol Frst', 12 | 'Force of Nature': 'Frce Nat', 13 | 'Keeper of the Grove': 'Kpr Grve', 14 | 'Druid of the Claw': 'Drd Claw', 15 | 'Stonetusk Boar': 'Stntsk Br', 16 | 'Raging Worgen': 'Rgng Wrgn', 17 | } 18 | 19 | 20 | def abbreviate(card_name): 21 | return card_abbreviations.get(card_name, card_name) 22 | 23 | 24 | def game_to_string(game): 25 | pass 26 | 27 | 28 | class GameRender: 29 | def __init__(self, window, game, viewing_player): 30 | if viewing_player is game.players[0]: 31 | self.top_player = game.players[1] 32 | self.bottom_player = game.players[0] 33 | else: 34 | self.top_player = game.players[0] 35 | self.bottom_player = game.players[1] 36 | 37 | curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE) 38 | curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_WHITE) 39 | curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_BLUE) 40 | curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_YELLOW) 41 | 42 | self.top_minion_window = window.derwin(3, 80, 4, 0) 43 | self.bottom_minion_window = window.derwin(3, 80, 8, 0) 44 | self.card_window = window.derwin(5, 80, 16, 0) 45 | self.card_window = window.derwin(5, 80, 16, 0) 46 | self.window = window 47 | self.game = game 48 | self.targets = None 49 | self.selected_target = None 50 | self.selection_index = -1 51 | 52 | def draw_minion(self, minion, window, y, x): 53 | status_array = [] 54 | color = curses.color_pair(0) 55 | if minion.can_attack(): 56 | status_array.append("*") 57 | if not self.targets: 58 | color = curses.color_pair(2) 59 | else: 60 | if not self.targets: 61 | color = curses.color_pair(1) 62 | if "attack" in minion.events: 63 | status_array.append("a") 64 | if "turn_start" in minion.events: 65 | status_array.append("b") 66 | if minion.charge: 67 | status_array.append("c") 68 | if minion.deathrattle is not None: 69 | status_array.append("d") 70 | if minion.enraged: 71 | status_array.append("e") 72 | if minion.frozen: 73 | status_array.append("f") 74 | if minion.immune: 75 | status_array.append("i") 76 | if minion.stealth: 77 | status_array.append("s") 78 | if minion.taunt: 79 | status_array.append("t") 80 | if minion.exhausted and not minion.charge: 81 | status_array.append("z") 82 | 83 | if self.targets: 84 | if minion is self.selected_target: 85 | color = curses.color_pair(4) 86 | elif minion in self.targets: 87 | color = curses.color_pair(3) 88 | 89 | name = abbreviate(minion.card.name)[:9] 90 | status = ''.join(status_array) 91 | power_line = "({0}) ({1})".format(minion.calculate_attack(), minion.health) 92 | window.addstr(y + 0, x, "{0:^9}".format(name), color) 93 | window.addstr(y + 1, x, "{0:^9}".format(power_line), color) 94 | window.addstr(y + 2, x, "{0:^9}".format(status), color) 95 | 96 | def draw_card(self, card, player, window, y, x): 97 | color = curses.color_pair(0) 98 | if card.can_use(player, player.game): 99 | status = "*" 100 | if not self.targets: 101 | color = curses.color_pair(2) 102 | else: 103 | status = " " 104 | if not self.targets: 105 | color = curses.color_pair(1) 106 | if self.targets: 107 | if card is self.selected_target: 108 | color = curses.color_pair(4) 109 | elif card in self.targets: 110 | color = curses.color_pair(3) 111 | 112 | name = card.name[:15] 113 | window.addstr(y + 0, x, " {0:>2} mana ({1}) ".format(card.mana_cost(), status), color) 114 | window.addstr(y + 1, x, "{0:^15}".format(name), color) 115 | 116 | def draw_hero(self, player, window, x, y): 117 | color = curses.color_pair(0) 118 | if self.targets: 119 | if player.hero is self.selected_target: 120 | color = curses.color_pair(4) 121 | elif player.hero in self.targets: 122 | color = curses.color_pair(3) 123 | if player.weapon is not None: 124 | weapon_power = "({0}) ({1})".format(player.weapon.base_attack, player.weapon.durability) 125 | window.addstr(y, x, "{0:^20}".format(player.weapon.card.name)) 126 | window.addstr(y + 1, x, "{0:^20}".format(weapon_power)) 127 | 128 | hero_power = "({0}) ({1}+{4}) -- {2}/{3}".format(player.hero.calculate_attack(), player.hero.health, 129 | player.mana, player.max_mana, player.hero.armor) 130 | window.addstr(y, x + 20, "{0:^20}".format(CHARACTER_CLASS.to_str(player.hero.character_class)), color) 131 | window.addstr(y + 1, x + 20, "{0:^20}".format(hero_power), color) 132 | 133 | window.addstr(y, x + 40, "{0:^20}".format("Hero Power")) 134 | if player.hero.power.can_use(): 135 | window.addstr(y + 1, x + 40, "{0:^20}".format("*")) 136 | 137 | def draw_game(self): 138 | self.window.clear() 139 | self.bottom_minion_window.clear() 140 | self.top_minion_window.clear() 141 | self.card_window.clear() 142 | 143 | def draw_minions(minions, window, main): 144 | l_offset = int((80 - 10 * len(minions)) / 2) 145 | index = 0 146 | for minion in minions: 147 | if main and index == self.selection_index: 148 | window.addstr(2, l_offset + index * 10 - 1, "^") 149 | self.draw_minion(minion, window, 0, l_offset + index * 10) 150 | index += 1 151 | if main and len(minions) == self.selection_index: 152 | window.addstr(2, l_offset + index * 10 - 1, "^") 153 | 154 | def draw_cards(cards, player, window, y): 155 | l_offset = int((80 - 16 * len(cards)) / 2) 156 | index = 0 157 | for card in cards: 158 | self.draw_card(card, player, window, y, l_offset + index * 16) 159 | index += 1 160 | 161 | draw_minions(self.top_player.minions, self.top_minion_window, False) 162 | draw_minions(self.bottom_player.minions, self.bottom_minion_window, True) 163 | 164 | draw_cards(self.bottom_player.hand[:5], self.bottom_player, self.card_window, 0) 165 | draw_cards(self.bottom_player.hand[5:], self.bottom_player, self.card_window, 3) 166 | 167 | self.draw_hero(self.top_player, self.window, 10, 0) 168 | self.draw_hero(self.bottom_player, self.window, 10, 12) 169 | self.window.refresh() 170 | self.bottom_minion_window.refresh() 171 | self.top_minion_window.refresh() 172 | self.card_window.refresh() 173 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/hsgame.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/hsgame.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/hsgame" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/hsgame" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | --------------------------------------------------------------------------------