I recently developed a hangman game with HTML, CSS and JavaScript and I would like to get your feedback and tips to improve it. The goal of the game is to guess a secret word before 6 incorrect guesses are completed.
I would like feedback on how I can improve the readability, efficiency and overall structure of the code. I am also open to suggestions on coding practices and standards I should follow.
Game screenshot:
Code:
const BLANKS_TAG = document.getElementById("current-word"); const VIRTUAL_KEYS = document.getElementById("virtual-keys"); const HINT_BUTTON = document.getElementById("hint-button"); const NEW_WORD_BUTTON = document.getElementById("new-word-button"); const HANGMAN_IMAGE = document.getElementById("hangman-image"); const MAX_TRIALS = 6; let trials = MAX_TRIALS; let hintUsed; let blanks; let words; let secretWord; let secretWordArray; /** * Updates the hangman image based on the number of attempts remaining. */ function updateHangmanImage() { HANGMAN_IMAGE.src = `img/${MAX_TRIALS - trials}.png`; } /** * Updates the blanks with the correctly guessed letter and marks the * corresponding span element as correct. * @param {string} LETTER - The letter to guess. * @returns {boolean} Returns true if it's a good guess, false otherwise. */ function guessAndUpdateBlanks(LETTER) { const SPANS = BLANKS_TAG.querySelectorAll("span"); let isGoodGuess = false; let lastCorrectSpan = null; for (const [I, CHAR] of secretWordArray.entries()) { if (CHAR === LETTER) { isGoodGuess = true; blanks[I] = LETTER; lastCorrectSpan = SPANS[I]; } } if (lastCorrectSpan) lastCorrectSpan.classList.remove("correct"); if (isGoodGuess) return true; else return false; } /** * Replaces the guessed letters in the string of blanks and updates the hangman image if the guess is incorrect. * @param {boolean} IS_GOOD_GUESS - Indicates if the guess is correct. * @param {string} LETTER - The guessed letter. */ function replaceGuessedLetters(IS_GOOD_GUESS, LETTER) { if (IS_GOOD_GUESS) { const BLANKS_STRING = blanks.join(" "); const UPDATED_BLANKS_STRING = BLANKS_STRING.replace( new RegExp(LETTER, "gi"), `<span class="correct">${LETTER}</span>` ); BLANKS_TAG.innerHTML = UPDATED_BLANKS_STRING; } else { trials--; updateHangmanImage(); } } /** * Provides a hint by disabling a number of letters that are not in the secret word. */ function hint() { HINT_BUTTON.style.visibility = "hidden"; hintUsed = true; const VIRTUAL_KEYS_CHILDREN = Array.from( VIRTUAL_KEYS.querySelectorAll(".btn:not(.disabled)") ); const MAX_LETTERS_TO_SHOW = Math.floor(Math.random() * 6) + 1; const INDEXES = []; while (INDEXES.length < MAX_LETTERS_TO_SHOW) { const RANDOM_INDEX = Math.floor( Math.random() * VIRTUAL_KEYS_CHILDREN.length ); const BUTTON = VIRTUAL_KEYS_CHILDREN[RANDOM_INDEX]; const LETTER = BUTTON.getAttribute("data-value"); if (!INDEXES.includes(RANDOM_INDEX) && !secretWordArray.includes(LETTER)) { BUTTON.classList.add("disabled"); INDEXES.push(RANDOM_INDEX); } } } /** * Checks the game result and displays the secret word accordingly. */ function checkGameResult() { const BLANKS_STRING = blanks.join(""); BLANKS_TAG.textContent = secretWord; NEW_WORD_BUTTON.style.visibility = "visible"; if (BLANKS_STRING === secretWord) BLANKS_TAG.classList.add("correct"); else BLANKS_TAG.classList.add("incorrect"); } /** * Initialize the game and choose a secret word at random. */ function initializeGame() { NEW_WORD_BUTTON.style.visibility = "hidden"; BLANKS_TAG.classList.remove("correct", "incorrect"); VIRTUAL_KEYS.querySelectorAll(".btn").forEach((BUTTON) => { BUTTON.classList.remove("disabled"); }); secretWord = words[Math.floor(Math.random() * words.length)]; trials = MAX_TRIALS; blanks = Array(secretWord.length).fill("_"); secretWordArray = secretWord.split(""); hintUsed = false; BLANKS_TAG.textContent = blanks.join(" "); updateHangmanImage(); } /** * Manages the game event when a virtual key is clicked. * @param {Event} event - The click event object. */ function play(event) { if (trials === 0 || !blanks.includes("_")) return; const BUTTON = event.target; const LETTER = BUTTON.getAttribute("data-value"); const IS_GOOD_GUESS = guessAndUpdateBlanks(LETTER); replaceGuessedLetters(IS_GOOD_GUESS, LETTER); BUTTON.classList.add("disabled"); if (trials === 1 && !hintUsed) HINT_BUTTON.style.visibility = "visible"; if (!trials || !blanks.includes("_")) checkGameResult(); } fetch("./words.txt") .then((response) => response.text()) .then((data) => { words = data.split(" "); initializeGame(); }) .catch((error) => { console.error(`Error reading word file: ${error}`); }); document.addEventListener("click", (event) => { const TARGET = event.target; if (TARGET === NEW_WORD_BUTTON) { initializeGame(); } else if (TARGET.parentNode === VIRTUAL_KEYS) { play(event); } else if (TARGET === HINT_BUTTON) { hint(); } });
body { max-width: 100vw; max-height: 100vh; } header { margin-top: 20px; margin-bottom: 20px; } h1 { text-align: center; } .game-stage { min-height: 350px; max-height: 350px; display: flex; align-items: center; justify-content: center; } .game-current-word { min-height: 50px; max-height: 50px; display: flex; justify-content: center; font-size: 22px; font-weight: bold; } .virtual-keyboard { min-height: 80px; max-height: 80px; max-width: 100vw; } .virtual-keys { max-width: 600px; display: flex; justify-content: center; align-items: center; flex-wrap: wrap; margin: 15px auto; } .btn { background: none; border: 0; background-color: #0066cc; box-shadow: 2px 2px 1px #036; color: #f3ffff; cursor: pointer; font: inherit; font-weight: bold; line-height: normal; overflow: visible; width: 35px; height: 35px; border-radius: 5px; display: flex; align-items: center; justify-content: space-evenly; padding: 0 10px; margin: 5px; } .hint-button, .new-word-button { width: 40px; font-size: 12px; visibility: hidden; } .disabled { background-color: #fff; color: #000; pointer-events: none; cursor: not-allowed; } .correct { color: green; font-size: 25px; } .incorrect { color: red; font-size: 25px; }
<!DOCTYPE html> <html lang="en"> <head> <title>Hangman game</title> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" type="text/css" href="assets/normalize.css"> <link rel="stylesheet" type="text/css" href="assets/style.css" /> <script src="assets/index.js" defer></script> </head> <body> <header><h1>Hangman game</h1></header> <section class="game-stage"> <button type="button" class="btn hint-button" id="hint-button">Hint?</button> <img id="hangman-image"/> <button type="button" class="btn new-word-button" id="new-word-button">New Word</button> </section> <section class="game-current-word"> <p id="current-word"></p> </section> <section class="virtual-keyboard"> <div class="virtual-keys" id="virtual-keys"> <button type="button" class="btn" data-value="a">A</button> <button type="button" class="btn" data-value="b">B</button> <button type="button" class="btn" data-value="c">C</button> <button type="button" class="btn" data-value="d">D</button> <button type="button" class="btn" data-value="e">E</button> <button type="button" class="btn" data-value="f">F</button> <button type="button" class="btn" data-value="g">G</button> <button type="button" class="btn" data-value="h">H</button> <button type="button" class="btn" data-value="i">I</button> <button type="button" class="btn" data-value="j">J</button> <button type="button" class="btn" data-value="k">K</button> <button type="button" class="btn" data-value="l">L</button> <button type="button" class="btn" data-value="m">M</button> <button type="button" class="btn" data-value="n">N</button> <button type="button" class="btn" data-value="o">O</button> <button type="button" class="btn" data-value="p">P</button> <button type="button" class="btn" data-value="q">Q</button> <button type="button" class="btn" data-value="r">R</button> <button type="button" class="btn" data-value="s">S</button> <button type="button" class="btn" data-value="t">T</button> <button type="button" class="btn" data-value="u">U</button> <button type="button" class="btn" data-value="v">V</button> <button type="button" class="btn" data-value="w">W</button> <button type="button" class="btn" data-value="x">X</button> <button type="button" class="btn" data-value="y">Y</button> <button type="button" class="btn" data-value="z">Z</button> </div> </section> </body> </html>
Github repository: https://github.com/xSyrax123/hangman
Thank you in advance for your help and constructive comments.
<script type="module">
, and then useimport
andexport
to connect them together. Those three modules could contain either aclass
or just the functions relevant to that module. I think OOP is good for game programming, but the function-based approach is more popular in web development.\$\endgroup\$