Merge branch 'master' of git://git.assembla.com/fpdboz

This commit is contained in:
gimick 2010-04-24 12:04:59 +01:00
commit 1cecbec4be
8 changed files with 136 additions and 62 deletions

View File

@ -505,9 +505,14 @@ class DerivedStats():
"""Returns true if player bet/raised the street as their first action""" """Returns true if player bet/raised the street as their first action"""
betOrRaise = False betOrRaise = False
for act in self.hand.actions[street]: for act in self.hand.actions[street]:
if act[0] == player and act[1] in ('bets', 'raises'): if act[0] == player:
betOrRaise = True if act[1] in ('bets', 'raises'):
else: betOrRaise = True
else:
# player found but did not bet or raise as their first action
pass
break break
#else:
# haven't found player's first action yet
return betOrRaise return betOrRaise

View File

@ -33,6 +33,11 @@ log = logging.getLogger("logview")
MAX_LINES = 100000 # max lines to display in window MAX_LINES = 100000 # max lines to display in window
EST_CHARS_PER_LINE = 150 # used to guesstimate number of lines in log file EST_CHARS_PER_LINE = 150 # used to guesstimate number of lines in log file
LOGFILES = [ [ 'Fpdb Errors', 'fpdb-errors.txt', False ] # label, filename, start value
, [ 'Fpdb Log', 'fpdb-log.txt', True ]
, [ 'HUD Errors', 'HUD-errors.txt', False ]
, [ 'HUD Log', 'HUD-log.txt', False ]
]
class GuiLogView: class GuiLogView:
@ -41,7 +46,7 @@ class GuiLogView:
self.main_window = mainwin self.main_window = mainwin
self.closeq = closeq self.closeq = closeq
self.logfile = self.config.log_file # name of logfile self.logfile = os.path.join(self.config.dir_log, LOGFILES[1][1])
self.dia = gtk.Dialog(title="Log Messages" self.dia = gtk.Dialog(title="Log Messages"
,parent=None ,parent=None
,flags=gtk.DIALOG_DESTROY_WITH_PARENT ,flags=gtk.DIALOG_DESTROY_WITH_PARENT
@ -69,10 +74,19 @@ class GuiLogView:
scrolledwindow.add(self.listview) scrolledwindow.add(self.listview)
self.vbox.pack_start(scrolledwindow, expand=True, fill=True, padding=0) self.vbox.pack_start(scrolledwindow, expand=True, fill=True, padding=0)
hb = gtk.HBox(False, 0)
grp = None
for logf in LOGFILES:
rb = gtk.RadioButton(group=grp, label=logf[0], use_underline=True)
if grp is None: grp = rb
rb.set_active(logf[2])
rb.connect('clicked', self.__set_logfile, logf[0])
hb.pack_start(rb, False, False, 3)
refreshbutton = gtk.Button("Refresh") refreshbutton = gtk.Button("Refresh")
refreshbutton.connect("clicked", self.refresh, None) refreshbutton.connect("clicked", self.refresh, None)
self.vbox.pack_start(refreshbutton, False, False, 3) hb.pack_start(refreshbutton, False, False, 3)
refreshbutton.show() refreshbutton.show()
self.vbox.pack_start(hb, False, False, 0)
self.listview.show() self.listview.show()
scrolledwindow.show() scrolledwindow.show()
@ -90,6 +104,14 @@ class GuiLogView:
self.dia.connect('response', self.dialog_response_cb) self.dia.connect('response', self.dialog_response_cb)
def __set_logfile(self, w, file):
#print "w is", w, "file is", file, "active is", w.get_active()
if w.get_active():
for logf in LOGFILES:
if logf[0] == file:
self.logfile = os.path.join(self.config.dir_log, logf[1])
self.refresh(w, file) # params are not used
def dialog_response_cb(self, dialog, response_id): def dialog_response_cb(self, dialog, response_id):
# this is called whether close button is pressed or window is closed # this is called whether close button is pressed or window is closed
self.closeq.put(self.__class__) self.closeq.put(self.__class__)
@ -131,11 +153,14 @@ class GuiLogView:
l = 0 l = 0
for line in open(self.logfile): for line in open(self.logfile):
# eg line: # example line in logfile format:
# 2009-12-02 15:23:21,716 - config DEBUG config logger initialised # 2009-12-02 15:23:21,716 - config DEBUG config logger initialised
l = l + 1 l = l + 1
if l > startline and len(line) > 49: if l > startline:
iter = self.liststore.append( (line[0:23], line[26:32], line[39:46], line[48:].strip(), True) ) if len(line) > 49 and line[23:26] == ' - ' and line[34:39] == ' ':
iter = self.liststore.append( (line[0:23], line[26:32], line[39:46], line[48:].strip(), True) )
else:
iter = self.liststore.append( ('', '', '', line.strip(), True) )
def sortCols(self, col, n): def sortCols(self, col, n):
try: try:

View File

@ -142,7 +142,7 @@ class GuiPlayerStats (threading.Thread):
self.stats_frame = gtk.Frame() self.stats_frame = gtk.Frame()
self.stats_frame.show() self.stats_frame.show()
self.stats_vbox = gtk.VBox(False, 0) self.stats_vbox = gtk.VPaned()
self.stats_vbox.show() self.stats_vbox.show()
self.stats_frame.add(self.stats_vbox) self.stats_frame.add(self.stats_vbox)
# self.fillStatsFrame(self.stats_vbox) # self.fillStatsFrame(self.stats_vbox)
@ -155,12 +155,15 @@ class GuiPlayerStats (threading.Thread):
# make sure Hand column is not displayed # make sure Hand column is not displayed
[x for x in self.columns if x[0] == 'hand'][0][1] = False [x for x in self.columns if x[0] == 'hand'][0][1] = False
self.last_pos = -1
def get_vbox(self): def get_vbox(self):
"""returns the vbox of this thread""" """returns the vbox of this thread"""
return self.main_hbox return self.main_hbox
def refreshStats(self, widget, data): def refreshStats(self, widget, data):
self.last_pos = self.stats_vbox.get_position()
try: self.stats_vbox.destroy() try: self.stats_vbox.destroy()
except AttributeError: pass except AttributeError: pass
self.liststore = [] self.liststore = []
@ -170,6 +173,8 @@ class GuiPlayerStats (threading.Thread):
self.stats_vbox.show() self.stats_vbox.show()
self.stats_frame.add(self.stats_vbox) self.stats_frame.add(self.stats_vbox)
self.fillStatsFrame(self.stats_vbox) self.fillStatsFrame(self.stats_vbox)
if self.last_pos > 0:
self.stats_vbox.set_position(self.last_pos)
def fillStatsFrame(self, vbox): def fillStatsFrame(self, vbox):
sites = self.filters.getSites() sites = self.filters.getSites()
@ -208,6 +213,7 @@ class GuiPlayerStats (threading.Thread):
def createStatsTable(self, vbox, playerids, sitenos, limits, type, seats, groups, dates, games): def createStatsTable(self, vbox, playerids, sitenos, limits, type, seats, groups, dates, games):
starttime = time() starttime = time()
show_detail = True
# Scrolled window for summary table # Scrolled window for summary table
swin = gtk.ScrolledWindow(hadjustment=None, vadjustment=None) swin = gtk.ScrolledWindow(hadjustment=None, vadjustment=None)
@ -224,25 +230,30 @@ class GuiPlayerStats (threading.Thread):
self.addGrid(swin, 'playerDetailedStats', flags, playerids self.addGrid(swin, 'playerDetailedStats', flags, playerids
,sitenos, limits, type, seats, groups, dates, games) ,sitenos, limits, type, seats, groups, dates, games)
# Separator if 'allplayers' in groups and groups['allplayers']:
vbox2 = gtk.VBox(False, 0) # can't currently do this combination so skip detailed table
heading = gtk.Label(self.filterText['handhead']) show_detail = False
heading.show()
vbox2.pack_start(heading, expand=False, padding=3)
# Scrolled window for detailed table (display by hand) if show_detail:
swin = gtk.ScrolledWindow(hadjustment=None, vadjustment=None) # Separator
swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) vbox2 = gtk.VBox(False, 0)
swin.show() heading = gtk.Label(self.filterText['handhead'])
vbox2.pack_start(swin, expand=True, padding=3) heading.show()
vbox.pack2(vbox2) vbox2.pack_start(heading, expand=False, padding=3)
vbox2.show()
# Detailed table # Scrolled window for detailed table (display by hand)
flags[0] = True swin = gtk.ScrolledWindow(hadjustment=None, vadjustment=None)
flags[2] = 1 swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
self.addGrid(swin, 'playerDetailedStats', flags, playerids swin.show()
,sitenos, limits, type, seats, groups, dates, games) vbox2.pack_start(swin, expand=True, padding=3)
vbox.pack2(vbox2)
vbox2.show()
# Detailed table
flags[0] = True
flags[2] = 1
self.addGrid(swin, 'playerDetailedStats', flags, playerids
,sitenos, limits, type, seats, groups, dates, games)
self.db.rollback() self.db.rollback()
print "Stats page displayed in %4.2f seconds" % (time() - starttime) print "Stats page displayed in %4.2f seconds" % (time() - starttime)
@ -421,6 +432,7 @@ class GuiPlayerStats (threading.Thread):
else: else:
treerow.append(' ') treerow.append(' ')
iter = self.liststore[grid].append(treerow) iter = self.liststore[grid].append(treerow)
#print treerow
sqlrow += 1 sqlrow += 1
row += 1 row += 1
vbox.show_all() vbox.show_all()

View File

@ -33,7 +33,7 @@ try:
from matplotlib.backends.backend_gtkagg import NavigationToolbar2GTKAgg as NavigationToolbar from matplotlib.backends.backend_gtkagg import NavigationToolbar2GTKAgg as NavigationToolbar
from matplotlib.finance import candlestick2 from matplotlib.finance import candlestick2
from numpy import diff, nonzero, sum, cumsum, max, min from numpy import diff, nonzero, sum, cumsum, max, min, append
# from matplotlib.dates import DateFormatter, WeekdayLocator, HourLocator, \ # from matplotlib.dates import DateFormatter, WeekdayLocator, HourLocator, \
# DayLocator, MONDAY, timezone # DayLocator, MONDAY, timezone
@ -184,7 +184,7 @@ class GuiSessionViewer (threading.Thread):
sitenos.append(siteids[site]) sitenos.append(siteids[site])
_q = self.sql.query['getPlayerId'] _q = self.sql.query['getPlayerId']
_name = Charset.to_utf8(heroes[site]) _name = Charset.to_utf8(heroes[site])
print 'DEBUG(_name) :: %s' % _name #print 'DEBUG(_name) :: %s' % _name
self.cursor.execute(_q, (_name,)) # arg = tuple self.cursor.execute(_q, (_name,)) # arg = tuple
result = self.db.cursor.fetchall() result = self.db.cursor.fetchall()
if len(result) == 1: if len(result) == 1:
@ -241,6 +241,9 @@ class GuiSessionViewer (threading.Thread):
#end def fillStatsFrame(self, vbox): #end def fillStatsFrame(self, vbox):
def generateDatasets(self, playerids, sitenos, limits, seats): 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)
# Get a list of all handids and their timestampts # Get a list of all handids and their timestampts
#FIXME: Query still need to filter on blind levels #FIXME: Query still need to filter on blind levels
@ -255,23 +258,31 @@ class GuiSessionViewer (threading.Thread):
q = q.replace("<ampersand_s>", "%s") q = q.replace("<ampersand_s>", "%s")
self.db.cursor.execute(q) self.db.cursor.execute(q)
THRESHOLD = 1800
hands = self.db.cursor.fetchall() hands = self.db.cursor.fetchall()
# Take that list and create an array of the time between hands # Take that list and create an array of the time between hands
times = map(lambda x:long(x[0]), hands) times = map(lambda x:long(x[0]), hands)
handids = map(lambda x:int(x[1]), hands) handids = map(lambda x:int(x[1]), hands)
winnings = map(lambda x:float(x[4]), hands) winnings = map(lambda x:float(x[4]), hands)
print "DEBUG: len(times) %s" %(len(times)) #print "DEBUG: len(times) %s" %(len(times))
diffs = diff(times) # This array is the difference in starttime between consecutive hands diffs = diff(times) # This array is the difference in starttime between consecutive hands
index = nonzero(diff(times) > THRESHOLD) # This array represents the indexes into 'times' for start/end times of sessions diffs2 = append(diffs,THRESHOLD + 1) # Append an additional session to the end of the diffs, so the next line
# ie. times[index[0][0]] is the end of the first session # includes an index into the last 'session'
index = nonzero(diffs2 > THRESHOLD) # This array represents the indexes into 'times' for start/end times of sessions
# times[index[0][0]] is the end of the first session,
#print "DEBUG: len(index[0]) %s" %(len(index[0])) #print "DEBUG: len(index[0]) %s" %(len(index[0]))
#print "DEBUG: index %s" %(index) if len(index[0]) > 0:
#print "DEBUG: index[0][0] %s" %(index[0][0]) #print "DEBUG: index[0][0] %s" %(index[0][0])
#print "DEBUG: index %s" %(index)
pass
else:
index = [[0]]
#print "DEBUG: index %s" %(index)
#print "DEBUG: index[0][0] %s" %(index[0][0])
pass
total = 0 total = 0
last_idx = 0 first_idx = 0
lowidx = 0 lowidx = 0
uppidx = 0 uppidx = 0
opens = [] opens = []
@ -281,27 +292,36 @@ class GuiSessionViewer (threading.Thread):
results = [] results = []
cum_sum = cumsum(winnings) cum_sum = cumsum(winnings)
cum_sum = cum_sum/100 cum_sum = cum_sum/100
sid = 1
# Take all results and format them into a list for feeding into gui model. # Take all results and format them into a list for feeding into gui model.
for i in range(len(index[0])): for i in range(len(index[0])):
sid = i # Session id hds = index[0][i] - first_idx + 1 # Number of hands in session
hds = index[0][i] - last_idx # Number of hands in session
if hds > 0: if hds > 0:
stime = strftime("%d/%m/%Y %H:%M", localtime(times[last_idx])) # Formatted start time 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 etime = strftime("%d/%m/%Y %H:%M", localtime(times[index[0][i]])) # Formatted end time
hph = (times[index[0][i]] - times[last_idx])/60 # Hands per hour minutesplayed = (times[index[0][i]] - times[first_idx])/60
won = sum(winnings[last_idx:index[0][i]])/100.0 if minutesplayed == 0:
hwm = max(cum_sum[last_idx:index[0][i]]) minutesplayed = 1
lwm = min(cum_sum[last_idx:index[0][i]]) minutesplayed = minutesplayed + PADDING
#print "DEBUG: range: (%s, %s) - (min, max): (%s, %s)" %(last_idx, index[0][i], hwm, lwm) 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]])
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)
results.append([sid, hds, stime, etime, hph, won]) results.append([sid, hds, stime, etime, hph, won])
opens.append((sum(winnings[:last_idx]))/100) opens.append(open)
closes.append((sum(winnings[:index[0][i]]))/100) closes.append(close)
highs.append(hwm) highs.append(hwm)
lows.append(lwm) lows.append(lwm)
#print "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] - last_idx) total = total + (index[0][i] - first_idx)
last_idx = index[0][i] + 1 first_idx = index[0][i] + 1
sid = sid+1
else:
print "hds <= 0"
return (results, opens, closes, highs, lows) return (results, opens, closes, highs, lows)
@ -330,11 +350,6 @@ class GuiSessionViewer (threading.Thread):
def generateGraph(self, opens, closes, highs, lows): def generateGraph(self, opens, closes, highs, lows):
self.clearGraphData() self.clearGraphData()
#FIXME: Weird - first data entry is crashing this for me
opens = opens[1:]
closes = closes[1:]
highs = highs[1:]
lows = lows[1:]
# print "DEBUG:" # print "DEBUG:"
# print "highs = %s" % highs # print "highs = %s" % highs
# print "lows = %s" % lows # print "lows = %s" % lows

View File

@ -62,7 +62,7 @@ class HandHistoryConverter():
codepage = "cp1252" codepage = "cp1252"
def __init__(self, config, in_path = '-', out_path = '-', follow=False, index=0, autostart=True, starsArchive=False): def __init__(self, config, in_path = '-', out_path = '-', follow=False, index=0, autostart=True, starsArchive=False, ftpArchive=False):
"""\ """\
in_path (default '-' = sys.stdin) in_path (default '-' = sys.stdin)
out_path (default '-' = sys.stdout) out_path (default '-' = sys.stdout)
@ -75,6 +75,7 @@ follow : whether to tail -f the input"""
self.index = index self.index = index
self.starsArchive = starsArchive self.starsArchive = starsArchive
self.ftpArchive = ftpArchive
self.in_path = in_path self.in_path = in_path
self.out_path = out_path self.out_path = out_path
@ -247,6 +248,11 @@ which it expects to find at self.re_TailSplitHands -- see for e.g. Everleaf.py.
m = re.compile('^Hand #\d+', re.MULTILINE) m = re.compile('^Hand #\d+', re.MULTILINE)
self.obs = m.sub('', self.obs) self.obs = m.sub('', self.obs)
if self.ftpArchive == True:
log.debug("Converting ftpArchive format to readable")
m = re.compile('^\*\*\*\*\*\*+\s#\s\d+\s\*\*\*\*\*+$', re.MULTILINE)
self.obs = m.sub('', self.obs)
if self.obs is None or self.obs == "": if self.obs is None or self.obs == "":
log.info("Read no hands.") log.info("Read no hands.")
return [] return []

View File

@ -50,8 +50,8 @@ class PokerStars(HandHistoryConverter):
(?P<BUYIN>([%(LS)s\+\d\.]+\s?(?P<TOUR_ISO>%(LEGAL_ISO)s)?)|Freeroll)\s+)? (?P<BUYIN>([%(LS)s\+\d\.]+\s?(?P<TOUR_ISO>%(LEGAL_ISO)s)?)|Freeroll)\s+)?
# close paren of tournament info # close paren of tournament info
(?P<MIXED>HORSE|8\-Game|HOSE)?\s?\(? (?P<MIXED>HORSE|8\-Game|HOSE)?\s?\(?
(?P<GAME>Hold\'em|Razz|7\sCard\sStud|7\sCard\sStud\sHi/Lo|Omaha|Omaha\sHi/Lo|Badugi|Triple\sDraw\s2\-7\sLowball|5\sCard\sDraw)\s (?P<GAME>Hold\'em|Razz|RAZZ|7\sCard\sStud|7\sCard\sStud\sHi/Lo|Omaha|Omaha\sHi/Lo|Badugi|Triple\sDraw\s2\-7\sLowball|5\sCard\sDraw)\s
(?P<LIMIT>No\sLimit|Limit|Pot\sLimit)\)?,?\s (?P<LIMIT>No\sLimit|Limit|LIMIT|Pot\sLimit)\)?,?\s
(-\sLevel\s(?P<LEVEL>[IVXLC]+)\s)? (-\sLevel\s(?P<LEVEL>[IVXLC]+)\s)?
\(? # open paren of the stakes \(? # open paren of the stakes
(?P<CURRENCY>%(LS)s|)? (?P<CURRENCY>%(LS)s|)?
@ -135,25 +135,29 @@ class PokerStars(HandHistoryConverter):
info = {} info = {}
m = self.re_GameInfo.search(handText) m = self.re_GameInfo.search(handText)
if not m: if not m:
print "DEBUG: determineGameType(): did not match" tmp = handText[0:100]
return None log.error("determineGameType: Unable to recognise gametype from: '%s'" % tmp)
log.error("determineGameType: Raising FpdbParseError")
raise FpdbParseError
mg = m.groupdict() mg = m.groupdict()
# translations from captured groups to fpdb info strings # translations from captured groups to fpdb info strings
Lim_Blinds = { '0.04': ('0.01', '0.02'), '0.10': ('0.02', '0.05'), '0.20': ('0.05', '0.10'), Lim_Blinds = { '0.04': ('0.01', '0.02'), '0.10': ('0.02', '0.05'), '0.20': ('0.05', '0.10'),
'0.50': ('0.10', '0.25'), '1.00': ('0.25', '0.50'), '2.00': ('0.50', '1.00'), '0.50': ('0.10', '0.25'), '1.00': ('0.25', '0.50'), '2.00': ('0.50', '1.00'),
'2': ('0.50', '1.00'), '4': ('1.00', '2.00'), '6': ('1.00', '3.00'),
'4.00': ('1.00', '2.00'), '6.00': ('1.00', '3.00'), '10.00': ('2.00', '5.00'), '4.00': ('1.00', '2.00'), '6.00': ('1.00', '3.00'), '10.00': ('2.00', '5.00'),
'20.00': ('5.00', '10.00'), '30.00': ('10.00', '15.00'), '60.00': ('15.00', '30.00'), '20.00': ('5.00', '10.00'), '30.00': ('10.00', '15.00'), '60.00': ('15.00', '30.00'),
'100.00': ('25.00', '50.00'),'200.00': ('50.00', '100.00'),'400.00': ('100.00', '200.00'), '100.00': ('25.00', '50.00'),'200.00': ('50.00', '100.00'),'400.00': ('100.00', '200.00'),
'1000.00': ('250.00', '500.00')} '1000.00': ('250.00', '500.00')}
limits = { 'No Limit':'nl', 'Pot Limit':'pl', 'Limit':'fl' } limits = { 'No Limit':'nl', 'Pot Limit':'pl', 'Limit':'fl', 'LIMIT':'fl' }
games = { # base, category games = { # base, category
"Hold'em" : ('hold','holdem'), "Hold'em" : ('hold','holdem'),
'Omaha' : ('hold','omahahi'), 'Omaha' : ('hold','omahahi'),
'Omaha Hi/Lo' : ('hold','omahahilo'), 'Omaha Hi/Lo' : ('hold','omahahilo'),
'Razz' : ('stud','razz'), 'Razz' : ('stud','razz'),
'RAZZ' : ('stud','razz'),
'7 Card Stud' : ('stud','studhi'), '7 Card Stud' : ('stud','studhi'),
'7 Card Stud Hi/Lo' : ('stud','studhilo'), '7 Card Stud Hi/Lo' : ('stud','studhilo'),
'Badugi' : ('draw','badugi'), 'Badugi' : ('draw','badugi'),
@ -182,8 +186,13 @@ class PokerStars(HandHistoryConverter):
info['type'] = 'tour' info['type'] = 'tour'
if info['limitType'] == 'fl' and info['bb'] is not None and info['type'] == 'ring' and info['base'] != 'stud': if info['limitType'] == 'fl' and info['bb'] is not None and info['type'] == 'ring' and info['base'] != 'stud':
info['sb'] = Lim_Blinds[mg['BB']][0] try:
info['bb'] = Lim_Blinds[mg['BB']][1] info['sb'] = Lim_Blinds[mg['BB']][0]
info['bb'] = Lim_Blinds[mg['BB']][1]
except KeyError:
log.error("determineGameType: Lim_Blinds has no lookup for '%s'" % mg['BB'])
log.error("determineGameType: Raising FpdbParseError")
raise FpdbParseError
# NB: SB, BB must be interpreted as blinds or bets depending on limit type. # NB: SB, BB must be interpreted as blinds or bets depending on limit type.
return info return info

View File

@ -2101,6 +2101,7 @@ class Sql:
,plposition ,plposition
,upper(gt.limitType) ,upper(gt.limitType)
,s.name ,s.name
having 1 = 1 <havingclause>
order by hp.playerId order by hp.playerId
,gt.base ,gt.base
,gt.category ,gt.category

View File

@ -468,6 +468,7 @@ class Importer:
errors = getattr(hhc, 'numErrors') errors = getattr(hhc, 'numErrors')
stored = getattr(hhc, 'numHands') stored = getattr(hhc, 'numHands')
stored -= duplicates stored -= duplicates
stored -= errors
else: else:
# conversion didn't work # conversion didn't work
# TODO: appropriate response? # TODO: appropriate response?