@johnmorrisdotca/kumimoji 1.3.1 → 1.5.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 CHANGED
@@ -6,6 +6,29 @@ All notable changes to this project are written down here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.5.0] - 2026-11-04
10
+
11
+ A minor version: nothing that 1.4.0 gave is taken away except what children should not have been handed. The English list, the rules and the saved-game format are unchanged.
12
+
13
+ ### Changed
14
+
15
+ - **The Japanese word list leaves out what is not for children** (MODULES-390). Children play this game, and the list was every kana reading of every JMdict entry. It now has the bar of Kotoba's kana lists (`scripts/children-bar.mjs` in Kotoba, which writes the list with `scripts/word-lists-ja.mjs`): a reading is out when every JMdict entry that spells it has a vulgar, X-rated, sensitive or derogatory mark on its first sense, or a first sense whose gloss names sex or the sex trade. A reading that an ordinary entry is also spelt like stays, and so does a line that another word plays: the tiles play ば and ぱ as は, so れいぷ is the line れいふ, which is also 霊符 (a charm). Of the 163,585 lines JMdict's release of 2026-11-04 gives, 1,347 are barred (110 of 3 kana, 347 of 4, 264 of 5, 626 longer), among them せっくす, ばいしゅん, ぽるの, えろい, おなに, ちくび, ふぇらちお, せいどれい, いんらん, ちんぽ and らぶほてる; the 5 words earlier lists held that JMdict no longer lists are kept, so a game kept half way still checks. The list holds **162,243** readings (163,461 in 1.4.0). A test (`src/childrenBar.test.ts`) holds the known bad readings out. The guide "Load and judge with word lists" says what is left out, in English and in Japanese.
16
+ - **A seeded Japanese game is dealt differently**, as the list has fewer words to choose among: the same seed gives another bag. English games are unchanged, and so is the saved-game format; a kept Japanese game checks as before unless it spells one of the barred lines.
17
+ - The Japanese list is made again from JMdict's release of 2026-11-04 (it was 2026-09-28): 129 readings join, 5 are kept that JMdict dropped.
18
+ - The package is a little smaller: the Japanese list is 715 kB (394 kB gzipped), and the sizes in `docs/bundle-size.json` are measured again.
19
+
20
+ ### Added
21
+
22
+ - The README opens with the package's thumbnail, and the demo carries it as its link preview (og:image) and publishes the card, a dark card and an icon on Pages at `thumbnail.png`, `thumbnail-dark.png` and `icon.png`.
23
+
24
+ ## [1.4.0] - 2026-11-02
25
+
26
+ ### Added
27
+
28
+ - A table can be laid in the family's five cloths: `<kumimoji-table cloth="blue">`, `mountKumimoji(el, { cloth })` and `set({ cloth })`, the `cloth` embed parameter and Builder option, and `KUMIMOJI_CLOTHS`, `isCloth` and `clothVars` from `/ui`. The demo's header patches lay it through the page's theme.
29
+
30
+ - **`checkKumimoji` gives its reason as a code and values as well as in English.** A refusal is now `{ ok: false, reason, code, values }`: `reason` is the sentence it has always been, word for word, and `code` (`"no-hand"`, `"givens-not-bag"`, `"wild-count-wrong"`, `"bag-exceeds-set"`, `"answer-not-grid"`, `"grid-not-the-bag"`, `"tiles-apart"`, `"not-a-word"` and `"grid-not-finished"`) and `values` (`{ size }` for `no-hand`, `{ word }` for `not-a-word`) let a site say it in its own language. `KUMIMOJI_CHECK_REASONS` lists every code with its English clause, `kumimojiRefusal(code, values)` makes a refusal, and the types are `KumimojiCheckCode` and `KumimojiCheckValues`. A test holds every refusal's `reason` to its code's clause filled with its values.
31
+
9
32
  ## [1.3.1] - 2026-10-31
10
33
 
11
34
  ### Changed
@@ -188,7 +211,9 @@ The first release.
188
211
  `@johnmorrisdotca/kumimoji/react`.
189
212
  - A static demo for GitHub Pages.
190
213
 
191
- [Unreleased]: https://github.com/johnmorrisdotca/kumimoji/compare/v1.3.0...HEAD
214
+ [Unreleased]: https://github.com/johnmorrisdotca/kumimoji/compare/v1.5.0...HEAD
215
+ [1.5.0]: https://github.com/johnmorrisdotca/kumimoji/compare/v1.4.0...v1.5.0
216
+ [1.4.0]: https://github.com/johnmorrisdotca/kumimoji/compare/v1.3.1...v1.4.0
192
217
  [1.3.0]: https://github.com/johnmorrisdotca/kumimoji/compare/v1.2.2...v1.3.0
193
218
  [1.2.1]: https://github.com/johnmorrisdotca/kumimoji/compare/v1.2.0...v1.2.1
194
219
  [1.1.1]: https://github.com/johnmorrisdotca/kumimoji/compare/v1.1.0...v1.1.1
package/NOTICE.md CHANGED
@@ -31,7 +31,7 @@ SCOWL's own distribution.
31
31
 
32
32
  `src/words.ja.data.ts` is derived from JMdict, the Japanese-Multilingual
33
33
  Dictionary of the Electronic Dictionary Research and Development Group
34
- (https://www.edrdg.org/), release 2026-09-28: 163,461 hiragana readings of 2 to
34
+ (https://www.edrdg.org/), release 2026-11-04: 162,243 hiragana readings of 2 to
35
35
  15 kana, each spelt in the 45 base kana the tiles show. JMdict is the property of
36
36
  the EDRDG and is used under the Creative Commons Attribution-ShareAlike 4.0
37
37
  licence, with the group's conditions: https://www.edrdg.org/edrdg/licence.html.
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ <p align="center"><a href="https://johnmorrisdotca.github.io/kumimoji/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kumimoji/main/docs/images/thumbnail-dark.png"><img src="https://raw.githubusercontent.com/johnmorrisdotca/kumimoji/main/docs/images/thumbnail.png" alt="Kumimoji: letter tiles laid in a crossword by the package's own computer player." width="1200"></picture></a></p>
2
+
1
3
  <h1 align="center">Kumimoji <sub>組み文字</sub> — Crossword tile game in English and Japanese</h1>
2
4
 
3
5
  <p align="center"><strong>The crossword tile race: a word game where you build one crossword from a bag of letter tiles.</strong><br>
@@ -10,7 +12,7 @@ In English and in Japanese kana. The rules as pure, seeded TypeScript, two word
10
12
  <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
13
  <img alt="No dependencies" src="https://img.shields.io/badge/dependencies-0-2f5d4a">
12
14
  <img alt="TypeScript" src="https://img.shields.io/badge/types-TypeScript-3178c6">
13
- <a href="https://johnmorrisdotca.github.io/kumimoji/docs/api/main/index.html"><img alt="679.1 kB minified and gzipped" src="https://img.shields.io/badge/min%2Bgzip-679.1%20kB-2f5d4a"></a>
15
+ <a href="https://johnmorrisdotca.github.io/kumimoji/docs/api/main/index.html"><img alt="677.1 kB minified and gzipped" src="https://img.shields.io/badge/min%2Bgzip-677.1%20kB-2f5d4a"></a>
14
16
  <a href="https://www.npmjs.com/package/@johnmorrisdotca/kumimoji#provenance"><img alt="Published with npm provenance" src="https://img.shields.io/badge/npm-provenance-2f5d4a"></a>
15
17
  </p>
16
18
 
@@ -103,7 +105,7 @@ Or with nothing to install, [play a game in the demo](https://johnmorrisdotca.gi
103
105
  - **Races and daily puzzles.** A seed deals one bag, the same for everybody,
104
106
  and `checkKumimoji` checks a finished grid against it without searching.
105
107
  - **Japanese.** A word game that plays in kana, with が on the か tile and ゃ
106
- on the や tile, and a list of 163,461 readings.
108
+ on the や tile, and a list of 162,243 readings.
107
109
  - **Learners and teachers** of English or Japanese spelling.
108
110
 
109
111
  ## Features
@@ -112,7 +114,7 @@ Or with nothing to install, [play a game in the demo](https://johnmorrisdotca.gi
112
114
  its tiles, so every bag can be finished.
113
115
  - **English and Japanese.** 144 tiles in each: the English letters, or 45 base
114
116
  kana where が plays as か, ゃ as や and を as お. 110,316 English words and
115
- 163,461 Japanese readings, of 2 to 15 tiles.
117
+ 162,243 Japanese readings, of 2 to 15 tiles.
116
118
  - **Wild tiles** that take any letter or kana, and are read as that in every
117
119
  word they are part of.
118
120
  - **Trades.** A tile you cannot use goes back into the bag for three.
@@ -155,7 +157,7 @@ Each picture is the table the package draws, taken from [the demo](https://johnm
155
157
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kumimoji/main/docs/images/kana-desk-dark.webp">
156
158
  <img src="https://raw.githubusercontent.com/johnmorrisdotca/kumimoji/main/docs/images/kana-desk-light.webp" alt="A Japanese board of kana tiles on the green table, a crossword of hiragana laid across and down on the grid of empty squares." width="400">
157
159
  </picture>
158
- <br><em><strong>Japanese kana.</strong> 45 base kana tiles, 163,461 readings.</em>
160
+ <br><em><strong>Japanese kana.</strong> 45 base kana tiles, 162,243 readings.</em>
159
161
  </td>
160
162
  </tr>
161
163
  <tr>
@@ -242,7 +244,7 @@ const wrong = decodeGrid("cat/2x")!;
242
244
  judgeWithWords(wrong, words).notWords; // → ["tx"]
243
245
 
244
246
  checkKumimoji(3, "nwosy", "snowy", { level: "hard" }); // → { ok: true }
245
- checkKumimoji(3, "nwosy", "snows", { level: "hard" }); // → { ok: false, reason: "the grid does not use exactly the tiles of the bag" }
247
+ checkKumimoji(3, "nwosy", "snows", { level: "hard" }); // → { ok: false, reason: "the grid does not use exactly the tiles of the bag", code: "grid-not-the-bag" }
246
248
  encodeGrid(tiles); // → "cat/2o/2w"
247
249
  ```
248
250
 
@@ -390,8 +392,8 @@ fetches when a game is first dealt.
390
392
  names only the files that define the tag and register the word lists.
391
393
  - **Sizes.** The rules, the computer player and the table of players are
392
394
  about 44 kB minified (16 kB gzipped); the table for a page is 34 kB (13 kB).
393
- The English list is 606 kB (277 kB gzipped) and the Japanese 718 kB
394
- (396 kB), each a file of its own that is fetched only when a game in that
395
+ The English list is 606 kB (277 kB gzipped) and the Japanese 715 kB
396
+ (394 kB), each a file of its own that is fetched only when a game in that
395
397
  language is first dealt.
396
398
  - **Where it runs.** Current Chrome, Edge, Firefox and Safari, on a desk or a
397
399
  phone. The rules have no DOM in them and run in Node 22 and later, Deno,
@@ -541,7 +543,7 @@ import { checkKumimoji, generateKumimoji, loadTileWords } from "@johnmorrisdotca
541
543
  await loadTileWords("english");
542
544
  const dealt = generateKumimoji(3, "hard", 44);
543
545
  checkKumimoji(3, dealt.givens, "snowy", { level: "hard" }); // → { ok: true }
544
- checkKumimoji(3, dealt.givens, "snows", { level: "hard" }); // → { ok: false, reason: "the grid does not use exactly the tiles of the bag" }
546
+ checkKumimoji(3, dealt.givens, "snows", { level: "hard" }); // → { ok: false, reason: "the grid does not use exactly the tiles of the bag", code: "grid-not-the-bag" }
545
547
  ```
546
548
 
547
549
  ### Mount the table with options
@@ -566,6 +568,8 @@ table.destroy();
566
568
 
567
569
  ### A look of your own
568
570
 
571
+ `<kumimoji-table cloth="blue">`, or `cloth: "blue"` when you mount, lays the table in one of the family's five cloths, `green`, `blue`, `red`, `black` or `wood`, the same set as the boards on itsutsu.com; `set({ cloth })` changes it in place, keeping the game, and `theme` is laid over it. `KUMIMOJI_CLOTHS` and `clothVars` hold the colours.
572
+
569
573
  Every colour is a CSS variable on `.km-root`, so a stylesheet or the `theme` option can restyle the table:
570
574
 
571
575
  ```ts no-run
@@ -670,11 +674,11 @@ import { BASE_KANA, deal, generateKumimoji, loadTileWords, tileKana } from "@joh
670
674
  const words = await loadTileWords("japanese");
671
675
  [...BASE_KANA].length; // → 45
672
676
  [tileKana("が"), tileKana("ゃ"), tileKana("を"), tileKana("っ")]; // → ["か", "や", "お", "つ"]
673
- words.allowed.size; // → 163461
677
+ words.allowed.size; // → 162243
674
678
  words.allowed.has("ことは"); // → true
675
679
 
676
680
  const dealt = generateKumimoji(7, "medium", 2026, { language: "japanese" });
677
- deal(dealt.givens, 7).hand.map(words.glyphOf).join(""); // → "よきめたんえら"
681
+ deal(dealt.givens, 7).hand.map(words.glyphOf).join(""); // → "つかてしんきし"
678
682
  ```
679
683
 
680
684
  The tiles are the 45 base hiragana. A word is spelt with the base of each of
@@ -768,7 +772,7 @@ The JSON, as `kumimojiToJSON` writes it:
768
772
  {
769
773
  "format": 1,
770
774
  "game": "kumimoji",
771
- "generator": "kumimoji 1.3.1",
775
+ "generator": "kumimoji 1.5.0",
772
776
  "size": 3,
773
777
  "level": "hard",
774
778
  "seed": 44,
@@ -813,7 +817,7 @@ with its notice too.
813
817
  | List | What it is | Where it comes from | Terms |
814
818
  | --- | --- | --- | --- |
815
819
  | 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. |
816
- | 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.** |
820
+ | Japanese, `dist/words.ja.data.js` | 162,243 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-11-04, 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.** |
817
821
 
818
822
  What that means for a project that uses this package:
819
823
 
@@ -886,23 +890,14 @@ as you type. The entries:
886
890
  | `deal(bag, handSize)` and `placeFromHand`, `moveOnTable`, `liftToHand`, `draw`, `trade` | The opening hand, and each move, every one returning a new game |
887
891
  | `judgeWithWords(tiles, words)` | What is wrong with a grid, or that it is sound |
888
892
  | `isFinished(play, verdict)` | Whether the bag is empty, the hand used and every run a word in one piece |
889
- | `checkKumimoji(size, givens, answer, options?)` | The one check a server runs on a finished grid |
893
+ | `checkKumimoji(size, givens, answer, options?)` | The one check a server runs on a finished grid; a refusal has `reason`, `code`, `values` (`KUMIMOJI_CHECK_REASONS`, `kumimojiRefusal`) |
890
894
  | `loadTileWords(language)` | A word list, fetched when first needed |
891
895
  | `mountKumimoji(element, options?)` and `<kumimoji-table>` | The table, in one call or one tag |
892
896
  | `kumimojiToJSON`, `kumimojiFromJSON` | A game kept, and read back without trusting it |
893
897
 
894
898
  ### The long tables
895
899
 
896
- The tables of every export for dealing and judging, tiles and grids, playing and keeping a game, a table of players and looking at the table are in [docs/API.md](docs/API.md), and every export of every entry point, with its signature and doc comment, is in the [API reference](https://johnmorrisdotca.github.io/kumimoji/api.html). The types the package exports are listed there too.
897
-
898
- The types are `TilePlay`, `Tiles`, `GridVerdict`, `GridRules`, `Run`,
899
- `RunLine`, `Bounds`, `Square`, `CrossingFit`, `KumimojiDeal`,
900
- `KumimojiOptions`, `KumimojiCheck`, `KumimojiLevel`, `KumimojiLength`,
901
- `KumimojiLanguage`, `TileWords`, `TileFaceOf`, `PartyGame`, `PartySettings`,
902
- `PartyPlayer`, `PartySeat`, `PartyEnding`, `JoinRefusal`, `LeaveRefusal`,
903
- `ComputerStep`, `ComputerSaid`, `KumimojiSaved`, `KumimojiExported`, `KumimojiStrings`,
904
- `KumimojiLocale`, `Random`, `Turn`, `Area`, `View`, `MixTile`, `MixShown` and
905
- `LengthRow`.
900
+ The types, and the tables of every export for dealing and judging, tiles and grids, playing and keeping a game, a table of players and looking at the table are in [docs/API.md](docs/API.md), and every export of every entry point, with its signature and doc comment, is in the [API reference](https://johnmorrisdotca.github.io/kumimoji/api.html). The types the package exports are listed there too.
906
901
 
907
902
  ### The table
908
903
 
@@ -1069,8 +1064,6 @@ kept as text made to be sent, and Itsutsu sends it; the sending is yours); a
1069
1064
  command line; CSV; and any name, art or wording of a published game. Kumimoji
1070
1065
  runs from a static page and costs nothing to host.
1071
1066
 
1072
- Ideas and pull requests are welcome.
1073
-
1074
1067
  ## Architecture
1075
1068
 
1076
1069
  The rules are plain functions over plain data with no DOM: a game is a value,
@@ -1180,16 +1173,16 @@ Kumimoji is one of thirty-two packages, each made for the same site, each at
1180
1173
 
1181
1174
  ```sh
1182
1175
  pnpm install --frozen-lockfile
1183
- pnpm check # lint, types and tests, including the checks on the README's tables and examples
1176
+ pnpm check # lint, types and tests, the README's own checks among them
1184
1177
  pnpm test:table # the demo in real browsers, by taps
1185
1178
  pnpm test:package # pack it as npm does, install it and import every entry
1186
1179
  pnpm test:frameworks # the six framework examples, built from the packed tarball and played
1187
1180
  pnpm test:readme # every TypeScript and JavaScript example in this README, type-checked and run
1188
1181
  pnpm site # build the demo into ./site
1189
- pnpm screenshots:readme # retake the README's pictures into docs/images (builds the demo first)
1182
+ pnpm screenshots:readme # retake the README's pictures (builds the demo first)
1190
1183
  ```
1191
1184
 
1192
- The pictures are taken on the maintainer's Mac and are retaken only when the look changes; they are in `docs/images` and are not in the package that npm installs.
1185
+ The README's pictures are in `docs/images`, which the npm package leaves out.
1193
1186
 
1194
1187
  ## Contributing
1195
1188
 
@@ -1201,7 +1194,7 @@ Please follow the [code of conduct](./CODE_OF_CONDUCT.md). A way to make a check
1201
1194
 
1202
1195
  See [CHANGELOG.md](./CHANGELOG.md).
1203
1196
 
1204
- The latest release, 1.3.1, changes nothing a player sees.
1197
+ The latest release, 1.5.0, leaves words that are not for children out of the Japanese list.
1205
1198
 
1206
1199
  ## Licence
1207
1200
 
@@ -0,0 +1,71 @@
1
+ import type { KumimojiCheck } from "./kumimoji.types.ts";
2
+ /**
3
+ * Why `checkKumimoji` will not take a grid, as a stable code and the English clause it says: `{name}` is a gap the
4
+ * check's `values` fill. The clause is what `reason` has always said, word for word; a site that speaks another
5
+ * language says the code in its own words and puts the values in the gaps.
6
+ *
7
+ * | Code | Values |
8
+ * | --- | --- |
9
+ * | `no-hand` | `size`: the hand that has no game |
10
+ * | `not-a-word` | `word`: the first word that is not in the word list |
11
+ * | `givens-not-bag`, `wild-count-wrong`, `bag-exceeds-set`, `answer-not-grid`, `grid-not-the-bag`, `tiles-apart`, `grid-not-finished` | none |
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { KUMIMOJI_CHECK_REASONS } from "@johnmorrisdotca/kumimoji";
16
+ *
17
+ * console.log(KUMIMOJI_CHECK_REASONS["not-a-word"]);
18
+ * ```
19
+ */
20
+ export declare const KUMIMOJI_CHECK_REASONS: {
21
+ readonly "no-hand": "no Kumimoji with a hand of {size}";
22
+ readonly "givens-not-bag": "the givens are not a bag of tiles";
23
+ readonly "wild-count-wrong": "the bag has the wrong number of wild tiles";
24
+ readonly "bag-exceeds-set": "the bag exceeds the tile set";
25
+ readonly "answer-not-grid": "the answer is not a grid";
26
+ readonly "grid-not-the-bag": "the grid does not use exactly the tiles of the bag";
27
+ readonly "tiles-apart": "the tiles are not all joined";
28
+ readonly "not-a-word": "{word} is not in the word list";
29
+ readonly "grid-not-finished": "the grid is not finished";
30
+ };
31
+ /**
32
+ * A reason `checkKumimoji` can give, by its stable code.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * import type { KumimojiCheckCode } from "@johnmorrisdotca/kumimoji";
37
+ *
38
+ * const code: KumimojiCheckCode = "tiles-apart";
39
+ * console.log(code);
40
+ * ```
41
+ */
42
+ export type KumimojiCheckCode = keyof typeof KUMIMOJI_CHECK_REASONS;
43
+ /**
44
+ * The numbers and words that fill a reason's gaps, by the gap's name (see `KUMIMOJI_CHECK_REASONS` for each code's).
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import type { KumimojiCheckValues } from "@johnmorrisdotca/kumimoji";
49
+ *
50
+ * const values: KumimojiCheckValues = { word: "aow" };
51
+ * console.log(values);
52
+ * ```
53
+ */
54
+ export type KumimojiCheckValues = Readonly<Record<string, string | number>>;
55
+ /**
56
+ * A refusal: `{ ok: false }`, the English `reason` (the clause of the code with the values in its gaps), the code
57
+ * and the values.
58
+ *
59
+ * @param code - Why the grid is not taken.
60
+ * @param values - What fills the clause's gaps, where it has any.
61
+ * @returns The refusal `checkKumimoji` gives.
62
+ * @example
63
+ * ```ts
64
+ * import { kumimojiRefusal } from "@johnmorrisdotca/kumimoji";
65
+ *
66
+ * console.log(kumimojiRefusal("not-a-word", { word: "aow" }));
67
+ * ```
68
+ */
69
+ export declare function kumimojiRefusal(code: KumimojiCheckCode, values?: KumimojiCheckValues): Extract<KumimojiCheck, {
70
+ ok: false;
71
+ }>;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Why `checkKumimoji` will not take a grid, as a stable code and the English clause it says: `{name}` is a gap the
3
+ * check's `values` fill. The clause is what `reason` has always said, word for word; a site that speaks another
4
+ * language says the code in its own words and puts the values in the gaps.
5
+ *
6
+ * | Code | Values |
7
+ * | --- | --- |
8
+ * | `no-hand` | `size`: the hand that has no game |
9
+ * | `not-a-word` | `word`: the first word that is not in the word list |
10
+ * | `givens-not-bag`, `wild-count-wrong`, `bag-exceeds-set`, `answer-not-grid`, `grid-not-the-bag`, `tiles-apart`, `grid-not-finished` | none |
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import { KUMIMOJI_CHECK_REASONS } from "@johnmorrisdotca/kumimoji";
15
+ *
16
+ * console.log(KUMIMOJI_CHECK_REASONS["not-a-word"]);
17
+ * ```
18
+ */
19
+ export const KUMIMOJI_CHECK_REASONS = {
20
+ "no-hand": "no Kumimoji with a hand of {size}",
21
+ "givens-not-bag": "the givens are not a bag of tiles",
22
+ "wild-count-wrong": "the bag has the wrong number of wild tiles",
23
+ "bag-exceeds-set": "the bag exceeds the tile set",
24
+ "answer-not-grid": "the answer is not a grid",
25
+ "grid-not-the-bag": "the grid does not use exactly the tiles of the bag",
26
+ "tiles-apart": "the tiles are not all joined",
27
+ "not-a-word": "{word} is not in the word list",
28
+ "grid-not-finished": "the grid is not finished",
29
+ };
30
+ /**
31
+ * A refusal: `{ ok: false }`, the English `reason` (the clause of the code with the values in its gaps), the code
32
+ * and the values.
33
+ *
34
+ * @param code - Why the grid is not taken.
35
+ * @param values - What fills the clause's gaps, where it has any.
36
+ * @returns The refusal `checkKumimoji` gives.
37
+ * @example
38
+ * ```ts
39
+ * import { kumimojiRefusal } from "@johnmorrisdotca/kumimoji";
40
+ *
41
+ * console.log(kumimojiRefusal("not-a-word", { word: "aow" }));
42
+ * ```
43
+ */
44
+ export function kumimojiRefusal(code, values) {
45
+ const reason = KUMIMOJI_CHECK_REASONS[code].replace(/\{(\w+)\}/g, (_gap, name) => String(values?.[name]));
46
+ return values === undefined ? { ok: false, reason, code } : { ok: false, reason, code, values };
47
+ }
package/dist/check.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { kumimojiRefusal } from "./check-reasons.js";
1
2
  import { decodeGrid, lettersOf, sameLetters } from "./grid.js";
2
3
  import { judgeWithWords } from "./judge.js";
3
4
  import { KUMIMOJI_SCORE, kumimojiTileCount, kumimojiWildCount, TILE_MIX_TOTAL } from "./tiles.constants.js";
@@ -43,37 +44,37 @@ export function checkKumimoji(size, givens, answer, options = {}) {
43
44
  inBag = kumimojiTileCount(size, gameLength, language === "english" ? TILE_MIX_TOTAL : setSize, doubleSet);
44
45
  }
45
46
  catch {
46
- return { ok: false, reason: `no Kumimoji with a hand of ${size}` };
47
+ return kumimojiRefusal("no-hand", { size });
47
48
  }
48
49
  const bag = [...givens];
49
50
  if (typeof givens !== "string" || bag.length !== inBag)
50
- return { ok: false, reason: "the givens are not a bag of tiles" };
51
+ return kumimojiRefusal("givens-not-bag");
51
52
  const expectedWilds = kumimojiWildCount(size, level, inBag);
52
53
  if (bag.filter(words.isWild).length !== expectedWilds)
53
- return { ok: false, reason: "the bag has the wrong number of wild tiles" };
54
+ return kumimojiRefusal("wild-count-wrong");
54
55
  for (const [tile, count] of lettersOf(bag.filter((each) => !words.isWild(each)))) {
55
56
  const face = words.inventoryKey(tile);
56
57
  const code = face === null ? null : words.codeOf(face);
57
58
  if (code === null || count > (words.mix.get(code) ?? 0) * multiplier)
58
- return { ok: false, reason: "the bag exceeds the tile set" };
59
+ return kumimojiRefusal("bag-exceeds-set");
59
60
  }
60
61
  const tiles = decodeGrid(answer);
61
62
  if (tiles === null)
62
- return { ok: false, reason: "the answer is not a grid" };
63
+ return kumimojiRefusal("answer-not-grid");
63
64
  const bagIdentities = bag.map(words.familyKey);
64
65
  const tileIdentities = [...tiles.values()].map(words.familyKey);
65
66
  if (bagIdentities.some((tile) => tile === null) ||
66
67
  tileIdentities.some((tile) => tile === null) ||
67
68
  !sameLetters(lettersOf(tileIdentities), lettersOf(bagIdentities))) {
68
- return { ok: false, reason: "the grid does not use exactly the tiles of the bag" };
69
+ return kumimojiRefusal("grid-not-the-bag");
69
70
  }
70
71
  const verdict = judgeWithWords(tiles, words, { diagonals: options.diagonals === true });
71
72
  if (verdict.apart.size > 0)
72
- return { ok: false, reason: "the tiles are not all joined" };
73
+ return kumimojiRefusal("tiles-apart");
73
74
  if (verdict.notWords.length > 0)
74
- return { ok: false, reason: `${verdict.notWords[0]} is not in the word list` };
75
+ return kumimojiRefusal("not-a-word", { word: verdict.notWords[0] });
75
76
  if (!verdict.sound)
76
- return { ok: false, reason: "the grid is not finished" };
77
+ return kumimojiRefusal("grid-not-finished");
77
78
  return { ok: true };
78
79
  }
79
80
  /** A finished game's leaderboard points (`KUMIMOJI_SCORE`): ten a tile, and up to as much again for speed.
package/dist/element.d.ts CHANGED
@@ -97,6 +97,9 @@ export declare class KumimojiTable extends ElementBase {
97
97
  /** The `controls` attribute, reflected. */
98
98
  get controls(): string | null;
99
99
  set controls(value: string | number | boolean | null | undefined);
100
+ /** The `cloth` attribute, reflected: green, blue, red, black or wood. */
101
+ get cloth(): string | null;
102
+ set cloth(value: string | number | boolean | null | undefined);
100
103
  /** The `theme` attribute, reflected. */
101
104
  get theme(): string | null;
102
105
  set theme(value: string | number | boolean | null | undefined);
package/dist/element.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { kumimojiDailySeed } from "./daily.js";
2
2
  import { embedDay } from "./embed-day.js";
3
3
  import { KUMIMOJI_HANDS } from "./tiles.constants.js";
4
+ import { isCloth } from "./ui/cloth.js";
4
5
  import { mountKumimoji } from "./ui/mount.js";
5
6
  import { KUMIMOJI_STYLE } from "./ui/style.js";
6
7
  const ElementBase = typeof HTMLElement === "undefined" ? class {
@@ -53,6 +54,7 @@ export const KUMIMOJI_EMBED_OPTION_KEYS = Object.keys({
53
54
  locale: true,
54
55
  strings: true,
55
56
  theme: true,
57
+ cloth: true,
56
58
  keep: true,
57
59
  controls: true,
58
60
  saved: true,
@@ -82,6 +84,7 @@ export class KumimojiTable extends ElementBase {
82
84
  "size",
83
85
  "width",
84
86
  "label",
87
+ "cloth",
85
88
  ];
86
89
  #table = null;
87
90
  #key = "";
@@ -205,6 +208,13 @@ export class KumimojiTable extends ElementBase {
205
208
  set controls(value) {
206
209
  this.#write("controls", value);
207
210
  }
211
+ /** The `cloth` attribute, reflected: green, blue, red, black or wood. */
212
+ get cloth() {
213
+ return this.getAttribute("cloth");
214
+ }
215
+ set cloth(value) {
216
+ this.#write("cloth", value);
217
+ }
208
218
  /** The `theme` attribute, reflected. */
209
219
  get theme() {
210
220
  return this.getAttribute("theme");
@@ -319,11 +329,19 @@ export class KumimojiTable extends ElementBase {
319
329
  ? lang
320
330
  : (this.#options.locale ??
321
331
  (this.ownerDocument.documentElement.lang.toLowerCase().startsWith("ja") ? "ja" : "en"));
332
+ const cloth = isCloth(this.getAttribute("cloth")) ? this.getAttribute("cloth") : this.#options.cloth;
322
333
  const keep = isOn(this.getAttribute("keep"), this.#options.keep ?? true);
323
334
  const controls = isOn(this.getAttribute("controls"), this.#options.controls ?? true);
324
335
  const key = JSON.stringify([language, hand, level, gameLength, diagonals, seed, day, this.#options.saved]);
325
336
  if (key === this.#key && this.#table !== null) {
326
- this.#table.set({ locale, strings: this.#options.strings, theme: this.#options.theme ?? {}, keep, controls });
337
+ this.#table.set({
338
+ locale,
339
+ strings: this.#options.strings,
340
+ cloth,
341
+ theme: this.#options.theme ?? {},
342
+ keep,
343
+ controls,
344
+ });
327
345
  return;
328
346
  }
329
347
  if (hand !== undefined && !HANDS.includes(hand))
@@ -342,6 +360,7 @@ export class KumimojiTable extends ElementBase {
342
360
  diagonals,
343
361
  seed,
344
362
  locale,
363
+ cloth,
345
364
  keep,
346
365
  controls,
347
366
  onChange: (saved) => {
package/dist/index.d.ts CHANGED
@@ -17,6 +17,7 @@
17
17
  * disk.
18
18
  */
19
19
  export * from "./check.ts";
20
+ export * from "./check-reasons.ts";
20
21
  export * from "./computer.types.ts";
21
22
  export * from "./computerPlay.ts";
22
23
  export * from "./computerTurn.ts";
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@
17
17
  * disk.
18
18
  */
19
19
  export * from "./check.js";
20
+ export * from "./check-reasons.js";
20
21
  export * from "./computer.types.js";
21
22
  export * from "./computerPlay.js";
22
23
  export * from "./computerTurn.js";
@@ -1,3 +1,4 @@
1
+ import type { KumimojiCheckCode, KumimojiCheckValues } from "./check-reasons.ts";
1
2
  /** How much of the set a game's bag holds: a short game, half the set, or all of it.
2
3
  *
3
4
  * @example
@@ -68,13 +69,13 @@ export type KumimojiOptions = {
68
69
  */
69
70
  diagonals?: boolean;
70
71
  };
71
- /** The answer to "does this grid finish the game?": yes, or no and why.
72
+ /** The answer to "does this grid finish the game?": yes, or no and why, in English (`reason`) and as a stable `code` with `values` a site can say in its own language (`KUMIMOJI_CHECK_REASONS`).
72
73
  *
73
74
  * @example
74
75
  * ```ts
75
76
  * import type { KumimojiCheck } from "@johnmorrisdotca/kumimoji";
76
77
  *
77
- * const value: KumimojiCheck = {ok:false,reason:"Not a finished grid"};
78
+ * const value: KumimojiCheck = {ok:false,reason:"the grid is not finished",code:"grid-not-finished"};
78
79
  * console.log(value);
79
80
  * ```
80
81
  */
@@ -83,6 +84,8 @@ export type KumimojiCheck = {
83
84
  } | {
84
85
  ok: false;
85
86
  reason: string;
87
+ code: KumimojiCheckCode;
88
+ values?: KumimojiCheckValues;
86
89
  };
87
90
  /**
88
91
  * A GAME DEALT (`generateKumimoji`): the bag, in the order its tiles come out,
@@ -0,0 +1,72 @@
1
+ /**
2
+ * THE CLOTHS A TABLE MAY BE LAID IN: the same five the whole family offers, and itsutsu.com's boards:
3
+ * green, blue, red, black and wood. Each is the felt's colour, its deep edge and the ink written on it.
4
+ * Given to a table as its `--km-felt` and `--km-accent` variables; `theme` is laid over it.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { KUMIMOJI_CLOTHS } from "@johnmorrisdotca/kumimoji/ui";
9
+ * console.log(Object.keys(KUMIMOJI_CLOTHS));
10
+ * ```
11
+ */
12
+ export declare const KUMIMOJI_CLOTHS: {
13
+ readonly green: {
14
+ readonly felt: "#2f5d4a";
15
+ readonly deep: "#1f4135";
16
+ readonly ink: "#f3efe4";
17
+ };
18
+ readonly blue: {
19
+ readonly felt: "#2865a6";
20
+ readonly deep: "#1a4677";
21
+ readonly ink: "#f3efe4";
22
+ };
23
+ readonly red: {
24
+ readonly felt: "#a3342e";
25
+ readonly deep: "#7a231f";
26
+ readonly ink: "#f3efe4";
27
+ };
28
+ readonly black: {
29
+ readonly felt: "#2f3236";
30
+ readonly deep: "#1b1d20";
31
+ readonly ink: "#ece8dc";
32
+ };
33
+ readonly wood: {
34
+ readonly felt: "#e2ba7a";
35
+ readonly deep: "#c4954f";
36
+ readonly ink: "#2b1d0e";
37
+ };
38
+ };
39
+ /** A cloth's name.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * import type { Cloth } from "@johnmorrisdotca/kumimoji/ui";
44
+ * const value: Cloth = "green";
45
+ * console.log(value);
46
+ * ```
47
+ */
48
+ export type Cloth = keyof typeof KUMIMOJI_CLOTHS;
49
+ /** Whether a text names a cloth.
50
+ *
51
+ * @param text - Text to parse, validate or format.
52
+ * @returns Whether a text names a cloth.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * import { isCloth } from "@johnmorrisdotca/kumimoji/ui";
57
+ * console.log(isCloth("green"), isCloth("unknown"));
58
+ * ```
59
+ */
60
+ export declare function isCloth(text: unknown): text is Cloth;
61
+ /** A cloth as CSS variables; nothing for a name that is not a cloth, so the defaults stay.
62
+ *
63
+ * @param cloth - A built-in cloth name; an unknown name leaves the defaults.
64
+ * @returns The CSS variables to set on the element, or an empty object.
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * import { clothVars } from "@johnmorrisdotca/kumimoji/ui";
69
+ * console.log(clothVars("green"));
70
+ * ```
71
+ */
72
+ export declare function clothVars(cloth: string | undefined | null): Record<`--km-${string}`, string>;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * THE CLOTHS A TABLE MAY BE LAID IN: the same five the whole family offers, and itsutsu.com's boards:
3
+ * green, blue, red, black and wood. Each is the felt's colour, its deep edge and the ink written on it.
4
+ * Given to a table as its `--km-felt` and `--km-accent` variables; `theme` is laid over it.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { KUMIMOJI_CLOTHS } from "@johnmorrisdotca/kumimoji/ui";
9
+ * console.log(Object.keys(KUMIMOJI_CLOTHS));
10
+ * ```
11
+ */
12
+ export const KUMIMOJI_CLOTHS = {
13
+ green: { felt: "#2f5d4a", deep: "#1f4135", ink: "#f3efe4" },
14
+ blue: { felt: "#2865a6", deep: "#1a4677", ink: "#f3efe4" },
15
+ red: { felt: "#a3342e", deep: "#7a231f", ink: "#f3efe4" },
16
+ black: { felt: "#2f3236", deep: "#1b1d20", ink: "#ece8dc" },
17
+ wood: { felt: "#e2ba7a", deep: "#c4954f", ink: "#2b1d0e" },
18
+ };
19
+ /** Whether a text names a cloth.
20
+ *
21
+ * @param text - Text to parse, validate or format.
22
+ * @returns Whether a text names a cloth.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * import { isCloth } from "@johnmorrisdotca/kumimoji/ui";
27
+ * console.log(isCloth("green"), isCloth("unknown"));
28
+ * ```
29
+ */
30
+ export function isCloth(text) {
31
+ return typeof text === "string" && Object.hasOwn(KUMIMOJI_CLOTHS, text);
32
+ }
33
+ /** A cloth as CSS variables; nothing for a name that is not a cloth, so the defaults stay.
34
+ *
35
+ * @param cloth - A built-in cloth name; an unknown name leaves the defaults.
36
+ * @returns The CSS variables to set on the element, or an empty object.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * import { clothVars } from "@johnmorrisdotca/kumimoji/ui";
41
+ * console.log(clothVars("green"));
42
+ * ```
43
+ */
44
+ export function clothVars(cloth) {
45
+ if (!isCloth(cloth))
46
+ return {};
47
+ const { felt, ink } = KUMIMOJI_CLOTHS[cloth];
48
+ return { "--km-felt": felt, "--km-accent": felt, "--km-accent-ink": ink };
49
+ }