├── domrl ├── __init__.py ├── agents │ ├── __init__.py │ ├── big_money_agent.py │ ├── priority_agent.py │ └── provincial_agent.py └── engine │ ├── __init__.py │ ├── cards │ ├── __init__.py │ └── base.py │ ├── trigger.py │ ├── util.py │ ├── effect.py │ ├── supply.py │ ├── game.py │ ├── state_view.py │ ├── card.py │ ├── agent.py │ ├── state.py │ ├── state_funcs.py │ ├── logger.py │ └── decision.py ├── tests └── engine │ ├── cards │ └── test_base.py │ ├── test_state_funcs.py │ └── test_state.py ├── nb ├── .ipynb_checkpoints │ └── RunGames-checkpoint.ipynb └── RunGames.ipynb ├── .gitignore ├── examples ├── play_vs_big_money.py └── provincial_vs_big_money.py ├── setup.py ├── LICENSE └── README.md /domrl/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /domrl/agents/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /domrl/engine/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /domrl/engine/cards/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/engine/cards/test_base.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nb/.ipynb_checkpoints/RunGames-checkpoint.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [], 3 | "metadata": {}, 4 | "nbformat": 4, 5 | "nbformat_minor": 4 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__/** 2 | *.py[cod] # Will match .pyc, .pyo and .pyd files. 3 | .DS_Store 4 | .idea/** 5 | .pytest_cache/** 6 | build/** 7 | dist/** 8 | DomRL.egg-info/** -------------------------------------------------------------------------------- /domrl/engine/trigger.py: -------------------------------------------------------------------------------- 1 | import domrl.engine.logger as log 2 | 3 | 4 | class Trigger: 5 | def condition(self, event: log.Event): 6 | raise NotImplementedError('`Trigger` must be subclassed to have condition') 7 | 8 | def apply(self, event: log.Event, state): 9 | raise NotImplementedError('`Trigger` must be subclassed to be applied') 10 | -------------------------------------------------------------------------------- /domrl/engine/util.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class CardType(Enum): 4 | VICTORY = 1 5 | TREASURE = 2 6 | ACTION = 3 7 | REACTION = 4 8 | ATTACK = 5 9 | CURSE = 6 10 | DURATION = 7 11 | NIGHT = 8 12 | 13 | class TurnPhase(Enum): 14 | ACTION_PHASE = 1 15 | TREASURE_PHASE = 2 16 | BUY_PHASE = 3 17 | END_PHASE = 4 18 | -------------------------------------------------------------------------------- /domrl/engine/effect.py: -------------------------------------------------------------------------------- 1 | class Effect(object): 2 | """ 3 | An effect is essentially a function, applied to a particular state and player. 4 | 5 | Cards will usually execute a series of effects, with the target player being 6 | the player that played the card. 7 | 8 | Must implement: 9 | run(state, player) -> None 10 | """ 11 | def run(self, state, player): 12 | raise Exception("Effect does not implement run!") 13 | -------------------------------------------------------------------------------- /examples/play_vs_big_money.py: -------------------------------------------------------------------------------- 1 | from domrl.engine.game import Game 2 | from domrl.engine.agent import StdinAgent 3 | from domrl.agents.big_money_agent import BigMoneyAgent 4 | import domrl.engine.cards.base as base 5 | 6 | 7 | if __name__ == '__main__': 8 | """ 9 | Run instances of the game. 10 | """ 11 | game = Game( 12 | agents=[StdinAgent(), BigMoneyAgent()], 13 | kingdoms=[base.BaseKingdom], 14 | ) 15 | game.run() 16 | 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="DomRL", 5 | version="0.1.0", 6 | description='Dominion simulation framework tailored to building agents that have the exact information set as ' 7 | 'humans in online play.', 8 | packages=find_packages(), 9 | license='MIT', 10 | install_requires=['numpy'], 11 | 12 | author="Ben Zhang", 13 | author_email="frenzybenzy@gmail.com", 14 | keywords="dominion game reinforcement-learning simulation gym framework", 15 | ) -------------------------------------------------------------------------------- /examples/provincial_vs_big_money.py: -------------------------------------------------------------------------------- 1 | from domrl.engine.game import Game 2 | from domrl.engine.agent import StdinAgent 3 | from domrl.agents.big_money_agent import BigMoneyAgent 4 | from domrl.agents.provincial_agent import ProvincialAgent 5 | import domrl.engine.cards.base as base 6 | 7 | 8 | if __name__ == '__main__': 9 | """ 10 | Run instances of the game. 11 | """ 12 | game = Game( 13 | agents=[ProvincialAgent(), BigMoneyAgent()], 14 | kingdoms=[base.BaseKingdom], 15 | ) 16 | game.run() 17 | print("Good") 18 | -------------------------------------------------------------------------------- /domrl/engine/supply.py: -------------------------------------------------------------------------------- 1 | import random 2 | from .card import * 3 | 4 | 5 | class SupplyPile(object): 6 | def __init__(self, card, qty, buyable=True): 7 | self.card = card 8 | self.qty = qty 9 | self.buyable = buyable 10 | 11 | def __str__(self): 12 | return str(self.card) 13 | 14 | 15 | def choose_supply_from_kingdoms(kingdoms): 16 | total_piles = {} 17 | for kingdom_piles in kingdoms: 18 | total_piles.update(kingdom_piles) 19 | 20 | keys = total_piles.keys() 21 | 22 | supply_keys = random.sample(keys, 10) 23 | 24 | supply_piles = { 25 | "Curse": SupplyPile(Curse, 10), 26 | "Estate": SupplyPile(Estate, 8), 27 | "Duchy": SupplyPile(Duchy, 8), 28 | "Province": SupplyPile(Province, 8), 29 | "Copper": SupplyPile(Copper, 46), 30 | "Silver": SupplyPile(Silver, 30), 31 | "Gold": SupplyPile(Gold, 16), 32 | } 33 | 34 | for key in supply_keys: 35 | supply_piles[key] = total_piles[key] 36 | 37 | return supply_piles 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | Copyright (c) 2020 Ben Zhang 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /tests/engine/test_state_funcs.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from domrl.engine.supply import SupplyPile 4 | from domrl.engine.state import Player, GameState 5 | from domrl.engine.card import * 6 | 7 | class TestStateFuncs(unittest.TestCase): 8 | def setUp(self): 9 | player1 = Player( 10 | name="kuroba", 11 | idx=0, 12 | agent=None, 13 | vp=0, 14 | actions=0, 15 | coins=3, 16 | buys=0, 17 | draw_pile=[Estate, Copper, Province], 18 | discard_pile=[Silver], 19 | hand=[Gold, Province, Duchy, Curse], 20 | play_area=[], 21 | ) 22 | 23 | player2 = Player( 24 | name="xtruffles", 25 | idx=0, 26 | agent=None, 27 | vp=3, 28 | actions=0, 29 | coins=3, 30 | buys=0, 31 | draw_pile=[Estate, Copper, Province], 32 | discard_pile=[Silver], 33 | hand=[Gold, Province], 34 | play_area=[], 35 | ) 36 | 37 | self.state = GameState( 38 | agents=None, 39 | players=[player1, player2], 40 | preset_supply={ 41 | "Curse": SupplyPile(Curse, 0), 42 | "Estate": SupplyPile(Estate, 8), 43 | "Duchy": SupplyPile(Duchy, 8), 44 | "Province": SupplyPile(Province, 8), 45 | "Copper": SupplyPile(Copper, 0), 46 | "Silver": SupplyPile(Silver, 30), 47 | "Gold": SupplyPile(Gold, 0), 48 | } 49 | ) 50 | pass 51 | 52 | def tearDown(self): 53 | pass 54 | 55 | def test_draw_cards(self): 56 | pass -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DomRL 2 | 3 | DomRL is a simulation environment for the card game Dominion, created by Donald X Vaccarino, meant to simplify the development and testing of various AI strategies, specifically Reinforcement Learning algorithms. 4 | 5 | The goal is to make an engine that stores a serializable state of the game and exposes a comprehensive state object to agents, which contain all the information that human players would have. This improved state management will be helpful for building more complex AI agents, beyond simple priority logic supported by most agents. 6 | 7 | The engine currently supports all cards in Base Dominion. 8 | 9 | ## Usage 10 | 11 | If you want to play from the command line, simply run: 12 | 13 | ``` 14 | python main.py 15 | ``` 16 | 17 | Currently, this runs two `StdinAgent` instances against each other, so you can choose all the moves from command line. 18 | 19 | If you want to run a game from a Jupyter notebook, between two separate agents, the following should suffice: 20 | 21 | ``` 22 | import engine.game.Game 23 | 24 | Game(YourAgent(...), YourAgent(...)).run() 25 | ``` 26 | 27 | ## Writing a Bot 28 | 29 | Implement an agent, which is a class derived from `engine.agent.Agent` that implements 30 | 31 | ``` 32 | policy(self, state_view, decision) -> List 33 | ``` 34 | 35 | where the output is the list of indices corresponding to the moves taken in this decision object. 36 | 37 | ## Upcoming Features 38 | 39 | The following features are necessary before this project can reach an acceptable state for proper RL agent development. 40 | 41 | - Testing for correctness. 42 | - Baseline bots (such as big money) 43 | - Undo feature (mostly for human convenience) 44 | - Vector encoding interface for decisions. 45 | - Improved logging (currently do not log draws), and unambiguous events (topdecking from hand vs topdecking from supply pile). 46 | -------------------------------------------------------------------------------- /domrl/agents/big_money_agent.py: -------------------------------------------------------------------------------- 1 | from ..engine.agent import Agent 2 | from ..engine.decision import * 3 | 4 | import numpy as np 5 | from typing import Union 6 | 7 | PlayDecision = Union[ActionPhaseDecision, TreasurePhaseDecision] 8 | 9 | 10 | def find_card_in_decision(decision, card_name): 11 | if isinstance(decision, PlayDecision.__args__): 12 | for idx, move in enumerate(decision.moves): 13 | if hasattr(move, 'card') and move.card.name == card_name: 14 | return [idx] 15 | elif isinstance(decision, BuyPhaseDecision): 16 | for idx, move in enumerate(decision.moves): 17 | if hasattr(move, 'card_name') and move.card_name == card_name: 18 | return [idx] 19 | return [0] 20 | 21 | 22 | def get_minimum_coin_card(decision): 23 | card_coins = [c.coins for c in decision.moves.card] 24 | return [np.argmin(card_coins)] 25 | 26 | 27 | class BigMoneyAgent(Agent): 28 | def policy(self, decision, state_view): 29 | if not decision.optional and len(decision.moves) == 1: 30 | return [0] 31 | if decision.optional and len(decision.moves) == 0: 32 | return [] 33 | 34 | if decision.prompt == 'Select a card to trash from enemy Bandit.' or \ 35 | decision.prompt == 'Discard down to 3 cards.': 36 | return get_minimum_coin_card(decision) 37 | 38 | if state_view.player.phase == TurnPhase.TREASURE_PHASE: 39 | return [1] 40 | 41 | if state_view.player.phase == TurnPhase.BUY_PHASE: 42 | all_cards_money = [c.coins for c in state_view.player.previous_deck] \ 43 | or [0] 44 | hand_money_ev = np.mean(all_cards_money) * 5 45 | 46 | if state_view.player.coins >= 8 and hand_money_ev > 8: 47 | return find_card_in_decision(decision, 'Province') 48 | elif state_view.player.coins >= 6: 49 | return find_card_in_decision(decision, 'Gold') 50 | elif state_view.player.coins >= 3: 51 | return find_card_in_decision(decision, 'Silver') 52 | 53 | return [0] 54 | -------------------------------------------------------------------------------- /domrl/agents/priority_agent.py: -------------------------------------------------------------------------------- 1 | from ..engine.agent import Agent 2 | from ..engine.decision import * 3 | 4 | import numpy as np 5 | from typing import Union 6 | 7 | PlayDecision = Union[ActionPhaseDecision, TreasurePhaseDecision] 8 | 9 | 10 | def find_card_in_decision(decision, card_name): 11 | if isinstance(decision, PlayDecision.__args__): 12 | for idx, move in enumerate(decision.moves): 13 | if hasattr(move, 'card') and move.card.name == card_name: 14 | return [idx] 15 | elif isinstance(decision, BuyPhaseDecision): 16 | for idx, move in enumerate(decision.moves): 17 | if hasattr(move, 'card_name') and move.card_name == card_name: 18 | return [idx] 19 | return [0] 20 | 21 | 22 | def get_minimum_coin_card(decision): 23 | card_coins = [c.coins for c in decision.moves.card] 24 | return [np.argmin(card_coins)] 25 | 26 | 27 | class PriorityAgent(Agent): 28 | """ 29 | Doesn't currently work right now... 30 | """ 31 | def __init__(self, card_priorities): 32 | self.card_priorities = card_priorities 33 | 34 | def policy(self, decision, state_view): 35 | if not decision.optional and len(decision.moves) == 1: 36 | return [0] 37 | if decision.optional and len(decision.moves) == 0: 38 | return [] 39 | 40 | if decision.prompt == 'Select a card to trash from enemy Bandit.' or \ 41 | decision.prompt == 'Discard down to 3 cards.': 42 | return get_minimum_coin_card(decision) 43 | 44 | if state_view.player.phase == TurnPhase.TREASURE_PHASE: 45 | return [1] 46 | 47 | if state_view.player.phase == TurnPhase.BUY_PHASE: 48 | all_cards_money = [c.coins for c in state_view.player.previous_deck] \ 49 | or [0] 50 | hand_money_ev = np.mean(all_cards_money) * 5 51 | 52 | if state_view.player.coins >= 8 and hand_money_ev > 8: 53 | return find_card_in_decision(decision, 'Province') 54 | elif state_view.player.coins >= 6: 55 | return find_card_in_decision(decision, 'Gold') 56 | elif state_view.player.coins >= 3: 57 | return find_card_in_decision(decision, 'Silver') 58 | 59 | return [0] 60 | -------------------------------------------------------------------------------- /domrl/engine/game.py: -------------------------------------------------------------------------------- 1 | import domrl.engine.state as st 2 | from .decision import * 3 | from .util import TurnPhase 4 | from .state_view import StateView 5 | import pandas as pd 6 | 7 | class Game(object): 8 | def __init__(self, agents, players=None, kingdoms=None, verbose=True): 9 | self.state = st.GameState(agents, players=players, kingdoms=kingdoms, verbose=verbose) 10 | self.agents = agents 11 | 12 | def run(self, print_logs=False): 13 | state = self.state 14 | 15 | while not state.is_game_over(): 16 | 17 | player = state.current_player 18 | agent = player.agent 19 | decision = None 20 | 21 | # Determine the phase, and decision. 22 | if player.phase == TurnPhase.ACTION_PHASE: 23 | decision = ActionPhaseDecision(player) 24 | elif player.phase == TurnPhase.TREASURE_PHASE: 25 | decision = TreasurePhaseDecision(player) 26 | elif player.phase == TurnPhase.BUY_PHASE: 27 | decision = BuyPhaseDecision(state, player) 28 | elif player.phase == TurnPhase.END_PHASE: 29 | decision = EndPhaseDecision(player) 30 | else: 31 | raise Exception("TurnContext: Unknown current player phase") 32 | 33 | # Print state of the board. 34 | process_decision(agent, decision, state) 35 | 36 | return self.state 37 | 38 | def get_log(self): 39 | return self.state.event_log 40 | 41 | def get_result_df(self): 42 | state = self.state 43 | winners_names = [p.name for p in state.get_winners()] 44 | dicts = [] 45 | for i, player in enumerate(state.players): 46 | player_dict = {} 47 | player_dict['Player'] = player.name 48 | player_dict['VP'] = player.total_vp() 49 | player_dict['Turns'] = player.turns 50 | player_dict['Winner'] = player.name in winners_names 51 | dicts.append(player_dict) 52 | return pd.DataFrame(dicts) 53 | 54 | def print_result(self): 55 | state = self.state 56 | winners = state.get_winners() 57 | if len(winners) == 1: 58 | print(f'The winner is {winners[0]}!') 59 | else: 60 | print("It's a tie!") 61 | 62 | vps = [player.total_vp() for player in state.players] 63 | vp_messages = ['Player {}: {} VP'.format(i+1, v) 64 | for i, v in enumerate(vps)] 65 | for message in vp_messages: 66 | print(message) 67 | -------------------------------------------------------------------------------- /domrl/engine/state_view.py: -------------------------------------------------------------------------------- 1 | class PlayerView(object): 2 | def __init__(self, player, is_player: bool): 3 | # Public player members. 4 | self.name = player.name 5 | self.idx = player.idx 6 | self.vp_tokens = player.vp_tokens 7 | self.total_vp = player.total_vp() 8 | self.actions = player.actions 9 | self.coins = player.coins 10 | self.buys = player.buys 11 | self.play_area = [card.name for card in player.play_area] 12 | self.phase = player.phase 13 | 14 | # Only visible to the player, not his opponents. 15 | self.discard_pile = [card.name for card in player.discard_pile] \ 16 | if is_player else [None for card in player.discard_pile] 17 | self.hand = [card.name for card in player.hand] \ 18 | if is_player else [None for card in player.discard_pile] 19 | # added to allow for full provincial functionality 20 | self.draw_pile = [card.name for card in player.draw_pile] \ 21 | if is_player else [None for card in player.draw_pile] 22 | 23 | # added to allow for full provincial functionality 24 | self.all_cards = [card for card in player.discard_pile] + \ 25 | [card for card in player.hand] + \ 26 | [card for card in player.draw_pile] 27 | 28 | # TODO(benzyx): do we really want this? 29 | self.previous_deck = player.previous_deck if is_player else \ 30 | [None for card in player.previous_deck] 31 | 32 | # Never public to player view, but should have some partial information about. 33 | self.discard_pile_size = len(player.discard_pile) 34 | self.hand_size = len(player.hand) 35 | self.draw_pile_size = len(player.draw_pile) 36 | 37 | # self.draw_pile_contents = {} 38 | # for card in player.draw_pile: 39 | # if card.name not in draw_pile_contents: 40 | # draw_pile_contents[card.name] = 0 41 | # draw_pile_contents[card.name] += 1 42 | 43 | def to_dict(self): 44 | raise NotImplemented("Too lazy for this right now") 45 | 46 | 47 | class SupplyPileView(object): 48 | def __init__(self, supply_pile): 49 | self.card_name = supply_pile.card.name 50 | self.qty = supply_pile.qty 51 | self.buyable = supply_pile.buyable 52 | 53 | def to_dict(self): 54 | return { 55 | "card_name": self.card_name, 56 | "qty": self.qty, 57 | "buyable": self.buyable, 58 | } 59 | 60 | 61 | class StateView(object): 62 | """ 63 | The StateView object is what we are going to pass to an agent before they make a decision. 64 | 65 | It needs to hide the aspects of State that should not be exposed to the agent. 66 | """ 67 | 68 | def __init__(self, state, player): 69 | self.supply_piles = {} 70 | for name, pile in state.supply_piles.items(): 71 | self.supply_piles[name] = SupplyPileView(pile) 72 | self.player = PlayerView(player, True) 73 | self.other_players = [PlayerView(opp, False) for opp in state.other_players(player)] 74 | self.events = state.event_log.hide_for_player(player) 75 | self.trash = [card.name for card in state.trash] 76 | -------------------------------------------------------------------------------- /domrl/engine/card.py: -------------------------------------------------------------------------------- 1 | from typing import List, Callable 2 | from .effect import Effect 3 | from .state_funcs import draw_into_hand 4 | from .util import CardType 5 | 6 | class Card(object): 7 | """ 8 | Base class for all cards. 9 | """ 10 | 11 | def __init__(self, 12 | name: str, 13 | types: List[CardType], 14 | cost: int, 15 | vp_constant: int = 0, 16 | coins: int = 0, 17 | add_cards: int = 0, 18 | add_actions: int = 0, 19 | add_buys: int = 0, 20 | add_vp: int = 0, 21 | effect_list: List[Effect] = [], 22 | effect_fn: Callable = None, 23 | vp_fn: Callable = None, 24 | global_trigger = None, 25 | ): 26 | self.name = name 27 | self.types = types 28 | self.cost = cost 29 | self.vp_constant = vp_constant 30 | self.coins = coins 31 | self.add_cards = add_cards 32 | self.add_actions = add_actions 33 | self.add_buys = add_buys 34 | self.add_vp = add_vp 35 | self.effect_list = effect_list 36 | self.effect_fn = effect_fn 37 | self.vp_fn = vp_fn 38 | self.global_trigger = global_trigger 39 | 40 | def __str__(self) -> str: 41 | return self.name 42 | 43 | """ 44 | TODO(benzyx): handle cost modifiers (such as bridge). 45 | """ 46 | 47 | def get_cost(self) -> int: 48 | return self.cost 49 | 50 | def is_type(self, card_type: CardType) -> bool: 51 | return card_type in self.types 52 | 53 | def is_card(self, card: str) -> bool: 54 | return card == self.name 55 | 56 | 57 | def play(self, state, player): 58 | """ 59 | The entry point for card play effect. 60 | - state: State of the Game 61 | - player: Player who played the card. 62 | """ 63 | 64 | # Draw cards 65 | draw_into_hand(state, player, self.add_cards) 66 | 67 | # Increment actions 68 | player.actions += self.add_actions 69 | 70 | # Increment Buys 71 | player.buys += self.add_buys 72 | 73 | # Increment Coins 74 | player.coins += self.coins 75 | 76 | for effect in self.effect_list: 77 | effect.run(state, player) 78 | 79 | if self.effect_fn: 80 | self.effect_fn(state, player) 81 | 82 | 83 | """ 84 | Implementations of base treasure and cards. 85 | """ 86 | Copper = Card( 87 | name="Copper", 88 | types=[CardType.TREASURE], 89 | cost=0, 90 | coins=1) 91 | 92 | Silver = Card( 93 | name="Silver", 94 | types=[CardType.TREASURE], 95 | cost=3, 96 | coins=2) 97 | 98 | Gold = Card( 99 | name="Gold", 100 | types=[CardType.TREASURE], 101 | cost=6, 102 | coins=3) 103 | 104 | Curse = Card( 105 | name="Curse", 106 | types=[CardType.VICTORY], 107 | cost=0, 108 | vp_constant=-1) 109 | 110 | Estate = Card( 111 | name="Estate", 112 | types=[CardType.VICTORY], 113 | cost=2, 114 | vp_constant=1) 115 | 116 | Duchy = Card( 117 | name="Duchy", 118 | types=[CardType.VICTORY], 119 | cost=5, 120 | vp_constant=3) 121 | 122 | Province = Card( 123 | name="Province", 124 | types=[CardType.VICTORY], 125 | cost=8, 126 | vp_constant=6) 127 | -------------------------------------------------------------------------------- /domrl/engine/agent.py: -------------------------------------------------------------------------------- 1 | import domrl.engine.logger as log 2 | from .decision import * 3 | 4 | class Agent(object): 5 | def policy(self, decision, state_view): 6 | return decision.moves[0] 7 | 8 | 9 | class StdinAgent(Agent): 10 | """ 11 | StdinAgent is an agent that reads from Stdin for its policy. 12 | 13 | It is used for humans to test games against bots. 14 | """ 15 | def policy(self, decision, state_view): 16 | 17 | # Autoplay 18 | if not decision.optional and len(decision.moves) == 1: 19 | return [0] 20 | if decision.optional and len(decision.moves) == 0: 21 | return [] 22 | 23 | player = decision.player 24 | 25 | # Get user input and process it. 26 | # TODO(benzyx): Refactor this into helper functions. 27 | choices = None 28 | while True: 29 | self.initial_prompt(decision) 30 | self.helper_prompt(decision) 31 | 32 | user_input = input() 33 | 34 | if user_input == "?": 35 | # Replay log. 36 | print("=== Replaying log from beginning ===") 37 | print(log.dict_log_to_string(state_view.events)) 38 | print("=== end ===") 39 | 40 | # Print the state of the board (quantities of piles). 41 | for _, pile in state_view.supply_piles.items(): 42 | print(f"{pile.card_name}: {pile.qty} remaining.") 43 | else: 44 | if user_input == "": 45 | choices = [] 46 | else: 47 | choices = [int(x.strip()) for x in user_input.split(',')] 48 | 49 | if self.check_choice_validity(decision, choices): 50 | break 51 | 52 | return choices 53 | 54 | def initial_prompt(self, decision): 55 | player = decision.player 56 | print(f" ==== Decision to be made by {player} ==== ") 57 | print(f"Actions: {player.actions} | Buys: {player.buys} | Coins: {player.coins}") 58 | print(f"Hand: {str(c) for c in player.hand}") 59 | print(decision.prompt) 60 | 61 | for idx, move in enumerate(decision.moves): 62 | print(f"{idx}: {move}") 63 | 64 | def helper_prompt(self, decision): 65 | print("Enter '?' to look at state and logs.") 66 | # Prompt how to make decision. 67 | if decision.optional: 68 | print(f"Make up to {decision.num_select} choice(s), each separated by a comma, or enter for no " 69 | f"selection.\n> ", end="") 70 | else: 71 | print(f"Make exactly {decision.num_select} choice(s), each separated by a comma, or enter for no " 72 | f"selection.\n> ", end="") 73 | 74 | def check_choice_validity(self, decision, choices) -> bool: 75 | valid = True 76 | if not decision.optional and len(choices) != decision.num_select: 77 | valid = False 78 | print(f"Must make exactly {decision.num_select} choices, {len(choices)} given.") 79 | if decision.optional and len(choices) > decision.num_select: 80 | valid = False 81 | print(f"Must make at most {decision.num_select} choices, {len(choices)} given.") 82 | 83 | used_choices = set() 84 | for choice in choices: 85 | if not (0 <= choice < len(decision.moves)): 86 | valid = False 87 | print(f"Decision index {choice} out of bounds.") 88 | if choice in used_choices: 89 | valid = False 90 | print(f"Decision index {choice} repeated.") 91 | used_choices.add(choice) 92 | return valid 93 | 94 | -------------------------------------------------------------------------------- /tests/engine/test_state.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from domrl.engine.supply import SupplyPile 4 | from domrl.engine.state import Player, GameState 5 | from domrl.engine.card import * 6 | from domrl.engine.util import TurnPhase 7 | 8 | class TestPlayer(unittest.TestCase): 9 | def test_create_default_player(self): 10 | player = Player("Lord Rattington", 0, None) 11 | self.assertEqual(player.total_vp(), 3) 12 | self.assertEqual(len(player.all_cards), 10) 13 | 14 | def test_create_complex_player(self): 15 | player = Player( 16 | name="Lord Rattington", 17 | idx=0, 18 | agent=None, 19 | vp=4, 20 | actions=0, 21 | coins=3, 22 | buys=0, 23 | draw_pile=[Estate, Copper, Province], 24 | discard_pile=[Silver], 25 | hand=[Gold, Province, Duchy, Curse], 26 | play_area=[], 27 | phase=TurnPhase.BUY_PHASE, 28 | ) 29 | self.assertEqual(player.total_vp(), 19) 30 | self.assertEqual(len(player.all_cards), 8) 31 | 32 | player.init_turn() 33 | self.assertEqual(player.actions, 1) 34 | self.assertEqual(player.buys, 1) 35 | self.assertEqual(player.coins, 0) 36 | self.assertEqual(player.phase, TurnPhase.ACTION_PHASE) 37 | 38 | 39 | class TestGameState(unittest.TestCase): 40 | def setUp(self): 41 | pass 42 | 43 | def tearDown(self): 44 | pass 45 | 46 | def test_basic_state(self): 47 | state = GameState( 48 | agents=[None, None], 49 | preset_supply={"Province": SupplyPile(Province, 0)}) 50 | 51 | self.assertEqual(state.current_player_idx, 0) 52 | self.assertTrue(state.is_game_over()) 53 | self.assertTrue(len(state.get_winners()), 2) 54 | 55 | state.next_player_turn() 56 | # Handle tiebreak on turns. 57 | self.assertTrue(len(state.get_winners()), 1) 58 | self.assertEqual(state.get_winners()[0].name, "Player 2") 59 | 60 | def test_advanced_state(self): 61 | player1 = Player( 62 | name="kuroba", 63 | idx=0, 64 | agent=None, 65 | vp=0, 66 | actions=0, 67 | coins=3, 68 | buys=0, 69 | draw_pile=[Estate, Copper, Province], 70 | discard_pile=[Silver], 71 | hand=[Gold, Province, Duchy, Curse], 72 | play_area=[], 73 | ) 74 | 75 | player2 = Player( 76 | name="xtruffles", 77 | idx=0, 78 | agent=None, 79 | vp=3, 80 | actions=0, 81 | coins=3, 82 | buys=0, 83 | draw_pile=[Estate, Copper, Province], 84 | discard_pile=[Silver], 85 | hand=[Gold, Province], 86 | play_area=[], 87 | ) 88 | 89 | state = GameState( 90 | agents=None, 91 | players=[player1, player2], 92 | preset_supply={ 93 | "Curse": SupplyPile(Curse, 0), 94 | "Estate": SupplyPile(Estate, 8), 95 | "Duchy": SupplyPile(Duchy, 8), 96 | "Province": SupplyPile(Province, 8), 97 | "Copper": SupplyPile(Copper, 0), 98 | "Silver": SupplyPile(Silver, 30), 99 | "Gold": SupplyPile(Gold, 0), 100 | } 101 | ) 102 | 103 | self.assertTrue(state.is_game_over()) 104 | self.assertEqual(state.get_winners(), [player2]) 105 | self.assertEqual(state.other_players(player1), [player2]) 106 | self.assertEqual(state.other_players(player2), [player1]) 107 | -------------------------------------------------------------------------------- /domrl/engine/state.py: -------------------------------------------------------------------------------- 1 | from random import shuffle 2 | from .util import TurnPhase 3 | from .supply import choose_supply_from_kingdoms 4 | from .card import * 5 | import domrl.engine.cards.base as base 6 | import domrl.engine.logger as log 7 | 8 | 9 | class Player(object): 10 | """ 11 | Player State Object. 12 | """ 13 | def __init__(self, 14 | name, 15 | idx, 16 | agent, 17 | vp=None, 18 | actions=None, 19 | coins=None, 20 | buys=None, 21 | draw_pile=None, 22 | discard_pile=None, 23 | hand=None, 24 | play_area=None, 25 | phase=None, 26 | ): 27 | self.name = name 28 | self.idx = idx 29 | self.agent = agent 30 | self.vp_tokens = vp or 0 31 | self.actions = actions or 0 32 | self.coins = coins or 0 33 | self.buys = buys or 0 34 | self.draw_pile = draw_pile or \ 35 | [Copper for _ in range(7)] + [Estate for _ in range(3)] 36 | self.discard_pile = discard_pile or [] 37 | self.hand = hand or [] 38 | self.play_area = play_area or [] 39 | self.phase = phase or TurnPhase.END_PHASE 40 | self.turns = 0 41 | self.immune_to_attack = False 42 | 43 | # TODO: I don't think this is necessary. 44 | self.previous_deck = [] 45 | 46 | # TODO: Move this to game start or something. 47 | shuffle(self.draw_pile) 48 | 49 | def __eq__(self, other): 50 | return self.idx == other.idx 51 | 52 | def __str__(self): 53 | return self.name 54 | 55 | def init_turn(self): 56 | self.actions = 1 57 | self.buys = 1 58 | self.coins = 0 59 | self.phase = TurnPhase.ACTION_PHASE 60 | self.turns += 1 61 | 62 | @property 63 | def all_cards(self): 64 | return self.hand + self.play_area + self.draw_pile + self.discard_pile 65 | 66 | def total_vp(self): 67 | total = self.vp_tokens 68 | for card in self.all_cards: 69 | total += card.vp_constant 70 | 71 | # TODO(benzyx): vp_fn really shouldn't take all_cards... 72 | if card.vp_fn: 73 | total += card.vp_fn(self.all_cards) 74 | return total 75 | 76 | 77 | class GameState(object): 78 | """ 79 | Keeps track of the game state. 80 | """ 81 | 82 | def __init__(self, 83 | agents=None, 84 | players=None, 85 | turn=None, 86 | current_player_idx=None, 87 | preset_supply=None, 88 | kingdoms=None, 89 | verbose=True): 90 | self.trash = [] 91 | self.event_log = log.EventLog(verbose) 92 | self.turn = turn or 0 93 | self.players = players or [Player(f"Player {i+1}", i, agent) for i, agent in enumerate(agents)] 94 | self.current_player_idx = current_player_idx or 0 95 | self.turn_triggers = [] 96 | self.global_triggers = [] 97 | 98 | """ 99 | TODO(benzyx): Make supply piles handle mixed piles. 100 | """ 101 | self.supply_piles = preset_supply or choose_supply_from_kingdoms(kingdoms) 102 | 103 | for _, pile in self.supply_piles.items(): 104 | if pile.card.global_trigger: 105 | self.global_triggers.append(pile.card.global_trigger) 106 | 107 | if players is None: 108 | for player in self.players: 109 | draw_into_hand(self, player, 5) 110 | 111 | self.players[0].init_turn() 112 | 113 | def __str__(self): 114 | """ 115 | TODO(benzyx): This sucks right now. Should integrate this with StateView. 116 | """ 117 | ret = "" 118 | # Supply piles. 119 | for name, pile in self.supply_piles.items(): 120 | ret += f"Supply pile contains {name}, {pile.qty} remaining.\n" 121 | 122 | # Who's turn it is. 123 | ret += f"{self.current_player.name}'s turn!" 124 | 125 | return ret 126 | 127 | @property 128 | def current_player(self): 129 | return self.players[self.current_player_idx] 130 | 131 | def next_player_turn(self): 132 | self.current_player_idx = self.next_player_idx(self.current_player_idx) 133 | 134 | def next_player_idx(self, idx): 135 | return (idx + 1) % len(self.players) 136 | 137 | @property 138 | def all_players(self): 139 | return self.players 140 | 141 | def other_players(self, player): 142 | ret = [] 143 | idx = self.next_player_idx(player.idx) 144 | while idx != player.idx: 145 | ret.append(self.players[idx]) 146 | idx = self.next_player_idx(idx) 147 | return ret 148 | 149 | def empty_piles(self): 150 | pile_outs = [] 151 | for _, pile in self.supply_piles.items(): 152 | if pile.qty == 0: 153 | pile_outs.append(pile) 154 | return pile_outs 155 | 156 | def is_game_over(self): 157 | province_pileout = self.supply_piles["Province"].qty == 0 158 | pileout_count = len(self.empty_piles()) 159 | return pileout_count >= 3 or province_pileout 160 | 161 | def get_winners(self): 162 | winners = [] 163 | best_vp = self.players[0].total_vp() 164 | best_turns = self.players[0].turns 165 | 166 | for player in self.players: 167 | if player.total_vp() > best_vp: 168 | winners = [player] 169 | best_vp = player.total_vp() 170 | best_turns = player.turns 171 | elif player.total_vp() == best_vp: 172 | if best_turns > player.turns: 173 | best_turns = player.turns 174 | winners = [player] 175 | elif best_turns == player.turns: 176 | winners.append(player) 177 | 178 | return winners 179 | 180 | -------------------------------------------------------------------------------- /domrl/engine/state_funcs.py: -------------------------------------------------------------------------------- 1 | from .util import CardType 2 | from random import shuffle 3 | import domrl.engine.logger as log 4 | 5 | 6 | def process_event(state, event): 7 | """ 8 | This function is the entrypoint for all new events that are processed. 9 | """ 10 | state.event_log.add_event(event) 11 | 12 | # Process all reaction triggers. 13 | for trigger in state.global_triggers: 14 | if trigger.condition(event): 15 | trigger.apply(event, state) 16 | 17 | # Process all turn triggers (Merchant, Goons, etc). 18 | for trigger in state.turn_triggers: 19 | if trigger.condition(event): 20 | trigger.apply(event, state) 21 | 22 | 23 | # The following functions are the entry points for most card operations. 24 | 25 | def trash(state, player, card, container): 26 | process_event(state, log.TrashEvent(player, card)) 27 | card_idx = container.index(card) 28 | container.pop(card_idx) 29 | state.trash.append(card) 30 | 31 | 32 | def discard(state, player, card, container): 33 | process_event(state, log.DiscardEvent(player, card)) 34 | card_idx = container.index(card) 35 | container.pop(card_idx) 36 | player.discard_pile.append(card) 37 | 38 | 39 | def topdeck(state, player, card, container): 40 | process_event(state, log.TopdeckEvent(player, card)) 41 | card_idx = container.index(card) 42 | container.pop(card_idx) 43 | player.draw_pile.append(card) 44 | 45 | 46 | def play_inplace(state, player, card): 47 | """ 48 | Play a card without moving it. 49 | 50 | Activates the effect of the card. 51 | Wraps the card effect with a EnterContext and ExitContext event. 52 | """ 53 | process_event(state, log.PlayEvent(player, card)) 54 | process_event(state, log.EnterContext()) 55 | card.play(state, player) 56 | process_event(state, log.ExitContext()) 57 | 58 | 59 | def play(state, player, card, container): 60 | """ 61 | Play a card normally: 62 | 1. Move card to play area. 63 | 2. Activate effect of card (calls play_inplace). 64 | """ 65 | card_idx = container.index(card) 66 | container.pop(card_idx) 67 | player.play_area.append(card) 68 | play_inplace(state, player, card) 69 | 70 | 71 | def reveal(state, player, card): 72 | process_event(state, log.RevealEvent(player, card)) 73 | 74 | # These next functions are for drawing cards. 75 | 76 | 77 | def _cycle_shuffle(state, player): 78 | """ 79 | Moves all cards from discard pile to draw_pile, and shuffles it. 80 | """ 81 | process_event(state, log.ShuffleEvent(player)) 82 | 83 | assert (len(player.draw_pile) == 0) 84 | player.draw_pile = player.discard_pile 85 | player.previous_deck = player.discard_pile 86 | player.discard_pile = [] 87 | shuffle(player.draw_pile) 88 | 89 | 90 | def draw_one(state, player, draw_event): 91 | if len(player.draw_pile) == 0: 92 | _cycle_shuffle(state, player) 93 | 94 | if len(player.draw_pile) >= 1: 95 | card = player.draw_pile.pop() 96 | if draw_event: 97 | process_event(state, log.DrawEvent(player, card)) 98 | return card 99 | else: 100 | return None 101 | 102 | 103 | def draw(state, player, num, draw_event=True): 104 | cards = [] 105 | for i in range(num): 106 | card = draw_one(state, player, draw_event) 107 | if card: 108 | cards.append(card) 109 | return cards 110 | 111 | 112 | def draw_into_hand(state, player, num): 113 | cards = draw(state, player, num) 114 | player.hand.extend(cards) 115 | 116 | 117 | def clean_up(state, player): 118 | player.discard_pile.extend(player.play_area) 119 | player.discard_pile.extend(player.hand) 120 | player.play_area = [] 121 | player.hand = [] 122 | draw_into_hand(state, player, 5) 123 | 124 | 125 | def end_turn(state): 126 | clean_up(state, state.current_player) 127 | 128 | # Reset all Triggers for this turn (such as Merchant). 129 | state.turn_triggers = [] 130 | 131 | process_event(state, log.EndTurnEvent(state.current_player)) 132 | 133 | if state.is_game_over(): 134 | return 135 | 136 | state.next_player_turn() 137 | if state.current_player_idx == 0: 138 | state.turn += 1 139 | 140 | state.current_player.init_turn() 141 | 142 | """ 143 | These apply the effect to a card in a player's hand. 144 | 145 | The follow (essential) functions will mutate state. 146 | """ 147 | 148 | def play_card_from_hand(state, player, card): 149 | """ Play card from hand """ 150 | if card.is_type(CardType.ACTION): 151 | assert (player.actions > 0) 152 | player.actions -= 1 153 | 154 | play(state, player, card, player.hand) 155 | 156 | 157 | def gain_card_to_discard(state, player, pile): 158 | if pile.qty > 0: 159 | process_event(state, log.GainEvent(player, pile.card)) 160 | player.discard_pile.append(pile.card) 161 | pile.qty -= 1 162 | 163 | 164 | def gain_card_to_hand(state, player, pile): 165 | if pile.qty > 0: 166 | process_event(state, log.GainEvent(player, pile.card)) 167 | player.hand.append(pile.card) 168 | pile.qty -= 1 169 | 170 | 171 | def gain_card_to_topdeck(state, player, pile): 172 | if pile.qty > 0: 173 | process_event(state, log.GainEvent(player, pile.card)) 174 | process_event(state, log.EnterContext()) 175 | process_event(state, log.TopdeckEvent(player, pile.card)) 176 | player.draw_pile.append(pile.card) 177 | pile.qty -= 1 178 | process_event(state, log.ExitContext()) 179 | 180 | 181 | def buy_card(state, player, card_name): 182 | """ 183 | TODO (henry-prior): assert statements break execution, can we handle w 184 | a return to decision context and warning? (will only come up when human 185 | is playing) 186 | """ 187 | assert (card_name in state.supply_piles) 188 | assert (state.supply_piles[card_name].qty > 0) 189 | 190 | pile = state.supply_piles[card_name] 191 | card = pile.card 192 | 193 | assert (player.coins >= card.cost) 194 | player.coins -= card.cost 195 | player.buys -= 1 196 | process_event(state, log.BuyEvent(player, card)) 197 | process_event(state, log.EnterContext()) 198 | gain_card_to_discard(state, player, pile) 199 | process_event(state, log.ExitContext()) 200 | 201 | 202 | 203 | def player_discard_card_from_hand(state, player, card): 204 | discard(state, player, card, player.hand) 205 | 206 | 207 | def player_trash_card_from_hand(state, player, card): 208 | trash(state, player, card, player.hand) 209 | -------------------------------------------------------------------------------- /domrl/engine/logger.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | import logging 3 | 4 | class EventType(Enum): 5 | CUSTOM = 0 6 | BUY = 1 7 | GAIN = 2 8 | PLAY = 3 9 | DRAW = 4 10 | DISCARD = 5 11 | TRASH = 6 12 | TOPDECK = 7 13 | REVEAL = 8 14 | SHUFFLE = 9 15 | CONTEXT = 10 16 | ENDTURN = 11 17 | 18 | def get_action_word(event_type: EventType) -> str: 19 | if event_type == EventType.BUY: 20 | return "buys" 21 | if event_type == EventType.GAIN: 22 | return "gains" 23 | if event_type == EventType.PLAY: 24 | return "plays" 25 | if event_type == EventType.DRAW: 26 | return "draws" 27 | if event_type == EventType.DISCARD: 28 | return "discards" 29 | if event_type == EventType.TRASH: 30 | return "trashes" 31 | if event_type == EventType.TOPDECK: 32 | return "topdecks" 33 | if event_type == EventType.REVEAL: 34 | return "reveals" 35 | return "[undefined action]" 36 | 37 | 38 | class Event(object): 39 | # By default, do not hide the action at all. 40 | def obfuscate(self, player): 41 | return self 42 | 43 | def to_dict(self): 44 | raise NotImplementedError("Event does not implement to_dict") 45 | 46 | 47 | class CardEvent(Event): 48 | def __init__(self, event_type, player, card): 49 | self.event_type = event_type 50 | self.player = player 51 | self.card = card 52 | 53 | def __str__(self): 54 | action_word = get_action_word(self.event_type) 55 | if self.card: 56 | return f"{self.player.name} {action_word} a {self.card}." 57 | else: 58 | return f"{self.player.name} {action_word} a card." 59 | 60 | def to_dict(self): 61 | return { 62 | "type": self.event_type.name, 63 | "player": self.player.name, 64 | "card": self.card.name if self.card else None, 65 | "str": str(self), 66 | } 67 | 68 | 69 | class BuyEvent(CardEvent): 70 | def __init__(self, player, card): 71 | super().__init__(EventType.BUY, player, card) 72 | 73 | 74 | class GainEvent(CardEvent): 75 | def __init__(self, player, card): 76 | super().__init__(EventType.GAIN, player, card) 77 | 78 | 79 | class PlayEvent(CardEvent): 80 | def __init__(self, player, card): 81 | super().__init__(EventType.PLAY, player, card) 82 | 83 | 84 | class DrawEvent(CardEvent): 85 | def __init__(self, player, card): 86 | super().__init__(EventType.DRAW, player, card) 87 | 88 | # Other players should not see the card being drawn. 89 | def obfuscate(self, player): 90 | if player != self.player: 91 | return DrawEvent(self.player, None) 92 | else: 93 | return self 94 | 95 | 96 | class DiscardEvent(CardEvent): 97 | """ 98 | TODO(benzyx): Actually need to obfuscate some discards... 99 | """ 100 | def __init__(self, player, card): 101 | super().__init__(EventType.DISCARD, player, card) 102 | 103 | 104 | class TrashEvent(CardEvent): 105 | def __init__(self, player, card): 106 | super().__init__(EventType.TRASH, player, card) 107 | 108 | 109 | class TopdeckEvent(CardEvent): 110 | def __init__(self, player, card): 111 | super().__init__(EventType.TOPDECK, player, card) 112 | 113 | 114 | class RevealEvent(CardEvent): 115 | def __init__(self, player, card): 116 | super().__init__(EventType.REVEAL, player, card) 117 | 118 | 119 | class ShuffleEvent(Event): 120 | def __init__(self, player): 121 | self.event_type = EventType.SHUFFLE 122 | self.player = player 123 | 124 | def __str__(self): 125 | return f"{self.player} shuffles their deck." 126 | 127 | def to_dict(self): 128 | return { 129 | "type": EventType.SHUFFLE.name, 130 | "player": self.player.name, 131 | "str": str(self), 132 | } 133 | 134 | 135 | class EnterContext(Event): 136 | def __init__(self): 137 | self.event_type = EventType.CONTEXT 138 | self.value = 1 139 | 140 | def to_dict(self): 141 | return { 142 | "type": self.event_type.name, 143 | "value": 1, 144 | } 145 | 146 | 147 | class ExitContext(Event): 148 | def __init__(self): 149 | self.event_type = EventType.CONTEXT 150 | self.value = -1 151 | 152 | def to_dict(self): 153 | return { 154 | "type": self.event_type.name, 155 | "value": -1, 156 | } 157 | 158 | class EndTurnEvent(Event): 159 | def __init__(self, player): 160 | self.event_type = EventType.ENDTURN 161 | self.player = player 162 | 163 | def __str__(self): 164 | return f"{self.player.name} ends their turn." 165 | 166 | def to_dict(self): 167 | return { 168 | "type": self.event_type.name, 169 | "str": str(self), 170 | } 171 | 172 | def dict_log_to_string(event_dict_list): 173 | """ 174 | This function will be used to easily print out a list of Events in dict form. 175 | """ 176 | log_str = "" 177 | context_level = 0 178 | for event in event_dict_list: 179 | if event['type'] == EventType.CONTEXT.name: 180 | context_level += event["value"] 181 | else: 182 | log_str += " " * context_level 183 | log_str += event['str'] 184 | log_str += "\n" 185 | return log_str 186 | 187 | 188 | class EventLog(object): 189 | """ 190 | Event log. 191 | """ 192 | def __init__(self, verbose): 193 | self.events = [] 194 | self.context_level = 0 195 | self.verbose = verbose 196 | 197 | def add_event(self, event): 198 | self.events.append(event) 199 | 200 | if event.event_type == EventType.CONTEXT: 201 | self.context_level += event.value 202 | else: 203 | if self.verbose: 204 | for i in range(self.context_level): 205 | print(" ", end="") 206 | print(event) 207 | 208 | def hide_for_player(self, player): 209 | return [event.obfuscate(player).to_dict() for event in self.events] 210 | 211 | def to_string(self): 212 | return dict_log_to_string(self.to_dict_log()) 213 | 214 | def to_dict_log(self): 215 | return [event.to_dict() for event in self.events] 216 | 217 | def print(self): 218 | """ 219 | This function will be used to easily print out a list of Events in dict form. 220 | """ 221 | print("=== Replaying log from beginning ===") 222 | print(self.to_string()) 223 | print("=== end ===") 224 | -------------------------------------------------------------------------------- /domrl/engine/decision.py: -------------------------------------------------------------------------------- 1 | from .util import * 2 | from .state_funcs import * 3 | import domrl.engine.state_view as stv 4 | 5 | def process_decision(agent, decision, state): 6 | 7 | # Create a StateView object, to hide information in state from the agent. 8 | state_view = stv.StateView(state, decision.player) 9 | 10 | # Get decision from agent, giving them the view of the state. 11 | move_indices = agent.policy(decision, state_view) 12 | 13 | # TODO(benzyx): Enforce that the indices are not repeated. 14 | if decision.optional: 15 | if len(move_indices) > decision.num_select: 16 | raise Exception("Decision election error! Too many moves selected.") 17 | else: 18 | if len(move_indices) != decision.num_select: 19 | raise Exception("Decision election error! Number of moves selected not correct.") 20 | 21 | for idx in move_indices: 22 | decision.moves[idx].do(state) 23 | 24 | 25 | class Move(object): 26 | """ 27 | An Move is an Action that a player can take. 28 | 29 | Must implement do(game_state), which is called when the player selects that Move. 30 | """ 31 | 32 | def __str__(self): 33 | return "Unimplemented string for Move." 34 | 35 | def do(self, state): 36 | raise Exception("Move does not implement do.") 37 | 38 | 39 | class Decision(object): 40 | """ 41 | An object that represents various Moves for a player to make. 42 | 43 | Members: 44 | - moves: List, list of Moves that a player can choose at this decision. 45 | - player: Player, the Player that needs to make the decision. 46 | - num_select: int, the number of moves that the player can legally select. 47 | - optional: bool, True if the player can make any number of decision up to num_select, 48 | False if the player must select exactly num_select Moves to make. 49 | - prompt: str, The string that explains to Agent when given this decision. 50 | 51 | """ 52 | 53 | def __init__( 54 | self, 55 | moves, 56 | player, 57 | num_select=1, 58 | optional=False, 59 | prompt="Unimplemented decision prompt"): 60 | self.moves = moves 61 | self.player = player 62 | self.num_select = num_select 63 | self.optional = optional 64 | self.prompt = prompt 65 | 66 | 67 | class ActionPhaseDecision(Decision): 68 | 69 | def __init__(self, player): 70 | moves = [] 71 | 72 | # Always allowed to end action Phase 73 | moves.append(EndActionPhase()) 74 | 75 | # Play any action cards if we have remaining actions. 76 | if player.actions > 0: 77 | for card_idx, card in enumerate(player.hand): 78 | if card.is_type(CardType.ACTION): 79 | moves.append(PlayCard(card)) 80 | 81 | super().__init__(moves, player, prompt="Action Phase, choose card to play.") 82 | 83 | 84 | class TreasurePhaseDecision(Decision): 85 | 86 | def __init__(self, player): 87 | # Always allowed to end treasure Phase 88 | moves = [EndTreasurePhase()] 89 | 90 | for card_idx, card in enumerate(player.hand): 91 | if card.is_type(CardType.TREASURE): 92 | moves.append(PlayCard(card)) 93 | 94 | super().__init__(moves, player, prompt="Treasure Phase, choose card to play.") 95 | 96 | 97 | class BuyPhaseDecision(Decision): 98 | 99 | def __init__(self, state, player): 100 | # Always allowed to end buy phase 101 | moves = [EndBuyPhase()] 102 | 103 | for card_name, supply_pile in state.supply_piles.items(): 104 | if (supply_pile.qty > 0 105 | and player.coins >= supply_pile.card.cost 106 | and player.buys > 0): 107 | moves.append(BuyCard(card_name)) 108 | 109 | super().__init__(moves, player, prompt="Buy Phase, choose card to buy.") 110 | 111 | 112 | class EndPhaseDecision(Decision): 113 | 114 | def __init__(self, player): 115 | moves = [EndTurn()] 116 | 117 | super().__init__(moves, player, prompt="End Turn") 118 | 119 | 120 | class ChooseCardsDecision(Decision): 121 | """ 122 | Decision for choosing cards (from the players hand). 123 | """ 124 | def __init__(self, 125 | player, 126 | num_select, 127 | prompt, 128 | filter_func=None, 129 | optional=True, 130 | card_container=None): 131 | 132 | self.cards = [] 133 | moves = [] 134 | 135 | # By default, this decision chooses from the player's hand. 136 | if card_container is None: 137 | card_container = player.hand 138 | 139 | for card_idx, card in enumerate(card_container): 140 | if filter_func is None or filter_func(card): 141 | moves.append(self.ChooseCard(card, self)) 142 | 143 | super().__init__( 144 | moves, 145 | player, 146 | num_select=num_select, 147 | optional=optional, 148 | prompt=prompt 149 | ) 150 | 151 | class ChooseCard(Move): 152 | """ 153 | Add a card to the context. 154 | """ 155 | 156 | def __init__(self, card, decision): 157 | self.decision = decision 158 | self.card = card 159 | 160 | def __str__(self): 161 | return f"Choose: {self.card}" 162 | 163 | def do(self, state): 164 | self.decision.cards.append(self.card) 165 | 166 | 167 | def choose_cards(state, player, num_select, prompt, filter_func=None, optional=True, card_container=None): 168 | """ 169 | Call this when you need to prompt a player to choose a card. 170 | """ 171 | 172 | # By default, pick a card from player's hand. 173 | if card_container is None: 174 | card_container = player.hand 175 | 176 | decision = ChooseCardsDecision( 177 | player=player, 178 | num_select=num_select, 179 | prompt=prompt, 180 | filter_func=filter_func, 181 | optional=optional, 182 | card_container=card_container, 183 | ) 184 | process_decision(player.agent, decision, state) 185 | return decision.cards 186 | 187 | 188 | class ChoosePileDecision(Decision): 189 | """ 190 | Decision for choosing one supply pile. 191 | 192 | TODO(benzyx): maybe one day you need to select multiple piles? 193 | """ 194 | 195 | def __init__(self, state, player, filter_func, prompt): 196 | moves = [] 197 | for card_name, pile in state.supply_piles.items(): 198 | if filter_func is None or filter_func(pile): 199 | moves.append(self.ChoosePile(self, pile)) 200 | 201 | self.pile = None 202 | 203 | super().__init__(moves, player, prompt=prompt) 204 | 205 | class ChoosePile(Move): 206 | """ 207 | Add a card to the Decision. 208 | """ 209 | 210 | def __init__(self, decision, pile): 211 | self.decision = decision 212 | self.pile = pile 213 | 214 | def __str__(self): 215 | return f"Choose: {self.pile.card.name} pile" 216 | 217 | def do(self, state): 218 | self.decision.pile = self.pile 219 | 220 | 221 | def boolean_choice(state, player, prompt, yes_prompt="Yes", no_prompt="No"): 222 | decision = BooleanDecision(state, player, prompt, yes_prompt, no_prompt) 223 | game.process_decision(player.agent, decision, state) 224 | return decision.value 225 | 226 | 227 | class BooleanDecision(Decision): 228 | """ 229 | Decision for choosing one of two options. 230 | 231 | TODO(benzyx): maybe one day you need to select multiple piles? 232 | """ 233 | 234 | def __init__(self, state, player, prompt, yes_prompt, no_prompt): 235 | moves = [self.YesMove(self, yes_prompt), self.NoMove(self, no_prompt)] 236 | self.value = None 237 | 238 | super().__init__(moves, player, prompt=prompt) 239 | 240 | class YesMove(Move): 241 | """ 242 | User chooses yes. 243 | """ 244 | 245 | def __init__(self, decision, prompt=None): 246 | self.decision = decision 247 | self.prompt = prompt or "Yes" 248 | 249 | def __str__(self): 250 | return self.prompt 251 | 252 | def do(self, state): 253 | self.decision.value = True 254 | 255 | class NoMove(Move): 256 | """ 257 | User chooses no. 258 | """ 259 | 260 | def __init__(self, decision, prompt=None): 261 | self.decision = decision 262 | self.prompt = prompt or "No" 263 | 264 | def __str__(self): 265 | return self.prompt 266 | 267 | def do(self, state): 268 | self.decision.value = False 269 | 270 | 271 | class PlayCard(Move): 272 | """ 273 | Player plays a card. 274 | """ 275 | 276 | def __init__(self, card): 277 | self.card = card 278 | 279 | def __str__(self): 280 | return f"Play: {self.card}" 281 | 282 | def do(self, state): 283 | play_card_from_hand(state, state.current_player, self.card) 284 | 285 | 286 | class BuyCard(Move): 287 | """ 288 | Player buys a card. 289 | """ 290 | 291 | def __init__(self, card_name): 292 | self.card_name = card_name 293 | 294 | def __str__(self): 295 | return f"Buy: {self.card_name}" 296 | 297 | def do(self, state): 298 | buy_card(state, state.current_player, self.card_name) 299 | 300 | 301 | class EndActionPhase(Move): 302 | """ 303 | End Action Phase. 304 | """ 305 | 306 | def __str__(self): 307 | return "End Action Phase" 308 | 309 | def do(self, state): 310 | assert (state.current_player.phase == TurnPhase.ACTION_PHASE) 311 | state.current_player.phase = TurnPhase.TREASURE_PHASE 312 | 313 | 314 | class EndTreasurePhase(Move): 315 | """ 316 | End Treasure Phase. 317 | """ 318 | 319 | def __str__(self): 320 | return "End Treasure Phase" 321 | 322 | def do(self, state): 323 | assert (state.current_player.phase == TurnPhase.TREASURE_PHASE) 324 | state.current_player.phase = TurnPhase.BUY_PHASE 325 | 326 | 327 | class EndBuyPhase(Move): 328 | """ 329 | End Buy Phase 330 | """ 331 | 332 | def __str__(self): 333 | return "End Buy Phase" 334 | 335 | def do(self, state): 336 | assert (state.current_player.phase == TurnPhase.BUY_PHASE) 337 | state.current_player.phase = TurnPhase.END_PHASE 338 | 339 | 340 | class EndTurn(Move): 341 | """ 342 | End Turn 343 | """ 344 | 345 | def __str__(self): 346 | return "End Turn" 347 | 348 | def do(self, state): 349 | assert (state.current_player.phase == TurnPhase.END_PHASE) 350 | end_turn(state) 351 | -------------------------------------------------------------------------------- /domrl/agents/provincial_agent.py: -------------------------------------------------------------------------------- 1 | from ..engine.agent import Agent 2 | from ..engine.decision import * 3 | 4 | import numpy as np 5 | from typing import Union 6 | 7 | PlayDecision = Union[ActionPhaseDecision, TreasurePhaseDecision] 8 | 9 | ######################## PRIORITY SCORES ######################### 10 | action_priority = { 11 | "Throne Room" : (116 - 2 + 1), 12 | "Laboratory" : (116 - 10 + 1), 13 | "Market" : (116 - 12 + 1), 14 | "Festival" : (116 - 14 + 1), 15 | "Village" : (116 - 17 + 1), 16 | "Cellar" : (116 - 38 + 1), 17 | "Witch" : (116 - 46 + 1), 18 | "Council Room" : (116 - 53 + 1), 19 | "Smithy" : (116 - 54 + 1), 20 | "Library" : (116 - 64 + 1), 21 | "Militia" : (116 - 65 + 1), 22 | "Moneylender" : (116 - 93 + 1), 23 | "Bureaucrat" : (116 - 102 + 1), 24 | "Mine" : (116 - 105 + 1), 25 | "Moat" : (116 - 108 + 1), 26 | "Remodel" : (116 - 113 + 1), 27 | "Workshop" : (116 - 114 + 1), 28 | "Chapel" : (116 - 115 + 1) 29 | } 30 | 31 | discard_priority = { 32 | "Estate" : 20, 33 | "Dutchy" : 20, 34 | "Province" : 20, 35 | "Curse" : 19, 36 | "Copper" : 18 37 | } 38 | 39 | mine_priority = { 40 | "Silver" : 19, 41 | "Copper" : 18 42 | } 43 | ####################### END PRIORITY SCORES ###################### 44 | 45 | ######################## HELPER FUNCTIONS ######################## 46 | # helper to find index of a card 47 | def find_card_in_decision(decision, card_name): 48 | if isinstance(decision, PlayDecision.__args__): 49 | for idx, move in enumerate(decision.moves): 50 | if hasattr(move, 'card') and move.card.name == card_name: 51 | return [idx] 52 | elif isinstance(decision, BuyPhaseDecision): 53 | for idx, move in enumerate(decision.moves): 54 | if hasattr(move, 'card_name') and move.card_name == card_name: 55 | return [idx] 56 | return [0] 57 | 58 | # helper to sort a list of tuples by the second element 59 | def Sort_List_Of_Tuples(tup): 60 | # getting length of list of tuples 61 | lst = len(tup) 62 | for i in range(0, lst): 63 | 64 | for j in range(0, lst-i-1): 65 | if (tup[j][1] < tup[j + 1][1]): 66 | temp = tup[j] 67 | tup[j]= tup[j + 1] 68 | tup[j + 1]= temp 69 | return tup 70 | 71 | # helper to check if a card is in their hand 72 | def hand_contains(state_view, card_name): 73 | for card in state_view.player.hand: 74 | if card.name == card_name: 75 | return True 76 | return False 77 | ###################### END HELPER FUNCTIONS ###################### 78 | 79 | ######################### PHASE FUNCTIONS ######################## 80 | # provincial's buy menu for Big Money 81 | def provincial_buy_menu_big_money(decision, state_view, coins): 82 | if coins >= 8: 83 | return find_card_in_decision(decision, 'Province') 84 | if coins >= 5 and state_view.supply_piles['Province'].qty <= 4: 85 | return find_card_in_decision(decision, 'Duchy') 86 | if coins >= 1 and state_view.supply_piles['Province'].qty <= 2: 87 | return find_card_in_decision(decision, 'Estate') 88 | elif coins >= 6: 89 | return find_card_in_decision(decision, 'Gold') 90 | elif coins >= 3: 91 | return find_card_in_decision(decision, 'Silver') 92 | else: 93 | return [0] #### buy nothing 94 | 95 | # provoncial treasure phase strategy 96 | def provincial_treasure_phase(decision): 97 | return [len(decision.moves) - 1] # defaults to play all treasures 98 | 99 | # provincial action phase strategy 100 | def provincial_action_phase(decision): 101 | # no actions in hand 102 | if len(decision.moves) == 1: 103 | return [0] 104 | 105 | # rank cards 106 | cards_ordered = [] 107 | for i in range(1, len(decision.moves)): 108 | move = decision.moves[i] 109 | if hasattr(move, 'card') and move.card.name == "Moneylender" and hand_contains("Copper"): 110 | cards_ordered.append((i, card_priority[move.card.name])) 111 | elif hasattr(move, 'card'): 112 | cards_ordered.append((i, card_priority[move.card.name])) 113 | 114 | return [Sort_List_Of_Tuples(cards_ordered)[0][0]] 115 | ####################### END PHASE FUNCTIONS ###################### 116 | 117 | ############################ REACTIONS ########################### 118 | # reaction for card cellar 119 | def provincial_reaction_cellar(decision): 120 | # no cards to discard 121 | if len(decision.moves) == 1: 122 | return [0] 123 | 124 | # rank cards 125 | cards_ordered = [] 126 | for i in range(1, len(decision.moves)): 127 | move = decision.moves[i] 128 | if hasattr(move, 'card') and (move.card.name in discard_priority or move.card.cost <= 2): 129 | return [i] 130 | 131 | return [0] 132 | 133 | # reaction for card chapel 134 | def provincial_reaction_chapel(decision, state_view): 135 | # no cards to trash 136 | if len(decision.moves) == 1: 137 | return [0] 138 | 139 | treasure_total = 0 140 | for card in state_view.player.all_cards: 141 | if card.name == 'Copper': 142 | treasure_total += 1 143 | elif card.name == 'Silver': 144 | treasure_total += 3 145 | elif card.name == 'Gold': 146 | treasure_total += 6 147 | 148 | trashCoppers = (treasure_total >= 7) 149 | 150 | for i in range(1, len(decision.moves)): 151 | move = decision.moves[i] 152 | if hasattr(move, 'card') and \ 153 | move.card.name == 'Chapel' or \ 154 | (move.card.name == 'Estate' and state_view.supply_piles['Province'].qty > 2) or \ 155 | move.card.name == 'Curse' or \ 156 | (trashCoppers and move.card.name == 'Copper'): 157 | return [i] 158 | 159 | return [0] 160 | 161 | # reaction for card workshop 162 | def provincial_reaction_workshop(decision, state_view): 163 | return provincial_buy_menu_big_money(decision, state_view, 4) # always use buy menu 164 | 165 | # reaction for card bureaucrat 166 | def provincial_reaction_bureaucrat(decision): 167 | if len(decision.moves) == 1: 168 | return [0] 169 | return [1] # provincial always just chooses the first victory card 170 | 171 | # reaction for card militia 172 | def provincial_reaction_militia(decision): 173 | # no cards to discard 174 | if len(decision.moves) == 1: 175 | return [0] 176 | 177 | # rank cards 178 | cards_ordered = [] 179 | for i in range(1, len(decision.moves)): 180 | move = decision.moves[i] 181 | if hasattr(move, 'card') and move.card.name in discard_priority: 182 | cards_ordered.append((i, discard_priority[move.card.name])) 183 | elif hasattr(move, 'card'): 184 | cards_ordered.append((i, -100 - move.card.cost)) # ranking provincial uses 185 | 186 | return [Sort_List_Of_Tuples(cards_ordered)[0][0]] 187 | 188 | # reaction for card throne room 189 | def provincial_reaction_throne_room(decision): 190 | return provincial_action_phase(decision) # always use action policy here 191 | 192 | # reaction for card library 193 | def provincial_reaction_library(state_view): 194 | # no actions imply no point in taking action card 195 | if state_view.player.actions == 0: 196 | return False 197 | else: 198 | return True # otherwise take the action card (independent of what it is) 199 | 200 | # reaction for card mine 201 | def provincial_reaction_mine(decision, state_view): 202 | # no treasure cards in hand 203 | if len(decision.moves) == 1: 204 | return [0] 205 | 206 | # rank cards 207 | cards_ordered = [] 208 | for i in range(1, len(decision.moves)): 209 | move = decision.moves[i] 210 | if hasattr(move, 'card') and move.card.name == 'Silver' and state_view.supply_piles['gold'].qty > 0: 211 | cards_ordered.append((i, mine_priority[move.card.name])) 212 | elif hasattr(move, 'card') and move.card.name == 'Copper' and state_view.supply_piles['silver'].qty > 0: 213 | cards_ordered.append((i, mine_priority[move.card.name])) 214 | elif hasattr(move, 'card'): 215 | cards_ordered.append((i, -1*move.card.cost)) # ranking provincial uses 216 | 217 | return [Sort_List_Of_Tuples(cards_ordered)[0][0]] 218 | ########################## END REACTIONS ######################### 219 | 220 | class ProvincialAgent(Agent): 221 | def policy(self, decision, state_view): 222 | ######################## DEFAULT ######################### 223 | if not decision.optional and len(decision.moves) == 1: 224 | return [0] 225 | if decision.optional and len(decision.moves) == 0: 226 | return [] 227 | ###################### END DEFAULT ####################### 228 | 229 | ######################## REACTIONS ####################### 230 | # cellar is played 231 | if decision.prompt == 'Discard as many cards as you would like to draw.': 232 | return provincial_reaction_cellar(decision) 233 | 234 | # chapel is played 235 | if decision.prompt == 'Trash up to 4 cards.': 236 | return provincial_reaction_chapel(decision) 237 | 238 | # moat is handeled by global trigger 239 | 240 | # workshop is played 241 | if decision.prompt == 'Choose a pile to gain a card into your hand.': 242 | return provincial_reaction_workshop(decision, state_view) 243 | 244 | # bureaucrat is played 245 | if decision.prompt == 'Choose a Victory Card to topdeck.': 246 | return provincial_reaction_bureaucrat(decision) 247 | 248 | # militia is played 249 | if decision.prompt == 'Discard down to 3 cards.': 250 | return provincial_reaction_militia(decision) 251 | 252 | # throneroom is played 253 | if decision.prompt == 'Select a card to play twice.': 254 | return provincial_reaction_throne_room(decision) 255 | 256 | # library is played 257 | if decision.prompt == 'Library draws': 258 | return provincial_reaction_library(state_view) 259 | 260 | # mine is played 261 | if decision.prompt == 'Choose a Treasure to upgrade.': 262 | return provincial_reaction_mine(decision) 263 | 264 | # chancelor is removed 265 | # feast is removed 266 | # spy is removed 267 | #################### END REACTIONS ####################### 268 | 269 | ######################## PHASES ########################## 270 | if state_view.player.phase == TurnPhase.TREASURE_PHASE: 271 | return provincial_treasure_phase(decision) 272 | 273 | if state_view.player.phase == TurnPhase.BUY_PHASE: 274 | return provincial_buy_menu_big_money(decision, state_view, state_view.player.coins) 275 | 276 | if state_view.player.phase == TurnPhase.ACTION_PHASE: 277 | provincial_reaction_chapel(decision) 278 | return provincial_action_phase(decision) 279 | ###################### END PHASES ######################## 280 | 281 | return [0] # always the default action 282 | -------------------------------------------------------------------------------- /domrl/engine/cards/base.py: -------------------------------------------------------------------------------- 1 | from ..state_funcs import * 2 | from ..card import * 3 | 4 | import domrl.engine.supply as supply 5 | import domrl.engine.decision as dec 6 | import domrl.engine.effect as effect 7 | import domrl.engine.game as game 8 | import domrl.engine.trigger as trig 9 | 10 | SupplyPile = supply.SupplyPile 11 | 12 | """ 13 | Base Expansion Dominion Cards. 14 | """ 15 | Village = Card( 16 | name="Village", 17 | types=[CardType.ACTION], 18 | cost=3, 19 | add_cards=1, 20 | add_actions=2) 21 | 22 | Laboratory = Card( 23 | name="Laboratory", 24 | types=[CardType.ACTION], 25 | cost=5, 26 | add_cards=2, 27 | add_actions=1) 28 | 29 | Festival = Card( 30 | name="Festival", 31 | types=[CardType.ACTION], 32 | cost=5, 33 | add_actions=2, 34 | add_buys=1, 35 | coins=2) 36 | 37 | Market = Card( 38 | name="Market", 39 | types=[CardType.ACTION], 40 | cost=5, 41 | add_actions=1, 42 | add_buys=1, 43 | coins=1, 44 | add_cards=1) 45 | 46 | Smithy = Card( 47 | name="Smithy", 48 | types=[CardType.ACTION], 49 | cost=4, 50 | add_cards=3) 51 | 52 | 53 | class DiscardCardsEffect(effect.Effect): 54 | """ 55 | Discard cards in player's hand. Example: Poacher. 56 | """ 57 | 58 | def __init__(self, num_cards, filter_func, optional): 59 | self.num_cards = num_cards 60 | self.filter_func = filter_func 61 | self.optional = optional 62 | 63 | def run(self, state, player): 64 | if self.optional: 65 | prompt = f"Discard up to {self.num_cards} cards." 66 | else: 67 | prompt = f"Discard exactly {self.num_cards} cards." 68 | 69 | cards = dec.choose_cards( 70 | state=state, 71 | player=player, 72 | num_select=self.num_cards, 73 | prompt=prompt, 74 | filter_func=self.filter_func, 75 | optional=self.optional, 76 | ) 77 | 78 | for card in cards: 79 | player_discard_card_from_hand(state, player, card) 80 | 81 | 82 | 83 | class DiscardDownToEffect(effect.Effect): 84 | """ 85 | Discard down to some number of cards in player's hand. Example: Militia. 86 | """ 87 | def __init__(self, num_cards_downto): 88 | self.num_cards_downto = num_cards_downto 89 | 90 | def run(self, state, player): 91 | prompt = f"Discard down to {self.num_cards_downto} cards." 92 | num_to_discard = len(player.hand) - self.num_cards_downto 93 | 94 | cards = dec.choose_cards( 95 | state=state, 96 | player=player, 97 | num_select=num_to_discard, 98 | prompt=prompt, 99 | filter_func=None, 100 | optional=False, 101 | ) 102 | 103 | for card in cards: 104 | player_discard_card_from_hand(state, player, card) 105 | 106 | 107 | class TrashCardsEffect(effect.Effect): 108 | def __init__(self, num_cards, filter_func=None, optional=True): 109 | self.num_cards = num_cards 110 | self.filter_func = filter_func 111 | self.optional = optional 112 | 113 | def run(self, state, player): 114 | if self.optional: 115 | prompt = f"Trash up to {self.num_cards} cards." 116 | else: 117 | prompt = f"Trash exactly {self.num_cards} cards." 118 | 119 | cards = dec.choose_cards( 120 | state=state, 121 | player=player, 122 | num_select=self.num_cards, 123 | prompt=prompt, 124 | filter_func=self.filter_func, 125 | optional=self.optional 126 | ) 127 | 128 | for card in cards: 129 | player_trash_card_from_hand(state, player, card) 130 | 131 | 132 | class GainCardToDiscardPileEffect(effect.Effect): 133 | def __init__(self, card_name): 134 | self.card_name = card_name 135 | 136 | def run(self, state, player): 137 | assert (self.card_name in state.supply_piles) 138 | pile = state.supply_piles[self.card_name] 139 | gain_card_to_discard(state, player, pile) 140 | 141 | 142 | class ChoosePileToGainEffect(effect.Effect): 143 | def __init__(self, filter_func): 144 | self.filter_func = filter_func 145 | 146 | def run(self, state, player): 147 | prompt = f"Choose a pile to gain card from." 148 | decision = dec.ChoosePileDecision(state, player, self.filter_func, prompt) 149 | game.process_decision(player.agent, decision, state) 150 | gain_card_to_discard(state, player, decision.pile) 151 | 152 | 153 | class ChoosePileToGainToHandEffect(effect.Effect): 154 | def __init__(self, filter_func): 155 | self.filter_func = filter_func 156 | 157 | def run(self, state, player): 158 | prompt = f"Choose a pile to gain a card into your hand." 159 | decision = dec.ChoosePileDecision(state, player, self.filter_func, prompt) 160 | game.process_decision(player.agent, decision, state) 161 | gain_card_to_hand(state, player, decision.pile) 162 | 163 | 164 | class OpponentsDiscardDownToEffect(effect.Effect): 165 | def __init__(self, num_cards_downto): 166 | self.num_cards_downto = num_cards_downto 167 | 168 | def run(self, state, player): 169 | for opp in state.other_players(player): 170 | if opp.immune_to_attack: 171 | opp.immune_to_attack = False 172 | continue 173 | DiscardDownToEffect(self.num_cards_downto).run(state, opp) 174 | 175 | 176 | class OpponentsGainCardEffect(effect.Effect): 177 | def __init__(self, card_name): 178 | self.card_name = card_name 179 | 180 | def run(self, state, player): 181 | for opp in state.other_players(player): 182 | if opp.immune_to_attack: 183 | opp.immune_to_attack = False 184 | continue 185 | GainCardToDiscardPileEffect(self.card_name).run(state, opp) 186 | 187 | 188 | Militia = Card( 189 | name="Militia", 190 | types=[CardType.ACTION, CardType.ATTACK], 191 | cost=4, 192 | coins=2, 193 | effect_list=[OpponentsDiscardDownToEffect(3)]) 194 | 195 | Gardens = Card( 196 | name="Gardens", 197 | types=[CardType.VICTORY], 198 | cost=4, 199 | vp_fn=lambda all_cards: len(all_cards) 200 | ) 201 | 202 | Chapel = Card( 203 | name="Chapel", 204 | types=[CardType.ACTION], 205 | cost=2, 206 | effect_list=[TrashCardsEffect(4, optional=True)] 207 | ) 208 | 209 | Witch = Card( 210 | name="Witch", 211 | types=[CardType.ACTION, CardType.ATTACK], 212 | cost=5, 213 | add_cards=2, 214 | effect_list=[OpponentsGainCardEffect("Curse")]) 215 | 216 | Workshop = Card( 217 | name="Workshop", 218 | types=[CardType.ACTION], 219 | cost=3, 220 | effect_list=[ 221 | ChoosePileToGainEffect( 222 | filter_func=lambda pile: (pile.card.cost <= 4) 223 | ) 224 | ] 225 | ) 226 | 227 | 228 | class TrashAndGainEffect(effect.Effect): 229 | def __init__(self, add_cost: int, gain_exact_cost: bool): 230 | self.add_cost = add_cost 231 | self.gain_exact_cost = gain_exact_cost 232 | 233 | def run(self, state, player): 234 | cards = dec.choose_cards( 235 | state=state, 236 | player=player, 237 | num_select=1, 238 | prompt=f"Choose a card to trash.", 239 | filter_func=None, 240 | optional=False 241 | ) 242 | 243 | assert (len(cards) == 1) 244 | trashed_card = cards[0] 245 | 246 | player_trash_card_from_hand(state, player, trashed_card) 247 | 248 | filter_function = lambda pile: pile.card.cost <= trashed_card.cost + self.add_cost 249 | if self.gain_exact_cost: 250 | filter_function = lambda pile: pile.card.cost == trashed_card.cost + self.add_cost 251 | 252 | ChoosePileToGainEffect( 253 | filter_func=filter_function 254 | ).run(state, player) 255 | 256 | 257 | Remodel = Card( 258 | name="Remodel", 259 | types=[CardType.ACTION], 260 | cost=4, 261 | effect_list=[TrashAndGainEffect(2, False)], 262 | ) 263 | 264 | 265 | def bandit_attack_fn(state, player): 266 | # Attack fn 267 | for opp in state.other_players(player): 268 | 269 | if opp.immune_to_attack: 270 | opp.immune_to_attack=False 271 | continue 272 | 273 | top_two_cards = draw(state, opp, 2, draw_event=False) 274 | 275 | treasures = [] 276 | non_treasures = [] 277 | for card in top_two_cards: 278 | if card.is_type(CardType.TREASURE) and card.name != "Copper": 279 | treasures.append(card) 280 | else: 281 | non_treasures.append(card) 282 | 283 | # If there are two treasures: 284 | if len(treasures) == 2: 285 | cards = dec.choose_cards( 286 | state=state, 287 | player=opp, 288 | num_select=1, 289 | prompt="Select a card to trash from enemy Bandit.", 290 | filter_func=None, 291 | optional=False, 292 | card_container=treasures, 293 | ) 294 | 295 | trash(state, opp, cards[0], treasures) 296 | discard(state, opp, treasures[0], treasures) 297 | assert (len(treasures) == 0) 298 | 299 | elif len(treasures) == 1: 300 | trash(state, player, treasures[0], treasures) 301 | assert (len(treasures) == 0) 302 | 303 | for card in non_treasures.copy(): 304 | discard(state, opp, card, non_treasures) 305 | 306 | 307 | Bandit = Card( 308 | name="Bandit", 309 | types=[CardType.ACTION, CardType.ATTACK], 310 | cost=5, 311 | effect_list=[ 312 | GainCardToDiscardPileEffect("Gold") 313 | ], 314 | effect_fn=bandit_attack_fn, 315 | ) 316 | 317 | 318 | def throne_fn(state, player): 319 | cards = dec.choose_cards( 320 | state, 321 | player, 322 | num_select=1, 323 | prompt="Select a card to play twice.", 324 | filter_func=lambda _card: _card.is_type(CardType.ACTION), 325 | optional=True, 326 | card_container=player.hand, 327 | ) 328 | 329 | if cards: 330 | card = cards[0] 331 | 332 | # Refactor the moving of one container to another. 333 | card_idx = player.hand.index(card) 334 | player.hand.pop(card_idx) 335 | player.play_area.append(card) 336 | 337 | play_inplace(state, player, card) 338 | play_inplace(state, player, card) 339 | 340 | 341 | ThroneRoom = Card( 342 | name="Throne Room", 343 | types=[CardType.ACTION], 344 | cost=4, 345 | effect_fn=throne_fn, 346 | ) 347 | 348 | 349 | class MerchantTrigger(trig.Trigger): 350 | def __init__(self, player): 351 | self.triggered = False 352 | self.player = player 353 | 354 | def condition(self, event): 355 | return event.event_type == log.EventType.PLAY and event.card == Silver 356 | 357 | def apply(self, event, state): 358 | if not self.triggered: 359 | self.player.coins += 1 360 | self.triggered = True 361 | 362 | 363 | def merchant_fn(state, player): 364 | state.turn_triggers.append(MerchantTrigger(player)) 365 | 366 | 367 | Merchant = Card( 368 | name="Merchant", 369 | types=[CardType.ACTION], 370 | cost=3, 371 | add_cards=1, 372 | add_actions=1, 373 | effect_fn=merchant_fn, 374 | ) 375 | 376 | 377 | def moneylender_fn(state, player): 378 | cards = dec.choose_cards( 379 | state, 380 | player, 381 | num_select=1, 382 | prompt="Trash a copper to gain +3 coins. Choose none to skip.", 383 | filter_func=lambda card: card.name == "Copper", 384 | optional=True, 385 | card_container=player.hand, 386 | ) 387 | 388 | if cards: 389 | trash(state, player, cards[0], player.hand) 390 | player.coins += 3 391 | 392 | 393 | Moneylender = Card( 394 | name="Moneylender", 395 | types=[CardType.ACTION], 396 | cost=4, 397 | effect_fn=moneylender_fn, 398 | ) 399 | 400 | 401 | def poacher_fn(state, player): 402 | pileout_count = len(state.empty_piles()) 403 | if pileout_count > 0: 404 | cards = dec.choose_cards( 405 | state, 406 | player, 407 | num_select=2, 408 | prompt=f"You must discard {pileout_count} card(s).", 409 | filter_func=lambda card: card.name == "Copper", 410 | optional=False, 411 | card_container=player.hand, 412 | ) 413 | for card in cards: 414 | player_discard_card_from_hand(state, player, card) 415 | 416 | 417 | Poacher = Card( 418 | name="Poacher", 419 | types=[CardType.ACTION], 420 | cost=4, 421 | add_cards=1, 422 | add_actions=1, 423 | coins=1, 424 | effect_fn=poacher_fn, 425 | ) 426 | 427 | 428 | def cellar_fn(state, player): 429 | chosen_cards = dec.choose_cards( 430 | state, 431 | player, 432 | num_select=len(player.hand), 433 | prompt="Discard as many cards as you would like to draw.", 434 | filter_func=None, 435 | optional=True, 436 | card_container=player.hand, 437 | ) 438 | 439 | num_discarded = len(chosen_cards) 440 | for card in chosen_cards: 441 | player_discard_card_from_hand(state, player, card) 442 | 443 | draw_into_hand(state, player, num_discarded) 444 | 445 | 446 | Cellar = Card( 447 | name="Cellar", 448 | types=[CardType.ACTION], 449 | cost=2, 450 | effect_fn=cellar_fn, 451 | ) 452 | 453 | 454 | def mine_fn(state, player): 455 | trashed_card = dec.choose_cards( 456 | state, 457 | player, 458 | num_select=1, 459 | prompt="Choose a Treasure to upgrade.", 460 | filter_func=lambda card: card.is_type(CardType.TREASURE), 461 | optional=True, 462 | card_container=player.hand, 463 | ) 464 | 465 | if trashed_card: 466 | trashed_card = trashed_card[0] 467 | card_cost = trashed_card.cost 468 | player_trash_card_from_hand(state, player, trashed_card) 469 | ChoosePileToGainToHandEffect( 470 | filter_func=lambda pile: pile.card.is_type(CardType.TREASURE) and pile.card.cost <= card_cost + 3 471 | ).run(state, player) 472 | 473 | 474 | Mine = Card( 475 | name="Mine", 476 | types=[CardType.ACTION], 477 | cost=5, 478 | effect_fn=mine_fn, 479 | ) 480 | 481 | 482 | def vassal_fn(state, player): 483 | cards = draw(state, player, 1, draw_event=False) 484 | if cards: 485 | drawn_card = cards[0] 486 | if drawn_card.is_type(CardType.ACTION): 487 | play(state, player, drawn_card, cards) 488 | else: 489 | discard(state, player, drawn_card, cards) 490 | 491 | 492 | Vassal = Card( 493 | name="Vassal", 494 | types=[CardType.ACTION], 495 | cost=3, 496 | coins=2, 497 | effect_fn=vassal_fn, 498 | ) 499 | 500 | 501 | def council_room_fn(state, player): 502 | for opp in state.other_players(player): 503 | draw_into_hand(state, opp, 1) 504 | 505 | 506 | CouncilRoom = Card( 507 | name="Council Room", 508 | types=[CardType.ACTION], 509 | cost=5, 510 | add_cards=4, 511 | add_buys=1, 512 | effect_fn=council_room_fn, 513 | ) 514 | 515 | 516 | def artisan_fn(state, player): 517 | ChoosePileToGainToHandEffect( 518 | filter_func=lambda pile: (pile.card.cost <= 5) 519 | ).run(state, player) 520 | 521 | # Topdeck a card. 522 | cards = dec.choose_cards(state, 523 | player, 524 | num_select=1, 525 | prompt="Choose a card to topdeck.", 526 | filter_func=None, 527 | optional=False) 528 | 529 | if cards: 530 | topdeck(state, player, cards[0], player.hand) 531 | 532 | 533 | Artisan = Card( 534 | name="Artisan", 535 | types=[CardType.ACTION], 536 | cost=6, 537 | effect_fn=artisan_fn, 538 | ) 539 | 540 | 541 | def bureaucrat_fn(state, player): 542 | gain_card_to_topdeck(state, player, state.supply_piles["Silver"]) 543 | 544 | # Attack code. 545 | for opp in state.other_players(player): 546 | 547 | if opp.immune_to_attack: 548 | opp.immune_to_attack=False 549 | continue 550 | 551 | victory_card_count = 0 552 | for card in opp.hand: 553 | if card.is_type(CardType.VICTORY): 554 | victory_card_count += 1 555 | if victory_card_count > 0: 556 | topdeck_card = dec.choose_cards( 557 | state, 558 | player=opp, 559 | num_select=1, 560 | prompt="Choose a Victory Card to topdeck.", 561 | filter_func=lambda card: card.is_type(CardType.VICTORY), 562 | optional=False 563 | ) 564 | if topdeck_card: 565 | topdeck(state, opp, topdeck_card[0], opp.hand) 566 | else: 567 | for card in opp.hand: 568 | reveal(state, opp, card) 569 | 570 | 571 | Bureaucrat = Card( 572 | name="Bureaucrat", 573 | types=[CardType.ACTION, CardType.ATTACK], 574 | cost=4, 575 | effect_fn=bureaucrat_fn, 576 | ) 577 | 578 | 579 | def sentry_fn(state, player): 580 | drawn_cards = draw(state, player, 2, draw_event=False) 581 | 582 | # Choose trash. 583 | trash_cards = dec.choose_cards(state, 584 | player, 585 | num_select=len(drawn_cards), 586 | prompt="You may trash cards with Sentry.", 587 | optional=True, 588 | card_container=drawn_cards) 589 | for card in trash_cards: 590 | trash(state, player, card, container=drawn_cards) 591 | 592 | # Choose discard. 593 | discard_cards = dec.choose_cards(state, 594 | player, 595 | num_select=len(drawn_cards), 596 | prompt="You may discard cards with Sentry.", 597 | optional=True, 598 | card_container=drawn_cards) 599 | for card in discard_cards: 600 | discard(state, player, card, container=drawn_cards) 601 | 602 | # Choose order to topdeck. 603 | topdeck_cards = dec.choose_cards(state, 604 | player, 605 | num_select=len(drawn_cards), 606 | prompt="You may put cards back in any order with Sentry. Cards are topdecked in " 607 | "order, so the card listed last is placed on top.", 608 | optional=False, 609 | card_container=drawn_cards) 610 | for card in topdeck_cards: 611 | topdeck(state, player, card, container=drawn_cards) 612 | 613 | 614 | Sentry = Card( 615 | name="Sentry", 616 | types=[CardType.ACTION], 617 | cost=5, 618 | add_cards=1, 619 | add_actions=1, 620 | effect_fn=sentry_fn, 621 | ) 622 | 623 | 624 | def harbinger_fn(state, player): 625 | cards = dec.choose_cards( 626 | state, 627 | player, 628 | num_select=1, 629 | prompt="You may topdeck a card from your discard pile.", 630 | optional=True, 631 | card_container=player.discard_pile, 632 | ) 633 | if cards: 634 | topdeck(state, player, cards[0], player.discard_pile) 635 | 636 | 637 | Harbinger = Card( 638 | name="Harbinger", 639 | types=[CardType.ACTION], 640 | cost=3, 641 | add_cards=1, 642 | add_actions=1, 643 | effect_list=[], 644 | ) 645 | 646 | 647 | def library_fn(state, player): 648 | set_aside = [] 649 | while len(player.hand) < 7: 650 | cards = draw(state, player, 1, draw_event=False) 651 | 652 | if cards: 653 | # Give Player a decision to keep or not. 654 | keep = True 655 | if cards[0].is_type(CardType.ACTION): 656 | ans = dec.boolean_choice(state, 657 | player, 658 | prompt=f"Library draws {cards[0]}, keep?", 659 | yes_prompt="Put into hand.", 660 | no_prompt="Discard.") 661 | print(ans) 662 | if not ans: 663 | keep = False 664 | 665 | if keep: 666 | player.hand.append(cards[0]) 667 | else: 668 | set_aside.append(cards[0]) 669 | else: 670 | break 671 | 672 | for card in set_aside: 673 | discard(state, player, card, set_aside) 674 | 675 | 676 | Library = Card( 677 | name="Library", 678 | types=[CardType.ACTION], 679 | cost=5, 680 | effect_fn=library_fn, 681 | ) 682 | 683 | 684 | class MoatTrigger(trig.Trigger): 685 | def condition(self, event): 686 | return event.event_type == log.EventType.PLAY and event.card.is_type(CardType.ATTACK) 687 | 688 | def apply(self, event: log.Event, state): 689 | for opp in state.other_players(event.player): 690 | if Moat in opp.hand: 691 | result = dec.boolean_choice(state, 692 | opp, 693 | "Reveal Moat to defend attack?") 694 | if result: 695 | reveal(state, opp, Moat) 696 | opp.immune_to_attack = True 697 | 698 | 699 | Moat = Card( 700 | name="Moat", 701 | types=[CardType.ACTION, CardType.REACTION], 702 | cost=2, 703 | add_cards=2, 704 | global_trigger=MoatTrigger(), 705 | ) 706 | 707 | 708 | BaseKingdom = { 709 | "Village": SupplyPile(Village, 10), 710 | "Laboratory": SupplyPile(Laboratory, 10), 711 | "Market": SupplyPile(Market, 10), 712 | "Festival": SupplyPile(Festival, 10), 713 | "Smithy": SupplyPile(Smithy, 10), 714 | "Militia": SupplyPile(Militia, 10), 715 | "Gardens": SupplyPile(Gardens, 8), 716 | "Chapel": SupplyPile(Chapel, 10), 717 | "Witch": SupplyPile(Witch, 10), 718 | "Workshop": SupplyPile(Workshop, 10), 719 | "Bandit": SupplyPile(Bandit, 10), 720 | "Remodel": SupplyPile(Remodel, 10), 721 | "Throne Room": SupplyPile(ThroneRoom, 10), 722 | "Moneylender": SupplyPile(Moneylender, 10), 723 | "Poacher": SupplyPile(Poacher, 10), 724 | "Merchant": SupplyPile(Merchant, 10), 725 | "Cellar": SupplyPile(Cellar, 10), 726 | "Mine": SupplyPile(Mine, 10), 727 | "Vassal": SupplyPile(Vassal, 10), 728 | "Council Room": SupplyPile(CouncilRoom, 10), 729 | "Artisan": SupplyPile(Artisan, 10), 730 | "Bureaucrat": SupplyPile(Bureaucrat, 10), 731 | "Sentry": SupplyPile(Sentry, 10), 732 | "Harbinger": SupplyPile(Harbinger, 10), 733 | "Library": SupplyPile(Library, 10), 734 | "Moat": SupplyPile(Moat, 10), 735 | } 736 | -------------------------------------------------------------------------------- /nb/RunGames.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 20, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from domrl.engine.game import Game\n", 10 | "from domrl.engine.agent import StdinAgent\n", 11 | "from domrl.agents.big_money_agent import BigMoneyAgent\n", 12 | "from domrl.agents.provincial_agent import ProvincialAgent\n", 13 | "\n", 14 | "import domrl.engine.cards.base as base" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": 21, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "game = Game(\n", 24 | " agents=[BigMoneyAgent(), BigMoneyAgent()],\n", 25 | " kingdoms=[base.BaseKingdom],\n", 26 | " verbose=False\n", 27 | ")" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": 22, 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "end_state = game.run()" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": 23, 42 | "metadata": {}, 43 | "outputs": [ 44 | { 45 | "data": { 46 | "text/html": [ 47 | "
\n", 48 | "\n", 61 | "\n", 62 | " \n", 63 | " \n", 64 | " \n", 65 | " \n", 66 | " \n", 67 | " \n", 68 | " \n", 69 | " \n", 70 | " \n", 71 | " \n", 72 | " \n", 73 | " \n", 74 | " \n", 75 | " \n", 76 | " \n", 77 | " \n", 78 | " \n", 79 | " \n", 80 | " \n", 81 | " \n", 82 | " \n", 83 | " \n", 84 | " \n", 85 | " \n", 86 | " \n", 87 | "
PlayerVPTurnsWinner
0Player 12723True
1Player 22723True
\n", 88 | "
" 89 | ], 90 | "text/plain": [ 91 | " Player VP Turns Winner\n", 92 | "0 Player 1 27 23 True\n", 93 | "1 Player 2 27 23 True" 94 | ] 95 | }, 96 | "execution_count": 23, 97 | "metadata": {}, 98 | "output_type": "execute_result" 99 | } 100 | ], 101 | "source": [ 102 | "game.get_result_df()" 103 | ] 104 | }, 105 | { 106 | "cell_type": "code", 107 | "execution_count": 19, 108 | "metadata": {}, 109 | "outputs": [ 110 | { 111 | "name": "stdout", 112 | "output_type": "stream", 113 | "text": [ 114 | "=== Replaying log from beginning ===\n", 115 | "Player 1 draws a Estate.\n", 116 | "Player 1 draws a Estate.\n", 117 | "Player 1 draws a Copper.\n", 118 | "Player 1 draws a Estate.\n", 119 | "Player 1 draws a Copper.\n", 120 | "Player 2 draws a Copper.\n", 121 | "Player 2 draws a Copper.\n", 122 | "Player 2 draws a Estate.\n", 123 | "Player 2 draws a Copper.\n", 124 | "Player 2 draws a Estate.\n", 125 | "Player 1 plays a Copper.\n", 126 | "Player 1 plays a Copper.\n", 127 | "Player 1 draws a Copper.\n", 128 | "Player 1 draws a Copper.\n", 129 | "Player 1 draws a Copper.\n", 130 | "Player 1 draws a Copper.\n", 131 | "Player 1 draws a Copper.\n", 132 | "Player 1 ends their turn.\n", 133 | "Player 2 plays a Copper.\n", 134 | "Player 2 plays a Copper.\n", 135 | "Player 2 plays a Copper.\n", 136 | "Player 2 buys a Silver.\n", 137 | " Player 2 gains a Silver.\n", 138 | "Player 2 draws a Copper.\n", 139 | "Player 2 draws a Copper.\n", 140 | "Player 2 draws a Copper.\n", 141 | "Player 2 draws a Estate.\n", 142 | "Player 2 draws a Copper.\n", 143 | "Player 2 ends their turn.\n", 144 | "Player 1 plays a Copper.\n", 145 | "Player 1 plays a Copper.\n", 146 | "Player 1 plays a Copper.\n", 147 | "Player 1 plays a Copper.\n", 148 | "Player 1 plays a Copper.\n", 149 | "Player 1 buys a Silver.\n", 150 | " Player 1 gains a Silver.\n", 151 | "Player 1 shuffles their deck.\n", 152 | "Player 1 draws a Copper.\n", 153 | "Player 1 draws a Copper.\n", 154 | "Player 1 draws a Estate.\n", 155 | "Player 1 draws a Silver.\n", 156 | "Player 1 draws a Copper.\n", 157 | "Player 1 ends their turn.\n", 158 | "Player 2 plays a Copper.\n", 159 | "Player 2 plays a Copper.\n", 160 | "Player 2 plays a Copper.\n", 161 | "Player 2 plays a Copper.\n", 162 | "Player 2 buys a Silver.\n", 163 | " Player 2 gains a Silver.\n", 164 | "Player 2 shuffles their deck.\n", 165 | "Player 2 draws a Silver.\n", 166 | "Player 2 draws a Copper.\n", 167 | "Player 2 draws a Estate.\n", 168 | "Player 2 draws a Copper.\n", 169 | "Player 2 draws a Copper.\n", 170 | "Player 2 ends their turn.\n", 171 | "Player 1 plays a Copper.\n", 172 | "Player 1 plays a Copper.\n", 173 | "Player 1 plays a Silver.\n", 174 | "Player 1 plays a Copper.\n", 175 | "Player 1 buys a Silver.\n", 176 | " Player 1 gains a Silver.\n", 177 | "Player 1 draws a Copper.\n", 178 | "Player 1 draws a Estate.\n", 179 | "Player 1 draws a Copper.\n", 180 | "Player 1 draws a Copper.\n", 181 | "Player 1 draws a Estate.\n", 182 | "Player 1 ends their turn.\n", 183 | "Player 2 plays a Silver.\n", 184 | "Player 2 plays a Copper.\n", 185 | "Player 2 plays a Copper.\n", 186 | "Player 2 plays a Copper.\n", 187 | "Player 2 buys a Silver.\n", 188 | " Player 2 gains a Silver.\n", 189 | "Player 2 draws a Copper.\n", 190 | "Player 2 draws a Copper.\n", 191 | "Player 2 draws a Estate.\n", 192 | "Player 2 draws a Copper.\n", 193 | "Player 2 draws a Silver.\n", 194 | "Player 2 ends their turn.\n", 195 | "Player 1 plays a Copper.\n", 196 | "Player 1 plays a Copper.\n", 197 | "Player 1 plays a Copper.\n", 198 | "Player 1 buys a Silver.\n", 199 | " Player 1 gains a Silver.\n", 200 | "Player 1 draws a Copper.\n", 201 | "Player 1 shuffles their deck.\n", 202 | "Player 1 draws a Estate.\n", 203 | "Player 1 draws a Copper.\n", 204 | "Player 1 draws a Copper.\n", 205 | "Player 1 draws a Copper.\n", 206 | "Player 1 ends their turn.\n", 207 | "Player 2 plays a Copper.\n", 208 | "Player 2 plays a Copper.\n", 209 | "Player 2 plays a Copper.\n", 210 | "Player 2 plays a Silver.\n", 211 | "Player 2 buys a Silver.\n", 212 | " Player 2 gains a Silver.\n", 213 | "Player 2 draws a Estate.\n", 214 | "Player 2 draws a Copper.\n", 215 | "Player 2 shuffles their deck.\n", 216 | "Player 2 draws a Copper.\n", 217 | "Player 2 draws a Silver.\n", 218 | "Player 2 draws a Silver.\n", 219 | "Player 2 ends their turn.\n", 220 | "Player 1 plays a Copper.\n", 221 | "Player 1 plays a Copper.\n", 222 | "Player 1 plays a Copper.\n", 223 | "Player 1 plays a Copper.\n", 224 | "Player 1 buys a Silver.\n", 225 | " Player 1 gains a Silver.\n", 226 | "Player 1 draws a Copper.\n", 227 | "Player 1 draws a Estate.\n", 228 | "Player 1 draws a Copper.\n", 229 | "Player 1 draws a Silver.\n", 230 | "Player 1 draws a Estate.\n", 231 | "Player 1 ends their turn.\n", 232 | "Player 2 plays a Copper.\n", 233 | "Player 2 plays a Copper.\n", 234 | "Player 2 plays a Silver.\n", 235 | "Player 2 plays a Silver.\n", 236 | "Player 2 buys a Gold.\n", 237 | " Player 2 gains a Gold.\n", 238 | "Player 2 draws a Copper.\n", 239 | "Player 2 draws a Estate.\n", 240 | "Player 2 draws a Copper.\n", 241 | "Player 2 draws a Estate.\n", 242 | "Player 2 draws a Copper.\n", 243 | "Player 2 ends their turn.\n", 244 | "Player 1 plays a Copper.\n", 245 | "Player 1 plays a Copper.\n", 246 | "Player 1 plays a Silver.\n", 247 | "Player 1 buys a Silver.\n", 248 | " Player 1 gains a Silver.\n", 249 | "Player 1 draws a Silver.\n", 250 | "Player 1 draws a Silver.\n", 251 | "Player 1 draws a Copper.\n", 252 | "Player 1 shuffles their deck.\n", 253 | "Player 1 draws a Estate.\n", 254 | "Player 1 draws a Copper.\n", 255 | "Player 1 ends their turn.\n", 256 | "Player 2 plays a Copper.\n", 257 | "Player 2 plays a Copper.\n", 258 | "Player 2 plays a Copper.\n", 259 | "Player 2 buys a Silver.\n", 260 | " Player 2 gains a Silver.\n", 261 | "Player 2 draws a Copper.\n", 262 | "Player 2 draws a Silver.\n", 263 | "Player 2 draws a Silver.\n", 264 | "Player 2 draws a Copper.\n", 265 | "Player 2 shuffles their deck.\n", 266 | "Player 2 draws a Silver.\n", 267 | "Player 2 ends their turn.\n", 268 | "Player 1 plays a Silver.\n", 269 | "Player 1 plays a Silver.\n", 270 | "Player 1 plays a Copper.\n", 271 | "Player 1 plays a Copper.\n", 272 | "Player 1 buys a Gold.\n", 273 | " Player 1 gains a Gold.\n", 274 | "Player 1 draws a Estate.\n", 275 | "Player 1 draws a Copper.\n", 276 | "Player 1 draws a Silver.\n", 277 | "Player 1 draws a Copper.\n", 278 | "Player 1 draws a Copper.\n", 279 | "Player 1 ends their turn.\n", 280 | "Player 2 plays a Copper.\n", 281 | "Player 2 plays a Silver.\n", 282 | "Player 2 plays a Silver.\n", 283 | "Player 2 plays a Copper.\n", 284 | "Player 2 plays a Silver.\n", 285 | "Player 2 buys a Gold.\n", 286 | " Player 2 gains a Gold.\n", 287 | "Player 2 draws a Copper.\n", 288 | "Player 2 draws a Estate.\n", 289 | "Player 2 draws a Copper.\n", 290 | "Player 2 draws a Copper.\n", 291 | "Player 2 draws a Copper.\n", 292 | "Player 2 ends their turn.\n", 293 | "Player 1 plays a Copper.\n", 294 | "Player 1 plays a Silver.\n", 295 | "Player 1 plays a Copper.\n", 296 | "Player 1 plays a Copper.\n", 297 | "Player 1 buys a Silver.\n", 298 | " Player 1 gains a Silver.\n", 299 | "Player 1 draws a Copper.\n", 300 | "Player 1 draws a Estate.\n", 301 | "Player 1 draws a Silver.\n", 302 | "Player 1 draws a Silver.\n", 303 | "Player 1 draws a Copper.\n", 304 | "Player 1 ends their turn.\n", 305 | "Player 2 plays a Copper.\n", 306 | "Player 2 plays a Copper.\n", 307 | "Player 2 plays a Copper.\n", 308 | "Player 2 plays a Copper.\n", 309 | "Player 2 buys a Silver.\n", 310 | " Player 2 gains a Silver.\n", 311 | "Player 2 draws a Silver.\n", 312 | "Player 2 draws a Estate.\n", 313 | "Player 2 draws a Copper.\n", 314 | "Player 2 draws a Estate.\n", 315 | "Player 2 draws a Silver.\n", 316 | "Player 2 ends their turn.\n", 317 | "Player 1 plays a Copper.\n", 318 | "Player 1 plays a Silver.\n", 319 | "Player 1 plays a Silver.\n", 320 | "Player 1 plays a Copper.\n", 321 | "Player 1 buys a Gold.\n", 322 | " Player 1 gains a Gold.\n", 323 | "Player 1 shuffles their deck.\n", 324 | "Player 1 draws a Copper.\n", 325 | "Player 1 draws a Estate.\n", 326 | "Player 1 draws a Gold.\n", 327 | "Player 1 draws a Copper.\n", 328 | "Player 1 draws a Silver.\n", 329 | "Player 1 ends their turn.\n", 330 | "Player 2 plays a Silver.\n", 331 | "Player 2 plays a Copper.\n", 332 | "Player 2 plays a Silver.\n", 333 | "Player 2 buys a Silver.\n", 334 | " Player 2 gains a Silver.\n", 335 | "Player 2 draws a Gold.\n", 336 | "Player 2 shuffles their deck.\n", 337 | "Player 2 draws a Copper.\n", 338 | "Player 2 draws a Copper.\n", 339 | "Player 2 draws a Silver.\n", 340 | "Player 2 draws a Copper.\n", 341 | "Player 2 ends their turn.\n", 342 | "Player 1 plays a Copper.\n", 343 | "Player 1 plays a Gold.\n", 344 | "Player 1 plays a Copper.\n", 345 | "Player 1 plays a Silver.\n", 346 | "Player 1 buys a Gold.\n", 347 | " Player 1 gains a Gold.\n", 348 | "Player 1 draws a Estate.\n", 349 | "Player 1 draws a Copper.\n", 350 | "Player 1 draws a Copper.\n", 351 | "Player 1 draws a Silver.\n", 352 | "Player 1 draws a Copper.\n", 353 | "Player 1 ends their turn.\n", 354 | "Player 2 plays a Gold.\n", 355 | "Player 2 plays a Copper.\n", 356 | "Player 2 plays a Copper.\n", 357 | "Player 2 plays a Silver.\n", 358 | "Player 2 plays a Copper.\n", 359 | "Player 2 buys a Gold.\n", 360 | " Player 2 gains a Gold.\n", 361 | "Player 2 draws a Estate.\n", 362 | "Player 2 draws a Copper.\n", 363 | "Player 2 draws a Gold.\n", 364 | "Player 2 draws a Silver.\n", 365 | "Player 2 draws a Silver.\n", 366 | "Player 2 ends their turn.\n", 367 | "Player 1 plays a Copper.\n", 368 | "Player 1 plays a Copper.\n", 369 | "Player 1 plays a Silver.\n", 370 | "Player 1 plays a Copper.\n", 371 | "Player 1 buys a Silver.\n", 372 | " Player 1 gains a Silver.\n", 373 | "Player 1 draws a Silver.\n", 374 | "Player 1 draws a Estate.\n", 375 | "Player 1 draws a Silver.\n", 376 | "Player 1 draws a Silver.\n", 377 | "Player 1 draws a Copper.\n", 378 | "Player 1 ends their turn.\n", 379 | "Player 2 plays a Copper.\n", 380 | "Player 2 plays a Gold.\n", 381 | "Player 2 plays a Silver.\n", 382 | "Player 2 plays a Silver.\n", 383 | "Player 2 buys a Gold.\n", 384 | " Player 2 gains a Gold.\n", 385 | "Player 2 draws a Copper.\n", 386 | "Player 2 draws a Estate.\n", 387 | "Player 2 draws a Silver.\n", 388 | "Player 2 draws a Estate.\n", 389 | "Player 2 draws a Silver.\n", 390 | "Player 2 ends their turn.\n", 391 | "Player 1 plays a Silver.\n", 392 | "Player 1 plays a Silver.\n", 393 | "Player 1 plays a Silver.\n", 394 | "Player 1 plays a Copper.\n", 395 | "Player 1 buys a Gold.\n", 396 | " Player 1 gains a Gold.\n", 397 | "Player 1 draws a Silver.\n", 398 | "Player 1 draws a Copper.\n", 399 | "Player 1 draws a Gold.\n", 400 | "Player 1 shuffles their deck.\n", 401 | "Player 1 draws a Silver.\n", 402 | "Player 1 draws a Copper.\n", 403 | "Player 1 ends their turn.\n", 404 | "Player 2 plays a Copper.\n", 405 | "Player 2 plays a Silver.\n", 406 | "Player 2 plays a Silver.\n", 407 | "Player 2 buys a Silver.\n", 408 | " Player 2 gains a Silver.\n", 409 | "Player 2 draws a Copper.\n", 410 | "Player 2 draws a Copper.\n", 411 | "Player 2 draws a Silver.\n", 412 | "Player 2 draws a Silver.\n", 413 | "Player 2 shuffles their deck.\n", 414 | "Player 2 draws a Copper.\n", 415 | "Player 2 ends their turn.\n", 416 | "Player 1 plays a Silver.\n", 417 | "Player 1 plays a Copper.\n", 418 | "Player 1 plays a Gold.\n", 419 | "Player 1 plays a Silver.\n", 420 | "Player 1 plays a Copper.\n", 421 | "Player 1 buys a Gold.\n", 422 | " Player 1 gains a Gold.\n", 423 | "Player 1 draws a Estate.\n", 424 | "Player 1 draws a Gold.\n", 425 | "Player 1 draws a Copper.\n", 426 | "Player 1 draws a Silver.\n", 427 | "Player 1 draws a Silver.\n", 428 | "Player 1 ends their turn.\n", 429 | "Player 2 plays a Copper.\n", 430 | "Player 2 plays a Copper.\n", 431 | "Player 2 plays a Silver.\n", 432 | "Player 2 plays a Silver.\n", 433 | "Player 2 plays a Copper.\n", 434 | "Player 2 buys a Gold.\n", 435 | " Player 2 gains a Gold.\n", 436 | "Player 2 draws a Gold.\n", 437 | "Player 2 draws a Silver.\n", 438 | "Player 2 draws a Gold.\n", 439 | "Player 2 draws a Copper.\n", 440 | "Player 2 draws a Silver.\n", 441 | "Player 2 ends their turn.\n", 442 | "Player 1 plays a Gold.\n", 443 | "Player 1 plays a Copper.\n", 444 | "Player 1 plays a Silver.\n", 445 | "Player 1 plays a Silver.\n", 446 | "Player 1 buys a Gold.\n", 447 | " Player 1 gains a Gold.\n", 448 | "Player 1 draws a Silver.\n", 449 | "Player 1 draws a Copper.\n", 450 | "Player 1 draws a Gold.\n", 451 | "Player 1 draws a Estate.\n", 452 | "Player 1 draws a Silver.\n", 453 | "Player 1 ends their turn.\n", 454 | "Player 2 plays a Gold.\n", 455 | "Player 2 plays a Silver.\n", 456 | "Player 2 plays a Gold.\n", 457 | "Player 2 plays a Copper.\n", 458 | "Player 2 plays a Silver.\n", 459 | "Player 2 buys a Gold.\n", 460 | " Player 2 gains a Gold.\n", 461 | "Player 2 draws a Gold.\n", 462 | "Player 2 draws a Estate.\n", 463 | "Player 2 draws a Gold.\n", 464 | "Player 2 draws a Silver.\n", 465 | "Player 2 draws a Silver.\n", 466 | "Player 2 ends their turn.\n", 467 | "Player 1 plays a Silver.\n", 468 | "Player 1 plays a Copper.\n", 469 | "Player 1 plays a Gold.\n", 470 | "Player 1 plays a Silver.\n", 471 | "Player 1 buys a Gold.\n", 472 | " Player 1 gains a Gold.\n", 473 | "Player 1 draws a Copper.\n", 474 | "Player 1 draws a Silver.\n", 475 | "Player 1 draws a Gold.\n", 476 | "Player 1 draws a Estate.\n", 477 | "Player 1 draws a Copper.\n", 478 | "Player 1 ends their turn.\n", 479 | "Player 2 plays a Gold.\n", 480 | "Player 2 plays a Gold.\n", 481 | "Player 2 plays a Silver.\n", 482 | "Player 2 plays a Silver.\n", 483 | "Player 2 buys a Gold.\n", 484 | " Player 2 gains a Gold.\n", 485 | "Player 2 draws a Copper.\n", 486 | "Player 2 draws a Silver.\n", 487 | "Player 2 draws a Copper.\n", 488 | "Player 2 draws a Estate.\n", 489 | "Player 2 draws a Copper.\n", 490 | "Player 2 ends their turn.\n", 491 | "Player 1 plays a Copper.\n", 492 | "Player 1 plays a Silver.\n", 493 | "Player 1 plays a Gold.\n", 494 | "Player 1 plays a Copper.\n", 495 | "Player 1 buys a Gold.\n", 496 | " Player 1 gains a Gold.\n", 497 | "Player 1 draws a Copper.\n", 498 | "Player 1 shuffles their deck.\n", 499 | "Player 1 draws a Gold.\n", 500 | "Player 1 draws a Estate.\n", 501 | "Player 1 draws a Copper.\n", 502 | "Player 1 draws a Gold.\n", 503 | "Player 1 ends their turn.\n", 504 | "Player 2 plays a Copper.\n", 505 | "Player 2 plays a Silver.\n", 506 | "Player 2 plays a Copper.\n", 507 | "Player 2 plays a Copper.\n", 508 | "Player 2 buys a Silver.\n", 509 | " Player 2 gains a Silver.\n", 510 | "Player 2 draws a Silver.\n", 511 | "Player 2 draws a Estate.\n", 512 | "Player 2 shuffles their deck.\n", 513 | "Player 2 draws a Silver.\n", 514 | "Player 2 draws a Gold.\n", 515 | "Player 2 draws a Silver.\n", 516 | "Player 2 ends their turn.\n", 517 | "Player 1 plays a Copper.\n", 518 | "Player 1 plays a Gold.\n", 519 | "Player 1 plays a Copper.\n", 520 | "Player 1 plays a Gold.\n", 521 | "Player 1 buys a Province.\n", 522 | " Player 1 gains a Province.\n", 523 | "Player 1 draws a Gold.\n", 524 | "Player 1 draws a Gold.\n", 525 | "Player 1 draws a Silver.\n", 526 | "Player 1 draws a Gold.\n", 527 | "Player 1 draws a Estate.\n", 528 | "Player 1 ends their turn.\n", 529 | "Player 2 plays a Silver.\n", 530 | "Player 2 plays a Silver.\n", 531 | "Player 2 plays a Gold.\n", 532 | "Player 2 plays a Silver.\n", 533 | "Player 2 buys a Province.\n", 534 | " Player 2 gains a Province.\n", 535 | "Player 2 draws a Copper.\n", 536 | "Player 2 draws a Silver.\n", 537 | "Player 2 draws a Copper.\n", 538 | "Player 2 draws a Silver.\n", 539 | "Player 2 draws a Copper.\n", 540 | "Player 2 ends their turn.\n", 541 | "Player 1 plays a Gold.\n", 542 | "Player 1 plays a Gold.\n", 543 | "Player 1 plays a Silver.\n", 544 | "Player 1 plays a Gold.\n", 545 | "Player 1 buys a Province.\n", 546 | " Player 1 gains a Province.\n", 547 | "Player 1 draws a Silver.\n", 548 | "Player 1 draws a Gold.\n", 549 | "Player 1 draws a Gold.\n", 550 | "Player 1 draws a Silver.\n", 551 | "Player 1 draws a Copper.\n", 552 | "Player 1 ends their turn.\n", 553 | "Player 2 plays a Copper.\n", 554 | "Player 2 plays a Silver.\n", 555 | "Player 2 plays a Copper.\n", 556 | "Player 2 plays a Silver.\n", 557 | "Player 2 plays a Copper.\n", 558 | "Player 2 buys a Gold.\n", 559 | " Player 2 gains a Gold.\n", 560 | "Player 2 draws a Copper.\n", 561 | "Player 2 draws a Copper.\n", 562 | "Player 2 draws a Gold.\n", 563 | "Player 2 draws a Gold.\n", 564 | "Player 2 draws a Estate.\n", 565 | "Player 2 ends their turn.\n", 566 | "Player 1 plays a Silver.\n", 567 | "Player 1 plays a Gold.\n", 568 | "Player 1 plays a Gold.\n", 569 | "Player 1 plays a Silver.\n", 570 | "Player 1 plays a Copper.\n", 571 | "Player 1 draws a Copper.\n", 572 | "Player 1 draws a Estate.\n", 573 | "Player 1 draws a Copper.\n", 574 | "Player 1 draws a Silver.\n", 575 | "Player 1 draws a Copper.\n", 576 | "Player 1 ends their turn.\n", 577 | "Player 2 plays a Copper.\n", 578 | "Player 2 plays a Copper.\n", 579 | "Player 2 plays a Gold.\n", 580 | "Player 2 plays a Gold.\n", 581 | "Player 2 buys a Province.\n", 582 | " Player 2 gains a Province.\n", 583 | "Player 2 draws a Gold.\n", 584 | "Player 2 draws a Gold.\n", 585 | "Player 2 draws a Silver.\n", 586 | "Player 2 draws a Silver.\n", 587 | "Player 2 draws a Estate.\n", 588 | "Player 2 ends their turn.\n", 589 | "Player 1 plays a Copper.\n", 590 | "Player 1 plays a Copper.\n", 591 | "Player 1 plays a Silver.\n", 592 | "Player 1 plays a Copper.\n", 593 | "Player 1 buys a Silver.\n", 594 | " Player 1 gains a Silver.\n", 595 | "Player 1 draws a Silver.\n", 596 | "Player 1 draws a Copper.\n", 597 | "Player 1 draws a Silver.\n", 598 | "Player 1 draws a Silver.\n", 599 | "Player 1 draws a Gold.\n", 600 | "Player 1 ends their turn.\n", 601 | "Player 2 plays a Gold.\n", 602 | "Player 2 plays a Gold.\n", 603 | "Player 2 plays a Silver.\n", 604 | "Player 2 plays a Silver.\n", 605 | "Player 2 buys a Province.\n", 606 | " Player 2 gains a Province.\n", 607 | "Player 2 draws a Copper.\n", 608 | "Player 2 draws a Silver.\n", 609 | "Player 2 draws a Copper.\n", 610 | "Player 2 draws a Gold.\n", 611 | "Player 2 draws a Gold.\n", 612 | "Player 2 ends their turn.\n", 613 | "Player 1 plays a Silver.\n", 614 | "Player 1 plays a Copper.\n", 615 | "Player 1 plays a Silver.\n", 616 | "Player 1 plays a Silver.\n", 617 | "Player 1 plays a Gold.\n", 618 | "Player 1 shuffles their deck.\n", 619 | "Player 1 draws a Silver.\n", 620 | "Player 1 draws a Gold.\n", 621 | "Player 1 draws a Silver.\n", 622 | "Player 1 draws a Silver.\n", 623 | "Player 1 draws a Province.\n", 624 | "Player 1 ends their turn.\n", 625 | "Player 2 plays a Copper.\n", 626 | "Player 2 plays a Silver.\n", 627 | "Player 2 plays a Copper.\n", 628 | "Player 2 plays a Gold.\n", 629 | "Player 2 plays a Gold.\n", 630 | "Player 2 buys a Province.\n", 631 | " Player 2 gains a Province.\n", 632 | "Player 2 draws a Silver.\n", 633 | "Player 2 shuffles their deck.\n", 634 | "Player 2 draws a Silver.\n", 635 | "Player 2 draws a Province.\n", 636 | "Player 2 draws a Gold.\n", 637 | "Player 2 draws a Copper.\n", 638 | "Player 2 ends their turn.\n", 639 | "Player 1 plays a Silver.\n", 640 | "Player 1 plays a Gold.\n", 641 | "Player 1 plays a Silver.\n", 642 | "Player 1 plays a Silver.\n", 643 | "Player 1 buys a Province.\n", 644 | " Player 1 gains a Province.\n", 645 | "Player 1 draws a Copper.\n", 646 | "Player 1 draws a Silver.\n", 647 | "Player 1 draws a Silver.\n", 648 | "Player 1 draws a Province.\n", 649 | "Player 1 draws a Gold.\n", 650 | "Player 1 ends their turn.\n", 651 | "Player 2 plays a Silver.\n", 652 | "Player 2 plays a Silver.\n", 653 | "Player 2 plays a Gold.\n", 654 | "Player 2 plays a Copper.\n", 655 | "Player 2 draws a Gold.\n", 656 | "Player 2 draws a Silver.\n", 657 | "Player 2 draws a Gold.\n", 658 | "Player 2 draws a Province.\n", 659 | "Player 2 draws a Silver.\n", 660 | "Player 2 ends their turn.\n", 661 | "Player 1 plays a Copper.\n", 662 | "Player 1 plays a Silver.\n", 663 | "Player 1 plays a Silver.\n", 664 | "Player 1 plays a Gold.\n", 665 | "Player 1 buys a Province.\n", 666 | " Player 1 gains a Province.\n", 667 | "\n", 668 | "=== end ===\n" 669 | ] 670 | } 671 | ], 672 | "source": [ 673 | "game.get_log().print()" 674 | ] 675 | }, 676 | { 677 | "cell_type": "code", 678 | "execution_count": null, 679 | "metadata": {}, 680 | "outputs": [], 681 | "source": [] 682 | }, 683 | { 684 | "cell_type": "code", 685 | "execution_count": null, 686 | "metadata": {}, 687 | "outputs": [], 688 | "source": [] 689 | }, 690 | { 691 | "cell_type": "code", 692 | "execution_count": null, 693 | "metadata": {}, 694 | "outputs": [], 695 | "source": [] 696 | } 697 | ], 698 | "metadata": { 699 | "kernelspec": { 700 | "display_name": "Python 3", 701 | "language": "python", 702 | "name": "python3" 703 | }, 704 | "language_info": { 705 | "codemirror_mode": { 706 | "name": "ipython", 707 | "version": 3 708 | }, 709 | "file_extension": ".py", 710 | "mimetype": "text/x-python", 711 | "name": "python", 712 | "nbconvert_exporter": "python", 713 | "pygments_lexer": "ipython3", 714 | "version": "3.7.4" 715 | } 716 | }, 717 | "nbformat": 4, 718 | "nbformat_minor": 4 719 | } 720 | --------------------------------------------------------------------------------