summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorJulien Dessaux2023-05-14 01:50:19 +0200
committerJulien Dessaux2023-05-14 01:53:57 +0200
commitefdf50a55a32c18c3563b883563f271531a6c38b (patch)
tree07343aa025294e93ea9207f0da2e437f167af8cd /lib
parentInitial import (diff)
downloadspacetraders-efdf50a55a32c18c3563b883563f271531a6c38b.tar.gz
spacetraders-efdf50a55a32c18c3563b883563f271531a6c38b.tar.bz2
spacetraders-efdf50a55a32c18c3563b883563f271531a6c38b.zip
Implemented a basic extraction loop
Diffstat (limited to 'lib')
-rw-r--r--lib/agent.js72
-rw-r--r--lib/api.js76
-rw-r--r--lib/priority_queue.js39
-rw-r--r--lib/ships.js35
4 files changed, 222 insertions, 0 deletions
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});
+}