// Pure layout of the move journal for the in-game history panel. The panel mirrors the // score plaque above the board: one column per seat (a seat's moves fill its own column, // top to bottom) instead of a single chronological list. Keeping the arrangement here (not // in Game.svelte) lets it be unit-tested without a DOM; the component only renders the // resulting cells with i18n/CSS. import type { MoveRecord } from './model'; /** A single cell of the history grid. The grid is row-major with one column per seat: a * seat's k-th move sits in row k of its column. */ export interface HistoryCell { kind: 'play' | 'action' | 'thinking' | 'empty'; /** Seat index this cell belongs to (its column). */ player: number; /** Words formed, main word first — set for kind 'play'. */ words?: string[]; /** Points scored by the move — set for kind 'play'. */ score?: number; /** The non-play action ('pass' | 'exchange' | 'resign' | 'timeout' | …) — set for kind * 'action'; the component localizes it. */ action?: string; } /** * historyGrid arranges the move journal into a row-major matrix with one column per seat. * Each seat's moves fill its column top to bottom, so a seat's k-th move lands in row k of * its column; with a regular turn order the rows read as rounds. When thinkingSeat is * non-null, that seat's next (still empty) cell becomes a 'thinking' placeholder — the move * being awaited. Positions with no move are 'empty', so every row is exactly seatCount wide * and the table rules evenly. Pass thinkingSeat=null when the game is over or when the * awaited mover is the viewer themselves (the viewer's own pending cell stays empty). */ export function historyGrid( moves: MoveRecord[], seatCount: number, thinkingSeat: number | null, ): HistoryCell[][] { const cols: HistoryCell[][] = Array.from({ length: seatCount }, () => []); for (const m of moves) { if (m.player < 0 || m.player >= seatCount) continue; // defensive: drop an out-of-range seat cols[m.player].push( m.action === 'play' ? { kind: 'play', player: m.player, words: m.words, score: m.score } : { kind: 'action', player: m.player, action: m.action }, ); } const think = thinkingSeat !== null && thinkingSeat >= 0 && thinkingSeat < seatCount ? thinkingSeat : null; let rows = cols.reduce((n, c) => Math.max(n, c.length), 0); if (think !== null) rows = Math.max(rows, cols[think].length + 1); const grid: HistoryCell[][] = []; for (let r = 0; r < rows; r++) { const row: HistoryCell[] = []; for (let p = 0; p < seatCount; p++) { if (r < cols[p].length) row.push(cols[p][r]); else if (think === p && r === cols[p].length) row.push({ kind: 'thinking', player: p }); else row.push({ kind: 'empty', player: p }); } grid.push(row); } return grid; }