@johnmorrisdotca/kumimoji 1.3.1 → 1.4.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,14 @@ All notable changes to this project are written down here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.4.0] - 2026-11-02
10
+
11
+ ### Added
12
+
13
+ - 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.
14
+
15
+ - **`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.
16
+
9
17
  ## [1.3.1] - 2026-10-31
10
18
 
11
19
  ### Changed
@@ -188,7 +196,8 @@ The first release.
188
196
  `@johnmorrisdotca/kumimoji/react`.
189
197
  - A static demo for GitHub Pages.
190
198
 
191
- [Unreleased]: https://github.com/johnmorrisdotca/kumimoji/compare/v1.3.0...HEAD
199
+ [Unreleased]: https://github.com/johnmorrisdotca/kumimoji/compare/v1.4.0...HEAD
200
+ [1.4.0]: https://github.com/johnmorrisdotca/kumimoji/compare/v1.3.1...v1.4.0
192
201
  [1.3.0]: https://github.com/johnmorrisdotca/kumimoji/compare/v1.2.2...v1.3.0
193
202
  [1.2.1]: https://github.com/johnmorrisdotca/kumimoji/compare/v1.2.0...v1.2.1
194
203
  [1.1.1]: https://github.com/johnmorrisdotca/kumimoji/compare/v1.1.0...v1.1.1
package/README.md CHANGED
@@ -10,7 +10,7 @@ In English and in Japanese kana. The rules as pure, seeded TypeScript, two word
10
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
11
  <img alt="No dependencies" src="https://img.shields.io/badge/dependencies-0-2f5d4a">
12
12
  <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>
13
+ <a href="https://johnmorrisdotca.github.io/kumimoji/docs/api/main/index.html"><img alt="679.3 kB minified and gzipped" src="https://img.shields.io/badge/min%2Bgzip-679.3%20kB-2f5d4a"></a>
14
14
  <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
15
  </p>
16
16
 
@@ -242,7 +242,7 @@ const wrong = decodeGrid("cat/2x")!;
242
242
  judgeWithWords(wrong, words).notWords; // → ["tx"]
243
243
 
244
244
  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" }
245
+ 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
246
  encodeGrid(tiles); // → "cat/2o/2w"
247
247
  ```
248
248
 
@@ -541,7 +541,7 @@ import { checkKumimoji, generateKumimoji, loadTileWords } from "@johnmorrisdotca
541
541
  await loadTileWords("english");
542
542
  const dealt = generateKumimoji(3, "hard", 44);
543
543
  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" }
544
+ 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
545
  ```
546
546
 
547
547
  ### Mount the table with options
@@ -566,6 +566,8 @@ table.destroy();
566
566
 
567
567
  ### A look of your own
568
568
 
569
+ `<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.
570
+
569
571
  Every colour is a CSS variable on `.km-root`, so a stylesheet or the `theme` option can restyle the table:
570
572
 
571
573
  ```ts no-run
@@ -768,7 +770,7 @@ The JSON, as `kumimojiToJSON` writes it:
768
770
  {
769
771
  "format": 1,
770
772
  "game": "kumimoji",
771
- "generator": "kumimoji 1.3.1",
773
+ "generator": "kumimoji 1.4.0",
772
774
  "size": 3,
773
775
  "level": "hard",
774
776
  "seed": 44,
@@ -886,23 +888,14 @@ as you type. The entries:
886
888
  | `deal(bag, handSize)` and `placeFromHand`, `moveOnTable`, `liftToHand`, `draw`, `trade` | The opening hand, and each move, every one returning a new game |
887
889
  | `judgeWithWords(tiles, words)` | What is wrong with a grid, or that it is sound |
888
890
  | `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 |
891
+ | `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
892
  | `loadTileWords(language)` | A word list, fetched when first needed |
891
893
  | `mountKumimoji(element, options?)` and `<kumimoji-table>` | The table, in one call or one tag |
892
894
  | `kumimojiToJSON`, `kumimojiFromJSON` | A game kept, and read back without trusting it |
893
895
 
894
896
  ### The long tables
895
897
 
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`.
898
+ 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
899
 
907
900
  ### The table
908
901
 
@@ -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.4.0, gives a check's reason as a code beside the English, and lets a table be laid in the family's five cloths.
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
+ }
@@ -2,6 +2,7 @@ import { type KumimojiSaved } from "../export.ts";
2
2
  import type { KumimojiDeal, KumimojiLanguage, KumimojiLength, KumimojiLevel } from "../kumimoji.types.ts";
3
3
  import { type TilePlay } from "../play.ts";
4
4
  import { type KumimojiLocale, type KumimojiStrings } from "../strings.ts";
5
+ import { type Cloth } from "./cloth.ts";
5
6
  /**
6
7
  * Optional initial game, presentation, readiness and callback choices.
7
8
  * With nothing, it is a short English game from a hand of seven, with some wild tiles.
@@ -39,6 +40,8 @@ export type KumimojiTableOptions = {
39
40
  strings?: Partial<KumimojiStrings>;
40
41
  /** CSS variables set on the table itself, which win in light and dark alike: `{ "--km-felt": "#23405a" }`. */
41
42
  theme?: Readonly<Record<string, string>>;
43
+ /** The cloth the table is laid in: "green", "blue", "red", "black" or "wood", the family's five. `theme` is laid over it. */
44
+ cloth?: Cloth;
42
45
  /** Whether the table offers saving and loading. True by default. */
43
46
  keep?: boolean;
44
47
  /** Show native play-action buttons; a host hiding them supplies its own actions. */
@@ -63,7 +66,7 @@ export type KumimojiTableHandle = {
63
66
  /** Settles once the initial deal or restored game is ready. */
64
67
  readonly ready: Promise<void>;
65
68
  /** Change presentation and controls without redealing. */
66
- set: (options: Pick<KumimojiTableOptions, "locale" | "strings" | "theme" | "keep" | "controls">) => void;
69
+ set: (options: Pick<KumimojiTableOptions, "locale" | "strings" | "cloth" | "theme" | "keep" | "controls">) => void;
67
70
  /** The game as it stands, or null while it is being dealt. */
68
71
  play: () => TilePlay | null;
69
72
  /** The game as it would be saved (`kumimojiToJSON`), or null while it is being dealt. */
package/dist/ui/mount.js CHANGED
@@ -5,6 +5,7 @@ import { deal, isFinished, liftToHand, moveOnTable, placeFromHand, swapWithHand
5
5
  import { kumimojiStrings } from "../strings.js";
6
6
  import { KUMIMOJI_HANDS } from "../tiles.constants.js";
7
7
  import { loadTileWords } from "../tileWords.js";
8
+ import { clothVars, isCloth } from "./cloth.js";
8
9
  import { button, node, tableDrawing } from "./draw.js";
9
10
  import { KUMIMOJI_STYLE } from "./style.js";
10
11
  /** Hand a string to the person as a file of that name. */
@@ -80,8 +81,17 @@ export function mountKumimoji(target, options = {}) {
80
81
  const root = node("div", "km-root");
81
82
  root.dataset.testid = "km-root";
82
83
  root.dataset.state = "dealing";
83
- for (const [name, value] of Object.entries(current.theme ?? {}))
84
- root.style.setProperty(name, value);
84
+ const wear = (cloth, theme) => {
85
+ for (const name of Object.keys(clothVars("green")))
86
+ root.style.removeProperty(name);
87
+ for (const [name, value] of Object.entries({ ...clothVars(cloth), ...theme }))
88
+ root.style.setProperty(name, value);
89
+ if (isCloth(cloth))
90
+ root.dataset.cloth = cloth;
91
+ else
92
+ delete root.dataset.cloth;
93
+ };
94
+ wear(current.cloth, current.theme ?? {});
85
95
  const style = node("style");
86
96
  style.textContent = KUMIMOJI_STYLE;
87
97
  const status = node("p", "km-status");
@@ -311,9 +321,8 @@ export function mountKumimoji(target, options = {}) {
311
321
  for (const name of Object.keys(current.theme ?? {}))
312
322
  if (changes.theme !== undefined && !(name in changes.theme))
313
323
  root.style.removeProperty(name);
314
- if (changes.theme !== undefined)
315
- for (const [name, value] of Object.entries(changes.theme))
316
- root.style.setProperty(name, value);
324
+ if (changes.theme !== undefined || "cloth" in changes)
325
+ wear("cloth" in changes ? changes.cloth : current.cloth, changes.theme ?? current.theme ?? {});
317
326
  current = { ...current, ...changes };
318
327
  if (changes.locale !== undefined)
319
328
  session.locale = changes.locale;
@@ -9,4 +9,4 @@
9
9
  * console.log(KUMIMOJI_STYLE.includes(".km-root")); // true
10
10
  * ```
11
11
  */
12
- export declare const KUMIMOJI_STYLE = "\n.km-root {\n --km-square: 44px; --km-felt: #2f5d4a; --km-grid-line: rgba(255,255,255,.12); --km-tile: #f3e6c8; --km-tile-ink: #2a2118;\n --km-wild: #f6d27a; --km-wrong: #d9534f; --km-held: #ffcf3f; --km-ink: #1f2320; --km-panel: #f7f3ea;\n --km-line: rgba(20,20,20,.35); --km-accent: #2f5d4a; --km-accent-ink: #fff;\n --km-radius: 12px; --km-font: system-ui, -apple-system, \"Segoe UI\", sans-serif;\n display: grid; gap: 10px; min-width: 0; color: var(--km-ink); font-family: var(--km-font); font-size: 15px;\n}\n@media (prefers-color-scheme: dark) {\n .km-root { --km-felt: #1e3a2f; --km-ink: #ece8dc; --km-panel: #1d201e; --km-line: rgba(255,255,255,.25); --km-accent: #6fb08f; --km-accent-ink: #10150f; }\n}\n.km-root > * { min-width: 0; }\n.km-status { margin: 0; min-height: 1.4em; font-variant-numeric: tabular-nums; }\n.km-board { background: var(--km-felt); border-radius: var(--km-radius); padding: 10px; overflow: auto; max-height: 70vh; }\n.km-grid { display: grid; gap: 2px; width: max-content; margin: 0 auto; }\n.km-square { width: var(--km-square); height: var(--km-square); border: 0; border-radius: 5px; background: var(--km-grid-line); padding: 0; cursor: pointer; touch-action: manipulation; }\n.km-tile { display: grid; place-items: center; width: var(--km-square); height: var(--km-square); border-radius: 6px; border: 0; padding: 0;\n background: var(--km-tile); color: var(--km-tile-ink); font: 700 calc(var(--km-square) * .55)/1 var(--km-font); text-transform: uppercase;\n box-shadow: 0 2px 0 rgba(0,0,0,.35); cursor: pointer; touch-action: manipulation; }\n.km-japanese .km-tile { font-size: calc(var(--km-square) * .52); text-transform: none; }\n.km-wild { background: var(--km-wild); }\n.km-misspelt { box-shadow: 0 0 0 3px var(--km-wrong) inset, 0 2px 0 rgba(0,0,0,.35); }\n.km-apart { opacity: .6; }\n.km-held { outline: 3px solid var(--km-held); outline-offset: 1px; }\n.km-square:focus-visible, .km-in-hand:focus-visible, .km-button:focus-visible, .km-keep-title:focus-visible { outline: 3px solid var(--km-held); outline-offset: 2px; }\n.km-notes { margin: 0; min-height: 1.4em; font-size: 14px; opacity: .8; overflow-wrap: anywhere; }\n.km-hand { display: flex; flex-wrap: wrap; gap: 6px; min-height: calc(var(--km-square) + 16px); align-items: center; padding: 8px; border-radius: 10px; background: var(--km-panel); }\n.km-empty { opacity: .6; font-size: 14px; }\n.km-picker { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }\n.km-picker:empty { display: none; }\n.km-picker-title { width: 100%; font-size: 14px; }\n.km-picker .km-button { padding: 0 8px; text-transform: uppercase; }\n.km-japanese .km-picker .km-button { text-transform: none; }\n.km-controls[hidden] { display: none; }\n.km-controls { display: flex; flex-wrap: wrap; gap: 8px; }\n.km-button { font: inherit; min-height: 44px; min-width: 44px; padding: 0 14px; border-radius: 8px; border: 1px solid var(--km-line); background: var(--km-panel); color: var(--km-ink); cursor: pointer; touch-action: manipulation; }\n.km-button:disabled { opacity: .45; cursor: default; }\n.km-primary { background: var(--km-accent); color: var(--km-accent-ink); border-color: transparent; }\n.km-keep { border-radius: 10px; background: var(--km-panel); padding: 0 10px; }\n.km-keep[open] { padding-bottom: 10px; }\n.km-keep-title { min-height: 44px; display: flex; align-items: center; font-weight: 600; cursor: pointer; }\n.km-note { margin: 8px 0 0; min-height: 1.4em; font-size: 13px; }\n";
12
+ export declare const KUMIMOJI_STYLE = "\n.km-root {\n --km-square: 44px; --km-felt: #2f5d4a; --km-grid-line: rgba(255,255,255,.12); --km-tile: #f3e6c8; --km-tile-ink: #2a2118;\n --km-wild: #f6d27a; --km-wrong: #d9534f; --km-held: #ffcf3f; --km-ink: #1f2320; --km-panel: #f7f3ea;\n --km-line: rgba(20,20,20,.35); --km-accent: #2f5d4a; --km-accent-ink: #fff;\n --km-radius: 12px; --km-font: system-ui, -apple-system, \"Segoe UI\", sans-serif;\n display: grid; gap: 10px; min-width: 0; color: var(--km-ink); font-family: var(--km-font); font-size: 15px;\n}\n@media (prefers-color-scheme: dark) {\n .km-root { --km-felt: #1e3a2f; --km-ink: #ece8dc; --km-panel: #1d201e; --km-line: rgba(255,255,255,.25); --km-accent: #6fb08f; --km-accent-ink: #10150f; }\n}\n.km-root > * { min-width: 0; }\n.km-status { margin: 0; min-height: 1.4em; font-variant-numeric: tabular-nums; }\n.km-root, .km-root * { line-height: 1.35; }\n.km-board { background: var(--km-felt); border-radius: var(--km-radius); padding: 10px; overflow: auto; max-height: 70vh; }\n.km-grid { display: grid; gap: 2px; width: max-content; margin: 0 auto; }\n.km-square { width: var(--km-square); height: var(--km-square); border: 0; border-radius: 5px; background: var(--km-grid-line); padding: 0; cursor: pointer; touch-action: manipulation; }\n.km-tile { display: grid; place-items: center; width: var(--km-square); height: var(--km-square); border-radius: 6px; border: 0; padding: 0;\n background: var(--km-tile); color: var(--km-tile-ink); font: 700 calc(var(--km-square) * .55)/1 var(--km-font); text-transform: uppercase;\n box-shadow: 0 2px 0 rgba(0,0,0,.35); cursor: pointer; touch-action: manipulation; }\n.km-japanese .km-tile { font-size: calc(var(--km-square) * .52); text-transform: none; }\n.km-wild { background: var(--km-wild); }\n.km-misspelt { box-shadow: 0 0 0 3px var(--km-wrong) inset, 0 2px 0 rgba(0,0,0,.35); }\n.km-apart { opacity: .6; }\n.km-held { outline: 3px solid var(--km-held); outline-offset: 1px; }\n.km-square:focus-visible, .km-in-hand:focus-visible, .km-button:focus-visible, .km-keep-title:focus-visible { outline: 3px solid var(--km-held); outline-offset: 2px; }\n.km-notes { margin: 0; min-height: 1.4em; font-size: 14px; opacity: .8; overflow-wrap: anywhere; }\n.km-hand { display: flex; flex-wrap: wrap; gap: 6px; min-height: calc(var(--km-square) + 16px); align-items: center; padding: 8px; border-radius: 10px; background: var(--km-panel); }\n.km-empty { opacity: .6; font-size: 14px; }\n.km-picker { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }\n.km-picker:empty { display: none; }\n.km-picker-title { width: 100%; font-size: 14px; }\n.km-picker .km-button { padding: 0 8px; text-transform: uppercase; }\n.km-japanese .km-picker .km-button { text-transform: none; }\n.km-controls[hidden] { display: none; }\n.km-controls { display: flex; flex-wrap: wrap; gap: 8px; }\n.km-button { font: inherit; min-height: 44px; min-width: 44px; padding: 0 14px; border-radius: 8px; border: 1px solid var(--km-line); background: var(--km-panel); color: var(--km-ink); cursor: pointer; touch-action: manipulation; }\n.km-button:disabled { opacity: .45; cursor: default; }\n.km-primary { background: var(--km-accent); color: var(--km-accent-ink); border-color: transparent; }\n.km-keep { border-radius: 10px; background: var(--km-panel); padding: 0 10px; }\n.km-keep[open] { padding-bottom: 10px; }\n.km-keep-title { min-height: 44px; display: flex; align-items: center; font-weight: 600; cursor: pointer; }\n.km-note { margin: 8px 0 0; min-height: 1.4em; font-size: 13px; }\n";
package/dist/ui/style.js CHANGED
@@ -22,6 +22,7 @@ export const KUMIMOJI_STYLE = `
22
22
  }
23
23
  .km-root > * { min-width: 0; }
24
24
  .km-status { margin: 0; min-height: 1.4em; font-variant-numeric: tabular-nums; }
25
+ .km-root, .km-root * { line-height: 1.35; }
25
26
  .km-board { background: var(--km-felt); border-radius: var(--km-radius); padding: 10px; overflow: auto; max-height: 70vh; }
26
27
  .km-grid { display: grid; gap: 2px; width: max-content; margin: 0 auto; }
27
28
  .km-square { width: var(--km-square); height: var(--km-square); border: 0; border-radius: 5px; background: var(--km-grid-line); padding: 0; cursor: pointer; touch-action: manipulation; }
package/dist/ui.d.ts CHANGED
@@ -5,5 +5,6 @@
5
5
  */
6
6
  export { KUMIMOJI_STRINGS, type KumimojiLocale, type KumimojiStrings, kumimojiSay, kumimojiStrings, } from "./strings.ts";
7
7
  export { BOARD_LEAST, BOARD_MARGIN, type BoardModel, type BoardSquare, boardModel, type TileMark } from "./ui/board.ts";
8
+ export { type Cloth, clothVars, isCloth, KUMIMOJI_CLOTHS } from "./ui/cloth.ts";
8
9
  export { type KumimojiTableHandle, type KumimojiTableOptions, mountKumimoji } from "./ui/mount.ts";
9
10
  export { KUMIMOJI_STYLE } from "./ui/style.ts";
package/dist/ui.js CHANGED
@@ -5,5 +5,6 @@
5
5
  */
6
6
  export { KUMIMOJI_STRINGS, kumimojiSay, kumimojiStrings, } from "./strings.js";
7
7
  export { BOARD_LEAST, BOARD_MARGIN, boardModel } from "./ui/board.js";
8
+ export { clothVars, isCloth, KUMIMOJI_CLOTHS } from "./ui/cloth.js";
8
9
  export { mountKumimoji } from "./ui/mount.js";
9
10
  export { KUMIMOJI_STYLE } from "./ui/style.js";
package/dist/version.d.ts CHANGED
@@ -6,4 +6,4 @@
6
6
  * console.log(KUMIMOJI_VERSION);
7
7
  * ```
8
8
  */
9
- export declare const KUMIMOJI_VERSION = "1.3.1";
9
+ export declare const KUMIMOJI_VERSION = "1.4.0";
package/dist/version.js CHANGED
@@ -6,4 +6,4 @@
6
6
  * console.log(KUMIMOJI_VERSION);
7
7
  * ```
8
8
  */
9
- export const KUMIMOJI_VERSION = "1.3.1";
9
+ export const KUMIMOJI_VERSION = "1.4.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/kumimoji",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "Kumimoji 組み文字 — Crossword tile game in English and Japanese: seeded bags, solo races, two-to-eight-player tables, word lists and a playable table for React, Vue, Svelte, Angular or plain HTML. Zero dependencies.",
5
5
  "keywords": [
6
6
  "ai",