1153 lines
33 KiB
Python
1153 lines
33 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
|
||
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 can’t put a
|
||
second house on New York Ave until you have a first house on St. James and Tennessee.
|
||
Then you can’t 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
|