Coverage for iPokerToFpdb.py: 0%
401 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-28 16:41 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-28 16:41 +0000
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4# Copyright 2010-2012, Carl Gherardi
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20########################################################################
22#import L10n
23#_ = L10n.get_translation()
25# This code is based on CarbonToFpdb.py by Matthew Boss
26#
27# TODO:
28#
29# -- No support for tournaments (see also the last item below)
30# -- Assumes that the currency of ring games is USD
31# -- No support for a bring-in or for antes (is the latter in fact unnecessary
32# for hold 'em on Carbon?)
33# -- hand.maxseats can only be guessed at
34# -- The last hand in a history file will often be incomplete and is therefore
35# rejected
36# -- Is behaviour currently correct when someone shows an uncalled hand?
37# -- Information may be lost when the hand ID is converted from the native form
38# xxxxxxxx-yyy(y*) to xxxxxxxxyyy(y*) (in principle this should be stored as
39# a string, but the database does not support this). Is there a possibility
40# of collision between hand IDs that ought to be distinct?
41# -- Cannot parse tables that run it twice (nor is this likely ever to be
42# possible)
43# -- Cannot parse hands in which someone is all in in one of the blinds. Until
44# this is corrected tournaments will be unparseable
46import sys
47from HandHistoryConverter import *
48from decimal_wrapper import Decimal
49from TourneySummary import *
51class iPoker(HandHistoryConverter):
52 """
53 A class for converting iPoker hand history files to the PokerTH format.
54 """
56 sitename = "iPoker"
57 filetype = "text"
58 codepage = ("utf8", "cp1252")
59 siteId = 14
60 copyGameHeader = True # NOTE: Not sure if this is necessary yet. The file is xml so its likely
61 summaryInFile = True
63 substitutions = {
64 'LS': r"\$|\xe2\x82\xac|\xe2\u201a\xac|\u20ac|\xc2\xa3|\£|RSD|kr|", # Used to remove currency symbols from the hand history
65 'PLYR': r'(?P<PNAME>[^\"]+)', # Regex pattern for matching player names
66 'NUM': r'(.,\d+)|(\d+)', # Regex pattern for matching numbers
67 'NUM2': r'\b((?:\d{1,3}(?:\s\d{3})*)|(?:\d+))\b', # Regex pattern for matching numbers with spaces
69 }
71 limits = { 'No limit':'nl',
72 'Pot limit':'pl',
73 'Limit':'fl',
74 'NL':'nl',
75 'SL':'nl',
76 u'БЛ':'nl',
77 'PL':'pl',
78 'LP':'pl',
79 'L':'fl',
80 'LZ':'nl',
81 }
82 games = { # base, category
83 '7 Card Stud' : ('stud','studhi'),
84 '7 Card Stud Hi-Lo' : ('stud','studhilo'),
85 '7 Card Stud HiLow' : ('stud','studhilo'),
86 '5 Card Stud' : ('stud','5_studhi'),
87 'Holdem' : ('hold','holdem'),
88 'Six Plus Holdem' : ('hold','6_holdem'),
89 'Omaha' : ('hold','omahahi'),
90 'Omaha Hi-Lo' : ('hold','omahahilo'),
91 'Omaha HiLow' : ('hold','omahahilo'),
92 }
94 currencies = { u'€':'EUR', '$':'USD', '':'T$', u'£':'GBP', 'RSD': 'RSD', 'kr': 'SEK'}
96 # translations from captured groups to fpdb info strings
97 Lim_Blinds = { '0.04': ('0.01', '0.02'), '0.08': ('0.02', '0.04'),
98 '0.10': ('0.02', '0.05'), '0.20': ('0.05', '0.10'),
99 '0.40': ('0.10', '0.20'), '0.50': ('0.10', '0.25'),
100 '1.00': ('0.25', '0.50'), '1': ('0.25', '0.50'),
101 '2.00': ('0.50', '1.00'), '2': ('0.50', '1.00'),
102 '4.00': ('1.00', '2.00'), '4': ('1.00', '2.00'),
103 '6.00': ('1.00', '3.00'), '6': ('1.00', '3.00'),
104 '8.00': ('2.00', '4.00'), '8': ('2.00', '4.00'),
105 '10.00': ('2.00', '5.00'), '10': ('2.00', '5.00'),
106 '20.00': ('5.00', '10.00'), '20': ('5.00', '10.00'),
107 '30.00': ('10.00', '15.00'), '30': ('10.00', '15.00'),
108 '40.00': ('10.00', '20.00'), '40': ('10.00', '20.00'),
109 '60.00': ('15.00', '30.00'), '60': ('15.00', '30.00'),
110 '80.00': ('20.00', '40.00'), '80': ('20.00', '40.00'),
111 '100.00': ('25.00', '50.00'), '100': ('25.00', '50.00'),
112 '150.00': ('50.00', '75.00'), '150': ('50.00', '75.00'),
113 '200.00': ('50.00', '100.00'), '200': ('50.00', '100.00'),
114 '400.00': ('100.00', '200.00'), '400': ('100.00', '200.00'),
115 '800.00': ('200.00', '400.00'), '800': ('200.00', '400.00'),
116 '1000.00': ('250.00', '500.00'), '1000': ('250.00', '500.00'),
117 '2000.00': ('500.00', '1000.00'), '2000': ('500.00', '1000.00'),
118 }
120 # translations from captured groups to fpdb info strings
121 Lim_Blinds = { '0.04': ('0.01', '0.02'), '0.08': ('0.02', '0.04'),
122 '0.10': ('0.02', '0.05'), '0.20': ('0.05', '0.10'),
123 '0.40': ('0.10', '0.20'), '0.50': ('0.10', '0.25'),
124 '1.00': ('0.25', '0.50'), '1': ('0.25', '0.50'),
125 '2.00': ('0.50', '1.00'), '2': ('0.50', '1.00'),
126 '4.00': ('1.00', '2.00'), '4': ('1.00', '2.00'),
127 '6.00': ('1.50', '3.00'), '6': ('1.50', '3.00'),
128 '8.00': ('2.00', '4.00'), '8': ('2.00', '4.00'),
129 '10.00': ('2.50', '5.00'), '10': ('2.50', '5.00'),
130 '20.00': ('5.00', '10.00'), '20': ('5.00', '10.00'),
131 '30.00': ('7.50', '15.00'), '30': ('7.50', '15.00'),
132 '40.00': ('10.00', '20.00'), '40': ('10.00', '20.00'),
133 '60.00': ('15.00', '30.00'), '60': ('15.00', '30.00'),
134 '80.00': ('20.00', '40.00'), '80': ('20.00', '40.00'),
135 '100.00': ('25.00', '50.00'), '100': ('25.00', '50.00'),
136 '150.00': ('50.00', '75.00'), '150': ('50.00', '75.00'),
137 '200.00': ('50.00', '100.00'), '200': ('50.00', '100.00'),
138 '300.00': ('75.00', '150.00'), '300': ('75.00', '150.00'),
139 '400.00': ('100.00', '200.00'), '400': ('100.00', '200.00'),
140 '600.00': ('150.00', '300.00'), '600': ('150.00', '300.00'),
141 '800.00': ('200.00', '400.00'), '800': ('200.00', '400.00'),
142 '1000.00': ('250.00', '500.00'), '1000': ('250.00', '500.00'),
143 '2000.00': ('500.00', '1000.00'), '2000': ('500.00', '1000.00'),
144 '4000.00': ('1000.00','2000.00'), '4000': ('1000.00', '2000.00'),
145 }
147 months = { 'Jan':1, 'Feb':2, 'Mar':3, 'Apr':4, 'May':5, 'Jun':6, 'Jul':7, 'Aug':8, 'Sep':9, 'Oct':10, 'Nov':11, 'Dec':12}
149 # Static regexes
150 re_client = re.compile(r'<client_version>(?P<CLIENT>.*?)</client_version>')
151 #re_Identify = re.compile(u"""<\?xml version=\"1\.0\" encoding=\"utf-8\"\?>""")
152 re_Identify = re.compile(u"""<game gamecode=\"\d+\">""")
153 re_SplitHands = re.compile(r'</game>')
154 re_TailSplitHands = re.compile(r'(</game>)')
155 re_GameInfo = re.compile(r"""
156 <gametype>(?P<GAME>((?P<CATEGORY>(5|7)\sCard\sStud(\sHi\-Lo|\sHiLow)?|(Six\sPlus\s)?Holdem|Omaha(\sHi\-Lo|\sHiLow)?)?\s?(?P<LIMIT>NL|SL|L|LZ|PL|БЛ|LP|No\slimit|Pot\slimit|Limit))|LH\s(?P<LSB>[%(NUM)s]+)(%(LS)s)?/(?P<LBB>[%(NUM)s]+)(%(LS)s)?.+?)
157 (\s(%(LS)s)?(?P<SB>[%(NUM)s]+)(%(LS)s)?/(%(LS)s)?(?P<BB>[%(NUM)s]+))?(%(LS)s)?(\sAnte\s(%(LS)s)?(?P<ANTE>[%(NUM)s]+)(%(LS)s)?)?</gametype>\s+?
158 <tablename>(?P<TABLE>.+)?</tablename>\s+?
159 (<(tablecurrency|tournamentcurrency)>(?P<TABLECURRENCY>.*)</(tablecurrency|tournamentcurrency)>\s+?)?
160 (<smallblind>.+</smallblind>\s+?)?
161 (<bigblind>.+</bigblind>\s+?)?
162 <duration>.+</duration>\s+?
163 <gamecount>.+</gamecount>\s+?
164 <startdate>.+</startdate>\s+?
165 <currency>(?P<CURRENCY>.+)?</currency>\s+?
166 <nickname>(?P<HERO>.+)?</nickname>
167 """ % substitutions, re.MULTILINE|re.VERBOSE)
168 re_GameInfoTrny = re.compile(r"""
169 (?:(<tour(?:nament)?code>(?P<TOURNO>\d+)</tour(?:nament)?code>))|
170 (?:(<tournamentname>(?P<NAME>[^<]*)</tournamentname>))|
171 (?:(<rewarddrawn>(?P<REWARD>[%(NUM2)s%(LS)s]+)</rewarddrawn>))|
172 (?:(<place>(?P<PLACE>.+?)</place>))|
173 (?:(<buyin>(?P<BIAMT>[%(NUM2)s%(LS)s]+)\s\+\s)?(?P<BIRAKE>[%(NUM2)s%(LS)s]+)\s\+\s(?P<BIRAKE2>[%(NUM2)s%(LS)s]+)</buyin>)|
174 (?:(<totalbuyin>(?P<TOTBUYIN>.*)</totalbuyin>))|
175 (?:(<win>(%(LS)s)?(?P<WIN>.+?|[%(NUM2)s%(LS)s]+)</win>))
176 """ % substitutions, re.VERBOSE)
177 re_GameInfoTrny2 = re.compile(r"""
178 (?:(<tour(?:nament)?code>(?P<TOURNO>\d+)</tour(?:nament)?code>))|
179 (?:(<tournamentname>(?P<NAME>[^<]*)</tournamentname>))|
180 (?:(<place>(?P<PLACE>.+?)</place>))|
181 (?:(<buyin>(?P<BIAMT>[%(NUM2)s%(LS)s]+)\s\+\s)?(?P<BIRAKE>[%(NUM2)s%(LS)s]+)</buyin>)|
182 (?:(<totalbuyin>(?P<TOTBUYIN>[%(NUM2)s%(LS)s]+)</totalbuyin>))|
183 (?:(<win>(%(LS)s)?(?P<WIN>.+?|[%(NUM2)s%(LS)s]+)</win>))
184 """ % substitutions, re.VERBOSE)
185 re_Buyin = re.compile(r"""(?:(<totalbuyin>(?P<TOTBUYIN>.*)</totalbuyin>))""" , re.VERBOSE)
186 re_TotalBuyin = re.compile(r"""(?:(<buyin>(?P<BIAMT>[%(NUM2)s%(LS)s]+)\s\+\s)?(?P<BIRAKE>[%(NUM2)s%(LS)s]+)\s\+\s(?P<BIRAKE2>[%(NUM2)s%(LS)s]+)</buyin>)""" % substitutions, re.VERBOSE)
187 re_HandInfo = re.compile(r'code="(?P<HID>[0-9]+)">\s*?<general>\s*?<startdate>(?P<DATETIME>[\.a-zA-Z-/: 0-9]+)</startdate>', re.MULTILINE)
188 re_PlayerInfo = re.compile(r'<player( (seat="(?P<SEAT>[0-9]+)"|name="%(PLYR)s"|chips="(%(LS)s)?(?P<CASH>[%(NUM2)s]+)(%(LS)s)?"|dealer="(?P<BUTTONPOS>(0|1))"|win="(%(LS)s)?(?P<WIN>[%(NUM2)s]+)(%(LS)s)?"|bet="(%(LS)s)?(?P<BET>[^"]+)(%(LS)s)?"|addon="\d*"|rebuy="\d*"|merge="\d*"|reg_code="[\d-]*"))+\s*/>' % substitutions, re.MULTILINE)
189 re_Board = re.compile(r'<cards( (type="(?P<STREET>Flop|Turn|River)"|player=""))+>(?P<CARDS>.+?)</cards>', re.MULTILINE)
190 re_EndOfHand = re.compile(r'<round id="END_OF_GAME"', re.MULTILINE)
191 re_Hero = re.compile(r'<nickname>(?P<HERO>.+)</nickname>', re.MULTILINE)
192 re_HeroCards = re.compile(r'<cards( (type="(Pocket|Second\sStreet|Third\sStreet|Fourth\sStreet|Fifth\sStreet|Sixth\sStreet|River)"|player="%(PLYR)s"))+>(?P<CARDS>.+?)</cards>' % substitutions, re.MULTILINE)
193 #re_Action = re.compile(r'<action ((no="(?P<ACT>[0-9]+)"|player="%(PLYR)s"|(actiontxt="[^"]+" turntime="[^"]+")|type="(?P<ATYPE>\d+)"|sum="(%(LS)s)(?P<BET>[%(NUM)s]+)"|cards="[^"]*") ?)*/>' % substitutions, re.MULTILINE)
194 re_Action = re.compile(r'<action(?:\s+player=\"%(PLYR)s\"|\s+type=\"(?P<ATYPE>\d+)\"|\s+no=\"(?P<ACT>[0-9]+)\"|\s+sum=\"(?P<BET>[%(NUM)s]+)(%(LS)s)\")+/>' % substitutions, re.MULTILINE)
195 re_SitsOut = re.compile(r'<event sequence="[0-9]+" type="SIT_OUT" player="(?P<PSEAT>[0-9])"/>', re.MULTILINE)
196 re_DateTime1 = re.compile("""(?P<D>[0-9]{2})\-(?P<M>[a-zA-Z]{3})\-(?P<Y>[0-9]{4})\s+(?P<H>[0-9]+):(?P<MIN>[0-9]+)(:(?P<S>[0-9]+))?""", re.MULTILINE)
197 re_DateTime2 = re.compile("""(?P<D>[0-9]{2})[\/\.](?P<M>[0-9]{2})[\/\.](?P<Y>[0-9]{4})\s+(?P<H>[0-9]+):(?P<MIN>[0-9]+)(:(?P<S>[0-9]+))?""", re.MULTILINE)
198 re_DateTime3 = re.compile("""(?P<Y>[0-9]{4})\/(?P<M>[0-9]{2})\/(?P<D>[0-9]{2})\s+(?P<H>[0-9]+):(?P<MIN>[0-9]+)(:(?P<S>[0-9]+))?""", re.MULTILINE)
199 re_MaxSeats = re.compile(r'<tablesize>(?P<SEATS>[0-9]+)</tablesize>', re.MULTILINE)
200 re_tablenamemtt = re.compile(r'<tablename>(?P<TABLET>.+?)</tablename>', re.MULTILINE)
201 re_TourNo = re.compile(r'(?P<TOURNO>\d+)$', re.MULTILINE)
202 re_non_decimal = re.compile(r'[^\d.,]+')
203 re_Partial = re.compile('<startdate>', re.MULTILINE)
204 re_UncalledBets = re.compile('<uncalled_bet_enabled>true<\/uncalled_bet_enabled>')
205 re_ClientVersion = re.compile('<client_version>(?P<VERSION>[.\d]+)</client_version>')
206 re_FPP = re.compile(r'Pts\s')
208 def compilePlayerRegexs(self, hand):
209 pass
211 def playerNameFromSeatNo(self, seatNo, hand):
212 """
213 Returns the name of the player from the given seat number.
215 This special function is required because Carbon Poker records actions by seat number, not by the player's name.
217 Args:
218 seatNo (int): The seat number of the player.
219 hand (Hand): The hand instance containing the players information.
221 Returns:
222 str: The name of the player from the given seat number.
223 """
224 for p in hand.players:
225 if p[0] == int(seatNo):
226 return p[1]
229 def readSupportedGames(self):
230 """
231 Return a list of supported games, where each game is a list of strings.
232 The first element of each game list is either "ring" or "tour".
233 The second element of each game list is either "stud" or "hold".
234 The third element of each game list is either "nl", "pl", or "fl".
235 """
236 return [
237 ["ring", "stud", "fl"], # ring game with stud format and fixed limit
238 ["ring", "hold", "nl"], # ring game with hold format and no limit
239 ["ring", "hold", "pl"], # ring game with hold format and pot limit
240 ["ring", "hold", "fl"], # ring game with hold format and fixed limit
241 ["tour", "hold", "nl"], # tournament with hold format and no limit
242 ["tour", "hold", "pl"], # tournament with hold format and pot limit
243 ["tour", "hold", "fl"], # tournament with hold format and fixed limit
244 ["tour", "stud", "fl"], # tournament with stud format and fixed limit
245 ]
247 def parseHeader(self, handText, whole_file):
248 """
249 Parses the header of a hand history and returns the game type.
251 Args:
252 hand_text (str): The text containing the header of the hand history.
253 whole_file (str): The entire text of the hand history.
255 Returns:
256 str: The game type, if it can be determined from the header or the whole file.
257 None otherwise.
259 Raises:
260 FpdbParseError: If the hand history is an iPoker hand lacking actions/starttime.
261 FpdbHandPartial: If the hand history is an iPoker partial hand history without a start date.
262 """
263 gametype = self.determineGameType(handText)
264 if gametype is None:
265 gametype = self.determineGameType(whole_file)
266 if gametype is None:
267 # Catch iPoker hands lacking actions/starttime and funnel them to partial
268 if self.re_Partial.search(whole_file):
269 tmp = handText[:200]
270 log.error(f"iPokerToFpdb.determineGameType: '{tmp}'")
271 raise FpdbParseError
272 else:
273 message = "No startdate"
274 raise FpdbHandPartial(f"iPoker partial hand history: {message}")
275 return gametype
277 def determineGameType(self, handText):
278 """
279 Given a hand history, extract information about the type of game being played.
280 """
281 m = self.re_GameInfo.search(handText)
282 if not m: return None
283 m2 = self.re_MaxSeats.search(handText)
284 m3 = self.re_tablenamemtt.search(handText)
285 self.info = {}
286 mg = m.groupdict()
287 mg2 = m2.groupdict()
288 mg3 = m3.groupdict()
289 tourney = False
290 #print "DEBUG: m.groupdict(): %s" % mg
291 if mg['GAME'][:2]=='LH':
292 mg['CATEGORY'] = 'Holdem'
293 mg['LIMIT'] = 'L'
294 mg['BB'] = mg['LBB']
295 if 'GAME' in mg:
296 if mg['CATEGORY'] is None:
297 (self.info['base'], self.info['category']) = ('hold', '5_omahahi')
298 else:
299 (self.info['base'], self.info['category']) = self.games[mg['CATEGORY']]
300 if 'LIMIT' in mg:
301 self.info['limitType'] = self.limits[mg['LIMIT']]
302 if 'HERO' in mg:
303 self.hero = mg['HERO']
304 if 'SB' in mg:
305 self.info['sb'] = self.clearMoneyString(mg['SB'])
306 if not mg['SB']: tourney = True
307 if 'BB' in mg:
308 self.info['bb'] = self.clearMoneyString(mg['BB'])
309 if 'SEATS' in mg2:
310 self.info['seats'] = mg2['SEATS']
312 if self.re_UncalledBets.search(handText):
313 self.uncalledbets = False
314 else:
315 self.uncalledbets = True
316 if mv := self.re_ClientVersion.search(handText):
317 major_version = mv.group('VERSION').split('.')[0]
318 if int(major_version) >= 20:
319 self.uncalledbets = False
321 if tourney:
322 self.info['type'] = 'tour'
323 self.info['currency'] = 'T$'
324 if 'TABLET' in mg3:
325 self.info['table_name'] = mg3['TABLET']
326 print(mg3['TABLET'])
327 # FIXME: The sb/bb isn't listed in the game header. Fixing to 1/2 for now
328 self.tinfo = {} # FIXME?: Full tourney info is only at the top of the file. After the
329 # first hand in a file, there is no way for auto-import to
330 # gather the info unless it reads the entire file every time.
331 mt = self.re_TourNo.search(mg['TABLE'])
332 if mt:
333 self.tinfo['tourNo'] = mt.group('TOURNO')
334 else:
335 tourNo = mg['TABLE'].split(',')[-1].strip().split(' ')[0]
336 if tourNo.isdigit():
337 self.tinfo['tourNo'] = tourNo
339 self.tablename = '1'
340 if not mg['CURRENCY'] or mg['CURRENCY']=='fun':
341 self.tinfo['buyinCurrency'] = 'play'
342 else:
343 self.tinfo['buyinCurrency'] = mg['CURRENCY']
344 self.tinfo['buyin'] = 0
345 self.tinfo['fee'] = 0
346 client_match = self.re_client.search(handText)
347 re_client_split = '.'.join(client_match['CLIENT'].split('.')[:2])
348 if re_client_split == '23.5': #betclic fr
349 matches = list(self.re_GameInfoTrny.finditer(handText))
350 if len(matches) > 0:
351 mg['TOURNO'] = matches[0].group('TOURNO')
352 mg['NAME'] = matches[1].group('NAME')
353 mg['REWARD'] = matches[2].group('REWARD')
354 mg['PLACE'] = matches[3].group('PLACE')
355 mg['BIAMT'] = matches[4].group('BIAMT')
356 mg['BIRAKE'] = matches[4].group('BIRAKE')
357 mg['BIRAKE2'] = matches[4].group('BIRAKE2')
358 mg['TOTBUYIN'] = matches[5].group('TOTBUYIN')
359 mg['WIN'] = matches[6].group('WIN')
361 else:
362 matches = list(self.re_GameInfoTrny2.finditer(handText))
363 if len(matches) > 0:
364 mg['TOURNO'] = matches[0].group('TOURNO')
365 mg['NAME'] = matches[1].group('NAME')
366 mg['PLACE'] = matches[2].group('PLACE')
367 mg['BIAMT'] = matches[3].group('BIAMT')
368 mg['BIRAKE'] = matches[3].group('BIRAKE')
369 mg['TOTBUYIN'] = matches[4].group('TOTBUYIN')
370 mg['WIN'] = matches[5].group('WIN')
373 if mg['TOURNO']:
374 self.tinfo['tour_name'] = mg['NAME']
375 self.tinfo['tourNo'] = mg['TOURNO']
376 if mg['PLACE'] and mg['PLACE'] != 'N/A':
377 self.tinfo['rank'] = int(mg['PLACE'])
379 if 'winnings' not in self.tinfo:
380 self.tinfo['winnings'] = 0 # Initialize 'winnings' if it doesn't exist yet
382 if mg['WIN'] and mg['WIN'] != 'N/A':
383 self.tinfo['winnings'] += int(100*Decimal(self.clearMoneyString(self.re_non_decimal.sub('',mg['WIN']))))
386 if not mg['BIRAKE']: #and mg['TOTBUYIN']:
387 m3 = self.re_TotalBuyin.search(handText)
388 if m3:
389 mg = m3.groupdict()
390 elif mg['BIAMT']: mg['BIRAKE'] = '0'
393 if mg['BIAMT'] and self.re_FPP.match(mg['BIAMT']):
394 self.tinfo['buyinCurrency'] = 'FPP'
396 if mg['BIRAKE']:
397 #FIXME: tournament no looks liek it is in the table name
398 mg['BIRAKE'] = self.clearMoneyString(self.re_non_decimal.sub('',mg['BIRAKE']))
399 mg['BIAMT'] = self.clearMoneyString(self.re_non_decimal.sub('',mg['BIAMT']))
400 if re_client_split == '23.5':
401 if mg['BIRAKE2']:
402 self.tinfo['buyin'] += int(100*Decimal(self.clearMoneyString(self.re_non_decimal.sub('',mg['BIRAKE2']))))
403 m4 = self.re_Buyin.search(handText)
404 if m4:
406 self.tinfo['fee'] = int(100*Decimal(self.clearMoneyString(self.re_non_decimal.sub('',mg['BIRAKE']))))
407 self.tinfo['buyin'] = int(100*Decimal(self.clearMoneyString(self.re_non_decimal.sub('',mg['BIRAKE2']))))
409 # FIXME: <place> and <win> not parsed at the moment.
410 # NOTE: Both place and win can have the value N/A
411 if self.tinfo['buyin'] == 0:
412 self.tinfo['buyinCurrency'] = 'FREE'
413 if self.tinfo.get('tourNo') is None:
414 log.error(("iPokerToFpdb.determineGameType: Could Not Parse tourNo"))
415 raise FpdbParseError
416 else:
417 self.info['type'] = 'ring'
418 self.tablename = mg['TABLE']
419 if not mg['TABLECURRENCY'] and not mg['CURRENCY']:
420 self.info['currency'] = 'play'
421 elif not mg['TABLECURRENCY']:
422 self.info['currency'] = mg['CURRENCY']
423 else:
424 self.info['currency'] = mg['TABLECURRENCY']
426 if self.info['limitType'] == 'fl' and self.info['bb'] is not None:
427 try:
428 self.info['sb'] = self.Lim_Blinds[self.clearMoneyString(mg['BB'])][0]
429 self.info['bb'] = self.Lim_Blinds[self.clearMoneyString(mg['BB'])][1]
430 except KeyError as e:
431 tmp = handText[:200]
432 log.error(
433 f"iPokerToFpdb.determineGameType: Lim_Blinds has no lookup for '{mg['BB']}' - '{tmp}'"
434 )
435 raise FpdbParseError from e
437 return self.info
439 def readHandInfo(self, hand):
440 """
441 Parses the hand text and extracts relevant information about the hand.
443 Args:
444 hand: An instance of the Hand class that represents the hand being parsed.
446 Raises:
447 FpdbParseError: If the hand text cannot be parsed.
449 Returns:
450 None
451 """
452 # Search for the relevant information in the hand text
453 m = self.re_HandInfo.search(hand.handText)
454 if m is None:
455 # If the information cannot be found, log an error and raise an exception
456 tmp = hand.handText[:200]
457 log.error(f"iPokerToFpdb.readHandInfo: '{tmp}'")
458 raise FpdbParseError
460 # Extract the relevant information from the match object
461 mg = m.groupdict()
464 # Set the table name and maximum number of seats for the hand
465 hand.tablename = self.tablename
466 if self.info['seats']:
467 hand.maxseats = int(self.info['seats'])
470 # Set the hand ID for the hand
471 hand.handid = m.group('HID')
473 # Parse the start time for the hand
474 if m2 := self.re_DateTime1.search(m.group('DATETIME')):
475 # If the datetime string matches the first format, parse it accordingly
476 month = self.months[m2.group('M')]
477 sec = m2.group('S')
478 if m2.group('S') is None:
479 sec = '00'
480 datetimestr = f"{m2.group('Y')}/{month}/{m2.group('D')} {m2.group('H')}:{m2.group('MIN')}:{sec}"
481 hand.startTime = datetime.datetime.strptime(datetimestr, "%Y/%m/%d %H:%M:%S")
482 else:
483 # If the datetime string does not match the first format, try the second format
484 try:
485 hand.startTime = datetime.datetime.strptime(m.group('DATETIME'), '%Y-%m-%d %H:%M:%S')
486 except ValueError as e:
487 # If the datetime string cannot be parsed, try the third format
488 if date_match := self.re_DateTime2.search(m.group('DATETIME')):
489 datestr = '%d/%m/%Y %H:%M:%S' if '/' in m.group('DATETIME') else '%d.%m.%Y %H:%M:%S'
490 if date_match.group('S') is None:
491 datestr = '%d/%m/%Y %H:%M'
492 else:
493 date_match1 = self.re_DateTime3.search(m.group('DATETIME'))
494 datestr = '%Y/%m/%d %H:%M:%S'
495 if date_match1 is None:
496 # If the datetime string cannot be parsed in any format, log an error and raise an exception
497 log.error(
498 f"iPokerToFpdb.readHandInfo Could not read datetime: '{hand.handid}'"
499 )
500 raise FpdbParseError from e
501 if date_match1.group('S') is None:
502 datestr = '%Y/%m/%d %H:%M'
503 hand.startTime = datetime.datetime.strptime(m.group('DATETIME'), datestr)
505 # If the hand is a tournament hand, set additional information
506 if self.info['type'] == 'tour':
507 hand.tourNo = self.tinfo['tourNo']
508 hand.buyinCurrency = self.tinfo['buyinCurrency']
509 hand.buyin = self.tinfo['buyin']
510 hand.fee = self.tinfo['fee']
511 hand.tablename = f"{self.info['table_name']}"
514 def readPlayerStacks(self, hand):
515 """
516 Extracts player information from the hand text and populates the Hand object with
517 player stacks and winnings.
519 Args:
520 hand (Hand): Hand object to populate with player information.
522 Raises:
523 FpdbParseError: If there are fewer than 2 players in the hand.
525 Returns:
526 None
527 """
528 # Initialize dictionaries and regex pattern
529 self.playerWinnings, plist = {}, {}
530 m = self.re_PlayerInfo.finditer(hand.handText)
532 # Extract player information from regex matches
533 for a in m:
534 ag = a.groupdict()
535 # Create a dictionary entry for the player with their seat, stack, winnings,
536 # and sitout status
537 plist[a.group('PNAME')] = [int(a.group('SEAT')), self.clearMoneyString(a.group('CASH')),
538 self.clearMoneyString(a.group('WIN')), False]
539 # If the player is the button, set the button position in the Hand object
540 if a.group('BUTTONPOS') == '1':
541 hand.buttonpos = int(a.group('SEAT'))
543 # Ensure there are at least 2 players in the hand
544 if len(plist)<=1:
545 # Hand cancelled
546 log.error(f"iPokerToFpdb.readPlayerStacks: '{hand.handid}'")
547 raise FpdbParseError
549 # Add remaining players to the Hand object and playerWinnings dictionary if they won
550 for pname in plist:
551 seat, stack, win, sitout = plist[pname]
552 hand.addPlayer(seat, pname, stack, None, sitout)
553 if Decimal(win) != 0:
554 self.playerWinnings[pname] = win
556 # Set the maxseats attribute in the Hand object if it is not already set
557 if hand.maxseats is None:
558 if self.info['type'] == 'tour' and self.maxseats==0:
559 hand.maxseats = self.guessMaxSeats(hand)
560 self.maxseats = hand.maxseats
561 elif self.info['type'] == 'tour':
562 hand.maxseats = self.maxseats
563 else:
564 hand.maxseats = None
567 def markStreets(self, hand):
568 """
569 Extracts the rounds of a hand and adds them to the Hand object
571 Args:
572 hand (Hand): the Hand object to which the rounds will be added
573 """
574 if hand.gametype['base'] in ('hold'):
575 # Extract rounds for hold'em game
576 m = re.search(
577 r'(?P<PREFLOP>.+(?=<round no="2">)|.+)' # Preflop round
578 r'(<round no="2">(?P<FLOP>.+(?=<round no="3">)|.+))?' # Flop round
579 r'(<round no="3">(?P<TURN>.+(?=<round no="4">)|.+))?' # Turn round
580 r'(<round no="4">(?P<RIVER>.+))?', # River round
581 hand.handText, re.DOTALL
582 )
583 elif hand.gametype['base'] in ('stud'):
584 # Extract rounds for stud game
585 if hand.gametype['category'] == '5_studhi':
586 # Extract rounds for 5-card stud high game
587 m = re.search(
588 r'(?P<ANTES>.+(?=<round no="2">)|.+)' # Antes round
589 r'(<round no="2">(?P<SECOND>.+(?=<round no="3">)|.+))?' # Second round
590 r'(<round no="3">(?P<THIRD>.+(?=<round no="4">)|.+))?' # Third round
591 r'(<round no="4">(?P<FOURTH>.+(?=<round no="5">)|.+))?' # Fourth round
592 r'(<round no="5">(?P<FIFTH>.+))?', # Fifth round
593 hand.handText, re.DOTALL
594 )
595 else:
596 # Extract rounds for 7-card stud high/low game
597 m = re.search(
598 r'(?P<ANTES>.+(?=<round no="2">)|.+)' # Antes round
599 r'(<round no="2">(?P<THIRD>.+(?=<round no="3">)|.+))?' # Third round
600 r'(<round no="3">(?P<FOURTH>.+(?=<round no="4">)|.+))?' # Fourth round
601 r'(<round no="4">(?P<FIFTH>.+(?=<round no="5">)|.+))?' # Fifth round
602 r'(<round no="5">(?P<SIXTH>.+(?=<round no="6">)|.+))?' # Sixth round
603 r'(<round no="6">(?P<SEVENTH>.+))?', # Seventh round
604 hand.handText, re.DOTALL
605 )
606 hand.addStreets(m)
609 def readCommunityCards(self, hand, street):
610 """
611 Parse the community cards for the given street and set them in the hand object.
613 Args:
614 hand (Hand): The hand object.
615 street (str): The street to parse the community cards for.
617 Raises:
618 FpdbParseError: If the community cards could not be parsed.
620 Returns:
621 None
622 """
623 cards = []
624 # Search for the board cards in the hand's streets
625 if m := self.re_Board.search(hand.streets[street]):
626 # Split the card string into a list of cards
627 cards = m.group('CARDS').strip().split(' ')
628 # Format the cards
629 cards = [c[1:].replace('10', 'T') + c[0].lower() for c in cards]
630 # Set the community cards in the hand object
631 hand.setCommunityCards(street, cards)
632 else:
633 # Log an error if the board cards could not be found
634 log.error(f"iPokerToFpdb.readCommunityCards: '{hand.handid}'")
635 # Raise an exception
636 raise FpdbParseError
639 def readAntes(self, hand):
640 """
641 Reads the antes for each player in the given hand.
643 Args:
644 hand (Hand): The hand to read the antes from.
646 Returns:
647 None
648 """
649 # Find all the antes in the hand text using a regular expression
650 m = self.re_Action.finditer(hand.handText)
652 # Loop through each ante found
653 for a in m:
654 # If the ante is of type 15, add it to the hand
655 if a.group('ATYPE') == '15':
656 hand.addAnte(a.group('PNAME'), self.clearMoneyString(a.group('BET')))
659 def readBringIn(self, hand):
660 """
661 Reads the bring-in for a hand and sets the small blind (sb) and big blind (bb) values if they are not already set.
663 Args:
664 hand (Hand): The hand object for which to read the bring-in.
666 Returns:
667 None
668 """
669 # If sb and bb are not already set, set them to default values
670 if hand.gametype['sb'] is None and hand.gametype['bb'] is None:
671 hand.gametype['sb'] = "1" # default small blind value
672 hand.gametype['bb'] = "2" # default big blind value
675 def readBlinds(self, hand):
676 """
677 Parses hand history to extract blind information for each player in the hand.
679 :param hand: Hand object containing the hand history.
680 :type hand: Hand
681 """
682 # Find all actions in the preflop street
683 for a in self.re_Action.finditer(hand.streets['PREFLOP']):
684 if a.group('ATYPE') == '1':
685 # If the action is a small blind, add it to the hand object
686 hand.addBlind(a.group('PNAME'), 'small blind', self.clearMoneyString(a.group('BET')))
687 # If the small blind amount is not already set, set it
688 if not hand.gametype['sb']:
689 hand.gametype['sb'] = self.clearMoneyString(a.group('BET'))
691 # Find all actions in the preflop street
692 m = self.re_Action.finditer(hand.streets['PREFLOP'])
693 # Create a dictionary to store big blind information for each player
694 blinds = {
695 int(a.group('ACT')): a.groupdict()
696 for a in m
697 if a.group('ATYPE') == '2'
698 }
699 # Iterate over the big blind information and add it to the hand object
700 for b in sorted(list(blinds.keys())):
701 type = 'big blind'
702 blind = blinds[b]
703 # If the big blind amount is not already set, set it
704 if not hand.gametype['bb']:
705 hand.gametype['bb'] = self.clearMoneyString(blind['BET'])
706 # If the small blind amount is set, check if the amount is bigger than the small blind amount
707 elif hand.gametype['sb']:
708 bb = Decimal(hand.gametype['bb'])
709 amount = Decimal(self.clearMoneyString(blind['BET']))
710 if amount > bb:
711 type = 'both'
712 # Add the big blind to the hand object
713 hand.addBlind(blind['PNAME'], type, self.clearMoneyString(blind['BET']))
714 # Fix tournament blinds if necessary
715 self.fixTourBlinds(hand)
718 def fixTourBlinds(self, hand):
719 """
720 Fix tournament blinds if small blind is missing or sb/bb is all-in.
722 :param hand: A dictionary containing the game type information.
723 :return: None
724 """
725 # FIXME
726 # The following should only trigger when a small blind is missing in a tournament, or the sb/bb is ALL_IN
727 # see http://sourceforge.net/apps/mantisbt/fpdb/view.php?id=115
728 if hand.gametype['type'] != 'tour':
729 return
730 if hand.gametype['sb'] is None and hand.gametype['bb'] is None:
731 hand.gametype['sb'] = "1"
732 hand.gametype['bb'] = "2"
733 elif hand.gametype['sb'] is None:
734 hand.gametype['sb'] = str(int(old_div(Decimal(hand.gametype['bb']),2)))
735 elif hand.gametype['bb'] is None:
736 hand.gametype['bb'] = str(int(Decimal(hand.gametype['sb']))*2)
737 if int(old_div(Decimal(hand.gametype['bb']),2)) != int(Decimal(hand.gametype['sb'])):
738 if int(old_div(Decimal(hand.gametype['bb']),2)) < int(Decimal(hand.gametype['sb'])):
739 hand.gametype['bb'] = str(int(Decimal(hand.gametype['sb']))*2)
740 else:
741 hand.gametype['sb'] = str(int((Decimal(hand.gametype['bb']))//2))
744 def readButton(self, hand):
745 # Found in re_Player
746 pass
748 def readHoleCards(self, hand):
749 """
750 Parses a Hand object to extract hole card information for each player on each street.
751 Adds the hole card information to the Hand object.
753 Args:
754 hand: Hand object to extract hole card information from
756 Returns:
757 None
758 """
760 # streets PREFLOP, PREDRAW, and THIRD are special cases beacause we need to grab hero's cards
761 for street in ('PREFLOP', 'DEAL'):
762 if street in hand.streets.keys():
763 # Find all instances of hero's cards in the street and add them to the Hand object
764 m = self.re_HeroCards.finditer(hand.streets[street])
765 for found in m:
766 player = found.group('PNAME')
767 cards = found.group('CARDS').split(' ')
768 cards = [c[1:].replace('10', 'T') + c[0].lower().replace('x', '') for c in cards]
769 if player == self.hero and cards[0]:
770 hand.hero = player
771 hand.addHoleCards(street, player, closed=cards, shown=True, mucked=False, dealt=True)
773 # Go through each street in the Hand object and add hole card information for each player
774 for street, text in list(hand.streets.items()):
775 if not text or street in ('PREFLOP', 'DEAL'):
776 continue # already done these
777 m = self.re_HeroCards.finditer(hand.streets[street])
778 for found in m:
779 player = found.group('PNAME')
780 if player is not None:
781 cards = found.group('CARDS').split(' ')
783 # Handle special case where hero is not the player and it's the seventh street in a stud game
784 if street == 'SEVENTH' and self.hero != player:
785 newcards = []
786 oldcards = [c[1:].replace('10', 'T') + c[0].lower() for c in cards if c[0].lower()!='x']
787 else:
788 newcards = [c[1:].replace('10', 'T') + c[0].lower() for c in cards if c[0].lower()!='x']
789 oldcards = []
791 # Handle special case where hero is the player and it's the third street in a stud game
792 if street == 'THIRD' and len(newcards) == 3 and self.hero == player:
793 hand.hero = player
794 hand.dealt.add(player)
795 hand.addHoleCards(
796 street,
797 player,
798 closed=newcards[:2],
799 open=[newcards[2]],
800 shown=True,
801 mucked=False,
802 dealt=False,
803 )
805 # Handle special case where hero is the player and it's the second street in a stud game
806 elif street == 'SECOND' and len(newcards) == 2 and self.hero == player:
807 hand.hero = player
808 hand.dealt.add(player)
809 hand.addHoleCards(street, player, closed=[newcards[0]], open=[newcards[1]], shown=True, mucked=False, dealt=False)
811 # Handle all other cases where hole card information needs to be added to the Hand object
812 else:
813 hand.addHoleCards(street, player, open=newcards, closed=oldcards, shown=True, mucked=False, dealt=False)
816 def readAction(self, hand, street):
817 """
818 Extracts actions from a hand and adds them to the corresponding street in a Hand object.
820 Args:
821 hand (Hand): Hand object to which the actions will be added.
822 street (int): Number of the street in the hand (0 for preflop, 1 for flop, etc.).
824 Returns:
825 None
826 """
827 # HH format doesn't actually print the actions in order!
828 m = self.re_Action.finditer(hand.streets[street])
829 actions = {int(a.group('ACT')): a.groupdict() for a in m}
831 # Add each action to the corresponding method of the Hand object.
832 # atype is the action type (0 for fold, 4 for check, etc.).
833 for a in sorted(list(actions.keys())):
834 action = actions[a]
835 atype = action['ATYPE']
836 player = action['PNAME']
837 bet = self.clearMoneyString(action['BET'])
839 if atype == '0':
840 hand.addFold(street, player)
841 elif atype == '4':
842 hand.addCheck(street, player)
843 elif atype == '3':
844 hand.addCall(street, player, bet)
845 elif atype == '23': # Raise to
846 hand.addRaiseTo(street, player, bet)
847 elif atype == '6': # Raise by
848 # This is only a guess
849 hand.addRaiseBy(street, player, bet)
850 elif atype == '5':
851 hand.addBet(street, player, bet)
852 elif atype == '16': # BringIn
853 hand.addBringIn(player, bet)
854 elif atype == '7':
855 hand.addAllIn(street, player, bet)
856 elif atype == '15': # Ante
857 pass # Antes dealt with in readAntes
858 elif atype in ['1', '2', '8']: # sb/bb/no action this hand (joined table)
859 pass
860 elif atype == '9': # FIXME: Sitting out
861 hand.addFold(street, player)
862 else:
863 log.error(
864 # Log an error for unimplemented actions
865 ("DEBUG:")
866 + " "
867 + f"Unimplemented readAction: '{action['PNAME']}' '{action['ATYPE']}'"
868 )
871 def readShowdownActions(self, hand):
872 # Cards lines contain cards
873 pass
875 def readCollectPot(self, hand):
876 """
877 Sets the uncalled bets for the given hand and adds collect pot actions for each player with non-zero winnings.
879 Args:
880 hand: The Hand object to update with the collect pot actions.
881 """
882 hand.setUncalledBets(self.uncalledbets)
883 for pname, pot in list(self.playerWinnings.items()):
884 hand.addCollectPot(player=pname, pot=self.clearMoneyString(pot))
885 # add collect pot action for player with non-zero winnings
888 def readShownCards(self, hand):
889 # Cards lines contain cards
890 pass
892 @staticmethod
893 def getTableTitleRe(type, table_name=None, tournament=None, table_number=None):
894 """
895 Generate a regular expression pattern for table title.
897 Args:
898 - type: A string value.
899 - table_name: A string value representing the table name.
900 - tournament: A string value representing the tournament.
901 - table_number: An integer value representing the table number.
903 Returns:
904 - A string value representing the regular expression pattern for table title.
905 """
906 # Log the input parameters
907 log.info(
908 f"iPoker getTableTitleRe: table_name='{table_name}' tournament='{tournament}' table_number='{table_number}'"
909 )
911 # Generate the regex pattern based on the input parameters
912 regex = f"{table_name}"
914 if type == "tour":
915 regex = f"([^\(]+)\s{table_number}"
918 print(regex)
920 return regex
921 elif table_name.find('(No DP),') != -1:
922 regex = table_name.split('(No DP),')[0]
923 elif table_name.find(',') != -1:
924 regex = table_name.split(',')[0]
925 else:
926 regex = table_name.split(' ')[0]
928 # Log the generated regex pattern and return it
929 log.info(f"iPoker getTableTitleRe: returns: '{regex}'")
930 return regex