Речь идет о обычной "Змейке", написанной на 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, скомпилировал игру и уже не увидел никаких багов (!). Хм... проблема в компиляции, наверное. Компилирую на компе из батника - бага нет... Стал думать, что проблема в версии компилятора, но обменяв экзешники с ноута и с компа, я увидел что на ноуте баг есть, а на компе нет. Для пущей уверенности, что дело в особенности клавиатуры ноутбука, я подключил обычную клаву к нему, но баг все же проявлялся и на обычной клаве... Запускаю игру на удаленной машине, через удаленный рабочий стол с компьютера и ноутбука и не появляется буквы "а", а на другой виртуалке которая есть и на ноуте, и на компе появляется только на ноуте. Короче, как я понимаю проблема именно в ноуте, и возможно именно конкретный ноутбук (но это нужно еще проверить).

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

Комментарии (14)


  1. build_your_web
    23.08.2022 07:43

    В файле невидимый utf символ?


    1. Weron2 Автор
      23.08.2022 09:12

      Не, точно нет)


  1. DanilinS
    23.08.2022 07:52

    Проблема в компиляции? Пробовали собранную программу переносить? Или нюансы разных консолей?

    Если проблема в компиляции - пробовали сделать декомпиляцию с последующим сравнением?


    1. Weron2 Автор
      23.08.2022 08:47
      +1

      Да, я пробовал переносить скомпилированную на ноутбуке программу на ПК и там бага не было, а потом пробовал ту что компилировал на ПК перенести на ноут и там баг проявлялся. Поэтому и решил что проблема не в компиляции. В обоих случаях размер программы после компиляции совпадал вплоть до байта.


      1. Dimsml
        23.08.2022 09:36
        -1

        А если подключить USB клавиатуру к ноутбуку? Может контроллер клавиатуры в нотбуке немного нестандартный?


  1. AndreyDmitriev
    23.08.2022 08:10
    +2

    Минуточку, у вас же чёрным по белому в коде (162-я строка):
    Console.Write("a");
    Ну а то, что оно не везде проявляется - это может быть с разными таймингами и разным временем реакции связано. У меня не воспроизводится, но если я сделаю вот так Console.Write("aaa"); то вот они ваши ааа без первой буквы

    Дальше лень разбираться, если честно. Я обычно говорю "чудес на свете не бывает, в компьютере только биты да байты, и любому наблюдаемому феномену обычно есть рациональное объяснение, а всё остальное можно списать на космическое излучение - прилетает высокоэнергетическая частица и вот - получите "а" в консоли ;).


    1. Weron2 Автор
      23.08.2022 08:55

      Мой косяк что оставил это после своих экспериментов. Но именно этот символ "а" ни на что не влияет, и тем более что в исходном (изначальном) коде никакого символа "а" не было вообще. Тайминги думаю не причем, не настолько сложная программа, хотя я и сам было подумывал про что-то подобное. Вообще очень похоже что проблема повторяется именно на том рабочем ноуте HP Windows 7 x64.

      PS три буквы а делал тоже кстати, но они показываются сразу, а потом прибавлялась еще одна четвертая уже справа от тех трех


  1. hard2018
    23.08.2022 08:27

    Стал думать, что проблема в версии компилятора, но обменяв экзешники с ноута и с компа, я увидел что на ноуте баг есть, а на компе нет.
    Короче, как я понимаю проблема именно в ноуте, и возможно именно конкретный ноутбук (но это нужно еще проверить).

    Есть такая либа .NET Framework. Она устанавливается со средой разработки или самостоятельно.
    Её версии на компе и на ноуте проверить не пробовали? Т.к. ваша приложуха работает под её посредством.


    1. Weron2 Автор
      23.08.2022 09:11

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


      1. Breathe_the_pressure
        23.08.2022 09:49

        Если на виртуальной машине на ноутбуке такой баг есть, я бы проверил драйвера видеокарты, ну винду переставил как вариант. Ради змейки :)


  1. Dimsml
    23.08.2022 09:04

    У вас батник занимается тем, что вызывает csc.exe, входящий в .NET Framework. Framework у вас либо шёл в комплекте с системой, либо был установлен вместе с каким-то софтом, причём с каждой версией фреймворка идёт свой компилятор. Если зайти в папку с фреймворками, там для каждой версии своя папка, в каждой есть компиляторы для C# и VB.NET.

    Как его использовать руками из командной строки можно почитать вот тут, например:

    https://docs.microsoft.com/en-us/previous-versions/ms379563(v=vs.80)

    Чтобы оно работало, нужно либо каждый раз прописывать полностью путь до компилятора нужной версии, либо добавить его папку в системную переменную PATH.

    https://docs.microsoft.com/en-us/previous-versions/office/developer/sharepoint-2010/ee537574(v=office.14)

    Кроме того, в Notepad++ можно руками добавить csc.exe для компиляции, правда сами авторы рекомендуют использовать бат-файл.

    https://community.notepad-plus-plus.org/topic/20688/faq-desk-how-do-i-use-notepad-to-compile-my-source-code-or-convert-my-text


  1. Nikoobraz
    23.08.2022 09:54

    Комп, семерка_х64, как и на вашем рабочем ноуте. Баг есть. Возможно дело в операционной системе.


  1. smind
    23.08.2022 10:44
    +3

    декомпилируйте 2 версии, с баком и без бага и сравните 2 кода.


  1. insecto
    23.08.2022 19:46

    Не знаю как в винде, но обычно у всех терминалов можно включать или выключать режим эхо. Этот режим указывает, будет ли терминал сразу же выводить на экран символ введённый с клавиатуры.