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
#ENTRYPOINT bash
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
import argparse
from nullptr.commander import Commander
import os
from nullptr.models.base import Base
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()
# X1-AG74-41076A
@ -11,7 +14,6 @@ def main(args):
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-s', '--store-file', default='data/store.npt')
parser.add_argument('-x', '--history-file', default='data/cmd.hst')
parser.add_argument('-d', '--data-dir', default='data')
args = parser.parse_args()
main(args)

View File

@ -4,6 +4,15 @@ from nullptr.models.system import System
from nullptr.models.waypoint import Waypoint
from dataclasses import dataclass
@dataclass
class TradeOption:
resource: str
source: Waypoint
dest: Waypoint
margin: int
dist: int
score: float
@dataclass
class SearchNode:
system: System
@ -88,3 +97,34 @@ class Analyzer:
if len(dest) == 0:
return None
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 threading import Thread
from nullptr.atlas_builder import AtlasBuilder
from nullptr.analyzer import Analyzer
class CentralCommandError(Exception):
pass
@ -14,9 +15,9 @@ class CentralCommand:
self.missions = {}
self.stopping = False
self.store = store
self.analyzer = Analyzer(store)
self.api = api
self.atlas_builder = AtlasBuilder(store, api)
self.update_missions()
def get_ready_missions(self):
result = []
@ -26,6 +27,7 @@ class CentralCommand:
return result
def tick(self):
self.update_missions()
missions = self.get_ready_missions()
if len(missions) == 0: return False
ship = choice(missions)
@ -41,7 +43,6 @@ class CentralCommand:
self.run()
print('manual mode')
def wait_for_stop(self):
try:
input()
@ -52,7 +53,6 @@ class CentralCommand:
def run(self):
self.update_missions()
while not self.stopping:
did_step = True
request_counter = self.api.requests_sent
@ -87,16 +87,41 @@ class CentralCommand:
return
ship.set_mission_state(nm, parsed_val)
def smipa(self,s,n,v):
self.set_mission_param(s,n,v)
def update_missions(self):
for s in self.store.all(Ship):
if s.mission_status == 'done':
s.mission = None
if s.mission is None:
if s in self.missions:
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)
if s in self.missions:
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):
if mtyp == 'none':
@ -113,6 +138,11 @@ class CentralCommand:
s.mission_state = {k: v.default for k,v in mclass.params().items()}
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):
mtype = s.mission
m = create_mission(mtype, s, self.store, self.api)

View File

@ -17,11 +17,13 @@ class CommandError(Exception):
pass
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
if os.path.isfile(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.api = Api(self.store, self.agent)
self.centcom = CentralCommand(self.store, self.api)
@ -124,12 +126,25 @@ class Commander(CommandLine):
print(f'mission: {self.ship.mission} ({self.ship.mission_status})')
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=''):
if not self.has_ship(): return
if arg:
self.centcom.init_mission(self.ship, arg)
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):
if not self.has_ship(): return
self.ship.mission_state = {}
@ -254,9 +269,11 @@ class Commander(CommandLine):
pprint(r)
def do_waypoints(self, system_str=''):
loc = None
if system_str == '':
if not self.has_ship(): return
system = self.ship.location.system
loc = self.ship.location
system = loc.system
else:
system = self.store.get(System, system_str)
print(f'=== waypoints in {system}')
@ -276,7 +293,7 @@ class Commander(CommandLine):
traits.append('METAL')
if 'PRECIOUS_METAL_DEPOSITS' in w.traits:
traits.append('GOLD')
if 'EXPLOSIVE_GASSES' in w.traits:
if 'EXPLOSIVE_GASES' in w.traits:
traits.append('GAS')
if 'MINERAL_DEPOSITS' in w.traits:
traits.append('MINS')
@ -288,6 +305,11 @@ class Commander(CommandLine):
typ = w.type[0]
if typ not in ['F','J'] and len(traits) == 0:
continue
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):
@ -441,17 +463,7 @@ class Commander(CommandLine):
def do_prices(self, resource=None):
if not self.has_ship(): return
system = self.ship.location.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.symbol,
'buy': p['buy'],
'sell': p['sell']
})
prices = self.analyzer.prices(system)
if resource is not None:
pprint(prices[resource.upper()])
else:

View File

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

View File

@ -97,7 +97,7 @@ class Mission:
logging.info(f'mission finished for {self.ship}')
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):
return self.status() in ['done','error']
@ -147,6 +147,10 @@ class BaseMission(Mission):
self.api.navigate(self.ship, site)
self.next_step = self.ship.arrival
def step_market(self):
loc = self.ship.location
self.api.marketplace(loc)
def step_unload(self):
delivery = self.st('delivery')
if delivery == 'sell':
@ -176,9 +180,15 @@ class BaseMission(Mission):
return 'more'
def step_load(self):
credits = self.api.agent.credits
cargo_space = self.ship.cargo_capacity - self.ship.cargo_units
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):
traject = self.st('traject')

View File

@ -18,8 +18,10 @@ class HaulMission(BaseMission):
def steps(self):
return {
**self.travel_steps('to', 'site', 'load'),
**self.travel_steps('to', 'site', 'market'),
'market': (self.step_market, 'load'),
'load': (self.step_load, 'travel-back'),
**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):
hops = self.st('hops')
next_hop = self.st('next-hop')

View File

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

View File

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

View File

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

View File

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