Created
December 31, 2020 02:29
-
-
Save nibuen/229af9b97bf815b7579f61c3cf544059 to your computer and use it in GitHub Desktop.
Kotlin RackTrack Design Example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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