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
}
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) {
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
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])
}
}
}
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) {
}
}
-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 ->
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]
}
}
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
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)
EXTENDED_DUTY(Subplot::ExtendedDuty),
NO_QUARTER(Subplot::NoQuarter),
VENDETTA(Subplot::Vendetta),
+ PLAUSIBLE_DENIABILITY(Subplot::PlausibleDeniability),
RECOVER_INFORMANT(Subplot::RecoverInformant),
}
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) {
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
isFemale = true,
faction = faction,
acumen = AdmiralRank.values().first {
- it.maxShipWeightClass.tier >= forBattleSize.maxWeightClass.tier
+ it.maxBattleSize >= forBattleSize
}.minAcumen + 500,
money = 0
)
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]
}
}
}
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
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(
-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 }
),
)
}
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(
-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,
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) {
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) {