Zum Inhalt
wannacode

Bildschirme, die eine Antwort zurückgeben

Kurz gesagt: Ein Bildschirm, der eine Antwort zurückgibt, schließt sich mit pop(result) und reicht das Ergebnis an den Bildschirm darunter weiter (onResult), während Änderungen am Stapel während des Zuges notiert und an seinem Ende mit apply ausgeführt werden.

Problem

Der Bildschirm fragt. Wer hört die Antwort?

In der letzten Lektion haben sich Bildschirme nur geöffnet und geschlossen. Jetzt muss ein Bildschirm etwas fragen und eine Antwort bekommen. Der Kampf hat zwei Feinde, der Spieler drückt „Angriff“, und der Kampf muss wissen, welcher. Das Menü muss „Bist du sicher?“ fragen, bevor es das Spiel beendet.

Der erste Versuch ist eine gemeinsame Variable: Der Ziel-Bildschirm schreibt die Wahl in selectedTarget, und der Kampf liest sie. Der Spieler wählt den Goblin, öffnet dann die Wahl noch einmal und überlegt es sich anders (x):

static String[] enemies = { "Wolf", "Goblin" };
static int selectedTarget = -1;
static void targetScreen(String command) {
if (!command.equals("x")) selectedTarget = Integer.parseInt(command) - 1;
}
static void fightTurn() {
if (selectedTarget >= 0) System.out.println("Du triffst " + enemies[selectedTarget] + ".");
else System.out.println("Niemand wird getroffen.");
}
// Der Spieler wählt 2 und überlegt es sich dann anders (x).
targetScreen("2");
fightTurn();
targetScreen("x");
fightTurn();
Ausgabe
Du triffst Goblin.
Du triffst Goblin.

Der zweite Treffer landet beim Goblin, obwohl der Spieler abgebrochen hat. Die Wahl vom letzten Zug steht noch in der Variable. Wer soll sie auf -1 zurücksetzen? Der Kampf, nachdem er sie gelesen hat? Und was, wenn das Inventar dieselbe Variable auch benutzt? Eine gemeinsame Variable hat keinen Besitzer, deshalb bleiben alte Antworten in ihr.

Der zweite Versuch ist, das Ziel den Kampf selbst aufrufen zu lassen:

class TargetScreen extends Screen {
FightScreen fight;
@Override
void handle(String command) {
// Das Ziel kennt den Kampf und ruft ihn selbst auf.
fight.attack(Integer.parseInt(command) - 1);
stack.pop();
}
}

Das funktioniert, aber jetzt kennt das Ziel den Kampf. Im Spiel fragt „Wen?“ ein Angriff, ein Zauber und ein Trank, der einen Verbündeten heilt. Jeder von ihnen will einen eigenen Ziel-Bildschirm, oder einen Bildschirm mit einem if für alle, die ihn benutzen. Du brauchst einen Weg, damit die Antwort zu dem zurückgeht, der gefragt hat, ohne dass der Bildschirm, der antwortet, weiß, wer das ist.

Die Idee

pop(result) und onResult

Der Bildschirm, der fragt, öffnet mit push den, der antwortet. Der, der antwortet, entfernt sich mit pop selbst und reicht die Antwort weiter: pop(result). Der Stapel weiß, wer darunter liegt: genau der Bildschirm, der gefragt hat. Er gibt die Antwort weiter, indem er dessen onResult aufruft.

Der Bildschirm bekommt eine weitere Methode. Standardmäßig tut sie nichts: Die meisten Bildschirme fragen nie etwas.

class Screen {
String name;
Screen(String name) {
this.name = name;
}
void draw() {}
void handle(String command) {}
void update() {}
// Die Antwort des Bildschirms, der über diesem lag.
void onResult(Object result) {}
}

Und pop aus der letzten Lektion wird zu pop(result):

void pop(Object result) {
screens.remove(screens.size() - 1);
Screen below = top();
if (below != null) below.onResult(result);
}

So kennen sich die beiden Seiten nicht. Das Ziel kennt die Frage und die Form der Antwort: die Nummer eines Feindes oder null für „abbrechen“. Der Kampf weiß, was er mit der Nummer tun soll. Es ist wie der Aufruf einer Methode, die einen Wert zurückgibt, nur dass zwischen Frage und Antwort Züge vergehen: Der Spieler denkt nach, drückt eine Taste und überlegt es sich vielleicht anders.

Sieh es dir an

Ein Ziel wählen

Das Ziel zeichnet eine Liste der lebenden Feinde und wartet auf eine Ziffer. x ist „zurück“ und gibt null zurück. Die Nummer wird ab 0 gezählt, weil sie ein Index in die Liste ist:

class TargetScreen extends Screen {
ScreenStack stack;
List<Enemy> enemies;
TargetScreen(ScreenStack stack, List<Enemy> enemies) {
super("Ziel");
this.stack = stack;
this.enemies = enemies;
}
@Override
void draw() {
String line = "Wen triffst du?";
for (int i = 0; i < enemies.size(); i++) {
line += " " + (i + 1) + ") " + enemies.get(i).name;
}
System.out.println(line + " x) zurück");
}
@Override
void handle(String command) {
if (command.equals("x")) {
stack.pop(null);
return;
}
int choice = Integer.parseInt(command);
if (choice >= 1 && choice <= enemies.size()) stack.pop(choice - 1);
}
}

Der Kampf öffnet das Ziel bei 1 und liest die Antwort in onResult. Wenn keine Feinde mehr da sind, gibt der Kampf auch dem Bildschirm unter sich, der Karte, eine Antwort zurück: „Sieg“.

class FightScreen extends Screen {
ScreenStack stack;
List<Enemy> enemies = new ArrayList<>();
FightScreen(ScreenStack stack) {
super("Kampf");
this.stack = stack;
enemies.add(new Enemy("Wolf", 4));
enemies.add(new Enemy("Goblin", 7));
}
@Override
void draw() {
String line = "Kampf:";
for (Enemy enemy : enemies) line += " " + enemy.name + " " + enemy.hp;
System.out.println(line + " | 1) Angriff");
}
@Override
void handle(String command) {
if (command.equals("1")) stack.push(new TargetScreen(stack, enemies));
}
@Override
void onResult(Object result) {
if (result == null) {
System.out.println("Du überlegst es dir anders. Niemand wird getroffen.");
return;
}
Enemy enemy = enemies.get((int) result);
enemy.hp -= 4;
if (enemy.hp > 0) {
System.out.println("Du triffst " + enemy.name + " (4).");
return;
}
System.out.println(enemy.name + " fällt.");
enemies.remove(enemy);
if (enemies.isEmpty()) stack.pop("Sieg");
}
}

Das Programm liest seine Befehle aus der Eingabe. Jeder Zug zeichnet nur den obersten Bildschirm, damit die Ausgabe kurz bleibt:

ScreenStack stack = new ScreenStack();
stack.push(new MapScreen(stack));
Scanner in = new Scanner(System.in);
while (stack.top() != null) {
stack.top().draw();
String command = in.nextLine().trim();
System.out.println("> " + command);
stack.top().handle(command);
}
System.out.println("Auf Wiedersehen.");
Ausgabe
Karte: Tränke 2 | f) Kampf d) Trank wegwerfen q) beenden
> f
Kampf: Wolf 4 Goblin 7 | 1) Angriff
> 1
Wen triffst du? 1) Wolf 2) Goblin x) zurück
> 2
Du triffst Goblin (4).
Kampf: Wolf 4 Goblin 3 | 1) Angriff
> 1
Wen triffst du? 1) Wolf 2) Goblin x) zurück
> x
Du überlegst es dir anders. Niemand wird getroffen.
Kampf: Wolf 4 Goblin 3 | 1) Angriff
> 1
Wen triffst du? 1) Wolf 2) Goblin x) zurück
> 2
Goblin fällt.
Kampf: Wolf 4 | 1) Angriff
> 1
Wen triffst du? 1) Wolf x) zurück
> 1
Wolf fällt.
Sieg! Zurück zur Karte.
Karte: Tränke 2 | f) Kampf d) Trank wegwerfen q) beenden
> d
Bist du sicher, dass du einen Trank wegwirfst? 1) ja 2) nein
> 2
Der Trank bleibt.
Karte: Tränke 2 | f) Kampf d) Trank wegwerfen q) beenden
> q
Bist du sicher, dass du das Spiel verlässt? 1) ja 2) nein
> 1
Auf Wiedersehen.

Die Eingabe ist:

Eingabe
f
1
2
1
x
1
2
1
1
d
2
q
1

Das Abbrechen (x) trifft niemanden: Die Antwort ist leer, und der Kampf weiß es. Die Wahl vom vorigen Zug bleibt nirgends liegen.

Genauer, für Neugierige

Das ganze Programm, mit der Karte und der Frage „Bist du sicher?“ von der nächsten Karte:

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
class Screen {
String name;
Screen(String name) {
this.name = name;
}
void draw() {}
void handle(String command) {}
void update() {}
// Die Antwort des Bildschirms, der über diesem lag.
void onResult(Object result) {}
}
class ScreenStack {
List<Screen> screens = new ArrayList<>();
void push(Screen screen) {
screens.add(screen);
}
void pop(Object result) {
screens.remove(screens.size() - 1);
Screen below = top();
if (below != null) below.onResult(result);
}
Screen top() {
return screens.isEmpty() ? null : screens.get(screens.size() - 1);
}
String names() {
List<String> names = new ArrayList<>();
for (Screen screen : screens) names.add(screen.name);
return "[" + String.join(", ", names) + "]";
}
}
class Enemy {
String name;
int hp;
Enemy(String name, int hp) {
this.name = name;
this.hp = hp;
}
}
class TargetScreen extends Screen {
ScreenStack stack;
List<Enemy> enemies;
TargetScreen(ScreenStack stack, List<Enemy> enemies) {
super("Ziel");
this.stack = stack;
this.enemies = enemies;
}
@Override
void draw() {
String line = "Wen triffst du?";
for (int i = 0; i < enemies.size(); i++) {
line += " " + (i + 1) + ") " + enemies.get(i).name;
}
System.out.println(line + " x) zurück");
}
@Override
void handle(String command) {
if (command.equals("x")) {
stack.pop(null);
return;
}
int choice = Integer.parseInt(command);
if (choice >= 1 && choice <= enemies.size()) stack.pop(choice - 1);
}
}
class FightScreen extends Screen {
ScreenStack stack;
List<Enemy> enemies = new ArrayList<>();
FightScreen(ScreenStack stack) {
super("Kampf");
this.stack = stack;
enemies.add(new Enemy("Wolf", 4));
enemies.add(new Enemy("Goblin", 7));
}
@Override
void draw() {
String line = "Kampf:";
for (Enemy enemy : enemies) line += " " + enemy.name + " " + enemy.hp;
System.out.println(line + " | 1) Angriff");
}
@Override
void handle(String command) {
if (command.equals("1")) stack.push(new TargetScreen(stack, enemies));
}
@Override
void onResult(Object result) {
if (result == null) {
System.out.println("Du überlegst es dir anders. Niemand wird getroffen.");
return;
}
Enemy enemy = enemies.get((int) result);
enemy.hp -= 4;
if (enemy.hp > 0) {
System.out.println("Du triffst " + enemy.name + " (4).");
return;
}
System.out.println(enemy.name + " fällt.");
enemies.remove(enemy);
if (enemies.isEmpty()) stack.pop("Sieg");
}
}
class ConfirmScreen extends Screen {
ScreenStack stack;
String question;
ConfirmScreen(ScreenStack stack, String question) {
super(question);
this.stack = stack;
this.question = question;
}
@Override
void draw() {
System.out.println(question + " 1) ja 2) nein");
}
@Override
void handle(String command) {
if (command.equals("1")) stack.pop(true);
else if (command.equals("2")) stack.pop(false);
}
}
class MapScreen extends Screen {
ScreenStack stack;
int potions = 2;
String pending = "";
MapScreen(ScreenStack stack) {
super("Karte");
this.stack = stack;
}
@Override
void draw() {
System.out.println("Karte: Tränke " + potions + " | f) Kampf d) Trank wegwerfen q) beenden");
}
@Override
void handle(String command) {
if (command.equals("f")) {
stack.push(new FightScreen(stack));
} else if (command.equals("d")) {
pending = "drop";
stack.push(new ConfirmScreen(stack, "Bist du sicher, dass du einen Trank wegwirfst?"));
} else if (command.equals("q")) {
pending = "quit";
stack.push(new ConfirmScreen(stack, "Bist du sicher, dass du das Spiel verlässt?"));
}
}
@Override
void onResult(Object result) {
String asked = pending;
pending = "";
if (asked.equals("drop")) {
if (result.equals(true)) {
potions--;
System.out.println("Du wirfst einen Trank weg.");
} else {
System.out.println("Der Trank bleibt.");
}
} else if (asked.equals("quit")) {
if (result.equals(true)) stack.pop(null);
} else if (result.equals("Sieg")) {
System.out.println("Sieg! Zurück zur Karte.");
}
}
}
public class Main {
public static void main(String[] args) {
ScreenStack stack = new ScreenStack();
stack.push(new MapScreen(stack));
Scanner in = new Scanner(System.in);
while (stack.top() != null) {
stack.top().draw();
String command = in.nextLine().trim();
System.out.println("> " + command);
stack.top().handle(command);
}
System.out.println("Auf Wiedersehen.");
}
}

Die Idee

Eine Frage für alles: „Bist du sicher?“

Der Frage-Bildschirm weiß nicht, was er fragt. Er zeigt den Text, den er bekommen hat, und gibt true oder false zurück:

class ConfirmScreen extends Screen {
ScreenStack stack;
String question;
ConfirmScreen(ScreenStack stack, String question) {
super(question);
this.stack = stack;
this.question = question;
}
@Override
void draw() {
System.out.println(question + " 1) ja 2) nein");
}
@Override
void handle(String command) {
if (command.equals("1")) stack.pop(true);
else if (command.equals("2")) stack.pop(false);
}
}

Die Karte benutzt ihn für zwei verschiedene Fragen: ob ein Trank weggeworfen und ob das Spiel beendet werden soll. Aber die Antwort ist nur „ja“ oder „nein“. Woher weiß die Karte, auf welche Frage sie antwortet? Sie merkt sich, was sie gefragt hat. Vor dem push schreibt sie pending, und wenn die Antwort kommt, liest sie es und leert es sofort:

class MapScreen extends Screen {
ScreenStack stack;
int potions = 2;
String pending = "";
MapScreen(ScreenStack stack) {
super("Karte");
this.stack = stack;
}
@Override
void draw() {
System.out.println("Karte: Tränke " + potions + " | f) Kampf d) Trank wegwerfen q) beenden");
}
@Override
void handle(String command) {
if (command.equals("f")) {
stack.push(new FightScreen(stack));
} else if (command.equals("d")) {
pending = "drop";
stack.push(new ConfirmScreen(stack, "Bist du sicher, dass du einen Trank wegwirfst?"));
} else if (command.equals("q")) {
pending = "quit";
stack.push(new ConfirmScreen(stack, "Bist du sicher, dass du das Spiel verlässt?"));
}
}
@Override
void onResult(Object result) {
String asked = pending;
pending = "";
if (asked.equals("drop")) {
if (result.equals(true)) {
potions--;
System.out.println("Du wirfst einen Trank weg.");
} else {
System.out.println("Der Trank bleibt.");
}
} else if (asked.equals("quit")) {
if (result.equals(true)) stack.pop(null);
} else if (result.equals("Sieg")) {
System.out.println("Sieg! Zurück zur Karte.");
}
}
}

Das Leeren ist wichtig. Bliebe pending auf „drop“, würde die nächste Antwort vom Kampf („Sieg“) als Antwort auf die Trank-Frage gelesen. Das ist derselbe Fehler wie bei der gemeinsamen Variable vom Anfang, nur jetzt innerhalb eines Bildschirms, der seine eigene Frage besitzt. Das echte Spiel macht dasselbe: Der Kampf merkt sich, ob er nach einem Angriff, einer Fähigkeit oder einem Trank gefragt hat.

Problem

Der Stapel ändert sich, während der Zug noch läuft

Bisher hat die Schleife nur handle für den obersten aufgerufen. Ein echter Zug hat auch update: Der Bildschirm aktualisiert nach dem Befehl seinen Zustand. Hier ist derselbe Kampf, aber jeder Bildschirm schreibt in ein Protokoll, was er tut. Im update zählt das Ziel die Feinde in seiner Liste:

class TargetScreen extends Screen {
ScreenStack stack;
List<String> enemies;
TargetScreen(ScreenStack stack, List<String> enemies) {
super("Ziel");
this.stack = stack;
this.enemies = enemies;
}
@Override
void handle(String command) {
int index = Integer.parseInt(command) - 1;
System.out.println("Ziel bekommt " + command + " und liefert " + index);
stack.pop(index);
}
@Override
void update() {
System.out.println("Ziel aktualisiert sich, Feinde in der Liste: " + enemies.size());
}
}

Die Schleife ist wie im echten Spiel: den obersten nehmen, ihm den Befehl geben, ihn aktualisieren.

Screen top = stack.top();
top.handle(command);
top.update();
import java.util.ArrayList;
import java.util.List;
class Screen {
String name;
Screen(String name) {
this.name = name;
}
void draw() {}
void handle(String command) {}
void update() {}
// Die Antwort des Bildschirms, der über diesem lag.
void onResult(Object result) {}
}
class ScreenStack {
List<Screen> screens = new ArrayList<>();
void push(Screen screen) {
screens.add(screen);
}
void pop(Object result) {
screens.remove(screens.size() - 1);
Screen below = top();
if (below != null) below.onResult(result);
}
Screen top() {
return screens.isEmpty() ? null : screens.get(screens.size() - 1);
}
String names() {
List<String> names = new ArrayList<>();
for (Screen screen : screens) names.add(screen.name);
return "[" + String.join(", ", names) + "]";
}
}
class TargetScreen extends Screen {
ScreenStack stack;
List<String> enemies;
TargetScreen(ScreenStack stack, List<String> enemies) {
super("Ziel");
this.stack = stack;
this.enemies = enemies;
}
@Override
void handle(String command) {
int index = Integer.parseInt(command) - 1;
System.out.println("Ziel bekommt " + command + " und liefert " + index);
stack.pop(index);
}
@Override
void update() {
System.out.println("Ziel aktualisiert sich, Feinde in der Liste: " + enemies.size());
}
}
class FightScreen extends Screen {
ScreenStack stack;
List<String> enemies = new ArrayList<>(List.of("Wolf", "Goblin"));
FightScreen(ScreenStack stack) {
super("Kampf");
this.stack = stack;
}
@Override
void handle(String command) {
System.out.println("Kampf bekommt " + command + " und öffnet Ziel");
stack.push(new TargetScreen(stack, enemies));
}
@Override
void onResult(Object result) {
String enemy = enemies.remove((int) result);
System.out.println("Kampf erhält " + result + ": " + enemy + " fällt");
if (enemies.isEmpty()) {
System.out.println("Kampf: keine Feinde mehr, liefert Sieg");
stack.pop("Sieg");
}
}
}
class MapScreen extends Screen {
ScreenStack stack;
MapScreen(ScreenStack stack) {
super("Karte");
this.stack = stack;
}
@Override
void onResult(Object result) {
System.out.println("Karte erhält " + result + ": öffnet Stufenaufstieg");
stack.push(new Screen("Stufenaufstieg"));
}
}
public class Main {
public static void main(String[] args) {
ScreenStack stack = new ScreenStack();
stack.push(new MapScreen(stack));
stack.push(new FightScreen(stack));
for (String command : new String[] { "1", "2", "1", "1" }) {
System.out.println("> " + command);
Screen top = stack.top();
top.handle(command);
top.update();
}
System.out.println(stack.names());
}
}
Ausgabe
> 1
Kampf bekommt 1 und öffnet Ziel
> 2
Ziel bekommt 2 und liefert 1
Kampf erhält 1: Goblin fällt
Ziel aktualisiert sich, Feinde in der Liste: 1
> 1
Kampf bekommt 1 und öffnet Ziel
> 1
Ziel bekommt 1 und liefert 0
Kampf erhält 0: Wolf fällt
Kampf: keine Feinde mehr, liefert Sieg
Karte erhält Sieg: öffnet Stufenaufstieg
Ziel aktualisiert sich, Feinde in der Liste: 0
[Karte, Stufenaufstieg]

Sieh dir den letzten Zug an. Das Ziel ist mit seinem handle noch nicht fertig, und mitten darin hat pop es schon entfernt, der Kampf hat den Wolf getroffen, der Kampf hat „Sieg“ zurückgegeben, und die Karte hat „Stufenaufstieg“ geöffnet. Erst dann ruft die Schleife update für das Ziel auf: Ein Bildschirm, der nicht mehr auf dem Stapel liegt, aktualisiert eine Liste von 0 Feinden für einen Kampf, der vorbei ist. Im echten Spiel würde so ein update einen Feind nach Nummer aus einer leeren Liste lesen, und das Spiel würde mit einem Fehler anhalten.

Würde die Schleife vor update noch einmal nach dem obersten fragen, würde sie „Stufenaufstieg“ aktualisieren: den falschen Bildschirm, der noch kein einziges Mal gezeichnet wurde. Es gibt zwei Wurzeln. Der Stapel ändert sich mitten im Zug. Und die Antworten laufen verschachtelt: Das onResult des Kampfes läuft im handle des Ziels, und das onResult der Karte darin.

Genauer, für Neugierige

Es wird schlimmer, wenn du alle Bildschirme auf dem Stapel durchgehst (sagen wir, jeder hat ein update) und sich einer von ihnen während des Durchgangs selbst entfernt. Dann verhalten sich die Sprachen unterschiedlich:

List<String> screens = new ArrayList<>(List.of("Karte", "Nachricht", "Kampf", "Ziel"));
try {
for (String screen : screens) {
System.out.println("update " + screen);
if (screen.equals("Nachricht")) screens.remove(screen);
}
} catch (ConcurrentModificationException e) {
System.out.println("ConcurrentModificationException");
}
Ausgabe
update Karte
update Nachricht
ConcurrentModificationException

Java bemerkt die Änderung und wirft beim nächsten Schritt eine ConcurrentModificationException. Vorsicht: Entfernst du das vorletzte Element, gibt es keinen Fehler. hasNext() vergleicht die Position mit der neuen Länge, sagt „nichts mehr“, und die Schleife hört still ohne das letzte auf:

List<String> screens = new ArrayList<>(List.of("Karte", "Nachricht", "Kampf", "Ziel"));
try {
for (String screen : screens) {
System.out.println("update " + screen);
if (screen.equals("Kampf")) screens.remove(screen);
}
} catch (ConcurrentModificationException e) {
System.out.println("ConcurrentModificationException");
}
Ausgabe
update Karte
update Nachricht
update Kampf

Ein Fehler, der das Programm anhält, ist wenigstens sichtbar. Ein stiller Fehler, bei dem ein Bildschirm in einem Zug nicht aktualisiert wird, ist viel schwerer zu fangen.

Die Idee

Aufgeschobene Änderungen: jetzt notieren, später ausführen

Die Lösung ist, den Stapel während des Zuges nicht anzufassen. push und pop bewegen nichts mehr. Sie notieren die Änderung nur in einer Liste, changes. Am Ende des Zuges geht apply die Liste durch und führt die Änderungen eine nach der anderen aus. Eine Änderung ist ein kleines Objekt: eine Art, ein Bildschirm (bei push) und eine Antwort (bei pop).

class Change {
String kind;
Screen screen;
Object result;
Change(String kind, Screen screen, Object result) {
this.kind = kind;
this.screen = screen;
this.result = result;
}
}
class ScreenStack {
List<Screen> screens = new ArrayList<>();
List<Change> changes = new ArrayList<>();
void push(Screen screen) {
changes.add(new Change("push", screen, null));
}
void pop(Object result) {
changes.add(new Change("pop", null, result));
}
Screen top() {
return screens.isEmpty() ? null : screens.get(screens.size() - 1);
}
void apply() {
int index = 0;
while (index < changes.size()) {
Change change = changes.get(index);
index++;
if (change.kind.equals("push")) {
screens.add(change.screen);
} else if (change.kind.equals("pop")) {
screens.remove(screens.size() - 1);
Screen below = top();
if (below != null) below.onResult(change.result);
}
}
changes.clear();
}
}

Die Schleife ruft apply zuletzt auf:

Screen top = stack.top();
top.handle(command);
top.update();
stack.apply();

Derselbe Kampf mit denselben Bildschirmen, nur der Stapel ist anders:

import java.util.ArrayList;
import java.util.List;
class Screen {
String name;
Screen(String name) {
this.name = name;
}
void draw() {}
void handle(String command) {}
void update() {}
// Die Antwort des Bildschirms, der über diesem lag.
void onResult(Object result) {}
}
class Change {
String kind;
Screen screen;
Object result;
Change(String kind, Screen screen, Object result) {
this.kind = kind;
this.screen = screen;
this.result = result;
}
}
class ScreenStack {
List<Screen> screens = new ArrayList<>();
List<Change> changes = new ArrayList<>();
void push(Screen screen) {
changes.add(new Change("push", screen, null));
}
void pop(Object result) {
changes.add(new Change("pop", null, result));
}
Screen top() {
return screens.isEmpty() ? null : screens.get(screens.size() - 1);
}
void apply() {
int index = 0;
while (index < changes.size()) {
Change change = changes.get(index);
index++;
if (change.kind.equals("push")) {
screens.add(change.screen);
} else if (change.kind.equals("pop")) {
screens.remove(screens.size() - 1);
Screen below = top();
if (below != null) below.onResult(change.result);
}
}
changes.clear();
}
String names() {
List<String> names = new ArrayList<>();
for (Screen screen : screens) names.add(screen.name);
return "[" + String.join(", ", names) + "]";
}
}
class TargetScreen extends Screen {
ScreenStack stack;
List<String> enemies;
TargetScreen(ScreenStack stack, List<String> enemies) {
super("Ziel");
this.stack = stack;
this.enemies = enemies;
}
@Override
void handle(String command) {
int index = Integer.parseInt(command) - 1;
System.out.println("Ziel bekommt " + command + " und liefert " + index);
stack.pop(index);
}
@Override
void update() {
System.out.println("Ziel aktualisiert sich, Feinde in der Liste: " + enemies.size());
}
}
class FightScreen extends Screen {
ScreenStack stack;
List<String> enemies = new ArrayList<>(List.of("Wolf", "Goblin"));
FightScreen(ScreenStack stack) {
super("Kampf");
this.stack = stack;
}
@Override
void handle(String command) {
System.out.println("Kampf bekommt " + command + " und öffnet Ziel");
stack.push(new TargetScreen(stack, enemies));
}
@Override
void onResult(Object result) {
String enemy = enemies.remove((int) result);
System.out.println("Kampf erhält " + result + ": " + enemy + " fällt");
if (enemies.isEmpty()) {
System.out.println("Kampf: keine Feinde mehr, liefert Sieg");
stack.pop("Sieg");
}
}
}
class MapScreen extends Screen {
ScreenStack stack;
MapScreen(ScreenStack stack) {
super("Karte");
this.stack = stack;
}
@Override
void onResult(Object result) {
System.out.println("Karte erhält " + result + ": öffnet Stufenaufstieg");
stack.push(new Screen("Stufenaufstieg"));
}
}
public class Main {
public static void main(String[] args) {
ScreenStack stack = new ScreenStack();
stack.push(new MapScreen(stack));
stack.push(new FightScreen(stack));
stack.apply();
for (String command : new String[] { "1", "2", "1", "1" }) {
System.out.println("> " + command);
Screen top = stack.top();
top.handle(command);
top.update();
stack.apply();
}
System.out.println(stack.names());
}
}
Ausgabe
> 1
Kampf bekommt 1 und öffnet Ziel
> 2
Ziel bekommt 2 und liefert 1
Ziel aktualisiert sich, Feinde in der Liste: 2
Kampf erhält 1: Goblin fällt
> 1
Kampf bekommt 1 und öffnet Ziel
> 1
Ziel bekommt 1 und liefert 0
Ziel aktualisiert sich, Feinde in der Liste: 1
Kampf erhält 0: Wolf fällt
Kampf: keine Feinde mehr, liefert Sieg
Karte erhält Sieg: öffnet Stufenaufstieg
[Karte, Stufenaufstieg]

Jetzt beendet das Ziel handle und update, solange es noch oben liegt und alles um es herum so ist wie vor dem Zug. Erst dann entfernt apply es, und der Kampf bekommt seine Antwort. Jeder Bildschirm arbeitet in einem Stapel, der sich nicht unter seinen Füßen verändert.

Drei Einzelheiten in apply sind wichtig:

  • while mit einem Index, nicht foreach. Während apply ein pop ausführt, bekommt der Kampf seine Antwort und kann eine neue Änderung schreiben: pop(Sieg). Sie landet am Ende derselben Liste, und while liest ihre Länge neu und erreicht sie auch. foreach würde den Fehler von der vorigen Karte werfen.
  • Die Liste wird am Ende geleert. Sonst würden im nächsten Zug dieselben Änderungen noch einmal ausgeführt.
  • Die Reihenfolge ist die Reihenfolge des Schreibens. Schreibt ein Bildschirm in einem Zug ein replace und dann ein push, tut der Stapel genau das.

Sieh es dir an

Ein Zug, Änderung für Änderung

class ScreenStack {
List<Screen> screens = new ArrayList<>();
List<Change> changes = new ArrayList<>();
void push(Screen screen) {
changes.add(new Change("push", screen, null));
}
void pop(Object result) {
changes.add(new Change("pop", null, result));
}
Screen top() {
return screens.isEmpty() ? null : screens.get(screens.size() - 1);
}
void apply() {
int index = 0;
while (index < changes.size()) {
Change change = changes.get(index);
index++;
if (change.kind.equals("push")) {
screens.add(change.screen);
} else if (change.kind.equals("pop")) {
screens.remove(screens.size() - 1);
Screen below = top();
if (below != null) below.onResult(change.result);
}
}
changes.clear();
}
}
// Ein Zug.
Screen top = stack.top();
top.handle(command);
top.update();
stack.apply();
Schritt für Schritt, in Worten
  1. Der letzte Zug des Kampfes. Auf dem Stapel liegen Karte, Kampf und oben Ziel. Nur der Wolf ist übrig. Die Liste der Änderungen ist leer.
  2. Der Befehl 1 geht an das oberste, Ziel. Es ruft pop(0) auf: 0 ist die Nummer des gewählten Feindes. Der Stapel rührt sich nicht. Die Änderung wird nur in changes notiert.
  3. update läuft für Ziel, das noch auf dem Stapel liegt. Alles, was es sieht, ist wie vor dem Zug: Der Kampf ist noch nicht vorbei.
  4. Ende des Zuges: apply nimmt Änderung 0. Ziel verlässt den Stapel, und 0 geht über onResult hinunter an Kampf. Der Wolf fällt, es sind keine Feinde mehr da, und Kampf ruft pop(Sieg) auf: eine neue Änderung am Ende der Liste.
  5. while liest die Länge von changes neu, also erreicht es auch die neue Änderung. Kampf verlässt den Stapel, und Karte bekommt „Sieg“. Karte schreibt push(Stufenaufstieg).
  6. Die dritte Änderung ist ein push: Stufenaufstieg kommt auf den Stapel, über Karte.
  7. Die Liste ist abgearbeitet. changes wird geleert, damit dieselben Änderungen im nächsten Zug nicht noch einmal ausgeführt werden. Der Stapel ist Karte, Stufenaufstieg. Jeder Bildschirm hat seine Antwort nach dem Zug bekommen, nie mitten in einem.

Probier es aus

Was fehlt?

Das ist die Schleife von apply. Wie muss die Bedingung lauten, damit sie auch die Änderungen erreicht, die geschrieben werden, während sie läuft?

void apply() {
    int index = 0;
    while (index < ) {
        Change change = changes.get(index);
        index++;
        if (change.kind.equals("push")) {
            screens.add(change.screen);
        } else if (change.kind.equals("pop")) {
            screens.remove(screens.size() - 1);
            Screen below = top();
            if (below != null) below.onResult(change.result);
        }
    }
    changes.clear();
}

Probier es aus

Sage die Ausgabe voraus

NamedScreen ist ein Bildschirm, der nur sagt, was er bekommen hat. Der Stapel ist der mit den aufgeschobenen Änderungen. Was gibt das Programm aus?

ScreenStack stack = new ScreenStack();
stack.push(new NamedScreen("Karte"));
stack.push(new NamedScreen("Kampf"));
stack.apply();
stack.pop("Sieg");
stack.push(new NamedScreen("Stufenaufstieg"));
System.out.println("Oben: " + stack.top().name);
stack.apply();
System.out.println("Oben: " + stack.top().name);
Antwort

Die Idee

Übergänge: replace und reset

Nicht jeder Wechsel des Bildschirms ist eine Frage mit Antwort. Manche sind Übergänge ohne Weg zurück. Für sie hat der Stapel noch zwei Änderungen:

void replace(Screen screen) {
changes.add(new Change("replace", screen, null));
}
void reset(Screen screen) {
changes.add(new Change("reset", screen, null));
}

apply hat für jede einen Zweig:

} else if (change.kind.equals("replace")) {
screens.remove(screens.size() - 1);
screens.add(change.screen);
} else if (change.kind.equals("reset")) {
screens.clear();
if (change.screen != null) screens.add(change.screen);
}
  • replace entfernt den obersten Bildschirm und setzt einen anderen an seine Stelle. Der Titelbildschirm öffnet die Heldenerschaffung, und die Heldenerschaffung öffnet das Dorf. Mit push bliebe der Titelbildschirm ganz unten, und bei „zurück“ würde der Spieler mitten im Spiel dort landen.
  • reset leert den ganzen Stapel und setzt einen Bildschirm (oder keinen: dann endet das Spiel). Es hat zwei Einsatzzwecke: Der Held ist gefallen (der Spielende-Bildschirm) und ein Spielstand wurde geladen (das Dorf oder die Karte).

Warum nicht pop für das Ende des Spiels benutzen? Der Stapel ist Dorf, Karte, Kampf. Hier sind beide Wege:

ScreenStack stack = new ScreenStack();
stack.push(new NamedScreen("Titel"));
stack.apply();
System.out.println(stack.names());
stack.replace(new NamedScreen("Neuer Held"));
stack.apply();
System.out.println(stack.names());
stack.replace(new NamedScreen("Dorf"));
stack.apply();
System.out.println(stack.names());
System.out.println("Der Held fällt. Mit pop:");
ScreenStack popped = inFight();
popped.pop("Niederlage");
popped.pop("Niederlage");
popped.push(new NamedScreen("Spielende"));
popped.apply();
System.out.println(popped.names());
System.out.println("Der Held fällt. Mit reset:");
ScreenStack clean = inFight();
clean.reset(new NamedScreen("Spielende"));
clean.apply();
System.out.println(clean.names());
Ausgabe
[Titel]
[Neuer Held]
[Dorf]
Der Held fällt. Mit pop:
Karte erhält Niederlage
Dorf erhält Niederlage
[Dorf, Spielende]
Der Held fällt. Mit reset:
[Spielende]

Bei pop bekommen die Karte und das Dorf „Niederlage“ und reagieren, als wäre der Kampf auf normale Weise zu Ende gegangen. Du musst auch wissen, wie viele Bildschirme darunter liegen. Und das Dorf bleibt unter dem Spielende-Bildschirm. reset ruft onResult bei niemandem auf: Das ist keine Antwort, es ist ein Neustart.

Genauer, für Neugierige

replace und reset sind auch aufgeschoben: Sie werden notiert und in apply in der Reihenfolge ausgeführt, in der sie geschrieben wurden. Die Heldenerschaffung im Spiel ruft replace(das Dorf) auf und gleich danach push(die Nachricht mit dem Anfang der Geschichte). Nach apply ist der Stapel das Dorf mit der Nachricht obenauf, nicht umgekehrt.

Das ganze Programm:

import java.util.ArrayList;
import java.util.List;
class Screen {
String name;
Screen(String name) {
this.name = name;
}
void draw() {}
void handle(String command) {}
void update() {}
// Die Antwort des Bildschirms, der über diesem lag.
void onResult(Object result) {}
}
class Change {
String kind;
Screen screen;
Object result;
Change(String kind, Screen screen, Object result) {
this.kind = kind;
this.screen = screen;
this.result = result;
}
}
class ScreenStack {
List<Screen> screens = new ArrayList<>();
List<Change> changes = new ArrayList<>();
void push(Screen screen) {
changes.add(new Change("push", screen, null));
}
void pop(Object result) {
changes.add(new Change("pop", null, result));
}
void replace(Screen screen) {
changes.add(new Change("replace", screen, null));
}
void reset(Screen screen) {
changes.add(new Change("reset", screen, null));
}
Screen top() {
return screens.isEmpty() ? null : screens.get(screens.size() - 1);
}
void apply() {
int index = 0;
while (index < changes.size()) {
Change change = changes.get(index);
index++;
if (change.kind.equals("push")) {
screens.add(change.screen);
} else if (change.kind.equals("pop")) {
screens.remove(screens.size() - 1);
Screen below = top();
if (below != null) below.onResult(change.result);
} else if (change.kind.equals("replace")) {
screens.remove(screens.size() - 1);
screens.add(change.screen);
} else if (change.kind.equals("reset")) {
screens.clear();
if (change.screen != null) screens.add(change.screen);
}
}
changes.clear();
}
String names() {
List<String> names = new ArrayList<>();
for (Screen screen : screens) names.add(screen.name);
return "[" + String.join(", ", names) + "]";
}
}
class NamedScreen extends Screen {
NamedScreen(String name) {
super(name);
}
@Override
void onResult(Object result) {
System.out.println(name + " erhält " + result);
}
}
public class Main {
static ScreenStack inFight() {
ScreenStack stack = new ScreenStack();
stack.push(new NamedScreen("Dorf"));
stack.push(new NamedScreen("Karte"));
stack.push(new NamedScreen("Kampf"));
stack.apply();
return stack;
}
public static void main(String[] args) {
ScreenStack stack = new ScreenStack();
stack.push(new NamedScreen("Titel"));
stack.apply();
System.out.println(stack.names());
stack.replace(new NamedScreen("Neuer Held"));
stack.apply();
System.out.println(stack.names());
stack.replace(new NamedScreen("Dorf"));
stack.apply();
System.out.println(stack.names());
System.out.println("Der Held fällt. Mit pop:");
ScreenStack popped = inFight();
popped.pop("Niederlage");
popped.pop("Niederlage");
popped.push(new NamedScreen("Spielende"));
popped.apply();
System.out.println(popped.names());
System.out.println("Der Held fällt. Mit reset:");
ScreenStack clean = inFight();
clean.reset(new NamedScreen("Spielende"));
clean.apply();
System.out.println(clean.names());
}
}

Die Idee

Im echten Spiel

Der Stapel der Chroniken von Arkanum ist derselbe, mit drei Dingen mehr:

// Der Stapel ändert sich während der Verarbeitung nicht: push, pop und replace merken die Änderung nur, und
// apply() wendet sie nach dem Zug an. Sonst würde ein Bildschirm, der vom Stapel genommen wird, weiterlaufen.
static class StackChange {
String kind;
Screen screen;
Object result;
StackChange(String kind, Screen screen, Object result) {
this.kind = kind;
this.screen = screen;
this.result = result;
}
}
static class ScreenStack {
List<Screen> screens = new ArrayList<>();
List<StackChange> changes = new ArrayList<>();
void push(Screen screen) {
this.changes.add(new StackChange("push", screen, null));
}
void pop(Object result) {
this.changes.add(new StackChange("pop", null, result));
}
void replace(Screen screen) {
this.changes.add(new StackChange("replace", screen, null));
}
// Leert den ganzen Stapel (Laden, Game over); mit null bleibt der Stapel leer.
void reset(Screen screen) {
this.changes.add(new StackChange("reset", screen, null));
}
Screen top() {
return this.screens.size() == 0 ? null : this.screens.get(this.screens.size() - 1);
}
boolean isEmpty() {
return this.screens.size() == 0 && this.changes.size() == 0;
}
boolean contains(String kindName) {
for (Screen screen : this.screens) {
if (screen.kindName.equals(kindName)) return true;
}
return false;
}
// pop auf einem leeren Stapel tut in JavaScript nichts; in Java wirft remove, darum gibt es eine Prüfung.
void removeTop() {
if (this.screens.size() > 0) this.screens.remove(this.screens.size() - 1);
}
void apply() {
int index = 0;
while (index < this.changes.size()) {
StackChange change = this.changes.get(index);
index++;
if (change.kind.equals("push")) {
this.screens.add(change.screen);
} else if (change.kind.equals("pop")) {
this.removeTop();
Screen below = this.top();
if (below != null) below.onResult(change.result);
} else if (change.kind.equals("replace")) {
this.removeTop();
this.screens.add(change.screen);
} else {
this.screens = new ArrayList<>();
if (change.screen != null) this.screens.add(change.screen);
}
}
this.changes = new ArrayList<>();
}
// Das Zeichnen beginnt beim obersten undurchsichtigen Bildschirm und geht nach oben.
void draw(CellBuffer buffer) {
int first = this.screens.size() - 1;
while (first > 0 && this.screens.get(first).isTransparent()) first--;
for (int i = first; i < this.screens.size(); i++) {
if (i >= 0) this.screens.get(i).draw(buffer);
}
}
}
  • isEmpty schaut auch auf die Änderungen. Das Spiel beginnt mit einem push des Titelbildschirms, und das Beenden ist ein reset ohne Bildschirm. Die Schleife hält an, wenn weder Bildschirme noch wartende Änderungen da sind.
  • contains fragt, ob der Stapel schon einen Bildschirm einer bestimmten Art hat. So kommt der Stufenaufstieg-Bildschirm nicht doppelt hinein, wenn in einem Zug zwei Stufen gewonnen werden.
  • draw zeichnet von unten nach oben ab dem obersten undurchsichtigen Bildschirm, wie in der letzten Lektion.

Die Zielwahl im Spiel ist ein durchsichtiger Bildschirm über dem Kampf. Er trifft selbst niemanden: Er gibt die Nummer mit pop zurück, und der Kampf entscheidet, was zu tun ist. Daneben stehen die allgemeine Wahl aus einer Liste (eine Fähigkeit, ein Trank) und die Wahl eines Speicherplatzes, die den Namen des Platzes zurückgibt, keine Zahl:

// Ein Ziel zu wählen ist ein durchsichtiger Bildschirm über dem Kampf. Er greift nicht an: pop(result) liefert die Zielnummer,
// und der Kampf entscheidet, was damit geschieht.
static class TargetScreen extends Screen {
Battle battle;
String note;
TargetScreen(Game game, Battle battle) {
super(game);
this.battle = battle;
this.note = "";
}
@Override
boolean isTransparent() {
return true;
}
@Override
void draw(CellBuffer buffer) {
List<Enemy> enemies = this.battle.aliveEnemies();
buffer.box(24, 9, 36, enemies.size() + 4, t("target.title"));
for (int i = 0; i < enemies.size(); i++) {
buffer.text(26, 10 + i, (i + 1) + ") " + pad(enemies.get(i).name, 22) + " " + enemies.get(i).hp + "/" + enemies.get(i).maxHp(), enemies.get(i).kind.color);
}
buffer.text(26, 11 + enemies.size(), this.note.length() > 0 ? this.note : t("target.help"), "gray");
}
@Override
void handle(String command) {
if (command.equals("x")) {
this.game.stack.pop(null);
return;
}
Integer choice = parseIntStrict(command);
if (choice == null || choice < 1 || choice > this.battle.aliveEnemies().size()) {
this.note = t("ui.unknown", "command", command);
return;
}
this.game.stack.pop(choice - 1);
}
}
// Eine allgemeine Auswahl aus einer Liste (Fähigkeit, Trank, Slot): liefert eine 0-basierte Nummer oder null.
static class ChoiceScreen extends Screen {
String title;
List<String> labels;
String note;
ChoiceScreen(Game game, String title, List<String> labels) {
super(game);
this.title = title;
this.labels = labels;
this.note = "";
}
@Override
boolean isTransparent() {
return true;
}
@Override
void draw(CellBuffer buffer) {
buffer.box(4, 3, 52, this.labels.size() + 4, this.title);
for (int i = 0; i < this.labels.size(); i++) buffer.text(6, 4 + i, (i + 1) + ") " + this.labels.get(i), "default");
buffer.text(6, 5 + this.labels.size(), this.note.length() > 0 ? this.note : t("choice.help"), "gray");
}
@Override
void handle(String command) {
if (command.equals("x")) {
this.game.stack.pop(null);
return;
}
Integer choice = parseIntStrict(command);
if (choice == null || choice < 1 || choice > this.labels.size()) {
this.note = t("ui.unknown", "command", command);
return;
}
this.game.stack.pop(choice - 1);
}
}
static class SlotScreen extends ChoiceScreen {
SlotScreen(Game game, String mode) {
super(game, t(mode.equals("save") ? "slot.saveTitle" : "slot.loadTitle"), slotLabels(game));
}
// In Java ruft der Konstruktor zuerst super auf, darum werden die Beschriftungen in einer eigenen Funktion gebaut.
static List<String> slotLabels(Game game) {
List<String> labels = new ArrayList<>();
for (String slot : SAVE_SLOTS) labels.add(t("slot.label", "slot", slot, "summary", slotSummary(game.io, slot)));
return labels;
}
@Override
void handle(String command) {
Integer choice = parseIntStrict(command);
if (choice != null && choice >= 1 && choice <= SAVE_SLOTS.size()) {
this.game.stack.pop(SAVE_SLOTS.get(choice - 1));
return;
}
super.handle(command);
}
}

Der Kampf merkt sich, was er gefragt hat (pending), und liest die Antwort in onResult:

void onResult(Object result) {
Battle battle = this.battle;
Hero hero = battle.hero;
Pending pending = this.pending;
this.pending = null;
if (result == null || pending == null) return;
int index = (Integer) result;
if (pending.kind.equals("attack")) {
battle.heroAct(new AttackAction(hero, battle.aliveEnemies().get(index)));
} else if (pending.kind.equals("skill")) {
Skill skill = SKILLS.get(hero.skills.get(index));
if (hero.mana < skill.mana) {
this.note = t("battle.noMana", "skill", nm(skill));
} else if (hero.cooldownOf(skill.id) > 0) {
this.note = t("battle.cooldown", "skill", nm(skill), "turns", hero.cooldownOf(skill.id));
} else if (skill.target.equals("one") && battle.aliveEnemies().size() > 1) {
this.pending = new Pending("skillTarget", skill);
this.game.stack.push(new TargetScreen(this.game, battle));
} else {
battle.heroAct(new SkillAction(hero, skill, battle.aliveEnemies().get(0)));
}
} else if (pending.kind.equals("skillTarget")) {
battle.heroAct(new SkillAction(hero, pending.skill, battle.aliveEnemies().get(index)));
} else {
battle.heroAct(new ItemAction(hero, this.potionIds().get(index)));
}
}

Sieh dir den Zweig der Fähigkeit an. Der Spieler hat eine Fähigkeit gewählt, die ein Ziel braucht, und es gibt mehr als einen Feind. Dann schreibt der Kampf in onResult eine neue Frage: ein push des Ziel-Bildschirms. Das geschieht während apply, und while erreicht es und führt es im selben Zug aus. Der Spieler sieht die Liste der Feinde gleich nach der Liste der Fähigkeiten.

Das Menü ist der andere Bildschirm, der fragt. „Bist du sicher?“ ist derselbe allgemeine Bildschirm, und die Antwort „ja“ auf die Frage zum Beenden ist ein reset ohne Bildschirm: Der Stapel wird leer, und das Spiel endet.

static class ConfirmScreen extends Screen {
String question;
ConfirmScreen(Game game, String question) {
super(game);
this.question = question;
}
@Override
boolean isTransparent() {
return true;
}
@Override
void draw(CellBuffer buffer) {
buffer.box(16, 6, 28, 5, "");
buffer.text(18, 7, this.question, "white");
buffer.text(18, 8, "1) " + t("ui.yes") + " 2) " + t("ui.no"), "default");
}
@Override
void handle(String command) {
if (command.equals("1")) this.game.stack.pop(true);
else if (command.equals("2") || command.equals("x")) this.game.stack.pop(false);
}
}
void onResult(Object result) {
Game game = this.game;
String pending = this.pending;
this.pending = "";
if (result == null) return;
if (pending.equals("save")) {
game.io.save((String) result, saveGame(game));
this.note = t("menu.saved", "slot", result);
} else if (pending.equals("load")) {
String error = loadGame(game, game.io.load((String) result));
if (error != null) this.note = error;
else enterWorld(game, t("load.done", "slot", result));
} else if (pending.equals("quit") && Boolean.TRUE.equals(result)) {
game.stack.reset(null);
}
}

Probier es aus

Finde den Fehler

Ein Kollege hat in apply zwei Zeilen vertauscht. Das Kampfprogramm aus „Aufgeschobene Änderungen“ gibt jetzt aus:

class ScreenStack {
List<Screen> screens = new ArrayList<>();
List<Change> changes = new ArrayList<>();
void push(Screen screen) {
changes.add(new Change("push", screen, null));
}
void pop(Object result) {
changes.add(new Change("pop", null, result));
}
Screen top() {
return screens.isEmpty() ? null : screens.get(screens.size() - 1);
}
void apply() {
int index = 0;
while (index < changes.size()) {
Change change = changes.get(index);
index++;
if (change.kind.equals("push")) {
screens.add(change.screen);
} else if (change.kind.equals("pop")) {
Screen below = top();
screens.remove(screens.size() - 1);
if (below != null) below.onResult(change.result);
}
}
changes.clear();
}
}
Ausgabe
> 1
Kampf bekommt 1 und öffnet Ziel
> 2
Ziel bekommt 2 und liefert 1
Ziel aktualisiert sich, Feinde in der Liste: 2
> 1
Kampf bekommt 1 und öffnet Ziel
> 1
Ziel bekommt 1 und liefert 0
Ziel aktualisiert sich, Feinde in der Liste: 2
[Karte, Kampf]

Das Ziel gibt eine Zahl zurück, aber der Kampf erfährt es nie: Beide Feinde leben, und am Ende ist der Stapel die Karte und der Kampf. Welche Zeile läuft zu früh?

Wähle die Zeile mit dem Fehler.

Genauer

Genauer, für Neugierige

Warum kein Callback? Der Kampf könnte dem Ziel eine Funktion geben: „Wenn der Spieler wählt, ruf das hier auf.“

// Der Kampf sagt dem Ziel, was es mit der Wahl tun soll.
stack.push(new TargetScreen(enemies, index -> attack(index)));

Das funktioniert, und es ist üblich. Der Preis steckt in den Einzelheiten. Die Funktion wird vom Ziel aufgerufen, in seinem handle: Wir sind wieder mitten im Zug, es sei denn, auch der Aufruf wird aufgeschoben. Die Funktion hält den Kampf fest, und das Spiel muss gespeichert werden: Ein Stapel von Bildschirmen mit Daten lässt sich speichern, aber eine Funktion lässt sich nicht in eine Datei schreiben. Und die Reihenfolge wird unsichtbar: Wer aufgerufen wird, ist erst klar, wenn man sieht, wer was übergeben hat. onResult sagt dasselbe mit einer Regel, die man im Stapel sieht: Die Antwort geht immer an den Bildschirm darunter.

Dasselbe anderswo. Auf Android öffnet eine Activity eine andere und wartet auf ein Ergebnis (startActivityForResult, heute die Activity Result API). Modale Dialoge in Desktop-Apps geben beim Schließen einen Wert zurück („OK“ oder „Abbrechen“). Die Navigation in mobilen Apps ist auch ein Stapel: Du öffnest einen Bildschirm obenauf, und „zurück“ bringt dich zurück, manchmal mit einem Ergebnis.

Mit await. In JavaScript, C# und Python kannst du const target = await targetScreen.open() schreiben: Der Kampf „wartet“ auf die Antwort wie bei einem gewöhnlichen Aufruf. Das sieht natürlicher aus, ist aber in einem rundenbasierten Spiel mit einer Schleife schwieriger. Der Kampf ist dann kein Objekt mehr, das Befehle empfängt, sondern eine Funktion, die mittendrin angehalten wurde. Ihr Zustand steckt in „wo sie stehen geblieben ist“, und das kommt nicht in eine Speicherdatei. Und es muss immer noch klar sein, an welchem Punkt des Zuges sie weitermacht.

Der Typ der Antwort. Hier ist die Antwort Object, und der Kampf weiß, dass es eine Zahl ist. Es geht auch strenger: Das Ziel gibt einen eigenen Typ zurück (sagen wir, einen Datensatz mit der Nummer), oder der Bildschirm ist generisch über den Typ der Antwort. Im Spiel sind die Antworten wenige und einfach (eine Zahl, „ja“ oder „nein“, ein Platzname, das Ergebnis eines Kampfes), deshalb reicht die Prüfung von pending.

Übung

Ein Laden mit zwei Fragen

Löse es in deinem Editor. Beginne mit dem Stapel mit aufgeschobenen Änderungen aus dieser Lektion und mit ConfirmScreen.

Laden. ShopScreen hat 20 Gold und verkauft einen Trank für 5 und ein Schwert für 12. Die Wahl eines Gegenstands öffnet QuantityScreen, der die Anzahl (von 1 bis 9) zurückgibt oder nichts, wenn der Spieler x drückt. Dann öffnet der Laden ConfirmScreen mit der Frage „Kaufst du 2 x Trank für 10 Gold?“. Ein Bildschirm sammelt zwei Antworten, eine nach der anderen: onResult weiß an pending, welche Antwort eingetroffen ist, das von "count" zu "confirm" wechselt. Reicht das Gold nicht, wird nichts gekauft. x im Laden schließt ihn.

Beachte: ConfirmScreen wird aus onResult geöffnet, also während apply noch läuft. Deshalb liest die Schleife in apply die Länge der Liste neu.

Eingabe
1
2
1
2
1
2
2
x
2
1
1
1
1
1
x
Ausgabe
Laden: Gold 20 | 1) Trank 5 2) Schwert 12 x) verlassen
> 1
Wie viele? 1-9 | x) zurück
> 2
Kaufst du 2 x Trank für 10 Gold? 1) ja 2) nein
> 1
Du kaufst 2 x Trank.
Laden: Gold 10 | 1) Trank 5 2) Schwert 12 x) verlassen
> 2
Wie viele? 1-9 | x) zurück
> 1
Kaufst du 1 x Schwert für 12 Gold? 1) ja 2) nein
> 2
Du kaufst nicht.
Laden: Gold 10 | 1) Trank 5 2) Schwert 12 x) verlassen
> 2
Wie viele? 1-9 | x) zurück
> x
Du überlegst es dir anders.
Laden: Gold 10 | 1) Trank 5 2) Schwert 12 x) verlassen
> 2
Wie viele? 1-9 | x) zurück
> 1
Kaufst du 1 x Schwert für 12 Gold? 1) ja 2) nein
> 1
Du hast nicht genug Gold.
Laden: Gold 10 | 1) Trank 5 2) Schwert 12 x) verlassen
> 1
Wie viele? 1-9 | x) zurück
> 1
Kaufst du 1 x Trank für 5 Gold? 1) ja 2) nein
> 1
Du kaufst 1 x Trank.
Laden: Gold 5 | 1) Trank 5 2) Schwert 12 x) verlassen
> x
Auf Wiedersehen.

Weitere Aufgaben

  • Verkaufen. Füge s) verkaufen hinzu: Der Laden fragt, welchen Gegenstand (ein weiterer Bildschirm mit einer Antwort) und wie viele, und zahlt den halben Preis zurück, abgerundet. Wie viele Werte hat pending jetzt?
  • Tasche. Halte die gekauften Gegenstände in einer Liste und gib sie beim Verlassen aus. Ein zweimal gekaufter Trank ist eine Zeile mit 3 Stück, nicht zwei Zeilen.
  • Kein pending. Lass QuantityScreen und ConfirmScreen einen Datensatz mit dem Namen der Frage und dem Wert zurückgeben. Kommt der Laden dann ohne pending aus? Was gewinnst du und was verlierst du?

Test

Teste dich

1. Warum ruft der Ziel-Bildschirm den Kampf nicht selbst auf, sondern gibt mit pop eine Zahl zurück?
2. Die Karte hat „Bist du sicher?“ geöffnet und true bekommen. Woher weiß sie, ob sie das Spiel beenden oder einen Trank wegwerfen soll?
3. Warum geht apply die Änderungen mit while (index < length) durch und nicht mit foreach?
4. Ein Kollege hat die letzte Zeile von apply gelöscht, die changes leert. Was passiert?
5. Der Held fällt im Kampf. Der Stapel ist Dorf, Karte, Kampf. Warum ruft das Spiel reset(Spielende) auf und nicht zweimal pop und ein push?

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