From bca94e1660bcab9021588767cb860528b82891f8 Mon Sep 17 00:00:00 2001 From: Alain Bryden <2285037+alainbryden@users.noreply.github.com> Date: Sun, 1 Dec 2024 01:00:05 -0400 Subject: [PATCH] Improvements to spending hashes - Autopilot can switch servers to boost as better servers become unlocked - Autopilot will kill and restart spend-hacknet-hashes to stop spending on Reduce_Minimum_Security once min security drops below 2. - Fixed spend-hacknet-hashes shutting down if it can't afford any new upgrades and the current nodes are at max hash capacity, but new notes could still be purchased. - Improved toast message when we cannot afford to upgrade hashes, and encourages the user to violate the budget / reserve if they deem it worthwhile. --- autopilot.js | 38 ++++++++++++++++++++++++++++--------- spend-hacknet-hashes.js | 42 +++++++++++++++++++++++++---------------- 2 files changed, 55 insertions(+), 25 deletions(-) diff --git a/autopilot.js b/autopilot.js index 71765e78..98102064 100644 --- a/autopilot.js +++ b/autopilot.js @@ -93,7 +93,8 @@ export async function main(ns) { let lastScriptsCheck = 0; // Last time we got a listing of all running scripts let homeRam = 0; // Amount of RAM on the home server, last we checked let killScripts = []; // A list of scripts flagged to be restarted due to changes in priority - let dictOwnedSourceFiles = (/**@returns{{[k: number]: number;}}*/() => [])(); + let dictOwnedSourceFiles = (/**@returns{{[k: number]: number;}}*/() => [])(); // Player owned source files + let dictServerHackReqs = (/**@returns{{[serverName: string]: number;}}*/() => undefined)(); // Hacking requirement for each server let unlockedSFs = [], nextBn = 0; // Info for the current bitnode let resetInfo = (/**@returns{ResetInfo}*/() => undefined)(); // Information about the current bitnode let bitNodeMults = (/**@returns{BitNodeMultipliers}*/() => undefined)(); // bitNode multipliers that can be automatically determined after SF-5 @@ -432,6 +433,7 @@ export async function main(ns) { /** Helper to get the first instance of a running script by name. * @param {NS} ns + * @param {string} baseScriptName The name of a script (before applying getFilePath) * @param {ProcessInfo[]} runningScripts - (optional) Cached list of running scripts to avoid repeating this expensive request * @param {(value: ProcessInfo, index: number, array: ProcessInfo[]) => unknown} filter - (optional) Filter the list of processes beyond just matching on the script name */ function findScriptHelper(baseScriptName, runningScripts, filter = null) { @@ -459,7 +461,8 @@ export async function main(ns) { if (lastScriptsCheck > Date.now() - options['interval-check-scripts']) return; lastScriptsCheck = Date.now(); const runningScripts = await getRunningScripts(ns); // Cache the list of running scripts for the duration - const findScript = (baseScriptName, filter = null) => findScriptHelper(baseScriptName, runningScripts, filter); + const findScript = /** @param {(value: ProcessInfo, index: number, array: ProcessInfo[]) => unknown} filter @returns {ProcessInfo} */ + (baseScriptName, filter = null) => findScriptHelper(baseScriptName, runningScripts, filter); // Kill any scripts that were flagged for restart while (killScripts.length > 0) @@ -486,21 +489,38 @@ export async function main(ns) { } // Spend hacknet hashes on our boosting best hack-income server once established - const spendingHashesOnHacking = findScript('spend-hacknet-hashes.js', s => s.args.includes("--spend-on-server")) - if ((9 in unlockedSFs) && !spendingHashesOnHacking && getTimeInAug() >= options['time-before-boosting-best-hack-server'] + const existingSpendHashesProc = findScript('spend-hacknet-hashes.js', s => s.args.includes("--spend-on-server")) + if ((9 in unlockedSFs) && getTimeInAug() >= options['time-before-boosting-best-hack-server'] && 0 != bitNodeMults.ScriptHackMoney * bitNodeMults.ScriptHackMoneyGain) // No point in boosting hack income if it's scaled to 0 in the current BN { const strServerIncomeInfo = ns.read('/Temp/analyze-hack.txt'); // HACK: Steal this file that Daemon also relies on if (strServerIncomeInfo) { const incomeByServer = JSON.parse(strServerIncomeInfo); - const dictServerHackReqs = await getNsDataThroughFile(ns, 'Object.fromEntries(ns.args.map(server => [server, ns.getServerRequiredHackingLevel(server)]))', - '/Temp/servers-hack-req.txt', incomeByServer.map(s => s.hostname)); + dictServerHackReqs ??= await getNsDataThroughFile(ns, 'Object.fromEntries(ns.args.map(server => [server, ns.getServerRequiredHackingLevel(server)]))', + '/Temp/getServerRequiredHackingLevel-all.txt', incomeByServer.map(s => s.hostname)); const [bestServer, gain] = incomeByServer.filter(s => dictServerHackReqs[s.hostname] <= player.skills.hacking) .reduce(([bestServer, bestIncome], target) => target.gainRate > bestIncome ? [target.hostname, target.gainRate] : [bestServer, bestIncome], [null, -1]); if (bestServer && gain > 1) { - log(ns, `Identified that the best hack income server is ${bestServer} worth ${formatMoney(gain)}/sec.`) - launchScriptHelper(ns, 'spend-hacknet-hashes.js', - ["--liquidate", "--spend-on", "Increase_Maximum_Money", "--spend-on", "Reduce_Minimum_Security", "--spend-on-server", bestServer]); + // Check whether we should be spending hashes to reduce minimum security + const serverMinSecurity = await getNsDataThroughFile(ns, 'ns.getServerMinSecurityLevel(ns.args[0])', null, [bestServer]); + const shouldReduceMinSecurity = serverMinSecurity > 2; // Each purchase reduces by 2%. Can't go below 1, but not worth the cost to keep going below 2. + // If we were already spending hashes to boost a server, check to see if things have changed + if (existingSpendHashesProc) { + const currentBoostTarget = existingSpendHashesProc.args[1 + existingSpendHashesProc.args.indexOf("--spend-on-server")]; + const isReducingSecurity = existingSpendHashesProc.args.includes("Reduce_Minimum_Security"); + if (currentBoostTarget != bestServer || isReducingSecurity != shouldReduceMinSecurity) { + log(ns, `Killing a prior spend-hacknet-hashes.js process targetting ${currentBoostTarget} because ` + + (currentBoostTarget != bestServer ? `The new best income server is ${bestServer}.` : 'We no longer need to reduce minimum security.'), true); + await killScript(ns, 'spend-hacknet-hashes.js', null, existingSpendHashesProc); + existingSpendHashesProc = false; + } + } + if (!existingSpendHashesProc) { // + log(ns, `Identified that the best hack income server is ${bestServer} worth ${formatMoney(gain)}/sec.`); + const spendHashesArgs = ["--liquidate", "--spend-on-server", bestServer, "--spend-on", "Increase_Maximum_Money"]; + if (shouldReduceMinSecurity) spendHashesArgs.push("--spend-on", "Reduce_Minimum_Security"); + launchScriptHelper(ns, 'spend-hacknet-hashes.js', spendHashesArgs); + } } else if (gain <= 1) log_once(ns, `INFO: Hack income is currently too severely penalized to merit launching spend-hacknet-hashes.js to boost servers.`); else diff --git a/spend-hacknet-hashes.js b/spend-hacknet-hashes.js index d7970e3e..945b7567 100644 --- a/spend-hacknet-hashes.js +++ b/spend-hacknet-hashes.js @@ -69,7 +69,7 @@ export async function main(ns) { let lastHashBalance = -1; // Balance of hashes last time we woke up. If unchanged, we go back to sleep quickly (game hasn't ticked) - let capacityMaxed = false; // Flag indicating we've maxed our hash capacity, to avoid repeatedly logging this fact. + let notifiedMaxCapacity = false; // Flag indicating we've maxed our hash capacity, to avoid repeatedly logging this fact. // Function determines the current cheapest upgrade of all the upgrades we wish to keep purchasing const getMinCost = spendActions => Math.min(...spendActions.map(p => ns.hacknet.hashCost(p))); // Helper to format hashes in log message @@ -83,7 +83,7 @@ export async function main(ns) { let currentHashes = ns.hacknet.numHashes(); // Go back to sleep if the game hasn't ticket yet (given us more hashes) since our last loop. if (lastHashBalance != capacity && lastHashBalance == currentHashes) continue; - log(ns, `INFO: Waking up, last hash balance has changed from ${lastHashBalance} to ${currentHashes}`); + //log(ns, `INFO: Waking up, last hash balance has changed from ${lastHashBalance} to ${currentHashes}`); // Compute the total income rate of all hacknet nodes. We have to spend faster than this when near capacity. const nodes = ns.hacknet.numNodes(); if (nodes == 0) { @@ -158,7 +158,7 @@ export async function main(ns) { `so we cannot increase our hash capacity. ${capacityMessage}`, false, remaining < hashesEarnedNextTick ? 'warning' : undefined); // Only warn via toast if we are running out of capacity } else { // Otherwise, try to upgrade hacknet capacity so we can save up for more upgrades - if (!capacityMaxed) // Log that we want to increase hash capacity (unless we've previously seen that we are maxed out) + if (!notifiedMaxCapacity) // Log that we want to increase hash capacity (unless we've previously seen that we are maxed out) log(ns, `INFO: ${capacityMessage}`); let lowestLevel = Number.MAX_SAFE_INTEGER, lowestIndex = null; for (let i = 0; i < nodes; i++) @@ -167,7 +167,8 @@ export async function main(ns) { const nextCacheUpgradeCost = lowestIndex == null ? Number.POSITIVE_INFINITY : ns.hacknet.getCacheUpgradeCost(lowestIndex, 1); const nextNodeCost = ns.hacknet.getPurchaseNodeCost(); const reservedMoney = options['reserve'] ?? Number(ns.read("reserve.txt") || 0); - const spendableMoney = Math.max(0, ns.getServerMoneyAvailable('home') - reservedMoney); + const playerMoney = ns.getServerMoneyAvailable('home'); + const spendableMoney = Math.max(0, playerMoney - reservedMoney); // If it's cheaper to buy a new hacknet node than to upgrade the cache of an existing one, do so if (nextNodeCost < nextCacheUpgradeCost && nextNodeCost < spendableMoney) { if (ns.hacknet.purchaseNode()) @@ -185,23 +186,32 @@ export async function main(ns) { log(ns, `WARNING: spend-hacknet-hashes.js attempted to spend ${formatMoney(nextCacheUpgradeCost)} to upgrade hacknet node ${lowestIndex} hash capacity, ` `but the purchase failed for an unknown reason (despite appearing to have ${formatMoney(spendableMoney)} to spend after reserves.)`, false, 'warning'); } else if (nodes > 0) { + // Prepare a message about our inability to upgrade hash capacity + let message = `Cannot upgrade hash capacity (currently ${formatHashes(capacity)} hashes max). `; + const nextCheapestCacheIncreaseCost = Math.min(nextCacheUpgradeCost, nextNodeCost); + const nextCheapestCacheIncrease = nextNodeCost < nextCacheUpgradeCost ? `buy hacknet node ${nodes + 1}` : `upgrade hacknet node ${lowestIndex} hash capacity`; + if (!Number.isFinite(nextCheapestCacheIncreaseCost)) + message += `Hash Capacity is at its maximum and hacknet server limit is reached.`; + else + message += ` We cannot afford to increase our hash capacity (${formatMoney(nextCheapestCacheIncreaseCost)} to ${nextCheapestCacheIncrease}).` + + (playerMoney < nextCheapestCacheIncreaseCost ? '' : // Don't bother mentioning budget if the cost exceeds all player money + `on our budget of ${formatMoney(spendableMoney)}` + (reservedMoney > 0 ? ` (after respecting reserve of ${formatMoney(reservedMoney)}).` : '.')); + // Include in the message information about what we are trying to spend hashes on const nextPurchaseCost = getMinCost(toBuy); - let message = `Failed to upgrade hash capacity (currently ${formatHashes(capacity)} hashes max) on budget of ${formatMoney(spendableMoney)}.`; - message += (Number.isFinite(nextCacheUpgradeCost) ? ` We cannot afford to increase our hash capacity at this time` : - ` Hash Capacity is at its maximum`) + ` (next cost is ${formatMoney(nextCacheUpgradeCost)}).`; if (nextPurchaseCost > capacity) message += ` We have insufficient hashes to buy any of the desired upgrades (${toBuy.join(", ")}) at our current hash capacity. ` + `The next cheapest purchase costs ${formatHashes(nextPurchaseCost)} hashes.`; - // Log a slightly different message about our failure to upgrade hash capacity depending on the root cause. - if (nextPurchaseCost > capacity && !Number.isFinite(nextCacheUpgradeCost)) - return log(ns, `SUCCESS: We've maxed all purchases. ${message}`); // Shut down, because we won't be able to buy anything further. - else if (!Number.isFinite(nextCacheUpgradeCost)) { - if (!capacityMaxed) // Only inform the user of this the first time it happens. - log(ns, `INFO: ${message}`, true, 'info'); - capacityMaxed = true; // Set a flag to no longer check or inform the user about this + // If we don't have the budget for the upgrade, toast a warning so the user can decide whether they think it worth manually intervening + if (Number.isFinite(nextCheapestCacheIncreaseCost)) { + if (playerMoney > nextCheapestCacheIncreaseCost) + message += ' Feel free to manually purchase this upgrade (despite the reserve/budget) if you deem it worthwhile.' + log(ns, `WARNING: spend-hacknet-hashes.js ${message}`, false, 'warning'); + } else if (nextPurchaseCost > capacity) // If we can't afford anything, and have maxed our hash capacity, we may as well shut down. + return log(ns, `SUCCESS: We've maxed all purchases. ${message}`); // Shut down, because we will never be able to buy anything further. + else if (!notifiedMaxCapacity) { // The first time we discover we are at max hash capacity (infinite cost) notify the user + log(ns, `INFO: spend-hacknet-hashes.js ${message}`, true, 'info'); // Only inform the user of this the first time it happens. + notifiedMaxCapacity = true; // Set the flag to avoid repeated notifications } - else - log(ns, `WARNING: ${message} `, false, 'warning'); } } // If for any of the above reasons, we weren't able to upgrade capacity, calling 'SpendHashes' once more