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 ########################################################################
22 _
= L10n
.get_translation()
28 # logging has been set up in fpdb.py or HUD_main.py, use their settings:
31 from HandHistoryConverter
import *
32 from decimal_wrapper
import Decimal
37 class Winamax(HandHistoryConverter
):
39 def my_f(*args
, **kwds
):
40 print ( "entering " + f
.__name
__)
41 result
= f(*args
, **kwds
)
42 print ( "exiting " + f
.__name
__)
44 my_f
.__name
= f
.__name
__
45 my_f
.__doc
__ = f
.__doc
__
51 codepage
= ("utf8", "cp1252")
52 siteId
= 14 # Needs to match id entry in Sites database
54 mixes
= { } # Legal mixed games
55 sym
= {'USD': "\$", 'CAD': "\$", 'T$': "", "EUR": u
"\xe2\x82\xac|\u20ac", "GBP": "\xa3"} # ADD Euro, Sterling, etc HERE
57 'LEGAL_ISO' : "USD|EUR|GBP|CAD|FPP", # legal ISO currency codes
58 'LS' : u
"\$|\xe2\x82\xac|\u20ac|" # legal currency symbols - Euro(cp1252, utf-8)
61 limits
= { 'no limit':'nl', 'pot limit' : 'pl','LIMIT':'fl'}
63 games
= { # base, category
64 "Holdem" : ('hold','holdem'),
65 'Omaha' : ('hold','omahahi'),
66 # 'Omaha Hi/Lo' : ('hold','omahahilo'),
67 # 'Razz' : ('stud','razz'),
68 # 'RAZZ' : ('stud','razz'),
69 # '7 Card Stud' : ('stud','studhi'),
70 # 'SEVEN_CARD_STUD_HI_LO' : ('stud','studhilo'),
71 # 'Badugi' : ('draw','badugi'),
72 # 'Triple Draw 2-7 Lowball' : ('draw','27_3draw'),
73 # '5 Card Draw' : ('draw','fivedraw')
77 # ***** End of hand R5-75443872-57 *****
78 re_SplitHands
= re
.compile(r
'\n\n')
82 # Winamax Poker - CashGame - HandId: #279823-223-1285031451 - Holdem no limit (0.02€/0.05€) - 2010/09/21 03:10:51 UTC
83 # Table: 'Charenton-le-Pont' 9-max (real money) Seat #5 is the button
84 re_HandInfo
= re
.compile(u
"""
85 \s*Winamax\sPoker\s-\s
89 buyIn:\s(?P<BUYIN>(?P<BIAMT>[%(LS)s\d\,]+)?\s\+?\s(?P<BIRAKE>[%(LS)s\d\,]+)?\+?(?P<BOUNTY>[%(LS)s\d\.]+)?\s?(?P<TOUR_ISO>%(LEGAL_ISO)s)?|Freeroll|Gratuit|Ticket\suniquement)?\s
90 (level:\s(?P<LEVEL>\d+))?
92 \s-\sHandId:\s\#(?P<HID1>\d+)-(?P<HID2>\d+)-(?P<HID3>\d+).*\s # REB says: HID3 is the correct hand number
93 (?P<GAME>Holdem|Omaha)\s
94 (?P<LIMIT>no\slimit|pot\slimit)\s
96 (((%(LS)s)?(?P<ANTE>[.0-9]+)(%(LS)s)?)/)?
97 ((%(LS)s)?(?P<SB>[.0-9]+)(%(LS)s)?)/
98 ((%(LS)s)?(?P<BB>[.0-9]+)(%(LS)s)?)
101 Table:\s\'(?P<TABLE>[^(]+)
102 (.(?P<TOURNO>\d+).\#(?P<TABLENO>\d+))?.*
104 \s(?P<MAXPLAYER>\d+)\-max
105 """ % substitutions
, re
.MULTILINE|re
.DOTALL|re
.VERBOSE
)
107 re_TailSplitHands
= re
.compile(r
'\n\s*\n')
108 re_Button
= re
.compile(r
'Seat\s#(?P<BUTTON>\d+)\sis\sthe\sbutton')
109 re_Board
= re
.compile(r
"\[(?P<CARDS>.+)\]")
110 re_Total
= re
.compile(r
"Total pot (?P<TOTAL>[\.\d]+).*(No rake|Rake (?P<RAKE>[\.\d]+))" % substitutions
)
112 # 2010/09/21 03:10:51 UTC
113 re_DateTime
= re
.compile("""
117 (?P<H>[0-9]+):(?P<MIN>[0-9]+):(?P<S>[0-9]+)\s
119 """, re
.MULTILINE|re
.VERBOSE
)
121 # Seat 1: some_player (5€)
122 # Seat 2: some_other_player21 (6.33€)
124 re_PlayerInfo
= re
.compile(u
'Seat\s(?P<SEAT>[0-9]+):\s(?P<PNAME>.*)\s\((%(LS)s)?(?P<CASH>[.0-9]+)(%(LS)s)?\)' % substitutions
)
126 def compilePlayerRegexs(self
, hand
):
127 players
= set([player
[1] for player
in hand
.players
])
128 if not players
<= self
.compiledPlayers
: # x <= y means 'x is subset of y'
129 # we need to recompile the player regexs.
130 # TODO: should probably rename re_HeroCards and corresponding method,
131 # since they are used to find all cards on lines starting with "Dealt to:"
132 # They still identify the hero.
133 self
.compiledPlayers
= players
135 #helander2222 posts blind ($0.25), lopllopl posts blind ($0.50).
136 player_re
= "(?P<PNAME>" + "|".join(map(re
.escape
, players
)) + ")"
137 subst
= {'PLYR': player_re
, 'CUR': self
.sym
[hand
.gametype
['currency']]}
138 self
.re_PostSB
= re
.compile('%(PLYR)s posts small blind (%(CUR)s)?(?P<SB>[\.0-9]+)(%(CUR)s)?' % subst
, re
.MULTILINE
)
139 self
.re_PostBB
= re
.compile('%(PLYR)s posts big blind (%(CUR)s)?(?P<BB>[\.0-9]+)(%(CUR)s)?' % subst
, re
.MULTILINE
)
140 self
.re_DenySB
= re
.compile('(?P<PNAME>.*) deny SB' % subst
, re
.MULTILINE
)
141 self
.re_Antes
= re
.compile(r
"^%(PLYR)s posts ante (%(CUR)s)?(?P<ANTE>[\.0-9]+)(%(CUR)s)?" % subst
, re
.MULTILINE
)
142 self
.re_BringIn
= re
.compile(r
"^%(PLYR)s brings[- ]in( low|) for (%(CUR)s)?(?P<BRINGIN>[\.0-9]+(%(CUR)s)?)" % subst
, re
.MULTILINE
)
143 self
.re_PostBoth
= re
.compile('(?P<PNAME>.*): posts small \& big blind \( (%(CUR)s)?(?P<SBBB>[\.0-9]+)(%(CUR)s)?\)' % subst
)
144 self
.re_PostDead
= re
.compile('(?P<PNAME>.*) posts dead blind \((%(CUR)s)?(?P<DEAD>[\.0-9]+)(%(CUR)s)?\)' % subst
, re
.MULTILINE
)
145 self
.re_HeroCards
= re
.compile('Dealt\sto\s%(PLYR)s\s\[(?P<CARDS>.*)\]' % subst
)
147 self
.re_Action
= re
.compile('(, )?(?P<PNAME>.*?)(?P<ATYPE> bets| checks| raises| calls| folds)( (%(CUR)s)?(?P<BET>[\d\.]+)(%(CUR)s)?)?( and is all-in)?' % subst
)
148 self
.re_ShowdownAction
= re
.compile('(?P<PNAME>[^\(\)\n]*) (\((small blind|big blind|button)\) )?shows \[(?P<CARDS>.+)\]')
150 self
.re_CollectPot
= re
.compile('\s*(?P<PNAME>.*)\scollected\s(%(CUR)s)?(?P<POT>[\.\d]+)(%(CUR)s)?.*' % subst
)
151 self
.re_ShownCards
= re
.compile("^Seat (?P<SEAT>[0-9]+): %(PLYR)s showed \[(?P<CARDS>.*)\].*" % subst
, re
.MULTILINE
)
152 self
.re_sitsOut
= re
.compile('(?P<PNAME>.*) sits out')
154 def readSupportedGames(self
):
156 ["ring", "hold", "fl"],
157 ["ring", "hold", "nl"],
158 ["ring", "hold", "pl"],
159 ["tour", "hold", "fl"],
160 ["tour", "hold", "nl"],
161 ["tour", "hold", "pl"],
164 def determineGameType(self
, handText
):
165 # Inspect the handText and return the gametype dict
166 # gametype dict is: {'limitType': xxx, 'base': xxx, 'category': xxx}
169 m
= self
.re_HandInfo
.search(handText
)
171 tmp
= handText
[0:100]
172 log
.error(_("Unable to recognise gametype from: '%s'") % tmp
)
173 log
.error("determineGameType: " + _("Raising FpdbParseError"))
174 raise FpdbParseError(_("Unable to recognise gametype from: '%s'") % tmp
)
179 info
['type'] = 'tour'
181 info
['type'] = 'ring'
183 info
['currency'] = 'EUR'
186 if mg
['LIMIT'] in self
.limits
:
187 info
['limitType'] = self
.limits
[mg
['LIMIT']]
189 tmp
= handText
[0:100]
190 log
.error(_("limit not found in self.limits(%s). hand: '%s'") % (str(mg
),tmp
))
191 log
.error("determineGameType: " + _("Raising FpdbParseError"))
192 raise FpdbParseError(_("limit not found in self.limits(%s). hand: '%s'") % (str(mg
),tmp
))
194 (info
['base'], info
['category']) = self
.games
[mg
['GAME']]
196 info
['sb'] = mg
['SB']
198 info
['bb'] = mg
['BB']
202 def readHandInfo(self
, hand
):
204 m
= self
.re_HandInfo
.search(hand
.handText
)
207 info
.update(m
.groupdict())
209 #log.debug("readHandInfo: %s" % info)
211 if key
== 'DATETIME':
212 a
= self
.re_DateTime
.search(info
[key
])
214 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'))
216 datetimestr
= "2010/Jan/01 01:01:01"
217 log
.error("readHandInfo: " + _("DATETIME not matched: '%s'") % info
[key
])
218 #print "DEBUG: readHandInfo: DATETIME not matched: '%s'" % info[key]
219 hand
.startTime
= datetime
.datetime
.strptime(datetimestr
, "%Y/%m/%d %H:%M:%S")
220 hand
.startTime
= HandHistoryConverter
.changeTimezone(hand
.startTime
, "CET", "UTC")
222 # Need to remove non-alphanumerics for MySQL
223 # hand.handid = "1%.9d%s%s"%(int(info['HID2']),info['HID1'],info['HID3'])
224 hand
.handid
= "%s%s%s"%(int(info
['HID1']),info
['HID2'],info
['HID3'])
225 if len (hand
.handid
) > 19:
226 hand
.handid
= "%s%s" % (int(info
['HID2']), int(info
['HID3']))
229 # hand.handid = int(info['HID3']) # correct hand no (REB)
231 hand
.tourNo
= info
[key
]
233 hand
.tablename
= info
[key
]
234 # TODO: long-term solution for table naming on Winamax.
235 if hand
.tablename
.endswith(u
'No Limit Hold\'em'):
236 hand
.tablename
= hand
.tablename
[:-len(u
'No Limit Hold\'em')] + u
'NLHE'
237 if key
== 'MAXPLAYER' and info
[key
] != None:
238 hand
.maxseats
= int(info
[key
])
241 if hand
.tourNo
!=None:
242 #print "DEBUG: info['BUYIN']: %s" % info['BUYIN']
243 #print "DEBUG: info['BIAMT']: %s" % info['BIAMT']
244 #print "DEBUG: info['BIRAKE']: %s" % info['BIRAKE']
245 #print "DEBUG: info['BOUNTY']: %s" % info['BOUNTY']
246 for k
in ['BIAMT','BIRAKE']:
247 if k
in info
.keys() and info
[k
]:
248 info
[k
] = info
[k
].replace(',','.')
250 if info
[key
] == 'Gratuit' or info
[key
] == 'Freeroll':
253 hand
.buyinCurrency
= "FREE"
255 if info
[key
].find("$")!=-1:
256 hand
.buyinCurrency
="USD"
257 elif info
[key
].find(u
"€")!=-1:
258 hand
.buyinCurrency
="EUR"
259 elif info
[key
].find("FPP")!=-1:
260 hand
.buyinCurrency
="PSFP"
262 #FIXME: handle other currencies (are there other currencies?)
263 raise FpdbParseError(_("Failed to detect currency.") + " " + _("Hand ID: %s: '%s'") % (hand
.handid
, info
[key
]))
265 info
['BIAMT'] = info
['BIAMT'].strip(u
'$€FPP')
267 if hand
.buyinCurrency
!="PSFP":
268 if info
['BOUNTY'] != None:
269 # There is a bounty, Which means we need to switch BOUNTY and BIRAKE values
271 info
['BOUNTY'] = info
['BIRAKE']
273 info
['BOUNTY'] = info
['BOUNTY'].strip(u
'$€') # Strip here where it isn't 'None'
274 hand
.koBounty
= int(100*Decimal(info
['BOUNTY']))
279 info
['BIRAKE'] = info
['BIRAKE'].strip(u
'$€')
281 # TODO: Is this correct? Old code tried to
282 # conditionally multiply by 100, but we
283 # want hand.buyin in 100ths of
284 # dollars/euros (so hand.buyin = 90 for $0.90 BI).
285 hand
.buyin
= int(100 * Decimal(info
['BIAMT']))
286 hand
.fee
= int(100 * Decimal(info
['BIRAKE']))
288 hand
.buyin
= int(Decimal(info
['BIAMT']))
292 hand
.level
= info
[key
]
294 m
= self
.re_Button
.search(hand
.handText
)
295 hand
.buttonpos
= m
.groupdict().get('BUTTON', None)
299 def readPlayerStacks(self
, hand
):
300 log
.debug(_("readplayerstacks: re is '%s'") % self
.re_PlayerInfo
)
301 m
= self
.re_PlayerInfo
.finditer(hand
.handText
)
303 hand
.addPlayer(int(a
.group('SEAT')), a
.group('PNAME'), a
.group('CASH'))
306 def markStreets(self
, hand
):
307 m
= re
.search(r
"\*\*\* ANTE\/BLINDS \*\*\*(?P<PREFLOP>.+(?=\*\*\* FLOP \*\*\*)|.+)"
308 r
"(\*\*\* FLOP \*\*\*(?P<FLOP> \[\S\S \S\S \S\S\].+(?=\*\*\* TURN \*\*\*)|.+))?"
309 r
"(\*\*\* TURN \*\*\* \[\S\S \S\S \S\S](?P<TURN>\[\S\S\].+(?=\*\*\* RIVER \*\*\*)|.+))?"
310 r
"(\*\*\* RIVER \*\*\* \[\S\S \S\S \S\S \S\S](?P<RIVER>\[\S\S\].+))?", hand
.handText
,re
.DOTALL
)
314 # print "adding street", m.group(0)
317 print (_("Failed to add streets. handtext=%s"))
319 #Needs to return a list in the format
320 # ['player1name', 'player2name', ...] where player1name is the sb and player2name is bb,
321 # addtional players are assumed to post a bb oop
323 def readButton(self
, hand
):
324 m
= self
.re_Button
.search(hand
.handText
)
326 hand
.buttonpos
= int(m
.group('BUTTON'))
327 log
.debug(_('readButton: button on pos %d') % hand
.buttonpos
)
329 log
.warning(_('readButton: not found'))
331 # def readCommunityCards(self, hand, street):
332 # #print hand.streets.group(street)
333 # if street in ('FLOP','TURN','RIVER'): # a list of streets which get dealt community cards (i.e. all but PREFLOP)
334 # m = self.re_Board.search(hand.streets.group(street))
335 # hand.setCommunityCards(street, m.group('CARDS').split(','))
337 def readCommunityCards(self
, hand
, street
): # street has been matched by markStreets, so exists in this hand
338 if street
in ('FLOP','TURN','RIVER'): # a list of streets which get dealt community cards (i.e. all but PREFLOP)
339 #print "DEBUG readCommunityCards:", street, hand.streets.group(street)
340 m
= self
.re_Board
.search(hand
.streets
[street
])
341 hand
.setCommunityCards(street
, m
.group('CARDS').split(' '))
343 def readBlinds(self
, hand
):
344 if not self
.re_DenySB
.search(hand
.handText
):
346 m
= self
.re_PostSB
.search(hand
.handText
)
347 hand
.addBlind(m
.group('PNAME'), 'small blind', m
.group('SB'))
348 except exceptions
.AttributeError: # no small blind
349 log
.warning( _("readBlinds in noSB exception - no SB created")+str(sys
.exc_info()) )
350 #hand.addBlind(None, None, None)
351 for a
in self
.re_PostBB
.finditer(hand
.handText
):
352 hand
.addBlind(a
.group('PNAME'), 'big blind', a
.group('BB'))
353 for a
in self
.re_PostDead
.finditer(hand
.handText
):
354 #print "DEBUG: Found dead blind: addBlind(%s, 'secondsb', %s)" %(a.group('PNAME'), a.group('DEAD'))
355 hand
.addBlind(a
.group('PNAME'), 'secondsb', a
.group('DEAD'))
356 for a
in self
.re_PostBoth
.finditer(hand
.handText
):
357 hand
.addBlind(a
.group('PNAME'), 'small & big blinds', a
.group('SBBB'))
359 def readAntes(self
, hand
):
360 log
.debug(_("reading antes"))
361 m
= self
.re_Antes
.finditer(hand
.handText
)
363 #~ logging.debug("hand.addAnte(%s,%s)" %(player.group('PNAME'), player.group('ANTE')))
364 hand
.addAnte(player
.group('PNAME'), player
.group('ANTE'))
366 def readBringIn(self
, hand
):
367 m
= self
.re_BringIn
.search(hand
.handText
,re
.DOTALL
)
369 #~ logging.debug("readBringIn: %s for %s" %(m.group('PNAME'), m.group('BRINGIN')))
370 hand
.addBringIn(m
.group('PNAME'), m
.group('BRINGIN'))
372 def readHeroCards(self
, hand
):
373 # streets PREFLOP, PREDRAW, and THIRD are special cases beacause
374 # we need to grab hero's cards
375 for street
in ('PREFLOP', 'DEAL', 'BLINDSANTES'):
376 if street
in hand
.streets
.keys():
377 m
= self
.re_HeroCards
.finditer(hand
.streets
[street
])
379 log
.debug(_("No hole cards found for %s") % street
)
381 hand
.hero
= found
.group('PNAME')
382 newcards
= found
.group('CARDS').split(' ')
383 # print "DEBUG: addHoleCards(%s, %s, %s)" %(street, hand.hero, newcards)
384 hand
.addHoleCards(street
, hand
.hero
, closed
=newcards
, shown
=False, mucked
=False, dealt
=True)
385 log
.debug(_("Hero cards %s: %s") % (hand
.hero
, newcards
))
387 def readAction(self
, hand
, street
):
388 m
= self
.re_Action
.finditer(hand
.streets
[street
])
390 acts
= action
.groupdict()
391 if action
.group('ATYPE') == ' raises':
392 hand
.addRaiseBy( street
, action
.group('PNAME'), action
.group('BET') )
393 elif action
.group('ATYPE') == ' calls':
394 hand
.addCall( street
, action
.group('PNAME'), action
.group('BET') )
395 elif action
.group('ATYPE') == ' bets':
396 hand
.addBet( street
, action
.group('PNAME'), action
.group('BET') )
397 elif action
.group('ATYPE') == ' folds':
398 hand
.addFold( street
, action
.group('PNAME'))
399 elif action
.group('ATYPE') == ' checks':
400 hand
.addCheck( street
, action
.group('PNAME'))
401 elif action
.group('ATYPE') == ' discards':
402 hand
.addDiscard(street
, action
.group('PNAME'), action
.group('BET'), action
.group('DISCARDED'))
403 elif action
.group('ATYPE') == ' stands pat':
404 hand
.addStandsPat( street
, action
.group('PNAME'))
406 log
.fatal(_("DEBUG:") + _("Unimplemented %s: '%s' '%s'") % ("readAction", action
.group('PNAME'), action
.group('ATYPE')))
407 # print "Processed %s"%acts
408 # print "committed=",hand.pot.committed
410 def readShowdownActions(self
, hand
):
411 for shows
in self
.re_ShowdownAction
.finditer(hand
.handText
):
412 #log.debug(_("add show actions %s") % shows)
413 cards
= shows
.group('CARDS')
414 cards
= cards
.split(' ')
415 # print "DEBUG: addShownCards(%s, %s)" %(cards, shows.group('PNAME'))
416 hand
.addShownCards(cards
, shows
.group('PNAME'))
418 def readCollectPot(self
,hand
):
419 # Winamax has unfortunately thinks that a sidepot is created
420 # when there is uncalled money in the pot - something that can
421 # only happen when a player is all-in
423 # Becuase of this, we need to do the same calculations as class Pot()
424 # and determine if the amount returned is the same as the amount collected
425 # if so then the collected line is invalid
427 total
= sum(hand
.pot
.committed
.values()) + sum(hand
.pot
.common
.values())
429 # Return any uncalled bet.
430 committed
= sorted([ (v
,k
) for (k
,v
) in hand
.pot
.committed
.items()])
431 #print "DEBUG: committed: %s" % committed
433 lastbet
= committed
[-1][0] - committed
[-2][0]
434 if lastbet
> 0: # uncalled
435 returnto
= committed
[-1][1]
436 #print "DEBUG: returning %f to %s" % (lastbet, returnto)
438 returned
[returnto
] = lastbet
442 tp
= self
.re_Total
.search(hand
.handText
)
443 rake
= tp
.group('RAKE')
446 for m
in self
.re_CollectPot
.finditer(hand
.handText
):
447 collectees
.append([m
.group('PNAME'), m
.group('POT')])
449 #print "DEBUG: Total pot: %s" % tp.groupdict()
450 #print "DEBUG: According to pot: %s" % total
451 #print "DEBUG: Rake: %s" % rake
453 if len(collectees
) == 1:
454 plyr
, p
= collectees
[0]
455 # p may be wrong, use calculated total - rake
456 p
= total
- Decimal(rake
)
457 #print "DEBUG: len1: addCollectPot(%s,%s)" %(plyr, p)
458 hand
.addCollectPot(player
=plyr
,pot
=p
)
460 for plyr
, p
in collectees
:
461 if plyr
in returned
.keys():
462 p
= Decimal(p
) - returned
[plyr
]
464 #print "DEBUG: addCollectPot(%s,%s)" %(plyr, p)
465 hand
.addCollectPot(player
=plyr
,pot
=p
)
467 def readShownCards(self
,hand
):
468 for m
in self
.re_ShownCards
.finditer(hand
.handText
):
469 log
.debug(_("Read shown cards: %s") % m
.group(0))
470 cards
= m
.group('CARDS')
471 cards
= cards
.split(' ') # needs to be a list, not a set--stud needs the order
472 (shown
, mucked
) = (False, False)
473 if m
.group('CARDS') is not None:
475 hand
.addShownCards(cards
=cards
, player
=m
.group('PNAME'), shown
=shown
, mucked
=mucked
)