Elevator Saga Help and API documentation

About the game

This is a game of programming!
Your task is to program the movement of elevators, by writing a program in JavaScript.

The goal is to transport people in an efficient manner.
Depending on how well you do it, you can progress through the ever more difficult levels.
Only the very best programs will be able to complete all the levels.

How to play

If you have never written one of these programs before, start on the learning track, which the game also lists under Learning in the level menu at the top of the page. It is eight small buildings that introduce this API one mistake at a time: each hands you a program that loses, and asks you to find the one thing wrong with it, with hints and an explanation of what the run was actually doing.

Enter your code in the input window below the game view, and press the Start button to run it. There is nothing to apply first: your program is saved as you type, and every run reads it afresh. While a run is going that button reads Pause, and Start over beside it throws the run away and begins the level again with whatever the editor holds by then.
You can increase or decrease the speed of time by pressing the and buttons.

Beside the building is a panel that keeps score while a run is going. Eight of its rows need a word. Moves first. One move is counted each time a car crosses the halfway mark between one floor and the next, so a trip of three floors is three moves. A car that turns round mid-flight pays twice for the mark it crosses and re-crosses, and braking carries a car on across a mark it was turned back just short of. Three of the levels are judged on that number, totalled over every car in the building, as well as on the people delivered, so on those a car that shuttles about empty can lose the run. Under it, Stops, which counts something quite different. One stop is counted each time a car comes to rest at a floor and opens its doors, so a car sent to the floor it is already standing on counts another one. That trip of three floors is three moves and a single stop, and the two rows are worth reading against each other — a program that sends a car off to every call the moment it lights up shows many stops for few moves, and one that lets a car finish what it is already doing shows the reverse. Then People per stop. Everyone who got in or out, over the stops counted above, so opening the doors where nobody is waiting brings it down. Both ends of a journey count here, the boarding and the getting out, so the figure sits higher than the number a lift engineer would quote for the same building; what it is good for is the direction it moves in rather than the size of it. Then the clocks. Avg delivery time and Max delivery time both run from the moment a passenger appears in the building to the moment they step out of a car at the floor they asked for, so the ride counts in them as much as the wait for it does: somebody who walks straight into a car already standing at their floor, and waits not one second for it, still adds every second of a nineteen-floor journey to both. Nine of the game's levels and two of the tutorial ones are judged on the second of them, which is the largest total any one passenger has reached — it keeps climbing while somebody is still on their way, and once reached it never comes down again. Between the two of them sit the two halves that neither one names. Avg wait for a car first. The clock starts when a passenger appears and stops when a car takes them, and the row below it is the rest of the journey. Only the passengers a car has already reached are in that average, so it is not where somebody left standing on a floor turns up — the maximum is. The row below it is Avg ride time. The clock starts when a car takes a passenger and stops when they step out at their floor, so this and the wait above it add up to the delivery time. The three of them are the three spans the lift industry measures a real building by, and they only add up exactly once nobody is in flight: a passenger still riding has already put their wait into the one average and has nothing yet to put into the other. Last, Avg load. How full the cars were, averaged over the moves counted above, so a car standing still is not in the figure at all — parking costs nothing here, and in several of the levels it is the right thing to do. In an ordinary run the figure sits far below full, and that is not a fault to be fixed: cars are rarely full, and nothing in the game pays for filling them. Nor does a higher figure mean better play. Of three programs run on the same eighteen-floor building, the one that holds a car at its floor until it is nearly full before setting off got its cars to about 70% and delivered the fewest people of the three, at nearly twice the wait of the best of them, while the program that delivered the most carried the emptiest cars of all, under a half. What the number is good for is comparing two programs that deliver about the same: at equal numbers delivered, the one with the higher load did it with fewer trips that carried nobody.

Inside the editor, Ctrl+Enter starts the level again with what you have written, which is what the Start over button does; Ctrl+S writes the program to storage at once instead of waiting for the autosave, and keeps your browser's own save dialog out of the way; Tab indents, and Esc moves the focus back out of the editor.

If your program contains an error, you can use the developer tools in your web browser to try and debug it. If you want to start over with the code, press the Reset code button. This will revert the code to a working but simplistic implementation.
If you have a favorite text editor, such as Sublime Text, feel free to edit the code there and paste it into the game editor.
Your code is automatically saved in your local storage, so don't worry - it doesn't disappear if you accidentally close the browser.

Basics

Your code must declare a function called init. It can declare update alongside it, and anything else it needs. Like this:

function init(elevators, floors) {
    // Do stuff with the elevators and floors, which are both arrays of objects
}

function update(dt, elevators, floors) {
    // Do more stuff with the elevators and floors
    // dt is always the same fraction of a game second: update runs 100 times per
    // simulated second, however fast or slow the browser is actually drawing
}

These functions will then be called by the game during the level.
init runs once, on the first frame of the run rather than at the moment you apply your code, and update runs on that same frame and on every simulated step after it — 100 times per game second, on a fixed schedule tied to game time rather than to how often the browser draws. That means dt is always the same value, and two runs of the same seed and the same play take the exact same sequence of steps whether the browser is fast or slow. Both functions are handed the same two arrays — one holding every elevator in the building, one holding every floor — so elevators.length is how many cars you have to work with, and neither array is replaced between calls. Whatever else your program declares at the top level — a const, a let, a helper function — both of them can see, so that is where anything your program has to remember from one frame to the next goes. None of it reaches the page around the game, and every start of the level builds it again from scratch: a counter you left at 300 is back at 0 by the time init next runs.

Normally you will put most of your code in the init function, to set up event listeners and logic.

Solutions written for the original game declare an object instead, with init and update as its members and a pair of braces around the lot. Those still run here exactly as they are — paste one in and press Start over. In that form the two are called as methods of the object you wrote, so this inside them is that object, and that is where such a solution usually keeps what it remembers. It works as long as they are written with function: an arrow function keeps the this of wherever it was written, which here is the page rather than your object.

The game used to load jQuery and lodash, so older solutions you find on the wiki often call $ or _. Neither is loaded any more and neither is in scope for your program: a solution that uses them will fail with $ is not defined or _ is not defined. Most of what they were used for here — _.filter, _.map, _.each and friends — is a built-in array method (filter, map, forEach) in every browser that can run this game. _.min and _.max are the exception: Math.min is not an array method, and it takes its arguments one at a time rather than an array, so _.min(floorNums) has to become Math.min(...floorNums). Watch the empty array while you are at it: Math.min() with nothing to compare is Infinity rather than a floor, and goToFloor refuses it, so an empty getPressedFloors() needs an answer of its own.

Code examples

How to control an elevator

elevator.goToFloor(1);
Tell the elevator to move to floor 1 after completing other tasks, if any. A request for the floor already at the end of the queue that it would be added to is dropped, so asking for the same floor over and over does not pile up. That is the only case that is dropped: a floor that is somewhere else in the queue will be queued again.
if(elevator.currentFloor() > 2) { ... }
Calling currentFloor gets the floor number that the elevator currently is on. Note that this is a rounded number and does not necessarily mean the elevator is in a stopped state.

Listening for events

It is possible to listen for events, like when stopping at a floor, or a button has been pressed. Elevators and floors both understand on, once, one, off and offAll; see event methods below for what each of them does.

elevator.on("idle", function() { elevator.goToFloor(0); });
Listen for the "idle" event issued by the elevator, when the task queue has been emptied and the elevator is doing nothing. In this example we tell it to move to floor 0.
elevator.on("floor_button_pressed", function(floorNum) { ... } );
Listen for the "floor_button_pressed" event, issued when a passenger pressed a button inside the elevator. This indicates that the passenger wants to go to that floor.
floor.on("up_button_pressed", function(floor) { ... } );
Listen for the "up_button_pressed" event, issued when a passenger pressed the up button on the floor they are waiting on. This indicates that the passenger wants to go to another floor. The handler is passed the floor the button was pressed on.

Every elevator has its own events, so a handler registered on one elevator only ever hears that elevator: a building with four of them needs the handler registered four times, and elevators.forEach(function(elevator) { ... }) is the short way to write that. If everything your handlers do seems to happen to the last elevator instead, the loop that registered them is the place to look, not the elevators. for (var i = 0; i < elevators.length; i++) { var elevator = elevators[i]; elevator.on("idle", function() { elevator.goToFloor(0); }); } registers each handler on the right elevator, but var gives the whole function a single elevator binding, and every handler runs later, after the loop has finished — by then that one binding holds the elevator the loop ended on, and that is the elevator all of them drive. let and const give each iteration a binding of its own, so for (const elevator of elevators) and forEach do what the var version looks like it does. Floors work the same way, and so does anything else a handler closes over in a loop.

API documentation

Event methods

Every elevator and every floor is an event emitter, and these are the methods it gives you. They all return the object they were called on, so they can be chained.

Method Explanation Example
on Register a listener. Listeners run in the order they were registered, and the same function may be registered more than once. Several event names separated by spaces register the same listener for all of them; when you name more than one, the listener is called with the name of the event that fired as its first argument, ahead of that event's own arguments.
elevator.on("idle", function() { ... });
floor.on("up_button_pressed down_button_pressed", function(eventName) { ... });
once Register a listener that runs at most once and is then removed. It is removed before it runs, so triggering the same event from inside it will not run it again. Takes a single event name.
elevator.once("stopped_at_floor", function(floorNum) { ... });
one The older name for once, and the one the original game gave you. Same behavior, single event name as well.
elevator.one("stopped_at_floor", function(floorNum) { ... });
off Remove listeners. With a function, removes just that function, however it was registered; without one, removes every listener of the named events. Accepts space separated names like on does, and the single name "*" for every event at once — a function passed alongside the star is ignored. You need a reference to the function you registered, so an inline anonymous function cannot be removed.
function goHome() { elevator.goToFloor(0); }
elevator.on("idle", goHome);
elevator.off("idle", goHome); // Just this one
elevator.off("idle"); // Every idle listener
elevator.off("*"); // Every listener, of every event
offAll Remove every listener you registered, for every event, on that elevator or floor. The listeners the game itself needs are separate, so the object keeps working — anything you register afterwards still fires. This is off("*") with a name of its own.
elevator.offAll();
floor.offAll();

You rarely need to remove listeners: the elevators and floors are thrown away when a level restarts, and your init is called afresh on the new ones. Removing is useful when you want a listener to apply only for a while.

Elevator object

Property Type Explanation Example
goToFloor function Queue the elevator to go to specified floor number. If you specify true as second argument, the elevator will go to that floor directly, and then go to any other queued floors. The request is dropped if the floor is already at the end of the queue it would join: the back for a normal call, the front for a call with true. The same floor further along the queue is queued again. A floor number outside the building is clamped to the nearest real floor, but a value that is not a number at all — NaN, undefined, a string that does not parse — is refused and reported as an error in your code.
elevator.goToFloor(3); // Do it after anything else -- queue: 3
elevator.goToFloor(2, true); // Do it before anything else -- queue: 2, 3
elevator.goToFloor(3); // Dropped, 3 is already last
elevator.goToFloor(2, true); // Dropped, 2 is already first
elevator.goToFloor(2); // Queued anyway -- queue: 2, 3, 2
stop function Clear the destination queue and stop the elevator if it is moving. Note that you normally don't need to stop elevators - it is intended for advanced solutions with in-transit rescheduling logic. Also, note that the elevator will probably not stop at a floor, so passengers will not get out.
elevator.stop();
currentFloor function Gets the floor number that the elevator currently is on.
if(elevator.currentFloor() === 0) {
    // Do something special?
}
goingUpIndicator function Gets or sets the going up indicator, which will affect passenger behavior when stopping at floors.
if(elevator.goingUpIndicator()) {
    elevator.goingDownIndicator(false);
}
goingDownIndicator function Gets or sets the going down indicator, which will affect passenger behavior when stopping at floors.
if(elevator.goingDownIndicator()) {
    elevator.goingUpIndicator(false);
}
maxPassengerCount function Gets the maximum number of passengers that can occupy the elevator at the same time.
if(elevator.maxPassengerCount() > 5) {
    // Use this elevator for something special, because it's big
}
loadFactor function Gets the load factor of the elevator. 0 means empty, 1 means full. Varies with passenger weights, which vary - not an exact measure.
if(elevator.loadFactor() < 0.4) {
    // Maybe use this elevator, since it's not full yet?
}
isFull function Gets whether every spot in the elevator is taken. Use this rather than comparing loadFactor to 1 - passenger weights vary, so a completely full elevator only reads about 0.775 on average. Someone who has started walking in counts, since they have taken their spot already.
if(!elevator.isFull()) {
    // Maybe pick someone up on the way?
}
isEmpty function Gets whether the elevator is carrying nobody at all. Not the opposite of isFull - an elevator with one passenger out of four is neither.
if(elevator.isEmpty()) {
    // Nobody on board - go wait somewhere useful?
}
destinationDirection function Gets the direction the elevator is currently going to move toward. Can be "up", "down" or "stopped".
isApproachingFloor function Gets whether the elevator is moving toward the given floor and has not passed it yet. Only the direction of travel counts, so a floor further along that way is approaching too, even if the elevator is going to stop before it. It is the same test the game itself makes before triggering passing_floor, so a floor this says no to can no longer raise that event. An elevator standing still is approaching nothing, and neither is one that has arrived at the floor you ask about. A floor number outside the building is clamped to the nearest real floor, and a value that is not a number at all - including a forgotten argument - is refused and reported as an error in your code, just like goToFloor.
if(elevator.isApproachingFloor(2)) {
    // Maybe stop and pick up whoever is waiting there?
}
destinationQueue array The current destination queue, meaning the floor numbers the elevator is scheduled to go to. Can be modified and emptied if desired. Note that you need to call checkDestinationQueue() for the change to take effect immediately. The next time the queue is checked, every entry in it that is not a finite number is dropped in one pass, and the first of them is reported as an error in your code - only the first, and only once per elevator. A finite floor number outside the building is left where it is, and sends the elevator past the end of the shaft.
elevator.destinationQueue = [];
elevator.checkDestinationQueue();
checkDestinationQueue function Checks the destination queue for any new destinations to go to. Note that you only need to call this if you modify the destination queue explicitly.
elevator.checkDestinationQueue();
getPressedFloors function Gets the currently pressed floor numbers as an array.
if(elevator.getPressedFloors().length > 0) {
    // Maybe go to some chosen floor first?
}
servedFloors function Gets the floors this elevator serves, as an array in ascending order. In a zoned building an elevator only carries passengers between the floors of its own zone, and its arrival clears no call button elsewhere. goToFloor will still send it anywhere, but such a trip carries nobody and still costs moves. An elevator with no zone of its own reports every floor in the building.
if(elevator.servedFloors().includes(floorNum)) {
    // Sending it anywhere else is a trip that carries nobody.
    elevator.goToFloor(floorNum);
}
takeRequest function Books this elevator for a journey somebody asked for, in a building where passengers announce a destination instead of pressing a call button. The people waiting on the first floor for the second one will board this elevator and no other, whichever way its indicators point. Booking does not move the elevator: send it with goToFloor, first to fetch them and then to where they are going. Returns false when there is no such journey to take, because nobody is waiting for it or because this elevator does not serve both ends of it.
floor.on("destination_requested", function(destinationFloor, floor) {
    if(elevator.takeRequest(floor.floorNum(), destinationFloor)) {
        // Booking picks the elevator; these two lines send it.
        elevator.goToFloor(floor.floorNum());
        elevator.goToFloor(destinationFloor);
    }
})
Event Explanation Example
idle Triggered when the elevator has completed all its tasks and is not doing anything.
elevator.on("idle", function() { ... });
floor_button_pressed Triggered when a passenger has pressed a button inside the elevator.
elevator.on("floor_button_pressed", function(floorNum) {
    // Maybe tell the elevator to go to that floor?
})
passing_floor Triggered slightly before the elevator will pass a floor. A good time to decide whether to stop at that floor. Note that this event is not triggered for the destination floor. Direction is either "up" or "down".
elevator.on("passing_floor", function(floorNum, direction) { ... });
stopped_at_floor Triggered when the elevator has arrived at a floor.
elevator.on("stopped_at_floor", function(floorNum) {
    // Maybe decide where to go next?
})

Floor object

Property Type Explanation Example
floorNum function Gets the floor number of the floor object.
if(floor.floorNum() > 3) { ... }
pendingDestinations function Gets the journeys people on this floor have asked for and are still waiting on, as an array in ascending floor order. Each entry has floorNum, where they are going, and waiting, how many of them are going there. What buttonStates is to a building with call buttons: everything the floor is asking for right now, rather than only what was announced when destination_requested fired. A request stays here until somebody boards an elevator for it, so this is where you find a request you booked an elevator for and then never sent it to fetch. Empty in a building with call buttons.
floor.pendingDestinations().forEach(function(request) {
    // Still nobody on the way to fetch this one?
    elevator.takeRequest(floor.floorNum(), request.floorNum);
})
Event Explanation Example
up_button_pressed Triggered when someone has pressed the up button at a floor. Note that passengers will press the button again if they fail to enter an elevator. The handler is passed the floor the button was pressed on.
floor.on("up_button_pressed", function(floor) {
    // Maybe tell an elevator to go to this floor?
})
down_button_pressed Triggered when someone has pressed the down button at a floor. Note that passengers will press the button again if they fail to enter an elevator. The handler is passed the floor the button was pressed on.
floor.on("down_button_pressed", function(floor) {
    // Maybe tell an elevator to go to this floor?
})
hall_button_pressed Triggered when someone has pressed either call button at a floor. Note that passengers will press the button again if they fail to enter an elevator. The handler is passed the direction that was asked for, either "up" or "down", and the floor the button was pressed on. It always follows the up_button_pressed or down_button_pressed event for the same press, never precedes it, so a program listening for both hears about that press twice.
floor.on("hall_button_pressed", function(direction, floor) {
    // Maybe send an elevator that is already going that way?
})
buttonstate_change Triggered when either call button at a floor was lit or cleared. The handler is passed the state of both buttons: an object with an up and a down property, each either "activated" or the empty string. It is a snapshot taken when the event fired, so holding on to it will not show you later presses.
floor.on("buttonstate_change", function(buttonStates) {
    if(buttonStates.up === "" && buttonStates.down === "") {
        // Nobody is waiting here any more?
    }
})
destination_requested Triggered when someone at a floor has asked to be taken to another floor, in a building whose passengers announce a destination instead of pressing a call button. The handler is passed the floor they want to reach and the floor they are waiting on. Someone joining a journey an elevator has already been assigned to is not announced, and the event is raised again if that elevator arrives full.
floor.on("destination_requested", function(destinationFloor, floor) {
    // Maybe pick an elevator for this journey?
})