// VERSUS · CRAPS, ONE YEAR APART
v1 vs. today
On 15 July 2025 the craps table was one 981-line HTML file that ran end to end on the first try. Both versions are playable below, and every claim on this page is a line you can go read in the source.
▸ THE TWO BUILDS
// FIRST WORKING BUILD
Complete Craps Table
15 Jul 2025 · gilfila/craps @ 03a6881 · 981 lines
One file, one class, nineteen betting boxes on green felt — two of which are wired to nothing. It knows the come-out and point phases, pays the hard ways correctly, and loses your money on a push. Preserved byte for byte.
Play v1// RUNNING NOW
Craps Table
Today · public/games/craps.html · 5,070 lines across 3 files
Come and Don't Come that travel to their point boxes, free odds behind every line bet, undo, persisted statistics and achievements, a bankroll shared with the rest of the casino, and 53 assertions holding the payout maths in place.
Play the current table▸ SCOREBOARD
▸ WHAT EACH TABLE ACTUALLY DOES
Both resolve the come-out and point phases correctly.
Both boxes are painted on the v1 felt and both accept chips. Neither has a line of code behind it. The current table travels come bets to their point boxes and resolves each one independently.
Odds behind the line, and behind travelled come bets, paid at true odds — the only bet on the table with no house edge.
v1 pays them from decimal approximations (1.167 for 7:6) and marks the point number as OFF, which is exactly backwards.
Both lose on the easy way or any 7. v1 got this one right.
v1 has Any 7, Any Craps, Ace-Deuce and Boxcars. The rebuild kept the first two and dropped the 30:1 pair — a deliberate trim, not an upgrade.
v1 detects the Don’t Pass push on 12 and then swallows the chips. See the receipt below.
v1 offers Clear All or nothing. A misclick on the wrong box was final.
Roll history, win rate, and unlockables held in localStorage.
v1 hardcodes 5,000 chips at construction. The rebuild carries one bankroll between craps, blackjack, roulette and slots.
v1 blocks the page with a native alert when you overbet.
▸ RECEIPTS
Four places where the difference is not a matter of taste. Left is v1 as shipped, right is the same decision in the current table.
The push that eats your money
A real bug, live in v1 todayV1
// resolveBet() — the push is detected correctly:
case 'dontpass':
} else if (total === 12) {
result = { remove: true, payout: betAmount }; // Push
}
// resolveRoll() — but payout is only ever banked on a win:
if (result.win) {
winnings += result.payout;
} else if (result.lose) {
message += ` | ${betType.toUpperCase()}: LOSE`;
}NOW
createPushResult(label, stake) {
return {
resolved: true,
label,
net: 0,
payout: stake, // stake goes back, net is zero
remove: true,
lineText: `${label} ${this.formatSignedAmount(0)}`
};
}The stake is deducted the moment you place the bet. A push has win false and lose false, so v1 deletes the bet without ever crediting the payout back — a Don’t Pass push on 12 quietly costs you the full wager. The rebuild routes every outcome, push included, through one result shape that always names its payout.
Two bets that look finished and aren’t
Takes your chips, never pays outV1
<!-- The felt: both boxes are drawn and both are clickable -->
<div class="come" data-bet="come"
onclick="placeBet('come')">
<div class="dont-come" data-bet="dontcome"
onclick="placeBet('dontcome')">
// resolveBet(): there is no case 'come' and no
// case 'dontcome'. Every roll falls through to:
let result = { win: false, lose: false,
remove: false, payout: 0 };NOW
case 'come':
if (total === 7 || total === 11)
return this.createWinResult('Come', betAmount, betAmount, true);
if (total === 2 || total === 3 || total === 12)
return this.createLossResult('Come', betAmount);
if (this.pointNumbers.includes(total))
return this.createMoveResult('Come', total); // travels
break;Clicking Come in v1 deducts the chips and puts a stack on the felt. It then sits there through every roll — never winning, never losing, unrecoverable except by clearing the whole table. The one field the constructor set aside for it, comePoint, is written once and never read. This is the difference the year actually bought: not that the rebuild has come bets, but that it does not ship a button with nothing behind it.
Payouts by approximation
Correct at some stakes, wrong at othersV1
case 'place6': case 'place8':
result = { payout: Math.floor(betAmount * 1.167) + betAmount };
case 'place5': case 'place9':
result = { payout: Math.floor(betAmount * 1.4) + betAmount };
case 'place4': case 'place10':
result = { payout: Math.floor(betAmount * 1.8) + betAmount };NOW
case 'place6': case 'place8':
return this.createWinResult(
name, betAmount, Math.floor(betAmount * 7 / 6), false);
case 'place5': case 'place9':
return this.createWinResult(
name, betAmount, Math.floor(betAmount * 7 / 5), false);
case 'place4': case 'place10':
return this.createWinResult(
name, betAmount, Math.floor(betAmount * 9 / 5), false);7:6 is not 1.167. On a $600 place-6 the approximation underpays by a dollar, and the error grows with the stake. The ratios are integers in the rebuild because that is what they actually are.
Parsing a bet name by slicing the string
Works by luckV1
case 'place4': case 'place10':
if (total === parseInt(betType.slice(-1)) ||
total === parseInt(betType.slice(-2))) {NOW
case 'place4': case 'place10':
if (total === parseInt(betType.replace('place', ''))) {For place10 the last character is "0" and the last two are "10", so the second half of the condition rescues the first. For place4 the last two characters parse to NaN, which happens to be harmless. Both branches are accidents — the name just needs its prefix removed.
▸ WHERE V1 STILL WINS
Two things got worse, and pretending otherwise would make the rest of this page worthless. The rebuild dropped Ace-Deuce and Boxcars, the two 30:1 one-roll props v1 had. And v1 is 981 lines you can read on a train — the current table is five thousand across three files, which is the real price of come bets, odds, persistence and a test suite. Bigger is not the achievement. Being right about the money is.