@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/CHANGELOG.md +12 -0
- package/CONTRIBUTING.md +11 -2
- package/README.md +2 -2
- package/dist/check.js +4 -2
- package/dist/computerPlay.js +11 -4
- package/dist/computerTurn.js +5 -2
- package/dist/daily.js +1 -1
- package/dist/element.d.ts +1 -1
- package/dist/element.js +313 -162
- package/dist/export.js +18 -5
- package/dist/generate.js +18 -5
- package/dist/grid.js +26 -4
- package/dist/index.d.ts +22 -22
- package/dist/index.js +22 -22
- package/dist/kana.js +39 -6
- package/dist/party.js +28 -5
- package/dist/partyKept.js +53 -8
- package/dist/partySeats.js +11 -4
- package/dist/partyTurns.js +16 -3
- package/dist/placement.js +5 -2
- package/dist/play.js +14 -3
- package/dist/react.js +33 -3
- package/dist/showcase.js +12 -3
- package/dist/tileWords.js +11 -6
- package/dist/tileWordsModule.d.ts +1 -1
- package/dist/tileWordsModule.js +3 -1
- package/dist/tiles.constants.js +72 -8
- package/dist/turn.js +6 -1
- package/dist/ui/board.js +16 -2
- package/dist/ui/draw.d.ts +62 -0
- package/dist/ui/draw.js +229 -0
- package/dist/ui/mount.js +163 -275
- package/dist/ui/style.d.ts +1 -1
- package/dist/ui/style.js +1 -1
- package/dist/ui.d.ts +3 -3
- package/dist/ui.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/wilds.js +3 -2
- package/package.json +12 -13
package/dist/ui/mount.js
CHANGED
|
@@ -1,31 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { kumimojiFromJSON, kumimojiToJSON, kumimojiToText } from "../export.js";
|
|
2
2
|
import { generateKumimoji } from "../generate.js";
|
|
3
3
|
import { judgeWithWords } from "../judge.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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 {
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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(
|
|
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 =
|
|
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 (
|
|
106
|
+
if (current.keep !== false)
|
|
108
107
|
root.append(keep);
|
|
109
108
|
target.append(root);
|
|
110
|
-
const savedNow = () =>
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
170
|
+
current.onChange?.(now);
|
|
171
171
|
render();
|
|
172
172
|
};
|
|
173
173
|
const tapSquare = (square) => {
|
|
174
|
-
|
|
174
|
+
const now = session.play;
|
|
175
|
+
if (now === null || session.finishedAt !== null)
|
|
175
176
|
return;
|
|
176
|
-
const there =
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
|
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"
|
|
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"
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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() {
|
|
307
|
+
get ready() {
|
|
308
|
+
return initialReady;
|
|
309
|
+
},
|
|
441
310
|
set: (changes) => {
|
|
442
|
-
for (const name of Object.keys(
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
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 = {
|
|
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,
|
|
479
|
-
finishedAt = Date.now();
|
|
480
|
-
play = saved.play;
|
|
481
|
-
note = "";
|
|
482
|
-
|
|
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, {
|
|
511
|
-
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
}
|
package/dist/ui/style.d.ts
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
|
-
|
|
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
|
-
|
|
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 {
|
|
7
|
-
export { BOARD_LEAST, BOARD_MARGIN,
|
|
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 {
|
|
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