UNPKG

@alexjamesmalcolm/poker-odds-machine

Version:
391 lines (334 loc) 9.92 kB
import { shuffle } from './util'; export enum Suits { club = 1, diamond, heart, spade, } export enum Ranks { two = 2, three, four, five, six, seven, eight, nine, ten, jack, queen, king, ace, } export enum HandRanks { highCard = 1, pair, twoPair, trips, straight, flush, fullHouse, quads, straightFlush, } export interface InternalInput { hands: string[]; numPlayers: number; board: string; boardSize: number; handSize: number; numDecks: number; returnHandStats: boolean; returnTieHandStats: boolean; iterations: number; } export type Input = | ExternalInputWithEverythingProvided | ExternalInputWithHands | ExternalInputWithNumberOfPlayers; export const isInputAnEternalInputWithEverythingProvided = ( input: Input ): input is ExternalInputWithEverythingProvided => isInputAnExternalInputWithHands(input) && isInputAnExternalInputWithNumberOfPlayers(input); interface ExternalInputWithEverythingProvided extends BaseExternalInput { hands: string[]; } export const isInputAnExternalInputWithHands = ( input: Input ): input is ExternalInputWithHands => 'hands' in input; interface ExternalInputWithHands extends BaseExternalInput { hands: string[]; } export const isInputAnExternalInputWithNumberOfPlayers = ( input: Input ): input is ExternalInputWithNumberOfPlayers => { return 'numPlayers' in input; }; interface ExternalInputWithNumberOfPlayers extends BaseExternalInput { numPlayers: number; } interface BaseExternalInput { board?: string; boardSize?: number; handSize?: number; numDecks?: number; returnHandStats?: boolean; returnTieHandStats?: boolean; iterations?: number; } export type BestHand = { hand: CardGroup; handRank: HandRanks; }; export const isStatsWithBothHandAndTieHandStats = ( stats: Stats ): stats is StatsWithBothHandAndTieHandStats => isStatsWithHandStats(stats) && isStatsWithTieHandStats(stats); type StatsWithBothHandAndTieHandStats = StatsWithHandStats & StatsWithTieHandStats; export const isStatsWithHandStats = ( stats: Stats ): stats is StatsWithHandStats => 'handStats' in stats; export const makeStatsHandStatsIfItIsNotAlready = ( stats: Stats ): StatsWithHandStats => { if (isStatsWithHandStats(stats)) return stats; (stats as StatsWithHandStats).handStats = {}; return stats as StatsWithHandStats; }; interface StatsWithHandStats extends BaseStats { handStats: HandStats; } export const isStatsWithTieHandStats = ( stats: Stats ): stats is StatsWithTieHandStats => 'tieHandStats' in stats; export const makeStatsTieHandStatsIfItIsNotAlready = ( stats: Stats ): StatsWithTieHandStats => { if (isStatsWithTieHandStats(stats)) return stats; (stats as StatsWithTieHandStats).tieHandStats = {}; return stats as StatsWithTieHandStats; }; interface StatsWithTieHandStats extends BaseStats { tieHandStats: HandStats; } export type Stats = | BaseStats | StatsWithHandStats | StatsWithTieHandStats | StatsWithBothHandAndTieHandStats; interface BaseStats { winCount: number; tieCount: number; winPercent?: number; tiePercent?: number; } type HandStats = Record< string, { count: number; percent: number; } >; class Suit { static fromString(s: string): Suits { switch (s) { case 'c': return Suits.club; case 'd': return Suits.diamond; case 'h': return Suits.heart; case 's': return Suits.spade; default: throw new Error(`Invalid card suit string: ${s}`); } } static toString(suit: Suits): string { if (!(suit in Suits)) throw new Error(`Invalid suit value: ${suit}`); return Suits[suit][0]; } static toLongName(suit: Suits, plural?: boolean): string { if (!(suit in Suits)) throw new Error(`Invalid suit value: ${suit}`); let longName = Suits[suit]; if (plural) longName += 's'; return longName; } } class Rank { static fromString(s: string): Ranks { switch (s) { case 'T': return Ranks.ten; case 'J': return Ranks.jack; case 'Q': return Ranks.queen; case 'K': return Ranks.king; case 'A': return Ranks.ace; default: const n = Number(s); if (isNaN(n) || n < Ranks.two || n > Ranks.nine) throw new Error(`Invalid card rank string: ${s}`); return n; } } static toString(r: Ranks): string { switch (r) { case Ranks.ten: return 'T'; case Ranks.jack: return 'J'; case Ranks.queen: return 'Q'; case Ranks.king: return 'K'; case Ranks.ace: return 'A'; default: if (isNaN(r) || r < Ranks.two || r > Ranks.ace) throw new Error(`Invalid card rank value: ${r}`); return r.toString(); } } static toLongName(rank: Ranks): string { if (!(rank in Ranks)) throw new Error(`Invalid rank value: ${rank}`); return Ranks[rank]; } } export class Card { private _rank: Ranks; private _suit: Suits; get rank(): Ranks { return this._rank; } get suit(): Suits { return this._suit; } constructor(s: string) { Card.validateCardString(s); this._rank = Rank.fromString(s[0]); this._suit = Suit.fromString(s[1]); } static validateCardString(s: string) { if (s.length !== 2) throw new Error( `Card string must have a length of 2. Invalid: ${s}` ); if ( !['T', 'J', 'Q', 'K', 'A'].includes(s[0]) && (+s[0] < 2 || +s[0] > 9) ) throw new Error( `Card string must begin with 2-9, T, J, Q, K, or A. Invalid: ${s}` ); if (!['c', 'd', 'h', 's'].includes(s[1])) throw new Error( `Card string must end with c, d, h, or s. Invalid: ${s}` ); } equals(card: Card): boolean { // todo: implement range card == standard card return this._rank === card.rank && this._suit === card.suit; } toString(): string { return Rank.toString(this._rank) + Suit.toString(this._suit); } toLongName(): string { return `${Rank.toLongName(this._rank)} of ${Suit.toLongName( this._suit, true )}`; } } export class CardGroup { protected _cards: Card[] = []; get cards(): Card[] { return this._cards; } constructor(cards?: string); constructor(cards?: Card[]); constructor(cards?: Card); constructor(cards?: string | Card | Card[]) { if (!cards) return; // todo: why are these conditions necessary to call function? if (Array.isArray(cards)) this.addCards(cards); else if (typeof cards === 'string') this.addCards(cards); else this.addCards(cards); } static validateCardGroupString(s: string) { for (const e of s.split(',')) { Card.validateCardString(e); } } toString(): string { return this._cards.map((c) => c.toString()).join(','); } addCardGroup(cardGroup: CardGroup): void { this._cards.push(...cardGroup.cards); } addCards(card: string): void; addCards(card: Card): void; addCards(card: Card[]): void; addCards(cards: string | Card | Card[]): void { if (typeof cards === 'string') this.addCardsString(cards); else if (Array.isArray(cards)) this._cards.push(...cards); else this._cards.push(cards); } sortDesc(): void { this._cards.sort((a, b) => b.suit - a.suit); this._cards.sort((a, b) => b.rank - a.rank); } countBy(type: 'rank' | 'suit'): Record<string, number> { const map: Record<string, number> = {}; for (const card of this._cards) { const prop = type === 'rank' ? card.rank : card.suit; if (!(prop in map)) map[prop] = 1; else map[prop]++; } return map; } private addCardsString(s: string) { for (const e of s.split(',')) { const card = new Card(e); this.addCards(card); } } } export class Deck extends CardGroup { constructor(numDecks: number) { const deckString = '2c,2d,2h,2s,3c,3d,3h,3s,4c,4d,4h,4s,5c,5d,5h,5s,6c,6d,6h,6s,7c,7d,7h,7s,8c,8d,8h,8s,9c,9d,9h,9s,Tc,Td,Th,Ts,Jc,Jd,Jh,Js,Qc,Qd,Qh,Qs,Kc,Kd,Kh,Ks,Ac,Ad,Ah,As'; const decksString = Array(numDecks).fill(deckString); super(decksString.join(',')); } pop(): Card { const card = this._cards.pop(); if (card === undefined) throw new Error( 'Deck is empty. There are either too many players, or the boardSize is too large' ); return card; } removeCard(cardToRemove: Card): Card { let found = false; for (let i = 0; i < this._cards.length; i++) { if (this._cards[i].equals(cardToRemove)) { this._cards.splice(i, 1); found = true; } } if (!found) throw new Error( `CardGroup does not contain card string: ${cardToRemove.toString()}` ); return cardToRemove; } shuffle(): void { shuffle(this._cards); } }