projects /Wardle | Turn-based Wordle clone built on HTML, CSS, JS

Project Overview

Wardle is a Wordle clone with a turn-based twist. The player will race against a CPU to guess a 5-letter word in 6 tries or fewer.

Wardle

Using data attributes, I was able to build a theme-changer. In each case, the green shade still represents correctly placed letters, but the traditional yellow tiles are replaced by a theme colour.

Wardle Themes

Technologies & Motivations

  • Vanilla HTML, CSS, JS

    • JS: DOM manipulation via browser events
    • JSON instead of API for word list
  • Aiming to have the game be playable on pc browser for initial version

    • mobile browser version: might require UI re-design, will keep it simple for now.

As Wardle was my first project under General Assembly (GA), I took the opportunity to deepen my understanding of CSS animations and DOM manipulation techniques. I also attempted designing a 'fake AI' system, to make my Wordle clone more unique.


Retrospective: Process Notes, Key Learning Points, Future Plans

Process notes:

At the beginning of the project week, I drafted some daily story points as a guide for my development process.

Monday
Tuesday
Wednesday
Thursday

Because of how straight-forward this project's goals were, and the relative simplicity of the UI, I was able to focus on the game logic.

Key Learning Points

Two challenges in particular were highly educational experiences.

1. Synthetic Events do not bubble by default

function submitCpuInput(lettersArr) {
  lettersArr.push("GO");
  let interval = 200;
  let increment = 1;
  let clickEvent = new Event("click", { bubbles: true });
  // sauce: https://stackoverflow.com/questions/25256535/javascript-set-interval-for-each-array-value-setinterval-array-foreach/37215055#37215055
  for (let i = 0; i <= WORD_LENGTH; i++) {
    let runner = setTimeout(() => {
      document
        .querySelector(`[data-cpu-key="${lettersArr[i]}"]`)
        .dispatchEvent(clickEvent);
      clearTimeout(runner);
    }, interval * increment);
    increment++;
  }
}

To simulate the CPU 'typing' out its answer, I dispatched events to the on-screen keyboard, simulating a player interacting with the keyboard. This eliminated the need to write unique animation logic for the CPU's side of the game.

However, after initially writing this dispatch function, I noticed the event was not triggering. MDN's documentation was not explicit about whether Event.bubbles defaulted to true or false. It was only after several rounds of trial and error, and research on StackOverflow did I realise this property was the source of the problem.


2. Object methods for filtering dictionary words with all valid letters

///////////////////////////////////////////////////////////
// CONTAINS VALID filter: shortlists words with all valid letters
let containsValidDuplicates = [];
noDupes.filter((word) => {
  for (let i = 0; i < uniqueValid.length; i++) {
    if (word.includes(uniqueValid[i]) == true)
      containsValidDuplicates.push(word);
  }
});
let containsValidFrequency = {};
containsValidDuplicates.forEach((word) => {
  containsValidFrequency[word] = (containsValidFrequency[word] || 0) + 1;
});
let filterValidByFrequency = Object.entries(containsValidFrequency);
let isolateContainsValid = filterValidByFrequency.filter(
  (filteredWordsInArrayFormat) => {
    if (filteredWordsInArrayFormat[1] === uniqueValid.length) {
      return filteredWordsInArrayFormat;
    }
  }
);
let trueValid = [];
isolateContainsValid.forEach((wordArray) => trueValid.push(wordArray[0]));
console.log(containsValidDuplicates);
///////////////////////////////////////////////////////////

As I was still relatively inexperienced with data structure manipulation and algorithms at this point in my learning process, I struggled to filter words containing all valid letters. Initially I thought I could use the Array.includes() method, but this produces an imperfect match. Words including some but not all, or words containing more copies of duplicate letters than what is correct, were included amongst the machine's guess list. Finally, I was able to settle on this relatively crude sorting function that utilises a hashmap.

Future Plans

1. Refine sorting algorithm

Given more time, I would have refactored my sorting algorithm to combine the use of a hashmap with regex functions. A pattern similar to Leetcode's Roman to Integer Question comes to mind.

var romanToInt = function (s) {
  const dict = {
    IV: 4,
    IX: 9,
    XL: 40,
    XC: 90,
    CD: 400,
    CM: 900,
    I: 1,
    V: 5,
    X: 10,
    L: 50,
    C: 100,
    D: 500,
    M: 1000,
  };

  let count = 0;
  let RE = new RegExp(Object.keys(dict).join("|"), "g");
  s.replace(RE, function (matched) {
    count += dict[matched];
  });
  return count;
};

2. Update CPU decision logic

I had initially also wanted to create a toggle to allow players to modify the difficulty of the game. At base, the CPU would always be aware of bad (grey) letters.

  • Easy mode: CPU knows placed (green) letters only. But not the placement of green letters.
  • Medium mode: CPU knows green and valid (yellow) letters. It would also know the placement of green letters, but not yellow letters.
  • Hard mode: CPU has the same knowledge as players: green and yellow letters, as well as their placement are known to CPU.

In the current iteration of the game, the sorting algorithm only receives information regarding the validity of the letters. It does not receive information about their placement. I would need to refactor the sorting algorithms to include placement (indices) in the calculation, as well as split the algorithm into multiple parts/ write separate functions to accommodate switching game modes.

This might also increase computation load, particularly if I expanded the dictionary in future. Therefore I would need to ensure that the time complexity of such sorting algorithms are taken into account.

3. Allow Players to Submit Words

The word dictionary was borrowed and refined from dracos' gist. Wardle test players (aka my dear frineds) flagged some notable issues:

  • Lots of scrabble words were included (which makes the game feel 'unfair')
  • Common plural versions of words (e.g. pains, prays, hopes) were previously missing from the list ((these have since been added)).

If I could set up a cheap database (e.g. using Gsheets/Airtable + Sheety api) and make a basic form for users to submit requests to add/ remove words, the overall quality of the word base would improve over time as people used it.

4. Moonshot plans

Perhaps use Wardle as a base project to build a python machine learning project on top of. The game could then be expanded beyond just copying Wordle's base mechanics (e.g. 5 letters) to possibly include more letters or even incorporate more unique features such as themed tourneys (e.g. given the theme 'cocktails', only cocktail names with a certain number of letters are valid guesses).