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.