@johnmorrisdotca/jarajara
| Install | |
|---|---|
npm install @johnmorrisdotca/jarajara |
|
| Latest Version: | 1.7.0 |
| License: | MIT |
| Last Updated: | Oct 15, 2026 |
| Links: | GitHub · npm |
Jarajara is the tiles; the games are played on them. The first is Awase (合わせ), the matching solitaire many know as Mahjong Solitaire or Shanghai: take the tiles away two at a time, two that match and that are both free, until the layout is clear. It is played at itsutsu.com, which this package was taken out of, and in the demo, with nothing to install.
In 30 seconds
npm install @johnmorrisdotca/jarajara
import { canTake, freePairs, geometryOf, isCleared, layoutFor, takePair } from "@johnmorrisdotca/jarajara";
import { checkAwase, generateAwase } from "@johnmorrisdotca/jarajara/awase";
import { layoutSvg } from "@johnmorrisdotca/jarajara/faces";
const deal = generateAwase(15, "medium", 12345); // the Turtle, 144 tiles, from seed 12345
const geometry = geometryOf(layoutFor(15)!); // what stands on and beside every slot, worked out once
let cells = deal.givens; // a face letter a slot, "." for one taken
const [a, b] = freePairs(geometry, cells, "group")[0]!;
if (canTake(geometry, cells, "group", a, b)) cells = takePair(cells, a, b);
document.querySelector("#board")!.innerHTML = layoutSvg(15, cells, { showFree: true })!;
isCleared(cells); // not yet
checkAwase(15, deal.givens, deal.solution); // { ok: true }: the deal's own answer clears it
And in a page, a game to play, with nothing else to set up:
<script type="module" src="https://cdn.jsdelivr.net/npm/@johnmorrisdotca/jarajara@1/dist/element-define.js"></script>
<jarajara-layout size="15" level="medium" seed="12345" controls timer cloth="green"></jarajara-layout>
Or from a terminal, with no code at all:
npx @johnmorrisdotca/jarajara deal --layout turtle --seed 12345
Who it is for
- Game sites and apps that want tiles with the rules already right: a deal everybody plays alike from a seed, a finished game a server can check by playing it, and the faces drawn as text that goes into any page.
- Anyone making a tile game of their own, who wants the set, the free rule, the layouts and the drawing, and to write only the game.
- Pages that just want the tiles: a tile, a rack to sort and mix, a table of the whole set or a tile looked up by name, as one tag each, in English or Japanese.
- People at a terminal, who want to deal, check or look something up without writing code: the command line.
Features
- The whole set. 144 tiles in 42 faces, each written as one letter, so a layout is a string; names in English and Japanese; hands written in the notation riichi players use (
123m456p). - Thirteen stacked layouts, from a Dragon to a Palace of 576 tiles, each with the geometry of what lies on and beside every slot worked out once. The last two, the Wall (288 tiles) and the Palace (576), are dealt from two and four sets of tiles.
- Awase, always clearable. A deal is laid in reverse from a seed, so every deal can be cleared, at three levels. A finished game is written as text and checked by playing it, with no search, so a server can trust it.
- Options, seven challenges and the day's game. Hints, shuffles and undo given, limited or taken away; a clock and a goal laid over a deal; and
dailyAwase(date), the same game for everybody from the date alone. - Awase at a table of two to four, with a computer in any seat.
- Solid tiles drawn as SVG text, in two designs (Jarajara's own, and FluffyStuff's riichi tiles), with five backs and the family's five cloths; a layout seen from the other side, and Find, which lights every tile that matches the one you point at.
- Seven custom elements:
<jarajara-tile>,<jarajara-rack>,<jarajara-layout>,<jarajara-table>,<jarajara-viewer>,<jarajara-group>and<jarajara-set>, with no framework needed and recipes for React, Vue, Svelte and Angular. - Sounds, off until asked: tile clacks, fetched the first time one plays.
- A command line that deals, checks a solve, gives the day's game, looks tiles up and plays computers at a table.
- English and Japanese, in the tags, the command line and the demo.
- No dependencies, and every function is pure: it returns new values and never changes what it was given.
What's in it
Each picture is the real thing, drawn by the package's own tags and taken from the demo with pnpm screenshots:readme, in light and dark. The deal is the same seed every time.
Rack builder and iframe guide.
Use it in your project
Jarajara is four things, each usable without the others: the rules (the set, the layouts, free tiles, matching, dealing, Awase and the table, as plain functions over strings), the drawing (SVG text), the tags (custom elements that draw and play them in a page) and the command line. The table under API says which entry holds which.
Install under another name
npm install mahjong@npm:@johnmorrisdotca/jarajara
import { MAHJONG_FACES } from "mahjong";
console.log(MAHJONG_FACES.length);
Install
npm install @johnmorrisdotca/jarajara
pnpm add @johnmorrisdotca/jarajara
yarn add @johnmorrisdotca/jarajara
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 tags from a CDN (@1 is the major version).
1. The API alone, on a server
import { generateAwase, checkAwase } from "@johnmorrisdotca/jarajara/awase";
const { givens } = generateAwase(15, "medium", 12345); // send `givens` to the browser; keep the seed and the answer
checkAwase(15, givens, answerFromThePlayer); // { ok: true } or { ok: false, reason }, with no search
Importing the main entry, /awase or /table on a server is safe: they touch no page. So are /faces (it makes strings) and /element (the classes extend nothing where there is no page).
2. One tag, no bundler
<script type="module" src="https://cdn.jsdelivr.net/npm/@johnmorrisdotca/jarajara@1/dist/element-define.js"></script>
<jarajara-tile code="east" size="large" flip></jarajara-tile>
<jarajara-layout id="game" size="15" controls></jarajara-layout>
<script>
document.getElementById("game").addEventListener("jarajara-clear", (event) => console.log(event.detail.moves));
</script>
3. A bundler, and a framework
import "@johnmorrisdotca/jarajara/element/define" once, in code that runs in the browser, and the seven tags are tags like any other. They draw inside a shadow root, speak through bubbling DOM events (jarajara-take, jarajara-clear and the rest, each with a detail), and are set by attributes, each read again when it changes.
// React 19
import { useEffect, useRef } from "react";
import "@johnmorrisdotca/jarajara/element/define";
export function Game({ seed, onCleared }) {
const game = useRef(null);
useEffect(() => {
const listen = (event) => onCleared(event.detail.moves);
game.current?.addEventListener("jarajara-clear", listen);
return () => game.current?.removeEventListener("jarajara-clear", listen);
}, [onCleared]);
return <jarajara-layout ref={game} size="15" level="medium" seed={seed} controls />;
}
<!-- Vue 3: tell the compiler the tags are not Vue components -->
<script setup>
import "@johnmorrisdotca/jarajara/element/define";
defineProps({ seed: Number });
</script>
<template>
<jarajara-layout size="15" level="medium" :seed="seed" controls @jarajara-clear="(event) => console.log(event.detail.moves)" />
</template>
<!-- in vite.config: vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith("jarajara-") } } }) -->
<!-- Svelte 5 -->
<script>
import "@johnmorrisdotca/jarajara/element/define";
let { seed } = $props();
let game;
$effect(() => {
const listen = (event) => console.log(event.detail.moves);
game.addEventListener("jarajara-clear", listen);
return () => game.removeEventListener("jarajara-clear", listen);
});
</script>
<jarajara-layout bind:this={game} size="15" level="medium" seed={seed} controls></jarajara-layout>
// Angular: a standalone component with CUSTOM_ELEMENTS_SCHEMA
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import "@johnmorrisdotca/jarajara/element/define";
@Component({
selector: "app-game",
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `<jarajara-layout size="15" level="medium" seed="12345" controls (jarajara-clear)="cleared($event)"></jarajara-layout>`,
})
export class Game {
cleared(event: Event) { console.log((event as CustomEvent).detail.moves); }
}
In Next.js or any server-rendering framework, import the define entry from a client component, so the tags are defined in the browser.
React, Vue and Svelte set a property on a custom element that has one of the attribute's name, so <jarajara-tile flip> would be tile.flip = true. Four names are methods too (flip on a tile, group and mark on a rack, undo on a layout) and three are read-only values (seed, cells and mirror on a layout): assigning one sets the attribute, and the method still works, so these all do what they say in every framework. pnpm test:frameworks builds a page in each of the four and a plain one from the packed tarball, with a layout, a flippable tile and a listener, opens each in Chromium and WebKit, plays the layout to its end and taps the tile, and e2e/properties.demo.mjs holds the assignments.
What a developer gets
- Typed results, with a doc comment on every export. Every function is pure and returns new values.
- No dependencies. ES modules, an entry per concern, and
sideEffectsset so that only the define entry has an effect. - Where it runs. See Browser support; on a server, Node 22 or later.
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 tags have a complete reference of their own in docs/ELEMENTS.md.
A game on a page with no script of your own
Save this as a file and open it: the Turtle, dealt from a seed, with the buttons, the clock and the line that says how the game stands. The tags are defined when the module is imported, and @1 is the major version.
<!doctype html>
<meta charset="utf-8">
<title>Awase</title>
<script type="module" src="https://cdn.jsdelivr.net/npm/@johnmorrisdotca/jarajara@1/dist/element-define.js"></script>
<jarajara-layout size="15" level="medium" seed="12345" controls timer show-free cloth="green"></jarajara-layout>
<p id="log"></p>
<script>
document.querySelector("jarajara-layout").addEventListener("jarajara-clear", (event) => {
document.getElementById("log").textContent = `Cleared in ${event.detail.moves.length / 4} pairs and ${event.detail.seconds} seconds`;
});
</script>
Deal on a server, and check what comes back
The server makes a deal, sends the tiles to the browser and keeps the answer. When the player sends a finished game, checkAwase plays it on the deal in a few thousand steps, with no search, and says whether it clears the layout and, if not, why.
import { checkAwase, generateAwase } from "@johnmorrisdotca/jarajara/awase";
const deal = generateAwase(4, "easy", 7); // the Tiny layout: 8 tiles, 4 pairs
console.log(deal.givens, deal.solution); // the tiles, a letter a slot; the answer, two slots a pair
console.log(checkAwase(4, deal.givens, deal.solution)); // the deal's own answer clears it
console.log(checkAwase(4, deal.givens, "0001")); // a pair the rules do not allow
console.log(checkAwase(4, deal.givens, "")); // no moves at all
LssIdCCd 0704060500030102
{ ok: true }
{ ok: false, reason: 'a move the rules do not allow' }
{ ok: false, reason: 'tiles are left on the layout' }
Play a deal to the end
Everything the tags do is a function of strings. A deal is cells, one letter a slot; the geometry of what lies on and beside every slot is worked out once; hintFor is the pair that lifts the layers highest; takePair is the next cells.
import { geometryOf, hintFor, isCleared, layoutFor, takePair, tilesLeft } from "@johnmorrisdotca/jarajara";
import { generateAwase } from "@johnmorrisdotca/jarajara/awase";
const geometry = geometryOf(layoutFor(15)!); // the Turtle
let cells = generateAwase(15, "medium", 12345).givens; // 144 tiles
let pairs = 0;
for (let hint = hintFor(geometry, cells, "group"); hint.pair !== null; hint = hintFor(geometry, cells, "group")) {
cells = takePair(cells, hint.pair[0], hint.pair[1]);
pairs += 1;
}
console.log(pairs, "pairs taken;", tilesLeft(cells), "tiles left; cleared:", isCleared(cells));
72 pairs taken; 0 tiles left; cleared: true
Keep a game as text, and play it back
A solve is each pair as its two slots in base 36, and a shuffle as *. decodeMoves reads it, playSolve plays it on a deal, and encodeMoves writes it again.
import { decodeMoves, encodeMoves, playSolve } from "@johnmorrisdotca/jarajara";
import { generateAwase } from "@johnmorrisdotca/jarajara/awase";
const deal = generateAwase(4, "easy", 7);
const moves = decodeMoves(deal.solution, 8)!; // [{ pair: [7, 4] }, { pair: [6, 5] }, …]
console.log(moves.length, "moves;", encodeMoves(moves) === deal.solution);
console.log(playSolve(4, deal.givens, moves)!); // what is left, and how many shuffles it took
console.log(decodeMoves("0g", 8)); // not a pair of slots: null
4 moves; true
{ cells: '........', shuffles: 0 }
null
The game of the day
dailyAwase makes the same game for everybody from the date alone: the layout, the level, the seed and the challenge.
import { dailyAwase } from "@johnmorrisdotca/jarajara/awase";
console.log(dailyAwase("2026-10-01"));
console.log(dailyAwase("2026-10-01").seed === dailyAwase("2026-10-01").seed);
{
date: '2026-10-01',
size: 13,
level: 'hard',
seed: 473803850,
challenge: 'gold'
}
true
Awase at a table, played by computers
Two to four players share one layout and take turns; a computer is a seat with computer: true. Here two seats play a whole game of the Torii, one pair each turn, until the layout is clear.
import { generateAwase } from "@johnmorrisdotca/jarajara/awase";
import { computerPair, playAtTable, readTable, startTable } from "@johnmorrisdotca/jarajara/table";
let table = startTable(generateAwase(8, "medium", 5), [{ name: "Ann", computer: true }, { name: "Ben", computer: true }])!;
let turns = 0;
while (!readTable(table)!.over) {
const pair = computerPair(table)!;
table = playAtTable(table, pair[0], pair[1])!.table;
turns += 1;
}
const result = readTable(table)!;
console.log(turns, "turns; scores", result.scores, "pairs", result.pairs, "winners", result.winners);
32 turns; scores [ 25, 30 ] pairs [ 16, 16 ] winners [ 1 ]
The tiles as SVG text, on a server
The faces are strings, so a server, a build step or an email can draw them. A tile is one <svg>, and a layout is one <svg> with every tile in it, stacked far to near.
import { layoutSvg, tileSvg } from "@johnmorrisdotca/jarajara/faces";
import { generateAwase } from "@johnmorrisdotca/jarajara/awase";
const dragon = tileSvg("F"); // the red dragon, as a whole <svg>
console.log(dragon!.startsWith("<svg"), dragon!.length > 500);
const board = layoutSvg(15, generateAwase(15, "medium", 12345).givens, { showFree: true, cloth: "green" })!;
console.log(board.startsWith("<svg"), (board.match(/data-slot=/g) ?? []).length, "tiles drawn");
true true
true 144 tiles drawn
Look a tile up, and write a hand
Tiles are found by a code, a name in English or Japanese, or the notation riichi players write hands in, and a hand is written back the same way.
import { findFace, readTiles, setInventory, tileName, writeNotation } from "@johnmorrisdotca/jarajara";
console.log(findFace("three circles")?.code, findFace("三筒")?.code, findFace("7z")?.code);
console.log(readTiles("123m456p east plum"));
console.log(writeNotation(["a", "b", "c", "B"]), tileName("B", "ja"), setInventory().length, "faces");
l l F
[ 'a', 'b', 'c', 'm', 'n', 'o', 'B', 'I' ]
123m1z 東 42 faces
Sort, group and mix a hand
arrangeTiles puts a hand in an order, groupTiles splits it, and mixTiles shuffles it from a seed, the same for the same seed.
import { arrangeTiles, groupTiles, mixTiles, readTiles } from "@johnmorrisdotca/jarajara";
const hand = readTiles("5p1m9s7z2m");
console.log(arrangeTiles(hand, "suit").join(""), "|", groupTiles(hand, "kind").map((group) => group.join("")).join(" / "));
console.log(mixTiles(hand, 42).join(""), mixTiles(hand, 42).join("") === mixTiles(hand, 42).join(""));
abnAF | abnA / F
nbAaF true
A rack in a page
A rack lines tiles up in front of you. Give it a hand in notation, let a tap pick tiles up, and call its methods; each answers a promise that settles when the tiles have stopped moving.
<script type="module" src="https://cdn.jsdelivr.net/npm/@johnmorrisdotca/jarajara@1/dist/element-define.js"></script>
<jarajara-rack id="hand" tiles="234m567p3456s11z77z" pick capacity="14" cloth="green"></jarajara-rack>
<button onclick="hand.sort().then(() => hand.group('suit'))">Sort and group</button>
<button onclick="hand.mixUp(7)">Mix up</button>
<script>
hand.addEventListener("jarajara-pick", (event) => console.log(event.detail)); // { index, code, lifted }
</script>
A table of three, in a page
players is how many sit down, people how many of the seats are people's, and the rest are computers; every pair taken is an event.
<jarajara-table players="3" people="1" names="Ann" size="10" cloth="blue" delay="500"></jarajara-table>
<script>
document.querySelector("jarajara-table").addEventListener("jarajara-table", (event) => console.log(event.detail.scores, event.detail.over));
</script>
A look of your own
The tags are themed by custom properties, which pass into their shadow roots, and the felt is one of the family's five cloths.
jarajara-layout { --jarajara-pad: 8px; --jarajara-note: #7a1f12; }
jarajara-tile { --jarajara-tile-width: 64px; --jarajara-flip-ms: 300ms; }
Deal, check and play from a terminal
jarajara deals, checks a solve, gives the day's game, looks tiles up and plays computers at a table, in English or Japanese, from the same seeds as the functions. The whole reference is in docs/COMMAND-LINE.md.
npx @johnmorrisdotca/jarajara deal --layout 4 --seed 7
npx @johnmorrisdotca/jarajara check 0704000506010302 --layout 4 --seed 7
npx @johnmorrisdotca/jarajara tile three circles
npx @johnmorrisdotca/jarajara play --players 3 --layout 8 --seed 5
Tiny, 8 tiles, medium, seed 7, usual rule
…
Cleared: 4 pairs and 0 shuffles take every tile.
l 3 of circles 三筒 Circles 3, 4 in the set, written 3p
Awase at a table of 3: Torii, medium, seed 5. 32 pairs taken, 0 shuffles.
…
The tiles
Three suits of nine, characters 萬子, circles 筒子 and bamboo 索子, four of each; the four winds and the three dragons, four of each; and one each of the four flowers and the four seasons. 144 tiles in 42 faces, each written as one letter so a whole layout is a string:
| Letters | Faces |
|---|---|
a–i |
characters 1–9 |
j–r |
circles 1–9 |
s–A |
bamboo 1–9 |
B–E |
east, south, west, north winds |
F–H |
red, green, white dragons |
I–L |
plum, orchid, chrysanthemum, bamboo (flowers) |
M–P |
spring, summer, autumn, winter (seasons) |
Two tiles match when they are the same face. The flowers and seasons match in
one of two ways, the bonus rule: group, the usual, where any flower takes any
flower and any season any season, or same, where only an identical tile
does and the deal holds them in identical pairs.
A tile is free when nothing lies on it and it has an open side, left or right. Only free tiles may be taken.
The layouts
| Size | Name | Tiles | Layers |
|---|---|---|---|
| 4 | Tiny | 8 | 3 |
| 8 | Torii | 64 | 3 |
| 9 | Fuji | 100 | 5 |
| 10 | Castle | 120 | 5 |
| 11 | Pagoda | 94 | 4 |
| 12 | Fortress | 116 | 3 |
| 13 | Pyramid | 142 | 4 |
| 14 | Bridge | 84 | 4 |
| 15 | Turtle | 144 | 5 |
| 16 | Butterfly | 112 | 4 |
| 17 | Dragon | 142 | 4 |
| 20 | Wall | 288 | 5 |
| 26 | Palace | 576 | 6 |
The Turtle is the layout the solitaire has been played on since it was first
published. A layout's size is its width in tiles, and names it. Tiny is for
tests, cleared in four pairs. MAHJONG_LAYOUTS holds the first five, which
never change (itsutsu.com's kept games are made on them); the next six are
Jarajara's own drawings of shapes anyone may draw, in MORE_LAYOUTS; and the
last two are the mega layouts in MEGA_LAYOUTS, and ALL_LAYOUTS holds them all.
The two mega layouts are laid with more than one set of tiles. The Wall 長城 is a stretch of the Great Wall: four tiles thick with a walkway along the top, a watchtower at each end and a gate tower in the middle that rises in five layers, 288 tiles, a double set. The Palace 宮殿 is a wall round a courtyard with a gate, towers at its corners and either side of the gate, and the great hall in the middle climbing in six steps, 576 tiles, a quadruple set. A deal of either is laid in reverse like any other, so every deal can be cleared, and is made in under a tenth of a second (the Palace) on a laptop. Under the usual rule a flower or a season is dealt once however many sets there are, so the eight bonus tiles of the further sets are four more pairs of ordinary tiles. A table of computers works on them, a turn taking a few hundredths of a second on the Palace. Every one is dealt and cleared by the same reverse dealing, and a test deals each at every level from several seeds and plays its own answer out.
Awase
import { checkAwase, freshAwaseSeed, generateAwase } from "@johnmorrisdotca/jarajara/awase";
A deal is laid in reverse, pair by pair onto slots that would be free, so
every deal can be cleared. Its level is how forgiving it is: five deals are
laid from the seed and each played out by a player taking any free pair at
random; easy is the one that player clears most often, hard the one it
clears least. When no pair is left, the tiles still on the layout may be
shuffled where they lie, drawn from the deal so a replay draws the same.
A solve is written as text: each pair its two slots in base 36 ("0a1c"), a
shuffle *. checkAwase plays it on the deal and asks that the layout ends
empty: a few thousand steps, no search, safe to run on a server.
Options and challenges
The same deal can be played for something else. Hints, shuffles and undo can be given, limited or taken away, and seven challenges lay a goal and a clock over a deal. None of it changes a deal, so every one can still be cleared.
import { dailyAwase, generateAwase, readRun, runHint, runTake, startRun } from "@johnmorrisdotca/jarajara/awase";
const deal = generateAwase(15, "medium", 12345);
let run = startRun(deal, { challenge: "rush", hints: 3, shuffles: 1 }); // a game: plain data
const hint = runHint(run); // { run, pair } or null once the hints are spent
run = runTake(hint!.run, ...hint!.pair, Date.now()) ?? run; // the new game, or null where the rules say no
readRun(run, Date.now()); // { state, because, remainingMs, goal, multiplier, ... }
dailyAwase("2026-10-01"); // today's layout, level, seed and challenge, from the date alone
The seven challenges, what each asks and the numbers behind them, are in docs/CHALLENGES.md. A run has no clock of its own: every move is given at, milliseconds on any clock you keep, so a run is testable and a server may replay one. <jarajara-layout challenge="spark" timer controls> plays them, its clock counting down.
The challenges come from the ideas of Mah's challenge modes (see CREDITS.md); the code, the numbers and the layouts are Jarajara's own.
Awase at a table
import { computerPair, playAtTable, readTable, startTable } from "@johnmorrisdotca/jarajara/table";
let table = startTable(generateAwase(15, "medium", 7), [{ name: "Ann" }, { name: "Computer", computer: true }]);
const pair = computerPair(table)!; // the computer's choice for whoever is to move
table = playAtTable(table, pair[0], pair[1])!.table;
readTable(table); // whose turn, the scores, the pairs taken, whether it is over
Two to four players share one layout and take turns, each taking one free pair. Every pair scores: the plain suit tiles one, the ones and nines two, a wind three, a dragon four, and a flower or season pair two and another turn. When the player to move has no pair, the tiles are shuffled and they go on. Most points when the layout is cleared wins. Everything is face up, so one device can be passed round with no cover screen.
Drawing
import { layoutSvg, tileSvg, tileFaceSymbols, TILE_INK } from "@johnmorrisdotca/jarajara/faces";
tileSvg("F"); // one tile, the red dragon, as a whole <svg>
layoutSvg(15, cells, { chosen: 12, showFree: true }); // a layout, stacked, far to near, the chosen tile ringed
The faces are a Japanese-style set in plain strokes that read at thirty pixels, drawn as strings, so they go into any page or framework. A tile is a solid block, with thickness and a shadow, in Jarajara's own design or FluffyStuff's riichi tiles; backs, cloths, mirrored views, Find and hints are options of the same calls. docs/DRAWING.md describes the block, the frame, the backs and cloths, every option of layoutSvg, and how hints and Find work.
Elements
Seven custom elements put the tiles on any page, with no framework. Load them once and write the tags:
<script type="module" src="https://cdn.jsdelivr.net/npm/@johnmorrisdotca/jarajara@1/dist/element-define.js"></script>
<jarajara-tile code="east" size="large" flip></jarajara-tile>
<jarajara-rack tiles="234m567p3456s11z77z" pick capacity="14"></jarajara-rack>
<jarajara-layout size="15" controls timer show-free cloth="green"></jarajara-layout>
Or import "@johnmorrisdotca/jarajara/element/define" in a bundle; @johnmorrisdotca/jarajara/element holds the classes alone, to extend or to define under other names. Importing either on a server is safe. Tiles and boards are never text-selectable, every element keeps one steady box, and each speaks English or Japanese (its own lang, or the page's, and it follows a language chooser).
| Element | What it is |
|---|---|
<jarajara-tile code="F"> |
One tile, face up or down, any design and back, turned by a tap with flip (and sound); marked; spin() |
<jarajara-rack tiles="…"> |
Tiles lined up in front of you: hide(), show(), toggle(tiles?), sort(order?), group("suit" | "kind"), ungroup(), unsort(), mixUp(seed?), take(tile), add(tile, at?), replace(), lift(), lower(), mark(), unmark(), spin(); a tap raises a tile with pick |
<jarajara-layout size="15"> |
A game of Awase to play on a stacked layout: free tiles, pick two to take, hint, shuffle, undo, a timer, limits on hints and shuffles, the seven challenges, matching tiles ringed, and the same layout lined up sorted by x, y and z |
<jarajara-table players="3"> |
Awase at a table of two to four, with computers in the seats that are not people's |
<jarajara-viewer query="east"> |
A tile looked up by its code, a name in English or Japanese, or hand notation, with what it is |
<jarajara-group group="winds"> |
A group of the set shown: a suit, honours, bonus, terminals, simples, … |
<jarajara-set> |
The whole set as an inventory: 42 faces and their counts, or all 144 tiles; given tiles, it counts what a hand holds |
Every attribute of every tag, its methods and its events are in docs/ELEMENTS.md, in a table for each tag, held to the code by a test.
Designs
Jarajara's own tiles are the default and cost nothing extra. A second design is FluffyStuff's riichi tiles (public domain, CC0), regular and black, with their own backs and the red fives; they draw no flowers or seasons, so those stay Jarajara's own within them. Each is fetched the first time it is asked for, so a page that stays with the default never loads them.
<jarajara-tile code="F" design="riichi"></jarajara-tile>
<jarajara-layout size="9" design="riichi-black" red-fives></jarajara-layout>
<jarajara-rack tiles="123m055p" design="riichi" red-fives></jarajara-rack>
import { loadTileDesign, tileSvg, layoutSvg } from "@johnmorrisdotca/jarajara/faces";
const design = await loadTileDesign("riichi"); // or "riichi-black"; "jarajara" is always at hand
tileSvg("e", { design, red: true }); // the red five of characters
layoutSvg(9, cells, { design, redFives: true }); // the first five of each suit, in slot order, is red
TILE_DESIGNS lists them. A design's own back is named like the design (back="riichi"), and an element given a
design and no back shows that design's own. Where the drawings come from and under what licence is in
CREDITS.md.
Sounds
import { createTileSounds } from "@johnmorrisdotca/jarajara/tile-sounds";
const sounds = createTileSounds(); // nothing fetched, no audio context, until the first sound
sounds.play("shuffle"); // pick, place, flip, pair, shuffle, win
sounds.play("place", { count: 13, delay: 900 });
Tile clacks cut from Kenney's Casino Audio (CC0), loaded the first time one is played; where they cannot be loaded or
decoded, a short click made in the browser stands in, and a platform with no audio is silent without an error. Every
element takes sound and is silent without it: every action of a tile and a rack (turned, spun, sorted, grouped, mixed, raised,
lowered, marked, taken out, put in, dealt), and of a board a tile chosen, a hint, a pair taken, an undo, a shuffle and a cleared layout, a
pair at a table. The recordings are @johnmorrisdotca/jarajara/sounds,
tens of kilobytes as text.
Looking tiles up
import { findFace, findFaces, readTiles, writeNotation, setInventory, groupFaces, tileName } from "@johnmorrisdotca/jarajara";
findFace("three circles")?.code; // "l"
findFace("三筒")?.code; // "l"
findFace("7z")?.code; // "F", the red dragon, in the notation riichi hands are written in
readTiles("123m456p east plum"); // ["a","b","c","m","n","o","B","I"]
writeNotation(["a", "b", "c", "B"]); // "123m1z"
tileName("B", "ja"); // "東"
setInventory(); // 42 faces, each with how many of it the 144 tiles hold
The notation is 123m characters, 123p circles, 123s bamboo, 1z–4z the winds and 5z–7z white, green and red dragons, and Jarajara's own 1f flowers and 1t seasons. 0m, 0p and 0s read as fives.
Sorting and grouping tiles
import { arrangeTiles, groupTiles, mixTiles, sortSlots, readSlotKeys } from "@johnmorrisdotca/jarajara";
arrangeTiles(hand, "suit"); // "suit", "rank", "kind", "code" or "dealt"
groupTiles(hand, "kind"); // [numbers, honours, bonus tiles]
mixTiles(hand, 42); // another order, the same one for the same seed
sortSlots(layout, ["-z", "x", "y"]); // a layout's slots from the top of the stacks, then across
The command line
npx @johnmorrisdotca/jarajara --help # or: npm install -g @johnmorrisdotca/jarajara, then `jarajara`
For a terminal, a script or a teacher's handout: the same deals, checks and tiles as the functions, with nothing to write. Every deal comes from its seed, so the same command prints the same tiles on every machine. It has six commands: layouts, deal, daily, check, play and tile; it speaks English and Japanese; and its 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. docs/COMMAND-LINE.md shows each command's output and lists every command and option.
API
The documentation includes bilingual guides, local search, signatures, parameters, return values and checked examples, built from source by pnpm docs:site. The API index preserves export anchors.
| Import | What it holds |
|---|---|
@johnmorrisdotca/jarajara |
the set (MAHJONG_FACES, faceOf, tilesMatch, matchClass, bonusRuleOf, setPairs, pairPoints), names and lookup (faceWords, tileName, findFace, findFaces, readTiles, writeTiles, readNotation, writeNotation, TILE_GROUPS, groupFaces, setInventory, copiesOf, countTiles), arranging (arrangeTiles, arrangeIndexes, groupTiles, mixTiles, sortSlots, readSlotKeys, describeSlot), the layouts (MAHJONG_LAYOUTS, MORE_LAYOUTS, ALL_LAYOUTS, layoutFor, layoutExtent), the board (geometryOf, isFree, freeSlots, freePairs, blockedBy, canTake, takePair, tilesLeft, isCleared, hintFor, matchesOf), dealing (layPairs, pairsLeft, shuffleTiles), games as text (encodeMoves, decodeMoves, playSolve, dealFits), seededRandom and shuffled |
@johnmorrisdotca/jarajara/awase |
generateAwase(size, level, seed), checkAwase(size, givens, answer), freshAwaseSeed(rule), bonusRuleOfSeed, clearRate; options and challenges: AWASE_CHALLENGES, awaseRules, startRun, runTake, runShuffle, runUndo, runHint(run, chosen?), runStart, runTick, runRemaining, runPairs, runMarked, readRun, dailyAwase |
@johnmorrisdotca/jarajara/table |
startTable, readTable, tablePairs, playAtTable, takeAtTable, undoAtTable, computerPair, encodeTable, decodeTable, seatName, SEAT_WINDS |
@johnmorrisdotca/jarajara/faces |
tileSvg, tileFaceSvg, tileFaceSymbols, faceWords, layoutSvg, layoutBox, layoutFrame, tileAt, TILE_BOX, the block (blockColours, blockSvg, shadowSvg, faceEdgeSvg, TILE_DEPTH, TILE_MIRRORS), the backs (tileBackSvg, tileBackFace, backBase, TILE_BACKS), designs (jarajaraDesign, loadTileDesign, TILE_DESIGNS, redFiveIndexes), cloths (JARAJARA_CLOTHS, CLOTHS, isCloth, clothVars), and the colours TILE_INK, TILE_BODY, TILE_MARKS |
@johnmorrisdotca/jarajara/tile-sounds |
createTileSounds, TILE_SOUND_KINDS, MOST_SOUNDS_AT_ONCE, soundTimes |
@johnmorrisdotca/jarajara/sounds |
TILE_SOUND_DATA, the recordings as base64 AAC |
@johnmorrisdotca/jarajara/designs/riichi |
RIICHI: the regular riichi tiles as drawings, over a hundred kilobytes |
@johnmorrisdotca/jarajara/designs/riichi-black |
RIICHI_BLACK: the black riichi tiles as drawings, over a hundred kilobytes |
@johnmorrisdotca/jarajara/element |
the element classes (JarajaraTile, JarajaraRack, JarajaraLayout, JarajaraTable, JarajaraGroup, JarajaraSet, JarajaraViewer), hintPair, setPageSounds, MOST_SOUNDING, rackPlaces, rackWidth, ELEMENT_SIZES, STRINGS, followLanguage, languageOf, allowanceOf |
@johnmorrisdotca/jarajara/element/define |
defines the seven tags on the page, for its effect |
Every function is pure: it returns new values and never changes what it was given.
Theming
Nothing here is branded. The tiles are light objects and their colours are fixed, since a tile looks the same on any table (TILE_INK, TILE_BODY and TILE_MARKS hold them, to read); the table under them is the page's. The tags are themed by custom properties, which pass into their shadow roots, so a page sets only the ones it wants different, on the tag or above it:
| Property | What it styles | Default |
|---|---|---|
--jarajara-tile-width |
the width of a tile in <jarajara-tile> and in the viewers, when no size or width says |
48px |
--jarajara-pad |
the padding between a cloth's rim and the board | 14px |
--jarajara-box |
the board's box, as a ratio, under box |
the ratio box names |
--jarajara-felt |
a cloth's felt | set by cloth |
--jarajara-felt-deep |
a cloth's deep edge | set by cloth |
--jarajara-felt-ink |
the ink written on a cloth, and its rim | #f3efe4 |
--jarajara-focus |
the ring round a tile, a rack's tile or a viewer's choice that has the keyboard | #b5452c |
--jarajara-marker |
the dot of a marked tile | #f2b134 |
--jarajara-note |
the board's line of notes, such as "That tile is not free" | #b5452c |
--jarajara-flip-ms |
how long a tile takes to turn over | 520ms |
jarajara-layout { --jarajara-pad: 8px; --jarajara-note: #7a1f12; }
jarajara-tile { --jarajara-tile-width: 64px; --jarajara-flip-ms: 300ms; }
A cloth is one choice of the three felt properties, which clothVars(name) gives as an object for a page that wants the felt without a tag:
| Cloth | Felt | Deep edge | Ink |
|---|---|---|---|
green |
#2f5d4a |
#1f4135 |
#f3efe4 |
blue |
#2865a6 |
#1a4677 |
#f3efe4 |
red |
#a3342e |
#7a231f |
#f3efe4 |
black |
#2f3236 |
#1b1d20 |
#ece8dc |
wood |
#e2ba7a |
#c4954f |
#2b1d0e |
The buttons and the viewer's box are drawn light too, with dark text, whatever the page's theme; on a dark page the tags look best on a cloth. The demo's own page is the worked example: its green felt and its cloth patches are the family's stylesheet, demo/family.css, the same file byte for byte in every sibling's demo. The parts of the tags carry part names (back, board, button, clock, controls, face, frame, marker, rack, seats, status and tile) for anything a property cannot reach.
Limits
All of these are held by tests, and the ones with a name are exported.
| Limit | Value | Where |
|---|---|---|
| The set | 144 tiles in 42 faces | MAHJONG_FACES, setInventory |
| Layouts | thirteen, 8 to 576 tiles, widths 4, 8 to 17, 20 and 26 | ALL_LAYOUTS, layoutFor |
| Levels | easy, medium, hard |
generateAwase |
| A seed | a whole number from 1 to 2,147,483,647 | SEED_MOST, freshAwaseSeed |
| Seeds for the identical rule | 1,500,000,000 to 1,599,999,999 | AWASE_SAME_BLOCK, bonusRuleOfSeed |
| A table | 2 to 4 players, a name up to 20 characters | AWASE_TABLE |
| Challenges | seven | AWASE_CHALLENGES |
| A solve | each pair is its two slots in four base-36 characters, a shuffle is * |
encodeMoves, decodeMoves |
| What a check does | plays the moves once: a few thousand steps at most, no search | checkAwase |
| Clicks at once | 12 from one element, and 8 from one call of the sound player | MOST_SOUNDING, MOST_SOUNDS_AT_ONCE |
| A tile's width | at most 600 pixels, from width |
<jarajara-tile>, the viewers |
| The riichi designs | over a hundred kilobytes of drawings each, fetched when first asked for | /designs/riichi, /designs/riichi-black |
| The recordings | tens of kilobytes as text, fetched when the first sound plays | /sounds |
A level is chosen by laying five deals from the seed and playing each out at random, so making a deal is real work, and a server that only checks should never make one: checkAwase plays the moves it is given and nothing more.
Accessibility
- Tiles and boards are named in English or Japanese. A single tile, a rack's tiles and the viewers' tiles have a name a screen reader says ("east wind", 東), and a layout is labelled with how many tiles are left. The board's line of how the game stands ("132 tiles left · 12 pairs you can take") and its notes ("That tile is not free") are a polite live region, so a change is spoken without moving focus.
- A tile is not told by its picture alone. Every suit tile and wind carries its number or letter small in the corner, for a reader who does not count dots at a glance or read 東 as east, and with
show-freea blocked tile is washed darker, which is a second sign beside the free tile's full colour. - The keyboard. A flippable tile and a rack's tiles are keyboard controls: Tab to one, Enter or Space to turn it or pick it up, and the arrow keys to move along a rack. The buttons of a game (New deal, Undo, Hint, Shuffle, Flip) are native buttons at least 44 pixels high, and so are the demo's options.
- Reduced motion. A tile turning, a rack rearranging, a spin and a pair lifting off are all skipped under
prefers-reduced-motion: reduce. - Colour and contrast. The tiles are light objects with fixed colours, and every tile is drawn with a seam and a raised edge so that a row of tiles can be counted in every design, black ones included; the colour pairs of the felts have not been measured against WCAG contrast ratios.
- Sound is optional and never the only sign of anything: off unless a page turns it on, and every move also changes the picture and the live line.
- Not yet: playing a layout by keyboard. A layout's tiles are played by pointer; moving between the free tiles and choosing one is on the Roadmap. A page that needs it today can call
take(a, b)andhint()from its own controls, as the demo's buttons do. Touch targets inside a layout are the tiles' own size, which depends on the board's box. - Not yet: Japanese read by a native reader (see Languages).
Browser support
The browser requirements, tested engines and server-safe entry points are detailed in Browser support.
Languages
English and Japanese. A tag speaks its own lang, or the nearest one above it, or the page's, and follows the page's language when it changes. The words of the tags are STRINGS (in @johnmorrisdotca/jarajara/element), the tiles' names are faceWords and faceWordsJa (tileName(code, language)), and the command line has its own words in src/cliWords.ts. The demo has a chooser of its own and takes the browser's language on a first visit. Japanese: included; not yet reviewed by a native reader. Corrections welcome. Every string of the tags and the command line is listed beside its English in docs/strings-ja.md, made from the source by pnpm docs:make and held by a test, and a wrong one is a Fix a translation issue away: open one.
Roadmap
Not here yet, and each welcome as an issue:
- Playing a layout by keyboard: moving between the free tiles and choosing one.
- A board turned a quarter, and free rotation.
docs/LOOK.mdsays what they would take. - More layouts: suggest one.
Left out on purpose: any account, ranking or storage. A page keeps its own games: jarajara-take and jarajara-deal hand over what is needed, moves is the game as text and restore(moves) plays it back. The first five layouts, and every deal made on them, never change.
Architecture
The record and source-layout guide explains the architecture; docs/ARCHITECTURE.md preserves the complete tree.
The name
Jarajara (ジャラジャラ) is the sound of mahjong tiles being shuffled, the rattle of a hundred and forty-four of them washed together face down on the table before a game. It is said in four beats, ja-ra-ja-ra. Awase (合わせ) is "matching", "putting together", from awaseru (合わせる), to join two things into a pair.
Where it comes from, and where it is used
Jarajara 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. Its Mahjong was made there, with its layouts, its deals and its table for two to four, and once the tiles and the solitaire stood alone it seemed worth sharing them. The deals and table games the site made before the move are made again, exactly, on every build.
Used by
- Itsutsu, for its Mahjong: the tiles, the layouts, the deals, the check and the table.
Using Jarajara in something? Open an Add my project issue and we will add you.
The family
Jarajara 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 Jarajara. The demos of all thirty-two share one header and footer, so each links the rest.
Development
pnpm install
pnpm check # lint, types and every test, every recorded deal made again
pnpm test:package # pack, install and import it as somebody who installed it would, and run its command line
pnpm test:cli # build it and run the command line as a child process
pnpm test:demo # build the demo and play it in a real browser, at a phone's width and a desk's
pnpm test:frameworks # build React, Vue, Svelte, Angular and plain pages from the packed tarball and play them in a browser
pnpm site # build the demo into site/, as the Pages workflow publishes it
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 dark
pnpm docs:make # rewrite docs/strings-ja.md after changing a word of the tags or the command line
Contributing
See CONTRIBUTING.md. The commands are under Development.
Please follow the code of conduct. A way to make the check or a deal run for a very long time, or text that gets out of a drawing into the page, is for the security policy, not a public issue.
Changes
See CHANGELOG.md. The latest release, 1.7.0, adds the rack's standard embed attributes, the shared embed builder, and the documentation site with a checked example for every export.
Licence
MIT, © John Morris. The tile faces are drawn for this package and are under the same licence. The riichi designs are FluffyStuff's (public domain) and the sounds Kenney's (CC0): see CREDITS.md.
Related Packages
A modern framework-agnostic UI kit built as Web Components. Requires Tailwind CS...
A framework-agnostic, customizable AI voice orb built as a strict, SSR-safe web...
Type-safe Fetch HTTP client with retries, interceptors, validation, and multi-ho...
Version History
| Version | Released | License |
|---|---|---|
| 1.7.0 | MIT | |
| 1.6.2 | MIT | |
| 1.6.1 | MIT | |
| 1.6.0 | MIT | |
| 1.5.0 | MIT | |
| 1.4.0 | MIT | |
| 1.3.0 | MIT | |
| 1.2.0 | MIT | |
| 1.1.0 | MIT | |
| 1.0.1 | MIT | |
| 1.0.0 | MIT |