Stage 7 (wip): UI shell, libs, mock transport, screens (lobby->game), e2e smoke

- plain Svelte 5 + TS + Vite (no SvelteKit); CSS-token design system (Telegram-ready), hash router, IndexedDB session
- pure libs: domain model, premium/value maps ported from solver, board replay, placement state machine, i18n en/ru
- in-memory mock transport + seed data; pnpm start runs lobby->active game->board with no backend
- board: pointer-drag + tap placement, MakeMove (popup / 1s-hold commit), two-state zoom, blank chooser, exchange, hint, word-check, chat
- Playwright smoke (mock) green; svelte-check clean; mock bundle ~37 KB gzip
This commit is contained in:
Ilia Denisov
2026-06-03 00:32:50 +02:00
parent 19ae8f04a2
commit 453ddc5e94
48 changed files with 5696 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
// Domain model — plain TypeScript shapes the screens use, deliberately decoupled
// from the FlatBuffers wire types. Both the real transport (which decodes
// FlatBuffers) and the mock transport speak this model, so the UI never touches
// generated wire code directly.
export type Variant = 'english' | 'russian' | 'erudit';
/** Backend game status strings. */
export type GameStatus = 'active' | 'finished' | string;
/** Decoded move action kinds (history-independent, see ARCHITECTURE §9.1). */
export type MoveAction = 'play' | 'pass' | 'exchange' | 'resign' | 'timeout' | string;
/** Play orientation: H is across a row, V is down a column. */
export type Direction = 'H' | 'V';
export interface Tile {
row: number;
col: number;
letter: string;
blank: boolean;
}
export interface Seat {
seat: number;
accountId: string;
displayName: string;
score: number;
hintsUsed: number;
isWinner: boolean;
}
export interface GameView {
id: string;
variant: Variant;
dictVersion: string;
status: GameStatus;
players: number;
toMove: number;
turnTimeoutSecs: number;
moveCount: number;
endReason: string;
seats: Seat[];
}
export interface MoveRecord {
player: number;
action: MoveAction;
dir: string;
mainRow: number;
mainCol: number;
tiles: Tile[];
words: string[];
count: number;
score: number;
total: number;
}
/** A seated player's private view of a game. */
export interface StateView {
game: GameView;
seat: number;
rack: string[];
bagLen: number;
hintsRemaining: number;
}
export interface MoveResult {
move: MoveRecord;
game: GameView;
}
export interface HintResult {
move: MoveRecord;
hintsRemaining: number;
}
export interface EvalResult {
legal: boolean;
score: number;
words: string[];
}
export interface WordCheckResult {
word: string;
legal: boolean;
}
export interface ChatMessage {
id: string;
gameId: string;
senderId: string;
kind: string;
body: string;
createdAtUnix: number;
}
export interface Profile {
userId: string;
displayName: string;
preferredLanguage: string;
timeZone: string;
hintBalance: number;
blockChat: boolean;
blockFriendRequests: boolean;
isGuest: boolean;
}
export interface Session {
token: string;
userId: string;
isGuest: boolean;
displayName: string;
}
export interface MatchResult {
matched: boolean;
game?: GameView;
}
export interface History {
gameId: string;
moves: MoveRecord[];
}
export interface GameList {
games: GameView[];
}
/** A live event delivered over the Subscribe stream. */
export type PushEvent =
| { kind: 'your_turn'; gameId: string; deadlineUnix: number }
| { kind: 'opponent_moved'; gameId: string; seat: number; action: string; score: number; total: number }
| { kind: 'chat_message'; message: ChatMessage }
| { kind: 'nudge'; gameId: string; fromUserId: string }
| { kind: 'match_found'; gameId: string }
| { kind: 'heartbeat' };