diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt index 99f9579113..ef46c576c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt @@ -319,6 +319,7 @@ fun ChessGameScreen( }, onResign = { chessViewModel.resign(gameId) }, isSpectatorOverride = isSpectating, + onGameEndDismiss = { chessViewModel.dismissGame(gameId) }, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelNew.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelNew.kt index a5ab59bba6..e6b569e8cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelNew.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelNew.kt @@ -166,6 +166,8 @@ class ChessViewModelNew( fun claimAbandonmentVictory(gameId: String) = logic.claimAbandonmentVictory(gameId) + fun dismissGame(gameId: String) = logic.dismissGame(gameId) + // ============================================ // Spectator operations // ============================================ diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessLobbyLogic.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessLobbyLogic.kt index 87e7bd9ed1..6c5df00c53 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessLobbyLogic.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessLobbyLogic.kt @@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst.commons.chess import com.vitorpamplona.quartz.nip64Chess.ChessGameEnd import com.vitorpamplona.quartz.nip64Chess.ChessMoveEvent import com.vitorpamplona.quartz.nip64Chess.Color +import com.vitorpamplona.quartz.nip64Chess.GameResult +import com.vitorpamplona.quartz.nip64Chess.GameStatus import com.vitorpamplona.quartz.nip64Chess.PieceType import com.vitorpamplona.quartz.nip64Chess.jester.JesterEvent import com.vitorpamplona.quartz.nip64Chess.jester.JesterGameEvents @@ -236,9 +238,9 @@ class ChessLobbyLogic( if (result != null) { val gameResult = when (result) { - "1-0" -> com.vitorpamplona.quartz.nip64Chess.GameResult.WHITE_WINS - "0-1" -> com.vitorpamplona.quartz.nip64Chess.GameResult.BLACK_WINS - "1/2-1/2" -> com.vitorpamplona.quartz.nip64Chess.GameResult.DRAW + "1-0" -> GameResult.WHITE_WINS + "0-1" -> GameResult.BLACK_WINS + "1/2-1/2" -> GameResult.DRAW else -> null } if (gameResult != null) { @@ -583,7 +585,14 @@ class ChessLobbyLogic( when (result) { is LoadGameResult.Success -> { - state.replaceGameState(startEventId, result.liveState) + // Check status FIRST to avoid briefly emitting a finished game through activeGames + val gameStatus = result.liveState.gameStatus.value + if (gameStatus is GameStatus.Finished) { + state.moveToCompleted(startEventId, gameStatus.result.notation, null) + pollingDelegate.removeGameId(startEventId) + } else { + state.replaceGameState(startEventId, result.liveState) + } } is LoadGameResult.Error -> { @@ -747,13 +756,26 @@ class ChessLobbyLogic( val newGameIds = discoveredGameIds - currentActiveIds - currentSpectatingIds + // Also check completed games to avoid re-adding them + val completedGameIds = + state.completedGames.value + .map { it.gameId } + .toSet() + for (startEventId in newGameIds) { + if (startEventId in completedGameIds) continue + val events = fetcher.fetchGameEvents(startEventId) val result = ChessGameLoader.loadGame(events, userPubkey) when (result) { is LoadGameResult.Success -> { - if (!result.liveState.isSpectator) { + // If the discovered game is already finished, send it straight to completed + val gameStatus = result.liveState.gameStatus.value + if (gameStatus is GameStatus.Finished) { + // Use the direct helper to avoid the addActiveGame → moveToCompleted flicker + state.addCompletedGameDirectly(startEventId, result.liveState, gameStatus.result.notation, null) + } else if (!result.liveState.isSpectator) { state.addActiveGame(startEventId, result.liveState) pollingDelegate.addGameId(startEventId) } else { @@ -815,6 +837,20 @@ class ChessLobbyLogic( return null } + /** + * Dismiss a finished game from the active/spectating list and move it to completed. + * Called when the user clicks "Continue" on the game end overlay, or automatically + * when a finished game is detected during polling refresh. + */ + fun dismissGame(gameId: String) { + val gameState = state.getGameState(gameId) ?: return + val status = gameState.gameStatus.value + if (status is GameStatus.Finished) { + state.moveToCompleted(gameId, status.result.notation, null) + pollingDelegate.removeGameId(gameId) + } + } + fun clearError() { state.setError(null) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessLobbyState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessLobbyState.kt index 5ebcd39f09..1f5fbaf306 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessLobbyState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessLobbyState.kt @@ -453,6 +453,40 @@ class ChessLobbyState( } } + /** + * Build and record a CompletedGame directly from a LiveChessGameState without requiring the + * game to be inserted into activeGames first. Use this when a newly discovered game is already + * finished so we avoid the intermediate addActiveGame → moveToCompleted flicker. + */ + fun addCompletedGameDirectly( + gameId: String, + liveState: LiveChessGameState, + result: String, + termination: String?, + ) { + val whitePubkey = + if (liveState.playerColor == Color.WHITE) liveState.playerPubkey else liveState.opponentPubkey + val blackPubkey = + if (liveState.playerColor == Color.BLACK) liveState.playerPubkey else liveState.opponentPubkey + + val completed = + CompletedGame( + gameId = gameId, + whitePubkey = whitePubkey, + whiteDisplayName = null, + blackPubkey = blackPubkey, + blackDisplayName = null, + result = result, + termination = termination, + moveCount = liveState.moveHistory.value.size, + completedAt = TimeUtils.now(), + ) + + _completedGames.update { current -> + if (current.any { it.gameId == gameId }) current else listOf(completed) + current + } + } + fun addSpectatingGame( gameId: String, state: LiveChessGameState, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/DesktopChessViewModelNew.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/DesktopChessViewModelNew.kt index b329a8c273..fb23322e7a 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/DesktopChessViewModelNew.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/DesktopChessViewModelNew.kt @@ -162,6 +162,8 @@ class DesktopChessViewModelNew( fun claimAbandonmentVictory(gameId: String) = logic.claimAbandonmentVictory(gameId) + fun dismissGame(gameId: String) = logic.dismissGame(gameId) + // ============================================ // Spectator operations // ============================================