From c7c1c44e570ce2eaac2435093a1735d9ea0214e7 Mon Sep 17 00:00:00 2001 From: net <96362337+netqo@users.noreply.github.com> Date: Wed, 24 Jun 2026 08:36:21 -0300 Subject: [PATCH] feat(lobby): mark the real last-played game on the lobby Add a LobbyViewModel that reads the most recent round from the Room game_round ledger and expose its game as lastPlayedGame. The lobby maps it onto the games grid so the actual last-played game carries the LAST PLAYED badge (the card design already existed), replacing the static preview default; no rounds yet means no card is marked. Covered by LobbyViewModelTest. --- CHANGELOG.md | 6 ++ .../feature/lobby/LobbyViewModel.kt | 35 ++++++++++ .../stackcasino/navigation/StackNavHost.kt | 4 ++ .../feature/lobby/LobbyViewModelTest.kt | 64 +++++++++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 app/src/main/java/com/plainstudio/stackcasino/feature/lobby/LobbyViewModel.kt create mode 100644 app/src/test/java/com/plainstudio/stackcasino/feature/lobby/LobbyViewModelTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e409b1..e716a3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Real "Last Played" highlight on the lobby (cu-03): the new + `LobbyViewModel` reads the most recent round from the Room `game_round` + ledger and the lobby marks that game's card with the existing LAST + PLAYED badge, instead of the static preview default. No game played yet + means no card is marked. Covered by `LobbyViewModelTest`. + - Playable Blackjack (cu-06): the fifth and final game, completing the games card. `BlackjackViewModel` -> `GameRepository.startBlackjack` draws the shuffled 52-card deck on the C++/JNI engine (committed via the diff --git a/app/src/main/java/com/plainstudio/stackcasino/feature/lobby/LobbyViewModel.kt b/app/src/main/java/com/plainstudio/stackcasino/feature/lobby/LobbyViewModel.kt new file mode 100644 index 0000000..cb8f1ba --- /dev/null +++ b/app/src/main/java/com/plainstudio/stackcasino/feature/lobby/LobbyViewModel.kt @@ -0,0 +1,35 @@ +package com.plainstudio.stackcasino.feature.lobby + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.plainstudio.stackcasino.domain.game.GameRepository +import com.plainstudio.stackcasino.model.GameKey +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +/** + * Backs the lobby with the bits of real round data it needs today: the + * last game the user played, so its card can carry the "LAST PLAYED" + * highlight. Reads the Room-backed [GameRepository.rounds] (newest first), + * so the most recent round's game is the last played, or null when no + * round has been played yet. + */ +@HiltViewModel +class LobbyViewModel + @Inject + constructor( + gameRepository: GameRepository, + ) : ViewModel() { + val lastPlayedGame: StateFlow = + gameRepository.rounds + .map { rounds -> rounds.firstOrNull()?.game } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(STOP_TIMEOUT_MILLIS), null) + + private companion object { + const val STOP_TIMEOUT_MILLIS = 5_000L + } + } diff --git a/app/src/main/java/com/plainstudio/stackcasino/navigation/StackNavHost.kt b/app/src/main/java/com/plainstudio/stackcasino/navigation/StackNavHost.kt index 50269a0..d84657c 100644 --- a/app/src/main/java/com/plainstudio/stackcasino/navigation/StackNavHost.kt +++ b/app/src/main/java/com/plainstudio/stackcasino/navigation/StackNavHost.kt @@ -29,6 +29,7 @@ import com.plainstudio.stackcasino.feature.housewallet.HouseWalletScreen import com.plainstudio.stackcasino.feature.kyc.KycScreen import com.plainstudio.stackcasino.feature.lobby.LobbyScreen import com.plainstudio.stackcasino.feature.lobby.LobbyUiState +import com.plainstudio.stackcasino.feature.lobby.LobbyViewModel import com.plainstudio.stackcasino.feature.lobby.previewLobbyData import com.plainstudio.stackcasino.feature.mines.MinesScreen import com.plainstudio.stackcasino.feature.mines.MinesViewModel @@ -262,11 +263,14 @@ private fun NavGraphBuilder.addLobbyRoute(navController: NavHostController) { val unreadCount by notificationsViewModel.unreadCount.collectAsStateWithLifecycle() val walletViewModel: WalletViewModel = hiltViewModel() val wallet by walletViewModel.walletData.collectAsStateWithLifecycle() + val lobbyViewModel: LobbyViewModel = hiltViewModel() + val lastPlayed by lobbyViewModel.lastPlayedGame.collectAsStateWithLifecycle() var showNotifications by rememberSaveable { mutableStateOf(false) } val lobbyData = previewLobbyData().let { it.copy( balance = it.balance.copy(amountLabel = wallet.availableLabel), + games = it.games.map { card -> card.copy(isLastPlayed = card.key == lastPlayed) }, ) } LobbyScreen( diff --git a/app/src/test/java/com/plainstudio/stackcasino/feature/lobby/LobbyViewModelTest.kt b/app/src/test/java/com/plainstudio/stackcasino/feature/lobby/LobbyViewModelTest.kt new file mode 100644 index 0000000..dcf5fb7 --- /dev/null +++ b/app/src/test/java/com/plainstudio/stackcasino/feature/lobby/LobbyViewModelTest.kt @@ -0,0 +1,64 @@ +package com.plainstudio.stackcasino.feature.lobby + +import app.cash.turbine.test +import com.plainstudio.stackcasino.domain.game.GameRepository +import com.plainstudio.stackcasino.domain.game.GameRound +import com.plainstudio.stackcasino.model.GameKey +import com.plainstudio.stackcasino.model.RoundOutcome +import com.plainstudio.stackcasino.testing.MainDispatcherRule +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test + +class LobbyViewModelTest { + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + private val gameRepository = mockk(relaxed = true) + + @Test + fun `last played is the most recent round's game`() = + runTest { + every { gameRepository.rounds } returns flowOf(listOf(round(GameKey.Mines), round(GameKey.Crash))) + + LobbyViewModel(gameRepository).lastPlayedGame.test { + var value = awaitItem() + while (value == null) value = awaitItem() + assertEquals(GameKey.Mines, value) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `last played is null with no rounds`() = + runTest { + every { gameRepository.rounds } returns flowOf(emptyList()) + + LobbyViewModel(gameRepository).lastPlayedGame.test { + assertEquals(null, awaitItem()) + cancelAndIgnoreRemainingEvents() + } + } + + private fun round(game: GameKey) = + GameRound( + id = "round-id", + game = game, + pick = "", + stake = 10.0, + payout = 0.0, + outcome = RoundOutcome.Loss, + multiplier = 0.0, + currency = "USDC", + serverSeed = "s", + clientSeed = "c", + nonce = 1L, + hash = "h", + resultRaw = "", + timestamp = 0L, + ) +}