Elevator Saga Справка и документация по API

Об игре

Это игра про программирование!
Ваша задача — управлять движением лифтов, написав программу на JavaScript.

Цель — возить пассажиров эффективно.
Чем лучше это получается, тем дальше вы продвигаетесь по всё более сложным уровням.
Пройти все уровни под силу только самым лучшим программам.

Как играть

Если вы никогда не писали таких программ, начните с учебной дорожки — в меню уровней наверху страницы её уроки собраны под заголовком Обучение. Это восемь небольших зданий, которые знакомят с этим API по одной ошибке за раз: в каждом выдаётся программа, которая проигрывает, и нужно найти в ней единственную ошибку — рядом есть подсказки и разбор того, что на самом деле происходило в прогоне.

Напишите код в окне под игровым полем и нажмите кнопку Старт, чтобы запустить его. Ничего применять заранее не нужно: программа сохраняется сама, пока вы печатаете, и каждый прогон читает её заново. Во время прогона та же кнопка читается как Пауза, а соседняя С начала обрывает прогон и начинает уровень сначала с тем кодом, что к этому времени в редакторе.
Скорость времени можно увеличивать и уменьшать кнопками и .

Рядом со зданием есть панель, которая ведёт счёт по ходу прогона. Восемь строк в ней стоит пояснить. Сначала Перемещения. Перемещение засчитывается каждый раз, когда кабина проходит середину пути от одного этажа до соседнего: проехать три этажа — это три перемещения. Кабина, повернувшая назад уже за серединой, проходит её дважды, и засчитываются оба раза; тормозит она не мгновенно, так что и поворот назад незадолго до середины обычно обходится в те же два. На трёх уровнях оценивается не только число перевезённых пассажиров, но и число перемещений — одно на все лифты здания, — так что там кабина, которая катается вхолостую, способна провалить прогон. Под ними — Остановки, и считают они совсем другое. Остановка засчитывается каждый раз, когда кабина замирает на этаже и открывает двери, так что кабина, отправленная на этаж, где она и так стоит, добавляет ещё одну. Те самые три этажа — это три перемещения и одна остановка, и две строки стоит читать друг против друга: программа, которая шлёт кабину на каждый вызов, едва тот загорелся, наберёт много остановок при малом числе перемещений, а та, что даёт кабине закончить начатое, — наоборот. Дальше — Людей на остановку. Все, кто вошёл или вышел, поделённые на остановки из строки выше, так что открытые двери там, где никого нет, эту цифру снижают. Считаются оба конца пути, и посадка, и выход, поэтому цифра выходит выше той, которую для того же дома назвал бы лифтовик; полезна она направлением, в котором движется, а не своей величиной. Дальше два времени. Сред. доставка и Макс. доставка отсчитываются от момента, когда пассажир появился в здании, до момента, когда он вышел из кабины на нужном ему этаже, так что поездка входит в них наравне с ожиданием: тот, кто сразу зашёл в стоявшую у его этажа кабину и не ждал ни секунды, всё равно добавит к обеим цифрам каждую секунду поездки на девятнадцать этажей. По второй из них оцениваются девять уровней игры и два учебных уровня; это наибольшее время, которое набрал хоть один пассажир, — пока кто-то ещё в пути, оно продолжает расти, а набранного уже не теряет. Между ними стоят обе половины, которыми ни та ни другая цифра не является. Сначала Сред. ожидание кабины. Отсчёт идёт от появления пассажира до того момента, как его забрала кабина, а строка под ней — это остальная часть пути. В среднее попадают только те, до кого кабина уже доехала, так что тот, кто так и остался стоять на этаже, виден не здесь, а в максимуме. Строка под ней — Сред. время поездки. Отсчёт идёт от того момента, как кабина забрала пассажира, до того, как он вышел на своём этаже, так что эта строка и ожидание над ней вместе дают время доставки. Все три — те самые три отрезка, которыми лифтовое дело меряет настоящее здание, и сходятся они в точности лишь тогда, когда никто не едет: у того, кто ещё в кабине, ожидание в среднее уже попало, а поездке пока попасть некуда. Наконец, Сред. загрузка. Насколько полными были кабины — в среднем по тем же перемещениям, что считаются выше, так что стоящая кабина в цифру не попадает вовсе: за простой здесь ничего не снимается, а на нескольких уровнях он и есть верный ход. В обычном прогоне цифра держится далеко от полной кабины, и исправлять тут нечего: кабины редко бывают полными, и за то, чтобы их набить, игра ничего не даёт. И выше — не значит лучше. Из трёх программ, прогнанных на одном и том же восемнадцатиэтажном здании, та, что держит кабину на этаже, пока она не наберётся почти полной, дошла до загрузки около семи десятых — и перевезла меньше всех троих, а ждали у неё почти вдвое дольше, чем у лучшей; у лучшей же кабины оказались самыми пустыми, меньше половины. Полезна эта цифра для сравнения двух программ, которые перевозят примерно поровну: при равном числе перевезённых та, у которой загрузка выше, обошлась меньшим числом пустых рейсов.

В редакторе Ctrl+Enter начинает уровень заново с тем, что вы написали, — то же самое делает кнопка С начала; Ctrl+S записывает программу в хранилище сразу, не дожидаясь автосохранения, и не даёт открыться окну сохранения браузера; Tab добавляет отступ, а Esc убирает фокус из редактора.

Если в программе ошибка, попробуйте разобраться с ней через инструменты разработчика в браузере. Чтобы начать с чистого листа, нажмите кнопку Сбросить код: код вернётся к рабочей, но совсем простой реализации.
Если у вас есть любимый текстовый редактор, например Sublime Text, пишите код в нём и вставляйте в редактор игры.
Код сам сохраняется в локальном хранилище браузера, так что не переживайте — он не пропадёт, если вы случайно закроете браузер.

Основы

Ваш код должен объявлять функцию init. Рядом можно объявить update — и всё остальное, что понадобится. Вот так:

function init(elevators, floors) {
    // Делайте что-нибудь с лифтами и этажами: и те и другие — массивы объектов
}

function update(dt, elevators, floors) {
    // Ещё что-нибудь с лифтами и этажами
    // dt — всегда одна и та же доля игровой секунды: update вызывается 100 раз в
    // секунду игрового времени, независимо от того, как быстро на самом деле рисует браузер
}

Эти функции игра вызывает по ходу уровня.
init вызывается один раз, на первом кадре прогона, а не в момент, когда вы применили код; update — на том же шаге и на каждом следующем шаге симуляции — 100 раз в игровую секунду, по расписанию, привязанному к игровому времени, а не к тому, как часто рисует браузер. Поэтому dt всегда одно и то же значение, а два прогона с одним и тем же сидом и одинаковыми действиями проходят одну и ту же последовательность шагов — независимо от того, быстрый браузер или медленный. Обеим передаются одни и те же два массива — все лифты здания и все его этажи, — так что elevators.length и есть число кабин, которыми вы распоряжаетесь; между вызовами эти массивы не подменяются. Всё остальное, что программа объявляет на верхнем уровне, — const, let, вспомогательную функцию — видят обе, так что именно там и хранится всё, что программе нужно помнить от кадра к кадру. Наружу, на страницу игры, из этого не попадает ничего, а каждый запуск уровня строит всё заново: счётчик, оставшийся на 300, к следующему вызову init снова равен 0.

Обычно основную часть кода пишут в функции init: там настраивают обработчики событий и логику.

Решения, написанные для оригинальной игры, объявляют вместо этого объект: init и update — его поля, а вокруг всего стоит пара фигурных скобок. Такие решения работают здесь как есть — вставьте и нажмите Начать заново. В этой форме обе функции вызываются как методы объявленного вами объекта, так что this внутри них — этот самый объект, и обычно такое решение хранит на нём всё, что помнит. Это верно, пока они написаны через function: стрелочная функция сохраняет this того места, где её написали, а здесь это страница, а не ваш объект.

Раньше игра подключала jQuery и lodash, поэтому в старых решениях с вики часто встречаются $ и _. Ни та, ни другая библиотека больше не подключается, и вашей программе они недоступны: решение с ними упадёт с ошибкой $ is not defined или _ is not defined. Почти всё, ради чего они здесь были нужны, — _.filter, _.map, _.each и им подобные — есть у самих массивов (filter, map, forEach) в любом браузере, который потянет эту игру. Исключение — _.min и _.max: Math.min методом массива не является и принимает аргументы по одному, а не массивом, поэтому вместо _.min(floorNums) придётся писать Math.min(...floorNums). Заодно следите за пустым массивом: Math.min() без единого аргумента возвращает Infinity, а не этаж, и goToFloor такое значение не примет, так что случай пустого getPressedFloors() придётся разобрать отдельно.

Примеры кода

Как управлять лифтом

elevator.goToFloor(1);
Отправить лифт на этаж 1 после всех остальных дел, если они есть. Если этот этаж уже стоит в том конце очереди, куда его собирались добавить, запрос отбрасывается — так что повторные запросы одного и того же этажа не копятся. Это единственный случай, когда запрос отбрасывается: этаж, стоящий в очереди где-то ещё, добавится ещё раз.
if(elevator.currentFloor() > 2) { ... }
Вызов currentFloor возвращает этаж, на котором лифт сейчас находится. Учтите, что это округлённое число и оно не обязательно означает, что лифт стоит.

Как слушать события

События можно слушать — например, остановку на этаже или нажатие кнопки. И лифты, и этажи понимают on, once, one, off и offAll; что делает каждый из них, написано ниже, в разделе методы событий.

elevator.on("idle", function() { elevator.goToFloor(0); });
Слушаем событие "idle": лифт присылает его, когда очередь задач опустела и делать ему нечего. В этом примере мы отправляем лифт на этаж 0.
elevator.on("floor_button_pressed", function(floorNum) { ... } );
Слушаем событие "floor_button_pressed": оно приходит, когда пассажир нажал кнопку внутри лифта. Значит, он хочет попасть на этот этаж.
floor.on("up_button_pressed", function(floor) { ... } );
Слушаем событие "up_button_pressed": оно приходит, когда пассажир нажал кнопку вызова вверх на этаже, где он ждёт. Значит, он хочет уехать на другой этаж. Обработчику передаётся этаж, на котором нажали кнопку.

У каждого лифта свои события, поэтому обработчик, подписанный на один лифт, слышит только его: в здании из четырёх лифтов обработчик придётся подписать четыре раза, и короче всего это записывается как elevators.forEach(function(elevator) { ... }). Если же все обработчики словно управляют одним и тем же, последним лифтом, искать надо не в лифтах, а в цикле, который их подписывал. for (var i = 0; i < elevators.length; i++) { var elevator = elevators[i]; elevator.on("idle", function() { elevator.goToFloor(0); }); } подписывает каждый обработчик на свой лифт, но var заводит одну-единственную переменную elevator на всю функцию, а срабатывают обработчики позже, когда цикл давно закончился, — и к этому моменту в ней лежит лифт, на котором цикл остановился, им они все и управляют. let и const заводят на каждой итерации свою переменную, поэтому for (const elevator of elevators) и forEach делают то, чего ждёшь от варианта с var, но не получаешь. С этажами всё устроено так же — и с любой другой переменной, которую обработчик захватывает в замыкание внутри цикла.

Документация по API

Методы событий

Каждый лифт и каждый этаж — источник событий, и вот какие методы он вам даёт. Все они возвращают объект, у которого были вызваны, так что вызовы можно собирать в цепочку.

Метод Описание Пример
on Подписать обработчик. Обработчики вызываются в порядке подписки, и одну и ту же функцию можно подписать несколько раз. Несколько имён событий через пробел подписывают один обработчик сразу на все; если имён больше одного, первым аргументом обработчику приходит имя сработавшего события, а за ним — аргументы самого события.
elevator.on("idle", function() { ... });
floor.on("up_button_pressed down_button_pressed", function(eventName) { ... });
once Подписать обработчик, который сработает не больше одного раза и будет снят. Снимается он до вызова, поэтому то же событие, вызванное изнутри него, второй раз его не запустит. Принимает одно имя события.
elevator.once("stopped_at_floor", function(floorNum) { ... });
one Старое имя для once — то самое, что было в оригинальной игре. Ведёт себя так же и тоже принимает одно имя события.
elevator.one("stopped_at_floor", function(floorNum) { ... });
off Снять обработчики. Если передать функцию, снимется только она, как бы она ни была подписана; если не передавать — снимутся все обработчики названных событий. Имена можно перечислять через пробел, как в on, а единственное имя "*" снимает обработчики всех событий сразу — функция, переданная вместе со звёздочкой, игнорируется. Нужна ссылка на подписанную функцию, поэтому анонимную функцию, объявленную прямо в вызове, снять нельзя.
function goHome() { elevator.goToFloor(0); }
elevator.on("idle", goHome);
elevator.off("idle", goHome); // Только этот
elevator.off("idle"); // Все обработчики idle
elevator.off("*"); // Все обработчики всех событий
offAll Снять все обработчики, которые подписали вы, на все события этого лифта или этажа. Обработчики, нужные самой игре, живут отдельно, так что объект продолжает работать — и всё, что вы подпишете потом, будет срабатывать как обычно. Это off("*") под собственным именем.
elevator.offAll();
floor.offAll();

Снимать обработчики обычно не нужно: при перезапуске уровня лифты и этажи выбрасываются, а ваш init вызывается заново уже на новых. Снятие пригодится, когда обработчик должен действовать только какое-то время.

Объект лифта

Свойство Тип Описание Пример
goToFloor функция Поставить в очередь поездку лифта на указанный этаж. Если вторым аргументом передать true, лифт поедет туда сразу, а уже потом — по остальным этажам из очереди. Запрос отбрасывается, если этаж уже стоит в том конце очереди, куда он попал бы: в хвосте при обычном вызове и в голове при вызове с true. Тот же этаж дальше по очереди добавится ещё раз. Номер этажа за пределами здания подтягивается к ближайшему существующему этажу, а вот значение, которое числом не является вовсе, — NaN, undefined, строка, которую не удаётся преобразовать в число, — не принимается: игра сообщает об ошибке в вашем коде.
elevator.goToFloor(3); // Поехать после всего остального — очередь: 3
elevator.goToFloor(2, true); // Поехать раньше всего остального — очередь: 2, 3
elevator.goToFloor(3); // Отброшено: 3 уже последний
elevator.goToFloor(2, true); // Отброшено: 2 уже первый
elevator.goToFloor(2); // Всё равно добавится — очередь: 2, 3, 2
stop функция Очистить очередь этажей и остановить лифт, если он едет. Обычно останавливать лифты не нужно — это на случай продвинутых решений, которые перестраивают маршрут на ходу. И учтите, что лифт, скорее всего, встанет не на этаже, так что пассажиры не выйдут.
elevator.stop();
currentFloor функция Возвращает этаж, на котором лифт сейчас находится.
if(elevator.currentFloor() === 0) {
    // Сделать что-нибудь особенное?
}
goingUpIndicator функция Возвращает или задаёт индикатор движения вверх — от него зависит, как поведут себя пассажиры при остановке на этаже.
if(elevator.goingUpIndicator()) {
    elevator.goingDownIndicator(false);
}
goingDownIndicator функция Возвращает или задаёт индикатор движения вниз — от него зависит, как поведут себя пассажиры при остановке на этаже.
if(elevator.goingDownIndicator()) {
    elevator.goingUpIndicator(false);
}
maxPassengerCount функция Возвращает, сколько пассажиров помещается в лифт одновременно.
if(elevator.maxPassengerCount() > 5) {
    // Приспособить этот лифт под что-то особенное — он большой
}
loadFactor функция Возвращает загрузку лифта: 0 — пустой, 1 — полный. Зависит от веса пассажиров, а он разный, так что мера неточная.
if(elevator.loadFactor() < 0.4) {
    // Может, взять этот лифт — он ещё не полный?
}
isFull функция Возвращает, все ли места в лифте заняты. Пользуйтесь этим, а не сравнением загрузки с 1: вес пассажиров разный, поэтому у полностью набитого лифта загрузка в среднем всего около 0,775. Тот, кто только начал заходить, уже считается — место он занял.
if(!elevator.isFull()) {
    // Может, подобрать кого-нибудь по пути?
}
isEmpty функция Возвращает, пуст ли лифт. Это не противоположность isFull: лифт с одним пассажиром из четырёх не подходит ни под то, ни под другое.
if(elevator.isEmpty()) {
    // Никого на борту — поехать ждать туда, где будет полезнее?
}
destinationDirection функция Возвращает направление, в котором лифт собирается ехать. Возможные значения — "up", "down" и "stopped".
isApproachingFloor функция Возвращает, едет ли лифт к указанному этажу и не проехал ли его. Учитывается только направление движения, так что лифт считается приближающимся и к тем этажам, которые лежат дальше по ходу движения, даже если он остановится раньше. Это та же проверка, которую делает сама игра перед событием passing_floor, так что если она ответила «нет», для этого этажа событие уже не сработает. Стоящий лифт не приближается ни к чему, и тот, что уже приехал на нужный этаж, — тоже. Номер этажа за пределами здания подтягивается к ближайшему существующему этажу, а значение, которое числом не является вовсе (в том числе забытый аргумент), не принимается: игра сообщает об ошибке в вашем коде, как и в goToFloor.
if(elevator.isApproachingFloor(2)) {
    // Может, остановиться и забрать тех, кто там ждёт?
}
destinationQueue массив Текущая очередь этажей — номера этажей, на которые лифт собирается заехать. Её можно менять и очищать. Учтите, что после изменения нужно вызвать checkDestinationQueue(), чтобы оно подействовало сразу. При ближайшей проверке очереди все записи, которые конечными числами не являются, отбрасываются разом, а игра сообщает об ошибке в вашем коде, называя только первую такую запись, — и только один раз для каждого лифта. Номер этажа за пределами здания, если это конечное число, из очереди не убирается: лифт просто уедет за пределы шахты.
elevator.destinationQueue = [];
elevator.checkDestinationQueue();
checkDestinationQueue функция Смотрит, не появилось ли в очереди этажей новых пунктов назначения. Вызывать это нужно, только если вы меняли очередь вручную.
elevator.checkDestinationQueue();
getPressedFloors функция Возвращает массив номеров нажатых этажей.
if(elevator.getPressedFloors().length > 0) {
    // Может, сначала заехать на один из выбранных этажей?
}
servedFloors функция Возвращает массив этажей, которые обслуживает лифт, по возрастанию. В здании с зонами лифт возит пассажиров только между этажами своей зоны, а на остальных этажах его приезд не гасит кнопку вызова. goToFloor по-прежнему увезёт его куда угодно, но в такой поездке никто не поедет, а ходы за неё всё равно спишутся. Лифт без своей зоны возвращает все этажи здания.
if(elevator.servedFloors().includes(floorNum)) {
    // На любой другой этаж это поездка, в которой никто не поедет.
    elevator.goToFloor(floorNum);
}
takeRequest функция Закрепляет за лифтом поездку, о которой попросили, — в здании, где пассажиры называют этаж назначения вместо кнопки вызова. Те, кто ждёт на первом указанном этаже поездки до второго, сядут именно в этот лифт и ни в какой другой, куда бы ни смотрели его индикаторы. Закрепление не двигает лифт: отправьте его через goToFloor — сначала за пассажирами, потом туда, куда они едут. Возвращает false, если такой поездки нет: её никто не ждёт или лифт не обслуживает оба её конца.
floor.on("destination_requested", function(destinationFloor, floor) {
    if(elevator.takeRequest(floor.floorNum(), destinationFloor)) {
        // Закрепление выбирает лифт, а эти две строки его отправляют.
        elevator.goToFloor(floor.floorNum());
        elevator.goToFloor(destinationFloor);
    }
})
Событие Описание Пример
idle Срабатывает, когда лифт выполнил все задачи и ничем не занят.
elevator.on("idle", function() { ... });
floor_button_pressed Срабатывает, когда пассажир нажал кнопку внутри лифта.
elevator.on("floor_button_pressed", function(floorNum) {
    // Может, отправить лифт на этот этаж?
})
passing_floor Срабатывает незадолго до того, как лифт проедет мимо этажа. Удобный момент, чтобы решить, останавливаться ли на нём. Учтите, что для этажа назначения это событие не срабатывает. Направление — "up" или "down".
elevator.on("passing_floor", function(floorNum, direction) { ... });
stopped_at_floor Срабатывает, когда лифт приехал на этаж.
elevator.on("stopped_at_floor", function(floorNum) {
    // Может, решить, куда ехать дальше?
})

Объект этажа

Свойство Тип Описание Пример
floorNum функция Возвращает номер этажа.
if(floor.floorNum() > 3) { ... }
pendingDestinations функция Возвращает поездки, о которых попросили с этого этажа и которых всё ещё ждут, — массив по возрастанию этажа. У каждого элемента есть floorNum, куда едут, и waiting, сколько человек туда едет. То же для здания без кнопок вызова, что и buttonStates для обычного: всё, о чём этаж просит прямо сейчас, а не только то, что прозвучало в момент destination_requested. Просьба остаётся здесь, пока по ней кто-нибудь не сядет в лифт, так что именно тут находится просьба, за которой вы закрепили лифт, но так и не отправили его. В здании с кнопками вызова массив пуст.
floor.pendingDestinations().forEach(function(request) {
    // За этой поездкой всё ещё никто не едет?
    elevator.takeRequest(floor.floorNum(), request.floorNum);
})
Событие Описание Пример
up_button_pressed Срабатывает, когда на этаже нажали кнопку вызова вверх. Учтите, что пассажиры нажмут её снова, если не смогли зайти в лифт. Обработчику передаётся этаж, на котором нажали кнопку.
floor.on("up_button_pressed", function(floor) {
    // Может, отправить сюда какой-нибудь лифт?
})
down_button_pressed Срабатывает, когда на этаже нажали кнопку вызова вниз. Учтите, что пассажиры нажмут её снова, если не смогли зайти в лифт. Обработчику передаётся этаж, на котором нажали кнопку.
floor.on("down_button_pressed", function(floor) {
    // Может, отправить сюда какой-нибудь лифт?
})
hall_button_pressed Срабатывает, когда на этаже нажали любую из кнопок вызова. Учтите, что пассажиры нажмут её снова, если не смогли зайти в лифт. Обработчику передаётся направление вызова — "up" или "down" — и этаж, на котором нажали кнопку. Всегда приходит после события up_button_pressed или down_button_pressed для того же нажатия и никогда раньше него, так что программа, подписанная на оба, узнает об этом нажатии дважды.
floor.on("hall_button_pressed", function(direction, floor) {
    // Может, отправить лифт, который и так едет в эту сторону?
})
buttonstate_change Срабатывает, когда одна из кнопок вызова на этаже загорелась или погасла. Обработчику передаётся состояние обеих кнопок: объект со свойствами up и down, каждое из которых — либо "activated", либо пустая строка. Это снимок на момент события, так что сохранённый объект о более поздних нажатиях не расскажет.
floor.on("buttonstate_change", function(buttonStates) {
    if(buttonStates.up === "" && buttonStates.down === "") {
        // Здесь больше никто не ждёт?
    }
})
destination_requested Срабатывает, когда пассажир на этаже попросил отвезти его на другой этаж — в здании, где вместо кнопок вызова называют этаж назначения. Обработчику передаётся нужный пассажиру этаж и этаж, на котором он ждёт. О пассажире, который присоединился к поездке с уже назначенным лифтом, событие молчит, а если этот лифт приехал полным, оно срабатывает снова.
floor.on("destination_requested", function(destinationFloor, floor) {
    // Может, выбрать лифт для этой поездки?
})