@johnmorrisdotca/kumimoji 1.2.2 → 1.3.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 (83) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/CONTRIBUTING.md +11 -2
  3. package/README.md +80 -39
  4. package/dist/check.d.ts +26 -1
  5. package/dist/check.js +30 -3
  6. package/dist/computer.types.d.ts +24 -2
  7. package/dist/computerPlay.d.ts +45 -2
  8. package/dist/computerPlay.js +56 -6
  9. package/dist/computerTurn.d.ts +42 -3
  10. package/dist/computerTurn.js +47 -5
  11. package/dist/daily.d.ts +22 -2
  12. package/dist/daily.js +23 -3
  13. package/dist/element-define.d.ts +1 -1
  14. package/dist/element-define.js +3 -13
  15. package/dist/element.d.ts +85 -4
  16. package/dist/element.js +359 -77
  17. package/dist/embed-day.d.ts +4 -0
  18. package/dist/embed-day.js +23 -0
  19. package/dist/export.d.ts +123 -6
  20. package/dist/export.js +109 -9
  21. package/dist/generate.d.ts +14 -0
  22. package/dist/generate.js +32 -5
  23. package/dist/grid.d.ts +181 -10
  24. package/dist/grid.js +148 -11
  25. package/dist/help.d.ts +36 -3
  26. package/dist/help.js +36 -3
  27. package/dist/index.d.ts +22 -22
  28. package/dist/index.js +22 -22
  29. package/dist/judge.d.ts +14 -0
  30. package/dist/judge.js +14 -0
  31. package/dist/kana.d.ts +23 -1
  32. package/dist/kana.js +62 -7
  33. package/dist/kumimoji.types.d.ts +70 -5
  34. package/dist/party.d.ts +268 -14
  35. package/dist/party.js +296 -19
  36. package/dist/party.types.d.ts +50 -2
  37. package/dist/partyKept.d.ts +74 -4
  38. package/dist/partyKept.js +127 -12
  39. package/dist/partySeats.d.ts +92 -6
  40. package/dist/partySeats.js +83 -8
  41. package/dist/partyTurns.d.ts +171 -5
  42. package/dist/partyTurns.js +187 -8
  43. package/dist/placement.d.ts +50 -2
  44. package/dist/placement.js +35 -2
  45. package/dist/play.d.ts +247 -15
  46. package/dist/play.js +251 -18
  47. package/dist/random.d.ts +34 -3
  48. package/dist/random.js +23 -2
  49. package/dist/react.d.ts +42 -4
  50. package/dist/react.js +54 -7
  51. package/dist/showcase.d.ts +83 -7
  52. package/dist/showcase.js +61 -7
  53. package/dist/strings.d.ts +54 -5
  54. package/dist/strings.js +34 -3
  55. package/dist/tableView.d.ts +151 -8
  56. package/dist/tableView.js +129 -6
  57. package/dist/tileFace.d.ts +59 -5
  58. package/dist/tileFace.js +48 -4
  59. package/dist/tileFamily.d.ts +9 -0
  60. package/dist/tileFamily.js +9 -0
  61. package/dist/tileWords.d.ts +93 -6
  62. package/dist/tileWords.js +94 -12
  63. package/dist/tileWordsModule.d.ts +12 -2
  64. package/dist/tileWordsModule.js +14 -2
  65. package/dist/tiles.constants.d.ts +99 -5
  66. package/dist/tiles.constants.js +171 -13
  67. package/dist/turn.d.ts +82 -4
  68. package/dist/turn.js +88 -5
  69. package/dist/ui/board.d.ts +60 -5
  70. package/dist/ui/board.js +44 -4
  71. package/dist/ui/draw.d.ts +62 -0
  72. package/dist/ui/draw.js +229 -0
  73. package/dist/ui/mount.d.ts +41 -2
  74. package/dist/ui/mount.js +190 -265
  75. package/dist/ui/style.d.ts +6 -1
  76. package/dist/ui/style.js +6 -0
  77. package/dist/ui.d.ts +3 -3
  78. package/dist/ui.js +2 -2
  79. package/dist/version.d.ts +9 -2
  80. package/dist/version.js +9 -2
  81. package/dist/wilds.d.ts +22 -1
  82. package/dist/wilds.js +25 -3
  83. package/package.json +68 -61
@@ -7,17 +7,72 @@ import type { TileWords } from "./tileWords.ts";
7
7
  * solo game's (`play.ts`), and the deal and the draw are `party.ts`'s. Pure:
8
8
  * each returns a new game, or the one it was given where the move is refused.
9
9
  */
10
- /** Whether the game is over. */
10
+ /** Whether the game is over.
11
+ *
12
+ * @param game - The current immutable shared-bag party game.
13
+ * @returns Whether the game is over.
14
+ * @example
15
+ * ```ts
16
+ * import { isOver, startParty } from "@johnmorrisdotca/kumimoji";
17
+ *
18
+ * const settings = {size:3,level:"hard",seed:44,gameLength:"short",language:"english",doubleSet:false,diagonals:false,hints:false} as const;
19
+ * const bag = "catdogsun";
20
+ * const game = startParty(settings,bag,["Seat 1","Seat 2"]);
21
+ * console.log(isOver(game));
22
+ * ```
23
+ */
11
24
  export declare function isOver(game: PartyGame): boolean;
12
25
  /**
13
26
  * Whether the player whose turn it is would go out with Done: their hand
14
27
  * used, their grid sound, and too few tiles in the bag for everybody still in
15
28
  * to draw.
29
+ *
30
+ * @param game - The current immutable shared-bag party game.
31
+ * @param verdict - The native word and connectivity verdict for this grid.
32
+ * @returns Whether the player whose turn it is would go out with Done: their hand used, their grid sound, and too few tiles in the bag for everybody still in to draw.
33
+ * @example
34
+ * ```ts
35
+ * import { loadTileWordsFromModule } from "@johnmorrisdotca/kumimoji/words";
36
+ * import { goesOut, judgeWithWords, startParty } from "@johnmorrisdotca/kumimoji";
37
+ *
38
+ * const words = await loadTileWordsFromModule("english");
39
+ * const verdict = judgeWithWords(new Map([["0,0","c"],["0,1","a"],["0,2","t"]]),words);
40
+ * const settings = {size:3,level:"hard",seed:44,gameLength:"short",language:"english",doubleSet:false,diagonals:false,hints:false} as const;
41
+ * const bag = "catdogsun";
42
+ * const game = startParty(settings,bag,["Seat 1","Seat 2"]);
43
+ * console.log(goesOut(game,verdict));
44
+ * ```
16
45
  */
17
46
  export declare function goesOut(game: PartyGame, verdict: GridVerdict): boolean;
18
- /** Whether the player whose turn it is has laid a tile on their table this turn: more tiles there than when it began. */
47
+ /** Whether the player whose turn it is has laid a tile on their table this turn: more tiles there than when it began.
48
+ *
49
+ * @param game - The current immutable shared-bag party game.
50
+ * @returns Whether the player whose turn it is has laid a tile on their table this turn: more tiles there than when it began.
51
+ * @example
52
+ * ```ts
53
+ * import { laidThisTurn, startParty } from "@johnmorrisdotca/kumimoji";
54
+ *
55
+ * const settings = {size:3,level:"hard",seed:44,gameLength:"short",language:"english",doubleSet:false,diagonals:false,hints:false} as const;
56
+ * const bag = "catdogsun";
57
+ * const game = startParty(settings,bag,["Seat 1","Seat 2"]);
58
+ * console.log(laidThisTurn(game));
59
+ * ```
60
+ */
19
61
  export declare function laidThisTurn(game: PartyGame): boolean;
20
- /** Whether the player whose turn it is is the only one still in: everybody else has resigned. */
62
+ /** Whether the player whose turn it is is the only one still in: everybody else has resigned.
63
+ *
64
+ * @param game - The current immutable shared-bag party game.
65
+ * @returns Whether the player whose turn it is is the only one still in: everybody else has resigned.
66
+ * @example
67
+ * ```ts
68
+ * import { lastStanding, startParty } from "@johnmorrisdotca/kumimoji";
69
+ *
70
+ * const settings = {size:3,level:"hard",seed:44,gameLength:"short",language:"english",doubleSet:false,diagonals:false,hints:false} as const;
71
+ * const bag = "catdogsun";
72
+ * const game = startParty(settings,bag,["Seat 1","Seat 2"]);
73
+ * console.log(lastStanding(game));
74
+ * ```
75
+ */
21
76
  export declare function lastStanding(game: PartyGame): boolean;
22
77
  /**
23
78
  * A HAND THAT SPELLS NOTHING MUST BE TRADED. John, 2026-09-28: "If no words
@@ -26,14 +81,59 @@ export declare function lastStanding(game: PartyGame): boolean;
26
81
  * spell no word, has laid nothing on (or lifted nothing from) their table this
27
82
  * turn, and has not traded — unless the bag cannot give three, when there is
28
83
  * nothing to trade for. `handSpells` answers for a hand (`handCanSpell`).
84
+ *
85
+ * @param game - The current immutable shared-bag party game.
86
+ * @param handSpells - Whether a hand can form any accepted word.
87
+ * @returns A HAND THAT SPELLS NOTHING MUST BE TRADED. John, 2026-09-28: "If no words can be formed which is possible, they must dump a tile to get 3 more. Easy rule." So Done waits while the player whose turn it is holds tiles that spell no word, has laid nothing on (or lifted nothing from) their table this turn, and has not traded — unless the bag cannot give three, when there is nothing to trade for. `handSpells` answers for a hand (`handCanSpell`).
88
+ * @example
89
+ * ```ts
90
+ * import { loadTileWordsFromModule } from "@johnmorrisdotca/kumimoji/words";
91
+ * import { handCanSpell, mustTradeFirst, startParty } from "@johnmorrisdotca/kumimoji";
92
+ *
93
+ * const words = await loadTileWordsFromModule("english");
94
+ * const settings = {size:3,level:"hard",seed:44,gameLength:"short",language:"english",doubleSet:false,diagonals:false,hints:false} as const;
95
+ * const bag = "catdogsun";
96
+ * const game = startParty(settings,bag,["Seat 1","Seat 2"]);
97
+ * console.log(mustTradeFirst(game,hand=>handCanSpell(hand,words)));
98
+ * ```
29
99
  */
30
100
  export declare function mustTradeFirst(game: PartyGame, handSpells: (hand: readonly string[]) => boolean): boolean;
31
- /** Whether a hand can spell a word from the list: any word of two tiles or more, a wild counting as able to be anything. */
101
+ /** Whether a hand can spell a word from the list: any word of two tiles or more, a wild counting as able to be anything.
102
+ *
103
+ * @param hand - The hand size or tile codes, as declared by the function signature.
104
+ * @param words - The loaded native language word list and tile mappings.
105
+ * @returns Whether a hand can spell a word from the list: any word of two tiles or more, a wild counting as able to be anything.
106
+ * @example
107
+ * ```ts
108
+ * import { loadTileWordsFromModule } from "@johnmorrisdotca/kumimoji/words";
109
+ * import { handCanSpell } from "@johnmorrisdotca/kumimoji";
110
+ *
111
+ * const words = await loadTileWordsFromModule("english");
112
+ * console.log(handCanSpell(["c","a","t"],words));
113
+ * ```
114
+ */
32
115
  export declare function handCanSpell(hand: readonly string[], words: TileWords): boolean;
33
116
  /**
34
117
  * Why Done cannot be pressed now, or null when it can: a hand to trade first,
35
118
  * or, for the last one standing, no tile laid on a sound grid yet (a hand
36
119
  * already used on a sound grid has nothing left to lay, and wins as it is).
120
+ *
121
+ * @param game - The current immutable shared-bag party game.
122
+ * @param verdict - The native word and connectivity verdict for this grid.
123
+ * @param handSpells - Whether a hand can form any accepted word.
124
+ * @returns Why Done cannot be pressed now, or null when it can: a hand to trade first, or, for the last one standing, no tile laid on a sound grid yet (a hand already used on a sound grid has nothing left to lay, and wins as it is).
125
+ * @example
126
+ * ```ts
127
+ * import { loadTileWordsFromModule } from "@johnmorrisdotca/kumimoji/words";
128
+ * import { doneRefused, handCanSpell, judgeWithWords, seatPlay, startParty } from "@johnmorrisdotca/kumimoji";
129
+ *
130
+ * const words = await loadTileWordsFromModule("english");
131
+ * const tiles = new Map([["0,0","c"],["0,1","a"],["0,2","t"]]);
132
+ * const settings = {size:3,level:"hard",seed:44,gameLength:"short",language:"english",doubleSet:false,diagonals:false,hints:false} as const;
133
+ * const bag = "catdogsun";
134
+ * const game = startParty(settings,bag,["Seat 1","Seat 2"]);
135
+ * console.log(doneRefused(game,judgeWithWords(seatPlay(game).tiles,words),hand=>handCanSpell(hand,words)));
136
+ * ```
37
137
  */
38
138
  export declare function doneRefused(game: PartyGame, verdict: GridVerdict, handSpells: (hand: readonly string[]) => boolean): "trade" | "standing" | null;
39
139
  /**
@@ -46,6 +146,23 @@ export declare function doneRefused(game: PartyGame, verdict: GridVerdict, handS
46
146
  * The last one standing, everybody else resigned, wins by laying a tile on a
47
147
  * sound grid and pressing Done. `verdict` is the grid of the player whose turn
48
148
  * it is. Refused, the game given back as it was, while `doneRefused` says so.
149
+ *
150
+ * @param game - The current immutable shared-bag party game.
151
+ * @param verdict - The native word and connectivity verdict for this grid.
152
+ * @param handSpells - Whether a hand can form any accepted word.
153
+ * @returns DONE: the end of a turn, and the device passed on. Before anybody is out, to the next player still in. A player who goes out with it starts the last round: everybody else still in, in order from the next, has one last turn, and any of them who goes out on it shares the win (John, 2026-09-28: "Sharing the win is correct"). After the last of those, the game is over.
154
+ * @example
155
+ * ```ts
156
+ * import { loadTileWordsFromModule } from "@johnmorrisdotca/kumimoji/words";
157
+ * import { endTurn, handCanSpell, judgeWithWords, seatPlay, startParty } from "@johnmorrisdotca/kumimoji";
158
+ *
159
+ * const words = await loadTileWordsFromModule("english");
160
+ * const tiles = new Map([["0,0","c"],["0,1","a"],["0,2","t"]]);
161
+ * const settings = {size:3,level:"hard",seed:44,gameLength:"short",language:"english",doubleSet:false,diagonals:false,hints:false} as const;
162
+ * const bag = "catdogsun";
163
+ * const game = startParty(settings,bag,["Seat 1","Seat 2"]);
164
+ * console.log(endTurn(game,judgeWithWords(seatPlay(game).tiles,words),hand=>handCanSpell(hand,words)).turn);
165
+ * ```
49
166
  */
50
167
  export declare function endTurn(game: PartyGame, verdict: GridVerdict, handSpells: (hand: readonly string[]) => boolean): PartyGame;
51
168
  /**
@@ -53,6 +170,18 @@ export declare function endTurn(game: PartyGame, verdict: GridVerdict, handSpell
53
170
  * give a trade, so nothing more can be got from it. John, 2026-09-28:
54
171
  * "Perhaps we offer a resign button only when there are no remaining tiles
55
172
  * they can get from the bag."
173
+ *
174
+ * @param game - The current immutable shared-bag party game.
175
+ * @returns Whether the player whose turn it is may resign: only once the bag cannot give a trade, so nothing more can be got from it. John, 2026-09-28: "Perhaps we offer a resign button only when there are no remaining tiles they can get from the bag."
176
+ * @example
177
+ * ```ts
178
+ * import { mayResign, startParty } from "@johnmorrisdotca/kumimoji";
179
+ *
180
+ * const settings = {size:3,level:"hard",seed:44,gameLength:"short",language:"english",doubleSet:false,diagonals:false,hints:false} as const;
181
+ * const bag = "catdogsun";
182
+ * const game = startParty(settings,bag,["Seat 1","Seat 2"]);
183
+ * console.log(mayResign(game));
184
+ * ```
56
185
  */
57
186
  export declare function mayResign(game: PartyGame): boolean;
58
187
  /**
@@ -63,13 +192,50 @@ export declare function mayResign(game: PartyGame): boolean;
63
192
  * resigned too — it ends tied between everybody who resigned since a player
64
193
  * still in last pressed Done. John: "if you resign, as the turns pass, if
65
194
  * someone else can't go and has to resign then they are tied."
195
+ *
196
+ * @param game - The current immutable shared-bag party game.
197
+ * @returns RESIGN: the player whose turn it is is out of the game, their table kept for the finish, and their turn passed over from then on. In the last round it is their last turn ended without going out. Otherwise the game goes on with whoever is still in; and when nobody is — the last one standing resigned too — it ends tied between everybody who resigned since a player still in last pressed Done. John: "if you resign, as the turns pass, if someone else can't go and has to resign then they are tied."
198
+ * @example
199
+ * ```ts
200
+ * import { resign, startParty } from "@johnmorrisdotca/kumimoji";
201
+ *
202
+ * const settings = {size:3,level:"hard",seed:44,gameLength:"short",language:"english",doubleSet:false,diagonals:false,hints:false} as const;
203
+ * const bag = "catdogsun";
204
+ * const game = startParty(settings,bag,["Seat 1","Seat 2"]);
205
+ * console.log(resign(game));
206
+ * ```
66
207
  */
67
208
  export declare function resign(game: PartyGame): PartyGame;
68
209
  /**
69
210
  * The winners of a finished game, by place: everybody who went out; the last
70
211
  * one standing; or, tied, the final run of resignations. Empty while it is
71
212
  * played.
213
+ *
214
+ * @param game - The current immutable shared-bag party game.
215
+ * @returns The winners of a finished game, by place: everybody who went out; the last one standing; or, tied, the final run of resignations. Empty while it is played.
216
+ * @example
217
+ * ```ts
218
+ * import { startParty, winnersOf } from "@johnmorrisdotca/kumimoji";
219
+ *
220
+ * const settings = {size:3,level:"hard",seed:44,gameLength:"short",language:"english",doubleSet:false,diagonals:false,hints:false} as const;
221
+ * const bag = "catdogsun";
222
+ * const game = startParty(settings,bag,["Seat 1","Seat 2"]);
223
+ * console.log(winnersOf(game));
224
+ * ```
72
225
  */
73
226
  export declare function winnersOf(game: PartyGame): readonly number[];
74
- /** Whether the turn now being played is a player's last. */
227
+ /** Whether the turn now being played is a player's last.
228
+ *
229
+ * @param game - The current immutable shared-bag party game.
230
+ * @returns Whether the turn now being played is a player's last.
231
+ * @example
232
+ * ```ts
233
+ * import { isLastTurn, startParty } from "@johnmorrisdotca/kumimoji";
234
+ *
235
+ * const settings = {size:3,level:"hard",seed:44,gameLength:"short",language:"english",doubleSet:false,diagonals:false,hints:false} as const;
236
+ * const bag = "catdogsun";
237
+ * const game = startParty(settings,bag,["Seat 1","Seat 2"]);
238
+ * console.log(isLastTurn(game));
239
+ * ```
240
+ */
75
241
  export declare function isLastTurn(game: PartyGame): boolean;
@@ -8,7 +8,20 @@ import { KUMIMOJI_TRADE } from "./tiles.constants.js";
8
8
  * solo game's (`play.ts`), and the deal and the draw are `party.ts`'s. Pure:
9
9
  * each returns a new game, or the one it was given where the move is refused.
10
10
  */
11
- /** Whether the game is over. */
11
+ /** Whether the game is over.
12
+ *
13
+ * @param game - The current immutable shared-bag party game.
14
+ * @returns Whether the game is over.
15
+ * @example
16
+ * ```ts
17
+ * import { isOver, startParty } from "@johnmorrisdotca/kumimoji";
18
+ *
19
+ * const settings = {size:3,level:"hard",seed:44,gameLength:"short",language:"english",doubleSet:false,diagonals:false,hints:false} as const;
20
+ * const bag = "catdogsun";
21
+ * const game = startParty(settings,bag,["Seat 1","Seat 2"]);
22
+ * console.log(isOver(game));
23
+ * ```
24
+ */
12
25
  export function isOver(game) {
13
26
  return game.ending !== null;
14
27
  }
@@ -16,15 +29,60 @@ export function isOver(game) {
16
29
  * Whether the player whose turn it is would go out with Done: their hand
17
30
  * used, their grid sound, and too few tiles in the bag for everybody still in
18
31
  * to draw.
32
+ *
33
+ * @param game - The current immutable shared-bag party game.
34
+ * @param verdict - The native word and connectivity verdict for this grid.
35
+ * @returns Whether the player whose turn it is would go out with Done: their hand used, their grid sound, and too few tiles in the bag for everybody still in to draw.
36
+ * @example
37
+ * ```ts
38
+ * import { loadTileWordsFromModule } from "@johnmorrisdotca/kumimoji/words";
39
+ * import { goesOut, judgeWithWords, startParty } from "@johnmorrisdotca/kumimoji";
40
+ *
41
+ * const words = await loadTileWordsFromModule("english");
42
+ * const verdict = judgeWithWords(new Map([["0,0","c"],["0,1","a"],["0,2","t"]]),words);
43
+ * const settings = {size:3,level:"hard",seed:44,gameLength:"short",language:"english",doubleSet:false,diagonals:false,hints:false} as const;
44
+ * const bag = "catdogsun";
45
+ * const game = startParty(settings,bag,["Seat 1","Seat 2"]);
46
+ * console.log(goesOut(game,verdict));
47
+ * ```
19
48
  */
20
49
  export function goesOut(game, verdict) {
21
- return !isOver(game) && game.players[game.turn].hand.length === 0 && verdict.sound && partyTilesLeft(game) < stillIn(game).length;
50
+ return (!isOver(game) &&
51
+ game.players[game.turn].hand.length === 0 &&
52
+ verdict.sound &&
53
+ partyTilesLeft(game) < stillIn(game).length);
22
54
  }
23
- /** Whether the player whose turn it is has laid a tile on their table this turn: more tiles there than when it began. */
55
+ /** Whether the player whose turn it is has laid a tile on their table this turn: more tiles there than when it began.
56
+ *
57
+ * @param game - The current immutable shared-bag party game.
58
+ * @returns Whether the player whose turn it is has laid a tile on their table this turn: more tiles there than when it began.
59
+ * @example
60
+ * ```ts
61
+ * import { laidThisTurn, startParty } from "@johnmorrisdotca/kumimoji";
62
+ *
63
+ * const settings = {size:3,level:"hard",seed:44,gameLength:"short",language:"english",doubleSet:false,diagonals:false,hints:false} as const;
64
+ * const bag = "catdogsun";
65
+ * const game = startParty(settings,bag,["Seat 1","Seat 2"]);
66
+ * console.log(laidThisTurn(game));
67
+ * ```
68
+ */
24
69
  export function laidThisTurn(game) {
25
70
  return game.players[game.turn].tiles.size > [...game.startTable].length;
26
71
  }
27
- /** Whether the player whose turn it is is the only one still in: everybody else has resigned. */
72
+ /** Whether the player whose turn it is is the only one still in: everybody else has resigned.
73
+ *
74
+ * @param game - The current immutable shared-bag party game.
75
+ * @returns Whether the player whose turn it is is the only one still in: everybody else has resigned.
76
+ * @example
77
+ * ```ts
78
+ * import { lastStanding, startParty } from "@johnmorrisdotca/kumimoji";
79
+ *
80
+ * const settings = {size:3,level:"hard",seed:44,gameLength:"short",language:"english",doubleSet:false,diagonals:false,hints:false} as const;
81
+ * const bag = "catdogsun";
82
+ * const game = startParty(settings,bag,["Seat 1","Seat 2"]);
83
+ * console.log(lastStanding(game));
84
+ * ```
85
+ */
28
86
  export function lastStanding(game) {
29
87
  return !isOver(game) && game.lastTurns === null && stillIn(game).length === 1;
30
88
  }
@@ -35,6 +93,21 @@ export function lastStanding(game) {
35
93
  * spell no word, has laid nothing on (or lifted nothing from) their table this
36
94
  * turn, and has not traded — unless the bag cannot give three, when there is
37
95
  * nothing to trade for. `handSpells` answers for a hand (`handCanSpell`).
96
+ *
97
+ * @param game - The current immutable shared-bag party game.
98
+ * @param handSpells - Whether a hand can form any accepted word.
99
+ * @returns A HAND THAT SPELLS NOTHING MUST BE TRADED. John, 2026-09-28: "If no words can be formed which is possible, they must dump a tile to get 3 more. Easy rule." So Done waits while the player whose turn it is holds tiles that spell no word, has laid nothing on (or lifted nothing from) their table this turn, and has not traded — unless the bag cannot give three, when there is nothing to trade for. `handSpells` answers for a hand (`handCanSpell`).
100
+ * @example
101
+ * ```ts
102
+ * import { loadTileWordsFromModule } from "@johnmorrisdotca/kumimoji/words";
103
+ * import { handCanSpell, mustTradeFirst, startParty } from "@johnmorrisdotca/kumimoji";
104
+ *
105
+ * const words = await loadTileWordsFromModule("english");
106
+ * const settings = {size:3,level:"hard",seed:44,gameLength:"short",language:"english",doubleSet:false,diagonals:false,hints:false} as const;
107
+ * const bag = "catdogsun";
108
+ * const game = startParty(settings,bag,["Seat 1","Seat 2"]);
109
+ * console.log(mustTradeFirst(game,hand=>handCanSpell(hand,words)));
110
+ * ```
38
111
  */
39
112
  export function mustTradeFirst(game, handSpells) {
40
113
  if (isOver(game) || game.traded)
@@ -44,7 +117,20 @@ export function mustTradeFirst(game, handSpells) {
44
117
  return false;
45
118
  return !handSpells(play.hand);
46
119
  }
47
- /** Whether a hand can spell a word from the list: any word of two tiles or more, a wild counting as able to be anything. */
120
+ /** Whether a hand can spell a word from the list: any word of two tiles or more, a wild counting as able to be anything.
121
+ *
122
+ * @param hand - The hand size or tile codes, as declared by the function signature.
123
+ * @param words - The loaded native language word list and tile mappings.
124
+ * @returns Whether a hand can spell a word from the list: any word of two tiles or more, a wild counting as able to be anything.
125
+ * @example
126
+ * ```ts
127
+ * import { loadTileWordsFromModule } from "@johnmorrisdotca/kumimoji/words";
128
+ * import { handCanSpell } from "@johnmorrisdotca/kumimoji";
129
+ *
130
+ * const words = await loadTileWordsFromModule("english");
131
+ * console.log(handCanSpell(["c","a","t"],words));
132
+ * ```
133
+ */
48
134
  export function handCanSpell(hand, words) {
49
135
  return hand.some(words.isWild) || wordsInHand(hand, words).length > 0;
50
136
  }
@@ -52,6 +138,23 @@ export function handCanSpell(hand, words) {
52
138
  * Why Done cannot be pressed now, or null when it can: a hand to trade first,
53
139
  * or, for the last one standing, no tile laid on a sound grid yet (a hand
54
140
  * already used on a sound grid has nothing left to lay, and wins as it is).
141
+ *
142
+ * @param game - The current immutable shared-bag party game.
143
+ * @param verdict - The native word and connectivity verdict for this grid.
144
+ * @param handSpells - Whether a hand can form any accepted word.
145
+ * @returns Why Done cannot be pressed now, or null when it can: a hand to trade first, or, for the last one standing, no tile laid on a sound grid yet (a hand already used on a sound grid has nothing left to lay, and wins as it is).
146
+ * @example
147
+ * ```ts
148
+ * import { loadTileWordsFromModule } from "@johnmorrisdotca/kumimoji/words";
149
+ * import { doneRefused, handCanSpell, judgeWithWords, seatPlay, startParty } from "@johnmorrisdotca/kumimoji";
150
+ *
151
+ * const words = await loadTileWordsFromModule("english");
152
+ * const tiles = new Map([["0,0","c"],["0,1","a"],["0,2","t"]]);
153
+ * const settings = {size:3,level:"hard",seed:44,gameLength:"short",language:"english",doubleSet:false,diagonals:false,hints:false} as const;
154
+ * const bag = "catdogsun";
155
+ * const game = startParty(settings,bag,["Seat 1","Seat 2"]);
156
+ * console.log(doneRefused(game,judgeWithWords(seatPlay(game).tiles,words),hand=>handCanSpell(hand,words)));
157
+ * ```
55
158
  */
56
159
  export function doneRefused(game, verdict, handSpells) {
57
160
  if (mustTradeFirst(game, handSpells))
@@ -63,7 +166,15 @@ export function doneRefused(game, verdict, handSpells) {
63
166
  }
64
167
  /** The game passed to `turn`, a new turn with nothing traded and its table as it stands. */
65
168
  function passTo(game, turn, rest) {
66
- return { ...game, ...rest, turns: game.turns + 1, turn, traded: false, startTable: tableTally(game.players[turn].tiles), passedBy: game.turn };
169
+ return {
170
+ ...game,
171
+ ...rest,
172
+ turns: game.turns + 1,
173
+ turn,
174
+ traded: false,
175
+ startTable: tableTally(game.players[turn].tiles),
176
+ passedBy: game.turn,
177
+ };
67
178
  }
68
179
  /**
69
180
  * DONE: the end of a turn, and the device passed on. Before anybody is out,
@@ -75,6 +186,23 @@ function passTo(game, turn, rest) {
75
186
  * The last one standing, everybody else resigned, wins by laying a tile on a
76
187
  * sound grid and pressing Done. `verdict` is the grid of the player whose turn
77
188
  * it is. Refused, the game given back as it was, while `doneRefused` says so.
189
+ *
190
+ * @param game - The current immutable shared-bag party game.
191
+ * @param verdict - The native word and connectivity verdict for this grid.
192
+ * @param handSpells - Whether a hand can form any accepted word.
193
+ * @returns DONE: the end of a turn, and the device passed on. Before anybody is out, to the next player still in. A player who goes out with it starts the last round: everybody else still in, in order from the next, has one last turn, and any of them who goes out on it shares the win (John, 2026-09-28: "Sharing the win is correct"). After the last of those, the game is over.
194
+ * @example
195
+ * ```ts
196
+ * import { loadTileWordsFromModule } from "@johnmorrisdotca/kumimoji/words";
197
+ * import { endTurn, handCanSpell, judgeWithWords, seatPlay, startParty } from "@johnmorrisdotca/kumimoji";
198
+ *
199
+ * const words = await loadTileWordsFromModule("english");
200
+ * const tiles = new Map([["0,0","c"],["0,1","a"],["0,2","t"]]);
201
+ * const settings = {size:3,level:"hard",seed:44,gameLength:"short",language:"english",doubleSet:false,diagonals:false,hints:false} as const;
202
+ * const bag = "catdogsun";
203
+ * const game = startParty(settings,bag,["Seat 1","Seat 2"]);
204
+ * console.log(endTurn(game,judgeWithWords(seatPlay(game).tiles,words),hand=>handCanSpell(hand,words)).turn);
205
+ * ```
78
206
  */
79
207
  export function endTurn(game, verdict, handSpells) {
80
208
  if (isOver(game) || doneRefused(game, verdict, handSpells) !== null)
@@ -85,7 +213,9 @@ export function endTurn(game, verdict, handSpells) {
85
213
  if (game.lastTurns === null) {
86
214
  if (out.length === 0)
87
215
  return passTo(game, nextIn(game, game.turn), { resignRun: [] });
88
- const lastTurns = stillIn(game).filter((at) => at !== game.turn).sort((a, b) => fromTurn(game, a) - fromTurn(game, b));
216
+ const lastTurns = stillIn(game)
217
+ .filter((at) => at !== game.turn)
218
+ .sort((a, b) => fromTurn(game, a) - fromTurn(game, b));
89
219
  if (lastTurns.length === 0)
90
220
  return passTo(game, game.turn, { out, lastTurns, ending: "out" });
91
221
  return passTo(game, lastTurns[0], { out, lastTurns, resignRun: [] });
@@ -108,6 +238,18 @@ function nextLastTurn(game, out, rest = {}) {
108
238
  * give a trade, so nothing more can be got from it. John, 2026-09-28:
109
239
  * "Perhaps we offer a resign button only when there are no remaining tiles
110
240
  * they can get from the bag."
241
+ *
242
+ * @param game - The current immutable shared-bag party game.
243
+ * @returns Whether the player whose turn it is may resign: only once the bag cannot give a trade, so nothing more can be got from it. John, 2026-09-28: "Perhaps we offer a resign button only when there are no remaining tiles they can get from the bag."
244
+ * @example
245
+ * ```ts
246
+ * import { mayResign, startParty } from "@johnmorrisdotca/kumimoji";
247
+ *
248
+ * const settings = {size:3,level:"hard",seed:44,gameLength:"short",language:"english",doubleSet:false,diagonals:false,hints:false} as const;
249
+ * const bag = "catdogsun";
250
+ * const game = startParty(settings,bag,["Seat 1","Seat 2"]);
251
+ * console.log(mayResign(game));
252
+ * ```
111
253
  */
112
254
  export function mayResign(game) {
113
255
  return !isOver(game) && partyTilesLeft(game) < KUMIMOJI_TRADE.take;
@@ -120,6 +262,18 @@ export function mayResign(game) {
120
262
  * resigned too — it ends tied between everybody who resigned since a player
121
263
  * still in last pressed Done. John: "if you resign, as the turns pass, if
122
264
  * someone else can't go and has to resign then they are tied."
265
+ *
266
+ * @param game - The current immutable shared-bag party game.
267
+ * @returns RESIGN: the player whose turn it is is out of the game, their table kept for the finish, and their turn passed over from then on. In the last round it is their last turn ended without going out. Otherwise the game goes on with whoever is still in; and when nobody is — the last one standing resigned too — it ends tied between everybody who resigned since a player still in last pressed Done. John: "if you resign, as the turns pass, if someone else can't go and has to resign then they are tied."
268
+ * @example
269
+ * ```ts
270
+ * import { resign, startParty } from "@johnmorrisdotca/kumimoji";
271
+ *
272
+ * const settings = {size:3,level:"hard",seed:44,gameLength:"short",language:"english",doubleSet:false,diagonals:false,hints:false} as const;
273
+ * const bag = "catdogsun";
274
+ * const game = startParty(settings,bag,["Seat 1","Seat 2"]);
275
+ * console.log(resign(game));
276
+ * ```
123
277
  */
124
278
  export function resign(game) {
125
279
  if (!mayResign(game))
@@ -137,6 +291,18 @@ export function resign(game) {
137
291
  * The winners of a finished game, by place: everybody who went out; the last
138
292
  * one standing; or, tied, the final run of resignations. Empty while it is
139
293
  * played.
294
+ *
295
+ * @param game - The current immutable shared-bag party game.
296
+ * @returns The winners of a finished game, by place: everybody who went out; the last one standing; or, tied, the final run of resignations. Empty while it is played.
297
+ * @example
298
+ * ```ts
299
+ * import { startParty, winnersOf } from "@johnmorrisdotca/kumimoji";
300
+ *
301
+ * const settings = {size:3,level:"hard",seed:44,gameLength:"short",language:"english",doubleSet:false,diagonals:false,hints:false} as const;
302
+ * const bag = "catdogsun";
303
+ * const game = startParty(settings,bag,["Seat 1","Seat 2"]);
304
+ * console.log(winnersOf(game));
305
+ * ```
140
306
  */
141
307
  export function winnersOf(game) {
142
308
  if (game.ending === "out")
@@ -147,7 +313,20 @@ export function winnersOf(game) {
147
313
  return game.resignRun;
148
314
  return [];
149
315
  }
150
- /** Whether the turn now being played is a player's last. */
316
+ /** Whether the turn now being played is a player's last.
317
+ *
318
+ * @param game - The current immutable shared-bag party game.
319
+ * @returns Whether the turn now being played is a player's last.
320
+ * @example
321
+ * ```ts
322
+ * import { isLastTurn, startParty } from "@johnmorrisdotca/kumimoji";
323
+ *
324
+ * const settings = {size:3,level:"hard",seed:44,gameLength:"short",language:"english",doubleSet:false,diagonals:false,hints:false} as const;
325
+ * const bag = "catdogsun";
326
+ * const game = startParty(settings,bag,["Seat 1","Seat 2"]);
327
+ * console.log(isLastTurn(game));
328
+ * ```
329
+ */
151
330
  export function isLastTurn(game) {
152
331
  return !isOver(game) && game.lastTurns !== null;
153
332
  }
@@ -1,9 +1,27 @@
1
- /** A square by its row and column. */
1
+ /** A square by its row and column.
2
+ *
3
+ * @example
4
+ * ```ts
5
+ * import type { Square } from "@johnmorrisdotca/kumimoji";
6
+ *
7
+ * const value: Square = {row:0,col:0};
8
+ * console.log(value);
9
+ * ```
10
+ */
2
11
  export type Square = {
3
12
  row: number;
4
13
  col: number;
5
14
  };
6
- /** A word's place: its first square, and the squares it lays a new tile on, in the word's order. */
15
+ /** A word's place: its first square, and the squares it lays a new tile on, in the word's order.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import type { CrossingFit } from "@johnmorrisdotca/kumimoji";
20
+ *
21
+ * const value: CrossingFit = {first:{row:0,col:0},fresh:[{row:0,col:1},{row:0,col:2}]};
22
+ * console.log(value);
23
+ * ```
24
+ */
7
25
  export type CrossingFit = {
8
26
  first: Square;
9
27
  fresh: Square[];
@@ -15,6 +33,21 @@ export type CrossingFit = {
15
33
  * neighbour at its side, or no new tile at all — and, given `diagonalWord`
16
34
  * (a game with Diagonals), a new tile standing in a diagonal run of three or
17
35
  * more that `diagonalWord` does not take (`diagonalsRead`).
36
+ *
37
+ * @param letterAt - The tile at a coordinate, or an empty string for no tile.
38
+ * @param inside - Whether a coordinate is within the caller’s working grid.
39
+ * @param word - The encoded word to lay or spell.
40
+ * @param anchor - The existing tile that the word crosses.
41
+ * @param at - The word offset, seat index or date specified by this signature.
42
+ * @param across - Whether to lay or type horizontally rather than vertically.
43
+ * @param diagonalWord - Optional diagonal-word predicate; null disables diagonal checking.
44
+ * @returns Where `word` stands with its letter `at` on the tile at `anchor`, running across or down, or null where it cannot: a square outside what may be used, a tile at either end, a different letter in its way, a new tile with a neighbour at its side, or no new tile at all — and, given `diagonalWord` (a game with Diagonals), a new tile standing in a diagonal run of three or more that `diagonalWord` does not take (`diagonalsRead`).
45
+ * @example
46
+ * ```ts
47
+ * import { crossingFit } from "@johnmorrisdotca/kumimoji";
48
+ *
49
+ * console.log(crossingFit((r,c)=>r===0&&c===0?"c":"",()=>true,"cat",{row:0,col:0},0,true));
50
+ * ```
18
51
  */
19
52
  export declare function crossingFit(letterAt: (row: number, col: number) => string, inside: (row: number, col: number) => boolean, word: string, anchor: Square, at: number, across: boolean, diagonalWord?: ((run: string) => boolean) | null): CrossingFit | null;
20
53
  /**
@@ -23,5 +56,20 @@ export declare function crossingFit(letterAt: (row: number, col: number) => stri
23
56
  * its new squares. Only a run through a new tile can change, and two new
24
57
  * tiles of one word never share a diagonal (they share a row or a column), so
25
58
  * each is read on its own. A run of two, a corner touch, is never read.
59
+ *
60
+ * @param letterAt - The tile at a coordinate, or an empty string for no tile.
61
+ * @param inside - Whether a coordinate is within the caller’s working grid.
62
+ * @param word - The encoded word to lay or spell.
63
+ * @param fit - The first coordinate and fresh tile coordinates of the proposed laying.
64
+ * @param isWord - The predicate that accepts a readable word.
65
+ * @returns Whether every diagonal run a laying's new tiles would stand in is taken by `isWord`: each walked from its top end down, with the word's own letters on its new squares. Only a run through a new tile can change, and two new tiles of one word never share a diagonal (they share a row or a column), so each is read on its own. A run of two, a corner touch, is never read.
66
+ * @example
67
+ * ```ts
68
+ * import { loadTileWordsFromModule } from "@johnmorrisdotca/kumimoji/words";
69
+ * import { diagonalsRead } from "@johnmorrisdotca/kumimoji";
70
+ *
71
+ * const words = await loadTileWordsFromModule("english");
72
+ * console.log(diagonalsRead(()=>"",()=>true,"cat",{first:{row:0,col:0},fresh:[{row:0,col:0},{row:0,col:1},{row:0,col:2}]},word=>words.allowed.has(word)));
73
+ * ```
26
74
  */
27
75
  export declare function diagonalsRead(letterAt: (row: number, col: number) => string, inside: (row: number, col: number) => boolean, word: string, fit: CrossingFit, isWord: (run: string) => boolean): boolean;
package/dist/placement.js CHANGED
@@ -28,6 +28,21 @@ import { DIAGONAL_RUN_LEAST } from "./grid.js";
28
28
  * neighbour at its side, or no new tile at all — and, given `diagonalWord`
29
29
  * (a game with Diagonals), a new tile standing in a diagonal run of three or
30
30
  * more that `diagonalWord` does not take (`diagonalsRead`).
31
+ *
32
+ * @param letterAt - The tile at a coordinate, or an empty string for no tile.
33
+ * @param inside - Whether a coordinate is within the caller’s working grid.
34
+ * @param word - The encoded word to lay or spell.
35
+ * @param anchor - The existing tile that the word crosses.
36
+ * @param at - The word offset, seat index or date specified by this signature.
37
+ * @param across - Whether to lay or type horizontally rather than vertically.
38
+ * @param diagonalWord - Optional diagonal-word predicate; null disables diagonal checking.
39
+ * @returns Where `word` stands with its letter `at` on the tile at `anchor`, running across or down, or null where it cannot: a square outside what may be used, a tile at either end, a different letter in its way, a new tile with a neighbour at its side, or no new tile at all — and, given `diagonalWord` (a game with Diagonals), a new tile standing in a diagonal run of three or more that `diagonalWord` does not take (`diagonalsRead`).
40
+ * @example
41
+ * ```ts
42
+ * import { crossingFit } from "@johnmorrisdotca/kumimoji";
43
+ *
44
+ * console.log(crossingFit((r,c)=>r===0&&c===0?"c":"",()=>true,"cat",{row:0,col:0},0,true));
45
+ * ```
31
46
  */
32
47
  export function crossingFit(letterAt, inside, word, anchor, at, across, diagonalWord = null) {
33
48
  const dr = across ? 0 : 1;
@@ -67,10 +82,28 @@ export function crossingFit(letterAt, inside, word, anchor, at, across, diagonal
67
82
  * its new squares. Only a run through a new tile can change, and two new
68
83
  * tiles of one word never share a diagonal (they share a row or a column), so
69
84
  * each is read on its own. A run of two, a corner touch, is never read.
85
+ *
86
+ * @param letterAt - The tile at a coordinate, or an empty string for no tile.
87
+ * @param inside - Whether a coordinate is within the caller’s working grid.
88
+ * @param word - The encoded word to lay or spell.
89
+ * @param fit - The first coordinate and fresh tile coordinates of the proposed laying.
90
+ * @param isWord - The predicate that accepts a readable word.
91
+ * @returns Whether every diagonal run a laying's new tiles would stand in is taken by `isWord`: each walked from its top end down, with the word's own letters on its new squares. Only a run through a new tile can change, and two new tiles of one word never share a diagonal (they share a row or a column), so each is read on its own. A run of two, a corner touch, is never read.
92
+ * @example
93
+ * ```ts
94
+ * import { loadTileWordsFromModule } from "@johnmorrisdotca/kumimoji/words";
95
+ * import { diagonalsRead } from "@johnmorrisdotca/kumimoji";
96
+ *
97
+ * const words = await loadTileWordsFromModule("english");
98
+ * console.log(diagonalsRead(()=>"",()=>true,"cat",{first:{row:0,col:0},fresh:[{row:0,col:0},{row:0,col:1},{row:0,col:2}]},word=>words.allowed.has(word)));
99
+ * ```
70
100
  */
71
101
  export function diagonalsRead(letterAt, inside, word, fit, isWord) {
72
- const freshAt = new Map(fit.fresh.map((square) => [`${square.row},${square.col}`, word[(square.row - fit.first.row) + (square.col - fit.first.col)]]));
73
- const at = (row, col) => (inside(row, col) ? letterAt(row, col) || (freshAt.get(`${row},${col}`) ?? "") : "");
102
+ const freshAt = new Map(fit.fresh.map((square) => [
103
+ `${square.row},${square.col}`,
104
+ word[square.row - fit.first.row + (square.col - fit.first.col)],
105
+ ]));
106
+ const at = (row, col) => inside(row, col) ? letterAt(row, col) || (freshAt.get(`${row},${col}`) ?? "") : "";
74
107
  for (const { row, col } of fit.fresh) {
75
108
  for (const lean of [1, -1]) {
76
109
  let top = 0;