Add new subplot and refactor ship-tier code
authorTheSaminator <TheSaminator@users.noreply.github.com>
Wed, 8 Jun 2022 18:33:38 +0000 (14:33 -0400)
committerTheSaminator <TheSaminator@users.noreply.github.com>
Wed, 8 Jun 2022 18:33:38 +0000 (14:33 -0400)
src/commonMain/kotlin/net/starshipfights/game/admiralty.kt
src/commonMain/kotlin/net/starshipfights/game/ai/ai_behaviors.kt
src/commonMain/kotlin/net/starshipfights/game/ai/ai_optimization.kt
src/commonMain/kotlin/net/starshipfights/game/game_subplots.kt
src/commonMain/kotlin/net/starshipfights/game/matchmaking.kt
src/commonMain/kotlin/net/starshipfights/game/ship_types.kt
src/jvmMain/kotlin/net/starshipfights/data/admiralty/admirals.kt
src/jvmMain/kotlin/net/starshipfights/data/admiralty/ship_prices.kt
src/jvmMain/kotlin/net/starshipfights/game/game_start_jvm.kt
src/jvmMain/kotlin/net/starshipfights/info/views_gdpr.kt
src/jvmMain/kotlin/net/starshipfights/info/views_ships.kt

index e6506b6b9b455954573da75b121964342653371c..f6203d3b4ef271947cbb81ad7979ee6e486fcc46 100644 (file)
@@ -10,6 +10,15 @@ enum class AdmiralRank {
        HIGH_ADMIRAL,
        LORD_ADMIRAL;
        
+       val maxShipTier: ShipTier
+               get() = when (this) {
+                       REAR_ADMIRAL -> ShipTier.CRUISER
+                       VICE_ADMIRAL -> ShipTier.BATTLECRUISER
+                       ADMIRAL -> ShipTier.BATTLESHIP
+                       HIGH_ADMIRAL -> ShipTier.BATTLESHIP
+                       LORD_ADMIRAL -> ShipTier.TITAN
+               }
+       
        val maxShipWeightClass: ShipWeightClass
                get() = when (this) {
                        REAR_ADMIRAL -> ShipWeightClass.CRUISER
@@ -20,7 +29,7 @@ enum class AdmiralRank {
                }
        
        val maxBattleSize: BattleSize
-               get() = BattleSize.values().last { it.maxWeightClass.tier <= maxShipWeightClass.tier }
+               get() = BattleSize.values().last { it.minRank <= this }
        
        val minAcumen: Int
                get() = when (this) {
index 803ae3a164cf7cb346a4037c1184ebca53327ae7..2982e2d610d73d7f8c521011bf6b7fe4630b7982 100644 (file)
@@ -43,7 +43,7 @@ suspend fun AIPlayer.behave(instincts: Instincts, mySide: GlobalSide) {
                                                        is ChatEntry.ShipIdentified -> {
                                                                val identifiedShip = state.ships[msg.ship] ?: continue
                                                                if (identifiedShip.owner != mySide)
-                                                                       brain[shipAttackPriority forShip identifiedShip.id] += identifiedShip.ship.shipType.weightClass.tier.toDouble().pow(instincts[combatTargetShipWeight])
+                                                                       brain[shipAttackPriority forShip identifiedShip.id] += (identifiedShip.ship.shipType.weightClass.tier.ordinal + 1.5).pow(instincts[combatTargetShipWeight])
                                                        }
                                                        is ChatEntry.ShipEscaped -> {
                                                                // handle escaping ship
@@ -70,7 +70,7 @@ suspend fun AIPlayer.behave(instincts: Instincts, mySide: GlobalSide) {
                                                        is ChatEntry.ShipDestroyed -> {
                                                                val targetedShip = state.ships[msg.ship] ?: continue
                                                                if (targetedShip.owner == mySide && msg.destroyedBy is ShipAttacker.EnemyShip)
-                                                                       brain[shipAttackPriority forShip msg.destroyedBy.id] += instincts[combatAvengeShipwrecks] * targetedShip.ship.shipType.weightClass.tier.toDouble().pow(instincts[combatAvengeShipWeight])
+                                                                       brain[shipAttackPriority forShip msg.destroyedBy.id] += instincts[combatAvengeShipwrecks] * (targetedShip.ship.shipType.weightClass.tier.ordinal + 1.5).pow(instincts[combatAvengeShipWeight])
                                                        }
                                                }
                                        }
@@ -292,11 +292,11 @@ suspend fun AIPlayer.behave(instincts: Instincts, mySide: GlobalSide) {
 fun deploy(gameState: GameState, mySide: GlobalSide, instincts: Instincts): Map<Id<ShipInstance>, Position> {
        val size = gameState.battleInfo.size
        val totalPoints = size.numPoints
-       val maxWC = size.maxWeightClass
+       val maxTier = size.maxTier
        
        val myStart = gameState.start.playerStart(mySide)
        
-       val deployable = myStart.deployableFleet.values.filter { it.shipType.weightClass.tier <= maxWC.tier }.toMutableSet()
+       val deployable = myStart.deployableFleet.values.filter { it.shipType.weightClass.tier <= maxTier }.toMutableSet()
        val deployed = mutableSetOf<Ship>()
        
        while (true) {
index 8b755b27d8a1a4f1da876377801e4267d2e6cc08..37b07173dc6e0c8e6fed51cb5f44d7cb49ec3b84 100644 (file)
@@ -157,11 +157,6 @@ suspend fun performTestSession(gameState: GameState, hostInstincts: Instincts, g
        }
 }
 
-val BattleSize.minRank: AdmiralRank
-       get() = AdmiralRank.values().first {
-               it.maxShipWeightClass.tier >= maxWeightClass.tier
-       }
-
 fun generateFleet(faction: Faction, rank: AdmiralRank, side: GlobalSide): Map<Id<Ship>, Ship> = ShipWeightClass.values()
        .flatMap { swc ->
                val shipTypes = ShipType.values().filter { st ->
@@ -171,7 +166,7 @@ fun generateFleet(faction: Faction, rank: AdmiralRank, side: GlobalSide): Map<Id
                if (shipTypes.isEmpty())
                        emptyList()
                else
-                       (0 until ((rank.maxShipWeightClass.tier - swc.tier + 1) * 2).coerceAtLeast(0)).map { i ->
+                       (0 until ((rank.maxShipTier.ordinal - swc.tier.ordinal + 1) * 2).coerceAtLeast(0)).map { i ->
                                shipTypes[i % shipTypes.size]
                        }
        }
index e47ef820f792f15687fa3118e50041a2c2fc84a4..ec6ac2229eb6b9eaac5bd3e9afc3fd683603bd3a 100644 (file)
@@ -105,6 +105,7 @@ sealed class Subplot {
                        val destroyedShipPointCount = enemyWrecks.filter { !it.isEscape }.sumOf { it.ship.pointCost }
                        
                        val success = when {
+                               gameState.phase == GamePhase.Deploy -> null
                                destroyedShipPointCount * 2 >= totalEnemyShipPointCount -> true
                                escapedShipPointCount * 2 >= totalEnemyShipPointCount -> false
                                else -> null
@@ -173,6 +174,48 @@ sealed class Subplot {
                else outcome
        }
        
+       @Serializable
+       class PlausibleDeniability private constructor(override val forPlayer: GlobalSide, private val againstShip: Id<ShipInstance>?, private val outcome: SubplotOutcome) : Subplot() {
+               constructor(forPlayer: GlobalSide) : this(forPlayer, null, SubplotOutcome.UNDECIDED)
+               constructor(forPlayer: GlobalSide, againstShip: Id<ShipInstance>) : this(forPlayer, againstShip, SubplotOutcome.UNDECIDED)
+               
+               override val type: SubplotType
+                       get() = SubplotType.PLAUSIBLE_DENIABILITY
+               
+               override val displayName: String
+                       get() = "Plausible Deniability"
+               
+               override fun displayObjective(gameState: GameState): GameObjective? {
+                       val shipName = gameState.getShipInfoOrNull(againstShip ?: return null)?.fullName ?: return null
+                       return GameObjective("Ensure that the $shipName is destroyed", outcome.toSuccess)
+               }
+               
+               override fun onAfterDeployShips(gameState: GameState): GameState {
+                       if (gameState.ships[againstShip] != null) return gameState
+                       
+                       val myShips = gameState.ships.values.filter { it.owner == forPlayer }
+                       val lowestShipTier = myShips.minOf { it.ship.shipType.weightClass }
+                       val shipsNotOfLowestTier = myShips.filter { it.ship.shipType.weightClass != lowestShipTier }.ifEmpty { myShips }
+                       
+                       val arkancideShip = shipsNotOfLowestTier.random().id
+                       return gameState.modifySubplotData(PlausibleDeniability(forPlayer, arkancideShip, SubplotOutcome.UNDECIDED))
+               }
+               
+               override fun onGameStateChanged(gameState: GameState): GameState {
+                       if (outcome != SubplotOutcome.UNDECIDED) return gameState
+                       
+                       val assassinateShipWreck = gameState.destroyedShips[againstShip ?: return gameState] ?: return gameState
+                       return if (assassinateShipWreck.isEscape)
+                               gameState.modifySubplotData(PlausibleDeniability(forPlayer, againstShip, SubplotOutcome.LOST))
+                       else
+                               gameState.modifySubplotData(PlausibleDeniability(forPlayer, againstShip, SubplotOutcome.WON))
+               }
+               
+               override fun getFinalGameResult(gameState: GameState, winner: GlobalSide?) = if (outcome == SubplotOutcome.UNDECIDED)
+                       SubplotOutcome.LOST
+               else outcome
+       }
+       
        @Serializable
        class RecoverInformant private constructor(override val forPlayer: GlobalSide, private val onBoardShip: Id<ShipInstance>?, private val outcome: SubplotOutcome, private val mostRecentChatMessages: Moment?) : Subplot() {
                constructor(forPlayer: GlobalSide) : this(forPlayer, null, SubplotOutcome.UNDECIDED, null)
@@ -237,6 +280,7 @@ enum class SubplotType(val factory: (GlobalSide) -> Subplot) {
        EXTENDED_DUTY(Subplot::ExtendedDuty),
        NO_QUARTER(Subplot::NoQuarter),
        VENDETTA(Subplot::Vendetta),
+       PLAUSIBLE_DENIABILITY(Subplot::PlausibleDeniability),
        RECOVER_INFORMANT(Subplot::RecoverInformant),
 }
 
index a3fda7fbd1bbad29444e3445ec9063ae0fe6b455..90b9f2d8613298a7299fbcfabe3fdc1d29872652 100644 (file)
@@ -3,27 +3,27 @@ package net.starshipfights.game
 import kotlinx.serialization.Serializable
 import net.starshipfights.data.Id
 
-enum class BattleSize(val numPoints: Int, val maxWeightClass: ShipWeightClass, val displayName: String) {
-       SKIRMISH(600, ShipWeightClass.CRUISER, "Skirmish"),
-       RAID(800, ShipWeightClass.CRUISER, "Raid"),
-       FIREFIGHT(1000, ShipWeightClass.BATTLECRUISER, "Firefight"),
-       BATTLE(1300, ShipWeightClass.BATTLECRUISER, "Battle"),
-       GRAND_CLASH(1600, ShipWeightClass.BATTLESHIP, "Grand Clash"),
-       APOCALYPSE(2000, ShipWeightClass.BATTLESHIP, "Apocalypse"),
-       LEGENDARY_STRUGGLE(2400, ShipWeightClass.COLOSSUS, "Legendary Struggle"),
-       CRUCIBLE_OF_HISTORY(3000, ShipWeightClass.COLOSSUS, "Crucible of History");
+enum class BattleSize(val numPoints: Int, val maxTier: ShipTier, val minRank: AdmiralRank, val displayName: String) {
+       SKIRMISH(600, ShipTier.CRUISER, AdmiralRank.REAR_ADMIRAL, "Skirmish"),
+       RAID(800, ShipTier.CRUISER, AdmiralRank.REAR_ADMIRAL, "Raid"),
+       FIREFIGHT(1000, ShipTier.BATTLECRUISER, AdmiralRank.REAR_ADMIRAL, "Firefight"),
+       BATTLE(1300, ShipTier.BATTLECRUISER, AdmiralRank.REAR_ADMIRAL, "Battle"),
+       GRAND_CLASH(1600, ShipTier.BATTLESHIP, AdmiralRank.ADMIRAL, "Grand Clash"),
+       APOCALYPSE(2000, ShipTier.BATTLESHIP, AdmiralRank.ADMIRAL, "Apocalypse"),
+       LEGENDARY_STRUGGLE(2400, ShipTier.TITAN, AdmiralRank.ADMIRAL, "Legendary Struggle"),
+       CRUCIBLE_OF_HISTORY(3000, ShipTier.TITAN, AdmiralRank.ADMIRAL, "Crucible of History");
 }
 
 val BattleSize.numSubplotsPerPlayer: Int
        get() = when (this) {
                BattleSize.SKIRMISH -> 0
                BattleSize.RAID -> 0
-               BattleSize.FIREFIGHT -> 0
-               BattleSize.BATTLE -> (0..1).random()
+               BattleSize.FIREFIGHT -> (0..1).random()
+               BattleSize.BATTLE -> 1
                BattleSize.GRAND_CLASH -> 1
                BattleSize.APOCALYPSE -> 1
-               BattleSize.LEGENDARY_STRUGGLE -> 1
-               BattleSize.CRUCIBLE_OF_HISTORY -> (1..2).random()
+               BattleSize.LEGENDARY_STRUGGLE -> (1..2).random()
+               BattleSize.CRUCIBLE_OF_HISTORY -> 2
        }
 
 enum class BattleBackground(val displayName: String, val color: String) {
index a88f4331410ceea6cd0cd438bf49a5f4c5300799..379a8c3b988ff0f604f1ee7e501640b901999b6a 100644 (file)
@@ -1,40 +1,47 @@
 package net.starshipfights.game
 
+enum class ShipTier {
+       ESCORT, LIGHT_CRUISER, CRUISER, BATTLECRUISER, BATTLESHIP, TITAN;
+       
+       val displayName: String
+               get() = name.lowercase().split('_').joinToString(separator = " ") { word -> word.replaceFirstChar { c -> c.uppercase() } }
+}
+
 enum class ShipWeightClass(
        val meshIndex: Int,
-       val tier: Int
+       val tier: ShipTier
 ) {
        // General
-       ESCORT(1, 0),
-       DESTROYER(2, 1),
-       CRUISER(3, 2),
-       BATTLECRUISER(4, 3),
-       BATTLESHIP(5, 4),
+       ESCORT(1, ShipTier.ESCORT),
+       DESTROYER(2, ShipTier.LIGHT_CRUISER),
+       CRUISER(3, ShipTier.CRUISER),
+       BATTLECRUISER(4, ShipTier.BATTLECRUISER),
+       BATTLESHIP(5, ShipTier.BATTLESHIP),
        
        // NdRC-specific
-       BATTLE_BARGE(5, 3),
+       BATTLE_BARGE(5, ShipTier.BATTLECRUISER),
        
        // Masra Draetsen-specific
-       GRAND_CRUISER(4, 3),
-       COLOSSUS(5, 5),
+       GRAND_CRUISER(4, ShipTier.BATTLECRUISER),
+       COLOSSUS(5, ShipTier.TITAN),
        
        // Felinae Felices-specific
-       FF_ESCORT(1, 1),
-       FF_DESTROYER(2, 2),
-       FF_CRUISER(3, 3),
-       FF_BATTLECRUISER(4, 4),
-       FF_BATTLESHIP(5, 5),
+       FF_ESCORT(1, ShipTier.LIGHT_CRUISER),
+       FF_DESTROYER(2, ShipTier.CRUISER),
+       FF_CRUISER(3, ShipTier.BATTLECRUISER),
+       FF_BATTLECRUISER(4, ShipTier.BATTLESHIP),
+       FF_BATTLESHIP(5, ShipTier.TITAN),
        
        // Isarnareykk-specific
-       AUXILIARY_SHIP(1, 0),
-       LIGHT_CRUISER(2, 1),
-       MEDIUM_CRUISER(3, 2),
-       HEAVY_CRUISER(4, 4),
+       AUXILIARY_SHIP(1, ShipTier.ESCORT),
+       LIGHT_CRUISER(2, ShipTier.LIGHT_CRUISER),
+       MEDIUM_CRUISER(3, ShipTier.CRUISER),
+       HEAVY_CRUISER(4, ShipTier.BATTLECRUISER),
        
        // Vestigium-specific
-       FRIGATE(1, 0),
-       LINE_SHIP(3, 2),
-       DREADNOUGHT(5, 4),
+       FRIGATE(1, ShipTier.ESCORT),
+       LINE_SHIP(3, ShipTier.CRUISER),
+       DREADNOUGHT(5, ShipTier.BATTLESHIP),
        ;
        
        val displayName: String
index 7c13054e2cd4b841eeab4ee5bd992c843c0123f4..ae3a2480a4a5fa94c1e5ca715d0413e692b8e56e 100644 (file)
@@ -47,7 +47,7 @@ fun genAI(faction: Faction, forBattleSize: BattleSize) = Admiral(
        isFemale = true,
        faction = faction,
        acumen = AdmiralRank.values().first {
-               it.maxShipWeightClass.tier >= forBattleSize.maxWeightClass.tier
+               it.maxBattleSize >= forBattleSize
        }.minAcumen + 500,
        money = 0
 )
@@ -141,7 +141,7 @@ fun generateFleet(admiral: Admiral): List<ShipInDrydock> = ShipWeightClass.value
                if (shipTypes.isEmpty())
                        emptyList()
                else
-                       (0 until ((admiral.rank.maxShipWeightClass.tier - swc.tier + 1) * 2).coerceAtLeast(0)).map { i ->
+                       (0 until ((admiral.rank.maxShipTier.ordinal - swc.tier.ordinal + 1) * 2).coerceAtLeast(0)).map { i ->
                                shipTypes[i % shipTypes.size]
                        }
        }
index 5acf4fdc606a97218fbea2fc305241de8f2ce3d2..a0502d587865db23753c427be5077c3c13ce454a 100644 (file)
@@ -18,7 +18,7 @@ fun ShipType.buyPriceChecked(admiral: Admiral, ownedShips: List<ShipInDrydock>):
 }
 
 fun ShipType.buyPrice(admiral: Admiral, ownedShips: List<ShipInDrydock>): Int? {
-       if (weightClass.tier > admiral.rank.maxShipWeightClass.tier) return null
+       if (weightClass.tier > admiral.rank.maxShipTier) return null
        if (weightClass.isUnique && ownedShips.any { it.shipType.weightClass == weightClass }) return null
        return when {
                admiral.faction == faction -> buyPrice
index 950e732fd99cb8f39c4fd61f71aea7cea90734d2..a6d573338c79c249ba5fe020c8e34fc509e32622 100644 (file)
@@ -23,7 +23,7 @@ suspend fun generateGameStart(hostInfo: InGameAdmiral, guestInfo: InGameAdmiral,
                        PI / 2,
                        PickBoundary.Rectangle(hostDeployCenter, deployWidth2, deployLength2),
                        PI / 2,
-                       getAdmiralsShips(hostInfo.id.reinterpret()).filterValues { it.shipType.weightClass.tier <= battleInfo.size.maxWeightClass.tier }
+                       getAdmiralsShips(hostInfo.id.reinterpret()).filterValues { it.shipType.weightClass.tier <= battleInfo.size.maxTier }
                ),
                
                PlayerStart(
@@ -31,7 +31,7 @@ suspend fun generateGameStart(hostInfo: InGameAdmiral, guestInfo: InGameAdmiral,
                        -PI / 2,
                        PickBoundary.Rectangle(guestDeployCenter, deployWidth2, deployLength2),
                        -PI / 2,
-                       getAdmiralsShips(guestInfo.id.reinterpret()).filterValues { it.shipType.weightClass.tier <= battleInfo.size.maxWeightClass.tier }
+                       getAdmiralsShips(guestInfo.id.reinterpret()).filterValues { it.shipType.weightClass.tier <= battleInfo.size.maxTier }
                ),
        )
 }
@@ -58,7 +58,7 @@ suspend fun generateTrainingInitialState(playerInfo: InGameAdmiral, enemyFaction
                                PickBoundary.Rectangle(hostDeployCenter, deployWidth2, deployLength2),
                                PI / 2,
                                getAdmiralsShips(playerInfo.id.reinterpret())
-                                       .filterValues { it.shipType.weightClass.tier <= battleInfo.size.maxWeightClass.tier }
+                                       .filterValues { it.shipType.weightClass.tier <= battleInfo.size.maxTier }
                        ),
                        
                        PlayerStart(
@@ -68,7 +68,7 @@ suspend fun generateTrainingInitialState(playerInfo: InGameAdmiral, enemyFaction
                                -PI / 2,
                                generateFleet(aiAdmiral)
                                        .associate { it.shipData.id to it.shipData }
-                                       .filterValues { it.shipType.weightClass.tier <= battleInfo.size.maxWeightClass.tier }
+                                       .filterValues { it.shipType.weightClass.tier <= battleInfo.size.maxTier }
                        )
                ),
                hostInfo = playerInfo,
index 20e59a15ded7153d6b65e1e9d33be021be5145e9..4c016b1b79687b3341f06914a80d68e89c60f9a0 100644 (file)
@@ -137,7 +137,7 @@ suspend fun ApplicationCall.privateInfo(): String {
                        appendLine("Admiral serves the ${admiral.faction.navyName}")
                        appendLine("Admiral's experience is ${admiral.acumen} acumen")
                        appendLine("Admiral's monetary wealth is ${admiral.money} ${admiral.faction.currencyName}")
-                       appendLine("Admiral can command ships as big as a ${admiral.rank.maxShipWeightClass.displayName}")
+                       appendLine("Admiral can command ships as big as ${admiral.rank.maxShipTier.displayName} size")
                        val ships = admiralShips[admiral.id]?.first.orEmpty()
                        appendLine("Admiral has ${ships.size} ships:")
                        for (ship in ships) {
index 648dda0075a2adb1221b4dc5396b4c92f392ec30..362b5c6d447e2711fcbf082e7a8c2bd3d81f5e69 100644 (file)
@@ -37,7 +37,7 @@ suspend fun ApplicationCall.shipsPage(): HTML.() -> Unit = page("Strategema Naut
                        
                        faction.blurbDesc(consumer)
                        
-                       for ((weightClass, weightedShipTypes) in factionShipTypes.groupBy { it.weightClass }.toSortedMap(Comparator.comparingInt(ShipWeightClass::tier))) {
+                       for ((weightClass, weightedShipTypes) in factionShipTypes.groupBy { it.weightClass }.toSortedMap(Comparator.comparing(ShipWeightClass::tier))) {
                                h3 { +weightClass.displayName }
                                ul {
                                        for (shipType in weightedShipTypes) {