diff --git a/app.py b/app.py index 1c46729..cd08cc4 100644 --- a/app.py +++ b/app.py @@ -1,145 +1,57 @@ -from typing import List, Dict -from functools import reduce +from roulette import init_bet, place_bet, interpret_bet -FEASIBLE_MOVES = sorted({ - *[f"street-{i}" for i in range(1,14)], - *[f"col-{i}" for i in range(1,4)], - *[f"corner-{i}-{i+1}-{i+3}-{i+4}" for i in range(1,36) if (i - 1)%3 < 2], - *["1-12", "13-24", "25-36", "1-18", "19-36", "even", "odd", "red", "black"], -}) - -ALIASES = {"reds", "blacks", "evens", "odds", "first-half", "last-half", "second-half", "first-18", "last-18", "second-18"} - -def expectation(bet): - odds = 0 - pmnt = 0 - return odds * pmnt +if __name__ == "__main__": -# 38 numbers, 6 street bets, 2 half-bets, + bet = init_bet() + #bet = place_bet(bet, 21, 20) + print(bet[21]) + #bet = interpret_bet("red", 36, bet) + #bet = interpret_bet("25-36", 1, bet) + #bet = interpret_bet("street-1", 3, bet) + #bet = interpret_bet("street-10", 3, bet) + #bet = interpret_bet("col-1", 12, bet) + + # james bond + bet = place_bet(bet, 0, 1) + for n in range(13,19): + bet = place_bet(bet, n, 5) + bet = interpret_bet("19-36", 14, bet) -# payout grid based on bets placed. -# a street bet is the same as splitting the bet across all the numbers in the group. -# will use a function to distribute / interpret the bets, but it seems like we only need to track the numbers on the wheel. + #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 - -def init_bet() -> Dict[int, float]: - D = {i: 0 for i in range(-1, 37)} - return D - - -def place_bet(bet: Dict[str, float], on: int, amount: float): - bet = bet.copy() - bet[on] += amount - return bet - - -def interpret_bet(on="red", amount=0, bet=None): - assert (on in FEASIBLE_MOVES) or (on in ALIASES), f"Bet `{on}` not understood. Choose from feasible moves:\n {FEASIBLE_MOVES}" - if bet is None: - bet = init_bet() - else: - bet = bet.copy() - REDS = {1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, 36} - BLACKS = set(range(37)) - REDS - NUMS = {} - on = on.strip().replace(" ", "-") - div = 18 - if on in ("red", "reds"): - NUMS = REDS - if on in ("black", "blacks"): - NUMS = BLACKS - if on in ("odd", "odds"): - NUMS = {i for i in range(1,37) if i % 2 == 0} - if on in ("even", "evens"): - NUMS = {i for i in range(1,37) if i % 2} - if on in ("1-18", "first-18", "first-half"): - NUMS = set(range(1, 19)) - if on in ("19-36", "last-18", "last-half", "second-half", "second-18"): - NUMS = set(range(19, 37)) - if on in ("1-12", "13-24", "25-36"): - low, high = on.split("-") - NUMS = set(range(int(low), int(high)+1)) - div = 12 - if not NUMS: - other_bet = on.split("-") - if other_bet[0] == "street": - street = int(other_bet[1]) - 1 - assert street in list(range(13)) - NUMS = {i for i in range(street+1, street+4)} - div = 3 - elif other_bet[0] == "col": - col = int(other_bet[1]) - 1 - assert col in list(range(0,3)) - NUMS = {i for i in range(1, 37) if (i-1) % 3 == col} - div = 12 - elif other_bet[0] == "split": # TODO: validate choices - num_1, num_2 = int(other_bet[1]), int(other_bet[2]) - NUMS = {num_1, num_2} - div = 2 - elif other_bet[0] == "corner": # TODO: validate choices - num_1, num_2 = int(other_bet[1]), int(other_bet[2]) - num_3, num_4 = int(other_bet[3]), int(other_bet[4]) - NUMS = {num_1, num_2, num_3, num_4} - div = 4 - else: - raise ValueError("unsupported bet") - - bet = reduce(lambda bet, num: place_bet(bet, num, amount / div), NUMS, bet) - - return bet - - -bet = init_bet() -#bet = place_bet(bet, 21, 20) -print(bet[21]) -#bet = interpret_bet("red", 36, bet) -#bet = interpret_bet("25-36", 1, bet) -#bet = interpret_bet("street-1", 3, bet) -#bet = interpret_bet("street-10", 3, bet) -#bet = interpret_bet("col-1", 12, bet) - -# james bond -bet = place_bet(bet, 0, 1) -for n in range(13,19): - bet = place_bet(bet, n, 5) -bet = interpret_bet("19-36", 14, bet) - - -#print(bet[21]) -import numpy as np -def expected(bet) -> float: - bets = np.array(list(bet.values())) - cond_bets = bets[bets > 0] - amt = sum(bets) - payout = amt*36/38 - print(f"bet {amt:.2f} to win {payout:.2f}: {payout/amt:2.4f} with std {np.std(bets*36)} mean win of {36*np.mean(cond_bets)} {sum(bets>0)}/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("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)) diff --git a/roulette.py b/roulette.py new file mode 100644 index 0000000..55a873e --- /dev/null +++ b/roulette.py @@ -0,0 +1,280 @@ +from typing import List, Dict, Optional +from functools import reduce +from dataclasses import dataclass, field + +Bet = Dict[int, float] + +FEASIBLE_MOVES = sorted({ + *[f"street-{i}" for i in range(1,14)], + *[f"col-{i}" for i in range(1,4)], + *[f"corner-{i}-{i+1}-{i+3}-{i+4}" for i in range(1,33) if (i - 1)%3 < 2], + *["1-12", "13-24", "25-36", "1-18", "19-36", "even", "odd", "red", "black"], + *["triple-0", "triple-00"] +}) + +ALIASES = {"reds", "blacks", "evens", "odds", "first-half", "last-half", "second-half", "first-18", "last-18", "second-18"} + +CHIP_VALUES = { 0.25, 0.5, 1, 5, 10, 25, 50, 100} + +def expectation(bet): + odds = 0 + pmnt = 0 + return odds * pmnt + + +# 38 numbers, 6 street bets, 2 half-bets, + + +# payout grid based on bets placed. +# a street bet is the same as splitting the bet across all the numbers in the group. +# will use a function to distribute / interpret the bets, but it seems like we only need to track the numbers on the wheel. + + +def init_bet() -> Bet: + D = {i: 0 for i in range(-1, 37)} + return D + + +def place_bet(bet: Bet, on: int, amount: float): + bet = bet.copy() + bet[on] += amount + return bet + + +def interpret_bet(on="red", amount=0, bet=Optional[Bet]): + assert (on in FEASIBLE_MOVES) or (on in ALIASES), f"Bet `{on}` not understood. Choose from feasible moves:\n {FEASIBLE_MOVES}" + if bet is None: + bet = init_bet() + else: + bet = bet.copy() + REDS = {1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, 36} + BLACKS = set(range(37)) - REDS + NUMS = {} + on = on.strip().replace(" ", "-") + div = 18 + if on in ("red", "reds"): + NUMS = REDS + if on in ("black", "blacks"): + NUMS = BLACKS + if on in ("odd", "odds"): + NUMS = {i for i in range(1,37) if i % 2 == 0} + if on in ("even", "evens"): + NUMS = {i for i in range(1,37) if i % 2} + if on in ("1-18", "first-18", "first-half"): + NUMS = set(range(1, 19)) + if on in ("19-36", "last-18", "last-half", "second-half", "second-18"): + NUMS = set(range(19, 37)) + if on in ("1-12", "13-24", "25-36"): + low, high = on.split("-") + NUMS = set(range(int(low), int(high)+1)) + div = 12 + if on in ["triple-0", "triple-00"]: + NUMS = {0, 1, 2} if on == "triple-0" else {-1, 2, 3} + div = 3 + if not NUMS: + other_bet = on.split("-") + if other_bet[0] == "street": + street = int(other_bet[1]) - 1 + assert street in list(range(13)) + NUMS = {i for i in range(street+1, street+4)} + div = 3 + elif other_bet[0] == "col": + col = int(other_bet[1]) - 1 + assert col in list(range(0,3)) + NUMS = {i for i in range(1, 37) if (i-1) % 3 == col} + div = 12 + elif other_bet[0] == "split": # TODO: validate choices, for now we disallow these. + num_1, num_2 = int(other_bet[1]), int(other_bet[2]) + NUMS = {num_1, num_2} + div = 2 + elif other_bet[0] == "corner": + num_1, num_2 = int(other_bet[1]), int(other_bet[2]) + num_3, num_4 = int(other_bet[3]), int(other_bet[4]) + NUMS = {num_1, num_2, num_3, num_4} + div = 4 + else: + raise ValueError("unsupported bet") + + bet = reduce(lambda bet, num: place_bet(bet, num, amount / div), NUMS, bet) + + return bet + + +@dataclass +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 + + Returns: + Placement: an object representing the placement of a stack of chips on a particular bet type. + """ + num: int + amt: float + on: str + + def __post_init__(self): + assert (self.on in FEASIBLE_MOVES) or (self.on in ALIASES), f"Bet `{self.on}` not understood. Choose from feasible moves:\n {FEASIBLE_MOVES}" + + @property + def value(self): + """ + Returns the value of the bet. + """ + return self.num*self.amt + + def place_bet(self, bet=None): + """ + Places a bet on the wheel based on the bet type. + """ + return interpret_bet(self.on, self.num*self.amt, bet) + + +# 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): + 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): + return reduce(lambda bet, placement: combine_bets(bet, placement.place_bet()), placements, {}) + +# create a list of random Placements +from random import choice, randint +placements = [Placement(randint(1, 10), 1, choice(list(FEASIBLE_MOVES))) for _ in range(10)] + + + +# for a given budget, generate placements until you run out of money, where the value of each placement is the number of chips times the value of each chip. + + +@dataclass +class Strategy: + budget: float = 200 + placements: List[Placement] = field(default_factory=list) + + def __repr__(self) -> str: + return f"Strategy(budget={self.budget}, value={self.value}, placements={self.placements})" + + @property + def value(self): + return sum([p.value for p in self.placements]) + + @classmethod + def generate_random(cls, budget) -> "Strategy": + placements = [] + initial_budget = budget + while budget > 0: + amt = choice([v for v in CHIP_VALUES if v <= budget]) + # guarantees the max bet cannot exceed budget: + num = randint(1, budget // amt) + # select random bet type + on = choice(list(FEASIBLE_MOVES)) + placement = Placement(num, amt, on) + placements.append(placement) + budget -= placement.value + return Strategy(budget=initial_budget, placements=placements) + + def print_all(self) -> None: + for p in self.placements: + print(p) + + def get_bet(self): + return place_bets(self.placements) + +@dataclass +class Player: + budget: float + strategy: Strategy + +def simulate_random_strategy( + min_num_games = 1, + total_budget = 200 +): + strategy_budget = total_budget // min_num_games + return Strategy.generate_random(strategy_budget) + + +# strategy = Strategy.generate_random(50) + +# strategy.print_all() + + +# define the minimum number of games that you want players to play + +# print the total sum of all the placements +# print("SUM") +# print(sum([p.value for p in placements])) + +# # place the bets +# bet = place_bets(placements) + + +# print(bet) + +min_games = randint(1, 10) +print(min_games, Player(200, simulate_random_strategy(min_num_games=min_games, 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 [Player(total_budget, simulate_random_strategy(min_num_games=min_num_games, total_budget=total_budget)) for _ in range(num_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): + # pick a random number + num = randint(-1, 36) + # 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 + winnings = [36 * bet.get(num, 0) for bet in bets] + # for each player, calculate their expected winnings + return winnings + +# simulate multiple games, reducing each player's budget by the amount of their bet and adding the amount of their winnings +def simulate_games( + players, + num_games = 10 +): + losers = [] + for g in range(num_games): + if not players: + break + print(f"GAME {g}") + winnings = simulate_game(players) + new_losers = [] + for i, p in enumerate(players): + p.budget -= p.strategy.value + p.budget += winnings[i] + # 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: + new_losers.append(p) + for l in new_losers: + players.remove(l) + losers.extend(new_losers) + + return players + losers + + +# generate players and print them out +players = generate_players(num_players=3, min_num_games=4, total_budget=200) +for p in players: + print(p,'\n') + +print("======================") +print("SIMULATING GAMES") +# simulate 10 games +players = simulate_games(players, num_games=100) + +for p in players: + print(p,'\n')