DEV Community

Cover image for I built a free Word Search puzzle game with drag-to-select, 7 categories, and 3 difficulty levels — all in the browser
7x Games
7x Games

Posted on

I built a free Word Search puzzle game with drag-to-select, 7 categories, and 3 difficulty levels — all in the browser

I just shipped a Word Search puzzle game on 7x.games — completely free, no login, no download. Here's a breakdown of how I built it and the design decisions behind it.

🎮 Play it here → 7x.games/games/word-search
What it does
Classic word search: a grid of random letters with hidden words placed in straight lines. You click and drag across the letters to highlight a word. If it matches one from the list, it stays highlighted and gets crossed off. Find all the words as fast as you can.

Features:

🐾 7 categories — Animals, Countries, Sports, Food, Science, Nature, and Random Mix
🟢🟡🔴 3 difficulty levels — Easy (8×8, 5 words), Medium (10×10, 7 words), Hard (12×12, 9 words)
📐 Directional complexity — Easy goes right/down only. Medium adds diagonals. Hard adds backwards too
⏱ Timer with personal bests — Saved per category × difficulty combo in localStorage
🔊 Audio feedback — Satisfying sounds for selections, correct matches, wrong attempts, and puzzle completion
📱 Fully touch-responsive — Drag to select works on mobile/tablet with touchAction: none and proper touch event handling
🎨 Multi-color highlights — Each found word is highlighted in a different color so you can visually distinguish overlapping words
The grid generation algorithm
The core challenge is placing words into a grid without conflicts. Here's how I approached it:

javascript
function generateGrid(words, size, directions) {
const grid = Array.from({ length: size }, () => Array(size).fill(''))
const shuffled = [...words].sort(() => Math.random() - 0.5)
for (const word of shuffled) {
const dirs = [...directions].sort(() => Math.random() - 0.5)

    for (let attempt = 0; attempt < 100; attempt++) {
        const dir = dirs[attempt % dirs.length]
        const [dr, dc] = DIRECTION_DELTAS[dir]
        // ... clamp start position, check every cell
        // Allow overlap only if the existing letter matches
        if (grid[r][c] !== '' && grid[r][c] !== word[i]) break
    }
}
// Fill blanks with random letters
for (let r = 0; r < size; r++)
    for (let c = 0; c < size; c++)
        if (grid[r][c] === '') 
            grid[r][c] = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[Math.floor(Math.random() * 26)]
Enter fullscreen mode Exit fullscreen mode

}
Key decisions:

Shuffle word order before placing — longer words placed first would always get priority, creating predictable layouts
100 attempts per word with rotating direction choices — balances placement success rate vs. generation speed
Allow letter overlap when characters match — this creates more compact, interesting puzzles and adds false leads for the player
Random filler letters — I considered weighting filler letters toward word-start letters for extra difficulty, but kept it uniform for now
Drag-to-select on touch and mouse
The selection system needs to work with both mouse and touch, constrained to straight lines only (horizontal, vertical, or 45° diagonal):

javascript
const computeLineCells = (start, end) => {
const [r1, c1] = start
const [r2, c2] = end
const dR = Math.abs(r2 - r1)
const dC = Math.abs(c2 - c1)
// Must be horizontal, vertical, or 45° diagonal
if (dR !== 0 && dC !== 0 && dR !== dC) return []
const steps = Math.max(dR, dC)
const dr = Math.sign(r2 - r1)
const dc = Math.sign(c2 - c1)

return Array.from({ length: steps + 1 }, (_, i) => 
    [r1 + dr * i, c1 + dc * i]
)
Enter fullscreen mode Exit fullscreen mode

}
The dR !== dC check is the key constraint — it ensures the selection snaps to valid lines and ignores arbitrary diagonal drags. On touch devices, I set touchAction: 'none' on the grid container and prevent default on touchmove to stop page scrolling while dragging.

Word matching — forward and reverse
On Hard difficulty, words can be placed backwards. So the selection check tests both directions:

javascript
const selectedWord = cells.map(([r, c]) => grid[r][c]).join('')
const reversedWord = [...selectedWord].reverse().join('')
for (const placement of placements) {
if (placement.word === selectedWord || placement.word === reversedWord)
return placement.word
}
This means a player can drag in either direction to find a word — left-to-right or right-to-left both work.

Design choices
Dark theme with cyan accent (#22d3ee) — consistent with the 7x.games design system
Color-coded found words — 9 rotating highlight colors so overlapping words remain distinguishable
Progress bar — found/total with a gradient fill that grows visually with each find
Inline word list above the grid — found words get line-through and dim to 50% opacity instead of disappearing (keeps the list stable, no layout shifts)
Audio using Web Audio API — no external files, just synthesized tones with OscillatorNode. Found words get an ascending 3-note chime, wrong selections get a low buzz
Stack
Next.js (App Router)
React with hooks (useState, useEffect, useRef, useCallback)
Lucide React for icons
Web Audio API for sound effects
localStorage for persisting best times
Zero external game libraries
What I'd add next
Daily challenge mode — same puzzle for everyone, leaderboard comparison
Hint system — reveal the first letter of an unfound word
Custom word lists — let players paste their own vocabulary lists
Multiplayer race — two players solve the same grid simultaneously via Firebase
Play it: 7x.games/games/word-search

If you have feedback or feature ideas, drop a comment! I'm building the entire 7x.games arcade one game at a time — 60+ games and counting.

Follow me for more browser game dev posts. I'm building 7x.games — a free, no-login arcade with 60+ games.

Top comments (0)