Kumimoji組み文字

@johnmorrisdotca/kumimoji 1.1.1 · 4 entry points · 209 exports

@johnmorrisdotca/kumimoji

afterComputerTurn Area arrowStep assignHandTile assignTableTile BASE_KANA bestLaying Bounds boundsOf checkKumimoji COMPUTER_STEPS_MOST computerName ComputerSaid ComputerStep CORNER_FORMS crossingFit CrossingFit deal decodeGrid decodeParty decodeTileProgress DIAGONAL_RUN_LEAST diagonalsRead doneRefused draw drawAll edgePan encodeGrid encodeParty encodeTileProgress endTurn familyKeyOf fittedMost fitView formsOfTile generateKumimoji goesOut GridRules gridToText GridVerdict groupsOf handCanSpell handSpelling HELP_WORDS_MOST holdsItsBag isComputer isFinished isLastTurn isOver isPartyFor JAPANESE_TILE_MIX joinParty JoinRefusal joinRefused judgeGrid judgeTiles judgeWithWords KANA_TILE_START KANA_WILD_START kanaTileCode keepInReach KUMIMOJI_BAG KUMIMOJI_DRAW KUMIMOJI_EXPORT_FORMAT KUMIMOJI_GRID_MOST KUMIMOJI_HANDS KUMIMOJI_PARTY KUMIMOJI_SCORE KUMIMOJI_STRINGS KUMIMOJI_TRADE KUMIMOJI_VERSION KUMIMOJI_WILDS KumimojiCheck kumimojiClock KumimojiDeal kumimojiExported KumimojiExported kumimojiFromJSON KumimojiLanguage KumimojiLength KumimojiLevel KumimojiLocale KumimojiOptions kumimojiPoints KumimojiSaved kumimojiSay kumimojiStrings KumimojiStrings kumimojiTileCount kumimojiToJSON kumimojiToText kumimojiWildCount laidThisTurn lastStanding leaveParty LeaveRefusal leaveRefused LengthRow lengthRows LENGTHS lettersOf liftAll liftToHand loadTileWords mayDraw mayDrawAll mayResign mayTrade mayTradeThisTurn mixShown MixShown MixTile moveOnTable mustTradeFirst nameOf nextIn nextTurn overflows panView PartyEnding partyFits PartyGame partyLength PartyPlayer partyPlayersAsked PartySeat PartySettings partyTilesLeft partyTilesNeeded passViewStart placeFromHand placeOf planComputerTurn Random readSettings readTileProgress readTileWordsWith readWilds resign Run RunLine runsOf sameLetters seatPlay seededRandom shuffled sortHand Square squareAt squareUnder startParty stepView stillIn swapWithHand TABLE tableArea tableTally tidyName TILE_MIX TILE_MIX_TOTAL tileDescription tileFace TileFaceOf tileKana TilePlay Tiles tilesHeldBy tilesLeft tileWords TileWords tileWordsFrom tileWordsReady trade tradeChoice TRY_IT Turn turnArea turnPlace turnView typingWay unpackLength unpackTileWords unturnPlace View WILD_SEARCH_MOST winnersOf withSeatPlay wordsInHand zoomView

function afterComputerTurn

afterComputerTurn(game: PartyGame, words: TileWords): PartyGame

The game a computer's turn passes on: its plan's last step, or the game as it was where there is nothing to play.

type Area

type Area = { top: number; left: number; rows: number; cols: number };

The squares shown: from top/left, rows by cols.

function arrowStep

arrowStep(key: string, turn: Turn): { row: number; col: number; } | null

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.

function assignHandTile

assignHandTile(play: TilePlay, handAt: number, face: string): TilePlay

Give a hand tile its chosen reading, without changing which physical tile it is.

function assignTableTile

assignTableTile(play: TilePlay, square: string, face: string): TilePlay

Give a tile on the table its chosen reading, without changing its square.

const BASE_KANA

BASE_KANA: "あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわん"

JAPANESE KUMIMOJI'S TILES ARE THE 45 BASE KANA, and every other kana is one of them played another way. John, 2026-09-28: "any letters can have it work like the HA letter", and of を, combine it "with something similar".

- A voiced or half-voiced kana is its base: が is か, ば and ぱ are は. - A small kana is its large one: ゃ is や, っ is つ. - を is お, which it sounds like; ゐ and ゑ, which no modern word uses, are い and え.

So a line of tiles is a word when it spells one read that way, the rule Japanese crosswords have always kept for small kana, and nobody chooses a form: は then ん is はん, ばん and ぱん at once. The word list is stored already read this way (scripts/word-lists-ja.mjs folds it with the same table), so checking a line is one lookup.

function bestLaying

bestLaying(play: TilePlay, words: TileWords, rules?: GridRules): { play: TilePlay; word: string; } | null

THE COMPUTER'S NEXT WORD: its play after laying the best word its hand can lay on its table, and the word, or null when there is none. The grid after it is judged against the list, by the game's rules (rules), and is always sound.

type Bounds

type Bounds = { top: number; left: number; bottom: number; right: number };

The rows and columns a grid's tiles stand within.

function boundsOf

boundsOf(tiles: Tiles): Bounds | null

The rows and columns the tiles stand within, or null for no tiles.

function checkKumimoji

checkKumimoji(size: number, givens: string, answer: string, options?: KumimojiOptions & { level?: KumimojiLevel; }): KumimojiCheck

WHETHER A GRID FINISHES A KUMIMOJI: the one check the server also runs. O(squares) and a lookup a word, nothing searched: the grid uses exactly the tiles of the bag, as many of each, it is all one piece, and every run of two or more across or down is in the list — and, for a game set up with Diagonals, every run of three or more along a diagonal. Which tiles were traded on the way does not matter — a finished game holds the whole bag whatever came out of it when — so the answer is the grid alone.

Written out here rather than asked of the play page, which is what made the grid; the list must have been loaded (loadTileWords), and a check that cannot read it refuses.

const COMPUTER_STEPS_MOST

COMPUTER_STEPS_MOST: 8

The most things a computer does in one turn before pressing Done: at the page's pause, a turn of about three seconds.

function computerName

computerName(players: readonly PartyPlayer[]): string

The name a new computer takes: "Computer" and the lowest number no player at the table already goes by.

type ComputerSaid

type ComputerSaid = | { kind: "rebuilt" } | { kind: "laid"; word: string } | { kind: "drew" } | { kind: "traded"; tile: string } | { kind: "done"; out: boolean } | { kind: "resigned" };

What a computer did in one step of its turn, for the line over its table.

type ComputerStep

type ComputerStep = { game: PartyGame; said: ComputerSaid };

One step of a computer's turn: the game after it, and what it did. The last step's game is the one kept.

const CORNER_FORMS

CORNER_FORMS: Readonly<Record<string, string>>

WHAT A TILE SHOWS SMALL IN ITS CORNER: the other ways it is commonly played (John, 2026-09-28: "the tiles like Yu can show the regular and small version in that tile to show its versatility"). Only the forms a player reaches for; the rare ones (ぁ, ゎ, ゔ) still play, and the rules say so.

function crossingFit

crossingFit(letterAt: (row: number, col: number): string, inside: (row: number, col: number) => boolean, word: string, anchor: Square, at: number, across: boolean, diagonalWord?: ((run: string) => boolean) | null) => CrossingFit | null

Where word stands with its letter at on the tile at anchor, running across or down, or null where it cannot: a square outside what may be used, a tile at either end, a different letter in its way, a new tile with a neighbour at its side, or no new tile at all — and, given diagonalWord (a game with Diagonals), a new tile standing in a diagonal run of three or more that diagonalWord does not take (diagonalsRead).

type CrossingFit

type CrossingFit = { first: Square; fresh: Square[] };

A word's place: its first square, and the squares it lays a new tile on, in the word's order.

function deal

deal(bag: string, handSize: number): TilePlay

The opening hand: the first handSize tiles of the bag, and an empty table.

function decodeGrid

decodeGrid(code: string): Map<string, string> | null

A grid read back, its first row at row 0, or null for anything that is not one: a stray character, a capital, a grid wider or taller than any grid of fifty tiles can be. An empty string is no tiles.

function decodeParty

decodeParty(code: string | null, familyKey?: (tile: string): string | null) => PartyGame | null

A kept game read back, or null for anything that is not one. With familyKey (the loaded word list's), it is also checked against its own bag: every tile held by anybody — hand and table — must be one taken out of it and not given back, as the solo game checks a kept run (decodeTileProgress). Without, its shape alone, which is enough to offer it on the set-up screen.

function decodeTileProgress

decodeTileProgress(code: string, bag: string, language?: "english" | "japanese"): TilePlay | null

A kept game opened again on its own bag, or null where it does not belong to it: every tile held — hand and table — must be one taken from the bag and not given back. Null opens the game fresh rather than on a grid that never came out of this bag.

const DIAGONAL_RUN_LEAST

DIAGONAL_RUN_LEAST: 3

The fewest tiles a diagonal run needs before it is read. Two tiles touching at a corner are what every crossword is full of — the letter above a word and the one beside it — so a pair is free, and only three or more in a line are a diagonal word.

function diagonalsRead

diagonalsRead(letterAt: (row: number, col: number): string, inside: (row: number, col: number) => boolean, word: string, fit: CrossingFit, isWord: (run: string) => boolean) => boolean

Whether every diagonal run a laying's new tiles would stand in is taken by isWord: each walked from its top end down, with the word's own letters on its new squares. Only a run through a new tile can change, and two new tiles of one word never share a diagonal (they share a row or a column), so each is read on its own. A run of two, a corner touch, is never read.

function doneRefused

doneRefused(game: PartyGame, verdict: GridVerdict, handSpells: (hand: readonly string[]): boolean) => "trade" | "standing" | null

Why Done cannot be pressed now, or null when it can: a hand to trade first, or, for the last one standing, no tile laid on a sound grid yet (a hand already used on a sound grid has nothing left to lay, and wins as it is).

function draw

draw(play: TilePlay, count?: number): TilePlay

The next tile or tiles out of the bag into the hand.

function drawAll

drawAll(game: PartyGame): PartyGame

DRAW: every player still in takes one tile, in turn order from the one who pressed it — the race game's rule that when one player uses their hand, everybody draws.

function edgePan

edgePan(px: number, py: number, width: number, height: number): { dx: number; dy: number; }

Which way, and how far, the table pans this frame for a tile dragged to (px, py): toward the edge it is near.

function encodeGrid

encodeGrid(tiles: Tiles): string

A grid as a string, drawn from its own top-left tile: cat/2o/2w. decodeGrid reads it back.

function encodeParty

encodeParty(game: PartyGame): string

A table of players as text to keep. decodeParty reads it back, and given the word list's familyKey checks every tile held against the bag.

function encodeTileProgress

encodeTileProgress(play: TilePlay): string

A GAME KEPT half way, as one string (PuzzleRun.progress): how many tiles were taken, the tiles traded back, the hand in its order and the grid (encodeGrid), as 12:q:ae:cat/2o/2w. The bag is the puzzle's own, from its seed, so it is not written again, and where on the table the grid stood is the view's and not kept.

function endTurn

endTurn(game: PartyGame, verdict: GridVerdict, handSpells: (hand: readonly string[]): boolean) => PartyGame

DONE: the end of a turn, and the device passed on. Before anybody is out, to the next player still in. A player who goes out with it starts the last round: everybody else still in, in order from the next, has one last turn, and any of them who goes out on it shares the win (John, 2026-09-28: "Sharing the win is correct"). After the last of those, the game is over.

The last one standing, everybody else resigned, wins by laying a tile on a sound grid and pressing Done. verdict is the grid of the player whose turn it is. Refused, the game given back as it was, while doneRefused says so.

function familyKeyOf

familyKeyOf(language: KumimojiLanguage): (tile: string) => string | null

WHICH TILE OF THE SET A TILE IS, without the word lists: a letter is itself, and any wild — blank or given a face — is the wild. The same answer the loaded list's familyKey gives (tileWords.ts), for code that must never load a dictionary: the server checking a party table's move on several devices (docs/plans/party-online/README.md, John 2026-09-29: the browser checks the words, the server only that the tiles are real). tileFamily.test.ts holds the two to one another.

function fittedMost

fittedMost(width: number, height: number): number

The largest a fitted tile is drawn in a table this big. tileMost in any table up to growsFrom across its shorter side, which is every phone's and the Regular desk table; past that, in proportion, so a table the player asked to be Large or Full (BoardScale) draws bigger squares rather than only more of them — never past what a hand may zoom to.

function fitView

fitView(area: Area, width: number, height: number, least?: number): View

The view that shows the whole area, centred, its tiles as big as fit between the least and the most. A picture nobody presses (a finished grid, the set-up preview) passes a smaller least, so a whole grid fits its box.

function formsOfTile

formsOfTile(kana: string): string[]

Every hiragana a tile plays as, itself first: は is は, ば and ぱ. None for a kana that is not a tile's own.

function generateKumimoji

generateKumimoji(size: number, level: KumimojiLevel, seed: number, options?: KumimojiOptions): KumimojiDeal

MAKING A KUMIMOJI, in the browser, from a seed: the bag the game is played from, in the order its tiles come out.

A bag drawn blind from the mix can be one nobody can finish — two Q's and no U, and every tile must be laid before the game ends. So the bag is made the other way round: a crossword is laid first, word by word, from letters drawn from the mix (TILE_MIX), and the bag is that crossword's tiles, shuffled. Every bag has at least one finished grid, which is the puzzle's solution — kept only to prove that, never shown — and a player may build any other.

The crossword grows from a word across the middle: each next word crosses a tile already down, its new tiles touching nothing at their sides, so every run on it is a word by construction. Words are chosen to use the letters drawn from the mix, and never a letter more times than the whole set holds, so the bag reads like a handful from the full set.

With Diagonals, the crossword must read as a word along its diagonals too: a new tile that would make a diagonal run of three or more is laid only where that run is a word (fit), so the proof holds under the rule the game is played by. Without, nothing about the laying changes, and a seed deals the bag it always dealt.

Deterministic in the seed, like every generator here: two browsers in a race, or one tomorrow, deal the same bag in the same order.

function goesOut

goesOut(game: PartyGame, verdict: GridVerdict): boolean

Whether the player whose turn it is would go out with Done: their hand used, their grid sound, and too few tiles in the bag for everybody still in to draw.

type GridRules

type GridRules = { diagonals?: boolean };

How a grid is read: whether its diagonals are (KumimojiOptions.diagonals).

function gridToText

gridToText(tiles: Tiles, language?: KumimojiLanguage): string

A grid as lines of text, the way it lies on the table: English letters in capitals with a space between squares and a dot for an empty one, kana side by side with a full-width dot for an empty one, so that either lines up in a fixed-width face, every row as wide as the crossword. A wild that has been given nothing is *. No tiles is "".

type GridVerdict

type GridVerdict = { sound: boolean; tiles: number; /** Tiles standing in a run the list does not know. */ misspelt: ReadonlySet<string>; /** Tiles in a group apart from the largest. */ apart: ReadonlySet<string>; /** The runs that are not words, as spelled, for the line under the table. */ notWords: readonly string[]; };

WHAT IS WRONG WITH A GRID, tile by tile, so the table can mark it: the tiles in a run that is not a word, the tiles not joined to the main grid, and whether the whole is sound — two tiles or more, all joined, every run a word. With Diagonals (rules), the diagonal runs of three or more are runs too: each must be a word, a misspelt one is marked as any other is, and each joins its tiles (groupsOf).

function groupsOf

groupsOf(tiles: Tiles, links?: readonly Run[]): string[][]

The tiles in groups that touch across or down, largest first. With links — the diagonal runs a grid with Diagonals reads — the tiles next to each other in one of those runs are joined too: a diagonal word holds a crossword together as a word across does. A pair touching at a corner is never a link, because it is never read.

function handCanSpell

handCanSpell(hand: readonly string[], words: TileWords): boolean

Whether a hand can spell a word from the list: any word of two tiles or more, a wild counting as able to be anything.

function handSpelling

handSpelling(play: TilePlay, word: string): TilePlay

The hand with word's tiles first, in its order, and the rest after them as they were.

const HELP_WORDS_MOST

HELP_WORDS_MOST: 40

How many words one hand offers before the presses go round again: the longest first.

function holdsItsBag

holdsItsBag(game: PartyGame, familyKey: (tile: string): string | null) => boolean

Whether what everybody holds is exactly what was taken from the bag, less what was given back.

function isComputer

isComputer(game: PartyGame, at: number): boolean

Whether a computer plays this seat.

function isFinished

isFinished(play: TilePlay, verdict: GridVerdict): boolean

Whether the game is over: the bag empty, the hand used, and the grid sound.

function isLastTurn

isLastTurn(game: PartyGame): boolean

Whether the turn now being played is a player's last.

function isOver

isOver(game: PartyGame): boolean

Whether the game is over.

function isPartyFor

isPartyFor(game: PartyGame, settings: PartySettings, players: number): boolean

Whether a kept game is the one an address asks for: the same settings, and dealt to as many players, whoever has joined or left since.

const JAPANESE_TILE_MIX

JAPANESE_TILE_MIX: Readonly<Record<string, number>>

THE JAPANESE SET: 144 hiragana tiles in 45 kinds (kana.ts), as the English set is 144 letters. Shared by how often each kana is used in the commonest words — the answers the kana Gomoji hides, each kana read as its tile — with one of any kana that comes out below one; measured that way, English comes out the shape of the table above. So ぬ, へ, ね, ろ, れ, む and の are the hard tiles, Japanese's Q, X and Z, and う, ん, い and し, which end and join everything, are its E's. John, 2026-09-28: "the letters that you don't really wanna get… should really be low counts just like Z and XNQ".

Fixed here rather than taken from the monthly dictionary refresh, which prints what it measures beside this table (scripts/word-lists-ja.mjs): a kept game's bag is checked against these counts.

function joinParty

joinParty(game: PartyGame, seat: PartySeat): PartyGame

A player, or a computer, sat down after the last seat with the next hand out of the bag.

type JoinRefusal

type JoinRefusal = "over" | "full" | "lastRound" | "bag";

Why nobody can join now, or null when a hand can be dealt.

function joinRefused

joinRefused(game: PartyGame): JoinRefusal | null

Why nobody can join the table now, or null when a hand can be dealt.

function judgeGrid

judgeGrid(tiles: Tiles, isWord: (word: string): boolean, readable?: (word: string) => string, rules?: GridRules) => GridVerdict

A grid judged against any test of a word: isWord answers for a run as spelt, and readable says how to name one that is not. judgeWithWords is this with a language's list.

function judgeTiles

judgeTiles(tiles: Tiles, words: TileWords, rules?: GridRules): GridVerdict

Whether a grid is sound, read against the loaded word list by the game's rules: the same judgement the desk shows.

function judgeWithWords

judgeWithWords(tiles: Tiles, words: TileWords, rules?: GridRules): GridVerdict

A GRID JUDGED AGAINST ITS LANGUAGE'S LIST, the one way the solo game, each seat of a pass-and-play game and the server's check all ask it: a run is a word when its tiles spell one the list holds (wordOf, so a wild reads as the letter or kana it was given), and a run that is not is named as it reads. A wild given no letter reads as whichever letter makes every run through it a word (readWilds); where none does, the grid is judged as laid and the run is named with its *. rules says whether the diagonals are read (GridRules).

const KANA_TILE_START

KANA_TILE_START: 57344

WHAT A TILE CODE SHOWS, without the word list. A bag, a kept game and a finished grid hold one character a tile: an English letter as itself (a wild given a letter is that letter in capitals, a wild with none is *), and a Japanese kana as a private-use character, its place in BASE_KANA above KANA_TILE_START, or above KANA_WILD_START for a wild given that kana. So the set-up's preview and a finished game's page draw Japanese tiles without fetching the dictionary, and tileWords reads the same codes.

const KANA_WILD_START

KANA_WILD_START: 61440

Where the codes of wild tiles given a kana begin: the kana's place in BASE_KANA above this.

function kanaTileCode

kanaTileCode(kana: string, wild?: boolean): string | null

The code of a base kana's tile, or of a wild given that kana.

function keepInReach

keepInReach(view: View, area: Area, width: number, height: number): View

Kept within reach: some of the area always on the screen, so a pan can never lose the crossword. A quarter of the screen or two tiles, whichever is less, stays in view on every side.

const KUMIMOJI_BAG

KUMIMOJI_BAG: Readonly<Record<number, number>>

SHORT GAME TOTALS, by opening hand. Medium and Full derive from the tile inventory in kumimojiTileCount; these stay the existing Short lengths.

const KUMIMOJI_DRAW

KUMIMOJI_DRAW: 1

How many tiles one Draw takes from the bag, once the hand is used and the grid is sound.

const KUMIMOJI_EXPORT_FORMAT

KUMIMOJI_EXPORT_FORMAT: 1

The shape of the JSON this package writes. It goes up only when a reader of the old shape would be wrong about the new one.

const KUMIMOJI_GRID_MOST

KUMIMOJI_GRID_MOST: 60

THERE IS NO BOARD. John, 2026-09-26: "curious if we just have a large board that iphone users have to zoom in and out, scroll, etc… since literally there is no board in the real world game." The tiles lie on a table that grows with them (tableView.ts), so a grid is as wide and as tall as its player builds it. This is only the most a finished grid may measure either way before the check refuses it unread: fifty tiles in one line is fifty.

const KUMIMOJI_HANDS

KUMIMOJI_HANDS: { readonly tiny: 3; readonly quick: 7; readonly classic: 11; }

THE HANDS, which are the puzzle's sizes: the tiles a game opens with. John, 2026-09-26: "you were given seven or 11 starting tiles". Three is for the browser tests alone — a game that can be finished in a few presses — and is never offered on the set-up screen.

const KUMIMOJI_PARTY

KUMIMOJI_PARTY: { readonly least: 2; readonly most: 8; readonly nameMost: 20; readonly doubleFrom: 6; }

doubleFrom: from this many players the set-up screen recommends the Double set, and chooses it when the count reaches it (John, 2026-09-28: "when having 6 or more players, we should recommend the double size 288 version"); in Japanese, which has no Double, the Full game instead.

const KUMIMOJI_SCORE

KUMIMOJI_SCORE: { readonly tile: 10; readonly slowestMsATile: 30000; }

WHAT A FINISHED GAME SCORES on its leaderboard, as the other puzzles score theirs (puzzlePoints.ts): ten for every tile laid, and as much again for speed, which falls away evenly until a game that took half a minute a tile earns no speed at all. A Classic game of fifty tiles in ten minutes is 500 + 300 = 800. The time is the browser's clock, as every solo time here is.

const KUMIMOJI_STRINGS

KUMIMOJI_STRINGS: Readonly<Record<KumimojiLocale, KumimojiStrings>>

The table's words in each language it speaks: KUMIMOJI_STRINGS.en, KUMIMOJI_STRINGS.ja.

const KUMIMOJI_TRADE

KUMIMOJI_TRADE: { readonly give: 1; readonly take: 3; }

THE TRADE: one awkward tile back into the bag for three new ones — the classic way out of a hand of Q, X and J. The tile given back goes to the bottom of the bag, so it comes round again before the end; offered only while the bag has three to give.

const KUMIMOJI_VERSION

KUMIMOJI_VERSION: "1.1.1"

The version of this package, as package.json has it. A test holds the two together.

const KUMIMOJI_WILDS

KUMIMOJI_WILDS: Readonly<Record<number, Readonly<Record<"medium" | "easy" | "hard", number>>>>

Wild tiles per original 40/50-tile game, included within the bag total.

type KumimojiCheck

type KumimojiCheck = { ok: true } | { ok: false; reason: string };

The answer to "does this grid finish the game?": yes, or no and why.

function kumimojiClock

kumimojiClock(ms: number): string

A time as minutes and seconds: 1:05.

type KumimojiDeal

type KumimojiDeal = { kind: "kumimoji"; /** The opening hand: 3, 7 or 11 tiles (`KUMIMOJI_HANDS`). */ size: number; level: KumimojiLevel; seed: number; givens: string; solution: string; gameLength: KumimojiLength; doubleSet: boolean; language: KumimojiLanguage; diagonals?: boolean; };

A GAME DEALT (generateKumimoji): the bag, in the order its tiles come out, as one string of tile codes (* a wild), and one finished grid of those tiles that proves the bag can be finished, never shown to the player.

function kumimojiExported

kumimojiExported(saved: KumimojiSaved): KumimojiExported

A game as the JSON export's object.

type KumimojiExported

type KumimojiExported = { /** The shape's number: `KUMIMOJI_EXPORT_FORMAT`. */ format: typeof KUMIMOJI_EXPORT_FORMAT; /** Always `"kumimoji"`, so a file of this shape is not mistaken for another game's. */ game: "kumimoji"; /** The package and version that wrote it, such as `"kumimoji 1.1.0"`. For people; never read back. */ generator: string; /** The opening hand: 3, 7 or 11. */ size: number; /** How many of the ti…

A whole JSON export of one game: what kumimojiToJSON writes and kumimojiFromJSON reads.

function kumimojiFromJSON

kumimojiFromJSON(text: string): Promise<KumimojiSaved | null>

A game from JSON that kumimojiToJSON wrote. Nothing in it is trusted: the bag is dealt again from the seed, and the game opens only if every tile held is one that came out of that bag. Resolves to null when the text is not JSON, is of a later format than this version reads, is another game's, or holds a hand or a grid that this bag never dealt.

The word list of the game's language is loaded first, since dealing needs it; where there is no browser, import @johnmorrisdotca/kumimoji/words once, or the promise is rejected as loadTileWords rejects it.

type KumimojiLanguage

type KumimojiLanguage = "english" | "japanese";

The language of the tiles and of the word list they are judged by.

type KumimojiLength

type KumimojiLength = "short" | "medium" | "full";

How much of the set a game's bag holds: a short game, half the set, or all of it.

type KumimojiLevel

type KumimojiLevel = "easy" | "medium" | "hard";

How many of the bag's tiles are wild: easy the most, hard none (KUMIMOJI_WILDS).

type KumimojiLocale

type KumimojiLocale = "en" | "ja";

The languages the table speaks: English and Japanese.

type KumimojiOptions

type KumimojiOptions = { gameLength?: KumimojiLength; doubleSet?: boolean; language?: KumimojiLanguage; /** * Diagonals (John, 2026-09-28: "we could allow people to play diagonally… * An option at startup is the right choice"): every diagonal run of three * or more tiles, read top to bottom, must be a word too, and joins its * tiles as a word across or down does (`judgeGrid`). Off by default. */ diagonals?: boolean;…

WHAT A KUMIMOJI WAS SET UP AS, beyond its hand and level: the choices that make one game a different game from another at the same seed, carried together wherever a game is made, checked, kept or raced. Each is optional, and its absence is the default: Short, English, one set, no diagonals.

function kumimojiPoints

kumimojiPoints(givens: string, elapsedMs: number): number

A finished game's leaderboard points (KUMIMOJI_SCORE): ten a tile, and up to as much again for speed.

type KumimojiSaved

type KumimojiSaved = { /** The game as dealt: `generateKumimoji`'s. */ deal: KumimojiDeal; /** Where the player has got to. */ play: TilePlay; /** Milliseconds spent on it so far. */ elapsedMs: number; };

A game alone as it is kept: the deal, where the player has got to, and the time spent so far.

function kumimojiSay

kumimojiSay(line: string, values?: Readonly<Record<string, string | number>>): string

A line from a table of strings with its braces filled in: kumimojiSay(strings.left, { n: 12, time: "1:05" }). A brace with no value is left as it is.

function kumimojiStrings

kumimojiStrings(locale: KumimojiLocale | string | undefined, own?: Partial<KumimojiStrings>): KumimojiStrings

The table of strings for a locale with a page's own words laid over it. An unknown locale is English.

type KumimojiStrings

type KumimojiStrings = { [Name in keyof typeof EN]: string };

Every word the table shows, by name: one table of these for each language.

function kumimojiTileCount

kumimojiTileCount(hand: number, length: KumimojiLength, setSize?: number, doubleSet?: boolean): number

Tile count for a selected length and inventory size. Double supplies two sets.

function kumimojiToJSON

kumimojiToJSON(saved: KumimojiSaved): string

A game as JSON, two spaces deep, with the format's number first. kumimojiFromJSON reads it back.

function kumimojiToText

kumimojiToText(saved: KumimojiSaved, strings?: KumimojiStrings): string

A game as plain text, for a chat or a note: how it was set up, the crossword as it lies, the hand, how many tiles are left, and the time. In English unless given another table of strings (KUMIMOJI_STRINGS.ja). The word list of the game's language must have been loaded, as for any judging. Lines end with a line feed.

function kumimojiWildCount

kumimojiWildCount(hand: number, level: "easy" | "medium" | "hard", totalTiles: number): number

Wilds scale with the bag; doubling the tile set doubles the wild count too.

function laidThisTurn

laidThisTurn(game: PartyGame): boolean

Whether the player whose turn it is has laid a tile on their table this turn: more tiles there than when it began.

function lastStanding

lastStanding(game: PartyGame): boolean

Whether the player whose turn it is is the only one still in: everybody else has resigned.

function leaveParty

leaveParty(game: PartyGame, at: number): PartyGame

A PLAYER LEAVES. Their tiles go back into the bag and the bag is shuffled — with the game's own seed and how far it has gone, so the same game left the same way deals the same tiles after a reload. A wild goes back blank.

The bag is kept as a line whose first taken tiles are the ones held, less what was traded back (holdsItsBag); after a leave it is written afresh as exactly that: the tiles everybody still holds, then the shuffled rest, with nothing traded back. Every seat after the leaver's moves up one, and the unnamed players keep the names they were shown ("Player 3" stays Player 3).

If it was their turn, the turn passes to the next still in, or in the last round to the next still to have their last turn; a last round with nobody left to play ends. With nobody still in — everybody left has resigned — the game ends tied between them.

type LeaveRefusal

type LeaveRefusal = "over" | "out" | "lastPerson";

Why this player cannot leave now, or null when they can.

function leaveRefused

leaveRefused(game: PartyGame, at: number): LeaveRefusal | null

Why the player at seat at cannot leave now, or null when they can.

type LengthRow

type LengthRow = { length: KumimojiLength; tiles: number; /** With the Double set, English only. */ doubleTiles: number; wilds: Record<"easy" | "medium" | "hard", number>; };

One length of game at one opening hand: how many tiles it deals from one set and from two, and how many of them are wild at each level.

function lengthRows

lengthRows(hand?: number): LengthRow[]

For one opening hand, each length of game: how many tiles it deals, from one set and from two, and how many are wild at each level.

const LENGTHS

LENGTHS: readonly KumimojiLength[]

The lengths of game, shortest first.

function lettersOf

lettersOf(letters: Iterable<string>): Map<string, number>

How many of each letter.

function liftAll

liftAll(play: TilePlay): TilePlay

Every tile on the table back to the hand.

function liftToHand

liftToHand(play: TilePlay, square: string): TilePlay

A tile on the table back to the end of the hand.

function loadTileWords

loadTileWords(language?: KumimojiLanguage): Promise<TileWords>

THE LIST, ONCE. In a browser (or its worker) it arrives as its own script, fetched by the dynamic import below the first time a Kumimoji needs it.

ONLY IN A BROWSER, AND SAID SO WHERE THE BUILD CAN SEE IT. The page's components are drawn on the server too, and a dynamic import in them is a copy of its target in every server function — two lists, 1.3 MB, which put the site's grouped function over its size limit (functionSizeGate). The build writes typeof window as a constant — "undefined" on the server, "object" in a browser bundle, its worker included — so on the server the branch, and the import with it, is gone before anything is traced. The server reads the list through tileWordsModule.ts instead, which only the server's own checks and the tests import.

function mayDraw

mayDraw(play: TilePlay, verdict: GridVerdict): boolean

Whether Draw may be pressed: the hand used, the grid sound, and a tile left to draw.

function mayDrawAll

mayDrawAll(game: PartyGame, verdict: GridVerdict): boolean

Whether Draw may be pressed: the hand of the player whose turn it is used, their grid sound, and a tile in the bag for every player still in. Never in the last round, when the bag already holds fewer than that.

function mayResign

mayResign(game: PartyGame): boolean

Whether the player whose turn it is may resign: only once the bag cannot give a trade, so nothing more can be got from it. John, 2026-09-28: "Perhaps we offer a resign button only when there are no remaining tiles they can get from the bag."

function mayTrade

mayTrade(play: TilePlay): boolean

Whether a trade may be made: the bag holds as many as a trade takes.

function mayTradeThisTurn

mayTradeThisTurn(game: PartyGame): boolean

ONE TRADE A TURN. John, 2026-09-29, after a player traded over and over and built up a hand: "if they swap tiles … give them a chance to lay some tiles but cannot ask for another swap until next turn." A trade gives one tile and takes three, so trading without end lets one player take the bag from the others. After a trade the player may still lay, lift and draw, and press Done; the next trade is on their next turn.

function mixShown

mixShown(language: KumimojiLanguage): MixShown

A language's set as a page shows it: every tile with its count, the total, and which are the rarest and the commonest.

type MixShown

type MixShown = { tiles: MixTile[]; total: number; /** The most of any one tile, which the bars are drawn against. */ most: number; /** The fewest of any one tile, and the tiles with only that many: the hard ones. */ fewest: number; rarest: string[]; /** The tiles with the most, the ones a hand is full of. */ commonest: string[]; };

A whole set, in its own order (a to z, あ to ん), with what a reader needs to see how uneven it is.

type MixTile

type MixTile = { code: string; glyph: string; forms: string; count: number };

One kind of tile in a set: the code the game deals, what is printed on it, what shows in its corner, and how many the set holds.

function moveOnTable

moveOnTable(play: TilePlay, from: string, to: string): TilePlay

A tile on the table to another square: to an empty one it moves, onto a tile the two change places.

function mustTradeFirst

mustTradeFirst(game: PartyGame, handSpells: (hand: readonly string[]): boolean) => boolean

A HAND THAT SPELLS NOTHING MUST BE TRADED. John, 2026-09-28: "If no words can be formed which is possible, they must dump a tile to get 3 more. Easy rule." So Done waits while the player whose turn it is holds tiles that spell no word, has laid nothing on (or lifted nothing from) their table this turn, and has not traded — unless the bag cannot give three, when there is nothing to trade for. handSpells answers for a hand (handCanSpell).

function nameOf

nameOf(game: PartyGame, at: number): string

What a player is called: their name, or "Player 2".

function nextIn

nextIn(game: PartyGame, from: number): number

The next player still in after from, round the table; from itself when nobody else is.

function nextTurn

nextTurn(turn: Turn): Turn

The next turn a press gives: a quarter clockwise, and four presses back to the start.

function overflows

overflows(area: Area, view: View, width: number, height: number): boolean

Whether the area, at this view's size, is bigger than the screen: then the table pans.

function panView

panView(view: View, dx: number, dy: number): View

The view moved by dx and dy screen pixels.

type PartyEnding

type PartyEnding = "out" | "standing" | "tied";

HOW A PASS-AND-PLAY GAME ENDS. out: somebody went out and everybody else had a last turn; the winners are everybody who went out. standing: every other player resigned and the last one in laid a tile. tied: nobody was left standing, and the players who resigned in that final run are tied.

function partyFits

partyFits(players: number, hand: number, tiles: number): boolean

Whether a bag of this many tiles can be played by this many: one player can play any bag.

type PartyGame

type PartyGame = { settings: PartySettings; /** The tiles in the order they come out. */ bag: string; /** Tiles traded back, in order; they come out after the bag. */ returned: string; /** How many tiles have been taken from `bag + returned`, by every player together. */ taken: number; /** Up to eight; two to eight when dealt, and one or more after players leave (`partySeats.ts`). */ players: readonly PartyPlayer[];…

A PASS-AND-PLAY KUMIMOJI: one bag, read as a line the way the solo game reads it (TilePlay), and a hand and a table for each player.

function partyLength

partyLength(players: number, hand: number, chosen: KumimojiLength, doubleSet: boolean): KumimojiLength

The game length a set-up plays at: the one chosen where the bag holds this many players, else the shortest that does, so a choice made for fewer players comes back when there are fewer again. A Full bag of one set is 144 tiles, and eight Classic hands with a round of draws are 96, so there is always one.

type PartyPlayer

type PartyPlayer = { name: string; hand: readonly string[]; tiles: Tiles; computer?: boolean; };

One player: their name as typed (empty for "Player N"), their hand and their own table, and whether the seat is a computer's (computerTurn.ts), whose turns play themselves in the browser. A computer's name is always written out ("Computer 1"), never left for its number.

function partyPlayersAsked

partyPlayersAsked(value: unknown): number

A players count from the address, or 1 (the solo game) for anything that is not two to eight.

type PartySeat

type PartySeat = { name: string; computer?: boolean };

A seat asked for before the deal or at a join: a name (empty for its number) and whether a computer plays it.

type PartySettings

type PartySettings = { /** The hand each player is dealt: 7 Quick or 11 Classic (3 in the browser tests alone). */ size: number; level: KumimojiLevel; /** The seed the bag was made from; the bag itself is kept too, so a kept game never depends on the generator. */ seed: number; gameLength: KumimojiLength; language: KumimojiLanguage; doubleSet: boolean; /** Whether Diagonals was chosen: every table in the game is rea…

What a pass-and-play game was set up as: the address's own choices, which a kept game is matched to.

function partyTilesLeft

partyTilesLeft(game: PartyGame): number

Tiles still in the shared bag.

function partyTilesNeeded

partyTilesNeeded(players: number, hand: number): number

The tiles a bag must hold for this many players: a hand each and one round of draws.

function passViewStart

passViewStart(game: PartyGame): number

WHOSE TABLE IS SHOWN. Nothing in a pass-and-play game is secret — John, 2026-09-28: "there are no secrets because they are face up" — so every table and every hand can be looked at by anybody, read-only. The pass screen opens on the table of whoever has just played (the first player's, before anybody has), and a swipe steps round every player in seat order, resigned ones included.

function placeFromHand

placeFromHand(play: TilePlay, handAt: number, square: string): TilePlay

A tile from the hand onto an empty square.

function placeOf

placeOf(square: string): { row: number; col: number; }

A square's row and column, from its name.

function planComputerTurn

planComputerTurn(game: PartyGame, words: TileWords): ComputerStep[]

A computer seat's whole turn as its steps, in order, for a page to show one at a time. None when it is not a computer's turn or the game is over.

type Random

type Random = () => number;

A number in [0, 1), like Math.random, from a stream a seed fixes.

function readSettings

readSettings(value: unknown): PartySettings | null

A game's settings as something outside this browser sent them — a table on several devices, a kept game — checked, or null.

function readTileProgress

readTileProgress(code: string): { taken: number; returned: string; hand: string[]; tiles: Map<string, string>; } | null

The parts of a kept game, or null for a string that is not one: its shape only, with no bag to check it against.

function readTileWordsWith

readTileWordsWith(source: (language: KumimojiLanguage): Promise<TileWords>) => void

Used by tileWordsModule.ts only: how to read a list where there is no browser.

function readWilds

readWilds(tiles: Tiles, words: TileWords, rules?: GridRules): Tiles | null

The grid with every blank wild read as a letter that makes all its runs words, or null for none; the grid itself when it holds no blank.

function resign

resign(game: PartyGame): PartyGame

RESIGN: the player whose turn it is is out of the game, their table kept for the finish, and their turn passed over from then on. In the last round it is their last turn ended without going out. Otherwise the game goes on with whoever is still in; and when nobody is — the last one standing resigned too — it ends tied between everybody who resigned since a player still in last pressed Done. John: "if you resign, as the turns pass, if someone else can't go and has to resign then they are tied."

type Run

type Run = { word: string; squares: string[]; line: RunLine };

A run of tiles along one line: the word it spells and the squares it stands on, in reading order.

type RunLine

type RunLine = "across" | "down" | "downRight" | "downLeft";

THE LINES A RUN MAY LIE ALONG, each read top to bottom (and across, left to right): across and down always, and the two diagonals when the game was set up with Diagonals — down to the right, and down to the left.

function runsOf

runsOf(tiles: Tiles, rules?: GridRules): Run[]

Every run the grid is read by: two or more tiles across, then down, and with Diagonals three or more down to the right, then down to the left. A tile with nothing beside it in a line is no run.

function sameLetters

sameLetters(a: Map<string, number>, b: Map<string, number>): boolean

Whether two tallies hold the same letters, as many of each.

function seatPlay

seatPlay(game: PartyGame): TilePlay

The player whose turn it is, as the solo game's moves take a game: the shared bag, and their own hand and table.

function seededRandom

seededRandom(seed: number): Random

A stream of numbers in [0, 1) that the seed fixes: mulberry32.

function shuffled

shuffled<T>(items: readonly T[], random: Random): T[]

A copy of the list in a random order (Fisher–Yates).

function sortHand

sortHand(play: TilePlay): TilePlay

The hand in order (John, 2026-09-28: "it gets sorted for letters alphabetically"): English A to Z, Japanese あいうえお, which is the order of the kana's tile codes, and the wilds last as they came.

type Square

type Square = { row: number; col: number };

A square by its row and column.

function squareAt

squareAt(row: number, col: number): string

A square's name, "row,col".

function squareUnder

squareUnder(view: View, px: number, py: number): { row: number; col: number; }

The square under a point on the screen.

function startParty

startParty(settings: PartySettings, bag: string, seats: readonly (string | PartySeat)[]): PartyGame

The deal: each player the next hand from the bag in turn, player one first, and every table empty. A seat is a name, or a name and whether a computer plays it; a computer is named "Computer 1", "Computer 2"… in seat order.

function stepView

stepView(game: PartyGame, from: number, by: number): number

The player a swipe or an arrow shows next: by seats on, round the table.

function stillIn

stillIn(game: PartyGame): number[]

The players still in — not resigned — by place, in turn order. Somebody who went out is still in until the game ends.

function swapWithHand

swapWithHand(play: TilePlay, handAt: number, square: string): TilePlay

A tile from the hand onto a square that holds one: they change places, the table's tile going to the hand where the other was.

const TABLE

TABLE: { readonly margin: 2; readonly tileLeast: 32; readonly zoomLeast: 12; readonly tileMost: 56; readonly growsFrom: 544; readonly zoomMost: 88; readonly edge: 36; readonly edgeStep: 8; readonly edgeDwellMs: 350; }

HOW THE TABLE IS LOOKED AT: which squares are shown, how big a tile is drawn and where. Pure, and apart from the game (play.ts): the tiles are where the player put them whatever the view, and a view is a function of the tiles, the screen and the player's own zooming and panning.

John, 2026-09-26: no board — "literally there is no board in the real world game." The table shown is the tiles' own extent plus a margin all round, so there is always room to build outward, and it zooms to fit the crossword: big tiles for a first word, smaller as the grid spreads, never smaller than a thumb can press (TABLE.tileLeast). Past that the table pans instead.

function tableArea

tableArea(tiles: Tiles, also?: readonly string[]): Area

The squares to show: the tiles' extent and the margin, or the margin round square (0, 0) on an empty table. Extra squares (the one a typed letter goes to) are kept in.

function tableTally

tableTally(tiles: Tiles): string

A table's tiles as one string in order, wherever they stand: what a turn compares to see whether anything was laid or lifted.

function tidyName

tidyName(name: string): string

A name as kept: trimmed, one line, and no longer than a line holds.

const TILE_MIX

TILE_MIX: Readonly<Record<string, number>>

THE MIX: how many of each letter a full set holds, 144 tiles in all. This is the letter-frequency table of the best-known anagram-grid tile game, a fact about how often English uses its letters and nobody's rule text. Short and Medium games draw from it; Full uses all 144 tiles. Double uses two copies.

To tune the letters, change the counts here. The total is checked by tiles.test.ts, so an edit that loses a tile says so.

const TILE_MIX_TOTAL

TILE_MIX_TOTAL: number

The whole set, counted from the table.

function tileDescription

tileDescription(tile: string): string

What a screen reader says for a tile: its letter, or that it is a wild and what it stands for.

function tileFace

tileFace(tile: string): TileFaceOf

What a tile code shows, read without the word list: its letter or kana, its corner forms, and whether it is a wild.

type TileFaceOf

type TileFaceOf = { /** The letter or kana printed on it: 五 on a wild nobody has given one. */ glyph: string; /** The other forms it plays as, small in its corner (`CORNER_FORMS`). */ forms: string; /** Whether it is a wild tile, given a letter or not. */ wild: boolean; /** A wild with no letter yet. */ blank: boolean; };

What a tile shows: what tileFace answers.

function tileKana

tileKana(kana: string): string | null

The tile a kana is played with, or null for anything that is not hiragana (ー, katakana, a letter).

type TilePlay

type TilePlay = { /** The tiles in the order they come out: the puzzle's givens. */ bag: string; /** Tiles traded back, in order; they come out after the bag. */ returned: string; /** How many tiles have been taken from `bag + returned`. */ taken: number; /** The tiles on the table, by square ("row,col"). */ tiles: Tiles; /** The hand, in the order the player keeps it. */ hand: readonly string[]; };

A KUMIMOJI BEING PLAYED: the bag, the tiles traded back into it, how many have been taken out, the tiles on the table and the hand. Every move is a function that returns a new state and leaves the one it was given alone, the way the engine's moves are. Nothing here knows how the table is being looked at — zoom and pan are the view's (tableView.ts), never the game's.

THE BAG IS A LINE, not a heap. Its tiles come out in the order the seed dealt them (givens), and a tile traded back joins the END of the line, so the tiles still to come are always bag + returned from taken on. That keeps a game the same for everybody at a seed, trades and all, and lets a kept game be checked: what is held — hand and table — is exactly what was taken, less what was given back.

type Tiles

type Tiles = ReadonlyMap<string, string>;

A KUMIMOJI GRID: letter tiles on a table with no edges. Each tile stands on a square named by its row and column, which may be any whole numbers, negative too — the table grows whichever way the player builds. Pure, like every rule here: nothing is changed in place.

As a string, for an answer and a kept game, the grid is drawn from its own top-left tile: its rows in order, joined by "/", each row written as runs of empty squares (a number) and tiles. English letters keep their short form; other tiles are URI-escaped between ~ and ;. cat/2o/2w is CAT across and COW down from its C. Only where the tiles stand beside one another is written, never where on the table they were, so one grid has one spelling however far it was dragged.

function tilesHeldBy

tilesHeldBy(game: PartyGame, at: number): number

How many tiles a player would put back in the bag by leaving: their hand and their table.

function tilesLeft

tilesLeft(play: TilePlay): number

How many tiles are still in the bag.

function tileWords

tileWords(language?: KumimojiLanguage): TileWords

The list already loaded, for judging and dealing. Throws where loadTileWords has not yet fetched it: a check that cannot read the list must not answer.

type TileWords

type TileWords = { language: KumimojiLanguage; /** Every word, for asking "is this a word?". */ allowed: ReadonlySet<string>; /** Encoded tile words of each length, for the generator to choose among. */ byLength: ReadonlyMap<number, readonly string[]>; /** The physical set, keyed by its one-character tile codes. */ mix: ReadonlyMap<string, number>; /** Visible glyph for a tile code. */ glyphOf: (tile: string) => str…

KUMIMOJI'S WORD LIST, loaded once, when a game opens.

The list is about six hundred kilobytes before compression (a hundred and ten thousand words, words.en.data.ts), so it is its own module, fetched by a dynamic import only when a Kumimoji is made or checked: no other page, and not the set-up screen, carries a byte of it. The kana Gomoji loads its lists the same way (kanaWords.ts).

loadTileWords fetches and reads it once; tileWords hands back the list already loaded, and refuses rather than answering for a list it does not have — a check that could not read the list must not say a word is fine.

function tileWordsFrom

tileWordsFrom(language: KumimojiLanguage, data: unknown): TileWords

A list read from its module, for tileWordsModule.ts.

function tileWordsReady

tileWordsReady(language?: KumimojiLanguage): boolean

Whether the list has been fetched yet, for a page to say "loading" rather than throw.

function trade

trade(play: TilePlay, handAt: number): TilePlay

One tile from the hand to the bottom of the bag, and the next three out of it.

function tradeChoice

tradeChoice(hand: readonly string[], words: TileWords): number | null

The tile a computer trades: its rarest letter in the set, the first of those in its hand; never a wild. Null for a hand of wilds.

const TRY_IT

TRY_IT: { readonly bag: "rotenasdi*"; readonly hand: 7; }

THE TRY-IT ON THE FRONT DOOR: a Kumimoji of ten tiles, played in the reader's browser with the game's own moves and the game's own word check. A hand of seven, then three to draw, the last of them a wild. The bag is fixed rather than dealt, so the page is the same for everybody and needs no generator; showcase.test.ts proves it can be finished, with a crossword checked against the real list.

type Turn

type Turn = 0 | 1 | 2 | 3;

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.

function turnArea

turnArea(area: Area, turn: Turn): Area

The squares a turned table shows: the grid's area, turned.

function turnPlace

turnPlace(row: number, col: number, turn: Turn): { row: number; col: number; }

Where square (row, col) of the grid is drawn with the table turned: a quarter clockwise sends a word running right to one running down, and one running down to one running left. It is linear, so it turns a step (a direction) as well as a square.

function turnView

turnView(view: View, width: number, height: number): View

A view the player zoomed or panned, turned a quarter clockwise with the table about the middle of its box: the same zoom, and the spot of the table that was in the middle stays there. (A fitted view simply fits again.)

On the screen a square's corner is x + col * tile; the turn takes a point (row v, col u) of the table to (row u, col 1 - v), which is a quarter turn about the middle of square (0, 0) — the same turn as turnPlace.

function typingWay

typingWay(across: boolean, turn: Turn): { arrow: string; name: string; }

Which way typed letters run ON THE SCREEN. Typing lays a word across or down the grid, so that it reads as a word; with the table turned, that is another way on the screen, and the cursor's arrow points it.

function unpackLength

unpackLength(packed: string, length: number): string[]

Front-coded words of one length read back (see scripts/tile-words.mjs): a shared-prefix character, then the rest.

function unpackTileWords

unpackTileWords(data: Readonly<Record<number, string>>): TileWords

The English list read from its packed data: every word, the words by length, the set's mix, and the functions that turn tiles into words.

function unturnPlace

unturnPlace(row: number, col: number, turn: Turn): { row: number; col: number; }

The grid's square drawn at (row, col) of the turned table: turnPlace undone.

type View

type View = { tile: number; x: number; y: number };

A view: a tile's side in pixels, and where the corner of square (0, 0) is drawn.

const WILD_SEARCH_MOST

WILD_SEARCH_MOST: 20000

A WILD LAID WITHOUT A LETTER READS AS WHATEVER MAKES THE GRID WORDS. John, 2026-09-29, having laid D, a blank wild, O, N, E and been told "Not a word: D*ONE": "Either we are smarter and use a regex or something to know that it's a valid possible word." Choosing the wild's letter is still offered, and a wild given one reads as that letter only; a blank one is any letter the list allows.

One blank can stand in two runs, across and down, and two blanks can share a run, so the letters are chosen together: blank by blank, each letter kept only while every run through it could still be a word — a run with blanks left in it is asked whether any word of its length fits the letters it has, a run with none whether it is one. Blanks that share no run are settled apart, so one crossword's wilds never multiply another's.

readWilds hands back the grid with each blank given the wild of a letter that works (wildFor), or null where no choice makes every run a word — and the grid is then judged as laid, blank and all, so the line under the table names the run as the player spelled it. It gives up, answering null, past WILD_SEARCH_MOST steps: a grid it could not settle is not called sound.

function winnersOf

winnersOf(game: PartyGame): readonly number[]

The winners of a finished game, by place: everybody who went out; the last one standing; or, tied, the final run of resignations. Empty while it is played.

function withSeatPlay

withSeatPlay(game: PartyGame, play: TilePlay): PartyGame

A move made on seatPlay put back: the bag as it left it, and the hand and table to the player whose turn it is. A second trade in one turn is refused, the game given back as it was.

function wordsInHand

wordsInHand(hand: readonly string[], words: TileWords): string[]

The words the hand's own tiles spell, longest first and then in the list's order; wilds are not used.

function zoomView

zoomView(view: View, factor: number, px: number, py: number): View

Zoomed by factor about the point (px, py) on the screen, which stays over the same spot of the table.

@johnmorrisdotca/kumimoji/words

loadTileWordsFromModule

function loadTileWordsFromModule

loadTileWordsFromModule(language?: KumimojiLanguage): Promise<TileWords>

The list, read from its module: loadTileWords for a caller with no browser.

@johnmorrisdotca/kumimoji/ui

BOARD_LEAST BOARD_MARGIN boardModel BoardModel BoardSquare KUMIMOJI_STRINGS KUMIMOJI_STYLE KumimojiLocale kumimojiSay kumimojiStrings KumimojiStrings KumimojiTableHandle KumimojiTableOptions mountKumimoji TileMark

const BOARD_LEAST

BOARD_LEAST: 7

The fewest squares a side, so an empty or small table is still a table.

const BOARD_MARGIN

BOARD_MARGIN: 2

How many empty squares are kept round the tiles on every side, so there is always room to build out.

function boardModel

boardModel(tiles: Tiles, verdict: GridVerdict | null, glyphOf?: (tile: string): string) => BoardModel

THE PART OF THE TABLE TO DRAW, for a grid and its verdict: the tiles and a margin round them, at least BOARD_LEAST a side, each square with its tile, what is printed on it, and how it stands. Row by row, left to right. The grid itself has no edges; this is only the window a board shows it through.

type BoardModel

type BoardModel = { rows: number; cols: number; squares: BoardSquare[] };

The part of the table to draw: how many rows and columns, and every square, row by row.

type BoardSquare

type BoardSquare = { /** Its key, `squareAt(row, col)`. */ square: string; row: number; col: number; /** The tile standing there, or null for an empty square. */ tile: string | null; /** What is printed on the tile; "" for an empty square. */ glyph: string; /** Whether the tile is a wild. */ wild: boolean; /** How the tile stands; null for an empty square, or with no verdict. */ mark: TileMark | null; };

One square of the board drawn.

const KUMIMOJI_STRINGS

KUMIMOJI_STRINGS: Readonly<Record<KumimojiLocale, KumimojiStrings>>

The table's words in each language it speaks: KUMIMOJI_STRINGS.en, KUMIMOJI_STRINGS.ja.

const KUMIMOJI_STYLE

KUMIMOJI_STYLE: "\n.km-root {\n --km-square: 44px; --km-felt: #2f5d4a; --km-grid-line: rgba(255,255,255,.12); --km-tile: #f3e6c8; --km-tile-ink: #2a2118;\n --km-wild: #f6d27a; --km-wrong: #d9534f; --km-held: #ffcf3f; --km-ink: #1f2320; --km-panel: #f7f3ea;\n --km-line: rgba(20,20,20,.35); --km-accent: #2f5d4a; --km-accent-ink: #fff;\n --km-radius: 12px; --km-font: system-ui, -apple-system, \"Segoe UI\", sans-serif;\…

The table's own styles, scoped to .km-root, every colour and size a CSS variable so a page can wear it in its own colours (set them on the element or any ancestor, or pass them as theme). Light and dark follow the reader's system. Everything to be tapped is at least 44px.

type KumimojiLocale

type KumimojiLocale = "en" | "ja";

The languages the table speaks: English and Japanese.

function kumimojiSay

kumimojiSay(line: string, values?: Readonly<Record<string, string | number>>): string

A line from a table of strings with its braces filled in: kumimojiSay(strings.left, { n: 12, time: "1:05" }). A brace with no value is left as it is.

function kumimojiStrings

kumimojiStrings(locale: KumimojiLocale | string | undefined, own?: Partial<KumimojiStrings>): KumimojiStrings

The table of strings for a locale with a page's own words laid over it. An unknown locale is English.

type KumimojiStrings

type KumimojiStrings = { [Name in keyof typeof EN]: string };

Every word the table shows, by name: one table of these for each language.

type KumimojiTableHandle

type KumimojiTableHandle = { /** The game as it stands, or null while it is being dealt. */ play: () => TilePlay | null; /** The game as it would be saved (`kumimojiToJSON`), or null while it is being dealt. */ saved: () => KumimojiSaved | null; /** A new game at the same table, with any options changed. Resolves when it is dealt. */ newGame: (options?: GameOptions) => Promise<void>; /** Put a game on the table: one…

What mountKumimoji hands back: the game being played, and the ways to change it from outside.

type KumimojiTableOptions

type KumimojiTableOptions = { /** The language of the tiles: `"english"` or `"japanese"` kana. English by default. */ language?: KumimojiLanguage; /** The opening hand: 3, 7 or 11 tiles. */ hand?: number; /** How many of the tiles are wild: `"easy"` the most, `"medium"` some, `"hard"` none. */ level?: KumimojiLevel; /** How much of the set the bag holds: `"short"`, `"medium"`, or `"full"`, all 144 tiles. */ gameLeng…

What mountKumimoji may be given. Everything is optional: with nothing, it is a short English game from a hand of seven, with some wild tiles.

function mountKumimoji

mountKumimoji(target: HTMLElement, options?: KumimojiTableOptions): KumimojiTableHandle

A GAME OF KUMIMOJI ALONE, IN PLAIN DOM: the bag dealt from a seed, your hand, and a table to build your crossword on, every run judged as you lay it. Tap a tile in your hand and then a square to lay it; tap a tile on the table and then a square to move it, or tap it again to take it back. When the hand is empty and every run is a word, Draw brings the next tile; a tile you cannot use can be traded for three. Use every tile in the bag to finish. Under the buttons, the game saves as JSON or as text, and a saved game loads back.

The word list is fetched the first time a game in its language is dealt.

type TileMark

type TileMark = "sound" | "misspelt" | "apart";

How a tile on the table stands: part of a sound crossword, in a run that is not a word, or apart from the rest.

@johnmorrisdotca/kumimoji/react

KumimojiBoard KumimojiBoardProps KumimojiTable KumimojiTableProps

function KumimojiBoard

KumimojiBoard({ tiles, verdict, glyphOf, onSquare, held, square, style, ...element }: KumimojiBoardProps): import("/home/runner/work/kumimoji/kumimoji/node_modules/.pnpm/@types+react@19.3.0/node_modules/@types/react/index").JSX.Element

A KUMIMOJI TABLE, AS A REACT COMPONENT: the tiles of a grid with a margin of empty squares round them, a run that is not a word ringed red and a tile apart from the crossword faded, and a press on any square reported by its key (squareAt(row, col)). It draws; what a press does is yours to decide with placeFromHand, moveOnTable and the rest.

type KumimojiBoardProps

type KumimojiBoardProps = { /** The tiles on the table, by square: `play.tiles`. */ tiles: Tiles; /** From `judgeWithWords`: marks the runs that are not words and the tiles apart. Without it, no marks. */ verdict?: GridVerdict | null; /** What to print on a tile: `words.glyphOf` for the language played. */ glyphOf?: (tile: string) => string; /** A square pressed, empty or not. Without it the board is only a picture.…

What KumimojiBoard takes: a grid to draw, and any attribute of its <div>.

function KumimojiTable

KumimojiTable({ language, hand, level, gameLength, diagonals, seed, onFinish, onChange, locale, strings, theme, keep, ...element }: KumimojiTableProps): import("/home/runner/work/kumimoji/kumimoji/node_modules/.pnpm/@types+react@19.3.0/node_modules/@types/react/index").JSX.Element

A whole game alone, as a React component: the plain-DOM table (mountKumimoji) mounted into this component's element once the browser has it. Options are read when it mounts; give it a new key to start over with different ones.

type KumimojiTableProps

type KumimojiTableProps = KumimojiTableOptions & Omit<HTMLAttributes<HTMLDivElement>, keyof KumimojiTableOptions>;

What KumimojiTable takes: the options of mountKumimoji, and any attribute of its <div>.