Coverage for iPokerToFpdb.py: 0%

406 statements  

« prev     ^ index     » next       coverage.py v7.6.4, created at 2024-11-07 02:19 +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 

46from HandHistoryConverter import HandHistoryConverter, FpdbParseError, FpdbHandPartial 

47from decimal import Decimal 

48import re 

49import logging 

50import datetime 

51 

52 

53log = logging.getLogger("parser") 

54 

55 

56class iPoker(HandHistoryConverter): 

57 """ 

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

59 """ 

60 

61 sitename = "iPoker" 

62 filetype = "text" 

63 codepage = ("utf8", "cp1252") 

64 siteId = 14 

65 copyGameHeader = True # NOTE: Not sure if this is necessary yet. The file is xml so its likely 

66 summaryInFile = True 

67 

68 substitutions = { 

69 "LS": r"\$|\xe2\x82\xac|\xe2\u201a\xac|\u20ac|\xc2\xa3|\£|RSD|kr|", # Used to remove currency symbols from the hand history 

70 "PLYR": r"(?P<PNAME>[^\"]+)", # Regex pattern for matching player names 

71 "NUM": r"(.,\d+)|(\d+)", # Regex pattern for matching numbers 

72 "NUM2": r"\b((?:\d{1,3}(?:\s\d{3})*)|(?:\d+))\b", # Regex pattern for matching numbers with spaces 

73 } 

74 

75 limits = { 

76 "No limit": "nl", 

77 "Pot limit": "pl", 

78 "Limit": "fl", 

79 "NL": "nl", 

80 "SL": "nl", 

81 "БЛ": "nl", 

82 "PL": "pl", 

83 "LP": "pl", 

84 "L": "fl", 

85 "LZ": "nl", 

86 } 

87 games = { # base, category 

88 "7 Card Stud": ("stud", "studhi"), 

89 "7 Card Stud Hi-Lo": ("stud", "studhilo"), 

90 "7 Card Stud HiLow": ("stud", "studhilo"), 

91 "5 Card Stud": ("stud", "5_studhi"), 

92 "Holdem": ("hold", "holdem"), 

93 "Six Plus Holdem": ("hold", "6_holdem"), 

94 "Omaha": ("hold", "omahahi"), 

95 "Omaha Hi-Lo": ("hold", "omahahilo"), 

96 "Omaha HiLow": ("hold", "omahahilo"), 

97 } 

98 

99 currencies = {"€": "EUR", "$": "USD", "": "T$", "£": "GBP", "RSD": "RSD", "kr": "SEK"} 

100 

101 # translations from captured groups to fpdb info strings 

102 Lim_Blinds = { 

103 "0.04": ("0.01", "0.02"), 

104 "0.08": ("0.02", "0.04"), 

105 "0.10": ("0.02", "0.05"), 

106 "0.20": ("0.05", "0.10"), 

107 "0.40": ("0.10", "0.20"), 

108 "0.50": ("0.10", "0.25"), 

109 "1.00": ("0.25", "0.50"), 

110 "1": ("0.25", "0.50"), 

111 "2.00": ("0.50", "1.00"), 

112 "2": ("0.50", "1.00"), 

113 "4.00": ("1.00", "2.00"), 

114 "4": ("1.00", "2.00"), 

115 "6.00": ("1.00", "3.00"), 

116 "6": ("1.00", "3.00"), 

117 "8.00": ("2.00", "4.00"), 

118 "8": ("2.00", "4.00"), 

119 "10.00": ("2.00", "5.00"), 

120 "10": ("2.00", "5.00"), 

121 "20.00": ("5.00", "10.00"), 

122 "20": ("5.00", "10.00"), 

123 "30.00": ("10.00", "15.00"), 

124 "30": ("10.00", "15.00"), 

125 "40.00": ("10.00", "20.00"), 

126 "40": ("10.00", "20.00"), 

127 "60.00": ("15.00", "30.00"), 

128 "60": ("15.00", "30.00"), 

129 "80.00": ("20.00", "40.00"), 

130 "80": ("20.00", "40.00"), 

131 "100.00": ("25.00", "50.00"), 

132 "100": ("25.00", "50.00"), 

133 "150.00": ("50.00", "75.00"), 

134 "150": ("50.00", "75.00"), 

135 "200.00": ("50.00", "100.00"), 

136 "200": ("50.00", "100.00"), 

137 "400.00": ("100.00", "200.00"), 

138 "400": ("100.00", "200.00"), 

139 "800.00": ("200.00", "400.00"), 

140 "800": ("200.00", "400.00"), 

141 "1000.00": ("250.00", "500.00"), 

142 "1000": ("250.00", "500.00"), 

143 "2000.00": ("500.00", "1000.00"), 

144 "2000": ("500.00", "1000.00"), 

145 } 

146 

147 # translations from captured groups to fpdb info strings 

148 Lim_Blinds = { 

149 "0.04": ("0.01", "0.02"), 

150 "0.08": ("0.02", "0.04"), 

151 "0.10": ("0.02", "0.05"), 

152 "0.20": ("0.05", "0.10"), 

153 "0.40": ("0.10", "0.20"), 

154 "0.50": ("0.10", "0.25"), 

155 "1.00": ("0.25", "0.50"), 

156 "1": ("0.25", "0.50"), 

157 "2.00": ("0.50", "1.00"), 

158 "2": ("0.50", "1.00"), 

159 "4.00": ("1.00", "2.00"), 

160 "4": ("1.00", "2.00"), 

161 "6.00": ("1.50", "3.00"), 

162 "6": ("1.50", "3.00"), 

163 "8.00": ("2.00", "4.00"), 

164 "8": ("2.00", "4.00"), 

165 "10.00": ("2.50", "5.00"), 

166 "10": ("2.50", "5.00"), 

167 "20.00": ("5.00", "10.00"), 

168 "20": ("5.00", "10.00"), 

169 "30.00": ("7.50", "15.00"), 

170 "30": ("7.50", "15.00"), 

171 "40.00": ("10.00", "20.00"), 

172 "40": ("10.00", "20.00"), 

173 "60.00": ("15.00", "30.00"), 

174 "60": ("15.00", "30.00"), 

175 "80.00": ("20.00", "40.00"), 

176 "80": ("20.00", "40.00"), 

177 "100.00": ("25.00", "50.00"), 

178 "100": ("25.00", "50.00"), 

179 "150.00": ("50.00", "75.00"), 

180 "150": ("50.00", "75.00"), 

181 "200.00": ("50.00", "100.00"), 

182 "200": ("50.00", "100.00"), 

183 "300.00": ("75.00", "150.00"), 

184 "300": ("75.00", "150.00"), 

185 "400.00": ("100.00", "200.00"), 

186 "400": ("100.00", "200.00"), 

187 "600.00": ("150.00", "300.00"), 

188 "600": ("150.00", "300.00"), 

189 "800.00": ("200.00", "400.00"), 

190 "800": ("200.00", "400.00"), 

191 "1000.00": ("250.00", "500.00"), 

192 "1000": ("250.00", "500.00"), 

193 "2000.00": ("500.00", "1000.00"), 

194 "2000": ("500.00", "1000.00"), 

195 "4000.00": ("1000.00", "2000.00"), 

196 "4000": ("1000.00", "2000.00"), 

197 } 

198 

199 months = { 

200 "Jan": 1, 

201 "Feb": 2, 

202 "Mar": 3, 

203 "Apr": 4, 

204 "May": 5, 

205 "Jun": 6, 

206 "Jul": 7, 

207 "Aug": 8, 

208 "Sep": 9, 

209 "Oct": 10, 

210 "Nov": 11, 

211 "Dec": 12, 

212 } 

213 

214 # Static regexes 

215 re_client = re.compile(r"<client_version>(?P<CLIENT>.*?)</client_version>") 

216 # re_Identify = re.compile(u"""<\?xml version=\"1\.0\" encoding=\"utf-8\"\?>""") 

217 re_Identify = re.compile("""<game gamecode=\"\d+\">""") 

218 re_SplitHands = re.compile(r"</game>") 

219 re_TailSplitHands = re.compile(r"(</game>)") 

220 re_GameInfo = re.compile( 

221 r""" 

222 <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)?.+?) 

223 (\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+? 

224 <tablename>(?P<TABLE>.+)?</tablename>\s+? 

225 (<(tablecurrency|tournamentcurrency)>(?P<TABLECURRENCY>.*)</(tablecurrency|tournamentcurrency)>\s+?)? 

226 (<smallblind>.+</smallblind>\s+?)? 

227 (<bigblind>.+</bigblind>\s+?)? 

228 <duration>.+</duration>\s+? 

229 <gamecount>.+</gamecount>\s+? 

230 <startdate>.+</startdate>\s+? 

231 <currency>(?P<CURRENCY>.+)?</currency>\s+? 

232 <nickname>(?P<HERO>.+)?</nickname> 

233 """ 

234 % substitutions, 

235 re.MULTILINE | re.VERBOSE, 

236 ) 

237 re_GameInfoTrny = re.compile( 

238 r""" 

239 (?:(<tour(?:nament)?code>(?P<TOURNO>\d+)</tour(?:nament)?code>))| 

240 (?:(<tournamentname>(?P<NAME>[^<]*)</tournamentname>))| 

241 (?:(<rewarddrawn>(?P<REWARD>[%(NUM2)s%(LS)s]+)</rewarddrawn>))|  

242 (?:(<place>(?P<PLACE>.+?)</place>))| 

243 (?:(<buyin>(?P<BIAMT>[%(NUM2)s%(LS)s]+)\s\+\s)?(?P<BIRAKE>[%(NUM2)s%(LS)s]+)\s\+\s(?P<BIRAKE2>[%(NUM2)s%(LS)s]+)</buyin>)| 

244 (?:(<totalbuyin>(?P<TOTBUYIN>.*)</totalbuyin>))| 

245 (?:(<win>(%(LS)s)?(?P<WIN>.+?|[%(NUM2)s%(LS)s]+)</win>)) 

246 """ 

247 % substitutions, 

248 re.VERBOSE, 

249 ) 

250 re_GameInfoTrny2 = re.compile( 

251 r""" 

252 (?:(<tour(?:nament)?code>(?P<TOURNO>\d+)</tour(?:nament)?code>))| 

253 (?:(<tournamentname>(?P<NAME>[^<]*)</tournamentname>))| 

254 (?:(<place>(?P<PLACE>.+?)</place>))| 

255 (?:(<buyin>(?P<BIAMT>[%(NUM2)s%(LS)s]+)\s\+\s)?(?P<BIRAKE>[%(NUM2)s%(LS)s]+)</buyin>)| 

256 (?:(<totalbuyin>(?P<TOTBUYIN>[%(NUM2)s%(LS)s]+)</totalbuyin>))| 

257 (?:(<win>(%(LS)s)?(?P<WIN>.+?|[%(NUM2)s%(LS)s]+)</win>)) 

258 """ 

259 % substitutions, 

260 re.VERBOSE, 

261 ) 

262 re_Buyin = re.compile(r"""(?:(<totalbuyin>(?P<TOTBUYIN>.*)</totalbuyin>))""", re.VERBOSE) 

263 re_TotalBuyin = re.compile( 

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

265 % substitutions, 

266 re.VERBOSE, 

267 ) 

268 re_HandInfo = re.compile( 

269 r'code="(?P<HID>[0-9]+)">\s*?<general>\s*?<startdate>(?P<DATETIME>[\.a-zA-Z-/: 0-9]+)</startdate>', re.MULTILINE 

270 ) 

271 re_PlayerInfo = re.compile( 

272 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*/>' 

273 % substitutions, 

274 re.MULTILINE, 

275 ) 

276 re_Board = re.compile( 

277 r'<cards( (type="(?P<STREET>Flop|Turn|River)"|player=""))+>(?P<CARDS>.+?)</cards>', re.MULTILINE 

278 ) 

279 re_EndOfHand = re.compile(r'<round id="END_OF_GAME"', re.MULTILINE) 

280 re_Hero = re.compile(r"<nickname>(?P<HERO>.+)</nickname>", re.MULTILINE) 

281 re_HeroCards = re.compile( 

282 r'<cards( (type="(Pocket|Second\sStreet|Third\sStreet|Fourth\sStreet|Fifth\sStreet|Sixth\sStreet|River)"|player="%(PLYR)s"))+>(?P<CARDS>.+?)</cards>' 

283 % substitutions, 

284 re.MULTILINE, 

285 ) 

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

287 re_Action = re.compile( 

288 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)\")+/>" 

289 % substitutions, 

290 re.MULTILINE, 

291 ) 

292 re_SitsOut = re.compile(r'<event sequence="[0-9]+" type="SIT_OUT" player="(?P<PSEAT>[0-9])"/>', re.MULTILINE) 

293 re_DateTime1 = re.compile( 

294 """(?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]+))?""", 

295 re.MULTILINE, 

296 ) 

297 re_DateTime2 = re.compile( 

298 """(?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]+))?""", 

299 re.MULTILINE, 

300 ) 

301 re_DateTime3 = re.compile( 

302 """(?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]+))?""", 

303 re.MULTILINE, 

304 ) 

305 re_MaxSeats = re.compile(r"<tablesize>(?P<SEATS>[0-9]+)</tablesize>", re.MULTILINE) 

306 re_tablenamemtt = re.compile(r"<tablename>(?P<TABLET>.+?)</tablename>", re.MULTILINE) 

307 re_TourNo = re.compile(r"(?P<TOURNO>\d+)$", re.MULTILINE) 

308 re_non_decimal = re.compile(r"[^\d.,]+") 

309 re_Partial = re.compile("<startdate>", re.MULTILINE) 

310 re_UncalledBets = re.compile("<uncalled_bet_enabled>true<\/uncalled_bet_enabled>") 

311 re_ClientVersion = re.compile("<client_version>(?P<VERSION>[.\d]+)</client_version>") 

312 re_FPP = re.compile(r"Pts\s") 

313 

314 def compilePlayerRegexs(self, hand): 

315 pass 

316 

317 def playerNameFromSeatNo(self, seatNo, hand): 

318 """ 

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

320 

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

322 

323 Args: 

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

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

326 

327 Returns: 

328 str: The name of the player from the given seat number. 

329 """ 

330 for p in hand.players: 

331 if p[0] == int(seatNo): 

332 return p[1] 

333 

334 def readSupportedGames(self): 

335 """ 

336 Return a list of supported games, where each game is a list of strings. 

337 The first element of each game list is either "ring" or "tour". 

338 The second element of each game list is either "stud" or "hold". 

339 The third element of each game list is either "nl", "pl", or "fl". 

340 """ 

341 return [ 

342 ["ring", "stud", "fl"], # ring game with stud format and fixed limit 

343 ["ring", "hold", "nl"], # ring game with hold format and no limit 

344 ["ring", "hold", "pl"], # ring game with hold format and pot limit 

345 ["ring", "hold", "fl"], # ring game with hold format and fixed limit 

346 ["tour", "hold", "nl"], # tournament with hold format and no limit 

347 ["tour", "hold", "pl"], # tournament with hold format and pot limit 

348 ["tour", "hold", "fl"], # tournament with hold format and fixed limit 

349 ["tour", "stud", "fl"], # tournament with stud format and fixed limit 

350 ] 

351 

352 def parseHeader(self, handText, whole_file): 

353 """ 

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

355 

356 Args: 

357 hand_text (str): The text containing the header of the hand history. 

358 whole_file (str): The entire text of the hand history. 

359 

360 Returns: 

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

362 None otherwise. 

363 

364 Raises: 

365 FpdbParseError: If the hand history is an iPoker hand lacking actions/starttime. 

366 FpdbHandPartial: If the hand history is an iPoker partial hand history without a start date. 

367 """ 

368 gametype = self.determineGameType(handText) 

369 if gametype is None: 

370 gametype = self.determineGameType(whole_file) 

371 if gametype is None: 

372 # Catch iPoker hands lacking actions/starttime and funnel them to partial 

373 if self.re_Partial.search(whole_file): 

374 tmp = handText[:200] 

375 log.error(f"iPokerToFpdb.determineGameType: '{tmp}'") 

376 raise FpdbParseError 

377 else: 

378 message = "No startdate" 

379 raise FpdbHandPartial(f"iPoker partial hand history: {message}") 

380 return gametype 

381 

382 def determineGameType(self, handText): 

383 """ 

384 Given a hand history, extract information about the type of game being played. 

385 """ 

386 m = self.re_GameInfo.search(handText) 

387 if not m: 

388 return None 

389 m2 = self.re_MaxSeats.search(handText) 

390 m3 = self.re_tablenamemtt.search(handText) 

391 self.info = {} 

392 mg = m.groupdict() 

393 mg2 = m2.groupdict() 

394 mg3 = m3.groupdict() 

395 tourney = False 

396 # print "DEBUG: m.groupdict(): %s" % mg 

397 if mg["GAME"][:2] == "LH": 

398 mg["CATEGORY"] = "Holdem" 

399 mg["LIMIT"] = "L" 

400 mg["BB"] = mg["LBB"] 

401 if "GAME" in mg: 

402 if mg["CATEGORY"] is None: 

403 (self.info["base"], self.info["category"]) = ("hold", "5_omahahi") 

404 else: 

405 (self.info["base"], self.info["category"]) = self.games[mg["CATEGORY"]] 

406 if "LIMIT" in mg: 

407 self.info["limitType"] = self.limits[mg["LIMIT"]] 

408 if "HERO" in mg: 

409 self.hero = mg["HERO"] 

410 if "SB" in mg: 

411 self.info["sb"] = self.clearMoneyString(mg["SB"]) 

412 if not mg["SB"]: 

413 tourney = True 

414 if "BB" in mg: 

415 self.info["bb"] = self.clearMoneyString(mg["BB"]) 

416 if "SEATS" in mg2: 

417 self.info["seats"] = mg2["SEATS"] 

418 

419 if self.re_UncalledBets.search(handText): 

420 self.uncalledbets = False 

421 else: 

422 self.uncalledbets = True 

423 if mv := self.re_ClientVersion.search(handText): 

424 major_version = mv.group("VERSION").split(".")[0] 

425 if int(major_version) >= 20: 

426 self.uncalledbets = False 

427 

428 if tourney: 

429 self.info["type"] = "tour" 

430 self.info["currency"] = "T$" 

431 if "TABLET" in mg3: 

432 self.info["table_name"] = mg3["TABLET"] 

433 print(mg3["TABLET"]) 

434 # FIXME: The sb/bb isn't listed in the game header. Fixing to 1/2 for now 

435 self.tinfo = {} # FIXME?: Full tourney info is only at the top of the file. After the 

436 # first hand in a file, there is no way for auto-import to 

437 # gather the info unless it reads the entire file every time. 

438 mt = self.re_TourNo.search(mg["TABLE"]) 

439 if mt: 

440 self.tinfo["tourNo"] = mt.group("TOURNO") 

441 else: 

442 tourNo = mg["TABLE"].split(",")[-1].strip().split(" ")[0] 

443 if tourNo.isdigit(): 

444 self.tinfo["tourNo"] = tourNo 

445 

446 self.tablename = "1" 

447 if not mg["CURRENCY"] or mg["CURRENCY"] == "fun": 

448 self.tinfo["buyinCurrency"] = "play" 

449 else: 

450 self.tinfo["buyinCurrency"] = mg["CURRENCY"] 

451 self.tinfo["buyin"] = 0 

452 self.tinfo["fee"] = 0 

453 client_match = self.re_client.search(handText) 

454 re_client_split = ".".join(client_match["CLIENT"].split(".")[:2]) 

455 if re_client_split == "23.5": # betclic fr 

456 matches = list(self.re_GameInfoTrny.finditer(handText)) 

457 if len(matches) > 0: 

458 mg["TOURNO"] = matches[0].group("TOURNO") 

459 mg["NAME"] = matches[1].group("NAME") 

460 mg["REWARD"] = matches[2].group("REWARD") 

461 mg["PLACE"] = matches[3].group("PLACE") 

462 mg["BIAMT"] = matches[4].group("BIAMT") 

463 mg["BIRAKE"] = matches[4].group("BIRAKE") 

464 mg["BIRAKE2"] = matches[4].group("BIRAKE2") 

465 mg["TOTBUYIN"] = matches[5].group("TOTBUYIN") 

466 mg["WIN"] = matches[6].group("WIN") 

467 

468 else: 

469 matches = list(self.re_GameInfoTrny2.finditer(handText)) 

470 if len(matches) > 0: 

471 mg["TOURNO"] = matches[0].group("TOURNO") 

472 mg["NAME"] = matches[1].group("NAME") 

473 mg["PLACE"] = matches[2].group("PLACE") 

474 mg["BIAMT"] = matches[3].group("BIAMT") 

475 mg["BIRAKE"] = matches[3].group("BIRAKE") 

476 mg["TOTBUYIN"] = matches[4].group("TOTBUYIN") 

477 mg["WIN"] = matches[5].group("WIN") 

478 

479 if mg["TOURNO"]: 

480 self.tinfo["tour_name"] = mg["NAME"] 

481 self.tinfo["tourNo"] = mg["TOURNO"] 

482 if mg["PLACE"] and mg["PLACE"] != "N/A": 

483 self.tinfo["rank"] = int(mg["PLACE"]) 

484 

485 if "winnings" not in self.tinfo: 

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

487 

488 if mg["WIN"] and mg["WIN"] != "N/A": 

489 self.tinfo["winnings"] += int( 

490 100 * Decimal(self.clearMoneyString(self.re_non_decimal.sub("", mg["WIN"]))) 

491 ) 

492 

493 if not mg["BIRAKE"]: # and mg['TOTBUYIN']: 

494 m3 = self.re_TotalBuyin.search(handText) 

495 if m3: 

496 mg = m3.groupdict() 

497 elif mg["BIAMT"]: 

498 mg["BIRAKE"] = "0" 

499 

500 if mg["BIAMT"] and self.re_FPP.match(mg["BIAMT"]): 

501 self.tinfo["buyinCurrency"] = "FPP" 

502 

503 if mg["BIRAKE"]: 

504 # FIXME: tournament no looks liek it is in the table name 

505 mg["BIRAKE"] = self.clearMoneyString(self.re_non_decimal.sub("", mg["BIRAKE"])) 

506 mg["BIAMT"] = self.clearMoneyString(self.re_non_decimal.sub("", mg["BIAMT"])) 

507 if re_client_split == "23.5": 

508 if mg["BIRAKE2"]: 

509 self.tinfo["buyin"] += int( 

510 100 * Decimal(self.clearMoneyString(self.re_non_decimal.sub("", mg["BIRAKE2"]))) 

511 ) 

512 m4 = self.re_Buyin.search(handText) 

513 if m4: 

514 self.tinfo["fee"] = int( 

515 100 * Decimal(self.clearMoneyString(self.re_non_decimal.sub("", mg["BIRAKE"]))) 

516 ) 

517 self.tinfo["buyin"] = int( 

518 100 * Decimal(self.clearMoneyString(self.re_non_decimal.sub("", mg["BIRAKE2"]))) 

519 ) 

520 

521 # FIXME: <place> and <win> not parsed at the moment. 

522 # NOTE: Both place and win can have the value N/A 

523 if self.tinfo["buyin"] == 0: 

524 self.tinfo["buyinCurrency"] = "FREE" 

525 if self.tinfo.get("tourNo") is None: 

526 log.error(("iPokerToFpdb.determineGameType: Could Not Parse tourNo")) 

527 raise FpdbParseError 

528 else: 

529 self.info["type"] = "ring" 

530 self.tablename = mg["TABLE"] 

531 if not mg["TABLECURRENCY"] and not mg["CURRENCY"]: 

532 self.info["currency"] = "play" 

533 elif not mg["TABLECURRENCY"]: 

534 self.info["currency"] = mg["CURRENCY"] 

535 else: 

536 self.info["currency"] = mg["TABLECURRENCY"] 

537 

538 if self.info["limitType"] == "fl" and self.info["bb"] is not None: 

539 try: 

540 self.info["sb"] = self.Lim_Blinds[self.clearMoneyString(mg["BB"])][0] 

541 self.info["bb"] = self.Lim_Blinds[self.clearMoneyString(mg["BB"])][1] 

542 except KeyError as e: 

543 tmp = handText[:200] 

544 log.error(f"iPokerToFpdb.determineGameType: Lim_Blinds has no lookup for '{mg['BB']}' - '{tmp}'") 

545 raise FpdbParseError from e 

546 

547 return self.info 

548 

549 def readHandInfo(self, hand): 

550 """ 

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

552 

553 Args: 

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

555 

556 Raises: 

557 FpdbParseError: If the hand text cannot be parsed. 

558 

559 Returns: 

560 None 

561 """ 

562 # Search for the relevant information in the hand text 

563 m = self.re_HandInfo.search(hand.handText) 

564 if m is None: 

565 # If the information cannot be found, log an error and raise an exception 

566 tmp = hand.handText[:200] 

567 log.error(f"iPokerToFpdb.readHandInfo: '{tmp}'") 

568 raise FpdbParseError 

569 

570 # Extract the relevant information from the match object 

571 m.groupdict() 

572 

573 # Set the table name and maximum number of seats for the hand 

574 hand.tablename = self.tablename 

575 if self.info["seats"]: 

576 hand.maxseats = int(self.info["seats"]) 

577 

578 # Set the hand ID for the hand 

579 hand.handid = m.group("HID") 

580 

581 # Parse the start time for the hand 

582 if m2 := self.re_DateTime1.search(m.group("DATETIME")): 

583 # If the datetime string matches the first format, parse it accordingly 

584 month = self.months[m2.group("M")] 

585 sec = m2.group("S") 

586 if m2.group("S") is None: 

587 sec = "00" 

588 datetimestr = f"{m2.group('Y')}/{month}/{m2.group('D')} {m2.group('H')}:{m2.group('MIN')}:{sec}" 

589 hand.startTime = datetime.datetime.strptime(datetimestr, "%Y/%m/%d %H:%M:%S") 

590 else: 

591 # If the datetime string does not match the first format, try the second format 

592 try: 

593 hand.startTime = datetime.datetime.strptime(m.group("DATETIME"), "%Y-%m-%d %H:%M:%S") 

594 except ValueError as e: 

595 # If the datetime string cannot be parsed, try the third format 

596 if date_match := self.re_DateTime2.search(m.group("DATETIME")): 

597 datestr = "%d/%m/%Y %H:%M:%S" if "/" in m.group("DATETIME") else "%d.%m.%Y %H:%M:%S" 

598 if date_match.group("S") is None: 

599 datestr = "%d/%m/%Y %H:%M" 

600 else: 

601 date_match1 = self.re_DateTime3.search(m.group("DATETIME")) 

602 datestr = "%Y/%m/%d %H:%M:%S" 

603 if date_match1 is None: 

604 # If the datetime string cannot be parsed in any format, log an error and raise an exception 

605 log.error(f"iPokerToFpdb.readHandInfo Could not read datetime: '{hand.handid}'") 

606 raise FpdbParseError from e 

607 if date_match1.group("S") is None: 

608 datestr = "%Y/%m/%d %H:%M" 

609 hand.startTime = datetime.datetime.strptime(m.group("DATETIME"), datestr) 

610 

611 # If the hand is a tournament hand, set additional information 

612 if self.info["type"] == "tour": 

613 hand.tourNo = self.tinfo["tourNo"] 

614 hand.buyinCurrency = self.tinfo["buyinCurrency"] 

615 hand.buyin = self.tinfo["buyin"] 

616 hand.fee = self.tinfo["fee"] 

617 hand.tablename = f"{self.info['table_name']}" 

618 

619 def readPlayerStacks(self, hand): 

620 """ 

621 Extracts player information from the hand text and populates the Hand object with 

622 player stacks and winnings. 

623 

624 Args: 

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

626 

627 Raises: 

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

629 

630 Returns: 

631 None 

632 """ 

633 # Initialize dictionaries and regex pattern 

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

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

636 

637 # Extract player information from regex matches 

638 for a in m: 

639 a.groupdict() 

640 # Create a dictionary entry for the player with their seat, stack, winnings, 

641 # and sitout status 

642 plist[a.group("PNAME")] = [ 

643 int(a.group("SEAT")), 

644 self.clearMoneyString(a.group("CASH")), 

645 self.clearMoneyString(a.group("WIN")), 

646 False, 

647 ] 

648 # If the player is the button, set the button position in the Hand object 

649 if a.group("BUTTONPOS") == "1": 

650 hand.buttonpos = int(a.group("SEAT")) 

651 

652 # Ensure there are at least 2 players in the hand 

653 if len(plist) <= 1: 

654 # Hand cancelled 

655 log.error(f"iPokerToFpdb.readPlayerStacks: '{hand.handid}'") 

656 raise FpdbParseError 

657 

658 # Add remaining players to the Hand object and playerWinnings dictionary if they won 

659 for pname in plist: 

660 seat, stack, win, sitout = plist[pname] 

661 hand.addPlayer(seat, pname, stack, None, sitout) 

662 if Decimal(win) != 0: 

663 self.playerWinnings[pname] = win 

664 

665 # Set the maxseats attribute in the Hand object if it is not already set 

666 if hand.maxseats is None: 

667 if self.info["type"] == "tour" and self.maxseats == 0: 

668 hand.maxseats = self.guessMaxSeats(hand) 

669 self.maxseats = hand.maxseats 

670 elif self.info["type"] == "tour": 

671 hand.maxseats = self.maxseats 

672 else: 

673 hand.maxseats = None 

674 

675 def markStreets(self, hand): 

676 """ 

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

678 

679 Args: 

680 hand (Hand): the Hand object to which the rounds will be added 

681 """ 

682 if hand.gametype["base"] in ("hold"): 

683 # Extract rounds for hold'em game 

684 m = re.search( 

685 r'(?P<PREFLOP>.+(?=<round no="2">)|.+)' # Preflop round 

686 r'(<round no="2">(?P<FLOP>.+(?=<round no="3">)|.+))?' # Flop round 

687 r'(<round no="3">(?P<TURN>.+(?=<round no="4">)|.+))?' # Turn round 

688 r'(<round no="4">(?P<RIVER>.+))?', # River round 

689 hand.handText, 

690 re.DOTALL, 

691 ) 

692 elif hand.gametype["base"] in ("stud"): 

693 # Extract rounds for stud game 

694 if hand.gametype["category"] == "5_studhi": 

695 # Extract rounds for 5-card stud high game 

696 m = re.search( 

697 r'(?P<ANTES>.+(?=<round no="2">)|.+)' # Antes round 

698 r'(<round no="2">(?P<SECOND>.+(?=<round no="3">)|.+))?' # Second round 

699 r'(<round no="3">(?P<THIRD>.+(?=<round no="4">)|.+))?' # Third round 

700 r'(<round no="4">(?P<FOURTH>.+(?=<round no="5">)|.+))?' # Fourth round 

701 r'(<round no="5">(?P<FIFTH>.+))?', # Fifth round 

702 hand.handText, 

703 re.DOTALL, 

704 ) 

705 else: 

706 # Extract rounds for 7-card stud high/low game 

707 m = re.search( 

708 r'(?P<ANTES>.+(?=<round no="2">)|.+)' # Antes round 

709 r'(<round no="2">(?P<THIRD>.+(?=<round no="3">)|.+))?' # Third round 

710 r'(<round no="3">(?P<FOURTH>.+(?=<round no="4">)|.+))?' # Fourth round 

711 r'(<round no="4">(?P<FIFTH>.+(?=<round no="5">)|.+))?' # Fifth round 

712 r'(<round no="5">(?P<SIXTH>.+(?=<round no="6">)|.+))?' # Sixth round 

713 r'(<round no="6">(?P<SEVENTH>.+))?', # Seventh round 

714 hand.handText, 

715 re.DOTALL, 

716 ) 

717 hand.addStreets(m) 

718 

719 def readCommunityCards(self, hand, street): 

720 """ 

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

722 

723 Args: 

724 hand (Hand): The hand object. 

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

726 

727 Raises: 

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

729 

730 Returns: 

731 None 

732 """ 

733 cards = [] 

734 # Search for the board cards in the hand's streets 

735 if m := self.re_Board.search(hand.streets[street]): 

736 # Split the card string into a list of cards 

737 cards = m.group("CARDS").strip().split(" ") 

738 # Format the cards 

739 cards = [c[1:].replace("10", "T") + c[0].lower() for c in cards] 

740 # Set the community cards in the hand object 

741 hand.setCommunityCards(street, cards) 

742 else: 

743 # Log an error if the board cards could not be found 

744 log.error(f"iPokerToFpdb.readCommunityCards: '{hand.handid}'") 

745 # Raise an exception 

746 raise FpdbParseError 

747 

748 def readAntes(self, hand): 

749 """ 

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

751 

752 Args: 

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

754 

755 Returns: 

756 None 

757 """ 

758 # Find all the antes in the hand text using a regular expression 

759 m = self.re_Action.finditer(hand.handText) 

760 

761 # Loop through each ante found 

762 for a in m: 

763 # If the ante is of type 15, add it to the hand 

764 if a.group("ATYPE") == "15": 

765 hand.addAnte(a.group("PNAME"), self.clearMoneyString(a.group("BET"))) 

766 

767 def readBringIn(self, hand): 

768 """ 

769 Reads the bring-in for a hand and sets the small blind (sb) and big blind (bb) values if they are not already set. 

770 

771 Args: 

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

773 

774 Returns: 

775 None 

776 """ 

777 # If sb and bb are not already set, set them to default values 

778 if hand.gametype["sb"] is None and hand.gametype["bb"] is None: 

779 hand.gametype["sb"] = "1" # default small blind value 

780 hand.gametype["bb"] = "2" # default big blind value 

781 

782 def readBlinds(self, hand): 

783 """ 

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

785 

786 :param hand: Hand object containing the hand history. 

787 :type hand: Hand 

788 """ 

789 # Find all actions in the preflop street 

790 for a in self.re_Action.finditer(hand.streets["PREFLOP"]): 

791 if a.group("ATYPE") == "1": 

792 # If the action is a small blind, add it to the hand object 

793 hand.addBlind(a.group("PNAME"), "small blind", self.clearMoneyString(a.group("BET"))) 

794 # If the small blind amount is not already set, set it 

795 if not hand.gametype["sb"]: 

796 hand.gametype["sb"] = self.clearMoneyString(a.group("BET")) 

797 

798 # Find all actions in the preflop street 

799 m = self.re_Action.finditer(hand.streets["PREFLOP"]) 

800 # Create a dictionary to store big blind information for each player 

801 blinds = {int(a.group("ACT")): a.groupdict() for a in m if a.group("ATYPE") == "2"} 

802 # Iterate over the big blind information and add it to the hand object 

803 for b in sorted(list(blinds.keys())): 

804 type = "big blind" 

805 blind = blinds[b] 

806 # If the big blind amount is not already set, set it 

807 if not hand.gametype["bb"]: 

808 hand.gametype["bb"] = self.clearMoneyString(blind["BET"]) 

809 # If the small blind amount is set, check if the amount is bigger than the small blind amount 

810 elif hand.gametype["sb"]: 

811 bb = Decimal(hand.gametype["bb"]) 

812 amount = Decimal(self.clearMoneyString(blind["BET"])) 

813 if amount > bb: 

814 type = "both" 

815 # Add the big blind to the hand object 

816 hand.addBlind(blind["PNAME"], type, self.clearMoneyString(blind["BET"])) 

817 # Fix tournament blinds if necessary 

818 self.fixTourBlinds(hand) 

819 

820 def fixTourBlinds(self, hand): 

821 """ 

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

823 

824 :param hand: A dictionary containing the game type information. 

825 :return: None 

826 """ 

827 if hand.gametype["type"] != "tour": 

828 return 

829 

830 if hand.gametype["sb"] is None and hand.gametype["bb"] is None: 

831 hand.gametype["sb"] = "1" 

832 hand.gametype["bb"] = "2" 

833 elif hand.gametype["sb"] is None: 

834 hand.gametype["sb"] = str(int(int(hand.gametype["bb"]) // 2)) 

835 elif hand.gametype["bb"] is None: 

836 hand.gametype["bb"] = str(int(hand.gametype["sb"]) * 2) 

837 

838 if int(hand.gametype["bb"]) // 2 != int(hand.gametype["sb"]): 

839 if int(hand.gametype["bb"]) // 2 < int(hand.gametype["sb"]): 

840 hand.gametype["bb"] = str(int(hand.gametype["sb"]) * 2) 

841 else: 

842 hand.gametype["sb"] = str(int(hand.gametype["bb"]) // 2) 

843 

844 def readButton(self, hand): 

845 # Found in re_Player 

846 pass 

847 

848 def readHoleCards(self, hand): 

849 """ 

850 Parses a Hand object to extract hole card information for each player on each street. 

851 Adds the hole card information to the Hand object. 

852 

853 Args: 

854 hand: Hand object to extract hole card information from 

855 

856 Returns: 

857 None 

858 """ 

859 

860 # streets PREFLOP, PREDRAW, and THIRD are special cases beacause we need to grab hero's cards 

861 for street in ("PREFLOP", "DEAL"): 

862 if street in hand.streets.keys(): 

863 # Find all instances of hero's cards in the street and add them to the Hand object 

864 m = self.re_HeroCards.finditer(hand.streets[street]) 

865 for found in m: 

866 player = found.group("PNAME") 

867 cards = found.group("CARDS").split(" ") 

868 cards = [c[1:].replace("10", "T") + c[0].lower().replace("x", "") for c in cards] 

869 if player == self.hero and cards[0]: 

870 hand.hero = player 

871 hand.addHoleCards(street, player, closed=cards, shown=True, mucked=False, dealt=True) 

872 

873 # Go through each street in the Hand object and add hole card information for each player 

874 for street, text in list(hand.streets.items()): 

875 if not text or street in ("PREFLOP", "DEAL"): 

876 continue # already done these 

877 m = self.re_HeroCards.finditer(hand.streets[street]) 

878 for found in m: 

879 player = found.group("PNAME") 

880 if player is not None: 

881 cards = found.group("CARDS").split(" ") 

882 

883 # Handle special case where hero is not the player and it's the seventh street in a stud game 

884 if street == "SEVENTH" and self.hero != player: 

885 newcards = [] 

886 oldcards = [c[1:].replace("10", "T") + c[0].lower() for c in cards if c[0].lower() != "x"] 

887 else: 

888 newcards = [c[1:].replace("10", "T") + c[0].lower() for c in cards if c[0].lower() != "x"] 

889 oldcards = [] 

890 

891 # Handle special case where hero is the player and it's the third street in a stud game 

892 if street == "THIRD" and len(newcards) == 3 and self.hero == player: 

893 hand.hero = player 

894 hand.dealt.add(player) 

895 hand.addHoleCards( 

896 street, 

897 player, 

898 closed=newcards[:2], 

899 open=[newcards[2]], 

900 shown=True, 

901 mucked=False, 

902 dealt=False, 

903 ) 

904 

905 # Handle special case where hero is the player and it's the second street in a stud game 

906 elif street == "SECOND" and len(newcards) == 2 and self.hero == player: 

907 hand.hero = player 

908 hand.dealt.add(player) 

909 hand.addHoleCards( 

910 street, 

911 player, 

912 closed=[newcards[0]], 

913 open=[newcards[1]], 

914 shown=True, 

915 mucked=False, 

916 dealt=False, 

917 ) 

918 

919 # Handle all other cases where hole card information needs to be added to the Hand object 

920 else: 

921 hand.addHoleCards( 

922 street, player, open=newcards, closed=oldcards, shown=True, mucked=False, dealt=False 

923 ) 

924 

925 def readAction(self, hand, street): 

926 """ 

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

928 

929 Args: 

930 hand (Hand): Hand object to which the actions will be added. 

931 street (int): Number of the street in the hand (0 for preflop, 1 for flop, etc.). 

932 

933 Returns: 

934 None 

935 """ 

936 # HH format doesn't actually print the actions in order! 

937 m = self.re_Action.finditer(hand.streets[street]) 

938 actions = {int(a.group("ACT")): a.groupdict() for a in m} 

939 

940 # Add each action to the corresponding method of the Hand object. 

941 # atype is the action type (0 for fold, 4 for check, etc.). 

942 for a in sorted(list(actions.keys())): 

943 action = actions[a] 

944 atype = action["ATYPE"] 

945 player = action["PNAME"] 

946 bet = self.clearMoneyString(action["BET"]) 

947 

948 if atype == "0": 

949 hand.addFold(street, player) 

950 elif atype == "4": 

951 hand.addCheck(street, player) 

952 elif atype == "3": 

953 hand.addCall(street, player, bet) 

954 elif atype == "23": # Raise to 

955 hand.addRaiseTo(street, player, bet) 

956 elif atype == "6": # Raise by 

957 # This is only a guess 

958 hand.addRaiseBy(street, player, bet) 

959 elif atype == "5": 

960 hand.addBet(street, player, bet) 

961 elif atype == "16": # BringIn 

962 hand.addBringIn(player, bet) 

963 elif atype == "7": 

964 hand.addAllIn(street, player, bet) 

965 elif atype == "15": # Ante 

966 pass # Antes dealt with in readAntes 

967 elif atype in ["1", "2", "8"]: # sb/bb/no action this hand (joined table) 

968 pass 

969 elif atype == "9": # FIXME: Sitting out 

970 hand.addFold(street, player) 

971 else: 

972 log.error( 

973 # Log an error for unimplemented actions 

974 ("DEBUG:") + " " + f"Unimplemented readAction: '{action['PNAME']}' '{action['ATYPE']}'" 

975 ) 

976 

977 def readShowdownActions(self, hand): 

978 # Cards lines contain cards 

979 pass 

980 

981 def readCollectPot(self, hand): 

982 """ 

983 Sets the uncalled bets for the given hand and adds collect pot actions for each player with non-zero winnings. 

984 

985 Args: 

986 hand: The Hand object to update with the collect pot actions. 

987 """ 

988 hand.setUncalledBets(self.uncalledbets) 

989 for pname, pot in list(self.playerWinnings.items()): 

990 hand.addCollectPot(player=pname, pot=self.clearMoneyString(pot)) 

991 # add collect pot action for player with non-zero winnings 

992 

993 def readShownCards(self, hand): 

994 # Cards lines contain cards 

995 pass 

996 

997 @staticmethod 

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

999 """ 

1000 Generate a regular expression pattern for table title. 

1001 

1002 Args: 

1003 - type: A string value. 

1004 - table_name: A string value representing the table name. 

1005 - tournament: A string value representing the tournament. 

1006 - table_number: An integer value representing the table number. 

1007 

1008 Returns: 

1009 - A string value representing the regular expression pattern for table title. 

1010 """ 

1011 # Log the input parameters 

1012 log.info( 

1013 f"iPoker getTableTitleRe: table_name='{table_name}' tournament='{tournament}' table_number='{table_number}'" 

1014 ) 

1015 

1016 # Generate the regex pattern based on the input parameters 

1017 regex = f"{table_name}" 

1018 

1019 if type == "tour": 

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

1021 

1022 print(regex) 

1023 

1024 return regex 

1025 elif table_name.find("(No DP),") != -1: 

1026 regex = table_name.split("(No DP),")[0] 

1027 elif table_name.find(",") != -1: 

1028 regex = table_name.split(",")[0] 

1029 else: 

1030 regex = table_name.split(" ")[0] 

1031 

1032 # Log the generated regex pattern and return it 

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

1034 return regex