improving haulers

This commit is contained in:
Richard 2024-01-06 07:17:53 +01:00
parent 524ba45639
commit 2181583843
9 changed files with 175 additions and 48 deletions

View File

@ -3,7 +3,16 @@ from nullptr.models.jumpgate import Jumpgate
from nullptr.models.system import System from nullptr.models.system import System
from nullptr.models.waypoint import Waypoint from nullptr.models.waypoint import Waypoint
from dataclasses import dataclass from dataclasses import dataclass
from copy import copy
class AnalyzerException(Exception):
pass
@dataclass
class Point:
x: int
y: int
@dataclass @dataclass
class TradeOption: class TradeOption:
resource: str resource: str
@ -70,27 +79,60 @@ class Analyzer:
return results return results
def solve_tsp(self, waypoints): def solve_tsp(self, waypoints):
# todo actually try to solve it wps = copy(waypoints)
return 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): def get_jumpgate(self, system):
gates = self.store.all_members(system, Jumpgate) gates = self.store.all_members(system, Jumpgate)
return next(gates, None) 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 depth < 1: return None
if seen is None: if seen is None:
seen = set() seen = set()
if type(orig) == System: if type(orig) == System:
orig = set([SearchNode(orig,None)]) 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: if len(result) > 0:
return result[0].path() return result[0].path()
dest = set() dest = set()
for o in orig: for o in orig:
jg = self.get_jumpgate(o.system) jg = self.get_jumpgate(o)
if jg is None: continue if jg is None: continue
for s in jg.systems: for s in jg.connections:
if s in seen: continue if s in seen: continue
seen.add(s) seen.add(s)
dest.add(SearchNode(s, o)) dest.add(SearchNode(s, o))
@ -128,3 +170,16 @@ class Analyzer:
if best is None or best.score < o.score: if best is None or best.score < o.score:
best = o best = o
return best 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))

View File

@ -235,8 +235,9 @@ class Api:
return result return result
######## Commerce ######### ######## Commerce #########
def sell(self, ship, typ): def sell(self, ship, typ,units=None):
units = ship.get_cargo(typ) if units is None:
units = ship.get_cargo(typ)
data = { data = {
'symbol': typ, 'symbol': typ,
'units': units 'units': units

View File

@ -138,10 +138,10 @@ class CentralCommand:
s.mission_state = {k: v.default for k,v in mclass.params().items()} s.mission_state = {k: v.default for k,v in mclass.params().items()}
self.start_mission(s) self.start_mission(s)
def restart_mission(self, s): def restart_mission(self, s, status='init'):
if s not in self.missions: if s not in self.missions:
raise CentralCommandError("no mission assigned") raise CentralCommandError("no mission assigned")
s.mission_status = 'init' s.mission_status = status
def start_mission(self, s): def start_mission(self, s):
mtype = s.mission mtype = s.mission

View File

@ -1,6 +1,6 @@
from nullptr.command_line import CommandLine from nullptr.command_line import CommandLine
from nullptr.store import Store from nullptr.store import Store
from nullptr.analyzer import Analyzer from nullptr.analyzer import Analyzer, Point
import argparse import argparse
from nullptr.models import * from nullptr.models import *
from nullptr.api import Api from nullptr.api import Api
@ -127,8 +127,10 @@ class Commander(CommandLine):
pprint(self.ship.mission_state) pprint(self.ship.mission_state)
def do_role(self, role): def do_role(self, role):
roles = ['hauler'] roles = [None, 'hauler']
if not self.has_ship(): return if not self.has_ship(): return
if role == 'none':
role = None
if role not in roles: if role not in roles:
print(f'role {role} not found. Choose from {roles}') print(f'role {role} not found. Choose from {roles}')
return return
@ -140,9 +142,9 @@ class Commander(CommandLine):
self.centcom.init_mission(self.ship, arg) self.centcom.init_mission(self.ship, arg)
self.print_mission() self.print_mission()
def do_mrestart(self): def do_mrestart(self, status='init'):
if not self.has_ship(): return if not self.has_ship(): return
self.centcom.restart_mission(self.ship) self.centcom.restart_mission(self.ship, status)
self.print_mission() self.print_mission()
def do_mreset(self): def do_mreset(self):
@ -217,11 +219,21 @@ class Commander(CommandLine):
self.centcom.set_mission_param(self.ship, 'hops', markets) self.centcom.set_mission_param(self.ship, 'hops', markets)
self.print_mission() 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): def do_sprobe(self):
if not self.has_ship(): return if not self.has_ship(): return
system = self.ship.location.system 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) m = self.analyzer.solve_tsp(m)
print("post", self.totaldist(m))
hops = [w.symbol for w in m] hops = [w.symbol for w in m]
self.centcom.init_mission(self.ship, 'probe') self.centcom.init_mission(self.ship, 'probe')
self.centcom.set_mission_param(self.ship, 'hops', hops) self.centcom.set_mission_param(self.ship, 'hops', hops)
@ -262,6 +274,10 @@ class Commander(CommandLine):
if w.type == 'JUMP_GATE': if w.type == 'JUMP_GATE':
self.api.jumps(w) 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): def do_system(self, system_str):
system = self.store.get(System, system_str) system = self.store.get(System, system_str)
@ -282,6 +298,8 @@ class Commander(CommandLine):
traits = [] traits = []
if w.type == 'JUMP_GATE': if w.type == 'JUMP_GATE':
traits.append('JUMP') traits.append('JUMP')
if w.type == 'GAS_GIANT':
traits.append('GAS')
if 'SHIPYARD' in w.traits: if 'SHIPYARD' in w.traits:
traits.append('SHIPYARD') traits.append('SHIPYARD')
if 'MARKETPLACE' in w.traits: if 'MARKETPLACE' in w.traits:
@ -293,8 +311,6 @@ class Commander(CommandLine):
traits.append('METAL') traits.append('METAL')
if 'PRECIOUS_METAL_DEPOSITS' in w.traits: if 'PRECIOUS_METAL_DEPOSITS' in w.traits:
traits.append('GOLD') traits.append('GOLD')
if 'EXPLOSIVE_GASES' in w.traits:
traits.append('GAS')
if 'MINERAL_DEPOSITS' in w.traits: if 'MINERAL_DEPOSITS' in w.traits:
traits.append('MINS') traits.append('MINS')
if 'STRIPPED' in w.traits: if 'STRIPPED' in w.traits:
@ -362,7 +378,7 @@ class Commander(CommandLine):
if arg.startswith('r'): if arg.startswith('r'):
r = self.api.list_ships() r = self.api.list_ships()
else: else:
r = list(self.store.all('Ship')) r = sorted(list(self.store.all('Ship')))
pprint(r) pprint(r)
def do_contracts(self, arg=''): def do_contracts(self, arg=''):
@ -482,9 +498,9 @@ class Commander(CommandLine):
self.api.buy(self.ship, resource.upper(), amt) self.api.buy(self.ship, resource.upper(), amt)
self.do_cargo() self.do_cargo()
def do_sell(self, resource): def do_sell(self, resource, amt=None):
if not self.has_ship(): return if not self.has_ship(): return
self.api.sell(self.ship, resource.upper()) self.api.sell(self.ship, resource.upper(), amt)
self.do_cargo() self.do_cargo()
def do_dump(self, resource): def do_dump(self, resource):

View File

@ -173,7 +173,14 @@ class BaseMission(Mission):
sellables.remove(target) sellables.remove(target)
if len(sellables) == 0: if len(sellables) == 0:
return 'done' 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: if len(sellables) == 1:
return 'done' return 'done'
else: else:
@ -186,19 +193,21 @@ class BaseMission(Mission):
loc = self.ship.location loc = self.ship.location
market = self.store.get('Marketplace', loc.symbol) market = self.store.get('Marketplace', loc.symbol)
price = market.buy_price(resource) price = market.buy_price(resource)
volume = market.volume(resource)
affordable = credits // price affordable = credits // price
amount = min(cargo_space, affordable) 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): def step_travel(self):
traject = self.st('traject') traject = self.st('traject')
if traject is None or traject == []: if traject is None or traject == []:
return 'done' return
dest = traject[-1] dest = traject[-1]
loc = self.ship.location loc = self.ship.location
if dest == loc:
self.sts('traject', None)
return 'done'
hop = traject.pop(0) hop = traject.pop(0)
if type(hop) == Waypoint: if type(hop) == Waypoint:
self.api.navigate(self.ship, hop) self.api.navigate(self.ship, hop)
@ -206,11 +215,21 @@ class BaseMission(Mission):
else: else:
self.api.jump(self.ship, hop) self.api.jump(self.ship, hop)
self.next_step = self.ship.cooldown self.next_step = self.ship.cooldown
if traject == []:
traject= None
self.sts('traject', traject) 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' return 'more'
def step_calculate_traject(self, dest): def step_calculate_traject(self, dest):
if type(dest) == str: if type(dest) == str:
dest = self.store.get(Waypoint, dest) dest = self.store.get(Waypoint, dest)
@ -222,7 +241,7 @@ class BaseMission(Mission):
dest_sys = dest.system dest_sys = dest.system
dest_jg = self.analyzer.get_jumpgate(dest_sys) dest_jg = self.analyzer.get_jumpgate(dest_sys)
if dest_sys == loc_sys: if dest_sys == loc_sys:
result = [dest] result = self.analyzer.find_nav_path(loc, dest, self.ship.range())
self.sts('traject', result) self.sts('traject', result)
return return
path = self.analyzer.find_path(loc_sys, dest_sys) path = self.analyzer.find_path(loc_sys, dest_sys)
@ -237,30 +256,47 @@ class BaseMission(Mission):
return result return result
def step_dock(self): def step_dock(self):
if self.ship.status == 'DOCKED':
return
self.api.dock(self.ship) self.api.dock(self.ship)
def step_refuel(self): def step_refuel(self):
if self.ship.fuel_capacity == 0: if self.ship.fuel_capacity == 0:
return return
if self.ship.fuel_current / self.ship.fuel_capacity < 0.5: #if self.ship.fuel_capacity - self.ship.fuel_current > 100:
try: try:
self.api.refuel(self.ship) self.api.refuel(self.ship)
except Exception as e: except Exception as e:
pass pass
def step_orbit(self): def step_orbit(self):
if self.ship.status != 'DOCKED':
return
self.api.orbit(self.ship) self.api.orbit(self.ship)
def travel_steps(self, nm, destination, next_step): def travel_steps(self, nm, destination, next_step):
destination = self.st(destination) destination = self.st(destination)
calc = partial(self.step_calculate_traject, destination) calc = partial(self.step_calculate_traject, destination)
return { steps = {
f'travel-{nm}': (self.step_orbit, f'calc-trav-{nm}'),
f'calc-trav-{nm}': (calc, f'go-{nm}'), f'travel-{nm}': (calc, f'dock-{nm}'),
f'go-{nm}': (self.step_travel, {
'done': f'dock-{nm}',
'more': f'go-{nm}'
}),
f'dock-{nm}': (self.step_dock, f'refuel-{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

View File

@ -18,10 +18,13 @@ class HaulMission(BaseMission):
def steps(self): def steps(self):
return { return {
**self.travel_steps('to', 'site', 'market'), **self.travel_steps('to', 'site', 'dock'),
'market': (self.step_market, 'load'), 'dock': (self.step_dock, 'market-pre'),
'load': (self.step_load, 'travel-back'), 'market-pre': (self.step_market, 'load'),
**self.travel_steps('back', 'dest', 'unload'), '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'), 'unload': (self.step_unload, 'market-dest'),
'market-dest': (self.step_market, 'done'), 'market-dest': (self.step_market, 'done'),
} }

View File

@ -57,6 +57,9 @@ class Base:
val = interp(val) val = interp(val)
setattr(self, attr, val) setattr(self, attr, val)
def __lt__(self, o):
return self.symbol < o.symbol
def setlst(self, attr, d, name, member=None, interp=None): def setlst(self, attr, d, name, member=None, interp=None):
val = sg(d, name) val = sg(d, name)
if val is not None: if val is not None:

View File

@ -19,6 +19,9 @@ class Marketplace(Base):
waypoint = self.store.get(Waypoint, self.symbol, create=True) waypoint = self.store.get(Waypoint, self.symbol, create=True)
self.waypoint = waypoint self.waypoint = waypoint
def is_fuel(self):
return self.imports + self.exports + self.exchange == ['FUEL']
def update(self, d): def update(self, d):
self.setlst('imports', d, 'imports', 'symbol') self.setlst('imports', d, 'imports', 'symbol')
self.setlst('exports', d, 'exports', 'symbol') self.setlst('exports', d, 'exports', 'symbol')
@ -40,6 +43,11 @@ class Marketplace(Base):
if resource not in self.prices: if resource not in self.prices:
return None return None
return self.prices[resource]['buy'] 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): def sellable_items(self, resources):
return [r for r in resources if r in self.prices] return [r for r in resources if r in self.prices]

View File

@ -23,6 +23,11 @@ class Ship(Base):
def ext(self): def ext(self):
return 'shp' return 'shp'
def range(self):
if self.fuel_capacity == 0:
return 100000
return self.fuel_capacity
def update(self, d): def update(self, d):
self.seta('status', d, 'nav.status') self.seta('status', d, 'nav.status')
getter = self.store.getter(Waypoint, create=True) getter = self.store.getter(Waypoint, create=True)