#!/usr/bin/python #Copyright 2008 Steffen Jobbagy-Felso #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. import traceback import threading import pygtk pygtk.require('2.0') import gtk import os import sys from time import time, strftime import Card import fpdb_import import Database import fpdb_db import Filters colalias,colshow,colheading,colxalign,colformat,coltype = 0,1,2,3,4,5 ranks = {'x':0, '2':2, '3':3, '4':4, '5':5, '6':6, '7':7, '8':8, '9':9, 'T':10, 'J':11, 'Q':12, 'K':13, 'A':14} class GuiPlayerStats (threading.Thread): def __init__(self, config, querylist, mainwin, debug=True): self.debug = debug self.conf = config self.main_window = mainwin self.sql = querylist self.liststore = None self.MYSQL_INNODB = 2 self.PGSQL = 3 self.SQLITE = 4 # create new db connection to avoid conflicts with other threads self.db = Database.Database(self.conf, sql=self.sql) self.cursor = self.db.cursor settings = {} settings.update(self.conf.get_db_parameters()) settings.update(self.conf.get_tv_parameters()) settings.update(self.conf.get_import_parameters()) settings.update(self.conf.get_default_paths()) # text used on screen stored here so that it can be configured self.filterText = {'handhead':'Hand Breakdown for all levels listed above' } filters_display = { "Heroes" : True, "Sites" : True, "Games" : False, "Limits" : True, "LimitSep" : True, "LimitType" : True, "Type" : True, "Seats" : True, "SeatSep" : True, "Dates" : True, "Groups" : True, "GroupsAll" : True, "Button1" : True, "Button2" : True } self.filters = Filters.Filters(self.db, self.conf, self.sql, display = filters_display) self.filters.registerButton1Name("_Filters") self.filters.registerButton1Callback(self.showDetailFilter) self.filters.registerButton2Name("_Refresh") self.filters.registerButton2Callback(self.refreshStats) # ToDo: store in config # ToDo: create popup to adjust column config # columns to display, keys match column name returned by sql, values in tuple are: # is column displayed, column heading, xalignment, formatting, celltype self.columns = [ ["game", True, "Game", 0.0, "%s", "str"] , ["hand", False, "Hand", 0.0, "%s", "str"] # true not allowed for this line , ["plposition", False, "Posn", 1.0, "%s", "str"] # true not allowed for this line (set in code) , ["pname", False, "Name", 0.0, "%s", "str"] # true not allowed for this line (set in code) , ["n", True, "Hds", 1.0, "%d", "str"] , ["avgseats", False, "Seats", 1.0, "%3.1f", "str"] , ["vpip", True, "VPIP", 1.0, "%3.1f", "str"] , ["pfr", True, "PFR", 1.0, "%3.1f", "str"] , ["pf3", True, "PF3", 1.0, "%3.1f", "str"] , ["steals", True, "Steals", 1.0, "%3.1f", "str"] , ["saw_f", True, "Saw_F", 1.0, "%3.1f", "str"] , ["sawsd", True, "SawSD", 1.0, "%3.1f", "str"] , ["wtsdwsf", True, "WtSDwsF", 1.0, "%3.1f", "str"] , ["wmsd", True, "W$SD", 1.0, "%3.1f", "str"] , ["flafq", True, "FlAFq", 1.0, "%3.1f", "str"] , ["tuafq", True, "TuAFq", 1.0, "%3.1f", "str"] , ["rvafq", True, "RvAFq", 1.0, "%3.1f", "str"] , ["pofafq", False, "PoFAFq", 1.0, "%3.1f", "str"] , ["net", True, "Net($)", 1.0, "%6.2f", "cash"] , ["bbper100", True, "bb/100", 1.0, "%4.2f", "str"] , ["rake", True, "Rake($)", 1.0, "%6.2f", "cash"] , ["bb100xr", True, "bbxr/100", 1.0, "%4.2f", "str"] , ["variance", True, "Variance", 1.0, "%5.2f", "str"] ] # Detail filters: This holds the data used in the popup window, extra values are # added at the end of these lists during processing # sql test, screen description, min, max self.handtests = [ # already in filter class : ['h.seats', 'Number of Players', 2, 10] ['h.maxSeats', 'Size of Table', 2, 10] ,['h.playersVpi', 'Players who VPI', 0, 10] ,['h.playersAtStreet1', 'Players at Flop', 0, 10] ,['h.playersAtStreet2', 'Players at Turn', 0, 10] ,['h.playersAtStreet3', 'Players at River', 0, 10] ,['h.playersAtStreet4', 'Players at Street7', 0, 10] ,['h.playersAtShowdown', 'Players at Showdown', 0, 10] ,['h.street0Raises', 'Bets to See Flop', 0, 5] ,['h.street1Raises', 'Bets to See Turn', 0, 5] ,['h.street2Raises', 'Bets to See River', 0, 5] ,['h.street3Raises', 'Bets to See Street7', 0, 5] ,['h.street4Raises', 'Bets to See Showdown', 0, 5] ] self.stats_frame = None self.stats_vbox = None self.detailFilters = [] # the data used to enhance the sql select #self.main_hbox = gtk.HBox(False, 0) #self.main_hbox.show() self.main_hbox = gtk.HPaned() self.stats_frame = gtk.Frame() self.stats_frame.show() self.stats_vbox = gtk.VBox(False, 0) self.stats_vbox.show() self.stats_frame.add(self.stats_vbox) # self.fillStatsFrame(self.stats_vbox) #self.main_hbox.pack_start(self.filters.get_vbox()) #self.main_hbox.pack_start(self.stats_frame, expand=True, fill=True) self.main_hbox.pack1(self.filters.get_vbox()) self.main_hbox.pack2(self.stats_frame) self.main_hbox.show() # make sure Hand column is not displayed [x for x in self.columns if x[0] == 'hand'][0][1] = False def get_vbox(self): """returns the vbox of this thread""" return self.main_hbox def refreshStats(self, widget, data): try: self.stats_vbox.destroy() except AttributeError: pass #self.stats_vbox = gtk.VBox(False, 0) self.stats_vbox = gtk.VPaned() self.stats_vbox.show() self.stats_frame.add(self.stats_vbox) self.fillStatsFrame(self.stats_vbox) def fillStatsFrame(self, vbox): sites = self.filters.getSites() heroes = self.filters.getHeroes() siteids = self.filters.getSiteIds() limits = self.filters.getLimits() type = self.filters.getType() seats = self.filters.getSeats() groups = self.filters.getGroups() dates = self.filters.getDates() sitenos = [] playerids = [] # Which sites are selected? for site in sites: if sites[site] == True: sitenos.append(siteids[site]) # Nasty hack to deal with multiple sites + same player name -Eric que = self.sql.query['getPlayerId'] + " AND siteId=%d" % siteids[site] self.cursor.execute(que, (heroes[site],)) result = self.db.cursor.fetchall() if len(result) == 1: playerids.append(result[0][0]) if not sitenos: #Should probably pop up here. print "No sites selected - defaulting to PokerStars" sitenos = [2] if not playerids: print "No player ids found" return if not limits: print "No limits found" return self.createStatsTable(vbox, playerids, sitenos, limits, type, seats, groups, dates) def createStatsTable(self, vbox, playerids, sitenos, limits, type, seats, groups, dates): starttime = time() # Scrolled window for summary table swin = gtk.ScrolledWindow(hadjustment=None, vadjustment=None) swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) swin.show() vbox.pack1(swin) # Display summary table at top of page # 3rd parameter passes extra flags, currently includes: # holecards - whether to display card breakdown (True/False) # numhands - min number hands required when displaying all players flags = [False, self.filters.getNumHands()] self.addTable(swin, 'playerDetailedStats', flags, playerids, sitenos, limits, type, seats, groups, dates) # Separator vbox2 = gtk.VBox(False, 0) heading = gtk.Label(self.filterText['handhead']) heading.show() vbox2.pack_start(heading, expand=False, padding=3) # Scrolled window for detailed table (display by hand) swin = gtk.ScrolledWindow(hadjustment=None, vadjustment=None) swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) swin.show() vbox2.pack_start(swin, expand=True, padding=3) vbox.pack2(vbox2) vbox2.show() # Detailed table flags[0] = True self.addTable(swin, 'playerDetailedStats', flags, playerids, sitenos, limits, type, seats, groups, dates) self.db.rollback() print "Stats page displayed in %4.2f seconds" % (time() - starttime) #end def fillStatsFrame(self, vbox): def reset_style_render_func(self, treeviewcolumn, cell, model, iter): cell.set_property('foreground', 'black') def ledger_style_render_func(self, tvcol, cell, model, iter): str = cell.get_property('text') if '-' in str: str = str.replace("-", "") str = "(%s)" %(str) cell.set_property('text', str) cell.set_property('foreground', 'red') else: cell.set_property('foreground', 'darkgreen') return def sortnums(self, model, iter1, iter2, n): try: ret = 0 a = self.liststore.get_value(iter1, n) b = self.liststore.get_value(iter2, n) if 'f' in self.cols_to_show[n][4]: try: a = float(a) except: a = 0.0 try: b = float(b) except: b = 0.0 if n == 0: a1,a2,a3 = ranks[a[0]], ranks[a[1]], (a+'o')[2] b1,b2,b3 = ranks[b[0]], ranks[b[1]], (b+'o')[2] if a1 > b1 or ( a1 == b1 and (a2 > b2 or (a2 == b2 and a3 > b3) ) ): ret = 1 else: ret = -1 else: if a < b: ret = -1 elif a == b: ret = 0 else: ret = 1 #print "n =", n, "iter1[n] =", self.liststore.get_value(iter1,n), "iter2[n] =", self.liststore.get_value(iter2,n), "ret =", ret except: err = traceback.extract_tb(sys.exc_info()[2]) print "***sortnums error: " + str(sys.exc_info()[1]) print "\n".join( [e[0]+':'+str(e[1])+" "+e[2] for e in err] ) return(ret) def sortcols(self, col, n): try: #This doesn't actually work yet - clicking heading in top section sorts bottom section :-( if col.get_sort_order() == gtk.SORT_ASCENDING: col.set_sort_order(gtk.SORT_DESCENDING) else: col.set_sort_order(gtk.SORT_ASCENDING) self.liststore.set_sort_column_id(n, col.get_sort_order()) self.liststore.set_sort_func(n, self.sortnums, n) except: err = traceback.extract_tb(sys.exc_info()[2]) print "***sortcols error: " + str(sys.exc_info()[1]) print "\n".join( [e[0]+':'+str(e[1])+" "+e[2] for e in err] ) def addTable(self, vbox, query, flags, playerids, sitenos, limits, type, seats, groups, dates): counter = 0 row = 0 sqlrow = 0 if not flags: holecards = False else: holecards = flags[0] tmp = self.sql.query[query] tmp = self.refineQuery(tmp, flags, playerids, sitenos, limits, type, seats, groups, dates) self.cursor.execute(tmp) result = self.cursor.fetchall() colnames = [desc[0].lower() for desc in self.cursor.description] # pre-fetch some constant values: self.cols_to_show = [x for x in self.columns if x[colshow]] hgametypeid_idx = colnames.index('hgametypeid') self.liststore = gtk.ListStore(*([str] * len(self.cols_to_show))) view = gtk.TreeView(model=self.liststore) view.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_BOTH) #vbox.pack_start(view, expand=False, padding=3) vbox.add(view) textcell = gtk.CellRendererText() textcell50 = gtk.CellRendererText() textcell50.set_property('xalign', 0.5) numcell = gtk.CellRendererText() numcell.set_property('xalign', 1.0) listcols = [] # Create header row eg column: ("game", True, "Game", 0.0, "%s") for col, column in enumerate(self.cols_to_show): if column[colalias] == 'game' and holecards: s = [x for x in self.columns if x[colalias] == 'hand'][0][colheading] else: s = column[colheading] listcols.append(gtk.TreeViewColumn(s)) view.append_column(listcols[col]) if column[colformat] == '%s': if column[colxalign] == 0.0: listcols[col].pack_start(textcell, expand=True) listcols[col].add_attribute(textcell, 'text', col) else: listcols[col].pack_start(textcell50, expand=True) listcols[col].add_attribute(textcell50, 'text', col) listcols[col].set_expand(True) else: listcols[col].pack_start(numcell, expand=True) listcols[col].add_attribute(numcell, 'text', col) listcols[col].set_expand(True) #listcols[col].set_alignment(column[colxalign]) # no effect? if column[coltype] == 'cash': listcols[col].set_clickable(True) listcols[col].set_sort_indicator(True) listcols[col].connect("clicked", self.sortcols, col) listcols[col].set_cell_data_func(numcell, self.ledger_style_render_func) else: listcols[col].set_cell_data_func(numcell, self.reset_style_render_func) rows = len(result) # +1 for title row while sqlrow < rows: treerow = [] for col,column in enumerate(self.cols_to_show): if column[colalias] in colnames: value = result[sqlrow][colnames.index(column[colalias])] if column[colalias] == 'plposition': if value == 'B': value = 'BB' elif value == 'S': value = 'SB' elif value == '0': value = 'Btn' else: if column[colalias] == 'game': if holecards: value = Card.twoStartCardString( result[sqlrow][hgametypeid_idx] ) else: minbb = result[sqlrow][colnames.index('minbigblind')] maxbb = result[sqlrow][colnames.index('maxbigblind')] value = result[sqlrow][colnames.index('limittype')] + ' ' \ + result[sqlrow][colnames.index('category')].title() + ' ' \ + result[sqlrow][colnames.index('name')] + ' $' if 100 * int(minbb/100.0) != minbb: value += '%.2f' % (minbb/100.0) else: value += '%.0f' % (minbb/100.0) if minbb != maxbb: if 100 * int(maxbb/100.0) != maxbb: value += ' - $' + '%.2f' % (maxbb/100.0) else: value += ' - $' + '%.0f' % (maxbb/100.0) else: continue if value and value != -999: treerow.append(column[colformat] % value) else: treerow.append(' ') iter = self.liststore.append(treerow) sqlrow += 1 row += 1 vbox.show_all() #end def addTable(self, query, vars, playerids, sitenos, limits, type, seats, groups, dates): def refineQuery(self, query, flags, playerids, sitenos, limits, type, seats, groups, dates): having = '' if not flags: holecards = False numhands = 0 else: holecards = flags[0] numhands = flags[1] if 'allplayers' in groups and groups['allplayers']: nametest = "(hp.playerId)" if holecards or groups['posn']: pname = "'all players'" # set flag in self.columns to not show player name column [x for x in self.columns if x[0] == 'pname'][0][1] = False # can't do this yet (re-write doing more maths in python instead of sql?) if numhands: nametest = "(-1)" else: pname = "p.name" # set flag in self.columns to show player name column [x for x in self.columns if x[0] == 'pname'][0][1] = True if numhands: having = ' and count(1) > %d ' % (numhands,) else: if playerids: nametest = str(tuple(playerids)) nametest = nametest.replace("L", "") nametest = nametest.replace(",)",")") else: nametest = "1 = 2" pname = "p.name" # set flag in self.columns to not show player name column [x for x in self.columns if x[0] == 'pname'][0][1] = False query = query.replace("", nametest) query = query.replace("", pname) query = query.replace("", having) if seats: query = query.replace('', 'between ' + str(seats['from']) + ' and ' + str(seats['to'])) if 'show' in seats and seats['show']: query = query.replace('', ',h.seats') query = query.replace('', ',h.seats') else: query = query.replace('', '') query = query.replace('', '') else: query = query.replace('', 'between 0 and 100') query = query.replace('', '') query = query.replace('', '') lims = [int(x) for x in limits if x.isdigit()] nolims = [int(x[0:-2]) for x in limits if len(x) > 2 and x[-2:] == 'nl'] bbtest = "and ( (gt.limitType = 'fl' and gt.bigBlind in " # and ( (limit and bb in()) or (nolimit and bb in ()) ) if lims: blindtest = str(tuple(lims)) blindtest = blindtest.replace("L", "") blindtest = blindtest.replace(",)",")") bbtest = bbtest + blindtest + ' ) ' else: bbtest = bbtest + '(-1) ) ' bbtest = bbtest + " or (gt.limitType = 'nl' and gt.bigBlind in " if nolims: blindtest = str(tuple(nolims)) blindtest = blindtest.replace("L", "") blindtest = blindtest.replace(",)",")") bbtest = bbtest + blindtest + ' ) )' else: bbtest = bbtest + '(-1) ) )' if type == 'ring': bbtest = bbtest + " and gt.type = 'ring' " elif type == 'tour': bbtest = bbtest + " and gt.type = 'tour' " query = query.replace("", bbtest) if holecards: # re-use level variables for hole card query query = query.replace("", "hp.startcards") query = query.replace("" , ",case when floor(hp.startcards/13) >= mod(hp.startcards,13) then hp.startcards + 0.1 " + " else 13*mod(hp.startcards,13) + floor(hp.startcards/13) " + " end desc ") else: query = query.replace("", "") groupLevels = "show" not in str(limits) if groupLevels: query = query.replace("", "-1") else: query = query.replace("", "h.gameTypeId") # process self.detailFilters (a list of tuples) flagtest = '' #self.detailFilters = [('h.seats', 5, 6)] # for debug if self.detailFilters: for f in self.detailFilters: if len(f) == 3: # X between Y and Z flagtest += ' and %s between %s and %s ' % (f[0], str(f[1]), str(f[2])) query = query.replace("", flagtest) # allow for differences in sql cast() function: if self.db.backend == self.MYSQL_INNODB: query = query.replace("", 'signed ') else: query = query.replace("", '') # Filter on dates query = query.replace("", " between '" + dates[0] + "' and '" + dates[1] + "'") # Group by position? if groups['posn']: #query = query.replace("", "case hp.position when '0' then 'Btn' else hp.position end") query = query.replace("", "hp.position") # set flag in self.columns to show posn column [x for x in self.columns if x[0] == 'plposition'][0][1] = True else: query = query.replace("", "'1'") # unset flag in self.columns to hide posn column [x for x in self.columns if x[0] == 'plposition'][0][1] = False #print "query =\n", query return(query) #end def refineQuery(self, query, playerids, sitenos, limits): def showDetailFilter(self, widget, data): detailDialog = gtk.Dialog(title="Detailed Filters", parent=self.main_window ,flags=gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT ,buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) handbox = gtk.VBox(True, 0) detailDialog.vbox.pack_start(handbox, False, False, 0) handbox.show() label = gtk.Label("Hand Filters:") handbox.add(label) label.show() betweenFilters = [] for htest in self.handtests: hbox = gtk.HBox(False, 0) handbox.pack_start(hbox, False, False, 0) hbox.show() cb = gtk.CheckButton() lbl_from = gtk.Label(htest[1]) lbl_from.set_alignment(xalign=0.0, yalign=0.5) lbl_tween = gtk.Label('between') lbl_to = gtk.Label('and') adj1 = gtk.Adjustment(value=htest[2], lower=0, upper=10, step_incr=1, page_incr=1, page_size=0) sb1 = gtk.SpinButton(adjustment=adj1, climb_rate=0.0, digits=0) adj2 = gtk.Adjustment(value=htest[3], lower=2, upper=10, step_incr=1, page_incr=1, page_size=0) sb2 = gtk.SpinButton(adjustment=adj2, climb_rate=0.0, digits=0) for df in [x for x in self.detailFilters if x[0] == htest[0]]: cb.set_active(True) hbox.pack_start(cb, expand=False, padding=3) hbox.pack_start(lbl_from, expand=True, padding=3) hbox.pack_start(lbl_tween, expand=False, padding=3) hbox.pack_start(sb1, False, False, 0) hbox.pack_start(lbl_to, expand=False, padding=3) hbox.pack_start(sb2, False, False, 0) cb.show() lbl_from.show() lbl_tween.show() sb1.show() lbl_to.show() sb2.show() htest[4:7] = [cb,sb1,sb2] response = detailDialog.run() if response == gtk.RESPONSE_ACCEPT: self.detailFilters = [] for ht in self.handtests: if ht[4].get_active(): self.detailFilters.append( (ht[0], ht[5].get_value_as_int(), ht[6].get_value_as_int()) ) ht[2],ht[3] = ht[5].get_value_as_int(), ht[6].get_value_as_int() print "detailFilters =", self.detailFilters self.refreshStats(None, None) detailDialog.destroy()