From cfab629779b02452f0e3c4e79ea54cc7de18a355 Mon Sep 17 00:00:00 2001 From: Worros Date: Mon, 6 Dec 2010 14:16:59 +0800 Subject: [PATCH 01/45] Regression: New Stars LHE file Adds a file with a hand where the big blind is also the button --- ...-1.00-200508.BB.and.Button.same.player.txt | 16 ++ ...00-200508.BB.and.Button.same.player.txt.hp | 188 ++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 pyfpdb/regression-test-files/cash/Stars/Flop/LHE-USD-1.00-200508.BB.and.Button.same.player.txt create mode 100644 pyfpdb/regression-test-files/cash/Stars/Flop/LHE-USD-1.00-200508.BB.and.Button.same.player.txt.hp diff --git a/pyfpdb/regression-test-files/cash/Stars/Flop/LHE-USD-1.00-200508.BB.and.Button.same.player.txt b/pyfpdb/regression-test-files/cash/Stars/Flop/LHE-USD-1.00-200508.BB.and.Button.same.player.txt new file mode 100644 index 00000000..31b92b9d --- /dev/null +++ b/pyfpdb/regression-test-files/cash/Stars/Flop/LHE-USD-1.00-200508.BB.and.Button.same.player.txt @@ -0,0 +1,16 @@ +PokerStars Game #2344646393: Hold'em Limit ($1/$2 USD) - 2005/08/15 14:27:10 ET +Table 'Phad' Seat #6 is the button +Seat 6: Hero ($40 in chips) +Seat 7: fblm2002 ($24.75 in chips) +fblm2002: posts small blind $0.50 +Hero: posts big blind $1 +*** HOLE CARDS *** +Dealt to Hero [3s 4s] +fblm2002: folds +Hero collected $1 from pot +*** SUMMARY *** +Total pot $1 | Rake $0 +Seat 6: Hero (button) (big blind) collected ($1) +Seat 7: fblm2002 (small blind) folded before Flop + + diff --git a/pyfpdb/regression-test-files/cash/Stars/Flop/LHE-USD-1.00-200508.BB.and.Button.same.player.txt.hp b/pyfpdb/regression-test-files/cash/Stars/Flop/LHE-USD-1.00-200508.BB.and.Button.same.player.txt.hp new file mode 100644 index 00000000..9ff13bea --- /dev/null +++ b/pyfpdb/regression-test-files/cash/Stars/Flop/LHE-USD-1.00-200508.BB.and.Button.same.player.txt.hp @@ -0,0 +1,188 @@ +{ u'Hero': { 'card1': 41, + 'card2': 42, + 'card3': 0, + 'card4': 0, + 'card5': 0, + 'card6': 0, + 'card7': 0, + 'foldBbToStealChance': False, + 'foldSbToStealChance': False, + 'foldToOtherRaisedStreet0': False, + 'foldToOtherRaisedStreet1': False, + 'foldToOtherRaisedStreet2': False, + 'foldToOtherRaisedStreet3': False, + 'foldToOtherRaisedStreet4': False, + 'foldToStreet1CBChance': False, + 'foldToStreet1CBDone': False, + 'foldToStreet2CBChance': False, + 'foldToStreet2CBDone': False, + 'foldToStreet3CBChance': False, + 'foldToStreet3CBDone': False, + 'foldToStreet4CBChance': False, + 'foldToStreet4CBDone': False, + 'foldedBbToSteal': False, + 'foldedSbToSteal': False, + 'other3BStreet0': False, + 'other4BStreet0': False, + 'otherRaisedStreet0': False, + 'otherRaisedStreet1': False, + 'otherRaisedStreet2': False, + 'otherRaisedStreet3': False, + 'otherRaisedStreet4': False, + 'position': 'B', + 'raiseFirstInChance': False, + 'raisedFirstIn': False, + 'rake': 0, + 'sawShowdown': False, + 'seatNo': 6, + 'sitout': False, + 'startCards': 28, + 'startCash': 4000, + 'street0Aggr': False, + 'street0Bets': 0, + 'street0Calls': 0, + 'street0Raises': 0, + 'street0VPI': False, + 'street0_3BChance': False, + 'street0_3BDone': False, + 'street0_4BChance': False, + 'street0_4BDone': False, + 'street1Aggr': False, + 'street1Bets': 0, + 'street1CBChance': False, + 'street1CBDone': False, + 'street1Calls': 0, + 'street1CheckCallRaiseChance': False, + 'street1CheckCallRaiseDone': False, + 'street1Raises': 0, + 'street1Seen': False, + 'street2Aggr': False, + 'street2Bets': 0, + 'street2CBChance': False, + 'street2CBDone': False, + 'street2Calls': 0, + 'street2CheckCallRaiseChance': False, + 'street2CheckCallRaiseDone': False, + 'street2Raises': 0, + 'street2Seen': False, + 'street3Aggr': False, + 'street3Bets': 0, + 'street3CBChance': False, + 'street3CBDone': False, + 'street3Calls': 0, + 'street3CheckCallRaiseChance': False, + 'street3CheckCallRaiseDone': False, + 'street3Raises': 0, + 'street3Seen': False, + 'street4Aggr': False, + 'street4Bets': 0, + 'street4CBChance': False, + 'street4CBDone': False, + 'street4Calls': 0, + 'street4CheckCallRaiseChance': False, + 'street4CheckCallRaiseDone': False, + 'street4Raises': 0, + 'street4Seen': False, + 'totalProfit': 50, + 'tourneyTypeId': None, + 'tourneysPlayersIds': None, + 'winnings': 100, + 'wonAtSD': 0.0, + 'wonWhenSeenStreet1': 0.0, + 'wonWhenSeenStreet2': 0.0, + 'wonWhenSeenStreet3': 0.0, + 'wonWhenSeenStreet4': 0.0}, + u'fblm2002': { 'card1': 0, + 'card2': 0, + 'card3': 0, + 'card4': 0, + 'card5': 0, + 'card6': 0, + 'card7': 0, + 'foldBbToStealChance': False, + 'foldSbToStealChance': False, + 'foldToOtherRaisedStreet0': False, + 'foldToOtherRaisedStreet1': False, + 'foldToOtherRaisedStreet2': False, + 'foldToOtherRaisedStreet3': False, + 'foldToOtherRaisedStreet4': False, + 'foldToStreet1CBChance': False, + 'foldToStreet1CBDone': False, + 'foldToStreet2CBChance': False, + 'foldToStreet2CBDone': False, + 'foldToStreet3CBChance': False, + 'foldToStreet3CBDone': False, + 'foldToStreet4CBChance': False, + 'foldToStreet4CBDone': False, + 'foldedBbToSteal': False, + 'foldedSbToSteal': False, + 'other3BStreet0': False, + 'other4BStreet0': False, + 'otherRaisedStreet0': False, + 'otherRaisedStreet1': False, + 'otherRaisedStreet2': False, + 'otherRaisedStreet3': False, + 'otherRaisedStreet4': False, + 'position': 'S', + 'raiseFirstInChance': True, + 'raisedFirstIn': False, + 'rake': 0, + 'sawShowdown': False, + 'seatNo': 7, + 'sitout': False, + 'startCards': 0, + 'startCash': 2475, + 'street0Aggr': False, + 'street0Bets': 0, + 'street0Calls': 0, + 'street0Raises': 0, + 'street0VPI': False, + 'street0_3BChance': False, + 'street0_3BDone': False, + 'street0_4BChance': False, + 'street0_4BDone': False, + 'street1Aggr': False, + 'street1Bets': 0, + 'street1CBChance': False, + 'street1CBDone': False, + 'street1Calls': 0, + 'street1CheckCallRaiseChance': False, + 'street1CheckCallRaiseDone': False, + 'street1Raises': 0, + 'street1Seen': False, + 'street2Aggr': False, + 'street2Bets': 0, + 'street2CBChance': False, + 'street2CBDone': False, + 'street2Calls': 0, + 'street2CheckCallRaiseChance': False, + 'street2CheckCallRaiseDone': False, + 'street2Raises': 0, + 'street2Seen': False, + 'street3Aggr': False, + 'street3Bets': 0, + 'street3CBChance': False, + 'street3CBDone': False, + 'street3Calls': 0, + 'street3CheckCallRaiseChance': False, + 'street3CheckCallRaiseDone': False, + 'street3Raises': 0, + 'street3Seen': False, + 'street4Aggr': False, + 'street4Bets': 0, + 'street4CBChance': False, + 'street4CBDone': False, + 'street4Calls': 0, + 'street4CheckCallRaiseChance': False, + 'street4CheckCallRaiseDone': False, + 'street4Raises': 0, + 'street4Seen': False, + 'totalProfit': -50, + 'tourneyTypeId': None, + 'tourneysPlayersIds': None, + 'winnings': 0, + 'wonAtSD': 0.0, + 'wonWhenSeenStreet1': 0.0, + 'wonWhenSeenStreet2': 0.0, + 'wonWhenSeenStreet3': 0.0, + 'wonWhenSeenStreet4': 0.0}} From 3fbaa0b0fba7f4538911d9d5830bec0372d7f094 Mon Sep 17 00:00:00 2001 From: Eratosthenes Date: Tue, 7 Dec 2010 10:20:20 -0500 Subject: [PATCH 02/45] Don't try to close HUD mw twice. --- pyfpdb/HUD_main.pyw | 1 - 1 file changed, 1 deletion(-) mode change 100755 => 100644 pyfpdb/HUD_main.pyw diff --git a/pyfpdb/HUD_main.pyw b/pyfpdb/HUD_main.pyw old mode 100755 new mode 100644 index 47cdfae0..a2aefc0f --- a/pyfpdb/HUD_main.pyw +++ b/pyfpdb/HUD_main.pyw @@ -312,7 +312,6 @@ def idle_kill(hud_main, table): try: if table in hud_main.hud_dict: hud_main.hud_dict[table].kill() - hud_main.hud_dict[table].main_window.destroy() hud_main.vb.remove(hud_main.hud_dict[table].tablehudlabel) del(hud_main.hud_dict[table]) hud_main.main_window.resize(1, 1) From a4dff38c9f38973c53a9f8e7dbda52090dc6994c Mon Sep 17 00:00:00 2001 From: Eratosthenes Date: Tue, 7 Dec 2010 10:24:06 -0500 Subject: [PATCH 03/45] Ensure table.key is always temp_key calculated in HUD_main. --- pyfpdb/HUD_main.pyw | 1 + pyfpdb/TableWindow.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyfpdb/HUD_main.pyw b/pyfpdb/HUD_main.pyw index a2aefc0f..38d435bc 100644 --- a/pyfpdb/HUD_main.pyw +++ b/pyfpdb/HUD_main.pyw @@ -270,6 +270,7 @@ class HUD_main(object): table_name = "%s %s" % (tour_number, tab_number) log.error("HUD create: table name %s not found, skipping." % table_name) else: + tablewindow.key = temp_key tablewindow.max = max tablewindow.site = site_name # Test that the table window still exists diff --git a/pyfpdb/TableWindow.py b/pyfpdb/TableWindow.py index ea2596de..1ee51650 100644 --- a/pyfpdb/TableWindow.py +++ b/pyfpdb/TableWindow.py @@ -126,13 +126,11 @@ class Table_Window(object): self.type = "tour" table_kwargs = dict(tournament = self.tournament, table_number = self.table) self.tableno_re = getTableNoRe(self.config, self.site, tournament = self.tournament) - self.key = "%s Table %s" % (tournament, str(self.table)) elif table_name is not None: self.name = table_name self.type = "cash" self.tournament = None table_kwargs = dict(table_name = table_name) - self.key = table_name # used as key for the hud_dict in HUD_main else: return None From 95d14911f315fc14a40e620860170f3553fb0f4e Mon Sep 17 00:00:00 2001 From: "chaz@pokeit.co" Date: Wed, 8 Dec 2010 16:32:26 -0500 Subject: [PATCH 04/45] Fixed a variety of bugs in storeSessionsCache() and the corresponding SQL statements --- pyfpdb/Database.py | 125 +++++++++++++++++++++------------------------ pyfpdb/SQL.py | 95 ++++++++++++++++++++++------------ 2 files changed, 119 insertions(+), 101 deletions(-) diff --git a/pyfpdb/Database.py b/pyfpdb/Database.py index 567ab166..cf69ddb0 100644 --- a/pyfpdb/Database.py +++ b/pyfpdb/Database.py @@ -2026,19 +2026,25 @@ class Database: pass def storeSessionsCache(self, pids, startTime, game, pdata): - """Update cached sessions. If update fails because no record exists, do an insert.""" + """Update cached sessions. If update fails because no record exists, do an insert""" - THRESHOLD = timedelta(seconds=int(self.sessionTimeout * 60)) #convert minutes to seconds + THRESHOLD = timedelta(seconds=int(self.sessionTimeout * 60)) bigBet = int(Decimal(game['bb'])*200) - check_sessionscache = self.sql.query['check_sessionscache'] - check_sessionscache = check_sessionscache.replace('%s', self.sql.query['placeholder']) - update_sessionscache = self.sql.query['update_sessionscache'] - update_sessionscache = update_sessionscache.replace('%s', self.sql.query['placeholder']) + select_sessionscache = self.sql.query['select_sessionscache'] + select_sessionscache = select_sessionscache.replace('%s', self.sql.query['placeholder']) + select_sessionscache_mid = self.sql.query['select_sessionscache_mid'] + select_sessionscache_mid = select_sessionscache_mid.replace('%s', self.sql.query['placeholder']) + select_sessionscache_start = self.sql.query['select_sessionscache_start'] + select_sessionscache_start = select_sessionscache_start.replace('%s', self.sql.query['placeholder']) + + update_sessionscache_mid = self.sql.query['update_sessionscache_mid'] + update_sessionscache_mid = update_sessionscache_mid.replace('%s', self.sql.query['placeholder']) update_sessionscache_start = self.sql.query['update_sessionscache_start'] update_sessionscache_start = update_sessionscache_start.replace('%s', self.sql.query['placeholder']) update_sessionscache_end = self.sql.query['update_sessionscache_end'] update_sessionscache_end = update_sessionscache_end.replace('%s', self.sql.query['placeholder']) + insert_sessionscache = self.sql.query['insert_sessionscache'] insert_sessionscache = insert_sessionscache.replace('%s', self.sql.query['placeholder']) merge_sessionscache = self.sql.query['merge_sessionscache'] @@ -2083,77 +2089,62 @@ class Database: cursor = self.get_cursor() for row in inserts: - check = [] - check.append(row[-1]-THRESHOLD) - check.append(row[-1]+THRESHOLD) - num = cursor.execute(check_sessionscache, check) - #DEBUG log.info(_("check yurself: '%s'") % (num.rowcount)) - - # Try to do the update first: - if ((self.backend == self.PGSQL and cursor.statusmessage == "UPDATE 1") - or (self.backend == self.MYSQL_INNODB and num == 1) - or (self.backend == self.SQLITE and num.rowcount == 1)): - update = row + row[-1:] - mid = cursor.execute(update_sessionscache, update) - #DEBUG log.info(_("update '%s' rows, no change to session times ") % str(mid.rowcount)) - if ((self.backend == self.PGSQL and cursor.statusmessage != "UPDATE 1") - or (self.backend == self.MYSQL_INNODB and mid == 0) - or (self.backend == self.SQLITE and mid.rowcount == 0)): - update_start = row[-1:] + row + check - start = cursor.execute(update_sessionscache_start, update_start) - #DEBUG log.info(_("update '%s' rows, and updated sessionStart") % str(start.rowcount)) - if ((self.backend == self.PGSQL and cursor.statusmessage != "UPDATE 1") - or (self.backend == self.MYSQL_INNODB and start == 0) - or (self.backend == self.SQLITE and start.rowcount == 0)): - update_end = row[-1:] + row + check - end = cursor.execute(update_sessionscache_end, update_end) - #DEBUG log.info(_("update '%s' rows, and updated sessionEnd") % str(end.rowcount)) + threshold = [] + threshold.append(row[-1]-THRESHOLD) + threshold.append(row[-1]+THRESHOLD) + cursor.execute(select_sessionscache, threshold) + num = cursor.rowcount + if (num == 1): + # Try to do the update first: + #print "DEBUG: found 1 record to update" + update_mid = row + row[-1:] + cursor.execute(select_sessionscache_mid, update_mid[-2:]) + mid = cursor.rowcount + if (mid == 0): + update_startend = row[-1:] + row + threshold + cursor.execute(select_sessionscache_start, update_startend[-3:]) + start = cursor.rowcount + if (start == 0): + #print "DEBUG:", start, " start record found. Update stats and start time" + cursor.execute(update_sessionscache_end, update_startend) else: - pass + #print "DEBUG: 1 end record found. Update stats and end time time" + cursor.execute(update_sessionscache_start, update_startend) else: - pass - elif ((self.backend == self.PGSQL and cursor.statusmessage != "UPDATE 1" and "UPDATE" in cursor.statusmessage) - or (self.backend == self.MYSQL_INNODB and num > 1) - or (self.backend == self.SQLITE and num.rowcount > 1)): - #DEBUG log.info(_("multiple matches")) - pass - #merge two sessions if there are multiple matches - cursor.execute(merge_sessionscache, check) + #print "DEBUG: update stats mid-session" + cursor.execute(update_sessionscache_mid, update_mid) + elif (num > 1): + # Multiple matches found - merge them into one session and update: + #print "DEBUG:", num, "matches found" + cursor.execute(merge_sessionscache, threshold) merge = cursor.fetchone() - cursor.execute(delete_sessions, check) + cursor.execute(delete_sessions, threshold) cursor.execute(insert_sessionscache, merge) - update = row + row[-1:] - mid = cursor.execute(update_sessionscache, update) - #DEBUG log.info(_("update '%s' rows, no change to session times ") % str(mid.rowcount)) - if ((self.backend == self.PGSQL and cursor.statusmessage != "UPDATE 1") - or (self.backend == self.MYSQL_INNODB and mid == 0) - or (self.backend == self.SQLITE and mid.rowcount == 0)): - update_start = row[-1:] + row + check - start = cursor.execute(update_sessionscache_start, update_start) - #DEBUG log.info(_("update '%s' rows, and updated sessionStart") % str(start.rowcount)) - if ((self.backend == self.PGSQL and cursor.statusmessage != "UPDATE 1") - or (self.backend == self.MYSQL_INNODB and start == 0) - or (self.backend == self.SQLITE and start.rowcount == 0)): - update_end = row[-1:] + row + check - end = cursor.execute(update_sessionscache_end, update_end) - #DEBUG log.info(_("update '%s' rows, and updated sessionEnd") % str(end.rowcount)) + update_mid = row + row[-1:] + cursor.execute(select_sessionscache_mid, update_mid[-2:]) + mid = cursor.rowcount + if (mid == 0): + update_startend = row[-1:] + row + threshold + cursor.execute(select_sessionscache_start, update_startend[-3:]) + start = cursor.rowcount + if (start == 0): + #print "DEBUG:", start, " start record found. Update stats and start time" + cursor.execute(update_sessionscache_end, update_startend) else: - pass + #print "DEBUG: 1 end record found. Update stats and end time time" + cursor.execute(update_sessionscache_start, update_startend) else: - pass - - elif ((self.backend == self.PGSQL and cursor.statusmessage != "UPDATE 1") - or (self.backend == self.MYSQL_INNODB and num == 0) - or (self.backend == self.SQLITE and num.rowcount == 0)): - #move the last 2 items in WHERE clause of row from the end of the array - # to the beginning for the INSERT statement - #print "DEBUG: using INSERT: %s" % num + #print "DEBUG: update stats mid-session" + cursor.execute(update_sessionscache_mid, update_mid) + elif (num == 0): + # No matches found, insert new session: insert = row + row[-1:] insert = insert[-2:] + insert[:-2] - #DEBUG log.info(_("insert row: '%s'") % (insert)) + #print "DEBUG: No matches found. Insert record", insert cursor.execute(insert_sessionscache, insert) else: - pass + # Something bad happened + pass def isDuplicate(self, gametypeID, siteHandNo): dup = False diff --git a/pyfpdb/SQL.py b/pyfpdb/SQL.py index ae54c880..f6bb068b 100644 --- a/pyfpdb/SQL.py +++ b/pyfpdb/SQL.py @@ -4019,26 +4019,57 @@ class Sql: (case when tourneyTypeId+0=%s then 1 else 0 end) end)=1 AND styleKey=%s""" - self.query['check_sessionscache'] = """ - UPDATE SessionsCache SET - sessionStart=sessionStart, - sessionEnd=sessionEnd, - ringHDs=ringHDs, - tourHDs=tourHDs, - totalProfit=totalProfit, - bigBets=bigBets - WHERE sessionEnd>=%s - AND sessionStart<=%s""" - - self.query['insert_sessionscache'] = """ - INSERT INTO SessionsCache ( - sessionStart, + self.query['get_hero_hudcache_start'] = """select min(hc.styleKey) + from HudCache hc + where hc.playerId in + and hc.styleKey like 'd%'""" + + #################################### + # Queries to rebuild/modify sessionscache + #################################### + + self.query['select_sessionscache'] = """ + SELECT sessionStart, sessionEnd, ringHDs, tourHDs, totalProfit, - bigBets) - VALUES (%s, %s, %s, %s, %s, %s)""" + bigBets + FROM SessionsCache + WHERE sessionEnd>=%s + AND sessionStart<=%s""" + + self.query['select_sessionscache_mid'] = """ + SELECT sessionStart, + sessionEnd, + ringHDs, + tourHDs, + totalProfit, + bigBets + FROM SessionsCache + WHERE sessionEnd>=%s + AND sessionStart<=%s""" + + self.query['select_sessionscache_start'] = """ + SELECT sessionStart, + sessionEnd, + ringHDs, + tourHDs, + totalProfit, + bigBets + FROM SessionsCache + WHERE sessionStart>%s + AND sessionEnd>=%s + AND sessionStart<=%s""" + + self.query['update_sessionscache_mid'] = """ + UPDATE SessionsCache SET + ringHDs=ringHDs+%s, + tourHDs=tourHDs+%s, + totalProfit=totalProfit+%s, + bigBets=bigBets+%s + WHERE sessionStart<=%s + AND sessionEnd>=%s""" self.query['update_sessionscache_start'] = """ UPDATE SessionsCache SET @@ -4050,7 +4081,7 @@ class Sql: WHERE sessionStart>%s AND sessionEnd>=%s AND sessionStart<=%s""" - + self.query['update_sessionscache_end'] = """ UPDATE SessionsCache SET sessionEnd=%s, @@ -4062,30 +4093,26 @@ class Sql: AND sessionEnd>=%s AND sessionStart<=%s""" - self.query['update_sessionscache'] = """ - UPDATE SessionsCache SET - ringHDs=ringHDs+%s, - tourHDs=tourHDs+%s, - totalProfit=totalProfit+%s, - bigBets=bigBets+%s - WHERE sessionStart<=%s - AND sessionEnd>=%s""" + self.query['insert_sessionscache'] = """ + INSERT INTO SessionsCache ( + sessionStart, + sessionEnd, + ringHDs, + tourHDs, + totalProfit, + bigBets) + VALUES (%s, %s, %s, %s, %s, %s)""" self.query['merge_sessionscache'] = """ SELECT min(sessionStart), max(sessionEnd), sum(ringHDs), sum(tourHDs), sum(totalProfit), sum(bigBets) FROM SessionsCache - WHERE sessionStart>=%s - AND sessionEnd<=%s""" + WHERE sessionEnd>=%s + AND sessionStart<=%s""" self.query['delete_sessions'] = """ DELETE FROM SessionsCache - WHERE sessionStart>=%s - AND sessionEnd<=%s""" - - self.query['get_hero_hudcache_start'] = """select min(hc.styleKey) - from HudCache hc - where hc.playerId in - and hc.styleKey like 'd%'""" + WHERE sessionEnd>=%s + AND sessionStart<=%s""" if db_server == 'mysql': self.query['analyze'] = """ From bc59e1276896cb6b4af740f93baef9edabcd41de Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 10 Dec 2010 17:53:59 +0800 Subject: [PATCH 05/45] Everleaf: Make re_GameInfo error handling consistant with other sites. When re_GameInfo fails to match now, the first 100 characters of the hand will be printed to the log. Makes it much easier to identify the offending hands. Error now also throws an FpdbParseError for the importing process to catch --- pyfpdb/EverleafToFpdb.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyfpdb/EverleafToFpdb.py b/pyfpdb/EverleafToFpdb.py index 586d36d9..fced271a 100755 --- a/pyfpdb/EverleafToFpdb.py +++ b/pyfpdb/EverleafToFpdb.py @@ -106,7 +106,10 @@ or None if we fail to get the info """ m = self.re_GameInfo.search(handText) if not m: - return None + tmp = handText[0:100] + log.error(_("determineGameType: Unable to recognise gametype from: '%s'") % tmp) + log.error(_("determineGameType: Raising FpdbParseError")) + raise FpdbParseError(_("Unable to recognise gametype from: '%s'") % tmp) mg = m.groupdict() From 3db9cfa6a75df17fad3f754fe6f05ae33b284c4c Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 10 Dec 2010 18:20:52 +0800 Subject: [PATCH 06/45] Options: Add -s --sitename option --- pyfpdb/Options.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyfpdb/Options.py b/pyfpdb/Options.py index fb23d43c..9492fc87 100644 --- a/pyfpdb/Options.py +++ b/pyfpdb/Options.py @@ -41,6 +41,9 @@ def fpdb_options(): parser.add_option("-k", "--konverter", dest="hhc", default="PokerStarsToFpdb", help=_("Module name for Hand History Converter")) + parser.add_option("-s", "--sitename", + dest="sitename", default=None, + help=_("A sitename")) parser.add_option("-l", "--logging", dest = "log_level", choices = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', 'EMPTY'), From 9a956e41b6df9531a2bfd93fbcebe4133bc8126b Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 10 Dec 2010 18:21:21 +0800 Subject: [PATCH 07/45] THP: Allow a single site to be specified on the cli The normal mode for running THP is to run though all the tests for all sites: ./TestHandsPlayers.py This is a bit noisy and slow when trying to fix issues in a single parser. Added '-s' so that you can run the regression tests against a single site eg. ./TestHandsPlayers.py -s "Full Tilt Poker" ./TestHandsPlayers.py -s PokerStars ./TestHandsPlayers.py -s Winamax --- pyfpdb/TestHandsPlayers.py | 50 ++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/pyfpdb/TestHandsPlayers.py b/pyfpdb/TestHandsPlayers.py index e0013ecc..34ca1e36 100755 --- a/pyfpdb/TestHandsPlayers.py +++ b/pyfpdb/TestHandsPlayers.py @@ -28,6 +28,7 @@ import Configuration import Database import SQL import fpdb_import +import Options class FpdbError: @@ -121,6 +122,19 @@ def main(argv=None): if argv is None: argv = sys.argv[1:] + (options, argv) = Options.fpdb_options() + + test_all_sites = True + + if options.usage == True: + #Print usage examples and exit + print "USAGE:" + sys.exit(0) + + if options.sitename: + print "Only regression testing '%s' files" % (options.sitename) + test_all_sites = False + config = Configuration.Config(file = "HUD_config.test.xml") db = Database.Database(config) sql = SQL.Sql(db_server = 'sqlite') @@ -159,21 +173,27 @@ def main(argv=None): ] sites = { - 'PokerStars' : True, - 'Full Tilt Poker' : True, - 'PartyPoker' : True, - 'Betfair' : True, - 'OnGame' : True, - 'Absolute' : True, - 'UltimateBet' : True, - 'Everleaf' : True, - 'Carbon' : True, - 'PKR' : False, - 'iPoker' : True, - 'Win2day' : True, - 'Winamax' : True, + 'PokerStars' : False, + 'Full Tilt Poker' : False, + 'PartyPoker' : False, + 'Betfair' : False, + 'OnGame' : False, + 'Absolute' : False, + 'UltimateBet' : False, + 'Everleaf' : False, + 'Carbon' : False, + #'PKR' : False, + 'iPoker' : False, + 'Win2day' : False, + 'Winamax' : False, } + if test_all_sites == True: + for s in sites: + sites[s] = True + else: + sites[options.sitename] = True + if sites['PokerStars'] == True: walk_testfiles("regression-test-files/cash/Stars/", compare, importer, PokerStarsErrors, "PokerStars") walk_testfiles("regression-test-files/tour/Stars/", compare, importer, PokerStarsErrors, "PokerStars") @@ -195,8 +215,8 @@ def main(argv=None): walk_testfiles("regression-test-files/cash/Everleaf/", compare, importer, EverleafErrors, "Everleaf") if sites['Carbon'] == True: walk_testfiles("regression-test-files/cash/Carbon/", compare, importer, CarbonErrors, "Carbon") - if sites['PKR'] == True: - walk_testfiles("regression-test-files/cash/PKR/", compare, importer, PKRErrors, "PKR") + #if sites['PKR'] == True: + # walk_testfiles("regression-test-files/cash/PKR/", compare, importer, PKRErrors, "PKR") if sites['iPoker'] == True: walk_testfiles("regression-test-files/cash/iPoker/", compare, importer, iPokerErrors, "iPoker") if sites['Winamax'] == True: From 267bf93fa5e51a44aa10f0e5be468831fd905c50 Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 10 Dec 2010 18:59:52 +0800 Subject: [PATCH 08/45] Database: Comment out unused 'printdata' Writing documentation for THP at the moment, and HandsActions data doesn't curently having any tests. Comment out to simplify documentation for the moment. --- pyfpdb/Database.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pyfpdb/Database.py b/pyfpdb/Database.py index cf69ddb0..952f2727 100644 --- a/pyfpdb/Database.py +++ b/pyfpdb/Database.py @@ -1860,10 +1860,12 @@ class Database: def storeHandsActions(self, hid, pids, hpid, adata, printdata = False): #print "DEBUG: %s %s %s" %(hid, pids, adata) - if printdata: - import pprint - pp = pprint.PrettyPrinter(indent=4) - pp.pprint(adata) + + # This can be used to generate test data. Currently unused + #if printdata: + # import pprint + # pp = pprint.PrettyPrinter(indent=4) + # pp.pprint(adata) inserts = [] for a in adata: From 8fa38a26ccc9d15f17bf62f71ce8ae45d91143b9 Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 10 Dec 2010 20:02:24 +0800 Subject: [PATCH 09/45] GuiStove: Temporary commit - add 'changed' hooks They dont actually do anything yet. --- pyfpdb/GuiStove.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/pyfpdb/GuiStove.py b/pyfpdb/GuiStove.py index 7ff0820e..f238e503 100644 --- a/pyfpdb/GuiStove.py +++ b/pyfpdb/GuiStove.py @@ -54,7 +54,7 @@ class GuiStove(): self.mainHBox.show_all() if DEBUG == False: - warning_string = """ + warning_string = _(""" Stove is a GUI mockup of a EV calculation page, and completely non functional. Unless you are interested in developing this feature, please ignore this page. @@ -62,7 +62,7 @@ Unless you are interested in developing this feature, please ignore this page. If you are interested in developing the code further see GuiStove.py and Stove.py Thankyou -""" +""") self.warning_box(warning_string) @@ -161,14 +161,14 @@ Against the range: { 69.91% 15.83% 14.26% """ - label = gtk.Label(outstring) - out_frame.add(label) + self.outputlabel = gtk.Label(outstring) + out_frame.add(self.outputlabel) # Input Frame table = gtk.Table(4, 4, True) label = gtk.Label("Board:") board = gtk.Entry() - #board.connect("changed", self._some_function, arg) + board.connect("changed", self.set_board_flop, board) btn1 = gtk.Button() btn1.set_image(gtk.image_new_from_stock(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)) @@ -180,7 +180,7 @@ Against the range: { label = gtk.Label("Player1:") board = gtk.Entry() - #board.connect("changed", self._some_function, arg) + board.connect("changed", self.set_hero_cards_flop, board) btn2 = gtk.Button() btn2.set_image(gtk.image_new_from_stock(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)) #btn.connect('clicked', self._some_function, arg) @@ -195,7 +195,7 @@ Against the range: { label = gtk.Label("Player2:") board = gtk.Entry() - #board.connect("changed", self._some_function, arg) + board.connect("changed", self.set_villain_cards_flop, board) btn4 = gtk.Button() btn4.set_image(gtk.image_new_from_stock(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)) #btn.connect('clicked', self._some_function, arg) @@ -210,6 +210,15 @@ Against the range: { #table.attach(label, i, i+1, j, j+1,) in_frame.add(table) + def set_board_flop(self, caller, string): + print "DEBUG: called set_board_flop: '%s' '%s'" %(caller ,string) + + def set_hero_cards_flop(self, caller, string): + print "DEBUG: called set_hero_cards_flop" + + def set_villain_cards_flop(self, caller, string): + print "DEBUG: called set_villain_cards_flop" + def get_vbox(self): From 02204edc48b0708a30bd8ef2ca961c61038bd508 Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 10 Dec 2010 21:35:49 +0800 Subject: [PATCH 10/45] Give storeHands the ability to output test data Will allow for regression testing of hand start time --- pyfpdb/Database.py | 8 +++++++- pyfpdb/Hand.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pyfpdb/Database.py b/pyfpdb/Database.py index 952f2727..7b6bfb4a 100644 --- a/pyfpdb/Database.py +++ b/pyfpdb/Database.py @@ -1692,7 +1692,13 @@ class Database: # NEWIMPORT CODE ########################### - def storeHand(self, p): + def storeHand(self, p, printdata = False): + if printdata: + print "######## Hands ##########" + import pprint + pp = pprint.PrettyPrinter(indent=4) + pp.pprint(p) + print "###### End Hands ########" #stores into table hands: q = self.sql.query['store_hand'] diff --git a/pyfpdb/Hand.py b/pyfpdb/Hand.py index 2d7088e6..b2e220b1 100644 --- a/pyfpdb/Hand.py +++ b/pyfpdb/Hand.py @@ -265,7 +265,7 @@ db: a connected Database object""" # seats TINYINT NOT NULL, hh['seats'] = len(self.dbid_pids) - self.dbid_hands = db.storeHand(hh) + self.dbid_hands = db.storeHand(hh, printdata = printtest) self.dbid_hpid = db.storeHandsPlayers(self.dbid_hands, self.dbid_pids, self.stats.getHandsPlayers(), printdata = printtest) if self.saveActions: From b543d08d80a0ba05b5b1ca17a2d6e01985824e5b Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 10 Dec 2010 22:27:44 +0800 Subject: [PATCH 11/45] Regression: .hands file for FTP with with funky date --- ...5.201007.Short.lived.date.format.txt.hands | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 pyfpdb/regression-test-files/cash/FTP/Flop/NLHE-9max-USD-0.02-0.05.201007.Short.lived.date.format.txt.hands diff --git a/pyfpdb/regression-test-files/cash/FTP/Flop/NLHE-9max-USD-0.02-0.05.201007.Short.lived.date.format.txt.hands b/pyfpdb/regression-test-files/cash/FTP/Flop/NLHE-9max-USD-0.02-0.05.201007.Short.lived.date.format.txt.hands new file mode 100644 index 00000000..767a0c6e --- /dev/null +++ b/pyfpdb/regression-test-files/cash/FTP/Flop/NLHE-9max-USD-0.02-0.05.201007.Short.lived.date.format.txt.hands @@ -0,0 +1,32 @@ +{ 'boardcard1': 0, + 'boardcard2': 0, + 'boardcard3': 0, + 'boardcard4': 0, + 'boardcard5': 0, + 'gametypeId': 6, + 'importTime': None, + 'maxSeats': 9, + 'playersAtShowdown': 0, + 'playersAtStreet-1': 8, + 'playersAtStreet0': 0, + 'playersAtStreet1': 0, + 'playersAtStreet2': 0, + 'playersAtStreet3': 0, + 'playersAtStreet4': 0, + 'playersVpi': 0, + 'seats': 9, + 'showdownPot': 0, + 'siteHandNo': u'22488827305', + 'startTime': datetime.datetime(2010, 6, 21, 20, 13, tzinfo=pytz.utc), + 'street0Raises': 0, + 'street1Pot': 0, + 'street1Raises': 0, + 'street2Pot': 0, + 'street2Raises': 0, + 'street3Pot': 0, + 'street3Raises': 0, + 'street4Pot': 0, + 'street4Raises': 0, + 'tableName': u'Flash', + 'texture': None, + 'tourneyId': None} From 7dd8b9de16ac86e7a511d33d5ecf9ca0fccf10ae Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 10 Dec 2010 22:31:12 +0800 Subject: [PATCH 12/45] Rename gameTypeId to gametypeId Go through and change all references to gameTypeId to gametypeId to make it consistent. The database field is named with the lowercase version, and MySQL is case sensitive. This may have been causing minor issues in multiple areas when attempting to join on gametype. --- pyfpdb/AlchemyMappings.py | 2 +- pyfpdb/Database.py | 2 +- pyfpdb/GuiPositionalStats.py | 4 +-- pyfpdb/GuiRingPlayerStats.py | 10 +++--- pyfpdb/GuiTourneyPlayerStats.py | 2 +- pyfpdb/Hand.py | 2 +- pyfpdb/SQL.py | 54 ++++++++++++++++----------------- 7 files changed, 38 insertions(+), 38 deletions(-) diff --git a/pyfpdb/AlchemyMappings.py b/pyfpdb/AlchemyMappings.py index 5e93a259..b5891c25 100644 --- a/pyfpdb/AlchemyMappings.py +++ b/pyfpdb/AlchemyMappings.py @@ -211,7 +211,7 @@ class HandInternal(DerivedStats): def isDuplicate(self, session): """Checks if current hand already exists in db - siteHandNo ans gameTypeId have to be setted + siteHandNo ans gametypeId have to be setted """ return session.query(HandInternal).filter_by( siteHandNo=self.siteHandNo, gametypeId=self.gametypeId).count()!=0 diff --git a/pyfpdb/Database.py b/pyfpdb/Database.py index 7b6bfb4a..fa23f1cd 100644 --- a/pyfpdb/Database.py +++ b/pyfpdb/Database.py @@ -1708,7 +1708,7 @@ class Database: c.execute(q, ( p['tableName'], - p['gameTypeId'], + p['gametypeId'], p['siteHandNo'], p['tourneyId'], p['startTime'], diff --git a/pyfpdb/GuiPositionalStats.py b/pyfpdb/GuiPositionalStats.py index 2f7c36cc..06d28883 100644 --- a/pyfpdb/GuiPositionalStats.py +++ b/pyfpdb/GuiPositionalStats.py @@ -397,7 +397,7 @@ class GuiPositionalStats (threading.Thread): query = query.replace("", bigblindselect) query = query.replace("", "") query = query.replace("", "-1") - query = query.replace("", "-1") + query = query.replace("", "-1") else: if self.db.backend == self.MYSQL_INNODB: bigblindselect = """concat('$', trim(leading ' ' from @@ -416,7 +416,7 @@ class GuiPositionalStats (threading.Thread): query = query.replace("", bigblindselect) query = query.replace("", ",gt.bigBlind") query = query.replace("", "hc.gametypeId") - query = query.replace("", "h.gameTypeId") + query = query.replace("", "h.gametypeId") # Filter on dates query = query.replace("", " between '" + dates[0] + "' and '" + dates[1] + "'") diff --git a/pyfpdb/GuiRingPlayerStats.py b/pyfpdb/GuiRingPlayerStats.py index 39ea97b4..d3ba1bee 100644 --- a/pyfpdb/GuiRingPlayerStats.py +++ b/pyfpdb/GuiRingPlayerStats.py @@ -652,18 +652,18 @@ class GuiRingPlayerStats (GuiPlayerStats.GuiPlayerStats): query = query.replace("", bbtest) if holecards: # re-use level variables for hole card query - query = query.replace("", "hp.startcards") - query = query.replace("" + query = query.replace("", "hp.startcards") + query = query.replace("" , ",case when floor((hp.startcards-1)/13) >= mod((hp.startcards-1),13) then hp.startcards + 0.1 " + " else 13*mod((hp.startcards-1),13) + floor((hp.startcards-1)/13) + 1 " + " end desc ") else: - query = query.replace("", "") + query = query.replace("", "") groupLevels = "show" not in str(limits) if groupLevels: - query = query.replace("", "p.name") # need to use p.name for sqlite posn stats to work + query = query.replace("", "p.name") # need to use p.name for sqlite posn stats to work else: - query = query.replace("", "h.gameTypeId") + query = query.replace("", "h.gametypeId") # process self.detailFilters (a list of tuples) flagtest = '' diff --git a/pyfpdb/GuiTourneyPlayerStats.py b/pyfpdb/GuiTourneyPlayerStats.py index ef6c0f66..0b830547 100644 --- a/pyfpdb/GuiTourneyPlayerStats.py +++ b/pyfpdb/GuiTourneyPlayerStats.py @@ -382,7 +382,7 @@ class GuiTourneyPlayerStats (GuiPlayerStats.GuiPlayerStats): #query = query.replace("", bbtest) - #query = query.replace("", "") + #query = query.replace("", "") # process self.detailFilters (a list of tuples) flagtest = '' diff --git a/pyfpdb/Hand.py b/pyfpdb/Hand.py index b2e220b1..8485ca9e 100644 --- a/pyfpdb/Hand.py +++ b/pyfpdb/Hand.py @@ -261,7 +261,7 @@ db: a connected Database object""" if not db.isDuplicate(self.dbid_gt, hh['siteHandNo']): # Hands - Summary information of hand indexed by handId - gameinfo - hh['gameTypeId'] = self.dbid_gt + hh['gametypeId'] = self.dbid_gt # seats TINYINT NOT NULL, hh['seats'] = len(self.dbid_pids) diff --git a/pyfpdb/SQL.py b/pyfpdb/SQL.py index f6bb068b..83e19f13 100644 --- a/pyfpdb/SQL.py +++ b/pyfpdb/SQL.py @@ -1400,11 +1400,11 @@ class Sql: self.query['addTourneyIndex'] = """CREATE UNIQUE INDEX siteTourneyNo ON Tourneys (siteTourneyNo, tourneyTypeId)""" if db_server == 'mysql': - self.query['addHandsIndex'] = """ALTER TABLE Hands ADD UNIQUE INDEX siteHandNo(siteHandNo, gameTypeId)""" + self.query['addHandsIndex'] = """ALTER TABLE Hands ADD UNIQUE INDEX siteHandNo(siteHandNo, gametypeId)""" elif db_server == 'postgresql': - self.query['addHandsIndex'] = """CREATE UNIQUE INDEX siteHandNo ON Hands (siteHandNo, gameTypeId)""" + self.query['addHandsIndex'] = """CREATE UNIQUE INDEX siteHandNo ON Hands (siteHandNo, gametypeId)""" elif db_server == 'sqlite': - self.query['addHandsIndex'] = """CREATE UNIQUE INDEX siteHandNo ON Hands (siteHandNo, gameTypeId)""" + self.query['addHandsIndex'] = """CREATE UNIQUE INDEX siteHandNo ON Hands (siteHandNo, gametypeId)""" if db_server == 'mysql': self.query['addPlayersIndex'] = """ALTER TABLE Players ADD UNIQUE INDEX name(name, siteId)""" @@ -2145,7 +2145,7 @@ class Sql: if db_server == 'mysql': self.query['playerDetailedStats'] = """ - select AS hgametypeid + select AS hgametypeid , AS pname ,gt.base ,gt.category @@ -2231,7 +2231,7 @@ class Sql: ,variance(hp.totalProfit/100.0) AS variance from HandsPlayers hp inner join Hands h on (h.id = hp.handId) - inner join Gametypes gt on (gt.Id = h.gameTypeId) + inner join Gametypes gt on (gt.Id = h.gametypeId) inner join Sites s on (s.Id = gt.siteId) inner join Players p on (p.Id = hp.playerId) where hp.playerId in @@ -2242,7 +2242,7 @@ class Sql: and date_format(h.startTime, '%Y-%m-%d %T') - group by hgameTypeId + group by hgametypeId ,pname ,gt.base ,gt.category @@ -2259,14 +2259,14 @@ class Sql: when 'S' then 'S' else concat('Z', ) end - + ,upper(gt.limitType) desc ,maxbigblind desc ,s.name """ elif db_server == 'postgresql': self.query['playerDetailedStats'] = """ - select AS hgametypeid + select AS hgametypeid , AS pname ,gt.base ,gt.category @@ -2352,7 +2352,7 @@ class Sql: ,variance(hp.totalProfit/100.0) AS variance from HandsPlayers hp inner join Hands h on (h.id = hp.handId) - inner join Gametypes gt on (gt.Id = h.gameTypeId) + inner join Gametypes gt on (gt.Id = h.gametypeId) inner join Sites s on (s.Id = gt.siteId) inner join Players p on (p.Id = hp.playerId) where hp.playerId in @@ -2363,7 +2363,7 @@ class Sql: and to_char(h.startTime, 'YYYY-MM-DD HH24:MI:SS') - group by hgameTypeId + group by hgametypeId ,pname ,gt.base ,gt.category @@ -2381,14 +2381,14 @@ class Sql: when '0' then 'Y' else 'Z'|| end - + ,upper(gt.limitType) desc ,maxbigblind desc ,s.name """ elif db_server == 'sqlite': self.query['playerDetailedStats'] = """ - select AS hgametypeid + select AS hgametypeid , AS pname ,gt.base ,gt.category AS category @@ -2474,7 +2474,7 @@ class Sql: ,variance(hp.totalProfit/100.0) AS variance from HandsPlayers hp inner join Hands h on (h.id = hp.handId) - inner join Gametypes gt on (gt.Id = h.gameTypeId) + inner join Gametypes gt on (gt.Id = h.gametypeId) inner join Sites s on (s.Id = gt.siteId) inner join Players p on (p.Id = hp.playerId) where hp.playerId in @@ -2485,7 +2485,7 @@ class Sql: and datetime(h.startTime) - group by hgameTypeId + group by hgametypeId ,hp.playerId ,gt.base ,gt.category @@ -2503,7 +2503,7 @@ class Sql: when '0' then 'Y' else 'Z'|| end - + ,upper(gt.limitType) desc ,max(gt.bigBlind) desc ,s.name @@ -2693,7 +2693,7 @@ class Sql: ,format( sum(activeSeats*HDs)/(sum(HDs)+0.0), 2) AS AvgSeats from Gametypes gt inner join Sites s on s.Id = gt.siteId - inner join HudCache hc on hc.gameTypeId = gt.Id + inner join HudCache hc on hc.gametypeId = gt.Id where hc.playerId in and and hc.activeSeats @@ -2714,7 +2714,7 @@ class Sql: else variance(hprof.profit/100.0) end as variance from - (select hp.handId, as gtId, hp.totalProfit as profit + (select hp.handId, as gtId, hp.totalProfit as profit from HandsPlayers hp inner join Hands h ON h.id = hp.handId where hp.playerId in @@ -2798,7 +2798,7 @@ class Sql: ,to_char(sum(activeSeats*HDs)/(sum(HDs)+0.0),'90D00') AS AvgSeats from Gametypes gt inner join Sites s on s.Id = gt.siteId - inner join HudCache hc on hc.gameTypeId = gt.Id + inner join HudCache hc on hc.gametypeId = gt.Id where hc.playerId in and and hc.activeSeats @@ -2819,7 +2819,7 @@ class Sql: else variance(hprof.profit/100.0) end as variance from - (select hp.handId, as gtId, hp.totalProfit as profit + (select hp.handId, as gtId, hp.totalProfit as profit from HandsPlayers hp inner join Hands h ON (h.id = hp.handId) where hp.playerId in @@ -2921,7 +2921,7 @@ class Sql: ,format( sum(activeSeats*HDs)/(sum(HDs)+0.0), 2) AS AvgSeats from Gametypes gt inner join Sites s on s.Id = gt.siteId - inner join HudCache hc on hc.gameTypeId = gt.Id + inner join HudCache hc on hc.gametypeId = gt.Id where hc.playerId in and and hc.activeSeats @@ -2951,7 +2951,7 @@ class Sql: else variance(hprof.profit/100.0) end as variance from - (select hp.handId, as gtId, hp.position + (select hp.handId, as gtId, hp.position , hp.totalProfit as profit from HandsPlayers hp inner join Hands h ON (h.id = hp.handId) @@ -3060,7 +3060,7 @@ class Sql: ,to_char(sum(activeSeats*HDs)/(sum(HDs)+0.0),'90D00') AS AvgSeats from Gametypes gt inner join Sites s on (s.Id = gt.siteId) - inner join HudCache hc on (hc.gameTypeId = gt.Id) + inner join HudCache hc on (hc.gametypeId = gt.Id) where hc.playerId in and and hc.activeSeats @@ -3090,14 +3090,14 @@ class Sql: else variance(hprof.profit/100.0) end as variance from - (select hp.handId, as gtId, hp.position + (select hp.handId, as gtId, hp.position , hp.totalProfit as profit from HandsPlayers hp inner join Hands h ON (h.id = hp.handId) where hp.playerId in and hp.tourneysPlayersId IS NULL and to_char(h.startTime, 'YYYY-MM-DD') - group by hp.handId, gameTypeId, hp.position, hp.totalProfit + group by hp.handId, gametypeId, hp.position, hp.totalProfit ) hprof group by hprof.gtId, PlPosition ) hprof2 @@ -3191,7 +3191,7 @@ class Sql: SELECT UNIX_TIMESTAMP(h.startTime) as time, hp.handId, hp.startCash, hp.winnings, hp.totalProfit FROM HandsPlayers hp INNER JOIN Hands h on (h.id = hp.handId) - INNER JOIN Gametypes gt on (gt.Id = h.gameTypeId) + INNER JOIN Gametypes gt on (gt.Id = h.gametypeId) INNER JOIN Sites s on (s.Id = gt.siteId) INNER JOIN Players p on (p.Id = hp.playerId) WHERE hp.playerId in @@ -3203,7 +3203,7 @@ class Sql: SELECT EXTRACT(epoch from h.startTime) as time, hp.handId, hp.startCash, hp.winnings, hp.totalProfit FROM HandsPlayers hp INNER JOIN Hands h on (h.id = hp.handId) - INNER JOIN Gametypes gt on (gt.Id = h.gameTypeId) + INNER JOIN Gametypes gt on (gt.Id = h.gametypeId) INNER JOIN Sites s on (s.Id = gt.siteId) INNER JOIN Players p on (p.Id = hp.playerId) WHERE hp.playerId in @@ -3215,7 +3215,7 @@ class Sql: SELECT STRFTIME('', h.startTime) as time, hp.handId, hp.startCash, hp.winnings, hp.totalProfit FROM HandsPlayers hp INNER JOIN Hands h on (h.id = hp.handId) - INNER JOIN Gametypes gt on (gt.Id = h.gameTypeId) + INNER JOIN Gametypes gt on (gt.Id = h.gametypeId) INNER JOIN Sites s on (s.Id = gt.siteId) INNER JOIN Players p on (p.Id = hp.playerId) WHERE hp.playerId in From b966be8ec39dfad74e6c5a0ab718c669d5052e94 Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 10 Dec 2010 22:46:49 +0800 Subject: [PATCH 13/45] Regression: .hands file for FTP EUR file By default EUR sites (.fr?) use CET in the HH file --- ....02.201008.Weird.table.character.txt.hands | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 pyfpdb/regression-test-files/cash/FTP/Flop/NLHE-6max-EUR-0.01-0.02.201008.Weird.table.character.txt.hands diff --git a/pyfpdb/regression-test-files/cash/FTP/Flop/NLHE-6max-EUR-0.01-0.02.201008.Weird.table.character.txt.hands b/pyfpdb/regression-test-files/cash/FTP/Flop/NLHE-6max-EUR-0.01-0.02.201008.Weird.table.character.txt.hands new file mode 100644 index 00000000..bbfe68a1 --- /dev/null +++ b/pyfpdb/regression-test-files/cash/FTP/Flop/NLHE-6max-EUR-0.01-0.02.201008.Weird.table.character.txt.hands @@ -0,0 +1,32 @@ +{ 'boardcard1': 0, + 'boardcard2': 0, + 'boardcard3': 0, + 'boardcard4': 0, + 'boardcard5': 0, + 'gametypeId': 7, + 'importTime': None, + 'maxSeats': 6, + 'playersAtShowdown': 1, + 'playersAtStreet-1': 6, + 'playersAtStreet0': 1, + 'playersAtStreet1': 1, + 'playersAtStreet2': 1, + 'playersAtStreet3': 1, + 'playersAtStreet4': 0, + 'playersVpi': 1, + 'seats': 6, + 'showdownPot': 0, + 'siteHandNo': u'20000000801', + 'startTime': datetime.datetime(2010, 8, 13, 20, 59, 2, tzinfo=pytz.utc), + 'street0Raises': 1, + 'street1Pot': 0, + 'street1Raises': 0, + 'street2Pot': 0, + 'street2Raises': 0, + 'street3Pot': 0, + 'street3Raises': 0, + 'street4Pot': 0, + 'street4Raises': 0, + 'tableName': u'Douai\u2013Lens', + 'texture': None, + 'tourneyId': None} From f9ee1510b17619da40f2ab86612231c17ce7275d Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 10 Dec 2010 22:59:26 +0800 Subject: [PATCH 14/45] Regression: FTP Razz file, with .hp and .hands We didn't have an FTP Razz file in the Regression test suite. This is the new date format from FTP, like Stars contains both local and ET time --- ...0.10-0.20-201012.CCT.and.ET.timeformat.txt | Bin 0 -> 3470 bytes ....20-201012.CCT.and.ET.timeformat.txt.hands | 32 + ...0-0.20-201012.CCT.and.ET.timeformat.txt.hp | 658 ++++++++++++++++++ 3 files changed, 690 insertions(+) create mode 100644 pyfpdb/regression-test-files/cash/FTP/Stud/Razz-USD-0.10-0.20-201012.CCT.and.ET.timeformat.txt create mode 100644 pyfpdb/regression-test-files/cash/FTP/Stud/Razz-USD-0.10-0.20-201012.CCT.and.ET.timeformat.txt.hands create mode 100644 pyfpdb/regression-test-files/cash/FTP/Stud/Razz-USD-0.10-0.20-201012.CCT.and.ET.timeformat.txt.hp diff --git a/pyfpdb/regression-test-files/cash/FTP/Stud/Razz-USD-0.10-0.20-201012.CCT.and.ET.timeformat.txt b/pyfpdb/regression-test-files/cash/FTP/Stud/Razz-USD-0.10-0.20-201012.CCT.and.ET.timeformat.txt new file mode 100644 index 0000000000000000000000000000000000000000..fd3d975b0d48c59c938c6052fe975f61c4ec2cbe GIT binary patch literal 3470 zcmb_fO>fgc5S=p;|6wIK0D?A-<0R!)(LyDtR3+^JSv@2nX(O7nN>d>!emn5qtT*1+ z>uOt&m3W<|i$CE$5Kgyk4;WBb6bKKWhoyfI(m#M6<{v;z=U~S}uw52PF>`6;H zl1Yj?ulI@_$rxzL9RsmaJ@4@J5)wN=wPd$gx3M;|FH6W!-b%Ef-3Sq1;Ku?VdNP2n zdt7!Wkshp3m;E;K0oQ4R~6xy;DIQK8QJ&=kiR3@NJ|%BpTrno>$<- z$SZix7!?s25M6LqMWi)ES|ECgsM}yP%u#8hQy|*PbW*dC z)eu>M=qsXMLy+ff8|0F4&_*{9dB29}7l;Ii9^$u=`+hUhQBeoCSeKQ+4EU0aFiw(@ z#Yr+VagvNAPLeD&OO`d%Eb~Tq8C1$Z4sai$RtIv3i*;^y$;a32KHt*gpex zNqRkjzJ=UCcBnrSDr zHb&T82lX^tNujrSmz7cHfW5rh2k2k3o+d7Ps>hwI4o=b$^u(VmKVNy&M!WySXq6LF@IhwUk-S!!VQ$#rsUzL6 z`xJiY-8MgG=#uW_PvM`dVuOA)!;|f2c4AyD2G-ZObKc?f!gM?B}H(r-kwV z!iQ5W=W7>V3t1wzdFFJi*|@Lu-I9U{vM_|NI~9C*4t=ba@ND{6E#Y}Jq$NWne9ML+ u3eT`1Ef4V$o@+x|x)1mgo^| Date: Fri, 10 Dec 2010 23:53:51 +0800 Subject: [PATCH 15/45] Regression: Change validated CET time, as date is during DLS --- ...HE-6max-EUR-0.01-0.02.201008.Weird.table.character.txt.hands | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyfpdb/regression-test-files/cash/FTP/Flop/NLHE-6max-EUR-0.01-0.02.201008.Weird.table.character.txt.hands b/pyfpdb/regression-test-files/cash/FTP/Flop/NLHE-6max-EUR-0.01-0.02.201008.Weird.table.character.txt.hands index bbfe68a1..9520547f 100644 --- a/pyfpdb/regression-test-files/cash/FTP/Flop/NLHE-6max-EUR-0.01-0.02.201008.Weird.table.character.txt.hands +++ b/pyfpdb/regression-test-files/cash/FTP/Flop/NLHE-6max-EUR-0.01-0.02.201008.Weird.table.character.txt.hands @@ -17,7 +17,7 @@ 'seats': 6, 'showdownPot': 0, 'siteHandNo': u'20000000801', - 'startTime': datetime.datetime(2010, 8, 13, 20, 59, 2, tzinfo=pytz.utc), + 'startTime': datetime.datetime(2010, 8, 13, 19, 59, 2, tzinfo=pytz.utc), 'street0Raises': 1, 'street1Pot': 0, 'street1Raises': 0, From e26e219327fb95e060365acf9117cd6bcba82db6 Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 10 Dec 2010 23:56:04 +0800 Subject: [PATCH 16/45] Regression: Change validated ET time as date during DLS --- ...-9max-USD-0.02-0.05.201007.Short.lived.date.format.txt.hands | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyfpdb/regression-test-files/cash/FTP/Flop/NLHE-9max-USD-0.02-0.05.201007.Short.lived.date.format.txt.hands b/pyfpdb/regression-test-files/cash/FTP/Flop/NLHE-9max-USD-0.02-0.05.201007.Short.lived.date.format.txt.hands index 767a0c6e..39c79c53 100644 --- a/pyfpdb/regression-test-files/cash/FTP/Flop/NLHE-9max-USD-0.02-0.05.201007.Short.lived.date.format.txt.hands +++ b/pyfpdb/regression-test-files/cash/FTP/Flop/NLHE-9max-USD-0.02-0.05.201007.Short.lived.date.format.txt.hands @@ -17,7 +17,7 @@ 'seats': 9, 'showdownPot': 0, 'siteHandNo': u'22488827305', - 'startTime': datetime.datetime(2010, 6, 21, 20, 13, tzinfo=pytz.utc), + 'startTime': datetime.datetime(2010, 6, 21, 19, 13, tzinfo=pytz.utc), 'street0Raises': 0, 'street1Pot': 0, 'street1Raises': 0, From 702bbb4c7168e09e957b3ee6ca1494853756dd2e Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 10 Dec 2010 23:57:28 +0800 Subject: [PATCH 17/45] Regression: Change validated date - July is the 7th month, not 6th --- ...-9max-USD-0.02-0.05.201007.Short.lived.date.format.txt.hands | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyfpdb/regression-test-files/cash/FTP/Flop/NLHE-9max-USD-0.02-0.05.201007.Short.lived.date.format.txt.hands b/pyfpdb/regression-test-files/cash/FTP/Flop/NLHE-9max-USD-0.02-0.05.201007.Short.lived.date.format.txt.hands index 39c79c53..115e758f 100644 --- a/pyfpdb/regression-test-files/cash/FTP/Flop/NLHE-9max-USD-0.02-0.05.201007.Short.lived.date.format.txt.hands +++ b/pyfpdb/regression-test-files/cash/FTP/Flop/NLHE-9max-USD-0.02-0.05.201007.Short.lived.date.format.txt.hands @@ -17,7 +17,7 @@ 'seats': 9, 'showdownPot': 0, 'siteHandNo': u'22488827305', - 'startTime': datetime.datetime(2010, 6, 21, 19, 13, tzinfo=pytz.utc), + 'startTime': datetime.datetime(2010, 7, 21, 19, 13, tzinfo=pytz.utc), 'street0Raises': 0, 'street1Pot': 0, 'street1Raises': 0, From d922984b91e4d98207e59bf994e68147a23e6aaf Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 10 Dec 2010 23:59:27 +0800 Subject: [PATCH 18/45] THP: Add support for Hands table Will search for .hands in the same manner as it does for .hp files --- pyfpdb/TestHandsPlayers.py | 101 +++++++++++++++++++++++++------------ 1 file changed, 70 insertions(+), 31 deletions(-) diff --git a/pyfpdb/TestHandsPlayers.py b/pyfpdb/TestHandsPlayers.py index 34ca1e36..2d978e67 100755 --- a/pyfpdb/TestHandsPlayers.py +++ b/pyfpdb/TestHandsPlayers.py @@ -29,6 +29,8 @@ import Database import SQL import fpdb_import import Options +import datetime +import pytz class FpdbError: @@ -60,6 +62,69 @@ class FpdbError: idx = f.find('regression') print "(%3d) : %s" %(self.histogram[f], f[idx:]) +def compare_handsplayers_file(filename, importer, errors): + hashfilename = filename + '.hp' + + in_fh = codecs.open(hashfilename, 'r', 'utf8') + whole_file = in_fh.read() + in_fh.close() + + testhash = eval(whole_file) + + hhc = importer.getCachedHHC() + handlist = hhc.getProcessedHands() + #We _really_ only want to deal with a single hand here. + for hand in handlist: + ghash = hand.stats.getHandsPlayers() + for p in ghash: + #print "DEBUG: player: '%s'" % p + pstat = ghash[p] + teststat = testhash[p] + + for stat in pstat: + #print "pstat[%s][%s]: %s == %s" % (p, stat, pstat[stat], teststat[stat]) + try: + if pstat[stat] == teststat[stat]: + # The stats match - continue + pass + else: + # Stats don't match - Doh! + errors.error_report(filename, hand, stat, ghash, testhash, p) + except KeyError, e: + errors.error_report(filename, False, "KeyError: '%s'" % stat, False, False, p) + +def compare_hands_file(filename, importer, errors): + hashfilename = filename + '.hands' + + in_fh = codecs.open(hashfilename, 'r', 'utf8') + whole_file = in_fh.read() + in_fh.close() + + testhash = eval(whole_file) + + hhc = importer.getCachedHHC() + handlist = hhc.getProcessedHands() + + for hand in handlist: + ghash = hand.stats.getHands() + for datum in ghash: + #print "DEBUG: hand: '%s'" % datum + try: + if ghash[datum] == testhash[datum]: + # The stats match - continue + pass + else: + # Stats don't match. + if datum == "gametypeId": + # Not an error. gametypeIds are dependent on the order added to the db. + #print "DEBUG: Skipping mismatched gamtypeId" + pass + else: + errors.error_report(filename, hand, datum, ghash, testhash, None) + except KeyError, e: + errors.error_report(filename, False, "KeyError: '%s'" % stat, False, False, p) + + def compare(leaf, importer, errors, site): filename = leaf #print "DEBUG: fileanme: %s" % filename @@ -70,39 +135,13 @@ def compare(leaf, importer, errors, site): importer.addBulkImportImportFileOrDir(filename, site=site) (stored, dups, partial, errs, ttime) = importer.runImport() - if os.path.isfile(filename + '.hp') and errs < 1: - # Compare them - hashfilename = filename + '.hp' - - in_fh = codecs.open(hashfilename, 'r', 'utf8') - whole_file = in_fh.read() - in_fh.close() - - testhash = eval(whole_file) - - hhc = importer.getCachedHHC() - handlist = hhc.getProcessedHands() - #We _really_ only want to deal with a single hand here. - for hand in handlist: - ghash = hand.stats.getHandsPlayers() - for p in ghash: - #print "DEBUG: player: '%s'" % p - pstat = ghash[p] - teststat = testhash[p] - - for stat in pstat: - #print "pstat[%s][%s]: %s == %s" % (p, stat, pstat[stat], teststat[stat]) - try: - if pstat[stat] == teststat[stat]: - # The stats match - continue - pass - else: - # Stats don't match - Doh! - errors.error_report(filename, hand, stat, ghash, testhash, p) - except KeyError, e: - errors.error_report(filename, False, "KeyError: '%s'" % stat, False, False, p) if errs > 0: errors.error_report(filename, False, "Parse", False, False, False) + else: + if os.path.isfile(filename + '.hp'): + compare_handsplayers_file(filename, importer, errors) + if os.path.isfile(filename + '.hands'): + compare_hands_file(filename, importer, errors) importer.clearFileList() From 3cd7eb881b3a60889acc4dc90c161a8be76a2ea8 Mon Sep 17 00:00:00 2001 From: Worros Date: Sat, 11 Dec 2010 00:01:00 +0800 Subject: [PATCH 19/45] FTP: Change date format handling The new FTP date format contains local and ET time information, in almost the same way as Stars. Tested against a new file, an old file, a EUR file and the weird date format that existed for about 3 days --- pyfpdb/FulltiltToFpdb.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/pyfpdb/FulltiltToFpdb.py b/pyfpdb/FulltiltToFpdb.py index 756dd494..487cb12d 100755 --- a/pyfpdb/FulltiltToFpdb.py +++ b/pyfpdb/FulltiltToFpdb.py @@ -63,10 +63,10 @@ class Fulltilt(HandHistoryConverter): [%(LS)s]?(?P[.0-9]+)/[%(LS)s]?(?P[.0-9]+)\s(Ante\s[%(LS)s]?(?P[.0-9]+)\s)?-\s [%(LS)s]?(?P[.0-9]+\sCap\s)? (?P[-\da-zA-Z\/\'\s]+)\s-\s - (?P\d+:\d+:\d+\s(?P\w+)\s-\s\d+/\d+/\d+|\d+:\d+\s(?P\w+)\s-\s\w+\,\s\w+\s\d+\,\s\d+) + (?P.*$) (?P\(partial\))?\n (?:.*?\n(?PHand\s\#(?P=HID)\shas\sbeen\scanceled))? - ''' % substitutions, re.VERBOSE|re.DOTALL) + ''' % substitutions, re.MULTILINE|re.VERBOSE) re_TourneyExtraInfo = re.compile('''(((?P[^$]+)? (?P[%(LS)s])?(?P[.0-9]+)?\s*\+\s*[%(LS)s]?(?P[.0-9]+)? (\s(?P(KO|Heads\sUp|Matrix\s\dx|Rebuy|Madness)))? @@ -123,6 +123,7 @@ class Fulltilt(HandHistoryConverter): re_Mixed = re.compile(r'\s\-\s(?PHA|HORSE|HOSE)\s\-\s', re.VERBOSE) re_Max = re.compile("(?P\d+)( max)?", re.MULTILINE) # NB: if we ever match "Full Tilt Poker" we should also match "FullTiltPoker", which PT Stud erroneously exports. + re_DateTime = re.compile("""((?P[0-9]+):(?P[0-9]+):(?P[0-9]+)\s(?P\w+)\s-\s(?P[0-9]{4})\/(?P[0-9]{2})\/(?P[0-9]{2})|(?P

[0-9]+):(?P[0-9]+)\s(?P\w+)\s-\s\w+\,\s(?P\w+)\s(?P\d+)\,\s(?P[0-9]{4}))""", re.MULTILINE) @@ -225,18 +226,24 @@ class Fulltilt(HandHistoryConverter): hand.handid = m.group('HID') hand.tablename = m.group('TABLE') - timezone = "ET" - if m.group('TZ1') == "CET" or m.group('TZ2') == "CET": - timezone = "CET" - try: - stringformat = "%H:%M:%S " + m.group('TZ1') + " - %Y/%m/%d" - hand.startTime = datetime.datetime.strptime(m.group('DATETIME'), stringformat) - except: - stringformat = "%H:%M " + m.group('TZ2') + " - %a, %B %d, %Y" - hand.startTime = datetime.datetime.strptime(m.group('DATETIME'), stringformat) + if m.group('DATETIME'): + # This section of code should match either a single date (which is ET) or + # the last date in the header, which is also recorded in ET. + timezone = "ET" + m1 = self.re_DateTime.finditer(m.group('DATETIME')) + datetimestr = "2000/01/01 00:00:00" + for a in m1: + if a.group('TZ2') == None: + datetimestr = "%s/%s/%s %s:%s:%s" % (a.group('Y'), a.group('M'),a.group('D'),a.group('H'),a.group('MIN'),a.group('S')) + timezone = a.group('TZ') + hand.startTime = datetime.datetime.strptime(datetimestr, "%Y/%m/%d %H:%M:%S") + else: # Short-lived date format + datetimestr = "%s/%s/%s %s:%s" % (a.group('Y2'), a.group('M2'),a.group('D2'),a.group('H2'),a.group('MIN2')) + timezone = a.group('TZ2') + hand.startTime = datetime.datetime.strptime(datetimestr, "%Y/%B/%d %H:%M") + + hand.startTime = HandHistoryConverter.changeTimezone(hand.startTime, timezone, "UTC") - hand.startTime = HandHistoryConverter.changeTimezone(hand.startTime, timezone, "UTC") - if m.group("CANCELLED") or m.group("PARTIAL"): raise FpdbParseError(hid=m.group('HID')) From ee0ed080ed40ea0929df6302dd08ce604a14363f Mon Sep 17 00:00:00 2001 From: Worros Date: Sat, 11 Dec 2010 00:34:27 +0800 Subject: [PATCH 20/45] DerivedStats: Remove erroneous comment --- pyfpdb/DerivedStats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyfpdb/DerivedStats.py b/pyfpdb/DerivedStats.py index cd63e871..003f5270 100644 --- a/pyfpdb/DerivedStats.py +++ b/pyfpdb/DerivedStats.py @@ -135,7 +135,7 @@ class DerivedStats(): #print "DEBUG: vpip: %s" %(self.hands['playersVpi']) self.playersAtStreetX(hand) # Gives playersAtStreet1..4 and Showdown #print "DEBUG: playersAtStreet 1:'%s' 2:'%s' 3:'%s' 4:'%s'" %(self.hands['playersAtStreet1'],self.hands['playersAtStreet2'],self.hands['playersAtStreet3'],self.hands['playersAtStreet4']) - self.streetXRaises(hand) # Empty function currently + self.streetXRaises(hand) def assembleHandsPlayers(self, hand): #street0VPI/vpip already called in Hand From c47fc8b8c04a2488c3bb478d2b0cdf52917a4233 Mon Sep 17 00:00:00 2001 From: Worros Date: Sat, 11 Dec 2010 01:17:15 +0800 Subject: [PATCH 21/45] Regression: FTP Razz crasher Player6 is listed as playing the hand and sits out between the listing and ante-up Crashes in encodeRazzStartHand --- ...0-201012.Player6.listed.but.doesnt.act.txt | Bin 0 -> 3342 bytes ...12.Player6.listed.but.doesnt.act.txt.hands | 32 + ...01012.Player6.listed.but.doesnt.act.txt.hp | 752 ++++++++++++++++++ 3 files changed, 784 insertions(+) create mode 100644 pyfpdb/regression-test-files/cash/FTP/Stud/Razz-USD-0.10-0.20-201012.Player6.listed.but.doesnt.act.txt create mode 100644 pyfpdb/regression-test-files/cash/FTP/Stud/Razz-USD-0.10-0.20-201012.Player6.listed.but.doesnt.act.txt.hands create mode 100644 pyfpdb/regression-test-files/cash/FTP/Stud/Razz-USD-0.10-0.20-201012.Player6.listed.but.doesnt.act.txt.hp diff --git a/pyfpdb/regression-test-files/cash/FTP/Stud/Razz-USD-0.10-0.20-201012.Player6.listed.but.doesnt.act.txt b/pyfpdb/regression-test-files/cash/FTP/Stud/Razz-USD-0.10-0.20-201012.Player6.listed.but.doesnt.act.txt new file mode 100644 index 0000000000000000000000000000000000000000..1f82d7fa41a557ce766c4638e810b2748843af22 GIT binary patch literal 3342 zcmb_fO>fgc5S=p;|6wIK00J7vA8~uDXrZd8RFd?7tR9>+X)2neN?aijza4mQ*N*M= zCUv2*TyNNUvorH%W`{q&Kgzw_;!!e`8+~52w+B1L&ZZ4|s<8F7=voJm}iPuO4<1d;<%0yc2UN&*hm6U|Xtf@U({~_`O10 zl)Qq)A$;mV?`uo8t7Xp-iBx`Q1id060)kPq5q<;VR}hS~ji5K=s5~Rc5gyKnByJ$W z1|rN6F%XQreMZzkL^*rb|k!&YPmTrWRS?dbQ@OL5C_`O7(XF6No z2p=Urs_O{f0ji-d$9PytW|z@C(iLDjLZ&mnv^$Yg8AG}&S8|DOg1lQ}<;GQgVGM~T z?qO-Nn4nj*lO~f1{Ay=fJR0)Mc~B*>;+GRCz>+jyB!MmbD|yiG&d@Ir!JumKy3&y+Gv^k(|IT zg(-|Iv)@ng*VM9^xK?`S9(`be6=hA0*1fPZf?UnfYsQ!e*NuGPE_>HP=T(MmC)n!?Oa-d9k#gVJ(reUwzy}&$7*rUg%3>} lB`@wN@v&C8Cr92xyu)}S@qZR%rJjLr@h?R#bDx}7-@oh}), + 'street0Raises': 0, + 'street1Pot': 0, + 'street1Raises': 1, + 'street2Pot': 0, + 'street2Raises': 1, + 'street3Pot': 0, + 'street3Raises': 0, + 'street4Pot': 0, + 'street4Raises': 0, + 'tableName': u'Mart', + 'texture': None, + 'tourneyId': None} diff --git a/pyfpdb/regression-test-files/cash/FTP/Stud/Razz-USD-0.10-0.20-201012.Player6.listed.but.doesnt.act.txt.hp b/pyfpdb/regression-test-files/cash/FTP/Stud/Razz-USD-0.10-0.20-201012.Player6.listed.but.doesnt.act.txt.hp new file mode 100644 index 00000000..2fffb326 --- /dev/null +++ b/pyfpdb/regression-test-files/cash/FTP/Stud/Razz-USD-0.10-0.20-201012.Player6.listed.but.doesnt.act.txt.hp @@ -0,0 +1,752 @@ +{ u'Hero': { 'card1': 36, + 'card2': 14, + 'card3': 44, + 'card4': 8, + 'card5': 34, + 'card6': 0, + 'card7': 0, + 'foldBbToStealChance': False, + 'foldSbToStealChance': False, + 'foldToOtherRaisedStreet0': False, + 'foldToOtherRaisedStreet1': False, + 'foldToOtherRaisedStreet2': True, + 'foldToOtherRaisedStreet3': False, + 'foldToOtherRaisedStreet4': False, + 'foldToStreet1CBChance': False, + 'foldToStreet1CBDone': False, + 'foldToStreet2CBChance': False, + 'foldToStreet2CBDone': False, + 'foldToStreet3CBChance': False, + 'foldToStreet3CBDone': False, + 'foldToStreet4CBChance': False, + 'foldToStreet4CBDone': False, + 'foldedBbToSteal': False, + 'foldedSbToSteal': False, + 'other3BStreet0': False, + 'other4BStreet0': False, + 'otherRaisedStreet0': False, + 'otherRaisedStreet1': False, + 'otherRaisedStreet2': True, + 'otherRaisedStreet3': False, + 'otherRaisedStreet4': False, + 'position': 5, + 'raiseFirstInChance': True, + 'raisedFirstIn': False, + 'rake': 0, + 'sawShowdown': False, + 'seatNo': 1, + 'sitout': False, + 'startCards': 395, + 'startCash': 574, + 'street0Aggr': False, + 'street0Bets': 0, + 'street0Calls': 1, + 'street0Raises': 0, + 'street0VPI': True, + 'street0_3BChance': False, + 'street0_3BDone': False, + 'street0_4BChance': False, + 'street0_4BDone': False, + 'street1Aggr': True, + 'street1Bets': 1, + 'street1CBChance': False, + 'street1CBDone': False, + 'street1Calls': 0, + 'street1CheckCallRaiseChance': False, + 'street1CheckCallRaiseDone': False, + 'street1Raises': 0, + 'street1Seen': True, + 'street2Aggr': False, + 'street2Bets': 0, + 'street2CBChance': False, + 'street2CBDone': False, + 'street2Calls': 0, + 'street2CheckCallRaiseChance': False, + 'street2CheckCallRaiseDone': False, + 'street2Raises': 0, + 'street2Seen': True, + 'street3Aggr': False, + 'street3Bets': 0, + 'street3CBChance': False, + 'street3CBDone': False, + 'street3Calls': 0, + 'street3CheckCallRaiseChance': False, + 'street3CheckCallRaiseDone': False, + 'street3Raises': 0, + 'street3Seen': False, + 'street4Aggr': False, + 'street4Bets': 0, + 'street4CBChance': False, + 'street4CBDone': False, + 'street4Calls': 0, + 'street4CheckCallRaiseChance': False, + 'street4CheckCallRaiseDone': False, + 'street4Raises': 0, + 'street4Seen': False, + 'totalProfit': -15, + 'tourneyTypeId': None, + 'tourneysPlayersIds': None, + 'winnings': 0, + 'wonAtSD': 0.0, + 'wonWhenSeenStreet1': 0.0, + 'wonWhenSeenStreet2': 0.0, + 'wonWhenSeenStreet3': 0.0, + 'wonWhenSeenStreet4': 0.0}, + u'Player2': { 'card1': 0, + 'card2': 0, + 'card3': 23, + 'card4': 0, + 'card5': 0, + 'card6': 0, + 'card7': 0, + 'foldBbToStealChance': False, + 'foldSbToStealChance': False, + 'foldToOtherRaisedStreet0': False, + 'foldToOtherRaisedStreet1': False, + 'foldToOtherRaisedStreet2': False, + 'foldToOtherRaisedStreet3': False, + 'foldToOtherRaisedStreet4': False, + 'foldToStreet1CBChance': False, + 'foldToStreet1CBDone': False, + 'foldToStreet2CBChance': False, + 'foldToStreet2CBDone': False, + 'foldToStreet3CBChance': False, + 'foldToStreet3CBDone': False, + 'foldToStreet4CBChance': False, + 'foldToStreet4CBDone': False, + 'foldedBbToSteal': False, + 'foldedSbToSteal': False, + 'other3BStreet0': False, + 'other4BStreet0': False, + 'otherRaisedStreet0': False, + 'otherRaisedStreet1': False, + 'otherRaisedStreet2': False, + 'otherRaisedStreet3': False, + 'otherRaisedStreet4': False, + 'position': 4, + 'raiseFirstInChance': False, + 'raisedFirstIn': False, + 'rake': 0, + 'sawShowdown': False, + 'seatNo': 2, + 'sitout': False, + 'startCards': -3, + 'startCash': 650, + 'street0Aggr': False, + 'street0Bets': 0, + 'street0Calls': 0, + 'street0Raises': 0, + 'street0VPI': False, + 'street0_3BChance': False, + 'street0_3BDone': False, + 'street0_4BChance': False, + 'street0_4BDone': False, + 'street1Aggr': False, + 'street1Bets': 0, + 'street1CBChance': False, + 'street1CBDone': False, + 'street1Calls': 0, + 'street1CheckCallRaiseChance': False, + 'street1CheckCallRaiseDone': False, + 'street1Raises': 0, + 'street1Seen': False, + 'street2Aggr': False, + 'street2Bets': 0, + 'street2CBChance': False, + 'street2CBDone': False, + 'street2Calls': 0, + 'street2CheckCallRaiseChance': False, + 'street2CheckCallRaiseDone': False, + 'street2Raises': 0, + 'street2Seen': False, + 'street3Aggr': False, + 'street3Bets': 0, + 'street3CBChance': False, + 'street3CBDone': False, + 'street3Calls': 0, + 'street3CheckCallRaiseChance': False, + 'street3CheckCallRaiseDone': False, + 'street3Raises': 0, + 'street3Seen': False, + 'street4Aggr': False, + 'street4Bets': 0, + 'street4CBChance': False, + 'street4CBDone': False, + 'street4Calls': 0, + 'street4CheckCallRaiseChance': False, + 'street4CheckCallRaiseDone': False, + 'street4Raises': 0, + 'street4Seen': False, + 'totalProfit': -2, + 'tourneyTypeId': None, + 'tourneysPlayersIds': None, + 'winnings': 0, + 'wonAtSD': 0.0, + 'wonWhenSeenStreet1': 0.0, + 'wonWhenSeenStreet2': 0.0, + 'wonWhenSeenStreet3': 0.0, + 'wonWhenSeenStreet4': 0.0}, + u'Player3': { 'card1': 0, + 'card2': 0, + 'card3': 26, + 'card4': 10, + 'card5': 0, + 'card6': 0, + 'card7': 0, + 'foldBbToStealChance': False, + 'foldSbToStealChance': False, + 'foldToOtherRaisedStreet0': False, + 'foldToOtherRaisedStreet1': True, + 'foldToOtherRaisedStreet2': False, + 'foldToOtherRaisedStreet3': False, + 'foldToOtherRaisedStreet4': False, + 'foldToStreet1CBChance': False, + 'foldToStreet1CBDone': False, + 'foldToStreet2CBChance': False, + 'foldToStreet2CBDone': False, + 'foldToStreet3CBChance': False, + 'foldToStreet3CBDone': False, + 'foldToStreet4CBChance': False, + 'foldToStreet4CBDone': False, + 'foldedBbToSteal': False, + 'foldedSbToSteal': False, + 'other3BStreet0': False, + 'other4BStreet0': False, + 'otherRaisedStreet0': False, + 'otherRaisedStreet1': True, + 'otherRaisedStreet2': False, + 'otherRaisedStreet3': False, + 'otherRaisedStreet4': False, + 'position': 3, + 'raiseFirstInChance': False, + 'raisedFirstIn': False, + 'rake': 0, + 'sawShowdown': False, + 'seatNo': 3, + 'sitout': False, + 'startCards': -13, + 'startCash': 690, + 'street0Aggr': False, + 'street0Bets': 0, + 'street0Calls': 1, + 'street0Raises': 0, + 'street0VPI': True, + 'street0_3BChance': False, + 'street0_3BDone': False, + 'street0_4BChance': False, + 'street0_4BDone': False, + 'street1Aggr': False, + 'street1Bets': 0, + 'street1CBChance': False, + 'street1CBDone': False, + 'street1Calls': 0, + 'street1CheckCallRaiseChance': False, + 'street1CheckCallRaiseDone': False, + 'street1Raises': 0, + 'street1Seen': True, + 'street2Aggr': False, + 'street2Bets': 0, + 'street2CBChance': False, + 'street2CBDone': False, + 'street2Calls': 0, + 'street2CheckCallRaiseChance': False, + 'street2CheckCallRaiseDone': False, + 'street2Raises': 0, + 'street2Seen': False, + 'street3Aggr': False, + 'street3Bets': 0, + 'street3CBChance': False, + 'street3CBDone': False, + 'street3Calls': 0, + 'street3CheckCallRaiseChance': False, + 'street3CheckCallRaiseDone': False, + 'street3Raises': 0, + 'street3Seen': False, + 'street4Aggr': False, + 'street4Bets': 0, + 'street4CBChance': False, + 'street4CBDone': False, + 'street4Calls': 0, + 'street4CheckCallRaiseChance': False, + 'street4CheckCallRaiseDone': False, + 'street4Raises': 0, + 'street4Seen': False, + 'totalProfit': -5, + 'tourneyTypeId': None, + 'tourneysPlayersIds': None, + 'winnings': 0, + 'wonAtSD': 0.0, + 'wonWhenSeenStreet1': 0.0, + 'wonWhenSeenStreet2': 0.0, + 'wonWhenSeenStreet3': 0.0, + 'wonWhenSeenStreet4': 0.0}, + u'Player4': { 'card1': 0, + 'card2': 0, + 'card3': 41, + 'card4': 0, + 'card5': 0, + 'card6': 0, + 'card7': 0, + 'foldBbToStealChance': False, + 'foldSbToStealChance': False, + 'foldToOtherRaisedStreet0': False, + 'foldToOtherRaisedStreet1': False, + 'foldToOtherRaisedStreet2': False, + 'foldToOtherRaisedStreet3': False, + 'foldToOtherRaisedStreet4': False, + 'foldToStreet1CBChance': False, + 'foldToStreet1CBDone': False, + 'foldToStreet2CBChance': False, + 'foldToStreet2CBDone': False, + 'foldToStreet3CBChance': False, + 'foldToStreet3CBDone': False, + 'foldToStreet4CBChance': False, + 'foldToStreet4CBDone': False, + 'foldedBbToSteal': False, + 'foldedSbToSteal': False, + 'other3BStreet0': False, + 'other4BStreet0': False, + 'otherRaisedStreet0': False, + 'otherRaisedStreet1': False, + 'otherRaisedStreet2': False, + 'otherRaisedStreet3': False, + 'otherRaisedStreet4': False, + 'position': 2, + 'raiseFirstInChance': False, + 'raisedFirstIn': False, + 'rake': 0, + 'sawShowdown': False, + 'seatNo': 4, + 'sitout': False, + 'startCards': -11, + 'startCash': 168, + 'street0Aggr': False, + 'street0Bets': 0, + 'street0Calls': 0, + 'street0Raises': 0, + 'street0VPI': False, + 'street0_3BChance': False, + 'street0_3BDone': False, + 'street0_4BChance': False, + 'street0_4BDone': False, + 'street1Aggr': False, + 'street1Bets': 0, + 'street1CBChance': False, + 'street1CBDone': False, + 'street1Calls': 0, + 'street1CheckCallRaiseChance': False, + 'street1CheckCallRaiseDone': False, + 'street1Raises': 0, + 'street1Seen': False, + 'street2Aggr': False, + 'street2Bets': 0, + 'street2CBChance': False, + 'street2CBDone': False, + 'street2Calls': 0, + 'street2CheckCallRaiseChance': False, + 'street2CheckCallRaiseDone': False, + 'street2Raises': 0, + 'street2Seen': False, + 'street3Aggr': False, + 'street3Bets': 0, + 'street3CBChance': False, + 'street3CBDone': False, + 'street3Calls': 0, + 'street3CheckCallRaiseChance': False, + 'street3CheckCallRaiseDone': False, + 'street3Raises': 0, + 'street3Seen': False, + 'street4Aggr': False, + 'street4Bets': 0, + 'street4CBChance': False, + 'street4CBDone': False, + 'street4Calls': 0, + 'street4CheckCallRaiseChance': False, + 'street4CheckCallRaiseDone': False, + 'street4Raises': 0, + 'street4Seen': False, + 'totalProfit': -2, + 'tourneyTypeId': None, + 'tourneysPlayersIds': None, + 'winnings': 0, + 'wonAtSD': 0.0, + 'wonWhenSeenStreet1': 0.0, + 'wonWhenSeenStreet2': 0.0, + 'wonWhenSeenStreet3': 0.0, + 'wonWhenSeenStreet4': 0.0}, + u'Player5': { 'card1': 0, + 'card2': 0, + 'card3': 6, + 'card4': 0, + 'card5': 0, + 'card6': 0, + 'card7': 0, + 'foldBbToStealChance': False, + 'foldSbToStealChance': False, + 'foldToOtherRaisedStreet0': False, + 'foldToOtherRaisedStreet1': False, + 'foldToOtherRaisedStreet2': False, + 'foldToOtherRaisedStreet3': False, + 'foldToOtherRaisedStreet4': False, + 'foldToStreet1CBChance': False, + 'foldToStreet1CBDone': False, + 'foldToStreet2CBChance': False, + 'foldToStreet2CBDone': False, + 'foldToStreet3CBChance': False, + 'foldToStreet3CBDone': False, + 'foldToStreet4CBChance': False, + 'foldToStreet4CBDone': False, + 'foldedBbToSteal': False, + 'foldedSbToSteal': False, + 'other3BStreet0': False, + 'other4BStreet0': False, + 'otherRaisedStreet0': False, + 'otherRaisedStreet1': False, + 'otherRaisedStreet2': False, + 'otherRaisedStreet3': False, + 'otherRaisedStreet4': False, + 'position': 1, + 'raiseFirstInChance': False, + 'raisedFirstIn': False, + 'rake': 0, + 'sawShowdown': False, + 'seatNo': 5, + 'sitout': False, + 'startCards': -7, + 'startCash': 802, + 'street0Aggr': False, + 'street0Bets': 0, + 'street0Calls': 0, + 'street0Raises': 0, + 'street0VPI': False, + 'street0_3BChance': False, + 'street0_3BDone': False, + 'street0_4BChance': False, + 'street0_4BDone': False, + 'street1Aggr': False, + 'street1Bets': 0, + 'street1CBChance': False, + 'street1CBDone': False, + 'street1Calls': 0, + 'street1CheckCallRaiseChance': False, + 'street1CheckCallRaiseDone': False, + 'street1Raises': 0, + 'street1Seen': False, + 'street2Aggr': False, + 'street2Bets': 0, + 'street2CBChance': False, + 'street2CBDone': False, + 'street2Calls': 0, + 'street2CheckCallRaiseChance': False, + 'street2CheckCallRaiseDone': False, + 'street2Raises': 0, + 'street2Seen': False, + 'street3Aggr': False, + 'street3Bets': 0, + 'street3CBChance': False, + 'street3CBDone': False, + 'street3Calls': 0, + 'street3CheckCallRaiseChance': False, + 'street3CheckCallRaiseDone': False, + 'street3Raises': 0, + 'street3Seen': False, + 'street4Aggr': False, + 'street4Bets': 0, + 'street4CBChance': False, + 'street4CBDone': False, + 'street4Calls': 0, + 'street4CheckCallRaiseChance': False, + 'street4CheckCallRaiseDone': False, + 'street4Raises': 0, + 'street4Seen': False, + 'totalProfit': -2, + 'tourneyTypeId': None, + 'tourneysPlayersIds': None, + 'winnings': 0, + 'wonAtSD': 0.0, + 'wonWhenSeenStreet1': 0.0, + 'wonWhenSeenStreet2': 0.0, + 'wonWhenSeenStreet3': 0.0, + 'wonWhenSeenStreet4': 0.0}, + u'Player6': { 'card1': 0, + 'card2': 0, + 'card3': 0, + 'card4': 0, + 'card5': 0, + 'card6': 0, + 'card7': 0, + 'foldBbToStealChance': False, + 'foldSbToStealChance': False, + 'foldToOtherRaisedStreet0': False, + 'foldToOtherRaisedStreet1': False, + 'foldToOtherRaisedStreet2': False, + 'foldToOtherRaisedStreet3': False, + 'foldToOtherRaisedStreet4': False, + 'foldToStreet1CBChance': False, + 'foldToStreet1CBDone': False, + 'foldToStreet2CBChance': False, + 'foldToStreet2CBDone': False, + 'foldToStreet3CBChance': False, + 'foldToStreet3CBDone': False, + 'foldToStreet4CBChance': False, + 'foldToStreet4CBDone': False, + 'foldedBbToSteal': False, + 'foldedSbToSteal': False, + 'other3BStreet0': False, + 'other4BStreet0': False, + 'otherRaisedStreet0': False, + 'otherRaisedStreet1': False, + 'otherRaisedStreet2': False, + 'otherRaisedStreet3': False, + 'otherRaisedStreet4': False, + 'position': 2, + 'raiseFirstInChance': False, + 'raisedFirstIn': False, + 'rake': 0, + 'sawShowdown': False, + 'seatNo': 6, + 'sitout': False, + 'startCards': 0, + 'startCash': 54, + 'street0Aggr': False, + 'street0Bets': 0, + 'street0Calls': 0, + 'street0Raises': 0, + 'street0VPI': False, + 'street0_3BChance': False, + 'street0_3BDone': False, + 'street0_4BChance': False, + 'street0_4BDone': False, + 'street1Aggr': False, + 'street1Bets': 0, + 'street1CBChance': False, + 'street1CBDone': False, + 'street1Calls': 0, + 'street1CheckCallRaiseChance': False, + 'street1CheckCallRaiseDone': False, + 'street1Raises': 0, + 'street1Seen': False, + 'street2Aggr': False, + 'street2Bets': 0, + 'street2CBChance': False, + 'street2CBDone': False, + 'street2Calls': 0, + 'street2CheckCallRaiseChance': False, + 'street2CheckCallRaiseDone': False, + 'street2Raises': 0, + 'street2Seen': False, + 'street3Aggr': False, + 'street3Bets': 0, + 'street3CBChance': False, + 'street3CBDone': False, + 'street3Calls': 0, + 'street3CheckCallRaiseChance': False, + 'street3CheckCallRaiseDone': False, + 'street3Raises': 0, + 'street3Seen': False, + 'street4Aggr': False, + 'street4Bets': 0, + 'street4CBChance': False, + 'street4CBDone': False, + 'street4Calls': 0, + 'street4CheckCallRaiseChance': False, + 'street4CheckCallRaiseDone': False, + 'street4Raises': 0, + 'street4Seen': False, + 'totalProfit': 0, + 'tourneyTypeId': None, + 'tourneysPlayersIds': None, + 'winnings': 0, + 'wonAtSD': 0.0, + 'wonWhenSeenStreet1': 0.0, + 'wonWhenSeenStreet2': 0.0, + 'wonWhenSeenStreet3': 0.0, + 'wonWhenSeenStreet4': 0.0}, + u'Player7': { 'card1': 0, + 'card2': 0, + 'card3': 52, + 'card4': 0, + 'card5': 0, + 'card6': 0, + 'card7': 0, + 'foldBbToStealChance': False, + 'foldSbToStealChance': False, + 'foldToOtherRaisedStreet0': False, + 'foldToOtherRaisedStreet1': False, + 'foldToOtherRaisedStreet2': False, + 'foldToOtherRaisedStreet3': False, + 'foldToOtherRaisedStreet4': False, + 'foldToStreet1CBChance': False, + 'foldToStreet1CBDone': False, + 'foldToStreet2CBChance': False, + 'foldToStreet2CBDone': False, + 'foldToStreet3CBChance': False, + 'foldToStreet3CBDone': False, + 'foldToStreet4CBChance': False, + 'foldToStreet4CBDone': False, + 'foldedBbToSteal': False, + 'foldedSbToSteal': False, + 'other3BStreet0': False, + 'other4BStreet0': False, + 'otherRaisedStreet0': False, + 'otherRaisedStreet1': False, + 'otherRaisedStreet2': False, + 'otherRaisedStreet3': False, + 'otherRaisedStreet4': False, + 'position': 0, + 'raiseFirstInChance': False, + 'raisedFirstIn': False, + 'rake': 0, + 'sawShowdown': False, + 'seatNo': 7, + 'sitout': False, + 'startCards': -13, + 'startCash': 281, + 'street0Aggr': False, + 'street0Bets': 0, + 'street0Calls': 0, + 'street0Raises': 0, + 'street0VPI': False, + 'street0_3BChance': False, + 'street0_3BDone': False, + 'street0_4BChance': False, + 'street0_4BDone': False, + 'street1Aggr': False, + 'street1Bets': 0, + 'street1CBChance': False, + 'street1CBDone': False, + 'street1Calls': 0, + 'street1CheckCallRaiseChance': False, + 'street1CheckCallRaiseDone': False, + 'street1Raises': 0, + 'street1Seen': False, + 'street2Aggr': False, + 'street2Bets': 0, + 'street2CBChance': False, + 'street2CBDone': False, + 'street2Calls': 0, + 'street2CheckCallRaiseChance': False, + 'street2CheckCallRaiseDone': False, + 'street2Raises': 0, + 'street2Seen': False, + 'street3Aggr': False, + 'street3Bets': 0, + 'street3CBChance': False, + 'street3CBDone': False, + 'street3Calls': 0, + 'street3CheckCallRaiseChance': False, + 'street3CheckCallRaiseDone': False, + 'street3Raises': 0, + 'street3Seen': False, + 'street4Aggr': False, + 'street4Bets': 0, + 'street4CBChance': False, + 'street4CBDone': False, + 'street4Calls': 0, + 'street4CheckCallRaiseChance': False, + 'street4CheckCallRaiseDone': False, + 'street4Raises': 0, + 'street4Seen': False, + 'totalProfit': -2, + 'tourneyTypeId': None, + 'tourneysPlayersIds': None, + 'winnings': 0, + 'wonAtSD': 0.0, + 'wonWhenSeenStreet1': 0.0, + 'wonWhenSeenStreet2': 0.0, + 'wonWhenSeenStreet3': 0.0, + 'wonWhenSeenStreet4': 0.0}, + u'Player8': { 'card1': 0, + 'card2': 0, + 'card3': 37, + 'card4': 46, + 'card5': 18, + 'card6': 0, + 'card7': 0, + 'foldBbToStealChance': False, + 'foldSbToStealChance': False, + 'foldToOtherRaisedStreet0': False, + 'foldToOtherRaisedStreet1': False, + 'foldToOtherRaisedStreet2': False, + 'foldToOtherRaisedStreet3': False, + 'foldToOtherRaisedStreet4': False, + 'foldToStreet1CBChance': False, + 'foldToStreet1CBDone': False, + 'foldToStreet2CBChance': False, + 'foldToStreet2CBDone': False, + 'foldToStreet3CBChance': False, + 'foldToStreet3CBDone': False, + 'foldToStreet4CBChance': False, + 'foldToStreet4CBDone': False, + 'foldedBbToSteal': False, + 'foldedSbToSteal': False, + 'other3BStreet0': False, + 'other4BStreet0': False, + 'otherRaisedStreet0': False, + 'otherRaisedStreet1': True, + 'otherRaisedStreet2': False, + 'otherRaisedStreet3': False, + 'otherRaisedStreet4': False, + 'position': 'S', + 'raiseFirstInChance': False, + 'raisedFirstIn': False, + 'rake': 2, + 'sawShowdown': False, + 'seatNo': 8, + 'sitout': False, + 'startCards': -2, + 'startCash': 187, + 'street0Aggr': False, + 'street0Bets': 0, + 'street0Calls': 0, + 'street0Raises': 0, + 'street0VPI': False, + 'street0_3BChance': False, + 'street0_3BDone': False, + 'street0_4BChance': False, + 'street0_4BDone': False, + 'street1Aggr': False, + 'street1Bets': 0, + 'street1CBChance': False, + 'street1CBDone': False, + 'street1Calls': 1, + 'street1CheckCallRaiseChance': False, + 'street1CheckCallRaiseDone': False, + 'street1Raises': 0, + 'street1Seen': True, + 'street2Aggr': True, + 'street2Bets': 1, + 'street2CBChance': False, + 'street2CBDone': False, + 'street2Calls': 0, + 'street2CheckCallRaiseChance': False, + 'street2CheckCallRaiseDone': False, + 'street2Raises': 0, + 'street2Seen': True, + 'street3Aggr': False, + 'street3Bets': 0, + 'street3CBChance': False, + 'street3CBDone': False, + 'street3Calls': 0, + 'street3CheckCallRaiseChance': False, + 'street3CheckCallRaiseDone': False, + 'street3Raises': 0, + 'street3Seen': False, + 'street4Aggr': False, + 'street4Bets': 0, + 'street4CBChance': False, + 'street4CBDone': False, + 'street4Calls': 0, + 'street4CheckCallRaiseChance': False, + 'street4CheckCallRaiseDone': False, + 'street4Raises': 0, + 'street4Seen': False, + 'totalProfit': 26, + 'tourneyTypeId': None, + 'tourneysPlayersIds': None, + 'winnings': 41, + 'wonAtSD': 0.0, + 'wonWhenSeenStreet1': 1.0, + 'wonWhenSeenStreet2': 1.0, + 'wonWhenSeenStreet3': 0.0, + 'wonWhenSeenStreet4': 0.0}} From 5a5ab451d1d06e4c7354d9e1c5f3cb990533a210 Mon Sep 17 00:00:00 2001 From: Worros Date: Sat, 11 Dec 2010 01:19:49 +0800 Subject: [PATCH 22/45] Regression: Fix datetime format for eval() call --- ...USD-0.10-0.20-201012.Player6.listed.but.doesnt.act.txt.hands | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyfpdb/regression-test-files/cash/FTP/Stud/Razz-USD-0.10-0.20-201012.Player6.listed.but.doesnt.act.txt.hands b/pyfpdb/regression-test-files/cash/FTP/Stud/Razz-USD-0.10-0.20-201012.Player6.listed.but.doesnt.act.txt.hands index 5ab8e46e..7123f7df 100644 --- a/pyfpdb/regression-test-files/cash/FTP/Stud/Razz-USD-0.10-0.20-201012.Player6.listed.but.doesnt.act.txt.hands +++ b/pyfpdb/regression-test-files/cash/FTP/Stud/Razz-USD-0.10-0.20-201012.Player6.listed.but.doesnt.act.txt.hands @@ -17,7 +17,7 @@ 'seats': 8, 'showdownPot': 0, 'siteHandNo': u'26190500040', - 'startTime': datetime.datetime(2010, 12, 7, 9, 19, tzinfo=), + 'startTime': datetime.datetime(2010, 12, 7, 9, 19, tzinfo=pytz.utc), 'street0Raises': 0, 'street1Pot': 0, 'street1Raises': 1, From caa2104fa94c0918dbb28c8e903c2781c1ef8d11 Mon Sep 17 00:00:00 2001 From: Worros Date: Sat, 11 Dec 2010 01:26:56 +0800 Subject: [PATCH 23/45] Fix for FTP Razz crasher When a player is listed as playing the hand and sits out between the listing and ante-up join_holecards returned only 2 cards causing Card.encodeRazzStartHand() to crash. Make sure that join_holecards must return at least 3 cards for stud, and add an entry for this case in the lookup table --- pyfpdb/Card.py | 1 + pyfpdb/Hand.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/pyfpdb/Card.py b/pyfpdb/Card.py index b3ce09ff..8d426a0d 100755 --- a/pyfpdb/Card.py +++ b/pyfpdb/Card.py @@ -303,6 +303,7 @@ def encodeRazzStartHand(cards): encodeRazzList = { '(00)A':-13,'(00)2':-12,'(00)3':-11,'(00)4':-10,'(00)5':-9,'(00)6':-8,'(00)7':-7,'(00)8':-6,'(00)9':-5,'(00)T':-4, '(00)J':-3,'(00)Q':-2,'(00)K':-1, + '(00)0':0, '(32)A':1,'(3A)2':2,'(2A)3':3,'(42)A':4,'(4A)2':5,'(2A)4':6,'(43)A':7,'(4A)3':8,'(3A)4':9, '(43)2':10,'(42)3':11,'(32)4':12,'(52)A':13,'(5A)2':14,'(2A)5':15,'(53)A':16,'(5A)3':17,'(3A)5':18,'(53)2':19, '(52)3':20,'(32)5':21,'(54)A':22,'(5A)4':23,'(4A)5':24,'(54)2':25,'(52)4':26,'(42)5':27,'(54)3':28,'(53)4':29, diff --git a/pyfpdb/Hand.py b/pyfpdb/Hand.py index 8485ca9e..c2bea220 100644 --- a/pyfpdb/Hand.py +++ b/pyfpdb/Hand.py @@ -1580,6 +1580,11 @@ Add a complete on [street] by [player] to [amountTo] else: log.warning(_("join_holecards: # of holecards should be either < 4, 4 or 7 - 5 and 6 should be impossible for anyone who is not a hero")) log.warning(_("join_holcards: holecards(%s): %s") %(player, holecards)) + if holecards == [u'0x', u'0x']: + log.warning(_("join_holecards: Player '%s' appears not to have been dealt a card")) + # If a player is listed but not dealt a card in a cash game this can occur + # Noticed in FTP Razz hand. Return 3 empty cards in this case + holecards = [u'0x', u'0x', u'0x'] return holecards From df7bfedd8978a0a57d7645d4986494665856d75c Mon Sep 17 00:00:00 2001 From: Worros Date: Sat, 11 Dec 2010 01:45:23 +0800 Subject: [PATCH 24/45] Regression: add PLO .hands file Appears I forgot to add this previously --- ...-0.01-0.02.201003.River.Showdown.txt.hands | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 pyfpdb/regression-test-files/cash/FTP/Flop/PLO-6max-USD-0.01-0.02.201003.River.Showdown.txt.hands diff --git a/pyfpdb/regression-test-files/cash/FTP/Flop/PLO-6max-USD-0.01-0.02.201003.River.Showdown.txt.hands b/pyfpdb/regression-test-files/cash/FTP/Flop/PLO-6max-USD-0.01-0.02.201003.River.Showdown.txt.hands new file mode 100644 index 00000000..9278fc04 --- /dev/null +++ b/pyfpdb/regression-test-files/cash/FTP/Flop/PLO-6max-USD-0.01-0.02.201003.River.Showdown.txt.hands @@ -0,0 +1,32 @@ +{ 'boardcard1': 37, + 'boardcard2': 20, + 'boardcard3': 8, + 'boardcard4': 51, + 'boardcard5': 5, + 'gametypeId': 8, + 'importTime': None, + 'maxSeats': 6, + 'playersAtShowdown': 2, + 'playersAtStreet-1': 6, + 'playersAtStreet0': 4, + 'playersAtStreet1': 3, + 'playersAtStreet2': 3, + 'playersAtStreet3': 2, + 'playersAtStreet4': 0, + 'playersVpi': 3, + 'seats': 6, + 'showdownPot': 0, + 'siteHandNo': u'18932478237', + 'startTime': datetime.datetime(2010, 3, 3, 16, 37, 56, tzinfo=pytz.utc), + 'street0Raises': 0, + 'street1Pot': 20, + 'street1Raises': 1, + 'street2Pot': 50, + 'street2Raises': 1, + 'street3Pot': 100, + 'street3Raises': 1, + 'street4Pot': 0, + 'street4Raises': 0, + 'tableName': u'Gaines', + 'texture': None, + 'tourneyId': None} From f0a5864bf4699822066d872f019c93b298754f23 Mon Sep 17 00:00:00 2001 From: Eratosthenes Date: Fri, 10 Dec 2010 13:41:04 -0500 Subject: [PATCH 25/45] Fix autoclose on tourney table change. --- pyfpdb/HUD_main.pyw | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyfpdb/HUD_main.pyw b/pyfpdb/HUD_main.pyw index 38d435bc..e58260c8 100644 --- a/pyfpdb/HUD_main.pyw +++ b/pyfpdb/HUD_main.pyw @@ -312,8 +312,9 @@ def idle_kill(hud_main, table): gtk.gdk.threads_enter() try: if table in hud_main.hud_dict: - hud_main.hud_dict[table].kill() hud_main.vb.remove(hud_main.hud_dict[table].tablehudlabel) + hud_main.hud_dict[table].main_window.destroy() + hud_main.hud_dict[table].kill() del(hud_main.hud_dict[table]) hud_main.main_window.resize(1, 1) except: From f023527595a4c7726217bea6c51f274dc8a7a660 Mon Sep 17 00:00:00 2001 From: Eratosthenes Date: Fri, 10 Dec 2010 19:35:43 -0500 Subject: [PATCH 26/45] Make HUD_main executable (again). --- pyfpdb/HUD_main.pyw | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 pyfpdb/HUD_main.pyw diff --git a/pyfpdb/HUD_main.pyw b/pyfpdb/HUD_main.pyw old mode 100644 new mode 100755 From 669fb58471d3678b40b4c5e7dc463fb3a8a8d3d4 Mon Sep 17 00:00:00 2001 From: Eratosthenes Date: Sat, 11 Dec 2010 10:42:04 -0500 Subject: [PATCH 27/45] Make windows more nearly happy - adapt change from Eric. --- pyfpdb/HUD_main.pyw | 3 ++- pyfpdb/TableWindow.py | 2 +- pyfpdb/WinTables.py | 5 +++++ 3 files changed, 8 insertions(+), 2 deletions(-) mode change 100755 => 100644 pyfpdb/HUD_main.pyw diff --git a/pyfpdb/HUD_main.pyw b/pyfpdb/HUD_main.pyw old mode 100755 new mode 100644 index 55fa7d93..34b701c9 --- a/pyfpdb/HUD_main.pyw +++ b/pyfpdb/HUD_main.pyw @@ -343,7 +343,8 @@ def idle_create(hud_main, new_hand_id, table, temp_key, max, poker_game, type, s gtk.gdk.threads_enter() try: - table.gdkhandle = gtk.gdk.window_foreign_new(table.number) + if table.gdkhandle is not None: # on windows this should already be set + table.gdkhandle = gtk.gdk.window_foreign_new(table.number) newlabel = gtk.Label("%s - %s" % (table.site, temp_key)) hud_main.vb.add(newlabel) newlabel.show() diff --git a/pyfpdb/TableWindow.py b/pyfpdb/TableWindow.py index 2f13634f..05c6ce4e 100644 --- a/pyfpdb/TableWindow.py +++ b/pyfpdb/TableWindow.py @@ -117,6 +117,7 @@ class Table_Window(object): self.config = config self.site = site self.hud = None # fill in later + self.gdkhandle = None if tournament is not None and table_number is not None: self.tournament = int(tournament) self.table = int(table_number) @@ -136,7 +137,6 @@ class Table_Window(object): self.search_string = getTableTitleRe(self.config, self.site, self.type, **table_kwargs) self.find_table_parameters() -# self.gdkhandle = gtk.gdk.window_foreign_new(self.number) geo = self.get_geometry() if geo is None: return None self.width = geo['width'] diff --git a/pyfpdb/WinTables.py b/pyfpdb/WinTables.py index b7114ae3..599e51c6 100644 --- a/pyfpdb/WinTables.py +++ b/pyfpdb/WinTables.py @@ -76,6 +76,11 @@ class Table(Table_Window): self.title = titles[hwnd] self.hud = None self.number = hwnd + if self.gdkhandle is not None: + try: # Windows likes this here - Linux doesn't + self.gdkhandle = gtk.gdk.window_foreign_new(self.number) + except AttributeError: + pass def get_geometry(self): try: From d351205fc8751d96bd35a2ead7cf2bbeeceefd22 Mon Sep 17 00:00:00 2001 From: gimick Date: Sat, 11 Dec 2010 23:59:45 +0000 Subject: [PATCH 28/45] pypokereval : Draft walkthrough to create python site-package --- ...ypokereval-win32-packaging-walkthrough.txt | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 packaging/windows/pypokereval-win32-packaging-walkthrough.txt diff --git a/packaging/windows/pypokereval-win32-packaging-walkthrough.txt b/packaging/windows/pypokereval-win32-packaging-walkthrough.txt new file mode 100644 index 00000000..f5527309 --- /dev/null +++ b/packaging/windows/pypokereval-win32-packaging-walkthrough.txt @@ -0,0 +1,155 @@ +pypokereval packaging for fpdb project +-------------------------------------- + +Created by Gimick on 11th December 2010 +Content is available under the the GNU Affero General Public License version 3 + +Background +---------- + +The walktrough builds an unoffical python installer package for pypokereval. + +In a previous walkthrough, the pypokereval dll(pyd) was built from source. +In this walkthrough, we are going to generate a windows package which will allow +the sources to be installed through distutils onto a client computer + +The current situation is that there is no windows build or windows installer provided +by the package authors. + +Until a package is available, the fpdb project needs some method of providing a windows +installation for the pypokereval module. This walkthrough will therefore be obsolete once +an official package becomes available. + +Actually, for windows fpdb users, the majority will use a pre-built fpdb executable (which contains the +pypokereval package) Therefore, pypokereval installation will only be needed for two groups of people: + i) anyone wanting to build an fpdb package using py2exe + ii) anyone wanting to run fpdb on windows from source + +Credits +------- + +To loic@dachary.org at pokersource http://pokersource.sourceforge.net/ +To donn.ingle@gmail.com for the tutorial here ... http://wiki.python.org/moin/Distutils/Tutorial +Official python reference here ... http://docs.python.org/distutils/index.html + +Assumptions +----------- + +The underlying dll(pyd) and pokereval library is built for win32 x86 platform only, so we will assume +32bit only in this walkthrough. Contributions for a x86-64 build are most welcome. + +Similarly, the underlying package is built against the python 2-6 library and is valid only for that version. +The underlying package was not built with sse enabled, and therefore should work for legacy systems. + + +1. Install pre-requisites +------------------------- + +System used for building is winXP home + +1.1/ Install python runtime from here ... + +Python 2.6.5 ... http://www.python.org/ftp/python/2.6.5/python-2.6.5.msi + +1.2/ Grab pypokereval stuff from fpdb project here ... + +pypokereval ... http://sourceforge.net/projects/fpdb/files/fpdb/pypoker-eval-win32/pypokereval-138-win32-py265-fpdb-1.1.exe/download + +1.3/ Double click the pypokereval-138-win32-py265-fpdb-1.1.exe and extract the folder to the desktop + + +2. Prepare a folder containing the items needing packaging +---------------------------------------------------------- + +dos>cd desktop +dos>mkdir pypokereval138-installer-win32-py265-fpdb +dos>cd pypokereval138-installer-win32-py265-fpdb +dos>mkdir payload +dos>cd payload + +dos>copy ..\..\pypokereval-138-win32-py265-fpdb-1.1\_pokereval_2_6.pyd +dos>copy ..\..\pypokereval-138-win32-py265-fpdb-1.1\pokereval.py + +dos>mkdir utils +dos>cd utils +dos>copy ..\..\..\pypokereval-138-win32-py265-fpdb-1.1\test.py +dos>cd .. + +The next step is to rename the pokereval.py file to __init__.py. The reason for doing this +is that site-packages require an __init__ file to be found when the module is imported at runtime +Inserting a dummy __init__.py which simply imports pokereval does not work, the reason being that the +PokerEval class is not seen by the caller. Syntax such as "from pokereval import PokerEval" +does not work unless the pokereval.py (which defines the PokerEval class) is renamed to __init__.py. + +dos> rename pokereval.py __init__.py + +3. Prepare the additional packaging files + +dos>cd desktop +dos>cd pypokereval138-installer-win32-py265-fpdb + +3.1/ setup.py +------------- + +dos> edit setup.py + +Include the following python code: + +#================================================== + +from distutils.core import setup + +filelist = ["utils/*", "_pokereval_2_6.pyd"] + +setup(name = "pokereval", +version = "138", +description = "pypokereval installer (unofficial)", +author = "project fpdb", +author_email = "Fpdb-main@lists.sourceforge.net", +url = "fpdb.sourceforge.net", +packages = ['pokereval'], +package_data = {'pokereval' : filelist }, +long_description = """An unofficial installer for pypokereval v138, win32, python v2.6.5 +pokereval official project page is at http://www.pokersource.info/""" +) + +#================================================== + +3.2 / review directory tree +--------------------------- + +The following structure should now exist. + +pypokereval138-installer-win32-py265-fpdb + |--setup.py + |--pokereval + |--_pokereval_2_6.pyd + |--__init__.py + |--utils + |--test.py + + +3.3 Build +--------- + +Navigate to the desktop/pypokereval138-installer-win32-py265-fpdb directory + +dos>c:\Python26\python.exe setup.py bdist_wininst --plat-name=win32 --user-access-control auto + +3.4 Complete +------------ + +The executable file pokereval-138.win32.exe will be in the newly-created dist folder. + +3.5 Install and test +-------------------- + +Double click to install +Navigate to c:\python26\lib\site-packages\pokereval\test + +execute dos>c:\Python26\python.exe test.py + +Output should scroll down the screen + + + From c6fbca1e374ffde15e4cd9d72dd74f483f339bd5 Mon Sep 17 00:00:00 2001 From: gimick Date: Sun, 12 Dec 2010 19:48:10 +0000 Subject: [PATCH 29/45] pokereval: remove windows pypokereval files - these files now installed in site-packages --- pyfpdb/_pokereval_2_6.pyd | Bin 143360 -> 0 bytes pyfpdb/pokereval.py | 338 -------------------------------------- 2 files changed, 338 deletions(-) delete mode 100644 pyfpdb/_pokereval_2_6.pyd delete mode 100644 pyfpdb/pokereval.py diff --git a/pyfpdb/_pokereval_2_6.pyd b/pyfpdb/_pokereval_2_6.pyd deleted file mode 100644 index f11d3081e396138da2d9b9bc2aadb69ca1388e57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 143360 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 diff --git a/pyfpdb/pokereval.py b/pyfpdb/pokereval.py deleted file mode 100644 index 08d08786..00000000 --- a/pyfpdb/pokereval.py +++ /dev/null @@ -1,338 +0,0 @@ -# -# Copyright (C) 2007, 2008 Loic Dachary -# Copyright (C) 2004, 2005, 2006 Mekensleep -# -# Mekensleep -# 24 rue vieille du temple -# 75004 Paris -# licensing@mekensleep.com -# -# 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 3 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. -# -# Authors: -# Loic Dachary -# -# -import sys - -# for fpdb alter the following line from __import__ to import...as... . -# fpdb py2exe package build does no recognise __import__ statement, -# and fails at runtime with _pokereval_2_6 not found - -#_pokereval = __import__('_pokereval_' + sys.version[0] + '_' + sys.version[2]) -import _pokereval_2_6 as _pokereval - -from types import * - -class PokerEval: - """\ -Evaluate the strengh of a poker hand for a given poker variant. -In all methods, when a list of cards is to be provided (for instance -with the "hand" argument of the "best" method), each member of the -list may be a number or a string designating a card according to -the following table: - - 2h/00 2d/13 2c/26 2s/39 - 3h/01 3d/14 3c/27 3s/40 - 4h/02 4d/15 4c/28 4s/41 - 5h/03 5d/16 5c/29 5s/42 - 6h/04 6d/17 6c/30 6s/43 - 7h/05 7d/18 7c/31 7s/44 - 8h/06 8d/19 8c/32 8s/45 - 9h/07 9d/20 9c/33 9s/46 - Th/08 Td/21 Tc/34 Ts/47 - Jh/09 Jd/22 Jc/35 Js/48 - Qh/10 Qd/23 Qc/36 Qs/49 - Kh/11 Kd/24 Kc/37 Ks/50 - Ah/12 Ad/25 Ac/38 As/51 - -The string __ (two underscore) or the number 255 are placeholders -meaning that the card is unknown. -""" - - def best(self, side, hand, board = []): - """\ -Return the best five card combination that can be made with the cards -listed in "hand" and, optionally, board. The "side" may be "hi" or -"low". The "board" argument must only be provided for variants where -knowing if a given card is taken from the board or not is significant -(such as Omaha but not Holdem). - -A list is returned. The first element is the numerical value -of the hand (better hands have higher values if "side" is "hi" and -lower values if "side" is "low"). The second element is a list whose -first element is the strength of the hand among the following: - -Nothing (only if "side" equals "low") -NoPair -TwoPair -Trips -Straight -Flush -FlHouse -Quads -StFlush - -The last five elements are numbers describing the best hand properly -sorted (for instance the ace is at the end for no pair if "side" is low or -at the beginning if "side" high). - -Examples: - -[134414336, ['StFlush', 29, 28, 27, 26, 38]] is the wheel five to ace, clubs -[475920, ['NoPair', 45, 29, 41, 39, 51]] is As, 8s, 5c, 4s, 2s -[268435455, ['Nothing']] means there is no qualifying low -""" - if len(hand + board) >= 5: - return _pokereval.eval_hand(side, hand, board) - else: - return False - - def best_hand(self, side, hand, board = []): - """\ -Return the best five card combination that can be made with the cards -listed in "hand" and, optionaly, board. The "side" may be "hi" or -"low". The returned value is the second element of the list returned -by the "best" method. -""" - if len(hand + board) >= 5: - return _pokereval.eval_hand(side, hand, board)[1] - else: - return False - - def best_hand_value(self, side, hand, board = []): - """\ -Return the best five card combination that can be made with the cards -listed in "hand" and, optionaly, board. The "side" may be "hi" or -"low". The returned value is the first element of the list returned -by the "best" method. -""" - if len(hand + board) >= 5: - return _pokereval.eval_hand(side, hand, board)[0] - else: - return False - - def evaln(self, cards): - """\ -Call the poker-eval Hand_EVAL_N function with the "cards" argument. -Return the strength of the "cards" as a number. The higher the -better. -""" - return _pokereval.evaln(cards) - - def winners(self, *args, **kwargs): - """\ -Return a list of the indexes of the best hands, relative to the "pockets" -keyword argument. For instance, if the first pocket and third pocket cards -tie, the list would be [0, 2]. Since there may be more than one way to -win a hand, a hash is returned with the list of the winners for each so -called side. For instace {'hi': [0], 'low': [1]} means pocket cards -at index 0 won the high side of the hand and pocket cards at index 1 -won the low side. - -See the"poker_eval" method for a detailed -explanation of the semantics of the arguments. - -If the keyword argument "fill_pockets" is set, pocket cards -can contain a placeholder (i.e. 255 or __) that will be be -used as specified in the "poker_eval" method documentation. - -If the keyword argument "fill_pockets" is not set, pocket cards -that contain at least one placeholder (i.e. 255 or __) are -ignored completly. For instance if winners is called as follows -o.winners(game = 'holdem', pockets = [ [ '__', 'As' ], [ 'Ks', 'Kd'] ]) -it is strictly equivalent as calling -o.winners(game = 'holdem', pockets = [ [ 'Ks', 'Kd'] ]). -""" - index2index = {} - normalized_pockets = [] - normalized_index = 0 - pockets = kwargs["pockets"][:] - for index in xrange(len(pockets)): - if not kwargs.has_key("fill_pockets"): - if 255 in pockets[index] or "__" in pockets[index]: - pockets[index] = [] - - if pockets[index] != []: - normalized_pockets.append(pockets[index]) - index2index[index] = normalized_index - normalized_index += 1 - kwargs["pockets"] = normalized_pockets - - results = _pokereval.poker_eval(*args, **kwargs) - - (count, haslopot, hashipot) = results.pop(0) - winners = { 'low': [], 'hi': [] } - for index in xrange(len(pockets)): - if index2index.has_key(index): - result = results[index2index[index]] - if result[1] == 1 or result[3] == 1: - winners["hi"].append(index) - if result[4] == 1 or result[6] == 1: - winners["low"].append(index) - - if not haslopot or len(winners["low"]) == 0: - del winners["low"] - if not hashipot: - del winners["hi"] - return winners - - def poker_eval(self, *args, **kwargs): - """\ -Provided with a description of a poker game, return the outcome (if at showdown) or -the expected value of each hand. The poker game description is provided as a set -of keyword arguments with the following meaning: - -game : the variant (holdem, holdem8, omaha, omaha8, 7stud, 7stud8, razz, - 5draw, 5draw8, 5drawnsq, lowball, lowball27). - Mandatory, no default. - -pockets : list of pocket cards for each player still in game. Each member - of the list is a list of cards. The position of the pocket cards - in the list is meaningfull for the value returned will refer to - this position when stating which player wins, tie or loose. - Example: [ ["tc", "ac"], ["3h", "ah"], ["8c", "6h"]] - Cards do not have to be real cards like "tc" or "4s". They may also be a - placeholder, denoted by "__" or 255. When using placeholders, the - keyword argument "iterations" may be specified to use Monte Carlo instead of - exhaustive exploration of all the possible combinations. - Example2: [ ["tc", "__"], [255, "ah"], ["8c", "6h"]] - - Mandatory, no default. - -board : list of community cards, for games where this is meaningfull. If - specified when irrelevant, the return value cannot be predicted. - Default: [] - -dead : list of dead cards. These cards won't be accounted for when exloring - the possible hands. - Default: [] - -iterations: the maximum number of iterations when exploring the - possible outcome of a given hand. Roughly speaking, each - iteration means to distribute cards that are missing (for - which there are place holders in the board or pockets - keywords arguments, i.e. 255 or __). If the number of - iterations is not specified and there are place holders, - the return value cannot be predicted. - Default: +infinite (i.e. exhaustive exploration) - -Example: object.poker_eval(game = "holdem", - pockets = [ ["tc", "ac"], ["3h", "ah"], ["8c", "6h"]], - dead = [], - board = ["7h", "3s", "2c"]) - -The return value is a map of two entries: -'info' contains three integers: - - the number of samples (which must be equal to the number of iterations given - in argument). - - 1 if the game has a low side, 0 otherwise - - 1 if the game has a high side, 0 otherwise -'eval' is a list of as many maps as there are pocket cards, each -made of the following entries: - 'scoop': the number of time these pocket cards scoop - 'winhi': the number of time these pocket cards win the high side - 'losehi': the number of time these pocket cards lose the high side - 'tiehi': the number of time these pocket cards tie for the high side - 'winlo': the number of time these pocket cards win the low side - 'loselo': the number of time these pocket cards lose the low side - 'tielo': the number of time these pocket cards tie for the low side - 'ev': the EV of these pocket cards as an int in the range [0,1000] with - 1000 being the best. - -It should be clear that if there is only one sample (i.e. because all the -cards are known which is the situation that occurs at showdown) the details -provided by the 'eval' entry is mostly irrelevant and the caller might -prefer to call the winners method instead. -""" - result = _pokereval.poker_eval(*args, **kwargs) - return { - 'info': result[0], - 'eval': [ { 'scoop': x[0], - 'winhi': x[1], - 'losehi': x[2], - 'tiehi': x[3], - 'winlo': x[4], - 'loselo': x[5], - 'tielo': x[6], - 'ev': int(x[7] * 1000) } for x in result[1:] ] - } - - def deck(self): - """\ -Return the list of all cards in the deck. -""" - return [ self.string2card(i + j) for i in "23456789TJQKA" for j in "hdcs" ] - - def nocard(self): - """Return 255, the numerical value of a place holder in a list of cards.""" - return 255 - - def string2card(self, cards): - """\ -Convert card names (strings) to card numbers (integers) according to the -following map: - - 2h/00 2d/13 2c/26 2s/39 - 3h/01 3d/14 3c/27 3s/40 - 4h/02 4d/15 4c/28 4s/41 - 5h/03 5d/16 5c/29 5s/42 - 6h/04 6d/17 6c/30 6s/43 - 7h/05 7d/18 7c/31 7s/44 - 8h/06 8d/19 8c/32 8s/45 - 9h/07 9d/20 9c/33 9s/46 - Th/08 Td/21 Tc/34 Ts/47 - Jh/09 Jd/22 Jc/35 Js/48 - Qh/10 Qd/23 Qc/36 Qs/49 - Kh/11 Kd/24 Kc/37 Ks/50 - Ah/12 Ad/25 Ac/38 As/51 - -The "cards" argument may be either a list in which case a converted list -is returned or a string in which case the corresponding number is -returned. -""" - if type(cards) is ListType or type(cards) is TupleType: - return [ _pokereval.string2card(card) for card in cards ] - else: - return _pokereval.string2card(cards) - - def card2string(self, cards): - """\ -Convert card numbers (integers) to card names (strings) according to the -following map: - - 2h/00 2d/13 2c/26 2s/39 - 3h/01 3d/14 3c/27 3s/40 - 4h/02 4d/15 4c/28 4s/41 - 5h/03 5d/16 5c/29 5s/42 - 6h/04 6d/17 6c/30 6s/43 - 7h/05 7d/18 7c/31 7s/44 - 8h/06 8d/19 8c/32 8s/45 - 9h/07 9d/20 9c/33 9s/46 - Th/08 Td/21 Tc/34 Ts/47 - Jh/09 Jd/22 Jc/35 Js/48 - Qh/10 Qd/23 Qc/36 Qs/49 - Kh/11 Kd/24 Kc/37 Ks/50 - Ah/12 Ad/25 Ac/38 As/51 - -The "cards" argument may be either a list in which case a converted list -is returned or an integer in which case the corresponding string is -returned. -""" - if type(cards) is ListType or type(cards) is TupleType: - return [ _pokereval.card2string(card) for card in cards ] - else: - return _pokereval.card2string(cards) - From 3e34a7804f6e7fd5ea11ac98c73452e68d416fc3 Mon Sep 17 00:00:00 2001 From: gimick Date: Sun, 12 Dec 2010 19:54:43 +0000 Subject: [PATCH 30/45] pokereval: update py2exe walkthrough, add build instructions for windows site-package --- .../windows/py2exeWalkthroughPython26.txt | 9 ++++--- ...ypokereval-win32-packaging-walkthrough.txt | 26 +++++++++++-------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/packaging/windows/py2exeWalkthroughPython26.txt b/packaging/windows/py2exeWalkthroughPython26.txt index 68a74c81..7ad865c0 100644 --- a/packaging/windows/py2exeWalkthroughPython26.txt +++ b/packaging/windows/py2exeWalkthroughPython26.txt @@ -30,14 +30,17 @@ py2exe 0.6.9 ... http://sourceforge.net/projects/py2exe/files/py2exe/0.6.9/py2ex psycopg2 ... http://www.stickpeople.com/projects/python/win-psycopg/psycopg2-2.2.1.win32-py2.6-pg8.4.3-release.exe (Note: stickpeople is the offical repository, not a community build) +(py)pokereval v138 ... http://sourceforge.net/projects/fpdb/files/fpdb/pypoker-eval-win32/pokereval-138.win32.exe/download +(Note: There are no official windows builds, this installer is built from source. A walkthrough is in the same directory as this walkthrough. 1.2/ MySQL -MySQL-python-1.2.3.win32-py2.6-fpdb0.20.exe ... http://www.mediafire.com/file/iodnnnznmj1/MySQL-python-1.2.3.win32-py2.6-fpdb0.20.exe +Install the following file: -This is an intaller built from source by gimick. There are no official mysql-python2.6 builds for windows. +MySQL-python-1.2.3.win32-py2.6-fpdb0.20.exe ... http://sourceforge.net/projects/fpdb/files/fpdb/MySQL-python-1.2.3-win32-py2.6/MySQL-python-1.2.3.win32-py2.6-fpdb0.20.exe/download -Community builds are also available from some developers. see www.codegood.com for example. +Note: This is an intaller built from source by gimick. A walkthrough to build this installer is in the same directory as this walkthrough. +Note: There is no official mysql-python2.6 build for windows. Community builds are available from some developers. see www.codegood.com for example. 1.3/ pytz fixup to work in an executable package diff --git a/packaging/windows/pypokereval-win32-packaging-walkthrough.txt b/packaging/windows/pypokereval-win32-packaging-walkthrough.txt index f5527309..b0fbe5f9 100644 --- a/packaging/windows/pypokereval-win32-packaging-walkthrough.txt +++ b/packaging/windows/pypokereval-win32-packaging-walkthrough.txt @@ -53,7 +53,7 @@ Python 2.6.5 ... http://www.python.org/ftp/python/2.6.5/python-2.6.5.msi 1.2/ Grab pypokereval stuff from fpdb project here ... -pypokereval ... http://sourceforge.net/projects/fpdb/files/fpdb/pypoker-eval-win32/pypokereval-138-win32-py265-fpdb-1.1.exe/download +pypokereval ... http://sourceforge.net/projects/fpdb/files/fpdb/pypoker-eval-win32/dev/pypokereval-138-win32-py265-fpdb-1.1.exe/download 1.3/ Double click the pypokereval-138-win32-py265-fpdb-1.1.exe and extract the folder to the desktop @@ -62,10 +62,10 @@ pypokereval ... http://sourceforge.net/projects/fpdb/files/fpdb/pypoker-eval-win ---------------------------------------------------------- dos>cd desktop -dos>mkdir pypokereval138-installer-win32-py265-fpdb -dos>cd pypokereval138-installer-win32-py265-fpdb -dos>mkdir payload -dos>cd payload +dos>mkdir temp +dos>cd temp +dos>mkdir pokereval +dos>cd pokereval dos>copy ..\..\pypokereval-138-win32-py265-fpdb-1.1\_pokereval_2_6.pyd dos>copy ..\..\pypokereval-138-win32-py265-fpdb-1.1\pokereval.py @@ -84,9 +84,10 @@ does not work unless the pokereval.py (which defines the PokerEval class) is ren dos> rename pokereval.py __init__.py 3. Prepare the additional packaging files +----------------------------------------- dos>cd desktop -dos>cd pypokereval138-installer-win32-py265-fpdb +dos>cd temp 3.1/ setup.py ------------- @@ -109,8 +110,9 @@ author_email = "Fpdb-main@lists.sourceforge.net", url = "fpdb.sourceforge.net", packages = ['pokereval'], package_data = {'pokereval' : filelist }, -long_description = """An unofficial installer for pypokereval v138, win32, python v2.6.5 -pokereval official project page is at http://www.pokersource.info/""" +long_description = """An unofficial and experimental installer for pypokereval v138 +Built for 32bit windows and python v2.6.5 +pokereval official project page is at http://www.pokersource.info/""" ) #================================================== @@ -120,7 +122,7 @@ pokereval official project page is at http://www.pokersource.info/""" The following structure should now exist. -pypokereval138-installer-win32-py265-fpdb +temp |--setup.py |--pokereval |--_pokereval_2_6.pyd @@ -132,9 +134,11 @@ pypokereval138-installer-win32-py265-fpdb 3.3 Build --------- -Navigate to the desktop/pypokereval138-installer-win32-py265-fpdb directory +Navigate to the desktop/temp directory -dos>c:\Python26\python.exe setup.py bdist_wininst --plat-name=win32 --user-access-control auto +dos>c:\Python26\python.exe setup.py bdist_wininst --plat-name=win32 --user-access-control force + +Note: UAC auto seems to cause lockup on my win7 system 3.4 Complete ------------ From 6d8f1ca4f9d1da86627def15cda943e1d8197944 Mon Sep 17 00:00:00 2001 From: Eratosthenes Date: Mon, 13 Dec 2010 13:45:04 -0500 Subject: [PATCH 31/45] New window from id routine. --- pyfpdb/XTables.py | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/pyfpdb/XTables.py b/pyfpdb/XTables.py index 5ae3112e..b9900054 100644 --- a/pyfpdb/XTables.py +++ b/pyfpdb/XTables.py @@ -65,28 +65,35 @@ class Table(Table_Window): if self.number is None: return None - (self.window, self.parent) = self.get_window_from_xid(self.number) +# def get_window_from_xid(self, id): +# for outside in root.query_tree().children: +# if outside.id == id: +# return (outside, outside.query_tree().parent) +# for inside in outside.query_tree().children: +# if inside.id == id: # GNOME, Xfce +# return (inside, inside.query_tree().parent) +# for wayinside in inside.query_tree().children: +# if wayinside.id == id: # KDE +# parent = wayinside.query_tree().parent +# return (wayinside, parent.query_tree().parent) +# return (None, None) + def get_window_from_xid(self, id): - for outside in root.query_tree().children: - if outside.id == id: - return (outside, outside.query_tree().parent) - for inside in outside.query_tree().children: - if inside.id == id: # GNOME, Xfce - return (inside, inside.query_tree().parent) - for wayinside in inside.query_tree().children: - if wayinside.id == id: # KDE - parent = wayinside.query_tree().parent - return (wayinside, parent.query_tree().parent) - return (None, None) + for top_level in root.query_tree().children: + if top_level.id == id: + return (top_level, None) + for w in treewalk(top_level): + if w.id == id: + return (w, top_level) def get_geometry(self): try: my_geo = self.window.get_geometry() if self.parent is None: - return {'x' : my_geo.x + pa_geo.x, - 'y' : my_geo.y + pa_geo.y, + return {'x' : my_geo.x, + 'y' : my_geo.y, 'width' : my_geo.width, 'height' : my_geo.height } @@ -116,3 +123,10 @@ class Table(Table_Window): # This is the gdkhandle for the HUD window gdkwindow = gtk.gdk.window_foreign_new(window.window.xid) gdkwindow.set_transient_for(self.gdkhandle) + +def treewalk(parent): + for w in parent.query_tree().children: + for ww in treewalk(w): + yield ww + yield w + From f58af287792805c166e0ce4cf80cd3e2d7c91ec6 Mon Sep 17 00:00:00 2001 From: Eratosthenes Date: Mon, 13 Dec 2010 13:48:24 -0500 Subject: [PATCH 32/45] Make executables executable. --- pyfpdb/HUD_main.pyw | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 pyfpdb/HUD_main.pyw diff --git a/pyfpdb/HUD_main.pyw b/pyfpdb/HUD_main.pyw old mode 100644 new mode 100755 From 46dd6524ec55b9c816e8089c3d7801fd28ce5b56 Mon Sep 17 00:00:00 2001 From: Worros Date: Wed, 15 Dec 2010 12:44:12 +0800 Subject: [PATCH 33/45] Options: Add site_alias function Function returns the FPDB name for a given site. Should make command line options a bit simpler --- pyfpdb/Options.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pyfpdb/Options.py b/pyfpdb/Options.py index 9492fc87..2b2dc06e 100644 --- a/pyfpdb/Options.py +++ b/pyfpdb/Options.py @@ -77,6 +77,35 @@ def fpdb_options(): (options, argv) = parser.parse_args() return (options, argv) +def site_alias(alias): + """Function for converting various site aliases to the FPDB name""" + tmp = alias + aliases = { + "PokerStars" : "PokerStars", + "Full Tilt Poker": "Full Tilt Poker", + "PartyPoker" : "PartyPoker", + "Betfair" : "Betfair", + "OnGame" : "OnGame", + "Absolute" : "Absolute", + "UltimateBet" : "UltimateBet", + "Everleaf" : "Everleaf", + "Carbon" : "Carbon", + "iPoker" : "iPoker", + "Winamax" : "Winamax", + "Win2day" : "Win2day", + "Stars" : "PokerStars", + "FTP" : "Full Tilt Poker", + "Party" : "PartyPoker", + "AP" : "Absolute", + "UB" : "UltimateBet", + } + try: + tmp = aliases[alias] + except KeyError, e: + print _("Alias '%s' unknown" % alias) + + return False + if __name__== "__main__": (options, argv) = fpdb_options() print "errorsToConsole =", options.errorsToConsole From e5b514cb553f2cdd2d94b2ca6337da3ade5e9191 Mon Sep 17 00:00:00 2001 From: Worros Date: Wed, 15 Dec 2010 12:49:52 +0800 Subject: [PATCH 34/45] Options: Modify site_alias so it returns False on failure --- pyfpdb/Options.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyfpdb/Options.py b/pyfpdb/Options.py index 2b2dc06e..65ed888e 100644 --- a/pyfpdb/Options.py +++ b/pyfpdb/Options.py @@ -102,9 +102,10 @@ def site_alias(alias): try: tmp = aliases[alias] except KeyError, e: + tmp = False print _("Alias '%s' unknown" % alias) - return False + return tmp if __name__== "__main__": (options, argv) = fpdb_options() From acf5e4a677facd7c1e2ccd8bf13d3bafddc99bac Mon Sep 17 00:00:00 2001 From: Worros Date: Wed, 15 Dec 2010 12:50:23 +0800 Subject: [PATCH 35/45] THP: Make THP use new Options.site_aliases function --- pyfpdb/TestHandsPlayers.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pyfpdb/TestHandsPlayers.py b/pyfpdb/TestHandsPlayers.py index 2d978e67..177fe9e2 100755 --- a/pyfpdb/TestHandsPlayers.py +++ b/pyfpdb/TestHandsPlayers.py @@ -157,6 +157,10 @@ def walk_testfiles(dir, function, importer, errors, site): else: compare(nfile, importer, errors, site) +def usage(): + print "USAGE:" + sys.exit(0) + def main(argv=None): if argv is None: argv = sys.argv[1:] @@ -166,11 +170,12 @@ def main(argv=None): test_all_sites = True if options.usage == True: - #Print usage examples and exit - print "USAGE:" - sys.exit(0) + usage() if options.sitename: + options.sitename = Options.site_alias(options.sitename) + if options.sitename == False: + usage() print "Only regression testing '%s' files" % (options.sitename) test_all_sites = False From ec62b1911d00992f2bf6969466734dad2f3de290 Mon Sep 17 00:00:00 2001 From: Worros Date: Wed, 15 Dec 2010 14:27:50 +0800 Subject: [PATCH 36/45] SessionViewer: Fix total profit for single session Bound to be lots more bugs, but the total profit for a single session is now being calculated correctly. --- pyfpdb/GuiSessionViewer.py | 45 ++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/pyfpdb/GuiSessionViewer.py b/pyfpdb/GuiSessionViewer.py index 6f0fd269..e559914a 100644 --- a/pyfpdb/GuiSessionViewer.py +++ b/pyfpdb/GuiSessionViewer.py @@ -265,8 +265,9 @@ Thankyou #end def fillStatsFrame(self, vbox): def generateDatasets(self, playerids, sitenos, limits, seats): - THRESHOLD = 1800 # Minimum number of seconds between consecutive hands before being considered a new session - PADDING = 5 # Additional time in minutes to add to a session, session startup, shutdown etc (FiXME: user configurable) + print "DEBUG: Starting generateDatasets" + THRESHOLD = 1800 # Min # of secs between consecutive hands before being considered a new session + PADDING = 5 # Additional time in minutes to add to a session, session startup, shutdown etc # Get a list of all handids and their timestampts #FIXME: Query still need to filter on blind levels @@ -288,6 +289,9 @@ Thankyou times = map(lambda x:long(x[0]), hands) handids = map(lambda x:int(x[1]), hands) winnings = map(lambda x:float(x[4]), hands) + #print "DEBUG: times : %s" % times + #print "DEBUG: handids : %s" % handids + #print "DEBUG: winnings: %s" % winnings #print "DEBUG: len(times) %s" %(len(times)) diffs = diff(times) # This array is the difference in starttime between consecutive hands diffs2 = append(diffs,THRESHOLD + 1) # Append an additional session to the end of the diffs, so the next line @@ -318,6 +322,7 @@ Thankyou cum_sum = cum_sum/100 sid = 1 # Take all results and format them into a list for feeding into gui model. + #print "DEBUG: range(len(index[0]): %s" % range(len(index[0])) for i in range(len(index[0])): hds = index[0][i] - first_idx + 1 # Number of hands in session if hds > 0: @@ -328,19 +333,21 @@ Thankyou minutesplayed = 1 minutesplayed = minutesplayed + PADDING hph = hds*60/minutesplayed # Hands per hour - won = sum(winnings[first_idx:index[0][i]])/100.0 - hwm = max(cum_sum[first_idx:index[0][i]]) - lwm = min(cum_sum[first_idx:index[0][i]]) + end_idx = first_idx+hds + won = sum(winnings[first_idx:end_idx])/100.0 + #print "DEBUG: winnings[%s:%s]: %s" % (first_idx, end_idx, winnings[first_idx:end_idx]) + hwm = max(cum_sum[first_idx:end_idx]) + lwm = min(cum_sum[first_idx:end_idx]) open = (sum(winnings[:first_idx]))/100 - close = (sum(winnings[:index[0][i]]))/100 - #print "DEBUG: range: (%s, %s) - (min, max): (%s, %s) - (open,close): (%s, %s)" %(first_idx, index[0][i], lwm, hwm, open, close) + close = (sum(winnings[:end_idx]))/100 + print "DEBUG: range: (%s, %s) - (min, max): (%s, %s) - (open,close): (%s, %s)" %(first_idx, index[0][i], lwm, hwm, open, close) results.append([sid, hds, stime, etime, hph, won]) opens.append(open) closes.append(close) highs.append(hwm) lows.append(lwm) - #print "DEBUG: Hands in session %4s: %4s Start: %s End: %s HPH: %s Profit: %s" %(sid, hds, stime, etime, hph, won) + print "DEBUG: Hands in session %4s: %4s Start: %s End: %s HPH: %s Profit: %s" %(sid, hds, stime, etime, hph, won) total = total + (index[0][i] - first_idx) first_idx = index[0][i] + 1 sid = sid+1 @@ -374,17 +381,17 @@ Thankyou def generateGraph(self, opens, closes, highs, lows): self.clearGraphData() -# print "DEBUG:" -# print "highs = %s" % highs -# print "lows = %s" % lows -# print "opens = %s" % opens -# print "closes = %s" % closes -# print "len(highs): %s == len(lows): %s" %(len(highs), len(lows)) -# print "len(opens): %s == len(closes): %s" %(len(opens), len(closes)) -# -# for i in range(len(highs)): -# print "DEBUG: (%s, %s, %s, %s)" %(lows[i], opens[i], closes[i], highs[i]) -# print "DEBUG: diffs h/l: %s o/c: %s" %(lows[i] - highs[i], opens[i] - closes[i]) + print "DEBUG:" + print "highs = %s" % highs + print "lows = %s" % lows + print "opens = %s" % opens + print "closes = %s" % closes + print "len(highs): %s == len(lows): %s" %(len(highs), len(lows)) + print "len(opens): %s == len(closes): %s" %(len(opens), len(closes)) + + for i in range(len(highs)): + print "DEBUG: (%s, %s, %s, %s)" %(lows[i], opens[i], closes[i], highs[i]) + print "DEBUG: diffs h/l: %s o/c: %s" %(lows[i] - highs[i], opens[i] - closes[i]) self.ax = self.fig.add_subplot(111) From f4805746ae1bc754caf39002035d64a9f2c63009 Mon Sep 17 00:00:00 2001 From: Worros Date: Wed, 15 Dec 2010 15:05:41 +0800 Subject: [PATCH 37/45] Grapher: Add 0th entry into winnings so graph starts at 0 --- pyfpdb/GuiGraphViewer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyfpdb/GuiGraphViewer.py b/pyfpdb/GuiGraphViewer.py index 443d65aa..2880a7d7 100644 --- a/pyfpdb/GuiGraphViewer.py +++ b/pyfpdb/GuiGraphViewer.py @@ -224,9 +224,6 @@ class GuiGraphViewer (threading.Thread): self.graphBox.add(self.canvas) self.canvas.show() self.canvas.draw() - - #TODO: Do something useful like alert user - #print "No hands returned by graph query" else: self.ax.set_title(_("Profit graph for ring games"+names),fontsize=12) @@ -340,7 +337,10 @@ class GuiGraphViewer (threading.Thread): if len(winnings) == 0: return (None, None, None) - green = map(lambda x:float(x[1]), winnings) + #Insert a 0th entry into winnings so graph starts 'zerod' + winnings.insert(0, (0,0,0)) + + green = map(lambda x: float(x[1]), winnings) blue = map(lambda x: float(x[1]) if x[2] == True else 0.0, winnings) red = map(lambda x: float(x[1]) if x[2] == False else 0.0, winnings) greenline = cumsum(green) From c333bb768972fb48a9e5e7173adba2644de08998 Mon Sep 17 00:00:00 2001 From: Worros Date: Wed, 15 Dec 2010 16:51:59 +0800 Subject: [PATCH 38/45] SessionViewer: A few updates - Added a 0th hand to the start of the winnings array - Miscalculating number of hands again High/Low watermark values appear to be much more accurate, as do max/min values for the session. --- pyfpdb/GuiSessionViewer.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/pyfpdb/GuiSessionViewer.py b/pyfpdb/GuiSessionViewer.py index e559914a..466f8d9b 100644 --- a/pyfpdb/GuiSessionViewer.py +++ b/pyfpdb/GuiSessionViewer.py @@ -37,8 +37,6 @@ try: from matplotlib.finance import candlestick2 from numpy import diff, nonzero, sum, cumsum, max, min, append -# from matplotlib.dates import DateFormatter, WeekdayLocator, HourLocator, \ -# DayLocator, MONDAY, timezone except ImportError, inst: print _("""Failed to load numpy and/or matplotlib in Session Viewer""") @@ -285,6 +283,8 @@ Thankyou self.db.cursor.execute(q) hands = self.db.cursor.fetchall() + hands.insert(0, (hands[0][0], 0, 0, 0, 0)) + # Take that list and create an array of the time between hands times = map(lambda x:long(x[0]), hands) handids = map(lambda x:int(x[1]), hands) @@ -324,7 +324,7 @@ Thankyou # Take all results and format them into a list for feeding into gui model. #print "DEBUG: range(len(index[0]): %s" % range(len(index[0])) for i in range(len(index[0])): - hds = index[0][i] - first_idx + 1 # Number of hands in session + hds = index[0][i] - first_idx # Number of hands in session if hds > 0: stime = strftime("%d/%m/%Y %H:%M", localtime(times[first_idx])) # Formatted start time etime = strftime("%d/%m/%Y %H:%M", localtime(times[index[0][i]])) # Formatted end time @@ -333,21 +333,21 @@ Thankyou minutesplayed = 1 minutesplayed = minutesplayed + PADDING hph = hds*60/minutesplayed # Hands per hour - end_idx = first_idx+hds + end_idx = first_idx+hds+1 won = sum(winnings[first_idx:end_idx])/100.0 #print "DEBUG: winnings[%s:%s]: %s" % (first_idx, end_idx, winnings[first_idx:end_idx]) hwm = max(cum_sum[first_idx:end_idx]) lwm = min(cum_sum[first_idx:end_idx]) open = (sum(winnings[:first_idx]))/100 close = (sum(winnings[:end_idx]))/100 - print "DEBUG: range: (%s, %s) - (min, max): (%s, %s) - (open,close): (%s, %s)" %(first_idx, index[0][i], lwm, hwm, open, close) + #print "DEBUG: range: (%s, %s) - (min, max): (%s, %s) - (open,close): (%s, %s)" %(first_idx, end_idx, lwm, hwm, open, close) results.append([sid, hds, stime, etime, hph, won]) opens.append(open) closes.append(close) highs.append(hwm) lows.append(lwm) - print "DEBUG: Hands in session %4s: %4s Start: %s End: %s HPH: %s Profit: %s" %(sid, hds, stime, etime, hph, won) + #print "DEBUG: Hands in session %4s: %4s Start: %s End: %s HPH: %s Profit: %s" %(sid, hds, stime, etime, hph, won) total = total + (index[0][i] - first_idx) first_idx = index[0][i] + 1 sid = sid+1 @@ -381,17 +381,17 @@ Thankyou def generateGraph(self, opens, closes, highs, lows): self.clearGraphData() - print "DEBUG:" - print "highs = %s" % highs - print "lows = %s" % lows - print "opens = %s" % opens - print "closes = %s" % closes - print "len(highs): %s == len(lows): %s" %(len(highs), len(lows)) - print "len(opens): %s == len(closes): %s" %(len(opens), len(closes)) + #print "DEBUG:" + #print "\thighs = %s" % highs + #print "\tlows = %s" % lows + #print "\topens = %s" % opens + #print "\tcloses = %s" % closes + #print "\tlen(highs): %s == len(lows): %s" %(len(highs), len(lows)) + #print "\tlen(opens): %s == len(closes): %s" %(len(opens), len(closes)) - for i in range(len(highs)): - print "DEBUG: (%s, %s, %s, %s)" %(lows[i], opens[i], closes[i], highs[i]) - print "DEBUG: diffs h/l: %s o/c: %s" %(lows[i] - highs[i], opens[i] - closes[i]) + #for i in range(len(highs)): + # print "DEBUG: (%s, %s, %s, %s)" %(lows[i], opens[i], closes[i], highs[i]) + # print "DEBUG: diffs h/l: %s o/c: %s" %(lows[i] - highs[i], opens[i] - closes[i]) self.ax = self.fig.add_subplot(111) From 5e2d64744bef8c8caad07484538369290f2dc933 Mon Sep 17 00:00:00 2001 From: Eratosthenes Date: Wed, 15 Dec 2010 12:59:07 -0500 Subject: [PATCH 39/45] See if I can quash HUD freeze bug. No resize, autofold. --- pyfpdb/HUD_main.pyw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyfpdb/HUD_main.pyw b/pyfpdb/HUD_main.pyw index 34b701c9..79b2fbfe 100644 --- a/pyfpdb/HUD_main.pyw +++ b/pyfpdb/HUD_main.pyw @@ -126,7 +126,7 @@ class HUD_main(object): self.main_window.set_icon_stock(gtk.STOCK_HOME) if not options.hidden: self.main_window.show_all() - gobject.timeout_add(100, self.check_tables) +# gobject.timeout_add(100, self.check_tables) except: log.exception("Error initializing main_window") From e0987e67a62326271cd7636acd889f125b8eab1b Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 17 Dec 2010 11:24:07 +0800 Subject: [PATCH 40/45] Stars: Add new blind levels to lookup --- pyfpdb/PokerStarsToFpdb.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyfpdb/PokerStarsToFpdb.py b/pyfpdb/PokerStarsToFpdb.py index 0e934db3..a46b2298 100644 --- a/pyfpdb/PokerStarsToFpdb.py +++ b/pyfpdb/PokerStarsToFpdb.py @@ -57,9 +57,11 @@ class PokerStars(HandHistoryConverter): '20.00': ('5.00', '10.00'), '20': ('5.00', '10.00'), '30.00': ('10.00', '15.00'), '30': ('10.00', '15.00'), '60.00': ('15.00', '30.00'), '60': ('15.00', '30.00'), + '80.00': ('20.00', '40.00'), '80': ('20.00', '40.00'), '100.00': ('25.00', '50.00'), '100': ('25.00', '50.00'), '200.00': ('50.00', '100.00'), '200': ('50.00', '100.00'), '400.00': ('100.00', '200.00'), '400': ('100.00', '200.00'), + '800.00': ('200.00', '400.00'), '800': ('200.00', '400.00'), '1000.00': ('250.00', '500.00'),'1000': ('250.00', '500.00') } From fd28ad398721fb571a76fa90b458b94201c6d3c2 Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 17 Dec 2010 15:56:15 +0800 Subject: [PATCH 41/45] Replayer: Add option to run from cli --- pyfpdb/GuiReplayer.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/pyfpdb/GuiReplayer.py b/pyfpdb/GuiReplayer.py index 4b95faf2..8307dd90 100644 --- a/pyfpdb/GuiReplayer.py +++ b/pyfpdb/GuiReplayer.py @@ -272,3 +272,41 @@ class GuiReplayer: def temp(self): pass +def main(argv=None): + """main can also be called in the python interpreter, by supplying the command line as the argument.""" + if argv is None: + argv = sys.argv[1:] + + def destroy(*args): # call back for terminating the main eventloop + gtk.main_quit() + + import Options + + (options, argv) = Options.fpdb_options() + + if options.usage == True: + #Print usage examples and exit + sys.exit(0) + + config = Configuration.Config(file = "HUD_config.test.xml") + db = Database.Database(config) + sql = SQL.Sql(db_server = 'sqlite') + + settings = {} + settings.update(config.get_db_parameters()) + settings.update(config.get_import_parameters()) + settings.update(config.get_default_paths()) + + + main_window = gtk.Window() + main_window.connect('destroy', destroy) + + replayer = GuiReplayer(config, sql, main_window, debug=True) + + main_window.add(replayer.get_vbox()) + main_window.set_default_size(800,800) + main_window.show_all() + gtk.main() + +if __name__ == '__main__': + sys.exit(main()) From caab83e6b6b2ea2aaf44cb3d0b6c4677de92ac22 Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 17 Dec 2010 16:50:32 +0800 Subject: [PATCH 42/45] Replayer: Add command line parsing for filename and site --- pyfpdb/GuiReplayer.py | 69 +++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/pyfpdb/GuiReplayer.py b/pyfpdb/GuiReplayer.py index 8307dd90..1045eb27 100644 --- a/pyfpdb/GuiReplayer.py +++ b/pyfpdb/GuiReplayer.py @@ -33,28 +33,38 @@ import gobject class GuiReplayer: - def __init__(self, config, querylist, mainwin, debug=True): + def __init__(self, config, querylist, mainwin, options = None, debug=True): self.debug = debug self.conf = config self.main_window = mainwin self.sql = querylist + # These are temporary variables until it becomes possible + # to select() a Hand object from the database + self.filename="regression-test-files/cash/Stars/Flop/NLHE-FR-USD-0.01-0.02-201005.microgrind.txt" + self.site="PokerStars" + + if options.filename != None: + self.filename = options.filename + if options.sitename != None: + self.site = options.sitename + self.db = Database.Database(self.conf, sql=self.sql) filters_display = { "Heroes" : True, - "Sites" : True, - "Games" : True, - "Limits" : True, - "LimitSep" : True, - "LimitType" : True, - "Type" : True, - "Seats" : True, - "SeatSep" : True, + "Sites" : False, + "Games" : False, + "Limits" : False, + "LimitSep" : False, + "LimitType" : False, + "Type" : False, + "Seats" : False, + "SeatSep" : False, "Dates" : True, - "Groups" : True, - "GroupsAll" : True, + "Groups" : False, + "GroupsAll" : False, "Button1" : True, - "Button2" : True + "Button2" : False } @@ -239,27 +249,23 @@ class GuiReplayer: be replaced by a function to select a hand from the db in the not so distant future. This code has been shamelessly stolen from Carl """ - config = Configuration.Config(file = "HUD_config.test.xml") - db = Database.Database(config) - sql = SQL.Sql(db_server = 'sqlite') settings = {} - settings.update(config.get_db_parameters()) - settings.update(config.get_import_parameters()) - settings.update(config.get_default_paths()) - #db.recreate_tables() - importer = fpdb_import.Importer(False, settings, config, None) + settings.update(self.conf.get_db_parameters()) + settings.update(self.conf.get_import_parameters()) + settings.update(self.conf.get_default_paths()) + + importer = fpdb_import.Importer(False, settings, self.conf, None) importer.setDropIndexes("don't drop") importer.setFailOnError(True) importer.setThreads(-1) importer.setCallHud(False) importer.setFakeCacheHHC(True) + #Get a simple regression file with a few hands of Hold'em - filename="regression-test-files/cash/Stars/Flop/NLHE-FR-USD-0.01-0.02-201005.microgrind.txt" - site="PokerStars" + print "DEBUG: self.filename: '%s' self.site: '%s'" %(self.filename, self.site) - - importer.addBulkImportImportFileOrDir(filename, site=site) + importer.addBulkImportImportFileOrDir(self.filename, site=self.site) (stored, dups, partial, errs, ttime) = importer.runImport() @@ -288,24 +294,23 @@ def main(argv=None): #Print usage examples and exit sys.exit(0) + if options.sitename: + options.sitename = Options.site_alias(options.sitename) + if options.sitename == False: + usage() + config = Configuration.Config(file = "HUD_config.test.xml") db = Database.Database(config) sql = SQL.Sql(db_server = 'sqlite') - settings = {} - settings.update(config.get_db_parameters()) - settings.update(config.get_import_parameters()) - settings.update(config.get_default_paths()) - - main_window = gtk.Window() main_window.connect('destroy', destroy) - replayer = GuiReplayer(config, sql, main_window, debug=True) + replayer = GuiReplayer(config, sql, main_window, options=options, debug=True) main_window.add(replayer.get_vbox()) main_window.set_default_size(800,800) - main_window.show_all() + main_window.show() gtk.main() if __name__ == '__main__': From 06978fb77bd82ec928606fbd522bc5a4d4a26b15 Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 17 Dec 2010 18:59:01 +0800 Subject: [PATCH 43/45] SQL: Add get_gameinfo_from_hid --- pyfpdb/SQL.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pyfpdb/SQL.py b/pyfpdb/SQL.py index 83e19f13..378c7288 100644 --- a/pyfpdb/SQL.py +++ b/pyfpdb/SQL.py @@ -1451,6 +1451,34 @@ class Sql: and (p.siteId = %s or %s = -1) """ + self.query['get_gameinfo_from_hid'] = """ + 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), + g.currency + FROM + Hands as h, + Sites as s, + Gametypes as g, + HandsPlayers as hp, + Players as p + WHERE + h.id = %s + and g.id = h.gametypeid + and hp.handid = h.id + and p.id = hp.playerid + and s.id = p.siteid + limit 1 + """ + self.query['get_stats_from_hand'] = """ SELECT hc.playerId AS player_id, hp.seatNo AS seat, From 20c5ecee8218ba5c43a06698399699e1c0d3502a Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 17 Dec 2010 19:02:01 +0800 Subject: [PATCH 44/45] Replayer: Add start of relacement importhand function --- pyfpdb/GuiReplayer.py | 57 +++++++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/pyfpdb/GuiReplayer.py b/pyfpdb/GuiReplayer.py index 1045eb27..bc29090c 100644 --- a/pyfpdb/GuiReplayer.py +++ b/pyfpdb/GuiReplayer.py @@ -249,31 +249,52 @@ class GuiReplayer: be replaced by a function to select a hand from the db in the not so distant future. This code has been shamelessly stolen from Carl """ - settings = {} - settings.update(self.conf.get_db_parameters()) - settings.update(self.conf.get_import_parameters()) - settings.update(self.conf.get_default_paths()) + if True: + settings = {} + settings.update(self.conf.get_db_parameters()) + settings.update(self.conf.get_import_parameters()) + settings.update(self.conf.get_default_paths()) - importer = fpdb_import.Importer(False, settings, self.conf, None) - importer.setDropIndexes("don't drop") - importer.setFailOnError(True) - importer.setThreads(-1) - importer.setCallHud(False) - importer.setFakeCacheHHC(True) + importer = fpdb_import.Importer(False, settings, self.conf, None) + importer.setDropIndexes("don't drop") + importer.setFailOnError(True) + importer.setThreads(-1) + importer.setCallHud(False) + importer.setFakeCacheHHC(True) + print "DEBUG: self.filename: '%s' self.site: '%s'" %(self.filename, self.site) + importer.addBulkImportImportFileOrDir(self.filename, site=self.site) + (stored, dups, partial, errs, ttime) = importer.runImport() - #Get a simple regression file with a few hands of Hold'em - print "DEBUG: self.filename: '%s' self.site: '%s'" %(self.filename, self.site) + hhc = importer.getCachedHHC() + handlist = hhc.getProcessedHands() - importer.addBulkImportImportFileOrDir(self.filename, site=self.site) - (stored, dups, partial, errs, ttime) = importer.runImport() + return handlist[0] + else: + # Fetch hand info + # We need at least sitename, gametype, handid + # for the Hand.__init__ + ####### Shift this section in Database.py for all to use ###### + handid = 40 + q = self.sql.query['get_gameinfo_from_hid'] + q = q.replace('%s', self.sql.query['placeholder']) - hhc = importer.getCachedHHC() - handlist = hhc.getProcessedHands() - - return handlist[0] + c = self.db.get_cursor() + c.execute(q, (handid,)) + 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]} + #FIXME: smallbet and bigbet are res[8] and res[9] respectively + ###### End section ######## + print "DEBUG: gametype: %s" % gametype + if gametype['base'] == 'hold': + h = HoldemOmahaHand(config = self.conf, hhc = None, sitename=res[0], gametype = gametype, handText=None, builtFrom = "DB", handid=handid) + h.select(self.db, handid) + elif gametype['base'] == 'stud': + print "DEBUG: Create stud hand here" + elif gametype['base'] == 'draw': + print "DEBUG: Create draw hand here" def temp(self): pass From 70c34801059cb8f4456d29b323f0d04bc6b1c127 Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 17 Dec 2010 19:03:50 +0800 Subject: [PATCH 45/45] Hand: Start select() method Can currently add the players in the hand. Still a long way to go, but a good start --- pyfpdb/Hand.py | 218 +++++++++++++++++++++---------------------------- 1 file changed, 93 insertions(+), 125 deletions(-) diff --git a/pyfpdb/Hand.py b/pyfpdb/Hand.py index c2bea220..10f15f57 100644 --- a/pyfpdb/Hand.py +++ b/pyfpdb/Hand.py @@ -282,75 +282,10 @@ db: a connected Database object""" def updateSessionsCache(self, db): db.storeSessionsCache(self.dbid_pids, self.startTime, self.gametype, self.stats.getHandsPlayers()) - def select(self, handId): + def select(self, db, handId): """ Function to create Hand object from database """ - 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]} - c = Configuration.Config() - h = HoldemOmahaHand(config = c, 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.startTime 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 + c = db.get_cursor() + q = """SELECT hp.seatno, round(hp.winnings / 100.0,2) as winnings, p.name, @@ -358,69 +293,101 @@ db: a connected Database object""" hp.card1,hp.card2, hp.position FROM - handsplayers as hp, - players as p + HandsPlayers as hp, + Players as p WHERE - hp.handid = %(handid)s + hp.handId = %s and p.id = hp.playerid - """, {'handid':handid}) + """ + q = q.replace('%s', db.sql.query['placeholder']) + + # PlayerStacks + c.execute(q, (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 + print "DEBUG: seat: '%s'\tname: '%s'\tchips: '%s'" % (seat, name, chips) + self.addPlayer(seat,name,str(chips)) + #if card1 and card2: + # self.addHoleCards(map(Card.valueSuitFromCard, (card1,card2)), name, dealt=True) + #if winnings > 0: + # self.addCollectPot(name, winnings) + #if position == 'B': + # self.buttonpos = seat + + + # 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.startTime 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 + + + #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] # 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 + #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 + #h.totalPot() + #h.rake = h.totalpot - h.totalcollected - return h def addPlayer(self, seat, name, chips): """\ @@ -853,10 +820,11 @@ class HoldemOmahaHand(Hand): hhc.readOther(self) #print "\nHand:\n"+str(self) elif builtFrom == "DB": - if handid is not None: - self.select(handid) # Will need a handId - else: - log.warning(_("HoldemOmahaHand.__init__:Can't assemble hand from db without a handid")) + #if handid is not None: + # self.select(handid) # Will need a handId + #else: + # log.warning(_("HoldemOmahaHand.__init__:Can't assemble hand from db without a handid")) + print "DEBUG: HoldemOmaha hand initialised for select()" else: log.warning(_("HoldemOmahaHand.__init__:Neither HHC nor DB+handid provided")) pass