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):

enemies = ["Wolf", "Goblin"]
selected_target = -1
def target_screen(command):
global selected_target
if command != "x":
selected_target = int(command) - 1
def fight_turn():
if selected_target >= 0:
print("Du triffst " + enemies[selected_target] + ".")
else:
print("Niemand wird getroffen.")
# Der Spieler wählt 2 und überlegt es sich dann anders (x).
target_screen("2")
fight_turn()
target_screen("x")
fight_turn()
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(Screen):
def handle(self, command):
# Das Ziel kennt den Kampf und ruft ihn selbst auf.
self.fight.attack(int(command) - 1)
self.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 on_result aufruft.

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

class Screen:
def __init__(self, name):
self.name = name
def draw(self):
pass
def handle(self, command):
pass
def update(self):
pass
# Die Antwort des Bildschirms, der über diesem lag.
def on_result(self, result):
pass

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

def pop(self, result):
self.screens.pop()
below = self.top()
if below is not None:
below.on_result(result)

So kennen sich die beiden Seiten nicht. Das Ziel kennt die Frage und die Form der Antwort: die Nummer eines Feindes oder None 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 None zurück. Die Nummer wird ab 0 gezählt, weil sie ein Index in die Liste ist:

class TargetScreen(Screen):
def __init__(self, stack, enemies):
super().__init__("Ziel")
self.stack = stack
self.enemies = enemies
def draw(self):
line = "Wen triffst du?"
for i in range(len(self.enemies)):
line += f" {i + 1}) {self.enemies[i].name}"
print(line + " x) zurück")
def handle(self, command):
if command == "x":
self.stack.pop(None)
return
choice = int(command)
if 1 <= choice <= len(self.enemies):
self.stack.pop(choice - 1)

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

class FightScreen(Screen):
def __init__(self, stack):
super().__init__("Kampf")
self.stack = stack
self.enemies = [Enemy("Wolf", 4), Enemy("Goblin", 7)]
def draw(self):
line = "Kampf:"
for enemy in self.enemies:
line += f" {enemy.name} {enemy.hp}"
print(line + " | 1) Angriff")
def handle(self, command):
if command == "1":
self.stack.push(TargetScreen(self.stack, self.enemies))
def on_result(self, result):
if result is None:
print("Du überlegst es dir anders. Niemand wird getroffen.")
return
enemy = self.enemies[result]
enemy.hp -= 4
if enemy.hp > 0:
print("Du triffst " + enemy.name + " (4).")
return
print(enemy.name + " fällt.")
self.enemies.remove(enemy)
if not self.enemies:
self.stack.pop("Sieg")

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

stack = ScreenStack()
stack.push(MapScreen(stack))
while stack.top() is not None:
stack.top().draw()
command = input().strip()
print("> " + command)
stack.top().handle(command)
print("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:

class Screen:
def __init__(self, name):
self.name = name
def draw(self):
pass
def handle(self, command):
pass
def update(self):
pass
# Die Antwort des Bildschirms, der über diesem lag.
def on_result(self, result):
pass
class ScreenStack:
def __init__(self):
self.screens = []
def push(self, screen):
self.screens.append(screen)
def pop(self, result):
self.screens.pop()
below = self.top()
if below is not None:
below.on_result(result)
def top(self):
return self.screens[-1] if self.screens else None
def names(self):
return "[" + ", ".join(s.name for s in self.screens) + "]"
class Enemy:
def __init__(self, name, hp):
self.name = name
self.hp = hp
class TargetScreen(Screen):
def __init__(self, stack, enemies):
super().__init__("Ziel")
self.stack = stack
self.enemies = enemies
def draw(self):
line = "Wen triffst du?"
for i in range(len(self.enemies)):
line += f" {i + 1}) {self.enemies[i].name}"
print(line + " x) zurück")
def handle(self, command):
if command == "x":
self.stack.pop(None)
return
choice = int(command)
if 1 <= choice <= len(self.enemies):
self.stack.pop(choice - 1)
class FightScreen(Screen):
def __init__(self, stack):
super().__init__("Kampf")
self.stack = stack
self.enemies = [Enemy("Wolf", 4), Enemy("Goblin", 7)]
def draw(self):
line = "Kampf:"
for enemy in self.enemies:
line += f" {enemy.name} {enemy.hp}"
print(line + " | 1) Angriff")
def handle(self, command):
if command == "1":
self.stack.push(TargetScreen(self.stack, self.enemies))
def on_result(self, result):
if result is None:
print("Du überlegst es dir anders. Niemand wird getroffen.")
return
enemy = self.enemies[result]
enemy.hp -= 4
if enemy.hp > 0:
print("Du triffst " + enemy.name + " (4).")
return
print(enemy.name + " fällt.")
self.enemies.remove(enemy)
if not self.enemies:
self.stack.pop("Sieg")
class ConfirmScreen(Screen):
def __init__(self, stack, question):
super().__init__(question)
self.stack = stack
self.question = question
def draw(self):
print(self.question + " 1) ja 2) nein")
def handle(self, command):
if command == "1":
self.stack.pop(True)
elif command == "2":
self.stack.pop(False)
class MapScreen(Screen):
def __init__(self, stack):
super().__init__("Karte")
self.stack = stack
self.potions = 2
self.pending = ""
def draw(self):
print("Karte: Tränke " + str(self.potions) + " | f) Kampf d) Trank wegwerfen q) beenden")
def handle(self, command):
if command == "f":
self.stack.push(FightScreen(self.stack))
elif command == "d":
self.pending = "drop"
self.stack.push(ConfirmScreen(self.stack, "Bist du sicher, dass du einen Trank wegwirfst?"))
elif command == "q":
self.pending = "quit"
self.stack.push(ConfirmScreen(self.stack, "Bist du sicher, dass du das Spiel verlässt?"))
def on_result(self, result):
asked = self.pending
self.pending = ""
if asked == "drop":
if result is True:
self.potions -= 1
print("Du wirfst einen Trank weg.")
else:
print("Der Trank bleibt.")
elif asked == "quit":
if result is True:
self.stack.pop(None)
elif result == "Sieg":
print("Sieg! Zurück zur Karte.")
stack = ScreenStack()
stack.push(MapScreen(stack))
while stack.top() is not None:
stack.top().draw()
command = input().strip()
print("> " + command)
stack.top().handle(command)
print("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(Screen):
def __init__(self, stack, question):
super().__init__(question)
self.stack = stack
self.question = question
def draw(self):
print(self.question + " 1) ja 2) nein")
def handle(self, command):
if command == "1":
self.stack.pop(True)
elif command == "2":
self.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(Screen):
def __init__(self, stack):
super().__init__("Karte")
self.stack = stack
self.potions = 2
self.pending = ""
def draw(self):
print("Karte: Tränke " + str(self.potions) + " | f) Kampf d) Trank wegwerfen q) beenden")
def handle(self, command):
if command == "f":
self.stack.push(FightScreen(self.stack))
elif command == "d":
self.pending = "drop"
self.stack.push(ConfirmScreen(self.stack, "Bist du sicher, dass du einen Trank wegwirfst?"))
elif command == "q":
self.pending = "quit"
self.stack.push(ConfirmScreen(self.stack, "Bist du sicher, dass du das Spiel verlässt?"))
def on_result(self, result):
asked = self.pending
self.pending = ""
if asked == "drop":
if result is True:
self.potions -= 1
print("Du wirfst einen Trank weg.")
else:
print("Der Trank bleibt.")
elif asked == "quit":
if result is True:
self.stack.pop(None)
elif result == "Sieg":
print("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(Screen):
def __init__(self, stack, enemies):
super().__init__("Ziel")
self.stack = stack
self.enemies = enemies
def handle(self, command):
index = int(command) - 1
print("Ziel bekommt " + command + " und liefert " + str(index))
self.stack.pop(index)
def update(self):
print("Ziel aktualisiert sich, Feinde in der Liste: " + str(len(self.enemies)))

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

top = stack.top()
top.handle(command)
top.update()
class Screen:
def __init__(self, name):
self.name = name
def draw(self):
pass
def handle(self, command):
pass
def update(self):
pass
# Die Antwort des Bildschirms, der über diesem lag.
def on_result(self, result):
pass
class ScreenStack:
def __init__(self):
self.screens = []
def push(self, screen):
self.screens.append(screen)
def pop(self, result):
self.screens.pop()
below = self.top()
if below is not None:
below.on_result(result)
def top(self):
return self.screens[-1] if self.screens else None
def names(self):
return "[" + ", ".join(s.name for s in self.screens) + "]"
class TargetScreen(Screen):
def __init__(self, stack, enemies):
super().__init__("Ziel")
self.stack = stack
self.enemies = enemies
def handle(self, command):
index = int(command) - 1
print("Ziel bekommt " + command + " und liefert " + str(index))
self.stack.pop(index)
def update(self):
print("Ziel aktualisiert sich, Feinde in der Liste: " + str(len(self.enemies)))
class FightScreen(Screen):
def __init__(self, stack):
super().__init__("Kampf")
self.stack = stack
self.enemies = ["Wolf", "Goblin"]
def handle(self, command):
print("Kampf bekommt " + command + " und öffnet Ziel")
self.stack.push(TargetScreen(self.stack, self.enemies))
def on_result(self, result):
enemy = self.enemies.pop(result)
print("Kampf erhält " + str(result) + ": " + enemy + " fällt")
if not self.enemies:
print("Kampf: keine Feinde mehr, liefert Sieg")
self.stack.pop("Sieg")
class MapScreen(Screen):
def __init__(self, stack):
super().__init__("Karte")
self.stack = stack
def on_result(self, result):
print("Karte erhält " + str(result) + ": öffnet Stufenaufstieg")
self.stack.push(Screen("Stufenaufstieg"))
stack = ScreenStack()
stack.push(MapScreen(stack))
stack.push(FightScreen(stack))
for command in ["1", "2", "1", "1"]:
print("> " + command)
top = stack.top()
top.handle(command)
top.update()
print(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:

screens = ["Karte", "Nachricht", "Kampf", "Ziel"]
for screen in screens:
print("update " + screen)
if screen == "Nachricht":
screens.remove(screen)
Ausgabe
update Karte
update Nachricht
update Ziel
Python sagt nichts. for geht nach Index: Nachricht wird entfernt, alles rutscht einen Platz nach links, und Kampf, der als Nächstes kam, wird übersprungen. Das Programm läuft mit einem falschen Ergebnis weiter.

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:
def __init__(self, kind, screen, result):
self.kind = kind
self.screen = screen
self.result = result
class ScreenStack:
def __init__(self):
self.screens = []
self.changes = []
def push(self, screen):
self.changes.append(Change("push", screen, None))
def pop(self, result):
self.changes.append(Change("pop", None, result))
def top(self):
return self.screens[-1] if self.screens else None
def apply(self):
index = 0
while index < len(self.changes):
change = self.changes[index]
index += 1
if change.kind == "push":
self.screens.append(change.screen)
elif change.kind == "pop":
self.screens.pop()
below = self.top()
if below is not None:
below.on_result(change.result)
self.changes.clear()

Die Schleife ruft apply zuletzt auf:

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

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

class Screen:
def __init__(self, name):
self.name = name
def draw(self):
pass
def handle(self, command):
pass
def update(self):
pass
# Die Antwort des Bildschirms, der über diesem lag.
def on_result(self, result):
pass
class Change:
def __init__(self, kind, screen, result):
self.kind = kind
self.screen = screen
self.result = result
class ScreenStack:
def __init__(self):
self.screens = []
self.changes = []
def push(self, screen):
self.changes.append(Change("push", screen, None))
def pop(self, result):
self.changes.append(Change("pop", None, result))
def top(self):
return self.screens[-1] if self.screens else None
def apply(self):
index = 0
while index < len(self.changes):
change = self.changes[index]
index += 1
if change.kind == "push":
self.screens.append(change.screen)
elif change.kind == "pop":
self.screens.pop()
below = self.top()
if below is not None:
below.on_result(change.result)
self.changes.clear()
def names(self):
return "[" + ", ".join(s.name for s in self.screens) + "]"
class TargetScreen(Screen):
def __init__(self, stack, enemies):
super().__init__("Ziel")
self.stack = stack
self.enemies = enemies
def handle(self, command):
index = int(command) - 1
print("Ziel bekommt " + command + " und liefert " + str(index))
self.stack.pop(index)
def update(self):
print("Ziel aktualisiert sich, Feinde in der Liste: " + str(len(self.enemies)))
class FightScreen(Screen):
def __init__(self, stack):
super().__init__("Kampf")
self.stack = stack
self.enemies = ["Wolf", "Goblin"]
def handle(self, command):
print("Kampf bekommt " + command + " und öffnet Ziel")
self.stack.push(TargetScreen(self.stack, self.enemies))
def on_result(self, result):
enemy = self.enemies.pop(result)
print("Kampf erhält " + str(result) + ": " + enemy + " fällt")
if not self.enemies:
print("Kampf: keine Feinde mehr, liefert Sieg")
self.stack.pop("Sieg")
class MapScreen(Screen):
def __init__(self, stack):
super().__init__("Karte")
self.stack = stack
def on_result(self, result):
print("Karte erhält " + str(result) + ": öffnet Stufenaufstieg")
self.stack.push(Screen("Stufenaufstieg"))
stack = ScreenStack()
stack.push(MapScreen(stack))
stack.push(FightScreen(stack))
stack.apply()
for command in ["1", "2", "1", "1"]:
print("> " + command)
top = stack.top()
top.handle(command)
top.update()
stack.apply()
print(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 auch hier zufällig funktionieren, aber while mit einem Index sagt „lies die Länge neu“ ausdrücklich und hängt nicht davon ab, wie sich der Iterator verhält.
  • 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:
def __init__(self):
self.screens = []
self.changes = []
def push(self, screen):
self.changes.append(Change("push", screen, None))
def pop(self, result):
self.changes.append(Change("pop", None, result))
def top(self):
return self.screens[-1] if self.screens else None
def apply(self):
index = 0
while index < len(self.changes):
change = self.changes[index]
index += 1
if change.kind == "push":
self.screens.append(change.screen)
elif change.kind == "pop":
self.screens.pop()
below = self.top()
if below is not None:
below.on_result(change.result)
self.changes.clear()
# Ein Zug.
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 on_result 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?

def apply(self):
    index = 0
    while index < :
        change = self.changes[index]
        index += 1
        if change.kind == "push":
            self.screens.append(change.screen)
        elif change.kind == "pop":
            self.screens.pop()
            below = self.top()
            if below is not None:
                below.on_result(change.result)
    self.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?

stack = ScreenStack()
stack.push(NamedScreen("Karte"))
stack.push(NamedScreen("Kampf"))
stack.apply()
stack.pop("Sieg")
stack.push(NamedScreen("Stufenaufstieg"))
print("Oben: " + stack.top().name)
stack.apply()
print("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:

def replace(self, screen):
self.changes.append(Change("replace", screen, None))
def reset(self, screen):
self.changes.append(Change("reset", screen, None))

apply hat für jede einen Zweig:

elif change.kind == "replace":
self.screens.pop()
self.screens.append(change.screen)
elif change.kind == "reset":
self.screens.clear()
if change.screen is not None:
self.screens.append(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:

stack = ScreenStack()
stack.push(NamedScreen("Titel"))
stack.apply()
print(stack.names())
stack.replace(NamedScreen("Neuer Held"))
stack.apply()
print(stack.names())
stack.replace(NamedScreen("Dorf"))
stack.apply()
print(stack.names())
print("Der Held fällt. Mit pop:")
popped = in_fight()
popped.pop("Niederlage")
popped.pop("Niederlage")
popped.push(NamedScreen("Spielende"))
popped.apply()
print(popped.names())
print("Der Held fällt. Mit reset:")
clean = in_fight()
clean.reset(NamedScreen("Spielende"))
clean.apply()
print(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:

class Screen:
def __init__(self, name):
self.name = name
def draw(self):
pass
def handle(self, command):
pass
def update(self):
pass
# Die Antwort des Bildschirms, der über diesem lag.
def on_result(self, result):
pass
class Change:
def __init__(self, kind, screen, result):
self.kind = kind
self.screen = screen
self.result = result
class ScreenStack:
def __init__(self):
self.screens = []
self.changes = []
def push(self, screen):
self.changes.append(Change("push", screen, None))
def pop(self, result):
self.changes.append(Change("pop", None, result))
def replace(self, screen):
self.changes.append(Change("replace", screen, None))
def reset(self, screen):
self.changes.append(Change("reset", screen, None))
def top(self):
return self.screens[-1] if self.screens else None
def apply(self):
index = 0
while index < len(self.changes):
change = self.changes[index]
index += 1
if change.kind == "push":
self.screens.append(change.screen)
elif change.kind == "pop":
self.screens.pop()
below = self.top()
if below is not None:
below.on_result(change.result)
elif change.kind == "replace":
self.screens.pop()
self.screens.append(change.screen)
elif change.kind == "reset":
self.screens.clear()
if change.screen is not None:
self.screens.append(change.screen)
self.changes.clear()
def names(self):
return "[" + ", ".join(s.name for s in self.screens) + "]"
class NamedScreen(Screen):
def on_result(self, result):
print(self.name + " erhält " + str(result))
def in_fight():
stack = ScreenStack()
stack.push(NamedScreen("Dorf"))
stack.push(NamedScreen("Karte"))
stack.push(NamedScreen("Kampf"))
stack.apply()
return stack
stack = ScreenStack()
stack.push(NamedScreen("Titel"))
stack.apply()
print(stack.names())
stack.replace(NamedScreen("Neuer Held"))
stack.apply()
print(stack.names())
stack.replace(NamedScreen("Dorf"))
stack.apply()
print(stack.names())
print("Der Held fällt. Mit pop:")
popped = in_fight()
popped.pop("Niederlage")
popped.pop("Niederlage")
popped.push(NamedScreen("Spielende"))
popped.apply()
print(popped.names())
print("Der Held fällt. Mit reset:")
clean = in_fight()
clean.reset(NamedScreen("Spielende"))
clean.apply()
print(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.
class ScreenStack:
def __init__(self):
self.screens = []
self.changes = []
def push(self, screen):
self.changes.append(SimpleNamespace(kind='push', screen=screen, result=None))
def pop(self, result):
self.changes.append(SimpleNamespace(kind='pop', screen=None, result=result))
def replace(self, screen):
self.changes.append(SimpleNamespace(kind='replace', screen=screen, result=None))
# Leert den ganzen Stapel (Laden, Game over); mit None bleibt der Stapel leer.
def reset(self, screen):
self.changes.append(SimpleNamespace(kind='reset', screen=screen, result=None))
def top(self):
return None if len(self.screens) == 0 else self.screens[-1]
def is_empty(self):
return len(self.screens) == 0 and len(self.changes) == 0
def contains(self, kind_name):
for screen in self.screens:
if getattr(screen, 'kind_name', None) == kind_name:
return True
return False
def apply(self):
index = 0
while index < len(self.changes):
change = self.changes[index]
index += 1
if change.kind == 'push':
self.screens.append(change.screen)
elif change.kind == 'pop':
# pop() auf einer leeren Liste ist in Python ein Fehler; in JavaScript liefert es undefined.
if len(self.screens) > 0:
self.screens.pop()
below = self.top()
if below is not None:
below.on_result(change.result)
elif change.kind == 'replace':
if len(self.screens) > 0:
self.screens.pop()
self.screens.append(change.screen)
else:
self.screens = []
if change.screen is not None:
self.screens.append(change.screen)
self.changes = []
# Das Zeichnen beginnt beim obersten undurchsichtigen Bildschirm und geht nach oben.
def draw(self, buffer):
first = len(self.screens) - 1
while first > 0 and self.screens[first].is_transparent():
first -= 1
for i in range(first, len(self.screens)):
if i >= 0:
self.screens[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.
class TargetScreen(Screen):
def __init__(self, game, battle):
super().__init__(game)
self.battle = battle
self.note = ''
def is_transparent(self):
return True
def draw(self, buffer):
enemies = self.battle.alive_enemies()
buffer.box(24, 9, 36, len(enemies) + 4, t('target.title'))
for i in range(len(enemies)):
buffer.text(26, 10 + i, str(i + 1) + ') ' + pad(enemies[i].name, 22) + ' ' + str(enemies[i].hp) + '/' + str(enemies[i].max_hp()), enemies[i].kind.color)
buffer.text(26, 11 + len(enemies), self.note if len(self.note) > 0 else t('target.help'), 'gray')
def handle(self, command):
if command == 'x':
self.game.stack.pop(None)
return
choice = parse_int_strict(command)
if choice is None or choice < 1 or choice > len(self.battle.alive_enemies()):
self.note = t('ui.unknown', {'command': command})
return
self.game.stack.pop(choice - 1)
# Eine allgemeine Auswahl aus einer Liste (Fähigkeit, Trank, Slot): liefert eine 0-basierte Nummer oder None.
class ChoiceScreen(Screen):
def __init__(self, game, title, labels):
super().__init__(game)
self.title = title
self.labels = labels
self.note = ''
def is_transparent(self):
return True
def draw(self, buffer):
buffer.box(4, 3, 52, len(self.labels) + 4, self.title)
for i in range(len(self.labels)):
buffer.text(6, 4 + i, str(i + 1) + ') ' + self.labels[i], 'default')
buffer.text(6, 5 + len(self.labels), self.note if len(self.note) > 0 else t('choice.help'), 'gray')
def handle(self, command):
if command == 'x':
self.game.stack.pop(None)
return
choice = parse_int_strict(command)
if choice is None or choice < 1 or choice > len(self.labels):
self.note = t('ui.unknown', {'command': command})
return
self.game.stack.pop(choice - 1)
class SlotScreen(ChoiceScreen):
def __init__(self, game, mode):
labels = []
for slot in SAVE_SLOTS:
labels.append(t('slot.label', {'slot': slot, 'summary': slot_summary(game.io, slot)}))
super().__init__(game, t('slot.saveTitle' if mode == 'save' else 'slot.loadTitle'), labels)
def handle(self, command):
choice = parse_int_strict(command)
if choice is not None and choice >= 1 and choice <= len(SAVE_SLOTS):
self.game.stack.pop(SAVE_SLOTS[choice - 1])
return
super().handle(command)

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

def on_result(self, result):
battle = self.battle
hero = battle.hero
pending = self.pending
self.pending = None
if result is None or pending is None:
return
if pending.kind == 'attack':
battle.hero_act(AttackAction(hero, battle.alive_enemies()[result]))
elif pending.kind == 'skill':
skill = SKILLS[hero.skills[result]]
if hero.mana < skill.mana:
self.note = t('battle.noMana', {'skill': nm(skill)})
elif hero.cooldown_of(skill.id) > 0:
self.note = t('battle.cooldown', {'skill': nm(skill), 'turns': hero.cooldown_of(skill.id)})
elif skill.target == 'one' and len(battle.alive_enemies()) > 1:
self.pending = SimpleNamespace(kind='skillTarget', skill=skill)
self.game.stack.push(TargetScreen(self.game, battle))
else:
battle.hero_act(SkillAction(hero, skill, battle.alive_enemies()[0]))
elif pending.kind == 'skillTarget':
battle.hero_act(SkillAction(hero, pending.skill, battle.alive_enemies()[result]))
else:
battle.hero_act(ItemAction(hero, self.potion_ids()[result]))

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.

class ConfirmScreen(Screen):
def __init__(self, game, question):
super().__init__(game)
self.question = question
def is_transparent(self):
return True
def draw(self, buffer):
buffer.box(16, 6, 28, 5, '')
buffer.text(18, 7, self.question, 'white')
buffer.text(18, 8, '1) ' + t('ui.yes') + ' 2) ' + t('ui.no'), 'default')
def handle(self, command):
if command == '1':
self.game.stack.pop(True)
elif command == '2' or command == 'x':
self.game.stack.pop(False)
def on_result(self, result):
game = self.game
pending = self.pending
self.pending = ''
if result is None:
return
if pending == 'save':
game.io.save(result, save_game(game))
self.note = t('menu.saved', {'slot': result})
elif pending == 'load':
error = load_game(game, game.io.load(result))
if error is not None:
self.note = error
else:
enter_world(game, t('load.done', {'slot': result}))
elif pending == 'quit' and result is True:
game.stack.reset(None)

Probier es aus

Finde den Fehler

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

class ScreenStack:
def __init__(self):
self.screens = []
self.changes = []
def push(self, screen):
self.changes.append(Change("push", screen, None))
def pop(self, result):
self.changes.append(Change("pop", None, result))
def top(self):
return self.screens[-1] if self.screens else None
def apply(self):
index = 0
while index < len(self.changes):
change = self.changes[index]
index += 1
if change.kind == "push":
self.screens.append(change.screen)
elif change.kind == "pop":
below = self.top()
self.screens.pop()
if below is not None:
below.on_result(change.result)
self.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.
self.stack.push(TargetScreen(self.enemies, lambda index: self.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 ein beliebiger Wert, 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: on_result 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 on_result 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 ·