Build Snake with Zircon and Kotlin Part 3


Previous tutorial: Part 2

At this part of the tutorial we want to create a real snake and let it eat some cherries.

To archieve this we have to:

Forming a tail

The main change in this step is to update the GameController class. We need to keep track of the snakeTiles and the maximum length of the snake.

To continuously update the snakes position we will use a Timer and the extension function Timer.schedule() to define a TimerTask as lambda.

src/main/kotlin/main/GameController.kt

class GameController(private val displayController: DisplayController, private var snakePosition: Position) {
    
    // starting direction
    var currentDirection = Direction.DOWN
    // list of all snake positions
    var snakePositions = ArrayList(listOf(snakePosition))
    // initial length of the snake
    private var length = 3

    // Define timer in constructor
    init {
        // Use extension function of kotlin concurrent module
        Timer().schedule(1000, 300) {
            // Move the snake and then update the position
            move()
            displayController.draw()
        }
    }
    // This is our old move() function which doesn't move the snake anymore
    // now it updates the current direction
    fun directionChanged(direction: Direction) {
        currentDirection = direction
    }

    // New move() function to update snakePositions and set them in the displayController
    private fun move() {
        snakePositions.add(snakePositions.last() + currentDirection.diff)
        displayController.snakePositions = snakePositions

        shortenSnake()
    }

    // Shorten snake if the length is above the set length
    private fun shortenSnake() {
        if (snakePositions.size > length) {
            snakePositions.removeAt(0).let {
                // Set the oldSnakePosition in displayController to set it to a blackTile
                displayController.oldSnakePosition = Optional.of(it)
            }
        }
    }
}

To support this change we need to update the DisplayController class too.

The main change is to replace the snakePosition with a List and make oldSnakePosition public to allow it to change from the GameController.

src/main/kotlin/main/DisplayController.kt

class DisplayController(tileGrid: TileGrid, startPosition: Position) {
    private val gameScreen = Screens.createScreenFor(tileGrid)

    private val gameLayer = gameScreen.layers.first()

    private val snakeTile = snakeTile()
    private val blackTile = blackTile()
    
    // Change type to list to support the snake tiles
    var snakePositions: List<Position> = listOf(startPosition)

    // Changed to public field
    var oldSnakePosition = Optional.empty<Position>()

    fun draw() {
        oldSnakePosition.ifPresent {
            gameLayer.setTileAt(it, blackTile)
        }

        // Draw all snake tiles
        snakePositions.forEach {
            gameLayer.setTileAt(it, snakeTile)
        }
        gameScreen.display()
    }
    ...
}
...

Last thing to update is the Main class.

We moved the draw routine into the GameController and set the callback of the GameInputListener to GameController::directionChanged.

src/main/kotlin/main/Main.kt

fun main(args: Array<String>) {

    val grid = SwingApplications.startTileGrid(
            tileConfig {
                withSize(Sizes.create(30, 20))
            }
    )

    val startPosition = Positions.create(2, 3)

    DisplayController(grid, startPosition).apply {

        GameController(this, startPosition).apply {
            // Changed callback to directionChanged
            grid.onKeyStroke(GameInputListener(this::directionChanged))
        }
        // Removed draw routine
    }
}
...

Eating the Cherry

At first we add the logic to the GameController.

We now need to know the size of the gamearea to generate a random cherry in it.

We check the position of the head of the snake against the cherry on each move.

If those positions match we update the cherryposition to a new random one and increment the length of the snake.

src/main/kotlin/main/GameController.kt

class GameController(private val displayController: DisplayController,
                     private var snakePosition: Position,
                     // Add size of playarea to constructor
                     private val size: Size) {

    var currentDirection = Direction.DOWN
    var snakePositions = ArrayList(listOf(snakePosition))

    // Add field to hold the position of the cherry
    var cherryPosition: Position

    // Use random to generate new cherry position
    private val random = Random()
    private var length = 3

    init {
        // generate random position for first cherry
        cherryPosition = randomPosition()

        Timer().schedule(1000, 300) {
            move()
            displayController.draw()
        }
    }

    private fun move() {
        snakePositions.add(snakePositions.last() + currentDirection.diff)
        displayController.snakePositions = snakePositions

        // Check if the snake eats the cherry 
        checkCherry()

        // Set cherry position in the display controller
        displayController.cherryPosition = Optional.of(cherryPosition)

        shortenSnake()
    }

    // Check if the snake eats the cherry
    private fun checkCherry() {
        if (snakePositions.last() == cherryPosition) {
            // tell displayController about old position to remove it
            displayController.oldCherryPosition = Optional.of(cherryPosition)

            // generate new position
            cherryPosition = randomPosition()

            // increment length of snake for an extra tile
            length++
        }
    }
    
    ...

    // Use Positions helper class to generate random position in playarea
    private fun randomPosition() = Positions.create(random.nextInt(size.width), random.nextInt(size.height))

    ...

}

We also need to update the DisplayController to be able to draw our cherry.

src/main/kotlin/main/DisplayController.kt

class DisplayController(tileGrid: TileGrid, startPosition: Position) {
    private val gameScreen = Screens.createScreenFor(tileGrid)

    private val gameLayer = gameScreen.layers.first()

    private val snakeTile = snakeTile()
    private val blackTile = blackTile()

    // Add new cherry tile
    private val cherryTile = cherryTile()
    
    ...
    // fields for old and current cherry poistion
    var cherryPosition = Optional.empty<Position>()
    var oldCherryPosition = Optional.empty<Position>()

    fun draw() {
        oldSnakePosition.ifPresent {
            gameLayer.setTileAt(it, blackTile)
        }

        // Black out old position if present
        oldCherryPosition.ifPresent {
            gameLayer.setTileAt(it, blackTile)
        }

        // Set cherry position if present
        cherryPosition.ifPresent {
            gameLayer.setTileAt(it, cherryTile)
        }

        snakePositions.forEach {
            gameLayer.setTileAt(it, snakeTile)
        }
        gameScreen.display()
    }

    ...

    // Generate cherry position with tileBuilder
    private fun cherryTile() = tileBuilder {
        withBackgroundColor(ANSITileColor.RED)
        withForegroundColor(ANSITileColor.WHITE)
        withCharacter('c')
    }

    ...
}
...

To let the snake actually eat the cherry we must update the main() function.

The only change we apply, is to extract the size of the playarea into a variable and put it into the GameController src/main/kotlin/main/Main.kt

fun main(args: Array<String>) {

    // safe size into variable
    val size = Sizes.create(30,20)
    val grid = SwingApplications.startTileGrid(
            tileConfig {
                withSize(Sizes.create(30, 20))
            }
    )

    val startPosition = Positions.create(2, 3)

    DisplayController(grid, startPosition).apply {

        // put size into GameController
        GameController(this, startPosition, size).apply {
            grid.onKeyStroke(GameInputListener(this::directionChanged))
        }
    }

}

To start this up we can just gradle run and can move the snake with W, A, S, D.

If we hit a tile with the snake it will extend.

Eat the Cherry

The whole code is available on github: zircon_part3

Things to do