From 4c2197f2b40df0581f5bb0e4bb501989f2e6d692 Mon Sep 17 00:00:00 2001 From: TheSaminator Date: Wed, 13 Jul 2022 12:00:07 -0400 Subject: [PATCH] Fix battle records --- .../data/admiralty/battle_records.kt | 43 ++--- .../net/starshipfights/game/server_game.kt | 148 ++++-------------- .../net/starshipfights/info/views_gdpr.kt | 98 ++++++------ .../net/starshipfights/info/views_user.kt | 70 +++------ 4 files changed, 117 insertions(+), 242 deletions(-) diff --git a/src/jvmMain/kotlin/net/starshipfights/data/admiralty/battle_records.kt b/src/jvmMain/kotlin/net/starshipfights/data/admiralty/battle_records.kt index fa9d739..aa06b52 100644 --- a/src/jvmMain/kotlin/net/starshipfights/data/admiralty/battle_records.kt +++ b/src/jvmMain/kotlin/net/starshipfights/data/admiralty/battle_records.kt @@ -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, - val guestUser: Id, - - val hostAdmiral: Id, - val guestAdmiral: Id, - - val hostEndingMessage: String, - val guestEndingMessage: String, + val participants: List, val winner: GlobalSide?, val winMessage: String, - val was2v1: Boolean = false, ) : DataDocument { - fun getSide(admiral: Id) = when (admiral) { - hostAdmiral -> GlobalSide.HOST - guestAdmiral -> GlobalSide.GUEST - else -> null - } - - fun getUserSide(user: Id) = when (user) { - hostUser -> GlobalSide.HOST - guestUser -> GlobalSide.GUEST - else -> null - } + fun getSide(admiral: Id) = 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) = getSide(admiral)?.let { wasWinner(it) } - fun didUserWin(user: Id) = getUserSide(user)?.let { wasWinner(it) } - companion object Table : DocumentTable 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, + val admiral: Id, + val side: GlobalShipController, + val endMessage: String, +) diff --git a/src/jvmMain/kotlin/net/starshipfights/game/server_game.kt b/src/jvmMain/kotlin/net/starshipfights/game/server_game.kt index 7141cc9..1f4013d 100644 --- a/src/jvmMain/kotlin/net/starshipfights/game/server_game.kt +++ b/src/jvmMain/kotlin/net/starshipfights/game/server_game.kt @@ -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() - val guestAdmiralId = guestInfo.id.reinterpret() + val playerSides = gameState.allShipControllers.associateBy { controller -> + gameState.admiralInfo(controller).id.reinterpret() + } 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() - val guestAdmiralId = guestInfo.id.reinterpret() - - 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() }.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() }.toSet() - val damagedShips = ships.filterValues { it.hullAmount < it.durability.maxHullPoints || it.troopsAmount < it.durability.troopsDefense }.keys.map { it.reinterpret() }.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) - } - } -} diff --git a/src/jvmMain/kotlin/net/starshipfights/info/views_gdpr.kt b/src/jvmMain/kotlin/net/starshipfights/info/views_gdpr.kt index d1be1e0..b587a2d 100644 --- a/src/jvmMain/kotlin/net/starshipfights/info/views_gdpr.kt +++ b/src/jvmMain/kotlin/net/starshipfights/info/views_gdpr.kt @@ -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") diff --git a/src/jvmMain/kotlin/net/starshipfights/info/views_user.kt b/src/jvmMain/kotlin/net/starshipfights/info/views_user.kt index 7482da2..0b366b4 100644 --- a/src/jvmMain/kotlin/net/starshipfights/info/views_user.kt +++ b/src/jvmMain/kotlin/net/starshipfights/info/views_user.kt @@ -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(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") } } } -- 2.25.1