Coverage for CakeToFpdb.py: 0%
271 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 2008-2011, 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########################################################################
21from __future__ import division
23from past.utils import old_div
24# import L10n
25# _ = L10n.get_translation()
27from HandHistoryConverter import HandHistoryConverter, FpdbParseError, FpdbHandPartial
28from decimal import Decimal
29import re
30import logging
31import datetime
33log = logging.getLogger("parser")
36class Cake(HandHistoryConverter):
37 """
38 A class for converting Cake hand histories to FPDB format.
40 Inherits from HandHistoryConverter.
42 Class Variables:
43 sitename (str): The name of the site.
44 filetype (str): The file type of the hand history.
45 codepage (tuple): The supported code pages for the hand history.
46 siteId (int): The ID of the site.
47 sym (dict): A dictionary mapping currencies to their symbols.
48 substitutions (dict): A dictionary mapping shorthand codes to regular expressions for parsing hand histories.
49 """
51 # Class Variables
53 sitename = "Cake"
54 filetype = "text"
55 codepage = ("utf8", "cp1252")
56 siteId = 17
57 sym = {"USD": "\$", "CAD": "\$", "T$": "", "EUR": "€", "GBP": "\xa3", "play": ""}
58 substitutions = {
59 "LEGAL_ISO": "USD|EUR|GBP|CAD|FPP", # legal ISO currency codes
60 "LS": r"\$|€|", # legal currency symbols - Euro(cp1252, utf-8)
61 "PLYR": r"(?P<PNAME>.+?)",
62 "CUR": r"(\$|€|)",
63 "NUM": r".(,|\s)\d\xa0",
64 "NUM2": r"\b((?:\d{1,3}(?:\s\d{3})*)|(?:\d+))\b", # Regex pattern for matching numbers with spaces
65 }
67 # translations from captured groups to fpdb info strings
68 Lim_Blinds = {
69 "0.04": ("0.01", "0.02"),
70 "0.08": ("0.02", "0.04"),
71 "0.10": ("0.02", "0.05"),
72 "0.20": ("0.05", "0.10"),
73 "0.40": ("0.10", "0.20"),
74 "0.50": ("0.10", "0.25"),
75 "1.00": ("0.25", "0.50"),
76 "1": ("0.25", "0.50"),
77 "2.00": ("0.50", "1.00"),
78 "2": ("0.50", "1.00"),
79 "4.00": ("1.00", "2.00"),
80 "4": ("1.00", "2.00"),
81 "6.00": ("1.50", "3.00"),
82 "6": ("1.50", "3.00"),
83 "8.00": ("2.00", "4.00"),
84 "8": ("2.00", "4.00"),
85 "10.00": ("2.50", "5.00"),
86 "10": ("2.50", "5.00"),
87 "12.00": ("3.00", "6.00"),
88 "12": ("3.00", "6.00"),
89 "20.00": ("5.00", "10.00"),
90 "20": ("5.00", "10.00"),
91 "30.00": ("7.50", "15.00"),
92 "30": ("7.50", "15.00"),
93 "40.00": ("10.00", "20.00"),
94 "40": ("10.00", "20.00"),
95 "60.00": ("15.00", "30.00"),
96 "60": ("15.00", "30.00"),
97 "80.00": ("20.00", "40.00"),
98 "80": ("20.00", "40.00"),
99 "100.00": ("25.00", "50.00"),
100 "100": ("25.00", "50.00"),
101 "200.00": ("50.00", "100.00"),
102 "200": ("50.00", "100.00"),
103 "400.00": ("100.00", "200.00"),
104 "400": ("100.00", "200.00"),
105 "800.00": ("200.00", "400.00"),
106 "800": ("200.00", "400.00"),
107 "1000.00": ("250.00", "500.00"),
108 "1000": ("250.00", "500.00"),
109 }
111 limits = {"NL": "nl", "PL": "pl", "FL": "fl"}
112 games = { # base, category
113 "Hold'em": ("hold", "holdem"),
114 "Omaha": ("hold", "omahahi"),
115 "Omaha Hi/Lo": ("hold", "omahahilo"),
116 "OmahaHiLo": ("hold", "omahahilo"),
117 }
118 currencies = {"€": "EUR", "$": "USD", "": "T$"}
120 # Static regexes
121 re_GameInfo = re.compile(
122 r"""
123 Hand\#(?P<HID>[A-Z0-9]+)\s+\-\s+(?P<TABLE>(?P<BUYIN1>(?P<BIAMT1>(%(LS)s)[%(NUM)s]+)\sNLH\s(?P<MAX1>\d+)\smax)?.+?)\s(\((Turbo,\s)?(?P<MAX>\d+)\-+[Mm]ax\)\s)?((?P<TOURNO>T\d+)|\d+)\s(\-\-\s(TICKET|CASH|TICKETCASH|FREEROLL)\s\-\-\s(?P<BUYIN>(?P<BIAMT>\$\d+)\s\+\s(?P<BIRAKE>\$\d+))\s\-\-\s(?P<TMAX>\d+)\sMax\s)?(\-\-\sTable\s(?P<TABLENO>\d+)\s)?\-\-\s(?P<CURRENCY>%(LS)s|)?(?P<ANTESB>(\-)?\d+)/(%(LS)s)?(?P<SBBB>\d+)(/(%(LS)s)?(?P<BB>\d+))?\s(?P<LIMIT>NL|FL||PL)\s(?P<GAME>Hold\'em|Omaha|Omaha\sHi/Lo|OmahaHiLo)\s-\-\s(?P<DATETIME>.*$)
125 """
126 % substitutions,
127 re.MULTILINE | re.VERBOSE,
128 )
130 re_PlayerInfo = re.compile(
131 r"""
132 ^Seat\s(?P<SEAT>[0-9]+):\s
133 (?P<PNAME>.+?)\s
134 \((%(LS)s)?(?P<CASH>[%(NUM2)s]+)\sin\schips\)
135 (\s\s\(EUR\s(%(CUR)s)?(?P<EUROVALUE>[%(NUM)s]+)\))?"""
136 % substitutions,
137 re.MULTILINE | re.VERBOSE,
138 )
140 re_Trim = re.compile("(Hand\#)")
141 re_Identify = re.compile("Hand\#[A-Z0-9]+\s\-\s")
142 re_SplitHands = re.compile("\n\n+")
143 re_Button = re.compile("Dealer: Seat (?P<BUTTON>\d+)", re.MULTILINE)
144 re_Board = re.compile(r"\[(?P<CARDS>.+)\]")
146 re_DateTime = re.compile(
147 """(?P<Y>[0-9]{4})[\/\-\.](?P<M>[0-9]{2})[\/\-\.](?P<D>[0-9]{2})[\- ]+(?P<H>[0-9]+):(?P<MIN>[0-9]+):(?P<S>[0-9]+)""",
148 re.MULTILINE,
149 )
150 re_PostSB = re.compile(
151 r"^%(PLYR)s: posts small blind %(CUR)s(?P<SB>[%(NUM)s]+)(\s\(EUR\s(%(CUR)s)?(?P<EUROVALUE>[%(NUM)s]+)\))?$"
152 % substitutions,
153 re.MULTILINE,
154 )
155 re_PostBB = re.compile(
156 r"^%(PLYR)s: posts big blind %(CUR)s(?P<BB>[%(NUM)s]+)(\s\(EUR\s(%(CUR)s)?(?P<EUROVALUE>[%(NUM)s]+)\))?$"
157 % substitutions,
158 re.MULTILINE,
159 )
160 re_Antes = re.compile(
161 r"^%(PLYR)s: posts ante of %(CUR)s(?P<ANTE>[%(NUM)s]+)(\s\(EUR\s(%(CUR)s)?(?P<EUROVALUE>[%(NUM)s]+)\))?"
162 % substitutions,
163 re.MULTILINE,
164 )
165 re_BringIn = re.compile(
166 r"^%(PLYR)s: brings[- ]in( low|) for %(CUR)s(?P<BRINGIN>[%(NUM)s]+)(\s\(EUR\s(%(CUR)s)?(?P<EUROVALUE>[%(NUM)s]+)\))?"
167 % substitutions,
168 re.MULTILINE,
169 )
170 re_PostBoth = re.compile(
171 r"^%(PLYR)s:posts dead blind %(CUR)s(\-)?(?P<SB>[%(NUM)s]+) and big blind %(CUR)s(?P<BB>[%(NUM)s]+)"
172 % substitutions,
173 re.MULTILINE,
174 )
175 re_HeroCards = re.compile(
176 r"^Dealt to %(PLYR)s(?: \[(?P<OLDCARDS>.+?)\])?( \[(?P<NEWCARDS>.+?)\])" % substitutions, re.MULTILINE
177 )
178 re_Action = re.compile(
179 r"""
180 ^%(PLYR)s:(?P<ATYPE>\sbets|\schecks|\sraises|\scalls|\sfolds|\sis\sall\sin)
181 (\s(to\s)?(%(CUR)s)?(?P<BET>[%(NUM)s]+))?(\s\(EUR\s(%(CUR)s)?(?P<EUROVALUE>[%(NUM)s]+)\))?$
182 """
183 % substitutions,
184 re.MULTILINE | re.VERBOSE,
185 )
186 re_sitsOut = re.compile("^%s sits out" % substitutions["PLYR"], re.MULTILINE)
187 re_ShownCards = re.compile(
188 r"^%s: (?P<SHOWED>shows|mucks) \[(?P<CARDS>.*)\] ?(\((?P<STRING>.*)\))?" % substitutions["PLYR"], re.MULTILINE
189 )
190 re_CollectPot = re.compile(
191 r"^%(PLYR)s:? wins (low pot |high pot )?%(CUR)s(?P<POT>[%(NUM)s]+)((\swith.+?)?\s+\(EUR\s(%(CUR)s)?(?P<EUROVALUE>[%(NUM)s]+)\))?"
192 % substitutions,
193 re.MULTILINE,
194 )
195 re_Finished = re.compile(r"%(PLYR)s:? finished \d+ out of \d+ players" % substitutions, re.MULTILINE)
196 re_Dealer = re.compile(r"Dealer:") # Some Cake hands just omit the game line so we can just discard them as partial
197 re_CoinFlip = re.compile(r"Coin\sFlip\sT\d+", re.MULTILINE)
198 re_ReturnBet = re.compile(r"returns\suncalled\sbet", re.MULTILINE)
199 re_ShowDown = re.compile(r"\*\*\*SHOW DOWN\*\*\*")
200 re_ShowDownLeft = re.compile(r"\*\*\*SHOW\sDOWN\*\*\*\nPlayer\sleft\sthe\stable$", re.MULTILINE)
202 def compilePlayerRegexs(self, hand):
203 """
204 Compiles regular expressions representing the cards in a player's hand.
206 Args:
207 hand (list[str]): The cards in the player's hand.
209 Returns:
210 list[re.Pattern]: A list of compiled regular expressions, one for each card in the player's hand.
211 """
212 pass # TODO: Implement this function.
214 def readSupportedGames(self):
215 """
216 Returns a list of supported games.
218 Each supported game is represented as a list of game modes.
219 """
220 return [
221 ["ring", "hold", "nl"], # Ring game, hold mode, no limit
222 ["ring", "hold", "pl"], # Ring game, hold mode, pot limit
223 ["ring", "hold", "fl"], # Ring game, hold mode, fixed limit
224 ["tour", "hold", "nl"], # Tournament, hold mode, no limit
225 ["tour", "hold", "pl"], # Tournament, hold mode, pot limit
226 ["tour", "hold", "fl"], # Tournament, hold mode, fixed limit
227 ]
229 def determineGameType(self, handText):
230 """
231 Determine the type of game from the given hand text.
233 Args:
234 handText (str): The text of the hand.
236 Returns:
237 dict: A dictionary containing information about the game type.
238 """
239 # Initialize dictionary to store game type info
240 info = {}
242 # Search for game info in hand text
243 m = self.re_GameInfo.search(handText)
245 # If no game info found, raise appropriate error
246 if not m:
247 if self.re_Finished.search(handText):
248 raise FpdbHandPartial
249 if self.re_Dealer.match(handText):
250 raise FpdbHandPartial
251 tmp = handText[:200]
252 log.error(f"CakeToFpdb.determineGameType: '{tmp}'")
253 raise FpdbParseError
255 # If no ShowDown or ShowDownLeft found, raise appropriate error
256 if not self.re_ShowDown.search(handText) or self.re_ShowDownLeft.search(handText):
257 raise FpdbHandPartial
259 # Extract game info from match object's group dictionary
260 mg = m.groupdict()
262 # Determine limit type and store in info dictionary
263 if "LIMIT" in mg:
264 info["limitType"] = self.limits[mg["LIMIT"]]
266 # Determine game category and base type and store in info dictionary
267 if "GAME" in mg:
268 (info["base"], info["category"]) = self.games[mg["GAME"]]
270 # Determine big blind and store in info dictionary
271 if "BB" in mg:
272 if not mg["BB"]:
273 info["bb"] = self.clearMoneyString(mg["SBBB"])
274 else:
275 info["bb"] = self.clearMoneyString(mg["BB"])
277 # Determine small blind and store in info dictionary
278 if "SBBB" in mg:
279 if not mg["BB"]:
280 info["sb"] = self.clearMoneyString(mg["ANTESB"])
281 else:
282 info["sb"] = self.clearMoneyString(mg["SBBB"])
284 # Determine currency and store in info dictionary
285 if "CURRENCY" in mg:
286 info["currency"] = self.currencies[mg["CURRENCY"]]
288 # Determine mix and store in info dictionary
289 if "MIXED" in mg and mg["MIXED"] is not None:
290 info["mix"] = self.mixes[mg["MIXED"]]
292 # Determine game type and store in info dictionary
293 if "TOURNO" in mg and mg["TOURNO"] is not None:
294 info["type"] = "tour"
295 else:
296 info["type"] = "ring"
298 # If play money game, set currency to 'play'
299 if "TABLE" in mg and "Play Money" in mg["TABLE"]:
300 info["currency"] = "play"
302 # If limit type is 'fl' and big blind is not None
303 if info["limitType"] == "fl" and info["bb"] is not None:
304 # If game type is 'ring'
305 if info["type"] == "ring":
306 try:
307 # Determine small blind and big blind and store in info dictionary
308 info["sb"] = self.Lim_Blinds[info["bb"]][0]
309 info["bb"] = self.Lim_Blinds[info["bb"]][1]
310 except KeyError as e:
311 tmp = handText[:200]
312 log.error(f"CakeToFpdb.determineGameType: Lim_Blinds has no lookup for '{mg['BB']}' - '{tmp}'")
313 raise FpdbParseError from e
314 # If game type is not 'ring'
315 else:
316 # Calculate small blind and big blind and store in info dictionary
317 info["sb"] = str((old_div(Decimal(info["sb"]), 2)).quantize(Decimal("0.01")))
318 info["bb"] = str(Decimal(info["sb"]).quantize(Decimal("0.01")))
320 return info
322 def readHandInfo(self, hand):
323 """
324 Reads information from a hand history string and updates the corresponding Hand object.
326 Parameters:
327 hand (Hand): The Hand object to update.
329 Returns:
330 None
331 """
333 # trim off malformatted text from partially written hands
334 if not self.re_Trim.match(hand.handText):
335 hand.handText = "".join(self.re_Trim.split(hand.handText)[1:])
337 info = {}
338 m = self.re_GameInfo.search(hand.handText)
339 if m is None:
340 tmp = hand.handText[:200]
341 log.error(f"CakeToFpdb.readHandInfo: '{tmp}'")
342 raise FpdbParseError
344 info |= m.groupdict()
346 for key in info:
347 # extract datetime information and convert to UTC timezone
348 if key == "DATETIME":
349 m1 = self.re_DateTime.finditer(info[key])
350 datetimestr = "2000/01/01 00:00:00" # default used if time not found
351 for a in m1:
352 datetimestr = (
353 f"{a.group('Y')}/{a.group('M')}/{a.group('D')} {a.group('H')}:{a.group('MIN')}:{a.group('S')}"
354 )
355 hand.startTime = datetime.datetime.strptime(datetimestr, "%Y/%m/%d %H:%M:%S")
356 hand.startTime = HandHistoryConverter.changeTimezone(hand.startTime, "ET", "UTC")
358 # extract hand ID
359 elif key == "HID":
360 hand.handid = re.sub("[A-Z]+", "", info[key])
362 # extract table name for ring games
363 if key == "TABLE" and hand.gametype["type"] == "ring":
364 hand.tablename = info[key]
366 # extract table name for tournament games
367 if key == "TABLENO" and hand.gametype["type"] == "tour":
368 hand.tablename = info[key]
370 # extract button position
371 if key == "BUTTON":
372 hand.buttonpos = info[key]
374 # extract maximum number of seats
375 if key == "MAX" and info[key]:
376 hand.maxseats = int(info[key])
378 # extract tournament number
379 if key == "TOURNO" and info[key]:
380 hand.tourNo = info[key].replace("T", "")
382 # extract maximum number of seats for tournament games
383 if key == "TMAX" and info[key]:
384 hand.maxseats = int(info[key])
385 if key == "TMAX1" and info[key]:
386 hand.maxseats = int(info[key])
388 # extract buy-in information
389 if key in ["BUYIN", "BUYIN1"] and info[key] and hand.tourNo is not None:
390 if info[key].find("$") != -1:
391 hand.buyinCurrency = "USD"
392 elif info[key].find("£") != -1:
393 hand.buyinCurrency = "GBP"
394 elif info[key].find("€") != -1:
395 hand.buyinCurrency = "EUR"
396 elif re.match("^[0-9+]*$", info[key]):
397 hand.buyinCurrency = "play"
398 else:
399 # FIXME: handle other currencies, play money
400 log.error(
401 f"CakeToFpdb.readHandInfo: Failed to detect currency. Hand ID: {hand.handid}: '{info[key]}'"
402 )
403 raise FpdbParseError
405 # extract buy-in amount and rake amount
406 if key == "BUYIN1":
407 info["BIAMT1"] = self.clearMoneyString(info["BIAMT1"].strip("$€£"))
408 hand.buyin = int(100 * Decimal(info["BIAMT1"]))
409 hand.fee = 0
410 else:
411 info["BIAMT"] = self.clearMoneyString(info["BIAMT"].strip("$€£"))
412 info["BIRAKE"] = self.clearMoneyString(info["BIRAKE"].strip("$€£"))
413 hand.buyin = int(100 * Decimal(info["BIAMT"]))
414 hand.fee = int(100 * Decimal(info["BIRAKE"]))
416 if hand.gametype["type"] == "tour" and not hand.buyin:
417 hand.buyin = 0
418 hand.fee = 0
419 hand.buyinCurrency = "NA"
421 def readButton(self, hand):
422 """
423 Parses a hand for the button position and updates the hand object.
425 Args:
426 hand (Hand): The hand object to update.
428 Returns:
429 None
430 """
431 # Search for the button position in the hand text
432 if m := self.re_Button.search(hand.handText):
433 # If found, update the button position in the hand object
434 hand.buttonpos = int(m.group("BUTTON"))
435 else:
436 # If not found, log an info message
437 log.info("readButton: " + ("not found"))
439 def readPlayerStacks(self, hand):
440 """
441 Reads player stacks from the given `hand` object and adds them to the `hand` object.
443 Args:
444 hand (Hand): The `Hand` object to read player stacks from.
446 Returns:
447 None
448 """
449 # Find each player's stack information in the hand text
450 m = self.re_PlayerInfo.finditer(hand.handText)
452 # Check if there was a coinflip in the hand
453 coinflip = bool(self.re_CoinFlip.search(hand.handText))
455 # Iterate over each player's stack information
456 for a in m:
457 # Check if the stack information has a EUROVALUE and set the roundPenny flag accordingly
458 if a.group("EUROVALUE"):
459 hand.roundPenny = True
460 cash_value = a.group("CASH")
461 cash_value = cash_value.encode("utf-8")
462 cash_value = cash_value.replace(b"\xe2\x80\xaf", b"")
463 cash_value = cash_value.decode("utf-8")
464 print("value:", cash_value)
465 print("type:", type(cash_value))
466 # Add the player's stack information to the `hand` object
467 hand.addPlayer(int(a.group("SEAT")), a.group("PNAME"), cash_value)
469 # Add the player's stack information to the `hand` object
470 # hand.addPlayer(int(a.group('SEAT')), a.group('PNAME'), self.convertMoneyString('CASH', a))
472 # If there was a coinflip, add the ante for the player
473 if coinflip:
474 hand.addAnte(a.group("PNAME"), self.convertMoneyString("CASH", a))
476 def markStreets(self, hand):
477 """
478 Given a Hand object, extract the street information from its handText attribute
479 and update the Hand object with the street information.
481 Args:
482 - hand: a Hand object to extract street information from
484 Returns:
485 - None
486 """
488 # The following regex matches the different streets in a hand history and captures the information
489 # in named groups: PREFLOP, FLOP, TURN, RIVER.
490 # It first matches everything up to the FLOP street, then optionally matches the FLOP street,
491 # then optionally matches the TURN street, and finally optionally matches the RIVER street.
492 # The information captured in each street is then stored in its respective named group.
493 regex = (
494 r"(?P<PREFLOP>.+(?=\*\*\* FLOP \*\*\*)|.+)"
495 r"(\*\*\* FLOP \*\*\*(?P<FLOP> \[\S\S,\S\S,\S\S\].+(?=\*\*\* TURN \*\*\*)|.+))?"
496 r"(\*\*\* TURN \*\*\* (?P<TURN>\[\S\S\].+(?=\*\*\* RIVER \*\*\*)|.+))?"
497 r"(\*\*\* RIVER \*\*\* (?P<RIVER>\[\S\S\].+))?"
498 )
500 # Use the regex to search for the street information in the hand's handText attribute
501 m = re.search(regex, hand.handText, re.DOTALL)
503 # Add the street information to the Hand object
504 hand.addStreets(m)
506 def readCommunityCards(self, hand, street):
507 """
508 Reads the community cards for a given street of the current hand and sets them in the hand object.
510 Args:
511 hand (Hand): The current hand object.
512 street (str): The street to read the community cards for.
514 Returns:
515 None
516 """
517 if street in ("FLOP", "TURN", "RIVER"):
518 # Parse the community cards from the hand object's streets dictionary
519 m = self.re_Board.search(hand.streets[street])
520 # Set the community cards in the hand object
521 hand.setCommunityCards(street, m.group("CARDS").split(","))
523 def readAntes(self, hand):
524 """
525 Reads the antes from the hand and adds them to the Hand object.
527 Args:
528 hand: The Hand object to add the antes to.
530 Returns:
531 None
532 """
533 log.debug(("reading antes"))
534 m = self.re_Antes.finditer(hand.handText)
535 for player in m:
536 # Uncomment the following line to enable logging
537 # logging.debug("hand.addAnte(%s,%s)" %(player.group('PNAME'), player.group('ANTE')))
538 hand.addAnte(player.group("PNAME"), self.convertMoneyString("ANTE", player))
540 def readBringIn(self, hand):
541 """
542 Reads the BringIn information from the hand's handText and adds it to the hand object.
544 Args:
545 hand (Hand): The Hand object to add the BringIn information to.
547 Returns:
548 None
549 """
550 if m := self.re_BringIn.search(hand.handText, re.DOTALL):
551 # The BringIn information was found, add it to the hand object.
552 # logging.debug("readBringIn: %s for %s" %(m.group('PNAME'), m.group('BRINGIN')))
553 hand.addBringIn(m.group("PNAME"), self.convertMoneyString("BRINGIN", m))
555 def readBlinds(self, hand):
556 """
557 Parses the hand text and extracts the blinds information.
559 Args:
560 hand: An instance of the Hand class representing the hand being parsed.
562 Returns:
563 None
564 """
566 # Flag to keep track of whether the small blind is still live.
567 liveBlind = True
569 # If no bets were returned, set the uncalled bets flag to True.
570 if not self.re_ReturnBet.search(hand.handText):
571 hand.setUncalledBets(True)
573 # Find all instances of the small blind and add them to the Hand object.
574 for a in self.re_PostSB.finditer(hand.handText):
575 if liveBlind:
576 hand.addBlind(a.group("PNAME"), "small blind", self.convertMoneyString("SB", a))
577 liveBlind = False
578 else:
579 # Post dead blinds as ante
580 hand.addBlind(a.group("PNAME"), "secondsb", self.convertMoneyString("SB", a))
582 # Find all instances of the big blind and add them to the Hand object.
583 for a in self.re_PostBB.finditer(hand.handText):
584 hand.addBlind(a.group("PNAME"), "big blind", self.convertMoneyString("BB", a))
586 # Find all instances of both blinds being posted and add them to the Hand object.
587 for a in self.re_PostBoth.finditer(hand.handText):
588 sb = Decimal(self.clearMoneyString(a.group("SB")))
589 bb = Decimal(self.clearMoneyString(a.group("BB")))
590 sbbb = sb + bb
591 hand.addBlind(a.group("PNAME"), "both", str(sbbb))
593 def readHoleCards(self, hand):
594 """
595 Reads the hero's hole cards from the given hand object and adds them to the corresponding streets.
597 Args:
598 hand (Hand): The hand object containing the streets and player information.
600 Returns:
601 None
602 """
603 # Iterate through the streets where hole cards can be found
604 for street in ("PREFLOP", "DEAL"):
605 # Check if the street is present in the hand object
606 if street in list(hand.streets.keys()):
607 # Use regex to find hero's cards in the street
608 m = self.re_HeroCards.finditer(hand.streets[street])
609 # Iterate through each match found
610 for found in m:
611 # Save the hero's name
612 hand.hero = found.group("PNAME")
613 # Split the hole cards string into individual cards
614 newcards = found.group("NEWCARDS").split(",")
615 # Add the hole cards to the corresponding street
616 hand.addHoleCards(street, hand.hero, closed=newcards, shown=False, mucked=False, dealt=True)
618 def readAction(self, hand, street):
619 """
620 Given a Hand object and a street string, reads the actions from the hand
621 and updates the Hand object with the appropriate information.
623 Args:
624 - hand: Hand object representing the current state of the hand
625 - street: string representing the current betting round of the hand
627 Returns:
628 None
629 """
630 # Find all the actions in the current street of the hand
631 m = self.re_Action.finditer(hand.streets[street])
633 # Loop through each action and update the Hand object accordingly
634 for action in m:
635 # acts = action.groupdict()
636 # print "DEBUG: acts: %s" %acts
637 bet = self.convertMoneyString("BET", action)
638 actionType = action.group("ATYPE")
640 # If the current action is a fold and not in preflop, add a fold to the Hand object
641 if street != "PREFLOP" or actionType != " folds":
642 hand.setUncalledBets(False)
643 if actionType == " folds":
644 hand.addFold(street, action.group("PNAME"))
646 # If the current action is a check, add a check to the Hand object
647 elif actionType == " checks":
648 hand.addCheck(street, action.group("PNAME"))
650 # If the current action is a call, add a call to the Hand object
651 elif actionType == " calls":
652 hand.addCall(street, action.group("PNAME"), bet)
654 # If the current action is a raise, add a raise to the Hand object
655 elif actionType == " raises":
656 hand.setUncalledBets(None)
657 hand.addRaiseTo(street, action.group("PNAME"), bet)
659 # If the current action is a bet, add a bet to the Hand object
660 elif actionType == " bets":
661 hand.addBet(street, action.group("PNAME"), bet)
663 # If the current action is an all-in, add an all-in to the Hand object
664 elif actionType == " is all in":
665 hand.addAllIn(street, action.group("PNAME"), bet)
667 # If the current action is not one of the above types, log an error
668 else:
669 log.error(
670 ("DEBUG:") + " " + f"Unimplemented readAction: '{action.group('PNAME')}' '{action.group('ATYPE')}'"
671 )
673 def readShowdownActions(self, hand):
674 """
675 Parses a hand of cards and returns the best possible action to take in a game of poker.
677 Args:
678 hand (list): A list of cards in the hand.
680 Returns:
681 str: The best possible action to take.
682 """
683 pass
685 def readCollectPot(self, hand):
686 """
687 Finds the collect pot for a given hand and adds it to the Hand object.
689 Args:
690 hand (Hand): The Hand object to which the collect pot will be added.
692 Returns:
693 None
694 """
695 # Find all instances of the collect pot in the hand text.
696 for m in self.re_CollectPot.finditer(hand.handText):
697 # Only consider the collect pot if it is not part of a tournament hand.
698 if not re.search("Tournament:\s", m.group("PNAME")):
699 # Add the collect pot to the Hand object.
700 hand.addCollectPot(player=m.group("PNAME"), pot=self.convertMoneyString("POT", m))
702 def readShownCards(self, hand):
703 """
704 Finds shown cards in a hand text and adds them to the Hand object.
706 Args:
707 hand: The Hand object to which shown cards will be added.
709 Returns:
710 None
711 """
713 # Find shown cards using regular expression.
714 for m in self.re_ShownCards.finditer(hand.handText):
715 if m.group("CARDS") is not None:
716 cards = m.group("CARDS")
717 string = m.group("STRING")
719 # Check if player showed or mucked cards.
720 (shown, mucked) = (False, False)
721 if m.group("SHOWED") == "shows":
722 shown = True
723 # Split cards into a list.
724 cards = cards.split(" ")
725 elif m.group("SHOWED") == "mucks":
726 mucked = True
727 # Split cards into a list and remove any leading/trailing whitespace.
728 cards = [c.strip() for c in cards.split(",")]
730 # Try to add shown cards to the hand.
731 try:
732 hand.checkPlayerExists(m.group("PNAME"))
733 player = m.group("PNAME")
734 except FpdbParseError:
735 # If the player doesn't exist, replace underscores with spaces in the player name.
736 player = m.group("PNAME").replace("_", " ")
738 # Add shown cards to the hand.
739 hand.addShownCards(cards=cards, player=player, shown=shown, mucked=mucked, string=string)
741 def convertMoneyString(self, type, match):
742 """
743 Converts a string of money to a float value.
745 Args:
746 - type: string type of currency (e.g. "USD", "GBP", etc.)
747 - match: string to be converted
749 Returns:
750 - float value of the money string or None if no match found
751 """
753 if match.group("EUROVALUE"):
754 # if the match is in EUROVALUE format, return the cleared money string
755 return self.clearMoneyString(match.group("EUROVALUE"))
756 elif match.group(type):
757 # if the match is in the specified currency format, return the cleared money string
758 return self.clearMoneyString(match.group(type))
759 else:
760 # if no match found, return None
761 return None
763 @staticmethod
764 def getTableTitleRe(type, table_name=None, tournament=None, table_number=None):
765 log.info(
766 f"cake.getTableTitleRe: table_name='{table_name}' tournament='{tournament}' table_number='{table_number}'"
767 )
768 regex = ""
769 print("regex get table cash title:", regex)
770 if tournament:
771 regex = f"Tournament:\s{tournament}\sBuy\-In\s\w+\s:\sTable\s{table_number}"
772 # Tournament: 17106061 Buy-In Freeroll : Table 10 - No Limit Holdem - 15/30
773 print("regex get mtt sng expresso cash title:", regex)
774 log.info(f"Seals.getTableTitleRe: returns: '{regex}'")
775 return regex