diff options
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" } |