Coverage for iPokerToFpdb.py: 0%
406 statements
« prev ^ index » next coverage.py v7.6.3, created at 2024-10-14 11:07 +0000
« prev ^ index » next coverage.py v7.6.3, created at 2024-10-14 11:07 +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
46from HandHistoryConverter import HandHistoryConverter, FpdbParseError, FpdbHandPartial
47from decimal import Decimal
48import re
49import logging
50import datetime
53log = logging.getLogger("parser")
56class iPoker(HandHistoryConverter):
57 """
58 A class for converting iPoker hand history files to the PokerTH format.
59 """
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
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 }
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 }
99 currencies = {"€": "EUR", "$": "USD", "": "T$", "£": "GBP", "RSD": "RSD", "kr": "SEK"}
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 }
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 }
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 }
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")
314 def compilePlayerRegexs(self, hand):
315 pass
317 def playerNameFromSeatNo(self, seatNo, hand):
318 """
319 Returns the name of the player from the given seat number.
321 This special function is required because Carbon Poker records actions by seat number, not by the player's name.
323 Args:
324 seatNo (int): The seat number of the player.
325 hand (Hand): The hand instance containing the players information.
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]
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 ]
352 def parseHeader(self, handText, whole_file):
353 """
354 Parses the header of a hand history and returns the game type.
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.
360 Returns:
361 str: The game type, if it can be determined from the header or the whole file.
362 None otherwise.
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
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"]
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
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
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")
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")
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"])
485 if "winnings" not in self.tinfo:
486 self.tinfo["winnings"] = 0 # Initialize 'winnings' if it doesn't exist yet
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 )
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"
500 if mg["BIAMT"] and self.re_FPP.match(mg["BIAMT"]):
501 self.tinfo["buyinCurrency"] = "FPP"
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 )
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"]
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
547 return self.info
549 def readHandInfo(self, hand):
550 """
551 Parses the hand text and extracts relevant information about the hand.
553 Args:
554 hand: An instance of the Hand class that represents the hand being parsed.
556 Raises:
557 FpdbParseError: If the hand text cannot be parsed.
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
570 # Extract the relevant information from the match object
571 m.groupdict()
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"])
578 # Set the hand ID for the hand
579 hand.handid = m.group("HID")
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)
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']}"
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.
624 Args:
625 hand (Hand): Hand object to populate with player information.
627 Raises:
628 FpdbParseError: If there are fewer than 2 players in the hand.
630 Returns:
631 None
632 """
633 # Initialize dictionaries and regex pattern
634 self.playerWinnings, plist = {}, {}
635 m = self.re_PlayerInfo.finditer(hand.handText)
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"))
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
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
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
675 def markStreets(self, hand):
676 """
677 Extracts the rounds of a hand and adds them to the Hand object
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)
719 def readCommunityCards(self, hand, street):
720 """
721 Parse the community cards for the given street and set them in the hand object.
723 Args:
724 hand (Hand): The hand object.
725 street (str): The street to parse the community cards for.
727 Raises:
728 FpdbParseError: If the community cards could not be parsed.
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
748 def readAntes(self, hand):
749 """
750 Reads the antes for each player in the given hand.
752 Args:
753 hand (Hand): The hand to read the antes from.
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)
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")))
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.
771 Args:
772 hand (Hand): The hand object for which to read the bring-in.
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
782 def readBlinds(self, hand):
783 """
784 Parses hand history to extract blind information for each player in the hand.
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"))
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)
820 def fixTourBlinds(self, hand):
821 """
822 Fix tournament blinds if small blind is missing or sb/bb is all-in.
824 :param hand: A dictionary containing the game type information.
825 :return: None
826 """
827 if hand.gametype["type"] != "tour":
828 return
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)
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)
844 def readButton(self, hand):
845 # Found in re_Player
846 pass
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.
853 Args:
854 hand: Hand object to extract hole card information from
856 Returns:
857 None
858 """
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)
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(" ")
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 = []
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 )
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 )
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 )
925 def readAction(self, hand, street):
926 """
927 Extracts actions from a hand and adds them to the corresponding street in a Hand object.
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.).
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}
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"])
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 )
977 def readShowdownActions(self, hand):
978 # Cards lines contain cards
979 pass
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.
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
993 def readShownCards(self, hand):
994 # Cards lines contain cards
995 pass
997 @staticmethod
998 def getTableTitleRe(type, table_name=None, tournament=None, table_number=None):
999 """
1000 Generate a regular expression pattern for table title.
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.
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 )
1016 # Generate the regex pattern based on the input parameters
1017 regex = f"{table_name}"
1019 if type == "tour":
1020 regex = f"([^\(]+)\s{table_number}"
1022 print(regex)
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]
1032 # Log the generated regex pattern and return it
1033 log.info(f"iPoker getTableTitleRe: returns: '{regex}'")
1034 return regex