Improve session viewer. There were some off-by-one errors.
[fpdb-dooglus.git] / pyfpdb / AbsoluteToFpdb.py
blob162b249d990652b2c9672d2469da8460dca4ab87
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
4 # Copyright 2008-2011, Carl Gherardi
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 #Note that this filter also supports UltimateBet, they are both owned by the same company and form the Cereus Network
23 import L10n
24 _ = L10n.get_translation()
26 # TODO: I have no idea if AP has multi-currency options, i just copied the regex out of Everleaf converter for the currency symbols.. weeeeee - Eric
27 import sys
28 import logging
29 from HandHistoryConverter import *
31 # Class for converting Absolute HH format.
33 class Absolute(HandHistoryConverter):
35 # Class Variables
36 sitename = "Absolute"
37 filetype = "text"
38 codepage = "cp1252"
39 siteid = 8
40 HORSEHand = False
42 # Static regexes
43 re_SplitHands = re.compile(r"\n\n+")
44 re_TailSplitHands = re.compile(r"(\nn\n+)")
45 #Stage #1571362962: Holdem No Limit $0.02 - 2009-08-05 15:24:06 (ET)
46 #Table: TORONTO AVE (Real Money) Seat #6 is the dealer
47 #Seat 6 - FETS63 ($0.75 in chips)
48 #Board [10s 5d Kh Qh 8c]
50 re_GameInfo = re.compile( ur"""
51 ^Stage\s+\#C?(?P<HID>[0-9]+):?\s+
52 (?:Tourney\ ID\ (?P<TRNY_ID>\d+)\s+)?
53 (?P<GAME>Holdem|Seven\ Card\ Hi\/L|HORSE)\s+
54 (?P<TRNY_TYPE>\(1\son\s1\)|Single\ Tournament|Multi\ Normal\ Tournament|)\s*
55 (?P<LIMIT>No\ Limit|Pot\ Limit|Normal|)\s?
56 (?P<CURRENCY>\$|\s€|)
57 (?P<SB>[.,0-9]+)/?(?:\$|\s€|)(?P<BB>[.,0-9]+)?
58 \s+
59 ((?P<TTYPE>(Turbo))\s+)?-\s+
60 ((?P<DATETIME>\d\d\d\d-\d\d-\d\d\ \d\d:\d\d:\d\d)(\.\d+)?)\s+
61 (?: \( (?P<TZ>[A-Z]+) \)\s+ )?
62 .*?
63 (Table:\ (?P<TABLE>.*?)\ \(Real\ Money\))?
64 """, re.MULTILINE|re.VERBOSE|re.DOTALL)
66 re_HorseGameInfo = re.compile(
67 ur"^Game Type: (?P<LIMIT>Limit) (?P<GAME>Holdem)",
68 re.MULTILINE)
70 re_HandInfo = re_GameInfo
72 # on HORSE STUD games, the table name isn't in the hand info!
73 re_RingInfoFromFilename = re.compile(ur".*IHH([0-9]+) (?P<TABLE>.*) -")
74 re_TrnyInfoFromFilename = re.compile(
75 ur"IHH\s?([0-9]+) (?P<TRNY_NAME>.*) "\
76 ur"ID (?P<TRNY_ID>\d+)\s?(\((?P<TABLE>\d+)\))? .* "\
77 ur"(?:\$|\s€|)(?P<BUYIN>[0-9.]+)\s*\+\s*(?:\$|\s€|)(?P<FEE>[0-9.]+)"
80 # TODO: that's not the right way to match for "dead" dealer is it?
81 re_Button = re.compile(ur"Seat #(?P<BUTTON>[0-9]) is the ?[dead]* dealer$", re.MULTILINE)
83 re_PlayerInfo = re.compile(
84 ur"^Seat (?P<SEAT>[0-9]) - (?P<PNAME>.*) "\
85 ur"\((?:\$| €|)(?P<CASH>[0-9]*[.,0-9]+) in chips\)",
86 re.MULTILINE)
88 re_Board = re.compile(ur"\[(?P<CARDS>[^\]]*)\]? *$", re.MULTILINE)
91 def compilePlayerRegexs(self, hand):
92 players = set([player[1] for player in hand.players])
93 if not players <= self.compiledPlayers: # x <= y means 'x is subset of y'
94 # we need to recompile the player regexs.
95 self.compiledPlayers = players
96 player_re = "(?P<PNAME>" + "|".join(map(re.escape, players)) + ")"
97 logging.debug("player_re: "+ player_re)
98 #(?P<CURRENCY>\$| €|)(?P<BB>[0-9]*[.0-9]+)
99 self.re_PostSB = re.compile(ur"^%s - Posts small blind (?:\$| €|)(?P<SB>[,.0-9]+)" % player_re, re.MULTILINE)
100 self.re_PostBB = re.compile(ur"^%s - Posts big blind (?:\$| €|)(?P<BB>[.,0-9]+)" % player_re, re.MULTILINE)
101 # TODO: Absolute posting when coming in new: %s - Posts $0.02 .. should that be a new Post line? where do we need to add support for that? *confused*
102 self.re_PostBoth = re.compile(ur"^%s - Posts dead (?:\$| €|)(?P<SBBB>[,.0-9]+)" % player_re, re.MULTILINE)
103 self.re_Action = re.compile(ur"^%s - (?P<ATYPE>Bets |Raises |All-In |All-In\(Raise\) |Calls |Folds|Checks)?\$?(?P<BET>[,.0-9]+)?" % player_re, re.MULTILINE)
104 self.re_ShowdownAction = re.compile(ur"^%s - Shows \[(?P<CARDS>.*)\]" % player_re, re.MULTILINE)
105 self.re_CollectPot = re.compile(ur"^Seat [0-9]: %s(?: \(dealer\)|)(?: \(big blind\)| \(small blind\)|) (?:won|collected) Total \((?:\$| €|)(?P<POT>[,.0-9]+)\)" % player_re, re.MULTILINE)
106 self.re_Antes = re.compile(ur"^%s - Ante \[(?:\$| €|)(?P<ANTE>[,.0-9]+)" % player_re, re.MULTILINE)
107 #self.re_BringIn = re.compile(ur"^%s posts bring-in (?:\$| €|)(?P<BRINGIN>[.0-9]+)\." % player_re, re.MULTILINE)
108 self.re_HeroCards = re.compile(ur"^Dealt to %s \[(?P<CARDS>.*)\]" % player_re, re.MULTILINE)
110 def readSupportedGames(self):
111 return [["ring", "hold", "nl"],
112 ["ring", "hold", "pl"],
113 ["ring", "hold", "fl"],
114 ["ring", "studhi", "fl"],
115 ["ring", "omahahi", "pl"],
116 ["tour", "hold", "nl"],
119 def determineGameType(self, handText):
120 """return dict with keys/values:
121 'type' in ('ring', 'tour')
122 'limitType' in ('nl', 'cn', 'pl', 'cp', 'fl')
123 'base' in ('hold', 'stud', 'draw')
124 'category' in ('holdem', 'omahahi', omahahilo', 'razz',
125 'studhi', 'studhilo', 'fivedraw', '27_1draw',
126 '27_3draw', 'badugi')
127 'hilo' in ('h','l','s')
128 'smallBlind' int?
129 'bigBlind' int?
130 'smallBet'
131 'bigBet'
132 'currency' in ('USD', 'EUR', 'T$', <countrycode>)
134 or None if we fail to get the info """
135 info = {'type':'ring'}
137 m = self.re_GameInfo.search(handText)
138 if not m:
139 tmp = handText[0:100]
140 log.error(_("Unable to recognise gametype from: '%s'") % tmp)
141 log.error("determineGameType: " + _("Raising FpdbParseError"))
142 raise FpdbParseError(_("Unable to recognise gametype from: '%s'") % tmp)
145 mg = m.groupdict()
146 #print "DEBUG: mg: %s" % mg
148 # translations from captured groups to our info strings
149 limits = { 'No Limit':'nl', 'Pot Limit':'pl', 'Normal':'fl', 'Limit':'fl'}
150 games = { # base, category
151 "Holdem" : ('hold','holdem'),
152 'Omaha' : ('hold','omahahi'),
153 'Razz' : ('stud','razz'),
154 'Seven Card Hi/L' : ('stud','studhilo'),
155 '7 Card Stud' : ('stud','studhi')
157 currencies = { u' €':'EUR', '$':'USD', '':'T$' }
158 if 'GAME' in mg and mg['GAME'] == "HORSE": # if we're a HORSE game, the game type is on the next line
159 self.HORSEHand = True
160 m = self.re_HorseGameInfo.search(handText)
161 if not m:
162 return None # it's a HORSE game and we don't understand the game type
163 temp = m.groupdict()
164 #print "AP HORSE processing"
165 if 'GAME' not in temp or 'LIMIT' not in temp:
166 return None # sort of understood it but not really
167 #print "temp=", temp
168 mg['GAME'] = temp['GAME']
169 mg['LIMIT'] = temp['LIMIT']
170 if 'GAME' in mg:
171 (info['base'], info['category']) = games[mg['GAME']]
172 if 'LIMIT' in mg:
173 info['limitType'] = limits[mg['LIMIT']]
174 if 'CURRENCY' in mg:
175 info['currency'] = currencies[mg['CURRENCY']]
176 if info['currency'] == 'T$':
177 info['type'] = 'tour'
178 if 'SB' in mg:
179 mg['SB'] = mg['SB'].replace(',', '')
180 info['sb'] = mg['SB']
181 if 'BB' in mg:
182 info['bb'] = mg['BB']
183 # NB: SB, BB must be interpreted as blinds or bets depending on limit type.
184 if info['bb'] is None:
185 mg['SB'] = mg['SB'].replace(',', '')
186 info['bb'] = mg['SB']
187 info['sb'] = str(float(mg['SB']) * 0.5) # TODO: AP does provide Small BET for Limit .. I think? at least 1-on-1 limit they do.. sigh
189 return info
192 def readHandInfo(self, hand):
193 is_trny = hand.gametype['type']=='tour'
195 m = self.re_HandInfo.search(hand.handText)
196 fname_re = self.re_TrnyInfoFromFilename if is_trny \
197 else self.re_RingInfoFromFilename
198 fname_info = fname_re.search(self.in_path)
200 #print "DEBUG: fname_info.groupdict(): %s" %(fname_info.groupdict())
202 if m is None or fname_info is None:
203 if m is None:
204 tmp = hand.handText[0:100]
205 logging.error(_("No match in readHandInfo: '%s'") % tmp)
206 raise FpdbParseError("Absolute: " + _("No match in readHandInfo: '%s'") % tmp)
207 elif fname_info is None:
208 logging.error(_("File name didn't match re_*InfoFromFilename"))
209 logging.error(_("File name: %s") % self.in_path)
210 raise FpdbParseError("Absolute: " + _("Didn't match re_*InfoFromFilename: '%s'") % self.in_path)
212 logging.debug("HID %s, Table %s" % (m.group('HID'), m.group('TABLE')))
213 hand.handid = m.group('HID')
214 if m.group('TABLE'):
215 hand.tablename = m.group('TABLE')
216 else:
217 hand.tablename = fname_info.group('TABLE')
219 hand.startTime = datetime.datetime.strptime(m.group('DATETIME'), "%Y-%m-%d %H:%M:%S")
221 if is_trny:
222 hand.fee = fname_info.group('FEE')
223 hand.buyin = fname_info.group('BUYIN')
224 hand.tourNo = m.group('TRNY_ID')
225 hand.tourneyComment = fname_info.group('TRNY_NAME')
227 # assume 6-max unless we have proof it's a larger/smaller game,
228 #since absolute doesn't give seat max info
229 # TODO: (1-on-1) does have that info in the game type line
230 hand.maxseats = 6
232 if self.HORSEHand:
233 hand.maxseats = 8 # todo : unless it's heads up!!?
234 return
236 def readPlayerStacks(self, hand):
237 m = self.re_PlayerInfo.finditer(hand.handText)
238 for a in m:
239 seatnum = int(a.group('SEAT'))
240 hand.addPlayer(seatnum, a.group('PNAME'), a.group('CASH'))
241 if seatnum > 6:
242 hand.maxseats = 9 # absolute does 2/4/6/9 games
243 # TODO: implement lookup list by table-name to determine maxes,
244 # then fall back to 6 default/10 here, if there's no entry in the list?
247 def markStreets(self, hand):
248 # PREFLOP = ** Dealing down cards **
249 # This re fails if, say, river is missing; then we don't get the ** that starts the river.
250 #m = re.search('(\*\* Dealing down cards \*\*\n)(?P<PREFLOP>.*?\n\*\*)?( Dealing Flop \*\* \[ (?P<FLOP1>\S\S), (?P<FLOP2>\S\S), (?P<FLOP3>\S\S) \])?(?P<FLOP>.*?\*\*)?( Dealing Turn \*\* \[ (?P<TURN1>\S\S) \])?(?P<TURN>.*?\*\*)?( Dealing River \*\* \[ (?P<RIVER1>\S\S) \])?(?P<RIVER>.*)', hand.handText,re.DOTALL)
251 if hand.gametype['base'] == 'hold':
252 m = re.search(r"\*\*\* POCKET CARDS \*\*\*(?P<PREFLOP>.+(?=\*\*\* FLOP \*\*\*)|.+)"
253 r"(\*\*\* FLOP \*\*\*(?P<FLOP>.+(?=\*\*\* TURN \*\*\*)|.+))?"
254 r"(\*\*\* TURN \*\*\*(?P<TURN>.+(?=\*\*\* RIVER \*\*\*)|.+))?"
255 r"(\*\*\* RIVER \*\*\*(?P<RIVER>.+))?", hand.handText, re.DOTALL)
257 elif hand.gametype['base'] == 'stud': # TODO: Not implemented yet
258 m = re.search(r"(?P<ANTES>.+(?=\*\* Dealing down cards \*\*)|.+)"
259 r"(\*\* Dealing down cards \*\*(?P<THIRD>.+(?=\*\*\*\* dealing 4th street \*\*\*\*)|.+))?"
260 r"(\*\*\*\* dealing 4th street \*\*\*\*(?P<FOURTH>.+(?=\*\*\*\* dealing 5th street \*\*\*\*)|.+))?"
261 r"(\*\*\*\* dealing 5th street \*\*\*\*(?P<FIFTH>.+(?=\*\*\*\* dealing 6th street \*\*\*\*)|.+))?"
262 r"(\*\*\*\* dealing 6th street \*\*\*\*(?P<SIXTH>.+(?=\*\*\*\* dealing river \*\*\*\*)|.+))?"
263 r"(\*\*\*\* dealing river \*\*\*\*(?P<SEVENTH>.+))?", hand.handText,re.DOTALL)
264 hand.addStreets(m)
266 def readCommunityCards(self, hand, street):
267 # street has been matched by markStreets, so exists in this hand
268 # If this has been called, street is a street which gets dealt
269 # community cards by type hand but it might be worth checking somehow.
270 # if street in ('FLOP','TURN','RIVER'):
271 # a list of streets which get dealt community cards (i.e. all but PREFLOP)
272 logging.debug("readCommunityCards (%s)" % street)
273 m = self.re_Board.search(hand.streets[street])
274 cards = m.group('CARDS')
275 cards = [validCard(card) for card in cards.split(' ')]
276 hand.setCommunityCards(street=street, cards=cards)
278 def readAntes(self, hand):
279 logging.debug(_("reading antes"))
280 m = self.re_Antes.finditer(hand.handText)
281 for player in m:
282 logging.debug("hand.addAnte(%s,%s)" %(player.group('PNAME'), player.group('ANTE')))
283 hand.addAnte(player.group('PNAME'), player.group('ANTE'))
285 def readBringIn(self, hand):
286 m = self.re_BringIn.search(hand.handText,re.DOTALL)
287 if m:
288 logging.debug(_("Player bringing in: %s for %s") % (m.group('PNAME'), m.group('BRINGIN')))
289 hand.addBringIn(m.group('PNAME'), m.group('BRINGIN'))
290 else:
291 logging.warning(_("No bringin found."))
293 def readBlinds(self, hand):
294 m = self.re_PostSB.search(hand.handText)
295 if m is not None:
296 hand.addBlind(m.group('PNAME'), 'small blind', m.group('SB'))
297 else:
298 logging.debug(_("No small blind"))
299 hand.addBlind(None, None, None)
300 for a in self.re_PostBB.finditer(hand.handText):
301 hand.addBlind(a.group('PNAME'), 'big blind', a.group('BB'))
302 for a in self.re_PostBoth.finditer(hand.handText):
303 hand.addBlind(a.group('PNAME'), 'both', a.group('SBBB'))
305 def readButton(self, hand):
306 hand.buttonpos = int(self.re_Button.search(hand.handText).group('BUTTON'))
308 def readHeroCards(self, hand):
309 m = self.re_HeroCards.search(hand.handText)
310 if m:
311 hand.hero = m.group('PNAME')
312 # "2c, qh" -> ["2c","qc"]
313 # Also works with Omaha hands.
314 cards = m.group('CARDS')
315 cards = [validCard(card) for card in cards.split(' ')]
316 # hand.addHoleCards(cards, m.group('PNAME'))
317 hand.addHoleCards('PREFLOP', hand.hero, closed=cards, shown=False, mucked=False, dealt=True)
319 else:
320 #Not involved in hand
321 hand.involved = False
323 def readStudPlayerCards(self, hand, street):
324 logging.warning(_("%s cannot read all stud/razz hands yet.") % hand.sitename)
326 def readAction(self, hand, street):
327 logging.debug("readAction (%s)" % street)
328 m = self.re_Action.finditer(hand.streets[street])
329 for action in m:
330 logging.debug("%s %s" % (action.group('ATYPE'), action.groupdict()))
331 if action.group('ATYPE') == 'Raises ' or action.group('ATYPE') == 'All-In(Raise) ':
332 bet = action.group('BET').replace(',', '')
333 hand.addCallandRaise( street, action.group('PNAME'), bet)
334 elif action.group('ATYPE') == 'Calls ':
335 bet = action.group('BET').replace(',', '')
336 hand.addCall( street, action.group('PNAME'), bet)
337 elif action.group('ATYPE') == 'Bets ' or action.group('ATYPE') == 'All-In ':
338 bet = action.group('BET').replace(',', '')
339 hand.addBet( street, action.group('PNAME'), bet)
340 elif action.group('ATYPE') == 'Folds':
341 hand.addFold( street, action.group('PNAME'))
342 elif action.group('ATYPE') == 'Checks':
343 hand.addCheck( street, action.group('PNAME'))
344 elif action.group('ATYPE') == ' complete to': # TODO: not supported yet ?
345 bet = action.group('BET').replace(',', '')
346 hand.addComplete( street, action.group('PNAME'), bet)
347 else:
348 logging.debug(_("Unimplemented %s: '%s' '%s'") % ("readAction", action.group('PNAME'), action.group('ATYPE')))
351 def readShowdownActions(self, hand):
352 """Reads lines where holecards are reported in a showdown"""
353 logging.debug("readShowdownActions")
354 for shows in self.re_ShowdownAction.finditer(hand.handText):
355 cards = shows.group('CARDS')
356 cards = [validCard(card) for card in cards.split(' ')]
357 logging.debug("readShowdownActions %s %s" %(cards, shows.group('PNAME')))
358 hand.addShownCards(cards, shows.group('PNAME'))
361 def readCollectPot(self,hand):
362 for m in self.re_CollectPot.finditer(hand.handText):
363 pot = m.group('POT').replace(',','')
364 hand.addCollectPot(player=m.group('PNAME'),pot=pot)
366 def readShownCards(self,hand):
367 """Reads lines where hole & board cards are mixed to form a hand (summary lines)"""
368 for m in self.re_CollectPot.finditer(hand.handText):
369 try:
370 if m.group('CARDS') is not None:
371 cards = m.group('CARDS')
372 cards = [validCard(card) for card in cards.split(' ')]
373 player = m.group('PNAME')
374 logging.debug("readShownCards %s cards=%s" % (player, cards))
375 # hand.addShownCards(cards=None, player=m.group('PNAME'), holeandboard=cards)
376 hand.addShownCards(cards=cards, player=m.group('PNAME'))
377 except IndexError:
378 pass # there's no "PLAYER - Mucks" at AP that I can see
380 def validCard(card):
381 card = card.strip()
382 if card == '10s': card = 'Ts'
383 if card == '10h': card = 'Th'
384 if card == '10d': card = 'Td'
385 if card == '10c': card = 'Tc'
386 return card