78 lines
3.0 KiB
JavaScript
78 lines
3.0 KiB
JavaScript
// Pure helpers for Word Search selection — no Phaser, easy to reason about/test.
|
|
|
|
// All 8 directions as [rowDelta, colDelta]. Selection always snaps to one of
|
|
// these regardless of difficulty; matchWord checks both orientations, so a word
|
|
// can be traced from either end.
|
|
export const DIRECTIONS = [
|
|
[0, 1], [0, -1], [1, 0], [-1, 0],
|
|
[1, 1], [1, -1], [-1, 1], [-1, -1],
|
|
];
|
|
|
|
// How many steps we can take from (br,bc) along (dr,dc) before leaving the grid.
|
|
function maxStepsInBounds(size, br, bc, dr, dc) {
|
|
let steps = Infinity;
|
|
if (dr > 0) steps = Math.min(steps, size - 1 - br);
|
|
else if (dr < 0) steps = Math.min(steps, br);
|
|
if (dc > 0) steps = Math.min(steps, size - 1 - bc);
|
|
else if (dc < 0) steps = Math.min(steps, bc);
|
|
return steps === Infinity ? 0 : steps;
|
|
}
|
|
|
|
// Given an anchor cell (br,bc) and a target cell (tr,tc), pick the straight line
|
|
// that best matches the drag and return its cells. The chosen direction is the
|
|
// one whose unit vector is most aligned with the drag; the length is the drag's
|
|
// projection onto that direction, clamped to the grid.
|
|
export function snapSelection(size, br, bc, tr, tc, allowedDirs = DIRECTIONS) {
|
|
const dR = tr - br;
|
|
const dC = tc - bc;
|
|
|
|
if (dR === 0 && dC === 0) {
|
|
return { dir: [0, 0], cells: [[br, bc]] };
|
|
}
|
|
|
|
let best = null;
|
|
for (const [dr, dc] of allowedDirs) {
|
|
const denom = dr * dr + dc * dc; // 1 (orthogonal) or 2 (diagonal)
|
|
const proj = (dR * dr + dC * dc) / denom; // steps along this direction
|
|
if (proj <= 0) continue; // wrong way — skip
|
|
const maxN = maxStepsInBounds(size, br, bc, dr, dc);
|
|
const n = Math.max(1, Math.min(Math.round(proj), maxN));
|
|
if (n < 1) continue;
|
|
|
|
// Landing point if we step n times; error vs. the actual target.
|
|
const lr = br + dr * n;
|
|
const lc = bc + dc * n;
|
|
const err = (lr - tr) * (lr - tr) + (lc - tc) * (lc - tc);
|
|
|
|
// Prefer the direction with the better unit-vector alignment, breaking ties
|
|
// by the smaller landing error.
|
|
const align = (dR * dr + dC * dc) / (Math.sqrt(dR * dR + dC * dC) * Math.sqrt(denom));
|
|
if (!best || align > best.align + 1e-9 || (Math.abs(align - best.align) <= 1e-9 && err < best.err)) {
|
|
best = { dr, dc, n, align, err };
|
|
}
|
|
}
|
|
|
|
if (!best) return { dir: [0, 0], cells: [[br, bc]] };
|
|
|
|
const cells = [];
|
|
for (let i = 0; i <= best.n; i++) {
|
|
cells.push([br + best.dr * i, bc + best.dc * i]);
|
|
}
|
|
return { dir: [best.dr, best.dc], cells };
|
|
}
|
|
|
|
// Letters under a list of [r,c] cells, joined into a string. `grid` is an array
|
|
// of row strings (grid[r][c] indexing works on strings).
|
|
export function lettersAt(grid, cells) {
|
|
return cells.map(([r, c]) => grid[r][c]).join('');
|
|
}
|
|
|
|
// If `letters` (or its reverse) is one of `words`, return the matched word;
|
|
// otherwise null.
|
|
export function matchWord(letters, words) {
|
|
const rev = [...letters].reverse().join('');
|
|
if (words.includes(letters)) return letters;
|
|
if (words.includes(rev)) return rev;
|
|
return null;
|
|
}
|