Изучаю Scala: Часть 1 — Игра змейка

от автора

Привет Хабр! Когда я изучаю новый язык я обычно делаю на нем змейку. Может какому нибудь новичку который тоже изучает Scala будет интересен код другого новичка в этом ЯП. У опытных скалистов скорее всего мой первый код на Scala вызовет грусть. За подробностями добро пожаловать под кат.

Содержание

  • Изучаю Scala: Часть 1 — Игра змейка

Ссылки

Исходники

Об игре

Для работы с графикой использовался libGdx на LWJGL бекенде. Управление происходит при помощи стрелочек на клавиатуре.

Domain

Для моделирования сушностей использовались case class потому что они не изменяемые, cсравниваются по значению и в целом похожи на record из Haskell.

Точка в 2д пространстве:

case class Point(x: Int, y: Int)

Направление движения. С помощью паттерн матчинга эти классы можно использовать как enum:

     //В каком нибудь Си подобном языке аналогом этому был бы     //enum Direction     //{     //    Up,     //    Down,     //    Right,     //    Left     //} sealed abstract class Direction  case object Up extends Direction  case object Down extends Direction  case object Right extends Direction  case object Left extends Direction

Фрейм в пределах которого движется змейка

case class Frame(min: Point, max: Point) {   def points = {     for (i <- min.x until max.x + 1;          j <- min.y until max.y + 1          if i == min.x ||            i == max.x ||            j == min.y ||            j == max.y)       yield Point(i, j)   } }

Еда для змейки:

case class Food(body: Point, random: Random) {   def moveRandomIn(frame: Frame): Food = {     val x = random.between(frame.min.x + 1, frame.max.x)     val y = random.between(frame.min.y + 1, frame.max.y)     copy(body = Point(x, y))   } }

Змейка:

case class Snake(body: List[Point], direction: Direction) {   def move(): Snake = {     val point = direction match {       case Up() => body.head.copy(y = body.head.y + 1)       case Down() => body.head.copy(y = body.head.y - 1)       case Left() => body.head.copy(x = body.head.x - 1)       case Right() => body.head.copy(x = body.head.x + 1)     }     copy(body = point :: body.filter(p => p != body.last))   }    def turn(direction: Direction): Snake = {     copy(direction = direction)   }    def eat(food: Food): Snake = {     copy(body = food.body :: body)   }    def canEat(food: Food): Boolean = {     food.body == body.head   }    def headIsIn(frame: Frame): Boolean = {     body.head.x < frame.max.x &&       body.head.y < frame.max.y &&       body.head.x > frame.min.x &&       body.head.y > frame.min.y   }    def isBitTail() = {     body.tail.exists(p => p == body.head)   } }

Игра:

package domain  case class Game(food: Food, snake: Snake, frame: Frame, elapsedTime: Float, start: Snake) {   val framePoints = frame.points.toList    def handle(input: List[Direction]): Game = {     if (input.isEmpty) {       this     } else {       copy(snake = input.foldLeft(snake)((s, d) => s.turn(d)))     }   }    def update(deltaTime: Float): Game = {     val elapsed = elapsedTime + deltaTime     if (elapsed > 0.1) {       val game = copy(snake = snake.move(), elapsedTime = 0)       if (!game.snake.headIsIn(frame)) {         game.reset()       } else if (game.snake.isBitTail()) {         game.reset()       } else if (game.snake.canEat(food)) {         game.copy(snake = snake.eat(food), food = food.moveRandomIn(frame))       } else {         game       }     } else {       copy(elapsedTime = elapsed)     }   }    def reset() = copy(snake = start)    def points: List[Point] = {     (food.body :: snake.body) ::: framePoints   } } 

Presentation

Класс который собирает информацию о нажатых кнопках

import com.badlogic.gdx.{InputAdapter}  class InputCondensate extends InputAdapter {    private var keys: List[Int] = Nil    def list: List[Int] = keys.reverse    def clear(): Unit = {     keys = Nil   }    override def keyDown(keycode: Int): Boolean = {     keys = keycode :: keys     true   } }

Класс управляющий отображением игры:

import com.badlogic.gdx.Input.Keys import com.badlogic.gdx.graphics.glutils.ShapeRenderer import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType import com.badlogic.gdx.graphics.{Color, GL20} import com.badlogic.gdx.{Game, Gdx}  class SnakeGame(var game: domain.Game, val sizeMultiplayer: Float) extends Game {   lazy val prs = new InputCondensate   lazy val shapeRenderer: ShapeRenderer = new ShapeRenderer()    override def create(): Unit = {     Gdx.input.setInputProcessor(prs)   }    override def render(): Unit = {     game = game       .handle(prs.list.map(i => i match {         case Keys.UP => domain.Up()         case Keys.DOWN => domain.Down()         case Keys.LEFT => domain.Left()         case Keys.RIGHT => domain.Right()       }))       .update(Gdx.graphics.getDeltaTime())      prs.clear()     Gdx.gl.glClearColor(1, 1, 1, 1)     Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT)     shapeRenderer.setColor(Color.BLACK)     shapeRenderer.begin(ShapeType.Filled)     for (p <- game.points)       shapeRenderer.circle(p.x * sizeMultiplayer, p.y * sizeMultiplayer, sizeMultiplayer / 2)     shapeRenderer.end()   } }

Главная точка входа игры:

import com.badlogic.gdx.backends.lwjgl.{LwjglApplication, LwjglApplicationConfiguration}  import scala.util.Random  object Main extends App {   val config = new LwjglApplicationConfiguration   config.title = "Scala Snake Game"   config.width = 300   config.height = 300   val food = domain.Food(domain.Point(4, 4), new Random())   val frame = domain.Frame(domain.Point(0, 0), domain.Point(30, 30))   val snake = domain.Snake(domain.Point(5, 5) :: domain.Point(6, 6) :: domain.Point(7, 7) :: Nil, domain.Right())   val game = domain.Game(food, snake, frame, 0, snake)   new LwjglApplication(new SnakeGame(game, 10), config) } 

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


Комментарии

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

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