@johnmorrisdotca/kumimoji 1.0.1 → 1.1.0
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/CHANGELOG.md +49 -1
- package/CONTRIBUTING.md +65 -20
- package/README.md +810 -146
- package/dist/computerTurn.d.ts +1 -0
- package/dist/computerTurn.js +1 -0
- package/dist/export.d.ts +87 -0
- package/dist/export.js +158 -0
- package/dist/grid.d.ts +4 -1
- package/dist/grid.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/kumimoji.types.d.ts +2 -0
- package/dist/partyKept.d.ts +1 -0
- package/dist/partyKept.js +1 -0
- package/dist/partySeats.d.ts +2 -0
- package/dist/partySeats.js +2 -0
- package/dist/placement.d.ts +1 -0
- package/dist/play.d.ts +7 -0
- package/dist/play.js +1 -1
- package/dist/random.d.ts +1 -0
- package/dist/random.js +1 -0
- package/dist/react.d.ts +4 -1
- package/dist/react.js +4 -4
- package/dist/showcase.d.ts +4 -0
- package/dist/showcase.js +5 -1
- package/dist/strings.d.ts +62 -0
- package/dist/strings.js +96 -0
- package/dist/tableView.d.ts +1 -0
- package/dist/tableView.js +1 -0
- package/dist/tileFace.d.ts +4 -0
- package/dist/tileFace.js +2 -0
- package/dist/tileWords.d.ts +2 -0
- package/dist/tileWords.js +2 -0
- package/dist/ui/board.d.ts +6 -0
- package/dist/ui/mount.d.ts +32 -2
- package/dist/ui/mount.js +157 -26
- package/dist/ui/style.d.ts +5 -4
- package/dist/ui/style.js +23 -15
- package/dist/ui.d.ts +1 -0
- package/dist/ui.js +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +102 -32
package/README.md
CHANGED
|
@@ -1,60 +1,336 @@
|
|
|
1
|
-
|
|
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></p>
|
|
2
16
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-

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