summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulien Dessaux2025-02-14 00:14:15 +0100
committerJulien Dessaux2025-02-14 00:14:15 +0100
commitd97985a694b218713ddf63ed684b6a509f931f3b (patch)
tree84609f4e242419bf89301e0ead4927f450d7bfe5
parent[golang] Bootstrap contracting and refactor the agent code (diff)
downloadspacetraders-d97985a694b218713ddf63ed684b6a509f931f3b.tar.gz
spacetraders-d97985a694b218713ddf63ed684b6a509f931f3b.tar.bz2
spacetraders-d97985a694b218713ddf63ed684b6a509f931f3b.zip
[golang] implement automation loop and add contract accepting
Diffstat (limited to '')
-rw-r--r--golang/pkg/agent/contracting.go47
-rw-r--r--golang/pkg/agent/error.go15
-rw-r--r--golang/pkg/agent/init.go60
-rw-r--r--golang/pkg/agent/run.go108
-rw-r--r--golang/pkg/api/contracts.go23
-rw-r--r--golang/pkg/api/ships.go12
-rw-r--r--golang/pkg/database/agents.go2
-rw-r--r--golang/pkg/database/db.go16
-rw-r--r--golang/pkg/database/tokens.go2
9 files changed, 222 insertions, 63 deletions
diff --git a/golang/pkg/agent/contracting.go b/golang/pkg/agent/contracting.go
new file mode 100644
index 0000000..44d7753
--- /dev/null
+++ b/golang/pkg/agent/contracting.go
@@ -0,0 +1,47 @@
+package agent
+
+import (
+ "fmt"
+ "time"
+
+ "git.adyxax.org/adyxax/spacetraders/golang/pkg/model"
+)
+
+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)
+ return
+ }
+ for _, contract := range contracts {
+ if contract.Fullfilled {
+ 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)
+ return
+ }
+ }
+ }
+ // TODO
+ //for {
+ // negotiate
+ // runContract
+ //}
+}
+
+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)
+ }
+ switch contract.Type {
+ // TODO
+ //case "PROCUREMENT":
+ default:
+ return fmt.Errorf("failed to run contract: handling contracts of type %s is not implemented yet", contract.Type)
+ }
+ return nil
+}
diff --git a/golang/pkg/agent/error.go b/golang/pkg/agent/error.go
new file mode 100644
index 0000000..64e6b8d
--- /dev/null
+++ b/golang/pkg/agent/error.go
@@ -0,0 +1,15 @@
+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/init.go b/golang/pkg/agent/init.go
new file mode 100644
index 0000000..30b18e9
--- /dev/null
+++ b/golang/pkg/agent/init.go
@@ -0,0 +1,60 @@
+package agent
+
+import (
+ "errors"
+ "fmt"
+ "log/slog"
+
+ "git.adyxax.org/adyxax/spacetraders/golang/pkg/api"
+ "git.adyxax.org/adyxax/spacetraders/golang/pkg/model"
+)
+
+func (a *agent) init() error {
+ token, err := a.db.GetToken()
+ if err == nil && token != "" {
+ a.client.SetToken(token)
+ var agent *model.Agent
+ agent, err = a.client.MyAgent() // we need err to carry over outside this block
+ if err == nil {
+ slog.Info("agent", "/my/agent", agent)
+ } else {
+ slog.Info("failed to get agent, handling server reset", "err", err)
+ a.db.Reset()
+ }
+ }
+ if err != nil || token == "" {
+ // No token or invalid token (since we carry over the error of MyAgent() in the previous if), we need to register
+ accountToken := a.getenv("SPACETRADERS_ACCOUNT_TOKEN")
+ if accountToken == "" {
+ return fmt.Errorf("the SPACETRADERS_ACCOUNT_TOKEN environment variable is not set")
+ }
+ a.client.SetToken(accountToken)
+ agent := a.getenv("SPACETRADERS_AGENT")
+ if agent == "" {
+ return fmt.Errorf("the SPACETRADERS_AGENT environment variable is not set")
+ }
+ faction := a.getenv("SPACETRADERS_FACTION")
+ if faction == "" {
+ return fmt.Errorf("the SPACETRADERS_FACTION environment variable is not set")
+ }
+ register, err := a.client.Register(faction, agent)
+ if err != nil {
+ apiError := &api.APIError{}
+ if errors.As(err, &apiError) {
+ switch apiError.Code {
+ case 4111: // Agent symbol has already been claimed
+ return fmt.Errorf("failed to register and failed to get a token from the database: someone stole our agent's callsign: %w", err)
+ default:
+ return fmt.Errorf("failed to register: %w", err)
+ }
+ } else {
+ return fmt.Errorf("failed to register with an invalid apiError: %w", err)
+ }
+ }
+ a.client.SetToken(register.Token)
+ if err := a.db.SaveToken(register.Token); err != nil {
+ return fmt.Errorf("failed to save token %s after a successful registration: %w", register.Token, err)
+ }
+ }
+ return nil
+}
diff --git a/golang/pkg/agent/run.go b/golang/pkg/agent/run.go
index db4c614..bc5254e 100644
--- a/golang/pkg/agent/run.go
+++ b/golang/pkg/agent/run.go
@@ -1,78 +1,76 @@
package agent
import (
- "errors"
"fmt"
"log/slog"
+ "sync"
"git.adyxax.org/adyxax/spacetraders/golang/pkg/api"
"git.adyxax.org/adyxax/spacetraders/golang/pkg/database"
+ "git.adyxax.org/adyxax/spacetraders/golang/pkg/model"
+)
+
+type agent struct {
+ channel chan shipError
+ client *api.Client
+ db *database.DB
+ getenv func(string) string
+ ships []model.Ship
+ wg sync.WaitGroup
+}
+
+type State int
+
+const (
+ start_running_contracts_with_the_command_ship = iota
)
func Run(
- apiClient *api.Client,
+ client *api.Client,
db *database.DB,
getenv func(string) string,
) error {
- accountToken := getenv("SPACETRADERS_ACCOUNT_TOKEN")
- if accountToken == "" {
- return fmt.Errorf("the SPACETRADERS_ACCOUNT_TOKEN environment variable is not set")
+ agent := agent{
+ channel: make(chan shipError),
+ client: client,
+ db: db,
+ getenv: getenv,
}
- agent := getenv("SPACETRADERS_AGENT")
- if agent == "" {
- return fmt.Errorf("the SPACETRADERS_AGENT environment variable is not set")
+ err := agent.init()
+ if err != nil {
+ return fmt.Errorf("failed to init agent: %w", err)
}
- faction := getenv("SPACETRADERS_FACTION")
- if faction == "" {
- return fmt.Errorf("the SPACETRADERS_FACTION environment variable is not set")
+
+ if agent.ships, err = client.MyShips(); err != nil {
+ return fmt.Errorf("failed to init the agent's ships: %w", err)
}
- // ----- Get token or register ---------------------------------------------
- apiClient.SetToken(accountToken)
- register, err := apiClient.Register(faction, agent)
- if err != nil {
- apiError := &api.APIError{}
- if errors.As(err, &apiError) {
- switch apiError.Code {
- case 4111: // Agent symbol has already been claimed
- token, err := db.GetToken()
- if err != nil || token == "" {
- return fmt.Errorf("failed to register and failed to get a token from the database: someone stole our agent's callsign: %w", err)
- }
- apiClient.SetToken(token)
- agent, err := apiClient.MyAgent()
- if err != nil {
- return fmt.Errorf("failed to get agent: %w", err)
- }
- slog.Info("agent", "/my/agent", agent)
+ var state State = start_running_contracts_with_the_command_ship
+ agent.wg.Add(1)
+ go func() {
+ defer agent.wg.Done()
+ for {
+ switch state {
+ case start_running_contracts_with_the_command_ship:
+ agent.wg.Add(1)
+ go agent.autoContracting(&agent.ships[0])
+ state++
+ return
default:
- return fmt.Errorf("failed to register: %w", err)
+ agent.sendShipError(fmt.Errorf("agent runner reach an unknown state: %d", state), nil)
+ return
}
- } else {
- return fmt.Errorf("failed to register with an invalid apiError: %w", err)
}
- } else {
- token, err := db.GetToken()
- if err != nil || token == "" {
- if err := db.AddToken(register.Token); err != nil {
- return fmt.Errorf("failed to save token: %w", err)
- }
- apiClient.SetToken(register.Token)
- } else {
- // We successfully registered but have a tainted database
- slog.Error("token", "token", register.Token)
- return fmt.Errorf("TODO server reset not implemented yet")
+ }()
+ var errWg sync.WaitGroup
+ errWg.Add(1)
+ go func() {
+ defer errWg.Done()
+ for shipErr := range agent.channel {
+ slog.Error("ship error", "err", shipErr.err, "ship", shipErr.ship.Symbol)
}
- }
- // ----- run agent ---------------------------------------------------------
- contracts, err := apiClient.MyContracts()
- if err != nil {
- return err
- }
- slog.Info("start", "contract", contracts[0], "err", err)
- ships, err := apiClient.MyShips()
- if err != nil {
- return err
- }
- slog.Info("start", "ship", ships[0].Nav.Status, "err", err)
+ }()
+ agent.wg.Wait()
+ close(agent.channel)
+ errWg.Wait()
return nil
}
diff --git a/golang/pkg/api/contracts.go b/golang/pkg/api/contracts.go
index f82ee6d..9478d4f 100644
--- a/golang/pkg/api/contracts.go
+++ b/golang/pkg/api/contracts.go
@@ -3,10 +3,33 @@ package api
import (
"fmt"
"net/url"
+ "path"
+ "git.adyxax.org/adyxax/spacetraders/golang/pkg/database"
"git.adyxax.org/adyxax/spacetraders/golang/pkg/model"
)
+func (c *Client) Accept(contract *model.Contract, db *database.DB) error {
+ if contract.Accepted {
+ return nil
+ }
+ uriRef := url.URL{Path: path.Join("my/contracts", contract.Id, "accept")}
+ type acceptResponse struct {
+ Agent *model.Agent `json:"agent"`
+ Contract *model.Contract `json:"contract"`
+ }
+ var response acceptResponse
+ if err := c.Send("POST", &uriRef, nil, &response); err != nil {
+ return fmt.Errorf("failed to accept contract %s: %w", contract.Id, err)
+ }
+ if err := db.SaveAgent(response.Agent); err != nil {
+ return fmt.Errorf("failed to accept contract %s: %w", contract.Id, err)
+ }
+ contract.Accepted = response.Contract.Accepted
+ contract.Terms = response.Contract.Terms
+ return nil
+}
+
func (c *Client) MyContracts() ([]model.Contract, error) {
uriRef := url.URL{Path: "my/contracts"}
var contracts []model.Contract
diff --git a/golang/pkg/api/ships.go b/golang/pkg/api/ships.go
index 485f437..93963c3 100644
--- a/golang/pkg/api/ships.go
+++ b/golang/pkg/api/ships.go
@@ -14,10 +14,10 @@ func (c *Client) dock(s *model.Ship) error {
return nil
}
uriRef := url.URL{Path: path.Join("my/ships", s.Symbol, "dock")}
- type DockResponse struct {
+ type dockResponse struct {
Nav *model.Nav `json:"nav"`
}
- var response DockResponse
+ var response dockResponse
if err := c.Send("POST", &uriRef, nil, &response); err != nil {
return fmt.Errorf("failed to dock ship %s: %w", s.Symbol, err)
}
@@ -39,10 +39,10 @@ func (c *Client) orbit(s *model.Ship) error {
return nil
}
uriRef := url.URL{Path: path.Join("my/ships", s.Symbol, "orbit")}
- type OrbitResponse struct {
+ type orbitResponse struct {
Nav *model.Nav `json:"nav"`
}
- var response OrbitResponse
+ var response orbitResponse
if err := c.Send("POST", &uriRef, nil, &response); err != nil {
return fmt.Errorf("failed to orbit ship %s: %w", s.Symbol, err)
}
@@ -58,12 +58,12 @@ func (c *Client) Refuel(s *model.Ship, db *database.DB) error {
return fmt.Errorf("failed to refuel ship %s: %w", s.Symbol, err)
}
uriRef := url.URL{Path: path.Join("my/ships", s.Symbol, "refuel")}
- type RefuelResponse struct {
+ type refuelResponse struct {
Agent *model.Agent `json:"agent"`
Fuel *model.Fuel `json:"fuel"`
Transaction *model.Transaction `json:"transaction"`
}
- var response RefuelResponse
+ var response refuelResponse
if err := c.Send("POST", &uriRef, nil, &response); err != nil {
return fmt.Errorf("failed to refuel ship %s: %w", s.Symbol, err)
}
diff --git a/golang/pkg/database/agents.go b/golang/pkg/database/agents.go
index cd9e0a2..e2580a9 100644
--- a/golang/pkg/database/agents.go
+++ b/golang/pkg/database/agents.go
@@ -12,7 +12,7 @@ func (db *DB) SaveAgent(agent *model.Agent) error {
if err != nil {
return fmt.Errorf("failed to marshal agent: %w", err)
}
- if _, err := db.Exec(`INSERT INTO agents SET data = (json(?));`, data); err != nil {
+ if _, err := db.Exec(`INSERT INTO agents VALUES data = (json(?));`, data); err != nil {
return fmt.Errorf("failed to insert agent data: %w", err)
}
return nil
diff --git a/golang/pkg/database/db.go b/golang/pkg/database/db.go
index cb15e52..f094039 100644
--- a/golang/pkg/database/db.go
+++ b/golang/pkg/database/db.go
@@ -5,6 +5,7 @@ import (
"database/sql"
"fmt"
"runtime"
+ "strings"
)
func initDB(ctx context.Context, url string) (*sql.DB, error) {
@@ -90,6 +91,21 @@ func (db *DB) Close() error {
return nil
}
+func (db *DB) Reset() error {
+ _, err := db.Exec(strings.Join([]string{
+ "DELETE FROM agents;",
+ "DELETE FROM markets;",
+ "DELETE FROM systems;",
+ "DELETE FROM tokens;",
+ "DELETE FROM transactions;",
+ "DELETE FROM waypoints;",
+ }, ""))
+ if err != nil {
+ return fmt.Errorf("failed to reset database: %w", err)
+ }
+ return nil
+}
+
func (db *DB) Exec(query string, args ...any) (sql.Result, error) {
return db.writeDB.ExecContext(db.ctx, query, args...)
}
diff --git a/golang/pkg/database/tokens.go b/golang/pkg/database/tokens.go
index 0356cb2..8111551 100644
--- a/golang/pkg/database/tokens.go
+++ b/golang/pkg/database/tokens.go
@@ -1,6 +1,6 @@
package database
-func (db *DB) AddToken(token string) error {
+func (db *DB) SaveToken(token string) error {
_, err := db.Exec(`INSERT INTO tokens(data) VALUES (?);`, token)
return err
}