diff --git a/nullptr/api.py b/nullptr/api.py index 16fa3fa..3f12edb 100644 --- a/nullptr/api.py +++ b/nullptr/api.py @@ -231,3 +231,24 @@ class Api: def shipyard(self, wp): return self.request('get', f'systems/{wp.system()}/waypoints/{wp}/shipyard') + + def extract(self, ship, survey=None): + data = {} + if survey is not None: + data['survey'] = survey.api_dict() + try: + data = self.request('post', f'my/ships/{ship}/extract', data=data) + except ApiError as e: + if e.code in [ 4221, 4224]: + survey.exhausted = True + else: + raise e + ship.update(data) + return ship + + def survey(self, ship): + data = self.request('post', f'my/ships/{ship}/survey') + ship.update(data) + result = self.store.update_list('Survey', mg(data, 'surveys')) + return result + \ No newline at end of file diff --git a/nullptr/central_command.py b/nullptr/central_command.py new file mode 100644 index 0000000..9e7ae99 --- /dev/null +++ b/nullptr/central_command.py @@ -0,0 +1,73 @@ +from nullptr.store import store +from nullptr.models.ship import Ship +from nullptr.mission import * +from random import choice +from time import sleep + +class CentralCommand: + def __init__(self, api): + self.missions = {} + self.stopping = False + self.api = api + self.update_missions() + + def get_ready_missions(self): + result = [] + for ship, mission in self.missions.items(): + if mission.is_ready(): + result.append(ship) + return result + + def tick(self): + missions = self.get_ready_missions() + if len(missions) == 0: return + ship = choice(missions) + mission = self.missions[ship] + mission.step() + + def run(self): + self.update_missions() + while not self.stopping: + self.tick() + self.api.save() + sleep(0.5) + self.stopping = False + + def stop(self): + self.stopping = True + + def set_mission_param(self, ship, nm, val): + if ship not in self.missions: + print('set a mission for this ship first') + return + mission = self.missions[ship] + params = mission.params() + if not nm in params: + print(f'{nm} is not a valid param') + return + param = params[nm] + try: + parsed_val = param.parse(val) + except ValueError as e: + print(e) + return + print('ok') + ship.mission_state[nm] = parsed_val + + def update_missions(self): + for s in store.all(Ship): + if s.mission is None: + if s in self.missions: + self.stop_mission(s) + elif s not in self.missions: + self.start_mission(s) + + def start_mission(self, s): + mtype = s.mission + m = create_mission(mtype, s, self.api) + self.missions[s] = m + return m + + def stop_mission(self, s): + if s in self.missions: + del self.missions[s] diff --git a/nullptr/commander.py b/nullptr/commander.py index f28ad94..503b289 100644 --- a/nullptr/commander.py +++ b/nullptr/commander.py @@ -104,6 +104,8 @@ class Commander(CommandLine): traits.append('SHIPYARD') if w.type == 'JUMP_GATE': traits.append('JUMP') + if w.type == 'ASTEROID_FIELD': + traits.append('ASTROIDS') print(w.symbol.split('-')[2], ', '.join(traits)) def do_wp(self, s=''): @@ -255,3 +257,8 @@ class Commander(CommandLine): ship_type = 'SHIP_' + ship_type s = self.api.purchase(ship_type, location) pprint(s) + + def do_survey(self): + if not self.has_ship(): return + r = self.api.survey(self.ship) + pprint(r) \ No newline at end of file diff --git a/nullptr/mission.py b/nullptr/mission.py new file mode 100644 index 0000000..493cfff --- /dev/null +++ b/nullptr/mission.py @@ -0,0 +1,268 @@ +from nullptr.store import store +from nullptr.models.waypoint import Waypoint +from nullptr.models.contract import Contract +from nullptr.models.survey import Survey +from nullptr.models.ship import Ship +from time import time +import logging +from util import * + +class MissionParam: + def __init__(self, cls, required=True, default=None): + self.cls = cls + self.required = required + self.default = default + + def parse(self, val): + if self.cls == str: + return str(val) + elif self.cls == int: + return int(val) + elif issubclass(self.cls, StoreObject): + data = store.get(self.cls, val) + if data is None: + raise ValueError('object not found') + return data + else: + raise ValueError('unknown param typr') + +class Mission: + ship: Ship + next_step: int = 0 + + @classmethod + def params(cls): + return { + + } + + def __init__(self, ship, api): + self.ship = ship + self.api = api + + def sts(self, nm, v): + self.ship.mission_state[nm] = v + + def st(self, nm): + if not nm in self.ship.mission_state: + return None + return self.ship.mission_state[nm] + + def status(self, nw=None): + if nw is None: + return self.ship.mission_status + else: + self.ship.mission_status = nw + + def start_state(self): + return 'done' + + def error(self, msg): + self.status('error') + print(msg) + + def init_state(self): + for name, param in self.params().items(): + if param.required and param.default is None: + if not name in self.ship.mission_state: + return self.error(f'Param {name} not set') + self.status(self.start_state()) + + def steps(self): + return { + + } + + def step_done(self): + logging.info(f'mission finished for {self.ship}') + + def is_waiting(self): + return self.next_step > time() + + def is_finished(self): + return self.status() in ['done','error'] + + def is_ready(self): + return not self.is_waiting() and not self.is_finished() + + def step(self): + steps = self.steps() + if self.status() == 'init': + self.init_state() + status = self.status() + if not status in steps: + logging.warning(f"Invalid mission status {status}") + self.status('error') + return + handler, next_step = steps[status] + try: + result = handler() + except Exception as e: + logging.error(e) + self.status('error') + return + if type(next_step) == str: + self.status(next_step) + elif type(next_step) == dict: + if result not in next_step: + logging.warning(f'Invalid step result {result}') + self.status('error') + return + else: + self.status(next_step[result]) + print(f'{self.ship} {status} -> {self.status()}') + + +class MiningMission(Mission): + @classmethod + def params(cls): + return { + 'site': MissionParam(Waypoint, True), + 'resource': MissionParam(str, True), + 'destination': MissionParam(Waypoint, True), + 'delivery': MissionParam(str, True, 'deliver'), + 'contract': MissionParam(Contract, False) + } + + def start_state(self): + return 'go_site' + + def steps(self): + return { + 'extract': (self.step_extract, { + 'done': 'dock', + 'more': 'extract' + }), + 'dock': (self.step_dock, 'sell'), + 'sell': (self.step_sell, { + 'more': 'sell', + 'done': 'orbit', + }), + 'orbit': (self.step_orbit, 'jettison'), + 'jettison': (self.step_dispose, { + 'more': 'jettison', + 'done': 'extract', + 'full': 'go_dest' + }), + 'go_dest': (self.step_go_dest, 'dock_dest'), + 'dock_dest': (self.step_dock, 'unload'), + 'unload': (self.step_unload, { + 'done': 'refuel', + 'more': 'unload' + }), + 'refuel': (self.step_refuel, 'orbit_dest'), + 'orbit_dest': (self.step_orbit, 'go_site'), + 'go_site': (self.step_go_site, 'extract') + } + + def get_survey(self): + resource = self.st('resource') + site = self.st('site') + # todo optimize + for s in store.all(Survey): + if resource in s.deposits and site == s.waypoint: + return s + return None + + def step_extract(self): + survey = self.get_survey() + print('using survey:', str(survey)) + result = self.api.extract(self.ship, survey) + symbol = sg(result,'extraction.yield.symbol') + units = sg(result,'extraction.yield.units') + print('extracted:', units, symbol) + self.next_step = self.ship.cooldown + if self.ship.cargo_units < self.ship.cargo_capacity: + return 'more' + else: + return 'done' + + def step_sell(self, except_resource=True): + target = self.st('resource') + market = self.api.market(self.ship.location) + sellables = market.sellable_items(self.ship.cargo.keys()) + if target in sellables and except_resource: + sellables.remove(target) + if len(sellables) == 0: + return 'done' + self.api.sell(self.ship, sellables[0]) + if len(sellables) == 1: + return 'done' + else: + return 'more' + + def step_go_dest(self): + destination = self.st('destination') + if self.ship.location == destination: + return + self.api.navigate(self.ship, destination) + self.next_step = self.ship.arrival + + def step_dock(self): + self.api.dock(self.ship) + + def step_unload(self): + contract = self.st('contract') + delivery = self.st('delivery') + if delivery == 'sell': + return self.step_sell(False) + typs = self.ship.deliverable_cargo(contract) + if len(typs) == 0: + return 'done' + self.api.deliver(self.ship, typs[0], contract) + if len(typs) == 1: + return 'done' + else: + return 'more' + + def step_refuel(self): + self.api.refuel(self.ship) + + def step_dispose(self): + contract = self.st('contract') + typs = self.ship.nondeliverable_cargo(contract) + if len(typs) > 0: + self.api.jettison(self.ship, typs[0]) + if len(typs) > 1: + return 'more' + elif self.ship.cargo_units > self.ship.cargo_capacity - 3: + return 'full' + else: + return 'done' + + + def step_orbit(self): + self.api.orbit(self.ship) + + def step_go_site(self): + site = self.st('site') + if self.ship.location == site: + return + self.api.navigate(self.ship, site) + self.next_step = self.ship.arrival + +class SurveyMission(Mission): + def start_state(self): + return 'survey' + + + def steps(self): + return { + 'survey': (self.step_survey, 'survey') + } + + def step_survey(self): + result = self.api.survey(self.ship) + #pprint(result, 2) + self.next_step = self.ship.cooldown + +def create_mission(mtype, ship, api): + types = { + 'survey': SurveyMission, + 'mine': MiningMission + } + if mtype not in types: + logging.warning(f'invalid mission type {mtype}') + return + m = types[mtype](ship, api) + return m diff --git a/nullptr/models/survey.py b/nullptr/models/survey.py new file mode 100644 index 0000000..e776c1e --- /dev/null +++ b/nullptr/models/survey.py @@ -0,0 +1,50 @@ +from time import time +from nullptr.util import * +from .system_member import SystemMember + +size_names = ['SMALL','MODERATE','LARGE'] + +class Survey(SystemMember): + identifier = 'signature' + type: str = '' + deposits: list[str] = [] + size: int = 0 + expires: int = 0 + expires_str: str = '' + exhausted: bool = False + + @classmethod + def ext(cls): + return 'svy' + + def path(self): + sector, system, waypoint, signature = self.symbol.split('-') + return f'atlas/{sector}/{system[0:1]}/{system}/{waypoint}-{signature}.{self.ext()}' + + + def is_expired(self): + return time() > self.expires or self.exhausted + + def api_dict(self): + return { + 'signature': self.symbol, + 'symbol': str(self.waypoint), + 'deposits': [{'symbol': d} for d in self.deposits], + 'expiration': self.expires_str, + 'size': size_names[self.size] + } + + def update(self, d): + sz = must_get(d, 'size') + self.size = size_names.index(sz) + self.deposits = [d['symbol'] for d in must_get(d, 'deposits')] + self.seta('expires',d, 'expiration',parse_timestamp) + self.seta('expires_str',d, 'expiration') + + def f(self, detail=1): + result = self.symbol + if detail > 1: + result += ' ' + ','.join(self.deposits) + minutes = max(self.expires - time(), 0) //60 + result += ' ' + str(int(minutes)) + 'm' + return result diff --git a/nullptr/store.py b/nullptr/store.py index 818cc26..95641e7 100644 --- a/nullptr/store.py +++ b/nullptr/store.py @@ -8,6 +8,7 @@ from nullptr.models.system_member import SystemMember from nullptr.models.jumpgate import Jumpgate from nullptr.models.ship import Ship from nullptr.models.contract import Contract +from nullptr.models.survey import Survey from os.path import isfile, dirname, isdir import os from os.path import basename