From 42413d1a281251df3d4a5ec59589d84c1b63eff2 Mon Sep 17 00:00:00 2001 From: Oliver Smith Date: Tue, 26 Aug 2014 06:50:32 -0700 Subject: [PATCH] Assorted runtime fixes introduced in conversion --- trade.py | 2 +- tradecalc.py | 76 +++++++++++++++++++++++++++-------------------- tradedb.py | 84 +++++++++++++++++++++++++--------------------------- 3 files changed, 86 insertions(+), 76 deletions(-) diff --git a/trade.py b/trade.py index d3e3e384..1668ab8a 100644 --- a/trade.py +++ b/trade.py @@ -210,7 +210,7 @@ def parse_command_line(): ship = tdb.getShip(args.ship) args.ship = ship if args.capacity is None: args.capacity = ship.capacity - if args.maxLyPer is None: args.maxLyPer = ship.maxJumpFull + if args.maxLyPer is None: args.maxLyPer = ship.maxLyFull if args.capacity is None: raise CommandLineError("Missing '--capacity' or '--ship' argument") if args.maxLyPer is None: diff --git a/tradecalc.py b/tradecalc.py index 6a883b54..605368c0 100644 --- a/tradecalc.py +++ b/tradecalc.py @@ -53,7 +53,7 @@ def __eq__(self, rhs): return self.gainCr == rhs.gainCr and len(self.jumps) == len(rhs.jumps) def str(self): - return "%s -> %s" % (self.route[0], self.route[-1]) + return "%s -> %s" % (self.route[0].str(), self.route[-1].str()) def detail(self, detail=0): credits = self.startCr @@ -67,7 +67,7 @@ def detail(self, detail=0): hop = self.hops[i] hopGainCr, hopTonnes = hop[1], 0 text += " >-> " if i == 0 else " + " - text += "At %s/%s, Buy:" % (route[i].system.str().upper(), route[i].station) + text += "At %s/%s, Buy:" % (route[i].system.name(), route[i].name()) for (item, qty) in sorted(hop[0], key=lambda item: item[1] * item[0].gainCr, reverse=True): if detail > 1: text += "\n | %4d x %-30s" % (qty, item.item) @@ -89,7 +89,7 @@ def detail(self, detail=0): text += "\n" gainCr += hopGainCr - text += " <-< %s gaining %scr => %scr total" % (route[-1], localedNo(gainCr), localedNo(credits + gainCr)) + text += " <-< %s gaining %scr => %scr total" % (route[-1].name(), localedNo(gainCr), localedNo(credits + gainCr)) text += "\n" return text @@ -109,7 +109,7 @@ def summary(self): class TradeCalc(object): """ Container for accessing trade calculations with common properties """ - def __init__(self, tdb, debug=False, capacity=None, maxUnits=None, margin=0.01, unique=False, fit=None): + def __init__(self, tdb, debug=0, capacity=None, maxUnits=None, margin=0.01, unique=False, fit=None): self.tdb = tdb self.debug = debug self.capacity = capacity or 4 @@ -212,10 +212,10 @@ def getBestTrade(self, src, dst, credits, capacity=None, avoidItems=None, focusI """ if not avoidItems: avoidItems = [] if not focusItems: focusItems = [] - if self.debug: print("# %s -> %s with %dcr" % (src, dst, credits)) + if self.debug: print("# %s -> %s with %dcr" % (src.name(), dst.name(), credits)) - if not dst in src.stations: - raise ValueError("%s does not have a link to %s" % (src, dst)) + if not dst in src.tradingWith: + raise ValueError("%s does not have a link to %s" % (src.name(), dst.name())) capacity = capacity or self.capacity if not capacity: @@ -223,7 +223,7 @@ def getBestTrade(self, src, dst, credits, capacity=None, avoidItems=None, focusI maxUnits = self.maxUnits or capacity - items = src.trades[dst.ID] + items = src.tradingWith[dst] if avoidItems: items = [ item for item in items if not item.item in avoidItems ] if focusItems: @@ -253,20 +253,29 @@ def getBestTrade(self, src, dst, credits, capacity=None, avoidItems=None, focusI return fitFunction(items, credits, capacity, maxUnits) - def getBestHopFrom(self, src, credits, capacity=None, maxJumps=None, maxLy=None, maxLyPer=None): - """ Determine the best trade run from a given station. """ - if isinstance(src, str): - src = self.tdb.getStation(src) - hop = None - for (destSys, destStn, jumps, ly, via) in src.getDestinations(maxJumps=maxJumps, maxLy=maxLy, maxLyPer=maxLyPer): - load = self.getBestTrade(src, destStn, credits, capacity=capacity) - if load and (not hop or (load.gainCr > hop.gainCr or (load.gainCr == hop.gainCr and len(jumps) < hop.jumps))): - hop = TradeHop(destSys=destSys, destStn=destStn, load=load.items, gainCr=load.gainCr, jumps=jumps, ly=ly) - return hop + def getBestHopFrom(self, src, credits, capacity=None, maxJumps=None, maxLyPer=None): + """ + Determine the best trade run from a given station. + """ + src = self.tdb.getStation(src) + bestHop = None + for dest in src.getDestinations(maxJumps=maxJumps, maxLyPer=maxLyPer): + load = self.getBestTrade(src, dest.station, credits, capacity=capacity) + if not load: + continue + if bestHop: + if load.gainCr > bestHop.gainCr: continue + if load.gainCr == bestHop.gainCr: + if dest.jumps > bestHop.jumps: continue + if dest.jumps == bestHop.jumps: + if dest.ly >= bestHop.ly: + continue + bestHop = TradeHop(destSys=dest.system, destStn=dest.station, load=load.items, gainCr=load.gainCr, jumps=dest.jumps, ly=dest.ly) + return besthop def getBestHops(self, routes, credits, restrictTo=None, avoidItems=None, avoidPlaces=None, maxJumps=None, - maxLy=None, maxJumpsPer=None, maxLyPer=None): + maxJumpsPer=None, maxLyPer=None): """ Given a list of routes, try all available next hops from each route. Store the results by destination so that we pick the best route-to-point for each destination at each step. If we @@ -291,24 +300,27 @@ def getBestHops(self, routes, credits, if jumpLimit <= 0: continue - for (destSys, destStn, jumps, ly) in src.getDestinations(maxJumps=jumpLimit, maxLy=maxLy, maxLyPer=maxLyPer, avoiding=avoidPlaces): + for dest in src.getDestinations(maxJumps=jumpLimit, maxLyPer=maxLyPer, avoiding=avoidPlaces): if self.debug: - print("#destSys = %s, destStn = %s, jumps = %s, ly = %s" % (destSys.str(), destStn, "->".join([jump.str() for jump in jumps]), ly)) - if not destStn in src.stations: - if self.debug: print("#%s is not in my station list" % destStn) + print("#destSys = %s, destStn = %s, jumps = %s, distLy = %s" % (dest.system.name(), dest.station.name(), "->".join([jump.str() for jump in dest.via]), dest.distLy)) + if not dest.station in src.tradingWith: + if self.debug > 2: print("#%s is not in my station list" % dest.station.name()) continue - if restrictTo and destStn != restrictTo: - if self.debug: print("#%s doesn't match restrict %s" % (destStn, restrictTo)) + if restrictTo: + if (isinstance(restrictTo, system) and dest.system != restrictTo) \ + or (isinstance(restrictTo, station) and dest.station != restrictTo): + if self.debug > 2: print("#%s doesn't match restrict %s" % (dest.station.name(), restrictTo)) continue - if unique and destStn in route.route: - if self.debug: print("#%s is already in the list, not unique" % destStn) + if unique and dest.station in route.route: + if self.debug > 2: print("#%s is already in the list, not unique" % dest.station.name()) continue - trade = self.getBestTrade(src, destStn, startCr, avoidItems=avoidItems) + + trade = self.getBestTrade(src, dest.station, startCr, avoidItems=avoidItems) if not trade: - if self.debug: print("#* No trade") + if self.debug > 2: print("#* No trade") continue - dstID = destStn.ID + dstID = dest.station.ID try: # See if there is already a candidate for this destination (bestStn, bestRoute, bestTrade, bestJumps, bestLy) = bestToDest[dstID] @@ -317,12 +329,12 @@ def getBestHops(self, routes, credits, newRouteGainCr = route.gainCr + trade[1] if bestRouteGainCr > newRouteGainCr: continue - if bestRouteGainCr == newRouteGainCr and bestLy <= ly: + if bestRouteGainCr == newRouteGainCr and bestLy <= dest.distLy: continue except KeyError: # No existing candidate, we win by default pass - bestToDest[dstID] = [ destStn, route, trade, jumps, ly ] + bestToDest[dstID] = [ dest.station, route, trade, dest.via, dest.distLy ] result = [] for (dst, route, trade, jumps, ly) in bestToDest.values(): diff --git a/tradedb.py b/tradedb.py index d8dd8338..d9a924c6 100644 --- a/tradedb.py +++ b/tradedb.py @@ -98,8 +98,7 @@ class Station(object): def __init__(self, ID, system, name, lsFromStar=0.0): self.ID, self.system, self.dbname, self.lsFromStar = ID, system, name, lsFromStar - self.trades = {} - self.stations = [] + self.tradingWith = {} # dict[tradingPartnerStation] -> [ available trades ] system.addStation(self) def name(self): @@ -111,12 +110,10 @@ def addTrade(self, dest, item, itemID, costCr, gainCr): station and sold for a gain at another. """ # TODO: Something smarter. - dstID = dest.ID - if not dstID in self.trades: - self.trades[dstID] = [] - self.stations.append(dest) + if not dest in self.tradingWith: + self.tradingWith[dest] = [] trade = Trade(item, itemID, costCr, gainCr) - self.trades[dstID].append(trade) + self.tradingWith[dest].append(trade) def getDestinations(self, maxJumps=None, maxLyPer=None, avoiding=None): """ @@ -135,9 +132,9 @@ def getDestinations(self, maxJumps=None, maxLyPer=None, avoiding=None): # The closed list is the list of nodes we've already been to (so # that we don't create loops A->B->C->A->B->C->...) - Node = namedtuple('Node', [ 'system', 'via', 'dist' ]) + Node = namedtuple('Node', [ 'system', 'via', 'distLy' ]) - openList = [ OpenNode(self.system, [], 0) ] + openList = [ Node(self.system, [], 0) ] pathList = { system.ID: Node(system, None, 0.0) # include avoids so we only have # to consult one place for exclusions @@ -157,39 +154,37 @@ def getDestinations(self, maxJumps=None, maxLyPer=None, avoiding=None): jumps += 1 for node in ring: - (nodeSys, nodeVia, nodeDist) = (node.system, node.via, node.dist) - for (destSys, destDist) in sys.links.items(): - # Range check - dist = nodeDist + destDist - if dist > maxLyPer: continue + for (destSys, destDist) in node.system.links.items(): + if destDist > maxLyPer: continue + dist = node.distLy + destDist try: prevNode = pathList[destSys.ID] # If we already have a shorter path, do nothing - if dist >= prevNode.dist: continue + if dist >= prevNode.distLy: continue except KeyError: pass # Add to the path list - pathList[destSys.ID] = Node(dest, nodeVia, dist) + pathList[destSys.ID] = Node(destSys, node.via, dist) # Add to the open list but also include node to the via # list so that it serves as the via list for all next-hops. - openList += Node(dest, nodeVia + [dest], dist) + openList += [ Node(destSys, node.via + [destSys], dist) ] - DestNode = namedtuple('DestNode', [ 'system', 'station', 'via', 'dist' ]) + Destination = namedtuple('Destination', [ 'system', 'station', 'via', 'distLy' ]) destStations = [] # always include the local stations, unless the user has indicated they are # avoiding this system. E.g. if you're in Chango but you've specified you # want to avoid Chango... if not self.system in avoiding: - for station in self.stations: - if not station in avoiding: - destStations += Node(self, station, [], 0.0) + for station in self.system.stations: + if station in self.tradingWith and not station in avoiding: + destStations += [ Destination(self, station, [], 0.0) ] avoidStations = [ station for station in avoiding if isinstance(station, Station) ] - epsiol = sys.float_info.epsilon + epsilon = sys.float_info.epsilon for node in pathList.values(): - if node.dist > epsilon: # Values indistinguishable from zero are avoidances + if node.distLy > epsilon: # Values indistinguishable from zero are avoidances for station in node.system.stations: - destStations += Node(node.system, station, node.via, node.dist) + destStations += [ Destination(node.system, station, [self] + node.via + [station], node.distLy) ] return destStations @@ -202,8 +197,9 @@ def str(self): def __repr__(self): return ''.format(self.ID, self.system.name(), self.dbname, self.lsFromStar) -class Ship(namedtuple('Ship', [ 'ID', 'name', 'capacity', 'mass', 'driveRating', 'maxLyEmpty', 'maxLyFull', 'maxSpeed', 'boostSpeed', 'stations' ])): - pass +class Ship(namedtuple('Ship', [ 'ID', 'dbname', 'capacity', 'mass', 'driveRating', 'maxLyEmpty', 'maxLyFull', 'maxSpeed', 'boostSpeed', 'stations' ])): + def name(self): + return self.dbname class TradeDB(object): """ @@ -396,7 +392,7 @@ def load(self): # the maximum "link" between any two stars. longestJumper = max(ships.values(), key=lambda ship: ship.maxLyEmpty) self.maxSystemLinkLy = longestJumper.maxLyEmpty + 0.01 - if self.debug > 2: print("# Max ship jump distance: %s @ %f" % (longestJumper.name, self.maxSystemLinkLy)) + if self.debug > 2: print("# Max ship jump distance: %s @ %f" % (longestJumper.name(), self.maxSystemLinkLy)) self.build_links(self.maxSystemLinkLy) @@ -421,7 +417,9 @@ def _validate(self): raise ValueError("System %s does not have a reciprocal link in %s's links" % (name, link.str())) def getSystem(self, key): - """ Look up a System object by it's name. """ + """ + Look up a System object by it's name. + """ if isinstance(key, System): return name if isinstance(key, Station): @@ -430,7 +428,9 @@ def getSystem(self, key): return TradeDB.list_search("System", name, self.systems.values(), key=lambda system: system.name) def getStation(self, name): - """ Look up a Station object by it's name or system. """ + """ + Look up a Station object by it's name or system. + """ if isinstance(name, Station): return name if isinstance(name, System): @@ -441,45 +441,43 @@ def getStation(self, name): stationID, station, systemID, system = None, None, None, None try: - system = TradeDB.list_search("System", name, self.systems.values(), key=lambda system: system.name) + system = TradeDB.list_search("System", name, self.systemByID.values(), key=lambda system: system.dbname) except LookupError: pass try: - stationName = TradeDB.list_search("Station", name, self.stationIDs.keys()) - stationID = self.stationIDs[stationName] - station = self.stations[stationID] + station = TradeDB.list_search("Station", name, self.stationByID.values(), key=lambda station: station.dbname) except LookupError: pass # If neither matched, we have a lookup error. - if not (stationID or systemID): + if not (station or system): raise LookupError("'%s' did not match any station or system." % (name)) # If we matched both a station and a system, make sure they resovle to the # the same station otherwise we have an ambiguity. Some stations have the # same name as their star system (Aulin/Aulin Enterprise) - if systemID and stationID and system != station.system: - raise AmbiguityError('Station', name, system.str(), station.str()) + if system and station and system != station.system: + raise AmbiguityError('Station', name, system.name(), station.name()) - if stationID: - return self.stations[stationID] + if station: + return station # If we only matched a system name, ensure that it's a single station system # otherwise they need to specify a station name. - system = self.systems[systemID] if len(system.stations) != 1: raise ValueError("System '%s' has %d stations, please specify a station instead." % (name, len(system.stations))) return system.stations[0] def getShip(self, name): - """ Look up a ship by name """ - return TradeDB.list_search("Ship", name, self.ships, key=lambda item: item.name) + """ + Look up a ship by name + """ + return TradeDB.list_search("Ship", name, self.shipByID.values(), key=lambda ship: ship.dbname) def getTrade(self, src, dst, item): """ Returns a Trade object describing purchase of item from src for sale at dst. """ srcStn = self.getStation(src) dstStn = self.getStation(dst) - trades = srcStn.trades[dstStn.ID] - return trades[item] + return srcStn.tradingWith[dstStn] @staticmethod def getDistanceSq(lhsX, lhsY, lhsZ, rhsX, rhsY, rhsZ):