Skip to content

Commit

Permalink
feat: parse PokerStars hands
Browse files Browse the repository at this point in the history
  • Loading branch information
mhuggins committed Jan 3, 2024
1 parent eaa7d90 commit 09ad594
Show file tree
Hide file tree
Showing 23 changed files with 1,154 additions and 77 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,51 +60,63 @@ The value returned is represented as a [`HandHistory`](https://github.com/poker-
},
"players": [
{
"bounty": "0",
"chipStack": "25",
"isAnonymous": false,
"isHero": true,
"name": "Big Blind",
"position": "BB",
"positionIndex": 2,
"seatNumber": 1,
},
{
"bounty": "0",
"chipStack": "24.4",
"isAnonymous": true,
"isHero": false,
"name": "UTG",
"position": "UTG",
"positionIndex": 3,
"seatNumber": 2,
},
{
"bounty": "0",
"chipStack": "7.5",
"isAnonymous": true,
"isHero": false,
"name": "UTG+1",
"position": "UTG+1",
"positionIndex": 4,
"seatNumber": 3,
},
{
"bounty": "0",
"chipStack": "33.77",
"isAnonymous": true,
"isHero": false,
"name": "UTG+2",
"position": "UTG+2",
"positionIndex": 5,
"seatNumber": 4,
},
{
"bounty": "0",
"chipStack": "26.87",
"isAnonymous": true,
"isHero": false,
"name": "Dealer",
"position": "BTN",
"positionIndex": 0,
"seatNumber": 5,
},
{
"bounty": "0",
"chipStack": "23.3",
"isAnonymous": true,
"isHero": false,
"name": "Small Blind",
"position": "SB",
"positionIndex": 1,
"seatNumber": 6,
},
],
Expand Down
115 changes: 115 additions & 0 deletions src/__fixtures__/hands/pokerstars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
export const HAND_CASH_NO_SHOWDOWN = `*********** # 2 **************
PokerStars Hand #247743239964: Hold'em No Limit ($0.25/$0.50 USD) - 2023/12/28 22:34:00 ET
Table 'Sirona' 6-max Seat #5 is the button
Seat 1: natek526 ($35.21 in chips)
Seat 2: PennStateWins ($53.85 in chips)
Seat 3: mhuggins ($50 in chips)
Seat 4: NutritionalFact ($80.17 in chips)
Seat 5: maketheca11 ($73.33 in chips)
Seat 6: omgmuffins ($49.50 in chips)
omgmuffins: posts small blind $0.25
natek526: posts big blind $0.50
*** HOLE CARDS ***
Dealt to mhuggins [3s Qh]
PennStateWins: folds
mhuggins: folds
NutritionalFact: raises $0.50 to $1
maketheca11: folds
omgmuffins: calls $0.75
natek526: folds
*** FLOP *** [9d 3d Qc]
omgmuffins: checks
NutritionalFact: bets $1.19
omgmuffins: calls $1.19
*** TURN *** [9d 3d Qc] [Qd]
omgmuffins: checks
NutritionalFact: checks
*** RIVER *** [9d 3d Qc Qd] [Js]
omgmuffins: bets $3.50
NutritionalFact: folds
Uncalled bet ($3.50) returned to omgmuffins
omgmuffins collected $4.64 from pot
omgmuffins: doesn't show hand
*** SUMMARY ***
Total pot $4.88 | Rake $0.24
Board [9d 3d Qc Qd Js]
Seat 1: natek526 (big blind) folded before Flop
Seat 2: PennStateWins folded before Flop (didn't bet)
Seat 3: mhuggins folded before Flop (didn't bet)
Seat 4: NutritionalFact folded on the River
Seat 5: maketheca11 (button) folded before Flop (didn't bet)
Seat 6: omgmuffins (small blind) collected ($4.64)`;

export const HAND_CASH_ALL_IN = `*********** # 97 **************
PokerStars Hand #247743232339: Hold'em No Limit ($0.50/$1.00 USD) - 2023/12/28 22:13:14 ET
Table 'Skat' 6-max Seat #4 is the button
Seat 1: gvillian03 ($23.76 in chips)
Seat 2: mhuggins ($258.53 in chips)
Seat 3: Schwalke ($156.36 in chips)
Seat 4: $$Top_Boy$$ OG ($97 in chips)
Seat 5: MadOreos ($176.06 in chips)
Seat 6: MiltonBradley ($105.75 in chips)
MadOreos: posts small blind $0.50
MiltonBradley: posts big blind $1
*** HOLE CARDS ***
Dealt to mhuggins [As 8c]
gvillian03: raises $1.50 to $2.50
mhuggins: folds
Schwalke: folds
$$Top_Boy$$ OG: folds
MadOreos: raises $10 to $12.50
MiltonBradley: folds
gvillian03: calls $10
*** FLOP *** [Th 6d 6c]
MadOreos: bets $16.30
gvillian03: calls $11.26 and is all-in
Uncalled bet ($5.04) returned to MadOreos
*** TURN *** [Th 6d 6c] [Kh]
*** RIVER *** [Th 6d 6c Kh] [7h]
*** SHOW DOWN ***
MadOreos: shows [Ad Qd] (a pair of Sixes)
gvillian03: shows [5h Ah] (a flush, Ace high)
gvillian03 collected $46.09 from pot
*** SUMMARY ***
Total pot $48.52 | Rake $2.43
Board [Th 6d 6c Kh 7h]
Seat 1: gvillian03 showed [5h Ah] and won ($46.09) with a flush, Ace high
Seat 2: mhuggins folded before Flop (didn't bet)
Seat 3: Schwalke folded before Flop (didn't bet)
Seat 4: $$Top_Boy$$ OG (button) folded before Flop (didn't bet)
Seat 5: MadOreos (small blind) showed [Ad Qd] and lost with a pair of Sixes
Seat 6: MiltonBradley (big blind) folded before Flop`;

export const HAND_TOURNAMENT_BOUNTY_AND_PLACEMENT = `*********** # 238 **************
PokerStars Hand #247678901194: Tournament #3429918960, $4.55+$4.55+$0.90 USD Hold'em No Limit - Level XXVI (2000/4000) - 2023/12/25 18:48:09 ET
Table '3429918960 5' 9-max Seat #1 is the button
Seat 1: mhuggins (755175 in chips, $148.95 bounty)
Seat 2: hammy6955 (184825 in chips, $38.62 bounty)
mhuggins: posts the ante 600
hammy6955: posts the ante 600
mhuggins: posts small blind 2000
hammy6955: posts big blind 4000
*** HOLE CARDS ***
Dealt to mhuggins [4s 2c]
mhuggins: raises 8000 to 12000
hammy6955: raises 24000 to 36000
mhuggins: calls 24000
*** FLOP *** [Jd 4h 7c]
hammy6955: bets 24000
mhuggins: calls 24000
*** TURN *** [Jd 4h 7c] [4c]
hammy6955: bets 124225 and is all-in
mhuggins: calls 124225
*** RIVER *** [Jd 4h 7c 4c] [Qd]
*** SHOW DOWN ***
hammy6955: shows [Ah Th] (a pair of Fours)
mhuggins: shows [4s 2c] (three of a kind, Fours)
mhuggins collected 369650 from pot
mhuggins wins $19.31 for eliminating hammy6955 and their own bounty increases by $19.31 to $168.26
hammy6955 finished the tournament in 2nd place and received $150.40.
mhuggins wins the tournament and receives $150.56 - congratulations!
*** SUMMARY ***
Total pot 369650 | Rake 0
Board [Jd 4h 7c 4c Qd]
Seat 1: mhuggins (button) (small blind) showed [4s 2c] and won (369650) with three of a kind, Fours
Seat 2: hammy6955 (big blind) showed [Ah Th] and lost with a pair of Fours`;
121 changes: 121 additions & 0 deletions src/grammar/PokerStars.g4
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
grammar PokerStars;

// file entry
handHistory: line | ((line EOL)+ line);
line:
( lineHandIndex
| lineCashGameMeta
| lineTournamentMeta
| lineTableMeta
| linePlayer
| linePostSmallBlind
| linePostBigBlind
| linePostAnte
| lineStreet
| lineDeal
| lineAction
| lineUncalled
| lineShowdown
| lineResult
| lineTotalPot
| lineBoard
| lineActionSummary
| lineAwardBounty
| lineTournamentPlacement
| lineMisc
);
lineHandIndex: '*'+ '#' INT '*'+;
lineCashGameMeta: 'PokerStars' 'Hand' '#' handNumber ':' variant bettingStructure '(' blinds currency ')' '-' timestamp;
lineTournamentMeta: 'PokerStars' 'Hand' '#' handNumber ':' 'Tournament' '#' tournamentNumber ',' buyIn ('+' bountyChip)? '+' entryFee currency variant bettingStructure '-' 'Level' levelNumber '(' blinds ')' '-' timestamp;
lineTableMeta: 'Table' '\'' tableName '\'' tableSize 'Seat' '#' INT 'is' 'the' 'button';
linePlayer: 'Seat' INT ':' playerName '(' playerChips playerBounty? ')';
linePostSmallBlind: playerName ':' 'posts' 'small' 'blind' currencyValue;
linePostBigBlind: playerName ':' 'posts' 'big' 'blind' currencyValue;
linePostAnte: playerName ':' 'posts' 'the' 'ante' currencyValue;
lineStreet: '*'+ STREET_HEADING '*'+ cardCollection*;
lineDeal: 'Dealt' 'to' playerName cardCollection;
lineAction: playerName ':' action;
lineUncalled: 'Uncalled' 'bet' '(' currencyValue ')' 'returned' 'to' playerName;
lineResult: playerName 'collected' currencyValue 'from' 'pot';
lineShowdown: playerName ':'
( 'doesn\'t' 'show' 'hand'
| 'mucks' 'hand'
| 'shows' cardCollection '(' handStrength ('-' handStrengthKicker)? ')'
);
lineTotalPot: 'Total' 'pot' currencyValue '|' 'Rake' currencyValue;
lineBoard: 'Board' cardCollection;
lineActionSummary: 'Seat' INT ':' playerName '(' position ')' actionSummary;
lineAwardBounty: playerName 'wins' currencyValue 'for' 'eliminating' playerName 'and' 'their' 'own' 'bounty' 'increases' 'by' currencyValue 'to' currencyValue;
lineTournamentPlacement: playerName 'finished' 'the' 'tournament' 'in' placement ('nd' | 'st' | 'rd' | 'th') 'place' 'and' 'received' currencyValue '.';
lineMisc:
( playerName 'leaves' 'the' 'table'
| playerName 'joins' 'the' 'table' 'at' 'seat' '#' INT
| playerName 'will' 'be' 'allowed' 'to' 'play' 'after' 'the' 'button'
);

handNumber: INT;
tournamentNumber: INT;
variant: 'Hold\'em';
bettingStructure: 'No Limit';
blinds: currencyValue '/' currencyValue;
currencyValue: '$' DECIMAL;
currency: WORD;
timestamp: INT '/' INT '/' INT INT ':' INT ':' INT timezone;
timezone: WORD;
tableName: (~'\'')+;
tableSize: INT '-' 'max';
playerName: USERNAME;
playerChips: currencyValue 'in' 'chips';
playerBounty: ',' currencyValue 'in' 'bounty';
cardCollection: '[' cards ']';
cards: CARD+;
position: 'small blind' | 'big blind' | 'dealer';
actionSummary:
( 'folded' 'before' 'Flop' ('(' 'didn\t' 'bet' ')')? |
| 'folded' 'on' 'the' ('Flop' | 'Turn' | 'River')
| 'collected' '(' currencyValue ')'
| 'showed' cardCollection 'and' 'won' '(' currencyValue ')' 'with' handStrength
| 'showed' cardCollection 'and' 'lost' 'with' handStrength
| 'mucked' cardCollection?
);
action:
( actionBet
| actionCall
| actionCheck
| actionFold
| actionRaise
);
actionBet: 'bets' currencyValue allIn?;
actionCall: 'calls' currencyValue allIn?;
actionCheck: 'checks';
actionFold: 'folds';
actionRaise: 'raises' currencyValue 'to' currencyValue allIn?;
allIn: 'and' 'is' 'all-in';
handStrength:
( 'high' 'card' WORD
| 'a' 'pair' 'of' WORD
| 'two' 'pair' ',' WORD+
| 'three' 'of' 'a' 'kind' ',' WORD
| 'a' 'straight' ',' WORD+
| 'a' 'flush' ',' WORD+
| 'a' 'full' 'house' ',' WORD+
| 'four' 'of' 'a' 'kind' ',' WORD // TODO: determine if this is correct
| 'a' 'straight' 'flush' ',' WORD // TODO: determine if this is correct
| 'a' 'royal' 'flush' ',' WORD // TODO: determine if this is correct
);
handStrengthKicker: WORD ('+' WORD)? 'kicker';
buyIn: currencyValue;
bountyChip: currencyValue;
entryFee: currencyValue;
levelNumber: ROMAN_NUMERALS;
placement: INT;

INT: [0-9]+;
DECIMAL: (INT ',')* INT ('.' INT)?;
USERNAME: [a-z0-9$_ ]+;
STREET_HEADING: 'HOLE CARDS' | 'FLOP' | 'TURN' | 'RIVER' | 'SHOW DOWN' | 'SUMMARY';
ROMAN_NUMERALS: [ivxlcdm]+;
CARD: [2-9tjqka][cdhs];
WORD: [a-z]+;
EOL: '\r' | '\n' | '\r\n';
WS: [ \t]+ -> skip;
22 changes: 9 additions & 13 deletions src/networks/ignition/IgnitionActionVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AbstractParseTreeVisitor } from 'antlr4ts/tree/AbstractParseTreeVisitor
import { LineActionContext } from '~/grammar/IgnitionParser';
import { IgnitionVisitor } from '~/grammar/IgnitionVisitor';
import { Action } from '~/types';
import { IgnitionChipCountVisitor } from './IgnitionChipCountVisitor';
import { getChipCount } from './getChipCount';
import { NotImplementedError } from './types';

export class IgnitionActionVisitor
Expand All @@ -22,7 +22,7 @@ export class IgnitionActionVisitor

const allInAction = action.actionAllIn();
if (allInAction) {
const amount = new IgnitionChipCountVisitor().visit(allInAction.chipCount()).toString();
const amount = getChipCount(allInAction.chipCount());
const playerName = ctx.position().text;

// This all-in action could actually represent a bet or a call. Unfortunately,
Expand All @@ -34,26 +34,22 @@ export class IgnitionActionVisitor

const allInRaiseAction = action.actionAllInRaise();
if (allInRaiseAction) {
const amount = new IgnitionChipCountVisitor()
.visit(allInRaiseAction.chipCount()[0])
.toString();
const totalBet = new IgnitionChipCountVisitor()
.visit(allInRaiseAction.chipCount()[1])
.toString();
const amount = getChipCount(allInRaiseAction.chipCount()[0]);
const totalBet = getChipCount(allInRaiseAction.chipCount()[1]);
const playerName = ctx.position().text;
return [{ type: 'raise', playerName, amount, totalBet, isAllIn: true }];
}

const betAction = action.actionBet();
if (betAction) {
const amount = new IgnitionChipCountVisitor().visit(betAction.chipCount()).toString();
const amount = getChipCount(betAction.chipCount());
const playerName = ctx.position().text;
return [{ type: 'bet', playerName, amount, isAllIn: false }];
}

const callAction = action.actionCall();
if (callAction) {
const amount = new IgnitionChipCountVisitor().visit(callAction.chipCount()).toString();
const amount = getChipCount(callAction.chipCount());
const playerName = ctx.position().text;
return [{ type: 'call', playerName, amount, isAllIn: false }];
}
Expand All @@ -72,15 +68,15 @@ export class IgnitionActionVisitor

const raiseAction = action.actionRaise();
if (raiseAction) {
const amount = new IgnitionChipCountVisitor().visit(raiseAction.chipCount()[0]).toString();
const totalBet = new IgnitionChipCountVisitor().visit(raiseAction.chipCount()[1]).toString();
const amount = getChipCount(raiseAction.chipCount()[0]);
const totalBet = getChipCount(raiseAction.chipCount()[1]);
const playerName = ctx.position().text;
return [{ type: 'raise', playerName, amount, totalBet, isAllIn: false }];
}

const anteAction = action.actionAnte();
if (anteAction) {
const amount = new IgnitionChipCountVisitor().visit(anteAction.chipCount()).toString();
const amount = getChipCount(anteAction.chipCount());
const playerName = ctx.position().text;
return [{ type: 'post', postType: 'ante', playerName, amount }];
}
Expand Down
21 changes: 0 additions & 21 deletions src/networks/ignition/IgnitionChipCountVisitor.ts

This file was deleted.

Loading

0 comments on commit 09ad594

Please sign in to comment.