commit 0c23f99667fa154fd6a3749879e9803a90c1a535 Author: zevav Date: Thu May 21 00:10:31 2020 +0200 first diff --git a/exceptions.py b/exceptions.py new file mode 100644 index 0000000..c22febb --- /dev/null +++ b/exceptions.py @@ -0,0 +1,14 @@ +class TooManyPlayers(Exception): + pass + + +class NotEnough(Exception): + pass + + +class Argument(Exception): + pass + + +class DidntFind(Exception): + pass diff --git a/monopoly.py b/monopoly.py new file mode 100644 index 0000000..bc69a79 --- /dev/null +++ b/monopoly.py @@ -0,0 +1,757 @@ +""" +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")