The Codex / Projects / Hangman / JavaScript
Tier 1 — Beginner

Hangman

Classic word-guessing game — Set for guessed letters, string masking, and game state as a plain object.

🐍 Python 🟨 JavaScript

1. The Problem

Build the classic Hangman game: pick a secret word, show blanks for each letter, let the player guess one letter at a time, track wrong guesses, and end when they win or run out of attempts.

Why this project? Set for tracking guessed letters (automatic deduplication), string masking with map(), game state as a plain object, and the win/lose conditions expressed as clean boolean checks.

2. How to Think About This

Store guessed letters in a Set — no duplicates, O(1) lookup. The display is word.split("").map(l => guessed.has(l) ? l : "_").join(" "). Win = every letter in the word is in the Set. Lose = wrong guess count reaches max (e.g. 6).

3. The Build

hangman.mjs
import * as readline from "readline/promises";
import { stdin, stdout } from "process";

export const WORDS = ["javascript","function","closure","promise","iterator","generator","prototype","destructuring"];

export function createGame(word = WORDS[Math.floor(Math.random() * WORDS.length)]) {
  return { word: word.toLowerCase(), guessed: new Set(), maxWrong: 6 };
}

export function getMasked(game) {
  return game.word.split("").map(l => game.guessed.has(l) ? l : "_").join(" ");
}

export function getWrongGuesses(game) {
  return [...game.guessed].filter(l => !game.word.includes(l));
}

export function isWon(game)  { return game.word.split("").every(l => game.guessed.has(l)); }
export function isLost(game) { return getWrongGuesses(game).length >= game.maxWrong; }
export function isDone(game) { return isWon(game) || isLost(game); }

export function guess(game, letter) {
  const l = letter.toLowerCase().trim();
  if (!/^[a-z]$/.test(l)) return { valid: false, reason: "Enter a single letter a–z" };
  if (game.guessed.has(l)) return { valid: false, reason: `Already guessed: ${l}` };
  game.guessed.add(l);
  return { valid: true, hit: game.word.includes(l) };
}

// CLI
const rl = readline.createInterface({ input: stdin, output: stdout });
const game = createGame();

while (!isDone(game)) {
  const wrong = getWrongGuesses(game);
  console.log(`
  ${getMasked(game)}`);
  console.log(`  Wrong (${wrong.length}/${game.maxWrong}): ${wrong.join(" ") || "none"}`);
  const input = await rl.question("  Guess a letter: ");
  const result = guess(game, input);
  if (!result.valid) { console.log(`  ${result.reason}`); continue; }
  console.log(result.hit ? `  ✓ Yes! "${input}" is in the word.` : `  ✗ Nope.`);
}

rl.close();
console.log(isWon(game) ? `
🎉 You won! The word was: ${game.word}` : `
💀 Game over! The word was: ${game.word}`);

4. Test & Prove

import { createGame, getMasked, getWrongGuesses, isWon, isLost, guess } from "./hangman.mjs";
let p=0,f=0;
function test(d,fn){try{fn();console.log("  ✓",d);p++;}catch(e){console.log("  ✗",d,"—",e.message);f++;}}
const g = createGame("cat");
test("masked at start",       () => { if(getMasked(g)!=="_ _ _") throw new Error(getMasked(g)); });
test("correct guess reveals", () => { guess(g,"c"); if(!getMasked(g).startsWith("c")) throw new Error(); });
test("wrong guess tracked",   () => { guess(g,"z"); if(getWrongGuesses(g).length!==1) throw new Error(); });
test("duplicate ignored",     () => { guess(g,"z"); if(getWrongGuesses(g).length!==1) throw new Error(); });
test("win condition",         () => { guess(g,"a"); guess(g,"t"); if(!isWon(g)) throw new Error(); });
const g2 = createGame("hi");
test("lose condition",        () => { ["a","b","c","d","e","f"].forEach(l=>guess(g2,l)); if(!isLost(g2)) throw new Error(); });
test("invalid char rejected", () => { const r=guess(g2,"1"); if(r.valid) throw new Error(); });
console.log(`
${p} passed, ${f} failed`);

5. The Interface

  _ _ _ _ _ _ _ _ _ _ _
  Wrong (0/6): none
  Guess a letter: j
  ✓ Yes! "j" is in the word.

  j _ _ _ _ _ _ _ _ _ _
  Wrong (0/6): none
  Guess a letter: q
  ✗ Nope.

6. Run It

node hangman.mjs
node hangman.test.mjs

🎯 Try this next

  1. Add ASCII art for the hangman that grows with each wrong guess
  2. Load words from a text file, one per line — pick a random one
  3. Add a hint system — reveal a letter for free once per game
  4. Build a browser version with click-to-guess letter buttons

What you practised

Set for tracking unique guesses with O(1) lookup, .map() + .every() for the masked display and win check, game state as a plain object (not a class), and separating pure game logic from I/O.

Reference: Set & Map · Arrays · Strings

Try It Yourself