Към съдържанието
wannacode

Заключената лаборатория 2: загадки и изход

Проблем

Всички врати са отворени

В първия урок лабораторията е заключена само на думи: играчът обикаля където си иска. Ключът и бележката си стоят, но не служат за нищо. Играта няма край.

Ескейп играта има нужда от:

  • заключена врата, която се отваря с предмет;
  • кодова ключалка, чийто код се сглобява от улики в различни стаи;
  • край: играчът е навън, и брой ходове, с които да се похвали;
  • код за продължение: играта е дълга и играчът иска да спре и да продължи после.

Кодът за продължение е кратък текст. Играта го печата, а играчът го пише обратно. Ако текстът е развален, играта трябва да го каже, а не да се срине.

Идеята

Заключена врата

Стаята получава ключалка. Тайната е името на предмета, който я отваря, или кодът:

private String secret = null;
private boolean codeLock = false;
private boolean locked = false;
public void lockWithKey(String keyName) {
secret = keyName;
codeLock = false;
locked = true;
}
public void lockWithCode(String code) {
secret = code;
codeLock = true;
locked = true;
}
public boolean isLocked() {
return locked;
}
public boolean isCodeLock() {
return codeLock;
}
public boolean opensWith(String attempt) {
return locked && secret.equals(attempt);
}
public void unlock() {
locked = false;
}

„иди“ вече спира пред заключена врата:

class GoCommand implements Command {
@Override
public String execute(Game game, String argument) {
Room next = game.getCurrent().getExit(argument);
if (next == null) {
return "Натам няма изход.";
}
if (next.isLocked()) {
return "Вратата към " + next.getName() + " е заключена.";
}
game.setCurrent(next);
return next.describe();
}
}

В света кабинетът е заключен с ключа от склада, а новото фоайе с код:

Room foyer = new Room("фоайе", "Стъклената врата към улицата е отворена.");
corridor.addExit("север", foyer);
foyer.addExit("юг", corridor);
office.lockWithKey("ключ");
foyer.lockWithCode("4719");

Идеята

Ключът отключва

„използвай ключ“ проверява раницата, после търси заключена съседна врата, която се отваря с този предмет:

class UseCommand implements Command {
@Override
public String execute(Game game, String argument) {
if (!game.has(argument)) {
return "Нямаш " + argument + ".";
}
for (Room room : game.getCurrent().getNeighbours()) {
if (!room.isCodeLock() && room.opensWith(argument)) {
game.unlock(room);
return "Отключи вратата към " + room.getName() + ".";
}
}
return "Тук " + argument + " не помага.";
}
}

Отключването минава през играта, а не направо през стаята. Така играта помни кои врати са отворени. Това ще трябва за кода за продължение.

Пробвай

Кой ключ, коя врата?

Играчът пише тези команди от началото на играта. Какво отговаря играта на трите „използвай ключ“, подред?

иди север
използвай ключ
иди изток
вземи ключ
използвай ключ
иди запад
използвай ключ
Отговор

Идеята

Кодова ключалка от улики

Фоайето е заключено с четири цифри. Играта не ги казва направо:

  • бележката в кабинета: „Кодът на фоайето: 47 и денят на инвентаризацията.“;
  • календарът в склада: „Инвентаризация – 19 октомври“.

Кодът е 4719. Команда „въведи“ го проверява срещу кодовата ключалка на съседна врата:

class CodeCommand implements Command {
@Override
public String execute(Game game, String argument) {
for (Room room : game.getCurrent().getNeighbours()) {
if (room.isCodeLock() && room.isLocked()) {
if (room.opensWith(argument)) {
game.unlock(room);
return "Ключалката щракна. Вратата към " + room.getName() + " е отворена.";
}
return "Грешен код. Ключалката светва в червено.";
}
}
return "Тук няма кодова ключалка.";
}
}

Загадката е в данните, а не в кода: друг код и други улики правят друга игра със същите класове.

Виж го

Навън

Играта е спечелена, когато играчът стигне фоайето:

public boolean isWon() {
return current.getName().equals("фоайе");
}

handle брои всяка разбрана команда като ход. Цикълът в main спира при победа:

Game game = new Game();
System.out.println(game.getCurrent().describe());
Scanner in = new Scanner(System.in);
while (!game.isWon() && in.hasNextLine()) {
String line = in.nextLine();
System.out.println("> " + line);
System.out.println(game.handle(line));
}
if (game.isWon()) {
System.out.println("Излезе на свобода за " + game.getMoves() + " хода.");
}
Изход
[лаборатория] Масите са пълни с колби. Вратата зад теб се е заключила сама.
Тук има: фенерче
Изходи: север
> иди север
[коридор] Дълъг коридор. Лампата на тавана мига.
Изходи: север, юг, изток, запад
> иди запад
Вратата към кабинет е заключена.
> иди изток
[склад] Рафтове с кашони до тавана. На стената има календар: „Инвентаризация – 19 октомври“.
Тук има: ключ
Изходи: запад
> вземи ключ
Взе ключ. Малък месингов ключ с етикет „кабинет“.
> иди запад
[коридор] Дълъг коридор. Лампата на тавана мига.
Изходи: север, юг, изток, запад
> използвай ключ
Отключи вратата към кабинет.
> иди запад
[кабинет] Бюро, стол и шкаф с папки.
Тук има: бележка
Изходи: изток
> вземи бележка
Взе бележка. „Кодът на фоайето: 47 и денят на инвентаризацията.“
> иди изток
[коридор] Дълъг коридор. Лампата на тавана мига.
Изходи: север, юг, изток, запад
> иди север
Вратата към фоайе е заключена.
> въведи 1234
Грешен код. Ключалката светва в червено.
> въведи 4719
Ключалката щракна. Вратата към фоайе е отворена.
> иди север
[фоайе] Стъклената врата към улицата е отворена.
Изходи: юг
Излезе на свобода за 13 хода.

С този вход:

Вход
иди север
иди запад
иди изток
вземи ключ
иди запад
използвай ключ
иди запад
вземи бележка
иди изток
иди север
въведи 1234
въведи 4719
иди север

Game game = new Game();
System.out.println(game.getCurrent().describe());
Scanner in = new Scanner(System.in);
while (!game.isWon() && in.hasNextLine()) {
String line = in.nextLine();
System.out.println("> " + line);
System.out.println(game.handle(line));
}
if (game.isWon()) {
System.out.println("Излезе на свобода за " + game.getMoves() + " хода.");
}
Стъпка по стъпка, с думи
  1. Започваш в „лаборатория“. Вратата към „кабинет“ се отключва с ключ, а към „фоайе“ с код от четири цифри.
  2. Ход 1: „иди север“. Влизаш в „коридор“.
  3. Ход 2: „иди запад“. Вратата към „кабинет“ е заключена: „иди“ спира, преди да те премести.
  4. Ход 3: „иди изток“. Влизаш в „склад“. На стената: „Инвентаризация – 19 октомври“.
  5. Ход 4: „вземи ключ“. Взимаш ключ. С него се отключва „кабинет“.
  6. Ход 5: „иди запад“. Влизаш в „коридор“.
  7. Ход 6: „използвай ключ“. Ключ е в раницата и отваря заключената съседна врата към „кабинет“.
  8. Ход 7: „иди запад“. Влизаш в „кабинет“.
  9. Ход 8: „вземи бележка“. Взимаш бележка: „Кодът на фоайето: 47 и денят на инвентаризацията.“
  10. Ход 9: „иди изток“. Влизаш в „коридор“.
  11. Ход 10: „иди север“. Вратата към „фоайе“ е заключена: „иди“ спира, преди да те премести.
  12. Ход 11: „въведи 1234“. 1234 не е кодът. Ключалката светва в червено.
  13. Ход 12: „въведи 4719“. 47 от бележката и 19 от календара: 4719. Ключалката на „фоайе“ щракна.
  14. Ход 13: „иди север“. Влизаш в „фоайе“.
  15. Стигаш до „фоайе“: isWon() е true и цикълът спира. Излизаш за 13 хода.

Идеята

Код за продължение

Цялото състояние на играта е четири неща: къде е играчът, какво носи, кои врати е отворил и колко хода е изиграл. Те се събират в кратък текст, разделен с ;:

public String saveCode() {
List<String> names = new ArrayList<>();
for (Item item : inventory) {
names.add(item.getName());
}
String items = names.isEmpty() ? "-" : String.join(",", names);
String doors = unlocked.isEmpty() ? "-" : String.join(",", unlocked);
return current.getName() + ";" + items + ";" + doors + ";" + moves;
}

Например кабинет;ключ,бележка;кабинет;9. Списъците са със запетаи, а празният списък е -, за да не остава празно място между два знака ;.

Играта печата кода, а играчът го пише обратно със „зареди“. Не е нужен файл: кодът може да се препише на лист или да се прати на приятел.

По-точно, за любопитните

Същият текст може да се запише и във файл, с наученото в модул „Текст и файлове“:

Files.writeString(Path.of("save.txt"), game.saveCode());
...
later.load(Files.readString(Path.of("save.txt")));

Files.writeString и Files.readString хвърлят IOException, затова методът, който ги вика, трябва да го хване или обяви.

Идеята

Развален код

Играчът може да сбърка буква или да опита да излъже: да се появи в заключения кабинет. Тогава зареждането хвърля свое изключение:

class InvalidSaveCodeException extends Exception {
public InvalidSaveCodeException(String message) {
super(message);
}
}

load строи нов свят и проверява кода част по част върху него. Чак когато всичко е минало, новият свят става текущ:

public void load(String code) throws InvalidSaveCodeException {
String[] parts = code.split(";");
if (parts.length != 4) {
throw new InvalidSaveCodeException("трябват 4 части, разделени с ;");
}
Map<String, Room> world = buildWorld();
Room room = world.get(parts[0]);
if (room == null) {
throw new InvalidSaveCodeException("няма стая " + parts[0]);
}
List<Item> items = new ArrayList<>();
if (!parts[1].equals("-")) {
for (String name : parts[1].split(",")) {
Item found = takeFrom(world, name);
if (found == null) {
throw new InvalidSaveCodeException("няма предмет " + name);
}
items.add(found);
}
}
List<String> doors = new ArrayList<>();
if (!parts[2].equals("-")) {
for (String name : parts[2].split(",")) {
Room door = world.get(name);
if (door == null || !door.isLocked()) {
throw new InvalidSaveCodeException("няма заключена врата към " + name);
}
door.unlock();
doors.add(name);
}
}
if (room.isLocked()) {
throw new InvalidSaveCodeException("стаята " + room.getName() + " е заключена");
}
int savedMoves;
try {
savedMoves = Integer.parseInt(parts[3]);
} catch (NumberFormatException e) {
throw new InvalidSaveCodeException("ходовете не са число: " + parts[3]);
}
rooms = world;
current = room;
inventory = items;
unlocked = doors;
moves = savedMoves;
}
private Item takeFrom(Map<String, Room> world, String name) {
for (Room room : world.values()) {
Item found = room.takeItem(name);
if (found != null) {
return found;
}
}
return null;
}

Командата „зареди“ хваща изключението и казва на играча какво не е наред:

class LoadCommand implements Command {
@Override
public String execute(Game game, String argument) {
try {
game.load(argument);
return "Продължаваш оттам, докъдето стигна.\n" + game.getCurrent().describe();
} catch (InvalidSaveCodeException e) {
return "Кодът не става: " + e.getMessage() + ".";
}
}
}
По-точно, за любопитните
  • Integer.parseInt хвърля NumberFormatException, а load го превръща в своето изключение. Така „зареди“ хваща само едно изключение и съобщението е на езика на играта.
  • Кодът не е защита от измама: който разбере формата, може да си напише „фоайе“. Проверката, че стаята не е заключена, пази само от явното. Истинските игри добавят контролна сума: число, сметнато от целия текст, което не съвпада, ако някой го е пипал.

Пробвай

Намери грешката

Играчът пише зареди кабинет;чук;-;3. Играта казва „Кодът не става: няма предмет чук.“, но след „огледай“ той е в кабинета, който е заключен. На кой ред е грешката?

Избери реда с грешката.

Виж го

Запази и продължи

Играчът взима ключа и запазва. После опитва три развалени кода и накрая един, който минава:

Вход
иди север
иди изток
вземи ключ
запази
зареди склад;чук;-;4
зареди коридор;ключ;кабинет;четири
зареди кабинет;ключ;-;7
зареди кабинет;ключ;кабинет;7
иди изток
Изход
[лаборатория] Масите са пълни с колби. Вратата зад теб се е заключила сама.
Тук има: фенерче
Изходи: север
> иди север
[коридор] Дълъг коридор. Лампата на тавана мига.
Изходи: север, юг, изток, запад
> иди изток
[склад] Рафтове с кашони до тавана. На стената има календар: „Инвентаризация – 19 октомври“.
Тук има: ключ
Изходи: запад
> вземи ключ
Взе ключ. Малък месингов ключ с етикет „кабинет“.
> запази
Код за продължение: склад;ключ;-;4
> зареди склад;чук;-;4
Кодът не става: няма предмет чук.
> зареди коридор;ключ;кабинет;четири
Кодът не става: ходовете не са число: четири.
> зареди кабинет;ключ;-;7
Кодът не става: стаята кабинет е заключена.
> зареди кабинет;ключ;кабинет;7
Продължаваш оттам, докъдето стигна.
[кабинет] Бюро, стол и шкаф с папки.
Тук има: бележка
Изходи: изток
> иди изток
[коридор] Дълъг коридор. Лампата на тавана мига.
Изходи: север, юг, изток, запад

Последният код е написан на ръка, но е валиден: кабинетът е отключен, а ключът е в раницата. Играта не знае дали кодът е отпечатан от нея. Тя знае само, че е възможно състояние.

Упражнение

Още загадки

Реши задачите в редактора си, с класовете от урока.

1. Тъмното мазе. На юг от лабораторията има мазе („Ниско мазе с тръби по тавана.“), а в него резервен предпазител. Влиза се само с фенерче в раницата. Подсказка: команда, която наследява GoCommand, проверява за мазето и вика родителя. Регистрирай я с game.addCommand на мястото на „иди“.

Вход
иди юг
вземи фенерче
иди юг
вземи предпазител
Изход
[лаборатория] Масите са пълни с колби. Вратата зад теб се е заключила сама.
Тук има: фенерче
Изходи: север, юг
> иди юг
Твърде тъмно е. Без светлина няма да намериш нищо.
> вземи фенерче
Взе фенерче. Работи, батерията е почти пълна.
> иди юг
[мазе] Ниско мазе с тръби по тавана.
Тук има: предпазител
Изходи: север
> вземи предпазител
Взе предпазител. Резервен предпазител за таблото.

2. Таймер. Охраната идва след 12 хода. Ако играчът не е навън дотогава, играта свършва.

Вход
иди север
иди изток
вземи ключ
иди запад
иди запад
използвай ключ
иди запад
вземи бележка
иди изток
иди изток
иди запад
въведи 4719
иди север
Изход
[лаборатория] Масите са пълни с колби. Вратата зад теб се е заключила сама.
Тук има: фенерче
Изходи: север
> иди север
[коридор] Дълъг коридор. Лампата на тавана мига.
Изходи: север, юг, изток, запад
> иди изток
[склад] Рафтове с кашони до тавана. На стената има календар: „Инвентаризация – 19 октомври“.
Тук има: ключ
Изходи: запад
> вземи ключ
Взе ключ. Малък месингов ключ с етикет „кабинет“.
> иди запад
[коридор] Дълъг коридор. Лампата на тавана мига.
Изходи: север, юг, изток, запад
> иди запад
Вратата към кабинет е заключена.
> използвай ключ
Отключи вратата към кабинет.
> иди запад
[кабинет] Бюро, стол и шкаф с папки.
Тук има: бележка
Изходи: изток
> вземи бележка
Взе бележка. „Кодът на фоайето: 47 и денят на инвентаризацията.“
> иди изток
[коридор] Дълъг коридор. Лампата на тавана мига.
Изходи: север, юг, изток, запад
> иди изток
[склад] Рафтове с кашони до тавана. На стената има календар: „Инвентаризация – 19 октомври“.
Изходи: запад
> иди запад
[коридор] Дълъг коридор. Лампата на тавана мига.
Изходи: север, юг, изток, запад
> въведи 4719
Ключалката щракна. Вратата към фоайе е отворена.
Охраната дойде. Хванаха те след 12 хода.

3. Подсказка срещу ход. Команда „подсказка“ казва следващата стъпка според състоянието: ключът, кабинетът, бележката, календарът. Струва ход, както всяка команда.

Вход
подсказка
иди север
иди изток
вземи ключ
подсказка
иди запад
иди запад
използвай ключ
подсказка
Изход
[лаборатория] Масите са пълни с колби. Вратата зад теб се е заключила сама.
Тук има: фенерче
Изходи: север
> подсказка
Подсказка: огледай склада.
> иди север
[коридор] Дълъг коридор. Лампата на тавана мига.
Изходи: север, юг, изток, запад
> иди изток
[склад] Рафтове с кашони до тавана. На стената има календар: „Инвентаризация – 19 октомври“.
Тук има: ключ
Изходи: запад
> вземи ключ
Взе ключ. Малък месингов ключ с етикет „кабинет“.
> подсказка
Подсказка: ключът е за кабинета.
> иди запад
[коридор] Дълъг коридор. Лампата на тавана мига.
Изходи: север, юг, изток, запад
> иди запад
Вратата към кабинет е заключена.
> използвай ключ
Отключи вратата към кабинет.
> подсказка
Подсказка: в кабинета има нещо за четене.

Още задачи

  • Предпазителят. Кодовата ключалка не работи, докато предпазителят от мазето не е „използван“ в коридора.
  • Ходовете в кода. Таймерът да важи и след „зареди“: кодът вече носи ходовете.
  • Контролна сума. Добави пета част: сборът от дължините на първите четири. Код с грешна сума е развален.

Тест

Провери се

1. Играчът е в склада и пише „използвай ключ“. Ключът е в раницата му. Какво става?
2. Кодът за продължение е „коридор;ключ,бележка;кабинет;9“. Какво казва третата част?
3. Защо load проверява всичко върху нов свят и чак накрая сменя текущата игра?
4. Защо броят на ходовете расте след find, а не преди него?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·