From c716dfd35eb9adbadfafc46cd6d07897c1fe19f9 Mon Sep 17 00:00:00 2001 From: steffen123 Date: Mon, 18 Aug 2008 23:53:25 +0100 Subject: [PATCH] p58 - added HUD from ray fpdb_parse now returns hand id rather than site hand no --- docs/known-bugs-and-planned-features.txt | 1 + pyfpdb/Cards01.png | Bin 0 -> 78351 bytes pyfpdb/Configuration.py | 211 ++++++++++++++++++ pyfpdb/Database.py | 156 ++++++++++++++ pyfpdb/GuiAutoImport.py | 2 +- pyfpdb/HUD_config.xml.example | 120 +++++++++++ pyfpdb/HUD_main.py | 94 ++++++++ pyfpdb/HandHistory.py | 194 +++++++++++++++++ pyfpdb/Hud.py | 189 ++++++++++++++++ pyfpdb/Mucked.py | 241 +++++++++++++++++++++ pyfpdb/SQL.py | 262 +++++++++++++++++++++++ pyfpdb/Stats.py | 186 ++++++++++++++++ pyfpdb/Tables.py | 135 ++++++++++++ pyfpdb/fpdb_import.py | 8 +- pyfpdb/fpdb_save_to_db.py | 9 +- 15 files changed, 1798 insertions(+), 10 deletions(-) create mode 100644 pyfpdb/Cards01.png create mode 100644 pyfpdb/Configuration.py create mode 100644 pyfpdb/Database.py create mode 100644 pyfpdb/HUD_config.xml.example create mode 100755 pyfpdb/HUD_main.py create mode 100644 pyfpdb/HandHistory.py create mode 100644 pyfpdb/Hud.py create mode 100644 pyfpdb/Mucked.py create mode 100644 pyfpdb/SQL.py create mode 100644 pyfpdb/Stats.py create mode 100644 pyfpdb/Tables.py diff --git a/docs/known-bugs-and-planned-features.txt b/docs/known-bugs-and-planned-features.txt index fec10458..f6ae60dc 100644 --- a/docs/known-bugs-and-planned-features.txt +++ b/docs/known-bugs-and-planned-features.txt @@ -4,6 +4,7 @@ Everything is subject to change and especially the order will often change. Patc alpha2 (release by 17Aug) ====== make windows use correct language version of Appdata, e.g. Anwendungdaten +stop bulk importer from executing HUD hook seperate and improve instructions for update update status or make a support matrix table for website add instructions for mailing list to contacts diff --git a/pyfpdb/Cards01.png b/pyfpdb/Cards01.png new file mode 100644 index 0000000000000000000000000000000000000000..2c64fc9e756a7d6b42c5dfb266c3a8a9d03c6f3d GIT binary patch literal 78351 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

kWJ$iiI|uNS6ouFw^0IhVcnVp!6N3 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 Jr(_fLe*jAO1TX*q literal 0 HcmV?d00001 diff --git a/pyfpdb/Configuration.py b/pyfpdb/Configuration.py new file mode 100644 index 00000000..2ade6bee --- /dev/null +++ b/pyfpdb/Configuration.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python +"""Configuration.py + +Handles HUD configuration files. +""" +# Copyright 2008, Ray E. Barker + +# +# 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 + +######################################################################## + +# Standard Library modules +import xml.dom.minidom +from xml.dom.minidom import Node + +class Layout: + def __init__(self, max): + self.max = int(max) + self.location = [] + for i in range(self.max + 1): self.location.append(None) + + def __str__(self): + temp = " Layout = %d max, width= %d, height = %d, fav_seat = %d\n" % (self.max, self.width, self.height, self.fav_seat) + temp = temp + " Locations = " + for i in range(1, len(self.location)): + temp = temp + "(%d,%d)" % self.location[i] + + return temp + +class Site: + def __init__(self, node): + self.site_name = node.getAttribute("site_name") + self.table_finder = node.getAttribute("table_finder") + self.screen_name = node.getAttribute("screen_name") + self.site_path = node.getAttribute("site_path") + self.HH_path = node.getAttribute("HH_path") + self.decoder = node.getAttribute("decoder") + self.layout = {} + + for layout_node in node.getElementsByTagName('layout'): + max = int( layout_node.getAttribute('max') ) + lo = Layout(max) + lo.fav_seat = int( layout_node.getAttribute('fav_seat') ) + lo.width = int( layout_node.getAttribute('width') ) + lo.height = int( layout_node.getAttribute('height') ) + + for location_node in layout_node.getElementsByTagName('location'): + lo.location[int( location_node.getAttribute('seat') )] = (int( location_node.getAttribute('x') ), int( location_node.getAttribute('y'))) + + self.layout[lo.max] = lo + + def __str__(self): + temp = "Site = " + self.site_name + "\n" + for key in dir(self): + if key.startswith('__'): continue + if key == 'layout': continue + value = getattr(self, key) + if callable(value): continue + temp = temp + ' ' + key + " = " + value + "\n" + + for layout in self.layout: + temp = temp + "%s" % self.layout[layout] + + return temp + +class Stat: + def __init__(self): + pass + + def __str__(self): + temp = " stat_name = %s, row = %d, col = %d, tip = %s, click = %s\n" % (self.stat_name, self.row, self.col, self.tip, self.click) + return temp + +class Game: + def __init__(self, node): + self.game_name = node.getAttribute("game_name") + self.db = node.getAttribute("db") + self.rows = int( node.getAttribute("rows") ) + self.cols = int( node.getAttribute("cols") ) + + self.stats = {} + for stat_node in node.getElementsByTagName('stat'): + stat = Stat() + stat.stat_name = stat_node.getAttribute("stat_name") + stat.row = int( stat_node.getAttribute("row") ) + stat.col = int( stat_node.getAttribute("col") ) + stat.tip = stat_node.getAttribute("tip") + stat.click = stat_node.getAttribute("click") + + self.stats[stat.stat_name] = stat + + def __str__(self): + temp = "Game = " + self.game_name + "\n" + temp = temp + " db = %s\n" % self.db + temp = temp + " rows = %d\n" % self.rows + temp = temp + " cols = %d\n" % self.cols + + for stat in self.stats.keys(): + temp = temp + "%s" % self.stats[stat] + + return temp + +class Database: + def __init__(self, node): + self.db_name = node.getAttribute("db_name") + self.db_server = node.getAttribute("db_server") + self.db_ip = node.getAttribute("db_ip") + self.db_user = node.getAttribute("db_user") + self.db_type = node.getAttribute("db_type") + self.db_pass = node.getAttribute("db_pass") + + def __str__(self): + temp = 'Database = ' + self.db_name + '\n' + for key in dir(self): + if key.startswith('__'): continue + value = getattr(self, key) + if callable(value): continue + temp = temp + ' ' + key + " = " + value + "\n" + return temp + +class Mucked: + def __init__(self, node): + self.name = node.getAttribute("mw_name") + self.cards = node.getAttribute("deck") + self.card_wd = node.getAttribute("card_wd") + self.card_ht = node.getAttribute("card_ht") + self.rows = node.getAttribute("rows") + self.cols = node.getAttribute("cols") + self.format = node.getAttribute("stud") + print "mw name = " + self.name + + def __str__(self): + temp = 'Mucked = ' + self.name + "\n" + for key in dir(self): + if key.startswith('__'): continue + value = getattr(self, key) + if callable(value): continue + temp = temp + ' ' + key + " = " + value + "\n" + return temp + +class Config: + def __init__(self, file = 'HUD_config.xml'): + + doc = xml.dom.minidom.parse(file) + + self.supported_sites = {} + self.supported_games = {} + self.supported_databases = {} + self.mucked_windows = {} + +# s_sites = doc.getElementsByTagName("supported_sites") + for site_node in doc.getElementsByTagName("site"): + site = Site(node = site_node) + self.supported_sites[site.site_name] = site + + s_games = doc.getElementsByTagName("supported_games") + for game_node in doc.getElementsByTagName("game"): + game = Game(node = game_node) + self.supported_games[game.game_name] = game + + s_dbs = doc.getElementsByTagName("supported_databases") + for db_node in doc.getElementsByTagName("database"): + db = Database(node = db_node) + self.supported_databases[db.db_name] = db + + s_dbs = doc.getElementsByTagName("mucked_windows") + for mw_node in doc.getElementsByTagName("mw"): + mw = Mucked(node = mw_node) + self.mucked_windows[mw.name] = mw + +if __name__== "__main__": + c = Config() + + print "\n----------- SUPPORTED SITES -----------" + for s in c.supported_sites.keys(): + print c.supported_sites[s] + + print "----------- END SUPPORTED SITES -----------" + + + print "\n----------- SUPPORTED GAMES -----------" + for game in c.supported_games.keys(): + print c.supported_games[game] + + print "----------- END SUPPORTED GAMES -----------" + + + print "\n----------- SUPPORTED DATABASES -----------" + for db in c.supported_databases.keys(): + print c.supported_databases[db] + + print "----------- END SUPPORTED DATABASES -----------" + + print "\n----------- MUCKED WINDOW FORMATS -----------" + for w in c.mucked_windows.keys(): + print c.mucked_windows[w] + + print "----------- END MUCKED WINDOW FORMATS -----------" diff --git a/pyfpdb/Database.py b/pyfpdb/Database.py new file mode 100644 index 00000000..0d6db697 --- /dev/null +++ b/pyfpdb/Database.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python +"""Database.py + +Create and manage the database objects. +""" +# Copyright 2008, Ray E. Barker +# +# 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 + +######################################################################## + +# postmaster -D /var/lib/pgsql/data + +# Standard Library modules + +# pyGTK modules + +# FreePokerTools modules +import Configuration +import SQL + +# pgdb database module for posgres via DB-API +#import pgdb +# pgdb uses pyformat. is that fixed or an option? + +# mysql bindings +import MySQLdb + +class Database: + def __init__(self, c, db_name, game): + print "db_name = " + db_name + if c.supported_databases[db_name].db_server == 'postgresql': + self.connection = pgdb.connect(dsn = c.supported_databases[db_name].db_ip, + user = c.supported_databases[db_name].db_user, + password = c.supported_databases[db_name].db_pass, + database = c.supported_databases[db_name].db_name) + + elif c.supported_databases[db_name].db_server == 'mysql': + self.connection = MySQLdb.connect(host = c.supported_databases[db_name].db_ip, + user = c.supported_databases[db_name].db_user, + passwd = c.supported_databases[db_name].db_pass, + db = c.supported_databases[db_name].db_name) + + else: + print "Database not recognized." + return(0) + + self.type = c.supported_databases[db_name].db_type + self.sql = SQL.Sql(game = game, type = self.type) + + def close(self): + self.connection.close + + def get_table_name(self, hand_id): + c = self.connection.cursor() + c.execute(self.sql.query['get_table_name'], (hand_id)) + row = c.fetchone() + return row + + def get_last_hand(self): + c = self.connection.cursor() + c.execute(self.sql.query['get_last_hand']) + row = c.fetchone() + return row[0] + + def get_xml(self, hand_id): + c = self.connection.cursor() + c.execute(self.sql.query['get_xml'], (hand_id)) + row = c.fetchone() + return row[0] + + def get_recent_hands(self, last_hand): + c = self.connection.cursor() + c.execute(self.sql.query['get_recent_hands'], {'last_hand': last_hand}) + return c.fetchall() + + def get_hand_info(self, new_hand_id): + c = self.connection.cursor() + c.execute(self.sql.query['get_hand_info'], new_hand_id) + return c.fetchall() + + def get_cards(self, hand): + c = self.connection.cursor() + c.execute(self.sql.query['get_cards'], hand) + colnames = [desc[0] for desc in c.description] + cards = {} + for row in c.fetchall(): + s_dict = {} + for name, val in zip(colnames, row): + s_dict[name] = val + cards[s_dict['seat_number']] = s_dict + return (cards) + + def get_stats_from_hand(self, hand, hero): + c = self.connection.cursor() + +# get the players in the hand and their seats + c.execute(self.sql.query['get_players_from_hand'], (hand)) + names = {} + seats = {} + for row in c.fetchall(): + names[row[0]] = row[2] + seats[row[0]] = row[1] + +# now get the stats + c.execute(self.sql.query['get_stats_from_hand'], (hand, hand)) + colnames = [desc[0] for desc in c.description] + stat_dict = {} + for row in c.fetchall(): + t_dict = {} + for name, val in zip(colnames, row): + t_dict[name] = val +# print t_dict + t_dict['screen_name'] = names[t_dict['player_id']] + t_dict['seat'] = seats[t_dict['player_id']] + stat_dict[t_dict['player_id']] = t_dict + return stat_dict + + def get_player_id(self, config, site, player_name): + print "site = %s, player name = %s" % (site, player_name) + c = self.connection.cursor() + c.execute(self.sql.query['get_player_id'], {'player': player_name, 'site': site}) + row = c.fetchone() + return row[0] + +if __name__=="__main__": + c = Configuration.Config() + + db_connection = Database(c, 'fpdb', 'holdem') # mysql fpdb holdem +# db_connection = Database(c, 'PTrackSv2', 'razz') # mysql razz +# db_connection = Database(c, 'ptracks', 'razz') # postgres + print "database connection object = ", db_connection.connection + print "database type = ", db_connection.type + + h = db_connection.get_last_hand() + print "last hand = ", h + + hero = db_connection.get_player_id(c, 'PokerStars', 'nutOmatic') + print "nutOmatic is id_player = %d" % hero + + stat_dict = db_connection.get_stats_from_hand(h, hero) + for p in stat_dict.keys(): + print p, " ", stat_dict[p] + db_connection.close diff --git a/pyfpdb/GuiAutoImport.py b/pyfpdb/GuiAutoImport.py index 3d345c29..af34ad58 100644 --- a/pyfpdb/GuiAutoImport.py +++ b/pyfpdb/GuiAutoImport.py @@ -55,7 +55,7 @@ class GuiAutoImport (threading.Thread): else: self.inputFile=self.path+os.sep+file fpdb_import.import_file_dict(self, self.settings) - print "GuiBulkImport.import_dir done" + print "GuiAutoImport.import_dir done" interval=int(self.intervalTBuffer.get_text(self.intervalTBuffer.get_start_iter(), self.intervalTBuffer.get_end_iter())) time.sleep(interval) diff --git a/pyfpdb/HUD_config.xml.example b/pyfpdb/HUD_config.xml.example new file mode 100644 index 00000000..7d76688f --- /dev/null +++ b/pyfpdb/HUD_config.xml.example @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pyfpdb/HUD_main.py b/pyfpdb/HUD_main.py new file mode 100755 index 00000000..1f71625b --- /dev/null +++ b/pyfpdb/HUD_main.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python + +"""Hud_main.py + +Main for FreePokerTools HUD. +""" +# Copyright 2008, Ray E. Barker +# +# 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 kill window on my seat +# to do adjust for preferred seat +# to do allow window resizing +# to do hud to echo, but ignore non numbers +# to do kill a hud +# to do no hud window for hero +# to do single click to display detailed stats +# to do things to add to config.xml +# to do font and size + +# Standard Library modules +import sys +import os + +# pyGTK modules +import pygtk +import gtk +import gobject + +# FreePokerTools modules +import Configuration +import Database +import Tables +import Hud + +# global dict for keeping the huds +hud_dict = {} +db_connection = 0; +config = 0; + +def destroy(*args): # call back for terminating the main eventloop + gtk.main_quit() + +def process_new_hand(source, condition): +# there is a new hand_id to be processed +# read the hand_id from stdin and strip whitespace + new_hand_id = sys.stdin.readline() + new_hand_id = new_hand_id.rstrip() + db_connection = Database.Database(config, 'fpdb', 'holdem') + + (table_name, max, poker_game) = db_connection.get_table_name(new_hand_id) +# if a hud for this table exists, just update it + if hud_dict.has_key(table_name): + hud_dict[table_name].update(new_hand_id, db_connection, config) +# otherwise create a new hud + else: + table_windows = Tables.discover(config) + for t in table_windows.keys(): + if table_windows[t].name == table_name: + hud_dict[table_name] = Hud.Hud(table_windows[t], max, poker_game, config, db_connection) + hud_dict[table_name].create(new_hand_id, config) + hud_dict[table_name].update(new_hand_id, db_connection, config) + break +# print "table name \"%s\" not identified, no hud created" % (table_name) + return(1) + +if __name__== "__main__": + main_window = gtk.Window() + main_window.connect("destroy", destroy) + label = gtk.Label('Fake main window, blah blah, blah\nblah, blah') + main_window.add(label) + main_window.show_all() + + config = Configuration.Config() + + db_connection = Database.Database(config, 'fpdb', 'holdem') + + s_id = gobject.io_add_watch(sys.stdin, gobject.IO_IN, process_new_hand) + + gtk.main() diff --git a/pyfpdb/HandHistory.py b/pyfpdb/HandHistory.py new file mode 100644 index 00000000..dd2fa427 --- /dev/null +++ b/pyfpdb/HandHistory.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python +"""HandHistory.py + +Parses HandHistory xml files and returns requested objects. +""" +# Copyright 2008, Ray E. Barker +# +# 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 + +######################################################################## +# Standard Library modules +import xml.dom.minidom +from xml.dom.minidom import Node + +class HandHistory: + def __init__(self, xml_string, elements = ('ALL')): + + doc = xml.dom.minidom.parseString(xml_string) + if elements == ('ALL'): + elements = ('BETTING', 'AWARDS', 'POSTS', 'PLAYERS', 'GAME') + + if 'BETTING' in elements: + self.BETTING = Betting(doc.getElementsByTagName('BETTING')[0]) + if 'AWARDS' in elements: + self.AWARDS = Awards (doc.getElementsByTagName('AWARDS')[0]) + if 'POSTS' in elements: + self.POSTS = Posts (doc.getElementsByTagName('POSTS')[0]) + if 'GAME' in elements: + self.GAME = Game (doc.getElementsByTagName('GAME')[0]) + if 'PLAYERS' in elements: + self.PLAYERS = {} + p_n = doc.getElementsByTagName('PLAYERS')[0] + for p in p_n.getElementsByTagName('PLAYER'): + a_player = Player(p) + self.PLAYERS[a_player.name] = a_player + +class Player: + def __init__(self, node): + self.name = node.getAttribute('NAME') + self.seat = node.getAttribute('SEAT') + self.stack = node.getAttribute('STACK') + self.showed_hand = node.getAttribute('SHOWED_HAND') + self.cards = node.getAttribute('CARDS') + self.allin = node.getAttribute('ALLIN') + self.sitting_out = node.getAttribute('SITTING_OUT') + self.hand = node.getAttribute('HAND') + self.start_cards = node.getAttribute('START_CARDS') + + if self.allin == '' or \ + self.allin == '0' or \ + self.allin.upper() == 'FALSE': self.allin = False + else: self.allin = True + + if self.sitting_out == '' or \ + self.sitting_out == '0' or \ + self.sitting_out.upper() == 'FALSE': self.sitting_out = False + else: self.sitting_out = True + + def __str__(self): + temp = "%s\n seat = %s\n stack = %s\n cards = %s\n" % \ + (self.name, self.seat, self.stack, self.cards) + temp = temp + " showed_hand = %s\n allin = %s\n" % \ + (self.showed_hand, self.allin) + temp = temp + " hand = %s\n start_cards = %s\n" % \ + (self.hand, self.start_cards) + return temp + +class Awards: + def __init__(self, node): + self.awards = [] # just an array of award objects + for a in node.getElementsByTagName('AWARD'): + self.awards.append(Award(a)) + + def __str__(self): + temp = "" + for a in self.awards: + temp = temp + "%s\n" % (a) + return temp + +class Award: + def __init__(self, node): + self.player = node.getAttribute('PLAYER') + self.amount = node.getAttribute('AMOUNT') + self.pot = node.getAttribute('POT') + + def __str__(self): + return self.player + " won " + self.amount + " from " + self.pot + +class Game: + def __init__(self, node): + print node + self.tags = {} + for tag in ( ('GAME_NAME', 'game_name'), ('MAX', 'max'), ('HIGHLOW', 'high_low'), + ('STRUCTURE', 'structure'), ('MIXED', 'mixed') ): + L = node.getElementsByTagName(tag[0]) + if (not L): continue + print L + for node2 in L: + title = "" + for node3 in node2.childNodes: + if (node3.nodeType == Node.TEXT_NODE): + title +=node3.data + self.tags[tag[1]] = title + + def __str__(self): + return "%s %s %s, (%s max), %s" % (self.tags['structure'], + self.tags['game_name'], + self.tags['game_name'], + self.tags['max'], + self.tags['game_name']) + +class Posts: + def __init__(self, node): + self.posts = [] # just an array of post objects + for p in node.getElementsByTagName('POST'): + self.posts.append(Post(p)) + + def __str__(self): + temp = "" + for p in self.posts: + temp = temp + "%s\n" % (p) + return temp + +class Post: + def __init__(self, node): + self.player = node.getAttribute('PLAYER') + self.amount = node.getAttribute('AMOUNT') + self.posted = node.getAttribute('POSTED') + self.live = node.getAttribute('LIVE') + + def __str__(self): + return ("%s posted %s %s %s") % (self.player, self.amount, self.posted, self.live) + +class Betting: + def __init__(self, node): + self.rounds = [] # a Betting object is just an array of rounds + for r in node.getElementsByTagName('ROUND'): + self.rounds.append(Round(r)) + + def __str__(self): + temp = "" + for r in self.rounds: + temp = temp + "%s\n" % (r) + return temp + +class Round: + def __init__(self, node): + self.name = node.getAttribute('ROUND_NAME') + self.action = [] + for a in node.getElementsByTagName('ACTION'): + self.action.append(Action(a)) + + def __str__(self): + temp = self.name + "\n" + for a in self.action: + temp = temp + " %s\n" % (a) + return temp + +class Action: + def __init__(self, node): + self.player = node.getAttribute('PLAYER') + self.action = node.getAttribute('ACT') + self.amount = node.getAttribute('AMOUNT') + self.allin = node.getAttribute('ALLIN') + + def __str__(self): + return self.player + " " + self.action + " " + self.amount + " " + self.allin + +if __name__== "__main__": + file = open('test.xml', 'r') + xml_string = file.read() + file.close() + + print xml_string + "\n\n\n" + h = HandHistory(xml_string, ('ALL')) + print h.GAME + print h.POSTS + print h.BETTING + print h.AWARDS + + for p in h.PLAYERS.keys(): + print h.PLAYERS[p] \ No newline at end of file diff --git a/pyfpdb/Hud.py b/pyfpdb/Hud.py new file mode 100644 index 00000000..9b35f283 --- /dev/null +++ b/pyfpdb/Hud.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python +"""Hud.py + +Create and manage the hud overlays. +""" +# Copyright 2008, Ray E. Barker + +# +# 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 + +######################################################################## +# Standard Library modules + +# pyGTK modules +import pygtk +import gtk +import pango +import gobject + +# FreePokerTools modules +import Tables # needed for testing only +import Configuration +import Stats +import Mucked +import Database + +class Hud: + + def __init__(self, table, max, poker_game, config, db_connection): + self.table = table + self.config = config + self.poker_game = poker_game + self.max = max + self.db_connection = db_connection + + self.stat_windows = {} + self.font = pango.FontDescription("Sans 8") + + + def create(self, hand, config): +# update this hud, to the stats and players as of "hand" +# hand is the hand id of the most recent hand played at this table +# +# this method also manages the creating and destruction of stat +# windows via calls to the Stat_Window class + for i in range(1, self.max + 1): + (x, y) = config.supported_sites[self.table.site].layout[self.max].location[i] + self.stat_windows[i] = Stat_Window(game = config.supported_games[self.poker_game], + table = self.table, + x = x, + y = y, + seat = i, + player_id = 'fake', + font = self.font) + + self.stats = [['', '', ''], ['', '', '']] + for stat in config.supported_games[self.poker_game].stats.keys(): + self.stats[config.supported_games[self.poker_game].stats[stat].row] \ + [config.supported_games[self.poker_game].stats[stat].col] = \ + config.supported_games[self.poker_game].stats[stat].stat_name + +# self.mucked_window = gtk.Window() +# self.m = Mucked.Mucked(self.mucked_window, self.db_connection) +# self.mucked_window.show_all() + + def update(self, hand, db, config): + stat_dict = db.get_stats_from_hand(hand, 3) + for s in stat_dict.keys(): + for r in range(0, 2): + for c in range(0, 3): + number = Stats.do_stat(stat_dict, player = stat_dict[s]['player_id'], stat = self.stats[r][c]) + self.stat_windows[stat_dict[s]['seat']].label[r][c].set_text(number[1]) + tip = stat_dict[s]['screen_name'] + "\n" + number[5] + "\n" + \ + number[3] + ", " + number[4] + Stats.do_tip(self.stat_windows[stat_dict[s]['seat']].e_box[r][c], tip) +# self.m.update(hand) + +class Stat_Window: + + def button_press_cb(self, widget, event, *args): +# This handles all callbacks from button presses on the event boxes in +# the stat windows. There is a bit of an ugly kludge to separate single- +# and double-clicks. + if event.button == 1: # left button event + if event.type == gtk.gdk.BUTTON_PRESS: # left button single click + if self.sb_click > 0: return + self.sb_click = gobject.timeout_add(250, self.single_click) + elif event.type == gtk.gdk._2BUTTON_PRESS: # left button double click + if self.sb_click > 0: + gobject.source_remove(self.sb_click) + self.sb_click = 0 + self.double_click(widget, event, *args) + + if event.button == 2: # middle button event + print "middle button clicked" + + if event.button == 3: # right button event + print "right button clicked" + + def single_click(self): +# Callback from the timeout in the single-click finding part of the +# button press call back. This needs to be modified to get all the +# arguments from the call. + print "left button clicked" + self.sb_click = 0 + return False + + def double_click(self, widget, event, *args): + self.toggle_decorated(widget) + + def toggle_decorated(self, widget): + top = widget.get_toplevel() + (x, y) = top.get_position() + + if top.get_decorated(): + top.set_decorated(0) + top.move(x, y) + else: + top.set_decorated(1) + top.move(x, y) + + def __init__(self, game, table, seat, x, y, player_id, font): + self.game = game + self.table = table + self.x = x + table.x + self.y = y + table.y + self.player_id = player_id + self.sb_click = 0 + + self.window = gtk.Window() + self.window.set_decorated(0) + self.window.set_gravity(gtk.gdk.GRAVITY_STATIC) + self.window.set_title("%s" % seat) + + self.grid = gtk.Table(rows = self.game.rows, columns = self.game.cols, homogeneous = False) + self.window.add(self.grid) + + self.e_box = [] + self.frame = [] + self.label = [] + for r in range(self.game.rows): + self.e_box.append([]) + self.label.append([]) + for c in range(self.game.cols): + self.e_box[r].append( gtk.EventBox() ) + Stats.do_tip(self.e_box[r][c], 'farts') + self.grid.attach(self.e_box[r][c], c, c+1, r, r+1, xpadding = 1, ypadding = 1) + self.label[r].append( gtk.Label('xxx') ) + self.e_box[r][c].add(self.label[r][c]) + self.e_box[r][c].connect("button_press_event", self.button_press_cb) +# font = pango.FontDescription("Sans 8") + self.label[r][c].modify_font(font) + self.window.realize + self.window.move(self.x, self.y) + self.window.set_keep_above(1) + self.window.show_all() + +def destroy(*args): # call back for terminating the main eventloop + gtk.main_quit() + +if __name__== "__main__": + main_window = gtk.Window() + main_window.connect("destroy", destroy) + label = gtk.Label('Fake main window, blah blah, blah\nblah, blah') + main_window.add(label) + main_window.show_all() + + c = Configuration.Config() + tables = Tables.discover(c) + db = Database.Database(c, 'PTrackSv2', 'razz') + + for t in tables: + win = Hud(t, 8, c, db) +# t.get_details() + win.update(8300, db, c) + + gtk.main() diff --git a/pyfpdb/Mucked.py b/pyfpdb/Mucked.py new file mode 100644 index 00000000..a1f76f87 --- /dev/null +++ b/pyfpdb/Mucked.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python +"""Mucked.py + +Mucked cards display for FreePokerTools HUD. +""" +# Copyright 2008, Ray E. Barker +# +# 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 +# problem with hand 30586 + +# Standard Library modules +import sys +import os +import string +import xml.dom.minidom +from xml.dom.minidom import Node + +# pyGTK modules +import pygtk +import gtk +import gobject + +# FreePokerTools modules +import Configuration +import Database +import Tables +import Hud +import Mucked +import HandHistory + +class Mucked: + def __init__(self, parent, db_connection): + + self.parent = parent #this is the parent of the mucked cards widget + self.db_connection = db_connection + + self.vbox = gtk.VBox() + self.parent.add(self.vbox) + + self.mucked_list = MuckedList (self.vbox, db_connection) + self.mucked_cards = MuckedCards(self.vbox, db_connection) + self.mucked_list.mucked_cards = self.mucked_cards + + def update(self, new_hand_id): + self.mucked_list.update(new_hand_id) + +class MuckedList: + def __init__(self, parent, db_connection): + + self.parent = parent + self.db_connection = db_connection + +# set up a scrolled window to hold the listbox + self.scrolled_window = gtk.ScrolledWindow() + self.scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS) + self.parent.add(self.scrolled_window) + +# create a ListStore to use as the model + self.liststore = gtk.ListStore(str, str, str) + self.treeview = gtk.TreeView(self.liststore) + self.tvcolumn0 = gtk.TreeViewColumn('HandID') + self.tvcolumn1 = gtk.TreeViewColumn('Cards') + self.tvcolumn2 = gtk.TreeViewColumn('Net') + +# add tvcolumn to treeview + self.treeview.append_column(self.tvcolumn0) + self.treeview.append_column(self.tvcolumn1) + self.treeview.append_column(self.tvcolumn2) + +# create a CellRendererText to render the data + self.cell = gtk.CellRendererText() + + # add the cell to the tvcolumn and allow it to expand + self.tvcolumn0.pack_start(self.cell, True) + self.tvcolumn1.pack_start(self.cell, True) + self.tvcolumn2.pack_start(self.cell, True) + self.tvcolumn0.add_attribute(self.cell, 'text', 0) + self.tvcolumn1.add_attribute(self.cell, 'text', 1) + self.tvcolumn2.add_attribute(self.cell, 'text', 2) +# resize the cols if nec + self.tvcolumn0.set_resizable(True) + self.treeview.connect("row-activated", self.activated_event) + + self.scrolled_window.add_with_viewport(self.treeview) + + def activated_event(self, path, column, data=None): + sel = self.treeview.get_selection() + (model, iter) = sel.get_selected() + self.mucked_cards.update(model.get_value(iter, 0)) + + def update(self, new_hand_id): + info_row = self.db_connection.get_hand_info(new_hand_id) + iter = self.liststore.append(info_row[0]) + sel = self.treeview.get_selection() + sel.select_iter(iter) + + vadj = self.scrolled_window.get_vadjustment() + vadj.set_value(vadj.upper) + self.mucked_cards.update(new_hand_id) + +class MuckedCards: + def __init__(self, parent, db_connection): + + self.parent = parent #this is the parent of the mucked cards widget + self.db_connection = db_connection + + self.card_images = self.get_card_images() + self.seen_cards = {} + self.grid_contents = {} + self.eb = {} + + self.rows = 8 + self.cols = 7 + self.grid = gtk.Table(self.rows, self.cols + 4, homogeneous = False) + + for r in range(0, self.rows): + for c in range(0, self.cols): + self.seen_cards[(c, r)] = gtk.image_new_from_pixbuf(self.card_images[('B', 'S')]) + self.eb[(c, r)]= gtk.EventBox() + +# set up the contents for the cells + for r in range(0, self.rows): + self.grid_contents[( 0, r)] = gtk.Label("%d" % (r + 1)) + self.grid_contents[( 1, r)] = gtk.Label("player %d" % (r + 1)) + self.grid_contents[( 4, r)] = gtk.Label("-") + self.grid_contents[( 9, r)] = gtk.Label("-") + self.grid_contents[( 2, r)] = self.eb[( 0, r)] + self.grid_contents[( 3, r)] = self.eb[( 1, r)] + self.grid_contents[( 5, r)] = self.eb[( 2, r)] + self.grid_contents[( 6, r)] = self.eb[( 3, r)] + self.grid_contents[( 7, r)] = self.eb[( 4, r)] + self.grid_contents[( 8, r)] = self.eb[( 5, r)] + self.grid_contents[(10, r)] = self.eb[( 6, r)] + for c in range(0, self.cols): + self.eb[(c, r)].add(self.seen_cards[(c, r)]) + +# add the cell contents to the table + for c in range(0, self.cols + 4): + for r in range(0, self.rows): + self.grid.attach(self.grid_contents[(c, r)], c, c+1, r, r+1, xpadding = 1, ypadding = 1) + + self.parent.add(self.grid) + + def update(self, new_hand_id): + cards = self.db_connection.get_cards(new_hand_id) + self.clear() + + for c in cards.keys(): + self.grid_contents[(1, cards[c]['seat_number'] - 1)].set_text(cards[c]['screen_name']) + + for i in ((0, 'hole_card_1'), (1, 'hole_card_2'), (2, 'hole_card_3'), (3, 'hole_card_4'), + (4, 'hole_card_5'), (5, 'hole_card_6'), (6, 'hole_card_7')): + if not cards[c][i[1]] == "": + self.seen_cards[(i[0], cards[c]['seat_number'] - 1)]. \ + set_from_pixbuf(self.card_images[self.split_cards(cards[c][i[1]])]) + + xml_text = self.db_connection.get_xml(new_hand_id) + hh = HandHistory.HandHistory(xml_text, ('BETTING')) + +# action in tool tips for 3rd street cards + tip = "%s" % hh.BETTING.rounds[0] + for c in (0, 1, 2): + for r in range(0, self.rows): + self.eb[(c, r)].set_tooltip_text(tip) + +# action in tools tips for later streets + round_to_col = (0, 3, 4, 5, 6) + for round in range(1, len(hh.BETTING.rounds)): + tip = "%s" % hh.BETTING.rounds[round] + for r in range(0, self.rows): + self.eb[(round_to_col[round], r)].set_tooltip_text(tip) + + def split_cards(self, card): + return (card[0], card[1].upper()) + + def clear(self): + for r in range(0, self.rows): + self.grid_contents[(1, r)].set_text(" ") + for c in range(0, 7): + self.seen_cards[(c, r)].set_from_pixbuf(self.card_images[('B', 'S')]) + self.eb[(c, r)].set_tooltip_text('') + def get_card_images(self): + card_images = {} + suits = ('S', 'H', 'D', 'C') + ranks = ('A', 'K', 'Q', 'J', 'T', '9', '8', '7', '6', '5', '4', '3', '2', 'B') + pb = gtk.gdk.pixbuf_new_from_file("Cards01.png") + + for j in range(0, 14): + for i in range(0, 4): + temp_pb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, pb.get_has_alpha(), pb.get_bits_per_sample(), 30, 42) + pb.copy_area(30*j, 42*i, 30, 42, temp_pb, 0, 0) + card_images[(ranks[j], suits[i])] = temp_pb + return(card_images) + +# cards are 30 wide x 42 high + +if __name__== "__main__": + + def destroy(*args): # call back for terminating the main eventloop + gtk.main_quit() # used only for testing + + def process_new_hand(source, condition): #callback from stdin watch -- testing only +# there is a new hand_id to be processed +# just read it and pass it to update + new_hand_id = sys.stdin.readline() + new_hand_id = new_hand_id.rstrip() # remove trailing whitespace + m.update(new_hand_id) + return(True) + + print "system = %s" % (os.name) + + config = Configuration.Config() + db_connection = Database.Database(config, 'PTrackSv2', 'razz') + + main_window = gtk.Window() + main_window.set_keep_above(True) + main_window.connect("destroy", destroy) + + m = Mucked(main_window, db_connection) + main_window.show_all() + + s_id = gobject.io_add_watch(sys.stdin, gobject.IO_IN, process_new_hand) + + gtk.main() + diff --git a/pyfpdb/SQL.py b/pyfpdb/SQL.py new file mode 100644 index 00000000..ffd02ea6 --- /dev/null +++ b/pyfpdb/SQL.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python +"""SQL.py + +Set up all of the SQL statements for a given game and database type. +""" +# Copyright 2008, Ray E. Barker +# +# 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 + +######################################################################## + +# Standard Library modules + +# pyGTK modules + +# FreePokerTools modules + +class Sql: + + def __init__(self, game = 'holdem', type = 'PT3'): + self.query = {} + + if game == 'razz' and type == 'ptracks': + + self.query['get_table_name'] = "select table_name from game where game_id = %s" + + self.query['get_last_hand'] = "select max(game_id) from game" + + self.query['get_recent_hands'] = "select game_id from game where game_id > %(last_hand)d" + + self.query['get_xml'] = "select xml from hand_history where game_id = %s" + + self.query['get_player_id'] = """ + select player_id from players + where screen_name = %(player)s + """ + + self.query['get_hand_info'] = """ + SELECT + game_id, + CONCAT(hole_card_1, hole_card_2, hole_card_3, hole_card_4, hole_card_5, hole_card_6, hole_card_7) AS hand, + total_won-total_bet AS net + FROM game_players + WHERE game_id = %s AND player_id = 3 + """ + + self.query['get_cards'] = """ + select + seat_number, + screen_name, + hole_card_1, + hole_card_2, + hole_card_3, + hole_card_4, + hole_card_5, + hole_card_6, + hole_card_7 + from game_players, players + where game_id = %s and game_players.player_id = players.player_id + order by seat_number + """ + + self.query['get_stats_from_hand'] = """ + SELECT player_id, + count(*) AS n, + sum(pre_fourth_raise_n) AS pfr, + sum(fourth_raise_n) AS raise_n_2, + sum(fourth_ck_raise_n) AS cr_n_2, + sum(fifth_bet_raise_n) AS br_n_3, + sum(fifth_bet_ck_raise_n) AS cr_n_3, + sum(sixth_bet_raise_n) AS br_n_4, + sum(sixth_bet_ck_raise_n) AS cr_n_4, + sum(river_bet_raise_n) AS br_n_5, + sum(river_bet_ck_raise_n) AS cr_n_5, + sum(went_to_showdown_n) AS sd, + sum(saw_fourth_n) AS saw_f, + sum(raised_first_pf) AS first_pfr, + sum(vol_put_money_in_pot) AS vpip, + sum(limp_with_prev_callers) AS limp_w_callers, + + sum(ppossible_actions) AS poss_a_pf, + sum(pfold) AS fold_pf, + sum(pcheck) AS check_pf, + sum(praise) AS raise_pf, + sum(pcall) AS raise_pf, + sum(limp_call_reraise_pf) AS limp_call_pf, + + sum(pfr_check) AS check_after_raise, + sum(pfr_call) AS call_after_raise, + sum(pfr_fold) AS fold_after_raise, + sum(pfr_bet) AS bet_after_raise, + sum(pfr_raise) AS raise_after_raise, + sum(folded_to_river_bet) AS fold_to_r_bet, + + sum(fpossible_actions) AS poss_a_2, + sum(ffold) AS fold_2, + sum(fcheck) AS check_2, + sum(fbet) AS bet_2, + sum(fraise) AS raise_2, + sum(fcall) AS raise_2, + + sum(fifpossible_actions) AS poss_a_3, + sum(fiffold) AS fold_3, + sum(fifcheck) AS check_3, + sum(fifbet) AS bet_3, + sum(fifraise) AS raise_3, + sum(fifcall) AS call_3, + + sum(spossible_actions) AS poss_a_4, + sum(sfold) AS fold_4, + sum(scheck) AS check_4, + sum(sbet) AS bet_4, + sum(sraise) AS raise_4, + sum(scall) AS call_4, + + sum(rpossible_actions) AS poss_a_5, + sum(rfold) AS fold_5, + sum(rcheck) AS check_5, + sum(rbet) AS bet_5, + sum(rraise) AS raise_5, + sum(rcall) AS call_5, + + sum(cold_call_pf) AS cc_pf, + sum(saw_fifth_n) AS saw_3, + sum(saw_sixth_n) AS saw_4, + sum(saw_river_n) AS saw_5 + FROM game_players + WHERE player_id in + (SELECT player_id FROM game_players + WHERE game_id = %s AND NOT player_id = %s) + GROUP BY player_id + """ +# alternate form of WHERE for above +# WHERE game_id = %(hand)d AND NOT player_id = %(hero)d) +# WHERE game_id = %s AND NOT player_id = %s) + + self.query['get_players_from_hand'] = """ + SELECT game_players.player_id, seat_number, screen_name + FROM game_players INNER JOIN players ON (game_players.player_id = players.player_id) + WHERE game_id = %s + """ + + if game == 'holdem' and type == 'PT3': + + self.query['get_last_hand'] = "select max(id_hand) from holdem_hand_summary" + + self.query['get_player_id'] = """ + select id_player from player, lookup_sites + where player_name = %(player)s + and site_name = %(site)s + and player.id_site = lookup_sites.id_site + """ + + self.query['get_stats_from_hand'] = """ + select id_player AS player_id, + count(*) AS n, + sum(CAST (flg_vpip AS integer)) as vpip, + sum(CAST (flg_p_first_raise AS integer)) as p_first_raise, + sum(CAST (flg_f_saw AS integer)) as f_saw, + sum(CAST (flg_p_open AS integer)) as p_open, + sum(CAST (flg_p_limp AS integer)) as p_limp, + sum(CAST (flg_p_fold AS integer)) as p_fold, + sum(CAST (flg_p_ccall AS integer)) as p_ccall, + sum(CAST (flg_f_bet AS integer)) as f_bet, + sum(CAST (flg_f_first_raise AS integer)) as f_first_raise, + sum(CAST (flg_f_check AS integer)) as f_check, + sum(CAST (flg_f_check_raise AS integer)) as f_check_raise, + sum(CAST (flg_f_fold AS integer)) as f_fold, + sum(CAST (flg_f_saw AS integer)) as f_saw, + sum(CAST (flg_t_bet AS integer)) as t_bet, + sum(CAST (flg_t_first_raise AS integer)) as t_first_raise, + sum(CAST (flg_t_check AS integer)) as t_check, + sum(CAST (flg_t_check_raise AS integer)) as t_check_raise, + sum(CAST (flg_t_fold AS integer)) as t_fold, + sum(CAST (flg_t_saw AS integer)) as t_saw, + sum(CAST (flg_r_bet AS integer)) as r_bet, + sum(CAST (flg_r_first_raise AS integer)) as r_first_raise, + sum(CAST (flg_r_check AS integer)) as r_check, + sum(CAST (flg_r_check_raise AS integer)) as r_check_raise, + sum(CAST (flg_r_fold AS integer)) as r_fold, + sum(CAST (flg_r_saw AS integer)) as r_saw, + sum(CAST (flg_sb_steal_fold AS integer)) as sb_steal_fold, + sum(CAST (flg_bb_steal_fold AS integer)) as bb_steal_fold, + sum(CAST (flg_blind_def_opp AS integer)) as blind_def_opp, + sum(CAST (flg_steal_att AS integer)) as steal_att, + sum(CAST (flg_steal_opp AS integer)) as steal_opp, + sum(CAST (flg_blind_k AS integer)) as blind_k, + sum(CAST (flg_showdown AS integer)) as showdown, + + sum(CAST (flg_p_squeeze AS integer)) as p_squeeze, + sum(CAST (flg_p_squeeze_opp AS integer)) as p_squeeze_opp, + sum(CAST (flg_p_squeeze_def_opp AS integer)) as p_squeeze_def_opp, + + sum(CAST (flg_f_cbet AS integer)) as f_cbet, + sum(CAST (flg_f_cbet_opp AS integer)) as f_cbet_opp, + sum(CAST (flg_f_cbet_def_opp AS integer)) as f_cbet_def_opp + + from holdem_hand_player_statistics + where id_hand = %(hand)d and not id_player = %(hero)d + group by id_player + """ + + if game == 'holdem' and type == 'fpdb': + + self.query['get_last_hand'] = "select max(id) from Hands" + + self.query['get_player_id'] = """ + select Players.id AS player_id from Players, Sites + where Players.name = %(player)s + and Sites.name = %(site)s + and Players.SiteId = Sites.id + """ + + self.query['get_stats_from_hand'] = """ + SELECT HudCache.playerId AS player_id, + sum(HDs) AS n, + sum(street0Aggr) AS pfr, + sum(street0VPI) AS vpip, + sum(sawShowdown) AS sd, + sum(wonAtSD) AS wmsd, + sum(street1Seen) AS saw_f, + sum(totalProfit) AS net + FROM HudCache, Hands + WHERE HudCache.PlayerId in + (SELECT PlayerId FROM HandsPlayers + WHERE handId = %s) + AND Hands.id = %s + AND Hands.gametypeId = HudCache.gametypeId + GROUP BY HudCache.PlayerId + """ + + self.query['get_players_from_hand'] = """ + SELECT HandsPlayers.playerId, seatNo, name + FROM HandsPlayers INNER JOIN Players ON (HandsPlayers.playerId = Players.id) + WHERE handId = %s + """ + + self.query['get_table_name'] = """ + select tableName, maxSeats, category + from Hands,Gametypes + where Hands.id = %s + and Gametypes.id = Hands.gametypeId + """ + +if __name__== "__main__": +# just print the default queries and exit + s = Sql(game = 'razz', type = 'ptracks') + for key in s.query: + print "For query " + key + ", sql =" + print s.query[key] diff --git a/pyfpdb/Stats.py b/pyfpdb/Stats.py new file mode 100644 index 00000000..edde911d --- /dev/null +++ b/pyfpdb/Stats.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python + +"""Manage collecting and formatting of stats and tooltips. +""" +# Copyright 2008, Ray E. Barker + +# +# 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 + +######################################################################## +# Standard Library modules + +# pyGTK modules +import pygtk +import gtk + +# FreePokerTools modules +import Configuration +import Database + +def do_tip(widget, tip): + widget.set_tooltip_text(tip) + +def do_stat(stat_dict, player = 24, stat = 'vpip'): + return eval("%(stat)s(stat_dict, %(player)d)" % {'stat': stat, 'player': player}) +# OK, for reference the tuple returned by the stat is: +# 0 - The stat, raw, no formating, eg 0.33333333 +# 1 - formatted stat with appropriate precision and punctuation, eg 33% +# 2 - formatted stat with appropriate precision, punctuation and a hint, eg v=33% +# 3 - same as #2 except name of stat instead of hint, eg vpip=33% +# 4 - the calculation that got the stat, eg 9/27 +# 5 - the name of the stat, useful for a tooltip, eg vpip + +########################################### +# functions that return individual stats +def vpip(stat_dict, player): + stat = 0.0 + try: + stat = float(stat_dict[player]['vpip'])/float(stat_dict[player]['n']) + return (stat, + '%3.1f' % (100*stat) + '%', + 'v=%3.1f' % (100*stat) + '%', + 'vpip=%3.1f' % (100*stat) + '%', + '(%d/%d)' % (stat_dict[player]['vpip'], stat_dict[player]['n']), + 'vpip' + ) + except: return (stat, + '%3.1f' % (0) + '%', + 'w=%3.1f' % (0) + '%', + 'wtsd=%3.1f' % (0) + '%', + '(%d/%d)' % (0, 0), + 'wtsd' + ) + +def pfr(stat_dict, player): + stat = 0.0 + try: + stat = float(stat_dict[player]['pfr'])/float(stat_dict[player]['n']) + return (stat, + '%3.1f' % (100*stat) + '%', + 'p=%3.1f' % (100*stat) + '%', + 'pfr=%3.1f' % (100*stat) + '%', + '(%d/%d)' % (stat_dict[player]['pfr'], stat_dict[player]['n']), + 'pfr' + ) + except: + return (stat, + '%3.1f' % (0) + '%', + 'w=%3.1f' % (0) + '%', + 'wtsd=%3.1f' % (0) + '%', + '(%d/%d)' % (0, 0), + 'wtsd' + ) + +def wtsd(stat_dict, player): + stat = 0.0 + try: + stat = float(stat_dict[player]['sd'])/float(stat_dict[player]['saw_f']) + return (stat, + '%3.1f' % (100*stat) + '%', + 'w=%3.1f' % (100*stat) + '%', + 'wtsd=%3.1f' % (100*stat) + '%', + '(%d/%d)' % (stat_dict[player]['sd'], stat_dict[player]['saw_f']), + 'wtsd' + ) + except: + return (stat, + '%3.1f' % (0) + '%', + 'w=%3.1f' % (0) + '%', + 'wtsd=%3.1f' % (0) + '%', + '(%d/%d)' % (0, 0), + 'wtsd' + ) + + +def saw_f(stat_dict, player): + try: + num = float(stat_dict[player]['saw_f']) + den = float(stat_dict[player]['n']) + stat = num/den + return (stat, + '%3.1f' % (100*stat) + '%', + 'sf=%3.1f' % (100*stat) + '%', + 'saw_f=%3.1f' % (100*stat) + '%', + '(%d/%d)' % (stat_dict[player]['saw_f'], stat_dict[player]['n']), + 'saw_f' + ) + except: + stat = 0.0 + num = 0 + den = 0 + return (stat, + '%3.1f' % (stat) + '%', + 'sf=%3.1f' % (stat) + '%', + 'saw_f=%3.1f' % (stat) + '%', + '(%d/%d)' % (num, den), + 'saw_f' + ) + +def n(stat_dict, player): + try: + return (stat_dict[player]['n'], + '%d' % (stat_dict[player]['n']), + 'n=%d' % (stat_dict[player]['n']), + 'n=%d' % (stat_dict[player]['n']), + '(%d)' % (stat_dict[player]['n']), + 'number hands seen' + ) + except: + return (stat_dict[player][0], + '%d' % (stat_dict[player][0]), + 'n=%d' % (stat_dict[player][0]), + 'n=%d' % (stat_dict[player][0]), + '(%d)' % (stat_dict[player][0]), + 'number hands seen' + ) + +def fold_f(stat_dict, player): + stat = 0.0 + try: + stat = stat_dict[player]['fold_2']/stat_dict[player]['saw_f'] + return (stat, + '%3.1f' % (100*stat) + '%', + 'ff=%3.1f' % (100*stat) + '%', + 'fold_f=%3.1f' % (100*stat) + '%', + '(%d/%d)' % (stat_dict[player]['fold_2'], stat_dict[player]['saw_f']), + 'folded fourth' + ) + except: + return (stat, + '%3.1f' % (0) + '%', + 'ff=%3.1f' % (0) + '%', + 'fold_f=%3.1f' % (0) + '%', + '(%d/%d)' % (0, 0), + 'folded fourth' + ) + + +if __name__=="__main__": + c = Configuration.Config() + db_connection = Database.Database(c, 'fpdb', 'holdem') + h = db_connection.get_last_hand() + stat_dict = db_connection.get_stats_from_hand(h, 0) + + for player in stat_dict.keys(): + print "player = ", player, do_stat(stat_dict, player = player, stat = 'vpip') + print "player = ", player, do_stat(stat_dict, player = player, stat = 'pfr') + print "player = ", player, do_stat(stat_dict, player = player, stat = 'wtsd') + print "player = ", player, do_stat(stat_dict, player = player, stat = 'saw_f') + print "player = ", player, do_stat(stat_dict, player = player, stat = 'n') + print "player = ", player, do_stat(stat_dict, player = player, stat = 'fold_f') + + db_connection.close + diff --git a/pyfpdb/Tables.py b/pyfpdb/Tables.py new file mode 100644 index 00000000..6c53a590 --- /dev/null +++ b/pyfpdb/Tables.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python +"""Discover_Tables.py + +Inspects the currently open windows and finds those of interest to us--that is +poker table windows from supported sites. Returns a list +of Table_Window objects representing the windows found. +""" +# Copyright 2008, Ray E. Barker + +# +# 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 + +######################################################################## + +# Standard Library modules +import os +import re + +# FreePokerTools modules +import Configuration + +class Table_Window: + def __str__(self): +# __str__ method for testing + temp = 'TableWindow object\n' + temp = temp + " name = %s\n site = %s\n number = %s\n title = %s\n" % (self.name, self.site, self.number, self.title) + temp = temp + " game = %s\n structure = %s\n max = %s\n" % (self.game, self.structure, self.max) + temp = temp + " width = %d\n height = %d\n x = %d\n y = %d\n" % (self.width, self.height, self.x, self.y) + if getattr(self, 'tournament', 0): + temp = temp + " tournament = %d\n table = %d" % (self.tournament, self.table) + return temp + + def get_details(table): + table.game = 'razz' + table.max = 8 + table.struture = 'limit' + table.tournament = 0 + +def discover(c): + tables = {} + for listing in os.popen('xwininfo -root -tree').readlines(): + if re.search('Lobby', listing): continue + if re.search('Instant Hand History', listing): continue + if not re.search('Logged In as ', listing): continue + for s in c.supported_sites.keys(): + if re.search(c.supported_sites[s].table_finder, listing): + mo = re.match('\s+([\dxabcdef]+) (.+):.+ (\d+)x(\d+)\+\d+\+\d+ \+(\d+)\+(\d+)', listing) + if mo.group(2) == '(has no name)': continue + if re.match('[\(\)\d\s]+', mo.group(2)): continue # this is a popup + tw = Table_Window() + tw.site = c.supported_sites[s].site_name + tw.number = mo.group(1) + tw.title = mo.group(2) + tw.width = int( mo.group(3) ) + tw.height = int( mo.group(4) ) + tw.x = int (mo.group(5) ) + tw.y = int (mo.group(6) ) + tw.title = re.sub('\"', '', tw.title) +# this rather ugly hack makes my fake table used for debugging work + if tw.title == "PokerStars.py": continue + +# use this eval thingie to call the title bar decoder specified in the config file + eval("%s(tw)" % c.supported_sites[s].decoder) + tables[tw.name] = tw + return tables + +def pokerstars_decode_table(tw): +# extract the table name OR the tournament number and table name from the title +# other info in title is redundant with data in the database + title_bits = re.split(' - ', tw.title) + name = title_bits[0] + mo = re.search('Tournament (\d+) Table (\d+)', name) + if mo: + tw.tournament = int( mo.group(1) ) + tw.table = int( mo.group(2) ) + tw.name = name + else: + tw.tournament = None + for pattern in [' no all-in', ' fast', ',']: + name = re.sub(pattern, '', name) + name = re.sub('\s+$', '', name) + tw.name = name + + mo = re.search('(Razz|Stud H/L|Stud|Omaha H/L|Omaha|Hold\'em|5-Card Draw|Triple Draw 2-7 Lowball)', tw.title) + +#Traceback (most recent call last): +# File "/home/fatray/razz-poker-productio/HUD_main.py", line 41, in process_new_hand +# table_windows = Tables.discover(config) +# File "/home/fatray/razz-poker-productio/Tables.py", line 58, in discover +# eval("%s(tw)" % c.supported_sites[s].decoder) +# File "", line 1, in +# File "/home/fatray/razz-poker-productio/Tables.py", line 80, in pokerstars_decode_table +# tw.game = mo.group(1).lower() +#AttributeError: 'NoneType' object has no attribute 'group' +# +#This problem happens with observed windows!! + + tw.game = mo.group(1).lower() + tw.game = re.sub('\'', '', tw.game) + tw.game = re.sub('h/l', 'hi/lo', tw.game) + + mo = re.search('(No Limit|Pot Limit)', tw.title) + if mo: + tw.structure = mo.group(1).lower() + else: + tw.structure = 'limit' + + tw.max = None + if tw.game in ('razz', 'stud', 'stud hi/lo'): + tw.max = 8 + elif tw.game in ('5-card draw', 'triple draw 2-7 lowball'): + tw.max = 6 + elif tw.game == 'holdem': + pass + elif tw.game in ('omaha', 'omaha hi/lo'): + pass + +if __name__=="__main__": + c = Configuration.Config() + tables = discover(c) + + for t in tables.keys(): + print tables[t] \ No newline at end of file diff --git a/pyfpdb/fpdb_import.py b/pyfpdb/fpdb_import.py index 38f2cf04..6bee055c 100755 --- a/pyfpdb/fpdb_import.py +++ b/pyfpdb/fpdb_import.py @@ -102,12 +102,12 @@ def import_file_dict(options, settings): try: if (category=="razz" or category=="studhi" or category=="studhilo"): raise fpdb_simple.FpdbError ("stud/razz currently out of order") - last_read_hand=fpdb_parse_logic.mainParser(db, cursor, site, category, hand) + handsId=fpdb_parse_logic.mainParser(db, cursor, site, category, hand) db.commit() stored+=1 if settings['imp-callFpdbHud']: - print "call to HUD here" + print "call to HUD here. handsId:",handsId db.commit() except fpdb_simple.DuplicateError: duplicates+=1 @@ -156,13 +156,13 @@ def import_file_dict(options, settings): for line_no in range(len(lines)): if lines[line_no].find("Game #")!=-1: final_game_line=lines[line_no] - last_read_hand=fpdb_simple.parseSiteHandNo(final_game_line) + handsId=fpdb_simple.parseSiteHandNo(final_game_line) #todo: this will cause return of an unstored hand number if the last hadn was error or partial db.commit() inputFile.close() cursor.close() db.close() - return last_read_hand + return handsId if __name__ == "__main__": diff --git a/pyfpdb/fpdb_save_to_db.py b/pyfpdb/fpdb_save_to_db.py index a5016904..a3ec95a5 100644 --- a/pyfpdb/fpdb_save_to_db.py +++ b/pyfpdb/fpdb_save_to_db.py @@ -34,7 +34,7 @@ def ring_stud(cursor, category, site_hand_no, gametype_id, hand_start_time, fpdb_simple.storeHudData(cursor, category, player_ids, hudImportData) fpdb_simple.storeActions(cursor, hands_players_ids, action_types, action_amounts) - return site_hand_no + return hands_id #end def ring_stud def ring_holdem_omaha(cursor, category, site_hand_no, gametype_id, hand_start_time, names, player_ids, start_cashes, positions, card_values, card_suits, board_values, board_suits, winnings, rakes, action_types, action_amounts, actionNos, hudImportData, maxSeats, tableName, seatNos): @@ -51,7 +51,7 @@ def ring_holdem_omaha(cursor, category, site_hand_no, gametype_id, hand_start_ti fpdb_simple.store_board_cards(cursor, hands_id, board_values, board_suits) fpdb_simple.storeActions(cursor, hands_players_ids, action_types, action_amounts, actionNos) - return site_hand_no + return hands_id #end def ring_holdem_omaha def tourney_holdem_omaha(cursor, category, siteTourneyNo, buyin, fee, knockout, entries, prizepool, tourney_start, payin_amounts, ranks, tourneyTypeId, siteId, #end of tourney specific params @@ -72,7 +72,7 @@ def tourney_holdem_omaha(cursor, category, siteTourneyNo, buyin, fee, knockout, fpdb_simple.store_board_cards(cursor, hands_id, board_values, board_suits) fpdb_simple.storeActions(cursor, hands_players_ids, action_types, action_amounts, actionNos) - return site_hand_no + return hands_id #end def tourney_holdem_omaha def tourney_stud(cursor, category, site_tourney_no, buyin, fee, knockout, entries, prizepool, @@ -95,6 +95,5 @@ def tourney_stud(cursor, category, site_tourney_no, buyin, fee, knockout, entrie fpdb_simple.storeHudData(cursor, category, player_ids, hudImportData) fpdb_simple.storeActions(cursor, hands_players_ids, action_types, action_amounts) - return site_hand_no + return hands_id #end def tourney_stud -