TinyGames · how it works

Yahtzee

Five dice, three rolls, thirteen boxes and no way to leave one empty. The scoring is where every version of this goes wrong.

Open index.html. No build step, no dependencies, no images — the dice are drawn.

yahtzee/
  index.html      markup
  style.css       the frame around the canvas
  js/dice.js      the scoring, the card and the turn
                  - no pixels, no engine, no DOM
  js/yahtzee.js   the playable layer
  test-dice.js    node test-dice.js

Draft. Built, compiled and tested like every other game, and not yet on the hub: "draft": true in build/games.json keeps it off the cards, out of the sitemap and without a readme page.


There is no clever loop here. It is all in what a hand is worth

Nothing in this game moves. There is no physics, no generator, no clock. Every mistake it can make is a scoring mistake, every one of them is silent, and every one of them is only ever wrong by a plausible-looking amount — which is why they survive so long in home-made versions.

Trap one: three of a kind scores the whole hand

3-3-3-6-5 is twenty, not nine. Three and four of a kind score the sum of all five dice, not the sum of the matching ones. The name suggests otherwise and the wrong reading looks perfectly reasonable on screen.

The two readings disagree on 120 of the 252 possible hands, so this is not an edge case, it is half the game.

Trap two: a straight lives in the set of faces, not in the sorted dice

Sort 1-2-2-3-4, walk it counting consecutive steps, and the repeated 2 breaks the run. A perfectly good small straight scores nothing. The hand contains 1-2-3-4; the scan just cannot see past the duplicate.

Detection has to ask which faces are present and look for a run through those. The sorted-scan version gets 6 of the 252 hands wrong — few enough that nobody hits it in testing, often enough that a real player will.

Its smaller sibling: a large straight is either 1-2-3-4-5 or 2-3-4-5-6. Requiring the hand to start at one is the same narrowing, one window later.

Trap three: a zero is a move

Late in a game a player writes a hopeless hand off into a box they cannot fill, deliberately, and scores nothing for it. An assign that reads if (score) card[cat] = score fills nothing, leaves the box open, and quietly hands out a free turn — and the game then never ends.

Trap four: the bonus is at sixty-three, inclusive

Three of each face is 3 × (1+2+3+4+5+6) = 63 exactly, and that is the whole design of the upper section: three of each and you have earned it. A > 63 costs the player thirty-five points on the one score the rule was written for, and on no other score at all — the two versions differ on exactly one number.

Trap five: the holds belong to the turn

Score a box, forget to clear the holds, and the next turn opens with dice already locked. The player's first roll rolls two of them and they cannot see why. take() therefore replaces the whole turn rather than setting the counter back to zero, which makes forgetting impossible rather than merely unlikely.

Trap six: a die has six faces

1 + Math.floor(rnd() * 6) is a seven the moment rnd() returns exactly 1. Math.random() never does, so this is dead code against a browser and live code against every seeded generator a test or a replay might use. A seven then scores as nothing at all in the boxes that count faces, and gets quietly added up by Chance — wrong in two different directions in the same hand.


So every hand is scored twice, by two different programs

Yahtzee is small enough to check exhaustively, which changes what the tests can claim. There are exactly 252 distinct hands of five six-sided dice — the number of multisets, C(10,5), which the suite counts a second way before trusting it — and every one is scored in all thirteen boxes against a second implementation written in the test, deliberately the slow and obvious way:

the game asks the reference asks
does any face appear three times? is any of the ten three-element subsets all the same?
is there a run of four in the set of faces? is any of the five four-element subsets a ladder?
counts of 3 and 2 among the faces is the sorted shape of the hand [2,3] or [5]?
tally[face] * face sum of the dice filtered to that face

Different ideas, not just different spellings, and the four-subset formulation is why the reference finds 1-2-2-3-4 without knowing that duplicates exist. All 3276 answers agree.

And three counts checked against arithmetic rather than against code

The strongest checks here do not compare two programs. Over the 7776 ordered rolls:

Alongside, the properties that have to hold whatever the hand

Every box scores ≥ 0 and nothing exceeds fifty. The six upper boxes together are always exactly the pip total, because every die counts in exactly one of them. Three and four of a kind are always either nothing or the whole hand. Four of a kind always scores a three of a kind, and the same amount. Every large straight is a small straight. Shuffling a hand never changes a single score — 1008 shuffles of it — which is what would catch an implementation that sorts once and then leans on the sort.

$ node test-dice.js
176 passed, 0 failed

A decision, written down rather than left implied

Five of a kind counts as a full house here, worth the usual 25. It is three of a kind and a pair, both of the same face, and the official rules reach the same answer by a longer road: the Joker rule lets a second Yahtzee be written into full house for 25. Taking the short road means one rule instead of two, and it never costs the player.

It is asserted directly in the test rather than only implied by the reference table, so anyone who wants the strict reading changes one line and one test.

Not implemented, also on purpose: the 100-point bonus for a second Yahtzee, and the rest of the Joker rule. Both exist to answer "what happens once the Yahtzee box is already used", and both are where published rule sets disagree with each other. One Yahtzee box, worth 50 or worth 0.


What the tests are worth

Eleven mutants. Ten broke something real; the eleventh changed nothing on purpose.

mutation result
three/four of a kind score only the matching dice caught (7 assertions)
small straight found by scanning the sorted hand caught (6)
large straight must start at one caught (6)
full house is any hand with two distinct faces caught (5)
the bonus needs more than 63, not 63 caught (6)
assign refuses to write a zero caught (10)
rolling rerolls the held dice too caught (3)
a fourth roll is allowed caught (3)
the next turn keeps the previous turn's holds caught (3)
a die can come up seven caught (3)
scoreIn wrapped, behaviour unchanged passes, as it should

The held-dice mutant was caught by a single assertion in the first version of the suite — one line standing between the tests and a real bug. The property was stated statistically ("a held die never changes, over 1500 turns") and nothing said which die. It is now also stated the plain way: roll five ones, keep two, roll five sixes, and the hand reads 1,1,6,6,6. Three assertions now catch it.

Two failures that were the test's fault, not the game's

Both worth recording, because both are the shape of mistake that makes a suite look stronger than it is.

The first: a helper built "three of each face" by padding the hand with ones, so for the ones box it produced five ones instead of three, and the upper section came to 65 rather than 63. The game was right; the fixture was wrong.

The second is the more interesting one. Four hundred randomly-filled cards were checked against an independent walk of the card, including the bonus — and the liveness counter read "0 with the bonus, 400 without". The upper section of a randomly-filled card averages about seventeen and never comes close to sixty-three, so four hundred cards had asserted the bonus arithmetic without once putting it to the question. Half the cards are now played by somebody chasing the upper section. Without that liveness counter the block would have passed forever while testing nothing.


Playing it

Tap ROLL for the first roll, then tap any dice you want to keep and roll again — three rolls a turn. Tap a box on the card to write the hand into it. The number shown against every open box is what the dice on the table would score there, asked of the rules rather than worked out by the display, so the card is never telling you something the scoring would disagree with.

You cannot leave a box empty, and you may deliberately write a zero into one. Thirteen turns, thirteen boxes, and the game ends when the card is full.

Keys: Space or Enter rolls, 1–5 keep the die under that number, Escape or P pauses, R starts over.