Zum Inhalt
wannacode

Das verschlossene Labor 1: Räume und Befehle

Kurz gesagt: Ein Text-Fluchtspiel: Räume und Gegenstände sind Objekte, die Ausgänge stehen in einer Map, das Inventar in einer Liste, jeder Befehl ist eine Klasse und ein unbekannter Befehl ist eine Exception.

Problem

Im Labor eingesperrt

Du bist abends lange im Labor geblieben. Das Sicherheitssystem hat die Türen automatisch verriegelt. Du musst raus, bevor morgens der Wachdienst kommt. Es gibt keine Kämpfe, nur Räume, Gegenstände und Rätsel.

Das Spiel läuft als Text. Der Spieler tippt Befehle, und das Spiel antwortet:

> geh norden
[flur] Ein langer Flur. Die Deckenlampe flackert.
Ausgänge: süden, osten, westen
> nimm schlüssel
Hier gibt es keinen Gegenstand „schlüssel“.

Die erste Idee ist ein großes if: Wenn der Befehl „geh“ ist, tu dies, wenn er „nimm“ ist, tu das. Es wächst mit jedem neuen Befehl, und ein Fehler in einem Zweig bringt die anderen kaputt. In dieser Lektion werden die Befehle zu eigenen Klassen, und die Räume und Gegenstände werden zu Objekten in Collections.

Die Idee

Räume und Gegenstände

Ein Gegenstand hat einen Namen und eine Beschreibung:

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

Ein Raum hat einen Namen, eine Beschreibung, Ausgänge und Gegenstände. Die Ausgänge stehen in einem Map: Die Richtung ist der Schlüssel, der Nachbarraum ist der Wert. Die Gegenstände stehen in einer Liste, weil ihre Anzahl sich ändert:

private static final String[] DIRECTIONS = { "norden", "süden", "osten", "westen" };
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 auf einer HashMap gibt null zurück, wenn der Schlüssel fehlt. „Kein Ausgang in diese Richtung“ ist also einfach null.

Sieh es dir an

Was im Raum ist

Beim Nehmen verschwindet der Gegenstand aus dem Raum und wird zurückgegeben. Ist er nicht da, kommt null zurück:

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

Die Beschreibung sammelt die Namen der Gegenstände und die Ausgänge. Die Richtungen stehen in fester Reihenfolge (norden, süden, osten, westen), damit der Text nicht von der Reihenfolge im Dictionary abhängt:

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

Probier es aus

Ausgang oder null?

Was gibt das Programm aus?

Room hall = new Room("flur", "Ein langer Flur.");
Room store = new Room("lager", "Regale mit Kartons.");
hall.addExit("osten", store);
store.addExit("westen", hall);
store.addItem(new Item("schlüssel", "Ein Messingschlüssel."));
System.out.println(hall.getExit("osten").getName());
System.out.println(hall.getExit("westen") == null);
System.out.println(store.takeItem("schlüssel") != null);
System.out.println(store.takeItem("schlüssel") != null);
Antwort

Die Idee

Jeder Befehl ist eine Klasse

Alle Befehle folgen einem Vertrag: Sie bekommen das Spiel und das Wort nach dem Befehl und geben den Text zurück, den der Spieler sieht:

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

Nehmen und Gehen sind zwei kleine Klassen. Jede kennt nur ihre eigene Aufgabe:

class TakeCommand implements Command {
@Override
public String execute(Game game, String argument) {
if (argument.isEmpty()) {
return "Was möchtest du nehmen?";
}
Item item = game.getCurrent().takeItem(argument);
if (item == null) {
return "Hier gibt es keinen Gegenstand „" + argument + "“.";
}
game.getInventory().add(item);
return "Du nimmst: " + 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 "In diese Richtung gibt es keinen Ausgang.";
}
game.setCurrent(next);
return next.describe();
}
}

Die Befehle geben nichts aus. Sie geben Text zurück. So entscheidet das Spiel, wo es ihn zeigt, und du kannst einen Befehl testen, ohne auf den Bildschirm zu schauen.

Die Idee

Ein Dictionary für die Befehle

Das Spiel hält die Befehle in einem Map: Das Wort ist der Schlüssel, das Befehlsobjekt ist der Wert. Einen Befehl hinzuzufügen ist eine Zeile:

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("labor");
commands.put("schau", new LookCommand());
commands.put("nimm", new TakeCommand());
commands.put("geh", new GoCommand());
}

handle entfernt die Leerzeichen an den Enden, macht die Zeile klein und zerlegt sie in Wörter. Das erste Wort ist der Befehl, das zweite das Argument. Steht das Wort nicht im Dictionary, wirft find eine eigene Exception, und handle fängt sie und sagt „Das verstehe ich nicht“:

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 "Ich verstehe „" + e.getVerb() + "“ nicht.";
}
}
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("unbekannter Befehl: " + verb);
this.verb = verb;
}
public String getVerb() {
return verb;
}
}
Genauer, für Neugierige
  • Das Spiel kennt nur Command. Welches execute läuft, entscheidet das Objekt im Dictionary. Das ist der Polymorphismus aus dem Modul über Vererbung, nur dass die Objekte in einem Dictionary liegen und nicht in einem Array.
  • Eine andere Möglichkeit ist ein switch auf das Wort. Für vier Befehle ist das in Ordnung. Bei zwanzig Befehlen mit jeweils eigenen Regeln sind eigene Klassen leichter zu lesen und zu ändern.
  • split(" ") nimmt eigentlich einen regulären Ausdruck. Mit split("\\s+") zählen mehrere Leerzeichen hintereinander als ein Trennzeichen. Hier genügt ein Leerzeichen.

Probier es aus

Finde den Fehler

Der Spieler tippt „Schau“ mit großem Anfangsbuchstaben, und das Spiel antwortet Ich verstehe „Schau“ nicht. In welcher Zeile steckt der Fehler?

Wähle die Zeile mit dem Fehler.

Sieh es dir an

Ein erster Rundgang

main zeigt den ersten Raum und liest dann Befehl für Befehl, bis die Eingabe zu Ende ist. Vor jedem Befehl gibt es > aus, damit du siehst, was der Spieler getippt hat:

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));
}
Ausgabe
[labor] Die Tische sind voller Kolben. Die Tür hinter dir hat sich von selbst verriegelt.
Hier siehst du: taschenlampe
Ausgänge: norden
> Nimm taschenlampe
Du nimmst: taschenlampe. Sie funktioniert, und der Akku ist fast voll.
> schau
[labor] Die Tische sind voller Kolben. Die Tür hinter dir hat sich von selbst verriegelt.
Ausgänge: norden
> nimm taschenlampe
Hier gibt es keinen Gegenstand „taschenlampe“.
> tanze
Ich verstehe „tanze“ nicht.
> geh norden
[flur] Ein langer Flur. Die Deckenlampe flackert.
Ausgänge: süden, osten, westen
> geh osten
[lager] Regale mit Kartons bis zur Decke. An der Wand hängt ein Kalender: „Inventur am 19. Oktober“.
Hier siehst du: schlüssel
Ausgänge: westen
> nimm schlüssel
Du nimmst: schlüssel. Ein kleiner Messingschlüssel. Auf dem Anhänger steht „Büro“.
> geh westen
[flur] Ein langer Flur. Die Deckenlampe flackert.
Ausgänge: süden, osten, westen
> geh oben
In diese Richtung gibt es keinen Ausgang.
> geh westen
[büro] Ein Schreibtisch, ein Stuhl und ein Schrank voller Ordner.
Hier siehst du: zettel
Ausgänge: osten
> nimm zettel
Du nimmst: zettel. „Foyer-Code: 47 und der Tag der Inventur.“

Mit dieser Eingabe:

Eingabe
Nimm taschenlampe
schau
nimm taschenlampe
tanze
geh norden
geh osten
nimm schlüssel
geh westen
geh oben
geh westen
nimm zettel

Der Zettel sagt, der Foyer-Code ist 47 und der Tag der Inventur. Der Kalender im Lager sagt 19. Aber ein Foyer gibt es noch nicht, und das Büro ist auch nicht verschlossen. Die Rätsel kommen in der nächsten Lektion.

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));
}
Schritt für Schritt, in Worten
  1. Das Spiel beginnt im Raum „labor“. Die Befehle stehen in einem Dictionary: Das Wort ist der Schlüssel, das Befehlsobjekt ist der Wert.
  2. Du tippst „Nimm taschenlampe“. Die Zeile wird zu Kleinbuchstaben. „nimm“ ist ein Schlüssel im Dictionary, und der Befehl ist TakeCommand. Der Gegenstand „taschenlampe“ wandert aus dem Raum in den Rucksack.
  3. Du tippst „schau“. „schau“ ist ein Schlüssel im Dictionary, und der Befehl ist LookCommand. Er beschreibt den Raum noch einmal, jetzt ohne den Gegenstand, den du genommen hast.
  4. Du tippst „nimm taschenlampe“. „nimm“ ist ein Schlüssel im Dictionary, und der Befehl ist TakeCommand. Im Raum ist „taschenlampe“ nicht mehr, also lautet die Antwort „Hier gibt es keinen Gegenstand“.
  5. Du tippst „tanze“. „tanze“ steht nicht im Dictionary: find wirft UnknownCommandException, und handle sagt „Das verstehe ich nicht“.
  6. Du tippst „geh norden“. „geh“ ist ein Schlüssel im Dictionary, und der Befehl ist GoCommand. getExit("norden") im Raum „labor“ ergibt „flur“.
  7. Du tippst „geh osten“. „geh“ ist ein Schlüssel im Dictionary, und der Befehl ist GoCommand. getExit("osten") im Raum „flur“ ergibt „lager“.
  8. Du tippst „nimm schlüssel“. „nimm“ ist ein Schlüssel im Dictionary, und der Befehl ist TakeCommand. Der Gegenstand „schlüssel“ wandert aus dem Raum in den Rucksack.
  9. Du tippst „geh westen“. „geh“ ist ein Schlüssel im Dictionary, und der Befehl ist GoCommand. getExit("westen") im Raum „lager“ ergibt „flur“.
  10. Du tippst „geh oben“. „geh“ ist ein Schlüssel im Dictionary, und der Befehl ist GoCommand. Die Ausgänge von „flur“ haben keinen Schlüssel „oben“: In diese Richtung gibt es keinen Ausgang.
  11. Du tippst „geh westen“. „geh“ ist ein Schlüssel im Dictionary, und der Befehl ist GoCommand. getExit("westen") im Raum „flur“ ergibt „büro“.
  12. Du tippst „nimm zettel“. „nimm“ ist ein Schlüssel im Dictionary, und der Befehl ist TakeCommand. Der Gegenstand „zettel“ wandert aus dem Raum in den Rucksack.
  13. Die Eingabe ist zu Ende, und die Schleife hört auf. Der Rucksack: taschenlampe, schlüssel, zettel.

Übung

Mehr Befehle und Räume

Löse die Aufgaben in deinem Editor, mit den Klassen aus der Lektion. Füge das Neue von außen hinzu, mit game.addCommand und game.getRoom, ohne Game anzufassen.

1. Inventar. Ein Befehl „inventar“ sagt, was im Rucksack ist oder dass er leer ist.

Eingabe
inventar
nimm taschenlampe
geh norden
geh osten
nimm schlüssel
inventar
Ausgabe
[labor] Die Tische sind voller Kolben. Die Tür hinter dir hat sich von selbst verriegelt.
Hier siehst du: taschenlampe
Ausgänge: norden
> inventar
Dein Rucksack ist leer.
> nimm taschenlampe
Du nimmst: taschenlampe. Sie funktioniert, und der Akku ist fast voll.
> geh norden
[flur] Ein langer Flur. Die Deckenlampe flackert.
Ausgänge: süden, osten, westen
> geh osten
[lager] Regale mit Kartons bis zur Decke. An der Wand hängt ein Kalender: „Inventur am 19. Oktober“.
Hier siehst du: schlüssel
Ausgänge: westen
> nimm schlüssel
Du nimmst: schlüssel. Ein kleiner Messingschlüssel. Auf dem Anhänger steht „Büro“.
> inventar
In deinem Rucksack: taschenlampe, schlüssel

2. Ein neuer Raum. Ein Serverraum nördlich des Lagers, mit einem Netzwerkkabel darin. Seine Beschreibung lautet „Racks mit blinkenden Lichtern. Die Lüfter summen.“

Eingabe
geh norden
geh osten
geh norden
nimm kabel
geh süden
Ausgabe
[labor] Die Tische sind voller Kolben. Die Tür hinter dir hat sich von selbst verriegelt.
Hier siehst du: taschenlampe
Ausgänge: norden
> geh norden
[flur] Ein langer Flur. Die Deckenlampe flackert.
Ausgänge: süden, osten, westen
> geh osten
[lager] Regale mit Kartons bis zur Decke. An der Wand hängt ein Kalender: „Inventur am 19. Oktober“.
Hier siehst du: schlüssel
Ausgänge: norden, westen
> geh norden
[serverraum] Racks mit blinkenden Lichtern. Die Lüfter summen.
Hier siehst du: kabel
Ausgänge: süden
> nimm kabel
Du nimmst: kabel. Ein langes Netzwerkkabel.
> geh süden
[lager] Regale mit Kartons bis zur Decke. An der Wand hängt ein Kalender: „Inventur am 19. Oktober“.
Hier siehst du: schlüssel
Ausgänge: norden, westen

3. Synonyme. „schnapp“ soll „nimm“ bedeuten, „lauf“ soll „geh“ bedeuten und „guck“ soll „schau“ bedeuten. Tipp: Derselbe Befehl kann der Wert mehrerer Schlüssel sein.

Eingabe
schnapp taschenlampe
lauf norden
guck
Ausgabe
[labor] Die Tische sind voller Kolben. Die Tür hinter dir hat sich von selbst verriegelt.
Hier siehst du: taschenlampe
Ausgänge: norden
> schnapp taschenlampe
Du nimmst: taschenlampe. Sie funktioniert, und der Akku ist fast voll.
> lauf norden
[flur] Ein langer Flur. Die Deckenlampe flackert.
Ausgänge: süden, osten, westen
> guck
[flur] Ein langer Flur. Die Deckenlampe flackert.
Ausgänge: süden, osten, westen

Weitere Aufgaben

  • Ablegen. Ein Befehl „leg schlüssel“ legt den Gegenstand aus dem Rucksack zurück in den Raum.
  • Hilfe. Ein Befehl „hilfe“ listet alle Wörter aus dem Dictionary der Befehle auf.
  • Kurze Richtungen. „n“, „s“, „o“, „w“ sollen norden, süden, osten, westen bedeuten.

Test

Teste dich

1. Der Spieler tippt „GEH NORDEN“ (mit zwei Leerzeichen). Was bekommt handle nach trim, Kleinbuchstaben und split an einem Leerzeichen?
2. Warum ist jeder Befehl eine eigene Klasse und kein Zweig in einem großen if/else?
3. Was gibt getExit für eine Richtung zurück, die nicht im Dictionary steht?
4. Ein unbekannter Befehl wirft UnknownCommandException. Wer fängt sie?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·