From 629159c78508a79df5e3f634b630c388f3f6d6fc Mon Sep 17 00:00:00 2001 From: Eric Blade Date: Fri, 21 Aug 2009 00:23:07 -0500 Subject: [PATCH 1/6] rename Summary-Everleaf to SummaryEverleaf, as I've discovered you can't import a module with a "-" in it's filename --- pyfpdb/{Summary-Everleaf.py => SummaryEverleaf.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename pyfpdb/{Summary-Everleaf.py => SummaryEverleaf.py} (99%) diff --git a/pyfpdb/Summary-Everleaf.py b/pyfpdb/SummaryEverleaf.py similarity index 99% rename from pyfpdb/Summary-Everleaf.py rename to pyfpdb/SummaryEverleaf.py index e2f56408..8923a3b1 100644 --- a/pyfpdb/Summary-Everleaf.py +++ b/pyfpdb/SummaryEverleaf.py @@ -169,7 +169,7 @@ class SummaryParser(htmllib.HTMLParser): # derive new HTML parser class EverleafSummary: def main(self): - file = urllib.urlopen("http://www.poker4ever.com/en.tournaments.tournament-statistics?tid=785119") + file = urllib.urlopen("http://www.poker4ever.com/en.tournaments.tournament-statistics?tid=817095") parser = SummaryParser(formatter.NullFormatter()) parser.feed(file.read()) print "site=",parser.SiteName, "tourneyname=", parser.TourneyName, "tourneyid=", parser.TourneyId From 832b7fe4db87a11ffd00b6d84a842b9598d6bb11 Mon Sep 17 00:00:00 2001 From: Eric Blade Date: Fri, 21 Aug 2009 00:42:19 -0500 Subject: [PATCH 2/6] add first revision of my TournamentTracker idea, actually based on the HUD_main program :) fix SummaryEverleaf to be a little more friendly to being imported and accessed from other modules TournamentTracker pops open an Edit Window when you add a tournament, but I don't know how to actually populate it with Edit boxes yet --- pyfpdb/SummaryEverleaf.py | 22 +-- pyfpdb/TournamentTracker.py | 300 ++++++++++++++++++++++++++++++++++++ 2 files changed, 313 insertions(+), 9 deletions(-) create mode 100644 pyfpdb/TournamentTracker.py diff --git a/pyfpdb/SummaryEverleaf.py b/pyfpdb/SummaryEverleaf.py index 8923a3b1..990b1af5 100644 --- a/pyfpdb/SummaryEverleaf.py +++ b/pyfpdb/SummaryEverleaf.py @@ -168,15 +168,19 @@ class SummaryParser(htmllib.HTMLParser): # derive new HTML parser self.TempResultPos += 1 class EverleafSummary: - def main(self): - file = urllib.urlopen("http://www.poker4ever.com/en.tournaments.tournament-statistics?tid=817095") - parser = SummaryParser(formatter.NullFormatter()) - parser.feed(file.read()) - print "site=",parser.SiteName, "tourneyname=", parser.TourneyName, "tourneyid=", parser.TourneyId - print "start time=",parser.TourneyStartTime, "end time=",parser.TourneyEndTime - print "structure=", parser.TourneyStructure, "game type=",parser.TourneyGameType - print "buy-in=", parser.TourneyBuyIn, "rebuys=", parser.TourneyRebuys, "total players=", parser.TourneyPlayers, "pool=", parser.TourneyPool - print "results=", parser.Results + def __init__(self): + if __name__ != "__main__": + self.main() + + def main(self, id="785119"): + file = urllib.urlopen("http://www.poker4ever.com/en.tournaments.tournament-statistics?tid="+id) + self.parser = SummaryParser(formatter.NullFormatter()) + self.parser.feed(file.read()) + print "site=",self.parser.SiteName, "tourneyname=", self.parser.TourneyName, "tourneyid=", self.parser.TourneyId + print "start time=",self.parser.TourneyStartTime, "end time=",self.parser.TourneyEndTime + print "structure=", self.parser.TourneyStructure, "game type=",self.parser.TourneyGameType + print "buy-in=", self.parser.TourneyBuyIn, "rebuys=", self.parser.TourneyRebuys, "total players=", self.parser.TourneyPlayers, "pool=", self.parser.TourneyPool + print "results=", self.parser.Results if __name__ == "__main__": diff --git a/pyfpdb/TournamentTracker.py b/pyfpdb/TournamentTracker.py new file mode 100644 index 00000000..2198263f --- /dev/null +++ b/pyfpdb/TournamentTracker.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python +"""TourneyTracker.py + Based on HUD_main .. who knows if we want to actually use this or not +""" +# Copyright 2008, 2009, Eric Blade +# +# 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 + +######################################################################## + +# to do allow window resizing +# to do hud to echo, but ignore non numbers +# to do no stat window for hero +# to do things to add to config.xml + +# Standard Library modules +import sys +import os +import Options +import traceback + +(options, sys.argv) = Options.fpdb_options() + +if not options.errorsToConsole: + print "Note: error output is being diverted to fpdb-error-log.txt and HUD-error.txt. Any major error will be reported there _only_." + errorFile = open('tourneyerror.txt', 'w', 0) + sys.stderr = errorFile + +import thread +import time +import string +import re + +# pyGTK modules +import pygtk +import gtk +import gobject + +# FreePokerTools modules +import Configuration +import Database +import SummaryEverleaf + +class Tournament: + """Tournament will hold the information about a tournament, I guess ? Remember I'm new to this language, so I don't know the best ways to do things""" + + def __init__(self, parent, site, tid): # site should probably be something in the config object, but i don't know how the config object works right now, so we're going to make it a str .. + print "Tournament init" + self.parent = parent + self.window = None + self.site = site + self.id = tid + self.starttime = time.time() + self.endtime = None + self.game = None + self.structure = None + self.buyin = 0 + self.fee = 0 + self.rebuys = False + self.numrebuys = 0 # this should probably be attached to the players list... + self.numplayers = 0 + self.prizepool = 0 + self.players = {} # eventually i'd guess we'd probably want to fill this with playername:playerid's + self.results = {} # i'd guess we'd want to load this up with playerid's instead of playernames, too, as well, also + + # if site == "Everleaf": # this should be attached to a button that says "retrieve tournament info" or something for sites that we know how to do it for + summary = SummaryEverleaf.EverleafSummary() + self.site = summary.parser.SiteName + self.id = summary.parser.TourneyId + self.starttime = summary.parser.TourneyStartTime + self.endtime = summary.parser.TourneyEndTime + self.game = summary.parser.TourneyGameType + self.structure = summary.parser.TourneyStructure + self.buyin = summary.parser.TourneyBuyIn # need to remember to parse the Fee out of this and move it to self.fee + self.rebuys = (summary.parser.TourneyRebuys == "yes") + self.prizepool = summary.parser.TourneyPool + self.numplayers = summary.parser.TourneyPlayers + + self.openwindow() # let's start by getting any info we need.. meh + + def openwindow(self, widget=None): + if self.window is not None: + self.window.show() # isn't there a better way to bring something to the front? not that GTK focus works right anyway, ever + else: + self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) + print "tournament edit window=", self.window + self.window.connect("delete_event", self.delete_event) + self.window.connect("destroy", self.destroy) + self.window.set_title("FPDB Tournament Entry") + self.window.set_border_width(1) + self.window.set_default_size(480,640) + self.window.set_resizable(True) + + self.main_vbox = gtk.VBox(False, 1) + self.main_vbox.set_border_width(1) + self.window.add(self.main_vbox) + self.window.show() + + def delete_event(self, widget, event, data=None): + return False + + def destroy(self, widget, data=None): + return False + #end def destroy + + +class ttracker_main(object): + """A main() object to own both the read_stdin thread and the gui.""" +# This class mainly provides state for controlling the multiple HUDs. + + def __init__(self, db_name = 'fpdb'): + self.db_name = db_name + self.config = Configuration.Config(file=options.config, dbname=options.dbname) + self.tourney_list = [] + +# a thread to read stdin + gobject.threads_init() # this is required + thread.start_new_thread(self.read_stdin, ()) # starts the thread + +# a main window + self.main_window = gtk.Window() + self.main_window.connect("destroy", self.destroy) + self.vb = gtk.VBox() + self.label = gtk.Label('Closing this window will stop the Tournament Tracker') + self.vb.add(self.label) + self.addbutton = gtk.Button(label="Enter Tournament") + self.addbutton.connect("clicked", self.addClicked, "add tournament") + self.vb.add(self.addbutton) + + self.main_window.add(self.vb) + self.main_window.set_title("FPDB Tournament Tracker") + self.main_window.show_all() + + def addClicked(self, widget, data): # what is "data"? i'm guessing anything i pass in after the function name in connect() but unsure because the documentation sucks + print "addClicked", widget, data + t = Tournament(self, None, None) + if t is not None: + print "new tournament=", t + self.tourney_list.append(t) + mylabel = gtk.Label("%s - %s - %s - %s - %s %s - %s - %s - %s - %s - %s" % (t.site, t.id, t.starttime, t.endtime, t.structure, t.game, t.buyin, t.fee, t.numrebuys, t.numplayers, t.prizepool)) + print "new label=", mylabel + editbutton = gtk.Button(label="Edit") + print "new button=", editbutton + editbutton.connect("clicked", t.openwindow) + self.vb.add(editbutton) # These should probably be put in.. a.. h-box? i don't know.. + self.vb.add(mylabel) + self.vb.show() + self.main_window.resize_children() + self.main_window.show() + mylabel.show() + editbutton.show() + t.mylabel = mylabel + t.editbutton = editbutton + print self.tourney_list + + return True + else: + return False + # when we move the start command over to the main program, we can have the main program ask for the tourney id, and pipe it into the stdin here + # at least that was my initial thought on it + + def destroy(*args): # call back for terminating the main eventloop + gtk.main_quit() + + def create_HUD(self, new_hand_id, table, table_name, max, poker_game, stat_dict, cards): + + def idle_func(): + + gtk.gdk.threads_enter() + try: + newlabel = gtk.Label("%s - %s" % (table.site, table_name)) + self.vb.add(newlabel) + newlabel.show() + self.main_window.resize_children() + + self.hud_dict[table_name].tablehudlabel = newlabel + self.hud_dict[table_name].create(new_hand_id, self.config, stat_dict, cards) + for m in self.hud_dict[table_name].aux_windows: + m.create() + m.update_gui(new_hand_id) + self.hud_dict[table_name].update(new_hand_id, self.config) + self.hud_dict[table_name].reposition_windows() + return False + finally: + gtk.gdk.threads_leave() + + self.hud_dict[table_name] = Hud.Hud(self, table, max, poker_game, self.config, self.db_connection) + self.hud_dict[table_name].table_name = table_name + self.hud_dict[table_name].stat_dict = stat_dict + self.hud_dict[table_name].cards = cards + [aw.update_data(new_hand_id, self.db_connection) for aw in self.hud_dict[table_name].aux_windows] + gobject.idle_add(idle_func) + + def update_HUD(self, new_hand_id, table_name, config): + """Update a HUD gui from inside the non-gui read_stdin thread.""" +# This is written so that only 1 thread can touch the gui--mainly +# for compatibility with Windows. This method dispatches the +# function idle_func() to be run by the gui thread, at its leisure. + def idle_func(): + gtk.gdk.threads_enter() + try: + self.hud_dict[table_name].update(new_hand_id, config) + [aw.update_gui(new_hand_id) for aw in self.hud_dict[table_name].aux_windows] + return False + finally: + gtk.gdk.threads_leave() + gobject.idle_add(idle_func) + + def read_stdin(self): # This is the thread function + """Do all the non-gui heavy lifting for the HUD program.""" + +# This db connection is for the read_stdin thread only. It should not +# be passed to HUDs for use in the gui thread. HUD objects should not +# need their own access to the database, but should open their own +# if it is required. + self.db_connection = Database.Database(self.config, self.db_name, 'temp') +# self.db_connection.init_hud_stat_vars(hud_days) + tourny_finder = re.compile('(\d+) (\d+)') + + while 1: # wait for a new hand number on stdin + new_hand_id = sys.stdin.readline() + new_hand_id = string.rstrip(new_hand_id) + if new_hand_id == "": # blank line means quit + self.destroy() + break # this thread is not always killed immediately with gtk.main_quit() +# get basic info about the new hand from the db +# if there is a db error, complain, skip hand, and proceed + try: + (table_name, max, poker_game, type) = self.db_connection.get_table_name(new_hand_id) + stat_dict = self.db_connection.get_stats_from_hand(new_hand_id, aggregate_stats[type] + ,hud_style, agg_bb_mult) + + cards = self.db_connection.get_cards(new_hand_id) + comm_cards = self.db_connection.get_common_cards(new_hand_id) + if comm_cards != {}: # stud! + cards['common'] = comm_cards['common'] + except Exception, err: + err = traceback.extract_tb(sys.exc_info()[2])[-1] + print "db error: skipping "+str(new_hand_id)+" "+err[2]+"("+str(err[1])+"): "+str(sys.exc_info()[1]) + if new_hand_id: # new_hand_id is none if we had an error prior to the store + sys.stderr.write("Database error %s in hand %d. Skipping.\n" % (err, int(new_hand_id))) + continue + + if type == "tour": # hand is from a tournament + mat_obj = tourny_finder.search(table_name) + if mat_obj: + (tour_number, tab_number) = mat_obj.group(1, 2) + temp_key = tour_number + else: # tourney, but can't get number and table + print "could not find tournament: skipping " + sys.stderr.write("Could not find tournament %d in hand %d. Skipping.\n" % (int(tour_number), int(new_hand_id))) + continue + + else: + temp_key = table_name + +# Update an existing HUD + if temp_key in self.hud_dict: + self.hud_dict[temp_key].stat_dict = stat_dict + self.hud_dict[temp_key].cards = cards + [aw.update_data(new_hand_id, self.db_connection) for aw in self.hud_dict[temp_key].aux_windows] + self.update_HUD(new_hand_id, temp_key, self.config) + +# Or create a new HUD + else: + if type == "tour": + tablewindow = Tables.discover_tournament_table(self.config, tour_number, tab_number) + else: + tablewindow = Tables.discover_table_by_name(self.config, table_name) + if tablewindow == None: +# If no client window is found on the screen, complain and continue + if type == "tour": + table_name = "%s %s" % (tour_number, tab_number) + sys.stderr.write("table name "+table_name+" not found, skipping.\n") + else: + self.create_HUD(new_hand_id, tablewindow, temp_key, max, poker_game, stat_dict, cards) + self.db_connection.connection.rollback() + +if __name__== "__main__": + + sys.stderr.write("tournament tracker starting\n") + sys.stderr.write("Using db name = %s\n" % (options.dbname)) + +# start the HUD_main object + hm = ttracker_main(db_name = options.dbname) + +# start the event loop + gtk.main() From d5d0c9aee38b37a49829462a210a5aa4f0ae4a83 Mon Sep 17 00:00:00 2001 From: Eric Blade Date: Fri, 21 Aug 2009 00:48:26 -0500 Subject: [PATCH 3/6] add "rebuy" button to TT --- pyfpdb/SummaryEverleaf.py | 2 +- pyfpdb/TournamentTracker.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pyfpdb/SummaryEverleaf.py b/pyfpdb/SummaryEverleaf.py index 990b1af5..ec282890 100644 --- a/pyfpdb/SummaryEverleaf.py +++ b/pyfpdb/SummaryEverleaf.py @@ -172,7 +172,7 @@ class EverleafSummary: if __name__ != "__main__": self.main() - def main(self, id="785119"): + def main(self, id="785646"): file = urllib.urlopen("http://www.poker4ever.com/en.tournaments.tournament-statistics?tid="+id) self.parser = SummaryParser(formatter.NullFormatter()) self.parser.feed(file.read()) diff --git a/pyfpdb/TournamentTracker.py b/pyfpdb/TournamentTracker.py index 2198263f..7a68a644 100644 --- a/pyfpdb/TournamentTracker.py +++ b/pyfpdb/TournamentTracker.py @@ -107,6 +107,11 @@ class Tournament: self.main_vbox.set_border_width(1) self.window.add(self.main_vbox) self.window.show() + + def addrebuy(self, widget=None): + t = self + t.numrebuys += 1 + t.mylabel.set_label("%s - %s - %s - %s - %s %s - %s - %s - %s - %s - %s" % (t.site, t.id, t.starttime, t.endtime, t.structure, t.game, t.buyin, t.fee, t.numrebuys, t.numplayers, t.prizepool)) def delete_event(self, widget, event, data=None): return False @@ -154,15 +159,20 @@ class ttracker_main(object): editbutton = gtk.Button(label="Edit") print "new button=", editbutton editbutton.connect("clicked", t.openwindow) + rebuybutton = gtk.Button(label="Rebuy") + rebuybutton.connect("clicked", t.addrebuy) + self.vb.add(rebuybutton) self.vb.add(editbutton) # These should probably be put in.. a.. h-box? i don't know.. self.vb.add(mylabel) - self.vb.show() self.main_window.resize_children() self.main_window.show() mylabel.show() editbutton.show() + rebuybutton.show() t.mylabel = mylabel t.editbutton = editbutton + t.rebuybutton = rebuybutton + self.vb.show() print self.tourney_list return True From 8420e2203845ed2bb6b80b28f60cd708cb64ce04 Mon Sep 17 00:00:00 2001 From: Eric Blade Date: Fri, 21 Aug 2009 05:57:04 -0500 Subject: [PATCH 4/6] add in Ante RegEx for Absolute .. hopefully it works, but I don't have the roll there to play holdem with Antes to find out --- pyfpdb/AbsoluteToFpdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyfpdb/AbsoluteToFpdb.py b/pyfpdb/AbsoluteToFpdb.py index 796e5e6e..e8a77d2b 100644 --- a/pyfpdb/AbsoluteToFpdb.py +++ b/pyfpdb/AbsoluteToFpdb.py @@ -73,7 +73,7 @@ class Absolute(HandHistoryConverter): #self.re_PostSB = re.compile(ur"^%s: posts small blind \[(?:\$| €|) (?P[.0-9]+)" % player_re, re.MULTILINE) #self.re_PostBB = re.compile(ur"^%s: posts big blind \[(?:\$| €|) (?P[.0-9]+)" % player_re, re.MULTILINE) #self.re_PostBoth = re.compile(ur"^%s: posts both blinds \[(?:\$| €|) (?P[.0-9]+)" % player_re, re.MULTILINE) - #self.re_Antes = re.compile(ur"^%s: posts ante \[(?:\$| €|) (?P[.0-9]+)" % player_re, re.MULTILINE) + self.re_Antes = re.compile(ur"^%s - Ante \[(?:\$| €|)(?P[.0-9]+)" % player_re, re.MULTILINE) #self.re_BringIn = re.compile(ur"^%s posts bring-in (?:\$| €|)(?P[.0-9]+)\." % player_re, re.MULTILINE) self.re_HeroCards = re.compile(ur"^Dealt to %s \[(?P.*)\]" % player_re, re.MULTILINE) #self.re_Action = re.compile(ur"^%s(?P: bets| checks| raises| calls| folds)(\s\[(?:\$| €|) (?P[.\d]+) (USD|EUR|)\])?" % player_re, re.MULTILINE) From 4a1dd26db3b780881815091c3ac4a351ed5f05a4 Mon Sep 17 00:00:00 2001 From: PassThePeas Date: Sat, 22 Aug 2009 00:09:34 +0200 Subject: [PATCH 5/6] First attempt to parsing summary files from FTP modified: FulltiltToFpdb.py * Modified re_HandInfo regex to take into account Matrix Tournament Hands Histories * Add Regex for Tourney Summaries files parsing * New methods : readSummaryInfo, determineTourneyType, getPlayersPositionsAndWinnings modified: HandHistoryConverter.py * Add a Tourney object in the attributes to allow storing the summary info retrieved and the ability to hand it over after parsing * Add a new attribut parsedObjectType (string : "HH" or "Summary") * In follow = False mode : read the first line to swicth between HH & Summary parsing * TO DO : Deal with parsing summary files in follow = True mode * New methods added : isSummary, getParsedObjectType (to be called in fpdb_import), readSummaryInfo : abstract (to be implemented in each specific HHC), getTourney (returns the new attribute) new file: Tourney.py * New object * Lots of attributes regarding the tourney info (buy-in, fee, entries, speed, Rebuy/add-on info, starting chips, KnockOut info, isHeadsUp, isShootout, isMatrix, ... * List of players with finishing positions (when available) and winnings (when available) * Methods : ** addPlayer(self, rank, name, winnings) ** incrementPlayerWinnings(self, name, additionnalWinnings): used for KO tourneys when KO occured (for Hero only) ** calculatePayinAmount : unused yet, should be necessary for DB storage ** some Hand methods copied that are still to be done including : assemble and insert ** TO DO : write the insert method for the object to be stored in DB. This will have to deal with the fact that the DB write can occur both before (Bulk Import) or after the HH file info has been stored (tourney might or might not already exist) modified: fpdb_import.py * import_file_dict modified : ** after the construction of the hhc, it now calls the new getParsedObjectType method of HHC in order to know what has been parsed by the HHC ** If it's a hand history file (actual expected behaviour) : do as before ** If it's a summary file, gets the tourney object that has been built from the hhc and calls the insert method on it (similar to NEWIMPORT=True for HH) --- pyfpdb/FulltiltToFpdb.py | 264 ++++++++++++++++++- pyfpdb/HandHistoryConverter.py | 56 +++- pyfpdb/Tourney.py | 462 +++++++++++++++++++++++++++++++++ pyfpdb/fpdb_import.py | 62 +++-- 4 files changed, 809 insertions(+), 35 deletions(-) create mode 100644 pyfpdb/Tourney.py 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) From 2e0c743671af2d822c2b1a361df992bd846b3b06 Mon Sep 17 00:00:00 2001 From: Ray Date: Sun, 23 Aug 2009 15:02:00 -0400 Subject: [PATCH 6/6] Detect and skip HHs flagged as partial. --- pyfpdb/FulltiltToFpdb.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyfpdb/FulltiltToFpdb.py b/pyfpdb/FulltiltToFpdb.py index 8e095043..493a6617 100755 --- a/pyfpdb/FulltiltToFpdb.py +++ b/pyfpdb/FulltiltToFpdb.py @@ -53,7 +53,8 @@ class Fulltilt(HandHistoryConverter): (\((?P.+)\)\s)?-\s \$?(?P[.0-9]+)/\$?(?P[.0-9]+)\s(Ante\s\$?(?P[.0-9]+)\s)?-\s (?P[a-zA-Z\/\'\s]+)\s-\s - (?P.*?)\n + (?P\d+:\d+:\d+\s\w+\s-\s\d+/\d+/\d+)\s? + (?P\(partial\))?\n (?:.*?\n(?PHand\s\#(?P=HID)\shas\sbeen\scanceled))? ''', re.VERBOSE|re.DOTALL) re_Button = re.compile('^The button is in seat #(?P