fix(ui): one-word games must not highlight phantom cross words; review polish
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 21s
CI / ui (pull_request) Successful in 1m25s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m2s

Board-highlight bug (reported on the contour): formedGeometry walked cross words
unconditionally, so in a single-word (one-word-per-turn) game a staged tile sitting
next to a committed tile lit up a green "cross word" the engine ignores — and which
need not even be a real word (the reported "БО lights up green in a one-word ПОПА
play"). Gate the cross-word walk on the game's multipleWordsPerTurn flag. The score
(8) was already correct — a premium square under the main word.

Also, from review:
- the turn strip reads the staged play's "WORD+WORD = N" while composing a legal move,
  reverting to the turn / result text otherwise;
- the Exchange/Pass dialog shows the bag count ("In the bag: N" / "Bag is empty")
  right-aligned in the title row, via a new optional Modal `titleAside`;
- cosmetics: half the turn strip's bottom padding (the plaques below carry their own
  top pad); a top gap above the landscape rack (it sat flush under the docked history);
  more horizontal padding on tab count badges so a 2-3 digit bag count clears the pill
  ends;
- admin console: the game Summary now shows the single-word / multiple-words rule.

Tests: formed single-word case added; full unit (584) + e2e (chromium + webkit, 113
each) green; backend build + adminconsole templates parse. Docs (FUNCTIONAL +_ru,
UI_DESIGN) updated.
This commit is contained in:
Ilia Denisov
2026-07-10 15:58:05 +02:00
parent 1e1117c28e
commit bf46b9492d
13 changed files with 111 additions and 36 deletions
+18 -5
View File
@@ -20,14 +20,14 @@ const cells = (g: ReturnType<typeof formedGeometry>) => [...(g?.cells ?? new Set
describe('formedGeometry', () => {
it('returns null with nothing staged', () => {
expect(formedGeometry(board(), [])).toBeNull();
expect(formedGeometry(board(), [], true)).toBeNull();
});
it('finds a fresh horizontal word with no cross words', () => {
const g = formedGeometry(board(), [
{ row: 7, col: 7 },
{ row: 7, col: 8 },
]);
], true);
expect(g?.main).toEqual({ row: 7, col: 7, dir: 'H', len: 2 });
expect(cells(g)).toEqual(['7,7', '7,8']);
});
@@ -36,14 +36,14 @@ describe('formedGeometry', () => {
const g = formedGeometry(board(), [
{ row: 7, col: 7 },
{ row: 8, col: 7 },
]);
], true);
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 }]);
const g = formedGeometry(board([[7, 7, 'A']]), [{ row: 7, col: 8 }], true);
expect(g?.main).toEqual({ row: 7, col: 7, dir: 'H', len: 2 });
expect(cells(g)).toEqual(['7,7', '7,8']);
});
@@ -54,11 +54,23 @@ describe('formedGeometry', () => {
const g = formedGeometry(board([[6, 8, 'X'], [8, 8, 'Y']]), [
{ row: 7, col: 7 },
{ row: 7, col: 8 },
]);
], true);
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('omits cross words under the single-word rule (multipleWords = false)', () => {
// The same board as the cross-word case, but a single-word game: the engine ignores the vertical
// X-_-Y run (and it need not be a real word), so the geometry must highlight only the main
// (horizontal) word — the reported "БО lights up green in a one-word game" bug.
const g = formedGeometry(board([[6, 8, 'X'], [8, 8, 'Y']]), [
{ row: 7, col: 7 },
{ row: 7, col: 8 },
], false);
expect(g?.main).toEqual({ row: 7, col: 7, dir: 'H', len: 2 });
expect(cells(g)).toEqual(['7,7', '7,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.
@@ -71,6 +83,7 @@ describe('formedGeometry', () => {
[8, 8, 'E'],
]),
[{ row: 7, col: 8 }],
true,
);
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']);