From 68344b5ec06e347400d6e8660e4d0416bfeaa59a Mon Sep 17 00:00:00 2001 From: Worros Date: Thu, 6 Nov 2008 18:39:49 +1300 Subject: [PATCH 001/100] Modified Auto import GUI to allow n-sites based on config file. Added filter option to display. Added option to enable or disable sites in Config --- pyfpdb/Configuration.py | 8 +- pyfpdb/GuiAutoImport.py | 185 +++++++++++++++++++--------------------- 2 files changed, 93 insertions(+), 100 deletions(-) diff --git a/pyfpdb/Configuration.py b/pyfpdb/Configuration.py index 5e599d4b..adc950ca 100755 --- a/pyfpdb/Configuration.py +++ b/pyfpdb/Configuration.py @@ -57,6 +57,7 @@ class Site: self.hudbgcolor = node.getAttribute("bgcolor") self.hudfgcolor = node.getAttribute("fgcolor") self.converter = node.getAttribute("converter") + self.enabled = node.getAttribute("enabled") self.layout = {} for layout_node in node.getElementsByTagName('layout'): @@ -494,13 +495,14 @@ class Config: parms["site_path"] = self.supported_sites[site].site_path parms["table_finder"] = self.supported_sites[site].table_finder parms["HH_path"] = self.supported_sites[site].HH_path + parms["enabled"] = self.supported_sites[site].enabled return parms def set_site_parameters(self, site_name, converter = None, decoder = None, hudbgcolor = None, hudfgcolor = None, hudopacity = None, screen_name = None, site_path = None, table_finder = None, - HH_path = None): + HH_path = None, enabled = None): """Sets the specified site parameters for the specified site.""" site_node = self.get_site_node(site_name) if not db_node == None: @@ -513,6 +515,7 @@ class Config: if not site_path == None: site_node.setAttribute("site_path", site_path) if not table_finder == None: site_node.setAttribute("table_finder", table_finder) if not HH_path == None: site_node.setAttribute("HH_path", HH_path) + if not enabled == None: site_node.setAttribute("enabled", enabled) if self.supported_databases.has_key(db_name): if not converter == None: self.supported_sites[site].converter = converter @@ -524,6 +527,7 @@ class Config: if not site_path == None: self.supported_sites[site].site_path = site_path if not table_finder == None: self.supported_sites[site].table_finder = table_finder if not HH_path == None: self.supported_sites[site].HH_path = HH_path + if not enabled == None: self.supported_sites[site].enabled = enabled return if __name__== "__main__": @@ -575,4 +579,4 @@ if __name__== "__main__": print "locs = ", c.get_locations("PokerStars", 8) for site in c.supported_sites.keys(): print "site = ", site, - print c.get_site_parameters(site) \ No newline at end of file + print c.get_site_parameters(site) diff --git a/pyfpdb/GuiAutoImport.py b/pyfpdb/GuiAutoImport.py index 916170f0..f8f4d8c1 100644 --- a/pyfpdb/GuiAutoImport.py +++ b/pyfpdb/GuiAutoImport.py @@ -26,32 +26,56 @@ import os import time import fpdb_import + class GuiAutoImport (threading.Thread): - def starsBrowseClicked(self, widget, data): - """runs when user clicks browse on auto import tab""" - #print "start of GuiAutoImport.starsBrowseClicked" - current_path=self.starsDirPath.get_text() + def __init__(self, settings, config): + """Constructor for GuiAutoImport""" + self.settings=settings + self.config=config + + self.input_settings = {} + + self.importer = fpdb_import.Importer(self,self.settings) + self.importer.setCallHud(True) + self.importer.setMinPrint(30) + self.importer.setQuiet(False) + self.importer.setFailOnError(False) + self.importer.setHandCount(0) +# self.importer.setWatchTime() - dia_chooser = gtk.FileChooserDialog(title="Please choose the path that you want to auto import", - action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, - buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK)) - #dia_chooser.set_current_folder(pathname) - dia_chooser.set_filename(current_path) - #dia_chooser.set_select_multiple(select_multiple) #not in tv, but want this in bulk import + self.server=settings['db-host'] + self.user=settings['db-user'] + self.password=settings['db-password'] + self.database=settings['db-databaseName'] - response = dia_chooser.run() - if response == gtk.RESPONSE_OK: - #print dia_chooser.get_filename(), 'selected' - self.starsDirPath.set_text(dia_chooser.get_filename()) - elif response == gtk.RESPONSE_CANCEL: - print 'Closed, no files selected' - dia_chooser.destroy() - #end def GuiAutoImport.starsBrowseClicked + self.mainVBox=gtk.VBox(False,1) + self.mainVBox.show() - def tiltBrowseClicked(self, widget, data): - """runs when user clicks browse on auto import tab""" - #print "start of GuiAutoImport.tiltBrowseClicked" - current_path=self.tiltDirPath.get_text() + self.settingsHBox = gtk.HBox(False, 0) + self.mainVBox.pack_start(self.settingsHBox, False, True, 0) + self.settingsHBox.show() + + self.intervalLabel = gtk.Label("Interval (ie. break) between imports in seconds:") + self.settingsHBox.pack_start(self.intervalLabel) + self.intervalLabel.show() + + self.intervalEntry=gtk.Entry() + self.intervalEntry.set_text(str(self.settings['hud-defaultInterval'])) + self.settingsHBox.pack_start(self.intervalEntry) + self.intervalEntry.show() + + self.addSites(self.mainVBox) + + self.startButton=gtk.Button("Start Autoimport") + self.startButton.connect("clicked", self.startClicked, "start clicked") + self.mainVBox.add(self.startButton) + self.startButton.show() + + + #end of GuiAutoImport.__init__ + def browseClicked(self, widget, data): + """runs when user clicks one of the browse buttons in the auto import tab""" + current_path=data[1].get_text() dia_chooser = gtk.FileChooserDialog(title="Please choose the path that you want to auto import", action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, @@ -63,11 +87,12 @@ class GuiAutoImport (threading.Thread): response = dia_chooser.run() if response == gtk.RESPONSE_OK: #print dia_chooser.get_filename(), 'selected' - self.tiltDirPath.set_text(dia_chooser.get_filename()) + data[1].set_text(dia_chooser.get_filename()) + self.input_settings[data[0]][0] = dia_chooser.get_filename() elif response == gtk.RESPONSE_CANCEL: print 'Closed, no files selected' dia_chooser.destroy() - #end def GuiAutoImport.tiltBrowseClicked + #end def GuiAutoImport.browseClicked def do_import(self): """Callback for timer to do an import iteration.""" @@ -106,12 +131,10 @@ class GuiAutoImport (threading.Thread): # command = command + " %s" % (self.database) # print "command = ", command # self.pipe_to_hud = os.popen(command, 'w') - self.starspath=self.starsDirPath.get_text() - self.tiltpath=self.tiltDirPath.get_text() -# Add directory to importer object. - self.importer.addImportDirectory(self.starspath, True, "PokerStars", "passthrough") - self.importer.addImportDirectory(self.tiltpath, True, "FullTilt", "passthrough") +# Add directories to importer object. + for site in self.input_settings: + self.importer.addImportDirectory(self.input_settings[site][0], True, site, self.input_settings[site][1]) self.do_import() interval=int(self.intervalEntry.get_text()) @@ -122,79 +145,45 @@ class GuiAutoImport (threading.Thread): """returns the vbox of this thread""" return self.mainVBox #end def get_vbox - - def __init__(self, settings, config, debug=True): - """Constructor for GuiAutoImport""" - self.settings=settings - self.config=config - self.importer = fpdb_import.Importer(self,self.settings) - self.importer.setCallHud(True) - self.importer.setMinPrint(30) - self.importer.setQuiet(False) - self.importer.setFailOnError(False) - self.importer.setHandCount(0) -# self.importer.setWatchTime() - - self.server=settings['db-host'] - self.user=settings['db-user'] - self.password=settings['db-password'] - self.database=settings['db-databaseName'] - - self.mainVBox=gtk.VBox(False,1) - self.mainVBox.show() - - self.settingsHBox = gtk.HBox(False, 0) - self.mainVBox.pack_start(self.settingsHBox, False, True, 0) - self.settingsHBox.show() - - self.intervalLabel = gtk.Label("Interval (ie. break) between imports in seconds:") - self.settingsHBox.pack_start(self.intervalLabel) - self.intervalLabel.show() - - self.intervalEntry=gtk.Entry() - self.intervalEntry.set_text(str(self.settings['hud-defaultInterval'])) - self.settingsHBox.pack_start(self.intervalEntry) - self.intervalEntry.show() - - self.pathHBox = gtk.HBox(False, 0) - self.mainVBox.pack_start(self.pathHBox, False, True, 0) - self.pathHBox.show() - - self.pathStarsLabel = gtk.Label("Path to PokerStars auto-import:") - self.pathHBox.pack_start(self.pathStarsLabel, False, False, 0) - self.pathStarsLabel.show() - - self.starsDirPath=gtk.Entry() - paths = self.config.get_default_paths("PokerStars") - self.starsDirPath.set_text(paths['hud-defaultPath']) - self.pathHBox.pack_start(self.starsDirPath, False, True, 0) - self.starsDirPath.show() - self.browseButton=gtk.Button("Browse...") - self.browseButton.connect("clicked", self.starsBrowseClicked, "Browse clicked") - self.pathHBox.pack_start(self.browseButton, False, False, 0) - self.browseButton.show() - - self.pathTiltLabel = gtk.Label("Path to Full Tilt auto-import:") - self.pathHBox.pack_start(self.pathTiltLabel, False, False, 0) - self.pathTiltLabel.show() + #Create the site line given required info and setup callbacks + #enabling and disabling sites from this interface not possible + #expects a box to layout the line horizontally + def createSiteLine(self, hbox, site, iconpath, hhpath, filter_name, active = True): + label = gtk.Label(site + " auto-import:") + hbox.pack_start(label, False, False, 0) + label.show() - self.tiltDirPath=gtk.Entry() - paths = self.config.get_default_paths("Full Tilt") - self.tiltDirPath.set_text(paths['hud-defaultPath']) - self.pathHBox.pack_start(self.tiltDirPath, False, True, 0) - self.tiltDirPath.show() + dirPath=gtk.Entry() + dirPath.set_text(hhpath) + hbox.pack_start(dirPath, False, True, 0) + dirPath.show() - self.browseButton=gtk.Button("Browse...") - self.browseButton.connect("clicked", self.tiltBrowseClicked, "Browse clicked") - self.pathHBox.pack_start(self.browseButton, False, False, 0) - self.browseButton.show() + browseButton=gtk.Button("Browse...") + browseButton.connect("clicked", self.browseClicked, [site] + [dirPath]) + hbox.pack_start(browseButton, False, False, 0) + browseButton.show() + + label = gtk.Label(site + " filter:") + hbox.pack_start(label, False, False, 0) + label.show() + + filter=gtk.Entry() + filter.set_text(filter_name) + hbox.pack_start(filter, False, True, 0) + filter.show() + + def addSites(self, vbox): + for site in self.config.supported_sites.keys(): + pathHBox = gtk.HBox(False, 0) + vbox.pack_start(pathHBox, False, True, 0) + pathHBox.show() + + paths = self.config.get_default_paths(site) + params = self.config.get_site_parameters(site) + self.createSiteLine(pathHBox, site, False, paths['hud-defaultPath'], params['converter'], params['enabled']) + self.input_settings[site] = [paths['hud-defaultPath']] + [params['converter']] - self.startButton=gtk.Button("Start Autoimport") - self.startButton.connect("clicked", self.startClicked, "start clicked") - self.mainVBox.add(self.startButton) - self.startButton.show() - #end of GuiAutoImport.__init__ if __name__== "__main__": def destroy(*args): # call back for terminating the main eventloop gtk.main_quit() From 2b9eed8958ab0ab32b35a78ec65e7220169aadbb Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 7 Nov 2008 18:12:48 +1300 Subject: [PATCH 002/100] Change db access function to always get the latest committed transactions. Fixes graph updating, so i made the label a bit prettier, and a bit better behaved. --- pyfpdb/GuiGraphViewer.py | 13 ++++++++----- pyfpdb/fpdb_db.py | 1 + 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pyfpdb/GuiGraphViewer.py b/pyfpdb/GuiGraphViewer.py index 7385e9fe..49a00d20 100644 --- a/pyfpdb/GuiGraphViewer.py +++ b/pyfpdb/GuiGraphViewer.py @@ -64,19 +64,22 @@ class GuiGraphViewer (threading.Thread): #Set graph properties self.ax = self.fig.add_subplot(111) - # + #Get graph data from DB + line = self.getRingProfitGraph(name, site) + self.ax.set_title("Profit graph for ring games") #Set axis labels and grid overlay properites self.ax.set_xlabel("Hands", fontsize = 12) self.ax.set_ylabel("$", fontsize = 12) self.ax.grid(color='g', linestyle=':', linewidth=0.2) - text = "All Hands, " + sitename + str(name) + text = "All Hands, " + sitename + str(name) + "\nProfit: $" + str(line[-1]) + "\nTotal Hands: " + str(len(line)) - self.ax.annotate (text, (61,25), xytext =(0.1, 0.9) , textcoords ="axes fraction" ,) + self.ax.annotate(text, xy=(10, -10), + xycoords='axes points', + horizontalalignment='left', verticalalignment='top', + fontsize=10) - #Get graph data from DB - line = self.getRingProfitGraph(name, site) #Draw plot self.ax.plot(line,) diff --git a/pyfpdb/fpdb_db.py b/pyfpdb/fpdb_db.py index 92a5ed86..b5fd53c9 100755 --- a/pyfpdb/fpdb_db.py +++ b/pyfpdb/fpdb_db.py @@ -47,6 +47,7 @@ class fpdb_db: else: raise fpdb_simple.FpdbError("unrecognised database backend:"+backend) self.cursor=self.db.cursor() + self.cursor.execute('SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED') # Set up query dictionary as early in the connection process as we can. self.sql = FpdbSQLQueries.FpdbSQLQueries(self.get_backend_name()) self.wrongDbVersion=False From 6490ee58829048ab6291bc4e07c361de8015f83f Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 7 Nov 2008 18:35:30 +1300 Subject: [PATCH 003/100] Updated Grapher screenshot --- website/img/Screenshot-Graph-v137.png | Bin 42998 -> 49556 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/website/img/Screenshot-Graph-v137.png b/website/img/Screenshot-Graph-v137.png index ae281d711e51293226a8705446bba46d6e6bcb80..5faaf7581b2275feeb0c0daedb25a45e0921dbdc 100644 GIT binary patch literal 49556 zcmeFZWmr~Q)HeFiC@s<{prmwnm!#62(%p?TQX;8zcT2aVAl+Tk-3{MlyZ8H^@4C+a z^XDutA9Sr})tqCFG4A^wgHQ!I36$sf&mj;9ij<`2dk6%U90Gv}L4*f;_E>l@CcM_tI$EQE(&3Q3k&of&|bq5Fpwe#sOR2(fT0oaM)C?zX~FpY!& zM~-%67^et!;W>(_If~d=Ss7b9LPYG1^&O3kNL|bvO-UuBWEE6Dq2fRwq!1}lAtl$@ zy#*(&m)j%=M?;mY`K&agY637~6=Fi=U&>GSIM?>-swx^A<;3_Jld38z);f7LquJ3h z^uH~UY^Bon!_(0oyWM_I*z3ex`sTIq&BVk+%(f6Ax)dti0#;}W7k%3xv60j_^qDqIfEZ`j%B^59lPYNd{B?+Ow zC(X`gG!Xr}#R`#yh(bikI=wWA1?hGUG9DAEQtOdf$Zun%D-23 ze5>(;;@v0_XSq-5ZwZRg_9PB{-18m<*oSKMv!y9kSSV`h7pS7>82@b4<)+6k#%-1( z`r>FUSQjj&O7&MVyKp(veMcG3f@s}Vy(+jgg*KjX;X-$B_t_z`pNs__STlCtY&ZAH7*{Wbb=HD`5(UW z(HXAu(%>RhiIo`^le+o|!L{yJj2sBUBA+^fHCNXApEGih`0h*uX9)R^W5n6a`T1jr z#$s}Bct?zBUtDIu%w)i}^-oRJfgk+lH`&kdA~e0MEZ8&mdgYH2 ztWIj`(Mj>?c_&3{X+uLpIU|Qe;B&0uY6nU+4UIov`NO_=^e6smXnuy)v%~i3YIBgt z^X>{GH#e6`x0KNRVU`Ha%Bsqk=h$%0cK^m{Z=seX!{zW56B7pZ#zwwc1zCgR0fhEl z(ca8te&v$)D3g$AEKE1qm#<`|cXH5zu!u|5S@mzz4sSglQ&|2)`DSNtt*-Y!W~f)? za611+5)5&ii6C$qDVgIe*5SidF6!e!JI3J(q1r^`u49<1HeBjETj@Y3)@dZxthMQB zygXzy6>LS;A~0>G4@TP)d;k8$&&4L8;Ux(slSR*DPPZ3p(A{l_m_J-hK~WJ|fNTI-G9IxM2zOB1T5A*l!al zT?j}XbGKV;s_X^4`!9RA59eA*-hSuk*it0_k{lbrcY9%Y8gJ!#u~+Z|RUrQSWW5Mi zz>L|lxWuRLID=~FH*xJA%nQW>k}%0@cgjy685YI3xkrB}|H=~*u?gHJ;&B(vy1q-) zaX%gFr%Y)N`9Y5L_&~dSv2Q;-<`HsrrPt)nhMy-7)7=eBTTnCrX>~1_^p!rW^%8q9 zX%ifT*A?<-XsFB#T@jV1AbafNHM=!D8UyGDK{;cbNB{Yb?ya@?>{sCo{w! z#{F^+3dYOJa;Y9kv({Q-Q;z6Qyu7Y1NyNPuMx8edF`s*EfrFHm79qFG{-^9eMwV)| z@W6~9zKPRnj@uWsXO2!4`V07p-@kvXdw86GCbryw;(D`(M~Y}mzZs4#o0R_kwd%Kh zRFdhU6aL$pf#G(T3a49C;Jbd7+UlR(mvCtHCB!6^l6hPbwG~cSq-SOMLtmd}xu2U~ zRePx75)kw+AS{g%vm2p1@6AVWtgJAyu!w|xlpXmI>N6WTc#OFn{~_*9x5@PxBwuw1 z3I66Le^(g+7)D1J4ny3*<8MNeoyiZ1EtuVKl8@}eQxd!JZ_C~K*#n)XYYY(}=~hXT z&v!EhnX(3NKa6xhf zf!^%}v#k?Wh9DFa8gXLVgz&emKWhx5T=Slc;4I{Bnf*vEpj)WXWhLgTNOmys!r zTdpKKIplyBwCVu<5e9@n)mkZab_S3|g=vA|l}gJkR5_w3SQWx!WmF z=JH20Rd{XECnNJm$Pvx;YjXg1fqg%cGDT&bLh=K z1$x=p*==KEnBeHQ!pGtn2MkPtFkhyuXL(pXoD_o+5lwmZ0kBNXs{alYfW(i!>z0=#dL@wQ$-!?_aM zQYVA1?a?liR*{Nq{qEwk<}fg;9o^md=eusyGV&>g;whJT+&xpD{DUhDCd|k4bx@Et z2H_!eT1^xSu2j-IMY9BCWc!g~i0bwx0i@X=94K(5r8~ClY_3glF6@sFP;L!FU3uO& ztX%q|FU1#pkLvTs08vg_nk$yZ35lIOP0u0;5fc+B@mp70Z$8~;>}CW92dLNY7C)(J zp?>^0MT%H}R>#T;D~~T4n{~xzmilqA-K#Oj$7N@j7#<#ev}S)fgA^i|G+6?ILsV|W zJ+NC9LS9GQfh2ZCHzH;FAN-ynBFi@@5Xj|hb@nORV~YDbt*Vc*>1BDr8w2=;LoHs2 z-1e{~n$=&Vc86C;>2$9tEv>8=t@m(W+Lfyo(QkJNr67qpIIuw4=4va<;_q>N`1rKX zwidNp*4?Z7hd3je`y)LRt3-^L2OCB2aN;cQQi&bH!s%YM)KmS>v8dfiJ9E-57^SH;NuzAX$VN3r2dB_IemD}j#lUAgkAu=WUo z9DP$)w2Ta2Nk_EHLmqP0m%8$t1#^AxVnV1IXIm{iX5sG|oqvx{OKv#H$RI<~RY_(x zIYh<8-c5ZlCK7AWp5EHpYU>+)UZTk^0bU_zuDd(en~y%Ng6aL&nCPYPGmDlAvr%2l zI$}T4^moCibOl8uF}ZG$fW;NpcD*^n^WL4nbmHT)-8oaI)2x5)urbh6HTDYm75+XVBB|D3GPypH$@uGUZuvC6o(YR7eG5Yvc^8+Qb(PIT z#m)PIzT@+KCnKYmkhZ>`THHgkKgs@g>6S!_WoHYLNr{gp4B^YF78fOPQyo?Be|Vi1 zPS-EVzq9d{fu!CaW4oee@i{`U2L$4A+vs|nNw>FGy3!R^N%j5@JtSQ+y0OkKi!Dl$ ztnWiH-CGO{yigPimGbSimdC!nfH~IiA*0vR$ z$ZG|^@m34Aq7;{{Ps&C1I|IJ`M8ci-pc^}eI{a2$=gb9KFX zk)n|NoS(cI*Lv?TEBc1x_*5(l{`~l^3u*p=^5rgv_0M^@uyXzVv~Px6 zsKG(O?a@n8#2m#Mx}$3cyt;9RFt7TXd6G6Jt=a@0y%PTPKY%ml{C#MC@ zZ|%X03VYsBQczGV&z=+D3b++&>>7zLa65r%x*!n+N#w=e5*oyGI$h_xlXf2$x!xIK zh@&H;;e{6A<4yBjF}=xqduE*$_dtQE(vDQGTg3`Lq}d~SUYd!W{^v*&BnSBmclcuf-W`W!>c$;sJLAJP#ijCAsU;6xm)j5zbt z!g%%iJacKf7g0(|_h>5jEzKrI-2;PXNLWX39cS@eITM?TcZ+!(@L(y)$q-1MTv~eq z2u3%1>JULi${yr>8)d_+U+!|>*?rb zB(_c}F}_cliMq$;X2K2ewFTxXzjx=;ZM19o`)B3IGLuCpT+Am6Q+`r6KgA@6{_&zA z4i6XWj>VKYZ`^DJ4wBa{;+_x46cmud#hjLS$%EyQta#T;xqv+Re$rPf^oz|ft80cB z82Y}wl7@r=i!VY$U5>9$4T%#(V~g01PXj--@T0=rM+`3b1g5s|_r=l&evG)E(4b`T zHk-qz_JeIV88le>B>kG zrv(~0&&9)^35trs{(hdAAu%<0ba_7F`ateY63({EzFH=$S?)Pup7=ent>hyLxRBKT zO>TDfgJYr^-J~g>58?dP@nE<(3V~jK4Uy~B76vI|c}ceOGKmIwsvlD>W%qi@h?Gi3@@kC4cR)C;`|X-8_!0_jSTmf zqrou4L?Hi{Qj#pnMk{|AdQ~pcX}-*f(0 z;1foWz84D}T|Yj&_fd2C=Trj{UAUQ@!H^fmeQQe|P5JuC4pg6y|3xFc|DM6+mXz@Q`?|j_UyImkMCqG(gU8BQ zDEaWzlzi2mh!&yu_B2+~?DF^9#ex7A61|8(ly2nlVi% zHF-NPIPCttdG;@hcTQyvl?_oP(LChg;o-J4SI9f=lhK1b-m5ReBO)fGB`hk}oWkJy zlydA}1FVj-KUXbPW!XPTl)ts=2Yr3Axg&o>vLNf_@EU}w?W)ZuoQBU~5wV~2=$E3s zQoKL{y(ae02+ds0z8_ZL#!t`Ubuq>quJTYVzH^B9eWzSBm983P&Ai>@dHtgK7F95i zhHY7^;(5d64GI$z)9Cu^{)XPWX5%?$*U3xqndfoC*a1D7iu!OU~T8dcbA^L&ae@7cl$Sx|LbD5Mj7uD^yb zt-Wch)&O=~6k4%H*+<8qJ_+|}m!oZ=20_2Yy}QiUrrx?x4HHixH&aQNIW#g4u?sI@v{l?EmUN4bcE6LTnj zOrljSU0F`m=5%6392pr=D|h|oc-;gS29|NCVKCb2Rt5~B2WF%jQbMn#&+Y}$-aPwe`%4O@^R+N5b4%0VOVHEFzq~8r7F*#>WFY zt_77oSQFYh{7XYgt+vnjJH14YDo6?Et1zcZ7ZmCZQ}eSI-G zF5+8TAz*z{7Hc%CxT}grS*K)Vkpp~p^stbx-b{mxd=@=44%-2b)>e;%Gltg%)r}DD zR}#S%9*%r$Q9D@Yao7?h@!6Le6IvlZ{Vg&I3SdbQI(vM6cG_hbZv-wjQ&tTmD*CjI zjuOVlZ{V`*pb>Jqsx{oTWsq*$)6yb^6R{B@puzpJ6fo~(R8vbV;&jhh9B(vnDSoEK zz+xl241cyaB5Uo<igG2_@+3vH!i!bDahPb&EzA@aYp2 zDqPOw`bnIHZP|VV4xS9xtl$lK6;S zhNXwtu>$c@aYO>mt*YX}`sV3UlYj7I_tr{5PjFx$mv{tj?xelbEXums{~GpcY}H}- zRhk1_eayc@dV7o4$Re!DqWc14I8)t_PNxAH0S#g0;XYWc#q|L#4q`pu6sYcivwYBu zASo#c@hwAZe)(s^Oog=dtU4nD>T664+gVrK;Rz!xX-sgFo4|&JjjGzs?BXEm2-C>u z6jMcoZFy}>i*tFch>T_U?+f-x75klxZw!Bw$~5rEq7h#uFl32W7jk+WK>r?io-XWW zQg0U-9xh7$_>tMnbB>a&NVVfn0n2*?a#Z5(@B+*V&vpDCW)p-QHVZ$`s0(m@e;$=A z8w|tw-BpQIRBpQc{7=8hXD0GTL2dacx@$irC8d|YfB$CoBrI%*$XpYix%j-AA?;bk ztW)@>EdOAeWU1g30275eTcK@>#-D)`8l`fL85$b@IA5-*j$s1(&Bvh`IXIytTAj@L3w$S6rSpyBSk|dQ$c~P?d|^gMJNbNFmg|?K+w;HW^5T*8FZ4zXK@ze z@xLgWTPNPe6rR6li#ptF_QZnkAwz}us+M)=ndwt&Tbt>>s=DjyS1o6(9hPox*&eGx zc+hG_ohVZ2`tC$9d^%-qHu`um+t>Gej>E>cBtrK4y12M_d2MY^MtT+?`b3EUov2>( zyhk3kqrDv}C`cR~w#zJAwe@5@xx(|8FruF?%jTfr+4M6`pPM@I%YBYHmxCb9fgL=4 zRON;LF{alo>Ua#wC#p&=+S*JnUhsDe52Hhl4i8@|K-Kjj|XP|da-85f=7MXWp~eqWZ#fp#PBp$ zU2>Caz@kGC7%Wr1r85jF+{wTAeH2T~jj@)Y0ih_wSk0W#I4E zK@6@J(w09Xx#Q7k@_a@qA@MtvUyS^=MyXyJJyevDnOPJA0ikZnzGIO{Q_svSEh(K{a-wJAtB2J(unQvu)ej_K8 za>LKa#wL`2x^H(d4;Pp4Gu%N7wIx zp=knk5)bK)*L`)5Lu~MVRAIbcH^_WG1R-laheN~mvK#*-RGk*f;kqycxcGVBaE^EI z+%E{e)&rQ*sH9SlerfY~hjew=pHy&SG$ya8JVP?0FK}1fu(@&bSX^gLlR9QHAlAyy zcl?_A*_g?Y$bCa9^C63IKifB)e-=YoXypSw=ZDr@yT^+OLMJ!|&BfoblpZ?u`85Le zo5~40J!ECvYy_$UonwQg=uHG15(WljAaQJO^Y$_tRjJX6ar@^w$KR=T%d%d2%dR;H z^IlT@vkwEKU|_hHVt^N`HMd?;(s#nXccP0aM$}gCh~tg z8Tc@#Rje^+=yzmkc=hiU6~(s-E8hcF1UPX#Fv6Mh^VGu=fvKs)w@~1O?BfT~@8?QV zd)sRgoGv#MWMmz$q~aMHz@hpOHC1HIIpO^MmB7b1`hIT+xTIN=z%hA_KgLiRU)KuE ztpb3nc1JW!anvZ`6!bN-AFk+;f*bLeeIEgr?{G-T?Yj<*m;W|+k zB^p zW~@X734r>d-*by*&G!PpE!MUF-sxk3s|hTOn5bZ&d9~f{2^X-aD1C5;X%|v5*|ii5 z(d+}`XCi`~mwXte3#>o?s|CQALnrHdxriqei=b3|5Sf%PSlArC4~22;B_GSv+1YKd zzT?E{vO-3dm8oxO6ktDSJknxCJMLW6&K?l*BNEKtdP@H@l;)lpV)Tl8(#&KEW*U5L zj|(wOACh977DNy}uW(7C<{kqKPHTwr_f=A?5HZY%4#P?NXcJ!eofyB6dLCbThv|Vv z_%lyFZFzeo(k|=4x1z!hFkTj}z-91XS@g4eetv{Kgg<9=zfyRBB~>K05D1~V)vU2c z(O|XkBi81Y-o)zFg&b7RKdt_ad45X;pSx;f0w6Kk-LIN8xf2D=oZ_tXRTF%Xq{x3? zK=vkz00s%kA?_B2$N8{h_B;t4izs5X>$Ab?bTrj@>sN^HDGr1FLuQWa!wFV#N%3%% zrU|Rf&QOYYEW%d)G|H0@6PcPmT3NmO__O&It-|TvJQ;Zw0jP|0YL&bxj=sAbHU>T2 zf`qeGC?Ka$d)U)}GF<9w{MzgalHm)B4HDhra9ETChTGB+r|n#JAe~&Z{rawkL=- z<>B@B`^2s&Sz~X*!CmIkdWZF3G1eVR*>-wUT53%mg~D;!(b?H28QIyp+q|1?nK|z_ zrS6!FAar!U_4V~51%i;*sBO@S#}ZtOQ+mfyKk`ZsyEUk0JM zmQR%rz8Nj0XVr+lqGZu6sr+`xlh)JW>qXG|=H`B{?maA4@UTT=4dKZ}$uP*pVmqka zNqjUv8k>D$>&NMJRC$>LXb)JzJ*2z`BI0LA4lFe|qHe{&wD-zBR?~2L{^4wJw#+-@ zdsw^mI=d>s9WTQ%S1E(WG&J;-mK4-Z!!|!h_*4CAX@e_)IQWzaptH}{7#$-@r zIQ8wknW&rZXWcP$kmOQJ$SatshvhybvU@;*d;=zw0xl|zw`V$(WmF15tdhVp{@^6q%WZ%UgO;2Pt!Tx7*@y3HGK-)g1g@(t>;pXlDl_ar zSeyQ(SA7HX`5^r0EI$y#kaA^Wmywfu+R&g4v4%_3E#R|12J%ZqIp_5~#kly;F7B<_ z0-^?I^St-m-~mCjwZhfQ)DpmL$3{C4nHK)!;hCvlJktqo?%rP3W3`xw{3kAqnu53hf)@x&D{D(H zH;3TKskYE5e!UR?_H=EG-Oq~UdiqzO07D`#^M9Ws{|wR(c(K0RAbi(MV>MGWS2MhcVEnGzl#{3(9qgA$)?Nr55@g8L?uYvaiGH@zg}#Zch^Zd$na) zX4gC6;{JDnefVGh81Z`DA$qu2`G3lY9t~DIk)+5>+d1#wfdwBuGDx6cN}4y&b~v~m#(t|-2di3u73fhHkWIgAyzqoH7k zm4G6#)osU8lV7txIjoSLuO#gr5h2FI1{S~fU2i)gSzk-$&!x7Sav~U^SgaH%2};qx zR(;Zc&a=LK{p-nCAfO~-_SGa-(V687RB`kX`Pb2st{(J?Do*g74R`eXU_=b&&=y|` z&0zdTh1#=3iYu^Zcc2?Jze^}<8|Xk1dl#?hM&L9#0c*9{b90x>{io($Ml`#(5{q_Z zxAwuU=7I0_tmAW>&xs%@86wx2u0BE!q4FZqt5^B&o5kbfcqqI4{V`@b&%RYLnR!A| z`5SulfC>J}*lr&U2!Jg{nfDzbf9$PdB4S%b1JzHcp`oF?Z6Tca(JXm{Di_ora`kuq z{Bpft#aoMxK_LGePv>8{;L+!`VYu z$RMl#=GZDrA6J<1*!C(UG(x#3S>0*z)=t{S+r58%B2P_`vaUg;6Yzfaopxj|LbGGO zeZ!mmVdkHoPqlq=dAMW<6agxYA5_4G_4M^WeH{1>0z0N>3 zl&}+q#R<-wU=80-r2^=m{5x-Vma5-B7;Qym4h9-_8q4mh-0ODr0n|c_CX{ z#>ry!jy7~-Ll}4zvG?{b*lo0eZ(XccJ4Z-a&Gv#pp@@lkDy3YipNl`d*@l3ZBbq$P zLdP#dq2aNqjzr&Mf$C&|_}#g)HD0%}xCsOZ{_XYh)^QBirk@l#DIzp6zb7IL49wZV zf@VXp)m)9FwDiwkF_b_DGvReTg6z-L_Wb!3bNz<={li9|^O%j-Ejcyyv$-1UpKHOF zwj1MitQNDdNZ4;5Z1~>B-T(f^a%5ZfcHng4l+yM5?pdjJOUQS&!mLMMA)1_+#49-W zh6Z)jNy`R?v9}g-#Nmm|cD(GC^8u9dskx1VVF?V!7Kbgir3!dCDYWVDE>lAyL!;># zG%AtVtrnl7mBCZ3@A(C_MA)6rI_=FjBRCOUE&CX2FHdE25%YRdEr^pv>un^v0lUSP zPACci6)vy%`E^$GI`Wy;6c(##`WjWbwkp<+*86eH9+n;wE~TO}*OO{3MXZM0N0}F! zT?9Ie>A>AoiZvn%+03(K+*FZCxZp!|`}5^w=I%x@5YUL?)dlGGPzvkaNa^Y2%<7-a zneEfA;@ARa*Mq9cx+~RZJNK6pstZ@I-@GxHK8mLLu!uEb-eA~OIKjLgqspKhDQkR7 z!O|nR3e-=-PE)gQst@&V7F+2kqp!p9A9w&20!l^n&GeysbV&fgD`b7kaLAi(-RO0j z#$)SL^5wvu_T9U15*~O^bEjIAymJPn#C2%)x#{59h3b_X`aUPA5};}QG^jgp}M4y z{PmSU1T+$%wyDQ`qA;n=5{+hT<)X&+_JX=mR#*t7zkHT8jLplp ziqF~R=PN+jT-;i8M@vrhz+rfA8rp|9vTa|Mg276VI^gDWzYjV9OvlYSQr!!C)UGhB zj#x7mqe(8873dnP2c+6kZSkr%4_mWYIDY;^dSgL?^DW+Z-YpS~_P;r|Z~9e=g-y4# z3Fm7Grsz|G0hgLP^gYI!YxB3J?@4d%Qi*1&M6=e;cFCI8%~kau(-q$-vcB6G)D5YtivoZd)(>^R1C*bP6`vY5 zD{-;M?Z^BkggM2b6PB^08-yq6$$;^&T``c1Te~lf za-pQIYN?j`$7KkFUb`V+cdD$PFi&8RfXA8c=g*%%A$I!%Itf)G%RHAi4F zm@Gwv=X3rX1|C@uTn(~(wmoJ#o{!5;)a;Kg7Op%F0b>CZsP$tOeu@kF1kP*r3A ze}c(pfe!scL-pC7Q2*c@{nuZ)`HpF8gY^8t@7;#zm4UtG>>fO{F~^^gH^ znYi5`|D6%FriL9bXToK=jpSzs&BzHDzCXfM;^T2SoL^oX2&6m9?AA>M6@-SK9faf! zrLLUKRBcV>#PqG>^SHy(y`>xfEGBMHoZqt3ZS&YJGHS1>p{BRDg<&>%U7=G*nACS6 z^)d1j0MoslmY!{ijy(4>5z)~YPeTpj^-Jpaq8o!DQ6vIC$H$d!xOH1RM?d-5Yz!nJ zSXfvDp4M2-_`xAzw{>^J*VNRg{V>GxCNIOf@hu{Br-5BVzoCI4liRH0c~lj*e~_=sxZK z2x}m&f|O!i(5F-jm4}Dtj>+I`&CZ`tpt~TElY^06z;qFQ zB$!@kv=Mj{j^rB`2WFy#(e94(smdL)tlk3yya5d4Vs8cs)VS_F&hiIqH6Rah6GH&& zV4ZIC>jkL&Yh~3&$Xm6wrHKUjzClNKgj$Oc21IYSWju#Tp7nnRY9DzF=+u8^aMkV4!ESY`qYe;oc07CIM z<0+f@wzF>z=6Utbrb|a(PiIGeq zBNW{8muUssNhe{(*X6ML+!>1>WC$dya|@}C_gj6gw)okRJ8f^X?V z^j~r<`(vPFC`-s6Uaj7K-DOjhMJA1(Xe>`Ym=w|VXc=l_FeMCx*yZU8L(eP>>Mx1t zY5RRf>6G2WpS+kH^_IrjFJ~Nps28=^dfs)zR+PPqhqoowUj_qejn!|sx!=IoOk$5t zfe(;t2^k3U7S=WBS+==Syy-N&Ol?l&a(eMpVuCGuZ?`||B$LF78v5**)9K{pe4%Pg z=t%3OwFo*a5b&Rq_eAzY5KQS?nMH!vbDgyKlDlx@Jx5|_6+a~UF}0F4o-R4;b-${? z;vF30fXQc2B&9G_6e)eYtS5>M${9MEW>`k@9tHR2s-=T~kwkDAKCDT6zDEm6=&+LYaB^;Io~ljxD!j{bK$q1uM6c`ikLLc)>6kZ6gwa2<@YASezR{2L@|wRcHnFZ4`RXz zVN_HqnLMs{lq1a&0igJNe|rgD5Ec$DLn4aU^>hOT(l#@Lr&^{Xw$R`N1(_<<_OGg{ znsu9=on74?%NyMk;B`9@b#K=*H~(zV^;!S+VjuWNzqB-xH?rRYylyWtT#r{7nV9s; zf~YWYl7Mw}UHOjWE7WVmr(A-TfR2Hr+EHZjeD{$ub1olo62X+4`6f3cdir&T-^Up{ zW#plvWT0*a9|?y(puV09M6tdlR<6}~bjFVqG|EmfZ_Ao0eNs%!%sv5aZh1@XkA=@A z8zA(cq7l2_ys@<{@5yM5Y6G$upvcvlEa4*4kU{ehDkC#HJS4dl*DtywIOo>~iWeCn zh8Vw-SYQS;YQ)~welM@7{5fcq^fE2Xo9++|xP+C}_ryi<44+65IUOzt#lukrd+YV$ zR)JUe6YCdGrASCLSKDzV^}$Hu`UNKN9}q7Xm?qEj=UF|GLykI62YZ>ITpw290C9i3 zydr{3$V<0yPH=mz2Y}v_DLTp^zmZG*{N|yBW-|fgy1Xh!U*u$CF(A1zNui0;)8m?4 zd4qygZz9%9^XPJc3VVNZ(cLF7mG0BZ56r1wsa`2m;%{t;uL^Bt05V+Ybgd1>?ZsPg zg@PZ%|1;yV1h?99bz|JX>sf))TJopjaUc7dHg6Q!{tI49<6rK zN`8IqdUd3WOw7%@V6`5ntB&sMPTcZW!nOT{} z8w1JgSRZl+0;=A2x8E#VJ;KC1l~r7r@i zc;SZJ7mtcJBm8P=uDrJN;!bzZEznra7NtuDKQ$P``qISFW<=Xyj+cHw+t~Ps%k7#z zf36f-phhRINJ;s_a*=Lq{rziVywHlwn?lV&eG&L}Q15GfpmTI#7A{?i75ku7L}RDq zlxFk?7Z-HfJb&36r9tepvNrV+WK{i3hv&JnNtt3{*kIANhhUJG+pURjS1vYtWP$0n zb#@M_s$${g^m|Jk9RGTLt zIQY~JLllp(+(-ARs{sZ7IwRHr?uY3Z>a%Ch)GAF|`kvBSt2A*G;B!%UJsx=b`wBw& z1y$TZgHfq&>$Afk6#FhaR_l3A%Y_Ck9-V|F3S<_ivf|l}tirMTxls4F ztII2`Bg19S`s$Q~i=2tR99^?uGbCRwxu5hsEHo_zqXxxQV>RL5{4B~~0FTaO*$XN} zfN22rIxsB_jr2wP($G_*){(6Xf+(35a(w_8a3OhJdHqzYxgS3#iFigDr^@m*a+tZD z_CMK|Dl@uHAB{UGy3h4Vy;H|W!5{Cifq~CzvQYk=%!lS+GNiyoJY)WTz1-2I=v6Y8 z55{3;$Mu_mE&!ynMi5la_r}m<@4QB)ml))`Pk+VaJ>jOoWbSyY3n{s3(@CQ-HRS!KF zdqVFPem4*r^5lBj9`3)yp20mWAQ861E1>-C-;~Ig@oGOdaU+U|o`m2+a}B06hLomo zGw};NLm-!Y+x3pC1Jsd(;SBmmM#`lYHa0gC?Xa5$fTDtY-Lg^*-GV3mknBeNM59$( z`CEPeH|8jR47Y&X7St~Ztt!JM?@SzbN4e(qF`vFhCML>l0RO3y=r0+F9TxqS3{ZxZ zfapiANF-8tqFkruyrV;OMbh_zK^NxP2X!2}fROU3@~SEm#t_!MiBpqu`d$Rs7l80= zn!gBh>XMX`_ESqp@XjUz9BPPDoDc3#@~GvLrwHatK;SpG+Lx4WLDR4YLQ=H6U?rTu`$QV*7Xg`WWLFx4`gKBk4 zl=3+RC%xfgtyHGsF6izzvZsdTNCGyJ>WR zH~qVf(tt_-k9D_W{7*0c0Ys7i|I#Y|pAN{VF(S}SXx-f0el=l-0a~lvx2TAg+vNb6 zho_Nl;1PsuL}X-9z~ut*nKW9`c4wUWTcwvr%G71_o7Hrx#rldCFo&^B|CmO}S4oPH z&!2yeWJ%aP+_@F2RX|@{T&Tw{fnM|qPzbJ5Y%V3fDhp0c7?;zAgW`(A={0mh($Q}&xA&+(vj4;e6|dtT3v!2HbOK&0Fr z&0#Vhr-HOa5O7q+2N^{ER|~+}T5Gce4FR#Gay`(_y2BL+KhZQm7zF3P1h5w3iw4@s ze?MMeg@R5X!|RiEL{wC9Ev@9UF-swI+vPSWLVi!&p;SH?$ji4HFI!q#jxH~QB`H9u zV^V|DuJohI+3%3U+FK?R!bJ#ATY# zL;EKf1lzT46wv;I4Q}TTM!Lb}kp6P_?b0ZZ^B(SGnQj!IBFWg+l#yqV4KyxzNDyQN zfSV*Hef0|J>HbGYf*=#rH!vWpvQ|()M@__jT#aCaxxja62&A0lH0C6CC(JsTvk5q5I z&JNH`(Su>(;hjM!1V=aL7CCcys!L#?`kK5i4~XPaxC22AuHHnUGL=&Pm-#c0(Y$e3 z!N{x%0xE~6suR+o^2^^?`{sI8TQ3egI{w%Qm9-Mzm03`xRisF6yB|b zr@Z!M#;Y`RjWVmJ2eAEicUN}bxa9z#27GhRWQnG6-|=b}l1wTu-u3CGJc}MWFhvvs z_TJw}NJufc6^`5QL2Gkfqctdud!h*eSA4X~^53g-zX*kWe7I!*pcV;Y>*N&IxVX^h z!erQutOPj38>t6S+-$qr`D|-<*RahODqpjf9`ueMuk};_f&*|>FWD?61M>1rf_#|P z#tS~+x%;kH81|sQcmWM04rF{*M5mo`C`dBD*AskFYG`h{ffhX9nE+%p5+@JOQ{^xi z`!=x7?5*dn}o^$NiH;NNaRZ1*-p_C_Z2-DFDU>>$dXWoNfC9VB6l=2@6n7T%&iZ_q{^1yVJL@-t=_PgA1COmk$ro3=Iu| z^qCF_kFCiP0?6`khKN9pw?rf%6y%9g<>%)IMI=EL6-=IQOkFtpl_UcdwF0_&Y>7H4 zMJ23A@7+T>%Ye~0ikM%Po9CXqC%#;pJacYCQX9A59QaSOiQWES%&p<_x|XN9u0Ctr zS5w!}_`bTfhJ}lpKWE)2JH&rmOtD>5)?yCOfuu8=6@f{unbl4M5&rjjTI>FqDGBg> zFlJd`-H3^c?-b;vex<;|!I_%`mM0S(9qkT&`3c1W@8_IM&><`}x;Ilbkta_wXSg+9 z5L1$WJk|Oz!QS%YhtlW(v{4xL+n4C*>6+H{VgffKQ9nQzu(uSn?=M>;(4TCs1srz5YNSj7n6A<&~Y3_gzi>F)b;{;*u9o zP}v0^@@S~2B!L9<9yt8-VG*?Li#eOk1agQ%IH2~=cRcuAoti01wd%>F%)y$Aa{^K5 zYc`0E9)GU6+$~pDR^<4Whho#yuUfew<&Ar(bxXyRU8Kg2REZ#!ya3_ z{h(d#W!_(!+d?*IIo6QB>uhSefzn(O)WmP@9(nHeFu5uNf^o#<2Nji%w~s)-1fxgb zb@J=~u=mzsRjpn7=tLAnMGOo=Q0Y*R6p%7NK%`qh>F#bN3__(t8tG6{T16V9L8L)i zLb^F)F3|0M_x|4BIoI`_f6jHd-gk@3wdQ>06Jy-t9`_iFn#*ZDXM204Y--kJr>M_) z!>A#g;{{F-!hdxig3J<}JQ*t*d}U&&!t1%WcaE~omPk!)??%bsyVIvnb6SNPbfmqi zP?t0}&+_y0Ys=CSJ%EFgl9pD#JbjqWZ1LLlcRt0O+$PmMq22n`Q{|^28@Pq0PDQ?@gSLQf2ouGbZ%#kemk9W4VLS(CYF830Lx72 zhOuzUyw6cT!~Ua9Fta%Q;@~MLx?EC}!=c@Ua0?_3Qq$_{>n$hE6v}Qt{N#?S-<_8Z zhK^ErV`VmTsgRJ6knKvgv5-ssmx%kJ1uXgPFFT*m1H&oo$GbirXRpS=z;G7`srF3g zC3@j3r*?Y8Ree8#sE!RjAq%2NSIQM3W;bktJfzjqq^!}`w*n@mYYDv}o^PF*BVN6d z9DDrv!y~vGc$T)rC}({K!zjo3XU;nt{oG5}Ah$yyY*Mlm1i6v;*|HtguC6X6XmpU? zsgkQ7$ZpbS=s*(AVUp)GzX|(f&B%J@du@G00lI|uAFj>~G9SGOO?TQMPhtr+H!v`8 z0?2eOi&1ZO!>GKEYAa($s9*P(F{EjdYcwj%De!Ws+wvUUY(mNRd}_YB#ov;&loL2^r7t zD)6Ugu^G)HnqN2d0P1LJA~CANAhRR^mR}F3!edp+4!2~eK_sJQy0_iHWH4CG_Nj!k z8@0d+QwZDi*(H)XuRg>(&(%h`auxgmn_kN;cmz0BNv+K%JRhPbqfObQa?mL*wbae^nkefzc^AR;c8 z?PBAyEo(d?);n>6K3N8G<0gMTL!c0ulMJ|oc0g$X{{C^FKHWeEW7A{y1OQM5@X=kB zSJX=HLn5v%Sj{dS2}l;&%VW%SZ4CB0PmX(prjrp(d;|p*4%B^^lEp z*|vjRiZYm}7I?aV;NZmkerILaeF2sW5{@r9GszI-niw{M1a}qLWP%@5o-TCWTDOar z$9AN)P~}4qmY8!`h^rTaQqG-;i3u*t@t4n^KPMm|Ndr`-JMiI={r6WwU->n^OdUjA zU3TBw<1kxNxYFmqvwm*;^k7k_Lpl5-ViThS2L*mi>7Sp#{PH24MiBE0k{=5@>zBv9htI2?@tgL{*z&Bu*s{xg7(&b?vJT@L-)7K2SAz3M@Sg0o8S^lc= z{KboWVtM#G_=rs^8GsvWX6HChpN#~@UDTcU#EdQAJioc$?q`NeNR+zh;bQqL26ps zU14G2T;GQ3>X#Ht)7_nzBSj$$g$FkJ445}(2sXl-0n&&RcJ*ImW0O^LS_%aB*j8Yn z<8z){PNmTDoez)oj}VGv*s#|54@;melv!fs?$qeR)Kq|(puiX^cidAs0HiM8e=jli zC88BvgB8Rm-Cx0g2ut0$^R(B<-P2RV5B9}tq9f?tT~p08-Oti9G6qwKy}l;Nw>gMU zDQsqD7RIXQmmnUM!O`z14GnjU7pq>qe93x*3*6zAVuL~`)frjPM$i|o~zUq9a8on_H!d^(~rJ&SoW zuLI1jCVhc|5#B|5_J?b*#7nSc47&4-Y8xBnA^$K_vZ9Kv8dL&WcNW<%rGUOEuD<^6 z=`&~OfFcE#FAbaRv71{2Y52>R9}29tb6PDjDC{0H2t-~G&vK9YE{rWY+YwjCBO5dl zu_ibi8G85XzT7y>sr%3^;XUW<`~1Y~gnv1Nt~KW@R(ZAO_bWcWE)c1S`ym?+EtN*- z%^m!|zlNFFe;nkHrvg()ZLyx8n0u_|DVV#n(or=&^}?-E%sFodRt&x7g*yFy4k=4l zSbVIIqdwu)r!;Zr_uKisv6GB;&5u5Zm9OyLGzL8}0QlJl&L$4Fam}uen0$4HZNTQZ zYI>T0!1l*ag0p80TH=K#n_}FY=iJC>U|!&ewk}V1J85?-F+g`Ib}{H@CvoeRJDlQ+ zULG7oFl1MbxO0yL<-r}ErkB%&z`%ZMsZ&-})p&CH;X4J*cM6se+@VJL6xH3iuNs6` z)!l(i&VZ6aI5uMFp@MMx%~>_aW}Un-2!e7gpeq)aP{Ww88eG6S9ENOA$`BEsQ0k2IBd31ycsC3DhZ&rEnfIkuk*$<5wGKWW)>Es z&Uf3&KMy@CG#sv7k?pp&u-xw%HhnAHV%z5B0F^| z4w64zy@~BHH=hukI#up_3=G1(idbwO3ILf23*QVfC%SZ^0x5X57F(4{9qr=!`oUqc z>a|dejE#Yv5r6mYortJtHZcejQG14>Q4tZ5merrbMpN2fUMD|(yf5OqofIc0XKi)$ z+oBl=4%lpF?!$ZuL)h6i7UeRtsF8jh%Vp5xEaa^?onRMm*)Jvm3-BhCMMtin4@iPV zKwWy^fkni`0x3#1=j*=Zn?#ka4jma88KJ#)O#x__?4rI%9tX?OcPx2v(rC;Bakc?8 zH>9JbO&-M$saQ6tkS3eV@>%)O6~|b*`pH@iuglms!(qJXBE-~n)&(F)v-WVCBo(!) zBQe?lysHl;*DoGIeqAUb%e&F26XlcuK*G*&CpI~3=YlAll*bIjsTU;I4$eK_`uS+J zFDRnOI(6L9I52Q9KR3_m#^D8#J9kXiNj5=glmwLoK;Q&Ce*D;S`WsuWNx!PY@(*+! z4NsX*P0Me(7@b-Pqxur4QerO)doE9W2Cuvc&K-nR50%Ma=YSYV)bCrKQ7V|9?!;b* zgJlg3)25ff_hbSf)BpPRAz*5w5-MG?xo<%$_W666v-Ut9-&cHw~>&L&{V59A9`g9T3B8nG~|tqGo}k+dSqb} zO>+SvvmXhRU;J4`HU+D!-<9|AW7{UH)e>pumUMLpE+PpN?X(V(F z@DDu`lZ2L*78`HSWDw`KWiqG5S7=C6vhal)bG<3G9VN!H&8+Q>vK?h|LGPwUmu(9? zLS}b0a0MS6sO&v=&XOK2Z}L6QeW=@OY1~{qP|HrIwE4Xbm4)VhqR!{1>18A&q6Y>Z z5PHVIf5A4PK;m9+=&EBfxUUbxBb4aj1{k3;_h83OgrmUWl&uQ@WbU$)4ZhNa5| zR#mtKh1IBgUoR)yz9785xzHFG5>ovI&M(r2U2%vt3<46phA=iRyZLyP!2u8jD*;jB zQ*xY09|9Kp^}d4E1cbfQz_03(hi7^$d!-fi2U>PoHXK zXk$9sYthJ~RA`wz9k5^Cc-YgBL#Wfo%c~WVl19NR)7^`LHO2W-E zz@sizlfOs;z(wY~8Z%Qkb;ZNo{cSk_W`uxz0I$z(?0hen;^)4;dCGk;HxI%g7Ij&x zQzII278Ma8ARvebzzLQotFPj56{*8rJv}{u3lh%GrJ!t((c0qM{)w+r=Bxq?_)OB_ zsrWuc*mG}})T_bo>+7co`jY5zBmf3n|B>B@M!OABeEoTwF56=+tPts(=X1Ufo0-dW zfIo0b4x%$YG@ye$&D}!%ChHEuZfJSA^|1e^xEQP3?2kP(Ti5I7t&spFIHKRF#!BX>m3s@;cd6 zsL;GD29z6E7zv2#Cat!>6-t7eZ31AP4AP+#wK8YxDVOE!C|c0Yz&pW@hiyaL*wr!p zEjP7qMZUS%ZlMj9vEuV*8Y(J5Nl8iji8sM1-d@k2OMwU7UajK$R_bX1#&i-wP*TS^ z0XCDq`w)IXx={)q+pvcfVnXx)PxcgKhp`zXtghOs6qw&e7tp9afL6LKRi3d({Siy| z*Jf*jYAfoyA}71ipdp;>AZkC~#^NThx7k%Q%qqNK2X>e}F5n z42J+-MkG!r>Y;xke-CpWFtZw85`Rfqed@b#5OUo|eg6aIAWl|t%D-OoQs3|R1vU5p zV!EB$DbQxwd+<0JE%M^3S+lnpJI=42#X1!qWf%`_Sq7nJEKSsM+{vqHmaap(C{KpW9A zGfP3N)z+1*bDsB}P`-R{6}s)wecuU@5d6=VlqUtG{(3;8;1!m@NuM~#xB94#wEq>k z5UcIn1BkqYQK*iFGLT`Hw>y^0Q#Dz6p2uDW{Czuw6@YLu!M4-W)7Rx~yX>sEK)6gX zGCDdLZc-`<;ABZ{XGx8%Pu?hBzI!;v%-`jrcoz6O!27P^GcWKzujE0O73JGw7O81R z%hGcJe{_jZ1;2bbV0vk0?tu0=>(0vz3^IUktblUvUI#&f9&l(}4$En<&Q{<6(eMP_ zClq?0pPpE6FmZ+eF`6!FUYX%cx06hMMnaXOBsgF;_0NY;?4jSD5_tH?ktLucWdTyP z23=Mr;f?xctn7!PKnNqIo>508wX)OG(nY`Z9uMMbc;6QRjE`EAdU3~{wHad28IY=;cf-Nwd zVrFIr$C3&HP%vP+V0#NuJeO}VQhWNcN}&us@9G1e71`i!i$z;>kw3K32!1ev2#^(| ze@nCd{l;tHR4g~9^SiI?fPv`nqZCESyj=Zuk+QW0lf^N7;8{0oP!2rlH{=9>@wIAe2v)OE;XN@3#g7 zwzRdu1>~5v zygX&$(9S~&V+(l@Tct-dw-3Uo7g>?)l0ajw^t<%SJxvmZ@u8Gl_Xt3+ulHhf8GwU= z&EZE;vb{d_EnDXW`2MjrqXYZ*f4#@~D&80_zTuV4mEUKd3OSEh)cLXXIXf8xt$TBr zif@DbE<|D&1#H6GdnWgI~s{TvBlNbQ1 zD1=x#K3qt66D%KkHl{42Uhpi3psN5=hWZo$676v>c=eU}tbZuLQr!mmcUy(m*}$Nn z1h`0d2V^*u_JUw@3yY;sM;OP2_|_$TUX)>0N@29ArzPrFcqa-B>A*e|{=+l?ypIDa z7T_BgdDWCwt3-G{NWyS3@XD}5Kvc3mnOZpP%S|B+kbb>vXM@jab?}LG1u|tMw^>F( zNliTkkPnTCo#*|dXkd%OIm}LX8G1(DmS?04xhi3Bm+gV{@;Q!c=H2TTZ+hx}uYPs- z*fBYP;a0#aY>hCwB>()`VeOEMSvF*j)m|`K4sU{R(1LrvninoCWt5A!!-AT<;D^?6 z!rhp-7^mV;^gb4n(*mci{EB<^JXdnRce0<6;b1%}Lip%qx{*e|yEXpXsPu)dBP~ycmKRenBZQ zT~^PBZL8A3Q-$mA-cOYhN#DEYi*t-52{22pUaKHVHkVJUD3ike&09L-bsG+ZG@3Gj zqzEC4cD4GT1cq_T5CUgWss8S(b4ro-Iwu4=Xy}Q1ii!rdPLa*59Q^on7tdSgT@H6z z*nunThEf0$lOwpHS&VZ+xh7xuT&8M+S9@LaQ!fm2@{$q- zK5=JNZ3R%~3j*>XJ z%2X{e42BRd89eS!b#y}wm{(%|EV;~WGcDJ=)}vt8ItNnSL0@ z!3EG9K(9^z<%v;RXu|&t5(#&6hf^&euK|}5qOa-dnF=7__!+T7w)3uH8rg6`P6S7kn8D!}z$k%`hLQ;mkdKdI+%a9pL-Zm^0vMCOgu@ zA>5qS7tgi@Pe?#aY#olem7vG0k&nqULT}>Ohy@k2G3n~9Y@FbRR+?&E+Vfk5z zpWhWAMk)gtr(QfHN#(TcKz;)Bzd=#4Nk1=K?x*^aIqT3R(U%z53%STiT|h{AeMi^U z)`0(L2VTf%s=T~>sa;;JOC>Qr{uYo{fEQYz4swd^Rc5MZUzNrSo`jqnRtSN?tn_CH z#9RFl@4Ts{J?{Jb2wHz8n&yUm@P5Jx#FRc#0pa)urZ>>fgRytLyE~u-w5-<-Ncqx4o z7l6%ZL^Cur1k`0B+0JT}J!D($(6vWQIf)ePKLFrEGakT6pXW5cb;j$>Js)|f#)*X- zoTyapFhrO+*3(L0Ig;MKl|UiIojXlvE{lNR`=Ww8>svH(8}NkV5E*%b_3ffF{Ga)+ zgoo0Y@HQ|zIp!m^M4bS50S4WK6c4((DB0Cx(WnQ+au}~uVeeL(&abq>)lb-9B+~-| z2*3(-u5p2HNHO1bxefG!EI1<|VF<{`G9fIWg(_00Ua76AX$I9dWCnHsy8LpTxG2C} zb%g_zl9Y4@c5|lLT}UKSBt!os>$-Fv{5+HXnKp%0vcLJ&jsUTNUcleE<|88Lxx&I? z>-2?#OeL(}nGw-zJ~aY7Johisez^J&W4>i1n$uKRFmL`J5}*IZ2>d5U@cU-}a-n}d>_1<@d~7ev>S}-WS~nIyHULn}kMZq)L$d#O zv27c1e-c*oMVLlyhhhG|j{1a4qfaoc0cn6boXOSuht#0Js-{mzCwr&V$k>p3K|Av?#wPA_kTzF^TFkN*SPjQ zP6$MZfU^8xXu|t@n9@5}m}MAlw8;d_1wY=uK{db=mW7L0#F(AJ41h@|GWRrL+SKJU zJ3J9?PN(KbcdyFV8RCIcJ|J^UT>J1U9VXlDirbd>Kmw8@jsyo>D{nVq;)1!01g-=4 z=!|U)-~K&g?Qa!`tP4HjOuk>Jt8Da<)*3MQ``)|~^_{qVCyIgiXMu#P>x;cCp4KT% zB*t19XN!18L0jv`tXlQCc+HbR3);sDd)tGa90jUmHsocj)ErH%JaOA3Md%_ zM48MkpS?Brgs%;C=^)gQCsg~d3HDEAP3~TplMuo!&Jrc}u@u)9-`9K2RWVyT8S0CZ<42H= zrsf^L_2p@U{9^GaUcY$2w!hsDq_8z0_8=FAe0Y|!^N+7rg{2^&{3A%2k&Mr|C;8nR z-`@{4P7v>R0Dh>TN%Nei^G`3euj%wxt^`p4Q}gvQJ|12m1uCUtJG1@dca+E@btp;V z;GB#XbJqT1*wLx!v^A5ZSJkCj{J6Z1*(oeZ=b(r z$`7zHBk%Z$GiRuTQ8u9WkF5H}EFXnnU=*Fk=v!|s1`WbGg-Yh2clAH&Si7A=E9Pgl z&eS!Kl|N8^mo`(E67F?QrqD6+C%>af-4~T2Sxesh+S<8pC3+SX|Ma$}Cr_TF5C(m5 zl6478JRqN3rw!|d*PPOTD^jg{{=>VC{EK(<#&|dWHykahrr!jdNK=bQH7YhzCY*cM zY&Vyzs+NlsmbKT32ZK?1WzbmT(BotnBPuP=A0}NeptbpuE_`|0{b9-ewq%*IUw1iBnTCg1J5$00)iwT`vkl9b`Mu z)B%vH-1U>7DRl-vDG8)TuhkSwP%A6g!W27lqs^intVBH-7q!<3gWZbuJ9z`FcqU`x zta#5)Bw?rp*Dz`i0&@LQ#^)zWBi^i-9C?1C&o^>UU1I>K{jO(%{vNSF>=aJ8+|Pa^ z2%$5mr9$dGes8AUtX>AqE(u2;>PYc!^2R z$yCcrfAA1w7!NU!y~;{hy1hQab~GC!eR9Q#3B7bmMV?nqRos;mm3|@!QFZSrU6(D^ zlKF*LCahIhW7n=ehUUGQe9tO=EZfB0$XI5_Wm)Wl3udwRmehXOy*Zo@%Z5r^{5qKn z=kw^RUfg|ICgz_HVlmg#IBjU^O1iq zg~;5ASId(zixON83#}!2G88) z@acZ<=ceZPugy!ZBVMmBd@d^OOrF`v;?M7Ez{j)p|$Ga0K>ePN}taq~vk*1068 zjD7Pd9XZ4iSu>CUBgkYtZ^h%u=f~L28;hTa9TjHi#&Inhsz!Y~g`S@f3PBQC8k2n! z6=k+%`t1vNqmFfI?VI@`9v;!)f8)k*F^-c{|M%*%15~gL?|SY%Di;e@+U)n_gr95? zLy~q`6SbtLW@@v|Pb8EI`7sHkZ!NVnO@E$M`rv?Skzo*Q7Y*-fPmw>|(ua*><$`Da z8FzpTJUMK=sl&*&ElpAGoP-^>^wXpbgHJy;4h)E6{xQ1pS#S}xYai2~PKaVInMSI@pa17qZwcPpIzRdLSSo5>|xKw!ZSG6Wl54|oQWOw&5kdt zgz8u|a1C;ABow`PjkR~czp_a!ej_J13Od?VYwgo3c&haL&k>rK!uPqVsXj^g)S(PgjyRH)b)3XX$<@)nDP(}8=qV~Y)Z}fY-f8aW1 zyS6n`yujdqYYazauDHHTv@uUO-gK_v4%W3Nn9JVv5vD9aO!+WJo1Z+}SsK@og&RQ$ zN|>l!3Kei;iXa9zZU_1cME0fR#oc+$Q~SA#fivZRDGz$1?f0#F=YsF`Yp&O>M6yTl zmpB(AuWya9L9%FNcR!bfTXLyizNNt6I5dqvK7p)KVQ$M5QTQ5%Kjzblc|4r6W0=>d zs`ei%j>LLTj3PppXuVIj(E;eNZT_Tjtu49qIwC&H1uTteNrK_N5BK)~V`6ev(s7UM zibM07{rd;(Nif`T>%ehmbDV+mn8iizW^Lj42x0Z_9z^G>!YeD!>?<-U4j^hmjfegn zjkWt;rloC$YMH=SuVPS_At(q*M*)pWZ%GM>m&%o&8vCm2uU=F0+l@UEU9>b`QVtkg zWB!BtPhQAj?6N!K$i#GFjIERNY2Tyj{TS%f_A~gs0 zL3KUBOa!v^+OoCsf01yYP%~c1^W%km9I9bvgBlUfjKf6RTfI z7XDLsBd4uRrJQ^MIMq8+xoXpE<8d!0ffZbzE62B*O3(imbQbh#5Fi&$=XEdkIn1(6 zDokm7CuRq?`jNw4umSx3ebv@$YL2C+y^u&})$J@5r>$JI{!R}o$BL;=*3v8a6$a5) zE!h7V?ptWObNnTJ1PLW5N7~WFKpab$uvD?XoY_A)&oz{b1830vY&I#|tH405d`F1- z{o>*4sRYSD#5q7uw<0%iO7EI7&0A6d$zU_znefgF65xVW4WWt%rNvrWTLXbMh3y5R z76X8ED7(21-QdPG2A{YD=wV{L5MJ@ATV9!)nmZJAo~KC=)(0-}I_?{aFYYD@S1d%L z>J!*Fs3-{4={|ib0kt#%etxkaohh1P>rk)q%fyftFyP;N2gZ`N3b zLwAN?(*Bz#TYh1w@8q}e13s@UJO>dnD9Gn z&oX@7J@#f_+b-IK1>Q@Keek$;V_(ww-HF?M^^agbYyW^ATx7v0O>9rPwcY8$mW9?qNj5t>fwWeNv8&#n2fn2FkUjqINbD zL5`6C-1rm(mME1@DGV7<7VY{bRF480p`{;P(va_N?J2OZTAh1%S+!W9vaGKG!)?!z zczM9N`ru*~<{2y^E*?z5v$MIN-;iU66p9Y$ASDV_m>}XGLo0Y7jGshhxFGqJl9o2oc=4MGH&WOm8^Edh z_){DP18UcVgE29$^S5+u-MFScef;#^G+(Vj1Y7V+E?sambycYC=>m3aO8Z%1U(C zG&`;I>JSq{-4nEX(*=ztDvpHY3YxcsB5hhq%G;1yz2^g9tFK(i{jCDc9XNgivqX3d zlEg#`>s|N6G2#BFsrfWfY|)8+4Jgz>55Mk%i6bOzZ@)vlrMZfI7jW~cY9s;aS3UsZ zWrHD(81uMi@*R%~i2yA5yUD*g+VnW$FCLxY&lm$lB^YKMbCLgT9-Z})n>5rqCxM3r z$6T>{9D5=YA4&u~*x9eRF9|}SUk+rF)9xx!WTJ;{7d#j8V>e_+5R^m5jBQW*@sWo0 zw-#W}(fRMv$AgjA^%QDg1MWvr)h|-PR4tN#0-Y)(<#z}qE6py@8^PzIiXMGf>Gonf zi>)!2?L_D8g{bU@z?cZR`$;W9z`M|1u9{j!bT*&OKJ z0~#>syKAhe@iD&-X~m>D=AK{Yy!3br;7JH!#Zw~3wUJ;H7EaFtK};zOdIQj7zJNO`Bt6c{rfIf&N*i?1k zbB4EIpjIbk9v=IgOo$-aapxqT<%TeUz!23eqRMa3BBBQ$gnaiRbqpCQucZh1t05ka&;sKths7o*+-a(udwC`OZimR09%z8*f|hZf3#R1UIQ zaP|(Lgc&|5#0#S!etv@O9LPOD_<~*=t%&kl1cC5ZAINl~GJvlNbLkw_yBmk7-*8?N z-8QD~>bLNK3q^Ymf!U;odLg%i_#GhF`ca8iZ9p9x%>ySNu_X-Bmj*Gu=9zjTe&iL~n!Sn4(i+;0u|C6)`K4tp&iMGQ|IEG)fR*!e?(mnMSOo6 zp-fyx$akhHEF}61hIouVLjW(DUdD|07^H=uP8x;h|Ie~_5<)maw%EwPNz7pXK7Y3^ zE4CfK?Nlo3b{%uPN#Nns|SQIUhMIX%>#PE{?AKk3~04Klm^xMz=+yx+no09@Lm|r}o_w+#>ff5wZBsmF+ zB=zRM*MQ93731V7XelT@&#;4VnNk>pg!gIBSSYXc$3eR-KH{be-WL#=q6}q4|##mz{0Dco3Eq zjX-I8Fa^k0D>4)=2t#GGwT8{~H%eiVyLT-iXR#Q}{AZbay=#rbu+(9MfO!cJCEP)x&1UB&=+J zdguY3-@(f+Grn7v{LAs*e;F*WYn<@VwDYd=vn8OrbEvQM#GdIshY3h^sMzX3`ELq! zyU0#i2{*of>Xp_fE;ffK7;aoR%5^mKVcnfWxS}!_ToWGAWC_2-5_lE8AJ2hc|1CMm zSly#6M{hg`!8#yWsrl83lKAERxJ$Q!EYxLx}5_W18L?@CEef#K>lD zAIGb}L1>P_mB&W}(z7oI2;K8>k#|R2horn+^*4|VGym(fwTOIBU{vV!lFmz_(HjKh z(Y3lg6yodG{BQJY=~dol#1qt4YD|+C-_q+}RP|zbWKAoG@Dm=3ekX<%{Tlr}^k~?T z1K8skhS_=ATm*;hZz23H(%y(GFVWGJ(dl#7S9~kGB_;PE>o3&z%XA~c!&G*3Cl}YI zJMc}r3o|={@`JS&uq?I+xnCEQmp;4xiZXz{;!qMZ5l4+bZ+%?T`fR%Mg^Y=7b$N&J zmiosxu+DKSYg69?o99KKxmebgb@EzC`a#ajENa@vF|jv3yyf?&|sz zNYYVlct$g~ZK+t~=ZfRE+NA7kQX&)4+qejg3Hncx*nLt&o7_e^V=O6Z9jlfR5nXu* zza2Bnz7(Si(Xjrh58if%ntT(T4qsYlIO0)DsM~2W{N>EKZ?%aeIs#ugFx$cjrbLU? z*iYxS$%`A^Zfjo_jmHfy#Q8Kzct|1jIK*lcd~N!_S&RwPNG z;b6)1>5H|?ZdNh%UzjDEW}$-x%Rbk`?R^Z-?9=g{r@lEW@J`!RrsW|u!aob}B6=v7 zfYk}~@`6aD!?>ue!94m#VpRkZ7LTy3*K=hPa8cV4CF!}Dl zQaAddq^R*tdaR%eDULd3)#=^eH>;g@fi?6#N#ym&*Bl+AGR= z%=UT)Po|Z=HbI4j(7;-PN9C8q%fYQJ2X$oYed{hQVL#AU=e~D@FuvetB{2(hRcdQ* z*BWn%Nls61FsnKr4D|#y4l1tZ59Id4{<+E$dz3+~^g6IMx??wOFOEez7=zOcgkn%0 z==7m;k9#_&{xv7d*lUY>hkbb8*l4NH`C}}Z5ibrS^#;{^0Hp-Q^AlPRpmhW1&{Ohn z#hXB`H$b@xGqihwMzrj1IC!;?Qm}xs0v*r@gz`98=R-$CSjfibYy67Rif&`u^;s=> zd#4s2i*g3N1XOmKL$8axf-k@y`BOlT3mg9thv{5-KTRkz>{U7&Ekk$>8wudxAs10l zQBi>+^H(4g)%xI?Hzf!amX>~+K=+FUb?$7{vou;7s33G|!*L5ljW12xNRg&s0&G^<};>{ZIIA+`|{^NRXXT%asxUl+zrC z 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 literal 42998 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 z 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 Date: Fri, 7 Nov 2008 21:18:12 +1300 Subject: [PATCH 004/100] Change import_file_dict to take new args and fix all callers. Rename the main parser and make room for site converters. --- pyfpdb/fpdb_import.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pyfpdb/fpdb_import.py b/pyfpdb/fpdb_import.py index ffa06cff..d0837f82 100755 --- a/pyfpdb/fpdb_import.py +++ b/pyfpdb/fpdb_import.py @@ -125,7 +125,7 @@ class Importer: #Run full import on filelist def runImport(self): for file in self.filelist: - self.import_file_dict(file) + self.import_file_dict(file, self.filelist[file][0], self.filelist[file][1]) #Run import on updated files, then store latest update time. def runUpdated(self): @@ -140,17 +140,25 @@ class Importer: try: lastupdate = self.updated[file] if stat_info.st_mtime > lastupdate: - self.import_file_dict(file) + self.import_file_dict(file, self.filelist[file][0], self.filelist[file][1]) self.updated[file] = time() except: self.updated[file] = time() # This codepath only runs first time the file is found, if modified in the last # minute run an immediate import. if (time() - stat_info.st_mtime) < 60: - self.import_file_dict(file) + self.import_file_dict(file, self.filelist[file][0], self.filelist[file][1]) # This is now an internal function that should not be called directly. - def import_file_dict(self, file): + def import_file_dict(self, file, site, filter): + if(filter == "passthrough"): + self.import_fpdb_file(file, site) + else: + #Load filter, and run filtered file though main importer + self.import_fpdb_file(file, site) + + + def import_fpdb_file(self, file, site): starttime = time() last_read_hand=0 loc = 0 From 7e41c737424e0e622bcccc5f3b8c4b72da6e692b Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 7 Nov 2008 21:47:00 +1300 Subject: [PATCH 005/100] Add initial versions of Hand Converter class, and initial implementation. --- pyfpdb/EverleafToFpdb.py | 35 ++++++++++++++++++++++++++++++++++ pyfpdb/HandHistoryConverter.py | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 pyfpdb/EverleafToFpdb.py create mode 100644 pyfpdb/HandHistoryConverter.py diff --git a/pyfpdb/EverleafToFpdb.py b/pyfpdb/EverleafToFpdb.py new file mode 100644 index 00000000..e3c7c390 --- /dev/null +++ b/pyfpdb/EverleafToFpdb.py @@ -0,0 +1,35 @@ +#!/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 HandHistoryConverter + +class EverleafPoker(HandHistoryConverter): + def __init__: + print "Everleaf Poker hand converter loading" + + def readSupportedGames(self): + return + def determineGameType(self): + return + def readPlayerStacks(self): + return + def readBlinds(self): + return + def readAction(self): + return + diff --git a/pyfpdb/HandHistoryConverter.py b/pyfpdb/HandHistoryConverter.py new file mode 100644 index 00000000..5b91a360 --- /dev/null +++ b/pyfpdb/HandHistoryConverter.py @@ -0,0 +1,33 @@ +#!/usr/bin/python + +#Copyright 2008 Carl Gherardi +#This program is free software: you can redistribute it and/or modify +#it under the terms of the GNU Affero General Public License as published by +#the Free Software Foundation, version 3 of the License. +# +#This program is distributed in the hope that it will be useful, +#but WITHOUT ANY WARRANTY; without even the implied warranty of +#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +#GNU General Public License for more details. +# +#You should have received a copy of the GNU Affero General Public License +#along with this program. If not, see . +#In the "official" distribution you can find the license in +#agpl-3.0.txt in the docs folder of the package. + +class HandHistoryConverter: + def __init__(self, site): + print "HandHistoryConverter __init__" + + def readSupportedGames(self): abstract + def determineGameType(self): abstract + def readPlayerStacks(self): abstract + def readBlinds(self): abstract + def readAction(self): abstract + + def readFile(self, filename): + """Read file""" + + def writeStars(self): + """Write out parsed data""" + From 70f9e6edca76b106359669af3f44d7882e9bd336 Mon Sep 17 00:00:00 2001 From: Worros Date: Fri, 7 Nov 2008 23:19:18 +1300 Subject: [PATCH 006/100] Make Everleaf actully inherit from the super class --- pyfpdb/EverleafToFpdb.py | 33 +++++++++++++++++++-------------- pyfpdb/HandHistoryConverter.py | 8 +++++--- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/pyfpdb/EverleafToFpdb.py b/pyfpdb/EverleafToFpdb.py index e3c7c390..9f6b760b 100644 --- a/pyfpdb/EverleafToFpdb.py +++ b/pyfpdb/EverleafToFpdb.py @@ -16,20 +16,25 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ######################################################################## -import HandHistoryConverter +from HandHistoryConverter import HandHistoryConverter -class EverleafPoker(HandHistoryConverter): - def __init__: - print "Everleaf Poker hand converter loading" +class Everleaf(HandHistoryConverter): + def __init__(self): + print "Initialising Everleaf converter class" + def readSupportedGames(self): + pass - def readSupportedGames(self): - return - def determineGameType(self): - return - def readPlayerStacks(self): - return - def readBlinds(self): - return - def readAction(self): - return + def determineGameType(self): + pass + def readPlayerStacks(self): + pass + + def readBlinds(self): + pass + + def readAction(self): + pass + +if __name__ == "__main__": + e = Everleaf() diff --git a/pyfpdb/HandHistoryConverter.py b/pyfpdb/HandHistoryConverter.py index 5b91a360..9a62776c 100644 --- a/pyfpdb/HandHistoryConverter.py +++ b/pyfpdb/HandHistoryConverter.py @@ -16,18 +16,20 @@ #agpl-3.0.txt in the docs folder of the package. class HandHistoryConverter: - def __init__(self, site): - print "HandHistoryConverter __init__" - + def __init__(self): + pass + # Functions to be implemented in the inheriting class def readSupportedGames(self): abstract def determineGameType(self): abstract def readPlayerStacks(self): abstract def readBlinds(self): abstract def readAction(self): abstract + # Functions not necessary to implement in sub class def readFile(self, filename): """Read file""" def writeStars(self): """Write out parsed data""" + From a5ead615fc05e81ee05b2a84e2fb20f415429cc2 Mon Sep 17 00:00:00 2001 From: Worros Date: Sat, 8 Nov 2008 22:41:06 +1000 Subject: [PATCH 007/100] Add parameter for base path to store HH's --- pyfpdb/HUD_config.xml.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyfpdb/HUD_config.xml.example b/pyfpdb/HUD_config.xml.example index 3a595fe2..5a943044 100644 --- a/pyfpdb/HUD_config.xml.example +++ b/pyfpdb/HUD_config.xml.example @@ -187,7 +187,7 @@ - + From c215360a922f48958b94d9fff8c13f32cd1597fb Mon Sep 17 00:00:00 2001 From: Worros Date: Sat, 8 Nov 2008 23:01:07 +1000 Subject: [PATCH 008/100] Read new parameter in config --- pyfpdb/Configuration.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyfpdb/Configuration.py b/pyfpdb/Configuration.py index adc950ca..b84dd934 100755 --- a/pyfpdb/Configuration.py +++ b/pyfpdb/Configuration.py @@ -178,11 +178,12 @@ class Popup: class Import: def __init__(self, node): - self.interval = node.getAttribute("interval") - self.callFpdbHud = node.getAttribute("callFpdbHud") + self.interval = node.getAttribute("interval") + self.callFpdbHud = node.getAttribute("callFpdbHud") + self.hhArchiveBase = node.getAttribute("hhArchiveBase") def __str__(self): - return " interval = %s\n callFpdbHud = %s\n" % (self.interval, self.callFpdbHud) + return " interval = %s\n callFpdbHud = %s\n hhArchiveBase = %s" % (self.interval, self.callFpdbHud, self.hhArchiveBase) class Tv: def __init__(self, node): From 4f64464df3f58c2814759c8143d1c05ab0bdbd15 Mon Sep 17 00:00:00 2001 From: Worros Date: Sat, 8 Nov 2008 23:45:14 +1000 Subject: [PATCH 009/100] Fix accessor method to import params in Coonfig. Make cli print using accessor --- pyfpdb/Configuration.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pyfpdb/Configuration.py b/pyfpdb/Configuration.py index b84dd934..0c88e2fd 100755 --- a/pyfpdb/Configuration.py +++ b/pyfpdb/Configuration.py @@ -439,11 +439,13 @@ class Config: def get_import_parameters(self): imp = {} try: - imp['imp-callFpdbHud'] = self.imp.callFpdbHud - imp['hud-defaultInterval'] = int(self.imp.interval) - except: # Default import parameters - imp['imp-callFpdbHud'] = True - imp['hud-defaultInterval'] = 10 + imp['callFpdbHud'] = self.callFpdbHud + imp['interval'] = self.interval + imp['hhArchiveBase'] = self.hhArchiveBase + except: # Default params + imp['callFpdbHud'] = 10 + imp['interval'] = True + imp['hhArchiveBase'] = "~/.fpdb/HandHistories/" return imp def get_default_paths(self, site = "PokerStars"): @@ -562,7 +564,9 @@ if __name__== "__main__": print "----------- END MUCKED WINDOW FORMATS -----------" print "\n----------- IMPORT -----------" -# print c.imp + tmp = c.get_import_parameters() + for param in tmp: + print " " + str(param) + ": " + str(tmp[param]) print "----------- END IMPORT -----------" print "\n----------- TABLE VIEW -----------" From 7df572895bd11f0100b846b12febbb0a7f112c40 Mon Sep 17 00:00:00 2001 From: Worros Date: Sun, 9 Nov 2008 09:49:05 +1000 Subject: [PATCH 010/100] Work on plugin initialisation, add Carbon poker plugin --- pyfpdb/CarbonToFpdb.py | 75 ++++++++++++++++++++++++++++++++++ pyfpdb/EverleafToFpdb.py | 12 +++++- pyfpdb/HandHistoryConverter.py | 56 +++++++++++++++++++++++-- 3 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 pyfpdb/CarbonToFpdb.py diff --git a/pyfpdb/CarbonToFpdb.py b/pyfpdb/CarbonToFpdb.py new file mode 100644 index 00000000..0dc7dfaf --- /dev/null +++ b/pyfpdb/CarbonToFpdb.py @@ -0,0 +1,75 @@ +#!/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 + +######################################################################## + +# Standard Library modules +import Configuration +import traceback +import xml.dom.minidom +from xml.dom.minidom import Node +from HandHistoryConverter import HandHistoryConverter + +# Carbon format looks like: + +# 1) +# 2) +# 3) +# +# ... +# 4) +# +# +# 5) +# +# 6) +# +# .... +# + +# The full sequence for a NHLE cash game is: +# BLINDS, PREFLOP, POSTFLOP, POSTTURN, POSTRIVER, SHOWDOWN, END_OF_GAME +# This sequence can be terminated after BLINDS at any time by END_OF_FOLDED_GAME + + +class CarbonPoker(HandHistoryConverter): + def __init__(self, config, filename): + print "Initialising Carbon Poker converter class" + HandHistoryConverter.__init__(self, config, filename) # Call super class init + self.sitename = "Carbon" + self.setFileType("xml") + + def readSupportedGames(self): + pass + def determineGameType(self): + desc_node = doc.getElementsByTagName("description") + type = desc_node.getAttribute("type") + stakes = desc_node.getAttribute("stakes") + + def readPlayerStacks(self): + pass + def readBlinds(self): + pass + def readAction(self): + pass + + +if __name__ == "__main__": + c = Configuration.Config() + e = CarbonPoker(c, "regression-test-files/carbon-poker/Niagara Falls (15245216).xml") + print str(e) diff --git a/pyfpdb/EverleafToFpdb.py b/pyfpdb/EverleafToFpdb.py index 9f6b760b..515f28d7 100644 --- a/pyfpdb/EverleafToFpdb.py +++ b/pyfpdb/EverleafToFpdb.py @@ -16,11 +16,16 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ######################################################################## +import Configuration from HandHistoryConverter import HandHistoryConverter class Everleaf(HandHistoryConverter): - def __init__(self): + def __init__(self, config, file): print "Initialising Everleaf converter class" + HandHistoryConverter.__init__(self, config, file) # Call super class init. + self.sitename = "Everleaf" + self.setFileType("text") + def readSupportedGames(self): pass @@ -37,4 +42,7 @@ class Everleaf(HandHistoryConverter): pass if __name__ == "__main__": - e = Everleaf() + c = Configuration.Config() + e = Everleaf(c, "regression-test-files/everleaf/Speed_Kuala.txt") + print str(e) + diff --git a/pyfpdb/HandHistoryConverter.py b/pyfpdb/HandHistoryConverter.py index 9a62776c..731b2bb5 100644 --- a/pyfpdb/HandHistoryConverter.py +++ b/pyfpdb/HandHistoryConverter.py @@ -16,8 +16,22 @@ #agpl-3.0.txt in the docs folder of the package. class HandHistoryConverter: - def __init__(self): - pass + def __init__(self, config, file): + print "HandHistory init called" + self.c = config + self.sitename = "" + self.obs = "" # One big string + self.filetype = "text" + self.doc = None # For XML based HH files + self.file = file + self.hhbase = self.c.get_import_parameters().get("hhArchiveBase") + + def __str__(self): + tmp = "HandHistoryConverter: '%s'\n" % (self.sitename) + tmp = tmp + "\thhbase: %s\n" % (self.hhbase) + tmp = tmp + "\tfiletype: %s\n" % (self.filetype) + return tmp + # Functions to be implemented in the inheriting class def readSupportedGames(self): abstract def determineGameType(self): abstract @@ -25,11 +39,47 @@ class HandHistoryConverter: def readBlinds(self): abstract def readAction(self): abstract + # Functions not necessary to implement in sub class + def setFileType(self, filetype = "text"): + self.filetype = filetype + + def processFile(self): + self.readFile() + def readFile(self, filename): """Read file""" + if(self.filetype == "text"): + infile=open(filename, "rU") + self.obs = readfile(inputFile) + inputFile.close() + elif(self.filetype == "xml"): + try: + doc = xml.dom.minidom.parse(filename) + self.doc = doc + except: + traceback.print_exc(file=sys.stderr) def writeStars(self): """Write out parsed data""" - +# print sitename + " Game #" + handid + ": " + gametype + " (" + sb + "/" + bb + " - " + starttime +# print "Table '" + tablename + "' " + maxseats + "-max Seat #" + buttonpos + " is the button" +# +# counter = 1 +# for player in seating: +# print "Seat " + counter + ": " + playername + "($" + playermoney + " in chips" +# +# print playername + ": posts small blind " + sb +# print playername + ": posts big blind " + bb +# +# print "*** HOLE CARDS ***" +# print "Dealt to " + hero + " [" + holecards + "]" +# +## ACTION STUFF +# +# print "*** SUMMARY ***" +# print "Total pot $" + totalpot + " | Rake $" + rake +# print "Board [" + boardcards + "]" +# +## SUMMARY STUFF From 328bba2d238d66afb419265205e384359f3c9a87 Mon Sep 17 00:00:00 2001 From: Worros Date: Sun, 9 Nov 2008 10:46:14 +1000 Subject: [PATCH 011/100] Updates - change HH object init call. - override carbon readFile function and hack so minidom can read it --- pyfpdb/CarbonToFpdb.py | 19 +++++++++++++++++-- pyfpdb/EverleafToFpdb.py | 3 ++- pyfpdb/HandHistoryConverter.py | 24 +++++++++++++++++------- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/pyfpdb/CarbonToFpdb.py b/pyfpdb/CarbonToFpdb.py index 0dc7dfaf..8282b3f2 100644 --- a/pyfpdb/CarbonToFpdb.py +++ b/pyfpdb/CarbonToFpdb.py @@ -21,6 +21,7 @@ # Standard Library modules import Configuration import traceback +import sys import xml.dom.minidom from xml.dom.minidom import Node from HandHistoryConverter import HandHistoryConverter @@ -50,8 +51,7 @@ from HandHistoryConverter import HandHistoryConverter class CarbonPoker(HandHistoryConverter): def __init__(self, config, filename): print "Initialising Carbon Poker converter class" - HandHistoryConverter.__init__(self, config, filename) # Call super class init - self.sitename = "Carbon" + HandHistoryConverter.__init__(self, config, filename, "Carbon") # Call super class init self.setFileType("xml") def readSupportedGames(self): @@ -68,8 +68,23 @@ class CarbonPoker(HandHistoryConverter): def readAction(self): pass + # Override read function as xml.minidom barfs on the Carbon layout + # This is pretty dodgy + def readFile(self, filename): + print "Carbon: Reading file: '%s'" %(filename) + infile=open(filename, "rU") + self.obs = infile.read() + infile.close() + self.obs = "\n" + self.obs + "" + try: + doc = xml.dom.minidom.parseString(self.obs) + self.doc = doc + except: + traceback.print_exc(file=sys.stderr) if __name__ == "__main__": c = Configuration.Config() e = CarbonPoker(c, "regression-test-files/carbon-poker/Niagara Falls (15245216).xml") + e.processFile() print str(e) + diff --git a/pyfpdb/EverleafToFpdb.py b/pyfpdb/EverleafToFpdb.py index 515f28d7..fe01c887 100644 --- a/pyfpdb/EverleafToFpdb.py +++ b/pyfpdb/EverleafToFpdb.py @@ -22,7 +22,7 @@ from HandHistoryConverter import HandHistoryConverter class Everleaf(HandHistoryConverter): def __init__(self, config, file): print "Initialising Everleaf converter class" - HandHistoryConverter.__init__(self, config, file) # Call super class init. + HandHistoryConverter.__init__(self, config, file, "Everleaf") # Call super class init. self.sitename = "Everleaf" self.setFileType("text") @@ -44,5 +44,6 @@ class Everleaf(HandHistoryConverter): if __name__ == "__main__": c = Configuration.Config() e = Everleaf(c, "regression-test-files/everleaf/Speed_Kuala.txt") + e.processFile() print str(e) diff --git a/pyfpdb/HandHistoryConverter.py b/pyfpdb/HandHistoryConverter.py index 731b2bb5..6d1bf4a9 100644 --- a/pyfpdb/HandHistoryConverter.py +++ b/pyfpdb/HandHistoryConverter.py @@ -15,21 +15,30 @@ #In the "official" distribution you can find the license in #agpl-3.0.txt in the docs folder of the package. +import Configuration +import sys +import traceback +import xml.dom.minidom +from xml.dom.minidom import Node + class HandHistoryConverter: - def __init__(self, config, file): + def __init__(self, config, file, sitename): print "HandHistory init called" self.c = config - self.sitename = "" + self.sitename = sitename self.obs = "" # One big string self.filetype = "text" self.doc = None # For XML based HH files self.file = file self.hhbase = self.c.get_import_parameters().get("hhArchiveBase") + self.hhdir = self.hhbase + sitename def __str__(self): tmp = "HandHistoryConverter: '%s'\n" % (self.sitename) - tmp = tmp + "\thhbase: %s\n" % (self.hhbase) - tmp = tmp + "\tfiletype: %s\n" % (self.filetype) + tmp = tmp + "\thhbase: '%s'\n" % (self.hhbase) + tmp = tmp + "\thhdir: '%s'\n" % (self.hhdir) + tmp = tmp + "\tfiletype: '%s'\n" % (self.filetype) + tmp = tmp + "\tinfile: '%s'\n" % (self.file) return tmp # Functions to be implemented in the inheriting class @@ -45,14 +54,15 @@ class HandHistoryConverter: self.filetype = filetype def processFile(self): - self.readFile() + self.readFile(self.file) def readFile(self, filename): """Read file""" + print "Reading file: '%s'" %(filename) if(self.filetype == "text"): infile=open(filename, "rU") - self.obs = readfile(inputFile) - inputFile.close() + self.obs = infile.read() + infile.close() elif(self.filetype == "xml"): try: doc = xml.dom.minidom.parse(filename) From f3224f87cca37b4828eec5824d71afc9bd6ba3ca Mon Sep 17 00:00:00 2001 From: Worros Date: Sun, 9 Nov 2008 12:58:46 +1000 Subject: [PATCH 012/100] Add sanity check function, creates ~/.fpdb/HandHistories/ if it doens't exist. Fixes hhbase and hhdir interpretation. --- pyfpdb/HandHistoryConverter.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/pyfpdb/HandHistoryConverter.py b/pyfpdb/HandHistoryConverter.py index 6d1bf4a9..16de1cd9 100644 --- a/pyfpdb/HandHistoryConverter.py +++ b/pyfpdb/HandHistoryConverter.py @@ -18,6 +18,8 @@ import Configuration import sys import traceback +import os +import os.path import xml.dom.minidom from xml.dom.minidom import Node @@ -31,7 +33,8 @@ class HandHistoryConverter: self.doc = None # For XML based HH files self.file = file self.hhbase = self.c.get_import_parameters().get("hhArchiveBase") - self.hhdir = self.hhbase + sitename + self.hhbase = os.path.expanduser(self.hhbase) + self.hhdir = os.path.join(self.hhbase,sitename) def __str__(self): tmp = "HandHistoryConverter: '%s'\n" % (self.sitename) @@ -48,12 +51,35 @@ class HandHistoryConverter: def readBlinds(self): abstract def readAction(self): abstract + def sanityCheck(self): + sane = False + base_w = False + #Check if hhbase exists and is writable + #Note: Will not try to create the base HH directory + if not (os.access(self.hhbase, os.W_OK) and os.path.isdir(self.hhbase)): + print "HH Sanity Check: Directory hhbase '" + self.hhbase + "' doesn't exist or is not writable" + else: + #Check if hhdir exists and is writable + if not os.path.isdir(self.hhdir): + # In first pass, dir may not exist. Attempt to create dir + print "Creating directory: '%s'" % (self.hhdir) + os.mkdir(self.hhdir) + sane = True + elif os.access(self.hhdir, os.W_OK): + sane = True + else: + print "HH Sanity Check: Directory hhdir '" + self.hhdir + "' or its parent directory are not writable" + + return sane # Functions not necessary to implement in sub class def setFileType(self, filetype = "text"): self.filetype = filetype def processFile(self): + if not self.sanityCheck(): + print "Cowardly refusing to continue after failed sanity check" + return self.readFile(self.file) def readFile(self, filename): From 7bc686c8597fbd041b350596debb8fcb97fb2da6 Mon Sep 17 00:00:00 2001 From: Worros Date: Sun, 9 Nov 2008 13:29:58 +1000 Subject: [PATCH 013/100] Add Regex file pokerstars_cash.py from pokerstats package (http://bostik.iki.fi/pokerstats/) Intend to turn into a class plugins can subclass and override the various regexes --- pyfpdb/FpdbRegex.py | 79 ++++++++++++++++++++++++++++++++++ pyfpdb/HandHistoryConverter.py | 3 ++ 2 files changed, 82 insertions(+) create mode 100644 pyfpdb/FpdbRegex.py diff --git a/pyfpdb/FpdbRegex.py b/pyfpdb/FpdbRegex.py new file mode 100644 index 00000000..bf16e864 --- /dev/null +++ b/pyfpdb/FpdbRegex.py @@ -0,0 +1,79 @@ +# pokerstars_cash.py +# -*- coding: iso-8859-15 +# +# PokerStats, an online poker statistics tracking software for Linux +# Copyright (C) 2007-2008 Mika Boström +# +# 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, version 3 of the License. +# +from errors import * +import re +import regex + +# These are PokerStars specific; +# More importantly, they are currently valid for cash game only. +##### +# XXX: There was a weird problem with saved hand histories in PokerStars +# client 2.491; if a user was present on the table (and thus anywhere in +# the hand history), with non-standard characters in their username, the +# client would prepend a literal Ctrl-P (ASCII 16, 0x10) character to +# the hand history title line. Hence, to allow these strangely saved +# hands to be parsed and imported, there is a conditional "one extra +# character" allowed at the start of the new hand regex. +__NEW_HAND_REGEX='^.?PokerStars Game #\d+:\s+Hold\'em' +__HAND_INFO_REGEX='^.*#(\d+):\s+(\S+)\s([\s\S]+)\s\(\$?([.0-9]+)/\$?([.0-9]+)\)\s-\s(\S+)\s-?\s?(\S+)\s\(?(\w+)\)?' +__TABLE_INFO_REGEX='^\S+\s+\'.*\'\s+(\d+)-max\s+Seat\s#(\d+)' +__PLAYER_INFO_REGEX='^Seat\s(\d+):\s(.*)\s\(\$?([.\d]+)\s' +__POST_SB_REGEX='^(.*):\sposts small blind' +__POST_BB_REGEX='^(.*):\sposts big blind' +__POST_BOTH_REGEX='^(.*):\sposts small & big blinds' +__HAND_STAGE_REGEX='^\*{3}\s(.*)\s\*{3}' +__HOLE_CARD_REGEX='^\*{3}\sHOLE CARDS' +__FLOP_CARD_REGEX='^\*{3}\sFLOP\s\*{3}\s\[(\S{2})\s(\S{2})\s(\S{2})\]' +__TURN_CARD_REGEX='^\*{3}\sTURN\s\*{3}\s\[\S{2}\s\S{2}\s\S{2}\]\s\[(\S{2})\]' +__RIVER_CARD_REGEX='^\*{3}\sRIVER\s\*{3}\s\[\S{2}\s\S{2}\s\S{2}\s\S{2}\]\s\[(\S{2})\]' +__SHOWDOWN_REGEX='^\*{3}\sSHOW DOWN' +__SUMMARY_REGEX='^\*{3}\sSUMMARY' +__UNCALLED_BET_REGEX='^Uncalled bet \(\$([.\d]+)\) returned to (.*)' +__POT_AND_RAKE_REGEX='^Total\spot\s\$([.\d]+).*\|\sRake\s\$([.\d]+)' +__COLLECT_POT_REGEX='^(.*)\scollected\s\$([.\d]+)\sfrom\s((main|side)\s)?pot' +__POCKET_CARDS_REGEX='^Dealt\sto\s(.*)\s\[(\S{2})\s(\S{2})\]' +__SHOWN_CARDS_REGEX='^(.*):\sshows\s\[(\S{2})\s(\S{2})\]' +__ACTION_STEP_REGEX='^(.*):\s(bets|checks|raises|calls|folds)((\s\$([.\d]+))?(\sto\s\$([.\d]+))?)?' + +__SHOWDOWN_ACTION_REGEX='^(.*):\s(shows|mucks)' +__SUMMARY_CARDS_REGEX='^Seat\s\d+:\s(.*)\s(showed|mucked)\s\[(\S{2})\s(\S{2})\]' +__SUMMARY_CARDS_EXTRA_REGEX='^Seat\s\d+:\s(.*)\s(\(.*\)\s)(showed|mucked)\s\[(\S{2})\s(\S{2})\]' + + +def regexes(): + m = regex.RegexMatch() +### Compile the regexes + m.hand_start_re = re.compile(__NEW_HAND_REGEX) + m.hand_info_re = re.compile(__HAND_INFO_REGEX) + m.table_info_re = re.compile(__TABLE_INFO_REGEX) + m.player_info_re = re.compile(__PLAYER_INFO_REGEX) + m.small_blind_re = re.compile(__POST_SB_REGEX) + m.big_blind_re = re.compile(__POST_BB_REGEX) + m.both_blinds_re = re.compile(__POST_BOTH_REGEX) + m.hand_stage_re = re.compile(__HAND_STAGE_REGEX) + m.hole_cards_re = re.compile(__HOLE_CARD_REGEX) + m.flop_cards_re = re.compile(__FLOP_CARD_REGEX) + m.turn_card_re = re.compile(__TURN_CARD_REGEX) + m.river_card_re = re.compile(__RIVER_CARD_REGEX) + m.showdown_re = re.compile(__SHOWDOWN_REGEX) + m.summary_re = re.compile(__SUMMARY_REGEX) + m.uncalled_bet_re = re.compile(__UNCALLED_BET_REGEX) + m.collect_pot_re = re.compile(__COLLECT_POT_REGEX) + m.pocket_cards_re = re.compile(__POCKET_CARDS_REGEX) + m.cards_shown_re = re.compile(__SHOWN_CARDS_REGEX) + m.summary_cards_re = re.compile(__SUMMARY_CARDS_REGEX) + m.summary_cards_extra_re = re.compile(__SUMMARY_CARDS_EXTRA_REGEX) + m.action_re = re.compile(__ACTION_STEP_REGEX) + m.rake_re = re.compile(__POT_AND_RAKE_REGEX) + m.showdown_action_re = re.compile(__SHOWDOWN_ACTION_REGEX) +### + return m + diff --git a/pyfpdb/HandHistoryConverter.py b/pyfpdb/HandHistoryConverter.py index 16de1cd9..6261637b 100644 --- a/pyfpdb/HandHistoryConverter.py +++ b/pyfpdb/HandHistoryConverter.py @@ -35,6 +35,7 @@ class HandHistoryConverter: self.hhbase = self.c.get_import_parameters().get("hhArchiveBase") self.hhbase = os.path.expanduser(self.hhbase) self.hhdir = os.path.join(self.hhbase,sitename) +# self.ofile = os.path.join(self.hhdir,file) def __str__(self): tmp = "HandHistoryConverter: '%s'\n" % (self.sitename) @@ -42,6 +43,7 @@ class HandHistoryConverter: tmp = tmp + "\thhdir: '%s'\n" % (self.hhdir) tmp = tmp + "\tfiletype: '%s'\n" % (self.filetype) tmp = tmp + "\tinfile: '%s'\n" % (self.file) +# tmp = tmp + "\toutfile: '%s'\n" % (self.ofile) return tmp # Functions to be implemented in the inheriting class @@ -81,6 +83,7 @@ class HandHistoryConverter: print "Cowardly refusing to continue after failed sanity check" return self.readFile(self.file) + self.determineGameType() def readFile(self, filename): """Read file""" From d6706a5bdf28518446f41744f98b713a4f08451f Mon Sep 17 00:00:00 2001 From: Worros Date: Sun, 9 Nov 2008 14:00:39 +1000 Subject: [PATCH 014/100] First pass at turning file from pokerstats into class --- pyfpdb/FpdbRegex.py | 182 +++++++++++++++++++++++++++++++------------- 1 file changed, 130 insertions(+), 52 deletions(-) diff --git a/pyfpdb/FpdbRegex.py b/pyfpdb/FpdbRegex.py index bf16e864..ffe3a03b 100644 --- a/pyfpdb/FpdbRegex.py +++ b/pyfpdb/FpdbRegex.py @@ -8,6 +8,9 @@ # it under the terms of the GNU General Public License as published by # the Free Software Foundation, version 3 of the License. # + +# Modified for use in fpdb by Carl Gherardi + from errors import * import re import regex @@ -22,58 +25,133 @@ import regex # the hand history title line. Hence, to allow these strangely saved # hands to be parsed and imported, there is a conditional "one extra # character" allowed at the start of the new hand regex. -__NEW_HAND_REGEX='^.?PokerStars Game #\d+:\s+Hold\'em' -__HAND_INFO_REGEX='^.*#(\d+):\s+(\S+)\s([\s\S]+)\s\(\$?([.0-9]+)/\$?([.0-9]+)\)\s-\s(\S+)\s-?\s?(\S+)\s\(?(\w+)\)?' -__TABLE_INFO_REGEX='^\S+\s+\'.*\'\s+(\d+)-max\s+Seat\s#(\d+)' -__PLAYER_INFO_REGEX='^Seat\s(\d+):\s(.*)\s\(\$?([.\d]+)\s' -__POST_SB_REGEX='^(.*):\sposts small blind' -__POST_BB_REGEX='^(.*):\sposts big blind' -__POST_BOTH_REGEX='^(.*):\sposts small & big blinds' -__HAND_STAGE_REGEX='^\*{3}\s(.*)\s\*{3}' -__HOLE_CARD_REGEX='^\*{3}\sHOLE CARDS' -__FLOP_CARD_REGEX='^\*{3}\sFLOP\s\*{3}\s\[(\S{2})\s(\S{2})\s(\S{2})\]' -__TURN_CARD_REGEX='^\*{3}\sTURN\s\*{3}\s\[\S{2}\s\S{2}\s\S{2}\]\s\[(\S{2})\]' -__RIVER_CARD_REGEX='^\*{3}\sRIVER\s\*{3}\s\[\S{2}\s\S{2}\s\S{2}\s\S{2}\]\s\[(\S{2})\]' -__SHOWDOWN_REGEX='^\*{3}\sSHOW DOWN' -__SUMMARY_REGEX='^\*{3}\sSUMMARY' -__UNCALLED_BET_REGEX='^Uncalled bet \(\$([.\d]+)\) returned to (.*)' -__POT_AND_RAKE_REGEX='^Total\spot\s\$([.\d]+).*\|\sRake\s\$([.\d]+)' -__COLLECT_POT_REGEX='^(.*)\scollected\s\$([.\d]+)\sfrom\s((main|side)\s)?pot' -__POCKET_CARDS_REGEX='^Dealt\sto\s(.*)\s\[(\S{2})\s(\S{2})\]' -__SHOWN_CARDS_REGEX='^(.*):\sshows\s\[(\S{2})\s(\S{2})\]' -__ACTION_STEP_REGEX='^(.*):\s(bets|checks|raises|calls|folds)((\s\$([.\d]+))?(\sto\s\$([.\d]+))?)?' - -__SHOWDOWN_ACTION_REGEX='^(.*):\s(shows|mucks)' -__SUMMARY_CARDS_REGEX='^Seat\s\d+:\s(.*)\s(showed|mucked)\s\[(\S{2})\s(\S{2})\]' -__SUMMARY_CARDS_EXTRA_REGEX='^Seat\s\d+:\s(.*)\s(\(.*\)\s)(showed|mucked)\s\[(\S{2})\s(\S{2})\]' -def regexes(): - m = regex.RegexMatch() -### Compile the regexes - m.hand_start_re = re.compile(__NEW_HAND_REGEX) - m.hand_info_re = re.compile(__HAND_INFO_REGEX) - m.table_info_re = re.compile(__TABLE_INFO_REGEX) - m.player_info_re = re.compile(__PLAYER_INFO_REGEX) - m.small_blind_re = re.compile(__POST_SB_REGEX) - m.big_blind_re = re.compile(__POST_BB_REGEX) - m.both_blinds_re = re.compile(__POST_BOTH_REGEX) - m.hand_stage_re = re.compile(__HAND_STAGE_REGEX) - m.hole_cards_re = re.compile(__HOLE_CARD_REGEX) - m.flop_cards_re = re.compile(__FLOP_CARD_REGEX) - m.turn_card_re = re.compile(__TURN_CARD_REGEX) - m.river_card_re = re.compile(__RIVER_CARD_REGEX) - m.showdown_re = re.compile(__SHOWDOWN_REGEX) - m.summary_re = re.compile(__SUMMARY_REGEX) - m.uncalled_bet_re = re.compile(__UNCALLED_BET_REGEX) - m.collect_pot_re = re.compile(__COLLECT_POT_REGEX) - m.pocket_cards_re = re.compile(__POCKET_CARDS_REGEX) - m.cards_shown_re = re.compile(__SHOWN_CARDS_REGEX) - m.summary_cards_re = re.compile(__SUMMARY_CARDS_REGEX) - m.summary_cards_extra_re = re.compile(__SUMMARY_CARDS_EXTRA_REGEX) - m.action_re = re.compile(__ACTION_STEP_REGEX) - m.rake_re = re.compile(__POT_AND_RAKE_REGEX) - m.showdown_action_re = re.compile(__SHOWDOWN_ACTION_REGEX) -### - return m +class FpdbRegex: + def __init__(self): + __NEW_HAND_REGEX='^.?PokerStars Game #\d+:\s+Hold\'em' + __HAND_INFO_REGEX='^.*#(\d+):\s+(\S+)\s([\s\S]+)\s\(\$?([.0-9]+)/\$?([.0-9]+)\)\s-\s(\S+)\s-?\s?(\S+)\s\(?(\w+)\)?' + __TABLE_INFO_REGEX='^\S+\s+\'.*\'\s+(\d+)-max\s+Seat\s#(\d+)' + __PLAYER_INFO_REGEX='^Seat\s(\d+):\s(.*)\s\(\$?([.\d]+)\s' + __POST_SB_REGEX='^(.*):\sposts small blind' + __POST_BB_REGEX='^(.*):\sposts big blind' + __POST_BOTH_REGEX='^(.*):\sposts small & big blinds' + __HAND_STAGE_REGEX='^\*{3}\s(.*)\s\*{3}' + __HOLE_CARD_REGEX='^\*{3}\sHOLE CARDS' + __FLOP_CARD_REGEX='^\*{3}\sFLOP\s\*{3}\s\[(\S{2})\s(\S{2})\s(\S{2})\]' + __TURN_CARD_REGEX='^\*{3}\sTURN\s\*{3}\s\[\S{2}\s\S{2}\s\S{2}\]\s\[(\S{2})\]' + __RIVER_CARD_REGEX='^\*{3}\sRIVER\s\*{3}\s\[\S{2}\s\S{2}\s\S{2}\s\S{2}\]\s\[(\S{2})\]' + __SHOWDOWN_REGEX='^\*{3}\sSHOW DOWN' + __SUMMARY_REGEX='^\*{3}\sSUMMARY' + __UNCALLED_BET_REGEX='^Uncalled bet \(\$([.\d]+)\) returned to (.*)' + __POT_AND_RAKE_REGEX='^Total\spot\s\$([.\d]+).*\|\sRake\s\$([.\d]+)' + __COLLECT_POT_REGEX='^(.*)\scollected\s\$([.\d]+)\sfrom\s((main|side)\s)?pot' + __POCKET_CARDS_REGEX='^Dealt\sto\s(.*)\s\[(\S{2})\s(\S{2})\]' + __SHOWN_CARDS_REGEX='^(.*):\sshows\s\[(\S{2})\s(\S{2})\]' + __ACTION_STEP_REGEX='^(.*):\s(bets|checks|raises|calls|folds)((\s\$([.\d]+))?(\sto\s\$([.\d]+))?)?' + + __SHOWDOWN_ACTION_REGEX='^(.*):\s(shows|mucks)' + __SUMMARY_CARDS_REGEX='^Seat\s\d+:\s(.*)\s(showed|mucked)\s\[(\S{2})\s(\S{2})\]' + __SUMMARY_CARDS_EXTRA_REGEX='^Seat\s\d+:\s(.*)\s(\(.*\)\s)(showed|mucked)\s\[(\S{2})\s(\S{2})\]' + self.m = regex.RegexMatch() + + def getRegexes(): + return self.m + + def compileRegexes(): + ### Compile the regexes + m.hand_start_re = re.compile(__NEW_HAND_REGEX) + m.hand_info_re = re.compile(__HAND_INFO_REGEX) + m.table_info_re = re.compile(__TABLE_INFO_REGEX) + m.player_info_re = re.compile(__PLAYER_INFO_REGEX) + m.small_blind_re = re.compile(__POST_SB_REGEX) + m.big_blind_re = re.compile(__POST_BB_REGEX) + m.both_blinds_re = re.compile(__POST_BOTH_REGEX) + m.hand_stage_re = re.compile(__HAND_STAGE_REGEX) + m.hole_cards_re = re.compile(__HOLE_CARD_REGEX) + m.flop_cards_re = re.compile(__FLOP_CARD_REGEX) + m.turn_card_re = re.compile(__TURN_CARD_REGEX) + m.river_card_re = re.compile(__RIVER_CARD_REGEX) + m.showdown_re = re.compile(__SHOWDOWN_REGEX) + m.summary_re = re.compile(__SUMMARY_REGEX) + m.uncalled_bet_re = re.compile(__UNCALLED_BET_REGEX) + m.collect_pot_re = re.compile(__COLLECT_POT_REGEX) + m.pocket_cards_re = re.compile(__POCKET_CARDS_REGEX) + m.cards_shown_re = re.compile(__SHOWN_CARDS_REGEX) + m.summary_cards_re = re.compile(__SUMMARY_CARDS_REGEX) + m.summary_cards_extra_re = re.compile(__SUMMARY_CARDS_EXTRA_REGEX) + m.action_re = re.compile(__ACTION_STEP_REGEX) + m.rake_re = re.compile(__POT_AND_RAKE_REGEX) + m.showdown_action_re = re.compile(__SHOWDOWN_ACTION_REGEX) + + # Set methods for plugins to override + + def setNewHandRegex(self, string): + __NEW_HAND_REGEX = string + + def setHandInfoRegex(self, string): + __HAND_INFO_REGEX = string + + def setTableInfoRegex(self, string): + __TABLE_INFO_REGEX = string + + def setPlayerInfoRegex(self, string): + __PLAYER_INFO_REGEX = string + + def setPostSbRegex(self, string): + __POST_SB_REGEX = string + + def setPostBbRegex(self, string): + __POST_BB_REGEX = string + + def setPostBothRegex(self, string): + __POST_BOTH_REGEX = string + + def setHandStageRegex(self, string): + __HAND_STAGE_REGEX = string + + def setHoleCardRegex(self, string): + __HOLE_CARD_REGEX = string + + def setFlopCardRegex(self, string): + __FLOP_CARD_REGEX = string + + def setTurnCardRegex(self, string): + __TURN_CARD_REGEX = string + + def setRiverCardRegex(self, string): + __RIVER_CARD_REGEX = string + + def setShowdownRegex(self, string): + __SHOWDOWN_REGEX = string + + def setSummaryRegex(self, string): + __SUMMARY_REGEX = string + + def setUncalledBetRegex(self, string): + __UNCALLED_BET_REGEX = string + + def setCollectPotRegex(self, string): + __COLLECT_POT_REGEX = string + + def setPocketCardsRegex(self, string): + __POCKET_CARDS_REGEX = string + + def setShownCardsRegex(self, string): + __SHOWN_CARDS_REGEX = string + + def setSummaryCardsRegex(self, string): + __SUMMARY_CARDS_REGEX = string + + def setSummaryCardsExtraRegex(self, string): + __SUMMARY_CARDS_EXTRA_REGEX = string + + def setActionStepRegex(self, string): + __ACTION_STEP_REGEX = string + + def setPotAndRakeRegex(self, string): + __POT_AND_RAKE_REGEX = string + + def setShowdownActionRegex(self, string): + __SHOWDOWN_ACTION_REGEX = string From 669382aa0179bcb5a23138476024882f59fab9ed Mon Sep 17 00:00:00 2001 From: Worros Date: Sun, 9 Nov 2008 15:30:57 +1000 Subject: [PATCH 015/100] Remove errors import --- pyfpdb/FpdbRegex.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyfpdb/FpdbRegex.py b/pyfpdb/FpdbRegex.py index ffe3a03b..cc5f3ae9 100644 --- a/pyfpdb/FpdbRegex.py +++ b/pyfpdb/FpdbRegex.py @@ -11,7 +11,6 @@ # Modified for use in fpdb by Carl Gherardi -from errors import * import re import regex From 43f8620deaee932e8170702d4e6f1fa80c2aa99c Mon Sep 17 00:00:00 2001 From: Worros Date: Sun, 9 Nov 2008 15:33:42 +1000 Subject: [PATCH 016/100] Remove FpdbRegex until functional --- pyfpdb/HandHistoryConverter.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pyfpdb/HandHistoryConverter.py b/pyfpdb/HandHistoryConverter.py index 6261637b..6b7c8a76 100644 --- a/pyfpdb/HandHistoryConverter.py +++ b/pyfpdb/HandHistoryConverter.py @@ -16,6 +16,7 @@ #agpl-3.0.txt in the docs folder of the package. import Configuration +#import FpdbRegex import sys import traceback import os @@ -35,6 +36,7 @@ class HandHistoryConverter: self.hhbase = self.c.get_import_parameters().get("hhArchiveBase") self.hhbase = os.path.expanduser(self.hhbase) self.hhdir = os.path.join(self.hhbase,sitename) + self.gametype = [] # self.ofile = os.path.join(self.hhdir,file) def __str__(self): @@ -44,10 +46,19 @@ class HandHistoryConverter: tmp = tmp + "\tfiletype: '%s'\n" % (self.filetype) tmp = tmp + "\tinfile: '%s'\n" % (self.file) # tmp = tmp + "\toutfile: '%s'\n" % (self.ofile) + tmp = tmp + "\tgametype: '%s'\n" % (self.gametype[0]) + tmp = tmp + "\tgamebase: '%s'\n" % (self.gametype[1]) + tmp = tmp + "\tlimit: '%s'\n" % (self.gametype[2]) + tmp = tmp + "\tsb/bb: '%s'\n" % (self.gametype[3], self.gametype[4]) return tmp # Functions to be implemented in the inheriting class def readSupportedGames(self): abstract + + # should return a list + # type base limit + # [ ring, hold, nl , sb, bb ] + # Valid types specified in docs/tabledesign.html in Gametypes def determineGameType(self): abstract def readPlayerStacks(self): abstract def readBlinds(self): abstract @@ -83,7 +94,7 @@ class HandHistoryConverter: print "Cowardly refusing to continue after failed sanity check" return self.readFile(self.file) - self.determineGameType() + gametype = self.determineGameType() def readFile(self, filename): """Read file""" From c53d78491ae31de50de5135f575240e2e277069b Mon Sep 17 00:00:00 2001 From: Worros Date: Sun, 9 Nov 2008 16:27:27 +1000 Subject: [PATCH 017/100] Minor update to Carbon poker - read gametype + sb/bb --- pyfpdb/CarbonToFpdb.py | 29 ++++++++++++++++++++++++----- pyfpdb/HandHistoryConverter.py | 19 +++++++++++++++++-- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/pyfpdb/CarbonToFpdb.py b/pyfpdb/CarbonToFpdb.py index 8282b3f2..cf9fc8d3 100644 --- a/pyfpdb/CarbonToFpdb.py +++ b/pyfpdb/CarbonToFpdb.py @@ -22,6 +22,7 @@ import Configuration import traceback import sys +import re import xml.dom.minidom from xml.dom.minidom import Node from HandHistoryConverter import HandHistoryConverter @@ -56,11 +57,29 @@ class CarbonPoker(HandHistoryConverter): def readSupportedGames(self): pass - def determineGameType(self): - desc_node = doc.getElementsByTagName("description") - type = desc_node.getAttribute("type") - stakes = desc_node.getAttribute("stakes") - + def determineGameType(self): + gametype = [] + desc_node = self.doc.getElementsByTagName("description") + #TODO: no examples of non ring type yet + gametype = gametype + ["ring"] + type = desc_node[0].getAttribute("type") + if(type == "Holdem"): + gametype = gametype + ["hold"] + else: + print "Unknown gametype: '%s'" % (type) + + stakes = desc_node[0].getAttribute("stakes") + #TODO: no examples of anything except nlhe + m = re.match('(?PNo Limit)\s\(\$?(?P[.0-9]+)/\$?(?P[.0-9]+)\)', stakes) + + if(m.group('LIMIT') == "No Limit"): + gametype = gametype + ["nl"] + + gametype = gametype + [self.float2int(m.group('SB'))] + gametype = gametype + [self.float2int(m.group('BB'))] + + return gametype + def readPlayerStacks(self): pass def readBlinds(self): diff --git a/pyfpdb/HandHistoryConverter.py b/pyfpdb/HandHistoryConverter.py index 6b7c8a76..ec1308cd 100644 --- a/pyfpdb/HandHistoryConverter.py +++ b/pyfpdb/HandHistoryConverter.py @@ -49,7 +49,7 @@ class HandHistoryConverter: tmp = tmp + "\tgametype: '%s'\n" % (self.gametype[0]) tmp = tmp + "\tgamebase: '%s'\n" % (self.gametype[1]) tmp = tmp + "\tlimit: '%s'\n" % (self.gametype[2]) - tmp = tmp + "\tsb/bb: '%s'\n" % (self.gametype[3], self.gametype[4]) + tmp = tmp + "\tsb/bb: '%s/%s'\n" % (self.gametype[3], self.gametype[4]) return tmp # Functions to be implemented in the inheriting class @@ -94,7 +94,7 @@ class HandHistoryConverter: print "Cowardly refusing to continue after failed sanity check" return self.readFile(self.file) - gametype = self.determineGameType() + self.gametype = self.determineGameType() def readFile(self, filename): """Read file""" @@ -133,3 +133,18 @@ class HandHistoryConverter: # ## SUMMARY STUFF +#takes a poker float (including , for thousand seperator and converts it to an int + def float2int (self, string): + pos=string.find(",") + if (pos!=-1): #remove , the thousand seperator + string=string[0:pos]+string[pos+1:] + + pos=string.find(".") + if (pos!=-1): #remove decimal point + string=string[0:pos]+string[pos+1:] + + result = int(string) + if pos==-1: #no decimal point - was in full dollars - need to multiply with 100 + result*=100 + return result +#end def float2int From d0218363c6fad08d277bf76900692acb5da76aa2 Mon Sep 17 00:00:00 2001 From: Worros Date: Sun, 9 Nov 2008 20:57:42 +1000 Subject: [PATCH 018/100] Fix GuiAutoImport startup after I deleted the default setting in a previous patch. Setting now comes from config --- pyfpdb/GuiAutoImport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyfpdb/GuiAutoImport.py b/pyfpdb/GuiAutoImport.py index f8f4d8c1..9e65f6b6 100644 --- a/pyfpdb/GuiAutoImport.py +++ b/pyfpdb/GuiAutoImport.py @@ -60,7 +60,7 @@ class GuiAutoImport (threading.Thread): self.intervalLabel.show() self.intervalEntry=gtk.Entry() - self.intervalEntry.set_text(str(self.settings['hud-defaultInterval'])) + self.intervalEntry.set_text(str(self.config.get_import_parameters().get("interval"))) self.settingsHBox.pack_start(self.intervalEntry) self.intervalEntry.show() From d4038c3f192c6d08cd2bc4dea5bac67cc62cea8b Mon Sep 17 00:00:00 2001 From: Worros Date: Sun, 9 Nov 2008 21:57:58 +1000 Subject: [PATCH 019/100] Lots of changes. Fixes the hud for auto-import - at least for me. Does configer Importer to use the Config class. --- pyfpdb/CliFpdb.py | 2 +- pyfpdb/Configuration.py | 4 +- pyfpdb/EverleafToFpdb.py | 44 +++++++++- pyfpdb/FpdbRegex.py | 150 ++++++++++++++++----------------- pyfpdb/GuiAutoImport.py | 9 +- pyfpdb/GuiBulkImport.py | 2 +- pyfpdb/HandHistoryConverter.py | 33 +++++++- pyfpdb/fpdb_import.py | 10 +-- 8 files changed, 162 insertions(+), 92 deletions(-) diff --git a/pyfpdb/CliFpdb.py b/pyfpdb/CliFpdb.py index 47649728..4e2a4361 100755 --- a/pyfpdb/CliFpdb.py +++ b/pyfpdb/CliFpdb.py @@ -53,7 +53,7 @@ if __name__ == "__main__": (options, sys.argv) = parser.parse_args() - settings={'imp-callFpdbHud':False, 'db-backend':2} + settings={'callFpdbHud':False, 'db-backend':2} settings['db-host']=options.server settings['db-user']=options.user settings['db-password']=options.password diff --git a/pyfpdb/Configuration.py b/pyfpdb/Configuration.py index 0c88e2fd..b587d7b0 100755 --- a/pyfpdb/Configuration.py +++ b/pyfpdb/Configuration.py @@ -443,8 +443,8 @@ class Config: imp['interval'] = self.interval imp['hhArchiveBase'] = self.hhArchiveBase except: # Default params - imp['callFpdbHud'] = 10 - imp['interval'] = True + imp['callFpdbHud'] = True + imp['interval'] = 10 imp['hhArchiveBase'] = "~/.fpdb/HandHistories/" return imp diff --git a/pyfpdb/EverleafToFpdb.py b/pyfpdb/EverleafToFpdb.py index fe01c887..b81117c4 100644 --- a/pyfpdb/EverleafToFpdb.py +++ b/pyfpdb/EverleafToFpdb.py @@ -17,7 +17,41 @@ ######################################################################## import Configuration -from HandHistoryConverter import HandHistoryConverter +from HandHistoryConverter import * + +# Everleaf HH format + +#Everleaf Gaming Game #55198191 +#***** Hand history for game #55198191 ***** +#Blinds $0.50/$1 NL Hold'em - 2008/09/01 - 10:02:11 +#Table Speed Kuala +#Seat 8 is the button +#Total number of players: 10 +#Seat 1: spicybum ( $ 77.50 USD ) +#Seat 2: harrydebeng ( new player ) +#Seat 3: EricBlade ( new player ) +#Seat 4: dollar_hecht ( $ 16.40 USD ) +#Seat 5: Apolon76 ( $ 154.10 USD ) +#Seat 6: dogge ( new player ) +#Seat 7: RonKoro ( $ 25.53 USD ) +#Seat 8: jay68w ( $ 48.50 USD ) +#Seat 9: KillerQueen1 ( $ 51.28 USD ) +#Seat 10: Cheburashka ( $ 49.61 USD ) +#KillerQueen1: posts small blind [$ 0.50 USD] +#Cheburashka: posts big blind [$ 1 USD] +#** Dealing down cards ** +#spicybum folds +#dollar_hecht calls [$ 1 USD] +#Apolon76 folds +#RonKoro folds +#jay68w raises [$ 4.50 USD] +#KillerQueen1 folds +#Cheburashka folds +#dollar_hecht folds +#jay68w does not show cards +#jay68w wins $ 3.50 USD from main pot + + class Everleaf(HandHistoryConverter): def __init__(self, config, file): @@ -25,12 +59,16 @@ class Everleaf(HandHistoryConverter): HandHistoryConverter.__init__(self, config, file, "Everleaf") # Call super class init. self.sitename = "Everleaf" self.setFileType("text") + self.rexx.setSplitHandRegex("\n\n\n") + self.rexx.compileRegexes() def readSupportedGames(self): pass def determineGameType(self): - pass + gametype = ["ring", "hold", "nl"] + + return gametype def readPlayerStacks(self): pass @@ -45,5 +83,5 @@ if __name__ == "__main__": c = Configuration.Config() e = Everleaf(c, "regression-test-files/everleaf/Speed_Kuala.txt") e.processFile() - print str(e) +# print str(e) diff --git a/pyfpdb/FpdbRegex.py b/pyfpdb/FpdbRegex.py index cc5f3ae9..f287c2f6 100644 --- a/pyfpdb/FpdbRegex.py +++ b/pyfpdb/FpdbRegex.py @@ -12,7 +12,6 @@ # Modified for use in fpdb by Carl Gherardi import re -import regex # These are PokerStars specific; # More importantly, they are currently valid for cash game only. @@ -28,129 +27,130 @@ import regex class FpdbRegex: def __init__(self): - __NEW_HAND_REGEX='^.?PokerStars Game #\d+:\s+Hold\'em' - __HAND_INFO_REGEX='^.*#(\d+):\s+(\S+)\s([\s\S]+)\s\(\$?([.0-9]+)/\$?([.0-9]+)\)\s-\s(\S+)\s-?\s?(\S+)\s\(?(\w+)\)?' - __TABLE_INFO_REGEX='^\S+\s+\'.*\'\s+(\d+)-max\s+Seat\s#(\d+)' - __PLAYER_INFO_REGEX='^Seat\s(\d+):\s(.*)\s\(\$?([.\d]+)\s' - __POST_SB_REGEX='^(.*):\sposts small blind' - __POST_BB_REGEX='^(.*):\sposts big blind' - __POST_BOTH_REGEX='^(.*):\sposts small & big blinds' - __HAND_STAGE_REGEX='^\*{3}\s(.*)\s\*{3}' - __HOLE_CARD_REGEX='^\*{3}\sHOLE CARDS' - __FLOP_CARD_REGEX='^\*{3}\sFLOP\s\*{3}\s\[(\S{2})\s(\S{2})\s(\S{2})\]' - __TURN_CARD_REGEX='^\*{3}\sTURN\s\*{3}\s\[\S{2}\s\S{2}\s\S{2}\]\s\[(\S{2})\]' - __RIVER_CARD_REGEX='^\*{3}\sRIVER\s\*{3}\s\[\S{2}\s\S{2}\s\S{2}\s\S{2}\]\s\[(\S{2})\]' - __SHOWDOWN_REGEX='^\*{3}\sSHOW DOWN' - __SUMMARY_REGEX='^\*{3}\sSUMMARY' - __UNCALLED_BET_REGEX='^Uncalled bet \(\$([.\d]+)\) returned to (.*)' - __POT_AND_RAKE_REGEX='^Total\spot\s\$([.\d]+).*\|\sRake\s\$([.\d]+)' - __COLLECT_POT_REGEX='^(.*)\scollected\s\$([.\d]+)\sfrom\s((main|side)\s)?pot' - __POCKET_CARDS_REGEX='^Dealt\sto\s(.*)\s\[(\S{2})\s(\S{2})\]' - __SHOWN_CARDS_REGEX='^(.*):\sshows\s\[(\S{2})\s(\S{2})\]' - __ACTION_STEP_REGEX='^(.*):\s(bets|checks|raises|calls|folds)((\s\$([.\d]+))?(\sto\s\$([.\d]+))?)?' + self.__SPLIT_HAND_REGEX='\n\n\n' + self.__NEW_HAND_REGEX='^.?PokerStars Game #\d+:\s+Hold\'em' + self.__HAND_INFO_REGEX='^.*#(\d+):\s+(\S+)\s([\s\S]+)\s\(\$?([.0-9]+)/\$?([.0-9]+)\)\s-\s(\S+)\s-?\s?(\S+)\s\(?(\w+)\)?' + self.__TABLE_INFO_REGEX='^\S+\s+\'.*\'\s+(\d+)-max\s+Seat\s#(\d+)' + self.__PLAYER_INFO_REGEX='^Seat\s(\d+):\s(.*)\s\(\$?([.\d]+)\s' + self.__POST_SB_REGEX='^(.*):\sposts small blind' + self.__POST_BB_REGEX='^(.*):\sposts big blind' + self.__POST_BOTH_REGEX='^(.*):\sposts small & big blinds' + self.__HAND_STAGE_REGEX='^\*{3}\s(.*)\s\*{3}' + self.__HOLE_CARD_REGEX='^\*{3}\sHOLE CARDS' + self.__FLOP_CARD_REGEX='^\*{3}\sFLOP\s\*{3}\s\[(\S{2})\s(\S{2})\s(\S{2})\]' + self.__TURN_CARD_REGEX='^\*{3}\sTURN\s\*{3}\s\[\S{2}\s\S{2}\s\S{2}\]\s\[(\S{2})\]' + self.__RIVER_CARD_REGEX='^\*{3}\sRIVER\s\*{3}\s\[\S{2}\s\S{2}\s\S{2}\s\S{2}\]\s\[(\S{2})\]' + self.__SHOWDOWN_REGEX='^\*{3}\sSHOW DOWN' + self.__SUMMARY_REGEX='^\*{3}\sSUMMARY' + self.__UNCALLED_BET_REGEX='^Uncalled bet \(\$([.\d]+)\) returned to (.*)' + self.__POT_AND_RAKE_REGEX='^Total\spot\s\$([.\d]+).*\|\sRake\s\$([.\d]+)' + self.__COLLECT_POT_REGEX='^(.*)\scollected\s\$([.\d]+)\sfrom\s((main|side)\s)?pot' + self.__POCKET_CARDS_REGEX='^Dealt\sto\s(.*)\s\[(\S{2})\s(\S{2})\]' + self.__SHOWN_CARDS_REGEX='^(.*):\sshows\s\[(\S{2})\s(\S{2})\]' + self.__ACTION_STEP_REGEX='^(.*):\s(bets|checks|raises|calls|folds)((\s\$([.\d]+))?(\sto\s\$([.\d]+))?)?' - __SHOWDOWN_ACTION_REGEX='^(.*):\s(shows|mucks)' - __SUMMARY_CARDS_REGEX='^Seat\s\d+:\s(.*)\s(showed|mucked)\s\[(\S{2})\s(\S{2})\]' - __SUMMARY_CARDS_EXTRA_REGEX='^Seat\s\d+:\s(.*)\s(\(.*\)\s)(showed|mucked)\s\[(\S{2})\s(\S{2})\]' - self.m = regex.RegexMatch() + self.__SHOWDOWN_ACTION_REGEX='^(.*):\s(shows|mucks)' + self.__SUMMARY_CARDS_REGEX='^Seat\s\d+:\s(.*)\s(showed|mucked)\s\[(\S{2})\s(\S{2})\]' + self.__SUMMARY_CARDS_EXTRA_REGEX='^Seat\s\d+:\s(.*)\s(\(.*\)\s)(showed|mucked)\s\[(\S{2})\s(\S{2})\]' - def getRegexes(): - return self.m - - def compileRegexes(): + def compileRegexes(self): ### Compile the regexes - m.hand_start_re = re.compile(__NEW_HAND_REGEX) - m.hand_info_re = re.compile(__HAND_INFO_REGEX) - m.table_info_re = re.compile(__TABLE_INFO_REGEX) - m.player_info_re = re.compile(__PLAYER_INFO_REGEX) - m.small_blind_re = re.compile(__POST_SB_REGEX) - m.big_blind_re = re.compile(__POST_BB_REGEX) - m.both_blinds_re = re.compile(__POST_BOTH_REGEX) - m.hand_stage_re = re.compile(__HAND_STAGE_REGEX) - m.hole_cards_re = re.compile(__HOLE_CARD_REGEX) - m.flop_cards_re = re.compile(__FLOP_CARD_REGEX) - m.turn_card_re = re.compile(__TURN_CARD_REGEX) - m.river_card_re = re.compile(__RIVER_CARD_REGEX) - m.showdown_re = re.compile(__SHOWDOWN_REGEX) - m.summary_re = re.compile(__SUMMARY_REGEX) - m.uncalled_bet_re = re.compile(__UNCALLED_BET_REGEX) - m.collect_pot_re = re.compile(__COLLECT_POT_REGEX) - m.pocket_cards_re = re.compile(__POCKET_CARDS_REGEX) - m.cards_shown_re = re.compile(__SHOWN_CARDS_REGEX) - m.summary_cards_re = re.compile(__SUMMARY_CARDS_REGEX) - m.summary_cards_extra_re = re.compile(__SUMMARY_CARDS_EXTRA_REGEX) - m.action_re = re.compile(__ACTION_STEP_REGEX) - m.rake_re = re.compile(__POT_AND_RAKE_REGEX) - m.showdown_action_re = re.compile(__SHOWDOWN_ACTION_REGEX) + self.split_hand_re = re.compile(self.__SPLIT_HAND_REGEX) + self.hand_start_re = re.compile(self.__NEW_HAND_REGEX) + self.hand_info_re = re.compile(self.__HAND_INFO_REGEX) + self.table_info_re = re.compile(self.__TABLE_INFO_REGEX) + self.player_info_re = re.compile(self.__PLAYER_INFO_REGEX) + self.small_blind_re = re.compile(self.__POST_SB_REGEX) + self.big_blind_re = re.compile(self.__POST_BB_REGEX) + self.both_blinds_re = re.compile(self.__POST_BOTH_REGEX) + self.hand_stage_re = re.compile(self.__HAND_STAGE_REGEX) + self.hole_cards_re = re.compile(self.__HOLE_CARD_REGEX) + self.flop_cards_re = re.compile(self.__FLOP_CARD_REGEX) + self.turn_card_re = re.compile(self.__TURN_CARD_REGEX) + self.river_card_re = re.compile(self.__RIVER_CARD_REGEX) + self.showdown_re = re.compile(self.__SHOWDOWN_REGEX) + self.summary_re = re.compile(self.__SUMMARY_REGEX) + self.uncalled_bet_re = re.compile(self.__UNCALLED_BET_REGEX) + self.collect_pot_re = re.compile(self.__COLLECT_POT_REGEX) + self.pocket_cards_re = re.compile(self.__POCKET_CARDS_REGEX) + self.cards_shown_re = re.compile(self.__SHOWN_CARDS_REGEX) + self.summary_cards_re = re.compile(self.__SUMMARY_CARDS_REGEX) + self.summary_cards_extra_re = re.compile(self.__SUMMARY_CARDS_EXTRA_REGEX) + self.action_re = re.compile(self.__ACTION_STEP_REGEX) + self.rake_re = re.compile(self.__POT_AND_RAKE_REGEX) + self.showdown_action_re = re.compile(self.__SHOWDOWN_ACTION_REGEX) # Set methods for plugins to override + def setSplitHandRegex(self, string): + self.__SPLIT_HAND_REGEX = string + def setNewHandRegex(self, string): - __NEW_HAND_REGEX = string + self.__NEW_HAND_REGEX = string def setHandInfoRegex(self, string): - __HAND_INFO_REGEX = string + self.__HAND_INFO_REGEX = string def setTableInfoRegex(self, string): - __TABLE_INFO_REGEX = string + self.__TABLE_INFO_REGEX = string def setPlayerInfoRegex(self, string): - __PLAYER_INFO_REGEX = string + self.__PLAYER_INFO_REGEX = string def setPostSbRegex(self, string): - __POST_SB_REGEX = string + self.__POST_SB_REGEX = string def setPostBbRegex(self, string): - __POST_BB_REGEX = string + self.__POST_BB_REGEX = string def setPostBothRegex(self, string): - __POST_BOTH_REGEX = string + self.__POST_BOTH_REGEX = string def setHandStageRegex(self, string): - __HAND_STAGE_REGEX = string + self.__HAND_STAGE_REGEX = string def setHoleCardRegex(self, string): - __HOLE_CARD_REGEX = string + self.__HOLE_CARD_REGEX = string def setFlopCardRegex(self, string): - __FLOP_CARD_REGEX = string + self.__FLOP_CARD_REGEX = string def setTurnCardRegex(self, string): - __TURN_CARD_REGEX = string + self.__TURN_CARD_REGEX = string def setRiverCardRegex(self, string): - __RIVER_CARD_REGEX = string + self.__RIVER_CARD_REGEX = string def setShowdownRegex(self, string): - __SHOWDOWN_REGEX = string + self.__SHOWDOWN_REGEX = string def setSummaryRegex(self, string): - __SUMMARY_REGEX = string + self.__SUMMARY_REGEX = string def setUncalledBetRegex(self, string): - __UNCALLED_BET_REGEX = string + self.__UNCALLED_BET_REGEX = string def setCollectPotRegex(self, string): - __COLLECT_POT_REGEX = string + self.__COLLECT_POT_REGEX = string def setPocketCardsRegex(self, string): - __POCKET_CARDS_REGEX = string + self.__POCKET_CARDS_REGEX = string def setShownCardsRegex(self, string): - __SHOWN_CARDS_REGEX = string + self.__SHOWN_CARDS_REGEX = string def setSummaryCardsRegex(self, string): - __SUMMARY_CARDS_REGEX = string + self.__SUMMARY_CARDS_REGEX = string def setSummaryCardsExtraRegex(self, string): - __SUMMARY_CARDS_EXTRA_REGEX = string + self.__SUMMARY_CARDS_EXTRA_REGEX = string def setActionStepRegex(self, string): - __ACTION_STEP_REGEX = string + self.__ACTION_STEP_REGEX = string def setPotAndRakeRegex(self, string): - __POT_AND_RAKE_REGEX = string + self.__POT_AND_RAKE_REGEX = string def setShowdownActionRegex(self, string): - __SHOWDOWN_ACTION_REGEX = string + self.__SHOWDOWN_ACTION_REGEX = string diff --git a/pyfpdb/GuiAutoImport.py b/pyfpdb/GuiAutoImport.py index 9e65f6b6..e33976cf 100644 --- a/pyfpdb/GuiAutoImport.py +++ b/pyfpdb/GuiAutoImport.py @@ -33,9 +33,14 @@ class GuiAutoImport (threading.Thread): self.settings=settings self.config=config + imp = self.config.get_import_parameters() + + print "Import parameters" + print imp + self.input_settings = {} - self.importer = fpdb_import.Importer(self,self.settings) + self.importer = fpdb_import.Importer(self,self.settings, self.config) self.importer.setCallHud(True) self.importer.setMinPrint(30) self.importer.setQuiet(False) @@ -195,7 +200,7 @@ if __name__== "__main__": settings['db-databaseName'] = "fpdb" settings['hud-defaultInterval'] = 10 settings['hud-defaultPath'] = 'C:/Program Files/PokerStars/HandHistory/nutOmatic' - settings['imp-callFpdbHud'] = True + settings['callFpdbHud'] = True i = GuiAutoImport(settings) main_window = gtk.Window() diff --git a/pyfpdb/GuiBulkImport.py b/pyfpdb/GuiBulkImport.py index ee5af2c6..9087cc79 100644 --- a/pyfpdb/GuiBulkImport.py +++ b/pyfpdb/GuiBulkImport.py @@ -80,7 +80,7 @@ class GuiBulkImport (threading.Thread): self.db=db self.settings=settings self.config=config - self.importer = fpdb_import.Importer(self,self.settings) + self.importer = fpdb_import.Importer(self,self.settings, config) self.vbox=gtk.VBox(False,1) self.vbox.show() diff --git a/pyfpdb/HandHistoryConverter.py b/pyfpdb/HandHistoryConverter.py index ec1308cd..181273ce 100644 --- a/pyfpdb/HandHistoryConverter.py +++ b/pyfpdb/HandHistoryConverter.py @@ -16,7 +16,8 @@ #agpl-3.0.txt in the docs folder of the package. import Configuration -#import FpdbRegex +import FpdbRegex +import re import sys import traceback import os @@ -38,6 +39,7 @@ class HandHistoryConverter: self.hhdir = os.path.join(self.hhbase,sitename) self.gametype = [] # self.ofile = os.path.join(self.hhdir,file) + self.rexx = FpdbRegex.FpdbRegex() def __str__(self): tmp = "HandHistoryConverter: '%s'\n" % (self.sitename) @@ -95,6 +97,13 @@ class HandHistoryConverter: return self.readFile(self.file) self.gametype = self.determineGameType() + self.splitFileIntoHands() + + def splitFileIntoHands(self): + hands = [] + list = self.rexx.split_hand_re.split(self.obs) + for l in list: + hands = hands + [Hand(l)] def readFile(self, filename): """Read file""" @@ -110,7 +119,7 @@ class HandHistoryConverter: except: traceback.print_exc(file=sys.stderr) - def writeStars(self): + def writeHand(self, file, hand): """Write out parsed data""" # print sitename + " Game #" + handid + ": " + gametype + " (" + sb + "/" + bb + " - " + starttime # print "Table '" + tablename + "' " + maxseats + "-max Seat #" + buttonpos + " is the button" @@ -148,3 +157,23 @@ class HandHistoryConverter: result*=100 return result #end def float2int + +class Hand: +# def __init__(self, sitename, gametype, sb, bb, string): + def __init__(self, string): +# self.sitename = sitename +# self.gametype = gametype +# self.sb = sb +# self.bb = bb + self.string = string + print string + + self.handid = None + self.tablename = "Slartibartfast" + self.maxseats = 10 + self.counted_seats = 0 + self.buttonpos = 0 + self.seating = [] + self.players = [] + self.action = [] + diff --git a/pyfpdb/fpdb_import.py b/pyfpdb/fpdb_import.py index b505a6ca..0f73e40e 100755 --- a/pyfpdb/fpdb_import.py +++ b/pyfpdb/fpdb_import.py @@ -41,23 +41,22 @@ from time import time class Importer: - def __init__(self, caller, settings): + def __init__(self, caller, settings, config): """Constructor""" self.settings=settings self.caller=caller + self.config = config self.db = None self.cursor = None self.filelist = {} self.dirlist = {} self.monitor = False self.updated = {} #Time last import was run {file:mtime} - self.callHud = False self.lines = None self.faobs = None #File as one big string self.pos_in_file = {} # dict to remember how far we have read in the file #Set defaults - if not self.settings.has_key('imp-callFpdbHud'): - self.settings['imp-callFpdbHud'] = False + self.callHud = self.config.get_import_parameters().get("callFpdbHud") if not self.settings.has_key('minPrint'): self.settings['minPrint'] = 30 self.dbConnect() @@ -237,8 +236,7 @@ class Importer: stored+=1 self.db.commit() -# if settings['imp-callFpdbHud'] and self.callHud and os.sep=='/': - if self.settings['imp-callFpdbHud'] and self.callHud: + if self.callHud: #print "call to HUD here. handsId:",handsId #pipe the Hands.id out to the HUD self.caller.pipe_to_hud.stdin.write("%s" % (handsId) + os.linesep) From 0bbf801d898b1dac64f35eff1aebd290ae36c547 Mon Sep 17 00:00:00 2001 From: Worros Date: Mon, 10 Nov 2008 16:41:04 +1000 Subject: [PATCH 020/100] Bit more Everleaf --- pyfpdb/EverleafToFpdb.py | 10 ++++++++-- pyfpdb/FpdbRegex.py | 5 +++++ pyfpdb/HandHistoryConverter.py | 28 +++++++++++++--------------- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/pyfpdb/EverleafToFpdb.py b/pyfpdb/EverleafToFpdb.py index b81117c4..f09c67d1 100644 --- a/pyfpdb/EverleafToFpdb.py +++ b/pyfpdb/EverleafToFpdb.py @@ -59,14 +59,20 @@ class Everleaf(HandHistoryConverter): HandHistoryConverter.__init__(self, config, file, "Everleaf") # Call super class init. self.sitename = "Everleaf" self.setFileType("text") - self.rexx.setSplitHandRegex("\n\n\n") + self.rexx.setGameInfoRegex('.*Blinds \$?(?P[.0-9]+)/\$?(?P[.0-9]+)') + self.rexx.setSplitHandRegex('\n\n\n') 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.game_info_re.search(self.obs) + gametype = gametype + [self.float2int(m.group('SB'))] + gametype = gametype + [self.float2int(m.group('BB'))] return gametype @@ -83,5 +89,5 @@ if __name__ == "__main__": c = Configuration.Config() e = Everleaf(c, "regression-test-files/everleaf/Speed_Kuala.txt") e.processFile() -# print str(e) + print str(e) diff --git a/pyfpdb/FpdbRegex.py b/pyfpdb/FpdbRegex.py index f287c2f6..311a309f 100644 --- a/pyfpdb/FpdbRegex.py +++ b/pyfpdb/FpdbRegex.py @@ -27,6 +27,7 @@ import re class FpdbRegex: def __init__(self): + self.__GAME_INFO_REGEX='' self.__SPLIT_HAND_REGEX='\n\n\n' self.__NEW_HAND_REGEX='^.?PokerStars Game #\d+:\s+Hold\'em' self.__HAND_INFO_REGEX='^.*#(\d+):\s+(\S+)\s([\s\S]+)\s\(\$?([.0-9]+)/\$?([.0-9]+)\)\s-\s(\S+)\s-?\s?(\S+)\s\(?(\w+)\)?' @@ -55,6 +56,7 @@ class FpdbRegex: def compileRegexes(self): ### Compile the regexes + self.game_info_re = re.compile(self.__GAME_INFO_REGEX) self.split_hand_re = re.compile(self.__SPLIT_HAND_REGEX) self.hand_start_re = re.compile(self.__NEW_HAND_REGEX) self.hand_info_re = re.compile(self.__HAND_INFO_REGEX) @@ -82,6 +84,9 @@ class FpdbRegex: # Set methods for plugins to override + def setGameInfoRegex(self, string): + self.__GAME_INFO_REGEX = string + def setSplitHandRegex(self, string): self.__SPLIT_HAND_REGEX = string diff --git a/pyfpdb/HandHistoryConverter.py b/pyfpdb/HandHistoryConverter.py index 181273ce..508e253c 100644 --- a/pyfpdb/HandHistoryConverter.py +++ b/pyfpdb/HandHistoryConverter.py @@ -54,6 +54,14 @@ class HandHistoryConverter: tmp = tmp + "\tsb/bb: '%s/%s'\n" % (self.gametype[3], self.gametype[4]) return tmp + def processFile(self): + if not self.sanityCheck(): + print "Cowardly refusing to continue after failed sanity check" + return + self.readFile(self.file) + self.gametype = self.determineGameType() + self.hands = self.splitFileIntoHands() + # Functions to be implemented in the inheriting class def readSupportedGames(self): abstract @@ -91,19 +99,12 @@ class HandHistoryConverter: def setFileType(self, filetype = "text"): self.filetype = filetype - def processFile(self): - if not self.sanityCheck(): - print "Cowardly refusing to continue after failed sanity check" - return - self.readFile(self.file) - self.gametype = self.determineGameType() - self.splitFileIntoHands() - def splitFileIntoHands(self): hands = [] list = self.rexx.split_hand_re.split(self.obs) for l in list: - hands = hands + [Hand(l)] + hands = hands + [Hand(self.sitename, self.gametype, l)] + return hands def readFile(self, filename): """Read file""" @@ -160,13 +161,10 @@ class HandHistoryConverter: class Hand: # def __init__(self, sitename, gametype, sb, bb, string): - def __init__(self, string): -# self.sitename = sitename -# self.gametype = gametype -# self.sb = sb -# self.bb = bb + def __init__(self, sitename, gametype, string): + self.sitename = sitename + self.gametype = gametype self.string = string - print string self.handid = None self.tablename = "Slartibartfast" From f767ec220719ef484f02c24f2673484a1d484bd5 Mon Sep 17 00:00:00 2001 From: eblade Date: Mon, 10 Nov 2008 03:22:21 -0500 Subject: [PATCH 021/100] windows git is buggy --- docs/default.conf | 22 +- docs/readme-dev.txt | 134 ++--- ignore-me_perl6/RunFpdbCLI.perl6 | 58 +- pyfpdb/GuiAutoImport.py | 1 + pyfpdb/fpdb_import.py | 6 +- .../ftp-omaha-hi-pl-ring-001-005.txt | 540 +++++++++--------- .../known-broken/ftp-stud-hilo-ring-001.txt | 120 ++-- 7 files changed, 443 insertions(+), 438 deletions(-) diff --git a/docs/default.conf b/docs/default.conf index 031b8fb7..5b44938d 100644 --- a/docs/default.conf +++ b/docs/default.conf @@ -1,11 +1,11 @@ -db-backend=2 -db-host=localhost -db-databaseName=fpdb -db-user=fpdb -db-password=enterYourPwHere -imp-callFpdbHud=True -tv-combinedStealFold=True -tv-combined2B3B=True -tv-combinedPostflop=True -bulkImport-defaultPath=default -hud-defaultPath=default +db-backend=2 +db-host=localhost +db-databaseName=fpdb +db-user=fpdb +db-password=enterYourPwHere +imp-callFpdbHud=True +tv-combinedStealFold=True +tv-combined2B3B=True +tv-combinedPostflop=True +bulkImport-defaultPath=default +hud-defaultPath=default diff --git a/docs/readme-dev.txt b/docs/readme-dev.txt index 25e013bd..c8b5cd3f 100644 --- a/docs/readme-dev.txt +++ b/docs/readme-dev.txt @@ -1,67 +1,67 @@ -Hi, -This document is to serve as a little overview (later: full technical doc) for current and prospective developers with: -a) introduction into the code structure -b) organisational/legal things - -What to do? -=========== -- Anything you want. -- The most useful (because it's the most boring) would be to update print_hand.py, update the expected files (testdata/*.found.txt) and create more .found.txt to ensure import processing is running correctly. -- There's a list of various bugs, deficiencies and important missing features in known_bugs_and_planned_features.txt. -- In the main GUI there's various menu points marked with todo - all of these will have to be done eventually. - -If you want to take a look at coding-style.txt. -Ideally use git (see git-instructions.txt for some commands) and let me know where to pull from, alternatively feel free to send patches or even just changed file in whatever code layout or naming convention you like best. I will, of course, still give you full credit. - -Contact/Communication -===================== -If you start working on something please open a bug or feature request at sf to avoid someone else from doing the same -Please see readme-overview - -Dependencies -============ -Please let me know before you add any new dependencies and ensure that they are source-compatible between *nix and Windows - -Code/File/Class Structure -========================= -Basically the code runs like this - -fpdb.py -> bulk importer tab (import_threaded.py) -> fpdb_import.py -> fpdb_parse_logic.py -> fpdb_save_to_db.py -or -fpdb.py -> table viewer tab (table_viewer.py) (todo: -> libTableViewer) - -All files call the simple methods that I just collected in fpdb_simple.py, to abstract the other files off the nitty gritty details as I was learning python. -I'm currently working on (amongst other things) integrating everything into the fpdb.py GUI with a view to allow easy creation of a CLI client, too. - -Also see filelist.txt. - -How to Commit -============= -Please make sure you read and accept the copyright policy as stated in this file. Then see git-instructions.txt. Don't get me wrong, I hate all this legalese, but unfortunately it's kinda necessary. - -Copyright/Licensing -=================== -Copyright by default is handled on a per-file basis. If you send in a patch or make a commit to an existing file it is done on the understanding that you transfer all rights (as far as legally possible in your jurisdiction) to the current copyright holder of that file, unless otherwise stated. If you create a new file please ensure to include a copyright and license statement. - -The licenses used by this project are the AGPL3 for code and FDL1.2 for documentation. See readme-overview.txt for reasons and if you wish to use fpdb with different licensing. - -Preferred File Formats -====================== -Preferred: Where possible simple text-based formats, e.g. plain text (with Unix end of line char) or (X)HTML. Preferred picture format is PNG. IE-compability for HTML files is optional as IE was never meant to be a real web browser, if it were they would've implemented web standards. - -Also good: Other free and open formats, e.g. ODF. - -Not good: Any format that doesn't have full documentation freely and publicly available with a proper license for anyone to implement it. Sadly, Microsoft has chosen not fulfil these requirements for ISO MS OOXML to become a truly open standard. - -License (of this file) -======= -Trademarks of third parties have been used under Fair Use or similar laws. - -Copyright 2008 Steffen Jobbagy-Felso -Permission is granted to copy, distribute and/or modify this -document under the terms of the GNU Free Documentation License, -Version 1.2 as published by the Free Software Foundation; with -no Invariant Sections, no Front-Cover Texts, and with no Back-Cover -Texts. A copy of the license can be found in fdl-1.2.txt - -The program itself is licensed under AGPLv3, see agpl-3.0.txt +Hi, +This document is to serve as a little overview (later: full technical doc) for current and prospective developers with: +a) introduction into the code structure +b) organisational/legal things + +What to do? +=========== +- Anything you want. +- The most useful (because it's the most boring) would be to update print_hand.py, update the expected files (testdata/*.found.txt) and create more .found.txt to ensure import processing is running correctly. +- There's a list of various bugs, deficiencies and important missing features in known_bugs_and_planned_features.txt. +- In the main GUI there's various menu points marked with todo - all of these will have to be done eventually. + +If you want to take a look at coding-style.txt. +Ideally use git (see git-instructions.txt for some commands) and let me know where to pull from, alternatively feel free to send patches or even just changed file in whatever code layout or naming convention you like best. I will, of course, still give you full credit. + +Contact/Communication +===================== +If you start working on something please open a bug or feature request at sf to avoid someone else from doing the same +Please see readme-overview + +Dependencies +============ +Please let me know before you add any new dependencies and ensure that they are source-compatible between *nix and Windows + +Code/File/Class Structure +========================= +Basically the code runs like this + +fpdb.py -> bulk importer tab (import_threaded.py) -> fpdb_import.py -> fpdb_parse_logic.py -> fpdb_save_to_db.py +or +fpdb.py -> table viewer tab (table_viewer.py) (todo: -> libTableViewer) + +All files call the simple methods that I just collected in fpdb_simple.py, to abstract the other files off the nitty gritty details as I was learning python. +I'm currently working on (amongst other things) integrating everything into the fpdb.py GUI with a view to allow easy creation of a CLI client, too. + +Also see filelist.txt. + +How to Commit +============= +Please make sure you read and accept the copyright policy as stated in this file. Then see git-instructions.txt. Don't get me wrong, I hate all this legalese, but unfortunately it's kinda necessary. + +Copyright/Licensing +=================== +Copyright by default is handled on a per-file basis. If you send in a patch or make a commit to an existing file it is done on the understanding that you transfer all rights (as far as legally possible in your jurisdiction) to the current copyright holder of that file, unless otherwise stated. If you create a new file please ensure to include a copyright and license statement. + +The licenses used by this project are the AGPL3 for code and FDL1.2 for documentation. See readme-overview.txt for reasons and if you wish to use fpdb with different licensing. + +Preferred File Formats +====================== +Preferred: Where possible simple text-based formats, e.g. plain text (with Unix end of line char) or (X)HTML. Preferred picture format is PNG. IE-compability for HTML files is optional as IE was never meant to be a real web browser, if it were they would've implemented web standards. + +Also good: Other free and open formats, e.g. ODF. + +Not good: Any format that doesn't have full documentation freely and publicly available with a proper license for anyone to implement it. Sadly, Microsoft has chosen not fulfil these requirements for ISO MS OOXML to become a truly open standard. + +License (of this file) +======= +Trademarks of third parties have been used under Fair Use or similar laws. + +Copyright 2008 Steffen Jobbagy-Felso +Permission is granted to copy, distribute and/or modify this +document under the terms of the GNU Free Documentation License, +Version 1.2 as published by the Free Software Foundation; with +no Invariant Sections, no Front-Cover Texts, and with no Back-Cover +Texts. A copy of the license can be found in fdl-1.2.txt + +The program itself is licensed under AGPLv3, see agpl-3.0.txt diff --git a/ignore-me_perl6/RunFpdbCLI.perl6 b/ignore-me_perl6/RunFpdbCLI.perl6 index 90401cdf..dbbad9a7 100644 --- a/ignore-me_perl6/RunFpdbCLI.perl6 +++ b/ignore-me_perl6/RunFpdbCLI.perl6 @@ -1,29 +1,29 @@ -#!/usr/bin/pugs - -#Copyright 2008 Steffen Jobbagy-Felso -#This program is free software: you can redistribute it and/or modify -#it under the terms of the GNU Affero General Public License as published by -#the Free Software Foundation, version 3 of the License. -# -#This program is distributed in the hope that it will be useful, -#but WITHOUT ANY WARRANTY; without even the implied warranty of -#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -#GNU General Public License for more details. -# -#You should have received a copy of the GNU Affero General Public License -#along with this program. If not, see . -#In the "official" distribution you can find the license in -#agpl-3.0.txt in the docs folder of the package. - -use v6; -#use strict; -use LibFpdbImport; -use LibFpdbShared; - - -my Database $db .= new(:backend, :host, :database, :user, :password); -#todo: below doesnt work -my Importer $imp .= new(:db($db), :filename); -#perlbug?: adding another named argument that isnt listed in the constructor gave very weird error. -say $imp; - +#!/usr/bin/pugs + +#Copyright 2008 Steffen Jobbagy-Felso +#This program is free software: you can redistribute it and/or modify +#it under the terms of the GNU Affero General Public License as published by +#the Free Software Foundation, version 3 of the License. +# +#This program is distributed in the hope that it will be useful, +#but WITHOUT ANY WARRANTY; without even the implied warranty of +#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +#GNU General Public License for more details. +# +#You should have received a copy of the GNU Affero General Public License +#along with this program. If not, see . +#In the "official" distribution you can find the license in +#agpl-3.0.txt in the docs folder of the package. + +use v6; +#use strict; +use LibFpdbImport; +use LibFpdbShared; + + +my Database $db .= new(:backend, :host, :database, :user, :password); +#todo: below doesnt work +my Importer $imp .= new(:db($db), :filename); +#perlbug?: adding another named argument that isnt listed in the constructor gave very weird error. +say $imp; + diff --git a/pyfpdb/GuiAutoImport.py b/pyfpdb/GuiAutoImport.py index f8f4d8c1..6a5f00ab 100644 --- a/pyfpdb/GuiAutoImport.py +++ b/pyfpdb/GuiAutoImport.py @@ -135,6 +135,7 @@ class GuiAutoImport (threading.Thread): # Add directories to importer object. for site in self.input_settings: self.importer.addImportDirectory(self.input_settings[site][0], True, site, self.input_settings[site][1]) + print "Adding import directories - Site: " + site + " dir: "+ str(self.input_settings[site][0]) self.do_import() interval=int(self.intervalEntry.get_text()) diff --git a/pyfpdb/fpdb_import.py b/pyfpdb/fpdb_import.py index d0837f82..a6764319 100755 --- a/pyfpdb/fpdb_import.py +++ b/pyfpdb/fpdb_import.py @@ -175,7 +175,11 @@ class Importer: self.pos_in_file[file] = inputFile.tell() inputFile.close() - firstline = self.lines[0] + try: + firstline = self.lines[0] + except: + print "import_fpdb_file", file, site, lines + return if firstline.find("Tournament Summary")!=-1: print "TODO: implement importing tournament summaries" diff --git a/regression-test/known-broken/ftp-omaha-hi-pl-ring-001-005.txt b/regression-test/known-broken/ftp-omaha-hi-pl-ring-001-005.txt index b2b4ebb6..bebd282b 100644 --- a/regression-test/known-broken/ftp-omaha-hi-pl-ring-001-005.txt +++ b/regression-test/known-broken/ftp-omaha-hi-pl-ring-001-005.txt @@ -1,271 +1,271 @@ -Full Tilt Poker Game #6929537410: Table Green (deep) - $0.50/$1 - Pot Limit Omaha Hi - 17:15:44 ET - 2008/06/22 -Seat 1: player16 ($94.90) -Seat 2: player25 ($147) -Seat 3: player18 ($62.80) -Seat 4: player19 ($136.55) -Seat 5: play-er26 ($56.05) -Seat 6: player21 ($252.95) -Seat 7: player22 ($200) -Seat 8: player23 ($162.50) -Seat 9: player24 ($270.70) -player24 posts the small blind of $0.50 -player16 posts the big blind of $1 -player22 posts $1 -The button is in seat #8 -*** HOLE CARDS *** -player25 folds -player25 stands up -player18 folds -player19 folds -play-er26 folds -player21 folds -player22 checks -player23 calls $1 -player17 adds $100 -player24 calls $0.50 -player16 checks -*** FLOP *** [4s Kc 8s] -player24 has 15 seconds left to act -player24 checks -player16 checks -player22 checks -player23 checks -*** TURN *** [4s Kc 8s] [6s] -player24 checks -player16 checks -player22 checks -player23 bets $4 -player24 calls $4 -player16 folds -player22 folds -*** RIVER *** [4s Kc 8s 6s] [Qc] -player24 checks -player23 checks -*** SHOW DOWN *** -player23 shows [Td 5s 3d Js] a flush, Jack high -player24 mucks -player23 wins the pot ($11.40) with a flush, Jack high -*** SUMMARY *** -Total pot $12 | Rake $0.60 -Board: [4s Kc 8s 6s Qc] -Seat 1: player16 (big blind) folded on the Turn -Seat 2: player25 didn't bet (folded) -Seat 3: player18 didn't bet (folded) -Seat 4: player19 didn't bet (folded) -Seat 5: play-er26 didn't bet (folded) -Seat 6: player21 didn't bet (folded) -Seat 7: player22 folded on the Turn -Seat 8: player23 (button) collected ($11.40) -Seat 9: player24 (small blind) mucked - - - -Full Tilt Poker Game #6929553738: Table Green (deep) - $0.50/$1 - Pot Limit Omaha Hi - 17:17:06 ET - 2008/06/22 -Seat 1: player16 ($93.90) -Seat 2: player17 ($100) -Seat 3: player18 ($62.80) -Seat 4: player19 ($136.55) -Seat 5: play-er26 ($56.05) -Seat 6: player21 ($252.95) -Seat 7: player22 ($199) -Seat 8: player23 ($168.90) -Seat 9: player24 ($265.70) -player16 posts the small blind of $0.50 -player17 posts the big blind of $1 -The button is in seat #9 -*** HOLE CARDS *** -player18 folds -play-er26 stands up -player19 raises to $2 -play-er26 folds -player21 calls $2 -player22 has 15 seconds left to act -player22 folds -player23 folds -player24 folds -player16 calls $1.50 -player17 calls $1 -*** FLOP *** [Jc 4c Kc] -player16 checks -player17 checks -player19 checks -player21 checks -*** TURN *** [Jc 4c Kc] [7h] -player16 checks -player17 checks -player19 bets $3.50 -player21 folds -player16 folds -player17 calls $3.50 -*** RIVER *** [Jc 4c Kc 7h] [8s] -player17 checks -player19 has 15 seconds left to act -player19 bets $10 -player17 calls $10 -*** SHOW DOWN *** -player19 shows [4s Tc As Ac] a flush, Ace high -player17 mucks -player19 wins the pot ($33.25) with a flush, Ace high -*** SUMMARY *** -Total pot $35 | Rake $1.75 -Board: [Jc 4c Kc 7h 8s] -Seat 1: player16 (small blind) folded on the Turn -Seat 2: player17 (big blind) mucked -Seat 3: player18 didn't bet (folded) -Seat 4: player19 collected ($33.25) -Seat 5: play-er26 didn't bet (folded) -Seat 6: player21 folded on the Turn -Seat 7: player22 didn't bet (folded) -Seat 8: player23 didn't bet (folded) -Seat 9: player24 (button) didn't bet (folded) - - - -Full Tilt Poker Game #6929572212: Table Green (deep) - $0.50/$1 - Pot Limit Omaha Hi - 17:18:40 ET - 2008/06/22 -Seat 1: player16 ($91.90) -Seat 2: player17 ($84.50) -Seat 3: player18 ($62.80) -Seat 4: player19 ($154.30) -Seat 6: player21 ($250.95) -Seat 7: player22 ($199) -Seat 8: player23 ($168.90) -Seat 9: player24 ($265.70) -player17 posts the small blind of $0.50 -player18 posts the big blind of $1 -The button is in seat #1 -*** HOLE CARDS *** -player19 folds -player21 folds -player20 adds $50 -player22 folds -player23 folds -player24 folds -player20 is sitting out -player16 raises to $2 -player17 folds -player18 folds -Uncalled bet of $1 returned to player16 -player16 mucks -player16 wins the pot ($2.50) -*** SUMMARY *** -Total pot $2.50 | Rake $0 -Seat 1: player16 (button) collected ($2.50), mucked -Seat 2: player17 (small blind) folded before the Flop -Seat 3: player18 (big blind) folded before the Flop -Seat 4: player19 didn't bet (folded) -Seat 6: player21 didn't bet (folded) -Seat 7: player22 didn't bet (folded) -Seat 8: player23 didn't bet (folded) -Seat 9: player24 didn't bet (folded) - - - -Full Tilt Poker Game #6929576743: Table Green (deep) - $0.50/$1 - Pot Limit Omaha Hi - 17:19:03 ET - 2008/06/22 -Seat 1: player16 ($93.40) -Seat 2: player17 ($84) -Seat 3: player18 ($61.80) -Seat 4: player19 ($154.30) -Seat 5: player20 ($50), is sitting out -Seat 6: player21 ($250.95) -Seat 7: player22 ($199) -Seat 8: player23 ($168.90) -Seat 9: player24 ($265.70) -player18 posts the small blind of $0.50 -player19 posts the big blind of $1 -The button is in seat #2 -*** HOLE CARDS *** -player20 has returned -player21 calls $1 -player22 folds -player23 calls $1 -player24 calls $1 -player16 raises to $4 -player17 folds -player18 folds -player19 folds -player21 folds -player23 folds -player17 is sitting out -player24 has 15 seconds left to act -player24 calls $3 -*** FLOP *** [Tc 9s 7h] -player24 checks -player16 has 15 seconds left to act -player16 bets $8 -player24 folds -Uncalled bet of $8 returned to player16 -player16 mucks -player16 wins the pot ($10.95) -*** SUMMARY *** -Total pot $11.50 | Rake $0.55 -Board: [Tc 9s 7h] -Seat 1: player16 collected ($10.95), mucked -Seat 2: player17 (button) didn't bet (folded) -Seat 3: player18 (small blind) folded before the Flop -Seat 4: player19 (big blind) folded before the Flop -Seat 5: player20 is sitting out -Seat 6: player21 folded before the Flop -Seat 7: player22 didn't bet (folded) -Seat 8: player23 folded before the Flop -Seat 9: player24 folded on the Flop - - - -Full Tilt Poker Game #6929587483: Table Green (deep) - $0.50/$1 - Pot Limit Omaha Hi - 17:19:57 ET - 2008/06/22 -Seat 1: player16 ($100.35) -Seat 2: player17 ($84), is sitting out -Seat 3: player18 ($61.30) -Seat 4: player19 ($153.30) -Seat 5: player20 ($50) -Seat 6: player21 ($249.95) -Seat 7: player22 ($199) -Seat 8: player23 ($167.90) -Seat 9: player24 ($261.70) -player19 posts the small blind of $0.50 -player20 posts the big blind of $1 -The button is in seat #3 -*** HOLE CARDS *** -player21 folds -player22 folds -player21 stands up -player23 calls $1 -player24 calls $1 -player16 folds -player18 folds -player19 calls $0.50 -player20 checks -*** FLOP *** [Jd Td 2c] -roguern adds $100 -player19 bets $3 -player20 folds -player23 folds -player24 has 15 seconds left to act -player24 raises to $11 -player19 raises to $37 -player24 raises to $115 -player19 raises to $152.30, and is all in -player24 calls $37.30 -player19 shows [Jc Jh 7s 5h] -player24 shows [Kh Ad 6h Qd] -*** TURN *** [Jd Td 2c] [As] -*** RIVER *** [Jd Td 2c As] [8s] -player19 shows three of a kind, Jacks -player24 shows a straight, Ace high -player24 wins the pot ($305.60) with a straight, Ace high -player19 is sitting out -*** SUMMARY *** -Total pot $308.60 | Rake $3 -Board: [Jd Td 2c As 8s] -Seat 1: player16 didn't bet (folded) -Seat 2: player17 is sitting out -Seat 3: player18 (button) didn't bet (folded) -Seat 4: player19 (small blind) showed [Jc Jh 7s 5h] and lost with three of a kind, Jacks -Seat 5: player20 (big blind) folded on the Flop -Seat 6: player21 didn't bet (folded) -Seat 7: player22 didn't bet (folded) -Seat 8: player23 folded on the Flop -Seat 9: player24 showed [Kh Ad 6h Qd] and won ($305.60) with a straight, Ace high - - - +Full Tilt Poker Game #6929537410: Table Green (deep) - $0.50/$1 - Pot Limit Omaha Hi - 17:15:44 ET - 2008/06/22 +Seat 1: player16 ($94.90) +Seat 2: player25 ($147) +Seat 3: player18 ($62.80) +Seat 4: player19 ($136.55) +Seat 5: play-er26 ($56.05) +Seat 6: player21 ($252.95) +Seat 7: player22 ($200) +Seat 8: player23 ($162.50) +Seat 9: player24 ($270.70) +player24 posts the small blind of $0.50 +player16 posts the big blind of $1 +player22 posts $1 +The button is in seat #8 +*** HOLE CARDS *** +player25 folds +player25 stands up +player18 folds +player19 folds +play-er26 folds +player21 folds +player22 checks +player23 calls $1 +player17 adds $100 +player24 calls $0.50 +player16 checks +*** FLOP *** [4s Kc 8s] +player24 has 15 seconds left to act +player24 checks +player16 checks +player22 checks +player23 checks +*** TURN *** [4s Kc 8s] [6s] +player24 checks +player16 checks +player22 checks +player23 bets $4 +player24 calls $4 +player16 folds +player22 folds +*** RIVER *** [4s Kc 8s 6s] [Qc] +player24 checks +player23 checks +*** SHOW DOWN *** +player23 shows [Td 5s 3d Js] a flush, Jack high +player24 mucks +player23 wins the pot ($11.40) with a flush, Jack high +*** SUMMARY *** +Total pot $12 | Rake $0.60 +Board: [4s Kc 8s 6s Qc] +Seat 1: player16 (big blind) folded on the Turn +Seat 2: player25 didn't bet (folded) +Seat 3: player18 didn't bet (folded) +Seat 4: player19 didn't bet (folded) +Seat 5: play-er26 didn't bet (folded) +Seat 6: player21 didn't bet (folded) +Seat 7: player22 folded on the Turn +Seat 8: player23 (button) collected ($11.40) +Seat 9: player24 (small blind) mucked + + + +Full Tilt Poker Game #6929553738: Table Green (deep) - $0.50/$1 - Pot Limit Omaha Hi - 17:17:06 ET - 2008/06/22 +Seat 1: player16 ($93.90) +Seat 2: player17 ($100) +Seat 3: player18 ($62.80) +Seat 4: player19 ($136.55) +Seat 5: play-er26 ($56.05) +Seat 6: player21 ($252.95) +Seat 7: player22 ($199) +Seat 8: player23 ($168.90) +Seat 9: player24 ($265.70) +player16 posts the small blind of $0.50 +player17 posts the big blind of $1 +The button is in seat #9 +*** HOLE CARDS *** +player18 folds +play-er26 stands up +player19 raises to $2 +play-er26 folds +player21 calls $2 +player22 has 15 seconds left to act +player22 folds +player23 folds +player24 folds +player16 calls $1.50 +player17 calls $1 +*** FLOP *** [Jc 4c Kc] +player16 checks +player17 checks +player19 checks +player21 checks +*** TURN *** [Jc 4c Kc] [7h] +player16 checks +player17 checks +player19 bets $3.50 +player21 folds +player16 folds +player17 calls $3.50 +*** RIVER *** [Jc 4c Kc 7h] [8s] +player17 checks +player19 has 15 seconds left to act +player19 bets $10 +player17 calls $10 +*** SHOW DOWN *** +player19 shows [4s Tc As Ac] a flush, Ace high +player17 mucks +player19 wins the pot ($33.25) with a flush, Ace high +*** SUMMARY *** +Total pot $35 | Rake $1.75 +Board: [Jc 4c Kc 7h 8s] +Seat 1: player16 (small blind) folded on the Turn +Seat 2: player17 (big blind) mucked +Seat 3: player18 didn't bet (folded) +Seat 4: player19 collected ($33.25) +Seat 5: play-er26 didn't bet (folded) +Seat 6: player21 folded on the Turn +Seat 7: player22 didn't bet (folded) +Seat 8: player23 didn't bet (folded) +Seat 9: player24 (button) didn't bet (folded) + + + +Full Tilt Poker Game #6929572212: Table Green (deep) - $0.50/$1 - Pot Limit Omaha Hi - 17:18:40 ET - 2008/06/22 +Seat 1: player16 ($91.90) +Seat 2: player17 ($84.50) +Seat 3: player18 ($62.80) +Seat 4: player19 ($154.30) +Seat 6: player21 ($250.95) +Seat 7: player22 ($199) +Seat 8: player23 ($168.90) +Seat 9: player24 ($265.70) +player17 posts the small blind of $0.50 +player18 posts the big blind of $1 +The button is in seat #1 +*** HOLE CARDS *** +player19 folds +player21 folds +player20 adds $50 +player22 folds +player23 folds +player24 folds +player20 is sitting out +player16 raises to $2 +player17 folds +player18 folds +Uncalled bet of $1 returned to player16 +player16 mucks +player16 wins the pot ($2.50) +*** SUMMARY *** +Total pot $2.50 | Rake $0 +Seat 1: player16 (button) collected ($2.50), mucked +Seat 2: player17 (small blind) folded before the Flop +Seat 3: player18 (big blind) folded before the Flop +Seat 4: player19 didn't bet (folded) +Seat 6: player21 didn't bet (folded) +Seat 7: player22 didn't bet (folded) +Seat 8: player23 didn't bet (folded) +Seat 9: player24 didn't bet (folded) + + + +Full Tilt Poker Game #6929576743: Table Green (deep) - $0.50/$1 - Pot Limit Omaha Hi - 17:19:03 ET - 2008/06/22 +Seat 1: player16 ($93.40) +Seat 2: player17 ($84) +Seat 3: player18 ($61.80) +Seat 4: player19 ($154.30) +Seat 5: player20 ($50), is sitting out +Seat 6: player21 ($250.95) +Seat 7: player22 ($199) +Seat 8: player23 ($168.90) +Seat 9: player24 ($265.70) +player18 posts the small blind of $0.50 +player19 posts the big blind of $1 +The button is in seat #2 +*** HOLE CARDS *** +player20 has returned +player21 calls $1 +player22 folds +player23 calls $1 +player24 calls $1 +player16 raises to $4 +player17 folds +player18 folds +player19 folds +player21 folds +player23 folds +player17 is sitting out +player24 has 15 seconds left to act +player24 calls $3 +*** FLOP *** [Tc 9s 7h] +player24 checks +player16 has 15 seconds left to act +player16 bets $8 +player24 folds +Uncalled bet of $8 returned to player16 +player16 mucks +player16 wins the pot ($10.95) +*** SUMMARY *** +Total pot $11.50 | Rake $0.55 +Board: [Tc 9s 7h] +Seat 1: player16 collected ($10.95), mucked +Seat 2: player17 (button) didn't bet (folded) +Seat 3: player18 (small blind) folded before the Flop +Seat 4: player19 (big blind) folded before the Flop +Seat 5: player20 is sitting out +Seat 6: player21 folded before the Flop +Seat 7: player22 didn't bet (folded) +Seat 8: player23 folded before the Flop +Seat 9: player24 folded on the Flop + + + +Full Tilt Poker Game #6929587483: Table Green (deep) - $0.50/$1 - Pot Limit Omaha Hi - 17:19:57 ET - 2008/06/22 +Seat 1: player16 ($100.35) +Seat 2: player17 ($84), is sitting out +Seat 3: player18 ($61.30) +Seat 4: player19 ($153.30) +Seat 5: player20 ($50) +Seat 6: player21 ($249.95) +Seat 7: player22 ($199) +Seat 8: player23 ($167.90) +Seat 9: player24 ($261.70) +player19 posts the small blind of $0.50 +player20 posts the big blind of $1 +The button is in seat #3 +*** HOLE CARDS *** +player21 folds +player22 folds +player21 stands up +player23 calls $1 +player24 calls $1 +player16 folds +player18 folds +player19 calls $0.50 +player20 checks +*** FLOP *** [Jd Td 2c] +roguern adds $100 +player19 bets $3 +player20 folds +player23 folds +player24 has 15 seconds left to act +player24 raises to $11 +player19 raises to $37 +player24 raises to $115 +player19 raises to $152.30, and is all in +player24 calls $37.30 +player19 shows [Jc Jh 7s 5h] +player24 shows [Kh Ad 6h Qd] +*** TURN *** [Jd Td 2c] [As] +*** RIVER *** [Jd Td 2c As] [8s] +player19 shows three of a kind, Jacks +player24 shows a straight, Ace high +player24 wins the pot ($305.60) with a straight, Ace high +player19 is sitting out +*** SUMMARY *** +Total pot $308.60 | Rake $3 +Board: [Jd Td 2c As 8s] +Seat 1: player16 didn't bet (folded) +Seat 2: player17 is sitting out +Seat 3: player18 (button) didn't bet (folded) +Seat 4: player19 (small blind) showed [Jc Jh 7s 5h] and lost with three of a kind, Jacks +Seat 5: player20 (big blind) folded on the Flop +Seat 6: player21 didn't bet (folded) +Seat 7: player22 didn't bet (folded) +Seat 8: player23 folded on the Flop +Seat 9: player24 showed [Kh Ad 6h Qd] and won ($305.60) with a straight, Ace high + + + diff --git a/regression-test/known-broken/ftp-stud-hilo-ring-001.txt b/regression-test/known-broken/ftp-stud-hilo-ring-001.txt index e23cb335..956d1d14 100644 --- a/regression-test/known-broken/ftp-stud-hilo-ring-001.txt +++ b/regression-test/known-broken/ftp-stud-hilo-ring-001.txt @@ -1,61 +1,61 @@ -Full Tilt Poker Game #6367428246: Table Mountain Mesa - $15/$30 Ante $3 - Limit Stud H/L - 23:47:38 ET - 2008/05/10 -Seat 1: Player_8 ($446), is sitting out -Seat 2: Play er9 ($303.50) -Seat 3: P layer10 ($613), is sitting out -Seat 4: Player_11 ($164) -Seat 5: Player1 2 ($543.50), is sitting out -Seat 6: Player13 ($912.50) -Seat 7: Player14 ($430), is sitting out -Seat 8: Player15 ($531.50) -Player15 antes $3 -Player_11 antes $3 -Player13 antes $3 -Play er9 antes $3 -*** 3RD STREET *** -Dealt to Play er9 [2s] -Dealt to Player_11 [3c] -Dealt to Player13 [8c] -Dealt to Player15 [Jc] -Play er9 is low with [2s] -Play er9 brings in for $5 -Player_11 folds -Player13 completes it to $15 -Player15 folds -Play er9 calls $10 -*** 4TH STREET *** -Dealt to Play er9 [2s] [6c] -Dealt to Player13 [8c] [5h] -Player13 bets $15 -Play er9 calls $15 -*** 5TH STREET *** -Dealt to Play er9 [2s 6c] [Ac] -Dealt to Player13 [8c 5h] [Ah] -Player13 bets $30 -Play er9 calls $30 -*** 6TH STREET *** -Dealt to Play er9 [2s 6c Ac] [2c] -Dealt to Player13 [8c 5h Ah] [Jd] -Play er9 bets $30 -Player13 calls $30 -*** 7TH STREET *** -Play er9 bets $30 -Player13 calls $30 -*** SHOW DOWN *** -Play er9 shows [5c 4h 2s 6c Ac 2c 2h] three of a kind, Twos, for high and 6,5,4,2,A, for low -Player13 mucks -Play er9 wins the high pot ($125) with three of a kind, Twos -Play er9 wins the low pot ($125) with 6,5,4,2,A -*** SUMMARY *** -Total pot $252 | Rake $2 -Seat 1: Player_8 is sitting out -Seat 2: Play er9 collected ($250) -Seat 3: P layer10 is sitting out -Seat 4: Player_11 folded on 3rd St. -Seat 5: Player1 2 is sitting out -Seat 6: Player13 mucked -Seat 7: Player14 is sitting out -Seat 8: Player15 folded on 3rd St. - - - +Full Tilt Poker Game #6367428246: Table Mountain Mesa - $15/$30 Ante $3 - Limit Stud H/L - 23:47:38 ET - 2008/05/10 +Seat 1: Player_8 ($446), is sitting out +Seat 2: Play er9 ($303.50) +Seat 3: P layer10 ($613), is sitting out +Seat 4: Player_11 ($164) +Seat 5: Player1 2 ($543.50), is sitting out +Seat 6: Player13 ($912.50) +Seat 7: Player14 ($430), is sitting out +Seat 8: Player15 ($531.50) +Player15 antes $3 +Player_11 antes $3 +Player13 antes $3 +Play er9 antes $3 +*** 3RD STREET *** +Dealt to Play er9 [2s] +Dealt to Player_11 [3c] +Dealt to Player13 [8c] +Dealt to Player15 [Jc] +Play er9 is low with [2s] +Play er9 brings in for $5 +Player_11 folds +Player13 completes it to $15 +Player15 folds +Play er9 calls $10 +*** 4TH STREET *** +Dealt to Play er9 [2s] [6c] +Dealt to Player13 [8c] [5h] +Player13 bets $15 +Play er9 calls $15 +*** 5TH STREET *** +Dealt to Play er9 [2s 6c] [Ac] +Dealt to Player13 [8c 5h] [Ah] +Player13 bets $30 +Play er9 calls $30 +*** 6TH STREET *** +Dealt to Play er9 [2s 6c Ac] [2c] +Dealt to Player13 [8c 5h Ah] [Jd] +Play er9 bets $30 +Player13 calls $30 +*** 7TH STREET *** +Play er9 bets $30 +Player13 calls $30 +*** SHOW DOWN *** +Play er9 shows [5c 4h 2s 6c Ac 2c 2h] three of a kind, Twos, for high and 6,5,4,2,A, for low +Player13 mucks +Play er9 wins the high pot ($125) with three of a kind, Twos +Play er9 wins the low pot ($125) with 6,5,4,2,A +*** SUMMARY *** +Total pot $252 | Rake $2 +Seat 1: Player_8 is sitting out +Seat 2: Play er9 collected ($250) +Seat 3: P layer10 is sitting out +Seat 4: Player_11 folded on 3rd St. +Seat 5: Player1 2 is sitting out +Seat 6: Player13 mucked +Seat 7: Player14 is sitting out +Seat 8: Player15 folded on 3rd St. + + + From e713a9c654ecbadfd8ef7ca20c7abb079c5d51b2 Mon Sep 17 00:00:00 2001 From: eblade Date: Mon, 10 Nov 2008 03:46:37 -0500 Subject: [PATCH 022/100] fix weird error on windows? possibly just with p4e? trapping if we read 0 lines from history file --- pyfpdb/fpdb_import.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyfpdb/fpdb_import.py b/pyfpdb/fpdb_import.py index 64c4a02c..b12dba4e 100755 --- a/pyfpdb/fpdb_import.py +++ b/pyfpdb/fpdb_import.py @@ -178,10 +178,10 @@ class Importer: self.pos_in_file[file] = inputFile.tell() inputFile.close() - try: + try: # sometimes we seem to be getting an empty self.lines, in which case, we just want to return. firstline = self.lines[0] except: - print "import_fpdb_file", file, site, lines +# print "import_fpdb_file", file, site, self.lines, "\n" return if firstline.find("Tournament Summary")!=-1: From 2074d755aacca9b2d5d5950a5c4f1e12f8dcb756 Mon Sep 17 00:00:00 2001 From: eblade Date: Mon, 10 Nov 2008 05:58:55 -0500 Subject: [PATCH 023/100] Hud.py: fix typos in definitions of reposition_windows and kill_hud, eliminating errors when those functions are called move pop-up window to button 3 when clicked on a stat window, added move/resize functionality to button 1 (hold shift to resize, or just click/drag to move) restore opacity in windows (new window movement works with opacity set) fix error in popup_window initilization referring to window instead of main_window, oops --- pyfpdb/Hud.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/pyfpdb/Hud.py b/pyfpdb/Hud.py index 2490ad91..bee81aaf 100644 --- a/pyfpdb/Hud.py +++ b/pyfpdb/Hud.py @@ -116,13 +116,13 @@ class Hud: return True return False - def kill_hud(self, args): + def kill_hud(self, *args): for k in self.stat_windows.keys(): self.stat_windows[k].window.destroy() self.main_window.destroy() self.deleted = True - def reposition_windows(self, args): + def reposition_windows(self, *args): for w in self.stat_windows: self.stat_windows[w].window.move(self.stat_windows[w].x, self.stat_windows[w].y) @@ -259,7 +259,8 @@ class Stat_Window: # This handles all callbacks from button presses on the event boxes in # the stat windows. There is a bit of an ugly kludge to separate single- # and double-clicks. - if event.button == 1: # left button event + + if event.button == 3: # right button event if event.type == gtk.gdk.BUTTON_PRESS: # left button single click if self.sb_click > 0: return self.sb_click = gobject.timeout_add(250, self.single_click, widget) @@ -270,12 +271,14 @@ class Stat_Window: self.double_click(widget, event, *args) if event.button == 2: # middle button event - pass # print "middle button clicked" - - if event.button == 3: # right button event pass -# print "right button clicked" + + if event.button == 1: # left button event + if event.state & gtk.gdk.SHIFT_MASK: + self.window.begin_resize_drag(gtk.gdk.WINDOW_EDGE_SOUTH_EAST, event.button, int(event.x_root), int(event.y_root), event.time) + else: + self.window.begin_move_drag(event.button, int(event.x_root), int(event.y_root), event.time) def single_click(self, widget): # Callback from the timeout in the single-click finding part of the @@ -349,8 +352,9 @@ class Stat_Window: # font = pango.FontDescription("Sans 8") self.label[r][c].modify_font(font) - if not os.name == 'nt': # seems to be a bug in opacity on windows - self.window.set_opacity(parent.colors['hudopacity']) +# if not os.name == 'nt': # seems to be a bug in opacity on windows + self.window.set_opacity(parent.colors['hudopacity']) + self.window.realize self.window.move(self.x, self.y) self.window.show_all() @@ -457,7 +461,7 @@ class Popup_window: self.lab.set_text(pu_text) self.window.show_all() - self.window.set_transient_for(stat_window.main_window) + self.window.set_transient_for(stat_window.window) # set_keep_above(1) for windows if os.name == 'nt': self.topify_window(self.window) From 816a9e3b581ba56319018e0bb2f16dc3a9a11031 Mon Sep 17 00:00:00 2001 From: Worros Date: Mon, 10 Nov 2008 23:02:56 +1000 Subject: [PATCH 024/100] More Everleaf converter updates, now parsing some hand info --- pyfpdb/EverleafToFpdb.py | 22 +++++++++++++++++++++- pyfpdb/HandHistoryConverter.py | 18 +++++++++++++----- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/pyfpdb/EverleafToFpdb.py b/pyfpdb/EverleafToFpdb.py index f09c67d1..61e32d85 100644 --- a/pyfpdb/EverleafToFpdb.py +++ b/pyfpdb/EverleafToFpdb.py @@ -60,7 +60,8 @@ class Everleaf(HandHistoryConverter): self.sitename = "Everleaf" self.setFileType("text") self.rexx.setGameInfoRegex('.*Blinds \$?(?P[.0-9]+)/\$?(?P[.0-9]+)') - self.rexx.setSplitHandRegex('\n\n\n') + self.rexx.setSplitHandRegex('\n\n\n\n') + self.rexx.setHandInfoRegex('.*#(?P[0-9]+)\n.*\nBlinds \$?(?P[.0-9]+)/\$?(?P[.0-9]+) (?P.*) - (?P[0-9]+)/(?P[0-9]+)/(?P[0-9]+) - (?P
[0-9]+):(?P[0-9]+):(?P[0-9]+)\nTable (?P[ a-zA-Z]+)') self.rexx.compileRegexes() def readSupportedGames(self): @@ -76,6 +77,25 @@ class Everleaf(HandHistoryConverter): return gametype + def readHandInfo(self, hand): + m = self.rexx.hand_info_re.search(hand.string) + hand.handid = m.group('HID') + hand.tablename = m.group('GAMETYPE') +# These work, but the info is already in the Hand class - should be usecd 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 UTC [2008/11/07 7: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 = "%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): pass diff --git a/pyfpdb/HandHistoryConverter.py b/pyfpdb/HandHistoryConverter.py index 508e253c..9a8fd456 100644 --- a/pyfpdb/HandHistoryConverter.py +++ b/pyfpdb/HandHistoryConverter.py @@ -61,6 +61,9 @@ class HandHistoryConverter: self.readFile(self.file) self.gametype = self.determineGameType() self.hands = self.splitFileIntoHands() + for hand in self.hands: + self.readHandInfo(hand) + self.writeHand("output file", hand) # Functions to be implemented in the inheriting class def readSupportedGames(self): abstract @@ -70,6 +73,7 @@ class HandHistoryConverter: # [ ring, hold, nl , sb, bb ] # Valid types specified in docs/tabledesign.html in Gametypes def determineGameType(self): abstract + def readHandInfo(self, hand): abstract def readPlayerStacks(self): abstract def readBlinds(self): abstract def readAction(self): abstract @@ -102,7 +106,9 @@ class HandHistoryConverter: def splitFileIntoHands(self): hands = [] list = self.rexx.split_hand_re.split(self.obs) + list.pop() #Last entry is empty for l in list: +# print "'" + l + "'" hands = hands + [Hand(self.sitename, self.gametype, l)] return hands @@ -122,8 +128,8 @@ class HandHistoryConverter: def writeHand(self, file, hand): """Write out parsed data""" -# print sitename + " Game #" + handid + ": " + gametype + " (" + sb + "/" + bb + " - " + starttime -# print "Table '" + tablename + "' " + maxseats + "-max Seat #" + buttonpos + " is the button" + print "%s Game #%s: %s (%d/%d) - %s" %(hand.sitename, hand.handid, "XXXXhand.gametype", hand.sb, hand.bb, hand.starttime) + print "Table '%s' %d-max Seat #%s is the button" %(hand.tablename, hand.maxseats, "XXXXhand.buttonpos") # # counter = 1 # for player in seating: @@ -132,12 +138,12 @@ class HandHistoryConverter: # print playername + ": posts small blind " + sb # print playername + ": posts big blind " + bb # -# print "*** HOLE CARDS ***" + print "*** HOLE CARDS ***" # print "Dealt to " + hero + " [" + holecards + "]" # ## ACTION STUFF # -# print "*** SUMMARY ***" + print "*** SUMMARY ***" # print "Total pot $" + totalpot + " | Rake $" + rake # print "Board [" + boardcards + "]" # @@ -166,7 +172,9 @@ class Hand: self.gametype = gametype self.string = string - self.handid = None + self.handid = 0 + self.sb = gametype[3] + self.bb = gametype[4] self.tablename = "Slartibartfast" self.maxseats = 10 self.counted_seats = 0 From da83795e5a295f5814eec3d7dd01d090080bf78a Mon Sep 17 00:00:00 2001 From: Worros Date: Mon, 10 Nov 2008 23:29:49 +1000 Subject: [PATCH 025/100] Fix 'duh' error and grab button position --- pyfpdb/EverleafToFpdb.py | 6 +++--- pyfpdb/HandHistoryConverter.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyfpdb/EverleafToFpdb.py b/pyfpdb/EverleafToFpdb.py index 61e32d85..5e702dbb 100644 --- a/pyfpdb/EverleafToFpdb.py +++ b/pyfpdb/EverleafToFpdb.py @@ -61,7 +61,7 @@ class Everleaf(HandHistoryConverter): self.setFileType("text") self.rexx.setGameInfoRegex('.*Blinds \$?(?P[.0-9]+)/\$?(?P[.0-9]+)') self.rexx.setSplitHandRegex('\n\n\n\n') - self.rexx.setHandInfoRegex('.*#(?P[0-9]+)\n.*\nBlinds \$?(?P[.0-9]+)/\$?(?P[.0-9]+) (?P.*) - (?P[0-9]+)/(?P[0-9]+)/(?P[0-9]+) - (?P
[0-9]+):(?P[0-9]+):(?P[0-9]+)\nTable (?P
[ a-zA-Z]+)') + self.rexx.setHandInfoRegex('.*#(?P[0-9]+)\n.*\nBlinds \$?(?P[.0-9]+)/\$?(?P[.0-9]+) (?P.*) - (?P[0-9]+)/(?P[0-9]+)/(?P[0-9]+) - (?P
[0-9]+):(?P[0-9]+):(?P[0-9]+)\nTable (?P
[ a-zA-Z]+)\nSeat (?P
[ a-zA-Z]+)\nSeat (?P
[ a-zA-Z]+)\nSeat (?P
[ a-zA-Z]+)\nSeat (?P
[ a-zA-Z]+)\nSeat (?P
[ a-zA-Z]+)\nSeat (?P