Files
monopoly_sim/monopoly.py

1153 lines
33 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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
https://blog.ed.ted.com/2017/12/01/heres-how-to-win-at-monopoly-according-to-math-experts/
TODO: maybe instead of all these classmethods, instances?
TODO: something more graceful than Game.games[0]
TODO: store LAST_ROLL in a global constant instead of passing it around to all the `action` methods
TODO: write some tests
TODO: break up into modules
TODO: add auctions
TODO: print the reason someone decided to/not to buy a property
TODO: print whether someone is in jail or just visiting
TODO: don't allow buying of multiple houses/a hotel on a property if the other properties in the same color don't have any
"""
from abc import ABC
from collections import defaultdict
from itertools import cycle
from random import choice, shuffle
from time import sleep
from typing import cast, List, NewType, Optional, Tuple, Type
from exceptions import (
Argument,
CantBuyBuildings,
CantMortgage,
DidntFind,
MustBeEqualAmounts,
NoOwner,
NotEnough,
TooMany,
TooManyPlayers,
NotEnoughPlayers,
)
ALL_MONEY = 20_580
NUM_HOUSES = 32
NUM_HOTELS = 12
Doubles = NewType("Doubles", bool)
LANGUAGE = "français"
BACKUP_LANGUAGE = "English"
BUILDING_TYPES = "house", "hotel"
MAX_ROUNDS = 5000
BUILDABLE_PROPERTY_COLORS = (
"yellow",
"red",
"light blue",
"brown",
"pink",
"orange",
"green",
"dark blue",
)
class Space:
def __repr__(self):
if hasattr(self, "_name"):
return self._name
return self.__name__
class NothingHappensWhenYouLandOnItSpace(Space):
@classmethod
def action(self, player: "Player", _):
pass
class Go(NothingHappensWhenYouLandOnItSpace):
"""
should this really be a NothingHappensWhenYouLandOnItSpace?
since you get to collect $200
"""
pass
class Jail(NothingHappensWhenYouLandOnItSpace):
pass
class FreeParking(NothingHappensWhenYouLandOnItSpace):
pass
class TaxSpace(Space):
amount = None
@classmethod
def action(cls, player, _):
print(f"{player} pays bank {cls.amount} ({cls.__name__})")
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()
pass
class CardSpace(Space):
deck = None
@classmethod
def action(cls, player, _):
# for lazy loading to avoid circular imports (?)
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
@classmethod
def action(self, player: "Player", _):
raise NotImplementedError
def __repr__(self):
return self.text
class ElectedPresidentCard(Card):
mandatory_action = True
text = {
"français": "Vous avez été elu president du conseil d'administration. Versez M50 à chaque joueur."
}
@classmethod
def action(cls, player, _):
print("{cls.__name__}")
for other_player in Game.games[0].active_players:
if other_player != player:
print(f"{player} is paying {other_player} 50")
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. "
}
@classmethod
def action(cls, player: "Player", _):
print(f"{player} got a get out of jail free card")
player.get_out_of_jail_free_card = True
class AdvanceCard(Card):
mandatory_action = True
kwarg = {}
@classmethod
def action(cls, player, _):
print(f"the card is {cls.__name__}")
player.advance(**cls.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.",
}
@classmethod
def action(self, player, _):
print(f"{self.__class__}: the bank pays {player} 150")
Bank.pay(player, 150)
class SpeedingCard(Card):
mandatory_action = True
text = {"français": "Amende pour excès de vitesse. Payez M15."}
@classmethod
def action(cls, player, _):
print(f"{cls.__name__}: {player} pays 15 to Bank")
player.pay(Bank, 15)
class RepairPropertyCard(Card):
text = {
"français": "Vous faites des réparations sur toutes vos propriétés: Versez M25"
" pour chaque maison M100 pour Chaque hôtel que vous possédez"
}
@classmethod
def action(cls, player, _):
num_houses, num_hotels = 0, 0
for property in player.buildable_properties:
num_houses += property.buildings["house"]
num_hotels += property.buildings["hotel"]
total_owed = sum([num_houses * 25, num_hotels * 100])
print(f"{player} pays the bank {total_owed} for {cls.__name__}")
player.pay(Bank, total_owed)
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,
GoToClosestRailroadCard,
]
class CommunityChestDeck(Deck):
# TODO: remove these and add the readl ones!
deck = [GoToJailCard, SpeedingCard]
def shuffle_decks():
for deck in (ChanceDeck, CommunityChestDeck):
deck.shuffle()
def buy_decision(property: "Property", player: "Player"):
return Game.games[0].buy_decision_algorithm(property, player)
class Decision:
pass
class Property(Space):
mortgaged = False
owner = None
cost = 0
instances = []
type = None
def __init__(self, _name):
self._name = _name
self.instances.append(self)
def __repr__(self):
if hasattr(self, "_name"):
return self._name[LANGUAGE]
return str(self.__class__)
@classmethod
def reset(cls):
for property in cls.instances:
property.owner = None
@classmethod
def get_num_of_type(cls, type):
return len(cls.instances_by_type()[type])
@property
def num_of_type(self):
return self.get_num_of_type(self.type)
@property
def properties_of_type(self):
return self.instances_by_type()[self.type]
@classmethod
def instances_by_type(cls):
ibt = defaultdict(list)
for i in cls.instances:
ibt[i.type].append(i)
return ibt
def action(self, player: "Player", last_roll=None):
if not self.owner:
buy = buy_decision(self, player)
if buy:
print(f"{player} will buy {self}")
return player.buy(self)
print(f"{player} decided not to buy {self}")
return
if self.owner == player or self.mortgaged:
print(f"{player} landed on his own property, {self}")
return
rent = self.calculate_rent(last_roll)
print(f"{player} pays {self.owner} ${rent} after landing on it.")
player.pay(self.owner, rent)
def calculate_rent(self, _):
if not self.owner:
raise NoOwner
class Utility(Property):
cost = 200
rent = {
0: lambda _: 0,
1: lambda dice_total: dice_total * 4,
2: lambda dice_total: dice_total * 10,
}
mortgage_cost = 75
unmortgage_cost = 83
type = "utility"
def calculate_rent(self, last_roll: int):
super().calculate_rent(last_roll)
if not last_roll:
return 10 * Player.roll_the_dice()[0]
return self.rent[self.owner.owns_x_of_type(self.type)](last_roll)
class Railroad(Property):
cost = 200
rent = {1: 25, 2: 50, 3: 100, 4: 200}
mortgage_cost = 100
unmortgage_cost = 110
type = "railroad"
def calculate_rent(self, _):
super().calculate_rent(_)
owns_x_of_type = self.owner.owns_x_of_type(self.type)
if not owns_x_of_type:
return 0
return self.rent[owns_x_of_type]
class BuildableProperty(Property):
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.type = color
self.mortgage_cost = mortgage_cost
self.unmortgage_cost = unmortgage_cost
self.buildings = {"house": 0, "hotel": 0}
def buy_building(self, building_type):
"""
TODO: Each property within a group must be no more than one house level away from all other
properties in that group. For example, if you own the Orange group, you cant put a
second house on New York Ave until you have a first house on St. James and Tennessee.
Then you cant put a third house on any property until you have two houses on
all properties.
"""
if not self.owner.owns_all_type(self.type):
raise CantBuyBuildings
if building_type == "hotel" and self.buildings["house"] != 4:
raise NotEnough
elif building_type == "house" and self.buildings["house"] == 4:
raise TooMany
cost = self.house_and_hotel_cost
self.owner.check_funds(cost)
Bank.get_building(building_type)
self.owner.pay(Bank, cost)
for property_ in self.properties_of_type:
if building_type == "hotel":
property_.buildings["house"] = 0
property_.buildings["hotel"] = 1
else:
property_.buildings["house"] += 1
def sell_buildings(self, building_type, quantity):
if not self.buildings[building_type]:
raise NotEnough
if quantity % self.num_of_type:
# TODO: this isn't right
# https://www.quora.com/When-can-a-player-place-a-house-in-monopoly
raise MustBeEqualAmounts
def mortgage(self, player: "Player"):
if self.buildings:
raise CantMortgage
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, _):
super().calculate_rent(_)
if self.buildings["house"] or self.buildings["hotel"]:
buildings = self.buildings
if buildings["house"]:
key = buildings["house"]
else:
key = "hotel"
elif self.owner.owns_all_type(self.type):
key = "monopoly"
else:
key = 0
return self.rent[key]
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,
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={"français": "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={"français": "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={"français": "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={"français": "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={"français": "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={"français": "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:
pass
class Bank(EconomicActor):
money = ALL_MONEY
NUM_HOUSES = NUM_HOUSES
NUM_HOTELS = NUM_HOTELS
@classmethod
def reset(cls):
cls.money = ALL_MONEY
cls.NUM_HOUSES = NUM_HOUSES
cls.NUM_HOTELS = NUM_HOTELS
@classmethod
def pay(cls, actor: "EconomicActor", amount: int):
if isinstance(actor, str):
actor = eval(actor)
cls.money -= amount
actor.money += amount
@classmethod
def get_building(cls, type_):
cls.check_building_type(type_)
store = cls.get_building_store(type_)
if not store:
raise NotEnough(f"Not enough {type_}s!")
else:
store -= 1
@classmethod
def put_building(cls, type_, quantity):
cls.check_building_type(type_)
store = cls.get_building_store(type_)
store += quantity
@staticmethod
def check_building_type(type_):
if type_ not in BUILDING_TYPES:
raise TypeError
@classmethod
def get_building_store(cls, type_):
return cls.NUM_HOUSES if type_ == "house" else cls.NUM_HOTELS
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(until_space_type, str):
until_space_type = eval(until_space_type)
if Board.spaces[index] == until_space_type or isinstance(
Board.spaces[index], until_space_type
):
return index
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
def get_property_with_least_number_of_houses(properties):
return sorted(properties, key=lambda prop: prop.buildings["house"], reverse=True)[0]
def get_property_with_no_hotels(properties):
return sorted(properties, key=lambda prop: prop.buildings["hotel"])[0]
class Monopoly:
def __init__(self, property_: "BuildableProperty"):
self.properties = Property.instances_by_type()[property_.type]
self.num_properties = len(self.properties)
self.max_num_houses = 4 * self.num_properties
self.max_num_hotels = self.num_properties
def __repr__(self):
return f"<Monopoly type={self.properties[0].type}"
@property
def num_houses(self):
return sum(property.buildings["house"] for property in self.properties)
@property
def num_hotels(self):
return sum(property.buildings["hotel"] for property in self.properties)
@property
def next_building(self) -> Tuple[Optional[str], Optional["BuildableProperty"]]:
num_houses, num_hotels, max_num_houses, max_num_hotels = (
self.num_houses,
self.num_hotels,
self.max_num_houses,
self.max_num_hotels,
)
first_prop = self.properties[0]
if not num_houses and not num_hotels:
return "house", first_prop
elif num_hotels == max_num_hotels:
return None, None
elif num_houses < max_num_houses:
if not num_hotels:
return (
"house",
get_property_with_least_number_of_houses(self.properties),
)
else:
return "hotel", get_property_with_no_hotels(self.properties)
elif num_houses == max_num_houses:
return "hotel", first_prop
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")
money = 0
passed_go_times = 0
monopolies = []
def __str__(self):
return self.name
def __init__(self):
self.name = choice([str(i) for i in range(10_000)])
Bank.pay(self, 1_500)
def pay(self, actor: Type["EconomicActor"], amount: int):
if isinstance(actor, str):
actor = eval(actor)
self.check_funds(amount)
self.money -= amount
actor.money += amount
def check_funds(self, amount):
if amount > self.money:
raise NotEnough
def buy(self, property_: "Property", from_=Bank, cost=None):
try:
self.pay(from_, cost or property_.cost)
except NotEnough:
return
property_.owner = self
if property_.__class__.__name__ == "BuildableProperty" and self.owns_all_type(
property_.type
):
monopoly = Monopoly(property_)
self.monopolies.append(monopoly)
def buy_buildings_if_possible(self):
if self.monopolies:
print(f"{self} has {self.monopolies}")
else:
print(f"{self} has no monopolies.")
for monopoly in self.monopolies:
while True:
next_building_type, property_ = monopoly.next_building
if not next_building_type:
break
print("next_building_type:", next_building_type, "property_:", property_)
if not self.can_afford(property_.house_and_hotel_cost):
print("can't afford")
break
try:
property_.buy_building(next_building_type)
except NotEnough:
print("can't afford")
break
print("bought a building")
def take_a_turn(self):
if self.in_jail:
print(f"{self} is in jail")
decision = GetOutOfJailDecision(self)
print(decision)
return decision
# TODO: you can buy buildings from jail! Fix this
self.buy_buildings_if_possible()
num_spaces, doubles = self.roll_the_dice()
print(f'{self} rolled', str(num_spaces))
if doubles:
self.go_again = True
else:
self.go_again = False
self.advance(num_spaces, just_rolled=True)
def owns_x_of_type(self, type_):
properties_of_this_type = self.properties_by_type.get(type_)
if properties_of_this_type is None:
return 0
if properties_of_this_type is None:
return 0
return len(properties_of_this_type)
def owns_all_type(self, type_):
return self.owns_x_of_type(type_) == Property.get_num_of_type(type_)
@property
def properties_by_type(self):
pbt = defaultdict(list)
for property in self.properties:
pbt[property.type].append(property)
return pbt
@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)
@property
def assets(self):
return self.money + self.total_property_mortgage_value
@property
def total_property_mortgage_value(self):
return sum(property.mortgage_cost for property in self.properties)
@property
def properties(self) -> List["Property"]:
# TODO: create an `instances` class attribute on `Property` that keeps track of them all
# then iterate through those instances to see which ones have an owner equal to `self`
return [p for p in Property.instances if p.owner == self]
@property
def buildable_properties(self) -> List[BuildableProperty]:
return [
p for p in self.properties if p.__class__.__name__ == "BuildableProperty"
]
def advance(
self,
num_spaces=None,
space_index=None,
until_space_type=None,
pass_go=True,
just_rolled=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(
self.current_space_index, until_space_type
)
if pass_go and new_space_index >= Board.NUM_SPACES - 1:
self.money += 200
print(f"{self} passed go and collected 200")
self.passed_go_times += 1
new_space_index = new_space_index - Board.NUM_SPACES
elif pass_go and self.current_space_index > new_space_index:
self.money += 200
print(f"{self} passed go and collected 200")
self.passed_go_times += 1
print("new_space_index", str(new_space_index))
self.current_space_index = new_space_index
if just_rolled:
last_roll = num_spaces
else:
last_roll = None
try:
self.do_action_of_current_space(last_roll=last_roll)
except NotEnough:
# TODO: is this always right?
# TODO: eventually make deals and mortgage prtoperties to avoid bankruptcy
self.bankrupt = True
print(f"{self} just went bankrupt!")
def do_action_of_current_space(self, last_roll=None):
space = Board.spaces[self.current_space_index]
print(f"space is {space}")
space.action(self, last_roll)
def go_to_jail(self):
self.in_jail = True
self.current_space_index = get_space_index("Jail")
def can_afford(self, cost):
return self.money >= cost
class Game:
games = []
rounds = 0
def __init__(self, num_players, buy_decision_algorithm, slow_down=False):
self.slow_down = slow_down
Bank.reset()
shuffle_decks()
Property.reset()
self.buy_decision_algorithm = buy_decision_algorithm()
# TODO: make this nicer
if self.games:
del self.games[0]
self.games.append(self)
if num_players < 2:
raise NotEnoughPlayers
if num_players > 8:
raise TooManyPlayers
self._players = [Player() for _ in range(num_players)]
self.players = cycle(self._players)
# TODO: roll to see who goes first, then order the players accordingly
self.start()
@property
def active_players(self):
return [player for player in self._players if not player.bankrupt]
def start(self):
while len(self.active_players) > 1 and self.rounds < MAX_ROUNDS:
current_player = next(self.players)
if not current_player.bankrupt:
if self.slow_down:
sleep(3)
print()
print()
current_player.take_a_turn()
while current_player.go_again and not current_player.bankrupt:
if self.slow_down:
sleep(3)
print()
print()
current_player.take_a_turn()
self.rounds += 1
def get_rounds_played_per_player(self):
return self.rounds / len(self._players)
def end(self):
for player in self._players:
del player