@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/tileFace.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { BASE_KANA, CORNER_FORMS } from "./kana.js";
|
|
2
|
+
/**
|
|
3
|
+
* WHAT A TILE CODE SHOWS, without the word list. A bag, a kept game and a
|
|
4
|
+
* finished grid hold one character a tile: an English letter as itself (a wild
|
|
5
|
+
* given a letter is that letter in capitals, a wild with none is `*`), and a
|
|
6
|
+
* Japanese kana as a private-use character, its place in `BASE_KANA` above
|
|
7
|
+
* `KANA_TILE_START`, or above `KANA_WILD_START` for a wild given that kana.
|
|
8
|
+
* So the set-up's preview and a finished game's page draw Japanese tiles
|
|
9
|
+
* without fetching the dictionary, and `tileWords` reads the same codes.
|
|
10
|
+
*/
|
|
11
|
+
export const KANA_TILE_START = 0xe000;
|
|
12
|
+
export const KANA_WILD_START = 0xf000;
|
|
13
|
+
const KANA = [...BASE_KANA];
|
|
14
|
+
/** The code of a base kana's tile, or of a wild given that kana. */
|
|
15
|
+
export function kanaTileCode(kana, wild = false) {
|
|
16
|
+
const at = KANA.indexOf(kana);
|
|
17
|
+
return at === -1 ? null : String.fromCodePoint((wild ? KANA_WILD_START : KANA_TILE_START) + at);
|
|
18
|
+
}
|
|
19
|
+
export function tileFace(tile) {
|
|
20
|
+
if (tile === "*")
|
|
21
|
+
return { glyph: "五", forms: "", wild: true, blank: true };
|
|
22
|
+
if (/^[A-Z]$/.test(tile))
|
|
23
|
+
return { glyph: tile.toLowerCase(), forms: "", wild: true, blank: false };
|
|
24
|
+
const point = tile.length === 1 ? tile.codePointAt(0) : -1;
|
|
25
|
+
const kana = KANA[point - KANA_TILE_START];
|
|
26
|
+
if (kana !== undefined)
|
|
27
|
+
return { glyph: kana, forms: CORNER_FORMS[kana] ?? "", wild: false, blank: false };
|
|
28
|
+
const wildKana = KANA[point - KANA_WILD_START];
|
|
29
|
+
if (wildKana !== undefined)
|
|
30
|
+
return { glyph: wildKana, forms: "", wild: true, blank: false };
|
|
31
|
+
return { glyph: tile, forms: "", wild: false, blank: false };
|
|
32
|
+
}
|
|
33
|
+
/** What a screen reader says for a tile: its letter, or that it is a wild and what it stands for. */
|
|
34
|
+
export function tileDescription(tile) {
|
|
35
|
+
const face = tileFace(tile);
|
|
36
|
+
if (face.wild)
|
|
37
|
+
return `Wild, ${face.blank ? "unassigned" : face.glyph}`;
|
|
38
|
+
return face.glyph.toUpperCase();
|
|
39
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { KumimojiLanguage } from "./kumimoji.types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* WHICH TILE OF THE SET A TILE IS, without the word lists: a letter is
|
|
4
|
+
* itself, and any wild — blank or given a face — is the wild. The same answer
|
|
5
|
+
* the loaded list's `familyKey` gives (`tileWords.ts`), for code that must
|
|
6
|
+
* never load a dictionary: the server checking a party table's move on
|
|
7
|
+
* several devices (docs/plans/party-online/README.md, John 2026-09-29: the
|
|
8
|
+
* browser checks the words, the server only that the tiles are real).
|
|
9
|
+
* `tileFamily.test.ts` holds the two to one another.
|
|
10
|
+
*/
|
|
11
|
+
export declare function familyKeyOf(language: KumimojiLanguage): (tile: string) => string | null;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { BASE_KANA } from "./kana.js";
|
|
2
|
+
import { KANA_TILE_START, KANA_WILD_START, tileFace } from "./tileFace.js";
|
|
3
|
+
/**
|
|
4
|
+
* WHICH TILE OF THE SET A TILE IS, without the word lists: a letter is
|
|
5
|
+
* itself, and any wild — blank or given a face — is the wild. The same answer
|
|
6
|
+
* the loaded list's `familyKey` gives (`tileWords.ts`), for code that must
|
|
7
|
+
* never load a dictionary: the server checking a party table's move on
|
|
8
|
+
* several devices (docs/plans/party-online/README.md, John 2026-09-29: the
|
|
9
|
+
* browser checks the words, the server only that the tiles are real).
|
|
10
|
+
* `tileFamily.test.ts` holds the two to one another.
|
|
11
|
+
*/
|
|
12
|
+
export function familyKeyOf(language) {
|
|
13
|
+
if (language === "english") {
|
|
14
|
+
return (tile) => (tile === "*" || /^[A-Z]$/.test(tile) ? "*" : /^[a-z]$/.test(tile) ? tile : null);
|
|
15
|
+
}
|
|
16
|
+
const offset = (tile, start) => (tile.length === 1 ? tile.codePointAt(0) - start : -1);
|
|
17
|
+
const within = (at) => at >= 0 && at < [...BASE_KANA].length;
|
|
18
|
+
return (tile) => {
|
|
19
|
+
if (tile === "*" || within(offset(tile, KANA_WILD_START)))
|
|
20
|
+
return "*";
|
|
21
|
+
return within(offset(tile, KANA_TILE_START)) ? tileFace(tile).glyph : null;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { KumimojiLanguage } from "./kumimoji.types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* KUMIMOJI'S WORD LIST, loaded once, when a game opens.
|
|
4
|
+
*
|
|
5
|
+
* The list is about six hundred kilobytes before compression (a hundred and
|
|
6
|
+
* ten thousand words, `words.en.data.ts`), so it is its own module, fetched by
|
|
7
|
+
* a dynamic import only when a Kumimoji is made or checked: no other page, and
|
|
8
|
+
* not the set-up screen, carries a byte of it. The kana Gomoji loads its lists
|
|
9
|
+
* the same way (`kanaWords.ts`).
|
|
10
|
+
*
|
|
11
|
+
* `loadTileWords` fetches and reads it once; `tileWords` hands back the list
|
|
12
|
+
* already loaded, and refuses rather than answering for a list it does not
|
|
13
|
+
* have — a check that could not read the list must not say a word is fine.
|
|
14
|
+
*/
|
|
15
|
+
export type TileWords = {
|
|
16
|
+
language: KumimojiLanguage;
|
|
17
|
+
/** Every word, for asking "is this a word?". */
|
|
18
|
+
allowed: ReadonlySet<string>;
|
|
19
|
+
/** Encoded tile words of each length, for the generator to choose among. */
|
|
20
|
+
byLength: ReadonlyMap<number, readonly string[]>;
|
|
21
|
+
/** The physical set, keyed by its one-character tile codes. */
|
|
22
|
+
mix: ReadonlyMap<string, number>;
|
|
23
|
+
/** Visible glyph for a tile code. */
|
|
24
|
+
glyphOf: (tile: string) => string;
|
|
25
|
+
/** Reading represented by a tile code, or null for an unassigned wild. */
|
|
26
|
+
soundOf: (tile: string) => string | null;
|
|
27
|
+
/** Turn a run of tile codes into its word, or null for an invalid/unassigned tile. */
|
|
28
|
+
wordOf: (tiles: string) => string | null;
|
|
29
|
+
/** Physical wildcard, with `sound` assigned. */
|
|
30
|
+
wildFor: (sound: string) => string | null;
|
|
31
|
+
/** Whether this code is a wildcard, assigned or not. */
|
|
32
|
+
isWild: (tile: string) => boolean;
|
|
33
|
+
/** Display the sound assigned to a wildcard, or null if it has none. */
|
|
34
|
+
wildSound: (tile: string) => string | null;
|
|
35
|
+
/** Identity used to verify that the exact physical bag was played. */
|
|
36
|
+
inventoryKey: (tile: string) => string | null;
|
|
37
|
+
/** Which tile of the set this is, a wild being any wild. */
|
|
38
|
+
familyKey: (tile: string) => string | null;
|
|
39
|
+
/** The other forms a tile plays as, shown small in its corner: ば and ぱ on は. */
|
|
40
|
+
formsOf: (tile: string) => string;
|
|
41
|
+
/** Possible readings for a wild tile. */
|
|
42
|
+
wildOptions: readonly string[];
|
|
43
|
+
/** Internal tile code for a visible letter or mora. */
|
|
44
|
+
codeOf: (face: string) => string | null;
|
|
45
|
+
};
|
|
46
|
+
/** Front-coded words of one length read back (see `scripts/tile-words.mjs`): a shared-prefix character, then the rest. */
|
|
47
|
+
export declare function unpackLength(packed: string, length: number): string[];
|
|
48
|
+
export declare function unpackTileWords(data: Readonly<Record<number, string>>): TileWords;
|
|
49
|
+
/** Used by `tileWordsModule.ts` only: how to read a list where there is no browser. */
|
|
50
|
+
export declare function readTileWordsWith(source: (language: KumimojiLanguage) => Promise<TileWords>): void;
|
|
51
|
+
/** A list read from its module, for `tileWordsModule.ts`. */
|
|
52
|
+
export declare function tileWordsFrom(language: KumimojiLanguage, data: unknown): TileWords;
|
|
53
|
+
/**
|
|
54
|
+
* THE LIST, ONCE. In a browser (or its worker) it arrives as its own script,
|
|
55
|
+
* fetched by the dynamic import below the first time a Kumimoji needs it.
|
|
56
|
+
*
|
|
57
|
+
* ONLY IN A BROWSER, AND SAID SO WHERE THE BUILD CAN SEE IT. The page's
|
|
58
|
+
* components are drawn on the server too, and a dynamic import in them is a
|
|
59
|
+
* copy of its target in every server function — two lists, 1.3 MB, which put
|
|
60
|
+
* the site's grouped function over its size limit (`functionSizeGate`). The
|
|
61
|
+
* build writes `typeof window` as a constant — "undefined" on the server,
|
|
62
|
+
* "object" in a browser bundle, its worker included — so on the server the
|
|
63
|
+
* branch, and the import with it, is gone before anything is traced. The
|
|
64
|
+
* server reads the list through `tileWordsModule.ts` instead, which only the
|
|
65
|
+
* server's own checks and the tests import.
|
|
66
|
+
*/
|
|
67
|
+
export declare function loadTileWords(language?: KumimojiLanguage): Promise<TileWords>;
|
|
68
|
+
export declare function tileWords(language?: KumimojiLanguage): TileWords;
|
|
69
|
+
/** Whether the list has been fetched yet, for a page to say "loading" rather than throw. */
|
|
70
|
+
export declare function tileWordsReady(language?: KumimojiLanguage): boolean;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { tileKana } from "./kana.js";
|
|
2
|
+
import { KANA_TILE_START, KANA_WILD_START, kanaTileCode, tileFace } from "./tileFace.js";
|
|
3
|
+
import { JAPANESE_TILE_MIX, TILE_MIX } from "./tiles.constants.js";
|
|
4
|
+
const loaded = new Map();
|
|
5
|
+
/** Front-coded words of one length read back (see `scripts/tile-words.mjs`): a shared-prefix character, then the rest. */
|
|
6
|
+
export function unpackLength(packed, length) {
|
|
7
|
+
const words = [];
|
|
8
|
+
const text = packed.replace(/\s+/g, "");
|
|
9
|
+
let before = "";
|
|
10
|
+
let at = 0;
|
|
11
|
+
while (at < text.length) {
|
|
12
|
+
const shared = parseInt(text[at], 16);
|
|
13
|
+
const rest = length - shared;
|
|
14
|
+
const word = before.slice(0, shared) + text.slice(at + 1, at + 1 + rest);
|
|
15
|
+
words.push(word);
|
|
16
|
+
before = word;
|
|
17
|
+
at += 1 + rest;
|
|
18
|
+
}
|
|
19
|
+
return words;
|
|
20
|
+
}
|
|
21
|
+
export function unpackTileWords(data) {
|
|
22
|
+
const byLength = new Map();
|
|
23
|
+
const allowed = new Set();
|
|
24
|
+
for (const [length, packed] of Object.entries(data)) {
|
|
25
|
+
const words = unpackLength(packed, Number(length));
|
|
26
|
+
byLength.set(Number(length), words);
|
|
27
|
+
for (const word of words)
|
|
28
|
+
allowed.add(word);
|
|
29
|
+
}
|
|
30
|
+
const letters = [...Object.keys(TILE_MIX)];
|
|
31
|
+
const assigned = new Map(letters.map((letter) => [letter.toUpperCase(), letter]));
|
|
32
|
+
const soundOf = (tile) => (tile === "*" ? null : assigned.get(tile) ?? (/^[a-z]$/.test(tile) ? tile : null));
|
|
33
|
+
const isWild = (tile) => tile === "*" || assigned.has(tile);
|
|
34
|
+
return {
|
|
35
|
+
language: "english",
|
|
36
|
+
allowed,
|
|
37
|
+
byLength,
|
|
38
|
+
mix: new Map(Object.entries(TILE_MIX)),
|
|
39
|
+
glyphOf: (tile) => soundOf(tile) ?? (tile === "*" ? "五" : tile),
|
|
40
|
+
soundOf,
|
|
41
|
+
wordOf: (tiles) => {
|
|
42
|
+
const sounds = [...tiles].map(soundOf);
|
|
43
|
+
return sounds.some((sound) => sound === null) ? null : sounds.join("");
|
|
44
|
+
},
|
|
45
|
+
wildFor: (sound) => (/^[a-z]$/.test(sound) ? sound.toUpperCase() : null),
|
|
46
|
+
isWild,
|
|
47
|
+
wildSound: (tile) => (assigned.has(tile) ? assigned.get(tile) : null),
|
|
48
|
+
inventoryKey: (tile) => (isWild(tile) ? "*" : /^[a-z]$/.test(tile) ? tile : null),
|
|
49
|
+
familyKey: (tile) => (isWild(tile) ? "*" : /^[a-z]$/.test(tile) ? tile : null),
|
|
50
|
+
formsOf: () => "",
|
|
51
|
+
wildOptions: letters,
|
|
52
|
+
codeOf: (face) => (/^[a-z]$/.test(face) ? face : null),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The Japanese list, spelt in the 45 base kana (`kana.ts`): a tile is its
|
|
57
|
+
* kana's code above `KANA_TILE_START`, so a line of tiles reads straight off
|
|
58
|
+
* as the folded word it spells, and が, ゃ and を are found by the tile they
|
|
59
|
+
* are played with.
|
|
60
|
+
*/
|
|
61
|
+
function unpackJapaneseWords(data) {
|
|
62
|
+
const kana = [...data.kana];
|
|
63
|
+
if (kana.some((face, at) => kanaTileCode(face) !== String.fromCodePoint(KANA_TILE_START + at)))
|
|
64
|
+
throw new Error("The Japanese Kumimoji list's kana are not the tiles' own order.");
|
|
65
|
+
const tileOfPacked = new Map([...data.codes].map((code, at) => [code, kanaTileCode(kana[at])]));
|
|
66
|
+
const byLength = new Map();
|
|
67
|
+
const allowed = new Set();
|
|
68
|
+
for (const [length, packed] of Object.entries(data.byLength)) {
|
|
69
|
+
const words = unpackLength(packed, Number(length)).map((word) => [...word].map((code) => tileOfPacked.get(code)).join(""));
|
|
70
|
+
byLength.set(Number(length), words);
|
|
71
|
+
for (const word of words)
|
|
72
|
+
allowed.add([...word].map((tile) => tileFace(tile).glyph).join(""));
|
|
73
|
+
}
|
|
74
|
+
const mix = new Map(Object.entries(JAPANESE_TILE_MIX).map(([face, count]) => [kanaTileCode(face), count]));
|
|
75
|
+
const isTile = (tile) => tile.length === 1 && tile.codePointAt(0) - KANA_TILE_START >= 0 && tile.codePointAt(0) - KANA_TILE_START < kana.length;
|
|
76
|
+
const isAssignedWild = (tile) => tile.length === 1 && tile.codePointAt(0) - KANA_WILD_START >= 0 && tile.codePointAt(0) - KANA_WILD_START < kana.length;
|
|
77
|
+
const isWild = (tile) => tile === "*" || isAssignedWild(tile);
|
|
78
|
+
const soundOf = (tile) => (isTile(tile) || isAssignedWild(tile) ? tileFace(tile).glyph : null);
|
|
79
|
+
const identity = (tile) => (isWild(tile) ? "*" : isTile(tile) ? tileFace(tile).glyph : null);
|
|
80
|
+
return {
|
|
81
|
+
language: "japanese",
|
|
82
|
+
allowed,
|
|
83
|
+
byLength,
|
|
84
|
+
mix,
|
|
85
|
+
glyphOf: (tile) => tileFace(tile).glyph,
|
|
86
|
+
soundOf,
|
|
87
|
+
wordOf: (tiles) => {
|
|
88
|
+
const sounds = [...tiles].map(soundOf);
|
|
89
|
+
return sounds.some((sound) => sound === null) ? null : sounds.join("");
|
|
90
|
+
},
|
|
91
|
+
wildFor: (sound) => {
|
|
92
|
+
const base = tileKana(sound);
|
|
93
|
+
return base === null ? null : kanaTileCode(base, true);
|
|
94
|
+
},
|
|
95
|
+
isWild,
|
|
96
|
+
wildSound: (tile) => (isAssignedWild(tile) ? tileFace(tile).glyph : null),
|
|
97
|
+
inventoryKey: identity,
|
|
98
|
+
familyKey: identity,
|
|
99
|
+
formsOf: (tile) => (isTile(tile) ? tileFace(tile).forms : ""),
|
|
100
|
+
wildOptions: kana,
|
|
101
|
+
codeOf: (face) => {
|
|
102
|
+
const base = tileKana(face);
|
|
103
|
+
return base === null ? null : kanaTileCode(base);
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Where the lists come from where there is no browser — a server checking a
|
|
109
|
+
* solve, a unit test, a browser spec's own process — registered by the
|
|
110
|
+
* loader that imports them (`tileWordsModule.ts`). Null until one does.
|
|
111
|
+
*/
|
|
112
|
+
let fromModule = null;
|
|
113
|
+
/** Used by `tileWordsModule.ts` only: how to read a list where there is no browser. */
|
|
114
|
+
export function readTileWordsWith(source) {
|
|
115
|
+
fromModule = source;
|
|
116
|
+
}
|
|
117
|
+
/** A list read from its module, for `tileWordsModule.ts`. */
|
|
118
|
+
export function tileWordsFrom(language, data) {
|
|
119
|
+
return language === "english"
|
|
120
|
+
? unpackTileWords(data)
|
|
121
|
+
: unpackJapaneseWords(data);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* THE LIST, ONCE. In a browser (or its worker) it arrives as its own script,
|
|
125
|
+
* fetched by the dynamic import below the first time a Kumimoji needs it.
|
|
126
|
+
*
|
|
127
|
+
* ONLY IN A BROWSER, AND SAID SO WHERE THE BUILD CAN SEE IT. The page's
|
|
128
|
+
* components are drawn on the server too, and a dynamic import in them is a
|
|
129
|
+
* copy of its target in every server function — two lists, 1.3 MB, which put
|
|
130
|
+
* the site's grouped function over its size limit (`functionSizeGate`). The
|
|
131
|
+
* build writes `typeof window` as a constant — "undefined" on the server,
|
|
132
|
+
* "object" in a browser bundle, its worker included — so on the server the
|
|
133
|
+
* branch, and the import with it, is gone before anything is traced. The
|
|
134
|
+
* server reads the list through `tileWordsModule.ts` instead, which only the
|
|
135
|
+
* server's own checks and the tests import.
|
|
136
|
+
*/
|
|
137
|
+
export async function loadTileWords(language = "english") {
|
|
138
|
+
const already = loaded.get(language);
|
|
139
|
+
if (already !== undefined)
|
|
140
|
+
return already;
|
|
141
|
+
let words;
|
|
142
|
+
if (typeof window !== "undefined") {
|
|
143
|
+
words = language === "english"
|
|
144
|
+
? unpackTileWords((await import("./words.en.data.js")).TILE_WORDS_EN)
|
|
145
|
+
: unpackJapaneseWords((await import("./words.ja.data.js")).TILE_WORDS_JA);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
if (fromModule === null)
|
|
149
|
+
throw new Error("Kumimoji's words are read on the server through tileWordsModule.ts (loadTileWordsFromModule), which was not imported.");
|
|
150
|
+
words = await fromModule(language);
|
|
151
|
+
}
|
|
152
|
+
loaded.set(language, words);
|
|
153
|
+
return words;
|
|
154
|
+
}
|
|
155
|
+
export function tileWords(language = "english") {
|
|
156
|
+
const words = loaded.get(language);
|
|
157
|
+
if (words === undefined)
|
|
158
|
+
throw new Error(`The ${language} Kumimoji words have not been loaded (loadTileWords).`);
|
|
159
|
+
return words;
|
|
160
|
+
}
|
|
161
|
+
/** Whether the list has been fetched yet, for a page to say "loading" rather than throw. */
|
|
162
|
+
export function tileWordsReady(language = "english") {
|
|
163
|
+
return loaded.has(language);
|
|
164
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type TileWords } from "./tileWords.ts";
|
|
2
|
+
import type { KumimojiLanguage } from "./kumimoji.types.ts";
|
|
3
|
+
/** The list, read from its module: `loadTileWords` for a caller with no browser. */
|
|
4
|
+
export declare function loadTileWordsFromModule(language?: KumimojiLanguage): Promise<TileWords>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { loadTileWords, readTileWordsWith, tileWordsFrom } from "./tileWords.js";
|
|
2
|
+
/**
|
|
3
|
+
* KUMIMOJI'S LISTS WHERE THERE IS NO BROWSER: a server checking a handed-in
|
|
4
|
+
* solve or a race (`preparePuzzleOnServer`), a unit test, a browser spec's own
|
|
5
|
+
* process. Importing this module is what lets `loadTileWords` answer there; a
|
|
6
|
+
* page's components never import it, so no page's server function carries the
|
|
7
|
+
* lists for the browser's sake (see `loadTileWords`).
|
|
8
|
+
*/
|
|
9
|
+
readTileWordsWith(async (language) => tileWordsFrom(language, language === "english" ? (await import("./words.en.data.js")).TILE_WORDS_EN : (await import("./words.ja.data.js")).TILE_WORDS_JA));
|
|
10
|
+
/** The list, read from its module: `loadTileWords` for a caller with no browser. */
|
|
11
|
+
export function loadTileWordsFromModule(language = "english") {
|
|
12
|
+
return loadTileWords(language);
|
|
13
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { KumimojiLength } from "./kumimoji.types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* KUMIMOJI 組文字: build one crossword of your own from letter tiles drawn
|
|
4
|
+
* from a bag, and draw more whenever the hand is used and the grid is sound.
|
|
5
|
+
*
|
|
6
|
+
* Every number the game is played by is here, in one table each, so John can
|
|
7
|
+
* tune the game without reading the code that plays it.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* THE MIX: how many of each letter a full set holds, 144 tiles in all. This is
|
|
11
|
+
* the letter-frequency table of the best-known anagram-grid tile game, a fact
|
|
12
|
+
* about how often English uses its letters and nobody's rule text. Short and
|
|
13
|
+
* Medium games draw from it; Full uses all 144 tiles. Double uses two copies.
|
|
14
|
+
*
|
|
15
|
+
* To tune the letters, change the counts here. The total is checked by
|
|
16
|
+
* `tiles.test.ts`, so an edit that loses a tile says so.
|
|
17
|
+
*/
|
|
18
|
+
export declare const TILE_MIX: Readonly<Record<string, number>>;
|
|
19
|
+
/**
|
|
20
|
+
* THE JAPANESE SET: 144 hiragana tiles in 45 kinds (`kana.ts`), as the English
|
|
21
|
+
* set is 144 letters. Shared by how often each kana is used in the commonest
|
|
22
|
+
* words — the answers the kana Gomoji hides, each kana read as its tile — with
|
|
23
|
+
* one of any kana that comes out below one; measured that way, English comes
|
|
24
|
+
* out the shape of the table above. So ぬ, へ, ね, ろ, れ, む and の are the
|
|
25
|
+
* hard tiles, Japanese's Q, X and Z, and う, ん, い and し, which end and join
|
|
26
|
+
* everything, are its E's. John, 2026-09-28: "the letters that you don't
|
|
27
|
+
* really wanna get… should really be low counts just like Z and XNQ".
|
|
28
|
+
*
|
|
29
|
+
* Fixed here rather than taken from the monthly dictionary refresh, which
|
|
30
|
+
* prints what it measures beside this table (`scripts/word-lists-ja.mjs`): a
|
|
31
|
+
* kept game's bag is checked against these counts.
|
|
32
|
+
*/
|
|
33
|
+
export declare const JAPANESE_TILE_MIX: Readonly<Record<string, number>>;
|
|
34
|
+
/** The whole set, counted from the table. */
|
|
35
|
+
export declare const TILE_MIX_TOTAL: number;
|
|
36
|
+
/**
|
|
37
|
+
* THERE IS NO BOARD. John, 2026-09-26: "curious if we just have a large board
|
|
38
|
+
* that iphone users have to zoom in and out, scroll, etc… since literally
|
|
39
|
+
* there is no board in the real world game." The tiles lie on a table that
|
|
40
|
+
* grows with them (`tableView.ts`), so a grid is as wide and as tall as its
|
|
41
|
+
* player builds it. This is only the most a finished grid may measure either
|
|
42
|
+
* way before the check refuses it unread: fifty tiles in one line is fifty.
|
|
43
|
+
*/
|
|
44
|
+
export declare const KUMIMOJI_GRID_MOST = 60;
|
|
45
|
+
/**
|
|
46
|
+
* THE HANDS, which are the puzzle's sizes: the tiles a game opens with. John,
|
|
47
|
+
* 2026-09-26: "you were given seven or 11 starting tiles". Three is for the
|
|
48
|
+
* browser tests alone — a game that can be finished in a few presses — and is
|
|
49
|
+
* never offered on the set-up screen.
|
|
50
|
+
*/
|
|
51
|
+
export declare const KUMIMOJI_HANDS: {
|
|
52
|
+
readonly tiny: 3;
|
|
53
|
+
readonly quick: 7;
|
|
54
|
+
readonly classic: 11;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* SHORT GAME TOTALS, by opening hand. Medium and Full derive from the tile
|
|
58
|
+
* inventory in `kumimojiTileCount`; these stay the existing Short lengths.
|
|
59
|
+
*/
|
|
60
|
+
export declare const KUMIMOJI_BAG: Readonly<Record<number, number>>;
|
|
61
|
+
/** Wild tiles per original 40/50-tile game, included within the bag total. */
|
|
62
|
+
export declare const KUMIMOJI_WILDS: Readonly<Record<number, Readonly<Record<"easy" | "medium" | "hard", number>>>>;
|
|
63
|
+
/** Tile count for a selected length and inventory size. Double supplies two sets. */
|
|
64
|
+
export declare function kumimojiTileCount(hand: number, length: KumimojiLength, setSize?: number, doubleSet?: boolean): number;
|
|
65
|
+
/** Wilds scale with the bag; doubling the tile set doubles the wild count too. */
|
|
66
|
+
export declare function kumimojiWildCount(hand: number, level: "easy" | "medium" | "hard", totalTiles: number): number;
|
|
67
|
+
/** How many tiles one Draw takes from the bag, once the hand is used and the grid is sound. */
|
|
68
|
+
export declare const KUMIMOJI_DRAW = 1;
|
|
69
|
+
/**
|
|
70
|
+
* THE TRADE: one awkward tile back into the bag for three new ones — the
|
|
71
|
+
* classic way out of a hand of Q, X and J. The tile given back goes to the
|
|
72
|
+
* bottom of the bag, so it comes round again before the end; offered only
|
|
73
|
+
* while the bag has three to give.
|
|
74
|
+
*/
|
|
75
|
+
export declare const KUMIMOJI_TRADE: {
|
|
76
|
+
readonly give: 1;
|
|
77
|
+
readonly take: 3;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* WHAT A FINISHED GAME SCORES on its leaderboard, as the other puzzles score
|
|
81
|
+
* theirs (`puzzlePoints.ts`): ten for every tile laid, and as much again for
|
|
82
|
+
* speed, which falls away evenly until a game that took half a minute a tile
|
|
83
|
+
* earns no speed at all. A Classic game of fifty tiles in ten minutes is
|
|
84
|
+
* 500 + 300 = 800. The time is the browser's clock, as every solo time here is.
|
|
85
|
+
*/
|
|
86
|
+
export declare const KUMIMOJI_SCORE: {
|
|
87
|
+
readonly tile: 10;
|
|
88
|
+
readonly slowestMsATile: 30000;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* PASS AND PLAY: up to eight people round one device, each with a hand and a
|
|
92
|
+
* table of their own from one shared bag (`party.ts`). John, 2026-09-28: "pass
|
|
93
|
+
* and play with up to eight players". One player is the solo game, unchanged.
|
|
94
|
+
* A name is optional and kept to a line: it is shown on the cover and the
|
|
95
|
+
* finish, never sent anywhere.
|
|
96
|
+
*/
|
|
97
|
+
/**
|
|
98
|
+
* `doubleFrom`: from this many players the set-up screen recommends the Double
|
|
99
|
+
* set, and chooses it when the count reaches it (John, 2026-09-28: "when having
|
|
100
|
+
* 6 or more players, we should recommend the double size 288 version"); in
|
|
101
|
+
* Japanese, which has no Double, the Full game instead.
|
|
102
|
+
*/
|
|
103
|
+
export declare const KUMIMOJI_PARTY: {
|
|
104
|
+
readonly least: 2;
|
|
105
|
+
readonly most: 8;
|
|
106
|
+
readonly nameMost: 20;
|
|
107
|
+
readonly doubleFrom: 6;
|
|
108
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KUMIMOJI 組文字: build one crossword of your own from letter tiles drawn
|
|
3
|
+
* from a bag, and draw more whenever the hand is used and the grid is sound.
|
|
4
|
+
*
|
|
5
|
+
* Every number the game is played by is here, in one table each, so John can
|
|
6
|
+
* tune the game without reading the code that plays it.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* THE MIX: how many of each letter a full set holds, 144 tiles in all. This is
|
|
10
|
+
* the letter-frequency table of the best-known anagram-grid tile game, a fact
|
|
11
|
+
* about how often English uses its letters and nobody's rule text. Short and
|
|
12
|
+
* Medium games draw from it; Full uses all 144 tiles. Double uses two copies.
|
|
13
|
+
*
|
|
14
|
+
* To tune the letters, change the counts here. The total is checked by
|
|
15
|
+
* `tiles.test.ts`, so an edit that loses a tile says so.
|
|
16
|
+
*/
|
|
17
|
+
export const TILE_MIX = {
|
|
18
|
+
a: 13, b: 3, c: 3, d: 6, e: 18, f: 3, g: 4, h: 3, i: 12, j: 2, k: 2, l: 5, m: 3,
|
|
19
|
+
n: 8, o: 11, p: 3, q: 2, r: 9, s: 6, t: 9, u: 6, v: 3, w: 3, x: 2, y: 3, z: 2,
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* THE JAPANESE SET: 144 hiragana tiles in 45 kinds (`kana.ts`), as the English
|
|
23
|
+
* set is 144 letters. Shared by how often each kana is used in the commonest
|
|
24
|
+
* words — the answers the kana Gomoji hides, each kana read as its tile — with
|
|
25
|
+
* one of any kana that comes out below one; measured that way, English comes
|
|
26
|
+
* out the shape of the table above. So ぬ, へ, ね, ろ, れ, む and の are the
|
|
27
|
+
* hard tiles, Japanese's Q, X and Z, and う, ん, い and し, which end and join
|
|
28
|
+
* everything, are its E's. John, 2026-09-28: "the letters that you don't
|
|
29
|
+
* really wanna get… should really be low counts just like Z and XNQ".
|
|
30
|
+
*
|
|
31
|
+
* Fixed here rather than taken from the monthly dictionary refresh, which
|
|
32
|
+
* prints what it measures beside this table (`scripts/word-lists-ja.mjs`): a
|
|
33
|
+
* kept game's bag is checked against these counts.
|
|
34
|
+
*/
|
|
35
|
+
export const JAPANESE_TILE_MIX = {
|
|
36
|
+
あ: 2, い: 11, う: 12, え: 1, お: 2, か: 7, き: 6, く: 6, け: 3, こ: 4,
|
|
37
|
+
さ: 3, し: 10, す: 2, せ: 3, そ: 2, た: 4, ち: 3, つ: 6, て: 2, と: 3,
|
|
38
|
+
な: 1, に: 1, ぬ: 1, ね: 1, の: 1, は: 2, ひ: 2, ふ: 2, へ: 1, ほ: 1,
|
|
39
|
+
ま: 2, み: 1, む: 1, め: 1, も: 1, や: 2, ゆ: 4, よ: 6, ら: 1, り: 3,
|
|
40
|
+
る: 3, れ: 1, ろ: 1, わ: 1, ん: 11,
|
|
41
|
+
};
|
|
42
|
+
/** The whole set, counted from the table. */
|
|
43
|
+
export const TILE_MIX_TOTAL = Object.values(TILE_MIX).reduce((sum, count) => sum + count, 0);
|
|
44
|
+
/**
|
|
45
|
+
* THERE IS NO BOARD. John, 2026-09-26: "curious if we just have a large board
|
|
46
|
+
* that iphone users have to zoom in and out, scroll, etc… since literally
|
|
47
|
+
* there is no board in the real world game." The tiles lie on a table that
|
|
48
|
+
* grows with them (`tableView.ts`), so a grid is as wide and as tall as its
|
|
49
|
+
* player builds it. This is only the most a finished grid may measure either
|
|
50
|
+
* way before the check refuses it unread: fifty tiles in one line is fifty.
|
|
51
|
+
*/
|
|
52
|
+
export const KUMIMOJI_GRID_MOST = 60;
|
|
53
|
+
/**
|
|
54
|
+
* THE HANDS, which are the puzzle's sizes: the tiles a game opens with. John,
|
|
55
|
+
* 2026-09-26: "you were given seven or 11 starting tiles". Three is for the
|
|
56
|
+
* browser tests alone — a game that can be finished in a few presses — and is
|
|
57
|
+
* never offered on the set-up screen.
|
|
58
|
+
*/
|
|
59
|
+
export const KUMIMOJI_HANDS = { tiny: 3, quick: 7, classic: 11 };
|
|
60
|
+
/**
|
|
61
|
+
* SHORT GAME TOTALS, by opening hand. Medium and Full derive from the tile
|
|
62
|
+
* inventory in `kumimojiTileCount`; these stay the existing Short lengths.
|
|
63
|
+
*/
|
|
64
|
+
export const KUMIMOJI_BAG = {
|
|
65
|
+
[KUMIMOJI_HANDS.tiny]: 5,
|
|
66
|
+
[KUMIMOJI_HANDS.quick]: 40,
|
|
67
|
+
[KUMIMOJI_HANDS.classic]: 50,
|
|
68
|
+
};
|
|
69
|
+
/** Wild tiles per original 40/50-tile game, included within the bag total. */
|
|
70
|
+
export const KUMIMOJI_WILDS = {
|
|
71
|
+
[KUMIMOJI_HANDS.tiny]: { easy: 0, medium: 0, hard: 0 },
|
|
72
|
+
[KUMIMOJI_HANDS.quick]: { easy: 6, medium: 3, hard: 0 },
|
|
73
|
+
[KUMIMOJI_HANDS.classic]: { easy: 8, medium: 4, hard: 0 },
|
|
74
|
+
};
|
|
75
|
+
/** Tile count for a selected length and inventory size. Double supplies two sets. */
|
|
76
|
+
export function kumimojiTileCount(hand, length, setSize = TILE_MIX_TOTAL, doubleSet = false) {
|
|
77
|
+
const short = KUMIMOJI_BAG[hand];
|
|
78
|
+
if (short === undefined)
|
|
79
|
+
throw new Error(`No Kumimoji with a hand of ${hand}.`);
|
|
80
|
+
if (!Number.isInteger(setSize) || setSize < 1)
|
|
81
|
+
throw new Error(`Invalid Kumimoji set size: ${setSize}.`);
|
|
82
|
+
const multiplier = doubleSet ? 2 : 1;
|
|
83
|
+
const available = setSize * multiplier;
|
|
84
|
+
if (short * multiplier > available)
|
|
85
|
+
throw new Error(`A short game needs more tiles than the ${available}-tile set holds.`);
|
|
86
|
+
if (length === "short")
|
|
87
|
+
return short * multiplier;
|
|
88
|
+
if (length === "medium")
|
|
89
|
+
return Math.floor(available / 2);
|
|
90
|
+
return available;
|
|
91
|
+
}
|
|
92
|
+
/** Wilds scale with the bag; doubling the tile set doubles the wild count too. */
|
|
93
|
+
export function kumimojiWildCount(hand, level, totalTiles) {
|
|
94
|
+
const short = KUMIMOJI_BAG[hand];
|
|
95
|
+
const wilds = KUMIMOJI_WILDS[hand];
|
|
96
|
+
if (short === undefined || wilds === undefined)
|
|
97
|
+
throw new Error(`No Kumimoji with a hand of ${hand}.`);
|
|
98
|
+
if (!Number.isInteger(totalTiles) || totalTiles < short)
|
|
99
|
+
throw new Error(`Invalid Kumimoji tile count: ${totalTiles}.`);
|
|
100
|
+
return Math.round((wilds[level] * totalTiles) / short);
|
|
101
|
+
}
|
|
102
|
+
/** How many tiles one Draw takes from the bag, once the hand is used and the grid is sound. */
|
|
103
|
+
export const KUMIMOJI_DRAW = 1;
|
|
104
|
+
/**
|
|
105
|
+
* THE TRADE: one awkward tile back into the bag for three new ones — the
|
|
106
|
+
* classic way out of a hand of Q, X and J. The tile given back goes to the
|
|
107
|
+
* bottom of the bag, so it comes round again before the end; offered only
|
|
108
|
+
* while the bag has three to give.
|
|
109
|
+
*/
|
|
110
|
+
export const KUMIMOJI_TRADE = { give: 1, take: 3 };
|
|
111
|
+
/**
|
|
112
|
+
* WHAT A FINISHED GAME SCORES on its leaderboard, as the other puzzles score
|
|
113
|
+
* theirs (`puzzlePoints.ts`): ten for every tile laid, and as much again for
|
|
114
|
+
* speed, which falls away evenly until a game that took half a minute a tile
|
|
115
|
+
* earns no speed at all. A Classic game of fifty tiles in ten minutes is
|
|
116
|
+
* 500 + 300 = 800. The time is the browser's clock, as every solo time here is.
|
|
117
|
+
*/
|
|
118
|
+
export const KUMIMOJI_SCORE = { tile: 10, slowestMsATile: 30000 };
|
|
119
|
+
/**
|
|
120
|
+
* PASS AND PLAY: up to eight people round one device, each with a hand and a
|
|
121
|
+
* table of their own from one shared bag (`party.ts`). John, 2026-09-28: "pass
|
|
122
|
+
* and play with up to eight players". One player is the solo game, unchanged.
|
|
123
|
+
* A name is optional and kept to a line: it is shown on the cover and the
|
|
124
|
+
* finish, never sent anywhere.
|
|
125
|
+
*/
|
|
126
|
+
/**
|
|
127
|
+
* `doubleFrom`: from this many players the set-up screen recommends the Double
|
|
128
|
+
* set, and chooses it when the count reaches it (John, 2026-09-28: "when having
|
|
129
|
+
* 6 or more players, we should recommend the double size 288 version"); in
|
|
130
|
+
* Japanese, which has no Double, the Full game instead.
|
|
131
|
+
*/
|
|
132
|
+
export const KUMIMOJI_PARTY = { least: 2, most: 8, nameMost: 20, doubleFrom: 6 };
|
package/dist/turn.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Turn } from "./kumimoji.types.ts";
|
|
2
|
+
import type { Area, View } from "./tableView.ts";
|
|
3
|
+
/**
|
|
4
|
+
* TURNING THE TABLE, WITH EVERY TILE KEPT UPRIGHT. John, 2026-09-28: "if you
|
|
5
|
+
* rotate the board like flip it 180° for example then all the tiles will flip
|
|
6
|
+
* 180° to right themselves directly just so that they're not backwards for you
|
|
7
|
+
* or upside down."
|
|
8
|
+
*
|
|
9
|
+
* So the turn is a mapping of squares, never a rotation of the picture: the
|
|
10
|
+
* grid (its squares, `encodeGrid`, the kept game, the check) stays where the
|
|
11
|
+
* player built it, and only WHERE each square is drawn is turned — about the
|
|
12
|
+
* middle of square (0, 0), a quarter clockwise per press. A tile drawn at its
|
|
13
|
+
* turned place is drawn as it always is, so its letter reads the right way up
|
|
14
|
+
* whichever way the table faces. A point on the screen means the square drawn
|
|
15
|
+
* there, so a tap or a drop lands where the finger is.
|
|
16
|
+
*
|
|
17
|
+
* Pure, like the rest of the view (`tableView.ts`), which works on the turned
|
|
18
|
+
* squares unchanged: fitting, zooming and panning are all in screen terms.
|
|
19
|
+
*/
|
|
20
|
+
/** The next turn a press gives: a quarter clockwise, and four presses back to the start. */
|
|
21
|
+
export declare function nextTurn(turn: Turn): Turn;
|
|
22
|
+
/**
|
|
23
|
+
* Where square (row, col) of the grid is drawn with the table turned: a
|
|
24
|
+
* quarter clockwise sends a word running right to one running down, and one
|
|
25
|
+
* running down to one running left. It is linear, so it turns a step (a
|
|
26
|
+
* direction) as well as a square.
|
|
27
|
+
*/
|
|
28
|
+
export declare function turnPlace(row: number, col: number, turn: Turn): {
|
|
29
|
+
row: number;
|
|
30
|
+
col: number;
|
|
31
|
+
};
|
|
32
|
+
/** The grid's square drawn at (row, col) of the turned table: `turnPlace` undone. */
|
|
33
|
+
export declare function unturnPlace(row: number, col: number, turn: Turn): {
|
|
34
|
+
row: number;
|
|
35
|
+
col: number;
|
|
36
|
+
};
|
|
37
|
+
/** The squares a turned table shows: the grid's area, turned. */
|
|
38
|
+
export declare function turnArea(area: Area, turn: Turn): Area;
|
|
39
|
+
/**
|
|
40
|
+
* A view the player zoomed or panned, turned a quarter clockwise with the
|
|
41
|
+
* table about the middle of its box: the same zoom, and the spot of the table
|
|
42
|
+
* that was in the middle stays there. (A fitted view simply fits again.)
|
|
43
|
+
*
|
|
44
|
+
* On the screen a square's corner is `x + col * tile`; the turn takes a point
|
|
45
|
+
* (row v, col u) of the table to (row u, col 1 - v), which is a quarter turn
|
|
46
|
+
* about the middle of square (0, 0) — the same turn as `turnPlace`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function turnView(view: View, width: number, height: number): View;
|
|
49
|
+
/**
|
|
50
|
+
* Which way typed letters run ON THE SCREEN. Typing lays a word across or
|
|
51
|
+
* down the grid, so that it reads as a word; with the table turned, that is
|
|
52
|
+
* another way on the screen, and the cursor's arrow points it.
|
|
53
|
+
*/
|
|
54
|
+
export declare function typingWay(across: boolean, turn: Turn): {
|
|
55
|
+
arrow: string;
|
|
56
|
+
name: string;
|
|
57
|
+
};
|
|
58
|
+
/** The step in the grid an arrow key means: the square that way on the screen, with the table turned. Null for any other key. */
|
|
59
|
+
export declare function arrowStep(key: string, turn: Turn): {
|
|
60
|
+
row: number;
|
|
61
|
+
col: number;
|
|
62
|
+
} | null;
|