fix(offline): keep the local composition; feat(rules): no repeated words in Erudit #287
@@ -41,8 +41,12 @@ const DIST = 'dist';
|
|||||||
// + the update-available nudge) and the unified-lobby / create-flow wiring ride the always-loaded
|
// + the update-available nudge) and the unified-lobby / create-flow wiring ride the always-loaded
|
||||||
// transport / App / Lobby / New Game screens (the dict-availability guard's `getDawg` stays lazy). The
|
// transport / App / Lobby / New Game screens (the dict-availability guard's `getDawg` stays lazy). The
|
||||||
// heavy parts — the dict loader, the move generator and the preload orchestration — still stay in lazy
|
// heavy parts — the dict loader, the move generator and the preload orchestration — still stay in lazy
|
||||||
// chunks. Scoped CSS lands in the CSS chunk, not this JS budget.
|
// chunks. Raised to 131 for the Erudit no-repeat-words rule: the per-game flag rides the decoded
|
||||||
const BUDGET = { app: 130, shared: 31, landing: 5 };
|
// GameView (codec + model), and the always-loaded game screen has to name the word its preview
|
||||||
|
// rejected, since a repeated word is a real word and the board alone cannot say why the play is
|
||||||
|
// refused. The rule's own logic — the played-word set and the rescoring — sits with the evaluator in
|
||||||
|
// the lazy dict chunk. Scoped CSS lands in the CSS chunk, not this JS budget.
|
||||||
|
const BUDGET = { app: 131, shared: 31, landing: 5 };
|
||||||
|
|
||||||
// gzipped returns the gzipped byte size of a built asset, or 0 when the reference is not a
|
// gzipped returns the gzipped byte size of a built asset, or 0 when the reference is not a
|
||||||
// local file (e.g. the Telegram SDK loaded from a CDN) or is missing.
|
// local file (e.g. the Telegram SDK loaded from a CDN) or is missing.
|
||||||
|
|||||||
+4
-13
@@ -276,18 +276,6 @@
|
|||||||
handleError(e);
|
handleError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// playedWords is the no-repeat-words rule's lookup set for the on-device preview: every word this
|
|
||||||
// game has already formed, as the history carries them (decoded, lower-case). Only a game that
|
|
||||||
// takes the rule needs it; for any other the preview scores unrestricted, so it returns undefined.
|
|
||||||
function playedWords(): Set<string> | undefined {
|
|
||||||
if (!view?.game.noRepeatWords) return undefined;
|
|
||||||
const played = new Set<string>();
|
|
||||||
for (const m of moves) {
|
|
||||||
if (m.action === 'play') for (const w of m.words) played.add(w);
|
|
||||||
}
|
|
||||||
return played;
|
|
||||||
}
|
|
||||||
|
|
||||||
let draftSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
let draftSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
// scheduleDraftSave persists the composition (rack order + pending tiles) after a short
|
// scheduleDraftSave persists the composition (rack order + pending tiles) after a short
|
||||||
// debounce; best-effort, so a failed save never interrupts play. The cache is updated at once
|
// debounce; best-effort, so a failed save never interrupts play. The cache is updated at once
|
||||||
@@ -845,7 +833,10 @@
|
|||||||
const reader = d.peekDawg(v.game.variant, v.game.dictVersion);
|
const reader = d.peekDawg(v.game.variant, v.game.dictVersion);
|
||||||
if (reader && hasAlphabet(v.game.variant)) {
|
if (reader && hasAlphabet(v.game.variant)) {
|
||||||
try {
|
try {
|
||||||
preview = d.evaluateLocal(reader, v.game.variant, board, sub.tiles, v.game.multipleWordsPerTurn, playedWords());
|
// Under the no-repeat-words rule the evaluator also needs the words this game has
|
||||||
|
// already formed; both it and that set live behind the same lazy import.
|
||||||
|
const played = v.game.noRepeatWords ? d.playedWordsFromHistory(moves) : undefined;
|
||||||
|
preview = d.evaluateLocal(reader, v.game.variant, board, sub.tiles, v.game.multipleWordsPerTurn, played);
|
||||||
notePreviewLocal();
|
notePreviewLocal();
|
||||||
return;
|
return;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -3,5 +3,6 @@
|
|||||||
// of the initial app bundle — it is a progressive enhancement over the network
|
// of the initial app bundle — it is a progressive enhancement over the network
|
||||||
// preview, which remains the fallback.
|
// preview, which remains the fallback.
|
||||||
export { evaluateLocal } from './eval';
|
export { evaluateLocal } from './eval';
|
||||||
|
export { playedWordsFromHistory } from './norepeat';
|
||||||
export { getDawg, peekDawg, hasDawg, dictLoadingDisabled, noteDictMiss, clearDictInstances, bustDictHttpCache } from './loader';
|
export { getDawg, peekDawg, hasDawg, dictLoadingDisabled, noteDictMiss, clearDictInstances, bustDictHttpCache } from './loader';
|
||||||
export { clearDictCache, listCachedDicts } from './store';
|
export { clearDictCache, listCachedDicts } from './store';
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
// variant, so a game started before the rule existed plays on without it.
|
// variant, so a game started before the rule existed plays on without it.
|
||||||
|
|
||||||
import type { Move } from './validate';
|
import type { Move } from './validate';
|
||||||
|
import type { MoveRecord } from '../model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* wordKey identifies a word for the rule. Words are alphabet-index letters, so the key ignores
|
* wordKey identifies a word for the rule. Words are alphabet-index letters, so the key ignores
|
||||||
@@ -36,6 +37,20 @@ export function playedWordKeys(words: Iterable<readonly number[]>): Set<string>
|
|||||||
return played;
|
return played;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* playedWordsFromHistory is the rule's lookup set for a game whose history the server decoded for
|
||||||
|
* us: every word its plays have formed, main words and cross-words alike, in the same lower-case
|
||||||
|
* form evaluateLocal decodes a candidate word into. It lives here, with the rest of the rule and
|
||||||
|
* behind the same lazy import as the evaluator, rather than in the game screen.
|
||||||
|
*/
|
||||||
|
export function playedWordsFromHistory(moves: readonly MoveRecord[]): Set<string> {
|
||||||
|
const played = new Set<string>();
|
||||||
|
for (const m of moves) {
|
||||||
|
if (m.action === 'play') for (const w of m.words) played.add(w);
|
||||||
|
}
|
||||||
|
return played;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* noRepeatScore applies the rule to an already validated move against the set of words already
|
* noRepeatScore applies the rule to an already validated move against the set of words already
|
||||||
* played. It returns null when the move's main word is one of them — the play is illegal — and
|
* played. It returns null when the move's main word is one of them — the play is illegal — and
|
||||||
|
|||||||
Reference in New Issue
Block a user