Expanded my entry in contributors.txt.
[fpdb-dooglus.git] / pyfpdb / FulltiltToFpdb.py
blob7a1cced4cb0820839022fdecfce6b94d031ce81f
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
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.
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.
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 ########################################################################
21 import L10n
22 _ = L10n.get_translation()
24 import logging
25 from HandHistoryConverter import *
26 #import TourneySummary
28 # Fulltilt HH Format converter
30 class Fulltilt(HandHistoryConverter):
32 sitename = "Fulltilt"
33 filetype = "text"
34 codepage = ["utf-16", "cp1252", "utf-8"]
35 siteId = 1 # Needs to match id entry in Sites database
37 substitutions = {
38 'LEGAL_ISO' : "USD|EUR|GBP|CAD|FPP", # legal ISO currency codes
39 'LS' : u"\$|\u20AC|\xe2\x82\xac|", # legal currency symbols - Euro(cp1252, utf-8)
40 'TAB' : u"-\u2013'\s\da-zA-Z#_", # legal characters for tablename
41 'NUM' : u".,\d", # legal characters in number format
44 Lim_Blinds = { '0.04': ('0.01', '0.02'), '0.10': ('0.02', '0.05'), '0.20': ('0.05', '0.10'),
45 '0.40': ('0.10', '0.20'), '0.50': ('0.10', '0.25'),
46 '1.00': ('0.25', '0.50'), '1': ('0.25', '0.50'),
47 '2.00': ('0.50', '1.00'), '2': ('0.50', '1.00'),
48 '4.00': ('1.00', '2.00'), '4': ('1.00', '2.00'),
49 '5.00': ('1.25', '2.50'), '5': ('1.25', '2.50'),
50 '6.00': ('1.00', '3.00'), '6': ('1.00', '3.00'),
51 '8.00': ('2.00', '4.00'), '8': ('2.00', '4.00'),
52 '10.00': ('2.00', '5.00'), '10': ('2.00', '5.00'),
53 '16.00': ('4.00', '8.00'), '16': ('4.00', '8.00'),
54 '20.00': ('5.00', '10.00'), '20': ('5.00', '10.00'),
55 '30.00': ('10.00', '15.00'), '30': ('10.00', '15.00'),
56 '40.00': ('10.00', '20.00'), '40': ('10.00', '20.00'),
57 '60.00': ('15.00', '30.00'), '60': ('15.00', '30.00'),
58 '80.00': ('20.00', '40.00'), '80': ('20.00', '40.00'),
59 '100.00': ('25.00', '50.00'), '100': ('25.00', '50.00'),
60 '200.00': ('50.00', '100.00'), '200': ('50.00', '100.00'),
61 '300.00': ('75.00', '150.00'), '300': ('75.00', '150.00'),
62 '400.00': ('100.00', '200.00'), '400': ('100.00', '200.00'),
63 '500.00': ('125.00', '250.00'), '500': ('125.00', '250.00'),
64 '800.00': ('200.00', '400.00'), '800': ('200.00', '400.00'),
65 '1000.00': ('250.00', '500.00'),'1000': ('250.00', '500.00'),
66 '2000.00': ('500.00', '750.00'),'2000': ('500.00', '1000.00'),
67 '3000.00': ('750.00', '1500.00'),'3000': ('750.00', '1500.00'),
70 # Static regexes
71 re_GameInfo = re.compile(u'''.*\#(?P<HID>[0-9]+):\s
72 (?:(?P<TOURNAMENT>.+)\s\((?P<TOURNO>\d+)\),\s)?
74 -\s(?P<CURRENCY>[%(LS)s]|)?
75 (?P<SB>[%(NUM)s]+)/
76 [%(LS)s]?(?P<BB>[%(NUM)s]+)\s
77 (Ante\s\$?(?P<ANTE>[%(NUM)s]+)\s)?-\s
78 [%(LS)s]?(?P<CAP>[%(NUM)s]+\sCap\s)?
79 (?P<LIMIT>(No\sLimit|Pot\sLimit|Limit))?\s
80 (?P<GAME>(Hold\'em|Omaha(\sH/L|\sHi/Lo|\sHi|)|7\sCard\sStud|Stud\sH/L|Razz|Stud\sHi|2-7\sTriple\sDraw|5\sCard\sDraw|Badugi|2-7\sSingle\sDraw))
81 ''' % substitutions, re.VERBOSE)
82 re_SplitHands = re.compile(r"\n\n\n+")
83 re_TailSplitHands = re.compile(r"(\n\n+)")
84 re_HandInfo = re.compile(u'''.*\#(?P<HID>[0-9]+):\s
85 (?:(?P<TOURNAMENT>.+)\s\((?P<TOURNO>\d+)\),\s)?
86 ((Table|Match)\s)?
87 (?P<PLAY>Play\sChip\s|PC)?
88 ((?P<TABLE>[%(TAB)s]+)(\s|,))
89 (?P<ENTRYID>\sEntry\s\#\d+\s)?
90 (\((?P<TABLEATTRIBUTES>.+)\)\s)?-\s
91 [%(LS)s]?(?P<SB>[%(NUM)s]+)/[%(LS)s]?(?P<BB>[%(NUM)s]+)\s(Ante\s[%(LS)s]?(?P<ANTE>[.0-9]+)\s)?-\s
92 [%(LS)s]?(?P<CAP>[%(NUM)s]+\sCap\s)?
93 (?P<GAMETYPE>[-\da-zA-Z\/\'\s]+)\s-\s
94 (?P<DATETIME>.*$)
95 (?P<PARTIAL>\(partial\))?\s
96 (?:.*?\n(?P<CANCELLED>Hand\s\#(?P=HID)\shas\sbeen\scanceled))?
97 ''' % substitutions, re.MULTILINE|re.VERBOSE)
98 re_TourneyExtraInfo = re.compile('''(((?P<TOURNEY_NAME>[^$]+)?
99 (?P<CURRENCY>[%(LS)s])?(?P<BUYIN>[.0-9]+)?\s*\+\s*[%(LS)s]?(?P<FEE>[.0-9]+)?
100 (\s(?P<SPECIAL>(KO|Heads\sUp|Matrix\s\dx|Rebuy|Madness)))?
101 (\s(?P<SHOOTOUT>Shootout))?
102 (\s(?P<SNG>Sit\s&\sGo))?
103 (\s\((?P<TURBO>Turbo)\))?)|(?P<UNREADABLE_INFO>.+))
104 ''' % substitutions, re.VERBOSE)
105 re_Button = re.compile('^The button is in seat #(?P<BUTTON>\d+)', re.MULTILINE)
106 re_PlayerInfo = re.compile('Seat (?P<SEAT>[0-9]+): (?P<PNAME>.{2,15}) \([%(LS)s]?(?P<CASH>[%(NUM)s]+)\)(?P<SITOUT>, is sitting out)?$' % substitutions, re.MULTILINE)
107 re_SummarySitout = re.compile('Seat (?P<SEAT>[0-9]+): (?P<PNAME>.{2,15}?) (\(button\) )?is sitting out?$' % substitutions, re.MULTILINE)
108 re_Board = re.compile(r"\[(?P<CARDS>.+)\]")
110 #static regex for tourney purpose
111 re_TourneyInfo = re.compile('''Tournament\sSummary\s
112 (?P<TOURNAMENT_NAME>[^$(]+)?\s*
113 ((?P<CURRENCY>[%(LS)s]|)?(?P<BUYIN>[.0-9]+)\s*\+\s*[%(LS)s]?(?P<FEE>[.0-9]+)\s)?
114 ((?P<SPECIAL>(KO|Heads\sUp|Matrix\s\dx|Rebuy|Madness))\s)?
115 ((?P<SHOOTOUT>Shootout)\s)?
116 ((?P<SNG>Sit\s&\sGo)\s)?
117 (\((?P<TURBO1>Turbo)\)\s)?
118 \((?P<TOURNO>\d+)\)\s
119 ((?P<MATCHNO>Match\s\d)\s)?
120 (?P<GAME>(Hold\'em|Omaha(\sHi/Lo|\sH/L|\sHi|)|7\sCard\sStud|Stud\sH/L|Razz|Stud\sHi))\s
121 (\((?P<TURBO2>Turbo)\)\s)?
122 (?P<LIMIT>(No\sLimit|Pot\sLimit|Limit))?
123 ''' % substitutions, re.VERBOSE)
124 re_TourneyBuyInFee = re.compile("Buy-In: (?P<BUYIN_CURRENCY>[%(LS)s]|)?(?P<BUYIN>[.0-9]+) \+ [%(LS)s]?(?P<FEE>[.0-9]+)" % substitutions)
125 re_TourneyBuyInChips = re.compile("Buy-In Chips: (?P<BUYINCHIPS>\d+)")
126 re_TourneyEntries = re.compile("(?P<ENTRIES>\d+) Entries")
127 re_TourneyPrizePool = re.compile("Total Prize Pool: (?P<PRIZEPOOL_CURRENCY>[%(LS)s]|)?(?P<PRIZEPOOL>[.,0-9]+)" % substitutions)
128 re_TourneyRebuyCost = re.compile("Rebuy: (?P<REBUY_CURRENCY>[%(LS)s]|)?(?P<REBUY_COST>[.,0-9]+)"% substitutions)
129 re_TourneyAddOnCost = re.compile("Add-On: (?P<ADDON_CURRENCY>[%(LS)s]|)?(?P<ADDON_COST>[.,0-9]+)"% substitutions)
130 re_TourneyRebuyCount = re.compile("performed (?P<REBUY_COUNT>\d+) Rebuy")
131 re_TourneyAddOnCount = re.compile("performed (?P<ADDON_COUNT>\d+) Add-On")
132 re_TourneyRebuysTotal = re.compile("Total Rebuys: (?P<REBUY_TOTAL>\d+)")
133 re_TourneyAddOnsTotal = re.compile("Total Add-Ons: (?P<ADDONS_TOTAL>\d+)")
134 re_TourneyRebuyChips = re.compile("Rebuy Chips: (?P<REBUY_CHIPS>\d+)")
135 re_TourneyAddOnChips = re.compile("Add-On Chips: (?P<ADDON_CHIPS>\d+)")
136 re_TourneyKOBounty = re.compile("Knockout Bounty: (?P<KO_BOUNTY_CURRENCY>[%(LS)s]|)?(?P<KO_BOUNTY_AMOUNT>[.,0-9]+)" % substitutions)
137 re_TourneyKoCount = re.compile("received (?P<COUNT_KO>\d+) Knockout Bounty Award(s)?")
138 re_TourneyTimeInfo = re.compile("Tournament started: (?P<STARTTIME>.*)\nTournament ((?P<IN_PROGRESS>is still in progress)?|(finished:(?P<ENDTIME>.*))?)$")
140 re_TourneysPlayersSummary = re.compile("^(?P<RANK>(Still Playing|\d+))( - |: )(?P<PNAME>[^\n,]+)(, )?(?P<WINNING_CURRENCY>[%(LS)s]|)?(?P<WINNING>[.\d]+)?" % substitutions, re.MULTILINE)
141 re_TourneyHeroFinishingP = re.compile("(?P<HERO_NAME>.*) finished in (?P<HERO_FINISHING_POS>\d+)(st|nd|rd|th) place")
143 #TODO: See if we need to deal with play money tourney summaries -- Not right now (they shouldn't pass the re_TourneyInfo)
144 ##Full Tilt Poker Tournament Summary 250 Play Money Sit & Go (102909471) Hold'em No Limit
145 ##Buy-In: 250 Play Chips + 0 Play Chips
146 ##Buy-In Chips: 1500
147 ##6 Entries
148 ##Total Prize Pool: 1,500 Play Chips
150 # These regexes are for FTP only
151 re_Mixed = re.compile(r'\s\-\s(?P<MIXED>7\-Game|8\-Game|9\-Game|10\-Game|HA|HEROS|HO|HOE|HORSE|HOSE|OA|OE|SE)\s\-\s', re.VERBOSE)
152 re_Max = re.compile("(?P<MAX>\d+)( max)?", re.MULTILINE)
153 # NB: if we ever match "Full Tilt Poker" we should also match "FullTiltPoker", which PT Stud erroneously exports.
154 re_DateTime = re.compile("""((?P<H>[0-9]+):(?P<MIN>[0-9]+):(?P<S>[0-9]+)\s(?P<TZ>\w+)\s-\s(?P<Y>[0-9]{4})\/(?P<M>[0-9]{2})\/(?P<D>[0-9]{2})|(?P<H2>[0-9]+):(?P<MIN2>[0-9]+)\s(?P<TZ2>\w+)\s-\s\w+\,\s(?P<M2>\w+)\s(?P<D2>\d+)\,\s(?P<Y2>[0-9]{4}))(?P<PARTIAL>\s\(partial\))?""", re.MULTILINE)
156 def compilePlayerRegexs(self, hand):
157 players = set([player[1] for player in hand.players])
158 if not players <= self.compiledPlayers: # x <= y means 'x is subset of y'
159 # we need to recompile the player regexs.
160 self.compiledPlayers = players
161 player_re = "(?P<PNAME>" + "|".join(map(re.escape, players)) + ")"
162 self.substitutions['PLAYERS'] = player_re
164 logging.debug("player_re: " + player_re)
165 self.re_PostSB = re.compile(r"^%(PLAYERS)s posts the small blind of [%(LS)s]?(?P<SB>[%(NUM)s]+)" % self.substitutions, re.MULTILINE)
166 self.re_PostDead = re.compile(r"^%(PLAYERS)s posts a dead small blind of [%(LS)s]?(?P<SB>[%(NUM)s]+)" % self.substitutions, re.MULTILINE)
167 self.re_PostBB = re.compile(r"^%(PLAYERS)s posts (the big blind of )?[%(LS)s]?(?P<BB>[%(NUM)s]+)" % self.substitutions, re.MULTILINE)
168 self.re_Antes = re.compile(r"^%(PLAYERS)s antes [%(LS)s]?(?P<ANTE>[%(NUM)s]+)" % self.substitutions, re.MULTILINE)
169 self.re_ReturnsAnte = re.compile(r"^Ante of [%(LS)s]?[%(NUM)s]+ returned to %(PLAYERS)s" % self.substitutions, re.MULTILINE)
170 self.re_BringIn = re.compile(r"^%(PLAYERS)s brings in for [%(LS)s]?(?P<BRINGIN>[%(NUM)s]+)" % self.substitutions, re.MULTILINE)
171 self.re_PostBoth = re.compile(r"^%(PLAYERS)s posts small \& big blinds \[[%(LS)s]? (?P<SBBB>[%(NUM)s]+)" % self.substitutions, re.MULTILINE)
172 self.re_HeroCards = re.compile(r"^Dealt to %s(?: \[(?P<OLDCARDS>.+?)\])?( \[(?P<NEWCARDS>.+?)\])" % player_re, re.MULTILINE)
173 self.re_Action = re.compile(r"^%(PLAYERS)s(?P<ATYPE> bets| checks| raises to| completes it to| calls| folds| discards| stands pat)( [%(LS)s]?(?P<BET>[%(NUM)s]+))?(\son|\scards?)?(\s\[(?P<CARDS>.+?)\])?" % self.substitutions, re.MULTILINE)
174 self.re_ShowdownAction = re.compile(r"^%s shows \[(?P<CARDS>.*)\]" % player_re, re.MULTILINE)
175 self.re_CollectPot = re.compile(r"^Seat (?P<SEAT>[0-9]+): %(PLAYERS)s (\(button\) |\(small blind\) |\(big blind\) )?(collected|showed \[.*\] and won) \([%(LS)s]?(?P<POT>[%(NUM)s]+)\)(, mucked| with.*)?" % self.substitutions, re.MULTILINE)
176 self.re_SitsOut = re.compile(r"^%s sits out" % player_re, re.MULTILINE)
177 self.re_ShownCards = re.compile(r"^Seat (?P<SEAT>[0-9]+): %s (\(button\) |\(small blind\) |\(big blind\) )?(?P<SHOWED>showed|mucked) \[(?P<CARDS>.*)\](( and won \(.*\) with | and lost with | \- )(?P<STRING>.*))?" % player_re, re.MULTILINE)
179 def readSupportedGames(self):
180 return [["ring", "hold", "nl"],
181 ["ring", "hold", "pl"],
182 ["ring", "hold", "fl"],
183 ["ring", "hold", "cn"],
185 ["ring", "stud", "fl"],
187 ["ring", "draw", "fl"],
188 ["ring", "draw", "pl"],
189 ["ring", "draw", "nl"],
191 ["tour", "hold", "nl"],
192 ["tour", "hold", "pl"],
193 ["tour", "hold", "fl"],
194 ["tour", "hold", "cn"],
196 ["tour", "stud", "fl"],
198 ["tour", "draw", "fl"],
199 ["tour", "draw", "pl"],
200 ["tour", "draw", "nl"],
203 def determineGameType(self, handText):
204 info = {'type':'ring'}
206 m = self.re_GameInfo.search(handText)
207 if not m:
208 tmp = handText[0:100]
209 log.error(_("Unable to recognise gametype from: '%s'") % tmp)
210 log.error("determineGameType: " + _("Raising FpdbParseError for file '%s'") % self.in_path)
211 raise FpdbParseError(_("Unable to recognise gametype from: '%s'") % tmp)
212 mg = m.groupdict()
214 # translations from captured groups to our info strings
215 limits = { 'No Limit':'nl', 'Pot Limit':'pl', 'Limit':'fl' }
216 games = { # base, category
217 "Hold'em" : ('hold','holdem'),
218 'Omaha Hi' : ('hold','omahahi'),
219 'Omaha' : ('hold','omahahi'),
220 'Omaha H/L' : ('hold','omahahilo'),
221 'Omaha Hi/Lo' : ('hold','omahahilo'),
222 'Razz' : ('stud','razz'),
223 'Stud Hi' : ('stud','studhi'),
224 'Stud H/L' : ('stud','studhilo'),
225 '2-7 Triple Draw' : ('draw','27_3draw'),
226 '5 Card Draw' : ('draw','fivedraw'),
227 'Badugi' : ('draw','badugi'),
228 '2-7 Single Draw' : ('draw','27_1draw')
230 mixes = {
231 '7-Game' : '7game',
232 '8-Game' : '8game',
233 '9-Game' : '9game',
234 '10-Game' : '10game',
235 'HA' : 'ha',
236 'HEROS' : 'heros',
237 'HO' : 'ho',
238 'HOE' : 'hoe',
239 'HORSE' : 'horse',
240 'HOSE' : 'hose',
241 'OA' : 'oa',
242 'OE' : 'oe',
243 'SE' : 'se'
245 currencies = { u'€':'EUR', '$':'USD', '':'T$' }
247 if 'SB' in mg:
248 info['sb'] = self.clearMoneyString(mg['SB'])
250 if 'BB' in mg:
251 info['bb'] = self.clearMoneyString(mg['BB'])
253 if mg['TOURNO'] is None: info['type'] = "ring"
254 else: info['type'] = "tour"
256 if mg['CAP']:
257 info['limitType'] = 'cn'
258 else:
259 info['limitType'] = limits[mg['LIMIT']]
261 if info['limitType'] == 'fl' and info['bb'] is not None and info['type'] == 'ring':
262 try:
263 bb = self.clearMoneyString(mg['BB'])
264 info['sb'] = self.Lim_Blinds[bb][0]
265 info['bb'] = self.Lim_Blinds[bb][1]
266 except KeyError:
267 log.error(_("Lim_Blinds has no lookup for '%s'") % mg['BB'])
268 log.error("determineGameType: " + _("Raising FpdbParseError"))
269 raise FpdbParseError(_("Lim_Blinds has no lookup for '%s'") % mg['BB'])
271 if mg['GAME'] is not None:
272 (info['base'], info['category']) = games[mg['GAME']]
273 if mg['CURRENCY'] is not None:
274 info['currency'] = currencies[mg['CURRENCY']]
275 # NB: SB, BB must be interpreted as blinds or bets depending on limit type.
276 m = self.re_Mixed.search(self.in_path)
277 if m: info['mix'] = mixes[m.groupdict()['MIXED']]
279 return info
281 def readHandInfo(self, hand):
282 m = self.re_HandInfo.search(hand.handText)
283 if m is None:
284 tmp = hand.handText[0:100]
285 log.error(_("Unable to recognise hand info from: '%s'") % tmp)
286 log.error("readHandInfo: " + _("Raising FpdbParseError"))
287 raise FpdbParseError(_("Unable to recognise hand info from: '%s'"))
289 #print "DEBUG: m.groupdict: %s" % m.groupdict()
290 hand.handid = m.group('HID')
291 hand.tablename = m.group('TABLE')
293 if m.group('DATETIME'):
294 # This section of code should match either a single date (which is ET) or
295 # the last date in the header, which is also recorded in ET.
296 timezone = "ET"
297 m1 = self.re_DateTime.finditer(m.group('DATETIME'))
298 datetimestr = "2000/01/01 00:00:00"
299 for a in m1:
300 if a.group('TZ2') == None:
301 datetimestr = "%s/%s/%s %s:%s:%s" % (a.group('Y'), a.group('M'),a.group('D'),a.group('H'),a.group('MIN'),a.group('S'))
302 timezone = a.group('TZ')
303 hand.startTime = datetime.datetime.strptime(datetimestr, "%Y/%m/%d %H:%M:%S")
304 else: # Short-lived date format
305 datetimestr = "%s/%s/%s %s:%s" % (a.group('Y2'), a.group('M2'),a.group('D2'),a.group('H2'),a.group('MIN2'))
306 timezone = a.group('TZ2')
307 hand.startTime = datetime.datetime.strptime(datetimestr, "%Y/%B/%d %H:%M")
308 if a.group('PARTIAL'):
309 raise FpdbParseError(hid=m.group('HID'))
311 hand.startTime = HandHistoryConverter.changeTimezone(hand.startTime, timezone, "UTC")
313 if m.group("CANCELLED") or m.group("PARTIAL"):
314 # It would appear this can't be triggered as DATETIME is a bit greedy
315 raise FpdbParseError(hid=m.group('HID'))
317 if m.group('TABLEATTRIBUTES'):
318 m2 = self.re_Max.search(m.group('TABLEATTRIBUTES'))
319 if m2: hand.maxseats = int(m2.group('MAX'))
321 hand.tourNo = m.group('TOURNO')
322 if m.group('PLAY') is not None:
323 hand.gametype['currency'] = 'play'
325 # Done: if there's a way to figure these out, we should.. otherwise we have to stuff it with unknowns
326 if m.group('TOURNAMENT') is not None:
327 n = self.re_TourneyExtraInfo.search(m.group('TOURNAMENT'))
328 if n.group('UNREADABLE_INFO') is not None:
329 hand.tourneyComment = n.group('UNREADABLE_INFO')
330 else:
331 hand.tourneyComment = n.group('TOURNEY_NAME') # can be None
332 if (n.group('CURRENCY') is not None and n.group('BUYIN') is not None and n.group('FEE') is not None):
333 if n.group('CURRENCY')=="$":
334 hand.buyinCurrency="USD"
335 elif n.group('CURRENCY')==u"€":
336 hand.buyinCurrency="EUR"
337 else:
338 hand.buyinCurrency="NA"
339 hand.buyin = int(100*Decimal(n.group('BUYIN')))
340 hand.fee = int(100*Decimal(n.group('FEE')))
341 if n.group('TURBO') is not None :
342 hand.speed = "Turbo"
343 if n.group('SPECIAL') is not None :
344 special = n.group('SPECIAL')
345 if special == "Rebuy":
346 hand.isRebuy = True
347 if special == "KO":
348 hand.isKO = True
349 if special == "Head's Up" or special == "Heads Up":
350 hand.maxseats = 2
351 if re.search("Matrix", special):
352 hand.isMatrix = True
353 if special == "Shootout":
354 hand.isShootout = True
355 if hand.buyin is None:
356 hand.buyin = 0
357 hand.fee=0
358 hand.buyinCurrency="NA"
360 if hand.level is None:
361 hand.level = "0"
363 def readPlayerStacks(self, hand):
364 # Split hand text for FTP, as the regex matches the player names incorrectly
365 # in the summary section
366 pre, post = hand.handText.split('*** SUMMARY ***')
367 m = self.re_PlayerInfo.finditer(pre)
368 plist = {}
370 # Get list of players in header.
371 for a in m:
372 plist[a.group('PNAME')] = [int(a.group('SEAT')), a.group('CASH')]
374 if hand.gametype['type'] == "ring" :
375 # Remove any listed as sitting out in the summary as start of hand info unreliable
376 n = self.re_SummarySitout.finditer(post)
377 for b in n:
378 if b.group('PNAME') in plist:
379 #print "DEBUG: Deleting '%s' from player dict" %(b.group('PNAME'))
380 del plist[b.group('PNAME')]
382 # Add remaining players
383 for a in plist:
384 seat, stack = plist[a]
385 hand.addPlayer(seat, a, stack)
387 if plist == {}:
388 #No players! The hand is either missing stacks or everyone is sitting out
389 raise FpdbParseError(_("readPlayerStacks: No players detected (hand #%s)") % hand.handid)
392 def markStreets(self, hand):
394 if hand.gametype['base'] == 'hold':
395 m = re.search(r"\*\*\* HOLE CARDS \*\*\*(?P<PREFLOP>.+(?=\*\*\* FLOP (1\s)?\*\*\*)|.+)"
396 r"(\*\*\* FLOP \*\*\*(?P<FLOP> \[\S\S \S\S \S\S\].+(?=\*\*\* TURN (1\s)?\*\*\*)|.+))?"
397 r"(\*\*\* TURN \*\*\* \[\S\S \S\S \S\S] (?P<TURN>\[\S\S\].+(?=\*\*\* RIVER (1\s)?\*\*\*)|.+))?"
398 r"(\*\*\* RIVER \*\*\* \[\S\S \S\S \S\S \S\S] (?P<RIVER>\[\S\S\].+))?"
399 r"(\*\*\* FLOP 1 \*\*\*(?P<FLOP1> \[\S\S \S\S \S\S\].+(?=\*\*\* TURN 1 \*\*\*)|.+))?"
400 r"(\*\*\* TURN 1 \*\*\* \[\S\S \S\S \S\S] (?P<TURN1>\[\S\S\].+(?=\*\*\* RIVER 1 \*\*\*)|.+))?"
401 r"(\*\*\* RIVER 1 \*\*\* \[\S\S \S\S \S\S \S\S] (?P<RIVER1>\[\S\S\].))?"
402 r"(\*\*\* FLOP 2 \*\*\*(?P<FLOP2> \[\S\S \S\S \S\S\].+(?=\*\*\* TURN 2 \*\*\*)|.+))?"
403 r"(\*\*\* TURN 2 \*\*\* \[\S\S \S\S \S\S] (?P<TURN2>\[\S\S\].+(?=\*\*\* RIVER 2 \*\*\*)|.+))?"
404 r"(\*\*\* RIVER 2 \*\*\* \[\S\S \S\S \S\S \S\S] (?P<RIVER2>\[\S\S\].+))?", hand.handText,re.DOTALL)
405 elif hand.gametype['base'] == "stud":
406 m = re.search(r"(?P<ANTES>.+(?=\*\*\* 3RD STREET \*\*\*)|.+)"
407 r"(\*\*\* 3RD STREET \*\*\*(?P<THIRD>.+(?=\*\*\* 4TH STREET \*\*\*)|.+))?"
408 r"(\*\*\* 4TH STREET \*\*\*(?P<FOURTH>.+(?=\*\*\* 5TH STREET \*\*\*)|.+))?"
409 r"(\*\*\* 5TH STREET \*\*\*(?P<FIFTH>.+(?=\*\*\* 6TH STREET \*\*\*)|.+))?"
410 r"(\*\*\* 6TH STREET \*\*\*(?P<SIXTH>.+(?=\*\*\* 7TH STREET \*\*\*)|.+))?"
411 r"(\*\*\* 7TH STREET \*\*\*(?P<SEVENTH>.+))?", hand.handText,re.DOTALL)
412 elif hand.gametype['base'] in ("draw"):
413 m = re.search(r"(?P<PREDEAL>.+(?=\*\*\* HOLE CARDS \*\*\*)|.+)"
414 r"(\*\*\* HOLE CARDS \*\*\*(?P<DEAL>.+(?=(\*\*\* FIRST DRAW \*\*\*|\*\*\* DRAW \*\*\*))|.+))?"
415 r"((\*\*\* FIRST DRAW \*\*\*|\*\*\* DRAW \*\*\*)(?P<DRAWONE>.+(?=\*\*\* SECOND DRAW \*\*\*)|.+))?"
416 r"(\*\*\* SECOND DRAW \*\*\*(?P<DRAWTWO>.+(?=\*\*\* THIRD DRAW \*\*\*)|.+))?"
417 r"(\*\*\* THIRD DRAW \*\*\*(?P<DRAWTHREE>.+))?", hand.handText,re.DOTALL)
419 hand.addStreets(m)
421 def readCommunityCards(self, hand, street): # street has been matched by markStreets, so exists in this hand
422 if street in ('FLOP','TURN','RIVER'): # a list of streets which get dealt community cards (i.e. all but PREFLOP)
423 #print "DEBUG readCommunityCards:", street, hand.streets[street]
424 m = self.re_Board.search(hand.streets[street])
425 hand.setCommunityCards(street, m.group('CARDS').split(' '))
426 if street in ('FLOP1', 'TURN1', 'RIVER1', 'FLOP2', 'TURN2', 'RIVER2'):
427 m = self.re_Board.search(hand.streets[street])
428 hand.setCommunityCards(street, m.group('CARDS').split(' '))
429 hand.runItTimes = 2
431 def readBlinds(self, hand):
432 try:
433 m = self.re_PostSB.search(hand.handText)
434 hand.addBlind(m.group('PNAME'), 'small blind', self.clearMoneyString(m.group('SB')))
435 except: # no small blind
436 hand.addBlind(None, None, None)
437 for a in self.re_PostDead.finditer(hand.handText):
438 hand.addBlind(a.group('PNAME'), 'secondsb', self.clearMoneyString(a.group('SB')))
439 for a in self.re_PostBB.finditer(hand.handText):
440 hand.addBlind(a.group('PNAME'), 'big blind', self.clearMoneyString(a.group('BB')))
441 for a in self.re_PostBoth.finditer(hand.handText):
442 hand.addBlind(a.group('PNAME'), 'small & big blinds', self.clearMoneyString(a.group('SBBB')))
444 def readAntes(self, hand):
445 logging.debug(_("reading antes"))
446 slist = []
447 n = self.re_ReturnsAnte.finditer(hand.handText)
448 for player in n:
449 #If a player has their ante returned, then they timed out and are actually sitting out
450 slist.append(player.group('PNAME'))
451 m = self.re_Antes.finditer(hand.handText)
452 for player in m:
453 logging.debug("hand.addAnte(%s,%s)" %(player.group('PNAME'), player.group('ANTE')))
454 if player.group('PNAME') not in slist:
455 hand.addAnte(player.group('PNAME'), player.group('ANTE'))
457 def readBringIn(self, hand):
458 m = self.re_BringIn.search(hand.handText,re.DOTALL)
459 if m:
460 logging.debug(_("Player bringing in: %s for %s") %(m.group('PNAME'), m.group('BRINGIN')))
461 hand.addBringIn(m.group('PNAME'), m.group('BRINGIN'))
462 else:
463 logging.debug(_("No bringin found, handid =%s") % hand.handid)
465 def readButton(self, hand):
466 try:
467 hand.buttonpos = int(self.re_Button.search(hand.handText).group('BUTTON'))
468 except AttributeError, e:
469 # FTP has no indication that a hand is cancelled.
470 raise FpdbParseError(_("%s Failed to detect button (hand #%s cancelled?)") % ("readButton:", hand.handid))
472 def readHeroCards(self, hand):
473 # streets PREFLOP, PREDRAW, and THIRD are special cases beacause
474 # we need to grab hero's cards
475 for street in ('PREFLOP', 'DEAL'):
476 if street in hand.streets.keys():
477 m = self.re_HeroCards.finditer(hand.streets[street])
478 for found in m:
479 # if m == None:
480 # hand.involved = False
481 # else:
482 hand.hero = found.group('PNAME')
483 newcards = found.group('NEWCARDS').split(' ')
484 hand.addHoleCards(street, hand.hero, closed=newcards, shown=False, mucked=False, dealt=True)
486 for street, text in hand.streets.iteritems():
487 if not text or street in ('PREFLOP', 'DEAL'): continue # already done these
488 m = self.re_HeroCards.finditer(hand.streets[street])
489 for found in m:
490 player = found.group('PNAME')
491 if found.group('NEWCARDS') is None:
492 newcards = []
493 else:
494 newcards = found.group('NEWCARDS').split(' ')
495 if found.group('OLDCARDS') is None:
496 oldcards = []
497 else:
498 oldcards = found.group('OLDCARDS').split(' ')
500 if street == 'THIRD' and len(oldcards) == 2: # hero in stud game
501 hand.hero = player
502 hand.dealt.add(player) # need this for stud??
503 hand.addHoleCards(street, player, closed=oldcards, open=newcards, shown=False, mucked=False, dealt=False)
504 else:
505 hand.addHoleCards(street, player, open=newcards, closed=oldcards, shown=False, mucked=False, dealt=False)
508 def readAction(self, hand, street):
509 m = self.re_Action.finditer(hand.streets[street])
510 for action in m:
511 if action.group('ATYPE') == ' raises to':
512 hand.addRaiseTo( street, action.group('PNAME'), action.group('BET') )
513 elif action.group('ATYPE') == ' completes it to':
514 hand.addComplete( street, action.group('PNAME'), action.group('BET') )
515 elif action.group('ATYPE') == ' calls':
516 hand.addCall( street, action.group('PNAME'), action.group('BET') )
517 elif action.group('ATYPE') == ' bets':
518 hand.addBet( street, action.group('PNAME'), action.group('BET') )
519 elif action.group('ATYPE') == ' folds':
520 hand.addFold( street, action.group('PNAME'))
521 elif action.group('ATYPE') == ' checks':
522 hand.addCheck( street, action.group('PNAME'))
523 elif action.group('ATYPE') == ' discards':
524 hand.addDiscard(street, action.group('PNAME'), action.group('BET'), action.group('CARDS'))
525 elif action.group('ATYPE') == ' stands pat':
526 hand.addStandsPat( street, action.group('PNAME'), action.group('CARDS'))
527 else:
528 print (_("DEBUG:") + " " + _("Unimplemented %s: '%s' '%s'") % ("readAction", action.group('PNAME'), action.group('ATYPE')))
531 def readShowdownActions(self, hand):
532 for shows in self.re_ShowdownAction.finditer(hand.handText):
533 cards = shows.group('CARDS')
534 cards = cards.split(' ')
535 hand.addShownCards(cards, shows.group('PNAME'))
537 def readCollectPot(self,hand):
538 for m in self.re_CollectPot.finditer(hand.handText):
539 hand.addCollectPot(player=m.group('PNAME'),pot=re.sub(u',',u'',m.group('POT')))
541 def readShownCards(self,hand):
542 for m in self.re_ShownCards.finditer(hand.handText):
543 if m.group('CARDS') is not None:
544 cards = m.group('CARDS')
545 cards = cards.split(' ') # needs to be a list, not a set--stud needs the order
546 string = m.group('STRING')
548 (shown, mucked) = (False, False)
549 if m.group('SHOWED') == "showed": shown = True
550 elif m.group('SHOWED') == "mucked": mucked = True
552 #print "DEBUG: hand.addShownCards(%s, %s, %s, %s)" %(cards, m.group('PNAME'), shown, mucked)
553 hand.addShownCards(cards=cards, player=m.group('PNAME'), shown=shown, mucked=mucked, string=string)
555 def guessMaxSeats(self, hand):
556 """Return a guess at max_seats when not specified in HH."""
557 mo = self.maxOccSeat(hand)
559 if mo == 10: return 10 #that was easy
561 if hand.gametype['base'] == 'stud':
562 if mo <= 8: return 8
563 else: return mo
565 if hand.gametype['base'] == 'draw':
566 if mo <= 6: return 6
567 else: return mo
569 if mo == 2: return 2
570 if mo <= 6: return 6
571 return 9
573 def readSummaryInfo(self, summaryInfoList):
574 self.status = True
576 #m = re.search("Tournament Summary", summaryInfoList[0])
577 #if m:
578 # # info list should be 2 lines : Tourney infos & Finsihing postions with winnings
579 # if (len(summaryInfoList) != 2 ):
580 # log.info("Too many or too few lines (%d) in file '%s' : '%s'" % (len(summaryInfoList), self.in_path, summaryInfoList) )
581 # self.status = False
582 # else:
583 # self.tourney = TourneySummary.TourneySummary(sitename = self.sitename, gametype = None, summaryText = summaryInfoList, builtFrom = "HHC")
584 # self.status = self.getPlayersPositionsAndWinnings(self.tourney)
585 # if self.status == True :
586 # self.status = self.determineTourneyType(self.tourney)
587 # #print self.tourney
588 # else:
589 # log.info("Parsing NOK : rejected")
590 #else:
591 # log.info( "This is not a summary file : '%s'" % (self.in_path) )
592 # self.status = False
594 return self.status
596 def determineTourneyType(self, tourney):
597 info = {'type':'tour'}
598 tourneyText = tourney.summaryText[0]
599 #print "Examine : '%s'" %(tourneyText)
601 m = self.re_TourneyInfo.search(tourneyText)
602 if not m:
603 log.info(_("Error:") + " determineTourneyType")
604 return False
605 mg = m.groupdict()
606 #print mg
608 # translations from captured groups to our info strings
609 limits = { 'No Limit':'nl', 'Pot Limit':'pl', 'Limit':'fl' }
610 games = { # base, category
611 "Hold'em" : ('hold','holdem'),
612 'Omaha Hi' : ('hold','omahahi'),
613 'Omaha H/L' : ('hold','omahahilo'),
614 'Razz' : ('stud','razz'),
615 'Stud Hi' : ('stud','studhi'),
616 'Stud H/L' : ('stud','studhilo')
618 currencies = { u' €':'EUR', '$':'USD', '':'T$' }
619 info['limitType'] = limits[mg['LIMIT']]
620 if mg['GAME'] is not None:
621 (info['base'], info['category']) = games[mg['GAME']]
622 if mg['CURRENCY'] is not None:
623 info['currency'] = currencies[mg['CURRENCY']]
624 if mg['TOURNO'] is None:
625 info['type'] = "ring"
626 else:
627 info['type'] = "tour"
628 # NB: SB, BB must be interpreted as blinds or bets depending on limit type.
630 # Info is now ready to be copied in the tourney object
631 tourney.gametype = info
633 # Additional info can be stored in the tourney object
634 if mg['BUYIN'] is not None:
635 tourney.buyin = 100*Decimal(self.clearMoneyString(mg['BUYIN']))
636 tourney.fee = 0
637 if mg['FEE'] is not None:
638 tourney.fee = 100*Decimal(self.clearMoneyString(mg['FEE']))
639 if mg['TOURNAMENT_NAME'] is not None:
640 # Tournament Name can have a trailing space at the end (depending on the tournament description)
641 tourney.tourneyName = mg['TOURNAMENT_NAME'].rstrip()
642 if mg['SPECIAL'] is not None:
643 special = mg['SPECIAL']
644 if special == "KO":
645 tourney.isKO = True
646 if special == "Heads Up":
647 tourney.maxseats = 2
648 if re.search("Matrix", special):
649 tourney.isMatrix = True
650 if special == "Rebuy":
651 tourney.isRebuy = True
652 if special == "Madness":
653 tourney.tourneyComment = "Madness"
654 if mg['SHOOTOUT'] is not None:
655 tourney.isShootout = True
656 if mg['TURBO1'] is not None or mg['TURBO2'] is not None :
657 tourney.speed = "Turbo"
658 if mg['TOURNO'] is not None:
659 tourney.tourNo = mg['TOURNO']
660 else:
661 log.info(_("Unable to get a valid Tournament ID -- File rejected"))
662 return False
663 if tourney.isMatrix:
664 if mg['MATCHNO'] is not None:
665 tourney.matrixMatchId = mg['MATCHNO']
666 else:
667 tourney.matrixMatchId = 0
670 # Get BuyIn/Fee
671 # Try and deal with the different cases that can occur :
672 # - No buy-in/fee can be on the first line (freerolls, Satellites sometimes ?, ...) but appears in the rest of the description ==> use this one
673 # - Buy-In/Fee from the first line differs from the rest of the description :
674 # * OK in matrix tourneys (global buy-in dispatched between the different matches)
675 # * NOK otherwise ==> issue a warning and store specific data as if were a Matrix Tourney
676 # - If no buy-in/fee can be found : assume it's a freeroll
677 m = self.re_TourneyBuyInFee.search(tourneyText)
678 if m is not None:
679 mg = m.groupdict()
680 if tourney.isMatrix :
681 if mg['BUYIN'] is not None:
682 tourney.subTourneyBuyin = 100*Decimal(self.clearMoneyString(mg['BUYIN']))
683 tourney.subTourneyFee = 0
684 if mg['FEE'] is not None:
685 tourney.subTourneyFee = 100*Decimal(self.clearMoneyString(mg['FEE']))
686 else :
687 if mg['BUYIN'] is not None:
688 if tourney.buyin is None:
689 tourney.buyin = 100*Decimal(clearMoneyString(mg['BUYIN']))
690 else :
691 if 100*Decimal(clearMoneyString(mg['BUYIN'])) != tourney.buyin:
692 log.error(_("Conflict between buyins read in top line (%s) and in BuyIn field (%s)") % (tourney.buyin, 100*Decimal(re.sub(u',', u'', "%s" % mg['BUYIN']))) )
693 tourney.subTourneyBuyin = 100*Decimal(clearMoneyString(mg['BUYIN']))
694 if mg['FEE'] is not None:
695 if tourney.fee is None:
696 tourney.fee = 100*Decimal(clearMoneyString(mg['FEE']))
697 else :
698 if 100*Decimal(clearMoneyString(mg['FEE'])) != tourney.fee:
699 log.error(_("Conflict between fees read in top line (%s) and in Fee field (%s)") % (tourney.fee, 100*Decimal(clearMoneyString(mg['FEE']))) )
700 tourney.subTourneyFee = 100*Decimal(clearMoneyString(mg['FEE']))
702 if tourney.buyin is None:
703 log.info(_("Unable to detect a buyin to this tournament : assume it's a freeroll"))
704 tourney.buyin = 0
705 tourney.fee = 0
706 else:
707 if tourney.fee is None:
708 #print "Couldn't initialize fee, even though buyin went OK : assume there are no fees"
709 tourney.fee = 0
711 #Get single line infos
712 dictRegex = { "BUYINCHIPS" : self.re_TourneyBuyInChips,
713 "ENTRIES" : self.re_TourneyEntries,
714 "PRIZEPOOL" : self.re_TourneyPrizePool,
715 "REBUY_COST" : self.re_TourneyRebuyCost,
716 "ADDON_COST" : self.re_TourneyAddOnCost,
717 "REBUY_TOTAL" : self.re_TourneyRebuysTotal,
718 "ADDONS_TOTAL" : self.re_TourneyAddOnsTotal,
719 "REBUY_CHIPS" : self.re_TourneyRebuyChips,
720 "ADDON_CHIPS" : self.re_TourneyAddOnChips,
721 "STARTTIME" : self.re_TourneyTimeInfo,
722 "KO_BOUNTY_AMOUNT" : self.re_TourneyKOBounty,
726 dictHolders = { "BUYINCHIPS" : "buyInChips",
727 "ENTRIES" : "entries",
728 "PRIZEPOOL" : "prizepool",
729 "REBUY_COST" : "rebuyCost",
730 "ADDON_COST" : "addOnCost",
731 "REBUY_TOTAL" : "totalRebuyCount",
732 "ADDONS_TOTAL" : "totalAddOnCount",
733 "REBUY_CHIPS" : "rebuyChips",
734 "ADDON_CHIPS" : "addOnChips",
735 "STARTTIME" : "starttime",
736 "KO_BOUNTY_AMOUNT" : "koBounty"
739 mg = {} # After the loop, mg will contain all the matching groups, including the ones that have not been used, like ENDTIME and IN-PROGRESS
740 for data in dictRegex:
741 m = dictRegex.get(data).search(tourneyText)
742 if m is not None:
743 mg.update(m.groupdict())
744 setattr(tourney, dictHolders[data], mg[data])
746 if mg['IN_PROGRESS'] is not None or mg['ENDTIME'] is not None:
747 # Assign endtime to tourney (if None, that's ok, it's because the tourney wans't over over when the summary file was produced)
748 tourney.endtime = mg['ENDTIME']
750 # Deal with hero specific information
751 if tourney.hero is not None :
752 m = self.re_TourneyRebuyCount.search(tourneyText)
753 if m is not None:
754 mg = m.groupdict()
755 if mg['REBUY_COUNT'] is not None :
756 tourney.rebuyCounts.update( { tourney.hero : Decimal(mg['REBUY_COUNT']) } )
757 m = self.re_TourneyAddOnCount.search(tourneyText)
758 if m is not None:
759 mg = m.groupdict()
760 if mg['ADDON_COUNT'] is not None :
761 tourney.addOnCounts.update( { tourney.hero : Decimal(mg['ADDON_COUNT']) } )
762 m = self.re_TourneyKoCount.search(tourneyText)
763 if m is not None:
764 mg = m.groupdict()
765 if mg['COUNT_KO'] is not None :
766 tourney.koCounts.update( { tourney.hero : Decimal(mg['COUNT_KO']) } )
768 # Deal with money amounts
769 tourney.koBounty = 100*Decimal(clearMoneyString(tourney.koBounty))
770 tourney.prizepool = 100*Decimal(clearMoneyString(tourney.prizepool))
771 tourney.rebuyCost = 100*Decimal(clearMoneyString(tourney.rebuyCost))
772 tourney.addOnCost = 100*Decimal(clearMoneyString(tourney.addOnCost))
774 # Calculate payin amounts and update winnings -- not possible to take into account nb of rebuys, addons or Knockouts for other players than hero on FTP
775 for p in tourney.players :
776 if tourney.isKO :
777 #tourney.incrementPlayerWinnings(tourney.players[p], Decimal(tourney.koBounty)*Decimal(tourney.koCounts[p]))
778 tourney.winnings[p] += Decimal(tourney.koBounty)*Decimal(tourney.koCounts[p])
779 #print "player %s : winnings %d" % (p, tourney.winnings[p])
781 #print mg
782 return True
783 #end def determineTourneyType
785 def getPlayersPositionsAndWinnings(self, tourney):
786 playersText = tourney.summaryText[1]
787 #print "Examine : '%s'" %(playersText)
788 m = self.re_TourneysPlayersSummary.finditer(playersText)
790 for a in m:
791 if a.group('PNAME') is not None and a.group('RANK') is not None:
792 if a.group('RANK') == "Still Playing":
793 rank = -1
794 else:
795 rank = Decimal(a.group('RANK'))
797 if a.group('WINNING') is not None:
798 winnings = 100*Decimal(clearMoneyString(a.group('WINNING')))
799 else:
800 winnings = "0"
802 tourney.addPlayer(rank, a.group('PNAME'), winnings, "USD", 0, 0, 0) #TODO: make it store actual winnings currency
803 else:
804 print (_("Player finishing stats unreadable : %s") % a)
806 # Find Hero
807 n = self.re_TourneyHeroFinishingP.search(playersText)
808 if n is not None:
809 heroName = n.group('HERO_NAME')
810 tourney.hero = heroName
811 # Is this really useful ?
812 if heroName not in tourney.ranks:
813 print (_("%s not found in %s...") % ("tourney.ranks", heroName))
814 elif (tourney.ranks[heroName] != Decimal(n.group('HERO_FINISHING_POS'))):
815 print (_("Error:")+ _("Parsed finish position incoherent : %s / %s") % (tourney.ranks[heroName], n.group('HERO_FINISHING_POS')))
817 return True