import net.starshipfights.data.auth.User
import net.starshipfights.data.invoke
import net.starshipfights.game.BattleInfo
+import net.starshipfights.game.GlobalShipController
import net.starshipfights.game.GlobalSide
+import org.litote.kmongo.div
import java.time.Instant
@Serializable
val whenStarted: @Contextual Instant,
val whenEnded: @Contextual Instant,
- val hostUser: Id<User>,
- val guestUser: Id<User>,
-
- val hostAdmiral: Id<Admiral>,
- val guestAdmiral: Id<Admiral>,
-
- val hostEndingMessage: String,
- val guestEndingMessage: String,
+ val participants: List<BattleParticipant>,
val winner: GlobalSide?,
val winMessage: String,
- val was2v1: Boolean = false,
) : DataDocument<BattleRecord> {
- fun getSide(admiral: Id<Admiral>) = when (admiral) {
- hostAdmiral -> GlobalSide.HOST
- guestAdmiral -> GlobalSide.GUEST
- else -> null
- }
-
- fun getUserSide(user: Id<User>) = when (user) {
- hostUser -> GlobalSide.HOST
- guestUser -> GlobalSide.GUEST
- else -> null
- }
+ fun getSide(admiral: Id<Admiral>) = participants.singleOrNull { it.admiral == admiral }?.side?.side
fun wasWinner(side: GlobalSide) = if (winner == null)
null
- else if (was2v1)
- winner == GlobalSide.HOST
else
winner == side
fun didAdmiralWin(admiral: Id<Admiral>) = getSide(admiral)?.let { wasWinner(it) }
- fun didUserWin(user: Id<User>) = getUserSide(user)?.let { wasWinner(it) }
-
companion object Table : DocumentTable<BattleRecord> by DocumentTable.create({
- index(BattleRecord::hostUser)
- index(BattleRecord::guestUser)
-
- index(BattleRecord::hostAdmiral)
- index(BattleRecord::guestAdmiral)
+ index(BattleRecord::participants / BattleParticipant::user)
+ index(BattleRecord::participants / BattleParticipant::admiral)
})
}
+
+@Serializable
+data class BattleParticipant(
+ val user: Id<User>,
+ val admiral: Id<Admiral>,
+ val side: GlobalShipController,
+ val endMessage: String,
+)
val end = session.gameEnd.await()
val endedAt = Instant.now()
- on1v1GameEnd(session.state.value, end, startedAt, endedAt)
+ onGameEnd(session.state.value, end, startedAt, endedAt)
unlockAdmiral(hostInfo.id.reinterpret())
unlockAdmiral(guestInfo.id.reinterpret())
aiJob.cancel()
- on2v1GameEnd(session.state.value, end, startedAt, endedAt)
+ onGameEnd(session.state.value, end, startedAt, endedAt)
unlockAdmiral(hostInfo.id.reinterpret())
unlockAdmiral(guestInfo.id.reinterpret())
private val BattleSize.acumenPerSubplotWon: Int
get() = numPoints / 100
-private suspend fun on1v1GameEnd(gameState: GameState, gameEnd: GameEvent.GameEnd, startedAt: Instant, endedAt: Instant) {
+private suspend fun onGameEnd(gameState: GameState, gameEnd: GameEvent.GameEnd, startedAt: Instant, endedAt: Instant) {
val damagedShipReadyAt = endedAt.plus(6, ChronoUnit.HOURS)
val escapedShipReadyAt = endedAt.plus(4, ChronoUnit.HOURS)
val shipWrecks = gameState.destroyedShips
val ships = gameState.ships
- val hostInfo = gameState.hostInfo.values.single()
- val guestInfo = gameState.guestInfo.values.single()
-
- val hostAdmiralId = hostInfo.id.reinterpret<Admiral>()
- val guestAdmiralId = guestInfo.id.reinterpret<Admiral>()
+ val playerSides = gameState.allShipControllers.associateBy { controller ->
+ gameState.admiralInfo(controller).id.reinterpret<Admiral>()
+ }
val battleRecord = BattleRecord(
battleInfo = gameState.battleInfo,
whenStarted = startedAt,
whenEnded = endedAt,
- hostUser = hostInfo.user.id.reinterpret(),
- guestUser = guestInfo.user.id.reinterpret(),
-
- hostAdmiral = hostAdmiralId,
- guestAdmiral = guestAdmiralId,
-
- hostEndingMessage = victoryTitle(GlobalShipController(GlobalSide.HOST, GlobalShipController.Player1Disambiguation), gameEnd.winner, gameEnd.subplotOutcomes),
- guestEndingMessage = victoryTitle(GlobalShipController(GlobalSide.GUEST, GlobalShipController.Player1Disambiguation), gameEnd.winner, gameEnd.subplotOutcomes),
+ participants = gameState.hostInfo.map { (disambiguation, admiral) ->
+ BattleParticipant(
+ user = admiral.user.id.reinterpret(),
+ admiral = admiral.id.reinterpret(),
+ side = GlobalShipController(GlobalSide.HOST, disambiguation),
+ victoryTitle(GlobalShipController(GlobalSide.HOST, disambiguation), gameEnd.winner, gameEnd.subplotOutcomes)
+ )
+ } + gameState.guestInfo.map { (disambiguation, admiral) ->
+ BattleParticipant(
+ user = admiral.user.id.reinterpret(),
+ admiral = admiral.id.reinterpret(),
+ side = GlobalShipController(GlobalSide.GUEST, disambiguation),
+ victoryTitle(GlobalShipController(GlobalSide.GUEST, disambiguation), gameEnd.winner, gameEnd.subplotOutcomes)
+ )
+ },
winner = gameEnd.winner,
- winMessage = gameEnd.message
+ winMessage = gameEnd.message,
)
val destructions = shipWrecks.filterValues { !it.isEscape }
name = wreck.ship.name,
shipType = wreck.ship.shipType,
destroyedAt = wreck.wreckedAt.instant,
- owningAdmiral = when (wreck.owner.side) {
- GlobalSide.HOST -> hostAdmiralId
- GlobalSide.GUEST -> guestAdmiralId
- },
+ owningAdmiral = gameState.admiralInfo(wreck.owner).id.reinterpret(),
destroyedIn = battleRecord.id
)
}
}
launch {
- Admiral.set(
- hostAdmiralId, combine(
+ Admiral.update(
+ Admiral::id `in` playerSides.filterValues { it.side == GlobalSide.HOST }.keys, combine(
inc(Admiral::acumen, hostAcumenGain),
inc(Admiral::money, hostPayment),
)
)
}
launch {
- Admiral.set(
- guestAdmiralId, combine(
+ Admiral.update(
+ Admiral::id `in` playerSides.filterValues { it.side == GlobalSide.GUEST }.keys, combine(
inc(Admiral::acumen, guestAcumenGain),
inc(Admiral::money, guestPayment),
)
}
}
}
-
-private suspend fun on2v1GameEnd(gameState: GameState, gameEnd: GameEvent.GameEnd, startedAt: Instant, endedAt: Instant) {
- val damagedShipReadyAt = endedAt.plus(6, ChronoUnit.HOURS)
- val escapedShipReadyAt = endedAt.plus(4, ChronoUnit.HOURS)
-
- val shipWrecks = gameState.destroyedShips
- val ships = gameState.ships
-
- val hostInfo = gameState.hostInfo.getValue(GlobalShipController.Player1Disambiguation)
- val guestInfo = gameState.hostInfo.getValue(GlobalShipController.Player2Disambiguation)
-
- val hostAdmiralId = hostInfo.id.reinterpret<Admiral>()
- val guestAdmiralId = guestInfo.id.reinterpret<Admiral>()
-
- val battleRecord = BattleRecord(
- battleInfo = gameState.battleInfo,
-
- whenStarted = startedAt,
- whenEnded = endedAt,
-
- hostUser = hostInfo.user.id.reinterpret(),
- guestUser = guestInfo.user.id.reinterpret(),
-
- hostAdmiral = hostAdmiralId,
- guestAdmiral = guestAdmiralId,
-
- hostEndingMessage = victoryTitle(GlobalShipController(GlobalSide.HOST, GlobalShipController.Player1Disambiguation), gameEnd.winner, gameEnd.subplotOutcomes),
- guestEndingMessage = victoryTitle(GlobalShipController(GlobalSide.HOST, GlobalShipController.Player2Disambiguation), gameEnd.winner, gameEnd.subplotOutcomes),
-
- winner = gameEnd.winner,
- winMessage = gameEnd.message,
- was2v1 = true,
- )
-
- val destructions = shipWrecks.filterValues { !it.isEscape }
- val destroyedShips = destructions.keys.map { it.reinterpret<ShipInDrydock>() }.toSet()
- val rememberedShips = destructions.values.map { wreck ->
- ShipMemorial(
- id = Id("RIP_${wreck.id.id}"),
- name = wreck.ship.name,
- shipType = wreck.ship.shipType,
- destroyedAt = wreck.wreckedAt.instant,
- owningAdmiral = when (wreck.owner.side) {
- GlobalSide.HOST -> hostAdmiralId
- GlobalSide.GUEST -> guestAdmiralId
- },
- destroyedIn = battleRecord.id
- )
- }
-
- val escapedShips = shipWrecks.filterValues { it.isEscape }.keys.map { it.reinterpret<ShipInDrydock>() }.toSet()
- val damagedShips = ships.filterValues { it.hullAmount < it.durability.maxHullPoints || it.troopsAmount < it.durability.troopsDefense }.keys.map { it.reinterpret<ShipInDrydock>() }.toSet()
-
- val battleSize = gameState.battleInfo.size
-
- val playersAcumenGainFromShips = shipWrecks.values.filter { it.owner.side == GlobalSide.GUEST && !it.isEscape }.sumOf { it.ship.pointCost / battleSize.shipPointsPerAcumen }
- val playersAcumenGainFromSubplots = gameEnd.subplotOutcomes.filterKeys { it.player.side == GlobalSide.HOST }.count { (_, outcome) -> outcome == SubplotOutcome.WON } * battleSize.acumenPerSubplotWon
- val playersAcumenGain = playersAcumenGainFromShips + playersAcumenGainFromSubplots
- val playersPayment = playersAcumenGain * 2
-
- coroutineScope {
- launch {
- ShipMemorial.put(rememberedShips)
- }
- launch {
- ShipInDrydock.remove(ShipInDrydock::id `in` destroyedShips)
- }
- launch {
- ShipInDrydock.update(ShipInDrydock::id `in` damagedShips, setValue(ShipInDrydock::readyAt, damagedShipReadyAt))
- }
- launch {
- ShipInDrydock.update(ShipInDrydock::id `in` escapedShips, setValue(ShipInDrydock::readyAt, escapedShipReadyAt))
- }
-
- launch {
- Admiral.set(
- hostAdmiralId, combine(
- inc(Admiral::acumen, playersAcumenGain),
- inc(Admiral::money, playersPayment),
- )
- )
- }
- launch {
- Admiral.set(
- guestAdmiralId, combine(
- inc(Admiral::acumen, playersAcumenGain),
- inc(Admiral::money, playersPayment),
- )
- )
- }
-
- launch {
- BattleRecord.put(battleRecord)
- }
- }
-}
import kotlinx.html.*
import net.starshipfights.auth.getUser
import net.starshipfights.auth.getUserSession
-import net.starshipfights.data.admiralty.Admiral
-import net.starshipfights.data.admiralty.BattleRecord
-import net.starshipfights.data.admiralty.ShipInDrydock
-import net.starshipfights.data.admiralty.ShipMemorial
+import net.starshipfights.data.admiralty.*
import net.starshipfights.data.auth.User
import net.starshipfights.data.auth.UserSession
import net.starshipfights.redirect
+import org.litote.kmongo.div
import org.litote.kmongo.eq
-import org.litote.kmongo.or
import java.time.Instant
suspend fun ApplicationCall.privateInfo(): String {
val getSessions = async { UserSession.filter(UserSession::user eq userId).toList() }
val getBattles = async {
BattleRecord.filter(
- or(
- BattleRecord::hostUser eq userId,
- BattleRecord::guestUser eq userId
- )
+ (BattleRecord::participants / BattleParticipant::user) eq userId
).toList()
}
val (userAdmirals, userSessions, userBattles) = userData
user ?: redirect("/login")
- val battleEndings = userBattles.associate { record ->
- record.id to record.didUserWin(userId)
+ val admiralBattles = userAdmirals.associate { admiral ->
+ admiral.id to userBattles.filter { record ->
+ record.participants.any { it.admiral == admiral.id }
+ }
}
- val (admiralShips, battleOpponents, battleAdmirals) = coroutineScope {
+ val (admiralShips, battleOtherParticipants) = coroutineScope {
val getShips = userAdmirals.associate { admiral ->
admiral.id to (async {
ShipInDrydock.filter(ShipInDrydock::owningAdmiral eq admiral.id).toList()
ShipMemorial.filter(ShipMemorial::owningAdmiral eq admiral.id).toList()
})
}
- val getOpponents = userBattles.associate { record ->
- val (opponentId, opponentAdmiralId) = if (record.hostUser == userId) record.guestUser to record.guestAdmiral else record.hostUser to record.hostAdmiral
-
- record.id to (async { User.get(opponentId) } to async { Admiral.get(opponentAdmiralId) })
- }
- val getAdmirals = userBattles.associate { record ->
- val admiralId = if (record.hostUser == userId) record.hostAdmiral else record.guestAdmiral
- record.id to userAdmirals.singleOrNull { it.id == admiralId }
+ val getOtherParticipants = admiralBattles.mapValues { (admiralId, records) ->
+ records.associate { record ->
+ record.id to record.participants.filter { it.admiral != admiralId }.map { participant ->
+ async { Admiral.get(participant.admiral) }
+ }
+ }
}
- Triple(
- getShips.mapValues { (_, pair) ->
- val (ships, graves) = pair
- ships.await() to graves.await()
- },
- getOpponents.mapValues { (_, deferred) -> deferred.let { (u, a) -> u.await() to a.await() } },
- getAdmirals
- )
+ getShips.mapValues { (_, pair) ->
+ val (ships, graves) = pair
+ ships.await() to graves.await()
+ } to getOtherParticipants.mapValues { (_, records) ->
+ records.mapValues { (_, admirals) ->
+ admirals.mapNotNull { admiralAsync ->
+ admiralAsync.await()
+ }
+ }
+ }
}
return buildString {
appendLine("${if (session.expiration > now) "Will expire" else "Has expired"} at: ${session.expiration}")
}
appendLine("")
- appendLine("## Battle-record data")
- for (record in userBattles.sortedBy { it.whenEnded }) {
- appendLine("")
- appendLine("### Battle record ${record.id}")
- appendLine("Battle size: ${record.battleInfo.size.displayName} (${record.battleInfo.size.numPoints})")
- appendLine("Battle background: ${record.battleInfo.bg.displayName}")
- appendLine("Battle started at: ${record.whenStarted}")
- appendLine("Battle completed at: ${record.whenEnded}")
- appendLine("Battle was fought by ${battleAdmirals[record.id]?.let { "${it.fullName} (https://starshipfights.net/admiral/${it.id})" } ?: "{deleted admiral}"}")
- appendLine("Battle was fought against ${battleOpponents[record.id]?.second?.let { "${it.fullName} (https://starshipfights.net/admiral/${it.id})" } ?: "{deleted admiral}"}")
- appendLine(" => ${battleOpponents[record.id]?.first?.let { "${it.profileName} (https://starshipfights.net/user/${it.id})" } ?: "{deleted user}"}")
- when (battleEndings[record.id]) {
- true -> appendLine("Battle ended in victory")
- false -> appendLine("Battle ended in defeat")
- null -> appendLine("Battle ended in stalemate")
- }
- appendLine(" => \"${record.winMessage}\"")
- }
appendLine("")
appendLine("## Admiral data")
for (admiral in userAdmirals) {
appendLine("Ship is a ${grave.shipType.fullerDisplayName}")
appendLine("Ship was destroyed at ${grave.destroyedAt} in battle recorded at ${grave.destroyedIn}")
}
+
+ val records = admiralBattles[admiral.id].orEmpty()
+ appendLine("Admiral has fought in ${records.size} battles:")
+ for (record in records.sortedBy { it.whenEnded }) {
+ appendLine("")
+ appendLine("##### Battle record ${record.id}")
+ appendLine("Battle size: ${record.battleInfo.size.displayName} (${record.battleInfo.size.numPoints})")
+ appendLine("Battle background: ${record.battleInfo.bg.displayName}")
+ appendLine("Battle started at: ${record.whenStarted}")
+ appendLine("Battle completed at: ${record.whenEnded}")
+
+ val otherParticipants = battleOtherParticipants[admiral.id]?.get(record.id)
+ .orEmpty()
+ .filter { record.getSide(it.id) != null }
+ .sortedBy { if (record.getSide(it.id) == record.getSide(admiral.id)) 0 else 1 }
+
+ for (otherParticipant in otherParticipants) {
+ val preposition = if (record.getSide(otherParticipant.id) == record.getSide(admiral.id))
+ "alongside"
+ else "against"
+
+ appendLine("Battle was fought $preposition ${otherParticipant.fullName} (https://starshipfights.net/admiral/${otherParticipant.id})")
+ }
+
+ val endMessage = record.participants.singleOrNull { it.admiral == admiral.id }?.endMessage ?: "Stalemate"
+ appendLine("Battle ended in a $endMessage")
+ appendLine(" => \"${record.winMessage}\"")
+ }
}
appendLine("")
appendLine("# More information")
import net.starshipfights.game.*
import net.starshipfights.redirect
import org.litote.kmongo.and
+import org.litote.kmongo.div
import org.litote.kmongo.eq
import org.litote.kmongo.gt
-import org.litote.kmongo.or
import java.time.Instant
+import kotlin.collections.component1
+import kotlin.collections.component2
+import kotlin.collections.set
suspend fun ApplicationCall.userPage(): HTML.() -> Unit {
val userId = Id<User>(parameters["id"]!!)
val (ships, graveyard, records) = coroutineScope {
val ships = async { ShipInDrydock.filter(ShipInDrydock::owningAdmiral eq admiralId).toList() }
val graveyard = async { ShipMemorial.filter(ShipMemorial::owningAdmiral eq admiralId).toList() }
- val records = async { BattleRecord.filter(or(BattleRecord::hostAdmiral eq admiralId, BattleRecord::guestAdmiral eq admiralId)).toList() }
+ val records = async { BattleRecord.filter(BattleRecord::participants / BattleParticipant::admiral eq admiralId).toList() }
Triple(ships.await(), graveyard.await(), records.await())
}
- val recordRoles = records.mapNotNull {
- when (admiralId) {
- it.hostAdmiral -> GlobalSide.HOST
- it.guestAdmiral -> GlobalSide.GUEST
- else -> null
- }?.let { role -> it.id to role }
- }.toMap()
-
- val recordOpponents = coroutineScope {
- records.mapNotNull {
- recordRoles[it.id]?.let { role ->
- val aId = when (role) {
- GlobalSide.HOST -> it.guestAdmiral
- GlobalSide.GUEST -> it.hostAdmiral
- }
- it.id to async { Admiral.get(aId) }
- }
- }.mapNotNull { (id, deferred) ->
- deferred.await()?.let { id to it }
- }.toMap()
+ val otherRecordAdmirals = coroutineScope {
+ records.associate { record ->
+ val currAdmiralSide = record.getSide(admiralId)
+ record.id to record.participants.filter { it.admiral != admiralId }.map { participant ->
+ async { Admiral.get(participant.admiral)?.let { admiral -> admiral to (participant.side.side == currAdmiralSide) } }
+ }
+ }.mapValues { (_, admiralsAsync) ->
+ admiralsAsync.mapNotNull { it.await() }
+ }
}
return page(
}
}
td {
- +"Destroyed by "
- val opponent = recordOpponents[ship.destroyedIn]
- if (opponent == null)
- i { +"(Deleted Admiral)" }
- else if (records.singleOrNull { it.id == ship.destroyedIn }?.was2v1 == true)
- i { +"(Non-Player Admiral)" }
- else
- a(href = "/admiral/${opponent.id}") {
- +opponent.fullName
- }
- br
- br
+"Destroyed at "
span(classes = "moment") {
style = "display:none"
+")"
}
td {
- +when (recordRoles[record.id]) {
+ +when (record.getSide(admiralId)) {
GlobalSide.HOST -> "Host"
GlobalSide.GUEST -> "Guest"
else -> "N/A"
}
}
td {
- val opponent = recordOpponents[record.id]
- if (opponent == null)
- i { +"(Deleted Admiral)" }
- else
- a(href = "/admiral/${opponent.id}") {
- +opponent.fullName
+ for ((otherAdmiral, onSameSide) in otherRecordAdmirals[record.id].orEmpty()) {
+ +if (onSameSide)
+ "With "
+ else
+ "Against "
+ a(href = "/admiral/${otherAdmiral.id}") {
+ +otherAdmiral.fullName
}
+ }
}
td {
- +when (recordRoles[record.id]) {
- GlobalSide.HOST -> record.hostEndingMessage
- GlobalSide.GUEST -> record.guestEndingMessage
- else -> "N/A"
- }
+ +(record.participants.singleOrNull { it.admiral == admiralId }?.endMessage ?: "Stalemate")
}
}
}