Изучаю 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)
}