@@ -258,19 +258,23 @@ export function parseHandHistory(raw: string): HandParseResult {
258258 const line = rawLine . trim ( ) ;
259259 if ( ! line ) continue ;
260260
261- // ── Header (iPoker "GAME #…" / GG-Stars "Poker Hand #…") ────────────────
262- if ( line . startsWith ( 'GAME #' ) || line . startsWith ( 'Poker Hand #' ) ) {
261+ // ── Header (iPoker "GAME #…" / GG-Stars "Poker Hand #…" / Winamax) ───────
262+ if (
263+ line . startsWith ( 'GAME #' ) ||
264+ line . startsWith ( 'Poker Hand #' ) ||
265+ line . startsWith ( 'Winamax' )
266+ ) {
263267 const idMatch = line . match ( / # ( \S + ?) [ \s : ] / ) ;
264268 gameId = idMatch ? idMatch [ 1 ] ! : null ;
265269 const dateMatch = line . match (
266270 / ( \d { 4 } [ - / ] \d { 2 } [ - / ] \d { 2 } \d { 2 } : \d { 2 } : \d { 2 } ) / ,
267271 ) ;
268272 dateTime = dateMatch ? dateMatch [ 1 ] ! : null ;
269- // GG/Stars carry blinds in the level, e.g. "Level1(10/20)".
270- const level = line . match ( / L e v e l \d + \s * \ (( \d + (?: \. \d + ) ? ) \/ ( \d + (?: \. \d + ) ? ) \) / i ) ;
271- if ( level ) {
272- smallBlind = Number ( level [ 1 ] ) ;
273- bigBlind = Number ( level [ 2 ] ) ;
273+ // Blinds in parens: GG "Level1(10/20)", Winamax "no limit (40/80 )".
274+ const blinds = line . match ( / \ (( \d + (?: \. \d + ) ? ) \/ ( \d + (?: \. \d + ) ? ) \) / ) ;
275+ if ( blinds ) {
276+ smallBlind = Number ( blinds [ 1 ] ) ;
277+ bigBlind = Number ( blinds [ 2 ] ) ;
274278 }
275279 continue ;
276280 }
@@ -286,8 +290,11 @@ export function parseHandHistory(raw: string): HandParseResult {
286290 continue ;
287291 }
288292
289- // GG/Stars table line: "Table '36270' 3-max Seat #1 is the button"
290- if ( line . startsWith ( 'Table ' ) ) {
293+ // Table line with the button seat:
294+ // GG: "Table '36270' 3-max Seat #1 is the button"
295+ // Winamax: "Table: 'Expresso(…)#0' 3-max (real money) Seat #3 is the button"
296+ // (iPoker "Table Info:" was already handled above.)
297+ if ( line . startsWith ( 'Table' ) ) {
291298 const btn = line . match ( / S e a t # ( \d + ) i s t h e b u t t o n / i) ;
292299 if ( btn ) buttonSeat = Number ( btn [ 1 ] ) ;
293300 continue ;
@@ -360,8 +367,8 @@ export function parseHandHistory(raw: string): HandParseResult {
360367 continue ;
361368 }
362369
363- // ── Winner via "name collected X from pot" (GG showdown body) ─────── ────
364- const collected = line . match ( / ^ ( .+ ?) \s + c o l l e c t e d \s + ( [ \d . , ] + ) \s + f r o m p o t / i) ;
370+ // ── Winner via "name collected X from [main/side] pot" (GG / Winamax) ────
371+ const collected = line . match ( / ^ ( .+ ?) \s + c o l l e c t e d \s + ( [ \d . , ] + ) \s + f r o m \b / i) ;
365372 if ( collected && ! inSummary ) {
366373 const amt = parseAmount ( collected [ 2 ] ! ) ;
367374 if ( amt ) addWinner ( collected [ 1 ] ! . trim ( ) , amt . amount ) ;
@@ -402,18 +409,43 @@ export function parseHandHistory(raw: string): HandParseResult {
402409 const desc = line . match ( / \b w i t h \s + ( .+ ) $ / i) ;
403410 addShow ( name , parseAllCardsInBrackets ( line ) ?? [ ] , desc ? desc [ 1 ] ! . trim ( ) : '' ) ;
404411 }
405- const won = line . match ( / \b w o n \s + \( ( [ \d . , ] + ) \) / i) ;
412+ // GG "won (540)" / Winamax "won 1438" (with or without parens).
413+ const won = line . match ( / \b w o n \s + \( ? ( [ \d . , ] + ) / i) ;
406414 if ( won ) addWinner ( name , Number ( won [ 1 ] ! . replace ( / , / g, '' ) ) ) ;
407415 continue ;
408416 }
409417 continue ;
410418 }
411419
412- // ── Action lines: "<name>: <verb> [amount]" ───────────────────────────
413- const actionMatch = line . match ( / ^ ( .+ ?) : \s + ( .+ ) $ / ) ;
414- if ( actionMatch ) {
415- const actor = actionMatch [ 1 ] ! . trim ( ) ;
416- const rest = actionMatch [ 2 ] ! . trim ( ) ;
420+ // ── Action lines ──────────────────────────────────────────────────────
421+ // iPoker/GG/Stars use "Name: verb …"; Winamax omits the colon
422+ // ("Atenea. posts small blind 40"). Try the colon form first, then fall
423+ // back to a verb-anchored match for the colon-less dialect.
424+ // Only treat a colon line as an action if what follows the colon is a known
425+ // verb — Winamax hand descriptions contain " : " (e.g. "Two pairs : 5 and
426+ // 4") which would otherwise false-match the "Name: rest" shape.
427+ const colonRaw = line . match ( / ^ ( .+ ?) : \s + ( .+ ) $ / ) ;
428+ const colonMatch =
429+ colonRaw &&
430+ / ^ ( p o s t s ? | r a i s e s ? | c a l l s ? | c h e c k s ? | f o l d s ? | b e t s ? | s h o w s ? | w i n s ? ) \b / i. test (
431+ colonRaw [ 2 ] ! ,
432+ )
433+ ? colonRaw
434+ : null ;
435+ const wmxMatch = colonMatch
436+ ? null
437+ : line . match (
438+ / ^ ( \S .* ?) \s + ( p o s t s | r a i s e s | c a l l s | c h e c k s | f o l d s | b e t s | s h o w s ) \b \s * ( .* ) $ / i,
439+ ) ;
440+ const actor = colonMatch
441+ ? colonMatch [ 1 ] ! . trim ( )
442+ : wmxMatch
443+ ? wmxMatch [ 1 ] ! . trim ( )
444+ : null ;
445+ if ( actor !== null ) {
446+ const rest = colonMatch
447+ ? colonMatch [ 2 ] ! . trim ( )
448+ : `${ wmxMatch ! [ 2 ] } ${ wmxMatch ! [ 3 ] ?? '' } ` . trim ( ) ;
417449 const lower = rest . toLowerCase ( ) ;
418450
419451 if ( lower . startsWith ( 'post' ) ) {
@@ -432,10 +464,14 @@ export function parseHandHistory(raw: string): HandParseResult {
432464 pushDecision ( actor , 'fold' , 0 ) ;
433465 continue ;
434466 }
435- // GG all-in runout shows cards inline: "Hero: shows [9c Qh]"
467+ // Cards shown inline (GG all-in runout / Winamax show down):
468+ // "Hero: shows [9c Qh]" · "Atenea. shows [5h 5c] (Two pairs : 5 and 4)"
436469 if ( lower . startsWith ( 'show' ) ) {
437470 const cards = parseAllCardsInBrackets ( line ) ;
438- if ( cards ) addShow ( actor , cards , '' ) ;
471+ if ( cards ) {
472+ const parenDesc = line . match ( / \( ( [ ^ ) ] * ) \) \s * $ / ) ;
473+ addShow ( actor , cards , parenDesc ? parenDesc [ 1 ] ! . trim ( ) : '' ) ;
474+ }
439475 continue ;
440476 }
441477
0 commit comments