Compare commits

...

15 Commits
v0.0.4 ... main

Author SHA1 Message Date
37f2dda0c3 remove unnecessary package 2023-04-14 16:11:23 -06:00
5ba5cedd02 fix release version
updates past this broke the app.
2023-04-14 15:58:36 -06:00
Michael Pilosov
d51382960a docstring improvements and some renamings 2022-11-28 18:56:57 -07:00
Michael Pilosov
5e776f2eac add some links and text 2022-11-27 11:32:12 -07:00
Michael Pilosov
0e744004c7 clean makefile 2022-11-27 03:44:42 -07:00
Michael Pilosov
da49513ac8 slight tweak 2022-11-27 03:14:52 -07:00
Michael Pilosov
8a400a3f30 different shuffle, quicker run 2022-11-27 03:09:22 -07:00
Michael Pilosov
ae65305d8d sky high values spotted 2022-11-27 03:03:29 -07:00
Michael Pilosov
69547c2b79 change dynamic to use decimals 2022-11-27 02:51:33 -07:00
Michael Pilosov
857338ba5f description cleanup and web example 2022-11-27 02:28:49 -07:00
Michael Pilosov
250649d439 better printout 2022-11-27 02:20:22 -07:00
Michael Pilosov
88c386d440 remove graphs 2022-11-27 01:45:31 -07:00
Michael Pilosov
6f3b00d266 print a graph 2022-11-27 01:41:02 -07:00
Michael Pilosov
6ccecba442 more games! 2022-11-27 01:28:01 -07:00
Michael Pilosov
d15b44340d whitespace 2022-11-27 01:24:17 -07:00
6 changed files with 228 additions and 86 deletions

View File

@ -18,8 +18,4 @@ test: # TODO: replace with real tests
serve: serve:
python -m http.server 1337 python -m http.server 1337
demo:
scp -P 10044 index.html git@mlden.com:/data/f/play.html
@echo "visit http://clfx.cc/play.html"
.PHONY: build publish clean lint test .PHONY: build publish clean lint test

View File

@ -23,27 +23,41 @@ pip install pyroulette
from pyroulette import generate_players, play_roulette from pyroulette import generate_players, play_roulette
players = generate_players( players = generate_players(
number_of_players=10, num_players=50,
minimum_number_of_games=10, min_num_games=min_games,
budget=100, total_budget=100
) )
for player in results: players = play_roulette(
print(player)
results = play_roulette(
players=players, players=players,
number_of_games=1000, games=1000,
) )
print("Results:")
for p in sorted(players, reverse=True):
print("\n", p)
print("Statistics")
# get the wallet values for all players as a list
wallets = [player.wallet for player in players]
# calculate some statistics
avg_wallet = sum(wallets) / len(wallets)
median_wallet = sorted(wallets)[len(wallets) // 2]
# calculate winnings
winnings = [p.wallet - p.budget for p in players]
num_losers = len([w for w in winnings if w <= 0])
num_winners = len([w for w in winnings if w > 0])
num_bankrupt = len([l for l in wallets if l == 0])
# print the results
print(f"Average wallet value: {avg_wallet}\n")
print(f"Median wallet value: {median_wallet}\n")
print(f"Number of players who lost money: {num_losers}, proportion: {num_losers / len(players):.2f}")
print(f"Number of players who went bankrupt: {num_bankrupt}, proportion: {num_bankrupt / len(players):.2f}")
print()
print(f"Number of players who won more than they started with: {num_winners}, proportion: {num_winners / len(players):.2f}")
``` ```
# uploading to pypi
```
pip install build
python -m build --sdist --wheel -n
twine upload dist/*
```

4
app.py
View File

@ -76,13 +76,13 @@ if __name__ == "__main__":
strategy = Strategy.generate_random(50) strategy = Strategy.generate_random(50)
strategy.print_all() strategy.print()
# define the minimum number of games that you want players to play # define the minimum number of games that you want players to play
# print the total sum of all the placements # print the total sum of all the placements
print("SUM") print("SUM")
print(sum([p.value for p in placements])) print(sum([p.cost for p in placements]))
# place the bets # place the bets
bet = Strategy.place_bets(placements) bet = Strategy.place_bets(placements)

View File

@ -7,9 +7,9 @@
<title>Roulette Demo</title> <title>Roulette Demo</title>
<!-- <link rel="icon" type="image/png" href="favicon.png" /> --> <!-- <link rel="icon" type="image/png" href="favicon.png" /> -->
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/releases/2022.09.1/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script> <script defer src="https://pyscript.net/releases/2022.09.1/pyscript.js"></script>
<link rel="stylesheet" href="./assets/css/examples.css" /> <link rel="stylesheet" href="./assets/css/examples.css" />
<link rel="stylesheet" href="./assets/prism/prism.css" /> <link rel="stylesheet" href="./assets/prism/prism.css" />
<script defer src="./assets/prism/prism.js"></script> <script defer src="./assets/prism/prism.js"></script>
@ -21,41 +21,81 @@
<!-- <a href="/"> <!-- <a href="/">
<img src="./logo.png" class="logo"> <img src="./logo.png" class="logo">
</a> --> </a> -->
<a class="title" href="" style="color: #f0ab3c;">Roulette</a> <a class="title" href="" style="color: #0d88c2;">Roulette</a>
<a href="https://git.clfx.cc/mm/roulette/src/branch/main/pyroulette/roulette.py" style="color: #ffffff;">(roulette.py)</a>
<a href="https://git.clfx.cc/mm/roulette/src/branch/main/index.html" style="color: #a6dc28;">(index.html)</a>
</div> </div>
</nav> </nav>
<section class="pyscript"> <section class="pyscript">
<div class="font-mono">Simulating 100 Games for 500 Players: <label id="outputDiv"></label></div> <div class="font-mono">
<h1 align="center">Let's Hack Roulette!</h1>
<h2 align="center">
Simulating 100 Games for 500 Players
</h2>
<p>
Note: winnings are re-invested, each player starts with $100 and chooses a strategy at random.
</p>
<label id="outputDiv"></label>
</div>
<center>
<div id="outputDiv2" class="font-mono"></div> <div id="outputDiv2" class="font-mono"></div>
</center>
<br>
<div id="outputDiv3" class="font-mono"></div> <div id="outputDiv3" class="font-mono"></div>
<br>
<py-config> <py-config>
packages = [ packages = [
"pyroulette", "pyroulette",
] ]
</py-config> </py-config>
<py-script> <py-script output="outputDiv2">
from pyroulette import * import pyroulette as pr
# seed(59) # seed(59)
from random import randint from random import random, randint
players = [] players = []
for _ in range(1, 20): # iterations * num_per_group = num_players
c = randint(0, 3) for _ in range(1, 50):
if c == 0: c = random()
if c < 0.25:
min_games = randint(1, 100) min_games = randint(1, 100)
elif c == 1: elif c < 0.5:
min_games = randint(1, 25) min_games = randint(1, 25)
else: else: # bold half
min_games = randint(1, 2) min_games = randint(1, 2)
players.extend(generate_players(num_players=25, min_num_games=min_games, total_budget=100)) # min_games = 100 # this caps out at about $250 wallet. need to bet bigger to win bigger.
players.extend(pr.generate_players(num_players=10, min_num_games=min_games, total_budget=100))
players = play_roulette(players, games=100) players = pr.play_roulette(players, games=100)
for p in sorted(players, reverse=True): # get the wallet values for all players as a list
print(p, "\n<br>") wallets = [player.wallet for player in players]
# calculate some statistics
avg_wallet = sum(wallets) / len(wallets)
median_wallet = sorted(wallets)[len(wallets) // 2]
# calculate winnings
winnings = [p.wallet - p.budget for p in players]
num_losers = len([w for w in winnings if w <= 0])
num_winners = len([w for w in winnings if w > 0])
num_bankrupt = len([l for l in wallets if l == 0])
# print the results
print(f"Average wallet value: {avg_wallet}\n")
print(f"Median wallet value: {median_wallet}\n")
print(f"Number of players who lost money: {num_losers}, proportion: {num_losers / len(players):.2f}")
print(f"Number of players who went bankrupt: {num_bankrupt}, proportion: {num_bankrupt / len(players):.2f}")
print()
print(f"Number of players who won more than they started with: {num_winners}, proportion: {num_winners / len(players):.2f}")
</py-script> </py-script>
<py-script output="outputDiv3">
for p in sorted(players, reverse=True):
print("\n<br>", p)
</py-script>
</section> </section>
</body> </body>

View File

@ -7,8 +7,14 @@ from random import choice, randint
from statistics import mean, stdev from statistics import mean, stdev
from typing import Dict, List, Optional from typing import Dict, List, Optional
"""Set[str]:
Bets on single numbers.
"""
SINGLE_BETS = {str(i) for i in range(-1, 37)} SINGLE_BETS = {str(i) for i in range(-1, 37)}
"""Set[str]:
The possible moves that can be made in a game of roulette.
"""
FEASIBLE_MOVES = sorted( FEASIBLE_MOVES = sorted(
{ {
*[f"street-{i}" for i in range(1, 14)], *[f"street-{i}" for i in range(1, 14)],
@ -20,7 +26,9 @@ FEASIBLE_MOVES = sorted(
} }
) )
"""Set[str]:
Aliases for different placements.
"""
ALIASES = { ALIASES = {
"reds", "reds",
"blacks", "blacks",
@ -34,7 +42,11 @@ ALIASES = {
"second-18", "second-18",
} }
CHIP_VALUES = {0.25, 0.5, 1, 5, 10, 25, 50, 100} CHIP_VALUES = {0.25, 0.5, 1, 5, 10, 25, 50, 100}
"""Set[float]:
The possible chip values that a player can place on a given placement.
"""
def init_spread() -> Dict[int, float]: def init_spread() -> Dict[int, float]:
@ -45,6 +57,7 @@ def init_spread() -> Dict[int, float]:
------- -------
Bet Bet
A dictionary representing the 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
@ -52,7 +65,19 @@ def init_spread() -> Dict[int, float]:
@dataclass @dataclass
class Bet: class Bet:
"""A class for representing a bet.""" """A class for representing a bet.
Parameters
----------
spread : Dict[int, float], optional
A dictionary representing the bet, by default init_spread()
Attributes
----------
spread : Dict[int, float]
A dictionary representing the bet.
"""
spread: Dict[int, float] = field(default_factory=init_spread) spread: Dict[int, float] = field(default_factory=init_spread)
@ -92,7 +117,7 @@ class Bet:
"""Return a copy of the bet.""" """Return a copy of the bet."""
return Bet(self.spread.copy()) return Bet(self.spread.copy())
def __iter__(self): def __iter__(self) -> iter:
return iter(self.spread.keys()) return iter(self.spread.keys())
def get(self, __name: int) -> float: def get(self, __name: int) -> float:
@ -101,8 +126,17 @@ class Bet:
@dataclass @dataclass
class Placement: 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.
Parameters
----------
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.
Attributes Attributes
---------- ----------
@ -137,24 +171,41 @@ class Placement:
return self.amt == other.amt and self.on == other.on return self.amt == other.amt and self.on == other.on
@property @property
def value(self): def cost(self) -> float:
""" """
Returns the value of the bet. Returns the value of the bet.
""" """
return self.num * self.amt return self.num * self.amt
def bet(self, bet=None) -> Bet: def bet(self, bet: Optional[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.
Parameters
----------
bet : Bet, optional
The bet to place, by default None
Returns
-------
Bet
The bet placed on the wheel.
""" """
return interpret_bet(self.on, self.num * self.amt, bet) return interpret_bet(self.on, self.num * self.amt, bet)
@dataclass @dataclass
class Strategy: class Strategy:
""" """Represents list of placements on the roulette wheel.
A strategy is a list of placements, each of which is a bet on a Each strategy is a bet on a particular number or group of numbers.
particular number or group of numbers.
Parameters
----------
budget : float
The amount of money to spend on the strategy.
placements : List[Placement]
A list of placements the player will make.
Attributes Attributes
---------- ----------
@ -171,17 +222,19 @@ class Strategy:
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
f"Strategy(budget={self.budget}," f"Strategy(budget={self.budget},"
+ f"value={self.value}, placements={self.placements})" + f"cost={self.cost}, placements={self.placements})"
) )
@property @property
def value(self): def cost(self) -> float:
return sum([p.value for p in self.placements]) """
Returns the total cost of the strategy.
"""
return sum([p.cost for p in self.placements])
@classmethod @classmethod
def generate_random(cls, budget: float, max_placements: int = 10) -> Strategy: def generate_random(cls, budget: float, max_placements: int = 10) -> Strategy:
""" """Generates a random strategy.
Generates a random strategy.
Parameters Parameters
---------- ----------
@ -218,25 +271,42 @@ class Strategy:
) # TODO: make a parameter, allow for just single bets. ) # TODO: make a parameter, allow for just single bets.
placement = Placement(num, amt, on) placement = Placement(num, amt, on)
placements.append(placement) placements.append(placement)
budget -= placement.value budget -= placement.cost
num_placements += 1 num_placements += 1
return Strategy(budget=initial_budget, placements=placements) return Strategy(budget=initial_budget, placements=placements)
def print_all(self) -> None: def print(self) -> None:
"""Prints all of the placements involved in the strategy."""
for p in self.placements: for p in self.placements:
print(p) print(p)
def get_bet(self): def get_bet(self) -> Bet:
"""Returns a bet object based on the strategy's placements.
Returns
-------
Bet
A bet object representing the individual spread \
on the inside of the table.
"""
return self.place_bets(self.placements) return self.place_bets(self.placements)
def get_placements(self): def get_placements(self) -> List[Placement]:
"""Returns the placements in the strategy.
Returns
-------
List[Placement]
A list of placements.
"""
return self.placements return self.placements
@staticmethod @staticmethod
def place_bets(placements: List[Placement]) -> Bet: def place_bets(placements: List[Placement]) -> Bet:
""" """Places a list of bets on the wheel given a list of Placements.
Places a list of bets on the wheel given a list of Placements.
Parameters Parameters
---------- ----------
@ -247,14 +317,26 @@ class Strategy:
------- -------
Bet Bet
A dictionary representing the bet. A dictionary representing the bet.
""" """
return sum([p.bet() for p in placements], Bet()) return sum([p.bet() for p in placements], Bet())
@dataclass @dataclass
class Player: class Player:
""" """A player of the game.
A player of the game.
Note: this dataclass is mutable.
Parameters
----------
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)
Attributes Attributes
---------- ----------
@ -267,7 +349,8 @@ class Player:
(default: random int of length 8) (default: random int of length 8)
wallet : float wallet : float
The amount of money the player has left. The amount of money the player has left.
(default: budget) (initialized to equal the budget)
""" """
budget: float budget: float
@ -285,18 +368,17 @@ class Player:
+ f"wallet={round(self.wallet, 2)}, " + f"wallet={round(self.wallet, 2)}, "
+ f"num_placements={len(self.strategy.placements)}, " + f"num_placements={len(self.strategy.placements)}, "
+ f"strategy_budget={round(self.strategy.budget, 2)}, " + f"strategy_budget={round(self.strategy.budget, 2)}, "
+ f"strategy_cost={round(self.strategy.value, 2)}" + f"strategy_cost={round(self.strategy.cost, 2)}"
+ f"\nstrategy:{_nl}{_nl.join(map(str, sorted(self.strategy.placements)))}" + f"\nstrategy:{_nl}{_nl.join(map(str, sorted(self.strategy.placements)))}"
+ "\n)" + "\n)"
) )
def __lt__(self, other): def __lt__(self, other: Player) -> bool:
return self.wallet < other.wallet return self.wallet < other.wallet
def expected(bet: Bet) -> float: def expected(bet: Bet) -> float: # todo: move into a stats module.
""" """Returns the expected value of a bet.
Returns the expected value of a bet.
Parameters Parameters
---------- ----------
@ -307,6 +389,7 @@ def expected(bet: Bet) -> float:
------- -------
float float
The expected value of the bet. The expected value of the bet.
""" """
bets = list(bet.spread.values()) bets = list(bet.spread.values())
cond_bets = filter(lambda x: x > 0, bets) cond_bets = filter(lambda x: x > 0, bets)
@ -321,8 +404,9 @@ def expected(bet: Bet) -> float:
def place_bet(bet: Bet, on: int, amount: float) -> Bet: def place_bet(bet: Bet, on: int, amount: float) -> Bet:
""" """Places a bet on a number.
Places a bet on a number.
Note: this function creates a copy of the Bet so as not to mutate the original.
Parameters Parameters
---------- ----------
@ -337,15 +421,15 @@ def place_bet(bet: Bet, on: int, amount: float) -> Bet:
------- -------
Bet Bet
A dictionary representing the bet with the new bet placed. 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]) -> Bet: def interpret_bet(on: str = "red", amount: float = 0, bet: Optional[Bet] = None) -> Bet:
""" """Interprets a bet and returns a dictionary representing the bet.
Interprets a bet and returns a dictionary representing the bet.
Parameters Parameters
---------- ----------
@ -361,6 +445,7 @@ def interpret_bet(on="red", amount=0, bet=Optional[Bet]) -> Bet:
------- -------
Bet Bet
A dictionary representing the bet. A dictionary representing the bet.
""" """
assert (on in FEASIBLE_MOVES) or ( assert (on in FEASIBLE_MOVES) or (
on in ALIASES on in ALIASES
@ -431,9 +516,11 @@ def interpret_bet(on="red", amount=0, bet=Optional[Bet]) -> Bet:
return bet return bet
def simulate_random_strategy(min_num_games=1, total_budget=200) -> Strategy: def simulate_random_strategy(
""" min_num_games: int = 1, total_budget: float = 200
Simulates a random strategy based on the minimum number of games that ) -> Strategy:
"""Simulates a random strategy for playing roulette.
The strategy is based on the minimum number of games that
the player wants to play and the total budget that the player has. the player wants to play and the total budget that the player has.
Parameters Parameters
@ -447,15 +534,18 @@ def simulate_random_strategy(min_num_games=1, total_budget=200) -> Strategy:
Returns Returns
------- -------
Strategy
The random strategy that the player will follow.
""" """
strategy_budget = total_budget // min_num_games strategy_budget = total_budget // min_num_games
return Strategy.generate_random(strategy_budget) return Strategy.generate_random(strategy_budget)
def generate_players(num_players=10, min_num_games=1, total_budget=200) -> List[Player]: def generate_players(
""" num_players: int = 10, min_num_games: int = 1, total_budget: float = 200
Generates a list of players with random strategies. ) -> List[Player]:
"""Generates a list of players with random strategies.
Parameters Parameters
---------- ----------
@ -470,6 +560,8 @@ def generate_players(num_players=10, min_num_games=1, total_budget=200) -> List[
Returns Returns
------- -------
List[Player] List[Player]
A list of players with random strategies.
""" """
players = [ players = [
Player( Player(
@ -495,8 +587,7 @@ def generate_players(num_players=10, min_num_games=1, total_budget=200) -> List[
def spin_wheel(players, verbose=False) -> List[float]: def spin_wheel(players, verbose=False) -> List[float]:
""" """Simulates a single game of roulette.
Simulates a single game of roulette.
Parameters Parameters
---------- ----------
@ -508,6 +599,7 @@ def spin_wheel(players, verbose=False) -> List[float]:
Returns Returns
------- -------
List[float] List[float]
The amount of money each player won.
""" """
# pick a random number # pick a random number
@ -523,8 +615,7 @@ def spin_wheel(players, verbose=False) -> List[float]:
def play_roulette(players: List[Player], games: int = 10) -> List[Player]: def play_roulette(players: List[Player], games: int = 10) -> List[Player]:
""" """Simulates playing multiple games of roulette.
Simulates playing multiple games of roulette.
Parameters Parameters
---------- ----------
@ -537,6 +628,7 @@ def play_roulette(players: List[Player], games: int = 10) -> List[Player]:
------- -------
List[Player] List[Player]
The players after playing the games. The players after playing the games.
""" """
losers = [] losers = []
for g in range(games): for g in range(games):
@ -546,13 +638,13 @@ def play_roulette(players: List[Player], games: int = 10) -> List[Player]:
winnings = spin_wheel(players) winnings = spin_wheel(players)
new_losers = [] new_losers = []
for i, p in enumerate(players): for i, p in enumerate(players):
p.wallet -= p.strategy.value p.wallet -= p.strategy.cost
p.wallet += winnings[i] p.wallet += winnings[i]
# TODO: reinvestment logic goes here. # TODO: reinvestment logic goes here.
# maybe add "reinvest" as a player attribute? # 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 losers. # remove them from the list of players and add them to losers.
if p.wallet < p.strategy.value: if p.wallet < p.strategy.cost:
new_losers.append(p) new_losers.append(p)
for losing_player in new_losers: for losing_player in new_losers:
players.remove(losing_player) players.remove(losing_player)

View File

@ -10,7 +10,7 @@ with open(BASEDIR.joinpath("README.md"), "r") as fp:
setup( setup(
name="pyroulette", name="pyroulette",
version="0.0.4", version="0.0.5",
description="A package for exploring roulette strategies.", description="A package for exploring roulette strategies.",
long_description=LONG_DESCRIPTION, long_description=LONG_DESCRIPTION,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",