function charInChars(char, chars) {
const indexes = charsAt(char, chars);
return indexes !== false;
}
function charsAt(char, chars) {
let indexes = [];
for (let i = 0; i < chars.length; i++) {
if (char == chars[i]) {
indexes.push(i);
}
}
return indexes.length == 0 ? false : indexes;
}
function updateLetters(chars) {
const letters = document.createElement("ul");
// create a DOM element for each letter
chars.forEach((char) => {
const item = document.createElement("li");
item.innerText = char;
letters.appendChild(item);
});
return letters;
}
class InvalidGuessError extends Error {}
class DuplicateGoodGuessError extends Error {}
class DuplicateBadGuessError extends Error {}
class BadGuessError extends Error {}
class WordGuesser {
// -- Constants
actualWord = [];
// -- Data
goodGuesses = [];
badGuesses = [];
lettersLeft = [];
currentWord = [];
constructor(wordChoices) {
this.wordChoices = wordChoices;
this.newGame();
}
// -- Clear the current game data and start a new game
newGame() {
this.badGuesses = [];
this.goodGuesses = [];
this.currentWord = [];
const randomChoice = Math.floor(Math.random() * this.wordChoices.length);
this.actualWord = this.wordChoices[randomChoice].split("");
// -- Create placeholder entries for the currentWord
for (let i = 0; i < this.actualWord.length; i++) {
this.currentWord[i] = "_";
}
}
// -- Determines if a letter is in a list of letters
guessLetter(letter) {
this.validateInput(letter);
// -- Find the match
const badGuessIndex = charInChars(letter, this.badGuesses);
const goodGuessIndex = charInChars(letter, this.goodGuesses);
// -- Check if the letter has been guessed already
if (badGuessIndex !== false) {
throw new DuplicateBadGuessError(
`The letter '${letter}' has already been guessed`,
);
}
if (goodGuessIndex !== false) {
throw new DuplicateGoodGuessError(
`The letter '${letter}' has already been guessed`,
);
}
// -- Determine if the letter exists in the actual word
const letterIndexes = charsAt(letter, this.actualWord);
if (letterIndexes === false) {
// Add the letter to the list of guessesd letters
this.badGuesses.push(letter);
throw new BadGuessError(
`Sorry, the letter ${letter} does not exist in the word`,
);
}
this.goodGuesses.push(letter);
this.updateCurrentWord(letter, letterIndexes);
return letterIndexes;
}
getActualWord() {
return this.actualWord;
}
getCurrentWord() {
return this.currentWord;
}
updateCurrentWord(letter, indexes) {
for (let i = 0; i < indexes.length; i++) {
this.currentWord[indexes[i]] = letter;
}
}
setCurrentWord(currentWord) {
if (!Array.isArray(currentWord)) {
throw Error("The current word is not an array!");
}
this.currentWord = currentWord;
}
getGuesses() {
return this.badGuesses;
}
guessCount() {
return this.badGuesses.length + this.goodGuesses.length;
}
gameOver() {
for (let i = 0; i < this.actualWord.length; i++) {
if (this.currentWord[i] != this.actualWord[i]) return false;
}
return true;
}
validateInput(input) {
const regex = /^[a-zA-Z]$/;
const result = regex.test(input);
if (!result) {
throw new InvalidGuessError(
`You must only guess a single letter. Received: '${input}'`,
);
}
}
}
class GameFields {
constructor(fieldNames = {}) {
const defaults = {
// -- User info
userName: "user_name",
userInfoForm: "user_info",
saveUserButton: "save_user_button",
// -- Game Inputs
alphabetContainer: "alphabet_container",
gameForm: "game_form",
guesses: "guesses",
guessCount: "guess_count",
guessLetter: "guess_letter",
guessButton: "guess_button",
currentWord: "current_word",
notifications: "notifications",
// Game sprites
gameIntro: "game_intro",
gameHowTo: "game-howto",
// -- Scoreboard
scoreboard: "scoreboard",
totalWordsCompleted: "total_words_completed",
totalIncorrectGuesses: "total_incorrect_guesses",
avgGuessesPerWord: "average_guesses_per_word",
avgTimePerWord: "average_time_per_word",
};
// Merge provided names with defaults
const config = { ...defaults, ...fieldNames };
// Automatically assign elements to "this"
Object.keys(config).forEach((key) => {
this[key] = document.getElementById(config[key]);
});
}
}
function reset(fields) {
fields.gameIntro.innerText =
"The game that insults you, whether you win or lose!";
fields.guessButton.value = "Guess";
fields.notifications.innerText = "";
fields.guessCount.value = "0";
// fields.guessLetter.value = "";
fields.guesses.replaceChildren();
fields.currentWord.replaceChildren();
if (fields.gameHowTo) {
fields.gameHowTo.style.display = "block";
}
}
class Scoreboard {
gamesPlayed = 0;
}
class Game {
static scoreboard = new Scoreboard();
static gamesPlayed = 0;
constructor(wordChoices, fields) {
Object.assign(this, fields);
this.wordChoices = wordChoices;
this.newGame();
this.userName.addEventListener("focus", (event) => {
if (event.target.value == "Guest") {
this.userName.value = "";
}
});
this.userName.addEventListener("blur", (event) => {
if (event.target.value.trim() == "") {
this.userName.value = "Guest";
}
});
const userInfoHandler = (event) => {
event.preventDefault();
this.userInfoForm.style.display = "none";
this.gameForm.style.display = "block";
console.log("Game ready");
};
this.userInfoForm.onsubmit = userInfoHandler;
// Use onclick to prevent stacking event listeners across multiple test initializations
const eventHandler = (event) => {
event.preventDefault();
if (this.game.gameOver()) {
// reset(this);
// this.gameIntro.innerText =
// "The game that insults you, whether you win or lose!";
// this.guessButton.value = "Guess";
// this.game = new WordGuesser(this.wordChoices);
this.newGame();
} else {
this.updateGuessButton();
this.handleGuessedLetter(event);
}
};
this.gameForm.onsubmit = eventHandler;
}
async init() {
this.dialogs = await import("./dialogs.js");
}
showScoreboard() {
const overlay = document.getElementById("scoreboard_overlay");
overlay.style.display = "flex";
}
updateGuessButton() {
this.guessButton.value = this.randomMessage(this.dialogs.guessButton);
}
renderAlphabet() {
const container = document.getElementById("alphabet_container");
this.alphabetContainer.innerHTML = ""; // Clear for new game
"abcdefghijklmnopqrstuvwxyz".split("").forEach((char) => {
const btn = document.createElement("button");
btn.type = "button";
btn.innerText = char.toUpperCase();
btn.className = "letter-btn";
// Custom attribute to find this button later by character
btn.dataset.char = char;
btn.onclick = () => {
// 1. Remove selection from previous button
if (this.selectedButton) {
this.selectedButton.classList.remove("selected");
}
this.guessLetter.value = char;
this.selectedButton = btn;
btn.classList.add("selected");
// this.handleGuessedLetter(event);
this.guessLetter;
// this.updateGuessButton();
// btn.disabled = true;
};
container.appendChild(btn);
});
}
reset() {
reset(this);
}
newGame() {
this.reset();
this.game = new WordGuesser(this.wordChoices);
this.renderAlphabet();
}
// -- Getters
getGuess() {
return this.guessLetter.value;
}
getUserName() {
return this.userName.value;
}
// -- Setters
clearGuess() {
this.guessLetter.value = "";
}
updateGuessCount(count) {
this.guessCount.value = count;
}
updateGuesses(chars) {
const letters = updateLetters(chars);
this.guesses.replaceChildren(...letters.childNodes);
}
invalidGuessError() {
const message = this.randomMessage(this.dialogs.invalidGuess);
this.updateNotifications("🚫", message);
}
duplicateGoodGuessError() {
const message = this.randomMessage(this.dialogs.duplicateGoodGuess);
this.updateNotifications("💎", message);
}
duplicateBadGuessError() {
const message = this.randomMessage(this.dialogs.duplicateBadGuess);
this.updateNotifications("🔁", message);
}
badGuessError() {
const message = this.randomMessage(this.dialogs.badGuess);
this.updateNotifications("❌", message);
}
goodGuess() {
const message = this.randomMessage(this.dialogs.goodGuess);
this.updateNotifications("✅", message);
}
gameOver() {
this.guessButton.value = this.randomMessage(
this.dialogs.guessButtonDefaultValue,
);
const message = this.randomMessage(this.dialogs.gameOver);
this.updateNotifications("🏆", message);
Game.gamesPlayed++;
}
changeGameIntro() {
let messages = [];
if (Game.gamesPlayed == 0) {
messages = this.dialogs.gameIntroInitial;
} else if (Game.gamesPlayed >= 1) {
messages = this.dialogs.gameIntro;
}
const message = this.randomMessage(messages);
this.updateGameIntro(message);
}
updateGameIntro(message) {
this.gameIntro.innerText = message;
}
updateNotifications(icon, message) {
this.notifications.innerText = `${icon} ${message}`;
}
updateCurrentWord(chars) {
const letters = updateLetters(chars);
this.currentWord.replaceChildren(...letters.childNodes);
}
// -- Event handlers
handleGuessedLetter(event) {
const letter = this.getGuess();
if (!letter) return;
const game = this.game;
if (game.guessCount() == 0) {
this.changeGameIntro();
if (this.gameHowTo) {
this.gameHowTo.style.display = "none";
}
}
try {
const goodGuess = game.guessLetter(letter);
this.goodGuess();
// Disable the button since it was a correct guess
if (this.selectedButton) {
this.selectedButton.disabled = true;
this.selectedButton.classList.remove("selected");
}
this.selectedButton.btn.disabled = true;
} catch (e) {
if (e instanceof BadGuessError) {
this.badGuessError();
// Disable even on bad guesses to prevent duplicates
if (this.selectedButton) {
this.selectedButton.disabled = true;
this.selectedButton.classList.remove("selected");
}
} else if (e instanceof DuplicateBadGuessError) {
this.duplicateBadGuessError();
} else if (e instanceof DuplicateGoodGuessError) {
this.duplicateGoodGuessError();
} else if (e instanceof InvalidGuessError) {
this.invalidGuessError();
} else {
this.updateNotifications("⚠️", e.message);
}
} finally {
const currentWord = game.getCurrentWord();
this.updateCurrentWord(currentWord);
this.updateGuesses(game.getGuesses());
this.updateGuessCount(game.guessCount());
this.clearGuess();
this.selectedButton = null;
if (game.gameOver()) {
this.gameOver();
}
}
}
randomMessage(messagesFn) {
const messages = messagesFn(this.getGuess(), this.getUserName());
const randomChoice = Math.floor(Math.random() * messages.length);
return messages[randomChoice];
}
}
document.addEventListener("DOMContentLoaded", async (_) => {
const wordChoices = (await import("./wordlist.js")).default;
const fields = new GameFields();
await new Game(wordChoices, fields).init();
});