Merge remote-tracking branch 'refs/remotes/origin/master'

This commit is contained in:
Richard Bronkhorst 2023-06-17 20:23:27 +02:00
commit 293b6d1c3e
6 changed files with 420 additions and 0 deletions

View File

@ -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

View File

@ -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]

View File

@ -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)

268
nullptr/mission.py Normal file
View File

@ -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

50
nullptr/models/survey.py Normal file
View File

@ -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

View File

@ -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