Заключената лаборатория 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; }}class Item{ public string Name { get; } public string Description { get; }
public Item(string name, string description) { Name = name; Description = description; }}class Item: def __init__(self, name, description): self._name = name self._description = description
@property def name(self): return self._name
@property def description(self): return self._descriptionclass Item { #name; #description;
constructor(name, description) { this.#name = name; this.#description = description; }
get name() { return this.#name; }
get description() { return this.#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<>();private static readonly string[] Directions = { "север", "юг", "изток", "запад" };
public string Name { get; }public string Description { get; }private Dictionary<string, Room> exits = new Dictionary<string, Room>();private List<Item> items = new List<Item>();DIRECTIONS = ("север", "юг", "изток", "запад")
def __init__(self, name, description): self._name = name self._description = description self._exits = {} self._items = []static DIRECTIONS = ["север", "юг", "изток", "запад"];
#name;#description;#exits = new Map();#items = [];public void addExit(String direction, Room room) { exits.put(direction, room);}
public Room getExit(String direction) { return exits.get(direction);}public void AddExit(string direction, Room room){ exits[direction] = room;}
public Room GetExit(string direction){ if (exits.ContainsKey(direction)) { return exits[direction]; } return null;}def add_exit(self, direction, room): self._exits[direction] = room
def get_exit(self, direction): return self._exits.get(direction)addExit(direction, room) { this.#exits.set(direction, room);}
getExit(direction) { return this.#exits.get(direction);}get на Map връща undefined, ако ключа го няма. Така „няма изход натам“ е просто undefined. DIRECTIONS е static: обща за всички стаи, и се чете като Room.DIRECTIONS.
Виж го
Какво има в стаята
Взимането маха предмета от стаята и го връща. Ако го няма, връща 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 void AddItem(Item item){ items.Add(item);}
public Item TakeItem(string name){ for (int i = 0; i < items.Count; i++) { if (items[i].Name == name) { Item item = items[i]; items.RemoveAt(i); return item; } } return null;}def add_item(self, item): self._items.append(item)
def take_item(self, name): for item in self._items: if item.name == name: self._items.remove(item) return item return NoneaddItem(item) { this.#items.push(item);}
takeItem(name) { for (let i = 0; i < this.#items.length; i++) { if (this.#items[i].name === name) { return this.#items.splice(i, 1)[0]; } } 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);}public string Describe(){ string text = "[" + Name + "] " + Description; if (items.Count > 0) { List<string> names = new List<string>(); foreach (Item item in items) { names.Add(item.Name); } text += "\nТук има: " + string.Join(", ", names); } List<string> ways = new List<string>(); foreach (string direction in Directions) { if (exits.ContainsKey(direction)) { ways.Add(direction); } } return text + "\nИзходи: " + string.Join(", ", ways);}def describe(self): text = f"[{self._name}] {self._description}" if self._items: names = [] for item in self._items: names.append(item.name) text += "\nТук има: " + ", ".join(names) ways = [] for direction in Room.DIRECTIONS: if direction in self._exits: ways.append(direction) return text + "\nИзходи: " + ", ".join(ways)describe() { let text = `[${this.#name}] ${this.#description}`; if (this.#items.length > 0) { const names = []; for (const item of this.#items) { names.push(item.name); } text += `\nТук има: ${names.join(", ")}`; } const ways = []; for (const direction of Room.DIRECTIONS) { if (this.#exits.has(direction)) { ways.push(direction); } } return `${text}\nИзходи: ${ways.join(", ")}`;}Пробвай
Изход или undefined?
Какво ще отпечата програмата?
const hall = new Room("коридор", "Дълъг коридор.");const store = new Room("склад", "Рафтове с кашони.");hall.addExit("изток", store);store.addExit("запад", hall);store.addItem(new Item("ключ", "Месингов ключ."));console.log(hall.getExit("изток").name);console.log(hall.getExit("запад") === undefined);console.log(store.takeItem("ключ") !== null);console.log(store.takeItem("ключ") !== null);От коридора на изток е складът. На запад от коридора изход няма, затова е undefined. Първото взимане на ключа намира предмета и го маха от списъка. Второто не намира нищо и връща null.
Идеята
Всяка команда е клас
Всички команди изпълняват един договор: получават играта и думата след командата и връщат текста, който играчът вижда:
interface Command { String execute(Game game, String argument);}interface ICommand{ string Execute(Game game, string argument);}class Command(ABC): @abstractmethod def execute(self, game, argument): pass// JavaScript няма interface. Договорът „команда“ е уговорка:// всяка команда е клас с метод execute(game, argument),// който връща текста, който играчът вижда.JavaScript няма интерфейси. Договорът е уговорка: всеки клас команда има метод execute(game, 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 TakeCommand : ICommand{ public string Execute(Game game, string argument) { if (argument == "") { return "Какво да взема?"; } Item item = game.Current.TakeItem(argument); if (item == null) { return "Тук няма " + argument + "."; } game.Inventory.Add(item); return "Взе " + item.Name + ". " + item.Description; }}class TakeCommand(Command): def execute(self, game, argument): if argument == "": return "Какво да взема?" item = game.current.take_item(argument) if item is None: return f"Тук няма {argument}." game.inventory.append(item) return f"Взе {item.name}. {item.description}"class TakeCommand { execute(game, argument) { if (argument === "") { return "Какво да взема?"; } const item = game.current.takeItem(argument); if (item === null) { return `Тук няма ${argument}.`; } game.inventory.push(item); return `Взе ${item.name}. ${item.description}`; }}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(); }}class GoCommand : ICommand{ public string Execute(Game game, string argument) { Room next = game.Current.GetExit(argument); if (next == null) { return "Натам няма изход."; } game.Current = next; return next.Describe(); }}class GoCommand(Command): def execute(self, game, argument): next_room = game.current.get_exit(argument) if next_room is None: return "Натам няма изход." game.current = next_room return next_room.describe()class GoCommand { execute(game, argument) { const next = game.current.getExit(argument); if (next === undefined) { return "Натам няма изход."; } game.current = 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());}private Dictionary<string, Room> rooms;private Dictionary<string, ICommand> commands = new Dictionary<string, ICommand>();
public Room Current { get; set; }public List<Item> Inventory { get; } = new List<Item>();
public Game(){ rooms = BuildWorld(); Current = rooms["лаборатория"]; commands["огледай"] = new LookCommand(); commands["вземи"] = new TakeCommand(); commands["иди"] = new GoCommand();}def __init__(self): self._rooms = self._build_world() self._current = self._rooms["лаборатория"] self._inventory = [] self._commands = { "огледай": LookCommand(), "вземи": TakeCommand(), "иди": GoCommand(), }#rooms;#commands = new Map();#inventory = [];#current;
constructor() { this.#rooms = this.#buildWorld(); this.#current = this.#rooms.get("лаборатория"); this.#commands.set("огледай", new LookCommand()); this.#commands.set("вземи", new TakeCommand()); this.#commands.set("иди", 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;}public string Handle(string line){ string[] words = line.Trim().ToLower().Split(' '); string argument = words.Length > 1 ? words[1] : ""; try { ICommand command = Find(words[0]); return command.Execute(this, argument); } catch (UnknownCommandException e) { return "Не разбирам „" + e.Verb + "“."; }}
private ICommand Find(string verb){ if (!commands.ContainsKey(verb)) { throw new UnknownCommandException(verb); } return commands[verb];}def handle(self, line): words = line.strip().lower().split(" ") argument = words[1] if len(words) > 1 else "" try: command = self._find(words[0]) return command.execute(self, argument) except UnknownCommandError as e: return f"Не разбирам „{e.verb}“."
def _find(self, verb): command = self._commands.get(verb) if command is None: raise UnknownCommandError(verb) return commandhandle(line) { const words = line.trim().toLowerCase().split(" "); const argument = words.length > 1 ? words[1] : ""; try { const command = this.#find(words[0]); return command.execute(this, argument); } catch (e) { if (!(e instanceof UnknownCommandError)) { throw e; } return `Не разбирам „${e.verb}“.`; }}
#find(verb) { const command = this.#commands.get(verb); if (command === undefined) { throw new UnknownCommandError(verb); } return command;}class UnknownCommandException extends Exception { private String verb;
public UnknownCommandException(String verb) { super("непозната команда: " + verb); this.verb = verb; }
public String getVerb() { return verb; }}class UnknownCommandException : Exception{ public string Verb { get; }
public UnknownCommandException(string verb) : base("непозната команда: " + verb) { Verb = verb; }}class UnknownCommandError(Exception): def __init__(self, verb): super().__init__(f"непозната команда: {verb}") self.verb = verbclass UnknownCommandError extends Error { constructor(verb) { super(`непозната команда: ${verb}`); this.name = "UnknownCommandError"; this.verb = verb; }}catch в JavaScript хваща всяка грешка, не само UnknownCommandError. Затова handle проверява с instanceof и хвърля нататък всичко друго: грешка в самата програма не трябва да изглежда като „Не разбирам“.
По-точно, за любопитните
- Играта знае само, че командата има
execute. Коеexecuteще се изпълни, решава обектът в речника. Това е полиморфизмът от модула за наследяване, само че обектите стоят в речник, а не в масив. - Друг начин е
switchпо думата. За четири команди е добре. За двадесет, с различни правила за всяка, отделните класове се четат и сменят по-лесно. split(/\s+/)с регулярен израз прави няколко интервала подред един разделител. Тук е достатъчен един интервал.
console.log("иди север".split(" "));console.log("иди север".split(/\s+/));[ 'иди', '', 'север' ][ 'иди', 'север' ]Пробвай
Намери грешката
Играчът пише „Огледай“ с главна буква и играта отговаря „Не разбирам „Огледай“.“ На кой ред е грешката?
Избери реда с грешката.
Ключовете в речника са с малки букви, а „Огледай“ не е „огледай“. Редът трябва да е line.trim().toLowerCase().split(" "). Тогава всяка команда се разбира, както и да я напише играчът.
Виж го
Първа разходка
Програмата показва първата стая и после чете команда по команда, докато входът свърши. Пред всяка команда печата >, за да се вижда кое е написал играчът:
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 и денят на инвентаризацията.“Game game = new Game();Console.WriteLine(game.Current.Describe());string line = Console.ReadLine();while (line != null){ Console.WriteLine("> " + line); Console.WriteLine(game.Handle(line)); line = Console.ReadLine();}[лаборатория] Масите са пълни с колби. Вратата зад теб се е заключила сама.Тук има: фенерчеИзходи: север> Вземи фенерчеВзе фенерче. Работи, батерията е почти пълна.> огледай[лаборатория] Масите са пълни с колби. Вратата зад теб се е заключила сама.Изходи: север> вземи фенерчеТук няма фенерче.> танцувайНе разбирам „танцувай“.> иди север[коридор] Дълъг коридор. Лампата на тавана мига.Изходи: юг, изток, запад> иди изток[склад] Рафтове с кашони до тавана. На стената има календар: „Инвентаризация – 19 октомври“.Тук има: ключИзходи: запад> вземи ключВзе ключ. Малък месингов ключ с етикет „кабинет“.> иди запад[коридор] Дълъг коридор. Лампата на тавана мига.Изходи: юг, изток, запад> иди нагореНатам няма изход.> иди запад[кабинет] Бюро, стол и шкаф с папки.Тук има: бележкаИзходи: изток> вземи бележкаВзе бележка. „Кодът на фоайето: 47 и денят на инвентаризацията.“game = Game()print(game.current.describe())for line in sys.stdin: line = line.rstrip("\n") print(f"> {line}") print(game.handle(line))[лаборатория] Масите са пълни с колби. Вратата зад теб се е заключила сама.Тук има: фенерчеИзходи: север> Вземи фенерчеВзе фенерче. Работи, батерията е почти пълна.> огледай[лаборатория] Масите са пълни с колби. Вратата зад теб се е заключила сама.Изходи: север> вземи фенерчеТук няма фенерче.> танцувайНе разбирам „танцувай“.> иди север[коридор] Дълъг коридор. Лампата на тавана мига.Изходи: юг, изток, запад> иди изток[склад] Рафтове с кашони до тавана. На стената има календар: „Инвентаризация – 19 октомври“.Тук има: ключИзходи: запад> вземи ключВзе ключ. Малък месингов ключ с етикет „кабинет“.> иди запад[коридор] Дълъг коридор. Лампата на тавана мига.Изходи: юг, изток, запад> иди нагореНатам няма изход.> иди запад[кабинет] Бюро, стол и шкаф с папки.Тук има: бележкаИзходи: изток> вземи бележкаВзе бележка. „Кодът на фоайето: 47 и денят на инвентаризацията.“const game = new Game();console.log(game.current.describe());let line = await ask("");while (line !== undefined) { console.log(`> ${line}`); console.log(game.handle(line)); line = await ask("");}rl.close();[лаборатория] Масите са пълни с колби. Вратата зад теб се е заключила сама.Тук има: фенерчеИзходи: север> Вземи фенерчеВзе фенерче. Работи, батерията е почти пълна.> огледай[лаборатория] Масите са пълни с колби. Вратата зад теб се е заключила сама.Изходи: север> вземи фенерчеТук няма фенерче.> танцувайНе разбирам „танцувай“.> иди север[коридор] Дълъг коридор. Лампата на тавана мига.Изходи: юг, изток, запад> иди изток[склад] Рафтове с кашони до тавана. На стената има календар: „Инвентаризация – 19 октомври“.Тук има: ключИзходи: запад> вземи ключВзе ключ. Малък месингов ключ с етикет „кабинет“.> иди запад[коридор] Дълъг коридор. Лампата на тавана мига.Изходи: юг, изток, запад> иди нагореНатам няма изход.> иди запад[кабинет] Бюро, стол и шкаф с папки.Тук има: бележкаИзходи: изток> вземи бележкаВзе бележка. „Кодът на фоайето: 47 и денят на инвентаризацията.“ask е помощникът за вход от модул „Основи“. Когато входът свърши, той връща undefined, и цикълът спира.
С този вход:
Вземи фенерчеогледайвземи фенерчетанцувайиди севериди изтоквземи ключиди западиди нагореиди западвземи бележкаБележката казва, че кодът на фоайето е 47 и денят на инвентаризацията. Календарът в склада казва 19. Но фоайето още го няма, а и кабинетът не е заключен. Загадките идват в следващия урок.
const game = new Game();console.log(game.current.describe());let line = await ask("");while (line !== undefined) { console.log(`> ${line}`); console.log(game.handle(line)); line = await ask("");}rl.close();Стъпка по стъпка, с думи
- Играта започва в лаборатория. Командите са в речник: думата е ключът, обектът команда е стойността.
- Пишеш „Вземи фенерче“. Редът става с малки букви. „вземи“ е ключ в речника и командата е TakeCommand. Предметът фенерче минава от стаята в раницата.
- Пишеш „огледай“. „огледай“ е ключ в речника и командата е LookCommand. Тя описва стаята наново, вече без взетия предмет.
- Пишеш „вземи фенерче“. „вземи“ е ключ в речника и командата е TakeCommand. В стаята вече няма фенерче, затова отговорът е „Тук няма“.
- Пишеш „танцувай“. „танцувай“ го няма в речника: #find хвърля UnknownCommandError, а handle казва „Не разбирам“.
- Пишеш „иди север“. „иди“ е ключ в речника и командата е GoCommand. getExit("север") на стаята лаборатория дава коридор.
- Пишеш „иди изток“. „иди“ е ключ в речника и командата е GoCommand. getExit("изток") на стаята коридор дава склад.
- Пишеш „вземи ключ“. „вземи“ е ключ в речника и командата е TakeCommand. Предметът ключ минава от стаята в раницата.
- Пишеш „иди запад“. „иди“ е ключ в речника и командата е GoCommand. getExit("запад") на стаята склад дава коридор.
- Пишеш „иди нагоре“. „иди“ е ключ в речника и командата е GoCommand. Изходите на коридор нямат ключ „нагоре“: натам няма изход.
- Пишеш „иди запад“. „иди“ е ключ в речника и командата е GoCommand. getExit("запад") на стаята коридор дава кабинет.
- Пишеш „вземи бележка“. „вземи“ е ключ в речника и командата е TakeCommand. Предметът бележка минава от стаята в раницата.
- Входът свърши и цикълът спира. Раницата: фенерче, ключ, бележка.
Упражнение
Още команди и стаи
Реши задачите в редактора си, с класовете от урока. Добави новото отвън, с game.addCommand и game.getRoom, без да пипаш Game.
1. Инвентар. Команда „инвентар“ казва какво има в раницата или че е празна.
инвентарвземи фенерчеиди севериди изтоквземи ключинвентар[лаборатория] Масите са пълни с колби. Вратата зад теб се е заключила сама.Тук има: фенерчеИзходи: север> инвентарРаницата е празна.> вземи фенерчеВзе фенерче. Работи, батерията е почти пълна.> иди север[коридор] Дълъг коридор. Лампата на тавана мига.Изходи: юг, изток, запад> иди изток[склад] Рафтове с кашони до тавана. На стената има календар: „Инвентаризация – 19 октомври“.Тук има: ключИзходи: запад> вземи ключВзе ключ. Малък месингов ключ с етикет „кабинет“.> инвентарВ раницата: фенерче, ключ2. Нова стая. Сървърна на север от склада, с мрежов кабел в нея. Описанието ѝ е „Шкафове с мигащи светлинки. Вентилаторите бучат.“
иди севериди изтокиди севервземи кабелиди юг[лаборатория] Масите са пълни с колби. Вратата зад теб се е заключила сама.Тук има: фенерчеИзходи: север> иди север[коридор] Дълъг коридор. Лампата на тавана мига.Изходи: юг, изток, запад> иди изток[склад] Рафтове с кашони до тавана. На стената има календар: „Инвентаризация – 19 октомври“.Тук има: ключИзходи: север, запад> иди север[сървърна] Шкафове с мигащи светлинки. Вентилаторите бучат.Тук има: кабелИзходи: юг> вземи кабелВзе кабел. Дълъг мрежов кабел.> иди юг[склад] Рафтове с кашони до тавана. На стената има календар: „Инвентаризация – 19 октомври“.Тук има: ключИзходи: север, запад3. Синоними. „грабни“ да значи „вземи“, „отиди“ да значи „иди“, а „виж“ да значи „огледай“. Подсказка: една и съща команда може да е стойност на няколко ключа.
грабни фенерчеотиди севервиж[лаборатория] Масите са пълни с колби. Вратата зад теб се е заключила сама.Тук има: фенерчеИзходи: север> грабни фенерчеВзе фенерче. Работи, батерията е почти пълна.> отиди север[коридор] Дълъг коридор. Лампата на тавана мига.Изходи: юг, изток, запад> виж[коридор] Дълъг коридор. Лампата на тавана мига.Изходи: юг, изток, западОще задачи
- Остави. Команда „остави ключ“ връща предмета от раницата в стаята.
- Помощ. Команда „помощ“ изброява всички думи от речника с командите.
- Кратки посоки. „с“, „ю“, „и“, „з“ да значат север, юг, изток, запад.
Тест