Code Sketch


Snake functional gaming
By: Alex
Category: Programming
cleari()
drawStage(white)

// globals
val canvas = canvasBounds
val collisionDetector = new net.kogics.kojo.fpgaming.CollisionDetector()

// the snake is represented as squares (or blocks)
val blockSize = 20


// represents where's the snake's head pointing to
trait Orientation
object Orientation {
    case object Right extends Orientation
    case object Down extends Orientation
    case object Left extends Orientation
    case object Up extends Orientation
}

// represents a location in the game area
case class VirtualLocation(x: Int, y: Int)
case class CanvasLocation(x: Double, y: Double)

// in order to simplfy calculations, let's create a virtual grid
// represented in blocks
case class GameArea(height: Int, width: Int) {

    // maps a virtual VirtualLocation to the canvas VirtualLocation
    def toCanvasPosition(virtual: VirtualLocation): CanvasLocation = {
        val x = collisionDetector.minX + (virtual.x * blockSize).toDouble
        val y = collisionDetector.minY + (virtual.y * blockSize).toDouble
        CanvasLocation(x = x, y = y)
    }

    def center: VirtualLocation = VirtualLocation(
        x = (width / 2).toInt,
        y = (height / 2).toInt
    )
}

// the snake has a head pointing in a specific direction and a tail
case class Snake(
    orientation: Orientation,
    head: VirtualLocation,
    tail: List[VirtualLocation]
    ) {

    // update's the snake's head orientation
    def newOrientation(target: Orientation): Snake = {
        // there are invalid moves which cause no effect:
        // - up can't go down
        // - down can't go up
        // - left can't go right
        // - right can't go left
        val invalid = List(
            Orientation.Left -> Orientation.Right,
            Orientation.Right -> Orientation.Left,
            Orientation.Up -> Orientation.Down,
            Orientation.Down -> Orientation.Up
        )

        if (invalid.contains(orientation -> target)) {
            this // invalid move, keep the same
        } else {
            copy(orientation = target)
        }
    }

    // move's the snake head by the given delta
    def move(deltaX: Int, deltaY: Int, appleLocation: VirtualLocation): SnakeMoved = {
        val newHead = VirtualLocation(
            x = (head.x + deltaX + gameArea.width) % gameArea.width,
            y = (head.y + deltaY + gameArea.height) % gameArea.height
        )
        
        val appleEaten = newHead == appleLocation
        val newTail = {
            if (appleEaten) head :: tail
            else (head :: tail).dropRight(1)
        }
        
        val newSnake = copy(
            head = newHead,
            tail = newTail
        )

        val crashed = newTail.contains(newHead)
        if (crashed) SnakeMoved.Crashed(this)
        else SnakeMoved.NewLocation(newSnake, appleEaten)
    }
}

// the effect after moving the snake
sealed trait SnakeMoved
object SnakeMoved {
    case class Crashed(snake: Snake) extends SnakeMoved
    case class NewLocation(snake: Snake, appleEaten: Boolean) extends SnakeMoved
}

case class GameState(snake: Snake, apple: VirtualLocation, crashed: Boolean)


sealed trait GameEvent
object GameEvent {
    // nothing to do this time
    case object None extends GameEvent

    // game progresses normally, move snake
    case object Tick extends GameEvent

    sealed trait OrientationUpdated extends GameEvent
    object OrientationUpdated {
        case object MoveRight extends OrientationUpdated
        case object MoveDown extends OrientationUpdated
        case object MoveLeft extends OrientationUpdated
        case object MoveUp extends OrientationUpdated
    }
}


val gameArea = GameArea(
    height = (collisionDetector.maxY - collisionDetector.minY).toInt / blockSize,
    width = (collisionDetector.maxX - collisionDetector.minX).toInt / blockSize
)

// creates an apple in a random VirtualLocation, making sure that it is
// not in the snake's body
def createApple(snake: Snake): VirtualLocation = {
    val apple = VirtualLocation(
        x = random(0, gameArea.width),
        y = random(0, gameArea.height)
    )

    // crea una apple que no este donde la Snake
    if (snake.head == apple || snake.tail.contains(apple))
      createApple(snake)
    else
      apple
}

def initialGameState: GameState = {
    val snake = Snake(
        head = gameArea.center,
        tail = (1 to 3).map { delta =>
            VirtualLocation(
                x = gameArea.center.x - delta,
                y = gameArea.center.y
            )
        }.toList,
        orientation = Orientation.Right
    )
    GameState(
        snake = snake,
        apple = createApple(snake),
        crashed = false
    )
}

def updateGameState(gameState: GameState, event: GameEvent): GameState = {
    event match {
        // keep the same, this can occur when pressing an unused key
        case GameEvent.None => gameState

        // update orientation
        case e: GameEvent.OrientationUpdated =>
            val newOrientation = e match {
                case GameEvent.OrientationUpdated.MoveDown => Orientation.Down
                case GameEvent.OrientationUpdated.MoveUp => Orientation.Up
                case GameEvent.OrientationUpdated.MoveLeft => Orientation.Left
                case GameEvent.OrientationUpdated.MoveRight => Orientation.Right
            }
            gameState.copy(
                snake = gameState.snake.newOrientation(newOrientation)
            )

        // move snake
        case GameEvent.Tick =>
            // computes the deltas based on the snake's orientation
            val (dx, dy) = gameState.snake.orientation match {
                case Orientation.Down => (0, -1)
                case Orientation.Up => (0, 1)
                case Orientation.Right => (1, 0)
                case Orientation.Left => (-1, 0)
            }

            gameState.snake.move(deltaX = dx, deltaY = dy, gameState.apple) match {
                case SnakeMoved.Crashed(newSnake) =>
                    gameState.copy(
                        snake = newSnake,
                        crashed = true
                    )

                case SnakeMoved.NewLocation(newSnake, true) =>
                    gameState.copy(
                        snake = newSnake,
                        apple = createApple(newSnake)
                    )

                case SnakeMoved.NewLocation(newSnake, false) =>
                    gameState.copy(snake = newSnake)
            }
    }
}


def drawApple(apple: VirtualLocation): Picture = {
    val canvasLocation = gameArea.toCanvasPosition(apple)
    Picture.rectangle(width = blockSize, height = blockSize)
        .thatsFilledWith(red)
        .thatsStrokeColored(black)
        .thatsTranslated(x = canvasLocation.x, y = canvasLocation.y)
}

def drawSnake(snake: Snake): Picture = {
    val headLocation = gameArea.toCanvasPosition(snake.head)
    val head = Picture.rectangle(width = blockSize, height = blockSize)
        .thatsFilledWith(blue)
        .thatsStrokeColored(black)
        .thatsTranslated(x = headLocation.x, y = headLocation.y)

    val tail = snake.tail.map { piece =>
        val location = gameArea.toCanvasPosition(piece)
        Picture.rectangle(width = blockSize, height = blockSize)
            .thatsFilledWith(green)
            .thatsStrokeColored(black)
            .thatsTranslated(x = location.x, y = location.y)
    }
    picStack((head :: tail).reverse)
}


def draw(gameState: GameState): Picture = {
    val apple = drawApple(apple = gameState.apple)
    val snake = drawSnake(gameState.snake)
    val images = {
        if (gameState.crashed)
            List(
                Picture.text("Game Over", 40),
                apple,
                snake
            )
        else
            List(apple, snake)
    }
    picStack(images)
}

val tickSubscription = Subscriptions.onAnimationFrame {
    GameEvent.Tick
}

val keysSubscription = Subscriptions.onKeyDown {
    case Kc.VK_LEFT  => GameEvent.OrientationUpdated.MoveLeft
    case Kc.VK_RIGHT => GameEvent.OrientationUpdated.MoveRight
    case Kc.VK_UP    => GameEvent.OrientationUpdated.MoveUp
    case Kc.VK_DOWN  => GameEvent.OrientationUpdated.MoveDown
    case _           => GameEvent.None
}

def subscriptions(gameState: GameState) = {
    if (gameState.crashed) List()
    else List(tickSubscription, keysSubscription)
}

runGame(
    init = initialGameState, 
    update = updateGameState, 
    view = draw, 
    subscriptions = subscriptions,
    refreshRate = 50
)

activateCanvas()