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/Hand.py b/pyfpdb/Hand.py index ec84707e..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, '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"):