Skip to content
wannacode

A stack of screens

In short: A stack of screens keeps the open screens on top of each other: input goes to the top one, push opens a new one, pop closes it, replace swaps it, and transparent ones are drawn over those below.

Problem

One variable for all screens

The game has a title screen, a village, the map, a battle, an inventory that opens over the map, and a menu that opens over the battle. Each screen reads commands its own way: on the map a is a step to the left, in the battle it is a hit, in the inventory it is nothing.

The first attempt almost everyone makes: one variable with the name of the screen, and an if on it. Once when handling the command and once more when drawing:

string screen = "title";
int wolf = 2;
string? line;
while ((line = Console.ReadLine()) != null)
{
string command = line.Trim();
if (screen == "title")
{
if (command == "n") screen = "map";
}
else if (screen == "map")
{
if (command == "i") screen = "inventory";
if (command == "f") screen = "battle";
}
else if (screen == "battle")
{
if (command == "i") screen = "inventory";
if (command == "a")
{
wolf--;
if (wolf == 0) screen = "map";
}
}
else if (screen == "inventory")
{
if (command == "x") screen = "map";
}
Console.Write(command + " → ");
if (screen == "title") Console.WriteLine("Chronicles of Arcanum");
else if (screen == "map") Console.WriteLine("Map, wolf: " + wolf);
else if (screen == "battle") Console.WriteLine("Battle, wolf: " + wolf);
else if (screen == "inventory") Console.WriteLine("Inventory: Potion x2");
}
Output
n → Map, wolf: 2
i → Inventory: Potion x2
x → Map, wolf: 2
f → Battle, wolf: 2
i → Inventory: Potion x2
x → Map, wolf: 2
a → Map, wolf: 2
a → Map, wolf: 2

The commands come one per line: n new game, i inventory, x close, f fight the wolf, a hit.

Input
n
i
x
f
i
x
a
a

Look at the sixth line. The inventory was opened from the battle, and x closes it to the map. The battle is gone, the wolf is alive with 2 life, and the two hits after that land on the map, where a means nothing. The variable remembers where you are, but not where you came from.

The second problem shows up as the game grows. Every new screen is a new branch in every if: in the handling, in the drawing, and in every screen that opens it. A menu over the battle and over the map is two more branches in two places. Forget one, and you get a screen that takes commands but is never drawn.

The idea

A screen decides about itself

Turn the question around. Instead of the loop asking "which screen am I on" and knowing every screen, every screen is an object with the same four methods:

  • Handle: what the command means here;
  • Update: what happens every turn while the screen is on top (in the battle, the wolf bites);
  • Draw: what the screen draws;
  • IsTransparent: whether the screen below it shows through.
abstract class Screen
{
public Game Game;
public string Name;
public Screen(Game game, string name)
{
Game = game;
Name = name;
}
public virtual bool IsTransparent()
{
return false;
}
public abstract void Handle(string command);
public virtual void Update() { }
public abstract void Draw(TextBuffer buffer);
}
abstract says that every screen must have its own Handle and its own Draw: without them the class does not compile. Update and IsTransparent have a default behavior, and a screen writes its own only if it needs to.

Here are the battle and the inventory. The battle knows only about itself: i opens the inventory on top, a hits, and when the wolf falls, the battle removes itself. The inventory does not know where it was opened from. All it says is "remove me":

class BattleScreen : Screen
{
public BattleScreen(Game game) : base(game, "Battle") { }
public override void Handle(string command)
{
if (command == "i") Game.Stack.Push(new InventoryScreen(Game));
if (command == "a")
{
Game.Wolf--;
if (Game.Wolf == 0) Game.Stack.Pop();
}
}
// Every turn of the battle, the wolf bites.
public override void Update()
{
if (Game.Wolf > 0) Game.Life--;
}
public override void Draw(TextBuffer buffer)
{
buffer.Box(0, 0, TextBuffer.W, TextBuffer.H);
buffer.Write(2, 1, "Wolf: " + Game.Wolf);
buffer.Write(2, 2, "Life: " + Game.Life);
buffer.Write(2, 3, "a: hit i: inventory");
}
}
class InventoryScreen : Screen
{
public InventoryScreen(Game game) : base(game, "Inventory") { }
public override void Handle(string command)
{
if (command == "x") Game.Stack.Pop();
}
public override void Draw(TextBuffer buffer)
{
buffer.Box(12, 1, 11, 3);
buffer.Write(13, 2, "Potion x2");
}
}

The screens draw into a TextBuffer: five rows of 24 characters. Write puts text at a given place, Box draws a frame. The whole buffer is in the program below, and how drawing works in a real terminal is the lesson on drawing.

A new screen is a new class and one line in the screen that opens it. No if on screen names grows.

More precisely, if you are curious

This is how it looks in the game. The screen region of the canon:

// Each screen is an object with the same interface. The game does not ask "which screen am I on"; it sends the command to
// the screen on top of the stack.
abstract class Screen
{
public Game Game;
public string KindName = "";
public Screen(Game game)
{
Game = game;
}
// A transparent screen (menu, inventory, dialog) lets the screen below it show through.
public virtual bool IsTransparent()
{
return false;
}
public virtual void Handle(string command) { }
public virtual void Update() { }
public virtual void Draw(CellBuffer buffer) { }
// The screen below the popped one gets its answer (the chosen target, "yes" or "no", the chosen slot).
public virtual void OnResult(object? result) { }
}

The OnResult method is for screens that send back an answer (the chosen target, "yes" or "no"). It is the topic of the next lesson.

The idea

The stack: where you came from

Screens sit on top of each other: the inventory over the battle, the battle over the map. When you close the top one, you want to be exactly where you were. That is a stack: the last one in is the first one out. The stack of screens has three operations:

  • push: open a new screen on top (the map opens the battle, the battle opens the inventory);
  • pop: close the top one and return to the one below it;
  • replace: swap the top one for a new one, with no way back (the title screen becomes the map, hero creation becomes the village).
class ScreenStack
{
public List<Screen> Screens = new List<Screen>();
public void Push(Screen screen)
{
Screens.Add(screen);
}
public void Pop()
{
Screens.RemoveAt(Screens.Count - 1);
}
public void Replace(Screen screen)
{
Pop();
Push(screen);
}
public Screen Top()
{
return Screens[Screens.Count - 1];
}
public string Names()
{
var names = new List<string>();
foreach (Screen screen in Screens) names.Add(screen.Name);
return "[" + string.Join(", ", names) + "]";
}
public void Draw(TextBuffer buffer)
{
Top().Draw(buffer);
}
}

The game loop gives the command only to the top. Not to the map, not to the battle under the inventory. Only the top knows what the command means right now. The same commands, now with the stack. Show prints the stack and the frame after every command:

static void Show(Game game, TextBuffer buffer)
{
Console.WriteLine(game.Stack.Names());
buffer.Clear();
game.Stack.Draw(buffer);
buffer.Print();
}
var game = new Game();
var buffer = new TextBuffer();
game.Stack.Push(new TitleScreen(game));
Show(game, buffer);
string? line;
while ((line = Console.ReadLine()) != null)
{
string command = line.Trim();
Console.WriteLine("> " + command);
Screen top = game.Stack.Top();
top.Handle(command);
top.Update();
Show(game, buffer);
}
Output
[Title]
Chronicles of Arcanum
n: new game
> n
[Map]
########################
#..@......~~~..........#
#.......w.~~~..........#
########################
Life: 10
> i
[Map, Inventory]
+---------+
|Potion x2|
+---------+
> x
[Map]
########################
#..@......~~~..........#
#.......w.~~~..........#
########################
Life: 10
> f
[Map, Battle]
+----------------------+
| Wolf: 2 |
| Life: 10 |
| a: hit i: inventory |
+----------------------+
> i
[Map, Battle, Inventory]
+---------+
|Potion x2|
+---------+
> x
[Map, Battle]
+----------------------+
| Wolf: 2 |
| Life: 9 |
| a: hit i: inventory |
+----------------------+
> a
[Map, Battle]
+----------------------+
| Wolf: 1 |
| Life: 8 |
| a: hit i: inventory |
+----------------------+
> a
[Map]
########################
#..@......~~~..........#
#.........~~~..........#
########################
Life: 8

The inventory opened from the battle now closes back to the battle: [Map, Battle]. The wolf falls on the second hit, the battle removes itself, and below it is the map, now without the w. No screen remembers where it came from. The stack does.

But look at the frames with the inventory. The map has disappeared, and the frame hangs in empty space. The stack's Draw draws only the top. That is next.

More precisely, if you are curious

Only the top is updated. The loop calls Update on the top. While the inventory is open over the battle, the battle waits and the wolf does not bite: the game is paused under the inventory. That is a choice, not a law. In a real-time game the world under the menu can keep going, and then the stack also updates screens below the top that say they do not stop.

pop on an empty stack. If the last screen removes itself, the stack is empty and the next pop has nothing to remove. In C#, RemoveAt(-1) throws ArgumentOutOfRangeException. In the game an empty stack means the end: the loop runs until the stack is empty, and "Quit" on the title screen clears the stack.

Try it

Predict the output

The stack is empty. What is on it at the end?

var game = new Game();
ScreenStack stack = game.Stack;
stack.Push(new TitleScreen(game));
stack.Replace(new MapScreen(game));
stack.Push(new BattleScreen(game));
stack.Push(new InventoryScreen(game));
stack.Pop();
stack.Pop();
stack.Push(new InventoryScreen(game));
Console.WriteLine(stack.Names());
Answer

The idea

Transparent screens

The inventory is a small frame over the map. For the map to show around it, the map has to be drawn first, and the inventory after it, on top of it. Like a painter who paints the background first, then the figures in front.

But not from the bottom screen. Under the battle is the map, and the battle covers all of it: drawing it is wasted work. So drawing starts at the topmost opaque screen. Go down from the top while the screens are transparent. The first one that is not transparent is the start. From there, draw upward:

public void Draw(TextBuffer buffer)
{
int first = Screens.Count - 1;
while (first > 0 && Screens[first].IsTransparent()) first--;
for (int i = first; i < Screens.Count; i++)
{
Screens[i].Draw(buffer);
}
}

The inventory says it is transparent:

class InventoryScreen : Screen
{
public InventoryScreen(Game game) : base(game, "Inventory") { }
public override bool IsTransparent()
{
return true;
}
public override void Handle(string command)
{
if (command == "x") Game.Stack.Pop();
}
public override void Draw(TextBuffer buffer)
{
buffer.Box(12, 1, 11, 3);
buffer.Write(13, 2, "Potion x2");
}
}

Only these two changes. The same loop, the same commands:

Output
[Title]
Chronicles of Arcanum
n: new game
> n
[Map]
########################
#..@......~~~..........#
#.......w.~~~..........#
########################
Life: 10
> i
[Map, Inventory]
########################
#..@......~~+---------+#
#.......w.~~|Potion x2|#
############+---------+#
Life: 10
> x
[Map]
########################
#..@......~~~..........#
#.......w.~~~..........#
########################
Life: 10
> f
[Map, Battle]
+----------------------+
| Wolf: 2 |
| Life: 10 |
| a: hit i: inventory |
+----------------------+
> i
[Map, Battle, Inventory]
+----------------------+
| Wolf: 2 +---------+|
| Life: 9 |Potion x2||
| a: hit i:+---------+|
+----------------------+
> x
[Map, Battle]
+----------------------+
| Wolf: 2 |
| Life: 9 |
| a: hit i: inventory |
+----------------------+
> a
[Map, Battle]
+----------------------+
| Wolf: 1 |
| Life: 8 |
| a: hit i: inventory |
+----------------------+
> a
[Map]
########################
#..@......~~~..........#
#.........~~~..........#
########################
Life: 8

[Map, Inventory]: the map, with the frame over it, and the wolf still shows underneath. [Map, Battle, Inventory]: drawing starts at the battle, because it is not transparent. The map under it is not drawn at all.

first > 0 stops at the bottom screen: it is always drawn, even if it is transparent, because there is nothing under it.

More precisely, if you are curious

The Draw method from the screen-stack region of the canon:

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

The i >= 0 check is for an empty stack: then first is −1 and there is nothing to draw.

Why not simply from zero? If every opaque screen covers everything, the result is the same, but every frame draws all the screens below: map, battle, target choice, dialog. There is also something worse: an opaque screen that does not write every cell lets pieces of the map show through. "I am not transparent" is a promise that says "I cover everything", and the stack relies on it.

See it

Turn by turn

The same program with the transparent inventory. On the left is the stack: each plate is one screen with what it draws on its own. The command goes to the plate on top. The faded plates are under the topmost opaque one and are not drawn. Under the stack is the frame the program prints.

class ScreenStack
{
public List<Screen> Screens = new List<Screen>();
public void Push(Screen screen)
{
Screens.Add(screen);
}
public void Pop()
{
Screens.RemoveAt(Screens.Count - 1);
}
public void Replace(Screen screen)
{
Pop();
Push(screen);
}
public Screen Top()
{
return Screens[Screens.Count - 1];
}
public string Names()
{
var names = new List<string>();
foreach (Screen screen in Screens) names.Add(screen.Name);
return "[" + string.Join(", ", names) + "]";
}
public void Draw(TextBuffer buffer)
{
int first = Screens.Count - 1;
while (first > 0 && Screens[first].IsTransparent()) first--;
for (int i = first; i < Screens.Count; i++)
{
Screens[i].Draw(buffer);
}
}
}
string? line;
while ((line = Console.ReadLine()) != null)
{
string command = line.Trim();
Console.WriteLine("> " + command);
Screen top = game.Stack.Top();
top.Handle(command);
top.Update();
Show(game, buffer);
}
Step by step, in words
  1. The game starts with one screen on the stack: the title screen. It is the top, so it gets the input.
  2. n goes to the title screen. It puts the map in its own place (replace): the title screen is not coming back.
  3. i goes to the map, which puts the inventory on top (push). The inventory is transparent: the map is drawn first, then the inventory over it.
  4. x goes to the inventory: it removes itself (pop). Below it is the map, exactly as you left it.
  5. f: the map puts the battle on top (push). The battle is opaque, so the map under it is not drawn at all.
  6. i now goes to the battle, not to the map. The battle opens the inventory over itself. Drawing starts from the battle, the topmost opaque screen. The wolf bit: life 9.
  7. x closes the inventory, and below it is the battle: you return where you came from. The single variable from the start of the lesson took you to the map here.
  8. a goes to the battle: the wolf is hit, and the stack does not change. The battle's update runs: the wolf bites back, life 8.
  9. The wolf falls. The battle removes itself (pop), and the map is on top again, now without the wolf.
  10. Input goes to the top. Drawing goes up from the topmost opaque screen. Screens that send back an answer, and changes that wait for the end of the turn, are the next lesson.

Try it

Fill in the blank

The stack's drawing. While the screen at first is transparent, go down:

public void Draw(TextBuffer buffer)
{
    int first = Screens.Count - 1;
    while (first > 0 && ) first--;
    for (int i = first; i < Screens.Count; i++)
    {
        Screens[i].Draw(buffer);
    }
}

Try it

Find the bug

A colleague rewrote the stack. The program with the commands n, i, x prints:

Output
[Title]
Chronicles of Arcanum
n: new game
> n
[Map]
########################
#..@......~~~..........#
#.......w.~~~..........#
########################
Life: 10
> i
[Map, Inventory]
########################
#..@......~~+---------+#
#.......w.~~|Potion x2|#
############+---------+#
Life: 10
> x
[Map, Inventory]
########################
#..@......~~+---------+#
#.......w.~~|Potion x2|#
############+---------+#
Life: 10

The inventory does not close: after x the stack is still [Map, Inventory]. Which line has the bug?

Pick the line with the bug.

The idea

The stack in the real game

The screen-stack region of the canon. The methods are the same, with a few differences:

// 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);
}
}
}
  • push, pop and replace do not touch the list right away. They record the change in changes, and Apply() carries them out after the turn. In our program the battle removes itself in Handle, and right after that the loop calls Update on the same battle, which is no longer on the stack. It works here because a dead wolf does not bite. In a bigger game this is a bug that takes a long time to find. Why and how the changes are deferred is the next lesson.
  • pop carries an answer: the screen below gets it in OnResult. That is how the target choice screen returns the target to the battle. That is also in the next lesson.
  • reset clears the whole stack (loading a game, the end of the game), and contains asks whether a screen of a given kind is already on the stack (for example, so that a second level-up screen does not open).

More precisely

The same idea elsewhere

Navigation in mobile apps is a stack of screens. On Android and iOS you open a screen on top (push), and the Back button removes it (pop). Going in after login is usually a replace: Back should not lead to the login form again.

A modal dialog in the browser is a screen over the page. While it is open, only it gets clicks and keys, and the page under it is visible but does not react. The same as the inventory over the map.

The call stack is the same idea in the language itself. A method calls another method (push), that one returns (pop), and the caller continues exactly where it stopped. The battle continues after the inventory in the same way.

When a stack is not enough. A stack says there is exactly one screen on top and it gets everything. Sometimes that is not true:

  • two screens side by side that work together: a map and a mini-map, a battle and the battle log, a two-player game on a split screen. Then the screen is a tree of panels, and input goes to the panel with focus;
  • things that are always on top, whatever the screen: a life bar, a "Game saved" notice. They are a separate layer, drawn after the stack;
  • windows that close in any order (a strategy game with five open windows). That is no longer a stack but a list with focus.

Exercise

A menu over everything

Solve this in your own editor. The whole program with transparent screens is below the task.

Menu. Add a MenuScreen named "Menu". m opens it from the map, from the battle and from the inventory. The menu is transparent, and x closes it. It draws a frame Box(14, 2, 10, 3) with x: back inside it, at (15, 3).

Input
n
f
i
m
x
x
a
a
Output
[Title]
Chronicles of Arcanum
n: new game
> n
[Map]
########################
#..@......~~~..........#
#.......w.~~~..........#
########################
Life: 10
> f
[Map, Battle]
+----------------------+
| Wolf: 2 |
| Life: 10 |
| a: hit i: inventory |
+----------------------+
> i
[Map, Battle, Inventory]
+----------------------+
| Wolf: 2 +---------+|
| Life: 9 |Potion x2||
| a: hit i:+---------+|
+----------------------+
> m
[Map, Battle, Inventory, Menu]
+----------------------+
| Wolf: 2 +---------+|
| Life: 9 |P+--------+
| a: hit i:+-|x: back |
+-------------+--------+
> x
[Map, Battle, Inventory]
+----------------------+
| Wolf: 2 +---------+|
| Life: 9 |Potion x2||
| a: hit i:+---------+|
+----------------------+
> x
[Map, Battle]
+----------------------+
| Wolf: 2 |
| Life: 9 |
| a: hit i: inventory |
+----------------------+
> a
[Map, Battle]
+----------------------+
| Wolf: 1 |
| Life: 8 |
| a: hit i: inventory |
+----------------------+
> a
[Map]
########################
#..@......~~~..........#
#.........~~~..........#
########################
Life: 8

Look at the frame with the menu: the battle, the inventory and the menu are drawn, in that order. The menu covers a corner of the inventory, because it is higher on the stack.

More precisely, if you are curious

The whole program with transparent screens:

var game = new Game();
var buffer = new TextBuffer();
game.Stack.Push(new TitleScreen(game));
Show(game, buffer);
string? line;
while ((line = Console.ReadLine()) != null)
{
string command = line.Trim();
Console.WriteLine("> " + command);
Screen top = game.Stack.Top();
top.Handle(command);
top.Update();
Show(game, buffer);
}
static void Show(Game game, TextBuffer buffer)
{
Console.WriteLine(game.Stack.Names());
buffer.Clear();
game.Stack.Draw(buffer);
buffer.Print();
}
class TextBuffer
{
public const int W = 24;
public const int H = 5;
string[] rows = new string[H];
public void Clear()
{
for (int y = 0; y < H; y++) rows[y] = new string(' ', W);
}
public void Write(int x, int y, string text)
{
rows[y] = rows[y].Substring(0, x) + text + rows[y].Substring(x + text.Length);
}
public void Box(int x, int y, int w, int h)
{
string edge = "+" + new string('-', w - 2) + "+";
Write(x, y, edge);
for (int i = 1; i < h - 1; i++) Write(x, y + i, "|" + new string(' ', w - 2) + "|");
Write(x, y + h - 1, edge);
}
public void Print()
{
foreach (string row in rows) Console.WriteLine(row.TrimEnd());
}
}
class Game
{
public ScreenStack Stack = new ScreenStack();
public int Life = 10;
public int Wolf = 2;
}
abstract class Screen
{
public Game Game;
public string Name;
public Screen(Game game, string name)
{
Game = game;
Name = name;
}
public virtual bool IsTransparent()
{
return false;
}
public abstract void Handle(string command);
public virtual void Update() { }
public abstract void Draw(TextBuffer buffer);
}
class ScreenStack
{
public List<Screen> Screens = new List<Screen>();
public void Push(Screen screen)
{
Screens.Add(screen);
}
public void Pop()
{
Screens.RemoveAt(Screens.Count - 1);
}
public void Replace(Screen screen)
{
Pop();
Push(screen);
}
public Screen Top()
{
return Screens[Screens.Count - 1];
}
public string Names()
{
var names = new List<string>();
foreach (Screen screen in Screens) names.Add(screen.Name);
return "[" + string.Join(", ", names) + "]";
}
public void Draw(TextBuffer buffer)
{
int first = Screens.Count - 1;
while (first > 0 && Screens[first].IsTransparent()) first--;
for (int i = first; i < Screens.Count; i++)
{
Screens[i].Draw(buffer);
}
}
}
class TitleScreen : Screen
{
public TitleScreen(Game game) : base(game, "Title") { }
public override void Handle(string command)
{
if (command == "n") Game.Stack.Replace(new MapScreen(Game));
}
public override void Draw(TextBuffer buffer)
{
buffer.Write(1, 1, "Chronicles of Arcanum");
buffer.Write(1, 3, "n: new game");
}
}
class MapScreen : Screen
{
static readonly string[] Rows =
{
"########################",
"#..@......~~~..........#",
"#.........~~~..........#",
"########################",
};
public MapScreen(Game game) : base(game, "Map") { }
public override void Handle(string command)
{
if (command == "i") Game.Stack.Push(new InventoryScreen(Game));
if (command == "f" && Game.Wolf > 0) Game.Stack.Push(new BattleScreen(Game));
}
public override void Draw(TextBuffer buffer)
{
for (int y = 0; y < Rows.Length; y++) buffer.Write(0, y, Rows[y]);
if (Game.Wolf > 0) buffer.Write(8, 2, "w");
buffer.Write(1, 4, "Life: " + Game.Life);
}
}
class BattleScreen : Screen
{
public BattleScreen(Game game) : base(game, "Battle") { }
public override void Handle(string command)
{
if (command == "i") Game.Stack.Push(new InventoryScreen(Game));
if (command == "a")
{
Game.Wolf--;
if (Game.Wolf == 0) Game.Stack.Pop();
}
}
// Every turn of the battle, the wolf bites.
public override void Update()
{
if (Game.Wolf > 0) Game.Life--;
}
public override void Draw(TextBuffer buffer)
{
buffer.Box(0, 0, TextBuffer.W, TextBuffer.H);
buffer.Write(2, 1, "Wolf: " + Game.Wolf);
buffer.Write(2, 2, "Life: " + Game.Life);
buffer.Write(2, 3, "a: hit i: inventory");
}
}
class InventoryScreen : Screen
{
public InventoryScreen(Game game) : base(game, "Inventory") { }
public override bool IsTransparent()
{
return true;
}
public override void Handle(string command)
{
if (command == "x") Game.Stack.Pop();
}
public override void Draw(TextBuffer buffer)
{
buffer.Box(12, 1, 11, 3);
buffer.Write(13, 2, "Potion x2");
}
}

More tasks

  • Hero creation. Between the title screen and the map, add a screen that asks for a name. Both transitions use replace: there is no way back from the hero.
  • Pause. p opens an opaque "Pause" screen from any screen. Check that the wolf does not bite while the game is paused.
  • Stack guard. Make pop refuse when only one screen is left on the stack, and print why.

Test

Check yourself

1. The stack is [Map, Battle, Inventory]. The player types a (hit). What happens to the wolf?
2. The stack is [Map, Battle, Inventory, Menu]. The inventory and the menu are transparent, the map and the battle are not. Which screens are drawn, and in what order?
3. Why does the title screen open the map with replace and not with push?
4. A colleague keeps screen and one more variable, previous, for "where I came from". When is that not enough?
5. You add a "Hero" screen that opens with c from the map. What changes in the program with the stack?

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