Implemented a basic extraction loop
This commit is contained in:
parent
f190aea975
commit
efdf50a55a
12 changed files with 2022 additions and 0 deletions
44
.eslintrc.json
Normal file
44
.eslintrc.json
Normal file
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"env": {
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:node/recommended"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.js"],
|
||||
"rules": {
|
||||
"no-constant-condition": "off"
|
||||
}
|
||||
}
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
"tab"
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"node/no-unsupported-features/es-syntax": [
|
||||
"error",
|
||||
{ "ignores": ["modules"] }
|
||||
]
|
||||
}
|
||||
}
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
spacetraders.db
|
||||
test.db
|
8
database/000_init.sql
Normal file
8
database/000_init.sql
Normal file
|
@ -0,0 +1,8 @@
|
|||
CREATE TABLE schema_version (
|
||||
version INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE config (
|
||||
id INTEGER PRIMARY KEY,
|
||||
key TEXT NOT NULL UNIQUE,
|
||||
value TEXT NOT NULL
|
||||
);
|
23
database/config.js
Normal file
23
database/config.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import db from './db.js';
|
||||
|
||||
const getTokenStatement = db.prepare(`SELECT value from config where key = 'token';`);
|
||||
const registerAgentStatement = db.prepare(`INSERT INTO config(key, value) VALUES ('symbol', ?), ('faction', ?), ('token', ?);`);
|
||||
|
||||
export function getToken() {
|
||||
try {
|
||||
return getTokenStatement.get().value;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function registerAgent(symbol, faction, token) {
|
||||
try {
|
||||
registerAgentStatement.run(symbol, faction, token);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return false;
|
||||
}
|
||||
}
|
29
database/db.js
Normal file
29
database/db.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import fs from 'fs';
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
const allMigrations = [
|
||||
'database/000_init.sql',
|
||||
];
|
||||
|
||||
const db = new Database(
|
||||
process.env.NODE_ENV === 'test' ? 'test.db' : 'spacetraders.db',
|
||||
process.env.NODE_ENV === 'development' ? { verbose: console.log } : null
|
||||
);
|
||||
db.pragma('foreign_keys = ON');
|
||||
|
||||
db.transaction(function migrate() {
|
||||
let version;
|
||||
try {
|
||||
version = db.prepare('SELECT version FROM schema_version').get().version;
|
||||
} catch {
|
||||
version = 0;
|
||||
}
|
||||
if (version === allMigrations.length) return;
|
||||
while (version < allMigrations.length) {
|
||||
db.exec(fs.readFileSync(allMigrations[version], 'utf8'));
|
||||
version++;
|
||||
}
|
||||
db.exec(`DELETE FROM schema_version; INSERT INTO schema_version (version) VALUES (${version});`);
|
||||
})();
|
||||
|
||||
export default db;
|
72
lib/agent.js
Normal file
72
lib/agent.js
Normal file
|
@ -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));
|
||||
}
|
76
lib/api.js
Normal file
76
lib/api.js
Normal file
|
@ -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);
|
||||
}
|
39
lib/priority_queue.js
Normal file
39
lib/priority_queue.js
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
35
lib/ships.js
Normal file
35
lib/ships.js
Normal file
|
@ -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});
|
||||
}
|
84
main.js
Executable file
84
main.js
Executable file
|
@ -0,0 +1,84 @@
|
|||
import * as agent from './lib/agent.js';
|
||||
import * as api from './lib/api.js';
|
||||
import * as ships from './lib/ships.js';
|
||||
|
||||
function usage() {
|
||||
console.log(`contracts\t\t\tList all of your contracts.
|
||||
extract [ship] [good]\t\tExtracts a good from the asteroid field the ship is orbiting and sell locally the unwanted ores until cargo is full
|
||||
init [symbol] [faction] [token]\tinits the database in case we have an already registered game
|
||||
my-agent\t\t\tFetch your agent's details.
|
||||
register [symbol] [faction]\tRegisters your agent then inits the database
|
||||
ships\t\t\tRetrieve all of your ships.`);
|
||||
}
|
||||
|
||||
switch(process.argv[2]) {
|
||||
case 'contracts':
|
||||
api.send({ endpoint: '/my/contracts'});
|
||||
break;
|
||||
case 'extract':
|
||||
if (process.argv[3] !== undefined && process.argv[4] !== undefined) {
|
||||
agent.extract({ship: process.argv[3], good: process.argv[4]});
|
||||
} else {
|
||||
usage();
|
||||
}
|
||||
break;
|
||||
case 'init':
|
||||
if (process.argv[3] !== undefined && process.argv[4] !== undefined && process.argv[5] !== undefined) {
|
||||
agent.init(process.argv[3], process.argv[4], process.argv[5]);
|
||||
} else {
|
||||
usage();
|
||||
}
|
||||
break;
|
||||
case 'my-agent':
|
||||
api.send({endpoint: '/my/agent'});
|
||||
break;
|
||||
case 'register':
|
||||
if (process.argv[3] !== undefined && process.argv[4] !== undefined) {
|
||||
agent.register(process.argv[3], process.argv[4]);
|
||||
} else {
|
||||
usage();
|
||||
}
|
||||
break;
|
||||
case 'ships':
|
||||
api.send({endpoint: '/my/ships'});
|
||||
break;
|
||||
default:
|
||||
// wip and manual actions
|
||||
switch(process.argv[2]) {
|
||||
case 'contract-accept':
|
||||
api.send({endpoint: `/my/contracts/${process.argv[3]}/accept`, method: 'POST'});
|
||||
break;
|
||||
case 'dock':
|
||||
ships.dock({ship: process.argv[3]});
|
||||
break;
|
||||
case 'market':
|
||||
api.send({endpoint: `/systems/${process.argv[3]}/waypoints/${process.argv[4]}/market`});
|
||||
break;
|
||||
case 'navigate':
|
||||
ships.navigate({ship: process.argv[3], waypoint: process.argv[4]});
|
||||
break;
|
||||
case 'orbit':
|
||||
ships.orbit({ship: process.argv[3]});
|
||||
break;
|
||||
case 'purchase':
|
||||
api.send({endpoint: '/my/ships', method: 'POST', payload: {
|
||||
shipType: 'SHIP_MINING_DRONE',
|
||||
waypointSymbol: process.argv[3],
|
||||
}});
|
||||
break;
|
||||
case 'refuel':
|
||||
ships.refuel({ship: process.argv[3]});
|
||||
break;
|
||||
case 'sell':
|
||||
ships.sell({ship: process.argv[3], good: process.argv[4], units: process.argv[5]});
|
||||
break;
|
||||
case 'shipyard':
|
||||
api.send({endpoint: `/systems/${process.argv[3]}/waypoints/${process.argv[4]}/shipyard`});
|
||||
break;
|
||||
case 'waypoints':
|
||||
api.send({endpoint: `/systems/${process.argv[3]}/waypoints?limit=20&page=1`});
|
||||
break;
|
||||
default:
|
||||
usage();
|
||||
}
|
||||
}
|
1598
package-lock.json
generated
Normal file
1598
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
11
package.json
Normal file
11
package.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=18.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^8.3.0",
|
||||
"eslint": "^8.30.0",
|
||||
"eslint-plugin-node": "^11.1.0"
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue