Add two-stage raise confirmation
This commit is contained in:
@@ -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.
|
straddles the outside rail rather than occupying the community-card band.
|
||||||
The terminal modal repeats the board, every public hand turned up (including
|
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
|
losing hands), and exact pot awards so dimming the table never hides the
|
||||||
explanation. Raise sizing combines coarse slider/presets with one-chip minus
|
explanation. The default action bar contains only Fold/Check-or-Call/Raise;
|
||||||
and plus controls; visible chip stacks grow with bets and the pot while numeric
|
Raise opens a confirmation screen with coarse slider/presets, one-chip minus
|
||||||
labels remain authoritative.
|
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
|
- 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
|
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.
|
explicitly choose "Reload to N & deal" from the completed-hand screen.
|
||||||
|
|||||||
@@ -68,8 +68,18 @@ fun adjustRaiseAmount(
|
|||||||
buttons: ActionButtons,
|
buttons: ActionButtons,
|
||||||
): Int = (current + change).coerceIn(buttons.sliderMin, buttons.sliderMax)
|
): 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
|
* 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
|
* after a call is [DecisionOffer.pot] plus the affordable call, and the final
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.jsjdesigns.poker
|
package com.jsjdesigns.poker
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -33,9 +34,9 @@ import androidx.compose.material3.SliderDefaults
|
|||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -757,8 +758,12 @@ private fun DecisionControls(
|
|||||||
onRaise: (Long, Int) -> Unit,
|
onRaise: (Long, Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
val buttons = buttonsFor(offer)
|
val buttons = buttonsFor(offer)
|
||||||
var raiseTo by remember { mutableIntStateOf(buttons.fixedRaiseTo) }
|
var sizingRaise by remember(offer.token) { mutableStateOf(false) }
|
||||||
LaunchedEffect(offer.token) { raiseTo = buttons.fixedRaiseTo }
|
var raiseTo by remember(offer.token) { mutableIntStateOf(buttons.fixedRaiseTo) }
|
||||||
|
|
||||||
|
BackHandler(enabled = sizingRaise) {
|
||||||
|
sizingRaise = false
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -771,129 +776,187 @@ private fun DecisionControls(
|
|||||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.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(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
BackToActionsButton { sizingRaise = false }
|
||||||
"RAISE TO",
|
Column(horizontalAlignment = Alignment.End) {
|
||||||
color = Muted.copy(alpha = 0.72f),
|
Text(
|
||||||
fontWeight = FontWeight.Bold,
|
"RAISE TO",
|
||||||
fontSize = 12.sp,
|
color = Muted.copy(alpha = 0.82f),
|
||||||
letterSpacing = 1.4.sp,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
fontSize = 12.sp,
|
||||||
Text(
|
letterSpacing = 1.4.sp,
|
||||||
"$raiseTo",
|
)
|
||||||
color = GoldLight,
|
Text(
|
||||||
fontWeight = FontWeight.ExtraBold,
|
"$amount",
|
||||||
fontSize = 22.sp,
|
color = GoldLight,
|
||||||
)
|
fontWeight = FontWeight.ExtraBold,
|
||||||
}
|
fontSize = 24.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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val presets = raisePresets(offer, buttons)
|
|
||||||
Row(
|
if (buttons.showSlider) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
presets.forEach { preset ->
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
Button(
|
) {
|
||||||
onClick = { raiseTo = preset.amount },
|
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
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.height(42.dp),
|
.height(44.dp),
|
||||||
contentPadding = PaddingValues(horizontal = 4.dp),
|
)
|
||||||
colors = ButtonDefaults.buttonColors(
|
FineTuneButton("+") {
|
||||||
containerColor = if (raiseTo == preset.amount) {
|
raiseTo = adjustRaiseAmount(raiseTo, 1, buttons)
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
val presets = raisePresets(offer, buttons)
|
||||||
}
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
Row(
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
) {
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
presets.forEach { preset ->
|
||||||
) {
|
Button(
|
||||||
if (buttons.showFold) {
|
onClick = { raiseTo = preset.amount },
|
||||||
ActionButton(
|
modifier = Modifier
|
||||||
title = "Fold",
|
.weight(1f)
|
||||||
detail = null,
|
.height(42.dp),
|
||||||
container = Danger.copy(alpha = 0.58f),
|
contentPadding = PaddingValues(horizontal = 4.dp),
|
||||||
content = Color(0xFFF0BDBD),
|
colors = ButtonDefaults.buttonColors(
|
||||||
modifier = Modifier.weight(1f),
|
containerColor = if (amount == preset.amount) {
|
||||||
onClick = { onFold(offer.token) },
|
TableGold.copy(alpha = 0.2f)
|
||||||
)
|
} else {
|
||||||
}
|
Color.White.copy(alpha = 0.06f)
|
||||||
ActionButton(
|
},
|
||||||
title = when {
|
contentColor = if (amount == preset.amount) GoldLight else Muted,
|
||||||
offer.canCheck -> "Check"
|
),
|
||||||
offer.callIsAllIn -> "All in"
|
shape = RoundedCornerShape(9.dp),
|
||||||
else -> "Call"
|
) {
|
||||||
},
|
Text(
|
||||||
detail = offer.callAmount.takeUnless { offer.canCheck }?.toString(),
|
"${preset.label} ${preset.amount}",
|
||||||
container = Color.White.copy(alpha = 0.09f),
|
fontWeight = FontWeight.Bold,
|
||||||
content = Cream,
|
fontSize = 11.sp,
|
||||||
modifier = Modifier.weight(1f),
|
maxLines = 1,
|
||||||
onClick = { onCheckCall(offer.token) },
|
)
|
||||||
)
|
}
|
||||||
if (buttons.showRaise) {
|
}
|
||||||
val amount = if (buttons.showSlider) {
|
|
||||||
raiseTo.coerceIn(buttons.sliderMin, buttons.sliderMax)
|
|
||||||
} else {
|
|
||||||
buttons.fixedRaiseTo
|
|
||||||
}
|
}
|
||||||
ActionButton(
|
} else {
|
||||||
title = if (amount >= buttons.sliderMax) "All in" else "Raise",
|
Text(
|
||||||
detail = if (amount >= buttons.sliderMax) "$amount" else "to $amount",
|
if (amount >= buttons.sliderMax) {
|
||||||
container = Brush.verticalGradient(listOf(GoldLight, Color(0xFFC79A38))),
|
"Your stack leaves one legal raise: all in for $amount."
|
||||||
content = Color(0xFF22190A),
|
} else {
|
||||||
modifier = Modifier.weight(1.12f),
|
"There is one legal raise size: $amount."
|
||||||
onClick = { onRaise(offer.token, 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
|
@Composable
|
||||||
private fun FineTuneButton(
|
private fun FineTuneButton(
|
||||||
label: String,
|
label: String,
|
||||||
|
|||||||
@@ -152,6 +152,17 @@ class ActionButtonsTest {
|
|||||||
assertEquals(20, adjustRaiseAmount(20, -1, b))
|
assertEquals(20, adjustRaiseAmount(20, -1, b))
|
||||||
assertEquals(500, adjustRaiseAmount(500, 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 {
|
class CallCostTest {
|
||||||
|
|||||||
Reference in New Issue
Block a user