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

Заключената лаборатория 1: стаи и команди

Проблем

Заключен в лабораторията

Остана до късно в лабораторията. Системата за сигурност заключи вратите автоматично. Трябва да излезеш, преди охраната да дойде сутринта. Няма битки, а само стаи, предмети и загадки.

Играта е текстова. Играчът пише команди, а играта отговаря:

> иди север
[коридор] Дълъг коридор. Лампата на тавана мига.
Изходи: юг, изток, запад
> вземи ключ
Тук няма ключ.

Първата идея е голям if: ако командата е „иди“, прави това, ако е „вземи“, прави онова. С всяка нова команда той расте, а грешка в един клон чупи другите. В този урок командите стават отделни класове, а стаите и предметите стават обекти в колекции.

Идеята

Стаи и предмети

Предметът има име и описание:

class Item {
private String name;
private String description;
public Item(String name, String description) {
this.name = name;
this.description = description;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
}

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

private static final String[] DIRECTIONS = { "север", "юг", "изток", "запад" };
private String name;
private String description;
private Map<String, Room> exits = new HashMap<>();
private List<Item> items = new ArrayList<>();
public void addExit(String direction, Room room) {
exits.put(direction, room);
}
public Room getExit(String direction) {
return exits.get(direction);
}

get на HashMap връща null, ако ключа го няма. Така „няма изход натам“ е просто null.

Виж го

Какво има в стаята

Взимането маха предмета от стаята и го връща. Ако го няма, връща null:

public void addItem(Item item) {
items.add(item);
}
public Item takeItem(String name) {
for (int i = 0; i < items.size(); i++) {
if (items.get(i).getName().equals(name)) {
return items.remove(i);
}
}
return null;
}

Описанието събира имената на предметите и изходите. Посоките са в постоянен ред (север, юг, изток, запад), за да не зависи текстът от реда в речника:

public String describe() {
String text = "[" + name + "] " + description;
if (!items.isEmpty()) {
List<String> names = new ArrayList<>();
for (Item item : items) {
names.add(item.getName());
}
text += "\nТук има: " + String.join(", ", names);
}
List<String> ways = new ArrayList<>();
for (String direction : DIRECTIONS) {
if (exits.containsKey(direction)) {
ways.add(direction);
}
}
return text + "\nИзходи: " + String.join(", ", ways);
}

Пробвай

Изход или null?

Какво ще отпечата програмата?

Room hall = new Room("коридор", "Дълъг коридор.");
Room store = new Room("склад", "Рафтове с кашони.");
hall.addExit("изток", store);
store.addExit("запад", hall);
store.addItem(new Item("ключ", "Месингов ключ."));
System.out.println(hall.getExit("изток").getName());
System.out.println(hall.getExit("запад") == null);
System.out.println(store.takeItem("ключ") != null);
System.out.println(store.takeItem("ключ") != null);
Отговор

Идеята

Всяка команда е клас

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

interface Command {
String execute(Game game, String argument);
}

Взимането и ходенето са два малки класа. Всеки знае само своето:

class TakeCommand implements Command {
@Override
public String execute(Game game, String argument) {
if (argument.isEmpty()) {
return "Какво да взема?";
}
Item item = game.getCurrent().takeItem(argument);
if (item == null) {
return "Тук няма " + argument + ".";
}
game.getInventory().add(item);
return "Взе " + item.getName() + ". " + item.getDescription();
}
}
class GoCommand implements Command {
@Override
public String execute(Game game, String argument) {
Room next = game.getCurrent().getExit(argument);
if (next == null) {
return "Натам няма изход.";
}
game.setCurrent(next);
return next.describe();
}
}

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

Идеята

Речник с командите

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

private Map<String, Room> rooms;
private Map<String, Command> commands = new HashMap<>();
private List<Item> inventory = new ArrayList<>();
private Room current;
public Game() {
rooms = buildWorld();
current = rooms.get("лаборатория");
commands.put("огледай", new LookCommand());
commands.put("вземи", new TakeCommand());
commands.put("иди", new GoCommand());
}

handle маха интервалите отстрани, обръща реда в малки букви и го разделя на думи. Първата дума е командата, втората е аргументът. Ако думата я няма в речника, find хвърля свое изключение, а handle го хваща и казва „Не разбирам“:

public String handle(String line) {
String[] words = line.trim().toLowerCase().split(" ");
String argument = words.length > 1 ? words[1] : "";
try {
Command command = find(words[0]);
return command.execute(this, argument);
} catch (UnknownCommandException e) {
return "Не разбирам „" + e.getVerb() + "“.";
}
}
private Command find(String verb) throws UnknownCommandException {
Command command = commands.get(verb);
if (command == null) {
throw new UnknownCommandException(verb);
}
return command;
}
class UnknownCommandException extends Exception {
private String verb;
public UnknownCommandException(String verb) {
super("непозната команда: " + verb);
this.verb = verb;
}
public String getVerb() {
return verb;
}
}
По-точно, за любопитните
  • Играта знае само Command. Кое execute ще се изпълни, решава обектът в речника. Това е полиморфизмът от модула за наследяване, само че обектите стоят в речник, а не в масив.
  • Друг начин е switch по думата. За четири команди е добре. За двадесет, с различни правила за всяка, отделните класове се четат и сменят по-лесно.
  • split(" ") всъщност приема регулярен израз. С split("\\s+") няколко интервала подред са един разделител. Тук е достатъчен един интервал.

Пробвай

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

Играчът пише „Огледай“ с главна буква и играта отговаря „Не разбирам „Огледай“.“ На кой ред е грешката?

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

Виж го

Първа разходка

main показва първата стая и после чете команда по команда, докато входът свърши. Пред всяка команда печата >, за да се вижда кое е написал играчът:

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

С този вход:

Вход
Вземи фенерче
огледай
вземи фенерче
танцувай
иди север
иди изток
вземи ключ
иди запад
иди нагоре
иди запад
вземи бележка

Бележката казва, че кодът на фоайето е 47 и денят на инвентаризацията. Календарът в склада казва 19. Но фоайето още го няма, а и кабинетът не е заключен. Загадките идват в следващия урок.

Game game = new Game();
System.out.println(game.getCurrent().describe());
Scanner in = new Scanner(System.in);
while (in.hasNextLine()) {
String line = in.nextLine();
System.out.println("> " + line);
System.out.println(game.handle(line));
}
Стъпка по стъпка, с думи
  1. Играта започва в лаборатория. Командите са в речник: думата е ключът, обектът команда е стойността.
  2. Пишеш „Вземи фенерче“. Редът става с малки букви. „вземи“ е ключ в речника и командата е TakeCommand. Предметът фенерче минава от стаята в раницата.
  3. Пишеш „огледай“. „огледай“ е ключ в речника и командата е LookCommand. Тя описва стаята наново, вече без взетия предмет.
  4. Пишеш „вземи фенерче“. „вземи“ е ключ в речника и командата е TakeCommand. В стаята вече няма фенерче, затова отговорът е „Тук няма“.
  5. Пишеш „танцувай“. „танцувай“ го няма в речника: find хвърля UnknownCommandException, а handle казва „Не разбирам“.
  6. Пишеш „иди север“. „иди“ е ключ в речника и командата е GoCommand. getExit("север") на стаята лаборатория дава коридор.
  7. Пишеш „иди изток“. „иди“ е ключ в речника и командата е GoCommand. getExit("изток") на стаята коридор дава склад.
  8. Пишеш „вземи ключ“. „вземи“ е ключ в речника и командата е TakeCommand. Предметът ключ минава от стаята в раницата.
  9. Пишеш „иди запад“. „иди“ е ключ в речника и командата е GoCommand. getExit("запад") на стаята склад дава коридор.
  10. Пишеш „иди нагоре“. „иди“ е ключ в речника и командата е GoCommand. Изходите на коридор нямат ключ „нагоре“: натам няма изход.
  11. Пишеш „иди запад“. „иди“ е ключ в речника и командата е GoCommand. getExit("запад") на стаята коридор дава кабинет.
  12. Пишеш „вземи бележка“. „вземи“ е ключ в речника и командата е TakeCommand. Предметът бележка минава от стаята в раницата.
  13. Входът свърши и цикълът спира. Раницата: фенерче, ключ, бележка.

Упражнение

Още команди и стаи

Реши задачите в редактора си, с класовете от урока. Добави новото отвън, с game.addCommand и game.getRoom, без да пипаш Game.

1. Инвентар. Команда „инвентар“ казва какво има в раницата или че е празна.

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

2. Нова стая. Сървърна на север от склада, с мрежов кабел в нея. Описанието ѝ е „Шкафове с мигащи светлинки. Вентилаторите бучат.“

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

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

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

Още задачи

  • Остави. Команда „остави ключ“ връща предмета от раницата в стаята.
  • Помощ. Команда „помощ“ изброява всички думи от речника с командите.
  • Кратки посоки. „с“, „ю“, „и“, „з“ да значат север, юг, изток, запад.

Тест

Провери се

1. Играчът пише „ИДИ север“ (с два интервала). Какво получава handle след trim, малки букви и split по интервал?
2. Защо всяка команда е отделен клас, а не клон в голям if/else?
3. Какво връща getExit за посока, която я няма в речника?
4. Непознатата команда хвърля UnknownCommandException. Кой го хваща?

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