Craps: An Engineering Marvel?
22 September, 2025
Vegas season is almost upon us in the AWS ecosystem where a lot of partners and customers attend Re:Invent. It's a pretty overwhelming and chaotic event but has lots of great networking and team bonding opportunities. I'm always happy to take prospects, customers, and partners to some killer (albeit overpriced) dinners.
One of my favorite things to do is gamble. I'm not a huge gambler, but I dabble. You probably won't find me at the poker tables in Vegas because while I do play, I prefer friendly home games with friends and smaller venues. I'm not a poker shark and will probably lose my lunch money at those tables.
My favorite game is craps! Super fun dice game; you can play it simple or jump around bets all over the table with various levels of risk (house always wins though). The past few months I've been working on a side project of building a craps web app which ended up being a fun and challenging project. I personally find it to be the coolest thing I've built yet because of the subtle but impactful engineering choices. In this post I'll talk about the features I love and the implementation along with some code samples and architecture patterns.
If you want to play it is (hopefully) online at https://craps.lfglance.dev
The Board
I needed an interactive board where different areas could be highlighted and chips would rest on them. Trying to make a static image and use Javascript to overlay on the DOM seemed like a nightmare so I opted to use SVG. I created a board from scratch with Inkscape.
Within Inkscape I can set the properties of the various shapes and pieces, including the ID or class name. Since SVG can be loaded directly into the HTML I am able to target the different pieces with CSS and Javascript to enable hover styles and click events.
The Betting
The betting mechanics are fairly straightforward; the different squares on the board have different odds. A 7:6 odds bet wins you $7 for every $6 you bet; a $100 bet with 7:6 odds wins you ~$116.
def determine_payout(amount: int, ratio: str) -> int:
"""
Simple math to get the payout for a given amount and ratio - rounds down.
"""
a, b = map(int, ratio.split(":"))
return int(amount * (a / b))
I have a class with all of the available bets, their odds, and friendly names.
class Bets(Enum):
TIP = ("1:1", "Tip")
PASS_LINE = ("1:1", "Pass Line")
PASS_LINE_ODDS_6 = ("6:5", "Pass Line Odds (6, 8)")
PASS_LINE_ODDS_8 = ("6:5", "Pass Line Odds (6, 8)")
PASS_LINE_ODDS_5 = ("3:2", "Pass Line Odds (5, 9)")
...
PROP_12 = ("30:1", "Boxcars")
One thing that became tedious was the sheer amount of "bet" objects that were created when a user is placing bets. If they lay down a stack of 10 $1 chips, that's 10 individual bets. Instead of resolving them individually I decided to implement a "roll-up" function to sum all of the chips on a given bet, which simplifies payouts:
def consolidate_active_bets(self):
"""
Go through all active bets in db and
roll up the amounts into a summary bet
for each individual user and bet.
"""
consolidated = db.session.query(
Bet.user_id,
Bet.bet,
func.count(Bet.bet).label("unique"),
func.sum(Bet.amount).label("total_amount")
).filter(
Bet.active == True
).group_by(
Bet.user_id,
Bet.bet
)
for bet in consolidated:
if bet.unique > 1:
existing = Bet.query.filter(
Bet.active == True,
Bet.user_id == bet.user_id,
Bet.bet == bet.bet
).all()
for b in existing:
db.session.delete(b)
db.session.commit()
b = Bet(
user_id=bet.user_id,
bet=bet.bet,
amount=bet.total_amount
)
db.session.add(b)
db.session.commit()
The pace of the game is carefully controlled by the backend process which communicates with all players via websockets and can control when bets are placed and reject invalid/illegal bets.
The Chips
The chips were also made in Inkscape but combined into a single image and served as CSS image sprites. When a div has a specific class it simply renders that chip on the page.
Chip placement on the SVG is fairly simple. Some Javascript state tracks which chip increment is currently selected, sets up click events on the board sections, and identifies the offsets in the DOM where the chips will be shown. Some neat tricks help with the overall UX when dropping chips.
There is an initial chip placement which randomly places it in the clicked area to give it the feel of a messy board with chips flying around. The click event when the chip is laid down also sends a websocket message to the backend which stores the bets.
const placeOffset = $(`#${data.bet}_USER_PLACEMENT`).offset()
const el = $(`<div class="sprite activeBets" id="${selectedChip}"></div>`);
el.css({
left: placeOffset.left + (Math.random() * 20) + 'px',
top: placeOffset.top + (Math.random() * 20) + 'px',
position: 'absolute',
display: 'block',
width: '80px',
transform: 'scale(.5)'
})
el.appendTo('body');
The backend takes in all the available bets, consolidates them into a one single bet, and for display purposes shows the whole bet broken into biggest chip increments (i.e. 25 $1 bets shows one $25 chip).
def split_amounts(self, amount: int) -> tuple:
"""
Take a given integer and split it into smaller
increments of 1, 5, 10, 25, 50, and 100 with
a greedy algorithm.
"""
hundreds = amount // 100
amount %= 100
fifties = amount // 50
amount %= 50
twentyfives = amount // 25
amount %= 25 # Remainder after using 25's
tens = amount // 10
amount %= 10 # Remainder after using 10's
fives = amount // 5
amount %= 5 # Remainder after using 5's
ones = amount # Remaining amount is all ones
return int(hundreds), int(fifties), int(twentyfives), int(tens), int(fives), int(ones)
Another Javascript function removes all of the user placed chips and takes data from the backend to formally lay down chips where they belong. Some clever CSS offsets give the chips a nice stacked appearance.
The State
State is maintained by a backend worker process and involves a combination of relational database and cache (PostgreSQL and Redis).
A websocket broker manages connected users and relays messages back and forth from client and server. One function handles all of the business logic of how to handle messages and a series of other functions get called to update state; all of the business logic from updating balances, consolidating bets, performing the payout calculations, updating other state like point, tracking losses, etc.
def get_state(self, name) -> dict:
return {
"point": self.get_item("point"),
"rolling": self.get_item("rolling"),
"betting": self.get_item("betting"),
"time_remaining": self.get_item("time_remaining"),
"balance": self.get_user_balance(name),
"bets": self.get_user_bets(name),
"last_rolls": self.get_last_rolls(),
"last_bets": self.get_last_bets(),
"players": self.get_players(),
"logs": self.get_logs()
}
The get_state
function retrieves items from cache and is constantly being updated with in-game events. Any activity such as a bet being placed is written to the database atomically and subsequently populated into cache where it can be presented to the user quickly.
All reads come right from cache for performance and to ease the burden on the database. The state is constantly being relayed to the client so Javascript can update the DOM.
The Dice
Every bet will be resolved based upon the results of a dice roll. Every roll is it's own database object with a random UUID assigned as a randomness hash to make it deterministic. You can always recreate the given dice pair with that UUID. There are some helper functions used throughout to make checking certain conditions easier. I don't have verifiable randomness implemented but that would be a must if dealing with real money.
class Roll(db.Model):
id = db.Column(db.String(200), default=gen_uuid, primary_key=True)
create_date = db.Column(db.DateTime, default=get_date)
def dice(self) -> tuple:
rn = Random(self.id)
d1 = rn.randint(1, 6)
d2 = rn.randint(1, 6)
return (d1, d2)
def as_str(self) -> str:
d1, d2 = self.dice()
return f"{d1},{d2}"
def is_craps(self) -> bool:
d1, d2 = self.dice()
total = d1 + d2
if total == 2 or total == 3 or total == 12:
return True
else:
return False
def is_natural(self) -> bool:
d1, d2 = self.dice()
total = d1 + d2
if total == 7 or total == 11:
return True
else:
return False
def __repr__(self):
return f'roll-{self.id}'
The statistics of dice rolls is a pretty easy thing to determine - roll the dice a million times and see how it turns out.
def rollstats():
totals = []
for i in range(1_000_000):
d1, d2 = Roll().dice()
totals.append(d1 + d2)
mean = np.mean(totals)
std = np.std(totals)
x = np.linspace(min(totals), max(totals), 100)
pdf = norm.pdf(x, mean, std)
plt.plot(x, pdf)
plt.hist(totals, density=True, alpha=0.6, color="g")
plt.xlabel("Value")
plt.ylabel("Probability Density")
plt.title("Bell Curve of Data")
plt.show()
Wrap-Up
Super fun project to take on. I decided while on a trip to South Lake Tahoe after spending the evening playings craps and poker that I was going to make a web version of my own just for the challenge. The websockets were a fun implementation and took some careful considerations in the implementation. It's far from perfect and actually has a huge weakness in that websocket connections are held in memory by the backend, which is incompatible with load balancing. I will need to refactor that to use a shared location like cache if I want this to scale, however, I don't think I'll have that problem as it's just a hobby thing that I enjoy playing solo.