haulin goods

This commit is contained in:
Richard 2024-01-04 21:34:31 +01:00
parent b47fa44cb0
commit 1b7a528655
15 changed files with 181 additions and 42 deletions

View File

@ -10,4 +10,4 @@ RUN chmod +x /app/main.py
VOLUME /data VOLUME /data
#ENTRYPOINT bash #ENTRYPOINT bash
ENTRYPOINT [ "python3", "/app/main.py"] ENTRYPOINT [ "python3", "/app/main.py"]
CMD ["-s", "/data/store.npt", "-x", "/data/cmd.hst"] CMD ["-d", "/data"]

View File

@ -1,9 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse import argparse
from nullptr.commander import Commander from nullptr.commander import Commander
import os
from nullptr.models.base import Base from nullptr.models.base import Base
def main(args): def main(args):
c = Commander(args.store_file) if not os.path.isdir(args.data_dir):
os.makedirs(args.data_dir )
c = Commander(args.data_dir)
c.run() c.run()
# X1-AG74-41076A # X1-AG74-41076A
@ -11,7 +14,6 @@ def main(args):
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('-s', '--store-file', default='data/store.npt') parser.add_argument('-d', '--data-dir', default='data')
parser.add_argument('-x', '--history-file', default='data/cmd.hst')
args = parser.parse_args() args = parser.parse_args()
main(args) main(args)

View File

@ -4,6 +4,15 @@ 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
@dataclass
class TradeOption:
resource: str
source: Waypoint
dest: Waypoint
margin: int
dist: int
score: float
@dataclass @dataclass
class SearchNode: class SearchNode:
system: System system: System
@ -88,3 +97,34 @@ class Analyzer:
if len(dest) == 0: if len(dest) == 0:
return None return None
return self.find_path(dest, to, depth-1, seen) return self.find_path(dest, to, depth-1, seen)
def prices(self, system):
prices = {}
for m in self.store.all_members(system, Marketplace):
for p in m.prices.values():
r = p['symbol']
if not r in prices:
prices[r] = []
prices[r].append({
'wp': m.waypoint,
'buy': p['buy'],
'sell': p['sell']
})
return prices
def find_trade(self, system):
prices = self.prices(system)
best = None
for resource, markets in prices.items():
source = sorted(markets, key=lambda x: x['buy'])[0]
dest = sorted(markets, key=lambda x: x['sell'])[-1]
margin = dest['sell'] -source['buy']
if margin < 0:
continue
dist = source['wp'].distance(dest['wp'])
dist = max(dist, 0.0001)
score = margin / dist
o = TradeOption(resource, source['wp'], dest['wp'], margin, dist, score)
if best is None or best.score < o.score:
best = o
return best

View File

@ -5,6 +5,7 @@ from random import choice
from time import sleep from time import sleep
from threading import Thread from threading import Thread
from nullptr.atlas_builder import AtlasBuilder from nullptr.atlas_builder import AtlasBuilder
from nullptr.analyzer import Analyzer
class CentralCommandError(Exception): class CentralCommandError(Exception):
pass pass
@ -14,9 +15,9 @@ class CentralCommand:
self.missions = {} self.missions = {}
self.stopping = False self.stopping = False
self.store = store self.store = store
self.analyzer = Analyzer(store)
self.api = api self.api = api
self.atlas_builder = AtlasBuilder(store, api) self.atlas_builder = AtlasBuilder(store, api)
self.update_missions()
def get_ready_missions(self): def get_ready_missions(self):
result = [] result = []
@ -26,6 +27,7 @@ class CentralCommand:
return result return result
def tick(self): def tick(self):
self.update_missions()
missions = self.get_ready_missions() missions = self.get_ready_missions()
if len(missions) == 0: return False if len(missions) == 0: return False
ship = choice(missions) ship = choice(missions)
@ -41,7 +43,6 @@ class CentralCommand:
self.run() self.run()
print('manual mode') print('manual mode')
def wait_for_stop(self): def wait_for_stop(self):
try: try:
input() input()
@ -52,7 +53,6 @@ class CentralCommand:
def run(self): def run(self):
self.update_missions()
while not self.stopping: while not self.stopping:
did_step = True did_step = True
request_counter = self.api.requests_sent request_counter = self.api.requests_sent
@ -86,18 +86,43 @@ class CentralCommand:
raise MissionError(e) raise MissionError(e)
return return
ship.set_mission_state(nm, parsed_val) ship.set_mission_state(nm, parsed_val)
def smipa(self,s,n,v):
self.set_mission_param(s,n,v)
def update_missions(self): def update_missions(self):
for s in self.store.all(Ship): for s in self.store.all(Ship):
if s.mission_status == 'done':
s.mission = None
if s.mission is None: if s.mission is None:
if s in self.missions: if s in self.missions:
self.stop_mission(s) self.stop_mission(s)
elif s not in self.missions: if s.mission is None:
self.assign_mission(s)
if s.mission is not None and s not in self.missions:
self.start_mission(s) self.start_mission(s)
if s in self.missions: if s in self.missions:
m = self.missions[s] m = self.missions[s]
m.next_step = max(s.cooldown, s.arrival)
def assign_mission(self, s):
if s.role == 'hauler':
self.assign_haul(s)
def assign_haul(self, s):
t = self.analyzer.find_trade(s.location.system)
if t is None:
print(f"No trade for {s} found. Idling")
self.init_mission(s,'idle')
self.smipa(s, 'seconds', 600)
return
print(f'assigning {s} to deliver {t.resource} from {t.source} to {t.dest} at a margin of {t.margin}')
self.init_mission(s, 'haul')
self.smipa(s, 'resource', t.resource)
self.smipa(s, 'site', t.source)
self.smipa(s, 'dest', t.dest)
self.smipa(s, 'delivery', 'sell')
def init_mission(self, s, mtyp): def init_mission(self, s, mtyp):
if mtyp == 'none': if mtyp == 'none':
s.mission_state = {} s.mission_state = {}
@ -112,6 +137,11 @@ class CentralCommand:
s.mission_status = 'init' s.mission_status = 'init'
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):
if s not in self.missions:
raise CentralCommandError("no mission assigned")
s.mission_status = 'init'
def start_mission(self, s): def start_mission(self, s):
mtype = s.mission mtype = s.mission

View File

@ -17,11 +17,13 @@ class CommandError(Exception):
pass pass
class Commander(CommandLine): class Commander(CommandLine):
def __init__(self, store_file='data/store.npt', hist_file = 'data/cmd.hst'): def __init__(self, data_dir='data'):
store_file = os.path.join(data_dir, 'store.npt')
hist_file = os.path.join(data_dir, 'cmd.hst')
self.hist_file = hist_file self.hist_file = hist_file
if os.path.isfile(hist_file): if os.path.isfile(hist_file):
readline.read_history_file(hist_file) readline.read_history_file(hist_file)
self.store = Store(store_file) self.store = Store(store_file, True)
self.agent = self.select_agent() self.agent = self.select_agent()
self.api = Api(self.store, self.agent) self.api = Api(self.store, self.agent)
self.centcom = CentralCommand(self.store, self.api) self.centcom = CentralCommand(self.store, self.api)
@ -123,6 +125,14 @@ class Commander(CommandLine):
def print_mission(self): def print_mission(self):
print(f'mission: {self.ship.mission} ({self.ship.mission_status})') print(f'mission: {self.ship.mission} ({self.ship.mission_status})')
pprint(self.ship.mission_state) pprint(self.ship.mission_state)
def do_role(self, role):
roles = ['hauler']
if not self.has_ship(): return
if role not in roles:
print(f'role {role} not found. Choose from {roles}')
return
self.ship.role = role
def do_mission(self, arg=''): def do_mission(self, arg=''):
if not self.has_ship(): return if not self.has_ship(): return
@ -130,6 +140,11 @@ 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):
if not self.has_ship(): return
self.centcom.restart_mission(self.ship)
self.print_mission()
def do_mreset(self): def do_mreset(self):
if not self.has_ship(): return if not self.has_ship(): return
self.ship.mission_state = {} self.ship.mission_state = {}
@ -254,9 +269,11 @@ class Commander(CommandLine):
pprint(r) pprint(r)
def do_waypoints(self, system_str=''): def do_waypoints(self, system_str=''):
loc = None
if system_str == '': if system_str == '':
if not self.has_ship(): return if not self.has_ship(): return
system = self.ship.location.system loc = self.ship.location
system = loc.system
else: else:
system = self.store.get(System, system_str) system = self.store.get(System, system_str)
print(f'=== waypoints in {system}') print(f'=== waypoints in {system}')
@ -276,7 +293,7 @@ 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_GASSES' in w.traits: if 'EXPLOSIVE_GASES' in w.traits:
traits.append('GAS') traits.append('GAS')
if 'MINERAL_DEPOSITS' in w.traits: if 'MINERAL_DEPOSITS' in w.traits:
traits.append('MINS') traits.append('MINS')
@ -288,7 +305,12 @@ class Commander(CommandLine):
typ = w.type[0] typ = w.type[0]
if typ not in ['F','J'] and len(traits) == 0: if typ not in ['F','J'] and len(traits) == 0:
continue continue
print(f'{wname:4} {typ} {traits}')
if loc:
dist = loc.distance(w)
print(f'{wname:4} {typ} {dist:6} {traits}')
else:
print(f'{wname:4} {typ} {traits}')
def do_members(self): def do_members(self):
if not self.has_ship(): return if not self.has_ship(): return
@ -441,17 +463,7 @@ class Commander(CommandLine):
def do_prices(self, resource=None): def do_prices(self, resource=None):
if not self.has_ship(): return if not self.has_ship(): return
system = self.ship.location.system system = self.ship.location.system
prices = {} prices = self.analyzer.prices(system)
for m in self.store.all_members(system, Marketplace):
for p in m.prices.values():
r = p['symbol']
if not r in prices:
prices[r] = []
prices[r].append({
'wp': m.symbol,
'buy': p['buy'],
'sell': p['sell']
})
if resource is not None: if resource is not None:
pprint(prices[resource.upper()]) pprint(prices[resource.upper()])
else: else:

View File

@ -3,6 +3,8 @@ from nullptr.missions.mine import MiningMission
from nullptr.missions.haul import HaulMission from nullptr.missions.haul import HaulMission
from nullptr.missions.travel import TravelMission from nullptr.missions.travel import TravelMission
from nullptr.missions.probe import ProbeMission from nullptr.missions.probe import ProbeMission
from nullptr.missions.idle import IdleMission
def get_mission_class( mtype): def get_mission_class( mtype):
types = { types = {
@ -10,7 +12,8 @@ def get_mission_class( mtype):
'mine': MiningMission, 'mine': MiningMission,
'haul': HaulMission, 'haul': HaulMission,
'travel': TravelMission, 'travel': TravelMission,
'probe': ProbeMission 'probe': ProbeMission,
'idle': IdleMission
} }
if mtype not in types: if mtype not in types:
raise ValueError(f'invalid mission type {mtype}') raise ValueError(f'invalid mission type {mtype}')

View File

@ -97,7 +97,7 @@ class Mission:
logging.info(f'mission finished for {self.ship}') logging.info(f'mission finished for {self.ship}')
def is_waiting(self): def is_waiting(self):
return self.next_step > time() return self.next_step > time() or self.ship.cooldown > time() or self.ship.arrival > time()
def is_finished(self): def is_finished(self):
return self.status() in ['done','error'] return self.status() in ['done','error']
@ -147,6 +147,10 @@ class BaseMission(Mission):
self.api.navigate(self.ship, site) self.api.navigate(self.ship, site)
self.next_step = self.ship.arrival self.next_step = self.ship.arrival
def step_market(self):
loc = self.ship.location
self.api.marketplace(loc)
def step_unload(self): def step_unload(self):
delivery = self.st('delivery') delivery = self.st('delivery')
if delivery == 'sell': if delivery == 'sell':
@ -176,9 +180,15 @@ class BaseMission(Mission):
return 'more' return 'more'
def step_load(self): def step_load(self):
credits = self.api.agent.credits
cargo_space = self.ship.cargo_capacity - self.ship.cargo_units cargo_space = self.ship.cargo_capacity - self.ship.cargo_units
resource = self.st('resource') resource = self.st('resource')
self.api.buy(self.ship, resource, cargo_space) loc = self.ship.location
market = self.store.get('Marketplace', loc.symbol)
price = market.buy_price(resource)
affordable = credits // price
amount = min(cargo_space, affordable)
self.api.buy(self.ship, resource, amount)
def step_travel(self): def step_travel(self):
traject = self.st('traject') traject = self.st('traject')

View File

@ -18,8 +18,10 @@ class HaulMission(BaseMission):
def steps(self): def steps(self):
return { return {
**self.travel_steps('to', 'site', 'load'), **self.travel_steps('to', 'site', 'market'),
'market': (self.step_market, 'load'),
'load': (self.step_load, 'travel-back'), 'load': (self.step_load, 'travel-back'),
**self.travel_steps('back', 'dest', 'unload'), **self.travel_steps('back', 'dest', 'unload'),
'unload': (self.step_unload, 'travel-to'), 'unload': (self.step_unload, 'market-dest'),
'market-dest': (self.step_market, 'done'),
} }

26
nullptr/missions/idle.py Normal file
View File

@ -0,0 +1,26 @@
from nullptr.missions.base import BaseMission, MissionParam
import time
class IdleMission(BaseMission):
def start_state(self):
return 'start'
def step_wait(self):
self.next_step = int(time.time()) + self.st('seconds')
def step_idle(self):
pass
@classmethod
def params(cls):
return {
'seconds': MissionParam(int, True)
}
def steps(self):
return {
'start': (self.step_wait, 'wait'),
'wait': (self.step_idle, 'done')
}

View File

@ -20,10 +20,6 @@ class ProbeMission(BaseMission):
} }
def step_market(self):
loc = self.ship.location
self.api.marketplace(loc)
def step_next_hop(self): def step_next_hop(self):
hops = self.st('hops') hops = self.st('hops')
next_hop = self.st('next-hop') next_hop = self.st('next-hop')

View File

@ -35,7 +35,12 @@ class Marketplace(Base):
price['volume'] = mg(g, 'tradeVolume') price['volume'] = mg(g, 'tradeVolume')
prices[symbol] = price prices[symbol] = price
self.prices = prices self.prices = prices
def buy_price(self, resource):
if resource not in self.prices:
return None
return self.prices[resource]['buy']
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

@ -17,6 +17,7 @@ class Ship(Base):
self.fuel_capacity:int = 0 self.fuel_capacity:int = 0
self.mission:str = None self.mission:str = None
self.mission_status:str = 'init' self.mission_status:str = 'init'
self.role = None
@classmethod @classmethod
def ext(self): def ext(self):
@ -100,6 +101,8 @@ class Ship(Base):
cooldown = int(self.cooldown - time()) cooldown = int(self.cooldown - time())
r = self.symbol r = self.symbol
if detail > 1: if detail > 1:
if self.role is not None:
r += f' {self.role}'
r += ' ' + self.status r += ' ' + self.status
r += f' [{self.fuel_current}/{self.fuel_capacity}]' r += f' [{self.fuel_current}/{self.fuel_capacity}]'
r += ' ' + str(self.location) r += ' ' + str(self.location)

View File

@ -2,6 +2,7 @@ from .base import Base, Reference
from nullptr.models.system import System from nullptr.models.system import System
from nullptr.util import * from nullptr.util import *
from time import time from time import time
from math import sqrt
class Waypoint(Base): class Waypoint(Base):
def define(self): def define(self):
@ -23,6 +24,9 @@ class Waypoint(Base):
self.seta('is_under_construction', d, 'isUnderConstruction') self.seta('is_under_construction', d, 'isUnderConstruction')
self.setlst('traits', d, 'traits', 'symbol') self.setlst('traits', d, 'traits', 'symbol')
self.uncharted = 'UNCHARTED' in self.traits self.uncharted = 'UNCHARTED' in self.traits
def distance(self, other):
return int(sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2))
@classmethod @classmethod
def ext(self): def ext(self):

View File

@ -23,10 +23,12 @@ class StoreUnpickler(pickle.Unpickler):
if pers_id == "STORE": if pers_id == "STORE":
return self.store return self.store
raise pickle.UnpicklingError("I don know the persid!") raise pickle.UnpicklingError("I don know the persid!")
CHUNK_MAGIC = b'ChNk'
class ChunkHeader: class ChunkHeader:
def __init__(self): def __init__(self):
self.magic = CHUNK_MAGIC
self.offset = 0 self.offset = 0
self.in_use = True self.in_use = True
self.size = 0 self.size = 0
@ -35,14 +37,16 @@ class ChunkHeader:
@classmethod @classmethod
def parse(cls, fil): def parse(cls, fil):
offset = fil.tell() offset = fil.tell()
d = fil.read(16) d = fil.read(20)
if len(d) < 16: if len(d) < 20:
return None return None
o = cls() o = cls()
o.offset = offset o.offset = offset
d, o.used = unpack('<QQ', d) o.magic, d, o.used = unpack('<4sQQ', d)
o.size = d & 0x7fffffffffffffff o.size = d & 0x7fffffffffffffff
o.in_use = d & 0x8000000000000000 != 0 o.in_use = d & 0x8000000000000000 != 0
if o.magic != CHUNK_MAGIC:
raise ValueError(f"Invalid chunk magic: {o.magic}")
# print(o) # print(o)
return o return o
@ -50,7 +54,7 @@ class ChunkHeader:
d = self.size d = self.size
if self.in_use: if self.in_use:
d |= 1 << 63 d |= 1 << 63
d = pack('<QQ', d, self.used) d = pack('<4sQQ', self.magic, d, self.used)
f.write(d) f.write(d)
def __repr__(self): def __repr__(self):
@ -113,11 +117,13 @@ class Store:
self.p(hdr) self.p(hdr)
total += hdr.size total += hdr.size
if not hdr.in_use: if not hdr.in_use:
print(f"skip {hdr.size} {self.fil.tell()}")
self.fil.seek(hdr.size, 1) self.fil.seek(hdr.size, 1)
free += hdr.size free += hdr.size
else: else:
data = self.fil.read(hdr.used) data = self.fil.read(hdr.used)
self.load_object(data, offset) self.load_object(data, offset)
print(f"pad {hdr.size - hdr.used}")
self.fil.seek(hdr.size - hdr.used, 1) self.fil.seek(hdr.size - hdr.used, 1)
cnt += 1 cnt += 1
offset = self.fil.tell() offset = self.fil.tell()

View File

@ -50,7 +50,7 @@ class TestStore(unittest.TestCase):
dum2 = self.s.get(Dummy, "7",create=True) dum2 = self.s.get(Dummy, "7",create=True)
self.reopen() self.reopen()
dum = self.s.get(Dummy, "5") dum = self.s.get(Dummy, "5")
dum.data = "A" * 100 dum.data = "A" * 1000
dum.count = 1337 dum.count = 1337
self.reopen() self.reopen()
dum = self.s.get(Dummy, "5") dum = self.s.get(Dummy, "5")
@ -73,7 +73,7 @@ class TestStore(unittest.TestCase):
dum2 = self.s.get(Dummy, "7",create=True) dum2 = self.s.get(Dummy, "7",create=True)
self.reopen() self.reopen()
dum2 = self.s.get(Dummy, "7") dum2 = self.s.get(Dummy, "7")
dum2.data = "A" * 100 dum2.data = "A" * 1000
dum2.count = 1337 dum2.count = 1337
dum3 = self.s.get(Dummy, "9",create=True) dum3 = self.s.get(Dummy, "9",create=True)
dum3.count = 1338 dum3.count = 1338