From d08e362ecd42a4f799ee5e06442a754e09a37521 Mon Sep 17 00:00:00 2001 From: Michael Pilosov Date: Sat, 26 Nov 2022 19:22:48 -0700 Subject: [PATCH] refactor, improvements, documentation! --- app.py | 74 ++++++++-------- roulette.py | 244 ++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 253 insertions(+), 65 deletions(-) diff --git a/app.py b/app.py index 0a7ceb1..e6ea63c 100644 --- a/app.py +++ b/app.py @@ -8,8 +8,9 @@ from roulette import ( Strategy, Placement, FEASIBLE_MOVES, + expected, ) -from random import choice, randint +from random import choice, randint, seed if __name__ == "__main__": @@ -29,44 +30,33 @@ if __name__ == "__main__": bet = interpret_bet("19-36", 14, bet) # print(bet[21]) - from statistics import stdev, mean - - def expected(bet) -> float: - bets = list(bet.values()) - cond_bets = filter(lambda x: x > 0, bets) - amt = sum(bets) - payout = amt * 36 / 38 - print( - f"bet: {amt:.2f}, expected: {payout:.2f}: {payout/amt:2.4f} with std {stdev(bets*36)} mean win of {36*mean(cond_bets)} {sum(filter(lambda x: x > 0, bets))}/38 times." - ) - return payout print("bond") print(bet) print(expected(bet)) print() - print("unknown") - bet = init_bet() - bet = interpret_bet("1-12", 15, bet) - bet = interpret_bet("13-24", 15, bet) - bet = interpret_bet("corner-26-27-29-30", 5, bet) - bet = interpret_bet("corner-32-33-35-36", 5, bet) - print(bet) - print(expected(bet)) - print() - print("singles") - bet = init_bet() - bet = place_bet(bet, 21, 40) - # bet = place_bet(bet, 1, 1) - print(expected(bet)) - print() - print("stupid") - bet = init_bet() - bet = interpret_bet("odd", 18, bet) - bet = interpret_bet("even", 18, bet) - # bet = place_bet(bet, -1, 1) - # bet = place_bet(bet, 0, 1) - print(expected(bet)) + # print("unknown") + # bet = init_bet() + # bet = interpret_bet("1-12", 15, bet) + # bet = interpret_bet("13-24", 15, bet) + # bet = interpret_bet("corner-26-27-29-30", 5, bet) + # bet = interpret_bet("corner-32-33-35-36", 5, bet) + # print(bet) + # print(expected(bet)) + # print() + # print("singles") + # bet = init_bet() + # bet = place_bet(bet, 21, 40) + # # bet = place_bet(bet, 1, 1) + # print(expected(bet)) + # print() + # print("stupid") + # bet = init_bet() + # bet = interpret_bet("odd", 18, bet) + # bet = interpret_bet("even", 18, bet) + # # bet = place_bet(bet, -1, 1) + # # bet = place_bet(bet, 0, 1) + # print(expected(bet)) # min_games = randint(1, 10) # print(min_games, Player(200, simulate_random_strategy(min_num_games=min_games, total_budget=200))) @@ -91,9 +81,12 @@ if __name__ == "__main__": # print(bet) + # set a random seed + seed(42) # generate players and print them out players = generate_players(num_players=3, min_num_games=4, total_budget=200) - players[0] = Player( + players[0] = player = Player( + id=0, budget=200.0, strategy=Strategy( budget=50, @@ -106,13 +99,18 @@ if __name__ == "__main__": ], ), ) - for p in players: + for p in sorted(players): print(p, "\n") print("======================") print("SIMULATING GAMES") # simulate 10 games - players = simulate_games(players, num_games=100) + # seed(59) + players = simulate_games(players, num_games=100000) - for p in players: + for p in sorted(players): print(p, "\n") + + print(player.strategy.get_bet()) + + # use sum to add up a list of lists diff --git a/roulette.py b/roulette.py index 7e4d392..de55fce 100644 --- a/roulette.py +++ b/roulette.py @@ -2,6 +2,7 @@ from typing import List, Dict, Optional from functools import reduce from dataclasses import dataclass, field from random import choice, randint +from statistics import stdev, mean Bet = Dict[int, float] @@ -36,10 +37,28 @@ ALIASES = { CHIP_VALUES = {0.25, 0.5, 1, 5, 10, 25, 50, 100} -def expectation(bet): - odds = 0 - pmnt = 0 - return odds * pmnt +def expected(bet) -> float: + """ + Returns the expected value of a bet. + + Parameters + ---------- + bet : Bet + The bet to calculate the expected value of. + + Returns + ------- + float + The expected value of the bet. + """ + bets = list(bet.values()) + cond_bets = filter(lambda x: x > 0, bets) + amt = sum(bets) + payout = amt * 36 / 38 + print( + f"bet: {amt:.2f}, expected: {payout:.2f}: {payout/amt:2.4f} with std {stdev(bets*36)} mean win of {36*mean(cond_bets)} {sum(filter(lambda x: x > 0, bets))}/38 times." + ) + return payout # 38 numbers, 6 street bets, 2 half-bets, @@ -51,17 +70,60 @@ def expectation(bet): def init_bet() -> Bet: + """ + Initializes a bet with all individual placements set to 0. + + Returns + ------- + Bet + A dictionary representing the bet. + """ D = {i: 0 for i in range(-1, 37)} return D def place_bet(bet: Bet, on: int, amount: float): + """ + Places a bet on a number. + + Parameters + ---------- + bet : Bet + The bet to place. + on : int + The number to bet on. + amount : float + The amount to bet. + + Returns + ------- + Bet + A dictionary representing the bet with the new bet placed. + """ bet = bet.copy() bet[on] += amount return bet -def interpret_bet(on="red", amount=0, bet=Optional[Bet]): +def interpret_bet(on="red", amount=0, bet=Optional[Bet]) -> Bet: + """ + Interprets a bet and returns a dictionary representing the bet. + + Parameters + ---------- + on : str + The type of bet to place. + amount : float + The amount to bet. + bet : Bet + The bet to add to. + (default is None, which creates a new bet) + + Returns + ------- + Bet + A dictionary representing the bet. + """ assert (on in FEASIBLE_MOVES) or ( on in ALIASES ), f"Bet `{on}` not understood. Choose from feasible moves:\n {FEASIBLE_MOVES}" @@ -135,13 +197,15 @@ class Placement: """ Defines a bet based on the number of chips and value of each chip. - Args: - num (int): number of chips - amt (float): value of each chip - on (str): bet type + Attributes + ---------- + num : int + The number of chips to bet. + amt : float + The value of each chip. + on : str + The type of bet to place for which the chips are being used. - Returns: - Placement: an object representing the placement of a stack of chips on a particular bet type. """ num: int @@ -153,6 +217,17 @@ class Placement: self.on in ALIASES ), f"Bet `{self.on}` not understood. Choose from feasible moves:\n {FEASIBLE_MOVES}" + def __gt__(self, other): + return self.amt > other.amt + + def __add__(self, other): + assert self.on == other.on, "Cannot add placements of different types." + assert self.amt == other.amt, "Cannot add placements of different values." + return Placement(self.num + other.num, self.amt, self.on) + + def __eq__(self, other): + return self.amt == other.amt and self.on == other.on + @property def value(self): """ @@ -160,7 +235,7 @@ class Placement: """ return self.num * self.amt - def place_bet(self, bet=None): + def place_bet(self, bet=None) -> Bet: """ Places a bet on the wheel based on the bet type. """ @@ -168,12 +243,40 @@ class Placement: # for two bets of structure Dict[int, float], iterate through all the keys and add up the values, returning a new dict. -def combine_bets(bet_1, bet_2): +def combine_bets(bet_1: Bet, bet_2: Bet) -> Bet: + """ + Combines two bets into a single bet. + + Parameters + ---------- + bet_1 : Bet + The first bet to combine. + bet_2 : Bet + The second bet to combine. + + Returns + ------- + Bet + The combined bet. + """ return {k: bet_1.get(k, 0) + bet_2.get(k, 0) for k in set(bet_1) | set(bet_2)} # for a list of Placements, call the place_bet method on each one and combine the results using reduce and combine_bets, starting with an empty dictionary as the initial argument -def place_bets(placements): +def place_bets(placements: List[Placement]) -> Bet: + """ + Places a list of bets on the wheel given a list of Placements. + + Parameters + ---------- + placements : List[Placement] + A list of Placements to place on the wheel. + + Returns + ------- + Bet + A dictionary representing the bet. + """ return reduce( lambda bet, placement: combine_bets(bet, placement.place_bet()), placements, {} ) @@ -181,6 +284,18 @@ def place_bets(placements): @dataclass class Strategy: + """ + A strategy is a list of placements, each of which is a bet on a particular number or group of numbers. + + Attributes + ---------- + budget : float + The amount of money to spend on the strategy. + placements : List[Placement] + A list of placements, each of which is a bet on a particular number or group of numbers. + + """ + budget: float = 200 placements: List[Placement] = field(default_factory=list) @@ -195,10 +310,13 @@ class Strategy: def generate_random(cls, budget) -> "Strategy": placements = [] initial_budget = budget - while budget > 0: + num_placements = 0 + max_placements = 10 + while (budget > 0) and (num_placements < max_placements): amt = choice([v for v in CHIP_VALUES if v <= budget]) # guarantees the max bet cannot exceed budget: - num = randint(1, budget // amt) + # 4 is the max number of chips because after that you might as well use a higher chip value. + num = randint(1, min(budget // amt, 4)) # select random bet type # todo: consider if this is the logic you want... if randint(0, 1) == 0: @@ -211,6 +329,8 @@ class Strategy: placement = Placement(num, amt, on) placements.append(placement) budget -= placement.value + num_placements += 1 + return Strategy(budget=initial_budget, placements=placements) def print_all(self) -> None: @@ -223,8 +343,35 @@ class Strategy: @dataclass class Player: + """ + A player of the game. + + Attributes + ---------- + budget : float + The amount of money the player starts with. + strategy : Strategy + The strategy the player uses to place bets. + id: int + The id of the player. + (default: random int of length 8) + wallet : float + The amount of money the player has left. + (default: budget) + """ + budget: float strategy: Strategy + id: int = field(default_factory=lambda: randint(1e8, 1e9 - 1)) + + def __post_init__(self): + self.wallet: float = self.budget + + def __repr__(self) -> str: + return f"Player(id={self.id}, budget={self.budget}, wallet={self.wallet}, strategy={sorted(self.strategy.placements)}, strategy_cost={self.strategy.value}, strategy_budget={self.strategy.budget}, num_placements={len(self.strategy.placements)}" + + def __lt__(self, other): + return self.id < other.id def simulate_random_strategy(min_num_games=1, total_budget=200): @@ -233,23 +380,66 @@ def simulate_random_strategy(min_num_games=1, total_budget=200): # given BUDGET, generate a bunch of random players, each with a random strategy, and return a list of players -def generate_players(num_players=10, min_num_games=1, total_budget=200): - return [ +def generate_players(num_players=10, min_num_games=1, total_budget=200) -> List[Player]: + """ + Generates a list of players with random strategies. + + Parameters + ---------- + num_players : int + The number of players to generate. + min_num_games : int + The minimum number of games each player will play using their strategy and budget. + total_budget : float + The total budget for each player. + + Returns + ------- + List[Player] + """ + players = [ Player( - total_budget, - simulate_random_strategy( + budget=total_budget, + strategy=simulate_random_strategy( min_num_games=min_num_games, total_budget=total_budget ), ) - for _ in range(num_players) + for i in range(num_players) ] + # if a player has placements with identical amt and on values, combine them into a single placement + for player in players: + placements = [] + for placement in player.strategy.placements: + if placement in placements: + placements[placements.index(placement)] += placement + else: + placements.append(placement) + player.strategy.placements = placements + return players + # simulate a game of roulette, picking a random integer from -1 to 37, taking the players as inputs and returning their expected winnings -def simulate_game(players): +def simulate_game(players, verbose=False) -> List[float]: + """ + Simulates a single game of roulette. + + Parameters + ---------- + players : List[Player] + The players in the game. + verbose : bool + Whether to print the winning number. + + Returns + ------- + List[float] + + """ # pick a random number num = randint(-1, 36) - # print("WINNER:", num) + if verbose: + print("WINNER:", num) # for each player, place their bets on the wheel bets = [p.strategy.get_bet() for p in players] # for each player, calculate their winnings @@ -264,17 +454,17 @@ def simulate_games(players, num_games=10): for g in range(num_games): if not players: break - print(f"GAME {g}") + # print(f"GAME {g}") winnings = simulate_game(players) new_losers = [] for i, p in enumerate(players): - p.budget -= p.strategy.value - p.budget += winnings[ + p.wallet -= p.strategy.value + p.wallet += winnings[ i ] # TODO: reinvestment logic goes here. maybe add "reinvest" as a player attribute? # if a player runs out of money to keep using their strategy, # remove them from the list of players and add them to the list of losers - if p.budget < p.strategy.value: + if p.wallet < p.strategy.value: new_losers.append(p) for l in new_losers: players.remove(l)