diff --git a/pyfpdb/FulltiltToFpdb.py b/pyfpdb/FulltiltToFpdb.py index d85675fd..f814feda 100755 --- a/pyfpdb/FulltiltToFpdb.py +++ b/pyfpdb/FulltiltToFpdb.py @@ -47,7 +47,7 @@ class Fulltilt(HandHistoryConverter): re_TailSplitHands = re.compile(r"(\n\n+)") re_HandInfo = re.compile(r'''.*\#(?P[0-9]+):\s (?:(?P.+)\s\((?P\d+)\),\s)? - Table\s + (Table|Match)\s (?PPlay\sChip\s|PC)? (?P[-\s\da-zA-Z]+)\s (\((?P.+)\)\s)?-\s @@ -61,6 +61,46 @@ class Fulltilt(HandHistoryConverter): re_TourneyPlayerInfo = re.compile('Seat (?P[0-9]+): (?P.*) \(\$?(?P[,.0-9]+)\)', re.MULTILINE) re_Board = re.compile(r"\[(?P.+)\]") + #static regex for tourney purpose + re_TourneyInfo = re.compile('''Tournament\sSummary\s + (?P[^$(]+)?\s* + ((?P\$|)?(?P[.0-9]+)\s*\+\s*\$?(?P[.0-9]+)\s)? + ((?P(KO|Heads\sUp|Matrix\s\dx|Rebuy))\s)? + ((?PShootout)\s)? + ((?PSit\s&\sGo)\s)? + (\((?PTurbo)\)\s)? + \((?P\d+)\)\s + ((?PMatch\s\d)\s)? + (?P(Hold\'em|Omaha\sHi|Omaha\sH/L|7\sCard\sStud|Stud\sH/L|Razz|Stud\sHi))\s + (\((?PTurbo)\)\s)? + (?P(No\sLimit|Pot\sLimit|Limit))? + ''', re.VERBOSE) + re_TourneyBuyInFee = re.compile("Buy-In: (?P\$|)?(?P[.0-9]+) \+ \$?(?P[.0-9]+)") + re_TourneyBuyInChips = re.compile("Buy-In Chips: (?P\d+)") + re_TourneyEntries = re.compile("(?P\d+) Entries") + re_TourneyPrizePool = re.compile("Total Prize Pool: (?P\$|)?(?P[.,0-9]+)") + re_TourneyRebuyAmount = re.compile("Rebuy: (?P\$|)?(?P[.,0-9]+)") + re_TourneyAddOnAmount = re.compile("Add-On: (?P\$|)?(?P[.,0-9]+)") + re_TourneyRebuyCount = re.compile("performed (?P\d+) Rebuy") + re_TourneyAddOnCount = re.compile("performed (?P\d+) Add-On") + re_TourneyRebuysTotal = re.compile("Total Rebuys: (?P\d+)") + re_TourneyAddOnsTotal = re.compile("Total Add-Ons: (?P\d+)") + re_TourneyRebuyChips = re.compile("Rebuy Chips: (?P\d+)") + re_TourneyAddOnChips = re.compile("Add-On Chips: (?P\d+)") + re_TourneyKOBounty = re.compile("Knockout Bounty: (?P\$|)?(?P[.,0-9]+)") + re_TourneyCountKO = re.compile("received (?P\d+) Knockout Bounty Award(s)?") + re_TourneyTimeInfo = re.compile("Tournament started: (?P.*)\nTournament ((?Pis still in progress)?|(finished:(?P.*))?)$") + + re_TourneyPlayersSummary = re.compile("^(?P(Still Playing|\d+))( - |: )(?P[^\n,]+)(, )?(?P\$|)?(?P[.\d]+)?", re.MULTILINE) + re_TourneyHeroFinishingP = re.compile("(?P.*) finished in (?P\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(?PHA|HORSE|HOSE)\s\-\s', re.VERBOSE) re_Max = re.compile("(?P\d+)( max)?", re.MULTILINE) @@ -371,6 +411,225 @@ class Fulltilt(HandHistoryConverter): else: hand.mixed = self.mixes[m.groupdict()['MIXED']] + def readSummaryInfo(self, summaryInfoList): + starttime = time.time() + 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 lines (%d) in file '%s' : '%s'" % (len(summaryInfoList), self.in_path, summaryInfoList) ) + self.status = False + else: + self.tourney = Tourney.Tourney(sitename = self.sitename, gametype = None, summaryText = summaryInfoList, builtFrom = "HHC") + self.status = self.determineTourneyType(self.tourney) + if self.status == True : + self.status = status = self.getPlayersPositionsAndWinnings(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'] == 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 = mg['BUYIN'] + tourney.fee = 0 + if mg['FEE'] is not None: + tourney.fee = 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.isHU = True + tourney.maxseats = 2 + if re.search("Matrix", special): + tourney.isMatrix = True + if special == "Rebuy": + tourney.isRebuy = True + 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 = mg['BUYIN'] + tourney.subTourneyFee = 0 + if mg['FEE'] is not None: + tourney.subTourneyFee = mg['FEE'] + else : + if mg['BUYIN'] is not None: + if tourney.buyin is None: + tourney.buyin = mg['BUYIN'] + else : + if mg['BUYIN'] != tourney.buyin: + log.error( "Conflict between buyins read in topline (%s) and in BuyIn field (%s)" % (touney.buyin, mg['BUYIN']) ) + tourney.subTourneyBuyin = mg['BUYIN'] + if mg['FEE'] is not None: + if tourney.fee is None: + tourney.fee = mg['FEE'] + else : + if mg['FEE'] != tourney.fee: + log.error( "Conflict between fees read in topline (%s) and in BuyIn field (%s)" % (touney.fee, mg['FEE']) ) + tourney.subTourneyFee = 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_AMOUNT" : self.re_TourneyRebuyAmount, + "ADDON_AMOUNT" : self.re_TourneyAddOnAmount, + "REBUY_COUNT" : self.re_TourneyRebuyCount, + "ADDON_COUNT" : self.re_TourneyAddOnCount, + "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, + "COUNT_KO" : self.re_TourneyCountKO + } + + + dictHolders = { "BUYINCHIPS" : "buyInChips", + "ENTRIES" : "entries", + "PRIZEPOOL" : "prizepool", + "REBUY_AMOUNT" : "rebuyAmount", + "ADDON_AMOUNT" : "addOnAmount", + "REBUY_COUNT" : "countRebuys", + "ADDON_COUNT" : "countAddOns", + "REBUY_TOTAL" : "totalRebuys", + "ADDONS_TOTAL" : "totalAddOns", + "REBUY_CHIPS" : "rebuyChips", + "ADDON_CHIPS" : "addOnChips", + "STARTTIME" : "starttime", + "KO_BOUNTY_AMOUNT" : "koBounty", + "COUNT_KO" : "countKO" + } + + 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'] + #print mg + + return True + + def getPlayersPositionsAndWinnings(self, tourney): + playersText = tourney.summaryText[1] + #print "Examine : '%s'" %(playersText) + m = self.re_TourneyPlayersSummary.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 = a.group('WINNING') + else: + winnings = "0" + + tourney.addPlayer(rank, a.group('PNAME'), winnings) + else: + print "Player finishing stats unreadable : %s" % a + + # Deal with KO tournaments for hero winnings calculation + n = self.re_TourneyHeroFinishingP.search(playersText) + if n is not None: + heroName = n.group('HERO_NAME') + tourney.hero = heroName + # Is this really useful ? + if (tourney.finishPositions[heroName] != Decimal(n.group('HERO_FINISHING_POS'))): + print "Bad parsing : finish position incoherent : %s / %s" % (tourney.finishPositions[heroName], n.group('HERO_FINISHING_POS')) + if tourney.isKO: + #Update the winnings with the (KO amount) * (# of KO) + tourney.incrementPlayerWinnings(n.group('HERO_NAME'), Decimal(tourney.koBounty)*Decimal(tourney.countKO)) + + 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") @@ -386,3 +645,6 @@ if __name__ == "__main__": (options, args) = parser.parse_args() e = Fulltilt(in_path = options.ipath, out_path = options.opath, follow = options.follow) + + + diff --git a/pyfpdb/HandHistoryConverter.py b/pyfpdb/HandHistoryConverter.py index d2d6c1b5..4a9821f8 100644 --- a/pyfpdb/HandHistoryConverter.py +++ b/pyfpdb/HandHistoryConverter.py @@ -17,6 +17,7 @@ #agpl-3.0.txt in the docs folder of the package. import Hand +import Tourney import re import sys import traceback @@ -53,6 +54,7 @@ class HandHistoryConverter(): # "utf_8" is more likely if there are funny characters codepage = "cp1252" + def __init__(self, in_path = '-', out_path = '-', follow=False, index=0, autostart=True): """\ in_path (default '-' = sys.stdin) @@ -67,6 +69,9 @@ follow : whether to tail -f the input""" self.out_path = out_path self.processedHands = [] + + # Tourney object used to store TourneyInfo when called to deal with a Summary file + self.tourney = None if in_path == '-': self.in_fh = sys.stdin @@ -88,6 +93,10 @@ follow : whether to tail -f the input""" self.follow = follow self.compiledPlayers = set() self.maxseats = 10 + + self.status = True + + self.parsedObjectType = "HH" #default behaviour : parsing HH files, can be "Summary" if the parsing encounters a Summary File if autostart: self.start() @@ -116,6 +125,7 @@ Otherwise, finish at EOF. numHands = 0 numErrors = 0 if self.follow: + #TODO: See how summary files can be handled on the fly (here they should be rejected as before) log.info("Tailing '%s'" % self.in_path) for handText in self.tailHands(): try: @@ -128,16 +138,28 @@ Otherwise, finish at EOF. else: handsList = self.allHandsAsList() log.info("Parsing %d hands" % len(handsList)) - for handText in handsList: - try: - self.processedHands.append(self.processHand(handText)) - except FpdbParseError, e: - numErrors+=1 - log.warning("Failed to convert hand %s" % e.hid) - log.debug(handText) - numHands = len(handsList) - endtime = time.time() - log.info("Read %d hands (%d failed) in %.3f seconds" % (numHands, numErrors, endtime - starttime)) + # Determine if we're dealing with a HH file or a Summary file + if self.isSummary(handsList[0]) == False: + self.parsedObjectType = "HH" + for handText in handsList: + try: + self.processedHands.append(self.processHand(handText)) + except FpdbParseError, e: + numErrors+=1 + log.warning("Failed to convert hand %s" % e.hid) + log.debug(handText) + numHands = len(handsList) + endtime = time.time() + log.info("Read %d hands (%d failed) in %.3f seconds" % (numHands, numErrors, endtime - starttime)) + else: + self.parsedObjectType = "Summary" + summaryParsingStatus = self.readSummaryInfo(handsList) + endtime = time.time() + if summaryParsingStatus : + log.info("Summary file '%s' correctly parsed (took %.3f seconds)" % (self.in_path, endtime - starttime)) + else : + log.warning("Error converting summary file '%s' (took %.3f seconds)" % (self.in_path, endtime - starttime)) + except IOError, ioe: log.exception("Error converting '%s'" % self.in_path) finally: @@ -421,7 +443,7 @@ or None if we fail to get the info """ def getStatus(self): #TODO: Return a status of true if file processed ok - return True + return self.status def getProcessedHands(self): return self.processedHands @@ -431,3 +453,15 @@ or None if we fail to get the info """ def getLastCharacterRead(self): return self.index + + def isSummary(self, topline): + return " Tournament Summary " in topline + + def getParsedObjectType(self): + return self.parsedObjectType + + #returns a status (True/False) indicating wether the parsing could be done correctly or not + def readSummaryInfo(self, summaryInfoList): abstract + + def getTourney(self): + return self.tourney diff --git a/pyfpdb/Tourney.py b/pyfpdb/Tourney.py new file mode 100644 index 00000000..1d8b1eb7 --- /dev/null +++ b/pyfpdb/Tourney.py @@ -0,0 +1,462 @@ +#!/usr/bin/python + +#Copyright 2009 Stephane Alessio +#This program is free software: you can redistribute it and/or modify +#it under the terms of the GNU Affero General Public License as published by +#the Free Software Foundation, version 3 of the License. +# +#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 Affero General Public License +#along with this program. If not, see . +#In the "official" distribution you can find the license in +#agpl-3.0.txt in the docs folder of the package. + +# TODO: check to keep only the needed modules + +import re +import sys +import traceback +import logging +import os +import os.path +from decimal import Decimal +import operator +import time,datetime +from copy import deepcopy +from Exceptions import * +import pprint +import DerivedStats +import Card + +log = logging.getLogger("parser") + +class Tourney(object): + +################################################################ +# Class Variables + UPS = {'a':'A', 't':'T', 'j':'J', 'q':'Q', 'k':'K', 'S':'s', 'C':'c', 'H':'h', 'D':'d'} # SAL- TO KEEP ?? + LCS = {'H':'h', 'D':'d', 'C':'c', 'S':'s'} # SAL- TO KEEP ?? + 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 } + + + def __init__(self, sitename, gametype, summaryText, builtFrom = "HHC"): + print "Tourney.__init__" + self.sitename = sitename + self.siteId = self.SITEIDS[sitename] + self.gametype = gametype + self.starttime = None + self.endtime = None + self.summaryText = summaryText + self.tourneyName = None + self.tourNo = None + self.buyin = None + self.fee = None # the Database code is looking for this one .. ? + self.hero = None + self.maxseats = None + self.entries = 0 + self.speed = "Normal" + self.prizepool = None # Make it a dict in order to deal (eventually later) with non-money winnings : {'MONEY' : amount, 'OTHER' : Value ??} + self.buyInChips = None + self.mixed = None + self.isRebuy = False + self.isKO = False + self.isHU = False + self.isMatrix = False + self.isShootout = False + self.matrixMatchId = None # For Matrix tourneys : 1-4 => match tables (traditionnal), 0 => Positional winnings info + self.subTourneyBuyin = None + self.subTourneyFee = None + self.rebuyChips = 0 + self.addOnChips = 0 + self.countRebuys = 0 + self.countAddOns = 0 + self.rebuyAmount = 0 + self.addOnAmount = 0 + self.totalRebuys = 0 + self.totalAddOns = 0 + self.koBounty = 0 + self.countKO = 0 #To use for winnings calculation which is not counted in the rest of the summary file + self.players = [] + + # Collections indexed by player names + self.finishPositions = {} + self.winnings = {} + + + + # currency symbol for this summary + self.sym = None + #self.sym = self.SYMBOL[self.gametype['currency']] # save typing! delete this attr when done + + def __str__(self): + #TODO : Update + vars = ( ("SITE", self.sitename), + ("START TIME", self.starttime), + ("END TIME", self.endtime), + ("TOURNEY NAME", self.tourneyName), + ("TOURNEY NO", self.tourNo), + ("BUYIN", self.buyin), + ("FEE", self.fee), + ("HERO", self.hero), + ("MAXSEATS", self.maxseats), + ("ENTRIES", self.entries), + ("SPEED", self.speed), + ("PRIZE POOL", self.prizepool), + ("STARTING CHIP COUNT", self.buyInChips), + ("MIXED", self.mixed), + ("REBUY ADDON", self.isRebuy), + ("KO", self.isKO), + ("HU", self.isHU), + ("MATRIX", self.isMatrix), + ("SHOOTOUT", self.isShootout), + ("MATRIX MATCH ID", self.matrixMatchId), + ("SUB TOURNEY BUY IN", self.subTourneyBuyin), + ("SUB TOURNEY FEE", self.subTourneyFee), + ("REBUY CHIPS", self.rebuyChips), + ("ADDON CHIPS", self.addOnChips), + ("REBUY AMOUNT", self.rebuyAmount), + ("ADDON AMOUNT", self.addOnAmount), + ("COUNT REBUYS", self.countRebuys), + ("COUNT ADDONS", self.countAddOns), + ("NB REBUYS", self.countRebuys), + ("NB ADDONS", self.countAddOns), + ("TOTAL REBUYS", self.totalRebuys), + ("TOTAL ADDONS", self.totalAddOns), + ("KO BOUNTY", self.koBounty), + ("NB OF KO", self.countKO) + ) + + structs = ( ("GAMETYPE", self.gametype), + ("PLAYERS", self.players), + ("POSITIONS", self.finishPositions), + ("WINNINGS", self.winnings), + ) + str = '' + for (name, var) in vars: + str = str + "\n%s = " % name + pprint.pformat(var) + + for (name, struct) in structs: + str = str + "\n%s =\n" % name + pprint.pformat(struct, 4) + return str + + def getSummaryText(self): + return self.summaryText + + def prepInsert(self, db): + pass + + def insert(self, db): + print "TODO: Insert Tourney in DB" + # First : check all needed info is filled in the object, especially for the initial select + + # Notes on DB Insert + # Some identified issues for tourneys already in the DB (which occurs when the HH file is parsed and inserted before the Summary) + # Be careful on updates that could make the HH import not match the tourney inserted from a previous summary import !! + # BuyIn/Fee can be at 0/0 => match may not be easy + # Only one existinf Tourney entry for Matrix Tourneys, but multiple Summary files + # Starttime may not match the one in the Summary file : HH = time of the first Hand / could be slighltly different from the one in the summary file + # Note: If the TourneyNo could be a unique id .... this would really be a relief to deal with matrix matches ==> Ask on the IRC / Ask Fulltilt ?? + + stored = 0 + duplicates = 0 + partial = 0 + errors = 0 + ttime = 0 + return (stored, duplicates, partial, errors, ttime) + + + def old_insert_from_Hand(self, db): + """ Function to insert Hand into database +Should not commit, and do minimal selects. Callers may want to cache commits +db: a connected fpdb_db object""" + # TODO: + # Players - base playerid and siteid tuple + sqlids = db.getSqlPlayerIDs([p[1] for p in self.players], self.siteId) + + #Gametypes + gtid = db.getGameTypeId(self.siteId, self.gametype) + + # HudCache data to come from DerivedStats class + # HandsActions - all actions for all players for all streets - self.actions + # Hands - Summary information of hand indexed by handId - gameinfo + #This should be moved to prepInsert + hh = {} + hh['siteHandNo'] = self.handid + hh['handStart'] = self.starttime + hh['gameTypeId'] = gtid + # seats TINYINT NOT NULL, + hh['tableName'] = self.tablename + hh['maxSeats'] = self.maxseats + hh['seats'] = len(sqlids) + # Flop turn and river may all be empty - add (likely) too many elements and trim with range + boardcards = self.board['FLOP'] + self.board['TURN'] + self.board['RIVER'] + [u'0x', u'0x', u'0x', u'0x', u'0x'] + cards = [Card.encodeCard(c) for c in boardcards[0:5]] + hh['boardcard1'] = cards[0] + hh['boardcard2'] = cards[1] + hh['boardcard3'] = cards[2] + hh['boardcard4'] = cards[3] + hh['boardcard5'] = cards[4] + + # texture smallint, + # playersVpi SMALLINT NOT NULL, /* num of players vpi */ + # Needs to be recorded + # playersAtStreet1 SMALLINT NOT NULL, /* num of players seeing flop/street4 */ + # Needs to be recorded + # playersAtStreet2 SMALLINT NOT NULL, + # Needs to be recorded + # playersAtStreet3 SMALLINT NOT NULL, + # Needs to be recorded + # playersAtStreet4 SMALLINT NOT NULL, + # Needs to be recorded + # playersAtShowdown SMALLINT NOT NULL, + # Needs to be recorded + # street0Raises TINYINT NOT NULL, /* num small bets paid to see flop/street4, including blind */ + # Needs to be recorded + # street1Raises TINYINT NOT NULL, /* num small bets paid to see turn/street5 */ + # Needs to be recorded + # street2Raises TINYINT NOT NULL, /* num big bets paid to see river/street6 */ + # Needs to be recorded + # street3Raises TINYINT NOT NULL, /* num big bets paid to see sd/street7 */ + # Needs to be recorded + # street4Raises TINYINT NOT NULL, /* num big bets paid to see showdown */ + # Needs to be recorded + + #print "DEBUG: self.getStreetTotals = (%s, %s, %s, %s, %s)" % self.getStreetTotals() + #FIXME: Pot size still in decimal, needs to be converted to cents + (hh['street1Pot'], hh['street2Pot'], hh['street3Pot'], hh['street4Pot'], hh['showdownPot']) = self.getStreetTotals() + + # comment TEXT, + # commentTs DATETIME + #print hh + handid = db.storeHand(hh) + # HandsPlayers - ? ... Do we fix winnings? + # Tourneys ? + # TourneysPlayers + + pass + + def select(self, tourneyId): + """ Function to create Tourney object from database """ + + + + def addPlayer(self, rank, name, winnings): + """\ +Adds a player to the tourney, and initialises data structures indexed by player. +rank (int) indicating the finishing rank (can be -1 if unknown) +name (string) player name +winnings (string) the money the player ended the tourney with (can be 0, or -1 if unknown) +""" + log.debug("addPlayer: rank:%s - name : '%s' - Winnings (%s)" % (rank, name, winnings)) + winnings = re.sub(u',', u'', winnings) #some sites have commas + self.players.append(name) + self.finishPositions.update( { name : Decimal(rank) } ) + self.winnings.update( { name : Decimal(winnings) } ) + + + def incrementPlayerWinnings(self, name, additionnalWinnings): + log.debug("incrementPlayerWinnings: name : '%s' - Add Winnings (%s)" % (name, additionnalWinnings)) + oldWins = 0 + if self.winnings.has_key(name): + oldWins = self.winnings[name] + else: + self.players.append([-1, name, 0]) + + self.winnings[name] = oldWins + Decimal(additionnalWinnings) + + + def calculatePayinAmount(self): + return self.buyin + self.fee + (self.rebuyAmount * self.countRebuys) + (self.addOnAmount * self.countAddOns ) + + + def checkPlayerExists(self,player): + if player not in [p[1] for p in self.players]: + print "checkPlayerExists", player, "fail" + raise FpdbParseError + + + def getGameTypeAsString(self): + """\ +Map the tuple self.gametype onto the pokerstars string describing it +""" + # currently it appears to be something like ["ring", "hold", "nl", sb, bb]: + gs = {"holdem" : "Hold'em", + "omahahi" : "Omaha", + "omahahilo" : "Omaha Hi/Lo", + "razz" : "Razz", + "studhi" : "7 Card Stud", + "studhilo" : "7 Card Stud Hi/Lo", + "fivedraw" : "5 Card Draw", + "27_1draw" : "FIXME", + "27_3draw" : "Triple Draw 2-7 Lowball", + "badugi" : "Badugi" + } + ls = {"nl" : "No Limit", + "pl" : "Pot Limit", + "fl" : "Limit", + "cn" : "Cap No Limit", + "cp" : "Cap Pot Limit" + } + + log.debug("gametype: %s" %(self.gametype)) + retstring = "%s %s" %(gs[self.gametype['category']], ls[self.gametype['limitType']]) + return retstring + + + def writeSummary(self, fh=sys.__stdout__): + print >>fh, "Override me" + + def printSummary(self): + self.writeSummary(sys.stdout) + + +def assemble(cnxn, tourneyId): + # TODO !! + c = cnxn.cursor() + + # We need at least sitename, gametype, handid + # for the Hand.__init__ + c.execute(""" +select + s.name, + g.category, + g.base, + g.type, + g.limitType, + g.hilo, + round(g.smallBlind / 100.0,2), + round(g.bigBlind / 100.0,2), + round(g.smallBet / 100.0,2), + round(g.bigBet / 100.0,2), + s.currency, + h.boardcard1, + h.boardcard2, + h.boardcard3, + h.boardcard4, + h.boardcard5 +from + hands as h, + sites as s, + gametypes as g, + handsplayers as hp, + players as p +where + h.id = %(handid)s +and g.id = h.gametypeid +and hp.handid = h.id +and p.id = hp.playerid +and s.id = p.siteid +limit 1""", {'handid':handid}) + #TODO: siteid should be in hands table - we took the scenic route through players here. + res = c.fetchone() + gametype = {'category':res[1],'base':res[2],'type':res[3],'limitType':res[4],'hilo':res[5],'sb':res[6],'bb':res[7], 'currency':res[10]} + h = HoldemOmahaHand(hhc = None, sitename=res[0], gametype = gametype, handText=None, builtFrom = "DB", handid=handid) + cards = map(Card.valueSuitFromCard, res[11:16] ) + if cards[0]: + h.setCommunityCards('FLOP', cards[0:3]) + if cards[3]: + h.setCommunityCards('TURN', [cards[3]]) + if cards[4]: + h.setCommunityCards('RIVER', [cards[4]]) + #[Card.valueSuitFromCard(x) for x in cards] + + # HandInfo : HID, TABLE + # BUTTON - why is this treated specially in Hand? + # answer: it is written out in hand histories + # still, I think we should record all the active seat positions in a seat_order array + c.execute(""" +SELECT + h.sitehandno as hid, + h.tablename as table, + h.handstart as starttime +FROM + hands as h +WHERE h.id = %(handid)s +""", {'handid':handid}) + res = c.fetchone() + h.handid = res[0] + h.tablename = res[1] + h.starttime = res[2] # automatically a datetime + + # PlayerStacks + c.execute(""" +SELECT + hp.seatno, + round(hp.winnings / 100.0,2) as winnings, + p.name, + round(hp.startcash / 100.0,2) as chips, + hp.card1,hp.card2, + hp.position +FROM + handsplayers as hp, + players as p +WHERE + hp.handid = %(handid)s +and p.id = hp.playerid +""", {'handid':handid}) + for (seat, winnings, name, chips, card1,card2, position) in c.fetchall(): + h.addPlayer(seat,name,chips) + if card1 and card2: + h.addHoleCards(map(Card.valueSuitFromCard, (card1,card2)), name, dealt=True) + if winnings > 0: + h.addCollectPot(name, winnings) + if position == 'B': + h.buttonpos = seat + + + # actions + c.execute(""" +SELECT + (ha.street,ha.actionno) as actnum, + p.name, + ha.street, + ha.action, + ha.allin, + round(ha.amount / 100.0,2) +FROM + handsplayers as hp, + handsactions as ha, + players as p +WHERE + hp.handid = %(handid)s +and ha.handsplayerid = hp.id +and p.id = hp.playerid +ORDER BY + ha.street,ha.actionno +""", {'handid':handid}) + res = c.fetchall() + for (actnum,player, streetnum, act, allin, amount) in res: + act=act.strip() + street = h.allStreets[streetnum+1] + if act==u'blind': + h.addBlind(player, 'big blind', amount) + # TODO: The type of blind is not recorded in the DB. + # TODO: preflop street name anomalies in Hand + elif act==u'fold': + h.addFold(street,player) + elif act==u'call': + h.addCall(street,player,amount) + elif act==u'bet': + h.addBet(street,player,amount) + elif act==u'check': + h.addCheck(street,player) + elif act==u'unbet': + pass + else: + print act, player, streetnum, allin, amount + # TODO : other actions + + #hhc.readShowdownActions(self) + #hc.readShownCards(self) + h.totalPot() + h.rake = h.totalpot - h.totalcollected + + + return h + diff --git a/pyfpdb/fpdb_import.py b/pyfpdb/fpdb_import.py index 6b4994b6..1282ee4c 100644 --- a/pyfpdb/fpdb_import.py +++ b/pyfpdb/fpdb_import.py @@ -395,29 +395,45 @@ class Importer: out_path = os.path.join(hhdir, "x"+strftime("%d-%m-%y")+os.path.basename(file)) filter_name = filter.replace("ToFpdb", "") - - mod = __import__(filter) - obj = getattr(mod, filter_name, None) - if callable(obj): - hhc = obj(in_path = file, out_path = out_path, index = 0) # Index into file 0 until changeover - if(hhc.getStatus() and self.NEWIMPORT == False): - (stored, duplicates, partial, errors, ttime) = self.import_fpdb_file(db, out_path, site, q) - elif (hhc.getStatus() and self.NEWIMPORT == True): - #This code doesn't do anything yet - handlist = hhc.getProcessedHands() - self.pos_in_file[file] = hhc.getLastCharacterRead() - - for hand in handlist: - #hand.prepInsert() - hand.insert(self.database) - else: - # conversion didn't work - # TODO: appropriate response? - return (0, 0, 0, 1, 0, -1) - else: - print "Unknown filter filter_name:'%s' in filter:'%s'" %(filter_name, filter) - return (0, 0, 0, 1, 0, -1) - + mod = __import__(filter) + obj = getattr(mod, filter_name, None) + if callable(obj): + hhc = obj(in_path = file, out_path = out_path, index = 0) # Index into file 0 until changeover + if hhc.getParsedObjectType() == "HH": + if(hhc.getStatus() and self.NEWIMPORT == False): + (stored, duplicates, partial, errors, ttime) = self.import_fpdb_file(db, out_path, site, q) + elif (hhc.getStatus() and self.NEWIMPORT == True): + #This code doesn't do anything yet + handlist = hhc.getProcessedHands() + self.pos_in_file[file] = hhc.getLastCharacterRead() + + for hand in handlist: + #hand.prepInsert() + hand.insert(self.database) + else: + # conversion didn't work + # TODO: appropriate response? + return (0, 0, 0, 1, 0, -1) + elif hhc.getParsedObjectType() == "Summary": + if(hhc.getStatus()): + tourney = hhc.getTourney() + #print tourney + #tourney.prepInsert() + (stored, duplicates, partial, errors, ttime) = tourney.insert(self.database) + return (stored, duplicates, partial, errors, ttime) + + else: + # conversion didn't work + # Could just be the parsing of a non summary file (classic HH file) + return (0, 0, 0, 0, 0) + else: + print "Unknown objects parsed by HHC :'%s'" %(hhc.getObjectTypeRead()) + return (0, 0, 0, 1, 0, -1) + + else: + print "Unknown filter filter_name:'%s' in filter:'%s'" %(filter_name, filter) + return (0, 0, 0, 1, 0, -1) + #This will barf if conv.getStatus != True return (stored, duplicates, partial, errors, ttime)