Игра Змейка на F#

Все исходники /  Язык программирования F# /  OS Windows /  Desktop /  Исходники приложений / Игра Змейка на F#

Консольная игра "Змейка"

Анимация змейки в консольной игре

Консольная игра "Змейка" на языке программирования F#. Настоящая змея похожа на цепочку, звенья которой поочерёдно проходят то место, где была голова змеи. В данной игрушке змейка также цепочка из специальных символов, при перемещениях, символы змейки следуют по траектории движения головы.

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

Статья подробно описывает работу программного кода игры на F#, внизу прикреплён архивный файл исходника. В исходнике уже всё готово для модификации игры под свои фантазии. Длину змейки можно изменять в коде. Открытые методы классов позволяют указывать направление движения и определять в любой момент координаты змеи.

Консоль

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

Поскольку Console статический класс, в исходнике он применяется внутри классов Snake и Game как глобальная переменная. Это не даёт возможность создавать чистые функции, но таким образом упрощается программный код работы с консольным окном. Создавать код полного управления над консолью вряд ли будет рациональным решением, разве что для выполнения спецзадания или развития программистских навыков.

Классы и модули игры

Исходник игры состоит из двух классов и 3-х модулей F#:
  • Класс Snake отвечает за прорисовку символов змейки и предоставляет метод передвижения тела змеи на один шаг в указанном направлении. Находится в одноимённом модуле-файле.
  • Класс Game управляет игрой и обеспечивает непрерывную анимацию движения змейки. В классе присутствует закрытый метод определения текущих координат головы змейки, который предназначен для создания различных событий игры. Расположен в модуле-файле с названием также Game
  • Модуль Program служит для запуска программы и формирует визуальный интерфейс игры, имеет программный код управления игрой посредством клавиш клавиатуры.
Общие сведения о классах F# можно дополнительно посмотреть на страницах конструкторы классов и Классы2

Класс Snake

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

Упростить реализацию стиля движения настоящей змеи позволил список F# - упорядоченная серия элементов одного типа. Элементами списка являются тип Records (Записи) хранения координат каждого символа змейки.

type pos = {row: int; col: int}

Запись не является членом класса Snake, но находится с ним в одном модуле. Список listPos член класса и хранит элементы типа pos.

let mutable listPos: pos list = [pos1; pos2; … posN ]

Максимальное количество элементов (а равно длину змейки) можно выбрать любое, не превышая габариты консольного окна. Хотя язык F# предпочитает неизменяемые значения (но не запрещает изменяемые), в данном случае изменяемость упрощает программный код. listPos - это закрытый и единственный изменяемый элемент, область его применения ограничена определением класса Snake.

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

Принцип передвижения компьютерной змейки такой: удаляется первый символ списка, на место первого сдвигается второй элемент, на место второго - третий элемент позиции и так далее. Затем к такому списку добавляется элемент-голова с новыми координатами, увеличенными на единицу в сторону выбранного направления движения. Таким образом количество элементов в списке всегда постоянно. На основе нового списка позиций рисуется змейка в положении на один шаг вперёд головой.

listPos1 = [pos1; pos2; pos3; pos4; pos5]
      listPos2 = [pos2; pos3; pos4; pos5; pos6]
            listPos3 = [pos3; pos4; pos5; pos6; pos7]

Если циклически повторять данный программный алгоритм, то все символы гарантировано перемещаются по траектории движения головного символа. Визуально создаётся естественность движения символьной змейки, как у настоящей змеи: когда хвост всегда следует по траектории головы.

Код класса 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

Класс 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. В составе исходника есть программа игры для ознакомительного тестирования. Исходный код Змейки оставляет большие просторы для доработки игры. Исходник игры можно дополнять различными логические трудности при движении змейки и различные события проигрыша и выигрыша в данной игре.

Скачать исходник