Add deterministic post-action coach

This commit is contained in:
Jay
2026-07-26 15:27:25 -04:00
parent 3aa55dd2ce
commit 4d8a62afb6
15 changed files with 756 additions and 22 deletions
@@ -10,6 +10,12 @@ class CashGameSessionTest {
@Test
fun `blank player name has an honest second-person fallback`() {
assertEquals("You", cashGameConfig(" ").playerName)
assertFalse("coach must be opt-in", cashGameConfig("Jay").coachEnabled)
}
@Test
fun `coach preference crosses the session boundary explicitly`() {
assertTrue(cashGameConfig("Jay", coachEnabled = true).coachEnabled)
}
@Test
@@ -0,0 +1,115 @@
package com.jsjdesigns.poker
import com.jsjdesigns.poker.bot.CoachingReview
import com.jsjdesigns.poker.bot.DecisionTrace
import com.jsjdesigns.poker.game.Action
import com.jsjdesigns.poker.game.ActionType
import com.jsjdesigns.poker.game.Street
import com.jsjdesigns.poker.game.TableSnapshot
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
class CoachPresentationTest {
private fun review(
handNumber: Int = 4,
token: Long = 12,
street: Street = Street.RIVER,
aligned: Boolean = false,
trace: DecisionTrace = postflopTrace(),
) = CoachingReview(
decisionToken = token,
handNumber = handNumber,
street = street,
trace = trace,
alignedWithBaseline = aligned,
)
@Test
fun `postflop presentation consumes audited equity and pot odds fields`() {
val presentation = coachPresentation(review())
assertEquals("Worth reviewing", presentation.title)
assertEquals(
"Estimated 34% equity vs random hands; calling needs 25%. Baseline: Call 20.",
presentation.detail,
)
}
@Test
fun `preflop presentation never describes percentile as equity`() {
val trace = DecisionTrace(
estimatedEquity = null,
handStrengthPercentile = 0.08,
breakEvenEquity = null,
decisionThreshold = null,
preflopRangeThreshold = 0.22,
potOdds = null,
intended = Action(ActionType.RAISE, 6),
chosen = Action(ActionType.RAISE, 8),
mistakeApplied = false,
adjustments = emptyList(),
reason = "chart baseline",
)
val presentation = coachPresentation(
review(street = Street.PREFLOP, aligned = true, trace = trace),
)
assertEquals("Baseline agrees", presentation.title)
assertTrue("percentile" in presentation.detail)
assertTrue("equity" !in presentation.detail)
assertTrue("Raise to 6" in presentation.detail)
}
@Test
fun `review is visible only for opted-in exact latest decision and hand`() {
val review = review()
val matching = UiState(
gameConfig = CashGameConfig("Jay", coachEnabled = true),
snapshot = snapshot(handNumber = 4),
coachReview = review,
latestHeroDecisionToken = 12,
)
assertEquals(review, matching.visibleCoachReview())
assertNull(
matching.copy(
gameConfig = matching.gameConfig?.copy(coachEnabled = false),
).visibleCoachReview(),
)
assertNull(matching.copy(latestHeroDecisionToken = 13).visibleCoachReview())
assertNull(matching.copy(snapshot = snapshot(handNumber = 5)).visibleCoachReview())
}
private fun postflopTrace() = DecisionTrace(
estimatedEquity = 0.34,
handStrengthPercentile = null,
breakEvenEquity = 0.25,
decisionThreshold = 0.25,
preflopRangeThreshold = null,
potOdds = "60:20",
intended = Action(ActionType.CALL, 20),
chosen = Action(ActionType.FOLD),
mistakeApplied = false,
adjustments = emptyList(),
reason = "fundamentals baseline",
)
private fun snapshot(handNumber: Int) = TableSnapshot(
handNumber = handNumber,
street = Street.RIVER,
phase = TableSnapshot.Phase.BETTING,
board = emptyList(),
pot = 80,
currentBet = 20,
minRaiseSize = 20,
button = 0,
seats = emptyList(),
toAct = null,
toActToken = null,
lastAction = null,
)
}