9 Commits

Author SHA1 Message Date
Richard Bronkhorst
293b6d1c3e Merge remote-tracking branch 'refs/remotes/origin/master' 2023-06-17 20:23:27 +02:00
Richard Bronkhorst
e31dce9861 Update commander.py, jumpgate.py and two other files 2023-06-17 20:23:24 +02:00
richmans
2e45acc513 racing 2023-06-17 20:18:14 +02:00
Richard Bronkhorst
b5850bcb5f Update api.py, commander.py and six other files 2023-06-17 14:59:52 +02:00
Richard Bronkhorst
bb93950fa3 Update api.py, commander.py and one other file 2023-06-16 23:05:47 +02:00
Richard Bronkhorst
3f93d863a0 Update api.py, command_line.py and six other files 2023-06-16 14:41:11 +02:00
Richard Bronkhorst
2c96cbb533 Update api.py, command_line.py and five other files 2023-06-16 13:03:19 +02:00
Richard Bronkhorst
92a5a02180 Update api.py, commander.py and one other file 2023-06-15 22:37:33 +02:00
Richard Bronkhorst
ffd094df87 Update main.py, api.py and three other files 2023-06-15 21:10:33 +02:00
15 changed files with 979 additions and 39 deletions

View File

@@ -2,7 +2,7 @@ import argparse
from nullptr.commander import Commander from nullptr.commander import Commander
def main(args): def main(args):
c = Commander(args.store_dir, args.agent) c = Commander(args.store_dir)
c.run() c.run()
# X1-AG74-41076A # X1-AG74-41076A
@@ -11,6 +11,5 @@ def main(args):
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('-s', '--store-dir', default='data') parser.add_argument('-s', '--store-dir', default='data')
parser.add_argument('-a', '--agent', default=None)
args = parser.parse_args() args = parser.parse_args()
main(args) main(args)

View File

@@ -3,6 +3,7 @@ from nullptr.models.system import System
from nullptr.models.waypoint import Waypoint from nullptr.models.waypoint import Waypoint
from nullptr.models.marketplace import Marketplace from nullptr.models.marketplace import Marketplace
from nullptr.models.jumpgate import Jumpgate from nullptr.models.jumpgate import Jumpgate
from nullptr.models.ship import Ship
from .util import * from .util import *
from time import sleep from time import sleep
class ApiError(Exception): class ApiError(Exception):
@@ -17,7 +18,9 @@ class Api:
def __init__(self, store, agent): def __init__(self, store, agent):
self.agent = agent self.agent = agent
self.store = store self.store = store
self.requests_sent = 0
self.meta = None self.meta = None
self.last_result = None
self.root = 'https://api.spacetraders.io/v2/' self.root = 'https://api.spacetraders.io/v2/'
def token(self): def token(self):
@@ -37,6 +40,7 @@ class Api:
headers = {} headers = {}
if need_token: if need_token:
headers['Authorization'] = 'Bearer ' + self.token() headers['Authorization'] = 'Bearer ' + self.token()
self.requests_sent += 1
if method == 'get': if method == 'get':
params['limit'] = 20 params['limit'] = 20
r = requests.request(method, self.root+path, json=data, headers=headers, params=params) r = requests.request(method, self.root+path, json=data, headers=headers, params=params)
@@ -86,11 +90,165 @@ class Api:
system = waypoint.system() system = waypoint.system()
symbol = str(waypoint) symbol = str(waypoint)
data = self.request('get', f'systems/{system}/waypoints/{waypoint}/market') data = self.request('get', f'systems/{system}/waypoints/{waypoint}/market')
return self.store.update(Marketplace, symbol, data) return self.store.update(Marketplace, data)
def jumps(self, waypoint): def jumps(self, waypoint):
data = self.request('get', f'systems/{waypoint.system()}/waypoints/{waypoint}/jump-gate') data = self.request('get', f'systems/{waypoint.system()}/waypoints/{waypoint}/jump-gate')
symbol = str(waypoint) symbol = str(waypoint)
return self.store.update(Jumpgate, symbol, data) return self.store.update(Jumpgate, data, symbol)
def list_ships(self):
data = self.request('get', 'my/ships')
return self.store.update_list(Ship, data)
def list_contracts(self):
data = self.request('get', 'my/contracts')
return self.store.update_list('Contract', data)
def negotiate(self, ship):
data = self.request('post', f'my/ships/{ship}/negotiate/contract')
if data is not None and 'contract' in data:
contract = self.store.update('Contract', data['contract'])
return contract
def deliver(self, ship, typ, contract):
units = ship.get_cargo(typ)
if units == 0:
print("Resource not in cargo")
return {}
data = {
'shipSymbol': str(ship),
'tradeSymbol': typ.upper(),
'units': units
}
data = self.request('post', f'my/contracts/{contract}/deliver', data)
if 'cargo' in data:
ship.update(data)
if 'contract' in data:
contract.update(data['contract'])
return contract
def fulfill(self, contract):
data = self.request('post', f'my/contracts/{contract}/fulfill')
if 'contract' in data:
contract.update(data['contract'])
if 'agent' in data:
self.agent.update(data['agent'])
return contract
def navigate(self, ship, wp):
data = {'waypointSymbol': str(wp)}
response = self.request('post', f'my/ships/{ship}/navigate', data)
ship.update(response)
def dock(self, ship):
data = self.request('post', f'my/ships/{ship}/dock')
ship.update(data)
return data
def orbit(self, ship):
data = self.request('post', f'my/ships/{ship}/orbit')
ship.update(data)
return data
def refuel(self, ship):
data = self.request('post', f'my/ships/{ship}/refuel')
if 'fuel' in data:
ship.update(data)
if 'agent' in data:
self.agent.update(data['agent'])
return data
def accept_contract(self, contract):
data = self.request('post', f'my/contracts/{contract.symbol.lower()}/accept')
if 'contract' in data:
contract.update(data['contract'])
if 'agent' in data:
self.agent.update(data['agent'])
return contract
def sell(self, ship, typ):
units = ship.get_cargo(typ)
data = {
'symbol': typ,
'units': units
}
data = self.request('post', f'my/ships/{ship}/sell', data)
if 'cargo' in data:
ship.update(data)
if 'agent' in data:
self.agent.update(data['agent'])
return data
def buy(self, ship, typ, amt):
data = {
'symbol': typ,
'units': amt
}
data = self.request('post', f'my/ships/{ship}/purchase', data)
if 'cargo' in data:
ship.update(data)
if 'agent' in data:
self.agent.update(data['agent'])
return data
def jettison(self, ship, typ):
units = ship.get_cargo(typ)
if units == 0:
print('cargo not found')
return
data = {
'symbol': typ,
'units': units
}
data = self.request('post', f'my/ships/{ship.symbol}/jettison', data)
if 'cargo' in data:
ship.update(data)
if 'agent' in data:
self.agent.update(data['agent'])
return data
def purchase(self, typ, wp):
data = {
'shipType': typ,
'waypointSymbol': str(wp)
}
data = self.request('post', 'my/ships', data)
if 'agent' in data:
self.agent.update(data['agent'])
if 'ship' in data:
ship = self.store.update('Ship', data['ship'])
return ship
def jump(self, ship, system):
data = {
"systemSymbol": system.symbol
}
data = self.request('post', f'my/ships/{ship}/jump', data)
if 'nav' in data:
ship.update(data)
return ship
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

@@ -41,7 +41,7 @@ class CommandLine:
print(f'command not found; {c}') print(f'command not found; {c}')
def handle_error(self, cmd, args, e): def handle_error(self, cmd, args, e):
logging.error(e, exc_info=str(type(e))!='ApiErrorp') logging.error(e, exc_info=type(e).__name__ not in ['ApiError','CommandError'])
def handle_empty(self): def handle_empty(self):
pass pass

View File

@@ -13,19 +13,35 @@ from time import sleep, time
from threading import Thread from threading import Thread
from nullptr.atlas_builder import AtlasBuilder from nullptr.atlas_builder import AtlasBuilder
class CommandError(Exception):
pass
class Commander(CommandLine): class Commander(CommandLine):
def __init__(self, store_dir='data', agent=None): def __init__(self, store_dir='data'):
self.store_dir = store_dir self.store_dir = store_dir
self.store = Store(store_dir) self.store = Store(store_dir)
self.store.load() self.store.load()
self.agent = self.select_agent(agent) self.agent = self.select_agent()
self.api = Api(self.store, self.agent) self.api = Api(self.store, self.agent)
self.atlas_builder = AtlasBuilder(self.store, self.api) self.atlas_builder = AtlasBuilder(self.store, self.api)
self.analyzer = Analyzer(self.store) self.analyzer = Analyzer(self.store)
self.ship = None
self.stop_auto= False self.stop_auto= False
super().__init__() super().__init__()
def prompt(self):
if self.ship:
return f'{self.ship.symbol}> '
else:
return '> '
def has_ship(self):
if self.ship is not None:
return True
else:
print('set a ship')
def ask_obj(self, typ, prompt): def ask_obj(self, typ, prompt):
obj = None obj = None
while obj is None: while obj is None:
@@ -35,22 +51,32 @@ class Commander(CommandLine):
print('not found') print('not found')
return obj return obj
def select_agent(self, agent_str): def select_agent(self):
if agent_str is not None: agents = self.store.all(Agent)
return self.store.get(Agent, agent_str) agent = next(agents, None)
if agent is None:
symbol = input('agent name: ')
agent = self.store.get(Agent, symbol, create=True)
return agent
def resolve(self, typ, arg):
arg = arg.upper()
matches = [c for c in self.store.all(typ) if c.symbol.startswith(arg)]
if len(matches) == 1:
return matches[0]
elif len(matches) > 1:
raise CommandError('multiple matches')
else: else:
agents = self.store.all(Agent) raise CommandError('not found')
agent = next(agents, None)
if agent is None:
symbol = input('agent name: ')
agent = self.store.get(Agent, symbol)
return agent
def after_cmd(self): def after_cmd(self):
self.store.flush() self.store.flush()
def do_info(self): def do_info(self, arg=''):
pprint(self.api.info(), 100) if arg.startswith('r'):
self.api.info()
pprint(self.agent, 100)
def do_register(self, faction): def do_register(self, faction):
self.api.register(faction.upper()) self.api.register(faction.upper())
@@ -61,18 +87,40 @@ class Commander(CommandLine):
def do_systems(self, page=1): def do_systems(self, page=1):
r = self.api.list_systems(int(page)) r = self.api.list_systems(int(page))
pprint(self.api.last_meta) pprint(self.api.last_meta)
def do_waypoints(self, system_str=''):
if system_str == '':
if not self.has_ship(): return
system = self.ship.location().system()
else:
system = self.store.get(System, system_str)
r = self.store.all_members(system, 'Waypoint')
for w in r:
traits = []
if 'MARKETPLACE' in w.traits:
traits.append('MARKET')
if 'SHIPYARD' in w.traits:
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=''):
self.do_waypoints(s)
def do_waypoints(self, system_str):
system = self.store.get(System, system_str.upper())
r = self.api.list_waypoints(system)
pprint(r)
def do_marketplace(self, waypoint_str): def do_marketplace(self, waypoint_str):
waypoint = self.store.get(Waypoint, waypoint_str.upper()) waypoint = self.store.get(Waypoint, waypoint_str.upper())
r = self.api.marketplace(waypoint) r = self.api.marketplace(waypoint)
def do_jumps(self, waypoint_str): def do_jumps(self, waypoint_str=None):
waypoint = self.store.get(Waypoint, waypoint_str.upper()) if waypoint_str is None:
if not self.has_ship(): return
waypoint = self.ship.location()
else:
waypoint = self.store.get(Waypoint, waypoint_str.upper())
r = self.api.jumps(waypoint) r = self.api.jumps(waypoint)
pprint(r) pprint(r)
@@ -94,3 +142,123 @@ class Commander(CommandLine):
# dest = self.store.get(System, 'X1-DA90') # dest = self.store.get(System, 'X1-DA90')
path = self.analyzer.find_path(orig, dest) path = self.analyzer.find_path(orig, dest)
pprint(path) pprint(path)
def do_ships(self, arg=''):
if arg.startswith('r'):
r = self.api.list_ships()
else:
r = list(self.store.all('Ship'))
pprint(r)
def do_contracts(self, arg=''):
if arg.startswith('r'):
r = self.api.list_contracts()
else:
r = list(self.store.all('Contract'))
pprint(r)
def do_ship(self, arg=''):
if arg != '':
symbol = f'{self.agent.symbol}-{arg}'
ship = self.store.get('Ship', symbol)
if ship is None:
print('not found')
return
else:
self.ship = ship
pprint(self.ship)
def do_pp(self):
pprint(self.api.last_result)
def do_go(self, arg):
if not self.has_ship(): return
system = self.ship.location().system()
symbol = f'{system}-{arg}'
dest = self.resolve('Waypoint', symbol)
self.api.navigate(self.ship, dest)
pprint(self.ship)
def do_dock(self):
if not self.has_ship(): return
self.api.dock(self.ship)
pprint(self.ship)
def do_orbit(self):
if not self.has_ship(): return
self.api.orbit(self.ship)
pprint(self.ship)
def do_negotiate(self):
if not self.has_ship(): return
r = self.api.negotiate(self.ship)
pprint(r)
def do_refuel(self):
if not self.has_ship(): return
r = self.api.refuel(self.ship)
pprint(self.ship)
def do_accept(self, c):
contract = self.resolve('Contract', c)
r = self.api.accept_contract(contract)
pprint(r)
def do_market(self, arg=''):
if arg == '':
if not self.has_ship(): return
waypoint = self.ship.location()
else:
waypoint = self.resolve('Waypoint', arg)
r = self.api.marketplace(waypoint)
pprint(r)
def do_cargo(self):
if not self.has_ship(): return
for c, units in self.ship.cargo.items():
print(f'{units:4d} {c}')
def do_buy(self, resource, amt=None):
if not self.has_ship(): return
if amt is None:
amt = self.ship.cargo_capacity - self.ship.cargo_units
self.api.buy(self.ship, resource.upper(), amt)
self.do_cargo()
def do_sell(self, resource):
if not self.has_ship(): return
self.api.sell(self.ship, resource.upper())
self.do_cargo()
def do_dump(self, resource):
if not self.has_ship(): return
self.api.jettison(self.ship, resource.upper())
self.do_cargo()
def do_shipyard(self):
if not self.has_ship(): return
location = self.ship.location()
pprint(self.api.shipyard(location))
def do_jump(self, system_str):
if not self.has_ship(): return
if '-' not in system_str:
sector = self.ship.location_str.split('-')[0]
system_str = f'{sector}-{system_str}'
system = self.resolve('System', system_str)
self.api.jump(self.ship, system)
pprint(self.ship)
def do_purchase(self, ship_type):
if not self.has_ship(): return
location = self.ship.location()
ship_type = ship_type.upper()
if not ship_type.startswith('SHIP'):
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

View File

@@ -4,6 +4,7 @@ from nullptr.util import sg
@dataclass @dataclass
class Base: class Base:
identifier = 'symbol'
symbol: str symbol: str
store: object store: object
@@ -17,11 +18,13 @@ class Base:
def __eq__(self, other): def __eq__(self, other):
return self.symbol == other.symbol and type(self) == type(other) return self.symbol == other.symbol and type(self) == type(other)
def seta(self, attr, d, name=None): def seta(self, attr, d, name=None, interp=None):
if name is None: if name is None:
name = attr name = attr
val = sg(d, name) val = sg(d, name)
if val is not None: if val is not None:
if interp is not None:
val = interp(val)
setattr(self, attr, val) setattr(self, attr, val)
def setlst(self, attr, d, name, member): def setlst(self, attr, d, name, member):
@@ -37,6 +40,9 @@ class Base:
def update(self, d): def update(self, d):
pass pass
def load(self, d):
self.__dict__ = d
def dict(self): def dict(self):
r = {} r = {}

View File

@@ -0,0 +1,58 @@
from time import time
from nullptr.util import *
from .base import Base
class Contract(Base):
identifier = 'id'
type: str
deliveries: list
accepted: bool
fulfilled: bool
expires: int
expires_str: str
pay: int
@classmethod
def ext(cls):
return 'cnt'
def path(self):
return f'contracts/{self.symbol}.{self.ext()}'
def is_expired(self):
return time() > self.expires
def api_dict(self):
return {
'id': self.symbol.lower(),
'expiration': self.expires_str,
}
def update(self, d):
self.seta('expires',d, 'terms.deadline',parse_timestamp)
self.seta('expires_str', d,'terms.deadline')
self.seta('accepted', d, 'accepted')
self.seta('fulfilled', d, 'fulfilled')
self.seta('type', d, 'type')
self.pay = mg(d, 'terms.payment.onAccepted') + mg(d, 'terms.payment.onFulfilled')
deliveries = must_get(d, 'terms.deliver')
self.deliveries = []
for e in deliveries:
delivery = {}
delivery['trade_symbol'] = must_get(e, 'tradeSymbol')
delivery['units_fulfilled'] = must_get(e, 'unitsFulfilled')
delivery['units_required'] = must_get(e, 'unitsRequired')
delivery['destination'] = must_get(e, 'destinationSymbol')
self.deliveries.append(delivery)
def f(self, detail=1):
hours = int(max(0, self.expires - time()) / 3600)
accepted = 'A' if self.accepted else '-'
fulfilled = 'F' if self.fulfilled else '-'
result = f'{self.symbol} {hours}h {accepted}{fulfilled}'
if detail > 1:
result += '\n'
for d in self.deliveries:
result += f"({d['units_fulfilled']} / {d['units_required']}) {d['trade_symbol']} to {d['destination']}"
return result

View File

@@ -1,10 +1,10 @@
from .system_member import SystemMember from .system_member import SystemMember
from typing import List from dataclasses import field
class Jumpgate(SystemMember): class Jumpgate(SystemMember):
range: int range: int
faction: str faction: str
systems:List[str] = [] systems: list = []
def update(self, d): def update(self, d):
self.setlst('systems', d, 'connectedSystems', 'symbol') self.setlst('systems', d, 'connectedSystems', 'symbol')
@@ -18,3 +18,10 @@ class Jumpgate(SystemMember):
def path(self): def path(self):
sector, system, _ = self.symbol.split('-') sector, system, _ = self.symbol.split('-')
return f'atlas/{sector}/{system[0:1]}/{system}/{self.symbol}.{self.ext()}' return f'atlas/{sector}/{system[0:1]}/{system}/{self.symbol}.{self.ext()}'
def f(self, detail=1):
r = self.symbol
if detail > 1:
r += '\n'
r += '\n'.join(self.systems)
return r

View File

@@ -1,21 +1,54 @@
from .system_member import SystemMember from .system_member import SystemMember
from typing import List from time import time
from nullptr.util import *
from dataclasses import field
class Marketplace(SystemMember): class Marketplace(SystemMember):
imports:List[str] = [] imports:list = []
exports:List[str] = [] exports:list = []
exchange:List[str] = [] exchange:list = []
prices:dict = {}
last_prices:int = 0
def update(self, d): def update(self, d):
self.setlst('imports', d, 'imports', 'symbol') self.setlst('imports', d, 'imports', 'symbol')
self.setlst('exports', d, 'exports', 'symbol') self.setlst('exports', d, 'exports', 'symbol')
self.setlst('exchange', d, 'exchange', 'symbol') self.setlst('exchange', d, 'exchange', 'symbol')
if 'tradeGoods' in d:
self.last_prices = time()
prices = {}
for g in mg(d, 'tradeGoods'):
price = {}
symbol= mg(g, 'symbol')
price['symbol'] = symbol
price['buy'] = mg(g, 'purchasePrice')
price['sell'] = mg(g, 'sellPrice')
prices[symbol] = price
self.prices = prices
@classmethod @classmethod
def ext(self): def ext(self):
return 'mkt' return 'mkt'
def rtype(self, r):
if r in self.imports:
return 'I'
if r in self.exports:
return 'E'
if r in self.exchange:
return 'X'
return '?'
def path(self): def path(self):
sector, system, _ = self.symbol.split('-') sector, system, _ = self.symbol.split('-')
return f'atlas/{sector}/{system[0:1]}/{system}/{self.symbol}.{self.ext()}' return f'atlas/{sector}/{system[0:1]}/{system}/{self.symbol}.{self.ext()}'
def f(self, detail=1):
r = self.symbol
if detail > 1:
r += '\n'
for p in self.prices.values():
t = self.rtype(p['symbol'])
r += f'{t} {p["symbol"]:25s} {p["sell"]:5d} {p["buy"]:5d}\n'
return r

99
nullptr/models/ship.py Normal file
View File

@@ -0,0 +1,99 @@
from .base import Base
from time import time
from nullptr.util import *
from dataclasses import dataclass, field
class Ship(Base):
cargo:dict = {}
mission_state:dict = {}
status:str = ''
cargo_capacity:int = 0
cargo_units:int = 0
location_str = ''
cooldown:int = 0
arrival:int = 0
fuel_current:int = 0
fuel_capacity:int = 0
mission:str = None
mission_status:str = 'init'
@classmethod
def ext(self):
return 'shp'
def location(self):
return self.store.get('Waypoint', self.location_str)
def path(self):
agent = self.symbol.split('-')[0]
return f'{agent}/{self.symbol}.{self.ext()}'
def update(self, d):
self.seta('status', d, 'nav.status')
self.seta('location_str', d, 'nav.waypointSymbol')
self.seta('cargo_capacity', d, 'cargo.capacity')
self.seta('cargo_units', d, 'cargo.units')
self.seta('fuel_capacity', d, 'fuel.capacity')
self.seta('fuel_current', d,'fuel.current')
cargo = sg(d, 'cargo.inventory')
if cargo is not None:
self.load_cargo(cargo)
self.seta('cooldown', d, 'cooldown.expiration', parse_timestamp)
self.seta('arrival', d, 'nav.route.arrival', parse_timestamp)
def tick(self):
if self.status == 'IN_TRANSIT' and self.arrival < time():
self.status = 'IN_ORBIT'
def is_cooldown(self):
return self.cooldown > time()
def is_travelling(self):
return self.status == 'IN_TRANSIT'
def get_cargo(self, typ):
if typ not in self.cargo:
return 0
return self.cargo[typ]
def load_cargo(self, cargo):
result = {}
for i in cargo:
symbol = must_get(i, 'symbol')
units = must_get(i, 'units')
result[symbol] = units
self.cargo = result
def deliverable_cargo(self, contract):
result = []
for d in contract.deliveries:
if self.get_cargo(d['trade_symbol']) > 0:
result.append(d['trade_symbol'])
return result
def nondeliverable_cargo(self, contract):
cargo = self.cargo.keys()
deliveries = [d['trade_symbol'] for d in contract.deliveries]
garbage = [c for c in cargo if c not in deliveries]
return garbage
def update_timers(self):
if self.status == 'IN_TRANSIT' and self.arrival < time():
self.status = 'IN_ORBIT'
def f(self, detail=1):
self.update_timers()
arrival = int(self.arrival - time())
cooldown = int(self.cooldown - time())
r = self.symbol
if detail > 1:
r += ' ' + self.status
r += f' [{self.fuel_current}/{self.fuel_capacity}]'
r += ' ' + str(self.location())
if self.is_travelling():
r += f' [A: {arrival}]'
if self.is_cooldown():
r += f' [C: {cooldown}]'
return r

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

@@ -22,3 +22,6 @@ class System(Base):
def distance(self, other): def distance(self, other):
return int(sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2)) return int(sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2))
def sector(self):
return self.symbol.split('-')[0]

View File

@@ -1,12 +1,12 @@
from .system_member import SystemMember from .system_member import SystemMember
from nullptr.util import * from nullptr.util import *
from typing import List from dataclasses import field
class Waypoint(SystemMember): class Waypoint(SystemMember):
x:int = 0 x:int = 0
y:int = 0 y:int = 0
type:str = 'unknown' type:str = 'unknown'
traits:List[str]=[] traits:list = []
faction:str = '' faction:str = ''
def update(self, d): def update(self, d):

View File

@@ -6,6 +6,9 @@ from nullptr.models.agent import Agent
from nullptr.models.marketplace import Marketplace from nullptr.models.marketplace import Marketplace
from nullptr.models.system_member import SystemMember from nullptr.models.system_member import SystemMember
from nullptr.models.jumpgate import Jumpgate 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 from os.path import isfile, dirname, isdir
import os import os
from os.path import basename from os.path import basename
@@ -24,6 +27,7 @@ class Store:
def init_models(self): def init_models(self):
self.models = all_subclasses(Base) self.models = all_subclasses(Base)
self.extensions = {c.ext(): c for c in self.models} self.extensions = {c.ext(): c for c in self.models}
self.model_names = {c.__name__: c for c in self.models}
def dirty(self, obj): def dirty(self, obj):
self.dirty_objects.add(obj) self.dirty_objects.add(obj)
@@ -44,8 +48,8 @@ class Store:
typ = self.extensions[ext] typ = self.extensions[ext]
obj = self.create(typ, symbol) obj = self.create(typ, symbol)
data['store'] = self obj.load(data)
obj.__dict__ = data obj.store = self
return obj return obj
def load(self): def load(self):
@@ -78,6 +82,9 @@ class Store:
return obj return obj
def get(self, typ, symbol, create=False): def get(self, typ, symbol, create=False):
if type(typ) == str and typ in self.model_names:
typ = self.model_names[typ]
symbol = symbol.upper()
if typ not in self.data: if typ not in self.data:
return None return None
if symbol not in self.data[typ]: if symbol not in self.data[typ]:
@@ -87,21 +94,32 @@ class Store:
return None return None
return self.data[typ][symbol] return self.data[typ][symbol]
def update(self, typ, symbol, data): def update(self, typ, data, symbol=None):
if type(typ) == str and typ in self.model_names:
typ = self.model_names[typ]
if symbol is None:
symbol = mg(data, typ.identifier)
obj = self.get(typ, symbol, True) obj = self.get(typ, symbol, True)
obj.update(data) obj.update(data)
return obj return obj
def update_list(self, typ, lst): def update_list(self, typ, lst):
return [self.update(typ, mg(d, 'symbol'), d) for d in lst] return [self.update(typ, d) for d in lst]
def all(self, typ): def all(self, typ):
if type(typ) == str and typ in self.model_names:
typ = self.model_names[typ]
for m in self.data[typ].values(): for m in self.data[typ].values():
yield m yield m
def all_members(self, system, typ=None): def all_members(self, system, typ=None):
if type(typ) == str and typ in self.model_names:
typ = self.model_names[typ]
if type(system) == System: if type(system) == System:
system = system.symbol system = system.symbol
if system not in self.system_members: if system not in self.system_members:
return return
for m in self.system_members[system]: for m in self.system_members[system]: