siphoning and hauling

This commit is contained in:
Richard 2024-01-20 20:33:50 +01:00
parent 592c628a46
commit 3f7a416fdc
13 changed files with 259 additions and 29 deletions

View File

@ -161,7 +161,8 @@ class Analyzer:
'wp': m.waypoint, 'wp': m.waypoint,
'buy': p.buy, 'buy': p.buy,
'sell': p.sell, 'sell': p.sell,
'volume': p.volume 'volume': p.volume,
'category': m.rtype(r)
}) })
return prices return prices
@ -203,3 +204,14 @@ class Analyzer:
best_margin = margin best_margin = margin
best_resource = r best_resource = r
return best_resource 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

View File

@ -213,6 +213,10 @@ class Api:
def siphon(self, ship): def siphon(self, ship):
data = self.request('post', f'my/ships/{ship}/siphon') data = self.request('post', f'my/ships/{ship}/siphon')
ship.update(data) 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'] return data['siphon']
def extract(self, ship, survey=None): def extract(self, ship, survey=None):
@ -229,6 +233,10 @@ class Api:
else: else:
raise e raise e
ship.update(data) 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 return data
def survey(self, ship): def survey(self, ship):
@ -239,6 +247,14 @@ class Api:
######## Commerce ######### ######## 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): def log_transaction(self, data):
if not 'transaction' in data: return if not 'transaction' in data: return
typ = mg(data, 'transaction.tradeSymbol') typ = mg(data, 'transaction.tradeSymbol')
@ -294,6 +310,19 @@ class Api:
self.agent.update(data['agent']) self.agent.update(data['agent'])
return data 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): def purchase(self, typ, wp):
data = { data = {
'shipType': typ, 'shipType': typ,

View File

@ -1,6 +1,7 @@
from nullptr.store import Store from nullptr.store import Store
from nullptr.models.ship import Ship from nullptr.models.ship import Ship
from nullptr.missions import create_mission, get_mission_class from nullptr.missions import create_mission, get_mission_class
from nullptr.models.waypoint import Waypoint
from random import choice, randrange from random import choice, randrange
from time import sleep from time import sleep
from threading import Thread from threading import Thread
@ -111,12 +112,34 @@ class CentralCommand:
if s in self.missions: if s in self.missions:
m = self.missions[s] 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): def assign_mission(self, s):
if s.role == 'trader': if s.role == 'trader':
self.assign_trade(s) self.assign_trade(s)
elif s.role == 'probe': elif s.role == 'probe':
self.assign_probe(s) 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): def assign_probe(self, s):
system = s.location.system system = s.location.system
@ -165,6 +188,7 @@ class CentralCommand:
mtype = s.mission mtype = s.mission
m = create_mission(mtype, s, self.store, self.api) m = create_mission(mtype, s, self.store, self.api)
self.missions[s] = m self.missions[s] = m
m.status(s.mission_status)
return m return m
def stop_mission(self, s): def stop_mission(self, s):

View File

@ -118,6 +118,12 @@ class Commander(CommandLine):
raise CommandError(f'{w} not found') raise CommandError(f'{w} not found')
return r 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 ######### ######## First run #########
def agent_setup(self): def agent_setup(self):
@ -170,12 +176,8 @@ class Commander(CommandLine):
def do_ship(self, arg=''): def do_ship(self, arg=''):
if arg != '': if arg != '':
symbol = f'{self.agent.symbol}-{arg}' ship = self.resolve_ship(arg)
ship = self.store.get('Ship', symbol)
if ship is None:
print('not found')
return
else:
self.ship = ship self.ship = ship
pprint(self.ship, 5) pprint(self.ship, 5)
@ -292,6 +294,17 @@ class Commander(CommandLine):
self.api.jettison(self.ship, resource.upper()) self.api.jettison(self.ship, resource.upper())
self.do_cargo() 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): def do_purchase(self, ship_type):
if not self.has_ship(): return if not self.has_ship(): return
@ -306,7 +319,6 @@ class Commander(CommandLine):
def do_siphon(self): def do_siphon(self):
if not self.has_ship(): return if not self.has_ship(): return
data = self.api.siphon(self.ship) data = self.api.siphon(self.ship)
pprint(data)
def do_survey(self): def do_survey(self):
if not self.has_ship(): return if not self.has_ship(): return
@ -334,7 +346,7 @@ class Commander(CommandLine):
pprint(self.ship.mission_state) pprint(self.ship.mission_state)
def do_role(self, role): def do_role(self, role):
roles = [None, 'trader', 'probe'] roles = [None, 'trader', 'probe', 'siphon', 'hauler']
if not self.has_ship(): return if not self.has_ship(): return
if role == 'none': if role == 'none':
role = None role = None
@ -565,7 +577,7 @@ class Commander(CommandLine):
for res, p in prices.items(): for res, p in prices.items():
print('==' + res) print('==' + res)
for m in p: 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): def do_path(self, waypoint_str):
if not self.has_ship(): return if not self.has_ship(): return

View File

@ -4,7 +4,8 @@ from nullptr.missions.trade import TradeMission
from nullptr.missions.travel import TravelMission from nullptr.missions.travel import TravelMission
from nullptr.missions.probe import ProbeMission from nullptr.missions.probe import ProbeMission
from nullptr.missions.idle import IdleMission from nullptr.missions.idle import IdleMission
from nullptr.missions.siphon import SiphonMission
from nullptr.missions.haul import HaulMission
def get_mission_class( mtype): def get_mission_class( mtype):
types = { types = {
@ -13,7 +14,10 @@ def get_mission_class( mtype):
'trade': TradeMission, 'trade': TradeMission,
'travel': TravelMission, 'travel': TravelMission,
'probe': ProbeMission, 'probe': ProbeMission,
'idle': IdleMission 'idle': IdleMission,
'siphon': SiphonMission,
'haul': HaulMission,
} }
if mtype not in types: if mtype not in types:
raise ValueError(f'invalid mission type {mtype}') raise ValueError(f'invalid mission type {mtype}')

View File

@ -51,8 +51,13 @@ class Mission:
self.ship = ship self.ship = ship
self.store = store self.store = store
self.api = api self.api = api
self.wait_for = None
self.next_step = 0 self.next_step = 0
self.analyzer = Analyzer(self.store) self.analyzer = Analyzer(self.store)
self.setup()
def setup(self):
pass
def sts(self, nm, v): def sts(self, nm, v):
if issubclass(type(v), Base): if issubclass(type(v), Base):
@ -74,6 +79,16 @@ class Mission:
if nw is None: if nw is None:
return self.ship.mission_status return self.ship.mission_status
else: else:
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 self.ship.mission_status = nw
def start_state(self): def start_state(self):
@ -96,10 +111,19 @@ class Mission:
} }
def step_done(self): 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): 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): def is_finished(self):
return self.status() in ['done','error'] return self.status() in ['done','error']
@ -116,7 +140,10 @@ class Mission:
self.ship.log(f"Invalid mission status {status}", 1) self.ship.log(f"Invalid mission status {status}", 1)
self.status('error') self.status('error')
return return
handler, next_step = steps[status]
handler = steps[status][0]
next_step = steps[status][1]
try: try:
result = handler() result = handler()
except Exception as e: except Exception as e:
@ -136,6 +163,15 @@ class Mission:
self.ship.log(f'{status} {result} -> {self.status()}', 8) self.ship.log(f'{status} {result} -> {self.status()}', 8)
class BaseMission(Mission): 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): def step_go_dest(self):
destination = self.rst(Waypoint, 'destination') destination = self.rst(Waypoint, 'destination')
if self.ship.location() == destination: if self.ship.location() == destination:
@ -179,8 +215,8 @@ class BaseMission(Mission):
amt_cargo = self.ship.get_cargo(resource) amt_cargo = self.ship.get_cargo(resource)
amount = min(amt_cargo, volume) 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: if len(sellables) == 1 and amt_cargo == amount:
return 'done' return 'done'
else: else:
@ -228,7 +264,7 @@ class BaseMission(Mission):
if dest_sys == loc_sys: if dest_sys == loc_sys:
result = self.analyzer.find_nav_path(loc, dest, self.ship.range()) result = self.analyzer.find_nav_path(loc, dest, self.ship.range())
self.sts('traject', result) self.sts('traject', result)
return return 'done' if len(result) == 0 else 'more'
path = self.analyzer.find_path(loc_sys, dest_sys) path = self.analyzer.find_path(loc_sys, dest_sys)
result = [] result = []
if loc.symbol != loc_jg.symbol: if loc.symbol != loc_jg.symbol:
@ -238,7 +274,7 @@ class BaseMission(Mission):
result.append(dest) result.append(dest)
self.sts('traject', result) self.sts('traject', result)
print(result) print(result)
return result return 'more'
def step_dock(self): def step_dock(self):
if self.ship.status == 'DOCKED': if self.ship.status == 'DOCKED':
@ -264,7 +300,10 @@ class BaseMission(Mission):
calc = partial(self.step_calculate_traject, destination) calc = partial(self.step_calculate_traject, destination)
steps = { 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'dock-{nm}': (self.step_dock, f'refuel-{nm}'),
f'refuel-{nm}': (self.step_refuel, f'orbit-{nm}'), f'refuel-{nm}': (self.step_refuel, f'orbit-{nm}'),
f'orbit-{nm}': (self.step_orbit, f'go-{nm}'), f'orbit-{nm}': (self.step_orbit, f'go-{nm}'),

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

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

View File

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

View File

@ -20,7 +20,8 @@ class TradeMission(BaseMission):
amount = min(cargo_space, affordable, volume) amount = min(cargo_space, affordable, volume)
if amount == 0: if amount == 0:
return 'done' 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' return 'done' if amount == cargo_space else 'more'
@classmethod @classmethod
@ -46,5 +47,6 @@ class TradeMission(BaseMission):
'more': 'unload', 'more': 'unload',
'done': 'market-dest' 'done': 'market-dest'
}), }),
'market-dest': (self.step_market, 'done'), 'market-dest': (self.step_market, 'report'),
'report': (self.step_done, 'done')
} }

View File

@ -102,5 +102,5 @@ class Marketplace(Base):
r += '\n' r += '\n'
for res, p in self.prices.items(): for res, p in self.prices.items():
t = self.rtype(res) 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 return r

View File

@ -89,6 +89,14 @@ class Ship(Base):
self.cargo_units = sum(self.cargo.values()) 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): def load_cargo(self, cargo):
result = {} result = {}
total = 0 total = 0

View File

@ -13,6 +13,7 @@ class Waypoint(Base):
self.faction:str = '' self.faction:str = ''
self.is_under_construction:bool = False self.is_under_construction:bool = False
self.uncharted = True self.uncharted = True
self.extracted:int = 0
def update(self, d): def update(self, d):

View File

@ -9,6 +9,7 @@ import pickle
from struct import unpack, pack from struct import unpack, pack
from functools import partial from functools import partial
from io import BytesIO from io import BytesIO
from copy import copy
class StorePickler(pickle.Pickler): class StorePickler(pickle.Pickler):
def persistent_id(self, obj): def persistent_id(self, obj):
@ -315,7 +316,7 @@ class Store:
self.cleanup() self.cleanup()
it = 0 it = 0
start_time = time() start_time = time()
for obj in self.dirty_objects: for obj in copy(self.dirty_objects):
it += 1 it += 1
if obj.is_expired(): if obj.is_expired():
self.purge(obj) self.purge(obj)