Prompt
Implement the logic for a Hangman-style word guessing game.
Write three pure functions:
getDisplay(word, guessed)— returns the word with unguessed letters as underscores (e.g. "P _ K A _ H U")processGuess(letter, state)— processes a letter guess, returns new { guessed, wrong, gameOver, won }isGameOver(state)— returns { won: boolean, lost: boolean }
The word is "PIKACHU", max 6 wrong guesses.
Solution
processGuess(letter, state) → newState is a reducer in disguise, and that's the point: the whole game is a state-transition function over { guessed, wrong }. The component becomes a thin shell — onPress={l => setState(s => processGuess(l, s))} — or drops straight into useReducer unchanged. Pure transitions mean every rule below is a one-line unit test, no renderer involved.
The rules encoded in the transition:
- Repeat guesses are a no-op — the early return keeps a double-tap on the same key from burning a wrong guess, and returns without cloning since nothing changed.
- Wrong only increments on a miss, compared against
MAX_WRONG, a named constant — the same number the UI uses to draw remaining lives, defined once. - Win is derived, not tracked:
getDisplaywith the new guesses has no underscores left ⇒ won. Reusing the display function as the win predicate guarantees the screen and the game state can never disagree — there is no second bookkeeping structure to drift. - New state is built with spread (
[...guessed, letter]); the incoming state object is never touched.
isGameOver stays trivial because the transition already did the work — it just projects { won, lost } for the end screen.
Red flag: maintaining a separate won boolean updated in a different code path from guessed. Two writes for one fact is exactly how a game shows the win banner while the display still has underscores.
Say it: "processGuess is a pure reducer — repeat guesses no-op, wrong counts against a named max, and the win is derived from the display so UI and state can't diverge."