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

Цикълът на играта

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

Проблем

Програма, която свършва

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

Първото, което идва наум, е while (true), който чете ред и прави всичко вътре. Коридор от 12 клетки: ти си @, гоблинът g те гони, изходът е >:

int hero = 4;
int goblin = 0;
while (true)
{
string command = Console.ReadLine()!;
if (command == "q") break;
if (command == "a") hero--;
if (command == "d") hero++;
string row = "";
for (int x = 0; x <= 11; x++)
{
row += x == hero ? "@" : x == goblin ? "g" : x == 11 ? ">" : ".";
}
Console.WriteLine(row);
if (hero == 11)
{
Console.WriteLine("Изход!");
break;
}
goblin += goblin < hero ? 1 : -1;
if (Math.Abs(hero - goblin) <= 1)
{
Console.WriteLine("Гоблинът те хвана.");
break;
}
}

Играчът пише x (сбъркал е клавиша) и после a:

Изход
g...@......>
.g.@.......>
Гоблинът те хвана.

Три неща не са наред:

  • Преди първата команда екранът е празен: програмата рисува чак след като прочете ред.
  • Печатната грешка x е ход: гоблинът е стъпил. Програмата не различава „играчът направи нещо“ от „играчът сбърка клавиша“.
  • Последният кадър е .g.@: между вас има празна клетка, а гоблинът те хваща. Кадърът е отпечатан след твоята стъпка и преди тази на гоблина. Положението, в което играта свърши, никой не го е видял.

И две неща не стават изобщо. Не можеш да нарисуваш екрана наново без ход: след помощта с ? или след инвентара рисуването трябва да се копира. Не можеш да провериш правилото „гоблинът стъпва към героя“, без да пишеш команди и да четеш текст. Причината е една: светът, правилата и рисуването са смесени на едно място.

Идеята

Четири стъпки в кръг

Разделяме ги:

  • Състоянието е всичко, което светът е, в един обект: героят, гоблинът, изходът, броячът на ходовете, съобщението, краят. Екранът не е част от него. Екранът се смята от него.
  • Draw само чете състоянието и печата кадър. Може да се вика колкото пъти искаш.
  • Handle превежда командата в намерение: Move е -1, 1 или 0. Никого не мести.
  • Update сменя света: ходът, героят, гоблинът, краят. Не печата нищо.

Цикълът ги вика в кръг: нарисувай, прочети, обработи, обнови. Това е цикълът на играта (game loop). Има го във всяка игра, от този коридор до Арканум.

class State
{
public int Hero = 4;
public int Goblin = 0;
public int Exit = 11;
public int Turn = 0;
public int Move = 0;
public string Message = "";
public bool Over = false;
}
var state = new State();
while (!state.Over)
{
Draw(state);
string? command = Read();
Handle(state, command);
Update(state);
}
Draw(state);

Over спира цикъла, а последното Draw след него показва края: изхода или гоблина, със съобщението.

Четенето е отделна функция, защото входът може да свърши:

static string? Read()
{
string? line = Console.ReadLine();
if (line != null) Console.WriteLine("> " + line);
return line;
}

Console.ReadLine() връща null в края на входа. Read го подава нататък, а Handle го смята за край на играта. Ехото > a е, за да се вижда в изхода коя команда е дошла. Конзолата на играча я показва сама.

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

Без проверката за края на входа всеки език се чупи различно. Java хвърля изключение, Python също. C# е най-коварен: Console.ReadLine() връща null, сравнението command == "q" е невярно, и първият опит не разбира, че входът е свършил: печата кадри с празни ходове, докато гоблинът не стигне героя сам. JavaScript получава undefined и прави същото. Входът свършва по-често, отколкото мислиш: файл с команди, затворен терминал, проверката на сайта.

Виж го

Един ход отблизо

Ето трите функции. В тях е цялата игра:

static void Draw(State s)
{
string row = "";
for (int x = 0; x <= s.Exit; x++)
{
if (x == s.Hero) row += "@";
else if (x == s.Goblin) row += "g";
else if (x == s.Exit) row += ">";
else row += ".";
}
Console.WriteLine("Ход " + s.Turn + " " + row);
if (s.Message != "") Console.WriteLine(s.Message);
}
static void Handle(State s, string? command)
{
s.Move = 0;
s.Message = "";
if (command == null)
{
s.Message = "Входът свърши.";
s.Over = true;
}
else if (command == "a")
{
s.Move = -1;
}
else if (command == "d")
{
s.Move = 1;
}
else if (command == "q")
{
s.Message = "Тръгна си.";
s.Over = true;
}
else
{
s.Message = "Не разбирам „" + command + "“: ходът не мина.";
}
}
static void Update(State s)
{
if (s.Move == 0) return;
s.Turn++;
s.Hero = Math.Max(0, s.Hero + s.Move);
if (s.Hero == s.Exit)
{
s.Message = "Изход! Избяга за " + s.Turn + " хода.";
s.Over = true;
return;
}
if (Math.Abs(s.Hero - s.Goblin) > 1) s.Goblin += s.Goblin < s.Hero ? 1 : -1;
if (Math.Abs(s.Hero - s.Goblin) <= 1)
{
s.Message = "Гоблинът те хвана.";
s.Over = true;
}
}

var state = new State();
while (!state.Over)
{
Draw(state);
string? command = Read();
Handle(state, command);
Update(state);
}
Draw(state);
static void Draw(State s)
{
string row = "";
for (int x = 0; x <= s.Exit; x++)
{
if (x == s.Hero) row += "@";
else if (x == s.Goblin) row += "g";
else if (x == s.Exit) row += ">";
else row += ".";
}
Console.WriteLine("Ход " + s.Turn + " " + row);
if (s.Message != "") Console.WriteLine(s.Message);
}
static void Handle(State s, string? command)
{
s.Move = 0;
s.Message = "";
if (command == null)
{
s.Message = "Входът свърши.";
s.Over = true;
}
else if (command == "a")
{
s.Move = -1;
}
else if (command == "d")
{
s.Move = 1;
}
else if (command == "q")
{
s.Message = "Тръгна си.";
s.Over = true;
}
else
{
s.Message = "Не разбирам „" + command + "“: ходът не мина.";
}
}
static void Update(State s)
{
if (s.Move == 0) return;
s.Turn++;
s.Hero = Math.Max(0, s.Hero + s.Move);
if (s.Hero == s.Exit)
{
s.Message = "Изход! Избяга за " + s.Turn + " хода.";
s.Over = true;
return;
}
if (Math.Abs(s.Hero - s.Goblin) > 1) s.Goblin += s.Goblin < s.Hero ? 1 : -1;
if (Math.Abs(s.Hero - s.Goblin) <= 1)
{
s.Message = "Гоблинът те хвана.";
s.Over = true;
}
}
Стъпка по стъпка, с думи
  1. Ход 1 е минал: героят е на клетка 5, гоблинът на клетка 1. Целият свят е обектът със състоянието, а цикълът е пак горе.
  2. Draw минава по 12-те клетки и печата кадъра .g...@.....>. Само чете състоянието: в панела нищо не се сменя.
  3. Read чака. Светът стои, докато играчът не натисне Enter. Пристига редът „a“.
  4. Handle само превежда командата в намерение: Move = -1. Още никой не е мръднал.
  5. Update сменя света. Броячът на ходовете става 2, а героят стъпва от 5 на 4. Не е изходът, затова ходът продължава.
  6. Гоблинът е на 3 клетки, затова стъпва към героя: от 1 на 2. Вече са 2: не е хванат, играта продължава.
  7. Отново горе Draw печата ..g.@......>: кадърът идва след целия ход, затова играчът вижда къде е гоблинът наистина.

С командите d, a, x и седем пъти d програмата печата:

Изход
Ход 0 g...@......>
> d
Ход 1 .g...@.....>
> a
Ход 2 ..g.@......>
> x
Ход 2 ..g.@......>
Не разбирам „x“: ходът не мина.
> d
Ход 3 ...g.@.....>
> d
Ход 4 ....g.@....>
> d
Ход 5 .....g.@...>
> d
Ход 6 ......g.@..>
> d
Ход 7 .......g.@.>
> d
Ход 8 ........g.@>
> d
Ход 9 ........g..@
Изход! Избяга за 9 хода.

На x кадърът се рисува наново, но ходът остава 2 и гоблинът стои: Update вижда Move = 0 и не прави нищо. Това става, защото Draw не пипа света. Гоблинът стъпва след героя, а кадърът идва след целия ход, затова виждаш точно къде е. На изхода гоблинът не стъпва: играта свършва преди неговата стъпка.

Правилата вече могат да се проверят без екран и без писане на команди. Сглобяваш състояние на ръка, викаш Update и гледаш числата:

var near = new State { Hero = 5, Goblin = 3, Move = -1 };
Update(near);
Console.WriteLine(near.Hero + " " + near.Goblin + " " + near.Message);
var far = new State { Move = 1 };
Update(far);
Console.WriteLine(far.Hero + " " + far.Goblin + " " + far.Turn);
Изход
4 3 Гоблинът те хвана.
5 1 1

Това е тест на правилото „гоблинът хваща, ако е до теб“. В истинска игра такива проверки са стотици. С първия опит не можеше да се напише нито една.

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

Защо Handle не мести героя направо? В коридора би могъл. Но в Арканум едно и също a значи „наляво“ на картата, „назад“ в менюто и „нищо“ в диалога. Командата се разбира от екрана, а светът се сменя от правилата. И второ: команда, която не е ход (печатна грешка, отваряне на инвентара, помощ), не трябва да движи света. Затова намерението се записва отделно, а update решава дали изобщо има ход.

Пробвай

Подреди цикъла

Подреди редовете на цикъла:

  1. Handle(state, command);
  2. {
  3. while (!state.Over)
  4. string? command = Read();
  5. }
  6. Draw(state);
  7. Update(state);

Пробвай

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

Същата програма получава командите a, x, a. Правилата са тези:

static void Handle(State s, string? command)
{
s.Move = 0;
s.Message = "";
if (command == null)
{
s.Message = "Входът свърши.";
s.Over = true;
}
else if (command == "a")
{
s.Move = -1;
}
else if (command == "d")
{
s.Move = 1;
}
else if (command == "q")
{
s.Message = "Тръгна си.";
s.Over = true;
}
else
{
s.Message = "Не разбирам „" + command + "“: ходът не мина.";
}
}
static void Update(State s)
{
if (s.Move == 0) return;
s.Turn++;
s.Hero = Math.Max(0, s.Hero + s.Move);
if (s.Hero == s.Exit)
{
s.Message = "Изход! Избяга за " + s.Turn + " хода.";
s.Over = true;
return;
}
if (Math.Abs(s.Hero - s.Goblin) > 1) s.Goblin += s.Goblin < s.Hero ? 1 : -1;
if (Math.Abs(s.Hero - s.Goblin) <= 1)
{
s.Message = "Гоблинът те хвана.";
s.Over = true;
}
}
Отговор

Пробвай

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

Колега реши, че гоблинът трябва да се движи „на всеки кадър“, и премести стъпката му от Update в Draw. С командите d, x, x, d програмата печата:

Изход
Ход 0 .g..@......>
> d
Ход 1 ..g..@.....>
> x
Ход 1 ...g.@.....>
Не разбирам „x“: ходът не мина.
> x
Ход 1 ....g@.....>
Не разбирам „x“: ходът не мина.
> d
Ход 2 .....g@....>
Ход 2 .....g@....>
Входът свърши.

Още на кадър 0 гоблинът е на клетка 1. На двете печатни грешки ходът стои 1, а гоблинът тича. Накрая е до героя и нищо не става. На кой ред е грешката?

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

Идеята

Ход и кадър

Коридорът е походов: светът стои, докато не дойде команда. Един ход, един кадър. Конзолата дава цял ред чак след Enter и четенето чака, затова играта в конзолата е походова по природа. Така са шахът, roguelike игрите (Rogue, NetHack) и боевете в много стратегии.

Игра в реално време не чака. Светът се движи по часовник: ако не правиш нищо, гоблинът пак идва. Цикълът се върти десетки пъти в секунда: поглежда дали има натиснат клавиш (без да чака), обновява света за изминалото време и рисува. Едно рисуване е кадър (frame), а една стъпка на света е отделно нещо.

Четирите части са същите. Сменя се кой върти цикъла: редът на играча или часовникът. И там разделянето на Draw и Update е още по-важно: бърз монитор рисува 144 кадъра в секунда, но светът трябва да стъпва с еднакво темпо на всеки компютър.

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

Фиксиран такт. Ако светът стъпва „колкото време е минало от миналия кадър“, физиката зависи от кадрите: при бавен кадър стрелата прескача стената, а два компютъра никога не дават същото. Затова светът стъпва на равни парчета, например 50 ms. Минатото време се натрупва (accumulator), и цикълът прави толкова стъпки, колкото парчета са се събрали. Остатъкът чака следващия кадър.

Тук часовникът е измислен (списък с моментите на кадрите), за да е изходът един и същ. Бързият компютър рисува на неравни интервали, бавният рядко:

static void Run(string name, int[] times)
{
int step = 50;
int acc = 0;
int x = 0;
int last = times[0];
foreach (int now in times)
{
acc += now - last;
last = now;
int steps = 0;
while (acc >= step)
{
x += 3;
steps++;
acc -= step;
}
Console.WriteLine(name + " " + now + " ms: стъпки " + steps + ", x = " + x + ", остатък " + acc + " ms");
}
}
Run("бърз", [0, 16, 40, 45, 110, 126, 150]);
Run("бавен", [0, 90, 150]);
Изход
бърз 0 ms: стъпки 0, x = 0, остатък 0 ms
бърз 16 ms: стъпки 0, x = 0, остатък 16 ms
бърз 40 ms: стъпки 0, x = 0, остатък 40 ms
бърз 45 ms: стъпки 0, x = 0, остатък 45 ms
бърз 110 ms: стъпки 2, x = 6, остатък 10 ms
бърз 126 ms: стъпки 0, x = 6, остатък 26 ms
бърз 150 ms: стъпки 1, x = 9, остатък 0 ms
бавен 0 ms: стъпки 0, x = 0, остатък 0 ms
бавен 90 ms: стъпки 1, x = 3, остатък 40 ms
бавен 150 ms: стъпки 2, x = 9, остатък 0 ms

На 150 ms и двата са направили по 3 стъпки и стрелата е на 9. Кадрите са различни, светът е същият. Остатъкът се ползва и за рисуването: при 10 ms от 50 стрелата се рисува 20% по пътя към следващата си позиция (интерполация), за да не трепти.

Четене без чакане и сурови клавиши. Терминалът сам държи реда, докато не натиснеш Enter. Програмата получава нищо, докато не дойде целият ред. За реално време терминалът трябва да мине в „суров“ режим (raw mode), в който всеки клавиш идва веднага, а програмата трябва да пита „има ли клавиш?“, без да чака.

Console.KeyAvailable казва дали чака клавиш, без да блокира, а Console.ReadKey(true) взима един клавиш без Enter и без да го отпечата. Цикъл в реално време: if (Console.KeyAvailable) { var key = Console.ReadKey(true); … }, после обновяване и рисуване.

Арканум остава походова. Същият код трябва да върви в браузъра, в проверката (командите идват от файл) и в терминала на всеки. Редът текст работи навсякъде. Суровият режим не съществува за файл.

Идеята

Семе и повторяемост

Играта има нужда от случайност: удар или пропуск, каква плячка пада, накъде тръгва вълкът. Но грешка, която зависи от случайността, не може да се повтори. Затова генераторът има семе (seed): от едно и също семе излизат едни и същи числа.

Това е генераторът на Арканум:

// mulberry32: малък генератор със семе. Само 32-битови цели числа (uint и unchecked), затова
// дава една и съща поредица на всеки език.
class Random
{
public uint State;
public Random(uint seed)
{
State = seed;
}
public uint Next32()
{
unchecked
{
State = State + 0x6d2b79f5;
uint z = State;
z = (z ^ (z >> 15)) * (z | 1);
z = z ^ (z + (z ^ (z >> 7)) * (z | 61));
return z ^ (z >> 14);
}
}
// От 0 до n-1, без дробни числа.
public int NextInt(int n)
{
return (int)(Next32() % (uint)n);
}
// От min до max, двете включени.
public int Range(int min, int max)
{
return min + NextInt(max - min + 1);
}
public bool Chance(int percent)
{
return NextInt(100) < percent;
}
}

Генераторът помни едно-единствено число, State. Всяко Next32() го мести с една голяма константа и разбърква битовете му. NextInt(n) взима остатъка при деление на n, тоест число от 0 до n − 1. Зар d20 е Range(1, 20). Хвърляме го пет пъти със семе 2026, после пак със 2026, после със 7:

static void Roll(uint seed)
{
var dice = new Random(seed);
string line = "семе " + seed + ":";
for (int i = 0; i < 5; i++) line += " " + dice.Range(1, 20);
Console.WriteLine(line);
}
Roll(2026);
Roll(2026);
Roll(7);
Изход
семе 2026: 16 19 3 17 18
семе 2026: 16 19 3 17 18
семе 7: 13 11 15 11 16

Същото семе, същите числа. На четирите езика.

public int Range(int min, int max)
{
    return min + NextInt();
}
По-точно, за любопитните

Защо не вграденият Random? Всеки език има свой алгоритъм. Java ползва линеен конгруентен генератор с 48 бита, .NET друг, Python Mersenne Twister, а Math.random() в JavaScript изобщо не приема семе. Едно и също семе дава различни числа на различните езици, а в JavaScript не можеш да го зададеш. mulberry32 е десетина реда, затова е написан четири пъти по един и същ начин.

Числата трябва да са 32-битови без знак навсякъде. C# има uint, а unchecked казва, че препълването е нарочно: битовете отгоре просто отпадат.

Идеята

Само цели числа

Семето не стига. Вземи едно и също деление на четирите езика:

Console.WriteLine(-7 / 2);
Console.WriteLine(10.0 / 2);
Изход
-3
5

На четирите езика изходът е такъв:

Езикцяло деление на −7 на 210 / 2 с дроб
Java-35.0
C#-35
Python-45.0
JavaScript-35

Python закръгля цялото деление надолу, а другите към нулата. Дробното число се печата с .0 или без. Един различен знак в изхода, и боят на Python вече не е същият като на Java. Затова каноните смятат само с цели числа, делят изрично и не делят отрицателни числа. Сортирането винаги има второ правило, а колекциите пазят реда на добавяне.

Наградата е голяма. Със семето, командите и версията на играта едно минаване е функция: същият вход дава същия изход. Играч пише: „На ход 412 вълкът ме удари през стената.“ Ако играта пази семето и командите му, пускаш ги и стигаш точно до ход 412, колкото пъти трябва, под дебъгер. Проверката на сайта прави същото: записаните команди и семе 2026 дават на четирите езика байт по байт един и същ изход.

Идеята

Истинският цикъл на Арканум

Ето цикъла на играта, без съкращения:

// Цикълът на играта: нарисувай → прочети команда → обработи → обнови света → приложи промените
// в стека. Повтаря, докато стекът не опустее.
// Входът и изходът: Write печата, ReadLine връща реда или null в края, Save и Load пазят записите.
interface IGameIo
{
void Write(string text);
string? ReadLine();
void Save(string slot, string text);
string? Load(string slot);
}
class Options
{
public bool Plain;
public long Seed = 2026;
public string Lang = "bg";
public long Simulate;
public bool MemorySaves;
}
static partial class Arcanum
{
// Main(io, options) е играта; входната точка Main(args) е в блока boot.
#pragma warning disable CS0028
public static void Main(IGameIo io, Options options)
#pragma warning restore CS0028
{
uint seed = unchecked((uint)options.Seed);
if (options.Lang != "bg" && options.Lang != "en" && options.Lang != "de" && options.Lang != "es")
{
throw new Exception("unknown language: " + options.Lang + " (use bg, en, de or es)");
}
LANG = options.Lang;
if (options.MemorySaves) io.Save("3", TamperedSave());
LoadData();
long simulateCount = options.Simulate;
if (simulateCount > 0)
{
Simulate(io, (int)simulateCount, seed);
return;
}
ITerminal terminal = options.Plain ? new PlainTerminal(io) : new AnsiTerminal(io);
Game game = new Game(io, seed);
CellBuffer buffer = new CellBuffer(SCREEN_WIDTH, SCREEN_HEIGHT);
game.Stack.Push(new TitleScreen(game));
game.Stack.Apply();
while (!game.Stack.IsEmpty())
{
buffer.Clear();
game.Stack.Draw(buffer);
terminal.Present(buffer);
string? line = io.ReadLine();
if (line == null)
{
io.Write(T("ui.endOfInput") + "\n");
return;
}
terminal.Echo(line);
game.RawInput = TrimSpaces(line);
Screen top = game.Stack.Top()!;
top.Handle(game.RawInput.ToLowerInvariant());
top.Update();
game.Stack.Apply();
}
io.Write(T("ui.goodbye") + "\n");
}
}

Ред по ред:

  • game.Stack.Push(new TitleScreen(game)) слага първия екран. Целият свят е в game, както в коридора е в state.
  • Цикълът върви, докато в стека от екрани има нещо. Изходът от менюто маха всички екрани и играта свършва. Това е темата на следващия урок.
  • Нарисувай. Екраните рисуват в буфер от клетки (знак и цвят), а терминалът решава как да го покаже: като обикновен текст или с цветове. Това е в урока за рисуването в конзолата.
  • Прочети. Ако входът е свършил, играта казва „край на входа“ и спира. Същото като нашето null. Ехото на реда е като нашето > a.
  • Обработи и обнови. Само екранът най-отгоре получава командата: картата, боят или менюто.
  • Приложи. Петата стъпка, която коридорът няма. Екраните искат промени в стека, докато обработват (започни бой, затвори менюто), а те се прилагат чак в края на хода. Защо не веднага, е в урока за екраните, които връщат отговор.

Къде е ходът на света? В коридора е в Update. В Арканум картата вика WorldTurn() от своя handle, само когато героят наистина е направил нещо: стъпил, отворил врата, изчакал. На печатна грешка или на i за инвентара светът не мърда. Това е нашето „move е 0, няма ход“:

// Ходът на света след хода на героя: ефектите, бавното възстановяване, враговете, засадата.
public void WorldTurn()
{
Level level = Level()!;
Hero hero = Hero;
Turn += 1;
Stats.Turns += 1;
ComputeFov(level, hero.X, hero.Y);
TickEffects(hero, this);
if (!hero.IsAlive())
{
HeroDied();
return;
}
if (Turn % 3 == 0 && !hero.HasEffect("bleed") && !hero.HasEffect("poison"))
{
hero.Heal(1);
if (hero.Mana < hero.MaxMana()) hero.Mana += 1;
}
foreach (Enemy enemy in new List<Enemy>(level.Enemies))
{
if (!enemy.IsAlive()) continue;
EnemyTurnOnMap(this, level, enemy);
bool visible = level.Visible[enemy.Y][enemy.X];
TickEffects(enemy, visible ? this : Quiet);
if (!enemy.IsAlive())
{
enemy.Dead = true;
if (visible) Say(T("map.bledOut", ("name", enemy.Name)));
// Изкървелият враг извън боя не хвърля плячка: нищо не го е обрало.
EnemyKilled(enemy, new Spoils { Gold = 0, Drop = null });
}
}
PruneDead();
ComputeFov(level, hero.X, hero.Y);
foreach (Enemy enemy in level.Enemies)
{
if (enemy.State == "chase" && Distance(enemy.X, enemy.Y, hero.X, hero.Y) <= 1)
{
StartBattle(enemy);
return;
}
}
}

Броячът на ходовете, ефектите (кървенето тече всеки ход), бавното възстановяване на всеки трети ход, ходът на всеки враг, враговете, които са изкървели. И краят, точно като гоблина: враг, който гони и е до героя, започва бой. В боя ходовете са по друго правило, по бързина, затова всеки екран сам решава кога е ход, а update на картата само преизчислява какво се вижда.

Упражнение

Гоблинът спи

Реши задачата в редактора си.

Спящият гоблин. Гоблинът в началото спи и се рисува като z. Събужда се, когато героят е на 3 клетки или по-близо. В хода, в който се събуди, не мърда, а после гони както досега и се рисува като g. Цикълът не се променя: трябват ти едно поле в състоянието, един знак в Draw и няколко реда в Update.

Входът:

Вход
d
d
a
a
a
a
Изход
Ход 0 z...@......>
> d
Ход 1 z....@.....>
> d
Ход 2 z.....@....>
> a
Ход 3 z....@.....>
> a
Ход 4 z...@......>
> a
Ход 5 g..@.......>
> a
Ход 6 .g@........>
Гоблинът те хвана.

Още задачи

  • Изчакай. Добави команда s: героят стои, но ходът минава и гоблинът стъпва. Как ще различиш „стой“ от „печатна грешка“ в Handle?
  • Запис на командите. Пази всяка прочетена команда в списък и в края го запиши във файл, по една команда на ред. Пусни програмата пак с този файл като вход: изходът трябва да е същият. Така работи повторението на боя в Арканум.
  • Два гоблина. Втори гоблин тръгва от клетка 11 и пази изхода. Кой стъпва пръв, когато и двамата са до героя?

Тест

Провери се

1. Защо в коридора draw само чете състоянието и не сменя нищо?
2. Играчът пише x в коридора. Какво става?
3. Защо играта в конзолата е походова?
4. Играч съобщава: „На ход 412 вълкът ме удари през стената.“ Какво ти трябва, за да видиш същото?
5. Цикъл в реално време стъпва физиката през 50 ms. От миналия кадър са останали 10 ms, а новият кадър идва след 120 ms. Колко стъпки прави физиката?

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