I just shipped Boggle Online on 7x.games — a free, browser-based Boggle word game. No login, no download. Here's a technical breakdown.
🎮 Play it → 7x.games/games/boggle-online
What it does
A grid of random letter dice is rolled. A countdown timer starts. You drag through adjacent letters to spell words — horizontally, vertically, or diagonally. Each die can only be used once per word. Longer words score exponentially more points.
Features:
🎲 Classic 4×4 (16 dice, 3 min) and Big 5×5 (25 dice, 4 min)
👆 Drag-to-select with undo — drag back to remove the last letter
🧠 DFS solver finds every valid word at game start
📊 Official Boggle scoring — 3-4 letters = 1pt, 5 = 2pts, 6 = 3pts, 7 = 5pts, 8+ = 11pts
⏱ Countdown timer with urgency colors (cyan → amber → red at 10s)
📖 3,000+ word dictionary merged from two curated sources
🏆 Best score persistence per grid mode in localStorage
🔊 Web Audio API sounds — no external audio files
📱 Full touch support for mobile/tablet
The grid solver — finding every valid word with DFS
When the game starts, I run a depth-first search from every cell to find all valid words the grid contains. This lets me show "words you missed" at the end.
javascript
function solveGrid(grid, size, dict) {
const found = new Set()
const visited = Array.from({ length: size }, () => Array(size).fill(false))
function dfs(r, c, word) {
if (word.length >= 3 && dict.has(word)) found.add(word)
if (word.length >= 8) return // cap depth
for (const [nr, nc] of getNeighbors(r, c, size)) {
if (visited[nr][nc]) continue
visited[nr][nc] = true
dfs(nr, nc, word + grid[nr][nc])
visited[nr][nc] = false
}
}
for (let r = 0; r < size; r++)
for (let c = 0; c < size; c++) {
visited[r][c] = true
dfs(r, c, grid[r][c])
visited[r][c] = false
}
return found
}
Key decisions:
Depth cap at 8 — words longer than 8 letters are extremely rare in Boggle and capping prevents exponential blowup on 5×5 grids
Set-based dictionary — O(1) lookups make the DFS fast. The dictionary is built at module load time from a comma-separated string
Backtracking visited array — standard DFS with mark/unmark ensures each die is used at most once per path
On a 4×4 grid this runs in ~5ms. On 5×5 it's ~20-50ms — fast enough to run synchronously on game start.
Official Boggle dice
I use the actual Boggle dice letter distributions, not random letters:
javascript
const DICE_4X4 = [
'AAEEGN', 'ABBJOO', 'ACHOPS', 'AFFKPS',
'AOOTTW', 'CIMOTU', 'DEILRX', 'DELRVY',
'DISTTY', 'EEGHNW', 'EEINSU', 'EHRTVW',
'EIOSST', 'ELRTTY', 'HIMNQU', 'HLNNRZ',
]
Each die has 6 faces. The dice are shuffled, then one random face is picked per die. This replicates the physical Boggle experience — certain letter combinations are more likely than pure randomness, creating grids that feel "right" with a good mix of vowels and consonants.
Drag-to-select with undo
The selection system needs to enforce adjacency and allow "undo" by dragging backwards:
javascript
const handlePointerMove = (e) => {
const cell = getCellFromEvent(e)
setSelectedPath(prev => {
// Undo: dragging back to second-to-last cell
if (prev.length >= 2) {
const secondLast = prev[prev.length - 2]
if (secondLast[0] === cell[0] && secondLast[1] === cell[1]) {
return prev.slice(0, -1) // remove last
}
}
// Skip if already in path
if (prev.some(([r, c]) => r === cell[0] && c === cell[1])) return prev
// Must be adjacent to last cell
if (!isAdjacent(prev[prev.length - 1], cell)) return prev
return [...prev, cell]
})
}
The undo mechanic is crucial — without it, one wrong drag means you have to release and start over. With it, you can "back up" naturally.
Dictionary: merging two word lists
I merged two sources for broad coverage:
javascript
import { BOGGLE_DICT } from '@/lib/boggleWords' // ~2,500 words (3-8 letters)
import { WORD_LIST } from '@/app/games/word-guess/words' // ~400 five-letter words
const FULL_DICT = new Set([...BOGGLE_DICT, ...WORD_LIST])
The boggleWords.js file stores all words as a single comma-separated string that's split into a Set at module load:
javascript
const RAW = "ACE,ACT,ADD,AGE,AGO,AID,AIM..."
export const BOGGLE_DICT = new Set(RAW.split(','))
This is ~17KB — small enough to bundle client-side without impacting load time, and the Set gives O(1) lookups during both the DFS solve and player word validation.
Audio without files
All sounds are synthesized using the Web Audio API:
javascript
function mkAudio() {
let ctx = null
const tone = (freq, type, duration, volume, delay) => {
const c = ctx, t = c.currentTime + delay
const osc = c.createOscillator(), gain = c.createGain()
osc.type = type
osc.frequency.setValueAtTime(freq, t)
gain.gain.setValueAtTime(volume, t)
gain.gain.exponentialRampToValueAtTime(0.001, t + duration)
osc.start(t); osc.stop(t + duration)
}
return {
found() { tone(523, 'sine', .1, .12); tone(659, 'sine', .1, .12, .09); tone(784, 'sine', .15, .12, .18) },
wrong() { tone(220, 'sawtooth', .12, .1) },
tick() { tone(800, 'sine', .03, .04) },
}
}
Zero audio files. The "found word" sound is an ascending C-E-G triad. The "wrong" sound is a low sawtooth buzz. The countdown tick is a subtle 800Hz blip. Total audio code: ~20 lines.
Scoring system
Following official Boggle scoring rules:
Word Length Points
3-4 letters 1
5 letters 2
6 letters 3
7 letters 5
8+ letters 11
The exponential scaling means one 7-letter word = five 3-letter words. This rewards pattern recognition and vocabulary depth over speed alone.
Stack
Next.js App Router
React hooks (useState, useEffect, useRef, useCallback)
Lucide React icons
Web Audio API for sounds
localStorage for best scores
Zero game engine dependencies
What I'd add next
Trie-based dictionary for prefix pruning (reject impossible paths early in DFS)
Multiplayer mode — same grid, compete on score via Firebase
Daily challenge — seeded grid so everyone solves the same one
Word definitions — show definitions of missed words to build vocabulary
Play it: 7x.games/games/boggle-online
I'm building 7x.games — a free, no-login arcade with 60+ browser games. If you have feedback or want to see specific features, drop a comment!
Top comments (0)