Fair dice for the table, with the odds of every throw.
Tap dice to build a roll, up to ten in any mix from a d4 to a d100, or type any dice at all. Exact probabilities, roll history and stats, in a tray that runs anywhere.
Roll some dice → · Read the documentation →
The demo on a desk: eight d6 and the stats of twelve throws. |
On a phone, in Japanese, in the device's light or dark. |
A dice roller and a dice notation parser for tabletop games, RPGs and board games, with the exact odds of every roll.
- What is different. The odds are exact, worked out and never simulated, for every notation it reads. The tray is finished: dice you tap, real dice sounds, history, stats and a link to any roll. And a seeded roll can be checked by anybody, die for die.
- What it costs a project. Nothing: no dependencies, and one import.
npm install @johnmorrisdotca/korokoro # or pnpm add, or yarn addimport { chanceAtLeast, parseNotation, roll } from "@johnmorrisdotca/korokoro";
const attack = parseNotation("2d20kh1+5")!; // advantage, plus 5
roll(attack).total; // 6 to 25, from the crypto generator
chanceAtLeast(attack, 15); // 0.7975: the exact chance of 15 or moreAnd 44 games come with their dice and their rules for reading them:
import { crapsPass, rollPreset } from "@johnmorrisdotca/korokoro";
rollPreset("yahtzee").reading.text; // "A full house", "Chance, for 13", …
rollPreset("チンチロリン", { language: "ja" }).reading.text; // "シゴロ(4・5・6)", …
crapsPass(); // [244n, 495n]: the shooter's exact chance, 244 in 495And from a terminal, on Linux, macOS or Windows:
npx @johnmorrisdotca/korokoro 2d20kh1+5 --seed table # 2d20kh1+5: 24 [19 (12)]Or with nothing to install, roll some dice in the demo.
- Roleplaying games. A d20 with advantage (
2d20kh1+5), ability scores (4d6dl1), a fireball's damage (8d6), a d20 and a d4 together (1d20+1d4). For Dungeons & Dragons, Pathfinder and anything else that rolls polyhedral dice. - Board games and dice games.
2d6for the table; five dice with holds for Yahtzee (5d6); six for Farkle (6d6); a die with your game's own faces (d[Hit,Miss,Miss]). - Fun. Tap the felt.
- Teaching and research. The exact odds and a chart for any roll:
2d6makes 7 one time in 6. A seed makes a classroom's rolls repeatable. - The curious and the suspicious. Loaded dice that say they are loaded
(
d6{6:3}), and a test for whether a real die is fair.
Game names are trademarks of their respective owners. Korokoro is not affiliated with or endorsed by them; it rolls the dice their rules call for.
- Every die a table needs, in any mix. Tap a die to add it: up to ten of
d4, d6, d8, d10, d12, d20, d30 and d100, up to four kinds in one roll
(
1d20+2d4+3), with a bonus, and advantage or disadvantage. - Hold and roll again. After a roll, tap a die to hold it and roll the rest, as Yahtzee and Farkle do. The odds follow the dice still to roll.
- And any other dice, by notation. A die of any size from 2 sides to 1000,
Fate dice, keep or drop (
4d6dl1), rerolls (2d6r<3,2d6ro<3) and exploding dice (3d6!), with what became of each die shown on the felt. - Dice pools. Count successes (
6d10>=8), with failures that take them away (f=1), dice that explode on the faces you say, compounding and penetrating dice, a least and a most for each die, critical marks, sorted dice and a label for the roll. The odds are exact for all of it. - Arithmetic.
(2d6+3)*2,1d20-1d4,floor(4d6/2),max(1d20,1d20)+5, worked out in exact fractions, with exact odds. And dice that all differ (4d6u). - Games, with their dice and their readings. Yahtzee, Risk, craps, backgammon, Catan, Farkle, chō-han (丁半), chinchirorin (チンチロリン) and more: 44 games in one searchable list, each read the way the game reads it, with the exact odds of each outcome. See Games.
- A game to play: Dice War. Everyone rolls, the highest total scores, and a tie is war. Two to eight players, any of them a computer, seeded and saved as text, with its exact odds, and in the tray's Games. See Dice War.
- Several rolls in one tap.
6#4d6dl1is six ability scores at once, with the highest, the lowest and the sum. - Dice of your own. A die with any faces you like, words or numbers
(
d[Yes,No,Maybe]), and sets of dice saved by name on the device and shared by a link. - Loaded dice, honestly marked, and a fairness test. A die weighted to
order (
d6{6:3}) that says so everywhere it appears, and a chi-square test that tells you whether a die, ours or a real one, looks fair. - Fair by construction. Rolls come from
crypto.getRandomValues, turned into faces by rejection sampling, so no face is favoured by a modulo. - Reproducible when asked. A seeded mode throws the same dice for the same seed on every device, so a table can check a roll.
- Exact odds. Each total's chance is worked out, never simulated: the chance to meet a target, the average, the spread and how lucky a throw was.
- History and stats. Up to 500 rolls kept on the device: luck, hot and cold streaks, matching dice, natural 20s and 1s, each face's count with a fairness test, and your totals drawn against the odds.
- Shareable. Any roll becomes a link that shows exactly what was thrown.
- A command line.
koro 2d20kh1+5in a terminal on Linux, macOS or Windows, with the odds, the games, JSON and CSV. See The command line. - Export. A history as CSV for a spreadsheet, JSON that reads back in, or plain text.
- Made for a phone. One thumb does everything: every control is at least 44px, nothing needs a hover or a long press, and nothing moves when the dice land. English and Japanese.
- It feels like dice. Tap anywhere on the felt or press Space. The dice tumble for about half a second and land, with the sound of real dice and a mute button. Light and dark, and themeable. Under reduced motion there is no tumble and the sound starts off.
Each picture is the real tray, drawn by the package and taken from the demo with pnpm screenshots:readme, in light and dark. Every roll is from the seed readme, so the pictures are the same each run.
Exact odds. Advantage plus 5: the chance of every total, worked out and not simulated. |
History. The latest rolls, kept on the device, with export to CSV, JSON or text. |
Dice pools. Count successes ( 6d10>=8f=1): the ticked dice are the ones that count.
|
Dice of your own. Faces you write, worth numbers or not: 3d[Hit=1,Miss=0,Miss=0].
|
Games. 44 games, each read the way the game reads its dice, with the exact odds of each outcome. |
Dice War. Everybody rolls, the highest scores, and a tie is war; two to eight players, any of them a computer. |
One die. A die that rolls when it is tapped, and a die that only shows the face you give it. |
Korokoro is an API and a tray, each usable without the other: an API of plain functions (roll, read notation, work out odds, keep a history), and a tray you mount into any element, which also comes as a React component, a Vue component and a web component.
npm install @johnmorrisdotca/korokoro
pnpm add @johnmorrisdotca/korokoro
yarn add @johnmorrisdotca/korokoroIt is ES modules only, with its types included, and needs Node 22 or later outside a browser. A page with no bundler loads the tag from a CDN (@1 is the major version).
import { checkNotation, distributionOf, roll, seededSource } from "@johnmorrisdotca/korokoro";
const read = checkNotation("4d6dl1"); // { ok: true, spec } or the part refused and why
if (read.ok) {
const thrown = roll(read.spec, seededSource("table-7"));
thrown.faces; // [6, 4, 2, 1]: every die, in the order thrown
thrown.kept; // [true, true, true, false]: the 1 was dropped
thrown.total; // 12
distributionOf(read.spec).probabilities; // the exact chance of every total from 3 to 18
}<div id="dice"></div>
<script type="module">
import { mountRoller } from "@johnmorrisdotca/korokoro";
mountRoller(document.getElementById("dice"), {
spec: { count: 1, sides: 20, modifier: 5 },
onRoll: (roll) => console.log(roll.total),
});
</script>import { DiceRoller } from "@johnmorrisdotca/korokoro/react";
export function Table() {
return <DiceRoller wide notation="2d20kh1+5" onRoll={(roll) => save(roll)} />;
}The component takes the tray's options as props, notation as a shorter way
to give the dice (the tray follows it when it changes), and any attribute for
its <div>. The tray mounts in the browser after the first render, so server
rendering draws an empty box and nothing needs a provider. In Next.js, use it
from a client component ("use client").
<script setup>
import { DiceRoller } from "@johnmorrisdotca/korokoro/vue";
</script>
<template>
<DiceRoller notation="2d20kh1+5" wide @roll="(roll) => save(roll)" />
</template>The props are the tray's options, with notation as a shorter way to give the
dice, and each roll is a roll event. The dice and locale are followed as
they change; roll(), history(), setSpec() and setLocale() are there on
a template ref. It renders an empty box on the server (Nuxt included) and
mounts the tray in the browser. Vue 3.3 or later.
<korokoro-roller notation="2d20kh1+5" wide></korokoro-roller>
<script type="module">
import { defineRoller } from "@johnmorrisdotca/korokoro/element";
defineRoller();
document.addEventListener("korokoro-roll", (event) => console.log(event.detail.total));
</script>A custom element, for any page and any framework that renders HTML. Call
defineRoller() once; each roll is a korokoro-roll event that bubbles, with
the roll as its detail.
Or with no call at all: importing @johnmorrisdotca/korokoro/element/define
registers the element by being imported, so one script tag is the whole of
it, from a CDN or
from your own bundle:
<script type="module" src="https://cdn.jsdelivr.net/npm/@johnmorrisdotca/korokoro@1/dist/element-define.js"></script>
<korokoro-roller notation="2d20kh1+5"></korokoro-roller>| Attribute | What it does |
|---|---|
notation |
The dice showing at first, and again whenever it changes |
lang |
ja for Japanese, anything else English; the page's own language when left out |
wide |
Tray and panels side by side on a wide screen |
size |
small is the felt and the result alone, medium adds the choice of dice, large (or left out) is everything |
sound="off" |
No sound and no mute button |
hold="off" |
Dice are not held |
placeholder="off" |
The opening dice are the user's own roll |
keyboard="off" |
Space rolls only when the focus is inside the tray |
language-chooser |
The tray's own choice of English or 日本語 |
dice-war |
Dice War among the games |
storage="none", storage-key |
Keep no history; or the key it is kept under |
animation-ms, share-base, query |
As the options of the same names |
cloth |
The felt's cloth: green (unless said), blue, red, black or wood, the family's five; changed in place, keeping the dice and the rolls |
one-pip |
The colour of a d6's one pip: red (unless said) or black; changed in place |
What an attribute cannot carry (a theme, your own words, your own sound) goes
on the element's options property. React 19, Vue 3 and Svelte 5 set a
property rather than an attribute on a custom element that has one of the
name; the only name here that is also a property is the die's face, and
die.face = 5 sets the face attribute, as the attribute does. roll(), setSpec() and history are
on the element. The tray is drawn in the element's own light DOM, so the
page's --kk-… variables theme it as they do a mounted tray.
The same one call in the framework's mount hook, and destroy() on the way
out:
<script>
import { onMount } from "svelte";
import { mountRoller } from "@johnmorrisdotca/korokoro";
let box;
onMount(() => {
const roller = mountRoller(box);
return () => roller.destroy();
});
</script>
<div bind:this={box}></div>// Angular: in a standalone component with <div #box></div> in its template
private box = viewChild.required<ElementRef<HTMLElement>>("box");
constructor() {
afterNextRender(() => (this.roller = mountRoller(this.box().nativeElement)));
}
ngOnDestroy() {
this.roller?.destroy();
}Each of the six (Vue, Svelte, Angular, React, the web component and a plain
page) is built from the packed tarball and rolled in Chromium and WebKit by
scripts/check-frameworks.mjs before a release names it.
- Typed results. TypeScript types for everything, with a doc comment on every export.
- A random source you can replace. The default is the platform's
cryptographic generator;
seededSource("any text")is reproducible; and anything with anext()that returns a 32-bit number will do. - No dependencies, ES modules, a
defaultexport condition for tools that resolve from CommonJS, andsideEffects: false, so a bundler drops what you do not import. - Sizes. Rolling, notation and odds alone are about 16 kB minified (6 kB gzipped) once a bundler has shaken the rest out. With the tray it is about 98 kB (35 kB gzipped). The recorded sounds are another 36 kB (23 kB gzipped), fetched only when a roll first needs them.
- Where it runs. Browsers from Chrome and Edge 111, Firefox 113 and Safari 16.2. The core runs in Node 22 and later, Deno and Bun.
Each example is a whole recipe: copy it and it works. The ones in TypeScript are run in CI against the built package (pnpm test:readme), so none of them is a guess, and the output shown is what they print. The odds, the notation and the games each have a reference of their own, linked from the section that names them.
Save this as a file and open it: dice to tap, real dice sounds, history, stats and the odds, in one tag. The module comes from a CDN, and @1 is the major version. Each roll is an event that bubbles.
<!doctype html>
<meta charset="utf-8">
<title>Dice</title>
<script type="module" src="https://cdn.jsdelivr.net/npm/@johnmorrisdotca/korokoro@1/dist/element-define.js"></script>
<korokoro-roller notation="2d20kh1+5" wide></korokoro-roller>
<p id="said"></p>
<script>
document.addEventListener("korokoro-roll", (event) => {
document.getElementById("said").textContent = `${event.detail.spec.count}d${event.detail.spec.sides} came to ${event.detail.total}`;
});
</script>The roll comes from the platform's cryptographic generator; the odds are worked out exactly, never simulated. A seeded source replays the same dice anywhere, so a table can check a roll.
import { chanceAtLeast, expectedTotal, parseNotation, roll, seededSource } from "@johnmorrisdotca/korokoro";
const attack = parseNotation("2d20kh1+5")!; // advantage, plus 5
const thrown = roll(attack, seededSource("table")); // the same dice for the same seed, on every device
console.log(thrown.faces, thrown.kept, "->", thrown.total);
console.log("at least 15:", chanceAtLeast(attack, 15).toFixed(4), "| average:", expectedTotal(attack));
console.log(roll(attack, seededSource("table")).total === thrown.total);[ 19, 12 ] [ true, false ] -> 24
at least 15: 0.7975 | average: 18.825
true
4d6dl1 is four d6 with the lowest dropped, and rollMany throws it six times from one generator, so a seed replays the whole lot.
import { parseNotation, rollMany, seededSource } from "@johnmorrisdotca/korokoro";
const scores = rollMany(parseNotation("4d6dl1")!, 6, seededSource("table"));
console.log(scores.rolls.map((one) => one.total), "sum", scores.sum, "highest", scores.highest, "lowest", scores.lowest);[ 8, 12, 11, 9, 13, 12 ] sum 65 highest 13 lowest 8
6d10>=8f=1 counts how many of six d10 show 8 or more, and each 1 takes a success away. The odds of three successes or more are exact.
import { chanceAtLeast, expectedTotal, parseNotation, roll, seededSource } from "@johnmorrisdotca/korokoro";
const pool = parseNotation("6d10>=8f=1")!;
const thrown = roll(pool, seededSource("pool"));
console.log(thrown.faces, "->", thrown.total, "successes");
console.log("three or more:", chanceAtLeast(pool, 3).toFixed(4), "| expected:", expectedTotal(pool).toFixed(1));[ 6, 9, 8, 9, 1, 8 ] -> 3 successes
three or more: 0.1859 | expected: 1.2
A bad roll is null from parseNotation, and checkNotation names the part that was refused, so a form can say so.
import { checkNotation } from "@johnmorrisdotca/korokoro";
for (const text of ["4d6dl1", "11d6", "2d6+"]) {
const read = checkNotation(text);
console.log(text.padEnd(8), read.ok ? "read" : `refused: ${read.message}`);
}4d6dl1 read
11d6 refused: “11”: roll 1 to 10 dice at a time
2d6+ refused: “+”: this is not dice notation
A die is whatever faces you write, and a die weighted to order says so everywhere it appears. isFair lets a site refuse anything loaded in one call; fairnessTest says whether a real die's counts look fair.
import { expectedTotal, fairnessTest, isFair, parseNotation, roll, seededSource } from "@johnmorrisdotca/korokoro";
const hits = parseNotation("3d[Hit=1,Miss=0,Miss=0]")!;
const thrown = roll(hits, seededSource("table-7"));
console.log(thrown.dice!.map((die) => die.label), "total", thrown.total, "| expected", expectedTotal(hits));
const optimist = parseNotation("d6{6:3}")!;
console.log("fair:", isFair(optimist), "| marked loaded:", roll(optimist, seededSource("x")).loaded);
console.log(fairnessTest([30, 30, 30, 30, 30, 90]).verdict); // a real d6 thrown 240 times[ 'Miss', 'Hit', 'Miss' ] total 1 | expected 1
fair: false | marked loaded: true
lopsided
Forty-four games come with their dice and their rules for reading them, found by any name the game goes by. A game is read, not run: Korokoro throws the dice and says what the game makes of them.
import { crapsPass, getPreset, rollPreset, seededSource } from "@johnmorrisdotca/korokoro";
const thrown = rollPreset("yahtzee", { source: seededSource("table") });
console.log(thrown.roll.faces, "-", thrown.reading.text, `(${thrown.reading.outcome})`);
console.log(getPreset("Yacht") === getPreset("yahtzee")); // other names find it too
console.log(crapsPass(), "-> the shooter's exact chance is 244 in 495");[ 1, 2, 1, 5, 4 ] - Chance, for 13 (chance)
true
[ 244n, 495n ] -> the shooter's exact chance is 244 in 495
As in Yahtzee and Farkle: keep the sixes, throw the others again from the same source, so a seeded game replays.
import { parseNotation, roll, rollHeld, seededSource } from "@johnmorrisdotca/korokoro";
const dice = seededSource("yacht");
const first = roll(parseNotation("5d6")!, dice);
const second = rollHeld(first, first.faces.map((face) => face === 6), dice); // true holds a die
console.log(first.faces, "->", second.faces);[ 3, 3, 6, 3, 6 ] -> [ 3, 4, 6, 1, 6 ]
The one game here that is played and not only read: everybody rolls, the highest scores, a tie is war. The computers' dice come from the seed, a person's are handed in, and the game is kept as text.
import { diceWarOdds, encodeDiceWar, playDiceWar, startDiceWar } from "@johnmorrisdotca/korokoro";
let game = startDiceWar({ players: ["You", "Aiko", "Ben"], computers: [false, true, true], seed: "table", to: 5 })!;
game = playDiceWar(game, { faces: { "0": [4] } })!; // you rolled a 4 at the table; the others are the seed's
console.log("scores", game.scores, "| a war in", (1 / diceWarOdds({ players: 3 }).war).toFixed(1), "throws");
console.log(encodeDiceWar(game).length > 40);scores [ 0, 1, 0 ] | a war in 4.2 throws
true
A history is written out as CSV for a spreadsheet, JSON that reads back in, or plain text for a chat, and any roll becomes a link that shows exactly what was thrown.
import { parseNotation, roll, seededSource, shareQuery, toCSV, toText } from "@johnmorrisdotca/korokoro";
const rolls = [roll(parseNotation("4d6dl1")!, seededSource("table-7"), 1759190400000)];
console.log(toText(rolls).trim());
console.log(toCSV(rolls).split("\r\n")[1]);
console.log(`https://johnmorrisdotca.github.io/korokoro/?${shareQuery(rolls[0]!)}`);2025-09-30T00:00:00.000Z 4d6kh3: 12 [6 4 2 (1)]
2025-09-30T00:00:00.000Z,4d6kh3,,12,6 4 2 (1),6 4 2 1,table-7,,,
https://johnmorrisdotca.github.io/korokoro/?roll=4d6kh3&faces=6%2C4%2C2%2C1&at=1759190400000&seed=table-7&v=2
Three commands are the same: korokoro, koro and roll. They need Node 22 and nothing else, on Linux, macOS and Windows; the whole reference is under The command line.
npx @johnmorrisdotca/korokoro 2d20kh1+5 --seed table
npm install -g @johnmorrisdotca/korokoro
koro 6#4d6dl1 --seed table
roll 2d6+32d20kh1+5: 24 [19 (12)]
4d6kh3: 8 [1 2 (1) 5]
4d6kh3: 12 [4 6 (1) 2]
…
A single die with nothing round it: a d20 that rolls when it is tapped, and a d6 that only shows a face. The tag comes from the same script as the tray.
<script type="module" src="https://cdn.jsdelivr.net/npm/@johnmorrisdotca/korokoro@1/dist/element-define.js"></script>
<korokoro-die sides="20" size="large"></korokoro-die>
<korokoro-die sides="6" face="5" rollable="off"></korokoro-die>Every colour is a custom property on the tray. Pass them as theme, which sets them on the tray itself and so wins in light and dark alike.
import { mountRoller } from "@johnmorrisdotca/korokoro";
mountRoller(document.getElementById("dice")!, { theme: { "--kk-felt": "#23405a", "--kk-felt-deep": "#162a3c", "--kk-accent": "#d4a017" } });A roll is written the way a character sheet writes it, and read back the same way. A few of the commonest, with what they mean:
| Notation | Means |
|---|---|
d20, 3d6+2, 4d8-1 |
one twenty-sided die; three d6 plus 2; four d8 minus 1 |
4dF |
four Fate dice, each −1, 0 or +1 |
2d20kh1, 2d20kl1 |
advantage and disadvantage: keep the highest or the lowest |
4d6dl1 |
four d6, drop the lowest one |
2d6r<3, 3d6! |
reroll the low faces; explode on the highest |
6d10>=8f=1 |
a pool: count the successes, a 1 takes one away |
(2d6+3)*2, floor(4d6/2), max(1d20,1d20)+5 |
arithmetic, in exact fractions |
6#4d6dl1 |
the whole roll six times: six ability scores |
d[Yes,No,Maybe], d6{6:3} |
a die of your own; a loaded die, marked as loaded |
The notation page has the whole table, the grammar, the formulas, the order the modifiers apply in, the success pools, and what is refused and why.
A roll is plain data: every die's face, whether it was kept, what became of it, the total, the time, the seed and the notation it was thrown from, as JSON. Rolls shows one in full.
A custom die is its faces: up to 20 of them, each up to 16 characters, worth a number or not (3d[Hit=1,Miss=0,Miss=0]). Dice of your own has the rules and the odds.
Korokoro's own dice are fair. It can also load one (d6{6:3}), and then it tells everybody: the die wears a mark everywhere it appears, and isFair lets a site refuse anything loaded. A chi-square test says whether a real die looks fair. More.
A set is a roll with a name, kept on the device and shared by a link. More.
6#4d6dl1 is six ability scores at once, with the highest, the lowest and the sum; rollMany throws them from one generator, so a seed replays the lot. More.
Korokoro knows the dice of 44 games and how each game reads them: Yahtzee, Risk, craps, backgammon, Catan, Farkle, chō-han (丁半), chinchirorin (チンチロリン) and more, in one searchable list, each with the exact odds of every outcome. A game is read, not run. Games and Dice War has the shelves, the code and the limits; the gallery shows every game one by one.
The one game here that is played and not only read: two to eight players, any of them a computer; everybody rolls, the highest total scores, and a tie is war. Seeded, kept as text, with exact odds, and in the tray's Games. More.
rollHeld keeps some dice of a roll and throws the rest again from the same source, as Yahtzee and Farkle do, so a seeded game replays. More.
The odds are worked out, never simulated: the chance of a total, of at least a target, the average, the spread, the most likely total and how lucky a throw was, for every notation it reads, including pools and formulas. Odds shows each call.
Installing the package puts three commands on the path, korokoro, koro and roll, which are the same: roll 2d6+3. They need Node 22 or later and nothing else, and print the roll, the odds, a game, JSON or CSV. The command line has every option and the output of each command.
A history is written out three ways: CSV for a spreadsheet, JSON that reads back in, and plain text. More.
A page at api/ shows a roll as plain text from its address, in your browser, for a link, a bookmark or a frame. More.
Under Randomness the tray switches between Fair, the device's cryptographic generator, and Seeded, where the same seed throws the same sequence everywhere, so anybody can check a roll die for die. More.
A single die with nothing round it, that rolls when tapped or only shows a face: mountDie and the <korokoro-die> tag. Embedding has the options.
The tray on a page you do not build, in an iframe or as one tag, in three sizes. Embedding has the address and the sizes.
The tray plays a shake while the dice tumble and a knock as each lands, from recordings of real dice. Ten dice landing are five knocks, not ten.
- It starts with the sound on, or off where the device asks for reduced motion. The speaker button on the felt mutes it, and the choice is remembered on the device.
- The recordings (36 kB) are fetched the first time a roll needs them and not
before: a muted tray, or one mounted with
sound: false, never downloads them. If they cannot be fetched or decoded, the tray plays a short knock it makes itself. - A browser only lets a page make sound after somebody has touched it, so a roll started by code before any tap is silent.
- Nothing throws where there is no audio, as on a server or in a test.
mountRoller(el, { sound: false }); // silent, no button
mountRoller(el, { playSound: ({ dice }) => myClack(dice) }); // your own soundDice sounds from Kenney's Casino Audio, CC0, kenney.nl. SOUNDS.md names the files and what was done to them.
Every function is pure unless it says otherwise, every type is exported, and each has a doc comment your editor will show. The API reference, made from the source by pnpm docs:site, lists every export of every entry point with its signature and its doc comment. The API page says what each call does, by what it is for.
| Import | What it holds |
|---|---|
@johnmorrisdotca/korokoro |
Dice, notation, exact odds, history and statistics, games, Dice War, export and sharing, and the tray to mount |
@johnmorrisdotca/korokoro/react |
DiceRoller: the tray as a React component |
@johnmorrisdotca/korokoro/vue |
DiceRoller: the tray as a Vue component |
@johnmorrisdotca/korokoro/element |
The custom elements, defined by defineRoller() |
@johnmorrisdotca/korokoro/element/define |
Defines <korokoro-roller> and <korokoro-die> by being imported |
@johnmorrisdotca/korokoro/sounds |
The recorded dice, as base64 AAC audio, fetched by the first roll that needs them |
| Call | What it does |
|---|---|
parseNotation(text) and checkNotation(text) |
A roll read from notation, or null; or the part refused and why |
roll(spec, source?) |
A roll: every die, what became of it, the total |
seededSource(text) |
A source that throws the same dice for the same seed |
chanceAtLeast(spec, n), expectedTotal(spec), distributionOf(spec) |
Exact odds |
rollMany, rollHeld, rollPreset |
Several rolls, held dice, and a game's dice |
mountRoller(element, options) |
The tray |
Every colour is a CSS variable on .kk-root. Pass them as theme, which sets
them on the tray itself and so wins in light and dark alike:
mountRoller(el, { theme: { "--kk-felt": "#23405a", "--kk-felt-deep": "#162a3c", "--kk-accent": "#d4a017" } });--kk-surface, --kk-ink, --kk-muted, --kk-rule, --kk-felt,
--kk-felt-deep, --kk-felt-ink, --kk-accent, --kk-accent-ink,
--kk-good, --kk-bad, --kk-die, --kk-die-edge, --kk-die-ink,
--kk-pip-one, --kk-radius, --kk-font.
All of these are exported constants, and the notation refuses anything past them by name.
| Limit | Value | Constant |
|---|---|---|
| Dice in one roll | 1 to 10 | MIN_DICE, MAX_DICE |
| Dice in one roll, asked for from code | up to 100, all plain | MAX_DICE_BY_CODE |
| Sides of dice that all differ | 100 | MAX_UNIQUE_SIDES |
| A number in a formula | 9999 | MAX_MATH_NUMBER |
| The span of a formula's totals | 1,000,000 | MAX_MATH_TOTALS |
| Kinds of dice in one roll | 4 | MAX_GROUPS |
| Rerolls until clear, for each die | 10 | MAX_REROLLS |
| Faces of a custom die | 2 to 20 | MAX_FACES |
| Characters in a custom face | 16 | MAX_LABEL |
| A custom face's value | −9999 to 9999 | MAX_FACE_VALUE |
| A loaded die | up to 100 sides, weights 0 to 99 | MAX_LOADED_SIDES, MAX_WEIGHT |
| Sets kept on a device | 50, names up to 40 characters | MAX_SETS, MAX_SET_NAME |
| Sides of a die | 2 to 1000, or F |
MIN_SIDES, MAX_SIDES |
| Bonus | −99 to +99 | MAX_MODIFIER |
| Explosions for each die | 10 more dice | MAX_EXPLOSIONS |
| Largest die that may explode | d100 | MAX_EXPLODING_SIDES |
| Times a roll is thrown in one go | 1 to 100 (1 to 10 in the tray) | MAX_TIMES |
| A roll's label | 40 characters | MAX_ROLL_LABEL |
| Rolls kept in a history | 500 | HISTORY_LIMIT |
| Length of notation read | 400 characters |
The tray and typed notation stop at ten dice: that is what fits a felt, and what a table throws. Code may ask for up to a hundred:
const volley = parseNotation("40d6", { maxDice: 100 })!; // null without the option
normalizeSpec({ count: 40, sides: 6 }, { maxDice: 100 }).count; // 40; 10 without it
roll(volley, seededSource("table"), { maxDice: 100 }).faces.length; // 40; 10 without it
expectedTotal(volley); // 140
chanceAtLeast(volley, 150); // 0.1902
exactCounts(volley)!.outcomes; // 6 to the 40th, a whole number of 32 digitsThe one option, maxDice, is the same on all three, and on rollMany. Each
stops at ten without it, as it always has: roll({ count: 50, sides: 6 })
throws ten dice. roll's third argument is the time of the roll, as before,
or { at, maxDice }. On the command line it is --max-dice 100.
Past ten, every kind has to be plain dice: fair numbered or Fate dice, all
added or one kept (100d20kh1), which is where the odds stay exact and quick
however many there are. Anything else past ten is refused by name.
- Every control is a real control. The tray's controls are native buttons, fields and summaries, at least 44 pixels square, and nothing needs a hover or a long press. Space rolls (or a tap on the felt), and a die on the felt can be held from the keyboard.
- Results are spoken. Each result is announced politely as it lands, in a live region, and the dice that will be rolled are said aloud as they change. A die is named by its face and what became of it ("d6: 6, exploded"). Dice War says its status in a polite line and lists the scores as a list.
- Nothing depends on colour alone. A die dropped, held, exploded or counted as a success is marked by shape (faded and struck through, a pin, a mark) as well as by colour, and a loaded die wears a mark everywhere it appears.
- Reduced motion is respected. The tumble and the landing are skipped on a device that asks for less motion, and the sound starts off there too. Nothing on the felt moves when the dice land: the box stays one size.
- Sound is optional and never the only sign. The speaker button mutes it and the choice is remembered; every roll is also written down.
- Light and dark follow the page, and every colour is a custom property (see Theming); the colour pairs have not been measured against WCAG contrast ratios.
- Not yet. The felt's dice are drawn as pictures, so a screen reader hears them through the result line and not die by die on the felt. The Japanese has not been read by a native reader (see Languages).
Any browser from the last few years: it needs ES2020 with BigInt,
crypto.getRandomValues and CSS color-mix (Chrome and Edge 111, Firefox
113, Safari 16.2). The sound needs the Web Audio API and AAC decoding, which
those browsers have; without them the tray is silent or plays its own knock.
It is tested in Chromium and in WebKit, Safari's engine, at phone size with
touch. The core also runs in Node 22 and later, Deno and Bun.
English and Japanese, chosen by locale or the page's lang, or by the
reader where a page turns on languageChooser. The demo has a chooser of its
own, follows the browser's language on a first visit, and takes ?lang=ja or
?lang=en in the address. Japanese:
included; not yet reviewed by a native reader. Corrections welcome. Every
Japanese string is listed beside its English in
docs/strings-ja.md, and there is an issue template
for fixing one. Any other language is a table of your own passed as strings.
- More notation, as tables ask for it (Notation compared keeps the list)
- Standalone executables of the command line, for machines without Node
- The documentation in Japanese
- More games: suggest one
- BCDice's notation, which Japanese tables use, as a candidate
- Ports to other languages are welcome: there is a specification and a conformance suite to write one against
- A hosted HTTP API: not planned, because it needs a server. The command line and the package will cover programs.
- Exploding dice that are kept or dropped, once a table's rule is chosen
Left out on purpose: shared live rooms, which need a server, dice skins for sale, and 3D dice. Korokoro runs from a static page and costs nothing to host.
Ideas and pull requests are welcome.
The core is plain functions over plain data with no DOM and no dependency: dice, notation, exact odds, history and statistics, each in a module of its own, and the command line a pure function too. The tray is a small DOM layer, and the React and Vue components and the custom element are thin wrappers around it, each its own entry point, so a page loads only what it uses. The tabletop games are data: a preset is a line of dice and a named reading. Architecture lists every source file and what it does.
Korokoro (コロコロ) is a Japanese sound-word for something small and round rolling or tumbling along: a die across a table, an acorn down a slope. Japanese has a great many words of this kind, which name a thing by the sound or the feel of it, and this one is the sound of what the package does. Say it in four even beats: ko-ro-ko-ro.
Dice, as it happens, are saikoro (サイコロ) in Japanese, which ends on the same two beats. We make no claim about where either word comes from; it is a pleasant echo.
Korokoro was built for Itsutsu, a site for board games, puzzles, card games and dice games played at your own pace. Itsutsu (五つ) is Japanese for "five", after five in a row, the game the site began with. The site needed dice that were fair and that anybody could check, and once they existed they seemed worth sharing.
- Itsutsu, for its dice.
That is the whole list so far. Using Korokoro in something? Open an Add my project issue and we will add you.
Korokoro is one of twenty-four packages, each made for the same site, each at github.com/johnmorrisdotca. The code of every one is MIT.
- Korokoro (コロコロ): dice, with notation, exact odds, real sounds and the dice of many games. Demo.
- Kyuubu (キューブ): a turning cube for the browser, 2×2 to 7×7, with record solves to replay. Demo.
- Hitotsu (一つ): a colour-card shedding game for two to eight, with the house rules people play. Demo.
- Toranpu (トランプ): a deck of playing cards, card games with computer players, and solitaires. Demo.
- Tane (種): seeded random numbers and daily seeds, the same in every browser and on every server. Demo.
- Narabe (並べ): one rules engine for abstract board games, from gomoku and Reversi to Go and checkers. Demo.
- Tenka (天下): world conquest for two to six, on a map of the real world. Demo.
- Kumimoji (組み文字): a crossword tile race, in English and Japanese kana. Demo.
- Tsunagi (繋ぎ): a line-joining logic puzzle whose every level has exactly one answer. Demo.
- Jarajara (ジャラジャラ): mahjong tiles drawn as SVG, stacked layouts, and the matching solitaire Awase. Demo.
- Suido (水道): a pipe puzzle: turn the pieces until the water reaches every drain. Demo.
- Domino (ドミノ): dominoes and Mexican Train. Demo.
- Kotoba (言葉): word lists and word-game rules in English, French, German and Japanese. Demo.
- Sugoroku (双六): backgammon and its variants, with the doubling cube and match play. Demo.
- Kazu (数): grid number puzzles: Sudoku and its variants, Futoshiki and Skyscrapers. Demo.
- Meikyuu (迷宮): mazes on squares, hexagons, triangles and circles, made from a seed and drawn through with a finger or the mouse. Demo.
- Hikidashi (引き出し): a drawer of small Japanese text tools: era dates, kanji numerals, readings and sentence difficulty. Demo.
- Chizu (地図): maps of the world and of countries' regions, in English and Japanese, with a quiz and callouts. Demo.
- Bushu (部首): find a kanji by the parts it is made of. Demo.
- Tobiishi (飛び石): peg solitaire with nine boards and seeded solvable challenges. Demo.
- Jirai (地雷): minesweeper on shaped grids with verified no-guess boards. Demo.
- Gunjin (軍人): five hidden-rank strategy games with pass-the-device play. Demo.
- Karakuri (からくり): eight hyper-casual puzzle games, some of them physics: draw a shield, pull pins, cut ropes, slide blocks, pour tubes. Demo.
- Houseki (宝石): gem and stone matching puzzles: falling triplets, stone collapse, colour chains and gem swap. Demo.
This package is Korokoro. The demos of all twenty-four share one header and footer, so each links the rest.
pnpm install
pnpm check # lint, types and tests
pnpm test:package # pack it, install it, and use it as published
pnpm test:cli # the command line, as a child process
pnpm test:tray # the demo and the documentation site, built and tapped in real browsers
pnpm test:readme # run every example in this README against the built package
pnpm site # build the demo into ./site, then serve it
pnpm screenshots:readme # take the README's pictures from the built demo, in light and dark
pnpm docs:site # build the documentation siteSee CONTRIBUTING.md; the commands are under Development.
Please follow the code of conduct.
See CHANGELOG.md, and docs/migrating.md for the one change so far that needs a second look: what r means. The latest release, 1.15.2, adds no code: it is this README in full, with pictures of the tray, examples that are run on every change, an Accessibility section, and the long reference material moved to pages under docs/.
MIT © John Morris. The dice recordings are CC0; see SOUNDS.md.