diff --git a/pyfpdb/CarbonToFpdb.py b/pyfpdb/CarbonToFpdb.py
index cc7afb81..0640d157 100644
--- a/pyfpdb/CarbonToFpdb.py
+++ b/pyfpdb/CarbonToFpdb.py
@@ -1,6 +1,7 @@
#!/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
# it under the terms of the GNU General Public License as published by
@@ -18,93 +19,286 @@
########################################################################
-# Standard Library modules
-import Configuration
-import traceback
+# This code is based heavily on EverleafToFpdb.py, by Carl Gherardi
+#
+# 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 re
-import xml.dom.minidom
-from xml.dom.minidom import Node
-from HandHistoryConverter import HandHistoryConverter
+import logging
+from HandHistoryConverter import *
+from decimal import Decimal
-# Carbon format looks like:
+class Carbon(HandHistoryConverter):
-# 1)
-# 2)
-# 3)
-#
-# ...
-# 4)
-#
-#
-# 5)
-#
-# 6)
-#
-# ....
-#
+ sitename = "Carbon"
+ filetype = "text"
+ codepage = "cp1252"
+ siteID = 11
-# The full sequence for a NHLE cash game is:
-# BLINDS, PREFLOP, POSTFLOP, POSTTURN, POSTRIVER, SHOWDOWN, END_OF_GAME
-# This sequence can be terminated after BLINDS at any time by END_OF_FOLDED_GAME
+ # Static regexes
+ re_SplitHands = re.compile(r'\n+(?=)')
+ re_GameInfo = re.compile(r'', re.MULTILINE)
+ re_HandInfo = re.compile(r'[0-9]+)">')
+ re_PlayerInfo = re.compile(r'', re.MULTILINE)
+ re_Board = re.compile(r'', re.MULTILINE)
+ re_PostBB = re.compile(r'', re.MULTILINE)
+ re_PostBoth = re.compile(r'', re.MULTILINE)
+ #re_Antes = ???
+ #re_BringIn = ???
+ re_HeroCards = re.compile(r'', re.MULTILINE)
+ re_ShowdownAction = re.compile(r'', re.MULTILINE)
+ re_CollectPot = re.compile(r'', re.MULTILINE)
+ re_ShownCards = re.compile(r'', re.MULTILINE)
-class CarbonPoker(HandHistoryConverter):
- def __init__(self, config, filename):
- print "Initialising Carbon Poker converter class"
- HandHistoryConverter.__init__(self, config, filename, "Carbon") # Call super class init
- self.setFileType("xml")
- self.siteId = 4 # Needs to match id entry in Sites database
+ def compilePlayerRegexs(self, hand):
+ pass
- def readSupportedGames(self):
- pass
- def determineGameType(self):
- gametype = []
- desc_node = self.doc.getElementsByTagName("description")
- #TODO: no examples of non ring type yet
- gametype = gametype + ["ring"]
- type = desc_node[0].getAttribute("type")
- if(type == "Holdem"):
- gametype = gametype + ["hold"]
- else:
- print "Carbon: Unknown gametype: '%s'" % (type)
+ 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]
- stakes = desc_node[0].getAttribute("stakes")
- #TODO: no examples of anything except nlhe
- m = re.match('(?PNo Limit)\s\(\$?(?P[.0-9]+)/\$?(?P[.0-9]+)\)', stakes)
+ def readSupportedGames(self):
+ return [["ring", "hold", "nl"],
+ ["tour", "hold", "nl"]]
- if(m.group('LIMIT') == "No Limit"):
- gametype = gametype + ["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$', )
+or None if we fail to get the info """
- gametype = gametype + [self.float2int(m.group('SB'))]
- gametype = gametype + [self.float2int(m.group('BB'))]
+ 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()
- return gametype
+ limits = { 'No Limit':'nl', 'Limit':'fl' }
+ games = { # base, category
+ 'Holdem' : ('hold','holdem'),
+ 'Holdem Tournament' : ('hold','holdem') }
- def readPlayerStacks(self):
- pass
- def readBlinds(self):
- pass
- def readAction(self):
- pass
+ 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'
- # Override read function as xml.minidom barfs on the Carbon layout
- # This is pretty dodgy
- def readFile(self, filename):
- print "Carbon: Reading file: '%s'" %(filename)
- infile=open(filename, "rU")
- self.obs = infile.read()
- infile.close()
- self.obs = "\n" + self.obs + ""
- try:
- doc = xml.dom.minidom.parseString(self.obs)
- self.doc = doc
- except:
- traceback.print_exc(file=sys.stderr)
+ 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)
+ 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'))
+
+ 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'(?P.+(?=(?P.+(?=(?P.+(?=(?P.+))?', 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):
+ try:
+ m = self.re_PostSB.search(hand.handText)
+ hand.addBlind(self.playerNameFromSeatNo(m.group('PSEAT'), hand),
+ 'small blind', m.group('SB'))
+ 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__":
- c = Configuration.Config()
- e = CarbonPoker(c, "regression-test-files/carbon-poker/Niagara Falls (15245216).xml")
- e.processFile()
- print str(e)
+ 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)
diff --git a/pyfpdb/Database.py b/pyfpdb/Database.py
index 20807583..5cd25d9d 100644
--- a/pyfpdb/Database.py
+++ b/pyfpdb/Database.py
@@ -1311,6 +1311,7 @@ class Database:
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 ('Partouche', 'EUR')")
+ c.execute("INSERT INTO Sites (name,currency) VALUES ('Carbon', 'USD')")
if self.backend == self.SQLITE:
c.execute("INSERT INTO TourneyTypes (id, siteId, buyin, fee) VALUES (NULL, 1, 0, 0);")
elif self.backend == self.PGSQL:
diff --git a/pyfpdb/FulltiltToFpdb.py b/pyfpdb/FulltiltToFpdb.py
index d5499636..2ee8bdd5 100755
--- a/pyfpdb/FulltiltToFpdb.py
+++ b/pyfpdb/FulltiltToFpdb.py
@@ -257,10 +257,13 @@ class Fulltilt(HandHistoryConverter):
##int(m.group('HR')), int(m.group('MIN')), int(m.group('SEC')))
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(hand.handText)
+ m = self.re_PlayerInfo.finditer(pre)
else: #if hand.gametype['type'] == "tour"
- m = self.re_TourneyPlayerInfo.finditer(hand.handText)
+ m = self.re_TourneyPlayerInfo.finditer(pre)
for a in m:
hand.addPlayer(int(a.group('SEAT')), a.group('PNAME'), a.group('CASH'))
diff --git a/pyfpdb/HUD_config.xml.example b/pyfpdb/HUD_config.xml.example
index 8b53e609..5a72c4c6 100644
--- a/pyfpdb/HUD_config.xml.example
+++ b/pyfpdb/HUD_config.xml.example
@@ -445,6 +445,43 @@ Left-Drag to Move"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -585,6 +622,7 @@ Left-Drag to Move"
+
diff --git a/pyfpdb/Hand.py b/pyfpdb/Hand.py
index 7023d50c..93661d70 100644
--- a/pyfpdb/Hand.py
+++ b/pyfpdb/Hand.py
@@ -43,7 +43,7 @@ class Hand(object):
LCS = {'H':'h', 'D':'d', 'C':'c', 'S':'s'}
SYMBOL = {'USD': '$', 'EUR': u'$', 'T$': '', 'play': ''}
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 }
+ 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"):
@@ -289,6 +289,24 @@ If a player has None chips he won't be added."""
c = c.replace(k,v)
return c
+ def addAllIn(self, street, player, amount):
+ """\
+For sites (currently only Carbon Poker) which record "all in" as a special action, which can mean either "calls and is all in" or "raises all in".
+"""
+ self.checkPlayerExists(player)
+ amount = re.sub(u',', u'', amount) #some sites have commas
+ Ai = Decimal(amount)
+ Bp = self.lastBet[street]
+ Bc = reduce(operator.add, self.bets[street][player], 0)
+ C = Bp - Bc
+ if Ai <= C:
+ self.addCall(street, player, amount)
+ elif Bp == 0:
+ self.addBet(street, player, amount)
+ else:
+ Rb = Ai - C
+ self._addRaise(street, player, C, Rb, Ai)
+
def addAnte(self, player, ante):
log.debug("%s %s antes %s" % ('BLINDSANTES', player, ante))
if player is not None: