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 != "x") selectedTarget = int.Parse(command) - 1;
}
static void FightTurn()
{
if (selectedTarget >= 0) Console.WriteLine("You hit " + enemies[selectedTarget] + ".");
else Console.WriteLine("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 : Screen
{
FightScreen fight;
public override void Handle(string command)
{
// The target knows the fight and calls it itself.
fight.Attack(int.Parse(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
{
public string Name;
public Screen(string name)
{
Name = name;
}
public virtual void Draw() { }
public virtual void Handle(string command) { }
public virtual void Update() { }
// The answer of the screen that was above this one.
public virtual void OnResult(object? result) { }
}

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

public void Pop(object? result)
{
Screens.RemoveAt(Screens.Count - 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 : Screen
{
ScreenStack stack;
List<Enemy> enemies;
public TargetScreen(ScreenStack stack, List<Enemy> enemies) : base("Target")
{
this.stack = stack;
this.enemies = enemies;
}
public override void Draw()
{
string line = "Who do you hit?";
for (int i = 0; i < enemies.Count; i++)
{
line += " " + (i + 1) + ") " + enemies[i].Name;
}
Console.WriteLine(line + " x) back");
}
public override void Handle(string command)
{
if (command == "x")
{
stack.Pop(null);
return;
}
int choice = int.Parse(command);
if (choice >= 1 && choice <= enemies.Count) 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 : Screen
{
ScreenStack stack;
List<Enemy> enemies = new List<Enemy>();
public FightScreen(ScreenStack stack) : base("Fight")
{
this.stack = stack;
enemies.Add(new Enemy("Wolf", 4));
enemies.Add(new Enemy("Goblin", 7));
}
public override void Draw()
{
string line = "Fight:";
foreach (Enemy enemy in enemies) line += " " + enemy.Name + " " + enemy.Hp;
Console.WriteLine(line + " | 1) attack");
}
public override void Handle(string command)
{
if (command == "1") stack.Push(new TargetScreen(stack, enemies));
}
public override void OnResult(object? result)
{
if (result == null)
{
Console.WriteLine("You change your mind. Nobody is hit.");
return;
}
Enemy enemy = enemies[(int)result];
enemy.Hp -= 4;
if (enemy.Hp > 0)
{
Console.WriteLine("You hit " + enemy.Name + " for 4.");
return;
}
Console.WriteLine(enemy.Name + " falls.");
enemies.Remove(enemy);
if (enemies.Count == 0) 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));
while (stack.Top() != null)
{
stack.Top()!.Draw();
string command = Console.ReadLine()!.Trim();
Console.WriteLine("> " + command);
stack.Top()!.Handle(command);
}
Console.WriteLine("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:

class Screen
{
public string Name;
public Screen(string name)
{
Name = name;
}
public virtual void Draw() { }
public virtual void Handle(string command) { }
public virtual void Update() { }
// The answer of the screen that was above this one.
public virtual void OnResult(object? result) { }
}
class ScreenStack
{
public List<Screen> Screens = new List<Screen>();
public void Push(Screen screen)
{
Screens.Add(screen);
}
public void Pop(object? result)
{
Screens.RemoveAt(Screens.Count - 1);
Screen? below = Top();
if (below != null) below.OnResult(result);
}
public Screen? Top()
{
return Screens.Count == 0 ? null : Screens[Screens.Count - 1];
}
public string Names()
{
return "[" + string.Join(", ", Screens.Select(s => s.Name)) + "]";
}
}
class Enemy
{
public string Name;
public int Hp;
public Enemy(string name, int hp)
{
Name = name;
Hp = hp;
}
}
class TargetScreen : Screen
{
ScreenStack stack;
List<Enemy> enemies;
public TargetScreen(ScreenStack stack, List<Enemy> enemies) : base("Target")
{
this.stack = stack;
this.enemies = enemies;
}
public override void Draw()
{
string line = "Who do you hit?";
for (int i = 0; i < enemies.Count; i++)
{
line += " " + (i + 1) + ") " + enemies[i].Name;
}
Console.WriteLine(line + " x) back");
}
public override void Handle(string command)
{
if (command == "x")
{
stack.Pop(null);
return;
}
int choice = int.Parse(command);
if (choice >= 1 && choice <= enemies.Count) stack.Pop(choice - 1);
}
}
class FightScreen : Screen
{
ScreenStack stack;
List<Enemy> enemies = new List<Enemy>();
public FightScreen(ScreenStack stack) : base("Fight")
{
this.stack = stack;
enemies.Add(new Enemy("Wolf", 4));
enemies.Add(new Enemy("Goblin", 7));
}
public override void Draw()
{
string line = "Fight:";
foreach (Enemy enemy in enemies) line += " " + enemy.Name + " " + enemy.Hp;
Console.WriteLine(line + " | 1) attack");
}
public override void Handle(string command)
{
if (command == "1") stack.Push(new TargetScreen(stack, enemies));
}
public override void OnResult(object? result)
{
if (result == null)
{
Console.WriteLine("You change your mind. Nobody is hit.");
return;
}
Enemy enemy = enemies[(int)result];
enemy.Hp -= 4;
if (enemy.Hp > 0)
{
Console.WriteLine("You hit " + enemy.Name + " for 4.");
return;
}
Console.WriteLine(enemy.Name + " falls.");
enemies.Remove(enemy);
if (enemies.Count == 0) stack.Pop("victory");
}
}
class ConfirmScreen : Screen
{
ScreenStack stack;
string question;
public ConfirmScreen(ScreenStack stack, string question) : base(question)
{
this.stack = stack;
this.question = question;
}
public override void Draw()
{
Console.WriteLine(question + " 1) yes 2) no");
}
public override void Handle(string command)
{
if (command == "1") stack.Pop(true);
else if (command == "2") stack.Pop(false);
}
}
class MapScreen : Screen
{
ScreenStack stack;
int potions = 2;
string pending = "";
public MapScreen(ScreenStack stack) : base("Map")
{
this.stack = stack;
}
public override void Draw()
{
Console.WriteLine("Map: potions " + potions + " | f) fight d) drop a potion q) quit");
}
public override void Handle(string command)
{
if (command == "f")
{
stack.Push(new FightScreen(stack));
}
else if (command == "d")
{
pending = "drop";
stack.Push(new ConfirmScreen(stack, "Are you sure you want to drop a potion?"));
}
else if (command == "q")
{
pending = "quit";
stack.Push(new ConfirmScreen(stack, "Are you sure you want to quit?"));
}
}
public override void OnResult(object? result)
{
string asked = pending;
pending = "";
if (asked == "drop")
{
if (result is true)
{
potions--;
Console.WriteLine("You drop a potion.");
}
else
{
Console.WriteLine("The potion stays.");
}
}
else if (asked == "quit")
{
if (result is true) stack.Pop(null);
}
else if (result is "victory")
{
Console.WriteLine("Victory! Back to the map.");
}
}
}
class Program
{
static void Main()
{
ScreenStack stack = new ScreenStack();
stack.Push(new MapScreen(stack));
while (stack.Top() != null)
{
stack.Top()!.Draw();
string command = Console.ReadLine()!.Trim();
Console.WriteLine("> " + command);
stack.Top()!.Handle(command);
}
Console.WriteLine("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 : Screen
{
ScreenStack stack;
string question;
public ConfirmScreen(ScreenStack stack, string question) : base(question)
{
this.stack = stack;
this.question = question;
}
public override void Draw()
{
Console.WriteLine(question + " 1) yes 2) no");
}
public override void Handle(string command)
{
if (command == "1") stack.Pop(true);
else if (command == "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 : Screen
{
ScreenStack stack;
int potions = 2;
string pending = "";
public MapScreen(ScreenStack stack) : base("Map")
{
this.stack = stack;
}
public override void Draw()
{
Console.WriteLine("Map: potions " + potions + " | f) fight d) drop a potion q) quit");
}
public override void Handle(string command)
{
if (command == "f")
{
stack.Push(new FightScreen(stack));
}
else if (command == "d")
{
pending = "drop";
stack.Push(new ConfirmScreen(stack, "Are you sure you want to drop a potion?"));
}
else if (command == "q")
{
pending = "quit";
stack.Push(new ConfirmScreen(stack, "Are you sure you want to quit?"));
}
}
public override void OnResult(object? result)
{
string asked = pending;
pending = "";
if (asked == "drop")
{
if (result is true)
{
potions--;
Console.WriteLine("You drop a potion.");
}
else
{
Console.WriteLine("The potion stays.");
}
}
else if (asked == "quit")
{
if (result is true) stack.Pop(null);
}
else if (result is "victory")
{
Console.WriteLine("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 : Screen
{
ScreenStack stack;
List<string> enemies;
public TargetScreen(ScreenStack stack, List<string> enemies) : base("Target")
{
this.stack = stack;
this.enemies = enemies;
}
public override void Handle(string command)
{
int index = int.Parse(command) - 1;
Console.WriteLine("Target takes " + command + " and returns " + index);
stack.Pop(index);
}
public override void Update()
{
Console.WriteLine("Target updates, enemies in the list: " + enemies.Count);
}
}

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();
class Screen
{
public string Name;
public Screen(string name)
{
Name = name;
}
public virtual void Draw() { }
public virtual void Handle(string command) { }
public virtual void Update() { }
// The answer of the screen that was above this one.
public virtual void OnResult(object? result) { }
}
class ScreenStack
{
public List<Screen> Screens = new List<Screen>();
public void Push(Screen screen)
{
Screens.Add(screen);
}
public void Pop(object? result)
{
Screens.RemoveAt(Screens.Count - 1);
Screen? below = Top();
if (below != null) below.OnResult(result);
}
public Screen? Top()
{
return Screens.Count == 0 ? null : Screens[Screens.Count - 1];
}
public string Names()
{
return "[" + string.Join(", ", Screens.Select(s => s.Name)) + "]";
}
}
class TargetScreen : Screen
{
ScreenStack stack;
List<string> enemies;
public TargetScreen(ScreenStack stack, List<string> enemies) : base("Target")
{
this.stack = stack;
this.enemies = enemies;
}
public override void Handle(string command)
{
int index = int.Parse(command) - 1;
Console.WriteLine("Target takes " + command + " and returns " + index);
stack.Pop(index);
}
public override void Update()
{
Console.WriteLine("Target updates, enemies in the list: " + enemies.Count);
}
}
class FightScreen : Screen
{
ScreenStack stack;
List<string> enemies = new List<string> { "Wolf", "Goblin" };
public FightScreen(ScreenStack stack) : base("Fight")
{
this.stack = stack;
}
public override void Handle(string command)
{
Console.WriteLine("Fight takes " + command + " and opens Target");
stack.Push(new TargetScreen(stack, enemies));
}
public override void OnResult(object? result)
{
string enemy = enemies[(int)result!];
enemies.RemoveAt((int)result!);
Console.WriteLine("Fight gets " + result + ": " + enemy + " falls");
if (enemies.Count == 0)
{
Console.WriteLine("Fight: no enemies left, returns victory");
stack.Pop("victory");
}
}
}
class MapScreen : Screen
{
ScreenStack stack;
public MapScreen(ScreenStack stack) : base("Map")
{
this.stack = stack;
}
public override void OnResult(object? result)
{
Console.WriteLine("Map gets " + result + ": opens Level up");
stack.Push(new Screen("Level up"));
}
}
class Program
{
static void Main()
{
ScreenStack stack = new ScreenStack();
stack.Push(new MapScreen(stack));
stack.Push(new FightScreen(stack));
foreach (string command in new[] { "1", "2", "1", "1" })
{
Console.WriteLine("> " + command);
Screen top = stack.Top()!;
top.Handle(command);
top.Update();
}
Console.WriteLine(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 List<string> { "Map", "Message", "Fight", "Target" };
try
{
foreach (string screen in screens)
{
Console.WriteLine("update " + screen);
if (screen == "Message") screens.Remove(screen);
}
}
catch (InvalidOperationException)
{
Console.WriteLine("InvalidOperationException");
}
Output
update Map
update Message
InvalidOperationException
C# notices the change and throws InvalidOperationException ("Collection was modified") on the next step of foreach.

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
{
public string Kind;
public Screen? Screen;
public object? Result;
public Change(string kind, Screen? screen, object? result)
{
Kind = kind;
Screen = screen;
Result = result;
}
}
class ScreenStack
{
public List<Screen> Screens = new List<Screen>();
public List<Change> Changes = new List<Change>();
public void Push(Screen screen)
{
Changes.Add(new Change("push", screen, null));
}
public void Pop(object? result)
{
Changes.Add(new Change("pop", null, result));
}
public Screen? Top()
{
return Screens.Count == 0 ? null : Screens[Screens.Count - 1];
}
public void Apply()
{
int index = 0;
while (index < Changes.Count)
{
Change change = Changes[index];
index++;
if (change.Kind == "push")
{
Screens.Add(change.Screen!);
}
else if (change.Kind == "pop")
{
Screens.RemoveAt(Screens.Count - 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:

class Screen
{
public string Name;
public Screen(string name)
{
Name = name;
}
public virtual void Draw() { }
public virtual void Handle(string command) { }
public virtual void Update() { }
// The answer of the screen that was above this one.
public virtual void OnResult(object? result) { }
}
class Change
{
public string Kind;
public Screen? Screen;
public object? Result;
public Change(string kind, Screen? screen, object? result)
{
Kind = kind;
Screen = screen;
Result = result;
}
}
class ScreenStack
{
public List<Screen> Screens = new List<Screen>();
public List<Change> Changes = new List<Change>();
public void Push(Screen screen)
{
Changes.Add(new Change("push", screen, null));
}
public void Pop(object? result)
{
Changes.Add(new Change("pop", null, result));
}
public Screen? Top()
{
return Screens.Count == 0 ? null : Screens[Screens.Count - 1];
}
public void Apply()
{
int index = 0;
while (index < Changes.Count)
{
Change change = Changes[index];
index++;
if (change.Kind == "push")
{
Screens.Add(change.Screen!);
}
else if (change.Kind == "pop")
{
Screens.RemoveAt(Screens.Count - 1);
Screen? below = Top();
if (below != null) below.OnResult(change.Result);
}
}
Changes.Clear();
}
public string Names()
{
return "[" + string.Join(", ", Screens.Select(s => s.Name)) + "]";
}
}
class TargetScreen : Screen
{
ScreenStack stack;
List<string> enemies;
public TargetScreen(ScreenStack stack, List<string> enemies) : base("Target")
{
this.stack = stack;
this.enemies = enemies;
}
public override void Handle(string command)
{
int index = int.Parse(command) - 1;
Console.WriteLine("Target takes " + command + " and returns " + index);
stack.Pop(index);
}
public override void Update()
{
Console.WriteLine("Target updates, enemies in the list: " + enemies.Count);
}
}
class FightScreen : Screen
{
ScreenStack stack;
List<string> enemies = new List<string> { "Wolf", "Goblin" };
public FightScreen(ScreenStack stack) : base("Fight")
{
this.stack = stack;
}
public override void Handle(string command)
{
Console.WriteLine("Fight takes " + command + " and opens Target");
stack.Push(new TargetScreen(stack, enemies));
}
public override void OnResult(object? result)
{
string enemy = enemies[(int)result!];
enemies.RemoveAt((int)result!);
Console.WriteLine("Fight gets " + result + ": " + enemy + " falls");
if (enemies.Count == 0)
{
Console.WriteLine("Fight: no enemies left, returns victory");
stack.Pop("victory");
}
}
}
class MapScreen : Screen
{
ScreenStack stack;
public MapScreen(ScreenStack stack) : base("Map")
{
this.stack = stack;
}
public override void OnResult(object? result)
{
Console.WriteLine("Map gets " + result + ": opens Level up");
stack.Push(new Screen("Level up"));
}
}
class Program
{
static void Main()
{
ScreenStack stack = new ScreenStack();
stack.Push(new MapScreen(stack));
stack.Push(new FightScreen(stack));
stack.Apply();
foreach (string command in new[] { "1", "2", "1", "1" })
{
Console.WriteLine("> " + command);
Screen top = stack.Top()!;
top.Handle(command);
top.Update();
stack.Apply();
}
Console.WriteLine(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
{
public List<Screen> Screens = new List<Screen>();
public List<Change> Changes = new List<Change>();
public void Push(Screen screen)
{
Changes.Add(new Change("push", screen, null));
}
public void Pop(object? result)
{
Changes.Add(new Change("pop", null, result));
}
public Screen? Top()
{
return Screens.Count == 0 ? null : Screens[Screens.Count - 1];
}
public void Apply()
{
int index = 0;
while (index < Changes.Count)
{
Change change = Changes[index];
index++;
if (change.Kind == "push")
{
Screens.Add(change.Screen!);
}
else if (change.Kind == "pop")
{
Screens.RemoveAt(Screens.Count - 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?

public void Apply()
{
    int index = 0;
    while (index < )
    {
        Change change = Changes[index];
        index++;
        if (change.Kind == "push")
        {
            Screens.Add(change.Screen!);
        }
        else if (change.Kind == "pop")
        {
            Screens.RemoveAt(Screens.Count - 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"));
Console.WriteLine("On top: " + stack.Top()!.Name);
stack.Apply();
Console.WriteLine("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:

public void Replace(Screen screen)
{
Changes.Add(new Change("replace", screen, null));
}
public void Reset(Screen? screen)
{
Changes.Add(new Change("reset", screen, null));
}

apply has one branch for each of them:

else if (change.Kind == "replace")
{
Screens.RemoveAt(Screens.Count - 1);
Screens.Add(change.Screen!);
}
else if (change.Kind == "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();
Console.WriteLine(stack.Names());
stack.Replace(new NamedScreen("New hero"));
stack.Apply();
Console.WriteLine(stack.Names());
stack.Replace(new NamedScreen("Village"));
stack.Apply();
Console.WriteLine(stack.Names());
Console.WriteLine("The hero falls. With pop:");
ScreenStack popped = InFight();
popped.Pop("defeat");
popped.Pop("defeat");
popped.Push(new NamedScreen("Game over"));
popped.Apply();
Console.WriteLine(popped.Names());
Console.WriteLine("The hero falls. With reset:");
ScreenStack clean = InFight();
clean.Reset(new NamedScreen("Game over"));
clean.Apply();
Console.WriteLine(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:

class Screen
{
public string Name;
public Screen(string name)
{
Name = name;
}
public virtual void Draw() { }
public virtual void Handle(string command) { }
public virtual void Update() { }
// The answer of the screen that was above this one.
public virtual void OnResult(object? result) { }
}
class Change
{
public string Kind;
public Screen? Screen;
public object? Result;
public Change(string kind, Screen? screen, object? result)
{
Kind = kind;
Screen = screen;
Result = result;
}
}
class ScreenStack
{
public List<Screen> Screens = new List<Screen>();
public List<Change> Changes = new List<Change>();
public void Push(Screen screen)
{
Changes.Add(new Change("push", screen, null));
}
public void Pop(object? result)
{
Changes.Add(new Change("pop", null, result));
}
public void Replace(Screen screen)
{
Changes.Add(new Change("replace", screen, null));
}
public void Reset(Screen? screen)
{
Changes.Add(new Change("reset", screen, null));
}
public Screen? Top()
{
return Screens.Count == 0 ? null : Screens[Screens.Count - 1];
}
public void Apply()
{
int index = 0;
while (index < Changes.Count)
{
Change change = Changes[index];
index++;
if (change.Kind == "push")
{
Screens.Add(change.Screen!);
}
else if (change.Kind == "pop")
{
Screens.RemoveAt(Screens.Count - 1);
Screen? below = Top();
if (below != null) below.OnResult(change.Result);
}
else if (change.Kind == "replace")
{
Screens.RemoveAt(Screens.Count - 1);
Screens.Add(change.Screen!);
}
else if (change.Kind == "reset")
{
Screens.Clear();
if (change.Screen != null) Screens.Add(change.Screen);
}
}
Changes.Clear();
}
public string Names()
{
return "[" + string.Join(", ", Screens.Select(s => s.Name)) + "]";
}
}
class NamedScreen : Screen
{
public NamedScreen(string name) : base(name) { }
public override void OnResult(object? result)
{
Console.WriteLine(Name + " gets " + result);
}
}
class Program
{
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;
}
static void Main()
{
ScreenStack stack = new ScreenStack();
stack.Push(new NamedScreen("Title"));
stack.Apply();
Console.WriteLine(stack.Names());
stack.Replace(new NamedScreen("New hero"));
stack.Apply();
Console.WriteLine(stack.Names());
stack.Replace(new NamedScreen("Village"));
stack.Apply();
Console.WriteLine(stack.Names());
Console.WriteLine("The hero falls. With pop:");
ScreenStack popped = InFight();
popped.Pop("defeat");
popped.Pop("defeat");
popped.Push(new NamedScreen("Game over"));
popped.Apply();
Console.WriteLine(popped.Names());
Console.WriteLine("The hero falls. With reset:");
ScreenStack clean = InFight();
clean.Reset(new NamedScreen("Game over"));
clean.Apply();
Console.WriteLine(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.
class ScreenChange
{
public string Kind = "";
public Screen? Screen;
public object? Result;
}
class ScreenStack
{
public List<Screen> Screens = new List<Screen>();
public List<ScreenChange> Changes = new List<ScreenChange>();
public void Push(Screen screen)
{
Changes.Add(new ScreenChange { Kind = "push", Screen = screen, Result = null });
}
public void Pop(object? result)
{
Changes.Add(new ScreenChange { Kind = "pop", Screen = null, Result = result });
}
public void Replace(Screen screen)
{
Changes.Add(new ScreenChange { Kind = "replace", Screen = screen, Result = null });
}
// Clears the whole stack (loading, game over); with null the stack stays empty.
public void Reset(Screen? screen)
{
Changes.Add(new ScreenChange { Kind = "reset", Screen = screen, Result = null });
}
public Screen? Top()
{
return Screens.Count == 0 ? null : Screens[Screens.Count - 1];
}
public bool IsEmpty()
{
return Screens.Count == 0 && Changes.Count == 0;
}
public bool Contains(string kindName)
{
foreach (Screen screen in Screens)
{
if (screen.KindName == kindName) return true;
}
return false;
}
public void Apply()
{
int index = 0;
while (index < Changes.Count)
{
ScreenChange change = Changes[index];
index++;
if (change.Kind == "push")
{
Screens.Add(change.Screen!);
}
else if (change.Kind == "pop")
{
Screens.RemoveAt(Screens.Count - 1);
Screen? below = Top();
if (below != null) below.OnResult(change.Result);
}
else if (change.Kind == "replace")
{
Screens.RemoveAt(Screens.Count - 1);
Screens.Add(change.Screen!);
}
else
{
Screens = new List<Screen>();
if (change.Screen != null) Screens.Add(change.Screen);
}
}
Changes = new List<ScreenChange>();
}
// Drawing starts from the topmost opaque screen and goes up.
public void Draw(CellBuffer buffer)
{
int first = Screens.Count - 1;
while (first > 0 && Screens[first].IsTransparent()) first--;
for (int i = first; i < Screens.Count; i++)
{
if (i >= 0) Screens[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.
class TargetScreen : Screen
{
public Battle Battle;
public string Note;
public TargetScreen(Game game, Battle battle) : base(game)
{
Battle = battle;
Note = "";
}
public override bool IsTransparent()
{
return true;
}
public override void Draw(CellBuffer buffer)
{
List<Enemy> enemies = Battle.AliveEnemies();
buffer.Box(24, 9, 36, enemies.Count + 4, T("target.title"));
for (int i = 0; i < enemies.Count; i++)
{
buffer.Text(26, 10 + i, (i + 1) + ") " + Pad(enemies[i].Name, 22) + " " + enemies[i].Hp + "/" + enemies[i].MaxHp(), enemies[i].Kind.Color);
}
buffer.Text(26, 11 + enemies.Count, Note.Length > 0 ? Note : T("target.help"), "gray");
}
public override void Handle(string command)
{
if (command == "x")
{
Game.Stack.Pop(null);
return;
}
long? choice = ParseIntStrict(command);
if (choice == null || choice < 1 || choice > Battle.AliveEnemies().Count)
{
Note = T("ui.unknown", ("command", command));
return;
}
Game.Stack.Pop((int)choice.Value - 1);
}
}
// A general choice from a list (skill, potion, slot): returns a 0-based number or null.
class ChoiceScreen : Screen
{
public string Title;
public List<string> Labels;
public string Note;
public ChoiceScreen(Game game, string title, List<string> labels) : base(game)
{
Title = title;
Labels = labels;
Note = "";
}
public override bool IsTransparent()
{
return true;
}
public override void Draw(CellBuffer buffer)
{
buffer.Box(4, 3, 52, Labels.Count + 4, Title);
for (int i = 0; i < Labels.Count; i++) buffer.Text(6, 4 + i, (i + 1) + ") " + Labels[i], "default");
buffer.Text(6, 5 + Labels.Count, Note.Length > 0 ? Note : T("choice.help"), "gray");
}
public override void Handle(string command)
{
if (command == "x")
{
Game.Stack.Pop(null);
return;
}
long? choice = ParseIntStrict(command);
if (choice == null || choice < 1 || choice > Labels.Count)
{
Note = T("ui.unknown", ("command", command));
return;
}
Game.Stack.Pop((int)choice.Value - 1);
}
}
class SlotScreen : ChoiceScreen
{
// C# calls the base class constructor before the body, so the labels are computed in a separate method.
public SlotScreen(Game game, string mode) : base(game, T(mode == "save" ? "slot.saveTitle" : "slot.loadTitle"), SlotLabels(game)) { }
static List<string> SlotLabels(Game game)
{
List<string> labels = new List<string>();
foreach (string slot in SAVE_SLOTS) labels.Add(T("slot.label", ("slot", slot), ("summary", SlotSummary(game.Io, slot))));
return labels;
}
public override void Handle(string command)
{
long? choice = ParseIntStrict(command);
if (choice != null && choice >= 1 && choice <= SAVE_SLOTS.Length)
{
Game.Stack.Pop(SAVE_SLOTS[(int)choice.Value - 1]);
return;
}
base.Handle(command);
}
}

The fight remembers what it asked (pending) and reads the answer in OnResult:

public override void OnResult(object? result)
{
Battle battle = Battle;
Hero hero = battle.Hero;
PendingChoice? pending = Pending;
Pending = null;
if (result == null || pending == null) return;
int index = (int)result;
if (pending.Kind == "attack")
{
battle.HeroAct(new AttackAction(hero, battle.AliveEnemies()[index]));
}
else if (pending.Kind == "skill")
{
Skill skill = SKILLS.Get(hero.Skills[index]);
if (hero.Mana < skill.Mana)
{
Note = T("battle.noMana", ("skill", Nm(skill)));
}
else if (hero.CooldownOf(skill.Id) > 0)
{
Note = T("battle.cooldown", ("skill", Nm(skill)), ("turns", hero.CooldownOf(skill.Id)));
}
else if (skill.Target == "one" && battle.AliveEnemies().Count > 1)
{
Pending = new PendingChoice { Kind = "skillTarget", Skill = skill };
Game.Stack.Push(new TargetScreen(Game, battle));
}
else
{
battle.HeroAct(new SkillAction(hero, skill, battle.AliveEnemies()[0]));
}
}
else if (pending.Kind == "skillTarget")
{
battle.HeroAct(new SkillAction(hero, pending.Skill!, battle.AliveEnemies()[index]));
}
else
{
battle.HeroAct(new ItemAction(hero, PotionIds()[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.

class ConfirmScreen : Screen
{
public string Question;
public ConfirmScreen(Game game, string question) : base(game)
{
Question = question;
}
public override bool IsTransparent()
{
return true;
}
public override void Draw(CellBuffer buffer)
{
buffer.Box(16, 6, 28, 5, "");
buffer.Text(18, 7, Question, "white");
buffer.Text(18, 8, "1) " + T("ui.yes") + " 2) " + T("ui.no"), "default");
}
public override void Handle(string command)
{
if (command == "1") Game.Stack.Pop(true);
else if (command == "2" || command == "x") Game.Stack.Pop(false);
}
}
public override void OnResult(object? result)
{
Game game = Game;
string pending = Pending;
Pending = "";
if (result == null) return;
if (pending == "save")
{
game.Io.Save((string)result, SaveGame(game));
Note = T("menu.saved", ("slot", result));
}
else if (pending == "load")
{
string? error = LoadGame(game, game.Io.Load((string)result));
if (error != null) Note = error;
else EnterWorld(game, T("load.done", ("slot", result)));
}
else if (pending == "quit" && result is bool yes && yes)
{
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
{
public List<Screen> Screens = new List<Screen>();
public List<Change> Changes = new List<Change>();
public void Push(Screen screen)
{
Changes.Add(new Change("push", screen, null));
}
public void Pop(object? result)
{
Changes.Add(new Change("pop", null, result));
}
public Screen? Top()
{
return Screens.Count == 0 ? null : Screens[Screens.Count - 1];
}
public void Apply()
{
int index = 0;
while (index < Changes.Count)
{
Change change = Changes[index];
index++;
if (change.Kind == "push")
{
Screens.Add(change.Screen!);
}
else if (change.Kind == "pop")
{
Screen? below = Top();
Screens.RemoveAt(Screens.Count - 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 ·