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


    Благодарности


    Спасибо Areso за найденные грамматические ошибки
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 5

      +6
      За подробностями добро пожаловать под кат

      А где подробности? В этих исходниках не хватает статьи.
      Для таких заметок люди придумали Github. Именно там принято выкладывать код без объяснений.

        0

        Как стартовая точка сойдёт, но теперь нужно провести к более преемлему виду. Класс Game смешивает и логику и данные, так делать не хорошо. Вынесите в отдельный класс, который будет выполнять это. В остальных классах тоже бы вынести. Case class без атрибутов лучше заменить на case object.

          0
          Да что уж там, класс Food зачем-то принимает Random и зачем-то знает про игровое поле.
            0
            Спасибо. Вынес всю логику в функции отдельные. Посмотрите пожалуйста на гитхабе и скажите как еще можно улучшить.
            +2
            У опытных скалистов скорее всего мой первый код на Scala вызовет грусть.

            Код вполне норм (на первый взгляд), зря вы настолько самокритично. Но тут правда не хватает «самой статьи», какое-никакое объяснение, что и для чего, в каком порядке, может, видеоролик демо. Просто исходники на этот ресурс не принято выкладывать. :-)

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое