Farkle (known in Germany as Zehntausend, "ten thousand") is a push-your-luck dice game. It is also a great little kata: the scoring rules are small enough to fit in your head, yet full of edge cases. In this post we will write the scorer as a pure function, then use it to compute the famous bust probabilities.
The rules in 60 seconds
- You roll six dice. You must set aside at least one scoring die (or combination).
- You may then stop and bank your turn total, or re-roll the remaining dice.
- If a roll contains no scoring dice, you bust (Farkle) and lose everything from this turn.
- If all six dice score ("hot dice"), you may pick up all six and keep going.
- The first player to reach 10,000 points wins.
Common scoring (house rules vary, which is exactly why a pure function is handy):
| Combination | Points |
|---|---|
| Single 1 | 100 |
| Single 5 | 50 |
| Three 1s | 1000 |
| Three of X (2-6) | X × 100 |
| Four / five / six of a kind | 2× / 4× / 8× the triple value |
| Straight 1-2-3-4-5-6 | 1500 |
| Three pairs | 750 |
The scoring function
The function takes an array of die faces and returns the best score that uses all given dice, or 0 if some die doesn't score. No I/O, no randomness, no mutation.
const counts = (dice) => {
const c = [0, 0, 0, 0, 0, 0, 0];
for (const d of dice) c[d]++;
return c;
};
const tripleValue = (face) => (face === 1 ? 1000 : face * 100);
export function score(dice) {
const c = counts(dice);
const n = dice.length;
if (n === 6 && c.slice(1).every((x) => x === 1)) return 1500; // straight
if (n === 6 && c.filter((x) => x === 2).length === 3) return 750; // three pairs
let total = 0;
for (let f = 1; f <= 6; f++) {
let k = c[f];
if (k >= 3) {
total += tripleValue(f) * 2 ** (k - 3);
k = 0;
}
if (k > 0) {
if (f === 1) total += 100 * k;
else if (f === 5) total += 50 * k;
else return 0; // a non-scoring die was included
}
}
return total;
}
// Does a roll contain *anything* that scores?
export const hasScore = (roll) => {
const c = counts(roll);
return c[1] > 0 || c[5] > 0 || c.some((x) => x >= 3) ||
score(roll) > 0; // catches three pairs
};
Some tests that pin down the behaviour:
console.assert(score([1]) === 100);
console.assert(score([5, 5]) === 100);
console.assert(score([2, 2, 2]) === 200);
console.assert(score([1, 1, 1, 1]) === 2000);
console.assert(score([1, 2, 3, 4, 5, 6]) === 1500);
console.assert(score([2, 2, 3, 3, 4, 4]) === 750);
console.assert(score([2, 3]) === 0);
Bust probability with 1-6 dice
Because hasScore is pure, we can brute-force every outcome of n dice (6^n, at most 46,656) and count the busts:
function bustChance(n) {
let busts = 0, total = 6 ** n;
for (let i = 0; i < total; i++) {
const roll = [];
for (let j = 0, x = i; j < n; j++, x = Math.floor(x / 6)) roll.push((x % 6) + 1);
if (!hasScore(roll)) busts++;
}
return busts / total;
}
| Dice left | Bust chance |
|---|---|
| 1 | 2/3 ≈ 66.7% |
| 2 | 4/9 ≈ 44.4% |
| 3 | 5/18 ≈ 27.8% |
| 4 | ≈ 15.7% |
| 5 | ≈ 7.7% |
| 6 | ≈ 2.3% |
Sanity check for small n: with one die only a 1 or 5 scores, so the bust chance is 4/6 = 2/3. With two dice both must avoid 1 and 5: (4/6)² = 4/9. With three dice, (4/6)³ = 8/27, minus the triples of 2, 3, 4, 6 (4/216), giving 60/216 = 5/18.
The table explains the strategy most players learn the hard way: re-rolling one or two dice is risky, while re-rolling five or six is almost free.
Kata extensions
- Make the scoring table a parameter so house rules become data.
- Write
bestSelection(roll)that returns the highest-scoring subset. - Compute the expected value of continuing vs. banking at a given turn total.
If you want to try the game with real-feeling virtual dice, roll them here: https://3droll.com/farkle-dice/
Top comments (0)