import { Card, Suit, SUITS } from './types'; export interface CardTrackerSnapshot { playedCardIds: string[]; } export interface CardTrackerValueRankResidue { value: number; knownCount: number; unseenCount: number; hasSingletonUnseenRankResidue: boolean; hasPairedUnseenRankResidue: boolean; } interface VisibleValueResidueKnowledge { unseenCards: Card[]; unseenCountBySuit: Record; unseenCountByValue: number[]; valueRankResidues: CardTrackerValueRankResidue[]; } function normalizeSnapshot(snapshot: CardTrackerSnapshot): CardTrackerSnapshot { return { playedCardIds: Array.from(new Set(snapshot.playedCardIds)), }; } function createEmptySuitCounts(): Record { const counts = {} as Record; for (const suit of SUITS) { counts[suit] = 0; } return counts; } /** * Tracks which cards have been played/captured during a round. * Used by AI to infer opponent hands WITHOUT cheating. */ export class CardTracker { private played: Set = new Set(); // card IDs that have been seen constructor(snapshot?: CardTrackerSnapshot) { if (snapshot) { this.restoreSnapshot(snapshot); } } static fromSnapshot(snapshot: CardTrackerSnapshot): CardTracker { return new CardTracker(snapshot); } toSnapshot(): CardTrackerSnapshot { return { playedCardIds: [...this.played], }; } restoreSnapshot(snapshot: CardTrackerSnapshot): void { const normalized = normalizeSnapshot(snapshot); this.played = new Set(normalized.playedCardIds); } /** Record a card being played to the table */ trackPlay(card: Card): void { this.played.add(card.id); } /** Record cards captured from the table */ trackCapture(cards: Card[]): void { for (const c of cards) { this.played.add(c.id); } } /** Reset for a new round */ reset(): void { this.played.clear(); } /** Has a specific card been seen played? */ hasBeenPlayed(cardId: string): boolean { return this.played.has(cardId); } /** Is the settebello (7 of denara) still unseen (not played/captured)? */ isSettebelloUnseen(): boolean { return !this.played.has('denara_7'); } private buildVisibleValueResidueKnowledge(myHand: Card[], table: Card[]): VisibleValueResidueKnowledge { const knownCardIds = new Set(this.played); for (const card of myHand) { knownCardIds.add(card.id); } for (const card of table) { knownCardIds.add(card.id); } const knownCountByValue = Array.from({ length: 11 }, () => 0); const unseenCountByValue = Array.from({ length: 11 }, () => 0); const unseenCountBySuit = createEmptySuitCounts(); const unseenCards: Card[] = []; for (const suit of SUITS) { for (let value = 1; value <= 10; value++) { const id = `${suit}_${value}`; if (knownCardIds.has(id)) { knownCountByValue[value] += 1; continue; } unseenCountByValue[value] += 1; unseenCountBySuit[suit] += 1; unseenCards.push({ suit, value, id }); } } const valueRankResidues: CardTrackerValueRankResidue[] = []; for (let value = 1; value <= 10; value++) { const unseenCount = unseenCountByValue[value]; valueRankResidues.push({ value, knownCount: knownCountByValue[value], unseenCount, hasSingletonUnseenRankResidue: unseenCount % 2 === 1, hasPairedUnseenRankResidue: unseenCount >= 2 && unseenCount % 2 === 0, }); } return { unseenCards, unseenCountBySuit, unseenCountByValue, valueRankResidues, }; } /** * Get cards that could be in opponent hands. * = full 40-card deck minus: already played, my hand, currently on table */ getUnseenCards(myHand: Card[], table: Card[]): Card[] { return this.buildVisibleValueResidueKnowledge(myHand, table).unseenCards; } /** Count how many cards of a suit are still unseen */ countRemainingSuit(suit: Suit, myHand: Card[], table: Card[]): number { return this.buildVisibleValueResidueKnowledge(myHand, table).unseenCountBySuit[suit]; } /** Count how many unseen cards share a value */ countRemainingValue(value: number, myHand: Card[], table: Card[]): number { return this.getValueRankResidue(value, myHand, table).unseenCount; } /** Get visible known-count, unseen-count, and same-rank residue for a single value */ getValueRankResidue(value: number, myHand: Card[], table: Card[]): CardTrackerValueRankResidue { const valueRankResidues = this.buildVisibleValueResidueKnowledge(myHand, table).valueRankResidues; return valueRankResidues[value - 1] ?? { value, knownCount: 0, unseenCount: 0, hasSingletonUnseenRankResidue: false, hasPairedUnseenRankResidue: false, }; } /** Get visible known-count, unseen-count, and same-rank residue for all card values */ getValueRankResidueSummary(myHand: Card[], table: Card[]): CardTrackerValueRankResidue[] { return this.buildVisibleValueResidueKnowledge(myHand, table).valueRankResidues; } /** Probability that a hidden hand contains at least one card with the requested value */ probabilityHandHasValue(value: number, handSize: number, myHand: Card[], table: Card[]): number { if (handSize <= 0) return 0; const visibleValueResidueKnowledge = this.buildVisibleValueResidueKnowledge(myHand, table); const unseen = visibleValueResidueKnowledge.unseenCards; const matching = visibleValueResidueKnowledge.unseenCountByValue[value] ?? 0; if (matching === 0) return 0; if (handSize >= unseen.length) return 1; let probNone = 1; for (let i = 0; i < handSize; i++) { probNone *= Math.max(0, unseen.length - matching - i) / (unseen.length - i); } return 1 - probNone; } /** Get count of all played/seen cards */ get playedCount(): number { return this.played.size; } }