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.
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.
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.
Or skip directly to the next step.
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.
||
operator for assignmentWe 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.
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.
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.
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.
Or skip directly to the next step.
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.
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.
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.
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.
We've barely started the function, but we can already test for two situations.
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.
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
.
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.
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.
Or skip directly to the next step.
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.
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.
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.
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.
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.
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?
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.
lockedRooms
array.lockedRooms
array is empty (all rooms are unlocked)Here we go.
Really deep, wasn't it?
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.
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.
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!
That was a lot, but you got to this point. Now you need to test it out. Here's one way.
repl.it
, click on the console tab.checkKeys();
checkKeys(rooms, "den", false);
firstfloor
set first)
checkKeys(firstFloor, "stairway", false);
checkKeys(rooms, "", true);
console.log("start
room: " + playerStart);
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.
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.
We need to group together just the functions that affect the lock and key situation.
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.
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.
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.