Module Complete

Build Hangman

You started with maskedWord: a word and some guesses in, a masked string out, small enough to test on its own. You built the rest one function at a time, each tested before the next existed, then composed them: processGuess takes a state and a letter and returns the next state, and playTurn runs the pieces in order and hands back what the page should show. The page draws that result. The logic never touches the page.

Two ideas were doing the work there. The first is the state transition: a function of the form (state, event) -> next state, computed without mutating anything. That is the shape of a reducer, and it is why the same move scales. An event-sourced system keeps a log of those events and rebuilds its current state by replaying them, and undo is just holding on to the previous state. The second is what Gary Bernhardt calls a functional core and an imperative shell: processGuess decides, the renderer acts. Keep the decisions pure and push the side effects to the edge, and the core stays testable on its own, with no browser to drive and nothing to mock.

The tests did more than confirm the code. Writing one first made you state what a function should do before deciding how. A concrete example pins down behavior more precisely than a description, which is the real point of test-driven development. Then the platform turned the question on the tests themselves: would they have failed if the code were wrong? That is mutation testing, a sharper check than coverage, since a line can be run by a test that asserts nothing. A test earns its place by ruling out a specific mistake, and that standard matters more as a program grows, not less.

That growth is the direction worth looking. One state object and one transition function is enough for one game. It stops being enough when several states depend on one another, when the state has to survive a reload, when two events arrive at the same moment, when the program is too large for anyone to hold in their head at once. Each of those is a real obstruction, and each summons a technique built to answer it. The move underneath does not change: find the pure core and push the effects to its edge.

The difference is not in the ideas. It is in the scale.


Further reading

  • Functions as Black-Box Abstractions — Why a problem breaks into pieces that each do one identifiable job, the decomposition you used to grow the game
  • Extracting State Logic into a Reducer — The (state, action) -> next state shape of processGuess, and why frameworks formalize the same pattern
  • Boundaries — Gary Bernhardt on functional core, imperative shell: the logic-pure / effects-at-the-edge split that kept playTurn out of the DOM
  • Canon TDD — Kent Beck's definition of test-driven development: list the behaviors, drive one at a time, refactor
  • What is Mutation Testing? — The idea behind the platform's test check: a test is only worth keeping if it would fail when the code is wrong
  • Catalog of Refactorings — Martin Fowler's named moves (Extract Function, Rename Variable) for changing structure without changing behavior