From: TheSaminator Date: Tue, 5 Jul 2022 23:03:52 +0000 (-0400) Subject: Incorporate faction- and fleet-placing into cluster generator X-Git-Url: https://gitweb.starshipfights.net/?a=commitdiff_plain;h=278365ca4916dc6667b212024af0ebb0bbab3a85;p=starship-fights Incorporate faction- and fleet-placing into cluster generator --- diff --git a/build.gradle.kts b/build.gradle.kts index 2e1af94..a415de8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -187,9 +187,3 @@ tasks.create("runAiTest", JavaExec::class.java) { classpath = sourceSets.getByName("test").runtimeClasspath mainClass.set("net.starshipfights.game.ai.AITesting") } - -tasks.create("runClusterGenTest", JavaExec::class.java) { - group = "test" - classpath = sourceSets.getByName("test").runtimeClasspath - mainClass.set("net.starshipfights.campaign.ClusterGenTesting") -} diff --git a/src/commonMain/kotlin/net/starshipfights/campaign/cluster_params.kt b/src/commonMain/kotlin/net/starshipfights/campaign/cluster_params.kt index 489f412..35db2d0 100644 --- a/src/commonMain/kotlin/net/starshipfights/campaign/cluster_params.kt +++ b/src/commonMain/kotlin/net/starshipfights/campaign/cluster_params.kt @@ -1,13 +1,15 @@ package net.starshipfights.campaign import kotlinx.serialization.Serializable +import net.starshipfights.game.FactionFlavor +import kotlin.jvm.JvmInline import kotlin.math.ceil import kotlin.math.floor import kotlin.math.roundToInt import kotlin.random.Random enum class ClusterSize(val maxStars: Int, val maxHyperlaneDistanceFactor: Double) { - SMALL(15, 1.5), MEDIUM(25, 2.0), LARGE(35, 2.5); + SMALL(20, 1.5), MEDIUM(35, 2.0), LARGE(50, 2.5); val displayName: String get() = name.lowercase().replaceFirstChar { it.uppercase() } @@ -47,11 +49,62 @@ enum class ClusterCorruption(val corruptedStarsPortion: Double) { get() = name.lowercase().replaceFirstChar { it.uppercase() } } +enum class ClusterFactionMode { + ALLOW, REQUIRE, EXCLUDE; + + val displayName: String + get() = name.lowercase().replaceFirstChar { it.uppercase() } +} + +@JvmInline +@Serializable +value class ClusterFactions private constructor(private val factions: Map) { + operator fun get(factionFlavor: FactionFlavor) = factions[factionFlavor] ?: ClusterFactionMode.ALLOW + + operator fun plus(other: ClusterFactions) = ClusterFactions(factions + other.factions) + + val seedSize: Int + get() = factions.count { (_, it) -> it == ClusterFactionMode.REQUIRE } + + fun asGenerationSequence() = sequence { + val required = factions.filterValues { it == ClusterFactionMode.REQUIRE }.keys + val included = factions.filterValues { it != ClusterFactionMode.EXCLUDE }.keys + + // first, start with the required flavors + yieldAll(required.shuffled()) + while (true) { + // continue with the included flavors + yieldAll(included.shuffled()) + } + } + + fun getRelatedFaction(faction: FactionFlavor) = factions + .filterKeys { it.loyalties == faction.loyalties } + .filterValues { it != ClusterFactionMode.EXCLUDE } + .keys.random() + + companion object { + val Default: ClusterFactions + get() = ClusterFactions(FactionFlavor.values().associateWith { ClusterFactionMode.ALLOW }) + + operator fun invoke(factions: Map) = Default + ClusterFactions(factions) + } +} + +enum class ClusterContention(val controlSpreadChance: Double, val maxFleets: Int, val fleetStrengthMult: Double) { + BLOODBATH(0.9, 5, 1.0), CONTESTED(0.65, 3, 0.8), PEACEFUL(0.5, 2, 0.5); + + val displayName: String + get() = name.lowercase().replaceFirstChar { it.uppercase() } +} + @Serializable data class ClusterGenerationSettings( val background: StarClusterBackground, val size: ClusterSize, val laneDensity: ClusterLaneDensity, val planetDensity: ClusterPlanetDensity, - val corruption: ClusterCorruption + val corruption: ClusterCorruption, + val factions: ClusterFactions, + val contention: ClusterContention ) diff --git a/src/jvmMain/kotlin/net/starshipfights/campaign/cluster_fleets.kt b/src/jvmMain/kotlin/net/starshipfights/campaign/cluster_fleets.kt index 763c1b5..e941c3b 100644 --- a/src/jvmMain/kotlin/net/starshipfights/campaign/cluster_fleets.kt +++ b/src/jvmMain/kotlin/net/starshipfights/campaign/cluster_fleets.kt @@ -1,10 +1,15 @@ package net.starshipfights.campaign import net.starshipfights.data.Id +import net.starshipfights.data.admiralty.AdmiralNameFlavor +import net.starshipfights.data.admiralty.AdmiralNames import net.starshipfights.data.admiralty.newShipName import net.starshipfights.data.invoke +import net.starshipfights.data.space.generateFleetName import net.starshipfights.game.* import net.starshipfights.game.ai.weightedRandom +import kotlin.math.roundToInt +import kotlin.random.Random val FactionFlavor.shipSource: Faction get() = when (this) { @@ -36,11 +41,11 @@ val FactionFlavor.shipSource: Faction FactionFlavor.COLEMAN_SF_BASE_VESTIGIUM -> Faction.VESTIGIUM } -fun generateNPCFleet(owner: FactionFlavor, rank: AdmiralRank): Map, Ship> { +fun generateNPCFleet(owner: FactionFlavor, rank: AdmiralRank, sizeMult: Double): Map, Ship> { val battleSize = BattleSize.values().filter { rank.maxShipTier >= it.maxTier }.associateWith { 100.0 / it.numPoints }.weightedRandom() val possibleShips = ShipType.values().filter { it.faction == owner.shipSource && it.weightClass.tier <= battleSize.maxTier } - val maxPoints = battleSize.numPoints + val maxPoints = (battleSize.numPoints * sizeMult).roundToInt() val chosenShipTypes = buildList { while (true) @@ -61,3 +66,18 @@ fun generateNPCFleet(owner: FactionFlavor, rank: AdmiralRank): Map, Shi } }.associateBy { it.id } } + +fun generateFleetPresences(owner: FactionFlavor, maxFleets: Int, sizeMult: Double): Map, FleetPresence> = (1..(maxFleets - Random.nextDiminishingInteger(maxFleets))).associate { _ -> + val admiralRank = AdmiralRank.values()[Random.nextIrwinHallInteger(AdmiralRank.values().size)] + val admiralIsFemale = owner == FactionFlavor.FELINAE_FELICES || Random.nextBoolean() + val admiralFleet = generateNPCFleet(owner, admiralRank, sizeMult) + + Id() to FleetPresence( + name = owner.generateFleetName(), + owner = owner, + ships = admiralFleet, + admiralName = AdmiralNames.randomName(AdmiralNameFlavor.forFactionFlavor(owner).random(), admiralIsFemale), + admiralIsFemale = admiralIsFemale, + admiralRank = admiralRank + ) +} diff --git a/src/jvmMain/kotlin/net/starshipfights/campaign/cluster_gen.kt b/src/jvmMain/kotlin/net/starshipfights/campaign/cluster_gen.kt index 8e547bc..e7fd3a3 100644 --- a/src/jvmMain/kotlin/net/starshipfights/campaign/cluster_gen.kt +++ b/src/jvmMain/kotlin/net/starshipfights/campaign/cluster_gen.kt @@ -46,8 +46,8 @@ class ClusterGenerator(val settings: ClusterGenerationSettings) { StarClusterView( background = settings.background, - systems = systems, - lanes = warpLanes + systems = generateFleets(assignFactions(systems, warpLanes)), + lanes = warpLanes, ) } ?: generateCluster() } @@ -346,6 +346,66 @@ class ClusterGenerator(val settings: ClusterGenerationSettings) { } }.toSet()) + private suspend fun assignFactions(starSystems: Map, StarSystem>, warpLanes: Set): Map, StarSystem> { + val systemControllers = (starSystems.keys.shuffled() zip settings.factions.asGenerationSequence().take(starSystems.size * 2 / 5).toList()).toMap().toMutableMap() + + val uncontrolledSystems = (starSystems.keys - systemControllers.keys).toMutableSet() + + while (uncontrolledSystems.size > starSystems.size / 5) { + val controlledSystems = systemControllers.keys.shuffled() + + var shouldKeepLooping = false + for (systemId in controlledSystems) { + val borderingSystems = mutableSetOf>() + + for (lane in warpLanes) { + throttle() + + if (systemId == lane.systemA) + borderingSystems += lane.systemB + if (systemId == lane.systemB) + borderingSystems += lane.systemA + } + + borderingSystems.retainAll(uncontrolledSystems) + + for (borderId in borderingSystems) { + throttle() + + uncontrolledSystems -= borderId + + if (Random.nextDouble() < settings.contention.controlSpreadChance) + systemControllers[borderId] = systemControllers.getValue(systemId).let { faction -> + if (Random.nextBoolean()) + faction + else + settings.factions.getRelatedFaction(faction) + } + + shouldKeepLooping = true + } + } + + if (!shouldKeepLooping) + break + } + + return starSystems.mapValues { (id, system) -> + system.copy(holder = systemControllers[id]) + } + } + + private suspend fun generateFleets(starSystems: Map, StarSystem>): Map, StarSystem> { + return starSystems.mapValues { (_, system) -> + throttle() + system.holder?.let { owner -> + system.copy( + fleets = generateFleetPresences(owner, settings.contention.maxFleets, settings.contention.fleetStrengthMult) + ) + } ?: system + } + } + companion object { const val SYSTEM_R = 1024.0 const val SYSTEM_K = 4 diff --git a/src/jvmMain/kotlin/net/starshipfights/campaign/cluster_test.kt b/src/jvmMain/kotlin/net/starshipfights/campaign/cluster_test.kt deleted file mode 100644 index 7379eae..0000000 --- a/src/jvmMain/kotlin/net/starshipfights/campaign/cluster_test.kt +++ /dev/null @@ -1,45 +0,0 @@ -package net.starshipfights.campaign - -import net.starshipfights.data.Id -import net.starshipfights.data.admiralty.AdmiralNameFlavor -import net.starshipfights.data.admiralty.AdmiralNames -import net.starshipfights.data.invoke -import net.starshipfights.data.space.generateFleetName -import net.starshipfights.game.* -import kotlin.random.Random - -fun StarClusterView.testPostProcess(): StarClusterView { - val ownerFlavors = FactionFlavor.values() - .toList() - .shuffled() - .repeatForever() - .take(systems.size) - .toList() - - val ownedSystems = (systems.toList().shuffled() zip ownerFlavors).associate { (systemWithId, flavor) -> - val (systemId, system) = systemWithId - - val numOfFleets = 3 - Random.nextDiminishingInteger(4) - if (numOfFleets == 0) - return@associate systemId to system - - val fleets = (1..numOfFleets).associate { _ -> - val admiralRank = AdmiralRank.values()[Random.nextIrwinHallInteger(AdmiralRank.values().size)] - val admiralIsFemale = flavor == FactionFlavor.FELINAE_FELICES || Random.nextBoolean() - val admiralFleet = generateNPCFleet(flavor, admiralRank) - - Id() to FleetPresence( - name = flavor.generateFleetName(), - owner = flavor, - ships = admiralFleet, - admiralName = AdmiralNames.randomName(AdmiralNameFlavor.forFactionFlavor(flavor).random(), admiralIsFemale), - admiralIsFemale = admiralIsFemale, - admiralRank = admiralRank - ) - } - - systemId to system.copy(holder = flavor, fleets = fleets) - } - - return copy(systems = ownedSystems) -} diff --git a/src/jvmMain/kotlin/net/starshipfights/campaign/endpoints_campaign.kt b/src/jvmMain/kotlin/net/starshipfights/campaign/endpoints_campaign.kt index d6e0ffb..6729a97 100644 --- a/src/jvmMain/kotlin/net/starshipfights/campaign/endpoints_campaign.kt +++ b/src/jvmMain/kotlin/net/starshipfights/campaign/endpoints_campaign.kt @@ -8,9 +8,7 @@ import io.ktor.routing.* import io.ktor.util.* import kotlinx.html.* import net.starshipfights.data.Id -import net.starshipfights.game.ClientMode -import net.starshipfights.game.toUrlSlug -import net.starshipfights.game.view +import net.starshipfights.game.* import net.starshipfights.labs.lab import net.starshipfights.labs.labPost @@ -31,7 +29,6 @@ fun Routing.installCampaign() { value = color.name required = true } - +Entities.nbsp +color.displayName br } @@ -46,7 +43,6 @@ fun Routing.installCampaign() { value = size.name required = true } - +Entities.nbsp +size.displayName br } @@ -61,7 +57,6 @@ fun Routing.installCampaign() { value = density.name required = true } - +Entities.nbsp +density.displayName br } @@ -76,7 +71,6 @@ fun Routing.installCampaign() { value = planets.name required = true } - +Entities.nbsp +planets.displayName br } @@ -91,11 +85,47 @@ fun Routing.installCampaign() { value = corruption.name required = true } - +Entities.nbsp +corruption.displayName br } } + h3 { +"Factional Contention" } + for (contention in ClusterContention.values()) { + val contentionId = "contention-${contention.toUrlSlug()}" + label { + htmlFor = contentionId + radioInput(name = "contention") { + id = contentionId + value = contention.name + required = true + } + +contention.displayName + br + } + } + h3 { +"Per-Faction Modes" } + for (factionFlavor in FactionFlavor.values()) + p { + strong { +factionFlavor.displayName } + br + +"Uses ${factionFlavor.shipSource.adjective} ships, is loyal to ${factionFlavor.loyalties.first().getDefiniteShortName()}." + br + for (mode in ClusterFactionMode.values()) { + val modeId = "mode-${factionFlavor.toUrlSlug()}-${mode.toUrlSlug()}" + label { + htmlFor = modeId + radioInput(name = "factions[${factionFlavor.toUrlSlug()}]") { + id = modeId + value = mode.name + required = true + if (mode == ClusterFactionMode.ALLOW) + checked = true + } + +mode.displayName + +Entities.nbsp + } + } + } submitInput { value = "Generate Star Cluster" } @@ -111,10 +141,14 @@ fun Routing.installCampaign() { val density = ClusterLaneDensity.valueOf(parameters.getOrFail("density")) val planets = ClusterPlanetDensity.valueOf(parameters.getOrFail("planets")) val corruption = ClusterCorruption.valueOf(parameters.getOrFail("corruption")) + val contention = ClusterContention.valueOf(parameters.getOrFail("contention")) + val factions = ClusterFactions(FactionFlavor.values().mapNotNull { faction -> + parameters["factions[${faction.toUrlSlug()}]"]?.let { faction to ClusterFactionMode.valueOf(it) } + }.toMap()) val cluster = ClusterGenerator( - ClusterGenerationSettings(color, size, density, planets, corruption) - ).generateCluster().testPostProcess() + ClusterGenerationSettings(color, size, density, planets, corruption, factions, contention) + ).generateCluster() val clientMode = ClientMode.CampaignMap( Id(""), diff --git a/src/jvmMain/kotlin/net/starshipfights/data/data_connection.kt b/src/jvmMain/kotlin/net/starshipfights/data/data_connection.kt index 1a417f9..494cc5f 100644 --- a/src/jvmMain/kotlin/net/starshipfights/data/data_connection.kt +++ b/src/jvmMain/kotlin/net/starshipfights/data/data_connection.kt @@ -75,7 +75,7 @@ sealed class ConnectionType { @Serializable @SerialName("external") - data class External(val url: String) : ConnectionType() { + data class External(val url: String = "mongodb://localhost:27017") : ConnectionType() { override fun createUrl() = url } } diff --git a/src/jvmMain/kotlin/net/starshipfights/data/space/star_names.kt b/src/jvmMain/kotlin/net/starshipfights/data/space/star_names.kt index 1a3abd4..f177084 100644 --- a/src/jvmMain/kotlin/net/starshipfights/data/space/star_names.kt +++ b/src/jvmMain/kotlin/net/starshipfights/data/space/star_names.kt @@ -4,6 +4,9 @@ import net.starshipfights.data.admiralty.LatinAdjective import net.starshipfights.data.admiralty.LatinNoun import net.starshipfights.data.admiralty.LatinNounForm import net.starshipfights.data.admiralty.describedBy +import net.starshipfights.game.nextDiminishingInteger +import net.starshipfights.game.nextIrwinHallInteger +import kotlin.random.Random fun newStarName(existingNames: MutableSet) = generateSequence { randomStarName() @@ -344,12 +347,10 @@ private val letters = 'A'..'Z' private fun Int.pow(x: Int) = (1..x).fold(1) { acc, _ -> acc * this } private fun generateNDigitNumber(n: Int) = (10.pow(n - 1) until 10.pow(n)).random() -private fun generateConstellationStarName(): String { - val prefix = letters.shuffled().take((1..3).random()).joinToString(separator = "") - val infix = listOf(" ", "-", "").random() - val suffix = generateNDigitNumber((2..4).random()) - return "$prefix$infix$suffix" -} +private fun generateConstellationStarNameLetters() = letters.shuffled().take(1 + Random.nextDiminishingInteger(3)).joinToString(separator = "") +private fun generateConstellationStarNameNumbers() = generateNDigitNumber(2 + Random.nextIrwinHallInteger(4)).toString() + +private fun generateConstellationStarName(): String = "${generateConstellationStarNameLetters()}-${generateConstellationStarNameNumbers()}" private val constellationNamesWithGenitives = listOf( "Antlia" to "Antliae", diff --git a/src/jvmMain/kotlin/net/starshipfights/info/views_user.kt b/src/jvmMain/kotlin/net/starshipfights/info/views_user.kt index ed52670..f333eb5 100644 --- a/src/jvmMain/kotlin/net/starshipfights/info/views_user.kt +++ b/src/jvmMain/kotlin/net/starshipfights/info/views_user.kt @@ -130,7 +130,6 @@ suspend fun ApplicationCall.manageUserPage(): HTML.() -> Unit { required = true checked = currentUser.preferredTheme == PreferredTheme.SYSTEM } - +Entities.nbsp +"System Choice" } br @@ -141,7 +140,6 @@ suspend fun ApplicationCall.manageUserPage(): HTML.() -> Unit { required = true checked = currentUser.preferredTheme == PreferredTheme.LIGHT } - +Entities.nbsp +"Light Theme" } br @@ -152,7 +150,6 @@ suspend fun ApplicationCall.manageUserPage(): HTML.() -> Unit { required = true checked = currentUser.preferredTheme == PreferredTheme.DARK } - +Entities.nbsp +"Dark Theme" } h3 { diff --git a/src/jvmTest/kotlin/net/starshipfights/campaign/ClusterGenTesting.kt b/src/jvmTest/kotlin/net/starshipfights/campaign/ClusterGenTesting.kt deleted file mode 100644 index a768989..0000000 --- a/src/jvmTest/kotlin/net/starshipfights/campaign/ClusterGenTesting.kt +++ /dev/null @@ -1,139 +0,0 @@ -package net.starshipfights.campaign - -import kotlinx.coroutines.runBlocking -import net.starshipfights.game.Vec2 -import net.starshipfights.game.magnitude -import net.starshipfights.game.plus -import net.starshipfights.game.times -import java.awt.BasicStroke -import java.awt.Color -import java.awt.Font -import java.awt.RenderingHints -import java.awt.image.BufferedImage -import java.io.File -import javax.imageio.ImageIO -import javax.swing.JOptionPane -import javax.swing.UIManager -import kotlin.math.roundToInt - -object ClusterGenTesting { - @JvmStatic - fun main(args: Array) { - UIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel") - - val clusterSizes = ClusterSize.values().toList() - val clusterSizeOptions = clusterSizes.map { it.toString() }.toTypedArray() - - val clusterSizeIndex = JOptionPane.showOptionDialog( - null, "Please select the size of your star cluster", - "Generate Star Cluster", JOptionPane.DEFAULT_OPTION, - JOptionPane.QUESTION_MESSAGE, null, - clusterSizeOptions, clusterSizeOptions[0] - ) - - if (clusterSizeIndex == JOptionPane.CLOSED_OPTION) return - - val laneDensities = ClusterLaneDensity.values().toList() - val laneDensityOptions = laneDensities.map { it.toString() }.toTypedArray() - - val laneDensityIndex = JOptionPane.showOptionDialog( - null, "Please select the warp-lane density of your star cluster", - "Generate Star Cluster", JOptionPane.DEFAULT_OPTION, - JOptionPane.QUESTION_MESSAGE, null, - laneDensityOptions, laneDensityOptions[0] - ) - - if (laneDensityIndex == JOptionPane.CLOSED_OPTION) return - - val planetDensities = ClusterPlanetDensity.values().toList() - val planetDensityOptions = planetDensities.map { it.toString() }.toTypedArray() - - val planetDensityIndex = JOptionPane.showOptionDialog( - null, "Please select the planet density of your star cluster", - "Generate Star Cluster", JOptionPane.DEFAULT_OPTION, - JOptionPane.QUESTION_MESSAGE, null, - planetDensityOptions, planetDensityOptions[0] - ) - - if (planetDensityIndex == JOptionPane.CLOSED_OPTION) return - - val settings = ClusterGenerationSettings( - StarClusterBackground.RED, - clusterSizes[clusterSizeIndex], - laneDensities[laneDensityIndex], - planetDensities[planetDensityIndex], - ClusterCorruption.MATERIAL - ) - - val starCluster = runBlocking { - ClusterGenerator(settings).generateCluster() - } - - val radius = (starCluster.systems.maxOf { (_, system) -> system.position.vector.magnitude } * SCALE_FACTOR) - val radiusShift = Vec2(radius, radius) - - val imageSize = ((radius + IMAGE_MARGIN) * 2).roundToInt() - val image = BufferedImage(imageSize, imageSize, BufferedImage.TYPE_INT_ARGB) - val g2d = image.createGraphics() - try { - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) - - g2d.font = Font(Font.SANS_SERIF, Font.BOLD, 16) - - g2d.color = Color.white - g2d.fillRect(0, 0, imageSize, imageSize) - - g2d.color = Color.black - g2d.fillRect(1, 1, imageSize - 2, imageSize - 2) - - g2d.color = Color.decode("#3366CC") - g2d.stroke = BasicStroke(2.5f) - for ((aId, bId) in starCluster.lanes) { - val aPos = starCluster.systems.getValue(aId).position.vector * SCALE_FACTOR + radiusShift - val bPos = starCluster.systems.getValue(bId).position.vector * SCALE_FACTOR + radiusShift - - g2d.drawLine( - (aPos.x + IMAGE_MARGIN).roundToInt(), - (aPos.y + IMAGE_MARGIN).roundToInt(), - (bPos.x + IMAGE_MARGIN).roundToInt(), - (bPos.y + IMAGE_MARGIN).roundToInt(), - ) - } - - g2d.color = Color.decode("#CC9933") - for (system in starCluster.systems.values) { - val pos = system.position.vector * SCALE_FACTOR + radiusShift - val r = system.radius * SCALE_FACTOR - - g2d.fillOval( - (pos.x + IMAGE_MARGIN - r).roundToInt(), - (pos.y + IMAGE_MARGIN - r).roundToInt(), - (r * 2).roundToInt(), - (r * 2).roundToInt(), - ) - } - - // Draw names - g2d.color = Color.white - for (system in starCluster.systems.values) { - val pos = system.position.vector * SCALE_FACTOR + radiusShift - - val minusX = g2d.fontMetrics.stringWidth(system.name) * 0.5 - val minusY = g2d.fontMetrics.ascent * -0.5 - - g2d.drawString( - system.name, - (pos.x + IMAGE_MARGIN - minusX).roundToInt(), - (pos.y + IMAGE_MARGIN - minusY).roundToInt(), - ) - } - } finally { - g2d.dispose() - } - - ImageIO.write(image, "PNG", File("test_output/cluster_gen.png")) - } - - private const val IMAGE_MARGIN = 51.2 - private const val SCALE_FACTOR = 0.32 -}