feat(ui): move in-game status to the board — highlight, score badge, full-width rack
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) Failing after 13s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Failing after 0s
CI / deploy (pull_request) Has been skipped
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) Failing after 13s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Failing after 0s
CI / deploy (pull_request) Has been skipped
Remove the under-board status strip and relocate its signals: - bag count -> a badge on the exchange/pass control + the foot of the move table - whose-turn / win-lose -> a thin strip above the score plaques - the tentative-move caption -> the board itself: staged tiles tint green (legal) or pink (illegal), the board tiles a formed word runs through go a shade darker, and an orange score badge sits on the main word (digit sized like a tile value, clamped on-board) The word geometry (covered cells + badge anchor) is a new pure client-side helper (ui/src/lib/formed.ts), independent of the move evaluator, so it works on the local or network preview path alike; the badge's number still comes from the preview score. Rack: a seven-column grid filling the tray width in both layouts — square, full-width tiles — with the confirm control in the fixed 7th slot. Settings: a touch-only "Zoom the board" toggle (default on, device-local) gates the tile-placement auto-zoom; taking a hint while zoomed in now zooms out so the highlighted hint word is never left off-screen. Docs (FUNCTIONAL +_ru, UI_DESIGN, ARCHITECTURE) and e2e/unit tests updated.
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { formedGeometry, badgePlacement } from './formed';
|
||||
import type { Board } from './board';
|
||||
|
||||
const SIZE = 15;
|
||||
|
||||
// board builds a 15×15 empty board with the given committed tiles placed.
|
||||
function board(tiles: Array<[number, number, string]> = []): Board {
|
||||
const b: Board = [];
|
||||
for (let r = 0; r < SIZE; r++) {
|
||||
const row: (Board[number][number])[] = [];
|
||||
for (let c = 0; c < SIZE; c++) row.push(null);
|
||||
b.push(row);
|
||||
}
|
||||
for (const [r, c, letter] of tiles) b[r][c] = { letter, blank: false };
|
||||
return b;
|
||||
}
|
||||
|
||||
const cells = (g: ReturnType<typeof formedGeometry>) => [...(g?.cells ?? new Set())].sort();
|
||||
|
||||
describe('formedGeometry', () => {
|
||||
it('returns null with nothing staged', () => {
|
||||
expect(formedGeometry(board(), [])).toBeNull();
|
||||
});
|
||||
|
||||
it('finds a fresh horizontal word with no cross words', () => {
|
||||
const g = formedGeometry(board(), [
|
||||
{ row: 7, col: 7 },
|
||||
{ row: 7, col: 8 },
|
||||
]);
|
||||
expect(g?.main).toEqual({ row: 7, col: 7, dir: 'H', len: 2 });
|
||||
expect(cells(g)).toEqual(['7,7', '7,8']);
|
||||
});
|
||||
|
||||
it('finds a fresh vertical word', () => {
|
||||
const g = formedGeometry(board(), [
|
||||
{ row: 7, col: 7 },
|
||||
{ row: 8, col: 7 },
|
||||
]);
|
||||
expect(g?.main).toEqual({ row: 7, col: 7, dir: 'V', len: 2 });
|
||||
expect(cells(g)).toEqual(['7,7', '8,7']);
|
||||
});
|
||||
|
||||
it('extends a committed tile into the main word (committed cell in the set)', () => {
|
||||
// A committed A at (7,7); a lone tile to its right forms the two-letter main word.
|
||||
const g = formedGeometry(board([[7, 7, 'A']]), [{ row: 7, col: 8 }]);
|
||||
expect(g?.main).toEqual({ row: 7, col: 7, dir: 'H', len: 2 });
|
||||
expect(cells(g)).toEqual(['7,7', '7,8']);
|
||||
});
|
||||
|
||||
it('collects a cross word through a staged tile, committed cells included', () => {
|
||||
// Committed X above and Y below (7,8) leave a one-cell gap; a horizontal play through it also
|
||||
// completes the vertical X-_-Y cross word.
|
||||
const g = formedGeometry(board([[6, 8, 'X'], [8, 8, 'Y']]), [
|
||||
{ row: 7, col: 7 },
|
||||
{ row: 7, col: 8 },
|
||||
]);
|
||||
expect(g?.main).toEqual({ row: 7, col: 7, dir: 'H', len: 2 });
|
||||
expect(cells(g)).toEqual(['6,8', '7,7', '7,8', '8,8']);
|
||||
});
|
||||
|
||||
it('takes the longer axis as the main word for a lone connecting tile', () => {
|
||||
// (7,8) joins a length-3 horizontal (cols 6-8) and a length-4 vertical (rows 5-8): the vertical
|
||||
// is longer, so it is the main word and the horizontal becomes a cross word.
|
||||
const g = formedGeometry(
|
||||
board([
|
||||
[7, 6, 'A'],
|
||||
[7, 7, 'B'],
|
||||
[5, 8, 'C'],
|
||||
[6, 8, 'D'],
|
||||
[8, 8, 'E'],
|
||||
]),
|
||||
[{ row: 7, col: 8 }],
|
||||
);
|
||||
expect(g?.main).toEqual({ row: 5, col: 8, dir: 'V', len: 4 });
|
||||
expect(cells(g)).toEqual(['5,8', '6,8', '7,6', '7,7', '7,8', '8,8']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('badgePlacement', () => {
|
||||
it('anchors a mid-board horizontal word at the last letter, top-right', () => {
|
||||
expect(badgePlacement({ row: 7, col: 5, dir: 'H', len: 3 })).toEqual({ row: 7, col: 7, corner: 'tr' });
|
||||
});
|
||||
|
||||
it('anchors a right-edge horizontal word at the first letter, bottom-left', () => {
|
||||
expect(badgePlacement({ row: 7, col: 12, dir: 'H', len: 3 })).toEqual({ row: 7, col: 12, corner: 'bl' });
|
||||
});
|
||||
|
||||
it('anchors a top-edge horizontal word at the first letter, bottom-left', () => {
|
||||
expect(badgePlacement({ row: 0, col: 5, dir: 'H', len: 3 })).toEqual({ row: 0, col: 5, corner: 'bl' });
|
||||
});
|
||||
|
||||
it('anchors a left-edge horizontal word at the last letter, top-right', () => {
|
||||
expect(badgePlacement({ row: 7, col: 0, dir: 'H', len: 3 })).toEqual({ row: 7, col: 2, corner: 'tr' });
|
||||
});
|
||||
|
||||
it('anchors a bottom-edge horizontal word at the last letter, top-right', () => {
|
||||
expect(badgePlacement({ row: 14, col: 5, dir: 'H', len: 3 })).toEqual({ row: 14, col: 7, corner: 'tr' });
|
||||
});
|
||||
|
||||
it('anchors a mid-board vertical word at the first letter, top-right', () => {
|
||||
expect(badgePlacement({ row: 5, col: 7, dir: 'V', len: 3 })).toEqual({ row: 5, col: 7, corner: 'tr' });
|
||||
});
|
||||
|
||||
it('anchors a top-edge vertical word at the last letter, bottom-left', () => {
|
||||
expect(badgePlacement({ row: 0, col: 7, dir: 'V', len: 3 })).toEqual({ row: 2, col: 7, corner: 'bl' });
|
||||
});
|
||||
|
||||
it('anchors a right-edge vertical word at the last letter, bottom-left', () => {
|
||||
expect(badgePlacement({ row: 5, col: 14, dir: 'V', len: 3 })).toEqual({ row: 7, col: 14, corner: 'bl' });
|
||||
});
|
||||
|
||||
it('anchors a full-width horizontal word at the last letter, top-right (clamped by the board)', () => {
|
||||
expect(badgePlacement({ row: 7, col: 0, dir: 'H', len: 15 })).toEqual({ row: 7, col: 14, corner: 'tr' });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user