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