diff --git a/Dockerfile b/Dockerfile index bbda0d3..f1611d8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,4 +9,5 @@ ADD --chown=user . /app RUN chmod +x /app/main.py VOLUME /data #ENTRYPOINT bash +RUN echo "python3 /app/main.py -d /data" > ~/.bash_history CMD ["/bin/sh", "-c", "python3 /app/main.py -d /data ; bash -i"] \ No newline at end of file diff --git a/nullptr/analyzer.py b/nullptr/analyzer.py index f0df3ac..2b148cd 100644 --- a/nullptr/analyzer.py +++ b/nullptr/analyzer.py @@ -52,172 +52,184 @@ class SearchNode: def __repr__(self): return self.system.symbol -class Analyzer: - def __init__(self, store): - self.store = store + +def find_markets(c, resource, sellbuy): + for m in c.store.all(Marketplace): + if 'sell' in sellbuy and resource in m.imports: + yield ('sell', 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(c, resource, sellbuy, location): + if type(location) == str: + location = c.store.get(Waypoint, location) + mkts = find_markets(resource, sellbuy) + candidates = [] + origin = location.system + for typ, m in mkts: + system = m.waypoint.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 = m.waypoint.system + p = find_jump_path(origin, system) + if p is None: continue + results.append((typ,m,d,len(p))) + return results + +def solve_tsp(c, waypoints): + wps = copy(waypoints) + path = [] + cur = Point(0,0) + while len(wps) > 0: + closest = wps[0] + for w in wps: + if w.distance(cur) < closest.distance(cur): + closest = w + cur = closest + path.append(closest) + wps.remove(closest) + return path - def find_markets(self, resource, sellbuy): - for m in self.store.all(Marketplace): - if 'sell' in sellbuy and resource in m.imports: - yield ('sell', 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 = location.system - for typ, m in mkts: - system = m.waypoint.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 = m.waypoint.system - p = self.find_path(origin, system) - if p is None: continue - results.append((typ,m,d,len(p))) - return results +def get_jumpgate(c, system): + gates = c.store.all_members(system, Jumpgate) + return next(gates, None) + +# dijkstra shmijkstra +def find_nav_path(c, orig, to, ran): + path = [] + mkts = [m.waypoint for m in c.store.all_members(orig.system, Marketplace)] + cur = orig + if orig == to: - def solve_tsp(self, waypoints): - wps = copy(waypoints) - path = [] - cur = Point(0,0) - while len(wps) > 0: - closest = wps[0] - for w in wps: - if w.distance(cur) < closest.distance(cur): - closest = w - cur = closest - path.append(closest) - wps.remove(closest) - return path - - def get_jumpgate(self, system): - gates = self.store.all_members(system, Jumpgate) - return next(gates, None) + return [] + while cur != to: + best = cur + bestdist = cur.distance(to) + if bestdist < ran: + path.append(to) + break + for m in mkts: + dist = m.distance(to) + if dist < bestdist and cur.distance(m) < ran: + best = m + bestdist = dist + if best == cur: + raise AnalyzerException(f'no path to {to}') + cur = best + path.append(cur) + return path - # dijkstra shmijkstra - def find_nav_path(self, orig, to, ran): - path = [] - mkts = [m.waypoint for m in self.store.all_members(orig.system, Marketplace)] - cur = orig - if orig == to: - - return [] - while cur != to: - best = cur - bestdist = cur.distance(to) - if bestdist < ran: - path.append(to) - break - for m in mkts: - dist = m.distance(to) - if dist < bestdist and cur.distance(m) < ran: - best = m - bestdist = dist - if best == cur: - raise AnalyzerException(f'no path to {to}') - cur = best - path.append(cur) - return path - - def find_jump_path(self, orig, to, depth=100, seen=None): - if depth < 1: return None - if seen is None: - seen = set() - if type(orig) == System: - orig = set([SearchNode(orig,None)]) - result = [n for n in orig if n==to] - if len(result) > 0: - return result[0].path() - dest = set() - for o in orig: - jg = self.get_jumpgate(o) - if jg is None: continue - for s in jg.connections: - if s in seen: continue - seen.add(s) - dest.add(SearchNode(s, o)) - if len(dest) == 0: - return None - return self.find_path(dest, to, depth-1, seen) +def find_jump_path(c, orig, to, depth=100, seen=None): + if depth < 1: return None + if seen is None: + seen = set() + if type(orig) == System: + orig = set([SearchNode(orig,None)]) + result = [n for n in orig if n==to] + if len(result) > 0: + return result[0].path() + dest = set() + for o in orig: + jg = get_jumpgate(o) + if jg is None: continue + for s in jg.connections: + if s in seen: continue + seen.add(s) + dest.add(SearchNode(s, o)) + if len(dest) == 0: + return None + return find_jump_path(dest, to, depth-1, seen) + +def prices(c, system): + prices = {} + for m in c.store.all_members(system, Marketplace): + for r, p in m.prices.items(): + if not r in prices: + prices[r] = [] + prices[r].append({ + 'wp': m.waypoint, + 'buy': p.buy, + 'sell': p.sell, + 'volume': p.volume, + 'category': m.rtype(r) + }) + return prices + +def find_trade(c, system): + max_traders = 3 + prices = prices(system) + occupied_routes = dict() + for s in c.store.all('Ship'): + if s.mission != 'trade': + continue + k = (s.mission_state['site'], s.mission_state['dest']) + if k in occupied_routes: + occupied_routes[k] += 1 + else: + occupied_routes[k] = 1 + best = None + for resource, markets in prices.items(): + source = sorted(markets, key=lambda x: x['buy'])[0] + dest = sorted(markets, key=lambda x: x['sell'])[-1] + swp = source['wp'] + dwp = dest['wp'] + margin = dest['sell'] -source['buy'] + k = (swp.symbol,dwp.symbol) + if k in occupied_routes and occupied_routes[k] > max_traders: + continue + dist = swp.distance(dwp) + dist = max(dist, 0.0001) + score = margin / dist + if margin < 2: + continue + o = TradeOption(resource, swp, dwp, source['buy'], margin, dist, score) + if best is None or best.score < o.score: + best = o + return best + +def find_deal(c, smkt, dmkt): + best_margin = 0 + best_resource = None + for r, sp in smkt.prices.items(): + if not r in dmkt.prices: + continue + dp = dmkt.prices[r] + margin = dp.sell - sp.buy + if margin > best_margin: + best_margin = margin + best_resource = r + return best_resource + +def best_sell_market(c, system, r): + best_price = 0 + best_market = None + for m in c.store.all_members(system, Marketplace): + if r not in m.prices: continue + price = m.prices[r].sell + if price > best_price: + best_price = price + best_market = m + return best_market - def prices(self, system): - prices = {} - for m in self.store.all_members(system, Marketplace): - for r, p in m.prices.items(): - if not r in prices: - prices[r] = [] - prices[r].append({ - 'wp': m.waypoint, - 'buy': p.buy, - 'sell': p.sell, - 'volume': p.volume, - 'category': m.rtype(r) - }) - return prices +def find_gas(c, system): + m = [w for w in c.store.all_members(system, 'Waypoint') if w.type == 'GAS_GIANT'] + if len(m)==0: + raise AnalyzerException('no gas giant found') + return m[0] - def find_trade(self, system): - max_traders = 3 - prices = self.prices(system) - occupied_routes = dict() - for s in self.store.all('Ship'): - if s.mission != 'trade': - continue - k = (s.mission_state['site'], s.mission_state['dest']) - if k in occupied_routes: - occupied_routes[k] += 1 - else: - occupied_routes[k] = 1 - best = None - for resource, markets in prices.items(): - source = sorted(markets, key=lambda x: x['buy'])[0] - dest = sorted(markets, key=lambda x: x['sell'])[-1] - swp = source['wp'] - dwp = dest['wp'] - margin = dest['sell'] -source['buy'] - k = (swp.symbol,dwp.symbol) - if k in occupied_routes and occupied_routes[k] > max_traders: - continue - dist = swp.distance(dwp) - dist = max(dist, 0.0001) - score = margin / dist - if margin < 2: - continue - o = TradeOption(resource, swp, dwp, source['buy'], margin, dist, score) - if best is None or best.score < o.score: - best = o - return best - - def find_deal(self, smkt, dmkt): - best_margin = 0 - best_resource = None - for r, sp in smkt.prices.items(): - if not r in dmkt.prices: - continue - dp = dmkt.prices[r] - margin = dp.sell - sp.buy - if margin > best_margin: - best_margin = margin - best_resource = r - return best_resource - - def best_sell_market(self, system, r): - best_price = 0 - best_market = None - for m in self.store.all_members(system, Marketplace): - if r not in m.prices: continue - price = m.prices[r].sell - if price > best_price: - best_price = price - best_market = m - return best_market +def find_metal(c, system): + m = [w for w in c.store.all_members(system, Waypoint) if 'COMMON_METAL_DEPOSITS' in w.traits] + if len(m) == 0: + return None + origin = Point(0,0) + m = sorted(m, key=lambda w: w.distance(origin)) + return m[0] + diff --git a/nullptr/api.py b/nullptr/api.py index 2247ad0..17ca3f5 100644 --- a/nullptr/api.py +++ b/nullptr/api.py @@ -7,7 +7,8 @@ from nullptr.models.ship import Ship from nullptr.models.shipyard import Shipyard from .util import * from time import sleep, time -class ApiError(Exception): + +class ApiError(AppError): def __init__(self, msg, code): super().__init__(msg) self.code = code @@ -16,9 +17,9 @@ class ApiLimitError(Exception): pass class Api: - def __init__(self, store, agent): + def __init__(self, c, agent): self.agent = agent - self.store = store + self.store = c.store self.requests_sent = 0 self.last_meta = None self.last_result = None diff --git a/nullptr/central_command.py b/nullptr/captain.py similarity index 50% rename from nullptr/central_command.py rename to nullptr/captain.py index abd71b7..222ec30 100644 --- a/nullptr/central_command.py +++ b/nullptr/captain.py @@ -6,19 +6,25 @@ from random import choice, randrange from time import sleep, time from threading import Thread from nullptr.atlas_builder import AtlasBuilder -from nullptr.analyzer import Analyzer, Point +from nullptr.general import General +from nullptr.util import * +from nullptr.roles import assign_mission -class CentralCommandError(Exception): +class CentralCommandError(AppError): pass -class CentralCommand: - def __init__(self, store, api): +class Captain: + def __init__(self, context): self.missions = {} self.stopping = False - self.store = store - self.analyzer = Analyzer(store) - self.api = api - self.atlas_builder = AtlasBuilder(store, api) + self.store = context.store + self.c = context + self.general = context.general + self.api = context.api + self.general = context.general + self.atlas_builder = AtlasBuilder(self.store, self.api) + + def setup(self): self.update_missions() def get_ready_missions(self): @@ -40,6 +46,7 @@ class CentralCommand: mission.step() def tick(self): + self.general.tick() self.update_missions() missions = self.get_ready_missions() if len(missions) == 0: return False @@ -64,9 +71,10 @@ class CentralCommand: self.stopping = True print('stopping...') - def run(self): while not self.stopping: + # any new orders? + self.c.general.tick() did_step = True request_counter = self.api.requests_sent start = time() @@ -116,99 +124,11 @@ class CentralCommand: if s in self.missions: self.stop_mission(s) if s.mission is None: - self.assign_mission(s) + assign_mission(self.c, s) if s.mission is not None and s not in self.missions: self.start_mission(s) if s in self.missions: m = self.missions[s] - - def find_gas(self, system): - m = [w for w in self.store.all_members(system, 'Waypoint') if w.type == 'GAS_GIANT'] - if len(m)==0: - raise CentralCommandError('no gas giant found') - return m[0] - - def find_metal(self, system): - m = [w for w in self.store.all_members(system, Waypoint) if 'COMMON_METAL_DEPOSITS' in w.traits] - if len(m) == 0: - return None - origin = Point(0,0) - m = sorted(m, key=lambda w: w.distance(origin)) - return m[0] - - def assign_mission(self, s): - if s.role == 'trader': - self.assign_trade(s) - elif s.role == 'probe': - self.assign_probe(s) - elif s.role == 'siphon': - self.assign_siphon(s) - elif s.role == 'hauler': - self.assign_hauler(s) - elif s.role == 'surveyor': - self.assign_survey(s) - elif s.role == 'miner': - self.assign_mine(s) - - def assign_mine(self, s): - if s.crew is None: - raise CentralCommandError('ship has no crew') - w = s.crew.site - resources = s.crew.resources - self.init_mission(s, 'mine') - self.smipa(s, 'site', w) - self.smipa(s, 'resources', resources) - - def assign_survey(self, s): - if s.crew is None: - raise CentralCommandError('ship has no crew') - w = s.crew.site - self.init_mission(s, 'survey') - self.smipa(s, 'site', w) - - - def assign_hauler(self, s): - if s.crew is None: - raise CentralCommandError('ship has no crew') - w = s.crew.site - resources = s.crew.resources - resource = choice(resources) - m = self.analyzer.best_sell_market(s.location.system, resource) - s.log(f'assigning haul mission from {w} to {m}') - self.init_mission(s, 'haul') - self.smipa(s, 'site', w) - self.smipa(s, 'dest', m) - self.smipa(s, 'resources', resources) - - def assign_siphon(self, s): - if s.crew is None: - raise CentralCommandError('ship has no crew') - w = s.crew.site - self.init_mission(s, 'siphon') - self.smipa(s, 'site', w) - - def assign_probe(self, s): - system = s.location.system - m = [m.waypoint for m in self.store.all_members(system, 'Marketplace')] - m = self.analyzer.solve_tsp(m) - hops = [w.symbol for w in m] - start_hop = randrange(0, len(hops)) - s.log(f'Assigning {s} to probe {len(hops)} starting at {hops[start_hop]}') - self.init_mission(s, 'probe') - self.smipa(s, 'hops', hops) - self.smipa(s, 'next-hop', start_hop) - - def assign_trade(self, s): - t = self.analyzer.find_trade(s.location.system) - if t is None: - print(f"No trade for {s} found. Idling") - self.init_mission(s,'idle') - self.smipa(s, 'seconds', 600) - return - s.log(f'assigning {s} to deliver {t.resource} from {t.source} to {t.dest} at a margin of {t.margin}') - self.init_mission(s, 'trade') - self.smipa(s, 'site', t.source) - self.smipa(s, 'dest', t.dest) def init_mission(self, s, mtyp): if mtyp == 'none': @@ -232,7 +152,7 @@ class CentralCommand: def start_mission(self, s): mtype = s.mission - m = create_mission(mtype, s, self.store, self.api) + m = create_mission(mtype, s, self.c) self.missions[s] = m m.status(s.mission_status) return m @@ -241,17 +161,4 @@ class CentralCommand: if s in self.missions: del self.missions[s] - def create_default_crews(self): - system = self.api.agent.headquarters.system - gas_w = self.find_gas(system) - metal_w = self.find_metal(system) - metal = self.store.get('Crew', 'METAL', create=True) - metal.site = metal_w - metal.resources = ['COPPER_ORE','IRON_ORE','ALUMINUM_ORE'] - gas = self.store.get('Crew', 'GAS', create=True) - gas.site = gas_w - gas.resources = ['HYDROCARBON','LIQUID_HYDROGEN','LIQUID_NITROGEN'] - return [gas, metal] - - - \ No newline at end of file + \ No newline at end of file diff --git a/nullptr/command_line.py b/nullptr/command_line.py index e61e9b7..60dbf2d 100644 --- a/nullptr/command_line.py +++ b/nullptr/command_line.py @@ -3,6 +3,7 @@ import inspect import sys import importlib import logging +from nullptr.util import AppError def func_supports_argcount(f, cnt): argspec = inspect.getargspec(f) @@ -41,7 +42,7 @@ class CommandLine: print(f'command not found; {c}') def handle_error(self, cmd, args, e): - logging.error(e, exc_info=type(e).__name__ not in ['ApiError','CommandError', 'CentralCommandError']) + logging.error(e, exc_info=not issubclass(type(e), AppError)) def handle_empty(self): pass diff --git a/nullptr/commander.py b/nullptr/commander.py index 89b8d7c..de653a2 100644 --- a/nullptr/commander.py +++ b/nullptr/commander.py @@ -1,19 +1,21 @@ from nullptr.command_line import CommandLine from nullptr.store import Store -from nullptr.analyzer import Analyzer, Point, path_dist +from nullptr.analyzer import * +from nullptr.context import Context import argparse from nullptr.models import * from nullptr.api import Api from .util import * from time import sleep, time from threading import Thread -from nullptr.central_command import CentralCommand +from nullptr.captain import Captain +from nullptr.general import General import readline import os from copy import copy -class CommandError(Exception): +class CommandError(AppError): pass class Commander(CommandLine): @@ -25,15 +27,20 @@ class Commander(CommandLine): if os.path.isfile(hist_file): readline.read_history_file(hist_file) self.store = Store(store_file, True) + self.c = Context(self.store) self.agent = self.select_agent() - self.api = Api(self.store, self.agent) - self.centcom = CentralCommand(self.store, self.api) + self.c.api = self.api = Api(self.c, self.agent) + self.c.general = self.general = General(self.c) + self.c.captain = self.captain = Captain(self.c) + + self.general.setup() + self.captain.setup() + self.api.info() - self.do_create_crews() - self.analyzer = Analyzer(self.store) + self.ship = None - self.stop_auto= False + self.stop_auto = False super().__init__() ######## INFRA ######### @@ -55,7 +62,7 @@ class Commander(CommandLine): self.store.flush() def do_auto(self): - self.centcom.run_interactive() + self.captain.run_interactive() def do_log(self, level): ship = self.has_ship() @@ -170,7 +177,7 @@ class Commander(CommandLine): self.api.list_ships() for s in self.store.all('Ship'): self.dump(s, 'all') - self.centcom.init_mission(s, 'none') + self.captain.init_mission(s, 'none') ######## Fleet ######### def do_info(self, arg=''): @@ -373,17 +380,17 @@ class Commander(CommandLine): def do_mission(self, arg=''): ship = self.has_ship() if arg: - self.centcom.init_mission(ship, arg) + self.captain.init_mission(ship, arg) self.print_mission() def do_mrestart(self, status='init'): ship = self.has_ship() - self.centcom.restart_mission(ship, status) + self.captain.restart_mission(ship, status) self.print_mission() def do_mstep(self): ship = self.has_ship() - self.centcom.single_step(ship) + self.captain.single_step(ship) self.print_mission() def do_mreset(self): @@ -392,7 +399,7 @@ class Commander(CommandLine): def do_mset(self, nm, val): ship = self.has_ship() - self.centcom.set_mission_param(ship, nm, val) + self.captain.set_mission_param(ship, nm, val) def do_crew(self, arg): ship = self.has_ship() @@ -402,7 +409,7 @@ class Commander(CommandLine): ######## Crews ######### def do_create_crews(self): - crews = self.centcom.create_default_crews() + crews = self.captain.create_default_crews() for c in crews: print(f'{c.symbol:15s} {c.site}') @@ -448,8 +455,8 @@ class Commander(CommandLine): def do_travel(self, dest): ship = self.has_ship() dest = self.resolve('Waypoint', dest) - self.centcom.init_mission(ship, 'travel') - self.centcom.set_mission_param(ship, 'dest', dest) + self.captain.init_mission(ship, 'travel') + self.captain.set_mission_param(ship, 'dest', dest) self.print_mission() def do_go(self, arg): @@ -535,7 +542,7 @@ class Commander(CommandLine): location = ship.location resource = resource.upper() print('Found markets:') - for typ, m, d, plen in self.analyzer.find_closest_markets(resource, 'buy,exchange',location): + for typ, m, d, plen in find_closest_markets(self.c, resource, 'buy,exchange',location): price = '?' if resource in m.prices: price = m.prices[resource]['buy'] @@ -544,13 +551,13 @@ class Commander(CommandLine): def do_findtrade(self): ship = self.has_ship() system = ship.location.system - t = self.analyzer.find_trade(system) + t = find_trade(self.c, system) pprint(t) def do_prices(self, resource=None): ship = self.has_ship() system = ship.location.system - prices = self.analyzer.prices(system) + prices = prices(self.c, system) if resource is not None: prices = {resource: prices[resource.upper()]} @@ -562,5 +569,5 @@ class Commander(CommandLine): def do_path(self, waypoint_str): ship = self.has_ship() w = self.resolve('Waypoint', waypoint_str) - p = self.analyzer.find_nav_path(ship.location, w, ship.fuel_capacity) + p = find_nav_path(self.c, ship.location, w, ship.fuel_capacity) pprint(p) diff --git a/nullptr/context.py b/nullptr/context.py new file mode 100644 index 0000000..7610800 --- /dev/null +++ b/nullptr/context.py @@ -0,0 +1,6 @@ +class Context: + def __init__(self, store, api=None, captain=None, general=None): + self.store = store + self.api = api + self.captain = captain + self.general = general \ No newline at end of file diff --git a/nullptr/general.py b/nullptr/general.py new file mode 100644 index 0000000..e1b9709 --- /dev/null +++ b/nullptr/general.py @@ -0,0 +1,91 @@ +from nullptr.util import * +from nullptr.analyzer import find_gas, find_metal +class GeneralError(AppError): + pass + +class General: + def __init__(self, context): + self.store = context.store + self.api = context.api + self.c = context + agents = self.store.all('Agent') + self.agent = next(agents, None) + self.phases = { + 'init': self.phase_startup, + 'probes': self.phase_probes, + 'trade': self.phase_trade, + 'mine': self.phase_mine, + 'siphon': self.phase_siphon, + 'rampup': self.phase_rampup, + 'gate': self.phase_gate + } + + def setup(self): + self.create_default_crews() + + def find_shipyard(self, stype): + for shipyard in self.store.all('Shipyard'): + if stype in shipyard.types: + return stype + return None + + + def tick(self): + phase = self.agent.phase + if phase not in self.phases: + raise GeneralError('Invalid phase') + hdl = self.phases[phase] + new_phase = hdl() + if new_phase: + self.agent.phase = new_phase + + + def phase_startup(self): + # * first pricing info + # * probe at shipyard that sells probes + ag = self.agent.symbol + command = self.store.get('Ship', f'{ag}-1') + probe = self.store.get('Ship', f'{ag}-2') + command.role = 'probe' + probe.role = 'sitter' + + def phase_probes(self): + # * probes on all markets + pass + + def phase_trade(self): + # 20? traders + pass + + def phase_mine(self): + # metal mining crew + pass + + def phase_siphon(self): + # siphon crew + pass + + def phase_rampup(self): + # stimulate markets for gate building + pass + + def phase_gate(self): + # build the gate + pass + + def create_default_crews(self): + system = self.api.agent.headquarters.system + gas_w = find_gas(self.c, system) + metal_w = find_metal(self.c, system) + metal = self.store.get('Crew', 'METAL', create=True) + metal.site = metal_w + metal.resources = ['COPPER_ORE','IRON_ORE','ALUMINUM_ORE'] + gas = self.store.get('Crew', 'GAS', create=True) + gas.site = gas_w + gas.resources = ['HYDROCARBON','LIQUID_HYDROGEN','LIQUID_NITROGEN'] + return [gas, metal] + + + + + \ No newline at end of file diff --git a/nullptr/missions/__init__.py b/nullptr/missions/__init__.py index a80e811..87a9f5a 100644 --- a/nullptr/missions/__init__.py +++ b/nullptr/missions/__init__.py @@ -25,8 +25,8 @@ def get_mission_class( mtype): raise ValueError(f'invalid mission type {mtype}') return types[mtype] -def create_mission(mtype, ship, store, api): +def create_mission(mtype, ship, c): typ = get_mission_class(mtype) - m = typ(ship, store, api) + m = typ(ship, c) return m diff --git a/nullptr/missions/base.py b/nullptr/missions/base.py index 432e5b3..51f8aed 100644 --- a/nullptr/missions/base.py +++ b/nullptr/missions/base.py @@ -5,7 +5,7 @@ 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 nullptr.analyzer import * from time import time from functools import partial import logging @@ -48,13 +48,13 @@ class Mission: } - def __init__(self, ship, store, api): + def __init__(self, ship, context): self.ship = ship - self.store = store - self.api = api + self.c = context + self.store = context.store + self.api = context.api self.wait_for = None self.next_step = 0 - self.analyzer = Analyzer(self.store) self.setup() def setup(self): @@ -263,15 +263,15 @@ class BaseMission(Mission): loc = self.ship.location loc_sys = loc.system - loc_jg = self.analyzer.get_jumpgate(loc_sys) + loc_jg = get_jumpgate(self.c, loc_sys) loc_jg_wp = self.store.get(Waypoint, loc_jg.symbol) dest_sys = dest.system - dest_jg = self.analyzer.get_jumpgate(dest_sys) + dest_jg = get_jumpgate(self.c, dest_sys) if dest_sys == loc_sys: - result = self.analyzer.find_nav_path(loc, dest, self.ship.range()) + result = find_nav_path(self.c, loc, dest, self.ship.range()) self.sts('traject', result) return 'done' if len(result) == 0 else 'more' - path = self.analyzer.find_path(loc_sys, dest_sys) + path = find_jump_path(self.c, loc_sys, dest_sys) result = [] if loc.symbol != loc_jg.symbol: result.append(loc_jg_wp) diff --git a/nullptr/missions/sit.py b/nullptr/missions/sit.py index 7cd551c..0f63b05 100644 --- a/nullptr/missions/sit.py +++ b/nullptr/missions/sit.py @@ -1,5 +1,6 @@ from nullptr.missions.base import BaseMission, MissionParam from nullptr.models.waypoint import Waypoint +from time import time class SitMission(BaseMission): def start_state(self): @@ -14,10 +15,10 @@ class SitMission(BaseMission): def steps(self): return { **self.travel_steps('to', 'dest', 'sit'), - 'sit': (self.step_pass, 'done', self.wait_forever) + 'sit': (self.step_sit, 'market'), + 'market': (self.step_market, 'sit') } + + def step_sit(self): + self.next_step = time() + 15 * 60 - def wait_forever(self): - return 0 - - diff --git a/nullptr/missions/trade.py b/nullptr/missions/trade.py index 9adac1b..3310bd2 100644 --- a/nullptr/missions/trade.py +++ b/nullptr/missions/trade.py @@ -2,6 +2,8 @@ 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.analyzer import find_deal + class TradeMission(BaseMission): def start_state(self): return 'travel-to' @@ -11,7 +13,7 @@ class TradeMission(BaseMission): cargo_space = self.ship.cargo_capacity - self.ship.cargo_units smkt = self.store.get('Marketplace', self.st('site')) dmkt = self.store.get('Marketplace', self.st('dest')) - resource = self.analyzer.find_deal(smkt, dmkt) + resource = find_deal(smkt, dmkt) if resource is None: return 'done' price = smkt.buy_price(resource) diff --git a/nullptr/models/agent.py b/nullptr/models/agent.py index e0e6636..af2d1f8 100644 --- a/nullptr/models/agent.py +++ b/nullptr/models/agent.py @@ -6,6 +6,7 @@ class Agent(Base): self.token: str = None self.credits: int = 0 self.headquarters: Waypoint = None + self.phase = 'init' def update(self, d): self.seta('credits', d) diff --git a/nullptr/roles/__init__.py b/nullptr/roles/__init__.py new file mode 100644 index 0000000..6a8a709 --- /dev/null +++ b/nullptr/roles/__init__.py @@ -0,0 +1,20 @@ +from nullptr.roles.trader import assign_trader +from nullptr.roles.probe import assign_probe +from nullptr.roles.siphon import assign_siphon +from nullptr.roles.hauler import assign_hauler +from nullptr.roles.surveyor import assign_surveyor +from nullptr.roles.miner import assign_miner + +def assign_mission(c, s): + if s.role == 'trader': + assign_trader(c, s) + elif s.role == 'probe': + assign_probe(c, s) + elif s.role == 'siphon': + assign_siphon(c, s) + elif s.role == 'hauler': + assign_hauler(c, s) + elif s.role == 'surveyor': + assign_surveyor(c, s) + elif s.role == 'miner': + assign_miner(c, s) diff --git a/nullptr/roles/hauler.py b/nullptr/roles/hauler.py new file mode 100644 index 0000000..032bf2a --- /dev/null +++ b/nullptr/roles/hauler.py @@ -0,0 +1,16 @@ +from nullptr.util import AppError +from nullptr.analyzer import best_sell_market +from random import choice + +def assign_hauler(c, s): + if s.crew is None: + raise AppError('ship has no crew') + w = s.crew.site + resources = s.crew.resources + resource = choice(resources) + m = best_sell_market(c,s.location.system, resource) + s.log(f'assigning haul mission from {w} to {m}') + c.captain.init_mission(s, 'haul') + c.captain.smipa(s, 'site', w) + c.captain.smipa(s, 'dest', m) + c.captain.smipa(s, 'resources', resources) \ No newline at end of file diff --git a/nullptr/roles/miner.py b/nullptr/roles/miner.py new file mode 100644 index 0000000..193e7ea --- /dev/null +++ b/nullptr/roles/miner.py @@ -0,0 +1,11 @@ +from nullptr.util import AppError + +def assign_miner(c, s): + if s.crew is None: + raise AppError('ship has no crew') + w = s.crew.site + resources = s.crew.resources + c.captain.init_mission(s, 'mine') + c.captain.smipa(s, 'site', w) + c.captain.smipa(s, 'resources', resources) + \ No newline at end of file diff --git a/nullptr/roles/probe.py b/nullptr/roles/probe.py new file mode 100644 index 0000000..31daa78 --- /dev/null +++ b/nullptr/roles/probe.py @@ -0,0 +1,15 @@ +from nullptr.analyzer import solve_tsp +from random import randrange + +def assign_probe(c, s): + system = s.location.system + m = [m.waypoint for m in c.store.all_members(system, 'Marketplace')] + m = solve_tsp(c, m) + hops = [w.symbol for w in m] + start_hop = randrange(0, len(hops)) + s.log(f'Assigning {s} to probe {len(hops)} starting at {hops[start_hop]}') + + c.captain.init_mission(s, 'probe') + c.captain.smipa(s, 'hops', hops) + c.captain.smipa(s, 'next-hop', start_hop) + \ No newline at end of file diff --git a/nullptr/roles/siphon.py b/nullptr/roles/siphon.py new file mode 100644 index 0000000..20283eb --- /dev/null +++ b/nullptr/roles/siphon.py @@ -0,0 +1,8 @@ +from nullptr.util import AppError + +def assign_siphon(c, s): + if s.crew is None: + raise AppError('ship has no crew') + w = s.crew.site + c.captain.init_mission(s, 'siphon') + c.captain.smipa(s, 'site', w) \ No newline at end of file diff --git a/nullptr/roles/surveyor.py b/nullptr/roles/surveyor.py new file mode 100644 index 0000000..0d653d6 --- /dev/null +++ b/nullptr/roles/surveyor.py @@ -0,0 +1,10 @@ +from nullptr.util import AppError + + +def assign_surveyor(c, s): + if s.crew is None: + raise AppError('ship has no crew') + w = s.crew.site + c.init_mission(s, 'survey') + c.smipa(s, 'site', w) + \ No newline at end of file diff --git a/nullptr/roles/trader.py b/nullptr/roles/trader.py new file mode 100644 index 0000000..b742968 --- /dev/null +++ b/nullptr/roles/trader.py @@ -0,0 +1,14 @@ +from nullptr.analyzer import find_trade + + +def assign_trader(c, s): + t = find_trade(c, s.location.system) + if t is None: + print(f"No trade for {s} found. Idling") + c.captain.init_mission(s,'idle') + c.captain.smipa(s, 'seconds', 600) + return + s.log(f'assigning {s} to deliver {t.resource} from {t.source} to {t.dest} at a margin of {t.margin}') + c.captain.init_mission(s, 'trade') + c.captain.smipa(s, 'site', t.source) + c.captain.smipa(s, 'dest', t.dest) \ No newline at end of file diff --git a/nullptr/util.py b/nullptr/util.py index e7ea553..0868176 100644 --- a/nullptr/util.py +++ b/nullptr/util.py @@ -4,6 +4,9 @@ import os from os.path import isfile, dirname import traceback +class AppError(Exception): + pass + def open_file(fn): d = dirname(fn) os.makedirs(d, exist_ok=True)