+ Holding multiple possible transformations in mind at once
+ strengthens working memory and trains the prefrontal cortex.
+
+
+
+
🔍
+
Pattern Recognition
+
+ Spotting letter patterns and word families sharpens visual memory
+ and orthographic processing in the left hemisphere.
+
+
+
+
🎯
+
Strategic Thinking
+
+ Planning several moves ahead builds executive function and
+ forward-planning ability tied to the prefrontal cortex.
+
+
+
+
📚
+
Vocabulary Recall
+
+ Retrieving words under mild pressure reinforces long-term memory
+ pathways and improves verbal fluency.
+
+
+
+
+
+ {/* ── Scientific Backing ── */}
+
+
📄 Scientific Backing
+
+
+ Verbal fluency & memory: Word puzzles improve
+ working memory and cognitive flexibility.{' '}
+
+ Journal of Neuropsychiatry & Clinical Neurosciences, 2014
+
+ .
+
+
+ Aphasia rehabilitation: Word-ladder style tasks are
+ used clinically to strengthen word-retrieval pathways damaged by
+ stroke. (Stroke Rehabilitation Research, 2018)
+
+
+ Cognitive Reserve Theory: Regular engagement with
+ word games builds mental resilience and is associated with reduced
+ risk of cognitive decline in aging.{' '}
+ Stern, Y. — Cognitive reserve in ageing and Alzheimer's disease, 2012
+ .
+
+
+
+
+ {/* ── Final CTA ── */}
+
+
+
+
+ )
+}
diff --git a/src/app/game/word-ladder/play/WordLadderGame.tsx b/src/app/game/word-ladder/play/WordLadderGame.tsx
new file mode 100644
index 0000000..5061153
--- /dev/null
+++ b/src/app/game/word-ladder/play/WordLadderGame.tsx
@@ -0,0 +1,466 @@
+'use client'
+
+import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'
+import { useRouter, useSearchParams } from 'next/navigation'
+import styles from './page.module.css'
+import {
+ getRandomPuzzle,
+ isValidWord,
+ getHint,
+ diffCount,
+ type Difficulty,
+ type PuzzlePair,
+} from '../wordList'
+
+// ---------------------------------------------------------------------------
+// Types
+// ---------------------------------------------------------------------------
+
+type GameStatus = 'playing' | 'won' | 'lost'
+
+type ErrorKind =
+ | 'not-a-word'
+ | 'wrong-length'
+ | 'multiple-changes'
+ | 'no-change'
+ | 'already-used'
+ | null
+
+// ---------------------------------------------------------------------------
+// Constants
+// ---------------------------------------------------------------------------
+
+const VALID_DIFFICULTIES: readonly Difficulty[] = ['easy', 'medium', 'hard']
+
+const MAX_LIVES: Record = {
+ easy: 5,
+ medium: 3,
+ hard: 2,
+}
+
+const ERROR_MESSAGES: Record, string> = {
+ 'not-a-word': '❌ Not a valid word.',
+ 'wrong-length': '❌ Word must be the same length as the start word.',
+ 'multiple-changes': '❌ You can only change ONE letter at a time.',
+ 'no-change': '❌ The word must be different from the current one.',
+ 'already-used': '⚠️ You already used that word.',
+}
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+/** Returns an array describing which positions changed vs the previous word */
+function getChangedPositions(prev: string, current: string): boolean[] {
+ return current.split('').map((ch, i) => ch !== prev[i])
+}
+
+/** Validates the difficulty search param against the allowlist */
+function parseDifficulty(value: string | null): Difficulty {
+ if (value !== null && (VALID_DIFFICULTIES as string[]).includes(value)) {
+ return value as Difficulty
+ }
+ return 'medium'
+}
+
+// ---------------------------------------------------------------------------
+// Component
+// ---------------------------------------------------------------------------
+
+export default function WordLadderGame() {
+ const router = useRouter()
+ const searchParams = useSearchParams()
+ const difficulty = parseDifficulty(searchParams.get('difficulty'))
+
+ const [puzzle, setPuzzle] = useState(null)
+ const [chain, setChain] = useState([])
+ const [input, setInput] = useState('')
+ const [status, setStatus] = useState('playing')
+ const [error, setError] = useState(null)
+ const [hint, setHint] = useState(null)
+ const [hintsUsed, setHintsUsed] = useState(0)
+ const [lives, setLives] = useState(() => MAX_LIVES[difficulty])
+ const [shake, setShake] = useState(false)
+
+ const inputRef = useRef(null)
+ // Refs to hold pending timeout IDs so we can clean them up
+ const shakeTimerRef = useRef | null>(null)
+ const lostTimerRef = useRef | null>(null)
+
+ const maxLives = MAX_LIVES[difficulty]
+
+ // ── Cleanup timers on unmount ───────────────────────────────────────────
+ useEffect(() => {
+ return () => {
+ if (shakeTimerRef.current) clearTimeout(shakeTimerRef.current)
+ if (lostTimerRef.current) clearTimeout(lostTimerRef.current)
+ }
+ }, [])
+
+ // ── Initialise / reset a puzzle ─────────────────────────────────────────
+ const initPuzzle = useCallback(() => {
+ // Cancel any pending timers from a previous game
+ if (shakeTimerRef.current) clearTimeout(shakeTimerRef.current)
+ if (lostTimerRef.current) clearTimeout(lostTimerRef.current)
+
+ const p = getRandomPuzzle(difficulty)
+ setPuzzle(p)
+ setChain([p.start])
+ setInput('')
+ setStatus('playing')
+ setError(null)
+ setHint(null)
+ setHintsUsed(0)
+ setLives(MAX_LIVES[difficulty])
+ }, [difficulty])
+
+ useEffect(() => {
+ initPuzzle()
+ }, [initPuzzle])
+
+ // Focus the input whenever the player can act
+ useEffect(() => {
+ if (status === 'playing') inputRef.current?.focus()
+ }, [status, chain])
+
+ // ── Derived values ───────────────────────────────────────────────────────
+ const stepsUsed = chain.length - 1
+ const stepsLeft = puzzle ? puzzle.maxSteps - stepsUsed : 0
+ const currentWord = chain[chain.length - 1] ?? ''
+
+ // O(1) lookup for already-used words — avoids O(n) array scan on every submit
+ const usedWordSet = useMemo(() => new Set(chain), [chain])
+
+ // ── Error helper ─────────────────────────────────────────────────────────
+ const triggerError = useCallback((kind: ErrorKind) => {
+ setError(kind)
+ setShake(true)
+ if (shakeTimerRef.current) clearTimeout(shakeTimerRef.current)
+ shakeTimerRef.current = setTimeout(() => setShake(false), 500)
+ }, [])
+
+ // ── Submit a word ────────────────────────────────────────────────────────
+ const handleSubmit = useCallback(() => {
+ if (!puzzle || status !== 'playing') return
+ const word = input.trim().toUpperCase()
+ if (word === '') return
+
+ setHint(null)
+
+ // ── Validations that do NOT cost a life ──────────────────────────────
+ if (word.length !== puzzle.start.length) {
+ triggerError('wrong-length')
+ return
+ }
+ if (word === currentWord) {
+ triggerError('no-change')
+ return
+ }
+ if (diffCount(word, currentWord) !== 1) {
+ triggerError('multiple-changes')
+ return
+ }
+ if (usedWordSet.has(word)) {
+ triggerError('already-used')
+ return
+ }
+
+ // ── Dictionary check — costs one life ────────────────────────────────
+ if (!isValidWord(word)) {
+ triggerError('not-a-word')
+ setLives((prev) => {
+ const next = prev - 1
+ if (next <= 0) {
+ if (lostTimerRef.current) clearTimeout(lostTimerRef.current)
+ lostTimerRef.current = setTimeout(() => setStatus('lost'), 400)
+ }
+ return next
+ })
+ return
+ }
+
+ // ── Valid move ───────────────────────────────────────────────────────
+ const newChain = [...chain, word]
+ setChain(newChain)
+ setInput('')
+ setError(null)
+
+ if (word === puzzle.target) {
+ setStatus('won')
+ return
+ }
+
+ if (newChain.length - 1 >= puzzle.maxSteps) {
+ setStatus('lost')
+ }
+ }, [puzzle, status, input, currentWord, usedWordSet, chain, triggerError])
+
+ // ── Hint ─────────────────────────────────────────────────────────────────
+ const handleHint = useCallback(() => {
+ if (!puzzle || status !== 'playing') return
+ // Pass the full chain so BFS never suggests an already-used word
+ const next = getHint(currentWord, puzzle.target, chain)
+ setHint(next ?? 'no-path')
+ setHintsUsed((h) => h + 1)
+ }, [puzzle, status, currentWord, chain])
+
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') handleSubmit()
+ }, [handleSubmit])
+
+ // ── Render ───────────────────────────────────────────────────────────────
+ if (!puzzle) {
+ return