diff options
author | Julien Dessaux | 2025-02-14 00:14:15 +0100 |
---|---|---|
committer | Julien Dessaux | 2025-02-14 00:14:15 +0100 |
commit | d97985a694b218713ddf63ed684b6a509f931f3b (patch) | |
tree | 84609f4e242419bf89301e0ead4927f450d7bfe5 | |
parent | [golang] Bootstrap contracting and refactor the agent code (diff) | |
download | spacetraders-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.go | 47 | ||||
-rw-r--r-- | golang/pkg/agent/error.go | 15 | ||||
-rw-r--r-- | golang/pkg/agent/init.go | 60 | ||||
-rw-r--r-- | golang/pkg/agent/run.go | 108 | ||||
-rw-r--r-- | golang/pkg/api/contracts.go | 23 | ||||
-rw-r--r-- | golang/pkg/api/ships.go | 12 | ||||
-rw-r--r-- | golang/pkg/database/agents.go | 2 | ||||
-rw-r--r-- | golang/pkg/database/db.go | 16 | ||||
-rw-r--r-- | golang/pkg/database/tokens.go | 2 |
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 } |