Fix battle records
authorTheSaminator <thesaminator@users.noreply.github.com>
Wed, 13 Jul 2022 16:00:07 +0000 (12:00 -0400)
committerTheSaminator <thesaminator@users.noreply.github.com>
Wed, 13 Jul 2022 16:00:07 +0000 (12:00 -0400)
src/jvmMain/kotlin/net/starshipfights/data/admiralty/battle_records.kt
src/jvmMain/kotlin/net/starshipfights/game/server_game.kt
src/jvmMain/kotlin/net/starshipfights/info/views_gdpr.kt
src/jvmMain/kotlin/net/starshipfights/info/views_user.kt

index fa9d73912623494e7a42c39befe4b7f789af66ca..aa06b52d8fa3c5a52254363c9c9414be4d75f4d9 100644 (file)
@@ -9,7 +9,9 @@ import net.starshipfights.data.Id
 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
@@ -22,47 +24,30 @@ data class BattleRecord(
        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,
+)
index 7141cc9f29b77c6f9675789570ec9cc5e1186846..1f4013d2a654addd7ec077d5ce44bcedf809810a 100644 (file)
@@ -42,7 +42,7 @@ object GameManager {
                        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())
@@ -99,7 +99,7 @@ object GameManager {
                        
                        aiJob.cancel()
                        
-                       on2v1GameEnd(session.state.value, end, startedAt, endedAt)
+                       onGameEnd(session.state.value, end, startedAt, endedAt)
                        
                        unlockAdmiral(hostInfo.id.reinterpret())
                        unlockAdmiral(guestInfo.id.reinterpret())
@@ -321,18 +321,16 @@ private val BattleSize.shipPointsPerAcumen: Int
 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,
@@ -340,17 +338,24 @@ private suspend fun on1v1GameEnd(gameState: GameState, gameEnd: GameEvent.GameEn
                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 }
@@ -361,10 +366,7 @@ private suspend fun on1v1GameEnd(gameState: GameState, gameEnd: GameEvent.GameEn
                        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
                )
        }
@@ -399,16 +401,16 @@ private suspend fun on1v1GameEnd(gameState: GameState, gameEnd: GameEvent.GameEn
                }
                
                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),
                                )
@@ -420,99 +422,3 @@ private suspend fun on1v1GameEnd(gameState: GameState, gameEnd: GameEvent.GameEn
                }
        }
 }
-
-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)
-               }
-       }
-}
index d1be1e0dc6c161445548013c1ce65f6b33e3df9f..b587a2d2414a1aec5d0b47d17883f6b44f18f45e 100644 (file)
@@ -7,15 +7,12 @@ import kotlinx.coroutines.flow.toList
 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 {
@@ -30,10 +27,7 @@ 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()
                }
                
@@ -42,11 +36,13 @@ suspend fun ApplicationCall.privateInfo(): String {
        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()
@@ -54,24 +50,24 @@ suspend fun ApplicationCall.privateInfo(): String {
                                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 {
@@ -105,24 +101,6 @@ suspend fun ApplicationCall.privateInfo(): String {
                        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) {
@@ -150,6 +128,34 @@ suspend fun ApplicationCall.privateInfo(): String {
                                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")
index 7482da2df9163156f05754a3ae2a81ee98402c28..0b366b4ad9ff6cb2e482eba2591245212076124a 100644 (file)
@@ -16,10 +16,13 @@ import net.starshipfights.forbid
 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"]!!)
@@ -392,31 +395,20 @@ suspend fun ApplicationCall.admiralPage(): HTML.() -> Unit {
        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(
@@ -501,18 +493,6 @@ suspend fun ApplicationCall.admiralPage(): HTML.() -> Unit {
                                                        }
                                                }
                                                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"
@@ -558,27 +538,25 @@ suspend fun ApplicationCall.admiralPage(): HTML.() -> Unit {
                                                        +")"
                                                }
                                                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")
                                                }
                                        }
                                }