Files
monopoly_sim/monopoly.py
2020-05-21 00:10:31 +02:00

758 lines
20 KiB
Python

"""
Purpose:
1) To simulate games of Monopoly in order to determine the best strategy
2) To play Monopoly on a computer
Along those lines, here's some prior art:
http://www.tkcs-collins.com/truman/monopoly/monopoly.shtml
# TODO: until_space_type -- evaluate this as a string
"""
from abc import abstractmethod, ABC
from itertools import cycle
from pprint import pprint
from random import shuffle, choice
from typing import Type, NewType, Tuple, cast
from exceptions import TooManyPlayers, NotEnough, DidntFind, Argument
Doubles = NewType("Doubles", bool)
LANGUAGE = "français"
BACKUP_LANGUAGE = "English"
class Space:
pass
class NothingHappensWhenYouLandOnItSpace(Space):
@classmethod
def action(self, player: "Player"):
pass
class Go(NothingHappensWhenYouLandOnItSpace):
pass
class Jail(NothingHappensWhenYouLandOnItSpace):
pass
class FreeParking(NothingHappensWhenYouLandOnItSpace):
pass
class TaxSpace(Space):
amount = None
@classmethod
def action(cls, player):
player.pay("Bank", cls.amount)
class LuxuryTax(TaxSpace):
_name = {"français": "Impôt Supplémentaire", "deutsch": "Nachsteuer"}
amount = 75
class IncomeTax(TaxSpace):
# TODO: _name
amount = 100
class GoToJail(Space):
@classmethod
def action(cls, player):
player.go_to_jail()
class CardSpace(Space):
deck = None
@classmethod
def action(cls, player):
deck = eval(cls.deck)
card = deck.get_card()
return card.action(player)
class CommunityChest(CardSpace):
deck = "CommunityChestDeck"
_name = {"français": "Chancellerie", "deutsch": "Kanzlei"}
class Chance(CardSpace):
deck = "ChanceDeck"
_name = {"français": "Chance", "deutsch": "Chance", "English": "Chance"}
class Card(ABC):
mandatory_action = False
cost = None
keep = False
@abstractmethod
def action(self, player: "Player"):
raise NotImplementedError
class ElectedPresidentCard(Card):
mandatory_action = True
text = {
"français": "Vous avez été elu president du conseil d'administration. Versez M50 à chaque joueur."
}
def action(self, player):
for other_player in Game.game.active_players:
if other_player != player:
player.pay(other_player, 50)
class GetOutOfJailFreeCard(Card):
text = {
"français": "Vous êtes libéré de prison. Cette carte peut être conservée jusqu'à ce "
"qu'elle soit utilisée ou vendue. "
}
def action(self, player: "Player"):
player.get_out_of_jail_free_card = True
class AdvanceCard(Card):
mandatory_action = True
kwarg = {}
def action(self, player):
player.advance(**self.kwarg)
class GoToJailCard(AdvanceCard):
text = {
"français": "Allez en prison. Avancez tout droit en prison. Ne passez pas par la case "
"départ. Ne recevez pas M200. "
}
kwarg = {"until_space_type": "Jail", "pass_go": False}
class AdvanceThreeSpacesCard(AdvanceCard):
mandatory_action = True
text = {"français": "Reculez de trois cases."}
kwarg = {"num_spaces": 3}
class GoToClosestRailroadCard(AdvanceCard):
text = {"français": "Avancez jusqu'à le chemin de fer le plus proche."}
kwarg = {"until_space_type": "Railroad"}
class GoToBernPlaceFederaleCard(AdvanceCard):
mandatory_action = True
text = {
"français": "Avancez jusqu'a Bern Place Federale. Si vous passez par la case départ, "
"vous touchez la prime habituelle de M200. "
}
kwarg = {"space_index": "Berne Place Fédérale"}
class BuildingAndLoanMaturesCard(Card):
mandatory_action = True
text = {
"français": "Votre immeuble et votre pret rapportent. Vous devez toucher M150.",
"English": "Your building and loan matures. Collect M150.",
}
def action(self, player):
Bank.pay(player, 150)
class SpeedingCard(Card):
mandatory_action = True
text = {"français": "Amende pour excès de vitesse. Payez M15."}
def action(self, player):
player.pay(Bank, 15)
class Deck:
deck = None
@classmethod
def shuffle(cls):
shuffle(cls.deck)
@classmethod
def get_card(cls):
card = cls.deck.pop()
if not card.keep:
cls.deck.insert(0, card)
return card
class ChanceDeck(Deck):
deck = [
AdvanceThreeSpacesCard,
ElectedPresidentCard,
BuildingAndLoanMaturesCard,
GoToBernPlaceFederaleCard,
GoToJailCard,
SpeedingCard,
]
class CommunityChestDeck(Deck):
deck = []
def shuffle_decks():
for deck in (ChanceDeck, CommunityChestDeck):
deck.shuffle()
class Decision:
pass
class BuyDecision(Decision):
def __init__(self, property: Type["Property"], player: "Player"):
pass
class Property(Space):
mortgaged = False
def __init__(self, _name):
self._name = _name
class Utility(Property):
cost = 200
rent = {1: lambda dice_total: dice_total * 4, 2: lambda dice_total: dice_total * 10}
mortgage_cost = 75
unmortgage_cost = 83
class Railroad(Property):
cost = 200
rent = ({1: 25, 2: 50, 3: 100, 4: 200},)
mortgage_cost = 100
unmortgage_cost = 110
class BuildableProperty(Property):
owner = None
def __init__(
self,
_name,
cost,
rent,
color,
house_and_hotel_cost,
mortgage_cost,
unmortgage_cost,
):
super().__init__(_name)
self.cost = cost
self.rent = rent
self.house_and_hotel_cost = house_and_hotel_cost
self.color = color
self.mortgage_cost = mortgage_cost
self.unmortgage_cost = unmortgage_cost
def action(self, player: "Player"):
# TODO: implement on Property, then extend here
if self.owner:
if self.mortgaged:
return
player.pay(self.owner, self.calculate_rent())
return BuyDecision(self, player)
def mortgage(self, player: "Player"):
Bank.pay(player, self.mortgage_cost)
self.mortgaged = True
def un_mortgage(self, player: "Player"):
player.pay(Bank, self.unmortgage_cost)
self.mortgaged = False
def calculate_rent(self):
raise NotImplementedError
# if not self.owner:
# raise NoOwner
# if self.buildings:
# key = self.buildings
# else:
# if self.owner.owns_all_type(self.)
class Board:
spaces = [
Go,
BuildableProperty(
_name={"français": "Coire Kornplatz"},
cost=60,
color="brown",
rent={0: 2, "monopoly": 4, 1: 10, 2: 30, 3: 90, 4: 160, "hotel": 250},
house_and_hotel_cost=50,
# TODO
mortgage_cost=30,
unmortgage_cost=33,
),
CommunityChest,
BuildableProperty(
_name={"français": "Schaffhouse Vordergasse"},
cost=60,
color="brown",
rent={0: 4, "monopoly": 8, 1: 20, 2: 60, 3: 180, 4: 320, "hotel": 450},
house_and_hotel_cost=50,
mortgage_cost=30,
unmortgage_cost=33,
),
IncomeTax,
Railroad(_name={"français": "Union des Chemins de Fer Privés"}),
BuildableProperty(
_name={"deutsche": "Aarau Rathausplatz"},
cost=100,
color="light blue",
rent={0: 6, "monopoly": 12, 1: 30, 2: 90, 3: 270, 4: 400, "hotel": 550},
house_and_hotel_cost=50,
mortgage_cost=50,
unmortgage_cost=55,
),
Chance,
BuildableProperty(
_name={"français": "Neuchâtel Place Pury"},
cost=100,
color="light blue",
rent={0: 6, "monopoly": 12, 1: 30, 2: 90, 3: 270, 4: 400, "hotel": 550},
house_and_hotel_cost=50,
mortgage_cost=50,
unmortgage_cost=55,
),
BuildableProperty(
_name={"français": "Thoune Hauptgasse"},
cost=120,
color="light blue",
rent={0: 8, "monopoly": 16, 1: 30, 2: 100, 3: 300, 4: 400, "hotel": 600},
house_and_hotel_cost=50,
mortgage_cost=60,
unmortgage_cost=66,
),
Jail,
BuildableProperty(
_name={"français": "Bâle Steinen-Vorstadt"},
cost=140,
color="pink",
rent={0: 10, "monopoly": 20, 1: 50, 2: 150, 3: 450, 4: 625, "hotel": 750},
house_and_hotel_cost=100,
mortgage_cost=70,
unmortgage_cost=77,
),
Utility(_name="Usines Électriques"),
BuildableProperty(
_name={"français": "Soleure Hauptgasse"},
cost=140,
color="pink",
rent={0: 10, "monopoly": 20, 1: 50, 2: 150, 3: 450, 4: 625, "hotel": 750},
house_and_hotel_cost=100,
mortgage_cost=70,
unmortgage_cost=77,
),
BuildableProperty(
_name={"italian": "Lugano Via Nassa"},
cost=160,
color="pink",
rent={0: 12, "monopoly": 24, 1: 60, 2: 180, 3: 500, 4: 700, "hotel": 900},
house_and_hotel_cost=100,
mortgage_cost=80,
unmortgage_cost=88,
),
BuildableProperty(
_name={"français": "Bienne Rue De Nidau"},
cost=180,
color="orange",
rent={0: 14, "monopoly": 28, 1: 70, 2: 200, 3: 550, 4: 750, "hotel": 950},
house_and_hotel_cost=100,
mortgage_cost=90,
unmortgage_cost=99,
),
CommunityChest,
BuildableProperty(
_name={"français": "Fribourg Avenue de la Gare"},
cost=180,
color="orange",
rent={0: 14, "monopoly": 28, 1: 70, 2: 200, 3: 550, 4: 750, "hotel": 950},
house_and_hotel_cost=100,
mortgage_cost=90,
unmortgage_cost=99,
),
BuildableProperty(
_name={"français": "La Chaux-de-Fonds Avenue Louis-Robert"},
cost=200,
color="orange",
rent={0: 16, "monopoly": 32, 1: 80, 2: 220, 3: 600, 4: 800, "hotel": 1_000},
house_and_hotel_cost=100,
mortgage_cost=100,
unmortgage_cost=110,
),
FreeParking,
BuildableProperty(
_name={"français": "Winterthour Bahnhofplatz"},
cost=220,
color="red",
rent={0: 18, "monopoly": 39, 1: 90, 2: 250, 3: 700, 4: 875, "hotel": 1_050},
house_and_hotel_cost=150,
mortgage_cost=110,
unmortgage_cost=121,
),
Chance,
BuildableProperty(
_name={"français": "St-Gall Markplatz"},
cost=220,
color="red",
rent={0: 18, "monopoly": 39, 1: 90, 2: 250, 3: 700, 4: 875, "hotel": 1_050},
house_and_hotel_cost=150,
mortgage_cost=110,
unmortgage_cost=121,
),
BuildableProperty(
_name={"français": "Berne Place Fédérale"},
cost=240,
color="red",
rent={
0: 20,
"monopoly": 40,
1: 100,
2: 300,
3: 750,
4: 925,
"hotel": 1_100,
},
house_and_hotel_cost=150,
mortgage_cost=120,
unmortgage_cost=132,
),
Railroad(_name="Tramways Interurbains"),
BuildableProperty(
_name={"français": "Lucerne Weggisgasse"},
cost=260,
color="yellow",
rent={
0: 22,
"monopoly": 34,
1: 110,
2: 330,
3: 800,
4: 975,
"hotel": 1_150,
},
house_and_hotel_cost=150,
mortgage_cost=130,
unmortgage_cost=143,
),
BuildableProperty(
_name={"français": "Zurich Rennweg"},
cost=260,
color="yellow",
rent={
0: 22,
"monopoly": 34,
1: 110,
2: 330,
3: 800,
4: 975,
"hotel": 1_150,
},
house_and_hotel_cost=150,
mortgage_cost=130,
unmortgage_cost=143,
),
Utility(_name="Usines Hydrauliques"),
BuildableProperty(
_name={"français": "Lausanne Rue de Bourg"},
cost=280,
color="yellow",
rent={
0: 24,
"monopoly": 48,
1: 120,
2: 360,
3: 850,
4: 1_025,
"hotel": 1_200,
},
house_and_hotel_cost=150,
mortgage_cost=140,
unmortgage_cost=154,
),
GoToJail,
BuildableProperty(
_name={"français": "Bâle Freie Strasse"},
cost=300,
color="green",
rent={
0: 26,
"monopoly": 52,
1: 130,
2: 390,
3: 900,
4: 1_100,
"hotel": 1_275,
},
house_and_hotel_cost=200,
mortgage_cost=150,
unmortgage_cost=165,
),
BuildableProperty(
_name={"français": "Genève Rue de la Croix-D'Or"},
cost=300,
color="green",
rent={
0: 26,
"monopoly": 52,
1: 130,
2: 390,
3: 900,
4: 1_100,
"hotel": 1_275,
},
house_and_hotel_cost=200,
mortgage_cost=150,
unmortgage_cost=165,
),
CommunityChest,
BuildableProperty(
_name={"français": "Berne Spitalgasse"},
cost=320,
color="green",
rent={
0: 28,
"monopoly": 56,
1: 150,
2: 450,
3: 1_000,
4: 1_200,
"hotel": 1_400,
},
house_and_hotel_cost=200,
mortgage_cost=160,
unmortgage_cost=176,
),
Railroad(_name="Association des Télépheriques"),
Chance,
BuildableProperty(
_name={"français": "Lausanne Place St. François"},
cost=350,
color="dark blue",
rent={
0: 35,
"monopoly": 70,
1: 175,
2: 500,
3: 1_100,
4: 1_300,
"hotel": 1_500,
},
house_and_hotel_cost=200,
mortgage_cost=175,
unmortgage_cost=193,
),
LuxuryTax,
BuildableProperty(
_name={"français": "Zurich Paradeplatz"},
cost=400,
color="dark blue",
rent={
0: 50,
"monopoly": 100,
1: 200,
2: 600,
3: 1_400,
4: 1_700,
"hotel": 2_000,
},
house_and_hotel_cost=200,
mortgage_cost=200,
unmortgage_cost=220,
),
]
SPACES_DICT = {}
for index, space in enumerate(spaces):
if hasattr(space, "_name"):
space_name = space._name
else:
space_name = space.__name__
if isinstance(space_name, dict):
space_name = space._name.get(LANGUAGE) or space._name.get(BACKUP_LANGUAGE)
SPACES_DICT[space_name] = index
# SPACES_DICT = {space.name: space for space in spaces}
NUM_SPACES = len(spaces)
def get_space_index(name):
return Board.SPACES_DICT[name]
class EconomicActor:
money = 0
@classmethod
def pay(cls, actor: Type["EconomicActor"], amount: int):
if amount > cls.money:
print(cls)
print("actor:", actor)
print("cls.money:", cls.money)
raise NotEnough
if isinstance(actor, str):
actor = eval(actor)
cls.money -= amount
actor.money += amount
class Bank(EconomicActor):
money = 20_580
NUM_HOUSES = 32
NUM_HOTELS = 12
@classmethod
def get_building(cls, type_, quantity):
if type_ not in ("house", "hotel"):
raise Argument
to_check = cls.NUM_HOUSES if type_ == "house" else cls.NUM_HOTELS
if to_check < quantity:
raise (f"Not enough {type_}s!")
else:
to_check -= quantity
def get_index_of_next_space_of_type(current_space_index, until_space_type):
space_indices_to_traverse = list(
range(current_space_index + 1, Board.NUM_SPACES)
) + list(range(current_space_index))
for index in space_indices_to_traverse:
if isinstance(Board.spaces[index], until_space_type):
return index
else:
# for debugging TODO: delete
print(type(Board.spaces[index]))
else:
# for debugging TODO: delete
raise DidntFind
def check_args(num_spaces, space_index, until_space_type):
num_args = sum(
1 for kwarg in (num_spaces, space_index, until_space_type) if kwarg is not None
)
if num_args > 1 or num_args == 0:
raise Argument("provide either num_spaces or space_index or until_space_type")
class GetOutOfJailDecision(Decision):
def __init__(self, player: "Player"):
pass
class Player(EconomicActor):
in_jail = False
bankrupt = False
get_out_of_jail_free_card = False
go_again = False
current_space_index = get_space_index("Go")
def __init__(self, name):
self.name = name
Bank.pay(self, 1_500)
def take_a_turn(self):
if self.in_jail:
return GetOutOfJailDecision(self)
num_spaces, doubles = self.roll_the_dice()
self.go_again = doubles
self.advance(num_spaces)
@staticmethod
def roll_the_dice() -> Tuple[int, Doubles]:
die_one, die_two = choice(range(1, 7)), choice(range(1, 7))
total = die_one + die_two
if die_one == die_two:
return total, cast(Doubles, True)
return total, cast(Doubles, False)
def advance(
self, num_spaces=None, space_index=None, until_space_type=None, pass_go=True
):
new_space_index = None
check_args(num_spaces, space_index, until_space_type)
if num_spaces:
new_space_index = self.current_space_index + num_spaces
elif space_index:
if isinstance(space_index, str):
space_index = get_space_index(space_index)
new_space_index = space_index
elif until_space_type:
new_space_index = get_index_of_next_space_of_type(until_space_type)
if pass_go and new_space_index >= Board.NUM_SPACES - 1:
print("You passed go! Here's 200 Monopoly Dollars")
self.money += 200
new_space_index = new_space_index - Board.NUM_SPACES
elif pass_go and self.current_space_index > new_space_index:
print("You passed go! Here's 200 Monopoly Dollars")
self.money += 200
self.current_space_index = new_space_index
self.do_action_of_current_space()
def do_action_of_current_space(self):
space = Board.spaces[self.current_space_index]
print(space)
space.action(self)
def go_to_jail(self):
self.in_jail = True
self.current_space_index = get_space_index("Jail")
class Game:
game = None
def __init__(self, *player_names):
self.game = self
if len(player_names) > 8:
raise TooManyPlayers
self._players = [Player(player_name) for player_name in player_names]
self.players = cycle(self._players)
# TODO: roll to see who goes first, then order the players accordingly
shuffle_decks()
self.next()
@property
def active_players(self):
return [player for player in self._players if not player.bankrupt]
def next(self):
while len(self.active_players) > 1:
current_player = next(self.players)
if not current_player.bankrupt:
current_player.take_a_turn()
while current_player.go_again:
current_player.take_a_turn()
self.end()
def end(self):
print(self.active_players[0], "is the winner!")
game = Game("Bot", "Ro")