Learning Scala: Part 1 - The Snake Game


Hi Habr! When I learn a new language, I usually make a snake on it. Maybe some newbie who also studies Scala will be interested in the code of another newbie in this PL. For experienced rockers, my first Scala code is likely to be sad. For details, welcome to cat.

Content


  • Learning Scala: Part 1 - The Snake Game

References


Source code

About the game


To work with graphics, libGdx was used on the LWJGL backend. Management takes place using the arrows on the keyboard.

Domain


The case class was used to simulate the drynesses because they are not mutable, are compared by value and are generally similar to the record from Haskell.

Point in 2D space:

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

Direction of travel. Using the matching pattern, these classes can be used as 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

The frame within which the snake moves

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)
  }
}

Food for the snake:

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))
  }
}

Snake:

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)
  }
}

A game:

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


A class that collects information about pressed buttons

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
  }
}

The class that controls the display of the game:

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()
  }
}

The main entry point of the game:

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)
}

All Articles