feat(SCOPONE-0009): complete iteration 0 dealer AI
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user