├── frozenlist ├── __init__.py └── frozen_list.py ├── benchmarks ├── profiles │ └── .gitkeep ├── benchmark_simulation.py └── benchmark_multiprocess.py ├── plackett_luce └── __init__.py ├── hearthstone ├── asyncio │ ├── __init__.py │ └── asyncio_utils.py ├── ladder │ ├── __init__.py │ ├── 8p_ladder.py │ └── 1v1_ladder.py ├── simulator │ ├── __init__.py │ ├── core │ │ ├── __init__.py │ │ ├── hero_graveyard.py │ │ ├── monster_types.py │ │ ├── discover_object.py │ │ ├── spell.py │ │ ├── combat_event_queue.py │ │ ├── card_graveyard.py │ │ ├── adaptations.py │ │ └── hero.py │ ├── host │ │ ├── __init__.py │ │ ├── host.py │ │ ├── cyborg_host.py │ │ ├── round_robin_host.py │ │ └── async_host.py │ ├── agent │ │ ├── __init__.py │ │ └── agent.py │ ├── replay │ │ ├── __init__.py │ │ ├── annotators │ │ │ ├── __init__.py │ │ │ ├── ranking_annotator.py │ │ │ └── final_board_annotator.py │ │ ├── observer.py │ │ └── replay.py │ └── main.py ├── testing │ ├── __init__.py │ └── battlegrounds_test_case.py ├── training │ ├── __init__.py │ ├── aigym │ │ ├── __init__.py │ │ ├── observation.py │ │ └── env.py │ ├── common │ │ └── __init__.py │ ├── pettingzoo │ │ ├── __init__.py │ │ ├── pettingzoo_agent.py │ │ └── hearthstone.py │ ├── pytorch │ │ ├── __init__.py │ │ ├── agents │ │ │ ├── __init__.py │ │ │ └── pytorch_bot.py │ │ ├── worker │ │ │ ├── __init__.py │ │ │ ├── distributed │ │ │ │ ├── __init__.py │ │ │ │ ├── remote_agent.py │ │ │ │ ├── simulation_worker.py │ │ │ │ ├── remote_net.py │ │ │ │ ├── tensorize_batch.py │ │ │ │ └── inference_worker.py │ │ │ ├── single_machine │ │ │ │ ├── __init__.py │ │ │ │ └── single_worker_pool.py │ │ │ └── postprocessing.py │ │ ├── encoding │ │ │ ├── __init__.py │ │ │ └── shared_tensor_pool_encoder.py │ │ ├── networks │ │ │ ├── __init__.py │ │ │ ├── save_load.py │ │ │ ├── feedforward_net.py │ │ │ └── running_norm.py │ │ ├── surveillance.py │ │ ├── optuna_inspect.py │ │ ├── util.py │ │ ├── replay.py │ │ ├── gae.py │ │ ├── replay_buffer.py │ │ └── optuner.py │ └── simple_learning_bots │ │ ├── __init__.py │ │ ├── train_stochastic_priority_bot.py │ │ └── train_simple_policy_bot.py ├── battlebots │ ├── __init__.py │ ├── no_action_bot.py │ ├── random_bot.py │ ├── bot_types.py │ ├── ordering.py │ ├── cheapo_bot.py │ ├── supremacy_bot.py │ ├── get_bot_contestants.py │ ├── saurolisk_bot.py │ ├── CardSpecificHeuristics.py │ ├── priority_bot.py │ ├── hero_bot.py │ ├── early_game_bot.py │ └── stochastic_priority_bot.py ├── text_agent │ ├── __init__.py │ ├── stdio.py │ ├── lighthouse_speech.py │ ├── line_reader.py │ └── play_tcp_game.py └── __init__.py ├── data └── learning │ ├── pytorch │ └── ppo │ │ └── .gitkeep │ └── priority_bot.1.json ├── rust └── pyrite_hearth_battles │ ├── src │ ├── stattype.rs │ ├── monstertypes.rs │ ├── lib.rs │ ├── eventtypes.rs │ ├── monstercards.rs │ ├── monstercard.rs │ ├── combat.rs │ └── warparty.rs │ ├── Cargo.toml │ └── tests │ └── combat_tests.rs ├── mypy.ini ├── doc ├── Tensorboard_Vega_Embed_example.png └── combat_death_algorithm.txt ├── tensorboard_vega_embed ├── tensorboard_vega_embed │ ├── svelte_frontend │ │ ├── public │ │ │ ├── index.js │ │ │ ├── favicon.png │ │ │ ├── bundle.css │ │ │ └── index.html │ │ ├── src │ │ │ ├── main.js │ │ │ ├── App.svelte │ │ │ └── Plot.svelte │ │ ├── package.json │ │ └── webpack.config.js │ ├── metadata.py │ ├── __init__.py │ ├── summary.py │ └── plugin.py ├── .gitignore └── setup.py ├── setup.py ├── .gitignore ├── requirements.txt ├── .github └── workflows │ └── main.yml ├── tests ├── pytorch │ ├── distributed │ │ ├── test_multiprocessing_forkserver.py │ │ ├── test_create_worker_pool.py │ │ ├── test_rpc_rref.py │ │ ├── test_play_game.py │ │ └── test_play_game_cuda.py │ └── test_pytorch.py ├── test_frozen_list.py ├── test_gameplay.py └── test_combat_event_queue.py ├── Readme.md └── proto └── messages.proto /frozenlist/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /benchmarks/profiles/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plackett_luce/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthstone/asyncio/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthstone/ladder/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthstone/simulator/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthstone/testing/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthstone/training/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/learning/pytorch/ppo/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthstone/battlebots/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthstone/simulator/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthstone/simulator/host/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthstone/text_agent/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthstone/training/aigym/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthstone/simulator/agent/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthstone/simulator/replay/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthstone/training/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthstone/training/pettingzoo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/agents/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/worker/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthstone/simulator/replay/annotators/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/encoding/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/networks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthstone/training/simple_learning_bots/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/worker/distributed/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/worker/single_machine/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hearthstone/__init__.py: -------------------------------------------------------------------------------- 1 | from .simulator.core import card_pool 2 | -------------------------------------------------------------------------------- /rust/pyrite_hearth_battles/src/stattype.rs: -------------------------------------------------------------------------------- 1 | pub type Stat = i32; 2 | pub type Tier = i8; -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.7 3 | mypy_path=stubs 4 | cache_dir=/dev/null 5 | 6 | [mypy-tests] 7 | ignore_errors=true -------------------------------------------------------------------------------- /doc/Tensorboard_Vega_Embed_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JDBumgardner/stone_ground_hearth_battles/HEAD/doc/Tensorboard_Vega_Embed_example.png -------------------------------------------------------------------------------- /tensorboard_vega_embed/tensorboard_vega_embed/svelte_frontend/public/index.js: -------------------------------------------------------------------------------- 1 | export async function render() { 2 | window.location = "./static/index.html" 3 | } 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages # type: ignore 2 | 3 | package_name = "hearthstone" 4 | setup( 5 | name=package_name, 6 | packages=find_packages(exclude=('tests',)) 7 | ) 8 | -------------------------------------------------------------------------------- /tensorboard_vega_embed/tensorboard_vega_embed/svelte_frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.svelte'; 2 | 3 | const app = new App({ 4 | target: document.body, 5 | props: { 6 | } 7 | }); 8 | 9 | export default app; -------------------------------------------------------------------------------- /hearthstone/training/pytorch/surveillance.py: -------------------------------------------------------------------------------- 1 | class GlobalStepContext: 2 | def get_global_step(self) -> int: 3 | raise NotImplemented 4 | 5 | def should_plot(self) -> bool: 6 | raise NotImplemented 7 | -------------------------------------------------------------------------------- /tensorboard_vega_embed/tensorboard_vega_embed/svelte_frontend/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JDBumgardner/stone_ground_hearth_battles/HEAD/tensorboard_vega_embed/tensorboard_vega_embed/svelte_frontend/public/favicon.png -------------------------------------------------------------------------------- /hearthstone/training/aigym/observation.py: -------------------------------------------------------------------------------- 1 | class Observation: 2 | """ 3 | A class that stores all the information a player observes at a given time step. 4 | 5 | Note that it contains mostly the same information as the Player class, except: 6 | """ 7 | -------------------------------------------------------------------------------- /tensorboard_vega_embed/tensorboard_vega_embed/svelte_frontend/public/bundle.css: -------------------------------------------------------------------------------- 1 | .stepSlider.svelte-1k9dm0c{width:100%} 2 | main.svelte-1h6otfa{text-align:center;padding:1em;max-width:240px;margin:0 auto}@media(min-width: 640px){main.svelte-1h6otfa{max-width:none}} 3 | -------------------------------------------------------------------------------- /rust/pyrite_hearth_battles/src/monstertypes.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Eq, PartialEq, Debug)] 2 | pub enum MonsterTypes { 3 | Beast, 4 | Dragon, 5 | Demon, 6 | Murloc, 7 | Elemental, 8 | Pirate, 9 | Mech, 10 | Quilboar, 11 | Neutral, 12 | All 13 | } -------------------------------------------------------------------------------- /rust/pyrite_hearth_battles/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod warparty; 2 | pub mod monstercard; 3 | pub mod monstercards; 4 | pub mod combat; 5 | pub mod stattype; 6 | pub mod monstertypes; 7 | pub mod eventtypes; 8 | use monstercard::MonsterCard; 9 | struct _Hand { 10 | cards: Vec, 11 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /hearthstone/venv 2 | *.egg-info/ 3 | /build 4 | /dist 5 | /.idea 6 | /.tox 7 | **/__pycache__/ 8 | index.txt 9 | venv/* 10 | /data/learning/pytorch/tensorboard/* 11 | /data/learning/pytorch/ppo/* 12 | /benchmarks/profiles/* 13 | *~ 14 | /rust/pyrite_hearth_battles/target/* 15 | *.pyc 16 | -------------------------------------------------------------------------------- /hearthstone/simulator/core/hero_graveyard.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from hearthstone.simulator.core.cards import one_minion_per_type, CardLocation 4 | from hearthstone.simulator.core.events import BuyPhaseContext 5 | from hearthstone.simulator.core.hero import Hero 6 | from hearthstone.simulator.core.player import StoreIndex, BoardIndex 7 | -------------------------------------------------------------------------------- /hearthstone/testing/battlegrounds_test_case.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import logging 4 | 5 | from hearthstone.simulator.core import player 6 | 7 | 8 | class BattleGroundsTestCase(unittest.TestCase): 9 | @classmethod 10 | def setUpClass(cls) -> None: 11 | logging.basicConfig(level=logging.DEBUG) 12 | player.TEST_MODE = True -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | torch~=1.7.1 2 | setuptools~=50.3.0 3 | optuna~=2.1.0 4 | trueskill~=0.4.5 5 | matplotlib~=3.3.0 6 | joblib~=0.16.0 7 | pandas~=1.1.2 8 | altair~=4.1.0 9 | six~=1.15.0 10 | Werkzeug~=1.0.1 11 | tensorboard~=2.3.0 12 | torchvision~=0.8.2 13 | numpy~=1.19.4 14 | aioupnp~=0.0.17 15 | pettingzoo 16 | boltons 17 | autoslot 18 | gym==0.18.3 19 | -------------------------------------------------------------------------------- /rust/pyrite_hearth_battles/src/eventtypes.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, rc::Rc}; 2 | 3 | use crate::monstercard::MonsterCard; 4 | 5 | #[derive(Clone, Eq, PartialEq, Debug)] 6 | pub enum EventTypes { 7 | MonsterSummon { 8 | card:Rc> 9 | }, 10 | MonsterDeath { 11 | card:Rc> 12 | } 13 | } -------------------------------------------------------------------------------- /rust/pyrite_hearth_battles/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pyrite_hearth_battles" 3 | version = "0.1.0" 4 | authors = ["Dante Kong "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | 11 | rand = "0.8.3" 12 | log = "0.4.14" 13 | env_logger = "0.9.0" -------------------------------------------------------------------------------- /hearthstone/text_agent/stdio.py: -------------------------------------------------------------------------------- 1 | from hearthstone.text_agent.text_agent import TextAgentProtocol 2 | 3 | 4 | class StdIOTransport(TextAgentProtocol): 5 | """ 6 | Note this agent is blocking, since it uses the same stdin/stdout for all agents. 7 | """ 8 | 9 | async def receive_line(self) -> str: 10 | return input() 11 | 12 | async def send(self, text: str): 13 | print(text, end='') 14 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/optuna_inspect.py: -------------------------------------------------------------------------------- 1 | import optuna 2 | 3 | 4 | def main(): 5 | study = optuna.create_study(storage="sqlite:///../../../data/learning/pytorch/optuna/study.db", 6 | study_name="ppo_study", direction="maximize", load_if_exists=True) 7 | df = study.trials_dataframe() 8 | print(df) 9 | print(study.best_trial) 10 | 11 | 12 | if __name__ == '__main__': 13 | main() 14 | -------------------------------------------------------------------------------- /tensorboard_vega_embed/tensorboard_vega_embed/svelte_frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Svelte app 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /hearthstone/ladder/8p_ladder.py: -------------------------------------------------------------------------------- 1 | from hearthstone.ladder.ladder import all_contestants, run_tournament, load_ratings, save_ratings 2 | 3 | 4 | def main(): 5 | contestants = all_contestants() 6 | standings_path = "../../data/standings/8p.json" 7 | load_ratings(contestants, standings_path) 8 | run_tournament(contestants, 1000, 8) 9 | save_ratings(contestants, standings_path) 10 | 11 | 12 | if __name__ == "__main__": 13 | main() 14 | -------------------------------------------------------------------------------- /hearthstone/simulator/core/monster_types.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | 4 | class MONSTER_TYPES(enum.Enum): 5 | BEAST = 1 6 | MECH = 2 7 | PIRATE = 3 8 | DRAGON = 4 9 | DEMON = 5 10 | MURLOC = 6 11 | ELEMENTAL = 7 12 | QUILBOAR = 8 13 | NEUTRAL = 9 14 | ALL = 10 15 | 16 | @classmethod 17 | def single_types(cls): 18 | return [monster_type for monster_type in cls if monster_type not in (cls.ALL, cls.NEUTRAL)] 19 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: pull_request 3 | jobs: 4 | 5 | lint: 6 | name: tests-unit 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-python@v1 11 | with: 12 | python-version: '3.7' 13 | - run: pip install coverage 14 | - run: pip install -r requirements.txt 15 | - run: pip install -e . 16 | - run: python -m unittest discover -vv tests 17 | -------------------------------------------------------------------------------- /hearthstone/simulator/replay/observer.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from hearthstone.simulator.agent.actions import Action 4 | from hearthstone.simulator.core.tavern import Tavern 5 | 6 | Annotation = Any 7 | 8 | 9 | class Observer: 10 | def name(self) -> str: 11 | pass 12 | 13 | def on_action(self, tavern: 'Tavern', player: str, action: 'Action') -> Annotation: 14 | pass 15 | 16 | def on_game_over(self, tavern: 'Tavern') -> Annotation: 17 | pass 18 | -------------------------------------------------------------------------------- /hearthstone/ladder/1v1_ladder.py: -------------------------------------------------------------------------------- 1 | from hearthstone.ladder.ladder import all_contestants, load_ratings, run_tournament, save_ratings, \ 2 | saved_learningbot_1v1_contestants 3 | 4 | 5 | def main(): 6 | contestants = all_contestants() + saved_learningbot_1v1_contestants() 7 | standings_path = "../../data/standings/1v1.json" 8 | load_ratings(contestants, standings_path) 9 | run_tournament(contestants, 1000, 2) 10 | save_ratings(contestants, standings_path) 11 | 12 | 13 | if __name__ == "__main__": 14 | main() 15 | -------------------------------------------------------------------------------- /hearthstone/simulator/replay/annotators/ranking_annotator.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from hearthstone.simulator.replay.observer import Observer, Annotation 4 | 5 | 6 | class RankingAnnotator(Observer): 7 | """ 8 | This annotator simply records the final ranking at the end of the game. 9 | """ 10 | 11 | def name(self) -> str: 12 | return "RankingAnnotator" 13 | 14 | def on_action(self, tavern: 'Tavern', player: str, action: 'Action') -> Annotation: 15 | return None 16 | 17 | def on_game_over(self, tavern: 'Tavern') -> List[str]: 18 | return list(reversed([name for name, player in tavern.losers])) 19 | -------------------------------------------------------------------------------- /tests/pytorch/distributed/test_multiprocessing_forkserver.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import unittest 3 | 4 | from torch import multiprocessing 5 | 6 | from hearthstone.testing.battlegrounds_test_case import BattleGroundsTestCase 7 | 8 | 9 | def test_fn(rank): 10 | print(f"hello world {rank}") 11 | 12 | 13 | class PytorchDistributedTests(BattleGroundsTestCase): 14 | def test_multiprocessing_forkserver(self): 15 | process_context = multiprocessing.start_processes( 16 | test_fn, 17 | nprocs=4, 18 | start_method="forkserver", 19 | join=False 20 | ) 21 | process_context.join() 22 | 23 | 24 | if __name__ == '__main__': 25 | unittest.main() 26 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/util.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from typing import Tuple 3 | 4 | 5 | def unravel_index( 6 | indices: torch.LongTensor, 7 | shape: Tuple[int, ...], 8 | ) -> torch.LongTensor: 9 | r"""Converts flat indices into unraveled coordinates in a target shape. 10 | 11 | This is a `torch` implementation of `numpy.unravel_index`. 12 | 13 | Args: 14 | indices: A tensor of (flat) indices, (*, N). 15 | shape: The targeted shape, (D,). 16 | 17 | Returns: 18 | The unraveled coordinates, (*, N, D). 19 | """ 20 | 21 | coord = [] 22 | 23 | for dim in reversed(shape): 24 | coord.append(indices % dim) 25 | indices = indices // dim 26 | 27 | coord = torch.stack(coord[::-1], dim=-1) 28 | 29 | return coord 30 | -------------------------------------------------------------------------------- /hearthstone/simulator/replay/annotators/final_board_annotator.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from hearthstone.simulator.agent.actions import Action 4 | from hearthstone.simulator.core.tavern import Tavern 5 | from hearthstone.simulator.replay.observer import Observer, Annotation 6 | 7 | 8 | class FinalBoardAnnotator(Observer): 9 | """ 10 | This annotator records the final boards for all players. 11 | """ 12 | 13 | def name(self) -> str: 14 | return "FinalBoardAnnotator" 15 | 16 | def on_action(self, tavern: 'Tavern', player: str, action: 'Action') -> Annotation: 17 | return None 18 | 19 | def on_game_over(self, tavern: 'Tavern') -> Dict[str, tuple]: 20 | return {name: [str(card) for card in player.in_play] for name, player in tavern.players.items()} 21 | -------------------------------------------------------------------------------- /benchmarks/benchmark_simulation.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import logging 4 | 5 | from hearthstone.asyncio import asyncio_utils 6 | from hearthstone.battlebots.random_bot import RandomBot 7 | from hearthstone.simulator.core.randomizer import DefaultRandomizer 8 | from hearthstone.simulator.host.async_host import AsyncHost 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | async def main(): 14 | hosts = [AsyncHost({f'RandomBot{i}': RandomBot(i + j) for i in range(8)}) 15 | for j in range(25) 16 | ] 17 | for j, host in enumerate(hosts): 18 | host.tavern.randomizer = DefaultRandomizer(j) 19 | tasks = [asyncio_utils.create_task(host.async_play_game(), logger=logger) for host in hosts] 20 | await asyncio.gather(*tasks) 21 | 22 | 23 | if __name__ == '__main__': 24 | loop = asyncio.get_event_loop() 25 | loop.run_until_complete(main()) 26 | -------------------------------------------------------------------------------- /tensorboard_vega_embed/tensorboard_vega_embed/metadata.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google Inc. All Rights Reserved. 2 | # Copyright 2019 The TensorFlow Authors. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # ============================================================================== 16 | """Tensorboard Plugin Metadata.""" 17 | 18 | PLUGIN_NAME = "vega_embedx" 19 | -------------------------------------------------------------------------------- /hearthstone/battlebots/no_action_bot.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from hearthstone.simulator.agent.actions import StandardAction, EndPhaseAction, DiscoverChoiceAction, \ 4 | RearrangeCardsAction, FreezeDecision 5 | from hearthstone.simulator.agent.agent import Agent 6 | 7 | if typing.TYPE_CHECKING: 8 | from hearthstone.simulator.core.player import Player 9 | from hearthstone.simulator.core.player import DiscoverIndex 10 | 11 | 12 | class NoActionBot(Agent): 13 | authors = ["Brian Kelly"] 14 | 15 | async def rearrange_cards(self, player: 'Player') -> RearrangeCardsAction: 16 | return RearrangeCardsAction([]) 17 | 18 | async def buy_phase_action(self, player: 'Player') -> StandardAction: 19 | return EndPhaseAction(FreezeDecision.NO_FREEZE) 20 | 21 | async def discover_choice_action(self, player: 'Player') -> DiscoverChoiceAction: 22 | return DiscoverChoiceAction(DiscoverIndex(0)) 23 | -------------------------------------------------------------------------------- /tests/pytorch/distributed/test_create_worker_pool.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import unittest 3 | 4 | import torch 5 | 6 | from hearthstone.testing.battlegrounds_test_case import BattleGroundsTestCase 7 | from hearthstone.training.pytorch.worker.distributed.worker_pool import DistributedWorkerPool 8 | from hearthstone.training.pytorch.worker.postprocessing import ReplaySink 9 | 10 | logging.basicConfig(level=logging.DEBUG) 11 | 12 | 13 | class PytorchDistributedTests(BattleGroundsTestCase): 14 | 15 | def test_create_worker_pool(self): 16 | p = DistributedWorkerPool(5, 17 | 1, 18 | True, 19 | 1024, 20 | ReplaySink(), 21 | torch.device('cpu'), 22 | ) 23 | p.shutdown() 24 | 25 | 26 | if __name__ == '__main__': 27 | unittest.main() 28 | -------------------------------------------------------------------------------- /hearthstone/text_agent/lighthouse_speech.py: -------------------------------------------------------------------------------- 1 | LIGHTHOUSE_SPEECH = 'Hark Triton, hark! Bellow, bid our father the Sea King rise from the depths full foul in his fury! Black waves teeming with salt foam to smother this young mouth with pungent slime, to choke ye, engorging your organs til’ ye turn blue and bloated with bilge and brine and can scream no more -- only when he, crowned in cockle shells with slitherin’ tentacle tail and steaming beard take up his fell be-finned arm, his coral-tine trident screeches banshee-like in the tempest and plunges right through yer gullet, bursting ye -- a bulging bladder no more, but a blasted bloody film now and nothing for the harpies and the souls of dead sailors to peck and claw and feed upon only to be lapped up and swallowed by the infinite waters of the Dread Emperor himself -- forgotten to any man, to any time, forgotten to any god or devil, forgotten even to the sea, for any stuff for part of Winslow, even any scantling of your soul is Winslow no more, but is now itself the sea!' 2 | -------------------------------------------------------------------------------- /tensorboard_vega_embed/tensorboard_vega_embed/svelte_frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tensorboard-vega-embed", 3 | "version": "1.0.0", 4 | "devDependencies": { 5 | "@tsconfig/svelte": "^1.0.0", 6 | "cross-env": "^5.2.0", 7 | "css-loader": "^2.1.1", 8 | "mini-css-extract-plugin": "^0.6.0", 9 | "serve": "^11.0.0", 10 | "style-loader": "^0.23.1", 11 | "svelte": "^3.29.0", 12 | "svelte-check": "^1.0.55", 13 | "svelte-loader": "2.13.3", 14 | "svelte-preprocess": "^4.4.2", 15 | "tslib": "^2.0.0", 16 | "typescript": "^3.9.3", 17 | "webpack": "^4.44.2", 18 | "webpack-cli": "^3.3.0", 19 | "webpack-dev-server": "^3.3.1" 20 | }, 21 | "dependencies": { 22 | "sirv-cli": "^1.0.0", 23 | "vega": "^5.17.3", 24 | "vega-embed": "^6.12.2", 25 | "vega-lite": "^4.16.8" 26 | }, 27 | "scripts": { 28 | "build": "cross-env NODE_ENV=production webpack", 29 | "build-dev": "cross-env webpack", 30 | "dev": "webpack-dev-server --content-base public", 31 | "validate": "svelte-check" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /doc/combat_death_algorithm.txt: -------------------------------------------------------------------------------- 1 | current implementation: 2 | 3 | IS_ATTACKED/ON_ATTACK events 4 | damage is dealt to attacker and defender(s) 5 | AFTER_ATTACK_DAMAGE event 6 | 7 | while there are any dying minions in play: // health <= 0 but not marked dead yet 8 | 9 | for each minion in play: 10 | mark death if necessary 11 | add to death event queue if necessary 12 | end for 13 | 14 | while the death event queue is not empty: 15 | get the next minion from the queue 16 | broadcast that minion's death event // this can cause damage to other minions, but they won't be added to the death event queue yet 17 | end while // minions are added to the deathrattle queue in handle_event 18 | 19 | while the deathrattle queue is not empty: 20 | get the next minion from the queue 21 | resolve that minion's deathrattle // this can cause damage to other minions, but they won't be added to the death event queue yet 22 | end while 23 | 24 | end while 25 | 26 | AFTER_ATTACK_DEATHRATTLES event 27 | -------------------------------------------------------------------------------- /tensorboard_vega_embed/tensorboard_vega_embed/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google Inc. All Rights Reserved. 2 | # Copyright 2019 The TensorFlow Authors. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # ============================================================================== 16 | """Entry point for the example_basic plugin package. 17 | 18 | Public submodules: 19 | summary: Summary-writing ops. 20 | 21 | Private submodules: 22 | metadata: Global constants and the like. 23 | plugin: TensorBoard backend plugin. 24 | """ 25 | -------------------------------------------------------------------------------- /data/learning/priority_bot.1.json: -------------------------------------------------------------------------------- 1 | { 2 | "MicroMachine": -1113, 3 | "DragonspawnLieutenant": -313, 4 | "VulgarHomunculus": 1117, 5 | "RedWhelp": -1225, 6 | "SelflessHero": -1018, 7 | "DeckSwabbie": -926, 8 | "RockpoolHunter": -362, 9 | "Scallywag": -1038, 10 | "ArcaneCannon": -545, 11 | "MurlocTidehunter": -1233, 12 | "GlyphGuardian": 401, 13 | "WrathWeaver": -1385, 14 | "OldMurkeye": 2883, 15 | "RighteousProtector": -1258, 16 | "Zoobot": 2884, 17 | "Imprisoner": 1660, 18 | "MurlocWarleader": 551, 19 | "KaboomBot": -724, 20 | "KindlyGrandmother": -1124, 21 | "MechaRoo": -1156, 22 | "UnstableGhoul": -582, 23 | "RatPack": -519, 24 | "ScavengingHyena": -911, 25 | "StewardOfTime": 1767, 26 | "RabidSaurolisk": 161, 27 | "MurlocTidecaller": -923, 28 | "FreedealingGambler": 308, 29 | "FiendishServant": -1093, 30 | "MetaltoothLeaper": 691, 31 | "HarvestGolem": -427, 32 | "NathrezimOverseer": 13, 33 | "MonstrousMacaw": -339, 34 | "SpawnOfNzoth": -612, 35 | "PogoHopper": -879, 36 | "AlleyCat": -1219, 37 | "TabbyCat": 0, 38 | "MurlocScout": 0 39 | } -------------------------------------------------------------------------------- /tensorboard_vega_embed/.gitignore: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google Inc. All Rights Reserved. 2 | # Copyright 2019 The TensorFlow Authors. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # ============================================================================== 16 | 17 | # Generated by running `python setup.py develop`. 18 | tensorboard_plugin_example.egg-info/ 19 | 20 | # Generated by running `python setup.py bdist_wheel`. 21 | build/ 22 | dist/ 23 | 24 | # Svelte frontend files 25 | tensorboard_vega_embed/svelte_frontend/node_modules/ 26 | tensorboard_vega_embed/svelte_frontend/public/build/ 27 | -------------------------------------------------------------------------------- /hearthstone/training/pettingzoo/pettingzoo_agent.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from hearthstone.simulator.agent.actions import StandardAction, RearrangeCardsAction, DiscoverChoiceAction 4 | from hearthstone.simulator.agent.agent import Agent 5 | 6 | 7 | class AgentRequestQueue: 8 | """ 9 | A class for passing data back and forth between the PettingZoo API and the PettingZooAgents. 10 | The `requests` queue contains tuples of (player_name, Future) 11 | """ 12 | def __init__(self, maxsize: int = 8): 13 | self.requests: asyncio.Queue = asyncio.Queue(maxsize=maxsize) 14 | 15 | async def request_agent_action(self, player_name: str): 16 | future = asyncio.Future() 17 | self.requests.put_nowait((player_name, future)) 18 | return await future 19 | 20 | 21 | 22 | 23 | class PettingZooAgent(Agent): 24 | def __init__(self, queue: AgentRequestQueue): 25 | self.queue = queue 26 | 27 | async def buy_phase_action(self, player: 'Player') -> StandardAction: 28 | queu 29 | 30 | async def rearrange_cards(self, player: 'Player') -> RearrangeCardsAction: 31 | pass 32 | 33 | async def discover_choice_action(self, player: 'Player') -> DiscoverChoiceAction: 34 | pass 35 | -------------------------------------------------------------------------------- /hearthstone/battlebots/random_bot.py: -------------------------------------------------------------------------------- 1 | import random 2 | import typing 3 | 4 | from hearthstone.simulator.agent.actions import generate_standard_actions, StandardAction, DiscoverChoiceAction, \ 5 | RearrangeCardsAction 6 | from hearthstone.simulator.agent.agent import Agent 7 | 8 | if typing.TYPE_CHECKING: 9 | from hearthstone.simulator.core.player import Player 10 | 11 | 12 | class RandomBot(Agent): 13 | authors = ["Jeremy Salwen"] 14 | 15 | def __init__(self, seed: int): 16 | self.local_random = random.Random(seed) 17 | 18 | async def rearrange_cards(self, player: 'Player') -> RearrangeCardsAction: 19 | permutation = list(range(len(player.in_play))) 20 | self.local_random.shuffle(permutation) 21 | return RearrangeCardsAction(permutation) 22 | 23 | async def buy_phase_action(self, player: 'Player') -> StandardAction: 24 | all_actions = list(generate_standard_actions(player)) 25 | for action in all_actions: 26 | assert action.valid(player) 27 | return self.local_random.choice(all_actions) 28 | 29 | async def discover_choice_action(self, player: 'Player') -> DiscoverChoiceAction: 30 | return DiscoverChoiceAction(self.local_random.choice(range(len(player.discover_queue[0].items)))) 31 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/worker/distributed/remote_agent.py: -------------------------------------------------------------------------------- 1 | from torch.distributed.rpc import RRef 2 | 3 | from hearthstone.simulator.agent import AnnotatingAgent, Annotation, DiscoverChoiceAction, StandardAction, \ 4 | RearrangeCardsAction, HeroChoiceAction 5 | 6 | 7 | class RemoteAgent(AnnotatingAgent): 8 | def __init__(self, remote_agent: RRef): 9 | self.remote_agent = remote_agent 10 | 11 | async def hero_choice_action(self, player: 'Player') -> HeroChoiceAction: 12 | return self.remote_agent.rpc_sync().hero_choice_action(player) 13 | 14 | async def annotated_rearrange_cards(self, player: 'Player') -> (RearrangeCardsAction, Annotation): 15 | return self.remote_agent.rpc_sync().annotated_rearrange_cards(player) 16 | 17 | async def annotated_buy_phase_action(self, player: 'Player') -> (StandardAction, Annotation): 18 | return self.remote_agent.rpc_sync().annotated_buy_phase_action(player) 19 | 20 | async def annotated_discover_choice_action(self, player: 'Player') -> (DiscoverChoiceAction, Annotation): 21 | return self.remote_agent.rpc_sync().annotated_discover_choice_action(player) 22 | 23 | async def game_over(self, player: 'Player', ranking: int) -> Annotation: 24 | return self.remote_agent.rpc_sync().game_over(player, ranking) 25 | -------------------------------------------------------------------------------- /tensorboard_vega_embed/tensorboard_vega_embed/svelte_frontend/webpack.config.js: -------------------------------------------------------------------------------- 1 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 2 | const path = require('path'); 3 | 4 | const mode = process.env.NODE_ENV || 'development'; 5 | const prod = mode === 'production'; 6 | 7 | module.exports = { 8 | entry: { 9 | bundle: ['./src/main.js'] 10 | }, 11 | resolve: { 12 | alias: { 13 | svelte: path.resolve('node_modules', 'svelte') 14 | }, 15 | extensions: ['.mjs', '.js', '.svelte'], 16 | mainFields: ['svelte', 'browser', 'module', 'main'] 17 | }, 18 | output: { 19 | path: __dirname + '/public', 20 | filename: '[name].js', 21 | chunkFilename: '[name].[id].js' 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.svelte$/, 27 | use: { 28 | loader: 'svelte-loader', 29 | options: { 30 | emitCss: true, 31 | hotReload: true 32 | } 33 | } 34 | }, 35 | { 36 | test: /\.css$/, 37 | use: [ 38 | /** 39 | * MiniCssExtractPlugin doesn't support HMR. 40 | * For developing, use 'style-loader' instead. 41 | * */ 42 | prod ? MiniCssExtractPlugin.loader : 'style-loader', 43 | 'css-loader' 44 | ] 45 | } 46 | ] 47 | }, 48 | mode, 49 | plugins: [ 50 | new MiniCssExtractPlugin({ 51 | filename: '[name].css' 52 | }) 53 | ], 54 | devtool: prod ? false: 'source-map' 55 | }; -------------------------------------------------------------------------------- /tensorboard_vega_embed/tensorboard_vega_embed/svelte_frontend/src/App.svelte: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 |
20 | 27 | {#if spec_promises.length == 0} 28 |
No Tags Found for this Run
29 | {/if} 30 | {#each spec_promises as spec_promise} 31 |
Tag {spec_promise.tag}
32 | 33 | {/each} 34 |
35 | 36 | -------------------------------------------------------------------------------- /tensorboard_vega_embed/tensorboard_vega_embed/svelte_frontend/src/Plot.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 |
24 | 25 | 26 | 27 | {#each steps as step} 28 | 29 | {/each} 30 | 31 | {#await promise} 32 |

...waiting

33 | {:then number} 34 |

Chart ready. Step {selectedStep}

35 |
36 | {:catch error} 37 |

{error.message}

38 | {/await} 39 |
40 | 41 | -------------------------------------------------------------------------------- /tensorboard_vega_embed/setup.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google Inc. All Rights Reserved. 2 | # Copyright 2019 The TensorFlow Authors. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # ============================================================================== 16 | 17 | from __future__ import absolute_import 18 | from __future__ import division 19 | from __future__ import print_function 20 | 21 | import setuptools 22 | 23 | setuptools.setup( 24 | name="tensorboard_vega_embed", 25 | version="0.1.0", 26 | description="Tensorboard plugin for embedding vega/vega-lite/altair plots.", 27 | packages=["tensorboard_vega_embed"], 28 | package_data={"tensorboard_vega_embed": ["svelte_frontend/public/**"], }, 29 | entry_points={ 30 | "tensorboard_plugins": [ 31 | "vega_embedx = tensorboard_vega_embed.plugin:VegaEmbedXPlugin", 32 | ], 33 | }, 34 | ) 35 | -------------------------------------------------------------------------------- /hearthstone/battlebots/bot_types.py: -------------------------------------------------------------------------------- 1 | import random 2 | import typing 3 | from typing import List, Callable 4 | 5 | from hearthstone.simulator.agent.actions import StandardAction, DiscoverChoiceAction, RearrangeCardsAction 6 | from hearthstone.simulator.agent.agent import Agent 7 | 8 | if typing.TYPE_CHECKING: 9 | from hearthstone.simulator.core.cards import MonsterCard 10 | from hearthstone.simulator.core.player import Player 11 | 12 | 13 | class PriorityFunctionBot(Agent): 14 | def __init__(self, authors: List[str], priority: Callable[['Player', 'MonsterCard'], float], seed: int): 15 | if not authors: 16 | authors = ["JB", "AS", "ES", "JS", "DVP"] 17 | self.authors = authors 18 | self.priority = priority 19 | self.local_random = random.Random(seed) 20 | 21 | async def discover_choice_action(self, player: 'Player') -> DiscoverChoiceAction: 22 | discover_cards = player.discover_queue[0].items 23 | discover_cards = sorted(discover_cards, key=lambda card: self.priority(player, card), reverse=True) 24 | return DiscoverChoiceAction(player.discover_queue[0].items.index(discover_cards[0])) 25 | 26 | async def rearrange_cards(self, player: 'Player') -> RearrangeCardsAction: 27 | permutation = list(range(len(player.in_play))) 28 | self.local_random.shuffle(permutation) 29 | return RearrangeCardsAction(permutation) 30 | 31 | async def buy_phase_action(self, player: 'Player') -> StandardAction: 32 | pass 33 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/replay.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional, NamedTuple 3 | 4 | import torch 5 | 6 | from hearthstone.simulator.agent.actions import Action 7 | from hearthstone.training.pytorch.encoding.default_encoder import EncodedActionSet 8 | from hearthstone.training.common.state_encoding import State 9 | 10 | 11 | @dataclass 12 | class ActorCriticGameStepInfo: 13 | """ 14 | Extra information attached to ReplaySteps when training Actor Critic models. 15 | 16 | This records the state of the game, the policy and value nets outputs, the action taken, and the reward received. It 17 | also optionally contains information propagated about future returns in gae_info. 18 | """ 19 | state: State 20 | valid_actions: EncodedActionSet 21 | action: Action 22 | action_log_prob: float 23 | value: float 24 | gae_info: Optional['GAEReplayInfo'] 25 | debug: Optional['ActorCriticGamestepDebugInfo'] 26 | 27 | 28 | @dataclass 29 | class ActorCriticGameStepDebugInfo: 30 | """ 31 | Additional data recorded for debugging purposes, but not necessary for training. 32 | """ 33 | component_policy: torch.Tensor 34 | permutation_logits: torch.Tensor 35 | 36 | 37 | class GAEReplayInfo(NamedTuple): 38 | """ 39 | Information calculated based on the entire episode, about future returns, to be attached to ReplaySteps. 40 | """ 41 | is_terminal: bool 42 | reward: float 43 | gae_return: float 44 | retrn: float 45 | -------------------------------------------------------------------------------- /hearthstone/battlebots/ordering.py: -------------------------------------------------------------------------------- 1 | from hearthstone.simulator.agent import RearrangeCardsAction 2 | from hearthstone.simulator.core.card_pool import * 3 | from hearthstone.simulator.core.tavern import Player 4 | 5 | 6 | def rate_position(card: 'MonsterCard') -> float: 7 | if type(card) is MonstrousMacaw: 8 | return 0.0 9 | if type(card) is UnstableGhoul or type(card) is SpawnOfNzoth: 10 | return 1.0 11 | if type(card) is SelflessHero or type(card) is GlyphGuardian or type(card) is DeflectOBot: 12 | return 2.0 13 | if type(card) is OldMurkeye: 14 | return 3.0 15 | if type(card) is InfestedWolf or type(card) is SavannahHighmane or type(card) is SecurityRover: 16 | return 4.5 17 | if type(card) is DragonspawnLieutenant or type(card) is Imprisoner or type(card) is ImpGangBoss or type( 18 | card) is TwilightEmissary: 19 | return 5.0 20 | if type(card) is ScavengingHyena or type(card) is RatPack: 21 | return 6.0 22 | if type(card) is PackLeader or type(card) is MurlocWarleader or type(card) is Khadgar or type( 23 | card) is SouthseaCaptain: 24 | return 6.5 25 | if type(card) is MamaBear or type(card) is SoulJuggler or type(card) is RipsnarlCaptain: 26 | return 7.0 27 | 28 | return 4.0 29 | 30 | 31 | def naive_rearrange_cards(player: 'Player') -> RearrangeCardsAction: 32 | in_play = list(enumerate(player.in_play)) 33 | in_play.sort(key=lambda pair: rate_position(pair[1])) 34 | return RearrangeCardsAction([pair[0] for pair in in_play]) 35 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/gae.py: -------------------------------------------------------------------------------- 1 | from hearthstone.simulator.replay.replay import Replay 2 | from hearthstone.training.pytorch.replay import GAEReplayInfo, ActorCriticGameStepInfo 3 | 4 | 5 | class GAEAnnotator: 6 | def __init__(self, player: str, gamma: float, lam: float): 7 | self.player = player 8 | self.gamma = gamma 9 | self.lam = lam 10 | 11 | def annotate(self, replay: Replay): 12 | reward = (len(replay.players) - 1) / 2.0 - replay.agent_annotations[self.player]['ranking'] 13 | retrn = reward 14 | gae_return = reward 15 | next_value = reward 16 | # We iterate backwards over the actions taken by this player only. For now, we are not learning from nonstandard actions. 17 | reversed_player_steps = reversed([game_step for game_step in replay.steps if 18 | game_step.player == self.player and isinstance(game_step.agent_annotation, 19 | ActorCriticGameStepInfo)]) 20 | for i, game_step in enumerate(reversed_player_steps): 21 | is_terminal = i == 0 22 | game_step.agent_annotation.gae_info = GAEReplayInfo( 23 | is_terminal=is_terminal, 24 | reward=reward if is_terminal else 0, 25 | gae_return=gae_return, 26 | retrn=retrn, 27 | ) 28 | gae_return = next_value + (gae_return - next_value) * self.gamma * self.lam 29 | next_value = self.gamma * game_step.agent_annotation.value 30 | retrn *= self.gamma 31 | -------------------------------------------------------------------------------- /rust/pyrite_hearth_battles/src/monstercards.rs: -------------------------------------------------------------------------------- 1 | use super::stattype::*; 2 | use super::monstertypes::*; 3 | 4 | #[derive(Clone, Eq, PartialEq, Debug)] 5 | pub struct BaseProperties{ 6 | pub attack: Stat, 7 | pub health: Stat, 8 | pub tier: Tier, 9 | pub monstertype: MonsterTypes, 10 | pub pacifist: bool, 11 | pub taunt: bool 12 | } 13 | 14 | impl BaseProperties { 15 | pub fn new(attack: Stat, health: Stat, tier: Tier, monstertype: MonsterTypes) -> BaseProperties { 16 | return BaseProperties { 17 | attack: attack, 18 | health: health, 19 | tier: tier, 20 | monstertype: monstertype, 21 | pacifist: false, 22 | taunt: false 23 | } 24 | } 25 | } 26 | 27 | #[derive(Clone, Copy, Eq, PartialEq, Debug)] 28 | pub enum MonsterName { 29 | AlleyCat, 30 | VulgarHomunculus, 31 | RabidSaurolisk, 32 | ScavengingHyena, 33 | DragonSpawnLieutenant 34 | } 35 | 36 | impl MonsterName { 37 | pub fn get_base_stats(&self) -> BaseProperties { 38 | match self { 39 | MonsterName::AlleyCat => BaseProperties::new(1, 1, 1, MonsterTypes::Beast), 40 | MonsterName::VulgarHomunculus => BaseProperties{taunt: true, ..BaseProperties::new( 2, 4, 1, MonsterTypes::Demon)}, 41 | MonsterName::RabidSaurolisk => BaseProperties::new(3, 2, 1, MonsterTypes::Beast), 42 | MonsterName::ScavengingHyena => BaseProperties::new(2, 2, 1, MonsterTypes::Beast), 43 | MonsterName::DragonSpawnLieutenant => BaseProperties{taunt: true, ..BaseProperties::new(2, 3, 1, MonsterTypes::Dragon)} 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/pytorch/distributed/test_rpc_rref.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from torch import multiprocessing 5 | from torch.distributed import rpc 6 | from torch.distributed.rpc import RRef 7 | 8 | 9 | def rpc_backed_options(): 10 | device_maps = { 11 | 'simulator': {0: 0}, 12 | 'inference': {0: 0} 13 | } 14 | return rpc.TensorPipeRpcBackendOptions(device_maps=device_maps) 15 | 16 | 17 | def run_worker(rank, num_workers: int): 18 | os.environ['MASTER_ADDR'] = 'localhost' 19 | os.environ['MASTER_PORT'] = '29500' 20 | rpc.init_rpc('simulator', rank=rank + 1, world_size=num_workers + 1, rpc_backend_options=rpc_backed_options()) 21 | rpc.shutdown() 22 | 23 | 24 | class ContainsRRef: 25 | def __init__(self, rref): 26 | self.rref = rref 27 | 28 | def foo(self): 29 | pass 30 | 31 | 32 | class PytorchDistributedTests(unittest.TestCase): 33 | 34 | def test_rpc_with_rref(self): 35 | os.environ['MASTER_ADDR'] = 'localhost' 36 | os.environ['MASTER_PORT'] = '29500' 37 | 38 | self.process_context = multiprocessing.start_processes( 39 | run_worker, 40 | args=(1,), 41 | nprocs=1, 42 | start_method="forkserver", 43 | join=False 44 | ) 45 | local_object = {} 46 | rpc.init_rpc('inference', rank=0, world_size=2, rpc_backend_options=rpc_backed_options()) 47 | sim_info = rpc.get_worker_info('simulator') 48 | remote_object = rpc.remote(sim_info, ContainsRRef, args=(RRef(local_object),)) 49 | remote_object.rpc_async().foo() 50 | 51 | 52 | if __name__ == '__main__': 53 | unittest.main() 54 | -------------------------------------------------------------------------------- /tests/test_frozen_list.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from frozenlist.frozen_list import FrozenList 4 | from hearthstone.testing.battlegrounds_test_case import BattleGroundsTestCase 5 | 6 | 7 | class MyTestCase(BattleGroundsTestCase): 8 | def test_creation(self): 9 | l = [1, 2, 3] 10 | f = FrozenList(l) 11 | self.assertEqual(f, l) 12 | 13 | def test_indexing(self): 14 | l = [1, 2, 3] 15 | f = FrozenList(l) 16 | self.assertEqual(f[0], 1) 17 | 18 | def test_slicing(self): 19 | l = [1, 2, 3] 20 | f = FrozenList(l) 21 | self.assertEqual(f[1:2], [2]) 22 | 23 | def test_modify_base(self): 24 | l = [1, 2, 3] 25 | f = FrozenList(l) 26 | self.assertEqual(f[0], 1) 27 | l[0] = 5 28 | self.assertEqual(f[0], 5) 29 | 30 | def test_modify_base_slice(self): 31 | l = [1, 2, 3] 32 | f = FrozenList(l) 33 | self.assertEqual(f[1:2], [2]) 34 | l[1] = 5 35 | self.assertEqual(f[1:2], [5]) 36 | 37 | def test_cant_mutate_slice(self): 38 | l = [1, 2, 3] 39 | f = FrozenList(l) 40 | with self.assertRaises(TypeError) as context: 41 | f[1:2] = [5] 42 | 43 | def test_cant_append(self): 44 | l = [1, 2, 3] 45 | f = FrozenList(l) 46 | with self.assertRaises(AttributeError) as context: 47 | f.append(6) 48 | 49 | def test_cant_extend(self): 50 | l = [1, 2, 3] 51 | f = FrozenList(l) 52 | with self.assertRaises(AttributeError) as context: 53 | f.extend([6, 7]) 54 | 55 | 56 | if __name__ == '__main__': 57 | unittest.main() 58 | -------------------------------------------------------------------------------- /rust/pyrite_hearth_battles/src/monstercard.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::fmt; 3 | 4 | use super::monstercards::*; 5 | use super::eventtypes::*; 6 | use super::monstertypes::*; 7 | 8 | #[derive(Clone, Eq, PartialEq)] 9 | pub struct MonsterCard { 10 | pub card_name: MonsterName, 11 | pub properties: BaseProperties, 12 | } 13 | 14 | impl Debug for MonsterCard { 15 | fn fmt (&self, f:&mut fmt::Formatter<'_>) -> fmt::Result { 16 | write!(f, " stats: {}/{}, tavern tier: {}", self.properties.attack, self.properties.health, self.properties.tier) 17 | } 18 | } 19 | 20 | impl MonsterCard { 21 | pub fn new(card:MonsterName) -> MonsterCard { 22 | return MonsterCard { card_name: card, properties: card.get_base_stats() } 23 | } 24 | pub fn cant_attack(&self) -> bool { 25 | self.properties.pacifist || self.properties.attack <= 0 26 | } 27 | 28 | pub fn event_handler(&mut self, event: &EventTypes) { 29 | match self.card_name { 30 | MonsterName::AlleyCat => { }, 31 | MonsterName::RabidSaurolisk => { }, 32 | MonsterName::ScavengingHyena => { 33 | match event { 34 | EventTypes::MonsterDeath { card } => { 35 | if card.borrow().properties.monstertype == MonsterTypes::Beast { 36 | self.properties.attack += 2; 37 | self.properties.health += 1; 38 | } 39 | }, 40 | _ => { } 41 | } 42 | }, 43 | MonsterName::VulgarHomunculus => { }, 44 | MonsterName::DragonSpawnLieutenant => { } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/networks/save_load.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | import torch 4 | from torch import nn 5 | 6 | from hearthstone.training.pytorch.encoding.default_encoder import DefaultEncoder 7 | from hearthstone.training.pytorch.networks.feedforward_net import HearthstoneFFNet 8 | from hearthstone.training.pytorch.networks.transformer_net import HearthstoneTransformerNet 9 | 10 | 11 | def create_net(hparams: Dict) -> nn.Module: 12 | assert hparams["nn.state_encoder"] == "Default" 13 | if hparams["nn.architecture"] == "feedforward": 14 | return HearthstoneFFNet(DefaultEncoder(), 15 | hparams["nn.hidden_layers"], 16 | hparams.get("nn.hidden_size") or 0, 17 | hparams.get("nn.shared") or False, 18 | hparams.get("nn.activation") or "") 19 | elif hparams["nn.architecture"] == "transformer": 20 | return HearthstoneTransformerNet(DefaultEncoder(), 21 | hparams["nn.hidden_layers"], 22 | hparams.get("nn.hidden_size") or 0, 23 | hparams.get("nn.shared") or False, 24 | hparams.get("nn.activation") or "", 25 | hparams.get("nn.encoding.redundant"), 26 | hparams.get("nn.encoding.normalize"), 27 | hparams.get("nn.encoding.normalize.gamma")) 28 | 29 | 30 | def load_from_saved(path, hparams) -> nn.Module: 31 | net = create_net(hparams) 32 | net.load_state_dict(torch.load(path)) 33 | net.eval() 34 | return net 35 | -------------------------------------------------------------------------------- /hearthstone/simulator/core/discover_object.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import itertools 3 | import typing 4 | from typing import Union, List, Callable 5 | 6 | from hearthstone.simulator.core.cards import MonsterCard 7 | from hearthstone.simulator.core.hero import Hero 8 | from hearthstone.simulator.core.secrets import Secret 9 | from hearthstone.simulator.core.spell import Spell 10 | 11 | if typing.TYPE_CHECKING: 12 | from hearthstone.simulator.core.player import DiscoverIndex, Player 13 | 14 | Discoverable = Union[MonsterCard, Hero, Spell, Secret] 15 | 16 | 17 | class DiscoverType(enum.Enum): 18 | CARD = 0 19 | HERO = 1 20 | SECRET = 2 21 | SPELL = 3 22 | 23 | 24 | class DiscoverObject: 25 | def __init__(self, items: List[Discoverable], discover_function: Callable[[Discoverable], None], 26 | dissolve_leftovers: bool, discover_type: DiscoverType): 27 | self.items = items 28 | self.discover_function = discover_function 29 | self.dissolve_leftovers = dissolve_leftovers 30 | self.discover_type = discover_type 31 | 32 | def select_item(self, index: 'DiscoverIndex', player: 'Player'): 33 | selected = self.items.pop(index) 34 | if isinstance(selected, MonsterCard): 35 | # for Bigglesworth (there is no other scenario where a token will be a discover option) 36 | # When Bigglesworth sells a discovered token, that token is added to the pool 37 | selected.token = False 38 | 39 | self.discover_function(selected) 40 | 41 | if self.dissolve_leftovers: 42 | assert all(isinstance(card, MonsterCard) for card in self.items) 43 | player.tavern.deck.return_cards(itertools.chain.from_iterable([card.dissolve() for card in self.items])) 44 | -------------------------------------------------------------------------------- /hearthstone/text_agent/line_reader.py: -------------------------------------------------------------------------------- 1 | # https://stackoverflow.com/a/53576829/94102 2 | class LineReader: 3 | def __init__(self, stream, max_line_length=16384): 4 | self.stream = stream 5 | self._line_generator = self.generate_lines(max_line_length) 6 | 7 | @staticmethod 8 | def generate_lines(max_line_length): 9 | buf = bytearray() 10 | find_start = 0 11 | while True: 12 | newline_idx = buf.find(b'\n', find_start) 13 | if newline_idx < 0: 14 | # no b'\n' found in buf 15 | if len(buf) > max_line_length: 16 | raise ValueError("line too long") 17 | # next time, start the search where this one left off 18 | find_start = len(buf) 19 | more_data = yield 20 | else: 21 | # b'\n' found in buf so return the line and move up buf 22 | line = buf[:newline_idx + 1] 23 | # Update the buffer in place, to take advantage of bytearray's 24 | # optimized delete-from-beginning feature. 25 | del buf[:newline_idx + 1] 26 | # next time, start the search from the beginning 27 | find_start = 0 28 | more_data = yield line 29 | 30 | if more_data is not None: 31 | buf += bytes(more_data) 32 | 33 | async def readline(self): 34 | line = next(self._line_generator) 35 | while line is None: 36 | more_data = await self.stream.receive_some(1024) 37 | if not more_data: 38 | return b'' # this is the EOF indication expected by my caller 39 | line = self._line_generator.send(more_data) 40 | return line 41 | -------------------------------------------------------------------------------- /hearthstone/simulator/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from hearthstone.battlebots.cheapo_bot import CheapoBot 4 | from hearthstone.battlebots.early_game_bot import EarlyGameBot 5 | from hearthstone.battlebots.priority_functions import PriorityFunctions 6 | from hearthstone.simulator.core.monster_types import MONSTER_TYPES 7 | from hearthstone.simulator.host.async_host import AsyncHost 8 | from hearthstone.text_agent.stdio import StdIOTransport 9 | from hearthstone.text_agent.text_agent import TextAgent 10 | 11 | 12 | def main(): 13 | logging.basicConfig(level=logging.DEBUG) 14 | host = AsyncHost({"dante_kong": TextAgent(StdIOTransport()), 15 | "david_stolfo": CheapoBot(1), 16 | "battlerattler_priority_bot": PriorityFunctions.battlerattler_priority_bot(2, EarlyGameBot), 17 | "priority_saurolisk_buff_bot": PriorityFunctions.priority_saurolisk_buff_bot(3, EarlyGameBot), 18 | "racist_priority_bot_mech": PriorityFunctions.racist_priority_bot(4, EarlyGameBot, 19 | MONSTER_TYPES.MECH), 20 | "racist_priority_bot_murloc": PriorityFunctions.racist_priority_bot(5, EarlyGameBot, 21 | MONSTER_TYPES.MURLOC), 22 | "racist_priority_bot_elemental": PriorityFunctions.racist_priority_bot(6, EarlyGameBot, 23 | MONSTER_TYPES.ELEMENTAL), 24 | "priority_adaptive_tripler_bot": PriorityFunctions.priority_adaptive_tripler_bot(7, EarlyGameBot), 25 | }) 26 | host.play_game() 27 | 28 | 29 | if __name__ == '__main__': 30 | main() 31 | -------------------------------------------------------------------------------- /tests/pytorch/distributed/test_play_game.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import unittest 3 | 4 | import torch 5 | 6 | from hearthstone.ladder.ladder import Contestant, ContestantAgentGenerator 7 | from hearthstone.testing.battlegrounds_test_case import BattleGroundsTestCase 8 | from hearthstone.training.pytorch.agents.pytorch_bot import PytorchBot 9 | from hearthstone.training.pytorch.encoding.default_encoder import DefaultEncoder 10 | from hearthstone.training.pytorch.networks.transformer_net import HearthstoneTransformerNet 11 | from hearthstone.training.pytorch.policy_gradient import easiest_contestants 12 | from hearthstone.training.pytorch.worker.distributed.worker_pool import DistributedWorkerPool 13 | from hearthstone.training.pytorch.worker.postprocessing import ReplaySink 14 | 15 | class PytorchDistributedTests(BattleGroundsTestCase): 16 | 17 | def test_play_game(self): 18 | p = DistributedWorkerPool(5, 19 | 1, 20 | True, 21 | 1024, 22 | ReplaySink(), 23 | torch.device('cpu')) 24 | encoder = DefaultEncoder() 25 | learning_bot_contestant = Contestant( 26 | "LearningBot", 27 | ContestantAgentGenerator(PytorchBot, 28 | net=HearthstoneTransformerNet(encoder), 29 | encoder=encoder, 30 | annotate=True, 31 | device=torch.device('cpu')) 32 | ) 33 | contestants = easiest_contestants() 34 | p.play_games(learning_bot_contestant=learning_bot_contestant, other_contestants=contestants, game_size=8) 35 | p.shutdown() 36 | 37 | 38 | if __name__ == '__main__': 39 | unittest.main() 40 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ## Stone Ground Hearth Battles 2 | 3 | Hearthstone battlegrounds simulator is licensed under the Apache 2.0 License 4 | 5 | This repository includes a simulator along with bots and allows the user to play against the bots. There are various 6 | attempts to use Pytorch to train a bot. 7 | 8 | ### Tensorboard Plugin 9 | 10 | This repo also contains a Tensorboard plugin for displaying vega/vega-lite/altair plots in tensorboard. We use this to 11 | plot debug information about our pytorch bots, but this plugin works standalone. 12 | 13 | ![Example Screenshot of Tensorboard_Vega_Embed plugin](doc/Tensorboard_Vega_Embed_example.png) 14 | 15 | To use it, run 16 | 17 | `$ python setup.py develop` 18 | 19 | from within the `tensorboard_vega_embed/` directory. When you launch tensorboard, it will show up as a new tab labeled " 20 | VEGA_EMBEDX". To uninstall it, run `$ python setup.py develop --uninstall`. 21 | 22 | ### Distributed Training Environment 23 | 24 | This repo also contains a distributed training setup to play several games in parallel using a single GPU, using Pytorch 25 | Distributed and python asyncio. 26 | 27 | ![Architecture Diagram](doc/architecture.svg) 28 | 29 | ### Benchmarks 30 | 31 | Speed of simulation can be important for Reinforcement Learning. Woe is upon us for choosing to write the simulator in 32 | python, thinking that it would not be the bottleneck. CPU is the bottleneck for experience generation, not GPU :( 33 | 34 | Therefore, we have benchmarks to profile the performance of our simulator, and identify bottlenecks. To run one of the 35 | profiles, run, e.g. 36 | 37 | ```shell 38 | $ PYTHONPATH=. python3 -m cProfile -o benchmarks/profiles/simulation.cprof benchmarks/benchmark_simulation.py 39 | ``` 40 | 41 | The resulting profile can be viewed using snakeviz: 42 | 43 | ```shell 44 | $ snakeviz benchmarks/profiles/simulation.cprof 45 | ``` -------------------------------------------------------------------------------- /hearthstone/asyncio/asyncio_utils.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import functools 3 | import logging 4 | from typing import Awaitable, Optional, TypeVar 5 | 6 | 7 | def get_or_create_event_loop(): 8 | try: 9 | return asyncio.get_event_loop() 10 | except RuntimeError as e: 11 | asyncio.set_event_loop(asyncio.new_event_loop()) 12 | return asyncio.get_event_loop() 13 | 14 | # From https://quantlane.com/blog/ensure-asyncio-task-exceptions-get-logged/ 15 | T = TypeVar('T') 16 | 17 | def create_task( 18 | coroutine: Awaitable[T], 19 | *, 20 | logger: logging.Logger, 21 | loop: Optional[asyncio.AbstractEventLoop] = None, 22 | ) -> 'asyncio.Task[T]': # This type annotation has to be quoted for Python < 3.9, see https://www.python.org/dev/peps/pep-0585/ 23 | ''' 24 | This helper function wraps a ``loop.create_task(coroutine())`` call and ensures there is 25 | an exception handler added to the resulting task. If the task raises an exception it is logged 26 | using the provided ``logger``. 27 | ''' 28 | if loop is None: 29 | loop = asyncio.get_running_loop() 30 | task = loop.create_task(coroutine) 31 | task.add_done_callback( 32 | functools.partial(_handle_task_result, logger=logger) 33 | ) 34 | return task 35 | 36 | 37 | def _handle_task_result( 38 | task: asyncio.Task, 39 | *, 40 | logger: logging.Logger, 41 | ) -> None: 42 | try: 43 | task.result() 44 | except asyncio.CancelledError: 45 | pass # Task cancellation should not be logged as an error. 46 | # Ad the pylint ignore: we want to handle all exceptions here so that the result of the task 47 | # is properly logged. There is no point re-raising the exception in this callback. 48 | except Exception: # pylint: disable=broad-except 49 | logger.exception("Exception in asyncio task:") 50 | -------------------------------------------------------------------------------- /tests/pytorch/distributed/test_play_game_cuda.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import unittest 3 | 4 | import torch 5 | 6 | from hearthstone.ladder.ladder import Contestant, ContestantAgentGenerator 7 | from hearthstone.testing.battlegrounds_test_case import BattleGroundsTestCase 8 | from hearthstone.training.pytorch.agents.pytorch_bot import PytorchBot 9 | from hearthstone.training.pytorch.encoding.default_encoder import DefaultEncoder 10 | from hearthstone.training.pytorch.networks.transformer_net import HearthstoneTransformerNet 11 | from hearthstone.training.pytorch.policy_gradient import easiest_contestants 12 | from hearthstone.training.pytorch.worker.distributed.worker_pool import DistributedWorkerPool 13 | from hearthstone.training.pytorch.worker.postprocessing import ReplaySink 14 | 15 | 16 | 17 | class PytorchDistributedTests(BattleGroundsTestCase): 18 | def test_play_game_cuda(self): 19 | device = torch.device('cuda') 20 | p = DistributedWorkerPool(6, 21 | 10, 22 | True, 23 | 1024, 24 | ReplaySink(), 25 | device) 26 | encoder = DefaultEncoder() 27 | learning_bot_contestant = Contestant( 28 | "LearningBot", 29 | ContestantAgentGenerator(PytorchBot, 30 | net=HearthstoneTransformerNet(encoder), 31 | encoder=encoder, 32 | annotate=True, 33 | device=torch.device('cpu')) 34 | ) 35 | contestants = easiest_contestants() 36 | p.play_games(learning_bot_contestant=learning_bot_contestant, other_contestants=contestants, game_size=8) 37 | p.shutdown() 38 | 39 | 40 | if __name__ == '__main__': 41 | unittest.main() 42 | -------------------------------------------------------------------------------- /hearthstone/battlebots/cheapo_bot.py: -------------------------------------------------------------------------------- 1 | import random 2 | import typing 3 | 4 | from hearthstone.simulator.agent.actions import StandardAction, generate_standard_actions, BuyAction, EndPhaseAction, \ 5 | SummonAction, DiscoverChoiceAction, RearrangeCardsAction, FreezeDecision 6 | from hearthstone.simulator.agent.agent import Agent 7 | from hearthstone.simulator.core.discover_object import DiscoverType 8 | 9 | from hearthstone.simulator.core.player import Player, DiscoverIndex 10 | 11 | 12 | class CheapoBot(Agent): 13 | authors = ["Brian Kelly"] 14 | 15 | def __init__(self, seed: int): 16 | self.local_random = random.Random(seed) 17 | 18 | async def rearrange_cards(self, player: 'Player') -> RearrangeCardsAction: 19 | permutation = list(range(len(player.in_play))) 20 | self.local_random.shuffle(permutation) 21 | return RearrangeCardsAction(permutation) 22 | 23 | async def buy_phase_action(self, player: 'Player') -> StandardAction: 24 | all_actions = list(generate_standard_actions(player)) 25 | 26 | summon_actions = [action for action in all_actions if type(action) is SummonAction] 27 | if summon_actions: 28 | return summon_actions[0] 29 | 30 | buy_actions = [action for action in all_actions if type(action) is BuyAction] 31 | buy_actions = sorted(buy_actions, key=lambda buy_action: player.store[buy_action.index].tier) 32 | if buy_actions: 33 | return buy_actions[0] 34 | 35 | return EndPhaseAction(FreezeDecision.NO_FREEZE) 36 | 37 | async def discover_choice_action(self, player: 'Player') -> DiscoverChoiceAction: 38 | discover_object = player.discover_queue[0] 39 | if discover_object.discover_type == DiscoverType.CARD: 40 | discover_object.items.sort(key=lambda card: card.tier) 41 | return DiscoverChoiceAction(DiscoverIndex(discover_object.items.index(discover_object.items[0]))) 42 | -------------------------------------------------------------------------------- /hearthstone/text_agent/play_tcp_game.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import sys 4 | from typing import Dict 5 | 6 | from hearthstone.battlebots.early_game_bot import EarlyGameBot 7 | from hearthstone.battlebots.priority_functions import PriorityFunctions 8 | from hearthstone.simulator.agent import Agent 9 | from hearthstone.simulator.host.cyborg_host import CyborgArena 10 | from hearthstone.text_agent.tcp import GameServer 11 | from hearthstone.text_agent.text_agent import TextAgent 12 | 13 | 14 | async def open_client_streams(max_players: int) -> Dict[str, Agent]: 15 | kill_event = asyncio.Event() 16 | game_server = GameServer(max_players, kill_event) 17 | game_server.serve_forever() 18 | await game_server.wait_for_ready() 19 | return { 20 | name: TextAgent(protocol) 21 | for name, protocol in game_server.protocols.items() 22 | } 23 | 24 | 25 | def main(): 26 | argv = sys.argv 27 | MAX_PLAYERS = 8 28 | if len(argv) > 1: 29 | MAX_PLAYERS = int(argv[1]) 30 | 31 | logging.basicConfig(level=logging.DEBUG) 32 | agents = { 33 | "battlerattler_priority_bot": PriorityFunctions.battlerattler_priority_bot(1, EarlyGameBot), 34 | "battlerattler_priority_bot2": PriorityFunctions.battlerattler_priority_bot(3, EarlyGameBot), 35 | "battlerattler_priority_bot3": PriorityFunctions.battlerattler_priority_bot(4, EarlyGameBot), 36 | "battlerattler_priority_bot4": PriorityFunctions.battlerattler_priority_bot(5, EarlyGameBot), 37 | "battlerattler_priority_bot5": PriorityFunctions.battlerattler_priority_bot(6, EarlyGameBot), 38 | "battlerattler_priority_bot6": PriorityFunctions.battlerattler_priority_bot(7, EarlyGameBot), 39 | } 40 | agents.update( 41 | asyncio_utils.get_or_create_event_loop().run_until_complete(open_client_streams(MAX_PLAYERS - len(agents)))) 42 | host = CyborgArena(agents) 43 | host.play_game() 44 | 45 | 46 | if __name__ == '__main__': 47 | main() 48 | -------------------------------------------------------------------------------- /proto/messages.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package pb; 4 | 5 | 6 | // 7 | //# full player object state 8 | //# other player health, tavern turn count, other player heros, most popular card type for each player, on win/lose streak 9 | //# notifications: golden 10 | //# combat 11 | 12 | 13 | enum CardType { 14 | ThneedThing = 0; 15 | } 16 | 17 | enum MonsterType { 18 | Beast = 0; 19 | } 20 | 21 | enum HeroType { 22 | Foo2 = 0; 23 | } 24 | 25 | message Hero { 26 | HeroType type = 1; 27 | bool active_power = 2; 28 | bool used_this_turn = 3; 29 | } 30 | 31 | message OtherPlayerView { 32 | HeroType hero_type = 1; 33 | uint32 health = 2; 34 | MonsterType most_common_minion = 3; 35 | uint32 most_common_count = 4; 36 | uint32 tavern_tier = 5; 37 | bool streak = 6; 38 | bool is_next_opponent = 7; 39 | string name = 8; 40 | } 41 | 42 | message Card { 43 | uint32 tier = 1; 44 | uint32 attack = 2; 45 | uint32 health = 3; 46 | bool golden = 4; 47 | bool taunt = 5; 48 | bool divine_shield = 6; 49 | bool poisonous = 7; 50 | bool magnetic = 8; 51 | bool windfury = 9; 52 | bool reborn = 10; 53 | bool deathrattle = 11; 54 | bool battlecry = 12; 55 | CardType card_type = 13; 56 | MonsterType monster_type = 14; 57 | } 58 | 59 | message Player { 60 | uint32 health = 1; 61 | uint32 turn_count = 2; 62 | uint32 coins = 3; 63 | uint32 tavern_tier = 4; 64 | Hero hero = 5; 65 | repeated Card hand = 6; 66 | repeated Card in_play = 7; 67 | repeated Card store = 8; 68 | string name = 9; 69 | } 70 | 71 | message SingleCombat { 72 | uint32 self = 1; 73 | uint32 opponent = 2; 74 | PostCombatBoard post_combat = 3; 75 | } 76 | 77 | message PostCombatBoard { 78 | repeated Card self = 1; 79 | repeated Card opponent = 2; 80 | } 81 | 82 | message CombatResult { 83 | string opponent_name = 1; 84 | repeated Card opponent_warparty = 2; 85 | repeated SingleCombat combat_events = 3; 86 | } 87 | -------------------------------------------------------------------------------- /hearthstone/training/pettingzoo/hearthstone.py: -------------------------------------------------------------------------------- 1 | from gym.spaces import Discrete 2 | from pettingzoo import AECEnv 3 | from pettingzoo.utils import wrappers 4 | 5 | from hearthstone.simulator.host.async_host import AsyncHost 6 | 7 | 8 | def env(): 9 | env = raw_env() 10 | env = wrappers.CaptureStdoutWrapper(env) 11 | env = wrappers.AssertOutOfBoundsWrapper(env) 12 | env = wrappers.OrderEnforcingWrapper(env) 13 | return env 14 | 15 | 16 | class raw_env(AECEnv): 17 | metadata = {'render.modes': ['human'], "name": "sghb"} 18 | 19 | def __init__(self, n_players=8): 20 | self.possible_agents = ["player_" + str(r) for r in range(n_players)] 21 | self.agent_name_mapping = dict(zip(self.possible_agents, list(range(len(self.possible_agents))))) 22 | self.action_spaces = {agent: Discrete(3) for agent in self.possible_agents} 23 | self.observation_spaces = {agent: Discrete(4) for agent in self.possible_agents} 24 | 25 | def render(self, mode="human"): 26 | print("Rendered, noob") 27 | 28 | def close(self): 29 | pass 30 | 31 | def reset(self): 32 | ''' 33 | Reset needs to initialize the following attributes 34 | - agents 35 | - rewards 36 | - _cumulative_rewards 37 | - dones 38 | - infos 39 | - agent_selection 40 | And must set up the environment so that render(), step(), and observe() 41 | can be called without issues. 42 | 43 | Here it sets up the state dictionary which is used by step() and the observations dictionary which is used by step() and observe() 44 | ''' 45 | self.agents = self.possible_agents[:] 46 | self.rewards = {agent: 0 for agent in self.agents} 47 | self._cumulative_rewards = {agent: 0 for agent in self.agents} 48 | self.dones = {agent: False for agent in self.agents} 49 | self.infos = {agent: {} for agent in self.agents} 50 | 51 | self.host = AsyncHost() 52 | self.agent_selection = self._agent_selector.next() 53 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/replay_buffer.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import random 3 | from queue import Queue 4 | from typing import List, Generator, Union 5 | 6 | from hearthstone.simulator.replay.replay import Replay 7 | from hearthstone.training.pytorch.replay import ActorCriticGameStepInfo 8 | 9 | 10 | class EpochBuffer: 11 | """ 12 | A replay buffer for a2c or ppo, containing an unordered list of transitions. 13 | 14 | """ 15 | 16 | def __init__(self, bot_name: str): 17 | """ 18 | :param bot_name: The name of the agent that this buffer is collecting samples for. Only this bots actions will be added to the replay buffer. 19 | :param observation_normalizer: Observation normalizer to use for computing rolling average observation normalization. 20 | """ 21 | self.bot_name = bot_name 22 | self.transitions: List[ActorCriticGameStepInfo] = [] 23 | 24 | def __len__(self): 25 | return len(self.transitions) 26 | 27 | def clear(self): 28 | self.transitions.clear() 29 | 30 | def recycle(self, queue: Union[Queue, collections.deque]): 31 | for transition in self.transitions: 32 | if isinstance(queue, collections.deque): 33 | queue.append((transition.state, transition.valid_actions)) 34 | else: 35 | queue.put_nowait((transition.state, transition.valid_actions)) 36 | self.clear() 37 | 38 | def add_replay(self, replay: Replay): 39 | for replay_step in replay.steps: 40 | if replay_step.player == self.bot_name and replay_step.agent_annotation: 41 | bot_info = replay_step.agent_annotation 42 | self.transitions.append(bot_info) 43 | 44 | def sample_minibatches(self, batch_size: int) -> Generator[List[ActorCriticGameStepInfo], None, None]: 45 | random.shuffle(self.transitions) 46 | i = 0 47 | while True: 48 | batch = self.transitions[i:i + batch_size] 49 | if len(batch) < batch_size: 50 | break 51 | yield batch 52 | i += batch_size 53 | -------------------------------------------------------------------------------- /tensorboard_vega_embed/tensorboard_vega_embed/summary.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google Inc. All Rights Reserved. 2 | # Copyright 2019 The TensorFlow Authors. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # ============================================================================== 16 | 17 | from __future__ import absolute_import 18 | from __future__ import division 19 | from __future__ import print_function 20 | 21 | from tensorboard.compat.proto import summary_pb2 22 | from tensorboard.compat.proto.summary_pb2 import Summary 23 | from tensorboard.compat.proto.tensor_pb2 import TensorProto 24 | from tensorboard.compat.proto.tensor_shape_pb2 import TensorShapeProto 25 | from tensorboard_vega_embed import metadata 26 | from torch.utils.tensorboard import SummaryWriter 27 | 28 | 29 | def vega_embed(writer: SummaryWriter, tag, vega_json, step=None, description=None): 30 | smd = _create_summary_metadata(description) 31 | tensor = TensorProto(dtype='DT_STRING', 32 | string_val=[vega_json.encode(encoding='utf_8')], 33 | tensor_shape=TensorShapeProto(dim=[TensorShapeProto.Dim(size=1)])) 34 | summary = Summary(value=[Summary.Value(tag=tag, metadata=smd, tensor=tensor)]) 35 | writer._get_file_writer().add_summary(summary, step) 36 | 37 | 38 | def _create_summary_metadata(description): 39 | return summary_pb2.SummaryMetadata( 40 | summary_description=description, 41 | plugin_data=summary_pb2.SummaryMetadata.PluginData( 42 | plugin_name=metadata.PLUGIN_NAME, 43 | content=b"", 44 | ), 45 | ) 46 | -------------------------------------------------------------------------------- /hearthstone/training/simple_learning_bots/train_stochastic_priority_bot.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | 4 | from hearthstone.battlebots.stochastic_priority_bot import LearnedPriorityBot 5 | from hearthstone.ladder.ladder import Contestant, update_ratings, print_standings, load_ratings, save_ratings, \ 6 | all_contestants 7 | from hearthstone.simulator.host import RoundRobinHost 8 | 9 | 10 | def main(): 11 | logging.getLogger().setLevel(logging.INFO) 12 | other_contestants = all_contestants() 13 | learning_bot = LearnedPriorityBot(None, 0.05, 10) 14 | learning_bot_contestant = Contestant("LearningBot", learning_bot) 15 | contestants = other_contestants + [learning_bot_contestant] 16 | bot_file = "../../../data/learning/priority_bot.1.json" 17 | standings_path = "../../../data/learning/standings.json" 18 | learning_bot.read_from_file(bot_file) 19 | load_ratings(contestants, standings_path) 20 | 21 | for _ in range(1000): 22 | round_contestants = [learning_bot_contestant] + random.sample(other_contestants, k=7) 23 | host = RoundRobinHost({contestant.name: contestant.agent_generator() for contestant in round_contestants}) 24 | host.play_game() 25 | winner_names = list(reversed([name for name, player in host.tavern.losers])) 26 | print("---------------------------------------------------------------") 27 | print(winner_names) 28 | # print(host.tavern.losers[-1][1].in_play) 29 | ranked_contestants = sorted(round_contestants, key=lambda c: winner_names.index(c.name)) 30 | update_ratings(ranked_contestants) 31 | print_standings(contestants) 32 | for contestant in round_contestants: 33 | contestant.games_played += 1 34 | if learning_bot_contestant in round_contestants: 35 | learning_bot.learn_from_game(ranked_contestants.index(learning_bot_contestant)) 36 | print("Favorite cards: ", 37 | sorted(learning_bot.priority_dict.items(), key=lambda item: item[1], reverse=True)) 38 | learning_bot.save_to_file(bot_file) 39 | 40 | save_ratings(contestants, standings_path) 41 | 42 | 43 | if __name__ == '__main__': 44 | main() 45 | -------------------------------------------------------------------------------- /hearthstone/simulator/core/spell.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from typing import Optional, List 3 | 4 | from hearthstone.simulator.core.cards import CardLocation 5 | 6 | if typing.TYPE_CHECKING: 7 | from hearthstone.simulator.core.events import BuyPhaseContext 8 | from hearthstone.simulator.core.player import BoardIndex, StoreIndex 9 | 10 | 11 | class Spell: 12 | base_cost: int = 0 13 | target_location: List['CardLocation'] = [] 14 | darkmoon_prize_tier: int = 0 15 | 16 | def __init__(self, tier: Optional[int] = None): 17 | self.cost = self.base_cost 18 | self.tier = tier 19 | 20 | def __repr__(self): 21 | rep = f"{type(self).__name__}" 22 | rep += f"({self.cost})" 23 | if self.tier is not None: 24 | rep += f", [tier {self.tier}]" 25 | return "{" + rep + "}" 26 | 27 | def valid(self, context: 'BuyPhaseContext', board_index: Optional['BoardIndex'] = None, 28 | store_index: Optional['StoreIndex'] = None) -> bool: 29 | if not self.target_location: 30 | return (board_index is None) and (store_index is None) 31 | if board_index is None and store_index is None: 32 | return False 33 | if board_index is not None: 34 | if CardLocation.BOARD not in self.target_location or not context.owner.valid_board_index( 35 | board_index): 36 | return False 37 | if store_index is not None: 38 | if CardLocation.STORE not in self.target_location or not context.owner.valid_store_index( 39 | store_index): 40 | return False 41 | if not self.valid_target(context, board_index, store_index): 42 | return False 43 | return True 44 | 45 | def valid_target(self, context: 'BuyPhaseContext', board_index: Optional['BoardIndex'] = None, 46 | store_index: Optional['StoreIndex'] = None): 47 | return True 48 | 49 | def on_play(self, context: 'BuyPhaseContext', board_index: Optional['BoardIndex'] = None, 50 | store_index: Optional['StoreIndex'] = None): 51 | pass 52 | 53 | def on_gain(self, context: 'BuyPhaseContext'): 54 | pass 55 | -------------------------------------------------------------------------------- /hearthstone/battlebots/supremacy_bot.py: -------------------------------------------------------------------------------- 1 | import random 2 | import typing 3 | 4 | from hearthstone.simulator.agent.actions import StandardAction, generate_standard_actions, BuyAction, EndPhaseAction, \ 5 | SummonAction, \ 6 | TavernUpgradeAction, DiscoverChoiceAction, RearrangeCardsAction, FreezeDecision 7 | from hearthstone.simulator.agent.agent import Agent 8 | 9 | if typing.TYPE_CHECKING: 10 | from hearthstone.simulator.core.player import Player 11 | 12 | 13 | class SupremacyBot(Agent): 14 | authors = ["Jeremy Salwen"] 15 | 16 | def __init__(self, monster_type: str, upgrade: bool, seed: int): 17 | self.local_random = random.Random(seed) 18 | self.monster_type = monster_type 19 | self.upgrade = upgrade 20 | 21 | async def rearrange_cards(self, player: 'Player') -> RearrangeCardsAction: 22 | permutation = list(range(len(player.in_play))) 23 | self.local_random.shuffle(permutation) 24 | return RearrangeCardsAction(permutation) 25 | 26 | async def buy_phase_action(self, player: 'Player') -> StandardAction: 27 | all_actions = list(generate_standard_actions(player)) 28 | 29 | if self.upgrade: 30 | upgrade_actions = [action for action in all_actions if type(action) is TavernUpgradeAction] 31 | if upgrade_actions: 32 | return upgrade_actions[0] 33 | 34 | summon_actions = [action for action in all_actions if type(action) is SummonAction] 35 | if summon_actions: 36 | return summon_actions[0] 37 | 38 | buy_actions = [action for action in all_actions if 39 | type(action) is BuyAction and player.store[action.index].monster_type == self.monster_type] 40 | buy_actions = sorted(buy_actions, key=lambda buy_action: player.store[buy_action.index].tier, reverse=True) 41 | if buy_actions: 42 | return buy_actions[0] 43 | 44 | return EndPhaseAction(FreezeDecision.NO_FREEZE) 45 | 46 | async def discover_choice_action(self, player: 'Player') -> DiscoverChoiceAction: 47 | discover_cards = player.discover_queue[0].items 48 | discover_cards = sorted(discover_cards, key=lambda card: card.tier, reverse=True) 49 | return DiscoverChoiceAction(player.discover_queue[0].items.index(discover_cards[0])) 50 | -------------------------------------------------------------------------------- /rust/pyrite_hearth_battles/tests/combat_tests.rs: -------------------------------------------------------------------------------- 1 | use pyrite_hearth_battles::warparty::WarParty; 2 | use pyrite_hearth_battles::monstercard::MonsterCard; 3 | use pyrite_hearth_battles::combat; 4 | use pyrite_hearth_battles::monstercards::*; 5 | use pyrite_hearth_battles::monstertypes::*; 6 | 7 | fn init() { 8 | let _ = env_logger::builder().is_test(true).try_init(); 9 | } 10 | 11 | #[test] 12 | fn test_vulgar_homunculus() { 13 | let card = MonsterCard::new(MonsterName::VulgarHomunculus); 14 | assert_eq!(card.properties.health, 4); 15 | assert_eq!(card.properties.attack, 2); 16 | } 17 | 18 | #[test] 19 | fn test_fruitless_war() { 20 | init(); 21 | let mut warparty1 = WarParty::new(vec![MonsterCard{card_name: MonsterName::AlleyCat, properties: BaseProperties::new(20, 2, 2, MonsterTypes::Beast)}]); 22 | let mut warparty2 = WarParty::new(vec![MonsterCard{card_name: MonsterName::AlleyCat, properties: BaseProperties::new(4, 3, 2, MonsterTypes::Beast)}]); 23 | combat::battle_boards(&mut warparty1, &mut warparty2); 24 | assert_eq!(*warparty1.index(0), MonsterCard{card_name: MonsterName::AlleyCat, properties: BaseProperties::new(14, 2, 2, MonsterTypes::Beast)}); 25 | assert_eq!(warparty2.len(), 0); 26 | println!("{:?} {:?}", warparty1, warparty2) 27 | } 28 | 29 | #[test] 30 | fn test_taunt() { 31 | init(); 32 | let mut warparty1 = WarParty::new(vec![MonsterCard::new(MonsterName::VulgarHomunculus), MonsterCard::new(MonsterName::AlleyCat), MonsterCard::new(MonsterName::AlleyCat)]); 33 | let mut warparty2 = WarParty::new(vec![MonsterCard::new(MonsterName::VulgarHomunculus), MonsterCard::new(MonsterName::RabidSaurolisk)]); 34 | combat::battle_boards(&mut warparty1, &mut warparty2); 35 | assert_eq!(warparty1.len(), 0); 36 | assert_eq!(warparty2.len(), 0); 37 | println!("{:?} {:?}", warparty1, warparty2) 38 | } 39 | 40 | #[test] 41 | fn test_scavenging_hyena() { 42 | init(); 43 | let mut warparty1 = WarParty::new(vec![MonsterCard::new(MonsterName::DragonSpawnLieutenant), MonsterCard::new(MonsterName::DragonSpawnLieutenant)]); 44 | let mut warparty2 = WarParty::new(vec![MonsterCard::new(MonsterName::ScavengingHyena), MonsterCard::new(MonsterName::ScavengingHyena)]); 45 | combat::battle_boards(&mut warparty1, &mut warparty2); 46 | assert_eq!(warparty1.len(), 0); 47 | assert_eq!(warparty2.len(), 0); 48 | println!("{:?} {:?}", warparty1, warparty2) 49 | } -------------------------------------------------------------------------------- /hearthstone/battlebots/get_bot_contestants.py: -------------------------------------------------------------------------------- 1 | from inspect import getmembers, isfunction 2 | 3 | from hearthstone.battlebots.CardSpecificHeuristics import MamasLove, SameTypeAdvantage, DragonPayoffs, \ 4 | MonstrousMacawPower 5 | from hearthstone.battlebots.early_game_bot import EarlyGameBot 6 | from hearthstone.battlebots.hero_bot import HeroBot 7 | from hearthstone.battlebots.priority_bot import PriorityBot 8 | from hearthstone.battlebots.priority_functions import PriorityFunctions 9 | from hearthstone.simulator.core.monster_types import MONSTER_TYPES 10 | 11 | 12 | def get_priority_bot_contestant_tuples(): 13 | priority_bots = [PriorityBot, HeroBot, EarlyGameBot] 14 | 15 | function_list = [member[1] for member in getmembers(PriorityFunctions, isfunction)] 16 | contestant_tuples = [] 17 | seed = 0 18 | for bot in priority_bots: 19 | for function in function_list: 20 | if function is PriorityFunctions.racist_priority_bot: 21 | for monster_type in MONSTER_TYPES: 22 | if monster_type != MONSTER_TYPES.ALL: 23 | seed += 1 24 | contestant_tuples.append((f"{bot.__name__}-{function.__name__}-{monster_type.name}", 25 | function(seed, bot, monster_type))) 26 | elif function is not PriorityFunctions.priority_callables_bot: 27 | seed += 1 28 | contestant_tuples.append((f"{bot.__name__}-{function.__name__}", function(seed, bot))) 29 | return contestant_tuples 30 | 31 | 32 | def get_priority_heuristics_bot_contestant_tuples(): 33 | priority_bots = [PriorityBot, HeroBot, EarlyGameBot] 34 | 35 | function_list = [member[1] for member in getmembers(PriorityFunctions, isfunction)] 36 | 37 | contestant_tuples = [] 38 | seed = 0 39 | for bot in priority_bots: 40 | for function in function_list: 41 | if function is PriorityFunctions.priority_callables_bot: 42 | contestant_tuples.append( 43 | (f"{bot.__name__}-{function.__name__}", function(seed, bot, None, [MamasLove(), SameTypeAdvantage(), 44 | DragonPayoffs(), 45 | MonstrousMacawPower()]))) 46 | seed += 1 47 | return contestant_tuples 48 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/worker/postprocessing.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from torch.utils.tensorboard import SummaryWriter 4 | 5 | from hearthstone.ladder.ladder import Contestant, update_ratings, print_standings 6 | from hearthstone.simulator.replay.replay import Replay 7 | from hearthstone.training.pytorch import tensorboard_altair 8 | from hearthstone.training.pytorch.gae import GAEAnnotator 9 | from hearthstone.training.pytorch.replay_buffer import EpochBuffer 10 | from hearthstone.training.pytorch.surveillance import GlobalStepContext 11 | 12 | 13 | class ReplaySink: 14 | def process(self, replay: Replay, learning_bot_contestant: Contestant, other_contestants: List[Contestant]): 15 | pass 16 | 17 | 18 | class ExperiencePostProcessor(ReplaySink): 19 | def __init__(self, 20 | epoch_buffer: EpochBuffer, 21 | annotator: GAEAnnotator, 22 | tensorboard: SummaryWriter, 23 | global_step_context: GlobalStepContext 24 | ): 25 | self.epoch_buffer = epoch_buffer 26 | self.annotator = annotator 27 | self.tensorboard = tensorboard 28 | self.global_step_context = global_step_context 29 | 30 | def process(self, replay: Replay, learning_bot_contestant: Contestant, other_contestants: List[Contestant]): 31 | self.annotator.annotate(replay) 32 | tensorboard_altair.plot_replay(replay, learning_bot_contestant.name, self.tensorboard, 33 | self.global_step_context) 34 | self._update_ratings(learning_bot_contestant, [learning_bot_contestant] + other_contestants, replay) 35 | self.epoch_buffer.add_replay(replay) 36 | 37 | @staticmethod 38 | def _update_ratings(learning_bot_contestant, all_contestants, replay): 39 | winner_names = replay.observer_annotations["RankingAnnotator"] 40 | final_boards = replay.observer_annotations["FinalBoardAnnotator"] 41 | print("---------------------------------------------------------------") 42 | print(winner_names) 43 | print("[" + ", ".join(final_boards[learning_bot_contestant.name]) + "]") 44 | ranked_contestants = sorted([c for c in all_contestants if c.name in winner_names], 45 | key=lambda c: winner_names.index(c.name)) 46 | update_ratings(ranked_contestants) 47 | print_standings(all_contestants) 48 | for contestant in ranked_contestants: 49 | contestant.games_played += 1 50 | -------------------------------------------------------------------------------- /hearthstone/simulator/core/combat_event_queue.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from collections import deque 3 | from typing import Optional, Tuple 4 | 5 | import autoslot 6 | 7 | from hearthstone.simulator.core.events import EVENTS 8 | from hearthstone.simulator.core.randomizer import Randomizer, DefaultRandomizer 9 | 10 | if typing.TYPE_CHECKING: 11 | from hearthstone.simulator.core.cards import MonsterCard 12 | from hearthstone.simulator.core.combat import WarParty 13 | 14 | 15 | class CombatEventQueue(autoslot.Slots): 16 | def __init__(self, war_party_1: 'WarParty', war_party_2: 'WarParty', 17 | randomizer: Optional['Randomizer'] = None): 18 | self.randomizer = randomizer or DefaultRandomizer() 19 | self.queues: typing.Dict[EVENTS, typing.Dict[WarParty, typing.Deque]] = { 20 | EVENTS.DEATHRATTLE_TRIGGERED: {war_party_1: deque(), war_party_2: deque()}, 21 | EVENTS.DIES: {war_party_1: deque(), war_party_2: deque()} 22 | } 23 | 24 | def load_minion(self, event: 'EVENTS', war_party: 'WarParty', minion: 'MonsterCard', 25 | foe: Optional['MonsterCard'] = None): 26 | self.queues[event][war_party].append((minion, foe)) 27 | 28 | def get_next_minion(self, event: 'EVENTS') -> Tuple['MonsterCard', Optional['MonsterCard'], 'WarParty', 'WarParty']: 29 | assert not self.all_empty() 30 | 31 | if all(bool(queue) for queue in self.queues[event].values()): 32 | non_empty_queue = self.randomizer.select_event_queue(list(self.queues[event].values())) 33 | else: 34 | non_empty_queue = [q for q in self.queues[event].values() if bool(q)][0] 35 | 36 | other_queue = [q for q in self.queues[event].values() if q != non_empty_queue][0] 37 | friendly_war_party = self.get_war_party(non_empty_queue, event) 38 | enemy_war_party = self.get_war_party(other_queue, event) 39 | minion, foe = non_empty_queue.popleft() 40 | 41 | return minion, foe, friendly_war_party, enemy_war_party 42 | 43 | def all_empty(self) -> bool: 44 | return all(not bool(queue) for pairs in self.queues.values() for queue in pairs.values()) 45 | 46 | def event_empty(self, event: 'EVENTS') -> bool: 47 | return all(not bool(queue) for queue in self.queues[event].values()) 48 | 49 | def get_war_party(self, queue: deque, event: 'EVENTS') -> 'WarParty': 50 | queues_of_event = list(self.queues[event].keys()) 51 | queue_index = list(self.queues[event].values()).index(queue) 52 | return queues_of_event[queue_index] 53 | -------------------------------------------------------------------------------- /hearthstone/battlebots/saurolisk_bot.py: -------------------------------------------------------------------------------- 1 | import random 2 | import typing 3 | 4 | from hearthstone.simulator.agent.actions import StandardAction, generate_standard_actions, BuyAction, EndPhaseAction, \ 5 | SummonAction, \ 6 | SellAction, TavernUpgradeAction, RerollAction, DiscoverChoiceAction, RearrangeCardsAction, \ 7 | FreezeDecision 8 | from hearthstone.simulator.agent.agent import Agent 9 | from hearthstone.simulator.core.card_pool import RabidSaurolisk 10 | from hearthstone.simulator.core.player import Player, BoardIndex 11 | 12 | if typing.TYPE_CHECKING: 13 | pass 14 | 15 | 16 | class SauroliskBot(Agent): 17 | authors = ["Jake Bumgardner"] 18 | 19 | def __init__(self, seed: int): 20 | self.local_random = random.Random(seed) 21 | 22 | async def rearrange_cards(self, player: 'Player') -> RearrangeCardsAction: 23 | permutation = list(range(len(player.in_play))) 24 | self.local_random.shuffle(permutation) 25 | return RearrangeCardsAction(permutation) 26 | 27 | @staticmethod 28 | def desired_card(card): 29 | return type(card) == RabidSaurolisk or card.deathrattles 30 | 31 | async def buy_phase_action(self, player: 'Player') -> StandardAction: 32 | all_actions = list(generate_standard_actions(player)) 33 | 34 | upgrade_actions = [action for action in all_actions if type(action) is TavernUpgradeAction] 35 | if upgrade_actions: 36 | return upgrade_actions[0] 37 | 38 | summon_actions = [action for action in all_actions if type(action) is SummonAction] 39 | if summon_actions: 40 | return summon_actions[0] 41 | 42 | buy_actions = [action for action in all_actions if 43 | type(action) is BuyAction and self.desired_card(player.store[action.index])] 44 | if buy_actions: 45 | return buy_actions[0] 46 | 47 | reroll_action = RerollAction() 48 | if reroll_action.valid(player): 49 | return reroll_action 50 | 51 | if len(player.in_play) == 7: 52 | for index, card in enumerate(player.in_play): 53 | if type(card) is not RabidSaurolisk: 54 | return SellAction(BoardIndex(index)) 55 | 56 | return EndPhaseAction(FreezeDecision.NO_FREEZE) 57 | 58 | async def discover_choice_action(self, player: 'Player') -> DiscoverChoiceAction: 59 | discover_cards = player.discover_queue[0].items 60 | discover_cards = sorted(discover_cards, key=lambda card: self.desired_card(card), reverse=True) 61 | return DiscoverChoiceAction(player.discover_queue[0].items.index(discover_cards[0])) 62 | -------------------------------------------------------------------------------- /hearthstone/simulator/host/host.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from typing import Dict, Optional, List 3 | 4 | from frozenlist.frozen_list import FrozenList 5 | from hearthstone.simulator import agent 6 | from hearthstone.simulator.core.tavern import Tavern 7 | from hearthstone.simulator.replay.replay import Replay, ReplayStep 8 | 9 | if typing.TYPE_CHECKING: 10 | from hearthstone.simulator.agent import agent 11 | from hearthstone.simulator.agent.agent import AnnotatingAgent 12 | from hearthstone.simulator.agent.actions import Action 13 | from hearthstone.simulator.core.randomizer import Randomizer 14 | from hearthstone.simulator.replay.observer import Observer 15 | 16 | 17 | class Host: 18 | tavern: Tavern 19 | agents: Dict[str, 'AnnotatingAgent'] 20 | replay: Replay 21 | observers: FrozenList # [Observer] 22 | 23 | def __init__(self, agents: Dict[str, 'AnnotatingAgent'], observers: Optional[List['Observer']] = None, 24 | randomizer: Optional['Randomizer'] = None): 25 | self.tavern = Tavern(randomizer=randomizer) 26 | self.agents = agents 27 | for player_name in sorted(agents.keys()): # Sorting is important for replays to be exact with RNG. 28 | self.tavern.add_player(player_name) 29 | self.replay = Replay(self.tavern.randomizer.seed, list(self.tavern.players.keys())) 30 | if not observers: 31 | observers = [] 32 | self.observers = FrozenList(observers) 33 | 34 | def _apply_and_record(self, player_name: str, action: 'Action', agent_annotation: 'agent.Annotation' = None): 35 | observer_annotations = {} 36 | for observer in self.observers: 37 | annotation = observer.on_action(self.tavern, player_name, action) 38 | if annotation is not None: 39 | observer_annotations[observer.name()] = annotation 40 | 41 | action.apply(self.tavern.players[player_name]) 42 | self.replay.append_action(ReplayStep(player_name, action, agent_annotation, observer_annotations)) 43 | 44 | def _on_game_over(self): 45 | for observer in self.observers: 46 | annotation = observer.on_game_over(self.tavern) 47 | if annotation is not None: 48 | self.replay.observer_annotate(observer.name(), annotation) 49 | 50 | def start_game(self): 51 | raise NotImplementedError() 52 | 53 | def play_round(self): 54 | raise NotImplementedError() 55 | 56 | def game_over(self): 57 | raise NotImplementedError() 58 | 59 | def play_game(self): 60 | raise NotImplementedError() 61 | 62 | def get_replay(self) -> Replay: 63 | raise NotImplementedError() 64 | -------------------------------------------------------------------------------- /tests/test_gameplay.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import logging 4 | 5 | from hearthstone.battlebots.early_game_bot import EarlyGameBot 6 | from hearthstone.battlebots.priority_bot import PriorityBot 7 | from hearthstone.battlebots.priority_functions import PriorityFunctions 8 | from hearthstone.simulator.core.monster_types import MONSTER_TYPES 9 | from hearthstone.simulator.core.randomizer import DefaultRandomizer 10 | from hearthstone.simulator.host.async_host import AsyncHost 11 | from hearthstone.testing.battlegrounds_test_case import BattleGroundsTestCase 12 | 13 | 14 | class GameplayTests(BattleGroundsTestCase): 15 | def test_basic_bots(self): 16 | host = AsyncHost({ 17 | "battlerattler_priority_bot": PriorityFunctions.battlerattler_priority_bot(1, EarlyGameBot), 18 | "priority_saurolisk_buff_bot": PriorityFunctions.priority_saurolisk_buff_bot(2, EarlyGameBot), 19 | "racist_priority_bot_mech": PriorityFunctions.racist_priority_bot(3, EarlyGameBot, MONSTER_TYPES.MECH), 20 | "racist_priority_bot_murloc": PriorityFunctions.racist_priority_bot(4, EarlyGameBot, MONSTER_TYPES.MURLOC), 21 | "priority_adaptive_tripler_bot": PriorityFunctions.priority_adaptive_tripler_bot(5, EarlyGameBot), 22 | "priority_pack_leader_bot": PriorityFunctions.priority_pack_leader_bot(7, PriorityBot), 23 | }, randomizer=DefaultRandomizer(107)) 24 | 25 | host.play_game() 26 | 27 | def test_replay_same_outcome(self): 28 | logging.basicConfig(level=logging.DEBUG) 29 | # TODO make replays work so this test passes. This requires handling the ordering of players joining and 30 | # Choosing their heros. 31 | host = AsyncHost({ 32 | "battlerattler_priority_bot": PriorityFunctions.battlerattler_priority_bot(1, EarlyGameBot), 33 | "priority_saurolisk_buff_bot": PriorityFunctions.priority_saurolisk_buff_bot(2, EarlyGameBot), 34 | "racist_priority_bot_mech": PriorityFunctions.racist_priority_bot(3, EarlyGameBot, MONSTER_TYPES.MECH), 35 | "racist_priority_bot_murloc": PriorityFunctions.racist_priority_bot(4, EarlyGameBot, MONSTER_TYPES.MURLOC), 36 | "priority_adaptive_tripler_bot": PriorityFunctions.priority_adaptive_tripler_bot(5, EarlyGameBot), 37 | "priority_pack_leader_bot": PriorityFunctions.priority_pack_leader_bot(7, PriorityBot), 38 | }, randomizer=DefaultRandomizer(11)) 39 | host.play_game() 40 | replay = host.get_replay() 41 | replayed_tavern = replay.run_replay() 42 | self.assertListEqual([name for name, _ in host.tavern.losers], [name for name, _ in replayed_tavern.losers]) 43 | 44 | 45 | if __name__ == '__main__': 46 | unittest.main() 47 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/optuner.py: -------------------------------------------------------------------------------- 1 | import joblib 2 | import optuna 3 | 4 | from hearthstone.training.pytorch.ppo import PPOHyperparameters, PPOLearner 5 | 6 | 7 | def objective(trial: optuna.Trial): 8 | hparams = PPOHyperparameters({ 9 | "optimizer": trial.suggest_categorical("optimizer", ["adam", "sgd"]), 10 | "batch_size": trial.suggest_int("batch_size", 1, 4096, log=True), 11 | "ppo_epochs": trial.suggest_int("ppo_epochs", 1, 40), 12 | "ppo_epsilon": trial.suggest_float("ppo_epsilon", 0.01, 0.5, log=True), 13 | "policy_weight": trial.suggest_float("policy_weight", 0.3, 3, log=True), 14 | "entropy_weight": trial.suggest_float("entropy_weight", 1e-7, 1e-2, log=True), 15 | "nn.hidden_layers": trial.suggest_int("nn.hidden_layers", 0, 3), 16 | "normalize_observations": trial.suggest_categorical("normalize_observations", [True, False]), 17 | "gradient_clipping": trial.suggest_float("gradient_clipping", 0.5, 0.5), 18 | "normalize_advantage": trial.suggest_categorical("normalize_advantage", [True, False]), 19 | }) 20 | hparams["num_workers"] = trial.suggest_int("num_workers", 1, hparams["batch_size"], log=True) 21 | 22 | if hparams["optimizer"] == "adam": 23 | hparams["adam.lr"] = trial.suggest_float("adam.lr", 1e-6, 1e-3, log=True) 24 | elif hparams["optimizer"] == "sgd": 25 | hparams["sgd_lr"] = trial.suggest_float("sgd_lr", 1e-6, 1e-3, log=True) 26 | hparams["sgd_momentum"] = trial.suggest_float("sgd_momentum", 0.0, 1.0) 27 | 28 | if hparams["nn.hidden_layers"] > 0: 29 | hparams["nn.hidden_size"] = trial.suggest_int("nn.hidden_size", 32, 2048) 30 | hparams["nn.shared"] = trial.suggest_categorical("nn.shared", [True, False]) 31 | hparams["nn.activation"] = trial.suggest_categorical("nn.activation", ["relu", "gelu", "tanh"]) 32 | 33 | ppo_learner = PPOLearner(hparams, 600, trial) 34 | return ppo_learner.run() 35 | 36 | 37 | def main(): 38 | """ 39 | A wise man once said, "You can optuna neural net but you can't optuna fish." - Albert Einstein. 40 | 41 | Returns: No returns. No refunds. No shirt. No service. 42 | """ 43 | study = optuna.create_study( 44 | storage="postgres://localhost/optuna", study_name="ppo_study", 45 | direction="maximize", 46 | load_if_exists=True, 47 | pruner=optuna.pruners.NopPruner()) 48 | try: 49 | try: 50 | with joblib.parallel_backend("multiprocessing"): 51 | study.optimize(objective, n_jobs=10, catch=(RuntimeError,)) 52 | except KeyboardInterrupt: 53 | pass 54 | except Exception as e: 55 | print(e) 56 | print(study.best_params) 57 | 58 | 59 | if __name__ == '__main__': 60 | main() 61 | -------------------------------------------------------------------------------- /rust/pyrite_hearth_battles/src/combat.rs: -------------------------------------------------------------------------------- 1 | 2 | use rand::{Rng}; 3 | use std::{ 4 | option::Option, 5 | usize, 6 | }; 7 | use crate::eventtypes::EventTypes; 8 | 9 | use super::warparty::WarParty; 10 | use super::monstercard::MonsterCard; 11 | use rand::prelude::SliceRandom; 12 | 13 | pub fn battle_boards<'a>(attacker: &'a mut WarParty, defender: &'a mut WarParty) { 14 | let player_two_active: bool = rand::random(); 15 | if player_two_active { 16 | std::mem::swap(attacker, defender) 17 | } 18 | loop { 19 | match (attacker.get_next_attacker_index(), select_target(defender)) { 20 | (Some(attacker_index), Some(defender_index)) => { 21 | println!("the attacker is: {:?}", attacker ); 22 | println!("the defender is: {:?}", defender ); 23 | fight(&mut attacker.index_mut(attacker_index), &mut defender.index_mut(defender_index)); 24 | check_casualties(attacker, defender); 25 | } 26 | (None, Some(_)) => { 27 | if !defender.has_attacker() { 28 | break 29 | } 30 | } 31 | _ => break, 32 | } 33 | std::mem::swap(attacker, defender); 34 | } 35 | } 36 | 37 | fn fight(attacker: &mut MonsterCard, defender: &mut MonsterCard) { 38 | attacker.properties.health -= defender.properties.attack; 39 | defender.properties.health -= attacker.properties.attack; 40 | } 41 | 42 | fn check_casualties_warparty(party: &mut WarParty, other_party: &mut WarParty) { 43 | let mut card_index: usize = 0; 44 | while card_index < party.len() { 45 | if party.index_mut(card_index).properties.health <= 0 { 46 | let card = party.remove(card_index); 47 | let event = EventTypes::MonsterDeath { card:card.clone() }; 48 | party.broadcast_event(&event); 49 | other_party.broadcast_event(&event); 50 | } else { 51 | card_index += 1; 52 | } 53 | } 54 | } 55 | 56 | fn check_casualties(attacker_party: &mut WarParty, defender_party: &mut WarParty) { 57 | check_casualties_warparty(attacker_party, defender_party); 58 | check_casualties_warparty(defender_party, attacker_party); 59 | } 60 | 61 | fn select_target(defender: &WarParty) -> Option { 62 | let taunt_indices: Vec = defender.iter().enumerate().filter_map( 63 | |(index, monstercard)| if monstercard.properties.taunt { Some(index) } else { None } 64 | ).collect(); 65 | if taunt_indices.len() > 0 { 66 | taunt_indices.choose(&mut rand::thread_rng()).map( 67 | |&x| x 68 | ) 69 | } else if defender.len() > 0 { 70 | Some(rand::thread_rng().gen_range(0..defender.len())) 71 | } else { 72 | None 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /frozenlist/frozen_list.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | 4 | class FrozenList: 5 | __slots__ = ["_private_list_dont_modify"] 6 | 7 | def __init__(self, l: List): 8 | self._private_list_dont_modify = l 9 | 10 | def copy(self): 11 | """ Return a shallow copy of the list. """ 12 | return self._private_list_dont_modify.copy() 13 | 14 | def count(self, x): 15 | """ Return number of occurrences of value. """ 16 | return self._private_list_dont_modify.count(x) 17 | 18 | def index(self, x, start=0, stop=9223372036854775807): 19 | """ 20 | Return first index of value. 21 | 22 | Raises ValueError if the value is not present. 23 | """ 24 | return self._private_list_dont_modify.index(x, start, stop) 25 | 26 | def __contains__(self, x): 27 | """ Return key in self. """ 28 | return x in self._private_list_dont_modify 29 | 30 | def __eq__(self, value): 31 | """ Return self==value. """ 32 | return self._private_list_dont_modify == value 33 | 34 | def __getitem__(self, key): 35 | if isinstance(key, slice): 36 | return FrozenList(self._private_list_dont_modify[key]) 37 | return self._private_list_dont_modify[key] 38 | 39 | def __ge__(self, value): 40 | return self._private_list_dont_modify >= value 41 | 42 | def __gt__(self, value): 43 | """ Return self>value. """ 44 | return self._private_list_dont_modify > value 45 | 46 | def __iter__(self): 47 | """ Implement iter(self). """ 48 | return iter(self._private_list_dont_modify) 49 | 50 | def __len__(self): 51 | """ Return len(self). """ 52 | return len(self._private_list_dont_modify) 53 | 54 | def __le__(self, value): 55 | """ Return self<=value. """ 56 | return self._private_list_dont_modify <= value 57 | 58 | def __lt__(self, value): 59 | """ Return self= 3 else 0 49 | 50 | 51 | class MonstrousMacawPower(CardHeuristic): 52 | cards_to_add = [MonstrousMacaw] 53 | 54 | def modification_value(self, player: 'Player', card: 'MonsterCard'): 55 | deathrattler_tiers = [player_card.tier for player_card in itertools.chain(player.in_play, player.hand) if 56 | player_card.deathrattles] 57 | if deathrattler_tiers: 58 | return max(deathrattler_tiers) 59 | return 0 60 | 61 | 62 | class Scavengers(CardHeuristic): 63 | cards_to_add = [ScavengingHyena] 64 | 65 | def modification_value(self, player: 'Player', card: 'MonsterCard'): 66 | if player.tavern_tier < 3: 67 | return len([player_card for player_card in itertools.chain(player.in_play, player.hand) if 68 | player_card.check_type(MONSTER_TYPES.BEAST)]) 69 | -------------------------------------------------------------------------------- /tests/pytorch/test_pytorch.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import torch 4 | from torch.distributions import Categorical 5 | 6 | from torch.profiler import profile, record_function, ProfilerActivity 7 | 8 | from hearthstone.simulator.core.tavern import Tavern 9 | from hearthstone.training.pytorch.encoding.default_encoder import DefaultEncoder 10 | from hearthstone.training.pytorch.networks.running_norm import WelfordAggregator 11 | 12 | 13 | class PytorchTests(unittest.TestCase): 14 | def test_encoding(self): 15 | tavern = Tavern() 16 | player_1 = tavern.add_player_with_hero("Dante_Kong") 17 | player_2 = tavern.add_player_with_hero("brian") 18 | tavern.buying_step() 19 | player_1_encoding = DefaultEncoder().encode_state(player_1) 20 | print(player_1_encoding) 21 | 22 | def test_valid_actions(self): 23 | tavern = Tavern() 24 | player_1 = tavern.add_player_with_hero("Dante_Kong") 25 | player_2 = tavern.add_player_with_hero("brian") 26 | tavern.buying_step() 27 | player_1_valid_actions = DefaultEncoder().encode_valid_actions(player_1, False) 28 | print(player_1_valid_actions) 29 | 30 | def test_get_stacked(self): 31 | tensor1 = torch.tensor([1, 2, 5, 6]) 32 | tensor2 = torch.tensor([5, 6, 83, 7]) 33 | print(tensor1.size()) 34 | print(tensor1.size() + tensor2.size()) 35 | torch.Size() 36 | 37 | # def test_gpu(self): 38 | # tensor1 = torch.tensor([1,2,5,6]) 39 | # if torch.cuda.is_available(): 40 | # for i in range(1000): 41 | # i_am_on_the_gpu = tensor1.cuda() 42 | # print("put some stuff on the GPU") 43 | 44 | def test_sample_distribution(self): 45 | tensor1 = torch.tensor([[1.5, 2.3, 3.8, 4.1], 46 | [0.1, 0.2, 0.3, 0.4]]) 47 | m = Categorical(tensor1) 48 | samp = m.sample() 49 | print(samp) 50 | prob = tensor1.gather(1, torch.tensor([[1, 3], [2, 3]])) 51 | print(prob) 52 | 53 | other = tensor1.gather(0, torch.tensor([[0, 1, 0, 0], [1, 0, 1, 1]])) 54 | print(other) 55 | 56 | def test_welford_aggregator(self): 57 | agg = WelfordAggregator(torch.Size()) 58 | data1 = torch.tensor([1., 2, 3, 4]) 59 | data2 = torch.tensor([5., 6, 7, 8]) 60 | data3 = torch.tensor([5., -1, -5, 8]) 61 | agg.update(data1) 62 | agg.update(data2) 63 | agg.update(data3) 64 | combined = torch.cat([data1, data2, data3]) 65 | self.assertAlmostEqual(agg.mean().item(), combined.mean().item()) 66 | self.assertAlmostEqual(agg.stdev().item(), torch.std(combined, unbiased=False).item(), 6) 67 | 68 | def test_cuda_memory_profiler(self): 69 | with profile(activities=[ProfilerActivity.CUDA], profile_memory=True, record_shapes=True) as prof: 70 | with record_function("model_inference"): 71 | pass 72 | 73 | 74 | if __name__ == '__main__': 75 | unittest.main() 76 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/worker/single_machine/single_worker_pool.py: -------------------------------------------------------------------------------- 1 | import random 2 | import time 3 | from typing import List 4 | 5 | import torch 6 | from torch.utils.tensorboard import SummaryWriter 7 | 8 | from hearthstone.ladder.ladder import Contestant, update_ratings, print_standings 9 | from hearthstone.simulator.host.round_robin_host import RoundRobinHost 10 | from hearthstone.training.pytorch.gae import GAEAnnotator 11 | from hearthstone.training.pytorch.replay_buffer import EpochBuffer 12 | from hearthstone.training.pytorch.surveillance import GlobalStepContext 13 | from hearthstone.training.pytorch.tensorboard_altair import TensorboardAltairAnnotator, plot_replay 14 | 15 | 16 | class SingleWorkerPool: 17 | def __init__(self, 18 | epoch_buffer: EpochBuffer, 19 | annotator: GAEAnnotator, 20 | tensorboard: SummaryWriter, 21 | global_step_context: GlobalStepContext): 22 | """ 23 | Worker is responsible for setting up games where the learning bot plays against a random set of opponents. 24 | """ 25 | self.epoch_buffer: EpochBuffer = epoch_buffer 26 | self.annotator: GAEAnnotator = annotator 27 | self.tensorboard = tensorboard 28 | self.global_step_context = global_step_context 29 | 30 | def play_games(self, learning_bot_contestant: Contestant, 31 | other_contestants: List[Contestant], 32 | game_size: int): 33 | round_contestants = [learning_bot_contestant] + random.sample(other_contestants, 34 | k=game_size - 1) 35 | with torch.no_grad(): 36 | host = RoundRobinHost( 37 | {contestant.name: contestant.agent_generator() for contestant in round_contestants}, 38 | [TensorboardAltairAnnotator([learning_bot_contestant.name])] 39 | ) 40 | start = time.time() 41 | host.play_game() 42 | print(f"Worker played 1 game. Time taken: {time.time() - start} seconds.") 43 | winner_names = list(reversed([name for name, player in host.tavern.losers])) 44 | print("---------------------------------------------------------------") 45 | print(winner_names) 46 | print(host.tavern.players[learning_bot_contestant.name].in_play, 47 | host.tavern.players[learning_bot_contestant.name].hero) 48 | ranked_contestants = sorted(round_contestants, key=lambda c: winner_names.index(c.name)) 49 | update_ratings(ranked_contestants) 50 | print_standings([learning_bot_contestant] + other_contestants) 51 | for contestant in round_contestants: 52 | contestant.games_played += 1 53 | 54 | replay = host.get_replay() 55 | self.annotator.annotate(replay) 56 | plot_replay(replay, learning_bot_contestant.name, self.tensorboard, self.global_step_context) 57 | self.epoch_buffer.add_replay(replay) 58 | -------------------------------------------------------------------------------- /benchmarks/benchmark_multiprocess.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import torch 4 | import torch.nn.functional as F 5 | from torch import nn 6 | 7 | from hearthstone.battlebots.no_action_bot import NoActionBot 8 | from hearthstone.simulator.host.round_robin_host import RoundRobinHost 9 | from hearthstone.training.pytorch.agents.pytorch_bot import PytorchBot 10 | from hearthstone.training.pytorch.encoding.default_encoder import DefaultEncoder 11 | from hearthstone.training.pytorch.networks.transformer_net import HearthstoneTransformerNet 12 | 13 | 14 | class MyNet(nn.Module): 15 | def __init__(self): 16 | super().__init__() 17 | self.layer1 = nn.Linear(50, 50) 18 | self.layer2 = nn.Linear(50, 50) 19 | 20 | def forward(self, input): 21 | return self.layer2(F.relu(self.layer1(input))) 22 | 23 | 24 | def process(net: MyNet, tensor: torch.Tensor): 25 | with torch.no_grad(): 26 | begin_time = time.time() 27 | for _ in range(80000): 28 | tensor = net(tensor) 29 | end_time = time.time() 30 | print("process time ", end_time - begin_time) 31 | result = float(tensor[0]) 32 | return result 33 | 34 | 35 | def process_hearthstone(): 36 | # set_num_threads is important here because openMP messes things up. 37 | torch.set_num_threads(1) 38 | with torch.no_grad(): 39 | host = RoundRobinHost( 40 | {"Bot1": PytorchBot(HearthstoneTransformerNet(DefaultEncoder(), 41 | hidden_layers=1, hidden_size=32), DefaultEncoder(), True), 42 | "Bot2": NoActionBot()}, 43 | [] 44 | ) 45 | print("Beginning!") 46 | start = time.time() 47 | host.start_game() 48 | for i in range(20): 49 | host.play_round() 50 | print("Done!", time.time() - start) 51 | 52 | 53 | def run_single_process_benchmark(num_workers: int): 54 | with torch.no_grad(): 55 | net = MyNet() 56 | tensor = torch.rand((50,)) 57 | begin_time = time.time() 58 | results = [ 59 | process_hearthstone() 60 | for _ in 61 | range(num_workers)] 62 | end_time = time.time() 63 | print("total time ", end_time - begin_time) 64 | 65 | 66 | def run_multiprocess_benchmark(num_workers: int): 67 | with torch.no_grad(): 68 | net = MyNet() 69 | net.share_memory() 70 | tensor = torch.rand((50,)) 71 | tensor.share_memory_() 72 | pool = torch.multiprocessing.Pool(processes=num_workers) 73 | 74 | begin_time = time.time() 75 | awaitables = [ 76 | pool.apply_async(process_hearthstone, ()) 77 | for _ in 78 | range(num_workers)] 79 | for promise in awaitables: 80 | promise.get() 81 | end_time = time.time() 82 | print("total time ", end_time - begin_time) 83 | 84 | 85 | def main(): 86 | num_workers = 2 87 | run_single_process_benchmark(num_workers) 88 | run_multiprocess_benchmark(num_workers) 89 | 90 | 91 | if __name__ == '__main__': 92 | main() 93 | -------------------------------------------------------------------------------- /hearthstone/training/simple_learning_bots/train_simple_policy_bot.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | 4 | from hearthstone.battlebots.cheapo_bot import CheapoBot 5 | from hearthstone.battlebots.priority_functions import racist_priority_bot 6 | from hearthstone.battlebots.random_bot import RandomBot 7 | from hearthstone.battlebots.simple_policy_bot import SimplePolicyBot 8 | from hearthstone.battlebots.supremacy_bot import SupremacyBot 9 | from hearthstone.ladder.ladder import Contestant, update_ratings, print_standings, save_ratings 10 | 11 | learning_rate = .1 12 | 13 | 14 | def learning_bot_opponents(): 15 | all_bots = [Contestant(f"RandomBot_{i}", RandomBot(1)) for i in range(20)] 16 | all_bots += [Contestant(f"CheapoBot", CheapoBot(3))] 17 | all_bots += [Contestant(f"SupremacyBot {t}", SupremacyBot(t, False, i)) for i, t in 18 | enumerate([MONSTER_TYPES.MURLOC, MONSTER_TYPES.BEAST, MONSTER_TYPES.MECH, MONSTER_TYPES.DRAGON, 19 | MONSTER_TYPES.DEMON, MONSTER_TYPES.PIRATE])] 20 | all_bots += [Contestant(f"PriorityRacistBot {t}", racist_priority_bot(t, i)) for i, t in 21 | enumerate([MONSTER_TYPES.MURLOC, MONSTER_TYPES.BEAST, MONSTER_TYPES.MECH, MONSTER_TYPES.DRAGON, 22 | MONSTER_TYPES.DEMON, MONSTER_TYPES.PIRATE])] 23 | return all_bots 24 | 25 | 26 | def main(): 27 | logging.getLogger().setLevel(logging.INFO) 28 | other_contestants = learning_bot_opponents() 29 | learning_bot = SimplePolicyBot(None, 1) 30 | learning_bot_contestant = Contestant("LearningBot", learning_bot) 31 | contestants = other_contestants + [learning_bot_contestant] 32 | bot_file = "../../data/learning/simple_policy_bot.1.json" 33 | standings_path = "../../data/learning/standings_policy_bot.json" 34 | # learning_bot.read_from_file(bot_file) 35 | # load_ratings(contestants, standings_path) 36 | 37 | for _ in range(10000): 38 | round_contestants = [learning_bot_contestant] + random.sample(other_contestants, k=7) 39 | host = RoundRobinHost({contestant.name: contestant.agent_generator() for contestant in round_contestants}) 40 | host.play_game() 41 | winner_names = list(reversed([name for name, player in host.tavern.losers])) 42 | print("---------------------------------------------------------------") 43 | print(winner_names) 44 | # print(host.tavern.losers[-1][1].in_play) 45 | ranked_contestants = sorted(round_contestants, key=lambda c: winner_names.index(c.name)) 46 | update_ratings(ranked_contestants) 47 | print_standings(contestants) 48 | for contestant in round_contestants: 49 | contestant.games_played += 1 50 | if learning_bot_contestant in round_contestants: 51 | learning_bot.learn_from_game(ranked_contestants.index(learning_bot_contestant), learning_rate) 52 | print("Favorite cards: ", 53 | sorted(learning_bot.priority_buy_dict.items(), key=lambda item: item[1], reverse=True)) 54 | # learning_bot.save_to_file(bot_file) 55 | 56 | save_ratings(contestants, standings_path) 57 | 58 | 59 | if __name__ == '__main__': 60 | main() 61 | -------------------------------------------------------------------------------- /hearthstone/simulator/core/card_graveyard.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from inspect import getmembers, isclass 3 | from typing import Union 4 | 5 | from hearthstone.simulator.core.cards import MonsterCard 6 | from hearthstone.simulator.core.events import CardEvent, EVENTS, BuyPhaseContext, CombatPhaseContext 7 | from hearthstone.simulator.core.monster_types import MONSTER_TYPES 8 | 9 | 10 | class FloatingWatcher(MonsterCard): 11 | tier = 4 12 | monster_type = MONSTER_TYPES.DEMON 13 | pool = MONSTER_TYPES.DEMON 14 | base_attack = 4 15 | base_health = 4 16 | mana_cost = 5 17 | 18 | def handle_event_powers(self, event: 'CardEvent', context: Union['BuyPhaseContext', 'CombatPhaseContext']): 19 | if event.event is EVENTS.PLAYER_DAMAGED: 20 | bonus = 4 if self.golden else 2 21 | self.attack += bonus 22 | self.health += bonus 23 | 24 | 25 | class ElistraTheImmortal(MonsterCard): 26 | tier = 6 27 | monster_type = MONSTER_TYPES.NEUTRAL 28 | base_attack = 4 29 | base_health = 4 30 | base_divine_shield = True 31 | base_reborn = True 32 | divert_taunt_attack = True 33 | legendary = True 34 | 35 | 36 | class BarrensBlacksmith(MonsterCard): 37 | tier = 3 38 | monster_type = None 39 | base_attack = 3 40 | base_health = 5 41 | 42 | def frenzy(self, context: CombatPhaseContext): 43 | bonus = 4 if self.golden else 2 44 | for card in context.friendly_war_party.board: 45 | if card != self: 46 | card.attack += bonus 47 | card.health += bonus 48 | 49 | 50 | class Siegebreaker(MonsterCard): 51 | tier = 4 52 | monster_type = MONSTER_TYPES.DEMON 53 | pool = MONSTER_TYPES.DEMON 54 | base_attack = 5 55 | base_health = 8 56 | base_taunt = True 57 | mana_cost = 7 58 | 59 | def handle_event_powers(self, event: 'CardEvent', context: Union['BuyPhaseContext', 'CombatPhaseContext']): 60 | bonus = 2 if self.golden else 1 61 | if event.event is EVENTS.COMBAT_PREPHASE or (event.event is EVENTS.SUMMON_COMBAT and event.card == self): 62 | demons = [card for card in context.friendly_war_party.board if 63 | card != self and card.check_type(MONSTER_TYPES.DEMON)] 64 | for demon in demons: 65 | demon.attack += bonus 66 | elif event.event is EVENTS.SUMMON_COMBAT and event.card in context.friendly_war_party.board \ 67 | and event.card != self and event.card.check_type(MONSTER_TYPES.DEMON): 68 | event.card.attack += bonus 69 | elif event.event is EVENTS.DIES and event.card == self: 70 | demons = [card for card in context.friendly_war_party.board if 71 | card != self and card.check_type(MONSTER_TYPES.DEMON)] 72 | for demon in demons: 73 | demon.attack -= bonus 74 | 75 | 76 | REMOVED_CARDS = [member[1] for member in getmembers(sys.modules[__name__], 77 | lambda member: isclass(member) and issubclass(member, 78 | MonsterCard) and member.__module__ == __name__)] 79 | -------------------------------------------------------------------------------- /hearthstone/simulator/replay/replay.py: -------------------------------------------------------------------------------- 1 | from typing import List, Any, Dict, Optional 2 | 3 | import autoslot 4 | 5 | from hearthstone.simulator.agent.actions import EndPhaseAction, Action, HeroChoiceAction, RearrangeCardsAction 6 | from hearthstone.simulator.core.randomizer import DefaultRandomizer 7 | from hearthstone.simulator.core.tavern import Tavern 8 | 9 | 10 | class ReplayStep(autoslot.Slots): 11 | def __init__(self, player: str, action: 'Action', agent_annotation: Any = None, 12 | observer_annotations: Optional[Dict[str, Any]] = None): 13 | self.player = player 14 | self.action = action 15 | self.agent_annotation = agent_annotation 16 | self.observer_annotations = observer_annotations or {} 17 | 18 | def __repr__(self): 19 | return f"{self.player}: {self.action} ({self.agent_annotation}) ({self.observer_annotations})" 20 | 21 | 22 | class Replay: 23 | def __init__(self, seed: int, players: List[str]): 24 | self.seed = seed 25 | self.players = players 26 | self.steps: List[ReplayStep] = [] 27 | self.agent_annotations: Dict[str, Any] = {} # mapping player name to agent annotation 28 | self.observer_annotations: Dict[str, Any] = {} # Mapping observer to its annotation. 29 | 30 | def append_action(self, replay_step: ReplayStep): 31 | self.steps.append(replay_step) 32 | 33 | def agent_annotate(self, player: str, annotation: Any): 34 | self.agent_annotations[player] = annotation 35 | 36 | def observer_annotate(self, observer: str, annotation: Any): 37 | self.observer_annotations[observer] = annotation 38 | 39 | def run_replay(self) -> 'Tavern': 40 | tavern = Tavern(randomizer=DefaultRandomizer(self.seed)) 41 | for player in sorted(self.players): # Sorting is important for replays to be exact with RNG. 42 | tavern.add_player(player) 43 | 44 | hero_chosen_players = set() 45 | for replay_step in self.steps[:len(self.players)]: 46 | assert isinstance(replay_step.action, HeroChoiceAction) 47 | replay_step.action.apply(tavern.players[replay_step.player]) 48 | hero_chosen_players.add(replay_step.player) 49 | assert hero_chosen_players == set(self.players) 50 | 51 | tavern.buying_step() 52 | end_phase_actions = set() 53 | i = len(self.players) 54 | while i < len(self.steps): 55 | replay_step = self.steps[i] 56 | if type(replay_step.action) is EndPhaseAction: 57 | assert replay_step.player not in end_phase_actions 58 | end_phase_actions.add(replay_step.player) 59 | replay_step.action.apply(tavern.players[replay_step.player]) 60 | if len(end_phase_actions) + len(tavern.losers) == len(self.players): 61 | while i + 1 < len(self.steps) and type(self.steps[i + 1].action) is RearrangeCardsAction: 62 | self.steps[i + 1].action.apply(tavern.players[replay_step.player]) 63 | i += 1 64 | tavern.combat_step() 65 | end_phase_actions = set() 66 | if not tavern.game_over(): 67 | tavern.buying_step() 68 | i += 1 69 | return tavern 70 | -------------------------------------------------------------------------------- /hearthstone/simulator/agent/agent.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from hearthstone.simulator.agent.actions import HeroChoiceAction, DiscoverChoiceAction, \ 4 | RearrangeCardsAction, StandardAction 5 | from hearthstone.simulator.core.player import HeroChoiceIndex 6 | from hearthstone.simulator.core.tavern import Player 7 | 8 | Annotation = Any 9 | 10 | 11 | class AnnotatingAgent: 12 | async def hero_choice_action(self, player: 'Player') -> HeroChoiceAction: 13 | return HeroChoiceAction(HeroChoiceIndex(0)) 14 | 15 | async def annotated_rearrange_cards(self, player: 'Player') -> (RearrangeCardsAction, Annotation): 16 | """ 17 | here the player selects a card arrangement one time per combat directly preceding combat 18 | 19 | Args: 20 | player: The player object controlled by this agent. This function should not modify it. 21 | 22 | Returns: A tuple containing an arrangement of the player's board, and the Agent Annotation to attach to the replay. 23 | 24 | """ 25 | pass 26 | 27 | async def annotated_buy_phase_action(self, player: 'Player') -> (StandardAction, Annotation): 28 | """ 29 | here the player chooses a buy phase action including: 30 | purchasing a card from the store 31 | summoning a card from hand to in_play 32 | selling a card from hand or from in_play 33 | and ending the buy phase 34 | 35 | Args: 36 | player: The player object controlled by this agent. This function should not modify it. 37 | 38 | Returns: 39 | A tuple containing the Action, and the Agent Annotation to attach to the replay. 40 | 41 | """ 42 | pass 43 | 44 | async def annotated_discover_choice_action(self, player: 'Player') -> (DiscoverChoiceAction, Annotation): 45 | """ 46 | 47 | Args: 48 | player: The player object controlled by this agent. This function should not modify it. 49 | 50 | Returns: 51 | Tuple of MonsterCard to discover, and Annotation to attach to the action. 52 | """ 53 | pass 54 | 55 | async def game_over(self, player: 'Player', ranking: int) -> Annotation: 56 | """ 57 | Notifies the agent that the game is over and the agent has achieved a given rank 58 | :param ranking: Integer index 0 to 7 of where the agent placed 59 | :return: 60 | """ 61 | pass 62 | 63 | 64 | class Agent(AnnotatingAgent): 65 | 66 | async def buy_phase_action(self, player: 'Player') -> StandardAction: 67 | pass 68 | 69 | async def annotated_buy_phase_action(self, player: 'Player') -> (StandardAction, Annotation): 70 | return await self.buy_phase_action(player), None 71 | 72 | async def rearrange_cards(self, player: 'Player') -> RearrangeCardsAction: 73 | pass 74 | 75 | async def annotated_rearrange_cards(self, player: 'Player') -> (RearrangeCardsAction, Annotation): 76 | return await self.rearrange_cards(player), None 77 | 78 | async def discover_choice_action(self, player: 'Player') -> DiscoverChoiceAction: 79 | pass 80 | 81 | async def annotated_discover_choice_action(self, player: 'Player') -> (DiscoverChoiceAction, Annotation): 82 | return await self.discover_choice_action(player), None 83 | -------------------------------------------------------------------------------- /hearthstone/simulator/host/cyborg_host.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import logging 4 | 5 | from hearthstone.asyncio import asyncio_utils 6 | from hearthstone.battlebots.early_game_bot import EarlyGameBot 7 | from hearthstone.battlebots.priority_functions import PriorityFunctions 8 | from hearthstone.simulator.agent import EndPhaseAction 9 | from hearthstone.simulator.host.async_host import AsyncHost 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | class CyborgArena(AsyncHost): 14 | async def async_play_round(self): 15 | self.tavern.buying_step() 16 | 17 | async def perform_player_actions(agent, player): 18 | for _ in range(40): 19 | if player.discover_queue: 20 | try: 21 | discovered_card = await agent.discover_choice_action(player) 22 | except ConnectionError: 23 | print("replace with a bot") 24 | # replace the agent and player 25 | agent = PriorityFunctions.battlerattler_priority_bot(3, EarlyGameBot) 26 | self.agents[player.name] = agent 27 | discovered_card = await agent.discover_choice_action(player) 28 | 29 | player.select_discover(discovered_card) 30 | else: 31 | try: 32 | action = await agent.buy_phase_action(player) 33 | except ConnectionError: 34 | print("replace with a bot") 35 | 36 | # replace the agent and player 37 | agent = PriorityFunctions.battlerattler_priority_bot(3, EarlyGameBot) 38 | self.agents[player.name] = agent 39 | action = await agent.buy_phase_action(player) 40 | action.apply(player) 41 | if type(action) is EndPhaseAction: 42 | break 43 | if len(player.in_play) > 1: 44 | try: 45 | arrangement = await agent.rearrange_cards(player) 46 | except ConnectionError: 47 | print("replace with a bot") 48 | # replace the agent and player 49 | agent = PriorityFunctions.battlerattler_priority_bot(3, EarlyGameBot) 50 | self.agents[player.name] = agent 51 | arrangement = await agent.rearrange_cards(player) 52 | player.rearrange_cards(arrangement.permutation) 53 | 54 | perform_player_action_tasks = [] 55 | for player_name, player in self.tavern.players.items(): 56 | if player.dead: 57 | continue 58 | perform_player_action_tasks.append( 59 | asyncio_utils.create_task(perform_player_actions(self.agents[player_name], player), logger=logger)) 60 | await asyncio.gather(*perform_player_action_tasks) 61 | 62 | self.tavern.combat_step() 63 | if self.tavern.game_over(): 64 | game_over_tasks = [] 65 | for position, (name, player) in enumerate(reversed(self.tavern.losers)): 66 | game_over_tasks.append(asyncio_utils.create_task(self.agents[name].game_over(player, position), logger=logger)) 67 | await asyncio.gather(*game_over_tasks) 68 | -------------------------------------------------------------------------------- /hearthstone/battlebots/priority_bot.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from hearthstone.battlebots.bot_types import PriorityFunctionBot 4 | from hearthstone.simulator.agent.actions import StandardAction, generate_standard_actions, BuyAction, EndPhaseAction, \ 5 | SummonAction, DiscoverChoiceAction, RearrangeCardsAction, FreezeDecision, TavernUpgradeAction, \ 6 | RerollAction, SellAction 7 | from hearthstone.simulator.core.player import Player, StoreIndex 8 | 9 | if typing.TYPE_CHECKING: 10 | from hearthstone.simulator.agent import StandardAction 11 | 12 | 13 | class PriorityBot(PriorityFunctionBot): 14 | async def rearrange_cards(self, player: 'Player') -> RearrangeCardsAction: 15 | permutation = list(range(len(player.in_play))) 16 | self.local_random.shuffle(permutation) 17 | return RearrangeCardsAction(permutation) 18 | 19 | async def buy_phase_action(self, player: 'Player') -> 'StandardAction': 20 | all_actions = list(generate_standard_actions(player)) 21 | 22 | upgrade_action = TavernUpgradeAction() 23 | if upgrade_action.valid(player): 24 | return upgrade_action 25 | 26 | top_hand_priority = max([self.priority(player, card) for card in player.hand], default=None) 27 | top_store_priority = max([self.priority(player, card) for card in player.store], default=None) 28 | bottom_board_priority = min([self.priority(player, card) for card in player.in_play], default=None) 29 | 30 | if top_hand_priority: 31 | if player.room_on_board(): 32 | return [action for action in all_actions if type(action) is SummonAction and self.priority(player, 33 | player.hand[ 34 | action.index]) == top_hand_priority][ 35 | 0] 36 | else: 37 | if top_hand_priority > bottom_board_priority: 38 | return [action for action in all_actions if type(action) is SellAction and self.priority(player, 39 | player.in_play[ 40 | action.index]) == bottom_board_priority][ 41 | 0] 42 | 43 | if top_store_priority: 44 | if player.room_on_board() or bottom_board_priority < top_store_priority: 45 | buy_action = BuyAction([StoreIndex(i) for i, card in enumerate(player.store) if 46 | self.priority(player, card) == top_store_priority][0]) 47 | if buy_action.valid(player): 48 | return buy_action 49 | 50 | reroll_action = RerollAction() 51 | if reroll_action.valid(player): 52 | return reroll_action 53 | 54 | return EndPhaseAction(FreezeDecision.NO_FREEZE) 55 | 56 | async def discover_choice_action(self, player: 'Player') -> DiscoverChoiceAction: 57 | discover_cards = player.discover_queue[0].items 58 | discover_cards = sorted(discover_cards, key=lambda card: self.priority(player, card), reverse=True) 59 | return DiscoverChoiceAction(player.discover_queue[0].items.index(discover_cards[0])) 60 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/worker/distributed/simulation_worker.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import random 3 | import time 4 | from typing import List 5 | 6 | import logging 7 | import torch 8 | from torch.distributed import rpc 9 | 10 | from hearthstone.asyncio import asyncio_utils 11 | from hearthstone.ladder.ladder import Contestant 12 | from hearthstone.simulator.host.async_host import AsyncHost 13 | from hearthstone.simulator.replay.annotators.final_board_annotator import FinalBoardAnnotator 14 | from hearthstone.simulator.replay.annotators.ranking_annotator import RankingAnnotator 15 | from hearthstone.simulator.replay.replay import Replay 16 | from hearthstone.training.pytorch.agents.pytorch_bot import PytorchBot 17 | from hearthstone.training.pytorch.tensorboard_altair import TensorboardAltairAnnotator 18 | from hearthstone.training.pytorch.worker.distributed.remote_net import RemoteNet, BatchedRemoteNet 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | class SimulationWorker: 23 | def __init__(self, inference_worker): 24 | self.id = rpc.get_worker_info().id 25 | self.inference_worker = inference_worker 26 | torch.set_num_threads(1) 27 | 28 | async def play_game(self, learning_bot_contestant, other_contestants, game_size): 29 | round_contestants = [learning_bot_contestant] + random.sample(other_contestants, 30 | k=game_size - 1) 31 | with torch.no_grad(): 32 | host = AsyncHost( 33 | {contestant.name: contestant.agent_generator() for contestant in round_contestants}, 34 | [RankingAnnotator(), 35 | FinalBoardAnnotator(), 36 | TensorboardAltairAnnotator([learning_bot_contestant.name])] 37 | ) 38 | await host.async_play_game() 39 | return host.get_replay() 40 | 41 | def play_interleaved_games(self, 42 | num_games: int, 43 | learning_bot_contestant: Contestant, 44 | other_contestants: List[Contestant], 45 | game_size: int) -> List[Replay]: 46 | start = time.time() 47 | 48 | async def run_games(): 49 | nets = {} 50 | for contestant in [learning_bot_contestant] + other_contestants: 51 | if contestant.agent_generator.function == PytorchBot: 52 | if type(contestant.agent_generator.kwargs['net']) is RemoteNet: 53 | if contestant.name not in nets: 54 | nets[contestant.name] = BatchedRemoteNet(contestant.name, self.inference_worker) 55 | contestant.agent_generator.kwargs['net'] = nets[contestant.name] 56 | for _, net in nets.items(): 57 | await net.start_worker() 58 | tasks = [asyncio_utils.create_task(self.play_game(learning_bot_contestant, other_contestants, game_size), logger=logger) for _ in 59 | range(num_games)] 60 | result = await asyncio.gather( 61 | *tasks) 62 | for _, net in nets.items(): 63 | await net.stop_worker() 64 | return result 65 | 66 | replays = asyncio_utils.get_or_create_event_loop().run_until_complete(run_games()) 67 | print(f"Worker played {num_games} game(s). Time taken: {time.time() - start} seconds.") 68 | return replays 69 | -------------------------------------------------------------------------------- /rust/pyrite_hearth_battles/src/warparty.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::{Ref, RefCell, RefMut}, rc::Rc}; 2 | 3 | use crate::eventtypes::EventTypes; 4 | 5 | use super::monstercard::MonsterCard; 6 | #[derive(Clone, Debug)] 7 | pub struct WarParty { 8 | attacker_index: usize, 9 | attacker_died: bool, 10 | cards: Vec>>, 11 | } 12 | 13 | impl WarParty { 14 | pub fn new(cards: Vec) -> Self { 15 | WarParty { 16 | attacker_index: 0, 17 | attacker_died: true, 18 | cards: cards.into_iter().map(|x| Rc::new(RefCell::new(x))).collect(), 19 | } 20 | } 21 | pub fn insert(&mut self, position: usize, card: MonsterCard) { 22 | self.cards.insert(position, Rc::new(RefCell::new(card))); 23 | if self.attacker_index >= position { 24 | self.attacker_index += 1; 25 | } 26 | } 27 | pub fn remove(&mut self, position: usize) -> Rc> { 28 | if self.attacker_index > position { 29 | self.attacker_index -= 1; 30 | } 31 | if self.attacker_index == position { 32 | self.attacker_died = true; 33 | } 34 | return self.cards.remove(position); 35 | } 36 | pub fn get_next_attacker_index(&mut self) -> Option { 37 | if !self.attacker_died { 38 | self.iterate_attacker_index(); 39 | } 40 | if self.cards.is_empty() { 41 | return None 42 | } 43 | let original_index = self.attacker_index; 44 | loop { 45 | if self.index_mut(self.attacker_index).cant_attack() { 46 | self.iterate_attacker_index(); 47 | } else { 48 | break 49 | } 50 | if self.attacker_index == original_index { 51 | return None 52 | } 53 | } 54 | self.attacker_died = false; 55 | return Some(self.attacker_index); 56 | } 57 | pub fn has_attacker(&self) -> bool { 58 | self.iter().any(|x| !x.cant_attack()) 59 | } 60 | pub fn iterate_attacker_index(&mut self) { 61 | self.attacker_index += 1; 62 | if self.attacker_index == self.cards.len() { 63 | self.attacker_index = 0; 64 | } 65 | } 66 | pub fn is_empty(&self) -> bool { 67 | self.cards.is_empty() 68 | } 69 | 70 | pub fn len(&self) -> usize { 71 | self.cards.len() 72 | } 73 | pub fn iter(&self) -> WarPartyIterator { 74 | return WarPartyIterator(self.cards.iter()) 75 | } 76 | 77 | pub fn index(&self, index: usize) -> Ref { 78 | self.cards[index].borrow() 79 | } 80 | 81 | pub fn index_mut(&mut self, index: usize) -> RefMut { 82 | self.cards[index].borrow_mut() 83 | } 84 | 85 | pub fn broadcast_event(&mut self, event: &EventTypes) 86 | { 87 | for card in &self.cards { 88 | card.borrow_mut().event_handler(&event); 89 | } 90 | } 91 | } 92 | 93 | pub struct WarPartyIterator<'a>(std::slice::Iter<'a,Rc>>); 94 | 95 | impl<'a> Iterator for WarPartyIterator<'a> { 96 | type Item = Ref<'a, MonsterCard>; 97 | 98 | fn next(&mut self) -> Option { 99 | match self.0.next() { 100 | None => None, 101 | Some(rc) => Some(rc.borrow()) 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /hearthstone/simulator/core/adaptations.py: -------------------------------------------------------------------------------- 1 | from inspect import isclass 2 | from typing import Type, List 3 | 4 | from hearthstone.simulator.core.cards import MonsterCard 5 | from hearthstone.simulator.core.events import CombatPhaseContext 6 | from hearthstone.simulator.core.monster_types import MONSTER_TYPES 7 | 8 | 9 | class Plant(MonsterCard): 10 | tier = 1 11 | monster_type = MONSTER_TYPES.NEUTRAL 12 | base_attack = 1 13 | base_health = 1 14 | not_in_pool = True 15 | 16 | 17 | class Adaptation: 18 | def apply(self, card: 'MonsterCard'): 19 | pass 20 | 21 | @classmethod 22 | def valid(cls, card: 'MonsterCard') -> bool: 23 | pass 24 | 25 | 26 | class AdaptBuffs: 27 | class CracklingShield(Adaptation): 28 | def apply(self, card: 'MonsterCard'): 29 | card.divine_shield = True 30 | 31 | @classmethod 32 | def valid(cls, card: 'MonsterCard') -> bool: 33 | return not card.divine_shield 34 | 35 | class FlamingClaws(Adaptation): 36 | def apply(self, card: 'MonsterCard'): 37 | card.attack += 3 38 | 39 | @classmethod 40 | def valid(cls, card: 'MonsterCard') -> bool: 41 | return True 42 | 43 | class LivingSpores(Adaptation): 44 | def apply(self, card: 'MonsterCard'): 45 | def deathrattle(self, context: 'CombatPhaseContext'): 46 | summon_index = context.friendly_war_party.get_index(self) 47 | for i in range(2 * context.summon_minion_multiplier()): 48 | plant = Plant() 49 | context.friendly_war_party.summon_in_combat(plant, context, summon_index + i + 1) 50 | 51 | card.deathrattles.append(deathrattle) 52 | 53 | @classmethod 54 | def valid(cls, card: 'MonsterCard') -> bool: 55 | return True 56 | 57 | class LightningSpeed(Adaptation): 58 | def apply(self, card: 'MonsterCard'): 59 | card.windfury = True 60 | 61 | @classmethod 62 | def valid(cls, card: 'MonsterCard') -> bool: 63 | return not card.windfury 64 | 65 | class Massive(Adaptation): 66 | def apply(self, card: 'MonsterCard'): 67 | card.taunt = True 68 | 69 | @classmethod 70 | def valid(cls, card: 'MonsterCard') -> bool: 71 | return not card.taunt 72 | 73 | class VolcanicMight(Adaptation): 74 | def apply(self, card: 'MonsterCard'): 75 | card.attack += 1 76 | card.health += 1 77 | 78 | @classmethod 79 | def valid(cls, card: 'MonsterCard') -> bool: 80 | return True 81 | 82 | class RockyCarapace(Adaptation): 83 | def apply(self, card: 'MonsterCard'): 84 | card.health += 3 85 | 86 | @classmethod 87 | def valid(cls, card: 'MonsterCard') -> bool: 88 | return True 89 | 90 | class PoisonSpit(Adaptation): 91 | def apply(self, card: 'MonsterCard'): 92 | card.poisonous = True 93 | 94 | @classmethod 95 | def valid(cls, card: 'MonsterCard') -> bool: 96 | return not card.poisonous 97 | 98 | 99 | def valid_adaptations(card: 'MonsterCard') -> List['Type']: 100 | return [adaptation for adaptation in all_adaptations() if adaptation.valid(card)] 101 | 102 | 103 | def all_adaptations() -> List['Type']: 104 | return [adaptation for adaptation in AdaptBuffs.__dict__.values() if isclass(adaptation)] 105 | -------------------------------------------------------------------------------- /hearthstone/simulator/host/round_robin_host.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import typing 3 | from typing import Dict, List, Optional 4 | 5 | from hearthstone.asyncio import asyncio_utils 6 | from hearthstone.simulator.agent.actions import EndPhaseAction 7 | from hearthstone.simulator.agent.agent import AnnotatingAgent 8 | from hearthstone.simulator.core.randomizer import Randomizer 9 | from hearthstone.simulator.host.host import Host 10 | from hearthstone.simulator.replay.observer import Observer 11 | from hearthstone.simulator.replay.replay import Replay 12 | 13 | 14 | class RoundRobinHost(Host): 15 | def __init__(self, agents: Dict[str, 'AnnotatingAgent'], 16 | observers: Optional[List['Observer']] = None, 17 | randomizer: Optional[Randomizer] = None): 18 | super().__init__(agents, observers, randomizer) 19 | 20 | def start_game(self): 21 | for player_name, player in self.tavern.players.items(): 22 | hero_choice_action = asyncio_utils.get_or_create_event_loop().run_until_complete( 23 | self.agents[player_name].hero_choice_action(player)) 24 | self._apply_and_record(player_name, hero_choice_action) 25 | 26 | def play_round_generator(self) -> typing.Generator: # TODO: think about how to test this code 27 | self.tavern.buying_step() 28 | for player_name, player in self.tavern.players.items(): 29 | agent = self.agents[player_name] 30 | for i in itertools.count(): 31 | if player.dead: 32 | break 33 | if player.discover_queue: 34 | discover_choice_action, agent_annotation = asyncio_utils.get_or_create_event_loop().run_until_complete( 35 | agent.annotated_discover_choice_action(player)) 36 | self._apply_and_record(player_name, discover_choice_action, agent_annotation) 37 | elif i > 40: 38 | break 39 | else: 40 | action, agent_annotation = asyncio_utils.get_or_create_event_loop().run_until_complete( 41 | agent.annotated_buy_phase_action(player)) 42 | self._apply_and_record(player_name, action, agent_annotation) 43 | yield 44 | if type(action) is EndPhaseAction: 45 | break 46 | if player.dead: 47 | continue 48 | if len(player.in_play) > 1: 49 | rearrange_action, agent_annotation = asyncio_utils.get_or_create_event_loop().run_until_complete( 50 | agent.annotated_rearrange_cards(player)) 51 | self._apply_and_record(player_name, rearrange_action, agent_annotation) 52 | self.tavern.combat_step() 53 | if self.tavern.game_over(): 54 | for position, (name, player) in enumerate(reversed(self.tavern.losers)): 55 | annotation = asyncio_utils.get_or_create_event_loop().run_until_complete( 56 | self.agents[name].game_over(player, position)) 57 | self.replay.agent_annotate(name, annotation) 58 | self._on_game_over() 59 | 60 | def play_round(self): 61 | for _ in self.play_round_generator(): 62 | pass 63 | 64 | def game_over(self): 65 | return self.tavern.game_over() 66 | 67 | def play_game(self): 68 | self.start_game() 69 | while not self.game_over(): 70 | self.play_round() 71 | 72 | def get_replay(self) -> Replay: 73 | return self.replay 74 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/agents/pytorch_bot.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import random 4 | from typing import Optional, Any, Dict 5 | 6 | import torch 7 | from torch import nn 8 | 9 | from hearthstone.simulator.agent.actions import StandardAction, DiscoverChoiceAction, RearrangeCardsAction, Action 10 | from hearthstone.simulator.agent.agent import AnnotatingAgent 11 | from hearthstone.training.pytorch.encoding.default_encoder import \ 12 | EncodedActionSet 13 | from hearthstone.training.common.state_encoding import State, Encoder 14 | from hearthstone.training.pytorch.replay import ActorCriticGameStepInfo 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | class PytorchBot(AnnotatingAgent): 20 | def __init__(self, net: nn.Module, encoder: Encoder, annotate: bool = True, device: Optional[torch.device] = None): 21 | self.authors = ["Jeremy Salwen"] 22 | self.net: nn.Module = net.to(device) 23 | self.encoder: Encoder = encoder 24 | self.annotate = annotate 25 | self.device = device 26 | 27 | async def async_net(self, *args, **kwargs): 28 | result = self.net(*args, **kwargs) 29 | if asyncio.iscoroutine(result): 30 | return await result 31 | else: 32 | return result 33 | 34 | async def act(self, player: 'Player', rearrange_cards: bool) -> (Action, ActorCriticGameStepInfo): 35 | with torch.no_grad(): 36 | discover_queue_empty = player.discover_queue == [] 37 | encoded_state: State = self.encoder.encode_state(player).to(self.device) 38 | valid_actions_mask: EncodedActionSet = self.encoder.encode_valid_actions(player, rearrange_cards).to( 39 | self.device) 40 | actions, action_log_probs, value, debug = await self.async_net( 41 | encoded_state.unsqueeze(), 42 | valid_actions_mask.unsqueeze(), 43 | None) 44 | assert (len(actions) == 1) 45 | action = actions[0] 46 | ac_game_step_info = None 47 | if self.annotate: 48 | ac_game_step_info = ActorCriticGameStepInfo( 49 | state=encoded_state, 50 | valid_actions=valid_actions_mask, 51 | action=action, 52 | action_log_prob=float(action_log_probs[0]), 53 | value=float(value), 54 | gae_info=None, 55 | debug=debug 56 | ) 57 | assert action.valid(player), action 58 | return action, ac_game_step_info 59 | 60 | async def annotated_buy_phase_action(self, player: 'Player') -> (StandardAction, ActorCriticGameStepInfo): 61 | action, ac_game_step_info = await self.act(player, False) 62 | assert isinstance(action, StandardAction), action 63 | return action, ac_game_step_info 64 | 65 | async def annotated_rearrange_cards(self, player: 'Player') -> (RearrangeCardsAction, ActorCriticGameStepInfo): 66 | action, ac_game_step_info = await self.act(player, True) 67 | assert isinstance(action, RearrangeCardsAction) 68 | return action, ac_game_step_info 69 | 70 | async def annotated_discover_choice_action(self, player: 'Player') -> ( 71 | DiscoverChoiceAction, ActorCriticGameStepInfo): 72 | action, ac_game_step_info = await self.act(player, False) 73 | assert isinstance(action, DiscoverChoiceAction), action 74 | return action, ac_game_step_info 75 | 76 | async def game_over(self, player: 'Player', ranking: int) -> Dict[str, Any]: 77 | return {'ranking': ranking} 78 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/worker/distributed/remote_net.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import os 4 | 5 | import torch 6 | 7 | from hearthstone.asyncio import asyncio_utils 8 | from hearthstone.training.pytorch.worker.distributed.tensorize_batch import _tensorize_batch, _untensorize_batch 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class RemoteNet: 14 | def __init__(self, net_name: str, inference_queue): 15 | self.net_name = net_name 16 | self.inference_queue = inference_queue 17 | 18 | async def __call__(self, *args): 19 | loop = asyncio.get_event_loop() 20 | f = loop.create_future() 21 | self.inference_queue.rpc_async().infer( 22 | self.net_name, args).add_done_callback( 23 | lambda fut: 24 | loop.call_soon_threadsafe( 25 | f.set_result, fut.value() 26 | ) 27 | ) 28 | return await f 29 | 30 | 31 | class BatchedRemoteNet: 32 | def __init__(self, net_name: str, inference_queue): 33 | self.net_name = net_name 34 | self.inference_queue = inference_queue 35 | 36 | self.unbatched_requests_event = asyncio.Event() 37 | self.unbatched_requests = [] 38 | self.active_rpc = None 39 | self._stop_worker = False 40 | self._worker_task = None 41 | 42 | async def __call__(self, *args): 43 | loop = asyncio.get_event_loop() 44 | f = loop.create_future() 45 | self.unbatched_requests.append((args, f)) 46 | self.unbatched_requests_event.set() 47 | return await f 48 | 49 | async def start_worker(self): 50 | assert self._worker_task is None 51 | self._stop_worker = False 52 | self._worker_task = asyncio_utils.create_task(self.worker_task(), logger=logger) 53 | 54 | async def stop_worker(self): 55 | self._stop_worker = True 56 | self.unbatched_requests_event.set() 57 | await self._worker_task 58 | 59 | async def worker_task(self): 60 | while True: 61 | await self.unbatched_requests_event.wait() 62 | if self._stop_worker: 63 | return 64 | self.unbatched_requests_event.clear() 65 | unbatched_futures = [b[1] for b in self.unbatched_requests] 66 | unbatched_args = [b[0] for b in self.unbatched_requests] 67 | self.unbatched_requests.clear() 68 | args = _tensorize_batch(unbatched_args, torch.device('cpu')) 69 | loop = asyncio.get_event_loop() 70 | f = loop.create_future() 71 | 72 | logger.debug(f"Calling RPC {os.getpid()}") 73 | self.inference_queue.rpc_async().infer(self.net_name, args).add_done_callback(lambda fut: 74 | loop.call_soon_threadsafe( 75 | f.set_result, fut.value() 76 | ) 77 | ) 78 | response = await f 79 | logger.debug(f"RPC complete {os.getpid()}") 80 | for future, result in zip( 81 | unbatched_futures, 82 | _untensorize_batch(unbatched_args, 83 | *response, 84 | torch.device('cpu'))): 85 | future.set_result(result) 86 | 87 | def to(self, device: torch.device): 88 | return self 89 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/encoding/shared_tensor_pool_encoder.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import queue 3 | from typing import List, Union 4 | 5 | import torch 6 | 7 | from hearthstone.simulator.agent.actions import StandardAction 8 | from hearthstone.simulator.core.player import Player 9 | from hearthstone.training.common.state_encoding import State, EncodedActionSet, Feature, Encoder, \ 10 | ActionComponent 11 | 12 | # A global singleton queue, since multiprocessing can't handle passing the queue through as a function argument. 13 | global_process_tensor_queue: queue.Queue = torch.multiprocessing.Queue() 14 | 15 | global_thread_tensor_queue: collections.deque = collections.deque() 16 | 17 | 18 | class SharedTensorPoolEncoder(Encoder): 19 | def __init__(self, base_encoder: Encoder, multiprocess: bool): 20 | self.base_encoder: Encoder = base_encoder 21 | self.multiprocess: bool = multiprocess 22 | # Pools of tensors to reuse 23 | self._states_pool: List[State] = [] 24 | self._valid_actions_pool: List[EncodedActionSet] = [] 25 | 26 | def _fill_from_queue(self): 27 | global global_process_tensor_queue 28 | global global_thread_tensor_queue 29 | if self._states_pool and self._valid_actions_pool: 30 | return 31 | try: 32 | if self.multiprocess: 33 | state, valid_actions = global_process_tensor_queue.get_nowait() 34 | else: 35 | state, valid_actions = global_thread_tensor_queue.popleft() 36 | except queue.Empty: 37 | return 38 | except AttributeError: 39 | return 40 | except IndexError: 41 | return 42 | self._states_pool.append(state) 43 | self._valid_actions_pool.append(valid_actions) 44 | 45 | def encode_state(self, player: Player) -> State: 46 | base_state = self.base_encoder.encode_state(player) 47 | self._fill_from_queue() 48 | if self._states_pool: 49 | reused_state = self._states_pool.pop() 50 | reused_state.player_tensor.copy_(base_state.player_tensor).detach_() 51 | reused_state.cards_tensor.copy_(base_state.cards_tensor).detach_() 52 | return reused_state 53 | else: 54 | return base_state 55 | 56 | def encode_valid_actions(self, player: Player, rearrange_phase: bool = False) -> EncodedActionSet: 57 | base_action_set: EncodedActionSet = self.base_encoder.encode_valid_actions(player, rearrange_phase) 58 | self._fill_from_queue() 59 | if self._valid_actions_pool: 60 | reused_action_set = self._valid_actions_pool.pop() 61 | reused_action_set.player_action_tensor.copy_(base_action_set.player_action_tensor).detach_() 62 | reused_action_set.card_action_tensor.copy_(base_action_set.card_action_tensor).detach_() 63 | reused_action_set.rearrange_phase.copy_(base_action_set.rearrange_phase).detach_() 64 | reused_action_set.cards_to_rearrange.copy_(base_action_set.cards_to_rearrange).detach_() 65 | return reused_action_set 66 | else: 67 | return base_action_set 68 | 69 | def player_encoding(self) -> Feature: 70 | return self.base_encoder.player_encoding() 71 | 72 | def cards_encoding(self) -> Feature: 73 | return self.base_encoder.cards_encoding() 74 | 75 | def action_encoding_size(self) -> int: 76 | return self.base_encoder.action_encoding_size() 77 | 78 | def get_action_component_index(self, action: Union[StandardAction, ActionComponent]) -> int: 79 | return self.base_encoder.get_action_component_index(action) 80 | 81 | def get_indexed_action_component(self, index: int) -> ActionComponent: 82 | return self.base_encoder.get_indexed_action_component(index) 83 | -------------------------------------------------------------------------------- /hearthstone/battlebots/hero_bot.py: -------------------------------------------------------------------------------- 1 | import random 2 | import typing 3 | from typing import List, Callable 4 | 5 | from hearthstone.simulator.agent.actions import StandardAction, generate_standard_actions, BuyAction, EndPhaseAction, \ 6 | SummonAction, DiscoverChoiceAction, RearrangeCardsAction, FreezeDecision, RerollAction, \ 7 | SellAction, TavernUpgradeAction, HeroPowerAction 8 | from hearthstone.simulator.agent.agent import Agent 9 | from hearthstone.simulator.core.player import Player, StoreIndex 10 | 11 | if typing.TYPE_CHECKING: 12 | from hearthstone.simulator.core.cards import MonsterCard 13 | 14 | 15 | class HeroBot(Agent): 16 | def __init__(self, authors: List[str], priority: Callable[['Player', 'MonsterCard'], float], seed: int): 17 | if not authors: 18 | authors = ["Jake Bumgardner", "Adam Salwen", "Ethan Saxenian"] 19 | self.authors = authors 20 | self.priority = priority 21 | self.local_random = random.Random(seed) 22 | 23 | async def rearrange_cards(self, player: 'Player') -> RearrangeCardsAction: 24 | permutation = list(range(len(player.in_play))) 25 | self.local_random.shuffle(permutation) 26 | return RearrangeCardsAction(permutation) 27 | 28 | async def buy_phase_action(self, player: 'Player') -> StandardAction: 29 | all_actions = list(generate_standard_actions(player)) 30 | 31 | if player.tavern_tier < 2: 32 | upgrade_action = TavernUpgradeAction() 33 | if upgrade_action.valid(player): 34 | return upgrade_action 35 | 36 | if not player.room_on_board(): 37 | hero_actions = [action for action in all_actions if type(action) is HeroPowerAction] 38 | if hero_actions: 39 | return self.local_random.choice(hero_actions) 40 | 41 | top_hand_priority = max([self.priority(player, card) for card in player.hand], default=None) 42 | top_store_priority = max([self.priority(player, card) for card in player.store], default=None) 43 | bottom_board_priority = min([self.priority(player, card) for card in player.in_play], default=None) 44 | 45 | if top_hand_priority: 46 | if player.room_on_board(): 47 | return [ 48 | action for action in all_actions 49 | if type(action) is SummonAction and self.priority(player, 50 | player.hand[action.index]) == top_hand_priority 51 | ][0] 52 | else: 53 | if top_hand_priority > bottom_board_priority: 54 | return [ 55 | action for action in all_actions 56 | if type(action) is SellAction and self.priority(player, player.in_play[ 57 | action.index]) == bottom_board_priority 58 | ][0] 59 | 60 | if top_store_priority: 61 | if player.room_on_board() or bottom_board_priority < top_store_priority: 62 | buy_action = BuyAction( 63 | [StoreIndex(index) for index, card in enumerate(player.store) if 64 | self.priority(player, card) == top_store_priority][0] 65 | ) 66 | if buy_action.valid(player): 67 | return buy_action 68 | 69 | reroll_action = RerollAction() 70 | if reroll_action.valid(player): 71 | return reroll_action 72 | 73 | return EndPhaseAction(FreezeDecision.NO_FREEZE) 74 | 75 | async def discover_choice_action(self, player: 'Player') -> DiscoverChoiceAction: 76 | discover_cards = player.discover_queue[0].items 77 | discover_cards = sorted(discover_cards, key=lambda card: self.priority(player, card), reverse=True) 78 | return DiscoverChoiceAction(player.discover_queue[0].items.index(discover_cards[0])) 79 | -------------------------------------------------------------------------------- /hearthstone/simulator/host/async_host.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import itertools 3 | from typing import Dict, Optional, List 4 | 5 | import logging 6 | 7 | from hearthstone.asyncio import asyncio_utils 8 | from hearthstone.simulator.agent.actions import EndPhaseAction 9 | from hearthstone.simulator.core.randomizer import Randomizer 10 | from hearthstone.simulator.host.host import Host 11 | from hearthstone.simulator.replay.replay import Replay 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | class AsyncHost(Host): 16 | 17 | def start_game(self): 18 | asyncio_utils.get_or_create_event_loop().run_until_complete(self.async_start_game()) 19 | 20 | async def async_start_game(self): 21 | async def set_player_choice(player_name, player): 22 | hero_choice = await self.agents[player_name].hero_choice_action(player) 23 | self._apply_and_record(player_name, hero_choice) 24 | 25 | player_choice_tasks = [] 26 | for player_name, player in self.tavern.players.items(): 27 | player_choice_tasks.append(asyncio_utils.create_task(set_player_choice(player_name, player), logger=logger)) 28 | await asyncio.gather(*player_choice_tasks) 29 | 30 | def play_round(self): 31 | return asyncio_utils.get_or_create_event_loop().run_until_complete(self.async_play_round()) 32 | 33 | async def async_play_round(self): 34 | self.tavern.buying_step() 35 | 36 | async def perform_player_actions(player_name, agent, player): 37 | for i in itertools.count(): 38 | if player.dead: 39 | return 40 | if player.discover_queue: 41 | discover_card_action, agent_annotation = await agent.annotated_discover_choice_action(player) 42 | self._apply_and_record(player_name, discover_card_action, agent_annotation) 43 | elif i > 40: 44 | break 45 | else: 46 | action, agent_annotation = await agent.annotated_buy_phase_action(player) 47 | self._apply_and_record(player_name, action, agent_annotation) 48 | if type(action) is EndPhaseAction: 49 | break 50 | if len(player.in_play) > 1: 51 | rearrange_action, agent_annotation = await agent.annotated_rearrange_cards(player) 52 | self._apply_and_record(player_name, rearrange_action, agent_annotation) 53 | 54 | perform_player_action_tasks = [] 55 | for player_name, player in self.tavern.players.items(): 56 | if player.dead: 57 | continue 58 | perform_player_action_tasks.append( 59 | asyncio_utils.create_task(perform_player_actions(player_name, self.agents[player_name], player), logger=logger)) 60 | await asyncio.gather(*perform_player_action_tasks) 61 | 62 | self.tavern.combat_step() 63 | if self.tavern.game_over(): 64 | async def report_game_over(name, player, position): 65 | annotation = await self.agents[name].game_over(player, position) 66 | self.replay.agent_annotate(name, annotation) 67 | 68 | game_over_tasks = [] 69 | for position, (name, player) in enumerate(reversed(self.tavern.losers)): 70 | game_over_tasks.append(asyncio_utils.create_task(report_game_over(name, player, position), logger=logger)) 71 | await asyncio.gather(*game_over_tasks) 72 | self._on_game_over() 73 | 74 | def game_over(self): 75 | return self.tavern.game_over() 76 | 77 | async def async_play_game(self): 78 | await self.async_start_game() 79 | while not self.game_over(): 80 | await self.async_play_round() 81 | 82 | def play_game(self): 83 | return asyncio_utils.get_or_create_event_loop().run_until_complete(self.async_play_game()) 84 | 85 | def get_replay(self) -> Replay: 86 | return self.replay 87 | -------------------------------------------------------------------------------- /hearthstone/training/aigym/env.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import gym 4 | from gym import spaces 5 | 6 | # TODO: Derive (some of) these constants from game data 7 | MAX_HEALTH = 50 8 | NUM_HEROES = 41 9 | HAND_SIZE = 10 10 | BOARD_SIZE = 7 11 | NUM_PLAYERS = 8 12 | NUM_MINIONS = 108 13 | NUM_TOKENS = 10 14 | # These can probably actually be unbounded... 15 | MAX_ATTACK = 10000 16 | MAX_DEFENSE = 10000 17 | NUM_MODIFIERS = 10 18 | NUM_TRIBES = 7 19 | NUM_DISCOVERS = 5 20 | MAX_TURNS = 50 21 | NUM_TIERS = 6 22 | MAX_LEVEL_COST = 11 23 | MAX_GOLD = 10 24 | MAX_DEATHRATTLES = 3 25 | 26 | 27 | def CountActions(): 28 | play_card = HAND_SIZE * (BOARD_SIZE) * (BOARD_SIZE - 1) 29 | use_hero_power = BOARD_SIZE + 1 30 | refresh = 1 31 | buy_card = BOARD_SIZE 32 | tier_up = 1 33 | play_discover = 1 34 | end_turn = 1 35 | rearrange = math.factorial(BOARD_SIZE) 36 | total_actions = (play_card + use_hero_power + refresh + buy_card + tier_up + play_discover + end_turn + rearrange) 37 | return total_actions * NUM_HEROES 38 | 39 | 40 | def card(): 41 | # Add one for empty card 42 | card_type = spaces.Discrete(NUM_MINIONS + NUM_TOKENS + NUM_DISCOVERS + 1) 43 | is_golden = spaces.Discrete(2) 44 | attack_range = spaces.Discrete(MAX_ATTACK + 1) 45 | defense_range = spaces.Discrete(MAX_DEFENSE + 1) 46 | monster_type = spaces.Discrete(NUM_TRIBES + 1) 47 | modifiers = spaces.MultiBinary(NUM_MODIFIERS + 1) 48 | extra_deathrattles = spaces.MultiDiscrete(MAX_DEATHRATTLES) 49 | return spaces.Tuple(card_type, is_golden, attack_range, defense_range, monster_type, modifiers, extra_deathrattles) 50 | 51 | 52 | class BattlegroundsEnv(gym.Env): 53 | def __init__(self): 54 | self.action_space = spaces.Discrete(CountActions()) 55 | self.observation_space = spaces.Dict() 56 | board_space = spaces.Dict() 57 | board_space["board_size"] = spaces.Discrete(BOARD_SIZE + 1) 58 | for i in range(BOARD_SIZE): 59 | board_space["board_" + i] = card() 60 | self.observation_space["board"] = board_space 61 | hero_space = spaces.Dict() 62 | hero_space["my_hero"] = spaces.Discrete(NUM_HEROES) 63 | hero_space["my_hero_health"] = spaces.Discrete(MAX_HEALTH + 1) 64 | hero_space["playing_next"] = spaces.Discrete(NUM_PLAYERS) 65 | for i in range(1, NUM_PLAYERS): 66 | hero_space["other_hero_" + i] = spaces.Discrete(NUM_HEROES) 67 | hero_space["other_hero_health_" + i] = spaces.Discrete(MAX_HEALTH + 1) 68 | hero_space["other_hero_streak_" + i] = spaces.Discrete(MAX_TURNS + 1) 69 | # This could be infinite? 70 | hero_space["other_hero_num_triples_" + i] = spaces.Discrete(MAX_TURNS * 4) 71 | hero_space["other_hero_played_0_" + i] = spaces.Discrete(NUM_PLAYERS) 72 | hero_space["other_hero_played_0_" + i] = spaces.Discrete(MAX_HEALTH * 2 + 1) 73 | hero_space["other_hero_damage_1_" + i] = spaces.Discrete(NUM_PLAYERS) 74 | hero_space["other_hero_damage_1_" + i] = spaces.Discrete(MAX_HEALTH * 2 + 1) 75 | hero_space["other_hero_tribe" + i] = spaces.Discrete(NUM_TRIBES + 1) 76 | hero_space["other_hero_tribe_count" + i] = spaces.Discrete(BOARD_SIZE + 1) 77 | hero_space["other_hero_prev_board" + i] = board_space.copy() 78 | hero_space["other_hero_last_seen" + i] = spaces.Discrete(MAX_TURNS + 1) 79 | self.observation_space["hero_info"] = hero_space 80 | hand_space = spaces.Dict() 81 | hand_space["hand_size"] = spaces.Discrete(HAND_SIZE + 1) 82 | for i in range(HAND_SIZE): 83 | hand_space["hand_" + i] = card() 84 | self.observation_space["hand"] = hand_space 85 | self.observation_space["tier"] = spaces.Discrete(NUM_TIERS) 86 | self.observation_space["level_cost"] = spaces.Discrete(MAX_LEVEL_COST + 1) 87 | self.observation_space["gold"] = spaces.MultiDiscrete([MAX_GOLD + 1, MAX_GOLD]) 88 | self.power_cost["power_cost"] = spaces.Discrete(MAX_GOLD + 1) 89 | self.observation_space["power_available"] = spaces.Discrete(2) 90 | self.observation_space["refresh_cost"] = spaces.Discrete(MAX_GOLD + 1) 91 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/networks/feedforward_net.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn.functional as F 3 | from torch import nn 4 | 5 | from hearthstone.training.pytorch.encoding.default_encoder import EncodedActionSet 6 | from hearthstone.training.common.state_encoding import State, Encoder 7 | 8 | 9 | class HearthstoneFFNet(nn.Module): 10 | def __init__(self, encoding: Encoder, hidden_layers=1, hidden_size=1024, shared=False, activation_function="gelu"): 11 | ''' This is a generic, fully connected feed-forward neural net. 12 | 13 | This is a pytorch module: https://pytorch.org/docs/master/generated/torch.nn.Module.html#torch.nn.Module 14 | 15 | Args: 16 | encoding (Encoder): [link to doc] 17 | hidden_layers (int): The number of hidden layers. 18 | hidden_size (int): The width of the hidden layers. 19 | shared (bool): Whether the policy and value NNs share the same weights in the hidden layers. 20 | activation_function (string): The activation function between layers. 21 | ''' 22 | super().__init__() 23 | input_size = encoding.player_encoding().flattened_size() + encoding.cards_encoding().flattened_size() 24 | if hidden_layers == 0: 25 | # If there are no hidden layers, just connect directly to output layers. 26 | hidden_size = input_size 27 | self.activation_function = activation_function 28 | self.shared = shared 29 | self.hidden_layers = hidden_layers 30 | self.policy_hidden_layers = [] 31 | self.value_hidden_layers = [] 32 | for i in range(hidden_layers): 33 | self.policy_hidden_layers.append(nn.Linear(input_size if i == 0 else hidden_size, hidden_size)) 34 | nn.init.orthogonal_(self.policy_hidden_layers[-1].weight) 35 | if shared: 36 | self.value_hidden_layers.append(self.policy_hidden_layers[-1]) 37 | else: 38 | # Create new hidden layers for the value network. 39 | self.value_hidden_layers.append(nn.Linear(input_size if i == 0 else hidden_size, hidden_size)) 40 | nn.init.orthogonal_(self.value_hidden_layers[-1].weight) 41 | 42 | # Output layers 43 | self.fc_policy = nn.Linear(hidden_size, encoding.action_encoding_size()) 44 | nn.init.constant_(self.fc_policy.weight, 0) 45 | nn.init.constant_(self.fc_policy.bias, 0) 46 | self.fc_value = nn.Linear(hidden_size, 1) 47 | 48 | def activation(self, x): 49 | if self.activation_function == "relu": 50 | return F.relu(x) 51 | elif self.activation_function == "gelu": 52 | return F.gelu(x) 53 | elif self.activation_function == "sigmoid": 54 | return torch.sigmoid(x) 55 | elif self.activation_function == "tanh": 56 | return torch.tanh(x) 57 | 58 | def forward(self, state: State, valid_actions: EncodedActionSet): 59 | # Because we have a fully connected NN, we can just flatten the input tensors. 60 | x_policy = torch.cat((state.player_tensor.flatten(1), state.cards_tensor.flatten(1)), dim=1) 61 | # The value network shares the input layer (for now) 62 | x_value = x_policy 63 | 64 | for i in range(self.hidden_layers): 65 | x_policy = self.activation(self.policy_hidden_layers[i](x_policy)) 66 | if self.shared: 67 | x_value = x_policy 68 | else: 69 | x_value = self.activation(self.value_hidden_layers[i](x_value)) 70 | policy = self.fc_policy(x_policy) 71 | # Disable invalid actions with a "masked" softmax 72 | valid_action_tensor = torch.cat( 73 | (valid_actions.player_action_tensor.flatten(1), valid_actions.card_action_tensor.flatten(1)), dim=1) 74 | policy = policy.masked_fill(valid_action_tensor.logical_not(), -1e30) 75 | 76 | # The policy network outputs an array of the log probability of each action. 77 | policy = F.log_softmax(policy, dim=1) 78 | # The value network outputs the linear combination of the last hidden layer. The value layer predicts the total reward at the end of the game, 79 | # which will be between -3.5 (8th place) at the minimum and 3.5 (1st place) at the max. 80 | value = self.fc_value(x_value).squeeze(1) 81 | return policy, value 82 | -------------------------------------------------------------------------------- /tests/test_combat_event_queue.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from hearthstone.simulator.core.card_pool import * 4 | from hearthstone.simulator.core.combat import WarParty 5 | from hearthstone.simulator.core.combat_event_queue import CombatEventQueue 6 | from hearthstone.simulator.core.events import EVENTS 7 | from hearthstone.simulator.core.tavern import Tavern 8 | from hearthstone.testing.battlegrounds_test_case import BattleGroundsTestCase 9 | 10 | 11 | class CombatEventQueueTest(BattleGroundsTestCase): 12 | def test_empty(self): 13 | tavern = Tavern() 14 | player_1 = tavern.add_player_with_hero("Player 1") 15 | player_2 = tavern.add_player_with_hero("Player 2") 16 | war_party_1 = WarParty(player_1) 17 | war_party_2 = WarParty(player_2) 18 | q = CombatEventQueue(war_party_1, war_party_2) 19 | self.assertTrue(q.all_empty()) 20 | self.assertTrue(q.event_empty(EVENTS.DEATHRATTLE_TRIGGERED)) 21 | self.assertTrue(q.event_empty(EVENTS.DIES)) 22 | 23 | def test_load_deathrattle(self): 24 | tavern = Tavern() 25 | player_1 = tavern.add_player_with_hero("Player 1") 26 | player_2 = tavern.add_player_with_hero("Player 2") 27 | war_party_1 = WarParty(player_1) 28 | war_party_2 = WarParty(player_2) 29 | q = CombatEventQueue(war_party_1, war_party_2) 30 | q.load_minion(EVENTS.DEATHRATTLE_TRIGGERED, war_party_1, KaboomBot()) 31 | self.assertIn(KaboomBot, [type(pair[0]) for pair in q.queues[EVENTS.DEATHRATTLE_TRIGGERED][war_party_1]]) 32 | 33 | def test_load_dies(self): 34 | tavern = Tavern() 35 | player_1 = tavern.add_player_with_hero("Player 1") 36 | player_2 = tavern.add_player_with_hero("Player 2") 37 | war_party_1 = WarParty(player_1) 38 | war_party_2 = WarParty(player_2) 39 | q = CombatEventQueue(war_party_1, war_party_2) 40 | q.load_minion(EVENTS.DIES, war_party_1, AlleyCat()) 41 | self.assertIn(AlleyCat, [type(pair[0]) for pair in q.queues[EVENTS.DIES][war_party_1]]) 42 | 43 | def test_deathrattle_queue_not_empty(self): 44 | tavern = Tavern() 45 | player_1 = tavern.add_player_with_hero("Player 1") 46 | player_2 = tavern.add_player_with_hero("Player 2") 47 | war_party_1 = WarParty(player_1) 48 | war_party_2 = WarParty(player_2) 49 | q = CombatEventQueue(war_party_1, war_party_2) 50 | q.load_minion(EVENTS.DEATHRATTLE_TRIGGERED, war_party_1, KaboomBot()) 51 | self.assertFalse(q.all_empty()) 52 | self.assertFalse(q.event_empty(EVENTS.DEATHRATTLE_TRIGGERED)) 53 | self.assertTrue(q.event_empty(EVENTS.DIES)) 54 | 55 | def test_dies_queue_not_empty(self): 56 | tavern = Tavern() 57 | player_1 = tavern.add_player_with_hero("Player 1") 58 | player_2 = tavern.add_player_with_hero("Player 2") 59 | war_party_1 = WarParty(player_1) 60 | war_party_2 = WarParty(player_2) 61 | q = CombatEventQueue(war_party_1, war_party_2) 62 | q.load_minion(EVENTS.DIES, war_party_1, AlleyCat()) 63 | self.assertFalse(q.all_empty()) 64 | self.assertFalse(q.event_empty(EVENTS.DIES)) 65 | self.assertTrue(q.event_empty(EVENTS.DEATHRATTLE_TRIGGERED)) 66 | 67 | def test_get_next_deathrattle(self): 68 | tavern = Tavern() 69 | player_1 = tavern.add_player_with_hero("Player 1") 70 | player_2 = tavern.add_player_with_hero("Player 2") 71 | war_party_1 = WarParty(player_1) 72 | war_party_2 = WarParty(player_2) 73 | q = CombatEventQueue(war_party_1, war_party_2) 74 | q.load_minion(EVENTS.DEATHRATTLE_TRIGGERED, war_party_1, KaboomBot()) 75 | tup = q.get_next_minion(EVENTS.DEATHRATTLE_TRIGGERED) 76 | self.assertIsInstance(tup[0], KaboomBot) 77 | self.assertTupleEqual((None, war_party_1, war_party_2), tup[1:]) 78 | 79 | def test_get_next_dies(self): 80 | tavern = Tavern() 81 | player_1 = tavern.add_player_with_hero("Player 1") 82 | player_2 = tavern.add_player_with_hero("Player 2") 83 | war_party_1 = WarParty(player_1) 84 | war_party_2 = WarParty(player_2) 85 | q = CombatEventQueue(war_party_1, war_party_2) 86 | q.load_minion(EVENTS.DIES, war_party_1, AlleyCat()) 87 | tup = q.get_next_minion(EVENTS.DIES) 88 | self.assertIsInstance(tup[0], AlleyCat) 89 | self.assertTupleEqual((None, war_party_1, war_party_2), tup[1:]) 90 | 91 | 92 | if __name__ == '__main__': 93 | unittest.main() 94 | -------------------------------------------------------------------------------- /hearthstone/battlebots/early_game_bot.py: -------------------------------------------------------------------------------- 1 | import random 2 | import typing 3 | from typing import List, Callable 4 | 5 | from hearthstone.simulator.agent.actions import StandardAction, generate_standard_actions, BuyAction, EndPhaseAction, \ 6 | SummonAction, \ 7 | SellAction, TavernUpgradeAction, RerollAction, DiscoverChoiceAction, RearrangeCardsAction, \ 8 | FreezeDecision 9 | from hearthstone.simulator.agent.agent import Agent 10 | from hearthstone.simulator.core.card_pool import MurlocTidehunter, AlleyCat 11 | from hearthstone.simulator.core.player import Player, StoreIndex, BoardIndex 12 | 13 | if typing.TYPE_CHECKING: 14 | from hearthstone.simulator.core.cards import MonsterCard 15 | 16 | 17 | class EarlyGameBot(Agent): 18 | def __init__(self, authors: List[str], priority: Callable[['Player', 'MonsterCard'], float], seed: int): 19 | if not authors: 20 | authors = ["Jake Bumgardner", "Adam Salwen", "Ethan Saxenian"] 21 | self.authors = authors 22 | self.priority = priority 23 | self.local_random = random.Random(seed) 24 | 25 | async def rearrange_cards(self, player: 'Player') -> RearrangeCardsAction: 26 | permutation = list(range(len(player.in_play))) 27 | self.local_random.shuffle(permutation) 28 | return RearrangeCardsAction(permutation) 29 | 30 | async def buy_phase_action(self, player: 'Player') -> StandardAction: 31 | all_actions = list(generate_standard_actions(player)) 32 | 33 | if player.tavern.turn_count == 0: 34 | token_store_index = [StoreIndex(player.store.index(card)) for card in player.store if 35 | type(card) is MurlocTidehunter or type(card) is AlleyCat] 36 | if token_store_index: 37 | buy_action = BuyAction(token_store_index[0]) 38 | if buy_action.valid(player): 39 | return buy_action 40 | 41 | if player.tavern.turn_count == 2: 42 | if player.tavern_tier == 2: 43 | token_board_index = [BoardIndex(player.in_play.index(card)) for card in player.in_play if card.token] 44 | if token_board_index and player.coins == 5: 45 | return SellAction(token_board_index[0]) 46 | 47 | if player.tavern.turn_count != 3: 48 | upgrade_action = TavernUpgradeAction() 49 | if upgrade_action.valid(player): 50 | return upgrade_action 51 | 52 | top_hand_priority = max([self.priority(player, card) for card in player.hand], default=None) 53 | top_store_priority = max([self.priority(player, card) for card in player.store], default=None) 54 | bottom_board_priority = min([self.priority(player, card) for card in player.in_play], default=None) 55 | 56 | if top_hand_priority: 57 | if player.room_on_board(): 58 | return [ 59 | action for action in all_actions 60 | if type(action) is SummonAction and self.priority(player, 61 | player.hand[action.index]) == top_hand_priority 62 | ][0] 63 | else: 64 | if top_hand_priority > bottom_board_priority: 65 | return [ 66 | action for action in all_actions 67 | if type(action) is SellAction and self.priority(player, player.in_play[ 68 | action.index]) == bottom_board_priority 69 | ][0] 70 | 71 | if top_store_priority: 72 | if player.room_on_board() or bottom_board_priority < top_store_priority: 73 | buy_action = BuyAction( 74 | [StoreIndex(index) for index, card in enumerate(player.store) if 75 | self.priority(player, card) == top_store_priority][0] 76 | ) 77 | if buy_action.valid(player): 78 | return buy_action 79 | 80 | reroll_action = RerollAction() 81 | if reroll_action.valid(player): 82 | return reroll_action 83 | 84 | return EndPhaseAction(FreezeDecision.NO_FREEZE) 85 | 86 | async def discover_choice_action(self, player: 'Player') -> DiscoverChoiceAction: 87 | discover_cards = player.discover_queue[0].items 88 | discover_cards = sorted(discover_cards, key=lambda card: self.priority(player, card), reverse=True) 89 | return DiscoverChoiceAction(player.discover_queue[0].items.index(discover_cards[0])) 90 | -------------------------------------------------------------------------------- /hearthstone/simulator/core/hero.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from typing import Union, Tuple, Optional, List, Any 3 | 4 | from hearthstone.simulator.core.cards import CardLocation 5 | from hearthstone.simulator.core.events import BuyPhaseContext, CombatPhaseContext, CardEvent, EVENTS 6 | from hearthstone.simulator.core.monster_types import MONSTER_TYPES 7 | 8 | if typing.TYPE_CHECKING: 9 | from hearthstone.simulator.core.player import BoardIndex, StoreIndex, DiscoverIndex, Player 10 | 11 | 12 | class Hero: 13 | base_power_cost: Optional[int] = None # default value is for heroes with passive hero powers 14 | hero_power_used = False 15 | can_use_power = True 16 | power_target_location: Optional[List['CardLocation']] = None 17 | multiple_power_uses_per_turn = False 18 | pool: 'MONSTER_TYPES' = MONSTER_TYPES.ALL 19 | 20 | def __init__(self): 21 | self.power_cost = self.base_power_cost 22 | self.discover_queue: List[List[Any]] = [] 23 | self.give_immunity = False 24 | 25 | def __repr__(self): 26 | return str(type(self).__name__) 27 | 28 | def starting_health(self) -> int: 29 | return 40 30 | 31 | def minion_cost(self) -> int: 32 | return 3 33 | 34 | def refresh_cost(self) -> int: 35 | return 1 36 | 37 | def tavern_upgrade_costs(self) -> Tuple[int, int, int, int, int, int]: 38 | return 0, 5, 7, 8, 9, 10 39 | 40 | def occupied_store_slots(self) -> int: 41 | return 0 42 | 43 | def occupied_hand_slots(self) -> int: 44 | return 0 45 | 46 | def handle_event(self, event: 'CardEvent', context: Union['BuyPhaseContext', 'CombatPhaseContext']): 47 | if event.event is EVENTS.BUY_END: 48 | self.give_immunity = False 49 | self.handle_event_powers(event, context) 50 | 51 | def handle_event_powers(self, event: 'CardEvent', context: Union['BuyPhaseContext', 'CombatPhaseContext']): 52 | pass 53 | 54 | def hero_power(self, context: BuyPhaseContext, board_index: Optional['BoardIndex'] = None, 55 | store_index: Optional['StoreIndex'] = None): 56 | assert self.hero_power_valid(context, board_index, store_index) 57 | context.owner.coins -= self.power_cost 58 | self.hero_power_used = True 59 | self.hero_power_impl(context, board_index, store_index) 60 | 61 | def hero_power_impl(self, context: 'BuyPhaseContext', board_index: Optional['BoardIndex'] = None, 62 | store_index: Optional['StoreIndex'] = None): 63 | pass 64 | 65 | def hero_power_valid(self, context: 'BuyPhaseContext', board_index: Optional['BoardIndex'] = None, 66 | store_index: Optional['StoreIndex'] = None): 67 | if self.power_cost is None: 68 | return False 69 | if context.owner.coins < self.power_cost: 70 | return False 71 | if not self.multiple_power_uses_per_turn: 72 | if self.hero_power_used: 73 | return False 74 | if not self.can_use_power: 75 | return False 76 | if self.power_target_location is None and (board_index is not None or store_index is not None): 77 | return False 78 | if self.power_target_location is not None: 79 | if board_index is None and store_index is None: 80 | return False 81 | if board_index is not None: 82 | if CardLocation.BOARD not in self.power_target_location or not context.owner.valid_board_index( 83 | board_index): 84 | return False 85 | if store_index is not None: 86 | if CardLocation.STORE not in self.power_target_location or not context.owner.valid_store_index( 87 | store_index): 88 | return False 89 | if not self.hero_power_valid_impl(context, board_index, store_index): 90 | return False 91 | return True 92 | 93 | def hero_power_valid_impl(self, context: 'BuyPhaseContext', board_index: Optional['BoardIndex'] = None, 94 | store_index: Optional['StoreIndex'] = None): 95 | return True 96 | 97 | def on_buy_step(self): 98 | self.hero_power_used = False 99 | 100 | def battlecry_multiplier(self) -> int: 101 | return 1 102 | 103 | def hero_info(self, player: 'Player') -> Optional[str]: 104 | return None 105 | 106 | 107 | class EmptyHero(Hero): 108 | pass 109 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/worker/distributed/tensorize_batch.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, List, Optional 2 | 3 | import torch 4 | 5 | from hearthstone.simulator.agent.actions import Action 6 | from hearthstone.training.common.state_encoding import EncodedActionSet, State 7 | from hearthstone.training.pytorch.replay import ActorCriticGameStepDebugInfo 8 | 9 | 10 | def _tensorize_batch(batch: List[Tuple[State, EncodedActionSet, Optional[List[Action]]]], 11 | device: torch.device) -> Tuple[ 12 | State, EncodedActionSet, Optional[List[Action]]]: 13 | player_tensor = torch.cat([b[0].player_tensor for b in batch], dim=0).detach() 14 | cards_tensor = torch.cat([b[0].cards_tensor for b in batch], dim=0).detach() 15 | spells_tensor = torch.cat([b[0].spells_tensor for b in batch], dim=0).detach() 16 | valid_player_actions_tensor = torch.cat( 17 | [b[1].player_action_tensor for b in batch], dim=0).detach() 18 | valid_card_actions_tensor = torch.cat( 19 | [b[1].card_action_tensor for b in batch], dim=0).detach() 20 | valid_no_target_battlecry_tensor = torch.cat([b[1].no_target_battlecry_tensor for b in batch], dim=0).detach() 21 | valid_battlecry_target_tensor = torch.cat( 22 | [b[1].battlecry_target_tensor for b in batch], dim=0).detach() 23 | valid_spell_action_tensor = torch.cat( 24 | [b[1].spell_action_tensor for b in batch], dim=0).detach() 25 | valid_no_target_spell_action_tensor = torch.cat( 26 | [b[1].no_target_spell_action_tensor for b in batch], dim=0).detach() 27 | valid_store_target_spell_action_tensor = torch.cat( 28 | [b[1].store_target_spell_action_tensor for b in batch], dim=0).detach() 29 | valid_board_target_spell_action_tensor = torch.cat( 30 | [b[1].board_target_spell_action_tensor for b in batch], dim=0).detach() 31 | rearrange_phase = torch.cat([b[1].rearrange_phase for b in batch], dim=0).detach() 32 | cards_to_rearrange = torch.cat( 33 | [b[1].cards_to_rearrange for b in batch], dim=0).detach() 34 | chosen_actions = None if batch[0][2] is None else [b[2] for b in batch] 35 | return (State(player_tensor=player_tensor.to(device), 36 | cards_tensor=cards_tensor.to(device), 37 | spells_tensor=spells_tensor.to(device)), 38 | EncodedActionSet(player_action_tensor=valid_player_actions_tensor, 39 | card_action_tensor=valid_card_actions_tensor, 40 | no_target_battlecry_tensor=valid_no_target_battlecry_tensor, 41 | battlecry_target_tensor=valid_battlecry_target_tensor, 42 | spell_action_tensor=valid_spell_action_tensor, 43 | no_target_spell_action_tensor=valid_no_target_spell_action_tensor, 44 | store_target_spell_action_tensor=valid_store_target_spell_action_tensor, 45 | board_target_spell_action_tensor=valid_board_target_spell_action_tensor, 46 | rearrange_phase=rearrange_phase, 47 | cards_to_rearrange=cards_to_rearrange, 48 | store_start=batch[0][1].store_start, 49 | hand_start=batch[0][1].hand_start, 50 | board_start=batch[0][1].board_start 51 | ).to(device), 52 | chosen_actions, 53 | ) 54 | 55 | 56 | def _untensorize_batch(batch_args: List[Tuple[State, EncodedActionSet, Optional[List[Action]]]], 57 | output_actions: List[Action], action_log_probs: torch.Tensor, value: torch.Tensor, 58 | debug_info: ActorCriticGameStepDebugInfo, device: torch.device) -> List[ 59 | Tuple[List[Action], torch.Tensor, torch.Tensor, ActorCriticGameStepDebugInfo]]: 60 | result = [] 61 | i = 0 62 | for (player_state_tensor, _, _), _, _ in batch_args: 63 | batch_entry_size = player_state_tensor.shape[0] 64 | result.append((output_actions[i:i + batch_entry_size], 65 | action_log_probs[i:i + batch_entry_size].detach().to(device), 66 | value[i:i + batch_entry_size].detach().to(device), 67 | ActorCriticGameStepDebugInfo( 68 | component_policy=debug_info.component_policy[i:i + batch_entry_size].detach().to(device), 69 | permutation_logits=debug_info.permutation_logits[i:i + batch_entry_size].detach().to(device), 70 | ) 71 | )) 72 | i += batch_entry_size 73 | 74 | return result 75 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/networks/running_norm.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from torch import nn 3 | 4 | from hearthstone.training.common.state_encoding import Encoder, State 5 | 6 | 7 | class WelfordAggregator: 8 | def __init__(self, shape: torch.Size): 9 | self.shape = shape 10 | self.count = 0 11 | self.mu = None 12 | self.m2 = None 13 | 14 | def update(self, value: torch.Tensor): 15 | if self.mu is None: 16 | self.mu = torch.zeros(self.shape, device=value.device) 17 | self.m2 = torch.zeros(self.shape, device=value.device) 18 | value = value.reshape((-1,) + self.shape) 19 | b_count = value.shape[0] 20 | b_mean = value.mean(dim=0) 21 | b_m2 = (value - b_mean).pow(2).sum(dim=0) 22 | n = self.count + b_count 23 | delta = b_mean - self.mu 24 | 25 | self.m2 = self.m2 + b_m2 + delta.pow(2) * self.count * b_count / n 26 | self.mu += delta * b_count / n 27 | self.count = n 28 | 29 | def decay(self, gamma: float): 30 | self.count *= gamma 31 | 32 | def mean(self): 33 | return self.mu 34 | 35 | def variance(self): 36 | return self.m2 / self.count 37 | 38 | def stdev(self): 39 | return torch.sqrt(self.variance()) 40 | 41 | 42 | class PPONormalizer(nn.Module): 43 | def __init__(self, shape: torch.Size, gamma: float, epsilon: float = 1e-5): 44 | """ 45 | This is the reward normalization scheme defined in https://openreview.net/pdf?id=r1etN1rtPB, Appendix A2. 46 | 47 | Note that it updates in eval mode but does not update in train mode, which is the opposite of a batch-norm layer. 48 | Args: 49 | shape (tuple): Shape of the observation tensor that we're normalizing. 50 | gamma (float): The running mean 51 | """ 52 | super().__init__() 53 | self.shape = shape 54 | self.gamma = gamma 55 | self.epsilon = epsilon 56 | self.exponential_mean = torch.zeros(shape) 57 | self.welford_aggregator = WelfordAggregator(shape) 58 | 59 | def forward(self, value: torch.Tensor): 60 | with torch.no_grad(): 61 | if not self.training: 62 | with torch.no_grad(): 63 | flattened = value.reshape((-1,) + self.shape) 64 | num_updates = flattened.shape[0] 65 | coefficients = torch.pow(self.gamma, torch.arange(num_updates)).view( 66 | (num_updates,) + (1,) * (len(flattened.shape) - 1)) 67 | self.exponential_mean = self.gamma ** num_updates * self.exponential_mean + flattened * coefficients 68 | self.welford_aggregator.update(self.exponential_mean) 69 | if self.welford_aggregator.count > 2: 70 | return value / (self.welford_aggregator.stdev() + self.epsilon) 71 | else: 72 | return value 73 | 74 | 75 | class EMANormalizer(nn.Module): 76 | def __init__(self, shape: torch.Size, gamma: float, epsilon: float = 1e-5): 77 | """ 78 | This is a raw normalizer which normalizes by a running mean/stddev. 79 | 80 | Note that it updates in eval mode but does not update in train mode, which is the opposite of a batch-norm layer. 81 | Args: 82 | shape (tuple): Shape of the observation tensor that we're normalizing. 83 | gamma (float): The running mean 84 | """ 85 | super().__init__() 86 | self.shape = shape 87 | self.gamma = gamma 88 | self.epsilon = epsilon 89 | self.exponential_mean = torch.zeros(shape) 90 | self.welford_aggregator = WelfordAggregator(shape) 91 | 92 | def forward(self, value: torch.Tensor): 93 | with torch.no_grad(): 94 | if not self.training: 95 | self.welford_aggregator.decay(self.gamma) 96 | self.welford_aggregator.update(value) 97 | if self.welford_aggregator.count > 2: 98 | return value - self.welford_aggregator.mean() / (self.welford_aggregator.stdev() + self.epsilon) 99 | else: 100 | return value 101 | 102 | 103 | class ObservationNormalizer(nn.Module): 104 | def __init__(self, encoding: Encoder, gamma: float): 105 | super().__init__() 106 | self.player_normalizer = EMANormalizer(torch.Size(encoding.player_encoding().size()), gamma) 107 | self.cards_normalizer = EMANormalizer(torch.Size(encoding.cards_encoding().size()[1:]), gamma) 108 | 109 | def forward(self, state: State): 110 | return State(player_tensor=self.player_normalizer(state.player_tensor), 111 | cards_tensor=self.cards_normalizer(state.cards_tensor) 112 | ) 113 | -------------------------------------------------------------------------------- /tensorboard_vega_embed/tensorboard_vega_embed/plugin.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google Inc. All Rights Reserved. 2 | # Copyright 2019 The TensorFlow Authors. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # ============================================================================== 16 | 17 | """A Plugin for displaying Vega/Vega lite embedded charts.""" 18 | 19 | from __future__ import absolute_import 20 | from __future__ import division 21 | from __future__ import print_function 22 | 23 | import json 24 | import os 25 | 26 | import six 27 | import werkzeug 28 | from tensorboard.plugins import base_plugin 29 | from tensorboard.util import tensor_util 30 | from tensorboard_vega_embed import metadata 31 | from werkzeug import wrappers 32 | from werkzeug.middleware.dispatcher import DispatcherMiddleware 33 | from werkzeug.middleware.shared_data import SharedDataMiddleware 34 | 35 | 36 | class VegaEmbedXPlugin(base_plugin.TBPlugin): 37 | plugin_name = metadata.PLUGIN_NAME 38 | 39 | def __init__(self, context): 40 | """Instantiates VegaEmbedXPlugin. 41 | 42 | Args: 43 | context: A base_plugin.TBContext instance. 44 | """ 45 | self._multiplexer = context.multiplexer 46 | self._static_path = os.path.join((os.path.dirname(__file__)), "svelte_frontend", "public") 47 | self._svelte_path = f'/data/plugin/{self.plugin_name}/static' 48 | 49 | def is_active(self): 50 | """Returns whether there is relevant data for the plugin to process. 51 | 52 | When there are no runs with greeting data, TensorBoard will hide the 53 | plugin from the main navigation bar. 54 | """ 55 | return bool( 56 | self._multiplexer.PluginRunToTagToContent(metadata.PLUGIN_NAME) 57 | ) 58 | 59 | def get_plugin_apps(self): 60 | return { 61 | "/*": DispatcherMiddleware( 62 | self.not_found, 63 | { 64 | self._svelte_path: SharedDataMiddleware( 65 | self.not_found, { 66 | "/": self._static_path 67 | }), 68 | self._svelte_path + "/tags": self._serve_tags, 69 | self._svelte_path + "/plot_specs": self._serve_plot_specs, 70 | }) 71 | } 72 | 73 | def frontend_metadata(self): 74 | return base_plugin.FrontendMetadata(es_module_path="/static/index.js") 75 | 76 | @wrappers.Request.application 77 | def _serve_tags(self, request): 78 | del request # unused 79 | mapping = self._multiplexer.PluginRunToTagToContent( 80 | metadata.PLUGIN_NAME 81 | ) 82 | result = {run: {} for run in self._multiplexer.Runs()} 83 | for (run, tag_to_content) in six.iteritems(mapping): 84 | for tag in tag_to_content: 85 | summary_metadata = self._multiplexer.SummaryMetadata(run, tag) 86 | result[run][tag] = { 87 | u"description": summary_metadata.summary_description, 88 | } 89 | contents = json.dumps(result, sort_keys=True) 90 | return werkzeug.Response(contents, content_type="application/json") 91 | 92 | @wrappers.Request.application 93 | def _serve_plot_specs(self, request): 94 | run = request.args.get("run") 95 | tag = request.args.get("tag") 96 | if run is None or tag is None: 97 | raise werkzeug.exceptions.BadRequest("Must specify run and tag") 98 | try: 99 | data = [ 100 | { 101 | "step": event.step, 102 | "vega_spec": tensor_util.make_ndarray(event.tensor_proto).item().decode("utf-8") 103 | } 104 | for event in self._multiplexer.Tensors(run, tag) 105 | ] 106 | except KeyError: 107 | raise werkzeug.exceptions.BadRequest("Invalid run or tag") 108 | contents = json.dumps(data, sort_keys=True) 109 | return werkzeug.Response(contents, content_type="application/json") 110 | 111 | @wrappers.Request.application 112 | def not_found(self, request): 113 | print(request.full_path) 114 | del request 115 | return werkzeug.exceptions.NotFound() 116 | -------------------------------------------------------------------------------- /hearthstone/training/pytorch/worker/distributed/inference_worker.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import logging 3 | import threading 4 | import time 5 | from typing import Dict 6 | 7 | import torch 8 | from torch import nn 9 | from torch.distributed import rpc 10 | 11 | from hearthstone.training.pytorch.worker.distributed.tensorize_batch import _tensorize_batch, _untensorize_batch 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class InferenceWorker: 17 | def __init__(self, max_batch_size: int, num_inference_threads: int, device): 18 | self.id = rpc.get_worker_info().id 19 | 20 | self.max_batch_size = max_batch_size 21 | self.num_inference_threads = num_inference_threads 22 | self.device = device 23 | 24 | self.nets: Dict[str, nn.Module] = {} 25 | self.queued_tasks_by_name = collections.defaultdict(list) 26 | 27 | self.inference_example_count = 0 28 | self.inference_count = 0 29 | 30 | # These are the only variables accessed from multiple threads. 31 | self.communication_queue = collections.deque() 32 | self.communication_event = threading.Event() 33 | self.done_event = threading.Event() 34 | 35 | self.inference_thread_lock = threading.Lock() 36 | 37 | def set_nets(self, nets: Dict[str, nn.Module]): 38 | self.nets = nets 39 | for name, net in nets.items(): 40 | net.to(self.device) 41 | 42 | @rpc.functions.async_execution 43 | def infer(self, net_name: str, args): 44 | future = rpc.Future() 45 | self.communication_queue.append((net_name, future, args)) 46 | self.communication_event.set() 47 | return future 48 | 49 | def _unload_communication_queue(self): 50 | logger.debug("unloading queue size {}".format(len(self.communication_queue))) 51 | while self.communication_queue: 52 | net_name, future, args = self.communication_queue.popleft() 53 | self.queued_tasks_by_name[net_name].append((future, args)) 54 | logger.debug("queued task size {} {}".format(len(self.queued_tasks_by_name), 55 | sum([len(v) for k, v in self.queued_tasks_by_name.items()]))) 56 | 57 | def _worker_thread(self): 58 | while True: 59 | with self.inference_thread_lock: 60 | self.communication_event.clear() 61 | self._unload_communication_queue() 62 | # Select the longest queue 63 | if self.queued_tasks_by_name: 64 | net_name, _ = max(self.queued_tasks_by_name.items(), 65 | key=lambda kv: len(kv[1])) 66 | tasks = self.queued_tasks_by_name.pop(net_name) 67 | # Remove the first batch worth from the net specific queue 68 | length = min(len(tasks), self.max_batch_size) 69 | batched_tasks = [tasks.pop() for _ in range(length)] 70 | self.queued_tasks_by_name[net_name] += tasks 71 | else: 72 | length = 0 73 | if length: 74 | # Run inference on batched tensor 75 | batch_args = [args for _, args in batched_tasks] 76 | t = time.time() 77 | state_batch, valid_actions_batch, chosen_actions_batch = _tensorize_batch(batch_args 78 | , self.device) 79 | self.inference_count += 1 80 | self.inference_example_count += state_batch[0].shape[0] 81 | 82 | logger.debug("Inference #{}: {} requests, {} total batch size, {} average batch size".format( 83 | self.inference_count, len(batched_tasks), 84 | state_batch[0].shape[0], 85 | float(self.inference_example_count) / self.inference_count)) 86 | 87 | net = self.nets[net_name] 88 | output_actions, action_log_probs, value, debug_info = net(state_batch, valid_actions_batch, 89 | chosen_actions_batch) 90 | for (future, _), unbatched in zip( 91 | batched_tasks, 92 | _untensorize_batch(batch_args, output_actions, action_log_probs, value, debug_info, 93 | torch.device('cpu'))): 94 | future.set_result(unbatched) 95 | logger.debug(f"Time taken is {time.time() - t}") 96 | self.communication_event.wait(1) 97 | if self.done_event.is_set(): 98 | return 99 | 100 | def start_worker_thread(self): 101 | for _ in range(self.num_inference_threads): 102 | inference_thread = threading.Thread(target=self._worker_thread) 103 | inference_thread.start() 104 | 105 | def kill_worker_thread(self): 106 | self.done_event.set() 107 | -------------------------------------------------------------------------------- /hearthstone/battlebots/stochastic_priority_bot.py: -------------------------------------------------------------------------------- 1 | import json 2 | import random 3 | import typing 4 | from collections import defaultdict 5 | from typing import List 6 | 7 | from hearthstone.simulator.agent.actions import RearrangeCardsAction, StandardAction, generate_standard_actions, \ 8 | TavernUpgradeAction, SummonAction, SellAction, BuyAction, RerollAction, EndPhaseAction, FreezeDecision, \ 9 | DiscoverChoiceAction 10 | from hearthstone.simulator.agent.agent import Agent 11 | 12 | if typing.TYPE_CHECKING: 13 | from hearthstone.simulator.core.player import Player, StoreIndex 14 | 15 | 16 | class LearnedPriorityBot(Agent): 17 | 18 | def __init__(self, authors: List[str], rand_factor: float, seed: int): 19 | if not authors: 20 | authors = ["Jeremy Salwen"] 21 | self.authors = authors 22 | self.priority_dict = defaultdict(lambda: 0) 23 | self.priority = None 24 | self.set_priority_function() 25 | self.local_random = random.Random(seed) 26 | self.rand_factor = rand_factor 27 | self.current_game_cards = defaultdict(lambda: 0) 28 | 29 | def learn_from_game(self, place: int): 30 | for card, score in self.current_game_cards.items(): 31 | self.priority_dict[card] += (3 - place) * score 32 | 33 | self.current_game_cards = defaultdict(lambda: 0) 34 | 35 | def set_priority_function(self): 36 | self.priority = lambda player, card: self.priority_dict[type(card).__name__] 37 | 38 | def save_to_file(self, path): 39 | with open(path, "w") as f: 40 | json.dump(self.priority_dict, f) 41 | 42 | def read_from_file(self, path): 43 | with open(path) as f: 44 | self.priority_dict.update(json.load(f)) 45 | self.set_priority_function() 46 | 47 | async def rearrange_cards(self, player: 'Player') -> RearrangeCardsAction: 48 | permutation = list(range(len(player.in_play))) 49 | self.local_random.shuffle(permutation) 50 | return RearrangeCardsAction(permutation) 51 | 52 | def adjusted_priority(self, player, card): 53 | score = self.priority(player, card) 54 | num_existing = len([existing for existing in player.hand + player.in_play if 55 | type(existing) == type(card) and not existing.golden]) 56 | if num_existing == 2: 57 | score += 100000 58 | elif num_existing == 1: 59 | score += 300 60 | score += 100 * (card.health + card.attack + card.tier) 61 | return score 62 | 63 | async def buy_phase_action(self, player: 'Player') -> StandardAction: 64 | all_actions = list(generate_standard_actions(player)) 65 | 66 | if player.tavern_tier < 2: 67 | upgrade_action = TavernUpgradeAction() 68 | if upgrade_action.valid(player): 69 | return upgrade_action 70 | 71 | top_hand_priority = max([self.adjusted_priority(player, card) for card in player.hand], default=None) 72 | top_store_priority = max([self.adjusted_priority(player, card) for card in player.store], default=None) 73 | bottom_board_priority = min([self.adjusted_priority(player, card) for card in player.in_play], default=None) 74 | 75 | if top_hand_priority is not None: 76 | if player.room_on_board(): 77 | return [action for action in all_actions if 78 | type(action) is SummonAction and self.adjusted_priority(player, 79 | action.card) == top_hand_priority][0] 80 | else: 81 | return [action for action in all_actions if 82 | type(action) is SellAction and self.adjusted_priority(player, player.in_play[ 83 | action.index]) == bottom_board_priority][0] 84 | 85 | if top_store_priority is not None: 86 | force_buy = False 87 | if self.local_random.random() < self.rand_factor: 88 | top_store_priority = self.adjusted_priority(player, self.local_random.choice(player.store)) 89 | force_buy = True 90 | if player.room_on_board() or bottom_board_priority < top_store_priority or force_buy: 91 | buy_action = BuyAction([StoreIndex(i) for i, card in enumerate(player.store) if 92 | self.priority(player, card) == top_store_priority][0]) 93 | if buy_action.valid(player): 94 | self.current_game_cards[type(buy_action.card).__name__] += 3 95 | for card in player.store: 96 | self.current_game_cards[type(card).__name__] -= 1 97 | return buy_action 98 | 99 | reroll_action = RerollAction() 100 | if reroll_action.valid(player): 101 | return reroll_action 102 | 103 | return EndPhaseAction(FreezeDecision.NO_FREEZE) 104 | 105 | async def discover_choice_action(self, player: 'Player') -> DiscoverChoiceAction: 106 | discover_cards = player.discover_queue[0].items 107 | discover_cards = sorted(discover_cards, key=lambda card: self.adjusted_priority(card), reverse=True) 108 | return DiscoverChoiceAction(player.discover_queue[0].items.index(discover_cards[0])) 109 | --------------------------------------------------------------------------------