Как я «дебажил» простенькую игру, но обнаружил кое-что еще

от автора

Речь идет о обычной «Змейке», написанной на C# и запускаемая в консоли. Во время игры рядом с «едой» после того как нажмешь кнопку управления змейкой, появлялся символ «а», которого в исходном коде просто не могло быть, поэтому мне и захотелось разобраться почему так происходит и как это можно починить:

Рядом с едой которая обозначена символом @ появляется буква "а", которая не исчезает и может перекрывать препятствия
Рядом с едой которая обозначена символом @ появляется буква «а», которая не исчезает и может перекрывать препятствия

Для пояснения почему вообще взялся за это дело, хочу начать из далека, чтобы вы понимали контекст. Началось все с того, что я, в свое время, искал программу для снятия скриншотов и обнаружил на ГитХабе батник, который создавал экзешник с программой для снятия скриншотов. Поскольку я сам как бы и не программист вовсе (чисто ради хобби), то для меня такое показалось чудом чудным — ведь программу можно просто писать в блокноте, а компилировать ее из батника. При таких возможностях, тот же скриншотер, например, можно отредактировать в блокноте так, чтобы скриншот сохранялся сразу на рабочий стол при запуске программы, а не так чтобы программа принимала какие-то аргументы, как это сделано в оригинале. Тогда же я понял, что так можно компилировать простенькие игры, и загуглил такие игры, работающие из консоли, где как раз и была в коллекции эта самая «Змейка». Первая проблема, которая была обнаружена, заключалась в том, что при проигрыше консоль сразу закрывалась, не выдавая результат игры. Пофиксить это было легко, просто добавив ожидание ввода Console.ReadLine(). А вот с описанным выше багом с буквой «а» рядом с едой, пришлось поразбираться сильно подольше. Для упрощения компиляции из блокнота, я добавил в контекстное меню проводника специальный пункт. Это позволяло проще создавать новые экзешники.

Пример как я сделал компиляцию exe файлов из cs файлов
Пример как я сделал компиляцию exe файлов из cs файлов

Чтобы вы понимали о чем речь, дам ссылку на исходный код «Змейки», который я компилировал. Для вычисления бага сначала пробовал менять символы еды и препятствий, подумав что возможно как-то хитро появляется символ «а» потому что он находится рядом с символами препятствий и еды, которые используются в программе. Это никак не сработало и я стал думать дальше. Потом я обратил внимание, что символ появляется только после нажатия на кнопку поворота змейки, и никак не зависит от того куда поворачивать. Долго-долго думал и пробовал еще кучу разных вариантов, и только потом сообразил нажать не на стрелку, а на любую другую клавишу, и сразу же понял в чем причина: рядом с едой появлялась тот символ, какую клавишу я нажимал на клавиатуре. Еда рисуется в определенной позиции, используя конструкцию Console.SetCursorPosition(food.col, food.row), поэтому при нажатии на кнопку считывается ее значение, как я понял, и выводится рядом с позицией курсора, который после вывода еды, смещается на один символ вправо. Чтобы обойти этот баг, я просто решил возвращать курсор на место еды перед считыванием кода клавиши, и это заработало, не пришлось даже менять символ еды на букву «а».

Чтобы еще улучшить пользовательский опыт, я обернул весь код в бесконечный цикл, чтобы окно консоли не закрывалось при проигрыше, а начиналась новая игра. Так же обнаружил, что иногда еда рисовалась на месте препятствия, или наоборот, но обойти это не составило труда, поскольку заметил что в условии сравнения вместо не равно, логичнее было бы ставить равно, как это сделано в предыдущих двух условиях. В итоге код стал выглядеть так (мои добавления в комментариях на русском):

исходный код Змейки
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Collections; using System.Threading;  namespace Snake {     struct Position     {         public int row;         public int col;         public Position(int row, int col)         {             this.row = row;             this.col = col;         }     }      class Program     {         static void Main(string[] args)         { while (true){             byte right = 0;             byte left = 1;             byte down = 2;             byte up = 3;             int lastFoodTime = 0;             int foodDissapearTime = 8000;             int negativePoints = 0;              Position[] directions = new Position[]             {                 new Position(0, 1), // right                 new Position(0, -1), // left                 new Position(1, 0), // down                 new Position(-1, 0), // up             };             double sleepTime = 100;             int direction = right;             Random randomNumbersGenerator = new Random();             Console.BufferHeight = Console.WindowHeight;             lastFoodTime = Environment.TickCount;              List<Position> obstacles = new List<Position>()             {                 new Position(12, 12),                 new Position(14, 20),                 new Position(7, 7),                 new Position(19, 19),                 new Position(6, 9),             };             foreach (Position obstacle in obstacles)             {                 Console.ForegroundColor = ConsoleColor.Cyan;                 Console.SetCursorPosition(obstacle.col, obstacle.row);                 Console.Write("X");             }              Queue<Position> snakeElements = new Queue<Position>();             for (int i = 0; i <= 5; i++)             {                 snakeElements.Enqueue(new Position(0, i));             }              Position food;             do             {                 food = new Position(randomNumbersGenerator.Next(0, Console.WindowHeight),                     randomNumbersGenerator.Next(0, Console.WindowWidth));             }             while (snakeElements.Contains(food) || obstacles.Contains(food));             Console.SetCursorPosition(food.col, food.row);             Console.ForegroundColor = ConsoleColor.Yellow;             Console.Write("@");              foreach (Position position in snakeElements)             {                 Console.SetCursorPosition(position.col, position.row);                 Console.ForegroundColor = ConsoleColor.DarkGray;                 Console.Write("*");             }              while (true)             {                 negativePoints++; Console.SetCursorPosition(food.col, food.row); // чтобы не появлялся символ "а" рядом с едой, поэтому возвращаю курсор на место еды                 if (Console.KeyAvailable)                 {                     ConsoleKeyInfo userInput = Console.ReadKey();                     if (userInput.Key == ConsoleKey.LeftArrow)                     {                         if (direction != right) direction = left;                     }                     if (userInput.Key == ConsoleKey.RightArrow)                     {                         if (direction != left) direction = right;                     }                     if (userInput.Key == ConsoleKey.UpArrow)                     {                         if (direction != down) direction = up;                     }                     if (userInput.Key == ConsoleKey.DownArrow)                     {                         if (direction != up) direction = down;                     }                 }                  Position snakeHead = snakeElements.Last();                 Position nextDirection = directions[direction];                  Position snakeNewHead = new Position(snakeHead.row + nextDirection.row,                     snakeHead.col + nextDirection.col);                  if (snakeNewHead.col < 0) snakeNewHead.col = Console.WindowWidth - 1;                 if (snakeNewHead.row < 0) snakeNewHead.row = Console.WindowHeight - 1;                 if (snakeNewHead.row >= Console.WindowHeight) snakeNewHead.row = 0;                 if (snakeNewHead.col >= Console.WindowWidth) snakeNewHead.col = 0;                  if (snakeElements.Contains(snakeNewHead) || obstacles.Contains(snakeNewHead))                 { //Console.Clear();                     Console.SetCursorPosition(0, 0);                     Console.ForegroundColor = ConsoleColor.Red;                     Console.WriteLine("Game over!");                     int userPoints = (snakeElements.Count - 6) * 100 - negativePoints;                     //if (userPoints < 0) userPoints = 0;                     userPoints = Math.Max(userPoints, 0);                     Console.WriteLine("Your points are: {0}", userPoints); Console.ReadLine(); // чтобы игра не закрывалась автоматически после проигрыша                     break;                 }                  Console.SetCursorPosition(snakeHead.col, snakeHead.row);                 Console.ForegroundColor = ConsoleColor.DarkGray;                 Console.Write("*");                  snakeElements.Enqueue(snakeNewHead);                 Console.SetCursorPosition(snakeNewHead.col, snakeNewHead.row);                 Console.ForegroundColor = ConsoleColor.Gray;                 if (direction == right) Console.Write(">");                 if (direction == left) Console.Write("<");                 if (direction == up) Console.Write("^");                 if (direction == down) Console.Write("V");                   if (snakeNewHead.col == food.col && snakeNewHead.row == food.row)                 {                     // feeding the snake                     do                     {                         food = new Position(randomNumbersGenerator.Next(0, Console.WindowHeight),                             randomNumbersGenerator.Next(0, Console.WindowWidth));                     }                     while (snakeElements.Contains(food) || obstacles.Contains(food));                     lastFoodTime = Environment.TickCount;                     Console.SetCursorPosition(food.col, food.row);                     Console.ForegroundColor = ConsoleColor.Yellow;                     Console.Write("a");                      sleepTime--;                      Position obstacle = new Position();                     do                     {                         obstacle = new Position(randomNumbersGenerator.Next(0, Console.WindowHeight),                             randomNumbersGenerator.Next(0, Console.WindowWidth));                     }                     while (snakeElements.Contains(obstacle) ||                         obstacles.Contains(obstacle) ||                         (food.row == obstacle.row && food.col == obstacle.row)); // тут в последнем условии заменил сравнение не равно на равно                     obstacles.Add(obstacle);                     Console.SetCursorPosition(obstacle.col, obstacle.row);                     Console.ForegroundColor = ConsoleColor.Cyan;                     Console.Write("X");                 }                 else                 {                     // moving...                     Position last = snakeElements.Dequeue();                     Console.SetCursorPosition(last.col, last.row);                     Console.Write(" ");                 }                  if (Environment.TickCount - lastFoodTime >= foodDissapearTime)                 {                     negativePoints = negativePoints + 50;                     Console.SetCursorPosition(food.col, food.row);                     Console.Write(" ");                     do                     {                         food = new Position(randomNumbersGenerator.Next(0, Console.WindowHeight),                             randomNumbersGenerator.Next(0, Console.WindowWidth));                     }                     while (snakeElements.Contains(food) || obstacles.Contains(food));                     lastFoodTime = Environment.TickCount;                 }                  Console.SetCursorPosition(food.col, food.row);                 Console.ForegroundColor = ConsoleColor.Yellow;                 Console.Write("@");                  sleepTime -= 0.01;                  Thread.Sleep((int)sleepTime);             }   Console.Clear(); }         }     } } 

В итоге баги кое-как «починил», но статья же называет что я обнаружил еще кое-что. А все дело в том, что коллекцию исходных кодов тех игр для консоли я скачал одним архивом, и, кажется, тогда даже и не видел видео о том, как они создавались. Зато вот буквально недавно, когда занимался этим дебагом, я наткнулся на репозиторий на Гитхабе, где были ссылки на видео, как создавались эти игры, с исходными кодами. Проверив исходный код змейки я сразу понял, что это и есть «моя» змейка, и мне стало интересно, как же так вышло, что разработчик с таким багом опубликовал игру на ГитХаб. Смотрю видео, и вижу, что у него такого бага и нет даже. Мне тогда мне снова стало интересно, что же это такое. По началу я подумал что проблема заключается в том, что я компилировал исходный код из меню проводника. Но нет, с батником на ноутбуке все работало точно так же, буква «а» была. Потом я закинул исходный код на виртуалку на компе с установленным на ней Visual Studio, скомпилировал игру и уже не увидел никаких багов (!). Хм… проблема в компиляции, наверное. Компилирую на компе из батника — бага нет… Стал думать, что проблема в версии компилятора, но обменяв экзешники с ноута и с компа, я увидел что на ноуте баг есть, а на компе нет. Для пущей уверенности, что дело в особенности клавиатуры ноутбука, я подключил обычную клаву к нему, но баг все же проявлялся и на обычной клаве… Запускаю игру на удаленной машине, через удаленный рабочий стол с компьютера и ноутбука и не появляется буквы «а», а на другой виртуалке которая есть и на ноуте, и на компе появляется только на ноуте. Короче, как я понимаю проблема именно в ноуте, и возможно именно конкретный ноутбук (но это нужно еще проверить).

Вот такая получилась история. В целом мне было интересно поразбираться с такой фигней, а еще захотелось поделиться этим с вами. Сильно не пинайте, просто мне интересно, возможно кто-то подскажет в чем возможна причина такого поведения.


ссылка на оригинал статьи https://habr.com/ru/post/684064/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *