data class DeployShip(val ship: Id<Ship>) : PlayerAbilityType() {
override suspend fun beginOnClient(gameState: GameState, playerSide: GlobalSide, pick: suspend (PickRequest) -> PickResponse?): PlayerAbilityData? {
if (gameState.phase != GamePhase.Deploy) return null
- if (gameState.ready == playerSide) return null
+ if (gameState.doneWithPhase == playerSide) return null
val pickBoundary = gameState.start.playerStart(playerSide).deployZone
val playerStart = gameState.start.playerStart(playerSide)
@Serializable
data class UndeployShip(val ship: Id<ShipInstance>) : PlayerAbilityType() {
override suspend fun beginOnClient(gameState: GameState, playerSide: GlobalSide, pick: suspend (PickRequest) -> PickResponse?): PlayerAbilityData? {
- return if (gameState.phase == GamePhase.Deploy && gameState.ready != playerSide) PlayerAbilityData.UndeployShip else null
+ return if (gameState.phase == GamePhase.Deploy && gameState.doneWithPhase != playerSide) PlayerAbilityData.UndeployShip else null
}
override fun finishOnServer(gameState: GameState, playerSide: GlobalSide, data: PlayerAbilityData): GameEvent {
data class MoveShip(override val ship: Id<ShipInstance>) : PlayerAbilityType(), ShipAbility {
override suspend fun beginOnClient(gameState: GameState, playerSide: GlobalSide, pick: suspend (PickRequest) -> PickResponse?): PlayerAbilityData? {
if (gameState.phase !is GamePhase.Move) return null
+ if (!gameState.canShipMove(ship)) return null
+
val shipInstance = gameState.ships[ship] ?: return null
if (shipInstance.isDoneCurrentPhase) return null
override fun finishOnServer(gameState: GameState, playerSide: GlobalSide, data: PlayerAbilityData): GameEvent {
if (data !is PlayerAbilityData.MoveShip) return GameEvent.InvalidAction("Internal error from using player ability")
+ if (!gameState.canShipMove(ship)) return GameEvent.InvalidAction("You do not have the initiative")
val shipInstance = gameState.ships[ship] ?: return GameEvent.InvalidAction("That ship does not exist")
if (shipInstance.isDoneCurrentPhase) return GameEvent.InvalidAction("Ships cannot be moved twice")
ships = newShips,
destroyedShips = newWrecks,
chatBox = newChatEntries,
- )
+ ).withRecalculatedInitiative { calculateMovePhaseInitiative() }
)
}
}
data class UseInertialessDrive(override val ship: Id<ShipInstance>) : PlayerAbilityType(), ShipAbility {
override suspend fun beginOnClient(gameState: GameState, playerSide: GlobalSide, pick: suspend (PickRequest) -> PickResponse?): PlayerAbilityData? {
if (gameState.phase !is GamePhase.Move) return null
+ if (!gameState.canShipMove(ship)) return null
+
val shipInstance = gameState.ships[ship] ?: return null
if (shipInstance.isDoneCurrentPhase) return null
+
if (!shipInstance.canUseInertialessDrive) return null
val movement = shipInstance.movement
if (movement !is FelinaeShipMovement) return null
override fun finishOnServer(gameState: GameState, playerSide: GlobalSide, data: PlayerAbilityData): GameEvent {
if (data !is PlayerAbilityData.UseInertialessDrive) return GameEvent.InvalidAction("Internal error from using player ability")
+ if (!gameState.canShipMove(ship)) return GameEvent.InvalidAction("You do not have the initiative")
val shipInstance = gameState.ships[ship] ?: return GameEvent.InvalidAction("That ship does not exist")
if (shipInstance.isDoneCurrentPhase) return GameEvent.InvalidAction("Ships cannot be moved twice")
ships = newShips,
destroyedShips = newWrecks,
chatBox = newChatEntries,
- )
+ ).withRecalculatedInitiative { calculateMovePhaseInitiative() }
)
}
}
data class ChargeLance(override val ship: Id<ShipInstance>, override val weapon: Id<ShipWeapon>) : PlayerAbilityType(), CombatAbility {
override suspend fun beginOnClient(gameState: GameState, playerSide: GlobalSide, pick: suspend (PickRequest) -> PickResponse?): PlayerAbilityData? {
if (gameState.phase !is GamePhase.Attack) return null
+
val shipInstance = gameState.ships[ship] ?: return null
if (shipInstance.weaponAmount <= 0) return null
if (weapon in shipInstance.usedArmaments) return null
+
val shipWeapon = shipInstance.armaments.weaponInstances[weapon] ?: return null
if (shipWeapon !is ShipWeaponInstance.Lance) return null
override fun finishOnServer(gameState: GameState, playerSide: GlobalSide, data: PlayerAbilityData): GameEvent {
if (gameState.phase !is GamePhase.Attack) return GameEvent.InvalidAction("Ships can only charge lances during Phase III")
+
val shipInstance = gameState.ships[ship] ?: return GameEvent.InvalidAction("That ship does not exist")
if (shipInstance.weaponAmount <= 0) return GameEvent.InvalidAction("Not enough power to charge lances")
if (weapon in shipInstance.usedArmaments) return GameEvent.InvalidAction("Cannot charge used lances")
+
val shipWeapon = shipInstance.armaments.weaponInstances[weapon] ?: return GameEvent.InvalidAction("That weapon does not exist")
if (shipWeapon !is ShipWeaponInstance.Lance) return GameEvent.InvalidAction("Cannot charge non-lance weapons")
data class UseWeapon(override val ship: Id<ShipInstance>, override val weapon: Id<ShipWeapon>) : PlayerAbilityType(), CombatAbility {
override suspend fun beginOnClient(gameState: GameState, playerSide: GlobalSide, pick: suspend (PickRequest) -> PickResponse?): PlayerAbilityData? {
if (gameState.phase !is GamePhase.Attack) return null
+
val shipInstance = gameState.ships[ship] ?: return null
if (!shipInstance.canUseWeapon(weapon)) return null
+
val shipWeapon = shipInstance.armaments.weaponInstances[weapon] ?: return null
val pickResponse = pick(shipInstance.getWeaponPickRequest(shipWeapon.weapon))
if (data !is PlayerAbilityData.UseWeapon) return GameEvent.InvalidAction("Internal error from using player ability")
if (gameState.phase !is GamePhase.Attack) return GameEvent.InvalidAction("Ships can only attack during Phase III")
+
val shipInstance = gameState.ships[ship] ?: return GameEvent.InvalidAction("That attacking ship does not exist")
if (!shipInstance.canUseWeapon(weapon)) return GameEvent.InvalidAction("That weapon cannot be used")
+
val shipWeapon = shipInstance.armaments.weaponInstances[weapon] ?: return GameEvent.InvalidAction("That weapon does not exist")
val pickRequest = shipInstance.getWeaponPickRequest(shipWeapon.weapon)
data class RecallStrikeCraft(override val ship: Id<ShipInstance>, override val weapon: Id<ShipWeapon>) : PlayerAbilityType(), CombatAbility {
override suspend fun beginOnClient(gameState: GameState, playerSide: GlobalSide, pick: suspend (PickRequest) -> PickResponse?): PlayerAbilityData? {
if (gameState.phase !is GamePhase.Attack) return null
+
val shipInstance = gameState.ships[ship] ?: return null
if (weapon !in shipInstance.usedArmaments) return null
+
val shipWeapon = shipInstance.armaments.weaponInstances[weapon] ?: return null
if (shipWeapon !is ShipWeaponInstance.Hangar) return null
override fun finishOnServer(gameState: GameState, playerSide: GlobalSide, data: PlayerAbilityData): GameEvent {
if (gameState.phase !is GamePhase.Attack) return GameEvent.InvalidAction("Ships can only recall strike craft during Phase III")
+
val shipInstance = gameState.ships[ship] ?: return GameEvent.InvalidAction("That ship does not exist")
if (weapon !in shipInstance.usedArmaments) return GameEvent.InvalidAction("Cannot recall unused strike craft")
+
val shipWeapon = shipInstance.armaments.weaponInstances[weapon] ?: return GameEvent.InvalidAction("That weapon does not exist")
if (shipWeapon !is ShipWeaponInstance.Hangar) return GameEvent.InvalidAction("Cannot recall non-hangar weapons")
data class DisruptionPulse(override val ship: Id<ShipInstance>) : PlayerAbilityType(), ShipAbility {
override suspend fun beginOnClient(gameState: GameState, playerSide: GlobalSide, pick: suspend (PickRequest) -> PickResponse?): PlayerAbilityData? {
if (gameState.phase !is GamePhase.Attack) return null
+
val shipInstance = gameState.ships[ship] ?: return null
if (!shipInstance.canUseDisruptionPulse) return null
if (shipInstance.hasUsedDisruptionPulse) return null
override fun finishOnServer(gameState: GameState, playerSide: GlobalSide, data: PlayerAbilityData): GameEvent {
if (gameState.phase !is GamePhase.Attack) return GameEvent.InvalidAction("Ships can only emit Disruption Pulses during Phase III")
+
val shipInstance = gameState.ships[ship] ?: return GameEvent.InvalidAction("That ship does not exist")
if (!shipInstance.canUseDisruptionPulse) return GameEvent.InvalidAction("Cannot use Disruption Pulse")
if (shipInstance.hasUsedDisruptionPulse) return GameEvent.InvalidAction("Cannot use Disruption Pulse twice")
data class RepairShipModule(override val ship: Id<ShipInstance>, val module: ShipModule) : PlayerAbilityType(), ShipAbility {
override suspend fun beginOnClient(gameState: GameState, playerSide: GlobalSide, pick: suspend (PickRequest) -> PickResponse?): PlayerAbilityData? {
if (gameState.phase !is GamePhase.Repair) return null
+
val shipInstance = gameState.ships[ship] ?: return null
if (shipInstance.durability !is StandardShipDurability) return null
if (shipInstance.remainingRepairTokens <= 0) return null
override fun finishOnServer(gameState: GameState, playerSide: GlobalSide, data: PlayerAbilityData): GameEvent {
if (gameState.phase !is GamePhase.Repair) return GameEvent.InvalidAction("Ships can only repair modules during Phase IV")
+
val shipInstance = gameState.ships[ship] ?: return GameEvent.InvalidAction("That ship does not exist")
if (shipInstance.durability !is StandardShipDurability) return GameEvent.InvalidAction("That ship cannot manually repair subsystems")
if (shipInstance.remainingRepairTokens <= 0) return GameEvent.InvalidAction("That ship has no remaining repair tokens")
data class ExtinguishFire(override val ship: Id<ShipInstance>) : PlayerAbilityType(), ShipAbility {
override suspend fun beginOnClient(gameState: GameState, playerSide: GlobalSide, pick: suspend (PickRequest) -> PickResponse?): PlayerAbilityData? {
if (gameState.phase !is GamePhase.Repair) return null
+
val shipInstance = gameState.ships[ship] ?: return null
if (shipInstance.durability !is StandardShipDurability) return null
if (shipInstance.remainingRepairTokens <= 0) return null
override fun finishOnServer(gameState: GameState, playerSide: GlobalSide, data: PlayerAbilityData): GameEvent {
if (gameState.phase !is GamePhase.Repair) return GameEvent.InvalidAction("Ships can only extinguish fires during Phase IV")
+
val shipInstance = gameState.ships[ship] ?: return GameEvent.InvalidAction("That ship does not exist")
if (shipInstance.durability !is StandardShipDurability) return GameEvent.InvalidAction("That ship cannot manually extinguish fires")
if (shipInstance.remainingRepairTokens <= 0) return GameEvent.InvalidAction("That ship has no remaining repair tokens")
data class Recoalesce(override val ship: Id<ShipInstance>) : PlayerAbilityType(), ShipAbility {
override suspend fun beginOnClient(gameState: GameState, playerSide: GlobalSide, pick: suspend (PickRequest) -> PickResponse?): PlayerAbilityData? {
if (gameState.phase !is GamePhase.Repair) return null
+
val shipInstance = gameState.ships[ship] ?: return null
if (shipInstance.durability !is FelinaeShipDurability) return null
if (!shipInstance.canUseRecoalescence) return null
override fun finishOnServer(gameState: GameState, playerSide: GlobalSide, data: PlayerAbilityData): GameEvent {
if (gameState.phase !is GamePhase.Repair) return GameEvent.InvalidAction("Ships can only extinguish fires during Phase IV")
+
val shipInstance = gameState.ships[ship] ?: return GameEvent.InvalidAction("That ship does not exist")
if (shipInstance.durability !is FelinaeShipDurability) return GameEvent.InvalidAction("That ship cannot recoalesce its hull")
if (!shipInstance.canUseRecoalescence) return GameEvent.InvalidAction("That ship is not in Recoalescence mode")
- val newHullAmount = Random.nextInt(shipInstance.hullAmount, shipInstance.durability.maxHullPoints)
+ val newHullAmountRange = (shipInstance.hullAmount + 1) until shipInstance.durability.maxHullPoints
+ val (newHullAmount, newMaxHullDamage) = if (newHullAmountRange.isEmpty())
+ shipInstance.hullAmount to shipInstance.recoalescenceMaxHullDamage
+ else
+ newHullAmountRange.random() to (shipInstance.recoalescenceMaxHullDamage + 1)
val repairs = shipInstance.modulesStatus.statuses.filterValues {
it == ShipModuleStatus.DAMAGED || it == ShipModuleStatus.DESTROYED
var newModules = shipInstance.modulesStatus
for (repair in repairs) {
if (Random.nextBoolean())
- newModules = newModules.repair(repair)
+ newModules = newModules.repair(repair, repairUnrepairable = true)
}
val newShip = shipInstance.copy(
hullAmount = newHullAmount,
- recoalescenceMaxHullDamage = shipInstance.recoalescenceMaxHullDamage + 1,
+ recoalescenceMaxHullDamage = newMaxHullDamage,
modulesStatus = newModules,
isDoneCurrentPhase = true
)
object Recoalesce : PlayerAbilityData()
}
-fun GameState.getPossibleAbilities(forPlayer: GlobalSide): List<PlayerAbilityType> = if (ready == forPlayer)
+fun GameState.getPossibleAbilities(forPlayer: GlobalSide): List<PlayerAbilityType> = if (doneWithPhase == forPlayer)
emptyList()
else when (phase) {
GamePhase.Deploy -> {
}
is GamePhase.Move -> {
val movableShips = ships
+ .filterKeys { canShipMove(it) }
.filterValues { it.owner == forPlayer && !it.isDoneCurrentPhase }
.keys
.map { PlayerAbilityType.MoveShip(it) }
val inertialessShips = ships
+ .filterKeys { canShipMove(it) }
.filterValues { it.owner == forPlayer && !it.isDoneCurrentPhase && it.canUseInertialessDrive }
.keys
.map { PlayerAbilityType.UseInertialessDrive(it) }
}
val usableWeapons = ships
+ .filterKeys { canShipAttack(it) }
.filterValues { it.owner == forPlayer }
.flatMap { (id, ship) ->
ship.armaments.weaponInstances.keys.mapNotNull { weaponId ->
}
val usableDisruptionPulses = ships
+ .filterKeys { canShipAttack(it) }
.filterValues { it.owner == forPlayer && !it.isDoneCurrentPhase && it.canUseDisruptionPulse }
.keys
.map { PlayerAbilityType.DisruptionPulse(it) }
PlayerAbilityType.ExtinguishFire(it)
}
- val recoalescence = ships
+ val recoalescibleShips = ships
.filterValues { it.owner == forPlayer && it.canUseRecoalescence }
.keys
.map {
listOf(PlayerAbilityType.DonePhase(GamePhase.Repair(phase.turn)))
else emptyList()
- repairableModules + extinguishableFires + recoalescence + finishRepairing
+ repairableModules + extinguishableFires + recoalescibleShips + finishRepairing
}
}
--- /dev/null
+package starshipfights.game
+
+import kotlinx.serialization.Serializable
+import starshipfights.data.Id
+
+@Serializable
+data class InitiativePair(
+ val hostSide: Double,
+ val guestSide: Double
+) {
+ constructor(map: Map<GlobalSide, Double>) : this(
+ map[GlobalSide.HOST] ?: 0.0,
+ map[GlobalSide.GUEST] ?: 0.0,
+ )
+
+ operator fun get(side: GlobalSide) = when (side) {
+ GlobalSide.HOST -> hostSide
+ GlobalSide.GUEST -> guestSide
+ }
+
+ fun copy(map: Map<GlobalSide, Double>) = copy(
+ hostSide = map[GlobalSide.HOST] ?: hostSide,
+ guestSide = map[GlobalSide.GUEST] ?: guestSide,
+ )
+}
+
+fun GameState.calculateMovePhaseInitiative(): InitiativePair = InitiativePair(
+ ships
+ .values
+ .groupBy { it.owner }
+ .mapValues { (_, shipList) ->
+ shipList
+ .filter { !it.isDoneCurrentPhase }
+ .sumOf { it.ship.pointCost * it.movementCoefficient }
+ }
+)
+
+fun GameState.calculateAttackPhaseInitiative(): InitiativePair = InitiativePair(
+ ships
+ .values
+ .groupBy { it.owner }
+ .mapValues { (_, shipList) ->
+ shipList
+ .filter { !it.isDoneCurrentPhase }
+ .sumOf { ship ->
+ val allWeapons = ship.armaments.weaponInstances
+ .filterValues { weaponInstance ->
+ ships.values.any { target ->
+ target.position.location in ship.getWeaponPickRequest(weaponInstance.weapon).boundary
+ }
+ }
+ val usableWeapons = allWeapons - ship.usedArmaments
+
+ val allWeaponShots = allWeapons.values.sumOf { it.weapon.numShots }
+ val usableWeaponShots = usableWeapons.values.sumOf { it.weapon.numShots }
+
+ ship.ship.pointCost * (usableWeaponShots.toDouble() / allWeaponShots)
+ }
+ }
+)
+
+fun GameState.withRecalculatedInitiative(initiativePairAccessor: GameState.() -> InitiativePair): GameState {
+ val initiativePair = initiativePairAccessor()
+
+ return copy(
+ calculatedInitiative = when {
+ initiativePair.hostSide > initiativePair.guestSide -> GlobalSide.HOST
+ initiativePair.hostSide < initiativePair.guestSide -> GlobalSide.GUEST
+ else -> calculatedInitiative?.other
+ }
+ )
+}
+
+fun GameState.canShipMove(ship: Id<ShipInstance>): Boolean {
+ val shipInstance = ships[ship] ?: return false
+ return currentInitiative != shipInstance.owner.other
+}
+
+fun GameState.canShipAttack(ship: Id<ShipInstance>): Boolean {
+ val shipInstance = ships[ship] ?: return false
+ return currentInitiative != shipInstance.owner.other
+}
val battleInfo: BattleInfo,
val phase: GamePhase = GamePhase.Deploy,
- val ready: GlobalSide? = null,
+ val doneWithPhase: GlobalSide? = null,
+ val calculatedInitiative: GlobalSide? = null,
val ships: Map<Id<ShipInstance>, ShipInstance> = emptyMap(),
val destroyedShips: Map<Id<ShipInstance>, ShipWreck> = emptyMap(),
fun getShipOwner(id: Id<ShipInstance>) = destroyedShips[id]?.owner ?: ships.getValue(id).owner
}
+val GameState.currentInitiative: GlobalSide?
+ get() = calculatedInitiative?.takeIf { it != doneWithPhase }
+
fun GameState.canFinishPhase(side: GlobalSide): Boolean {
return when (phase) {
GamePhase.Deploy -> {
var newShips = ships
val newWrecks = destroyedShips.toMutableMap()
val newChatEntries = mutableListOf<ChatEntry>()
+ var newInitiative: GameState.() -> InitiativePair = { InitiativePair(emptyMap()) }
when (phase) {
+ is GamePhase.Power -> {
+ // Prepare for move phase
+ newInitiative = { calculateMovePhaseInitiative() }
+ }
is GamePhase.Move -> {
// Set velocity to 0 for halted ships
newShips = newShips.mapValues { (_, ship) ->
ship.copy(usedInertialessDriveShots = ship.usedInertialessDriveShots - 1)
else ship
}
+
+ // Prepare for attack phase
+ newInitiative = { calculateAttackPhaseInitiative() }
}
is GamePhase.Attack -> {
val strikeWingDamage = mutableMapOf<ShipHangarWing, Double>()
}
}
- return copy(phase = phase.next(), ships = newShips.mapValues { (_, ship) -> ship.copy(isDoneCurrentPhase = false) }, destroyedShips = newWrecks, chatBox = chatBox + newChatEntries)
+ return copy(
+ phase = phase.next(),
+ ships = newShips.mapValues { (_, ship) ->
+ ship.copy(isDoneCurrentPhase = false)
+ },
+ destroyedShips = newWrecks,
+ chatBox = chatBox + newChatEntries
+ ).withRecalculatedInitiative(newInitiative)
}
-fun GameState.afterPlayerReady(playerSide: GlobalSide) = if (ready == playerSide.other) {
- afterPhase().copy(ready = null)
+fun GameState.afterPlayerReady(playerSide: GlobalSide) = if (doneWithPhase == playerSide.other) {
+ afterPhase().copy(doneWithPhase = null)
} else
- copy(ready = playerSide)
+ copy(doneWithPhase = playerSide)
private fun GameState.victoryMessage(winner: GlobalSide): String {
val winnerName = admiralInfo(winner).fullName