Skip to content

Instantly share code, notes, and snippets.

@jaguilar
Last active February 25, 2024 16:18
Show Gist options
  • Save jaguilar/a8f7cb8da78cbddc2a06415fdf97df27 to your computer and use it in GitHub Desktop.
Save jaguilar/a8f7cb8da78cbddc2a06415fdf97df27 to your computer and use it in GitHub Desktop.
bitburner: early-mid hacking setup

Problem Statement

  • Allocate resources from one server to work on another.
  • Weaken and grow first, before beginning to hack.
  • Allocate resources toward the most efficient available task, subject to some allowances for early progression.
  • Minimize RAM usage (scheduling overhead of around 30GB).

Overview

I designed a system with three main components:

  • A spider to find and hack servers.
  • A distributor to coordinate work among the available owned servers.
  • Two simple workers.

Spider

The spider is very straightfoward, as you will see below in spider2.js. It uses a breadth first search across the nodes starting from home, hacking any nodes we have the capability to. It stores the hacked node list in a newline separated file, so that other scripts don't have to invoke a function or spend precious CPU time reconstructing the list.

Distributor

The distributor is the most interesting part. The distributor consists of a loop that

  • cancels all existing distributor controlled workers,
  • schedules new workers, then
  • awaits a signal that something material has changed.

We cancel all existing workers because it is easier to solve this problem if you don't have to keep track of state. Netscripts programming capabilities are some of the most challenging and inconsistent I've ever worked with, so I want to write as little complex code as possible. Cancelling all our existing workers has some minor drawbacks in terms of performance, but what it wins us in simplicity dominates such considerations. We'll be able to spend more time thinking about algorithmic improvements if we don't have to do fiddly things like managing state.

The new worker scheduling algorithm currently has two basic priorities. The first priority is to focus on weakening the weakest pending node. It iterates through the targets in the order the spider observed them (i.e. generally weakest first). If it encounters any that are significanly more secure than their minimum security level, it will dedicate as many threads as possible among all the hosts to weakening that server. It also spawns a small watcher script to notify the distributor when a node like this has been weakened down to the minimum level.

Currently, I only do this preparation step for security level, but I should probably also grow servers before beginning to hack them. That's a TODO for tomorrow night.

The second priority is to schedule "flexihack" workers. We try to schedule these backwards in the targets list, focusing on the highest growth servers first. I found the task of balancing grow and hack calls tedious, so my flexihack worker calls grow and hack adaptively (when the available money drops below 95% of max, grow is called). We also schedule a small group of flexihack workers on any totally weakened target, even if it's not the most optimal one, so that we can at least have some income from hacking early on. My current stance is to use one weaken worker per six flexihack workers, which seems to be about as much as is required to keep up with either six grow threads or six hack threads.

The signal can come from either the spider, when it gains enough strength to hack a new server, or the watcher, when a server has been sufficiently weakened (or, soon, grown). You can also run a small script called "signal.script" to manually reschedule (e.g. if you have bought servers manually, or grown your home server's RAM). The signal is just the presence of a certain file on the home computer.

Workers

My weaken worker is trivial.

flexihack costs about .5 GB more RAM than hack and grow separately, but that isn't even an order of magnitude. It's totally worth the gain in simplicity. To prevent overshooting hack/grow thresholds, we schedule flexihack workers in small batches. We accomplish this by adding a final, meaningless tag argument to the flexihack.script invocation, and using scriptKill to kill flexihack. flexihack keeps server money at about 95% of max.

Conclusion

I'm not sure whether any of this is really optimal, since I'm just playing around with this new system. I think the work scheduling stuff is pretty useful, but I'm not sure about the prioritization. That said, it's fun to talk about code, even when it's incorrect!

Please see the source code listings for all non-trivial scripts. I'd love to hear criticism, but try to only base it on things I'm likely to already know about the game as someone who has been playing it for two days! :) Pointers on how to write better JS are super helpful, since I've never spent much time in this language before.

function maxThreads(cmd, host) {
r = getServerRam(host);
sr = getScriptRam(cmd, host);
out = Math.floor((r[0] - r[1]) / sr);
print("maxThreads(" + cmd + ", " + host + "): " + out);
return out;
}
function execa(args, permissive) {
scp(args[0], "home", args[1]);
nthreads = 0;
if (args.length < 3) {
nthreads = 1;
} else {
nthreads = args[2];
}
if (nthreads === 0) return;
if (args.length == 2) {
exec(args[0], args[1], nthreads);
} else if (args.length == 3) {
exec(args[0], args[1], nthreads);
} else if (args.length == 4) {
exec(args[0], args[1], nthreads, args[3]);
} else if (args.length == 5) {
exec(args[0], args[1], nthreads, args[3], args[4]);
} else if (args.length == 6) {
exec(args[0], args[1], nthreads, args[3], args[4], args[5]);
}
}
execa(args);
sm = getServerMaxMoney(args[0]);
while (true) {
if (getServerMoneyAvailable(args[0]) < 0.95 * sm) grow(args[0]);
else hack(args[0]);
}
import {execa} from "exec.script";
// Other scripts can import this and use it to signal us to
// reschedule our initial weaken/flexihack work.
function signal() {
tprint("notifying hack-dist.script of a rescheduling condition");
f = "hack-dist_notification.txt";
exec("touch.script", "home", 1, f);
}
function consumeSignal() {
f = "hack-dist_notification.txt";
if (fileExists(f)) {
rm(f);
return true;
}
return false;
}
function awaitSignal() {
while (!consumeSignal()) sleep(10000);
}
function killHacks(h) {
scriptKill("flexihack.script", h);
scriptKill("weaken.script", h);
if (h != "home") {
killall(h); // temporary:
scp(ls("home", "script"), "home", h);
}
}
// Tag is an unused argument to differentiate different instances of the same script.
function scheduleOn(hostNames, hostMaxRam, jobScript, jobTasks, jobTarget, tag) {
// Script should have been copied out to all candidate hosts already.
ramPerTask = getScriptRam(jobScript, "home");
while (jobTasks > 0 && hostNames.length > 0) {
// Schedule as many threads of the task as possible on the
// first host.
numThisHost = Math.min(Math.floor(hostMaxRam[0] / ramPerTask), jobTasks);
jobTasks -= numThisHost;
args = [jobScript, hostNames[0], numThisHost, jobTarget, tag];
tprint("execute: " + args);
if (numThisHost > 0) execa(args);
// If we still have tasks left, that means we exhausted the
// RAM of this host.
if (jobTasks > 0) {
tprint("consumed all ram available on " + hostNames[0]);
hostNames.shift();
hostMaxRam.shift();
} else {
// We were able to schedule everything, but we still need to update
// the ram consumption.
hostMaxRam[0] = hostMaxRam[0] - numThisHost * ramPerTask;
tprint(hostNames[0] + " has " + hostMaxRam[0] + " left to schedule");
}
}
}
// targets is an array of target machines to weaken and/or hack.
// hostnames is the names of hosts where work can be scheduled.
// hostmaxram is the amount of ram we're allowed to use on each
// host.
function doHacks(targets, hostnames, hostmaxram) {
// Iterate over the targets, which should be in order of easiest to hardest.
// We're going to spend as many threads as possible to weaken each target to
// get it to its weakest level as fast as possible.
//
// We spawn at least a few flexihacks on each server so handled to make early
// cashflow less painful, although things only really get hopping once we
// have weakened the biggest server we can hack. At that point, we dedicate
// all excess capacity to hacking said server, which gets lucrative fast.
weakenTargets = targets.slice(0, targets.length);
while (targets.length > 0 && hostnames.length > 0) {
t = weakenTargets.shift();
tprint("weakenloop: " + t);
if (getServerSecurityLevel(t) <= 3 + getServerMinSecurityLevel(t)) {
tprint("already weakened");
// Just spawn one weaken and a round of flexihacks. This is to make
// early game cashflow less painful.
tag = -1;
scheduleOn(hostnames, hostmaxram, "weaken.script", 1, t, tag);
scheduleOn(hostnames, hostmaxram, "flexihack.script", 6, t, tag);
} else {
// Spawn only weakens until security is near minimum.
nWeakens = Math.floor((getServerSecurityLevel(t) - getServerMinSecurityLevel(t)) / 0.05);
tprint("needs " + nWeakens + " weaken threads");
scheduleOn(hostnames, hostmaxram, "weaken.script", nWeakens, t, tag);
// Watch for the security level on this host to get low, and signal ourselves when it does.
// We assume that we are able to afford to run one of these on the local server.
// If not, we are probably under-buying local ram.
run("watch-security.script", 1, t);
}
}
// If there are any resources left, it's time to hack! We will schedule up to ~100
// hack workers targetting a given server. Generally speaking we want to target
// servers that are deeper in the search tree. These will tend to appear at the end
// of the target list.
while (targets.length > 0 && hostnames.length > 0) {
t = targets.pop();
tprint("flexihack loop: " + t);
// We stagger the scheduling of flexihacks so that not all hundred threads
// are hacking or growing at the same time. We believe this will reduce waste.
// We are also staggering the scheduling of weakens, which seems like a waste.
// We can probably make a reasonable guess as to how many will actually be used.
//
// Rule of thumb: one weaken per six flexihacks. Derivation: hack takes
// 0.002 security, grow takes 2.5x as long at takes 0.005. Weaken takes ~3.5x
// and 0.05. Divide the rates through and it seems like no matter whether you're
// doing grow or hack, you're using about 1/6th as much security per unit time (
// not accounting for failed hacks).
for (i = 0; hostnames.length > 0 && i < 15; ++i) {
tprint("scheduling flexihack round " + i);
scheduleOn(hostnames, hostmaxram, "weaken.script", 1, t, i);
scheduleOn(hostnames, hostmaxram, "flexihack.script", 6, t, i);
}
}
}
consumeSignal();
minHomeRamAvailable = 100; // Ballpark adjustment.
while (true) {
// The spider is responsible for finding targets.
targets = read("spider_data.txt").split("\n");
tprint("targets=" + targets);
hosts = targets.concat(getPurchasedServers()).concat(["home"]);
tprint("hosts=" + hosts);
// Cancel all running weakens and flexihacks on hosts, and record
// their available RAM.
for (i = 0; i < hosts.length; ++i) {
killHacks(hosts[i]);
}
sleep(1000);
// Compute the available ram for each server, reserving RAM on home especially
// for running other scripts.
ram = [];
for (i = 0; i < hosts.length; ++i) {
v = getServerRam(hosts[i])[0];
if (hosts[i] == "home") {
v = Math.max(0, v - minHomeRamAvailable);
}
ram.push(v);
}
tprint("ram=" + ram);
doHacks(targets, hosts, ram);
// At the end of the loop, we wait for a signal that something has changed.
// This entails seeing any value in the signalPort.
awaitSignal();
tprint("notified! restarting everything");
}
import {signal} from "hack-dist.script";
function prep(target) {
if (getServerRequiredHackingLevel(target) >
getHackingLevel()) {
return false;
}
if (hasRootAccess(target)) return true;
function can(action) {
return fileExists(action + ".exe", "home");
}
ports = 0;
if (can("brutessh")) { brutessh(target); ++ports; }
if (can("ftpcrack")) { ftpcrack(target); ++ports; }
if (can("relaysmtp")) { relaysmtp(target); ++ports; }
if (can("httpworm")) { httpworm(target); ++ports; }
if (can("sqlinject")) { sqlinject(target); ++ports; }
if (ports >= getServerNumPortsRequired(target)) {
return nuke(target);
} else {
return false;
}
}
spiderDataFile = "spider_data.txt";
// Iterate over all the servers we can connect to. If we've never hacked
// the server before, start our hack loops and add them to the spider data file.
function spider() {
hosts = ["home"];
// Special case: we don't look at the darkweb, since it's not hackable, nor
// our purchased servers.
seen = ["darkweb"].concat(getPurchasedServers());
hacked = read(spiderDataFile).split("\n");
if (hacked.length == 1 && hacked[0] === "") hacked = [];
initialLength = hacked.length;
while (hosts.length > 0) {
h = hosts.shift();
// We've already seen this host during this scan.
if (seen.indexOf(h) != -1) continue;
seen.push(h);
print("host: " + h);
if (!prep(h)) {
print("cannot crack");
continue;
}
if (h != "home" && hacked.indexOf(h) == -1) {
hacked.push(h);
}
hosts = hosts.concat(scan(h));
}
write(spiderDataFile, hacked.join("\n"), "w");
if (hacked.length != initialLength) signal();
}
sleep(1000);
rm(spiderDataFile);
while (true) {
spider();
sleep(60000);
}
@AnsonSeidr
Copy link

AnsonSeidr commented May 29, 2022

there's some spots where u used script instead of .js

ex:

spider2.js
import {signal} from "hack-dist.script";

function prep(target) {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment