Skip to content
wannacode

Screens that return an answer

In short: A screen that returns an answer closes with pop(result) and hands the result to the screen below (onResult), while changes to the stack are written down during the turn and applied at its end with apply.

Problem

The screen asks. Who hears the answer?

In the last lesson screens only opened and closed. Now a screen has to ask something and get an answer. The fight has two enemies, the player presses "attack", and the fight needs to know which one. The menu has to ask "Are you sure?" before quitting the game.

The first attempt is a shared variable: the target screen writes the choice into selectedTarget, and the fight reads it. The player picks the Goblin, then opens the choice again and changes their mind (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("You hit " + enemies[selectedTarget] + ".");
else System.out.println("Nobody is hit.");
}
// The player picks 2, then changes their mind (x).
targetScreen("2");
fightTurn();
targetScreen("x");
fightTurn();
Output
You hit Goblin.
You hit Goblin.

The second hit lands on the Goblin, but the player cancelled. The choice from the last turn is still in the variable. Who should set it back to -1? The fight, after reading it? And what if the inventory uses the same variable too? A shared variable has no owner, so old answers stay in it.

The second attempt is to let the target call the fight itself:

class TargetScreen extends Screen {
FightScreen fight;
@Override
void handle(String command) {
// The target knows the fight and calls it itself.
fight.attack(Integer.parseInt(command) - 1);
stack.pop();
}
}

That works, but now the target knows the fight. In the game, "who?" is asked by an attack, by a spell and by a potion that heals an ally. Each of them wants its own target screen, or one screen with an if for everyone who uses it. You need a way for the answer to go back to whoever asked, without the screen that answers knowing who that is.

The idea

pop(result) and onResult

The screen that asks opens the one that answers with push. The one that answers removes itself with pop and hands over the answer: pop(result). The stack knows who is below: exactly the screen that asked. So it passes the answer on by calling its onResult.

The screen gets one more method. By default it does nothing: most screens never ask anything.

class Screen {
String name;
Screen(String name) {
this.name = name;
}
void draw() {}
void handle(String command) {}
void update() {}
// The answer of the screen that was above this one.
void onResult(Object result) {}
}

And pop from the last lesson becomes pop(result):

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

This way the two sides do not know each other. The target knows the question and the shape of the answer: the number of an enemy, or null for "cancel". The fight knows what to do with the number. It is like calling a method that returns a value, except that turns pass between the question and the answer: the player thinks, presses a key, may change their mind.

See it

Picking a target

The target draws a list of the living enemies and waits for a digit. x is "back" and returns null. The number is counted from 0, because it is an index into the list:

class TargetScreen extends Screen {
ScreenStack stack;
List<Enemy> enemies;
TargetScreen(ScreenStack stack, List<Enemy> enemies) {
super("Target");
this.stack = stack;
this.enemies = enemies;
}
@Override
void draw() {
String line = "Who do you hit?";
for (int i = 0; i < enemies.size(); i++) {
line += " " + (i + 1) + ") " + enemies.get(i).name;
}
System.out.println(line + " x) back");
}
@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);
}
}

The fight opens the target on 1 and reads the answer in onResult. When there are no enemies left, the fight also returns an answer to the screen below it, the map: "victory".

class FightScreen extends Screen {
ScreenStack stack;
List<Enemy> enemies = new ArrayList<>();
FightScreen(ScreenStack stack) {
super("Fight");
this.stack = stack;
enemies.add(new Enemy("Wolf", 4));
enemies.add(new Enemy("Goblin", 7));
}
@Override
void draw() {
String line = "Fight:";
for (Enemy enemy : enemies) line += " " + enemy.name + " " + enemy.hp;
System.out.println(line + " | 1) attack");
}
@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("You change your mind. Nobody is hit.");
return;
}
Enemy enemy = enemies.get((int) result);
enemy.hp -= 4;
if (enemy.hp > 0) {
System.out.println("You hit " + enemy.name + " for 4.");
return;
}
System.out.println(enemy.name + " falls.");
enemies.remove(enemy);
if (enemies.isEmpty()) stack.pop("victory");
}
}

The program reads its commands from the input. Each turn draws only the screen on top, to keep the output short:

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("Goodbye.");
Output
Map: potions 2 | f) fight d) drop a potion q) quit
> f
Fight: Wolf 4 Goblin 7 | 1) attack
> 1
Who do you hit? 1) Wolf 2) Goblin x) back
> 2
You hit Goblin for 4.
Fight: Wolf 4 Goblin 3 | 1) attack
> 1
Who do you hit? 1) Wolf 2) Goblin x) back
> x
You change your mind. Nobody is hit.
Fight: Wolf 4 Goblin 3 | 1) attack
> 1
Who do you hit? 1) Wolf 2) Goblin x) back
> 2
Goblin falls.
Fight: Wolf 4 | 1) attack
> 1
Who do you hit? 1) Wolf x) back
> 1
Wolf falls.
Victory! Back to the map.
Map: potions 2 | f) fight d) drop a potion q) quit
> d
Are you sure you want to drop a potion? 1) yes 2) no
> 2
The potion stays.
Map: potions 2 | f) fight d) drop a potion q) quit
> q
Are you sure you want to quit? 1) yes 2) no
> 1
Goodbye.

The input is:

Input
f
1
2
1
x
1
2
1
1
d
2
q
1

Cancelling (x) hits nobody: the answer is empty and the fight knows it. The choice from the previous turn is not left anywhere.

More precisely, if you are curious

The whole program, with the map and the "Are you sure?" question from the next card:

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() {}
// The answer of the screen that was above this one.
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("Target");
this.stack = stack;
this.enemies = enemies;
}
@Override
void draw() {
String line = "Who do you hit?";
for (int i = 0; i < enemies.size(); i++) {
line += " " + (i + 1) + ") " + enemies.get(i).name;
}
System.out.println(line + " x) back");
}
@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("Fight");
this.stack = stack;
enemies.add(new Enemy("Wolf", 4));
enemies.add(new Enemy("Goblin", 7));
}
@Override
void draw() {
String line = "Fight:";
for (Enemy enemy : enemies) line += " " + enemy.name + " " + enemy.hp;
System.out.println(line + " | 1) attack");
}
@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("You change your mind. Nobody is hit.");
return;
}
Enemy enemy = enemies.get((int) result);
enemy.hp -= 4;
if (enemy.hp > 0) {
System.out.println("You hit " + enemy.name + " for 4.");
return;
}
System.out.println(enemy.name + " falls.");
enemies.remove(enemy);
if (enemies.isEmpty()) stack.pop("victory");
}
}
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) yes 2) no");
}
@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("Map");
this.stack = stack;
}
@Override
void draw() {
System.out.println("Map: potions " + potions + " | f) fight d) drop a potion q) quit");
}
@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, "Are you sure you want to drop a potion?"));
} else if (command.equals("q")) {
pending = "quit";
stack.push(new ConfirmScreen(stack, "Are you sure you want to quit?"));
}
}
@Override
void onResult(Object result) {
String asked = pending;
pending = "";
if (asked.equals("drop")) {
if (result.equals(true)) {
potions--;
System.out.println("You drop a potion.");
} else {
System.out.println("The potion stays.");
}
} else if (asked.equals("quit")) {
if (result.equals(true)) stack.pop(null);
} else if (result.equals("victory")) {
System.out.println("Victory! Back to the map.");
}
}
}
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("Goodbye.");
}
}

The idea

One question for everything: "Are you sure?"

The question screen does not know what it is asking. It shows the text it was given and returns true or false:

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) yes 2) no");
}
@Override
void handle(String command) {
if (command.equals("1")) stack.pop(true);
else if (command.equals("2")) stack.pop(false);
}
}

The map uses it for two different questions: whether to drop a potion and whether to quit. But the answer is only "yes" or "no". How does the map know which question it answers? It remembers what it asked. Before the push it writes pending, and when the answer comes, it reads it and clears it right away:

class MapScreen extends Screen {
ScreenStack stack;
int potions = 2;
String pending = "";
MapScreen(ScreenStack stack) {
super("Map");
this.stack = stack;
}
@Override
void draw() {
System.out.println("Map: potions " + potions + " | f) fight d) drop a potion q) quit");
}
@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, "Are you sure you want to drop a potion?"));
} else if (command.equals("q")) {
pending = "quit";
stack.push(new ConfirmScreen(stack, "Are you sure you want to quit?"));
}
}
@Override
void onResult(Object result) {
String asked = pending;
pending = "";
if (asked.equals("drop")) {
if (result.equals(true)) {
potions--;
System.out.println("You drop a potion.");
} else {
System.out.println("The potion stays.");
}
} else if (asked.equals("quit")) {
if (result.equals(true)) stack.pop(null);
} else if (result.equals("victory")) {
System.out.println("Victory! Back to the map.");
}
}
}

The clearing matters. If pending stayed "drop", the next answer from the fight ("victory") would be read as the answer to the potion question. That is the same bug as the shared variable from the start, but now inside one screen that owns its own question. The real game does the same: the fight remembers whether it asked for an attack, a skill or a potion.

Problem

The stack changes while the turn is still running

So far the loop called only handle on the top. A real turn also has update: the screen updates its state after the command. Here is the same fight, but every screen writes into a log what it does. In update, the target counts the enemies in its list:

class TargetScreen extends Screen {
ScreenStack stack;
List<String> enemies;
TargetScreen(ScreenStack stack, List<String> enemies) {
super("Target");
this.stack = stack;
this.enemies = enemies;
}
@Override
void handle(String command) {
int index = Integer.parseInt(command) - 1;
System.out.println("Target takes " + command + " and returns " + index);
stack.pop(index);
}
@Override
void update() {
System.out.println("Target updates, enemies in the list: " + enemies.size());
}
}

The loop is like in the real game: take the top, give it the command, update it.

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() {}
// The answer of the screen that was above this one.
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("Target");
this.stack = stack;
this.enemies = enemies;
}
@Override
void handle(String command) {
int index = Integer.parseInt(command) - 1;
System.out.println("Target takes " + command + " and returns " + index);
stack.pop(index);
}
@Override
void update() {
System.out.println("Target updates, enemies in the list: " + enemies.size());
}
}
class FightScreen extends Screen {
ScreenStack stack;
List<String> enemies = new ArrayList<>(List.of("Wolf", "Goblin"));
FightScreen(ScreenStack stack) {
super("Fight");
this.stack = stack;
}
@Override
void handle(String command) {
System.out.println("Fight takes " + command + " and opens Target");
stack.push(new TargetScreen(stack, enemies));
}
@Override
void onResult(Object result) {
String enemy = enemies.remove((int) result);
System.out.println("Fight gets " + result + ": " + enemy + " falls");
if (enemies.isEmpty()) {
System.out.println("Fight: no enemies left, returns victory");
stack.pop("victory");
}
}
}
class MapScreen extends Screen {
ScreenStack stack;
MapScreen(ScreenStack stack) {
super("Map");
this.stack = stack;
}
@Override
void onResult(Object result) {
System.out.println("Map gets " + result + ": opens Level up");
stack.push(new Screen("Level up"));
}
}
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());
}
}
Output
> 1
Fight takes 1 and opens Target
> 2
Target takes 2 and returns 1
Fight gets 1: Goblin falls
Target updates, enemies in the list: 1
> 1
Fight takes 1 and opens Target
> 1
Target takes 1 and returns 0
Fight gets 0: Wolf falls
Fight: no enemies left, returns victory
Map gets victory: opens Level up
Target updates, enemies in the list: 0
[Map, Level up]

Look at the last turn. The target has not finished its handle yet, and inside it pop has already removed it, the fight has hit the Wolf, the fight has returned "victory", and the map has opened "Level up". Only then does the loop call update on the target: a screen that is no longer on the stack updates a list of 0 enemies for a fight that is over. In the real game, such an update would read an enemy by number from an empty list, and the game would stop with an error.

If the loop asked for the top again before update, it would update "Level up": the wrong screen, one that has not been drawn even once. There are two roots. The stack changes in the middle of the turn. And the answers run nested: the fight's onResult runs inside the target's handle, and the map's onResult inside that.

More precisely, if you are curious

It gets worse when you walk all screens on the stack (say, each has an update) and one of them removes itself during the walk. Then the languages behave differently:

List<String> screens = new ArrayList<>(List.of("Map", "Message", "Fight", "Target"));
try {
for (String screen : screens) {
System.out.println("update " + screen);
if (screen.equals("Message")) screens.remove(screen);
}
} catch (ConcurrentModificationException e) {
System.out.println("ConcurrentModificationException");
}
Output
update Map
update Message
ConcurrentModificationException

Java notices the change and throws ConcurrentModificationException on the next step. Careful: if you remove the second to last element, there is no error. hasNext() compares the position with the new length, says "nothing more", and the loop quietly stops without the last one:

List<String> screens = new ArrayList<>(List.of("Map", "Message", "Fight", "Target"));
try {
for (String screen : screens) {
System.out.println("update " + screen);
if (screen.equals("Fight")) screens.remove(screen);
}
} catch (ConcurrentModificationException e) {
System.out.println("ConcurrentModificationException");
}
Output
update Map
update Message
update Fight

An error that stops the program is at least visible. A silent bug, where one screen is not updated on one turn, is much harder to catch.

The idea

Deferred changes: write now, apply later

The fix is to not touch the stack during the turn. push and pop no longer move anything. They only write down the change into a list, changes. At the end of the turn, apply walks the list and applies the changes one by one. A change is a small object: a kind, a screen (for push) and an answer (for 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();
}
}

The loop calls apply last:

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

The same fight with the same screens, only the stack is different:

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() {}
// The answer of the screen that was above this one.
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("Target");
this.stack = stack;
this.enemies = enemies;
}
@Override
void handle(String command) {
int index = Integer.parseInt(command) - 1;
System.out.println("Target takes " + command + " and returns " + index);
stack.pop(index);
}
@Override
void update() {
System.out.println("Target updates, enemies in the list: " + enemies.size());
}
}
class FightScreen extends Screen {
ScreenStack stack;
List<String> enemies = new ArrayList<>(List.of("Wolf", "Goblin"));
FightScreen(ScreenStack stack) {
super("Fight");
this.stack = stack;
}
@Override
void handle(String command) {
System.out.println("Fight takes " + command + " and opens Target");
stack.push(new TargetScreen(stack, enemies));
}
@Override
void onResult(Object result) {
String enemy = enemies.remove((int) result);
System.out.println("Fight gets " + result + ": " + enemy + " falls");
if (enemies.isEmpty()) {
System.out.println("Fight: no enemies left, returns victory");
stack.pop("victory");
}
}
}
class MapScreen extends Screen {
ScreenStack stack;
MapScreen(ScreenStack stack) {
super("Map");
this.stack = stack;
}
@Override
void onResult(Object result) {
System.out.println("Map gets " + result + ": opens Level up");
stack.push(new Screen("Level up"));
}
}
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());
}
}
Output
> 1
Fight takes 1 and opens Target
> 2
Target takes 2 and returns 1
Target updates, enemies in the list: 2
Fight gets 1: Goblin falls
> 1
Fight takes 1 and opens Target
> 1
Target takes 1 and returns 0
Target updates, enemies in the list: 1
Fight gets 0: Wolf falls
Fight: no enemies left, returns victory
Map gets victory: opens Level up
[Map, Level up]

Now the target finishes handle and update while it is still on top and everything around it is as it was before the turn. Only then does apply remove it, and the fight gets its answer. Every screen works in a stack that does not change under its feet.

Three details in apply matter:

  • while with an index, not foreach. While apply is applying a pop, the fight gets its answer and may write a new change: pop(victory). It goes to the end of the same list, and while reads its length again and reaches it too. foreach would throw the error from the previous card.
  • The list is cleared at the end. Otherwise, on the next turn, the same changes would be applied again.
  • The order is the order of writing. If a screen writes a replace and then a push in one turn, the stack does exactly that.

See it

One turn, change by change

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();
}
}
// One turn.
Screen top = stack.top();
top.handle(command);
top.update();
stack.apply();
Step by step, in words
  1. The last turn of the fight. On the stack: Map, Fight, and Target on top. Only the Wolf is left. The list of changes is empty.
  2. Command 1 goes to the top, Target. It calls pop(0): 0 is the number of the chosen enemy. The stack does not move. The change is only written into changes.
  3. update runs on Target, which is still on the stack. Everything it sees is as it was before the turn: the fight is not over yet.
  4. End of the turn: apply takes change 0. Target leaves the stack, and 0 goes down to Fight through onResult. The Wolf falls, no enemies are left, and Fight calls pop(victory): a new change at the end of the list.
  5. while reads the length of changes again, so it reaches the new change too. Fight leaves, and Map gets "victory". It writes push(Level up).
  6. The third change is a push: Level up goes on the stack, above Map.
  7. The list is done. changes is cleared, so the same changes are not applied again next turn. The stack is Map, Level up. Every screen got its answer after the turn, never in the middle of one.

Try it

Fill in the blank

This is the loop of apply. What must the condition be, so that it also reaches the changes written while it is running?

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

Try it

Predict the output

NamedScreen is a screen that only says what it got. The stack is the one with deferred changes. What does the program print?

ScreenStack stack = new ScreenStack();
stack.push(new NamedScreen("Map"));
stack.push(new NamedScreen("Fight"));
stack.apply();
stack.pop("victory");
stack.push(new NamedScreen("Level up"));
System.out.println("On top: " + stack.top().name);
stack.apply();
System.out.println("On top: " + stack.top().name);
Answer

The idea

Transitions: replace and reset

Not every change of screen is a question and an answer. Some are transitions with no way back. For those the stack has two more changes:

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

apply has one branch for each of them:

} 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 removes the top screen and puts another one in its place. The title screen opens hero creation, and hero creation opens the village. With push, the title screen would stay at the bottom, and on "back" the player would land in it in the middle of the game.
  • reset clears the whole stack and puts one screen (or none: then the game ends). There are two uses for it: the hero has fallen (the game over screen) and a save has been loaded (the village or the map).

Why not use pop for the end of the game? The stack is Village, Map, Fight. Here are both ways:

ScreenStack stack = new ScreenStack();
stack.push(new NamedScreen("Title"));
stack.apply();
System.out.println(stack.names());
stack.replace(new NamedScreen("New hero"));
stack.apply();
System.out.println(stack.names());
stack.replace(new NamedScreen("Village"));
stack.apply();
System.out.println(stack.names());
System.out.println("The hero falls. With pop:");
ScreenStack popped = inFight();
popped.pop("defeat");
popped.pop("defeat");
popped.push(new NamedScreen("Game over"));
popped.apply();
System.out.println(popped.names());
System.out.println("The hero falls. With reset:");
ScreenStack clean = inFight();
clean.reset(new NamedScreen("Game over"));
clean.apply();
System.out.println(clean.names());
Output
[Title]
[New hero]
[Village]
The hero falls. With pop:
Map gets defeat
Village gets defeat
[Village, Game over]
The hero falls. With reset:
[Game over]

With pop, the map and the village get "defeat" and react as if the fight had ended the normal way. You also have to know how many screens are below. And the village stays under the game over screen. reset calls onResult on nobody: this is not an answer, it is a fresh start.

More precisely, if you are curious

replace and reset are deferred too: they are written down and applied in apply, in the order they were written. Hero creation in the game calls replace(the village) and right after it push(the message with the start of the story). After apply the stack is the village with the message on top, not the other way around.

The whole program:

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() {}
// The answer of the screen that was above this one.
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 + " gets " + result);
}
}
public class Main {
static ScreenStack inFight() {
ScreenStack stack = new ScreenStack();
stack.push(new NamedScreen("Village"));
stack.push(new NamedScreen("Map"));
stack.push(new NamedScreen("Fight"));
stack.apply();
return stack;
}
public static void main(String[] args) {
ScreenStack stack = new ScreenStack();
stack.push(new NamedScreen("Title"));
stack.apply();
System.out.println(stack.names());
stack.replace(new NamedScreen("New hero"));
stack.apply();
System.out.println(stack.names());
stack.replace(new NamedScreen("Village"));
stack.apply();
System.out.println(stack.names());
System.out.println("The hero falls. With pop:");
ScreenStack popped = inFight();
popped.pop("defeat");
popped.pop("defeat");
popped.push(new NamedScreen("Game over"));
popped.apply();
System.out.println(popped.names());
System.out.println("The hero falls. With reset:");
ScreenStack clean = inFight();
clean.reset(new NamedScreen("Game over"));
clean.apply();
System.out.println(clean.names());
}
}

The idea

In the real game

The stack of Chronicles of Arcanum is the same, with three more things:

// The stack does not change during handling: push, pop and replace only record the change, and
// apply() applies it after the turn. Otherwise a screen being removed from the stack would keep running.
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));
}
// Clears the whole stack (loading, game over); with null the stack stays empty.
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 on an empty stack in JavaScript does nothing; in Java remove throws, so there is a check.
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<>();
}
// Drawing starts from the topmost opaque screen and goes up.
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 also looks at the changes. The game starts with a push of the title screen, and quitting is a reset with no screen. The loop stops when there are neither screens nor waiting changes.
  • contains asks whether the stack already has a screen of a given kind. That way the level up screen does not go in twice if two levels are gained in one turn.
  • draw draws bottom up from the topmost opaque screen, as in the last lesson.

Picking a target in the game is a transparent screen over the fight. It does not hit anyone itself: it returns the number with pop, and the fight decides what to do. Next to it are the general pick from a list (a skill, a potion) and the pick of a save slot, which returns the name of the slot, not a number:

// Choosing a target is a transparent screen over the battle. It does not attack: pop(result) returns the target number,
// and the battle decides what to do with it.
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);
}
}
// A general choice from a list (skill, potion, slot): returns a 0-based number or 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 the constructor calls super first, so the labels are made in a separate function.
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);
}
}

The fight remembers what it asked (pending) and reads the answer 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)));
}
}

Look at the skill branch. The player picked a skill that needs a target, and there is more than one enemy. Then the fight, inside onResult, writes a new question: a push of the target screen. This happens during apply, and while reaches it and applies it in the same turn. The player sees the list of enemies right after the list of skills.

The menu is the other screen that asks. "Are you sure?" is the same general screen, and the answer "yes" to the quit question is a reset with no screen: the stack becomes empty and the game ends.

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

Try it

Find the bug

A colleague swapped two lines in apply. The fight program from "Deferred changes" now prints:

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();
}
}
Output
> 1
Fight takes 1 and opens Target
> 2
Target takes 2 and returns 1
Target updates, enemies in the list: 2
> 1
Fight takes 1 and opens Target
> 1
Target takes 1 and returns 0
Target updates, enemies in the list: 2
[Map, Fight]

The target returns a number, but the fight never finds out: both enemies are alive, and at the end the stack is the map and the fight. Which line runs too early?

Pick the line with the bug.

More precisely

More precisely, for the curious

Why not a callback? The fight could give the target a function: "when the player picks, call this".

// The fight tells the target what to do with the choice.
stack.push(new TargetScreen(enemies, index -> attack(index)));

That works, and it is common. The cost is in the details. The function is called by the target, inside its handle: we are in the middle of the turn again, unless the call is deferred too. The function holds on to the fight, and the game has to be saved: a stack of screens with data can be saved, but a function cannot be written to a file. And the order becomes invisible: who will be called is clear only once you see who passed what. onResult says the same with a rule you can see in the stack: the answer always goes to the screen below.

The same thing elsewhere. On Android, one activity opens another and waits for a result (startActivityForResult, today the Activity Result API). Modal dialogs in desktop apps return a value when they close ("OK" or "Cancel"). Navigation in mobile apps is a stack too: you open a screen on top, and "back" returns you, sometimes with a result.

With await. In JavaScript, C# and Python you can write const target = await targetScreen.open(): the fight "waits" for the answer as if it were an ordinary call. It looks more natural, but in a turn-based game with one loop it is harder. The fight is no longer an object that receives commands, it is a function paused in the middle. Its state is hidden in "where it stopped", and that does not go into a save file. And it still has to be clear at which moment of the turn it resumes.

The type of the answer. Here the answer is Object, and the fight knows it is a number. It can be stricter: the target returns its own type (say, a record with the number), or the screen is generic over the type of the answer. In the game the answers are few and simple (a number, "yes" or "no", a slot name, the result of a fight), so the check of pending is enough.

Exercise

A shop with two questions

Solve it in your editor. Start from the stack with deferred changes from this lesson and from ConfirmScreen.

Shop. ShopScreen has 20 gold and sells a potion for 5 and a sword for 12. Choosing an item opens QuantityScreen, which returns the count (from 1 to 9), or nothing if the player presses x. Then the shop opens ConfirmScreen with the question "Buy 2 x Potion for 10 gold?". One screen collects two answers, one after the other: onResult knows which answer has arrived by pending, which moves from "count" to "confirm". If there is not enough gold, nothing is bought. x in the shop closes it.

Note: ConfirmScreen is opened from onResult, that is, while apply is still running. That is why the loop in apply reads the length of the list again.

Input
1
2
1
2
1
2
2
x
2
1
1
1
1
1
x
Output
Shop: gold 20 | 1) Potion 5 2) Sword 12 x) leave
> 1
How many? 1-9 | x) back
> 2
Buy 2 x Potion for 10 gold? 1) yes 2) no
> 1
You buy 2 x Potion.
Shop: gold 10 | 1) Potion 5 2) Sword 12 x) leave
> 2
How many? 1-9 | x) back
> 1
Buy 1 x Sword for 12 gold? 1) yes 2) no
> 2
You do not buy.
Shop: gold 10 | 1) Potion 5 2) Sword 12 x) leave
> 2
How many? 1-9 | x) back
> x
You change your mind.
Shop: gold 10 | 1) Potion 5 2) Sword 12 x) leave
> 2
How many? 1-9 | x) back
> 1
Buy 1 x Sword for 12 gold? 1) yes 2) no
> 1
Not enough gold.
Shop: gold 10 | 1) Potion 5 2) Sword 12 x) leave
> 1
How many? 1-9 | x) back
> 1
Buy 1 x Potion for 5 gold? 1) yes 2) no
> 1
You buy 1 x Potion.
Shop: gold 5 | 1) Potion 5 2) Sword 12 x) leave
> x
Goodbye.

More tasks

  • Selling. Add s) sell: the shop asks which item (one more screen with an answer) and how many, and pays back half the price, rounded down. How many values will pending have now?
  • Bag. Keep the bought items in a list and print it on leaving. A potion bought twice is one line with 3 pieces, not two lines.
  • No pending. Make QuantityScreen and ConfirmScreen return a record with the name of the question and the value. Can the shop then do without pending? What do you gain and what do you lose?

Test

Check yourself

1. Why does the target screen not call the fight itself, and instead return a number with pop?
2. The map opened "Are you sure?" and got true. How does it know whether to quit the game or to drop a potion?
3. Why does apply walk the changes with while (index < length) and not with foreach?
4. A colleague deleted the last line of apply, the one that clears changes. What happens?
5. The hero falls in a fight. The stack is Village, Map, Fight. Why does the game call reset(Game over) and not two pops and a push?

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