diff options
Diffstat (limited to '')
-rw-r--r-- | golang/pkg/agent/contracting.go | 68 | ||||
-rw-r--r-- | golang/pkg/agent/error.go | 15 | ||||
-rw-r--r-- | golang/pkg/agent/run.go | 27 | ||||
-rw-r--r-- | golang/pkg/agent/trading.go | 139 | ||||
-rw-r--r-- | golang/pkg/agent/utils.go | 117 | ||||
-rw-r--r-- | golang/pkg/agent/visit.go | 51 |
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) } |