summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--golang/cmd/spacetraders/main.go78
-rw-r--r--golang/go.mod5
-rw-r--r--golang/go.sum2
-rw-r--r--golang/pkg/api/agents.go14
-rw-r--r--golang/pkg/api/api.go87
-rw-r--r--golang/pkg/api/client.go75
-rw-r--r--golang/pkg/api/errors.go23
-rw-r--r--golang/pkg/api/priority_queue.go44
-rw-r--r--golang/pkg/api/register.go20
-rw-r--r--golang/pkg/database/migrations.go78
-rw-r--r--golang/pkg/database/sql/000_init.sql7
-rw-r--r--golang/pkg/database/tokens.go14
-rw-r--r--nodejs/automation/agent.ts95
-rw-r--r--nodejs/automation/contracting.ts80
-rw-r--r--nodejs/automation/init.ts17
-rw-r--r--nodejs/automation/mining.ts4
-rw-r--r--nodejs/automation/selling.ts9
-rw-r--r--nodejs/database/005_shipyards.sql6
-rw-r--r--nodejs/database/agents.ts20
-rw-r--r--nodejs/database/contracts.ts26
-rw-r--r--nodejs/database/db.ts8
-rw-r--r--nodejs/database/shipyards.ts27
-rw-r--r--nodejs/lib/agent.ts45
-rw-r--r--nodejs/lib/contracts.ts159
-rw-r--r--nodejs/lib/ships.ts78
-rw-r--r--nodejs/lib/systems.ts26
-rw-r--r--nodejs/lib/types.ts55
-rw-r--r--nodejs/lib/utils.ts33
-rwxr-xr-xnodejs/main.ts23
-rw-r--r--nodejs/package-lock.json21
-rw-r--r--nodejs/package.json14
31 files changed, 919 insertions, 274 deletions
diff --git a/golang/cmd/spacetraders/main.go b/golang/cmd/spacetraders/main.go
new file mode 100644
index 0000000..937144c
--- /dev/null
+++ b/golang/cmd/spacetraders/main.go
@@ -0,0 +1,78 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+ "os"
+ "os/signal"
+
+ "git.adyxax.org/adyxax/spacetraders/v2/pkg/api"
+ "git.adyxax.org/adyxax/spacetraders/v2/pkg/database"
+)
+
+func main() {
+ opts := &slog.HandlerOptions{
+ // //AddSource: true,
+ Level: slog.LevelDebug,
+ }
+ //logger := slog.New(slog.NewJSONHandler(os.Stdout, opts))
+ logger := slog.New(slog.NewTextHandler(os.Stdout, opts))
+ slog.SetDefault(logger)
+
+ ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
+ defer cancel()
+ db, err := database.DBInit(ctx, "./spacetraders.db")
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "DbInit error %+v\n", err)
+ os.Exit(1)
+ }
+ client := api.NewClient(ctx)
+ defer client.Close()
+ err = run( //ctx,
+ db,
+ client,
+ //os.Args,
+ //os.Getenv,
+ //os.Getwd,
+ //os.Stdin,
+ //os.Stdout,
+ //os.Stderr,
+ )
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "%s\n", err)
+ if err = db.Close(); err != nil {
+ fmt.Fprintf(os.Stderr, "%s\n", err)
+ }
+ os.Exit(2)
+ }
+}
+
+func run( // ctx context.Context,
+ db *database.DB,
+ client *api.Client,
+ //args []string,
+ //getenv func(string) string,
+ //getwd func() (string, error),
+ //stdin io.Reader,
+ //stdout, stderr io.Writer,
+) (err error) {
+ // ----- Get token or register ---------------------------------------------
+ token, err := db.GetToken()
+ if err != nil || token == "" {
+ var r api.APIMessage[api.RegisterMessage, any]
+ if r, err = client.Register("COSMIC", "ADYXAX-GO"); err != nil {
+ // TODO handle server reset
+ fmt.Printf("%+v, %+v\n", r, err)
+ return err
+ }
+ if err = db.AddToken(r.Data.Token); err != nil {
+ return err
+ }
+ }
+ client.SetToken(token)
+ // ----- Update agent ------------------------------------------------------
+ agent, err := client.MyAgent()
+ slog.Info("agent", "agent", agent, "err", err)
+ return err
+}
diff --git a/golang/go.mod b/golang/go.mod
new file mode 100644
index 0000000..25e76bd
--- /dev/null
+++ b/golang/go.mod
@@ -0,0 +1,5 @@
+module git.adyxax.org/adyxax/spacetraders/v2
+
+go 1.22.2
+
+require github.com/mattn/go-sqlite3 v1.14.22
diff --git a/golang/go.sum b/golang/go.sum
new file mode 100644
index 0000000..e8d092a
--- /dev/null
+++ b/golang/go.sum
@@ -0,0 +1,2 @@
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
diff --git a/golang/pkg/api/agents.go b/golang/pkg/api/agents.go
new file mode 100644
index 0000000..aa8107a
--- /dev/null
+++ b/golang/pkg/api/agents.go
@@ -0,0 +1,14 @@
+package api
+
+type AgentMessage struct {
+ AccountID string `json:"accountId"`
+ Credits int `json:"credits"`
+ Headquarters string `json:"headquarters"`
+ ShipCount int `json:"shipCount"`
+ StartingFaction string `json:"startingFaction"`
+ Symbol string `json:"symbol"`
+}
+
+func (c *Client) MyAgent() (APIMessage[AgentMessage, any], error) {
+ return Send[AgentMessage](c, "GET", "/my/agent", nil)
+}
diff --git a/golang/pkg/api/api.go b/golang/pkg/api/api.go
new file mode 100644
index 0000000..662bb87
--- /dev/null
+++ b/golang/pkg/api/api.go
@@ -0,0 +1,87 @@
+package api
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+ "log/slog"
+ "net/http"
+ "time"
+)
+
+type Error[T any] struct {
+ Code int `json:"code"`
+ Data T `json:"data"`
+ Message string `json:"message"`
+}
+
+type APIMessage[T any, E any] struct {
+ Data T `json:"data"`
+ Error Error[E] `json:"error"`
+ //meta
+}
+
+type Response struct {
+ Response []byte
+ Err error
+}
+
+func Send[T any](c *Client, method, path string, payload any) (message APIMessage[T, any], err error) {
+ resp := make(chan *Response)
+ c.channel <- &Request{
+ method: method,
+ path: path,
+ payload: payload,
+ priority: 10,
+ resp: resp,
+ }
+ res := <-resp
+ if res.Err != nil {
+ return message, res.Err
+ }
+ err = json.Unmarshal(res.Response, &message)
+ return message, err
+}
+
+func (c *Client) sendOne(method, path string, payload any) (body []byte, err error) {
+ slog.Debug("Request", "method", method, "path", path, "payload", payload)
+ var req *http.Request
+ if payload != nil {
+ body, err = json.Marshal(payload)
+ if err == nil {
+ req, err = http.NewRequest(method, c.baseURL+path, bytes.NewBuffer(body))
+ } else {
+ return nil, err
+ }
+ } else {
+ req, err = http.NewRequest(method, c.baseURL+path, nil)
+ }
+ if err != nil {
+ return nil, err
+ }
+ req.Header = *c.headers
+ req = req.WithContext(c.ctx)
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ slog.Error("sendOne Do", "method", method, "path", path, "error", err)
+ return nil, err
+ }
+ defer func() {
+ if e := resp.Body.Close(); err == nil {
+ err = e
+ }
+ }()
+ if body, err = io.ReadAll(resp.Body); err != nil {
+ slog.Error("sendOne ReadAll", "method", method, "path", path, "error", err)
+ return nil, err
+ }
+ slog.Debug("Response", "body", string(body))
+ switch resp.StatusCode {
+ case 429:
+ e := decode429(body)
+ time.Sleep(time.Duration(e.Error.Data.RetryAfter * float64(time.Second)))
+ return c.sendOne(method, path, payload)
+ }
+ return body, nil
+}
diff --git a/golang/pkg/api/client.go b/golang/pkg/api/client.go
new file mode 100644
index 0000000..70f3e68
--- /dev/null
+++ b/golang/pkg/api/client.go
@@ -0,0 +1,75 @@
+package api
+
+import (
+ "container/heap"
+ "context"
+ "net/http"
+ "time"
+)
+
+type Client struct {
+ baseURL string
+ channel chan *Request
+ ctx context.Context
+ headers *http.Header
+ httpClient *http.Client
+ pq *PriorityQueue
+}
+
+func NewClient(ctx context.Context) *Client {
+ pq := make(PriorityQueue, 0)
+ heap.Init(&pq)
+ client := &Client{
+ baseURL: "https://api.spacetraders.io/v2",
+ channel: make(chan *Request),
+ ctx: ctx,
+ headers: &http.Header{
+ "Content-Type": {"application/json"},
+ },
+ httpClient: &http.Client{
+ Timeout: time.Minute,
+ },
+ pq: &pq,
+ }
+ go queueProcessor(client)
+ return client
+}
+
+func (c *Client) Close() {
+ close(c.channel)
+}
+
+func (c *Client) SetToken(token string) {
+ c.headers.Set("Authorization", "Bearer "+token)
+}
+
+func queueProcessor(client *Client) {
+ var ok bool
+ for {
+ // The queue is empty so we do this blocking call
+ req := <-client.channel
+ heap.Push(client.pq, req)
+ // we enqueue all values read from the channel and process the queue's
+ // contents until empty. We keep reading the channel as long as this
+ // emptying goes on
+ for {
+ select {
+ case req = <-client.channel:
+ heap.Push(client.pq, req)
+ default:
+ if client.pq.Len() == 0 {
+ break
+ }
+ // we process one
+ if req, ok = heap.Pop(client.pq).(*Request); !ok {
+ panic("queueProcessor got something other than a Request on its channel")
+ }
+ response, err := client.sendOne(req.method, req.path, req.payload)
+ req.resp <- &Response{
+ Response: response,
+ Err: err,
+ }
+ }
+ }
+ }
+}
diff --git a/golang/pkg/api/errors.go b/golang/pkg/api/errors.go
new file mode 100644
index 0000000..d39a205
--- /dev/null
+++ b/golang/pkg/api/errors.go
@@ -0,0 +1,23 @@
+package api
+
+import (
+ "encoding/json"
+ "fmt"
+ "time"
+)
+
+type RateLimitError struct {
+ LimitType string `json:"type"`
+ RetryAfter float64 `json:"retryAfter"`
+ LimitBurst int `json:"limitBurst"`
+ LimitPerSecond int `json:"limitPerSecond"`
+ Remaining int `json:"remaining"`
+ Reset time.Time `json:"reset"`
+}
+
+func decode429(msg []byte) (e APIMessage[any, RateLimitError]) {
+ if err := json.Unmarshal(msg, &e); err != nil {
+ panic(fmt.Sprintf("Failed to decode419: %+v", err))
+ }
+ return e
+}
diff --git a/golang/pkg/api/priority_queue.go b/golang/pkg/api/priority_queue.go
new file mode 100644
index 0000000..077c8f7
--- /dev/null
+++ b/golang/pkg/api/priority_queue.go
@@ -0,0 +1,44 @@
+package api
+
+type Request struct {
+ index int
+ priority int
+
+ method string
+ path string
+ payload any
+ resp chan *Response
+}
+
+type PriorityQueue []*Request
+
+func (pq PriorityQueue) Len() int {
+ return len(pq)
+}
+
+func (pq PriorityQueue) Less(i, j int) bool {
+ return pq[i].priority < pq[j].priority
+}
+
+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.(*Request)
+ 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 // avoid memory leak
+ item.index = -1 // for safety
+ *pq = old[0 : n-1]
+ return item
+}
diff --git a/golang/pkg/api/register.go b/golang/pkg/api/register.go
new file mode 100644
index 0000000..4f95e2f
--- /dev/null
+++ b/golang/pkg/api/register.go
@@ -0,0 +1,20 @@
+package api
+
+type RegisterMessage struct {
+ //agent
+ //contract
+ //faction
+ //ship
+ Token string `json:"token"`
+}
+
+func (c *Client) Register(faction, symbol string) (APIMessage[RegisterMessage, any], error) {
+ type RegisterRequest struct {
+ Faction string `json:"faction"`
+ Symbol string `json:"symbol"`
+ }
+ return Send[RegisterMessage](c, "POST", "/register", RegisterRequest{
+ Faction: faction,
+ Symbol: symbol,
+ })
+}
diff --git a/golang/pkg/database/migrations.go b/golang/pkg/database/migrations.go
new file mode 100644
index 0000000..94207a5
--- /dev/null
+++ b/golang/pkg/database/migrations.go
@@ -0,0 +1,78 @@
+package database
+
+import (
+ "context"
+ "database/sql"
+ "embed"
+ "io/fs"
+
+ _ "github.com/mattn/go-sqlite3"
+)
+
+type DB struct {
+ ctx context.Context
+ db *sql.DB
+}
+
+//go:embed sql/*.sql
+var schemaFiles embed.FS
+
+func DBInit(ctx context.Context, url string) (myDB *DB, err error) {
+ var db *sql.DB
+ if db, err = sql.Open("sqlite3", url); err != nil {
+ return nil, err
+ }
+ defer func() {
+ if err != nil {
+ _ = db.Close()
+ }
+ }()
+
+ if _, err = db.ExecContext(ctx, "PRAGMA foreign_keys = ON"); err != nil {
+ return nil, err
+ }
+ if _, err = db.ExecContext(ctx, "PRAGMA journal_mode = WAL"); err != nil {
+ return nil, err
+ }
+
+ var version int
+ if err = db.QueryRowContext(ctx, `SELECT version FROM schema_version;`).Scan(&version); err != nil {
+ if err.Error() == "no such table: schema_version" {
+ version = 0
+ } else {
+ return nil, err
+ }
+ }
+
+ statements := make([]string, 0)
+ err = fs.WalkDir(schemaFiles, ".", func(path string, d fs.DirEntry, err error) error {
+ if d.IsDir() || err != nil {
+ return err
+ }
+ var stmts []byte
+ if stmts, err = schemaFiles.ReadFile(path); err != nil {
+ return err
+ } else {
+ statements = append(statements, string(stmts))
+ }
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ for version < len(statements) {
+ if _, err = db.ExecContext(ctx, statements[version]); err != nil {
+ return nil, err
+ }
+ version++
+ }
+ if _, err = db.ExecContext(ctx, `DELETE FROM schema_version; INSERT INTO schema_version (version) VALUES (?);`, version); err != nil {
+ return nil, err
+ }
+ return &DB{ctx: ctx, db: db}, nil
+}
+
+func (db *DB) Close() error {
+ return db.db.Close()
+}
diff --git a/golang/pkg/database/sql/000_init.sql b/golang/pkg/database/sql/000_init.sql
new file mode 100644
index 0000000..c06d2d3
--- /dev/null
+++ b/golang/pkg/database/sql/000_init.sql
@@ -0,0 +1,7 @@
+CREATE TABLE schema_version (
+ version INTEGER NOT NULL
+);
+CREATE TABLE tokens (
+ id INTEGER PRIMARY KEY,
+ data TEXT NOT NULL
+);
diff --git a/golang/pkg/database/tokens.go b/golang/pkg/database/tokens.go
new file mode 100644
index 0000000..16bda2b
--- /dev/null
+++ b/golang/pkg/database/tokens.go
@@ -0,0 +1,14 @@
+package database
+
+func (db DB) AddToken(token string) error {
+ _, err := db.db.ExecContext(db.ctx, `INSERT INTO tokens(data) VALUES (?);`, token)
+ return err
+}
+
+func (db DB) GetToken() (string, error) {
+ var token string
+ if err := db.db.QueryRowContext(db.ctx, `SELECT data FROM tokens;`).Scan(&token); err != nil {
+ return "", err
+ }
+ return token, nil
+}
diff --git a/nodejs/automation/agent.ts b/nodejs/automation/agent.ts
new file mode 100644
index 0000000..9ee0ea8
--- /dev/null
+++ b/nodejs/automation/agent.ts
@@ -0,0 +1,95 @@
+import events from 'events';
+
+import * as autoContracting from './contracting.ts';
+import { debugLog, send, sleep } from '../lib/api.ts';
+import { getAgent } from '../lib/agent.ts';
+import { getShips, Ship } from '../lib/ships.ts';
+import { market, shipyard, trait, waypoint } from '../lib/systems.ts';
+import { Waypoint } from '../lib/types.ts';
+import {
+ distance,
+ sortByDistanceFrom,
+} from '../lib/utils.ts';
+
+const bus = new events.EventEmitter(); // a bus to notify the agent to start purchasing ships
+let running = false;
+let state = 0;
+enum states {
+ start_running_contracts_with_the_command_ship = 0,
+ visit_all_shipyards,
+ send_the_starting_probe_to_a_shipyard_that_sells_probes,
+}
+
+export async function run(): Promise<void> {
+ if (running) {
+ throw 'refusing to start a second agent processor';
+ }
+ running = true;
+ state = 0;
+ try {
+ while(true) {
+ const ships = getShips();
+ switch(state) {
+ case states.start_running_contracts_with_the_command_ship:
+ // TODO await autoContracting.run(ships[0]);
+ state++;
+ continue;
+ case states.visit_all_shipyards:
+ await visit_all_shipyards(ships[1]);
+ state++;
+ continue;
+ case states.send_the_starting_probe_to_a_shipyard_that_sells_probes:
+ await send_the_starting_probe_to_a_shipyard_that_sells_probes(ships[1]);
+ state++;
+ continue;
+ default:
+ debugLog('No more agent processor states implemented, exiting!')
+ return;
+ }
+ }
+ } catch (e) {
+ running = false;
+ throw e;
+ }
+}
+
+async function send_the_starting_probe_to_a_shipyard_that_sells_probes(probe: Ship) {
+ const probeWaypoint = await waypoint(probe.nav.waypointSymbol);
+ const myShipyard = await shipyard(probeWaypoint);
+ if (myShipyard.shipTypes.some(t => t.type === 'SHIP_PROBE')) return;
+ // our starting probe is not at a shipyard that sells probes, let's move
+ const shipyardWaypoints = await trait(probe.nav.systemSymbol, 'SHIPYARD');
+ let candidates: Array<{price: number, waypoint: Waypoint}> = [];
+ for (const w of shipyardWaypoints) {
+ const shipyardData = await shipyard(w);
+ const probeData = shipyardData.ships.filter(t => t.type === 'SHIP_PROBE');
+ if (probeData.length === 0) continue;
+ candidates.push({price: probeData[0].purchasePrice, waypoint: w });
+ };
+ candidates.sort(function(a, b) {
+ if (a.price < b.price) {
+ return -1;
+ } else if (a.price > b.price) {
+ return 1;
+ }
+ return 0;
+ });
+ await probe.navigate(candidates[0].waypoint);
+}
+
+async function visit_all_shipyards(probe: Ship) {
+ const probeWaypoint = await waypoint(probe.nav.waypointSymbol);
+ const shipyardWaypoints = await trait(probe.nav.systemSymbol, 'SHIPYARD');
+ let candidates: Array<Waypoint> = [];
+ for (const w of shipyardWaypoints) {
+ const shipyardData = await shipyard(w);
+ if (shipyardData.ships) continue;
+ candidates.push(w);
+ }
+ const nexts = sortByDistanceFrom(probeWaypoint, candidates).map(o => o.data);
+ for (const next of nexts) {
+ await probe.navigate(next);
+ await market(next);
+ await shipyard(next);
+ }
+}
diff --git a/nodejs/automation/contracting.ts b/nodejs/automation/contracting.ts
index 93c2f8e..80569ef 100644
--- a/nodejs/automation/contracting.ts
+++ b/nodejs/automation/contracting.ts
@@ -1,10 +1,8 @@
import { debugLog } from '../lib/api.ts';
import { Ship } from '../lib/ships.ts';
-import { Contract } from '../lib/types.ts';
import * as mining from './mining.js';
import * as selling from './selling.js';
-import * as dbContracts from '../database/contracts.ts';
-import * as libContracts from '../lib/contracts.ts';
+import { Contract, getContracts } from '../lib/contracts.ts';
import * as libSystems from '../lib/systems.ts';
import * as systems from '../lib/systems.ts';
import {
@@ -12,7 +10,7 @@ import {
} from '../lib/utils.ts';
export async function run(ship: Ship): Promise<void> {
- const contracts = await libContracts.getContracts();
+ const contracts = await getContracts();
const active = contracts.filter(function(c) {
if (c.fulfilled) return false;
const deadline = new Date(c.terms.deadline).getTime();
@@ -29,14 +27,14 @@ export async function run(ship: Ship): Promise<void> {
async function runOne(contract: Contract, ship: Ship): Promise<void> {
debugLog(contract);
- await libContracts.accept(contract);
+ await contract.accept();
switch(contract.type) {
case 'PROCUREMENT':
- if (contract.terms.deliver[0].tradeSymbol.match(/_ORE$/)) {
- await runOreProcurement(contract, ship);
- } else {
- await runTradeProcurement(contract, ship);
- }
+ //if (contract.terms.deliver[0].tradeSymbol.match(/_ORE$/)) {
+ // await runOreProcurement(contract, ship);
+ //} else {
+ await runTradeProcurement(contract, ship);
+ //}
break;
default:
throw `Handling of contract type ${contract.type} is not implemented yet`;
@@ -58,7 +56,7 @@ async function runOreProcurement(contract: Contract, ship: Ship): Promise<void>
break;
case deliveryPoint.symbol:
if (goodCargo !== undefined) { // we could be here if a client restart happens right after selling before we navigate away
- contract = await libContracts.deliver(contract, ship);
+ await contract.deliver(ship);
if (contract.fulfilled) return;
}
await ship.navigate(asteroid);
@@ -78,46 +76,48 @@ async function runTradeProcurement(contract: Contract, ship: Ship): Promise<void
const goodCargo = ship.cargo.inventory.filter(i => i.symbol === wantedCargo)[0]
// make sure we are not carrying useless stuff
await selling.sell(ship, wantedCargo);
- // go buy what we need
- const markets = sortByDistanceFrom(ship.nav.route.destination, await libSystems.trait(ship.nav.systemSymbol, 'MARKETPLACE'));
- // check from the closest one that exports what we need
- let buyingPoint: string = "";
- outer: for (let i = 0; i < markets.length; i++) {
- const waypoint = await libSystems.waypoint(markets[i].data.symbol);
- const market = await libSystems.market(waypoint);
- for (let j = 0; j < market.exports.length; j++) {
- if (market.exports[j].symbol === wantedCargo) {
- buyingPoint = market.symbol;
- break outer;
- }
- }
- }
- // if we did not find an exporting market we look for an exchange
- if (buyingPoint === "") {
+ if (ship.cargo.units < ship.cargo.capacity) {
+ // go buy what we need
+ const markets = sortByDistanceFrom(ship.nav.route.destination, await libSystems.trait(ship.nav.systemSymbol, 'MARKETPLACE'));
+ // check from the closest one that exports what we need
+ let buyingPoint: string = "";
outer: for (let i = 0; i < markets.length; i++) {
const waypoint = await libSystems.waypoint(markets[i].data.symbol);
const market = await libSystems.market(waypoint);
- for (let j = 0; j < market.exchange.length; j++) {
- if (market.exchange[j].symbol === wantedCargo) {
+ for (let j = 0; j < market.exports.length; j++) {
+ if (market.exports[j].symbol === wantedCargo) {
buyingPoint = market.symbol;
break outer;
}
}
}
+ // if we did not find an exporting market we look for an exchange
+ if (buyingPoint === "") {
+ outer: for (let i = 0; i < markets.length; i++) {
+ const waypoint = await libSystems.waypoint(markets[i].data.symbol);
+ const market = await libSystems.market(waypoint);
+ for (let j = 0; j < market.exchange.length; j++) {
+ if (market.exchange[j].symbol === wantedCargo) {
+ buyingPoint = market.symbol;
+ break outer;
+ }
+ }
+ }
+ }
+ if (buyingPoint === "") {
+ throw `runTradeProcurement failed, no market exports or exchanges ${wantedCargo}`;
+ }
+ // go buy what we need
+ await ship.navigate(await libSystems.waypoint(buyingPoint));
+ const units = Math.min(
+ deliver.unitsRequired - deliver.unitsFulfilled,
+ ship.cargo.capacity - ship.cargo.units,
+ );
+ await ship.purchase(wantedCargo, units);
}
- if (buyingPoint === "") {
- throw `runTradeProcurement failed, no market exports or exchanges ${wantedCargo}`;
- }
- // go buy what we need
- await ship.navigate(await libSystems.waypoint(buyingPoint));
- const units = Math.min(
- deliver.unitsRequired - deliver.unitsFulfilled,
- ship.cargo.capacity - ship.cargo.units,
- );
- await ship.purchase(wantedCargo, units);
// then make a delivery
await ship.navigate(deliveryPoint);
- contract = await libContracts.deliver(contract, ship);
+ await contract.deliver(ship);
if (contract.fulfilled) return;
}
console.log("runTradeProcurement not implemented");
diff --git a/nodejs/automation/init.ts b/nodejs/automation/init.ts
index 1e8ea72..1fa9949 100644
--- a/nodejs/automation/init.ts
+++ b/nodejs/automation/init.ts
@@ -1,15 +1,11 @@
-import * as dbAgents from '../database/agents.ts';
import * as db from '../database/db.ts';
-import * as dbContracts from '../database/contracts.ts';
import * as dbTokens from '../database/tokens.ts';
import {
Response,
} from '../lib/api.ts';
-import {
- Agent,
- Contract,
-} from '../lib/types.ts';
-import { Ship } from '../lib/ships.ts';
+import { Agent, initAgent, setAgent } from '../lib/agent.ts';
+import { Contract } from '../lib/contracts.ts';
+import { initShips, Ship } from '../lib/ships.ts';
import * as libContracts from '../lib/contracts.ts';
const symbol = process.env.NODE_ENV === 'test' ? 'ADYXAX-0' : 'ADYXAX-JS';
@@ -31,7 +27,8 @@ export async function init(): Promise<void> {
switch(json.error?.code) {
case 4111: // 4111 means the agent symbol has already been claimed so no server reset happened
// TODO await agents.agents();
- await libContracts.getContracts();
+ await initAgent();
+ await initShips();
return;
default:
throw json;
@@ -39,6 +36,6 @@ export async function init(): Promise<void> {
}
db.reset();
dbTokens.addToken(json.data.token);
- dbAgents.addAgent(json.data.agent);
- dbContracts.setContract(json.data.contract);
+ setAgent(json.data.agent);
+ await initShips();
}
diff --git a/nodejs/automation/mining.ts b/nodejs/automation/mining.ts
index cdfcb78..e7ba62f 100644
--- a/nodejs/automation/mining.ts
+++ b/nodejs/automation/mining.ts
@@ -1,8 +1,7 @@
import * as selling from './selling.js';
-import * as dbContracts from '../database/contracts.js';
+import { Contract } from '../lib/contracts.js';
import { Ship } from '../lib/ships.js';
import {
- Contract,
Waypoint,
} from '../lib/types.ts';
import { categorizeCargo } from '../lib/utils.ts';
@@ -11,7 +10,6 @@ export async function mineUntilFullFor(contract: Contract, ship: Ship, asteroid:
// TODO find a good asteroid
while(true) {
await mineUntilFull(ship);
- contract = dbContracts.getContract(contract.id);
const deliver = contract.terms.deliver[0];
const cargo = categorizeCargo(ship.cargo, deliver.tradeSymbol);
const wantedUnits = Object.values(cargo.wanted).reduce((acc, e) => acc += e, 0);
diff --git a/nodejs/automation/selling.ts b/nodejs/automation/selling.ts
index 5dee8f7..b17aad0 100644
--- a/nodejs/automation/selling.ts
+++ b/nodejs/automation/selling.ts
@@ -3,12 +3,9 @@ import * as libSystems from '../lib/systems.ts';
import {
categorizeCargo,
sortByDistanceFrom,
+ whatCanBeTradedAt,
} from '../lib/utils.ts';
import { Ship } from '../lib/ships.ts';
-import {
- CargoManifest,
- CommonThing,
-} from '../lib/types.ts';
// example ctx { ship: {XXX}, keep: 'SILVER_ORE' }
export async function sell(ship: Ship, good: string): Promise<Ship> {
@@ -54,7 +51,3 @@ export async function sell(ship: Ship, good: string): Promise<Ship> {
throw new Error(`Ship {ship.symbol} has found no importing or exchanging market for its cargo in the system`);
}
}
-
-function whatCanBeTradedAt(cargo: CargoManifest, goods: Array<CommonThing>): Array<CommonThing> {
- return goods.filter(g => cargo[g.symbol] !== undefined );
-}
diff --git a/nodejs/database/005_shipyards.sql b/nodejs/database/005_shipyards.sql
new file mode 100644
index 0000000..e4d3f28
--- /dev/null
+++ b/nodejs/database/005_shipyards.sql
@@ -0,0 +1,6 @@
+CREATE TABLE shipyards (
+ id INTEGER PRIMARY KEY,
+ data JSON NOT NULL,
+ updated DATE DEFAULT NULL
+);
+CREATE UNIQUE INDEX shipyards_data_symbol on shipyards (json_extract(data, '$.symbol'));
diff --git a/nodejs/database/agents.ts b/nodejs/database/agents.ts
deleted file mode 100644
index 5221dc7..0000000
--- a/nodejs/database/agents.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Agent } from '../lib/types.ts';
-import { DbData, db } from './db.ts';
-
-const addAgentStatement = db.prepare(`INSERT INTO agents(data) VALUES (json(?));`);
-const getAgentStatement = db.prepare(`SELECT data FROM agents;`);
-const setAgentStatement = db.prepare(`UPDATE agents SET data = json(?);`);
-
-export function addAgent(agent: Agent): void {
- addAgentStatement.run(JSON.stringify(agent));
-}
-
-export function getAgent(): Agent|null {
- const data = getAgentStatement.get() as DbData|undefined;
- if (!data) return null;
- return JSON.parse(data.data);
-}
-
-export function setAgent(agent: Agent): void {
- setAgentStatement.run(JSON.stringify(agent));
-}
diff --git a/nodejs/database/contracts.ts b/nodejs/database/contracts.ts
deleted file mode 100644
index 9adb4c8..0000000
--- a/nodejs/database/contracts.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { Contract } from '../lib/types.ts';
-import { DbData, db } from './db.ts';
-
-const addContractStatement = db.prepare(`INSERT INTO contracts(data) VALUES (json(?));`);
-const getContractStatement = db.prepare(`SELECT data FROM contracts WHERE data->>'id' = ?;`);
-const getContractsStatement = db.prepare(`SELECT data FROM contracts WHERE data->>'fulfilled' = false;`);
-const updateContractStatement = db.prepare(`UPDATE contracts SET data = json(:data) WHERE data->>'id' = :id;`);
-
-export function getContract(id: string): Contract {
- const data = getContractStatement.get(id) as DbData|undefined;
- if (!data) throw `invalid id ${id} in getContract database call`;
- return JSON.parse(data.data);
-}
-
-export function getContracts(): Array<Contract> {
- const data = getContractsStatement.all() as Array<DbData>;
- return data.map(contractData => JSON.parse(contractData.data));
-}
-
-export function setContract(data: Contract): void {
- const changes = updateContractStatement.run({
- data: JSON.stringify(data),
- id: data.id,
- }).changes;
- if (changes === 0) addContractStatement.run(JSON.stringify(data));
-}
diff --git a/nodejs/database/db.ts b/nodejs/database/db.ts
index 115d6a3..2fba09b 100644
--- a/nodejs/database/db.ts
+++ b/nodejs/database/db.ts
@@ -2,12 +2,6 @@ import fs from 'fs';
import path from 'path';
import Database from 'better-sqlite3';
-let allMigrations: Array<string> = [];
-fs.readdir('./database/', function(err, files) {
- if (err) throw err;
- allMigrations = files.filter(e => e.match(/\.sql$/)).map(e => path.join('./database', e));
-});
-
export type DbData = {data: string};
export const db = new Database(
@@ -18,6 +12,8 @@ db.pragma('foreign_keys = ON');
db.pragma('journal_mode = WAL');
function init(): void {
+ const filenames = fs.readdirSync('./database/');
+ const allMigrations = filenames.filter(e => e.match(/\.sql$/)).map(e => path.join('./database', e));
db.transaction(function migrate() {
let version;
try {
diff --git a/nodejs/database/shipyards.ts b/nodejs/database/shipyards.ts
new file mode 100644
index 0000000..4c243f9
--- /dev/null
+++ b/nodejs/database/shipyards.ts
@@ -0,0 +1,27 @@
+import { DbData, db } from './db.ts';
+import { Shipyard } from '../lib/types';
+
+const addStatement = db.prepare(`INSERT INTO shipyards(data, updated) VALUES (json(:data), :date);`);
+const getStatement = db.prepare(`SELECT data FROM shipyards WHERE data->>'symbol' = ?;`);
+const updateStatement = db.prepare(`UPDATE shipyards SET data = json(:data), updated = :date WHERE data->>'symbol' = :symbol;`);
+
+export function get(symbol: string): Shipyard|null {
+ const data = getStatement.get(symbol) as DbData|undefined;
+ if (!data) return null;
+ return JSON.parse(data.data);
+}
+
+export function set(data: Shipyard): void {
+ if (get(data.symbol) === null) {
+ addStatement.run({
+ data: JSON.stringify(data),
+ date: new Date().toISOString(),
+ });
+ } else {
+ updateStatement.run({
+ data: JSON.stringify(data),
+ date: new Date().toISOString(),
+ symbol: data.symbol,
+ });
+ }
+}
diff --git a/nodejs/lib/agent.ts b/nodejs/lib/agent.ts
new file mode 100644
index 0000000..ee7200c
--- /dev/null
+++ b/nodejs/lib/agent.ts
@@ -0,0 +1,45 @@
+import { debugLog, send } from './api.ts';
+
+export class Agent {
+ accountId: string;
+ credits: number;
+ headquarters: string;
+ shipCount: number;
+ startingFaction: string;
+ symbol: string;
+ constructor() {
+ this.accountId = "";
+ this.credits = 0;
+ this.headquarters = "";
+ this.shipCount = 0;
+ this.startingFaction = "";
+ this.symbol = "";
+ }
+ set(agent: Agent) {
+ this.accountId = agent.accountId;
+ this.credits = agent.credits;
+ this.headquarters = agent.headquarters;
+ this.shipCount = agent.shipCount;
+ this.startingFaction = agent.startingFaction;
+ this.symbol = agent.symbol;
+ }
+};
+
+let myAgent : Agent = new Agent();
+
+export function getAgent(): Agent {
+ return myAgent;
+}
+
+export async function initAgent(): Promise<void> {
+ const response = await send<Agent>({endpoint: `/my/agent`, page: 1});
+ if (response.error) {
+ debugLog(response);
+ throw response;
+ }
+ myAgent.set(response.data);
+}
+
+export function setAgent(agent: Agent): void {
+ myAgent.set(agent);
+}
diff --git a/nodejs/lib/contracts.ts b/nodejs/lib/contracts.ts
index 0582cc7..f9ba212 100644
--- a/nodejs/lib/contracts.ts
+++ b/nodejs/lib/contracts.ts
@@ -1,7 +1,5 @@
import {
- Agent,
Cargo,
- Contract,
} from './types.ts';
import {
APIError,
@@ -9,86 +7,99 @@ import {
send,
sendPaginated,
} from './api.ts';
+import { Agent, setAgent } from './agent.ts';
import { Ship } from './ships.ts';
-import * as dbAgents from '../database/agents.ts';
-import * as dbContracts from '../database/contracts.ts';
-
-export async function accept(contract: Contract): Promise<Contract> {
- contract = dbContracts.getContract(contract.id);
- if (contract.accepted) return contract;
- const response = await send<{agent: Agent, contract: Contract, type: ''}>({endpoint: `/my/contracts/${contract.id}/accept`, method: 'POST'});
- if (response.error) {
- debugLog(response);
- throw response;
- }
- dbAgents.setAgent(response.data.agent);
- dbContracts.setContract(response.data.contract);
- return response.data.contract;
-}
export async function getContracts(): Promise<Array<Contract>> {
const response = await sendPaginated<Contract>({endpoint: '/my/contracts'});
- response.forEach(contract => dbContracts.setContract(contract));
- return response;
-}
-
-export async function getContract(contract: Contract): Promise<Contract> {
- try {
- return dbContracts.getContract(contract.id);
- } catch {}
- const response = await send<Contract>({endpoint: `/my/contracts/${contract.id}`});
- if (response.error) {
- debugLog(response);
- throw response;
- }
- dbContracts.setContract(response.data);
- return response.data;
+ return response.map(contract => new Contract(contract));
}
-export async function deliver(contract: Contract, ship: Ship): Promise<Contract> {
- contract = dbContracts.getContract(contract.id);
- if (contract.terms.deliver[0].unitsRequired <= contract.terms.deliver[0].unitsFulfilled) {
- return await fulfill(contract);
+export class Contract {
+ accepted: boolean;
+ deadlineToAccept: Date;
+ expiration: Date;
+ factionSymbol: string;
+ fulfilled: boolean;
+ id: string;
+ terms: {
+ deadline: Date;
+ payment: {
+ onAccepted: number;
+ onFulfilled: number;
+ },
+ deliver: Array<{
+ tradeSymbol: string;
+ destinationSymbol: string;
+ unitsRequired: number;
+ unitsFulfilled: number;
+ }>;
+ };
+ type: string;
+ constructor(contract: Contract) {
+ this.accepted = contract.accepted;
+ this.deadlineToAccept = contract.deadlineToAccept;
+ this.expiration = contract.expiration;
+ this.factionSymbol = contract.factionSymbol;
+ this.fulfilled = contract.fulfilled;
+ this.id = contract.id;
+ this.terms = contract.terms;
+ this.type = contract.type;
}
- const tradeSymbol = contract.terms.deliver[0].tradeSymbol;
- let units = 0;
- ship.cargo.inventory.forEach(i => {if (i.symbol === tradeSymbol) units = i.units; });
- await ship.dock(); // we need to be docked to deliver
- const response = await send<{contract: Contract, cargo: Cargo}>({ endpoint: `/my/contracts/${contract.id}/deliver`, method: 'POST', payload: {
- shipSymbol: ship.symbol,
- tradeSymbol: tradeSymbol,
- units: units,
- }});
- if (response.error) {
- switch(response.error.code) {
- case 4503: // contract has expired
- // TODO sell cargo? the next trading loop should take care of it by itself
- contract.fulfilled = true;
- return contract;
- case 4509: // contract delivery terms have been met
- return await fulfill(contract);
- default: // yet unhandled error
- debugLog(response);
- throw response;
+ async accept(): Promise<void> {
+ if (this.accepted) return;
+ const response = await send<{agent: Agent, contract: Contract, type: ''}>({endpoint: `/my/contracts/${this.id}/accept`, method: 'POST'});
+ if (response.error) {
+ debugLog(response);
+ throw response;
}
+ this.accepted = response.data.contract.accepted;
+ this.terms = response.data.contract.terms;
+ setAgent(response.data.agent);
}
- dbContracts.setContract(response.data.contract);
- ship.cargo = response.data.cargo;
- if(response.data.contract.terms.deliver[0].unitsRequired <= response.data.contract.terms.deliver[0].unitsFulfilled) {
- return await fulfill(response.data.contract);
+ async deliver(ship: Ship): Promise<void> {
+ const unitsRemaining = this.terms.deliver[0].unitsRequired - this.terms.deliver[0].unitsFulfilled;
+ if (unitsRemaining <= 0) return await this.fulfill();
+ const tradeSymbol = this.terms.deliver[0].tradeSymbol;
+ let units = 0;
+ ship.cargo.inventory.forEach(i => {if (i.symbol === tradeSymbol) units = i.units; });
+ if (units === 0) return;
+ if (units > unitsRemaining) units = unitsRemaining;
+ await ship.dock(); // we need to be docked to deliver
+ const response = await send<{contract: Contract, cargo: Cargo}>({ endpoint: `/my/contracts/${this.id}/deliver`, method: 'POST', payload: {
+ shipSymbol: ship.symbol,
+ tradeSymbol: tradeSymbol,
+ units: units,
+ }});
+ if (response.error) {
+ switch(response.error.code) {
+ case 4503: // contract has expired
+ // TODO sell cargo? the next trading loop should take care of it by itself
+ this.fulfilled = true;
+ return;
+ case 4509: // contract delivery terms have been met
+ return await this.fulfill();
+ default: // yet unhandled error
+ debugLog(response);
+ throw response;
+ }
+ }
+ this.terms = response.data.contract.terms;
+ ship.cargo = response.data.cargo;
+ if(response.data.contract.terms.deliver[0].unitsRequired <= response.data.contract.terms.deliver[0].unitsFulfilled) {
+ return await this.fulfill();
+ }
}
- return response.data.contract;
-}
-
-export async function fulfill(contract: Contract): Promise<Contract> {
- contract = dbContracts.getContract(contract.id);
- if (contract.fulfilled) return contract;
- const response = await send<{agent: Agent, contract: Contract}>({ endpoint: `/my/contracts/${contract.id}/fulfill`, method: 'POST'});
- if (response.error) {
- debugLog(response);
- throw response;
+ async fulfill(): Promise<void> {
+ if (this.terms.deliver[0].unitsRequired < this.terms.deliver[0].unitsFulfilled) return;
+ if (this.fulfilled) return;
+ const response = await send<{agent: Agent, contract: Contract}>({ endpoint: `/my/contracts/${this.id}/fulfill`, method: 'POST'});
+ if (response.error) {
+ debugLog(response);
+ throw response;
+ }
+ setAgent(response.data.agent);
+ this.fulfilled = true;
+ this.terms = response.data.contract.terms;
}
- dbAgents.setAgent(response.data.agent);
- dbContracts.setContract(response.data.contract);
- return response.data.contract;
-}
+};
diff --git a/nodejs/lib/ships.ts b/nodejs/lib/ships.ts
index 4ae64c7..7daae71 100644
--- a/nodejs/lib/ships.ts
+++ b/nodejs/lib/ships.ts
@@ -10,11 +10,11 @@ import {
ShipIsStillOnCooldownError,
ShipRequiresMoreFuelForNavigationError,
} from './errors.ts';
+import { Agent, setAgent } from './agent.ts';
+import { Contract } from './contracts.ts';
import * as libSystems from './systems.ts';
import {
- Agent,
Cargo,
- Contract,
Cooldown,
Fuel,
Nav,
@@ -24,17 +24,6 @@ import {
import {
shortestPath,
} from './utils.ts';
-import * as dbAgents from '../database/agents.ts';
-import * as dbContracts from '../database/contracts.ts';
-
-export async function getShips(): Promise<Array<Ship>> {
- const response = await send<Array<Ship>>({endpoint: `/my/ships`, page: 1});
- if (response.error) {
- debugLog(response);
- throw response;
- }
- return response.data.map(ship => new Ship(ship));
-}
export class Ship {
cargo: Cargo;
@@ -99,11 +88,27 @@ export class Ship {
await sleep(response.data.cooldown.remainingSeconds*1000);
return this.cargo;
}
+ //async flightMode(mode: string): Promise<void> {
+ // if (this.nav.flightMode === mode) return;
+ // const response = await send<nav>({endpoint: `/my/ships/${this.symbol}/nav`, method: 'PATCH', payload: { flightmode: mode }});
+ // if (response.error) {
+ // switch(response.error.code) {
+ // case 4214:
+ // const sicite = response.error.data as ShipIsCurrentlyInTransitError;
+ // await sleep(sicite.secondsToArrival * 1000);
+ // return await this.flightMode(mode);
+ // default: // yet unhandled error
+ // debugLog(response);
+ // throw response;
+ // }
+ // }
+ // this.nav = response.data;
+ //}
isFull(): boolean {
return this.cargo.units >= this.cargo.capacity * 0.9;
}
async navigate(waypoint: Waypoint): Promise<void> {
- let path = shortestPath(await libSystems.waypoint(this.nav.route.destination.symbol), waypoint, this.fuel.capacity, await libSystems.waypoints(this.nav.systemSymbol));
+ let path = await shortestPath(await libSystems.waypoint(this.nav.route.destination.symbol), waypoint, this.fuel.capacity, await libSystems.waypoints(this.nav.systemSymbol));
while (path.length > 0) {
const next = path.pop();
if (next === undefined) break;
@@ -116,6 +121,7 @@ export class Ship {
}
private async navigateTo(symbol: string): Promise<void> {
await this.orbit();
+ //if (this.fuel.capacity === 0) this.flightMode('BURN');
const response = await send<{fuel: Fuel, nav: Nav}>({endpoint: `/my/ships/${this.symbol}/navigate`, method: 'POST', payload: { waypointSymbol: symbol }}); // TODO events field
if (response.error) {
switch(response.error.code) {
@@ -154,8 +160,7 @@ export class Ship {
throw response;
}
}
- dbContracts.setContract(response.data.contract);
- return response.data.contract;
+ return new Contract(response.data.contract);
}
async orbit(): Promise<void> {
if (this.nav.status === 'IN_ORBIT') return;
@@ -190,7 +195,7 @@ export class Ship {
}
}
this.cargo = response.data.cargo;
- dbAgents.setAgent(response.data.agent);
+ setAgent(response.data.agent);
}
async refuel(): Promise<void> {
if (this.fuel.current === this.fuel.capacity) return;
@@ -202,20 +207,47 @@ export class Ship {
throw response;
}
this.fuel = response.data.fuel;
- dbAgents.setAgent(response.data.agent);
+ setAgent(response.data.agent);
}
- async sell(tradeSymbol: string): Promise<Cargo> {
+ async sell(tradeSymbol: string, maybeUnits?: number): Promise<Cargo> {
// TODO check if our current waypoint has a marketplace and buys tradeSymbol?
await this.dock();
let units = 0;
- this.cargo.inventory.forEach(i => {if (i.symbol === tradeSymbol) units = i.units; });
+ if (maybeUnits !== undefined) {
+ units = maybeUnits;
+ } else {
+ this.cargo.inventory.forEach(i => {if (i.symbol === tradeSymbol) units = i.units; });
+ }
+ // TODO take into account the tradevolume if we know it already, we might need to buy in multiple steps
const response = await send<{agent: Agent, cargo: Cargo}>({endpoint: `/my/ships/${this.symbol}/sell`, method: 'POST', payload: { symbol: tradeSymbol, units: units }}); // TODO transaction field
if (response.error) {
- debugLog(response);
- throw response;
+ switch(response.error.code) {
+ case 4604: // units per transaction limit exceeded
+ const mtve = response.error.data as MarketTradeVolumeError;
+ await this.sell(tradeSymbol, mtve.tradeVolume); // TODO cache this information
+ return await this.sell(tradeSymbol, units - mtve.tradeVolume);
+ default:
+ debugLog(response);
+ throw response;
+ }
}
this.cargo = response.data.cargo;
- dbAgents.setAgent(response.data.agent);
+ setAgent(response.data.agent);
return this.cargo;
}
}
+
+let myShips: Array<Ship> = [];
+
+export function getShips(): Array<Ship> {
+ return myShips;
+}
+
+export async function initShips(): Promise<void> {
+ const response = await send<Array<Ship>>({endpoint: `/my/ships`, page: 1});
+ if (response.error) {
+ debugLog(response);
+ throw response;
+ }
+ myShips = response.data.map(ship => new Ship(ship));
+}
diff --git a/nodejs/lib/systems.ts b/nodejs/lib/systems.ts
index d4b3be5..f07f92b 100644
--- a/nodejs/lib/systems.ts
+++ b/nodejs/lib/systems.ts
@@ -4,17 +4,22 @@ import {
sendPaginated,
} from './api.ts';
import * as dbMarkets from '../database/markets.ts';
+import * as dbShipyards from '../database/shipyards.ts';
import * as dbSystems from '../database/systems.ts';
import {
Market,
+ Shipyard,
System,
Waypoint,
} from './types.ts'
-import { systemFromWaypoint } from './utils.ts';
+import {
+ is_there_a_ship_at_this_waypoint,
+ systemFromWaypoint,
+} from './utils.ts';
export async function market(waypoint: Waypoint): Promise<Market> {
const data = dbMarkets.getMarketAtWaypoint(waypoint.symbol);
- if (data) { return data; }
+ if (data && (data.tradeGoods || !is_there_a_ship_at_this_waypoint(waypoint))) { return data; }
const systemSymbol = systemFromWaypoint(waypoint.symbol);
let response = await send<Market>({endpoint: `/systems/${systemSymbol}/waypoints/${waypoint.symbol}/market`});
if (response.error) {
@@ -25,11 +30,18 @@ export async function market(waypoint: Waypoint): Promise<Market> {
return response.data;
}
-//export async function shipyard(waypoint: string): Promise<unknown> {
-// // TODO database caching
-// const systemSymbol = systemFromWaypoint(waypoint);
-// return await send({endpoint: `/systems/${systemSymbol}/waypoints/${waypoint}/shipyard`});
-//}
+export async function shipyard(waypoint: Waypoint): Promise<Shipyard> {
+ const data = dbShipyards.get(waypoint.symbol);
+ if (data && (data.ships || !is_there_a_ship_at_this_waypoint(waypoint))) { return data; }
+ const systemSymbol = systemFromWaypoint(waypoint.symbol);
+ const response = await send<Shipyard>({endpoint: `/systems/${systemSymbol}/waypoints/${waypoint.symbol}/shipyard`});
+ if (response.error) {
+ debugLog(response);
+ throw response;
+ }
+ dbShipyards.set(response.data);
+ return response.data;
+}
export async function system(symbol: string): Promise<System> {
let data = dbSystems.getSystem(symbol);
diff --git a/nodejs/lib/types.ts b/nodejs/lib/types.ts
index a8e748c..e03e5a7 100644
--- a/nodejs/lib/types.ts
+++ b/nodejs/lib/types.ts
@@ -1,12 +1,3 @@
-export type Agent = {
- accountId: string;
- credits: number;
- headquarters: string;
- shipCount: number;
- startingFaction: string;
- symbol: string;
-};
-
export type CommonThing = {
description: string;
name: string;
@@ -30,29 +21,6 @@ export type Chart = {
submittedOn: Date;
};
-export type Contract = {
- id: string;
- factionSymbol: string;
- type: string;
- terms: {
- deadline: Date;
- payment: {
- onAccepted: number;
- onFulfilled: number;
- },
- deliver: Array<{
- tradeSymbol: string;
- destinationSymbol: string;
- unitsRequired: number;
- unitsFulfilled: number;
- }>;
- };
- accepted: boolean;
- fulfilled: boolean;
- expiration: Date;
- deadlineToAccept: Date;
-};
-
export type Cooldown = {
shipSymbol: string;
totalSeconds: number;
@@ -112,6 +80,29 @@ export type RouteEndpoint = {
y: number;
};
+export type Shipyard = {
+ modificationFee: number;
+ ships: Array<ShipyardShip>;
+ shipTypes: Array<{type: string}>;
+ symbol: string;
+ //transactions: Array<Transaction>;
+};
+
+export type ShipyardShip = {
+ activity: string;
+ // crew
+ description: string;
+ // engine
+ // frame
+ // modules
+ // mounts
+ name: string;
+ purchasePrice: number;
+ // reactor
+ supply: string;
+ type: string;
+};
+
export type System = {
symbol: string;
sectorSymbol: string;
diff --git a/nodejs/lib/utils.ts b/nodejs/lib/utils.ts
index c8f3d7f..6d051ea 100644
--- a/nodejs/lib/utils.ts
+++ b/nodejs/lib/utils.ts
@@ -1,7 +1,13 @@
+import {
+ debugLog,
+} from './api.ts';
import { PriorityQueue } from './priority_queue.ts';
+import { getShips } from './ships.ts';
+import { market } from './systems.ts';
import {
Cargo,
CargoManifest,
+ CommonThing,
Waypoint,
} from './types.ts';
@@ -34,6 +40,10 @@ export function distance(a: Point, b: Point) {
return Math.sqrt((a.x-b.x)**2 + (a.y-b.y)**2);
}
+export function is_there_a_ship_at_this_waypoint(waypoint: Waypoint): boolean {
+ return getShips().some(s => s.nav.waypointSymbol === waypoint.symbol);
+}
+
export function sortByDistanceFrom<T extends Point>(a: Point, points: Array<T>): Array<{data: T, distance: number}>{
let result = points.map(function (m) { return {
data: m,
@@ -53,7 +63,8 @@ export function sortByDistanceFrom<T extends Point>(a: Point, points: Array<T>):
type Step = {waypoint: Waypoint, prev: string, fuel: number, total: number};
type ShortestPath = Array<{symbol: string, fuel: number}>;
-export function shortestPath(origin: Waypoint, destination: Waypoint, range: number, waypoints: Array<Waypoint>): ShortestPath {
+export async function shortestPath(origin: Waypoint, destination: Waypoint, range: number, waypoints: Array<Waypoint>): Promise<ShortestPath> {
+ if (range === 0) range = Infinity;
let backtrace: {[key: string]: Step} = {};
let fuels: {[key: string]: number} = {}; // fuel = distance + 1 per hop
let unvisited: {[key: string]: Waypoint} = {};
@@ -69,7 +80,6 @@ export function shortestPath(origin: Waypoint, destination: Waypoint, range: num
const symbol = step.waypoint.symbol;
if (!(symbol in unvisited)) continue;
backtrace[symbol] = step;
- if (symbol === destination.symbol) break;
const prev = unvisited[symbol];
delete unvisited[symbol];
for(const ws in unvisited) {
@@ -79,13 +89,22 @@ export function shortestPath(origin: Waypoint, destination: Waypoint, range: num
const total = step.fuel + f;
if (fuels[ws] > total) {
fuels[ws] = total;
- queue.enqueue({waypoint: w, prev: symbol, fuel: f, total: total}, total);
+ const nextStep = {waypoint: w, prev: symbol, fuel: f, total: total};
+ if (ws === destination.symbol) {
+ backtrace[ws] = nextStep;
+ break;
+ }
+ if (!w.traits.some(t => t.symbol === 'MARKETPLACE')) continue;
+ const m = await market(w);
+ if (whatCanBeTradedAt({"FUEL":1}, m.exports.concat(m.exchange)).length === 0) continue;
+ queue.enqueue(nextStep, total);
}
}
}
let result: ShortestPath = [];
- for (let step = backtrace[destination.symbol]; step.waypoint.symbol != origin.symbol; step = backtrace[step.prev]) {
- if (step === undefined) throw `Cannot compute shortest path from ${origin.symbol} to ${destination.symbol} with range ${range}.`;
+ let step = backtrace[destination.symbol];
+ if (step === undefined) throw `Cannot compute shortest path from ${origin.symbol} to ${destination.symbol} with range ${range}.`;
+ for (; step.waypoint.symbol != origin.symbol; step = backtrace[step.prev]) {
result.push({symbol: step.waypoint.symbol, fuel: step.fuel});
}
return result;
@@ -94,3 +113,7 @@ export function shortestPath(origin: Waypoint, destination: Waypoint, range: num
export function systemFromWaypoint(waypoint: string): string {
return waypoint.split('-').slice(0,2).join('-');
}
+
+export function whatCanBeTradedAt(cargo: CargoManifest, goods: Array<CommonThing>): Array<CommonThing> {
+ return goods.filter(g => cargo[g.symbol] !== undefined );
+}
diff --git a/nodejs/main.ts b/nodejs/main.ts
index 2d15c60..cb819c7 100755
--- a/nodejs/main.ts
+++ b/nodejs/main.ts
@@ -1,10 +1,23 @@
-import * as autoContracting from './automation/contracting.ts';
+import * as autoAgent from './automation/agent.ts';
//import * as autoExploring from './automation/exploration.ts';
import * as autoInit from './automation/init.ts';
-import { getShips } from './lib/ships.ts';
+import { getAgent } from './lib/agent.ts';
+import { debugLog } from './lib/api.ts';
+
+//debugLog(await send({endpoint: '/'}));
await autoInit.init();
-const ships = await getShips();
-await autoContracting.run(ships[0]); // dedicate the command ship to running contracts
-//autoExploring.init();
+debugLog(getAgent());
+
+await autoAgent.run();
+
+//import { market, trait } from './lib/systems.ts';
+//const ws = await trait(ships[0].nav.systemSymbol, 'SHIPYARD');
+//debugLog(ws);
+
+//for (let w of ws) {
+// debugLog(await market(w));
+//}
+//
+//await ships[0].navigate(await waypoint('X1-GR47-I59'));
diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json
index 5762904..9c011cc 100644
--- a/nodejs/package-lock.json
+++ b/nodejs/package-lock.json
@@ -1,12 +1,15 @@
{
- "name": "nodejs",
+ "name": "spacetraders",
+ "version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
+ "name": "spacetraders",
+ "version": "0.0.1",
"dependencies": {
- "@types/better-sqlite3": "^7.6.9",
- "better-sqlite3": "^9.4.3"
+ "@types/better-sqlite3": "^7.6.10",
+ "better-sqlite3": "^9.6.0"
},
"devDependencies": {
"esrun": "^3.2.26",
@@ -376,9 +379,9 @@
}
},
"node_modules/@types/better-sqlite3": {
- "version": "7.6.9",
- "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.9.tgz",
- "integrity": "sha512-FvktcujPDj9XKMJQWFcl2vVl7OdRIqsSRX9b0acWwTmwLK9CF2eqo/FRcmMLNpugKoX/avA6pb7TorDLmpgTnQ==",
+ "version": "7.6.10",
+ "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.10.tgz",
+ "integrity": "sha512-TZBjD+yOsyrUJGmcUj6OS3JADk3+UZcNv3NOBqGkM09bZdi28fNZw8ODqbMOLfKCu7RYCO62/ldq1iHbzxqoPw==",
"dependencies": {
"@types/node": "*"
}
@@ -424,9 +427,9 @@
]
},
"node_modules/better-sqlite3": {
- "version": "9.5.0",
- "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.5.0.tgz",
- "integrity": "sha512-01qVcM4gPNwE+PX7ARNiHINwzVuD6nx0gdldaAAcu+MrzyIAukQ31ZDKEpzRO/CNA9sHpxoTZ8rdjoyAin4dyg==",
+ "version": "9.6.0",
+ "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.6.0.tgz",
+ "integrity": "sha512-yR5HATnqeYNVnkaUTf4bOP2dJSnyhP4puJN/QPRyx4YkBEEUxib422n2XzPqDEHjQQqazoYoADdAm5vE15+dAQ==",
"hasInstallScript": true,
"dependencies": {
"bindings": "^1.5.0",
diff --git a/nodejs/package.json b/nodejs/package.json
index 21050b8..805148b 100644
--- a/nodejs/package.json
+++ b/nodejs/package.json
@@ -1,16 +1,18 @@
{
- "module": "nodenext",
- "type": "module",
"engines": {
"node": ">=21.6.2"
},
+ "dependencies": {
+ "@types/better-sqlite3": "^7.6.10",
+ "better-sqlite3": "^9.6.0"
+ },
"devDependencies": {
"esrun": "^3.2.26",
"typescript": "^5.4.3",
"typescript-language-server": "^4.3.3"
},
- "dependencies": {
- "@types/better-sqlite3": "^7.6.9",
- "better-sqlite3": "^9.4.3"
- }
+ "module": "nodenext",
+ "name": "spacetraders",
+ "type": "module",
+ "version": "0.0.1"
}