@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
@@ -0,0 +1,62 @@
1
+ import type { GridVerdict } from "../grid.ts";
2
+ import { type TilePlay } from "../play.ts";
3
+ import { type KumimojiLocale, type KumimojiStrings } from "../strings.ts";
4
+ import type { TileWords } from "../tileWords.ts";
5
+ /** What is picked up: a tile in the hand, or one on the table. */
6
+ export type Held = {
7
+ from: "hand";
8
+ at: number;
9
+ } | {
10
+ from: "table";
11
+ square: string;
12
+ } | null;
13
+ export declare function node<K extends keyof HTMLElementTagNameMap>(name: K, className?: string, text?: string): HTMLElementTagNameMap[K];
14
+ export declare function button(text: string, onClick: () => void, primary?: boolean, testId?: string): HTMLButtonElement;
15
+ /** The game as the drawing reads it, shared with the mount so a move and a redraw see the same table. */
16
+ export type TableSession = {
17
+ play: TilePlay | null;
18
+ finishedAt: number | null;
19
+ started: number;
20
+ words: TileWords | null;
21
+ held: Held;
22
+ onSquare: string | null;
23
+ onHand: number;
24
+ say: KumimojiStrings;
25
+ locale: KumimojiLocale;
26
+ note: string;
27
+ destroyed: boolean;
28
+ };
29
+ /** The elements the mount built once; a redraw fills them and never replaces them. */
30
+ export type TableParts = {
31
+ root: HTMLElement;
32
+ status: HTMLElement;
33
+ board: HTMLElement;
34
+ notes: HTMLElement;
35
+ hand: HTMLElement;
36
+ picker: HTMLElement;
37
+ controls: HTMLElement;
38
+ keepTitle: HTMLElement;
39
+ saveJson: HTMLButtonElement;
40
+ saveText: HTMLButtonElement;
41
+ load: HTMLButtonElement;
42
+ keepNote: HTMLElement;
43
+ };
44
+ /** What a button does to the game. The mount owns the rules; the drawing only asks. */
45
+ export type TableActions = {
46
+ verdictOf: (now: TilePlay) => GridVerdict;
47
+ change: (next: TilePlay) => void;
48
+ newGame: () => void;
49
+ };
50
+ /**
51
+ * Draw the table from the session and the elements the mount built: the status
52
+ * line, the board, the hand, the wild-tile picker, the play buttons and the
53
+ * save labels, then put the keyboard focus back on the control it was on.
54
+ * @param session - The game, the words and what is held, shared with the mount.
55
+ * @param parts - The elements the drawing fills.
56
+ * @param actions - How a button changes the game or starts another.
57
+ * @returns The redraw, and the status line alone for the clock.
58
+ */
59
+ export declare function tableDrawing(session: TableSession, parts: TableParts, actions: TableActions): {
60
+ render: () => void;
61
+ renderStatus: () => void;
62
+ };
@@ -0,0 +1,229 @@
1
+ import { kumimojiClock } from "../export.js";
2
+ import { assignHandTile, draw, liftAll, mayDraw, mayTrade, sortHand, tilesLeft, trade, } from "../play.js";
3
+ import { kumimojiSay } from "../strings.js";
4
+ import { tileFace } from "../tileFace.js";
5
+ import { boardModel } from "./board.js";
6
+ /** What a wild nobody has given a letter yet shows in the hand. */
7
+ const WILD_MARK = "★";
8
+ export function node(name, className, text) {
9
+ const made = document.createElement(name);
10
+ if (className !== undefined)
11
+ made.className = className;
12
+ if (text !== undefined)
13
+ made.textContent = text;
14
+ return made;
15
+ }
16
+ export function button(text, onClick, primary = false, testId) {
17
+ const made = node("button", primary ? "km-button km-primary" : "km-button", text);
18
+ made.type = "button";
19
+ if (testId !== undefined)
20
+ made.dataset.testid = testId;
21
+ made.addEventListener("click", onClick);
22
+ return made;
23
+ }
24
+ /**
25
+ * Draw the table from the session and the elements the mount built: the status
26
+ * line, the board, the hand, the wild-tile picker, the play buttons and the
27
+ * save labels, then put the keyboard focus back on the control it was on.
28
+ * @param session - The game, the words and what is held, shared with the mount.
29
+ * @param parts - The elements the drawing fills.
30
+ * @param actions - How a button changes the game or starts another.
31
+ * @returns The redraw, and the status line alone for the clock.
32
+ */
33
+ export function tableDrawing(session, parts, actions) {
34
+ function renderStatus() {
35
+ const now = session.play;
36
+ parts.root.dataset.state = now === null ? "dealing" : session.finishedAt !== null ? "finished" : "playing";
37
+ if (now === null) {
38
+ parts.status.textContent = session.say.dealing;
39
+ return;
40
+ }
41
+ const left = tilesLeft(now);
42
+ parts.root.dataset.left = String(left);
43
+ const time = kumimojiClock((session.finishedAt ?? Date.now()) - session.started);
44
+ parts.status.textContent =
45
+ session.finishedAt !== null
46
+ ? kumimojiSay(session.say.finished, { time })
47
+ : kumimojiSay(left === 1 ? session.say.leftOne : session.say.left, { n: left, time });
48
+ }
49
+ function renderBoard(verdict) {
50
+ const playing = session.play;
51
+ const words = session.words;
52
+ const model = boardModel(playing.tiles, playing.tiles.size === 0 ? null : verdict, words.glyphOf);
53
+ const grid = node("div", "km-grid");
54
+ grid.style.gridTemplateColumns = `repeat(${model.cols}, var(--km-square))`;
55
+ grid.setAttribute("role", "group");
56
+ grid.setAttribute("aria-label", session.say.tableLabel);
57
+ grid.setAttribute("aria-description", session.say.tableKeys);
58
+ grid.dataset.cols = String(model.cols);
59
+ const cells = [];
60
+ for (const square of model.squares) {
61
+ const cell = node("button", "km-square");
62
+ cell.type = "button";
63
+ cell.dataset.square = square.square;
64
+ if (square.tile !== null) {
65
+ cell.classList.add("km-tile");
66
+ if (square.wild)
67
+ cell.classList.add("km-wild");
68
+ if (square.mark !== null)
69
+ cell.classList.add(`km-${square.mark}`);
70
+ cell.textContent = square.glyph;
71
+ cell.dataset.tile = square.glyph;
72
+ cell.setAttribute("aria-label", square.wild ? kumimojiSay(session.say.wildTile, { tile: square.glyph }) : square.glyph);
73
+ }
74
+ else {
75
+ cell.setAttribute("aria-label", session.say.emptySquare);
76
+ }
77
+ if (session.held?.from === "table" && session.held.square === square.square)
78
+ cell.classList.add("km-held");
79
+ cell.tabIndex = -1;
80
+ cells.push(cell);
81
+ grid.append(cell);
82
+ }
83
+ // The one Tab lands on: where the keyboard was, else a tile, else the middle.
84
+ const stop = cells.find((cell) => cell.dataset.square === session.onSquare) ??
85
+ cells.find((cell) => cell.classList.contains("km-tile")) ??
86
+ cells[Math.floor(cells.length / 2)];
87
+ if (stop !== undefined) {
88
+ stop.tabIndex = 0;
89
+ session.onSquare = stop.dataset.square ?? null;
90
+ }
91
+ parts.board.replaceChildren(grid);
92
+ parts.notes.textContent =
93
+ verdict.notWords.length > 0
94
+ ? kumimojiSay(session.say.notWords, { words: verdict.notWords.join(", ") })
95
+ : verdict.apart.size > 0
96
+ ? session.say.apart
97
+ : "";
98
+ }
99
+ function renderHand() {
100
+ const playing = session.play;
101
+ const words = session.words;
102
+ parts.hand.replaceChildren();
103
+ parts.hand.setAttribute("aria-label", session.say.handLabel);
104
+ playing.hand.forEach((tile, at) => {
105
+ const face = tileFace(tile);
106
+ const made = node("button", "km-tile km-in-hand");
107
+ made.type = "button";
108
+ made.dataset.hand = String(at);
109
+ made.textContent = face.blank ? WILD_MARK : words.glyphOf(tile);
110
+ if (face.wild)
111
+ made.classList.add("km-wild");
112
+ if (session.held?.from === "hand" && session.held.at === at)
113
+ made.classList.add("km-held");
114
+ made.dataset.tile = face.blank ? "*" : words.glyphOf(tile);
115
+ made.setAttribute("aria-label", face.blank
116
+ ? session.say.wildBlank
117
+ : face.wild
118
+ ? kumimojiSay(session.say.wildTile, { tile: words.glyphOf(tile) })
119
+ : words.glyphOf(tile));
120
+ made.tabIndex = at === Math.min(session.onHand, playing.hand.length - 1) ? 0 : -1;
121
+ parts.hand.append(made);
122
+ });
123
+ if (playing.hand.length === 0 && session.finishedAt === null)
124
+ parts.hand.append(node("span", "km-empty", session.say.noTiles));
125
+ }
126
+ function renderPicker() {
127
+ parts.picker.replaceChildren();
128
+ if (session.held?.from !== "hand" || session.play === null)
129
+ return;
130
+ const at = session.held.at;
131
+ const tile = session.play.hand[at];
132
+ if (tile === undefined || !tileFace(tile).wild)
133
+ return;
134
+ parts.picker.append(node("span", "km-picker-title", tile === "*" ? session.say.giveWild : session.say.changeWild));
135
+ const words = session.words;
136
+ for (const sound of words.wildOptions) {
137
+ const code = words.wildFor(sound);
138
+ if (code === null)
139
+ continue;
140
+ parts.picker.append(button(sound, () => {
141
+ session.held = { from: "hand", at };
142
+ actions.change(assignHandTile(session.play, at, code));
143
+ }));
144
+ }
145
+ }
146
+ function renderControls(verdict) {
147
+ parts.controls.replaceChildren();
148
+ if (session.finishedAt !== null) {
149
+ parts.controls.append(button(session.say.newGame, () => actions.newGame(), true, "km-new"));
150
+ return;
151
+ }
152
+ const now = session.play;
153
+ const drawButton = button(session.say.draw, () => {
154
+ session.held = null;
155
+ actions.change(draw(now));
156
+ }, true, "km-draw");
157
+ drawButton.disabled = !mayDraw(now, verdict);
158
+ const tradeButton = button(session.say.trade, () => {
159
+ if (session.held?.from !== "hand")
160
+ return;
161
+ const at = session.held.at;
162
+ session.held = null;
163
+ actions.change(trade(now, at));
164
+ }, false, "km-trade");
165
+ tradeButton.disabled = session.held?.from !== "hand" || !mayTrade(now);
166
+ parts.controls.append(drawButton, tradeButton, button(session.say.sort, () => actions.change(sortHand(now)), false, "km-sort"), button(session.say.liftAll, () => {
167
+ session.held = null;
168
+ actions.change(liftAll(now));
169
+ }, false, "km-lift"), button(session.say.newGame, () => actions.newGame(), false, "km-new"));
170
+ }
171
+ function renderKeep() {
172
+ parts.keepTitle.textContent = session.say.keep;
173
+ parts.saveJson.textContent = session.say.saveJson;
174
+ parts.saveText.textContent = session.say.saveText;
175
+ parts.load.textContent = session.say.load;
176
+ parts.saveJson.disabled = session.play === null;
177
+ parts.saveText.disabled = session.play === null;
178
+ parts.keepNote.textContent = session.note;
179
+ }
180
+ /** Where the keyboard is, so that it can be put back after everything is drawn again: the zone, and which of it. */
181
+ function keyboardAt() {
182
+ const on = document.activeElement;
183
+ if (on === null || !parts.root.contains(on))
184
+ return null;
185
+ const mark = (zone, id) => id === undefined ? null : { zone, id };
186
+ if (parts.board.contains(on))
187
+ return mark("board", on.dataset.square);
188
+ if (parts.hand.contains(on))
189
+ return mark("hand", on.dataset.hand);
190
+ if (parts.controls.contains(on))
191
+ return mark("controls", on.dataset.testid);
192
+ if (parts.picker.contains(on))
193
+ return mark("picker", String([...parts.picker.querySelectorAll("button")].indexOf(on)));
194
+ return null;
195
+ }
196
+ function keyboardBack(at) {
197
+ if (at === null)
198
+ return;
199
+ const zone = { board: parts.board, hand: parts.hand, controls: parts.controls, picker: parts.picker }[at.zone];
200
+ const found = at.zone === "board"
201
+ ? zone.querySelector(`[data-square="${at.id}"]`)
202
+ : at.zone === "hand"
203
+ ? zone.querySelector(`[data-hand="${at.id}"]`)
204
+ : at.zone === "controls"
205
+ ? zone.querySelector(`[data-testid="${at.id}"]`)
206
+ : zone.querySelectorAll("button")[Number(at.id)];
207
+ // A button that has become disabled cannot hold the focus; the table keeps it rather than dropping it on the page.
208
+ if (found !== undefined && found !== null && !found.disabled)
209
+ found.focus({ preventScroll: true });
210
+ }
211
+ function render() {
212
+ if (session.destroyed)
213
+ return;
214
+ const where = keyboardAt();
215
+ parts.root.lang = session.locale;
216
+ renderStatus();
217
+ renderKeep();
218
+ const now = session.play;
219
+ if (now === null || session.words === null)
220
+ return;
221
+ const verdict = actions.verdictOf(now);
222
+ renderBoard(verdict);
223
+ renderPicker();
224
+ renderHand();
225
+ renderControls(verdict);
226
+ keyboardBack(where);
227
+ }
228
+ return { render, renderStatus };
229
+ }
@@ -2,7 +2,16 @@ import { type KumimojiSaved } from "../export.ts";
2
2
  import type { KumimojiDeal, KumimojiLanguage, KumimojiLength, KumimojiLevel } from "../kumimoji.types.ts";
3
3
  import { type TilePlay } from "../play.ts";
4
4
  import { type KumimojiLocale, type KumimojiStrings } from "../strings.ts";
5
- /** What `mountKumimoji` may be given. Everything is optional: with nothing, it is a short English game from a hand of seven, with some wild tiles. */
5
+ /**
6
+ * Optional initial game, presentation, readiness and callback choices.
7
+ * With nothing, it is a short English game from a hand of seven, with some wild tiles.
8
+ * @example
9
+ * ```ts
10
+ * import type { KumimojiTableOptions } from "@johnmorrisdotca/kumimoji/ui";
11
+ * const options: KumimojiTableOptions = { hand: 7, keep: false, controls: true };
12
+ * console.log(options.hand);
13
+ * ```
14
+ */
6
15
  export type KumimojiTableOptions = {
7
16
  /** The language of the tiles: `"english"` or `"japanese"` kana. English by default. */
8
17
  language?: KumimojiLanguage;
@@ -32,11 +41,29 @@ export type KumimojiTableOptions = {
32
41
  theme?: Readonly<Record<string, string>>;
33
42
  /** Whether the table offers saving and loading. True by default. */
34
43
  keep?: boolean;
44
+ /** Show native play-action buttons; a host hiding them supplies its own actions. */
45
+ controls?: boolean;
46
+ /** A validated saved game to restore initially instead of dealing a new one. */
47
+ saved?: KumimojiSaved;
48
+ /** Called after a word-list load or initial restoration fails. */
49
+ onError?: (error: Error) => void;
35
50
  };
36
51
  /** The options that make a game, which a new game may change. */
37
52
  type GameOptions = Pick<KumimojiTableOptions, "language" | "hand" | "level" | "gameLength" | "diagonals" | "seed">;
38
- /** What `mountKumimoji` hands back: the game being played, and the ways to change it from outside. */
53
+ /**
54
+ * Readiness, structured play and presentation/game controls for a mounted table.
55
+ * @example
56
+ * ```ts
57
+ * import type { KumimojiTableHandle } from "@johnmorrisdotca/kumimoji/ui";
58
+ * const setJapanese = (table: KumimojiTableHandle): void => table.set({ locale: "ja" });
59
+ * console.log(typeof setJapanese);
60
+ * ```
61
+ */
39
62
  export type KumimojiTableHandle = {
63
+ /** Settles once the initial deal or restored game is ready. */
64
+ readonly ready: Promise<void>;
65
+ /** Change presentation and controls without redealing. */
66
+ set: (options: Pick<KumimojiTableOptions, "locale" | "strings" | "theme" | "keep" | "controls">) => void;
40
67
  /** The game as it stands, or null while it is being dealt. */
41
68
  play: () => TilePlay | null;
42
69
  /** The game as it would be saved (`kumimojiToJSON`), or null while it is being dealt. */
@@ -61,6 +88,18 @@ export type KumimojiTableHandle = {
61
88
  * game loads back.
62
89
  *
63
90
  * The word list is fetched the first time a game in its language is dealt.
91
+ * @param target - The host element for this table.
92
+ * @param options - Initial deal or saved state, presentation and callbacks.
93
+ * @returns The native handle, including ready and presentation updates without redealing.
94
+ * @example
95
+ * ```ts
96
+ * import { mountKumimoji } from "@johnmorrisdotca/kumimoji/ui";
97
+ * if (typeof document !== "undefined") {
98
+ * const table = mountKumimoji(document.createElement("div"), { keep: false });
99
+ * void table.ready.then(() => table.destroy());
100
+ * }
101
+ * console.log(typeof mountKumimoji);
102
+ * ```
64
103
  */
65
104
  export declare function mountKumimoji(target: HTMLElement, options?: KumimojiTableOptions): KumimojiTableHandle;
66
105
  export {};