Coverage for monopoly.py : 48%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2Purpose:
41) To simulate games of Monopoly in order to determine the best strategy
52) To play Monopoly on a computer
7Along those lines, here's some prior art:
9http://www.tkcs-collins.com/truman/monopoly/monopoly.shtml
10https://blog.ed.ted.com/2017/12/01/heres-how-to-win-at-monopoly-according-to-math-experts/
13# TODO: maybe instead of all these classmethods, instances?
14# TODO: something more graceful than Game.games[0]
15# TODO: store LAST_ROLL in a global constant instead of passing it around to all the `action` methods
16# TODO: write some tests
17# TODO: break up into modules
18"""
19from abc import ABC
20from collections import defaultdict
21from itertools import cycle
22from random import choice, shuffle
23from typing import cast, List, NewType, Optional, Tuple, Type
25from exceptions import (
26 Argument,
27 CantBuyBuildings,
28 CantMortgage,
29 DidntFind,
30 MustBeEqualAmounts,
31 NoOwner,
32 NotEnough,
33 TooMany,
34 TooManyPlayers,
35 NotEnoughPlayers,
36)
38ALL_MONEY = 20_580
39NUM_HOUSES = 32
40NUM_HOTELS = 12
42Doubles = NewType("Doubles", bool)
44LANGUAGE = "français"
45BACKUP_LANGUAGE = "English"
46BUILDING_TYPES = "house", "hotel"
47MAX_ROUNDS = 5000
49BUILDABLE_PROPERTY_COLORS = (
50 "yellow",
51 "red",
52 "light blue",
53 "brown",
54 "pink",
55 "orange",
56 "green",
57 "dark blue",
58)
61class Space:
62 def __repr__(self):
63 if hasattr(self, "_name"):
64 return self._name
65 return self.__name__
68class NothingHappensWhenYouLandOnItSpace(Space):
69 @classmethod
70 def action(self, player: "Player", _):
71 pass
74class Go(NothingHappensWhenYouLandOnItSpace):
75 pass
78class Jail(NothingHappensWhenYouLandOnItSpace):
79 pass
82class FreeParking(NothingHappensWhenYouLandOnItSpace):
83 pass
86class TaxSpace(Space):
87 amount = None
89 @classmethod
90 def action(cls, player, _):
91 player.pay("Bank", cls.amount)
94class LuxuryTax(TaxSpace):
95 _name = {"français": "Impôt Supplémentaire", "deutsch": "Nachsteuer"}
96 amount = 75
99class IncomeTax(TaxSpace):
100 # TODO: _name
101 amount = 100
104class GoToJail(Space):
105 @classmethod
106 def action(cls, player, _):
107 # player.go_to_jail()
108 pass
111class CardSpace(Space):
112 deck = None
114 @classmethod
115 def action(cls, player, _):
116 # for lazy loading to avoid circular imports (?)
117 deck = eval(cls.deck)
118 card = deck.get_card()
119 return card.action(player, _)
122class CommunityChest(CardSpace):
123 deck = "CommunityChestDeck"
124 _name = {"français": "Chancellerie", "deutsch": "Kanzlei"}
127class Chance(CardSpace):
128 deck = "ChanceDeck"
129 _name = {"français": "Chance", "deutsch": "Chance", "English": "Chance"}
132class Card(ABC):
133 mandatory_action = False
134 cost = None
135 keep = False
137 @classmethod
138 def action(self, player: "Player", _):
139 raise NotImplementedError
141 def __repr__(self):
142 return self.text
145class ElectedPresidentCard(Card):
146 mandatory_action = True
147 text = {
148 "français": "Vous avez été elu president du conseil d'administration. Versez M50 à chaque joueur."
149 }
151 @classmethod
152 def action(cls, player, _):
153 for other_player in Game.games[0].active_players:
154 if other_player != player:
155 player.pay(other_player, 50)
158class GetOutOfJailFreeCard(Card):
159 text = {
160 "français": "Vous êtes libéré de prison. Cette carte peut être conservée jusqu'à ce "
161 "qu'elle soit utilisée ou vendue. "
162 }
164 @classmethod
165 def action(cls, player: "Player", _):
166 player.get_out_of_jail_free_card = True
169class AdvanceCard(Card):
170 mandatory_action = True
171 kwarg = {}
173 @classmethod
174 def action(cls, player, _):
175 player.advance(**cls.kwarg)
178class GoToJailCard(AdvanceCard):
179 text = {
180 "français": "Allez en prison. Avancez tout droit en prison. Ne passez pas par la case "
181 "départ. Ne recevez pas M200. "
182 }
183 kwarg = {"until_space_type": "Jail", "pass_go": False}
186class AdvanceThreeSpacesCard(AdvanceCard):
187 mandatory_action = True
188 text = {"français": "Reculez de trois cases."}
189 kwarg = {"num_spaces": 3}
192class GoToClosestRailroadCard(AdvanceCard):
193 text = {"français": "Avancez jusqu'à le chemin de fer le plus proche."}
194 kwarg = {"until_space_type": "Railroad"}
197class GoToBernPlaceFederaleCard(AdvanceCard):
198 mandatory_action = True
199 text = {
200 "français": "Avancez jusqu'a Bern Place Federale. Si vous passez par la case départ, "
201 "vous touchez la prime habituelle de M200. "
202 }
204 kwarg = {"space_index": "Berne Place Fédérale"}
207class BuildingAndLoanMaturesCard(Card):
208 mandatory_action = True
209 text = {
210 "français": "Votre immeuble et votre pret rapportent. Vous devez toucher M150.",
211 "English": "Your building and loan matures. Collect M150.",
212 }
214 @classmethod
215 def action(self, player, _):
216 Bank.pay(player, 150)
219class SpeedingCard(Card):
220 mandatory_action = True
221 text = {"français": "Amende pour excès de vitesse. Payez M15."}
223 @classmethod
224 def action(cls, player, _):
225 player.pay(Bank, 15)
228class RepairPropertyCard(Card):
229 text = {
230 "français": "Vous faites des réparations sur toutes vos propriétés: Versez M25"
231 " pour chaque maison M100 pour Chaque hôtel que vous possédez"
232 }
234 @classmethod
235 def action(cls, player, _):
236 num_houses, num_hotels = 0, 0
237 for property in player.buildable_properties:
238 num_houses += property.buildings["house"]
239 num_hotels += property.buildings["hotel"]
240 player.pay(Bank, sum([num_houses * 25, num_hotels * 100]))
243class Deck:
244 deck = None
246 @classmethod
247 def shuffle(cls):
248 shuffle(cls.deck)
250 @classmethod
251 def get_card(cls):
252 card = cls.deck.pop()
253 if not card.keep:
254 cls.deck.insert(0, card)
255 return card
258class ChanceDeck(Deck):
259 deck = [
260 AdvanceThreeSpacesCard,
261 ElectedPresidentCard,
262 BuildingAndLoanMaturesCard,
263 GoToBernPlaceFederaleCard,
264 GoToJailCard,
265 SpeedingCard,
266 GoToClosestRailroadCard,
267 ]
270class CommunityChestDeck(Deck):
271 # TODO: remove these and add the readl ones!
272 deck = [GoToJailCard, SpeedingCard]
275def shuffle_decks():
276 for deck in (ChanceDeck, CommunityChestDeck):
277 deck.shuffle()
280def buy_decision(property: "Property", player: "Player"):
281 return Game.games[0].buy_decision_algorithm(property, player)
284class Decision:
285 pass
288class Property(Space):
289 mortgaged = False
290 owner = None
291 cost = 0
292 instances = []
293 type = None
295 def __init__(self, _name):
296 self._name = _name
297 self.instances.append(self)
299 def __repr__(self):
300 if hasattr(self, "_name"):
301 return self._name[LANGUAGE]
302 return str(self.__class__)
304 @classmethod
305 def reset(cls):
306 for property in cls.instances:
307 property.owner = None
309 @classmethod
310 def get_num_of_type(cls, type):
311 return len(cls.instances_by_type()[type])
313 @property
314 def num_of_type(self):
315 return self.get_num_of_type(self.type)
317 @property
318 def properties_of_type(self):
319 return self.instances_by_type()[self.type]
321 @classmethod
322 def instances_by_type(cls):
323 ibt = defaultdict(list)
324 for i in cls.instances:
325 ibt[i.type].append(i)
326 return ibt
328 def action(self, player: "Player", last_roll=None):
329 if not self.owner:
330 buy = buy_decision(self, player)
331 if buy:
332 return player.buy(self)
333 return
334 if self.owner == player or self.mortgaged:
335 return
336 player.pay(self.owner, self.calculate_rent(last_roll))
338 def calculate_rent(self, _):
339 if not self.owner:
340 raise NoOwner
343class Utility(Property):
344 cost = 200
345 rent = {
346 0: lambda _: 0,
347 1: lambda dice_total: dice_total * 4,
348 2: lambda dice_total: dice_total * 10,
349 }
350 mortgage_cost = 75
351 unmortgage_cost = 83
352 type = "utility"
354 def calculate_rent(self, last_roll: int):
355 super().calculate_rent(last_roll)
356 if not last_roll:
357 return 10 * Player.roll_the_dice()[0]
358 return self.rent[self.owner.owns_x_of_type(self.type)](last_roll)
361class Railroad(Property):
362 cost = 200
363 rent = {1: 25, 2: 50, 3: 100, 4: 200}
364 mortgage_cost = 100
365 unmortgage_cost = 110
366 type = "railroad"
368 def calculate_rent(self, _):
369 super().calculate_rent(_)
370 owns_x_of_type = self.owner.owns_x_of_type(self.type)
371 if not owns_x_of_type:
372 return 0
373 return self.rent[owns_x_of_type]
376class BuildableProperty(Property):
377 def __init__(
378 self,
379 _name,
380 cost,
381 rent,
382 color,
383 house_and_hotel_cost,
384 mortgage_cost,
385 unmortgage_cost,
386 ):
387 super().__init__(_name)
388 self.cost = cost
389 self.rent = rent
390 self.house_and_hotel_cost = house_and_hotel_cost
391 self.color = color
392 self.type = color
393 self.mortgage_cost = mortgage_cost
394 self.unmortgage_cost = unmortgage_cost
395 self.buildings = {"house": 0, "hotel": 0}
397 def buy_building(self, building_type):
398 """
399 TODO: Each property within a group must be no more than one house level away from all other
400 properties in that group. For example, if you own the Orange group, you can’t put a
401 second house on New York Ave until you have a first house on St. James and Tennessee.
402 Then you can’t put a third house on any property until you have two houses on
403 all properties.
404 """
405 if not self.owner.owns_all_type(self.type):
406 raise CantBuyBuildings
408 if building_type == "hotel" and self.buildings["house"] != 4:
409 raise NotEnough
410 elif building_type == "house" and self.buildings["house"] == 4:
411 raise TooMany
413 cost = self.house_and_hotel_cost
414 self.owner.check_funds(cost)
415 Bank.get_building(building_type)
416 self.owner.pay(Bank, cost)
418 for property_ in self.properties_of_type:
419 if building_type == "hotel":
420 property_.buildings["house"] = 0
421 property_.buildings["hotel"] = 1
422 else:
423 property_.buildings["house"] += 1
425 def sell_buildings(self, building_type, quantity):
426 if not self.buildings[building_type]:
427 raise NotEnough
428 if quantity % self.num_of_type:
429 # TODO: this isn't right
430 # https://www.quora.com/When-can-a-player-place-a-house-in-monopoly
431 raise MustBeEqualAmounts
433 def mortgage(self, player: "Player"):
434 if self.buildings:
435 raise CantMortgage
436 Bank.pay(player, self.mortgage_cost)
437 self.mortgaged = True
439 def un_mortgage(self, player: "Player"):
440 player.pay(Bank, self.unmortgage_cost)
441 self.mortgaged = False
443 def calculate_rent(self, _):
444 super().calculate_rent(_)
445 if self.buildings["house"] or self.buildings["hotel"]:
446 buildings = self.buildings
447 if buildings["house"]:
448 key = buildings["house"]
449 else:
450 key = "hotel"
451 elif self.owner.owns_all_type(self.type):
452 key = "monopoly"
453 else:
454 key = 0
455 return self.rent[key]
458class Board:
459 spaces = [
460 Go,
461 BuildableProperty(
462 _name={"français": "Coire Kornplatz"},
463 cost=60,
464 color="brown",
465 rent={0: 2, "monopoly": 4, 1: 10, 2: 30, 3: 90, 4: 160, "hotel": 250},
466 house_and_hotel_cost=50,
467 mortgage_cost=30,
468 unmortgage_cost=33,
469 ),
470 CommunityChest,
471 BuildableProperty(
472 _name={"français": "Schaffhouse Vordergasse"},
473 cost=60,
474 color="brown",
475 rent={0: 4, "monopoly": 8, 1: 20, 2: 60, 3: 180, 4: 320, "hotel": 450},
476 house_and_hotel_cost=50,
477 mortgage_cost=30,
478 unmortgage_cost=33,
479 ),
480 IncomeTax,
481 Railroad(_name={"français": "Union des Chemins de Fer Privés"}),
482 BuildableProperty(
483 _name={"français": "Aarau Rathausplatz"},
484 cost=100,
485 color="light blue",
486 rent={0: 6, "monopoly": 12, 1: 30, 2: 90, 3: 270, 4: 400, "hotel": 550},
487 house_and_hotel_cost=50,
488 mortgage_cost=50,
489 unmortgage_cost=55,
490 ),
491 Chance,
492 BuildableProperty(
493 _name={"français": "Neuchâtel Place Pury"},
494 cost=100,
495 color="light blue",
496 rent={0: 6, "monopoly": 12, 1: 30, 2: 90, 3: 270, 4: 400, "hotel": 550},
497 house_and_hotel_cost=50,
498 mortgage_cost=50,
499 unmortgage_cost=55,
500 ),
501 BuildableProperty(
502 _name={"français": "Thoune Hauptgasse"},
503 cost=120,
504 color="light blue",
505 rent={0: 8, "monopoly": 16, 1: 30, 2: 100, 3: 300, 4: 400, "hotel": 600},
506 house_and_hotel_cost=50,
507 mortgage_cost=60,
508 unmortgage_cost=66,
509 ),
510 Jail,
511 BuildableProperty(
512 _name={"français": "Bâle Steinen-Vorstadt"},
513 cost=140,
514 color="pink",
515 rent={0: 10, "monopoly": 20, 1: 50, 2: 150, 3: 450, 4: 625, "hotel": 750},
516 house_and_hotel_cost=100,
517 mortgage_cost=70,
518 unmortgage_cost=77,
519 ),
520 Utility(_name={"français": "Usines Électriques"}),
521 BuildableProperty(
522 _name={"français": "Soleure Hauptgasse"},
523 cost=140,
524 color="pink",
525 rent={0: 10, "monopoly": 20, 1: 50, 2: 150, 3: 450, 4: 625, "hotel": 750},
526 house_and_hotel_cost=100,
527 mortgage_cost=70,
528 unmortgage_cost=77,
529 ),
530 BuildableProperty(
531 _name={"français": "Lugano Via Nassa"},
532 cost=160,
533 color="pink",
534 rent={0: 12, "monopoly": 24, 1: 60, 2: 180, 3: 500, 4: 700, "hotel": 900},
535 house_and_hotel_cost=100,
536 mortgage_cost=80,
537 unmortgage_cost=88,
538 ),
539 BuildableProperty(
540 _name={"français": "Bienne Rue De Nidau"},
541 cost=180,
542 color="orange",
543 rent={0: 14, "monopoly": 28, 1: 70, 2: 200, 3: 550, 4: 750, "hotel": 950},
544 house_and_hotel_cost=100,
545 mortgage_cost=90,
546 unmortgage_cost=99,
547 ),
548 CommunityChest,
549 BuildableProperty(
550 _name={"français": "Fribourg Avenue de la Gare"},
551 cost=180,
552 color="orange",
553 rent={0: 14, "monopoly": 28, 1: 70, 2: 200, 3: 550, 4: 750, "hotel": 950},
554 house_and_hotel_cost=100,
555 mortgage_cost=90,
556 unmortgage_cost=99,
557 ),
558 BuildableProperty(
559 _name={"français": "La Chaux-de-Fonds Avenue Louis-Robert"},
560 cost=200,
561 color="orange",
562 rent={0: 16, "monopoly": 32, 1: 80, 2: 220, 3: 600, 4: 800, "hotel": 1_000},
563 house_and_hotel_cost=100,
564 mortgage_cost=100,
565 unmortgage_cost=110,
566 ),
567 FreeParking,
568 BuildableProperty(
569 _name={"français": "Winterthour Bahnhofplatz"},
570 cost=220,
571 color="red",
572 rent={0: 18, "monopoly": 39, 1: 90, 2: 250, 3: 700, 4: 875, "hotel": 1_050},
573 house_and_hotel_cost=150,
574 mortgage_cost=110,
575 unmortgage_cost=121,
576 ),
577 Chance,
578 BuildableProperty(
579 _name={"français": "St-Gall Markplatz"},
580 cost=220,
581 color="red",
582 rent={0: 18, "monopoly": 39, 1: 90, 2: 250, 3: 700, 4: 875, "hotel": 1_050},
583 house_and_hotel_cost=150,
584 mortgage_cost=110,
585 unmortgage_cost=121,
586 ),
587 BuildableProperty(
588 _name={"français": "Berne Place Fédérale"},
589 cost=240,
590 color="red",
591 rent={
592 0: 20,
593 "monopoly": 40,
594 1: 100,
595 2: 300,
596 3: 750,
597 4: 925,
598 "hotel": 1_100,
599 },
600 house_and_hotel_cost=150,
601 mortgage_cost=120,
602 unmortgage_cost=132,
603 ),
604 Railroad(_name={"français": "Tramways Interurbains"}),
605 BuildableProperty(
606 _name={"français": "Lucerne Weggisgasse"},
607 cost=260,
608 color="yellow",
609 rent={
610 0: 22,
611 "monopoly": 34,
612 1: 110,
613 2: 330,
614 3: 800,
615 4: 975,
616 "hotel": 1_150,
617 },
618 house_and_hotel_cost=150,
619 mortgage_cost=130,
620 unmortgage_cost=143,
621 ),
622 BuildableProperty(
623 _name={"français": "Zurich Rennweg"},
624 cost=260,
625 color="yellow",
626 rent={
627 0: 22,
628 "monopoly": 34,
629 1: 110,
630 2: 330,
631 3: 800,
632 4: 975,
633 "hotel": 1_150,
634 },
635 house_and_hotel_cost=150,
636 mortgage_cost=130,
637 unmortgage_cost=143,
638 ),
639 Utility(_name={"français": "Usines Hydrauliques"}),
640 BuildableProperty(
641 _name={"français": "Lausanne Rue de Bourg"},
642 cost=280,
643 color="yellow",
644 rent={
645 0: 24,
646 "monopoly": 48,
647 1: 120,
648 2: 360,
649 3: 850,
650 4: 1_025,
651 "hotel": 1_200,
652 },
653 house_and_hotel_cost=150,
654 mortgage_cost=140,
655 unmortgage_cost=154,
656 ),
657 GoToJail,
658 BuildableProperty(
659 _name={"français": "Bâle Freie Strasse"},
660 cost=300,
661 color="green",
662 rent={
663 0: 26,
664 "monopoly": 52,
665 1: 130,
666 2: 390,
667 3: 900,
668 4: 1_100,
669 "hotel": 1_275,
670 },
671 house_and_hotel_cost=200,
672 mortgage_cost=150,
673 unmortgage_cost=165,
674 ),
675 BuildableProperty(
676 _name={"français": "Genève Rue de la Croix-D'Or"},
677 cost=300,
678 color="green",
679 rent={
680 0: 26,
681 "monopoly": 52,
682 1: 130,
683 2: 390,
684 3: 900,
685 4: 1_100,
686 "hotel": 1_275,
687 },
688 house_and_hotel_cost=200,
689 mortgage_cost=150,
690 unmortgage_cost=165,
691 ),
692 CommunityChest,
693 BuildableProperty(
694 _name={"français": "Berne Spitalgasse"},
695 cost=320,
696 color="green",
697 rent={
698 0: 28,
699 "monopoly": 56,
700 1: 150,
701 2: 450,
702 3: 1_000,
703 4: 1_200,
704 "hotel": 1_400,
705 },
706 house_and_hotel_cost=200,
707 mortgage_cost=160,
708 unmortgage_cost=176,
709 ),
710 Railroad(_name={"français": "Association des Télépheriques"}),
711 Chance,
712 BuildableProperty(
713 _name={"français": "Lausanne Place St. François"},
714 cost=350,
715 color="dark blue",
716 rent={
717 0: 35,
718 "monopoly": 70,
719 1: 175,
720 2: 500,
721 3: 1_100,
722 4: 1_300,
723 "hotel": 1_500,
724 },
725 house_and_hotel_cost=200,
726 mortgage_cost=175,
727 unmortgage_cost=193,
728 ),
729 LuxuryTax,
730 BuildableProperty(
731 _name={"français": "Zurich Paradeplatz"},
732 cost=400,
733 color="dark blue",
734 rent={
735 0: 50,
736 "monopoly": 100,
737 1: 200,
738 2: 600,
739 3: 1_400,
740 4: 1_700,
741 "hotel": 2_000,
742 },
743 house_and_hotel_cost=200,
744 mortgage_cost=200,
745 unmortgage_cost=220,
746 ),
747 ]
748 SPACES_DICT = {}
749 for index, space in enumerate(spaces):
750 if hasattr(space, "_name"):
751 space_name = space._name
752 else:
753 space_name = space.__name__
755 if isinstance(space_name, dict):
756 space_name = space._name.get(LANGUAGE) or space._name.get(BACKUP_LANGUAGE)
757 SPACES_DICT[space_name] = index
759 # SPACES_DICT = {space.name: space for space in spaces}
760 NUM_SPACES = len(spaces)
763def get_space_index(name):
764 return Board.SPACES_DICT[name]
767class EconomicActor:
768 pass
771class Bank(EconomicActor):
772 money = ALL_MONEY
773 NUM_HOUSES = NUM_HOUSES
774 NUM_HOTELS = NUM_HOTELS
776 @classmethod
777 def reset(cls):
778 cls.money = ALL_MONEY
779 cls.NUM_HOUSES = NUM_HOUSES
780 cls.NUM_HOTELS = NUM_HOTELS
782 @classmethod
783 def pay(cls, actor: "EconomicActor", amount: int):
784 if isinstance(actor, str):
785 actor = eval(actor)
786 cls.money -= amount
787 actor.money += amount
789 @classmethod
790 def get_building(cls, type_):
791 cls.check_building_type(type_)
792 store = cls.get_building_store(type_)
793 if not store:
794 raise NotEnough(f"Not enough {type_}s!")
795 else:
796 store -= 1
798 @classmethod
799 def put_building(cls, type_, quantity):
800 cls.check_building_type(type_)
801 store = cls.get_building_store(type_)
802 store += quantity
804 @staticmethod
805 def check_building_type(type_):
806 if type_ not in BUILDING_TYPES:
807 raise TypeError
809 @classmethod
810 def get_building_store(cls, type_):
811 return cls.NUM_HOUSES if type_ == "house" else cls.NUM_HOTELS
814def get_index_of_next_space_of_type(current_space_index, until_space_type):
815 space_indices_to_traverse = list(
816 range(current_space_index + 1, Board.NUM_SPACES)
817 ) + list(range(current_space_index))
818 for index in space_indices_to_traverse:
819 if isinstance(until_space_type, str):
820 until_space_type = eval(until_space_type)
821 if Board.spaces[index] == until_space_type or isinstance(
822 Board.spaces[index], until_space_type
823 ):
824 return index
825 raise DidntFind
828def check_args(num_spaces, space_index, until_space_type):
829 num_args = sum(
830 1 for kwarg in (num_spaces, space_index, until_space_type) if kwarg is not None
831 )
832 if num_args > 1 or num_args == 0:
833 raise Argument("provide either num_spaces or space_index or until_space_type")
836class GetOutOfJailDecision(Decision):
837 def __init__(self, player: "Player"):
838 pass
841def get_property_with_least_number_of_houses(properties):
842 return sorted(properties, key=lambda prop: prop.buildings["house"], reverse=True)[0]
845def get_property_with_no_hotels(properties):
846 return sorted(properties, key=lambda prop: prop.buildings["hotel"])[0]
849class Monopoly:
850 def __init__(self, property_: "BuildableProperty"):
851 self.properties = Property.instances_by_type()[property_.type]
852 self.num_properties = len(self.properties)
853 self.max_num_houses = 4 * self.num_properties
854 self.max_num_hotels = self.num_properties
856 def __repr__(self):
857 return f"<Monopoly type={self.properties[0].type}"
859 @property
860 def num_houses(self):
861 return sum(property.buildings["house"] for property in self.properties)
863 @property
864 def num_hotels(self):
865 return sum(property.buildings["hotel"] for property in self.properties)
867 @property
868 def next_building(self) -> Tuple[Optional[str], Optional["BuildableProperty"]]:
869 num_houses, num_hotels, max_num_houses, max_num_hotels = (
870 self.num_houses,
871 self.num_hotels,
872 self.max_num_houses,
873 self.max_num_hotels,
874 )
875 first_prop = self.properties[0]
877 if not num_houses and not num_hotels:
878 return "house", first_prop
880 elif num_hotels == max_num_hotels:
881 return None, None
883 elif num_houses < max_num_houses:
884 if not num_hotels:
885 return (
886 "house",
887 get_property_with_least_number_of_houses(self.properties),
888 )
889 else:
890 return "hotel", get_property_with_no_hotels(self.properties)
891 elif num_houses == max_num_houses:
892 return "hotel", first_prop
895class Player(EconomicActor):
896 in_jail = False
897 bankrupt = False
898 get_out_of_jail_free_card = False
899 go_again = False
900 current_space_index = get_space_index("Go")
901 money = 0
902 passed_go_times = 0
903 monopolies = []
905 def __init__(self):
906 Bank.pay(self, 1_500)
908 def pay(self, actor: Type["EconomicActor"], amount: int):
909 if isinstance(actor, str):
910 actor = eval(actor)
911 self.check_funds(amount)
912 self.money -= amount
913 actor.money += amount
915 def check_funds(self, amount):
916 if amount > self.money:
917 raise NotEnough
919 def buy(self, property_: "Property", from_=Bank, cost=None):
920 try:
921 self.pay(from_, cost or property_.cost)
922 except NotEnough:
923 return
924 property_.owner = self
926 if property_.__class__.__name__ == "BuildableProperty" and self.owns_all_type(
927 property_.type
928 ):
929 monopoly = Monopoly(property_)
930 self.monopolies.append(monopoly)
932 def buy_buildings_if_possible(self):
933 if self.monopolies:
934 print(f"{self} has {self.monopolies}")
935 else:
936 print(f"{self} has no monopolies.")
937 for monopoly in self.monopolies:
938 while True:
939 next_building_type, property_ = monopoly.next_building
940 if not next_building_type:
941 break
942 if not self.can_afford(property_.house_and_hotel_cost):
943 break
944 try:
945 property_.buy_building(next_building_type)
946 except NotEnough:
947 break
949 def take_a_turn(self):
950 if self.in_jail:
951 return GetOutOfJailDecision(self)
952 self.buy_buildings_if_possible()
953 num_spaces, doubles = self.roll_the_dice()
954 if doubles:
955 self.go_again = True
956 else:
957 self.go_again = False
959 self.advance(num_spaces, just_rolled=True)
961 def owns_x_of_type(self, type_):
962 properties_of_this_type = self.properties_by_type.get(type_)
963 if properties_of_this_type is None:
964 return 0
965 if properties_of_this_type is None:
966 return 0
967 return len(properties_of_this_type)
969 def owns_all_type(self, type_):
970 return self.owns_x_of_type(type_) == Property.get_num_of_type(type_)
972 @property
973 def properties_by_type(self):
974 pbt = defaultdict(list)
975 for property in self.properties:
976 pbt[property.type].append(property)
977 return pbt
979 @staticmethod
980 def roll_the_dice() -> Tuple[int, Doubles]:
981 die_one, die_two = choice(range(1, 7)), choice(range(1, 7))
982 total = die_one + die_two
983 if die_one == die_two:
984 return total, cast(Doubles, True)
985 return total, cast(Doubles, False)
987 @property
988 def assets(self):
989 return self.money + self.total_property_mortgage_value
991 @property
992 def total_property_mortgage_value(self):
993 return sum(property.mortgage_cost for property in self.properties)
995 @property
996 def properties(self) -> List["Property"]:
997 # TODO: create an `instances` class attribute on `Property` that keeps track of them all
998 # then iterate through those instances to see which ones have an owner equal to `self`
999 return [p for p in Property.instances if p.owner == self]
1001 @property
1002 def buildable_properties(self) -> List[BuildableProperty]:
1003 return [
1004 p for p in self.properties if p.__class__.__name__ == "BuildableProperty"
1005 ]
1007 def advance(
1008 self,
1009 num_spaces=None,
1010 space_index=None,
1011 until_space_type=None,
1012 pass_go=True,
1013 just_rolled=True,
1014 ):
1015 new_space_index = None
1016 check_args(num_spaces, space_index, until_space_type)
1017 if num_spaces:
1018 new_space_index = self.current_space_index + num_spaces
1019 elif space_index:
1020 if isinstance(space_index, str):
1021 space_index = get_space_index(space_index)
1022 new_space_index = space_index
1023 elif until_space_type:
1024 new_space_index = get_index_of_next_space_of_type(
1025 self.current_space_index, until_space_type
1026 )
1028 if pass_go and new_space_index >= Board.NUM_SPACES - 1:
1029 self.money += 200
1030 self.passed_go_times += 1
1031 new_space_index = new_space_index - Board.NUM_SPACES
1032 elif pass_go and self.current_space_index > new_space_index:
1033 self.money += 200
1034 self.passed_go_times += 1
1036 self.current_space_index = new_space_index
1038 if just_rolled:
1039 last_roll = num_spaces
1040 else:
1041 last_roll = None
1043 try:
1044 self.do_action_of_current_space(last_roll=last_roll)
1045 except NotEnough:
1046 # TODO: is this always right?
1047 # TODO: eventually make deals and mortgage prtoperties to avoid bankruptcy
1048 self.bankrupt = True
1050 def do_action_of_current_space(self, last_roll=None):
1051 space = Board.spaces[self.current_space_index]
1052 space.action(self, last_roll)
1054 def go_to_jail(self):
1055 self.in_jail = True
1056 self.current_space_index = get_space_index("Jail")
1058 def can_afford(self, cost):
1059 return self.money >= cost
1062class Game:
1063 games = []
1064 rounds = 0
1066 def __init__(self, num_players, buy_decision_algorithm):
1067 Bank.reset()
1068 shuffle_decks()
1069 Property.reset()
1070 self.buy_decision_algorithm = buy_decision_algorithm()
1071 # TODO: make this nicer
1072 if self.games:
1073 del self.games[0]
1074 self.games.append(self)
1075 if num_players < 2:
1076 raise NotEnoughPlayers
1077 if num_players > 8:
1078 raise TooManyPlayers
1079 self._players = [Player() for _ in range(num_players)]
1080 self.players = cycle(self._players)
1081 # TODO: roll to see who goes first, then order the players accordingly
1082 self.start()
1084 @property
1085 def active_players(self):
1086 return [player for player in self._players if not player.bankrupt]
1088 def start(self):
1089 while len(self.active_players) > 1 and self.rounds < MAX_ROUNDS:
1090 current_player = next(self.players)
1091 if not current_player.bankrupt:
1092 current_player.take_a_turn()
1093 while current_player.go_again and not current_player.bankrupt:
1094 current_player.take_a_turn()
1095 self.rounds += 1
1097 def get_rounds_played_per_player(self):
1098 return self.rounds / len(self._players)
1100 def end(self):
1101 for player in self._players:
1102 del player