@johnmorrisdotca/korokoro
| Install | |
|---|---|
npm install @johnmorrisdotca/korokoro |
|
| Latest Version: | 1.16.0 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 15, 2026 |
| Links: | GitHub · npm |
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.
Roll in 30 seconds
npm install @johnmorrisdotca/korokoro # or pnpm add, or yarn add
import { 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 more
And 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 495
And 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.
Who it is for
- 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.
Features
- 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.
What's in it
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.
Use it in your project
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.
Install
npm install @johnmorrisdotca/korokoro
pnpm add @johnmorrisdotca/korokoro
yarn add @johnmorrisdotca/korokoro
It 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).
Install under another name
The alias changes the import name in your project; it installs the same package.
npm install dice@npm:@johnmorrisdotca/korokoro
import { roll } from "dice";
1. The API alone
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
}
2. The tray, in plain HTML
<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>
3. React
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").
4. Vue
<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.
5. A web component
<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 |
width |
Pixel width, within the available host space |
theme |
light, dark or auto for the controls |
label |
An accessible name for the dice group |
seed |
The same seed gives the same sequence of throws |
one-pip |
The colour of a d6's one pip: red (unless said) or black; changed in place |
What an attribute cannot carry (custom CSS variables, 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. Every watched attribute is reflected as a property, with camel-case
aliases for hyphenated names. The die's existing numeric face getter remains,
and die.face = 5 still sets the face attribute. 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.
6. Svelte and Angular
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.
What a developer gets
- 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.
Examples
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.
A tray on a page with no script of your own
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>
Roll with advantage, and know the odds
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
Six ability scores in one go
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
A dice pool, counted
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
What the notation refuses, and why
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
Dice of your own, and loaded dice that say so
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
A game's dice, read the way the game reads them
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
Hold some dice and roll the rest
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 ]
Dice War, played from a seed
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
Export a history, and share a roll
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
From a terminal
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+3
2d20kh1+5: 24 [19 (12)]
4d6kh3: 8 [1 2 (1) 5]
4d6kh3: 12 [4 6 (1) 2]
…
One die on a page
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>
A look of your own
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" } });
Dice notation
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.
What a roll returns
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.
Custom dice
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.
Loaded dice, and testing a die
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.
Sets of dice
A set is a roll with a name, kept on the device and shared by a link. More.
Several rolls in one go
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.
Games
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.
Dice War
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.
Holding dice
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.
Odds
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.
The command line
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.
Export
A history is written out three ways: CSV for a spreadsheet, JSON that reads back in, and plain text. More.
Dice from an address
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.
Seeded and shared rolls
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.
One die on its own
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.
Embed it on any site
The shared builder previews a tray or one die and writes code for your page. The tray fits an iframe or one tag, in three sizes. Embedding has the address and the sizes.
Sound
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 sound
Dice sounds from Kenney's Casino Audio, CC0, kenney.nl. SOUNDS.md names the files and what was done to them.
API
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.
Entry points
| 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 |
The calls to learn first
| 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 |
Theming
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.
Limits
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 |
More than ten dice
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 digits
The 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.
Accessibility
- 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).
Browser support
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.
Languages
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.
Roadmap
- 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.
Architecture
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.
The name
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.
Where it comes from, and where it is used
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.
Used by
- 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.
The family
Korokoro is one of thirty-two packages, each made for the same site, each at github.com/johnmorrisdotca or github.com/spxis. The code of every one is MIT.
Games — board
- Domino (ドミノ): dominoes and Mexican Train. Demo.
- Gunjin (軍人): five hidden-rank strategy games with pass-the-device play. Demo.
- Narabe (並べ): one rules engine for abstract board games, from gomoku and Reversi to Go and checkers. Demo.
- Sugoroku (双六): backgammon and its variants, with the doubling cube and match play. Demo.
- Tenka (天下): world conquest for two to six, on a map of the real world. Demo.
Games — cards & tiles
- Hitotsu (一つ): a colour-card shedding game for two to eight, with the house rules people play. Demo.
- Jarajara (ジャラジャラ): mahjong tiles drawn as SVG, stacked layouts, and the matching solitaire Awase. Demo.
- Kumimoji (組み文字): a crossword tile race, in English and Japanese kana. Demo.
- Toranpu (トランプ): a deck of playing cards, card games with computer players, and solitaires. Demo.
Games — puzzles
- Houseki (宝石): gem and stone matching puzzles: falling triplets, stone collapse, colour chains and gem swap. Demo.
- Jirai (地雷): minesweeper on shaped grids with verified no-guess boards. Demo.
- Karakuri (からくり): eight hyper-casual puzzle games, some of them physics: draw a shield, pull pins, cut ropes, slide blocks, pour tubes. Demo.
- Kazu (数): grid number puzzles: Sudoku and its variants, Futoshiki and Skyscrapers. Demo.
- Kyuubu (キューブ): a turning cube for the browser, 2×2 to 7×7, with record solves to replay. Demo.
- Meikyuu (迷宮): mazes on squares, hexagons, triangles and circles, made from a seed and drawn through with a finger or the mouse. Demo.
- Suido (水道): a pipe puzzle: turn the pieces until the water reaches every drain. Demo.
- Tobiishi (飛び石): peg solitaire with nine boards and seeded solvable challenges. Demo.
- Tsunagi (繋ぎ): a line-joining logic puzzle whose every level has exactly one answer. Demo.
Games — dice & random
- Korokoro (コロコロ): dice, with notation, exact odds, real sounds and the dice of many games. Demo.
- Tane (種): seeded random numbers and daily seeds, the same in every browser and on every server. Demo.
Language
- Bushu (部首): find a kanji by the parts it is made of. Demo.
- Hikidashi (引き出し): a drawer of small Japanese text tools: era dates, kanji numerals, readings and sentence difficulty. Demo.
- Kotoba (言葉): word lists and word-game rules in English, French, German and Japanese. Demo.
Geography & data
- address-plus (アドレスプラス): parse and format addresses in the United States, Canada, Japan, Australia, France, Germany and the United Kingdom. Demo.
- Chizu (地図): maps of the world and of countries' regions, in English and Japanese, with a quiz and callouts. Demo.
- Hata (旗): country and regional flags as SVG, at their true proportions and keyed by ISO code. Demo.
- Kuni (国): countries and their regions, with ISO codes and names in English and Japanese. Demo.
Sports
- Dougu Sports (道具): sports emblems as SVG: trophies, medals, crests, and the ball and gear of every sport, with optional 3D turning. Demo.
- Kyogijo Stadiums (競技場): sports fields, rinks, courts and ballparks drawn to scale as SVG, with each team and the score on them. Demo.
- Senshu Jerseys (選手): sports jerseys, sweaters, kits, caps, helmets and retired-number banners as SVG, front and back, with optional 2.5D turning. Demo.
- Taisen Brackets (対戦): a sport-agnostic tournament bracket, drawn as SVG, from a list of ties. Demo.
Developer tools
- REST in Pieces (レスト・イン・ピーシーズ): seeded test data and REST APIs for frontend development, with latency and error drills. Demo.
This package is Korokoro. The demos of all thirty-two share one header and footer, so each links the rest.
Development
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 site
pnpm docs:check # type-check and run every public API and guide example
pnpm size --write # refresh the measured entry-point sizes
Contributing
See CONTRIBUTING.md; the commands are under Development.
Please follow the code of conduct.
Changes
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.16.0, adds the embed builder, the standard attributes and properties of the tray and die elements, and the documentation site with a checked example for every export.
Licence
MIT © John Morris. The dice recordings are CC0; see SOUNDS.md.
Related Packages
Jarajara ジャラジャラ — Mahjong tiles and mahjong solitaire: the 144-tile set in...
Embed Querri views in your application. Core SDK + React, Vue, Svelte, and Angul...
Dependency-free, accessible, framework-agnostic clock-face time picker and Web C...
Microsoft's animated Fluent emojis as one component. They play on load or hover,...
Accessible (WCAG 2.2 AA / ATAG 2.0), RTL-first, CSP-safe rich text editor for an...
Version History
| Version | Released | License |
|---|---|---|
| 1.16.0 | MIT | |
| 1.15.2 | MIT | |
| 1.15.1 | MIT | |
| 1.15.0 | MIT | |
| 1.14.1 | MIT | |
| 1.14.0 | MIT | |
| 1.13.0 | MIT | |
| 1.12.0 | MIT | |
| 1.11.0 | MIT | |
| 1.10.0 | MIT | |
| 1.9.0 | MIT | |
| 1.8.0 | MIT | |
| 1.7.0 | MIT | |
| 1.6.0 | MIT | |
| 1.5.0 | MIT | |
| 1.3.0 | MIT | |
| 1.0.0 | MIT |