The colour-card game, with the house rules people actually play.
Match the colour or the number, and call Hitotsu with one card left. Two to eight players, a computer for any seat, a table for several devices, and a deck of its own.
The demo on a desk: a classic game for four, three turns in. |
On a phone, in Japanese, in the device's light or dark. |
Hitotsu means "one" in Japanese: the call a player makes with one card left. It began as the colour-card game on Itsutsu, a site for board and table games, which uses this package for its rules, its computer player, its tables on several devices and its cards.
npm install @johnmorrisdotca/hitotsu # or pnpm add, or yarn addimport { HITOTSU_PARTY, mountHitotsu } from "@johnmorrisdotca/hitotsu";
mountHitotsu(document.getElementById("table")!, { rules: HITOTSU_PARTY });That is a whole table: you and three computers, with the party rules. Or with no script of your own:
<script type="module" src="https://cdn.jsdelivr.net/npm/@johnmorrisdotca/hitotsu@1/dist/element-define.js"></script>
<hitotsu-table rules="party" computers="3"></hitotsu-table>- Anybody putting a card game on a page. The rules, the deal, the turn order, the scoring and a computer player are done, and so is a table to play it at. You can take all of it, or only the rules and draw your own.
- Games sites and apps. A table for several devices checks what arrives over the wire, and a saved game cannot hold a position the rules would not reach.
- Bots and research. Pure functions over plain data: play a thousand games in a loop, and replay any one from its seed.
- Teaching. The rules are a few hundred lines you can read, with their tests beside them.
- The familiar game. Numbers in four colours, Skip, Reverse, Draw Two, Wild and Wild Draw Four; the call with one card left, and two cards for forgetting it. Scored by the cards left in the other hands, to 200 or 500 points, or a single hand.
- The house rules, as options. Stacking draw cards (the same card, or any
on any), jump-in, sevens and zeros, draw until you can play, and the Wild
Draw Four challenged or played without bluffing.
HITOTSU_CLASSICandHITOTSU_PARTYare ready-made; mix your own. - Pure and replayable. Every function takes a game and returns a new one. A game is its set-up, a seed and its moves, kept as one line of text and read back by playing the moves again through the rules, so a saved game can never hold a position the rules would not reach.
- A computer player that stacks rather than takes, challenges a big hand's Wild Draw Four, saves its wilds, never bluffs and always calls. It sees only what a player at the table could.
- Ready for several devices.
readTableSetUp,startTableandreadTableMovecheck what arrives over the wire, and jumping in, a race no server can referee fairly, is taken off. - A deck of its own, drawn as SVG: each colour carries one of the five elements in its corners (火 fire, 土 earth, 木 wood, 水 water), so no card is told by colour alone.
- A table for any page, in plain DOM, as a
<hitotsu-table>tag, and as React components for the table and the cards. Light and dark, and themeable. - English and Japanese, at the table and on the command line, following the
page's
lang. - Card sounds, optional: cards dealt, played and shuffled, from real recordings, fetched only when the first one plays.
- A command line,
hitotsu, that deals a game, lets computers play it out and reads a saved game back through the rules.
Each picture is the real table, drawn by the package and taken from the demo with pnpm screenshots:readme, in light and dark.
The table. The seats, the stock and the pile, and your hand with the cards you may play glowing. |
Up to eight players. You and seven computers; the seats wrap on a narrow screen. |
Party mode. Stacking draw cards, jumping in, and sevens and zeros; a wild asks for its colour. |
The deck. Four colours with an element in the corners, so no card is told by colour alone, and the wilds. |
The options. Classic or party rules, one to seven computers, and sound. |
The code behind the table. The panel shows the tag, the script and the command line that make the game on the screen. |
npm install @johnmorrisdotca/hitotsu
# or: pnpm add @johnmorrisdotca/hitotsu
# or: yarn add @johnmorrisdotca/hitotsuEvery version is also a GitHub release with the built package attached, if you would rather install that file by its address.
ES modules with TypeScript types. The core and the table have no
dependencies; the React components need React 18 or later. A page with no
bundler loads the tag from a CDN (@1 is the major version).
<script type="module" src="https://cdn.jsdelivr.net/npm/@johnmorrisdotca/hitotsu@1/dist/element-define.js"></script>
<hitotsu-table rules="party" computers="5" sound></hitotsu-table>
<hitotsu-table lang="ja" players="あなた,アキ,ベン"></hitotsu-table>| Attribute | What it does |
|---|---|
rules |
classic (the default) or party |
computers |
how many computers sit with you, 1 to 7 |
players |
the names at the table, comma-separated, seat 0 first (yours); two to eight of them |
size |
1 for a single hand (the default), 200 or 500 points |
seed |
the number the deals are shuffled from; a fresh one if not given |
lang |
ja for Japanese; the page's language unless said |
sound |
card sounds, when present (sound="off" turns them off again) |
Changing an attribute deals again. Every move is announced as a hitotsu-move
event carrying the game in detail, and the element has a game property and
a deal() method. The script above is @johnmorrisdotca/hitotsu/element/define
(@johnmorrisdotca/hitotsu/element-define is the same file under its first name),
which registers the tag by being imported; @johnmorrisdotca/hitotsu/element
exports the same class and registers nothing until you call defineHitotsuTable(). Any framework that
passes unknown tags through will carry it; Vue needs to be told that tags
starting hitotsu- are custom elements (isCustomElement), and Angular needs
CUSTOM_ELEMENTS_SCHEMA.
<div id="table"></div>
<script type="module">
import { HITOTSU_PARTY, mountHitotsu } from "@johnmorrisdotca/hitotsu";
mountHitotsu(document.getElementById("table"), {
players: ["You", "Aki", "Ben", "Cy"], // seat 0 is you, the rest computers
rules: HITOTSU_PARTY,
sound: true,
onMove: (game) => console.log(game.news),
});
</script>import { HITOTSU_CLASSIC } from "@johnmorrisdotca/hitotsu";
import { HitotsuCardImage, HitotsuTable } from "@johnmorrisdotca/hitotsu/react";
export function Table() {
return <HitotsuTable rules={{ ...HITOTSU_CLASSIC, stacking: "any" }} size={500} />;
}
export const RedFive = () => <HitotsuCardImage card="R50" width={80} />;HitotsuTable mounts in the browser after the first render, so server
rendering draws an empty box and nothing needs a provider.
import { HITOTSU_PARTY, hitotsuComputer, hitotsuMoves, hitotsuWinners, playHitotsu, startHitotsu } from "@johnmorrisdotca/hitotsu";
let game = startHitotsu(1, ["Ann", "Ben", "Cy"], 42, HITOTSU_PARTY)!; // one hand, seed 42
hitotsuMoves(game); // every legal move for the player to move
game = playHitotsu(game, { draw: true })!; // null for a move the rules refuse
while (game.phase === "playing") game = playHitotsu(game, hitotsuComputer(game))!;
hitotsuWinners(game); // [1]Vue needs to be told that tags starting hitotsu- are custom elements, which is a compiler option; after that the tag is used as it is, and its event is a native one.
<script setup>
import "@johnmorrisdotca/hitotsu/element/define"; // registers <hitotsu-table> by being imported
function moved(event) {
console.log(event.detail.news); // what just happened, as data
}
</script>
<template>
<hitotsu-table rules="party" computers="3" @hitotsu-move="moved"></hitotsu-table>
</template>// vite.config.js: tell Vue that hitotsu-* tags are custom elements
import vue from "@vitejs/plugin-vue";
export default { plugins: [vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith("hitotsu-") } } })] };Svelte passes unknown tags through, and on: listens to a custom event by its name.
<script>
import "@johnmorrisdotca/hitotsu/element/define";
let news = "";
const moved = (event) => { news = event.detail.news; };
</script>
<hitotsu-table rules="classic" computers="2" on:hitotsu-move={moved}></hitotsu-table>
<p>{JSON.stringify(news)}</p>Angular needs CUSTOM_ELEMENTS_SCHEMA for a tag it does not know.
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import "@johnmorrisdotca/hitotsu/element/define";
@Component({
selector: "app-table",
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `<hitotsu-table rules="party" computers="3" (hitotsu-move)="moved($event)"></hitotsu-table>`,
})
export class TableComponent {
moved(event: Event) { console.log((event as CustomEvent).detail.news); }
}Each example is a whole recipe: copy it and it works. The ones in TypeScript and JavaScript 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.
Save this as a file and open it. The tag registers itself when its module is imported, deals a game of four with the party rules, and plays it: you, and three computers.
<!doctype html>
<meta charset="utf-8">
<title>Hitotsu</title>
<script type="module" src="https://cdn.jsdelivr.net/npm/@johnmorrisdotca/hitotsu@1/dist/element-define.js"></script>
<hitotsu-table rules="party" computers="3" seed="2026" sound></hitotsu-table>Every move is announced as a hitotsu-move event carrying the game in detail, so a page can keep a log, show a score of its own or send the game to a server.
<hitotsu-table id="table" rules="classic" computers="2"></hitotsu-table>
<ol id="log"></ol>
<script type="module">
import "https://cdn.jsdelivr.net/npm/@johnmorrisdotca/hitotsu@1/dist/element-define.js";
document.getElementById("table").addEventListener("hitotsu-move", (event) => {
const item = document.createElement("li");
item.textContent = JSON.stringify(event.detail.news); // what just happened, as data
document.getElementById("log").append(item);
});
</script>mountHitotsu takes an element and the choices, and gives back the game and a way to start again. Everything the table says can be replaced, and its colours are CSS variables.
import { HITOTSU_PARTY, mountHitotsu } from "@johnmorrisdotca/hitotsu";
const table = mountHitotsu(document.querySelector<HTMLElement>("#table")!, {
players: ["You", "Aki", "Ben", "Cy"], // seat 0 is you, the rest are computers
rules: HITOTSU_PARTY,
size: 200, // play to 200 points, not a single hand
language: "ja",
strings: { again: "もう一局" }, // one of the table's words, changed
theme: { "--ht-felt": "#234" }, // the table's own colours
onMove: (game) => console.log(game.phase, game.scores),
});
table.restart(); // deal again with the same choices
table.destroy(); // when the page lets it goThe rules are pure functions over plain data: a game goes in, a new one comes out, and hitotsuComputer is a move for whoever is to play. This is the game the command line plays for seed 42 with three players and the party rules.
import { HITOTSU_PARTY, hitotsuComputer, hitotsuWinners, playHitotsu, startHitotsu } from "@johnmorrisdotca/hitotsu";
let game = startHitotsu(1, ["Ann", "Ben", "Cy"], 42, HITOTSU_PARTY)!; // one hand
let moves = 0;
while (game.phase === "playing") {
game = playHitotsu(game, hitotsuComputer(game))!;
moves += 1;
}
console.log(moves, "moves; winners:", hitotsuWinners(game), "scores:", game.scores);28 moves; winners: [ 1 ] scores: [ 0, 71, 0 ]
For a bot, a balance check or research, play a lot of hands: each is a seed, and any one of them can be replayed. Here, 200 hands of four computers on the classic rules.
import { HITOTSU_CLASSIC, hitotsuComputer, hitotsuWinners, playHitotsu, startHitotsu } from "@johnmorrisdotca/hitotsu";
const wins = [0, 0, 0, 0];
for (let seed = 1; seed <= 200; seed += 1) {
let game = startHitotsu(1, ["a", "b", "c", "d"], seed, HITOTSU_CLASSIC)!;
while (game.phase === "playing") game = playHitotsu(game, hitotsuComputer(game))!;
for (const seat of hitotsuWinners(game)) wins[seat]! += 1;
}
console.log("hands won by seat:", wins);hands won by seat: [ 58, 51, 55, 36 ]
The two ready-made sets are HITOTSU_CLASSIC and HITOTSU_PARTY; any mix is an object. startHitotsu is null for a table outside the limits.
import { HITOTSU_CLASSIC, HITOTSU_PARTY, startHitotsu } from "@johnmorrisdotca/hitotsu";
console.log(HITOTSU_CLASSIC);
const mine = { ...HITOTSU_CLASSIC, stacking: "same" as const, drawToMatch: true }; // stack the same card, and draw until you can play
const game = startHitotsu(200, ["Ann", "Ben"], 7, mine)!;
console.log(game.options.stacking, game.options.drawToMatch, game.hands.map((hand) => hand.length), HITOTSU_PARTY.deal);
console.log(startHitotsu(1, ["only one"], 1), startHitotsu(7, ["Ann", "Ben"], 1)); // outside the limits: null{
stacking: 'off',
jumpIn: false,
sevenZero: false,
drawToMatch: false,
wildFour: 'challenge',
deal: 7
}
same true [ 7, 7 ] 5
null null
A game is its set-up, a seed and its moves. Reading it back plays every move through the rules again, so a saved game can never hold a position the rules would not reach, and one changed by hand is null.
import { HITOTSU_PARTY, decodeHitotsu, encodeHitotsu, hitotsuComputer, playHitotsu, startHitotsu } from "@johnmorrisdotca/hitotsu";
let game = startHitotsu(1, ["Ann", "Ben", "Cy"], 42, HITOTSU_PARTY)!;
for (let move = 0; move < 5; move += 1) game = playHitotsu(game, hitotsuComputer(game))!;
const kept = encodeHitotsu(game);
console.log(kept.length, "characters;", decodeHitotsu(kept)?.moves.length, "moves read back");
console.log(decodeHitotsu(kept.replace('"seed":42', '"seed":43'))); // a changed seed no longer makes these moves: null323 characters; 5 moves read back
null
On a server, a game for friends each on their own phone needs three checks: the set-up the host sent, a move a player sent, and whose turn it is. readTableSetUp takes jumping in off, because a race between two phones is one no server can referee fairly.
import { HITOTSU_PARTY, readTableMove, readTableSetUp, startTable, tableToPlay } from "@johnmorrisdotca/hitotsu";
const setUp = readTableSetUp({ seed: 5, options: HITOTSU_PARTY }); // what arrived over the wire, checked
console.log(setUp?.options.jumpIn); // false: taken off, though the party rules have it on
const game = startTable(1, 3, setUp)!; // a game for three seats
console.log(game.phase, tableToPlay(game)); // whose seat is to move
console.log(readTableMove({ draw: true }), readTableMove({ jump: "R50", seat: 1 }), readTableMove("junk"));false
playing 0
{ draw: true } null null
The deck is drawn once, as shapes, and every drawing of it (the SVG here, the table, React's HitotsuCardImage) comes from those. Each colour carries its element in the corners, so no card is told by colour alone. hitotsuWords names a card for a screen reader.
import { hitotsuCardSvg, hitotsuWords } from "@johnmorrisdotca/hitotsu";
console.log(hitotsuWords("R50", "en"), "|", hitotsuWords("R50", "ja"), "|", hitotsuWords("GD0", "en"));
const svg = hitotsuCardSvg("R50", { width: 80 });
console.log(svg.startsWith("<svg"), svg.includes('width="80"'));red five | 赤の5 | green draw two
true true
hitotsu deals a game, lets computers play it out and reads a saved game back through the rules. The same seed prints the same cards on every machine.
npx @johnmorrisdotca/hitotsu deal --seed 42 --players 2
npx @johnmorrisdotca/hitotsu play --seed 42 --players 3 --rules party
npx @johnmorrisdotca/hitotsu play --seed 42 --players 3 --rules party --saved > game.json
npx @johnmorrisdotca/hitotsu check game.jsonSeat 1: R3 R5 YD G0 G6 BD WF
Seat 2: R0 G2 B1 B2 B8 BS WF
On the pile: Y7
Hitotsu for 3, party rules, seed 42: 28 moves. Won by Seat 2.
Scores: 0 71 0
Hitotsu for 3, seed 42, 28 moves. Over: won by Seat 2.
sound: true plays a card dealt, a card played and the deck shuffled, from real recordings fetched when the first one plays. For your own control, createCardSounds gives a sound you can mute and set the volume of.
import { createCardSounds } from "@johnmorrisdotca/hitotsu/card-sounds";
import { mountHitotsu } from "@johnmorrisdotca/hitotsu";
const sound = createCardSounds({ muted: true, volume: 0.5 });
mountHitotsu(document.querySelector<HTMLElement>("#table")!, { sound });
document.querySelector("#unmute")!.addEventListener("click", () => sound.setMuted(false)); // a browser lets a page make sound only after a touch| Rule | Classic | Party | Option |
|---|---|---|---|
| Cards dealt | 7 | 5 | deal: 7 | 5 |
| Stacking draw cards | off | any on any | stacking: "off" | "same" | "any" |
| Jump-in (an identical card, out of turn) | off | on | jumpIn |
| Sevens and zeros (a 7 swaps hands, a 0 passes every hand on) | off | on | sevenZero |
| Drawing | one card | one card | drawToMatch |
| Wild Draw Four | may be challenged | may be challenged | wildFour: "challenge" | "strict" |
The length is separate: startHitotsu(size, …) with 200 or 500 points, or 1
for a single hand. A card left in a hand scores its number, an action card 20
and a wild 50. A hand nobody can finish, with the stock and pile spent and
everybody passing, goes to whoever holds the fewest points.
A card is a short name: its colour (R, Y, G, B, or W for a wild),
its face (a digit, S skip, R reverse, D draw two, W wild, F wild draw
four) and which copy it is. The two red fives are R50 and R51.
npx @johnmorrisdotca/hitotsu deal --seed 42 # the hands, and the card to start the pile
npx @johnmorrisdotca/hitotsu play --seed 42 --players 3 --rules party
npx @johnmorrisdotca/hitotsu play --seed 42 --saved > game.json
npx @johnmorrisdotca/hitotsu check game.json # read it back through the rules| Command or option | What it does |
|---|---|
deal |
prints each seat's hand and the card the pile starts with |
play |
computers play a whole game; prints who won and the scores |
check <file> |
reads a saved game back through the rules, from a file, from --stdin, or given as the text itself; exits 1 if the rules cannot play it out |
-s, --seed N |
the number the deals are shuffled from; a fresh one is named on standard error if not given |
-p, --players N |
how many play, 2 to 8 (4 if not given) |
--rules R |
classic (the default) or party |
--size N |
1 for a single hand (the default), 200 or 500 points |
--saved |
play: prints the saved game and nothing else |
--stdin |
check: reads the saved game from standard input |
-j, --json |
prints JSON |
--lang L |
en or ja |
The exit code is 0 when all went well, 1 when what was asked for could not be done and 2 when the command itself was wrong. The same seed prints the same cards on every machine.
A game is its set-up, a seed and its moves, kept as one line of text:
encodeHitotsu(game) and decodeHitotsu(text). Reading it back plays every
move again through the rules, so a saved game can never hold a position the
rules would not reach, and a game that was changed by hand reads as null.
readHitotsuOptions and readHitotsuMove check what arrives over the wire,
and the table functions under the API below run a game on several devices.
Off unless asked: sound: true (or the sound attribute) plays a card dealt,
a card played and the deck shuffled, from real recordings. Nothing is fetched
until the first sound plays, and where a browser cannot play them a short sound
made in the page stands in. For your own control, createCardSounds from
@johnmorrisdotca/hitotsu/card-sounds gives a sound you can mute, set the
volume of and hand to sound.
The recordings are Kenney's Casino Audio, CC0, and CREDITS.md says which, with where each came from and the day its licence was checked.
The API reference lists every export of every entry point with its signature and its doc comment. It is made from the source by pnpm site, so it cannot fall behind the code.
Every function is pure, and every type is exported.
startHitotsu(size, players, seed?, options?, computers?): HitotsuGame | null
hitotsuMoves(game): HitotsuMove[] // the player to move's legal moves
hitotsuJumpIns(game): HitotsuMove[] // cards other seats may jump in with
playHitotsu(game, move): HitotsuGame | null
hitotsuWinners(game): number[]
hitotsuTop(game), hitotsuPlayable(game), hitotsuMatches(game, card)
type HitotsuMove =
| { play: HitotsuCard; colour?: HitotsuColour; swap?: number; call?: boolean }
| { draw: true } | { pass: true } | { take: true } | { challenge: true }
| { jump: HitotsuCard; seat: number; colour?: HitotsuColour; swap?: number; call?: boolean };movesFor(game, seat) // its turn's moves, or its jump-ins at another's turn
playableFor(game, seat) // the cards it may put down now
callMatters(game, seat) // whether calling Hitotsu! is a choice now
waysFor(game, seat, card, call) // each way that card may go down
quickMove(game, seat, card, call) // the one way, or null if there is a colour or a seat to choosehitotsuComputer(game): HitotsuMove // for the player to move
hitotsuComputerJump(game): HitotsuMove | null // a computer seat jumping inencodeHitotsu(game): string
decodeHitotsu(text): HitotsuGame | null // replays every move through the rules
readHitotsuOptions(sent), readHitotsuMove(sent)
readTableSetUp(sent): { seed, options } | null // jumping in taken off
startTable(size, count, sent, computers?): HitotsuGame | null
readTableMove(sent): HitotsuMove | null // never a jump
tableToPlay(game), tableComputerMove(game, seat), namedTable(game, names)HITOTSU_DECK, shuffledHitotsu(seed, hand), sortHitotsu(hand)
colourOf(card), faceOf(card), isWild(card), hitotsuPoints(card), hitotsuWords(card)
hitotsuCardSvg(card | null, { width?, called?, title? }): string
hitotsuCardShapes(card | null, called?): HitotsuShape[]
HITOTSU_COLOUR_LOOK, HITOTSU_FACE_MARKhitotsuCardShapes is the design, once: hitotsuCardSvg, the table and the
React HitotsuCardDrawing all draw it.
mountHitotsu(element, {
players?, rules?, size?, seed?,
language?, // "en" or "ja"; the page's language if not given
computerMs?, // how long a computer thinks: your window to jump in
strings?, // your own words, over the language's
sound?, // true, or a createCardSounds() of your own
theme?, // CSS variables, such as { "--ht-felt": "#234" }
onMove?,
}): { game(), restart(options?), destroy() }
hitotsuStrings(language), hitotsuLanguageOf(tag), HITOTSU_STRINGS, HITOTSU_STRINGS_JA
defineHitotsuTable() // registers <hitotsu-table>, from "/element"
createCardSounds({ muted?, volume? }) // from "/card-sounds"| Import | What it holds |
|---|---|
@johnmorrisdotca/hitotsu |
The rules, the deck, the computer player, saved games, the table functions, the card's drawing and mountHitotsu |
@johnmorrisdotca/hitotsu/element |
The <hitotsu-table> class and defineHitotsuTable(), which registers nothing until called |
@johnmorrisdotca/hitotsu/element/define |
Registers <hitotsu-table> by being imported (/element-define is the same file under its first name) |
@johnmorrisdotca/hitotsu/react |
HitotsuTable, HitotsuCardImage and HitotsuCardDrawing |
@johnmorrisdotca/hitotsu/card-sounds |
createCardSounds: the card sounds, to mute or hand to a table |
| Call | What it does |
|---|---|
startHitotsu(size, players, seed, options, computers) |
A new game, or null outside the limits |
hitotsuMoves(game) |
Every legal move for the player to move |
playHitotsu(game, move) |
The game after a move, or null for a move the rules refuse |
hitotsuComputer(game) |
A move for the player to move |
encodeHitotsu(game) and decodeHitotsu(text) |
A game as one line of text, and back |
mountHitotsu(element, options) |
The table, in plain DOM |
Every colour is a CSS variable on .ht-root: --ht-surface, --ht-ink,
--ht-muted, --ht-rule, --ht-felt, --ht-felt-deep, --ht-felt-ink,
--ht-accent, --ht-accent-ink, --ht-playable, --ht-radius, --ht-card
and --ht-font. Pass them as theme when you mount, or set them in a rule on
.ht-root, such as hitotsu-table .ht-root { --ht-felt: #234; }. The demo
passes the family's cloth by reference, so a cloth chosen in its header changes
the felt at once.
| Limit | Value | Constant |
|---|---|---|
| Players | 2 to 8 | HITOTSU_MOST_PLAYERS |
| How long a game lasts | 1 (a single hand), 200 or 500 points | HITOTSU_SIZES |
| Cards in the deck | 108 | HITOTSU_DECK_SIZE |
| Cards dealt | 7, or 5 in party mode | deal in the options |
| Cards taken for forgetting to call | 2 | HITOTSU_CAUGHT |
| Cards taken for a Wild Draw Four challenged and found honest | 6 | HITOTSU_CHALLENGE_LOST |
| A seed on the command line | a whole number from 1 to 2,147,483,647 | SEED_MOST |
startHitotsu returns null for anything outside them, and playHitotsu
returns null for a move the rules refuse. The command line takes the same
limits.
- No card is told by colour alone. Each colour carries one of the five elements in the card's corners (火 fire, 土 earth, 木 wood, 水 water, and 五 for a wild), and a face has its own mark (⊘ skip, ⇄ reverse, +2, +4), so a player who cannot tell red from green can still read the card.
- A screen reader hears every card and every turn. Each card in the hand is a real
buttonlabelled with its name ("red five", 赤の5), and a card that may not be played is disabled. The line that says whose turn it is and what is wanted is anaria-live="polite"region, so the game is followed without moving focus; what just happened ("Computer 1 took 2.") is a plain line under it, which a screen reader finds by reading on, and is not announced by itself. - The keyboard. The whole table is native buttons: Tab to a card, the draw stock, the call of Hitotsu or a colour, Enter or Space to press it.
- Touch targets and fit. The table fits a phone at 390 pixels, with a hand that wraps rather than scrolls sideways.
- Reduced motion. The table's one transition, a card lifting when it may be played, is off under
prefers-reduced-motion. - Sound is optional. Off unless asked, never the only sign of anything: every move is also a line of text.
- Light and dark follow the page, and every colour is a CSS variable (see Theming).
- Not yet. The colour pairs have not been measured against WCAG contrast ratios, a computer takes 900 milliseconds to move (
computerMschanges it) and a person cannot skip the wait, and the Japanese has not been read by a native reader (see Languages).
Any browser from the last few years: the core and the table need ES2020, and
the table CSS custom properties and aspect-ratio (Chrome and Edge 88,
Firefox 89, Safari 15). The demo's tests run in Chromium and WebKit, Safari's
engine. The core has no DOM and no platform needs at all, so it runs the same
in Node, Deno, Bun, a worker or a server function; the package is tested in
Node 22 and 24, on Linux, macOS and Windows, installed from the tarball npm
makes.
The table speaks English and Japanese. It follows the page's lang (on the
element, an ancestor or the document) unless you say language: "ja" when you
mount it, and the <hitotsu-table> tag redraws when <html lang> changes, as
a language chooser does. Card names read to a screen reader follow it too: "red
five", or 赤の5. The command line takes --lang, or the environment's
language.
import { HITOTSU_STRINGS_JA, hitotsuStrings, mountHitotsu } from "@johnmorrisdotca/hitotsu";
mountHitotsu(table, { language: "ja" }); // the Japanese words
mountHitotsu(table, { language: "ja", strings: { again: "もう一局" } }); // one of them changed
hitotsuStrings("ja") === HITOTSU_STRINGS_JA; // trueJapanese: 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 house rules: seven-card Draw, No Mercy's bigger draw cards, and forced play
- A second computer player that counts cards
- A deal animation
- Another language: a table of your own works today, and a real one is a native reader's to write
Ideas and pull requests are welcome.
The rules, the computer player and the saved-game format are plain functions
over plain data with no DOM and no dependency: a game is a value, and every
move returns the next one. The table is drawn by a small DOM layer under
ui/, kept apart so a server or a test can use the rules alone, and the card's
look is decided in one place (card.ts) that plain DOM and React both draw
from.
src/
├── card-sounds.ts the "/card-sounds" entry: the table's card sounds, to mute or hand to a table
├── card.ts the deck's own design as shapes: one place decides how a card looks
├── cli.ts the command line as a pure function, with its words in English and Japanese
├── codec.ts a game as text and back: its table, its seed and every move, played again through the rules
├── computer.ts the computer player, which sees only what a person at the table sees
├── constants.ts how long a game lasts, its "size": 200 or 500 points, or a single hand
├── deck.ts the 108-card deck as short names, and how a card is read
├── element-define.ts the "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/element/define" entry (also "/element-define"): registers <hitotsu-table> by being imported
├── element.ts the "/element" entry: the <hitotsu-table> class, and defineHitotsuTable()
├── index.ts the main entry: the rules, the deck, the computer player, the codec, and the table to mount
├── random.ts a seeded source of numbers in [0, 1)
├── react.tsx the "/react" entry: the card's drawing as React elements
├── rules.ts the rules and nothing else: deal, list the legal moves, play one, score a hand
├── seat.ts one seat's view of what it may do, for a table on one device or on several
├── sounds.ts the recordings, as base64, written by `pnpm sounds` from ./sounds
├── table.ts what a server needs to run a game whose players each hold their own phone
├── types.ts the vocabulary of the game: cards, moves, options and a game
├── version.ts the version, held to package.json by a test
└── ui/ the table that draws and plays a game in plain DOM
├── cardSounds.ts the card sounds: the recordings fetched on the first one played, a made sound if they cannot be
├── dom.ts a few lines of DOM building, so the table needs no framework
├── mount.ts the table itself: mounting it on a page, and the options it takes
├── strings.ts every word the table says, in English and Japanese, which a host page can replace
└── style.ts the table's look, injected once per document
Tests sit beside the code they test (*.test.ts). scripts/ builds the demo
and its API reference page, and demo/ is the page published on GitHub Pages.
Hitotsu (一つ) is Japanese for "one", the word for a single thing when you count: "one card" is hitotsu. It is said in three beats, hi-to-tsu. It is also the call at this table, made with one card left in your hand, which is why the game and the package carry it.
It began as the colour-card game on Itsutsu, which uses this package for its rules, its computer player, its tables on several devices and its cards. The demo is the table, set up with a few presses.
- Itsutsu, a site for board and table games, for its rules, its computer player, its tables on several devices and its cards.
Using it somewhere? Tell us.
Hitotsu 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 Hitotsu. The demos of all twenty-four share one header and footer, so each links the rest.
pnpm install
pnpm check # lint, types and tests: the same as CI
pnpm site # build the demo into ./site, then serve it
pnpm test:demo # build the demo and tap through it in a real browser
pnpm test:cli # the command line, as a child process
pnpm test:package # pack it, install the tarball, and use it as published
pnpm test:readme # run every example in this README against the built package
pnpm screenshots:readme # take the README's pictures from the built demo, in light and darkSee CONTRIBUTING.md. Please follow the code of conduct. Ideas and pull requests are welcome.
See CHANGELOG.md. The latest release, 1.3.2, adds no code: it is this README in full, with pictures of the table, the deck and the options, examples that are run on every change, examples for Vue, Svelte and Angular, and an Accessibility section.
The code is MIT © John Morris. The card sounds are Kenney's Casino Audio, which is CC0: CREDITS.md says which recordings, where each came from and the day its licence was checked, and the package ships it.