Advertisement

← poker-tutor

Sources/PokerSession/FreeView.swift

4.7 KB Download file
import Foundation
import PokerCoach

/// What a player who has not paid is shown of their own scorecard.
///
/// Ken, 2026-09-11: "a user can play 100 hands and get only their most common leak. Not the most
/// expensive, the most common. And then blur out all of the others, so that they can see that
/// there are other leaks to their game, but not what they are. And give them access only to the
/// drill that addresses that one leak."
///
/// This is the one place that decides. Every screen asks it — the scorecard, the assessment
/// report, the drills list, the practice buttons in the lessons, the card on Home — and nothing
/// else in the app works out for itself who is allowed to read what. A subscriber sees everything;
/// the questions below all answer "open" for them without looking at the card.
///
/// Two refusals carry over from the scorecard unchanged. "Most common" is chosen among habits the
/// card would already name — ones that have cleared their own floor of chances, happen more often
/// than a steady player manages, and have happened at least three times — so the free tier never
/// names a habit the hands cannot support. And where nothing has cleared a floor yet, the row shown
/// is the most common of the ones the card is only *watching*, and it is labelled as not proved,
/// exactly as that tier is labelled for a subscriber.
public struct FreeView: Sendable, Equatable {

    /// Why the one row was chosen, which is also how it is labelled.
    public enum Basis: Sendable, Equatable {
        /// A habit the card names: enough chances, too often, at least three times.
        case named
        /// Nothing has cleared its floor; this is the most common of the ones being watched, and
        /// the screen says it is not proved.
        case watching
    }

    /// Whether anything is held back at all. False for a subscriber.
    public let isRestricted: Bool
    /// The one habit shown in full, if the hands have produced one.
    public let visibleLeakID: String?
    public let basis: Basis?
    /// How many other rows are on the card but blurred.
    public let hiddenCount: Int

    /// The view for a subscriber, or for a build with no tally: nothing hidden.
    public static let open = FreeView(isRestricted: false, visibleLeakID: nil, basis: nil, hiddenCount: 0)

    public init(isRestricted: Bool, visibleLeakID: String?, basis: Basis?, hiddenCount: Int) {
        self.isRestricted = isRestricted
        self.visibleLeakID = visibleLeakID
        self.basis = basis
        self.hiddenCount = hiddenCount
    }

    /// The view for this player over this card.
    public init(of card: LeakScorecard, entitlement: Entitlement) {
        if case .subscribed = entitlement {
            self = .open
            return
        }
        let chosen = FreeView.mostCommon(in: card)
        // "Other leaks in their game": the habits the card names or is watching, less the one
        // shown. Habits measured and found fine are not a reveal and are not counted.
        let inTheirGame = card.costing.count + card.watching.count
        self.init(isRestricted: true,
                  visibleLeakID: chosen?.entry.id,
                  basis: chosen?.basis,
                  hiddenCount: max(0, inTheirGame - (chosen == nil ? 0 : 1)))
    }

    /// The most common habit the card would name; failing that, the most common one it watches.
    ///
    /// "Most common" is how many times it happened — not what it cost, which is the order the
    /// card itself uses. A tie goes to the one that happens more often per chance.
    static func mostCommon(in card: LeakScorecard) -> (entry: LeakScorecard.Entry, basis: Basis)? {
        func pick(_ list: [LeakScorecard.Entry]) -> LeakScorecard.Entry? {
            list.max { a, b in
                if a.times != b.times { return a.times < b.times }
                return a.rate < b.rate
            }
        }
        if let named = pick(card.costing) { return (named, .named) }
        if let watched = pick(card.watching) { return (watched, .watching) }
        return nil
    }

    /// Whether this habit's name and numbers are hidden from the player. Every row but the one
    /// shown, on any screen that would otherwise say how the player does at it.
    public func isBlurred(leakID: String) -> Bool {
        guard isRestricted else { return false }
        return leakID != visibleLeakID
    }

    /// Whether the player may open the drill (or the re-check, or the lesson's practice) for this
    /// habit.
    public func mayDrill(leakID: String) -> Bool {
        guard isRestricted else { return true }
        return leakID == visibleLeakID
    }
}