diff --git a/nullptr/analyzer.py b/nullptr/analyzer.py index 2bf0c77..017ddc3 100644 --- a/nullptr/analyzer.py +++ b/nullptr/analyzer.py @@ -3,7 +3,16 @@ from nullptr.models.jumpgate import Jumpgate from nullptr.models.system import System from nullptr.models.waypoint import Waypoint from dataclasses import dataclass +from copy import copy +class AnalyzerException(Exception): + pass + +@dataclass +class Point: + x: int + y: int + @dataclass class TradeOption: resource: str @@ -70,27 +79,60 @@ class Analyzer: return results def solve_tsp(self, waypoints): - # todo actually try to solve it - return waypoints + wps = copy(waypoints) + path = [] + cur = Point(0,0) + while len(wps) > 0: + closest = wps[0] + for w in wps: + if w.distance(cur) < closest.distance(cur): + closest = w + cur = closest + path.append(closest) + wps.remove(closest) + return path def get_jumpgate(self, system): gates = self.store.all_members(system, Jumpgate) return next(gates, None) - - def find_path(self, orig, to, depth=100, seen=None): + + # dijkstra shmijkstra + def find_nav_path(self, orig, to, ran): + path = [] + mkts = [m.waypoint for m in self.store.all_members(orig.system, Marketplace)] + cur = orig + + while cur != to: + best = cur + bestdist = cur.distance(to) + if bestdist < ran: + path.append(to) + break + for m in mkts: + dist = m.distance(to) + if dist < bestdist and cur.distance(m) < ran: + best = m + bestdist = dist + if best == cur: + raise AnalyzerException(f'no path to {to}') + cur = best + path.append(cur) + return path + + def find_jump_path(self, orig, to, depth=100, seen=None): if depth < 1: return None if seen is None: seen = set() if type(orig) == System: orig = set([SearchNode(orig,None)]) - result = [n for n in orig if n.system==to] + result = [n for n in orig if n==to] if len(result) > 0: return result[0].path() dest = set() for o in orig: - jg = self.get_jumpgate(o.system) + jg = self.get_jumpgate(o) if jg is None: continue - for s in jg.systems: + for s in jg.connections: if s in seen: continue seen.add(s) dest.add(SearchNode(s, o)) @@ -128,3 +170,16 @@ class Analyzer: if best is None or best.score < o.score: best = o return best + + def market_scan(self, system): + m = [w.waypoint for w in self.store.all_members(system, Marketplace) if not w.is_fuel()] + ms = len(m) + path = self.solve_tsp(m) + cur = Point(0,0) + for w in path: + print(w, w.distance(cur)) + cur = w + far = self.store.get(Waypoint, 'X1-NN7-B7') + for w in path: + print(w, w.distance(far)) + \ No newline at end of file diff --git a/nullptr/api.py b/nullptr/api.py index a1209d1..2a64ba8 100644 --- a/nullptr/api.py +++ b/nullptr/api.py @@ -235,8 +235,9 @@ class Api: return result ######## Commerce ######### - def sell(self, ship, typ): - units = ship.get_cargo(typ) + def sell(self, ship, typ,units=None): + if units is None: + units = ship.get_cargo(typ) data = { 'symbol': typ, 'units': units diff --git a/nullptr/central_command.py b/nullptr/central_command.py index d931915..46f2f46 100644 --- a/nullptr/central_command.py +++ b/nullptr/central_command.py @@ -138,10 +138,10 @@ class CentralCommand: s.mission_state = {k: v.default for k,v in mclass.params().items()} self.start_mission(s) - def restart_mission(self, s): + def restart_mission(self, s, status='init'): if s not in self.missions: raise CentralCommandError("no mission assigned") - s.mission_status = 'init' + s.mission_status = status def start_mission(self, s): mtype = s.mission diff --git a/nullptr/commander.py b/nullptr/commander.py index c7a8034..84a4b25 100644 --- a/nullptr/commander.py +++ b/nullptr/commander.py @@ -1,6 +1,6 @@ from nullptr.command_line import CommandLine from nullptr.store import Store -from nullptr.analyzer import Analyzer +from nullptr.analyzer import Analyzer, Point import argparse from nullptr.models import * from nullptr.api import Api @@ -127,8 +127,10 @@ class Commander(CommandLine): pprint(self.ship.mission_state) def do_role(self, role): - roles = ['hauler'] + roles = [None, 'hauler'] if not self.has_ship(): return + if role == 'none': + role = None if role not in roles: print(f'role {role} not found. Choose from {roles}') return @@ -140,9 +142,9 @@ class Commander(CommandLine): self.centcom.init_mission(self.ship, arg) self.print_mission() - def do_mrestart(self): + def do_mrestart(self, status='init'): if not self.has_ship(): return - self.centcom.restart_mission(self.ship) + self.centcom.restart_mission(self.ship, status) self.print_mission() def do_mreset(self): @@ -217,11 +219,21 @@ class Commander(CommandLine): self.centcom.set_mission_param(self.ship, 'hops', markets) self.print_mission() + def totaldist(self, m): + t = 0 + o = Point(0,0) + for w in m: + t +=w.distance(o) + o = w + return t + def do_sprobe(self): if not self.has_ship(): return system = self.ship.location.system - m = self.store.all_members(system, 'Marketplace') + m = [m.waypoint for m in self.store.all_members(system, 'Marketplace')] + print("pre", self.totaldist(m)) m = self.analyzer.solve_tsp(m) + print("post", self.totaldist(m)) hops = [w.symbol for w in m] self.centcom.init_mission(self.ship, 'probe') self.centcom.set_mission_param(self.ship, 'hops', hops) @@ -262,6 +274,10 @@ class Commander(CommandLine): if w.type == 'JUMP_GATE': self.api.jumps(w) + def do_market_scan(self): + if not self.has_ship(): return + loc = self.ship.location.system + pprint(self.analyzer.market_scan(loc)) def do_system(self, system_str): system = self.store.get(System, system_str) @@ -282,6 +298,8 @@ class Commander(CommandLine): traits = [] if w.type == 'JUMP_GATE': traits.append('JUMP') + if w.type == 'GAS_GIANT': + traits.append('GAS') if 'SHIPYARD' in w.traits: traits.append('SHIPYARD') if 'MARKETPLACE' in w.traits: @@ -293,8 +311,6 @@ class Commander(CommandLine): traits.append('METAL') if 'PRECIOUS_METAL_DEPOSITS' in w.traits: traits.append('GOLD') - if 'EXPLOSIVE_GASES' in w.traits: - traits.append('GAS') if 'MINERAL_DEPOSITS' in w.traits: traits.append('MINS') if 'STRIPPED' in w.traits: @@ -362,7 +378,7 @@ class Commander(CommandLine): if arg.startswith('r'): r = self.api.list_ships() else: - r = list(self.store.all('Ship')) + r = sorted(list(self.store.all('Ship'))) pprint(r) def do_contracts(self, arg=''): @@ -482,9 +498,9 @@ class Commander(CommandLine): self.api.buy(self.ship, resource.upper(), amt) self.do_cargo() - def do_sell(self, resource): + def do_sell(self, resource, amt=None): if not self.has_ship(): return - self.api.sell(self.ship, resource.upper()) + self.api.sell(self.ship, resource.upper(), amt) self.do_cargo() def do_dump(self, resource): diff --git a/nullptr/missions/base.py b/nullptr/missions/base.py index 09bf769..e8073f4 100644 --- a/nullptr/missions/base.py +++ b/nullptr/missions/base.py @@ -173,7 +173,14 @@ class BaseMission(Mission): sellables.remove(target) if len(sellables) == 0: return 'done' - self.api.sell(self.ship, sellables[0]) + resource = sellables[0] + volume = market.volume(resource) + + amount = self.ship.get_cargo(resource) + while amount > 0: + amt = min(amount, volume) + self.api.sell(self.ship, resource, amt) + amount -= amt if len(sellables) == 1: return 'done' else: @@ -186,19 +193,21 @@ class BaseMission(Mission): loc = self.ship.location market = self.store.get('Marketplace', loc.symbol) price = market.buy_price(resource) + volume = market.volume(resource) affordable = credits // price amount = min(cargo_space, affordable) - self.api.buy(self.ship, resource, amount) + while amount > 0: + amt = min(amount, volume) + self.api.buy(self.ship, resource, amt) + amount -= amt def step_travel(self): traject = self.st('traject') if traject is None or traject == []: - return 'done' + return dest = traject[-1] loc = self.ship.location - if dest == loc: - self.sts('traject', None) - return 'done' + hop = traject.pop(0) if type(hop) == Waypoint: self.api.navigate(self.ship, hop) @@ -206,11 +215,21 @@ class BaseMission(Mission): else: self.api.jump(self.ship, hop) self.next_step = self.ship.cooldown - if traject == []: - traject= None self.sts('traject', traject) + + def step_navigate_traject(self): + traject = self.st('traject') + + + loc = self.ship.location + + if traject is None or traject == []: + return 'done' + dest =traject[-1] + if dest == loc: + return 'done' return 'more' - + def step_calculate_traject(self, dest): if type(dest) == str: dest = self.store.get(Waypoint, dest) @@ -222,7 +241,7 @@ class BaseMission(Mission): dest_sys = dest.system dest_jg = self.analyzer.get_jumpgate(dest_sys) if dest_sys == loc_sys: - result = [dest] + result = self.analyzer.find_nav_path(loc, dest, self.ship.range()) self.sts('traject', result) return path = self.analyzer.find_path(loc_sys, dest_sys) @@ -237,30 +256,47 @@ class BaseMission(Mission): return result def step_dock(self): + if self.ship.status == 'DOCKED': + return self.api.dock(self.ship) def step_refuel(self): if self.ship.fuel_capacity == 0: return - if self.ship.fuel_current / self.ship.fuel_capacity < 0.5: - try: - self.api.refuel(self.ship) - except Exception as e: - pass + #if self.ship.fuel_capacity - self.ship.fuel_current > 100: + try: + self.api.refuel(self.ship) + except Exception as e: + pass def step_orbit(self): + if self.ship.status != 'DOCKED': + return self.api.orbit(self.ship) def travel_steps(self, nm, destination, next_step): destination = self.st(destination) calc = partial(self.step_calculate_traject, destination) - return { - f'travel-{nm}': (self.step_orbit, f'calc-trav-{nm}'), - f'calc-trav-{nm}': (calc, f'go-{nm}'), - f'go-{nm}': (self.step_travel, { - 'done': f'dock-{nm}', - 'more': f'go-{nm}' - }), + steps = { + + f'travel-{nm}': (calc, f'dock-{nm}'), f'dock-{nm}': (self.step_dock, f'refuel-{nm}'), - f'refuel-{nm}': (self.step_refuel, next_step) + f'refuel-{nm}': (self.step_refuel, f'orbit-{nm}'), + f'orbit-{nm}': (self.step_orbit, f'go-{nm}'), + f'go-{nm}': (self.step_travel, f'nav-{nm}'), + f'nav-{nm}': (self.step_navigate_traject, { + 'done': next_step, + 'more': f'dock-{nm}' + }) } + if self.ship.fuel_capacity == 0: + steps = { + + f'travel-{nm}': (calc, f'go-{nm}'), + f'go-{nm}': (self.step_travel, f'nav-{nm}'), + f'nav-{nm}': (self.step_navigate_traject, { + 'done': next_step, + 'more': f'go-{nm}' + }), + } + return steps diff --git a/nullptr/missions/haul.py b/nullptr/missions/haul.py index 5de19cc..1b2bdd4 100644 --- a/nullptr/missions/haul.py +++ b/nullptr/missions/haul.py @@ -18,10 +18,13 @@ class HaulMission(BaseMission): def steps(self): return { - **self.travel_steps('to', 'site', 'market'), - 'market': (self.step_market, 'load'), - 'load': (self.step_load, 'travel-back'), - **self.travel_steps('back', 'dest', 'unload'), + **self.travel_steps('to', 'site', 'dock'), + 'dock': (self.step_dock, 'market-pre'), + 'market-pre': (self.step_market, 'load'), + 'load': (self.step_load, 'market-post'), + 'market-post': (self.step_market, 'travel-back'), + **self.travel_steps('back', 'dest', 'dock-dest'), + 'dock-dest': (self.step_dock, 'unload'), 'unload': (self.step_unload, 'market-dest'), 'market-dest': (self.step_market, 'done'), } diff --git a/nullptr/models/base.py b/nullptr/models/base.py index 0f7ba6d..892bb40 100644 --- a/nullptr/models/base.py +++ b/nullptr/models/base.py @@ -57,6 +57,9 @@ class Base: val = interp(val) setattr(self, attr, val) + def __lt__(self, o): + return self.symbol < o.symbol + def setlst(self, attr, d, name, member=None, interp=None): val = sg(d, name) if val is not None: diff --git a/nullptr/models/marketplace.py b/nullptr/models/marketplace.py index d511d61..834a916 100644 --- a/nullptr/models/marketplace.py +++ b/nullptr/models/marketplace.py @@ -19,6 +19,9 @@ class Marketplace(Base): waypoint = self.store.get(Waypoint, self.symbol, create=True) self.waypoint = waypoint + def is_fuel(self): + return self.imports + self.exports + self.exchange == ['FUEL'] + def update(self, d): self.setlst('imports', d, 'imports', 'symbol') self.setlst('exports', d, 'exports', 'symbol') @@ -40,6 +43,11 @@ class Marketplace(Base): if resource not in self.prices: return None return self.prices[resource]['buy'] + + def volume(self, resource): + if resource not in self.prices: + return None + return self.prices[resource]['volume'] def sellable_items(self, resources): return [r for r in resources if r in self.prices] diff --git a/nullptr/models/ship.py b/nullptr/models/ship.py index 006c02d..82cf99c 100644 --- a/nullptr/models/ship.py +++ b/nullptr/models/ship.py @@ -23,6 +23,11 @@ class Ship(Base): def ext(self): return 'shp' + def range(self): + if self.fuel_capacity == 0: + return 100000 + return self.fuel_capacity + def update(self, d): self.seta('status', d, 'nav.status') getter = self.store.getter(Waypoint, create=True)