The Room Adventure: Additional Features

Checking that the Player Can Unlock the Locks

Difficulty: ★★★☆☆

Requirement: Lock and Key Feature

We installed a locked door feature in Add Locked Rooms. This requires you, as the developer, to place keys in the rooms and to lock doors. This must be done so you don't have a key hidden behind a locked door you need it for.

Even with good planning, mistakes can happen. You put the brass key in the office to unlock the dining room. You put the silver key in the dining room to unlock the hallway. Sounds good, except, you have to pass through the hallway to get to the office, so you can never get to the brass key, therefore you can never get to the silver key, and you can never unlock the hallway, which would let you get the brass key. (Follow all that?)

In addition, if you use certain randomization features, this whole process becomes a real mess.

We need a way to check to make sure the lock and key setup will allow the player to complete the game. Even if you set up the locks and keys yourself, you should create this and at least run it from the console to make sure your game is playable.

What we need to do

  • Create a set of tracking variables so we don't change actual room or player information.
  • Make a list of locked rooms and all keys from the room set we are testing.
  • Take keys from any room we are in and try to unlock any doors we can.
  • Randomly move to any possible, unlocked exit.
  • Continue until all locked doors are unlocked or we run out of tests.

Using console.log to debug

This function has a few tricky details. It can be hard to keep track of it all because we can't see what's happening.

For situations like this, I make heavy use of console.log(). I put variables names in there and then check the console when I run the function so I can check to see what's happening. It helps me figure out if something goes wrong.

In the code here, you will see a lot of console commands. You do not have to type these in if you don't want to. They won't change how your code runs at all. They're just there so you can see some output in the console.

You can see the output in the repl.it console window or by accessing developer tools, which you can usually find by pressing f12 on your keyboard.

Step 1: Initializing our variables

We only want this function to test the locks and keys. We don't want it to actually unlock anything or really move the player around. So we need to set up a few variables that will hold some information for us.

Code breakdown

Or skip directly to the next step.

The arguments

We are taking in three arguments here. One is a set of availableRooms you want to test out. This could be the global set of rooms, or it could be a smaller set of them, like one floor of the house, or an area of a forest. We are going to put in a failsafe so that if you leave out this argument, the function will default to using the global rooms object.

The playerStart is where you want the player to start in the house. This could make a huge difference to the game, depending on locks and keys! If you have a specific setup in mind, you need to control this carefully.

Like the rooms, we're going to have a backup plan if you don't send in the starting point for the player. In that case, we will use the player's current room.

We are also using an allowRandomStart switch. If the playerStart location is not a valid room, we can either say the setup doesn't work, or we could randomly pick a room for the player to start in and test that.

Leaving this out when you call the function would make it default to undefined, which would be treated as false when we test for it, therefore not allowing the starting point to be randomized.

If we pass in true for this, then it may help you figure out a good starting room if you're using the console to show values.

It is generally better to control your arguments directly by sending in specific parameters when you write your code. I like to have failsafes in place. By the way, if you don't send in a set of rooms, then you can't send in a player's starting place or the lock-in-place flag. Arguments and parameters have to line up when you send them in and when you assign them in the function.

Using the OR || operator for assignment

We usually use OR when we do a conditional test, like in an if or while statement. But JavaScript lets us use it when we assign values too.

Go the the equals sign and then start at the first value. If that translates to true, then it gets assigned to the variable. So if we send in a set of available rooms, then that's what we'll use. But if we didn't send in a set, leaving it undefined, that would be a false statement. The OR || operator then moves on to the next variable. If it's the last one in the set, it assigns it, whatever its value. In this case, the global rooms object.

If you have several OR operators in a row, it would keep moving down the list until either one of them was true or until it reaches the last one.

The underscore

We usually avoid using the underscore when we name things. By convention, it is used for private variables, or variables that really should not be touched by another programmer. It's not a rule and there's nothing stopping them from messing with it. But it's asking them not to.

We're using it to control a temporary set of keys that we need to keep track of while we check the locked-room-and-key layout. We are attaching it to the global player object so it can be used again if needed. Why? Well, if you are using the floor/area separator, you may need to know what keys the player has before coming into an area when you're testing things out. And we don't want to change the player's actual key inventory.

Copying an array

When we need a shallow copy of an array (like copying strings and numbers), we need to use the slice method. This leaves the original array intact and sends back a new copy of it. slice can also return just sections of an array, but here we need the whole copy.

We can't just assign the array as equal without the slice. If we did, then the new array would just be a pointer to the original array. So changing one would change "both." It's not really "both" because there's only one array with two names, like having a nickname. So, to make an array copy, we need slice.

Step 2: Make a list of locks and keys

Let's loop through the list of available rooms. We'll count the rooms we have, and make lists of the rooms that are locked, and all the keys that can be found.

You've seen code like this before.

Code breakdown

Or skip directly to the next step.

Counting and listing the rooms

We're keeping track of the number of rooms to help us figure out how many tries we should have to see if the keys and locks work. It's probably not that necessary, but why not?

We're keeping a list of startRooms in case there isn't a valid starting room for the player. This will then be the list of rooms we could pick from to try out.

Concatenating arrays

Concatenation just means that we add things together, like when we add strings. Concatenation takes two (or more) arrays and puts them all together into one bigger array, like copying both onto a new sheet.

The thing to remember, and it's easy to forget, is that this does not change the original arrays. Instead, it sends back a new array. That means you have to save it to a variable if you want to use it, and what's the point of making it if you don't want to use it?

We're overwriting the foundKeys array here. In essence, it adds the second array to the end of the first one instead of creating a new array. Well, that's just what it looks like is happening.

Testing object properties safely

You'll use lines like this a lot when you need to test certain properties of objects.

When you decided to add locks to your rooms, did you add a locked property to every single room, or just the ones you locked? We oly added them to the ones we locked, so most of your rooms won't even have this property at all. That means it's undefined. By itself, that's no big deal.

But the locked property is an array (when it's there). All arrays, even empty ones, test as true, so we can only tell if the room is locked by seeing if there is at least one key in the locked list.

So we need the length property of the room's locked array. Buuuut. If the room doesn't even have a locked property, then we would cause an error when we try to get its length.

Ugh, right? So, we first need to make sure that the object has the property we are looking for before we can check to see if that list has anything in it.

We use the AND && operator for this and it has to be set up in this order. We test for the locked property first to see if it even exists. If it doesn't, the JavaScript doesn't even look at the next part because the AND operator needs everything to be true, so if the first part isn't true then the whole thing can't be true either. This allows us to prevent an error.

At times, we take it one step further and start by testing if there even is an object! Like this.

Step 3: Printing to the console

Optional: Or skip directly to the next step.

Let's print our first set of variables to the console so we can check them when we run them. This can help us to troubleshoot any problems later when we run the function.

There are many ways to debug a program. You can use breakpoints and step through your code, tracking and watching variables as you go. Or you can do something like this.

I like this method in the beginning of troubleshooting because it gives a quick overview and I can usually find things faster. Also, when you have something that loops a lot, if you're stepping through it, it can take a really long time.

By the way, if you want to send an object to the console, you can, but put it on its own console line. Don't use the strings like I did. So if you wanted to print the available rooms, you could include this.

The reason is that once you have the strings in there, JavaScript converts it to a string using that data type's toString method. In doing that, an object just gets printed as [object Object] which isn't useful at all. Doing it the other way allows you to see all the properties and their values.

Step 4: Reporting back if we have an answer already

We've barely started the function, but we can already test for two situations.

  1. If no rooms are locked, then these rooms can be played.
  2. If any rooms are locked but there are no keys, these rooms cannot be played.

Let's test for these and if either condition happens let's exit out of the function and report back.

Again, the console calls are optional. You can leave them out if you don't want to include them.

Step 5: Was a valid starting position sent in?

Let's check to make sure the starting room sent in is in the set of rooms we received too. If not, then we have to do one of two things, and which one depends on that third parameter, allowRandomStart.

Step 6: Determining the number of steps to take

We are not using artificial intelligence or a tree search, like a breadth-first search or a depth-first search, to find out if the keys and locks work. Those would be better and more efficient ways of doing this process, and if set up correctly, they would always give you the right answer. But they are complicated processes with recursive functions and so on.

Instead, we're going to use a random walk. The computer is going to randomly pick a valid exit and go in that direction, pick up a key if there is one, unlock a door if it can.

But for how long should it try? Until everything is unlocked? Well, that's no good if one key is unreachable. So we need to set a limit on the number of times the computer will try to walk from room to room before giving up. It is possible that a room is solvable but we might run out of steps before it's figured out.

This calculation is pretty arbitrary. It's not based on anything complex. It's affected by the total number of rooms and the number of locked rooms. And it's bound between 1000 and 10000 steps. Yes, 10000.

Sidenote: If you want to run an iteration test like I did, I'm making the function available for you. The difference is, I'm not going to explain all of it aside from the comments that are directly in the code. This link will show you the code as a webpage. You can follow along and type it in to your own code or simply copy and paste it in too.

With my base set of 14 room, two locks and two keys, I ran 100,000 iterations to test things out. Yes, one hundred thousand. I created a helper function to do this and to track the results with a temporary tweak or two to the checkKeys function to make it work. I had the player randomly placed into any of the rooms to start and then tested the lock/key placement. At a 100,000 times running the check, it took just over 2.2 seconds. The player took a maximum of 943 steps before determining if the room could or could not be solved. On average, the player took about 97 steps to traverse the house. And randomly placing the player in a room with my locks and keys where they were created a 78% able-to-play-the-game completion rate. Not bad for our function here.

Code breakdown

Or skip directly to the next step.

Getting a number within a range

We start by calculating a number of iterations based on the total number of rooms sent in and the number of rooms that are locked in that set. You could multiply it by any number you want, but I figured a hundred steps per room per locked room was plenty.

If you have very few rooms and locks, there many not be enough steps (iterations) to test it all out. On the other hand, if you have huge numbers, it could take too long or cause a memory issue for the browser.

So we want to contain the values. I picked 1000 to 10000. Again, it's arbitrary and you can change them.

We use a combination of Math.min and Math.max to do this. You use Math.max to set the lower boundary. And then you use Math.min to set the upper.

It seems backward. Let's say your calculation produced 200 iterations. If you use Math.min(200, 1000);, then it would pop out 1000. Now put that with 10000 into Math.min(1000, 10000) and you get 1000 iterations, which is our lower limit.

Or, let's say your calculation came up with 3000 iterations. If you use Math.min(3000, 1000);, then it would pop out 3000. Now put that with 10000 into Math.min(3000, 10000) and you get 3000 iterations, which was your number.

Finally, let's say your calculation came up with 60000 iterations. If you use Math.min(60000, 1000);, then it would pop out 60000. Now put that with 10000 into Math.min(60000, 10000) and you get 10000 iterations, which is our upper limit.

Step 7: Start the 'iteration' loop

It's time to start moving the computerized player randomly through the house to test out the layout. We'll start by setting up two pre-loop variables we need, starting the loop, and then I'm putting in some console displays again (which you can still leave out if you want).

Let's reset the key list to what it was when the function started and put the fake player into the room.

Step 8: Looking for a key

For this checker to work, the computer-player needs to pick up a key in any room it's in that has a key. That's what happens here. We also make sure the computer doesn't end up with hundred of copies of keys. We're not actually removing them from the rooms because we're not altering the original rooms at all, so each time the computer goes in the room with the key, it would take another copy!

Be wary. We're going to get nested pretty deep here, so watch your braces carefully.

At this point, if there were any keys in this room, you've got them all.

Step 9: Starting the 'exit' loop

We are still inside the iteration loop, remember. This loop is pretty big because there are a few things we need to look for. At one point, it's going to get nested even deeper than that last one! Just keep track of those braces and you'll be fine.

We're going to make a list of possible exits we can choose for later. These will be unlocked doors from the room we're in. To do that, we will start with an empty array. Then we begin a for...in loop to run through all the current room's exits.

Edge cases

We have one edge case we need to be concerned with. We have to make sure we aren't starting in a room that's locked, unless we already have its key, or unless the key is in that room. So on the first time through the loop, when the loop counter, i, is 0, we're going to look at the room we're in, look for keys, check the locks, and if we're stuck, then this test is a bust.

Another edge case we have to watch for is unavailable rooms. It is possible that one of the rooms that was sent in to the function has an exit to a room that was not sent into the function. Say, for example, we are checking locks and keys on the first floor, but there's an exit to the second floor and we don't have the list of rooms from the second floor here. If that happens, we can't check that room.

There's a bit more coming to this loop, but that's enough to get it started. Let's go.

Step 10: If it's not locked, it's a possible exit

After that last step, here's an easy one. We'll make a helper variable that use we'll again after this. Remember we made that list of lockedRooms in the very beginning of the function in that very first loop?

We're going to take the name of the exit we're looking at to see if it's on that locked room list. If it isn't, then we can assume that it isn't locked and so, it's a safe exit for us.

After this step, we're going to start unlocking rooms and taking room names off that lockedRooms list. That's how we're simulating the unlocking of rooms without actually messing up the rooms themselves. Sneaky, huh?

Step 11: Is it locked and can we unlock it?

Now we're at the crux of what we need this function to do: Find locked rooms and try to unlock them.

The problem is... doing that requires quite a bit more nesting. We are already three layers deep. We're inside the 'exit' loop, which is inside the 'iteration' loop, which is inside the function itself. And here we need to go further in.

Part of the issue is that it's possible for a room to have more than one key that opens it. That means we have to check all those possibilities, which adds another loop into the mix.

Here's the plan.

Here we go.

Really deep, wasn't it?

Step 12: Finishing the 'exits' loop

We are getting close to the finish. We have one more detail to tend to before we close up the 'exits' loop.

The first time through the 'iteration' loop, we're just checking the room we're in to make sure we can leave it. If we can, it will have already been placed on the exitList. But right now, it's trying to loop through all the exits of that room, so if it has four exits, then this room will end up on that list four times. It's not a big deal, really, but unnecessary. So, if we're still in the first iteration of the loop, let's break out of it here, then end the 'exits' loop.

Step 13: Closing the 'iteration' loop

At this point in the function, whatever room the computer-player is in, all the exits have been checked and possibly unlocked. We have a list of exits we can possibly go to.

Now we need to pick one of the those exits at random—that's where the name random walk comes from—and move the player there to keep the check going.

Step 14: Finishing the function

Ok, we set the computer-player loose in the rooms, picking up keys and unlocking doors, seeing if it was possible to unlock them all. If it is, then this function escaped back up when the lockedRooms array was emptied out.

We just closed the 'iteration' loop a moment ago, so what's left to do? Well, if the function hasn't returned a successful rating at this point, then it means the computer was not able to unlock everything in the number of steps (iterations) it had available.

So all that's left is to send that result back and close the function.

We're there... at the end. Let's finish this function!

What's next?

That was a lot, but you got to this point. Now you need to test it out. Here's one way.

  1. Make sure you have at least one locked room and one key set up in your layout.
  2. Run your game. If you're in the text version, quit when you can.
  3. Get to the console window while your game variables are in memory.
    • In repl.it, click on the console tab.
    • In a browser, open developer tools. Try pressing f12.
  4. Call the new function from the console.
    • This will run the default: Full room set based on the player's current room.
    • checkKeys();
    • Or substitute in other values as desired.
    • Choose a specific room to start checking from.
      • checkKeys(rooms, "den", false);
    • Send in a subset of rooms and a starting room (you have to create the firstfloor set first)
      • checkKeys(firstFloor, "stairway", false);
    • Let the computer pick a random room and test it out
      • checkKeys(rooms, "", true);
      • The console will tell you which room was the starting point if you have this line of code in there before the iteration loop starts.
        • console.log("start room: " + playerStart);
  5. Read the output in the console.
    • If it says true then it works with the settings you put in. Congrats!
    • If it says false, it's possible the setup could still be correct.
      • This is because we're testing by random walk, not by a tree search.
      • You could run it again with the same settings and see if it figures it out a second time.
      • But it did run 1000 to 10000 steps through your house... Unless you passed in a lot of rooms at once, maybe it just won't work the way it's set up.
  6. Adjust your locks and keys as needed.

What if I want to use other randomizers?

If you plan to use randomizers like randomizeRoomLayout, randomizePlayerStart, or randomizeLocksAndKeys, then you'll need to use the checkKeys function inside startGame.

Here's a quick brute-force solution. It will cause your game to shuffle things a bunch of times until the game is playable. That might cause it to hang when you start it up.

Original code section

Inside your startGame function, you have a set of lines that look like this, though possibly in a different order and some may be missing if you didn't add certain features.

Reorganize those lines to this

We need to group together just the functions that affect the lock and key situation.

Wrap the related randomizers in a loop

We don't want this to run forever. We want it to break out at some point no matter what. I'm going to have two checks. One is an iteration counter and the other is whether we are successful.

I'm setting up the two variable first and then wrapping the related functions in the loop. The other functions will be after the loop. I'm not deleting them.

alert vs throw

Typically, instead of an alert, programmers would use a throw to cause a "crash" in the browser. It's on purpose, so it's not the same as when something goes wrong. The alert is less scary. If want to use the throw, swap out the alert line with this.

When you throw an error, the program stops running completely. There's more you can do with throw, but we're not getting into it right here.

Simpler success?

Because of how we set up our checkKeys function, we could simplify this line as shown.

But this is only ok to do if we are using the global set of all rooms. Currently, my code here does. But it's better to send in the expected parameters any time you can for readability and code clarity.

May you always be able to unlock the doors to your future!

—Dr. Wolf