Lingo and Explorations in JavaScript

One of my favorite game shows from back in the mid 2000’s was the game Lingo. My wife and I used to occasionally watch the show, and would play each other on the online Flash version. Soon after this, the show went off the air, and eventually the game disappeared from the Internet. My wife and I continued to play Lingo in paper-and-pencil format, usually on the back of diner place-mats.

For those who have never played the game before, here is the general idea: You have to guess a five-letter word to which you are given the first letter. After each guess is made, you are told whether each character that you guess is: 1) in the word, and/or 2) in the correct position in the word. Based on this information, you make another guess, to which you are given another clue, and the game continues until you guess the correct word. To get a real idea how it works, you can play it here, as GSN resurrected the show back in 2011, unbeknownst to me, as we ‘cut the cord’ long ago. Or even better, you can play my version of the game.

So, one project idea that I’ve had in the back of my mind for a while was to create a clone of the game Lingo. Two weeks ago, I decided to use this project idea as a chance to further develop my JavaScript skills, and try to incorporate some Object-Oriented JavaScript principles into my code.

Project Goals

My goals of the project and code were the following:

  • The program would randomly select a word from a predetermined list
  • The program would present the first hint, which would be the first character of this word to the user.
  • The user would make a guess.
  • The program would compare the user’s guess to the selected word. For each position in the word, it would determine if the guessed character matched the actual character in the position, or if the guessed character could be found in the word, or neither of these.
  • Based on this comparison, the program would present the next hint. There would be a visual mechanism to display to the user whether a guessed character was in the word/in position.
  • The user would make another guess.
  • The process would continue until the user guessed the selected word.

For my own personal skill-building, my goal for this project was to focus on writing the code to do all of the above, in a clean, object-oriented way. I did not want to focus too much on front-end aspects. In fact, my first version of this program was played in the Chrome console, just by calling methods. Eventually, I added a UI so that I could share the completed app with everyone, but did not spend a lot of time working on the design (and though, the design is simple, I think it shows).

The process

To get started, I created three classes:

function Round() {
  this.word = '';
  this.hint = [];
  this.unguessedCharacters = [];
}

This class stores the current states of the game, as well as provides methods for the game mechanics. Round has three properties: word, which is the selected word for the round, hint, which is an object of Hint type, and stores the current hint to presented to the user, and unguessedCharacters, which is an array of characters left in the word which have not been guessed.

function Hint() {};

This class is basically just an array of Letter objects, which are stored in indices 0-4 of this object, therefore, giving formal property names to the class was not necessary. The Hint class provides a toString() method which provides a textual version of the hint, and is used for debugging.

function Letter(character, inWord, inPosition) {
  this.character = character || '';
  this.inWord = inWord || false;
  this.inPosition = inPosition || false;
}

The final class in the program is a Letter class. This object holds the data about each letter in hint, which character is to be displayed (if any), whether the character is in the word, and whether the character is in the correct position.

The game starts like this:

$(document).ready(function() {
  $.getScript('word-list.js', function() {
    round.start();
    hintToCells(round.hint);
    $('.guessInput').focus();
  });
. . .

Once the page has loaded, a jQuery function gets the word list, so that it is available to the code, a new instance of Round is created, and the start() method is called on it.

Round.prototype.start = function() {
  this.word = getNewWord();
  this.hint = getFirstHint(this.word);
  this.unguessedCharacters = setUnguessedCharacters(this.word);

This function utilizes three helper functions to set the first states of the game.

  function getNewWord() {
    return WORD_LIST[Math.floor((Math.random() * WORD_LIST.length))];
  }

chooses a word randomly from WORD_LIST. (Note: for readability, all of the words are stored in a separate file word-list.js, which is loaded in the HTML document before the main app script is.)

  function getFirstHint(word) {
    var hint = new Hint();
    for (var i = 0; i < 5; i++) {
      if (i === 0) {
        hint[i] = new Letter(word.charAt(i), true, true);
      } else {
        hint[i] = new Letter();  
      }
    }   
    return hint;
  }

creates a new Hint object, and populates it with new Letter objects, the first of which contains the first letter, in position/word.

  function setUnguessedCharacters(word) {
    var unguessedCharacters = [];
    for (var i = 1; i < word.length; i++) {
      unguessedCharacters[i-1] = word.charAt(i); 
    }
    return unguessedCharacters;
  }
};

loads up the unguessedCharacters array with the remaining unknown letters of the word.

Next, the hintToCells() function is called:

function hintToCells(hint) {
  var row = ''; $('.guessTable').append(row); 
  for (var i = 0; i < WORD_LENGTH; i++) { 
    var statusClass = ''; 
    if (hint[i].inWord) { 
      statusClass = ' inWord'; 
    } 
    if (hint[i].inPosition) { 
      statusClass = ' inPosition'; 
    } 
  $('.guessRow').last().append('' + hint[i].character + ''); 
  } 
}

This function adds a row to the gameboard table, reads round.hint and propagates the five cells with the characters from from the Letter objects stored in round.hint. In addition, cells are styled according to the values of inPosition (colored green if true), and inWord (colored yellow if true).

At this point, the app is listening for the click event on the ‘Guess’ button. The user types a word into the box, and clicks ‘Guess.’ This triggers the submitGuess function:

function submitGuess() {
  var guess = $('.guessInput').val(); 
  var gameWon = round.guess(guess);
  if (gameWon) {
    guessToCells(guess);
    $('.guessRow').last().children().css('background-color', 'green');
    $('.play').hide();
    $('.winner').show();
  } else {
    var newHint = round.hint;
    hintToCells(newHint); 
    guessToCells(guess);
    $('.guessInput').val('').focus();
  }
}

This function takes the guess from the input, and passes it to the Round.guess(), which will return true if the word is correct, and will otherwise return false. If the game is won, the final guess is displayed on the grid, the elements involved in gameplay are hidden, and a winner message is shown. The page can then be reloaded to start a new game. If the game is not won, a new hint is added to the table, merged with the player’s last guess (these leaves a nice visual record of each guess in the game).

Several other methods and functions are involved to make this all happen:

Round.prototype.guess = function(guessedWord) {
  guessedWord = guessedWord.toUpperCase();
  if (guessedWord === this.word) {
    return true;
  }
  this.hint = getNextHint(guessedWord, this.word);
  unsetGuessedCharacters(guessedWord);
  return false;
  . . .

The main function, which utilizes a chain of helper functions to do the individual tasks. The guessedWord is converted to uppercase for comparison (the words in word-list.js are stored in uppercase). If the words do not match, the getNextHint() helper function is called, and any guessed characters are removed from the round.unguessedCharacters property.

  . . . 
  function getNextHint(guessedWord, word) {
    var hint = new Hint();
    for (var i = 0; i < WORD_LENGTH; i++) {
      hint[i] = compare(guessedWord, word, i);
    }
    return hint;
  }
  . . .

For each position in the hint, the characters between the actual word and the guess are compared:

  . . .
  function compare(guessedWord, word, i) {
    if (word.charAt(i) === guessedWord.charAt(i)) {
      return new Letter(word.charAt(i), true, true);
    }
    if (isUnguessedCharacter(guessedWord.charAt(i))) {
      return new Letter(guessedWord.charAt(i), true, false); 
    }
    return new Letter();
  }
  ...

This function basically checks each character for one of three conditions (in position, in word, not in word), creating the corresponding Letter objects.

  . . .
  function isUnguessedCharacter(character) {
    if (round.unguessedCharacters.indexOf(character) > -1) {
      return true;
    }
    return false;
  }
  . . .

This checks if the compared character has been guessed. This mechanism was necessary to ensure that letters that were already guess and in place were not later labeled as being ‘in word’ in the hint, as this might lead the user to think that there is another one of those letters in the word.

  ...
  function unsetGuessedCharacters(guessedWord) {
    for (i = 0; i < guessedWord.length; i++) {       
      if (round.unguessedCharacters.indexOf(guessedWord.charAt(i)) > -1 && 
          round.word.charAt(i) === guessedWord.charAt(i)) {
        var indexOfCharacter = round.unguessedCharacters.indexOf(guessedWord.charAt(i));
        round.unguessedCharacters.splice(indexOfCharacter, 1);
      }
    }
  }
};

The last helper function involved in Round.guess() unsets any guessed characters from the round.unguessedCharacters property. It does this by finding the guessed characters in the string, and splicing out the characters.

One more method to highlight is the Round.getUserHint() method. I added this as some of the words to guess are difficult. In order to get this word list, I did a Google search for ‘five letter words in the English language,’ and came across this page. You’ll see from the start that there are a lot of unheard of words on this list. I had originally started removing these words from the list, but when I realized how long this would actually take, I gave up, and added a ‘Need a Hint?’ button. When the user clicks this button, this method is called:

Round.prototype.getUserHint = function() {
  for (var i = 0; i < WORD_LENGTH; i++) {
    if (!this.hint[i].inPosition) {
      break;
    }
  }
  this.hint[i].character = this.word.charAt(i);
  this.hint[i].inWord = true;
  this.hint[i].inPosition = true;
  hintToCells(this.hint);
};

which basically gives a new hint with the next ‘in position’ letter added to the hint.

Challenges

The biggest challenge of this project for me was the comparison logic involved. The ‘in-position’ or not logic is an easy string comparison, but the trouble comes when you have to determine if a character is in the word. The array function indexOf() was a good start, but this is a little overzealous, and continues to indicate that a letter is ‘in word,’ even if that same letter is already in position elsewhere. This is why I later added the unguessedCharacter property to round. This has mostly solved the problem, though there are still some issues I have to work out. One in particular is if your guess has double letters, (for example ‘BEERS’), and the target word has an ‘E’ in it, both E’s will light up in the next hint. Does that mean the target word has one E or two? This is not clear.

Some of my challenges were actually on the front-end, when I started building the UI. I experimented with several input options, including removing the input box altogether, and using keyboard event listeners to detect keystrokes and place the corresponding characters into empty divs. While this looked nicer, as it mimics the show and the online game, it was very inaccessible, especially from a mobile device. Without an input box, the keyboard doesn’t rise, making it impossible to enter an answer without a physical keyboard. I went back to the input box, but had to play around with CSS a lot to get it to look right, and I am still not completely satisfied with the list.

The last challenge is the word list itself. As mentioned earlier, there are some very unfamiliar words in the list, and rooting them out of the list by hand would take more time than I have. A possible solution would be to automate the process of querying each word in a Google search, and collecting how many results are returned. Then, a threshold could be set where all words under a certain result count would be rejected.

Conclusion and thoughts for the future

For right now, I am considering this project to be finished, as I have other things that I would like to work on. However, there are several things that, at some point in time, I would love to add to this project:

  • Automatically giving the user a bonus letter (user hint) after every five guesses
  • Limiting the allowed guesses to actual, non-proper, words (utilizing the original word list).
  • Guesses that didn’t match this criteria would be rejected. This would prevent users from entering gibberish to find letters (for example, entering ‘AEIOU’ as a guess to see which vowels were present).
  • Cleaning up the UI
  • Moving the word list to a MySQL database, and writing a PHP script that would serve the words on an AJAX call from the client
  • A scoring system

Overall, this has been a great learning experience. It has been thrilling to take a project from start to (minimally) finished, and I feel this has made me a better programmer.

If you are reading this, you actually made it down here–awesome! So, thoughts? How would you approach some of the challenges? What would be another great feature to add that would give me the opportunity to learn something else?

Thanks for reading!!

Leave a Reply

Your email address will not be published. Required fields are marked *