diff --git a/app/src/main/java/org/prolibertate/games/MainActivity.kt b/app/src/main/java/org/prolibertate/games/MainActivity.kt index 1e6b2ab..fd05752 100644 --- a/app/src/main/java/org/prolibertate/games/MainActivity.kt +++ b/app/src/main/java/org/prolibertate/games/MainActivity.kt @@ -10,6 +10,7 @@ import org.prolibertate.games.net.LobbyController import org.prolibertate.games.net.ScorekeeperSession import org.prolibertate.games.score.ScorekeeperRepository import org.prolibertate.games.settings.SettingsRepository +import org.prolibertate.games.settings.YahtzeeStatsRepository import org.prolibertate.games.ui.AppEnv import org.prolibertate.games.ui.AppRoot import org.prolibertate.games.ui.theme.ProLibertateTheme @@ -43,6 +44,7 @@ class MainActivity : ComponentActivity() { ), peerId = peerId, saves = SavedGameStore(File(filesDir, "saved-games")), + yahtzeeStats = YahtzeeStatsRepository(applicationContext), ) setContent { diff --git a/app/src/main/java/org/prolibertate/games/game/yahtzee/YahtzeeStats.kt b/app/src/main/java/org/prolibertate/games/game/yahtzee/YahtzeeStats.kt new file mode 100644 index 0000000..45d6ddf --- /dev/null +++ b/app/src/main/java/org/prolibertate/games/game/yahtzee/YahtzeeStats.kt @@ -0,0 +1,58 @@ +package org.prolibertate.games.game.yahtzee + +import kotlinx.serialization.Serializable + +/** What one finished game says about the local player's card. */ +@Serializable +data class YahtzeeRecord( + val total: Int, + val gotBonus: Boolean, + /** Yahtzees scored, counting the extra ones that earned the hundred. */ + val yahtzees: Int, + /** Boxes written in as nothing. */ + val zeros: Int, + val players: Int, + val won: Boolean, +) + +/** Builds the record of [seat]'s card in a finished game. Ties for first count as a win. */ +fun recordOf(state: YahtzeeState, seat: Int): YahtzeeRecord { + val card = state.cards[seat] + val mine = state.totalFor(seat) + val best = state.cards.indices.maxOf { state.totalFor(it) } + return YahtzeeRecord( + total = mine, + gotBonus = card.upperBonus > 0, + yahtzees = (if (card[YahtzeeCategory.YAHTZEE] == YAHTZEE_SCORE) 1 else 0) + + state.yahtzeeBonuses[seat] / YAHTZEE_BONUS, + zeros = card.boxes.count { it == 0 }, + players = state.cards.size, + won = mine >= best, + ) +} + +/** A game set against the ones before it. [rank] is 1 for a new best. */ +data class YahtzeeComparison( + val current: YahtzeeRecord, + val previousGames: Int, + val bestBefore: Int?, + val averageBefore: Double?, + val rank: Int, + val gamesRanked: Int, + val bonusRateBefore: Double?, + val yahtzeesBefore: Int, + val winRateBefore: Double?, +) + +fun compareToHistory(previous: List, current: YahtzeeRecord): YahtzeeComparison = + YahtzeeComparison( + current = current, + previousGames = previous.size, + bestBefore = previous.maxOfOrNull { it.total }, + averageBefore = previous.takeIf { it.isNotEmpty() }?.map { it.total }?.average(), + rank = 1 + previous.count { it.total > current.total }, + gamesRanked = previous.size + 1, + bonusRateBefore = previous.takeIf { it.isNotEmpty() }?.let { p -> p.count { it.gotBonus }.toDouble() / p.size }, + yahtzeesBefore = previous.sumOf { it.yahtzees }, + winRateBefore = previous.takeIf { it.isNotEmpty() }?.let { p -> p.count { it.won }.toDouble() / p.size }, + ) diff --git a/app/src/main/java/org/prolibertate/games/settings/SettingsRepository.kt b/app/src/main/java/org/prolibertate/games/settings/SettingsRepository.kt index ccbb2d0..ae2b75f 100644 --- a/app/src/main/java/org/prolibertate/games/settings/SettingsRepository.kt +++ b/app/src/main/java/org/prolibertate/games/settings/SettingsRepository.kt @@ -24,6 +24,8 @@ data class Settings( val soundEnabled: Boolean = true, val animationSpeed: Float = 1.0f, val checkForUpdatesOnLaunch: Boolean = true, + /** Multiplies the system font size across the whole app. */ + val textScale: Float = 1.0f, /** * Exactly what the user has typed, which may be blank while they are * mid-edit. Nothing substitutes a default into this value: doing so meant @@ -46,6 +48,8 @@ data class Settings( companion object { const val MIN_SPEED = 0.5f const val MAX_SPEED = 2.0f + const val MIN_TEXT_SCALE = 0.8f + const val MAX_TEXT_SCALE = 1.6f const val DEFAULT_PLAYER_NAME = "Player" } } @@ -56,6 +60,7 @@ class SettingsRepository(private val context: Context) { val SOUND = booleanPreferencesKey("sound_enabled") val SPEED = floatPreferencesKey("animation_speed") val UPDATE_ON_LAUNCH = booleanPreferencesKey("check_updates_on_launch") + val TEXT_SCALE = floatPreferencesKey("text_scale") val NAME = stringPreferencesKey("player_name") // There is deliberately no update-channel key. The channel is a // property of the installed APK, not a preference — an install that @@ -69,6 +74,8 @@ class SettingsRepository(private val context: Context) { soundEnabled = prefs[Keys.SOUND] ?: true, animationSpeed = prefs[Keys.SPEED] ?: 1.0f, checkForUpdatesOnLaunch = prefs[Keys.UPDATE_ON_LAUNCH] ?: true, + textScale = (prefs[Keys.TEXT_SCALE] ?: 1.0f) + .coerceIn(Settings.MIN_TEXT_SCALE, Settings.MAX_TEXT_SCALE), playerName = prefs[Keys.NAME] ?: "", ) } @@ -83,6 +90,12 @@ class SettingsRepository(private val context: Context) { } } + suspend fun setTextScale(scale: Float) { + context.dataStore.edit { + it[Keys.TEXT_SCALE] = scale.coerceIn(Settings.MIN_TEXT_SCALE, Settings.MAX_TEXT_SCALE) + } + } + suspend fun setCheckForUpdatesOnLaunch(enabled: Boolean) { context.dataStore.edit { it[Keys.UPDATE_ON_LAUNCH] = enabled } } diff --git a/app/src/main/java/org/prolibertate/games/settings/YahtzeeStatsRepository.kt b/app/src/main/java/org/prolibertate/games/settings/YahtzeeStatsRepository.kt new file mode 100644 index 0000000..2d4020d --- /dev/null +++ b/app/src/main/java/org/prolibertate/games/settings/YahtzeeStatsRepository.kt @@ -0,0 +1,42 @@ +package org.prolibertate.games.settings + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.first +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.prolibertate.games.game.yahtzee.YahtzeeComparison +import org.prolibertate.games.game.yahtzee.YahtzeeRecord +import org.prolibertate.games.game.yahtzee.compareToHistory + +private val Context.yahtzeeStatsStore: DataStore by preferencesDataStore( + name = "yahtzee_stats", +) + +/** The local player's finished Yahtzee games, newest last, kept to the last [KEEP]. */ +class YahtzeeStatsRepository(private val context: Context) { + + private val key = stringPreferencesKey("games") + private val json = Json { ignoreUnknownKeys = true } + + private fun decode(stored: String?): List = + stored?.let { runCatching { json.decodeFromString>(it) }.getOrNull() } + .orEmpty() + + /** Compares [record] with every earlier game, then adds it to the history. */ + suspend fun finish(record: YahtzeeRecord): YahtzeeComparison { + val previous = decode(context.yahtzeeStatsStore.data.first()[key]) + context.yahtzeeStatsStore.edit { + it[key] = json.encodeToString((previous + record).takeLast(KEEP)) + } + return compareToHistory(previous, record) + } + + private companion object { + const val KEEP = 500 + } +} diff --git a/app/src/main/java/org/prolibertate/games/ui/AppRoot.kt b/app/src/main/java/org/prolibertate/games/ui/AppRoot.kt index e464b49..7ac58f1 100644 --- a/app/src/main/java/org/prolibertate/games/ui/AppRoot.kt +++ b/app/src/main/java/org/prolibertate/games/ui/AppRoot.kt @@ -20,6 +20,8 @@ import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -49,6 +51,7 @@ import org.prolibertate.games.net.ScorekeeperSession import org.prolibertate.games.score.ScorekeeperRepository import org.prolibertate.games.settings.Settings import org.prolibertate.games.settings.SettingsRepository +import org.prolibertate.games.settings.YahtzeeStatsRepository import org.prolibertate.games.ui.game.LocalCardSpeed import org.prolibertate.games.update.UpdateChannel import org.prolibertate.games.update.Updater @@ -68,6 +71,7 @@ class AppEnv( val peerId: String, /** Games in progress, kept between sessions. */ val saves: SavedGameStore, + val yahtzeeStats: YahtzeeStatsRepository, ) /** @@ -139,7 +143,14 @@ fun AppRoot(env: AppEnv) { // Card motion reads its speed from here rather than from a parameter. // Every card screen animates, most of them several layers deep, and the // alternative was passing one unchanging number down through all of it. - CompositionLocalProvider(LocalCardSpeed provides settings.animationSpeed) { + val density = LocalDensity.current + val scaledDensity = remember(density, settings.textScale) { + Density(density.density, density.fontScale * settings.textScale) + } + CompositionLocalProvider( + LocalCardSpeed provides settings.animationSpeed, + LocalDensity provides scaledDensity, + ) { AppContent( env = env, settings = settings, diff --git a/app/src/main/java/org/prolibertate/games/ui/PlayScreen.kt b/app/src/main/java/org/prolibertate/games/ui/PlayScreen.kt index 50435f0..ff3039b 100644 --- a/app/src/main/java/org/prolibertate/games/ui/PlayScreen.kt +++ b/app/src/main/java/org/prolibertate/games/ui/PlayScreen.kt @@ -594,7 +594,12 @@ fun PlayScreen( ) } StartMatch(env, controller, route) - YahtzeeScreen(controller = controller, localSeat = route.localSeat, onExit = onExit) + YahtzeeScreen( + controller = controller, + localSeat = route.localSeat, + stats = env.yahtzeeStats, + onExit = onExit, + ) } GameCatalog.KLONDIKE -> { diff --git a/app/src/main/java/org/prolibertate/games/ui/SettingsScreen.kt b/app/src/main/java/org/prolibertate/games/ui/SettingsScreen.kt index 26410ef..ce45e8b 100644 --- a/app/src/main/java/org/prolibertate/games/ui/SettingsScreen.kt +++ b/app/src/main/java/org/prolibertate/games/ui/SettingsScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch +import kotlin.math.roundToInt import org.prolibertate.games.settings.Settings import org.prolibertate.games.update.Release import org.prolibertate.games.update.UpdateChannel @@ -83,6 +84,26 @@ fun SettingsScreen( Divider() + Column { + Text("Text size", fontWeight = FontWeight.Bold) + Text( + text = "${(settings.textScale * 100).roundToInt()}% — " + + "scales text throughout the app, on top of your phone's setting", + style = MaterialTheme.typography.bodySmall, + ) + Slider( + value = settings.textScale, + onValueChange = { scale -> + scope.launch { env.settingsRepository.setTextScale(scale) } + }, + valueRange = Settings.MIN_TEXT_SCALE..Settings.MAX_TEXT_SCALE, + // 10% increments across 80%-160%. + steps = 7, + ) + } + + Divider() + Column { Text("Your name", fontWeight = FontWeight.Bold) Text( diff --git a/app/src/main/java/org/prolibertate/games/ui/game/YahtzeeScreen.kt b/app/src/main/java/org/prolibertate/games/ui/game/YahtzeeScreen.kt index 61e4caf..0202de1 100644 --- a/app/src/main/java/org/prolibertate/games/ui/game/YahtzeeScreen.kt +++ b/app/src/main/java/org/prolibertate/games/ui/game/YahtzeeScreen.kt @@ -6,7 +6,11 @@ import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -30,12 +34,17 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import org.prolibertate.games.game.yahtzee.DICE_COUNT import org.prolibertate.games.game.yahtzee.RollDice import org.prolibertate.games.game.yahtzee.ScoreIn import org.prolibertate.games.game.yahtzee.YahtzeeCategory +import org.prolibertate.games.game.yahtzee.YahtzeeComparison +import org.prolibertate.games.game.yahtzee.recordOf +import org.prolibertate.games.settings.YahtzeeStatsRepository +import kotlin.math.roundToInt import org.prolibertate.games.game.yahtzee.YahtzeeMove import org.prolibertate.games.game.yahtzee.YahtzeeSection import org.prolibertate.games.game.yahtzee.YahtzeeState @@ -61,6 +70,7 @@ private const val TITLE = "Yahtzee" fun YahtzeeScreen( controller: MatchController, localSeat: Int, + stats: YahtzeeStatsRepository, onExit: () -> Unit, ) { val state by controller.state.collectAsState() @@ -69,6 +79,11 @@ fun YahtzeeScreen( val kept = remember { mutableStateListOf() } var confirmingEnd by remember { mutableStateOf(false) } + var confirmingZero by remember { mutableStateOf(null) } + var comparison by remember { mutableStateOf(null) } + // Only a game seen to finish is recorded: reopening a saved game that was + // already over must not count it a second time. + var sawInProgress by remember { mutableStateOf(false) } val endGameAction: @Composable () -> Unit = { TextButton(onClick = { confirmingEnd = true }) { Text("End game") } @@ -89,6 +104,15 @@ fun YahtzeeScreen( // refused, and the Throw button would look broken again. LaunchedEffect(current.turn, current.round) { kept.clear() } + LaunchedEffect(current.isOver) { + if (!current.isOver) { + sawInProgress = true + } else if (sawInProgress) { + sawInProgress = false + comparison = stats.finish(recordOf(current, localSeat)) + } + } + val yours = current.turn == localSeat && !current.isOver val canRoll = legal.any { it is RollDice } val writable = legal.filterIsInstance().map { it.category }.toSet() @@ -138,8 +162,14 @@ fun YahtzeeScreen( localSeat = localSeat, writable = if (yours) writable else emptySet(), onWrite = { - controller.submit(ScoreIn(it)) - kept.clear() + // A zero is a box spent for nothing, and one mis-tap on a + // dense card is all it takes, so ask first. + if (scoreOf(it, current.dice) == 0) { + confirmingZero = it + } else { + controller.submit(ScoreIn(it)) + kept.clear() + } }, ) @@ -150,9 +180,79 @@ fun YahtzeeScreen( abandoned?.let { LeftCardDialog(it, onExit, controller::dismissAbandoned) } if (confirmingEnd) EndCardDialog(onExit) { confirmingEnd = false } + comparison?.let { StatsDialog(it, onExit) { comparison = null } } + confirmingZero?.let { category -> + ZeroScoreDialog( + category = category, + onConfirm = { + confirmingZero = null + controller.submit(ScoreIn(category)) + kept.clear() + }, + onCancel = { confirmingZero = null }, + ) + } } } +@Composable +private fun StatsDialog(c: YahtzeeComparison, onLeave: () -> Unit, onClose: () -> Unit) { + fun pct(v: Double?) = v?.let { "${(it * 100).roundToInt()}%" } ?: "—" + val headline = when { + c.previousGames == 0 -> "Your first recorded game." + c.rank == 1 -> "A new personal best!" + else -> "${ordinal(c.rank)} best of ${c.gamesRanked} games." + } + AlertDialog( + onDismissRequest = onClose, + title = { Text("Game over: ${c.current.total} points") }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + Text(headline, fontWeight = FontWeight.Bold) + StatLine("", "This game", "Before") + StatLine("Score", "${c.current.total}", c.averageBefore?.let { "avg ${it.roundToInt()}, best ${c.bestBefore}" } ?: "—") + StatLine("Upper bonus", if (c.current.gotBonus) "Yes" else "No", pct(c.bonusRateBefore)) + StatLine("Yahtzees", "${c.current.yahtzees}", "${c.yahtzeesBefore} total") + StatLine("Zeroed boxes", "${c.current.zeros}", "") + if (c.current.players > 1) { + StatLine("Result", if (c.current.won) "Won" else "Lost", "won ${pct(c.winRateBefore)}") + } + Text( + "Games before this one: ${c.previousGames}", + style = MaterialTheme.typography.bodySmall, + ) + } + }, + confirmButton = { TextButton(onClick = onLeave) { Text("Leave the game") } }, + dismissButton = { TextButton(onClick = onClose) { Text("Look at the card") } }, + ) +} + +@Composable +private fun StatLine(label: String, now: String, before: String) { + Row(modifier = Modifier.fillMaxWidth()) { + Text(label, modifier = Modifier.weight(1.2f), fontWeight = FontWeight.Bold) + Text(now, modifier = Modifier.weight(1f)) + Text(before, modifier = Modifier.weight(1.6f)) + } +} + +private fun ordinal(n: Int): String { + val suffix = if (n % 100 in 11..13) "th" else when (n % 10) { 1 -> "st"; 2 -> "nd"; 3 -> "rd"; else -> "th" } + return "$n$suffix" +} + +@Composable +private fun ZeroScoreDialog(category: YahtzeeCategory, onConfirm: () -> Unit, onCancel: () -> Unit) { + AlertDialog( + onDismissRequest = onCancel, + title = { Text("Score zero?") }, + text = { Text("These dice are worth 0 in ${category.label}. Write the zero there?") }, + confirmButton = { TextButton(onClick = onConfirm) { Text("Score 0") } }, + dismissButton = { TextButton(onClick = onCancel) { Text("Choose another") } }, + ) +} + @Composable private fun DiceRow( dice: List, @@ -190,12 +290,29 @@ private fun Die(face: Int?, held: Boolean, enabled: Boolean, onClick: () -> Unit .clickable(enabled = enabled) { onClick() }, contentAlignment = Alignment.Center, ) { - Text( - text = face?.toString() ?: "·", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - color = WallaceRed, - ) + if (face != null) { + Canvas(modifier = Modifier.size(48.dp)) { + val radius = size.minDimension * 0.08f + pipPositions(face).forEach { (x, y) -> + drawCircle(WallaceRed, radius, Offset(size.width * x, size.height * y)) + } + } + } + } +} + +/** Pip centres as fractions of the die face, in the usual casino layout. */ +private fun pipPositions(face: Int): List> { + val lo = 0.28f + val mid = 0.5f + val hi = 0.72f + return when (face) { + 1 -> listOf(mid to mid) + 2 -> listOf(lo to lo, hi to hi) + 3 -> listOf(lo to lo, mid to mid, hi to hi) + 4 -> listOf(lo to lo, hi to lo, lo to hi, hi to hi) + 5 -> listOf(lo to lo, hi to lo, mid to mid, lo to hi, hi to hi) + else -> listOf(lo to lo, hi to lo, lo to mid, hi to mid, lo to hi, hi to hi) } } @@ -206,28 +323,61 @@ private fun ScoreCard( writable: Set, onWrite: (YahtzeeCategory) -> Unit, ) { - Column(modifier = Modifier.fillMaxWidth()) { - Row(modifier = Modifier.fillMaxWidth()) { - Text("", modifier = Modifier.weight(2f), style = MaterialTheme.typography.labelSmall) - state.cards.indices.forEach { seat -> - Text( - text = if (seat == localSeat) "You" else "S$seat", - modifier = Modifier.weight(1f), - style = MaterialTheme.typography.labelSmall, - fontWeight = FontWeight.Bold, - ) - } - } - YahtzeeSection.entries.forEach { section -> - YahtzeeCategory.entries.filter { it.section == section }.forEach { category -> - CardRow(state, localSeat, category, writable, onWrite) - } - if (section == YahtzeeSection.UPPER) { + // Upper boxes and bonus on the left, lower boxes and total on the right, so + // each half is wide enough for text that can actually be read. Each half + // has a floor on its width; if two of them do not fit (many players, large + // text) the pair scrolls sideways instead of squeezing. + BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { + val needed = (CARD_LABEL_WIDTH + CARD_SEAT_WIDTH * state.cards.size).dp + val half = maxOf((maxWidth - CARD_GAP.dp) / 2, needed) + Row( + modifier = Modifier.horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(CARD_GAP.dp), + ) { + Column(modifier = Modifier.width(half)) { + CardHeader(state, localSeat) + CardSection(YahtzeeSection.UPPER, state, localSeat, writable, onWrite) TotalRow("Bonus at 63", state.cards.map { it.upperBonus }) } + Column(modifier = Modifier.width(half)) { + CardHeader(state, localSeat) + CardSection(YahtzeeSection.LOWER, state, localSeat, writable, onWrite) + Divider() + TotalRow("Total", state.cards.indices.map { state.totalFor(it) }, bold = true) + } } - Divider() - TotalRow("Total", state.cards.indices.map { state.totalFor(it) }, bold = true) + } +} + +private const val CARD_LABEL_WIDTH = 120 +private const val CARD_SEAT_WIDTH = 56 +private const val CARD_GAP = 12 + +@Composable +private fun CardHeader(state: YahtzeeState, localSeat: Int) { + Row(modifier = Modifier.fillMaxWidth()) { + Text("", modifier = Modifier.weight(2f)) + state.cards.indices.forEach { seat -> + Text( + text = if (seat == localSeat) "You" else "S$seat", + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Bold, + ) + } + } +} + +@Composable +private fun CardSection( + section: YahtzeeSection, + state: YahtzeeState, + localSeat: Int, + writable: Set, + onWrite: (YahtzeeCategory) -> Unit, +) { + YahtzeeCategory.entries.filter { it.section == section }.forEach { category -> + CardRow(state, localSeat, category, writable, onWrite) } } @@ -244,12 +394,12 @@ private fun CardRow( modifier = Modifier .fillMaxWidth() .clickable(enabled = open) { onWrite(category) } - .padding(vertical = 2.dp), + .padding(vertical = 5.dp), ) { Text( text = category.label, modifier = Modifier.weight(2f), - style = MaterialTheme.typography.bodySmall, + style = MaterialTheme.typography.bodyMedium, fontWeight = if (open) FontWeight.Bold else FontWeight.Normal, ) state.cards.forEachIndexed { seat, card -> @@ -263,7 +413,7 @@ private fun CardRow( else -> "—" }, modifier = Modifier.weight(1f), - style = MaterialTheme.typography.bodySmall, + style = MaterialTheme.typography.bodyMedium, color = if (written == null && open) WallaceRed else MaterialTheme.colorScheme.onSurface, ) } @@ -272,18 +422,18 @@ private fun CardRow( @Composable private fun TotalRow(label: String, values: List, bold: Boolean = false) { - Row(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp)) { + Row(modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp)) { Text( text = label, modifier = Modifier.weight(2f), - style = MaterialTheme.typography.bodySmall, + style = MaterialTheme.typography.bodyMedium, fontWeight = if (bold) FontWeight.Bold else FontWeight.Normal, ) values.forEach { Text( text = "$it", modifier = Modifier.weight(1f), - style = MaterialTheme.typography.bodySmall, + style = MaterialTheme.typography.bodyMedium, fontWeight = if (bold) FontWeight.Bold else FontWeight.Normal, ) } diff --git a/app/src/test/java/org/prolibertate/games/game/yahtzee/YahtzeeStatsTest.kt b/app/src/test/java/org/prolibertate/games/game/yahtzee/YahtzeeStatsTest.kt new file mode 100644 index 0000000..eedb645 --- /dev/null +++ b/app/src/test/java/org/prolibertate/games/game/yahtzee/YahtzeeStatsTest.kt @@ -0,0 +1,30 @@ +package org.prolibertate.games.game.yahtzee + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class YahtzeeStatsTest { + private fun game(total: Int, bonus: Boolean = false, yahtzees: Int = 0, won: Boolean = true) = + YahtzeeRecord(total, bonus, yahtzees, zeros = 0, players = 2, won = won) + + @Test + fun firstGameHasNothingToCompareTo() { + val c = compareToHistory(emptyList(), game(200)) + assertEquals(1, c.rank) + assertEquals(0, c.previousGames) + assertNull(c.bestBefore) + assertNull(c.averageBefore) + } + + @Test + fun ranksAgainstPreviousGames() { + val history = listOf(game(150, bonus = true, yahtzees = 1), game(250), game(200, won = false)) + val c = compareToHistory(history, game(220)) + assertEquals(2, c.rank) + assertEquals(4, c.gamesRanked) + assertEquals(250, c.bestBefore) + assertEquals(200.0, c.averageBefore!!, 0.001) + assertEquals(1, c.yahtzeesBefore) + } +}