-
Notifications
You must be signed in to change notification settings - Fork 272
/
analyze-hack.js
200 lines (183 loc) · 13.2 KB
/
analyze-hack.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
import { getConfiguration, disableLogs, formatMoney as importedFormatMoney, formatDuration, scanAllServers } from './helpers.js'
const argsSchema = [
['all', false], // Set to true to report on all servers, not just the ones within our hack level
['silent', false], // Set to true to disable outputting the best servers to the terminal
['at-hack-level', 0], // Simulate expected gains when the player reaches the specified hack level. 0 means use the player's current hack level.
['hack-percent', -1], // Compute gains when hacking a certain percentage of each server's money. -1 estimates hack percentage based on current ram available, capped at 98%
['include-hacknet-ram', false], // Whether to include hacknet servers' RAM when computing current ram available
['disable-formulas-api', false], // Disables use of the formulas API even if it is available (useful for debugging the fallback logic used when formulas is unavailable)
];
export function autocomplete(data, args) {
data.flags(argsSchema);
return [];
}
/** @param {NS} ns **/
export async function main(ns) {
const options = getConfiguration(ns, argsSchema);
if (!options) return; // Invalid options, or ran in --help mode.
disableLogs(ns, ["scan", "sleep"]);
let serverNames = [""]; // Provide a type hint to the IDE
serverNames = scanAllServers(ns);
var weaken_ram = 1.75;
var grow_ram = 1.75;
var hack_ram = 1.7;
var hack_percent = options['hack-percent'] / 100;
var use_est_hack_percent = false;
if (options['hack-percent'] == -1) {
use_est_hack_percent = true;
} else {
hack_percent = options['hack-percent'] / 100;
if (hack_percent <= 0 || hack_percent >= 1) {
ns.tprint("hack-percent out of range (0-100)");
return;
}
}
var player = ns.getPlayer();
//ns.print(JSON.stringify(player));
if (options['at-hack-level']) player.skills.hacking = options['at-hack-level'];
let servers = serverNames.map(ns.getServer);
// Compute the total RAM available to us on all servers (e.g. for running hacking scripts)
var ram_total = servers.reduce(function (total, server) {
if (!server.hasAdminRights || (server.hostname.startsWith('hacknet') && !options['include-hacknet-ram'])) return total;
return total + server.maxRam;
}, 0);
// Override the imported formatMoney to handle amounts less than 0.01:
let formatMoney = (amt) => amt > 0.01 ? importedFormatMoney(amt) : '$' + amt.toPrecision(3);
/** Helper to compute server gain/exp rates at a specific hacking level
* @param {Server} server
* @param {Player} player */
function getRatesAtHackLevel(server, player, hackLevel) {
let theoreticalGainRate, cappedGainRate, expRate;
let useFormulas = !options['disable-formulas-api'];
if (useFormulas) {
// Temporarily change the hack level on the player object to the requested level
const real_player_hack_skill = player.skills.hacking;
player.skills.hacking = hackLevel;
// Assume we will have wekened the server to min-security and taken it to max money before targetting
server.hackDifficulty = server.minDifficulty;
server.moneyAvailable = server.moneyMax;
try {
// Compute the cost (ram*seconds) for each tool
const weakenCost = weaken_ram * ns.formulas.hacking.weakenTime(server, player);
const growCost = grow_ram * ns.formulas.hacking.growTime(server, player) + weakenCost * 0.004 / 0.05;
const hackCost = hack_ram * ns.formulas.hacking.hackTime(server, player) + weakenCost * 0.002 / 0.05;
// Compute the growth and hack gain rates
const growGain = Math.log(ns.formulas.hacking.growPercent(server, 1, player, 1));
const hackGain = ns.formulas.hacking.hackPercent(server, player);
// If hack gain is less than this minimum (very high BN12 levels?) We must coerce it to some minimum value to avoid NAN results.
const minHackGain = 1e-10;
if (hackGain <= minHackGain)
ns.print(`WARN: hackGain is ${hackGain.toPrecision(3)}. Coercing it to the minimum value ${minHackGain} (${server.hostname})`);
server.estHackPercent = Math.max(minHackGain, Math.min(0.98,
Math.min(ram_total * hackGain / hackCost, 1 - 1 / Math.exp(ram_total * growGain / growCost)))); // TODO: I think these might be off by a factor of 2x
if (use_est_hack_percent) hack_percent = server.estHackPercent;
const grows_per_cycle = -Math.log(1 - hack_percent) / growGain;
const hacks_per_cycle = hack_percent / hackGain;
const hackProfit = server.moneyMax * hack_percent * ns.formulas.hacking.hackChance(server, player);
// Compute the relative monetary gain
theoreticalGainRate = hackProfit / (growCost * grows_per_cycle + hackCost * hacks_per_cycle) * 1000 /* Convert per-millisecond rate to per-second */;
expRate = ns.formulas.hacking.hackExp(server, player) * (1 + 0.002 / 0.05) / (hackCost) * 1000;
// The practical cap on revenue is based on your hacking scripts. For my hacking scripts this is about 20% per second, adjust as needed
// No idea why we divide by ram_total - Basically ensures that as our available RAM gets larger, the sort order merely becomes "by server max money"
cappedGainRate = Math.min(theoreticalGainRate, hackProfit / ram_total);
ns.print(`At hack level ${hackLevel} and steal ${(hack_percent * 100).toPrecision(3)}%: ` +
`Theoretical ${formatMoney(theoreticalGainRate)}, Limit: ${formatMoney(hackProfit / ram_total)}, Exp: ${expRate.toPrecision(3)}, ` +
`Hack Chance: ${(ns.formulas.hacking.hackChance(server, player) * 100).toPrecision(3)}% (${server.hostname})`);
}
catch { // Formulas API unavailable?
useFormulas = false;
} finally {
player.skills.hacking = real_player_hack_skill; // Restore the real hacking skill if we changed it temporarily
}
}
// Solution for when formulas API is disabled or unavailable
if (!useFormulas) {
// Fall-back to returning a "gain rates" based purely on current hack time (i.e. ignoring the RAM associated with required grow/weaken threads)
let timeToHack = ns.getWeakenTime(server.hostname) / 4.0;
// Realistically, batching scripts run on carefully timed intervals (e.g. batches scheduled no less than 200 ms apart).
// So for very small time-to-weakens, we use a "capped" gain rate based on a more achievable number of hacks per second.
let cappedTimeToHack = Math.max(timeToHack, 200)
// the server computes experience gain based on the server's base difficulty. To get a rate, we divide that by the timeToWeaken
let relativeExpGain = 3 + server.minDifficulty * 0.3; // Ignore HackExpGain mults since they affect all servers equally
server.estHackPercent = 1; // Our simple calculations below are based on 100% of server money on every server.
[theoreticalGainRate, cappedGainRate, expRate] = [server.moneyMax / timeToHack, server.moneyMax / cappedTimeToHack, relativeExpGain / timeToHack];
ns.print(`Without formulas.exe, based on max money ${formatMoney(server.moneyMax)} and hack-time ${formatDuration(timeToHack)} (capped at ${formatDuration(cappedTimeToHack)})): ` +
`Theoretical ${formatMoney(theoreticalGainRate)}, Limit: ${formatMoney(cappedGainRate)}, Exp: ${expRate.toPrecision(3)} (${server.hostname})`);
}
return [theoreticalGainRate, cappedGainRate, expRate];
}
ns.print(`All? ${options['all']} Player hack: ${player.skills.hacking} Ram total: ${ram_total}`);
//ns.print(`\n` + servers.map(s => `${s.hostname} bought: ${s.purchasedByPlayer} moneyMax: ${s.moneyMax} admin: ${s.hasAdminRights} hack: ${s.requiredHackingSkill}`).join('\n'));
// Filter down to the list of servers we wish to report on
servers = servers.filter(server => !server.purchasedByPlayer && (server.moneyMax || 0) > 0 &&
(options['all'] || server.hasAdminRights && server.requiredHackingSkill <= player.skills.hacking));
// First address the servers within our hacking level
const unlocked_servers = servers.filter(s => s.requiredHackingSkill <= player.skills.hacking)
.map(function (server) {
[server.theoreticalGainRate, server.gainRate, server.expRate] = getRatesAtHackLevel(server, player, player.skills.hacking);
return server;
});
// The best server's gain rate will be used to pro-rate the relative gain of servers that haven't been unlocked yet (if they were unlocked at this level)
const best_unlocked_server = unlocked_servers.sort((a, b) => b.gainRate - a.gainRate)[0];
ns.print("Best unlocked server: ", best_unlocked_server.hostname, " with ", formatMoney(best_unlocked_server.gainRate), " per ram-second");
// Compute locked server's gain rates (pro rated back to the current player's hack level)
const locked_servers = servers.filter(s => s.requiredHackingSkill > player.skills.hacking).sort((a, b) => a.requiredHackingSkill - b.requiredHackingSkill)
.map(function (server) {
// We will need to fake the hacking skill to get the numbers for when this server will first be unlocked, but to keep the comparison
// fair, we will need to scale down the gain by the amount current best server gains now, verses what it would gain at that hack level.
const [bestUnlockedScaledGainRate, _, bestUnlockedScaledExpRate] = getRatesAtHackLevel(best_unlocked_server, player, server.requiredHackingSkill);
const gainRateScaleFactor = bestUnlockedScaledGainRate ? best_unlocked_server.theoreticalGainRate / bestUnlockedScaledGainRate : 1;
const expRateScaleFactor = bestUnlockedScaledExpRate ? best_unlocked_server.expRate / bestUnlockedScaledExpRate : 1;
const [theoreticalGainRate, cappedGainRate, expRate] = getRatesAtHackLevel(server, player, server.requiredHackingSkill);
// Apply the scaling factors, as well as the same cap as above
server.theoreticalGainRate = theoreticalGainRate * gainRateScaleFactor;
server.expRate = expRate * expRateScaleFactor;
server.gainRate = Math.min(server.theoreticalGainRate, cappedGainRate);
ns.print(`${server.hostname}: Scaled theoretical gain by ${gainRateScaleFactor.toPrecision(3)} to ${formatMoney(server.theoreticalGainRate)} ` +
`(capped at ${formatMoney(cappedGainRate)}) and exp by ${expRateScaleFactor.toPrecision(3)} to ${server.expRate.toPrecision(3)}`);
return server;
}) || [];
// Combine the lists, sort, and display a summary.
const server_eval = unlocked_servers.concat(locked_servers);
const best_server = server_eval.sort((a, b) => b.gainRate - a.gainRate)[0];
if (!options['silent'])
ns.tprint("Best server: ", best_server.hostname, " with ", formatMoney(best_server.gainRate), " per ram-second");
// Print all servers by best to work hack money value
let order = 1;
let serverListByGain = `Servers in order of best to worst hack money at Hack ${player.skills.hacking}:`;
for (const server of server_eval)
serverListByGain += `\n ${order++} ${server.hostname}, with ${formatMoney(server.gainRate)} per ram-second while stealing ` +
`${(server.estHackPercent * 100).toPrecision(3)}% (unlocked at hack ${server.requiredHackingSkill})`;
ns.print(serverListByGain);
// Reorder servers by exp and sort by best to work hack experience gain rate
var best_exp_server = server_eval.sort(function (a, b) {
return b.expRate - a.expRate;
})[0];
if (!options['silent'])
ns.tprint("Best exp server: ", best_exp_server.hostname, " with ", best_exp_server.expRate, " exp per ram-second");
order = 1;
let serverListByExp = `Servers in order of best to worst hack exp at Hack ${player.skills.hacking}:`;
for (let i = 0; i < Math.min(5, server_eval.length); i++)
serverListByExp += `\n ${order++} ${server_eval[i].hostname}, with ${server_eval[i].expRate.toPrecision(3)} exp per ram-second`;
ns.print(serverListByExp);
ns.write('/Temp/analyze-hack.txt', JSON.stringify(server_eval.map(s => ({
hostname: s.hostname,
gainRate: s.gainRate,
expRate: s.expRate
}))), "w");
// Below is stats for hacknet servers - uncomment at cost of 4 GB Ram
/*
var hacknet_nodes = [...(function* () {
var n = ns.hacknet.numNodes();
for (var i = 0; i < n; i++) {
var server = ns.hacknet.getNodeStats(i);
server.gainRate = 1000000 / 4 * server.production / server.ram;
yield server;
}
})()];
var best_hacknet_node = hacknet_nodes.sort(function (a, b) {
return b.gainRate - a.gainRate;
})[0];
if (best_hacknet_node) ns.tprint("Best hacknet node: ", best_hacknet_node.name, " with $", best_hacknet_node.gainRate, " per ram-second");
*/
}