Bitburner Journal Days 1 through 4
Bitburner journal.
i'm documenting my time with bitburner for fun.
update: part 2 is up
irl day 1
i booted up the game in the late evening after work and wiped my cloud files, making sure i was starting from scratch. i didn't load up my github because those scripts are bad.
first thing i did was open the city map and hit foodnstuff, got a part time job doing.. actually i don't know but whatever it is it makes a hundred bucks a second, which is enough to start buying hacknet nodes and upgrading them with most of my money.
pretty soon the hacknet nodes made more than the grocery gig and i was bored of clicking hacknet upgrades, so i started to write a script.
i named it hacknet.js, real creative. i just want it to find the cheapest upgrade out of all upgrades, and buy it if it's less than some percent of my money. i didn't get to finish it because i had real life stuff, went to bed making money on just the hacknet nodes i had upgraded by hand.
irl day 2
woke up with a lot of money from just the hacknet. i went to the city map and alpha entertainment, bought as much ram as i could. i forget how much, but i clicked until i couldn't.
resumed writing the hacknet script and renamed it hacknet-manager, and finished it. at 52 lines of code it's a little bigger than i'd like it to be, and not as clean. even though hacknet is kind of a waste of time, i'm not happy with this script and think it needs more refinement.
hacknet-manager.js (v1)
/** @param {NS} ns */
// script that maintains hacknet node upgrades by spending a small fraction of our money (1%, arguably too much)
export async function main(ns) {
const spendRatio = 0.01;
// shorthand for getting how much money we have.
const money = () => ns.getServerMoneyAvailable("home");
// object here homogenizes the operations of cost-check, purchase, and max-checking hacknet upgrades.
const options = ({
nodes: ({ cost: () => { ns.hacknet.getPurchaseNodeCost() }, buy: () => { ns.hacknet.purchaseNode(); },
count: () => { ns.hacknet.numNodes()}, max: () => { ns.hacknet.maxNumNodes() } }),
levels: (i) => ({ cost: () => ns.hacknet.getLevelUpgradeCost(i, 1), buy: () => ns.hacknet.upgradeLevel(i, 1),
count: () => { ns.hacknet.getNodeStats(i).level }, max: () => 200 }),
ram: (i) => ({ cost: () => ns.hacknet.getRamUpgradeCost(i, 1), buy: () => ns.hacknet.upgradeRam(i, 1),
count: () => { ns.hacknet.getNodeStats(i).ram }, max: () => 64 }),
cores: (i) => ({ cost: () => ns.hacknet.getCoreUpgradeCost(i, 1), buy: () => ns.hacknet.upgradeCore(i, 1),
count: () => { ns.hacknet.getNodeStats(i).cores }, max: () => 16 })
});
// gives us something to loop through for upgrades specifically. I'm lazy
const upgradesArray = [options.levels, options.ram, options.cores];
while (true) {
var lowestCost = money();
// default to nodes being the best option. They're not, which is why we default to them.
var bestOption = options.nodes;
// defaults to whether nodes are maxed. We check every upgrade on every node in our loop and set it to false if anything isn't maxed.
var isMaxed = bestOption.count() >= bestOption.max();
for(var i = 0; i < options.nodes.count(); i++) {
for(var upgrade of upgradesArray) {
// track whether upgrades aren't maxed. if the value is true at the end, the script shuts down.
if (upgrade(i).count() < upgrade(i).max()) {
isMaxed = false;
}
// if the upgrade has a lower cost than the rest make it our priority
if (upgrade(i).cost() < lowestCost) {
lowestCost = upgrade(i).cost();
bestOption = upgrade(i);
}
}
}
// buy the best option if it's less than some arbitrary % of our wallet.
if (bestOption.cost() < money() * spendRatio) {
bestOption.buy();
}
// if we're maxed, quit
if (isMaxed) {
tprint("hacknet's maxed, shutting down");
break;
}
// need to sleep briefly
await ns.sleep(20);
}
}
i let the hacknet script run at 1% until it spent me down to about $10m before i decided 1% was too much, and dialed it back to 0.1%.
i started working on a sweatier version of my old daemon.js to play the game for me. i didn't finish it, but i got the server-scan part of it written and i decided to leverage maps, which i might wind up not needing.
i left the hacknets running. hopefully i'll have a few more ram upgrades to buy tomorrow.
irl day 3
this is when i decided to make this journal.
continued plucking at daemon.js. hacknet production earned up to 64 gb of ram.
switched gears, decided that i wanted to clean up the hacknet script more, for fun and form rather than function. it's ugly to me. i decided i wanted to learn about imports and exports, and also classes, static and private fields and methods, and public getters and setters.
before i flexed those on the hacknet-manager, i used those concepts to build a logger implementation i could share easily between my scripts.
logger.js
/** @class LogLevel : class representing a single log level to abstract the equatability of levels */
export class LogLevel {
#name;
#value;
constructor(name, value) {
this.#name = name;
this.#value = value;
}
get name() { return this.#name; }
get value() { return this.#value; }
/** @param {LogLevel} logLevel : the logLevel we're attempting to log at. If our log level is <= the log level, return true. */
shows(logLevel) { return this.value <= logLevel.value }
}
/** @class LogLevels : class that has log levels predefined for easier consumption */
export class LogLevels {
static #trace = new LogLevel("trace", 0);
static #debug = new LogLevel("debug", 1);
static #info = new LogLevel("info", 2);
static #warn = new LogLevel("warn", 3);
static #error = new LogLevel("error", 4);
static get trace() { return LogLevels.#trace; }
static get debug() { return LogLevels.#debug; }
static get info() { return LogLevels.#info; }
static get warn() { return LogLevels.#warn; }
static get error() { return LogLevels.#error; }
}
/** @class Logger : class that has logger helpers to make logging in other scripts consistent */
export class Logger {
#level;
/** @param {NS} ns : an instance of ns so that the logger can call tprint conditionally
* @param {LogLevel} l : log level that determines what log levels show up, defaults to info */
constructor(ns, logLevel = LogLevels.info) {
this.ns = ns;
this.#level = logLevel;
}
get level() { return this.#level; }
/** @param {LogLevel} l : a log level to set the logger level to. */
set level(l) { this.#level = l; }
logIf(s, l) { this.level.shows(l) && this.ns.tprint(`${this.level.name.toUpperCase()}: ${s}`); }
trace(s) { this.logIf(s, LogLevels.trace); }
debug(s) { this.logIf(s, LogLevels.debug); }
info(s) { this.logIf(s, LogLevels.info); }
warn(s) { this.logIf(s, LogLevels.warn); }
error(s) { this.logIf(s, LogLevels.error); }
}
i managed to finish the logger impl and felt pretty good about it, enough to post it on discord, but then i made it a bit more formal, and this is where it finally landed.
i think this is where i will leave it.
irl day 4 (isn't over)
wrapped up the hacknet-manager v2, but before that i had to fix some bugs and twiddle on the logger. the hacknet-manager was a lot of trial and error, but the logger helped find errors, so it's already paid off quite a bit.
i found myself needing a formatter because i don't like how fractions of money print. not much to this yet, but i figure i may need other formatting stuff later, so i made formatter.js
formatter.js
/** @param {number} d */
export function formatMoney(d) {
return Math.trunc(d * 100) / 100;
}
while quite a bit more "complex" than the old one (doubled in size), the structure of the hacknet manager feels a lot cleaner and simpler now. if i wanted to add functionality to it, i think it would be easier in its current state.
hacknet-manager.js (v2)
import { LogLevels, Logger } from "logger.js";
import { formatMoney } from "formatter.js";
// how much to spend at most on a single upgrade, as a ratio of our current money. change this if it spends more than you want.
const spendRatio = 0.001;
const logLevel = LogLevels.info;
/** @type {NS} q : a globally scoped instance of NS so i can be lazy */
let q;
/** @type {Hacknet} hn : a globally scoped instance of Hacknet so i can be lazy */
let hn;
/** lambda to get how much money we have at any given moment. */
let allowance = () => q.getServerMoneyAvailable("home") * spendRatio;
/** @type {Logger} log : need a logger instance to log stuff */
let log;
/** class representing a homogenized node upgrade, whose features are predetermined; variance is the index of the node */
class HacknetUpgrade {
constructor(name, costFunc, buyFunc, countFunc, maxFunc, i = -1) {
this.name = name;
this.cost = costFunc;
this.buy = buyFunc;
this.count = countFunc;
this.max = maxFunc;
this.index = i;
}
get isMaxed() { return this.count() >= this.max(); }
}
/** class representing the node upgrade, specifically, and its functions */
class NodeUpgrade extends HacknetUpgrade {
/** @type {HacknetNode[]} nodes */
#nodes;
constructor() {
super("node", () => hn.getPurchaseNodeCost(), () => { hn.purchaseNode(); this.#nodes.push(new HacknetNode(this.count() - 1)); }, () => hn.numNodes(), () => hn.maxNumNodes());
log.trace(`creating ${this.count()} nodes`);
this.#nodes = [...Array(this.count()).keys()].map((i) => new HacknetNode(i));
log.trace(`created node upgrade, which has no index`);
}
get nodes() { return this.#nodes; }
}
/** class representing the level upgrade, specifically, and its functions */
class LevelUpgrade extends HacknetUpgrade {
/** @param {number} i : the index of the node this upgrade belongs to */
constructor(i) {
super("level", () => hn.getLevelUpgradeCost(this.index, 1), () => hn.upgradeLevel(this.index, 1), () => hn.getNodeStats(this.index).level, () => 200, i);
log.trace(`created level upgrade for node ${i}`);
}
}
/** class representing the ram upgrade, specifically, and its functions */
class RamUpgrade extends HacknetUpgrade {
/** @param {number} i : the index of the node this upgrade belongs to */
constructor(i) {
super("ram", () => hn.getRamUpgradeCost(this.index, 1), () => hn.upgradeRam(this.index, 1), () => hn.getNodeStats(this.index).ram, () => 64, i);
log.trace(`created ram upgrade for node ${i}`);
}
}
/** class representing the cores upgrade, specifically, and its functions */
class CoreUpgrade extends HacknetUpgrade {
/** @param {number} i : the index of the node this upgrade belongs to */
constructor(i) {
super("core", () => hn.getCoreUpgradeCost(this.index, 1), () => hn.upgradeCore(this.index, 1), () => hn.getNodeStats(this.index).cores, () => 16, i);
log.trace(`created core upgrade for node ${i}`);
}
}
/** class representing a single hacknet node, which uses dot notation to give you access to its upgrade options */
class HacknetNode {
/** @param {number} i : the index of the node, determines what index its upgrade function suppliers pass to each function */
constructor(i) { this.upgrades = [new LevelUpgrade(i), new RamUpgrade(i), new CoreUpgrade(i)]; log.trace(`created node ${i}`); }
}
/** @param {NS} ns */
// script that maintains hacknet node upgrades by spending a small fraction of our money (0.1%, arguably too much)
export async function main(ns) {
q = ns;
hn = q.hacknet;
log = new Logger(q, logLevel);
// ubiquitous upgrade represents how many nodes we have and the functions to buy them
// this is also the "root" HacknetUpgrade, it controls other upgrades
let nodeUpgrade = new NodeUpgrade();
var isMaxed = false;
while (!isMaxed) {
let isInactive = true;
let lowest = nodeUpgrade;
isMaxed = lowest.isMaxed;
for (var node of nodeUpgrade.nodes) {
for (var upgrade of node.upgrades) {
isMaxed = isMaxed && upgrade.isMaxed;
if (isMaxed) { break; }
if (upgrade.cost() < lowest.cost()) { lowest = upgrade; }
}
}
if (allowance() >= lowest.cost()) {
log.info(`upgrading node ${lowest.index}'s ${lowest.name} at $${formatMoney(lowest.cost())}`);
lowest.buy();
isInactive = false;
}
// need to sleep briefly if active, otherwise a full second.
await q.sleep(isInactive ? 1000 : 1);
}
}
i'm gonna leave this here for now. i want to keep doing more stuff since the day isn't over.
i will probably make another post like this in a few days.