@johnmorrisdotca/kumimoji 1.0.1 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,60 +1,393 @@
1
- # Kumimoji 組み文字
1
+ <h1 align="center">Kumimoji <sub>組み文字</sub></h1>
2
+
3
+ <p align="center"><strong>The crossword tile race: a word game where you build one crossword from a bag of letter tiles.</strong><br>
4
+ In English and in Japanese kana. The rules as pure, seeded TypeScript, two word lists, a computer player, and a table to play on in React, Vue, Svelte, Angular or plain HTML.</p>
5
+
6
+ <p align="center">
7
+ <a href="https://github.com/johnmorrisdotca/kumimoji/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/johnmorrisdotca/kumimoji/actions/workflows/ci.yml/badge.svg"></a>
8
+ <a href="https://www.npmjs.com/package/@johnmorrisdotca/kumimoji"><img alt="npm" src="https://img.shields.io/npm/v/@johnmorrisdotca/kumimoji?color=2f5d4a"></a>
9
+ <a href="./LICENSE"><img alt="Code: MIT licence" src="https://img.shields.io/badge/code-MIT-2f5d4a"></a>
10
+ <a href="./NOTICE.md"><img alt="Japanese word list: CC BY-SA 4.0" src="https://img.shields.io/badge/Japanese%20words-CC%20BY--SA%204.0-b5452c"></a>
11
+ <img alt="No dependencies" src="https://img.shields.io/badge/dependencies-0-2f5d4a">
12
+ <img alt="TypeScript" src="https://img.shields.io/badge/types-TypeScript-3178c6">
13
+ </p>
14
+
15
+ <p align="center"><a href="https://johnmorrisdotca.github.io/kumimoji/"><strong>Play a game →</strong></a> · <a href="https://johnmorrisdotca.github.io/kumimoji/api.html">API reference</a></p>
2
16
 
3
- [![CI](https://github.com/johnmorrisdotca/kumimoji/actions/workflows/ci.yml/badge.svg)](https://github.com/johnmorrisdotca/kumimoji/actions/workflows/ci.yml)
4
- [![npm](https://img.shields.io/npm/v/@johnmorrisdotca/kumimoji.svg)](https://www.npmjs.com/package/@johnmorrisdotca/kumimoji)
5
- [![Licence: MIT](https://img.shields.io/badge/licence-MIT-blue.svg)](LICENSE)
6
- ![Types included](https://img.shields.io/badge/types-TypeScript-3178c6.svg)
7
- ![Dependencies: none](https://img.shields.io/badge/dependencies-none-brightgreen.svg)
17
+ <p align="center">
18
+ <img src="docs/desktop.jpg" alt="An English game half way: a crossword on the green table, a hand of tiles under it, and the buttons to draw, trade and sort" width="720">
19
+ <img src="docs/phone.jpg" alt="A game in Japanese kana on a phone in dark mode" width="220">
20
+ </p>
8
21
 
9
- **The crossword tile race, in English and Japanese, as pure and seeded TypeScript.**
22
+ A word game engine for the race to build a crossword. Every player lays their
23
+ own tiles as words that cross, draws another when the hand is used and every
24
+ run is a word, and the first to use the last tile wins. Play alone against
25
+ the clock, or at a table of two to eight sharing one bag.
26
+
27
+ - **What is different.** A game is dealt from a seed by laying a crossword
28
+ first, so every bag can be finished and two players racing a seed get the
29
+ same tiles in the same order. It plays in Japanese as well as English, on
30
+ 45 kana tiles. And a finished grid is checked in time proportional to its
31
+ size, so a server can check every game handed in.
32
+ - **What it costs a project.** No dependencies. The word lists are large, and
33
+ are fetched only when a game first needs them.
34
+
35
+ > **The word lists have licences of their own.** The code is MIT. The English
36
+ > list is from SCOWL, under a permissive licence whose notice must travel with
37
+ > the list. **The Japanese list is derived from JMdict and is under
38
+ > [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/): attribution
39
+ > and share-alike.** If you pass the Japanese list on, or a list made from it,
40
+ > you must credit JMdict and the EDRDG and share it under the same licence.
41
+ > [The word lists and their licences](#the-word-lists-and-their-licences) has
42
+ > the whole of it, and [NOTICE.md](./NOTICE.md) the notices themselves.
43
+
44
+ ## Play in 30 seconds
10
45
 
11
- Build one crossword from every tile in the bag. Lay your hand as words that
12
- cross, and when your hand is empty and every run across and down is a word, draw
13
- the next tile. A tile you cannot use can be traded for three. Use the last tile
14
- in the bag and you are done. Play alone against the clock, or at a table of two
15
- to eight sharing one bag, in English or in Japanese.
46
+ ```sh
47
+ npm install @johnmorrisdotca/kumimoji # or pnpm add, or yarn add
48
+ ```
16
49
 
17
- *Kumimoji* (組み文字) is "letters put together".
50
+ ```ts
51
+ import "@johnmorrisdotca/kumimoji/words"; // on a server or in a script: reads the lists from this package's files
52
+ import { deal, draw, generateKumimoji, isFinished, judgeWithWords, loadTileWords, mayDraw, placeFromHand, squareAt } from "@johnmorrisdotca/kumimoji";
53
+
54
+ const words = await loadTileWords("english");
55
+ const dealt = generateKumimoji(3, "hard", 44); // a hand of three, no wild tiles, seed 44: the smallest game there is
56
+ dealt.givens; // → "nwosy"
57
+
58
+ let play = deal(dealt.givens, 3);
59
+ play.hand; // → ["n", "w", "o"]
60
+ play = placeFromHand(play, 0, squareAt(0, 0)); // N
61
+ play = placeFromHand(play, 1, squareAt(0, 1)); // O
62
+ play = placeFromHand(play, 0, squareAt(0, 2)); // W
63
+
64
+ mayDraw(play, judgeWithWords(play.tiles, words)); // → true
65
+ play = placeFromHand(draw(play), 0, squareAt(0, -1)); // S, in front: the table has no edges
66
+ play = placeFromHand(draw(play), 0, squareAt(0, 3)); // Y
67
+ isFinished(play, judgeWithWords(play.tiles, words)); // → true
68
+ ```
18
69
 
19
- **[Play it](https://johnmorrisdotca.github.io/kumimoji/)**
70
+ NOW, then SNOW, then SNOWY. And a table to play on, in a page:
20
71
 
21
- <p>
22
- <img src="docs/desktop.jpg" alt="An English game: BACK across and RAW down, with a wild tile left in hand" width="560">
23
- <img src="docs/phone.jpg" alt="The same game on a phone in dark mode" width="200">
24
- </p>
72
+ ```js
73
+ import { mountKumimoji } from "@johnmorrisdotca/kumimoji/ui";
74
+
75
+ mountKumimoji(document.getElementById("table"), { language: "english", hand: 7 });
76
+ ```
77
+
78
+ Or with nothing to install, [play a game in the demo](https://johnmorrisdotca.github.io/kumimoji/).
79
+
80
+ ## Who it is for
81
+
82
+ - **Games sites and apps.** A finished word game to put on a page: the table
83
+ in one call, or the rules under a board of your own.
84
+ - **Anybody building a tile word game.** Dealing, laying, wild tiles, trades,
85
+ judging a grid, a table of players with a last round, and a computer player,
86
+ all tested.
87
+ - **Races and daily puzzles.** A seed deals one bag, the same for everybody,
88
+ and `checkKumimoji` checks a finished grid against it without searching.
89
+ - **Japanese.** A word game that plays in kana, with が on the か tile and ゃ
90
+ on the や tile, and a list of 163,461 readings.
91
+ - **Learners and teachers** of English or Japanese spelling.
92
+
93
+ ## Use it in your project
94
+
95
+ Kumimoji is four things, each usable without the others: **the rules**, plain
96
+ functions over plain values; **the word lists**, loaded when first needed;
97
+ **a table** you mount into any element; and **React components**.
98
+
99
+ ### 1. The API alone
100
+
101
+ ```ts
102
+ import "@johnmorrisdotca/kumimoji/words";
103
+ import { checkKumimoji, decodeGrid, encodeGrid, judgeWithWords, loadTileWords, runsOf } from "@johnmorrisdotca/kumimoji";
104
+
105
+ const words = await loadTileWords("english");
106
+ const tiles = decodeGrid("cat/2o/2w")!; // CAT across, and down from its T
107
+ runsOf(tiles).map((run) => run.word); // → ["cat", "tow"]
108
+ judgeWithWords(tiles, words).sound; // → true
109
+
110
+ const wrong = decodeGrid("cat/2x")!;
111
+ judgeWithWords(wrong, words).notWords; // → ["tx"]
112
+
113
+ checkKumimoji(3, "nwosy", "snowy", { level: "hard" }); // → { ok: true }
114
+ checkKumimoji(3, "nwosy", "snows", { level: "hard" }); // → { ok: false, reason: "the grid does not use exactly the tiles of the bag" }
115
+ encodeGrid(tiles); // → "cat/2o/2w"
116
+ ```
117
+
118
+ ### 2. The table, in plain HTML
119
+
120
+ ```html
121
+ <div id="table"></div>
122
+ <p id="left"></p>
123
+ <script type="module">
124
+ import { mountKumimoji } from "@johnmorrisdotca/kumimoji/ui";
125
+
126
+ mountKumimoji(document.getElementById("table"), {
127
+ language: "english",
128
+ hand: 7,
129
+ onChange: ({ play }) => (document.getElementById("left").textContent = play.hand.length),
130
+ });
131
+ </script>
132
+ ```
133
+
134
+ Without a bundler, import from the files as they are published:
135
+ `./node_modules/@johnmorrisdotca/kumimoji/dist/ui.js`, or a copy of `dist/`.
136
+
137
+ ### 3. React
138
+
139
+ ```jsx
140
+ import { useState } from "react";
141
+ import { KumimojiTable } from "@johnmorrisdotca/kumimoji/react";
25
142
 
26
- - **Dealt from a seed.** `generateKumimoji` lays a crossword first and deals its
27
- tiles, so every bag can be finished, and two players racing the same seed get
28
- the same tiles in the same order.
29
- - **English and Japanese.** 144 tiles in each: the English letter mix, or 45 base
143
+ export function App() {
144
+ const [left, setLeft] = useState("");
145
+ return (
146
+ <>
147
+ <KumimojiTable language="english" hand={7} onChange={({ play }) => setLeft(play.hand.length)} />
148
+ <p id="left">{left}</p>
149
+ </>
150
+ );
151
+ }
152
+ ```
153
+
154
+ `KumimojiTable` takes the table's options as props, plus any attribute for its
155
+ `<div>`. It mounts in the browser after the first render, so server rendering
156
+ draws an empty box and nothing needs a provider. In Next.js, use it from a
157
+ client component (`"use client"`). `KumimojiBoard` is the grid alone, for a
158
+ table of your own: see [The React components](#the-react-components).
159
+
160
+ ### 4. Vue
161
+
162
+ ```vue
163
+ <script setup>
164
+ import { onBeforeUnmount, onMounted, ref } from "vue";
165
+ import { mountKumimoji } from "@johnmorrisdotca/kumimoji/ui";
166
+
167
+ const box = ref(null);
168
+ const left = ref("");
169
+ let table;
170
+ onMounted(() => {
171
+ table = mountKumimoji(box.value, { language: "english", hand: 7, onChange: ({ play }) => (left.value = play.hand.length) });
172
+ });
173
+ onBeforeUnmount(() => table?.destroy());
174
+ </script>
175
+
176
+ <template>
177
+ <div ref="box"></div>
178
+ <p id="left">{{ left }}</p>
179
+ </template>
180
+ ```
181
+
182
+ ### 5. Svelte
183
+
184
+ ```svelte
185
+ <script>
186
+ import { onMount } from "svelte";
187
+ import { mountKumimoji } from "@johnmorrisdotca/kumimoji/ui";
188
+
189
+ let box;
190
+ let left = $state("");
191
+ onMount(() => {
192
+ const table = mountKumimoji(box, { language: "english", hand: 7, onChange: ({ play }) => (left = play.hand.length) });
193
+ return () => table.destroy();
194
+ });
195
+ </script>
196
+
197
+ <div bind:this={box}></div>
198
+ <p id="left">{left}</p>
199
+ ```
200
+
201
+ ### 6. Angular
202
+
203
+ ```typescript
204
+ import { Component, ElementRef, OnDestroy, afterNextRender, provideZonelessChangeDetection, signal, viewChild } from "@angular/core";
205
+ import { bootstrapApplication } from "@angular/platform-browser";
206
+ import { mountKumimoji, type KumimojiTableHandle } from "@johnmorrisdotca/kumimoji/ui";
207
+
208
+ @Component({
209
+ selector: "app-root",
210
+ template: `<div #box></div><p id="left">{{ left() }}</p>`,
211
+ })
212
+ class App implements OnDestroy {
213
+ private box = viewChild.required<ElementRef<HTMLElement>>("box");
214
+ private table?: KumimojiTableHandle;
215
+ left = signal("");
216
+ constructor() {
217
+ afterNextRender(() => {
218
+ this.table = mountKumimoji(this.box().nativeElement, { language: "english", hand: 7, onChange: ({ play }) => this.left.set(String(play.hand.length)) });
219
+ });
220
+ }
221
+ ngOnDestroy() {
222
+ this.table?.destroy();
223
+ }
224
+ }
225
+
226
+ bootstrapApplication(App, { providers: [provideZonelessChangeDetection()] });
227
+ ```
228
+
229
+ Each of the five is taken from this page as it is written, built from the
230
+ packed tarball in a project of its own, and played by taps in Chromium and
231
+ WebKit, by `scripts/check-frameworks.mjs`, before a release names it. In each
232
+ the bundler splits the word lists into files of their own, which the browser
233
+ fetches when a game is first dealt.
234
+
235
+ ### What a developer gets
236
+
237
+ - **Typed results.** TypeScript types for everything, with a doc comment on
238
+ every export, which a test holds.
239
+ - **A game you can keep.** `kumimojiToJSON` and `kumimojiFromJSON`, and plain
240
+ text; what is read back is dealt again from its seed and checked against
241
+ its bag, never trusted.
242
+ - **No dependencies**, ES modules, a `default` export condition so that
243
+ `require()` loads it too (Node 22 and later).
244
+ - **Sizes.** The rules, the computer player and the table of players are
245
+ about 44 kB minified (16 kB gzipped); the table for a page is 34 kB (13 kB).
246
+ The English list is 606 kB (277 kB gzipped) and the Japanese 718 kB
247
+ (396 kB), each a file of its own that is fetched only when a game in that
248
+ language is first dealt.
249
+ - **Where it runs.** Current Chrome, Edge, Firefox and Safari, on a desk or a
250
+ phone. The rules have no DOM in them and run in Node 20 and later, Deno,
251
+ Bun and web workers.
252
+
253
+ ## Architecture
254
+
255
+ The rules are plain functions over plain data with no DOM: a game is a value,
256
+ every move returns the next one, and the same seed deals the same bag in every
257
+ browser. Word lists are loaded only when a game opens, and the table is its own
258
+ entry (`/ui` for plain DOM, `/react` for React), so a page that wants only the
259
+ rules carries none of it. A computer player is pure too, so its turn replays
260
+ exactly after a reload.
261
+
262
+ ```text
263
+ src/
264
+ ├── check.ts whether a finished grid completes a game: the one check a server also runs
265
+ ├── computer.types.ts what a computer did in one step of its turn
266
+ ├── computerPlay.ts the word a computer player lays: from its own hand, with no randomness
267
+ ├── computerTurn.ts a computer's whole turn, planned as steps the page can show one at a time
268
+ ├── export.ts a game written out as JSON or plain text, and read back
269
+ ├── generate.ts making a game from a seed: a crossword laid first, then its tiles become the bag
270
+ ├── grid.ts the grid of tiles on a table with no edges, and how it is written as a string
271
+ ├── help.ts arranging the hand into a word, for a player who asks for help
272
+ ├── index.ts the main entry: the rules, the generator, the computer player and the table's pure helpers
273
+ ├── judge.ts judging a grid against its language's word list, run by run
274
+ ├── kana.ts Japanese tiles: the 45 base kana, and how every other kana is played as one of them
275
+ ├── kumimoji.types.ts the game's types: how much of the set a bag holds, and the rest
276
+ ├── party.ts pass and play: two to eight people round one device, one bag, a hand and a table each
277
+ ├── party.types.ts what a pass-and-play game was set up as
278
+ ├── partyKept.ts a pass-and-play game kept in the browser as one string of JSON
279
+ ├── partySeats.ts people and computers joining and leaving a pass-and-play game between turns
280
+ ├── partyTurns.ts how a pass-and-play turn ends, and how the game does, and who won
281
+ ├── placement.ts where a word can cross a crossword without spoiling it
282
+ ├── play.ts a game being played: the bag, the hand and the table, and every move on them
283
+ ├── random.ts a seeded random, so one seed deals one game in every browser
284
+ ├── react.tsx the "/react" entry: a grid to draw in React
285
+ ├── showcase.ts what the rules page shows of the game, worked out from the game's own tables
286
+ ├── strings.ts every word Kumimoji shows a person, in English and Japanese
287
+ ├── tableView.ts how the table is looked at: which squares are shown, and how big a tile is drawn
288
+ ├── tileFace.ts what a tile code shows, without loading a word list
289
+ ├── tileFamily.ts which tile of the set a tile is, without loading a word list
290
+ ├── tileWords.ts the word list, loaded once when a game opens
291
+ ├── tileWordsModule.ts the same lists where there is no browser, imported only by a server or a test
292
+ ├── tiles.constants.ts every number the game is played by: tile mixes, hand sizes, lengths of a game
293
+ ├── turn.ts turning the table while every tile stays upright
294
+ ├── ui.ts the "/ui" entry: Kumimoji played in the browser, in plain DOM
295
+ ├── version.ts the version of this package, as package.json has it
296
+ ├── wilds.ts a wild tile with no letter read as whatever makes the grid words
297
+ ├── words.en.data.ts the English word list
298
+ ├── words.ja.data.ts the Japanese word list and tile mix
299
+ ├── words.ts the "/words" entry: the word lists, for a place with no browser, such as a server or a test
300
+ └── ui/ the table that draws and plays a game
301
+ ├── board.ts how a tile on the table is drawn: sound, in a run that is not a word, or apart
302
+ ├── mount.ts the table itself: mounting it on a page, and the options it takes
303
+ └── style.ts the table's own styles, every colour and size a CSS variable so a page can restyle it
304
+ ```
305
+
306
+ Tests sit beside the code they test (`*.test.ts`). `scripts/` builds the demo
307
+ and checks the package as npm packs it, `demo/` is the page published on
308
+ GitHub Pages, and `table/` taps it in real browsers.
309
+
310
+ ## The name
311
+
312
+ *Kumimoji* (組み文字) is made of *kumi* (組み, from 組む, to put together or
313
+ assemble) and *moji* (文字, a letter or character): letters put together,
314
+ which is what a player does with the tiles. Say it in four even beats:
315
+ ku-mi-mo-ji. In Japanese printing the same word names something else, several
316
+ characters set together in the space of one; the game borrows only its plain
317
+ sense.
318
+
319
+ ## Where it comes from, and where it is used
320
+
321
+ Kumimoji was built for [Itsutsu](https://itsutsu.com), a site for board games,
322
+ puzzles, card games and dice games played at your own pace. *Itsutsu* (五つ) is
323
+ Japanese for "five", after five in a row, the game the site began with. It is
324
+ [the Kumimoji played there](https://itsutsu.com/games/kumimoji): alone, in
325
+ races, round one device and across several.
326
+
327
+ ### Used by
328
+
329
+ - [Itsutsu](https://itsutsu.com), for its game of Kumimoji: its rules, its
330
+ word lists and its computer player.
331
+
332
+ That is the whole list so far. Using Kumimoji in something? Open an
333
+ [*Add my project*](https://github.com/johnmorrisdotca/kumimoji/issues/new?template=add-my-project.md)
334
+ issue and we will add you.
335
+
336
+ ### The family
337
+
338
+ Kumimoji has siblings, each made for the same site, each MIT, each at
339
+ [github.com/johnmorrisdotca](https://github.com/johnmorrisdotca):
340
+
341
+ - [Korokoro](https://github.com/johnmorrisdotca/korokoro) (コロコロ, the sound
342
+ of something small rolling along): a dice roller and a dice notation
343
+ parser, with the exact odds of every roll.
344
+ - [Kyuubu](https://github.com/johnmorrisdotca/kyuubu) (キューブ, how Japanese
345
+ says "cube"): a turning cube for the browser, 2×2 to 7×7, drawn in CSS 3D.
346
+ - [Hitotsu](https://github.com/johnmorrisdotca/hitotsu) (一つ, "one"): a
347
+ colour-card game, named for the call a player makes with one card left.
348
+ - [Toranpu](https://github.com/johnmorrisdotca/toranpu) (トランプ, the everyday
349
+ Japanese word for a deck of playing cards): ten card games, complete, with a
350
+ computer for every seat.
351
+ - [Tane](https://github.com/johnmorrisdotca/tane) (種, a seed, the kind you
352
+ plant): seeded random numbers and daily seeds.
353
+ - [Narabe](https://github.com/johnmorrisdotca/narabe) (並べ, "line them up"):
354
+ a rules engine for forty-eight board games, from five in a row to Go.
355
+ - [Tenka](https://github.com/johnmorrisdotca/tenka) (天下, "all under
356
+ heaven"): world conquest for two to six, on a map of the real world.
357
+
358
+ ## Features
359
+
360
+ - **Dealt from a seed.** `generateKumimoji` lays a crossword first and deals
361
+ its tiles, so every bag can be finished.
362
+ - **English and Japanese.** 144 tiles in each: the English letters, or 45 base
30
363
  kana where が plays as か, ゃ as や and を as お. 110,316 English words and
31
- 163,461 Japanese readings, from 2 to 15 tiles.
32
- - **Wild tiles** that take any letter or kana, and are read as that letter in
33
- every word they are part of.
364
+ 163,461 Japanese readings, of 2 to 15 tiles.
365
+ - **Wild tiles** that take any letter or kana, and are read as that in every
366
+ word they are part of.
367
+ - **Trades.** A tile you cannot use goes back into the bag for three.
34
368
  - **Diagonals**, if chosen: every diagonal run of three or more must be a word
35
369
  too.
36
- - **The rules as plain functions.** A game in progress is a value
37
- (`TilePlay`): `deal`, `placeFromHand`, `moveOnTable`, `liftToHand`, `draw`,
38
- `trade` each return a new one. `judgeWithWords` says which runs are not words
39
- and which tiles are apart; `checkKumimoji` checks a finished grid against its
40
- bag, in time proportional to its size.
41
- - **A table of two to eight** (`startParty`, `endTurn`, `drawAll`, `resign`,
42
- `joinParty`, `leaveParty`), kept as text (`encodeParty`, `decodeParty`), with a
43
- computer player that can take a seat (`planComputerTurn`).
44
- - **A table to play on.** `mountKumimoji` plays a whole game alone in plain DOM;
45
- `KumimojiBoard` draws a grid in React.
46
-
47
- It is the Kumimoji on [Itsutsu](https://itsutsu.com/games/kumimoji), which plays
48
- alone, in races, round one device and across several with this package.
370
+ - **Three lengths.** A short game, half the set, or all 144 tiles; and two
371
+ English sets at once.
372
+ - **The rules as plain functions.** A game in progress is a value (`TilePlay`):
373
+ `deal`, `placeFromHand`, `moveOnTable`, `liftToHand`, `draw` and `trade` each
374
+ return a new one.
375
+ - **A table of two to eight** sharing one bag, with a last round, resigning,
376
+ joining and leaving, kept as text, and a computer player for any seat.
377
+ - **Help**, if chosen: the words the hand's own tiles spell.
378
+ - **A table to play on.** `mountKumimoji` plays a whole game alone in plain
379
+ DOM, saves it and loads it back; `KumimojiBoard` draws a grid in React.
380
+ - **The table's words in English and Japanese**, and any other language by a
381
+ table of your own. **Themeable**: every colour is a CSS variable.
49
382
 
50
383
  ## How to play
51
384
 
52
- 1. Everyone starts with a hand of tiles from the bag: 3, 7 or 11.
385
+ 1. Everyone starts with a hand of tiles from the bag: 7 or 11.
53
386
  2. Lay your tiles on the table as a crossword of your own. Every run of two or
54
387
  more tiles, across and down, must be a word, and every tile must join the
55
- one crossword. Move and take back tiles as often as you like.
56
- 3. When your hand is empty and the crossword is sound, draw the next tile. At a
57
- table, everyone draws together.
388
+ one crossword. Move tiles and take them back as often as you like.
389
+ 3. When your hand is empty and the crossword is sound, draw the next tile. At
390
+ a table, everyone draws together.
58
391
  4. Stuck with a tile you cannot use? Trade it for three from the bag.
59
392
  5. A wild tile is any letter or kana you give it, and reads as that in every
60
393
  word it is part of.
@@ -62,167 +395,556 @@ alone, in races, round one device and across several with this package.
62
395
  crossword goes out. Everyone else has one last turn to do the same, and all
63
396
  who go out win. Alone, you use every tile in the bag and race the clock.
64
397
 
65
- ## Install
398
+ | Rule | Value | Constant |
399
+ | --- | --- | --- |
400
+ | Tiles in a set | 144 | `TILE_MIX`, `TILE_MIX_TOTAL`, `JAPANESE_TILE_MIX` |
401
+ | Opening hands | 7 or 11 (and 3, for tests and examples) | `KUMIMOJI_HANDS` |
402
+ | Tiles in a short game, for a hand of 3, 7, 11 | 5, 40, 50 | `KUMIMOJI_BAG`, `kumimojiTileCount` |
403
+ | Wild tiles in a short game of 40, at easy, medium, hard | 6, 3, 0 | `KUMIMOJI_WILDS`, `kumimojiWildCount` |
404
+ | A draw | 1 tile | `KUMIMOJI_DRAW` |
405
+ | A trade | 1 tile back for 3 | `KUMIMOJI_TRADE` |
406
+ | A diagonal run is read from | 3 tiles | `DIAGONAL_RUN_LEAST` |
407
+ | Players at a table | 2 to 8 | `KUMIMOJI_PARTY` |
408
+ | Points for a finished game | 10 a tile, and up to as much again for speed | `KUMIMOJI_SCORE`, `kumimojiPoints` |
409
+
410
+ ### Dealing a game
411
+
412
+ `generateKumimoji(hand, level, seed, options?)` takes a hand of 3, 7 or 11; a
413
+ level of `"easy"`, `"medium"` or `"hard"` (the most wild tiles, some, none);
414
+ and the options `language` (`"english"` or `"japanese"`), `gameLength`
415
+ (`"short"`, `"medium"`, or `"full"`, the whole set), `doubleSet` (two English
416
+ sets) and `diagonals`. It returns a `KumimojiDeal`: the bag as `givens`, one
417
+ character a tile in the order they come out, and one finished grid as
418
+ `solution`, which proves the bag can be finished and is never shown.
66
419
 
67
- ```sh
68
- pnpm add @johnmorrisdotca/kumimoji # or: npm install @johnmorrisdotca/kumimoji
69
- ```
420
+ ```ts
421
+ import "@johnmorrisdotca/kumimoji/words";
422
+ import { deal, generateKumimoji, loadTileWords, tilesLeft, wordsInHand } from "@johnmorrisdotca/kumimoji";
70
423
 
71
- Or straight from a GitHub release, pinned to its version:
424
+ const words = await loadTileWords("english");
425
+ const dealt = generateKumimoji(7, "medium", 2026);
426
+ dealt.givens.length; // → 40
427
+ dealt.givens.slice(0, 7); // → "xbsnnny"
428
+ [...dealt.givens].filter((tile) => tile === "*").length; // → 3
72
429
 
73
- ```sh
74
- pnpm add https://github.com/johnmorrisdotca/kumimoji/releases/download/v1.0.1/johnmorrisdotca-kumimoji-1.0.1.tgz
430
+ const play = deal(dealt.givens, 7);
431
+ tilesLeft(play); // → 33
432
+ wordsInHand(play.hand, words); // → ["by"]
75
433
  ```
76
434
 
77
- ES modules with types, and no dependencies. The React components need React 18
78
- or later.
435
+ ### Tiles, squares and grids
436
+
437
+ - **A square** is a string key, `squareAt(row, col)`, and `placeOf(square)`
438
+ reads it back. Rows and columns are any whole numbers: the table has no
439
+ edges.
440
+ - **A tile** is one character. An English letter is itself; `*` is a wild with
441
+ no letter; a capital is a wild given that letter (`assignHandTile`). A kana
442
+ is a private-use character, its place in `BASE_KANA` above
443
+ `KANA_TILE_START`, or above `KANA_WILD_START` for a wild given that kana.
444
+ `tileFace(tile)` says what is printed on any of them, and
445
+ the loaded list's `glyphOf`, `wordOf` and `wildFor` turn tiles into words
446
+ and back.
447
+ - **A grid** is a `Map` from square to tile. `encodeGrid` writes it as a
448
+ string drawn from its own top-left tile, `cat/2o/2w`, and `decodeGrid` reads
449
+ that back.
450
+
451
+ ### Playing
452
+
453
+ | Move | What it does |
454
+ | --- | --- |
455
+ | `deal(bag, handSize)` | The opening hand, and an empty table |
456
+ | `placeFromHand(play, handAt, square)` | A tile from the hand onto an empty square |
457
+ | `swapWithHand(play, handAt, square)` | A tile from the hand onto a square that holds one: they change places |
458
+ | `moveOnTable(play, from, to)` | A tile on the table to another square |
459
+ | `liftToHand(play, square)`, `liftAll(play)` | A tile, or every tile, back to the hand |
460
+ | `assignHandTile(play, handAt, face)`, `assignTableTile(play, square, face)` | Give a wild its letter or kana |
461
+ | `sortHand(play)` | The hand in order, wilds last |
462
+ | `draw(play)` | The next tile out of the bag; `mayDraw(play, verdict)` says whether it may be pressed |
463
+ | `trade(play, handAt)` | One tile back for three; `mayTrade(play)` says whether the bag has them |
464
+ | `isFinished(play, verdict)` | The bag empty, the hand used, every run a word in one piece |
465
+
466
+ Each returns a new `TilePlay` and leaves the one it was given alone.
79
467
 
80
- ## Play a game in code
468
+ ### Japanese
81
469
 
82
470
  ```ts
83
- import { deal, draw, generateKumimoji, judgeWithWords, loadTileWords, mayDraw, placeFromHand, squareAt } from "@johnmorrisdotca/kumimoji";
471
+ import "@johnmorrisdotca/kumimoji/words";
472
+ import { BASE_KANA, deal, generateKumimoji, loadTileWords, tileKana } from "@johnmorrisdotca/kumimoji";
84
473
 
85
- const words = await loadTileWords("english"); // fetched once, the first time
86
- const dealt = generateKumimoji(7, "medium", 2026); // a hand of 7, some wilds, seed 2026
87
- let play = deal(dealt.givens, 7);
474
+ const words = await loadTileWords("japanese");
475
+ [...BASE_KANA].length; // → 45
476
+ [tileKana("が"), tileKana("ゃ"), tileKana("を"), tileKana("っ")]; // → ["か", "や", "お", "つ"]
477
+ words.allowed.size; // → 163461
478
+ words.allowed.has("ことは"); // → true
88
479
 
89
- play = placeFromHand(play, 0, squareAt(0, 0)); // the first tile of the hand, on row 0, column 0
90
- const verdict = judgeWithWords(play.tiles, words);
91
- if (mayDraw(play, verdict)) play = draw(play);
480
+ const dealt = generateKumimoji(7, "medium", 2026, { language: "japanese" });
481
+ deal(dealt.givens, 7).hand.map(words.glyphOf).join(""); // → "よきめたんえら"
92
482
  ```
93
483
 
94
- - `generateKumimoji(hand, level, seed, options?)`: a hand of 3, 7 or 11; a
95
- level of `"easy"`, `"medium"` or `"hard"` (more wild tiles, fewer, none);
96
- options `language`, `gameLength` (`"short"`, `"medium"`, or `"full"`, the
97
- whole set), `doubleSet` (two English sets) and `diagonals`. Returns the bag as
98
- `givens` and one finished grid as `solution`.
99
- - A square is a string key, `squareAt(row, col)`; the grid has no edges.
100
- - A tile is one character: an English letter, `*` for a wild with no letter, a
101
- capital for a wild given that letter (`assignHandTile`), and for Japanese a
102
- private-use character per kana (`tileFace` says what is printed on any tile).
103
- - `isFinished(play, verdict)`: the hand empty, the bag empty, and every run a
104
- word in one piece.
484
+ The tiles are the 45 base hiragana. A word is spelt with the base of each of
485
+ its kana, so ことば is played, and listed, as ことは: a tile shows the other
486
+ forms it plays as in its corner (`CORNER_FORMS`, `formsOfTile`).
105
487
 
106
- ### Checking a finished game
488
+ ### A table of players
107
489
 
108
490
  ```ts
109
- import { checkKumimoji, encodeGrid } from "@johnmorrisdotca/kumimoji";
491
+ import "@johnmorrisdotca/kumimoji/words";
492
+ import { decodeParty, encodeParty, endTurn, generateKumimoji, handCanSpell, judgeWithWords, loadTileWords, nameOf, placeFromHand, seatPlay, squareAt, startParty, withSeatPlay } from "@johnmorrisdotca/kumimoji";
493
+
494
+ const words = await loadTileWords("english");
495
+ const settings = { size: 7, level: "medium", seed: 2026, gameLength: "short", language: "english", doubleSet: false, diagonals: false, hints: false } as const;
496
+ const dealt = generateKumimoji(settings.size, settings.level, settings.seed);
110
497
 
111
- checkKumimoji(7, dealt.givens, encodeGrid(play.tiles), { level: "medium" });
112
- // { ok: true } or { ok: false, reason: "zqx is not in the word list" }
498
+ let game = startParty(settings, dealt.givens, ["Ann", { name: "Computer 1", computer: true }]);
499
+ game.players.map((player) => player.hand.length); // → [7, 7]
500
+ nameOf(game, game.turn); // → "Ann"
501
+
502
+ // The player to move plays their own hand and table as a TilePlay:
503
+ game = withSeatPlay(game, placeFromHand(seatPlay(game), 1, squareAt(0, 0)));
504
+ game = withSeatPlay(game, placeFromHand(seatPlay(game), 5, squareAt(0, 1)));
505
+ const verdict = judgeWithWords(seatPlay(game).tiles, words);
506
+ verdict.sound; // → true
507
+ game = endTurn(game, verdict, (hand) => handCanSpell(hand, words));
508
+ nameOf(game, game.turn); // → "Computer 1"
509
+
510
+ decodeParty(encodeParty(game), words.familyKey)!.turn; // → 1
113
511
  ```
114
512
 
115
- It checks that the grid uses exactly the tiles of the bag, is all one piece, and
116
- that every run is a word. Nothing is searched, so a server can run it on every
117
- game handed in.
513
+ Ann lays BY and ends her turn. `afterComputerTurn(game, words)` plays a
514
+ computer seat's whole turn, and `planComputerTurn` lists its steps, for a page
515
+ to show them one at a time. `encodeParty` keeps a table as text, and
516
+ `decodeParty` reads it back, checking every tile held against the bag.
118
517
 
119
- ### The word lists
518
+ ## Keeping a game, export and import
120
519
 
121
- In a browser, `loadTileWords(language)` fetches the list as its own module the
122
- first time it is needed (about 600 KB for English, 700 KB for Japanese before
123
- compression), so nothing else carries it. On a server, in a test or in a script,
124
- import the `words` entry once, which reads the lists from this package's files:
520
+ A game alone is kept as how it was set up, its seed, and where the player has
521
+ got to; never the bag, which the seed deals again.
125
522
 
126
523
  ```ts
127
524
  import "@johnmorrisdotca/kumimoji/words";
525
+ import { KUMIMOJI_STRINGS, deal, draw, encodeGrid, generateKumimoji, kumimojiFromJSON, kumimojiToJSON, kumimojiToText, loadTileWords, placeFromHand, squareAt } from "@johnmorrisdotca/kumimoji";
526
+
527
+ await loadTileWords("english");
528
+ const dealt = generateKumimoji(3, "hard", 44);
529
+ let play = deal(dealt.givens, 3);
530
+ play = placeFromHand(play, 0, squareAt(0, 0));
531
+ play = placeFromHand(play, 1, squareAt(0, 1));
532
+ play = placeFromHand(play, 0, squareAt(0, 2));
533
+ play = placeFromHand(draw(play), 0, squareAt(0, -1));
534
+ const saved = { deal: dealt, play, elapsedMs: 65_000 };
535
+
536
+ const text = kumimojiToJSON(saved);
537
+ JSON.parse(text).progress; // → "4:::snow"
538
+ const back = await kumimojiFromJSON(text);
539
+ encodeGrid(back!.play.tiles); // → "snow"
540
+ back!.play.bag; // → "nwosy"
541
+
542
+ await kumimojiFromJSON(text.replace("4:::snow", "4:::snob")); // → null
543
+ await kumimojiFromJSON("not a game"); // → null
544
+
545
+ kumimojiToText(saved); // → "Kumimoji: English, a hand of 3, seed 44\nS N O W\nHand: empty\nIn the bag: 1\nTime: 1:05\n"
546
+ kumimojiToText(saved, KUMIMOJI_STRINGS.ja).split("\n")[0]; // → "組み文字: 英語、手札3枚、シード 44"
128
547
  ```
129
548
 
130
- `tileWords(language)` then hands back the loaded list: `allowed` (every word),
131
- `byLength`, the tile `mix`, and the functions that turn tiles into words.
549
+ The JSON, as `kumimojiToJSON` writes it:
550
+
551
+ ```json
552
+ {
553
+ "format": 1,
554
+ "game": "kumimoji",
555
+ "generator": "kumimoji 1.1.1",
556
+ "size": 3,
557
+ "level": "hard",
558
+ "seed": 44,
559
+ "language": "english",
560
+ "gameLength": "short",
561
+ "doubleSet": false,
562
+ "diagonals": false,
563
+ "progress": "4:::snow",
564
+ "elapsedMs": 65000
565
+ }
566
+ ```
132
567
 
133
- ### A table of players
568
+ - **The JSON reads back in, and nothing in it is trusted.** `kumimojiFromJSON`
569
+ deals the bag again from the seed, and opens the game only if every tile in
570
+ the hand and on the table is one that came out of that bag and was not
571
+ given back. A B nobody drew, a later `format`, another game's file or
572
+ anything that is not JSON resolves to `null`. It is a promise because it
573
+ loads the word list of the game's language first.
574
+ - **`format`** is `KUMIMOJI_EXPORT_FORMAT`, and goes up only when a reader of
575
+ the old shape would be wrong about the new one.
576
+ - **`progress`** is what `encodeTileProgress` writes: how many tiles have been
577
+ taken, the tiles traded back, the hand in its order, and the grid
578
+ (`encodeGrid`), with colons between. `decodeTileProgress(progress, bag,
579
+ language)` reads it against a bag.
580
+ - **The text** is the crossword as it lies, for a chat or a note:
581
+ `gridToText(tiles, language)` alone is the grid, English letters spaced with
582
+ a dot for an empty square, kana side by side with a full-width dot.
583
+ - **There is no CSV.** A crossword is not a table of rows, and a spreadsheet
584
+ has nothing to do with one.
585
+ - **A table of players** is kept by `encodeParty` and read by `decodeParty`,
586
+ as above.
587
+ - **In the table** it is *Save and load*, under the buttons: save as JSON or
588
+ as text, and load a saved game back, its clock going on from where it was.
589
+
590
+ ## The word lists and their licences
591
+
592
+ **The code of this package is MIT. The two word lists are other people's
593
+ work, under their own terms**, and [NOTICE.md](./NOTICE.md) sets the notices
594
+ out in full. It is shipped in the package, and each list's own file opens
595
+ with its notice too.
596
+
597
+ | List | What it is | Where it comes from | Terms |
598
+ | --- | --- | --- | --- |
599
+ | English, `dist/words.en.data.js` | 110,316 words of 2 to 15 letters, English and American spellings | [SCOWL](http://wordlist.aspell.net/) 2020.12.07, sizes 10 to 70 | © 2000–2018 Kevin Atkinson and the authors of the lists SCOWL is built from. Permission to use, copy, modify, distribute and sell for any purpose, **provided the copyright notice appears in all copies, and both it and the permission notice in supporting documentation**. No share-alike. |
600
+ | Japanese, `dist/words.ja.data.js` | 163,461 hiragana readings of 2 to 15 kana, each spelt in the 45 base kana of the tiles | [JMdict](https://www.edrdg.org/jmdict/j_jmdict.html), release 2026-09-28, of the Electronic Dictionary Research and Development Group | **[CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)**, with the [EDRDG's conditions](https://www.edrdg.org/edrdg/licence.html). **Attribution is required, and the list, or anything made from it, may be passed on only under the same licence.** |
601
+
602
+ What that means for a project that uses this package:
603
+
604
+ - **Using the package in a site or an app**, MIT code and all, asks for the
605
+ notices: keep `NOTICE.md` with the package, and where your project lists its
606
+ credits, credit SCOWL, and JMdict and the EDRDG.
607
+ - **The Japanese list is share-alike.** If you copy it, change it, or make a
608
+ list of your own from it, and pass that on, it stays under CC BY-SA 4.0 and
609
+ says where it came from. The code stays MIT.
610
+ - **If you need only one list**, load only that one: each is a file of its
611
+ own, nothing else in the package carries a word, and a page fetches a list
612
+ only when it deals a game in that language.
613
+
614
+ This is a description, not legal advice; the licences themselves are what
615
+ hold.
616
+
617
+ ### Loading them
618
+
619
+ In a browser, `loadTileWords(language)` fetches the list as its own module
620
+ the first time it is needed, so no page carries it that does not deal a game.
621
+ On a server, in a test or in a script there is no browser to fetch it, and the
622
+ `words` entry reads the lists from this package's files instead. Import it
623
+ once, before anything that deals or judges:
134
624
 
135
625
  ```ts
136
- import { endTurn, handCanSpell, judgeWithWords, placeFromHand, seatPlay, squareAt, startParty, withSeatPlay } from "@johnmorrisdotca/kumimoji";
137
-
138
- let game = startParty(
139
- { size: 7, level: "medium", seed: 1, gameLength: "short", language: "english", doubleSet: false, diagonals: false, hints: false },
140
- dealt.givens,
141
- ["Ann", { name: "Computer", computer: true }],
142
- );
143
- // The player to move plays their own hand and table as a TilePlay:
144
- game = withSeatPlay(game, placeFromHand(seatPlay(game), 0, squareAt(0, 0)));
145
- game = endTurn(game, judgeWithWords(seatPlay(game).tiles, words), (hand) => handCanSpell(hand, words));
626
+ import "@johnmorrisdotca/kumimoji/words";
627
+ import { loadTileWords, tileWords, tileWordsReady } from "@johnmorrisdotca/kumimoji";
628
+
629
+ const words = await loadTileWords("english");
630
+ tileWordsReady("english"); // → true
631
+ words.allowed.size; // → 110316
632
+ words.allowed.has("crossword"); // → true
633
+ words.allowed.has("kumimoji"); // → false
634
+ tileWords("english") === words; // → true
146
635
  ```
147
636
 
148
- `afterComputerTurn(game, words)` plays a computer seat's whole turn;
149
- `planComputerTurn` lists its steps, for a page to show them one at a time.
637
+ `tileWords(language)` hands back the list already loaded, and throws where it
638
+ has not been: a check that cannot read the list must not answer. A `TileWords`
639
+ has `allowed` (every word), `byLength`, the tile `mix`, and the functions that
640
+ turn tiles into words: `glyphOf`, `soundOf`, `wordOf`, `wildFor`, `isWild`,
641
+ `wildSound`, `inventoryKey`, `familyKey`, `formsOf`, `wildOptions` and
642
+ `codeOf`.
150
643
 
151
- ## Play on a page
644
+ ## API
152
645
 
153
- ```html
154
- <div id="table"></div>
155
- <script type="module">
156
- import { mountKumimoji } from "@johnmorrisdotca/kumimoji/ui";
157
- mountKumimoji(document.getElementById("table"), {
158
- language: "english",
159
- hand: 7,
160
- onFinish: ({ elapsedMs }) => console.log(`Finished in ${elapsedMs / 1000}s`),
161
- });
162
- </script>
646
+ The [API reference](https://johnmorrisdotca.github.io/kumimoji/api.html) 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.
647
+
648
+ Every function, type and constant has a doc comment, so an editor shows this
649
+ as you type. The entries:
650
+
651
+ | Entry | What it holds |
652
+ | --- | --- |
653
+ | `@johnmorrisdotca/kumimoji` | Dealing, playing, judging, the table of players, the computer player, keeping and export, and the table's words |
654
+ | `@johnmorrisdotca/kumimoji/words` | Reads both word lists from this package's files, for a server, a test or a script: `loadTileWordsFromModule` |
655
+ | `@johnmorrisdotca/kumimoji/ui` | `mountKumimoji`, a whole game alone in plain DOM, and `boardModel` |
656
+ | `@johnmorrisdotca/kumimoji/react` | `KumimojiBoard` and `KumimojiTable` |
657
+
658
+ ### Dealing and judging
659
+
660
+ | Export | What it does |
661
+ | --- | --- |
662
+ | `generateKumimoji(hand, level, seed, options?)` | A game dealt from a seed: the bag, and one grid that finishes it |
663
+ | `judgeWithWords(tiles, words, rules?)` | What is wrong with a grid: the tiles in runs that are not words, the tiles apart, and whether it is sound |
664
+ | `judgeGrid(tiles, isWord, readable?, rules?)` | The same against any test of a word |
665
+ | `runsOf(tiles, rules?)`, `groupsOf(tiles, links?)` | Every run the grid is read by; the tiles in groups that touch |
666
+ | `checkKumimoji(size, givens, answer, options?)` | Whether a grid finishes a game: the one check a server runs |
667
+ | `kumimojiPoints(givens, elapsedMs)` | A finished game's points |
668
+ | `readWilds(tiles, words, rules?)`, `WILD_SEARCH_MOST` | A grid with each wild that has no letter read as the one that makes its runs words, or `null`; and how far it looks before giving up |
669
+ | `wordsInHand(hand, words)`, `handSpelling(play, word)` | Help: the words a hand spells, and the hand arranged as one |
670
+ | `crossingFit`, `diagonalsRead` | Where a word may be laid crossing a tile, and the diagonal runs a new tile would make, for a generator or a computer player |
671
+ | `seededRandom(seed)`, `shuffled(items, random)` | The seeded random a deal is drawn from |
672
+
673
+ ### Tiles and grids
674
+
675
+ | Export | What it does |
676
+ | --- | --- |
677
+ | `squareAt(row, col)`, `placeOf(square)` | A square's key, and back |
678
+ | `encodeGrid(tiles)`, `decodeGrid(code)`, `boundsOf(tiles)` | A grid as a string, and back; the rows and columns it stands within |
679
+ | `lettersOf(letters)`, `sameLetters(a, b)` | How many of each tile, and whether two tallies match |
680
+ | `tileFace(tile)`, `tileDescription(tile)`, `kanaTileCode(kana, wild?)` | What a tile shows; what a screen reader says; a kana's tile |
681
+ | `tileKana(kana)`, `BASE_KANA`, `CORNER_FORMS`, `formsOfTile(kana)` | The tile a kana is played with, the 45 tiles, and the forms each plays as |
682
+ | `familyKeyOf(language)` | Which tile of the set a code is, without the word list |
683
+ | `loadTileWords`, `tileWords`, `tileWordsReady` | The word list: fetched, handed back, asked after |
684
+ | `unpackTileWords`, `unpackLength`, `tileWordsFrom`, `readTileWordsWith` | How a list is read from its packed data, for the loader |
685
+ | `mixShown(language)`, `lengthRows(hand?)`, `LENGTHS`, `TRY_IT` | A set and the lengths of game as a page shows them |
686
+
687
+ ### Playing, and a game kept
688
+
689
+ The moves are in [Playing](#playing) above. With them:
690
+
691
+ | Export | What it does |
692
+ | --- | --- |
693
+ | `tilesLeft(play)` | How many tiles are still in the bag |
694
+ | `encodeTileProgress(play)`, `readTileProgress(code)`, `decodeTileProgress(code, bag, language?)` | A game half way as one string; its shape read; and opened again on its own bag |
695
+ | `kumimojiToJSON(saved)`, `kumimojiFromJSON(text)`, `kumimojiExported(saved)` | A game as JSON, and back, or `null` |
696
+ | `kumimojiToText(saved, strings?)`, `gridToText(tiles, language?)`, `kumimojiClock(ms)` | A game, a grid and a time as text |
697
+ | `KUMIMOJI_EXPORT_FORMAT`, `KUMIMOJI_VERSION` | The JSON's format number, and this package's version |
698
+ | `KUMIMOJI_STRINGS`, `kumimojiStrings(locale, own?)`, `kumimojiSay(line, values)` | The table's words in English and Japanese |
699
+
700
+ ### A table of players
701
+
702
+ | Export | What it does |
703
+ | --- | --- |
704
+ | `startParty(settings, bag, seats)` | A table dealt: two to eight seats, each a name or `{ name, computer }` |
705
+ | `seatPlay(game)`, `withSeatPlay(game, play)` | The player to move's hand and table as a `TilePlay`, and the game with it put back |
706
+ | `endTurn(game, verdict, handSpells)`, `doneRefused(…)` | End the turn; why Done is refused, if it is |
707
+ | `drawAll(game)`, `mayDrawAll(game, verdict)` | Everyone draws together |
708
+ | `resign(game)`, `mayResign(game)` | Give up the game |
709
+ | `joinParty(game, seat)`, `joinRefused(game)`, `leaveParty(game, at)`, `leaveRefused(game, at)` | Joining and leaving a game under way |
710
+ | `isOver(game)`, `winnersOf(game)`, `goesOut(game, verdict)`, `isLastTurn(game)`, `lastStanding(game)` | How the game stands and ends |
711
+ | `mayTradeThisTurn(game)`, `mustTradeFirst(game, handSpells)`, `laidThisTurn(game)`, `handCanSpell(hand, words)` | What this turn may and must do |
712
+ | `nameOf(game, at)`, `tidyName(name)`, `computerName(players)`, `isComputer(game, at)` | Names, and whether a seat is a computer's |
713
+ | `stillIn(game)`, `nextIn(game, from)`, `partyTilesLeft(game)`, `tilesHeldBy(game, at)`, `tableTally(tiles)` | Who is in, whose turn is next, and the tiles |
714
+ | `partyTilesNeeded`, `partyFits`, `partyLength`, `partyPlayersAsked` | Whether a bag is big enough for a table, for a set-up screen |
715
+ | `passViewStart(game)`, `stepView(game, from, by)` | Whose table a shared screen shows |
716
+ | `encodeParty(game)`, `decodeParty(code, familyKey?)`, `readSettings(value)`, `holdsItsBag(game, familyKey)`, `isPartyFor` | A table kept as text, read back and checked, and matched to the set-up it was dealt for |
717
+ | `afterComputerTurn(game, words)`, `planComputerTurn(game, words)`, `COMPUTER_STEPS_MOST` | A computer seat's turn, whole or step by step |
718
+ | `bestLaying(play, words, rules?)`, `tradeChoice(hand, words)`, `judgeTiles(tiles, words, rules?)` | What the computer would lay, and trade |
719
+
720
+ ### Looking at the table
721
+
722
+ A table with no edges is looked at through a view, which the package keeps
723
+ apart from the game: `tableArea`, `fitView`, `fittedMost`, `zoomView`,
724
+ `panView`, `keepInReach`, `overflows`, `squareUnder`, `edgePan` and the
725
+ numbers in `TABLE`. A player may turn the table a quarter at a time with every
726
+ tile kept upright: `nextTurn`, `turnPlace`, `unturnPlace`, `turnArea`,
727
+ `turnView`, `typingWay` and `arrowStep`.
728
+
729
+ The types are `TilePlay`, `Tiles`, `GridVerdict`, `GridRules`, `Run`,
730
+ `RunLine`, `Bounds`, `Square`, `CrossingFit`, `KumimojiDeal`,
731
+ `KumimojiOptions`, `KumimojiCheck`, `KumimojiLevel`, `KumimojiLength`,
732
+ `KumimojiLanguage`, `TileWords`, `TileFaceOf`, `PartyGame`, `PartySettings`,
733
+ `PartyPlayer`, `PartySeat`, `PartyEnding`, `JoinRefusal`, `LeaveRefusal`,
734
+ `ComputerStep`, `ComputerSaid`, `KumimojiSaved`, `KumimojiExported`, `KumimojiStrings`,
735
+ `KumimojiLocale`, `Random`, `Turn`, `Area`, `View`, `MixTile`, `MixShown` and
736
+ `LengthRow`.
737
+
738
+ ### The table
739
+
740
+ `mountKumimoji(element, options?)` from `@johnmorrisdotca/kumimoji/ui` deals
741
+ and plays a whole game alone in the element, and returns a handle.
742
+
743
+ | Option | Default | What it does |
744
+ | --- | --- | --- |
745
+ | `language` | `"english"` | The language of the tiles: `"english"` or `"japanese"` |
746
+ | `hand` | `7` | The opening hand: 3, 7 or 11 tiles |
747
+ | `level` | `"medium"` | Wild tiles: `"easy"` the most, `"medium"` some, `"hard"` none |
748
+ | `gameLength` | `"short"` | `"short"`, `"medium"`, or `"full"`, all 144 tiles |
749
+ | `diagonals` | `false` | Every diagonal run of three or more must be a word too |
750
+ | `seed` | a new one each game | The seed the bag is dealt from |
751
+ | `onFinish` | | Called when the last tile is laid and every run is a word, with the time it took |
752
+ | `onChange` | | Called after every change, with the game as it would be saved |
753
+ | `locale` | the page's `lang` | The language of the table's words: `"en"` or `"ja"` |
754
+ | `strings` | | Words of your own, laid over the locale's |
755
+ | `theme` | | CSS variables set on the table itself |
756
+ | `keep` | `true` | Whether the table offers saving and loading |
757
+
758
+ | Handle | What it does |
759
+ | --- | --- |
760
+ | `play()` | The game as it stands, or `null` while it is being dealt |
761
+ | `saved()` | The game as it would be saved, or `null` while it is being dealt |
762
+ | `newGame(options?)` | A new game; any of the first six options may change |
763
+ | `setGame(saved)` | Put a game on the table: one read back by `kumimojiFromJSON` |
764
+ | `setLocale(locale, strings?)` | Change the language of the table's words |
765
+ | `destroy()` | Take the table off the page and stop its clock |
766
+
767
+ To play it: tap a tile in your hand and then a square to lay it; tap a tile on
768
+ the table and then a square to move it, or tap it again to take it back. A
769
+ wild is given its letter before it is laid. To draw a board yourself, the same
770
+ entry has `boardModel(tiles, verdict, glyphOf?)`, which works out the squares
771
+ to draw and how each tile stands, with `BOARD_MARGIN` and `BOARD_LEAST`, and
772
+ `KUMIMOJI_STYLE`, the table's stylesheet as a string.
773
+
774
+ ### The React components
775
+
776
+ ```jsx
777
+ import { useEffect, useState } from "react";
778
+ import { deal, generateKumimoji, judgeWithWords, liftToHand, loadTileWords, placeFromHand } from "@johnmorrisdotca/kumimoji";
779
+ import { KumimojiBoard } from "@johnmorrisdotca/kumimoji/react";
780
+
781
+ export function Board() {
782
+ const [words, setWords] = useState(null);
783
+ const [play, setPlay] = useState(null);
784
+ useEffect(() => {
785
+ void loadTileWords("english").then((loaded) => {
786
+ setWords(loaded);
787
+ setPlay(deal(generateKumimoji(7, "hard", 2026).givens, 7));
788
+ });
789
+ }, []);
790
+ if (words === null || play === null) return <p>Dealing…</p>;
791
+ // A press on an empty square lays the first tile of the hand there; a press on a tile takes it back.
792
+ const press = (square) => setPlay(play.tiles.has(square) ? liftToHand(play, square) : play.hand.length > 0 ? placeFromHand(play, 0, square) : play);
793
+ return <KumimojiBoard tiles={play.tiles} verdict={judgeWithWords(play.tiles, words)} glyphOf={words.glyphOf} onSquare={press} square={44} />;
794
+ }
163
795
  ```
164
796
 
165
- Options: `language`, `hand`, `level`, `gameLength`, `diagonals`, `seed`,
166
- `onFinish`. The handle has `play()`, `newGame(options?)` and `destroy()`.
167
- Colours are CSS variables on `.km-root` (`--km-felt`, `--km-tile` and the
168
- rest), light and dark.
797
+ | `KumimojiBoard` prop | What it does |
798
+ | --- | --- |
799
+ | `tiles` | The tiles on the table: `play.tiles` |
800
+ | `verdict` | From `judgeWithWords`: rings the runs that are not words and fades the tiles apart |
801
+ | `glyphOf` | What to print on a tile: `words.glyphOf` for the language played |
802
+ | `onSquare` | Called with a square's key when it is pressed, empty or not |
803
+ | `held` | A square to ring, such as the one holding a tile picked up |
804
+ | `square` | The side of a square in pixels; 36 by default |
805
+ | anything else | Passed to the `<div>` |
806
+
807
+ `KumimojiTable` takes every option of `mountKumimoji` as a prop, and anything
808
+ else for its `<div>`. Options are read when it mounts; give it a new `key` to
809
+ start over with different ones.
810
+
811
+ ## Theming
812
+
813
+ Every colour is a CSS variable on `.km-root`. Set them in your stylesheet, on
814
+ the table's element or any ancestor, or pass them as `theme`, which sets them
815
+ on the table itself and so wins in light and dark alike.
816
+
817
+ | Variable | What it sets | Light | Dark |
818
+ | --- | --- | --- | --- |
819
+ | `--km-square` | The side of a square and of a tile | `44px` | the same |
820
+ | `--km-felt` | The table | `#2f5d4a` | `#1e3a2f` |
821
+ | `--km-grid-line` | An empty square | `rgba(255,255,255,.12)` | the same |
822
+ | `--km-tile`, `--km-tile-ink` | A tile and its letter | `#f3e6c8`, `#2a2118` | the same |
823
+ | `--km-wild` | A wild tile | `#f6d27a` | the same |
824
+ | `--km-wrong` | The ring round a run that is not a word | `#d9534f` | the same |
825
+ | `--km-held` | The ring round the tile picked up | `#ffcf3f` | the same |
826
+ | `--km-ink` | Text | `#1f2320` | `#ece8dc` |
827
+ | `--km-panel` | The hand, the buttons, and saving and loading | `#f7f3ea` | `#1d201e` |
828
+ | `--km-line` | Button edges | `rgba(20,20,20,.35)` | `rgba(255,255,255,.25)` |
829
+ | `--km-accent`, `--km-accent-ink` | The main button | `#2f5d4a`, `#fff` | `#6fb08f`, `#10150f` |
830
+ | `--km-radius` | The table's corners | `12px` | the same |
831
+ | `--km-font` | The typeface | `system-ui, …` | the same |
832
+
833
+ A slate table with ivory tiles, a little smaller:
834
+
835
+ ```js
836
+ import { mountKumimoji } from "@johnmorrisdotca/kumimoji/ui";
837
+
838
+ mountKumimoji(document.getElementById("table"), {
839
+ theme: { "--km-felt": "#23405a", "--km-tile": "#fffdf7", "--km-wild": "#d4a017", "--km-accent": "#b5452c", "--km-square": "40px" },
840
+ });
841
+ ```
842
+
843
+ `KumimojiBoard` draws with colours of its own; style it through its `style`
844
+ and `className`.
845
+
846
+ ## Languages
169
847
 
170
- ### In React
848
+ Two things here have a language, and they are chosen apart. **The tiles** are
849
+ English letters or Japanese kana: the `language` option. **The table's words**,
850
+ its buttons and status lines, are English or Japanese: `locale`, or the page's
851
+ `lang`, changed at any time with `setLocale`. An English page can play kana.
171
852
 
172
- ```tsx
173
- import { KumimojiBoard, KumimojiTable } from "@johnmorrisdotca/kumimoji/react";
853
+ The demo has a chooser of its own, follows the browser's language on a first
854
+ visit, and takes `?lang=ja` or `?lang=en` in the address. **Japanese:
855
+ included; not yet reviewed by a native reader. Corrections welcome.** Every
856
+ Japanese string is listed beside its English in
857
+ [docs/strings-ja.md](./docs/strings-ja.md), and there is an
858
+ [issue template](https://github.com/johnmorrisdotca/kumimoji/issues/new?template=fix-a-translation.md)
859
+ for fixing one. Any other language is a table of your own passed as `strings`:
174
860
 
175
- <KumimojiBoard tiles={play.tiles} verdict={verdict} glyphOf={words.glyphOf} onSquare={(square) => press(square)} />
176
- <KumimojiTable language="japanese" hand={7} />
861
+ ```ts
862
+ import { KUMIMOJI_STRINGS, kumimojiSay, kumimojiStrings } from "@johnmorrisdotca/kumimoji";
863
+
864
+ kumimojiSay(KUMIMOJI_STRINGS.ja.left, { n: 12, time: "1:05" }); // → "袋の残り12枚 · 1:05"
865
+ kumimojiStrings("en", { draw: "Robar" }).draw; // → "Robar"
177
866
  ```
178
867
 
179
- ## API at a glance
868
+ A new language for the tiles is another matter: it needs a real dictionary of
869
+ that language, whose licence lets its words be shipped.
180
870
 
181
- | Entry | What it holds |
182
- | --- | --- |
183
- | `@johnmorrisdotca/kumimoji` | Dealing: `generateKumimoji`, `KUMIMOJI_HANDS`; playing alone: `deal`, `placeFromHand`, `moveOnTable`, `swapWithHand`, `liftToHand`, `liftAll`, `sortHand`, `assignHandTile`, `draw`, `trade`, `mayDraw`, `mayTrade`, `isFinished`; judging: `judgeWithWords`, `runsOf`, `checkKumimoji`, `kumimojiPoints`; words: `loadTileWords`, `tileWords`, `wordsInHand`; a table: `startParty`, `endTurn`, `drawAll`, `resign`, `joinParty`, `leaveParty`, `encodeParty`, `decodeParty`; the computer: `planComputerTurn`, `afterComputerTurn`; views: `fitView`, `zoomView`, `panView`, `turnView`; and every type (`TilePlay`, `PartyGame`, `GridVerdict`, …). |
184
- | `@johnmorrisdotca/kumimoji/words` | Loads both word lists from this package's files, for a server, a test or a script. |
185
- | `@johnmorrisdotca/kumimoji/ui` | `mountKumimoji`, a whole game in plain DOM; `boardModel` for drawing a board yourself. |
186
- | `@johnmorrisdotca/kumimoji/react` | `KumimojiBoard` and `KumimojiTable`. |
871
+ ## Limits
187
872
 
188
- Every function is typed and documented in the source, and your editor shows the
189
- documentation as you type.
873
+ | Limit | Value | Constant |
874
+ | --- | --- | --- |
875
+ | Opening hand | 3, 7 or 11 | `KUMIMOJI_HANDS` |
876
+ | A word | 2 to 15 tiles | |
877
+ | A finished grid, either way | 60 squares | `KUMIMOJI_GRID_MOST` |
878
+ | Players at a table | 2 to 8 | `KUMIMOJI_PARTY` |
879
+ | A player's name | 20 characters | `KUMIMOJI_PARTY` |
880
+ | Words one hand is offered as help | 40 | `HELP_WORDS_MOST` |
881
+ | A seed | a whole number from 0 to 4,294,967,295 | |
882
+ | Two sets at once | English only | |
190
883
 
191
884
  ## Browser support
192
885
 
193
- Any browser that runs ES2022 modules: current Chrome, Edge, Firefox and Safari,
194
- on a desk or a phone. The rules have no DOM in them and run the same in Node 20
195
- or later, Deno, Bun and web workers. The table follows the system's light or
196
- dark mode and needs nothing but a container element.
886
+ Any current browser: Chrome, Edge, Firefox and Safari, on a desk or a phone.
887
+ It needs ES2020 and dynamic `import()`. The table is tested in Chromium and in
888
+ WebKit, Safari's engine, at phone size with touch and on a desktop. The rules
889
+ run in Node 20 and later, Deno, Bun and web workers, and load by `import` and
890
+ by `require()`.
197
891
 
198
- ## Develop
892
+ ## The command line
199
893
 
200
- ```sh
201
- pnpm install
202
- pnpm test # the rules, the lists, the table and the computer player
203
- pnpm run build # dist/
204
- pnpm run site # the demo in site/, as GitHub Pages serves it
894
+ There is none, on purpose. Checking a word or seeing a deal is a few lines of
895
+ Node, and a command that did only that would be a dictionary tool, which this
896
+ package is not:
897
+
898
+ ```ts
899
+ import "@johnmorrisdotca/kumimoji/words";
900
+ import { generateKumimoji, loadTileWords } from "@johnmorrisdotca/kumimoji";
901
+
902
+ const words = await loadTileWords("english");
903
+ words.allowed.has("snowy"); // → true
904
+ [...generateKumimoji(3, "hard", 44).givens].join(" "); // → "n w o s y"
205
905
  ```
206
906
 
207
907
  ## Roadmap
208
908
 
209
- - More languages, each from a real dictionary of that language.
210
- - Playing the plain-DOM table from a keyboard, as the React one can.
909
+ - More languages for the tiles, each from a real dictionary of that language.
910
+ - Playing the plain-DOM table from a keyboard, as the React one on Itsutsu
911
+ can be.
912
+ - A table of players in plain DOM, beside the game alone.
211
913
  - A computer player at three strengths.
212
914
  - Word definitions for a finished crossword.
915
+ - A Vue wrapper and a web component, beside the React one.
916
+
917
+ Left out on purpose: play between devices, which needs a server (a table is
918
+ kept as text made to be sent, and Itsutsu sends it; the sending is yours); a
919
+ command line; CSV; and any name, art or wording of a published game. Kumimoji
920
+ runs from a static page and costs nothing to host.
921
+
922
+ Ideas and pull requests are welcome.
213
923
 
214
924
  ## Contributing
215
925
 
216
- Issues and pull requests are welcome. [CONTRIBUTING.md](CONTRIBUTING.md) says
217
- how to set up, what the checks are and how a change is written up, and everyone
218
- taking part follows the [code of conduct](CODE_OF_CONDUCT.md).
926
+ See [CONTRIBUTING.md](./CONTRIBUTING.md). In short:
927
+
928
+ ```sh
929
+ pnpm install
930
+ pnpm check # lint, types and tests
931
+ pnpm test:table # the demo in real browsers, by taps
932
+ pnpm site # build the demo into ./site, then serve it
933
+ ```
934
+
935
+ Please follow the [code of conduct](./CODE_OF_CONDUCT.md).
936
+
937
+ ## Changes
938
+
939
+ See [CHANGELOG.md](./CHANGELOG.md).
219
940
 
220
- ## Licence and notices
941
+ ## Licence
221
942
 
222
- The code is MIT, © John Morris; see [LICENSE](LICENSE).
943
+ The code is [MIT](./LICENSE) © John Morris.
223
944
 
224
- The word lists are other people's work, used under their own terms, and set out
225
- in full in [NOTICE.md](NOTICE.md):
945
+ The word lists are other people's work, under their own terms, set out in
946
+ [The word lists and their licences](#the-word-lists-and-their-licences) and in
947
+ full in [NOTICE.md](./NOTICE.md):
226
948
 
227
949
  - **English**: from [SCOWL](http://wordlist.aspell.net/), © Kevin Atkinson and
228
950
  others, under a permissive licence whose notice travels with the lists.
@@ -230,4 +952,5 @@ in full in [NOTICE.md](NOTICE.md):
230
952
  [JMdict](https://www.edrdg.org/jmdict/j_jmdict.html), © the Electronic
231
953
  Dictionary Research and Development Group, under
232
954
  [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). The
233
- Japanese list file (`src/words.ja.data.ts`) is shared under the same licence.
955
+ Japanese list (`src/words.ja.data.ts`, shipped as `dist/words.ja.data.js`)
956
+ is shared under the same licence.