@johnmorrisdotca/kumimoji 1.3.0 → 1.4.0
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 +22 -1
- package/CONTRIBUTING.md +11 -2
- package/README.md +9 -16
- package/dist/check-reasons.d.ts +71 -0
- package/dist/check-reasons.js +47 -0
- package/dist/check.js +14 -11
- package/dist/computerPlay.js +11 -4
- package/dist/computerTurn.js +5 -2
- package/dist/daily.js +1 -1
- package/dist/element.d.ts +4 -1
- package/dist/element.js +332 -162
- package/dist/export.js +18 -5
- package/dist/generate.js +18 -5
- package/dist/grid.js +26 -4
- package/dist/index.d.ts +23 -22
- package/dist/index.js +23 -22
- package/dist/kana.js +39 -6
- package/dist/kumimoji.types.d.ts +5 -2
- 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/cloth.d.ts +72 -0
- package/dist/ui/cloth.js +49 -0
- package/dist/ui/draw.d.ts +62 -0
- package/dist/ui/draw.js +229 -0
- package/dist/ui/mount.d.ts +4 -1
- package/dist/ui/mount.js +176 -279
- package/dist/ui/style.d.ts +2 -2
- package/dist/ui/style.js +2 -1
- package/dist/ui.d.ts +4 -3
- package/dist/ui.js +3 -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,13 @@
|
|
|
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 { clothVars, isCloth } from "./cloth.js";
|
|
9
|
+
import { button, node, tableDrawing } from "./draw.js";
|
|
10
10
|
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
11
|
/** Hand a string to the person as a file of that name. */
|
|
30
12
|
function save(name, type, text) {
|
|
31
13
|
const url = URL.createObjectURL(new Blob([text], { type }));
|
|
@@ -60,29 +42,56 @@ function save(name, type, text) {
|
|
|
60
42
|
* ```
|
|
61
43
|
*/
|
|
62
44
|
export function mountKumimoji(target, options = {}) {
|
|
63
|
-
let
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
45
|
+
let current = options;
|
|
46
|
+
let settings = {
|
|
47
|
+
language: current.language,
|
|
48
|
+
hand: current.hand,
|
|
49
|
+
level: current.level,
|
|
50
|
+
gameLength: current.gameLength,
|
|
51
|
+
diagonals: current.diagonals,
|
|
52
|
+
seed: current.seed,
|
|
53
|
+
};
|
|
54
|
+
const pageLocale = () => typeof document !== "undefined" && document.documentElement.lang.toLowerCase().startsWith("ja") ? "ja" : "en";
|
|
55
|
+
let own = current.strings ?? {};
|
|
56
|
+
const openingLocale = current.locale ?? pageLocale();
|
|
73
57
|
// 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
|
-
|
|
58
|
+
const session = {
|
|
59
|
+
play: null,
|
|
60
|
+
finishedAt: null,
|
|
61
|
+
started: 0,
|
|
62
|
+
words: null,
|
|
63
|
+
held: null,
|
|
64
|
+
onSquare: null,
|
|
65
|
+
onHand: 0,
|
|
66
|
+
say: kumimojiStrings(openingLocale, own),
|
|
67
|
+
locale: openingLocale,
|
|
68
|
+
note: "",
|
|
69
|
+
destroyed: false,
|
|
70
|
+
};
|
|
71
|
+
let dealt = null;
|
|
79
72
|
// Each deal and each game put on the table takes a number; one that is overtaken while its list loads gives way.
|
|
80
73
|
let asked = 0;
|
|
74
|
+
// The drawing is built once the elements exist. Until then nothing calls these.
|
|
75
|
+
const bridge = {
|
|
76
|
+
render: () => { },
|
|
77
|
+
renderStatus: () => { },
|
|
78
|
+
newGame: () => { },
|
|
79
|
+
};
|
|
80
|
+
const render = () => bridge.render();
|
|
81
81
|
const root = node("div", "km-root");
|
|
82
82
|
root.dataset.testid = "km-root";
|
|
83
83
|
root.dataset.state = "dealing";
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
const wear = (cloth, theme) => {
|
|
85
|
+
for (const name of Object.keys(clothVars("green")))
|
|
86
|
+
root.style.removeProperty(name);
|
|
87
|
+
for (const [name, value] of Object.entries({ ...clothVars(cloth), ...theme }))
|
|
88
|
+
root.style.setProperty(name, value);
|
|
89
|
+
if (isCloth(cloth))
|
|
90
|
+
root.dataset.cloth = cloth;
|
|
91
|
+
else
|
|
92
|
+
delete root.dataset.cloth;
|
|
93
|
+
};
|
|
94
|
+
wear(current.cloth, current.theme ?? {});
|
|
86
95
|
const style = node("style");
|
|
87
96
|
style.textContent = KUMIMOJI_STYLE;
|
|
88
97
|
const status = node("p", "km-status");
|
|
@@ -99,15 +108,20 @@ export function mountKumimoji(target, options = {}) {
|
|
|
99
108
|
const picker = node("div", "km-picker");
|
|
100
109
|
picker.dataset.testid = "km-picker";
|
|
101
110
|
const controls = node("div", "km-controls");
|
|
102
|
-
controls.hidden =
|
|
111
|
+
controls.hidden = current.controls === false;
|
|
103
112
|
controls.dataset.testid = "km-controls";
|
|
104
113
|
const keep = node("details", "km-keep");
|
|
105
114
|
keep.dataset.testid = "km-keep";
|
|
106
115
|
root.append(style, status, board, notes, picker, hand, controls);
|
|
107
|
-
if (
|
|
116
|
+
if (current.keep !== false)
|
|
108
117
|
root.append(keep);
|
|
109
118
|
target.append(root);
|
|
110
|
-
const savedNow = () =>
|
|
119
|
+
const savedNow = () => {
|
|
120
|
+
const now = session.play;
|
|
121
|
+
if (now === null || dealt === null)
|
|
122
|
+
return null;
|
|
123
|
+
return { deal: dealt, play: now, elapsedMs: (session.finishedAt ?? Date.now()) - session.started };
|
|
124
|
+
};
|
|
111
125
|
// The saving and loading are made once, so the fold stays open or shut as the person left it; only their words change.
|
|
112
126
|
const keepTitle = node("summary", "km-keep-title");
|
|
113
127
|
const keepNote = node("p", "km-note");
|
|
@@ -120,15 +134,15 @@ export function mountKumimoji(target, options = {}) {
|
|
|
120
134
|
file.dataset.testid = "km-file";
|
|
121
135
|
const read = async (text) => {
|
|
122
136
|
const loaded = await kumimojiFromJSON(text).catch(() => null);
|
|
123
|
-
if (destroyed)
|
|
137
|
+
if (session.destroyed)
|
|
124
138
|
return;
|
|
125
139
|
if (loaded === null) {
|
|
126
|
-
note = say.loadBad;
|
|
140
|
+
session.note = session.say.loadBad;
|
|
127
141
|
render();
|
|
128
142
|
return;
|
|
129
143
|
}
|
|
130
144
|
await handle.setGame(loaded);
|
|
131
|
-
note = say.loaded;
|
|
145
|
+
session.note = session.say.loaded;
|
|
132
146
|
render();
|
|
133
147
|
};
|
|
134
148
|
file.addEventListener("change", () => {
|
|
@@ -146,60 +160,59 @@ export function mountKumimoji(target, options = {}) {
|
|
|
146
160
|
const saveText = button("", () => {
|
|
147
161
|
const now = savedNow();
|
|
148
162
|
if (now !== null)
|
|
149
|
-
save(`kumimoji-${now.deal.seed}.txt`, "text/plain", kumimojiToText(now, say));
|
|
163
|
+
save(`kumimoji-${now.deal.seed}.txt`, "text/plain", kumimojiToText(now, session.say));
|
|
150
164
|
}, false, "km-save-text");
|
|
151
165
|
const load = button("", () => file.click(), false, "km-load");
|
|
152
166
|
const keepButtons = node("div", "km-controls");
|
|
153
167
|
keepButtons.append(saveJson, saveText, load);
|
|
154
168
|
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 });
|
|
169
|
+
const verdictOf = (now) => judgeWithWords(now.tiles, session.words, { diagonals: settings.diagonals === true });
|
|
160
170
|
const change = (next) => {
|
|
161
|
-
play = next;
|
|
171
|
+
session.play = next;
|
|
162
172
|
const verdict = verdictOf(next);
|
|
163
|
-
if (finishedAt === null && isFinished(next, verdict)) {
|
|
164
|
-
finishedAt = Date.now();
|
|
165
|
-
|
|
173
|
+
if (session.finishedAt === null && isFinished(next, verdict)) {
|
|
174
|
+
session.finishedAt = Date.now();
|
|
175
|
+
current.onFinish?.({ deal: dealt, play: next, elapsedMs: session.finishedAt - session.started });
|
|
166
176
|
}
|
|
167
|
-
note = "";
|
|
177
|
+
session.note = "";
|
|
168
178
|
const now = savedNow();
|
|
169
179
|
if (now !== null)
|
|
170
|
-
|
|
180
|
+
current.onChange?.(now);
|
|
171
181
|
render();
|
|
172
182
|
};
|
|
173
183
|
const tapSquare = (square) => {
|
|
174
|
-
|
|
184
|
+
const now = session.play;
|
|
185
|
+
if (now === null || session.finishedAt !== null)
|
|
175
186
|
return;
|
|
176
|
-
const there =
|
|
187
|
+
const there = now.tiles.get(square);
|
|
188
|
+
const held = session.held;
|
|
177
189
|
if (held === null) {
|
|
178
190
|
if (there !== undefined)
|
|
179
|
-
held = { from: "table", square };
|
|
191
|
+
session.held = { from: "table", square };
|
|
180
192
|
render();
|
|
181
193
|
return;
|
|
182
194
|
}
|
|
183
195
|
if (held.from === "hand") {
|
|
184
196
|
const at = held.at;
|
|
185
|
-
const tile =
|
|
197
|
+
const tile = now.hand[at];
|
|
186
198
|
if (tile === undefined)
|
|
187
199
|
return;
|
|
188
200
|
// A wild is given a letter before it is laid.
|
|
189
201
|
if (tile === "*")
|
|
190
202
|
return;
|
|
191
|
-
held = null;
|
|
192
|
-
change(there === undefined ? placeFromHand(
|
|
203
|
+
session.held = null;
|
|
204
|
+
change(there === undefined ? placeFromHand(now, at, square) : swapWithHand(now, at, square));
|
|
193
205
|
return;
|
|
194
206
|
}
|
|
195
207
|
const from = held.square;
|
|
196
|
-
held = null;
|
|
197
|
-
change(from === square ? liftToHand(
|
|
208
|
+
session.held = null;
|
|
209
|
+
change(from === square ? liftToHand(now, square) : moveOnTable(now, from, square));
|
|
198
210
|
};
|
|
199
211
|
const tapHand = (at) => {
|
|
200
|
-
if (play === null || finishedAt !== null)
|
|
212
|
+
if (session.play === null || session.finishedAt !== null)
|
|
201
213
|
return;
|
|
202
|
-
held = held
|
|
214
|
+
const held = session.held;
|
|
215
|
+
session.held = held?.from === "hand" && held.at === at ? null : { from: "hand", at };
|
|
203
216
|
render();
|
|
204
217
|
};
|
|
205
218
|
board.addEventListener("click", (event) => {
|
|
@@ -222,7 +235,23 @@ export function mountKumimoji(target, options = {}) {
|
|
|
222
235
|
const cells = [...grid.children];
|
|
223
236
|
const cols = Number(grid.dataset.cols);
|
|
224
237
|
const at = cells.indexOf(cell);
|
|
225
|
-
const step = event.key === "ArrowLeft"
|
|
238
|
+
const step = event.key === "ArrowLeft"
|
|
239
|
+
? at % cols > 0
|
|
240
|
+
? -1
|
|
241
|
+
: 0
|
|
242
|
+
: event.key === "ArrowRight"
|
|
243
|
+
? at % cols < cols - 1
|
|
244
|
+
? 1
|
|
245
|
+
: 0
|
|
246
|
+
: event.key === "ArrowUp"
|
|
247
|
+
? at - cols >= 0
|
|
248
|
+
? -cols
|
|
249
|
+
: 0
|
|
250
|
+
: event.key === "ArrowDown"
|
|
251
|
+
? at + cols < cells.length
|
|
252
|
+
? cols
|
|
253
|
+
: 0
|
|
254
|
+
: null;
|
|
226
255
|
if (step === null)
|
|
227
256
|
return;
|
|
228
257
|
event.preventDefault();
|
|
@@ -231,18 +260,18 @@ export function mountKumimoji(target, options = {}) {
|
|
|
231
260
|
return;
|
|
232
261
|
cell.tabIndex = -1;
|
|
233
262
|
next.tabIndex = 0;
|
|
234
|
-
onSquare = next.dataset.square ?? null;
|
|
263
|
+
session.onSquare = next.dataset.square ?? null;
|
|
235
264
|
next.focus();
|
|
236
265
|
});
|
|
237
266
|
board.addEventListener("focusin", (event) => {
|
|
238
267
|
const square = event.target.closest("[data-square]")?.getAttribute("data-square");
|
|
239
268
|
if (square !== null && square !== undefined)
|
|
240
|
-
onSquare = square;
|
|
269
|
+
session.onSquare = square;
|
|
241
270
|
});
|
|
242
271
|
hand.addEventListener("focusin", (event) => {
|
|
243
272
|
const at = event.target.closest("[data-hand]")?.getAttribute("data-hand");
|
|
244
273
|
if (at !== null && at !== undefined)
|
|
245
|
-
onHand = Number(at);
|
|
274
|
+
session.onHand = Number(at);
|
|
246
275
|
});
|
|
247
276
|
hand.addEventListener("keydown", (event) => {
|
|
248
277
|
const tile = event.target.closest("[data-hand]");
|
|
@@ -250,7 +279,15 @@ export function mountKumimoji(target, options = {}) {
|
|
|
250
279
|
return;
|
|
251
280
|
const tiles = [...hand.querySelectorAll("[data-hand]")];
|
|
252
281
|
const at = tiles.indexOf(tile);
|
|
253
|
-
const to = event.key === "ArrowLeft"
|
|
282
|
+
const to = event.key === "ArrowLeft"
|
|
283
|
+
? at - 1
|
|
284
|
+
: event.key === "ArrowRight"
|
|
285
|
+
? at + 1
|
|
286
|
+
: event.key === "Home"
|
|
287
|
+
? 0
|
|
288
|
+
: event.key === "End"
|
|
289
|
+
? tiles.length - 1
|
|
290
|
+
: null;
|
|
254
291
|
if (to === null)
|
|
255
292
|
return;
|
|
256
293
|
event.preventDefault();
|
|
@@ -259,234 +296,82 @@ export function mountKumimoji(target, options = {}) {
|
|
|
259
296
|
return;
|
|
260
297
|
tile.tabIndex = -1;
|
|
261
298
|
next.tabIndex = 0;
|
|
262
|
-
onHand = to;
|
|
299
|
+
session.onHand = to;
|
|
263
300
|
next.focus();
|
|
264
301
|
});
|
|
265
302
|
root.addEventListener("keydown", (event) => {
|
|
266
|
-
if (event.key !== "Escape" || held === null)
|
|
303
|
+
if (event.key !== "Escape" || session.held === null)
|
|
267
304
|
return;
|
|
268
|
-
held = null;
|
|
305
|
+
session.held = null;
|
|
269
306
|
render();
|
|
270
307
|
});
|
|
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
|
-
}
|
|
308
|
+
const drawing = tableDrawing(session, { root, status, board, notes, hand, picker, controls, keepTitle, saveJson, saveText, load, keepNote }, { verdictOf, change, newGame: () => bridge.newGame() });
|
|
309
|
+
bridge.render = () => drawing.render();
|
|
310
|
+
bridge.renderStatus = () => drawing.renderStatus();
|
|
311
|
+
const tick = setInterval(() => {
|
|
312
|
+
if (session.play !== null && session.finishedAt === null)
|
|
313
|
+
bridge.renderStatus();
|
|
314
|
+
}, 1000);
|
|
438
315
|
let initialReady = Promise.resolve();
|
|
439
316
|
const handle = {
|
|
440
|
-
get ready() {
|
|
317
|
+
get ready() {
|
|
318
|
+
return initialReady;
|
|
319
|
+
},
|
|
441
320
|
set: (changes) => {
|
|
442
|
-
for (const name of Object.keys(
|
|
321
|
+
for (const name of Object.keys(current.theme ?? {}))
|
|
443
322
|
if (changes.theme !== undefined && !(name in changes.theme))
|
|
444
323
|
root.style.removeProperty(name);
|
|
445
|
-
if (changes.theme !== undefined)
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
options = { ...options, ...changes };
|
|
324
|
+
if (changes.theme !== undefined || "cloth" in changes)
|
|
325
|
+
wear("cloth" in changes ? changes.cloth : current.cloth, changes.theme ?? current.theme ?? {});
|
|
326
|
+
current = { ...current, ...changes };
|
|
449
327
|
if (changes.locale !== undefined)
|
|
450
|
-
locale = changes.locale;
|
|
328
|
+
session.locale = changes.locale;
|
|
451
329
|
if (changes.strings !== undefined)
|
|
452
330
|
own = changes.strings;
|
|
453
|
-
say = kumimojiStrings(locale, own);
|
|
454
|
-
if (
|
|
331
|
+
session.say = kumimojiStrings(session.locale, own);
|
|
332
|
+
if (current.keep === false)
|
|
455
333
|
keep.remove();
|
|
456
334
|
else if (keep.parentNode !== root)
|
|
457
335
|
root.append(keep);
|
|
458
|
-
controls.hidden =
|
|
336
|
+
controls.hidden = current.controls === false;
|
|
459
337
|
render();
|
|
460
338
|
},
|
|
461
|
-
play: () => play,
|
|
339
|
+
play: () => session.play,
|
|
462
340
|
saved: savedNow,
|
|
463
341
|
setGame: async (saved) => {
|
|
464
342
|
const mine = ++asked;
|
|
465
343
|
const language = saved.deal.language;
|
|
466
|
-
settings = {
|
|
344
|
+
settings = {
|
|
345
|
+
language,
|
|
346
|
+
hand: saved.deal.size,
|
|
347
|
+
level: saved.deal.level,
|
|
348
|
+
gameLength: saved.deal.gameLength,
|
|
349
|
+
diagonals: saved.deal.diagonals === true,
|
|
350
|
+
seed: undefined,
|
|
351
|
+
};
|
|
467
352
|
const list = await loadTileWords(language);
|
|
468
|
-
if (destroyed || mine !== asked)
|
|
353
|
+
if (session.destroyed || mine !== asked)
|
|
469
354
|
return;
|
|
470
|
-
words = list;
|
|
355
|
+
session.words = list;
|
|
471
356
|
root.classList.toggle("km-japanese", language === "japanese");
|
|
472
357
|
dealt = saved.deal;
|
|
473
358
|
root.dataset.seed = String(saved.deal.seed);
|
|
474
|
-
held = null;
|
|
475
|
-
finishedAt = null;
|
|
476
|
-
started = Date.now() - saved.elapsedMs;
|
|
359
|
+
session.held = null;
|
|
360
|
+
session.finishedAt = null;
|
|
361
|
+
session.started = Date.now() - saved.elapsedMs;
|
|
477
362
|
// 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
|
-
|
|
363
|
+
if (isFinished(saved.play, judgeWithWords(saved.play.tiles, list, { diagonals: settings.diagonals === true })))
|
|
364
|
+
session.finishedAt = Date.now();
|
|
365
|
+
session.play = saved.play;
|
|
366
|
+
session.note = "";
|
|
367
|
+
current.onChange?.(saved);
|
|
483
368
|
render();
|
|
484
369
|
},
|
|
485
370
|
setLocale: (next, strings) => {
|
|
486
|
-
locale = next;
|
|
371
|
+
session.locale = next;
|
|
487
372
|
if (strings !== undefined)
|
|
488
373
|
own = strings;
|
|
489
|
-
say = kumimojiStrings(locale, own);
|
|
374
|
+
session.say = kumimojiStrings(session.locale, own);
|
|
490
375
|
render();
|
|
491
376
|
},
|
|
492
377
|
newGame: async (changed = {}) => {
|
|
@@ -494,31 +379,43 @@ export function mountKumimoji(target, options = {}) {
|
|
|
494
379
|
settings = { ...settings, ...changed, seed: undefined };
|
|
495
380
|
const language = settings.language ?? "english";
|
|
496
381
|
const size = settings.hand ?? KUMIMOJI_HANDS.quick;
|
|
497
|
-
play = null;
|
|
498
|
-
held = null;
|
|
499
|
-
finishedAt = null;
|
|
500
|
-
note = "";
|
|
382
|
+
session.play = null;
|
|
383
|
+
session.held = null;
|
|
384
|
+
session.finishedAt = null;
|
|
385
|
+
session.note = "";
|
|
501
386
|
render();
|
|
502
387
|
const list = await loadTileWords(language);
|
|
503
|
-
if (destroyed || mine !== asked)
|
|
388
|
+
if (session.destroyed || mine !== asked)
|
|
504
389
|
return;
|
|
505
|
-
words = list;
|
|
390
|
+
session.words = list;
|
|
506
391
|
// A seed deals one game: the next is dealt from a new one unless asked for by seed again.
|
|
507
392
|
const seed = changed.seed ?? Math.floor(Math.random() * 2 ** 31);
|
|
508
393
|
root.classList.toggle("km-japanese", language === "japanese");
|
|
509
394
|
root.dataset.seed = String(seed);
|
|
510
|
-
dealt = generateKumimoji(size, settings.level ?? "medium", seed, {
|
|
511
|
-
|
|
395
|
+
dealt = generateKumimoji(size, settings.level ?? "medium", seed, {
|
|
396
|
+
language,
|
|
397
|
+
gameLength: settings.gameLength,
|
|
398
|
+
diagonals: settings.diagonals,
|
|
399
|
+
});
|
|
400
|
+
session.started = Date.now();
|
|
512
401
|
change(deal(dealt.givens, size));
|
|
513
402
|
},
|
|
514
403
|
destroy: () => {
|
|
515
|
-
destroyed = true;
|
|
404
|
+
session.destroyed = true;
|
|
516
405
|
clearInterval(tick);
|
|
517
406
|
root.remove();
|
|
518
407
|
},
|
|
519
408
|
};
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
409
|
+
bridge.newGame = () => {
|
|
410
|
+
void handle.newGame();
|
|
411
|
+
};
|
|
412
|
+
initialReady = current.saved === undefined ? handle.newGame(settings) : handle.setGame(current.saved);
|
|
413
|
+
void initialReady.catch((error) => {
|
|
414
|
+
if (session.destroyed)
|
|
415
|
+
return;
|
|
416
|
+
const problem = error instanceof Error ? error : new Error(String(error));
|
|
417
|
+
status.textContent = session.say.loadError;
|
|
418
|
+
current.onError?.(problem);
|
|
419
|
+
});
|
|
523
420
|
return handle;
|
|
524
421
|
}
|
package/dist/ui/style.d.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
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
|
|
10
10
|
* ```
|
|
11
11
|
*/
|
|
12
|
-
export declare const KUMIMOJI_STYLE = "\n.km-root {\n --km-square: 44px; --km-felt: #2f5d4a; --km-grid-line: rgba(255,255,255,.12); --km-tile: #f3e6c8; --km-tile-ink: #2a2118;\n --km-wild: #f6d27a; --km-wrong: #d9534f; --km-held: #ffcf3f; --km-ink: #1f2320; --km-panel: #f7f3ea;\n --km-line: rgba(20,20,20,.35); --km-accent: #2f5d4a; --km-accent-ink: #fff;\n --km-radius: 12px; --km-font: system-ui, -apple-system, \"Segoe UI\", sans-serif;\n display: grid; gap: 10px; min-width: 0; color: var(--km-ink); font-family: var(--km-font); font-size: 15px;\n}\n@media (prefers-color-scheme: dark) {\n .km-root { --km-felt: #1e3a2f; --km-ink: #ece8dc; --km-panel: #1d201e; --km-line: rgba(255,255,255,.25); --km-accent: #6fb08f; --km-accent-ink: #10150f; }\n}\n.km-root > * { min-width: 0; }\n.km-status { margin: 0; min-height: 1.4em; font-variant-numeric: tabular-nums; }\n.km-board { background: var(--km-felt); border-radius: var(--km-radius); padding: 10px; overflow: auto; max-height: 70vh; }\n.km-grid { display: grid; gap: 2px; width: max-content; margin: 0 auto; }\n.km-square { width: var(--km-square); height: var(--km-square); border: 0; border-radius: 5px; background: var(--km-grid-line); padding: 0; cursor: pointer; touch-action: manipulation; }\n.km-tile { display: grid; place-items: center; width: var(--km-square); height: var(--km-square); border-radius: 6px; border: 0; padding: 0;\n background: var(--km-tile); color: var(--km-tile-ink); font: 700 calc(var(--km-square) * .55)/1 var(--km-font); text-transform: uppercase;\n box-shadow: 0 2px 0 rgba(0,0,0,.35); cursor: pointer; touch-action: manipulation; }\n.km-japanese .km-tile { font-size: calc(var(--km-square) * .52); text-transform: none; }\n.km-wild { background: var(--km-wild); }\n.km-misspelt { box-shadow: 0 0 0 3px var(--km-wrong) inset, 0 2px 0 rgba(0,0,0,.35); }\n.km-apart { opacity: .6; }\n.km-held { outline: 3px solid var(--km-held); outline-offset: 1px; }\n.km-square:focus-visible, .km-in-hand:focus-visible, .km-button:focus-visible, .km-keep-title:focus-visible { outline: 3px solid var(--km-held); outline-offset: 2px; }\n.km-notes { margin: 0; min-height: 1.4em; font-size: 14px; opacity: .8; overflow-wrap: anywhere; }\n.km-hand { display: flex; flex-wrap: wrap; gap: 6px; min-height: calc(var(--km-square) + 16px); align-items: center; padding: 8px; border-radius: 10px; background: var(--km-panel); }\n.km-empty { opacity: .6; font-size: 14px; }\n.km-picker { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }\n.km-picker:empty { display: none; }\n.km-picker-title { width: 100%; font-size: 14px; }\n.km-picker .km-button { padding: 0 8px; text-transform: uppercase; }\n.km-japanese .km-picker .km-button { text-transform: none; }\n.km-controls[hidden] { display: none; }\n.km-controls { display: flex; flex-wrap: wrap; gap: 8px; }\n.km-button { font: inherit; min-height: 44px; min-width: 44px; padding: 0 14px; border-radius: 8px; border: 1px solid var(--km-line); background: var(--km-panel); color: var(--km-ink); cursor: pointer; touch-action: manipulation; }\n.km-button:disabled { opacity: .45; cursor: default; }\n.km-primary { background: var(--km-accent); color: var(--km-accent-ink); border-color: transparent; }\n.km-keep { border-radius: 10px; background: var(--km-panel); padding: 0 10px; }\n.km-keep[open] { padding-bottom: 10px; }\n.km-keep-title { min-height: 44px; display: flex; align-items: center; font-weight: 600; cursor: pointer; }\n.km-note { margin: 8px 0 0; min-height: 1.4em; font-size: 13px; }\n";
|
|
12
|
+
export declare const KUMIMOJI_STYLE = "\n.km-root {\n --km-square: 44px; --km-felt: #2f5d4a; --km-grid-line: rgba(255,255,255,.12); --km-tile: #f3e6c8; --km-tile-ink: #2a2118;\n --km-wild: #f6d27a; --km-wrong: #d9534f; --km-held: #ffcf3f; --km-ink: #1f2320; --km-panel: #f7f3ea;\n --km-line: rgba(20,20,20,.35); --km-accent: #2f5d4a; --km-accent-ink: #fff;\n --km-radius: 12px; --km-font: system-ui, -apple-system, \"Segoe UI\", sans-serif;\n display: grid; gap: 10px; min-width: 0; color: var(--km-ink); font-family: var(--km-font); font-size: 15px;\n}\n@media (prefers-color-scheme: dark) {\n .km-root { --km-felt: #1e3a2f; --km-ink: #ece8dc; --km-panel: #1d201e; --km-line: rgba(255,255,255,.25); --km-accent: #6fb08f; --km-accent-ink: #10150f; }\n}\n.km-root > * { min-width: 0; }\n.km-status { margin: 0; min-height: 1.4em; font-variant-numeric: tabular-nums; }\n.km-root, .km-root * { line-height: 1.35; }\n.km-board { background: var(--km-felt); border-radius: var(--km-radius); padding: 10px; overflow: auto; max-height: 70vh; }\n.km-grid { display: grid; gap: 2px; width: max-content; margin: 0 auto; }\n.km-square { width: var(--km-square); height: var(--km-square); border: 0; border-radius: 5px; background: var(--km-grid-line); padding: 0; cursor: pointer; touch-action: manipulation; }\n.km-tile { display: grid; place-items: center; width: var(--km-square); height: var(--km-square); border-radius: 6px; border: 0; padding: 0;\n background: var(--km-tile); color: var(--km-tile-ink); font: 700 calc(var(--km-square) * .55)/1 var(--km-font); text-transform: uppercase;\n box-shadow: 0 2px 0 rgba(0,0,0,.35); cursor: pointer; touch-action: manipulation; }\n.km-japanese .km-tile { font-size: calc(var(--km-square) * .52); text-transform: none; }\n.km-wild { background: var(--km-wild); }\n.km-misspelt { box-shadow: 0 0 0 3px var(--km-wrong) inset, 0 2px 0 rgba(0,0,0,.35); }\n.km-apart { opacity: .6; }\n.km-held { outline: 3px solid var(--km-held); outline-offset: 1px; }\n.km-square:focus-visible, .km-in-hand:focus-visible, .km-button:focus-visible, .km-keep-title:focus-visible { outline: 3px solid var(--km-held); outline-offset: 2px; }\n.km-notes { margin: 0; min-height: 1.4em; font-size: 14px; opacity: .8; overflow-wrap: anywhere; }\n.km-hand { display: flex; flex-wrap: wrap; gap: 6px; min-height: calc(var(--km-square) + 16px); align-items: center; padding: 8px; border-radius: 10px; background: var(--km-panel); }\n.km-empty { opacity: .6; font-size: 14px; }\n.km-picker { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }\n.km-picker:empty { display: none; }\n.km-picker-title { width: 100%; font-size: 14px; }\n.km-picker .km-button { padding: 0 8px; text-transform: uppercase; }\n.km-japanese .km-picker .km-button { text-transform: none; }\n.km-controls[hidden] { display: none; }\n.km-controls { display: flex; flex-wrap: wrap; gap: 8px; }\n.km-button { font: inherit; min-height: 44px; min-width: 44px; padding: 0 14px; border-radius: 8px; border: 1px solid var(--km-line); background: var(--km-panel); color: var(--km-ink); cursor: pointer; touch-action: manipulation; }\n.km-button:disabled { opacity: .45; cursor: default; }\n.km-primary { background: var(--km-accent); color: var(--km-accent-ink); border-color: transparent; }\n.km-keep { border-radius: 10px; background: var(--km-panel); padding: 0 10px; }\n.km-keep[open] { padding-bottom: 10px; }\n.km-keep-title { min-height: 44px; display: flex; align-items: center; font-weight: 600; cursor: pointer; }\n.km-note { margin: 8px 0 0; min-height: 1.4em; font-size: 13px; }\n";
|