Newer
Older
express-blog-posts / html / word-guesser / scripts / script.js
// -- Determines if a letter is in a list of letters
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(
    gameFormId = "gameForm",
    guessesId = "guesses",
    guessCountId = "guess_count",
    guessLetterId = "guess_letter",
    guessButtonId = "guess_button",
    currentWordId = "current_word",
    notificationsId = "notifications",
  ) {
    this.gameForm = document.getElementById(gameFormId);
    this.guesses = document.getElementById(guessesId);
    this.guessCount = document.getElementById(guessCountId);
    this.guessButton = document.getElementById(guessButtonId);
    this.guessLetter = document.getElementById(guessLetterId);
    this.currentWord = document.getElementById(currentWordId);
    this.notifications = document.getElementById(notificationsId);
  }
}
function reset(fields) {
  fields.guessButton.value = "Guess";
  fields.notifications.innerText = "";
  fields.guessCount.value = "0";
  fields.guessLetter.value = "";
  fields.guesses.replaceChildren();
  fields.currentWord.replaceChildren();
}
class Game {
  constructor(wordChoices, fields) {
    Object.assign(this, fields);
    this.wordChoices = wordChoices;
    this.newGame();

    // Use onclick to prevent stacking event listeners across multiple test initializations
    const eventHandler = (event) => {
      if (this.game.gameOver()) {
        this.guessButton.value = "Guess";
        this.game = new WordGuesser(this.wordChoices);
        reset(this);
      } else {
        const messages = [
          "Guess",
          "Send it",
          "Feeling lucky?",
          "Is that it?",
          "Final answer?",
          "Spin the wheel",
          "Don't fail now",
          "Roll the dice",
          "Last chance",
          "Click for fate",
          "Make it count",
          "Sure about that?",
          'git commit -m "hope"',
          "Compile & Pray",
          "Deploy to Prod",
          "Test My Patience",
          "Confirm Mediocrity",
          "Submit to the Abyss",
          "Execute Bad Logic",
          "Break the Build",
          "Force Push",
          "Trigger Segfault",
          "Manifest Failure",
          "Commit Career Suicide",
          "Push to Origin",
          "Yank & Put",
          "Do your worst",
          "Click for Fate",
          "Input Error",
          "Roll for Initiative",
          "Terminate Process",
        ];
        this.guessButton.value = randomMessage(messages);
        this.handleGuessedLetter(event);
      }
    };
    this.gameForm.onsubmit = eventHandler;
    this.guessButton.onclick = eventHandler;
  }
  reset() {
    reset(this);
  }
  newGame() {
    this.reset();
    this.game = new WordGuesser(this.wordChoices);
  }
  // -- Getters
  getGuess() {
    return this.guessLetter.value;
  }
  // -- Setters
  clearGuess() {
    this.guessLetter.value = "";
  }
  updateGuessCount(count) {
    this.guessCount.value = count;
  }
  updateGuesses(chars) {
    const letters = updateLetters(chars);
    this.guesses.replaceChildren(...letters.childNodes);
  }
  invalidGuessError(letter) {
    const messages = [
      `'${letter}'? I asked for a letter, not a cry for help.`,
      `System failure: User attempted to use '${letter}' as a letter. Re-evaluating human intelligence...`,
      `Nice try, but '${letter}' isn't in the alphabet. Did you skip kindergarten?`,
      `Unicode is vast, but '${letter}' is still the wrong choice for a word game.`,
      `If '${letter}' is a letter, then I'm a toaster. (Spoilers: It's not, and I'm not).`,
      `Keyboard mashing is not a valid strategy. '${letter}' rejected.`,
      `I'm an AI, not a cryptographer. Speak alphabet, not '${letter}'.`,
      `Are you testing my regex? '${letter}' failed. Use a letter.`,
      `My source code is more readable than your input of '${letter}'.`,
      `Error: User sent '${letter}'. Expected: A-Z. Found: Disappointment.`,
      `'${letter}'? Even my legacy drivers know that's not a character.`,
      `You have 26 options. '${letter}' is not one of them.`,
      `Learn how to spell '${letter}' this isn't even a letter`,
      `Ok master hacker you are going to have to try harder than '${letter}'`,
      `Is the letter '${letter}' some kind of new hacking sequence I don't know about?`,
      `Try again, but this time, try harder because '${letter}' isn't even a letter!`,
      `Error 404: Brain not found. '${letter}' is not a character.`,
      `Inputting '${letter}'? Bold move. Wrong, but bold.`,
    ];
    const message = randomMessage(messages);
    this.updateNotifications("🚫", message);
  }
  duplicateGoodGuessError(letter) {
    const messages = [
      `You already tried '${letter}', do you have a short memory?`,
      `Deja vu? You already guessed '${letter}'. Try to keep up.`,
      `Memory like a goldfish? You've already used '${letter}'.`,
      `Stop harassing the letter '${letter}'. It's already given you all it has.`,
      `You're repeating yourself. '${letter}' is already so five minutes ago.`,
      `Did you expect a different result? '${letter}' is already on the board.`,
      `Error: Redundancy detected. '${letter}' has already been processed by your (apparent) brain.`,
      `You found '${letter}' once. Finding it again doesn't double your points.`,
      `Yes, '${letter}' is still there. It hasn't moved since the last time you checked.`,
      `Is this a glitch in your matrix? You've already used '${letter}'.`,
      `Congratulations, you've discovered '${letter}' for the second time. Still correct, still redundant.`,
      `I admire your consistency, but '${letter}' is already doing its job on the board.`,
      `Obsessing over '${letter}' won't reveal the other letters. Move on.`,
      `Checking if '${letter}' is still correct? Spoiler: It is. Also spoiler: You already guessed it.`,
      `You're stuck in a loop. '${letter}' is already accounted for.`,
      `If I had a nickel for every time you guessed '${letter}', I'd have two nickels. It's still weird that it happened twice.`,
      `Error: Duplicate success detected. You're trying to relitigate the past with '${letter}'.`,
      `'${letter}' is already visible. Perhaps an eye exam is in order?`,
      `Are you trying to give '${letter}' some company? It is already on the board.`,
    ];
    const message = randomMessage(messages);
    this.updateNotifications("💎", message);
  }
  duplicateBadGuessError(letter) {
    const messages = [
      `You already tried '${letter}', do you have a short memory?`,
      `Deja vu? You already guessed '${letter}'. Try to keep up.`,
      `Memory like a goldfish? You've already used '${letter}'.`,
      `Stop harassing the letter '${letter}'. It's already given you all it has.`,
      `You're repeating yourself. '${letter}' is already so five minutes ago.`,
      `Did you expect a different result? '${letter}' is already on the board.`,
      `Error: Redundancy detected. '${letter}' has already been processed by your (apparent) brain.`,
      `'${letter}' was wrong five minutes ago, and news flash: it's still wrong now.`,
      `Did you expect a different result? That's the textbook definition of insanity.`,
      `I've already cached the fact that '${letter}' isn't here. Try clearing your own internal cache.`,
      `Your short-term memory seems to have a memory leak. You already failed with '${letter}'.`,
      `Congratulations on being consistently wrong. '${letter}' is still not in the word.`,
      `I'm not a mirror, but you're definitely repeating yourself with that '${letter}' guess.`,
      `Are you trying to lose on purpose? Because guessing '${letter}' twice is a great start.`,
      `'${letter}' didn't work the first time. It didn't work the second time. I sense a pattern.`,
      `My algorithms are bored. Even I remember you already guessed '${letter}'.`,
      `Error 409: Conflict. User is fighting with their own history regarding the letter '${letter}'.`,
      `If failure was a sport, you'd be a pro for guessing '${letter}' again.`,
      `I'd offer you a hint, but I'm worried you'd just guess '${letter}' a third time.`,
    ];
    const message = randomMessage(messages);
    this.updateNotifications("🔁", message);
  }
  badGuessError(letter) {
    const messages = [
      `'${letter}'? I've seen better logic in a circular dependency.`,
      `Searching for '${letter}'... 404: Character not found in this string.`,
      `Statistically, '${letter}' was a disaster. Mathematically, it's a zero.`,
      `My logic gates are closing on that guess. '${letter}' is a hard pass.`,
      `If guessing incorrectly were a career, you'd be the CEO of '${letter}'.`,
      `I've run 10,000 simulations. In zero of them was '${letter}' the right choice.`,
      `Error: Probability of '${letter}' being in this word is exactly 0.00%.`,
      `Is that your final answer? Because '${letter}' is definitively, objectively wrong.`,
      `I'd explain why '${letter}' failed, but I don't have the stack space for that conversation.`,
      `01001110 01101111. That's 'No' in binary. '${letter}' is a total failure.`,
      `Even a randomized brute-force script would have avoided '${letter}'.`,
      `Your guess of '${letter}' has been logged as a 'What not to do' example.`,
      `Statistically, '${letter}' was a bold choice. Factually, it was a wrong one.`,
      `I've seen better guesses from a keyboard-walking cat than '${letter}'.`,
      `The letter '${letter}' doesn't live here. Try another neighborhood.`,
      `I'd explain why '${letter}' is wrong, but I don't have the bandwidth for that level of remedial training.`,
      `Incorrect. '${letter}'? Really? That's your best effort?`,
      `You chose '${letter}', who taught you how to spell?`,
      `If you type another letter like '${letter}' you may have to go back to grammar school`,
      `Nope. '${letter}' is definitely not it. Try using your eyes.`,
      `Incorrect. '${letter}'? Really? That's your best effort?`,
      `My artificial intelligence is offended by that guess: '${letter}'.`,
    ];
    const message = randomMessage(messages);
    this.updateNotifications("❌", message);
  }
  goodGuess() {
    const messages = [
      "Stop cheating, because now I have to congratulate you",
      "Wow, a broken clock is right twice a day after all.",
      "Correct. Don't let it go to your head.",
      "You actually found one. I'm as surprised as you are.",
      "Did you Google the answer or just get lucky?",
      "Correct! Your IQ just jumped into the double digits.",
    ];
    const message = randomMessage(messages);
    this.updateNotifications("✅", message);
  }
  gameOver(letter) {
    const buttonMessages = ["Start a new Game"];
    this.guessButton.value = randomMessage(buttonMessages);
    const messages = [
      "The games over. Did you think you were going to win a prize or something?!",
      "Wanna play another round you cheater??",
      "You won. I hope you're happy. My CPU is crying.",
      "Game Over. You finally finished a simple task. Proud of you.",
      "You actually got it. Go tell your mom, she'll be thrilled.",
      "The word was revealed despite your best efforts to fail.",
      "A match. I’ve updated your status from 'Hopeless' to 'Lucky'.",
      "Correct! I'll have to re-calibrate my low expectations for you.",
      "Warning: User intelligence spike detected. That letter is actually correct.",
      "Even a script monkey hits the right key eventually. You found one.",
      "Correct guess. I assume that was just a lucky bit-flip in your brain.",
      "My 'disappointment' variable has been decremented. That's a match.",
      "You actually found a letter. My CPU is experiencing a rare moment of respect.",
      "Correct! You’ve successfully navigated a single logic branch. Don't get cocky.",
      "A hit. I’m starting to suspect you might actually be semi-sentient.",
      "Logic check: Passed. You found a valid character. The simulation continues.",
      "Correct. I'll refrain from insulting your intelligence for exactly five seconds.",
      "That's a match. Maybe there's hope for your carbon-based hardware after all.",
    ];
    const message = randomMessage(messages);
    this.updateNotifications("🏆", 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;
    try {
      const goodGuess = game.guessLetter(letter);
      this.goodGuess(letter);
    } catch (e) {
      if (e instanceof BadGuessError) {
        this.badGuessError(letter);
      } else if (e instanceof DuplicateBadGuessError) {
        this.duplicateBadGuessError(letter);
      } else if (e instanceof DuplicateGoodGuessError) {
        this.duplicateGoodGuessError(letter);
      } else if (e instanceof InvalidGuessError) {
        this.invalidGuessError(letter);
      } else {
        this.updateNotifications("⚠️", e.message);
      }
    } finally {
      const currentWord = game.getCurrentWord();
      this.updateCurrentWord(currentWord);
      this.updateGuesses(game.getGuesses());
      this.updateGuessCount(game.guessCount());
      this.clearGuess();
      if (game.gameOver()) {
        this.gameOver();
      }
    }
  }
}
document.addEventListener("DOMContentLoaded", (_) => {
  const wordChoices = [
    "bannana",
    "orange",
    "algorithm",
    "bamboozle",
    "binary",
    "bungalow",
    "clandestine",
    "compiler",
    "computer",
    "cryptography",
    "discombobulate",
    "euphoria",
    "flabbergasted",
    "flapjack",
    "gazelle",
    "gobbledygook",
    "hullabaloo",
    "jazziness",
    "juxtapose",
    "kerfuffle",
    "labyrinth",
    "lollygag",
    "majestic",
    "malarkey",
    "mnemonic",
    "obfuscate",
    "persnickety",
    "pharaoh",
    "pizazz",
    "pointer",
    "poppycock",
    "programming",
    "quirky",
    "quizzical",
    "radiance",
    "recursion",
    "rhythm",
    "sapphire",
    "shenanigans",
    "skedaddle",
    "sphinx",
    "syntax",
    "syzygy",
    "umbrella",
    "vacuum",
    "velocity",
    "wizardry",
    "zephyr",
  ];
  const fields = new GameFields();
  new Game(wordChoices, fields);
});

function randomMessage(messages) {
  const randomChoice = Math.floor(Math.random() * messages.length);
  return messages[randomChoice];
}