Zum Inhalt
wannacode

Das verschlossene Labor 2: Rätsel und der Ausweg

Kurz gesagt: Das Ende des Fluchtspiels: eine verschlossene Tür und ein Schlüssel, ein Zahlenschloss aus Hinweisen, der Sieg mit einer Zugzahl und ein Speichercode, den das Spiel ausgibt und wieder einliest.

Problem

Alle Türen sind offen

In der ersten Lektion ist das Labor nur in Worten verschlossen: Der Spieler geht, wohin er will. Der Schlüssel und der Zettel liegen nur herum und nützen nichts. Das Spiel hat kein Ende.

Ein Fluchtspiel braucht:

  • eine verschlossene Tür, die sich mit einem Gegenstand öffnet;
  • ein Zahlenschloss, dessen Code du aus Hinweisen in verschiedenen Räumen zusammensetzt;
  • ein Ende: Der Spieler ist draußen, mit einer Zugzahl zum Angeben;
  • einen Speichercode: Das Spiel ist lang, und der Spieler will aufhören und später weitermachen.

Der Speichercode ist ein kurzer Text. Das Spiel gibt ihn aus, und der Spieler tippt ihn wieder ein. Ist der Text kaputt, muss das Spiel das sagen und nicht abstürzen.

Die Idee

Eine verschlossene Tür

Der Raum bekommt ein Schloss. Das Geheimnis ist der Name des Gegenstands, der es öffnet, oder der Code:

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

„geh“ hält jetzt vor einer verschlossenen Tür an:

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.";
}
if (next.isLocked()) {
return "Die Tür zum " + next.getName() + " ist verschlossen.";
}
game.setCurrent(next);
return next.describe();
}
}

In der Welt ist das Büro mit dem Schlüssel aus dem Lager verschlossen und das neue Foyer mit einem Code:

Room foyer = new Room("foyer", "Die Glastür zur Straße ist offen.");
corridor.addExit("norden", foyer);
foyer.addExit("süden", corridor);
office.lockWithKey("schlüssel");
foyer.lockWithCode("4719");

Die Idee

Der Schlüssel schließt auf

„benutze schlüssel“ prüft den Rucksack und sucht dann eine verschlossene Nachbartür, die sich mit diesem Gegenstand öffnet:

class UseCommand implements Command {
@Override
public String execute(Game game, String argument) {
if (!game.has(argument)) {
return "Du hast keinen Gegenstand „" + argument + "“.";
}
for (Room room : game.getCurrent().getNeighbours()) {
if (!room.isCodeLock() && room.opensWith(argument)) {
game.unlock(room);
return "Du schließt die Tür zum " + room.getName() + " auf.";
}
}
return "„" + argument + "“ hilft hier nicht.";
}
}

Das Aufschließen läuft über das Spiel, nicht direkt über den Raum. So merkt sich das Spiel, welche Türen offen sind. Das brauchst du für den Speichercode.

Probier es aus

Welcher Schlüssel, welche Tür?

Der Spieler tippt diese Befehle ab dem Start des Spiels. Was antwortet das Spiel der Reihe nach auf die drei „benutze schlüssel“-Befehle?

geh norden
benutze schlüssel
geh osten
nimm schlüssel
benutze schlüssel
geh westen
benutze schlüssel
Antwort

Die Idee

Ein Zahlenschloss aus Hinweisen

Das Foyer ist mit vier Ziffern verschlossen. Das Spiel sagt sie nicht direkt:

  • der Zettel im Büro: „Foyer-Code: 47 und der Tag der Inventur.“;
  • der Kalender im Lager: „Inventur am 19. Oktober“.

Der Code ist 4719. Der Befehl „tippe“ vergleicht ihn mit dem Zahlenschloss einer Nachbartür:

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 "Das Schloss klickt. Die Tür zum " + room.getName() + " ist offen.";
}
return "Falscher Code. Das Schloss blinkt rot.";
}
}
return "Hier gibt es kein Zahlenschloss.";
}
}

Das Rätsel steckt in den Daten, nicht im Code: Ein anderer Code und andere Hinweise ergeben ein anderes Spiel mit denselben Klassen.

Sieh es dir an

Draußen

Das Spiel ist gewonnen, wenn der Spieler das Foyer erreicht:

public boolean isWon() {
return current.getName().equals("foyer");
}

handle zählt jeden Befehl, den es versteht, als Zug. Die Schleife in main endet, wenn du gewinnst:

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("Du bist nach " + game.getMoves() + " Zügen draußen.");
}
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: norden, süden, osten, westen
> geh westen
Die Tür zum büro ist verschlossen.
> 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: norden, süden, osten, westen
> benutze schlüssel
Du schließt die Tür zum büro auf.
> 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.“
> geh osten
[flur] Ein langer Flur. Die Deckenlampe flackert.
Ausgänge: norden, süden, osten, westen
> geh norden
Die Tür zum foyer ist verschlossen.
> tippe 1234
Falscher Code. Das Schloss blinkt rot.
> tippe 4719
Das Schloss klickt. Die Tür zum foyer ist offen.
> geh norden
[foyer] Die Glastür zur Straße ist offen.
Ausgänge: süden
Du bist nach 13 Zügen draußen.

Mit dieser Eingabe:

Eingabe
geh norden
geh westen
geh osten
nimm schlüssel
geh westen
benutze schlüssel
geh westen
nimm zettel
geh osten
geh norden
tippe 1234
tippe 4719
geh norden

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("Du bist nach " + game.getMoves() + " Zügen draußen.");
}
Schritt für Schritt, in Worten
  1. Du startest im Raum „labor“. Die Tür zum Raum „büro“ geht mit „schlüssel“ auf, die Tür zum Raum „foyer“ mit einem vierstelligen Code.
  2. Zug 1: „geh norden“. Du betrittst „flur“.
  3. Zug 2: „geh westen“. Die Tür zum Raum „büro“ ist verschlossen: „geh“ hält davor an, bevor es dich bewegt.
  4. Zug 3: „geh osten“. Du betrittst „lager“. An der Wand hängt ein Kalender: „Inventur am 19. Oktober“.
  5. Zug 4: „nimm schlüssel“. Du nimmst „schlüssel“. Damit geht „büro“ auf.
  6. Zug 5: „geh westen“. Du betrittst „flur“.
  7. Zug 6: „benutze schlüssel“. „schlüssel“ ist im Rucksack und öffnet die verschlossene Nachbartür zu „büro“.
  8. Zug 7: „geh westen“. Du betrittst „büro“.
  9. Zug 8: „nimm zettel“. Du nimmst „zettel“: „Foyer-Code: 47 und der Tag der Inventur.“
  10. Zug 9: „geh osten“. Du betrittst „flur“.
  11. Zug 10: „geh norden“. Die Tür zum Raum „foyer“ ist verschlossen: „geh“ hält davor an, bevor es dich bewegt.
  12. Zug 11: „tippe 1234“. 1234 ist nicht der Code. Das Schloss blinkt rot.
  13. Zug 12: „tippe 4719“. 47 vom Zettel und 19 vom Kalender: 4719. Das Schloss von „foyer“ klickt.
  14. Zug 13: „geh norden“. Du betrittst „foyer“.
  15. Du erreichst „foyer“: isWon() ist true, und die Schleife endet. Du bist nach 13 Zügen draußen.

Die Idee

Ein Speichercode

Der ganze Zustand des Spiels besteht aus vier Dingen: wo der Spieler ist, was er trägt, welche Türen er geöffnet hat und wie viele Züge er gespielt hat. Sie kommen in einen kurzen Text, getrennt durch ;:

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

Zum Beispiel büro;schlüssel,zettel;büro;9. Die Listen sind mit Kommas getrennt, und eine leere Liste ist -, damit nie eine leere Stelle zwischen zwei ; steht.

Das Spiel gibt den Code aus, und der Spieler tippt ihn mit „lade“ wieder ein. Du brauchst dafür keine Datei: Den Code kannst du auf Papier abschreiben oder einem Freund schicken.

Genauer, für Neugierige

Du kannst denselben Text auch in eine Datei schreiben, mit dem, was du im Modul „Text und Dateien“ gelernt hast:

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

Files.writeString und Files.readString werfen IOException, darum muss die aufrufende Methode sie fangen oder deklarieren.

Die Idee

Ein kaputter Code

Der Spieler vertippt sich vielleicht oder will schummeln: im verschlossenen Büro auftauchen. Dann wirft das Laden eine eigene Exception:

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

load baut eine neue Welt und prüft den Code Teil für Teil an ihr. Erst wenn alles durch ist, wird die neue Welt zur aktuellen:

public void load(String code) throws InvalidSaveCodeException {
String[] parts = code.split(";");
if (parts.length != 4) {
throw new InvalidSaveCodeException("es braucht 4 Teile, getrennt durch ;");
}
Map<String, Room> world = buildWorld();
Room room = world.get(parts[0]);
if (room == null) {
throw new InvalidSaveCodeException("es gibt keinen Raum " + 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("es gibt keinen Gegenstand " + 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("es gibt keine verschlossene Tür zu " + name);
}
door.unlock();
doors.add(name);
}
}
if (room.isLocked()) {
throw new InvalidSaveCodeException("der Raum " + room.getName() + " ist verschlossen");
}
int savedMoves;
try {
savedMoves = Integer.parseInt(parts[3]);
} catch (NumberFormatException e) {
throw new InvalidSaveCodeException("die Züge sind keine Zahl (" + 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;
}

Der Befehl „lade“ fängt die Exception und sagt dem Spieler, was nicht stimmt:

class LoadCommand implements Command {
@Override
public String execute(Game game, String argument) {
try {
game.load(argument);
return "Du machst dort weiter, wo du aufgehört hast.\n" + game.getCurrent().describe();
} catch (InvalidSaveCodeException e) {
return "Der Code funktioniert nicht: " + e.getMessage() + ".";
}
}
}
Genauer, für Neugierige
  • Integer.parseInt wirft NumberFormatException, und load macht daraus eine eigene Exception. So fängt „lade“ nur eine einzige Exception, und die Meldung ist in der Sprache des Spiels.
  • Der Code ist kein Schutz gegen Schummeln: Wer das Format herausfindet, kann „foyer“ hineinschreiben. Die Prüfung, dass der Raum nicht verschlossen ist, schützt nur vor dem Offensichtlichen. Echte Spiele fügen eine Prüfsumme hinzu: eine Zahl, die aus dem ganzen Text berechnet wird und nicht mehr stimmt, wenn jemand ihn angefasst hat.

Probier es aus

Finde den Fehler

Der Spieler tippt lade büro;hammer;-;3. Das Spiel sagt „Der Code funktioniert nicht: es gibt keinen Gegenstand hammer.“, aber nach „schau“ steht der Spieler im Büro, das verschlossen ist. In welcher Zeile steckt der Fehler?

Wähle die Zeile mit dem Fehler.

Sieh es dir an

Speichern und weitermachen

Der Spieler nimmt den Schlüssel und speichert. Dann probiert er drei kaputte Codes und zuletzt einen, der funktioniert:

Eingabe
geh norden
geh osten
nimm schlüssel
speichere
lade lager;hammer;-;4
lade flur;schlüssel;büro;vier
lade büro;schlüssel;-;7
lade büro;schlüssel;büro;7
geh osten
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: norden, 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“.
> speichere
Speichercode: lager;schlüssel;-;4
> lade lager;hammer;-;4
Der Code funktioniert nicht: es gibt keinen Gegenstand hammer.
> lade flur;schlüssel;büro;vier
Der Code funktioniert nicht: die Züge sind keine Zahl (vier).
> lade büro;schlüssel;-;7
Der Code funktioniert nicht: der Raum büro ist verschlossen.
> lade büro;schlüssel;büro;7
Du machst dort weiter, wo du aufgehört hast.
[büro] Ein Schreibtisch, ein Stuhl und ein Schrank voller Ordner.
Hier siehst du: zettel
Ausgänge: osten
> geh osten
[flur] Ein langer Flur. Die Deckenlampe flackert.
Ausgänge: norden, süden, osten, westen

Der letzte Code wurde von Hand geschrieben, ist aber gültig: Das Büro ist offen, und der Schlüssel liegt im Rucksack. Das Spiel weiß nicht, ob es den Code selbst ausgegeben hat. Es weiß nur, dass es ein möglicher Zustand ist.

Übung

Mehr Rätsel

Löse die Aufgaben in deinem Editor, mit den Klassen aus der Lektion.

1. Der dunkle Keller. Südlich vom Labor liegt ein Keller („Ein niedriger Keller mit Rohren an der Decke.“), und darin eine Ersatzsicherung. Hinein kommst du nur mit einer Taschenlampe im Rucksack. Tipp: Ein Befehl, der GoCommand erweitert, prüft auf den Keller und ruft die Basisklasse auf. Registriere ihn mit game.addCommand anstelle von „geh“.

Eingabe
geh süden
nimm taschenlampe
geh süden
nimm sicherung
Ausgabe
[labor] Die Tische sind voller Kolben. Die Tür hinter dir hat sich von selbst verriegelt.
Hier siehst du: taschenlampe
Ausgänge: norden, süden
> geh süden
Es ist zu dunkel. Ohne Licht findest du nichts.
> nimm taschenlampe
Du nimmst: taschenlampe. Sie funktioniert, und der Akku ist fast voll.
> geh süden
[keller] Ein niedriger Keller mit Rohren an der Decke.
Hier siehst du: sicherung
Ausgänge: norden
> nimm sicherung
Du nimmst: sicherung. Eine Ersatzsicherung für den Sicherungskasten.

2. Ein Timer. Die Wachen kommen nach 12 Zügen. Ist der Spieler bis dahin nicht draußen, ist das Spiel vorbei.

Eingabe
geh norden
geh osten
nimm schlüssel
geh westen
geh westen
benutze schlüssel
geh westen
nimm zettel
geh osten
geh osten
geh westen
tippe 4719
geh norden
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: norden, 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: norden, süden, osten, westen
> geh westen
Die Tür zum büro ist verschlossen.
> benutze schlüssel
Du schließt die Tür zum büro auf.
> 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.“
> geh osten
[flur] Ein langer Flur. Die Deckenlampe flackert.
Ausgänge: norden, süden, osten, westen
> geh osten
[lager] Regale mit Kartons bis zur Decke. An der Wand hängt ein Kalender: „Inventur am 19. Oktober“.
Ausgänge: westen
> geh westen
[flur] Ein langer Flur. Die Deckenlampe flackert.
Ausgänge: norden, süden, osten, westen
> tippe 4719
Das Schloss klickt. Die Tür zum foyer ist offen.
Die Wachen kamen. Sie haben dich nach 12 Zügen erwischt.

3. Ein Hinweis für einen Zug. Ein Befehl „hinweis“ nennt den nächsten Schritt je nach Zustand: der Schlüssel, das Büro, der Zettel, der Kalender. Er kostet einen Zug, wie jeder Befehl.

Eingabe
hinweis
geh norden
geh osten
nimm schlüssel
hinweis
geh westen
geh westen
benutze schlüssel
hinweis
Ausgabe
[labor] Die Tische sind voller Kolben. Die Tür hinter dir hat sich von selbst verriegelt.
Hier siehst du: taschenlampe
Ausgänge: norden
> hinweis
Hinweis: Sieh dich im Lager um.
> geh norden
[flur] Ein langer Flur. Die Deckenlampe flackert.
Ausgänge: norden, 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“.
> hinweis
Hinweis: Der Schlüssel gehört zum Büro.
> geh westen
[flur] Ein langer Flur. Die Deckenlampe flackert.
Ausgänge: norden, süden, osten, westen
> geh westen
Die Tür zum büro ist verschlossen.
> benutze schlüssel
Du schließt die Tür zum büro auf.
> hinweis
Hinweis: Im Büro gibt es etwas zu lesen.

Weitere Aufgaben

  • Die Sicherung. Das Zahlenschloss funktioniert erst, wenn die Sicherung aus dem Keller im Flur „benutzt“ wurde.
  • Züge im Code. Lass den Timer auch nach „lade“ funktionieren: Der Code trägt die Züge schon in sich.
  • Eine Prüfsumme. Füge einen fünften Teil hinzu: die Summe der Längen der ersten vier. Ein Code mit falscher Summe ist kaputt.

Test

Teste dich

1. Der Spieler steht im Lager und tippt „benutze schlüssel“. Der Schlüssel ist im Rucksack. Was passiert?
2. Der Speichercode lautet „flur;schlüssel,zettel;büro;9“. Was sagt der dritte Teil?
3. Warum prüft load alles an einer neuen Welt und ersetzt erst ganz am Ende das aktuelle Spiel?
4. Warum steigt der Zugzähler nach find und nicht davor?

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