Expanded my entry in contributors.txt.
[fpdb-dooglus.git] / pyfpdb / CarbonToFpdb.py
blobbd1a8621cfaa1c38b6925856f3dcf58c26f36ae1
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
4 # Copyright 2010-2011, Matthew Boss
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
20 ########################################################################
22 import L10n
23 _ = L10n.get_translation()
25 # This code is based heavily on EverleafToFpdb.py, by Carl Gherardi
27 # TODO:
29 # -- No support for tournaments (see also the last item below)
30 # -- Assumes that the currency of ring games is USD
31 # -- Only accepts 'realmoney="true"'
32 # -- A hand's time-stamp does not record seconds past the minute (a
33 # limitation of the history format)
34 # -- hand.maxseats can only be guessed at
35 # -- The last hand in a history file will often be incomplete and is therefore
36 # rejected
37 # -- Is behaviour currently correct when someone shows an uncalled hand?
38 # -- Information may be lost when the hand ID is converted from the native form
39 # xxxxxxxx-yyy(y*) to xxxxxxxxyyy(y*) (in principle this should be stored as
40 # a string, but the database does not support this). Is there a possibility
41 # of collision between hand IDs that ought to be distinct?
42 # -- Cannot parse tables that run it twice
43 # -- Cannot parse hands in which someone is all in in one of the blinds. Until
44 # this is corrected tournaments will be unparseable
46 import sys
47 import logging
48 from HandHistoryConverter import *
49 from decimal_wrapper import Decimal
52 class Carbon(HandHistoryConverter):
53 sitename = "Carbon"
54 filetype = "text"
55 codepage = "cp1252"
56 siteId = 11
57 copyGameHeader = True
59 limits = { 'No Limit':'nl', 'No Limit ':'nl', 'Limit':'fl', 'Pot Limit':'pl', 'Pot Limit ':'pl', 'Half Pot Limit':'hp'}
60 games = { # base, category
61 'Holdem' : ('hold','holdem'),
62 'Holdem Tournament' : ('hold','holdem'),
63 'Omaha' : ('hold','omahahi'),
64 'Omaha Tournament' : ('hold','omahahi'),
65 '2-7 Lowball' : ('draw','27_3draw'),
66 'Badugi' : ('draw','badugi'),
67 '7-Stud' : ('stud','studhi'),
68 '5-Stud' : ('stud','5studhi'),
69 'Razz' : ('stud','razz'),
72 # Static regexes
73 re_SplitHands = re.compile(r'</game>\n+(?=<game)')
74 re_TailSplitHands = re.compile(r'(</game>)')
75 re_GameInfo = re.compile(r'<description type="(?P<GAME>[-0-9a-zA-Z ]+)" stakes="(?P<LIMIT>[a-zA-Z ]+)\s\(?\$?(?P<SB>[.0-9]+)?/?\$?(?P<BB>[.0-9]+)?(?P<blah>.*)\)?"/>', re.MULTILINE)
76 re_HandInfo = re.compile(r'<game id="(?P<HID1>[0-9]+)-(?P<HID2>[0-9]+)" starttime="(?P<DATETIME>[0-9]+)" numholecards="[0-9]+" gametype="[0-9]+" realmoney="(?P<REALMONEY>(true|false))" data="[0-9]+\|(?P<TABLE>[-\ \#a-zA-Z\d\']+)(\(\d+\))?\|(?P<TOURNO>\d+)?.*>', re.MULTILINE)
77 re_Button = re.compile(r'<players dealer="(?P<BUTTON>[0-9]+)">')
78 re_PlayerInfo = re.compile(r'<player seat="(?P<SEAT>[0-9]+)" nickname="(?P<PNAME>.+)" balance="\$(?P<CASH>[.0-9]+)" dealtin="(?P<DEALTIN>(true|false))" />', re.MULTILINE)
79 re_Board = re.compile(r'<cards type="COMMUNITY" cards="(?P<CARDS>[^"]+)"', re.MULTILINE)
80 re_EndOfHand = re.compile(r'<round id="END_OF_GAME"', re.MULTILINE)
82 # The following are also static regexes: there is no need to call
83 # compilePlayerRegexes (which does nothing), since players are identified
84 # not by name but by seat number
85 re_PostSB = re.compile(r'<event sequence="[0-9]+" type="(SMALL_BLIND|RETURN_BLIND)" (?P<TIMESTAMP>timestamp="[0-9]+" )?player="(?P<PSEAT>[0-9])" amount="(?P<SB>[.0-9]+)"/>', re.MULTILINE)
86 re_PostBB = re.compile(r'<event sequence="[0-9]+" type="(BIG_BLIND|INITIAL_BLIND)" (?P<TIMESTAMP>timestamp="[0-9]+" )?player="(?P<PSEAT>[0-9])" amount="(?P<BB>[.0-9]+)"/>', re.MULTILINE)
87 re_PostBoth = re.compile(r'<event sequence="[0-9]+" type="(RETURN_BLIND)" player="(?P<PSEAT>[0-9])" amount="(?P<SBBB>[.0-9]+)"/>', re.MULTILINE)
88 re_Antes = re.compile(r'<event sequence="[0-9]+" type="ANTE" (?P<TIMESTAMP>timestamp="\d+" )?player="(?P<PSEAT>[0-9])" amount="(?P<ANTE>[.0-9]+)"/>', re.MULTILINE)
89 re_BringIn = re.compile(r'<event sequence="[0-9]+" type="BRING_IN" (?P<TIMESTAMP>timestamp="\d+" )?player="(?P<PSEAT>[0-9])" amount="(?P<BRINGIN>[.0-9]+)"/>', re.MULTILINE)
90 re_HeroCards = re.compile(r'<cards type="HOLE" cards="(?P<CARDS>.+)" player="(?P<PSEAT>[0-9])"', re.MULTILINE)
91 re_Action = re.compile(r'<event sequence="[0-9]+" type="(?P<ATYPE>FOLD|CHECK|CALL|BET|RAISE|ALL_IN|SIT_OUT)" (?P<TIMESTAMP>timestamp="[0-9]+" )?player="(?P<PSEAT>[0-9])"( amount="(?P<BET>[.0-9]+)")?/>', re.MULTILINE)
92 re_ShowdownAction = re.compile(r'<cards type="SHOWN" cards="(?P<CARDS>..,..)" player="(?P<PSEAT>[0-9])"/>', re.MULTILINE)
93 re_CollectPot = re.compile(r'<winner amount="(?P<POT>[.0-9]+)" uncalled="(true|false)" potnumber="[0-9]+" player="(?P<PSEAT>[0-9])"', re.MULTILINE)
94 re_SitsOut = re.compile(r'<event sequence="[0-9]+" type="SIT_OUT" player="(?P<PSEAT>[0-9])"/>', re.MULTILINE)
95 re_ShownCards = re.compile(r'<cards type="(SHOWN|MUCKED)" cards="(?P<CARDS>.+)" player="(?P<PSEAT>[0-9])"/>', re.MULTILINE)
97 def compilePlayerRegexs(self, hand):
98 pass
100 def playerNameFromSeatNo(self, seatNo, hand):
101 # This special function is required because Carbon Poker records
102 # actions by seat number, not by the player's name
103 for p in hand.players:
104 if p[0] == int(seatNo):
105 return p[1]
107 def readSupportedGames(self):
108 return [["ring", "hold", "nl"],
109 ["ring", "hold", "pl"],
110 ["ring", "hold", "fl"],
112 ["ring", "stud", "fl"],
113 ["ring", "stud", "pl"],
115 ["ring", "draw", "fl"],
116 ["ring", "draw", "pl"],
117 ["ring", "draw", "nl"],
118 ["ring", "draw", "hp"],
120 ["tour", "hold", "nl"],
121 ["tour", "hold", "pl"],
122 ["tour", "hold", "fl"]]
124 def determineGameType(self, handText):
125 """return dict with keys/values:
126 'type' in ('ring', 'tour')
127 'limitType' in ('nl', 'cn', 'pl', 'cp', 'fl')
128 'base' in ('hold', 'stud', 'draw')
129 'category' in ('holdem', 'omahahi', omahahilo', 'razz', 'studhi', 'studhilo', 'fivedraw', '27_1draw', '27_3draw', 'badugi')
130 'hilo' in ('h','l','s')
131 'smallBlind' int?
132 'bigBlind' int?
133 'smallBet'
134 'bigBet'
135 'currency' in ('USD', 'EUR', 'T$', <countrycode>)
136 or None if we fail to get the info """
138 m = self.re_GameInfo.search(handText)
139 if not m:
140 # Information about the game type appears only at the beginning of
141 # a hand history file; hence it is not supplied with the second
142 # and subsequent hands. In these cases we use the value previously
143 # stored.
144 try:
145 self.info
146 return self.info
147 except AttributeError:
148 tmp = handText[0:100]
149 log.error(_("Unable to recognise gametype from: '%s'") % tmp)
150 log.error("determineGameType: " + _("Raising FpdbParseError"))
151 raise FpdbParseError(_("Unable to recognise gametype from: '%s'") % tmp)
153 self.info = {}
154 mg = m.groupdict()
155 print "DEBUG: mg: %s" % mg
157 if 'LIMIT' in mg:
158 self.info['limitType'] = self.limits[mg['LIMIT']]
159 if 'GAME' in mg:
160 (self.info['base'], self.info['category']) = self.games[mg['GAME']]
161 if 'SB' in mg:
162 self.info['sb'] = mg['SB']
163 if 'BB' in mg:
164 self.info['bb'] = mg['BB']
165 if 'Tournament' in mg['GAME']:
166 self.info['type'] = 'tour'
167 self.info['currency'] = 'T$'
168 else:
169 self.info['type'] = 'ring'
170 self.info['currency'] = 'USD'
172 return self.info
174 def readHandInfo(self, hand):
175 m = self.re_HandInfo.search(hand.handText)
176 if m is None:
177 logging.info(_("No match in readHandInfo: '%s'") % hand.handText[0:100])
178 logging.info(hand.handText)
179 raise FpdbParseError(_("No match in readHandInfo: '%s'") % hand.handText[0:100])
180 logging.debug("HID %s-%s, Table %s" % (m.group('HID1'),
181 m.group('HID2'), m.group('TABLE')[:-1]))
182 hand.handid = m.group('HID1') + m.group('HID2')
183 if hand.gametype['type'] == 'tour':
184 hand.tablename = m.group('TABLE')
185 hand.tourNo = m.group('TOURNO')
186 else:
187 hand.tablename = m.group('TABLE')[:-1]
188 hand.maxseats = 2 # This value may be increased as necessary
189 hand.startTime = datetime.datetime.strptime(m.group('DATETIME')[:12],'%Y%m%d%H%M')
190 # Check that the hand is complete up to the awarding of the pot; if
191 # not, the hand is unparseable
192 if self.re_EndOfHand.search(hand.handText) is None:
193 raise FpdbParseError("readHandInfo failed: EndOfHand missing HID: '%s-%s'" %(m.group('HID1'), m.group('HID2')))
195 def readPlayerStacks(self, hand):
196 m = self.re_PlayerInfo.finditer(hand.handText)
197 for a in m:
198 seatno = int(a.group('SEAT'))
199 # It may be necessary to adjust 'hand.maxseats', which is an
200 # educated guess, starting with 2 (indicating a heads-up table) and
201 # adjusted upwards in steps to 6, then 9, then 10. An adjustment is
202 # made whenever a player is discovered whose seat number is
203 # currently above the maximum allowable for the table.
204 if seatno >= hand.maxseats:
205 if seatno > 8:
206 hand.maxseats = 10
207 elif seatno > 5:
208 hand.maxseats = 9
209 else:
210 hand.maxseats = 6
211 if a.group('DEALTIN') == "true":
212 hand.addPlayer(seatno, a.group('PNAME'), a.group('CASH'))
214 def markStreets(self, hand):
215 if hand.gametype['base'] == 'hold':
216 m = re.search(r'<round id="PREFLOP" sequence="[0-9]+">(?P<PREFLOP>.+(?=<round id="POSTFLOP")|.+)(<round id="POSTFLOP" sequence="[0-9]+">(?P<FLOP>.+(?=<round id="POSTTURN")|.+))?(<round id="POSTTURN" sequence="[0-9]+">(?P<TURN>.+(?=<round id="POSTRIVER")|.+))?(<round id="POSTRIVER" sequence="[0-9]+">(?P<RIVER>.+))?', hand.handText, re.DOTALL)
217 elif hand.gametype['base'] == 'draw':
218 if hand.gametype['category'] in ('27_3draw','badugi'):
219 m = re.search(r'(?P<PREDEAL>.+(?=<round id="PRE_FIRST_DRAW" sequence="[0-9]+">)|.+)'
220 r'(<round id="PRE_FIRST_DRAW" sequence="[0-9]+">(?P<DEAL>.+(?=<round id="FIRST_DRAW" sequence="[0-9]+">)|.+))?'
221 r'(<round id="FIRST_DRAW" sequence="[0-9]+">(?P<DRAWONE>.+(?=<round id="SECOND_DRAW" sequence="[0-9]+">)|.+))?'
222 r'(<round id="SECOND_DRAW" sequence="[0-9]+">(?P<DRAWTWO>.+(?=<round id="THIRD_DRAW" sequence="[0-9]+">)|.+))?'
223 r'(<round id="THIRD_DRAW" sequence="[0-9]+">(?P<DRAWTHREE>.+))?', hand.handText,re.DOTALL)
224 elif hand.gametype['base'] == 'stud':
225 m = re.search(r'(?P<ANTES>.+(?=<round id="BRING_IN" sequence="[0-9]+">)|.+)'
226 r'(<round id="BRING_IN" sequence="[0-9]+">(?P<THIRD>.+(?=<round id="FOURTH_STREET" sequence="[0-9]+">)|.+))?'
227 r'(<round id="FOURTH_STREET" sequence="[0-9]+">(?P<FOURTH>.+(?=<round id="FIFTH_STREET" sequence="[0-9]+">)|.+))?'
228 r'(<round id="FIFTH_STREET" sequence="[0-9]+">(?P<FIFTH>.+(?=<round id="SIXTH_STREET" sequence="[0-9]+">)|.+))?'
229 r'(<round id="SIXTH_STREET" sequence="[0-9]+">(?P<SIXTH>.+(?=<round id="SEVENTH_STREET" sequence="[0-9]+">)|.+))?'
230 r'(<round id="SEVENTH_STREET" sequence="[0-9]+">(?P<SEVENTH>.+))?', hand.handText,re.DOTALL)
232 hand.addStreets(m)
234 def readCommunityCards(self, hand, street):
235 m = self.re_Board.search(hand.streets[street])
236 if street == 'FLOP':
237 hand.setCommunityCards(street, m.group('CARDS').split(','))
238 elif street in ('TURN','RIVER'):
239 hand.setCommunityCards(street, [m.group('CARDS').split(',')[-1]])
241 def readAntes(self, hand):
242 m = self.re_Antes.finditer(hand.handText)
243 for player in m:
244 pname = self.playerNameFromSeatNo(player.group('PSEAT'), hand)
245 #print "DEBUG: hand.addAnte(%s,%s)" %(pname, player.group('ANTE'))
246 hand.addAnte(pname, player.group('ANTE'))
248 def readBringIn(self, hand):
249 m = self.re_BringIn.search(hand.handText)
250 if m:
251 pname = self.playerNameFromSeatNo(m.group('PSEAT'), hand)
252 #print "DEBUG: hand.addBringIn(%s,%s)" %(pname, m.group('BRINGIN'))
253 hand.addBringIn(pname, m.group('BRINGIN'))
255 def readBlinds(self, hand):
256 for a in self.re_PostSB.finditer(hand.handText):
257 #print "DEBUG: found sb: '%s' '%s'" %(self.playerNameFromSeatNo(a.group('PSEAT'), hand), a.group('SB'))
258 hand.addBlind(self.playerNameFromSeatNo(a.group('PSEAT'), hand),'small blind', a.group('SB'))
259 if not hand.gametype['sb']:
260 hand.gametype['sb'] = a.group('SB')
261 for a in self.re_PostBB.finditer(hand.handText):
262 #print "DEBUG: found bb: '%s' '%s'" %(self.playerNameFromSeatNo(a.group('PSEAT'), hand), a.group('BB'))
263 hand.addBlind(self.playerNameFromSeatNo(a.group('PSEAT'), hand), 'big blind', a.group('BB'))
264 if not hand.gametype['bb']:
265 hand.gametype['bb'] = a.group('BB')
266 for a in self.re_PostBoth.finditer(hand.handText):
267 bb = Decimal(self.info['bb'])
268 amount = Decimal(a.group('SBBB'))
269 if amount < bb:
270 hand.addBlind(self.playerNameFromSeatNo(a.group('PSEAT'),
271 hand), 'small blind', a.group('SBBB'))
272 elif amount == bb:
273 hand.addBlind(self.playerNameFromSeatNo(a.group('PSEAT'),
274 hand), 'big blind', a.group('SBBB'))
275 else:
276 hand.addBlind(self.playerNameFromSeatNo(a.group('PSEAT'),
277 hand), 'both', a.group('SBBB'))
279 def readButton(self, hand):
280 hand.buttonpos = int(self.re_Button.search(hand.handText).group('BUTTON'))
282 def readHeroCards(self, hand):
283 for street in ('PREFLOP', 'DEAL', 'THIRD'):
284 if street in hand.streets.keys():
285 m = self.re_HeroCards.search(hand.handText)
286 if m:
287 hand.hero = self.playerNameFromSeatNo(m.group('PSEAT'), hand)
288 cards = m.group('CARDS').split(',')
289 #print "DEBUG: cards: %s" % cards
290 hand.addHoleCards(street, hand.hero, closed=cards, shown=False, mucked=False, dealt=True)
292 def readAction(self, hand, street):
293 logging.debug("readAction (%s)" % street)
294 m = self.re_Action.finditer(hand.streets[street])
295 for action in m:
296 logging.debug("%s %s" % (action.group('ATYPE'),
297 action.groupdict()))
298 player = self.playerNameFromSeatNo(action.group('PSEAT'), hand)
299 if action.group('ATYPE') == 'RAISE':
300 hand.addCallandRaise(street, player, action.group('BET'))
301 elif action.group('ATYPE') == 'CALL':
302 hand.addCall(street, player, action.group('BET'))
303 elif action.group('ATYPE') == 'BET':
304 hand.addBet(street, player, action.group('BET'))
305 elif action.group('ATYPE') in ('FOLD', 'SIT_OUT'):
306 hand.addFold(street, player)
307 elif action.group('ATYPE') == 'CHECK':
308 hand.addCheck(street, player)
309 elif action.group('ATYPE') == 'ALL_IN':
310 hand.addAllIn(street, player, action.group('BET'))
311 else:
312 logging.debug(_("Unimplemented %s: '%s' '%s'") % ("readAction", action.group('PSEAT'), action.group('ATYPE')))
314 def readShowdownActions(self, hand):
315 for street in ('RIVER', 'SEVENTH', 'DRAWTHREE'):
316 if street in hand.streets.keys() and hand.streets[street] != None:
317 for shows in self.re_ShowdownAction.finditer(hand.streets[street]):
318 cards = shows.group('CARDS').split(',')
319 hand.addShownCards(cards, self.playerNameFromSeatNo(shows.group('PSEAT'), hand))
321 def readCollectPot(self, hand):
322 pots = [Decimal(0) for n in range(hand.maxseats)]
323 for m in self.re_CollectPot.finditer(hand.handText):
324 pots[int(m.group('PSEAT'))] += Decimal(m.group('POT'))
325 # Regarding the processing logic for "committed", see Pot.end() in
326 # Hand.py
327 committed = sorted([(v,k) for (k,v) in hand.pot.committed.items()])
328 for p in range(hand.maxseats):
329 pname = self.playerNameFromSeatNo(p, hand)
330 if committed[-1][1] == pname:
331 pots[p] -= committed[-1][0] - committed[-2][0]
332 if pots[p] > 0:
333 hand.addCollectPot(player=pname, pot=pots[p])
335 def readShownCards(self, hand):
336 for street in ('FLOP', 'TURN', 'RIVER', 'SEVENTH', 'DRAWTHREE'):
337 if street in hand.streets.keys() and hand.streets[street] != None:
338 for m in self.re_ShownCards.finditer(hand.streets[street]):
339 cards = m.group('CARDS').split(',')
340 hand.addShownCards(cards=cards, player=self.playerNameFromSeatNo(m.group('PSEAT'),hand))