Coverage for MergeSummary.py: 0%
291 statements
« prev ^ index » next coverage.py v7.6.3, created at 2024-10-15 19:33 +0000
« prev ^ index » next coverage.py v7.6.3, created at 2024-10-15 19:33 +0000
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
4# Copyright 2008-2011 Carl Gherardi
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Affero General Public License as published by
7# the Free Software Foundation, version 3 of the License.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16# In the "official" distribution you can find the license in agpl-3.0.txt.
19# import L10n
20# _ = L10n.get_translation()
22from decimal import Decimal
23import datetime
24from bs4 import BeautifulSoup
26from HandHistoryConverter import HandHistoryConverter, FpdbParseError
27import re
28import logging
29import MergeToFpdb, MergeStructures
30from TourneySummary import TourneySummary
32# Merge HH Format
33log = logging.getLogger("parser")
36class MergeSummary(TourneySummary):
37 limits = {
38 "No Limit": "nl",
39 "No Limit ": "nl",
40 "Fixed Limit": "fl",
41 "Limit": "fl",
42 "Pot Limit": "pl",
43 "Pot Limit ": "pl",
44 "Half Pot Limit": "hp",
45 }
46 games = { # base, category
47 "Holdem": ("hold", "holdem"),
48 "Holdem Tournament": ("hold", "holdem"),
49 "Omaha": ("hold", "omahahi"),
50 "Omaha Tournament": ("hold", "omahahi"),
51 "Omaha H/L8": ("hold", "omahahilo"),
52 "2-7 Lowball": ("draw", "27_3draw"),
53 "A-5 Lowball": ("draw", "a5_3draw"),
54 "Badugi": ("draw", "badugi"),
55 "5-Draw w/Joker": ("draw", "fivedraw"),
56 "5-Draw": ("draw", "fivedraw"),
57 "7-Stud": ("stud", "studhi"),
58 "7-Stud H/L8": ("stud", "studhilo"),
59 "5-Stud": ("stud", "5_studhi"),
60 "Razz": ("stud", "razz"),
61 }
62 games_html = {
63 "Texas Holdem": ("hold", "holdem"),
64 "Omaha": ("hold", "omahahi"),
65 "Omaha HiLo": ("hold", "omahahilo"),
66 "2-7 Low Triple Draw": ("draw", "27_3draw"),
67 "Badugi": ("draw", "badugi"),
68 "Seven Card Stud": ("stud", "studhi"),
69 "Seven Card Stud HiLo": ("stud", "studhilo"),
70 "Five Card Stud": ("stud", "studhilo"),
71 "Razz": ("stud", "razz"),
72 }
74 mixes = {"HA": "ha", "RASH": "rash", "HO": "ho", "SHOE": "shoe", "HORSE": "horse", "HOSE": "hose", "HAR": "har"}
76 months = {
77 "January": 1,
78 "Jan": 1,
79 "February": 2,
80 "Feb": 2,
81 "March": 3,
82 "Mar": 3,
83 "April": 4,
84 "Apr": 4,
85 "May": 5,
86 "June": 6,
87 "Jun": 6,
88 "July": 7,
89 "Jul": 7,
90 "August": 8,
91 "Aug": 8,
92 "September": 9,
93 "Sep": 9,
94 "October": 10,
95 "Oct": 10,
96 "November": 11,
97 "Nov": 11,
98 "December": 12,
99 "Dec": 12,
100 }
102 substitutions = {
103 "LEGAL_ISO": "USD|EUR|GBP|CAD|FPP", # legal ISO currency codes
104 "LS": "\$|€|", # legal currency symbols
105 }
106 re_Identify = re.compile("<title>Online\sPoker\sTournament\sDetails\s\-\sCarbonPoker</title>")
107 re_NotFound = re.compile("Tournament not found")
108 re_GameTypeHH = re.compile(
109 r'<description type="(?P<GAME>Holdem|Omaha|Omaha|Omaha\sH/L8|2\-7\sLowball|A\-5\sLowball|Badugi|5\-Draw\sw/Joker|5\-Draw|7\-Stud|7\-Stud\sH/L8|5\-Stud|Razz|HORSE|RASH|HA|HO|SHOE|HOSE|HAR)(?P<TYPE>\sTournament)?" stakes="(?P<LIMIT>[a-zA-Z ]+)(\s\(?\$?(?P<SB>[.0-9]+)?/?\$?(?P<BB>[.0-9]+)?(?P<blah>.*)\)?)?"(\sversion="\d+")?/>\s?',
110 re.MULTILINE,
111 )
112 re_HandInfoHH = re.compile(
113 r'<game id="(?P<HID1>[0-9]+)-(?P<HID2>[0-9]+)" starttime="(?P<DATETIME>.+?)" numholecards="[0-9]+" gametype="[0-9]+" (multigametype="(?P<MULTIGAMETYPE>\d+)" )?(seats="(?P<SEATS>[0-9]+)" )?realmoney="(?P<REALMONEY>(true|false))" data="[0-9]+[|:](?P<TABLENAME>[^|:]+)[|:](?P<TDATA>[^|:]+)[|:]?.*>',
114 re.MULTILINE,
115 )
116 re_DateTimeHH = re.compile(
117 r"(?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]+)",
118 re.MULTILINE,
119 )
120 re_turboHH = re.compile(r"Turbo\s\-\s")
122 re_HTMLName = re.compile("Name\s+?</th>\s+?<td>(?P<NAME>.+?)\s+?</td>")
123 re_HTMLGameType = re.compile(
124 """Game Type\s+?</th>\s+?<td>(?P<LIMIT>Fixed Limit|No Limit|Pot Limit) (?P<GAME>Texas\sHoldem|Omaha|Omaha\sHiLo|2\-7\sLow\sTriple\sDraw|Badugi|Seven\sCard\sStud|Seven\sCard\sStud\sHiLo|Five\sCard\sStud|Razz|HORSE|HA|HO)\s+?</td>"""
125 )
126 re_HTMLBuyIn = re.compile("Buy In\s+?</th>\s+?<td>(?P<BUYIN>[0-9,.]+)\s+?</td>")
127 re_HTMLFee = re.compile("Entry Fee\s+?</th>\s+?<td>(?P<FEE>[0-9,.]+)\s+?</td>")
128 re_HTMLBounty = re.compile("Bounty\s+?</th>\s+?<td>(?P<KOBOUNTY>.+?)\s+?</td>")
129 re_HTMLAddons = re.compile("Addons\s+?</th>\s+?<td>(?P<ADDON>.+?)\s+?</td>")
130 re_HTMLRebuy = re.compile("Rebuys\s+?</th>\s+?<td>(?P<REBUY>.+?)\s+?</td>")
131 re_HTMLTourNo = re.compile("Game ID</th>\s+?<td>(?P<TOURNO>[0-9]+)-1</td>")
132 re_HTMLPlayer = re.compile(
133 """<tr>(<td align="center">)?\s+?(?P<RANK>\d+)</td>\s+?<td>(?P<PNAME>.+?)</td>\s+?<td>(?P<WINNINGS>.+?)</td>\s+?</tr>"""
134 )
135 # re_HTMLDetails = re.compile(u"""<p class="text">(?P<LABEL>.+?) : (?P<VALUE>.+?)</p>""")
136 re_HTMLPrizepool = re.compile("""(Freeroll|Total) Prizepool\s+?</th>\s+?<td>(?P<PRIZEPOOL>[0-9,.]+)\s+?</td>""")
137 re_HTMLStartTime = re.compile("Start Time\s+?</th>\s+?<td>(?P<STARTTIME>.+?)\s+?</td>")
138 re_HTMLDateTime = re.compile(
139 "\w+?\s+?(?P<D>\d+)\w+?\s+(?P<M>\w+)\s+(?P<Y>\d+),?\s+(?P<H>\d+):(?P<MIN>\d+):(?P<S>\d+)"
140 )
142 re_Ticket = re.compile(""" / Ticket (?P<VALUE>[0-9.]+)€""")
144 codepage = ["utf-8"]
146 @staticmethod
147 def getSplitRe(self, head):
148 re_SplitTourneys = re.compile("PokerStars Tournament ")
149 return re_SplitTourneys
151 def parseSummary(self):
152 # id type of file and call correct function
153 m = self.re_GameTypeHH.search(self.summaryText)
154 if m:
155 mg = m.groupdict()
156 if " Tournament" == mg["TYPE"]:
157 self.parseSummaryFromHH(mg)
158 elif not self.re_NotFound.search(self.summaryText):
159 self.parseSummaryFile()
160 else:
161 log.error(("The tournament was not found or is invalid"))
162 raise FpdbParseError
164 def parseSummaryFromHH(self, mg):
165 obj = getattr(MergeToFpdb, "Merge", None)
166 hhc = obj(self.config, in_path=self.in_path, sitename=None, autostart=False)
167 # update = False
168 handsList = hhc.allHandsAsList()
169 handsDict = {}
170 Structures = MergeStructures.MergeStructures()
171 for handText in handsList:
172 m = self.re_HandInfoHH.search(handText)
173 if m is None:
174 tmp = self.summaryText[0:200]
175 log.error(("MergeSummary.readHandInfo: '%s'") % tmp)
176 continue
177 tourNo = re.split("-", m.group("TDATA"))[0]
178 hands = handsDict.get(tourNo)
179 if hands is None:
180 handsDict[tourNo] = [handText]
181 else:
182 hands.append(handText)
183 for tourNo, hands in list(handsDict.items()):
184 self.resetInfo()
185 self.db.resetBulkCache()
186 m = self.re_GameTypeHH.search(hands[0])
187 if m:
188 mg = m.groupdict()
190 if "LIMIT" in mg:
191 self.gametype["limitType"] = self.limits[mg["LIMIT"]]
192 if "GAME" in mg:
193 if mg["GAME"] == "HORSE":
194 log.error(("MergeSummary.determineGameType: HORSE found, unsupported"))
195 raise FpdbParseError
196 # (self.info['base'], self.info['category']) = self.Multigametypes[m2.group('MULTIGAMETYPE')]
197 else:
198 self.gametype["category"] = self.games[mg["GAME"]][1]
199 if "SEATS" in mg and mg["SEATS"] is not None:
200 self.maxseats = int(mg["SEATS"])
202 for handText in hands:
203 m = self.re_HandInfoHH.search(handText)
204 if m is None:
205 tmp = self.summaryText[0:200]
206 log.error(("MergeSummary.readHandInfo: '%s'") % tmp)
207 continue
208 # raise FpdbParseError
209 # print 'DEBUG:', m.groupdict()
211 tourneyNameFull = m.group("TABLENAME").replace(" - ", " - ").strip()
212 self.tourneyName = m.group("TABLENAME")[:40]
213 self.tourNo = tourNo
214 m1 = self.re_DateTimeHH.search(m.group("DATETIME"))
215 if m1:
216 mg = m1.groupdict()
217 datetimestr = "%s/%s/%s %s:%s:%s" % (mg["Y"], mg["M"], mg["D"], mg["H"], mg["MIN"], mg["S"])
218 # tz = a.group('TZ') # just assume ET??
219 self.startTime = datetime.datetime.strptime(
220 datetimestr, "%Y/%m/%d %H:%M:%S"
221 ) # also timezone at end, e.g. " ET"
222 else:
223 self.startTime = datetime.datetime.strptime(m.group("DATETIME")[:14], "%Y%m%d%H%M%S")
224 self.startTime = HandHistoryConverter.changeTimezone(self.startTime, "ET", "UTC")
226 if self.re_turboHH.match(tourneyNameFull):
227 if self.maxseats == 6:
228 tourneyNameFull += " (6-max)"
230 structure = Structures.lookupSnG(tourneyNameFull, self.startTime)
231 if structure is None:
232 log.error(("MergeSummary.determineGameType: No match in SnG_Structures"))
233 continue
234 raise FpdbParseError
236 self.buyin = int(100 * structure["buyIn"])
237 self.fee = int(100 * structure["fee"])
238 if "max" in structure:
239 self.entries = structure["max"]
240 else:
241 self.entries = structure["seats"]
242 self.buyinCurrency = structure["currency"]
243 self.currency = structure["payoutCurrency"]
244 self.maxseats = structure["seats"]
245 if "speed" in structure:
246 self.speed = structure["speed"]
247 if "doubleOrNothing" in structure:
248 self.isDoubleOrNothing = True
249 if "bounty" in structure:
250 self.isKO = True
251 self.koBounty = int(100 * structure["bounty"])
253 self.prizepool = sum(structure["payouts"])
254 payouts = len(structure["payouts"])
255 self.isSng = True
257 if structure["multi"]:
258 log.error(("MergeSummary.determineGameType: Muli-table SnG found, unsupported"))
259 continue
261 players, out, won = {}, [], []
262 for m in hhc.re_PlayerOut.finditer(handText):
263 if m.group("PSEAT") is not None:
264 out.append(m.group("PSEAT"))
265 if out:
266 for m in hhc.re_PlayerInfo.finditer(handText):
267 players[m.group("SEAT")] = m.group("PNAME")
268 if not players:
269 continue
270 for m in hhc.re_CollectPot.finditer(handText):
271 won.append(m.group("PSEAT"))
273 if self.isDoubleOrNothing:
274 if handText == hands[-1]:
275 won = [w for w in list(players.keys()) if w not in out or w in won]
276 out = [p for p in list(players.keys())]
277 i = 0
278 for n in out:
279 winnings = 0
280 if n in won:
281 rank = 1
282 winnings = int(100 * structure["payouts"][0])
283 else:
284 rank = len(players) - i
285 if rank <= payouts:
286 winnings = int(100 * structure["payouts"][rank - 1])
287 i += 1
288 self.addPlayer(rank, players[n], winnings, self.currency, None, None, None)
289 self.insertOrUpdate()
291 def resetInfo(self):
292 self.tourneyName = None
293 self.tourneyTypeId = None
294 self.tourneyId = None
295 self.startTime = None
296 self.endTime = None
297 self.tourNo = None
298 self.currency = None
299 self.buyinCurrency = None
300 self.buyin = 0
301 self.fee = 0
302 self.hero = None
303 self.maxseats = 0
304 self.entries = 0
305 self.speed = "Normal"
306 self.prizepool = 0 # Make it a dict in order to deal (eventually later) with non-money winnings : {'MONEY' : amount, 'OTHER' : Value ??}
307 self.buyInChips = 0
308 self.mixed = None
309 self.isRebuy = False
310 self.isAddOn = False
311 self.isKO = False
312 self.isMatrix = False
313 self.isShootout = False
314 self.isZoom = False
315 self.matrixMatchId = (
316 None # For Matrix tourneys : 1-4 => match tables (traditionnal), 0 => Positional winnings info
317 )
318 self.matrixIdProcessed = None
319 self.subTourneyBuyin = None
320 self.subTourneyFee = None
321 self.rebuyChips = None
322 self.addOnChips = None
323 self.rebuyCost = 0
324 self.addOnCost = 0
325 self.totalRebuyCount = None
326 self.totalAddOnCount = None
327 self.koBounty = 0
328 self.tourneyComment = None
329 self.players = {}
330 self.isSng = False
331 self.isSatellite = False
332 self.isDoubleOrNothing = False
333 self.guarantee = None
334 self.added = None
335 self.addedCurrency = None
336 self.gametype = {"category": None, "limitType": None, "mix": "none"}
337 self.comment = None
338 self.commentTs = None
340 # Collections indexed by player names
341 self.playerIds = {}
342 self.tourneysPlayersIds = {}
343 self.ranks = {}
344 self.winnings = {}
345 self.winningsCurrency = {}
346 self.rebuyCounts = {}
347 self.addOnCounts = {}
348 self.koCounts = {}
350 # currency symbol for this summary
351 self.sym = None
353 def parseSummaryFile(self):
354 self.buyinCurrency = "USD"
355 soup = BeautifulSoup(self.summaryText)
356 tables = soup.findAll("table")
357 if len(tables) > 1:
358 table1 = BeautifulSoup(str(tables[0])).findAll("tr")
359 table2 = BeautifulSoup(str(tables[1])).findAll("tr")
360 # FIXME: Searching every line for all regexes is pretty horrible
361 # FIXME: Need to search for 'Status: Finished'
362 # print self.in_path
363 for p in table1:
364 m = self.re_HTMLGameType.search(str(p))
365 if m:
366 # print "DEBUG: re_HTMLGameType: '%s' '%s'" %(m.group('LIMIT'), m.group('GAME'))
367 if m.group("GAME").strip() in self.mixes:
368 self.gametype["category"] = self.mixes[m.group("GAME").strip()]
369 else:
370 self.gametype["category"] = self.games_html[m.group("GAME").strip()][1]
371 self.gametype["limitType"] = self.limits[m.group("LIMIT").strip()]
372 m = self.re_HTMLTourNo.search(str(p))
373 if m:
374 # print "DEBUG: re_HTMLTourNo: '%s'" % m.group('TOURNO')
375 self.tourNo = m.group("TOURNO").strip()
376 m = self.re_HTMLName.search(str(p))
377 if m:
378 # print "DEBUG: re_HTMLName: '%s'" % m.group('NAME')
379 self.tourneyName = m.group("NAME").strip()[:40]
380 if m.group("NAME").find("$") != -1:
381 self.buyinCurrency = "USD"
382 elif m.group("NAME").find("€") != -1:
383 self.buyinCurrency = "EUR"
384 m = self.re_HTMLPrizepool.search(str(p))
385 if m:
386 # print "DEBUG: re_HTMLPrizepool: '%s'" % m.group('PRIZEPOOL')
387 self.prizepool = int(self.convert_to_decimal(m.group("PRIZEPOOL").strip()))
388 m = self.re_HTMLBuyIn.search(str(p))
389 if m:
390 # print "DEBUG: re_HTMLBuyIn: '%s'" % m.group('BUYIN')
391 self.buyin = int(100 * self.convert_to_decimal(m.group("BUYIN").strip()))
392 if self.buyin == 0:
393 self.buyinCurrency = "FREE"
394 m = self.re_HTMLFee.search(str(p))
395 if m:
396 # print "DEBUG: re_HTMLFee: '%s'" % m.group('FEE')
397 self.fee = int(100 * self.convert_to_decimal(m.group("FEE").strip()))
398 m = self.re_HTMLBounty.search(str(p))
399 if m:
400 # print "DEBUG: re_HTMLBounty: '%s'" % m.group('KOBOUNTY')
401 if m.group("KOBOUNTY").strip() != "0.00":
402 self.isKO = True
403 self.koBounty = int(100 * self.convert_to_decimal(m.group("KOBOUNTY").strip()))
404 m = self.re_HTMLAddons.search(str(p))
405 if m:
406 # print "DEBUG: re_HTMLAddons: '%s'" % m.group('ADDON')
407 if m.group("ADDON").strip() != "0":
408 self.isAddOn = True
409 self.addOnCost = self.buyin
410 m = self.re_HTMLRebuy.search(str(p))
411 if m:
412 # print "DEBUG: re_HTMLRebuy: '%s'" % m.group('REBUY')
413 if m.group("REBUY").strip() != "0":
414 self.isRebuy = True
415 self.rebuyCost = self.buyin
416 m = self.re_HTMLStartTime.search(str(p))
417 if m:
418 m2 = self.re_HTMLDateTime.search(m.group("STARTTIME"))
419 if m2:
420 month = self.months[m2.group("M")]
421 datetimestr = "%s/%s/%s %s:%s:%s" % (
422 m2.group("Y"),
423 month,
424 m2.group("D"),
425 m2.group("H"),
426 m2.group("MIN"),
427 m2.group("S"),
428 )
429 self.startTime = datetime.datetime.strptime(datetimestr, "%Y/%m/%d %H:%M:%S")
430 self.startTime = HandHistoryConverter.changeTimezone(self.startTime, "ET", "UTC")
432 self.currency = self.buyinCurrency
433 for p in table2:
434 m = self.re_HTMLPlayer.search(str(p))
435 if m:
436 self.entries += 1
437 # print "DEBUG: rank: %s pname: %s won: %s" %(m.group('RANK'), m.group('PNAME'), m.group('WINNINGS'))
438 winnings = 0
439 rebuyCount = None
440 addOnCount = None
441 koCount = None
443 rank = int(m.group("RANK"))
444 name = m.group("PNAME")
445 if m.group("WINNINGS") is not None:
446 if m.group("WINNINGS").find("$") != -1:
447 self.currency = "USD"
448 elif m.group("WINNINGS").find("€") != -1:
449 self.currency = "EUR"
450 winnings = int(100 * self.convert_to_decimal(m.group("WINNINGS")))
451 self.addPlayer(rank, name, winnings, self.currency, rebuyCount, addOnCount, koCount)
453 if self.gametype["category"] is None:
454 log.error(("MergeSummary.parseSummaryFile: Could not parse summary file"))
455 raise FpdbParseError
457 def convert_to_decimal(self, string):
458 dec = self.clearMoneyString(string)
459 dec = Decimal(dec)
460 return dec