Hide keyboard shortcuts

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: 

3 

41) To simulate games of Monopoly in order to determine the best strategy 

52) To play Monopoly on a computer 

6 

7Along those lines, here's some prior art: 

8 

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/ 

11 

12 

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 

24 

25from exceptions import ( 

26 Argument, 

27 CantBuyBuildings, 

28 CantMortgage, 

29 DidntFind, 

30 MustBeEqualAmounts, 

31 NoOwner, 

32 NotEnough, 

33 TooMany, 

34 TooManyPlayers, 

35 NotEnoughPlayers, 

36) 

37 

38ALL_MONEY = 20_580 

39NUM_HOUSES = 32 

40NUM_HOTELS = 12 

41 

42Doubles = NewType("Doubles", bool) 

43 

44LANGUAGE = "français" 

45BACKUP_LANGUAGE = "English" 

46BUILDING_TYPES = "house", "hotel" 

47MAX_ROUNDS = 5000 

48 

49BUILDABLE_PROPERTY_COLORS = ( 

50 "yellow", 

51 "red", 

52 "light blue", 

53 "brown", 

54 "pink", 

55 "orange", 

56 "green", 

57 "dark blue", 

58) 

59 

60 

61class Space: 

62 def __repr__(self): 

63 if hasattr(self, "_name"): 

64 return self._name 

65 return self.__name__ 

66 

67 

68class NothingHappensWhenYouLandOnItSpace(Space): 

69 @classmethod 

70 def action(self, player: "Player", _): 

71 pass 

72 

73 

74class Go(NothingHappensWhenYouLandOnItSpace): 

75 pass 

76 

77 

78class Jail(NothingHappensWhenYouLandOnItSpace): 

79 pass 

80 

81 

82class FreeParking(NothingHappensWhenYouLandOnItSpace): 

83 pass 

84 

85 

86class TaxSpace(Space): 

87 amount = None 

88 

89 @classmethod 

90 def action(cls, player, _): 

91 player.pay("Bank", cls.amount) 

92 

93 

94class LuxuryTax(TaxSpace): 

95 _name = {"français": "Impôt Supplémentaire", "deutsch": "Nachsteuer"} 

96 amount = 75 

97 

98 

99class IncomeTax(TaxSpace): 

100 # TODO: _name 

101 amount = 100 

102 

103 

104class GoToJail(Space): 

105 @classmethod 

106 def action(cls, player, _): 

107 # player.go_to_jail() 

108 pass 

109 

110 

111class CardSpace(Space): 

112 deck = None 

113 

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, _) 

120 

121 

122class CommunityChest(CardSpace): 

123 deck = "CommunityChestDeck" 

124 _name = {"français": "Chancellerie", "deutsch": "Kanzlei"} 

125 

126 

127class Chance(CardSpace): 

128 deck = "ChanceDeck" 

129 _name = {"français": "Chance", "deutsch": "Chance", "English": "Chance"} 

130 

131 

132class Card(ABC): 

133 mandatory_action = False 

134 cost = None 

135 keep = False 

136 

137 @classmethod 

138 def action(self, player: "Player", _): 

139 raise NotImplementedError 

140 

141 def __repr__(self): 

142 return self.text 

143 

144 

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 } 

150 

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) 

156 

157 

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 } 

163 

164 @classmethod 

165 def action(cls, player: "Player", _): 

166 player.get_out_of_jail_free_card = True 

167 

168 

169class AdvanceCard(Card): 

170 mandatory_action = True 

171 kwarg = {} 

172 

173 @classmethod 

174 def action(cls, player, _): 

175 player.advance(**cls.kwarg) 

176 

177 

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} 

184 

185 

186class AdvanceThreeSpacesCard(AdvanceCard): 

187 mandatory_action = True 

188 text = {"français": "Reculez de trois cases."} 

189 kwarg = {"num_spaces": 3} 

190 

191 

192class GoToClosestRailroadCard(AdvanceCard): 

193 text = {"français": "Avancez jusqu'à le chemin de fer le plus proche."} 

194 kwarg = {"until_space_type": "Railroad"} 

195 

196 

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 } 

203 

204 kwarg = {"space_index": "Berne Place Fédérale"} 

205 

206 

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 } 

213 

214 @classmethod 

215 def action(self, player, _): 

216 Bank.pay(player, 150) 

217 

218 

219class SpeedingCard(Card): 

220 mandatory_action = True 

221 text = {"français": "Amende pour excès de vitesse. Payez M15."} 

222 

223 @classmethod 

224 def action(cls, player, _): 

225 player.pay(Bank, 15) 

226 

227 

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 } 

233 

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])) 

241 

242 

243class Deck: 

244 deck = None 

245 

246 @classmethod 

247 def shuffle(cls): 

248 shuffle(cls.deck) 

249 

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 

256 

257 

258class ChanceDeck(Deck): 

259 deck = [ 

260 AdvanceThreeSpacesCard, 

261 ElectedPresidentCard, 

262 BuildingAndLoanMaturesCard, 

263 GoToBernPlaceFederaleCard, 

264 GoToJailCard, 

265 SpeedingCard, 

266 GoToClosestRailroadCard, 

267 ] 

268 

269 

270class CommunityChestDeck(Deck): 

271 # TODO: remove these and add the readl ones! 

272 deck = [GoToJailCard, SpeedingCard] 

273 

274 

275def shuffle_decks(): 

276 for deck in (ChanceDeck, CommunityChestDeck): 

277 deck.shuffle() 

278 

279 

280def buy_decision(property: "Property", player: "Player"): 

281 return Game.games[0].buy_decision_algorithm(property, player) 

282 

283 

284class Decision: 

285 pass 

286 

287 

288class Property(Space): 

289 mortgaged = False 

290 owner = None 

291 cost = 0 

292 instances = [] 

293 type = None 

294 

295 def __init__(self, _name): 

296 self._name = _name 

297 self.instances.append(self) 

298 

299 def __repr__(self): 

300 if hasattr(self, "_name"): 

301 return self._name[LANGUAGE] 

302 return str(self.__class__) 

303 

304 @classmethod 

305 def reset(cls): 

306 for property in cls.instances: 

307 property.owner = None 

308 

309 @classmethod 

310 def get_num_of_type(cls, type): 

311 return len(cls.instances_by_type()[type]) 

312 

313 @property 

314 def num_of_type(self): 

315 return self.get_num_of_type(self.type) 

316 

317 @property 

318 def properties_of_type(self): 

319 return self.instances_by_type()[self.type] 

320 

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 

327 

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)) 

337 

338 def calculate_rent(self, _): 

339 if not self.owner: 

340 raise NoOwner 

341 

342 

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" 

353 

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) 

359 

360 

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" 

367 

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] 

374 

375 

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} 

396 

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 

407 

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 

412 

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) 

417 

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 

424 

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 

432 

433 def mortgage(self, player: "Player"): 

434 if self.buildings: 

435 raise CantMortgage 

436 Bank.pay(player, self.mortgage_cost) 

437 self.mortgaged = True 

438 

439 def un_mortgage(self, player: "Player"): 

440 player.pay(Bank, self.unmortgage_cost) 

441 self.mortgaged = False 

442 

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] 

456 

457 

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__ 

754 

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 

758 

759 # SPACES_DICT = {space.name: space for space in spaces} 

760 NUM_SPACES = len(spaces) 

761 

762 

763def get_space_index(name): 

764 return Board.SPACES_DICT[name] 

765 

766 

767class EconomicActor: 

768 pass 

769 

770 

771class Bank(EconomicActor): 

772 money = ALL_MONEY 

773 NUM_HOUSES = NUM_HOUSES 

774 NUM_HOTELS = NUM_HOTELS 

775 

776 @classmethod 

777 def reset(cls): 

778 cls.money = ALL_MONEY 

779 cls.NUM_HOUSES = NUM_HOUSES 

780 cls.NUM_HOTELS = NUM_HOTELS 

781 

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 

788 

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 

797 

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 

803 

804 @staticmethod 

805 def check_building_type(type_): 

806 if type_ not in BUILDING_TYPES: 

807 raise TypeError 

808 

809 @classmethod 

810 def get_building_store(cls, type_): 

811 return cls.NUM_HOUSES if type_ == "house" else cls.NUM_HOTELS 

812 

813 

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 

826 

827 

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") 

834 

835 

836class GetOutOfJailDecision(Decision): 

837 def __init__(self, player: "Player"): 

838 pass 

839 

840 

841def get_property_with_least_number_of_houses(properties): 

842 return sorted(properties, key=lambda prop: prop.buildings["house"], reverse=True)[0] 

843 

844 

845def get_property_with_no_hotels(properties): 

846 return sorted(properties, key=lambda prop: prop.buildings["hotel"])[0] 

847 

848 

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 

855 

856 def __repr__(self): 

857 return f"<Monopoly type={self.properties[0].type}" 

858 

859 @property 

860 def num_houses(self): 

861 return sum(property.buildings["house"] for property in self.properties) 

862 

863 @property 

864 def num_hotels(self): 

865 return sum(property.buildings["hotel"] for property in self.properties) 

866 

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] 

876 

877 if not num_houses and not num_hotels: 

878 return "house", first_prop 

879 

880 elif num_hotels == max_num_hotels: 

881 return None, None 

882 

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 

893 

894 

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 = [] 

904 

905 def __init__(self): 

906 Bank.pay(self, 1_500) 

907 

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 

914 

915 def check_funds(self, amount): 

916 if amount > self.money: 

917 raise NotEnough 

918 

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 

925 

926 if property_.__class__.__name__ == "BuildableProperty" and self.owns_all_type( 

927 property_.type 

928 ): 

929 monopoly = Monopoly(property_) 

930 self.monopolies.append(monopoly) 

931 

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 

948 

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 

958 

959 self.advance(num_spaces, just_rolled=True) 

960 

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) 

968 

969 def owns_all_type(self, type_): 

970 return self.owns_x_of_type(type_) == Property.get_num_of_type(type_) 

971 

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 

978 

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) 

986 

987 @property 

988 def assets(self): 

989 return self.money + self.total_property_mortgage_value 

990 

991 @property 

992 def total_property_mortgage_value(self): 

993 return sum(property.mortgage_cost for property in self.properties) 

994 

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] 

1000 

1001 @property 

1002 def buildable_properties(self) -> List[BuildableProperty]: 

1003 return [ 

1004 p for p in self.properties if p.__class__.__name__ == "BuildableProperty" 

1005 ] 

1006 

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 ) 

1027 

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 

1035 

1036 self.current_space_index = new_space_index 

1037 

1038 if just_rolled: 

1039 last_roll = num_spaces 

1040 else: 

1041 last_roll = None 

1042 

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 

1049 

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) 

1053 

1054 def go_to_jail(self): 

1055 self.in_jail = True 

1056 self.current_space_index = get_space_index("Jail") 

1057 

1058 def can_afford(self, cost): 

1059 return self.money >= cost 

1060 

1061 

1062class Game: 

1063 games = [] 

1064 rounds = 0 

1065 

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() 

1083 

1084 @property 

1085 def active_players(self): 

1086 return [player for player in self._players if not player.bankrupt] 

1087 

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 

1096 

1097 def get_rounds_played_per_player(self): 

1098 return self.rounds / len(self._players) 

1099 

1100 def end(self): 

1101 for player in self._players: 

1102 del player