Към съдържанието
wannacode

Стек от екрани

Накратко: Стекът от екрани пази отворените екрани един върху друг: входът отива към горния, push отваря нов, pop го затваря, replace го сменя, а прозрачните се рисуват върху тези под тях.

Проблем

Една променлива за всички екрани

Играта има начален екран, село, карта, бой, инвентар, който се отваря върху картата, и меню, което се отваря върху боя. Всеки екран разбира командите по своему: на картата a е крачка наляво, в боя е удар, в инвентара е нищо.

Първият опит, който почти всеки прави: една променлива с името на екрана и if по нея. Веднъж в обработката на командата и още веднъж в рисуването:

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("Хрониките на Арканум");
else if (screen == "map") Console.WriteLine("Карта, вълк: " + wolf);
else if (screen == "battle") Console.WriteLine("Бой, вълк: " + wolf);
else if (screen == "inventory") Console.WriteLine("Инвентар: Отвара x2");
}
Изход
n → Карта, вълк: 2
i → Инвентар: Отвара x2
x → Карта, вълк: 2
f → Бой, вълк: 2
i → Инвентар: Отвара x2
x → Карта, вълк: 2
a → Карта, вълк: 2
a → Карта, вълк: 2

Командите идват по една на ред: n нова игра, i инвентар, x затваря, f бой с вълка, a удар.

Вход
n
i
x
f
i
x
a
a

Виж шестия ред. Инвентарът е отворен от боя, а x го затваря към картата. Боят е изчезнал, вълкът е жив с 2 живот, а двата удара след това падат на картата, където a не значи нищо. Променливата помни къде си, но не и откъде дойде.

Второто се вижда, когато играта порасне. Всеки нов екран е нов клон във всеки if: в обработката, в рисуването и във всеки екран, от който се отваря. Меню върху боя и върху картата е още два клона на две места. Забравиш ли един, получаваш екран, който приема команди, но не се рисува.

Идеята

Екранът решава сам за себе си

Обръщаме въпроса. Вместо цикълът да пита „на кой екран съм“ и да знае всеки екран, всеки екран е обект с едни и същи четири метода:

  • Handle: какво значи командата тук;
  • Update: какво става всеки ход, докато екранът е отгоре (в боя вълкът хапе);
  • Draw: какво рисува екранът;
  • IsTransparent: дали екранът под него се вижда.
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 казва, че всеки екран е длъжен да има свой Handle и свой Draw: без тях класът не се компилира. Update и IsTransparent имат поведение по подразбиране и екранът ги пише наново само ако му трябват.

Ето боя и инвентара. Боят знае само за себе си: i отваря инвентара отгоре, a удря, а когато вълкът падне, боят маха себе си. Инвентарът не знае откъде е отворен. Той само казва „махни ме“:

class BattleScreen : Screen
{
public BattleScreen(Game game) : base(game, "Бой") { }
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();
}
}
// Всеки ход на боя вълкът хапе.
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, "Вълк: " + Game.Wolf);
buffer.Write(2, 2, "Живот: " + Game.Life);
buffer.Write(2, 3, "a: удар i: инвентар");
}
}
class InventoryScreen : Screen
{
public InventoryScreen(Game game) : base(game, "Инвентар") { }
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, "Отвара x2");
}
}

Екраните рисуват в TextBuffer: пет реда по 24 знака. Write пише текст от дадено място, Box рисува рамка. Целият е в програмата по-долу, а как се рисува в истинския терминал, е в урока за рисуването.

Нов екран е нов клас и един ред в екрана, който го отваря. Никой if по имена на екрани не расте.

По-точно, за любопитните

Така е в играта. Регионът screen от канона:

// Всеки екран е обект с еднакъв интерфейс. Играта не пита „на кой екран съм“, а праща командата към
// екрана на върха на стека.
abstract class Screen
{
public Game Game;
public string KindName = "";
public Screen(Game game)
{
Game = game;
}
// Прозрачният екран (меню, инвентар, диалог) оставя екрана под себе си да се вижда.
public virtual bool IsTransparent()
{
return false;
}
public virtual void Handle(string command) { }
public virtual void Update() { }
public virtual void Draw(CellBuffer buffer) { }
// Екранът под попнатия получава неговия отговор (избрана цел, „да“ или „не“, избран слот).
public virtual void OnResult(object? result) { }
}

Методът OnResult е за екрани, които връщат отговор (избраната цел, „да“ или „не“). За него е следващият урок.

Идеята

Стекът: откъдето дойде

Екраните са един върху друг: инвентар върху боя, бой върху картата. Когато затвориш горния, искаш да си точно там, където беше. Това е стек: последният сложен се маха пръв. Стекът от екрани има три операции:

  • push: отвори нов екран отгоре (картата отваря боя, боят отваря инвентара);
  • pop: затвори горния и се върни към този под него;
  • replace: смени горния с нов, без връщане (началният екран става карта, създаването на героя става село).
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);
}
}

Цикълът на играта дава командата само на върха. Не на картата, не на боя под инвентара. Само върхът знае какво значи командата сега. Същите команди, сега със стека. Show печата стека и кадъра след всяка команда:

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);
}
Изход
[Начало]
Хрониките на Арканум
n: нова игра
> n
[Карта]
########################
#..@......~~~..........#
#.......w.~~~..........#
########################
Живот: 10
> i
[Карта, Инвентар]
+---------+
|Отвара x2|
+---------+
> x
[Карта]
########################
#..@......~~~..........#
#.......w.~~~..........#
########################
Живот: 10
> f
[Карта, Бой]
+----------------------+
| Вълк: 2 |
| Живот: 10 |
| a: удар i: инвентар |
+----------------------+
> i
[Карта, Бой, Инвентар]
+---------+
|Отвара x2|
+---------+
> x
[Карта, Бой]
+----------------------+
| Вълк: 2 |
| Живот: 9 |
| a: удар i: инвентар |
+----------------------+
> a
[Карта, Бой]
+----------------------+
| Вълк: 1 |
| Живот: 8 |
| a: удар i: инвентар |
+----------------------+
> a
[Карта]
########################
#..@......~~~..........#
#.........~~~..........#
########################
Живот: 8

Инвентарът, отворен от боя, сега се затваря към боя: [Карта, Бой]. Вълкът пада от втория удар, боят маха себе си и отдолу е картата, вече без w. Никой екран не помни откъде е дошъл. Помни го стекът.

Само че виж кадрите с инвентара. Картата е изчезнала, а рамката виси в празно. Draw на стека рисува само върха. За това е след малко.

По-точно, за любопитните

Само върхът се обновява. Цикълът вика Update на върха. Докато инвентарът е отворен върху боя, боят чака и вълкът не хапе: играта е на пауза под инвентара. Това е избор, не закон. В игра в реално време светът под менюто може да продължи, и тогава стекът обновява и екрани под върха, които кажат, че не спират.

pop на празен стек. Ако последният екран махне себе си, стекът е празен и следващото махане няма какво да махне. В C# RemoveAt(-1) хвърля ArgumentOutOfRangeException. В играта празният стек значи край: цикълът върти, докато стекът не опустее, а „Изход“ от началния екран изчиства стека.

Пробвай

Предвиди изхода

Стекът е празен. Какво е в него накрая?

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());
Отговор

Идеята

Прозрачните екрани

Инвентарът е малка рамка върху картата. За да се види картата около нея, картата трябва да се нарисува първа, а инвентарът след нея, върху нея. Като художник, който рисува първо фона, после фигурите отпред.

Но не от най-долния екран. Под боя е картата, а боят я закрива целия: да я рисуваш е напразно. Затова рисуването започва от най-горния непрозрачен екран. Тръгваш от върха надолу, докато екраните прозират. Първият, който не прозира, е началото. Оттам рисуваш нагоре:

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 InventoryScreen : Screen
{
public InventoryScreen(Game game) : base(game, "Инвентар") { }
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, "Отвара x2");
}
}

Само тези две промени. Същият цикъл, същите команди:

Изход
[Начало]
Хрониките на Арканум
n: нова игра
> n
[Карта]
########################
#..@......~~~..........#
#.......w.~~~..........#
########################
Живот: 10
> i
[Карта, Инвентар]
########################
#..@......~~+---------+#
#.......w.~~|Отвара x2|#
############+---------+#
Живот: 10
> x
[Карта]
########################
#..@......~~~..........#
#.......w.~~~..........#
########################
Живот: 10
> f
[Карта, Бой]
+----------------------+
| Вълк: 2 |
| Живот: 10 |
| a: удар i: инвентар |
+----------------------+
> i
[Карта, Бой, Инвентар]
+----------------------+
| Вълк: 2 +---------+|
| Живот: 9 |Отвара x2||
| a: удар i+---------+|
+----------------------+
> x
[Карта, Бой]
+----------------------+
| Вълк: 2 |
| Живот: 9 |
| a: удар i: инвентар |
+----------------------+
> a
[Карта, Бой]
+----------------------+
| Вълк: 1 |
| Живот: 8 |
| a: удар i: инвентар |
+----------------------+
> a
[Карта]
########################
#..@......~~~..........#
#.........~~~..........#
########################
Живот: 8

[Карта, Инвентар]: картата, а върху нея рамката, а вълкът се вижда отдолу. [Карта, Бой, Инвентар]: рисуването започва от боя, защото той не прозира. Картата под него не се рисува изобщо.

first > 0 спира на най-долния екран: той се рисува винаги, дори да прозира, защото под него няма нищо.

По-точно, за любопитните

Методът Draw от региона screen-stack на канона:

// Рисуването започва от най-горния непрозрачен екран и върви нагоре.
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);
}
}

Проверката i >= 0 е за празен стек: тогава first е −1 и няма какво да се рисува.

Защо не просто от нулата? Ако всеки непрозрачен екран закрива всичко, резултатът е същият, но всеки кадър рисува всички екрани отдолу: карта, бой, избор на цел, диалог. Има и по-лошо: непрозрачен екран, който не пише във всяка клетка, ще остави да личат парчета от картата. „Не прозирам“ е обещание „покривам всичко“, и стекът разчита на него.

Виж го

Ход по ход

Същата програма с прозрачния инвентар. Отляво е стекът: всяка плоча е един екран с това, което той рисува сам. Командата отива към плочата на върха. Бледите плочи са под най-горната непрозрачна и не се рисуват. Под стека е кадърът, който програмата печата.

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);
}
Стъпка по стъпка, с думи
  1. Играта започва с един екран в стека: началния. Той е на върха и получава входа.
  2. n отива към началния екран. Той слага картата на своето място (replace): началният екран няма да се върне.
  3. i отива към картата и тя слага инвентара отгоре (push). Инвентарът прозира: първо се рисува картата, после инвентарът върху нея.
  4. x отива към инвентара: той се маха (pop). Отдолу е картата, точно каквато я остави.
  5. f: картата слага боя отгоре (push). Боят не прозира, затова картата под него изобщо не се рисува.
  6. Сега i отива към боя, не към картата. Боят отваря инвентара над себе си. Рисуването започва от боя, най-горния непрозрачен екран. Вълкът ухапа: живот 9.
  7. x затваря инвентара и отдолу е боят: връщаш се там, откъдето дойде. Единствената променлива от началото на урока тук те върна на картата.
  8. a отива към боя: вълкът е ранен, а стекът не се променя. Минава update на боя: вълкът хапе, живот 8.
  9. Вълкът пада. Боят се маха (pop) и картата пак е на върха, вече без вълка.
  10. Входът отива към върха. Рисуването върви нагоре от най-горния непрозрачен екран. Екраните, които връщат отговор, и промените, които чакат края на хода, са в следващия урок.

Пробвай

Попълни празнината

Рисуването на стека. Докато екранът на first прозира, слизаш надолу:

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

Пробвай

Намери грешката

Колега написа стека наново. Програмата с командите n, i, x печата:

Изход
[Начало]
Хрониките на Арканум
n: нова игра
> n
[Карта]
########################
#..@......~~~..........#
#.......w.~~~..........#
########################
Живот: 10
> i
[Карта, Инвентар]
########################
#..@......~~+---------+#
#.......w.~~|Отвара x2|#
############+---------+#
Живот: 10
> x
[Карта, Инвентар]
########################
#..@......~~+---------+#
#.......w.~~|Отвара x2|#
############+---------+#
Живот: 10

Инвентарът не се затваря: след x стекът пак е [Карта, Инвентар]. На кой ред е грешката?

Избери реда с грешката.

Идеята

Стекът в истинската игра

Регионът screen-stack от канона. Методите са същите, но има разлики:

// Стекът не се променя по време на обработката: Push, Pop и Replace само записват промяната, а
// Apply() ги прилага след хода. Иначе екран, който се маха от стека, би продължил да работи.
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 });
}
// Изчиства целия стек (зареждане, край на играта); с null стекът остава празен.
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>();
}
// Рисуването започва от най-горния непрозрачен екран и върви нагоре.
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 и replace не пипат списъка веднага. Записват промяната в changes, а Apply() ги изпълнява след хода. В нашата програма боят маха себе си в Handle, а веднага след това цикълът вика Update на същия бой, който вече не е в стека. Тук минава, защото мъртъв вълк не хапе. В по-голяма игра това е бъг, който се търси дълго. Защо и как се отлагат промените, е следващият урок.
  • pop носи отговор: екранът отдолу го получава в OnResult. Така екранът за избор на цел връща целта на боя. И това е в следващия урок.
  • reset изчиства целия стек (зареждане на игра, край на играта), а contains пита дали екран от даден вид вече е вътре (например да не се отвори второ качване на ниво).

По-точно

Същата идея другаде

Навигацията в мобилните приложения е стек от екрани. В Android и iOS отваряш екран отгоре (push), а бутонът „Назад“ го маха (pop). Влизането след логин обикновено е replace: назад не трябва да води пак към формата за вход.

Модалният диалог в браузъра е екран върху страницата. Докато е отворен, само той получава кликове и клавиши, а страницата под него се вижда, но не реагира. Същото като инвентара върху картата.

Стекът на извикванията е същата идея в самия език. Метод вика друг метод (push), той връща (pop), и извикващият продължава точно откъдето е спрял. Боят продължава след инвентара по същия начин.

Кога стекът не стига. Стекът казва, че отгоре е точно един екран и той получава всичко. Понякога това не е вярно:

  • два екрана един до друг, които работят заедно: карта и мини-карта, бой и дневник на боя, игра за двама на разделен екран. Тогава екранът е дърво от панели, а входът отива към панела на фокуса;
  • неща, които са винаги отгоре, независимо от екрана: лента с живота, известие „Играта е записана“. Те са отделен слой, който се рисува след стека;
  • прозорци, които се затварят в произволен ред (стратегия с пет отворени прозореца). Това вече не е стек, а списък с фокус.

Упражнение

Меню върху всичко

Реши задачата в редактора си. Цялата програма с прозрачните екрани е под задачата.

Меню. Добави MenuScreen с име „Меню“. m го отваря от картата, от боя и от инвентара. Менюто прозира, а x го затваря. Рисува рамка Box(14, 2, 10, 3) и в нея x: назад от място (15, 3).

Вход
n
f
i
m
x
x
a
a
Изход
[Начало]
Хрониките на Арканум
n: нова игра
> n
[Карта]
########################
#..@......~~~..........#
#.......w.~~~..........#
########################
Живот: 10
> f
[Карта, Бой]
+----------------------+
| Вълк: 2 |
| Живот: 10 |
| a: удар i: инвентар |
+----------------------+
> i
[Карта, Бой, Инвентар]
+----------------------+
| Вълк: 2 +---------+|
| Живот: 9 |Отвара x2||
| a: удар i+---------+|
+----------------------+
> m
[Карта, Бой, Инвентар, Меню]
+----------------------+
| Вълк: 2 +---------+|
| Живот: 9 |О+--------+
| a: удар i+-|x: назад|
+-------------+--------+
> x
[Карта, Бой, Инвентар]
+----------------------+
| Вълк: 2 +---------+|
| Живот: 9 |Отвара x2||
| a: удар i+---------+|
+----------------------+
> x
[Карта, Бой]
+----------------------+
| Вълк: 2 |
| Живот: 9 |
| a: удар i: инвентар |
+----------------------+
> a
[Карта, Бой]
+----------------------+
| Вълк: 1 |
| Живот: 8 |
| a: удар i: инвентар |
+----------------------+
> a
[Карта]
########################
#..@......~~~..........#
#.........~~~..........#
########################
Живот: 8

Виж кадъра с менюто: рисуват се боят, инвентарът и менюто, в този ред. Менюто покрива ъгъла на инвентара, защото е по-горе в стека.

По-точно, за любопитните

Цялата програма с прозрачните екрани:

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, "Начало") { }
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, "Хрониките на Арканум");
buffer.Write(1, 3, "n: нова игра");
}
}
class MapScreen : Screen
{
static readonly string[] Rows =
{
"########################",
"#..@......~~~..........#",
"#.........~~~..........#",
"########################",
};
public MapScreen(Game game) : base(game, "Карта") { }
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, "Живот: " + Game.Life);
}
}
class BattleScreen : Screen
{
public BattleScreen(Game game) : base(game, "Бой") { }
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();
}
}
// Всеки ход на боя вълкът хапе.
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, "Вълк: " + Game.Wolf);
buffer.Write(2, 2, "Живот: " + Game.Life);
buffer.Write(2, 3, "a: удар i: инвентар");
}
}
class InventoryScreen : Screen
{
public InventoryScreen(Game game) : base(game, "Инвентар") { }
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, "Отвара x2");
}
}

Още задачи

  • Създаване на героя. Между началния екран и картата сложи екран, който пита за име. И двата прехода са с replace: от героя назад няма.
  • Пауза. p отваря непрозрачен екран „Пауза“ от всеки екран. Провери, че вълкът не хапе, докато играта е на пауза.
  • Пазач на стека. Направи pop да отказва, когато в стека е останал само един екран, и да печата защо.

Тест

Провери се

1. Стекът е [Карта, Бой, Инвентар]. Играчът пише a (удар). Какво става с вълка?
2. Стекът е [Карта, Бой, Инвентар, Меню]. Инвентарът и менюто прозират, картата и боят не. Кои екрани се рисуват и в какъв ред?
3. Защо началният екран отваря картата с replace, а не с push?
4. Колега пази screen и още една променлива previous за „откъде дойдох“. Кога това не стига?
5. Добавяш екран „Герой“, който се отваря с c от картата. Какво се променя в програмата със стека?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·