@johnmorrisdotca/kumimoji 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/CONTRIBUTING.md +38 -0
  3. package/LICENSE +21 -0
  4. package/NOTICE.md +38 -0
  5. package/README.md +233 -0
  6. package/dist/check.d.ts +19 -0
  7. package/dist/check.js +70 -0
  8. package/dist/computer.types.d.ts +23 -0
  9. package/dist/computer.types.js +1 -0
  10. package/dist/computerPlay.d.ts +17 -0
  11. package/dist/computerPlay.js +203 -0
  12. package/dist/computerTurn.d.ts +31 -0
  13. package/dist/computerTurn.js +94 -0
  14. package/dist/generate.d.ts +29 -0
  15. package/dist/generate.js +278 -0
  16. package/dist/grid.d.ts +97 -0
  17. package/dist/grid.js +208 -0
  18. package/dist/help.d.ts +20 -0
  19. package/dist/help.js +57 -0
  20. package/dist/index.d.ts +44 -0
  21. package/dist/index.js +44 -0
  22. package/dist/judge.d.ts +13 -0
  23. package/dist/judge.js +19 -0
  24. package/dist/kana.d.ts +25 -0
  25. package/dist/kana.js +38 -0
  26. package/dist/kumimoji.types.d.ts +49 -0
  27. package/dist/kumimoji.types.js +1 -0
  28. package/dist/party.d.ts +91 -0
  29. package/dist/party.js +172 -0
  30. package/dist/party.types.d.ts +86 -0
  31. package/dist/party.types.js +1 -0
  32. package/dist/partyKept.d.ts +17 -0
  33. package/dist/partyKept.js +149 -0
  34. package/dist/partySeats.d.ts +53 -0
  35. package/dist/partySeats.js +119 -0
  36. package/dist/partyTurns.d.ts +75 -0
  37. package/dist/partyTurns.js +153 -0
  38. package/dist/placement.d.ts +26 -0
  39. package/dist/placement.js +87 -0
  40. package/dist/play.d.ts +76 -0
  41. package/dist/play.js +165 -0
  42. package/dist/random.d.ts +11 -0
  43. package/dist/random.js +25 -0
  44. package/dist/react.d.ts +32 -0
  45. package/dist/react.js +53 -0
  46. package/dist/showcase.d.ts +51 -0
  47. package/dist/showcase.js +59 -0
  48. package/dist/tableView.d.ts +92 -0
  49. package/dist/tableView.js +127 -0
  50. package/dist/tileFace.d.ts +25 -0
  51. package/dist/tileFace.js +39 -0
  52. package/dist/tileFamily.d.ts +11 -0
  53. package/dist/tileFamily.js +23 -0
  54. package/dist/tileWords.d.ts +70 -0
  55. package/dist/tileWords.js +164 -0
  56. package/dist/tileWordsModule.d.ts +4 -0
  57. package/dist/tileWordsModule.js +13 -0
  58. package/dist/tiles.constants.d.ts +108 -0
  59. package/dist/tiles.constants.js +132 -0
  60. package/dist/turn.d.ts +62 -0
  61. package/dist/turn.js +96 -0
  62. package/dist/ui/board.d.ts +29 -0
  63. package/dist/ui/board.js +37 -0
  64. package/dist/ui/mount.d.ts +36 -0
  65. package/dist/ui/mount.js +257 -0
  66. package/dist/ui/style.d.ts +6 -0
  67. package/dist/ui/style.js +40 -0
  68. package/dist/ui.d.ts +8 -0
  69. package/dist/ui.js +8 -0
  70. package/dist/wilds.d.ts +27 -0
  71. package/dist/wilds.js +111 -0
  72. package/dist/words.d.ts +6 -0
  73. package/dist/words.en.data.d.ts +23 -0
  74. package/dist/words.en.data.js +610 -0
  75. package/dist/words.ja.data.d.ts +29 -0
  76. package/dist/words.ja.data.js +29 -0
  77. package/dist/words.js +6 -0
  78. package/package.json +90 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,53 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are written down here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [1.0.1] - 2026-09-30
10
+
11
+ ### Fixed
12
+
13
+ - The package loads through `require()` as well as `import` (Node 22 and
14
+ later, and test runners that compile to CommonJS): each export's condition is
15
+ `default` rather than `import`.
16
+
17
+ ## [1.0.0] - 2026-09-30
18
+
19
+ The first stable release: the API as documented in the README is now kept stable
20
+ until a 2.0.0.
21
+
22
+ ## [0.1.0] - 2026-09-30
23
+
24
+ The first release.
25
+
26
+ ### Added
27
+
28
+ - Games dealt from a seed by laying a crossword first, so every bag can be
29
+ finished: hands of 3, 7 or 11, three levels of wild tiles, short, medium and
30
+ full games, and two English sets at once.
31
+ - English and Japanese: 144 tiles in each, 110,316 English words from SCOWL and
32
+ 163,461 Japanese readings from JMdict, loaded when first needed.
33
+ - Playing a game as plain values: lay, move, swap, lift, sort, draw and trade
34
+ one tile for three; wild tiles given any letter or kana.
35
+ - Every run across and down judged against the list, with Diagonals as a
36
+ choice; a finished grid checked against its bag in time proportional to its
37
+ size; points for a finish, ten a tile and more for speed.
38
+ - Help: the words the hand's own tiles spell, longest first.
39
+ - A table of two to eight sharing one bag: turns, drawing together, the last
40
+ round, going out, resigning, joining and leaving, kept as text; and a
41
+ computer player that can take a seat.
42
+ - Turning the table a quarter at a time with every tile kept upright, and the
43
+ sums for fitting, zooming and panning a view of it.
44
+ - A whole game alone in plain DOM (`@johnmorrisdotca/kumimoji/ui`), light and
45
+ dark, themeable through CSS variables.
46
+ - `KumimojiBoard` and `KumimojiTable`, React components, from
47
+ `@johnmorrisdotca/kumimoji/react`.
48
+ - A static demo for GitHub Pages.
49
+
50
+ [Unreleased]: https://github.com/johnmorrisdotca/kumimoji/compare/v1.0.1...HEAD
51
+ [1.0.1]: https://github.com/johnmorrisdotca/kumimoji/releases/tag/v1.0.1
52
+ [1.0.0]: https://github.com/johnmorrisdotca/kumimoji/releases/tag/v1.0.0
53
+ [0.1.0]: https://github.com/johnmorrisdotca/kumimoji/commits/v1.0.0
@@ -0,0 +1,38 @@
1
+ # Contributing to Kumimoji
2
+
3
+ Thank you for wanting to help. Bug reports, rule questions and pull requests are
4
+ all welcome.
5
+
6
+ ## Reporting a bug
7
+
8
+ Open an [issue](https://github.com/johnmorrisdotca/kumimoji/issues) with what you did,
9
+ what you expected and what happened. A game's seed makes almost any bug
10
+ reproducible: include it, and the moves if you have them.
11
+
12
+ ## Making a change
13
+
14
+ ```sh
15
+ git clone https://github.com/johnmorrisdotca/kumimoji.git
16
+ cd kumimoji
17
+ pnpm install
18
+ pnpm run check # lint, types and tests, as CI runs them
19
+ pnpm run site # build the demo into site/ and open site/index.html
20
+ ```
21
+
22
+ - The rules are pure functions over plain values: a function returns a new
23
+ game and never changes the one it was given. Keep it that way.
24
+ - Anything random is drawn from the game's own seeded random, so a game always
25
+ replays from its seed. Never call `Math.random` inside the rules.
26
+ - Add or change a test beside the code you change (`*.test.ts`).
27
+ - Note the change under **Unreleased** in [CHANGELOG.md](CHANGELOG.md).
28
+ - Keep pull requests to one change each, with a message that says what a player
29
+ or a developer would notice.
30
+
31
+ ## Releasing
32
+
33
+ Maintainers bump the version in `package.json`, move **Unreleased** in the
34
+ changelog under the new version and date, tag `v<version>`, and publish with
35
+ `pnpm publish --access public`.
36
+
37
+ By contributing you agree that your work is released under the
38
+ [MIT licence](LICENSE), and to follow the [code of conduct](CODE_OF_CONDUCT.md).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 John Morris
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE.md ADDED
@@ -0,0 +1,38 @@
1
+ # Notice: the word lists' licences
2
+
3
+ The code of this package is under the MIT licence (see [LICENSE](LICENSE)). The
4
+ two word lists it ships are other people's work, used under their own terms.
5
+
6
+ ## English: SCOWL
7
+
8
+ `src/words.en.data.ts` is written from SCOWL 2020.12.07
9
+ (http://wordlist.aspell.net/), sizes 10 to 70, English and American spellings,
10
+ every length from 2 to 15: 110,316 words. Its notice, which its licence asks to
11
+ travel with the lists:
12
+
13
+ > The collective work is Copyright 2000-2018 by Kevin Atkinson as well
14
+ > as any of the copyrights mentioned below:
15
+ >
16
+ > Copyright 2000-2018 by Kevin Atkinson
17
+ >
18
+ > Permission to use, copy, modify, distribute and sell these word
19
+ > lists, the associated scripts, the output created from the scripts,
20
+ > and its documentation for any purpose is hereby granted without fee,
21
+ > provided that the above copyright notice appears in all copies and
22
+ > that both that copyright notice and this permission notice appear in
23
+ > supporting documentation. Kevin Atkinson makes no representations
24
+ > about the suitability of this array for any purpose. It is provided
25
+ > "as is" without express or implied warranty.
26
+
27
+ The full notice, with the copyrights of the lists SCOWL is built from, is in
28
+ SCOWL's own distribution.
29
+
30
+ ## Japanese: JMdict
31
+
32
+ `src/words.ja.data.ts` is derived from JMdict, the Japanese-Multilingual
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
35
+ 15 kana, each spelt in the 45 base kana the tiles show. JMdict is the property of
36
+ the EDRDG and is used under the Creative Commons Attribution-ShareAlike 4.0
37
+ licence, with the group's conditions: https://www.edrdg.org/edrdg/licence.html.
38
+ This derived list is shared under the same licence.
package/README.md ADDED
@@ -0,0 +1,233 @@
1
+ # Kumimoji 組み文字
2
+
3
+ [![CI](https://github.com/johnmorrisdotca/kumimoji/actions/workflows/ci.yml/badge.svg)](https://github.com/johnmorrisdotca/kumimoji/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/@johnmorrisdotca/kumimoji.svg)](https://www.npmjs.com/package/@johnmorrisdotca/kumimoji)
5
+ [![Licence: MIT](https://img.shields.io/badge/licence-MIT-blue.svg)](LICENSE)
6
+ ![Types included](https://img.shields.io/badge/types-TypeScript-3178c6.svg)
7
+ ![Dependencies: none](https://img.shields.io/badge/dependencies-none-brightgreen.svg)
8
+
9
+ **The crossword tile race, in English and Japanese, as pure and seeded TypeScript.**
10
+
11
+ Build one crossword from every tile in the bag. Lay your hand as words that
12
+ cross, and when your hand is empty and every run across and down is a word, draw
13
+ the next tile. A tile you cannot use can be traded for three. Use the last tile
14
+ in the bag and you are done. Play alone against the clock, or at a table of two
15
+ to eight sharing one bag, in English or in Japanese.
16
+
17
+ *Kumimoji* (組み文字) is "letters put together".
18
+
19
+ **[Play it](https://johnmorrisdotca.github.io/kumimoji/)**
20
+
21
+ <p>
22
+ <img src="docs/desktop.jpg" alt="An English game: BACK across and RAW down, with a wild tile left in hand" width="560">
23
+ <img src="docs/phone.jpg" alt="The same game on a phone in dark mode" width="200">
24
+ </p>
25
+
26
+ - **Dealt from a seed.** `generateKumimoji` lays a crossword first and deals its
27
+ tiles, so every bag can be finished, and two players racing the same seed get
28
+ the same tiles in the same order.
29
+ - **English and Japanese.** 144 tiles in each: the English letter mix, or 45 base
30
+ kana where が plays as か, ゃ as や and を as お. 110,316 English words and
31
+ 163,461 Japanese readings, from 2 to 15 tiles.
32
+ - **Wild tiles** that take any letter or kana, and are read as that letter in
33
+ every word they are part of.
34
+ - **Diagonals**, if chosen: every diagonal run of three or more must be a word
35
+ too.
36
+ - **The rules as plain functions.** A game in progress is a value
37
+ (`TilePlay`): `deal`, `placeFromHand`, `moveOnTable`, `liftToHand`, `draw`,
38
+ `trade` each return a new one. `judgeWithWords` says which runs are not words
39
+ and which tiles are apart; `checkKumimoji` checks a finished grid against its
40
+ bag, in time proportional to its size.
41
+ - **A table of two to eight** (`startParty`, `endTurn`, `drawAll`, `resign`,
42
+ `joinParty`, `leaveParty`), kept as text (`encodeParty`, `decodeParty`), with a
43
+ computer player that can take a seat (`planComputerTurn`).
44
+ - **A table to play on.** `mountKumimoji` plays a whole game alone in plain DOM;
45
+ `KumimojiBoard` draws a grid in React.
46
+
47
+ It is the Kumimoji on [Itsutsu](https://itsutsu.com/games/kumimoji), which plays
48
+ alone, in races, round one device and across several with this package.
49
+
50
+ ## How to play
51
+
52
+ 1. Everyone starts with a hand of tiles from the bag: 3, 7 or 11.
53
+ 2. Lay your tiles on the table as a crossword of your own. Every run of two or
54
+ more tiles, across and down, must be a word, and every tile must join the
55
+ one crossword. Move and take back tiles as often as you like.
56
+ 3. When your hand is empty and the crossword is sound, draw the next tile. At a
57
+ table, everyone draws together.
58
+ 4. Stuck with a tile you cannot use? Trade it for three from the bag.
59
+ 5. A wild tile is any letter or kana you give it, and reads as that in every
60
+ word it is part of.
61
+ 6. Near the end of the bag, a player who lays their last tile in a sound
62
+ crossword goes out. Everyone else has one last turn to do the same, and all
63
+ who go out win. Alone, you use every tile in the bag and race the clock.
64
+
65
+ ## Install
66
+
67
+ ```sh
68
+ pnpm add @johnmorrisdotca/kumimoji # or: npm install @johnmorrisdotca/kumimoji
69
+ ```
70
+
71
+ Or straight from a GitHub release, pinned to its version:
72
+
73
+ ```sh
74
+ pnpm add https://github.com/johnmorrisdotca/kumimoji/releases/download/v1.0.1/johnmorrisdotca-kumimoji-1.0.1.tgz
75
+ ```
76
+
77
+ ES modules with types, and no dependencies. The React components need React 18
78
+ or later.
79
+
80
+ ## Play a game in code
81
+
82
+ ```ts
83
+ import { deal, draw, generateKumimoji, judgeWithWords, loadTileWords, mayDraw, placeFromHand, squareAt } from "@johnmorrisdotca/kumimoji";
84
+
85
+ const words = await loadTileWords("english"); // fetched once, the first time
86
+ const dealt = generateKumimoji(7, "medium", 2026); // a hand of 7, some wilds, seed 2026
87
+ let play = deal(dealt.givens, 7);
88
+
89
+ play = placeFromHand(play, 0, squareAt(0, 0)); // the first tile of the hand, on row 0, column 0
90
+ const verdict = judgeWithWords(play.tiles, words);
91
+ if (mayDraw(play, verdict)) play = draw(play);
92
+ ```
93
+
94
+ - `generateKumimoji(hand, level, seed, options?)`: a hand of 3, 7 or 11; a
95
+ level of `"easy"`, `"medium"` or `"hard"` (more wild tiles, fewer, none);
96
+ options `language`, `gameLength` (`"short"`, `"medium"`, or `"full"`, the
97
+ whole set), `doubleSet` (two English sets) and `diagonals`. Returns the bag as
98
+ `givens` and one finished grid as `solution`.
99
+ - A square is a string key, `squareAt(row, col)`; the grid has no edges.
100
+ - A tile is one character: an English letter, `*` for a wild with no letter, a
101
+ capital for a wild given that letter (`assignHandTile`), and for Japanese a
102
+ private-use character per kana (`tileFace` says what is printed on any tile).
103
+ - `isFinished(play, verdict)`: the hand empty, the bag empty, and every run a
104
+ word in one piece.
105
+
106
+ ### Checking a finished game
107
+
108
+ ```ts
109
+ import { checkKumimoji, encodeGrid } from "@johnmorrisdotca/kumimoji";
110
+
111
+ checkKumimoji(7, dealt.givens, encodeGrid(play.tiles), { level: "medium" });
112
+ // { ok: true } or { ok: false, reason: "zqx is not in the word list" }
113
+ ```
114
+
115
+ It checks that the grid uses exactly the tiles of the bag, is all one piece, and
116
+ that every run is a word. Nothing is searched, so a server can run it on every
117
+ game handed in.
118
+
119
+ ### The word lists
120
+
121
+ In a browser, `loadTileWords(language)` fetches the list as its own module the
122
+ first time it is needed (about 600 KB for English, 700 KB for Japanese before
123
+ compression), so nothing else carries it. On a server, in a test or in a script,
124
+ import the `words` entry once, which reads the lists from this package's files:
125
+
126
+ ```ts
127
+ import "@johnmorrisdotca/kumimoji/words";
128
+ ```
129
+
130
+ `tileWords(language)` then hands back the loaded list: `allowed` (every word),
131
+ `byLength`, the tile `mix`, and the functions that turn tiles into words.
132
+
133
+ ### A table of players
134
+
135
+ ```ts
136
+ import { endTurn, handCanSpell, judgeWithWords, placeFromHand, seatPlay, squareAt, startParty, withSeatPlay } from "@johnmorrisdotca/kumimoji";
137
+
138
+ let game = startParty(
139
+ { size: 7, level: "medium", seed: 1, gameLength: "short", language: "english", doubleSet: false, diagonals: false, hints: false },
140
+ dealt.givens,
141
+ ["Ann", { name: "Computer", computer: true }],
142
+ );
143
+ // The player to move plays their own hand and table as a TilePlay:
144
+ game = withSeatPlay(game, placeFromHand(seatPlay(game), 0, squareAt(0, 0)));
145
+ game = endTurn(game, judgeWithWords(seatPlay(game).tiles, words), (hand) => handCanSpell(hand, words));
146
+ ```
147
+
148
+ `afterComputerTurn(game, words)` plays a computer seat's whole turn;
149
+ `planComputerTurn` lists its steps, for a page to show them one at a time.
150
+
151
+ ## Play on a page
152
+
153
+ ```html
154
+ <div id="table"></div>
155
+ <script type="module">
156
+ import { mountKumimoji } from "@johnmorrisdotca/kumimoji/ui";
157
+ mountKumimoji(document.getElementById("table"), {
158
+ language: "english",
159
+ hand: 7,
160
+ onFinish: ({ elapsedMs }) => console.log(`Finished in ${elapsedMs / 1000}s`),
161
+ });
162
+ </script>
163
+ ```
164
+
165
+ Options: `language`, `hand`, `level`, `gameLength`, `diagonals`, `seed`,
166
+ `onFinish`. The handle has `play()`, `newGame(options?)` and `destroy()`.
167
+ Colours are CSS variables on `.km-root` (`--km-felt`, `--km-tile` and the
168
+ rest), light and dark.
169
+
170
+ ### In React
171
+
172
+ ```tsx
173
+ import { KumimojiBoard, KumimojiTable } from "@johnmorrisdotca/kumimoji/react";
174
+
175
+ <KumimojiBoard tiles={play.tiles} verdict={verdict} glyphOf={words.glyphOf} onSquare={(square) => press(square)} />
176
+ <KumimojiTable language="japanese" hand={7} />
177
+ ```
178
+
179
+ ## API at a glance
180
+
181
+ | Entry | What it holds |
182
+ | --- | --- |
183
+ | `@johnmorrisdotca/kumimoji` | Dealing: `generateKumimoji`, `KUMIMOJI_HANDS`; playing alone: `deal`, `placeFromHand`, `moveOnTable`, `swapWithHand`, `liftToHand`, `liftAll`, `sortHand`, `assignHandTile`, `draw`, `trade`, `mayDraw`, `mayTrade`, `isFinished`; judging: `judgeWithWords`, `runsOf`, `checkKumimoji`, `kumimojiPoints`; words: `loadTileWords`, `tileWords`, `wordsInHand`; a table: `startParty`, `endTurn`, `drawAll`, `resign`, `joinParty`, `leaveParty`, `encodeParty`, `decodeParty`; the computer: `planComputerTurn`, `afterComputerTurn`; views: `fitView`, `zoomView`, `panView`, `turnView`; and every type (`TilePlay`, `PartyGame`, `GridVerdict`, …). |
184
+ | `@johnmorrisdotca/kumimoji/words` | Loads both word lists from this package's files, for a server, a test or a script. |
185
+ | `@johnmorrisdotca/kumimoji/ui` | `mountKumimoji`, a whole game in plain DOM; `boardModel` for drawing a board yourself. |
186
+ | `@johnmorrisdotca/kumimoji/react` | `KumimojiBoard` and `KumimojiTable`. |
187
+
188
+ Every function is typed and documented in the source, and your editor shows the
189
+ documentation as you type.
190
+
191
+ ## Browser support
192
+
193
+ Any browser that runs ES2022 modules: current Chrome, Edge, Firefox and Safari,
194
+ on a desk or a phone. The rules have no DOM in them and run the same in Node 20
195
+ or later, Deno, Bun and web workers. The table follows the system's light or
196
+ dark mode and needs nothing but a container element.
197
+
198
+ ## Develop
199
+
200
+ ```sh
201
+ pnpm install
202
+ pnpm test # the rules, the lists, the table and the computer player
203
+ pnpm run build # dist/
204
+ pnpm run site # the demo in site/, as GitHub Pages serves it
205
+ ```
206
+
207
+ ## Roadmap
208
+
209
+ - More languages, each from a real dictionary of that language.
210
+ - Playing the plain-DOM table from a keyboard, as the React one can.
211
+ - A computer player at three strengths.
212
+ - Word definitions for a finished crossword.
213
+
214
+ ## Contributing
215
+
216
+ Issues and pull requests are welcome. [CONTRIBUTING.md](CONTRIBUTING.md) says
217
+ how to set up, what the checks are and how a change is written up, and everyone
218
+ taking part follows the [code of conduct](CODE_OF_CONDUCT.md).
219
+
220
+ ## Licence and notices
221
+
222
+ The code is MIT, © John Morris; see [LICENSE](LICENSE).
223
+
224
+ The word lists are other people's work, used under their own terms, and set out
225
+ in full in [NOTICE.md](NOTICE.md):
226
+
227
+ - **English**: from [SCOWL](http://wordlist.aspell.net/), © Kevin Atkinson and
228
+ others, under a permissive licence whose notice travels with the lists.
229
+ - **Japanese**: readings derived from
230
+ [JMdict](https://www.edrdg.org/jmdict/j_jmdict.html), © the Electronic
231
+ Dictionary Research and Development Group, under
232
+ [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). The
233
+ Japanese list file (`src/words.ja.data.ts`) is shared under the same licence.
@@ -0,0 +1,19 @@
1
+ import type { KumimojiCheck, KumimojiLevel, KumimojiOptions } from "./kumimoji.types.ts";
2
+ /**
3
+ * WHETHER A GRID FINISHES A KUMIMOJI: the one check the server also runs.
4
+ * O(squares) and a lookup a word, nothing searched: the grid uses exactly the
5
+ * tiles of the bag, as many of each, it is all one piece, and every run of two
6
+ * or more across or down is in the list — and, for a game set up with
7
+ * Diagonals, every run of three or more along a diagonal. Which tiles were
8
+ * traded on the way does not matter — a finished game holds the whole bag
9
+ * whatever came out of it when — so the answer is the grid alone.
10
+ *
11
+ * Written out here rather than asked of the play page, which is what made the
12
+ * grid; the list must have been loaded (`loadTileWords`), and a check that
13
+ * cannot read it refuses.
14
+ */
15
+ export declare function checkKumimoji(size: number, givens: string, answer: string, options?: KumimojiOptions & {
16
+ level?: KumimojiLevel;
17
+ }): KumimojiCheck;
18
+ /** A finished game's leaderboard points (`KUMIMOJI_SCORE`): ten a tile, and up to as much again for speed. */
19
+ export declare function kumimojiPoints(givens: string, elapsedMs: number): number;
package/dist/check.js ADDED
@@ -0,0 +1,70 @@
1
+ import { decodeGrid, lettersOf, sameLetters } from "./grid.js";
2
+ import { judgeWithWords } from "./judge.js";
3
+ import { kumimojiTileCount, kumimojiWildCount, KUMIMOJI_SCORE, TILE_MIX_TOTAL } from "./tiles.constants.js";
4
+ import { tileWords } from "./tileWords.js";
5
+ /**
6
+ * WHETHER A GRID FINISHES A KUMIMOJI: the one check the server also runs.
7
+ * O(squares) and a lookup a word, nothing searched: the grid uses exactly the
8
+ * tiles of the bag, as many of each, it is all one piece, and every run of two
9
+ * or more across or down is in the list — and, for a game set up with
10
+ * Diagonals, every run of three or more along a diagonal. Which tiles were
11
+ * traded on the way does not matter — a finished game holds the whole bag
12
+ * whatever came out of it when — so the answer is the grid alone.
13
+ *
14
+ * Written out here rather than asked of the play page, which is what made the
15
+ * grid; the list must have been loaded (`loadTileWords`), and a check that
16
+ * cannot read it refuses.
17
+ */
18
+ export function checkKumimoji(size, givens, answer, options = {}) {
19
+ const gameLength = options.gameLength ?? "short";
20
+ const language = options.language ?? "english";
21
+ const doubleSet = language === "english" && (options.doubleSet ?? false);
22
+ const level = options.level ?? "medium";
23
+ const multiplier = doubleSet ? 2 : 1;
24
+ let words;
25
+ let inBag;
26
+ try {
27
+ words = tileWords(language);
28
+ const setSize = [...words.mix.values()].reduce((sum, count) => sum + count, 0);
29
+ inBag = kumimojiTileCount(size, gameLength, language === "english" ? TILE_MIX_TOTAL : setSize, doubleSet);
30
+ }
31
+ catch {
32
+ return { ok: false, reason: `no Kumimoji with a hand of ${size}` };
33
+ }
34
+ const bag = [...givens];
35
+ if (typeof givens !== "string" || bag.length !== inBag)
36
+ return { ok: false, reason: "the givens are not a bag of tiles" };
37
+ const expectedWilds = kumimojiWildCount(size, level, inBag);
38
+ if (bag.filter(words.isWild).length !== expectedWilds)
39
+ return { ok: false, reason: "the bag has the wrong number of wild tiles" };
40
+ for (const [tile, count] of lettersOf(bag.filter((each) => !words.isWild(each)))) {
41
+ const face = words.inventoryKey(tile);
42
+ const code = face === null ? null : words.codeOf(face);
43
+ if (code === null || count > (words.mix.get(code) ?? 0) * multiplier)
44
+ return { ok: false, reason: "the bag exceeds the tile set" };
45
+ }
46
+ const tiles = decodeGrid(answer);
47
+ if (tiles === null)
48
+ return { ok: false, reason: "the answer is not a grid" };
49
+ const bagIdentities = bag.map(words.familyKey);
50
+ const tileIdentities = [...tiles.values()].map(words.familyKey);
51
+ if (bagIdentities.some((tile) => tile === null) || tileIdentities.some((tile) => tile === null) || !sameLetters(lettersOf(tileIdentities), lettersOf(bagIdentities))) {
52
+ return { ok: false, reason: "the grid does not use exactly the tiles of the bag" };
53
+ }
54
+ const verdict = judgeWithWords(tiles, words, { diagonals: options.diagonals === true });
55
+ if (verdict.apart.size > 0)
56
+ return { ok: false, reason: "the tiles are not all joined" };
57
+ if (verdict.notWords.length > 0)
58
+ return { ok: false, reason: `${verdict.notWords[0]} is not in the word list` };
59
+ if (!verdict.sound)
60
+ return { ok: false, reason: "the grid is not finished" };
61
+ return { ok: true };
62
+ }
63
+ /** A finished game's leaderboard points (`KUMIMOJI_SCORE`): ten a tile, and up to as much again for speed. */
64
+ export function kumimojiPoints(givens, elapsedMs) {
65
+ const tiles = givens.length;
66
+ const base = KUMIMOJI_SCORE.tile * tiles;
67
+ const slowest = KUMIMOJI_SCORE.slowestMsATile * tiles;
68
+ const speed = Math.max(0, Math.round(base * (1 - Math.max(0, elapsedMs) / slowest)));
69
+ return base + speed;
70
+ }
@@ -0,0 +1,23 @@
1
+ import type { PartyGame } from "./party.types.ts";
2
+ /** What a computer did in one step of its turn, for the line over its table. */
3
+ export type ComputerSaid = {
4
+ kind: "rebuilt";
5
+ } | {
6
+ kind: "laid";
7
+ word: string;
8
+ } | {
9
+ kind: "drew";
10
+ } | {
11
+ kind: "traded";
12
+ tile: string;
13
+ } | {
14
+ kind: "done";
15
+ out: boolean;
16
+ } | {
17
+ kind: "resigned";
18
+ };
19
+ /** One step of a computer's turn: the game after it, and what it did. The last step's game is the one kept. */
20
+ export type ComputerStep = {
21
+ game: PartyGame;
22
+ said: ComputerSaid;
23
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import { type GridRules, type GridVerdict, type Tiles } from "./grid.ts";
2
+ import { type TilePlay } from "./play.ts";
3
+ import type { TileWords } from "./tileWords.ts";
4
+ /** Whether a grid is sound, read against the loaded word list by the game's rules: the same judgement the desk shows. */
5
+ export declare function judgeTiles(tiles: Tiles, words: TileWords, rules?: GridRules): GridVerdict;
6
+ /**
7
+ * THE COMPUTER'S NEXT WORD: its play after laying the best word its hand can
8
+ * lay on its table, and the word, or null when there is none. The grid after
9
+ * it is judged against the list, by the game's rules (`rules`), and is
10
+ * always sound.
11
+ */
12
+ export declare function bestLaying(play: TilePlay, words: TileWords, rules?: GridRules): {
13
+ play: TilePlay;
14
+ word: string;
15
+ } | null;
16
+ /** The tile a computer trades: its rarest letter in the set, the first of those in its hand; never a wild. Null for a hand of wilds. */
17
+ export declare function tradeChoice(hand: readonly string[], words: TileWords): number | null;