Make bot traces and position adjustments honest

This commit is contained in:
Jay
2026-07-26 10:42:11 -04:00
parent 0ac69b101a
commit db1f96b421
3 changed files with 115 additions and 29 deletions
@@ -17,11 +17,17 @@ import kotlin.random.Random
* LLM. The model narrates these values; it never computes them.
*/
data class DecisionTrace(
val equity: Double,
val breakEvenEquity: Double,
/** The final threshold actually used after every adjustment. */
val decisionThreshold: Double,
val potOdds: String,
/** Monte Carlo equity when equity is actually estimated; null pre-flop. */
val estimatedEquity: Double?,
/** Pre-flop chart percentile, where 0 is strongest; null post-flop. */
val handStrengthPercentile: Double?,
/** Raw pot-odds threshold when it drives the decision; null pre-flop. */
val breakEvenEquity: Double?,
/** Final post-flop equity threshold after adjustments; null pre-flop. */
val decisionThreshold: Double?,
/** Top fraction of starting hands played in this spot; null post-flop. */
val preflopRangeThreshold: Double?,
val potOdds: String?,
/** What the strategy selected before a skill error was applied. */
val intended: Action,
val chosen: Action,
@@ -33,6 +39,26 @@ data class DecisionTrace(
data class ThresholdAdjustment(val name: String, val factor: Double)
/**
* Balances an in-position multiplier against all out-of-position seats.
*
* If exactly one of N active players is in position, the weighted average factor
* is 1.0 for any table size. Positive [inPositionDelta] widens a range in
* position; negative values lower a calling threshold in position.
*/
internal fun balancedPositionFactor(
activeOpponents: Int,
inPosition: Boolean,
awareness: Double,
inPositionDelta: Double,
): Double {
val players = (activeOpponents + 1).coerceAtLeast(2)
val inPositionShare = 1.0 / players
val delta = inPositionDelta * awareness
val outOfPositionDelta = -delta * inPositionShare / (1.0 - inPositionShare)
return if (inPosition) 1.0 + delta else 1.0 + outOfPositionDelta
}
/**
* A bot that decides from equity and pot odds, then distorts that decision through
* its [SkillLevel] and [PlayStyle].
@@ -91,9 +117,14 @@ class MathBot(
bar *= styleFactor
adjustments += ThresholdAdjustment("style risk tolerance", styleFactor)
// Position is worth real equity, and better players know it.
val positionFactor = if (ctx.inPosition) 1.0 - 0.12 * skill.positionAwareness
else 1.0 + 0.10 * skill.positionAwareness
// Position changes where calls are made, not how often overall. Derive
// the out-of-position counterpart from the active table size.
val positionFactor = balancedPositionFactor(
activeOpponents = ctx.activeOpponents,
inPosition = ctx.inPosition,
awareness = skill.positionAwareness,
inPositionDelta = -0.12,
)
bar *= positionFactor
adjustments += ThresholdAdjustment("position", positionFactor)
@@ -125,9 +156,11 @@ class MathBot(
val mistakeApplied = chosen != intended
lastTrace = DecisionTrace(
equity = equity,
estimatedEquity = equity,
handStrengthPercentile = null,
breakEvenEquity = breakEven,
decisionThreshold = bar,
preflopRangeThreshold = null,
potOdds = if (ctx.toCall > 0) "${ctx.pot}:${ctx.toCall}" else "no bet to call",
intended = intended,
chosen = chosen,
@@ -182,13 +215,13 @@ class MathBot(
action
}
val mistakeApplied = final != action
val equityScore = 1.0 - pct
val threshold = 1.0 - gate
lastTrace = DecisionTrace(
equity = equityScore,
breakEvenEquity = Equity.potOdds(ctx.pot, ctx.toCall),
decisionThreshold = threshold,
potOdds = if (ctx.toCall > 0) "${ctx.pot}:${ctx.toCall}" else "no bet to call",
estimatedEquity = null,
handStrengthPercentile = pct,
breakEvenEquity = null,
decisionThreshold = null,
preflopRangeThreshold = gate,
potOdds = null,
intended = action,
chosen = final,
mistakeApplied = mistakeApplied,
@@ -340,11 +373,12 @@ class MathBot(
}
private fun preflopPositionFactor(ctx: DecisionContext, awareness: Double): Double {
val players = (ctx.activeOpponents + 1).coerceAtLeast(2)
val inPositionShare = 1.0 / players
val widening = 0.45 * awareness
val narrowing = widening * inPositionShare / (1.0 - inPositionShare)
return if (ctx.inPosition) 1.0 + widening else 1.0 - narrowing
return balancedPositionFactor(
activeOpponents = ctx.activeOpponents,
inPosition = ctx.inPosition,
awareness = awareness,
inPositionDelta = 0.45,
)
}
private companion object {