You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
314 lines
15 KiB
314 lines
15 KiB
#!/usr/bin/env python |
|
# -*- coding: utf-8 -*- |
|
# |
|
# Copyright 2010-2011, Matthew Boss |
|
# |
|
# 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() |
|
|
|
# This code is based heavily on EverleafToFpdb.py, by Carl Gherardi |
|
# |
|
# TODO: |
|
# |
|
# -- No support for games other than NL hold 'em cash. Hand histories for other |
|
# games required |
|
# -- No support for limit hold 'em yet, though this would be easy to add |
|
# -- No support for tournaments (see also the last item below) |
|
# -- Assumes that the currency of ring games is USD |
|
# -- Only works for 'gametype="2"'. What is 'gametype'? |
|
# -- Only accepts 'realmoney="true"' |
|
# -- A hand's time-stamp does not record seconds past the minute (a |
|
# limitation of the history format) |
|
# -- No support for a bring-in or for antes (is the latter in fact unnecessary |
|
# for hold 'em on Carbon?) |
|
# -- hand.maxseats can only be guessed at |
|
# -- The last hand in a history file will often be incomplete and is therefore |
|
# rejected |
|
# -- Is behaviour currently correct when someone shows an uncalled hand? |
|
# -- Information may be lost when the hand ID is converted from the native form |
|
# xxxxxxxx-yyy(y*) to xxxxxxxxyyy(y*) (in principle this should be stored as |
|
# a string, but the database does not support this). Is there a possibility |
|
# of collision between hand IDs that ought to be distinct? |
|
# -- Cannot parse tables that run it twice (nor is this likely ever to be |
|
# possible) |
|
# -- Cannot parse hands in which someone is all in in one of the blinds. Until |
|
# this is corrected tournaments will be unparseable |
|
|
|
import sys |
|
import logging |
|
from HandHistoryConverter import * |
|
from decimal_wrapper import Decimal |
|
|
|
|
|
class Carbon(HandHistoryConverter): |
|
|
|
sitename = "Carbon" |
|
filetype = "text" |
|
codepage = "cp1252" |
|
siteID = 11 |
|
|
|
# Static regexes |
|
re_SplitHands = re.compile(r'</game>\n+(?=<game)') |
|
re_TailSplitHands = re.compile(r'(</game>)') |
|
re_GameInfo = re.compile(r'<description type="(?P<GAME>[a-zA-Z ]+)" stakes="(?P<LIMIT>[a-zA-Z ]+) \(\$(?P<SB>[.0-9]+)/\$(?P<BB>[.0-9]+)\)"/>', re.MULTILINE) |
|
re_HandInfo = re.compile(r'<game id="(?P<HID1>[0-9]+)-(?P<HID2>[0-9]+)" starttime="(?P<DATETIME>[0-9]+)" numholecards="2" gametype="2" realmoney="true" data="[0-9]+\|(?P<TABLE>[^\(]+)', re.MULTILINE) |
|
re_Button = re.compile(r'<players dealer="(?P<BUTTON>[0-9]+)">') |
|
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) |
|
re_Board = re.compile(r'<cards type="COMMUNITY" cards="(?P<CARDS>[^"]+)"', re.MULTILINE) |
|
re_EndOfHand = re.compile(r'<round id="END_OF_GAME"', re.MULTILINE) |
|
|
|
# The following are also static regexes: there is no need to call |
|
# compilePlayerRegexes (which does nothing), since players are identified |
|
# not by name but by seat number |
|
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) |
|
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) |
|
re_PostBoth = re.compile(r'<event sequence="[0-9]+" type="(RETURN_BLIND)" player="(?P<PSEAT>[0-9])" amount="(?P<SBBB>[.0-9]+)"/>', re.MULTILINE) |
|
#re_Antes = ??? |
|
#re_BringIn = ??? |
|
re_HeroCards = re.compile(r'<cards type="HOLE" cards="(?P<CARDS>.+)" player="(?P<PSEAT>[0-9])"', re.MULTILINE) |
|
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) |
|
re_ShowdownAction = re.compile(r'<cards type="SHOWN" cards="(?P<CARDS>..,..)" player="(?P<PSEAT>[0-9])"/>', re.MULTILINE) |
|
re_CollectPot = re.compile(r'<winner amount="(?P<POT>[.0-9]+)" uncalled="(true|false)" potnumber="[0-9]+" player="(?P<PSEAT>[0-9])"', re.MULTILINE) |
|
re_SitsOut = re.compile(r'<event sequence="[0-9]+" type="SIT_OUT" player="(?P<PSEAT>[0-9])"/>', re.MULTILINE) |
|
re_ShownCards = re.compile(r'<cards type="(SHOWN|MUCKED)" cards="(?P<CARDS>..,..)" player="(?P<PSEAT>[0-9])"/>', re.MULTILINE) |
|
|
|
def compilePlayerRegexs(self, hand): |
|
pass |
|
|
|
def playerNameFromSeatNo(self, seatNo, hand): |
|
# This special function is required because Carbon Poker records |
|
# actions by seat number, not by the player's name |
|
for p in hand.players: |
|
if p[0] == int(seatNo): |
|
return p[1] |
|
|
|
def readSupportedGames(self): |
|
return [["ring", "hold", "nl"], |
|
["tour", "hold", "nl"]] |
|
|
|
def determineGameType(self, handText): |
|
"""return dict with keys/values: |
|
'type' in ('ring', 'tour') |
|
'limitType' in ('nl', 'cn', 'pl', 'cp', 'fl') |
|
'base' in ('hold', 'stud', 'draw') |
|
'category' in ('holdem', 'omahahi', omahahilo', 'razz', 'studhi', 'studhilo', 'fivedraw', '27_1draw', '27_3draw', 'badugi') |
|
'hilo' in ('h','l','s') |
|
'smallBlind' int? |
|
'bigBlind' int? |
|
'smallBet' |
|
'bigBet' |
|
'currency' in ('USD', 'EUR', 'T$', <countrycode>) |
|
or None if we fail to get the info """ |
|
|
|
m = self.re_GameInfo.search(handText) |
|
if not m: |
|
# Information about the game type appears only at the beginning of |
|
# a hand history file; hence it is not supplied with the second |
|
# and subsequent hands. In these cases we use the value previously |
|
# stored. |
|
try: |
|
self.info |
|
return self.info |
|
except AttributeError: |
|
tmp = handText[0:100] |
|
log.error(_("Unable to recognise gametype from: '%s'") % tmp) |
|
log.error(_("determineGameType: Raising FpdbParseError")) |
|
raise FpdbParseError(_("Unable to recognise gametype from: '%s'") % tmp) |
|
|
|
self.info = {} |
|
mg = m.groupdict() |
|
print mg |
|
|
|
limits = { 'No Limit':'nl', 'No Limit ':'nl', 'Limit':'fl' } |
|
games = { # base, category |
|
'Holdem' : ('hold','holdem'), |
|
'Holdem Tournament' : ('hold','holdem') } |
|
|
|
if 'LIMIT' in mg: |
|
self.info['limitType'] = limits[mg['LIMIT']] |
|
if 'GAME' in mg: |
|
(self.info['base'], self.info['category']) = games[mg['GAME']] |
|
if 'SB' in mg: |
|
self.info['sb'] = mg['SB'] |
|
if 'BB' in mg: |
|
self.info['bb'] = mg['BB'] |
|
if mg['GAME'] == 'Holdem Tournament': |
|
self.info['type'] = 'tour' |
|
self.info['currency'] = 'T$' |
|
else: |
|
self.info['type'] = 'ring' |
|
self.info['currency'] = 'USD' |
|
|
|
return self.info |
|
|
|
def readHandInfo(self, hand): |
|
m = self.re_HandInfo.search(hand.handText) |
|
if m is None: |
|
logging.info(_("Didn't match re_HandInfo")) |
|
logging.info(hand.handText) |
|
raise FpdbParseError(_("No match in readHandInfo.")) |
|
logging.debug("HID %s-%s, Table %s" % (m.group('HID1'), |
|
m.group('HID2'), m.group('TABLE')[:-1])) |
|
hand.handid = m.group('HID1') + m.group('HID2') |
|
hand.tablename = m.group('TABLE')[:-1] |
|
hand.maxseats = 2 # This value may be increased as necessary |
|
hand.startTime = datetime.datetime.strptime(m.group('DATETIME')[:12], |
|
'%Y%m%d%H%M') |
|
# Check that the hand is complete up to the awarding of the pot; if |
|
# not, the hand is unparseable |
|
if self.re_EndOfHand.search(hand.handText) is None: |
|
raise FpdbParseError("readHandInfo failed: HID: '%s' HID2: '%s'" %(m.group('HID1'), m.group('HID2'))) |
|
|
|
def readPlayerStacks(self, hand): |
|
m = self.re_PlayerInfo.finditer(hand.handText) |
|
for a in m: |
|
seatno = int(a.group('SEAT')) |
|
# It may be necessary to adjust 'hand.maxseats', which is an |
|
# educated guess, starting with 2 (indicating a heads-up table) and |
|
# adjusted upwards in steps to 6, then 9, then 10. An adjustment is |
|
# made whenever a player is discovered whose seat number is |
|
# currently above the maximum allowable for the table. |
|
if seatno >= hand.maxseats: |
|
if seatno > 8: |
|
hand.maxseats = 10 |
|
elif seatno > 5: |
|
hand.maxseats = 9 |
|
else: |
|
hand.maxseats = 6 |
|
if a.group('DEALTIN') == "true": |
|
hand.addPlayer(seatno, a.group('PNAME'), a.group('CASH')) |
|
|
|
def markStreets(self, hand): |
|
#if hand.gametype['base'] == 'hold': |
|
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) |
|
hand.addStreets(m) |
|
|
|
def readCommunityCards(self, hand, street): |
|
m = self.re_Board.search(hand.streets[street]) |
|
if street == 'FLOP': |
|
hand.setCommunityCards(street, m.group('CARDS').split(',')) |
|
elif street in ('TURN','RIVER'): |
|
hand.setCommunityCards(street, [m.group('CARDS').split(',')[-1]]) |
|
|
|
def readAntes(self, hand): |
|
pass # ??? |
|
|
|
def readBringIn(self, hand): |
|
pass # ??? |
|
|
|
def readBlinds(self, hand): |
|
for a in self.re_PostSB.finditer(hand.handText): |
|
#print "DEBUG: found sb: '%s' '%s'" %(self.playerNameFromSeatNo(a.group('PSEAT'), hand), a.group('SB')) |
|
hand.addBlind(self.playerNameFromSeatNo(a.group('PSEAT'), hand),'small blind', a.group('SB')) |
|
|
|
for a in self.re_PostBB.finditer(hand.handText): |
|
#print "DEBUG: found bb: '%s' '%s'" %(self.playerNameFromSeatNo(a.group('PSEAT'), hand), a.group('BB')) |
|
hand.addBlind(self.playerNameFromSeatNo(a.group('PSEAT'), hand), 'big blind', a.group('BB')) |
|
for a in self.re_PostBoth.finditer(hand.handText): |
|
bb = Decimal(self.info['bb']) |
|
amount = Decimal(a.group('SBBB')) |
|
if amount < bb: |
|
hand.addBlind(self.playerNameFromSeatNo(a.group('PSEAT'), |
|
hand), 'small blind', a.group('SBBB')) |
|
elif amount == bb: |
|
hand.addBlind(self.playerNameFromSeatNo(a.group('PSEAT'), |
|
hand), 'big blind', a.group('SBBB')) |
|
else: |
|
hand.addBlind(self.playerNameFromSeatNo(a.group('PSEAT'), |
|
hand), 'both', a.group('SBBB')) |
|
|
|
def readButton(self, hand): |
|
hand.buttonpos = int(self.re_Button.search(hand.handText).group('BUTTON')) |
|
|
|
def readHeroCards(self, hand): |
|
m = self.re_HeroCards.search(hand.handText) |
|
if m: |
|
hand.hero = self.playerNameFromSeatNo(m.group('PSEAT'), hand) |
|
cards = m.group('CARDS').split(',') |
|
hand.addHoleCards('PREFLOP', hand.hero, closed=cards, shown=False, |
|
mucked=False, dealt=True) |
|
|
|
def readAction(self, hand, street): |
|
logging.debug("readAction (%s)" % street) |
|
m = self.re_Action.finditer(hand.streets[street]) |
|
for action in m: |
|
logging.debug("%s %s" % (action.group('ATYPE'), |
|
action.groupdict())) |
|
player = self.playerNameFromSeatNo(action.group('PSEAT'), hand) |
|
if action.group('ATYPE') == 'RAISE': |
|
hand.addCallandRaise(street, player, action.group('BET')) |
|
elif action.group('ATYPE') == 'CALL': |
|
hand.addCall(street, player, action.group('BET')) |
|
elif action.group('ATYPE') == 'BET': |
|
hand.addBet(street, player, action.group('BET')) |
|
elif action.group('ATYPE') in ('FOLD', 'SIT_OUT'): |
|
hand.addFold(street, player) |
|
elif action.group('ATYPE') == 'CHECK': |
|
hand.addCheck(street, player) |
|
elif action.group('ATYPE') == 'ALL_IN': |
|
hand.addAllIn(street, player, action.group('BET')) |
|
else: |
|
logging.debug(_("Unimplemented readAction: %s %s") |
|
% (action.group('PSEAT'),action.group('ATYPE'))) |
|
|
|
def readShowdownActions(self, hand): |
|
for shows in self.re_ShowdownAction.finditer(hand.handText): |
|
cards = shows.group('CARDS').split(',') |
|
hand.addShownCards(cards, |
|
self.playerNameFromSeatNo(shows.group('PSEAT'), |
|
hand)) |
|
|
|
def readCollectPot(self, hand): |
|
pots = [Decimal(0) for n in range(hand.maxseats)] |
|
for m in self.re_CollectPot.finditer(hand.handText): |
|
pots[int(m.group('PSEAT'))] += Decimal(m.group('POT')) |
|
# Regarding the processing logic for "committed", see Pot.end() in |
|
# Hand.py |
|
committed = sorted([(v,k) for (k,v) in hand.pot.committed.items()]) |
|
for p in range(hand.maxseats): |
|
pname = self.playerNameFromSeatNo(p, hand) |
|
if committed[-1][1] == pname: |
|
pots[p] -= committed[-1][0] - committed[-2][0] |
|
if pots[p] > 0: |
|
hand.addCollectPot(player=pname, pot=pots[p]) |
|
|
|
def readShownCards(self, hand): |
|
for m in self.re_ShownCards.finditer(hand.handText): |
|
cards = m.group('CARDS').split(',') |
|
hand.addShownCards(cards=cards, player=self.playerNameFromSeatNo(m.group('PSEAT'), hand)) |
|
|
|
if __name__ == "__main__": |
|
parser = OptionParser() |
|
parser.add_option("-i", "--input", dest="ipath", help=_("parse input hand history"), default="-") |
|
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() |
|
|
|
LOG_FILENAME = './logging.out' |
|
logging.basicConfig(filename=LOG_FILENAME, level=options.verbosity) |
|
|
|
e = Carbon(in_path = options.ipath, |
|
out_path = options.opath, |
|
follow = options.follow, |
|
autostart = True) |
|
|
|
|