fix(ui): highlight the word the engine played, not the run it abuts #291
Reference in New Issue
Block a user
Delete Branch "fix/offline-highlight-direction"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The bug
In a one-word-per-turn game, staging a single tile that plays a valid word down a column while merely abutting a longer horizontal word greened that horizontal run instead — a word the play does not form. The score, the caption and the committed move were all correct; only the highlight (and the score badge, which anchors on the same main word) were wrong.
Reported from an offline vs-robot game, but the trigger is the rule, not the transport:
formedGeometryis called on both paths, so online one-word games were affected too.Root cause
Two independent implementations of the play's orientation:
lib/dict/direction.ts— a faithful port ofengine.resolveDirection+(*Game).playDirection. For a lone tile under the single-word rule the two orientations can differ in legality, so it runs both through the validator and keeps the higher-scoring legal one.lib/formed.ts— the highlight geometry, which re-guessed the axis ash.len >= v.len ? 'H' : 'V'. It knows nothing about the dictionary, so it took the longer run even when that run is no word at all.The earlier fix
bf46b94("one-word games must not highlight phantom cross words") closed the cross-word half of this file; the orientation half was missed.The fix
The engine's answer is already on the wire and had no consumer:
EvalResult.dir, filled by the on-device evaluator, the networkevaluateand a reused hint alike.formedGeometrynow takes it as a parameter and no longer infers the axis at all — there is no second implementation left to drift.previewis nulled on every placement change, sopreview.diralways describes the current staging.Tests
formed.test.ts— the new contract, plus a regression case built from the reported position.formed.engine.test.ts(new) — a differential sweep: random positions × both rules × two dictionaries; for every legal play the engine port finds, the highlighted cells must equal the cells of the words the engine says the play forms. Revertingformed.tsto the old inference fails it on exactly the one-word-rule cases and leaves the multi-word ones green.Verified locally, the whole
uiCI job:check,test:unit(674 passed),build,bundle-size(app entry 130.3 / 131 KB),test:e2e(256 passed, chromium + webkit).No schema, wire or doc change —
docs/FUNCTIONAL.mdalready describes the intended behaviour.The board's green highlight re-derived the play's orientation itself ("the longer run wins"), while the engine settles a lone staged tile under the single-word rule through the validator, because its two orientations can differ in legality. A vertical play that merely abuts a longer horizontal word therefore greened that horizontal run — a word the play does not form — and anchored the score badge on it, while the caption, the score and the committed move all stayed correct. formedGeometry now takes the orientation from the preview, which every preview source already carries (EvalResult.dir: the on-device evaluator, the network evaluate and a reused hint), and no longer infers it. The rule, not the transport, is the trigger, so this fixes online one-word games as well as offline ones. A differential test now walks random positions, asks the engine port for every legal play it can find, and asserts the highlighted cells are exactly the cells of the words the engine says that play forms.