import math # noqa import os import time from collections import namedtuple from typing import List, Tuple from otree.api import * # noqa from otree.api import (BaseConstants, BaseGroup, BasePlayer, BaseSubsession, Page, WaitPage, models, widgets) from tabulate import tabulate class C(BaseConstants): """Class with Otree specific constants""" NUM_ROUNDS = 10 # "sim_k", "sim_d", "sim_r" or "sample" DIE_NAME = "sim_k" PLAYERS_PER_GROUP = None NAME_IN_URL = os.path.basename(os.path.dirname(__file__)) INSTRUCTIONS_TEMPLATE = f'{NAME_IN_URL}/Instructions.html' MAX_SHARES = 15 MAX_PER_ROUND = 15 INITIAL_WALLET = 12000.0 AUCTION_TIMEOUT = 90 RESULTS_TIMEOUT = 180 TOLL_COST = 5 ######################################################### tabulate.PRESERVE_WHITESPACE = True doc = """ TODO """ # Game Constants ## Simulation to play # SIMULATION = "sim_d" ## Maintenance cost for each simulation MAINTENANCE = { "sim_k": 600, "sim_d": 900, "sim_r": 300, "sample": 300, } ## Other useful constants BUY = 0 SELL = 1 LLC = "LLC" ULC = "ULC" SIGN = [1, -1] # START OTREE CODE class Subsession(BaseSubsession): def creating_session(self): for g in self.get_groups(): if self.round_number > 1: prev = g.in_round(self.round_number - 1) g.llc_shares = prev.llc_shares g.llc_bankrupt = prev.llc_bankrupt g.ulc_shares = prev.ulc_shares g.ulc_bankrupt = prev.ulc_bankrupt for p in self.get_players(): if self.round_number == 1: p.wallet = C.INITIAL_WALLET else: prev = p.in_round(self.round_number - 1) p.llc_shares = prev.llc_shares p.ulc_shares = prev.ulc_shares wallet = calculate_wallet_at(p, start=True) p.wallet = wallet.total class Group(BaseGroup): # Properties for Company LLC llc_shares = models.IntegerField(initial=C.MAX_SHARES) llc_die = models.IntegerField() llc_bankrupt = models.BooleanField(initial=False) llc_profit = models.CurrencyField(initial=0.0) llc_uniform_price = models.CurrencyField(initial=0.0) # Properties for Company ULC ulc_shares = models.IntegerField(initial=C.MAX_SHARES) ulc_die = models.IntegerField() ulc_bankrupt = models.BooleanField(initial=False) ulc_profit = models.CurrencyField(initial=0.0) ulc_uniform_price = models.CurrencyField(initial=0.0) class Player(BasePlayer): wallet = models.CurrencyField( initial=C.INITIAL_WALLET ) # represents the number of shares of LLC llc_shares = models.IntegerField( initial=0 ) # represents the number of shares of ULC ulc_shares = models.IntegerField( initial=0 ) # Form Fields # represents the choice to BUY/SELL shares of LLC llc_choice = models.IntegerField( initial=BUY, choices=[[BUY, 'Buy'], [SELL, 'Sell']], widget=widgets.RadioSelect, blank=True ) # represents the price per share of LLC to BUY/SELL for llc_price = models.CurrencyField( initial=0.0, min=0.0, blank=True ) # represents the number of shares of LLC to BUY/SELL llc_number = models.IntegerField( initial=0, min=0, max=C.MAX_PER_ROUND, blank=True ) # represents the actual number of shares of LLC that were BOUGHT/SOLD llc_number_final = models.IntegerField( initial=0, min=0, blank=True ) # represents the profit gained from shares of LLC llc_profit = models.CurrencyField( initial=0.0, min=0.0, blank=True ) # represents the choice to BUY/SELL shares of ULC ulc_choice = models.IntegerField( initial=BUY, choices=[[BUY, 'Buy'], [SELL, 'Sell']], widget=widgets.RadioSelect, ) # represents the price per share of ULC to BUY/SELL for ulc_price = models.CurrencyField( initial=0.0, min=0.0, blank=True ) # represents the number of shares of ULC to BUY/SELL ulc_number = models.IntegerField( initial=0, min=0, max=C.MAX_PER_ROUND, blank=True ) # represents the actual number of shares of ULC that were BOUGHT/SOLD ulc_number_final = models.IntegerField( initial=0, min=0, blank=True ) # represents the profit gained from shares of ULC ulc_profit = models.CurrencyField( initial=0.0, min=0.0, blank=True ) # represents the number of shares of LLC that were confiscated to pay for ULC confiscated_shares = models.IntegerField( initial=0 ) # represents the revenue earned from the confiscated shares of LLC confiscated_revenue = models.CurrencyField( initial=0.0, min=0.0 ) # represents the time the participant responded, used to sort participants time = models.FloatField(initial=0.0) def str_id(self): return f'PL{self.id_in_subsession:>02}' def z(value: float, w=9): """Formats value in the game's currency""" return f'$ {value:{w},.2f}' def print_revenue(g: Group, profit=False): # DIE = g.session.DIE headers = [f"ROUND {g.round_number:2}", "COMP.", "SIDE", "CARS", "REVENUE"] table = [ ["", "LLC", f'{(g.llc_die + 1):2}', f'{DIE.sides[g.llc_die].cars:2}', z(DIE.sides[g.llc_die].toll_revenue)], ["", "ULC", f'{(g.ulc_die + 1):2}', f'{DIE.sides[g.ulc_die].cars:2}', z(DIE.sides[g.ulc_die].toll_revenue)], ] llc_profit = DIE.sides[g.llc_die].toll_revenue - DIE.maintenance ulc_profit = DIE.sides[g.ulc_die].toll_revenue - DIE.maintenance if profit: headers.append("PROFIT") table[0].append(z(llc_profit)) table[1].append(z(ulc_profit)) headers.append("BANKRUPT?") table[0].append(" XXXXX " if llc_profit < 0 else "") table[1].append(" XXXXX " if ulc_profit < 0 else "") print(tabulate(table, headers=headers, disable_numparse=True)) print() # FUNCTIONS def creating_session(subsession: Subsession): """Creates a Session, formed by several subsessions (Rounds) """ print(f'{">>>> creating_session ":-<50}') # get session and number session = subsession.session index = 0 if "NUMBER" in session.vars: index = session.NUMBER session.NUMBER = index + 1 # print(f'">>>>>>>> Session Vars [{session.vars}] "') # print(f'">>>>>>>> Session [{index:02}] "') # create DIE if not yet # Get DIE filename ## 1. From Session # die_filename = session.config['die'] # if isinstance(die_filename, str): # die_filename = die_filename.lower() # else: # die_filename = die_filename[index].lower() # session.DIE_FILENAME = die_filename ## 2. From Constant # die_filename = C.DIE_NAME # print(f'">>>>>>>> using die [{die_filename}.tsv] "') # DIE = Die(get_die(die_filename), MAINTENANCE[die_filename]) # session.DIE = DIE # for each Group for g in subsession.get_groups(): # import random here as instructed by Otree, to get # real randomness import random # throw the dice for LLC, and ULC g.llc_die = random.randint(0, len(DIE.sides) - 1) g.ulc_die = random.randint(0, len(DIE.sides) - 1) # TEST TEST TEST # if g.round_number == 1: # g.llc_die = 5 # g.ulc_die = 1 # if g.round_number == 2: # g.llc_die = 1 # g.ulc_die = 0 # if g.round_number == 3: # g.llc_die = 1 # g.ulc_die = 0 # if g.round_number == 4: # g.llc_die = 5 # g.ulc_die = 6 # if g.round_number == 5: # g.llc_die = 3 # g.ulc_die = 3 print_revenue(g) print(f'{"<<<< creating_session ":-<50}') class Offer(object): """Represents a SINGLE Buy/Sell offer with a price, the player who made the offer, and the company it is for""" def __init__(self, player: Player, price, company, time): self.player = player self.price = price self.company = company self.time = time def get_id(self) -> str: if self.player: return self.player.str_id() return "HOUSE" def tab(self): return [self.get_id(), z(self.price)] SortedOffers = namedtuple("SortedOffers", "buy_offers sell_offers") def sort_buyers_sellers(group: Group) -> Tuple[SortedOffers, SortedOffers]: """Sorts all the buy and sell offers from highest to lowest. Returns the sorted list in this order: LLC buy offers, LLC sell offers ULC buy offers, ULC sell offers """ # sort buyers and sellers llc_buy_offers = [] llc_sell_offers = [] ulc_buy_offers = [] ulc_sell_offers = [] for p in group.get_players(): # if player chose to BUY if p.llc_choice == BUY: # for each share, create an offer and add it to the list for i in range(p.llc_number): offer = Offer(p, p.llc_price, LLC, p.time) llc_buy_offers.append(offer) elif p.llc_choice == SELL and p.llc_number: for i in range(p.llc_number): offer = Offer(p, p.llc_price, LLC, p.time) llc_sell_offers.append(offer) if p.ulc_choice == BUY and p.ulc_number: for i in range(p.ulc_number): offer = Offer(p, p.ulc_price, ULC, p.time) ulc_buy_offers.append(offer) elif p.ulc_choice == SELL and p.ulc_number: for i in range(p.ulc_number): offer = Offer(p, p.ulc_price, ULC, p.time) ulc_sell_offers.append(offer) # if there are still shares in the HOUSE, add them with price 0 for i in range(group.llc_shares): offer = Offer(None, 0, LLC, time.time()) llc_sell_offers.append(offer) for i in range(group.ulc_shares): offer = Offer(None, 0, ULC, time.time()) ulc_sell_offers.append(offer) # sort the offers (buy offers high-to-low, sell offers low-to-high) llc_buy_offers.sort(key=lambda x: (-x.price, x.time)) llc_sell_offers.sort(key=lambda x: (x.price, x.time)) ulc_buy_offers.sort(key=lambda x: (-x.price, x.time)) ulc_sell_offers.sort(key=lambda x: (x.price, x.time)) rows = max(len(llc_buy_offers), len(llc_sell_offers)) table = [] for i in range(rows): row = [f'{(i+1):02}'] row.extend(llc_buy_offers[i].tab() if len(llc_buy_offers) > i else ["--", z(0.00)]) row.extend(llc_sell_offers[i].tab() if len(llc_sell_offers) > i else ["--", z(0.00)]) table.append(row) print(tabulate(table, headers=["LLC", "BUYER", "PRICE", "SELLER", "PRICE"])) print() rows = max(len(ulc_buy_offers), len(ulc_sell_offers)) table = [] for i in range(rows): row = [f'{(i+1):02}'] row.extend(ulc_buy_offers[i].tab() if len(ulc_buy_offers) > i else ["--", z(0.00)]) row.extend(ulc_sell_offers[i].tab() if len(ulc_sell_offers) > i else ["--", z(0.00)]) table.append(row) print(tabulate(table, headers=["ULC", "BUYER", "PRICE", "SELLER", "PRICE"])) llc_offers = SortedOffers(llc_buy_offers, llc_sell_offers) ulc_offers = SortedOffers(ulc_buy_offers, ulc_sell_offers) return llc_offers, ulc_offers def calculate_profit(group: Group): print(f'{">>>> calculate_profit ":-<50}') # DIE = group.session.DIE print_revenue(group, profit=True) group.llc_profit = DIE.sides[group.llc_die].toll_revenue - DIE.maintenance group.ulc_profit = DIE.sides[group.ulc_die].toll_revenue - DIE.maintenance # BANKRUPCIES and PROFITS ## LLC if group.llc_profit < 0 or group.llc_bankrupt: group.llc_profit = 0 group.llc_bankrupt = True print(f'{" ~~~ LLC IS BANKRUPT! ":-<50}') ## ULC if group.ulc_bankrupt: group.ulc_profit = 0 if group.ulc_profit < 0: group.ulc_bankrupt = True print(f'{" ~~~ ULC HAS SHUT DOWN! ":-<50}') print(f'{" -- PROFITS ":-<50}') headers = ["PLAYER", "COMP.", "SHARES", "PROFIT"] table = [] # Pay the profits for p in group.get_players(): row = [] llc_shares = p.llc_shares if llc_shares: p.llc_profit = group.llc_profit / C.MAX_SHARES * llc_shares row.append([p.str_id(), "LLC", llc_shares, z(p.llc_profit)]) if p.ulc_shares: p.ulc_profit = group.ulc_profit / C.MAX_SHARES * p.ulc_shares row.append([p.str_id(), "ULC", p.ulc_shares, z(p.ulc_profit)]) table.extend(row) wallet = calculate_wallet_at(p, start=False) # check sign for ULC profits if p.ulc_profit < 0: # this can only happen if ULC profits were negative, and the player ran out of money. # In this case, sell LLC stock to recover if wallet.total < 0: if group.llc_uniform_price: # find out how many shares of LLC to confiscate p.confiscated_shares = math.ceil(wallet.total / group.llc_uniform_price) p.confiscated_revenue = group.llc_uniform_price * p.confiscated_shares # new_total = wallet.total + p.confiscated_revenue else: p.confiscated_shares = p.llc_shares p.confiscated_revenue = -wallet.total # new_total = 0 row.append([p.str_id(), "CONF.", p.confiscated_shares, z(p.confiscated_revenue)]) print(tabulate(table, headers=headers)) print(f'{"<<<< calculate_profit ":-<50}') def print_status(g: Group, title: str): players = g.get_players() print() print(f'{" STATUS (" + title + ") ":-<50}') headers = ["", "CAPITAL", "LLC", "ULC"] table = [] for p in players: row = [p.str_id(), z(p.wallet), p.llc_shares, p.ulc_shares] table.append(row) # FIXME wallet = calculate_wallet_at(p, start=False) # llc_transaction = SIGN[p.llc_choice] * p.llc_number_final * g.llc_uniform_price # ulc_transaction = SIGN[p.ulc_choice] * p.ulc_number_final * g.ulc_uniform_price # if round == 1: # llc_transaction = SIGN[p.llc_choice] * p.llc_number_final * p.llc_price # ulc_transaction = SIGN[p.ulc_choice] * p.ulc_number_final * p.ulc_price if p.llc_number_final: table.append(["", z(-wallet.llc_tx), p.llc_number_final, ""]) # table.append(["", z(-llc_transaction), p.llc_number_final, ""]) if p.ulc_number_final: table.append(["", z(-wallet.ulc_tx), "", p.ulc_number_final]) # table.append(["", z(-ulc_transaction), "", p.ulc_number_final]) if p.llc_profit: table.append(["", z(p.llc_profit), "*", ""]) if p.ulc_profit: table.append(["", z(p.ulc_profit), "", "*"]) print(tabulate(table, headers=headers)) print() def get_price_index_for_company(offers: List[List[Offer]]) -> Tuple[int, float]: """Calculates the uniform price for the list of offers """ print(f'{">>>>>> get_price_index_for_company ":-<50}') min_offers = min(C.MAX_SHARES, len(offers[BUY]), len(offers[SELL])) # get price for company index = -1 print(f'{min_offers=}') for i in range(min_offers): print(f'{offers[BUY][i].price=}') print(f'{offers[SELL][i].price=}') if offers[BUY][i].price < offers[SELL][i].price: # if the BUY price is lower than the SELL price, stop break else: index = i print(f'{index=}') sell_price = 0 if index >= 0: sell_price = (offers[BUY][index].price + offers[SELL][index].price) / 2 print(f'{"<<<<<< get_price_index_for_company ":-<50}') return index, sell_price Wallet = namedtuple("Wallet", "total llc_tx ulc_tx llc_profit ulc_profit") def calculate_wallet_at(player: Player, start=True) -> Wallet: round = player.round_number group = player.group prev = player prev_group = group initial_round = 1 if start: prev = player.in_round(round - 1) prev_group = prev_group = group.in_round(round - 1) initial_round = 2 llc_transaction = SIGN[prev.llc_choice] * prev.llc_number_final * prev_group.llc_uniform_price ulc_transaction = SIGN[prev.ulc_choice] * prev.ulc_number_final * prev_group.ulc_uniform_price if round == initial_round: llc_transaction = SIGN[prev.llc_choice] * prev.llc_number_final * prev.llc_price ulc_transaction = SIGN[prev.ulc_choice] * prev.ulc_number_final * prev.ulc_price final = prev.wallet - llc_transaction - ulc_transaction + prev.llc_profit + prev.ulc_profit final += prev.confiscated_revenue wallet = Wallet(final, llc_transaction, ulc_transaction, prev.llc_profit, prev.ulc_profit) return wallet def calculate_round(group: Group): """Calculate the results of the round. 1. Defines shares that are bought/sold, and the seller/buyer 2. Calculate the profits """ # TEST TEST TEST # DIE = group.session.DIE # llc_profit = DIE.sides[group.llc_die].toll_revenue - DIE.maintenance # ulc_profit = DIE.sides[group.ulc_die].toll_revenue - DIE.maintenance # for p in group.get_players(): # # if llc_profit < 0 or group.llc_bankrupt: # if group.llc_bankrupt: # test_player_llc(p, 0, 0, 0) # # if ulc_profit < 0 or group.ulc_bankrupt: # if group.ulc_bankrupt: # test_player_ulc(p, 0, 0, 0) round = group.round_number print(f'{">>>> calculate_round ":-<50}') # sorts the player by time sorted_players = group.get_players() sorted_players.sort(key=lambda x: x.time, reverse=False) for i, p in enumerate(sorted_players): print(f"{p.str_id()} --> {i:02} {p.time}") # print the initial status print_status(group, f"ROUND {group.round_number:02} (START)") # 1. Defines shares that are bought/sold, and the seller/buyer ## 1.1. get and sort buy/sell offers llc_offers, ulc_offers = sort_buyers_sellers(group) ## 1.2. match buyer with sellers, and sell ### 1.2.1. In ROUND 1, buy only from the HOUSE if round == 1: max_llc = min(C.MAX_SHARES, len(llc_offers[BUY])) for i in range(max_llc): offer = llc_offers[BUY][i] player = offer.player player.llc_shares += 1 player.llc_number_final += 1 # player.llc_transaction -= offer.price group.llc_shares -= 1 max_ulc = min(C.MAX_SHARES, len(ulc_offers[BUY])) for i in range(max_ulc): offer = ulc_offers[BUY][i] player = offer.player player.ulc_shares += 1 player.ulc_number_final += 1 # player.ulc_transaction -= offer.price group.ulc_shares -= 1 ### 1.2.2. In Round 2+, buy from other players (and HOUSE if there are shares left) else: # get the uniform price for LLC and ULC llc_index, llc_price = get_price_index_for_company(llc_offers) print(f" *** LLC SELL PRICE: {z(llc_price)}") ulc_index, ulc_price = get_price_index_for_company(ulc_offers) print(f" *** ULC SELL PRICE: {z(ulc_price)}") group.llc_uniform_price = llc_price group.ulc_uniform_price = ulc_price for i in range(llc_index + 1): buyer = llc_offers[BUY][i].player seller = llc_offers[SELL][i].player buyer.llc_shares += 1 buyer.llc_number_final += 1 if seller: seller.llc_shares -= 1 seller.llc_number_final += 1 else: group.llc_shares -= 1 for i in range(ulc_index + 1): buyer = ulc_offers[BUY][i].player seller = ulc_offers[SELL][i].player buyer.ulc_shares += 1 buyer.ulc_number_final += 1 if seller: seller.ulc_shares -= 1 seller.ulc_number_final += 1 else: group.ulc_shares -= 1 print_status(group, f"ROUND {group.round_number:02} (AFTER BUY/SELL)") # 2. calculate profits calculate_profit(group) print_status(group, f"ROUND {group.round_number:02} (AFTER PROFITS)") print(f'{"<<<< calculate_round ":-<50}') # PAGES class Welcome(Page): @staticmethod def is_displayed(player: Player): return player.round_number == 1 @staticmethod def vars_for_template(player: Player): """Passes the variables to the Welcome page""" sample = Die(get_die("sample"), MAINTENANCE["sample"]) sample_die_result = 7 sample_profit = sample.sides[sample_die_result + 1].toll_revenue - sample.maintenance result = { "wallet": f'{C.INITIAL_WALLET:,.2f}', "wallet_cu": z(C.INITIAL_WALLET, w=0), "toll_cost": z(C.TOLL_COST, w=0), "max_revenue": z(sample.max_revenue()), "min_revenue": z(sample.min_revenue()), "mean_revenue": z(sample.mean_revenue()), "maintenance": z(sample.maintenance), "mean_profit": z(sample.mean_revenue() - sample.maintenance), "sample_die": sample_die_result, "sample_cars": sample.sides[sample_die_result + 1].cars, "sample_revenue": z(sample.sides[sample_die_result + 1].toll_revenue), "sample_profit": z(sample_profit), "sample_dividend": z(sample_profit / C.MAX_SHARES), # "sim": player.session.DIE_FILENAME, "sim": C.DIE_NAME, "die_sides": sample.sides[0:10], "die_sides2": sample.sides[10:] } return result def test_player_llc(p: Player, choice, number, price): p.llc_choice = choice p.llc_number = number p.llc_price = price if choice == SELL: p.llc_number = min(p.llc_shares, number) elif number: max_per_share = math.floor(p.wallet / number) p.llc_price = min(max_per_share, price) def test_player_ulc(p: Player, choice, number, price): p.ulc_choice = choice p.ulc_number = number p.ulc_price = price if choice == SELL: p.ulc_number = min(p.ulc_shares, number) elif number: max_per_share = math.floor(p.wallet / number) p.ulc_price = min(max_per_share, price) class Auction(Page): timeout_seconds = C.AUCTION_TIMEOUT form_model = 'player' @staticmethod def is_displayed(p: Player): # TEST TEST TEST # round = p.round_number # if p.str_id() == "PL01": # if round == 1: # test_player_llc(p, BUY, 6, 1000.0) # test_player_ulc(p, BUY, 3, 1000.0) # elif round == 2: # test_player_ulc(p, BUY, 2, 1612.0) # if p.str_id() == "PL02": # if round == 1: # test_player_llc(p, BUY, 6, 800.0) # test_player_ulc(p, BUY, 6, 700.0) # elif round == 2: # test_player_ulc(p, SELL, 2, 1612.0) # else: # test_player_llc(p, SELL, 1, 2000.0) # test_player_ulc(p, SELL, 4, 900.0) # if p.str_id() == "PL03": # if round == 1: # test_player_llc(p, BUY, 6, 300.0) # test_player_ulc(p, BUY, 2, 1000.0) # elif round == 2: # pass # else: # test_player_llc(p, SELL, 4, 1000.0) # test_player_ulc(p, BUY, 3, 10.0) return True @staticmethod def get_form_fields(player: Player): """Shows which fields will be in the form on the Auction page """ llc_fields = [ 'llc_choice', 'llc_price', 'llc_number', ] ulc_fields = [ 'ulc_choice', 'ulc_price', 'ulc_number', ] form_fields = [] if not player.group.llc_bankrupt: form_fields.extend(llc_fields) if not player.group.ulc_bankrupt: form_fields.extend(ulc_fields) return form_fields @staticmethod def vars_for_template(player: Player): """Passes the variables to the Auction page""" round = player.round_number # print(f'Auction.vars_for_template ({player.str_id()})') # print(f'{player.id_in_group=}') # print(f'{player.id_in_subsession=}') if round == 1: player.wallet = C.INITIAL_WALLET else: prev = player.in_round(round - 1) g = player.group prev_group = g.in_round(round - 1) # calculate waller at start # print("++++ TEST TEST TEST ++++") # print(f"{prev.llc_choice=}") # print(f"{SIGN[prev.llc_choice]=}") # print(f"{prev.llc_number_final=}") # print(f"{prev_group.llc_uniform_price=}") # print("++++ TEST TEST TEST ++++") # calculate the wallet at start wallet = calculate_wallet_at(player, start=True) player.wallet = wallet.total player.llc_shares = prev.llc_shares - prev.confiscated_shares player.ulc_shares = prev.ulc_shares # player.confiscated_shares = prev.confiscated_shares # load results of shares and bankrupt from previous round g.llc_shares = prev_group.llc_shares g.llc_bankrupt = prev_group.llc_bankrupt g.ulc_shares = prev_group.ulc_shares g.ulc_bankrupt = prev_group.ulc_bankrupt result = { "round": round, "wallet": f'{z(player.wallet)}' } return result @staticmethod def error_message(player: Player, values): # check that player does not spend more than available # if buying shares, check that amount is valid total_amount = 0 llc_amount = 0 ulc_amount = 0 group = player.group if (not group.llc_bankrupt) and values['llc_number'] is None: return "Specify a number of LLC shares" if (not group.llc_bankrupt) and values['llc_price'] is None: return "Specify a price of LLC shares" if (not group.ulc_bankrupt) and values['ulc_number'] is None: return "Specify a number of ULC shares" if (not group.ulc_bankrupt) and values['ulc_price'] is None: return "Specify a price of ULC shares" if (not group.llc_bankrupt) and values['llc_choice'] == BUY and values['llc_number'] > 0: llc_amount = values['llc_number'] * values['llc_price'] total_amount += llc_amount if (not group.ulc_bankrupt) and values['ulc_choice'] == BUY and values['ulc_number'] > 0: ulc_amount = values['ulc_number'] * values['ulc_price'] total_amount += ulc_amount if player.wallet < total_amount: return f'Not enough money to buy shares ($ {player.wallet:,f} < $ {llc_amount:,f} + $ {ulc_amount:,f})' # check that not selling more than available if (not group.llc_bankrupt) and values['llc_choice'] == SELL and values['llc_number'] > 0: if values['llc_number'] > player.llc_shares: return f'Cannot sell more LLC shares than available [{player.llc_shares}]' if (not group.ulc_bankrupt) and values['ulc_choice'] == SELL and values['ulc_number'] > 0: if values['ulc_number'] > player.ulc_shares: return f'Cannot sell more ULC shares than available [{player.ulc_shares}]' @staticmethod def before_next_page(player: Player, timeout_happened): # import time player.time = time.time() # if timetout, clear values if timeout_happened: player.llc_number = 0 player.ulc_number = 0 class ResultsWaitPage(WaitPage): """Wait for all players to play, and calculate shares """ after_all_players_arrive = calculate_round class Results(Page): timeout_seconds = C.RESULTS_TIMEOUT @staticmethod def vars_for_template(player: Player): """Passes the variables to the Results page""" round = player.round_number group = player.group show_transactions = False if player.llc_number_final or player.ulc_number_final: show_transactions = True show_profit = False if player.llc_profit or player.ulc_profit: show_profit = True # print(f'{player.str_id()}: {player.wallet=}') wallet = calculate_wallet_at(player, start=False) # add transactions to template signs = ["+", "-"] llc_sign = signs[player.llc_choice] ulc_sign = signs[player.ulc_choice] # calculate initial shares init_llc_shares = 0 init_ulc_shares = 0 if player.round_number != 1: init_llc_shares = player.llc_shares - SIGN[player.llc_choice] * player.llc_number_final init_ulc_shares = player.ulc_shares - SIGN[player.ulc_choice] * player.ulc_number_final llc_uniform = group.llc_uniform_price ulc_uniform = group.ulc_uniform_price if round == 1: llc_uniform = player.llc_price ulc_uniform = player.ulc_price # DIE = group.session.DIE return { "round": round, "die_sides": DIE.sides[:10], "die_sides2": DIE.sides[10:], "init_wallet": z(player.wallet), "show_transactions": show_transactions, "show_profit": show_profit, "maintenance": z(DIE.maintenance), "init_llc_shares": init_llc_shares, "llc_transaction": z(-wallet.llc_tx), "llc_sign": llc_sign, "llc_profit_str": z(player.llc_profit), "llc_die": group.llc_die + 1, "end_llc_shares": player.llc_shares - player.confiscated_shares, "init_ulc_shares": init_ulc_shares, "ulc_transaction": z(-wallet.ulc_tx), "ulc_sign": ulc_sign, "ulc_profit_str": z(player.ulc_profit), "ulc_profit_negative": player.ulc_profit < 0, "ulc_die": group.ulc_die + 1, "total_profit_str": z(player.llc_profit + player.ulc_profit), "ulc_confiscated_revenue": z(player.confiscated_revenue), "wallet": z(wallet.total), "llc_uniform": z(llc_uniform), "ulc_uniform": z(ulc_uniform), "llc_cars": DIE.sides[group.llc_die].cars, "ulc_cars": DIE.sides[group.ulc_die].cars, "llc_toll_revenue": z(DIE.sides[group.llc_die].toll_revenue), "ulc_toll_revenue": z(DIE.sides[group.ulc_die].toll_revenue), } class Payment(Page): @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS @staticmethod def vars_for_template(player: Player): """Passes the variables to the Payment page""" # recalculate the final wallet group = player.group llc_transaction = SIGN[player.llc_choice] * player.llc_number_final * group.llc_uniform_price ulc_transaction = SIGN[player.ulc_choice] * player.ulc_number_final * group.ulc_uniform_price final_wallet = player.wallet - llc_transaction - ulc_transaction final_wallet += player.llc_profit + player.ulc_profit player.payoff = final_wallet / (1900) result = { "wallet": z(final_wallet), "payoff": f'$ {player.payoff:,.2}', } return result page_sequence = [Welcome, Auction, ResultsWaitPage, Results, Payment] # Die Class and initialization DieSide = namedtuple("DieSide", "side cars toll_revenue") class Die(object): """Represents a side of a dice with a number of cars and a toll revenue""" def __init__(self, sides: List[DieSide], maintenance: float): self.sides = sides self.maintenance = maintenance def max_revenue(self) -> float: return self.sides[-1].toll_revenue def min_revenue(self) -> float: return self.sides[0].toll_revenue def mean_revenue(self) -> float: avg = 0.0 for side in self.sides: avg += side.toll_revenue return avg / len(self.sides) def get_die(filename: str) -> list[DieSide]: """Read a 20-sided die from a file, and converts it into a list of DieSide :rtype: list[DieSide] """ with open(os.path.join(C.NAME_IN_URL, f"{filename}.tsv"), 'r') as die_file: lines = die_file.read().splitlines() die = [] for i, line in enumerate(lines, start=1): # break the parts of the line parts = line.split("\t") # read the cars and revenue cars = int(parts[1]) toll_revenue = int(parts[2]) # append it to the list as a DieSide die.append(DieSide(i, cars, toll_revenue)) return die DIE = Die(get_die(C.DIE_NAME), MAINTENANCE[C.DIE_NAME])