Last active
March 7, 2021 05:05
-
-
Save dcabines/c49bd98ed55d6f156658fd8795c5655b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const root = 'https://api.spacetraders.io'; | |
// utils | |
const timeout = (seconds) => new Promise(resolve => setTimeout(resolve, seconds * 1000)); | |
const sleeper = (seconds) => (x) => new Promise(resolve => setTimeout(() => resolve(x), seconds * 1000)); | |
const userName = () => `user-${new Date().getTime()}-${Math.random()}`; | |
const querySeparator = (path) => path.indexOf('?') === -1 ? '?' : '&'; | |
const get = (path) => fetch(`${root}${path}`).then(x => x.json()).then(saveState).then(saveArchive).then(sleeper(1)); | |
const post = (path, body) => fetch(`${root}${path}`, postOptions(body)).then(x => x.json()).then(saveState).then(saveArchive).then(sleeper(1)); | |
const put = (path, body) => fetch(`${root}${path}`, putOptions(body)).then(x => x.json()).then(saveState).then(saveArchive).then(sleeper(1)); | |
const authGet = (path) => get(`${path}${querySeparator(path)}token=${state.token}`); | |
const authPost = (path, body) => post(`${path}${querySeparator(path)}token=${state.token}`, body); | |
const authPut = (path, body) => put(`${path}${querySeparator(path)}token=${state.token}`, body); | |
const postOptions = (body) => ({ | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify(body) | |
}); | |
const putOptions = (body) => ({ | |
method: 'PUT', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify(body) | |
}); | |
const upsert = (findKey) => (destination, incoming) => { | |
const existing = destination.find(x => findKey(x, incoming)); | |
const others = destination.filter(x => x !== existing); | |
const updated = {...existing, ...incoming }; | |
return [...others, updated]; | |
}; | |
// state | |
const state = {}; | |
const saveState = (data) => Object.assign(state, data); | |
const system = () => state.systems[0]; | |
const locations = () => system().locations; | |
const systemLocation = (symbol) => locations().find(x => x.symbol === symbol); | |
const marketItem = (symbol) => state.planet.marketplace.find(x => x.symbol === symbol); | |
const ship = (shipId) => state.user.ships.find(x => x.id === shipId); | |
const cargo = (shipId, good) => ship(shipId).cargo.find(x => x.good === good); | |
const cargoTotal = (shipId) => ship(shipId).cargo.reduce((p, c) => p + c.quantity, 0); | |
const remainingCargo = (shipId) => ship(shipId).maxCargo - cargoTotal(shipId); | |
const goodQuantity = (shipId, good) => cargo(shipId, good).quantity; | |
const maxAfford = (good, creditReserve) => { | |
const credits = state.user.credits - creditReserve; | |
const price = marketItem(good).pricePerUnit; | |
return credits / price; | |
}; | |
// archive | |
const archive = {}; | |
const planet = (symbol) => archive.planets.find(x => x.symbol === symbol); | |
const unexploredLocations = () => locations().filter(location => !planet(location)); | |
const symbolMatch = (existing, incoming) => existing.symbol === incoming.symbol; | |
const fillHold = (shipId, good, creditReserve) => { | |
const space = remainingCargo(shipId); | |
const price = maxAfford(good, creditReserve); | |
return Math.min(space, price); | |
}; | |
const beginTrip = () => { | |
archive.startCredits = state.user.credits; | |
}; | |
const endTrip = () => { | |
archive.lastProfit = state.user.credits - archive.startCredits; | |
archive.trips = (archive.trips || 0) + 1; | |
archive.totalProfit = (archive.totalProfit || 0) + archive.lastProfit; | |
}; | |
const archiveLocations = () => { | |
if (!state.locations) return; | |
archive.locations = state.locations.reduce( | |
upsert(symbolMatch), | |
archive.locations || [] | |
); | |
}; | |
const archivePlanet = () => { | |
if (!state.planet) return; | |
archive.planets = [state.planet].reduce( | |
upsert(symbolMatch), | |
archive.planets || [] | |
); | |
}; | |
const saveArchive = () => { | |
archiveLocations(); | |
archivePlanet(); | |
}; | |
// report | |
const reportHeader = '/*\\'; | |
const linePrefix = '|*|'; | |
const reportFooter = '\\*/'; | |
const takeLongest = (p, c) => c.length > p ? c.length : p; | |
const longestKey = (array, keyPicker) => array.map(keyPicker).reduce(takeLongest, 0); | |
const sortLocations = (a, b) => a.name < b.name ? -1 : 1; | |
const sortMarket = (a, b) => a.symbol < b.symbol ? -1 : 1; | |
const marketLine = (item) => `${item.symbol} ${item.pricePerUnit} ${item.available}`; | |
const marketReport = (marketplace) => { | |
const header = { | |
symbol: 'name', | |
pricePerUnit: 'price', | |
available: 'available' | |
}; | |
const marketplaceWithHeader = [header, ...marketplace]; | |
const longestSymbol = longestKey(marketplaceWithHeader, x => x.symbol); | |
const longestPrice = longestKey(marketplaceWithHeader, x => x.pricePerUnit.toString()); | |
const longestAvailable = longestKey(marketplaceWithHeader, x => x.available.toString()); | |
const lineData = (item) => ({ | |
symbol: item.symbol.padEnd(longestSymbol, ' '), | |
pricePerUnit: item.pricePerUnit.toString().padStart(longestPrice, ' '), | |
available: item.available.toString().padStart(longestAvailable, ' ') | |
}); | |
return [header, ...marketplace.sort(sortMarket)].map(lineData).map(marketLine); | |
}; | |
const prefixLines = (data) => data.map(x => `${linePrefix} ${x}`); | |
const report = (data) => `${reportHeader}\n${prefixLines(data).join(`\n`)}\n${reportFooter}`; | |
const planetReport = (planet) => [`${planet.name} ${planet.symbol}`, ...marketReport(planet.marketplace)]; | |
const systemReport = () => [].concat(...archive.planets.sort(sortLocations).map(x => [...planetReport(x), ''])); | |
const buyReport = (order) => `Bought ${order.quantity} ${order.good} for ${order.total} at ${order.pricePerUnit} each.`; | |
const sellReport = (order) => `Sold ${order.quantity} ${order.good} for ${order.total}.`; | |
const creditReport = () => `${state.user.credits} credits remaining.`; | |
const flightReport = () => `Flying to ${systemLocation(state.flightPlan.destination).name}.`; | |
const tripReport = () => `Completed trip number ${archive.trips}.`; | |
const profitReport = () => `${archive.lastProfit} profit.` | |
const totalProfitReport = () => `${archive.totalProfit} total profit.`; | |
const availableReport = (good) => `${good} is not available here.`; | |
const poorReport = (good, quantity) => `Cannot afford to buy ${quantity} of ${good}`; | |
// path | |
const user = (path) => `/users/${state.user.username}/${path}`; | |
const game = (path) => `/game/${path}`; | |
// api | |
const getStatus = () => get(game('status')); | |
const createToken = (username) => post(`/users/${username}/token`); | |
const getUser = (username) => authGet(`/users/${username}`); | |
const getLoans = () => authGet(game('loans')); | |
const takeLoan = (type) => authPost(user('loans'), { type }); | |
const payLoan = (loanId) => authPut(`${user('loans')}/${loanId}`); | |
const getShips = () => authGet(game('ships')); | |
const buyShip = (location, type) => authPost(user('ships'), { location, type }); | |
const getSystems = () => authGet(game('systems')); | |
const getLocations = (type) => authGet(game(`systems/OE/locations?type=${type}`)); | |
const getMarket = (location) => authGet(game(`locations/${location}/marketplace`)); | |
const createFlightPlan = (shipId, destination) => authPost(user('flight-plans'), { shipId, destination }); | |
const getFlightPlan = (flightPlanId) => authGet(user(`flight-plans/${flightPlanId}`)); | |
const buyGood = (shipId, good, quantity) => authPost(user('purchase-orders'), { shipId, good, quantity: Math.trunc(quantity) }); | |
const sellGood = (shipId, good, quantity) => authPost(user('sell-orders'), { shipId, good, quantity: Math.trunc(quantity) }); | |
// actions | |
const getAccount = () => getUser(state.user.username); | |
const getMarkets = async() => { | |
for (const ship of state.user.ships) { | |
await getMarket(ship.location); | |
} | |
}; | |
const fly = async(shipId, destination) => { | |
const myShip = ship(shipId); | |
if(myShip.location === destination) return; | |
await createFlightPlan(shipId, destination); | |
await getAccount(); | |
console.log(flightReport()); | |
await timeout(state.flightPlan.timeRemainingInSeconds); | |
await timeout(5); // docking | |
await getMarket(destination); | |
console.log(report(planetReport(state.planet))); | |
}; | |
const buy = async(shipId, good, quantity) => { | |
const item = marketItem(good); | |
if(!item) { | |
console.log(availableReport(good)); | |
return; | |
} | |
const canAfford = state.user.credits / item.pricePerUnit; | |
const toBuy = Math.min(canAfford, item.available, quantity); | |
if(isNaN(toBuy)) debugger; | |
if(toBuy <= 0) return; | |
await buyGood(shipId, good, toBuy); | |
await getAccount(); | |
console.log(report([buyReport(state.order[0]), creditReport()])); | |
}; | |
const sell = async(shipId, good, quantity) => { | |
if(quantity <= 0) return; | |
await sellGood(shipId, good, quantity); | |
await getAccount(); | |
console.log(report([sellReport(state.order[0]), creditReport()])); | |
}; | |
const refuel = async(shipId, wantedFuel) => { | |
const cargoFuel = cargo(shipId, 'FUEL') || { quantity: 0 }; | |
const neededFuel = wantedFuel - cargoFuel.quantity; | |
if(isNaN(neededFuel)) debugger; | |
if(neededFuel <= 0) return; | |
await buy(shipId, 'FUEL', neededFuel); | |
}; | |
const logIn = async(options) => { | |
if(options) { | |
state.token = options.token; | |
state.user = { username: options.username }; | |
} | |
else { | |
await createToken(userName()); | |
} | |
await getAccount(); | |
await getSystems(); | |
console.log('Username', state.user.username); | |
}; | |
const startup = async() => { | |
await takeLoan('STARTUP'); | |
await buyShip('OE-G4', 'JW-MK-I'); | |
await getMarket('OE-G4'); | |
await refuel(state.user.ships[0].id, 30); | |
console.log(report(planetReport(state.planet))); | |
}; | |
const trade = async (options) => { | |
while(true) { | |
const location = ship(options.shipId).location; | |
if(!location) { | |
await getAccount(); | |
continue; | |
} | |
beginTrip(); | |
const inCargo = cargo(options.shipId, options.good); | |
if(!inCargo) { | |
await fly(options.shipId, options.source); | |
await refuel(options.shipId, options.fuel); | |
const toBuy = fillHold(options.shipId, options.good, options.creditReserve); | |
await buy(options.shipId, options.good, toBuy); | |
} | |
await fly(options.shipId, options.destination); | |
const toSell = goodQuantity(options.shipId, options.good); | |
await sell(options.shipId, options.good, toSell); | |
await refuel(options.shipId, options.fuel); | |
endTrip(); | |
const reports = [ | |
tripReport(), | |
profitReport(), | |
totalProfitReport() | |
]; | |
console.log(report(reports)); | |
if(state.user.credits < 1000) { | |
console.log('You\'re broke! Go home.'); | |
return; | |
} | |
} | |
}; | |
const explore = async(options) => { | |
const symbols = unexploredLocations().map(x => x.symbol); | |
for(let i = 0; i < symbols.length; i++) { | |
await refuel(options.shipId, 100); | |
await fly(options.shipId, symbols[i]); | |
} | |
console.log(report(systemReport())); | |
console.log('Exploration complete.'); | |
}; | |
const richMan = async() => { | |
await logIn({ | |
token: 'a1e93722-98d0-4f1f-b747-d1983aef0723', | |
username: 'user-1615001801402-0.5537007097229811' | |
}); | |
const myShip = ship('cklx7v1zc363076t889qtvaa555'); | |
if(myShip.location) { | |
await getMarket(myShip.location); | |
} | |
await trade({ | |
good: 'MACHINERY', | |
source: 'OE-D2', | |
destination: 'OE-G4', | |
fuel: 30, | |
creditReserve: 10000, | |
shipId: 'cklx7v1zc363076t889qtvaa555' | |
}); | |
}; | |
const richExplorer = async() => { | |
await logIn({ | |
token: 'a1e93722-98d0-4f1f-b747-d1983aef0723', | |
username: 'user-1615001801402-0.5537007097229811' | |
}); | |
const myShip = ship('cklygo8wk466961k089bwiotx6u'); | |
if(myShip.location) { | |
await getMarket(myShip.location); | |
} | |
await explore({ | |
shipId: myShip.id | |
}); | |
}; | |
const poorMan = async() => { | |
await logIn(); | |
await startup(); | |
await trade({ | |
good: 'MACHINERY', | |
source: 'OE-G4', | |
destination: 'OE-D2', | |
fuel: 30, | |
creditReserve: 0, | |
shipId: state.user.ships[0].id | |
}); | |
await poorMan(); | |
}; | |
const explorer = async() => { | |
await logIn(); | |
await startup(); | |
await explore({ | |
shipId: state.user.ships[0].id | |
}); | |
}; | |
console.clear(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment