Learning Scala: Parte 1 - El juego de la serpiente


Hola habr Cuando aprendo un nuevo idioma, generalmente hago una serpiente en él. Tal vez algún novato que también estudie Scala esté interesado en el código de otro novato en este PL. Para los rockeros experimentados, es probable que mi primer código Scala sea triste. Para más detalles, bienvenido a cat.

Contenido


  • Learning Scala: Parte 1 - El juego de la serpiente

Referencias


Código fuente

Sobre el juego


Para trabajar con gráficos, se usó libGdx en el backend LWJGL. La gestión se lleva a cabo utilizando las flechas del teclado.

Dominio


La clase de caso se usó para simular sequedades porque no son mutables, se comparan en valor y generalmente son similares al registro de Haskell.

Punto en espacio 2D:

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

Dirección de viaje. Usando el patrón coincidente, estas clases se pueden usar como enumeración:


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

El marco dentro del cual se mueve la serpiente

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

Comida para la serpiente:

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

Serpiente:

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

Un juego:

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

Presentación


Una clase que recopila información sobre botones presionados

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

La clase que controla la visualización del juego:

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

El principal punto de entrada del juego:

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