refactor, improvements, documentation!

This commit is contained in:
Michael Pilosov 2022-11-26 19:22:48 -07:00
parent 73167f78f4
commit d08e362ecd
2 changed files with 253 additions and 65 deletions

74
app.py
View File

@ -8,8 +8,9 @@ from roulette import (
Strategy, Strategy,
Placement, Placement,
FEASIBLE_MOVES, FEASIBLE_MOVES,
expected,
) )
from random import choice, randint from random import choice, randint, seed
if __name__ == "__main__": if __name__ == "__main__":
@ -29,44 +30,33 @@ if __name__ == "__main__":
bet = interpret_bet("19-36", 14, bet) bet = interpret_bet("19-36", 14, bet)
# print(bet[21]) # 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("bond")
print(bet) print(bet)
print(expected(bet)) print(expected(bet))
print() print()
print("unknown") # print("unknown")
bet = init_bet() # bet = init_bet()
bet = interpret_bet("1-12", 15, bet) # bet = interpret_bet("1-12", 15, bet)
bet = interpret_bet("13-24", 15, bet) # bet = interpret_bet("13-24", 15, bet)
bet = interpret_bet("corner-26-27-29-30", 5, bet) # bet = interpret_bet("corner-26-27-29-30", 5, bet)
bet = interpret_bet("corner-32-33-35-36", 5, bet) # bet = interpret_bet("corner-32-33-35-36", 5, bet)
print(bet) # print(bet)
print(expected(bet)) # print(expected(bet))
print() # print()
print("singles") # print("singles")
bet = init_bet() # bet = init_bet()
bet = place_bet(bet, 21, 40) # bet = place_bet(bet, 21, 40)
# bet = place_bet(bet, 1, 1) # # bet = place_bet(bet, 1, 1)
print(expected(bet)) # print(expected(bet))
print() # print()
print("stupid") # print("stupid")
bet = init_bet() # bet = init_bet()
bet = interpret_bet("odd", 18, bet) # bet = interpret_bet("odd", 18, bet)
bet = interpret_bet("even", 18, bet) # bet = interpret_bet("even", 18, bet)
# bet = place_bet(bet, -1, 1) # # bet = place_bet(bet, -1, 1)
# bet = place_bet(bet, 0, 1) # # bet = place_bet(bet, 0, 1)
print(expected(bet)) # print(expected(bet))
# min_games = randint(1, 10) # min_games = randint(1, 10)
# print(min_games, Player(200, simulate_random_strategy(min_num_games=min_games, total_budget=200))) # 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) # print(bet)
# set a random seed
seed(42)
# generate players and print them out # generate players and print them out
players = generate_players(num_players=3, min_num_games=4, total_budget=200) 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, budget=200.0,
strategy=Strategy( strategy=Strategy(
budget=50, budget=50,
@ -106,13 +99,18 @@ if __name__ == "__main__":
], ],
), ),
) )
for p in players: for p in sorted(players):
print(p, "\n") print(p, "\n")
print("======================") print("======================")
print("SIMULATING GAMES") print("SIMULATING GAMES")
# simulate 10 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(p, "\n")
print(player.strategy.get_bet())
# use sum to add up a list of lists

View File

@ -2,6 +2,7 @@ from typing import List, Dict, Optional
from functools import reduce from functools import reduce
from dataclasses import dataclass, field from dataclasses import dataclass, field
from random import choice, randint from random import choice, randint
from statistics import stdev, mean
Bet = Dict[int, float] Bet = Dict[int, float]
@ -36,10 +37,28 @@ ALIASES = {
CHIP_VALUES = {0.25, 0.5, 1, 5, 10, 25, 50, 100} CHIP_VALUES = {0.25, 0.5, 1, 5, 10, 25, 50, 100}
def expectation(bet): def expected(bet) -> float:
odds = 0 """
pmnt = 0 Returns the expected value of a bet.
return odds * pmnt
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, # 38 numbers, 6 street bets, 2 half-bets,
@ -51,17 +70,60 @@ def expectation(bet):
def init_bet() -> 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)} D = {i: 0 for i in range(-1, 37)}
return D return D
def place_bet(bet: Bet, on: int, amount: float): 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 = bet.copy()
bet[on] += amount bet[on] += amount
return bet 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 ( assert (on in FEASIBLE_MOVES) or (
on in ALIASES on in ALIASES
), f"Bet `{on}` not understood. Choose from feasible moves:\n {FEASIBLE_MOVES}" ), 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. Defines a bet based on the number of chips and value of each chip.
Args: Attributes
num (int): number of chips ----------
amt (float): value of each chip num : int
on (str): bet type 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 num: int
@ -153,6 +217,17 @@ class Placement:
self.on in ALIASES self.on in ALIASES
), f"Bet `{self.on}` not understood. Choose from feasible moves:\n {FEASIBLE_MOVES}" ), 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 @property
def value(self): def value(self):
""" """
@ -160,7 +235,7 @@ class Placement:
""" """
return self.num * self.amt 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. 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. # 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)} 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 # 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( return reduce(
lambda bet, placement: combine_bets(bet, placement.place_bet()), placements, {} lambda bet, placement: combine_bets(bet, placement.place_bet()), placements, {}
) )
@ -181,6 +284,18 @@ def place_bets(placements):
@dataclass @dataclass
class Strategy: 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 budget: float = 200
placements: List[Placement] = field(default_factory=list) placements: List[Placement] = field(default_factory=list)
@ -195,10 +310,13 @@ class Strategy:
def generate_random(cls, budget) -> "Strategy": def generate_random(cls, budget) -> "Strategy":
placements = [] placements = []
initial_budget = budget 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]) amt = choice([v for v in CHIP_VALUES if v <= budget])
# guarantees the max bet cannot exceed 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 # select random bet type
# todo: consider if this is the logic you want... # todo: consider if this is the logic you want...
if randint(0, 1) == 0: if randint(0, 1) == 0:
@ -211,6 +329,8 @@ class Strategy:
placement = Placement(num, amt, on) placement = Placement(num, amt, on)
placements.append(placement) placements.append(placement)
budget -= placement.value budget -= placement.value
num_placements += 1
return Strategy(budget=initial_budget, placements=placements) return Strategy(budget=initial_budget, placements=placements)
def print_all(self) -> None: def print_all(self) -> None:
@ -223,8 +343,35 @@ class Strategy:
@dataclass @dataclass
class Player: 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 budget: float
strategy: Strategy 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): 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 # 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): def generate_players(num_players=10, min_num_games=1, total_budget=200) -> List[Player]:
return [ """
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( Player(
total_budget, budget=total_budget,
simulate_random_strategy( strategy=simulate_random_strategy(
min_num_games=min_num_games, total_budget=total_budget 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 # 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 # pick a random number
num = randint(-1, 36) num = randint(-1, 36)
# print("WINNER:", num) if verbose:
print("WINNER:", num)
# for each player, place their bets on the wheel # for each player, place their bets on the wheel
bets = [p.strategy.get_bet() for p in players] bets = [p.strategy.get_bet() for p in players]
# for each player, calculate their winnings # for each player, calculate their winnings
@ -264,17 +454,17 @@ def simulate_games(players, num_games=10):
for g in range(num_games): for g in range(num_games):
if not players: if not players:
break break
print(f"GAME {g}") # print(f"GAME {g}")
winnings = simulate_game(players) winnings = simulate_game(players)
new_losers = [] new_losers = []
for i, p in enumerate(players): for i, p in enumerate(players):
p.budget -= p.strategy.value p.wallet -= p.strategy.value
p.budget += winnings[ p.wallet += winnings[
i i
] # TODO: reinvestment logic goes here. maybe add "reinvest" as a player attribute? ] # TODO: reinvestment logic goes here. maybe add "reinvest" as a player attribute?
# if a player runs out of money to keep using their strategy, # 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 # 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) new_losers.append(p)
for l in new_losers: for l in new_losers:
players.remove(l) players.remove(l)