A word search looks like a database problem — a dictionary, a lattice, a solver — but it is really just a 2-D array of characters and a pinch of vector geometry. I built a full one: eight words hidden in a fresh 12×12 grid every round, placed across, down, diagonally, forwards and backwards, selectable by dragging a straight line of cells. It came to about 120 lines of vanilla JavaScript, no libraries, no word database beyond a small array. Here is how it fits together.
The board is a 2-D array, and directions are vectors
There is no canvas and no tile atlas. The whole puzzle is an N×N array where every cell holds one character, and I start it as empty strings so the placement step can tell "free" from "taken." A parallel placements list records each hidden word and the exact cells it occupies — that record, not the letters, is the ground truth I check a player's selection against later.
The eight compass directions reduce to eight (row, col) step vectors. Once a direction is a vector, walking a word across the grid is one loop: add the vector to the current cell to get the next. The four directions with a negative component are exactly what give you backwards and upward words.
const DIRS = [
[0, 1], [0,-1], // E, W
[1, 0], [-1,0], // S, N
[1, 1], [1,-1], // SE, SW
[-1,1], [-1,-1] // NE, NW
];
// cell i of a word = { r: r0 + dr*i, c: c0 + dc*i }
Placing a word is a gamble with one overlap rule
To hide a word I pick a random start cell and direction, bounds-check the far end, then walk the path. Each cell is legal if it is empty or already holds the exact letter this word wants there — that second case is a shared crossing, exactly like a crossword. Any other occupied cell is a clash and the whole attempt is rejected; I roll again up to 200 times.
function fits(word, r0, c0, dr, dc){
for (let i = 0; i < word.length; i++){
const cell = grid[r0+dr*i][c0+dc*i];
if (cell !== "" && cell !== word[i]) return false; // clash
}
return true; // free or matching
}
That one rule is what makes the board feel dense instead of a few words floating in noise. After eight words stick, I flood every still-empty cell with a random letter — that camouflage turns placed strings into an actual puzzle.
Selection is placement run backwards
The nicest part: reading a drag is the same walk in reverse. Two clicked cells — where you pressed and where the pointer is now — define a line only if it is horizontal, vertical, or a perfect 45° diagonal (dr===0 || dc===0 || |dr|===|dc|). When it qualifies, the step is the sign of each delta and the cells between are the same start + step × i I used to place.
function lineBetween(a, b){
const dr = b.r - a.r, dc = b.c - a.c;
if (!(dr===0 || dc===0 || Math.abs(dr)===Math.abs(dc))) return [a];
const len = Math.max(Math.abs(dr), Math.abs(dc));
const sr = Math.sign(dr), sc = Math.sign(dc);
const path = [];
for (let i=0;i<=len;i++) path.push({ r:a.r+sr*i, c:a.c+sc*i });
return path;
}
One set of pointer handlers gives mouse-drag and touch for free: pointerdown sets the anchor, pointermove uses elementFromPoint to find the cell under the finger, pointerup commits. A no-move tap routes to a tap-start / tap-end flow instead.
Validating without being fooled
When a line commits I compare its cell path — not the letters it spells — against the placements, forwards or reversed (a word reads the same swept either way). Matching cells rather than a string is deliberate: a lucky run through random filler can spell a real word by accident, and path-matching means that coincidence never falsely counts.
function samePath(a, b){ // b, forwards or reversed
if (a.length !== b.length) return false;
const fwd = a.every((p,i) => p.r===b[i].r && p.c===b[i].c);
const rev = a.every((p,i) => p.r===b[b.length-1-i].r && p.c===b[b.length-1-i].c);
return fwd || rev;
}
The Hint button reuses the classic solver — stand on every cell, try all eight directions, match letter by letter — which is rows × cols × 8 × length, brute force, and on a 12×12 board effectively instant.
Play it (drag a line, or tap start then end):
https://dev48v.infy.uk/game/day42-word-search.html
Top comments (0)