Skip to content
wannacode

The locked lab 1: rooms and commands

Problem

Locked in the lab

You stayed late in the lab. The security system locked the doors automatically. You have to get out before the guards come in the morning. There are no fights, only rooms, items and puzzles.

The game is played in text. The player types commands, and the game answers:

> go north
[corridor] A long corridor. The ceiling light flickers.
Exits: south, east, west
> take key
There is no key here.

The first idea is a big if: if the command is "go", do this, if it is "take", do that. It grows with every new command, and a bug in one branch breaks the others. In this lesson the commands become separate classes, and the rooms and items become objects in collections.

The idea

Rooms and items

An item has a name and a description:

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;
}
}

A room has a name, a description, exits and items. The exits are a map: the direction is the key, and the next room is the value. The items are a list, because how many there are changes:

private static final String[] DIRECTIONS = { "north", "south", "east", "west" };
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 on a HashMap returns null if the key is not there. So "no exit that way" is just null.

See it

What is in the room

Taking an item removes it from the room and returns it. If it is not there, it returns 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;
}

The description collects the names of the items and the exits. The directions are in a fixed order (north, south, east, west), so the text does not depend on the order in the dictionary:

public String describe() {
String text = "[" + name + "] " + description;
if (!items.isEmpty()) {
List<String> names = new ArrayList<>();
for (Item item : items) {
names.add(item.getName());
}
text += "\nYou see: " + String.join(", ", names);
}
List<String> ways = new ArrayList<>();
for (String direction : DIRECTIONS) {
if (exits.containsKey(direction)) {
ways.add(direction);
}
}
return text + "\nExits: " + String.join(", ", ways);
}

Try it

Exit or null?

What will the program print?

Room hall = new Room("corridor", "A long corridor.");
Room store = new Room("storeroom", "Shelves of boxes.");
hall.addExit("east", store);
store.addExit("west", hall);
store.addItem(new Item("key", "A brass key."));
System.out.println(hall.getExit("east").getName());
System.out.println(hall.getExit("west") == null);
System.out.println(store.takeItem("key") != null);
System.out.println(store.takeItem("key") != null);
Answer

The idea

Every command is a class

All commands follow one contract: they get the game and the word after the command, and they return the text the player sees:

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

Taking and going are two small classes. Each one knows only its own job:

class TakeCommand implements Command {
@Override
public String execute(Game game, String argument) {
if (argument.isEmpty()) {
return "Take what?";
}
Item item = game.getCurrent().takeItem(argument);
if (item == null) {
return "There is no " + argument + " here.";
}
game.getInventory().add(item);
return "You take the " + 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 "There is no exit that way.";
}
game.setCurrent(next);
return next.describe();
}
}

The commands do not print. They return text. That way the game decides where to show it, and you can test a command without looking at the screen.

The idea

A dictionary of commands

The game keeps the commands in a map: the word is the key, and the command object is the value. Adding a command is one line:

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("lab");
commands.put("look", new LookCommand());
commands.put("take", new TakeCommand());
commands.put("go", new GoCommand());
}

handle removes the spaces at the ends, turns the line to lowercase and splits it into words. The first word is the command, and the second is the argument. If the word is not in the dictionary, find throws its own exception, and handle catches it and says "I don't understand":

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 "I don't understand \"" + 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("unknown command: " + verb);
this.verb = verb;
}
public String getVerb() {
return verb;
}
}
More precisely, if you are curious
  • The game knows only Command. The object in the dictionary decides which execute runs. This is the polymorphism from the inheritance module, except that the objects sit in a dictionary and not in an array.
  • Another way is a switch on the word. For four commands that is fine. For twenty, with different rules for each, separate classes are easier to read and change.
  • split(" ") actually takes a regular expression. With split("\\s+"), several spaces in a row count as one separator. Here one space is enough.

Try it

Find the bug

The player types "Look" with a capital letter, and the game answers I don't understand "Look". On which line is the bug?

Pick the line with the bug.

See it

A first walk

main shows the first room and then reads command after command until the input runs out. Before each command it prints >, so you can see what the player typed:

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));
}
Output
[lab] The tables are full of flasks. The door behind you locked by itself.
You see: flashlight
Exits: north
> Take flashlight
You take the flashlight. It works, and the battery is almost full.
> look
[lab] The tables are full of flasks. The door behind you locked by itself.
Exits: north
> take flashlight
There is no flashlight here.
> dance
I don't understand "dance".
> go north
[corridor] A long corridor. The ceiling light flickers.
Exits: south, east, west
> go east
[storeroom] Shelves of boxes up to the ceiling. A calendar on the wall says: Stocktaking on October 19.
You see: key
Exits: west
> take key
You take the key. A small brass key. Its tag says office.
> go west
[corridor] A long corridor. The ceiling light flickers.
Exits: south, east, west
> go up
There is no exit that way.
> go west
[office] A desk, a chair and a cabinet full of folders.
You see: note
Exits: east
> take note
You take the note. Lobby code: 47 and the stocktaking day.

With this input:

Input
Take flashlight
look
take flashlight
dance
go north
go east
take key
go west
go up
go west
take note

The note says the lobby code is 47 and the stocktaking day. The calendar in the storeroom says 19. But there is no lobby yet, and the office is not locked either. The puzzles come in the next lesson.

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));
}
Step by step, in words
  1. The game starts in the lab. The commands are in a dictionary: the word is the key, and the command object is the value.
  2. You type "Take flashlight". The line becomes lowercase. "take" is a key in the dictionary, and the command is TakeCommand. The flashlight moves from the room to the backpack.
  3. You type "look". "look" is a key in the dictionary, and the command is LookCommand. It describes the room again, now without the item you took.
  4. You type "take flashlight". "take" is a key in the dictionary, and the command is TakeCommand. There is no flashlight in the room anymore, so the answer is "There is no".
  5. You type "dance". "dance" is not in the dictionary: find throws UnknownCommandException, and handle says "I don't understand".
  6. You type "go north". "go" is a key in the dictionary, and the command is GoCommand. getExit("north") on the lab gives the corridor.
  7. You type "go east". "go" is a key in the dictionary, and the command is GoCommand. getExit("east") on the corridor gives the storeroom.
  8. You type "take key". "take" is a key in the dictionary, and the command is TakeCommand. The key moves from the room to the backpack.
  9. You type "go west". "go" is a key in the dictionary, and the command is GoCommand. getExit("west") on the storeroom gives the corridor.
  10. You type "go up". "go" is a key in the dictionary, and the command is GoCommand. The exits of the corridor have no key "up": there is no exit that way.
  11. You type "go west". "go" is a key in the dictionary, and the command is GoCommand. getExit("west") on the corridor gives the office.
  12. You type "take note". "take" is a key in the dictionary, and the command is TakeCommand. The note moves from the room to the backpack.
  13. The input ran out, and the loop stops. The backpack: flashlight, key, note.

Exercise

More commands and rooms

Solve the tasks in your editor, with the classes from the lesson. Add the new parts from outside, with game.addCommand and game.getRoom, without touching Game.

1. Inventory. A command "inventory" says what is in the backpack or that it is empty.

Input
inventory
take flashlight
go north
go east
take key
inventory
Output
[lab] The tables are full of flasks. The door behind you locked by itself.
You see: flashlight
Exits: north
> inventory
Your backpack is empty.
> take flashlight
You take the flashlight. It works, and the battery is almost full.
> go north
[corridor] A long corridor. The ceiling light flickers.
Exits: south, east, west
> go east
[storeroom] Shelves of boxes up to the ceiling. A calendar on the wall says: Stocktaking on October 19.
You see: key
Exits: west
> take key
You take the key. A small brass key. Its tag says office.
> inventory
In your backpack: flashlight, key

2. A new room. A server room north of the storeroom, with a network cable in it. Its description is "Racks with blinking lights. The fans hum."

Input
go north
go east
go north
take cable
go south
Output
[lab] The tables are full of flasks. The door behind you locked by itself.
You see: flashlight
Exits: north
> go north
[corridor] A long corridor. The ceiling light flickers.
Exits: south, east, west
> go east
[storeroom] Shelves of boxes up to the ceiling. A calendar on the wall says: Stocktaking on October 19.
You see: key
Exits: north, west
> go north
[server room] Racks with blinking lights. The fans hum.
You see: cable
Exits: south
> take cable
You take the cable. A long network cable.
> go south
[storeroom] Shelves of boxes up to the ceiling. A calendar on the wall says: Stocktaking on October 19.
You see: key
Exits: north, west

3. Synonyms. Make "grab" mean "take", "walk" mean "go", and "view" mean "look". Hint: the same command can be the value of several keys.

Input
grab flashlight
walk north
view
Output
[lab] The tables are full of flasks. The door behind you locked by itself.
You see: flashlight
Exits: north
> grab flashlight
You take the flashlight. It works, and the battery is almost full.
> walk north
[corridor] A long corridor. The ceiling light flickers.
Exits: south, east, west
> view
[corridor] A long corridor. The ceiling light flickers.
Exits: south, east, west

More tasks

  • Drop. A command "drop key" puts the item from the backpack back in the room.
  • Help. A command "help" lists all the words in the dictionary of commands.
  • Short directions. Make "n", "s", "e", "w" mean north, south, east, west.

Test

Check yourself

1. The player types "GO north" (with two spaces). What does handle get after trim, lowercase and split on a space?
2. Why is every command a separate class and not a branch in a big if/else?
3. What does getExit return for a direction that is not in the dictionary?
4. An unknown command throws UnknownCommandException. Who catches it?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·