From efdf50a55a32c18c3563b883563f271531a6c38b Mon Sep 17 00:00:00 2001 From: Julien Dessaux Date: Sun, 14 May 2023 01:50:19 +0200 Subject: Implemented a basic extraction loop --- lib/agent.js | 72 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/api.js | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++ lib/priority_queue.js | 39 ++++++++++++++++++++++++++ lib/ships.js | 35 ++++++++++++++++++++++++ 4 files changed, 222 insertions(+) create mode 100644 lib/agent.js create mode 100644 lib/api.js create mode 100644 lib/priority_queue.js create mode 100644 lib/ships.js (limited to 'lib') diff --git a/lib/agent.js b/lib/agent.js new file mode 100644 index 0000000..ca9da0d --- /dev/null +++ b/lib/agent.js @@ -0,0 +1,72 @@ +import { registerAgent } from '../database/config.js'; +import * as api from './api.js'; +import * as ships from './ships.js'; + +// This starts an extraction loop with a ship which ends when the ship's cargo is at least 90% full with only one desired good +// ctx must must have two attributes: `ship` and `good` +export function extract(ctx, response) { + if (response !== undefined) { + if (response.error !== undefined) { + switch(response.error.code) { + case 4000: // ship is on cooldown + setTimeout(extract, response.error.data.cooldown.remainingSeconds * 1000, ctx); + return; + case 4228: // ship is full. Running the ship inventory function to list the cargo so that know if we need to sell + ships.ship({ship: ctx.ship, next:{action: extract, ship: ctx.ship, good: ctx.good}}); + return; + default: + throw response; + } + } + if (response.data.extraction !== undefined && response.data.extraction.yield !== undefined) { // yield won't be defined if we reached this point from an inventory request + console.log(`${ctx.ship}: extracted ${response.data.extraction.yield.units} of ${response.data.extraction.yield.symbol}`); + } + if (response.data.cargo !== undefined && response.data.cargo.capacity * 0.9 <= response.data.cargo.units) { // > 90% full + const good = response.data.cargo.inventory.filter(i => i.symbol === ctx.good)[0]; + const inventory = response.data.cargo.inventory.filter(i => i.symbol !== ctx.good); + if (good?.units >= response.data.cargo.capacity * 0.9) { // > 90% full + console.log(`ship's cargo is full with ${response.data.cargo.units} of ${ctx.good}!`); + return; + } + let actions = [{ action: ships.dock, ship: ctx.ship }]; + inventory.forEach(i => actions.push({action: ships.sell, ship: ctx.ship, good: i.symbol, units: i.units})); + actions.push({action: ships.orbit, ship: ctx.ship}); + actions.push({action: extract, ship: ctx.ship, good: ctx.good}); + api.chain(actions); + return; + } else { // we need to mine more + if (response.data.cooldown) { // we are on cooldown, call ourselves again in a moment + setTimeout(extract, response.data.cooldown.remainingSeconds * 1000, ctx); + return; + } + } + } + ships.extract({ship: ctx.ship, good: ctx.good, next: { action: extract, ship: ctx.ship, good: ctx.good }}); +} + +// This function inits the database in case we have an already registered game +export function init(symbol, faction, token) { + registerAgent(symbol, faction, token); +} + +// This function registers then inits the database +export function register(symbol, faction) { + fetch( + 'https://api.spacetraders.io/v2/register', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + symbol: symbol, + faction: faction, + }), + }) + .then(response => response.json()) + .then(response => { + console.log(JSON.stringify(response, null, 2)); + init(symbol, faction, response.data.token); + }) + .catch(err => console.error(err)); +} diff --git a/lib/api.js b/lib/api.js new file mode 100644 index 0000000..b1cdcb8 --- /dev/null +++ b/lib/api.js @@ -0,0 +1,76 @@ +import { getToken } from '../database/config.js'; +import { PriorityQueue } from './priority_queue.js'; + +let busy = false; // lets us know if we are already sending api requests or not. +let headers = undefined; // a file scope variable so that we only evaluate these once. +let queue = new PriorityQueue(); // a priority queue to hold api calls we want to send, allows for throttling. + +// chain takes an array of actions as argument. For each one it sets the `next` property. +// example action: { +// action: function to call, +// next: optional nested action object, would get overriden by this function, except for the last action, +// ... other attributes as required by the action function (for example ship or waypoing symbol...) +// } +export function chain(actions) { + for(let i=actions.length-1;i>0;--i) { + actions[i-1].next = actions[i]; + } + actions[0].action(actions[0]); +} + +// send takes a data object as argument +// example data: { +// endpoint: the url endpoint to call, +// method: HTTP method for `fetch` call, defaults to 'GET', +// next: optional nested action object, as specified above with the chain function, +// payload: optional json object that will be send along with the request, +// } +export function send(data) { + if (!busy) { + send_this(data); + } else { + queue.enqueue(data, data.priority ? data.priority : 10); + } +} + +function send_next() { + if (queue.isEmpty()) { + busy = false; + } else { + send_this(queue.dequeue().element); + } +} + +function send_this(data) { + if (headers === undefined) { + const token = getToken(); + if (token === null) { + throw 'Could not get token from the database. Did you init or register yet?'; + } + headers = { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }; + } + let options = { + headers: headers, + }; + if (data.method !== undefined) { + options['method'] = data.method; + } + if (data.payload !== undefined) { + options['body'] = JSON.stringify(data.payload); + } + busy = true; + fetch(`https://api.spacetraders.io/v2${data.endpoint}`, options) + .then(response => response.json()) + .then(response => { + if (data.next !== undefined) { // if we have a next action, call it now + data.next.action(data.next, response); + } else { // otherwise use this debug action + console.log(JSON.stringify(response, null, 2)); + } + }) + .catch(err => console.error(err)); + setTimeout(send_next, 500); +} diff --git a/lib/priority_queue.js b/lib/priority_queue.js new file mode 100644 index 0000000..da526b2 --- /dev/null +++ b/lib/priority_queue.js @@ -0,0 +1,39 @@ +export class QElement { + constructor(element, priority) { + this.element = element; + this.priority = priority; + } +} + +export class PriorityQueue { + constructor(elt) { + this.items = []; + if (elt !== undefined) { + this.enqueue(elt, 0); + } + } + + enqueue(element, priority) { + let qElement = new QElement(element, priority); + + for (let i = 0; i < this.items.length; ++i) { + if (this.items[i].priority > qElement.priority) { + this.items.splice(i, 0, qElement); + return; + } + } + this.items.push(qElement); + } + dequeue() { + return this.items.shift(); // we would use pop to get the highest priority, shift() gives us the lowest priority + } + front() { + return this.items[0]; + } + rear() { + return this.items[this.items.length - 1]; + } + isEmpty() { + return this.items.length === 0; + } +} diff --git a/lib/ships.js b/lib/ships.js new file mode 100644 index 0000000..e536e73 --- /dev/null +++ b/lib/ships.js @@ -0,0 +1,35 @@ +import * as api from './api.js'; + +export function extract(ctx) { + console.log(`${ctx.ship}: extracting`); + api.send({endpoint: `/my/ships/${ctx.ship}/extract`, method: 'POST', next: ctx.next}); +} + +export function dock(ctx) { + console.log(`${ctx.ship}: docking`); + api.send({endpoint: `/my/ships/${ctx.ship}/dock`, method: 'POST', next: ctx.next}); +} + +export function navigate(ctx) { + console.log(`${ctx.ship}: navigating to ${ctx.waypoint}`); + api.send({endpoint: `/my/ships/${ctx.ship}/navigate`, method: 'POST', payload: { waypointSymbol: ctx.waypoint }, next: ctx.next}); +} + +export function orbit(ctx) { + console.log(`${ctx.ship}: orbiting`); + api.send({endpoint: `/my/ships/${ctx.ship}/orbit`, method: 'POST', next: ctx.next}); +} + +export function refuel(ctx) { + console.log(`${ctx.ship}: refueling`); + api.send({endpoint: `/my/ships/${ctx.ship}/refuel`, method: 'POST', next: ctx.next}); +} + +export function sell(ctx) { + console.log(`${ctx.ship}: selling ${ctx.units} of ${ctx.good}`); + api.send({endpoint: `/my/ships/${ctx.ship}/sell`, method: 'POST', payload: { symbol: ctx.good, units: ctx.units }, next: ctx.next}); +} + +export function ship(ctx) { + api.send({endpoint: `/my/ships/${ctx.ship}`, next: ctx.next}); +} -- cgit v1.2.3