By JD Isaacks

In this article we’re going to build a lock and key system. Each lock has its own unique key. The lock holds some secret data and only returns the data if accessed with the correct key. We’ll then build a game where we generate three locks and give the player only one key and one chance to unlock a prize!

Save 37% off Get Programming with JavaScript Next with code fccisaacks at manning.com.

Creating the Lock & Key System

Let’s start by designing the lock API. We want to have a function called lock that accepts a single parameter which is the data we’re locking up. It should return a unique key (key as in key to a lock) and an unlock function that reveals the secret if invoked with the correct key. The high level API is { key, unlock } = lock(secret).

The secret can be any single datum. Both lock and unlock are functions, but we need to figure out what data type the key is going to be. A good solution would be a randomly generated string, but strings aren’t unique; it’s possible they may overlap or even be guessable. We can use a complex string which is hard to guess or rarely overlaps like a SHA1, but that’d take a lot of effort or require installing a library. We already have a data type which is easy to generate and guaranteed to be unique, the Symbol. Let’s use that.

 
 function lock(secret) {
   const key = Symbol('key')
  
   return {
     key, unlock(keyUsed) {
       if (keyUsed === key) {
         return secret
       } else {
         // do something else
       }
     }
   }
 }
 

Here we have a lock function which, when invoked, is given a secret and generates a new key which uses a unique symbol. Remember, every symbol created this way is unique and never overlaps. The lock function returns an object with the generated key and an unlock function. The unlock function accepts a keyUsed parameter and compares the key used to unlock the secret against the correct key. If they’re the same, it returns the secret.

 
 const { key, unlock } = lock('JavaScript is Awesome!')
 unlock(key) 
 

JavaScript is Awesome!

We need to figure out what to do if an incorrect key is used. In a real-world app, we’d probably throw an error if the wrong key is used. For the sake of this exercise, let’s make it mask the value. We can use String.prototype.repeat to return a masked copy of the string. Something like:

 
'*'.repeat( secret.length || secret.toString().length )
 

Here’s the updated function:

Listing 1

 
 function lock(secret) {
   const key = Symbol('key')
  
   return {
     key, unlock(keyUsed) {
       if (keyUsed === key) {
         return secret
       } else {
         return '*'.repeat( secret.length || secret.toString().length )
       }
     }
   }
 }
  
 const { key, unlock } = lock(42)
  
 unlock() 
 unlock(Symbol('key')) 
 unlock('key') 
 unlock(key) 
 

**

**

**

42

Awesome! We’ve completed our lock & key system and it works great! Best of all, we did it with minimal code and no need for an external library. Now we can create several locks with several keys and each lock is only accessible with its associated key. What if we created several locks and keys and mixed them up?

Creating a Choose the Door Game

If we created three locks and presented them to a player as Door #1, Door #2 and Door #3, and gave the player a single key, we could ask the player to guess which door their key goes to. If they guess right, they win the prize behind the door. Hopefully that sounds like a fun game because it’s exactly what we’re going to build. Because the locks in this game are going to be doors, then the secret can be what’s behind the door. This may seem like a familiar game but the twist is that the player may choose any door, but only if their key opens the door do they find out what’s behind it and win the prize. Ready? Let’s build it. Let’s start by building the main user interface to our game. We need a way to present some options to a player and for that player to select an option. To keep it simple we’ll use prompt for this task.

Listing 2

 
 function choose(message, options, secondAttempt) {
   const opts = options.map(function(option, index) { 
     return `Type ${index+1} for: ${option}`
   })
  
   const resp = Number( prompt(`${message}\n\n${opts.join('\n')}`) ) - 1 
  
   if ( options[resp] ) {
     return resp 
   } else if (!secondAttempt) {
     return choose(`Last try!\n${message}`, options, true) 
   } else {
     throw Error('No selection') 
   }
 }
 

We build the multiple-choice options displayed in the prompt. We add one to each index to start at 1 instead of 0.

We get back the number that the user typed into the prompt. We need to subtract the one that we added earlier.

If we got an appropriate response from the prompt, we return the value.

If we didn’t get a valid value, we make a second attempt.

If we still didn’t get a valid value after a second attempt, we throw an error.

This choose function accepts a single message and an array of options. It handles wiring everything to allow the player to see the message, options, and a number correlating to each option to make a selection. If an incorrect input is received, the player is asked one last time, and if a valid option is still not selected by the player, an error is thrown. Once a valid option is selected, the index of that option is returned from the choose function. By handling all of those details in this choose function we can focus on what the message and options are elsewhere in our game. We can use the choose function like this:

Listing 3

 
 const message = 'Who is the greatest superhero of all time?'
  
 const options = ['Superman', 'Batman', 'Iron Man', 'Captain America']
 const hero = choose(message, options)
 

Now that we have a way to ask the user to choose a door, we need to generate three doors (locks) and assign the player a random key. First let’s generate the doors with prizes. How do you think we should do that? You may try something like:

 
 const { key1, door1 } = lock('A new car')
 const { key2, door2 } = lock('A trip to Hawaii')
 const { key3, door3 } = lock('$100 Dollars')
 

This won’t work. Remember, you have to destructure objects using the correct property names assigned to that object. The objects returned from lock only have the properties key and unlock and must be destructured. That may lead you to try this:

 
 const { key, unlock } = lock('A new car')
 const { key, unlock } = lock('A trip to Hawaii')
 const { key, unlock } = lock('$100 Dollars')
 

This won’t work either. After the first destructure, the constants key and unlock are already taken and can’t be declared again, which is attempted on lines 2 and 3. All isn’t lost, however, when destructuring property names there’s a special syntax to assign them to a different variable name. This is exactly what we need to do now:

 const { key:key1, unlock:door1 } = lock('A new car')
 const { key:key2, unlock:door2 } = lock('A trip to Hawaii')
 const { key:key3, unlock:door3 } = lock('$100 Dollars')

Now we have three keys and three doors. Let’s put them each in an array and grab a random key which we’ll give to the user:

 
 const keys = [key1, key2, key3]
 const doors = [door1, door2, door3]
  
 const key = keys[Math.floor(Math.random() * 3)]
 

Easy enough, now let’s create the message and options that we’ll present to the player:

 
 const message = 'You have been given a \u{1F511} please choose a door.' 
  
 const options = doors.map(function(door, index) {
   return `Door #${index+1}: ${door()}`
 })
 

\u{1F511} is the unicode key character

Notice how we used \u{1F511} to escape our unicode character. I could’ve put the actual character into the message like this:

 
const message = 'You have been given a 𑠀 please choose a door.'
 

This would be a lot harder to expect the reader to type. Incidentally, it brings up another new feature. Prior to ES2015 the syntax for unicode escaping was \uXXXX where XXXX was the unicode hexadecimal code. It only allowed up to four hex characters (16 bits), but notice how our character used five: 1F511. To solve this prior to ES2015 you had to use what’s called a surrogate pair which uses two smaller unicode values to generate a larger one. To get our key we could use \uD83D\uDD11. The concept of surrogate pairs or how to generate them is beyond the scope of this article, but ES2015 introduced an easier way with \u{XXXXX} which allows you to escape any size unicode characters.

Now that we have our message and options, we can pass them to our choose function which’ll ask the user to choose a door. Once they choose a door we can attempt to unlock it with their key. If the key is correct, we’ll get the secret behind the door, otherwise we’ll get the masked text. Either way we’ll alert the response back to the user.

 
 const door = doors[ choose(message, options) ]
 alert( door(key) )
 

Let’s put all this together in a function called init:

Listing 4

 
 function init() {
   const { key:key1, unlock:door1 } = lock('A new car')
   const { key:key2, unlock:door2 } = lock('A trip to Hawaii')
   const { key:key3, unlock:door3 } = lock('$100 Dollars')
  
   const keys = [key1, key2, key3]
   const doors = [door1, door2, door3]
  
   const key = keys[Math.floor(Math.random() * 3)]
  
   const message = 'You have been given a \u{1F511} please choose a door.'
  
   const options = doors.map(function(door, index) {
     return `Door #${index+1}: ${door()}`
   })
  
   const door = doors[ choose(message, options) ]
  
   alert( door(key) )
 }
 

Now if the user selects the correct door they’ll see what’s behind it, winning the prize. What other uses can you think of for this lock and key system? Maybe a multiplayer game with hidden treasure chests and their keys spread throughout. What if you took the key from one lock and locked it up in another lock – that could get interesting!

That’s all for this article. For more, download the free first chapter of Get Programming with JavaScript Next and see this Slideshare presentation.