Compare commits

..

No commits in common. "main" and "v0.0.2" have entirely different histories.
main ... v0.0.2

6 changed files with 72 additions and 285 deletions

View File

@ -15,7 +15,4 @@ lint:
test: # TODO: replace with real tests
python app.py && python strategy.py
serve:
python -m http.server 1337
.PHONY: build publish clean lint test
.PHONY: build publish clean lint test

View File

@ -23,41 +23,27 @@ pip install pyroulette
from pyroulette import generate_players, play_roulette
players = generate_players(
num_players=50,
min_num_games=min_games,
total_budget=100
number_of_players=10,
minimum_number_of_games=10,
budget=100,
)
players = play_roulette(
for player in results:
print(player)
results = play_roulette(
players=players,
games=1000,
number_of_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/*
```

7
app.py
View File

@ -76,13 +76,13 @@ if __name__ == "__main__":
strategy = Strategy.generate_random(50)
strategy.print()
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.cost for p in placements]))
print(sum([p.value for p in placements]))
# place the bets
bet = Strategy.place_bets(placements)
@ -112,8 +112,9 @@ if __name__ == "__main__":
print("======================")
print("SIMULATING GAMES")
# simulate 10 games
# seed(59)
players = play_roulette(players, games=1000)
players = play_roulette(players, games=100000)
for p in sorted(players):
print(p, "\n")

View File

@ -1,103 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Roulette Demo</title>
<!-- <link rel="icon" type="image/png" href="favicon.png" /> -->
<link rel="stylesheet" href="https://pyscript.net/releases/2022.09.1/pyscript.css" />
<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/prism/prism.css" />
<script defer src="./assets/prism/prism.js"></script>
</head>
<body>
<nav class="navbar" style="background-color: #000000;">
<div class="app-header">
<!-- <a href="/">
<img src="./logo.png" class="logo">
</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>
</nav>
<section class="pyscript">
<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>
</center>
<br>
<div id="outputDiv3" class="font-mono"></div>
<br>
<py-config>
packages = [
"pyroulette",
]
</py-config>
<py-script output="outputDiv2">
import pyroulette as pr
# seed(59)
from random import random, randint
players = []
# iterations * num_per_group = num_players
for _ in range(1, 50):
c = random()
if c < 0.25:
min_games = randint(1, 100)
elif c < 0.5:
min_games = randint(1, 25)
else: # bold half
min_games = randint(1, 2)
# 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 = pr.play_roulette(players, games=100)
# 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}")
</py-script>
<py-script output="outputDiv3">
for p in sorted(players, reverse=True):
print("\n<br>", p)
</py-script>
</section>
</body>
</html>

View File

@ -7,14 +7,8 @@ from random import choice, randint
from statistics import mean, stdev
from typing import Dict, List, Optional
"""Set[str]:
Bets on single numbers.
"""
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(
{
*[f"street-{i}" for i in range(1, 14)],
@ -26,9 +20,7 @@ FEASIBLE_MOVES = sorted(
}
)
"""Set[str]:
Aliases for different placements.
"""
ALIASES = {
"reds",
"blacks",
@ -42,11 +34,7 @@ ALIASES = {
"second-18",
}
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]:
@ -57,7 +45,6 @@ def init_spread() -> Dict[int, float]:
-------
Bet
A dictionary representing the bet.
"""
D = {i: 0 for i in range(-1, 37)}
return D
@ -65,19 +52,7 @@ def init_spread() -> Dict[int, float]:
@dataclass
class 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.
"""
"""A class for representing a bet."""
spread: Dict[int, float] = field(default_factory=init_spread)
@ -117,7 +92,7 @@ class Bet:
"""Return a copy of the bet."""
return Bet(self.spread.copy())
def __iter__(self) -> iter:
def __iter__(self):
return iter(self.spread.keys())
def get(self, __name: int) -> float:
@ -126,17 +101,8 @@ class Bet:
@dataclass
class Placement:
"""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.
"""
Defines a bet based on the number of chips and value of each chip.
Attributes
----------
@ -171,41 +137,24 @@ class Placement:
return self.amt == other.amt and self.on == other.on
@property
def cost(self) -> float:
def value(self):
"""
Returns the value of the bet.
"""
return self.num * self.amt
def bet(self, bet: Optional[Bet] = None) -> Bet:
def bet(self, bet=None) -> Bet:
"""
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)
@dataclass
class Strategy:
"""Represents list of placements on the roulette wheel.
Each strategy is a bet on a 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.
"""
A strategy is a list of placements, each of which is a bet on a
particular number or group of numbers.
Attributes
----------
@ -222,19 +171,17 @@ class Strategy:
def __repr__(self) -> str:
return (
f"Strategy(budget={self.budget},"
+ f"cost={self.cost}, placements={self.placements})"
+ f"value={self.value}, placements={self.placements})"
)
@property
def cost(self) -> float:
"""
Returns the total cost of the strategy.
"""
return sum([p.cost for p in self.placements])
def value(self):
return sum([p.value for p in self.placements])
@classmethod
def generate_random(cls, budget: float, max_placements: int = 10) -> Strategy:
"""Generates a random strategy.
"""
Generates a random strategy.
Parameters
----------
@ -271,42 +218,25 @@ class Strategy:
) # TODO: make a parameter, allow for just single bets.
placement = Placement(num, amt, on)
placements.append(placement)
budget -= placement.cost
budget -= placement.value
num_placements += 1
return Strategy(budget=initial_budget, placements=placements)
def print(self) -> None:
"""Prints all of the placements involved in the strategy."""
def print_all(self) -> None:
for p in self.placements:
print(p)
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.
"""
def get_bet(self):
return self.place_bets(self.placements)
def get_placements(self) -> List[Placement]:
"""Returns the placements in the strategy.
Returns
-------
List[Placement]
A list of placements.
"""
def get_placements(self):
return self.placements
@staticmethod
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
----------
@ -317,26 +247,14 @@ class Strategy:
-------
Bet
A dictionary representing the bet.
"""
return sum([p.bet() for p in placements], Bet())
@dataclass
class Player:
"""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)
"""
A player of the game.
Attributes
----------
@ -349,8 +267,7 @@ class Player:
(default: random int of length 8)
wallet : float
The amount of money the player has left.
(initialized to equal the budget)
(default: budget)
"""
budget: float
@ -363,22 +280,21 @@ class Player:
def __repr__(self) -> str:
_nl = "\n\t" # custom newline character
return (
f"Player(id={self.id}, "
+ f"budget={round(self.budget, 2)}, "
+ f"wallet={round(self.wallet, 2)}, "
f"Player(id={self.id}, budget={self.budget}, wallet={self.wallet},"
+ f"num_placements={len(self.strategy.placements)}, "
+ f"strategy_budget={round(self.strategy.budget, 2)}, "
+ f"strategy_cost={round(self.strategy.cost, 2)}"
+ f"strategy_budget={self.strategy.budget}, "
+ f"strategy_cost={self.strategy.value}"
+ f"\nstrategy:{_nl}{_nl.join(map(str, sorted(self.strategy.placements)))}"
+ "\n)"
)
def __lt__(self, other: Player) -> bool:
return self.wallet < other.wallet
def __lt__(self, other):
return self.id < other.id
def expected(bet: Bet) -> float: # todo: move into a stats module.
"""Returns the expected value of a bet.
def expected(bet: Bet) -> float:
"""
Returns the expected value of a bet.
Parameters
----------
@ -389,7 +305,6 @@ def expected(bet: Bet) -> float: # todo: move into a stats module.
-------
float
The expected value of the bet.
"""
bets = list(bet.spread.values())
cond_bets = filter(lambda x: x > 0, bets)
@ -404,9 +319,8 @@ def expected(bet: Bet) -> float: # todo: move into a stats module.
def place_bet(bet: Bet, on: int, amount: float) -> Bet:
"""Places a bet on a number.
Note: this function creates a copy of the Bet so as not to mutate the original.
"""
Places a bet on a number.
Parameters
----------
@ -421,15 +335,15 @@ def place_bet(bet: Bet, on: int, amount: float) -> Bet:
-------
Bet
A dictionary representing the bet with the new bet placed.
"""
bet = bet.copy()
bet[on] += amount
return 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.
def interpret_bet(on="red", amount=0, bet=Optional[Bet]) -> Bet:
"""
Interprets a bet and returns a dictionary representing the bet.
Parameters
----------
@ -445,7 +359,6 @@ def interpret_bet(on: str = "red", amount: float = 0, bet: Optional[Bet] = None)
-------
Bet
A dictionary representing the bet.
"""
assert (on in FEASIBLE_MOVES) or (
on in ALIASES
@ -516,11 +429,9 @@ def interpret_bet(on: str = "red", amount: float = 0, bet: Optional[Bet] = None)
return bet
def simulate_random_strategy(
min_num_games: int = 1, total_budget: float = 200
) -> Strategy:
"""Simulates a random strategy for playing roulette.
The strategy is based on the minimum number of games that
def simulate_random_strategy(min_num_games=1, total_budget=200) -> Strategy:
"""
Simulates a random strategy based on the minimum number of games that
the player wants to play and the total budget that the player has.
Parameters
@ -534,18 +445,15 @@ def simulate_random_strategy(
Returns
-------
Strategy
The random strategy that the player will follow.
"""
strategy_budget = total_budget // min_num_games
return Strategy.generate_random(strategy_budget)
def generate_players(
num_players: int = 10, min_num_games: int = 1, total_budget: float = 200
) -> List[Player]:
"""Generates a list of players with random strategies.
def generate_players(num_players=10, min_num_games=1, total_budget=200) -> List[Player]:
"""
Generates a list of players with random strategies.
Parameters
----------
@ -560,8 +468,6 @@ def generate_players(
Returns
-------
List[Player]
A list of players with random strategies.
"""
players = [
Player(
@ -587,7 +493,8 @@ def generate_players(
def spin_wheel(players, verbose=False) -> List[float]:
"""Simulates a single game of roulette.
"""
Simulates a single game of roulette.
Parameters
----------
@ -599,7 +506,6 @@ def spin_wheel(players, verbose=False) -> List[float]:
Returns
-------
List[float]
The amount of money each player won.
"""
# pick a random number
@ -615,7 +521,8 @@ def spin_wheel(players, verbose=False) -> List[float]:
def play_roulette(players: List[Player], games: int = 10) -> List[Player]:
"""Simulates playing multiple games of roulette.
"""
Simulates playing multiple games of roulette.
Parameters
----------
@ -628,7 +535,6 @@ def play_roulette(players: List[Player], games: int = 10) -> List[Player]:
-------
List[Player]
The players after playing the games.
"""
losers = []
for g in range(games):
@ -638,13 +544,13 @@ def play_roulette(players: List[Player], games: int = 10) -> List[Player]:
winnings = spin_wheel(players)
new_losers = []
for i, p in enumerate(players):
p.wallet -= p.strategy.cost
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 losers.
if p.wallet < p.strategy.cost:
if p.wallet < p.strategy.value:
new_losers.append(p)
for losing_player in new_losers:
players.remove(losing_player)

View File

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