@idealic/poker-engine
Version:
Poker game engine and hand evaluator
1,474 lines (1,313 loc) • 93.4 kB
text/typescript
import { describe, expect, test } from 'vitest';
import { Hand } from '../../..';
import { Game } from '../../../Game';
import { applyAction } from '../../../game/progress';
import { Stats } from '../../../Stats';
// Sample game for testing
const sampleGame: Hand = {
variant: 'NT',
players: ['Alice', 'Bob', 'Carol'],
startingStacks: [1000, 1000, 1000],
blindsOrStraddles: [0, 10, 20],
antes: [0, 0, 0],
actions: [],
minBet: 20,
seed: 12345,
};
describe('Statistics Tracking', () => {
describe('Showdown', () => {
test('should track showdown statistics', () => {
// This test checks that showdown-related stats are tracked correctly.
// It simulates a hand where two players go to showdown and verifies that the 'wentToShowdown' stat is updated.
const table = Game(sampleGame);
// Deal cards
applyAction(table, 'd dh p1 AhKh');
applyAction(table, 'd dh p2 QhJh');
applyAction(table, 'd dh p3 2c3c');
// Preflop betting - all players limp
applyAction(table, 'p1 cc');
expect(Stats.forPlayerStreet(table, 0, 'preflop')).toMatchObject({
calls: 1,
voluntaryPutMoneyInPotTimes: 1,
limps: 1,
limpOpportunities: 1,
});
applyAction(table, 'p2 cc');
expect(Stats.forPlayerStreet(table, 1, 'preflop')).toMatchObject({
calls: 1,
voluntaryPutMoneyInPotTimes: 1,
limps: 1,
limpOpportunities: 1,
});
applyAction(table, 'p3 cc');
expect(Stats.forPlayerStreet(table, 2, 'preflop')).toMatchObject({
calls: 1,
voluntaryPutMoneyInPotTimes: 1,
limps: 1,
limpOpportunities: 1,
});
// Deal flop, turn, river
applyAction(table, 'd db AcKcQc');
applyAction(table, 'p2 cc');
applyAction(table, 'p3 cc');
applyAction(table, 'p1 cc');
applyAction(table, 'd db Jc');
applyAction(table, 'p2 cc');
applyAction(table, 'p3 cc');
applyAction(table, 'p1 cc');
applyAction(table, 'd db Tc');
// River betting leads to showdown
applyAction(table, 'p2 cbr 100');
applyAction(table, 'p3 f');
applyAction(table, 'p1 cc 100');
// Showdown actions
applyAction(table, 'p2 sm QhJh');
applyAction(table, 'p1 sm AhKh');
// Check showdown statistics
const bobRiverStats = Stats.forPlayerStreet(table, 1, 'river');
const carolRiverStats = Stats.forPlayerStreet(table, 2, 'river');
const aliceRiverStats = Stats.forPlayerStreet(table, 0, 'river');
expect(bobRiverStats).toMatchObject({
wentToShowdown: 1,
investments: 100,
});
expect(carolRiverStats).toMatchObject({
wentToShowdown: 0,
});
expect(aliceRiverStats).toMatchObject({
wentToShowdown: 1,
investments: 100,
});
// Test getting all player stats
const aliceStats = Stats.forPlayerStreet(table, 0, 'river');
expect(aliceStats).toMatchObject({
wentToShowdown: 1,
});
});
});
describe('Basic Actions', () => {
test('should track voluntaryPutMoneyInPotTimes when player calls', () => {
// This test verifies that a player's 'voluntaryPutMoneyInPotTimes' statistic is incremented
// when they call a bet pre-flop. This action indicates a willing investment in the pot.
const table = Game(sampleGame);
// Deal cards
applyAction(table, 'd dh p1 AhKh');
applyAction(table, 'd dh p2 QhJh');
applyAction(table, 'd dh p3 2c3c');
// Player 0 (Alice) calls the big blind
applyAction(table, 'p1 cc 20');
const aliceStats = Stats.forPlayerStreet(table, 0, 'preflop');
expect(aliceStats).toMatchObject({
calls: 1,
voluntaryPutMoneyInPotTimes: 1,
limps: 1,
limpOpportunities: 1,
});
});
test('should track betting statistics', () => {
// This test covers a range of betting actions across multiple streets.
// It verifies that stats like raises, calls, folds, and checks are correctly recorded for each player.
const table = Game(sampleGame);
// Deal cards
applyAction(table, 'd dh p1 AhKh');
applyAction(table, 'd dh p2 QhJh');
applyAction(table, 'd dh p3 2c3c');
// Player 0 (Alice) raises - had opportunity to limp but chose to raise
applyAction(table, 'p1 cbr 60');
expect(Stats.forPlayerStreet(table, 0, 'preflop')).toMatchObject({
bets: 0,
raises: 1,
calls: 0,
folds: 0,
voluntaryPutMoneyInPotTimes: 1,
limps: 0,
limpOpportunities: 1,
});
// Player 1 (Bob) calls - no limp opportunity because there was already a raise
applyAction(table, 'p2 cc');
expect(Stats.forPlayerStreet(table, 1, 'preflop')).toMatchObject({
bets: 0,
raises: 0,
checks: 0,
calls: 1,
folds: 0,
voluntaryPutMoneyInPotTimes: 1,
limps: 0,
limpOpportunities: 0,
});
// Player 2 (Carol) folds - no limp opportunity because there was already a raise
applyAction(table, 'p3 f');
expect(Stats.forPlayerStreet(table, 2, 'preflop')).toMatchObject({
bets: 0,
raises: 0,
calls: 0,
folds: 1,
voluntaryPutMoneyInPotTimes: 0,
limps: 0,
limpOpportunities: 0,
});
// Deal flop
applyAction(table, 'd db AcKcQc');
expect(Stats.forPlayerStreet(table, 1, 'flop')).toMatchObject({
bets: 0,
raises: 0,
calls: 0,
folds: 0,
voluntaryPutMoneyInPotTimes: 0,
limps: 0,
limpOpportunities: 0,
});
applyAction(table, 'p2 cc');
expect(Stats.forPlayerStreet(table, 1, 'flop')).toMatchObject({
bets: 0,
raises: 0,
calls: 0,
checks: 1,
folds: 0,
voluntaryPutMoneyInPotTimes: 0,
limps: 0,
limpOpportunities: 0,
});
applyAction(table, 'p1 cbr 100');
expect(Stats.forPlayerStreet(table, 0, 'flop')).toMatchObject({
bets: 1,
raises: 0,
calls: 0,
folds: 0,
voluntaryPutMoneyInPotTimes: 1,
limps: 0,
limpOpportunities: 0,
});
applyAction(table, 'p2 cc');
expect(Stats.forPlayerStreet(table, 1, 'flop')).toMatchObject({
bets: 0,
raises: 0,
calls: 1,
checks: 1,
passivities: 2,
folds: 0,
voluntaryPutMoneyInPotTimes: 0,
limps: 0,
limpOpportunities: 0,
});
});
});
describe('All-ins', () => {
test('should track when a player goes all-in', () => {
// This test ensures that optional statistics, like 'allIns', are tracked correctly when the situation arises.
// It simulates a pre-flop all-in to verify that the 'allIns' counter is incremented.
const table = Game(sampleGame);
// Deal hole cards
applyAction(table, 'd dh p1 AhKh');
expect(Stats.forPlayerStreet(table, 0, 'preflop')).toMatchObject({
limpOpportunities: 0,
});
applyAction(table, 'd dh p2 QhJh');
applyAction(table, 'd dh p3 2c3c');
// Preflop all-in
applyAction(table, 'p1 cbr 1000');
expect(Stats.forPlayerStreet(table, 0, 'preflop')).toMatchObject({
raises: 1,
voluntaryPutMoneyInPotTimes: 1,
allIns: 1,
limpOpportunities: 1,
});
});
});
describe('decision duration', () => {
test('should track decision duration', () => {
// This test ensures that the duration of a player's decision is tracked.
// It creates a simple scenario where a player makes a raise and checks that the decision time is recorded.
const table = Game(sampleGame);
applyAction(table, 'd dh p1 AhKh');
applyAction(table, 'd dh p2 QhJh');
applyAction(table, 'd dh p3 2c3c');
applyAction(table, 'p1 cbr 60');
expect(Stats.forPlayerStreet(table, 0, 'preflop')?.decisionDuration).toBeLessThan(1000);
});
test('should track decision duration', () => {
// This test verifies that decision durations are correctly calculated based on timestamps in the actions.
// It simulates a hand with multiple actions at different timestamps and checks that the average and total decision durations are accurate.
const table = Game({ ...sampleGame, timestamp: 1000 });
applyAction(table, 'd dh p1 AhKh #0000000001001');
applyAction(table, 'd dh p2 QhJh #0000000001011');
applyAction(table, 'd dh p3 2c3c #0000000001111');
applyAction(table, 'p1 cbr 60 #0000000011111');
expect(Stats.forPlayerStreet(table, 0, 'preflop')).toMatchObject({
decisionDuration: 10000,
});
applyAction(table, 'p2 cc #0000000211111');
expect(Stats.forPlayerStreet(table, 1, 'preflop')).toMatchObject({
decisionDuration: 200000,
});
applyAction(table, 'p3 f #0000003211111');
expect(Stats.forPlayerStreet(table, 2, 'preflop')).toMatchObject({
decisionDuration: 3000000,
});
applyAction(table, 'd db AcKcQc #0000043211111');
applyAction(table, 'p2 f #0000243211111');
expect(Stats.forPlayerStreet(table, 1, 'flop')).toMatchObject({
decisionDuration: 200000000,
});
expect(Stats.aggregate(table.stats, ['player'] as const)).toMatchObject({
[Game.getPlayerName(table, 0)]: {
decisionDurationAverage: 10000,
decisionDuration: 10000,
decisions: 1,
},
[Game.getPlayerName(table, 1)]: {
decisionDurationAverage: 100100000,
decisionDuration: 200200000,
decisions: 2,
},
[Game.getPlayerName(table, 2)]: {
decisionDurationAverage: 3000000,
decisionDuration: 3000000,
decisions: 1,
},
total: {
decisionDurationAverage: 203210000 / 4,
decisionDuration: 203210000,
decisions: 4,
},
});
});
});
test('should track showdown statistics', () => {
// This test checks that showdown-related stats are tracked correctly.
// It simulates a hand where two players go to showdown and verifies that the 'wentToShowdown' stat is updated.
const table = Game(sampleGame);
// Deal cards
applyAction(table, 'd dh p1 AhKh');
applyAction(table, 'd dh p2 QhJh');
applyAction(table, 'd dh p3 2c3c');
// Preflop betting - all players limp
applyAction(table, 'p1 cc');
expect(Stats.forPlayerStreet(table, 0, 'preflop')).toMatchObject({
calls: 1,
voluntaryPutMoneyInPotTimes: 1,
limps: 1,
limpOpportunities: 1,
});
applyAction(table, 'p2 cc');
expect(Stats.forPlayerStreet(table, 1, 'preflop')).toMatchObject({
calls: 1,
voluntaryPutMoneyInPotTimes: 1,
limps: 1,
limpOpportunities: 1,
});
applyAction(table, 'p3 cc');
expect(Stats.forPlayerStreet(table, 2, 'preflop')).toMatchObject({
calls: 1,
voluntaryPutMoneyInPotTimes: 1,
limps: 1,
limpOpportunities: 1,
});
// Deal flop, turn, river
applyAction(table, 'd db AcKcQc');
applyAction(table, 'p2 cc');
applyAction(table, 'p3 cc');
applyAction(table, 'p1 cc');
applyAction(table, 'd db Jc');
applyAction(table, 'p2 cc');
applyAction(table, 'p3 cc');
applyAction(table, 'p1 cc');
applyAction(table, 'd db Tc');
// River betting leads to showdown
applyAction(table, 'p2 cbr 100');
applyAction(table, 'p3 f');
applyAction(table, 'p1 cc 100');
// Showdown actions
applyAction(table, 'p2 sm QhJh');
applyAction(table, 'p1 sm AhKh');
// Check showdown statistics
const bobRiverStats = Stats.forPlayerStreet(table, 1, 'river');
const carolRiverStats = Stats.forPlayerStreet(table, 2, 'river');
const aliceRiverStats = Stats.forPlayerStreet(table, 0, 'river');
expect(bobRiverStats).toMatchObject({
wentToShowdown: 1,
});
expect(carolRiverStats).toMatchObject({
wentToShowdown: 0,
});
expect(aliceRiverStats).toMatchObject({
wentToShowdown: 1,
});
// Test getting all player stats
const aliceStats = Stats.forPlayerStreet(table, 0, 'river');
expect(aliceStats).toMatchObject({
wentToShowdown: 1,
});
});
describe('steal', () => {
it('should not count small raises as steal attempts', () => {
// This test ensures that a raise from a steal position (BTN) that is less than the defined
// steal threshold (2.5x BB) is not counted as a steal attempt.
const table = Game({
...sampleGame,
players: ['BTN', 'SB', 'BB'],
startingStacks: [1000, 1000, 1000],
blindsOrStraddles: [0, 10, 20],
});
applyAction(table, 'd dh p1 AhKh');
applyAction(table, 'd dh p2 QhJh');
applyAction(table, 'd dh p3 2c3c');
// Button raises less than 2.5x BB - should not count as steal
applyAction(table, 'p1 cbr 400');
expect(Stats.forPlayerStreet(table, 0, 'preflop')).toMatchObject({
stealIpOpportunities: 1,
stealIpAttempts: 0,
});
// Complete the hand
applyAction(table, 'p2 f');
applyAction(table, 'p3 f');
});
it('should track steal attempts', () => {
// This test verifies that raises from the button meeting the size criteria (2.5x-4x BB)
// are correctly identified and tracked as in-position steal attempts.
const table = Game({
...sampleGame,
players: ['BTN', 'SB', 'BB'],
startingStacks: [1000, 1000, 1000],
blindsOrStraddles: [0, 10, 20],
});
applyAction(table, 'd dh p1 AhKh');
applyAction(table, 'd dh p2 QhJh');
applyAction(table, 'd dh p3 2c3c');
// Button raises exactly 2.5x BB - should count as steal
applyAction(table, 'p1 cbr 50'); // 20 BB * 2.5 = 50
expect(Stats.forPlayerStreet(table, 0, 'preflop')).toMatchObject({
stealIpOpportunities: 1,
stealIpAttempts: 1,
});
// New hand with larger steal attempt
const table2 = Game({
...sampleGame,
players: ['BTN', 'SB', 'BB'],
startingStacks: [1000, 1000, 1000],
blindsOrStraddles: [0, 10, 20],
});
applyAction(table2, 'd dh p1 AhKh');
applyAction(table2, 'd dh p2 QhJh');
applyAction(table2, 'd dh p3 2c3c');
// Button raises 3x BB - should count as steal
applyAction(table2, 'p1 cbr 60');
expect(Stats.forPlayerStreet(table2, 0, 'preflop')).toMatchObject({
stealIpAttempts: 1,
stealIpOpportunities: 1,
});
});
it('should track steal OOP from SB and IP defense from BB', () => {
// This scenario tests an out-of-position steal attempt from the Small Blind after the Button folds.
// It also verifies that the Big Blind's call is tracked as an in-position defense against the steal.
const table = Game({
...sampleGame,
players: ['BTN', 'SB', 'BB'],
startingStacks: [1000, 1000, 1000],
blindsOrStraddles: [0, 10, 20],
});
applyAction(table, 'd dh p1 AhKh'); // BTN
applyAction(table, 'd dh p2 QhJh'); // SB
applyAction(table, 'd dh p3 2c3c'); // BB
applyAction(table, 'p1 f'); // BTN folds
// SB has opportunity to steal from OOP
expect(Stats.forPlayerStreet(table, 1, 'preflop')).toMatchObject({
stealOopOpportunities: 1,
stealIpOpportunities: 0,
});
// SB raises -> steal attempt from OOP
applyAction(table, 'p2 cbr 60');
expect(Stats.forPlayerStreet(table, 1, 'preflop')).toMatchObject({
stealOopOpportunities: 1,
stealOopAttempts: 1,
stealIpAttempts: 0,
});
// BB defends IP by calling
applyAction(table, 'p3 cc');
expect(Stats.forPlayerStreet(table, 2, 'preflop')).toMatchObject({
stealIpChallenges: 1,
stealIpContinues: 1,
stealOopChallenges: 0,
});
});
it('should track steal IP from CO and OOP defense from BB', () => {
// This test covers an in-position steal attempt from the Cutoff.
// It then checks the Big Blind's defense out-of-position, which in this case is a fold.
const table = Game({
...sampleGame,
players: ['CO', 'BTN', 'SB', 'BB'],
startingStacks: [1000, 1000, 1000, 1000],
blindsOrStraddles: [0, 0, 10, 20],
});
applyAction(table, 'd dh p1 AhKh'); // CO
applyAction(table, 'd dh p2 QhJh'); // BTN
applyAction(table, 'd dh p3 2c3c'); // SB
applyAction(table, 'd dh p4 7d8d'); // BB
// CO raises, BTN folds. This is an IP steal attempt.
applyAction(table, 'p1 cbr 60');
applyAction(table, 'p2 f');
expect(Stats.forPlayerStreet(table, 0, 'preflop')).toMatchObject({
stealIpOpportunities: 1,
stealIpAttempts: 1,
stealOopAttempts: 0,
});
// SB folds, BB defends OOP by folding
applyAction(table, 'p3 f');
applyAction(table, 'p4 f');
expect(Stats.forPlayerStreet(table, 3, 'preflop')).toMatchObject({
stealOopChallenges: 1,
stealOopFolds: 1,
stealIpChallenges: 0,
});
});
it('should track OOP defense when blinds fold to a BTN steal', () => {
// This scenario verifies that when both blinds are faced with an in-position steal from the Button,
// their folds are correctly recorded as out-of-position defense folds.
const table = Game({
...sampleGame,
players: ['BTN', 'SB', 'BB'],
startingStacks: [1000, 1000, 1000],
blindsOrStraddles: [0, 10, 20],
});
applyAction(table, 'd dh p1 AhKh');
applyAction(table, 'd dh p2 QhJh');
applyAction(table, 'd dh p3 2c3c');
// BTN raises (IP steal)
applyAction(table, 'p1 cbr 60');
// SB folds (OOP defense)
applyAction(table, 'p2 f');
expect(Stats.forPlayerStreet(table, 1, 'preflop')).toMatchObject({
stealOopChallenges: 1,
stealOopFolds: 1,
stealIpChallenges: 0,
});
// BB folds (OOP defense)
applyAction(table, 'p3 f');
expect(Stats.forPlayerStreet(table, 2, 'preflop')).toMatchObject({
stealOopChallenges: 1,
stealOopFolds: 1,
stealIpChallenges: 0,
});
});
});
describe('threeBet', () => {
it('should count 3bet opportunities with sufficient stack', () => {
// This test case follows a pre-flop betting sequence to verify 3-bet and 4-bet opportunities.
// It tracks an OOP 3-bet from the SB and the subsequent folds from the BB and the original raiser (BTN).
const table = Game({
...sampleGame,
players: ['BTN', 'SB', 'BB'],
startingStacks: [1000, 1000, 1000],
blindsOrStraddles: [0, 10, 20],
});
applyAction(table, 'd dh p1 AhKh');
applyAction(table, 'd dh p2 QhJh');
applyAction(table, 'd dh p3 2c3c');
// Position after BB is BTN
applyAction(table, 'p1 cbr 60');
expect(Stats.forPlayerStreet(table, 0, 'preflop')).toMatchObject({
raises: 1,
voluntaryPutMoneyInPotTimes: 1,
stealIpAttempts: 1,
stealIpOpportunities: 1,
firstAggressions: 1,
});
// Then SB
applyAction(table, 'p2 cbr 180');
expect(Stats.forPlayerStreet(table, 1, 'preflop')).toMatchObject({
raises: 1,
voluntaryPutMoneyInPotTimes: 1,
stealIpOpportunities: 0,
stealOopOpportunities: 0,
threeBetOopOpportunities: 1,
threeBetOopAttempts: 1,
firstAggressions: 0,
lastAggressions: 1,
});
// BB folds
applyAction(table, 'p3 f');
expect(Stats.forPlayerStreet(table, 2, 'preflop')).toMatchObject({
folds: 1,
stealIpOpportunities: 0,
stealOopOpportunities: 0,
fourBetIpOpportunities: 1,
fourBetIpAttempts: 0,
threeBetIpOpportunities: 0,
threeBetOopOpportunities: 0,
threeBetIpAttempts: 0,
threeBetOopAttempts: 0,
threeBetIpFolds: 1,
threeBetIpChallenges: 1,
threeBetOopFolds: 0,
threeBetOopChallenges: 0,
firstAggressions: 0,
lastAggressions: 0,
});
// BTN folds
applyAction(table, 'p1 f');
expect(Stats.forPlayerStreet(table, 0, 'preflop')).toMatchObject({
raises: 1,
folds: 1,
voluntaryPutMoneyInPotTimes: 1,
stealIpAttempts: 1,
stealIpOpportunities: 1,
threeBetIpOpportunities: 0,
threeBetOopOpportunities: 0,
threeBetIpAttempts: 0,
threeBetOopAttempts: 0,
threeBetIpFolds: 1,
threeBetIpChallenges: 1,
firstAggressions: 1,
lastAggressions: 0,
});
});
it('should track 3-bet IP and defense OOP', () => {
// This scenario tests an in-position 3-bet from the Button against an open from the Cutoff.
// It then verifies the Cutoff's out-of-position defense when they call the 3-bet.
const table = Game({
...sampleGame,
players: ['UTG', 'CO', 'BTN', 'SB', 'BB'],
blindsOrStraddles: [0, 0, 0, 10, 20],
startingStacks: [100, 100, 100, 100, 100],
});
applyAction(table, 'd dh p1 2h2c');
applyAction(table, 'd dh p2 3h3c');
applyAction(table, 'd dh p3 4h4c');
applyAction(table, 'd dh p4 5h5c');
applyAction(table, 'd dh p5 6h6c');
applyAction(table, 'p1 f');
applyAction(table, 'p2 cbr 60'); // CO opens
applyAction(table, 'p3 cbr 180'); // BTN 3-bets in position
applyAction(table, 'p4 f'); // SB folds
applyAction(table, 'p5 f'); // BB folds
applyAction(table, 'p2 cc'); // CO calls, defending OOP
const btnStats = Stats.forPlayerStreet(table, 2, 'preflop');
expect(btnStats).toMatchObject({
threeBetIpAttempts: 1,
threeBetIpOpportunities: 1,
aggressionsInPosition: 1,
});
const coStats = Stats.forPlayerStreet(table, 1, 'preflop');
expect(coStats).toMatchObject({
threeBetOopChallenges: 1,
threeBetOopContinues: 1,
challengesInPosition: 0,
});
});
it('should track 3-bet OOP and defense IP', () => {
// This test covers an out-of-position 3-bet from the Small Blind against a Button open.
// It then verifies the Button's in-position defense when they call the 3-bet.
const table = Game({
...sampleGame,
players: ['BTN', 'SB', 'BB'],
blindsOrStraddles: [0, 10, 20],
startingStacks: [100, 100, 100],
});
applyAction(table, 'd dh p1 2h2c');
applyAction(table, 'd dh p2 3h3c');
applyAction(table, 'd dh p3 4h4c');
applyAction(table, 'p1 cbr 60'); // BTN opens
applyAction(table, 'p2 cbr 180'); // SB 3-bets out of position
applyAction(table, 'p3 f'); // BB folds
applyAction(table, 'p1 cc'); // BTN calls, defending IP
const sbStats = Stats.forPlayerStreet(table, 1, 'preflop');
expect(sbStats).toMatchObject({
threeBetOopAttempts: 1,
threeBetOopOpportunities: 1,
aggressionsInPosition: 0,
});
const btnStats = Stats.forPlayerStreet(table, 0, 'preflop');
expect(btnStats).toMatchObject({
threeBetIpChallenges: 1,
threeBetIpContinues: 1,
challengesInPosition: 1,
});
});
});
describe('squeeze', () => {
it('marks squeeze on 3bet after BTN open + SB call', () => {
// This test case verifies a squeeze play from the Big Blind.
// A squeeze is a 3-bet made after an initial raise and at least one call.
// It also tracks the subsequent defensive actions from the original raiser and the caller.
const table = Game({
...sampleGame,
players: ['BTN', 'SB', 'BB'],
startingStacks: [1000, 1000, 1000],
blindsOrStraddles: [0, 10, 20], // BTN = dealer, SB = 10, BB = 20
});
applyAction(table, 'd dh p1 AhKh'); // BTN
applyAction(table, 'd dh p2 QhJh'); // SB
applyAction(table, 'd dh p3 2c3c'); // BB
// BTN opens
applyAction(table, 'p1 cbr 60');
// SB calls (creates squeeze opportunity for BB)
applyAction(table, 'p2 cc');
// BB 3bets -> squeeze
applyAction(table, 'p3 cbr 220');
expect(Stats.forPlayerStreet(table, 2, 'preflop')).toMatchObject({
threeBetIpAttempts: 0,
squeezeIpOpportunities: 0,
squeezeIpAttempts: 0,
squeezeOopOpportunities: 1,
squeezeOopAttempts: 1,
threeBetOopAttempts: 1,
});
// BTN folds facing squeeze
applyAction(table, 'p1 f');
expect(Stats.forPlayerStreet(table, 0, 'preflop')).toMatchObject({
squeezeIpFolds: 1,
squeezeIpChallenges: 1,
});
// SB defends by calling
applyAction(table, 'p2 cc');
expect(Stats.forPlayerStreet(table, 1, 'preflop')).toMatchObject({
squeezeOopContinues: 1,
squeezeOopChallenges: 1,
});
});
it('does not mark squeeze when no caller before 3bet', () => {
// This test ensures that a standard 3-bet (a raise followed by a re-raise with no callers in between)
// is not incorrectly identified as a squeeze play.
const table = Game(sampleGame);
applyAction(table, 'd dh p1 AhKh');
applyAction(table, 'd dh p2 QhJh');
applyAction(table, 'd dh p3 2c3c');
// Open raise then immediate 3bet (no call in between)
applyAction(table, 'p1 cbr 60');
applyAction(table, 'p2 cbr 200');
expect(Stats.forPlayerStreet(table, 1, 'preflop')).toMatchObject({
threeBetOopAttempts: 1,
squeezeIpOpportunities: 0,
squeezeOopOpportunities: 0,
squeezeIpAttempts: 0,
squeezeOopAttempts: 0,
});
});
it('should track squeeze IP and defense OOP', () => {
// This test covers an in-position squeeze from the Button after an open from MP and a call from the CO.
// It then tracks the out-of-position defensive actions from both the original raiser and the caller.
const table = Game({
...sampleGame,
players: ['MP', 'CO', 'BTN', 'SB', 'BB'],
blindsOrStraddles: [0, 0, 0, 10, 20],
startingStacks: [100, 100, 100, 100, 100],
});
applyAction(table, 'd dh p1 2h2c');
applyAction(table, 'd dh p2 3h3c');
applyAction(table, 'd dh p3 4h4c');
applyAction(table, 'd dh p4 5h5c');
applyAction(table, 'd dh p5 6h6c');
applyAction(table, 'p1 cbr 60'); // MP opens
applyAction(table, 'p2 cc'); // CO calls
applyAction(table, 'p3 cbr 300'); // BTN squeezes IP
applyAction(table, 'p4 f');
applyAction(table, 'p5 f');
applyAction(table, 'p1 f'); // MP folds
applyAction(table, 'p2 cc'); // CO calls
const btnStats = Stats.forPlayerStreet(table, 2, 'preflop');
expect(btnStats).toMatchObject({
squeezeIpAttempts: 1,
squeezeIpOpportunities: 1,
threeBetIpAttempts: 1,
aggressionsInPosition: 1,
});
const mpStats = Stats.forPlayerStreet(table, 0, 'preflop');
expect(mpStats).toMatchObject({
squeezeOopChallenges: 1,
squeezeOopFolds: 1,
});
const coStats = Stats.forPlayerStreet(table, 1, 'preflop');
expect(coStats).toMatchObject({
squeezeOopChallenges: 1,
squeezeOopContinues: 1,
squeezeOopFolds: 0,
});
});
it('should track squeeze OOP and defense IP', () => {
// SCENARIO: OOP squeeze from BB after BTN open and SB call
// INPUT: 6 players, BB raises over BTN open and SB flat
// EXPECTED: Track squeeze and defensive folds from BTN and SB
const table = Game({
...sampleGame,
players: ['UTG', 'MP', 'CO', 'BTN', 'SB', 'BB'],
blindsOrStraddles: [0, 0, 0, 0, 10, 20],
startingStacks: [100, 100, 100, 100, 100, 100],
});
applyAction(table, 'd dh p1 2h2c');
applyAction(table, 'd dh p2 3h3c');
applyAction(table, 'd dh p3 4h4c');
applyAction(table, 'd dh p4 5h5c');
applyAction(table, 'd dh p5 6h6c');
applyAction(table, 'd dh p6 7h7c');
applyAction(table, 'p1 f');
applyAction(table, 'p2 f');
applyAction(table, 'p3 f');
applyAction(table, 'p4 cbr 60'); // BTN raises
applyAction(table, 'p5 cc'); // SB calls
applyAction(table, 'p6 cbr 300'); // BB squeezes OOP
applyAction(table, 'p4 f'); // BTN folds
applyAction(table, 'p5 f'); // SB folds
const bbStats = Stats.forPlayerStreet(table, 5, 'preflop');
expect(bbStats).toMatchObject({
squeezeOopAttempts: 1,
squeezeOopOpportunities: 1,
threeBetOopAttempts: 1,
aggressionsInPosition: 0,
});
const btnStats = Stats.forPlayerStreet(table, 3, 'preflop');
expect(btnStats).toMatchObject({
squeezeIpChallenges: 1,
squeezeIpFolds: 1,
});
const sbStats = Stats.forPlayerStreet(table, 4, 'preflop');
expect(sbStats).toMatchObject({
squeezeOopChallenges: 1,
squeezeOopFolds: 1,
});
});
});
describe('fourBet', () => {
it('should count 4bet opportunities', () => {
// This test verifies the tracking of 4-bet opportunities and actions.
// It simulates a scenario where BTN opens, SB 3-bets, BB folds (facing a 3-bet), and BTN 4-bets.
const table = Game({
...sampleGame,
players: ['BTN', 'SB', 'BB'],
startingStacks: [35, 1000, 1000].map(x => x * 20), // BTN has only 35BB
blindsOrStraddles: [0, 10, 20],
});
applyAction(table, 'd dh p1 AhKh');
applyAction(table, 'd dh p2 QhJh');
applyAction(table, 'd dh p3 2c3c');
// BTN raises first
applyAction(table, 'p1 cbr 60');
// SB 3bets
applyAction(table, 'p2 cbr 180');
expect(Stats.forPlayerStreet(table, 1, 'preflop')).toMatchObject({
threeBetOopOpportunities: 1,
threeBetOopAttempts: 1,
});
// BB has turn
applyAction(table, 'p3 f');
expect(Stats.forPlayerStreet(table, 2, 'preflop')).toMatchObject({
threeBetIpOpportunities: 0,
threeBetOopOpportunities: 0,
threeBetIpAttempts: 0,
threeBetOopAttempts: 0,
threeBetIpFolds: 1,
threeBetIpChallenges: 1,
fourBetIpOpportunities: 1,
fourBetIpAttempts: 0,
});
expect(Stats.forPlayerStreet(table, 0, 'preflop')).toMatchObject({
fourBetIpOpportunities: 1,
fourBetIpAttempts: 0,
threeBetIpChallenges: 1,
threeBetIpFolds: 0,
threeBetIpOpportunities: 0,
threeBetOopOpportunities: 0,
});
// BTN 4bets - SB should not have 4bet opportunity due to small stack
applyAction(table, 'p1 cbr 540');
expect(Stats.forPlayerStreet(table, 0, 'preflop')).toMatchObject({
fourBetIpOpportunities: 1,
fourBetIpAttempts: 1,
threeBetIpChallenges: 1,
threeBetIpContinues: 1,
threeBetIpFolds: 0,
lastAggressions: 1,
firstAggressions: 1,
});
expect(Stats.forPlayerStreet(table, 1, 'preflop')).toMatchObject({
fourBetIpOpportunities: 0,
fourBetOopOpportunities: 0,
fourBetIpAttempts: 0,
fourBetOopAttempts: 0,
threeBetIpChallenges: 0, // SB did 3bet, not faced with fold to three bet
threeBetOopChallenges: 0,
threeBetIpFolds: 0,
threeBetOopFolds: 0,
lastAggressions: 0,
firstAggressions: 0,
});
// Complete the hand
applyAction(table, 'p2 f');
expect(Stats.forPlayerStreet(table, 1, 'preflop')).toMatchObject({
fourBetIpOpportunities: 0,
fourBetOopOpportunities: 0,
fourBetIpAttempts: 0,
fourBetOopAttempts: 0,
threeBetIpFolds: 0,
threeBetOopFolds: 0,
threeBetIpChallenges: 0,
threeBetOopChallenges: 0,
fourBetOopFolds: 1,
fourBetOopChallenges: 1,
});
});
it('should track 4-bet OOP and defense IP', () => {
// This scenario tracks an out-of-position 4-bet from UTG against a 3-bet from MP.
// It then verifies the in-position defense from MP when they call the 4-bet.
const table = Game({
...sampleGame,
players: ['UTG', 'MP', 'CO', 'BTN', 'SB', 'BB'],
blindsOrStraddles: [0, 0, 0, 0, 10, 20],
startingStacks: [1000, 1000, 1000, 1000, 1000, 1000],
});
applyAction(table, 'd dh p1 2h2c');
applyAction(table, 'd dh p2 3h3c');
applyAction(table, 'd dh p3 4h4c');
applyAction(table, 'd dh p4 5h5c');
applyAction(table, 'd dh p5 6h6c');
applyAction(table, 'd dh p6 7h7c');
applyAction(table, 'p1 cbr 60'); // UTG opens
applyAction(table, 'p2 cbr 180'); // MP 3-bets
applyAction(table, 'p3 f'); // CO folds
applyAction(table, 'p4 f'); // BTN folds
applyAction(table, 'p5 f'); // SB folds
applyAction(table, 'p6 f'); // BB folds
applyAction(table, 'p1 cbr 540'); // UTG 4-bets OOP
applyAction(table, 'p2 cc'); // MP calls, defending IP
const utgStats = Stats.forPlayerStreet(table, 0, 'preflop');
expect(utgStats).toMatchObject({
fourBetOopAttempts: 1,
fourBetOopOpportunities: 1,
aggressionsInPosition: 0,
});
const mpStats = Stats.forPlayerStreet(table, 1, 'preflop');
expect(mpStats).toMatchObject({
fourBetIpChallenges: 1,
fourBetIpContinues: 1,
challengesInPosition: 1,
});
});
it('should track 4-bet IP and defense OOP', () => {
// This test covers an in-position 4-bet from the Button against a 3-bet from the Small Blind.
// It then verifies the Small Blind's out-of-position defense when they call the 4-bet.
const table = Game({
...sampleGame,
players: ['BTN', 'SB', 'BB'],
blindsOrStraddles: [0, 10, 20],
startingStacks: [1000, 1000, 1000],
});
applyAction(table, 'd dh p1 2h2c');
applyAction(table, 'd dh p2 3h3c');
applyAction(table, 'd dh p3 4h4c');
applyAction(table, 'p1 cbr 60'); // BTN opens
applyAction(table, 'p2 cbr 180'); // SB 3-bets OOP
applyAction(table, 'p3 f'); // BB folds
applyAction(table, 'p1 cbr 540'); // BTN 4-bets IP
applyAction(table, 'p2 cc'); // SB calls, defending OOP
const btnStats = Stats.forPlayerStreet(table, 0, 'preflop');
expect(btnStats).toMatchObject({
fourBetIpAttempts: 1,
fourBetIpOpportunities: 1,
aggressionsInPosition: 1,
});
const sbStats = Stats.forPlayerStreet(table, 1, 'preflop');
expect(sbStats).toMatchObject({
fourBetOopChallenges: 1,
fourBetOopContinues: 1,
challengesInPosition: 0,
});
});
});
describe('cbet', () => {
it('should count cbet opportunities', () => {
// This test verifies the tracking of continuation bet (c-bet) opportunities and actions.
// It simulates a scenario where the pre-flop 3-bettor makes a c-bet on the flop.
const table = Game(sampleGame);
applyAction(table, 'd dh p1 AhKh');
applyAction(table, 'd dh p2 QhJh');
applyAction(table, 'd dh p3 2c3c');
// Preflop betting
applyAction(table, 'p1 cbr 60'); // First aggressor
expect(Stats.forPlayerStreet(table, 0, 'preflop')).toMatchObject({
firstAggressions: 1,
lastAggressions: 1,
});
applyAction(table, 'p2 cbr 180'); // Last aggressor
expect(Stats.forPlayerStreet(table, 1, 'preflop')).toMatchObject({
lastAggressions: 1,
threeBetOopAttempts: 1,
});
applyAction(table, 'p3 f');
expect(Stats.forPlayerStreet(table, 2, 'preflop')).toMatchObject({
threeBetIpFolds: 1,
cbetIpOpportunities: 0,
cbetOopOpportunities: 0,
cbetIpChallenges: 0,
cbetOopChallenges: 0,
threeBetIpChallenges: 1,
});
applyAction(table, 'p1 cc');
expect(Stats.forPlayerStreet(table, 0, 'preflop')).toMatchObject({
cbetIpChallenges: 0,
cbetOopChallenges: 0,
threeBetIpChallenges: 1,
});
// Flop
applyAction(table, 'd db AcKcQc');
applyAction(table, 'p2 cbr 100'); // Previous street aggressor bets
expect(Stats.forPlayerStreet(table, 1, 'flop')).toMatchObject({
cbetOopAttempts: 1,
cbetOopOpportunities: 1,
lastAggressions: 1,
});
applyAction(table, 'p1 f');
expect(Stats.forPlayerStreet(table, 0, 'flop')).toMatchObject({
cbetIpFolds: 1,
cbetIpChallenges: 1,
cbetOopOpportunities: 0,
cbetIpOpportunities: 0,
});
});
it('should not count cbet opportunities for non-first aggressors', () => {
// This test ensures that a player who was not the pre-flop aggressor does not get a c-bet opportunity.
// A player who just called the pre-flop raise leads out on the flop, which is not a c-bet.
const table = Game(sampleGame);
applyAction(table, 'd dh p1 AhKh');
applyAction(table, 'd dh p2 QhJh');
applyAction(table, 'd dh p3 2c3c');
// Preflop betting
applyAction(table, 'p1 cbr 60'); // First aggressor
applyAction(table, 'p2 cc');
applyAction(table, 'p3 cc');
// Flop - p2 bets first, so p1 (preflop aggressor) should not have cbet opportunity
applyAction(table, 'd db AcKcQc');
applyAction(table, 'p2 cbr 100');
expect(Stats.forPlayerStreet(table, 1, 'flop')).toMatchObject({
bets: 1,
cbetIpOpportunities: 0,
cbetOopOpportunities: 0,
});
applyAction(table, 'p3 f');
applyAction(table, 'p1 cc');
expect(Stats.forPlayerStreet(table, 0, 'flop')).toMatchObject({
calls: 1,
cbetIpOpportunities: 0,
cbetOopOpportunities: 0,
});
});
it('should track c-bet OOP and defense IP', () => {
// This scenario tracks an out-of-position continuation bet from the pre-flop aggressor in MP.
// It also verifies the in-position defensive call from the Button.
const table = Game({
...sampleGame,
players: ['MP', 'CO', 'BTN', 'SB', 'BB'],
blindsOrStraddles: [0, 0, 0, 10, 20],
startingStacks: [100, 100, 100, 100, 100],
});
applyAction(table, 'd dh p1 2h2c');
applyAction(table, 'd dh p2 3h3c');
applyAction(table, 'd dh p3 4h4c');
applyAction(table, 'd dh p4 5h5c');
applyAction(table, 'd dh p5 6h6c');
applyAction(table, 'p1 cbr 60'); // MP opens
applyAction(table, 'p2 f'); // CO folds
applyAction(table, 'p3 cc'); // BTN calls
applyAction(table, 'p4 f'); // SB folds
applyAction(table, 'p5 cc'); // BB calls
applyAction(table, 'd db AcKcQc');
applyAction(table, 'p5 cc'); // BB checks first on flop
applyAction(table, 'p1 cbr 100'); // MP c-bets OOP
applyAction(table, 'p3 cc'); // BTN calls IP
applyAction(table, 'p5 f'); // BB folds
const mpStats = Stats.forPlayerStreet(table, 0, 'flop');
expect(mpStats).toMatchObject({
cbetOopAttempts: 1,
cbetOopOpportunities: 1,
aggressionsInPosition: 0,
});
const btnStats = Stats.forPlayerStreet(table, 2, 'flop');
expect(btnStats).toMatchObject({
cbetIpChallenges: 1,
cbetIpContinues: 1,
challengesInPosition: 1,
});
});
it('should track c-bet IP and defense OOP', () => {
// This test covers an in-position continuation bet from the Button as the pre-flop aggressor.
// It then verifies the out-of-position defensive call from the Big Blind.
const table = Game({
...sampleGame,
players: ['BTN', 'BB'],
blindsOrStraddles: [10, 20],
startingStacks: [100, 100],
});
applyAction(table, 'd dh p1 2h2c');
applyAction(table, 'd dh p2 3h3c');
applyAction(table, 'p1 cbr 60'); // BTN opens
applyAction(table, 'p2 cc'); // BB calls
applyAction(table, 'd db AcKcQc');
applyAction(table, 'p2 cc'); // BB checks
applyAction(table, 'p1 cbr 100'); // BTN c-bets
applyAction(table, 'p2 cc'); // BB calls OOP
const btnStats = Stats.forPlayerStreet(table, 0, 'flop');
expect(btnStats).toMatchObject({
cbetIpAttempts: 1,
cbetIpOpportunities: 1,
aggressionsInPosition: 1,
});
const bbStats = Stats.forPlayerStreet(table, 1, 'flop');
expect(bbStats).toMatchObject({
cbetOopChallenges: 1,
cbetOopContinues: 1,
challengesInPosition: 0,
});
});
});
describe('decision making stats', () => {
test('should track multiway all in', () => {
// This test case simulates a multi-way all-in situation to ensure that financial calculations and showdown stats are handled correctly.
// It involves complex betting sequences across multiple streets leading to a showdown.
const sampleGame: Hand = {
variant: 'NT',
players: ['Alice', 'Bob', 'Carol'],
startingStacks: [1000, 1000, 1000],
blindsOrStraddles: [0, 10, 20],
antes: [0, 0, 0],
actions: [],
minBet: 20,
seed: 12345,
};
const table = Game(sampleGame);
// Deal hole cards
applyAction(table, 'd dh p1 AhKh');
expect(Stats.forPlayerStreet(table, 0, 'preflop')).toMatchObject({
limpOpportunities: 0,
});
applyAction(table, 'd dh p2 QhJh');
applyAction(table, 'd dh p3 2c3c');
// Preflop decisions
applyAction(table, 'p1 cbr 60');
expect(Stats.forPlayerStreet(table, 0, 'preflop')).toMatchObject({
bets: 0,
raises: 1,
voluntaryPutMoneyInPotTimes: 1,
limpOpportunities: 1,
});
applyAction(table, 'p2 cc');
expect(Stats.forPlayerStreet(table, 1, 'preflop')).toMatchObject({
calls: 1,
voluntaryPutMoneyInPotTimes: 1,
limpOpportunities: 0,
});
applyAction(table, 'p3 f');
expect(Stats.forPlayerStreet(table, 2, 'preflop')).toMatchObject({
folds: 1,
limpOpportunities: 0,
});
// Flop
applyAction(table, 'd db AcKcQc');
applyAction(table, 'p2 cbr 100'); // First bet
expect(Stats.forPlayerStreet(table, 1, 'flop')).toMatchObject({
bets: 1,
limpOpportunities: 0,
});
applyAction(table, 'p1 cbr 300'); // Second bet (would be three bet)
expect(Stats.forPlayerStreet(table, 0, 'flop')).toMatchObject({
raises: 1,
limpOpportunities: 0,
threeBetIpOpportunities: 0,
threeBetOopOpportunities: 0,
threeBetIpAttempts: 0,
threeBetOopAttempts: 0,
});
applyAction(table, 'p2 cbr 900'); // Third bet (four-bet)
expect(Stats.forPlayerStreet(table, 1, 'flop')).toMatchObject({
raises: 1,
limpOpportunities: 0,
});
applyAction(table, 'p1 cc'); // Call the three-bet
expect(Stats.forPlayerStreet(table, 0, 'flop')).toMatchObject({
calls: 1,
limpOpportunities: 0,
});
// Turn
applyAction(table, 'd db Kd');
applyAction(table, 'p2 cbr 100');
expect(Stats.forPlayerStreet(table, 1, 'turn')).toMatchObject({
bets: 1,
limpOpportunities: 0,
});
applyAction(table, 'p1 cc');
expect(Stats.forPlayerStreet(table, 0, 'turn')).toMatchObject({
calls: 1,
limpOpportunities: 0,
});
// River
applyAction(table, 'd db 2d');
// Showdown - both players show cards, multi way all in
applyAction(table, 'p2 sm QhJh');
applyAction(table, 'p1 sm AhKh');
// Final stats check for all players
expect(Stats.forPlayerStreet(table, 0, 'turn')).toMatchObject({
calls: 1,
limpOpportunities: 0,
profits: 1020,
winnings: 2020, // Won the pot
losses: 0,
returns: 0,
});
expect(Stats.forPlayerStreet(table, 1, 'turn')).toMatchObject({
bets: 1,
limpOpportunities: 0,
winnings: 0,
losses: 1000, // Lost their bets
returns: 0,
});
});
test('should track decision making stats', () => {
// This is a comprehensive test that tracks various decision-making statistics throughout a hand.
// It includes raises, calls, and bets across all streets to verify the accuracy of the stats engine.
const table = Game(sampleGame);
// Deal hole cards
applyAction(table, 'd dh p1 AhKh');
expect(Stats.forPlayerStreet(table, 0, 'preflop')).toMatchObject({
limpOpportunities: 0,
});
applyAction(table, 'd dh p2 QhJh');
applyAction(table, 'd dh p3 2c3c');
// Preflop decisions
applyAction(table, 'p1 cbr 60');
expect(Stats.forPlayerStreet(table, 0, 'preflop')).toMatchObject({
raises: 1,
voluntaryPutMoneyInPotTimes: 1,
limpOpportunities: 1,
});
applyAction(table, 'p2 cc');
expect(Stats.forPlayerStreet(table, 1, 'preflop')).toMatchObject({
calls: 1,
voluntaryPutMoneyInPotTimes: 1,
limpOpportunities: 0,
});
applyAction(table, 'p3 f');
expect(Stats.forPlayerStreet(table, 2, 'preflop')).toMatchObject({
folds: 1,
limpOpportunities: 0,
});
// Flop
applyAction(table, 'd db AcKcQc');
applyAction(table, 'p2 cbr 100'); // First bet
expect(Stats.forPlayerStreet(table, 1, 'flop')).toMatchObject({
bets: 1,
limpOpportunities: 0,
cbetIpAttempts: 0,
cbetOopAttempts: 0,
cbetIpOpportunities: 0,
cbetOopOpportunities: 0,
});
applyAction(table, 'p1 cbr 300'); // Second bet (raise)
expect(Stats.forPlayerStreet(table, 0, 'flop')).toMatchObject({
raises: 1,
limpOpportunities: 0,
});
applyAction(table, 'p2 cbr 500'); // Third bet (re-raise)
expect(Stats.forPlayerStreet(table, 1, 'flop')).toMatchObject({
raises: 1,
limpOpportunities: 0,
});
applyAction(table, 'p1 cc'); // Call the re-raise
expect(Stats.forPlayerStreet(table, 0, 'flop')).toMatchObject({
calls: 1,
limpOpportunities: 0,
});
// Turn
applyAction(table, 'd db Kd');
applyAction(table, 'p2 cbr 50');
expect(Stats.forPlayerStreet(table, 1, 'turn')).toMatchObject({
bets: 1,
limpOpportunities: 0,
});
applyAction(table, 'p1 cc');
expect(Stats.forPlayerStreet(table, 0, 'turn')).toMatchObject({
calls: 1,
limpOpportunities: 0,
});
// River
applyAction(table, 'd db 2d');
applyAction(table, 'p2 cbr 50');
expect(Stats.forPlayerStreet(table, 1, 'river')).toMatchObject({
bets: 1,
limpOpportunities: 0,
cbetOopAttempts: 0,
cbetOopOpportunities: 0,
});
applyAction(table, 'p1 cc');
expect(Stats.forPlayerStreet(table, 0, 'river')).toMatchObject({
calls: 1,
limpOpportunities: 0,
});
// Showdown - both players show cards
applyAction(table, 'p2 sm QhJh');
applyAction(table, 'p1 sm AhKh');
// Final stats check for all players
expect(Stats.forPlayerStreet(table, 0, 'river')).toMatchObject({
calls: 1,
limpOpportunities: 0,
winnings: 1340,
investments: 50,
returns: 0,
profits: 680, // Won the pot
losses: 0,
stackBefore: 390,
stackAfter: 1680,
balance: 1290,
});
expect(Stats.forPlayerStreet(table, 1, 'river')).toMatchObject({
bets: 1,
limpOpportunities: 0,
winnings: 0,
investments: 50,
losses: 660, // Lost their bets
stackBefore: 390,
stackAfter: 340,
balance: -50,
returns: 0,
});
expect(Stats.forPlayerStreet(table, 2, 'preflop')).toMatchObject({
limpOpportunities: 0,
winnings: 0,
losses: 20,
});
});
});
test('should track optional stats when available', () => {
// This test ensures that optional statistics, like 'allIns', are tracked correctly when the situation arises.
// It simulates a pre-flop all-in to verify that the 'allIns' counter is incremented.
const table = Game(sampleGame);
// Deal hole cards
applyAction(table, 'd dh p1 AhKh');
expect(Stats.forPlayerStreet(table, 0, 'preflop')).toMatchObject({
limpOpportunities: 0,
});
applyAction(table, 'd dh p2 QhJh');
applyAction(table, 'd dh p3 2c3c');
// Preflop all-in
applyAction(table, 'p1 cbr 1000');
expect(Stats.forPlayerStreet(table, 0, 'preflop')).toMatchObject({
raises: 1,
voluntaryPutMoneyInPotTimes: 1,
allIns: 1,
limpOpportunities: 1,
});
});
describe('limp', () => {
test('should track limp opportunities correctly', () => {
// This test verifies that limp opportunities are correctly identified.
// A player has a limp opportunity if no one has raised yet pre-flop.
const table = Game(sampleGame);
applyAction(table, 'd dh p1 AhKh');
applyAction(table, 'd dh p2 QhJh');
applyAction(table, 'd dh p3 2c3c');
// First player raises - had opportunity to limp but chose to raise
applyAction(table, 'p1 cbr 60');
expect(Stats.forPlayerStreet(table, 0, 'preflop')).toMatchObject({
calls: 0,
raises: 1,
bets: 0,
voluntaryPutMoneyInPotTimes: 1,
limps: 0,
limpOpportunities: 1, // Had opportunity to limp but chose to raise
})