This commit is contained in:
nelle 2023-10-28 18:45:17 -06:00
parent b5e0527986
commit b2b3c74d94
4 changed files with 133 additions and 4 deletions

View file

@ -0,0 +1,74 @@
package group.ouroboros.potrogue.builders
import group.ouroboros.potrogue.blocks.GameBlock
import group.ouroboros.potrogue.extensions.sameLevelNeighborsShuffled
import group.ouroboros.potrogue.world.World
import org.hexworks.zircon.api.data.Position3D
import org.hexworks.zircon.api.data.Size3D
// We take the worldSize from the outside world. This is useful because later it can be parameterized.
class WorldBuilder (private val worldSize: Size3D) {
private val width = worldSize.xLength
private val height = worldSize.zLength
// We maintain a Map of Blocks which we will use when we build the World
private var blocks: MutableMap<Position3D, GameBlock> = mutableMapOf()
// With makeCaves we create a fluent interface so that the users of WorldBuilder can use it in a similar manner as we build Tiles and Components in Zircon.
fun makeCaves(): WorldBuilder {
return randomizeTiles()
.smooth(8)
}
// When we build the World we take a visible size which will be used by the GameArea.
fun build(visibleSize: Size3D): World = World(blocks, visibleSize, worldSize)
private fun randomizeTiles(): WorldBuilder {
forAllPositions { pos ->
// In Kotlin if is not a statement but an expression. This means that it returns a value so we can assign it to our Map.
blocks[pos] = if (Math.random() < 0.5) {
GameBlockFactory.floor()
} else GameBlockFactory.wall()
}
return this
}
private fun smooth(iterations: Int): WorldBuilder {
// We are going to need a new Map of blocks for our smoothing because we cant do it in place. Modifying the original Map would render our cellular automata algorithm useless because it needs to calculate the new state from the old state.
val newBlocks = mutableMapOf<Position3D, GameBlock>()
repeat(iterations) {
forAllPositions { pos ->
// We create a 3D world, so we need not only x and y, but also z. What you see here is called destructuring
val (x, y, z) = pos
var floors = 0
var rocks = 0
// Here we iterate over a list of the current position and all its neighbors
pos.sameLevelNeighborsShuffled().plus(pos).forEach { neighbor ->
// And we only care about the positions which have a corresponding block (when they are not outside of the game world)
blocks.whenPresent(neighbor) { block ->
if (block.isFloor) {
floors++
} else rocks++
}
}
newBlocks[Position3D.create(x, y, z)] =
if (floors >= rocks) GameBlockFactory.floor() else GameBlockFactory.wall()
}
// When were done with smoothing we replace the old Map with the new one.
blocks = newBlocks
}
return this
}
// This is just a convenience function for iterating over all of the worlds positions which I added as a demonstration of how functions with lambdas work. Here you can pass any function which takes a Position3D and returns Unit (Unit is the equivalent of Javas Void).
private fun forAllPositions(fn: (Position3D) -> Unit) {
worldSize.fetchPositions().forEach(fn)
}
// This function is an example of defining an extension function which takes a function as a parameter. What the header of the function means here is:
//
// Augment all MutableMaps which are holding Position3D to GameBlock mappings to have a function named “whenPresent” which takes a position and a function.
private fun MutableMap<Position3D, GameBlock>.whenPresent(pos: Position3D, fn: (GameBlock) -> Unit) {
this[pos]?.let(fn)
}
}

View file

@ -0,0 +1,15 @@
package group.ouroboros.potrogue.extensions
import org.hexworks.zircon.api.data.Position3D
// We add the extension function to Position3D. We do it by defining a function not with a simple name, but by the format: fun <target class>.<function name>: return type { // ....
fun Position3D.sameLevelNeighborsShuffled(): List<Position3D> {
return (-1..1).flatMap { x ->
// We use functional programming here. flatMap and map works in a similar way as you might've been used to it in Java 8s Stream API.
(-1..1).map { y ->
// When you write extension functions this will be bound to the class being extended. So this here will point to the Position3D instance on which sameLevelNeighborsShuffled is called.
this.withRelativeX(x).withRelativeY(y)
}
// minus here will remove this position from the List and return a new List. shuffled will also return a new list which contains the same elements but shuffled.
}.minus(this).shuffled()
}

View file

@ -4,15 +4,20 @@ import group.ouroboros.potrogue.GameConfig
import group.ouroboros.potrogue.GameConfig.LOG_AREA_HEIGHT import group.ouroboros.potrogue.GameConfig.LOG_AREA_HEIGHT
import group.ouroboros.potrogue.GameConfig.SIDEBAR_WIDTH import group.ouroboros.potrogue.GameConfig.SIDEBAR_WIDTH
import group.ouroboros.potrogue.GameConfig.WINDOW_WIDTH import group.ouroboros.potrogue.GameConfig.WINDOW_WIDTH
import group.ouroboros.potrogue.builders.GameTileRepository
import group.ouroboros.potrogue.world.Game
import org.hexworks.cobalt.databinding.api.extension.toProperty
import org.hexworks.zircon.api.ComponentDecorations.box import org.hexworks.zircon.api.ComponentDecorations.box
import org.hexworks.zircon.api.Components import org.hexworks.zircon.api.Components
import org.hexworks.zircon.api.component.ColorTheme import org.hexworks.zircon.api.component.ColorTheme
import org.hexworks.zircon.api.component.ComponentAlignment import org.hexworks.zircon.api.component.ComponentAlignment
import org.hexworks.zircon.api.game.ProjectionMode
import org.hexworks.zircon.api.grid.TileGrid import org.hexworks.zircon.api.grid.TileGrid
import org.hexworks.zircon.api.view.base.BaseView import org.hexworks.zircon.api.view.base.BaseView
import org.hexworks.zircon.internal.game.impl.GameAreaComponentRenderer
class PlayView (private val grid: TileGrid, theme: ColorTheme = GameConfig.THEME) : BaseView(grid, theme) { class PlayView (private val grid: TileGrid, private val game: Game = Game.create(), theme: ColorTheme = GameConfig.THEME) : BaseView(grid, theme) {
init { init {
//Create Sidebar //Create Sidebar
val sidebar = Components.panel() val sidebar = Components.panel()
@ -22,11 +27,25 @@ class PlayView (private val grid: TileGrid, theme: ColorTheme = GameConfig.THEME
//Create area for logging //Create area for logging
val logArea = Components.logArea() val logArea = Components.logArea()
.withDecorations(box(title = "Log")) // 1 .withDecorations(box(title = "Log"))
.withSize(WINDOW_WIDTH - SIDEBAR_WIDTH, LOG_AREA_HEIGHT) .withSize(WINDOW_WIDTH - SIDEBAR_WIDTH, LOG_AREA_HEIGHT)
.withAlignmentWithin(screen, ComponentAlignment.BOTTOM_RIGHT) // 2 .withAlignmentWithin(screen, ComponentAlignment.BOTTOM_RIGHT)
.build() .build()
screen.addComponents(sidebar, logArea) //Create Game view
val gameComponent = Components.panel()
.withSize(game.world.visibleSize.to2DSize())
.withComponentRenderer(
GameAreaComponentRenderer(
gameArea = game.world,
projectionMode = ProjectionMode.TOP_DOWN.toProperty(),
fillerTile = GameTileRepository.FLOOR
)
)
.withAlignmentWithin(screen, ComponentAlignment.TOP_RIGHT)
.build()
screen.addComponents(sidebar, logArea, gameComponent)
} }
} }

View file

@ -0,0 +1,21 @@
package group.ouroboros.potrogue.world
import group.ouroboros.potrogue.GameConfig.GAME_AREA_SIZE
import group.ouroboros.potrogue.GameConfig.WORLD_SIZE
import group.ouroboros.potrogue.builders.WorldBuilder
import org.hexworks.zircon.api.data.Size3D
class Game (val world: World) {
companion object {
fun create(
worldSize: Size3D = WORLD_SIZE,
visibleSize: Size3D = GAME_AREA_SIZE
) = Game(
WorldBuilder(worldSize)
.makeCaves()
.build(visibleSize)
)
}
}