Practice 4 - Hangman (State-Driven UI)
Introduction
In this practice, we build a small but complete game: Hangman.
The goal is not only the game itself, but also a key frontend mindset:
- keep your state in JavaScript variables,
- render the DOM from that state,
- and re-render after every user action.
This is the same pattern used in modern frameworks (React, Vue, Svelte), only here we do it manually in vanilla JavaScript.
By the end of this session you will be able to:
- Model game state with variables (
targetWord,guessedLetters,gameOver) - Build a simple UI lifecycle (
start -> render -> user input -> render) - Use event delegation for dynamic button lists
- Check win conditions with array methods (
every,includes) - Understand why state-driven rendering reduces bugs
Project Structure
p4/
├── index.html # Game layout (controls + hidden game area)
└── main.js # Game logic and rendering
HTML Overview
index.html contains two main sections:
#controls(visible at first):- start button
- result message
#game(hidden initially):- table row for the masked word
- alphabet buttons container
- SVG placeholder for drawing the hangman later
<h1>Hangman</h1>
<div id="controls">
<button id="start">Start new game</button>
<div id="result"></div>
</div>
<div id="game" style="display: none">
<table>
<tr></tr>
</table>
<div id="buttons"></div>
<svg width="200px" height="200px" stroke="black"></svg>
</div>
The SVG is currently just a placeholder. This is great design: we can add losing logic and drawing phases later without changing the base page structure.
1. Game State
At the top of main.js, we store the whole game in a few variables:
const wordList = ["javascript", "programming"];
let targetWord;
let guessedLetters;
let gameOver = false;
Why this matters
targetWord: the selected word for the current roundguessedLetters: all letters guessed so fargameOver: blocks input after finishing
This is your source of truth. The DOM should be treated as a visual output, not as the real data model.
2. Starting a New Game
The startGame() function initializes state and toggles visibility:
function startGame() {
targetWord = wordList[Math.floor(Math.random() * wordList.length)];
guessedLetters = [];
gameOver = false;
document.querySelector("#controls").style.display = "none";
document.querySelector("#game").style.display = "block";
renderState();
}
document.querySelector("#start").addEventListener("click", startGame);
Key idea
startGame() does not manually paint each small UI part. It just resets state and calls renderState(). That keeps responsibilities clean.
3. Render Lifecycle
The central function:
function renderState() {
renderButtons();
renderWord();
checkEndGame();
}
Think of it as a mini render pipeline:
- draw alphabet buttons,
- draw current word mask,
- evaluate if the game has ended.
Whenever state changes, call renderState().
A reliable rule: if a user action changes data, call a single render entry point right after.
4. Rendering the Alphabet Buttons
function renderButtons() {
const ABC = "ABCDEFGHIJKLMNOPQRSTVUWXYZ";
document.querySelector("#buttons").innerHTML = ABC.split("")
.map((letter) => {
return `<button id=${letter}
${guessedLetters.includes(letter.toUpperCase()) ? "disabled" : ""}
>${letter}</button>`;
})
.join("");
}
What is happening
ABC.split("")creates an array of characters.map(...)converts each letter to<button>...</button>HTML.- Already-guessed letters are rendered with
disabled. join("")merges everything into one HTML string.
This is the same list-rendering pattern you used in Practice 3.
Small quality note
The string "ABCDEFGHIJKLMNOPQRSTVUWXYZ" has an unusual order around U/V/W. For now this does not break the architecture, but you may want to replace it with a correctly ordered alphabet constant later.
5. Rendering the Word Mask
function renderWord() {
const lines = document.querySelector("tr");
lines.innerHTML = targetWord.split("")
.map((char) => {
return `<td>${guessedLetters.includes(char.toUpperCase()) ? char : '_'}</td>`;
})
.join("");
}
Logic
For each character in targetWord:
- if guessed, show the real letter,
- otherwise show
_.
This gives instant visual feedback after each click.
6. Handling Letter Clicks (Event Delegation)
function handleButtonClick(e) {
if (!gameOver && e.target.matches("button")) {
guessedLetters.push(e.target.innerText.toUpperCase());
renderState();
}
}
document.querySelector("#buttons").addEventListener("click", handleButtonClick);
Why delegation is perfect here
The letter buttons are recreated with innerHTML on every render. If you attached listeners to each button individually, they would be lost after rerender.
Instead, one listener on #buttons catches all button clicks through event bubbling.
7. Win Detection
function checkEndGame() {
if (targetWord.toUpperCase().split("").every(letter => guessedLetters.includes(letter))) {
document.querySelector("#result").innerText = `Congratulations, you have guessed the word!`;
document.querySelector("#controls").style.display = "block";
gameOver = true;
}
}
Why every(...)
every(...) returns true only when all letters satisfy the condition.
Condition here: each letter in targetWord must exist in guessedLetters.
When true:
- show success message,
- show controls again,
- lock interactions with
gameOver = true.
Data Flow Summary
Start button click
-> startGame()
-> state reset (targetWord, guessedLetters, gameOver)
-> renderState()
Letter click
-> handleButtonClick()
-> guessedLetters update
-> renderState()
-> checkEndGame()
This is the complete loop of an interactive UI.
Suggested Extensions
- Add lose condition:
- introduce
wrongGuesses - stop game after N wrong letters
- introduce
- Draw the hangman in the SVG step by step.
- Add a Play again button that resets both state and UI.
- Show used letters in a dedicated area (e.g.,
Used: A, C, T). - Improve accessibility:
- add
aria-labelto controls, - ensure focus styles are visible.
- add
Common Pitfalls
- Forgetting to reset
guessedLetterswhen starting a new game - Adding duplicate letters to
guessedLetters(disabled buttons usually prevent this) - Reading game state from DOM text instead of JS variables
- Updating one part of UI but forgetting to rerender the others
State first, UI second. If your state is correct and renderState() is consistent, the UI will stay correct too.
Takeaway
Practice 4 is your first full mini-application with a clear architecture.
The most important lesson is not the game rules. It is the pattern:
- keep state in JavaScript,
- render from state,
- rerender after actions,
- keep event handling simple with delegation.
If you master this pattern now, larger frontend projects will feel much more manageable later.