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