From 19107b153e9e3a5e4a758b9db6a867c031467320 Mon Sep 17 00:00:00 2001 From: Julien Dessaux Date: Fri, 26 May 2023 00:04:16 +0200 Subject: Automated the extraction and delivery loop for a ship --- lib/agent.js | 187 +++++++++++++++++++++++++---------------------------------- lib/api.js | 11 +++- lib/ships.js | 6 +- 3 files changed, 92 insertions(+), 112 deletions(-) (limited to 'lib') diff --git a/lib/agent.js b/lib/agent.js index 69e64a5..18dbac3 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -4,114 +4,85 @@ import * as contracts from './contracts.js'; import * as ships from './ships.js'; import * as systems from './systems.js'; -// This starts a delivery loop with a ship which ends when the delivery is fullfilled -// ctx must must have multiple attributes: `ship`, `contract` (with the contractId), `good`, `destination` (the delivery waypoint), `field` (the asteroid field waypoint). The ship needs to be orbiting the asteroid field. -//export function deliver(ctx, response) { -// let next = {...ctx}; -// next.action = deliver; // calling ourselves after the action is a good default -// if (response !== undefined) { -// if (response.error !== undefined) { -// console.log("ctx:", JSON.stringify(ctx, null, 2)); -// console.log("response:", JSON.stringify(response, null, 2)); -// switch(response.error.code) { -// case 4000: // ship is on cooldown -// setTimeout(deliver, response.error.data.cooldown.remainingSeconds * 1000, ctx); -// return; -// case 4228: // ship cannot extract because it is full. Running the ship inventory function to list the cargo so that we know if we need to sell -// ships.ship({ship: ctx.ship, next: next}); -// return; -// default: // yet unhandled error -// 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 ${good.units} of ${ctx.good}!`); -// next.units = good.units; // set the unit property so that we know how much to deliver -// ships.navigate({ship: ctx.ship, waypoint: ctx.destination, next: next}); -// } -// 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: deliver, ship: ctx.ship, good: ctx.good}); -// api.chain(actions); -// return; -// } else if (response.data.nav !== undefined) { // we are moving -// const delay = new Date(response.data.nav.route.arrival) - new Date(); -// console.log(new Date(response.data.nav.route.arrival), new Date(), delay); -// if (delay > 0) { // the ship did not arrive yet, we wait -// setTimeout(deliver, delay+1000, ctx, response); -// return; -// } -// if (response.data.nav.route.destination.symbol === ctx.destination) { // we arrived to the delivery point -// api.chain([{ action: ships.dock, ship: ctx.ship }, -// { action: ships.refuel, ship: ctx.ship }, -// { action: ships.deliver, contract: ctx.contract, ship: ctx.ship, good: ctx.good, units: ctx.units }, -// { action: ships.navigate, ship: ctx.ship, waypoint: ctx.field, next: next }]); -// return; -// } else if (next.units !== undefined) { // we arrived to the asteroid field -// delete next.units; // remove the unit property -// api.chain([{ action: ships.dock, ship: ctx.ship }, -// { action: ships.refuel, ship: ctx.ship }, -// { action: ships.orbit, ship: ctx.ship, next: next }]); // TODO we are on cooldown and cannot mine right now -// return; -// } -// } else { // we need to mine more -// if (response.data.cooldown) { // we are on cooldown, call ourselves again in a moment -// setTimeout(deliver, response.data.cooldown.remainingSeconds * 1000, ctx); -// return; -// } -// } -// } -// ships.extract({ship: ctx.ship, good: ctx.good, next: next}); -//} -// -//// 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`. The ship needs to be orbiting an asteroid field -//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 }}); -//} +export async function auto(ctx) { + let ship = await ships.ship({ship: ctx.ship}); + // Fetch our contracts in the system the ship currently is in + let cs = await contracts.contracts(); + cs = cs.data.filter(c => c.terms.deliver[0].destinationSymbol.startsWith(ship.data.nav.systemSymbol)); + if (cs === []) throw `No contract at ${ctx.ship}'s location`; + let contract = cs[0]; + if (!contract.accepted) { + console.log(new Date(), `accepting contract ${contract.id}`); + await contracts.accept({id: contract.id}); + } + const good = contract.terms.deliver[0].tradeSymbol; + const deliveryPoint = contract.terms.deliver[0].destinationSymbol; + const asteroidFields = await systems.type({symbol: ship.data.nav.systemSymbol, type: 'ASTEROID_FIELD'}); + const asteroidField = asteroidFields[0].symbol; + while (true) { + ship = await ships.ship({ship: ctx.ship}); // TODO we should not need to fetch this + // If we are in transit, we wait until we arrive + const delay = new Date(ship.data.nav.route.arrival) - new Date(); + if (delay > 0) await api.sleep(delay); + // Then it depends on where we are + let goodCargo = ship.data.cargo.inventory.filter(i => i.symbol === good)[0]; + // the switch makes this 'resumable' + switch (ship.data.nav.waypointSymbol) { + case asteroidField: + let response = await mineUntilFullOf({good: good, ship: ctx.ship}); + //console.log(`${ctx.ship}'s cargo is full with ${response.units} of ${good}!`); + await ships.navigate({ship: ctx.ship, waypoint: deliveryPoint}); + break; + case deliveryPoint: + await ships.dock({ship: ctx.ship}); + await ships.refuel({ship: ctx.ship}); + console.log(`delivering ${goodCargo.units} of ${good}`); + await contracts.deliver({contract: contract.id, ship: ctx.ship, good: good, units: goodCargo.units }); + await ships.navigate({ship: ctx.ship, waypoint: asteroidField}); + await ships.dock({ship: ctx.ship}); + await ships.refuel({ship: ctx.ship}); + await ships.orbit({ship: ctx.ship}); + break; + default: + throw `where is the ship?`; + } + } +} + +// example ctx { good: 'SILVER_ORE', ship: 'ADYXAX-2' } +// returns the number of units of the good the ship extracted +async function mineUntilFullOf(ctx) { + while(true) { + let response = await mineUntilFull({ship: ctx.ship}); + if (response === null) response = await ships.ship({ship: ctx.ship}); // TODO we should not need to fetch this + let good = response.data.cargo.inventory.filter(i => i.symbol === ctx.good)[0]; + const inventory = response.data.cargo.inventory.filter(i => i.symbol !== ctx.good); + const antimatter = response.data.cargo.inventory.filter(i => i.symbol === 'ANTIMATTER')[0]; + if (good?.units + antimatter?.units >= response.data.cargo.capacity * 0.9) { // > 90% full of the valuable goods + return good.units; + } else { // we are full but need to sell junk + await ships.dock({ship: ctx.ship}); + for (let i=0; i= response.data.cargo.capacity * 0.9) return response; + } +} // This function inits the database in case we have an already registered game export function init(symbol, faction, token) { diff --git a/lib/api.js b/lib/api.js index abcd85d..1519888 100644 --- a/lib/api.js +++ b/lib/api.js @@ -1,3 +1,5 @@ +import * as fs from 'fs'; + import { getToken } from '../database/config.js'; import { PriorityQueue } from './priority_queue.js'; @@ -58,10 +60,15 @@ function send_this(data) { if (data.request.payload !== undefined) { options['body'] = JSON.stringify(data.request.payload); } + fs.writeFileSync('log', JSON.stringify({event: 'send', date: new Date(), data: data}) + '\n', {flag: 'a+'}); fetch(`https://api.spacetraders.io/v2${data.request.endpoint}`, options) .then(response => response.json()) - .then(response => data.resolve(response)) - .catch(err => data.reject(err)); + .then(response => { + fs.writeFileSync('log', JSON.stringify({event: 'response', date: new Date(), data: response}) + '\n', {flag: 'a+'}); + return data.resolve(response);}) + .catch(err => { + fs.writeFileSync('log', JSON.stringify({event: 'error', date: new Date(), data: err}) + '\n', {flag: 'a+'}); + data.reject(err)}); setTimeout(send_next, 500); } diff --git a/lib/ships.js b/lib/ships.js index 9a57c12..6eefd87 100644 --- a/lib/ships.js +++ b/lib/ships.js @@ -5,7 +5,7 @@ export async function extract(ctx) { if (response.error !== undefined) { switch(response.error.code) { case 4000: // ship is on cooldown - await api.sleep(response.error.data.remainingSeconds * 1000); + await api.sleep(response.error.data.cooldown.remainingSeconds * 1000); return extract(ctx); case 4228: // ship is full return null; @@ -31,7 +31,9 @@ export async function dock(ctx) { } export async function navigate(ctx) { - return await api.send({endpoint: `/my/ships/${ctx.ship}/navigate`, method: 'POST', payload: { waypointSymbol: ctx.waypoint }}); + const response = await api.send({endpoint: `/my/ships/${ctx.ship}/navigate`, method: 'POST', payload: { waypointSymbol: ctx.waypoint }}); + const delay = new Date(response.data.nav.route.arrival) - new Date(); + await api.sleep(delay); } export async function orbit(ctx) { -- cgit v1.2.3