From 1b7a5286550766d5482918c16948836c77d1ad53 Mon Sep 17 00:00:00 2001 From: Richard Date: Thu, 4 Jan 2024 21:34:31 +0100 Subject: [PATCH] haulin goods --- Dockerfile | 2 +- main.py | 8 ++++--- nullptr/analyzer.py | 40 +++++++++++++++++++++++++++++++ nullptr/central_command.py | 40 +++++++++++++++++++++++++++---- nullptr/commander.py | 44 ++++++++++++++++++++++------------- nullptr/missions/__init__.py | 5 +++- nullptr/missions/base.py | 14 +++++++++-- nullptr/missions/haul.py | 6 +++-- nullptr/missions/idle.py | 26 +++++++++++++++++++++ nullptr/missions/probe.py | 4 ---- nullptr/models/marketplace.py | 7 +++++- nullptr/models/ship.py | 3 +++ nullptr/models/waypoint.py | 4 ++++ nullptr/store.py | 16 +++++++++---- nullptr/test_store.py | 4 ++-- 15 files changed, 181 insertions(+), 42 deletions(-) create mode 100644 nullptr/missions/idle.py diff --git a/Dockerfile b/Dockerfile index 75c67d5..8c0ace9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/main.py b/main.py index 98416e4..e0b2cc1 100644 --- a/main.py +++ b/main.py @@ -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) diff --git a/nullptr/analyzer.py b/nullptr/analyzer.py index 184c64f..2bf0c77 100644 --- a/nullptr/analyzer.py +++ b/nullptr/analyzer.py @@ -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 diff --git a/nullptr/central_command.py b/nullptr/central_command.py index 4eab9ee..d931915 100644 --- a/nullptr/central_command.py +++ b/nullptr/central_command.py @@ -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 @@ -86,18 +86,43 @@ class CentralCommand: raise MissionError(e) 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': s.mission_state = {} @@ -112,6 +137,11 @@ class CentralCommand: s.mission_status = 'init' 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 diff --git a/nullptr/commander.py b/nullptr/commander.py index e622f3e..4720c19 100644 --- a/nullptr/commander.py +++ b/nullptr/commander.py @@ -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) @@ -123,6 +125,14 @@ class Commander(CommandLine): def print_mission(self): 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 @@ -130,6 +140,11 @@ class Commander(CommandLine): 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,7 +305,12 @@ class Commander(CommandLine): typ = w.type[0] if typ not in ['F','J'] and len(traits) == 0: 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): if not self.has_ship(): return @@ -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: diff --git a/nullptr/missions/__init__.py b/nullptr/missions/__init__.py index 3abf95a..adb5c87 100644 --- a/nullptr/missions/__init__.py +++ b/nullptr/missions/__init__.py @@ -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}') diff --git a/nullptr/missions/base.py b/nullptr/missions/base.py index 51478f6..09bf769 100644 --- a/nullptr/missions/base.py +++ b/nullptr/missions/base.py @@ -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') diff --git a/nullptr/missions/haul.py b/nullptr/missions/haul.py index c38c8f0..5de19cc 100644 --- a/nullptr/missions/haul.py +++ b/nullptr/missions/haul.py @@ -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'), } diff --git a/nullptr/missions/idle.py b/nullptr/missions/idle.py new file mode 100644 index 0000000..3260c81 --- /dev/null +++ b/nullptr/missions/idle.py @@ -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') + } + + diff --git a/nullptr/missions/probe.py b/nullptr/missions/probe.py index 1b1bb58..80dab1c 100644 --- a/nullptr/missions/probe.py +++ b/nullptr/missions/probe.py @@ -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') diff --git a/nullptr/models/marketplace.py b/nullptr/models/marketplace.py index 3b3281a..d511d61 100644 --- a/nullptr/models/marketplace.py +++ b/nullptr/models/marketplace.py @@ -35,7 +35,12 @@ class Marketplace(Base): price['volume'] = mg(g, 'tradeVolume') 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] diff --git a/nullptr/models/ship.py b/nullptr/models/ship.py index 6989eac..006c02d 100644 --- a/nullptr/models/ship.py +++ b/nullptr/models/ship.py @@ -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) diff --git a/nullptr/models/waypoint.py b/nullptr/models/waypoint.py index aa8b181..d4e3d9f 100644 --- a/nullptr/models/waypoint.py +++ b/nullptr/models/waypoint.py @@ -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): @@ -23,6 +24,9 @@ class Waypoint(Base): self.seta('is_under_construction', d, 'isUnderConstruction') 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): diff --git a/nullptr/store.py b/nullptr/store.py index 536e914..f8d219f 100644 --- a/nullptr/store.py +++ b/nullptr/store.py @@ -23,10 +23,12 @@ class StoreUnpickler(pickle.Unpickler): if pers_id == "STORE": 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('