Skip to content

Commit

Permalink
Merge pull request #52 from evanofslack/simulation
Browse files Browse the repository at this point in the history
Add big money ultimate bot
  • Loading branch information
evanofslack authored Nov 18, 2021
2 parents 4566a98 + f5041ed commit cda309c
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 38 deletions.
21 changes: 8 additions & 13 deletions examples/simulation.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
from pyminion.bots.big_money import BigMoney
from pyminion.expansions.base import base_cards, basic_cards, start_cards
from pyminion.bots.big_money_ultimate import BigMoneyUltimate
from pyminion.expansions.base import base_cards, basic_cards, smithy, start_cards
from pyminion.game import Game
from pyminion.players import Human
from pyminion.simulator import Simulator

human = Human()
bot_1 = BigMoney(player_id="Bot 1")
bot_2 = BigMoney(player_id="Bot 2")
bot_3 = BigMoney(player_id="Bot 3")
bot_4 = BigMoney(player_id="Bot 4")
bot_1 = BigMoney(player_id="Big Money")
bot_2 = BigMoneyUltimate(player_id="Big Money Ultimate")

players = [bot_2, bot_1]

players = [bot_1, bot_2]
expansions = [base_cards]


game = Game(
players,
expansions,
basic_cards,
start_cards,
)
game = Game(players, expansions, basic_cards, start_cards, kingdom_cards=[smithy])

sim = Simulator(game, iterations=100)
sim = Simulator(game, iterations=1000)

if __name__ == "__main__":
sim.run()
Expand Down
8 changes: 6 additions & 2 deletions pyminion/bots/big_money.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging

from pyminion.exceptions import EmptyPile
from pyminion.expansions.base import gold, province, silver
from pyminion.game import Game
from pyminion.models.core import Deck
Expand Down Expand Up @@ -40,5 +41,8 @@ def start_buy_phase(self, game: Game):
buy_card = silver
else:
return
self.buy(buy_card, supply=game.supply)
logger.info(f"{self.player_id} buys {buy_card}")

try:
self.buy(buy_card, supply=game.supply)
except EmptyPile:
pass
105 changes: 105 additions & 0 deletions pyminion/bots/big_money_ultimate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import logging
from typing import List

from pyminion.exceptions import EmptyPile
from pyminion.expansions.base import duchy, estate, gold, province, silver, smithy
from pyminion.game import Game
from pyminion.models.core import Card, Deck
from pyminion.players import Bot

logger = logging.getLogger()


class BigMoneyUltimate(Bot):
"""
Attempt the following buys in order:
Buy Province if total money in deck is > 15
Buy Duchy if remaining Provinces < 5
Buy Estate if remaining Provinces < 3
Buy Gold
Buy Duchy if remaining Provinces < 7
Buy Smithy if #Smithies < (#Treasures / 11)
Buy Silver
"""

def __init__(
self,
deck: Deck = None,
player_id: str = "big_money",
):
super().__init__(deck=deck, player_id=player_id)

def start_action_phase(self, game: Game):
viable_actions: List[Card] = [
card for card in self.hand.cards if "Action" in card.type
]
logger.info(f"{self.player_id}'s hand: {self.hand}")

while viable_actions and self.state.actions:

if viable_actions:
print(viable_actions)

self.play(target_card=smithy, game=game)

return

def start_buy_phase(self, game: Game):

money = self.state.money
num_province = game.supply.pile_length(pile_name="Province")
num_smithy = self.get_card_count(card=smithy)
num_treasure = len(
[card for card in self.get_all_cards() if "Treasure" in card.type]
)

while self.state.buys and self.state.money:
if self.get_deck_money() > 15 and num_province > 1 and money >= 8:
try:
self.buy(card=province, supply=game.supply)
return
except EmptyPile:
pass
if num_province < 5 and money >= 5:
try:
self.buy(card=duchy, supply=game.supply)
return
except EmptyPile:
pass
if num_province < 3 and money >= 2:
try:
self.buy(card=estate, supply=game.supply)
return
except EmptyPile:
pass
if money >= 6:
try:
self.buy(card=gold, supply=game.supply)
return
except EmptyPile:
pass
if num_province < 7 and money >= 5:
try:
self.buy(card=duchy, supply=game.supply)
return
except EmptyPile:
pass
if num_smithy < num_treasure / 11 and money >= 4:
try:
self.buy(card=smithy, supply=game.supply)
return
except EmptyPile:
pass
if money >= 3:
try:
self.buy(card=silver, supply=game.supply)
return
except EmptyPile:
pass
else:
logger.info(f"{self} buys nothing")
return
# self.buy(buy_card, supply=game.supply)
# logger.info(f"{self.player_id} buys {buy_card}")
35 changes: 27 additions & 8 deletions pyminion/models/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def __init__(self, cards: List[Card] = None):

def remove(self, card: Card) -> Card:
if len(self.cards) < 1:
raise EmptyPile
raise EmptyPile(f"{self.name} pile is empty, cannot gain card")
self.cards.remove(card)
return card

Expand Down Expand Up @@ -189,6 +189,7 @@ def draw(self, num_cards: int = 1, destination: AbstractDeck = None) -> None:
elif len(self.deck) == 0:
self.discard_pile.move_to(self.deck)
self.deck.shuffle()
self.shuffles += 1
destination.add(self.deck.draw())
else:
destination.add(self.deck.draw())
Expand All @@ -213,7 +214,7 @@ def play(self, target_card: Card, game: "Game", generic_play: bool = True) -> No
"""
for card in self.hand.cards:
if card == target_card:
if card.name == target_card.name:
if "Action" in card.type:
try:
card.play(player=self, game=game, generic_play=generic_play)
Expand Down Expand Up @@ -265,10 +266,14 @@ def buy(self, card: Card, supply: "Supply") -> None:
raise InsufficientBuys(
f"{self.player_id}: Not enough buys to buy {card.name}"
)
try:
supply.gain_card(card)
except EmptyPile as e:
raise e
self.state.money -= card.cost
self.state.buys -= 1
self.discard_pile.add(card)
supply.gain_card(card)
logger.info(f"{self} buys {card}")

def gain(
self, card: Card, supply: "Supply", destination: AbstractDeck = None
Expand All @@ -295,7 +300,7 @@ def trash(self, target_card: Card, trash: "Trash") -> None:
trash.add(self.hand.remove(card))
break

def start_turn(self):
def start_turn(self) -> None:
self.turns += 1
self.state.actions = 1
self.state.money = 0
Expand All @@ -315,6 +320,14 @@ def get_all_cards(self) -> List[Card]:
)
return all_cards

def get_card_count(self, card: Card) -> int:
"""
Get count of a specific card in player's whole deck
"""

return self.get_all_cards().count(card)

def get_victory_points(self) -> int:
total_vp: int = 0
for card in self.get_all_cards():
Expand Down Expand Up @@ -369,11 +382,11 @@ def gain_card(self, card: Card) -> Optional[Card]:
if card.name == pile.name:
try:
return pile.remove(card)
except EmptyPile:
logger.info("Pile is empty, you cannot gain that card")
return None

raise PileNotFound
except EmptyPile as e:
raise e

raise PileNotFound(f"{card} not found in the supply")

def return_card(self, card: Card):
"""
Expand Down Expand Up @@ -402,3 +415,9 @@ def num_empty_piles(self) -> int:
if len(pile) == 0:
empty_piles += 1
return empty_piles

def pile_length(self, pile_name: str) -> int:
for pile in self.piles:
if pile.name == pile_name:
return len(pile)
raise PileNotFound(f"{pile_name} pile is not valid")
2 changes: 1 addition & 1 deletion pyminion/players.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ def __init__(
def play(self, target_card: Card, game: "Game", generic_play: bool = True) -> None:
for card in self.hand.cards:
try:
if card == target_card and card.type == "Action":
if card.name == target_card.name and "Action" in card.type:
card.play(player=self, game=game, generic_play=generic_play)
return
except Exception as e:
Expand Down
1 change: 1 addition & 0 deletions pyminion/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def __init__(self, game: Game, iterations: int = 100):
self.winners: List[Union[Player, Human, Bot]] = None

def run(self) -> List[str]:
logger.info(f"Simulating {self.iterations} games...")
winners = []
for i in range(self.iterations):
game = copy.copy((self.game))
Expand Down
11 changes: 6 additions & 5 deletions tests/test_cards/test_actions/test_poacher.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ def test_poacher_no_empty_pile(human: Human, game: Game):


def test_poacher_one_empty_pile(human: Human, game: Game, monkeypatch):
for i in range(8):
# Single player game only has 5 of each victory card
for i in range(5):
game.supply.gain_card(card=estate)
assert game.supply.num_empty_piles() == 1
human.hand.add(poacher)
Expand All @@ -29,9 +30,9 @@ def test_poacher_one_empty_pile(human: Human, game: Game, monkeypatch):


def test_poacher_two_empty_piles(human: Human, game: Game, monkeypatch):
for i in range(8):
for i in range(5):
game.supply.gain_card(card=estate)
for i in range(8):
for i in range(5):
game.supply.gain_card(card=duchy)
assert game.supply.num_empty_piles() == 2
human.hand.add(poacher)
Expand All @@ -47,9 +48,9 @@ def test_poacher_two_empty_piles(human: Human, game: Game, monkeypatch):


def test_poacher_two_empty_piles_one_in_hand(human: Human, game: Game, monkeypatch):
for i in range(8):
for i in range(5):
game.supply.gain_card(card=estate)
for i in range(8):
for i in range(5):
game.supply.gain_card(card=duchy)
assert game.supply.num_empty_piles() == 2
human.hand.add(poacher)
Expand Down
11 changes: 6 additions & 5 deletions tests/test_core/test_game.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,19 @@ def test_game_is_over_false(game: Game):


def test_game_is_over_true_provinces(game: Game):
game.supply.gain_card(card=province)
assert not game.is_over()
for i in range(7):
# Single player game ony has 5 provinces
for i in range(4):
game.supply.gain_card(card=province)
assert not game.is_over()
game.supply.gain_card(card=province)
assert game.is_over()


def test_game_is_over_true_three_piles(game: Game):
for i in range(8):
for i in range(5):
game.supply.gain_card(card=estate)
assert not game.is_over()
for i in range(8):
for i in range(5):
game.supply.gain_card(card=duchy)
assert not game.is_over()
for i in range(29):
Expand Down
54 changes: 53 additions & 1 deletion tests/test_core/test_player.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import pytest

from pyminion.exceptions import (
InsufficientActions,
InsufficientBuys,
Expand All @@ -14,8 +13,11 @@
duchy,
estate,
gardens,
market,
poacher,
province,
smithy,
vassal,
)
from pyminion.models.core import DiscardPile, Hand, Player, Playmat, Supply, Trash

Expand Down Expand Up @@ -209,3 +211,53 @@ def test_player_draw_to_discard(player: Player):
assert len(player.discard_pile) == 0
player.draw(num_cards=1, destination=player.discard_pile)
assert len(player.discard_pile) == 1


def test_shuffle_count(player: Player):
assert player.shuffles == 0
player.discard_pile.add(copper)
player.draw(11)
assert player.shuffles == 1
player.discard_pile.add(copper)
player.draw(1)
assert player.shuffles == 2


def test_treasure_money(player: Player):
assert player.get_treasure_money() == 7
player.deck.add(copper)
assert player.get_treasure_money() == 8


def test_action_money(player: Player):
assert player.get_action_money() == 0
player.deck.add(copper)
assert player.get_action_money() == 0
player.deck.add(vassal)
assert player.get_action_money() == 2
player.hand.add(market)
assert player.get_action_money() == 3
player.discard_pile.add(poacher)
assert player.get_action_money() == 4


def test_deck_money(player: Player):
assert player.get_deck_money() == 7
player.hand.add(copper)
player.deck.add(vassal)
assert player.get_deck_money() == 10


def test_all_cards(player: Player):
assert len(player.get_all_cards()) == 10
player.hand.add(copper)
player.discard_pile.add(copper)
assert len(player.get_all_cards()) == 12


def test_card_count(player: Player):
assert player.get_card_count(card=copper) == 7
assert player.get_card_count(card=estate) == 3
assert player.get_card_count(card=smithy) == 0
player.hand.add(smithy)
assert player.get_card_count(card=smithy) == 1
Loading

0 comments on commit cda309c

Please sign in to comment.