Learning Scala: Bagian 1 - The Snake Game


Hai Habr! Ketika saya belajar bahasa baru, saya biasanya membuat ular di atasnya. Mungkin beberapa pemula yang juga mempelajari Scala akan tertarik pada kode pemula lain di PL ini. Untuk rocker berpengalaman, kode Scala pertama saya cenderung sedih. Untuk detailnya, selamat datang di kucing.

Kandungan


  • Learning Scala: Bagian 1 - The Snake Game

Referensi


Kode sumber

Tentang permainan


Untuk bekerja dengan grafik, libGdx digunakan pada backend LWJGL. Manajemen berlangsung menggunakan panah pada keyboard.

Domain


Kelas kasus digunakan untuk mensimulasikan kekeringan karena mereka tidak bisa berubah, dibandingkan nilainya dan umumnya mirip dengan catatan dari Haskell.

Titik dalam ruang 2D:

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

Arah perjalanan. Menggunakan pola yang cocok, kelas-kelas ini dapat digunakan sebagai 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

Bingkai di mana ular itu bergerak

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

Makanan untuk ular:

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

Ular:

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

Permainan:

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

Presentasi


Kelas yang mengumpulkan informasi tentang tombol yang ditekan

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

Kelas yang mengontrol tampilan permainan:

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

Titik masuk utama permainan:

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