diff options
| author | kboz <kutayboz@gmail.com> | 2026-09-27 22:36:34 +0200 |
|---|---|---|
| committer | kboz <kutayboz@gmail.com> | 2026-09-27 22:36:34 +0200 |
| commit | 1b59961ce2385252e3346ea6107b15a2c1feafe6 (patch) | |
| tree | fbe9e0cb968c47cff353e937e4dc835a64058c60 /backend-go/internal/core/graph.go | |
| parent | 29c4b57db9a34e0f815d93cac6a2cae543778a93 (diff) | |
| download | haul-1b59961ce2385252e3346ea6107b15a2c1feafe6.tar.gz | |
feat(backend): port route-planning backend to Go
Add a Go implementation mirroring the Python pipeline (loader, graph,
pathfinder, arbitrage, market, route, manager) under internal/core, with
ESI client/API integration and setup endpoints. Reuses backend/src as
the working directory so existing data/cache paths keep working.
Diffstat (limited to 'backend-go/internal/core/graph.go')
| -rw-r--r-- | backend-go/internal/core/graph.go | 191 |
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 +} |
