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\$