diff --git a/Tasks/contractor.js b/Tasks/contractor.js index 78af7df5..d247861b 100644 --- a/Tasks/contractor.js +++ b/Tasks/contractor.js @@ -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') })) @@ -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)); diff --git a/Tasks/contractor.js.solver.js b/Tasks/contractor.js.solver.js index fa367ed6..bbc33c96 100644 --- a/Tasks/contractor.js.solver.js +++ b/Tasks/contractor.js.solver.js @@ -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' @@ -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) { @@ -879,7 +898,6 @@ const codingContractTypesMetadata = [{ return cipher; } }, - { name: "Encryption II: Vigenère Cipher", solver: function (data) { @@ -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 + 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(); + } } - ] \ No newline at end of file diff --git a/helpers.js b/helpers.js index 459eb431..5afeee3a 100644 --- a/helpers.js +++ b/helpers.js @@ -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; @@ -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. @@ -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) @@ -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