@johnmorrisdotca/kumimoji 1.3.0 → 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.
package/dist/ui/mount.js CHANGED
@@ -1,31 +1,12 @@
1
- import { kumimojiClock, kumimojiFromJSON, kumimojiToJSON, kumimojiToText } from "../export.js";
1
+ import { kumimojiFromJSON, kumimojiToJSON, kumimojiToText } from "../export.js";
2
2
  import { generateKumimoji } from "../generate.js";
3
3
  import { judgeWithWords } from "../judge.js";
4
- import { assignHandTile, deal, draw, isFinished, liftAll, liftToHand, mayDraw, mayTrade, moveOnTable, placeFromHand, sortHand, swapWithHand, tilesLeft, trade } from "../play.js";
5
- import { kumimojiSay, kumimojiStrings } from "../strings.js";
4
+ import { deal, isFinished, liftToHand, moveOnTable, placeFromHand, swapWithHand } from "../play.js";
5
+ import { kumimojiStrings } from "../strings.js";
6
6
  import { KUMIMOJI_HANDS } from "../tiles.constants.js";
7
- import { tileFace } from "../tileFace.js";
8
7
  import { loadTileWords } from "../tileWords.js";
9
- import { boardModel } from "./board.js";
8
+ import { button, node, tableDrawing } from "./draw.js";
10
9
  import { KUMIMOJI_STYLE } from "./style.js";
11
- /** What a wild nobody has given a letter yet shows in the hand. */
12
- const WILD_MARK = "★";
13
- function node(name, className, text) {
14
- const made = document.createElement(name);
15
- if (className !== undefined)
16
- made.className = className;
17
- if (text !== undefined)
18
- made.textContent = text;
19
- return made;
20
- }
21
- function button(text, onClick, primary = false, testId) {
22
- const made = node("button", primary ? "km-button km-primary" : "km-button", text);
23
- made.type = "button";
24
- if (testId !== undefined)
25
- made.dataset.testid = testId;
26
- made.addEventListener("click", onClick);
27
- return made;
28
- }
29
10
  /** Hand a string to the person as a file of that name. */
30
11
  function save(name, type, text) {
31
12
  const url = URL.createObjectURL(new Blob([text], { type }));
@@ -60,28 +41,46 @@ function save(name, type, text) {
60
41
  * ```
61
42
  */
62
43
  export function mountKumimoji(target, options = {}) {
63
- let settings = { language: options.language, hand: options.hand, level: options.level, gameLength: options.gameLength, diagonals: options.diagonals, seed: options.seed };
64
- const pageLocale = () => (typeof document !== "undefined" && document.documentElement.lang.toLowerCase().startsWith("ja") ? "ja" : "en");
65
- let locale = options.locale ?? pageLocale();
66
- let own = options.strings ?? {};
67
- let say = kumimojiStrings(locale, own);
68
- let note = "";
69
- let words = null;
70
- let dealt = null;
71
- let play = null;
72
- let held = null;
44
+ let current = options;
45
+ let settings = {
46
+ language: current.language,
47
+ hand: current.hand,
48
+ level: current.level,
49
+ gameLength: current.gameLength,
50
+ diagonals: current.diagonals,
51
+ seed: current.seed,
52
+ };
53
+ const pageLocale = () => typeof document !== "undefined" && document.documentElement.lang.toLowerCase().startsWith("ja") ? "ja" : "en";
54
+ let own = current.strings ?? {};
55
+ const openingLocale = current.locale ?? pageLocale();
73
56
  // Where a keyboard is: the square and the hand tile that Tab lands on, and the one the arrow keys move from.
74
- let onSquare = null;
75
- let onHand = 0;
76
- let started = 0;
77
- let finishedAt = null;
78
- let destroyed = false;
57
+ const session = {
58
+ play: null,
59
+ finishedAt: null,
60
+ started: 0,
61
+ words: null,
62
+ held: null,
63
+ onSquare: null,
64
+ onHand: 0,
65
+ say: kumimojiStrings(openingLocale, own),
66
+ locale: openingLocale,
67
+ note: "",
68
+ destroyed: false,
69
+ };
70
+ let dealt = null;
79
71
  // Each deal and each game put on the table takes a number; one that is overtaken while its list loads gives way.
80
72
  let asked = 0;
73
+ // The drawing is built once the elements exist. Until then nothing calls these.
74
+ const bridge = {
75
+ render: () => { },
76
+ renderStatus: () => { },
77
+ newGame: () => { },
78
+ };
79
+ const render = () => bridge.render();
81
80
  const root = node("div", "km-root");
82
81
  root.dataset.testid = "km-root";
83
82
  root.dataset.state = "dealing";
84
- for (const [name, value] of Object.entries(options.theme ?? {}))
83
+ for (const [name, value] of Object.entries(current.theme ?? {}))
85
84
  root.style.setProperty(name, value);
86
85
  const style = node("style");
87
86
  style.textContent = KUMIMOJI_STYLE;
@@ -99,15 +98,20 @@ export function mountKumimoji(target, options = {}) {
99
98
  const picker = node("div", "km-picker");
100
99
  picker.dataset.testid = "km-picker";
101
100
  const controls = node("div", "km-controls");
102
- controls.hidden = options.controls === false;
101
+ controls.hidden = current.controls === false;
103
102
  controls.dataset.testid = "km-controls";
104
103
  const keep = node("details", "km-keep");
105
104
  keep.dataset.testid = "km-keep";
106
105
  root.append(style, status, board, notes, picker, hand, controls);
107
- if (options.keep !== false)
106
+ if (current.keep !== false)
108
107
  root.append(keep);
109
108
  target.append(root);
110
- const savedNow = () => (play === null || dealt === null ? null : { deal: dealt, play, elapsedMs: (finishedAt ?? Date.now()) - started });
109
+ const savedNow = () => {
110
+ const now = session.play;
111
+ if (now === null || dealt === null)
112
+ return null;
113
+ return { deal: dealt, play: now, elapsedMs: (session.finishedAt ?? Date.now()) - session.started };
114
+ };
111
115
  // The saving and loading are made once, so the fold stays open or shut as the person left it; only their words change.
112
116
  const keepTitle = node("summary", "km-keep-title");
113
117
  const keepNote = node("p", "km-note");
@@ -120,15 +124,15 @@ export function mountKumimoji(target, options = {}) {
120
124
  file.dataset.testid = "km-file";
121
125
  const read = async (text) => {
122
126
  const loaded = await kumimojiFromJSON(text).catch(() => null);
123
- if (destroyed)
127
+ if (session.destroyed)
124
128
  return;
125
129
  if (loaded === null) {
126
- note = say.loadBad;
130
+ session.note = session.say.loadBad;
127
131
  render();
128
132
  return;
129
133
  }
130
134
  await handle.setGame(loaded);
131
- note = say.loaded;
135
+ session.note = session.say.loaded;
132
136
  render();
133
137
  };
134
138
  file.addEventListener("change", () => {
@@ -146,60 +150,59 @@ export function mountKumimoji(target, options = {}) {
146
150
  const saveText = button("", () => {
147
151
  const now = savedNow();
148
152
  if (now !== null)
149
- save(`kumimoji-${now.deal.seed}.txt`, "text/plain", kumimojiToText(now, say));
153
+ save(`kumimoji-${now.deal.seed}.txt`, "text/plain", kumimojiToText(now, session.say));
150
154
  }, false, "km-save-text");
151
155
  const load = button("", () => file.click(), false, "km-load");
152
156
  const keepButtons = node("div", "km-controls");
153
157
  keepButtons.append(saveJson, saveText, load);
154
158
  keep.append(keepTitle, keepButtons, keepNote, file);
155
- const tick = setInterval(() => {
156
- if (play !== null && finishedAt === null)
157
- renderStatus();
158
- }, 1000);
159
- const verdictOf = (now) => judgeWithWords(now.tiles, words, { diagonals: settings.diagonals === true });
159
+ const verdictOf = (now) => judgeWithWords(now.tiles, session.words, { diagonals: settings.diagonals === true });
160
160
  const change = (next) => {
161
- play = next;
161
+ session.play = next;
162
162
  const verdict = verdictOf(next);
163
- if (finishedAt === null && isFinished(next, verdict)) {
164
- finishedAt = Date.now();
165
- options.onFinish?.({ deal: dealt, play: next, elapsedMs: finishedAt - started });
163
+ if (session.finishedAt === null && isFinished(next, verdict)) {
164
+ session.finishedAt = Date.now();
165
+ current.onFinish?.({ deal: dealt, play: next, elapsedMs: session.finishedAt - session.started });
166
166
  }
167
- note = "";
167
+ session.note = "";
168
168
  const now = savedNow();
169
169
  if (now !== null)
170
- options.onChange?.(now);
170
+ current.onChange?.(now);
171
171
  render();
172
172
  };
173
173
  const tapSquare = (square) => {
174
- if (play === null || finishedAt !== null)
174
+ const now = session.play;
175
+ if (now === null || session.finishedAt !== null)
175
176
  return;
176
- const there = play.tiles.get(square);
177
+ const there = now.tiles.get(square);
178
+ const held = session.held;
177
179
  if (held === null) {
178
180
  if (there !== undefined)
179
- held = { from: "table", square };
181
+ session.held = { from: "table", square };
180
182
  render();
181
183
  return;
182
184
  }
183
185
  if (held.from === "hand") {
184
186
  const at = held.at;
185
- const tile = play.hand[at];
187
+ const tile = now.hand[at];
186
188
  if (tile === undefined)
187
189
  return;
188
190
  // A wild is given a letter before it is laid.
189
191
  if (tile === "*")
190
192
  return;
191
- held = null;
192
- change(there === undefined ? placeFromHand(play, at, square) : swapWithHand(play, at, square));
193
+ session.held = null;
194
+ change(there === undefined ? placeFromHand(now, at, square) : swapWithHand(now, at, square));
193
195
  return;
194
196
  }
195
197
  const from = held.square;
196
- held = null;
197
- change(from === square ? liftToHand(play, square) : moveOnTable(play, from, square));
198
+ session.held = null;
199
+ change(from === square ? liftToHand(now, square) : moveOnTable(now, from, square));
198
200
  };
199
201
  const tapHand = (at) => {
200
- if (play === null || finishedAt !== null)
202
+ if (session.play === null || session.finishedAt !== null)
201
203
  return;
202
- held = held?.from === "hand" && held.at === at ? null : { from: "hand", at };
204
+ const held = session.held;
205
+ session.held = held?.from === "hand" && held.at === at ? null : { from: "hand", at };
203
206
  render();
204
207
  };
205
208
  board.addEventListener("click", (event) => {
@@ -222,7 +225,23 @@ export function mountKumimoji(target, options = {}) {
222
225
  const cells = [...grid.children];
223
226
  const cols = Number(grid.dataset.cols);
224
227
  const at = cells.indexOf(cell);
225
- const step = event.key === "ArrowLeft" ? (at % cols > 0 ? -1 : 0) : event.key === "ArrowRight" ? (at % cols < cols - 1 ? 1 : 0) : event.key === "ArrowUp" ? (at - cols >= 0 ? -cols : 0) : event.key === "ArrowDown" ? (at + cols < cells.length ? cols : 0) : null;
228
+ const step = event.key === "ArrowLeft"
229
+ ? at % cols > 0
230
+ ? -1
231
+ : 0
232
+ : event.key === "ArrowRight"
233
+ ? at % cols < cols - 1
234
+ ? 1
235
+ : 0
236
+ : event.key === "ArrowUp"
237
+ ? at - cols >= 0
238
+ ? -cols
239
+ : 0
240
+ : event.key === "ArrowDown"
241
+ ? at + cols < cells.length
242
+ ? cols
243
+ : 0
244
+ : null;
226
245
  if (step === null)
227
246
  return;
228
247
  event.preventDefault();
@@ -231,18 +250,18 @@ export function mountKumimoji(target, options = {}) {
231
250
  return;
232
251
  cell.tabIndex = -1;
233
252
  next.tabIndex = 0;
234
- onSquare = next.dataset.square ?? null;
253
+ session.onSquare = next.dataset.square ?? null;
235
254
  next.focus();
236
255
  });
237
256
  board.addEventListener("focusin", (event) => {
238
257
  const square = event.target.closest("[data-square]")?.getAttribute("data-square");
239
258
  if (square !== null && square !== undefined)
240
- onSquare = square;
259
+ session.onSquare = square;
241
260
  });
242
261
  hand.addEventListener("focusin", (event) => {
243
262
  const at = event.target.closest("[data-hand]")?.getAttribute("data-hand");
244
263
  if (at !== null && at !== undefined)
245
- onHand = Number(at);
264
+ session.onHand = Number(at);
246
265
  });
247
266
  hand.addEventListener("keydown", (event) => {
248
267
  const tile = event.target.closest("[data-hand]");
@@ -250,7 +269,15 @@ export function mountKumimoji(target, options = {}) {
250
269
  return;
251
270
  const tiles = [...hand.querySelectorAll("[data-hand]")];
252
271
  const at = tiles.indexOf(tile);
253
- const to = event.key === "ArrowLeft" ? at - 1 : event.key === "ArrowRight" ? at + 1 : event.key === "Home" ? 0 : event.key === "End" ? tiles.length - 1 : null;
272
+ const to = event.key === "ArrowLeft"
273
+ ? at - 1
274
+ : event.key === "ArrowRight"
275
+ ? at + 1
276
+ : event.key === "Home"
277
+ ? 0
278
+ : event.key === "End"
279
+ ? tiles.length - 1
280
+ : null;
254
281
  if (to === null)
255
282
  return;
256
283
  event.preventDefault();
@@ -259,234 +286,83 @@ export function mountKumimoji(target, options = {}) {
259
286
  return;
260
287
  tile.tabIndex = -1;
261
288
  next.tabIndex = 0;
262
- onHand = to;
289
+ session.onHand = to;
263
290
  next.focus();
264
291
  });
265
292
  root.addEventListener("keydown", (event) => {
266
- if (event.key !== "Escape" || held === null)
293
+ if (event.key !== "Escape" || session.held === null)
267
294
  return;
268
- held = null;
295
+ session.held = null;
269
296
  render();
270
297
  });
271
- function renderStatus() {
272
- root.dataset.state = play === null ? "dealing" : finishedAt !== null ? "finished" : "playing";
273
- if (play === null) {
274
- status.textContent = say.dealing;
275
- return;
276
- }
277
- const left = tilesLeft(play);
278
- root.dataset.left = String(left);
279
- const time = kumimojiClock((finishedAt ?? Date.now()) - started);
280
- status.textContent = finishedAt !== null ? kumimojiSay(say.finished, { time }) : kumimojiSay(left === 1 ? say.leftOne : say.left, { n: left, time });
281
- }
282
- function renderBoard(verdict) {
283
- const model = boardModel(play.tiles, play.tiles.size === 0 ? null : verdict, words.glyphOf);
284
- const grid = node("div", "km-grid");
285
- grid.style.gridTemplateColumns = `repeat(${model.cols}, var(--km-square))`;
286
- grid.setAttribute("role", "group");
287
- grid.setAttribute("aria-label", say.tableLabel);
288
- grid.setAttribute("aria-description", say.tableKeys);
289
- grid.dataset.cols = String(model.cols);
290
- const cells = [];
291
- for (const square of model.squares) {
292
- const cell = node("button", "km-square");
293
- cell.type = "button";
294
- cell.dataset.square = square.square;
295
- if (square.tile !== null) {
296
- cell.classList.add("km-tile");
297
- if (square.wild)
298
- cell.classList.add("km-wild");
299
- if (square.mark !== null)
300
- cell.classList.add(`km-${square.mark}`);
301
- cell.textContent = square.glyph;
302
- cell.dataset.tile = square.glyph;
303
- cell.setAttribute("aria-label", square.wild ? kumimojiSay(say.wildTile, { tile: square.glyph }) : square.glyph);
304
- }
305
- else {
306
- cell.setAttribute("aria-label", say.emptySquare);
307
- }
308
- if (held?.from === "table" && held.square === square.square)
309
- cell.classList.add("km-held");
310
- cell.tabIndex = -1;
311
- cells.push(cell);
312
- grid.append(cell);
313
- }
314
- // The one Tab lands on: where the keyboard was, else a tile, else the middle.
315
- const stop = cells.find((cell) => cell.dataset.square === onSquare) ?? cells.find((cell) => cell.classList.contains("km-tile")) ?? cells[Math.floor(cells.length / 2)];
316
- if (stop !== undefined) {
317
- stop.tabIndex = 0;
318
- onSquare = stop.dataset.square ?? null;
319
- }
320
- board.replaceChildren(grid);
321
- notes.textContent = verdict.notWords.length > 0 ? kumimojiSay(say.notWords, { words: verdict.notWords.join(", ") }) : verdict.apart.size > 0 ? say.apart : "";
322
- }
323
- function renderHand() {
324
- hand.replaceChildren();
325
- hand.setAttribute("aria-label", say.handLabel);
326
- play.hand.forEach((tile, at) => {
327
- const face = tileFace(tile);
328
- const made = node("button", "km-tile km-in-hand");
329
- made.type = "button";
330
- made.dataset.hand = String(at);
331
- made.textContent = face.blank ? WILD_MARK : words.glyphOf(tile);
332
- if (face.wild)
333
- made.classList.add("km-wild");
334
- if (held?.from === "hand" && held.at === at)
335
- made.classList.add("km-held");
336
- made.dataset.tile = face.blank ? "*" : words.glyphOf(tile);
337
- made.setAttribute("aria-label", face.blank ? say.wildBlank : face.wild ? kumimojiSay(say.wildTile, { tile: words.glyphOf(tile) }) : words.glyphOf(tile));
338
- made.tabIndex = at === Math.min(onHand, play.hand.length - 1) ? 0 : -1;
339
- hand.append(made);
340
- });
341
- if (play.hand.length === 0 && finishedAt === null)
342
- hand.append(node("span", "km-empty", say.noTiles));
343
- }
344
- function renderPicker() {
345
- picker.replaceChildren();
346
- if (held?.from !== "hand" || play === null)
347
- return;
348
- const at = held.at;
349
- const tile = play.hand[at];
350
- if (tile === undefined || !tileFace(tile).wild)
351
- return;
352
- picker.append(node("span", "km-picker-title", tile === "*" ? say.giveWild : say.changeWild));
353
- for (const sound of words.wildOptions) {
354
- const code = words.wildFor(sound);
355
- if (code === null)
356
- continue;
357
- picker.append(button(sound, () => {
358
- held = { from: "hand", at };
359
- change(assignHandTile(play, at, code));
360
- }));
361
- }
362
- }
363
- function renderControls(verdict) {
364
- controls.replaceChildren();
365
- if (finishedAt !== null) {
366
- controls.append(button(say.newGame, () => void handle.newGame(), true, "km-new"));
367
- return;
368
- }
369
- const now = play;
370
- const drawButton = button(say.draw, () => {
371
- held = null;
372
- change(draw(now));
373
- }, true, "km-draw");
374
- drawButton.disabled = !mayDraw(now, verdict);
375
- const tradeButton = button(say.trade, () => {
376
- if (held?.from !== "hand")
377
- return;
378
- const at = held.at;
379
- held = null;
380
- change(trade(now, at));
381
- }, false, "km-trade");
382
- tradeButton.disabled = held?.from !== "hand" || !mayTrade(now);
383
- controls.append(drawButton, tradeButton, button(say.sort, () => change(sortHand(now)), false, "km-sort"), button(say.liftAll, () => {
384
- held = null;
385
- change(liftAll(now));
386
- }, false, "km-lift"), button(say.newGame, () => void handle.newGame(), false, "km-new"));
387
- }
388
- function renderKeep() {
389
- keepTitle.textContent = say.keep;
390
- saveJson.textContent = say.saveJson;
391
- saveText.textContent = say.saveText;
392
- load.textContent = say.load;
393
- saveJson.disabled = play === null;
394
- saveText.disabled = play === null;
395
- keepNote.textContent = note;
396
- }
397
- /** Where the keyboard is, so that it can be put back after everything is drawn again: the zone, and which of it. */
398
- function keyboardAt() {
399
- const on = document.activeElement;
400
- if (on === null || !root.contains(on))
401
- return null;
402
- const mark = (zone, id) => (id === undefined ? null : { zone, id });
403
- if (board.contains(on))
404
- return mark("board", on.dataset.square);
405
- if (hand.contains(on))
406
- return mark("hand", on.dataset.hand);
407
- if (controls.contains(on))
408
- return mark("controls", on.dataset.testid);
409
- if (picker.contains(on))
410
- return mark("picker", String([...picker.querySelectorAll("button")].indexOf(on)));
411
- return null;
412
- }
413
- function keyboardBack(at) {
414
- if (at === null)
415
- return;
416
- const zone = { board, hand, controls, picker }[at.zone];
417
- const found = at.zone === "board" ? zone.querySelector(`[data-square="${at.id}"]`) : at.zone === "hand" ? zone.querySelector(`[data-hand="${at.id}"]`) : at.zone === "controls" ? zone.querySelector(`[data-testid="${at.id}"]`) : zone.querySelectorAll("button")[Number(at.id)];
418
- // A button that has become disabled cannot hold the focus; the table keeps it rather than dropping it on the page.
419
- if (found !== undefined && found !== null && !found.disabled)
420
- found.focus({ preventScroll: true });
421
- }
422
- function render() {
423
- if (destroyed)
424
- return;
425
- const where = keyboardAt();
426
- root.lang = locale;
427
- renderStatus();
428
- renderKeep();
429
- if (play === null || words === null)
430
- return;
431
- const verdict = verdictOf(play);
432
- renderBoard(verdict);
433
- renderPicker();
434
- renderHand();
435
- renderControls(verdict);
436
- keyboardBack(where);
437
- }
298
+ const drawing = tableDrawing(session, { root, status, board, notes, hand, picker, controls, keepTitle, saveJson, saveText, load, keepNote }, { verdictOf, change, newGame: () => bridge.newGame() });
299
+ bridge.render = () => drawing.render();
300
+ bridge.renderStatus = () => drawing.renderStatus();
301
+ const tick = setInterval(() => {
302
+ if (session.play !== null && session.finishedAt === null)
303
+ bridge.renderStatus();
304
+ }, 1000);
438
305
  let initialReady = Promise.resolve();
439
306
  const handle = {
440
- get ready() { return initialReady; },
307
+ get ready() {
308
+ return initialReady;
309
+ },
441
310
  set: (changes) => {
442
- for (const name of Object.keys(options.theme ?? {}))
311
+ for (const name of Object.keys(current.theme ?? {}))
443
312
  if (changes.theme !== undefined && !(name in changes.theme))
444
313
  root.style.removeProperty(name);
445
314
  if (changes.theme !== undefined)
446
315
  for (const [name, value] of Object.entries(changes.theme))
447
316
  root.style.setProperty(name, value);
448
- options = { ...options, ...changes };
317
+ current = { ...current, ...changes };
449
318
  if (changes.locale !== undefined)
450
- locale = changes.locale;
319
+ session.locale = changes.locale;
451
320
  if (changes.strings !== undefined)
452
321
  own = changes.strings;
453
- say = kumimojiStrings(locale, own);
454
- if (options.keep === false)
322
+ session.say = kumimojiStrings(session.locale, own);
323
+ if (current.keep === false)
455
324
  keep.remove();
456
325
  else if (keep.parentNode !== root)
457
326
  root.append(keep);
458
- controls.hidden = options.controls === false;
327
+ controls.hidden = current.controls === false;
459
328
  render();
460
329
  },
461
- play: () => play,
330
+ play: () => session.play,
462
331
  saved: savedNow,
463
332
  setGame: async (saved) => {
464
333
  const mine = ++asked;
465
334
  const language = saved.deal.language;
466
- settings = { language, hand: saved.deal.size, level: saved.deal.level, gameLength: saved.deal.gameLength, diagonals: saved.deal.diagonals === true, seed: undefined };
335
+ settings = {
336
+ language,
337
+ hand: saved.deal.size,
338
+ level: saved.deal.level,
339
+ gameLength: saved.deal.gameLength,
340
+ diagonals: saved.deal.diagonals === true,
341
+ seed: undefined,
342
+ };
467
343
  const list = await loadTileWords(language);
468
- if (destroyed || mine !== asked)
344
+ if (session.destroyed || mine !== asked)
469
345
  return;
470
- words = list;
346
+ session.words = list;
471
347
  root.classList.toggle("km-japanese", language === "japanese");
472
348
  dealt = saved.deal;
473
349
  root.dataset.seed = String(saved.deal.seed);
474
- held = null;
475
- finishedAt = null;
476
- started = Date.now() - saved.elapsedMs;
350
+ session.held = null;
351
+ session.finishedAt = null;
352
+ session.started = Date.now() - saved.elapsedMs;
477
353
  // A game loaded already finished stays finished, at the time it took, and is not announced again.
478
- if (isFinished(saved.play, judgeWithWords(saved.play.tiles, words, { diagonals: settings.diagonals === true })))
479
- finishedAt = Date.now();
480
- play = saved.play;
481
- note = "";
482
- options.onChange?.(saved);
354
+ if (isFinished(saved.play, judgeWithWords(saved.play.tiles, list, { diagonals: settings.diagonals === true })))
355
+ session.finishedAt = Date.now();
356
+ session.play = saved.play;
357
+ session.note = "";
358
+ current.onChange?.(saved);
483
359
  render();
484
360
  },
485
361
  setLocale: (next, strings) => {
486
- locale = next;
362
+ session.locale = next;
487
363
  if (strings !== undefined)
488
364
  own = strings;
489
- say = kumimojiStrings(locale, own);
365
+ session.say = kumimojiStrings(session.locale, own);
490
366
  render();
491
367
  },
492
368
  newGame: async (changed = {}) => {
@@ -494,31 +370,43 @@ export function mountKumimoji(target, options = {}) {
494
370
  settings = { ...settings, ...changed, seed: undefined };
495
371
  const language = settings.language ?? "english";
496
372
  const size = settings.hand ?? KUMIMOJI_HANDS.quick;
497
- play = null;
498
- held = null;
499
- finishedAt = null;
500
- note = "";
373
+ session.play = null;
374
+ session.held = null;
375
+ session.finishedAt = null;
376
+ session.note = "";
501
377
  render();
502
378
  const list = await loadTileWords(language);
503
- if (destroyed || mine !== asked)
379
+ if (session.destroyed || mine !== asked)
504
380
  return;
505
- words = list;
381
+ session.words = list;
506
382
  // A seed deals one game: the next is dealt from a new one unless asked for by seed again.
507
383
  const seed = changed.seed ?? Math.floor(Math.random() * 2 ** 31);
508
384
  root.classList.toggle("km-japanese", language === "japanese");
509
385
  root.dataset.seed = String(seed);
510
- dealt = generateKumimoji(size, settings.level ?? "medium", seed, { language, gameLength: settings.gameLength, diagonals: settings.diagonals });
511
- started = Date.now();
386
+ dealt = generateKumimoji(size, settings.level ?? "medium", seed, {
387
+ language,
388
+ gameLength: settings.gameLength,
389
+ diagonals: settings.diagonals,
390
+ });
391
+ session.started = Date.now();
512
392
  change(deal(dealt.givens, size));
513
393
  },
514
394
  destroy: () => {
515
- destroyed = true;
395
+ session.destroyed = true;
516
396
  clearInterval(tick);
517
397
  root.remove();
518
398
  },
519
399
  };
520
- initialReady = options.saved === undefined ? handle.newGame(settings) : handle.setGame(options.saved);
521
- void initialReady.catch((error) => { if (destroyed)
522
- return; const problem = error instanceof Error ? error : new Error(String(error)); status.textContent = say.loadError; options.onError?.(problem); });
400
+ bridge.newGame = () => {
401
+ void handle.newGame();
402
+ };
403
+ initialReady = current.saved === undefined ? handle.newGame(settings) : handle.setGame(current.saved);
404
+ void initialReady.catch((error) => {
405
+ if (session.destroyed)
406
+ return;
407
+ const problem = error instanceof Error ? error : new Error(String(error));
408
+ status.textContent = session.say.loadError;
409
+ current.onError?.(problem);
410
+ });
523
411
  return handle;
524
412
  }
@@ -3,7 +3,7 @@
3
3
  * variable so a page can wear it in its own colours (set them on the element
4
4
  * or any ancestor, or pass them as `theme`). Light and dark follow the
5
5
  * reader's system. Everything to be tapped is at least 44px.
6
- * @example
6
+ * @example
7
7
  * ```ts
8
8
  * import { KUMIMOJI_STYLE } from "@johnmorrisdotca/kumimoji/ui";
9
9
  * console.log(KUMIMOJI_STYLE.includes(".km-root")); // true
package/dist/ui/style.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * variable so a page can wear it in its own colours (set them on the element
4
4
  * or any ancestor, or pass them as `theme`). Light and dark follow the
5
5
  * reader's system. Everything to be tapped is at least 44px.
6
- * @example
6
+ * @example
7
7
  * ```ts
8
8
  * import { KUMIMOJI_STYLE } from "@johnmorrisdotca/kumimoji/ui";
9
9
  * console.log(KUMIMOJI_STYLE.includes(".km-root")); // true
package/dist/ui.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * (`mountKumimoji`), and the window a board shows the grid through
4
4
  * (`boardModel`). Its own entry, so the rules never carry a stylesheet.
5
5
  */
6
- export { mountKumimoji, type KumimojiTableHandle, type KumimojiTableOptions } from "./ui/mount.ts";
7
- export { BOARD_LEAST, BOARD_MARGIN, boardModel, type BoardModel, type BoardSquare, type TileMark } from "./ui/board.ts";
6
+ export { KUMIMOJI_STRINGS, type KumimojiLocale, type KumimojiStrings, kumimojiSay, kumimojiStrings, } from "./strings.ts";
7
+ export { BOARD_LEAST, BOARD_MARGIN, type BoardModel, type BoardSquare, boardModel, type TileMark } from "./ui/board.ts";
8
+ export { type KumimojiTableHandle, type KumimojiTableOptions, mountKumimoji } from "./ui/mount.ts";
8
9
  export { KUMIMOJI_STYLE } from "./ui/style.ts";
9
- export { KUMIMOJI_STRINGS, kumimojiSay, kumimojiStrings, type KumimojiLocale, type KumimojiStrings } from "./strings.ts";
package/dist/ui.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * (`mountKumimoji`), and the window a board shows the grid through
4
4
  * (`boardModel`). Its own entry, so the rules never carry a stylesheet.
5
5
  */
6
- export { mountKumimoji } from "./ui/mount.js";
6
+ export { KUMIMOJI_STRINGS, kumimojiSay, kumimojiStrings, } from "./strings.js";
7
7
  export { BOARD_LEAST, BOARD_MARGIN, boardModel } from "./ui/board.js";
8
+ export { mountKumimoji } from "./ui/mount.js";
8
9
  export { KUMIMOJI_STYLE } from "./ui/style.js";
9
- export { KUMIMOJI_STRINGS, kumimojiSay, kumimojiStrings } from "./strings.js";
package/dist/version.d.ts CHANGED
@@ -6,4 +6,4 @@
6
6
  * console.log(KUMIMOJI_VERSION);
7
7
  * ```
8
8
  */
9
- export declare const KUMIMOJI_VERSION = "1.3.0";
9
+ export declare const KUMIMOJI_VERSION = "1.3.1";