diff --git a/app/src/main/java/com/jsjdesigns/poker/ActionButtons.kt b/app/src/main/java/com/jsjdesigns/poker/ActionButtons.kt index 856014f..1674464 100644 --- a/app/src/main/java/com/jsjdesigns/poker/ActionButtons.kt +++ b/app/src/main/java/com/jsjdesigns/poker/ActionButtons.kt @@ -11,6 +11,8 @@ import com.jsjdesigns.poker.game.DecisionOffer data class ActionButtons( val showFold: Boolean, val checkOrCallLabel: String, + /** What calling actually costs: the engine clamps a call to the stack. */ + val effectiveCall: Int, val showRaise: Boolean, /** False when there is exactly one legal raise size, so a slider is pointless. */ val showSlider: Boolean, @@ -29,10 +31,20 @@ fun buttonsFor(offer: DecisionOffer): ActionButtons { val low = if (shoveOnly) offer.maxRaiseTo else offer.minRaiseTo val high = offer.maxRaiseTo + // Facing more than you have left is a call for your whole stack: the engine + // clamps the commitment, so quoting the full price promises something the + // player cannot pay and does not owe. + val effectiveCall = minOf(offer.toCall, offer.stack) + return ActionButtons( // Folding a free hand is never correct, so don't invite an accidental muck. showFold = !offer.canCheck, - checkOrCallLabel = if (offer.canCheck) "Check" else "Call ${offer.toCall}", + checkOrCallLabel = when { + offer.canCheck -> "Check" + effectiveCall >= offer.stack -> "All in $effectiveCall" + else -> "Call $effectiveCall" + }, + effectiveCall = effectiveCall, showRaise = offer.canRaise, showSlider = high > low, sliderMin = low, diff --git a/app/src/main/java/com/jsjdesigns/poker/PokerViewModel.kt b/app/src/main/java/com/jsjdesigns/poker/PokerViewModel.kt index 21ecba4..844cd48 100644 --- a/app/src/main/java/com/jsjdesigns/poker/PokerViewModel.kt +++ b/app/src/main/java/com/jsjdesigns/poker/PokerViewModel.kt @@ -153,7 +153,10 @@ class PokerViewModel : ViewModel() { fun checkOrCall(token: Long) { val o = offer.value ?: return if (o.token != token) return - submit(token, if (o.canCheck) Action(ActionType.CHECK) else Action(ActionType.CALL, o.toCall)) + // Send what the button promised. The engine would clamp an oversized call + // anyway, but the submitted action should not disagree with the label. + val cost = minOf(o.toCall, o.stack) + submit(token, if (o.canCheck) Action(ActionType.CHECK) else Action(ActionType.CALL, cost)) } fun raiseTo(token: Long, amount: Int) = submit(token, Action(ActionType.RAISE, amount)) diff --git a/app/src/test/java/com/jsjdesigns/poker/ActionButtonsTest.kt b/app/src/test/java/com/jsjdesigns/poker/ActionButtonsTest.kt index 106d07c..6b7e5a6 100644 --- a/app/src/test/java/com/jsjdesigns/poker/ActionButtonsTest.kt +++ b/app/src/test/java/com/jsjdesigns/poker/ActionButtonsTest.kt @@ -97,3 +97,48 @@ class ActionButtonsTest { assertEquals("All in", raiseLabel(b, 500)) } } + +class CallCostTest { + + private fun offer(toCall: Int, stack: Int) = DecisionOffer( + token = 1L, + handNumber = 1, + street = Street.RIVER, + seat = 0, + hole = listOf(0, 1), + board = emptyList(), + pot = 100, + toCall = toCall, + minRaiseTo = 999, + maxRaiseTo = stack, + stack = stack, + canCheck = false, + canRaise = false, + ) + + /** + * Facing more than you have left is a call for your whole stack; the engine + * clamps the commitment. Quoting the full price promised a payment the player + * could not make and did not owe. + */ + @Test + fun `facing more than the stack is labelled as an all in for the stack`() { + val b = buttonsFor(offer(toCall = 35, stack = 20)) + assertEquals("All in 20", b.checkOrCallLabel) + assertEquals(20, b.effectiveCall) + } + + @Test + fun `calling exactly the stack is also an all in`() { + val b = buttonsFor(offer(toCall = 20, stack = 20)) + assertEquals("All in 20", b.checkOrCallLabel) + assertEquals(20, b.effectiveCall) + } + + @Test + fun `an affordable call quotes the real price`() { + val b = buttonsFor(offer(toCall = 35, stack = 500)) + assertEquals("Call 35", b.checkOrCallLabel) + assertEquals(35, b.effectiveCall) + } +}