Word puzzle games have had a cultural moment. Wordle’s meteoric rise in 2022 (and subsequent acquisition by The New York Times) inspired a wave of browser-based clones and variants. I built two for this site: a Wordle clone and Word Hunt, a daily 4×4 letter grid puzzle. Both taught me a lot about puzzle generation, seeding randomness, and cross-device input.

What Makes a Good Daily Puzzle

The defining feature of Wordle — and what made it go viral — is that everyone plays the same puzzle each day. There is no leaderboard and no reward, but players could compare results with friends because everyone had the same experience. That shared context is enormously powerful for word-of-mouth growth.

Implementing a daily puzzle without a server requires a deterministic puzzle selection function tied to the current date. If everyone runs the same function with the same date, they get the same puzzle.

function getDayIndex() {
  const epoch = new Date('2024-01-01');
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  return Math.floor((today - epoch) / (1000 * 60 * 60 * 24));
}

function seededRandom(seed) {
  // Mulberry32 — fast, simple, good distribution
  let t = seed + 0x6D2B79F5;
  t = Math.imul(t ^ t >>> 15, t | 1);
  t ^= t + Math.imul(t ^ t >>> 7, t | 61);
  return ((t ^ t >>> 14) >>> 0) / 4294967296;
}

function todaysWord(wordList) {
  const index = getDayIndex();
  const rng = seededRandom(index);
  return wordList[Math.floor(rng * wordList.length)];
}

The epoch date (January 1, 2024) is arbitrary but must be fixed — changing it would shift all future puzzles.

Wordle: The Color-Coding Logic

Wordle’s feedback system looks simple but has a subtle edge case. Given a guess and a secret word, you need to return one of three colors for each letter:

  • Green: correct letter, correct position
  • Yellow: correct letter, wrong position
  • Gray: letter not in the word

The tricky case is duplicate letters. If the secret word is “OCEAN” and the guess is “LABEL”, the two Ls should not both be yellow just because L appears in the secret word once. The standard Wordle rules give yellow to the first match and gray to any excess occurrences.

The implementation I use does two passes:

function checkGuess(guess, secret) {
  const result = Array(5).fill('gray');
  const secretLetters = secret.split('');
  const guessLetters = guess.split('');

  // First pass: mark exact matches (green)
  for (let i = 0; i < 5; i++) {
    if (guessLetters[i] === secretLetters[i]) {
      result[i] = 'green';
      secretLetters[i] = null;   // consume this letter
      guessLetters[i] = null;
    }
  }

  // Second pass: mark partial matches (yellow)
  for (let i = 0; i < 5; i++) {
    if (guessLetters[i] === null) continue;
    const j = secretLetters.indexOf(guessLetters[i]);
    if (j !== -1) {
      result[i] = 'yellow';
      secretLetters[j] = null;   // consume this letter
    }
  }

  return result;
}

The null sentinel marks consumed letters. Without this two-pass approach with consumption, duplicate-letter handling would be incorrect.

Word Hunt: Grid Generation and Pathfinding

Word Hunt is a different kind of puzzle. A 4×4 letter grid contains a set of hidden words. Players find them by selecting adjacent letters in sequence (horizontally, vertically, or diagonally). The grid is generated fresh each day using a seed.

Generating a Valid Grid

A naive approach — place words and fill remaining cells randomly — often results in no additional unintended words (which is fine) or causes the intended words to be unrecognisable. My approach:

  1. Start with a list of target words for today (seeded selection from a curated word set)
  2. Attempt to place them on the grid using a backtracking depth-first search
  3. Fill remaining cells with random consonant/vowel patterns that avoid forming extra valid words where possible
  4. Verify that every target word is findable
function placeWord(grid, word, used) {
  // Try placing starting from every empty cell
  for (let r = 0; r < 4; r++) {
    for (let c = 0; c < 4; c++) {
      if (grid[r][c] !== '' && grid[r][c] !== word[0]) continue;
      const path = findPath(grid, word, r, c, used);
      if (path) return path;
    }
  }
  return null;
}

Adjacency Constraints

Finding a valid path for a word is a depth-first search where each step must be adjacent (including diagonals) to the previous one, and no cell can be reused within a single word:

function findPath(grid, word, r, c, usedCells, index = 0, path = []) {
  if (index === word.length) return path;
  if (r < 0 || r >= 4 || c < 0 || c >= 4) return null;
  if (usedCells.has(`${r},${c}`)) return null;
  if (grid[r][c] !== '' && grid[r][c] !== word[index]) return null;

  usedCells.add(`${r},${c}`);
  path.push([r, c]);

  const directions = [[-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1]];
  for (const [dr, dc] of directions) {
    const result = findPath(grid, word, r+dr, c+dc, usedCells, index+1, [...path]);
    if (result) {
      usedCells.delete(`${r},${c}`);
      return result;
    }
  }

  usedCells.delete(`${r},${c}`);
  return null;
}

This backtracking ensures we explore all possible paths through the grid.

Touch Input: Making it Feel Native

Keyboard-based word entry is natural on desktop. On mobile, you want tap-to-select letters, not a software keyboard. Both games needed separate input paths for keyboard and touch.

For Word Hunt, each letter cell is a DOM button. Tapping selects that letter and adds it to the current word. A submit button (or pressing Enter) checks the word.

For the Wordle clone (which uses Canvas rendering), I intercept physical keyboard events on desktop and render virtual keyboard buttons on the canvas for mobile touch:

document.addEventListener('keydown', e => {
  if (e.key === 'Enter') submitGuess();
  else if (e.key === 'Backspace') deleteLetter();
  else if (/^[a-zA-Z]$/.test(e.key)) addLetter(e.key.toUpperCase());
});

// Canvas click detection for virtual keyboard
canvas.addEventListener('click', e => {
  const rect = canvas.getBoundingClientRect();
  const x = e.clientX - rect.left;
  const y = e.clientY - rect.top;
  const key = getKeyAtPosition(x, y);
  if (key) handleKey(key);
});

Dictionary Management

The Wordle word list needs two components:

  1. Answer words: A curated list of common 5-letter words that make for satisfying answers (not obscure or offensive)
  2. Valid guesses: A much larger list of all valid 5-letter words that players are allowed to guess

The original Wordle used approximately 2,300 answer words and 10,600 valid guesses. My implementation uses a similar split. The lists are embedded directly in the JavaScript file as arrays — no network requests needed.

For Word Hunt, the dictionary is a curated set of 3–8 letter words chosen for puzzle-friendliness (common, unambiguous, family-appropriate). The list is small enough (~500 words for puzzle selection) that loading it inline is not a problem.

Accessibility Considerations

Word games need careful accessibility work:

  • Screen reader announcements: When a letter changes colour in Wordle, a visually hidden aria-live region announces the result (“CRANE: C gray, R yellow, A green, N gray, E gray”)
  • Keyboard navigation: All interactive elements in Word Hunt can be reached and activated with Tab and Enter
  • Colour-blind mode: The green/yellow/gray colour scheme is distinguishable for most types of colour blindness, but a high-contrast mode is on the roadmap
  • Focus management: After submitting a guess in Wordle, focus returns to the first empty cell

Conclusion

Building daily word puzzles from scratch is a surprisingly deep exercise in:

  • Pseudorandom number generation and seeding for deterministic daily puzzles
  • Careful multi-pass logic for the Wordle colour-coding system
  • Backtracking search algorithms for grid layout
  • Handling both keyboard and touch input paths
  • Dictionary curation and size management

Both games are live and receive a fresh puzzle each day. Try Wordle or Word Hunt and see how you do!