731 lines
38 KiB
Python
Executable File
731 lines
38 KiB
Python
Executable File
#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
#
|
||
# Copyright 2008-2010, Carl Gherardi
|
||
#
|
||
# 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 L10n
|
||
_ = L10n.get_translation()
|
||
|
||
import logging
|
||
from HandHistoryConverter import *
|
||
#import TourneySummary
|
||
|
||
# Fulltilt HH Format converter
|
||
|
||
class Fulltilt(HandHistoryConverter):
|
||
|
||
sitename = "Fulltilt"
|
||
filetype = "text"
|
||
codepage = ["utf-16", "cp1252", "utf-8"]
|
||
siteId = 1 # Needs to match id entry in Sites database
|
||
|
||
substitutions = {
|
||
'LEGAL_ISO' : "USD|EUR|GBP|CAD|FPP", # legal ISO currency codes
|
||
'LS' : u"\$|\u20AC|\xe2\x82\xac|", # legal currency symbols - Euro(cp1252, utf-8)
|
||
'TAB' : u"-\u2013'\s\da-zA-Z"
|
||
}
|
||
|
||
# Static regexes
|
||
re_GameInfo = re.compile(u'''.*\#(?P<HID>[0-9]+):\s
|
||
(?:(?P<TOURNAMENT>.+)\s\((?P<TOURNO>\d+)\),\s)?
|
||
.+
|
||
-\s(?P<CURRENCY>[%(LS)s]|)?
|
||
(?P<SB>[.0-9]+)/
|
||
[%(LS)s]?(?P<BB>[.0-9]+)\s
|
||
(Ante\s\$?(?P<ANTE>[.0-9]+)\s)?-\s
|
||
[%(LS)s]?(?P<CAP>[.0-9]+\sCap\s)?
|
||
(?P<LIMIT>(No\sLimit|Pot\sLimit|Limit))?\s
|
||
(?P<GAME>(Hold\'em|Omaha\sHi|Omaha\sH/L|7\sCard\sStud|Stud\sH/L|Razz|Stud\sHi|2-7\sTriple\sDraw|5\sCard\sDraw|Badugi))
|
||
''' % substitutions, re.VERBOSE)
|
||
re_SplitHands = re.compile(r"\n\n\n+")
|
||
re_TailSplitHands = re.compile(r"(\n\n+)")
|
||
re_HandInfo = re.compile(r'''.*\#(?P<HID>[0-9]+):\s
|
||
(?:(?P<TOURNAMENT>.+)\s\((?P<TOURNO>\d+)\),\s)?
|
||
(Table|Match)\s
|
||
(?P<PLAY>Play\sChip\s|PC)?
|
||
(?P<TABLE>[%(TAB)s]+)\s
|
||
(\((?P<TABLEATTRIBUTES>.+)\)\s)?-\s
|
||
[%(LS)s]?(?P<SB>[.0-9]+)/[%(LS)s]?(?P<BB>[.0-9]+)\s(Ante\s[%(LS)s]?(?P<ANTE>[.0-9]+)\s)?-\s
|
||
[%(LS)s]?(?P<CAP>[.0-9]+\sCap\s)?
|
||
(?P<GAMETYPE>[-\da-zA-Z\/\'\s]+)\s-\s
|
||
(?P<DATETIME>.*$)
|
||
(?P<PARTIAL>\(partial\))?\n
|
||
(?:.*?\n(?P<CANCELLED>Hand\s\#(?P=HID)\shas\sbeen\scanceled))?
|
||
''' % substitutions, re.MULTILINE|re.VERBOSE)
|
||
re_TourneyExtraInfo = re.compile('''(((?P<TOURNEY_NAME>[^$]+)?
|
||
(?P<CURRENCY>[%(LS)s])?(?P<BUYIN>[.0-9]+)?\s*\+\s*[%(LS)s]?(?P<FEE>[.0-9]+)?
|
||
(\s(?P<SPECIAL>(KO|Heads\sUp|Matrix\s\dx|Rebuy|Madness)))?
|
||
(\s(?P<SHOOTOUT>Shootout))?
|
||
(\s(?P<SNG>Sit\s&\sGo))?
|
||
(\s\((?P<TURBO>Turbo)\))?)|(?P<UNREADABLE_INFO>.+))
|
||
''' % substitutions, re.VERBOSE)
|
||
re_Button = re.compile('^The button is in seat #(?P<BUTTON>\d+)', re.MULTILINE)
|
||
re_PlayerInfo = re.compile('Seat (?P<SEAT>[0-9]+): (?P<PNAME>.{2,15}) \([%(LS)s](?P<CASH>[,.0-9]+)\)$' % substitutions, re.MULTILINE)
|
||
re_TourneysPlayerInfo = re.compile('Seat (?P<SEAT>[0-9]+): (?P<PNAME>.{2,15}) \([%(LS)s]?(?P<CASH>[,.0-9]+)\)(, is sitting out)?$' % substitutions, re.MULTILINE)
|
||
re_Board = re.compile(r"\[(?P<CARDS>.+)\]")
|
||
|
||
#static regex for tourney purpose
|
||
re_TourneyInfo = re.compile('''Tournament\sSummary\s
|
||
(?P<TOURNAMENT_NAME>[^$(]+)?\s*
|
||
((?P<CURRENCY>[%(LS)s]|)?(?P<BUYIN>[.0-9]+)\s*\+\s*[%(LS)s]?(?P<FEE>[.0-9]+)\s)?
|
||
((?P<SPECIAL>(KO|Heads\sUp|Matrix\s\dx|Rebuy|Madness))\s)?
|
||
((?P<SHOOTOUT>Shootout)\s)?
|
||
((?P<SNG>Sit\s&\sGo)\s)?
|
||
(\((?P<TURBO1>Turbo)\)\s)?
|
||
\((?P<TOURNO>\d+)\)\s
|
||
((?P<MATCHNO>Match\s\d)\s)?
|
||
(?P<GAME>(Hold\'em|Omaha\sHi|Omaha\sH/L|7\sCard\sStud|Stud\sH/L|Razz|Stud\sHi))\s
|
||
(\((?P<TURBO2>Turbo)\)\s)?
|
||
(?P<LIMIT>(No\sLimit|Pot\sLimit|Limit))?
|
||
''' % substitutions, re.VERBOSE)
|
||
re_TourneyBuyInFee = re.compile("Buy-In: (?P<BUYIN_CURRENCY>[%(LS)s]|)?(?P<BUYIN>[.0-9]+) \+ [%(LS)s]?(?P<FEE>[.0-9]+)" % substitutions)
|
||
re_TourneyBuyInChips = re.compile("Buy-In Chips: (?P<BUYINCHIPS>\d+)")
|
||
re_TourneyEntries = re.compile("(?P<ENTRIES>\d+) Entries")
|
||
re_TourneyPrizePool = re.compile("Total Prize Pool: (?P<PRIZEPOOL_CURRENCY>[%(LS)s]|)?(?P<PRIZEPOOL>[.,0-9]+)" % substitutions)
|
||
re_TourneyRebuyCost = re.compile("Rebuy: (?P<REBUY_CURRENCY>[%(LS)s]|)?(?P<REBUY_COST>[.,0-9]+)"% substitutions)
|
||
re_TourneyAddOnCost = re.compile("Add-On: (?P<ADDON_CURRENCY>[%(LS)s]|)?(?P<ADDON_COST>[.,0-9]+)"% substitutions)
|
||
re_TourneyRebuyCount = re.compile("performed (?P<REBUY_COUNT>\d+) Rebuy")
|
||
re_TourneyAddOnCount = re.compile("performed (?P<ADDON_COUNT>\d+) Add-On")
|
||
re_TourneyRebuysTotal = re.compile("Total Rebuys: (?P<REBUY_TOTAL>\d+)")
|
||
re_TourneyAddOnsTotal = re.compile("Total Add-Ons: (?P<ADDONS_TOTAL>\d+)")
|
||
re_TourneyRebuyChips = re.compile("Rebuy Chips: (?P<REBUY_CHIPS>\d+)")
|
||
re_TourneyAddOnChips = re.compile("Add-On Chips: (?P<ADDON_CHIPS>\d+)")
|
||
re_TourneyKOBounty = re.compile("Knockout Bounty: (?P<KO_BOUNTY_CURRENCY>[%(LS)s]|)?(?P<KO_BOUNTY_AMOUNT>[.,0-9]+)" % substitutions)
|
||
re_TourneyKoCount = re.compile("received (?P<COUNT_KO>\d+) Knockout Bounty Award(s)?")
|
||
re_TourneyTimeInfo = re.compile("Tournament started: (?P<STARTTIME>.*)\nTournament ((?P<IN_PROGRESS>is still in progress)?|(finished:(?P<ENDTIME>.*))?)$")
|
||
|
||
re_TourneysPlayersSummary = re.compile("^(?P<RANK>(Still Playing|\d+))( - |: )(?P<PNAME>[^\n,]+)(, )?(?P<WINNING_CURRENCY>[%(LS)s]|)?(?P<WINNING>[.\d]+)?" % substitutions, re.MULTILINE)
|
||
re_TourneyHeroFinishingP = re.compile("(?P<HERO_NAME>.*) finished in (?P<HERO_FINISHING_POS>\d+)(st|nd|rd|th) place")
|
||
|
||
#TODO: See if we need to deal with play money tourney summaries -- Not right now (they shouldn't pass the re_TourneyInfo)
|
||
##Full Tilt Poker Tournament Summary 250 Play Money Sit & Go (102909471) Hold'em No Limit
|
||
##Buy-In: 250 Play Chips + 0 Play Chips
|
||
##Buy-In Chips: 1500
|
||
##6 Entries
|
||
##Total Prize Pool: 1,500 Play Chips
|
||
|
||
# These regexes are for FTP only
|
||
re_Mixed = re.compile(r'\s\-\s(?P<MIXED>HA|HORSE|HOSE)\s\-\s', re.VERBOSE)
|
||
re_Max = re.compile("(?P<MAX>\d+)( max)?", re.MULTILINE)
|
||
# NB: if we ever match "Full Tilt Poker" we should also match "FullTiltPoker", which PT Stud erroneously exports.
|
||
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}))""", re.MULTILINE)
|
||
|
||
|
||
|
||
|
||
mixes = { 'HORSE': 'horse', '7-Game': '7game', 'HOSE': 'hose', 'HA': 'ha'}
|
||
|
||
|
||
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'
|
||
# we need to recompile the player regexs.
|
||
self.compiledPlayers = players
|
||
player_re = "(?P<PNAME>" + "|".join(map(re.escape, players)) + ")"
|
||
self.substitutions['PLAYERS'] = player_re
|
||
|
||
logging.debug("player_re: " + player_re)
|
||
self.re_PostSB = re.compile(r"^%(PLAYERS)s posts the small blind of [%(LS)s]?(?P<SB>[.0-9]+)" % self.substitutions, re.MULTILINE)
|
||
self.re_PostDead = re.compile(r"^%(PLAYERS)s posts a dead small blind of [%(LS)s]?(?P<SB>[.0-9]+)" % self.substitutions, re.MULTILINE)
|
||
self.re_PostBB = re.compile(r"^%(PLAYERS)s posts (the big blind of )?[%(LS)s]?(?P<BB>[.0-9]+)" % self.substitutions, re.MULTILINE)
|
||
self.re_Antes = re.compile(r"^%(PLAYERS)s antes [%(LS)s]?(?P<ANTE>[.0-9]+)" % self.substitutions, re.MULTILINE)
|
||
self.re_BringIn = re.compile(r"^%(PLAYERS)s brings in for [%(LS)s]?(?P<BRINGIN>[.0-9]+)" % self.substitutions, re.MULTILINE)
|
||
self.re_PostBoth = re.compile(r"^%(PLAYERS)s posts small \& big blinds \[[%(LS)s]? (?P<SBBB>[.0-9]+)" % self.substitutions, re.MULTILINE)
|
||
self.re_HeroCards = re.compile(r"^Dealt to %s(?: \[(?P<OLDCARDS>.+?)\])?( \[(?P<NEWCARDS>.+?)\])" % player_re, re.MULTILINE)
|
||
self.re_Action = re.compile(r"^%(PLAYERS)s(?P<ATYPE> bets| checks| raises to| completes it to| calls| folds)( [%(LS)s]?(?P<BET>[.,\d]+))?" % self.substitutions, re.MULTILINE)
|
||
self.re_ShowdownAction = re.compile(r"^%s shows \[(?P<CARDS>.*)\]" % player_re, re.MULTILINE)
|
||
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>[.,\d]+)\)(, mucked| with.*)" % self.substitutions, re.MULTILINE)
|
||
self.re_SitsOut = re.compile(r"^%s sits out" % player_re, re.MULTILINE)
|
||
self.re_ShownCards = re.compile(r"^Seat (?P<SEAT>[0-9]+): %s (\(button\) |\(small blind\) |\(big blind\) )?(?P<ACT>showed|mucked) \[(?P<CARDS>.*)\].*" % player_re, re.MULTILINE)
|
||
|
||
def readSupportedGames(self):
|
||
return [["ring", "hold", "nl"],
|
||
["ring", "hold", "pl"],
|
||
["ring", "hold", "fl"],
|
||
["ring", "hold", "cn"],
|
||
|
||
["ring", "stud", "fl"],
|
||
|
||
["ring", "draw", "fl"],
|
||
["ring", "draw", "pl"],
|
||
["ring", "draw", "nl"],
|
||
|
||
["tour", "hold", "nl"],
|
||
["tour", "hold", "pl"],
|
||
["tour", "hold", "fl"],
|
||
|
||
["tour", "stud", "fl"],
|
||
]
|
||
|
||
def determineGameType(self, handText):
|
||
# Full Tilt Poker Game #10777181585: Table Deerfly (deep 6) - $0.01/$0.02 - Pot Limit Omaha Hi - 2:24:44 ET - 2009/02/22
|
||
# Full Tilt Poker Game #10773265574: Table Butte (6 max) - $0.01/$0.02 - Pot Limit Hold'em - 21:33:46 ET - 2009/02/21
|
||
# Full Tilt Poker Game #9403951181: Table CR - tay - $0.05/$0.10 - No Limit Hold'em - 9:40:20 ET - 2008/12/09
|
||
# Full Tilt Poker Game #10809877615: Table Danville - $0.50/$1 Ante $0.10 - Limit Razz - 21:47:27 ET - 2009/02/23
|
||
# Full Tilt Poker.fr Game #23057874034: Table Douai–Lens (6 max) - €0.01/€0.02 - No Limit Hold'em - 21:59:17 CET - 2010/08/13
|
||
info = {'type':'ring'}
|
||
|
||
m = self.re_GameInfo.search(handText)
|
||
if not m:
|
||
tmp = handText[0:100]
|
||
log.error(_("determineGameType: Unable to recognise gametype from: '%s'") % tmp)
|
||
log.error(_("determineGameType: Raising FpdbParseError"))
|
||
raise FpdbParseError(_("Unable to recognise gametype from: '%s'") % tmp)
|
||
mg = m.groupdict()
|
||
|
||
# translations from captured groups to our info strings
|
||
limits = { 'No Limit':'nl', 'Pot Limit':'pl', 'Limit':'fl' }
|
||
games = { # base, category
|
||
"Hold'em" : ('hold','holdem'),
|
||
'Omaha Hi' : ('hold','omahahi'),
|
||
'Omaha H/L' : ('hold','omahahilo'),
|
||
'Razz' : ('stud','razz'),
|
||
'Stud Hi' : ('stud','studhi'),
|
||
'Stud H/L' : ('stud','studhilo'),
|
||
'2-7 Triple Draw' : ('draw','27_3draw'),
|
||
'5 Card Draw' : ('draw','fivedraw'),
|
||
'Badugi' : ('draw','badugi'),
|
||
}
|
||
currencies = { u'€':'EUR', '$':'USD', '':'T$' }
|
||
if mg['CAP']:
|
||
info['limitType'] = 'cn'
|
||
else:
|
||
info['limitType'] = limits[mg['LIMIT']]
|
||
info['sb'] = mg['SB']
|
||
info['bb'] = mg['BB']
|
||
if mg['GAME'] is not None:
|
||
(info['base'], info['category']) = games[mg['GAME']]
|
||
if mg['CURRENCY'] is not None:
|
||
info['currency'] = currencies[mg['CURRENCY']]
|
||
if mg['TOURNO'] is None: info['type'] = "ring"
|
||
else: info['type'] = "tour"
|
||
# NB: SB, BB must be interpreted as blinds or bets depending on limit type.
|
||
return info
|
||
|
||
def readHandInfo(self, hand):
|
||
m = self.re_HandInfo.search(hand.handText)
|
||
if m is None:
|
||
tmp = hand.handText[0:100]
|
||
log.error(_("readHandInfo: Unable to recognise handinfo from: '%s'") % tmp)
|
||
raise FpdbParseError(_("No match in readHandInfo."))
|
||
hand.handid = m.group('HID')
|
||
hand.tablename = m.group('TABLE')
|
||
|
||
if m.group('DATETIME'):
|
||
# This section of code should match either a single date (which is ET) or
|
||
# the last date in the header, which is also recorded in ET.
|
||
timezone = "ET"
|
||
m1 = self.re_DateTime.finditer(m.group('DATETIME'))
|
||
datetimestr = "2000/01/01 00:00:00"
|
||
for a in m1:
|
||
if a.group('TZ2') == None:
|
||
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'))
|
||
timezone = a.group('TZ')
|
||
hand.startTime = datetime.datetime.strptime(datetimestr, "%Y/%m/%d %H:%M:%S")
|
||
else: # Short-lived date format
|
||
datetimestr = "%s/%s/%s %s:%s" % (a.group('Y2'), a.group('M2'),a.group('D2'),a.group('H2'),a.group('MIN2'))
|
||
timezone = a.group('TZ2')
|
||
hand.startTime = datetime.datetime.strptime(datetimestr, "%Y/%B/%d %H:%M")
|
||
|
||
hand.startTime = HandHistoryConverter.changeTimezone(hand.startTime, timezone, "UTC")
|
||
|
||
if m.group("CANCELLED") or m.group("PARTIAL"):
|
||
raise FpdbParseError(hid=m.group('HID'))
|
||
|
||
if m.group('TABLEATTRIBUTES'):
|
||
m2 = self.re_Max.search(m.group('TABLEATTRIBUTES'))
|
||
if m2: hand.maxseats = int(m2.group('MAX'))
|
||
|
||
hand.tourNo = m.group('TOURNO')
|
||
if m.group('PLAY') is not None:
|
||
hand.gametype['currency'] = 'play'
|
||
|
||
# Done: if there's a way to figure these out, we should.. otherwise we have to stuff it with unknowns
|
||
if m.group('TOURNAMENT') is not None:
|
||
n = self.re_TourneyExtraInfo.search(m.group('TOURNAMENT'))
|
||
if n.group('UNREADABLE_INFO') is not None:
|
||
hand.tourneyComment = n.group('UNREADABLE_INFO')
|
||
else:
|
||
hand.tourneyComment = n.group('TOURNEY_NAME') # can be None
|
||
if (n.group('CURRENCY') is not None and n.group('BUYIN') is not None and n.group('FEE') is not None):
|
||
if n.group('CURRENCY')=="$":
|
||
hand.buyinCurrency="USD"
|
||
elif n.group('CURRENCY')==u"€":
|
||
hand.buyinCurrency="EUR"
|
||
else:
|
||
hand.buyinCurrency="NA"
|
||
hand.buyin = int(100*Decimal(n.group('BUYIN')))
|
||
hand.fee = int(100*Decimal(n.group('FEE')))
|
||
if n.group('TURBO') is not None :
|
||
hand.speed = "Turbo"
|
||
if n.group('SPECIAL') is not None :
|
||
special = n.group('SPECIAL')
|
||
if special == "Rebuy":
|
||
hand.isRebuy = True
|
||
if special == "KO":
|
||
hand.isKO = True
|
||
if special == "Head's Up" or special == "Heads Up":
|
||
hand.maxseats = 2
|
||
if re.search("Matrix", special):
|
||
hand.isMatrix = True
|
||
if special == "Shootout":
|
||
hand.isShootout = True
|
||
if hand.buyin is None:
|
||
hand.buyin = 0
|
||
hand.fee=0
|
||
hand.buyinCurrency="NA"
|
||
|
||
if hand.level is None:
|
||
hand.level = "0"
|
||
|
||
def readPlayerStacks(self, hand):
|
||
# Split hand text for FTP, as the regex matches the player names incorrectly
|
||
# in the summary section
|
||
pre, post = hand.handText.split('SUMMARY')
|
||
if hand.gametype['type'] == "ring" :
|
||
m = self.re_PlayerInfo.finditer(pre)
|
||
else: #if hand.gametype['type'] == "tour"
|
||
m = self.re_TourneysPlayerInfo.finditer(pre)
|
||
|
||
for a in m:
|
||
hand.addPlayer(int(a.group('SEAT')), a.group('PNAME'), a.group('CASH'))
|
||
|
||
|
||
def markStreets(self, hand):
|
||
|
||
if hand.gametype['base'] == 'hold':
|
||
m = re.search(r"\*\*\* HOLE CARDS \*\*\*(?P<PREFLOP>.+(?=\*\*\* FLOP \*\*\*)|.+)"
|
||
r"(\*\*\* FLOP \*\*\*(?P<FLOP> \[\S\S \S\S \S\S\].+(?=\*\*\* TURN \*\*\*)|.+))?"
|
||
r"(\*\*\* TURN \*\*\* \[\S\S \S\S \S\S] (?P<TURN>\[\S\S\].+(?=\*\*\* RIVER \*\*\*)|.+))?"
|
||
r"(\*\*\* RIVER \*\*\* \[\S\S \S\S \S\S \S\S] (?P<RIVER>\[\S\S\].+))?", hand.handText,re.DOTALL)
|
||
elif hand.gametype['base'] == "stud":
|
||
m = re.search(r"(?P<ANTES>.+(?=\*\*\* 3RD STREET \*\*\*)|.+)"
|
||
r"(\*\*\* 3RD STREET \*\*\*(?P<THIRD>.+(?=\*\*\* 4TH STREET \*\*\*)|.+))?"
|
||
r"(\*\*\* 4TH STREET \*\*\*(?P<FOURTH>.+(?=\*\*\* 5TH STREET \*\*\*)|.+))?"
|
||
r"(\*\*\* 5TH STREET \*\*\*(?P<FIFTH>.+(?=\*\*\* 6TH STREET \*\*\*)|.+))?"
|
||
r"(\*\*\* 6TH STREET \*\*\*(?P<SIXTH>.+(?=\*\*\* 7TH STREET \*\*\*)|.+))?"
|
||
r"(\*\*\* 7TH STREET \*\*\*(?P<SEVENTH>.+))?", hand.handText,re.DOTALL)
|
||
elif hand.gametype['base'] in ("draw"):
|
||
m = re.search(r"(?P<PREDEAL>.+(?=\*\*\* HOLE CARDS \*\*\*)|.+)"
|
||
r"(\*\*\* HOLE CARDS \*\*\*(?P<DEAL>.+(?=\*\*\* FIRST DRAW \*\*\*)|.+))?"
|
||
r"(\*\*\* FIRST DRAW \*\*\*(?P<DRAWONE>.+(?=\*\*\* SECOND DRAW \*\*\*)|.+))?"
|
||
r"(\*\*\* SECOND DRAW \*\*\*(?P<DRAWTWO>.+(?=\*\*\* THIRD DRAW \*\*\*)|.+))?"
|
||
r"(\*\*\* THIRD DRAW \*\*\*(?P<DRAWTHREE>.+))?", hand.handText,re.DOTALL)
|
||
|
||
hand.addStreets(m)
|
||
|
||
def readCommunityCards(self, hand, street): # street has been matched by markStreets, so exists in this hand
|
||
if street in ('FLOP','TURN','RIVER'): # a list of streets which get dealt community cards (i.e. all but PREFLOP)
|
||
#print "DEBUG readCommunityCards:", street, hand.streets.group(street)
|
||
m = self.re_Board.search(hand.streets[street])
|
||
hand.setCommunityCards(street, m.group('CARDS').split(' '))
|
||
|
||
|
||
def readBlinds(self, hand):
|
||
try:
|
||
m = self.re_PostSB.search(hand.handText)
|
||
hand.addBlind(m.group('PNAME'), 'small blind', m.group('SB'))
|
||
except: # no small blind
|
||
hand.addBlind(None, None, None)
|
||
for a in self.re_PostDead.finditer(hand.handText):
|
||
hand.addBlind(a.group('PNAME'), 'secondsb', a.group('SB'))
|
||
for a in self.re_PostBB.finditer(hand.handText):
|
||
hand.addBlind(a.group('PNAME'), 'big blind', a.group('BB'))
|
||
for a in self.re_PostBoth.finditer(hand.handText):
|
||
hand.addBlind(a.group('PNAME'), 'small & big blinds', a.group('SBBB'))
|
||
|
||
def readAntes(self, hand):
|
||
logging.debug(_("reading antes"))
|
||
m = self.re_Antes.finditer(hand.handText)
|
||
for player in m:
|
||
logging.debug("hand.addAnte(%s,%s)" %(player.group('PNAME'), player.group('ANTE')))
|
||
# if player.group() !=
|
||
hand.addAnte(player.group('PNAME'), player.group('ANTE'))
|
||
|
||
def readBringIn(self, hand):
|
||
m = self.re_BringIn.search(hand.handText,re.DOTALL)
|
||
if m:
|
||
logging.debug(_("Player bringing in: %s for %s") %(m.group('PNAME'), m.group('BRINGIN')))
|
||
hand.addBringIn(m.group('PNAME'), m.group('BRINGIN'))
|
||
else:
|
||
logging.warning(_("No bringin found, handid =%s") % hand.handid)
|
||
|
||
def readButton(self, hand):
|
||
hand.buttonpos = int(self.re_Button.search(hand.handText).group('BUTTON'))
|
||
|
||
def readHeroCards(self, hand):
|
||
# streets PREFLOP, PREDRAW, and THIRD are special cases beacause
|
||
# we need to grab hero's cards
|
||
for street in ('PREFLOP', 'DEAL'):
|
||
if street in hand.streets.keys():
|
||
m = self.re_HeroCards.finditer(hand.streets[street])
|
||
for found in m:
|
||
# if m == None:
|
||
# hand.involved = False
|
||
# else:
|
||
hand.hero = found.group('PNAME')
|
||
newcards = found.group('NEWCARDS').split(' ')
|
||
hand.addHoleCards(street, hand.hero, closed=newcards, shown=False, mucked=False, dealt=True)
|
||
|
||
for street, text in hand.streets.iteritems():
|
||
if not text or street in ('PREFLOP', 'DEAL'): continue # already done these
|
||
m = self.re_HeroCards.finditer(hand.streets[street])
|
||
for found in m:
|
||
player = found.group('PNAME')
|
||
if found.group('NEWCARDS') is None:
|
||
newcards = []
|
||
else:
|
||
newcards = found.group('NEWCARDS').split(' ')
|
||
if found.group('OLDCARDS') is None:
|
||
oldcards = []
|
||
else:
|
||
oldcards = found.group('OLDCARDS').split(' ')
|
||
|
||
if street == 'THIRD' and len(oldcards) == 2: # hero in stud game
|
||
hand.hero = player
|
||
hand.dealt.add(player) # need this for stud??
|
||
hand.addHoleCards(street, player, closed=oldcards, open=newcards, shown=False, mucked=False, dealt=False)
|
||
else:
|
||
hand.addHoleCards(street, player, open=newcards, closed=oldcards, shown=False, mucked=False, dealt=False)
|
||
|
||
|
||
def readAction(self, hand, street):
|
||
m = self.re_Action.finditer(hand.streets[street])
|
||
for action in m:
|
||
if action.group('ATYPE') == ' raises to':
|
||
hand.addRaiseTo( street, action.group('PNAME'), action.group('BET') )
|
||
elif action.group('ATYPE') == ' completes it to':
|
||
hand.addComplete( street, action.group('PNAME'), action.group('BET') )
|
||
elif action.group('ATYPE') == ' calls':
|
||
hand.addCall( street, action.group('PNAME'), action.group('BET') )
|
||
elif action.group('ATYPE') == ' bets':
|
||
hand.addBet( street, action.group('PNAME'), action.group('BET') )
|
||
elif action.group('ATYPE') == ' folds':
|
||
hand.addFold( street, action.group('PNAME'))
|
||
elif action.group('ATYPE') == ' checks':
|
||
hand.addCheck( street, action.group('PNAME'))
|
||
else:
|
||
print _("FullTilt: DEBUG: unimplemented readAction: '%s' '%s'") %(action.group('PNAME'),action.group('ATYPE'),)
|
||
|
||
|
||
def readShowdownActions(self, hand):
|
||
for shows in self.re_ShowdownAction.finditer(hand.handText):
|
||
cards = shows.group('CARDS')
|
||
cards = cards.split(' ')
|
||
hand.addShownCards(cards, shows.group('PNAME'))
|
||
|
||
def readCollectPot(self,hand):
|
||
for m in self.re_CollectPot.finditer(hand.handText):
|
||
hand.addCollectPot(player=m.group('PNAME'),pot=re.sub(u',',u'',m.group('POT')))
|
||
|
||
def readShownCards(self,hand):
|
||
for m in self.re_ShownCards.finditer(hand.handText):
|
||
if m.group('CARDS') is not None:
|
||
if m.group('ACT'):
|
||
hand.addShownCards(cards=m.group('CARDS').split(' '), player=m.group('PNAME'), shown = False, mucked = True)
|
||
else:
|
||
hand.addShownCards(cards=m.group('CARDS').split(' '), player=m.group('PNAME'), shown = True, mucked = False)
|
||
|
||
def guessMaxSeats(self, hand):
|
||
"""Return a guess at max_seats when not specified in HH."""
|
||
mo = self.maxOccSeat(hand)
|
||
|
||
if mo == 10: return 10 #that was easy
|
||
|
||
if hand.gametype['base'] == 'stud':
|
||
if mo <= 8: return 8
|
||
else: return mo
|
||
|
||
if hand.gametype['base'] == 'draw':
|
||
if mo <= 6: return 6
|
||
else: return mo
|
||
|
||
if mo == 2: return 2
|
||
if mo <= 6: return 6
|
||
return 9
|
||
|
||
def readOther(self, hand):
|
||
m = self.re_Mixed.search(self.in_path)
|
||
if m is None:
|
||
hand.mixed = None
|
||
else:
|
||
hand.mixed = self.mixes[m.groupdict()['MIXED']]
|
||
|
||
def readSummaryInfo(self, summaryInfoList):
|
||
self.status = True
|
||
|
||
#m = re.search("Tournament Summary", summaryInfoList[0])
|
||
#if m:
|
||
# # info list should be 2 lines : Tourney infos & Finsihing postions with winnings
|
||
# if (len(summaryInfoList) != 2 ):
|
||
# log.info("Too many or too few lines (%d) in file '%s' : '%s'" % (len(summaryInfoList), self.in_path, summaryInfoList) )
|
||
# self.status = False
|
||
# else:
|
||
# self.tourney = TourneySummary.TourneySummary(sitename = self.sitename, gametype = None, summaryText = summaryInfoList, builtFrom = "HHC")
|
||
# self.status = self.getPlayersPositionsAndWinnings(self.tourney)
|
||
# if self.status == True :
|
||
# self.status = self.determineTourneyType(self.tourney)
|
||
# #print self.tourney
|
||
# else:
|
||
# log.info("Parsing NOK : rejected")
|
||
#else:
|
||
# log.info( "This is not a summary file : '%s'" % (self.in_path) )
|
||
# self.status = False
|
||
|
||
return self.status
|
||
|
||
def determineTourneyType(self, tourney):
|
||
info = {'type':'tour'}
|
||
tourneyText = tourney.summaryText[0]
|
||
#print "Examine : '%s'" %(tourneyText)
|
||
|
||
m = self.re_TourneyInfo.search(tourneyText)
|
||
if not m:
|
||
log.info(_("determineTourneyType : Parsing NOK"))
|
||
return False
|
||
mg = m.groupdict()
|
||
#print mg
|
||
|
||
# translations from captured groups to our info strings
|
||
limits = { 'No Limit':'nl', 'Pot Limit':'pl', 'Limit':'fl' }
|
||
games = { # base, category
|
||
"Hold'em" : ('hold','holdem'),
|
||
'Omaha Hi' : ('hold','omahahi'),
|
||
'Omaha H/L' : ('hold','omahahilo'),
|
||
'Razz' : ('stud','razz'),
|
||
'Stud Hi' : ('stud','studhi'),
|
||
'Stud H/L' : ('stud','studhilo')
|
||
}
|
||
currencies = { u' €':'EUR', '$':'USD', '':'T$' }
|
||
info['limitType'] = limits[mg['LIMIT']]
|
||
if mg['GAME'] is not None:
|
||
(info['base'], info['category']) = games[mg['GAME']]
|
||
if mg['CURRENCY'] is not None:
|
||
info['currency'] = currencies[mg['CURRENCY']]
|
||
if mg['TOURNO'] is None:
|
||
info['type'] = "ring"
|
||
else:
|
||
info['type'] = "tour"
|
||
# NB: SB, BB must be interpreted as blinds or bets depending on limit type.
|
||
|
||
# Info is now ready to be copied in the tourney object
|
||
tourney.gametype = info
|
||
|
||
# Additional info can be stored in the tourney object
|
||
if mg['BUYIN'] is not None:
|
||
tourney.buyin = 100*Decimal(re.sub(u',', u'', "%s" % mg['BUYIN']))
|
||
tourney.fee = 0
|
||
if mg['FEE'] is not None:
|
||
tourney.fee = 100*Decimal(re.sub(u',', u'', "%s" % mg['FEE']))
|
||
if mg['TOURNAMENT_NAME'] is not None:
|
||
# Tournament Name can have a trailing space at the end (depending on the tournament description)
|
||
tourney.tourneyName = mg['TOURNAMENT_NAME'].rstrip()
|
||
if mg['SPECIAL'] is not None:
|
||
special = mg['SPECIAL']
|
||
if special == "KO":
|
||
tourney.isKO = True
|
||
if special == "Heads Up":
|
||
tourney.maxseats = 2
|
||
if re.search("Matrix", special):
|
||
tourney.isMatrix = True
|
||
if special == "Rebuy":
|
||
tourney.isRebuy = True
|
||
if special == "Madness":
|
||
tourney.tourneyComment = "Madness"
|
||
if mg['SHOOTOUT'] is not None:
|
||
tourney.isShootout = True
|
||
if mg['TURBO1'] is not None or mg['TURBO2'] is not None :
|
||
tourney.speed = "Turbo"
|
||
if mg['TOURNO'] is not None:
|
||
tourney.tourNo = mg['TOURNO']
|
||
else:
|
||
log.info(_("Unable to get a valid Tournament ID -- File rejected"))
|
||
return False
|
||
if tourney.isMatrix:
|
||
if mg['MATCHNO'] is not None:
|
||
tourney.matrixMatchId = mg['MATCHNO']
|
||
else:
|
||
tourney.matrixMatchId = 0
|
||
|
||
|
||
# Get BuyIn/Fee
|
||
# Try and deal with the different cases that can occur :
|
||
# - No buy-in/fee can be on the first line (freerolls, Satellites sometimes ?, ...) but appears in the rest of the description ==> use this one
|
||
# - Buy-In/Fee from the first line differs from the rest of the description :
|
||
# * OK in matrix tourneys (global buy-in dispatched between the different matches)
|
||
# * NOK otherwise ==> issue a warning and store specific data as if were a Matrix Tourney
|
||
# - If no buy-in/fee can be found : assume it's a freeroll
|
||
m = self.re_TourneyBuyInFee.search(tourneyText)
|
||
if m is not None:
|
||
mg = m.groupdict()
|
||
if tourney.isMatrix :
|
||
if mg['BUYIN'] is not None:
|
||
tourney.subTourneyBuyin = 100*Decimal(re.sub(u',', u'', "%s" % mg['BUYIN']))
|
||
tourney.subTourneyFee = 0
|
||
if mg['FEE'] is not None:
|
||
tourney.subTourneyFee = 100*Decimal(re.sub(u',', u'', "%s" % mg['FEE']))
|
||
else :
|
||
if mg['BUYIN'] is not None:
|
||
if tourney.buyin is None:
|
||
tourney.buyin = 100*Decimal(re.sub(u',', u'', "%s" % mg['BUYIN']))
|
||
else :
|
||
if 100*Decimal(re.sub(u',', u'', "%s" % mg['BUYIN'])) != tourney.buyin:
|
||
log.error(_("Conflict between buyins read in topline (%s) and in BuyIn field (%s)") % (tourney.buyin, 100*Decimal(re.sub(u',', u'', "%s" % mg['BUYIN']))) )
|
||
tourney.subTourneyBuyin = 100*Decimal(re.sub(u',', u'', "%s" % mg['BUYIN']))
|
||
if mg['FEE'] is not None:
|
||
if tourney.fee is None:
|
||
tourney.fee = 100*Decimal(re.sub(u',', u'', "%s" % mg['FEE']))
|
||
else :
|
||
if 100*Decimal(re.sub(u',', u'', "%s" % mg['FEE'])) != tourney.fee:
|
||
log.error(_("Conflict between fees read in topline (%s) and in BuyIn field (%s)") % (tourney.fee, 100*Decimal(re.sub(u',', u'', "%s" % mg['FEE']))) )
|
||
tourney.subTourneyFee = 100*Decimal(re.sub(u',', u'', "%s" % mg['FEE']))
|
||
|
||
if tourney.buyin is None:
|
||
log.info(_("Unable to affect a buyin to this tournament : assume it's a freeroll"))
|
||
tourney.buyin = 0
|
||
tourney.fee = 0
|
||
else:
|
||
if tourney.fee is None:
|
||
#print "Couldn't initialize fee, even though buyin went OK : assume there are no fees"
|
||
tourney.fee = 0
|
||
|
||
#Get single line infos
|
||
dictRegex = { "BUYINCHIPS" : self.re_TourneyBuyInChips,
|
||
"ENTRIES" : self.re_TourneyEntries,
|
||
"PRIZEPOOL" : self.re_TourneyPrizePool,
|
||
"REBUY_COST" : self.re_TourneyRebuyCost,
|
||
"ADDON_COST" : self.re_TourneyAddOnCost,
|
||
"REBUY_TOTAL" : self.re_TourneyRebuysTotal,
|
||
"ADDONS_TOTAL" : self.re_TourneyAddOnsTotal,
|
||
"REBUY_CHIPS" : self.re_TourneyRebuyChips,
|
||
"ADDON_CHIPS" : self.re_TourneyAddOnChips,
|
||
"STARTTIME" : self.re_TourneyTimeInfo,
|
||
"KO_BOUNTY_AMOUNT" : self.re_TourneyKOBounty,
|
||
}
|
||
|
||
|
||
dictHolders = { "BUYINCHIPS" : "buyInChips",
|
||
"ENTRIES" : "entries",
|
||
"PRIZEPOOL" : "prizepool",
|
||
"REBUY_COST" : "rebuyCost",
|
||
"ADDON_COST" : "addOnCost",
|
||
"REBUY_TOTAL" : "totalRebuyCount",
|
||
"ADDONS_TOTAL" : "totalAddOnCount",
|
||
"REBUY_CHIPS" : "rebuyChips",
|
||
"ADDON_CHIPS" : "addOnChips",
|
||
"STARTTIME" : "starttime",
|
||
"KO_BOUNTY_AMOUNT" : "koBounty"
|
||
}
|
||
|
||
mg = {} # After the loop, mg will contain all the matching groups, including the ones that have not been used, like ENDTIME and IN-PROGRESS
|
||
for data in dictRegex:
|
||
m = dictRegex.get(data).search(tourneyText)
|
||
if m is not None:
|
||
mg.update(m.groupdict())
|
||
setattr(tourney, dictHolders[data], mg[data])
|
||
|
||
if mg['IN_PROGRESS'] is not None or mg['ENDTIME'] is not None:
|
||
# Assign endtime to tourney (if None, that's ok, it's because the tourney wans't over over when the summary file was produced)
|
||
tourney.endtime = mg['ENDTIME']
|
||
|
||
# Deal with hero specific information
|
||
if tourney.hero is not None :
|
||
m = self.re_TourneyRebuyCount.search(tourneyText)
|
||
if m is not None:
|
||
mg = m.groupdict()
|
||
if mg['REBUY_COUNT'] is not None :
|
||
tourney.rebuyCounts.update( { tourney.hero : Decimal(mg['REBUY_COUNT']) } )
|
||
m = self.re_TourneyAddOnCount.search(tourneyText)
|
||
if m is not None:
|
||
mg = m.groupdict()
|
||
if mg['ADDON_COUNT'] is not None :
|
||
tourney.addOnCounts.update( { tourney.hero : Decimal(mg['ADDON_COUNT']) } )
|
||
m = self.re_TourneyKoCount.search(tourneyText)
|
||
if m is not None:
|
||
mg = m.groupdict()
|
||
if mg['COUNT_KO'] is not None :
|
||
tourney.koCounts.update( { tourney.hero : Decimal(mg['COUNT_KO']) } )
|
||
|
||
# Deal with money amounts
|
||
tourney.koBounty = 100*Decimal(re.sub(u',', u'', "%s" % tourney.koBounty))
|
||
tourney.prizepool = 100*Decimal(re.sub(u',', u'', "%s" % tourney.prizepool))
|
||
tourney.rebuyCost = 100*Decimal(re.sub(u',', u'', "%s" % tourney.rebuyCost))
|
||
tourney.addOnCost = 100*Decimal(re.sub(u',', u'', "%s" % tourney.addOnCost))
|
||
|
||
# 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
|
||
for p in tourney.players :
|
||
if tourney.isKO :
|
||
#tourney.incrementPlayerWinnings(tourney.players[p], Decimal(tourney.koBounty)*Decimal(tourney.koCounts[p]))
|
||
tourney.winnings[p] += Decimal(tourney.koBounty)*Decimal(tourney.koCounts[p])
|
||
#print "player %s : winnings %d" % (p, tourney.winnings[p])
|
||
|
||
#print mg
|
||
return True
|
||
#end def determineTourneyType
|
||
|
||
def getPlayersPositionsAndWinnings(self, tourney):
|
||
playersText = tourney.summaryText[1]
|
||
#print "Examine : '%s'" %(playersText)
|
||
m = self.re_TourneysPlayersSummary.finditer(playersText)
|
||
|
||
for a in m:
|
||
if a.group('PNAME') is not None and a.group('RANK') is not None:
|
||
if a.group('RANK') == "Still Playing":
|
||
rank = -1
|
||
else:
|
||
rank = Decimal(a.group('RANK'))
|
||
|
||
if a.group('WINNING') is not None:
|
||
winnings = 100*Decimal(re.sub(u',', u'', "%s" % a.group('WINNING')))
|
||
else:
|
||
winnings = "0"
|
||
|
||
tourney.addPlayer(rank, a.group('PNAME'), winnings, "USD", 0, 0, 0) #TODO: make it store actual winnings currency
|
||
else:
|
||
print (_("FullTilt: Player finishing stats unreadable : %s") % a)
|
||
|
||
# Find Hero
|
||
n = self.re_TourneyHeroFinishingP.search(playersText)
|
||
if n is not None:
|
||
heroName = n.group('HERO_NAME')
|
||
tourney.hero = heroName
|
||
# Is this really useful ?
|
||
if heroName not in tourney.ranks:
|
||
print (_("FullTilt: %s not found in tourney.ranks ...") % heroName)
|
||
elif (tourney.ranks[heroName] != Decimal(n.group('HERO_FINISHING_POS'))):
|
||
print (_("FullTilt: Bad parsing : finish position incoherent : %s / %s") % (tourney.ranks[heroName], n.group('HERO_FINISHING_POS')))
|
||
|
||
return True
|
||
|
||
if __name__ == "__main__":
|
||
parser = OptionParser()
|
||
parser.add_option("-i", "--input", dest="ipath", help=_("parse input hand history"), default="regression-test-files/fulltilt/razz/FT20090223 Danville - $0.50-$1 Ante $0.10 - Limit Razz.txt")
|
||
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 = Fulltilt(in_path = options.ipath, out_path = options.opath, follow = options.follow)
|
||
|