feat(SCOPONE-0009): complete iteration 0 dealer AI

This commit is contained in:
Giancarmine Salucci
2026-04-08 21:50:40 +02:00
parent c9accb7ae4
commit d0a44d295a
7 changed files with 597 additions and 174 deletions

View File

@@ -4,12 +4,35 @@ export interface CardTrackerSnapshot {
playedCardIds: string[];
}
export interface CardTrackerValueParityResidue {
value: number;
knownCount: number;
unseenCount: number;
hasOddUnseenResidue: boolean;
hasEvenUnseenResidue: boolean;
}
interface VisibleValueResidueKnowledge {
unseenCards: Card[];
unseenCountBySuit: Record<Suit, number>;
unseenCountByValue: number[];
valueParityResidues: CardTrackerValueParityResidue[];
}
function normalizeSnapshot(snapshot: CardTrackerSnapshot): CardTrackerSnapshot {
return {
playedCardIds: Array.from(new Set(snapshot.playedCardIds)),
};
}
function createEmptySuitCounts(): Record<Suit, number> {
const counts = {} as Record<Suit, number>;
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.
@@ -65,44 +88,96 @@ export class CardTracker {
return !this.played.has('denara_7');
}
private buildVisibleValueResidueKnowledge(myHand: Card[], table: Card[]): VisibleValueResidueKnowledge {
const knownCardIds = new Set<string>(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 valueParityResidues: CardTrackerValueParityResidue[] = [];
for (let value = 1; value <= 10; value++) {
const unseenCount = unseenCountByValue[value];
valueParityResidues.push({
value,
knownCount: knownCountByValue[value],
unseenCount,
hasOddUnseenResidue: unseenCount % 2 === 1,
hasEvenUnseenResidue: unseenCount % 2 === 0,
});
}
return {
unseenCards,
unseenCountBySuit,
unseenCountByValue,
valueParityResidues,
};
}
/**
* 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[] {
const known = new Set<string>();
for (const id of this.played) known.add(id);
for (const c of myHand) known.add(c.id);
for (const c of table) known.add(c.id);
const unseen: Card[] = [];
for (const suit of SUITS) {
for (let v = 1; v <= 10; v++) {
const id = `${suit}_${v}`;
if (!known.has(id)) {
unseen.push({ suit, value: v, id });
}
}
}
return unseen;
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.getUnseenCards(myHand, table).filter(c => c.suit === suit).length;
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.getUnseenCards(myHand, table).filter(c => c.value === value).length;
return this.getValueParityResidue(value, myHand, table).unseenCount;
}
/** Get visible known-count, unseen-count, and parity residue for a single value */
getValueParityResidue(value: number, myHand: Card[], table: Card[]): CardTrackerValueParityResidue {
const valueParityResidues = this.buildVisibleValueResidueKnowledge(myHand, table).valueParityResidues;
return valueParityResidues[value - 1] ?? {
value,
knownCount: 0,
unseenCount: 0,
hasOddUnseenResidue: false,
hasEvenUnseenResidue: true,
};
}
/** Get visible known-count, unseen-count, and parity residue for all card values */
getValueParityResidueSummary(myHand: Card[], table: Card[]): CardTrackerValueParityResidue[] {
return this.buildVisibleValueResidueKnowledge(myHand, table).valueParityResidues;
}
/** 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 unseen = this.getUnseenCards(myHand, table);
const matching = unseen.filter(c => c.value === value).length;
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;