Author Topic: Arena (7DRL 2014) [FAILURE]  (Read 18747 times)

SoulBeaver

  • Newcomer
  • Posts: 20
  • Karma: +0/-0
    • View Profile
    • Email
Arena (7DRL 2014) [FAILURE]
« on: March 09, 2014, 12:04:02 PM »
Hello everyone,


my name is Christian and this is my first time entering into any sort of coding competition. I've been programming for around three years now and I'll be attempting my first ever roguelike using Kotlin. Kotlin is Java but without all of the boilerplate and of the C syntax, so it should be a fairly easy read for anybody interested.

It's a simple roguelike about fighting a boss in an arena. There will hopefully be a few classes to choose from, a variety of perks, skills and abilities, but at the very least one dude fighting one big guy and dying (a lot).

All of my code will be available on GitHub: https://github.com/SoulBeaver/7DRL

Thanks for your time and good luck everyone!
« Last Edit: March 18, 2014, 08:26:59 AM by SoulBeaver »

SoulBeaver

  • Newcomer
  • Posts: 20
  • Karma: +0/-0
    • View Profile
    • Email
Re: Arena (7DRL 2014)
« Reply #1 on: March 09, 2014, 01:07:36 PM »
Update #1

I've successfully bootstrapped my application. I'll use LWJGL because it seemed the easiest to learn. I've never used it before, but creating some primitive graphics shouldn't be too hard.

I'm also including a settings.yml file to configure some of my program's parameters without needing to remake it. For now, this is just the screen real estate, but I'll hopefully use it for everything. Yaml is pretty cool.

Code: [Select]
private fun readConfiguration(file: String): Configuration {
    val configurationPath = Paths.get(file)!!.toAbsolutePath()!!

    Preconditions.checkArgument(Files.exists(configurationPath),
                                "The configuration file $file does not exist!")

    val configurationMap = Files.newBufferedReader(configurationPath, StandardCharsets.UTF_8).use {
        Yaml().load(it) as Map<String, Any?>
    }

    return Configuration(configurationMap)
}

No tests yet, just experimenting and verifying all of my dependencies and the first project structure.
« Last Edit: March 09, 2014, 01:17:23 PM by SoulBeaver »

SoulBeaver

  • Newcomer
  • Posts: 20
  • Karma: +0/-0
    • View Profile
    • Email
Re: Arena (7DRL 2014)
« Reply #2 on: March 09, 2014, 01:23:06 PM »
Intermezzo

Need to read up on game loops again. I'll try this one.

http://gafferongames.com/game-physics/fix-your-timestep/

SoulBeaver

  • Newcomer
  • Posts: 20
  • Karma: +0/-0
    • View Profile
    • Email
Re: Arena (7DRL 2014)
« Reply #3 on: March 09, 2014, 04:17:43 PM »
So I tried my hand at creating a little cave generator. I used a Cellular Automate described in http://www.roguebasin.com/index.php?title=Cellular_Automata_Method_for_Generating_Random_Cave-Like_Levels

The code works, the spec passes, but the cave doesn't look anything like on the page. I'm probably screwing up the algorithm somewhere, but I'll get there eventually :)

Code: [Select]
###........#..##.##...##.#...#.......#...#####...
.#......#...#..#.###.#....##..#..#####..##..###.#
##.###.##...#.#...#..##.#....#...#.#.....#....#.#
..#...#.#.######.#.##.#.#.#..#.#......#....#..##.
..#.##....#......#..#...##.#...#.#####...#.....#.
##.##......#.##....#..#.##.#.#.......##...#.###..
#..#.....#.......#.###.##.#.###.#..#.##.#####..##
...##.....##...#....#.#.#.#..#..#..#.#.##..#....#
......#....#.#..##..###......##...#.##...#..##.##
.#....#....#.#..###.......#...####.##....#.#.#...
#..##..###.#.##..##......#.#....#..###.####.#..##
##.......#.##...#.....#..#.#..#.#.##......#...##.
###.....#.#...#.#....####......##.##....#.#...#..
.##.....#....#..#..##.....##.......###...#....#..
##..###.###.#..#..#.####.#.#.#.##........#.....#.
###.....###......#..#.#.#....#....#.##.....##....
.#.#..#.##..##...#..#....#.....#..#...##.#.......
...#.#....#.##..#.##..#.#..##.#..##.#..##.#.#....
#..#####........#.#.###.....#.....#.##.....#.#..#
.#....###...#..#.##.#....##.#......#........###.#
#.......###...#.#####.#.##.###..##...##..##.#.#..
###...#.#.#.....#..#.##..#...#..###..#..#####.#..
#.......#..#..#.......##...###.#..##..#.##.#..#..
.##..#.##...#.#.#.#...#.....#####.##.#......#..#.
##.#.#.#..###..#.#.###...#....#...#....##....###.
#.######....###...#...#........#.###...###..##..#
..#.##..##........#....#.#...##...#.#.##.###.#.##
......#.....#....##.#..#..#..#..###.#.#.#..#....#
#...#..##..#...###....#####...#........#..#.#....
#..#.##.....##.#...#..#..#.##..##.##..###.#......
....#.#.#..#..#.##....#....#...#....##.#...#.....
.#.###.#..#####....#....#.###.#.####..#....#.##..
...##...##.##.#..#.#.#.#..#.###.#..#...#......#.#
..##.#.#...#..##.#..##...#..##.###.........##...#
.##.#..#...#..#.#.#...#.#.#...#.#.##...####.####.
....#...##....#...###....#...#.#.#....##..#..#...
...#........#..#......#.######.###..#..#...###.#.
...####.......##..#.........#..#.#..#.##.###..#..
##...##......####..#..#.#..#...#.......####....##
.##.#######...#..#.#####....#.####..#.#..#.#.#.##
.#.#..#...#...##.#.##...#..###.######..#.#.#....#
...#...####.#..####...##.###.####..#..#.#...#....
#.#..#..#.#...#.#..##.#.#..##....#.#...##....#..#
#..#.#...#.......#...#.#...##.##..##...#..###.#..
.#...#...##.#..###...##..#..##....#.#####.......#
...#.#..###.#...##..#..##....##..#.#..#.##.#....#
#.#..##.####...##.#.........#.###..#.#.#....##..#
...#.#.#...#.#..#...#.....#..#...........#......#
...#.#.#....#..##..#....##.#..###.#.###.#...#..##
....#.#...#.#...#...#..##.......#.#....#...##.#.#
#...#.#..#.......#..#.##.#..#.....##.....#...##.#

The code is on my github, but I know how lazy I can be, so I'm posting it here as well:

Code: [Select]
enum class FloorType {
    Floor
    Wall
}

/**
 * CaveGenerator's purpose is to create an asymmetrical, open-ended cave with few openings.
 * Algorithm for CaveGenerator inspired (stolen) from:
 *
 * http://www.roguebasin.com/index.php?title=Cellular_Automata_Method_for_Generating_Random_Cave-Like_Levels
 */
class CaveGenerator(val configuration: Configuration) {
    private val random = Random()

    /**
     * Generates an asymmetrical, open-ended cave.
     *
     * @param dimension The target width and height of the cave
     * @return the floor layout of the cave.
     */
    fun generate(dimension: Dimension): Array<FloorType> {
        val (width, height) = dimension

        Preconditions.checkArgument(width > 0 && height > 0,
                "Cannot generate a map with negative width or height")

        val numberOfPasses = configuration.numberOfPasses
        val wallCreationProbability = configuration.wallCreationProbability
        val neighborsRequiredToRemainAWall = configuration.neighborsRequiredToRemainAWall
        val neighborsRequiredToCreateAWall = configuration.neighborsRequiredToCreateAWall

        Preconditions.checkArgument(numberOfPasses > 0,
                "Cannot generate a map without at least one pass")
        Preconditions.checkArgument(wallCreationProbability < 100,
                "If wall creation is set to 100%, the map will be one huge wall.")
        Preconditions.checkArgument(neighborsRequiredToRemainAWall > 0 && neighborsRequiredToRemainAWall <= 9,
                "NeighborsRequiredToRemainAWall describes the number of adjacent neighbors required" +
                " for a wall to remain a wall. This cannot be less than 0 or greater than 9.")
        Preconditions.checkArgument(neighborsRequiredToCreateAWall > 0 && neighborsRequiredToCreateAWall <= 9,
                "NeighborsRequiredToCreateAWall describes the number of adjacent neighbors required" +
                " for a space to become a wall. This cannot be less than 0 or greater than 9.")

        // First pass, create walls when probability exceeds threshold
        val cave = Array<FloorType>(width * height, {
            if (random.nextInt(100) < wallCreationProbability)
                FloorType.Wall
            else
                FloorType.Floor
        })

        for (pass in 1..numberOfPasses) {
            cave.withIndices().map {
                floorWithIndex -> {
                    val (index, floor) = floorWithIndex

                    val neighbors = neighbors(dimension, cave, index)
                    val neighborsWhichAreWalls = neighbors.count { it == FloorType.Wall }

                    if (floor == FloorType.Wall) {
                        if (neighborsWhichAreWalls < neighborsRequiredToRemainAWall)
                            FloorType.Floor
                    }
                    else {
                        if (neighborsWhichAreWalls >= neighborsRequiredToCreateAWall)
                            FloorType.Wall
                    }
                }
            }
        }

        return cave
    }

    /**
     * Returns the list of all neighbors at floorIndex in cave.
     *
     * @param dimension The Dimensions of the cave
     * @param cave The context for the floorIndex
     * @param floorIndex The current floor in the cave
     * @return all adjacent neighbors to floorIndex; size may vary depending on edges, corners of floorIndex
     */
    private fun neighbors(dimension: Dimension, cave: Array<FloorType>, floorIndex: Int): List<FloorType> {
        /**
         * To understand what's happening here, consider a 3x3 matrix of Floors
         * where W = Wall, F = Floor.
         *
         * W F W
         * F F F
         * W F W
         *
         * Our floorIndex is the center of this matrix, or (1, 1) in xy coordinates.
         * However, cave is a single list, so the cells are laid out as such:
         *
         * W F W F F F W F W
         *
         * The floorIndex is now 4 (starting from 0).
         *
         * It's trivial to get the left and right neighbors by adding or subtracting 1
         * To get the bottom and top neighbors, consider again the first 3x3 matrix.
         *
         * W F W
         * F F F
         * W F W
         *
         * A cave still has width and height, so subtracting width from our current floorIndex will result int
         *
         *  n = floorIndex - width
         *  n = 4 - 3
         *  n = 1
         *
         *  If 1 is the new index, then we can see that it is the F in the first row, the top neighbor to our
         *  current FloorIndex.
         *
         *  It should now be easy to see that adding width will yield the bottom F.
         *
         *  Adding or subtracting 1 to the bottom and top neighbors will yield their respective right and left
         *  neighbors, or the diagonals of our floor index. It is thus possible to return all 8 neighbors of floorIndex.
         */
        return listOf(
                cave[floorIndex - 1],
                cave[floorIndex + 1],
                cave[floorIndex - dimension.width],
                cave[floorIndex + dimension.width],
                cave[floorIndex - 1 - dimension.width],
                cave[floorIndex + 1 - dimension.width],
                cave[floorIndex - 1 + dimension.width],
                cave[floorIndex + 1 + dimension.width]
        )
    }
}


SoulBeaver

  • Newcomer
  • Posts: 20
  • Karma: +0/-0
    • View Profile
    • Email
Re: Arena (7DRL 2014)
« Reply #4 on: March 09, 2014, 05:23:47 PM »
Now this looks a lot better!

Code: [Select]
..................................................
..####............................................
######............................................
######......................................#....#
######.....................................###....
####..............##...##..................####...
..................##...##...................###...
..................##....##..................####..
.....##..........##.....##...................#####
#....###........###.....##...............##...####
##....##.....#######.....................##.....##
###...###....#######....................##.......#
####...##.....#####..................#####.......#
#####.........####..................######.......#
######....#...###..................####...........
.#####...###......................####....##......
..#####...##......................####....##......
...#####..##......................#####...........
...#######........................######..........
...######..........##..............#####..........
...#####........#######..............###.......##.
...####.....############..............#........##.
.....#.....#############.......................###
..........#############........................##.
........###############.........#.................
........###############........###..##............
...#.....########...##..........#########.........
..###....#######.................#########........
...###.....####...................########........
...##.............................#########.......
...##...............##..##........##########......
..####......####....######........#########.......
...####.....####....#######.......########.....##.
.....###.....###.....#######.......#######....####
......####...........###########....#######..#####
#.....####...........############...##############
#.....###............############...##############
#......##............###########....######..######
##.....##.............#########.....#####....#####
###...####..#..............####.....####......####
###...#########.............####...#####.......###
......##########.......##...#############.......#.
......##########.......##..###############........
.....##########.............################......
.....##########.............#################.....
....###########...............#################...
....###########...##...........###..###########...
....###########..####...........#....##########...
....###########.#####.................########....
.....##.##.......##....................####.#.....

It turns out that it was a bug in my code and none of the passes were executing. It simply returned the initialization of my array, but no smoothing. Fixed now!

Here's the updated code:

Code: [Select]
package com.sbg.arena.core

import com.sbg.arena.configuration.Configuration
import com.google.common.base.Preconditions
import java.util.ArrayList
import java.util.Random
import com.sbg.arena.util.bindFirst

enum class FloorType {
    Floor
    Wall
}

/**
 * CaveGenerator's purpose is to create an asymmetrical, open-ended cave with few openings.
 * Algorithm for CaveGenerator inspired (stolen) from:
 *
 * http://www.roguebasin.com/index.php?title=Cellular_Automata_Method_for_Generating_Random_Cave-Like_Levels
 */
class CaveGenerator(val configuration: Configuration) {
    {
        Preconditions.checkArgument(configuration.numberOfPasses > 0,
                "Cannot generate a map without at least one pass")

        Preconditions.checkArgument(configuration.wallCreationProbability < 100,
                "If wall creation is set to 100%, the map will be one huge wall.")

        Preconditions.checkArgument(configuration.neighborsRequiredToRemainAWall > 0 &&
                                    configuration.neighborsRequiredToRemainAWall <= 9,
                "NeighborsRequiredToRemainAWall describes the number of adjacent neighbors required" +
                " for a wall to remain a wall. This cannot be less than 0 or greater than 9.")

        Preconditions.checkArgument(configuration.neighborsRequiredToCreateAWall > 0 &&
                                    configuration.neighborsRequiredToCreateAWall <= 9,
                "NeighborsRequiredToCreateAWall describes the number of adjacent neighbors required" +
                " for a space to become a wall. This cannot be less than 0 or greater than 9.")
    }

    private val random = Random()

    /**
     * Generates an asymmetrical, open-ended cave.
     *
     * @param dimension The target width and height of the cave
     * @return the floor layout of the cave.
     */
    fun generate(dimension: Dimension): Array<FloorType> {
        Preconditions.checkArgument(dimension.width > 0 && dimension.height > 0,
                "Cannot generate a map with negative width or height")

        var cave = Array<FloorType>(dimension.width * dimension.height, { initialFloorType() })

        (1..configuration.numberOfPasses) forEach { smooth(dimension, cave) }

        return cave
    }

    private fun initialFloorType(): FloorType {
        return if (random.nextInt(100) < configuration.wallCreationProbability) FloorType.Wall else FloorType.Floor
    }

    /**
     * Returns the list of all neighbors at floorIndex in cave.
     *
     * @param dimension The Dimensions of the cave
     * @param cave The context for the floorIndex
     * @param floorIndex The current floor in the cave
     * @return all adjacent neighbors to floorIndex; size may vary depending on edges, corners of floorIndex
     */
    private fun neighbors(dimension: Dimension, cave: Array<FloorType>, floorIndex: Int): List<FloorType> {
        val (width, height) = dimension

        /*
         * To understand what's happening here, consider a 3x3 matrix of Floors
         * where W = Wall, F = Floor.
         *
         * W F W
         * F F F
         * W F W
         *
         * Our floorIndex is the center of this matrix, or (1, 1) in xy coordinates.
         * However, cave is a single list, so the cells are laid out as such:
         *
         * W F W F F F W F W
         *
         * The floorIndex is now 4 (starting from 0).
         *
         * It's trivial to get the left and right neighbors by adding or subtracting 1
         * To get the bottom and top neighbors, consider again the first 3x3 matrix.
         *
         * W F W
         * F F F
         * W F W
         *
         * A cave still has width and height, so subtracting width from our current floorIndex will result int
         *
         *  n = floorIndex - width
         *  n = 4 - 3
         *  n = 1
         *
         *  If 1 is the new index, then we can see that it is the F in the first row, the top neighbor to our
         *  current FloorIndex.
         *
         *  It should now be easy to see that adding width will yield the bottom F.
         *
         *  Adding or subtracting 1 to the bottom and top neighbors will yield their respective right and left
         *  neighbors, or the diagonals of our floor index. It is thus possible to return all 8 neighbors of floorIndex.
         */
        val neighbors = ArrayList<FloorType>()

        if (floorIndex - width > 0) {
            neighbors.add(cave[floorIndex - 1])
            neighbors.add(cave[floorIndex - width])
            neighbors.add(cave[floorIndex + 1 - width])
        }

        if (floorIndex + width < width * height) {
            neighbors.add(cave[floorIndex + 1])
            neighbors.add(cave[floorIndex + width])
            neighbors.add(cave[floorIndex - 1 + width])
        }

        if (floorIndex - 1 - width > 0)
            neighbors.add(cave[floorIndex - 1 - width])

        if (floorIndex + 1 + width < width * height)
            neighbors.add(cave[floorIndex + 1 + width])

        return neighbors
    }

    /**
     * Smooth the cave by comparing each cell to its neighbors.
     * <b>Caution: This mutates the cave!</b>
     *
     * @param dimension The Dimenions of the cave (width and height)
     * @param cave the cave to smooth
     */
    private fun smooth(dimension: Dimension, cave: Array<FloorType>) {
        val neighborsRequiredToRemainAWall = configuration.neighborsRequiredToRemainAWall
        val neighborsRequiredToCreateAWall = configuration.neighborsRequiredToCreateAWall

        for ((index, floor) in cave.withIndices()) {
            val neighbors = neighbors(dimension, cave, index)
            val neighborsWhichAreWalls = neighbors.count { it == FloorType.Wall }

            if (floor == FloorType.Wall) {
                if (neighborsWhichAreWalls < neighborsRequiredToRemainAWall)
                    cave[index] = FloorType.Floor
            }
            else {
                if (neighborsWhichAreWalls >= neighborsRequiredToCreateAWall)
                    cave[index] = FloorType.Wall
            }
        }
    }
}

SoulBeaver

  • Newcomer
  • Posts: 20
  • Karma: +0/-0
    • View Profile
    • Email
Re: Arena (7DRL 2014)
« Reply #5 on: March 09, 2014, 06:38:48 PM »
Slicks is being mean. For some reason it won't draw my wall and floor tiles correctly. If anybody has any experience with Slicks, mind telling me what's wrong?

Code: [Select]
class Arena(val configuration: Configuration): BasicGame(configuration.gameTitle) {
    private val logger = LogManager.getLogger(javaClass<Arena>())!!

    private var levelGenerator: Generator by Delegates.notNull()
    private var cave: Array<FloorType> by Delegates.notNull()

    private var floorTile: Image by Delegates.notNull()
    private var wallTile: Image by Delegates.notNull()

    override fun init(gc: GameContainer?) {
        levelGenerator = when (configuration.levelGenerator) {
            "cave" -> CaveGenerator(configuration)
            else   -> throw IllegalArgumentException("Generation strategy ${configuration.levelGenerator} not recognized")
        }

        cave = levelGenerator.generate(Dimension(configuration.columns, configuration.rows))
        logCave(cave)

        floorTile = Image("assets/FloorTile.png")
        wallTile  = Image("assets/WallTile.png")
    }

    override fun update(gc: GameContainer?, i: Int) {
        // Nothing to do here yet
    }

    override fun render(gc: GameContainer?, g: Graphics?) {
        g!!.setBackground(Color.white)

        var row = 20F
        for ((index, floor) in cave.withIndices()) {
            if (index % 50 == 0)
                row += 20F

            if (floor == FloorType.Wall)
                wallTile.draw(index * 20F, row)
            else
                floorTile.draw(index * 20F, row)
        }
    }

    private fun logCave(cave: Array<FloorType>) {
        val graphicalCave = cave.map { if (it == FloorType.Floor) "." else "#" }

        val stringBuilder = StringBuilder()
        for ((index, floor) in graphicalCave.withIndices()) {
            if (index % 50 == 0)
                stringBuilder.append(System.lineSeparator())

            stringBuilder.append(floor)
        }

        logger.debug(stringBuilder.toString())
    }
}

Anyway, that's it for the day! It's growing late and I'm pretty tired :)

What happened today:

- Project was set up
- Features and Milestones were set, repository created
- Map generation has reached a nice alpha
- Drawing on the screen almost works

I guess that's okay, especially for not having programmed a roguelike before. Hopefully I'll pick up some extra speed tomorrow. Have fun everyone!

SoulBeaver

  • Newcomer
  • Posts: 20
  • Karma: +0/-0
    • View Profile
    • Email
Re: Arena (7DRL 2014)
« Reply #6 on: March 09, 2014, 06:41:41 PM »
Oh! One last thing. I encourage almost everyone who is able to to create a settings file! It makes testing and development so much easier :) Many things I would have to recompile the code for can now be easily managed via the settings.yml

Code: [Select]
# General options
gameTitle: Arena

# Video
width: 1024
height: 768
fullScreen: false

# Generation

# Which generation scheme to use
levelGenerator: cave
# The height of the generated cave
rows: 20
# The width of the generated cave
columns: 20

# Cave generation

# I'm using a cellular automata described here: http://www.roguebasin.com/index.php?title=Cellular_Automata_Method_for_Generating_Random_Cave-Like_Levels
# The algorithm repeatedly loops over the map, filling in walls depending on a certain set of rules
# These rules can be adjusted to change the appearance of the cave

# Probability of creating a wall on the first pass
wallCreationProbability: 40
# Requirement to remain a wall for every successive pass
neighborsRequiredToRemainAWall: 3
# Requirement to becomea wall for every successive pass
neighborsRequiredToCreateAWall: 5
# Number of passes. Each pass will (hopefully) smooth out the map
numberOfPasses: 10

If you're using Java, loading a yaml file is nothing more than importing SnakeYaml and creating a little loading function:

Code: [Select]
fun loadConfiguration(filename: String): Configuration {
    val configurationPath = Paths.get(filename)!!.toAbsolutePath()!!

    Preconditions.checkArgument(Files.exists(configurationPath),
                                "The configuration file $filename does not exist!")

    val configurationMap = Files.newBufferedReader(configurationPath, StandardCharsets.UTF_8).use {
        Yaml().load(it) as Map<String, Any?>
    }

    return Configuration(configurationMap)
}

SoulBeaver

  • Newcomer
  • Posts: 20
  • Karma: +0/-0
    • View Profile
    • Email
Re: Arena (7DRL 2014)
« Reply #7 on: March 10, 2014, 08:39:48 AM »
Good morning everyone! I hope everyone's feeling well.

I left off yesterday with my unsuccessful rendering of the Cave. I didn't address the issue yet because using a naked array in code is just brrrr. Can't have that yo. So we wrap that shizzle in a class and provide accessors and iterators to smooth out the handling.

Code: [Select]
class Level(val dimension: Dimension,
            private val level: Array<FloorType>) {
    {
        val area = dimension.width * dimension.height
        Preconditions.checkArgument(area == level.size,
                                    "Level does not conform to the given dimensions; expected ${area}, but was ${level.size}")
    }

    private val logger = LogManager.getLogger(javaClass<Level>())

    private val asciiRepresentation: String by Delegates.lazy {
        val asciiLevel = Array<String>(level.size, {
            if (level[it] == FloorType.Floor) "." else "#"
        })

        val asciiRepresentationBuilder = StringBuilder()
        for ((index, floor) in asciiLevel.withIndices()) {
            if (index % dimension.width == 0)
                asciiRepresentationBuilder.append(System.lineSeparator())

            asciiRepresentationBuilder.append(floor)
        }
        asciiRepresentationBuilder.toString()
    }

    val width  = dimension.width
    val height = dimension.height
    val area   = width * height

    fun get(point: Point): FloorType {
        return get(point.x, point.y)
    }

    fun get(x: Int, y: Int): FloorType {
        return level[x + y * width]
    }

    fun toString():String {
        return asciiRepresentation
    }
}

fun Level.iterable(): Iterable<FloorType> {
    return object: Iterable<FloorType> {
        override fun iterator(): Iterator<FloorType> {
            return iterator()
        }
    }
}

fun Level.iterator(): Iterator<FloorType> {
    return object: Iterator<FloorType> {
        var x = 0
        var y = 0

        override fun hasNext(): Boolean {
            return (y != height)
        }

        override fun next(): FloorType {
            val next = get(x, y)

            x += 1
            if (x % width == 0) {
                y += 1
                x = 0
            }

            return next
        }
    }
}

This has several advantages.

1) No longer having to do array arithmetic at every other point in the code.
2) We can use the (x,y) coordinates which feels a lot more natural
3) It's easy to extend the class with utility functions such as iterators and iterables
4) We still have the base layout to work with and can "decorate" this with Skins

Skins are unimplemented yet, but they will provide the look and feel of my level. With this it should be trivial to swap one sprite pallette out for another. Even though this is a seven day roguelike and roguelikes aren't necessarily known for graphics, I believe this is a useful feature to have. A single entry in the settings.yml should be enough to quickly switch between different palettes. In any case, I'll probably only create a single palette because seven days is a damn short timeframe.

I've sadly encountered a bug that causes my level to StackOverflow if I use any of the Iterable extension functions (map, filter, count, find, and others). There's a corresponding issue open with the great Kotlin guys, and hopefully that will be fixed before I need it outside of testing!

Speaking of testing, what a life saver! I now have the (illusion of) guarantee that my accessors and iterators work 100% for levels of varying size. That's pretty cool.

deepshock

  • Rogueliker
  • ***
  • Posts: 80
  • Karma: +0/-0
  • May the Voice always ring true in your ears
    • View Profile
    • Email
Re: Arena (7DRL 2014)
« Reply #8 on: March 10, 2014, 12:46:27 PM »
You have some solid ideas here. I'm doing something sort of similar, and I hope your 7DRL goes well! :) I'll keep an eye out.
Bardess- A party-based roguelike work in progress. Currently in beta and miles away from a release state. Feedback is always welcome.

http://roguetemple.com/forums/index.php?topic=2228.0
https://sites.google.com/site/bardesstemp/

SoulBeaver

  • Newcomer
  • Posts: 20
  • Karma: +0/-0
    • View Profile
    • Email
Re: Arena (7DRL 2014)
« Reply #9 on: March 10, 2014, 01:48:15 PM »
@deepshock:  Thank you for your motivating comments! I'm glad you find some use in the various snippets of code I've posted here :) I'll be sure to return the favor once I dig out your thread!

---

The beast lives!



It's not much, but I enjoy it when a concept works. I've created a Skin class which loads all relevant tiles from my asset directory per the settings:

Code: [Select]
class Skin(val configuration: Configuration) {
    private var floorTile: Image by Delegates.notNull()
    private var wallTile: Image by Delegates.notNull()

    fun loadTiles() {
        val skinsDirectory = "assets/${configuration.skin}"

        floorTile = Image("${skinsDirectory}/FloorTile.png")
        wallTile  = Image("${skinsDirectory}/WallTile.png")
    }

    fun floorTile(): Image {
        return floorTile
    }

    fun wallTile(): Image {
        return wallTile
    }

    fun render(level: Level) {
        val tileWidth = configuration.tileWidth
        val tileHeight = configuration.tileHeight

        for ((point, floor) in level.withIndices()) {
            val x = point.x.toFloat() * tileWidth
            val y = point.y.toFloat() * tileHeight

            when (floor) {
                FloorType.Floor -> floorTile().draw(x, y)
                FloorType.Wall  -> wallTile().draw(x, y)
            }
        }
    }
}

As you can see, rendering the level became a piece of cake with the help of just One More Abstraction. Martin Fowler never ceases to disappoint or blow my mind. I believe this is a good separation of responsibilities:  the level knows its layout, the skin knows how to render the layout.

I'll start adding the character with some movement now, or I'll implement a Grid. I'm pretty sure I'm going to need a grid real soon so I can use the mouse when selecting items, monsters, or the ground.

SoulBeaver

  • Newcomer
  • Posts: 20
  • Karma: +0/-0
    • View Profile
    • Email
Re: Arena (7DRL 2014)
« Reply #10 on: March 10, 2014, 03:20:54 PM »
Okay, so I implemented a "Player" with basic input. I'm not happy with the implementation by any stretch of the imagination, but it's a first result.

A player, like everything, can be configured from the settings, but only has HitPoints and Attack so far. I managed to get around the grid issue by treating the player as a FloorType, of which there are now three.

Code: [Select]
enum class FloorType {
    Floor
    Wall
    Player
}

This is cool, because my Level was already a catalog of x,y coordinates for everything in the level. A player is nothing but an entry in that catalog. Moving the Player along the grid isn't anymore complicated than shifting its position and making sure I erase the old one. That's a nice solution.

I once read a book called Game Development something something. It explained that you shouldn't handle input directly. Instead, the result of any input should be a Request- what the input should do. The main program interprets how to fulfill that request. This means that directional movement results in a Direction which is then interpreted by my main program as "Move the x,y coordinates of the player". Since it's an implementation detail, it shouldn't be directly visible to the Player.

Nevertheless, I'm not sure all of the logic should be in the Arena class or I run the risk of creating a God object. Anywho, it's still a task I'm working on and, quite frankly, I need a game loop or the speed of inputs will run out of control. It is quite fun to see my little player zip around the screen though :)

Player:

Code: [Select]
class Player(configuration: Configuration) {
    var maxHitPoints: Int
    var hitPoints: Int
    var attack: Int

    {
        maxHitPoints = configuration.hitPoints
        hitPoints = configuration.hitPoints
        attack = configuration.attack
    }

    fun takeDamage(amount: Int) {
        hitPoints = if (hitPoints - amount < 0) 0 else (hitPoints - amount)
    }

    fun heal(amount: Int) {
        hitPoints += if (hitPoints + amount > maxHitPoints) maxHitPoints else (hitPoints + amount)
    }

    fun move(gc: GameContainer): Direction? {
        val input = gc.getInput()!!

        return when {
            input.isKeyDown(Input.KEY_W) -> Direction.NORTH
            input.isKeyDown(Input.KEY_A) -> Direction.WEST
            input.isKeyDown(Input.KEY_S) -> Direction.SOUTH
            input.isKeyDown(Input.KEY_D) -> Direction.EAST
            else                         -> null
        }
    }
}

Input interpretation:

Code: [Select]
override fun update(gc: GameContainer?, delta: Int) {
    val requestedDirection = player.move(gc!!)

    if (requestedDirection != null) {
        when (requestedDirection) {
            Direction.SOUTH -> tryMove(Point(playerCoordinates.x, playerCoordinates.y + 1))
            Direction.NORTH -> tryMove(Point(playerCoordinates.x, playerCoordinates.y - 1))
            Direction.EAST  -> tryMove(Point(playerCoordinates.x + 1, playerCoordinates.y))
            Direction.WEST  -> tryMove(Point(playerCoordinates.x - 1, playerCoordinates.y))
        }
    }
}

private fun tryMove(destination: Point) {
    if (destination.x < 0 || destination.y < 0 ||
        destination.x >= configuration.width || destination.y >= configuration.height)
        return

    if (level[destination] == FloorType.Floor) {
        level[destination] = FloorType.Player
        level[playerCoordinates] = FloorType.Floor

        playerCoordinates = destination
    }
}

SoulBeaver

  • Newcomer
  • Posts: 20
  • Karma: +0/-0
    • View Profile
    • Email
Re: Arena (7DRL 2014)
« Reply #11 on: March 10, 2014, 05:47:50 PM »
We can scroll now! I'd post a gif but my filming program (Mirillis Action) spectacularly crashed. I guess it's not quite ready for Windows 8. A gif of a scrolling block isn't that exciting anyway, so let's get right to the nitty gritty details :)

I didn't know how to scroll, and now, after reading a few posts:

http://gamedev.stackexchange.com/questions/44256/how-to-add-a-scrolling-camera-to-a-2d-java-game
http://slick.ninjacave.com/forum/viewtopic.php?t=1906
http://www.java-gaming.org/index.php/topic,24800.0

I think I know how to do it for *this* scenario at least. You want to be able to render your map in local coordinates, but transform the world. This is silly graphics speak for "Draw it like you usually do, but move the camera instead." Let's take an example:

We start with a 4x10 grid.

Code: [Select]
W W F W W W W W W W
W P F F W W W F F W
W F F F F F F F F W
W W W W W W W W W W

But wait! You can only see 3x3 of these at any time.

Code: [Select]
[W W F] W W W W W W W
[W P F] F W W W F F W
[W F F] F F F F F F W
W W W W W W W W W W W

Okay, and now we want to scroll it. Draw it like usual...

Code: [Select]
W W F W W W W W W W
W F F F W W W F F W
W P F F F F F F F W
W W W W W W W W W W

And now move the camera instead!

Code: [Select]
W W F W W W W W W W
[W F F] F W W W F F W
[W P F] F F F F F F W
[W W W] W W W W W W W W

Draw as usual, move the world.

The moving the world part is accomplished by translating the current view by an x and y amount.

Code: [Select]
fun renderGameplay(graphics: Graphics, use: () -> Unit) {
graphics.translate(-cameraCenter.x.toFloat(), -cameraCenter.y.toFloat())

And you shouldn't ever forget to un-translate so you can draw the GUI without wondering where the hell it went.

Code: [Select]
fun renderGameplay(graphics: Graphics, use: () -> Unit) {
    graphics.translate(-cameraCenter.x.toFloat(), -cameraCenter.y.toFloat())

    use()

    graphics.translate(cameraCenter.x.toFloat(), cameraCenter.y.toFloat())
}

I use a function argument that will render the gameplay within the translated context and then automatically un-translate the view before I return. This way, I should hopefully never forget to reset anything :)

Code: [Select]
class Camera(val configuration: Configuration) {
    private val logger = LogManager.getLogger(javaClass<Camera>())!!

    private var cameraCenter = Point(0, 0)

    val viewportWidth: Int
    val viewportHeight: Int
    val worldWidth: Int
    val worldHeight: Int

    private val maximumOffset: Dimension
    private val minimumOffset: Dimension;

    {
        viewportWidth  = configuration.viewportWidth
        viewportHeight = configuration.viewportHeight
        worldWidth     = configuration.columns * configuration.tileWidth
        worldHeight    = configuration.rows    * configuration.tileHeight

        maximumOffset = Dimension(worldWidth  - viewportWidth,
                                  worldHeight - viewportHeight)
        minimumOffset = Dimension(0, 0)
    }

    fun update(centerOn: Point) {
        logger.debug("CenterOn coordinates:  (${centerOn.x}, ${centerOn.y})")

        var cameraX = (centerOn.x * configuration.tileWidth)  - viewportWidth / 2
        var cameraY = (centerOn.y * configuration.tileHeight) - viewportHeight / 2

        if (cameraX > maximumOffset.width)
            cameraX = maximumOffset.width
        else if (cameraX < minimumOffset.width)
            cameraX = minimumOffset.width

        if (cameraY > maximumOffset.height)
            cameraY = maximumOffset.height
        else if (cameraY < minimumOffset.height)
            cameraY = minimumOffset.height

        cameraCenter = Point(cameraX, cameraY)
        logger.debug("Camera Center:  (${cameraCenter.x}, ${cameraCenter.y})")
    }

    /**
     * Renders the scene at the current camera position.
     *
     * @param graphics The graphics device to render the screen
     * @param use Function block in which the currently rendered scene is to be filled
     */
    fun renderGameplay(graphics: Graphics, use: () -> Unit) {
        graphics.translate(-cameraCenter.x.toFloat(), -cameraCenter.y.toFloat())
   
        use()
   
        graphics.translate(cameraCenter.x.toFloat(), cameraCenter.y.toFloat())
    }
}

And to render:

Code: [Select]
override fun render(gameContainer: GameContainer?, graphics: Graphics?) {
    graphics!!.setBackground(Color.white)

    camera.renderGameplay(graphics) {
        levelSkin.render(level)
    }
}

SoulBeaver

  • Newcomer
  • Posts: 20
  • Karma: +0/-0
    • View Profile
    • Email
Re: Arena (7DRL 2014)
« Reply #12 on: March 10, 2014, 08:48:11 PM »
For my last post I wanted to talk about inputs and handling them.

In the last few revisions of my code the player directly chose which inputs he would return as a direction. This is hard-coding the key to a certain behavior, even though the behavior was already abstracted. However, a player should not be able to pick and choose which inputs to ignore and which to honor, especially not which key should activate which behavior!

Therefore an InputController was born. The name reeks of OOP at its worst. Perhaps I should create no such class and have only one single function:  updateInput(): KeyMap, however, that's a story for another day.

Code: [Select]
class InputController {
    private val validInputs = mapOf("W" to Input.KEY_W,
                                    "A" to Input.KEY_A,
                                    "S" to Input.KEY_S,
                                    "D" to Input.KEY_D,
                                    "Up" to Input.KEY_UP,
                                    "Left" to Input.KEY_LEFT,
                                    "Down" to Input.KEY_DOWN,
                                    "Right" to Input.KEY_RIGHT)

    private var oldState: Input? = null
    private var currentState: Input by Delegates.notNull()

    /**
     * Poll input controllers for list of entered commands.
     *
     * @param gc Container with current Input state
     * @return list of pressed keys
     */
    fun update(gc: GameContainer): List<Pair<String, Int>> {
        currentState = gc.getInput()!!
        if (oldState == null)
            oldState = currentState

        val userInputs = validInputs map {
            val (key, value) = it

            if (currentState.isKeyPressed(value))
                key to value
            else
                null
        } filter { it != null } map { it!! }

        oldState = currentState

        return userInputs
    }
}

I've still hard-coded all the keys for now. I don't have the mental wherewithall to properly edit my settings to allow for key bindings, but that's a feature I will finish tomorrow. For now it's nice to know that inputs simply loops through a list of available keys and returns whether or not this key was pressed- not held down. Holding and pressing a key create two very distinct behaviors- movement is a lot smoother if I let a user keep a key pressed, but certains actions should not follow the same rules. It's something I'll have to refactor.

The settings should also contain the information about what action each key will perform. This is also currently hard-coded, but since the settings is a list of key-value pairs, it shouldn't be so difficult to describe.

Code: [Select]
[Action] = [Key]
MOVE_LEFT = A
MOVE_UP = W
SHOOT = SPACE
...

I read these key bindings and then create some convenient wrapper-functions such as isMovementKey(input) which will check the key against the action is was mapped to. I can then process that action. Commands are a Design Pattern excellently suited for this task and, time permitting, I will implement exactly that. For now, a simple if-else tree will suffice:

Code: [Select]
   override fun update(gc: GameContainer?, delta: Int) {
        val inputs = inputController.update(gc!!)
        logger.debug("Inputs received:  $inputs")

        inputs.forEach {
            if (isMovementKey(it)) {
                val requestedDirection = getRequestedDirection(it)

                when (requestedDirection) {
                    Direction.NORTH -> tryMove(Point(playerCoordinates.x, playerCoordinates.y - 1))
                    Direction.WEST  -> tryMove(Point(playerCoordinates.x - 1, playerCoordinates.y))
                    Direction.SOUTH -> tryMove(Point(playerCoordinates.x, playerCoordinates.y + 1))
                    Direction.EAST  -> tryMove(Point(playerCoordinates.x + 1, playerCoordinates.y))
                }
            }
        }

        camera.update(playerCoordinates)
    }

    private fun isMovementKey(input: Pair<String, Int>): Boolean = listOf("W", "A", "S", "D").containsItem(input.first)

SoulBeaver

  • Newcomer
  • Posts: 20
  • Karma: +0/-0
    • View Profile
    • Email
Re: Arena (7DRL 2014)
« Reply #13 on: March 10, 2014, 08:56:01 PM »
Alright, last post behind me! No more programming for the day!

What did I accomplish:

1) Rendered a level
2) Introduced a player
3) Gave player movement
4) Level scrolling centered on the player
5) Almost scripted key bindings

For having only very few hours today- four or so- I think I accomplished quite a bit! Just wish I had a little more time in the day, but if you're sick and need to visit a doctor, well, programming be damned. Just glad everything is better and I'm all set to spend the majority of tomorrow programming!

With any luck tomorrow will consist of:

1) Completing input handling
2) Let the player do "something"- I think building and destroying walls could be fun.
3) Add an enemy
4) Let the player die

and if I'm really ambitious

5) Implement a death screen.

Who knows, I might just sit on my ass, eat chips, and watch speed runs :P Best of luck to everyone and I hope you have a great evening/day/morning wherever you're currently at!

SoulBeaver

  • Newcomer
  • Posts: 20
  • Karma: +0/-0
    • View Profile
    • Email
Re: Arena (7DRL 2014)
« Reply #14 on: March 11, 2014, 10:31:46 AM »
Good morning and a happy new day to everyone!

I said I would try to work off my list today and I'm happy to say that it's going well so far!

1) Input

Input is complete. It didn't work as nicely as I had hoped (does that ever happen? :P), but it works, so I have that going for me which is nice. The player has nine commands to choose from at the moment- sounds more than it is, see for yourself:

Code: [Select]
# Key bindings
moveUp: W
moveLeft: A
moveDown: S
moveRight: D

toggleWallLeft: Left
toggleWallRight: Right
toggleWallUp: Up
toggleWallDown: Down

shoot: Space

Shooting doesn't actually work yet, so just ignore it for the moment. Each of the keys is presented in a human-readable format- as a character from the keyboard. Slick doesn't use any of those, instead changing them to numbers internally. There also isn't a convenient function to transform a character to a keycode, so that had to be done by hand. Nevertheless, once you have that little nuisance out of the way, you have key-bound inputs!

Code: [Select]
class InputController(val configuration: Configuration) {
    private val validInputs = listOf(KeyMap[configuration.moveUp]!!,
                                     KeyMap[configuration.moveDown]!!,
                                     KeyMap[configuration.moveLeft]!!,
                                     KeyMap[configuration.moveRight]!!,
                                     KeyMap[configuration.toggleWallUp]!!,
                                     KeyMap[configuration.toggleWallDown]!!,
                                     KeyMap[configuration.toggleWallLeft]!!,
                                     KeyMap[configuration.toggleWallRight]!!,
                                     KeyMap[configuration.shoot]!!)

    private var currentState: Input by Delegates.notNull()

    /**
     * Poll input controllers for list of entered commands.
     *
     * @param gc Container with current Input state
     * @return list of pressed keys
     */
    fun update(gc: GameContainer): List<Int> {
        currentState = gc.getInput()!!

        val userInputs = validInputs filter {
            currentState.isKeyDown(it)
        }

        return userInputs
    }
}

Which I map to commands in my Game class:

Code: [Select]
private var inputCommands: Map<Int, (Level) -> Unit> by Delegates.notNull()

inputController = InputController(configuration)
        inputCommands = mapOf(KeyMap[configuration.moveUp]!!    to ::moveUp,
                              KeyMap[configuration.moveDown]!!  to ::moveDown,
                              KeyMap[configuration.moveLeft]!!  to ::moveLeft,
                              KeyMap[configuration.moveRight]!! to ::moveRight,

                              KeyMap[configuration.toggleWallUp]!!    to ::toggleWallUp,
                              KeyMap[configuration.toggleWallDown]!!  to ::toggleWallDown,
                              KeyMap[configuration.toggleWallLeft]!!  to ::toggleWallLeft,
                              KeyMap[configuration.toggleWallRight]!! to ::toggleWallRight)

This code says, "Each key is mapped to a function which accepts a Level and returns nothing (Unit). These functions are free-floating functions contained within a file:

Code: [Select]
fun toggleWallUp(level: Level) {
    val playerCoordinates = level.playerCoordinates

    level.toggleFloor(playerCoordinates.let { Point(it.x, it.y - 1) })
}

fun toggleWallDown(level: Level) {
    val playerCoordinates = level.playerCoordinates

    level.toggleFloor(playerCoordinates.let { Point(it.x, it.y + 1) })
}

fun toggleWallLeft(level: Level) {
    val playerCoordinates = level.playerCoordinates

    level.toggleFloor(playerCoordinates.let { Point(it.x - 1, it.y) })
}

fun toggleWallRight(level: Level) {
    val playerCoordinates = level.playerCoordinates

    level.toggleFloor(playerCoordinates.let { Point(it.x + 1, it.y) })
}

And voila! We can now eat walls :)