
Привет Хабр! Когда я изучаю новый язык я обычно делаю на нем змейку. Может какому-нибудь новичку который тоже изучает 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) }
Благодарности
Спасибо Areso за найденные грамматические ошибки
