Persist cross-session coach history
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
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 org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class PlayerHistoryTest {
|
||||
|
||||
private fun review(
|
||||
street: Street,
|
||||
intended: ActionType,
|
||||
chosen: ActionType,
|
||||
aligned: Boolean = false,
|
||||
) = CoachingReview(
|
||||
decisionToken = 1,
|
||||
handNumber = 1,
|
||||
street = street,
|
||||
trace = DecisionTrace(
|
||||
estimatedEquity = null,
|
||||
handStrengthPercentile = null,
|
||||
breakEvenEquity = null,
|
||||
decisionThreshold = null,
|
||||
preflopRangeThreshold = null,
|
||||
potOdds = null,
|
||||
intended = Action(intended),
|
||||
chosen = Action(chosen),
|
||||
mistakeApplied = false,
|
||||
adjustments = emptyList(),
|
||||
reason = "test",
|
||||
),
|
||||
alignedWithBaseline = aligned,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `every strategic disagreement has one stable aggregate category`() {
|
||||
val cases = listOf(
|
||||
Triple(Street.PREFLOP, ActionType.FOLD to ActionType.CALL, LeakKind.PREFLOP_TOO_LOOSE),
|
||||
Triple(Street.PREFLOP, ActionType.CALL to ActionType.FOLD, LeakKind.PREFLOP_TOO_TIGHT),
|
||||
Triple(Street.PREFLOP, ActionType.RAISE to ActionType.CALL, LeakKind.PREFLOP_MISSED_RAISE),
|
||||
Triple(Street.PREFLOP, ActionType.CALL to ActionType.RAISE, LeakKind.PREFLOP_OVERAGGRESSION),
|
||||
Triple(Street.RIVER, ActionType.FOLD to ActionType.CALL, LeakKind.POSTFLOP_LOOSE_CONTINUE),
|
||||
Triple(Street.RIVER, ActionType.CALL to ActionType.FOLD, LeakKind.POSTFLOP_OVERFOLD),
|
||||
Triple(Street.RIVER, ActionType.BET to ActionType.CHECK, LeakKind.POSTFLOP_MISSED_AGGRESSION),
|
||||
Triple(Street.RIVER, ActionType.CHECK to ActionType.BET, LeakKind.POSTFLOP_OVERAGGRESSION),
|
||||
)
|
||||
|
||||
for ((street, actions, expected) in cases) {
|
||||
assertEquals(
|
||||
expected,
|
||||
classifyLeak(review(street, actions.first, actions.second)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `persisted leak identifiers are unique and version one stable`() {
|
||||
assertEquals(1, PLAYER_HISTORY_SCHEMA_VERSION)
|
||||
assertEquals(
|
||||
listOf(
|
||||
"preflop_too_loose",
|
||||
"preflop_too_tight",
|
||||
"preflop_missed_raise",
|
||||
"preflop_overaggression",
|
||||
"postflop_loose_continue",
|
||||
"postflop_overfold",
|
||||
"postflop_missed_aggression",
|
||||
"postflop_overaggression",
|
||||
),
|
||||
LeakKind.entries.map(LeakKind::id),
|
||||
)
|
||||
assertEquals(LeakKind.entries.size, LeakKind.entries.map(LeakKind::id).toSet().size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `aligned decisions improve agreement without inventing a leak`() {
|
||||
val aligned = review(
|
||||
street = Street.FLOP,
|
||||
intended = ActionType.BET,
|
||||
chosen = ActionType.RAISE,
|
||||
aligned = true,
|
||||
)
|
||||
val history = PlayerHistory().recordCoachReview(aligned)
|
||||
|
||||
assertEquals(1L, history.decisionsReviewed)
|
||||
assertEquals(1L, history.baselineAligned)
|
||||
assertTrue(history.leakCounts.isEmpty())
|
||||
assertNull(classifyLeak(aligned))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `session preferences hands and reviews aggregate without raw hand data`() {
|
||||
val looseCall = review(Street.TURN, ActionType.FOLD, ActionType.CALL)
|
||||
val history = PlayerHistory()
|
||||
.recordSession("Jay", coachEnabled = true)
|
||||
.recordCompletedHand()
|
||||
.recordCompletedHand()
|
||||
.recordCoachReview(looseCall)
|
||||
.recordCoachReview(looseCall)
|
||||
|
||||
assertEquals("Jay", history.playerName)
|
||||
assertTrue(history.coachEnabled)
|
||||
assertEquals(1L, history.sessionsStarted)
|
||||
assertEquals(2L, history.handsCompleted)
|
||||
assertEquals(2L, history.decisionsReviewed)
|
||||
assertEquals(2L, history.leakCount(LeakKind.POSTFLOP_LOOSE_CONTINUE))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `history refuses to call a small sample a trend`() {
|
||||
val history = PlayerHistory(
|
||||
sessionsStarted = 1,
|
||||
handsCompleted = 4,
|
||||
decisionsReviewed = 3,
|
||||
baselineAligned = 1,
|
||||
leakCounts = mapOf(LeakKind.POSTFLOP_OVERFOLD to 2),
|
||||
)
|
||||
val presentation = historyPresentation(history)!!
|
||||
|
||||
assertEquals("4 hands saved • 3 coached decisions", presentation.headline)
|
||||
assertTrue("7 more coached decisions" in presentation.detail)
|
||||
assertTrue("trend" in presentation.detail)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mature history reports agreement and the most repeated review`() {
|
||||
val history = PlayerHistory(
|
||||
sessionsStarted = 3,
|
||||
handsCompleted = 42,
|
||||
decisionsReviewed = 20,
|
||||
baselineAligned = 13,
|
||||
leakCounts = mapOf(
|
||||
LeakKind.PREFLOP_TOO_LOOSE to 3,
|
||||
LeakKind.POSTFLOP_OVERFOLD to 4,
|
||||
),
|
||||
)
|
||||
val presentation = historyPresentation(history)!!
|
||||
|
||||
assertTrue("Baseline agreement 65%" in presentation.detail)
|
||||
assertTrue(LeakKind.POSTFLOP_OVERFOLD.label in presentation.detail)
|
||||
assertTrue("(4)" in presentation.detail)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `brand new player has no fake history card`() {
|
||||
assertNull(historyPresentation(PlayerHistory()))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user