Build Snake with Zircon and Kotlin Part 2


Previous tutorial: Part 1

At this part of the tutorial we want to make the square move on keyboard inputs.

To archieve this we have to:

Receiving input

At first we need to define a direction interface. It contains the directions UP, DOWN, LEFT, RIGHT.

src/main/kotlin/main/Direction.kt

package main

import org.hexworks.zircon.api.Positions
import org.hexworks.zircon.api.data.Position

// 1. Add the position difference directly to the enum
enum class Direction(val diff: Position) {
    UP(Positions.create(0, -1)),
    DOWN(Positions.create(0, 1)),
    LEFT(Positions.create(-1, 0)),
    RIGHT(Position.create(1, 0));
}

Next thing we need to call the TileGrid.onKeyStroke() method to add an input listener. This method expects an object that inplements the KeyStrokeListener interface.

We add a new class src/main/kotlin/main/GameInputListener.kt and let it implement KeyStrokeListener.

This class also needs expects an interface in the Contructor. This interface takes a Direction and returns nothing.

src/main/kotlin/main/GameInputListener.kt

package main

import org.hexworks.zircon.api.input.KeyStroke
import org.hexworks.zircon.api.listener.KeyStrokeListener

// 1. Use the kotlin lambda notation to declare the interface
class GameInputListener(val callback: (Direction) -> Unit) : KeyStrokeListener {

    override fun keyStroked(keyStroke: KeyStroke) {
        // 2. Use Kotins when operator tho match the character and call the callback
        when (keyStroke.getCharacter()) {
            'w' -> callback(Direction.UP)
            'a' -> callback(Direction.LEFT)
            's' -> callback(Direction.DOWN)
            'd' -> callback(Direction.RIGHT)
        }
    }
}

React on key presses

To implement this interface we need a new class. We dont want to put it into the main loop because its too specific and also not in DisplayController because it has nothing to do with drawing on screen.

src/main/kotlin/main/GameController.kt

package main

import org.hexworks.zircon.api.data.Position

// 1. Initialize GameController and set the displayController and the starting position for the snake
class GameController(private val displayController: DisplayController, private var snakePosition: Position) {

    // 2. Implement move lambda function
    fun move(direction: Direction) {
        snakePosition += direction.diff
        displayController.snakePosition = snakePosition
    }
}

Move the snake

To make this work we have to add the snakePosition variable to the DisplayController and wire those two class in the main method together.

We start with extending the DisplayController to fit our needs:

src/main/kotlin/main/DisplayController.kt

// 1. Add the starting position of the snake to the constructor
class DisplayController(tileGrid: TileGrid, startPosition: Position) {
    private val gameScreen = Screens.createScreenFor(tileGrid)

    private val gameLayer = gameScreen.layers.first()

    private val snakeTile = snakeTile()

    // 2. Add a new blackTile for neutralizing the old position of the snake
    private val blackTile = blackTile()

    // 3. Create the equivalent of a setter in java and save the old position of the snake to clear it later
    var snakePosition = startPosition
        set(value) {
            oldSnakePosition = Optional.of(snakePosition)
            field = value
        }

    // 4. Initialize the old position with empty because we dont have it yet
    private var oldSnakePosition = Optional.empty<Position>()

    fun draw() {
        // 5. If the old position is present erase it with the black tile
        oldSnakePosition.ifPresent {
            gameLayer.setTileAt(it, blackTile)
        }
        gameLayer.setTileAt(snakePosition, snakeTile)
        gameScreen.display()
    }

    private fun snakeTile() = tileBuilder {
        withBackgroundColor(ANSITileColor.GREEN)
        withForegroundColor(ANSITileColor.WHITE)
        withCharacter('o')
    }

    // 6. Add new creation function for the black tile
    private fun blackTile() = tileBuilder {
        withBackgroundColor(ANSITileColor.BLACK)
        withForegroundColor(ANSITileColor.BLACK)
        withCharacter(' ')
    }
}

fun tileBuilder(lambda: TileBuilder.() -> Unit): Tile {
    return Tiles.newBuilder().apply(lambda).build()
}

Glueing it together

At last we wire it together in the main function:

src/main/kotlin/main/Main.kt

fun main(args: Array<String>) {

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

    // 1. Create a starting position for the snake
    val startPosition = Positions.create(2, 3)

    // 2. Initialize the display controller with the starting position
    DisplayController(grid, startPosition).apply {
        
        // 3. Use the DisplayController to initialize the GameController
        GameController(this, startPosition).apply {

            // 4. Wire the created GameController to our GameInputListener
            grid.onKeyStroke(GameInputListener(this::move))
        }

        while (true) {
            draw()
        }
    }
}

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

The whole code is available on github: zircon_part2