diff --git a/nullptr/analyzer.py b/nullptr/analyzer.py index ed173e1..5941372 100644 --- a/nullptr/analyzer.py +++ b/nullptr/analyzer.py @@ -161,7 +161,8 @@ class Analyzer: 'wp': m.waypoint, 'buy': p.buy, 'sell': p.sell, - 'volume': p.volume + 'volume': p.volume, + 'category': m.rtype(r) }) return prices @@ -203,3 +204,14 @@ class Analyzer: 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 diff --git a/nullptr/api.py b/nullptr/api.py index 6392591..eab7237 100644 --- a/nullptr/api.py +++ b/nullptr/api.py @@ -213,6 +213,10 @@ class Api: def siphon(self, ship): data = self.request('post', f'my/ships/{ship}/siphon') ship.update(data) + amt = mg(data, 'siphon.yield.units') + rec = mg(data, 'siphon.yield.symbol') + ship.log(f"siphoned {amt} {rec}") + ship.location.extracted += amt return data['siphon'] def extract(self, ship, survey=None): @@ -229,6 +233,10 @@ class Api: else: raise e ship.update(data) + amt = mg(data, 'extraction.yield.units') + rec = mg(data, 'extraction.yield.symbol') + ship.log(f"extracted {amt} {rec}") + ship.location.extracted += amt return data def survey(self, ship): @@ -239,6 +247,14 @@ class Api: ######## Commerce ######### + def transaction_cost(self, data): + if not 'transaction' in data: return 0 + act = mg(data,'transaction.type') + minus = -1 if act == 'PURCHASE' else 1 + units = mg(data, 'transaction.units') + ppu = mg(data, 'transaction.pricePerUnit') + return ppu * units * minus + def log_transaction(self, data): if not 'transaction' in data: return typ = mg(data, 'transaction.tradeSymbol') @@ -294,6 +310,19 @@ class Api: self.agent.update(data['agent']) return data + def transfer(self, sship, dship, typ, amt): + data = { + 'tradeSymbol': typ, + 'units': amt, + 'shipSymbol': dship.symbol + } + data = self.request('post', f'my/ships/{sship.symbol}/transfer', data) + sship.log(f'transferred {amt} {typ} to {dship}') + dship.log(f'received {amt} {typ} from {sship}') + if 'cargo' in data: + sship.update(data) + dship.put_cargo(typ, amt) + def purchase(self, typ, wp): data = { 'shipType': typ, diff --git a/nullptr/central_command.py b/nullptr/central_command.py index 7bf7dee..5503377 100644 --- a/nullptr/central_command.py +++ b/nullptr/central_command.py @@ -1,6 +1,7 @@ from nullptr.store import Store from nullptr.models.ship import Ship from nullptr.missions import create_mission, get_mission_class +from nullptr.models.waypoint import Waypoint from random import choice, randrange from time import sleep from threading import Thread @@ -111,13 +112,35 @@ class CentralCommand: if s in self.missions: m = self.missions[s] - + def find_gas(self, s): + system = s.location.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 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) + + def assign_hauler(self, s): + w = self.find_gas(s) + m = self.analyzer.best_sell_market(s.location.system, 'HYDROCARBON') + self.init_mission(s, 'haul') + self.smipa(s, 'site', w) + self.smipa(s, 'dest', m) + + def assign_siphon(self, s): + w = self.find_gas(s) + 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')] @@ -165,6 +188,7 @@ class CentralCommand: mtype = s.mission m = create_mission(mtype, s, self.store, self.api) self.missions[s] = m + m.status(s.mission_status) return m def stop_mission(self, s): diff --git a/nullptr/commander.py b/nullptr/commander.py index 1db43eb..54f4307 100644 --- a/nullptr/commander.py +++ b/nullptr/commander.py @@ -118,7 +118,13 @@ class Commander(CommandLine): raise CommandError(f'{w} not found') return r - + def resolve_ship(self, arg): + symbol = f'{self.agent.symbol}-{arg}' + ship = self.store.get('Ship', symbol) + if ship is None: + raise CommandError(f'ship {arg} not found') + return ship + ######## First run ######### def agent_setup(self): symbol = input('agent name: ') @@ -170,13 +176,9 @@ class Commander(CommandLine): 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 + ship = self.resolve_ship(arg) + + self.ship = ship pprint(self.ship, 5) ######## Atlas ######### @@ -292,6 +294,17 @@ class Commander(CommandLine): self.api.jettison(self.ship, resource.upper()) self.do_cargo() + def do_transfer(self, resource, dship, amount=None): + if not self.has_ship(): return + resource = resource.upper() + avail = self.ship.get_cargo(resource) + if amount is None: amount = avail + amount = int(amount) + if avail < amount: + raise CommandError('resource not in cargo') + dship = self.resolve_ship(dship) + self.api.transfer(self.ship, dship, resource, amount) + def do_purchase(self, ship_type): if not self.has_ship(): return @@ -306,7 +319,6 @@ class Commander(CommandLine): def do_siphon(self): if not self.has_ship(): return data = self.api.siphon(self.ship) - pprint(data) def do_survey(self): if not self.has_ship(): return @@ -334,7 +346,7 @@ class Commander(CommandLine): pprint(self.ship.mission_state) def do_role(self, role): - roles = [None, 'trader', 'probe'] + roles = [None, 'trader', 'probe', 'siphon', 'hauler'] if not self.has_ship(): return if role == 'none': role = None @@ -565,7 +577,7 @@ class Commander(CommandLine): for res, p in prices.items(): print('==' + res) for m in p: - print(f"{m['wp'].symbol:12s} {m['volume']:5d} {m['buy']:5d} {m['sell']:5d}") + print(f"{m['wp'].symbol:12s} {m['category']} {m['volume']:5d} {m['buy']:5d} {m['sell']:5d}") def do_path(self, waypoint_str): if not self.has_ship(): return diff --git a/nullptr/missions/__init__.py b/nullptr/missions/__init__.py index 6d181aa..51b4660 100644 --- a/nullptr/missions/__init__.py +++ b/nullptr/missions/__init__.py @@ -4,7 +4,8 @@ from nullptr.missions.trade import TradeMission from nullptr.missions.travel import TravelMission from nullptr.missions.probe import ProbeMission from nullptr.missions.idle import IdleMission - +from nullptr.missions.siphon import SiphonMission +from nullptr.missions.haul import HaulMission def get_mission_class( mtype): types = { @@ -13,7 +14,10 @@ def get_mission_class( mtype): 'trade': TradeMission, 'travel': TravelMission, 'probe': ProbeMission, - 'idle': IdleMission + 'idle': IdleMission, + 'siphon': SiphonMission, + 'haul': HaulMission, + } if mtype not in types: raise ValueError(f'invalid mission type {mtype}') diff --git a/nullptr/missions/base.py b/nullptr/missions/base.py index 60a714c..0d82e74 100644 --- a/nullptr/missions/base.py +++ b/nullptr/missions/base.py @@ -51,9 +51,14 @@ class Mission: self.ship = ship self.store = store self.api = api + self.wait_for = None self.next_step = 0 self.analyzer = Analyzer(self.store) + self.setup() + def setup(self): + pass + def sts(self, nm, v): if issubclass(type(v), Base): v = v.symbol @@ -74,7 +79,17 @@ class Mission: if nw is None: return self.ship.mission_status else: - self.ship.mission_status = nw + steps = self.steps() + if nw in ['init','done', 'error']: + self.ship.mission_status = nw + return + elif nw not in steps: + self.ship.log(f"Invalid mission status {nw}", 1) + self.ship.mission_status = 'error' + return + wait_for = steps[nw][2] if len(steps[nw]) > 2 else None + self.wait_for = wait_for + self.ship.mission_status = nw def start_state(self): return 'done' @@ -96,10 +111,19 @@ class Mission: } def step_done(self): - self.ship.log(f'mission finished', 3) + self.ship.log(f'mission finished with balance {self.balance()}', 3) def is_waiting(self): - return self.next_step > time() or self.ship.cooldown > time() or self.ship.arrival > time() + if self.next_step > time() or self.ship.cooldown > time() or self.ship.arrival > time(): + return True + if self.wait_for is not None: + if self.wait_for(): + self.wait_for = None + return False + else: + return True + return False + def is_finished(self): return self.status() in ['done','error'] @@ -116,7 +140,10 @@ class Mission: self.ship.log(f"Invalid mission status {status}", 1) self.status('error') return - handler, next_step = steps[status] + + handler = steps[status][0] + next_step = steps[status][1] + try: result = handler() except Exception as e: @@ -136,6 +163,15 @@ class Mission: self.ship.log(f'{status} {result} -> {self.status()}', 8) class BaseMission(Mission): + def balance(self, amt=0): + if type(amt) == dict: + amt = self.api.transaction_cost(amt) + balance = self.st('balance') + if balance is None: balance = 0 + balance += amt + self.sts('balance', balance) + return balance + def step_go_dest(self): destination = self.rst(Waypoint, 'destination') if self.ship.location() == destination: @@ -179,8 +215,8 @@ class BaseMission(Mission): amt_cargo = self.ship.get_cargo(resource) amount = min(amt_cargo, volume) - self.api.sell(self.ship, resource, amount) - + res = self.api.sell(self.ship, resource, amount) + self.balance(res) if len(sellables) == 1 and amt_cargo == amount: return 'done' else: @@ -228,7 +264,7 @@ class BaseMission(Mission): if dest_sys == loc_sys: result = self.analyzer.find_nav_path(loc, dest, self.ship.range()) self.sts('traject', result) - return + return 'done' if len(result) == 0 else 'more' path = self.analyzer.find_path(loc_sys, dest_sys) result = [] if loc.symbol != loc_jg.symbol: @@ -238,7 +274,7 @@ class BaseMission(Mission): result.append(dest) self.sts('traject', result) print(result) - return result + return 'more' def step_dock(self): if self.ship.status == 'DOCKED': @@ -264,7 +300,10 @@ class BaseMission(Mission): calc = partial(self.step_calculate_traject, destination) steps = { - f'travel-{nm}': (calc, f'dock-{nm}'), + f'travel-{nm}': (calc, { + 'more': f'dock-{nm}', + 'done': next_step + }), f'dock-{nm}': (self.step_dock, f'refuel-{nm}'), f'refuel-{nm}': (self.step_refuel, f'orbit-{nm}'), f'orbit-{nm}': (self.step_orbit, f'go-{nm}'), diff --git a/nullptr/missions/haul.py b/nullptr/missions/haul.py new file mode 100644 index 0000000..995823e --- /dev/null +++ b/nullptr/missions/haul.py @@ -0,0 +1,73 @@ +from nullptr.missions.base import BaseMission, MissionParam +from nullptr.models.waypoint import Waypoint + +class HaulMission(BaseMission): + def start_state(self): + return 'travel-to' + + def step_turn(self): + self.ship.log('starting haul load') + + def wait_turn(self): + for s in self.store.all('Ship'): + if s.mission != 'haul': continue + if s.location != self.ship.location: + continue + if s.mission_state['dest'] != self.st('dest'): + continue + if s.mission_status != 'load': + continue + return False + return True + + def wait_cargo(self): + dmkt = self.store.get('Marketplace', self.st('dest')) + sellables = dmkt.prices.keys() + for s in self.store.all("Ship"): + if s.location != self.ship.location: continue + if s.mission not in ['mine','siphon']: continue + for r, a in s.cargo.items(): + if r not in sellables: continue + return True + return False + + def step_load(self): + cargo_space = self.ship.cargo_capacity - self.ship.cargo_units + dmkt = self.store.get('Marketplace', self.st('dest')) + sellables = dmkt.prices.keys() + for s in self.store.all("Ship"): + if s.location != self.ship.location: continue + if s.mission not in ['mine','siphon']: continue + for r, a in s.cargo.items(): + if r not in sellables: continue + amount = min(cargo_space, a) + + res = self.api.transfer(s, self.ship, r, amount) + + return 'done' if amount == cargo_space else 'more' + return 'more' + + @classmethod + def params(cls): + return { + 'site': MissionParam(Waypoint, True), + 'dest': MissionParam(Waypoint, True), + } + + def steps(self): + return { + **self.travel_steps('to', 'site', 'wait-turn'), + 'wait-turn': (self.step_turn, 'load', self.wait_turn), + 'load': (self.step_load, { + 'more': 'load', + 'done': 'travel-back' + }, self.wait_cargo), + **self.travel_steps('back', 'dest', 'dock-dest'), + 'dock-dest': (self.step_dock, 'unload'), + 'unload': (self.step_sell, { + 'more': 'unload', + 'done': 'market-dest' + }), + 'market-dest': (self.step_market, 'report'), + 'report': (self.step_done, 'done') + } diff --git a/nullptr/missions/siphon.py b/nullptr/missions/siphon.py new file mode 100644 index 0000000..81a45b2 --- /dev/null +++ b/nullptr/missions/siphon.py @@ -0,0 +1,25 @@ +from nullptr.missions.base import BaseMission, MissionParam +from nullptr.models.waypoint import Waypoint + +class SiphonMission(BaseMission): + def start_state(self): + return 'travel-to' + + @classmethod + def params(cls): + return { + 'site': MissionParam(Waypoint, True), + } + + def steps(self): + return { + **self.travel_steps('to', 'site', 'siphon'), + 'siphon': (self.step_siphon, 'done', self.cargo_full) + } + + def cargo_full(self): + return self.ship.cargo_capacity - self.ship.cargo_units > 5 + + def step_siphon(self): + result = self.api.siphon(self.ship) + self.next_step = self.ship.cooldown diff --git a/nullptr/missions/trade.py b/nullptr/missions/trade.py index a0ba551..9adac1b 100644 --- a/nullptr/missions/trade.py +++ b/nullptr/missions/trade.py @@ -20,7 +20,8 @@ class TradeMission(BaseMission): amount = min(cargo_space, affordable, volume) if amount == 0: return 'done' - self.api.buy(self.ship, resource, amount) + res = self.api.buy(self.ship, resource, amount) + self.balance(res) return 'done' if amount == cargo_space else 'more' @classmethod @@ -46,5 +47,6 @@ class TradeMission(BaseMission): 'more': 'unload', 'done': 'market-dest' }), - 'market-dest': (self.step_market, 'done'), + 'market-dest': (self.step_market, 'report'), + 'report': (self.step_done, 'done') } diff --git a/nullptr/models/marketplace.py b/nullptr/models/marketplace.py index 8303334..56bdc93 100644 --- a/nullptr/models/marketplace.py +++ b/nullptr/models/marketplace.py @@ -102,5 +102,5 @@ class Marketplace(Base): r += '\n' for res, p in self.prices.items(): t = self.rtype(res) - r += f'{t} {res:25s} {p.sell:5d} {p.buy:5d}\n' + r += f'{t} {res:25s} {p.buy:5d} {p.sell:5d}\n' return r diff --git a/nullptr/models/ship.py b/nullptr/models/ship.py index c2c604b..33fbf2d 100644 --- a/nullptr/models/ship.py +++ b/nullptr/models/ship.py @@ -88,6 +88,14 @@ class Ship(Base): self.cargo[typ] -= amt self.cargo_units = sum(self.cargo.values()) + + def put_cargo(self, typ, amt): + if typ not in self.cargo: + self.cargo[typ] = amt + else: + self.cargo[typ] += amt + + self.cargo_units = sum(self.cargo.values()) def load_cargo(self, cargo): result = {} diff --git a/nullptr/models/waypoint.py b/nullptr/models/waypoint.py index d42bb10..5b3ef0f 100644 --- a/nullptr/models/waypoint.py +++ b/nullptr/models/waypoint.py @@ -13,6 +13,7 @@ class Waypoint(Base): self.faction:str = '' self.is_under_construction:bool = False self.uncharted = True + self.extracted:int = 0 def update(self, d): diff --git a/nullptr/store.py b/nullptr/store.py index d2dee16..7d9d49e 100644 --- a/nullptr/store.py +++ b/nullptr/store.py @@ -9,6 +9,7 @@ import pickle from struct import unpack, pack from functools import partial from io import BytesIO +from copy import copy class StorePickler(pickle.Pickler): def persistent_id(self, obj): @@ -315,7 +316,7 @@ class Store: self.cleanup() it = 0 start_time = time() - for obj in self.dirty_objects: + for obj in copy(self.dirty_objects): it += 1 if obj.is_expired(): self.purge(obj)