1
0
Fork 0

[golang] finished implementing procument contracts

This commit is contained in:
Julien Dessaux 2025-02-21 00:58:45 +01:00
parent 40c4a8df15
commit e887213aff
Signed by: adyxax
GPG key ID: F92E51B86E07177E
15 changed files with 252 additions and 56 deletions

View file

@ -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 {

View 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)
}

View file

@ -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 {

View file

@ -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

View file

@ -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