@johnmorrisdotca/kumimoji 1.0.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 +53 -0
- package/CONTRIBUTING.md +38 -0
- package/LICENSE +21 -0
- package/NOTICE.md +38 -0
- package/README.md +233 -0
- package/dist/check.d.ts +19 -0
- package/dist/check.js +70 -0
- package/dist/computer.types.d.ts +23 -0
- package/dist/computer.types.js +1 -0
- package/dist/computerPlay.d.ts +17 -0
- package/dist/computerPlay.js +203 -0
- package/dist/computerTurn.d.ts +31 -0
- package/dist/computerTurn.js +94 -0
- package/dist/generate.d.ts +29 -0
- package/dist/generate.js +278 -0
- package/dist/grid.d.ts +97 -0
- package/dist/grid.js +208 -0
- package/dist/help.d.ts +20 -0
- package/dist/help.js +57 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +44 -0
- package/dist/judge.d.ts +13 -0
- package/dist/judge.js +19 -0
- package/dist/kana.d.ts +25 -0
- package/dist/kana.js +38 -0
- package/dist/kumimoji.types.d.ts +49 -0
- package/dist/kumimoji.types.js +1 -0
- package/dist/party.d.ts +91 -0
- package/dist/party.js +172 -0
- package/dist/party.types.d.ts +86 -0
- package/dist/party.types.js +1 -0
- package/dist/partyKept.d.ts +17 -0
- package/dist/partyKept.js +149 -0
- package/dist/partySeats.d.ts +53 -0
- package/dist/partySeats.js +119 -0
- package/dist/partyTurns.d.ts +75 -0
- package/dist/partyTurns.js +153 -0
- package/dist/placement.d.ts +26 -0
- package/dist/placement.js +87 -0
- package/dist/play.d.ts +76 -0
- package/dist/play.js +165 -0
- package/dist/random.d.ts +11 -0
- package/dist/random.js +25 -0
- package/dist/react.d.ts +32 -0
- package/dist/react.js +53 -0
- package/dist/showcase.d.ts +51 -0
- package/dist/showcase.js +59 -0
- package/dist/tableView.d.ts +92 -0
- package/dist/tableView.js +127 -0
- package/dist/tileFace.d.ts +25 -0
- package/dist/tileFace.js +39 -0
- package/dist/tileFamily.d.ts +11 -0
- package/dist/tileFamily.js +23 -0
- package/dist/tileWords.d.ts +70 -0
- package/dist/tileWords.js +164 -0
- package/dist/tileWordsModule.d.ts +4 -0
- package/dist/tileWordsModule.js +13 -0
- package/dist/tiles.constants.d.ts +108 -0
- package/dist/tiles.constants.js +132 -0
- package/dist/turn.d.ts +62 -0
- package/dist/turn.js +96 -0
- package/dist/ui/board.d.ts +29 -0
- package/dist/ui/board.js +37 -0
- package/dist/ui/mount.d.ts +36 -0
- package/dist/ui/mount.js +257 -0
- package/dist/ui/style.d.ts +6 -0
- package/dist/ui/style.js +40 -0
- package/dist/ui.d.ts +8 -0
- package/dist/ui.js +8 -0
- package/dist/wilds.d.ts +27 -0
- package/dist/wilds.js +111 -0
- package/dist/words.d.ts +6 -0
- package/dist/words.en.data.d.ts +23 -0
- package/dist/words.en.data.js +610 -0
- package/dist/words.ja.data.d.ts +29 -0
- package/dist/words.ja.data.js +29 -0
- package/dist/words.js +6 -0
- package/package.json +90 -0
package/dist/grid.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A KUMIMOJI GRID: letter tiles on a table with no edges. Each tile stands on
|
|
3
|
+
* a square named by its row and column, which may be any whole numbers,
|
|
4
|
+
* negative too — the table grows whichever way the player builds. Pure, like
|
|
5
|
+
* every rule here: nothing is changed in place.
|
|
6
|
+
*
|
|
7
|
+
* As a string, for an answer and a kept game, the grid is drawn from its own
|
|
8
|
+
* top-left tile: its rows in order, joined by "/", each row written as runs of
|
|
9
|
+
* empty squares (a number) and tiles. English letters keep their short form;
|
|
10
|
+
* other tiles are URI-escaped between `~` and `;`. `cat/2o/2w` is CAT across
|
|
11
|
+
* and COW down from its C. Only
|
|
12
|
+
* where the tiles stand beside one another is written, never where on the
|
|
13
|
+
* table they were, so one grid has one spelling however far it was dragged.
|
|
14
|
+
*/
|
|
15
|
+
export type Tiles = ReadonlyMap<string, string>;
|
|
16
|
+
/** A square's name, "row,col". */
|
|
17
|
+
export declare function squareAt(row: number, col: number): string;
|
|
18
|
+
/** A square's row and column, from its name. */
|
|
19
|
+
export declare function placeOf(square: string): {
|
|
20
|
+
row: number;
|
|
21
|
+
col: number;
|
|
22
|
+
};
|
|
23
|
+
/** The rows and columns the tiles stand within, or null for no tiles. */
|
|
24
|
+
export type Bounds = {
|
|
25
|
+
top: number;
|
|
26
|
+
left: number;
|
|
27
|
+
bottom: number;
|
|
28
|
+
right: number;
|
|
29
|
+
};
|
|
30
|
+
export declare function boundsOf(tiles: Tiles): Bounds | null;
|
|
31
|
+
export declare function encodeGrid(tiles: Tiles): string;
|
|
32
|
+
/**
|
|
33
|
+
* A grid read back, its first row at row 0, or null for anything that is not
|
|
34
|
+
* one: a stray character, a capital, a grid wider or taller than any grid of
|
|
35
|
+
* fifty tiles can be. An empty string is no tiles.
|
|
36
|
+
*/
|
|
37
|
+
export declare function decodeGrid(code: string): Map<string, string> | null;
|
|
38
|
+
/** How many of each letter. */
|
|
39
|
+
export declare function lettersOf(letters: Iterable<string>): Map<string, number>;
|
|
40
|
+
/** Whether two tallies hold the same letters, as many of each. */
|
|
41
|
+
export declare function sameLetters(a: Map<string, number>, b: Map<string, number>): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* THE LINES A RUN MAY LIE ALONG, each read top to bottom (and across, left to
|
|
44
|
+
* right): across and down always, and the two diagonals when the game was set
|
|
45
|
+
* up with Diagonals — down to the right, and down to the left.
|
|
46
|
+
*/
|
|
47
|
+
export type RunLine = "across" | "down" | "downRight" | "downLeft";
|
|
48
|
+
/**
|
|
49
|
+
* The fewest tiles a diagonal run needs before it is read. Two tiles touching
|
|
50
|
+
* at a corner are what every crossword is full of — the letter above a word
|
|
51
|
+
* and the one beside it — so a pair is free, and only three or more in a
|
|
52
|
+
* line are a diagonal word.
|
|
53
|
+
*/
|
|
54
|
+
export declare const DIAGONAL_RUN_LEAST = 3;
|
|
55
|
+
/** How a grid is read: whether its diagonals are (`KumimojiOptions.diagonals`). */
|
|
56
|
+
export type GridRules = {
|
|
57
|
+
diagonals?: boolean;
|
|
58
|
+
};
|
|
59
|
+
/** A run of tiles along one line: the word it spells and the squares it stands on, in reading order. */
|
|
60
|
+
export type Run = {
|
|
61
|
+
word: string;
|
|
62
|
+
squares: string[];
|
|
63
|
+
line: RunLine;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Every run the grid is read by: two or more tiles across, then down, and
|
|
67
|
+
* with Diagonals three or more down to the right, then down to the left. A
|
|
68
|
+
* tile with nothing beside it in a line is no run.
|
|
69
|
+
*/
|
|
70
|
+
export declare function runsOf(tiles: Tiles, rules?: GridRules): Run[];
|
|
71
|
+
/**
|
|
72
|
+
* The tiles in groups that touch across or down, largest first. With `links`
|
|
73
|
+
* — the diagonal runs a grid with Diagonals reads — the tiles next to each
|
|
74
|
+
* other in one of those runs are joined too: a diagonal word holds a
|
|
75
|
+
* crossword together as a word across does. A pair touching at a corner is
|
|
76
|
+
* never a link, because it is never read.
|
|
77
|
+
*/
|
|
78
|
+
export declare function groupsOf(tiles: Tiles, links?: readonly Run[]): string[][];
|
|
79
|
+
/**
|
|
80
|
+
* WHAT IS WRONG WITH A GRID, tile by tile, so the table can mark it: the tiles
|
|
81
|
+
* in a run that is not a word, the tiles not joined to the main grid, and
|
|
82
|
+
* whether the whole is sound — two tiles or more, all joined, every run a word.
|
|
83
|
+
* With Diagonals (`rules`), the diagonal runs of three or more are runs too:
|
|
84
|
+
* each must be a word, a misspelt one is marked as any other is, and each
|
|
85
|
+
* joins its tiles (`groupsOf`).
|
|
86
|
+
*/
|
|
87
|
+
export type GridVerdict = {
|
|
88
|
+
sound: boolean;
|
|
89
|
+
tiles: number;
|
|
90
|
+
/** Tiles standing in a run the list does not know. */
|
|
91
|
+
misspelt: ReadonlySet<string>;
|
|
92
|
+
/** Tiles in a group apart from the largest. */
|
|
93
|
+
apart: ReadonlySet<string>;
|
|
94
|
+
/** The runs that are not words, as spelled, for the line under the table. */
|
|
95
|
+
notWords: readonly string[];
|
|
96
|
+
};
|
|
97
|
+
export declare function judgeGrid(tiles: Tiles, isWord: (word: string) => boolean, readable?: (word: string) => string, rules?: GridRules): GridVerdict;
|
package/dist/grid.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { KUMIMOJI_GRID_MOST } from "./tiles.constants.js";
|
|
2
|
+
/** A square's name, "row,col". */
|
|
3
|
+
export function squareAt(row, col) {
|
|
4
|
+
return `${row},${col}`;
|
|
5
|
+
}
|
|
6
|
+
/** A square's row and column, from its name. */
|
|
7
|
+
export function placeOf(square) {
|
|
8
|
+
const [row, col] = square.split(",").map(Number);
|
|
9
|
+
return { row: row, col: col };
|
|
10
|
+
}
|
|
11
|
+
export function boundsOf(tiles) {
|
|
12
|
+
let bounds = null;
|
|
13
|
+
for (const square of tiles.keys()) {
|
|
14
|
+
const { row, col } = placeOf(square);
|
|
15
|
+
bounds =
|
|
16
|
+
bounds === null
|
|
17
|
+
? { top: row, left: col, bottom: row, right: col }
|
|
18
|
+
: { top: Math.min(bounds.top, row), left: Math.min(bounds.left, col), bottom: Math.max(bounds.bottom, row), right: Math.max(bounds.right, col) };
|
|
19
|
+
}
|
|
20
|
+
return bounds;
|
|
21
|
+
}
|
|
22
|
+
export function encodeGrid(tiles) {
|
|
23
|
+
const bounds = boundsOf(tiles);
|
|
24
|
+
if (bounds === null)
|
|
25
|
+
return "";
|
|
26
|
+
const rows = [];
|
|
27
|
+
for (let row = bounds.top; row <= bounds.bottom; row += 1) {
|
|
28
|
+
let line = "";
|
|
29
|
+
let gap = 0;
|
|
30
|
+
for (let col = bounds.left; col <= bounds.right; col += 1) {
|
|
31
|
+
const letter = tiles.get(squareAt(row, col));
|
|
32
|
+
if (letter === undefined) {
|
|
33
|
+
gap += 1;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (letter === "")
|
|
37
|
+
throw new Error("A Kumimoji tile cannot be empty.");
|
|
38
|
+
line += `${gap > 0 ? gap : ""}${/^[a-z]$/.test(letter) ? letter : `~${encodeURIComponent(letter)};`}`;
|
|
39
|
+
gap = 0;
|
|
40
|
+
}
|
|
41
|
+
rows.push(line);
|
|
42
|
+
}
|
|
43
|
+
return rows.join("/");
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A grid read back, its first row at row 0, or null for anything that is not
|
|
47
|
+
* one: a stray character, a capital, a grid wider or taller than any grid of
|
|
48
|
+
* fifty tiles can be. An empty string is no tiles.
|
|
49
|
+
*/
|
|
50
|
+
export function decodeGrid(code) {
|
|
51
|
+
if (typeof code !== "string")
|
|
52
|
+
return null;
|
|
53
|
+
const tiles = new Map();
|
|
54
|
+
if (code === "")
|
|
55
|
+
return tiles;
|
|
56
|
+
const rows = code.split("/");
|
|
57
|
+
if (rows.length > KUMIMOJI_GRID_MOST)
|
|
58
|
+
return null;
|
|
59
|
+
for (const [row, line] of rows.entries()) {
|
|
60
|
+
let col = 0;
|
|
61
|
+
let at = 0;
|
|
62
|
+
while (at < line.length) {
|
|
63
|
+
const gapStart = at;
|
|
64
|
+
while (at < line.length && /\d/.test(line[at]))
|
|
65
|
+
at += 1;
|
|
66
|
+
const gap = line.slice(gapStart, at);
|
|
67
|
+
let letter;
|
|
68
|
+
if (/^[a-z]$/.test(line[at] ?? "")) {
|
|
69
|
+
letter = line[at];
|
|
70
|
+
at += 1;
|
|
71
|
+
}
|
|
72
|
+
else if (line[at] === "~") {
|
|
73
|
+
const end = line.indexOf(";", at + 1);
|
|
74
|
+
if (end === -1)
|
|
75
|
+
return null;
|
|
76
|
+
try {
|
|
77
|
+
letter = decodeURIComponent(line.slice(at + 1, end));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
if (letter.length === 0 || letter.length > 8)
|
|
83
|
+
return null;
|
|
84
|
+
at = end + 1;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
col += gap === "" ? 0 : Number(gap);
|
|
90
|
+
if (col >= KUMIMOJI_GRID_MOST)
|
|
91
|
+
return null;
|
|
92
|
+
tiles.set(squareAt(row, col), letter);
|
|
93
|
+
col += 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return tiles;
|
|
97
|
+
}
|
|
98
|
+
/** How many of each letter. */
|
|
99
|
+
export function lettersOf(letters) {
|
|
100
|
+
const counts = new Map();
|
|
101
|
+
for (const letter of letters)
|
|
102
|
+
if (letter !== "")
|
|
103
|
+
counts.set(letter, (counts.get(letter) ?? 0) + 1);
|
|
104
|
+
return counts;
|
|
105
|
+
}
|
|
106
|
+
/** Whether two tallies hold the same letters, as many of each. */
|
|
107
|
+
export function sameLetters(a, b) {
|
|
108
|
+
if (a.size !== b.size)
|
|
109
|
+
return false;
|
|
110
|
+
for (const [letter, count] of a)
|
|
111
|
+
if (b.get(letter) !== count)
|
|
112
|
+
return false;
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
/** One step along each line, as rows and columns. */
|
|
116
|
+
const STEP = { across: [0, 1], down: [1, 0], downRight: [1, 1], downLeft: [1, -1] };
|
|
117
|
+
/**
|
|
118
|
+
* The fewest tiles a diagonal run needs before it is read. Two tiles touching
|
|
119
|
+
* at a corner are what every crossword is full of — the letter above a word
|
|
120
|
+
* and the one beside it — so a pair is free, and only three or more in a
|
|
121
|
+
* line are a diagonal word.
|
|
122
|
+
*/
|
|
123
|
+
export const DIAGONAL_RUN_LEAST = 3;
|
|
124
|
+
function runsAlong(tiles, line, least) {
|
|
125
|
+
const [down, across] = STEP[line];
|
|
126
|
+
const runs = [];
|
|
127
|
+
for (const [square] of tiles) {
|
|
128
|
+
const { row, col } = placeOf(square);
|
|
129
|
+
// Only from a run's first tile: nothing before it in this line.
|
|
130
|
+
if (tiles.has(squareAt(row - down, col - across)))
|
|
131
|
+
continue;
|
|
132
|
+
const squares = [];
|
|
133
|
+
for (let step = 0;; step += 1) {
|
|
134
|
+
const at = squareAt(row + step * down, col + step * across);
|
|
135
|
+
if (!tiles.has(at))
|
|
136
|
+
break;
|
|
137
|
+
squares.push(at);
|
|
138
|
+
}
|
|
139
|
+
if (squares.length >= least)
|
|
140
|
+
runs.push({ word: squares.map((at) => tiles.get(at)).join(""), squares, line });
|
|
141
|
+
}
|
|
142
|
+
return runs;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Every run the grid is read by: two or more tiles across, then down, and
|
|
146
|
+
* with Diagonals three or more down to the right, then down to the left. A
|
|
147
|
+
* tile with nothing beside it in a line is no run.
|
|
148
|
+
*/
|
|
149
|
+
export function runsOf(tiles, rules = {}) {
|
|
150
|
+
const runs = [...runsAlong(tiles, "across", 2), ...runsAlong(tiles, "down", 2)];
|
|
151
|
+
if (rules.diagonals === true)
|
|
152
|
+
runs.push(...runsAlong(tiles, "downRight", DIAGONAL_RUN_LEAST), ...runsAlong(tiles, "downLeft", DIAGONAL_RUN_LEAST));
|
|
153
|
+
return runs;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The tiles in groups that touch across or down, largest first. With `links`
|
|
157
|
+
* — the diagonal runs a grid with Diagonals reads — the tiles next to each
|
|
158
|
+
* other in one of those runs are joined too: a diagonal word holds a
|
|
159
|
+
* crossword together as a word across does. A pair touching at a corner is
|
|
160
|
+
* never a link, because it is never read.
|
|
161
|
+
*/
|
|
162
|
+
export function groupsOf(tiles, links = []) {
|
|
163
|
+
const joined = new Map();
|
|
164
|
+
const join = (a, b) => joined.set(a, [...(joined.get(a) ?? []), b]);
|
|
165
|
+
for (const run of links) {
|
|
166
|
+
for (let at = 1; at < run.squares.length; at += 1) {
|
|
167
|
+
join(run.squares[at - 1], run.squares[at]);
|
|
168
|
+
join(run.squares[at], run.squares[at - 1]);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const seen = new Set();
|
|
172
|
+
const groups = [];
|
|
173
|
+
for (const start of tiles.keys()) {
|
|
174
|
+
if (seen.has(start))
|
|
175
|
+
continue;
|
|
176
|
+
const group = [];
|
|
177
|
+
const queue = [start];
|
|
178
|
+
seen.add(start);
|
|
179
|
+
while (queue.length > 0) {
|
|
180
|
+
const at = queue.pop();
|
|
181
|
+
group.push(at);
|
|
182
|
+
const { row, col } = placeOf(at);
|
|
183
|
+
for (const near of [squareAt(row - 1, col), squareAt(row + 1, col), squareAt(row, col - 1), squareAt(row, col + 1), ...(joined.get(at) ?? [])]) {
|
|
184
|
+
if (tiles.has(near) && !seen.has(near)) {
|
|
185
|
+
seen.add(near);
|
|
186
|
+
queue.push(near);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
groups.push(group);
|
|
191
|
+
}
|
|
192
|
+
return groups.sort((a, b) => b.length - a.length);
|
|
193
|
+
}
|
|
194
|
+
export function judgeGrid(tiles, isWord, readable = (word) => word, rules = {}) {
|
|
195
|
+
const misspelt = new Set();
|
|
196
|
+
const notWords = [];
|
|
197
|
+
const runs = runsOf(tiles, rules);
|
|
198
|
+
for (const run of runs) {
|
|
199
|
+
if (isWord(run.word))
|
|
200
|
+
continue;
|
|
201
|
+
notWords.push(readable(run.word));
|
|
202
|
+
for (const at of run.squares)
|
|
203
|
+
misspelt.add(at);
|
|
204
|
+
}
|
|
205
|
+
const groups = groupsOf(tiles, runs.filter((run) => run.line === "downRight" || run.line === "downLeft"));
|
|
206
|
+
const apart = new Set(groups.slice(1).flat());
|
|
207
|
+
return { sound: tiles.size >= 2 && groups.length === 1 && notWords.length === 0, tiles: tiles.size, misspelt, apart, notWords };
|
|
208
|
+
}
|
package/dist/help.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TilePlay } from "./play.ts";
|
|
2
|
+
import type { TileWords } from "./tileWords.ts";
|
|
3
|
+
/**
|
|
4
|
+
* HELP: THE HAND ARRANGED INTO A WORD. John, 2026-09-28: "a help button next
|
|
5
|
+
* to the sort button where if you click it, the letters in your board are all
|
|
6
|
+
* arranged to make words. Every time you click the help button it cycles
|
|
7
|
+
* through a different word… Even though it's a little cheating, they still
|
|
8
|
+
* have to find a place for it on the board so it's not super powerful."
|
|
9
|
+
*
|
|
10
|
+
* Chosen on the set-up screen or not at all (the puzzles' `hints=1`), and each
|
|
11
|
+
* press is counted as a hint, so a helped game is kept and priced as one.
|
|
12
|
+
* Only the hand's own tiles are read — a wild is left for the player to place
|
|
13
|
+
* — and nothing on the table is moved.
|
|
14
|
+
*/
|
|
15
|
+
/** How many words one hand offers before the presses go round again: the longest first. */
|
|
16
|
+
export declare const HELP_WORDS_MOST = 40;
|
|
17
|
+
/** The words the hand's own tiles spell, longest first and then in the list's order; wilds are not used. */
|
|
18
|
+
export declare function wordsInHand(hand: readonly string[], words: TileWords): string[];
|
|
19
|
+
/** The hand with `word`'s tiles first, in its order, and the rest after them as they were. */
|
|
20
|
+
export declare function handSpelling(play: TilePlay, word: string): TilePlay;
|
package/dist/help.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HELP: THE HAND ARRANGED INTO A WORD. John, 2026-09-28: "a help button next
|
|
3
|
+
* to the sort button where if you click it, the letters in your board are all
|
|
4
|
+
* arranged to make words. Every time you click the help button it cycles
|
|
5
|
+
* through a different word… Even though it's a little cheating, they still
|
|
6
|
+
* have to find a place for it on the board so it's not super powerful."
|
|
7
|
+
*
|
|
8
|
+
* Chosen on the set-up screen or not at all (the puzzles' `hints=1`), and each
|
|
9
|
+
* press is counted as a hint, so a helped game is kept and priced as one.
|
|
10
|
+
* Only the hand's own tiles are read — a wild is left for the player to place
|
|
11
|
+
* — and nothing on the table is moved.
|
|
12
|
+
*/
|
|
13
|
+
/** How many words one hand offers before the presses go round again: the longest first. */
|
|
14
|
+
export const HELP_WORDS_MOST = 40;
|
|
15
|
+
/** The words the hand's own tiles spell, longest first and then in the list's order; wilds are not used. */
|
|
16
|
+
export function wordsInHand(hand, words) {
|
|
17
|
+
const have = new Map();
|
|
18
|
+
for (const tile of hand)
|
|
19
|
+
if (!words.isWild(tile))
|
|
20
|
+
have.set(tile, (have.get(tile) ?? 0) + 1);
|
|
21
|
+
const tiles = [...have.values()].reduce((sum, count) => sum + count, 0);
|
|
22
|
+
const found = [];
|
|
23
|
+
const need = new Map();
|
|
24
|
+
for (let length = tiles; length >= 2 && found.length < HELP_WORDS_MOST; length -= 1) {
|
|
25
|
+
for (const word of words.byLength.get(length) ?? []) {
|
|
26
|
+
need.clear();
|
|
27
|
+
let spelt = true;
|
|
28
|
+
for (const tile of word) {
|
|
29
|
+
const count = (need.get(tile) ?? 0) + 1;
|
|
30
|
+
if (count > (have.get(tile) ?? 0)) {
|
|
31
|
+
spelt = false;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
need.set(tile, count);
|
|
35
|
+
}
|
|
36
|
+
if (!spelt)
|
|
37
|
+
continue;
|
|
38
|
+
found.push(word);
|
|
39
|
+
if (found.length >= HELP_WORDS_MOST)
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return found;
|
|
44
|
+
}
|
|
45
|
+
/** The hand with `word`'s tiles first, in its order, and the rest after them as they were. */
|
|
46
|
+
export function handSpelling(play, word) {
|
|
47
|
+
const rest = [...play.hand];
|
|
48
|
+
const front = [];
|
|
49
|
+
for (const tile of word) {
|
|
50
|
+
const at = rest.indexOf(tile);
|
|
51
|
+
if (at === -1)
|
|
52
|
+
return play;
|
|
53
|
+
front.push(tile);
|
|
54
|
+
rest.splice(at, 1);
|
|
55
|
+
}
|
|
56
|
+
return { ...play, hand: [...front, ...rest] };
|
|
57
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kumimoji 組み文字: the race to build a crossword from a bag of letter
|
|
3
|
+
* tiles, in English or Japanese, alone or at a table of two to eight, as
|
|
4
|
+
* plain functions over plain data.
|
|
5
|
+
*
|
|
6
|
+
* A game is dealt from a seed (`generateKumimoji`), so two players racing the
|
|
7
|
+
* same bag get the same tiles in the same order. The tiles you hold and the
|
|
8
|
+
* grid you build are values (`deal`, `placeFromHand`, `draw`, `trade` and the
|
|
9
|
+
* rest), and every run across and down is judged against a word list
|
|
10
|
+
* (`judgeWithWords`, `checkKumimoji`). A table of players shares one bag
|
|
11
|
+
* (`startParty`, `endTurn`), and a computer player can sit at it
|
|
12
|
+
* (`planComputerTurn`). Nothing here touches the DOM or a clock.
|
|
13
|
+
*
|
|
14
|
+
* The word lists are loaded when first needed: in a browser by
|
|
15
|
+
* `loadTileWords`, which fetches them as their own modules; on a server or in
|
|
16
|
+
* a test through `@johnmorrisdotca/kumimoji/words`, which reads them from
|
|
17
|
+
* disk.
|
|
18
|
+
*/
|
|
19
|
+
export * from "./kumimoji.types.ts";
|
|
20
|
+
export * from "./tiles.constants.ts";
|
|
21
|
+
export * from "./kana.ts";
|
|
22
|
+
export * from "./tileFace.ts";
|
|
23
|
+
export * from "./tileFamily.ts";
|
|
24
|
+
export * from "./tileWords.ts";
|
|
25
|
+
export * from "./grid.ts";
|
|
26
|
+
export * from "./placement.ts";
|
|
27
|
+
export * from "./wilds.ts";
|
|
28
|
+
export * from "./judge.ts";
|
|
29
|
+
export * from "./check.ts";
|
|
30
|
+
export * from "./generate.ts";
|
|
31
|
+
export * from "./play.ts";
|
|
32
|
+
export * from "./help.ts";
|
|
33
|
+
export * from "./turn.ts";
|
|
34
|
+
export * from "./tableView.ts";
|
|
35
|
+
export * from "./showcase.ts";
|
|
36
|
+
export * from "./party.types.ts";
|
|
37
|
+
export * from "./party.ts";
|
|
38
|
+
export * from "./partyTurns.ts";
|
|
39
|
+
export * from "./partySeats.ts";
|
|
40
|
+
export * from "./partyKept.ts";
|
|
41
|
+
export * from "./computer.types.ts";
|
|
42
|
+
export * from "./computerPlay.ts";
|
|
43
|
+
export * from "./computerTurn.ts";
|
|
44
|
+
export * from "./random.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kumimoji 組み文字: the race to build a crossword from a bag of letter
|
|
3
|
+
* tiles, in English or Japanese, alone or at a table of two to eight, as
|
|
4
|
+
* plain functions over plain data.
|
|
5
|
+
*
|
|
6
|
+
* A game is dealt from a seed (`generateKumimoji`), so two players racing the
|
|
7
|
+
* same bag get the same tiles in the same order. The tiles you hold and the
|
|
8
|
+
* grid you build are values (`deal`, `placeFromHand`, `draw`, `trade` and the
|
|
9
|
+
* rest), and every run across and down is judged against a word list
|
|
10
|
+
* (`judgeWithWords`, `checkKumimoji`). A table of players shares one bag
|
|
11
|
+
* (`startParty`, `endTurn`), and a computer player can sit at it
|
|
12
|
+
* (`planComputerTurn`). Nothing here touches the DOM or a clock.
|
|
13
|
+
*
|
|
14
|
+
* The word lists are loaded when first needed: in a browser by
|
|
15
|
+
* `loadTileWords`, which fetches them as their own modules; on a server or in
|
|
16
|
+
* a test through `@johnmorrisdotca/kumimoji/words`, which reads them from
|
|
17
|
+
* disk.
|
|
18
|
+
*/
|
|
19
|
+
export * from "./kumimoji.types.js";
|
|
20
|
+
export * from "./tiles.constants.js";
|
|
21
|
+
export * from "./kana.js";
|
|
22
|
+
export * from "./tileFace.js";
|
|
23
|
+
export * from "./tileFamily.js";
|
|
24
|
+
export * from "./tileWords.js";
|
|
25
|
+
export * from "./grid.js";
|
|
26
|
+
export * from "./placement.js";
|
|
27
|
+
export * from "./wilds.js";
|
|
28
|
+
export * from "./judge.js";
|
|
29
|
+
export * from "./check.js";
|
|
30
|
+
export * from "./generate.js";
|
|
31
|
+
export * from "./play.js";
|
|
32
|
+
export * from "./help.js";
|
|
33
|
+
export * from "./turn.js";
|
|
34
|
+
export * from "./tableView.js";
|
|
35
|
+
export * from "./showcase.js";
|
|
36
|
+
export * from "./party.types.js";
|
|
37
|
+
export * from "./party.js";
|
|
38
|
+
export * from "./partyTurns.js";
|
|
39
|
+
export * from "./partySeats.js";
|
|
40
|
+
export * from "./partyKept.js";
|
|
41
|
+
export * from "./computer.types.js";
|
|
42
|
+
export * from "./computerPlay.js";
|
|
43
|
+
export * from "./computerTurn.js";
|
|
44
|
+
export * from "./random.js";
|
package/dist/judge.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type GridRules, type GridVerdict, type Tiles } from "./grid.ts";
|
|
2
|
+
import type { TileWords } from "./tileWords.ts";
|
|
3
|
+
/**
|
|
4
|
+
* A GRID JUDGED AGAINST ITS LANGUAGE'S LIST, the one way the solo game, each
|
|
5
|
+
* seat of a pass-and-play game and the server's check all ask it: a run is a
|
|
6
|
+
* word when its tiles spell one the list holds (`wordOf`, so a wild reads as
|
|
7
|
+
* the letter or kana it was given), and a run that is not is named as it
|
|
8
|
+
* reads. A wild given no letter reads as whichever letter makes every run
|
|
9
|
+
* through it a word (`readWilds`); where none does, the grid is judged as
|
|
10
|
+
* laid and the run is named with its `*`. `rules` says whether the diagonals
|
|
11
|
+
* are read (`GridRules`).
|
|
12
|
+
*/
|
|
13
|
+
export declare function judgeWithWords(tiles: Tiles, words: TileWords, rules?: GridRules): GridVerdict;
|
package/dist/judge.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { judgeGrid } from "./grid.js";
|
|
2
|
+
import { readWilds } from "./wilds.js";
|
|
3
|
+
/**
|
|
4
|
+
* A GRID JUDGED AGAINST ITS LANGUAGE'S LIST, the one way the solo game, each
|
|
5
|
+
* seat of a pass-and-play game and the server's check all ask it: a run is a
|
|
6
|
+
* word when its tiles spell one the list holds (`wordOf`, so a wild reads as
|
|
7
|
+
* the letter or kana it was given), and a run that is not is named as it
|
|
8
|
+
* reads. A wild given no letter reads as whichever letter makes every run
|
|
9
|
+
* through it a word (`readWilds`); where none does, the grid is judged as
|
|
10
|
+
* laid and the run is named with its `*`. `rules` says whether the diagonals
|
|
11
|
+
* are read (`GridRules`).
|
|
12
|
+
*/
|
|
13
|
+
export function judgeWithWords(tiles, words, rules = {}) {
|
|
14
|
+
const read = readWilds(tiles, words, rules) ?? tiles;
|
|
15
|
+
return judgeGrid(read, (codes) => {
|
|
16
|
+
const word = words.wordOf(codes);
|
|
17
|
+
return word !== null && words.allowed.has(word);
|
|
18
|
+
}, (codes) => words.wordOf(codes) ?? codes, rules);
|
|
19
|
+
}
|
package/dist/kana.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JAPANESE KUMIMOJI'S TILES ARE THE 45 BASE KANA, and every other kana is one
|
|
3
|
+
* of them played another way. John, 2026-09-28: "any letters can have it work
|
|
4
|
+
* like the HA letter", and of を, combine it "with something similar".
|
|
5
|
+
*
|
|
6
|
+
* - A voiced or half-voiced kana is its base: が is か, ば and ぱ are は.
|
|
7
|
+
* - A small kana is its large one: ゃ is や, っ is つ.
|
|
8
|
+
* - を is お, which it sounds like; ゐ and ゑ, which no modern word uses, are い and え.
|
|
9
|
+
*
|
|
10
|
+
* So a line of tiles is a word when it spells one read that way, the rule
|
|
11
|
+
* Japanese crosswords have always kept for small kana, and nobody chooses a
|
|
12
|
+
* form: は then ん is はん, ばん and ぱん at once. The word list is stored
|
|
13
|
+
* already read this way (`scripts/word-lists-ja.mjs` folds it with the same
|
|
14
|
+
* table), so checking a line is one lookup.
|
|
15
|
+
*/
|
|
16
|
+
export declare const BASE_KANA = "\u3042\u3044\u3046\u3048\u304A\u304B\u304D\u304F\u3051\u3053\u3055\u3057\u3059\u305B\u305D\u305F\u3061\u3064\u3066\u3068\u306A\u306B\u306C\u306D\u306E\u306F\u3072\u3075\u3078\u307B\u307E\u307F\u3080\u3081\u3082\u3084\u3086\u3088\u3089\u308A\u308B\u308C\u308D\u308F\u3093";
|
|
17
|
+
/** The tile a kana is played with, or null for anything that is not hiragana (ー, katakana, a letter). */
|
|
18
|
+
export declare function tileKana(kana: string): string | null;
|
|
19
|
+
/**
|
|
20
|
+
* WHAT A TILE SHOWS SMALL IN ITS CORNER: the other ways it is commonly played
|
|
21
|
+
* (John, 2026-09-28: "the tiles like Yu can show the regular and small version
|
|
22
|
+
* in that tile to show its versatility"). Only the forms a player reaches for;
|
|
23
|
+
* the rare ones (ぁ, ゎ, ゔ) still play, and the rules say so.
|
|
24
|
+
*/
|
|
25
|
+
export declare const CORNER_FORMS: Readonly<Record<string, string>>;
|
package/dist/kana.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JAPANESE KUMIMOJI'S TILES ARE THE 45 BASE KANA, and every other kana is one
|
|
3
|
+
* of them played another way. John, 2026-09-28: "any letters can have it work
|
|
4
|
+
* like the HA letter", and of を, combine it "with something similar".
|
|
5
|
+
*
|
|
6
|
+
* - A voiced or half-voiced kana is its base: が is か, ば and ぱ are は.
|
|
7
|
+
* - A small kana is its large one: ゃ is や, っ is つ.
|
|
8
|
+
* - を is お, which it sounds like; ゐ and ゑ, which no modern word uses, are い and え.
|
|
9
|
+
*
|
|
10
|
+
* So a line of tiles is a word when it spells one read that way, the rule
|
|
11
|
+
* Japanese crosswords have always kept for small kana, and nobody chooses a
|
|
12
|
+
* form: は then ん is はん, ばん and ぱん at once. The word list is stored
|
|
13
|
+
* already read this way (`scripts/word-lists-ja.mjs` folds it with the same
|
|
14
|
+
* table), so checking a line is one lookup.
|
|
15
|
+
*/
|
|
16
|
+
export const BASE_KANA = "あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわん";
|
|
17
|
+
const SMALL_AND_OLD = {
|
|
18
|
+
ぁ: "あ", ぃ: "い", ぅ: "う", ぇ: "え", ぉ: "お", っ: "つ", ゃ: "や", ゅ: "ゆ", ょ: "よ", ゎ: "わ", ゕ: "か", ゖ: "け", を: "お", ゐ: "い", ゑ: "え",
|
|
19
|
+
};
|
|
20
|
+
/** The tile a kana is played with, or null for anything that is not hiragana (ー, katakana, a letter). */
|
|
21
|
+
export function tileKana(kana) {
|
|
22
|
+
const bare = kana.normalize("NFD").replace(/[゙゚]/gu, "").normalize("NFC");
|
|
23
|
+
const base = SMALL_AND_OLD[bare] ?? bare;
|
|
24
|
+
return base.length === 1 && BASE_KANA.includes(base) ? base : null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* WHAT A TILE SHOWS SMALL IN ITS CORNER: the other ways it is commonly played
|
|
28
|
+
* (John, 2026-09-28: "the tiles like Yu can show the regular and small version
|
|
29
|
+
* in that tile to show its versatility"). Only the forms a player reaches for;
|
|
30
|
+
* the rare ones (ぁ, ゎ, ゔ) still play, and the rules say so.
|
|
31
|
+
*/
|
|
32
|
+
export const CORNER_FORMS = {
|
|
33
|
+
か: "が", き: "ぎ", く: "ぐ", け: "げ", こ: "ご",
|
|
34
|
+
さ: "ざ", し: "じ", す: "ず", せ: "ぜ", そ: "ぞ",
|
|
35
|
+
た: "だ", ち: "ぢ", つ: "っづ", て: "で", と: "ど",
|
|
36
|
+
は: "ばぱ", ひ: "びぴ", ふ: "ぶぷ", へ: "べぺ", ほ: "ぼぽ",
|
|
37
|
+
や: "ゃ", ゆ: "ゅ", よ: "ょ", お: "を",
|
|
38
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type KumimojiLength = "short" | "medium" | "full";
|
|
2
|
+
/** How many of the bag's tiles are wild: easy the most, hard none (`KUMIMOJI_WILDS`). */
|
|
3
|
+
export type KumimojiLevel = "easy" | "medium" | "hard";
|
|
4
|
+
export type KumimojiLanguage = "english" | "japanese";
|
|
5
|
+
/** How far the player has turned the table to look at it: quarter turns clockwise, 0 to 3. The view alone; the grid never turns. */
|
|
6
|
+
export type Turn = 0 | 1 | 2 | 3;
|
|
7
|
+
/**
|
|
8
|
+
* WHAT A KUMIMOJI WAS SET UP AS, beyond its hand and level: the choices that
|
|
9
|
+
* make one game a different game from another at the same seed, carried
|
|
10
|
+
* together wherever a game is made, checked, kept or raced. Each is optional,
|
|
11
|
+
* and its absence is the default: Short, English, one set, no diagonals.
|
|
12
|
+
*/
|
|
13
|
+
export type KumimojiOptions = {
|
|
14
|
+
gameLength?: KumimojiLength;
|
|
15
|
+
doubleSet?: boolean;
|
|
16
|
+
language?: KumimojiLanguage;
|
|
17
|
+
/**
|
|
18
|
+
* Diagonals (John, 2026-09-28: "we could allow people to play diagonally…
|
|
19
|
+
* An option at startup is the right choice"): every diagonal run of three
|
|
20
|
+
* or more tiles, read top to bottom, must be a word too, and joins its
|
|
21
|
+
* tiles as a word across or down does (`judgeGrid`). Off by default.
|
|
22
|
+
*/
|
|
23
|
+
diagonals?: boolean;
|
|
24
|
+
};
|
|
25
|
+
/** The answer to "does this grid finish the game?": yes, or no and why. */
|
|
26
|
+
export type KumimojiCheck = {
|
|
27
|
+
ok: true;
|
|
28
|
+
} | {
|
|
29
|
+
ok: false;
|
|
30
|
+
reason: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* A GAME DEALT (`generateKumimoji`): the bag, in the order its tiles come out,
|
|
34
|
+
* as one string of tile codes (`*` a wild), and one finished grid of those
|
|
35
|
+
* tiles that proves the bag can be finished, never shown to the player.
|
|
36
|
+
*/
|
|
37
|
+
export type KumimojiDeal = {
|
|
38
|
+
kind: "kumimoji";
|
|
39
|
+
/** The opening hand: 3, 7 or 11 tiles (`KUMIMOJI_HANDS`). */
|
|
40
|
+
size: number;
|
|
41
|
+
level: KumimojiLevel;
|
|
42
|
+
seed: number;
|
|
43
|
+
givens: string;
|
|
44
|
+
solution: string;
|
|
45
|
+
gameLength: KumimojiLength;
|
|
46
|
+
doubleSet: boolean;
|
|
47
|
+
language: KumimojiLanguage;
|
|
48
|
+
diagonals?: boolean;
|
|
49
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|