@idealic/poker-engine
Version:
Poker game engine and hand evaluator
1,198 lines (1,078 loc) • 47.6 kB
text/typescript
import { describe, expect, it } from 'vitest';
import { Hand } from '../../..';
import { Game } from '../../../Game';
import { Stats } from '../../../Stats';
import { applyAction } from '../../../game/progress';
// A sample game definition to be used as a base for tests.
const sampleGame: Hand = {
variant: 'NT',
players: [],
startingStacks: [],
blindsOrStraddles: [],
antes: [],
actions: [],
minBet: 20,
seed: 12345,
};
describe('Positional & Maneuver Stat Tracking', () => {
describe('Fundamentals', () => {
it('1) should correctly determine IP/OOP order heads-up (SB IP, BB OOP)', () => {
// Setup: Heads-up game with SB and BB.
// Action: SB opens, BB calls.
// Assert: On the flop, confirm that SB is correctly identified as In Position (IP)
// and BB is Out of Position (OOP) for any relevant stat opportunities (e.g., c-bet).
const table = Game({
...sampleGame,
players: ['SB', 'BB'],
blindsOrStraddles: [10, 20],
startingStacks: [1000, 1000],
});
applyAction(table, 'd dh p1 2h2c');
applyAction(table, 'd dh p2 3h3c');
applyAction(table, 'p1 cbr 60');
applyAction(table, 'p2 cc');
applyAction(table, 'd db 4c5c6d');
// Flop: In a HU pot, the BB is OOP and acts first; SB is IP and acts last.
applyAction(table, 'p2 cc'); // BB checks
const sbFlopStats = Stats.forPlayerStreet(table, 0, 'flop');
expect(sbFlopStats).toMatchObject({ cbetIpOpportunities: 1, cbetOopOpportunities: 0 });
const bbFlopStats = Stats.forPlayerStreet(table, 1, 'flop');
expect(bbFlopStats).toMatchObject({ cbetIpOpportunities: 0, cbetOopOpportunities: 0 });
});
it('2A) should give NO flop c-bet opp when PFA is all-in preflop (multiway to HU)', () => {
// Setup: 4-handed. SB short and becomes last aggressor by shoving.
const table = Game({
...sampleGame,
players: ['UTG', 'BTN', 'SB', 'BB'],
blindsOrStraddles: [0, 0, 10, 20],
startingStacks: [1000, 1000, 220, 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');
// Preflop: UTG opens, BTN calls, SB (short) shoves = last aggressor; BB folds; UTG & BTN call.
applyAction(table, 'p1 cbr 60'); // UTG open
applyAction(table, 'p2 cc'); // BTN call
applyAction(table, 'p3 cbr 220'); // SB all-in (last aggressor)
applyAction(table, 'p4 f'); // BB fold
applyAction(table, 'p1 cc'); // UTG call
applyAction(table, 'p2 cc'); // BTN call
// Flop: PFA (SB) is all-in → nobody can c-bet.
applyAction(table, 'd db Ac7c2d');
const utgFlop = Stats.forPlayerStreet(table, 0, 'flop');
const btnFlop = Stats.forPlayerStreet(table, 1, 'flop');
expect(utgFlop?.cbetIpOpportunities).toBe(0);
expect(utgFlop?.cbetOopOpportunities).toBe(0);
expect(btnFlop).toBeUndefined();
});
it('2B) should give UTG an OOP flop c-bet opp after re-taking last aggression preflop', () => {
// Setup: same seats; UTG becomes LAST aggressor by 4-betting over SB shove.
const table = Game({
...sampleGame,
players: ['UTG', 'BTN', 'SB', 'BB'],
blindsOrStraddles: [0, 0, 10, 20],
startingStacks: [1000, 1000, 220, 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');
// Preflop line: UTG open, BTN call, SB shove, BB fold, UTG re-raises (takes PFA), BTN continues.
applyAction(table, 'p1 cbr 60'); // UTG open
applyAction(table, 'p2 cc'); // BTN call
applyAction(table, 'p3 cbr 220'); // SB all-in
applyAction(table, 'p4 f'); // BB fold
applyAction(table, 'p1 cbr 500'); // UTG 4-bet covering SB → UTG is LAST aggressor (PFA)
applyAction(table, 'p2 cc'); // BTN continues
// Flop: button is p2; acting order skips SB (all-in), skips BB (folded), lands on UTG first → OOP vs BTN.
applyAction(table, 'd db Ac7c2d');
const utgFlop = Stats.forPlayerStreet(table, 0, 'flop');
expect(utgFlop?.cbetOopOpportunities).toBe(1);
expect(utgFlop?.cbetIpOpportunities).toBe(0);
const btnFlop = Stats.forPlayerStreet(table, 1, 'flop');
expect(btnFlop).toBeUndefined();
});
it.skip('3) should adjust projected preflop order with a straddle', () => {
// Setup: 4-handed with a live UTG straddle; BTN is the dealer.
// Seats: p1=UTG(straddle), p2=BTN, p3=SB, p4=BB
const table = Game({
...sampleGame,
players: ['UTG', 'BTN', 'SB', 'BB'],
blindsOrStraddles: [40, 0, 10, 20], // UTG posts 40 (live straddle)
startingStacks: [1000, 1000, 1000, 1000],
});
// Deal
applyAction(table, 'd dh p1 2h2c');
applyAction(table, 'd dh p2 3h3c');
applyAction(table, 'd dh p3 4h4c');
applyAction(table, 'd dh p4 5h5c');
// --- Preflop order starts with UTG, then proceeds clockwise. ---
applyAction(table, 'p1 cbr 120'); // UTG raises their own straddle.
applyAction(table, 'p2 cc'); // BTN calls.
applyAction(table, 'p3 f'); // SB folds.
applyAction(table, 'p4 f'); // BB folds.
applyAction(table, 'd db 2c3c4d');
// Flop: Action starts with the first active player to the left of the button, which is UTG.
// UTG was the PFA and is OOP vs BTN, giving them an OOP c-bet opportunity.
const utgFlopStats = Stats.forPlayerStreet(table, 0, 'flop');
expect(utgFlopStats).toMatchObject({ cbetOopOpportunities: 1, cbetIpOpportunities: 0 });
});
});
describe('Preflop Maneuvers', () => {
it('4) should track steal attempts from CO/BTN/SB with size thresholds', () => {
// Setup: Three separate hands with a CO open-raise.
// Action A: CO raises less than 2.5x BB.
// Action B: CO raises exactly 2.5x BB.
// Action C: CO raises more than 4x BB.
// Assert A: A raise < 2.5x BB should not count as a steal attempt.
// Assert B & C: Raises >= 2.5x should count as a steal attempt.
const tableA = Game({
...sampleGame,
players: ['CO', 'BTN', 'SB', 'BB'],
blindsOrStraddles: [0, 0, 10, 20],
startingStacks: [1000, 1000, 1000, 1000],
});
applyAction(tableA, 'd dh p1 2h2c');
applyAction(tableA, 'd dh p2 3h3c');
applyAction(tableA, 'd dh p3 4h4c');
applyAction(tableA, 'd dh p4 5h5c');
applyAction(tableA, 'p1 cbr 45'); // CO < 2.5x
const coStatsA = Stats.forPlayerStreet(tableA, 0, 'preflop');
expect(coStatsA).toMatchObject({ stealIpOpportunities: 1, stealIpAttempts: 0 });
const tableB = Game({
...sampleGame,
players: ['CO', 'BTN', 'SB', 'BB'],
blindsOrStraddles: [0, 0, 10, 20],
startingStacks: [1000, 1000, 1000, 1000],
});
applyAction(tableB, 'd dh p1 2h2c');
applyAction(tableB, 'd dh p2 3h3c');
applyAction(tableB, 'd dh p3 4h4c');
applyAction(tableB, 'd dh p4 5h5c');
applyAction(tableB, 'p1 cbr 50'); // CO = 2.5x
const coStatsB = Stats.forPlayerStreet(tableB, 0, 'preflop');
expect(coStatsB).toMatchObject({ stealIpOpportunities: 1, stealIpAttempts: 1 });
const tableC = Game({
...sampleGame,
players: ['CO', 'BTN', 'SB', 'BB'],
blindsOrStraddles: [0, 0, 10, 20],
startingStacks: [1000, 1000, 1000, 1000],
});
applyAction(tableC, 'd dh p1 2h2c');
applyAction(tableC, 'd dh p2 3h3c');
applyAction(tableC, 'd dh p3 4h4c');
applyAction(tableC, 'd dh p4 5h5c');
applyAction(tableC, 'p1 cbr 90'); // CO > 4x
const coStatsC = Stats.forPlayerStreet(tableC, 0, 'preflop');
expect(coStatsC).toMatchObject({ stealIpOpportunities: 1, stealIpAttempts: 0 }); // Fails because raise size is > 4bb
});
it('5) should identify an SB open as an OOP steal attempt vs BB', () => {
// Setup: 3-handed, folds to SB.
// Action: SB open-raises, BB folds.
// Assert: SB's action is an OOP steal attempt and takedown. BB faces an OOP steal and folds.
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 f');
applyAction(table, 'p2 cbr 60');
applyAction(table, 'p3 f');
const sbStats = Stats.forPlayerStreet(table, 1, 'preflop');
expect(sbStats).toMatchObject({
stealOopOpportunities: 1,
stealOopAttempts: 1,
stealOopTakedowns: 1,
});
const bbStats = Stats.forPlayerStreet(table, 2, 'preflop');
expect(bbStats).toMatchObject({
stealOopChallenges: 0,
stealOopFolds: 0,
stealIpChallenges: 1,
stealIpFolds: 1,
});
});
it('6) should detect a squeeze vs an original raiser and one or more callers', () => {
// Setup: UTG opens, CO calls.
// Action: BB 3-bets.
// Assert: BB's action is a squeeze attempt (and a 3-bet). UTG and CO face a squeeze.
const table = Game({
...sampleGame,
players: ['UTG', 'CO', 'BTN', 'SB', 'BB'],
blindsOrStraddles: [0, 0, 0, 10, 20],
startingStacks: [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, 'p1 cbr 60'); // UTG
applyAction(table, 'p2 cc'); // CO
applyAction(table, 'p3 f');
applyAction(table, 'p4 f');
applyAction(table, 'p5 cbr 260'); // BB
const bbStats = Stats.forPlayerStreet(table, 4, 'preflop');
expect(bbStats).toMatchObject({
squeezeOopOpportunities: 1,
squeezeOopAttempts: 1,
threeBetOopAttempts: 1,
});
// UTG and CO now face a squeeze
const utgStats = Stats.forPlayerStreet(table, 0, 'preflop');
expect(utgStats).toMatchObject({ squeezeIpChallenges: 1 });
applyAction(table, 'p1 cc');
const coStats = Stats.forPlayerStreet(table, 1, 'preflop');
expect(coStats).toMatchObject({ squeezeIpChallenges: 1, squeezeOopChallenges: 0 });
});
it('7) should track cold 3-bet and cold 4-bet opportunities', () => {
// Setup: UTG opens, MP calls, then CO cold 3-bets. BB then cold 4-bets.
// Action: UTG opens, MP calls, CO 3-bets, folds to BB, who 4-bets.
// Assert: CO's action is a cold 3-bet. BB's action is a cold 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 Open
applyAction(table, 'p2 cc'); // MP Call
applyAction(table, 'p3 cbr 240'); // CO Cold 3-bet
applyAction(table, 'p4 f'); // BTN Fold
applyAction(table, 'p5 f'); // SB Fold
applyAction(table, 'p6 cbr 720'); // BB Cold 4-bet
const coStats = Stats.forPlayerStreet(table, 2, 'preflop');
expect(coStats).toMatchObject({ threeBetIpAttempts: 1 });
const bbStats = Stats.forPlayerStreet(table, 5, 'preflop');
expect(bbStats).toMatchObject({ fourBetOopAttempts: 1 });
});
it('8) should not provide a 3-bet/4-bet opportunity when raising is closed', () => {
// Setup: CO opens, BTN calls, SB shoves all-in for less than a legal raise.
// Action: Action is on the BB and then returns to the CO.
// Assert: No player should have a 3-bet or 4-bet opportunity, as the raise was not reopened.
const table = Game({
...sampleGame,
players: ['CO', 'BTN', 'SB', 'BB'],
blindsOrStraddles: [0, 0, 10, 20],
startingStacks: [1000, 1000, 95, 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, 'p1 cbr 60'); // CO
applyAction(table, 'p2 cc'); // BTN
applyAction(table, 'p3 cbr 95'); // SB all-in, not a full raise
applyAction(table, 'p4 cc'); // BB calls
applyAction(table, 'p1 cc'); // CO can only call
const bbStats = Stats.forPlayerStreet(table, 3, 'preflop');
expect(bbStats).toMatchObject({ threeBetOopOpportunities: 0, fourBetOopOpportunities: 0 });
const coStats = Stats.forPlayerStreet(table, 0, 'preflop');
expect(coStats).toMatchObject({ fourBetOopOpportunities: 0, raises: 1 }); // Initial raise
});
it('9) should increment both maneuver and shove counters for 3-bet/4-bet shoves', () => {
// Setup: BTN opens, SB 3-bet shoves, BTN 4-bet shoves over the top.
// Action: BTN opens, SB shoves, BTN re-shoves.
// Assert: SB's action increments both threeBetOopAttempts and shoveOopAttempts.
// BTN's action increments both fourBetIpAttempts and shoveIpAttempts.
const table = Game({
...sampleGame,
players: ['BTN', 'SB', 'BB'],
blindsOrStraddles: [0, 10, 20],
startingStacks: [2000, 1000, 2000],
});
applyAction(table, 'd dh p1 2h2c');
applyAction(table, 'd dh p2 3h3c');
applyAction(table, 'd dh p3 4h4c');
applyAction(table, 'p1 cbr 60'); // BTN
applyAction(table, 'p2 cbr 1000'); // SB 3-bet shove
applyAction(table, 'p3 f'); // BB folds
applyAction(table, 'p1 cbr 2000'); // BTN 4-bet shove
const sbStats = Stats.forPlayerStreet(table, 1, 'preflop');
expect(sbStats).toMatchObject({ threeBetOopAttempts: 1, shoveOopAttempts: 1 });
const btnStats = Stats.forPlayerStreet(table, 0, 'preflop');
expect(btnStats).toMatchObject({ fourBetIpAttempts: 1, shoveIpAttempts: 1 });
});
it('10) should track 5-bet opportunities and attempts', () => {
// Setup: Standard preflop war.
// Action: UTG opens, BTN 3-bets, UTG 4-bets, BTN 5-bets, UTG folds.
// Assert: BTN has a 5-bet IP opportunity and attempt. UTG faces a 5-bet OOP and folds.
const table = Game({
...sampleGame,
players: ['UTG', 'BTN', 'SB', 'BB'],
blindsOrStraddles: [0, 0, 10, 20],
startingStacks: [2000, 2000, 2000, 2000],
});
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, 'p1 cbr 60');
applyAction(table, 'p2 cbr 180');
applyAction(table, 'p3 f');
applyAction(table, 'p4 f');
applyAction(table, 'p1 cbr 540');
applyAction(table, 'p2 cbr 1100');
applyAction(table, 'p1 f');
const btnStats = Stats.forPlayerStreet(table, 1, 'preflop');
expect(btnStats).toMatchObject({ fiveBetIpOpportunities: 1, fiveBetIpAttempts: 1 });
const utgStats = Stats.forPlayerStreet(table, 0, 'preflop');
expect(utgStats).toMatchObject({ fiveBetOopChallenges: 1, fiveBetOopFolds: 1 });
});
});
describe('Postflop — Aggression Maneuvers', () => {
it('11) should identify flop c-bet opportunities OOP and IP in a HU pot', () => {
// Setup Case 1 (OOP PFA): SB opens, BB 3-bets, SB calls. BB is PFA and OOP postflop.
// Assert: On flop, BB has a cbetOopOpportunities.
const tableOop = Game({
...sampleGame,
players: ['SB', 'BB'],
blindsOrStraddles: [10, 20],
startingStacks: [1000, 1000],
});
applyAction(tableOop, 'd dh p1 2h2c');
applyAction(tableOop, 'd dh p2 3h3c');
applyAction(tableOop, 'p1 cbr 60');
applyAction(tableOop, 'p2 cbr 220');
applyAction(tableOop, 'p1 cc');
applyAction(tableOop, 'd db 4c5c6d');
applyAction(tableOop, 'p2 cbr 100');
const bbStatsOop = Stats.forPlayerStreet(tableOop, 1, 'flop');
expect(bbStatsOop).toMatchObject({ cbetOopOpportunities: 1, cbetOopAttempts: 1 });
// Setup Case 2 (IP PFA): BTN opens, BB calls. BTN is PFA and IP postflop.
// Assert: On flop, after BB checks, BTN has a cbetIpOpportunities.
const tableIp = Game({
...sampleGame,
players: ['BTN', 'BB'],
blindsOrStraddles: [10, 20],
startingStacks: [1000, 1000],
});
applyAction(tableIp, 'd dh p1 2h2c');
applyAction(tableIp, 'd dh p2 3h3c');
applyAction(tableIp, 'p1 cbr 60');
applyAction(tableIp, 'p2 cc');
applyAction(tableIp, 'd db 4c5c6d');
applyAction(tableIp, 'p2 cc');
applyAction(tableIp, 'p1 cbr 100');
// Flop: BB is in the blinds and OOP, so they act first.
const btnStatsIp = Stats.forPlayerStreet(tableIp, 0, 'flop');
expect(btnStatsIp).toMatchObject({ cbetIpOpportunities: 1, cbetIpAttempts: 1 });
});
it('12) should not give a c-bet opportunity if a non-PFA leads out (donks)', () => {
// Setup: MP opens, BB defends.
// Action: On the flop, BB leads out with a bet.
// Assert: BB's action is a donk bet. MP (the PFA) should have cbet*Opportunities: 0.
const table = Game({
...sampleGame,
players: ['MP', 'BTN', 'SB', 'BB'],
blindsOrStraddles: [0, 0, 10, 20],
startingStacks: [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 4h4c');
// Preflop: Action starts to the left of the big blind, which is MP.
applyAction(table, 'p1 cbr 60'); // MP
applyAction(table, 'p2 f');
applyAction(table, 'p3 f');
applyAction(table, 'p4 cc'); // BB calls
applyAction(table, 'd db Ac7c2d');
// Flop: Action is on BB, who is OOP.
applyAction(table, 'p4 cbr 100'); // BB donks
const bbStats = Stats.forPlayerStreet(table, 3, 'flop');
expect(bbStats).toMatchObject({ donkBetOpportunities: 1, donkBetAttempts: 1 });
const mpStats = Stats.forPlayerStreet(table, 0, 'flop');
expect(mpStats).toMatchObject({
cbetIpOpportunities: 0,
cbetOopOpportunities: 0,
donkBetChallenges: 1,
});
});
it('13) should track delayed c-bet on turn when flop was checked through (IP & OOP)', () => {
// Setup IP PFA: BTN raises, BB calls. Flop is checked by both.
// Action: On turn, BB checks.
// Assert: BTN has a delayedCbetIpOpportunities.
const tableIp = Game({
...sampleGame,
players: ['BTN', 'BB'],
blindsOrStraddles: [10, 20],
startingStacks: [1000, 1000],
});
applyAction(tableIp, 'd dh p1 2h2c');
applyAction(tableIp, 'd dh p2 3h3c');
applyAction(tableIp, 'p1 cbr 60');
applyAction(tableIp, 'p2 cc');
applyAction(tableIp, 'd db 2c3c4d');
applyAction(tableIp, 'p2 cc');
applyAction(tableIp, 'p1 cc');
applyAction(tableIp, 'd db 5h');
// Turn: BB is OOP and acts first.
applyAction(tableIp, 'p2 cc');
applyAction(tableIp, 'p1 cbr 100');
const btnStats = Stats.forPlayerStreet(tableIp, 0, 'turn');
expect(btnStats).toMatchObject({ delayedCbetIpOpportunities: 1, delayedCbetIpAttempts: 1 });
});
it('14) should not give a delayed c-bet opportunity if another player bets first on the turn', () => {
// Setup: HU. p1 = BTN/SB (acts first preflop, last postflop), p2 = BB (acts last preflop, first postflop).
const table = Game({
...sampleGame,
players: ['BTN', 'BB'],
blindsOrStraddles: [10, 20],
startingStacks: [1000, 1000],
});
// Deal
applyAction(table, 'd dh p1 2h2c');
applyAction(table, 'd dh p2 3h3c');
// Preflop: BTN opens, BB calls. BTN is PFA.
expect(table.nextPlayerIndex).toBe(0); // BTN acts first preflop in HU
applyAction(table, 'p1 cbr 60'); // BTN raise
expect(table.nextPlayerIndex).toBe(1); // BB to act
applyAction(table, 'p2 cc'); // BB call
// Flop: BB (OOP) acts first; checks. BTN (PFA/IP) checks back → flop checks through.
applyAction(table, 'd db 2c3c4d');
expect(table.nextPlayerIndex).toBe(1); // BB first on flop
applyAction(table, 'p2 cc'); // BB check
expect(table.nextPlayerIndex).toBe(0); // BTN to act
applyAction(table, 'p1 cc'); // BTN check-back
// Turn: BB acts first again. If BB LEADS, BTN cannot have delayed-cbet opportunity.
applyAction(table, 'd db 5h');
expect(table.nextPlayerIndex).toBe(1); // BB first on turn
// Assert BEFORE BB acts: no delayed-cbet opportunity has been granted to BTN
expect(Stats.forPlayerStreet(table, 0, 'turn')).toBeUndefined();
// BB leads the turn → this is a PROBE bet attempt (since PFA skipped flop cbet).
applyAction(table, 'p2 cbr 150');
// Assert probe classification for BB; still no delayed-cbet for BTN
expect(Stats.forPlayerStreet(table, 1, 'turn')).toMatchObject({
probeBetAttempts: 1,
probeBetOpportunities: 1,
});
expect(Stats.forPlayerStreet(table, 0, 'turn')).toMatchObject({
delayedCbetIpOpportunities: 0,
delayedCbetIpAttempts: 0,
});
// Optional defender reaction to validate challenge stats
applyAction(table, 'p1 f'); // BTN folds to the probe
expect(Stats.forPlayerStreet(table, 0, 'turn')).toMatchObject({
probeBetChallenges: 1,
probeBetFolds: 1,
});
});
it('15) should track double and triple barrels in a continuous chain', () => {
// Setup: BTN is PFA IP.
// Action: BTN c-bets flop, bets turn, and bets river.
// Assert: On the turn, BTN has a doubleBarrelIpOpportunities and attempt.
// On the river, BTN has a tripleBarrelIpOpportunities and attempt.
const table = Game({
...sampleGame,
players: ['BTN', 'BB'],
blindsOrStraddles: [10, 20],
startingStacks: [1000, 1000],
});
applyAction(table, 'd dh p1 2h2c');
applyAction(table, 'd dh p2 3h3c');
applyAction(table, 'p1 cbr 60');
applyAction(table, 'p2 cc');
applyAction(table, 'd db 2c3c4d');
applyAction(table, 'p2 cc');
applyAction(table, 'p1 cbr 100'); // C-bet
applyAction(table, 'p2 cc');
applyAction(table, 'd db 5h');
// Turn: BB is OOP and acts first.
applyAction(table, 'p2 cc');
applyAction(table, 'p1 cbr 250'); // Double barrel
applyAction(table, 'p2 cc');
applyAction(table, 'd db 6h');
// River: BB is OOP and acts first.
applyAction(table, 'p2 cc');
applyAction(table, 'p1 cbr 400'); // Triple barrel
const btnFlopStats = Stats.forPlayerStreet(table, 0, 'flop');
expect(btnFlopStats).toMatchObject({ cbetIpAttempts: 1 });
const btnTurnStats = Stats.forPlayerStreet(table, 0, 'turn');
expect(btnTurnStats).toMatchObject({
doubleBarrelIpOpportunities: 1,
doubleBarrelIpAttempts: 1,
});
const btnRiverStats = Stats.forPlayerStreet(table, 0, 'river');
expect(btnRiverStats).toMatchObject({
tripleBarrelIpOpportunities: 1,
tripleBarrelIpAttempts: 1,
});
});
it('16) should not track double/triple barrels if the aggressor skipped a street', () => {
// Setup: BTN is PFA IP.
// Action: BTN c-bets flop, checks turn, then bets river.
// Assert: On the river, the bet is not a triple barrel; tripleBarrel*Opportunities should be 0.
const table = Game({
...sampleGame,
players: ['BTN', 'BB'],
blindsOrStraddles: [10, 20],
startingStacks: [1000, 1000],
});
applyAction(table, 'd dh p1 2h2c');
applyAction(table, 'd dh p2 3h3c');
applyAction(table, 'p1 cbr 60');
applyAction(table, 'p2 cc');
applyAction(table, 'd db 2c3c4d');
applyAction(table, 'p2 cc');
applyAction(table, 'p1 cbr 100'); // C-bet
applyAction(table, 'p2 cc');
applyAction(table, 'd db 5h');
// Turn: BB is OOP and acts first.
applyAction(table, 'p2 cc');
applyAction(table, 'p1 cc'); // Check turn
applyAction(table, 'd db 6h');
// River: BB is OOP and acts first.
applyAction(table, 'p2 cc');
applyAction(table, 'p1 cbr 400'); // Not a triple barrel
const btnRiverStats = Stats.forPlayerStreet(table, 0, 'river');
expect(btnRiverStats).toMatchObject({
tripleBarrelIpOpportunities: 0,
tripleBarrelIpAttempts: 0,
});
});
});
describe('Postflop — Donk / Probe / Float', () => {
it('17) should identify a donk bet as always OOP vs the PFA', () => {
// Setup: BTN is PFA IP.
// Action: On the flop, the BB (OOP) leads out with a bet.
// Assert: BB's bet is a donkBetAttempt. It should not be a c-bet.
const table = Game({
...sampleGame,
players: ['BTN', 'BB'],
blindsOrStraddles: [10, 20],
startingStacks: [1000, 1000],
});
applyAction(table, 'd dh p1 2h2c');
applyAction(table, 'd dh p2 3h3c');
applyAction(table, 'p1 cbr 60');
applyAction(table, 'p2 cc');
applyAction(table, 'd db 2c3c4d');
applyAction(table, 'p2 cbr 100');
// Flop: BB is OOP and acts first.
const bbStats = Stats.forPlayerStreet(table, 1, 'flop');
expect(bbStats).toMatchObject({ donkBetOpportunities: 1, donkBetAttempts: 1 });
const btnStats = Stats.forPlayerStreet(table, 0, 'flop');
expect(btnStats).toMatchObject({ cbetIpOpportunities: 0 });
});
it('18) should identify a probe bet as always OOP when PFA skipped the prior street', () => {
// Setup: BTN is PFA. Flop is checked through.
// Action: On the turn, BB (OOP) leads out with a bet.
// Assert: BB's bet is a probeBetAttempt.
const table = Game({
...sampleGame,
players: ['BTN', 'BB'],
blindsOrStraddles: [10, 20],
startingStacks: [1000, 1000],
});
applyAction(table, 'd dh p1 2h2c');
applyAction(table, 'd dh p2 3h3c');
applyAction(table, 'p1 cbr 60');
applyAction(table, 'p2 cc');
applyAction(table, 'd db 2c3c4d');
applyAction(table, 'p2 cc');
applyAction(table, 'p1 cc');
applyAction(table, 'd db 5h');
// Turn: BB is OOP and acts first.
applyAction(table, 'p2 cbr 120');
const bbStats = Stats.forPlayerStreet(table, 1, 'turn');
expect(bbStats).toMatchObject({ probeBetOpportunities: 1, probeBetAttempts: 1 });
});
it('19) should track a float (the call) IP vs a c-bet', () => {
// Setup: BB is PFA and OOP postflop (HU).
// Action: On flop, BB c-bets, and SB (IP) calls.
// Assert: SB's call is a floatAttempt.
const table = Game({
...sampleGame,
players: ['SB', 'BB'],
blindsOrStraddles: [10, 20],
startingStacks: [1000, 1000],
});
applyAction(table, 'd dh p1 2h2c');
applyAction(table, 'd dh p2 3h3c');
applyAction(table, 'p1 cbr 60');
applyAction(table, 'p2 cbr 220'); // BB is PFA
applyAction(table, 'p1 cc');
applyAction(table, 'd db 2c3c4d');
// Flop: BB is PFA and OOP, acts first.
applyAction(table, 'p2 cbr 100');
applyAction(table, 'p1 cc');
const sbStats = Stats.forPlayerStreet(table, 0, 'flop');
expect(sbStats).toMatchObject({ floatOpportunities: 1, floatAttempts: 1 });
});
it('20) should not track float opportunities for OOP calls', () => {
// Setup: BTN is PFA and IP.
// Action: On flop, BTN c-bets, BB (OOP) calls.
// Assert: BB has floatOpportunities: 0.
const table = Game({
...sampleGame,
players: ['BTN', 'BB'],
blindsOrStraddles: [10, 20],
startingStacks: [1000, 1000],
});
applyAction(table, 'd dh p1 2h2c');
applyAction(table, 'd dh p2 3h3c');
applyAction(table, 'p1 cbr 60');
applyAction(table, 'p2 cc');
applyAction(table, 'd db 2c3c4d');
// Flop: BB is OOP and acts first.
applyAction(table, 'p2 cc');
applyAction(table, 'p1 cbr 100');
applyAction(table, 'p2 cc');
const bbStats = Stats.forPlayerStreet(table, 1, 'flop');
expect(bbStats).toMatchObject({ floatOpportunities: 0, floatAttempts: 0 });
});
it('21) should track a float bet on the turn after floating the flop', () => {
// Setup: BB is PFA (OOP postflop, HU), SB (IP) floats the flop.
// Action: On the turn, BB checks, SB bets.
// Assert: SB's bet is a floatBetAttempt. BB faces a floatBetChallenge.
const table = Game({
...sampleGame,
players: ['SB', 'BB'],
blindsOrStraddles: [10, 20],
startingStacks: [1000, 1000],
});
applyAction(table, 'd dh p1 2h2c');
applyAction(table, 'd dh p2 3h3c');
applyAction(table, 'p1 cbr 60');
applyAction(table, 'p2 cbr 220'); // BB is PFA
applyAction(table, 'p1 cc');
applyAction(table, 'd db 2c3c4d');
// Flop: BB is PFA and OOP, acts first.
applyAction(table, 'p2 cbr 100');
applyAction(table, 'p1 cc'); // Float call
applyAction(table, 'd db 8d');
// Turn: BB is PFA and OOP, acts first.
applyAction(table, 'p2 cc');
applyAction(table, 'p1 cbr 180'); // Float bet
const sbStats = Stats.forPlayerStreet(table, 0, 'turn');
expect(sbStats).toMatchObject({ floatBetOpportunities: 1, floatBetAttempts: 1 });
const bbStats = Stats.forPlayerStreet(table, 1, 'turn');
expect(bbStats).toMatchObject({ floatBetChallenges: 1 });
});
it('22) should (or should not) track a river float bet', () => {
// Setup: Like the float bet test, but the turn is checked through.
// Action: On the river, BB checks, SB bets.
// Assert: Depending on engine logic, this is either a floatBetAttempt or just a regular bet.
// Check that floatBet* stats are 0 if not supported on the river.
const table = Game({
...sampleGame,
players: ['SB', 'BB'],
blindsOrStraddles: [10, 20],
startingStacks: [1000, 1000],
});
applyAction(table, 'd dh p1 2h2c');
applyAction(table, 'd dh p2 3h3c');
applyAction(table, 'p1 cbr 60');
applyAction(table, 'p2 cbr 220');
applyAction(table, 'p1 cc');
applyAction(table, 'd db 2c3c4d');
// Flop: BB is PFA and OOP, acts first.
applyAction(table, 'p2 cbr 100');
applyAction(table, 'p1 cc');
applyAction(table, 'd db 8d');
// Turn: BB is PFA and OOP, acts first.
applyAction(table, 'p2 cc');
applyAction(table, 'p1 cc');
applyAction(table, 'd db 9d');
// River: BB is PFA and OOP, acts first.
applyAction(table, 'p2 cc');
applyAction(table, 'p1 cbr 300');
const sbStats = Stats.forPlayerStreet(table, 0, 'river');
expect(sbStats).toMatchObject({ floatBetOpportunities: 0, floatBetAttempts: 0 });
});
it('23) should classify an OOP bet after a PFA check as a probe, not a float bet', () => {
// Setup: BTN is PFA. Flop checks through.
// Action: On the turn, BB (OOP) bets.
// Assert: BB's bet is a probeBetAttempt, and floatBetAttempts is 0.
const table = Game({
...sampleGame,
players: ['BTN', 'BB'],
blindsOrStraddles: [10, 20],
startingStacks: [1000, 1000],
});
applyAction(table, 'd dh p1 2h2c');
applyAction(table, 'd dh p2 3h3c');
applyAction(table, 'p1 cbr 60');
applyAction(table, 'p2 cc');
applyAction(table, 'd db 2c3c4d');
// Flop: BB is OOP and acts first.
applyAction(table, 'p2 cc');
applyAction(table, 'p1 cc');
applyAction(table, 'd db 5h');
// Turn: BB is OOP and acts first.
applyAction(table, 'p2 cbr 120');
const bbStats = Stats.forPlayerStreet(table, 1, 'turn');
expect(bbStats).toMatchObject({ probeBetAttempts: 1, floatBetAttempts: 0 });
});
});
describe('Defensive Stats', () => {
it('24) should log separate squeeze challenges for OR and caller(s)', () => {
// Setup: UTG opens, CO calls, BB squeezes.
// Action: UTG folds, CO calls.
// Assert: UTG has squeezeOopChallenges:1 and squeezeOopFolds:1.
// CO has squeezeIpChallenges:1 and squeezeIpContinues:1.
const table = Game({
...sampleGame,
players: ['UTG', 'CO', 'BTN', 'SB', 'BB'],
blindsOrStraddles: [0, 0, 0, 10, 20],
startingStacks: [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, 'p1 cbr 60');
applyAction(table, 'p2 cc');
applyAction(table, 'p3 f');
applyAction(table, 'p4 f');
applyAction(table, 'p5 cbr 260');
applyAction(table, 'p1 f');
applyAction(table, 'p2 cc');
// BB = squeezer, OOP attempt
const bbStats = Stats.forPlayerStreet(table, 4, 'preflop');
expect(bbStats).toMatchObject({
squeezeOopAttempts: 1,
});
// UTG faced the squeeze and FOLDED → IP challenge vs BB
const utgStats = Stats.forPlayerStreet(table, 0, 'preflop');
expect(utgStats).toMatchObject({
squeezeIpChallenges: 1,
squeezeIpFolds: 1,
squeezeIpContinues: 0,
// and make sure the OOP variants stay 0
squeezeOopChallenges: 0,
squeezeOopFolds: 0,
squeezeOopContinues: 0,
});
// CO faced the squeeze and CONTINUED → IP challenge vs BB
const coStats = Stats.forPlayerStreet(table, 1, 'preflop');
expect(coStats).toMatchObject({
squeezeIpChallenges: 1,
squeezeIpFolds: 0,
squeezeIpContinues: 1,
});
});
it('25) should correctly assign 4-bet defense challenges', () => {
// Setup: UTG opens, CO 3-bets, UTG 4-bets, CO calls.
const table = Game({
...sampleGame,
players: ['UTG', 'CO', 'BTN', 'SB', 'BB'],
blindsOrStraddles: [0, 0, 0, 10, 20],
startingStacks: [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');
// Preflop: Action starts to the left of the big blind, which is UTG.
applyAction(table, 'p1 cbr 60');
applyAction(table, 'p2 cbr 180');
applyAction(table, 'p3 f');
applyAction(table, 'p4 f');
applyAction(table, 'p5 f');
applyAction(table, 'p1 cbr 540');
applyAction(table, 'p2 cc');
const coStats = Stats.forPlayerStreet(table, 1, 'preflop');
expect(coStats).toMatchObject({
fourBetIpChallenges: 1,
fourBetIpContinues: 1,
fourBetIpFolds: 0,
});
});
it('26) should record exactly one defense outcome (fold or continue) per challenge', () => {
// Setup: Any scenario where a player faces a raise (e.g., a 3-bet).
// Action: The player folds.
// Assert: The player's stats show ...Folds:1 and ...Continues:0.
// The total must be Challenges = Folds + Continues.
const table = Game({
...sampleGame,
players: ['UTG', 'BTN'],
blindsOrStraddles: [10, 20],
startingStacks: [1000, 1000],
});
applyAction(table, 'd dh p1 2h2c');
applyAction(table, 'd dh p2 3h3c');
applyAction(table, 'p1 cbr 60');
applyAction(table, 'p2 cbr 180');
applyAction(table, 'p1 f');
const utgStats = Stats.forPlayerStreet(table, 0, 'preflop');
expect(utgStats).toMatchObject({
threeBetIpChallenges: 1,
threeBetIpFolds: 1,
threeBetIpContinues: 0,
threeBetOopChallenges: 0,
threeBetOopFolds: 0,
threeBetOopContinues: 0,
});
});
});
describe('Side Pots, Rake, and Showdown', () => {
it('29) should award a takedown for a successful preflop steal or open-shove', () => {
// Setup: 3-handed.
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');
// Preflop: Action starts on the BTN.
applyAction(table, 'p1 cbr 60');
applyAction(table, 'p2 f');
applyAction(table, 'p3 f');
const btnStats = Stats.forPlayerStreet(table, 0, 'preflop');
expect(btnStats).toMatchObject({ stealIpTakedowns: 1 });
});
it('30) should only count tabling cards as wentToShowdown, not mucking', () => {
// Setup: 3-way pot to the river.
// Action: A player is called on the river, sees they are beat, and mucks.
// Assert: The two players who tabled their hands have wentToShowdown:1. The mucking player has wentToShowdown:0.
const table = Game({
...sampleGame,
players: ['P1', 'P2', 'P3'],
blindsOrStraddles: [0, 10, 20],
startingStacks: [1000, 1000, 1000],
});
applyAction(table, 'd dh p1 AsAc'); // P1
applyAction(table, 'd dh p2 KsKc'); // P2
applyAction(table, 'd dh p3 QsQc'); // P3
// Preflop
applyAction(table, 'p1 cbr 60');
applyAction(table, 'p2 cc');
applyAction(table, 'p3 cc');
// Flop
applyAction(table, 'd db 2h7d8c');
applyAction(table, 'p2 cc');
applyAction(table, 'p3 cc');
applyAction(table, 'p1 cbr 100');
applyAction(table, 'p2 cc');
applyAction(table, 'p3 cc');
// Turn
applyAction(table, 'd db 9s');
applyAction(table, 'p2 cc');
applyAction(table, 'p3 cc');
applyAction(table, 'p1 cc');
// River
applyAction(table, 'd db Ts');
applyAction(table, 'p2 cc');
applyAction(table, 'p3 cc');
applyAction(table, 'p1 cbr 200');
applyAction(table, 'p2 cc');
applyAction(table, 'p3 cc');
// Showdown
applyAction(table, 'p1 sm AsAc');
applyAction(table, 'p2 sm KsKc');
applyAction(table, 'p3 sm'); // P3 mucks
// P3 does not act ('sm'), thus mucks.
const p1Stats = Stats.forPlayerStreet(table, 0, 'river');
expect(p1Stats).toMatchObject({ wentToShowdown: 1, investments: 200 });
const p2Stats = Stats.forPlayerStreet(table, 1, 'river');
expect(p2Stats).toMatchObject({ wentToShowdown: 1, investments: 200 });
const p3Stats = Stats.forPlayerStreet(table, 2, 'river');
expect(p3Stats).toMatchObject({ wentToShowdown: 0, investments: 200 });
});
});
describe('More Coverage for Your Existing Areas', () => {
it('33) should deny a c-bet opportunity in a multiway pot if another player acts first', () => {
// Setup: 3-handed. Seat mapping by blinds:
// players: ['UTG', 'BTN', 'BB'] with blindsOrStraddles [0,10,20]
// => p2 is SB (10), p3 is BB (20), p1 has 0 and is effectively the Button.
// We'll make p1 ('UTG' label, actually BTN seat) the PFA preflop.
const table = Game({
...sampleGame,
players: ['UTG', 'BTN', 'BB'],
blindsOrStraddles: [0, 10, 20],
startingStacks: [1000, 1000, 1000],
});
// Deal
applyAction(table, 'd dh p1 2h2c'); // p1 (Button seat)
applyAction(table, 'd dh p2 3h3c'); // p2 (SB)
applyAction(table, 'd dh p3 4h4c'); // p3 (BB)
// Preflop: p1 opens (PFA), p2 calls, p3 calls → multiway to flop
applyAction(table, 'p1 cbr 60'); // PFA
applyAction(table, 'p2 cc'); // SB call
applyAction(table, 'p3 cc'); // BB call
// Flop: action starts with the first active player to left of the button → SB (p2), then BB (p3), then p1 last.
applyAction(table, 'd db Ac7c2d');
// SB must act before BB can "lead"; SB checks to maintain proper order.
applyAction(table, 'p2 cc'); // SB check
// BB now leads into the PFA before the PFA has a chance to act → this is a DONK bet.
applyAction(table, 'p3 cbr 100'); // BB donk
// Assertions:
// 1) PFA (p1) did NOT get a c-bet opportunity on the flop because someone else bet first.
expect(Stats.forPlayerStreet(table, 0, 'flop')).toMatchObject({
cbetIpOpportunities: 0,
cbetOopOpportunities: 0,
});
// 2) BB's lead is classified as a donk bet (OOP vs PFA, acting before PFA).
expect(Stats.forPlayerStreet(table, 2, 'flop')).toMatchObject({
donkBetOpportunities: 1,
donkBetAttempts: 1,
});
// Optional: continue the hand to record defender stats
applyAction(table, 'p1 f'); // PFA folds to donk
expect(Stats.forPlayerStreet(table, 0, 'flop')).toMatchObject({
donkBetChallenges: 1,
donkBetFolds: 1,
investments: 0,
});
applyAction(table, 'p2 f'); // SB folds, hand ends
});
it('34) should not offer a delayed c-bet opportunity on the river', () => {
// Setup: Flop and turn are checked through by the PFA.
// Action: On the river, the action is on the PFA.
// Assert: The PFA has delayedCbet*Opportunities:0.
const table = Game({
...sampleGame,
players: ['BTN', 'BB'],
blindsOrStraddles: [10, 20],
startingStacks: [1000, 1000],
});
applyAction(table, 'd dh p1 2h2c');
applyAction(table, 'd dh p2 3h3c');
applyAction(table, 'p1 cbr 60');
applyAction(table, 'p2 cc');
applyAction(table, 'd db 2c3c4d');
// Flop: BB is OOP and acts first.
applyAction(table, 'p2 cc');
applyAction(table, 'p1 cc');
applyAction(table, 'd db 5h');
// Turn: BB is OOP and acts first.
applyAction(table, 'p2 cc');
applyAction(table, 'p1 cc');
applyAction(table, 'd db 6h');
// River: BB is OOP and acts first.
const btnStats = Stats.forPlayerStreet(table, 1, 'river');
expect(btnStats).toMatchObject({
delayedCbetIpOpportunities: 0,
delayedCbetOopOpportunities: 0,
investments: 0,
});
});
it('35) should correctly classify shoves under the open shove vs re-raise shove taxonomy', () => {
// Setup Hand A: First player to act preflop shoves.
// Assert A: This is an openShoveAttempt and a shoveAttempt.
const tableOpen = Game({
...sampleGame,
players: ['BTN', 'SB', 'BB'],
blindsOrStraddles: [0, 10, 20],
startingStacks: [1000, 1000, 1000],
});
applyAction(tableOpen, 'd dh p1 2h2c');
applyAction(tableOpen, 'd dh p2 3h3c');
applyAction(tableOpen, 'd dh p3 4h4c');
applyAction(tableOpen, 'p1 cbr 1000');
const btnStatsOpen = Stats.forPlayerStreet(tableOpen, 0, 'preflop');
expect(btnStatsOpen).toMatchObject({
openShoveIpAttempts: 1,
shoveIpAttempts: 1,
investments: 1000,
}); // It's an OPEN shove, also a shove
// Setup Hand B: A player open-raises, another player re-raises all-in.
// Assert B: This is a threeBet*Attempt and a shove*Attempt, but NOT an openShove*Attempt.
const table3b = Game({
...sampleGame,
players: ['BTN', 'SB', 'BB'],
blindsOrStraddles: [0, 10, 20],
startingStacks: [1000, 1000, 1000],
});
applyAction(table3b, 'd dh p1 2h2c');
applyAction(table3b, 'd dh p2 3h3c');
applyAction(table3b, 'd dh p3 4h4c');
applyAction(table3b, 'p1 cbr 60');
applyAction(table3b, 'p2 cbr 1000');
const sbStats3b = Stats.forPlayerStreet(table3b, 1, 'preflop');
expect(sbStats3b).toMatchObject({
threeBetOopAttempts: 1,
shoveOopAttempts: 1,
openShoveOopAttempts: 0,
investments: 1000,
});
});
});
describe('Double and Triple Barrel', () => {
it('should track double and triple barrel challenges, folds, and takedowns', () => {
const table = Game({
...sampleGame,
players: ['BTN', 'BB'],
blindsOrStraddles: [10, 20],
startingStacks: [1000, 1000],
});
applyAction(table, 'd dh p1 AhKh'); // BTN
applyAction(table, 'd dh p2 QhJh'); // BB
// Preflop: BTN raises, BB calls. BTN is PFA.
applyAction(table, 'p1 cbr 60');
applyAction(table, 'p2 cc');
// Flop: BB checks, BTN c-bets IP, BB calls.
applyAction(table, 'd db 2c3c4d');
applyAction(table, 'p2 cc');
applyAction(table, 'p1 cbr 100');
applyAction(table, 'p2 cc');
// Turn: BB checks, BTN double barrels, BB calls.
applyAction(table, 'd db 5h');
applyAction(table, 'p2 cc');
applyAction(table, 'p1 cbr 200'); // Double barrel
const btnTurnStats = Stats.forPlayerStreet(table, 0, 'turn');
expect(btnTurnStats).toMatchObject({
doubleBarrelIpOpportunities: 1,
doubleBarrelIpAttempts: 1,
});
const bbTurnStatsBeforeCall = Stats.forPlayerStreet(table, 1, 'turn');
expect(bbTurnStatsBeforeCall).toMatchObject({
doubleBarrelOopChallenges: 1, // This will fail before the fix
doubleBarrelOopContinues: 0,
doubleBarrelIpFolds: 0,
});
applyAction(table, 'p2 cc');
const bbTurnStatsAfterCall = Stats.forPlayerStreet(table, 1, 'turn');
expect(bbTurnStatsAfterCall).toMatchObject({
doubleBarrelOopChallenges: 1,
doubleBarrelOopContinues: 1, // This will fail before the fix
doubleBarrelIpFolds: 0,
});
// River: BB checks, BTN triple barrels, BB folds.
applyAction(table, 'd db 6h');
applyAction(table, 'p2 cc');
applyAction(table, 'p1 cbr 400'); // Triple barrel
const btnRiverStats = Stats.forPlayerStreet(table, 0, 'river');
expect(btnRiverStats).toMatchObject({
tripleBarrelIpOpportunities: 1,
tripleBarrelIpAttempts: 1,
});
const bbRiverStatsBeforeFold = Stats.forPlayerStreet(table, 1, 'river');
expect(bbRiverStatsBeforeFold).toMatchObject({
tripleBarrelOopChallenges: 1, // This will fail before the fix
tripleBarrelOopContinues: 0,
tripleBarrelIpFolds: 0,
});
applyAction(table, 'p2 f');
const bbRiverStatsAfterFold = Stats.forPlayerStreet(table, 1, 'river');
expect(bbRiverStatsAfterFold).toMatchObject({
tripleBarrelOopChallenges: 1,
tripleBarrelOopContinues: 0,
tripleBarrelOopFolds: 1, // This will fail before the fix
});
// Check takedown
const btnRiverStatsAfterTakedown = Stats.forPlayerStreet(table, 0, 'river');
expect(btnRiverStatsAfterTakedown).toMatchObject({
tripleBarrelIpTakedowns: 1, // This will fail before the fix
});
});
});
});