diff --git a/trade.py b/trade.py index 9528d079..80c2aa19 100644 --- a/trade.py +++ b/trade.py @@ -50,7 +50,8 @@ def parse_command_line(): parser.add_argument('--unique', help='Only visit each station once', default=False, required=False, action='store_true') parser.add_argument('--insurance', metavar="", help="Reserve at least this many credits", type=int, default=0, required=False) parser.add_argument('--margin', metavar="", help="Reduce gains by this much to provide a margin of error for market fluctuations (e.g. 0.25 reduces gains by 1/4). 0<=m<=0.25. Default: 0.02", default=0.02, type=float, required=False) - parser.add_argument('--debug', help="Enable verbose output", default=False, required=False, action='store_true') + parser.add_argument('--detail', help="Give detailed jump information for multi-jump hops", default=False, required=False, action='store_true') + parser.add_argument('--debug', help="Enable diagnostic output", default=False, required=False, action='store_true') parser.add_argument('--routes', metavar="", help="Maximum number of routes to show", type=int, default=1, required=False) args = parser.parse_args() @@ -133,11 +134,12 @@ def main(): parse_command_line() startCr = args.credits - args.insurance - routes = [ Route([src], [], startCr, 0, 0) for src in origins ] + routes = [ Route(stations=[src], hops=[], jumps=[], startCr=startCr, gainCr=0) for src in origins ] numHops = args.hops lastHop = numHops - 1 - print("From %s via %s to %s with %d credits for %d hops" % (originName, viaName, destName, startCr, numHops)) + if args.debug: + print("From %s via %s to %s with %d credits for %d hops" % (originName, viaName, destName, startCr, numHops)) calc = TradeCalc(tdb, debug=args.debug, capacity=args.capacity, maxUnits=maxUnits, margin=args.margin, unique=args.unique) for hopNo in range(numHops): @@ -163,7 +165,7 @@ def main(): routes.sort() for i in range(0, min(len(routes), args.routes)): - print(routes[i]) + print(routes[i].detail(args.detail)) if __name__ == "__main__": diff --git a/tradecalc.py b/tradecalc.py index 21234612..a18326bb 100644 --- a/tradecalc.py +++ b/tradecalc.py @@ -20,32 +20,40 @@ def __init__(self, stations, hops, startCr, gainCr, jumps): self.jumps = jumps def plus(self, dst, hop, jumps): - rvalue = Route(self.route + [dst], self.hops + [hop], self.startCr, self.gainCr + hop[1], self.jumps + jumps) + rvalue = Route(self.route + [dst], self.hops + [hop], self.startCr, self.gainCr + hop[1], self.jumps + [jumps]) return rvalue def __lt__(self, rhs): if rhs.gainCr < self.gainCr: # reversed return True - return rhs.gainCr == self.gainCr and rhs.jumps < self.jumps + return rhs.gainCr == self.gainCr and len(rhs.jumps) < len(self.jumps) def __eq__(self, rhs): - return self.gainCr == rhs.gainCr and self.jumps == rhs.jumps + return self.gainCr == rhs.gainCr and len(self.jumps) == len(rhs.jumps) - def __repr__(self): + def str(self): + return "%s -> %s" % (self.route[0], self.route[-1]) + + def detail(self, verbose=False): src = self.route[0] credits = self.startCr gainCr = 0 route = self.route - str = "%s -> %s:\n" % (src, route[-1]) + str = self.str() + ":\n" for i in range(len(route) - 1): hop = self.hops[i] - str += " @ %-20s Buy" % route[i] + str += " >-> " if i == 0 else " -+- " + str += "%-20s Buy" % route[i] for item in hop[0]: str += " %d*%s," % (item[1], item[0]) str += "\n" + if verbose and len(self.jumps[i]) > 2: + str += " | " + str += " -> ".join([ jump.str() for jump in self.jumps[i] ]) + str += "\n" gainCr += hop[1] - str += " $ %s %dcr + %dcr => %dcr total" % (route[-1], credits, gainCr, credits + gainCr) + str += " <-< %s %dcr + %dcr => %dcr total" % (route[-1], credits, gainCr, credits + gainCr) return str @@ -109,9 +117,9 @@ def getBestHopFrom(self, src, credits, capacity=None, maxJumps=None, maxLy=None, if isinstance(src, str): src = self.tdb.getStation(src) hop = None - for (destSys, destStn, jumps, ly) in src.getDestinations(maxJumps=maxJumps, maxLy=maxLy, maxLyPer=maxLyPer): + 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[1] > hop.gainCr or (load[1] == hop.gainCr and jumps < hop.jumps))): + if load and (not hop or (load[1] > hop.gainCr or (load[1] == hop.gainCr and len(jumps) < hop.jumps))): hop = TradeHop(destSys=destSys, destStn=destStn, load=load[0], gainCr=load[1], jumps=jumps, ly=ly) return hop @@ -128,29 +136,40 @@ def getBestHops(self, routes, credits, restrictTo=None, maxJumps=None, maxLy=Non perJumpLimit = maxJumpsPer if (maxJumpsPer or 0) > 0 else 0 for route in routes: src = route.route[-1] + if self.debug: print("# route = %s" % route.str()) startCr = credits + int(route.gainCr * safetyMargin) - routeJumps = route.jumps + routeJumps = len(route.jumps) jumpLimit = perJumpLimit if (maxJumps or 0) > 0: jumpLimit = min(maxJumps - routeJumps, perJumpLimit) if perJumpLimit > 0 else maxJumps - routeJumps - if jumpLimit == 0: + if jumpLimit <= 0: + if self.debug: print("Jump Limit") continue + for (destSys, destStn, jumps, ly) in src.getDestinations(maxJumps=jumpLimit, maxLy=maxLy, maxLyPer=maxLyPer): + 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) continue if restrictTo and destStn != restrictTo: + if self.debug: print("#%s doesn't match restrict %s" % (destStn, restrictTo)) continue if unique and destStn in route.route: + if self.debug: print("#%s is already in the list, not unique" % destStn) continue trade = self.getBestTrade(src, destStn, startCr) if not trade: + if self.debug: print("#* No trade") continue dstID = destStn.ID try: best = bestToDest[dstID] - if best[1].gainCr + best[2][1] >= route.gainCr + trade[1]: + oldRouteGainCr = best[1].gainCr + best[2][1] + newRouteGainCr = route.gainCr + trade[1] + if oldRouteGainCr >= newRouteGainCr: continue - except: pass + except KeyError: pass bestToDest[dstID] = [ destStn, route, trade, jumps, ly ] result = [] diff --git a/tradedb.py b/tradedb.py index 7543d24b..9a1753cc 100644 --- a/tradedb.py +++ b/tradedb.py @@ -91,17 +91,19 @@ def organizeTrades(self): def getDestinations(self, maxJumps=None, maxLy=None, maxLyPer=None): openList, closedList, destStations = Queue(), dict(), [] - openList.put([self.system, 0, 0]) + openList.put([self.system, [], 0]) maxJumpDist = maxLyPer or sys.maxint while not openList.empty(): (sys, jumps, dist) = openList.get() - if (maxJumps and jumps > maxJumps) or (maxLy and dist > maxLy): + if maxJumps and len(jumps) > maxJumps: + continue + if maxLy and dist > maxLy: continue for stn in sys.stations: if stn != self: - destStations.append([sys, stn, jumps, dist]) - jumps += 1 - if (maxJumps and jumps > maxJumps): + destStations.append([sys, stn, list(jumps), dist]) + jumps.append(sys) + if (maxJumps and len(jumps) > maxJumps): continue for (destSys, destDist) in sys.links.items(): if dist > maxLyPer: @@ -110,7 +112,7 @@ def getDestinations(self, maxJumps=None, maxLy=None, maxLyPer=None): continue if destSys in closedList: continue - openList.put([destSys, jumps, dist + destDist]) + openList.put([destSys, list(jumps), dist + destDist]) closedList[destSys] = 1 return destStations @@ -120,8 +122,9 @@ def __repr__(self): class TradeDB(object): - def __init__(self, path=r'.\TradeDangerous.accdb'): + def __init__(self, path=r'.\TradeDangerous.accdb', debug=0): self.path = "Driver={Microsoft Access Driver (*.mdb, *.accdb)};DBQ=" + path + self.debug = debug self.load() def load(self, avoiding=[], ignoreLinks=False): @@ -131,6 +134,8 @@ def load(self, avoiding=[], ignoreLinks=False): cur.execute('SELECT system FROM Stations GROUP BY system') self.systems = { row[0]: System(row[0]) for row in cur } + if self.debug: + print(self.systems) cur.execute("""SELECT frmSys.system, toSys.system, Links.distLy FROM Stations AS frmSys, Links, Stations as toSys WHERE frmSys.ID = Links.from AND toSys.ID = Links.to""") @@ -148,7 +153,7 @@ def load(self, avoiding=[], ignoreLinks=False): """ Populate 'items' from the database """ cur.execute('SELECT id, item FROM Items') self.items = { row[0]: row[1] for row in cur } - self.itemIDs = { self.items[name]: name for name in self.items } + self.itemIDs = { name: itemID for (itemID, name) in self.items.items() } stations, items = self.stations, self.items @@ -157,7 +162,7 @@ def load(self, avoiding=[], ignoreLinks=False): ' FROM Prices AS src INNER JOIN Prices AS dst ON src.item_id = dst.item_id' ' WHERE src.buy_cr > 0 AND dst.sell_cr > src.buy_cr' ' AND src.ui_order > 0 AND dst.ui_order > 0' - ' ORDER BY (dst.sell_cr - src.buy_cr) DESC') + ) for row in cur: if not (items[row[2]] in avoiding): stations[row[0]].addTrade(stations[row[1]], items[row[2]], row[2], row[3], row[4]) @@ -165,6 +170,32 @@ def load(self, avoiding=[], ignoreLinks=False): for station in stations.values(): station.organizeTrades() + if self.debug: + self.validate() + + def validate(self): + # Check that things correctly reference themselves. + for (stnID, stn) in self.stations.items(): + if self.stations[stn.ID] != stn: + raise ValueError("Station not pointing to self correctly" % stn.station) + for (stnName, stnID) in self.stationIDs.items(): + if self.stations[stnID].station.upper() != stnName: + raise ValueError("Station name not pointing to self correctly" % stnName) + for (itemID, item) in self.items.items(): + if self.itemIDs[item] != itemID: + raise ValueError("Item %s not pointing to itself correctly" % item, itemID, item, self.itemIDs[item]) + # Check that system links are bi-directional + for (name, sys) in self.systems.items(): + if not sys.links: + raise ValueError("System %s has no links" % name) + if sys in sys.links: + raise ValueError("System %s has a link to itself!" % name) + if name in sys.links: + raise ValueError("System %s's name occurs in sys.links" % name) + for link in sys.links: + if not sys in link.links: + raise ValueError("System %s does not have a reciprocal link in %s's links" % (name, link.str())) + def getStation(self, name): if isinstance(name, Station): return name