Files
The_Bitburner_Scripts/Mizzajl/home/Ramses/S4utils.js
2024-10-06 10:09:10 +02:00

420 lines
14 KiB
JavaScript

/*
We've got a brand new class to look at, but the rest of the file remains unchanged.
*/
/** @param {NS} ns */
export async function main(ns) {
ns.tprint("This is just a function library, it doesn't do anything.");
}
/*
This is an overengineered abomination of a custom data structure. It is essentially a double-ended queue,
but also has a Map stapled to it, just in case we need to access items by id (we don't.)
The idea is that it can fetch/peek items from the front or back with O(1) timing. This gets around the issue of
dynamic arrays taking O(n) time to shift, which is terrible behavior for very long queues like the one we're using.
*/
export class Deque {
#capacity = 0; // The maximum length.
#length = 0; // The actual number of items in the queue
#front = 0; // The index of the "head" where data is read from the queue.
#deleted = 0; // The number of "dead" items in the queue. These occur when items are deleted by index. They are bad.
#elements; // An inner array to store the data.
#index = new Map(); // A hash table to track items by ID. Try not to delete items using this, it's bad.
// Create a new queue with a specific capacity.
constructor(capacity) {
this.#capacity = capacity;
this.#elements = new Array(capacity);
}
// You can also convert arrays.
static fromArray(array, overallocation = 0) {
const result = new Deque(array.length + overallocation);
array.forEach(item => result.push(item));
return result;
}
// Deleted items don't count towards length, but they still take up space in the array until they can be cleared.
// Seriously, don't use the delete function unless it's absolutely necessary.
get size() {
return this.#length - this.#deleted;
}
isEmpty() {
return this.#length - this.#deleted === 0;
}
// Again, "deleted" items still count towards this. Use caution.
isFull() {
return this.#length === this.#capacity;
}
// The "tail" where data is typically written to.
// Unlike the front, which points at the first piece of data, this point at the first empty slot.
get #back() {
return (this.#front + this.#length) % this.#capacity;
}
// Push a new element into the queue.
push(value) {
if (this.isFull()) {
throw new Error("The deque is full. You cannot add more items.");
}
this.#elements[this.#back] = value;
this.#index.set(value.id, this.#back);
++this.#length;
}
// Pop an item off the back of the queue.
pop() {
while (!this.isEmpty()) {
--this.#length;
const item = this.#elements[this.#back];
this.#elements[this.#back] = undefined; // Free up the item for garbage collection.
this.#index.delete(item.id); // Don't confuse index.delete() with this.delete()
if (item.status !== "deleted") return item; // Clear any "deleted" items we encounter.
else --this.#deleted; // If you needed another reason to avoid deleting by ID, this breaks the O(1) time complexity.
}
throw new Error("The deque is empty. You cannot delete any items.");
}
// Shift an item off the front of the queue. This is the main method for accessing data.
shift() {
while (!this.isEmpty()) {
// Our pointer already knows exactly where the front of the queue is. This is much faster than the array equivalent.
const item = this.#elements[this.#front];
this.#elements[this.#front] = undefined;
this.#index.delete(item.id);
// Move the head up and wrap around if we reach the end of the array. This is essentially a circular buffer.
this.#front = (this.#front + 1) % this.#capacity;
--this.#length;
if (item.status !== "deleted") return item;
else --this.#deleted;
}
throw new Error("The deque is empty. You cannot delete any items.");
}
// Place an item at the front of the queue. Slightly slower than pushing, but still faster than doing it on an array.
unshift(value) {
if (this.isFull()) {
throw new Error("The deque is full. You cannot add more items.");
}
this.#front = (this.#front - 1 + this.#capacity) % this.#capacity;
this.#elements[this.#front] = value;
this.#index.set(value.id, this.#front);
++this.#length;
}
// Peeking at the front is pretty quick, since the head is already looking at it. We just have to clear those pesky "deleted" items first.
peekFront() {
if (this.isEmpty()) {
throw new Error("The deque is empty. You cannot peek.");
}
while (this.#elements[this.#front].status === "deleted") {
this.#index.delete(this.#elements[this.#front]?.id);
this.#elements[this.#front] = undefined;
this.#front = (this.#front + 1) % this.#capacity;
--this.#deleted;
--this.#length;
if (this.isEmpty()) {
throw new Error("The deque is empty. You cannot peek.");
}
}
return this.#elements[this.#front];
}
// Peeking at the back is ever so slightly slower, since we need to recalculate the pointer.
// It's a tradeoff for the faster push function, and it's a very slight difference either way.
peekBack() {
if (this.isEmpty()) {
throw new Error("The deque is empty. You cannot peek.");
}
let back = (this.#front + this.#length - 1) % this.#capacity;
while (this.#elements[back].status === "deleted") {
this.#index.delete(this.#elements[back].id);
this.#elements[back] = undefined;
back = (back - 1 + this.#capacity) % this.#capacity;
--this.#deleted;
--this.#length;
if (this.isEmpty()) {
throw new Error("The deque is empty. You cannot peek.");
}
}
return this.#elements[back];
}
// Fill the queue with a single value.
fill(value) {
while (!this.isFull()) {
this.push(value);
}
}
// Empty the whole queue.
clear() {
while (!this.isEmpty()) {
this.pop();
}
}
// Check if an ID exists.
exists(id) {
return this.#index.has(id);
}
// Fetch an item by ID
get(id) {
let pos = this.#index.get(id);
return pos !== undefined ? this.#elements[pos] : undefined;
}
// DON'T
delete(id) {
let item = this.get(id);
if (item !== undefined) {
item.status = "deleted";
++this.#deleted;
return item;
} else {
throw new Error("Item not found in the deque.");
}
}
}
// The recursive server navigation algorithm. The lambda predicate determines which servers to add to the final list.
// You can also plug other functions into the lambda to perform other tasks that check all servers at the same time.
/** @param {NS} ns */
export function getServers(ns, lambdaCondition = () => true, hostname = "home", servers = [], visited = []) {
if (visited.includes(hostname)) return;
visited.push(hostname);
if (lambdaCondition(hostname)) servers.push(hostname);
const connectedNodes = ns.scan(hostname);
if (hostname !== "home") connectedNodes.shift();
for (const node of connectedNodes) getServers(ns, lambdaCondition, node, servers, visited);
return servers;
}
// Here are a couple of my own getServers modules.
// This one finds the best target for hacking. It tries to balance expected return with time taken.
/** @param {NS} ns */
export function checkTarget(ns, server, target = "n00dles", forms = false) {
if (!ns.hasRootAccess(server)) return target;
const player = ns.getPlayer();
const serverSim = ns.getServer(server);
const pSim = ns.getServer(target);
let previousScore;
let currentScore;
if (serverSim.requiredHackingSkill <= player.skills.hacking / (forms ? 1 : 2)) {
if (forms) {
serverSim.hackDifficulty = serverSim.minDifficulty;
pSim.hackDifficulty = pSim.minDifficulty;
previousScore = pSim.moneyMax / ns.formulas.hacking.weakenTime(pSim, player) * ns.formulas.hacking.hackChance(pSim, player);
currentScore = serverSim.moneyMax / ns.formulas.hacking.weakenTime(serverSim, player) * ns.formulas.hacking.hackChance(serverSim, player);
} else {
const weight = (serv) => {
// Calculate the difference between max and available money
let diff = serv.moneyMax - serv.moneyAvailable;
// Calculate the scaling factor as the ratio of the difference to the max money
// The constant here is just an adjustment to fine tune the influence of the scaling factor
let scalingFactor = diff / serv.moneyMax * 0.95;
// Adjust the weight based on the difference, applying the scaling penalty
return (serv.moneyMax / serv.minDifficulty) * (1 - scalingFactor);
}
previousScore = weight(pSim)
currentScore = weight(serverSim)
}
if (currentScore > previousScore) target = server;
}
return target;
}
// A simple function for copying a list of scripts to a server.
/** @param {NS} ns */
export function copyScripts(ns, server, scripts, overwrite = false) {
for (const script of scripts) {
if ((!ns.fileExists(script, server) || overwrite) && ns.hasRootAccess(server)) {
ns.scp(script, server);
}
}
}
// A generic function to check that a given server is prepped. Mostly just a convenience.
export function isPrepped(ns, server) {
const tolerance = 0.0001;
const maxMoney = ns.getServerMaxMoney(server);
const money = ns.getServerMoneyAvailable(server);
const minSec = ns.getServerMinSecurityLevel(server);
const sec = ns.getServerSecurityLevel(server);
const secFix = Math.abs(sec - minSec) < tolerance;
return (money === maxMoney && secFix) ? true : false;
}
/*
This prep function isn't part of the tutorial, but the rest of the code wouldn't work without it.
I don't make any guarantees, but I've been using it and it's worked well enough. I'll comment it anyway.
The prep strategy uses a modified proto-batching technique, which will be covered in part 2.
*/
/** @param {NS} ns */
export async function prep(ns, values, ramNet) {
const maxMoney = values.maxMoney;
const minSec = values.minSec;
let money = values.money;
let sec = values.sec;
while (!isPrepped(ns, values.target)) {
const wTime = ns.getWeakenTime(values.target);
const gTime = wTime * 0.8;
const dataPort = ns.getPortHandle(ns.pid);
dataPort.clear();
const pRam = ramNet.cloneBlocks();
const maxThreads = Math.floor(ramNet.maxBlockSize / 1.75);
const totalThreads = ramNet.prepThreads;
let wThreads1 = 0;
let wThreads2 = 0;
let gThreads = 0;
let batchCount = 1;
let script, mode;
/*
Modes:
0: Security only
1: Money only
2: One shot
*/
if (money < maxMoney) {
gThreads = Math.ceil(ns.growthAnalyze(values.target, maxMoney / money));
wThreads2 = Math.ceil(ns.growthAnalyzeSecurity(gThreads) / 0.05);
}
if (sec > minSec) {
wThreads1 = Math.ceil((sec - minSec) * 20);
if (!(wThreads1 + wThreads2 + gThreads <= totalThreads && gThreads <= maxThreads)) {
gThreads = 0;
wThreads2 = 0;
batchCount = Math.ceil(wThreads1 / totalThreads);
if (batchCount > 1) wThreads1 = totalThreads;
mode = 0;
} else mode = 2;
} else if (gThreads > maxThreads || gThreads + wThreads2 > totalThreads) {
mode = 1;
const oldG = gThreads;
wThreads2 = Math.max(Math.floor(totalThreads / 13.5), 1);
gThreads = Math.floor(wThreads2 * 12.5);
batchCount = Math.ceil(oldG / gThreads);
} else mode = 2;
// Big buffer here, since all the previous calculations can take a while. One second should be more than enough.
const wEnd1 = Date.now() + wTime + 1000;
const gEnd = wEnd1 + values.spacer;
const wEnd2 = gEnd + values.spacer;
// "metrics" here is basically a mock Job object. Again, this is just an artifact of repurposed old code.
const metrics = {
batch: "prep",
target: values.target,
type: "none",
time: 0,
end: 0,
port: ns.pid,
log: values.log,
report: false
};
// Actually assigning threads. We actually allow grow threads to be spread out in mode 1.
// This is because we don't mind if the effect is a bit reduced from higher security unlike a normal batcher.
// We're not trying to grow a specific amount, we're trying to grow as much as possible.
for (const block of pRam) {
while (block.ram >= 1.75) {
const bMax = Math.floor(block.ram / 1.75)
let threads = 0;
if (wThreads1 > 0) {
script = "S4tWeaken.js";
metrics.type = "pWeaken1";
metrics.time = wTime;
metrics.end = wEnd1;
threads = Math.min(wThreads1, bMax);
if (wThreads2 === 0 && wThreads1 - threads <= 0) metrics.report = true;
wThreads1 -= threads;
} else if (wThreads2 > 0) {
script = "S4tWeaken.js";
metrics.type = "pWeaken2";
metrics.time = wTime;
metrics.end = wEnd2;
threads = Math.min(wThreads2, bMax);
if (wThreads2 - threads === 0) metrics.report = true;
wThreads2 -= threads;
} else if (gThreads > 0 && mode === 1) {
script = "S4tGrow.js";
metrics.type = "pGrow";
metrics.time = gTime;
metrics.end = gEnd;
threads = Math.min(gThreads, bMax);
metrics.report = false;
gThreads -= threads;
} else if (gThreads > 0 && bMax >= gThreads) {
script = "S4tGrow.js";
metrics.type = "pGrow";
metrics.time = gTime;
metrics.end = gEnd;
threads = gThreads;
metrics.report = false;
gThreads = 0;
} else break;
metrics.server = block.server;
const pid = ns.exec(script, block.server, { threads: threads, temporary: true }, JSON.stringify(metrics));
if (!pid) throw new Error("Unable to assign all jobs.");
block.ram -= 1.75 * threads;
}
}
// Fancy UI stuff to update you on progress.
const tEnd = ((mode === 0 ? wEnd1 : wEnd2) - Date.now()) * batchCount + Date.now();
const timer = setInterval(() => {
ns.clearLog();
switch (mode) {
case 0:
ns.print(`Weakening security on ${values.target}...`);
break;
case 1:
ns.print(`Maximizing money on ${values.target}...`);
break;
case 2:
ns.print(`Finalizing preparation on ${values.target}...`);
}
ns.print(`Security: +${ns.formatNumber(sec - minSec, 3)}`);
ns.print(`Money: \$${ns.formatNumber(money, 2)}/${ns.formatNumber(maxMoney, 2)}`);
const time = tEnd - Date.now();
ns.print(`Estimated time remaining: ${ns.tFormat(time)}`);
ns.print(`~${batchCount} ${(batchCount === 1) ? "batch" : "batches"}.`);
}, 200);
ns.atExit(() => clearInterval(timer));
// Wait for the last weaken to finish.
do await dataPort.nextWrite(); while (!dataPort.read().startsWith("pWeaken"));
clearInterval(timer);
await ns.sleep(100);
money = ns.getServerMoneyAvailable(values.target);
sec = ns.getServerSecurityLevel(values.target);
}
return true;
}
// I don't actually use this anywhere it the code. It's a debugging tool that I use to test the runtimes of functions.
export function benchmark(lambda) {
let result = 0;
for (let i = 0; i <= 1000; ++i) {
const start = performance.now();
lambda(i);
result += performance.now() - start;
}
return result / 1000;
}