Skip to content

Instantly share code, notes, and snippets.

@nibuen
Created December 31, 2020 02:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nibuen/229af9b97bf815b7579f61c3cf544059 to your computer and use it in GitHub Desktop.
Save nibuen/229af9b97bf815b7579f61c3cf544059 to your computer and use it in GitHub Desktop.
Kotlin RackTrack Design Example
@DslMarker
annotation class RaceTrackDsl
@Serializable
@RaceTrackDsl
data class RackTrackDesign(
var name: String? = null,
var childSegments: MutableList<Segment> = mutableListOf(),
var numberLanes: Int = 2,
) {
fun finalSegment(init: Segment.() -> Unit) = Segment(trackType = TrackType.Finish).also {
childSegments.add(it)
it.init()
}
fun segments(length: Int, init: Segments.() -> Unit) = Segments(length = length).also {
(0 until it.length).forEach { _ ->
val new = it.segmentTemplate?.copy() ?: Segment()
childSegments.add(new)
it.childSegments.add(new)
}
it.init()
}
override fun toString(): String {
val track = childSegments.joinToString(separator = "") { "------------" }
val stringBuilder = StringBuilder()
stringBuilder.append("$name : \n")
stringBuilder.append("$track \n")
(0 until numberLanes).map { i ->
val hazards = childSegments.joinToString(separator = "") {
val lane = it.lanes[i]
lane.spots.joinToString(separator = "") { type ->
when (type) {
is RaceTrackType -> type.symbol
else -> "- "
}
}
}
stringBuilder.append(hazards)
stringBuilder.append("\n")
}
stringBuilder.append("$track \n")
return stringBuilder.toString()
}
companion object {
fun trackDslDesign() = rackTrackDesign(name = "Level 1") {
segments(length = 5) {}
segments(length = 5) {
random {
lanes.forEach {
it.spots[2] = RaceTrackType.AiCar.Any
}
}
}
segments(length = 10) {
random {
val lane = lanes.random()
lane.apply {
spots[1] = RaceTrackType.RoadCarPartType.Gas
spots[2] = RaceTrackType.RoadHazardType.TrafficBarrel
spots[3] = RaceTrackType.RoadHazardType.TrafficBarrel
spots[4] = RaceTrackType.RoadHazardType.TrafficBarrel
spots[5] = RaceTrackType.AiCar.Any
}
}
}
segments(length = 10) {
random {
val lane = lanes.random()
lane.apply {
spots[1] = RaceTrackType.RoadHazardType.TrafficBarrel
spots[3] = RaceTrackType.RoadHazardType.TrafficBarrel
spots[5] = RaceTrackType.RoadHazardType.TrafficBarrel
}
}
}
segments(length = 10) {
random {
val lane = lanes.random()
lane.apply {
spots[1] = RaceTrackType.RoadHazardType.TrafficBarrel
spots[2] = RaceTrackType.RoadHazardType.TrafficBarrel
spots[3] = RaceTrackType.RoadHazardType.TrafficBarrel
spots[5] = RaceTrackType.AiCar.Any
}
}
}
segments(length = 10) {
random {
val lane = lanes.random()
lane.apply {
spots[1] = RaceTrackType.RoadHazardType.TrafficBarrel
spots[3] = RaceTrackType.RoadHazardType.TrafficBarrel
spots[5] = RaceTrackType.RoadHazardType.TrafficBarrel
}
}
}
segments(length = 10) {
random {
val lane = lanes.random()
lane.apply {
spots[3] = RaceTrackType.AiCar.Any
}
}
}
finalSegment {}
}
fun trackDesign2(): RackTrackDesign {
val laneSpots = 6
val rackTrackSpots = (10 * laneSpots)..(60 * laneSpots)
val roadHazardPositions = fitRanges(
ranges = listOf(
1..5,
1..10,
1..5,
1..5,
1..5,
1..5,
1..5
),
parentRange = rackTrackSpots
)
println(roadHazardPositions)
return generate(
laneWidth = laneSpots,
raceTrackObjects = RaceTrackObjects(
trackPositions = roadHazardPositions,
trackObjects = mutableMapOf(
RaceTrackType.RoadHazardType.TrafficBarrel to 20,
RaceTrackType.AiCar.Ambulance to 6,
),
),
trackTypes = mapOf(TrackType.Basic to 61),
)
}
fun trackDesign1(): RackTrackDesign {
val laneSpots = 6
val rackTrackSpots = (10 * laneSpots)..(60 * laneSpots)
// TODO need to find a better way to guarantee spread on these
val roadHazardPositions = fitRanges(
ranges = listOf(
1..5,
1..5,
1..5,
1..5,
1..5,
1..5,
1..5,
1..5,
1..5,
1..5,
1..5,
1..5,
1..5,
1..5,
),
parentRange = rackTrackSpots
)
println(roadHazardPositions)
return generate(
laneWidth = laneSpots,
raceTrackObjects = RaceTrackObjects(
trackPositions = roadHazardPositions,
trackObjects = mutableMapOf(
RaceTrackType.AiCar.Any to 12,
),
),
trackTypes = mapOf(TrackType.Basic to 61),
)
}
fun generate(
laneWidth: Int = 6,
raceTrackObjects: RaceTrackObjects,
trackTypes: Map<TrackType, Int>,
hasFinishLine: Boolean = true
): RackTrackDesign {
// Fist layout all the tracks
val tracks = trackTypes.flatMap { (trackType, amount) ->
(0 until amount).map { Segment(trackType = trackType) }
}.toMutableList()
raceTrackObjects.fit(
tracks = tracks,
numberSpots = laneWidth
)
if (hasFinishLine) {
tracks.add(Segment(trackType = TrackType.Finish))
tracks.add(Segment(trackType = TrackType.Finish))
tracks.add(Segment(trackType = TrackType.Finish))
tracks.add(Segment(trackType = TrackType.Finish))
}
return RackTrackDesign(name = "Generated", childSegments = tracks)
}
}
}
class RaceTrackObjects(
private val trackPositions: List<IntRange>,
private val trackObjects: MutableMap<RaceTrackType, Int>,
) {
fun fit(tracks: List<Segment>, numberSpots: Int) {
val spotItems = trackObjects.flatMap { entry ->
List(entry.value) { entry.key }
}.shuffled().toMutableList()
// now find valid spots for hazards
trackPositions
.flatten()
.shuffled()
.take(spotItems.size)
.map { index ->
tracks[index / numberSpots].lanes.random().let { lane ->
val spotIndex = index % lane.width
lane.spots[spotIndex] = spotItems.removeAt(0)
}
}
}
}
@Serializable
@RaceTrackDsl
data class Segment(
var lanes: Array<Lane> = arrayOf(Lane(), Lane()),
var trackType: TrackType = TrackType.Basic,
)
@Serializable
@RaceTrackDsl
data class Lane(
val spots: Array<RaceTrackType?> = arrayOfNulls<RaceTrackType?>(6),
val width: Int = 6
)
@RaceTrackDsl
data class Segments(var length: Int = 1,
var segmentTemplate: Segment? = null,
var childSegments: MutableList<Segment> = mutableListOf()) {
fun random(init: Segment.() -> Unit): Segment {
val segment = childSegments.random()
segment.init()
return segment
}
}
fun rackTrackDesign(name: String = "DSL Generated Design", init: RackTrackDesign.() -> Unit) = RackTrackDesign(name = name).apply { init() }
fun fitRanges(ranges: List<IntRange>, parentRange: IntRange = 0..60): List<IntRange> {
val rangesPlaced = mutableListOf<IntRange>()
ranges.forEach { rangeToFit ->
require(rangeToFit.fitsWithin(parentRange)) { "Range is not bound inside parent's range" }
var placed = false
for (i in 0..10000) {
val position = parentRange.random()
val proposedRange = position..position + (rangeToFit.last - rangeToFit.first)
if (proposedRange.last >= parentRange.last) {
continue
}
val overlapped = rangesPlaced.firstOrNull { placedRange ->
placedRange.overlap(proposedRange)
}
if (overlapped != null) {
continue
}
rangesPlaced.add(IntRange(start = rangeToFit.first + position, endInclusive = rangeToFit.last + position))
placed = true
break
}
require(placed) { " Couldn't find spot to fit range" }
}
return rangesPlaced.sortedBy { it.first }
}
fun IntRange.overlap(other: IntRange): Boolean {
return first <= other.last && last >= other.first
}
fun IntRange.within(other: IntRange): Boolean {
return first >= other.first && last <= other.last
}
fun IntRange.fitsWithin(other: IntRange): Boolean {
return last - first <= other.last - other.first
}
@Serializable
sealed class RaceTrackType {
abstract val symbol: String
@Serializable
sealed class AiCar(override val symbol: String) : RaceTrackType() {
@Serializable
object Any : AiCar(symbol = "\uD83D\uDE97 ")
@Serializable
object Ambulance : AiCar(symbol = "A ")
}
@Serializable
sealed class RoadHazardType(override val symbol: String) : RaceTrackType() {
@Serializable
object TrafficBarrel : RoadHazardType(symbol = "\uD83D\uDEE2 ")
@Serializable
object Dirt : RoadHazardType(symbol = "# ")
}
@Serializable
sealed class RoadCarPartType(override val symbol: String) : RaceTrackType() {
@Serializable
object Gas : RoadCarPartType(symbol = "⛽ ")
}
}
fun main() {
val trackDesignDSl = trackDslDesign()
println(trackDesignDSl)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment