CarbonToFpdb.py - Converter for Carbon poker

Currently only supports NLHE cash games. Further hand hstories required.

Plugin contributed by Matthew Boss.
This commit is contained in:
Worros 2010-01-28 21:36:02 +08:00
parent 7c90102677
commit ad75bedbf1
3 changed files with 272 additions and 77 deletions

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# Copyright 2008, Carl Gherardi # -*- coding: utf-8 -*-
#
# Copyright 2010, Matthew Boss
# #
# This program is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@ -18,93 +19,286 @@
######################################################################## ########################################################################
# Standard Library modules # This code is based heavily on EverleafToFpdb.py, by Carl Gherardi
import Configuration #
import traceback # OUTSTANDING MATTERS
#
# -- No siteID assigned
# -- 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 sys
import re import logging
import xml.dom.minidom from HandHistoryConverter import *
from xml.dom.minidom import Node from decimal import Decimal
from HandHistoryConverter import HandHistoryConverter
# Carbon format looks like: class Carbon(HandHistoryConverter):
# 1) <description type="Holdem" stakes="No Limit ($0.25/$0.50)"/> sitename = "Carbon"
# 2) <game id="14902583-5578" starttime="20081006145401" numholecards="2" gametype="2" realmoney="true" data="20081006|Niagara Falls (14902583)|14902583|14902583-5578|false"> filetype = "text"
# 3) <players dealer="8"> codepage = "cp1252"
# <player seat="3" nickname="PlayerInSeat3" balance="$43.29" dealtin="true" /> siteID = 11
# ...
# 4) <round id="BLINDS" sequence="1">
# <event sequence="1" type="SMALL_BLIND" player="0" amount="0.25"/>
# <event sequence="2" type="BIG_BLIND" player="1" amount="0.50"/>
# 5) <round id="PREFLOP" sequence="2">
# <event sequence="3" type="CALL" player="2" amount="0.50"/>
# 6) <round id="POSTFLOP" sequence="3">
# <event sequence="16" type="BET" player="3" amount="1.00"/>
# ....
# <cards type="COMMUNITY" cards="7d,Jd,Jh"/>
# The full sequence for a NHLE cash game is: # Static regexes
# BLINDS, PREFLOP, POSTFLOP, POSTTURN, POSTRIVER, SHOWDOWN, END_OF_GAME re_SplitHands = re.compile(r'</game>\n+(?=<game)')
# This sequence can be terminated after BLINDS at any time by END_OF_FOLDED_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)" 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)" 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)" 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)
class CarbonPoker(HandHistoryConverter): def compilePlayerRegexs(self, hand):
def __init__(self, config, filename): pass
print "Initialising Carbon Poker converter class"
HandHistoryConverter.__init__(self, config, filename, "Carbon") # Call super class init def playerNameFromSeatNo(self, seatNo, hand):
self.setFileType("xml") # This special function is required because Carbon Poker records
self.siteId = 4 # Needs to match id entry in Sites database # 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): def readSupportedGames(self):
pass return [["ring", "hold", "nl"],
def determineGameType(self): ["tour", "hold", "nl"]]
gametype = []
desc_node = self.doc.getElementsByTagName("description") def determineGameType(self, handText):
#TODO: no examples of non ring type yet """return dict with keys/values:
gametype = gametype + ["ring"] 'type' in ('ring', 'tour')
type = desc_node[0].getAttribute("type") 'limitType' in ('nl', 'cn', 'pl', 'cp', 'fl')
if(type == "Holdem"): 'base' in ('hold', 'stud', 'draw')
gametype = gametype + ["hold"] '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.
return self.info
self.info = {}
mg = m.groupdict()
limits = { '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: else:
print "Carbon: Unknown gametype: '%s'" % (type) self.info['type'] = 'ring'
self.info['currency'] = 'USD'
stakes = desc_node[0].getAttribute("stakes") return self.info
#TODO: no examples of anything except nlhe
m = re.match('(?P<LIMIT>No Limit)\s\(\$?(?P<SB>[.0-9]+)/\$?(?P<BB>[.0-9]+)\)', stakes)
if(m.group('LIMIT') == "No Limit"): def readHandInfo(self, hand):
gametype = gametype + ["nl"] m = self.re_HandInfo.search(hand.handText)
if m is None:
logging.info("Didn't match re_HandInfo")
logging.info(hand.handText)
return None
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(hid=m.group('HID1') + "-" + m.group('HID2'))
gametype = gametype + [self.float2int(m.group('SB'))] def readPlayerStacks(self, hand):
gametype = gametype + [self.float2int(m.group('BB'))] 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'))
return gametype 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 readPlayerStacks(self): def readCommunityCards(self, hand, street):
pass m = self.re_Board.search(hand.streets[street])
def readBlinds(self): if street == 'FLOP':
pass hand.setCommunityCards(street, m.group('CARDS').split(','))
def readAction(self): elif street in ('TURN','RIVER'):
pass hand.setCommunityCards(street, [m.group('CARDS').split(',')[-1]])
# Override read function as xml.minidom barfs on the Carbon layout def readAntes(self, hand):
# This is pretty dodgy pass # ???
def readFile(self, filename):
print "Carbon: Reading file: '%s'" %(filename) def readBringIn(self, hand):
infile=open(filename, "rU") pass # ???
self.obs = infile.read()
infile.close() def readBlinds(self, hand):
self.obs = "<CarbonHHFile>\n" + self.obs + "</CarbonHHFile>"
try: try:
doc = xml.dom.minidom.parseString(self.obs) m = self.re_PostSB.search(hand.handText)
self.doc = doc hand.addBlind(self.playerNameFromSeatNo(m.group('PSEAT'), hand),
except: 'small blind', m.group('SB'))
traceback.print_exc(file=sys.stderr) except: # no small blind
hand.addBlind(None, None, None)
for a in self.re_PostBB.finditer(hand.handText):
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__": if __name__ == "__main__":
c = Configuration.Config() parser = OptionParser()
e = CarbonPoker(c, "regression-test-files/carbon-poker/Niagara Falls (15245216).xml") parser.add_option("-i", "--input", dest="ipath", help="parse input hand history", default="-")
e.processFile() parser.add_option("-o", "--output", dest="opath", help="output translation to", default="-")
print str(e) 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)

View File

@ -1311,6 +1311,7 @@ class Database:
c.execute("INSERT INTO Sites (name,currency) VALUES ('Absolute', 'USD')") c.execute("INSERT INTO Sites (name,currency) VALUES ('Absolute', 'USD')")
c.execute("INSERT INTO Sites (name,currency) VALUES ('PartyPoker', 'USD')") c.execute("INSERT INTO Sites (name,currency) VALUES ('PartyPoker', 'USD')")
c.execute("INSERT INTO Sites (name,currency) VALUES ('Partouche', 'EUR')") c.execute("INSERT INTO Sites (name,currency) VALUES ('Partouche', 'EUR')")
c.execute("INSERT INTO Sites (name,currency) VALUES ('Carbon', 'USD')")
if self.backend == self.SQLITE: if self.backend == self.SQLITE:
c.execute("INSERT INTO TourneyTypes (id, siteId, buyin, fee) VALUES (NULL, 1, 0, 0);") c.execute("INSERT INTO TourneyTypes (id, siteId, buyin, fee) VALUES (NULL, 1, 0, 0);")
elif self.backend == self.PGSQL: elif self.backend == self.PGSQL:

View File

@ -43,7 +43,7 @@ class Hand(object):
LCS = {'H':'h', 'D':'d', 'C':'c', 'S':'s'} LCS = {'H':'h', 'D':'d', 'C':'c', 'S':'s'}
SYMBOL = {'USD': '$', 'EUR': u'$', 'T$': '', 'play': ''} SYMBOL = {'USD': '$', 'EUR': u'$', 'T$': '', 'play': ''}
MS = {'horse' : 'HORSE', '8game' : '8-Game', 'hose' : 'HOSE', 'ha': 'HA'} MS = {'horse' : 'HORSE', '8game' : '8-Game', 'hose' : 'HOSE', 'ha': 'HA'}
SITEIDS = {'Fulltilt':1, 'PokerStars':2, 'Everleaf':3, 'Win2day':4, 'OnGame':5, 'UltimateBet':6, 'Betfair':7, 'Absolute':8, 'PartyPoker':9, 'Carbon':10 } SITEIDS = {'Fulltilt':1, 'PokerStars':2, 'Everleaf':3, 'Win2day':4, 'OnGame':5, 'UltimateBet':6, 'Betfair':7, 'Absolute':8, 'PartyPoker':9, 'Partouche':10, 'Carbon':11 }
def __init__(self, sitename, gametype, handText, builtFrom = "HHC"): def __init__(self, sitename, gametype, handText, builtFrom = "HHC"):