From 695b3a53cf383b87ac4ea6ad0550810762251184 Mon Sep 17 00:00:00 2001 From: sqlcoder Date: Mon, 15 Dec 2008 23:15:54 +0000 Subject: [PATCH 01/23] update mysql query for position stats (postgres one still to do ...) --- pyfpdb/FpdbSQLQueries.py | 171 ++++++++++++++++++++++++++++----------- 1 file changed, 124 insertions(+), 47 deletions(-) diff --git a/pyfpdb/FpdbSQLQueries.py b/pyfpdb/FpdbSQLQueries.py index d233f2d2..07fa15bb 100644 --- a/pyfpdb/FpdbSQLQueries.py +++ b/pyfpdb/FpdbSQLQueries.py @@ -867,53 +867,130 @@ class FpdbSQLQueries: if(self.dbname == 'MySQL InnoDB'): self.query['playerStatsByPosition'] = """ - select /* stats from hudcache */ - hc.position - ,sum(HDs) as n - ,format(round(100.0*sum(street0VPI)/sum(HDs)),1) AS vpip - ,format(round(100.0*sum(street0Aggr)/sum(HDs)),1) AS pfr - ,format(round(100.0*sum(street1Seen)/sum(HDs)),1) AS saw_f - ,format(round(100.0*sum(sawShowdown)/sum(HDs)),1) AS sawsd - ,case when sum(street1Seen) = 0 then 'oo' - else format(round(100.0*sum(sawShowdown)/sum(street1Seen)),1) - end AS wtsdwsf - ,case when sum(sawShowdown) = 0 then 'oo' - else format(round(100.0*sum(wonAtSD)/sum(sawShowdown)),1) - end AS wmsd - ,case when sum(street1Seen) = 0 then 'oo' - else format(round(100.0*sum(street1Aggr)/sum(street1Seen)),1) - end AS FlAFq - ,case when sum(street2Seen) = 0 then 'oo' - else format(round(100.0*sum(street2Aggr)/sum(street2Seen)),1) - end AS TuAFq - ,case when sum(street3Seen) = 0 then 'oo' - else format(round(100.0*sum(street3Aggr)/sum(street3Seen)),1) - end AS RvAFq - ,case when sum(street1Seen)+sum(street2Seen)+sum(street3Seen) = 0 then 'oo' - else format(round(100.0*(sum(street1Aggr)+sum(street2Aggr)+sum(street3Aggr)) - /(sum(street1Seen)+sum(street2Seen)+sum(street3Seen))),1) - end AS PoFAFq - ,format(sum(totalProfit)/100.0,2) AS Net - ,case when sum(HDs) = 0 then 'oo' - else format((sum(totalProfit)/(gt.bigBlind+0.0)) / (sum(HDs)/100.0),2) - end AS BBper100 - from Gametypes gt - inner join Sites s on (s.Id = gt.siteId) - inner join HudCache hc on (hc.gameTypeId = gt.Id) - inner join Players p on (p.id = hc.playerId) - where hc.playerId in - and gt.type = 'ring' - and gt.id = /* must specify gametypeid */ - /* and stats.n > 100 optional stat-based queries */ - group by hc.position, gt.bigBlind - order by case when hc.position = 'B' then -2 - when hc.position = 'S' then -1 - when hc.position = 'D' then 0 - when hc.position = 'C' then 1 - when hc.position = 'M' then 2 - when hc.position = 'E' then 5 - else 9 - end + SELECT + concat(upper(stats.limitType), ' ' + ,concat(upper(substring(stats.category,1,1)),substring(stats.category,2) ), ' ' + ,stats.name, ' $' + ,cast(trim(leading ' ' from + case when stats.bigBlind < 100 then format(stats.bigBlind/100.0,2) + else format(stats.bigBlind/100.0,0) + end ) as char) + ) AS Game + ,case when stats.PlPosition = -2 then 'BB' + when stats.PlPosition = -1 then 'SB' + when stats.PlPosition = 0 then 'Btn' + when stats.PlPosition = 1 then 'CO' + when stats.PlPosition = 2 then 'MP' + when stats.PlPosition = 5 then 'EP' + else '??' + end AS PlPosition + ,stats.n + ,stats.vpip + ,stats.pfr + ,stats.saw_f + ,stats.sawsd + ,stats.wtsdwsf + ,stats.wmsd + ,stats.FlAFq + ,stats.TuAFq + ,stats.RvAFq + ,stats.PoFAFq + /* if you have handsactions data the next 3 fields should give same answer as + following 3 commented out fields */ + ,stats.Net + ,stats.BBper100 + ,stats.Profitperhand + /*,format(hprof2.sum_profit/100.0,2) AS Net + ,format((hprof2.sum_profit/(stats.bigBlind+0.0)) / (stats.n/100.0),2) + AS BBlPer100 + ,hprof2.profitperhand AS Profitperhand + */ + ,format(hprof2.variance,2) AS Variance + FROM + (select /* stats from hudcache */ + gt.base + ,gt.category + ,upper(gt.limitType) as limitType + ,s.name + ,gt.bigBlind + ,hc.gametypeId + ,case when hc.position = 'B' then -2 + when hc.position = 'S' then -1 + when hc.position = 'D' then 0 + when hc.position = 'C' then 1 + when hc.position = 'M' then 2 + when hc.position = 'E' then 5 + else 9 + end as PlPosition + ,sum(HDs) AS n + ,format(100.0*sum(street0VPI)/sum(HDs),1) AS vpip + ,format(100.0*sum(street0Aggr)/sum(HDs),1) AS pfr + ,format(100.0*sum(street1Seen)/sum(HDs),1) AS saw_f + ,format(100.0*sum(sawShowdown)/sum(HDs),1) AS sawsd + ,case when sum(street1Seen) = 0 then 'oo' + else format(100.0*sum(sawShowdown)/sum(street1Seen),1) + end AS wtsdwsf + ,case when sum(sawShowdown) = 0 then 'oo' + else format(100.0*sum(wonAtSD)/sum(sawShowdown),1) + end AS wmsd + ,case when sum(street1Seen) = 0 then 'oo' + else format(100.0*sum(street1Aggr)/sum(street1Seen),1) + end AS FlAFq + ,case when sum(street2Seen) = 0 then 'oo' + else format(100.0*sum(street2Aggr)/sum(street2Seen),1) + end AS TuAFq + ,case when sum(street3Seen) = 0 then 'oo' + else format(100.0*sum(street3Aggr)/sum(street3Seen),1) + end AS RvAFq + ,case when sum(street1Seen)+sum(street2Seen)+sum(street3Seen) = 0 then 'oo' + else format(100.0*(sum(street1Aggr)+sum(street2Aggr)+sum(street3Aggr)) + /(sum(street1Seen)+sum(street2Seen)+sum(street3Seen)),1) + end AS PoFAFq + ,format(sum(totalProfit)/100.0,2) AS Net + ,format((sum(totalProfit)/(gt.bigBlind+0.0)) / (sum(HDs)/100.0),2) + AS BBper100 + ,format( (sum(totalProfit)/100.0) / sum(HDs), 4) AS Profitperhand + from Gametypes gt + inner join Sites s on s.Id = gt.siteId + inner join HudCache hc on hc.gameTypeId = gt.Id + where hc.playerId in + # use here ? + group by gt.base + ,gt.category + ,upper(gt.limitType) + ,s.name + ,gt.bigBlind + ,hc.gametypeId + ,PlPosition + ) stats + inner join + ( select # profit from handsplayers/handsactions + hprof.gameTypeId, + case when hprof.position = 'B' then -2 + when hprof.position = 'S' then -1 + when hprof.position in ('3','4') then 2 + when hprof.position in ('6','7') then 5 + else hprof.position + end as PlPosition, + sum(hprof.profit) as sum_profit, + avg(hprof.profit/100.0) as profitperhand, + variance(hprof.profit/100.0) as variance + from + (select hp.handId, h.gameTypeId, hp.position, hp.winnings, SUM(ha.amount) + costs, hp.winnings - SUM(ha.amount) profit + from HandsPlayers hp + inner join Hands h ON h.id = hp.handId + left join HandsActions ha ON ha.handPlayerId = hp.id + where hp.playerId in + # use here ? + and hp.tourneysPlayersId IS NULL + group by hp.handId, h.gameTypeId, hp.position, hp.winnings + ) hprof + group by hprof.gameTypeId, PlPosition + ) hprof2 + on ( hprof2.gameTypeId = stats.gameTypeId + and hprof2.PlPosition = stats.PlPosition) + order by stats.category, stats.limittype, stats.bigBlind, cast(stats.PlPosition as signed) """ elif(self.dbname == 'PostgreSQL'): self.query['playerStatsByPosition'] = """ From 9c5d0f45981c899827a6902d585636ba3af879a8 Mon Sep 17 00:00:00 2001 From: Matt Turnbull Date: Mon, 15 Dec 2008 23:56:19 +0000 Subject: [PATCH 02/23] Writes hands to stderr, miscellanous crap to stdout; usuable as cmdline filter: ./Everleaf 'hhfile' 1>/dev/null 2>outfile Holecards are sets -- should work on Omaha hi hands also. Successfully imported Speed_Kuala_full.txt to fpdb. Added gettext. cards strings are handled a little better (one fewer regex) Testfile can be supplied as first cmd line arg. --- pyfpdb/EverleafToFpdb.py | 38 +++++++----- pyfpdb/Hand.py | 110 +++++++++++++++++---------------- pyfpdb/HandHistoryConverter.py | 9 ++- 3 files changed, 89 insertions(+), 68 deletions(-) diff --git a/pyfpdb/EverleafToFpdb.py b/pyfpdb/EverleafToFpdb.py index 188e4ade..a34f2a63 100755 --- a/pyfpdb/EverleafToFpdb.py +++ b/pyfpdb/EverleafToFpdb.py @@ -76,11 +76,11 @@ class Everleaf(HandHistoryConverter): self.rexx.setPostSbRegex('.*\n(?P.*): posts small blind \[\$? (?P[.0-9]+)') self.rexx.setPostBbRegex('.*\n(?P.*): posts big blind \[\$? (?P[.0-9]+)') self.rexx.setPostBothRegex('.*\n(?P.*): posts small \& big blinds \[\$? (?P[.0-9]+)') - # mct : what about posting small & big blinds simultaneously? - self.rexx.setHeroCardsRegex('.*\nDealt\sto\s(?P.*)\s\[ (?P\S\S), (?P\S\S) \]') + self.rexx.setHeroCardsRegex('.*\nDealt\sto\s(?P.*)\s\[ (?P.*) \]') self.rexx.setActionStepRegex('.*\n(?P.*)(?P: bets| checks| raises| calls| folds)(\s\[\$ (?P[.\d]+) USD\])?') self.rexx.setShowdownActionRegex('.*\n(?P.*) shows \[ (?P.*) \]') - self.rexx.setCollectPotRegex('.*\n(?P.*) wins \$ (?P[.\d]+) USD(.*\[ (?P.*) \])?') + self.rexx.setCollectPotRegex('.*\n(?P.*) wins \$ (?P[.\d]+) USD(.*\[ (?P.*) \])?') + self.rexx.sits_out_re = re.compile('(?P.*) sits out') self.rexx.compileRegexes() def readSupportedGames(self): @@ -163,7 +163,11 @@ class Everleaf(HandHistoryConverter): hand.involved = False else: hand.hero = m.group('PNAME') - hand.addHoleCards([m.group('HOLE1'), m.group('HOLE2')], m.group('PNAME')) + # "2c, qh" -> set(["2c","qc"]) + # Also works with Omaha hands. + cards = m.group('CARDS') + cards = set(cards.split(', ')) + hand.addHoleCards(cards, m.group('PNAME')) def readAction(self, hand, street): m = self.rexx.action_re.finditer(hand.streets.group(street)) @@ -185,25 +189,31 @@ class Everleaf(HandHistoryConverter): def readShowdownActions(self, hand): - for shows in self.rexx.showdown_action_re.finditer(hand.string): - print shows.groups() - re_card = re.compile('(?P[0-9tjqka][schd])') # copied from earlier - cards = [card.group('CARD') for card in re_card.finditer(shows.group('CARDS'))] - print cards + for shows in self.rexx.showdown_action_re.finditer(hand.string): + cards = shows.group('CARDS') + cards = set(cards.split(', ')) + #re_card = re.compile('(?P[0-9tjqka][schd])') # copied from earlier + #cards = set([card.group('CARD') for card in re_card.finditer(shows.group('CARDS'))]) hand.addShownCards(cards, shows.group('PNAME')) def readCollectPot(self,hand): for m in self.rexx.collect_pot_re.finditer(hand.string): - if m.group('HAND') is not None: - re_card = re.compile('(?P[0-9tjqka][schd])') # copied from earlier - cards = set([hand.card(card.group('CARD')) for card in re_card.finditer(m.group('HAND'))]) + if m.group('CARDS') is not None: + cards = m.group('CARDS') + cards = set(cards.split(', ')) + #re_card = re.compile('(?P[0-9tjqka][schd])') # copied from earlier + #cards = set([hand.card(card.group('CARD')) for card in re_card.finditer(m.group('HAND'))]) hand.addShownCards(cards=None, player=m.group('PNAME'), holeandboard=cards) hand.addCollectPot(player=m.group('PNAME'),pot=m.group('POT')) if __name__ == "__main__": c = Configuration.Config() - e = Everleaf(c, "regression-test-files/everleaf/Speed_Kuala_full.txt") + if sys.argv[0] == '': + testfile = "regression-test-files/everleaf/Speed_Kuala_full.txt" + else: + testfile = sys.argv[1] + print "Converting: ", testfile + e = Everleaf(c, testfile) e.processFile() print str(e) - diff --git a/pyfpdb/Hand.py b/pyfpdb/Hand.py index d9af1a42..d746f3ab 100644 --- a/pyfpdb/Hand.py +++ b/pyfpdb/Hand.py @@ -106,7 +106,7 @@ chips (string) the chips the player has at the start of the hand (can be None) If a player has None chips he won't be added.""" if chips is not None: self.players.append([seat, name, chips]) - self.holecards[name] = [] + self.holecards[name] = set() for street in self.streetList: self.bets[street][name] = [] @@ -125,29 +125,27 @@ If a player has None chips he won't be added.""" def addHoleCards(self, cards, player): """\ Assigns observed holecards to a player. -cards list of card bigrams e.g. ['2h','jc'] +cards set of card bigrams e.g. set(['2h','Jc']) player (string) name of player -hand -Note, will automatically uppercase the rank letter. """ try: self.checkPlayerExists(player) - self.holecards[player] = set([self.card(c) for c in cards]) + cards = set([self.card(c) for c in cards]) + self.holecards[player].update(cards) except FpdbParseError, e: print "Tried to add holecards for unknown player: %s" % (player,) def addShownCards(self, cards, player, holeandboard=None): """\ For when a player shows cards for any reason (for showdown or out of choice). +Card ranks will be uppercased """ if cards is not None: self.shown.add(player) self.addHoleCards(cards,player) elif holeandboard is not None: + holeandboard = set([self.card(c) for c in holeandboard]) board = set([c for s in self.board.values() for c in s]) - #print board - #print holeandboard - #print holeandboard.difference(board) self.addHoleCards(holeandboard.difference(board),player) @@ -232,14 +230,11 @@ Add a raise on [street] by [player] to [amountTo] if player not in self.collected: self.collected[player] = pot else: - # possibly lines like "p collected $ from pot" appear during the showdown - # but they are usually unique in the summary, so it's best to try to get them from there. - print "%s collected pot more than once; avoidable by reading winnings only from summary lines?" + print "[WARNING] %s collected pot more than once; avoidable by reading winnings only from summary lines?" def totalPot(self): - """If all bets and blinds have been added, totals up the total pot size -Known bug: doesn't take into account side pots""" + """If all bets and blinds have been added, totals up the total pot size""" if self.totalpot is None: self.totalpot = 0 @@ -288,10 +283,6 @@ Known bug: doesn't take into account side pots""" for amount in self.collected.values(): self.totalcollected += Decimal(amount) - # TODO: Some sites (Everleaf) don't record uncalled bets. Figure out if a bet is uncalled and subtract it from self.totalcollected. - # remember that portions of bets may be uncalled, so: - # bet followed by no call is an uncalled bet - # bet x followed by call y where y < x has x-y uncalled (and second player all in) @@ -322,81 +313,89 @@ Map the tuple self.gametype onto the pokerstars string describing it return string - def printHand(self): + def writeHand(self, fh=sys.__stdout__): # PokerStars format. - print "\n### Pseudo stars format ###" - print "%s Game #%s: %s ($%s/$%s) - %s" %(self.sitename, self.handid, self.getGameTypeAsString(), self.sb, self.bb, self.starttime) - print "Table '%s' %d-max Seat #%s is the button" %(self.tablename, self.maxseats, self.buttonpos) - for player in self.players: - print "Seat %s: %s ($%s)" %(player[0], player[1], player[2]) + #print "\n### Pseudo stars format ###" + #print >>fh, _("%s Game #%s: %s ($%s/$%s) - %s" %(self.sitename, self.handid, self.getGameTypeAsString(), self.sb, self.bb, self.starttime)) + print >>fh, _("%s Game #%s: %s ($%s/$%s) - %s" %("PokerStars", self.handid, self.getGameTypeAsString(), self.sb, self.bb, self.starttime)) + print >>fh, _("Table '%s' %d-max Seat #%s is the button" %(self.tablename, self.maxseats, self.buttonpos)) + + players_who_act_preflop = set([x[0] for x in self.actions['PREFLOP']]) + print players_who_act_preflop + print [x[1] for x in self.players] + print [x for x in self.players if x[1] in players_who_act_preflop] + for player in [x for x in self.players if x[1] in players_who_act_preflop]: + #Only print stacks of players who do something preflop + print >>fh, _("Seat %s: %s ($%s)" %(player[0], player[1], player[2])) if(self.posted[0] is None): - print "No small blind posted" + #print >>fh, _("No small blind posted") # PS doesn't say this + pass else: - print "%s: posts small blind $%s" %(self.posted[0], self.sb) + print >>fh, _("%s: posts small blind $%s" %(self.posted[0], self.sb)) #May be more than 1 bb posting for a in self.posted[1:]: - print "%s: posts big blind $%s" %(self.posted[1], self.bb) + print >>fh, _("%s: posts big blind $%s" %(self.posted[1], self.bb)) - # What about big & small blinds? + # TODO: What about big & small blinds? - print "*** HOLE CARDS ***" + print >>fh, _("*** HOLE CARDS ***") if self.involved: - print "Dealt to %s [%s]" %(self.hero , " ".join(self.holecards[self.hero])) + print >>fh, _("Dealt to %s [%s]" %(self.hero , " ".join(self.holecards[self.hero]))) if 'PREFLOP' in self.actions: for act in self.actions['PREFLOP']: - self.printActionLine(act) + self.printActionLine(act, fh) if 'FLOP' in self.actions: - print "*** FLOP *** [%s]" %( " ".join(self.board['Flop'])) + print >>fh, _("*** FLOP *** [%s]" %( " ".join(self.board['Flop']))) for act in self.actions['FLOP']: - self.printActionLine(act) + self.printActionLine(act, fh) if 'TURN' in self.actions: - print "*** TURN *** [%s] [%s]" %( " ".join(self.board['Flop']), " ".join(self.board['Turn'])) + print >>fh, _("*** TURN *** [%s] [%s]" %( " ".join(self.board['Flop']), " ".join(self.board['Turn']))) for act in self.actions['TURN']: - self.printActionLine(act) + self.printActionLine(act, fh) if 'RIVER' in self.actions: - print "*** RIVER *** [%s] [%s]" %(" ".join(self.board['Flop']+self.board['Turn']), " ".join(self.board['River']) ) + print >>fh, _("*** RIVER *** [%s] [%s]" %(" ".join(self.board['Flop']+self.board['Turn']), " ".join(self.board['River']) )) for act in self.actions['RIVER']: - self.printActionLine(act) + self.printActionLine(act, fh) #Some sites don't have a showdown section so we have to figure out if there should be one # The logic for a showdown is: at the end of river action there are at least two players in the hand # we probably don't need a showdown section in pseudo stars format for our filtering purposes if 'SHOWDOWN' in self.actions: - print "*** SHOW DOWN ***" - print "what do they show" + print >>fh, _("*** SHOW DOWN ***") + print >>fh, "DEBUG: what do they show" - print "*** SUMMARY ***" - print "Total pot $%s | Rake $%.2f" % (self.totalcollected, self.rake) # TODO: side pots + print >>fh, _("*** SUMMARY ***") + print >>fh, _("Total pot $%s | Rake $%.2f" % (self.totalcollected, self.rake)) # TODO: side pots board = [] for s in self.board.values(): board += s if board: # sometimes hand ends preflop without a board - print "Board [%s]" % (" ".join(board)) + print >>fh, _("Board [%s]" % (" ".join(board))) for player in self.players: seatnum = player[0] name = player[1] if name in self.collected and self.holecards[name]: - print "Seat %d: %s showed [%s] and won ($%s)" % (seatnum, name, " ".join(self.holecards[name]), self.collected[name]) + print >>fh, _("Seat %d: %s showed [%s] and won ($%s)" % (seatnum, name, " ".join(self.holecards[name]), self.collected[name])) elif name in self.collected: - print "Seat %d: %s collected ($%s)" % (seatnum, name, self.collected[name]) + print >>fh, _("Seat %d: %s collected ($%s)" % (seatnum, name, self.collected[name])) elif player[1] in self.shown: - print "Seat %d: %s showed [%s]" % (seatnum, name, " ".join(self.holecards[name])) + print >>fh, _("Seat %d: %s showed [%s]" % (seatnum, name, " ".join(self.holecards[name]))) elif player[1] in self.folded: - print "Seat %d: %s folded" % (seatnum, name) + print >>fh, _("Seat %d: %s folded" % (seatnum, name)) else: - print "Seat %d: %s mucked" % (seatnum, name) + print >>fh, _("Seat %d: %s mucked" % (seatnum, name)) - print + print >>fh, "\n\n" # TODO: # logic for side pots # logic for which players get to showdown @@ -411,17 +410,22 @@ Map the tuple self.gametype onto the pokerstars string describing it #print "Seat %d: %s showed %s" % (player[0], player[1], hole) #else: #print "Seat %d: %s mucked or folded" % (player[0], player[1]) + + def printHand(self): + self.writeHand(sys.stdout) - def printActionLine(self, act): - if act[1] == 'folds' or act[1] == 'checks': - print "%s: %s " %(act[0], act[1]) + def printActionLine(self, act, fh): + if act[1] == 'folds': + print >>fh, _("%s: folds" %(act[0])) + elif act[1] == 'checks': + print >>fh, _("%s: checks" %(act[0])) if act[1] == 'calls': - print "%s: %s $%s" %(act[0], act[1], act[2]) + print >>fh, _("%s: calls $%s" %(act[0], act[2])) if act[1] == 'bets': - print "%s: %s $%s" %(act[0], act[1], act[2]) + print >>fh, _("%s: bets $%s" %(act[0], act[2])) if act[1] == 'raises': - print "%s: %s $%s to $%s" %(act[0], act[1], act[2], act[3]) + print >>fh, _("%s: raises $%s to $%s" %(act[0], act[2], act[3])) # going to use pokereval to figure out hands at some point. # these functions are copied from pokergame.py diff --git a/pyfpdb/HandHistoryConverter.py b/pyfpdb/HandHistoryConverter.py index 68d00e96..256de673 100644 --- a/pyfpdb/HandHistoryConverter.py +++ b/pyfpdb/HandHistoryConverter.py @@ -30,6 +30,8 @@ import operator from xml.dom.minidom import Node from pokereval import PokerEval from time import time +import gettext + #from pokerengine.pokercards import * # provides letter2name{}, letter2names{}, visible_card(), not_visible_card(), is_visible(), card_value(), class PokerCards # but it's probably not installed so here are the ones we may want: @@ -65,6 +67,11 @@ letter2names = { '2': 'Deuces' } +import gettext +gettext.install('myapplication') + + + class HandHistoryConverter: eval = PokerEval() def __init__(self, config, file, sitename): @@ -124,7 +131,7 @@ class HandHistoryConverter: hand.totalPot() self.getRake(hand) - hand.printHand() + hand.writeHand(sys.stderr) #if(hand.involved == True): #self.writeHand("output file", hand) #hand.printHand() From 988a7e3eb5af3532f40c5727004730cc2b0c8356 Mon Sep 17 00:00:00 2001 From: Matt Turnbull Date: Tue, 16 Dec 2008 04:29:11 +0000 Subject: [PATCH 03/23] Added 'and is all-in' logic. Altered to read actions in correct street order. hand.streetList must be set correctly for different types of games. --- pyfpdb/EverleafToFpdb.py | 3 +-- pyfpdb/Hand.py | 40 ++++++++++++++++++++++++---------- pyfpdb/HandHistoryConverter.py | 6 +++-- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/pyfpdb/EverleafToFpdb.py b/pyfpdb/EverleafToFpdb.py index a34f2a63..9942105d 100755 --- a/pyfpdb/EverleafToFpdb.py +++ b/pyfpdb/EverleafToFpdb.py @@ -209,11 +209,10 @@ class Everleaf(HandHistoryConverter): if __name__ == "__main__": c = Configuration.Config() - if sys.argv[0] == '': + if len(sys.argv) == 1: testfile = "regression-test-files/everleaf/Speed_Kuala_full.txt" else: testfile = sys.argv[1] - print "Converting: ", testfile e = Everleaf(c, testfile) e.processFile() print str(e) diff --git a/pyfpdb/Hand.py b/pyfpdb/Hand.py index d746f3ab..a91038f6 100644 --- a/pyfpdb/Hand.py +++ b/pyfpdb/Hand.py @@ -28,6 +28,7 @@ import codecs from decimal import Decimal import operator from time import time +from copy import deepcopy class Hand: # def __init__(self, sitename, gametype, sb, bb, string): @@ -38,7 +39,7 @@ class Hand: self.gametype = gametype self.string = string - self.streetList = ['BLINDS','PREFLOP','FLOP','TURN','RIVER'] # a list of the observed street names in order + self.streetList = ['PREFLOP','FLOP','TURN','RIVER'] # a list of the observed street names in order self.handid = 0 self.sb = gametype[3] @@ -77,6 +78,8 @@ class Hand: # dict from player names to lists of hole cards self.holecards = {} + + self.stacks = {} # dict from player names to amounts collected self.collected = {} @@ -106,6 +109,7 @@ chips (string) the chips the player has at the start of the hand (can be None) If a player has None chips he won't be added.""" if chips is not None: self.players.append([seat, name, chips]) + self.stacks[name] = Decimal(chips) self.holecards[name] = set() for street in self.streetList: self.bets[street][name] = [] @@ -176,7 +180,9 @@ Card ranks will be uppercased # if player is None, it's a missing small blind. if player is not None: self.bets['PREFLOP'][player].append(Decimal(amount)) - self.actions['PREFLOP'] += [(player, 'posts', blindtype, amount)] + self.stacks[player] -= Decimal(amount) + print "DEBUG %s stack %s" % (player, self.stacks[player]) + self.actions['PREFLOP'] += [(player, 'posts', blindtype, amount, self.stacks[player]==0)] if blindtype == 'big blind': self.lastBet['PREFLOP'] = Decimal(amount) elif blindtype == 'small & big blinds': @@ -191,7 +197,9 @@ Card ranks will be uppercased if amount is not None: self.bets[street][player].append(Decimal(amount)) #self.lastBet[street] = Decimal(amount) - self.actions[street] += [(player, 'calls', amount)] + self.stacks[player] -= Decimal(amount) + self.actions[street] += [(player, 'calls', amount, self.stacks[player]==0)] + def addRaiseTo(self, street, player, amountTo): """\ @@ -208,13 +216,19 @@ Add a raise on [street] by [player] to [amountTo] self.lastBet[street] = Decimal(amountTo) amountBy = Decimal(amountTo) - amountToCall self.bets[street][player].append(amountBy+amountToCall) - self.actions[street] += [(player, 'raises', amountBy, amountTo, amountToCall)] + self.stacks[player] -= (Decimal(amountBy)+Decimal(amountToCall)) + print "DEBUG %s stack %s" % (player, self.stacks[player]) + self.actions[street] += [(player, 'raises', amountBy, amountTo, amountToCall, self.stacks[player]==0)] + def addBet(self, street, player, amount): self.checkPlayerExists(player) self.bets[street][player].append(Decimal(amount)) - self.actions[street] += [(player, 'bets', amount)] + self.stacks[player] -= Decimal(amount) + print "DEBUG %s stack %s" % (player, self.stacks[player]) + self.actions[street] += [(player, 'bets', amount, self.stacks[player]==0)] self.lastBet[street] = Decimal(amount) + def addFold(self, street, player): self.checkPlayerExists(player) @@ -246,9 +260,10 @@ Add a raise on [street] by [player] to [amountTo] self.totalpot += reduce(operator.add, self.bets[street][player], 0) print "conventional totalpot:", self.totalpot + + self.totalpot = 0 - - print self.actions + for street in self.actions: uncalled = 0 calls = [0] @@ -278,6 +293,7 @@ Add a raise on [street] by [player] to [amountTo] self.totalpot -= (uncalled - max(calls)) print "new totalpot:", self.totalpot + if self.totalcollected is None: self.totalcollected = 0; for amount in self.collected.values(): @@ -417,15 +433,15 @@ Map the tuple self.gametype onto the pokerstars string describing it def printActionLine(self, act, fh): if act[1] == 'folds': - print >>fh, _("%s: folds" %(act[0])) + print >>fh, _("%s: folds " %(act[0])) elif act[1] == 'checks': - print >>fh, _("%s: checks" %(act[0])) + print >>fh, _("%s: checks " %(act[0])) if act[1] == 'calls': - print >>fh, _("%s: calls $%s" %(act[0], act[2])) + print >>fh, _("%s: calls $%s%s" %(act[0], act[2], ' and is all-in' if act[3] else '')) if act[1] == 'bets': - print >>fh, _("%s: bets $%s" %(act[0], act[2])) + print >>fh, _("%s: bets $%s%s" %(act[0], act[2], ' and is all-in' if act[3] else '')) if act[1] == 'raises': - print >>fh, _("%s: raises $%s to $%s" %(act[0], act[2], act[3])) + print >>fh, _("%s: raises $%s to $%s%s" %(act[0], act[2], act[3], ' and is all-in' if act[5] else '')) # going to use pokereval to figure out hands at some point. # these functions are copied from pokergame.py diff --git a/pyfpdb/HandHistoryConverter.py b/pyfpdb/HandHistoryConverter.py index 256de673..858c18b3 100644 --- a/pyfpdb/HandHistoryConverter.py +++ b/pyfpdb/HandHistoryConverter.py @@ -115,13 +115,15 @@ class HandHistoryConverter: print "\nInput:\n"+hand.string self.readHandInfo(hand) self.readPlayerStacks(hand) + print "DEBUG", hand.stacks self.markStreets(hand) self.readBlinds(hand) self.readHeroCards(hand) # want to generalise to draw games self.readCommunityCards(hand) # read community cards self.readShowdownActions(hand) - # Read action (Note: no guarantee this is in hand order. - for street in hand.streets.groupdict(): + + # Read actions in street order + for street in hand.streetList: # go through them in order if hand.streets.group(street) is not None: self.readAction(hand, street) From 1a462301766791b3ec3d741137f2e2c72ac3da5a Mon Sep 17 00:00:00 2001 From: Worros Date: Tue, 16 Dec 2008 23:45:58 +0900 Subject: [PATCH 04/23] Add first pass Full Tilt converter --- pyfpdb/FulltiltToFpdb.py | 219 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100755 pyfpdb/FulltiltToFpdb.py diff --git a/pyfpdb/FulltiltToFpdb.py b/pyfpdb/FulltiltToFpdb.py new file mode 100755 index 00000000..a77b129e --- /dev/null +++ b/pyfpdb/FulltiltToFpdb.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python +# Copyright 2008, Carl Gherardi +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +######################################################################## + +import sys +import Configuration +from HandHistoryConverter import * + +# FullTilt HH Format + +#Full Tilt Poker Game #9403951181: Table CR - tay - $0.05/$0.10 - No Limit Hold'em - 9:40:20 ET - 2008/12/09 +#Seat 1: rigoise ($15.95) +#Seat 2: K2dream ($6.70) +#Seat 4: ravens2216 ($10) +#Seat 5: rizkouner ($4) +#Seat 6: Sorrowful ($8.35) +#rigoise posts the small blind of $0.05 +#K2dream posts the big blind of $0.10 +#5 seconds left to act +#rizkouner posts $0.10 +#The button is in seat #6 +#*** HOLE CARDS *** +#Dealt to Sorrowful [8h Qc] +#ravens2216 folds +#rizkouner checks +#Sorrowful has 15 seconds left to act +#Sorrowful folds +#rigoise folds +#K2dream checks +#*** FLOP *** [9d Kc 5c] +#K2dream checks +#rizkouner checks +#*** TURN *** [9d Kc 5c] [5h] +#K2dream has 15 seconds left to act +#K2dream bets $0.20 +#rizkouner calls $0.20 +#*** RIVER *** [9d Kc 5c 5h] [6h] +#K2dream checks +#rizkouner has 15 seconds left to act +#rizkouner bets $0.20 +#K2dream folds +#Uncalled bet of $0.20 returned to rizkouner +#rizkouner mucks +#rizkouner wins the pot ($0.60) +#*** SUMMARY *** +#Total pot $0.65 | Rake $0.05 +#Board: [9d Kc 5c 5h 6h] +#Seat 1: rigoise (small blind) folded before the Flop +#Seat 2: K2dream (big blind) folded on the River +#Seat 4: ravens2216 didn't bet (folded) +#Seat 5: rizkouner collected ($0.60), mucked +#Seat 6: Sorrowful (button) didn't bet (folded) + + +class FullTilt(HandHistoryConverter): + def __init__(self, config, file): + print "Initialising FullTilt converter class" + HandHistoryConverter.__init__(self, config, file, sitename="FullTilt") # Call super class init. + self.sitename = "FullTilt" + self.setFileType("text", "cp1252") + self.rexx.setGameInfoRegex('.*- \$?(?P[.0-9]+)/\$?(?P[.0-9]+)') + self.rexx.setSplitHandRegex('\n\n+') + self.rexx.setHandInfoRegex('.*#(?P[0-9]+): Table (?P[- a-zA-Z]+) - \$?(?P[.0-9]+)/\$?(?P[.0-9]+) - (?P[a-zA-Z\' ]+) - (?P
[0-9]+):(?P[0-9]+):(?P[0-9]+) ET - (?P[0-9]+)/(?P[0-9]+)/(?P[0-9]+)') +# self.rexx.setHandInfoRegex('.*#(?P[0-9]+): Table (?P
[ a-zA-Z]+) - \$?(?P[.0-9]+)/\$?(?P[.0-9]+) - (?P.*) - (?P
[0-9]+):(?P[0-9]+) ET - (?P[0-9]+)/(?P[0-9]+)/(?P[0-9]+)Table (?P
[ a-zA-Z]+)\nSeat (?P
[- a-zA-Z]+) - \$?(?P[.0-9]+)/\$?(?P[.0-9]+) - (?P[a-zA-Z\' ]+) - (?P
[0-9]+):(?P[0-9]+):(?P[0-9]+) ET - (?P[0-9]+)/(?P[0-9]+)/(?P[0-9]+)') # self.rexx.setHandInfoRegex('.*#(?P[0-9]+): Table (?P
[ a-zA-Z]+) - \$?(?P[.0-9]+)/\$?(?P[.0-9]+) - (?P.*) - (?P
[0-9]+):(?P[0-9]+) ET - (?P[0-9]+)/(?P[0-9]+)/(?P[0-9]+)Table (?P
[ a-zA-Z]+)\nSeat (?P
[- a-zA-Z]+) - \$?(?P[.0-9]+)/\$?(?P[.0-9]+) - (?P[a-zA-Z\' ]+) - (?P
[0-9]+):(?P[0-9]+):(?P[0-9]+) ET - (?P[0-9]+)/(?P[0-9]+)/(?P[0-9]+)') # self.rexx.setHandInfoRegex('.*#(?P[0-9]+): Table (?P
[ a-zA-Z]+) - \$?(?P[.0-9]+)/\$?(?P[.0-9]+) - (?P.*) - (?P
[0-9]+):(?P[0-9]+) ET - (?P[0-9]+)/(?P[0-9]+)/(?P[0-9]+)Table (?P
[ a-zA-Z]+)\nSeat (?P
[- a-zA-Z]+) - \$?(?P[.0-9]+)/\$?(?P[.0-9]+) - (?P[a-zA-Z\' ]+) - (?P
[0-9]+):(?P[0-9]+):(?P[0-9]+) ET - (?P[0-9]+)/(?P[0-9]+)/(?P[0-9]+)') + self.rexx.setHandInfoRegex('.*#(?P[0-9]+): Table (?P
[- a-zA-Z]+) (\((?P.+)\) )?- \$?(?P[.0-9]+)/\$?(?P[.0-9]+) - (?P[a-zA-Z\' ]+) - (?P
[0-9]+):(?P[0-9]+):(?P[0-9]+) ET - (?P[0-9]+)/(?P[0-9]+)/(?P[0-9]+)') # self.rexx.setHandInfoRegex('.*#(?P[0-9]+): Table (?P
[ a-zA-Z]+) - \$?(?P[.0-9]+)/\$?(?P[.0-9]+) - (?P.*) - (?P
[0-9]+):(?P[0-9]+) ET - (?P[0-9]+)/(?P[0-9]+)/(?P[0-9]+)Table (?P
[ a-zA-Z]+)\nSeat (?P
[ a-zA-Z]+)\nSeat (?P
[ a-zA-Z]+)\nSeat (?P
[- a-zA-Z]+) (\((?P.+)\) )?- \$?(?P[.0-9]+)/\$?(?P[.0-9]+) - (?P[a-zA-Z\' ]+) - (?P
[0-9]+):(?P[0-9]+):(?P[0-9]+) ET - (?P[0-9]+)/(?P[0-9]+)/(?P[0-9]+)') + self.rexx.setHandInfoRegex('.*#(?P[0-9]+): Table (?P
[- a-zA-Z]+) (\((?P.+)\) )?- \$?(?P[.0-9]+)/\$?(?P[.0-9]+) - (?P[a-zA-Z\' ]+) - (?P.*)') # self.rexx.setHandInfoRegex('.*#(?P[0-9]+): Table (?P
[ a-zA-Z]+) - \$?(?P[.0-9]+)/\$?(?P[.0-9]+) - (?P.*) - (?P
[0-9]+):(?P[0-9]+) ET - (?P[0-9]+)/(?P[0-9]+)/(?P[0-9]+)Table (?P
[ a-zA-Z]+)\nSeat (?P
[ \w]+), (?P\d\d \w+ \d\d\d\d \d\d:\d\d (AM|PM))") + # SB BB HID TABLE DAY MON YEAR HR12 MIN AMPM + + self.rexx.button_re = re.compile('#SUMMARY\nDealer: (?P.*)\n') + + #Seat 1: .Lucchess ($4.17 in chips) + self.rexx.setPlayerInfoRegex('Seat (?P[0-9]+): (?P.*) \((\$(?P[.0-9]+) in chips)\)') + + #ANTES/BLINDS + #helander2222 posts blind ($0.25), lopllopl posts blind ($0.50). + self.rexx.setPostSbRegex('(?P.*) posts blind \(\$?(?P[.0-9]+)\), ') + self.rexx.setPostBbRegex('\), (?P.*) posts blind \(\$?(?P[.0-9]+)\).') + self.rexx.setPostBothRegex('.*\n(?P.*): posts small \& big blinds \[\$? (?P[.0-9]+)') + self.rexx.setHeroCardsRegex('.*\nDealt\sto\s(?P.*)\s\[ (?P.*) \]') + + #lopllopl checks, Eurolll checks, .Lucchess checks. + self.rexx.setActionStepRegex('(, )?(?P.*?)(?P bets| checks| raises| calls| folds)( \$(?P\d*\.?\d*))?[.]?') + + #Uchilka shows [ KC,JD ] + self.rexx.setShowdownActionRegex('(?P.*) shows \[ (?P.+) \]') + + # TODO: read SUMMARY correctly for collected pot stuff. + #Uchilka, bets $11.75, collects $23.04, net $11.29 + self.rexx.setCollectPotRegex('(?P.*), bets.+, collects \$(?P\d*\.?\d*), net.* ') + self.rexx.sits_out_re = re.compile('(?P.*) sits out') + self.rexx.compileRegexes() + + def readSupportedGames(self): + pass + + def determineGameType(self): + # Cheating with this regex, only support nlhe at the moment + gametype = ["ring", "hold", "nl"] + + m = self.rexx.hand_info_re.search(self.obs) + gametype = gametype + [m.group('SB')] + gametype = gametype + [m.group('BB')] + + return gametype + + def readHandInfo(self, hand): + m = self.rexx.hand_info_re.search(hand.string) + hand.handid = m.group('HID') + hand.tablename = m.group('TABLE') + #hand.buttonpos = self.rexx.button_re.search(hand.string).group('BUTTONPNAME') +# These work, but the info is already in the Hand class - should be used for tourneys though. +# m.group('SB') +# m.group('BB') +# m.group('GAMETYPE') + +# Believe Everleaf time is GMT/UTC, no transation necessary +# Stars format (Nov 10 2008): 2008/11/07 12:38:49 CET [2008/11/07 7:38:49 ET] +# or : 2008/11/07 12:38:49 ET +# Not getting it in my HH files yet, so using +# 2008/11/10 3:58:52 ET +#TODO: Do conversion from GMT to ET +#TODO: Need some date functions to convert to different timezones (Date::Manip for perl rocked for this) + + hand.starttime = time.strptime(m.group('DATETIME'), "%d %b %Y %I:%M %p") + #hand.starttime = "%d/%02d/%02d %d:%02d:%02d ET" %(int(m.group('YEAR')), int(m.group('MON')), int(m.group('DAY')), + #int(m.group('HR')), int(m.group('MIN')), int(m.group('SEC'))) + + def readPlayerStacks(self, hand): + m = self.rexx.player_info_re.finditer(hand.string) + players = [] + for a in m: + hand.addPlayer(int(a.group('SEAT')), a.group('PNAME'), a.group('CASH')) + + def markStreets(self, hand): + # PREFLOP = ** Dealing down cards ** + # This re fails if, say, river is missing; then we don't get the ** that starts the river. + #m = re.search('(\*\* Dealing down cards \*\*\n)(?P.*?\n\*\*)?( Dealing Flop \*\* \[ (?P\S\S), (?P\S\S), (?P\S\S) \])?(?P.*?\*\*)?( Dealing Turn \*\* \[ (?P\S\S) \])?(?P.*?\*\*)?( Dealing River \*\* \[ (?P\S\S) \])?(?P.*)', hand.string,re.DOTALL) + + m = re.search(r"PRE-FLOP(?P.+(?=FLOP)|.+(?=SHOWDOWN))" + r"(FLOP (?P\[board cards .+ \].+(?=TURN)|.+(?=SHOWDOWN)))?" + r"(TURN (?P\[board cards .+ \].+(?=RIVER)|.+(?=SHOWDOWN)))?" + r"(RIVER (?P\[board cards .+ \].+(?=SHOWDOWN)))?", hand.string,re.DOTALL) + + hand.addStreets(m) + + + def readCommunityCards(self, hand, street): + self.rexx.board_re = re.compile(r"\[board cards (?P.+) \]") + print hand.streets.group(street) + if street in ('FLOP','TURN','RIVER'): # a list of streets which get dealt community cards (i.e. all but PREFLOP) + m = self.rexx.board_re.search(hand.streets.group(street)) + hand.setCommunityCards(street, m.group('CARDS').split(',')) + + def readBlinds(self, hand): + try: + m = self.rexx.small_blind_re.search(hand.string) + hand.addBlind(m.group('PNAME'), 'small blind', m.group('SB')) + except: # no small blind + hand.addBlind(None, None, None) + for a in self.rexx.big_blind_re.finditer(hand.string): + hand.addBlind(a.group('PNAME'), 'big blind', a.group('BB')) + for a in self.rexx.both_blinds_re.finditer(hand.string): + hand.addBlind(a.group('PNAME'), 'small & big blinds', a.group('SBBB')) + + def readHeroCards(self, hand): + m = self.rexx.hero_cards_re.search(hand.string) + if(m == None): + #Not involved in hand + hand.involved = False + else: + hand.hero = m.group('PNAME') + # "2c, qh" -> set(["2c","qc"]) + # Also works with Omaha hands. + cards = m.group('CARDS') + cards = set(cards.split(',')) + hand.addHoleCards(cards, m.group('PNAME')) + + def readAction(self, hand, street): + m = self.rexx.action_re.finditer(hand.streets.group(street)) + for action in m: + if action.group('ATYPE') == ' raises': + hand.addRaiseTo( street, action.group('PNAME'), action.group('BET') ) + elif action.group('ATYPE') == ' calls': + hand.addCall( street, action.group('PNAME'), action.group('BET') ) + elif action.group('ATYPE') == ' bets': + hand.addBet( street, action.group('PNAME'), action.group('BET') ) + elif action.group('ATYPE') == ' folds': + hand.addFold( street, action.group('PNAME')) + elif action.group('ATYPE') == ' checks': + hand.addCheck( street, action.group('PNAME')) + else: + print "DEBUG: unimplemented readAction: %s %s" %(action.group('PNAME'),action.group('ATYPE'),) + #hand.actions[street] += [[action.group('PNAME'), action.group('ATYPE')]] + # TODO: Everleaf does not record uncalled bets. + + def readShowdownActions(self, hand): + for shows in self.rexx.showdown_action_re.finditer(hand.string): + cards = shows.group('CARDS') + cards = set(cards.split(',')) + hand.addShownCards(cards, shows.group('PNAME')) + + def readCollectPot(self,hand): + for m in self.rexx.collect_pot_re.finditer(hand.string): + hand.addCollectPot(player=m.group('PNAME'),pot=m.group('POT')) + + def readShownCards(self,hand): + return + #for m in self.rexx.collect_pot_re.finditer(hand.string): + #if m.group('CARDS') is not None: + #cards = m.group('CARDS') + #cards = set(cards.split(',')) + #hand.addShownCards(cards=None, player=m.group('PNAME'), holeandboard=cards) + + + + +if __name__ == "__main__": + c = Configuration.Config() + if len(sys.argv) == 1: + testfile = "regression-test-files/ongame/nlhe/ong NLH handhq_0.txt" + else: + testfile = sys.argv[1] + e = OnGame(c, testfile) + e.processFile() + print str(e) From a5bd7499597cc8e45383593d71f3d6feb47b7e88 Mon Sep 17 00:00:00 2001 From: Matt Turnbull Date: Wed, 17 Dec 2008 00:30:31 +0000 Subject: [PATCH 13/23] quick hack to Hand to help ongame --- pyfpdb/Hand.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyfpdb/Hand.py b/pyfpdb/Hand.py index 26a60c71..e215e99d 100644 --- a/pyfpdb/Hand.py +++ b/pyfpdb/Hand.py @@ -33,7 +33,7 @@ from copy import deepcopy class Hand: # def __init__(self, sitename, gametype, sb, bb, string): - UPS = {'a':'A', 't':'T', 'j':'J', 'q':'Q', 'k':'K'} + UPS = {'a':'A', 't':'T', 'j':'J', 'q':'Q', 'k':'K', 'S':'s', 'C':'c', 'H':'h', 'D':'d'} def __init__(self, sitename, gametype, string): self.sitename = sitename self.gametype = gametype From e5e8643557167bd341ed7764057dce511b294881 Mon Sep 17 00:00:00 2001 From: Worros Date: Wed, 17 Dec 2008 13:46:16 +0900 Subject: [PATCH 14/23] Minor update to handinfo regex for OnGame --- pyfpdb/OnGameToFpdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyfpdb/OnGameToFpdb.py b/pyfpdb/OnGameToFpdb.py index 78ecc99c..c905905a 100755 --- a/pyfpdb/OnGameToFpdb.py +++ b/pyfpdb/OnGameToFpdb.py @@ -77,7 +77,7 @@ class OnGame(HandHistoryConverter): #Texas Hold'em $.5-$1 NL (real money), hand #P4-76915775-797 #Table Kuopio, 20 Sep 2008 11:59 PM - self.rexx.setHandInfoRegex(r"Texas Hold'em \$?(?P[.0-9]+)-\$?(?P[.0-9]+) NL \(real money\), hand #(?P[A-Z\d-]+)\nTable\ (?P
[ \w]+), (?P\d\d \w+ \d\d\d\d \d\d:\d\d (AM|PM))") + self.rexx.setHandInfoRegex(r"Texas Hold'em \$?(?P[.0-9]+)-\$?(?P[.0-9]+) NL \(real money\), hand #(?P[-A-Z\d]+)\nTable\ (?P
[\' \w]+), (?P\d\d \w+ \d\d\d\d \d\d:\d\d (AM|PM))") # SB BB HID TABLE DAY MON YEAR HR12 MIN AMPM self.rexx.button_re = re.compile('#SUMMARY\nDealer: (?P.*)\n') From dd7ede890332e123bbfe880eeb8990fc0c594b14 Mon Sep 17 00:00:00 2001 From: Worros Date: Wed, 17 Dec 2008 14:04:29 +0900 Subject: [PATCH 15/23] Adjust regex to read 'and is all-in' --- pyfpdb/OnGameToFpdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyfpdb/OnGameToFpdb.py b/pyfpdb/OnGameToFpdb.py index c905905a..ee16eb41 100755 --- a/pyfpdb/OnGameToFpdb.py +++ b/pyfpdb/OnGameToFpdb.py @@ -93,7 +93,7 @@ class OnGame(HandHistoryConverter): self.rexx.setHeroCardsRegex('.*\nDealt\sto\s(?P.*)\s\[ (?P.*) \]') #lopllopl checks, Eurolll checks, .Lucchess checks. - self.rexx.setActionStepRegex('(, )?(?P.*?)(?P bets| checks| raises| calls| folds)( \$(?P\d*\.?\d*))?[.]?') + self.rexx.setActionStepRegex('(, )?(?P.*?)(?P bets| checks| raises| calls| folds)( \$(?P\d*\.?\d*))?( and is all-in)?') #Uchilka shows [ KC,JD ] self.rexx.setShowdownActionRegex('(?P.*) shows \[ (?P.+) \]') From 877f0771ab1695d0e763d5c636b6e93c473a373b Mon Sep 17 00:00:00 2001 From: Matt Turnbull Date: Wed, 17 Dec 2008 11:22:02 +0000 Subject: [PATCH 16/23] nongreedy matches in collect_pot_re to fix kicker being picked up instead of hand bug --- pyfpdb/EverleafToFpdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyfpdb/EverleafToFpdb.py b/pyfpdb/EverleafToFpdb.py index 03b22995..bce92e79 100755 --- a/pyfpdb/EverleafToFpdb.py +++ b/pyfpdb/EverleafToFpdb.py @@ -79,7 +79,7 @@ class Everleaf(HandHistoryConverter): self.rexx.setHeroCardsRegex('.*\nDealt\sto\s(?P.*)\s\[ (?P.*) \]') self.rexx.setActionStepRegex('.*\n(?P.*)(?P: bets| checks| raises| calls| folds)(\s\[\$ (?P[.\d]+) USD\])?') self.rexx.setShowdownActionRegex('.*\n(?P.*) shows \[ (?P.*) \]') - self.rexx.setCollectPotRegex('.*\n(?P.*) wins \$ (?P[.\d]+) USD(.*\[ (?P.*) \])?') + self.rexx.setCollectPotRegex('.*\n(?P.*) wins \$ (?P[.\d]+) USD(.*?\[ (?P.*?) \])?') self.rexx.sits_out_re = re.compile('(?P.*) sits out') self.rexx.compileRegexes() From d16816649558f4a4cd08f3e5b513ae65619b278e Mon Sep 17 00:00:00 2001 From: Matt Turnbull Date: Wed, 17 Dec 2008 11:54:26 +0000 Subject: [PATCH 17/23] Added: addCallandRaise - when reported amount is the actual amount transfered addRaiseBy - when reported is the amount additional to the previous bet _addRaise - common helper --- pyfpdb/Hand.py | 61 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/pyfpdb/Hand.py b/pyfpdb/Hand.py index e215e99d..f4a7bec7 100644 --- a/pyfpdb/Hand.py +++ b/pyfpdb/Hand.py @@ -204,25 +204,62 @@ Card ranks will be uppercased print "DEBUG %s calls %s, stack %s" % (player, amount, self.stacks[player]) self.actions[street] += [(player, 'calls', amount, self.stacks[player]==0)] - - def addRaiseTo(self, street, player, amountTo): + def addRaiseBy(self, street, player, amountBy): """\ -Add a raise on [street] by [player] to [amountTo] +Add a raise by amountBy on [street] by [player] """ - #Given only the amount raised to, the amount of the raise can be calculated by + #Given only the amount raised by, the amount of the raise can be calculated by # working out how much this player has already in the pot # (which is the sum of self.bets[street][player]) # and how much he needs to call to match the previous player # (which is tracked by self.lastBet) + # let Bp = previous bet + # Bc = amount player has committed so far + # Rb = raise by + # then: C = Bp - Bc (amount to call) + # Rt = Bp + Rb (raise to) + # self.checkPlayerExists(player) - committedThisStreet = reduce(operator.add, self.bets[street][player], 0) - amountToCall = self.lastBet[street] - committedThisStreet - self.lastBet[street] = Decimal(amountTo) - amountBy = Decimal(amountTo) - amountToCall - self.bets[street][player].append(amountBy+amountToCall) - self.stacks[player] -= (Decimal(amountBy)+Decimal(amountToCall)) - print "DEBUG %s stack %s" % (player, self.stacks[player]) - self.actions[street] += [(player, 'raises', amountBy, amountTo, amountToCall, self.stacks[player]==0)] + Rb = Decimal(amountBy) + Bp = self.lastBet[street] + Bc = reduce(operator.add, self.bets[street][player], 0) + C = Bp - Bc + Rt = Bp + Rb + + self.bets[street][player].append(C + Rb) + self.stacks[player] -= (C + Rb) + self.actions[street] += [(player, 'raises', Rb, Rt, C, self.stacks[player]==0)] + self.lastBet[street] = Rt + + def addCallandRaise(self, street, player, amount): + """\ +For sites which by "raises x" mean "calls and raises putting a total of x in the por". """ + self.checkPlayerExists(player) + CRb = Decimal(amount) + Bp = self.lastBet[street] + Bc = reduce(operator.add, self.bets[street][player], 0) + C = Bp - Bc + Rb = CRb - C + Rt = Bp + Rb + + self._addRaise(street, player, C, Rb, Rt) + + def _addRaise(self, street, player, C, Rb, Rt): + self.bets[street][player].append(C + Rb) + self.stacks[player] -= (C + Rb) + self.actions[street] += [(player, 'raises', Rb, Rt, C, self.stacks[player]==0)] + self.lastBet[street] = Rt + + def addRaiseTo(self, street, player, amountTo): + """\ +Add a raise on [street] by [player] to [amountTo] +""" + self.checkPlayerExists(player) + Bc = reduce(operator.add, self.bets[street][player], 0) + Rt = Decimal(amountTo) + C = Bp - Bc + Rb = Rt - C + self._addRaise(street, player, C, Rb, Rt) def addBet(self, street, player, amount): From fe2c8068226ce5108b8c379041c27fd960826b2d Mon Sep 17 00:00:00 2001 From: Matt Turnbull Date: Wed, 17 Dec 2008 11:57:06 +0000 Subject: [PATCH 18/23] Everleaf appears to need addCallandRaise --- pyfpdb/EverleafToFpdb.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyfpdb/EverleafToFpdb.py b/pyfpdb/EverleafToFpdb.py index bce92e79..d0885d92 100755 --- a/pyfpdb/EverleafToFpdb.py +++ b/pyfpdb/EverleafToFpdb.py @@ -80,6 +80,7 @@ class Everleaf(HandHistoryConverter): self.rexx.setActionStepRegex('.*\n(?P.*)(?P: bets| checks| raises| calls| folds)(\s\[\$ (?P[.\d]+) USD\])?') self.rexx.setShowdownActionRegex('.*\n(?P.*) shows \[ (?P.*) \]') self.rexx.setCollectPotRegex('.*\n(?P.*) wins \$ (?P[.\d]+) USD(.*?\[ (?P.*?) \])?') + #self.rexx.setCollectPotRegex('.*\n(?P.*) wins \$ (?P[.\d]+) USD(.*\[ (?P) \S\S, \S\S, \S\S, \S\S, \S\S \])?') self.rexx.sits_out_re = re.compile('(?P.*) sits out') self.rexx.compileRegexes() @@ -169,7 +170,7 @@ class Everleaf(HandHistoryConverter): m = self.rexx.action_re.finditer(hand.streets.group(street)) for action in m: if action.group('ATYPE') == ' raises': - hand.addRaiseTo( street, action.group('PNAME'), action.group('BET') ) + hand.addCallandRaise( street, action.group('PNAME'), action.group('BET') ) elif action.group('ATYPE') == ' calls': hand.addCall( street, action.group('PNAME'), action.group('BET') ) elif action.group('ATYPE') == ': bets': From 7803f523071d9b81d5e81480745cda1dc089c705 Mon Sep 17 00:00:00 2001 From: Matt Turnbull Date: Thu, 18 Dec 2008 17:49:17 +0000 Subject: [PATCH 19/23] autoimport a bit better, no? --- pyfpdb/GuiAutoImport.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/pyfpdb/GuiAutoImport.py b/pyfpdb/GuiAutoImport.py index 30fe7e67..820642e4 100644 --- a/pyfpdb/GuiAutoImport.py +++ b/pyfpdb/GuiAutoImport.py @@ -104,9 +104,12 @@ class GuiAutoImport (threading.Thread): def do_import(self): """Callback for timer to do an import iteration.""" - self.importer.runUpdated() - print "GuiAutoImport.import_dir done" - return self.doAutoImportBool + if self.doAutoImportBool: + self.importer.runUpdated() + print "GuiAutoImport.import_dir done" + return True + else: + return False def startClicked(self, widget, data): """runs when user clicks start on auto import tab""" @@ -149,12 +152,15 @@ class GuiAutoImport (threading.Thread): interval=int(self.intervalEntry.get_text()) gobject.timeout_add(interval*1000, self.do_import) else: # toggled off - self.doAutoImportBool = False # do_import will return this and stop the gobject callback timer - #TODO: other clean up, such as killing HUD - print "Stopping autoimport" - self.pipe_to_hud.communicate('\n') # waits for process to terminate - self.pipe_to_hud = None - widget.set_label(u'Start Autoimport') + self.doAutoImportBool = False # do_import will return this and stop the gobject callback timer + print "Stopping autoimport" + print >>self.pipe_to_hud.stdin, "\n" + #self.pipe_to_hud.communicate('\n') # waits for process to terminate + self.pipe_to_hud = None + self.startButton.set_label(u'Start Autoimport') + + + #end def GuiAutoImport.startClicked def get_vbox(self): From 49aa8921e39427aa0a697b281170fabe414a446a Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 19 Dec 2008 16:52:32 +0900 Subject: [PATCH 20/23] Grapher: Update to support mutiple sites and players Makes sites actually selectable via checkboxes. Removed the sitename from the graph string for the moment - How that string is generated needs a major overhaul --- pyfpdb/FpdbSQLQueries.py | 9 +++- pyfpdb/GuiGraphViewer.py | 78 ++++++++++++++++++++++++---------- pyfpdb/HUD_config.xml.example | 2 +- pyfpdb/psnlheparser-mct.tgz | Bin 26184 -> 0 bytes 4 files changed, 63 insertions(+), 26 deletions(-) delete mode 100644 pyfpdb/psnlheparser-mct.tgz diff --git a/pyfpdb/FpdbSQLQueries.py b/pyfpdb/FpdbSQLQueries.py index 07fa15bb..9afdc28f 100644 --- a/pyfpdb/FpdbSQLQueries.py +++ b/pyfpdb/FpdbSQLQueries.py @@ -635,6 +635,11 @@ class FpdbSQLQueries: elif(self.dbname == 'SQLite'): self.query['getPlayerId'] = """SELECT id from Players where name = %s""" + if(self.dbname == 'MySQL InnoDB') or (self.dbname == 'PostgreSQL'): + self.query['getSiteId'] = """SELECT id from Sites where name = %s""" + elif(self.dbname == 'SQLite'): + self.query['getSiteId'] = """SELECT id from Sites where name = %s""" + if(self.dbname == 'MySQL InnoDB') or (self.dbname == 'PostgreSQL'): self.query['getRingProfitAllHandsPlayerIdSite'] = """ SELECT hp.handId, hp.winnings, coalesce(hp.ante,0) + SUM(ha.amount) @@ -643,8 +648,8 @@ class FpdbSQLQueries: INNER JOIN Players pl ON hp.playerId = pl.id INNER JOIN Hands h ON h.id = hp.handId INNER JOIN HandsActions ha ON ha.handPlayerId = hp.id - WHERE pl.name = %s - AND pl.siteId = %s + where pl.id in + AND pl.siteId in AND hp.tourneysPlayersId IS NULL GROUP BY hp.handId, hp.winnings, h.handStart, hp.ante ORDER BY h.handStart""" diff --git a/pyfpdb/GuiGraphViewer.py b/pyfpdb/GuiGraphViewer.py index 94732295..a0a2e326 100644 --- a/pyfpdb/GuiGraphViewer.py +++ b/pyfpdb/GuiGraphViewer.py @@ -50,22 +50,27 @@ class GuiGraphViewer (threading.Thread): try: self.canvas.destroy() except AttributeError: pass - # Whaich sites are selected? - # TODO: - # What hero names for the selected site? - # TODO: + sitenos = [] + playerids = [] - name = self.heroes[self.sites] + # Which sites are selected? + for site in self.sites: + if self.sites[site] == True: + sitenos.append(self.siteid[site]) + self.cursor.execute(self.sql.query['getPlayerId'], (self.heroes[site],)) + result = self.db.cursor.fetchall() + if len(result) == 1: + playerids.append(result[0][0]) - if self.sites == "PokerStars": - site=2 - sitename="PokerStars: " - elif self.sites=="Full Tilt": - site=1 - sitename="Full Tilt: " - else: - print "invalid text in site selection in graph, defaulting to PS" - site=2 + if sitenos == []: + #Should probably pop up here. + print "No sites selected - defaulting to PokerStars" + sitenos = [2] + + + if playerids == []: + print "No player ids found" + return self.fig = Figure(figsize=(5,4), dpi=100) @@ -74,7 +79,7 @@ class GuiGraphViewer (threading.Thread): #Get graph data from DB starttime = time() - line = self.getRingProfitGraph(name, site) + line = self.getRingProfitGraph(playerids, sitenos) print "Graph generated in: %s" %(time() - starttime) self.ax.set_title("Profit graph for ring games") @@ -87,7 +92,8 @@ class GuiGraphViewer (threading.Thread): #TODO: Do something useful like alert user print "No hands returned by graph query" else: - text = "All Hands, " + sitename + str(name) + "\nProfit: $" + str(line[-1]) + "\nTotal Hands: " + str(len(line)) +# text = "All Hands, " + sitename + str(name) + "\nProfit: $" + str(line[-1]) + "\nTotal Hands: " + str(len(line)) + text = "All Hands, " + "\nProfit: $" + str(line[-1]) + "\nTotal Hands: " + str(len(line)) self.ax.annotate(text, xy=(10, -10), @@ -103,8 +109,26 @@ class GuiGraphViewer (threading.Thread): self.canvas.show() #end of def showClicked - def getRingProfitGraph(self, name, site): - self.cursor.execute(self.sql.query['getRingProfitAllHandsPlayerIdSite'], (name, site)) + def getRingProfitGraph(self, names, sites): + tmp = self.sql.query['getRingProfitAllHandsPlayerIdSite'] +# print "DEBUG: getRingProfitGraph" + + #Buggered if I can find a way to do this 'nicely' take a list of intergers and longs + # and turn it into a tuple readale by sql. + # [5L] into (5) not (5,) and [5L, 2829L] into (5, 2829) + nametest = str(tuple(names)) + sitetest = str(tuple(sites)) + nametest = nametest.replace("L", "") + nametest = nametest.replace(",)",")") + sitetest = sitetest.replace(",)",")") + + #Must be a nicer way to deal with tuples of size 1 ie. (2,) - which makes sql barf + tmp = tmp.replace("", nametest) + tmp = tmp.replace("", sitetest) + +# print "DEBUG: sql query:" +# print tmp + self.cursor.execute(tmp) #returns (HandId,Winnings,Costs,Profit) winnings = self.db.cursor.fetchall() @@ -125,7 +149,6 @@ class GuiGraphViewer (threading.Thread): pname.set_text(player) pname.set_width_chars(20) hbox.pack_start(pname, False, True, 0) - #TODO: Need to connect a callback here pname.connect("changed", self.__set_hero_name, site) #TODO: Look at GtkCompletion - to fill out usernames pname.show() @@ -134,7 +157,7 @@ class GuiGraphViewer (threading.Thread): def __set_hero_name(self, w, site): self.heroes[site] = w.get_text() - print "DEBUG: settings heroes[%s]: %s"%(site, self.heroes[site]) +# print "DEBUG: settings heroes[%s]: %s"%(site, self.heroes[site]) def createSiteLine(self, hbox, site): cb = gtk.CheckButton(site) @@ -144,8 +167,9 @@ class GuiGraphViewer (threading.Thread): def __set_site_select(self, w, site): # This doesn't behave as intended - self.site only allows 1 site for the moment. - self.sites = site - print "self.sites set to %s" %(self.sites) + print w.get_active() + self.sites[site] = w.get_active() + print "self.sites[%s] set to %s" %(site, self.sites[site]) def fillPlayerFrame(self, vbox): for site in self.conf.supported_sites.keys(): @@ -162,6 +186,13 @@ class GuiGraphViewer (threading.Thread): vbox.pack_start(hbox, False, True, 0) hbox.show() self.createSiteLine(hbox, site) + #Get db site id for filtering later + self.cursor.execute(self.sql.query['getSiteId'], (site)) + result = self.db.cursor.fetchall() + if len(result) == 1: + self.siteid[site] = result[0][0] + else: + print "Either 0 or more than one site matched - EEK" def fillDateFrame(self, vbox): # Hat tip to Mika Bostrom - calendar code comes from PokerStats @@ -261,7 +292,8 @@ class GuiGraphViewer (threading.Thread): self.sql=querylist self.conf = config - self.sites = "PokerStars" + self.sites = {} + self.siteid = {} self.heroes = {} # For use in date ranges. diff --git a/pyfpdb/HUD_config.xml.example b/pyfpdb/HUD_config.xml.example index 355c9d2f..bb48c482 100644 --- a/pyfpdb/HUD_config.xml.example +++ b/pyfpdb/HUD_config.xml.example @@ -49,7 +49,7 @@ - + diff --git a/pyfpdb/psnlheparser-mct.tgz b/pyfpdb/psnlheparser-mct.tgz deleted file mode 100644 index b04c80183ec34f4a740acfe9b0f9bebfbcf8e36d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26184 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 z 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 Y^1)&MzteJi9f=iZ()H#E3(k}O1NKQU4FCWD From 2d700820f42ec95edb21213d1e2bbeb65fb46ea7 Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 19 Dec 2008 16:58:24 +0900 Subject: [PATCH 21/23] Add Everleaf to DB init process Should these come from config on startup...? --- pyfpdb/fpdb_db.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyfpdb/fpdb_db.py b/pyfpdb/fpdb_db.py index e51538ee..8c0ab66e 100644 --- a/pyfpdb/fpdb_db.py +++ b/pyfpdb/fpdb_db.py @@ -177,6 +177,7 @@ class fpdb_db: self.cursor.execute("INSERT INTO Settings VALUES (118);") self.cursor.execute("INSERT INTO Sites VALUES (DEFAULT, 'Full Tilt Poker', 'USD');") self.cursor.execute("INSERT INTO Sites VALUES (DEFAULT, 'PokerStars', 'USD');") + self.cursor.execute("INSERT INTO Sites VALUES (DEFAULT, 'Everleaf', 'USD');") self.cursor.execute("INSERT INTO TourneyTypes VALUES (DEFAULT, 1, 0, 0, 0, False);") #end def fillDefaultData From db6a8c5b31adbf665e52e95bbd71fb5fa856686e Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 19 Dec 2008 17:21:58 +0900 Subject: [PATCH 22/23] Grapher: Make date ranges work - MySQL --- pyfpdb/FpdbSQLQueries.py | 2 ++ pyfpdb/GuiGraphViewer.py | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/pyfpdb/FpdbSQLQueries.py b/pyfpdb/FpdbSQLQueries.py index 9afdc28f..4df2fa75 100644 --- a/pyfpdb/FpdbSQLQueries.py +++ b/pyfpdb/FpdbSQLQueries.py @@ -650,6 +650,8 @@ class FpdbSQLQueries: INNER JOIN HandsActions ha ON ha.handPlayerId = hp.id where pl.id in AND pl.siteId in + AND h.handStart > '' + AND h.handStart < '' AND hp.tourneysPlayersId IS NULL GROUP BY hp.handId, hp.winnings, h.handStart, hp.ante ORDER BY h.handStart""" diff --git a/pyfpdb/GuiGraphViewer.py b/pyfpdb/GuiGraphViewer.py index a0a2e326..ae8895ff 100644 --- a/pyfpdb/GuiGraphViewer.py +++ b/pyfpdb/GuiGraphViewer.py @@ -112,6 +112,12 @@ class GuiGraphViewer (threading.Thread): def getRingProfitGraph(self, names, sites): tmp = self.sql.query['getRingProfitAllHandsPlayerIdSite'] # print "DEBUG: getRingProfitGraph" + start_date, end_date = self.__get_dates() + + if start_date == '': + start_date = '1970-01-01' + if end_date == '': + end_date = '2020-12-12' #Buggered if I can find a way to do this 'nicely' take a list of intergers and longs # and turn it into a tuple readale by sql. @@ -125,6 +131,8 @@ class GuiGraphViewer (threading.Thread): #Must be a nicer way to deal with tuples of size 1 ie. (2,) - which makes sql barf tmp = tmp.replace("", nametest) tmp = tmp.replace("", sitetest) + tmp = tmp.replace("", start_date) + tmp = tmp.replace("", end_date) # print "DEBUG: sql query:" # print tmp From 659f0bb508aefd11f29a90a75b49101b6defb4fc Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 19 Dec 2008 17:27:18 +0900 Subject: [PATCH 23/23] Grapher: Fix Postgres to work again --- pyfpdb/FpdbSQLQueries.py | 6 ++++-- pyfpdb/GuiGraphViewer.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pyfpdb/FpdbSQLQueries.py b/pyfpdb/FpdbSQLQueries.py index 4df2fa75..fe170c8c 100644 --- a/pyfpdb/FpdbSQLQueries.py +++ b/pyfpdb/FpdbSQLQueries.py @@ -663,8 +663,10 @@ class FpdbSQLQueries: INNER JOIN Players pl ON hp.playerId = pl.id INNER JOIN Hands h ON h.id = hp.handId INNER JOIN HandsActions ha ON ha.handPlayerId = hp.id - WHERE pl.name = %s - AND pl.siteId = %s + where pl.id in + AND pl.siteId in + AND h.handStart > '' + AND h.handStart < '' AND hp.tourneysPlayersId IS NULL GROUP BY hp.handId, hp.winnings, h.handStart ORDER BY h.handStart""" diff --git a/pyfpdb/GuiGraphViewer.py b/pyfpdb/GuiGraphViewer.py index ae8895ff..8026a2c7 100644 --- a/pyfpdb/GuiGraphViewer.py +++ b/pyfpdb/GuiGraphViewer.py @@ -195,7 +195,7 @@ class GuiGraphViewer (threading.Thread): hbox.show() self.createSiteLine(hbox, site) #Get db site id for filtering later - self.cursor.execute(self.sql.query['getSiteId'], (site)) + self.cursor.execute(self.sql.query['getSiteId'], (site,)) result = self.db.cursor.fetchall() if len(result) == 1: self.siteid[site] = result[0][0]