47 Commits

Author SHA1 Message Date
Richard Bronkhorst
9d124179bf Update base.py 2023-06-26 20:50:29 +02:00
Richard Bronkhorst
9b9a149e3f Update base.py 2023-06-26 20:19:13 +02:00
Richard Bronkhorst
9e6583ac24 Update haul.py 2023-06-26 14:55:25 +02:00
Richard Bronkhorst
6c98eec738 Update commander.py 2023-06-26 13:40:11 +02:00
Richard Bronkhorst
11031599cf Update base.py 2023-06-26 05:48:19 +02:00
Richard Bronkhorst
7eea63ac82 Update mine.py 2023-06-25 22:39:33 +02:00
Richard Bronkhorst
dc862088cd Update mine.py 2023-06-25 22:37:33 +02:00
Richard Bronkhorst
35bc586b72 Update api.py 2023-06-25 20:21:49 +02:00
Richard Bronkhorst
2a5680c16d Update api.py 2023-06-25 19:32:35 +02:00
Richard Bronkhorst
4d51ad53c0 Update analyzer.py, central_command.py and five other files 2023-06-25 17:35:06 +02:00
Richard Bronkhorst
5fbce54285 Update analyzer.py and commander.py 2023-06-23 13:49:09 +02:00
Richard Bronkhorst
27bd054e8b Update analyzer.py and commander.py 2023-06-23 13:32:08 +02:00
Richard Bronkhorst
38a2ee7870 Update central_command.py, command_line.py and seven other files 2023-06-22 19:57:07 +02:00
Richard Bronkhorst
7c3eaa825f Update commander.py and mission.py 2023-06-22 14:56:51 +02:00
Richard Bronkhorst
ddd693a66e Update commander.py 2023-06-22 09:22:34 +02:00
Richard Bronkhorst
b43568f476 Update commander.py 2023-06-22 08:49:43 +02:00
Richard Bronkhorst
ff4643d7ac Update commander.py 2023-06-22 08:46:47 +02:00
Richard Bronkhorst
0e3f939b9a Update commander.py 2023-06-22 07:34:45 +02:00
Richard Bronkhorst
2d792dffae Update mission.py 2023-06-21 09:54:07 +02:00
Richard Bronkhorst
4043c5585e Update commander.py 2023-06-21 09:37:14 +02:00
Richard Bronkhorst
b19e3ed2b2 Update analyzer.py and commander.py 2023-06-21 09:32:31 +02:00
Richard Bronkhorst
b7d3347fac Update commander.py 2023-06-20 23:14:17 +02:00
Richard Bronkhorst
42e370fde5 Update commander.py and mission.py 2023-06-20 23:12:57 +02:00
Richard Bronkhorst
b202b80541 Update mission.py 2023-06-20 22:38:29 +02:00
Richard Bronkhorst
b023718450 Update mission.py 2023-06-20 22:30:21 +02:00
Richard Bronkhorst
fbda97df61 Update analyzer.py 2023-06-20 22:18:20 +02:00
Richard Bronkhorst
707f142e7a Update analyzer.py, api.py and four other files 2023-06-20 21:46:05 +02:00
Richard Bronkhorst
35ea9e2e04 Update commander.py 2023-06-19 10:36:30 +02:00
Richard Bronkhorst
3a85c6c367 Update commander.py 2023-06-18 21:56:49 +02:00
Richard Bronkhorst
21f93f078d Update api.py 2023-06-18 20:47:34 +02:00
Richard Bronkhorst
3a3e3b9da2 Update base.py and marketplace.py 2023-06-18 20:42:30 +02:00
Richard Bronkhorst
f849f871d2 Update mission.py 2023-06-18 20:21:08 +02:00
Richard Bronkhorst
9369c6982f Update central_command.py and mission.py 2023-06-18 20:02:33 +02:00
Richard Bronkhorst
8b29ca8f58 Update central_command.py, commander.py and eleven other files 2023-06-18 19:15:51 +02:00
Richard Bronkhorst
a229b9e300 Update commander.py 2023-06-18 07:23:01 +02:00
Richard Bronkhorst
46f9597e2e Cleanup 2023-06-18 07:06:32 +02:00
Richard Bronkhorst
c2a1f787a2 Update api.py, commander.py and one other file 2023-06-17 23:07:53 +02:00
Richard Bronkhorst
9987481848 Update base.py 2023-06-17 21:23:18 +02:00
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
23 changed files with 1454 additions and 71 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

@@ -1,6 +1,7 @@
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.system import System from nullptr.models.system import System
from nullptr.models.waypoint import Waypoint
from dataclasses import dataclass from dataclasses import dataclass
@dataclass @dataclass
@@ -29,15 +30,43 @@ class Analyzer:
def find_markets(self, resource, sellbuy): def find_markets(self, resource, sellbuy):
for m in self.store.all(Marketplace): for m in self.store.all(Marketplace):
resources = m.imports if sellbuy == 'sell' else m.exports if 'sell' in sellbuy and resource in m.imports:
if resource in resources: yield ('sell', m)
yield m
elif 'buy' in sellbuy and resource in m.exports:
yield ('buy', m)
elif 'exchange' in sellbuy and resource in m.exchange:
yield ('exchange', m)
def find_closest_markets(self, resource, sellbuy, location):
if type(location) == str:
location = self.store.get(Waypoint, location)
mkts = self.find_markets(resource, sellbuy)
candidates = []
origin = self.store.get(System, location.system())
for typ, m in mkts:
system = self.store.get(System, m.system())
d = origin.distance(system)
candidates.append((typ, m, d))
possibles = sorted(candidates, key=lambda m: m[2])
possibles = possibles[:10]
results = []
for typ,m,d in possibles:
system = self.store.get(System, m.system())
p = self.find_path(origin, system)
if p is None: continue
results.append((typ,m,d,len(p)))
return results
def solve_tsp(self, waypoints):
# todo actually try to solve it
return waypoints
def get_jumpgate(self, system): def get_jumpgate(self, system):
gates = self.store.all_members(system, Jumpgate) gates = self.store.all_members(system, Jumpgate)
return next(gates, None) return next(gates, None)
def find_path(self, orig, to, depth=100, seen=None): def find_path(self, orig, to, depth=100, seen=None):
if depth < 1: return None if depth < 1: return None
if seen is None: if seen is None:

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):
@@ -28,7 +31,7 @@ class Api:
def request(self, method, path, data=None, need_token=True, params={}): def request(self, method, path, data=None, need_token=True, params={}):
try: try:
return self.request_once(method, path, data, need_token, params) return self.request_once(method, path, data, need_token, params)
except ApiLimitError: except (ApiLimitError, requests.exceptions.Timeout):
print('oops, hit the limit. take a break') print('oops, hit the limit. take a break')
sleep(10) sleep(10)
return self.request_once(method, path, data, need_token, params) return self.request_once(method, path, data, need_token, params)
@@ -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,167 @@ 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.symbol.lower()}/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.symbol.lower()}/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):
if type(system) == System:
system = system.symbol
data = {
"systemSymbol": system
}
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 data
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

115
nullptr/central_command.py Normal file
View File

@@ -0,0 +1,115 @@
from nullptr.store import Store
from nullptr.models.ship import Ship
from nullptr.missions import create_mission, get_mission_class
from random import choice
from time import sleep
from threading import Thread
class CentralCommandError(Exception):
pass
class CentralCommand:
def __init__(self, store, api):
self.missions = {}
self.stopping = False
self.store = store
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 False
ship = choice(missions)
mission = self.missions[ship]
mission.step()
return True
def wait_for_stop(self):
try:
input()
except EOFError:
pass
self.stopping = True
print('stopping...')
def run_interactive(self):
print('auto mode. hit enter to stop')
t = Thread(target=self.wait_for_stop)
t.daemon = True
t.start()
self.run()
print('manual mode')
def run(self):
self.update_missions()
while not self.stopping:
did_step = True
request_counter = self.api.requests_sent
while request_counter == self.api.requests_sent and did_step:
did_step = self.tick()
self.store.flush()
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, self.store)
except ValueError as e:
raise MissionError(e)
return
ship.set_mission_state(nm, parsed_val)
def update_missions(self):
for s in self.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)
if s in self.missions:
m = self.missions[s]
m.next_step = max(s.cooldown, s.arrival)
def init_mission(self, s, mtyp):
if mtyp == 'none':
s.mission_state = {}
s.mission_status = None
s.mission = None
return
try:
mclass = get_mission_class(mtyp)
except ValueError:
raise CentralCommandError('no such mission')
s.mission = mtyp
s.mission_status = 'init'
s.mission_state = {k: v.default for k,v in mclass.params().items()}
self.start_mission(s)
def start_mission(self, s):
mtype = s.mission
m = create_mission(mtype, s, self.store, 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', 'CentralCommandError'])
def handle_empty(self): def handle_empty(self):
pass pass
@@ -90,5 +90,8 @@ class CommandLine:
except EOFError: except EOFError:
self.handle_eof() self.handle_eof()
break break
try:
self.handle_cmd(c) self.handle_cmd(c)
except Exception as e:
logging.error(e, exc_info=True)

View File

@@ -12,48 +12,166 @@ from .util import *
from time import sleep, time 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
from nullptr.central_command import CentralCommand
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.centcom = CentralCommand(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:
symbol = input(prompt) symbol = input(prompt).strip()
obj = self.store.get(typ, symbol.upper()) obj = self.store.get(typ, symbol.upper())
if obj is None: if obj is None:
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:
return self.store.get(Agent, agent_str)
else:
agents = self.store.all(Agent) agents = self.store.all(Agent)
agent = next(agents, None) agent = next(agents, None)
if agent is None: if agent is None:
symbol = input('agent name: ') symbol = input('agent name: ')
agent = self.store.get(Agent, symbol) agent = self.store.get(Agent, symbol, create=True)
return agent 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:
raise CommandError('not found')
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_auto(self):
self.centcom.run_interactive()
def print_mission(self):
print(f'mission: {self.ship.mission} ({self.ship.mission_status})')
pprint(self.ship.mission_state)
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_mreset(self):
if not self.has_ship(): return
self.ship.mission_state = {}
def do_mset(self, nm, val):
if not self.has_ship(): return
self.centcom.set_mission_param(self.ship, nm, val)
def active_contract(self):
for c in self.store.all('Contract'):
if c.accepted and not c.fulfilled: return c
raise CommandError('no active contract')
def do_cmine(self):
if not self.has_ship(): return
site = self.ship.location_str
contract = self.active_contract()
delivery = contract.unfinished_delivery()
if delivery is None:
raise CommandError('no delivery')
resource = delivery['trade_symbol']
destination = delivery['destination']
self.centcom.init_mission(self.ship, 'mine')
self.centcom.set_mission_param(self.ship, 'site', site)
self.centcom.set_mission_param(self.ship, 'resource', resource)
self.centcom.set_mission_param(self.ship, 'dest', destination)
self.centcom.set_mission_param(self.ship, 'contract', contract.symbol)
self.print_mission()
def do_chaul(self):
if not self.has_ship(): return
contract = self.active_contract()
delivery = contract.unfinished_delivery()
if delivery is None:
raise CommandError('no delivery')
resource = delivery['trade_symbol']
destination = delivery['destination']
m = self.analyzer.find_closest_markets(resource, 'buy', destination)
if len(m) == 0:
m = self.analyzer.find_closest_markets(resource, 'exchange', destination)
if len(m) == 0:
print('no market found')
return
_, m, _, _ = m[0]
site = self.store.get(Waypoint, m.symbol)
self.centcom.init_mission(self.ship, 'haul')
self.centcom.set_mission_param(self.ship, 'site', site.symbol)
self.centcom.set_mission_param(self.ship, 'resource', resource)
self.centcom.set_mission_param(self.ship, 'dest', destination)
self.centcom.set_mission_param(self.ship, 'contract', contract.symbol)
self.print_mission()
def do_cprobe(self):
if not self.has_ship(): return
contract = self.active_contract()
delivery = contract.unfinished_delivery()
if delivery is None:
raise CommandError('no delivery')
resource = delivery['trade_symbol']
destination = delivery['destination']
m = self.analyzer.find_closest_markets(resource, 'buy,exchange', destination)
if len(m) is None:
print('no market found')
return
markets = [ mkt[1] for mkt in m]
markets = self.analyzer.solve_tsp(markets)
hops = ','.join([m.symbol for m in markets])
self.centcom.init_mission(self.ship, 'probe')
self.centcom.set_mission_param(self.ship, 'hops', hops)
self.print_mission()
def do_travel(self, dest):
dest = self.resolve('Waypoint', dest)
self.centcom.init_mission(self.ship, 'travel')
self.centcom.set_mission_param(self.ship, 'dest', dest.symbol)
self.print_mission()
def do_register(self, faction): def do_register(self, faction):
self.api.register(faction.upper()) self.api.register(faction.upper())
pprint(self.api.agent)
def do_universe(self, page=1): def do_universe(self, page=1):
self.atlas_builder.run(page) self.atlas_builder.run(page)
@@ -62,30 +180,60 @@ class Commander(CommandLine):
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): def do_stats(self):
system = self.store.get(System, system_str.upper()) total = 0
r = self.api.list_waypoints(system) for t in self.store.data:
pprint(r) num = len(self.store.data[t])
nam = t.__name__
total += num
print(f'{num:5d} {nam}')
print(f'{total:5d} total')
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_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):
if waypoint_str is None:
if not self.has_ship(): return
waypoint = self.ship.location()
else:
waypoint = self.store.get(Waypoint, waypoint_str.upper()) waypoint = self.store.get(Waypoint, waypoint_str.upper())
r = self.api.jumps(waypoint) r = self.api.jumps(waypoint)
pprint(r) pprint(r)
def do_query(self): def do_query(self, resource):
location = self.ask_obj(System, 'Where are you? ') if not self.has_ship(): return
resource = input('what resource?').upper() location = self.ship.location()
sellbuy = self.ask_multichoice(['sell','buy'], 'do you want to sell or buy?') resource = resource.upper()
print('Found markets:') print('Found markets:')
for m in self.analyzer.find_markets(resource, sellbuy): for typ, m, d, plen in self.analyzer.find_closest_markets(resource, 'buy,exchange',location):
system = self.store.get(System, m.system()) price = '?'
p = self.analyzer.find_path(location, system) if resource in m.prices:
if p is None: continue price = m.prices[resource]['buy']
print(m, f'{len(p)-1} hops') print(m, typ[0], f'{plen-1:3} hops {price}')
def do_path(self): def do_path(self):
orig = self.ask_obj(System, 'from: ') orig = self.ask_obj(System, 'from: ')
@@ -94,3 +242,154 @@ 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_deliver(self):
if not self.has_ship(): return
site = self.ship.location_str
contract = self.active_contract()
delivery = contract.unfinished_delivery()
if delivery is None:
raise CommandError('no delivery')
resource = delivery['trade_symbol']
self.api.deliver(self.ship, resource, contract)
pprint(contract)
def do_fulfill(self):
contract = self.active_contract()
self.api.fulfill(contract)
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()
data = self.api.shipyard(location)
for s in must_get(data, 'ships'):
print(s['type'], s['purchasePrice'])
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)
def do_surveys(self):
pprint(list(self.store.all('Survey')))
def do_extract(self, survey_str=''):
if not self.has_ship(): return
survey = None
if survey_str != '':
survey = self.resolve('Survey', survey_str)
result = self.api.extract(self.ship, survey)
symbol = mg(result,'extraction.yield.symbol')
units = mg(result,'extraction.yield.units')
print(units, symbol)

View File

@@ -0,0 +1,23 @@
from nullptr.missions.survey import SurveyMission
from nullptr.missions.mine import MiningMission
from nullptr.missions.haul import HaulMission
from nullptr.missions.travel import TravelMission
from nullptr.missions.probe import ProbeMission
def get_mission_class( mtype):
types = {
'survey': SurveyMission,
'mine': MiningMission,
'haul': HaulMission,
'travel': TravelMission,
'probe': ProbeMission
}
if mtype not in types:
raise ValueError(f'invalid mission type {mtype}')
return types[mtype]
def create_mission(mtype, ship, store, api):
typ = get_mission_class(mtype)
m = typ(ship, store, api)
return m

250
nullptr/missions/base.py Normal file
View File

@@ -0,0 +1,250 @@
from nullptr.store import Store
from nullptr.models.base import Base
from nullptr.models.waypoint import Waypoint
from nullptr.models.contract import Contract
from nullptr.models.system import System
from nullptr.models.survey import Survey
from nullptr.models.ship import Ship
from nullptr.analyzer import Analyzer
from time import time
from functools import partial
import logging
from nullptr.util import *
class MissionError(Exception):
pass
class MissionParam:
def __init__(self, cls, required=True, default=None):
self.cls = cls
self.required = required
self.default = default
def parse(self, val, store):
if self.cls == str:
return str(val)
elif self.cls == int:
return int(val)
elif self.cls == list:
return [i.strip() for i in val.split(',')]
elif issubclass(self.cls, Base):
data = store.get(self.cls, val)
if data is None:
raise ValueError('object not found')
return data.symbol
else:
raise ValueError('unknown param typr')
class Mission:
@classmethod
def params(cls):
return {
}
def __init__(self, ship, store, api):
self.ship = ship
self.store = store
self.api = api
self.next_step = 0
self.analyzer = Analyzer(self.store)
def sts(self, nm, v):
if issubclass(type(v), Base):
v = v.symbol
self.ship.set_mission_state(nm, v)
def rst(self, typ, nm):
symbol = self.st(nm)
return self.store.get(typ, symbol)
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, exc_info=True)
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 BaseMission(Mission):
def step_go_dest(self):
destination = self.rst(Waypoint, 'destination')
if self.ship.location() == destination:
return
self.api.navigate(self.ship, destination)
self.next_step = self.ship.arrival
def step_go_site(self):
site = self.rst(Waypoint,'site')
if self.ship.location() == site:
return
self.api.navigate(self.ship, site)
self.next_step = self.ship.arrival
def step_unload(self):
contract = self.rst(Contract, '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_sell(self, except_resource=True):
target = self.st('resource')
market = self.store.get('Marketplace', self.ship.location_str)
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_load(self):
cargo_space = self.ship.cargo_capacity - self.ship.cargo_units
resource = self.st('resource')
self.api.buy(self.ship, resource, cargo_space)
def step_travel(self):
traject = self.st('traject')
if traject is None or traject == []:
return 'done'
dest = self.store.get(Waypoint, traject[-1])
loc = self.ship.location()
print(dest, loc)
if dest == loc:
self.sts('traject', None)
return 'done'
hop = traject.pop(0)
if len(hop.split('-')) == 3:
self.api.navigate(self.ship, hop)
self.next_step = self.ship.arrival
else:
self.api.jump(self.ship, hop)
self.next_step = self.ship.cooldown
if traject == []:
traject= None
self.sts('traject', traject)
return 'more'
def step_calculate_traject(self, dest):
if type(dest) == str:
dest = self.store.get(Waypoint, dest)
loc = self.ship.location()
loc_sys = self.store.get(System, loc.system())
loc_jg = self.analyzer.get_jumpgate(loc_sys)
dest_sys = self.store.get(System, dest.system())
dest_jg = self.analyzer.get_jumpgate(dest_sys)
if dest_sys == loc_sys:
result = [dest.symbol]
self.sts('traject', result)
return
path = self.analyzer.find_path(loc_sys, dest_sys)
result = []
if loc.symbol != loc_jg.symbol:
result.append(loc_jg.symbol)
result += [s.symbol for s in path[1:]]
if dest_jg.symbol != dest.symbol:
result.append(dest.symbol)
self.sts('traject', result)
print(result)
return result
def step_dock(self):
self.api.dock(self.ship)
def step_refuel(self):
if self.ship.fuel_capacity == 0:
return
if self.ship.fuel_current / self.ship.fuel_capacity < 0.5:
try:
self.api.refuel(self.ship)
except Exception as e:
pass
def step_orbit(self):
self.api.orbit(self.ship)
def travel_steps(self, nm, destination, next_step):
destination = self.st(destination)
calc = partial(self.step_calculate_traject, destination)
return {
f'travel-{nm}': (self.step_orbit, f'calc-trav-{nm}'),
f'calc-trav-{nm}': (calc, f'go-{nm}'),
f'go-{nm}': (self.step_travel, {
'done': f'dock-{nm}',
'more': f'go-{nm}'
}),
f'dock-{nm}': (self.step_dock, f'refuel-{nm}'),
f'refuel-{nm}': (self.step_refuel, next_step)
}

25
nullptr/missions/haul.py Normal file
View File

@@ -0,0 +1,25 @@
from nullptr.missions.base import BaseMission, MissionParam
from nullptr.models.waypoint import Waypoint
from nullptr.models.survey import Survey
from nullptr.models.contract import Contract
class HaulMission(BaseMission):
def start_state(self):
return 'travel-to'
@classmethod
def params(cls):
return {
'site': MissionParam(Waypoint, True),
'resource': MissionParam(str, True),
'dest': MissionParam(Waypoint, True),
'delivery': MissionParam(str, True, 'deliver'),
'contract': MissionParam(Contract, False)
}
def steps(self):
return {
**self.travel_steps('to', 'site', 'load'),
'load': (self.step_load, 'travel-back'),
**self.travel_steps('back', 'dest', 'unload'),
'unload': (self.step_unload, 'travel-to'),
}

80
nullptr/missions/mine.py Normal file
View File

@@ -0,0 +1,80 @@
from nullptr.missions.base import BaseMission, MissionParam
from nullptr.models.waypoint import Waypoint
from nullptr.models.survey import Survey
from nullptr.models.contract import Contract
from nullptr.util import *
class MiningMission(BaseMission):
@classmethod
def params(cls):
return {
'site': MissionParam(Waypoint, True),
'resource': MissionParam(str, True),
'dest': MissionParam(Waypoint, True),
'delivery': MissionParam(str, True, 'deliver'),
'contract': MissionParam(Contract, False)
}
def start_state(self):
return 'travel-to'
def steps(self):
return {
**self.travel_steps('to', 'site', 'orbit1'),
'orbit1': (self.step_orbit, 'extract'),
'extract': (self.step_extract, {
'done': 'dock',
'more': 'extract'
}),
'dock': (self.step_dock, 'sell'),
'sell': (self.step_sell, {
'more': 'sell',
'done': 'orbit2',
}),
'orbit2': (self.step_orbit, 'jettison'),
'jettison': (self.step_dispose, {
'more': 'jettison',
'done': 'extract',
'full': 'travel-back'
}),
**self.travel_steps('back', 'dest', 'unload'),
'unload': (self.step_unload, {
'done': 'travel-to',
'more': 'unload'
}),
}
def get_survey(self):
resource = self.st('resource')
site = self.rst(Waypoint,'site')
# todo optimize
for s in self.store.all(Survey):
if resource in s.deposits and site.symbol == 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_dispose(self):
contract = self.rst(Contract, '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'

33
nullptr/missions/probe.py Normal file
View File

@@ -0,0 +1,33 @@
from nullptr.missions.base import BaseMission, MissionParam
from nullptr.models.waypoint import Waypoint
class ProbeMission(BaseMission):
def start_state(self):
return 'next-hop'
@classmethod
def params(cls):
return {
'hops': MissionParam(list, True),
'next-hop': MissionParam(int, True, 0)
}
def steps(self):
return {
'next-hop': (self.step_next_hop, 'travel-to'),
**self.travel_steps('to', 'site', 'market'),
'market': (self.step_market, 'next-hop'),
}
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')
hop = hops[next_hop]
self.sts('site', hop)
self.sts('next-hop', (next_hop+1) % len(hops))

View File

@@ -0,0 +1,15 @@
from nullptr.missions.base import BaseMission, MissionParam
class SurveyMission(BaseMission):
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

View File

@@ -0,0 +1,16 @@
from nullptr.missions.base import BaseMission, MissionParam
from nullptr.models.waypoint import Waypoint
class TravelMission(BaseMission):
def start_state(self):
return 'travel-to'
@classmethod
def params(cls):
return {
'dest': MissionParam(Waypoint, True)
}
def steps(self):
return self.travel_steps('to', 'dest', 'done')

View File

@@ -1,8 +1,9 @@
from .base import Base from .base import Base
class Agent(Base): class Agent(Base):
token: str = None def define(self):
credits: int = 0 self.token: str = None
self.credits: int = 0
def update(self, d): def update(self, d):
self.seta('credits', d) self.seta('credits', d)

View File

@@ -1,15 +1,20 @@
from copy import deepcopy from copy import deepcopy
from dataclasses import dataclass
from nullptr.util import sg from nullptr.util import sg
@dataclass
class Base: class Base:
identifier = 'symbol'
symbol: str symbol: str
store: object store: object
def __init__(self, symbol, store): def __init__(self, symbol, store):
self.disable_dirty = True
self.store = store self.store = store
self.symbol = symbol self.symbol = symbol
self.define()
self.disable_dirty = False
def define(self):
pass
def __hash__(self): def __hash__(self):
return hash((str(type(self)), self.symbol)) return hash((str(type(self)), self.symbol))
@@ -17,11 +22,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):
@@ -31,13 +38,21 @@ class Base:
setattr(self, attr, lst) setattr(self, attr, lst)
def __setattr__(self, name, value): def __setattr__(self, name, value):
if name not in ['symbol','store','__dict__']: if name not in ['symbol','store','disable_dirty'] and not self.disable_dirty:
self.store.dirty(self) self.store.dirty(self)
super().__setattr__(name, value) super().__setattr__(name, value)
def update(self, d): def update(self, d):
pass pass
def is_expired(self):
return False
def load(self, d):
self.disable_dirty = True
self.__dict__.update(d)
self.disable_dirty = False
def dict(self): def dict(self):
r = {} r = {}
for k,v in self.__dict__.items(): for k,v in self.__dict__.items():

View File

@@ -0,0 +1,72 @@
from time import time
from nullptr.util import *
from .base import Base
class Contract(Base):
identifier = 'id'
def define(self):
self.type: str = ''
self.deliveries: list = []
self.accepted: bool = False
self.fulfilled: bool = False
self.expires: int = 0
self.expires_str: str = ''
self.pay: int = 0
@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 is_done(self):
for d in self.deliveries:
if d['units_fulfilled'] > d['units_requires']:
return False
return False
def unfinished_delivery(self):
for d in self.deliveries:
if d['units_required'] > d['units_fulfilled']:
return d
return None
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,11 @@
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 def define(self):
faction: str self.range: int = 0
systems:List[str] = [] self.faction: str = ''
self.systems: list = []
def update(self, d): def update(self, d):
self.setlst('systems', d, 'connectedSystems', 'symbol') self.setlst('systems', d, 'connectedSystems', 'symbol')
@@ -18,3 +19,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,58 @@
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] = [] def define(self):
exports:List[str] = [] self.imports:list = []
exchange:List[str] = [] self.exports:list = []
self.exchange:list = []
self.prices:dict = {}
self.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
def sellable_items(self, resources):
return [r for r in resources if r in self.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

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

@@ -0,0 +1,104 @@
from .base import Base
from time import time
from nullptr.util import *
from dataclasses import dataclass, field
class Ship(Base):
def define(self):
self.cargo:dict = {}
self.mission_state:dict = {}
self.status:str = ''
self.cargo_capacity:int = 0
self.cargo_units:int = 0
self.location_str = ''
self.cooldown:int = 0
self.arrival:int = 0
self.fuel_current:int = 0
self.fuel_capacity:int = 0
self.mission:str = None
self.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 set_mission_state(self, nm, val):
self.mission_state[nm] = val
self.store.dirty(self)
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

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

@@ -0,0 +1,55 @@
from time import time
from nullptr.util import *
from .system_member import SystemMember
size_names = ['SMALL','MODERATE','LARGE']
class Survey(SystemMember):
identifier = 'signature'
def define(self):
self.type: str = ''
self.deposits: list[str] = []
self.size: int = 0
self.expires: int = 0
self.expires_str: str = ''
self.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}/{self.symbol}.{self.ext()}'
def is_expired(self):
return time() > self.expires or self.exhausted
def waypoint(self):
p = self.symbol.split('-')
return '-'.join(p[:3])
def api_dict(self):
return {
'signature': self.symbol,
'symbol': 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

@@ -3,9 +3,10 @@ from .base import Base
from math import sqrt from math import sqrt
class System(Base): class System(Base):
x:int = 0 def define(self):
y:int = 0 self.x:int = 0
type:str = 'unknown' self.y:int = 0
self.type:str = 'unknown'
def update(self, d): def update(self, d):
self.seta('x', d) self.seta('x', d)
@@ -22,3 +23,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,13 +1,14 @@
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 def define(self):
y:int = 0 self.x:int = 0
type:str = 'unknown' self.y:int = 0
traits:List[str]=[] self.type:str = 'unknown'
faction:str = '' self.traits:list = []
self.faction:str = ''
def update(self, d): def update(self, d):
self.seta('x', d) self.seta('x', 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
@@ -20,10 +23,13 @@ class Store:
self.data = {m: {} for m in self.models} self.data = {m: {} for m in self.models}
self.system_members = {} self.system_members = {}
self.dirty_objects = set() self.dirty_objects = set()
self.cleanup_interval = 600
self.last_cleanup = 0
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 +50,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 +84,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,28 +96,58 @@ 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
print('typ', typ)
for m in self.system_members[system]: for m in self.system_members[system]:
if typ is None or type(m) == typ: if typ is None or type(m) == typ:
yield m yield m
def cleanup(self):
if time() < self.last_cleanup + self.cleanup_interval:
return
start_time = time()
expired = list()
for t in self.data:
for o in self.all(t):
if o.is_expired():
expired.append(o)
for o in expired:
path = o.path()
if isfile(path):
os.remove(path)
del self.data[type(o)][o.symbol]
dur = time() - start_time
# print(f'cleaned {len(expired)} in {dur:.03f} seconds')
def flush(self): def flush(self):
self.cleanup()
it = 0 it = 0
start_time = time() start_time = time()
for obj in self.dirty_objects: for obj in self.dirty_objects: