haulin goods
This commit is contained in:
parent
b47fa44cb0
commit
1b7a528655
@ -10,4 +10,4 @@ RUN chmod +x /app/main.py
|
||||
VOLUME /data
|
||||
#ENTRYPOINT bash
|
||||
ENTRYPOINT [ "python3", "/app/main.py"]
|
||||
CMD ["-s", "/data/store.npt", "-x", "/data/cmd.hst"]
|
||||
CMD ["-d", "/data"]
|
||||
|
8
main.py
8
main.py
@ -1,9 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
from nullptr.commander import Commander
|
||||
import os
|
||||
from nullptr.models.base import Base
|
||||
def main(args):
|
||||
c = Commander(args.store_file)
|
||||
if not os.path.isdir(args.data_dir):
|
||||
os.makedirs(args.data_dir )
|
||||
c = Commander(args.data_dir)
|
||||
c.run()
|
||||
|
||||
# X1-AG74-41076A
|
||||
@ -11,7 +14,6 @@ def main(args):
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-s', '--store-file', default='data/store.npt')
|
||||
parser.add_argument('-x', '--history-file', default='data/cmd.hst')
|
||||
parser.add_argument('-d', '--data-dir', default='data')
|
||||
args = parser.parse_args()
|
||||
main(args)
|
||||
|
@ -4,6 +4,15 @@ from nullptr.models.system import System
|
||||
from nullptr.models.waypoint import Waypoint
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class TradeOption:
|
||||
resource: str
|
||||
source: Waypoint
|
||||
dest: Waypoint
|
||||
margin: int
|
||||
dist: int
|
||||
score: float
|
||||
|
||||
@dataclass
|
||||
class SearchNode:
|
||||
system: System
|
||||
@ -88,3 +97,34 @@ class Analyzer:
|
||||
if len(dest) == 0:
|
||||
return None
|
||||
return self.find_path(dest, to, depth-1, seen)
|
||||
|
||||
def prices(self, system):
|
||||
prices = {}
|
||||
for m in self.store.all_members(system, Marketplace):
|
||||
for p in m.prices.values():
|
||||
r = p['symbol']
|
||||
if not r in prices:
|
||||
prices[r] = []
|
||||
prices[r].append({
|
||||
'wp': m.waypoint,
|
||||
'buy': p['buy'],
|
||||
'sell': p['sell']
|
||||
})
|
||||
return prices
|
||||
|
||||
def find_trade(self, system):
|
||||
prices = self.prices(system)
|
||||
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]
|
||||
margin = dest['sell'] -source['buy']
|
||||
if margin < 0:
|
||||
continue
|
||||
dist = source['wp'].distance(dest['wp'])
|
||||
dist = max(dist, 0.0001)
|
||||
score = margin / dist
|
||||
o = TradeOption(resource, source['wp'], dest['wp'], margin, dist, score)
|
||||
if best is None or best.score < o.score:
|
||||
best = o
|
||||
return best
|
||||
|
@ -5,6 +5,7 @@ from random import choice
|
||||
from time import sleep
|
||||
from threading import Thread
|
||||
from nullptr.atlas_builder import AtlasBuilder
|
||||
from nullptr.analyzer import Analyzer
|
||||
|
||||
class CentralCommandError(Exception):
|
||||
pass
|
||||
@ -14,9 +15,9 @@ class CentralCommand:
|
||||
self.missions = {}
|
||||
self.stopping = False
|
||||
self.store = store
|
||||
self.analyzer = Analyzer(store)
|
||||
self.api = api
|
||||
self.atlas_builder = AtlasBuilder(store, api)
|
||||
self.update_missions()
|
||||
|
||||
def get_ready_missions(self):
|
||||
result = []
|
||||
@ -26,6 +27,7 @@ class CentralCommand:
|
||||
return result
|
||||
|
||||
def tick(self):
|
||||
self.update_missions()
|
||||
missions = self.get_ready_missions()
|
||||
if len(missions) == 0: return False
|
||||
ship = choice(missions)
|
||||
@ -41,7 +43,6 @@ class CentralCommand:
|
||||
self.run()
|
||||
print('manual mode')
|
||||
|
||||
|
||||
def wait_for_stop(self):
|
||||
try:
|
||||
input()
|
||||
@ -52,7 +53,6 @@ class CentralCommand:
|
||||
|
||||
|
||||
def run(self):
|
||||
self.update_missions()
|
||||
while not self.stopping:
|
||||
did_step = True
|
||||
request_counter = self.api.requests_sent
|
||||
@ -87,16 +87,41 @@ class CentralCommand:
|
||||
return
|
||||
ship.set_mission_state(nm, parsed_val)
|
||||
|
||||
def smipa(self,s,n,v):
|
||||
self.set_mission_param(s,n,v)
|
||||
|
||||
def update_missions(self):
|
||||
for s in self.store.all(Ship):
|
||||
if s.mission_status == 'done':
|
||||
s.mission = None
|
||||
if s.mission is None:
|
||||
if s in self.missions:
|
||||
self.stop_mission(s)
|
||||
elif s not in self.missions:
|
||||
if s.mission is None:
|
||||
self.assign_mission(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]
|
||||
m.next_step = max(s.cooldown, s.arrival)
|
||||
|
||||
|
||||
def assign_mission(self, s):
|
||||
if s.role == 'hauler':
|
||||
self.assign_haul(s)
|
||||
|
||||
def assign_haul(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
|
||||
print(f'assigning {s} to deliver {t.resource} from {t.source} to {t.dest} at a margin of {t.margin}')
|
||||
self.init_mission(s, 'haul')
|
||||
self.smipa(s, 'resource', t.resource)
|
||||
self.smipa(s, 'site', t.source)
|
||||
self.smipa(s, 'dest', t.dest)
|
||||
self.smipa(s, 'delivery', 'sell')
|
||||
|
||||
def init_mission(self, s, mtyp):
|
||||
if mtyp == 'none':
|
||||
@ -113,6 +138,11 @@ class CentralCommand:
|
||||
s.mission_state = {k: v.default for k,v in mclass.params().items()}
|
||||
self.start_mission(s)
|
||||
|
||||
def restart_mission(self, s):
|
||||
if s not in self.missions:
|
||||
raise CentralCommandError("no mission assigned")
|
||||
s.mission_status = 'init'
|
||||
|
||||
def start_mission(self, s):
|
||||
mtype = s.mission
|
||||
m = create_mission(mtype, s, self.store, self.api)
|
||||
|
@ -17,11 +17,13 @@ class CommandError(Exception):
|
||||
pass
|
||||
|
||||
class Commander(CommandLine):
|
||||
def __init__(self, store_file='data/store.npt', hist_file = 'data/cmd.hst'):
|
||||
def __init__(self, data_dir='data'):
|
||||
store_file = os.path.join(data_dir, 'store.npt')
|
||||
hist_file = os.path.join(data_dir, 'cmd.hst')
|
||||
self.hist_file = hist_file
|
||||
if os.path.isfile(hist_file):
|
||||
readline.read_history_file(hist_file)
|
||||
self.store = Store(store_file)
|
||||
self.store = Store(store_file, True)
|
||||
self.agent = self.select_agent()
|
||||
self.api = Api(self.store, self.agent)
|
||||
self.centcom = CentralCommand(self.store, self.api)
|
||||
@ -124,12 +126,25 @@ class Commander(CommandLine):
|
||||
print(f'mission: {self.ship.mission} ({self.ship.mission_status})')
|
||||
pprint(self.ship.mission_state)
|
||||
|
||||
def do_role(self, role):
|
||||
roles = ['hauler']
|
||||
if not self.has_ship(): return
|
||||
if role not in roles:
|
||||
print(f'role {role} not found. Choose from {roles}')
|
||||
return
|
||||
self.ship.role = role
|
||||
|
||||
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_mrestart(self):
|
||||
if not self.has_ship(): return
|
||||
self.centcom.restart_mission(self.ship)
|
||||
self.print_mission()
|
||||
|
||||
def do_mreset(self):
|
||||
if not self.has_ship(): return
|
||||
self.ship.mission_state = {}
|
||||
@ -254,9 +269,11 @@ class Commander(CommandLine):
|
||||
pprint(r)
|
||||
|
||||
def do_waypoints(self, system_str=''):
|
||||
loc = None
|
||||
if system_str == '':
|
||||
if not self.has_ship(): return
|
||||
system = self.ship.location.system
|
||||
loc = self.ship.location
|
||||
system = loc.system
|
||||
else:
|
||||
system = self.store.get(System, system_str)
|
||||
print(f'=== waypoints in {system}')
|
||||
@ -276,7 +293,7 @@ class Commander(CommandLine):
|
||||
traits.append('METAL')
|
||||
if 'PRECIOUS_METAL_DEPOSITS' in w.traits:
|
||||
traits.append('GOLD')
|
||||
if 'EXPLOSIVE_GASSES' in w.traits:
|
||||
if 'EXPLOSIVE_GASES' in w.traits:
|
||||
traits.append('GAS')
|
||||
if 'MINERAL_DEPOSITS' in w.traits:
|
||||
traits.append('MINS')
|
||||
@ -288,6 +305,11 @@ class Commander(CommandLine):
|
||||
typ = w.type[0]
|
||||
if typ not in ['F','J'] and len(traits) == 0:
|
||||
continue
|
||||
|
||||
if loc:
|
||||
dist = loc.distance(w)
|
||||
print(f'{wname:4} {typ} {dist:6} {traits}')
|
||||
else:
|
||||
print(f'{wname:4} {typ} {traits}')
|
||||
|
||||
def do_members(self):
|
||||
@ -441,17 +463,7 @@ class Commander(CommandLine):
|
||||
def do_prices(self, resource=None):
|
||||
if not self.has_ship(): return
|
||||
system = self.ship.location.system
|
||||
prices = {}
|
||||
for m in self.store.all_members(system, Marketplace):
|
||||
for p in m.prices.values():
|
||||
r = p['symbol']
|
||||
if not r in prices:
|
||||
prices[r] = []
|
||||
prices[r].append({
|
||||
'wp': m.symbol,
|
||||
'buy': p['buy'],
|
||||
'sell': p['sell']
|
||||
})
|
||||
prices = self.analyzer.prices(system)
|
||||
if resource is not None:
|
||||
pprint(prices[resource.upper()])
|
||||
else:
|
||||
|
@ -3,6 +3,8 @@ from nullptr.missions.mine import MiningMission
|
||||
from nullptr.missions.haul import HaulMission
|
||||
from nullptr.missions.travel import TravelMission
|
||||
from nullptr.missions.probe import ProbeMission
|
||||
from nullptr.missions.idle import IdleMission
|
||||
|
||||
|
||||
def get_mission_class( mtype):
|
||||
types = {
|
||||
@ -10,7 +12,8 @@ def get_mission_class( mtype):
|
||||
'mine': MiningMission,
|
||||
'haul': HaulMission,
|
||||
'travel': TravelMission,
|
||||
'probe': ProbeMission
|
||||
'probe': ProbeMission,
|
||||
'idle': IdleMission
|
||||
}
|
||||
if mtype not in types:
|
||||
raise ValueError(f'invalid mission type {mtype}')
|
||||
|
@ -97,7 +97,7 @@ class Mission:
|
||||
logging.info(f'mission finished for {self.ship}')
|
||||
|
||||
def is_waiting(self):
|
||||
return self.next_step > time()
|
||||
return self.next_step > time() or self.ship.cooldown > time() or self.ship.arrival > time()
|
||||
|
||||
def is_finished(self):
|
||||
return self.status() in ['done','error']
|
||||
@ -147,6 +147,10 @@ class BaseMission(Mission):
|
||||
self.api.navigate(self.ship, site)
|
||||
self.next_step = self.ship.arrival
|
||||
|
||||
def step_market(self):
|
||||
loc = self.ship.location
|
||||
self.api.marketplace(loc)
|
||||
|
||||
def step_unload(self):
|
||||
delivery = self.st('delivery')
|
||||
if delivery == 'sell':
|
||||
@ -176,9 +180,15 @@ class BaseMission(Mission):
|
||||
return 'more'
|
||||
|
||||
def step_load(self):
|
||||
credits = self.api.agent.credits
|
||||
cargo_space = self.ship.cargo_capacity - self.ship.cargo_units
|
||||
resource = self.st('resource')
|
||||
self.api.buy(self.ship, resource, cargo_space)
|
||||
loc = self.ship.location
|
||||
market = self.store.get('Marketplace', loc.symbol)
|
||||
price = market.buy_price(resource)
|
||||
affordable = credits // price
|
||||
amount = min(cargo_space, affordable)
|
||||
self.api.buy(self.ship, resource, amount)
|
||||
|
||||
def step_travel(self):
|
||||
traject = self.st('traject')
|
||||
|
@ -18,8 +18,10 @@ class HaulMission(BaseMission):
|
||||
|
||||
def steps(self):
|
||||
return {
|
||||
**self.travel_steps('to', 'site', 'load'),
|
||||
**self.travel_steps('to', 'site', 'market'),
|
||||
'market': (self.step_market, 'load'),
|
||||
'load': (self.step_load, 'travel-back'),
|
||||
**self.travel_steps('back', 'dest', 'unload'),
|
||||
'unload': (self.step_unload, 'travel-to'),
|
||||
'unload': (self.step_unload, 'market-dest'),
|
||||
'market-dest': (self.step_market, 'done'),
|
||||
}
|
||||
|
26
nullptr/missions/idle.py
Normal file
26
nullptr/missions/idle.py
Normal file
@ -0,0 +1,26 @@
|
||||
from nullptr.missions.base import BaseMission, MissionParam
|
||||
import time
|
||||
|
||||
class IdleMission(BaseMission):
|
||||
def start_state(self):
|
||||
return 'start'
|
||||
|
||||
def step_wait(self):
|
||||
self.next_step = int(time.time()) + self.st('seconds')
|
||||
|
||||
def step_idle(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def params(cls):
|
||||
return {
|
||||
'seconds': MissionParam(int, True)
|
||||
}
|
||||
|
||||
def steps(self):
|
||||
return {
|
||||
'start': (self.step_wait, 'wait'),
|
||||
'wait': (self.step_idle, 'done')
|
||||
}
|
||||
|
||||
|
@ -20,10 +20,6 @@ class ProbeMission(BaseMission):
|
||||
|
||||
}
|
||||
|
||||
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')
|
||||
|
@ -36,6 +36,11 @@ class Marketplace(Base):
|
||||
prices[symbol] = price
|
||||
self.prices = prices
|
||||
|
||||
def buy_price(self, resource):
|
||||
if resource not in self.prices:
|
||||
return None
|
||||
return self.prices[resource]['buy']
|
||||
|
||||
def sellable_items(self, resources):
|
||||
return [r for r in resources if r in self.prices]
|
||||
|
||||
|
@ -17,6 +17,7 @@ class Ship(Base):
|
||||
self.fuel_capacity:int = 0
|
||||
self.mission:str = None
|
||||
self.mission_status:str = 'init'
|
||||
self.role = None
|
||||
|
||||
@classmethod
|
||||
def ext(self):
|
||||
@ -100,6 +101,8 @@ class Ship(Base):
|
||||
cooldown = int(self.cooldown - time())
|
||||
r = self.symbol
|
||||
if detail > 1:
|
||||
if self.role is not None:
|
||||
r += f' {self.role}'
|
||||
r += ' ' + self.status
|
||||
r += f' [{self.fuel_current}/{self.fuel_capacity}]'
|
||||
r += ' ' + str(self.location)
|
||||
|
@ -2,6 +2,7 @@ from .base import Base, Reference
|
||||
from nullptr.models.system import System
|
||||
from nullptr.util import *
|
||||
from time import time
|
||||
from math import sqrt
|
||||
|
||||
class Waypoint(Base):
|
||||
def define(self):
|
||||
@ -24,6 +25,9 @@ class Waypoint(Base):
|
||||
self.setlst('traits', d, 'traits', 'symbol')
|
||||
self.uncharted = 'UNCHARTED' in self.traits
|
||||
|
||||
def distance(self, other):
|
||||
return int(sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2))
|
||||
|
||||
@classmethod
|
||||
def ext(self):
|
||||
return 'way'
|
||||
|
@ -24,9 +24,11 @@ class StoreUnpickler(pickle.Unpickler):
|
||||
return self.store
|
||||
raise pickle.UnpicklingError("I don know the persid!")
|
||||
|
||||
CHUNK_MAGIC = b'ChNk'
|
||||
|
||||
class ChunkHeader:
|
||||
def __init__(self):
|
||||
self.magic = CHUNK_MAGIC
|
||||
self.offset = 0
|
||||
self.in_use = True
|
||||
self.size = 0
|
||||
@ -35,14 +37,16 @@ class ChunkHeader:
|
||||
@classmethod
|
||||
def parse(cls, fil):
|
||||
offset = fil.tell()
|
||||
d = fil.read(16)
|
||||
if len(d) < 16:
|
||||
d = fil.read(20)
|
||||
if len(d) < 20:
|
||||
return None
|
||||
o = cls()
|
||||
o.offset = offset
|
||||
d, o.used = unpack('<QQ', d)
|
||||
o.magic, d, o.used = unpack('<4sQQ', d)
|
||||
o.size = d & 0x7fffffffffffffff
|
||||
o.in_use = d & 0x8000000000000000 != 0
|
||||
if o.magic != CHUNK_MAGIC:
|
||||
raise ValueError(f"Invalid chunk magic: {o.magic}")
|
||||
# print(o)
|
||||
return o
|
||||
|
||||
@ -50,7 +54,7 @@ class ChunkHeader:
|
||||
d = self.size
|
||||
if self.in_use:
|
||||
d |= 1 << 63
|
||||
d = pack('<QQ', d, self.used)
|
||||
d = pack('<4sQQ', self.magic, d, self.used)
|
||||
f.write(d)
|
||||
|
||||
def __repr__(self):
|
||||
@ -113,11 +117,13 @@ class Store:
|
||||
self.p(hdr)
|
||||
total += hdr.size
|
||||
if not hdr.in_use:
|
||||
print(f"skip {hdr.size} {self.fil.tell()}")
|
||||
self.fil.seek(hdr.size, 1)
|
||||
free += hdr.size
|
||||
else:
|
||||
data = self.fil.read(hdr.used)
|
||||
self.load_object(data, offset)
|
||||
print(f"pad {hdr.size - hdr.used}")
|
||||
self.fil.seek(hdr.size - hdr.used, 1)
|
||||
cnt += 1
|
||||
offset = self.fil.tell()
|
||||
|
@ -50,7 +50,7 @@ class TestStore(unittest.TestCase):
|
||||
dum2 = self.s.get(Dummy, "7",create=True)
|
||||
self.reopen()
|
||||
dum = self.s.get(Dummy, "5")
|
||||
dum.data = "A" * 100
|
||||
dum.data = "A" * 1000
|
||||
dum.count = 1337
|
||||
self.reopen()
|
||||
dum = self.s.get(Dummy, "5")
|
||||
@ -73,7 +73,7 @@ class TestStore(unittest.TestCase):
|
||||
dum2 = self.s.get(Dummy, "7",create=True)
|
||||
self.reopen()
|
||||
dum2 = self.s.get(Dummy, "7")
|
||||
dum2.data = "A" * 100
|
||||
dum2.data = "A" * 1000
|
||||
dum2.count = 1337
|
||||
dum3 = self.s.get(Dummy, "9",create=True)
|
||||
dum3.count = 1338
|
||||
|
Loading…
Reference in New Issue
Block a user