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")
-}
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() }
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<FactionFlavor, ClusterFactionMode>) {
+ 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<FactionFlavor, ClusterFactionMode>) = 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
)
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) {
FactionFlavor.COLEMAN_SF_BASE_VESTIGIUM -> Faction.VESTIGIUM
}
-fun generateNPCFleet(owner: FactionFlavor, rank: AdmiralRank): Map<Id<Ship>, Ship> {
+fun generateNPCFleet(owner: FactionFlavor, rank: AdmiralRank, sizeMult: Double): Map<Id<Ship>, 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)
}
}.associateBy { it.id }
}
+
+fun generateFleetPresences(owner: FactionFlavor, maxFleets: Int, sizeMult: Double): Map<Id<FleetPresence>, 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<FleetPresence>() to FleetPresence(
+ name = owner.generateFleetName(),
+ owner = owner,
+ ships = admiralFleet,
+ admiralName = AdmiralNames.randomName(AdmiralNameFlavor.forFactionFlavor(owner).random(), admiralIsFemale),
+ admiralIsFemale = admiralIsFemale,
+ admiralRank = admiralRank
+ )
+}
StarClusterView(
background = settings.background,
- systems = systems,
- lanes = warpLanes
+ systems = generateFleets(assignFactions(systems, warpLanes)),
+ lanes = warpLanes,
)
} ?: generateCluster()
}
}
}.toSet())
+ private suspend fun assignFactions(starSystems: Map<Id<StarSystem>, StarSystem>, warpLanes: Set<WarpLane>): Map<Id<StarSystem>, 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<Id<StarSystem>>()
+
+ 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<Id<StarSystem>, StarSystem>): Map<Id<StarSystem>, 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
+++ /dev/null
-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<FleetPresence>() 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)
-}
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
value = color.name
required = true
}
- +Entities.nbsp
+color.displayName
br
}
value = size.name
required = true
}
- +Entities.nbsp
+size.displayName
br
}
value = density.name
required = true
}
- +Entities.nbsp
+density.displayName
br
}
value = planets.name
required = true
}
- +Entities.nbsp
+planets.displayName
br
}
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"
}
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(""),
@Serializable
@SerialName("external")
- data class External(val url: String) : ConnectionType() {
+ data class External(val url: String = "mongodb://localhost:27017") : ConnectionType() {
override fun createUrl() = url
}
}
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<String>) = generateSequence {
randomStarName()
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",
required = true
checked = currentUser.preferredTheme == PreferredTheme.SYSTEM
}
- +Entities.nbsp
+"System Choice"
}
br
required = true
checked = currentUser.preferredTheme == PreferredTheme.LIGHT
}
- +Entities.nbsp
+"Light Theme"
}
br
required = true
checked = currentUser.preferredTheme == PreferredTheme.DARK
}
- +Entities.nbsp
+"Dark Theme"
}
h3 {
+++ /dev/null
-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<String>) {
- 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
-}