Compare commits

...

3 Commits

Author SHA1 Message Date
developer 0eb1e0ef47 Merge pull request 'perf(ui): reuse hint result as move preview, skip redundant evaluate' (#85) from feature/hint-preview-reuse into development
CI / changes (push) Successful in 2s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 53s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 1m8s
2026-06-19 10:18:14 +00:00
Ilia Denisov d0681c5efe fix(ui): lower-case hint preview words to match evaluate
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 52s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m20s
A hint's MoveRecord words are upper-cased on decode (for the move-history view),
whereas an EvalResult keeps the backend's lower case. Seeding the move preview
from the hint verbatim flipped the score caption to upper case and nudged its
height. Lower-case the words in previewFromHint so the caption reads the same
as the evaluate path it replaces.
2026-06-19 12:12:53 +02:00
Ilia Denisov 0dd4099d68 perf(ui): reuse hint result as move preview, skip redundant evaluate
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 53s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m9s
Taking a hint auto-placed the suggested tiles and then called recompute(),
which round-trips a debounced evaluate for that exact placement. The hint is
the engine's own top-ranked, fully scored legal move, so its move already
carries the score, words and direction an evaluate would return — the second
call was pure duplicate work and added a visible disabled->enabled flicker on
the submit button over slow links.

Seed the move preview directly from the hint move via previewFromHint and
cancel any pending evaluate timer so a stale one cannot clobber it. A later
manual edit re-arms recompute() as before, so rearranged tiles are re-evaluated
normally. Client-only; no backend, wire or schema change.
2026-06-19 12:03:27 +02:00
3 changed files with 42 additions and 2 deletions
+9 -1
View File
@@ -29,6 +29,7 @@
newPlacement, newPlacement,
place, place,
placementFromHint, placementFromHint,
previewFromHint,
rackView, rackView,
recallAt, recallAt,
recallToSlot, recallToSlot,
@@ -751,7 +752,14 @@
if (isCoarse()) zoomed = true; if (isCoarse()) zoomed = true;
view = { ...view, hintsRemaining: h.hintsRemaining, walletBalance: h.walletBalance }; view = { ...view, hintsRemaining: h.hintsRemaining, walletBalance: h.walletBalance };
syncWallet(h.walletBalance); syncWallet(h.walletBalance);
recompute(); // The hint is the engine's own top-ranked, fully scored legal move: reuse it as the
// preview instead of a redundant evaluate (same engine call, same placement). Cancel any
// pending evaluate so a stale one cannot clobber it; a later manual edit re-arms recompute().
if (previewTimer) {
clearTimeout(previewTimer);
previewTimer = null;
}
preview = previewFromHint(h.move);
} }
} catch (e) { } catch (e) {
// The backend does not spend a hint when there is no move. // The backend does not spend a hint when there is no move.
+19
View File
@@ -6,6 +6,7 @@ import {
newPlacement, newPlacement,
place, place,
placementFromHint, placementFromHint,
previewFromHint,
rackView, rackView,
recallAt, recallAt,
recallIndex, recallIndex,
@@ -105,6 +106,24 @@ describe('placementFromHint', () => {
}); });
}); });
describe('previewFromHint', () => {
it('reuses the hint move as a legal preview, lower-casing words to match evaluate', () => {
const preview = previewFromHint({
player: 0,
action: 'play',
dir: 'V',
mainRow: 7,
mainCol: 7,
tiles: [{ row: 7, col: 7, letter: 'C', blank: false }],
words: ['CAT', 'AN'],
count: 0,
score: 18,
total: 42,
});
expect(preview).toEqual({ legal: true, score: 18, words: ['cat', 'an'], dir: 'V' });
});
});
describe('reorderIndices', () => { describe('reorderIndices', () => {
it('lifts an element and drops it at the given slot among the others', () => { it('lifts an element and drops it at the given slot among the others', () => {
expect(reorderIndices(4, 0, 2)).toEqual([1, 2, 0, 3]); expect(reorderIndices(4, 0, 2)).toEqual([1, 2, 0, 3]);
+14 -1
View File
@@ -4,7 +4,7 @@
// payload. It is board-agnostic (the gateway/engine does full legality validation at // payload. It is board-agnostic (the gateway/engine does full legality validation at
// submit), which keeps it trivially unit-testable. // submit), which keeps it trivially unit-testable.
import type { Tile } from './model'; import type { EvalResult, MoveRecord, Tile } from './model';
import type { PlacedTile } from './client'; import type { PlacedTile } from './client';
export interface PendingTile { export interface PendingTile {
@@ -55,6 +55,19 @@ export function placementFromHint(tiles: Tile[], rack: string[]): Placement {
return { rack: [...rack], pending }; return { rack: [...rack], pending };
} }
/**
* previewFromHint turns a hint's move into a move preview. A hint is the engine's own
* top-ranked, fully scored legal play, so its move already carries the score, words and
* direction an evaluate would compute for the same placement; reusing it lets the client
* skip a redundant evaluate round-trip after auto-placing the hint. The words are
* lower-cased to match the evaluate result the caption renders: a hint's MoveRecord words
* are upper-cased on decode (for the move-history view), whereas an EvalResult keeps the
* backend's lower case, so reusing them verbatim would otherwise change the caption's case.
*/
export function previewFromHint(move: MoveRecord): EvalResult {
return { legal: true, score: move.score, words: move.words.map((w) => w.toLowerCase()), dir: move.dir };
}
function usedIndexes(p: Placement): Set<number> { function usedIndexes(p: Placement): Set<number> {
return new Set(p.pending.map((t) => t.rackIndex)); return new Set(p.pending.map((t) => t.rackIndex));
} }