[golang] finished implementing procument contracts
This commit is contained in:
parent
40c4a8df15
commit
e887213aff
15 changed files with 252 additions and 56 deletions
|
@ -72,7 +72,7 @@ func (a *agent) runProcurement(contract *model.Contract, ship *model.Ship) error
|
|||
}
|
||||
}
|
||||
// deliver the goods
|
||||
if err := a.client.Navigate(ship, deliver.DestinationSymbol); err != nil {
|
||||
if err := a.navigate(ship, deliver.DestinationSymbol); err != nil {
|
||||
return fmt.Errorf("failed to navigate to %s: %w", deliver.DestinationSymbol, err)
|
||||
}
|
||||
if err := a.client.Deliver(contract, ship); err != nil {
|
||||
|
|
167
golang/pkg/agent/navigate.go
Normal file
167
golang/pkg/agent/navigate.go
Normal file
|
@ -0,0 +1,167 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math"
|
||||
"slices"
|
||||
|
||||
"git.adyxax.org/adyxax/spacetraders/golang/pkg/api"
|
||||
"git.adyxax.org/adyxax/spacetraders/golang/pkg/model"
|
||||
)
|
||||
|
||||
func (a *agent) navigate(ship *model.Ship, waypointSymbol string) error {
|
||||
if ship.Nav.WaypointSymbol == waypointSymbol {
|
||||
return nil
|
||||
}
|
||||
if ship.Fuel.Capacity == 0 {
|
||||
if err := a.client.Navigate(ship, waypointSymbol); err != nil {
|
||||
return fmt.Errorf("failed to navigate to %s: %w", waypointSymbol, err)
|
||||
}
|
||||
}
|
||||
path, cost, err := a.shortestPath(ship.Nav.WaypointSymbol, waypointSymbol, ship.Fuel.Capacity)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compute shortest path: %w", err)
|
||||
}
|
||||
slog.Debug("shortest path", "origin", ship.Nav.WaypointSymbol, "destination", waypointSymbol, "path", path, "cost", cost)
|
||||
for _, next := range path {
|
||||
if err := a.client.Refuel(ship); err != nil {
|
||||
return fmt.Errorf("failed to refuel: %w", err)
|
||||
}
|
||||
if err := a.client.Navigate(ship, next); err != nil {
|
||||
return fmt.Errorf("failed to navigate to %s: %w", next, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *agent) shortestPath(origin string, destination string, fuelCapacity int) ([]string, int, error) {
|
||||
if fuelCapacity == 0 { // Probes
|
||||
fuelCapacity = math.MaxInt
|
||||
}
|
||||
systemSymbol := api.WaypointSymbolToSystemSymbol(origin)
|
||||
waypoints, err := a.client.ListWaypointsInSystem(systemSymbol)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to list waypoints in system %s: %w", systemSymbol, err)
|
||||
}
|
||||
backtrace := make(map[string]string) // backtrace map with the shortest path from one point to another
|
||||
costs := make(map[string]int) // cost to reach each waypoint from the origin. cost = distance + 1
|
||||
costs[origin] = 0
|
||||
unvisited := make(map[string]*model.Waypoint)
|
||||
for i := range waypoints {
|
||||
symbol := waypoints[i].Symbol
|
||||
costs[symbol] = math.MaxInt
|
||||
unvisited[waypoints[i].Symbol] = &waypoints[i]
|
||||
// We need to know which waypoints allow refueling
|
||||
waypoints[i].Traits = slices.DeleteFunc(waypoints[i].Traits, func(trait model.Common) bool {
|
||||
if trait.Symbol == "MARKETPLACE" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if len(waypoints[i].Traits) > 0 {
|
||||
market, err := a.client.GetMarket(symbol, a.ships)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to get market %s: %w", symbol, err)
|
||||
}
|
||||
market.Exchange = slices.DeleteFunc(market.Exchange, func(item model.Common) bool {
|
||||
if item.Symbol == "FUEL" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
market.Exports = slices.DeleteFunc(market.Exports, func(item model.Common) bool {
|
||||
if item.Symbol == "FUEL" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if len(market.Exchange) == 0 && len(market.Exports) == 0 {
|
||||
waypoints[i].Traits = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
costs[origin] = 0
|
||||
pq := make(PriorityQueue, 1)
|
||||
pq[0] = &Node{
|
||||
waypointSymbol: origin,
|
||||
}
|
||||
heap.Init(&pq)
|
||||
outer:
|
||||
for pq.Len() > 0 {
|
||||
node := heap.Pop(&pq).(*Node)
|
||||
waypoint, ok := unvisited[node.waypointSymbol]
|
||||
if !ok { // already visited
|
||||
continue
|
||||
}
|
||||
delete(unvisited, node.waypointSymbol)
|
||||
for _, candidate := range unvisited {
|
||||
fuel := int(math.Floor(math.Sqrt(float64(distance2(waypoint, candidate))))) + 1
|
||||
if fuel > fuelCapacity {
|
||||
continue
|
||||
}
|
||||
cost := node.cost + fuel
|
||||
if cost < costs[candidate.Symbol] {
|
||||
backtrace[candidate.Symbol] = node.waypointSymbol
|
||||
costs[candidate.Symbol] = cost
|
||||
if candidate.Symbol == destination {
|
||||
break outer
|
||||
}
|
||||
if len(candidate.Traits) > 0 {
|
||||
heap.Push(&pq, &Node{
|
||||
cost: cost,
|
||||
waypointSymbol: candidate.Symbol,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
path := []string{destination}
|
||||
step, ok := backtrace[destination]
|
||||
if !ok {
|
||||
slog.Debug("shortest path failure", "backtraces", backtrace, "costs", costs)
|
||||
return nil, 0, fmt.Errorf("no path exists between origin and destination with the given fuel capacity")
|
||||
}
|
||||
for step != origin {
|
||||
path = append([]string{step}, path...)
|
||||
step = backtrace[step]
|
||||
}
|
||||
return path, costs[destination], nil
|
||||
}
|
||||
|
||||
// Priority queue implementation with container/heap
|
||||
type Node struct {
|
||||
cost int
|
||||
waypointSymbol string
|
||||
index int // needed by the heap.Interface methods
|
||||
}
|
||||
|
||||
type PriorityQueue []*Node
|
||||
|
||||
func (pq PriorityQueue) Len() int { return len(pq) }
|
||||
func (pq PriorityQueue) Less(i, j int) bool { return pq[i].cost < pq[j].cost }
|
||||
func (pq PriorityQueue) Swap(i, j int) {
|
||||
pq[i], pq[j] = pq[j], pq[i]
|
||||
pq[i].index = i
|
||||
pq[j].index = j
|
||||
}
|
||||
func (pq *PriorityQueue) Push(x any) {
|
||||
n := len(*pq)
|
||||
item := x.(*Node)
|
||||
item.index = n
|
||||
*pq = append(*pq, item)
|
||||
}
|
||||
func (pq *PriorityQueue) Pop() any {
|
||||
old := *pq
|
||||
n := len(old)
|
||||
item := old[n-1]
|
||||
old[n-1] = nil // don't stop the GC from reclaiming the item eventually
|
||||
item.index = -1 // for safety
|
||||
*pq = old[0 : n-1]
|
||||
return item
|
||||
}
|
||||
func (pq *PriorityQueue) Update(n *Node, cost int) {
|
||||
n.cost = cost
|
||||
heap.Fix(pq, n.index)
|
||||
}
|
|
@ -36,7 +36,7 @@ func (a *agent) buyTradeGood(ship *model.Ship, tradeGoodToBuy string) error {
|
|||
// find the closest place to buy TODO
|
||||
waypoint, err := a.client.GetWaypoint(ship.Nav.WaypointSymbol)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get nav waypoint %s: %w", ship.Nav.WaypointSymbol, err)
|
||||
return fmt.Errorf("failed to get waypoint %s: %w", ship.Nav.WaypointSymbol, err)
|
||||
}
|
||||
waypoints := make([]model.Waypoint, 0)
|
||||
for i := range markets {
|
||||
|
@ -48,10 +48,13 @@ func (a *agent) buyTradeGood(ship *model.Ship, tradeGoodToBuy string) error {
|
|||
}
|
||||
sortByDistanceFrom(waypoint, waypoints)
|
||||
// Go there and refresh our market data
|
||||
if err := a.client.Navigate(ship, waypoints[0].Symbol); err != nil {
|
||||
if err := a.navigate(ship, waypoints[0].Symbol); err != nil {
|
||||
return fmt.Errorf("failed to navigate to %s: %w", waypoints[0].Symbol, err)
|
||||
}
|
||||
market, err := a.client.GetMarket(waypoints[0].Symbol)
|
||||
if err := a.client.Dock(ship); err != nil {
|
||||
return fmt.Errorf("failed to dock: %w", err)
|
||||
}
|
||||
market, err := a.client.GetMarket(waypoints[0].Symbol, a.ships)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get market %s: %w", waypoints[0].Symbol, err)
|
||||
}
|
||||
|
@ -72,11 +75,13 @@ func (a *agent) buyTradeGood(ship *model.Ship, tradeGoodToBuy string) error {
|
|||
|
||||
func (a *agent) sellEverythingExcept(ship *model.Ship, keep string) error {
|
||||
// First lets see what we need to sell
|
||||
cargo := ship.Cargo.Inventory
|
||||
cargo = slices.DeleteFunc(cargo, func(inventory model.Inventory) bool {
|
||||
return inventory.Symbol == keep
|
||||
})
|
||||
if len(cargo) == 0 {
|
||||
var inventoryItems []model.InventoryItem
|
||||
for _, item := range ship.Cargo.Inventory {
|
||||
if item.Symbol != keep {
|
||||
inventoryItems = append(inventoryItems, item)
|
||||
}
|
||||
}
|
||||
if len(inventoryItems) == 0 {
|
||||
return nil
|
||||
}
|
||||
// list markets would buy our goods
|
||||
|
@ -85,9 +90,9 @@ func (a *agent) sellEverythingExcept(ship *model.Ship, keep string) error {
|
|||
return fmt.Errorf("failed to list markets in system %s: %w", ship.Nav.SystemSymbol, err)
|
||||
}
|
||||
markets = slices.DeleteFunc(markets, func(market model.Market) bool {
|
||||
for _, item := range market.Imports {
|
||||
for _, cargoItem := range cargo {
|
||||
if item.Symbol == cargoItem.Symbol {
|
||||
for _, marketItem := range market.Imports {
|
||||
for _, inventoryItem := range inventoryItems {
|
||||
if marketItem.Symbol == inventoryItem.Symbol {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -112,15 +117,18 @@ func (a *agent) sellEverythingExcept(ship *model.Ship, keep string) error {
|
|||
}
|
||||
sortByDistanceFrom(waypoint, waypoints)
|
||||
// Go there and refresh our market data
|
||||
if err := a.client.Navigate(ship, waypoints[0].Symbol); err != nil {
|
||||
if err := a.navigate(ship, waypoints[0].Symbol); err != nil {
|
||||
return fmt.Errorf("failed to navigate to %s: %w", waypoints[0].Symbol, err)
|
||||
}
|
||||
market, err := a.client.GetMarket(waypoints[0].Symbol)
|
||||
if err := a.client.Dock(ship); err != nil {
|
||||
return fmt.Errorf("failed to dock: %w", err)
|
||||
}
|
||||
market, err := a.client.GetMarket(waypoints[0].Symbol, a.ships)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get market %s: %w", waypoints[0].Symbol, err)
|
||||
}
|
||||
// sell everything we can
|
||||
for _, cargoItem := range cargo {
|
||||
for _, cargoItem := range inventoryItems {
|
||||
units := cargoItem.Units
|
||||
for _, tradeGood := range market.TradeGoods {
|
||||
if tradeGood.Type == "IMPORT" && tradeGood.Symbol == cargoItem.Symbol {
|
||||
|
|
|
@ -47,7 +47,7 @@ func (a *agent) listMarketsInSystem(systemSymbol string) ([]model.Market, error)
|
|||
}
|
||||
var markets []model.Market
|
||||
for i := range waypoints {
|
||||
market, err := a.client.GetMarket(waypoints[i].Symbol)
|
||||
market, err := a.client.GetMarket(waypoints[i].Symbol, a.ships)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get market %s: %w", waypoints[i].Symbol, err)
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ func (a *agent) listShipyardsInSystem(systemSymbol string) ([]model.Shipyard, er
|
|||
}
|
||||
var shipyards []model.Shipyard
|
||||
for i := range waypoints {
|
||||
shipyard, err := a.client.GetShipyard(waypoints[i].Symbol)
|
||||
shipyard, err := a.client.GetShipyard(waypoints[i].Symbol, a.ships)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get shipyard %s: %w", waypoints[i].Symbol, err)
|
||||
}
|
||||
|
@ -86,6 +86,9 @@ func (a *agent) sendShipToShipyardThatSells(ship *model.Ship, shipType string) e
|
|||
}
|
||||
return true
|
||||
})
|
||||
if len(shipyards) == 0 {
|
||||
return fmt.Errorf("no shipyards sells that ship type")
|
||||
}
|
||||
// sort by cheapest
|
||||
slices.SortFunc(shipyards, func(a, b model.Shipyard) int {
|
||||
aPrice := math.MaxInt
|
||||
|
@ -104,7 +107,7 @@ func (a *agent) sendShipToShipyardThatSells(ship *model.Ship, shipType string) e
|
|||
}
|
||||
return cmp.Compare(aPrice, bPrice)
|
||||
})
|
||||
if err := a.client.Navigate(ship, shipyards[0].Symbol); err != nil {
|
||||
if err := a.navigate(ship, shipyards[0].Symbol); err != nil {
|
||||
return fmt.Errorf("failed to navigate to %s: %w", shipyards[0].Symbol, err)
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -14,7 +14,7 @@ func (a *agent) visitAllShipyards(ship *model.Ship) error {
|
|||
}
|
||||
shipyards = slices.DeleteFunc(shipyards, func(shipyard model.Shipyard) bool {
|
||||
// filter out shipyards for which we already have ships prices
|
||||
if shipyard.Ships != nil {
|
||||
if len(shipyard.Ships) > 0 {
|
||||
return true
|
||||
}
|
||||
// filter out shipyards for which a ship is either present or inbound
|
||||
|
@ -36,16 +36,16 @@ func (a *agent) visitAllShipyards(ship *model.Ship) error {
|
|||
waypoints = append(waypoints, *waypoint)
|
||||
}
|
||||
sortByDistanceFrom(waypoint, waypoints)
|
||||
if err := a.client.Navigate(ship, waypoints[0].Symbol); err != nil {
|
||||
if err := a.navigate(ship, waypoints[0].Symbol); err != nil {
|
||||
return fmt.Errorf("failed to navigate to %s: %w", waypoints[0].Symbol, err)
|
||||
}
|
||||
if _, err := a.client.GetShipyard(waypoints[0].Symbol); err != nil {
|
||||
if _, err := a.client.GetShipyard(waypoints[0].Symbol, a.ships); err != nil {
|
||||
return fmt.Errorf("failed to get shipyard %s: %w", waypoints[0].Symbol, err)
|
||||
}
|
||||
// If this waypoint is also a marketplace, get its data
|
||||
for _, trait := range waypoints[0].Traits {
|
||||
if trait.Symbol == "MARKETPLACE" {
|
||||
if _, err := a.client.GetMarket(waypoints[0].Symbol); err != nil {
|
||||
if _, err := a.client.GetMarket(waypoints[0].Symbol, a.ships); err != nil {
|
||||
return fmt.Errorf("failed to get market %s: %w", waypoints[0].Symbol, err)
|
||||
}
|
||||
break
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue