summaryrefslogtreecommitdiff
path: root/backend-go/internal/core/graph.go
diff options
context:
space:
mode:
Diffstat (limited to 'backend-go/internal/core/graph.go')
-rw-r--r--backend-go/internal/core/graph.go191
1 files changed, 191 insertions, 0 deletions
diff --git a/backend-go/internal/core/graph.go b/backend-go/internal/core/graph.go
new file mode 100644
index 0000000..f0c77a9
--- /dev/null
+++ b/backend-go/internal/core/graph.go
@@ -0,0 +1,191 @@
+package core
+
+import (
+ "math"
+)
+
+const (
+ AuInM = 149597870700.0
+)
+
+
+type Edge struct {
+ To int
+ Time float64
+ Risk float64
+ Weight float64
+}
+
+type Graph struct {
+ Stations map[int]*Station
+ SolarSystems map[int]*SolarSystem
+ Ship *Ship
+ Adj map[int][]Edge
+}
+
+func NewGraph(stations map[int]*Station, systems map[int]*SolarSystem, ship *Ship) *Graph {
+ g := &Graph{
+ Stations: stations,
+ SolarSystems: systems,
+ Ship: ship,
+ Adj: make(map[int][]Edge),
+ }
+ return g
+}
+
+func (g *Graph) AddEdge(u, v int) {
+ // Add only structural adjacency initially
+ g.Adj[u] = append(g.Adj[u], Edge{To: v})
+ g.Adj[v] = append(g.Adj[v], Edge{To: u})
+}
+
+// Build compiles all edges based on stations and stargates
+func (g *Graph) Build(connections [][2]int) {
+ // 1. Intra-system connections
+ stationsBySystem := make(map[int][]int)
+ for id, station := range g.Stations {
+ sysID := station.SolarSystemID
+ stationsBySystem[sysID] = append(stationsBySystem[sysID], id)
+ }
+
+ for sysID, sysStations := range stationsBySystem {
+ for _, stationID := range sysStations {
+ g.AddEdge(sysID, stationID)
+ }
+ // Direct station to station within same system
+ for i := 0; i < len(sysStations); i++ {
+ for j := i + 1; j < len(sysStations); j++ {
+ g.AddEdge(sysStations[i], sysStations[j])
+ }
+ }
+ }
+
+ // 2. Inter-system connections (Stargates)
+ for _, conn := range connections {
+ from, to := conn[0], conn[1]
+ if _, hasFrom := g.Stations[from]; hasFrom {
+ if _, hasTo := g.Stations[to]; hasTo {
+ g.AddEdge(from, to)
+ }
+ }
+ }
+
+ // 3. Compute time and risk
+ g.calculateWeights()
+}
+
+func (g *Graph) getPosition(nodeID int) Position {
+ if st, ok := g.Stations[nodeID]; ok {
+ return st.Position
+ }
+ return Position{X: 0, Y: 0, Z: 0}
+}
+
+func (g *Graph) calculateWeights() {
+ playerCost := g.Ship.PlayerCost
+ riskCost := g.Ship.RiskCost
+
+ // We used a simple slice append above, which might have duplicates if A->B and B->A were added twice.
+ // But AddEdge adds undirected, so we need to deduplicate or just overwrite weights carefully.
+ // For simplicity, we just rebuild Adj with weights.
+ newAdj := make(map[int][]Edge)
+
+ for u, neighbors := range g.Adj {
+ visited := make(map[int]bool)
+ var uniqueEdges []Edge
+
+ for _, edge := range neighbors {
+ v := edge.To
+ if visited[v] {
+ continue
+ }
+ visited[v] = true
+
+ time := g.calculateTime(u, v)
+ risk := g.calculateRisk(v)
+ weight := time*playerCost + riskCost*risk
+
+ uniqueEdges = append(uniqueEdges, Edge{
+ To: v,
+ Time: time,
+ Risk: risk,
+ Weight: weight,
+ })
+ }
+ newAdj[u] = uniqueEdges
+ }
+ g.Adj = newAdj
+}
+
+func (g *Graph) calculateTime(fromNode, toNode int) float64 {
+ stFrom, isStFrom := g.Stations[fromNode]
+ stTo, isStTo := g.Stations[toNode]
+
+ if isStFrom && isStTo && stFrom.SolarSystemID != stTo.SolarSystemID {
+ return 9.0 // Fixed stargate time
+ }
+
+ posFrom := g.getPosition(fromNode)
+ posTo := g.getPosition(toNode)
+
+ dx := posFrom.X - posTo.X
+ dy := posFrom.Y - posTo.Y
+ dz := posFrom.Z - posTo.Z
+ warpDist := math.Sqrt(dx*dx + dy*dy + dz*dz)
+
+ return g.calculateTimeInWarp(warpDist) + 2.0
+}
+
+func (g *Graph) calculateTimeInWarp(warpDist float64) float64 {
+ kAccel := g.Ship.MaxWarpSpeed
+ kDecel := math.Min(g.Ship.MaxWarpSpeed/3.0, 2.0)
+ warpDropout := math.Min(g.Ship.MaxSubwarpSpeed/2.0, 100.0)
+ maxMsWarpSpeed := g.Ship.MaxWarpSpeed * AuInM
+
+ accelDist := AuInM
+ decelDist := maxMsWarpSpeed / kDecel
+ minDist := accelDist + decelDist
+
+ cruiseTime := 0.0
+ if minDist > warpDist {
+ maxMsWarpSpeed = warpDist * kAccel * kDecel / (kAccel + kDecel)
+ } else {
+ cruiseTime = (warpDist - minDist) / maxMsWarpSpeed
+ }
+
+ accelTime := math.Log(maxMsWarpSpeed/kAccel) / kAccel
+ decelTime := math.Log(maxMsWarpSpeed/warpDropout) / kDecel
+
+ return cruiseTime + accelTime + decelTime
+}
+
+func (g *Graph) calculateRisk(toNode int) float64 {
+ security := 1.0
+ sysName := ""
+
+ if st, ok := g.Stations[toNode]; ok {
+ security = st.Security
+ sysName = st.SolarSystemName
+ } else if sys, ok := g.SolarSystems[toNode]; ok {
+ // Wait, some hubs don't have security field inside SolarSystem in the python code.
+ // Python fallbacks to 1.0 if not found, since SolarSystem doesn't actually store security directly in Loader.
+ // If we need it, we can fallback to 1.0
+ security = 1.0
+ sysName = sys.ItemName
+ }
+
+ risk := 0.00004 + (1.0-security)*0.0004
+ if security < 0.5 {
+ risk += 0.005
+ }
+ if security < 0.1 {
+ risk += 0.5
+ }
+
+ if g.Ship.GankersAreas != nil {
+ if penalty, exists := g.Ship.GankersAreas[sysName]; exists {
+ risk += penalty
+ }
+ }
+ return risk
+}