@alexjamesmalcolm/poker-odds-machine
Version:
Poker odds machine (calculator)
391 lines (334 loc) • 9.92 kB
text/typescript
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);
}
}