@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/tileWords.js
CHANGED
|
@@ -51,7 +51,7 @@ export function unpackTileWords(data) {
|
|
|
51
51
|
}
|
|
52
52
|
const letters = [...Object.keys(TILE_MIX)];
|
|
53
53
|
const assigned = new Map(letters.map((letter) => [letter.toUpperCase(), letter]));
|
|
54
|
-
const soundOf = (tile) =>
|
|
54
|
+
const soundOf = (tile) => tile === "*" ? null : (assigned.get(tile) ?? (/^[a-z]$/.test(tile) ? tile : null));
|
|
55
55
|
const isWild = (tile) => tile === "*" || assigned.has(tile);
|
|
56
56
|
return {
|
|
57
57
|
language: "english",
|
|
@@ -94,8 +94,12 @@ function unpackJapaneseWords(data) {
|
|
|
94
94
|
allowed.add([...word].map((tile) => tileFace(tile).glyph).join(""));
|
|
95
95
|
}
|
|
96
96
|
const mix = new Map(Object.entries(JAPANESE_TILE_MIX).map(([face, count]) => [kanaTileCode(face), count]));
|
|
97
|
-
const isTile = (tile) => tile.length === 1 &&
|
|
98
|
-
|
|
97
|
+
const isTile = (tile) => tile.length === 1 &&
|
|
98
|
+
tile.codePointAt(0) - KANA_TILE_START >= 0 &&
|
|
99
|
+
tile.codePointAt(0) - KANA_TILE_START < kana.length;
|
|
100
|
+
const isAssignedWild = (tile) => tile.length === 1 &&
|
|
101
|
+
tile.codePointAt(0) - KANA_WILD_START >= 0 &&
|
|
102
|
+
tile.codePointAt(0) - KANA_WILD_START < kana.length;
|
|
99
103
|
const isWild = (tile) => tile === "*" || isAssignedWild(tile);
|
|
100
104
|
const soundOf = (tile) => (isTile(tile) || isAssignedWild(tile) ? tileFace(tile).glyph : null);
|
|
101
105
|
const identity = (tile) => (isWild(tile) ? "*" : isTile(tile) ? tileFace(tile).glyph : null);
|
|
@@ -196,9 +200,10 @@ export async function loadTileWords(language = "english") {
|
|
|
196
200
|
return already;
|
|
197
201
|
let words;
|
|
198
202
|
if (typeof window !== "undefined") {
|
|
199
|
-
words =
|
|
200
|
-
|
|
201
|
-
|
|
203
|
+
words =
|
|
204
|
+
language === "english"
|
|
205
|
+
? unpackTileWords((await import("./words.en.data.js")).TILE_WORDS_EN)
|
|
206
|
+
: unpackJapaneseWords((await import("./words.ja.data.js")).TILE_WORDS_JA);
|
|
202
207
|
}
|
|
203
208
|
else {
|
|
204
209
|
if (fromModule === null)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type TileWords } from "./tileWords.ts";
|
|
2
1
|
import type { KumimojiLanguage } from "./kumimoji.types.ts";
|
|
2
|
+
import { type TileWords } from "./tileWords.ts";
|
|
3
3
|
/** The list, read from its module: `loadTileWords` for a caller with no browser.
|
|
4
4
|
*
|
|
5
5
|
* @param language - The English or Japanese tile language.
|
package/dist/tileWordsModule.js
CHANGED
|
@@ -6,7 +6,9 @@ import { loadTileWords, readTileWordsWith, tileWordsFrom } from "./tileWords.js"
|
|
|
6
6
|
* page's components never import it, so no page's server function carries the
|
|
7
7
|
* lists for the browser's sake (see `loadTileWords`).
|
|
8
8
|
*/
|
|
9
|
-
readTileWordsWith(async (language) => tileWordsFrom(language, language === "english"
|
|
9
|
+
readTileWordsWith(async (language) => tileWordsFrom(language, language === "english"
|
|
10
|
+
? (await import("./words.en.data.js")).TILE_WORDS_EN
|
|
11
|
+
: (await import("./words.ja.data.js")).TILE_WORDS_JA));
|
|
10
12
|
/** The list, read from its module: `loadTileWords` for a caller with no browser.
|
|
11
13
|
*
|
|
12
14
|
* @param language - The English or Japanese tile language.
|
package/dist/tiles.constants.js
CHANGED
|
@@ -21,8 +21,32 @@
|
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
23
|
export const TILE_MIX = {
|
|
24
|
-
a: 13,
|
|
25
|
-
|
|
24
|
+
a: 13,
|
|
25
|
+
b: 3,
|
|
26
|
+
c: 3,
|
|
27
|
+
d: 6,
|
|
28
|
+
e: 18,
|
|
29
|
+
f: 3,
|
|
30
|
+
g: 4,
|
|
31
|
+
h: 3,
|
|
32
|
+
i: 12,
|
|
33
|
+
j: 2,
|
|
34
|
+
k: 2,
|
|
35
|
+
l: 5,
|
|
36
|
+
m: 3,
|
|
37
|
+
n: 8,
|
|
38
|
+
o: 11,
|
|
39
|
+
p: 3,
|
|
40
|
+
q: 2,
|
|
41
|
+
r: 9,
|
|
42
|
+
s: 6,
|
|
43
|
+
t: 9,
|
|
44
|
+
u: 6,
|
|
45
|
+
v: 3,
|
|
46
|
+
w: 3,
|
|
47
|
+
x: 2,
|
|
48
|
+
y: 3,
|
|
49
|
+
z: 2,
|
|
26
50
|
};
|
|
27
51
|
/**
|
|
28
52
|
* THE JAPANESE SET: 144 hiragana tiles in 45 kinds (`kana.ts`), as the English
|
|
@@ -45,11 +69,51 @@ export const TILE_MIX = {
|
|
|
45
69
|
* ```
|
|
46
70
|
*/
|
|
47
71
|
export const JAPANESE_TILE_MIX = {
|
|
48
|
-
あ: 2,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
72
|
+
あ: 2,
|
|
73
|
+
い: 11,
|
|
74
|
+
う: 12,
|
|
75
|
+
え: 1,
|
|
76
|
+
お: 2,
|
|
77
|
+
か: 7,
|
|
78
|
+
き: 6,
|
|
79
|
+
く: 6,
|
|
80
|
+
け: 3,
|
|
81
|
+
こ: 4,
|
|
82
|
+
さ: 3,
|
|
83
|
+
し: 10,
|
|
84
|
+
す: 2,
|
|
85
|
+
せ: 3,
|
|
86
|
+
そ: 2,
|
|
87
|
+
た: 4,
|
|
88
|
+
ち: 3,
|
|
89
|
+
つ: 6,
|
|
90
|
+
て: 2,
|
|
91
|
+
と: 3,
|
|
92
|
+
な: 1,
|
|
93
|
+
に: 1,
|
|
94
|
+
ぬ: 1,
|
|
95
|
+
ね: 1,
|
|
96
|
+
の: 1,
|
|
97
|
+
は: 2,
|
|
98
|
+
ひ: 2,
|
|
99
|
+
ふ: 2,
|
|
100
|
+
へ: 1,
|
|
101
|
+
ほ: 1,
|
|
102
|
+
ま: 2,
|
|
103
|
+
み: 1,
|
|
104
|
+
む: 1,
|
|
105
|
+
め: 1,
|
|
106
|
+
も: 1,
|
|
107
|
+
や: 2,
|
|
108
|
+
ゆ: 4,
|
|
109
|
+
よ: 6,
|
|
110
|
+
ら: 1,
|
|
111
|
+
り: 3,
|
|
112
|
+
る: 3,
|
|
113
|
+
れ: 1,
|
|
114
|
+
ろ: 1,
|
|
115
|
+
わ: 1,
|
|
116
|
+
ん: 11,
|
|
53
117
|
};
|
|
54
118
|
/** The whole set, counted from the table.
|
|
55
119
|
*
|
|
@@ -203,7 +267,7 @@ export const KUMIMOJI_TRADE = { give: 1, take: 3 };
|
|
|
203
267
|
* console.log(KUMIMOJI_SCORE);
|
|
204
268
|
* ```
|
|
205
269
|
*/
|
|
206
|
-
export const KUMIMOJI_SCORE = { tile: 10, slowestMsATile:
|
|
270
|
+
export const KUMIMOJI_SCORE = { tile: 10, slowestMsATile: 30_000 };
|
|
207
271
|
/**
|
|
208
272
|
* PASS AND PLAY: up to eight people round one device, each with a hand and a
|
|
209
273
|
* table of their own from one shared bag (`party.ts`). John, 2026-09-28: "pass
|
package/dist/turn.js
CHANGED
|
@@ -92,7 +92,12 @@ export function unturnPlace(row, col, turn) {
|
|
|
92
92
|
export function turnArea(area, turn) {
|
|
93
93
|
const a = turnPlace(area.top, area.left, turn);
|
|
94
94
|
const b = turnPlace(area.top + area.rows - 1, area.left + area.cols - 1, turn);
|
|
95
|
-
return {
|
|
95
|
+
return {
|
|
96
|
+
top: Math.min(a.row, b.row),
|
|
97
|
+
left: Math.min(a.col, b.col),
|
|
98
|
+
rows: Math.abs(a.row - b.row) + 1,
|
|
99
|
+
cols: Math.abs(a.col - b.col) + 1,
|
|
100
|
+
};
|
|
96
101
|
}
|
|
97
102
|
/**
|
|
98
103
|
* A view the player zoomed or panned, turned a quarter clockwise with the
|
package/dist/ui/board.js
CHANGED
|
@@ -55,8 +55,22 @@ export function boardModel(tiles, verdict, glyphOf = (tile) => tileFace(tile).gl
|
|
|
55
55
|
for (let col = left; col < left + cols; col += 1) {
|
|
56
56
|
const square = squareAt(row, col);
|
|
57
57
|
const tile = tiles.get(square) ?? null;
|
|
58
|
-
const mark = tile === null || verdict === null
|
|
59
|
-
|
|
58
|
+
const mark = tile === null || verdict === null
|
|
59
|
+
? null
|
|
60
|
+
: verdict.misspelt.has(square)
|
|
61
|
+
? "misspelt"
|
|
62
|
+
: verdict.apart.has(square)
|
|
63
|
+
? "apart"
|
|
64
|
+
: "sound";
|
|
65
|
+
squares.push({
|
|
66
|
+
square,
|
|
67
|
+
row,
|
|
68
|
+
col,
|
|
69
|
+
tile,
|
|
70
|
+
glyph: tile === null ? "" : glyphOf(tile),
|
|
71
|
+
wild: tile !== null && tileFace(tile).wild,
|
|
72
|
+
mark,
|
|
73
|
+
});
|
|
60
74
|
}
|
|
61
75
|
}
|
|
62
76
|
return { rows, cols, squares };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { GridVerdict } from "../grid.ts";
|
|
2
|
+
import { type TilePlay } from "../play.ts";
|
|
3
|
+
import { type KumimojiLocale, type KumimojiStrings } from "../strings.ts";
|
|
4
|
+
import type { TileWords } from "../tileWords.ts";
|
|
5
|
+
/** What is picked up: a tile in the hand, or one on the table. */
|
|
6
|
+
export type Held = {
|
|
7
|
+
from: "hand";
|
|
8
|
+
at: number;
|
|
9
|
+
} | {
|
|
10
|
+
from: "table";
|
|
11
|
+
square: string;
|
|
12
|
+
} | null;
|
|
13
|
+
export declare function node<K extends keyof HTMLElementTagNameMap>(name: K, className?: string, text?: string): HTMLElementTagNameMap[K];
|
|
14
|
+
export declare function button(text: string, onClick: () => void, primary?: boolean, testId?: string): HTMLButtonElement;
|
|
15
|
+
/** The game as the drawing reads it, shared with the mount so a move and a redraw see the same table. */
|
|
16
|
+
export type TableSession = {
|
|
17
|
+
play: TilePlay | null;
|
|
18
|
+
finishedAt: number | null;
|
|
19
|
+
started: number;
|
|
20
|
+
words: TileWords | null;
|
|
21
|
+
held: Held;
|
|
22
|
+
onSquare: string | null;
|
|
23
|
+
onHand: number;
|
|
24
|
+
say: KumimojiStrings;
|
|
25
|
+
locale: KumimojiLocale;
|
|
26
|
+
note: string;
|
|
27
|
+
destroyed: boolean;
|
|
28
|
+
};
|
|
29
|
+
/** The elements the mount built once; a redraw fills them and never replaces them. */
|
|
30
|
+
export type TableParts = {
|
|
31
|
+
root: HTMLElement;
|
|
32
|
+
status: HTMLElement;
|
|
33
|
+
board: HTMLElement;
|
|
34
|
+
notes: HTMLElement;
|
|
35
|
+
hand: HTMLElement;
|
|
36
|
+
picker: HTMLElement;
|
|
37
|
+
controls: HTMLElement;
|
|
38
|
+
keepTitle: HTMLElement;
|
|
39
|
+
saveJson: HTMLButtonElement;
|
|
40
|
+
saveText: HTMLButtonElement;
|
|
41
|
+
load: HTMLButtonElement;
|
|
42
|
+
keepNote: HTMLElement;
|
|
43
|
+
};
|
|
44
|
+
/** What a button does to the game. The mount owns the rules; the drawing only asks. */
|
|
45
|
+
export type TableActions = {
|
|
46
|
+
verdictOf: (now: TilePlay) => GridVerdict;
|
|
47
|
+
change: (next: TilePlay) => void;
|
|
48
|
+
newGame: () => void;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Draw the table from the session and the elements the mount built: the status
|
|
52
|
+
* line, the board, the hand, the wild-tile picker, the play buttons and the
|
|
53
|
+
* save labels, then put the keyboard focus back on the control it was on.
|
|
54
|
+
* @param session - The game, the words and what is held, shared with the mount.
|
|
55
|
+
* @param parts - The elements the drawing fills.
|
|
56
|
+
* @param actions - How a button changes the game or starts another.
|
|
57
|
+
* @returns The redraw, and the status line alone for the clock.
|
|
58
|
+
*/
|
|
59
|
+
export declare function tableDrawing(session: TableSession, parts: TableParts, actions: TableActions): {
|
|
60
|
+
render: () => void;
|
|
61
|
+
renderStatus: () => void;
|
|
62
|
+
};
|
package/dist/ui/draw.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { kumimojiClock } from "../export.js";
|
|
2
|
+
import { assignHandTile, draw, liftAll, mayDraw, mayTrade, sortHand, tilesLeft, trade, } from "../play.js";
|
|
3
|
+
import { kumimojiSay } from "../strings.js";
|
|
4
|
+
import { tileFace } from "../tileFace.js";
|
|
5
|
+
import { boardModel } from "./board.js";
|
|
6
|
+
/** What a wild nobody has given a letter yet shows in the hand. */
|
|
7
|
+
const WILD_MARK = "★";
|
|
8
|
+
export function node(name, className, text) {
|
|
9
|
+
const made = document.createElement(name);
|
|
10
|
+
if (className !== undefined)
|
|
11
|
+
made.className = className;
|
|
12
|
+
if (text !== undefined)
|
|
13
|
+
made.textContent = text;
|
|
14
|
+
return made;
|
|
15
|
+
}
|
|
16
|
+
export function button(text, onClick, primary = false, testId) {
|
|
17
|
+
const made = node("button", primary ? "km-button km-primary" : "km-button", text);
|
|
18
|
+
made.type = "button";
|
|
19
|
+
if (testId !== undefined)
|
|
20
|
+
made.dataset.testid = testId;
|
|
21
|
+
made.addEventListener("click", onClick);
|
|
22
|
+
return made;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Draw the table from the session and the elements the mount built: the status
|
|
26
|
+
* line, the board, the hand, the wild-tile picker, the play buttons and the
|
|
27
|
+
* save labels, then put the keyboard focus back on the control it was on.
|
|
28
|
+
* @param session - The game, the words and what is held, shared with the mount.
|
|
29
|
+
* @param parts - The elements the drawing fills.
|
|
30
|
+
* @param actions - How a button changes the game or starts another.
|
|
31
|
+
* @returns The redraw, and the status line alone for the clock.
|
|
32
|
+
*/
|
|
33
|
+
export function tableDrawing(session, parts, actions) {
|
|
34
|
+
function renderStatus() {
|
|
35
|
+
const now = session.play;
|
|
36
|
+
parts.root.dataset.state = now === null ? "dealing" : session.finishedAt !== null ? "finished" : "playing";
|
|
37
|
+
if (now === null) {
|
|
38
|
+
parts.status.textContent = session.say.dealing;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const left = tilesLeft(now);
|
|
42
|
+
parts.root.dataset.left = String(left);
|
|
43
|
+
const time = kumimojiClock((session.finishedAt ?? Date.now()) - session.started);
|
|
44
|
+
parts.status.textContent =
|
|
45
|
+
session.finishedAt !== null
|
|
46
|
+
? kumimojiSay(session.say.finished, { time })
|
|
47
|
+
: kumimojiSay(left === 1 ? session.say.leftOne : session.say.left, { n: left, time });
|
|
48
|
+
}
|
|
49
|
+
function renderBoard(verdict) {
|
|
50
|
+
const playing = session.play;
|
|
51
|
+
const words = session.words;
|
|
52
|
+
const model = boardModel(playing.tiles, playing.tiles.size === 0 ? null : verdict, words.glyphOf);
|
|
53
|
+
const grid = node("div", "km-grid");
|
|
54
|
+
grid.style.gridTemplateColumns = `repeat(${model.cols}, var(--km-square))`;
|
|
55
|
+
grid.setAttribute("role", "group");
|
|
56
|
+
grid.setAttribute("aria-label", session.say.tableLabel);
|
|
57
|
+
grid.setAttribute("aria-description", session.say.tableKeys);
|
|
58
|
+
grid.dataset.cols = String(model.cols);
|
|
59
|
+
const cells = [];
|
|
60
|
+
for (const square of model.squares) {
|
|
61
|
+
const cell = node("button", "km-square");
|
|
62
|
+
cell.type = "button";
|
|
63
|
+
cell.dataset.square = square.square;
|
|
64
|
+
if (square.tile !== null) {
|
|
65
|
+
cell.classList.add("km-tile");
|
|
66
|
+
if (square.wild)
|
|
67
|
+
cell.classList.add("km-wild");
|
|
68
|
+
if (square.mark !== null)
|
|
69
|
+
cell.classList.add(`km-${square.mark}`);
|
|
70
|
+
cell.textContent = square.glyph;
|
|
71
|
+
cell.dataset.tile = square.glyph;
|
|
72
|
+
cell.setAttribute("aria-label", square.wild ? kumimojiSay(session.say.wildTile, { tile: square.glyph }) : square.glyph);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
cell.setAttribute("aria-label", session.say.emptySquare);
|
|
76
|
+
}
|
|
77
|
+
if (session.held?.from === "table" && session.held.square === square.square)
|
|
78
|
+
cell.classList.add("km-held");
|
|
79
|
+
cell.tabIndex = -1;
|
|
80
|
+
cells.push(cell);
|
|
81
|
+
grid.append(cell);
|
|
82
|
+
}
|
|
83
|
+
// The one Tab lands on: where the keyboard was, else a tile, else the middle.
|
|
84
|
+
const stop = cells.find((cell) => cell.dataset.square === session.onSquare) ??
|
|
85
|
+
cells.find((cell) => cell.classList.contains("km-tile")) ??
|
|
86
|
+
cells[Math.floor(cells.length / 2)];
|
|
87
|
+
if (stop !== undefined) {
|
|
88
|
+
stop.tabIndex = 0;
|
|
89
|
+
session.onSquare = stop.dataset.square ?? null;
|
|
90
|
+
}
|
|
91
|
+
parts.board.replaceChildren(grid);
|
|
92
|
+
parts.notes.textContent =
|
|
93
|
+
verdict.notWords.length > 0
|
|
94
|
+
? kumimojiSay(session.say.notWords, { words: verdict.notWords.join(", ") })
|
|
95
|
+
: verdict.apart.size > 0
|
|
96
|
+
? session.say.apart
|
|
97
|
+
: "";
|
|
98
|
+
}
|
|
99
|
+
function renderHand() {
|
|
100
|
+
const playing = session.play;
|
|
101
|
+
const words = session.words;
|
|
102
|
+
parts.hand.replaceChildren();
|
|
103
|
+
parts.hand.setAttribute("aria-label", session.say.handLabel);
|
|
104
|
+
playing.hand.forEach((tile, at) => {
|
|
105
|
+
const face = tileFace(tile);
|
|
106
|
+
const made = node("button", "km-tile km-in-hand");
|
|
107
|
+
made.type = "button";
|
|
108
|
+
made.dataset.hand = String(at);
|
|
109
|
+
made.textContent = face.blank ? WILD_MARK : words.glyphOf(tile);
|
|
110
|
+
if (face.wild)
|
|
111
|
+
made.classList.add("km-wild");
|
|
112
|
+
if (session.held?.from === "hand" && session.held.at === at)
|
|
113
|
+
made.classList.add("km-held");
|
|
114
|
+
made.dataset.tile = face.blank ? "*" : words.glyphOf(tile);
|
|
115
|
+
made.setAttribute("aria-label", face.blank
|
|
116
|
+
? session.say.wildBlank
|
|
117
|
+
: face.wild
|
|
118
|
+
? kumimojiSay(session.say.wildTile, { tile: words.glyphOf(tile) })
|
|
119
|
+
: words.glyphOf(tile));
|
|
120
|
+
made.tabIndex = at === Math.min(session.onHand, playing.hand.length - 1) ? 0 : -1;
|
|
121
|
+
parts.hand.append(made);
|
|
122
|
+
});
|
|
123
|
+
if (playing.hand.length === 0 && session.finishedAt === null)
|
|
124
|
+
parts.hand.append(node("span", "km-empty", session.say.noTiles));
|
|
125
|
+
}
|
|
126
|
+
function renderPicker() {
|
|
127
|
+
parts.picker.replaceChildren();
|
|
128
|
+
if (session.held?.from !== "hand" || session.play === null)
|
|
129
|
+
return;
|
|
130
|
+
const at = session.held.at;
|
|
131
|
+
const tile = session.play.hand[at];
|
|
132
|
+
if (tile === undefined || !tileFace(tile).wild)
|
|
133
|
+
return;
|
|
134
|
+
parts.picker.append(node("span", "km-picker-title", tile === "*" ? session.say.giveWild : session.say.changeWild));
|
|
135
|
+
const words = session.words;
|
|
136
|
+
for (const sound of words.wildOptions) {
|
|
137
|
+
const code = words.wildFor(sound);
|
|
138
|
+
if (code === null)
|
|
139
|
+
continue;
|
|
140
|
+
parts.picker.append(button(sound, () => {
|
|
141
|
+
session.held = { from: "hand", at };
|
|
142
|
+
actions.change(assignHandTile(session.play, at, code));
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function renderControls(verdict) {
|
|
147
|
+
parts.controls.replaceChildren();
|
|
148
|
+
if (session.finishedAt !== null) {
|
|
149
|
+
parts.controls.append(button(session.say.newGame, () => actions.newGame(), true, "km-new"));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const now = session.play;
|
|
153
|
+
const drawButton = button(session.say.draw, () => {
|
|
154
|
+
session.held = null;
|
|
155
|
+
actions.change(draw(now));
|
|
156
|
+
}, true, "km-draw");
|
|
157
|
+
drawButton.disabled = !mayDraw(now, verdict);
|
|
158
|
+
const tradeButton = button(session.say.trade, () => {
|
|
159
|
+
if (session.held?.from !== "hand")
|
|
160
|
+
return;
|
|
161
|
+
const at = session.held.at;
|
|
162
|
+
session.held = null;
|
|
163
|
+
actions.change(trade(now, at));
|
|
164
|
+
}, false, "km-trade");
|
|
165
|
+
tradeButton.disabled = session.held?.from !== "hand" || !mayTrade(now);
|
|
166
|
+
parts.controls.append(drawButton, tradeButton, button(session.say.sort, () => actions.change(sortHand(now)), false, "km-sort"), button(session.say.liftAll, () => {
|
|
167
|
+
session.held = null;
|
|
168
|
+
actions.change(liftAll(now));
|
|
169
|
+
}, false, "km-lift"), button(session.say.newGame, () => actions.newGame(), false, "km-new"));
|
|
170
|
+
}
|
|
171
|
+
function renderKeep() {
|
|
172
|
+
parts.keepTitle.textContent = session.say.keep;
|
|
173
|
+
parts.saveJson.textContent = session.say.saveJson;
|
|
174
|
+
parts.saveText.textContent = session.say.saveText;
|
|
175
|
+
parts.load.textContent = session.say.load;
|
|
176
|
+
parts.saveJson.disabled = session.play === null;
|
|
177
|
+
parts.saveText.disabled = session.play === null;
|
|
178
|
+
parts.keepNote.textContent = session.note;
|
|
179
|
+
}
|
|
180
|
+
/** Where the keyboard is, so that it can be put back after everything is drawn again: the zone, and which of it. */
|
|
181
|
+
function keyboardAt() {
|
|
182
|
+
const on = document.activeElement;
|
|
183
|
+
if (on === null || !parts.root.contains(on))
|
|
184
|
+
return null;
|
|
185
|
+
const mark = (zone, id) => id === undefined ? null : { zone, id };
|
|
186
|
+
if (parts.board.contains(on))
|
|
187
|
+
return mark("board", on.dataset.square);
|
|
188
|
+
if (parts.hand.contains(on))
|
|
189
|
+
return mark("hand", on.dataset.hand);
|
|
190
|
+
if (parts.controls.contains(on))
|
|
191
|
+
return mark("controls", on.dataset.testid);
|
|
192
|
+
if (parts.picker.contains(on))
|
|
193
|
+
return mark("picker", String([...parts.picker.querySelectorAll("button")].indexOf(on)));
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
function keyboardBack(at) {
|
|
197
|
+
if (at === null)
|
|
198
|
+
return;
|
|
199
|
+
const zone = { board: parts.board, hand: parts.hand, controls: parts.controls, picker: parts.picker }[at.zone];
|
|
200
|
+
const found = at.zone === "board"
|
|
201
|
+
? zone.querySelector(`[data-square="${at.id}"]`)
|
|
202
|
+
: at.zone === "hand"
|
|
203
|
+
? zone.querySelector(`[data-hand="${at.id}"]`)
|
|
204
|
+
: at.zone === "controls"
|
|
205
|
+
? zone.querySelector(`[data-testid="${at.id}"]`)
|
|
206
|
+
: zone.querySelectorAll("button")[Number(at.id)];
|
|
207
|
+
// A button that has become disabled cannot hold the focus; the table keeps it rather than dropping it on the page.
|
|
208
|
+
if (found !== undefined && found !== null && !found.disabled)
|
|
209
|
+
found.focus({ preventScroll: true });
|
|
210
|
+
}
|
|
211
|
+
function render() {
|
|
212
|
+
if (session.destroyed)
|
|
213
|
+
return;
|
|
214
|
+
const where = keyboardAt();
|
|
215
|
+
parts.root.lang = session.locale;
|
|
216
|
+
renderStatus();
|
|
217
|
+
renderKeep();
|
|
218
|
+
const now = session.play;
|
|
219
|
+
if (now === null || session.words === null)
|
|
220
|
+
return;
|
|
221
|
+
const verdict = actions.verdictOf(now);
|
|
222
|
+
renderBoard(verdict);
|
|
223
|
+
renderPicker();
|
|
224
|
+
renderHand();
|
|
225
|
+
renderControls(verdict);
|
|
226
|
+
keyboardBack(where);
|
|
227
|
+
}
|
|
228
|
+
return { render, renderStatus };
|
|
229
|
+
}
|