diff --git a/CHANGES.txt b/CHANGES.txt index a1d90d7e..c942f2cb 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -2,6 +2,15 @@ TradeDangerous, Copyright (C) Oliver "kfsone" Smith, July 2014 ============================================================================== +v6.6.1 Jan 10 2015 +. (kfsone) Added "--blackmarket" option to "run" command, restrictions + to stations which have a black market. +. (kfsone) Added "--end-jumps" ('-e') to "run" command, includes stations + from systems within this many jumps of the specified --to. + e.g. + trade.py run --from sol --to lave -e 3 + will find runs from sol to somewhere within 3 jumps of lave. + v6.6.0 Jan 08 2015 . (kfsone) Overhaul of loading of trades and adjancent-system finding - Item data is loaded as discrete sales and purchases in TradeCalc, diff --git a/README.txt b/README.txt index ff42274d..51012220 100644 --- a/README.txt +++ b/README.txt @@ -221,10 +221,16 @@ RUN sub-command: --start-jumps N -s N - Considers stations from stations upto this many jumps from your + Considers stations from systems upto this many jumps from your specified start location. --from beagle2 --ly-per 7.56 --empty 10.56 -s 2 + --end-jumps N + -e N + Considers stations from systems upto this many jumps from your + specified destination (--to). + --to lave -e 3 (find runs that end within 3 jumps of lave) + --via Lets you specify a station that must be between the second and final hop. Requires that hops be at least 2. @@ -249,6 +255,10 @@ RUN sub-command: --pad ? (unknown only), --pad L (large only, ignores unknown) + --black-market + -bm + Only consider stations that have a black market. + --ls-penalty N.NN --lsp N.NN DEFAULT: 0.5 diff --git a/commands/TEMPLATE.py b/commands/TEMPLATE.py index 4c1b07fd..c55aa790 100644 --- a/commands/TEMPLATE.py +++ b/commands/TEMPLATE.py @@ -1,13 +1,16 @@ from __future__ import absolute_import, with_statement, print_function, division, unicode_literals +from commands.commandenv import ResultRow from commands.parsing import MutuallyExclusiveGroup, ParseArgument +from formatting import RowFormat, ColumnFormat ###################################################################### # Parser config -help=#'Terse description of command' -name=#'cmd' -epilog=#None -wantsTradeDB=True +help = 'Describe your command briefly here for the top-level --help.' +name = 'TEMPLATE' # name of your .py file excluding the _cmd +epilog = None # text to print at the bottom of --help +wantsTradeDB = True # Should we try to load the cache at startup? +usesTradeData = True # Will we be needing trading data? arguments = [ #ParseArgument('near', help='System to start from', type=str), ] @@ -27,7 +30,19 @@ # Perform query and populate result set def run(results, cmdenv, tdb): - from commands.commandenv import ResultRow + """ + Implement code that validates arguments, collects and prepares + any data you will need to generate your results for the user. + + If your command has finished and has no output to generate, + return None, otherwise return "results" to be forwarded to + the 'render' function. + + DO NOT print() during 'run', this allows run() functions to + be re-used between modules and allows them to be used beyond + the trade.py command line - e.g. someone writing a TD GUI + will call run() and then render the results themselves. + """ ### TODO: Implement @@ -37,6 +52,11 @@ def run(results, cmdenv, tdb): # Transform result set into output def render(results, cmdenv, tdb): - from formatting import RowFormat, ColumnFormat + """ + If run() returns a non-None value, the trade.py code will then + call the corresponding render() function. + + This is where you should generate any output from your command. + """ ### TODO: Implement diff --git a/commands/commandenv.py b/commands/commandenv.py index e956dfe2..91de7f28 100644 --- a/commands/commandenv.py +++ b/commands/commandenv.py @@ -47,6 +47,7 @@ def __init__(self, properties, argv, cmdModule): self._cmd = cmdModule or __main__ self.wantsTradeDB = getattr(cmdModule, 'wantsTradeDB', True) + self.usesTradeData = getattr(cmdModule, 'usesTradeData', False) # We need to relocate to the working directory so that # we can load a TradeDB after this without things going diff --git a/commands/run_cmd.py b/commands/run_cmd.py index 1c5d2324..d16afa82 100644 --- a/commands/run_cmd.py +++ b/commands/run_cmd.py @@ -1,14 +1,18 @@ from __future__ import absolute_import, with_statement, print_function, division, unicode_literals -from commands.parsing import MutuallyExclusiveGroup, ParseArgument +from commands.commandenv import ResultRow from commands.exceptions import * +from commands.parsing import MutuallyExclusiveGroup, ParseArgument +from formatting import RowFormat, ColumnFormat from tradedb import System, Station ###################################################################### # Parser config -help='Calculate best trade run.' -name='run' -epilog=None +help = 'Calculate best trade run.' +name = 'run' +epilog = None +usesTradeData = True + arguments = [ ParseArgument('--capacity', help='Maximum capacity of cargo hold.', @@ -27,6 +31,7 @@ type=float, ), ] + switches = [ ParseArgument('--from', help='Starting system/station.', @@ -70,11 +75,17 @@ default=None, ), ParseArgument('--start-jumps', '-s', - help='Allow this many jumps before loading up.', + help='Consider stations within this many jumps of the origin (requires --from).', dest='startJumps', default=0, type=int, ), + ParseArgument('--end-jumps', '-e', + help='Consider stations within this many jumps of the destination (requires --to).', + dest='endJumps', + default=0, + type=int, + ), ParseArgument('--limit', help='Maximum units of any one cargo item to buy (0: unlimited).', metavar='N', @@ -86,6 +97,16 @@ type=float, dest='maxAge', ), + ParseArgument('--pad-size', '-p', + help='Limit the padsize to this ship size (S,M,L or ? for unkown).', + metavar='PADSIZES', + dest='padSize', + ), + ParseArgument('--black-market', '-bm', + help='Require stations with a black market.', + action='store_true', + dest='blackMarket', + ), ParseArgument('--ls-penalty', '--lsp', help="Penalty per 1kls stations are from their stars.", default=0.6, @@ -117,11 +138,6 @@ metavar='N', type=int, ), - ParseArgument('--pad-size', '-p', - help='Limit the padsize to this ship size (S,M,L or ? for unkown).', - metavar='PADSIZES', - dest='padSize', - ), ParseArgument('--checklist', help='Provide a checklist flow for the route.', action='store_true', @@ -226,33 +242,35 @@ def run(self, route, cr): sleep(1.5) -def extendOriginsForStartJumps(tdb, cmdenv): +def expandForJumps(tdb, cmdenv, origins, jumps, srcName): """ Find all the stations you could reach if you made a given number of jumps away from the origin list. """ - startJumps = cmdenv.startJumps - if not startJumps: - return cmdenv.origins + if not jumps: + return origins + + origSys = set() + for place in origins: + if isinstance(place, Station): + origSys.add(place.system) + elif isinstance(place, System): + origSys.add(place) - origSys = [o.system for o in cmdenv.origins] maxLyPer = cmdenv.emptyLyPer or cmdenv.maxLyPer avoidPlaces = cmdenv.avoidPlaces if cmdenv.debug: cmdenv.DEBUG0( - "Checking start stations " - "{} jumps at " - "{}ly per jump " - "from {}", - startJumps, + "extending {} list {} by {} jumps at {}ly per jump", + srcName, + [sys.dbname for sys in origSys], + jumps, maxLyPer, - [sys.dbname for sys in origSys] - ) + ) - origSys = set(origSys) nextJump = set(origSys) - for jump in range(0, startJumps): + for jump in range(jumps): if not nextJump: break thisJump, nextJump = nextJump, set() @@ -261,18 +279,19 @@ def extendOriginsForStartJumps(tdb, cmdenv): "Ring {}: {}", jump, [sys.dbname for sys in thisJump] - ) + ) for sys in thisJump: for dest, dist in tdb.genSystemsInRange(sys, maxLyPer): - if dest not in avoidPlaces: + if dest not in origSys and dest not in avoidPlaces: origSys.add(dest) nextJump.add(dest) if cmdenv.debug: cmdenv.DEBUG0( - "Extended start systems: {}", + "Expanded {} systems: {}", + srcName, [sys.dbname for sys in origSys] - ) + ) # Filter down to stations with trade data origins = [] @@ -283,11 +302,76 @@ def extendOriginsForStartJumps(tdb, cmdenv): if cmdenv.debug: cmdenv.DEBUG0( - "Extended start stations: {}", + "expanded {} stations: {}", + srcName, [sys.name() for sys in origins] - ) + ) - return origins + return set(origins) + + +def checkForEmptyStationList(category, focusPlace, stationList, jumps): + if stationList: + return + if jumps: + raise NoDataError( + "Local database has no price data for any " + "stations within {} jumps of {} ({})".format( + jumps, + focusPlace.name(), + category, + )) + if isinstance(focusPlace, System): + raise NoDataError( + "Local database has no price data for " + "stations in {} ({})".format( + focusPlace.name(), + category, + )) + raise NoDataError( + "Local database has no price data for {} ({})".format( + focusPlace.name(), + category, + )) + + +def checkAnchorNotInVia(hops, anchorName, place, viaSet): + """ + Ensure that '--to' or '--from' is not in the via set. + """ + + if hops != 2: + return + if isinstance(place, Station) and place in viaSet: + raise CommandLineError( + "{} used in {} and --via with only 2 hops".format( + place.name(), + anchorName, + )) + + +def filterStationSet(src, cmdenv, stnSet): + if not stnSet: + return stnSet + bm, mps = cmdenv.blackMarket, cmdenv.maxPadSize + for place in stnSet: + if not isinstance(place, Station): + continue + if place.itemCount == 0: + stnSet.remove(place) + continue + if mps and not place.checkPadSize(mps): + stnSet.remove(place) + continue + if bm and place.blackMarket != 'Y': + stnSet.remove(place) + continue + if not stnSet: + raise CommandLineError( + "No {} station met your criteria.".format( + src + )) + return stnSet def validateRunArguments(tdb, cmdenv): @@ -317,31 +401,57 @@ def validateRunArguments(tdb, cmdenv): cmdenv.origins = list(cmdenv.origPlace.stations) if not cmdenv.origins: raise CommandLineError( - "No stations at origin system, {}" + "No stations at --from system, {}" .format(cmdenv.origPlace.name()) ) else: cmdenv.origins = [ cmdenv.origPlace ] cmdenv.startStation = cmdenv.origPlace - cmdenv.origins = extendOriginsForStartJumps(tdb, cmdenv) + cmdenv.origins = expandForJumps( + tdb, cmdenv, + cmdenv.origins, + cmdenv.startJumps, + "--from" + ) + checkForEmptyStationList( + "--from", cmdenv.origPlace, + cmdenv.origins, cmdenv.startJumps + ) else: - cmdenv.origins = [ station for station in tdb.stationByID.values() ] + cmdenv.origins = [ + station + for station in tdb.stationByID.values() + if station.itemCount > 0 + ] + if cmdenv.startJumps: + raise CommandLineError("--start-jumps (-s) only works with --from") - cmdenv.stopStation = None + cmdenv.destinations = None if cmdenv.destPlace: if isinstance(cmdenv.destPlace, Station): - cmdenv.stopStation = cmdenv.destPlace + checkStationSuitability(cmdenv, cmdenv.destPlace, '--to') + cmdenv.destinations = [ cmdenv.destPlace ] elif isinstance(cmdenv.destPlace, System): - if not cmdenv.destPlace.stations: - raise CommandLineError( - "No known/trading stations in {}.".format( - cmdenv.destPlace.name() - )) + cmdenv.destinations = [ cmdenv.destPlace ] + cmdenv.destinations = expandForJumps( + tdb, cmdenv, + [ cmdenv.destPlace ], + cmdenv.endJumps, + "--to" + ) + checkForEmptyStationList( + "--to", cmdenv.destPlace, + cmdenv.destinations, cmdenv.endJumps + ) + else: + if cmdenv.endJumps: + raise CommandLineError("--end-jumps (-e) only works with --to") - if cmdenv.stopStation: - if cmdenv.hops == 1 and cmdenv.startStation: - if cmdenv.startStation == cmdenv.stopStation: - raise CommandLineError("Same to/from; more than one hop required.") + origins, destns = cmdenv.origins or [], cmdenv.destinations or [] + + if cmdenv.hops == 1 and len(origins) == 1 and len(destns) == 1: + if origins == destns: + raise CommandLineError("Same to/from; more than one hop required.") viaSet = cmdenv.viaSet = set(cmdenv.viaPlaces) cmdenv.DEBUG0("Via: {}", viaSet) @@ -357,6 +467,9 @@ def validateRunArguments(tdb, cmdenv): else: viaSystems.add(place) + checkAnchorNotInVia(cmdenv.hops, "--from", cmdenv.origPlace, viaSet) + checkAnchorNotInVia(cmdenv.hops, "--to", cmdenv.destPlace, viaSet) + avoids = cmdenv.avoidPlaces or [] for via in viaSet: if isinstance(via, Station): @@ -407,33 +520,28 @@ def validateRunArguments(tdb, cmdenv): if cmdenv.insurance and cmdenv.insurance >= (cmdenv.credits + arbitraryInsuranceBuffer): raise CommandLineError("Insurance leaves no margin for trade") - startStn, stopStn = cmdenv.startStation, cmdenv.stopStation + # Filter from, via and to stations based on additional user criteria: + if not isinstance(cmdenv.origPlace, Station) and not cmdenv.startJumps: + cmdenv.origins = filterStationSet('--from', cmdenv, cmdenv.origins) + if not isinstance(cmdenv.destPlace, Station) and not cmdenv.endJumps: + cmdenv.destinations = filterStationSet('--to', cmdenv, cmdenv.destinations) + cmdenv.viaSet = filterStationSet('--via', cmdenv, cmdenv.viaSet) + if cmdenv.unique and cmdenv.hops >= len(tdb.stationByID): raise CommandLineError("Requested unique trip with more hops than there are stations...") if cmdenv.unique: - startConflict = (startStn and (startStn == stopStn or startStn in viaSet)) - stopConflict = (stopStn and stopStn in viaSet) - if startConflict or stopConflict: - raise CommandLineError("from/to/via repeat conflicts with --unique") + # if there's only one start and stop... + if len(origins) == 1 and len(destns) == 1: + raise CommandLineError("Can't have same from/to with --unique") + if viaSet: + if len(origins) == 1 and origins[0] in viaSet: + raise("Can't have --from station in --via list with --unique") + if len(destns) == 1 and destns[1] in viaSet: + raise("Can't have --to station in --via list with --unique") if cmdenv.mfd: cmdenv.mfd.display("Loading Trades") - if startStn and startStn.itemCount == 0: - raise NoDataError("Start station {} doesn't have any price data.".format( - startStn.name())) - if stopStn and stopStn.itemCount == 0: - raise NoDataError("End station {} doesn't have any price data.".format( - stopStn.name())) - if cmdenv.origins: - tradingOrigins = [ - stn for stn in cmdenv.origins - if stn.itemCount > 0 - ] - if not tradingOrigins: - raise NoDataError("No price data at origin stations.") - cmdenv.origins = tradingOrigins - ###################################################################### @@ -480,8 +588,6 @@ def filterByVia(routes, viaSet, viaStartPos): # Perform query and populate result set def run(results, cmdenv, tdb): - from commands.commandenv import ResultRow - cmdenv.DEBUG1("loading trades") if tdb.tradingCount == 0: @@ -492,16 +598,8 @@ def run(results, cmdenv, tdb): from tradecalc import TradeCalc, Route origPlace, viaSet = cmdenv.origPlace, cmdenv.viaSet - avoidPlaces = cmdenv.avoidPlaces - - if cmdenv.destPlace: - if isinstance(cmdenv.destPlace, System): - stopStations = set(cmdenv.destPlace.stations) - else: - stopStations = set([cmdenv.destPlace]) - else: - stopStations = set() + stopStations = cmdenv.destinations startCr = cmdenv.credits - cmdenv.insurance @@ -519,21 +617,6 @@ def run(results, cmdenv, tdb): viaStartPos = 1 if origPlace else 0 cmdenv.maxJumps = None - cmdenv.DEBUG0( - "From {fromStn}, To {toStn}, Via {via}, " - "Cap {cap}, Credits {cr}, " - "Hops {hops}, Jumps/Hop {jumpsPer}, Ly/Jump {lyPer:.2f}" - "\n".format( - fromStn=origPlace.name() if origPlace else 'Anywhere', - toStn=str([s.name() for s in stopStations]) if stopStations else 'Anywhere', - via=';'.join([stn.name() for stn in viaSet]) or 'None', - cap=cmdenv.capacity, - cr=startCr, - hops=numHops, - jumpsPer=cmdenv.maxJumpsPer, - lyPer=cmdenv.maxLyPer, - )) - # Instantiate the calculator object calc = TradeCalc(tdb, cmdenv) @@ -545,7 +628,7 @@ def run(results, cmdenv, tdb): for hopNo in range(numHops): if not cmdenv.quiet and not cmdenv.debug: - print("* Hop {}...".format(hopNo+1), end='\r') + print("* Hop {:3n}: {:.>10n} routes".format(hopNo+1, len(routes)), end='\r') elif cmdenv.debug: cmdenv.DEBUG0("Hop {}...", hopNo+1) @@ -568,6 +651,7 @@ def run(results, cmdenv, tdb): stn.name() for stn in restrictions[0:-1] ]) dests += " or " + restrictions[-1].name() + print(restrictions) results.summary.exception += ( "SORRY: Could not find any routes that " "delivered a profit to {} at hop #{}\n" @@ -583,7 +667,7 @@ def run(results, cmdenv, tdb): break routes = newRoutes if not cmdenv.quiet: - print("{:20}".format(" "), end='\r') + print("{:40}".format(" "), end='\r') if not routes: raise NoDataError("No profitable trades matched your critera, or price data along the route is missing.") @@ -603,8 +687,6 @@ def run(results, cmdenv, tdb): # Transform result set into output def render(results, cmdenv, tdb): - from formatting import RowFormat, ColumnFormat - exception = results.summary.exception if exception: print('#' * 76) diff --git a/data/Station.csv b/data/Station.csv index 8c85fe23..7b5d5e77 100644 --- a/data/Station.csv +++ b/data/Station.csv @@ -1268,8 +1268,8 @@ unq:name@System.system_id,unq:name,ls_from_star,blackmarket,max_pad_size 'GLUSARGIZ','Oren Dock',250,'?','?' 'GLUTIA','Powers Terminal',0,'?','?' 'GLUTIA','Windt Hanger',0,'?','?' -'GNOWEE','Abasheli City',0,'?','?' -'GNOWEE','Kelleam Port',0,'?','?' +'GNOWEE','Abasheli City',1424,'?','M' +'GNOWEE','Kelleam Port',1916,'?','M' 'GOMAN','Gustav Sporer Port',291,'N','L' 'GONDAN','Bouwens Dock',199,'N','M' 'GONDAN','Draper Terminal',96,'N','L' @@ -1844,8 +1844,8 @@ unq:name@System.system_id,unq:name,ls_from_star,blackmarket,max_pad_size 'KWATEE','Smith Dock',472,'?','?' 'KWATILES','Elion Works',1598,'Y','M' 'L 190-21','Gurragchaa Orbital',946,'Y','L' -'L 258-146','Mendeleev Hanger',0,'?','?' -'L 258-146','Tito Camp',0,'?','?' +'L 258-146','Mendeleev Hanger',402,'?','M' +'L 258-146','Tito Camp',696,'?','M' 'L 26-27','Chretien Colony',0,'?','M' 'L 26-27','McCoy City',417,'N','L' 'LA ROCHELLE','Shaver Dock',0,'?','?' @@ -2675,9 +2675,9 @@ unq:name@System.system_id,unq:name,ls_from_star,blackmarket,max_pad_size 'MUTUJIALI','Siegel Orbital',0,'?','?' 'MZ URSAE MAJORIS','Collins Settlement',1522,'N','M' 'MZ URSAE MAJORIS','Tenn Landing',1445,'N','L' -'NAGNATAE','Cartwright City',0,'?','?' -'NAGNATAE','Howard City',0,'?','?' -'NAGNATAE','Kondakova Port',0,'?','?' +'NAGNATAE','Cartwright City',1122,'?','M' +'NAGNATAE','Howard City',817,'?','L' +'NAGNATAE','Kondakova Port',1547,'?','M' 'NAITI','Deluc Settlement',0,'?','?' 'NAITS','McDivitt Orbital',1368,'N','L' 'NAKUMA','Nelder Mines',0,'?','?' @@ -2785,8 +2785,8 @@ unq:name@System.system_id,unq:name,ls_from_star,blackmarket,max_pad_size 'NU INDI','Shatalov Colony',281,'Y','M' 'NU TAURI','Faraday Dock',1124,'?','?' 'NU TAURI','Parmitano Terminal',405,'?','?' -'NU-2 LUPI','Gauss City',0,'?','?' -'NU-2 LUPI','Pudwill Gorie Holdings',0,'?','?' +'NU-2 LUPI','Gauss City',2403,'N','M' +'NU-2 LUPI','Pudwill Gorie Holdings',3438,'?','M' 'NUAKEA','Oblivion',1183,'Y','L' 'NUENETS','Forrester Dock',11425,'N','M' 'NUENETS','Harbaugh Port',11349,'N','M' diff --git a/trade.py b/trade.py index b01e50ca..105f5f88 100755 --- a/trade.py +++ b/trade.py @@ -46,6 +46,27 @@ def main(argv): cmdenv = cmdIndex.parse(sys.argv) tdb = tradedb.TradeDB(cmdenv, load=cmdenv.wantsTradeDB) + if cmdenv.usesTradeData: + tsc = tdb.tradingStationCount + if tsc == 0: + raise exceptions.NoDataError( + "There is no trading data for ANY station in " + "the local database. Please enter or import " + "price data." + ) + if tsc == 1: + raise exceptions.NoDataError( + "The local database only contains trading data " + "for one station. Please enter or import data " + "for additional stations." + ) + if tsc < 8: + cmdenv.NOTE( + "The local database only contains trading data " + "for {} stations. Please enter or import data " + "for additional stations.".format( + tsc + )) results = cmdenv.run(tdb) if results: diff --git a/tradecalc.py b/tradecalc.py index 2da73045..bd907983 100644 --- a/tradecalc.py +++ b/tradecalc.py @@ -585,6 +585,7 @@ def getBestHops(self, routes, restrictTo=None): assert not restrictTo or isinstance(restrictTo, set) maxJumpsPer = tdenv.maxJumpsPer maxLyPer = tdenv.maxLyPer + reqBlackMarket = getattr(tdenv, 'blackMarket', False) credits = tdenv.credits - getattr(tdenv, 'insurance', 0) bestToDest = {} @@ -686,6 +687,9 @@ def considerStation(dstStation, dest): if unique and dstStation in route.route: continue + if reqBlackMarket and dstStation.blackMarket != 'Y': + continue + if tdenv.debug >= 1: tdenv.DEBUG1("destSys {}, destStn {}, jumps {}, distLy {}", dstStation.system.dbname, diff --git a/tradedb.py b/tradedb.py index 626b204b..1ff74808 100644 --- a/tradedb.py +++ b/tradedb.py @@ -411,6 +411,7 @@ def __init__(self, self.pricesFilename = str(self.pricesPath) self.avgSelling, self.avgBuying = None, None + self.tradingStationCount = 0 if load: self.reloadCache() @@ -701,6 +702,7 @@ def _loadStations(self): self.cur.execute(stmt) stationByID = {} systemByID = self.systemByID + self.tradingStationCount = 0 for ( ID, systemID, name, lsFromStar, blackMarket, maxPadSize, @@ -711,6 +713,8 @@ def _loadStations(self): lsFromStar, blackMarket, maxPadSize, itemCount ) + if itemCount > 0: + self.tradingStationCount += 1 stationByID[ID] = station self.stationByID = stationByID