@johnmorrisdotca/kumimoji 1.3.0 → 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,26 @@ 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
+
17
+ ## [1.3.1] - 2026-10-31
18
+
19
+ ### Changed
20
+
21
+ - Biome replaces ESLint as the formatter and linter, with the family's shared settings in `scripts/biome-family.json`; `pnpm lint` checks formatting too, and `pnpm format` applies it. The code is formatted once to 120 columns. Nothing a user of the package sees has changed.
22
+ - TypeScript targets ES2022, treats an indexed read as possibly missing, and refuses switch fallthrough and unmarked overrides.
23
+ - `pnpm check` also holds every source file to at most 500 lines of code (`scripts/check-size.mjs`; blank lines, comments, tests and data files are not counted).
24
+
25
+ ### Fixed
26
+
27
+ - The issue chooser's security link shows again: an unquoted colon had made `.github/ISSUE_TEMPLATE/config.yml` invalid YAML.
28
+
9
29
  ## [1.3.0] - 2026-10-14
10
30
 
11
31
  The table is a custom element with the family's standard embed attributes and a shared builder, and the package has an English and Japanese documentation site with a checked example for every export. The rules, generation, seeds and saved games are unchanged.
@@ -176,7 +196,8 @@ The first release.
176
196
  `@johnmorrisdotca/kumimoji/react`.
177
197
  - A static demo for GitHub Pages.
178
198
 
179
- [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
180
201
  [1.3.0]: https://github.com/johnmorrisdotca/kumimoji/compare/v1.2.2...v1.3.0
181
202
  [1.2.1]: https://github.com/johnmorrisdotca/kumimoji/compare/v1.2.0...v1.2.1
182
203
  [1.1.1]: https://github.com/johnmorrisdotca/kumimoji/compare/v1.1.0...v1.1.1
package/CONTRIBUTING.md CHANGED
@@ -21,7 +21,8 @@ shape before you spend time on it. Taking part follows the
21
21
 
22
22
  ```sh
23
23
  pnpm install
24
- pnpm check # lint, types and tests: the same as CI
24
+ pnpm check # format, lint, types, file sizes and tests: the same as CI
25
+ pnpm format # format the code and apply the safe lint fixes
25
26
  pnpm test:package # pack it as npm does, install it in an empty project, import every entry
26
27
  pnpm site # build the demo into ./site, as GitHub Pages publishes it
27
28
  ```
@@ -31,12 +32,18 @@ data scripts) are listed under its own heading below.
31
32
 
32
33
  ## House rules, shared by every package of the family
33
34
 
35
+ How the code is written, formatted and checked, and why, is in
36
+ [CODE-STYLE.md](CODE-STYLE.md), the same text in every package.
37
+
34
38
  - **No runtime dependencies.** Development dependencies are for tests, builds and
35
39
  documentation only.
36
40
  - **The core is pure.** Every function in it returns new values and never
37
41
  changes what it was given.
38
42
  - **Test what you change.** Tests sit beside the code they test. A rule you
39
43
  change has a test that would have caught it.
44
+ - **At most 500 lines of code a file** under `src/`, not counting blank lines,
45
+ comments, tests or `*.data.ts` data. A file that reaches it is split by what
46
+ it does, never squeezed.
40
47
  - **Words a person reads come in English and Japanese.** If you cannot write the
41
48
  Japanese, say so in the pull request and someone will.
42
49
  - **Option values and names are kebab case.**
@@ -48,7 +55,9 @@ data scripts) are listed under its own heading below.
48
55
  together with the code.
49
56
  - **The family's own files are the same in every package**: `demo/family.css`,
50
57
  `scripts/family-template.mjs`, `scripts/family-readme.mjs`,
51
- `scripts/release-notes.mjs`, the files in `scripts/community/` and
58
+ `scripts/release-notes.mjs`, the files in `scripts/community/`,
59
+ `CODE-STYLE.md`, `scripts/biome-family.json`, `scripts/check-size.mjs`,
60
+ `src/embed-attributes.ts` where the package has an element, and
52
61
  `family.test.js` (in `src/`, or in `test/`). Do not edit one here. To change
53
62
  one, change it in every repository at once, bump `FAMILY_TEMPLATE_VERSION` for
54
63
  the template, and record the new hash in `family.test.js`. What is the
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.0",
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 is 1.3.0: the table's standard embed attributes, the shared embed builder, and the documentation site with a checked example for every export.
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,6 +1,7 @@
1
+ import { kumimojiRefusal } from "./check-reasons.js";
1
2
  import { decodeGrid, lettersOf, sameLetters } from "./grid.js";
2
3
  import { judgeWithWords } from "./judge.js";
3
- import { kumimojiTileCount, kumimojiWildCount, KUMIMOJI_SCORE, TILE_MIX_TOTAL } from "./tiles.constants.js";
4
+ import { KUMIMOJI_SCORE, kumimojiTileCount, kumimojiWildCount, TILE_MIX_TOTAL } from "./tiles.constants.js";
4
5
  import { tileWords } from "./tileWords.js";
5
6
  /**
6
7
  * WHETHER A GRID FINISHES A KUMIMOJI: the one check the server also runs.
@@ -43,35 +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
- if (bagIdentities.some((tile) => tile === null) || tileIdentities.some((tile) => tile === null) || !sameLetters(lettersOf(tileIdentities), lettersOf(bagIdentities))) {
66
- return { ok: false, reason: "the grid does not use exactly the tiles of the bag" };
66
+ if (bagIdentities.some((tile) => tile === null) ||
67
+ tileIdentities.some((tile) => tile === null) ||
68
+ !sameLetters(lettersOf(tileIdentities), lettersOf(bagIdentities))) {
69
+ return kumimojiRefusal("grid-not-the-bag");
67
70
  }
68
71
  const verdict = judgeWithWords(tiles, words, { diagonals: options.diagonals === true });
69
72
  if (verdict.apart.size > 0)
70
- return { ok: false, reason: "the tiles are not all joined" };
73
+ return kumimojiRefusal("tiles-apart");
71
74
  if (verdict.notWords.length > 0)
72
- return { ok: false, reason: `${verdict.notWords[0]} is not in the word list` };
75
+ return kumimojiRefusal("not-a-word", { word: verdict.notWords[0] });
73
76
  if (!verdict.sound)
74
- return { ok: false, reason: "the grid is not finished" };
77
+ return kumimojiRefusal("grid-not-finished");
75
78
  return { ok: true };
76
79
  }
77
80
  /** A finished game's leaderboard points (`KUMIMOJI_SCORE`): ten a tile, and up to as much again for speed.
@@ -76,7 +76,7 @@ function lacking(word, hand, most) {
76
76
  /** How many tiles already down one word may run through: two lets it bridge a gap, as players do. */
77
77
  const CROSSED_MOST = 2;
78
78
  /** How many placements are tried in one search at most, so a turn's thinking stays a fraction of a second on a phone. */
79
- const FITS_MOST = 20000;
79
+ const FITS_MOST = 20_000;
80
80
  /** A laying's worth: tiles used first, rare letters next, wilds spent last. */
81
81
  function worth(fresh, wilds, words) {
82
82
  let rare = 0;
@@ -118,7 +118,8 @@ function layings(play, words, rules) {
118
118
  }
119
119
  const bounds = boundsOf(play.tiles);
120
120
  const fits = (first, last) => bounds === null ||
121
- (Math.max(bounds.bottom, last.row) - Math.min(bounds.top, first.row) < KUMIMOJI_GRID_MOST && Math.max(bounds.right, last.col) - Math.min(bounds.left, first.col) < KUMIMOJI_GRID_MOST);
121
+ (Math.max(bounds.bottom, last.row) - Math.min(bounds.top, first.row) < KUMIMOJI_GRID_MOST &&
122
+ Math.max(bounds.right, last.col) - Math.min(bounds.left, first.col) < KUMIMOJI_GRID_MOST);
122
123
  const found = [];
123
124
  let tried = 0;
124
125
  let mostFresh = 0;
@@ -150,9 +151,15 @@ function layings(play, words, rules) {
150
151
  if (fit === null)
151
152
  continue;
152
153
  const step = across ? { row: 0, col: 1 } : { row: 1, col: 0 };
153
- if (!fits(fit.first, { row: fit.first.row + (length - 1) * step.row, col: fit.first.col + (length - 1) * step.col }))
154
+ if (!fits(fit.first, {
155
+ row: fit.first.row + (length - 1) * step.row,
156
+ col: fit.first.col + (length - 1) * step.col,
157
+ }))
154
158
  continue;
155
- const fresh = fit.fresh.map((square) => ({ square: squareAt(square.row, square.col), letter: word[(square.row - fit.first.row) + (square.col - fit.first.col)] }));
159
+ const fresh = fit.fresh.map((square) => ({
160
+ square: squareAt(square.row, square.col),
161
+ letter: word[square.row - fit.first.row + (square.col - fit.first.col)],
162
+ }));
156
163
  const wilds = wildsNeeded(fresh, hand);
157
164
  if (wilds !== null)
158
165
  keep({ word, fresh, wilds, score: worth(fresh, wilds, words) });
@@ -1,6 +1,6 @@
1
1
  import { bestLaying, judgeTiles, tradeChoice } from "./computerPlay.js";
2
2
  import { drawAll, isComputer, mayDrawAll, mayTradeThisTurn, seatPlay, withSeatPlay } from "./party.js";
3
- import { doneRefused, endTurn, goesOut, handCanSpell, isLastTurn, laidThisTurn, mayResign, resign } from "./partyTurns.js";
3
+ import { doneRefused, endTurn, goesOut, handCanSpell, isLastTurn, laidThisTurn, mayResign, resign, } from "./partyTurns.js";
4
4
  import { liftAll, mayTrade, trade } from "./play.js";
5
5
  /**
6
6
  * A COMPUTER'S TURN in a pass-and-play game (John, 2026-09-28: "Also you can
@@ -100,7 +100,10 @@ export function planComputerTurn(game, words) {
100
100
  break;
101
101
  }
102
102
  const hand = now.players[now.turn].hand;
103
- const stuck = hand.length > 0 && !laidThisTurn(now) && !mayTrade(seatPlay(now)) && bestLaying(seatPlay(now), words, rules) === null;
103
+ const stuck = hand.length > 0 &&
104
+ !laidThisTurn(now) &&
105
+ !mayTrade(seatPlay(now)) &&
106
+ bestLaying(seatPlay(now), words, rules) === null;
104
107
  const refused = doneRefused(now, verdict(), handSpells) !== null;
105
108
  if ((refused || (stuck && !isLastTurn(now))) && mayResign(now)) {
106
109
  take(resign(now), { kind: "resigned" });
package/dist/daily.js CHANGED
@@ -38,5 +38,5 @@ export function kumimojiDay(at) {
38
38
  */
39
39
  export function kumimojiDailySeed(at) {
40
40
  const [year, month, day] = kumimojiDay(at).split("-").map(Number);
41
- return year * 10000 + month * 100 + day;
41
+ return year * 10_000 + month * 100 + day;
42
42
  }
package/dist/element.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { type KumimojiTableHandle, type KumimojiTableOptions } from "./ui/mount.ts";
2
1
  import type { KumimojiSaved } from "./export.ts";
2
+ import { type KumimojiTableHandle, type KumimojiTableOptions } from "./ui/mount.ts";
3
3
  declare const ElementBase: typeof HTMLElement;
4
4
  /**
5
5
  * THE `<kumimoji-table>` ELEMENT: a game of Kumimoji alone in a tag, with no
@@ -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);