summaryrefslogtreecommitdiff
path: root/golang/pkg/agent
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--golang/pkg/agent/contracting.go68
-rw-r--r--golang/pkg/agent/error.go15
-rw-r--r--golang/pkg/agent/run.go27
-rw-r--r--golang/pkg/agent/trading.go139
-rw-r--r--golang/pkg/agent/utils.go117
-rw-r--r--golang/pkg/agent/visit.go51
6 files changed, 368 insertions, 49 deletions
diff --git a/golang/pkg/agent/contracting.go b/golang/pkg/agent/contracting.go
index 0f07170..4e476a6 100644
--- a/golang/pkg/agent/contracting.go
+++ b/golang/pkg/agent/contracting.go
@@ -11,39 +11,79 @@ func (a *agent) autoContracting(ship *model.Ship) {
defer a.wg.Done()
contracts, err := a.client.MyContracts()
if err != nil {
- a.sendShipError(fmt.Errorf("failed to get my contracts: %w", err), ship)
+ a.channel <- fmt.Errorf("failed to get my contracts with ship %s: %w", ship.Symbol, err)
return
}
for _, contract := range contracts {
- if contract.Fullfilled {
+ if contract.Fulfilled {
continue
}
now := time.Now()
if now.Before(contract.Terms.Deadline) {
if err := a.runContract(&contract, ship); err != nil {
- a.sendShipError(fmt.Errorf("failed to run contracts: %w", err), ship)
+ a.channel <- fmt.Errorf("failed to run contract %s with ship %s: %w", contract.Id, ship.Symbol, err)
return
}
}
}
- a.sendShipError(fmt.Errorf("failed to run contracts: negotiating new contracts is not implemented yet"), ship)
- // TODO
- //for {
- // negotiate
- // runContract
- //}
+ a.channel <- fmt.Errorf("negotiating new contracts is not implemented yet")
+ for {
+ contract, err := a.client.NegotiateContract(ship)
+ if err != nil {
+ a.channel <- fmt.Errorf("failed to negotiate contract: %w", err)
+ return
+ }
+ if err := a.runContract(contract, ship); err != nil {
+ a.channel <- fmt.Errorf("failed to run contract %s: %w", contract.Id, err)
+ return
+ }
+ }
}
func (a *agent) runContract(contract *model.Contract, ship *model.Ship) error {
- if err := a.client.Accept(contract, a.db); err != nil {
- return fmt.Errorf("failed to run contract: %w", err)
+ if err := a.client.Accept(contract); err != nil {
+ return fmt.Errorf("failed to accept contract: %w", err)
}
//slog.Info("running contract", "contract", contract, "ship", ship.Symbol)
switch contract.Type {
- // TODO
- //case "PROCUREMENT":
+ case "PROCUREMENT":
+ if err := a.runProcurement(contract, ship); err != nil {
+ return fmt.Errorf("failed to run procurement: %w", err)
+ }
default:
- return fmt.Errorf("failed to run contract: handling contracts of type %s is not implemented yet", contract.Type)
+ return fmt.Errorf("handling contracts of type %s is not implemented yet", contract.Type)
}
return nil
}
+
+func (a *agent) runProcurement(contract *model.Contract, ship *model.Ship) error {
+ if contract.Fulfilled {
+ return nil
+ }
+ deliver := contract.Terms.Deliver[0]
+ // make sure we are not carrying useless stuff
+ if err := a.sellEverythingExcept(ship, deliver.TradeSymbol); err != nil {
+ return fmt.Errorf("failed to sell everything except %s for ship %s: %w", deliver.TradeSymbol, ship.Symbol, err)
+ }
+ // procure the desired goods
+ if ship.Cargo.Units < min(deliver.UnitsRequired-deliver.UnitsFulfilled, ship.Cargo.Capacity) {
+ if err := a.buyTradeGood(ship, deliver.TradeSymbol); err != nil {
+ return fmt.Errorf("failed to buy trade good %s with ship %s: %w", deliver.TradeSymbol, ship.Symbol, err)
+ }
+ }
+ // deliver the goods
+ if err := a.client.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 {
+ return fmt.Errorf("failed to deliver: %w", err)
+ }
+ deliver = contract.Terms.Deliver[0]
+ if deliver.UnitsRequired == deliver.UnitsFulfilled {
+ if err := a.client.Fulfill(contract); err != nil {
+ return fmt.Errorf("failed to fulfill: %w", err)
+ }
+ return nil
+ }
+ return a.runProcurement(contract, ship)
+}
diff --git a/golang/pkg/agent/error.go b/golang/pkg/agent/error.go
deleted file mode 100644
index 64e6b8d..0000000
--- a/golang/pkg/agent/error.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package agent
-
-import "git.adyxax.org/adyxax/spacetraders/golang/pkg/model"
-
-type shipError struct {
- err error
- ship *model.Ship
-}
-
-func (a *agent) sendShipError(err error, ship *model.Ship) {
- a.channel <- shipError{
- err: err,
- ship: ship,
- }
-}
diff --git a/golang/pkg/agent/run.go b/golang/pkg/agent/run.go
index 1d70be8..0591102 100644
--- a/golang/pkg/agent/run.go
+++ b/golang/pkg/agent/run.go
@@ -11,7 +11,7 @@ import (
)
type agent struct {
- channel chan shipError
+ channel chan error
client *api.Client
db *database.DB
getenv func(string) string
@@ -23,7 +23,8 @@ type State int
const (
start_running_contracts_with_the_command_ship = iota
- visit_all_shipyards
+ visit_all_shipyards_with_the_starting_probe
+ send_the_starting_probe_to_a_shipyard_that_sells_probes
)
func Run(
@@ -32,7 +33,7 @@ func Run(
getenv func(string) string,
) error {
agent := agent{
- channel: make(chan shipError),
+ channel: make(chan error),
client: client,
db: db,
getenv: getenv,
@@ -43,7 +44,7 @@ func Run(
}
if agent.ships, err = client.MyShips(); err != nil {
- return fmt.Errorf("failed to init the agent's ships: %w", err)
+ return fmt.Errorf("failed to get my ships: %w", err)
}
var state State = start_running_contracts_with_the_command_ship
agent.wg.Add(1)
@@ -55,14 +56,20 @@ func Run(
agent.wg.Add(1)
go agent.autoContracting(&agent.ships[0])
state++
- case visit_all_shipyards:
+ case visit_all_shipyards_with_the_starting_probe:
if err := agent.visitAllShipyards(&agent.ships[1]); err != nil {
- agent.sendShipError(fmt.Errorf("agent runner returned an error on state %d: %w", state, err), &agent.ships[1])
+ agent.channel <- fmt.Errorf("failed to visit all shipyards with ship %s: %w", agent.ships[1].Symbol, err)
+ return
+ }
+ state++
+ case send_the_starting_probe_to_a_shipyard_that_sells_probes:
+ if err := agent.sendShipToShipyardThatSells(&agent.ships[1], "SHIP_PROBE"); err != nil {
+ agent.channel <- fmt.Errorf("failed to send the starting probe to a shipyard that sells probes: %w", err)
+ return
}
state++
- return
default:
- agent.sendShipError(fmt.Errorf("agent runner reach an unknown state: %d", state), nil)
+ agent.channel <- fmt.Errorf("state not implemented: %d", state)
return
}
}
@@ -71,8 +78,8 @@ func Run(
errWg.Add(1)
go func() {
defer errWg.Done()
- for shipErr := range agent.channel {
- slog.Error("ship error", "err", shipErr.err, "ship", shipErr.ship.Symbol)
+ for err := range agent.channel {
+ slog.Error("agent run error", "err", err)
}
}()
agent.wg.Wait()
diff --git a/golang/pkg/agent/trading.go b/golang/pkg/agent/trading.go
new file mode 100644
index 0000000..b900f7a
--- /dev/null
+++ b/golang/pkg/agent/trading.go
@@ -0,0 +1,139 @@
+package agent
+
+import (
+ "fmt"
+ "slices"
+
+ "git.adyxax.org/adyxax/spacetraders/golang/pkg/model"
+)
+
+type TradeGoodNotFoundError struct{}
+
+func (err *TradeGoodNotFoundError) Error() string {
+ return "trade good not found"
+}
+
+func (a *agent) buyTradeGood(ship *model.Ship, tradeGoodToBuy string) error {
+ if ship.Cargo.Units == ship.Cargo.Capacity {
+ return nil
+ }
+ // list markets would sell our goods
+ markets, err := a.listMarketsInSystem(ship.Nav.SystemSymbol)
+ if err != nil {
+ 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.Exports {
+ if item.Symbol == tradeGoodToBuy {
+ return false
+ }
+ }
+ return true
+ })
+ if len(markets) == 0 {
+ return &TradeGoodNotFoundError{}
+ }
+ // 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)
+ }
+ waypoints := make([]model.Waypoint, 0)
+ for i := range markets {
+ waypoint, err := a.client.GetWaypoint(markets[i].Symbol)
+ if err != nil {
+ return fmt.Errorf("failed to get waypoint %s: %w", markets[i].Symbol, err)
+ }
+ waypoints = append(waypoints, *waypoint)
+ }
+ sortByDistanceFrom(waypoint, waypoints)
+ // Go there and refresh our market data
+ if err := a.client.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 != nil {
+ return fmt.Errorf("failed to get market %s: %w", waypoints[0].Symbol, err)
+ }
+ // Buy until full
+ for _, tradeGood := range market.TradeGoods {
+ if tradeGood.Type == "EXPORT" && tradeGood.Symbol == tradeGoodToBuy {
+ for ship.Cargo.Units < ship.Cargo.Capacity {
+ increment := min(ship.Cargo.Capacity-ship.Cargo.Units, tradeGood.TradeVolume)
+ if err := a.client.Purchase(ship, tradeGoodToBuy, increment); err != nil {
+ return fmt.Errorf("failed to purchase %d units of %s: %w", increment, tradeGoodToBuy, err)
+ }
+ }
+ break
+ }
+ }
+ return a.buyTradeGood(ship, tradeGoodToBuy)
+}
+
+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 {
+ return nil
+ }
+ // list markets would buy our goods
+ markets, err := a.listMarketsInSystem(ship.Nav.SystemSymbol)
+ if err != nil {
+ 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 {
+ return false
+ }
+ }
+ }
+ return true
+ })
+ if len(markets) == 0 {
+ return nil
+ }
+ // find the closest place to sell something 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)
+ }
+ waypoints := make([]model.Waypoint, 0)
+ for i := range markets {
+ waypoint, err := a.client.GetWaypoint(markets[i].Symbol)
+ if err != nil {
+ return fmt.Errorf("failed to get waypoint %s: %w", markets[i].Symbol, err)
+ }
+ waypoints = append(waypoints, *waypoint)
+ }
+ sortByDistanceFrom(waypoint, waypoints)
+ // Go there and refresh our market data
+ if err := a.client.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 != nil {
+ return fmt.Errorf("failed to get market %s: %w", waypoints[0].Symbol, err)
+ }
+ // sell everything we can
+ for _, cargoItem := range cargo {
+ units := cargoItem.Units
+ for _, tradeGood := range market.TradeGoods {
+ if tradeGood.Type == "IMPORT" && tradeGood.Symbol == cargoItem.Symbol {
+ for units > 0 {
+ increment := min(units, tradeGood.TradeVolume)
+ if err := a.client.Sell(ship, cargoItem.Symbol, increment); err != nil {
+ return fmt.Errorf("failed to sell %d units of %s: %w", units, cargoItem.Symbol, err)
+ }
+ units = units - increment
+ }
+ break
+ }
+ }
+ }
+ return a.sellEverythingExcept(ship, keep)
+}
diff --git a/golang/pkg/agent/utils.go b/golang/pkg/agent/utils.go
new file mode 100644
index 0000000..7aa9c7b
--- /dev/null
+++ b/golang/pkg/agent/utils.go
@@ -0,0 +1,117 @@
+package agent
+
+import (
+ "cmp"
+ "fmt"
+ "math"
+ "slices"
+
+ "git.adyxax.org/adyxax/spacetraders/golang/pkg/model"
+)
+
+func distance2(a *model.Waypoint, b *model.Waypoint) int {
+ x2 := a.X - b.X
+ y2 := a.Y - b.Y
+ return x2*x2 + y2*y2
+}
+
+func (a *agent) isThereAShipAtWaypoint(waypointSymbol string) bool {
+ for _, ship := range a.ships {
+ if ship.Nav.WaypointSymbol == waypointSymbol {
+ return true
+ }
+ }
+ return false
+}
+
+func (a *agent) listWaypointsInSystemWithTrait(systemSymbol string, trait string) ([]model.Waypoint, error) {
+ waypoints, err := a.client.ListWaypointsInSystem(systemSymbol)
+ if err != nil {
+ return nil, fmt.Errorf("failed to list waypoints: %w", err)
+ }
+ waypoints = slices.DeleteFunc(waypoints, func(waypoint model.Waypoint) bool {
+ for _, t := range waypoint.Traits {
+ if t.Symbol == trait {
+ return false
+ }
+ }
+ return true
+ })
+ return waypoints, nil
+}
+
+func (a *agent) listMarketsInSystem(systemSymbol string) ([]model.Market, error) {
+ waypoints, err := a.listWaypointsInSystemWithTrait(systemSymbol, "MARKETPLACE")
+ if err != nil {
+ return nil, fmt.Errorf("failed to list waypoints in system %s with trait MARKETPLACE: %w", systemSymbol, err)
+ }
+ var markets []model.Market
+ for i := range waypoints {
+ market, err := a.client.GetMarket(waypoints[i].Symbol)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get market %s: %w", waypoints[i].Symbol, err)
+ }
+ markets = append(markets, *market)
+ }
+ return markets, nil
+}
+
+func (a *agent) listShipyardsInSystem(systemSymbol string) ([]model.Shipyard, error) {
+ waypoints, err := a.listWaypointsInSystemWithTrait(systemSymbol, "SHIPYARD")
+ if err != nil {
+ return nil, fmt.Errorf("failed to list waypoints in system %s with trait SHIPYARD: %w", systemSymbol, err)
+ }
+ var shipyards []model.Shipyard
+ for i := range waypoints {
+ shipyard, err := a.client.GetShipyard(waypoints[i].Symbol)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get shipyard %s: %w", waypoints[i].Symbol, err)
+ }
+ shipyards = append(shipyards, *shipyard)
+ }
+ return shipyards, nil
+}
+
+func (a *agent) sendShipToShipyardThatSells(ship *model.Ship, shipType string) error {
+ shipyards, err := a.listShipyardsInSystem(ship.Nav.SystemSymbol)
+ if err != nil {
+ return fmt.Errorf("failed to list shipyards in system %s: %w", ship.Nav.SystemSymbol, err)
+ }
+ // filter out the shipyards that do not sell our ship
+ shipyards = slices.DeleteFunc(shipyards, func(shipyard model.Shipyard) bool {
+ for _, t := range shipyard.ShipTypes {
+ if t.Type == shipType {
+ return false
+ }
+ }
+ return true
+ })
+ // sort by cheapest
+ slices.SortFunc(shipyards, func(a, b model.Shipyard) int {
+ aPrice := math.MaxInt
+ for _, ship := range a.Ships {
+ if ship.Type == shipType {
+ aPrice = ship.PurchasePrice
+ break
+ }
+ }
+ bPrice := math.MaxInt
+ for _, ship := range b.Ships {
+ if ship.Type == shipType {
+ bPrice = ship.PurchasePrice
+ break
+ }
+ }
+ return cmp.Compare(aPrice, bPrice)
+ })
+ if err := a.client.Navigate(ship, shipyards[0].Symbol); err != nil {
+ return fmt.Errorf("failed to navigate to %s: %w", shipyards[0].Symbol, err)
+ }
+ return nil
+}
+
+func sortByDistanceFrom(origin *model.Waypoint, destinations []model.Waypoint) {
+ slices.SortFunc(destinations, func(a, b model.Waypoint) int {
+ return cmp.Compare(distance2(origin, &a), distance2(origin, &b))
+ })
+}
diff --git a/golang/pkg/agent/visit.go b/golang/pkg/agent/visit.go
index 019a6e5..e420186 100644
--- a/golang/pkg/agent/visit.go
+++ b/golang/pkg/agent/visit.go
@@ -2,23 +2,54 @@ package agent
import (
"fmt"
- "log/slog"
+ "slices"
"git.adyxax.org/adyxax/spacetraders/golang/pkg/model"
)
func (a *agent) visitAllShipyards(ship *model.Ship) error {
- system, err := a.client.GetSystem(ship.Nav.SystemSymbol, a.db)
+ shipyards, err := a.listShipyardsInSystem(ship.Nav.SystemSymbol)
if err != nil {
- return fmt.Errorf("failed to visit all shipyards: %w", err)
+ return fmt.Errorf("failed to list shipyards in system %s: %w", ship.Nav.SystemSymbol, err)
}
- waypoints, err := a.client.ListWaypointsInSystem(system, a.db)
+ shipyards = slices.DeleteFunc(shipyards, func(shipyard model.Shipyard) bool {
+ // filter out shipyards for which we already have ships prices
+ if shipyard.Ships != nil {
+ return true
+ }
+ // filter out shipyards for which a ship is either present or inbound
+ return a.isThereAShipAtWaypoint(shipyard.Symbol)
+ })
+ if len(shipyards) == 0 {
+ return nil
+ }
+ waypoint, err := a.client.GetWaypoint(ship.Nav.WaypointSymbol)
if err != nil {
- return fmt.Errorf("failed to visit all shipyards: %w", err)
+ return fmt.Errorf("failed to get nav waypoint %s: %w", ship.Nav.WaypointSymbol, err)
}
- //slog.Info("get system", "system", system.Waypoints, "err", err)
- //waypoint, err := a.client.GetWaypoint("X1-RR14-J88", a.db)
- slog.Info("get waypoint", "waypoint", waypoints[0])
-
- return fmt.Errorf("failed to visit all shipyards: not implemented yet")
+ waypoints := make([]model.Waypoint, 0)
+ for i := range shipyards {
+ waypoint, err := a.client.GetWaypoint(shipyards[i].Symbol)
+ if err != nil {
+ return fmt.Errorf("failed to get waypoint %s: %w", shipyards[i].Symbol, err)
+ }
+ waypoints = append(waypoints, *waypoint)
+ }
+ sortByDistanceFrom(waypoint, waypoints)
+ if err := a.client.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 {
+ 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 {
+ return fmt.Errorf("failed to get market %s: %w", waypoints[0].Symbol, err)
+ }
+ break
+ }
+ }
+ return a.visitAllShipyards(ship)
}