Coverage for exceptions.py : + 100% +
+
+ diff --git a/.coverage b/.coverage new file mode 100644 index 0000000..b44c0cb Binary files /dev/null and b/.coverage differ diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..d3d9b3a --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[run] +omit = + */site-packages/* + */distutils/* + tests/* diff --git a/__pycache__/buy_decision_algos.cpython-37.pyc b/__pycache__/buy_decision_algos.cpython-37.pyc new file mode 100644 index 0000000..c5dacfe Binary files /dev/null and b/__pycache__/buy_decision_algos.cpython-37.pyc differ diff --git a/__pycache__/buy_decision_algos.cpython-39.pyc b/__pycache__/buy_decision_algos.cpython-39.pyc new file mode 100644 index 0000000..add2f1a Binary files /dev/null and b/__pycache__/buy_decision_algos.cpython-39.pyc differ diff --git a/__pycache__/exceptions.cpython-37.pyc b/__pycache__/exceptions.cpython-37.pyc new file mode 100644 index 0000000..934d4a3 Binary files /dev/null and b/__pycache__/exceptions.cpython-37.pyc differ diff --git a/__pycache__/exceptions.cpython-39.pyc b/__pycache__/exceptions.cpython-39.pyc new file mode 100644 index 0000000..9a48bba Binary files /dev/null and b/__pycache__/exceptions.cpython-39.pyc differ diff --git a/__pycache__/monopoly.cpython-37.pyc b/__pycache__/monopoly.cpython-37.pyc new file mode 100644 index 0000000..c02a034 Binary files /dev/null and b/__pycache__/monopoly.cpython-37.pyc differ diff --git a/__pycache__/monopoly.cpython-39.pyc b/__pycache__/monopoly.cpython-39.pyc new file mode 100644 index 0000000..f26b69e Binary files /dev/null and b/__pycache__/monopoly.cpython-39.pyc differ diff --git a/__pycache__/simulate.cpython-37.pyc b/__pycache__/simulate.cpython-37.pyc new file mode 100644 index 0000000..080ab60 Binary files /dev/null and b/__pycache__/simulate.cpython-37.pyc differ diff --git a/__pycache__/simulate.cpython-39.pyc b/__pycache__/simulate.cpython-39.pyc new file mode 100644 index 0000000..0763384 Binary files /dev/null and b/__pycache__/simulate.cpython-39.pyc differ diff --git a/buy_decision_algos.py b/buy_decision_algos.py new file mode 100644 index 0000000..88adf3c --- /dev/null +++ b/buy_decision_algos.py @@ -0,0 +1,88 @@ +""" +from +[here](https://blog.ed.ted.com/2017/12/01/heres-how-to-win-at-monopoly-according-to-math-experts/): +'For every property (apart from the brown set — which, let’s be honest, is basically pointless), +it’s the third house that is really worth investing in quickly. After that, build more if you have +the money, but it’s probably worth waiting a few turns if cash is a bit tight. Since there are a +limited number of houses in the game, building three houses on properties early and then waiting +to upgrade further has the added advantage of potentially blocking the building projects of other +players. Sneaky, huh?' +'utilities are completely pointless.' +""" +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING + +from monopoly import Property, Player + + +class BuyDecision(ABC): + @abstractmethod + def __call__(self, property_: "Property", player: "Player"): + pass + + +class BuyEverything(BuyDecision): + def __call__(self, _, __): + return True + + +class BuyIfHaveThreeTimesPrice(BuyDecision): + def __call__(self, property_: "Property", player: "Player"): + return player.money >= property_.cost * 3 + + +class BuyIfDontHaveTwoPartialMonopoliesOfOtherColors(BuyDecision): + def __call__(self, property_: "Property", player: "Player"): + num_partial_monopolies = 0 + for property_type, properties in player.properties_by_type.items(): + if property_type in ("railroad", "utility"): + continue + if len(properties) == 3: + num_partial_monopolies += 1 + if num_partial_monopolies == 3: + print( + f"{player.name} isn't buying {property_} because " + f"they have {num_partial_monopolies} monopolies" + ) + return False + return True + + +class BuyIfOwnFewerThanFivePropertiesOrHaveOneOfThisColor(BuyDecision): + def __call__(self, property_, player): + num_properties = len(player.properties) + if num_properties < 5: + print(f"{player} wants to buy {property_}") + return True + for property_type, properties in player.properties_by_type.items(): + if property_type == property_.type: + print(f"{player} wants to buy {property_}") + return True + print(f"{player} doesn't want to buy {property_}") + return False + + +class BuyIfNoOneOwnsTypeAndIsOfTheOneTypeOwned(BuyDecision): + """ +ALGORITHM +--------- +If nobody owns this type of property, buy it. +If only one player owns all owned property of this type, buy it. +If this player owns any of this type fo property, buy it. + """ + + def __call__(self, property_, player): + properties_of_this_type = Property.instances_by_type()[property_.type] + if not any(p.owner for p in properties_of_this_type): + return True + # prevent others from getting monopolies + if ( + len(set([p.owner for p in Property.instances_by_type()[property_.type]])) + == 1 + ): + return True + + players_property_types = player.properties_by_type.keys() + if player.properties and property_.type in players_property_types: + return True + return False diff --git a/exceptions.py b/exceptions.py index bad5012..46297e8 100644 --- a/exceptions.py +++ b/exceptions.py @@ -10,6 +10,10 @@ class NotEnough(Exception): pass +class NotEnoughPlayers(NotEnough): + pass + + class MustBeEqualAmounts(Exception): pass diff --git a/htmlcov/coverage_html.js b/htmlcov/coverage_html.js new file mode 100644 index 0000000..3bf04bf --- /dev/null +++ b/htmlcov/coverage_html.js @@ -0,0 +1,589 @@ +// Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +// For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +// Coverage.py HTML report browser code. +/*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */ +/*global coverage: true, document, window, $ */ + +coverage = {}; + +// Find all the elements with shortkey_* class, and use them to assign a shortcut key. +coverage.assign_shortkeys = function () { + $("*[class*='shortkey_']").each(function (i, e) { + $.each($(e).attr("class").split(" "), function (i, c) { + if (/^shortkey_/.test(c)) { + $(document).bind('keydown', c.substr(9), function () { + $(e).click(); + }); + } + }); + }); +}; + +// Create the events for the help panel. +coverage.wire_up_help_panel = function () { + $("#keyboard_icon").click(function () { + // Show the help panel, and position it so the keyboard icon in the + // panel is in the same place as the keyboard icon in the header. + $(".help_panel").show(); + var koff = $("#keyboard_icon").offset(); + var poff = $("#panel_icon").position(); + $(".help_panel").offset({ + top: koff.top-poff.top, + left: koff.left-poff.left + }); + }); + $("#panel_icon").click(function () { + $(".help_panel").hide(); + }); +}; + +// Create the events for the filter box. +coverage.wire_up_filter = function () { + // Cache elements. + var table = $("table.index"); + var table_rows = table.find("tbody tr"); + var table_row_names = table_rows.find("td.name a"); + var no_rows = $("#no_rows"); + + // Create a duplicate table footer that we can modify with dynamic summed values. + var table_footer = $("table.index tfoot tr"); + var table_dynamic_footer = table_footer.clone(); + table_dynamic_footer.attr('class', 'total_dynamic hidden'); + table_footer.after(table_dynamic_footer); + + // Observe filter keyevents. + $("#filter").on("keyup change", $.debounce(150, function (event) { + var filter_value = $(this).val(); + + if (filter_value === "") { + // Filter box is empty, remove all filtering. + table_rows.removeClass("hidden"); + + // Show standard footer, hide dynamic footer. + table_footer.removeClass("hidden"); + table_dynamic_footer.addClass("hidden"); + + // Hide placeholder, show table. + if (no_rows.length > 0) { + no_rows.hide(); + } + table.show(); + + } + else { + // Filter table items by value. + var hidden = 0; + var shown = 0; + + // Hide / show elements. + $.each(table_row_names, function () { + var element = $(this).parents("tr"); + + if ($(this).text().indexOf(filter_value) === -1) { + // hide + element.addClass("hidden"); + hidden++; + } + else { + // show + element.removeClass("hidden"); + shown++; + } + }); + + // Show placeholder if no rows will be displayed. + if (no_rows.length > 0) { + if (shown === 0) { + // Show placeholder, hide table. + no_rows.show(); + table.hide(); + } + else { + // Hide placeholder, show table. + no_rows.hide(); + table.show(); + } + } + + // Manage dynamic header: + if (hidden > 0) { + // Calculate new dynamic sum values based on visible rows. + for (var column = 2; column < 20; column++) { + // Calculate summed value. + var cells = table_rows.find('td:nth-child(' + column + ')'); + if (!cells.length) { + // No more columns...! + break; + } + + var sum = 0, numer = 0, denom = 0; + $.each(cells.filter(':visible'), function () { + var ratio = $(this).data("ratio"); + if (ratio) { + var splitted = ratio.split(" "); + numer += parseInt(splitted[0], 10); + denom += parseInt(splitted[1], 10); + } + else { + sum += parseInt(this.innerHTML, 10); + } + }); + + // Get footer cell element. + var footer_cell = table_dynamic_footer.find('td:nth-child(' + column + ')'); + + // Set value into dynamic footer cell element. + if (cells[0].innerHTML.indexOf('%') > -1) { + // Percentage columns use the numerator and denominator, + // and adapt to the number of decimal places. + var match = /\.([0-9]+)/.exec(cells[0].innerHTML); + var places = 0; + if (match) { + places = match[1].length; + } + var pct = numer * 100 / denom; + footer_cell.text(pct.toFixed(places) + '%'); + } + else { + footer_cell.text(sum); + } + } + + // Hide standard footer, show dynamic footer. + table_footer.addClass("hidden"); + table_dynamic_footer.removeClass("hidden"); + } + else { + // Show standard footer, hide dynamic footer. + table_footer.removeClass("hidden"); + table_dynamic_footer.addClass("hidden"); + } + } + })); + + // Trigger change event on setup, to force filter on page refresh + // (filter value may still be present). + $("#filter").trigger("change"); +}; + +// Loaded on index.html +coverage.index_ready = function ($) { + // Look for a localStorage item containing previous sort settings: + var sort_list = []; + var storage_name = "COVERAGE_INDEX_SORT"; + var stored_list = undefined; + try { + stored_list = localStorage.getItem(storage_name); + } catch(err) {} + + if (stored_list) { + sort_list = JSON.parse('[[' + stored_list + ']]'); + } + + // Create a new widget which exists only to save and restore + // the sort order: + $.tablesorter.addWidget({ + id: "persistentSort", + + // Format is called by the widget before displaying: + format: function (table) { + if (table.config.sortList.length === 0 && sort_list.length > 0) { + // This table hasn't been sorted before - we'll use + // our stored settings: + $(table).trigger('sorton', [sort_list]); + } + else { + // This is not the first load - something has + // already defined sorting so we'll just update + // our stored value to match: + sort_list = table.config.sortList; + } + } + }); + + // Configure our tablesorter to handle the variable number of + // columns produced depending on report options: + var headers = []; + var col_count = $("table.index > thead > tr > th").length; + + headers[0] = { sorter: 'text' }; + for (i = 1; i < col_count-1; i++) { + headers[i] = { sorter: 'digit' }; + } + headers[col_count-1] = { sorter: 'percent' }; + + // Enable the table sorter: + $("table.index").tablesorter({ + widgets: ['persistentSort'], + headers: headers + }); + + coverage.assign_shortkeys(); + coverage.wire_up_help_panel(); + coverage.wire_up_filter(); + + // Watch for page unload events so we can save the final sort settings: + $(window).unload(function () { + try { + localStorage.setItem(storage_name, sort_list.toString()) + } catch(err) {} + }); +}; + +// -- pyfile stuff -- + +coverage.pyfile_ready = function ($) { + // If we're directed to a particular line number, highlight the line. + var frag = location.hash; + if (frag.length > 2 && frag[1] === 't') { + $(frag).addClass('highlight'); + coverage.set_sel(parseInt(frag.substr(2), 10)); + } + else { + coverage.set_sel(0); + } + + $(document) + .bind('keydown', 'j', coverage.to_next_chunk_nicely) + .bind('keydown', 'k', coverage.to_prev_chunk_nicely) + .bind('keydown', '0', coverage.to_top) + .bind('keydown', '1', coverage.to_first_chunk) + ; + + $(".button_toggle_run").click(function (evt) {coverage.toggle_lines(evt.target, "run");}); + $(".button_toggle_exc").click(function (evt) {coverage.toggle_lines(evt.target, "exc");}); + $(".button_toggle_mis").click(function (evt) {coverage.toggle_lines(evt.target, "mis");}); + $(".button_toggle_par").click(function (evt) {coverage.toggle_lines(evt.target, "par");}); + + coverage.assign_shortkeys(); + coverage.wire_up_help_panel(); + + coverage.init_scroll_markers(); + + // Rebuild scroll markers when the window height changes. + $(window).resize(coverage.build_scroll_markers); +}; + +coverage.toggle_lines = function (btn, cls) { + btn = $(btn); + var show = "show_"+cls; + if (btn.hasClass(show)) { + $("#source ." + cls).removeClass(show); + btn.removeClass(show); + } + else { + $("#source ." + cls).addClass(show); + btn.addClass(show); + } + coverage.build_scroll_markers(); +}; + +// Return the nth line div. +coverage.line_elt = function (n) { + return $("#t" + n); +}; + +// Return the nth line number div. +coverage.num_elt = function (n) { + return $("#n" + n); +}; + +// Set the selection. b and e are line numbers. +coverage.set_sel = function (b, e) { + // The first line selected. + coverage.sel_begin = b; + // The next line not selected. + coverage.sel_end = (e === undefined) ? b+1 : e; +}; + +coverage.to_top = function () { + coverage.set_sel(0, 1); + coverage.scroll_window(0); +}; + +coverage.to_first_chunk = function () { + coverage.set_sel(0, 1); + coverage.to_next_chunk(); +}; + +// Return a string indicating what kind of chunk this line belongs to, +// or null if not a chunk. +coverage.chunk_indicator = function (line_elt) { + var klass = line_elt.attr('class'); + if (klass) { + var m = klass.match(/\bshow_\w+\b/); + if (m) { + return m[0]; + } + } + return null; +}; + +coverage.to_next_chunk = function () { + var c = coverage; + + // Find the start of the next colored chunk. + var probe = c.sel_end; + var chunk_indicator, probe_line; + while (true) { + probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + return; + } + chunk_indicator = c.chunk_indicator(probe_line); + if (chunk_indicator) { + break; + } + probe++; + } + + // There's a next chunk, `probe` points to it. + var begin = probe; + + // Find the end of this chunk. + var next_indicator = chunk_indicator; + while (next_indicator === chunk_indicator) { + probe++; + probe_line = c.line_elt(probe); + next_indicator = c.chunk_indicator(probe_line); + } + c.set_sel(begin, probe); + c.show_selection(); +}; + +coverage.to_prev_chunk = function () { + var c = coverage; + + // Find the end of the prev colored chunk. + var probe = c.sel_begin-1; + var probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + return; + } + var chunk_indicator = c.chunk_indicator(probe_line); + while (probe > 0 && !chunk_indicator) { + probe--; + probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + return; + } + chunk_indicator = c.chunk_indicator(probe_line); + } + + // There's a prev chunk, `probe` points to its last line. + var end = probe+1; + + // Find the beginning of this chunk. + var prev_indicator = chunk_indicator; + while (prev_indicator === chunk_indicator) { + probe--; + probe_line = c.line_elt(probe); + prev_indicator = c.chunk_indicator(probe_line); + } + c.set_sel(probe+1, end); + c.show_selection(); +}; + +// Return the line number of the line nearest pixel position pos +coverage.line_at_pos = function (pos) { + var l1 = coverage.line_elt(1), + l2 = coverage.line_elt(2), + result; + if (l1.length && l2.length) { + var l1_top = l1.offset().top, + line_height = l2.offset().top - l1_top, + nlines = (pos - l1_top) / line_height; + if (nlines < 1) { + result = 1; + } + else { + result = Math.ceil(nlines); + } + } + else { + result = 1; + } + return result; +}; + +// Returns 0, 1, or 2: how many of the two ends of the selection are on +// the screen right now? +coverage.selection_ends_on_screen = function () { + if (coverage.sel_begin === 0) { + return 0; + } + + var top = coverage.line_elt(coverage.sel_begin); + var next = coverage.line_elt(coverage.sel_end-1); + + return ( + (top.isOnScreen() ? 1 : 0) + + (next.isOnScreen() ? 1 : 0) + ); +}; + +coverage.to_next_chunk_nicely = function () { + coverage.finish_scrolling(); + if (coverage.selection_ends_on_screen() === 0) { + // The selection is entirely off the screen: select the top line on + // the screen. + var win = $(window); + coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop())); + } + coverage.to_next_chunk(); +}; + +coverage.to_prev_chunk_nicely = function () { + coverage.finish_scrolling(); + if (coverage.selection_ends_on_screen() === 0) { + var win = $(window); + coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop() + win.height())); + } + coverage.to_prev_chunk(); +}; + +// Select line number lineno, or if it is in a colored chunk, select the +// entire chunk +coverage.select_line_or_chunk = function (lineno) { + var c = coverage; + var probe_line = c.line_elt(lineno); + if (probe_line.length === 0) { + return; + } + var the_indicator = c.chunk_indicator(probe_line); + if (the_indicator) { + // The line is in a highlighted chunk. + // Search backward for the first line. + var probe = lineno; + var indicator = the_indicator; + while (probe > 0 && indicator === the_indicator) { + probe--; + probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + break; + } + indicator = c.chunk_indicator(probe_line); + } + var begin = probe + 1; + + // Search forward for the last line. + probe = lineno; + indicator = the_indicator; + while (indicator === the_indicator) { + probe++; + probe_line = c.line_elt(probe); + indicator = c.chunk_indicator(probe_line); + } + + coverage.set_sel(begin, probe); + } + else { + coverage.set_sel(lineno); + } +}; + +coverage.show_selection = function () { + var c = coverage; + + // Highlight the lines in the chunk + $(".linenos .highlight").removeClass("highlight"); + for (var probe = c.sel_begin; probe > 0 && probe < c.sel_end; probe++) { + c.num_elt(probe).addClass("highlight"); + } + + c.scroll_to_selection(); +}; + +coverage.scroll_to_selection = function () { + // Scroll the page if the chunk isn't fully visible. + if (coverage.selection_ends_on_screen() < 2) { + // Need to move the page. The html,body trick makes it scroll in all + // browsers, got it from http://stackoverflow.com/questions/3042651 + var top = coverage.line_elt(coverage.sel_begin); + var top_pos = parseInt(top.offset().top, 10); + coverage.scroll_window(top_pos - 30); + } +}; + +coverage.scroll_window = function (to_pos) { + $("html,body").animate({scrollTop: to_pos}, 200); +}; + +coverage.finish_scrolling = function () { + $("html,body").stop(true, true); +}; + +coverage.init_scroll_markers = function () { + var c = coverage; + // Init some variables + c.lines_len = $('#source p').length; + c.body_h = $('body').height(); + c.header_h = $('div#header').height(); + + // Build html + c.build_scroll_markers(); +}; + +coverage.build_scroll_markers = function () { + var c = coverage, + min_line_height = 3, + max_line_height = 10, + visible_window_h = $(window).height(); + + c.lines_to_mark = $('#source').find('p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par'); + $('#scroll_marker').remove(); + // Don't build markers if the window has no scroll bar. + if (c.body_h <= visible_window_h) { + return; + } + + $("body").append("
+
+ 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 +
+1class TooMany(Exception):
+2 pass
+ + +5class TooManyPlayers(TooMany):
+6 pass
+ + +9class NotEnough(Exception):
+10 pass
+ + +13class NotEnoughPlayers(NotEnough):
+14 pass
+ + +17class MustBeEqualAmounts(Exception):
+18 pass
+ + +21class Argument(Exception):
+22 pass
+ + +25class DidntFind(Exception):
+26 pass
+ + +29class NoOwner(Exception):
+30 pass
+ + +33class CantMortgage(Exception):
+34 pass
+ + +37class CantBuyBuildings(Exception):
+38 pass
+
+
+
+ Hot-keys on this page
++ n + s + m + x + c change column sorting +
+| Module | +statements | +missing | +excluded | +coverage | +
|---|---|---|---|---|
| Total | +505 | +250 | +0 | +50% | +
| exceptions.py | +20 | +0 | +0 | +100% | +
| monopoly.py | +485 | +250 | +0 | +48% | +
+ No items found using the specified filter. +
+| t |
+
+ 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
+