Coverage for iPokerToFpdb.py: 0%

401 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-09-27 18:50 +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 

19 

20######################################################################## 

21 

22#import L10n 

23#_ = L10n.get_translation() 

24 

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 

45 

46import sys 

47from HandHistoryConverter import * 

48from decimal_wrapper import Decimal 

49from TourneySummary import * 

50 

51class iPoker(HandHistoryConverter): 

52 """ 

53 A class for converting iPoker hand history files to the PokerTH format. 

54 """ 

55 

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 

62 

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 

68 

69 } 

70 

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 } 

93 

94 currencies = { u'€':'EUR', '$':'USD', '':'T$', u'£':'GBP', 'RSD': 'RSD', 'kr': 'SEK'} 

95 

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 } 

119 

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 } 

146 

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} 

148 

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') 

207 

208 def compilePlayerRegexs(self, hand): 

209 pass 

210 

211 def playerNameFromSeatNo(self, seatNo, hand): 

212 """ 

213 Returns the name of the player from the given seat number. 

214 

215 This special function is required because Carbon Poker records actions by seat number, not by the player's name. 

216 

217 Args: 

218 seatNo (int): The seat number of the player. 

219 hand (Hand): The hand instance containing the players information. 

220 

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] 

227 

228 

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 ] 

246 

247 def parseHeader(self, handText, whole_file): 

248 """ 

249 Parses the header of a hand history and returns the game type. 

250 

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. 

254 

255 Returns: 

256 str: The game type, if it can be determined from the header or the whole file. 

257 None otherwise. 

258 

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 

276 

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'] 

311 

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 

320 

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 

338 

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') 

360 

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') 

371 

372 

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']) 

378 

379 if 'winnings' not in self.tinfo: 

380 self.tinfo['winnings'] = 0 # Initialize 'winnings' if it doesn't exist yet 

381 

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'])))) 

384 

385 

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' 

391 

392 

393 if mg['BIAMT'] and self.re_FPP.match(mg['BIAMT']): 

394 self.tinfo['buyinCurrency'] = 'FPP' 

395 

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: 

405 

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'])))) 

408 

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'] 

425 

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 

436 

437 return self.info 

438 

439 def readHandInfo(self, hand): 

440 """ 

441 Parses the hand text and extracts relevant information about the hand. 

442 

443 Args: 

444 hand: An instance of the Hand class that represents the hand being parsed. 

445 

446 Raises: 

447 FpdbParseError: If the hand text cannot be parsed. 

448 

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 

459 

460 # Extract the relevant information from the match object 

461 mg = m.groupdict() 

462 

463 

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']) 

468 

469 

470 # Set the hand ID for the hand 

471 hand.handid = m.group('HID') 

472 

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) 

504 

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']}" 

512 

513 

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. 

518 

519 Args: 

520 hand (Hand): Hand object to populate with player information. 

521 

522 Raises: 

523 FpdbParseError: If there are fewer than 2 players in the hand. 

524 

525 Returns: 

526 None 

527 """ 

528 # Initialize dictionaries and regex pattern 

529 self.playerWinnings, plist = {}, {} 

530 m = self.re_PlayerInfo.finditer(hand.handText) 

531 

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')) 

542 

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 

548 

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 

555 

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 

565 

566 

567 def markStreets(self, hand): 

568 """ 

569 Extracts the rounds of a hand and adds them to the Hand object 

570 

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) 

607 

608 

609 def readCommunityCards(self, hand, street): 

610 """ 

611 Parse the community cards for the given street and set them in the hand object. 

612 

613 Args: 

614 hand (Hand): The hand object. 

615 street (str): The street to parse the community cards for. 

616 

617 Raises: 

618 FpdbParseError: If the community cards could not be parsed. 

619 

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 

637 

638 

639 def readAntes(self, hand): 

640 """ 

641 Reads the antes for each player in the given hand. 

642 

643 Args: 

644 hand (Hand): The hand to read the antes from. 

645 

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) 

651 

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'))) 

657 

658 

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. 

662 

663 Args: 

664 hand (Hand): The hand object for which to read the bring-in. 

665 

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 

673 

674 

675 def readBlinds(self, hand): 

676 """ 

677 Parses hand history to extract blind information for each player in the hand. 

678 

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')) 

690 

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) 

716 

717 

718 def fixTourBlinds(self, hand): 

719 """ 

720 Fix tournament blinds if small blind is missing or sb/bb is all-in. 

721 

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)) 

742 

743 

744 def readButton(self, hand): 

745 # Found in re_Player 

746 pass 

747 

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. 

752 

753 Args: 

754 hand: Hand object to extract hole card information from 

755 

756 Returns: 

757 None 

758 """ 

759 

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) 

772 

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(' ') 

782 

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 = [] 

790 

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 ) 

804 

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) 

810 

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) 

814 

815 

816 def readAction(self, hand, street): 

817 """ 

818 Extracts actions from a hand and adds them to the corresponding street in a Hand object. 

819 

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.). 

823 

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} 

830 

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']) 

838 

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 ) 

869 

870 

871 def readShowdownActions(self, hand): 

872 # Cards lines contain cards 

873 pass 

874 

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. 

878 

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 

886 

887 

888 def readShownCards(self, hand): 

889 # Cards lines contain cards 

890 pass 

891 

892 @staticmethod 

893 def getTableTitleRe(type, table_name=None, tournament=None, table_number=None): 

894 """ 

895 Generate a regular expression pattern for table title. 

896 

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. 

902 

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 ) 

910 

911 # Generate the regex pattern based on the input parameters 

912 regex = f"{table_name}" 

913 

914 if type == "tour": 

915 regex = f"([^\(]+)\s{table_number}" 

916 

917 

918 print(regex) 

919 

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] 

927 

928 # Log the generated regex pattern and return it 

929 log.info(f"iPoker getTableTitleRe: returns: '{regex}'") 

930 return regex 

931