fertig-classic-games/src/games/wordsearch/WordSearchLogic.js

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;
}