Files
scrabble-game/ui/src/lib/splash.ts
T
Ilia Denisov 9642cafc1f
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m18s
fix(ui): hold each splash word for the pause before the readiness check
The dismiss check fired at the instant a word finished laying, so a word
was never held: ЭРУДИТ fell straight into the lobby (too fast) and
ЗАГРУЗКА got no readable pause. The pause was a *leading* gap before the
next word, not a hold after the current one.

Move the hold to after each word and run the check after it: every word
(ЭРУДИТ included) now stays up for PAUSE_MS before the splash either
dismisses or lays the next word. prefixMs = WORD_MS + PAUSE_MS,
cycleMs = 2*(WORD_MS + PAUSE_MS).
2026-06-21 10:42:31 +02:00

141 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Pure model for the cold-start loading splash: a small Scrabble crossword that lays the
// words ЭРУДИТ / ЗАГРУЗКА / ОЖИДАНИЕ out of tiles, letter by letter, until the lobby is
// ready. Kept free of any DOM or reactive state so it unit-tests in the node environment;
// components/Splash.svelte wraps it with the reactive reveal set and the timer driver.
//
// The tile point values are hardcoded from the Эрудит ruleset (scrabble-solver
// rules.Erudit). They cannot come from alphabet.valueForLetter() here: the splash renders on
// a cold start, before the server has sent the variant's alphabet table that lookup reads.
/**
* ERUDIT_VALUES is the per-letter point value (вес) for every letter the splash words use,
* taken from the Эрудит ruleset. Letters are upper-cased, matching the rest of the UI.
*/
export const ERUDIT_VALUES: Record<string, number> = {
А: 1,
Г: 3,
Д: 2,
Е: 1,
Ж: 5,
З: 5,
И: 1,
К: 2,
Н: 1,
О: 1,
Р: 2,
Т: 2,
У: 3,
Э: 10,
};
/** SplashWord names the three crossword words, used to group tiles for the reveal sequence. */
export type SplashWord = 'erudit' | 'zagruzka' | 'ozhidanie';
/** SplashTile is one placed tile on the 6×8 splash grid. */
export interface SplashTile {
/** row is the grid row, 0..7 (ЭРУДИТ sits on row 3). */
row: number;
/** col is the grid column, 0..5. */
col: number;
/** letter is the upper-cased Cyrillic glyph shown on the tile. */
letter: string;
/** value is the tile's Эрудит point value. */
value: number;
/** word groups the tile by its crossword word. */
word: SplashWord;
/** key is the stable `"row-col"` identifier used by the renderer's reveal set. */
key: string;
}
/** COLS and ROWS are the splash grid dimensions (6 wide, 8 tall). */
export const COLS = 6;
export const ROWS = 8;
function tile(row: number, col: number, letter: string, word: SplashWord): SplashTile {
return { row, col, letter, value: ERUDIT_VALUES[letter] ?? 0, word, key: `${row}-${col}` };
}
// ЭРУДИТ — horizontal on row 3, columns 0..5 (laid left → right).
const ERUDIT_TILES: SplashTile[] = [...'ЭРУДИТ'].map((l, i) => tile(3, i, l, 'erudit'));
// ЗАГРУЗКА — vertical in column 1, rows 0..7 (laid top → bottom). Its row-3 letter (Р) is the
// tile already placed by ЭРУДИТ, so it is dropped here ("через имеющиеся буквы").
const ZAGRUZKA_TILES: SplashTile[] = [...'ЗАГРУЗКА']
.map((l, r) => tile(r, 1, l, 'zagruzka'))
.filter((t) => t.row !== 3);
// ОЖИДАНИЕ — vertical in column 3, rows 0..7. Its row-3 letter (Д) is shared with ЭРУДИТ and
// likewise dropped.
const OZHIDANIE_TILES: SplashTile[] = [...'ОЖИДАНИЕ']
.map((l, r) => tile(r, 3, l, 'ozhidanie'))
.filter((t) => t.row !== 3);
/** SPLASH_TILES is every unique tile on the grid (ЭРУДИТ owns the two shared crossings). */
export const SPLASH_TILES: SplashTile[] = [...ERUDIT_TILES, ...ZAGRUZKA_TILES, ...OZHIDANIE_TILES];
/** ERUDIT_KEYS is the set of ЭРУДИТ tile keys — the tiles kept on screen across a clear. */
export const ERUDIT_KEYS: ReadonlySet<string> = new Set(ERUDIT_TILES.map((t) => t.key));
/** WORD_MS is the time to lay one whole word; PAUSE_MS the hold after each word — so the word
* stays readable before the next word starts or the splash dismisses into the lobby. */
export const WORD_MS = 1000;
export const PAUSE_MS = 250;
/** SplashStep is one scheduled action: reveal a tile, check whether the lobby is ready (and
* if so dismiss), or clear every non-ЭРУДИТ tile before the cycle repeats. atMs is relative
* to the start of its segment (prefix or cycle). */
export interface SplashStep {
atMs: number;
kind: 'reveal' | 'check' | 'clear';
/** key is the tile to reveal, set only on a 'reveal' step. */
key?: string;
}
/** SplashSchedule is the timed plan the renderer drives: the one-time prefix (ЭРУДИТ) and the
* repeating cycle (ЗАГРУЗКА → ОЖИДАНИЕ → clear). The driver plays the prefix once, then loops
* the cycle every cycleMs until the splash is dismissed. */
export interface SplashSchedule {
prefix: SplashStep[];
cycle: SplashStep[];
prefixMs: number;
cycleMs: number;
}
function revealSteps(tiles: SplashTile[], base: number, perTile: number): SplashStep[] {
return tiles.map((t, i) => ({ atMs: Math.round(base + i * perTile), kind: 'reveal', key: t.key }));
}
/**
* splashSchedule builds the reveal plan. Each word is laid over wordMs (so its per-tile
* stagger is wordMs divided by the word's tile count), then **held for pauseMs** so it stays
* readable; only after that hold does a readiness check land — so the splash never dismisses
* (nor starts the next word) the instant a word finishes. The next word begins right at the
* check. After ОЖИДАНИЕ the cycle clears the two vertical words and restarts (ЭРУДИТ persists).
* wordMs and pauseMs are overridable for tuning/tests.
*/
export function splashSchedule(opts: { wordMs?: number; pauseMs?: number } = {}): SplashSchedule {
const wordMs = opts.wordMs ?? WORD_MS;
const pauseMs = opts.pauseMs ?? PAUSE_MS;
const sV = wordMs / ZAGRUZKA_TILES.length;
const hold = wordMs + pauseMs; // lay the word, then hold it readable before the check
// Prefix: ЭРУДИТ left → right, held, then a readiness check.
const prefix: SplashStep[] = [
...revealSteps(ERUDIT_TILES, 0, wordMs / ERUDIT_TILES.length),
{ atMs: hold, kind: 'check' },
];
// Cycle: ЗАГРУЗКА, hold, check, ОЖИДАНИЕ, hold, check, clear. Each word starts at the previous
// word's check (no leading gap); the hold lives at the end of every word.
const endAt = 2 * hold;
const cycle: SplashStep[] = [
...revealSteps(ZAGRUZKA_TILES, 0, sV),
{ atMs: hold, kind: 'check' },
...revealSteps(OZHIDANIE_TILES, hold, sV),
{ atMs: endAt, kind: 'check' },
{ atMs: endAt, kind: 'clear' },
];
return { prefix, cycle, prefixMs: hold, cycleMs: endAt };
}