diff --git a/html/word-guesser/config.yaml b/html/word-guesser/config.yaml
new file mode 100644
index 0000000..1a413f3
--- /dev/null
+++ b/html/word-guesser/config.yaml
@@ -0,0 +1,9 @@
+title: "Word Guesser"
+layout: "main"
+page: "page"
+slug: "word-guesser"
+styles:
+ - "css/styles.css"
+scripts:
+ - "scripts/script.js"
+file: "index.html"
diff --git a/html/word-guesser/css/styles.css b/html/word-guesser/css/styles.css
new file mode 100644
index 0000000..7fb1512
--- /dev/null
+++ b/html/word-guesser/css/styles.css
@@ -0,0 +1,169 @@
+/* Game Container Styling */
+.form_container {
+ background-color: #ffffff;
+ border: 1px solid #dee2e6;
+ border-radius: 8px;
+ padding: 2rem;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
+ margin-top: 1rem;
+}
+
+.game-header h2 {
+ color: #2c3e50;
+ border-bottom: 2px solid #2c3e50;
+ display: inline-block;
+ margin-bottom: 1.5rem;
+ padding-bottom: 0.2rem;
+}
+
+.game-instructions {
+ text-align: center;
+ margin: 1.5rem 0;
+ font-size: 0.95rem;
+ color: #6c757d;
+}
+
+/* Word Board (Wheel of Fortune Style) */
+#current_word {
+ display: flex !important;
+ flex-direction: row !important;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+ list-style: none;
+ padding: 1rem 0;
+ width: 100%;
+ font-weight: bold;
+ letter-spacing: 5px;
+ font-size: 1.5rem;
+}
+
+#current_word li {
+ width: 45px;
+ height: 60px;
+ background-color: #f8f9fa;
+ border: 2px solid #2c3e50;
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 2rem;
+ font-weight: 800;
+ color: #2c3e50;
+ box-shadow: 2px 2px 0px rgba(44, 62, 80, 0.2);
+ text-transform: uppercase;
+}
+.word-display-field {
+ display: block !important; /* Break out of the 2-column grid */
+ text-align: center;
+ padding: 2rem 0;
+ border-bottom: 2px solid #f1f3f5;
+}
+
+/* Form Layout */
+.formField {
+ display: grid;
+ grid-template-columns: 120px 1fr;
+ gap: 20px;
+ align-items: center;
+ padding: 0.75rem 0;
+ border-bottom: 1px solid #f1f3f5;
+}
+
+.formField:last-of-type {
+ border-bottom: none;
+}
+
+.formField label {
+ font-weight: 600;
+ color: #495057;
+ font-size: 0.9rem;
+}
+
+/* Inputs & Buttons */
+#guess_letter {
+ width: 50px;
+ padding: 10px;
+ text-align: center;
+ font-size: 1.2rem;
+ font-weight: bold;
+ border: 2px solid #dee2e6;
+ border-radius: 4px;
+ transition: border-color 0.2s;
+}
+
+#guess_letter:focus {
+ border-color: #2c3e50;
+ outline: none;
+}
+
+#guess_count {
+ width: 60px;
+ background: #e9ecef;
+ border: 1px solid #ced4da;
+ padding: 5px;
+ text-align: center;
+ font-family: monospace;
+ font-weight: bold;
+ border-radius: 4px;
+}
+
+.submitField {
+ padding: 1.5rem 0;
+ text-align: center;
+}
+
+#guess_button {
+ background-color: #2c3e50;
+ color: #ffffff;
+ border: none;
+ padding: 0.8rem 2rem;
+ font-size: 1rem;
+ font-weight: 600;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+#guess_button:hover {
+ background-color: #34495e;
+ transform: translateY(-1px);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
+}
+
+/* Guess History Pills */
+#guesses {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ list-style: none;
+}
+
+#guesses li {
+ background-color: #e3f2fd;
+ color: #1976d2;
+ padding: 2px 10px;
+ border-radius: 20px;
+ font-size: 0.85rem;
+ font-family: "Courier New", Courier, monospace;
+ font-weight: bold;
+ border: 1px solid #bbdefb;
+}
+
+/* Notifications */
+#notifications {
+ text-align: center;
+ font-weight: 600;
+ min-height: 1.5rem;
+ margin-bottom: 1rem;
+}
+
+.success-msg {
+ color: #2e7d32;
+}
+.error-msg {
+ color: #c62828;
+}
diff --git a/html/word-guesser/index.html b/html/word-guesser/index.html
new file mode 100644
index 0000000..130238e
--- /dev/null
+++ b/html/word-guesser/index.html
@@ -0,0 +1,49 @@
+
+
diff --git a/html/word-guesser/scripts/script.js b/html/word-guesser/scripts/script.js
new file mode 100644
index 0000000..a698195
--- /dev/null
+++ b/html/word-guesser/scripts/script.js
@@ -0,0 +1,474 @@
+// -- 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];
+}
diff --git a/html/word-guesser/scripts/tests.js b/html/word-guesser/scripts/tests.js
new file mode 100644
index 0000000..26ab9ae
--- /dev/null
+++ b/html/word-guesser/scripts/tests.js
@@ -0,0 +1,679 @@
+function WordGuesserTests(wordChoices, fields) {
+ const reporter = new DOMReporter("test_container");
+ class TestRunner {
+ constructor(appName) {
+ this.root = new TestSuite(appName);
+ }
+
+ // Proxy to the root suite
+ expect(expectation) {
+ return this.root.expect(expectation);
+ }
+
+ describe(name) {
+ return this.root.describe(name);
+ }
+
+ report() {
+ console.debug("=".repeat(30));
+ this.root.report();
+ const stats = this.root.getStats();
+ console.debug("=".repeat(30));
+ console.debug(
+ `FINAL REPORT: ${stats.passed}/${stats.total} tests passed.`,
+ );
+ reporter.render(this.root);
+ reporter.renderFinalStats(stats);
+ reset(fields);
+ }
+ }
+ class TestSuite {
+ constructor(name) {
+ this.name = name;
+ this.tests = [];
+ this.subSuites = [];
+ }
+ // Create a new test within this suite
+ expect(expectation) {
+ const test = new Test(this.name).expect(expectation);
+ this.tests.push(test);
+ return test; // Returns the Test instance for further chaining (.value, .toBe, etc.)
+ }
+ // Create and return a nested suite
+ describe(name) {
+ const subSuite = new TestSuite(name);
+ this.subSuites.push(subSuite);
+ return subSuite;
+ }
+ // Recursive reporting
+ report(depth = 0) {
+ const indent = " ".repeat(depth);
+
+ this.tests.forEach((t) => {
+ const status = t.isPassed ? "✅" : "❌";
+ console.debug(`${indent} ${status} ${t.expectation}`);
+
+ if (!t.isPassed) {
+ let expected, got;
+
+ if (t.assertionType === "exception") {
+ expected = "Error satisfying validation logic";
+ got = t.actualResult;
+ } else {
+ expected = JSON.stringify(t.trialValue);
+ got = JSON.stringify(t.actualResult);
+ }
+
+ console.error(`${indent} Expected: ${expected} | Got: ${got}`);
+ }
+ });
+
+ this.subSuites.forEach((s) => s.report(depth + 1));
+ }
+ // Helper to aggregate total counts
+ getStats() {
+ let passed = this.tests.filter((t) => t.isPassed).length;
+ let total = this.tests.length;
+
+ this.subSuites.forEach((s) => {
+ const stats = s.getStats();
+ passed += stats.passed;
+ total += stats.total;
+ });
+
+ return { passed, total };
+ }
+ }
+ class Test {
+ constructor(name) {
+ this.name = name;
+ this.expectation = null;
+ this.sample = null;
+ this.trialValue = null;
+ this.actualResult = null;
+ this.isPassed = false;
+ this.errorCaught = null;
+
+ // -- Has been called?
+ this._trialValue = false;
+ this._sample = false;
+ this._callback = null;
+ this._threwError = null;
+ this._actualResult = null;
+ }
+
+ // Define the description of the test
+ expect(expectation) {
+ this.expectation = expectation;
+ return this;
+ }
+
+ // The value or function being tested
+ value(sample) {
+ this._sample = true;
+ this.sample = sample;
+ return this;
+ }
+
+ // The value to compare the result against
+ toBe(value) {
+ this._trialValue = true;
+ this.trialValue = value;
+ return this;
+ }
+
+ toThrow(validator) {
+ this._actualResult = true;
+ try {
+ if (typeof this.sample === "function") {
+ this.sample();
+ }
+ } catch (e) {
+ this.errorCaught = e;
+ this.isPassed = validator(e);
+ this.actualResult = e;
+ this._actualResult = true;
+ return this;
+ }
+
+ // If no error was thrown, the test fails
+ this.isPassed = false;
+ this.actualResult = "No error was thrown";
+ return this;
+ }
+ assert(callback) {
+ if (this._actualResult !== null) {
+ return this;
+ }
+ try {
+ if (typeof this.sample == "function") {
+ this.sample = this.sample();
+ }
+ if (typeof callback === "function") {
+ try {
+ this.actualResult = callback(this.sample);
+ } catch (e) {
+ this.threwError = false;
+ this.actualResult = e.message;
+ }
+ this._callback = true;
+ } else {
+ this.actualResult = this.sample;
+ this._callback = false;
+ }
+
+ this.isPassed =
+ JSON.stringify(this.actualResult) === JSON.stringify(this.trialValue);
+ } catch (e) {
+ this.actualResult = `Test Engine Error: ${e.message}`;
+ this.isPassed = false;
+ }
+ return this;
+ }
+ _reportFailure() {
+ console.error(
+ `Test Suite: ${this.name}\n`,
+ "Assertion Failed: ",
+ this.expectation,
+ "\n" + "Sample Input: ",
+ this.sample,
+ "\n" + "Expected Value: ",
+ this.trialValue,
+ "\n" + "Actual Result: ",
+ this.actualResult,
+ "\n",
+ );
+ }
+ reportFailure() {
+ console.error(
+ `Test Suite: ${this.name}\n`,
+ "Assertion Failed: ",
+ JSON.stringify(this.expectation),
+ "\n" + "Sample Input: ",
+ JSON.stringify(this.sample),
+ "\n" + "Expected Value: ",
+ JSON.stringify(this.trialValue),
+ "\n" + "Actual Result: ",
+ JSON.stringify(this.actualResult),
+ "\n",
+ );
+ }
+ }
+ let testNumber = 0;
+ function tests(word) {
+ testNumber++;
+ reset(fields);
+ const test = new TestRunner(`Word Guesser App Test ${testNumber}`);
+ const wordGuesser = new WordGuesser([word]);
+ const game = new Game([word], fields);
+
+ const validLetter = word[0];
+ const invalidLetter =
+ "abcdefghijklmnopqrstuvwxyz".split("").find((c) => !word.includes(c)) ||
+ "z";
+
+ // -- Test charsAt(char, chars)
+ const testCharsAt = test.describe("charsAt(char, chars)");
+
+ testCharsAt
+ .expect("No characters should be found")
+ .value({ char: "a", list: ["b", "c"] })
+ .toBe(false)
+ .assert((input) => charsAt(input.char, input.list));
+
+ testCharsAt
+ .expect("One character should be found at index 1")
+ .value({ char: "a", list: ["b", "a", "c"] })
+ .toBe([1])
+ .assert((input) => charsAt(input.char, input.list));
+
+ testCharsAt
+ .expect("Two characters should be found at indices 1 and 3")
+ .value({ char: "a", list: ["b", "a", "c", "a", "d"] })
+ .toBe([1, 3])
+ .assert((input) => charsAt(input.char, input.list));
+
+ // -- Test charInChars(char, chars)
+ const testCharInChars = test.describe("charsInChars(char, chars)");
+
+ testCharInChars
+ .expect("No characters should be found")
+ .value({ char: "a", list: ["b", "c"] })
+ .toBe(false)
+ .assert((input) => charInChars(input.char, input.list));
+
+ testCharInChars
+ .expect("One character should be found at index 1")
+ .value({ char: "a", list: ["b", "a", "c"] })
+ .toBe(true)
+ .assert((input) => charInChars(input.char, input.list));
+
+ testCharInChars
+ .expect("Two characters should be found at indices 1 and 3")
+ .value({ char: "a", list: ["b", "a", "c", "a", "d"] })
+ .toBe(true)
+ .assert((input) => charInChars(input.char, input.list));
+
+ const testWordGuesserLogic = test.describe("WordGuesser Class");
+ testWordGuesserLogic
+ .expect("gameOver should return false")
+ .value(() => wordGuesser.gameOver())
+ .toBe(false)
+ .assert();
+ testWordGuesserLogic
+ .expect("Current word to be placeholders")
+ .value(() => wordGuesser.getCurrentWord())
+ .toBe(Array(wordGuesser.getActualWord().length).fill("_"))
+ .assert();
+ testWordGuesserLogic
+ .expect("actual word to be " + word + " char list")
+ .value(() => wordGuesser.getActualWord())
+ .toBe(word.split(""))
+ .assert();
+ testWordGuesserLogic
+ .expect("actual word to be " + word)
+ .value(() => wordGuesser.getActualWord().join(""))
+ .toBe(word)
+ .assert();
+ testWordGuesserLogic
+ .expect("guesses")
+ .value(() => wordGuesser.guessCount())
+ .toBe(0)
+ .assert();
+ testWordGuesserLogic
+ .expect("to reject a bad guess")
+ .value(() => wordGuesser.guessLetter(invalidLetter))
+ .toThrow((err) => err instanceof BadGuessError);
+ testWordGuesserLogic
+ .expect("count bad guesses")
+ .value(() => wordGuesser.guessCount())
+ .toBe(1)
+ .assert();
+ const duplicateGuessError = (err) => {
+ return (
+ err instanceof Error && err.message.includes("already been guessed")
+ );
+ };
+ testWordGuesserLogic
+ .expect("ignore duplicate bad guesses")
+ .value(() => wordGuesser.guessLetter(invalidLetter))
+ .toThrow(duplicateGuessError);
+ testWordGuesserLogic
+ .expect("accepts a good guess and does not increase the guess count")
+ .value(() => {
+ try {
+ wordGuesser.guessLetter(validLetter);
+ } finally {
+ return wordGuesser.guessCount();
+ }
+ })
+ .toBe(1)
+ .assert();
+ testWordGuesserLogic
+ .expect("rejects duplicate good guesses")
+ .value(() => wordGuesser.guessLetter(validLetter))
+ .toThrow(duplicateGuessError);
+
+ testWordGuesserLogic
+ .expect("Updates current word")
+ .value(() => {
+ return wordGuesser.getCurrentWord()[0];
+ })
+ .toBe(validLetter)
+ .assert();
+ testWordGuesserLogic
+ .expect("Updates current word")
+ .value(() => {
+ return wordGuesser.getCurrentWord()[0];
+ })
+ .toBe(validLetter)
+ .assert();
+
+ const testLetter = (letter) => {
+ if (
+ !word.includes(letter) ||
+ wordGuesser.getCurrentWord().includes(letter)
+ )
+ return true;
+
+ const expectedCount = word.split("").filter((c) => c === letter).length;
+ const indexes = wordGuesser.guessLetter(letter);
+
+ if (indexes.length != expectedCount) throw new Error("Length mismatch");
+ if (
+ charsAt(letter, wordGuesser.getCurrentWord()).length !== expectedCount
+ )
+ throw new Error("Current word is not updated");
+ if (charsAt(letter, wordGuesser.getGuesses()) !== false)
+ throw new Error(`Guesses for '${letter}' are not false`);
+ return true;
+ };
+
+ const unGuessedLetters = [...new Set(word.split(""))].filter(
+ (c) => c !== validLetter,
+ );
+ const l1 = unGuessedLetters.length > 0 ? unGuessedLetters[0] : validLetter;
+ const l2 = unGuessedLetters.length > 1 ? unGuessedLetters[1] : l1;
+
+ testWordGuesserLogic
+ .expect(`Handles the letter '${l1}'`)
+ .value(() => testLetter(l1))
+ .toBe(true)
+ .assert();
+ testWordGuesserLogic
+ .expect(`Handles the letter '${l2}'`)
+ .value(() => testLetter(l2))
+ .toBe(true)
+ .assert();
+ testWordGuesserLogic
+ .expect("Handles game over")
+ .value(() => {
+ const testGame = new WordGuesser([word]);
+ const uniqueChars = [...new Set(word.split(""))];
+ uniqueChars.forEach((c) => testGame.guessLetter(c));
+ return testGame.gameOver();
+ })
+ .toBe(true)
+ .assert();
+ testWordGuesserLogic
+ .expect("Handles all guesses")
+ .value(() => {
+ const testGame = new WordGuesser([word]);
+ let validGuesses = [];
+ const alphabet = "abcdefghijklmnopqrstuvwxyz";
+ alphabet.split("").forEach((guess) => {
+ validGuesses.push(guess.toLowerCase());
+ validGuesses.push(guess.toUpperCase());
+ });
+
+ const goodGuesses = validGuesses.filter(
+ (badGuess) =>
+ charInChars(badGuess, testGame.getActualWord()) !== false,
+ );
+ const badGuesses = validGuesses.filter(
+ (badGuess) =>
+ charInChars(badGuess, testGame.getActualWord()) === false,
+ );
+ const failedBadGuesses = badGuesses.filter((badGuess) => {
+ try {
+ if (testGame.guessLetter(badGuess) !== false) return true;
+ } catch (e) {
+ if (
+ !(e instanceof BadGuessError) &&
+ !(e instanceof InvalidGuessError)
+ )
+ return true;
+ }
+ });
+ const failedGoodGuesses = goodGuesses.filter((goodGuess) => {
+ try {
+ testGame.guessLetter(goodGuess);
+ } catch (e) {
+ return true;
+ }
+ });
+ return failedGoodGuesses.concat(failedBadGuesses);
+ })
+ .toBe([])
+ .assert();
+ const testValidation = test.describe("Validation");
+ testValidation
+ .expect("to handle bad input")
+ .value(() => {
+ const badInput = "-0123456789!@#$%^&*()[]+=\\|/?,.{}\"'><:;".split("");
+ return badInput.filter((input) => {
+ try {
+ wordGuesser.guessLetter(input);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ });
+ })
+ .toBe([])
+ .assert();
+
+ const testWordGuesserDOM = test.describe("DOM testing");
+ // Test for Button Element
+ testWordGuesserDOM
+ .expect("Field 'guess_button' should be an HTML Button Element")
+ .value(
+ fields.guessButton instanceof HTMLButtonElement ||
+ fields.guessButton?.type === "button",
+ )
+ .toBe(true)
+ .assert();
+ // -- Test the letter input
+ testWordGuesserDOM
+ .expect("Field 'guess_count' should be an input of type 'number'")
+ .value(
+ fields.guessCount instanceof HTMLInputElement &&
+ fields.guessCount.type === "number",
+ )
+ .toBe(true)
+ .assert();
+ testWordGuesserDOM
+ .expect("Field 'guess' should be an input of type 'text'")
+ .value(
+ fields.guessLetter instanceof HTMLInputElement &&
+ fields.guessLetter.type === "text",
+ )
+ .toBe(true)
+ .assert();
+
+ // Test for Generic DOM Elements (Paragraphs/Divs)
+ testWordGuesserDOM
+ .expect("Field 'currentWord' should be a valid DOM element")
+ .value(fields.currentWord instanceof HTMLElement)
+ .toBe(true)
+ .assert();
+
+ testWordGuesserDOM
+ .expect("Field 'guesses' should be a valid DOM element")
+ .value(fields.guesses instanceof HTMLElement)
+ .toBe(true)
+ .assert();
+ // -- Tareted tests
+ testWordGuesserLogic
+ .expect(
+ "Verify updateCurrentWord places letters at the exact provided indices",
+ )
+ .value(() => {
+ const logicTest = new WordGuesser([word]);
+ const firstLetter = logicTest.getActualWord()[0];
+ const indexes = charsAt(firstLetter, logicTest.getActualWord());
+
+ logicTest.updateCurrentWord(firstLetter, indexes);
+ return logicTest.getCurrentWord()[indexes[0]];
+ })
+ .toBe(word.charAt(0))
+ .assert();
+
+ testWordGuesserLogic
+ .expect("gameOver identifies completed word")
+ .value(() => {
+ const overTest = new WordGuesser([word]);
+ const uniqueChars = [...new Set(word.split(""))];
+ uniqueChars.forEach((c) => overTest.guessLetter(c));
+ return overTest.gameOver();
+ })
+ .toBe(true)
+ .assert();
+
+ // -- updateLetters
+ const testUpdateLetters = test.describe("Utility: updateLetters");
+
+ const letters = updateLetters(["A", "B", "C"]);
+ testUpdateLetters
+ .expect("updateLetters should append the correct number of LI elements")
+ .value(letters.children.length)
+ .toBe(3)
+ .assert();
+ testUpdateLetters
+ .expect("updateLetters should handle empty arrays without error")
+ .value(() => updateLetters([]).children.length)
+ .toBe(0)
+ .assert();
+
+ // -- Test suite for Game Class Logic (Integration Testing)
+ const testGameIntegration = test.describe("Game Class Integration");
+
+ testGameIntegration
+ .expect(
+ "Game should update the DOM 'guessCount' display when a guess is made",
+ )
+ .value(() => {
+ game.newGame();
+ game.guessLetter.value = invalidLetter;
+ game.guessButton.click();
+ return game.guessCount.value;
+ })
+ .toBe("1")
+ .assert();
+
+ testGameIntegration
+ .expect("Input field should be cleared after a guess is submitted")
+ .value(() => {
+ game.newGame();
+ game.guessLetter.value = validLetter;
+ game.guessButton.click();
+ return game.guessLetter.value;
+ })
+ .toBe("")
+ .assert();
+
+ testGameIntegration
+ .expect("Game should update notifications area on duplicate guess error")
+ .value(() => {
+ game.newGame();
+ game.guessLetter.value = invalidLetter;
+ game.guessButton.click();
+ game.guessLetter.value = invalidLetter;
+ game.guessButton.click();
+ return game.notifications.innerText.toLowerCase().includes("🔁");
+ })
+ .toBe(true)
+ .assert();
+
+ testGameIntegration
+ .expect(
+ "UI 'guesses' list should contain a matching number of LI items to the guess count",
+ )
+ .value(() => {
+ game.newGame();
+ game.guessLetter.value = invalidLetter;
+ game.guessButton.click();
+ return (
+ game.guesses.querySelectorAll("li").length === game.game.guessCount()
+ );
+ })
+ .toBe(true)
+ .assert();
+
+ testGameIntegration
+ .expect("Game should display '🔁' icon for duplicate bad guesses")
+ .value(() => {
+ game.newGame();
+ const badLetter = invalidLetter;
+ game.guessLetter.value = badLetter;
+ game.guessButton.click();
+
+ game.guessLetter.value = badLetter;
+ game.guessButton.click();
+
+ return game.notifications.innerText.includes("🔁");
+ })
+ .toBe(true)
+ .assert();
+
+ testGameIntegration
+ .expect("Game should display '💎' icon for duplicate good guesses")
+ .value(() => {
+ game.newGame();
+ const goodLetter = validLetter;
+ game.guessLetter.value = goodLetter;
+ game.guessButton.click();
+
+ game.guessLetter.value = goodLetter;
+ game.guessButton.click();
+
+ return game.notifications.innerText.includes("💎");
+ })
+ .toBe(true)
+ .assert();
+
+ test.report();
+ }
+ wordChoices.forEach(tests);
+}
+
+class DOMReporter {
+ constructor(containerId) {
+ this.container = document.getElementById(containerId);
+ this.showFailedOnly = true;
+ }
+
+ render(rootSuite) {
+ if (!this.container) return;
+ this.container.appendChild(this.buildSuiteUI(rootSuite, 0));
+ }
+
+ buildSuiteUI(suite, depth) {
+ const suiteDiv = document.createElement("div");
+ suiteDiv.className = "suite-container";
+ suiteDiv.style.marginLeft = `${depth * 20}px`;
+
+ const title = document.createElement("h5");
+ title.innerText = `Suite: ${suite.name}`;
+ suiteDiv.appendChild(title);
+
+ const list = document.createElement("ul");
+ list.style.listStyle = "none";
+ list.style.padding = "0";
+
+ // Render Tests
+ suite.tests.forEach((test) => {
+ const item = document.createElement("li");
+ item.className = test.isPassed ? "test-pass" : "test-fail";
+ item.style.padding = "5px";
+ item.style.borderLeft = `4px solid ${test.isPassed ? "#4CAF50" : "#f44336"}`;
+ item.style.marginBottom = "5px";
+ item.style.backgroundColor = test.isPassed ? "#e8f5e9" : "#ffebee";
+
+ let content = `${test.isPassed ? "✅" : "❌"} ${test.expectation}`;
+
+ if (!test.isPassed) {
+ const expected = JSON.stringify(test.trialValue);
+ const got = JSON.stringify(test.actualResult);
+ content += `
+ Expected: ${expected} | Got: ${got}
+
`;
+ } else {
+ if (this.showFailedOnly) return;
+ }
+
+ item.innerHTML = content;
+ list.appendChild(item);
+ });
+
+ suiteDiv.appendChild(list);
+
+ // Recursive call for sub-suites
+ suite.subSuites.forEach((sub) => {
+ suiteDiv.appendChild(this.buildSuiteUI(sub, depth + 1));
+ });
+
+ return suiteDiv;
+ }
+
+ renderFinalStats(stats) {
+ const summary = document.createElement("div");
+ summary.style.marginTop = "20px";
+ summary.style.padding = "10px";
+ summary.style.fontWeight = "bold";
+ summary.style.borderTop = "2px solid #ccc";
+ summary.innerText = `Final Result: ${stats.passed} / ${stats.total} Passed`;
+ summary.style.color = stats.passed === stats.total ? "#2e7d32" : "#c62828";
+ this.container.appendChild(summary);
+ }
+}
diff --git a/navLinks.json b/navLinks.json
index 1f8b218..f1fb7b6 100644
--- a/navLinks.json
+++ b/navLinks.json
@@ -1,103 +1,106 @@
[
- {
- "href": "/",
- "label": "Home"
- },
- {
- "label": "About",
+ {
+ "href": "/",
+ "label": "Home"
+ },
+ {
+ "label": "About",
+ "submenu": [
+ {
+ "href": "/about/me",
+ "label": "About Me"
+ },
+ {
+ "href": "/about/blog",
+ "label": "About This Blog"
+ }
+ ]
+ },
+ {
+ "href": "/newsletter",
+ "label": "Newsletter"
+ },
+ {
+ "label": "Tools I use",
+ "submenu": [
+ {
+ "href": "/tools",
+ "label": "How I Use Them"
+ },
+ {
+ "href": "/stack",
+ "label": "Tech Stack Overview"
+ }
+ ]
+ },
+ {
+ "label": "Projects",
+ "submenu": [
+ {
+ "href": "/games/word-guesser",
+ "label": "A Word Guessing Game"
+ },
+ {
+ "label": "Express Blog",
"submenu": [
- {
- "href": "/about/me",
- "label": "About Me"
- },
- {
- "href": "/about/blog",
- "label": "About This Blog"
- }
+ {
+ "href": "/about/blog",
+ "label": "About This Blog"
+ },
+ {
+ "href": "https://github.com/jpoage1/expressjs-blog",
+ "label": "Source Code",
+ "target": "_blank",
+ "rel": "noopener noreferrer"
+ },
+ {
+ "href": "/docs",
+ "label": "Documentation"
+ },
+ {
+ "href": "/projects/website-presentation",
+ "label": "Website Project Presentation"
+ }
]
- },
- {
- "href": "/newsletter",
- "label": "Newsletter"
- },
- {
- "label": "Tools I use",
- "submenu": [
- {
- "href": "/tools",
- "label": "How I Use Them"
- },
- {
- "href": "/stack",
- "label": "Tech Stack Overview"
- }
- ]
- },
- {
- "label": "Projects",
- "submenu": [
- {
- "label": "Express Blog",
- "submenu":
- [
- {
- "href": "/about/blog",
- "label": "About This Blog"
- },
- {
- "href": "https://github.com/jpoage1/expressjs-blog",
- "label": "Source Code",
- "target":"_blank",
- "rel": "noopener noreferrer"
- },
- {
- "href": "/docs",
- "label": "Documentation"
- },
- {
- "href": "/projects/website-presentation",
- "label": "Website Project Presentation"
- }
- ]
- }
- ]
- },
- {
- "href": "/contact",
- "label": "Contact"
- },
- {
- "label": "Admin",
- "secure": "True",
- "submenu": [
- {
- "href": "/admin/logs",
- "label": "Logs"
- },
- {
- "href": "https://jasonpoage.com",
- "label": "Production",
- "appendCurrentPath": "True"
- },
- {
- "href": "https://test.jasonpoage.com",
- "label": "Testing",
- "appendCurrentPath": "True"
- },
- {
- "href": "https://dev.jasonpoage.com",
- "label": "Development",
- "appendCurrentPath": "True"
- },
- {
- "href": "https://jenkins.jasonpoage.com",
- "label": "Jenkins"
- },
- {
- "href": "https://auth.jasonpoage.com/api/logout",
- "method": "post",
- "label": "Logout"
- }
- ]
- }
+ }
+ ]
+ },
+ {
+ "href": "/contact",
+ "label": "Contact"
+ },
+ {
+ "label": "Admin",
+ "secure": "True",
+ "submenu": [
+ {
+ "href": "/admin/logs",
+ "label": "Logs"
+ },
+ {
+ "href": "https://jasonpoage.com",
+ "label": "Production",
+ "appendCurrentPath": "True"
+ },
+ {
+ "href": "https://test.jasonpoage.com",
+ "label": "Testing",
+ "appendCurrentPath": "True"
+ },
+ {
+ "href": "https://dev.jasonpoage.com",
+ "label": "Development",
+ "appendCurrentPath": "True"
+ },
+ {
+ "href": "https://jenkins.jasonpoage.com",
+ "label": "Jenkins"
+ },
+ {
+ "href": "https://auth.jasonpoage.com/api/logout",
+ "method": "post",
+ "label": "Logout"
+ }
+ ]
+ }
]