# Python prog to take .pbn files or .inc files (RR) and do a cross-imp scored Butler # # Paul Bethe # # #Copyright (c) 2007, Paul Bethe # All rights reserved. # # Redistribution and use in source and binary forms, # with or without modification, are permitted provided # that the following conditions are met: # # 1. Redistributions of source code must retain the # above copyright notice, this list of conditions # and the following disclaimer. # # 2. Redistributions in binary form must reproduce the # above copyright notice, this list of conditions # and the following disclaimer in the documentation # and/or other materials provided with the # distribution. # # 3. Neither the name of Paul Bethe or the # names of its contributors may be used to endorse # or promote products derived from this software # without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND # CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL # THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER # IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. import sys, optparse from lin2pbn import PBNDeal, PBNDealSet, PBNResult TrackPair = None class Comparison: def __init__(self): self.timps = 0 self.compares = 1 def add(self, imps): self.timps += imps self.compares +=1 def avg(self): return float(self.timps) / self.compares def __str__(self): return "IMPS = %s (%d compares)" % (self.timps, self.compares) def parseDeal(fp): done = False res = None inAuction = False inPlay = False while not done: line = fp.readline() if len(line) == 0 or line[0] == "*": if res: return res else: return None line = line.strip() if len(line) == 0: if res: return res # else leading blank line - so burn elif line[0] == "[": toks = line[1:-1].split() key = toks[0] value = " ".join(toks[1:]) [1:-1] if key == "Event": res = PBNDeal("o1") elif key == "Auction": inAuction = True elif key == "Play": inPlay = True if key == "Note": if not inPlay: res.auctionNotes.append(value) else: res.playNotes.append(value) else: res.data[key] = value elif len(line) > 0: if inPlay: for p in line.split(): if p[0] != "=": res.play.append(p) else: res.play[-1] = res.play[-1] + " " + p elif inAuction: for bid in line.split(): if bid[0] != "=": res.auction.append(bid) else: res.auction[-1] = res.auction[-1] + " " + bid def parsePBNFile(pbnFn): fp = open (pbnFn) # burn firs three lines dummy = [ fp.readline() for x in range(3) ] done = False deals = [] while not done: deal = parseDeal(fp) if deal: deals.append(deal) else: done = True fp.close() return deals def makeCaps(name, delim): toks = name.split(delim) res = [ tok[0:1].upper() + tok[1:].lower() for tok in toks ] return delim.join(res) def makeName(name): #print name try: res = makeCaps (name, "'") res = makeCaps (res, " ") except: print name print res raise return res def genPair (deal, NS=True): if NS: dirs = ("North", "South") else: dirs = ("East", "West") if deal.data[dirs[1]] == dirs[1][0]: return "".join ([deal.data[x] for x in dirs ]) pair = map(makeName, [deal.data[x] for x in dirs ]) pair.sort () return " - ".join (pair) impTable = [0, 20, 50, 90, 130, 170, 220, 270, 320, 370, 430, 500, 600, 750, 900, 1100, 1300, 1500, 1750, 2000, 2250, 2500, 3000, 3500, 4000] def bsearch_bracket(list, value): #print ("Looking for (%d) in " + str(list)) % value size = len(list) if size < 1: return -1 if size < 2: if list[0] <= value: return 0 else: return -1 else: index = (size-1)/2 if list[index] <= value: if list[index+1] > value: return index else: return bsearch_bracket(list[index+1:], value) + index + 1 else: return bsearch_bracket(list[:index], value) def calcImps(netScore): if netScore < 0: negS = True netScore = abs(netScore) else: negS = False imps = bsearch_bracket (impTable, netScore) if negS: return -imps else: return imps def calcButler(deals, mlength): tables = {} EW = {} for deal in deals: # for NS, mult in ((True, 1), (False, -1)): try: pairStr = genPair (deal, NS=True) except: deal.write() raise if not tables.has_key(pairStr): tables[pairStr] = {} if deal.data["Declarer"] in ("E","W"): tables[pairStr][deal.data["Board"]] = -int (deal.data["Score"]) else: tables[pairStr][deal.data["Board"]] = int (deal.data["Score"]) EW[pairStr] = genPair (deal, NS=False) imps = {} keys = tables.keys() for key in keys: imps[key] = {} # init results tab for board in tables[key].keys(): imps[key][board] = Comparison() for i, pairStr in enumerate(keys): for pairStr2 in keys[i+1:]: if TrackPair and (pairStr == TrackPair or pairStr2 == TrackPair): print "%s vs %s" % (pairStr, pairStr2) elif TrackPair and (TrackPair == EW[pairStr] or TrackPair == EW[pairStr2]): print "%s vs %s" % (EW[pairStr], EW[pairStr2]) for board in tables[pairStr].keys(): if tables[pairStr2].has_key(board): score = calcImps(tables[pairStr][board] - tables[pairStr2][board]) imps[pairStr][board].add(score) imps[pairStr2][board].add(-score) if TrackPair: if pairStr == TrackPair or TrackPair == EW[pairStr2]: print "Bd %s %d vs %d => %d" % (board, tables[pairStr][board], tables[pairStr2][board], score) elif pairStr2 == TrackPair or TrackPair == EW[pairStr]: print "Bd %s %d vs %d => %d" % (board, -tables[pairStr][board], -tables[pairStr2][board], -score) ratings = {} for pairStr in keys: results = imps[pairStr].values() #val = (float(sum(results)) / len(keys)) / len(results) #print pairStr #print results val = sum (map (Comparison.avg, results)) / len(results) # total imps w/L divided by num tables in play # (not now )and then number of boards played # == average imps per board ratings[pairStr] = val ratings[EW[pairStr]] = -val boardlist = [ int(board) for board in tables[keys[0]].keys() ] return ratings, boardlist def printText(s2P, boardlist): print "%-45s%d to %d" % ("Pair", boardlist[0], boardlist[-1]) for key, value in s2P: print "%-45s%.3f" % (key , value) def cmpIncTuple (left, right): if left[1] > right[1]: return -1 if left[1] < right[1]: return 1 else: return 0 def getkey(line, sindex): res = line.split("=")[0][sindex:].strip() if res[-1] == "_" or res[0] == "_": return None return res def setkey (hash, line, sindex): key = getkey(line, sindex) if key: hash[key] = line.split('"')[1] def parseInc2PBN(fn, rlength): fp = open (fn) names = {} contract = {} declarer = {} lead = {} tricks = {} result = {} deals = [] for line in fp.readlines(): try: if line[1:6] == "names": setkey(names, line, 7) elif line[1:9] == "contract": setkey(contract,line, 9) elif line[1:9] == "declarer": setkey(declarer,line, 9) elif line[1:5] == "lead": setkey(lead, line,5) elif line[1:7] == "tricks": setkey(tricks,line, 7) elif line[1:7] == "result": setkey(result, line,7) # else ignore line except ValueError: pass for key in result.keys(): #print key bd, team1, team2 = key.split("_") if int(team1) < int(team2): room="o" else: room ="c" deal = PBNDeal(room+bd) for dir, pl in zip (("North", "South", "East", "West"), names["_".join([team1,team2])].split(",")): if pl == "": if dir in ("North", "East"): pl = "Team" if dir in ("North", "South"): pl += str(team1) else: pl += str(team2) pl += " Round" + str(1+(int(bd)-1)/rlength) + " "+ dir[0] else: pl = dir[0] #sys.stderr.write (names["_".join([team1,team2])] + "\n") #sys.stderr.write ("reset player to " + pl + "\n") deal.data[dir] = pl #print pl # already done in init deal.data["Board"] = bd deal.data["Room"] = { "o": "Open" , "c" : "Closed" }[room] deal.data["Contract"] = contract[key] deal.data["Declarer"] = declarer[key] if deal.data["Declarer"] in ("E","W"): deal.data["Score"] = str(-int(result[key])) else: deal.data["Score"] = result[key] # all that is needed ... deals.append (deal) return deals parser = optparse.OptionParser() parser.add_option("-c", "--config-file", dest="config", default=None, help="Config File", metavar="Config") parser.add_option("-t", "--type", dest="ftype", default="PBN", help="Site where event was played", metavar="File Type") parser.add_option("-l", "--length", dest="length", type="int", default=7, help="Length of Round", metavar="MatchLength") parser.add_option("-p", "--track-pair", dest="TrackPair", default=None, help="Debug: TrackPair Id", metavar="TP") def main(): deals = [] (options, args) = parser.parse_args() global TrackPair TrackPair = options.TrackPair if TrackPair: print "Tracking Pair '%s'" % TrackPair if options.ftype == "PBN": for filename in args: dummy = [ deals.append(x) for x in parsePBNFile (filename) ] else: # .inc files for filename in args: dummy = [ deals.append(x) for x in parseInc2PBN (filename, options.length) ] ratings, boardlist = calcButler (deals, options.length) boardlist.sort() s2P = ratings.items() s2P.sort(cmpIncTuple) # for now - text if not TrackPair: printText ( s2P, boardlist) if __name__ == "__main__": main()