fpdb/pyfpdb/PartyPokerToFpdb.py
Erki Ferenc c7e86513c7 Added validation to blind detection
Included checks for any ring game hands if there's a blind could be
found and if either SB or BB detection fails then determineGameType will
return None. It's needed for handling some invalid ring hands (e.g.
table closed messages at the end of a HH file).

Signed-off-by: steffen123 <steffen@schaumburger.info>
2010-08-10 02:05:14 +02:00

518 lines
21 KiB
Python
Executable File

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright 2009-2010, Grigorij Indigirkin
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
########################################################################
import sys
from collections import defaultdict
from Exceptions import FpdbParseError
from HandHistoryConverter import *
# PartyPoker HH Format
class FpdbParseError(FpdbParseError):
"Usage: raise FpdbParseError(<msg>[, hh=<hh>][, hid=<hid>])"
def __init__(self, msg='', hh=None, hid=None):
return super(FpdbParseError, self).__init__(msg, hid=hid)
def wrapHh(self, hh):
return ("%(DELIMETER)s\n%(HH)s\n%(DELIMETER)s") % \
{'DELIMETER': '#'*50, 'HH': hh}
class PartyPoker(HandHistoryConverter):
sitename = "PartyPoker"
codepage = "cp1252"
siteId = 9
filetype = "text"
sym = {'USD': "\$", }
# Static regexes
# $5 USD NL Texas Hold'em - Saturday, July 25, 07:53:52 EDT 2009
# NL Texas Hold'em $1 USD Buy-in Trny:45685440 Level:8 Blinds-Antes(600/1 200 -50) - Sunday, May 17, 11:25:07 MSKS 2009
re_GameInfoRing = re.compile("""
(?P<CURRENCY>\$|)\s*(?P<RINGLIMIT>[.,0-9]+)([.,0-9/$]+)?\s*(?:USD)?\s*
(?P<LIMIT>(NL|PL|))\s*
(?P<GAME>(Texas\ Hold\'em|Omaha))
\s*\-\s*
(?P<DATETIME>.+)
""", re.VERBOSE)
re_GameInfoTrny = re.compile("""
(?P<LIMIT>(NL|PL|))\s*
(?P<GAME>(Texas\ Hold\'em|Omaha))\s+
(?:(?P<BUYIN>\$?[.,0-9]+)\s*(?P<BUYIN_CURRENCY>USD)?\s*Buy-in\s+)?
Trny:\s?(?P<TOURNO>\d+)\s+
Level:\s*(?P<LEVEL>\d+)\s+
((Blinds|Stakes)(?:-Antes)?)\(
(?P<SB>[.,0-9 ]+)\s*
/(?P<BB>[.,0-9 ]+)
(?:\s*-\s*(?P<ANTE>[.,0-9 ]+)\$?)?
\)
\s*\-\s*
(?P<DATETIME>.+)
""", re.VERBOSE)
re_Hid = re.compile("^Game \#(?P<HID>\d+) starts.")
re_PlayerInfo = re.compile("""
Seat\s(?P<SEAT>\d+):\s
(?P<PNAME>.*)\s
\(\s*\$?(?P<CASH>[0-9,.]+)\s*(?:USD|)\s*\)
""" ,
re.VERBOSE)
re_HandInfo = re.compile("""
^Table\s+(?P<TTYPE>[$a-zA-Z0-9 ]+)?\s+
(?: \#|\(|)(?P<TABLE>\d+)\)?\s+
(?:[a-zA-Z0-9 ]+\s+\#(?P<MTTTABLE>\d+).+)?
(\(No\sDP\)\s)?
\((?P<PLAY>Real|Play)\s+Money\)\s+ # FIXME: check if play money is correct
Seat\s+(?P<BUTTON>\d+)\sis\sthe\sbutton
\s+Total\s+number\s+of\s+players\s+\:\s+(?P<PLYRS>\d+)/?(?P<MAX>\d+)?
""",
re.VERBOSE|re.MULTILINE|re.DOTALL)
re_CountedSeats = re.compile("^Total\s+number\s+of\s+players\s*:\s*(?P<COUNTED_SEATS>\d+)", re.MULTILINE)
re_SplitHands = re.compile('\x00+')
re_TailSplitHands = re.compile('(\x00+)')
lineSplitter = '\n'
re_Button = re.compile('Seat (?P<BUTTON>\d+) is the button', re.MULTILINE)
re_Board = re.compile(r"\[(?P<CARDS>.+)\]")
re_NoSmallBlind = re.compile(
'^There is no Small Blind in this hand as the Big Blind '
'of the previous hand left the table', re.MULTILINE)
re_ringSB = re.compile(r"(?P<PLAYER>.*) posts small blind \[\$(?P<RINGSB>[.,0-9]*) USD\]\.")
re_ringBB = re.compile(r"(?P<PLAYER>.*) posts big blind \[\$(?P<RINGBB>[.,0-9]*) USD\]\.")
def allHandsAsList(self):
list = HandHistoryConverter.allHandsAsList(self)
if list is None:
return []
return filter(lambda text: len(text.strip()), list)
def guessMaxSeats(self, hand):
"""Return a guess at max_seats when not specified in HH."""
mo = self.maxOccSeat(hand)
if mo == 10: return mo
if mo == 2: return 2
if mo <= 6: return 6
# there are 9-max tables for cash and 10-max for tournaments
return 9 if hand.gametype['type']=='ring' else 10
def compilePlayerRegexs(self, hand):
players = set([player[1] for player in hand.players])
if not players <= self.compiledPlayers: # x <= y means 'x is subset of y'
self.compiledPlayers = players
player_re = "(?P<PNAME>" + "|".join(map(re.escape, players)) + ")"
subst = {'PLYR': player_re, 'CUR_SYM': hand.SYMBOL[hand.gametype['currency']],
'CUR': hand.gametype['currency'] if hand.gametype['currency']!='T$' else ''}
for key in ('CUR_SYM', 'CUR'):
subst[key] = re.escape(subst[key])
self.re_PostSB = re.compile(
r"^%(PLYR)s posts small blind \[%(CUR_SYM)s(?P<SB>[.,0-9]+) ?%(CUR)s\]\." % subst,
re.MULTILINE)
self.re_PostBB = re.compile(
r"^%(PLYR)s posts big blind \[%(CUR_SYM)s(?P<BB>[.,0-9]+) ?%(CUR)s\]\." % subst,
re.MULTILINE)
self.re_PostDead = re.compile(
r"^%(PLYR)s posts big blind \+ dead \[(?P<BBNDEAD>[.,0-9]+) ?%(CUR_SYM)s\]\." % subst,
re.MULTILINE)
self.re_Antes = re.compile(
r"^%(PLYR)s posts ante \[%(CUR_SYM)s(?P<ANTE>[.,0-9]+) ?%(CUR)s\]" % subst,
re.MULTILINE)
self.re_HeroCards = re.compile(
r"^Dealt to %(PLYR)s \[\s*(?P<NEWCARDS>.+)\s*\]" % subst,
re.MULTILINE)
self.re_Action = re.compile(r"""
^%(PLYR)s\s+(?P<ATYPE>bets|checks|raises|calls|folds|is\sall-In)
(?:\s+\[%(CUR_SYM)s(?P<BET>[.,\d]+)\s*%(CUR)s\])?
""" % subst,
re.MULTILINE|re.VERBOSE)
self.re_ShownCards = re.compile(
r"^%s (?P<SHOWED>(?:doesn\'t )?shows?) " % player_re +
r"\[ *(?P<CARDS>.+) *\](?P<COMBINATION>.+)\.",
re.MULTILINE)
self.re_CollectPot = re.compile(
r"""^%(PLYR)s \s+ wins \s+
%(CUR_SYM)s(?P<POT>[.,\d]+)\s*%(CUR)s""" % subst,
re.MULTILINE|re.VERBOSE)
def readSupportedGames(self):
return [["ring", "hold", "nl"],
["ring", "hold", "pl"],
["ring", "hold", "fl"],
["tour", "hold", "nl"],
["tour", "hold", "pl"],
["tour", "hold", "fl"],
]
def _getGameType(self, handText):
if not hasattr(self, '_gameType'):
self._gameType = None
if self._gameType is None:
# let's determine whether hand is trny
# and whether 5-th line contains head line
headLine = handText.split(self.lineSplitter)[4]
for headLineContainer in headLine, handText:
for regexp in self.re_GameInfoTrny, self.re_GameInfoRing:
m = regexp.search(headLineContainer)
if m is not None:
self._gameType = m
return self._gameType
return self._gameType
def determineGameType(self, handText):
"""inspect the handText and return the gametype dict
gametype dict is:
{'limitType': xxx, 'base': xxx, 'category': xxx}"""
info = {}
m = self._getGameType(handText)
m_sb = self.re_ringSB.search(handText)
m_bb = self.re_ringBB.search(handText)
if m is None:
return None
mg = m.groupdict()
# translations from captured groups to fpdb info strings
limits = { 'NL':'nl', 'PL':'pl', '':'fl' }
games = { # base, category
"Texas Hold'em" : ('hold','holdem'),
'Omaha' : ('hold','omahahi'),
}
currencies = { '$':'USD', '':'T$' }
for expectedField in ['LIMIT', 'GAME']:
if mg[expectedField] is None:
raise FpdbParseError( "Cannot fetch field '%s'" % expectedField)
try:
info['limitType'] = limits[mg['LIMIT'].strip()]
except:
raise FpdbParseError("Unknown limit '%s'" % mg['LIMIT'])
try:
(info['base'], info['category']) = games[mg['GAME']]
except:
raise FpdbParseError("Unknown game type '%s'" % mg['GAME'])
if 'TOURNO' in mg:
info['type'] = 'tour'
else:
info['type'] = 'ring'
if info['type'] == 'ring':
if (m_sb is None) or (m_bb is None):
return None
else:
info['sb'] = m_sb.group('RINGSB')
info['bb'] = m_bb.group('RINGBB')
info['currency'] = currencies[mg['CURRENCY']]
else:
info['sb'] = clearMoneyString(mg['SB'])
info['bb'] = clearMoneyString(mg['BB'])
info['currency'] = 'T$'
return info
def readHandInfo(self, hand):
info = {}
try:
info.update(self.re_Hid.search(hand.handText).groupdict())
except:
raise FpdbParseError("Cannot read HID for current hand")
try:
info.update(self.re_HandInfo.search(hand.handText,re.DOTALL).groupdict())
except:
raise FpdbParseError("Cannot read Handinfo for current hand", hid = info['HID'])
try:
info.update(self._getGameType(hand.handText).groupdict())
except:
raise FpdbParseError("Cannot read GameType for current hand", hid = info['HID'])
m = self.re_CountedSeats.search(hand.handText)
if m: info.update(m.groupdict())
# FIXME: it's dirty hack
# party doesnt subtract uncalled money from commited money
# so hand.totalPot calculation has to be redefined
from Hand import Pot, HoldemOmahaHand
def getNewTotalPot(origTotalPot):
def totalPot(self):
if self.totalpot is None:
self.pot.end()
self.totalpot = self.pot.total
for i,v in enumerate(self.collected):
if v[0] in self.pot.returned:
self.collected[i][1] = Decimal(v[1]) - self.pot.returned[v[0]]
self.collectees[v[0]] -= self.pot.returned[v[0]]
self.pot.returned[v[0]] = 0
return origTotalPot()
return totalPot
instancemethod = type(hand.totalPot)
hand.totalPot = instancemethod(getNewTotalPot(hand.totalPot), hand, HoldemOmahaHand)
log.debug("readHandInfo: %s" % info)
for key in info:
if key == 'DATETIME':
#Saturday, July 25, 07:53:52 EDT 2009
#Thursday, July 30, 21:40:41 MSKS 2009
#Sunday, October 25, 13:39:07 MSK 2009
m2 = re.search("\w+, (?P<M>\w+) (?P<D>\d+), (?P<H>\d+):(?P<MIN>\d+):(?P<S>\d+) (?P<TZ>[A-Z]+) (?P<Y>\d+)", info[key])
# we cant use '%B' due to locale problems
months = ['January', 'February', 'March', 'April','May', 'June',
'July','August','September','October','November','December']
month = months.index(m2.group('M')) + 1
datetimestr = "%s/%s/%s %s:%s:%s" % (m2.group('Y'), month,m2.group('D'),m2.group('H'),m2.group('MIN'),m2.group('S'))
hand.startTime = datetime.datetime.strptime(datetimestr, "%Y/%m/%d %H:%M:%S")
# FIXME: some timezone correction required
#tzShift = defaultdict(lambda:0, {'EDT': -5, 'EST': -6, 'MSKS': 3})
#hand.starttime -= datetime.timedelta(hours=tzShift[m2.group('TZ')])
if key == 'HID':
hand.handid = info[key]
if key == 'TABLE':
hand.tablename = info[key]
if key == 'MTTTABLE':
if info[key] != None:
hand.tablename = info[key]
hand.tourNo = info['TABLE']
if key == 'BUTTON':
hand.buttonpos = info[key]
if key == 'TOURNO':
hand.tourNo = info[key]
if key == 'TABLE_ID_WRAPPER':
if info[key] == '#':
# FIXME: there is no such property in Hand class
self.isSNG = True
if key == 'BUYIN':
# FIXME: it's dirty hack T_T
# code below assumes that tournament rake is equal to zero
if info[key] == None:
hand.buyin = '$0+$0'
else:
cur = info[key][0] if info[key][0] not in '0123456789' else ''
hand.buyin = info[key] + '+%s0' % cur
if key == 'LEVEL':
hand.level = info[key]
if key == 'PLAY' and info['PLAY'] != 'Real':
# if realy party doesn's save play money hh
hand.gametype['currency'] = 'play'
if key == 'MAX' and info[key] is not None:
hand.maxseats = int(info[key])
def readButton(self, hand):
m = self.re_Button.search(hand.handText)
if m:
hand.buttonpos = int(m.group('BUTTON'))
else:
log.info('readButton: not found')
def readPlayerStacks(self, hand):
log.debug("readPlayerStacks")
m = self.re_PlayerInfo.finditer(hand.handText)
players = []
for a in m:
hand.addPlayer(int(a.group('SEAT')), a.group('PNAME'),
clearMoneyString(a.group('CASH')))
def markStreets(self, hand):
m = re.search(
r"\*{2} Dealing down cards \*{2}"
r"(?P<PREFLOP>.+?)"
r"(?:\*{2} Dealing Flop \*{2} (?P<FLOP>\[ \S\S, \S\S, \S\S \].+?))?"
r"(?:\*{2} Dealing Turn \*{2} (?P<TURN>\[ \S\S \].+?))?"
r"(?:\*{2} Dealing River \*{2} (?P<RIVER>\[ \S\S \].+?))?$"
, hand.handText,re.DOTALL)
hand.addStreets(m)
def readCommunityCards(self, hand, street):
if street in ('FLOP','TURN','RIVER'):
m = self.re_Board.search(hand.streets[street])
hand.setCommunityCards(street, renderCards(m.group('CARDS')))
def readAntes(self, hand):
log.debug("reading antes")
m = self.re_Antes.finditer(hand.handText)
for player in m:
hand.addAnte(player.group('PNAME'), player.group('ANTE'))
def readBlinds(self, hand):
noSmallBlind = bool(self.re_NoSmallBlind.search(hand.handText))
if hand.gametype['type'] == 'ring':
try:
assert noSmallBlind==False
liveBlind = True
for m in self.re_PostSB.finditer(hand.handText):
if liveBlind:
hand.addBlind(m.group('PNAME'), 'small blind', m.group('SB'))
liveBlind = False
else:
# Post dead blinds as ante
hand.addBlind(m.group('PNAME'), 'secondsb', m.group('SB'))
except: # no small blind
hand.addBlind(None, None, None)
for a in self.re_PostBB.finditer(hand.handText):
hand.addBlind(a.group('PNAME'), 'big blind', a.group('BB'))
deadFilter = lambda s: s.replace(',', '.')
for a in self.re_PostDead.finditer(hand.handText):
hand.addBlind(a.group('PNAME'), 'both', deadFilter(a.group('BBNDEAD')))
else:
# party doesn't track blinds for tournaments
# so there're some cra^Wcaclulations
if hand.buttonpos == 0:
self.readButton(hand)
# NOTE: code below depends on Hand's implementation
# playersMap - dict {seat: (pname,stack)}
playersMap = dict([(f[0], f[1:3]) for f in hand.players])
maxSeat = max(playersMap)
def findFirstNonEmptySeat(startSeat):
while startSeat not in playersMap:
if startSeat >= maxSeat:
startSeat = 0
startSeat += 1
return startSeat
smartMin = lambda A,B: A if float(A) <= float(B) else B
if noSmallBlind:
hand.addBlind(None, None, None)
smallBlindSeat = int(hand.buttonpos)
else:
smallBlindSeat = findFirstNonEmptySeat(int(hand.buttonpos) + 1)
blind = smartMin(hand.sb, playersMap[smallBlindSeat][1])
hand.addBlind(playersMap[smallBlindSeat][0], 'small blind', blind)
bigBlindSeat = findFirstNonEmptySeat(smallBlindSeat + 1)
blind = smartMin(hand.bb, playersMap[bigBlindSeat][1])
hand.addBlind(playersMap[bigBlindSeat][0], 'big blind', blind)
def readHeroCards(self, hand):
# we need to grab hero's cards
for street in ('PREFLOP',):
if street in hand.streets.keys():
m = self.re_HeroCards.finditer(hand.streets[street])
for found in m:
hand.hero = found.group('PNAME')
newcards = renderCards(found.group('NEWCARDS'))
hand.addHoleCards(street, hand.hero, closed=newcards, shown=False, mucked=False, dealt=True)
def readAction(self, hand, street):
m = self.re_Action.finditer(hand.streets[street])
for action in m:
acts = action.groupdict()
playerName = action.group('PNAME')
amount = clearMoneyString(action.group('BET')) if action.group('BET') else None
actionType = action.group('ATYPE')
if actionType == 'is all-In':
# party's allin can mean either raise or bet or call
Bp = hand.lastBet[street]
if Bp == 0:
actionType = 'bets'
elif Bp < Decimal(amount):
actionType = 'raises'
else:
actionType = 'calls'
if actionType == 'raises':
if street == 'PREFLOP' and \
playerName in [item[0] for item in hand.actions['BLINDSANTES'] if item[2]!='ante']:
# preflop raise from blind
hand.addCallandRaise( street, playerName, amount )
else:
hand.addCallandRaise( street, playerName, amount )
elif actionType == 'calls':
hand.addCall( street, playerName, amount )
elif actionType == 'bets':
hand.addBet( street, playerName, amount )
elif actionType == 'folds':
hand.addFold( street, playerName )
elif actionType == 'checks':
hand.addCheck( street, playerName )
else:
raise FpdbParseError(
"Unimplemented readAction: '%s' '%s'" % (playerName,actionType,),
hid = hand.hid, )
def readShowdownActions(self, hand):
# all action in readShownCards
pass
def readCollectPot(self,hand):
for m in self.re_CollectPot.finditer(hand.handText):
hand.addCollectPot(player=m.group('PNAME'),pot=clearMoneyString(m.group('POT')))
def readShownCards(self,hand):
for m in self.re_ShownCards.finditer(hand.handText):
if m.group('CARDS') is not None:
cards = renderCards(m.group('CARDS'))
mucked = m.group('SHOWED') != "show"
hand.addShownCards(cards=cards, player=m.group('PNAME'), shown=True, mucked=mucked)
@staticmethod
def getTableTitleRe(type, table_name=None, tournament = None, table_number=None):
"Returns string to search in windows titles"
if type=="tour":
TableName = table_name.split(" ")
print 'party', 'getTableTitleRe', "%s.+Table\s#%s" % (TableName[0], table_number)
return "%s.+Table\s#%s" % (TableName[0], table_number)
else:
print 'party', 'getTableTitleRe', table_number
return table_name
def clearMoneyString(money):
"Renders 'numbers' like '1 200' and '2,000'"
return money.replace(' ', '').replace(',', '')
def renderCards(string):
"Splits strings like ' Js, 4d '"
cards = string.strip().split(' ')
return filter(len, map(lambda x: x.strip(' ,'), cards))
if __name__ == "__main__":
parser = OptionParser()
parser.add_option("-i", "--input", dest="ipath", help="parse input hand history")
parser.add_option("-o", "--output", dest="opath", help="output translation to", default="-")
parser.add_option("-f", "--follow", dest="follow", help="follow (tail -f) the input", action="store_true", default=False)
parser.add_option("-q", "--quiet",
action="store_const", const=logging.CRITICAL, dest="verbosity", default=logging.INFO)
parser.add_option("-v", "--verbose",
action="store_const", const=logging.INFO, dest="verbosity")
parser.add_option("--vv",
action="store_const", const=logging.DEBUG, dest="verbosity")
(options, args) = parser.parse_args()
e = PartyPoker(in_path = options.ipath, out_path = options.opath, follow = options.follow)