Skip to content
Back to Projects

// 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.

// 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
Lines of code
Table, shared casino runtime, and the test harness.
V1
981
NOW
5,070
Files
craps.html, shared.js, craps-tests.html.
V1
1
NOW
3
Bet types
v1 paints Come and Don’t Come on the felt and never resolves them. Today: seventeen on the felt, plus free odds behind each of the four line bets.
V1
19 boxes, 17 live
NOW
21
Automated tests
Assertions over payout maths and roll resolution.
V1
0
NOW
53
Survives a reload
v1 rebuilds itself at 5,000 chips every time the page loads.
V1
nothing
NOW
bankroll, stats, achievements
Pass / Don’t PassV1NOW

Both resolve the come-out and point phases correctly.

Come / Don’t ComeV1NOW

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.

Free oddsV1NOW

Odds behind the line, and behind travelled come bets, paid at true odds — the only bet on the table with no house edge.

Place bets 4–10V1NOW

v1 pays them from decimal approximations (1.167 for 7:6) and marks the point number as OFF, which is exactly backwards.

Hard waysV1NOW

Both lose on the easy way or any 7. v1 got this one right.

One-roll propsV1NOW

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.

Push returns your stakeV1NOW

v1 detects the Don’t Pass push on 12 and then swallows the chips. See the receipt below.

Undo / take bets downV1NOW

v1 offers Clear All or nothing. A misclick on the wrong box was final.

Statistics and achievementsV1NOW

Roll history, win rate, and unlockables held in localStorage.

Shared bankroll across the casinoV1NOW

v1 hardcodes 5,000 chips at construction. The rebuild carries one bankroll between craps, blackjack, roulette and slots.

Errors without alert()V1NOW

v1 blocks the page with a native alert when you overbet.

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 today

V1

// 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 out

V1

<!-- 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 others

V1

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 luck

V1

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.

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.