Консольная игра "Змейка" на языке программирования F#. Настоящая змея похожа на цепочку, звенья которой поочерёдно проходят то место, где была голова змеи. В данной игрушке змейка также цепочка из специальных символов, при перемещениях, символы змейки следуют по траектории движения головы.
В игре реализована анимация движения змейки по строкам и столбцам консольного окна. Управлять направлением можно соответствующими клавишами-стрелками клавиатуры компьютера, создавая фигурные траектории перемещения. Пока правила предельно просты - необходимо только управлять движением забавной змейки.
Статья подробно описывает работу программного кода игры на F#, внизу прикреплён архивный файл исходника. В исходнике уже всё готово для модификации игры под свои фантазии. Длину змейки можно изменять в коде. Открытые методы классов позволяют указывать направление движения и определять в любой момент координаты змеи.
Для визуализации игры используется консольное окно посредством статического класса Console. Консоль представляет собой простейшее окно и в основном применяется для ввода и вывода текстовых символов. В игре тело змеи формируют специальные символы, длину змеи можно выбирать любую, в пределах доступности размера консольного окна. Необходимо отметить, что размер консольного окна измеряется в символьных ячейках, упорядоченных по строкам и столбцам, но не в пикселях.
Поскольку Console статический класс, в исходнике он применяется внутри классов Snake и Game как глобальная переменная. Это не даёт возможность создавать чистые функции, но таким образом упрощается программный код работы с консольным окном. Создавать код полного управления над консолью вряд ли будет рациональным решением, разве что для выполнения спецзадания или развития программистских навыков.
Исходник игры состоит из двух классов и 3-х модулей F#:
Класс Snake отвечает за прорисовку символов змейки и предоставляет метод передвижения тела змеи на один шаг в указанном направлении. Находится в одноимённом модуле-файле.
Класс Game управляет игрой и обеспечивает непрерывную анимацию движения змейки. В классе присутствует закрытый метод определения текущих координат головы змейки, который предназначен для создания различных событий игры. Расположен в модуле-файле с названием также Game
Модуль Program служит для запуска программы и формирует визуальный интерфейс игры, имеет программный код управления игрой посредством клавиш клавиатуры.
Общие сведения о классах F# можно дополнительно посмотреть на страницах конструкторы классов и Классы2
Класс, превращающий последовательность напечатанных символов в динамичное тело змейки. При движениях и поворотах любой конфигурации все символы повторяют траекторию головы, копируя изящный стиль передвижения настоящей змеи.
Упростить реализацию стиля движения настоящей змеи позволил список F# - упорядоченная серия элементов одного типа. Элементами списка являются тип Records (Записи) хранения координат каждого символа змейки.
type pos = {row: int; col: int}
Запись не является членом класса Snake, но находится с ним в одном модуле. Список listPos член класса и хранит элементы типа pos.
let mutable listPos: pos list = [pos1; pos2; … posN ]
Максимальное количество элементов (а равно длину змейки) можно выбрать любое, не превышая габариты консольного окна. Хотя язык F# предпочитает неизменяемые значения (но не запрещает изменяемые), в данном случае изменяемость упрощает программный код. listPos - это закрытый и единственный изменяемый элемент, область его применения ограничена определением класса Snake.
Список listPos хранит текущие координаты символов змейки в обратном порядке, т. е. хвост - это первый элемент, голова змеи последний элемент. Физически цепочка символов движется последним элементом вперёд, но визуально змейка движется естественно, головой вперёд.
Принцип передвижения компьютерной змейки такой: удаляется первый символ списка, на место первого сдвигается второй элемент, на место второго - третий элемент позиции и так далее. Затем к такому списку добавляется элемент-голова с новыми координатами, увеличенными на единицу в сторону выбранного направления движения. Таким образом количество элементов в списке всегда постоянно. На основе нового списка позиций рисуется змейка в положении на один шаг вперёд головой.
Если циклически повторять данный программный алгоритм, то все символы гарантировано перемещаются по траектории движения головного символа. Визуально создаётся естественность движения символьной змейки, как у настоящей змеи: когда хвост всегда следует по траектории головы.
Код класса Snake максимально упрощен, открытый интерфейс взаимодействия с классом состоит из одного метода перемещения Move(..) и одного свойства Pos для получения текущей позиции головы змейки. Остальной программный код класса Snake состоит из закрытых вспомогательных значений и небольших функций.
Полный листинг программного кода класса Snake:
module Snake
open System
// Элемент-звено змейки, содержит свои координаты
type pos = {row: int; col: int}
// Класс змейки состоящей из звеньев.
// Голова змейки это последний символ в цепочке.
// Физически движение задом вперёд, но
// визуально головой вперёд.
type Snake() =
// Символ тела змейки.
let symbolChain = "●"
// Символ головы змейки.
let symbolHead = "○"
// Данные позиций звеньев тела змейки.
let mutable listPos: pos list = []
// Индекс последнего звена змейки,
// Это звено является головой.
let lastIndex() = listPos.Length - 1
// Рисование головы змейки
let drawHead() =
Console.SetCursorPosition(listPos.[lastIndex()].row, listPos.[lastIndex()].col)
Console.Write(symbolHead)
// Рисование змейки с головой
let drawSnake() =
for d in listPos do
Console.SetCursorPosition(d.row, d.col)
Console.Write(symbolChain)
drawHead()
// Стирание первого символа в списке звеньев,
// первый символ - это хвост змейки.
let erase() =
Console.SetCursorPosition(listPos.Item(0).row, listPos.Item(0).col)
Console.Write(" ")
// Передвижение на один шаг змейки в требуемом направлении.
let moveSnake direction =
// Получаем данные позиции последнего звена.
let posHead = listPos.Item(lastIndex())
// Новая позиция головы змейки.
let mutable x = 0
let mutable y = 0
// Напоминает конструкцию switch-case C#, но дополнительно
// ещё можно устанавливать условия совпадения.
match direction with
// Вправо, если змейка не достигла стороны окна консоли.
| 1 when listPos.[lastIndex()].row < (Console.WindowWidth - 1) -> x <- 1
// Влево, если можно.
| 2 when listPos.[lastIndex()].row > 0 -> x <- -1
// Вниз, если можно.
| 3 when listPos.[lastIndex()].col < (Console.WindowHeight - 1) -> y <- 1
// Вверх, если можно.
| 4 when listPos.[lastIndex()].col > 0 -> y <- -1
| _ -> ()
// Двигаемся только если получено направление.
if x <> 0 || y <> 0 then
// Стираем последнее звено.
erase()
// Для головы новая координата в выбранном направлении.
let posNew = {row = posHead.row + x; col = posHead.col + y}
// Удаляем хвостовое звено, голову передвигаем на новое место
// и тем самым перемещаем все звенья на один шаг вперёд.
listPos <- listPos.Tail @ [posNew]
// Рисуем змейку
drawSnake()
// Инициализация цепочки символов и рисование змейки.
do
for i = 0 to 100 do
let pos = {row = i; col = 0}
listPos <- listPos @ [pos]
drawSnake()
// Открытый метод перемещения змейки в указанном направлении.
member x.Move (dir: int) = moveSnake dir
// Открытое свойство получения текущей координаты головы змейки.
member x.Pos with get() = listPos.[lastIndex()]
Класс Game обеспечивает непрерывную анимацию движения змейки в выбранном направлении. Непрерывную анимацию обеспечивает таймер. Таймер инкапсулирован (скрыт) в классе, а для управления движениями змейки созданы открытые методы Right(), Left(), Down(), Up(), инициирующие движения в выбранном направлении.
Изменяемое поле let mutable direction временно хранит внутри объекта класса значение выбранного направления движения. При значениях 1, 2, 3, 4 переменной direction событие таймера timerClick (...) начинает пошагово двигать змейку в выбранном направлении. От интервала тиков таймера зависит скорость перемещения змейки.
Закрытый метод gameStop(…) предназначен для создания различных событий игры, например: наезд на случайно размещенных по полю символов, окончание игры в случае касания границ окна и других игровых действий. Для примера, в данном исходном коде, запрограммированно движение змейки по периметру консольного окна в направлении часовой стрелки.
Программный код класса Game:
module Game
open System
open System.Timers
type Game() =
// Таймер инициируется при создании объекта класса.
// Значение Interval - это скорость анимации змейки.
let timer =
new Timer(Interval = 150, Enabled = true)
// Змейка инициируется при создании объекта класса.
let snake = Snake.Snake()
// Поле временного хранения значения выбранного направления движения.
let mutable direction = 0
// Функция получения текущих координат головы змейки.
// В данном примере содержит логику движения змейки по кругу.
let gameStop (pos: Snake.pos) =
if pos.row = 0 && pos.col = 0 then
direction <- 1
if pos.row = Console.WindowWidth - 1 && pos.col = 0 then
direction <- 3
if pos.row = Console.WindowWidth - 1
&& pos.col = Console.WindowHeight - 1 then
direction <- 2
if pos.row = 0 && pos.col = Console.WindowHeight - 1 then
direction <- 4
let timerClick (source: Object) (e: System.Timers.ElapsedEventArgs) : unit =
snake.Move direction
// Проверка события остановки игры.
gameStop(snake.Pos)
// Для надёжного скрытия курсора.
// При изменении размеров окна курсор вновь появляется.
Console.CursorVisible <- false
do
// Присвоение таймеру обработчика событий.
timer.Elapsed.AddHandler(timerClick)
// Открытые методы взаимодействия с пользователем.
member x.Right() = direction <- 1
member x.Left() = direction <- 2
member x.Down() = direction <- 3
member x.Up() = direction <- 4
В модуле Program происходит инициализация игры, запуск программы игры и управление передвижениями змейки. Цвет поля игры изменён на желтый, змейка тёмно-красного цвета. Чтобы специальные символы отображались корректно, добавлена поддержка кодировки UTF-8. Для обновления консольного окна в новый цвет необходимо вызвать метод консоли Console.Clear(), иначе окрашивается только задний фон символов.
Запуск игры осуществляется любой из клавиш стрелок-направления. Действие игры происходит в бесконечном цикле while (...) do. Прослушиваются все клавиши и при нажатии клавиш направления ← → и ↑ ↓, происходит смена направления движения змейки. При нажатии на клавишу пробел, игра останавливается и окно закрывается.
Программный код модуля Program:
open System
// Выбираем цвета элементов игры.
Console.BackgroundColor <- ConsoleColor.Yellow
Console.ForegroundColor <- ConsoleColor.DarkRed
// Включаем поддержку UTF-8
Console.OutputEncoding <- System.Text.Encoding.UTF8
// Окрашиваем в новый цвет полностью окно консоли.
Console.Clear()
// Инициализация функциональности игры.
let game = Game.Game()
[]
let main argv =
let mutable res = ConsoleKey.A
while res <> ConsoleKey.Spacebar do
// Прослушка клавиш клавиатуры.
// Значение true запрещает отображать символ клавиши.
let res1 = Console.ReadKey(true)
if res1.Key = ConsoleKey.RightArrow then game.Right()
if res1.Key = ConsoleKey.LeftArrow then game.Left()
if res1.Key = ConsoleKey.DownArrow then game.Down()
if res1.Key = ConsoleKey.UpArrow then game.Up()
res <- res1.Key
0 // return an integer exit code
Исходный код игры "Змейка" написан на языке F# в интегрированной среде программирования MS Visual Studio 2022 на платформе .NET5. В составе исходника есть программа игры для ознакомительного тестирования. Исходный код Змейки оставляет большие просторы для доработки игры. Исходник игры можно дополнять различными логические трудности при движении змейки и различные события проигрыша и выигрыша в данной игре.