Skip to content

Commit

Permalink
extrasolar behaviour complete!
Browse files Browse the repository at this point in the history
  • Loading branch information
Ctri-The-Third committed Aug 12, 2023
1 parent 910f751 commit bc4f9f8
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 6 deletions.
172 changes: 167 additions & 5 deletions behaviours/explore_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@
sys.path.append(".")
from behaviours.generic_behaviour import Behaviour
from straders_sdk import SpaceTraders
from straders_sdk.ship import Ship
from straders_sdk.models import Waypoint, System
import math
import logging
from straders_sdk.utils import try_execute_select, set_logging
import networkx
import heapq
from datetime import datetime
import time

BEHAVIOUR_NAME = "EXPLORE_ONE_SYSTEM"

Expand All @@ -18,19 +24,34 @@ def __init__(
behaviour_params: dict = ...,
config_file_name="user.json",
) -> None:
self.graph = None
super().__init__(agent_name, ship_name, behaviour_params, config_file_name)

def run(self):
self.graph = self.populate_graph()
ship = self.ship
st = self.st
agent = st.view_my_self()
# check all markets in the system
tar_sys = self.behaviour_params["target_system"] or ship.nav.system_symbol

st.logging_client.log_beginning(BEHAVIOUR_NAME, ship.name, agent.credits)

if ship.nav.system_symbol != tar_sys:
self.ship_extrasolar(tar_sys)
time.sleep(max(ship.seconds_until_cooldown, ship.nav.travel_time_remaining))

tar_sys_sql = """SELECT w1.system_symbol, j.x, j.y, last_updated, jump_gate_waypoint
FROM public.mkt_shpyrds_systems_last_updated_jumpgates j
JOIN waypoints w1 on j.symbol = w1.symbol
order by last_updated, random()"""
target = try_execute_select(self.connection, tar_sys_sql, ())[0]
self.logger.debug("Random destination selected: target %s", target[0])
d_sys = System(target[0], "", "", target[1], target[2], [])

arrived = True
if ship.nav.system_symbol != d_sys:
arrived = self.ship_extrasolar(ship, d_sys)
if not arrived:
self.logger.error("Couldn't jump! Unknown reason.")
return

self.scan_local_system()
# travel to target system
# scan target system
Expand Down Expand Up @@ -76,6 +97,73 @@ def scan_local_system(self):
if waypoint.type == "JUMP_GATE":
jump_gate = st.system_jumpgate(waypoint, True)

def populate_graph(self):
graph = networkx.Graph()
sql = """select s.symbol, s.sector_symbol, s.type, s.x, s.y from jump_gates jg
join waypoints w on jg.waypoint_symbol = w.symbol
join systems s on w.system_symbol = s.symbol"""

# the graph should be populated with Systems and Connections.
# but note that the connections themselves need to by systems.
# sql = """SELECT symbol, sector_symbol, type, x, y FROM systems"""
# for row in rows:
# syst = System(row[0], row[1], row[2], row[3], row[4], [])

results = try_execute_select(self.connection, sql, ())
if results:
nodes = {
row[0]: System(row[0], row[1], row[2], row[3], row[4], [])
for row in results
}
graph.add_nodes_from(nodes)

else:
return graph
sql = """select w1.system_symbol, destination_waypoint from jumpgate_connections jc
join waypoints w1 on jc.source_waypoint = w1.symbol
"""
results = try_execute_select(self.connection, sql, ())
connections = []
for row in results:
try:
connections.append((nodes[row[0]], nodes[row[1]]))
except KeyError:
pass
# this happens when the gate we're connected to is not one that we've scanned yet.
if results:
graph.add_edges_from(connections)
return graph

def ship_extrasolar(
self, ship: "Ship", destination_system: System, route: list = None
):
st = self.st
o_sys = st.systems_view_one(ship.nav.system_symbol)
route = route or astar(self.graph, o_sys, destination_system)
if not route:
self.logger.error(f"Unable to jump to {o_sys.symbol} - no route found")
return None

if ship.nav.status == "DOCKED":
st.ship_orbit(ship)
if ship.nav.travel_time_remaining > 0:
time.sleep(ship.nav.travel_time_remaining)
current_wp = st.waypoints_view_one(
ship.nav.system_symbol, ship.nav.waypoint_symbol
)
if current_wp.type != "JUMP_GATE":
jg_wp = st.find_waypoints_by_type_one(ship.nav.system_symbol, "JUMP_GATE")
resp = self.ship_intrasolar(jg_wp.symbol)
if not resp:
self.logger.warn("Unable to jump - not at warp gate.")
return False
route.pop()
for next_sys in route:
next_sys: System
st.ship_jump(ship, next_sys.symbol)
time.sleep(ship.seconds_until_cooldown)
# Then, hit it.


def nearest_neighbour(waypoints: list[Waypoint], start: Waypoint):
path = []
Expand Down Expand Up @@ -107,9 +195,83 @@ def nearest_neighbour_systems(systems: list[System], start: System):
return path


def astar(graph: networkx.Graph, start: Waypoint, goal: Waypoint):
if start not in graph.nodes:
return None
if goal not in graph.nodes:
return None
# freely admit used chatgpt to get started here.

# Priority queue to store nodes based on f-score (priority = f-score)
# C'tri note - I think this will be 1 for all edges?
# Update - no, F-score is the distance between the specific node and the start
open_set = []
heapq.heappush(open_set, (0, start))

# note to self - this dictionary is setting all g_scores to infinity- they have not been calculated yet.
g_score = {node: float("inf") for node in graph.nodes}
g_score[start] = 0

# Data structure to store the f-score (g-score + heuristic) for each node
f_score = {node: float("inf") for node in graph.nodes}
f_score[start] = h(
start, goal
) # heuristic function - standard straight-line X/Y distance

# this is how we reconstruct our route back.Z came from Y. Y came from X. X came from start.
came_from = {}
while open_set:
# Get the node with the lowest estimated total cost from the priority queue
current = heapq.heappop(open_set)[1]
# print(f"NEW NODE: {f_score[current]}")
if current == goal:
# first list item = destination
total_path = [current]
while current in came_from:
# +1 list item = -1 from destination
current = came_from[current]
total_path.append(current)
# reverse so frist list_item = source.
logging.debug("Completed A* - total jumps = %s", len(total_path))
return list(reversed(total_path))
# Reconstruct the shortest path
# the path will have been filled with every other step we've taken so far.

for neighbour in graph.neighbors(current):
# yes, g_score is the total number of jumps to get to this node.
tentative_global_score = g_score[current] + 1

if tentative_global_score < g_score[neighbour]:
# what if the neighbour hasn't been g_scored yet?
# ah we inf'd them, so unexplored is always higher
# so if we're in here, neighbour is the one behind us.

came_from[neighbour] = current
g_score[neighbour] = tentative_global_score
f_score[neighbour] = tentative_global_score + h(neighbour, goal)
# print(f" checked: {f_score[neighbour]}")
# this f_score part I don't quite get - we're storing number of jumps + remaining distance
# I can't quite visualise but but if we're popping the lowest f_score in the heap - then we get the one that's closest?
# which is good because if we had variable jump costs, that would be factored into the g_score - for example time.
# actually that's a great point, time is the bottleneck we want to cut down on, not speed.
# this function isn't built with that in mind tho so I'm not gonna bother _just yet_

# add this neighbour to the priority queue - the one with the lowest remaining distance will be the next one popped.
heapq.heappush(open_set, (f_score[neighbour], neighbour))

return None


def h(start: System, goal: System):
return ((start.x - goal.x) ** 2 + (start.y - goal.y) ** 2) ** 0.5


def calculate_distance(src: Waypoint, dest: Waypoint):
return math.sqrt((src.x - dest.x) ** 2 + (src.y - dest.y) ** 2)


if __name__ == "__main__":
ExploreSystem("CTRI-TEST-ALDV", "CTRI-TEST-ALDV-1").run()
set_logging(level=logging.DEBUG)
agent_symbol = "CTRI-LWK5-"
ship_suffix = "1"
ExploreSystem(agent_symbol, f"{agent_symbol}-{ship_suffix}").run()
6 changes: 6 additions & 0 deletions behaviours/generic_behaviour.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def __init__(
db_pass=db_pass,
current_agent_symbol=agent_name,
)
self.connection = self.st.db_client.connection
self.ship = self.st.ships_view_one(ship_name, force=True)
self.st.ship_cooldown(self.ship)
# get the cooldown info as well from the DB
Expand All @@ -71,6 +72,11 @@ def ship_intrasolar(self, target_wp_symbol: "str", sleep_till_done=True):
sleep_until_ready(self.ship)
ship.nav.status = "IN_ORBIT"
ship.nav.waypoint_symbol = target_wp_symbol
self.logger.debug(
"moved to %s, time to destination %s",
ship.name,
ship.nav.travel_time_remaining,
)
return resp

def extract_till_full(self, cargo_to_target: list = None):
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pytest
requests
psycopg2-binary
networkx
markdown
19 changes: 18 additions & 1 deletion straders_sdk/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,14 @@ def has_jump_gate(self) -> bool:
def __str__(self):
return self.symbol

def __hash__(self) -> int:
return self.symbol.__hash__()

def __eq__(self, o: object) -> bool:
if isinstance(o, Waypoint):
return self.symbol == o.symbol
return False


class JumpGate:
def __init__(
Expand Down Expand Up @@ -395,6 +403,14 @@ def from_json(cls, json_data: dict):
wayps,
)

def __hash__(self) -> int:
return self.symbol.__hash__()

def __eq__(self, o: object) -> bool:
if isinstance(o, System):
return self.symbol == o.symbol
return False


@dataclass
class JumpGateConnection(SymbolClass):
Expand Down Expand Up @@ -468,7 +484,8 @@ class Shipyard:
def from_json(cls, json_data: dict):
types = [type_["type"] for type_ in json_data["shipTypes"]]
ships = {
ship["type"]: ShipyardShip(ship) for ship in json_data.get("ships", [])
ship["type"]: ShipyardShip.from_json(ship)
for ship in json_data.get("ships", [])
}

return cls(json_data["symbol"], types, ships)
Expand Down

0 comments on commit bc4f9f8

Please sign in to comment.