In this article17
- Step 1: pick a secret word
- Template strings
- A random item from a list
- Step 2: from a word to letters
- split
- join
- Filling an array
- Step 3: guess a letter
- The classic for loop
- Same position, not push
- Step 4: attempts, winning and losing
- A counter that goes down
- Functions that answer yes or no
- Step 5: guess the whole word
- Step 6: the game loop
- while
- Step 7: the game on a page
Time for a real project. In this lesson you'll build Hangman, the word-guessing game, one small step at a time. Every step uses something you've already learned (random numbers, booleans, if, functions, loops, arrays) and adds one new idea. At the end you'll have a game you can play on a web page.
The rules: the computer picks a secret word. The player sees one blank per letter and guesses letters. A right letter shows up in every place it appears; a wrong letter costs one of 5 attempts. The player can also guess the whole word at once. Find the word before the attempts run out and you win; otherwise you lose.
Each step has a short lesson, an exercise and an answer you can open. Try the exercise yourself first.
Step 1: pick a secret word
Template strings
So far you've joined text with +. A template string is often easier: write it between backticks ` ` and put any value inside ${ }`:
const name = "Sara";
const age = 16;
console.log("Hi " + name + ", you are " + age); // the old way
console.log(`Hi ${name}, you are ${age}`); // a template stringA random item from a list
You met this in the random numbers lesson: Math.floor(Math.random() * list.length) is a random position from 0 to the last one, so list[position] is a random item.
const colours = ["red", "green", "blue"];
const index = Math.floor(Math.random() * colours.length);
console.log(`Position ${index}: ${colours[index]}`);Show the answer
const possibleWords = ["Camel", "Candy", "Colombia", "Andalusia", "Naruto"];
function pickWord(words) {
const index = Math.floor(Math.random() * words.length);
return words[index].toLowerCase();
}
console.log(pickWord(possibleWords));
console.log(pickWord(possibleWords));Small letters from the start mean you never have to worry about "C" and "c" being different later.
Step 2: from a word to letters
split
A string can be turned into an array of its letters with split(""). Each letter gets its own position, starting from 0, so you can reach any letter by its number:
const word = "camel";
const letters = word.split("");
console.log(letters); // ["c", "a", "m", "e", "l"]
console.log(letters.length); // 5
console.log(letters[2]); // "m"join
join does the opposite: it glues an array back into one string, with whatever you put between the brackets in the gaps:
console.log(["c", "a", "t"].join("")); // "cat"
console.log(["c", "a", "t"].join(" ")); // "c a t"Filling an array
Array(5).fill("_") makes an array of 5 items, all "_". It's how you get one blank per letter:
const blanks = Array(5).fill("_");
console.log(blanks.join(" ")); // _ _ _ _ _Show the answer
function makeBlanks(word) {
return Array(word.length).fill("_");
}
const shown = makeBlanks("colombia");
console.log(shown.join(" ")); // _ _ _ _ _ _ _ _shown is what the player sees. As they guess right letters, you'll swap blanks for letters.
Step 3: guess a letter
The classic for loop
for...of gives you each item, but here you also need its position, so you know which blank to fill. The classic for loop counts the positions for you:
const letters = ["c", "o", "l", "o", "m", "b", "i", "a"];
for (let i = 0; i < letters.length; i++) {
console.log(`position ${i}: ${letters[i]}`);
}Read for (let i = 0; i < letters.length; i++) as: start i at 0; keep going while i is less than the length; add 1 to i after each round.
Same position, not push
To reveal a letter, put it in the same position in shown as in the word. push can't do that: it always adds to the end of an array. Writing shown[i] = letter replaces exactly the blank at position i:
const shown = ["_", "_", "_", "_"];
shown[1] = "a";
console.log(shown.join(" ")); // _ a _ _
shown.push("x");
console.log(shown.join(" ")); // _ a _ _ x (added at the end!)Quick check
The word is "colombia" and the player guesses "o". Which positions should be filled?
Why
c=0, o=1, l=2, o=3. A letter can appear more than once, so the loop must check every position and fill each one that matches, not stop at the first.
Show the answer
function revealLetter(word, shown, letter) {
let found = false;
for (let i = 0; i < word.length; i++) {
if (word[i] === letter) {
shown[i] = letter;
found = true;
}
}
return found;
}
const word = "colombia";
const shown = Array(word.length).fill("_");
console.log(revealLetter(word, shown, "o")); // true
console.log(shown.join(" ")); // _ o _ o _ _ _ _
console.log(revealLetter(word, shown, "z")); // falseA string can be read by position too: word[i] is the letter at i, without split. found starts as false and turns true the moment one letter matches.
Step 4: attempts, winning and losing
A counter that goes down
The number of attempts left is a let, because it changes: each wrong letter takes one away with attempts = attempts - 1 (or the short form attempts--):
let attempts = 5;
attempts--;
attempts--;
console.log(`${attempts} attempts left`); // 3 attempts leftFunctions that answer yes or no
A function that returns true or false reads like a question in an if. Name it like a question, too:
const isEven = (n) => n % 2 === 0;
if (isEven(4)) {
console.log("4 is even");
}Show the answer
const isWon = (shown) => !shown.includes("_");
const isLost = (attempts) => attempts <= 0;
console.log(isWon(["c", "a", "t"])); // true
console.log(isWon(["c", "_", "t"])); // false
console.log(isLost(0)); // true
console.log(isLost(3)); // false! flips the answer: no blank left means won. attempts <= 0 is safer than === 0: if a bug ever made it go below zero, the game would still end.
Step 5: guess the whole word
Comparing two whole strings is one ===. Compare them in small letters, and trim spaces the player typed by accident with trim():
const secret = "naruto";
const guess = " Naruto ";
console.log(guess === secret); // false
console.log(guess.trim().toLowerCase() === secret); // trueShow the answer
function guessWord(secret, guess) {
return guess.trim().toLowerCase() === secret;
}
console.log(guessWord("naruto", "Naruto")); // true
console.log(guessWord("naruto", "nar")); // false, no crash
console.log(guessWord("naruto", " NARUTO ")); // trueStep 6: the game loop
while
A while loop repeats its code as long as its condition is true. Something inside the loop must eventually make the condition false, or it never stops:
let attempts = 3;
while (attempts > 0) {
console.log(`Attempts left: ${attempts}`);
attempts--;
}
console.log("Out of attempts");In your first version of Hangman, a while (gameIsRunning) loop asks with prompt again and again. This version runs in VS Code with a browser (prompt opens a little box that asks the player to type):
const words = ["camel", "candy", "colombia", "andalusia", "naruto"];
const secret = words[Math.floor(Math.random() * words.length)];
const shown = Array(secret.length).fill("_");
let attempts = 5;
let gameIsRunning = true;
while (gameIsRunning) {
const input = prompt(`${shown.join(" ")}\nA letter or the word:`);
if (input === null) break; // the player pressed Cancel
const guess = input.trim().toLowerCase();
if (guess === secret) {
alert("You win!");
gameIsRunning = false;
} else if (guess.length === 1 && secret.includes(guess)) {
for (let i = 0; i < secret.length; i++) {
if (secret[i] === guess) shown[i] = guess;
}
if (!shown.includes("_")) {
alert(`You win! The word was ${secret}`);
gameIsRunning = false;
}
} else {
attempts--;
if (attempts === 0) {
alert(`You lose. The word was ${secret}`);
gameIsRunning = false;
}
}
}It works, but prompt freezes the whole page while it waits, and nothing else on the page can happen. Real web games don't loop: they wait for the player to click, run one round, and update the page. That's the version you'll build next.
Step 7: the game on a page
The page version uses everything above. One function, play(guess), runs one round: it checks the guess, updates shown or attempts, then calls draw() to show the new state. The button calls play each time it's clicked, so there's no loop and the page never freezes. Press Run and play:
<!DOCTYPE html>
<html lang="en">
<body style="padding: 16px; font-family: sans-serif">
<p id="word" style="font: 700 32px monospace; letter-spacing: 6px"></p>
<p id="info"></p>
<input id="guess" maxlength="12" placeholder="A letter or the word" />
<button id="go">Guess</button>
<button id="again">New game</button>
<p id="message" style="font-weight: bold"></p>
<script>
const words = ["camel", "candy", "colombia", "andalusia", "naruto"];
let secret, shown, attempts, used, gameIsRunning;
function newGame() {
secret = words[Math.floor(Math.random() * words.length)];
shown = Array(secret.length).fill("_");
attempts = 5;
used = [];
gameIsRunning = true;
draw("Guess a letter!");
}
function draw(message) {
document.getElementById("word").textContent = shown.join(" ");
document.getElementById("info").textContent =
`Attempts left: ${attempts} · Tried: ${used.join(", ")}`;
document.getElementById("message").textContent = message;
}
function play(input) {
const guess = input.trim().toLowerCase();
if (!gameIsRunning || guess === "") return;
if (guess.length > 1) {
if (guess === secret) {
shown = secret.split("");
gameIsRunning = false;
return draw("You win!");
}
attempts--;
} else if (used.includes(guess)) {
return draw(`You already tried ${guess}.`);
} else {
used.push(guess);
let found = false;
for (let i = 0; i < secret.length; i++) {
if (secret[i] === guess) {
shown[i] = guess;
found = true;
}
}
if (!found) attempts--;
}
if (!shown.includes("_")) {
gameIsRunning = false;
draw("You win!");
} else if (attempts <= 0) {
gameIsRunning = false;
shown = secret.split("");
draw("You lose. This was the word.");
} else {
draw(guess.length > 1 ? "Not that word." : "Next guess!");
}
}
const box = document.getElementById("guess");
document.getElementById("go").addEventListener("click", () => {
play(box.value);
box.value = "";
box.focus();
});
document.getElementById("again").addEventListener("click", newGame);
newGame();
</script>
</body>
</html>A few things to notice:
newGame()sets everything back to the start, so "New game" is one function call.usedremembers tried letters, so the same wrong letter isn't counted twice. Herepushis right: the order of tries doesn't matter.return draw(…)shows a message and stops the round in one line.
Show the answer
Add a button next to the others:
<button id="hint">Hint</button>and this before newGame(); at the end of the script:
document.getElementById("hint").addEventListener("click", () => {
if (!gameIsRunning || attempts <= 1) return;
for (let i = 0; i < secret.length; i++) {
if (shown[i] === "_") {
attempts--;
play(secret[i]);
return;
}
}
});return inside the loop stops it after the first hidden letter. Since that letter is in the word, play reveals it without taking another attempt.