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.
@johnmorrisdotca/kumimoji 1.1.1 · 4 entry points · 209 exports
@johnmorrisdotca/kumimojiafterComputerTurn 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
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 = { top: number; left: number; rows: number; cols: number };
The squares shown: from top/left, rows by cols.
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.
assignHandTile(play: TilePlay, handAt: number, face: string): TilePlay
Give a hand tile its chosen reading, without changing which physical tile it is.
assignTableTile(play: TilePlay, square: string, face: string): TilePlay
Give a tile on the table its chosen reading, without changing its square.
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.
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 = { top: number; left: number; bottom: number; right: number };
The rows and columns a grid's tiles stand within.
boundsOf(tiles: Tiles): Bounds | null
The rows and columns the tiles stand within, or null for no tiles.
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.
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.
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 = | { 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 = { 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.
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.
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 = { 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.
deal(bag: string, handSize: number): TilePlay
The opening hand: the first handSize tiles of the bag, and an empty table.
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.
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.
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.
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.
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.
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).
draw(play: TilePlay, count?: number): TilePlay
The next tile or tiles out of the bag into the hand.
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.
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.
encodeGrid(tiles: Tiles): string
A grid as a string, drawn from its own top-left tile: cat/2o/2w. decodeGrid reads it back.
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.
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.
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.
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.
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.
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.
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.
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.
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 = { diagonals?: boolean };
How a grid is read: whether its diagonals are (KumimojiOptions.diagonals).
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 = { 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).
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.
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.
handSpelling(play: TilePlay, word: string): TilePlay
The hand with word's tiles first, in its order, and the rest after them as they were.
HELP_WORDS_MOST: 40
How many words one hand offers before the presses go round again: the longest first.
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.
isComputer(game: PartyGame, at: number): boolean
Whether a computer plays this seat.
isFinished(play: TilePlay, verdict: GridVerdict): boolean
Whether the game is over: the bag empty, the hand used, and the grid sound.
isLastTurn(game: PartyGame): boolean
Whether the turn now being played is a player's last.
isOver(game: PartyGame): boolean
Whether the game is over.
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.
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.
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 = "over" | "full" | "lastRound" | "bag";
Why nobody can join now, or null when a hand can be dealt.
joinRefused(game: PartyGame): JoinRefusal | null
Why nobody can join the table now, or null when a hand can be dealt.
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.
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.
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).
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.
KANA_WILD_START: 61440
Where the codes of wild tiles given a kana begin: the kana's place in BASE_KANA above this.
kanaTileCode(kana: string, wild?: boolean): string | null
The code of a base kana's tile, or of a wild given that kana.
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.
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.
KUMIMOJI_DRAW: 1
How many tiles one Draw takes from the bag, once the hand is used and the grid is sound.
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.
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.
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.
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.
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.
KUMIMOJI_STRINGS: Readonly<Record<KumimojiLocale, KumimojiStrings>>
The table's words in each language it speaks: KUMIMOJI_STRINGS.en, KUMIMOJI_STRINGS.ja.
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.
KUMIMOJI_VERSION: "1.1.1"
The version of this package, as package.json has it. A test holds the two together.
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 = { ok: true } | { ok: false; reason: string };
The answer to "does this grid finish the game?": yes, or no and why.
kumimojiClock(ms: number): string
A time as minutes and seconds: 1:05.
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.
kumimojiExported(saved: KumimojiSaved): KumimojiExported
A game as the JSON export's object.
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.
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 = "english" | "japanese";
The language of the tiles and of the word list they are judged by.
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 = "easy" | "medium" | "hard";
How many of the bag's tiles are wild: easy the most, hard none (KUMIMOJI_WILDS).
type KumimojiLocale = "en" | "ja";
The languages the table speaks: English and Japanese.
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.
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 = { /** 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.
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.
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 = { [Name in keyof typeof EN]: string };
Every word the table shows, by name: one table of these for each language.
kumimojiTileCount(hand: number, length: KumimojiLength, setSize?: number, doubleSet?: boolean): number
Tile count for a selected length and inventory size. Double supplies two sets.
kumimojiToJSON(saved: KumimojiSaved): string
A game as JSON, two spaces deep, with the format's number first. kumimojiFromJSON reads it back.
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.
kumimojiWildCount(hand: number, level: "easy" | "medium" | "hard", totalTiles: number): number
Wilds scale with the bag; doubling the tile set doubles the wild count too.
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.
lastStanding(game: PartyGame): boolean
Whether the player whose turn it is is the only one still in: everybody else has resigned.
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 = "over" | "out" | "lastPerson";
Why this player cannot leave now, or null when they can.
leaveRefused(game: PartyGame, at: number): LeaveRefusal | null
Why the player at seat at cannot leave now, or null when they can.
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.
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.
LENGTHS: readonly KumimojiLength[]
The lengths of game, shortest first.
lettersOf(letters: Iterable<string>): Map<string, number>
How many of each letter.
liftAll(play: TilePlay): TilePlay
Every tile on the table back to the hand.
liftToHand(play: TilePlay, square: string): TilePlay
A tile on the table back to the end of the hand.
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.
mayDraw(play: TilePlay, verdict: GridVerdict): boolean
Whether Draw may be pressed: the hand used, the grid sound, and a tile left to draw.
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.
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."
mayTrade(play: TilePlay): boolean
Whether a trade may be made: the bag holds as many as a trade takes.
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.
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 = { 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 = { 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.
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.
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).
nameOf(game: PartyGame, at: number): string
What a player is called: their name, or "Player 2".
nextIn(game: PartyGame, from: number): number
The next player still in after from, round the table; from itself when nobody else is.
nextTurn(turn: Turn): Turn
The next turn a press gives: a quarter clockwise, and four presses back to the start.
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.
panView(view: View, dx: number, dy: number): View
The view moved by dx and dy screen pixels.
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.
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 = { 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.
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 = { 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.
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 = { 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 = { /** 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.
partyTilesLeft(game: PartyGame): number
Tiles still in the shared bag.
partyTilesNeeded(players: number, hand: number): number
The tiles a bag must hold for this many players: a hand each and one round of draws.
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.
placeFromHand(play: TilePlay, handAt: number, square: string): TilePlay
A tile from the hand onto an empty square.
placeOf(square: string): { row: number; col: number; }
A square's row and column, from its name.
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 = () => number;
A number in [0, 1), like Math.random, from a stream a seed fixes.
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.
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.
readTileWordsWith(source: (language: KumimojiLanguage): Promise<TileWords>) => void
Used by tileWordsModule.ts only: how to read a list where there is no browser.
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.
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 = { 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 = "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.
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.
sameLetters(a: Map<string, number>, b: Map<string, number>): boolean
Whether two tallies hold the same letters, as many of each.
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.
seededRandom(seed: number): Random
A stream of numbers in [0, 1) that the seed fixes: mulberry32.
shuffled<T>(items: readonly T[], random: Random): T[]
A copy of the list in a random order (Fisher–Yates).
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 = { row: number; col: number };
A square by its row and column.
squareAt(row: number, col: number): string
A square's name, "row,col".
squareUnder(view: View, px: number, py: number): { row: number; col: number; }
The square under a point on the screen.
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.
stepView(game: PartyGame, from: number, by: number): number
The player a swipe or an arrow shows next: by seats on, round the table.
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.
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.
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.
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.
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.
tidyName(name: string): string
A name as kept: trimmed, one line, and no longer than a line holds.
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.
TILE_MIX_TOTAL: number
The whole set, counted from the table.
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.
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 = { /** 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.
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 = { /** 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 = 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.
tilesHeldBy(game: PartyGame, at: number): number
How many tiles a player would put back in the bag by leaving: their hand and their table.
tilesLeft(play: TilePlay): number
How many tiles are still in the bag.
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 = { 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.
tileWordsFrom(language: KumimojiLanguage, data: unknown): TileWords
A list read from its module, for tileWordsModule.ts.
tileWordsReady(language?: KumimojiLanguage): boolean
Whether the list has been fetched yet, for a page to say "loading" rather than throw.
trade(play: TilePlay, handAt: number): TilePlay
One tile from the hand to the bottom of the bag, and the next three out of it.
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.
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 = 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.
turnArea(area: Area, turn: Turn): Area
The squares a turned table shows: the grid's area, turned.
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.
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.
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.
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.
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.
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 = { tile: number; x: number; y: number };
A view: a tile's side in pixels, and where the corner of square (0, 0) is drawn.
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.
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.
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.
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.
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/wordsloadTileWordsFromModule(language?: KumimojiLanguage): Promise<TileWords>
The list, read from its module: loadTileWords for a caller with no browser.
@johnmorrisdotca/kumimoji/uiBOARD_LEAST BOARD_MARGIN boardModel BoardModel BoardSquare KUMIMOJI_STRINGS KUMIMOJI_STYLE KumimojiLocale kumimojiSay kumimojiStrings KumimojiStrings KumimojiTableHandle KumimojiTableOptions mountKumimoji TileMark
BOARD_LEAST: 7
The fewest squares a side, so an empty or small table is still a table.
BOARD_MARGIN: 2
How many empty squares are kept round the tiles on every side, so there is always room to build out.
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 = { 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 = { /** 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.
KUMIMOJI_STRINGS: Readonly<Record<KumimojiLocale, KumimojiStrings>>
The table's words in each language it speaks: KUMIMOJI_STRINGS.en, KUMIMOJI_STRINGS.ja.
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 = "en" | "ja";
The languages the table speaks: English and Japanese.
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.
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 = { [Name in keyof typeof EN]: string };
Every word the table shows, by name: one table of these for each language.
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 = { /** 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.
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 = "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/reactKumimojiBoard KumimojiBoardProps KumimojiTable KumimojiTableProps
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 = { /** 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>.
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 = KumimojiTableOptions & Omit<HTMLAttributes<HTMLDivElement>, keyof KumimojiTableOptions>;
What KumimojiTable takes: the options of mountKumimoji, and any attribute of its <div>.
この日本語は、まだ日本語を母語とする方の確認を受けていません。訂正を歓迎します。