From b6ea330e07e6bc09faa53fcaf754a3e3ec10e350 Mon Sep 17 00:00:00 2001 From: Jay Date: Mon, 27 Jul 2026 10:50:41 -0400 Subject: [PATCH] Add two-stage raise confirmation --- CLAUDE.md | 8 +- .../com/jsjdesigns/poker/ActionButtons.kt | 12 +- .../java/com/jsjdesigns/poker/TableScreen.kt | 279 +++++++++++------- .../com/jsjdesigns/poker/ActionButtonsTest.kt | 11 + 4 files changed, 198 insertions(+), 112 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1ef324f..ea33a03 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -154,9 +154,11 @@ export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home" straddles the outside rail rather than occupying the community-card band. The terminal modal repeats the board, every public hand turned up (including losing hands), and exact pot awards so dimming the table never hides the - explanation. Raise sizing combines coarse slider/presets with one-chip minus - and plus controls; visible chip stacks grow with bets and the pot while numeric - labels remain authoritative. + explanation. The default action bar contains only Fold/Check-or-Call/Raise; + Raise opens a confirmation screen with coarse slider/presets, one-chip minus + and plus controls, and explicit UI/system Back handling. Merely opening or + leaving sizing never submits an action. Visible chip stacks grow with bets and + the pot while numeric labels remain authoritative. - A cash-game session begins at an explicit take-a-seat screen. Opponents may auto-reload below the big blind; the human is never silently topped up and must explicitly choose "Reload to N & deal" from the completed-hand screen. diff --git a/app/src/main/java/com/jsjdesigns/poker/ActionButtons.kt b/app/src/main/java/com/jsjdesigns/poker/ActionButtons.kt index a86cf09..29e3bb2 100644 --- a/app/src/main/java/com/jsjdesigns/poker/ActionButtons.kt +++ b/app/src/main/java/com/jsjdesigns/poker/ActionButtons.kt @@ -68,8 +68,18 @@ fun adjustRaiseAmount( buttons: ActionButtons, ): Int = (current + change).coerceIn(buttons.sliderMin, buttons.sliderMax) +/** The exact amount submitted by the confirmation screen. */ +fun raiseConfirmationAmount( + buttons: ActionButtons, + selected: Int, +): Int = if (buttons.showSlider) { + selected.coerceIn(buttons.sliderMin, buttons.sliderMax) +} else { + buttons.fixedRaiseTo +} + /** - * Honest, legal raise-to shortcuts for the always-visible sizing rail. + * Honest, legal raise-to shortcuts for the raise-sizing screen. * * Facing a bet, a fraction describes the extra raise after calling: the pot * after a call is [DecisionOffer.pot] plus the affordable call, and the final diff --git a/app/src/main/java/com/jsjdesigns/poker/TableScreen.kt b/app/src/main/java/com/jsjdesigns/poker/TableScreen.kt index 38b9521..bffa2e6 100644 --- a/app/src/main/java/com/jsjdesigns/poker/TableScreen.kt +++ b/app/src/main/java/com/jsjdesigns/poker/TableScreen.kt @@ -1,5 +1,6 @@ package com.jsjdesigns.poker +import androidx.activity.compose.BackHandler import androidx.compose.foundation.Canvas import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -33,9 +34,9 @@ import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -757,8 +758,12 @@ private fun DecisionControls( onRaise: (Long, Int) -> Unit, ) { val buttons = buttonsFor(offer) - var raiseTo by remember { mutableIntStateOf(buttons.fixedRaiseTo) } - LaunchedEffect(offer.token) { raiseTo = buttons.fixedRaiseTo } + var sizingRaise by remember(offer.token) { mutableStateOf(false) } + var raiseTo by remember(offer.token) { mutableIntStateOf(buttons.fixedRaiseTo) } + + BackHandler(enabled = sizingRaise) { + sizingRaise = false + } Column( modifier = Modifier @@ -771,129 +776,187 @@ private fun DecisionControls( .padding(horizontal = 14.dp, vertical = 10.dp), verticalArrangement = Arrangement.spacedBy(8.dp), ) { - if (buttons.showRaise && buttons.showSlider) { + if (!sizingRaise) { + PrimaryDecisionButtons( + offer = offer, + buttons = buttons, + onFold = onFold, + onCheckCall = onCheckCall, + onOpenRaise = { sizingRaise = true }, + ) + } else { + val amount = raiseConfirmationAmount(buttons, raiseTo) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - Text( - "RAISE TO", - color = Muted.copy(alpha = 0.72f), - fontWeight = FontWeight.Bold, - fontSize = 12.sp, - letterSpacing = 1.4.sp, - ) - Text( - "$raiseTo", - color = GoldLight, - fontWeight = FontWeight.ExtraBold, - fontSize = 22.sp, - ) - } - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - FineTuneButton("−") { - raiseTo = adjustRaiseAmount(raiseTo, -1, buttons) - } - Slider( - value = raiseTo.coerceIn(buttons.sliderMin, buttons.sliderMax).toFloat(), - onValueChange = { raiseTo = it.roundToInt() }, - valueRange = buttons.sliderMin.toFloat()..buttons.sliderMax.toFloat(), - colors = SliderDefaults.colors( - thumbColor = GoldLight, - activeTrackColor = TableGold, - inactiveTrackColor = Color.White.copy(alpha = 0.12f), - ), - modifier = Modifier - .weight(1f) - .height(44.dp), - ) - FineTuneButton("+") { - raiseTo = adjustRaiseAmount(raiseTo, 1, buttons) + BackToActionsButton { sizingRaise = false } + Column(horizontalAlignment = Alignment.End) { + Text( + "RAISE TO", + color = Muted.copy(alpha = 0.82f), + fontWeight = FontWeight.Bold, + fontSize = 12.sp, + letterSpacing = 1.4.sp, + ) + Text( + "$amount", + color = GoldLight, + fontWeight = FontWeight.ExtraBold, + fontSize = 24.sp, + ) } } - val presets = raisePresets(offer, buttons) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(6.dp), - ) { - presets.forEach { preset -> - Button( - onClick = { raiseTo = preset.amount }, + + if (buttons.showSlider) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + FineTuneButton("−") { + raiseTo = adjustRaiseAmount(raiseTo, -1, buttons) + } + Slider( + value = amount.toFloat(), + onValueChange = { raiseTo = it.roundToInt() }, + valueRange = buttons.sliderMin.toFloat()..buttons.sliderMax.toFloat(), + colors = SliderDefaults.colors( + thumbColor = GoldLight, + activeTrackColor = TableGold, + inactiveTrackColor = Color.White.copy(alpha = 0.12f), + ), modifier = Modifier .weight(1f) - .height(42.dp), - contentPadding = PaddingValues(horizontal = 4.dp), - colors = ButtonDefaults.buttonColors( - containerColor = if (raiseTo == preset.amount) { - TableGold.copy(alpha = 0.2f) - } else { - Color.White.copy(alpha = 0.06f) - }, - contentColor = if (raiseTo == preset.amount) GoldLight else Muted, - ), - shape = RoundedCornerShape(9.dp), - ) { - Text( - "${preset.label} ${preset.amount}", - fontWeight = FontWeight.Bold, - fontSize = 11.sp, - maxLines = 1, - ) + .height(44.dp), + ) + FineTuneButton("+") { + raiseTo = adjustRaiseAmount(raiseTo, 1, buttons) } } - } - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - if (buttons.showFold) { - ActionButton( - title = "Fold", - detail = null, - container = Danger.copy(alpha = 0.58f), - content = Color(0xFFF0BDBD), - modifier = Modifier.weight(1f), - onClick = { onFold(offer.token) }, - ) - } - ActionButton( - title = when { - offer.canCheck -> "Check" - offer.callIsAllIn -> "All in" - else -> "Call" - }, - detail = offer.callAmount.takeUnless { offer.canCheck }?.toString(), - container = Color.White.copy(alpha = 0.09f), - content = Cream, - modifier = Modifier.weight(1f), - onClick = { onCheckCall(offer.token) }, - ) - if (buttons.showRaise) { - val amount = if (buttons.showSlider) { - raiseTo.coerceIn(buttons.sliderMin, buttons.sliderMax) - } else { - buttons.fixedRaiseTo + val presets = raisePresets(offer, buttons) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + presets.forEach { preset -> + Button( + onClick = { raiseTo = preset.amount }, + modifier = Modifier + .weight(1f) + .height(42.dp), + contentPadding = PaddingValues(horizontal = 4.dp), + colors = ButtonDefaults.buttonColors( + containerColor = if (amount == preset.amount) { + TableGold.copy(alpha = 0.2f) + } else { + Color.White.copy(alpha = 0.06f) + }, + contentColor = if (amount == preset.amount) GoldLight else Muted, + ), + shape = RoundedCornerShape(9.dp), + ) { + Text( + "${preset.label} ${preset.amount}", + fontWeight = FontWeight.Bold, + fontSize = 11.sp, + maxLines = 1, + ) + } + } } - ActionButton( - title = if (amount >= buttons.sliderMax) "All in" else "Raise", - detail = if (amount >= buttons.sliderMax) "$amount" else "to $amount", - container = Brush.verticalGradient(listOf(GoldLight, Color(0xFFC79A38))), - content = Color(0xFF22190A), - modifier = Modifier.weight(1.12f), - onClick = { onRaise(offer.token, amount) }, + } else { + Text( + if (amount >= buttons.sliderMax) { + "Your stack leaves one legal raise: all in for $amount." + } else { + "There is one legal raise size: $amount." + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + color = Cream.copy(alpha = 0.82f), + fontSize = 13.sp, + textAlign = TextAlign.Center, ) } + + ActionButton( + title = if (amount >= buttons.sliderMax) "Confirm all in" else "Confirm raise", + detail = if (amount >= buttons.sliderMax) "$amount" else "to $amount", + container = Brush.verticalGradient(listOf(GoldLight, Color(0xFFC79A38))), + content = Color(0xFF22190A), + modifier = Modifier.fillMaxWidth(), + onClick = { onRaise(offer.token, amount) }, + ) } } } +@Composable +private fun PrimaryDecisionButtons( + offer: DecisionOffer, + buttons: ActionButtons, + onFold: (Long) -> Unit, + onCheckCall: (Long) -> Unit, + onOpenRaise: () -> Unit, +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (buttons.showFold) { + ActionButton( + title = "Fold", + detail = null, + container = Danger.copy(alpha = 0.58f), + content = Color(0xFFF0BDBD), + modifier = Modifier.weight(1f), + onClick = { onFold(offer.token) }, + ) + } + ActionButton( + title = when { + offer.canCheck -> "Check" + offer.callIsAllIn -> "All in" + else -> "Call" + }, + detail = offer.callAmount.takeUnless { offer.canCheck }?.toString(), + container = Color.White.copy(alpha = 0.09f), + content = Cream, + modifier = Modifier.weight(1f), + onClick = { onCheckCall(offer.token) }, + ) + if (buttons.showRaise) { + ActionButton( + title = "Raise", + detail = null, + container = Brush.verticalGradient(listOf(GoldLight, Color(0xFFC79A38))), + content = Color(0xFF22190A), + modifier = Modifier.weight(1.12f), + onClick = onOpenRaise, + ) + } + } +} + +@Composable +private fun BackToActionsButton(onClick: () -> Unit) { + Button( + onClick = onClick, + modifier = Modifier.height(44.dp), + contentPadding = PaddingValues(horizontal = 14.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.White.copy(alpha = 0.09f), + contentColor = Cream, + ), + shape = RoundedCornerShape(12.dp), + ) { + Text("‹ Back", fontWeight = FontWeight.Bold, fontSize = 14.sp) + } +} + @Composable private fun FineTuneButton( label: String, diff --git a/app/src/test/java/com/jsjdesigns/poker/ActionButtonsTest.kt b/app/src/test/java/com/jsjdesigns/poker/ActionButtonsTest.kt index c46b537..50f2bf1 100644 --- a/app/src/test/java/com/jsjdesigns/poker/ActionButtonsTest.kt +++ b/app/src/test/java/com/jsjdesigns/poker/ActionButtonsTest.kt @@ -152,6 +152,17 @@ class ActionButtonsTest { assertEquals(20, adjustRaiseAmount(20, -1, b)) assertEquals(500, adjustRaiseAmount(500, 1, b)) } + + @Test + fun `raise confirmation clamps a range but ignores selection for an exact raise`() { + val range = buttonsFor(offer(minRaiseTo = 20, maxRaiseTo = 500)) + assertEquals(20, raiseConfirmationAmount(range, -10)) + assertEquals(60, raiseConfirmationAmount(range, 60)) + assertEquals(500, raiseConfirmationAmount(range, 900)) + + val exact = buttonsFor(offer(minRaiseTo = 100, maxRaiseTo = 100)) + assertEquals(100, raiseConfirmationAmount(exact, 999)) + } } class CallCostTest {