Skip to content

Commit

Permalink
#381 Support upcoming contract "square root"
Browse files Browse the repository at this point in the history
  • Loading branch information
alainbryden committed Oct 19, 2024
1 parent ed4bc82 commit ebc280d
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 71 deletions.
12 changes: 8 additions & 4 deletions Tasks/contractor.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { getFilePath, getNsDataThroughFile, disableLogs, scanAllServers } from '../helpers.js'
import { instanceCount, getFilePath, getNsDataThroughFile, disableLogs } from '../helpers.js'
const scriptSolver = getFilePath("/Tasks/contractor.js.solver.js");

/** @param {NS} ns **/
export async function main(ns) {
// Prevent multiple instances of this script from being started
if (await instanceCount(ns, "home", false, false) > 1)
return log(ns, 'Another instance is already running. Shutting down...');

disableLogs(ns, ["scan"]);
ns.print("Getting server list...");
const servers = scanAllServers(ns);
const servers = await getNsDataThroughFile(ns, 'scanAllServers(ns)');
ns.print(`Got ${servers.length} servers. Searching for contracts on each...`);
// Retrieve all contracts and convert them to objects with the required information to solve
const contractsDb = servers.map(hostname => ({ hostname, contracts: ns.ls(hostname, '.cct') }))
Expand All @@ -20,9 +24,9 @@ export async function main(ns) {
let contractsDictCommand = async (command, tempName) => await getNsDataThroughFile(ns,
`Object.fromEntries(JSON.parse(ns.args[0]).map(c => [c.contract, ${command}]))`, tempName, [serializedContractDb]);
let dictContractTypes = await contractsDictCommand('ns.codingcontract.getContractType(c.contract, c.hostname)', '/Temp/contract-types.txt');
let dictContractData = await contractsDictCommand('ns.codingcontract.getData(c.contract, c.hostname)', '/Temp/contract-data.txt');
let dictContractDataStrings = await contractsDictCommand('JSON.stringify(ns.codingcontract.getData(c.contract, c.hostname), jsonReplacer)', '/Temp/contract-data-stringified.txt');
contractsDb.forEach(c => c.type = dictContractTypes[c.contract]);
contractsDb.forEach(c => c.data = dictContractData[c.contract]);
contractsDb.forEach(c => c.dataJson = dictContractDataStrings[c.contract]);

// Let this script die to free up ram, and start up a new script (after a delay) that will solve all these contracts using the minimum ram footprint of 11.6 GB
ns.run(getFilePath('/Tasks/run-with-delay.js'), { temporary: true }, scriptSolver, 1, JSON.stringify(contractsDb));
Expand Down
171 changes: 108 additions & 63 deletions Tasks/contractor.js.solver.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { jsonReviver } from '../helpers.js'
const fUnsolvedContracts = '/Temp/unsolved-contracts.txt'; // A global, persistent array of contracts we couldn't solve, so we don't repeatedly log about them.

let heartbeat = null;

//Silly human, you can't import a typescript module into a javascript (wouldn't that be slick though?)
//import { codingContractTypesMetadata } from 'https://raw.githubusercontent.com/danielyxie/bitburner/master/src/data/codingcontracttypes.ts'

Expand All @@ -9,75 +12,91 @@ const fUnsolvedContracts = '/Temp/unsolved-contracts.txt'; // A global, persiste
/** @param {NS} ns **/
export async function main(ns) {
if (ns.args.length < 1)
ns.tprint('Contractor solver was incorrectly invoked without arguments.')
let contractsDb = JSON.parse(ns.args[0]);
const fContents = ns.read(fUnsolvedContracts);
const notified = fContents ? JSON.parse(fContents) : [];

// Don't spam toast notifications and console messages if there are more than 20 contracts to solve:
const quietSolve = contractsDb.length > 20;
let failureCount = 0;
if (quietSolve) {
const message = `Welcome back. There are ${contractsDb.length} to solve, so we won't generate a notification for each.`
ns.toast(message, 'success');
ns.tprint(message);
}

for (const contractInfo of contractsDb) {
const answer = findAnswer(contractInfo)
let notice = null;
if (answer != null) {
let solvingResult = false;
try {
solvingResult = ns.codingcontract.attempt(answer, contractInfo.contract, contractInfo.hostname, { returnReward: true })
if (solvingResult) {
if (!quietSolve) {
const message = `Solved ${contractInfo.contract} on ${contractInfo.hostname} (${contractInfo.type}). Reward: ${solvingResult}`;
ns.toast(message, 'success');
ns.tprint(message);
ns.tprint('Contractor solver was incorrectly invoked without arguments.');

// Hack: Use global memory to avoid multiple instances running concurrently (without paying for ns.ps)
if (heartbeat != null) // If this variable is set, another instance is likely running!
if (performance.now() - heartbeat <= 1000 * 60) // If last start was more 1 minute ago, assume it blew up and isn't actually still running
return ns.print("WARNING: Another contractor appears to already be running. Ignoring request.");
heartbeat = performance.now();

try {
let contractsDb = JSON.parse(ns.args[0]);
const fContents = ns.read(fUnsolvedContracts);
const notified = fContents ? JSON.parse(fContents) : [];

// Don't spam toast notifications and console messages if there are more than 20 contracts to solve:
const quietSolve = contractsDb.length > 20;
let failureCount = 0;
if (quietSolve) {
const message = `Welcome back. There are ${contractsDb.length} to solve, so we won't generate a notification for each.`
ns.toast(message, 'success');
ns.tprint(message);
}

for (const contractInfo of contractsDb) {
heartbeat = performance.now();
const answer = findAnswer(contractInfo)
let notice = null;
if (answer != null) {
let solvingResult = false;
try {
solvingResult = ns.codingcontract.attempt(answer, contractInfo.contract, contractInfo.hostname, { returnReward: true })
if (solvingResult) {
if (!quietSolve) {
const message = `Solved ${contractInfo.contract} on ${contractInfo.hostname} (${contractInfo.type}). Reward: ${solvingResult}`;
ns.toast(message, 'success');
ns.tprint(message);
}
} else {
notice = `ERROR: Wrong answer for contract type "${contractInfo.type}" (${contractInfo.contract} on ${contractInfo.hostname}):` +
`\nIncorrect Answer Given: ${JSON.stringify(answer)}`;
}
} catch (err) {
failureCount++;
let errorMessage = typeof err === 'string' ? err : err.message || JSON.stringify(err);
if (err?.stack) errorMessage += '\n' + err.stack;
notice = `ERROR: Attempt to solve contract raised an error. (Answer Given: ${JSON.stringify(answer)})\n"${errorMessage}"`;
// Suppress errors about missing contracts. This can happen if this script gets while another instance is already running.
if (errorMessage.indexOf("Cannot find contract") == -1) {
ns.print(notice); // Still log it to the terminal in case we're debugging a fake contract.
notice = null;
}
} else {
notice = `ERROR: Wrong answer for contract type "${contractInfo.type}" (${contractInfo.contract} on ${contractInfo.hostname}):` +
`\nIncorrect Answer Given: ${JSON.stringify(answer)}`;
}
} catch (err) {
failureCount++;
let errorMessage = typeof err === 'string' ? err : err.message || JSON.stringify(err);
if (err?.stack) errorMessage += '\n' + err.stack;
// Ignore errors about missing contracts. This can happen if this script gets while another instance is already running.
if (errorMessage.indexOf("Cannot find contract") == -1)
notice = `ERROR: Attemt to solve contract raised an error. (Answer Given: ${JSON.stringify(answer)})\n"${errorMessage}"`;
}
} else {
notice = `WARNING: No solver available for contract type "${contractInfo.type}"`;
}
if (notice) {
if (!notified.includes(contractInfo.contract) && !quietSolve) {
ns.tprint(notice + `\nContract Info: ${JSON.stringify(contractInfo)}`)
ns.toast(notice, 'warning');
notified.push(contractInfo.contract)
}
// Always print errors to scripts own tail window
ns.print(notice + `\nContract Info: ${JSON.stringify(contractInfo)}`);
}
await ns.sleep(10)
} else {
notice = `WARNING: No solver available for contract type "${contractInfo.type}"`;
}
if (notice) {
if (!notified.includes(contractInfo.contract) && !quietSolve) {
ns.tprint(notice + `\nContract Info: ${JSON.stringify(contractInfo)}`)
ns.toast(notice, 'warning');
notified.push(contractInfo.contract)
}
// Always print errors to scripts own tail window
ns.print(notice + `\nContract Info: ${JSON.stringify(contractInfo)}`);
}
await ns.sleep(10)
}
// Keep tabs of failed contracts
if (notified.length > 0)
await ns.write(fUnsolvedContracts, JSON.stringify(notified), "w");
// Let the user know when we're done solving a large number of contracts.
if (quietSolve) {
const message = `Done solving ${contractsDb.length}. ${contractsDb.length - failureCount} succeeded, and ${failureCount} failed. See tail logs for errors.`
if (failureCount > 0)
ns.tail();
ns.toast(message, 'success');
ns.tprint(message);
}
}
// Keep tabs of failed contracts
if (notified.length > 0)
await ns.write(fUnsolvedContracts, JSON.stringify(notified), "w");
// Let the user know when we're done solving a large number of contracts.
if (quietSolve) {
const message = `Done solving ${contractsDb.length}. ${contractsDb.length - failureCount} succeeded, and ${failureCount} failed. See tail logs for errors.`
if (failureCount > 0)
ns.tail();
ns.toast(message, 'success');
ns.tprint(message);
finally {
heartbeat = null; // Signal that we're no longer running in case another contractor wants to start running.
}
}

function findAnswer(contract) {
const codingContractSolution = codingContractTypesMetadata.find((codingContractTypeMetadata) => codingContractTypeMetadata.name === contract.type)
return codingContractSolution ? codingContractSolution.solver(contract.data) : null;
return codingContractSolution ? codingContractSolution.solver(JSON.parse(contract.dataJson, jsonReviver)) : null;
}

function convert2DArrayToString(arr) {
Expand Down Expand Up @@ -879,7 +898,6 @@ const codingContractTypesMetadata = [{
return cipher;
}
},

{
name: "Encryption II: Vigenère Cipher",
solver: function (data) {
Expand All @@ -894,6 +912,33 @@ const codingContractTypesMetadata = [{
.join("");
return cipher;
}
},
{
name: "Square Root",
/** Uses the Newton-Raphson method to iteratively improve the guess until the answer is found.
* @param {bigint} n */
solver: function (n) {
const two = BigInt(2);
if (n < two) return n; // Square root of 1 is 1, square root of 0 is 0
let root = n / two; // Initial guess
let x1 = (root + n / root) / two;
while (x1 < root) {
root = x1;
x1 = (root + n / root) / two;
}
// That's it, solved! At least, we've converged an an answer which should be as close as we can get (might be off by 1)
// We want the answer to the "nearest integer". Check the answer on either side of the one we converged on to see what's closest
const bigAbs = (x) => x < 0n ? -x : x; // There's no Math.abs where we're going...
let absDiff = bigAbs(root * root - n); // How far off we from the perfect square root
if (absDiff == 0n) return root; // Note that this coding contract doesn't guarantee there's an exact integer square root
else if (absDiff > bigAbs((root - 1n) * (root - 1n) - n)) root = root - 1n; // Do we get a better answer by subtracting 1?
else if (absDiff > bigAbs((root + 1n) * (root + 1n) - n)) root = root + 1n; // Do we get a better answer by adding 1?
// Sanity check: We should be able to tell if we got this right without wasting a guess. Adding/Subtracting 1 should now always be worse

Check failure on line 936 in Tasks/contractor.js.solver.js

View workflow job for this annotation

GitHub Actions / woke

`Sanity` may be insensitive, use `confidence`, `quick check`, `coherence check` instead
absDiff = bigAbs(root * root - n);
if (absDiff > bigAbs((root - 1n) * (root - 1n) - n) ||
absDiff > bigAbs((root + 1n) * (root + 1n) - n))
throw new Error(`Square Root did not converge. Arrived at answer:\n${root} - which when squared, gives:\n${root * root} instead of\n${n}`);
return root.toString();
}
}

]
30 changes: 26 additions & 4 deletions helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const memorySuffixes = ["GB", "TB", "PB", "EB"];

/** Formats some RAM amount as a round number of GB/TB/PB/EB with thousands separators e.g. `1.028 TB` */
export function formatRam(num, printGB) {
if(printGB) {
if (printGB) {
return `${Math.round(num).toLocaleString('en')} GB`;
}
let idx = Math.floor(Math.log10(num) / 3) || 0;
Expand Down Expand Up @@ -223,7 +223,26 @@ export async function getNsDataThroughFile_Custom(ns, fnRun, command, fName = nu
`\nThe script was likely passed invalid arguments. Please post a screenshot of this error on discord.`),
maxRetries, retryDelayMs, undefined, verbose, verbose, silent);
if (verbose) log(ns, `Read the following data for command ${command}:\n${fileData}`);
return JSON.parse(fileData); // Deserialize it back into an object/array and return
return JSON.parse(fileData, jsonReviver); // Deserialize it back into an object/array and return
}

/** Allows us to serialize types not normally supported by JSON.serialize */
export function jsonReplacer(key, value) {
if (typeof value === 'bigint') {
return {
type: 'bigint',
value: value.toString()
};
} else {
return value;
}
}

/** Allows us to deserialize special values created by the above jsonReplacer */
export function jsonReviver(key, value) {
if (value && value.type == 'bigint')
return BigInt(value.value);
return value;
}

/** Evaluate an arbitrary ns command by writing it to a new script and then running or executing it.
Expand Down Expand Up @@ -268,8 +287,10 @@ export async function runCommand_Custom(ns, fnRun, command, fileName, args = [],
if (!Array.isArray(args)) throw new Error(`args specified were a ${typeof args}, but an array is required.`);
if (!verbose) disableLogs(ns, ['sleep']);
// Auto-import any helpers that the temp script attempts to use
const required = getExports(ns).filter(e => command.includes(`${e}(`));
let script = (required.length > 0 ? `import { ${required.join(", ")} } from 'helpers.js'\n` : '') +
let importFunctions = getExports(ns).filter(e => command.includes(`${e}`)) // Check if the script includes the name of any functions
// To avoid false positives, narrow these to "whole word" matches (no alpha characters on either side)
.filter(e => new RegExp(`(^|[^\\w])${e}([^\\w]|\$)`).test(command));
let script = (importFunctions.length > 0 ? `import { ${importFunctions.join(", ")} } from 'helpers.js'\n` : '') +
`export async function main(ns) { ${command} }`;
fileName = fileName || getDefaultCommandFileName(command, '.js');
if (verbose)
Expand Down Expand Up @@ -604,6 +625,7 @@ async function getHardCodedBitNodeMultipliers(ns, fnGetNsDataThroughFile) {
}

/** Returns the number of instances of the current script running on the specified host.
* Uses ram-dodging (which costs 1GB for ns.run if you aren't already using it.
* @param {NS} ns The nestcript instance passed to your script's main entry point
* @param {string} onHost - The host to search for the script on
* @param {boolean} warn - Whether to automatically log a warning when there are more than other running instances
Expand Down

0 comments on commit ebc280d

Please sign in to comment.