package com.jsjdesigns.poker.bot

import com.jsjdesigns.poker.game.ActionType
import com.jsjdesigns.poker.game.HandEvent

/**
 * A running read on how aggressive each opponent is.
 *
 * Only consulted by skill levels with [SkillLevel.readsOpponents] set, which is
 * what separates a player who merely plays their own cards well from one who
 * adjusts to the table.
 */
class OpponentModel {

    private val aggressiveActions = HashMap<Int, Int>()
    private val totalActions = HashMap<Int, Int>()
    private var consumed = 0
    private var currentHand = -1

    /**
     * Folds in any events not yet seen.
     *
     * The hand number is required, not inferred. History is cleared each hand, so
     * a size comparison is not enough: if we consumed 3 events last hand and first
     * look at the next hand once it already has 4, `4 < 3` is false and the first
     * three events would be silently skipped.
     */
    fun observe(history: List<HandEvent>, handNumber: Int) {
        if (handNumber != currentHand) {
            currentHand = handNumber
            consumed = 0
        }
        while (consumed < history.size) {
            val e = history[consumed++]
            totalActions[e.seat] = (totalActions[e.seat] ?: 0) + 1
            if (e.action.type == ActionType.BET || e.action.type == ActionType.RAISE) {
                aggressiveActions[e.seat] = (aggressiveActions[e.seat] ?: 0) + 1
            }
        }
    }

    /**
     * Share of this opponent's actions that were bets or raises, or
     * [NEUTRAL_AGGRESSION] until there is enough of a sample to judge.
     */
    fun aggressionRate(seat: Int): Double {
        val total = totalActions[seat] ?: 0
        if (total < MIN_SAMPLE) return NEUTRAL_AGGRESSION
        return (aggressiveActions[seat] ?: 0).toDouble() / total
    }

    fun actionsObserved(seat: Int): Int = totalActions[seat] ?: 0

    companion object {
        /** Below this, a read is noise rather than information. */
        private const val MIN_SAMPLE = 25

        /**
         * Prior for an ordinary player's bet-and-raise share of actions.
         *
         * Actions counted include folds, checks and calls. The controlled style
         * calibration measures a neutral Tight-Aggressive profile at about 0.22;
         * this prior is guarded there rather than justified by an assertion that
         * merely checks whether a constant looks plausible.
         */
        const val NEUTRAL_AGGRESSION = 0.22
    }
}
