Last active 1697523181

My bitburner scripts. adapted from alain mostly

Alyssa Smith revised this gist 1697559181. Go to revision

1 file changed, 2 insertions

stockmaster.js

@@ -1,3 +1,5 @@
1 + // this is in here just to be able to disable toasts
2 +
1 3 import {
2 4 instanceCount, getConfiguration, getNsDataThroughFile, runCommand, getActiveSourceFiles, tryGetBitNodeMultipliers,
3 5 formatMoney, formatNumberShort, formatDuration, getStockSymbols

Alyssa Smith revised this gist 1697559083. Go to revision

8 files changed, 707 insertions, 39 deletions

_firstboot.js

@@ -1,18 +1,16 @@
1 1 /** @param {NS} ns */
2 2 // script to be run after each augment to recover hack level and money
3 - // 1. run scan.js, backdoor n00dles and foodnstuff
3 + // 1. run scan.js, backdoor n00dles and foodnstuff using the links provided
4 4 // 2. run this script, starts hacking those servers
5 5 // 3. once you have 200k, go to aevum, then run alain/casino.js
6 6 // 3a. go to tech store, buy darkweb, connect darkweb, buy -a
7 - // 4. run scan.js, update targets.js with any extra servers you can backdoor, make sure you do backdoor them with the links in scan's output
7 + // 4. run scan.js, nuke and backdoor all available servers
8 8 // 5. run purchase.js, update with higher ram allowance (32>2048) if you have enough money
9 9 // * run delete-pserv.js if there's an issue with buying the servers
10 10 // 6. run init.js, this will boot up stockmaster and stats, as well as starting the hack scripts running across all pservs and home
11 11 export async function main(ns) {
12 12 const scripts = [
13 13 {name: "/alain/stats.js", args:[]},
14 - // {name: "/alain/stockmaster.js", args:[]},
15 - // {name: "/pserv.js", args:[]},
16 14 {name: "/local.js", args:[]},
17 15 ];
18 16 for(const {name,args,waitFor=false} of scripts) {

_init.js

@@ -2,13 +2,19 @@
2 2 export async function main(ns) {
3 3 const scripts = [
4 4 {name: "/alain/stats.js", args:[]},
5 - {name: "/alain/stockmaster.js", args:[]},
6 - {name: "/pserv.js", args:[]},
7 - {name: "/local.js", args:[]},
5 + {name: "/stockmaster.js", args:["--toastStyle", ""], tail:true},
6 + {name: "/pserv.js", args:[], tail:true},
7 + {name: "/local.js", args:[], tail:true},
8 + {name: "/alain/hacknet-upgrade-manager.js", args:["-c", "--reserve", "5000000000", "--time", "1000h", "--interval", "100"], tail:true},
8 9 ];
9 - for(const {name,args,waitFor=false} of scripts) {
10 + for(const {name,args,waitFor=false,tail=false} of scripts) {
10 11 const pid = ns.run(name,1,...args);
11 12 if(pid) {
13 + if (tail) {
14 + ns.tail(pid);
15 + await ns.sleep(0);
16 + ns.resizeTail(425, 203, pid);
17 + }
12 18 ns.tprint(`Started ${name} with [${args}]`);
13 19 if(waitFor) {
14 20 ns.tprint("Waiting for it to exit...");

local.js

@@ -2,9 +2,7 @@ export async function main(ns) {
2 2 var i = 0;
3 3 var ram = ns.getScriptRam("early-hack-template.js")*100,
4 4 total_ram = ns.getServerMaxRam("home")-1024,
5 - used_ram = ns.getServerUsedRam("home")
6 -
7 - ns.tail();
5 + used_ram = ns.getServerUsedRam("home");
8 6
9 7 while (i < ((total_ram-used_ram)/ram)) {
10 8 ns.run("early-hack-template.js", 100);

open.js(file created)

@@ -0,0 +1,11 @@
1 + /** @param {NS} ns **/
2 + export async function main(ns) {
3 + ns.tail()
4 + const target = ns.args[0];
5 + try { ns.brutessh(target); } catch { }
6 + try { ns.ftpcrack(target); } catch { }
7 + try { ns.relaysmtp(target); } catch { }
8 + try { ns.httpworm(target); } catch { }
9 + try { ns.sqlinject(target); } catch { }
10 + try { ns.nuke(target); } catch { }
11 + }

pserv.js

@@ -3,7 +3,6 @@ import { targets } from "/targets.js";
3 3 export async function main(ns) {
4 4 // Iterator we'll use for our loop
5 5 let i = 0;
6 - ns.tail();
7 6
8 7 while (i < ns.getPurchasedServerLimit()) {
9 8 let hostname = "pserv-" + i;

scan.js

@@ -3,6 +3,7 @@
3 3 * @returns interactive server map
4 4 */
5 5 export function main(ns) {
6 + var targets_outp = [];
6 7 const factionServers = ["CSEC", "avmnite-02h", "I.I.I.I", "run4theh111z", "w0r1d_d43m0n", "fulcrumassets"],
7 8 css = ` <style id="scanCSS">
8 9 .serverscan {white-space:pre; color:#ccc; font:14px monospace; line-height: 16px; }
@@ -14,6 +15,7 @@ export function main(ns) {
14 15 .serverscan .hack {display:inline-block; font:12px monospace}
15 16 .serverscan .red {color:red;}
16 17 .serverscan .green {color:green;}
18 + .serverscan .nuke {color:#6f3}
17 19 .serverscan .backdoor {color:#6f3}
18 20 .serverscan .linky {font:12px monospace}
19 21 .serverscan .linky > a {cursor:pointer; text-decoration:underline;}
@@ -47,17 +49,26 @@ export function main(ns) {
47 49 canHack = requiredHackLevel <= myHackLevel,
48 50 shouldBackdoor = !server?.backdoorInstalled && canHack && serverName != 'home' && rooted && !server.purchasedByPlayer,
49 51 contracts = ns.ls(serverName, ".cct")
52 + if (canHack &&
53 + serverName != 'home' &&
54 + rooted &&
55 + !server.purchasedByPlayer &&
56 + !factionServers.includes(serverName)) targets_outp.push(serverName);
57 + if (!canHack && ns.args[0] != "-a") return "";
50 58
51 59 return `<span id="${serverName}">`
52 60 + `<a class="server${factionServers.includes(serverName) ? " faction" : ""}`
53 61 + `${rooted ? " rooted" : ""}">${serverName}</a>`
54 62 + (server.purchasedByPlayer ? '' : ` <span class="hack ${(canHack ? 'green' : 'red')} monitor linky">(<a>${requiredHackLevel}</a>)</span>`)
63 + + `${((canHack && !rooted) ? ' <span class="nuke linky">[<a>nuke</a>]</span>' : '')}`
55 64 + `${((canHack && !rooted) || shouldBackdoor ? ' <span class="backdoor linky">[<a>backdoor</a>]</span>' : '')}`
56 65 + ` ${contracts.map(c => `<span class="cct" title="${c}">@</span>`)}`
57 66 + "</span>"
58 67 },
59 68 buildOutput = (parent = servers[0], prefix = ["\n"]) => {
60 - let output = prefix.join("") + createServerEntry(parent)
69 + let serverEntry = createServerEntry(parent);
70 + if (serverEntry == "") return "";
71 + let output = prefix.join("") + serverEntry
61 72 for (let i = 0; i < servers.length; i++) {
62 73 if (parentByIndex[i] != parent) continue
63 74 let newPrefix = prefix.slice()
@@ -105,7 +116,14 @@ export function main(ns) {
105 116 .addEventListener('click', setNavCommand.bind(null, routes[serverEntry.childNodes[0].nodeValue])))
106 117 doc.querySelectorAll(".serverscan.new .monitor").forEach(monitorButton => monitorButton
107 118 .addEventListener('click', setNavCommand.bind(null, "run monitor.js " + monitorButton.parentNode.childNodes[0].childNodes[0].nodeValue)))
119 + doc.querySelectorAll(".serverscan.new .nuke").forEach(nukeButton => nukeButton
120 + .addEventListener('click', setNavCommand.bind(null, "home;run open.js " + nukeButton.parentNode.childNodes[0].childNodes[0].nodeValue)))
108 121 doc.querySelectorAll(".serverscan.new .backdoor").forEach(backdoorButton => backdoorButton
109 - .addEventListener('click', setNavCommand.bind(null, routes[backdoorButton.parentNode.childNodes[0].childNodes[0].nodeValue] + ";run brutessh.exe;run httpworm.exe;run sqlinject.exe;run ftpcrack.exe;run relaysmtp.exe;run nuke.exe;backdoor")))
122 + .addEventListener('click', setNavCommand.bind(null, routes[backdoorButton.parentNode.childNodes[0].childNodes[0].nodeValue] + ";backdoor")))
110 123 doc.querySelector(".serverscan.new").classList.remove("new")
124 + ns.write("/targets.js", "export var targets = [", "w");
125 + for (let name of targets_outp) {
126 + ns.write("/targets.js", `"${name}",`, "a");
127 + }
128 + ns.write("/targets.js", "]", "a")
111 129 }

stockmaster.js(file created)

@@ -0,0 +1,662 @@
1 + import {
2 + instanceCount, getConfiguration, getNsDataThroughFile, runCommand, getActiveSourceFiles, tryGetBitNodeMultipliers,
3 + formatMoney, formatNumberShort, formatDuration, getStockSymbols
4 + } from 'alain/helpers.js'
5 +
6 + let disableShorts = false;
7 + let commission = 100000; // Buy/sell commission. Expected profit must exceed this to buy anything.
8 + let totalProfit = 0.0; // We can keep track of how much we've earned since start.
9 + let lastLog = ""; // We update faster than the stock-market ticks, but we don't log anything unless there's been a change
10 + let allStockSymbols = null; // Stores the set of all symbols collected at start
11 + let mock = false; // If set to true, will "mock" buy/sell but not actually buy/sell anythingorecast
12 + let noisy = false; // If set to true, tprints and announces each time stocks are bought/sold
13 + let toastStyle = "info";
14 + let dictSourceFiles; // Populated at init, a dictionary of source-files the user has access to, and their level
15 + // Pre-4S configuration (influences how we play the stock market before we have 4S data, after which everything's fool-proof)
16 + let showMarketSummary = false; // If set to true, will always generate and display the pre-4s forecast table in a separate tail window
17 + let minTickHistory; // This much history must be gathered before we will offer a stock forecast.
18 + let longTermForecastWindowLength; // This much history will be used to determine the historical probability of the stock (so long as no inversions are detected)
19 + let nearTermForecastWindowLength; // This much history will be used to detect recent negative trends and act on them immediately.
20 + // The following pre-4s constants are hard-coded (not configurable via command line) but may require tweaking
21 + const marketCycleLength = 75; // Every this many ticks, all stocks have a 45% chance of "reversing" their probability. Something we must detect and act on quick to not lose profits.
22 + const maxTickHistory = 151; // This much history will be kept for purposes of detemining volatility and perhaps one day pinpointing the market cycle tick
23 + const inversionDetectionTolerance = 0.10; // If the near-term forecast is within this distance of (1 - long-term forecast), consider it a potential "inversion"
24 + const inversionLagTolerance = 5; // An inversion is "trusted" up to this many ticks after the normal nearTermForecastWindowLength expected detection time
25 + // (Note: 33 total stocks * 45% inversion chance each cycle = ~15 expected inversions per cycle)
26 + // The following pre-4s values are set during the lifetime of the program
27 + let marketCycleDetected = false; // We should not make risky purchasing decisions until the stock market cycle is detected. This can take a long time, but we'll be thanked
28 + let detectedCycleTick = 0; // This will be reset to zero once we've detected the market cycle point.
29 + let inversionAgreementThreshold = 6; // If this many stocks are detected as being in an "inversion", consider this the stock market cycle point
30 + const expectedTickTime = 6000;
31 + const catchUpTickTime = 4000;
32 + let lastTick = 0;
33 + let sleepInterval = 1000;
34 + let resetInfo = (/**@returns{ResetInfo}*/() => undefined)(); // Information about the current bitnode
35 +
36 + let options;
37 + const argsSchema = [
38 + ['l', false], // Stop any other running stockmaster.js instances and sell all stocks
39 + ['liquidate', false], // Long-form alias for the above flag.
40 + ['mock', false], // If set to true, will "mock" buy/sell but not actually buy/sell anything
41 + ['noisy', false], // If set to true, tprints and announces each time stocks are bought/sold
42 + ['toastStyle', toastStyle],
43 + ['disable-shorts', false], // If set to true, will not short any stocks. Will be set depending on having SF8.2 by default.
44 + ['reserve', null], // A fixed amount of money to not spend
45 + ['fracB', 0.4], // Fraction of assets to have as liquid before we consider buying more stock
46 + ['fracH', 0.2], // Fraction of assets to retain as cash in hand when buying
47 + ['buy-threshold', 0.0001], // Buy only stocks forecasted to earn better than a 0.01% return (1 Basis Point)
48 + ['sell-threshold', 0], // Sell stocks forecasted to earn less than this return (default 0% - which happens when prob hits 50% or worse)
49 + ['diversification', 0.34], // Before we have 4S data, we will not hold more than this fraction of our portfolio as a single stock
50 + ['disableHud', false], // Disable showing stock value in the HUD panel
51 + ['disable-purchase-tix-api', false], // Disable purchasing the TIX API if you do not already have it.
52 + // The following settings are related only to tweaking pre-4s stock-market logic
53 + ['show-pre-4s-forecast', false], // If set to true, will always generate and display the pre-4s forecast (if false, it's only shown while we hold no stocks)
54 + ['show-market-summary', false], // Same effect as "show-pre-4s-forecast", this market summary has become so informative, it's valuable even with 4s
55 + ['pre-4s-buy-threshold-probability', 0.15], // Before we have 4S data, only buy stocks whose probability is more than this far away from 0.5, to account for imprecision
56 + ['pre-4s-buy-threshold-return', 0.0015], // Before we have 4S data, Buy only stocks forecasted to earn better than this return (default 0.25% or 25 Basis Points)
57 + ['pre-4s-sell-threshold-return', 0.0005], // Before we have 4S data, Sell stocks forecasted to earn less than this return (default 0.15% or 15 Basis Points)
58 + ['pre-4s-min-tick-history', 21], // This much history must be gathered before we will use pre-4s stock forecasts to make buy/sell decisions. (Default 21)
59 + ['pre-4s-forecast-window', 51], // This much history will be used to determine the historical probability of the stock (so long as no inversions are detected) (Default 76)
60 + ['pre-4s-inversion-detection-window', 10], // This much history will be used to detect recent negative trends and act on them immediately. (Default 10)
61 + ['pre-4s-min-blackout-window', 10], // Do not make any new purchases this many ticks before the detected stock market cycle tick, to avoid buying a position that reverses soon after
62 + ['pre-4s-minimum-hold-time', 10], // A recently bought position must be held for this long before selling, to avoid rash decisions due to noise after a fresh market cycle. (Default 10)
63 + ['buy-4s-budget', 0.8], // Maximum corpus value we will sacrifice in order to buy 4S. Setting to 0 will never buy 4s.
64 + ];
65 +
66 + export function autocomplete(data, args) {
67 + data.flags(argsSchema);
68 + return [];
69 + }
70 +
71 + /** Requires access to the TIX API. Purchases access to the 4S Mkt Data API as soon as it can
72 + * @param {NS} ns */
73 + export async function main(ns) {
74 + const runOptions = getConfiguration(ns, argsSchema);
75 + if (!runOptions) return; // Invalid options, or ran in --help mode.
76 +
77 + // If given the "liquidate" command, try to kill any versions of this script trading in stocks
78 + // NOTE: We must do this immediately before we start resetting / overwriting global state below (which is shared between script instances)
79 + const hasTixApiAccess = await getNsDataThroughFile(ns, 'ns.stock.hasTIXAPIAccess()');
80 + if (runOptions.l || runOptions.liquidate) {
81 + if (!hasTixApiAccess) return log(ns, 'ERROR: Cannot liquidate stocks because we do not have Tix Api Access', true, 'error');
82 + log(ns, 'INFO: Killing any other stockmaster processes...', false, toastStyle);
83 + await runCommand(ns, `ns.ps().filter(proc => proc.filename == '${ns.getScriptName()}' && !proc.args.includes('-l') && !proc.args.includes('--liquidate'))` +
84 + `.forEach(proc => ns.kill(proc.pid))`, '/Temp/kill-stockmarket-scripts.js');
85 + log(ns, 'INFO: Checking for and liquidating any stocks...', false, toastStyle);
86 + await liquidate(ns); // Sell all stocks
87 + return;
88 + } // Otherwise, prevent multiple instances of this script from being started, even with different args.
89 + if (await instanceCount(ns) > 1) return;
90 +
91 + ns.disableLog("ALL");
92 + // Extract various options from the args (globals, purchasing decision factors, pre-4s factors)
93 + options = runOptions; // We don't set the global "options" until we're sure this is the only running instance
94 + mock = options.mock;
95 + noisy = options.noisy;
96 + const fracB = options.fracB;
97 + const fracH = options.fracH;
98 + const diversification = options.diversification;
99 + const disableHud = options.disableHud || options.liquidate || options.mock;
100 + disableShorts = options['disable-shorts'];
101 + const pre4sBuyThresholdProbability = options['pre-4s-buy-threshold-probability'];
102 + const pre4sMinBlackoutWindow = options['pre-4s-min-blackout-window'] || 1;
103 + const pre4sMinHoldTime = options['pre-4s-minimum-hold-time'] || 0;
104 + minTickHistory = options['pre-4s-min-tick-history'] || 21;
105 + nearTermForecastWindowLength = options['pre-4s-inversion-detection-window'] || 10;
106 + longTermForecastWindowLength = options['pre-4s-forecast-window'] || (marketCycleLength + 1);
107 + showMarketSummary = options['show-pre-4s-forecast'] || options['show-market-summary'];
108 + // Other global values must be reset at start lest they be left in memory from a prior run
109 + lastTick = 0, totalProfit = 0, lastLog = "", marketCycleDetected = false, detectedCycleTick = 0, inversionAgreementThreshold = 6;
110 + let myStocks = [], allStocks = [];
111 + let player = await getPlayerInfo(ns);
112 + resetInfo = await getNsDataThroughFile(ns, 'ns.getResetInfo()');
113 +
114 + if (!hasTixApiAccess) { // You cannot use the stockmaster until you have API access
115 + if (options['disable-purchase-tix-api'])
116 + return log(ns, "ERROR: You do not have stock market API access, and --disable-purchase-tix-api is set.", true);
117 + let success = false;
118 + log(ns, `INFO: You are missing stock market API access. (NOTE: This is granted for free once you have SF8). ` +
119 + `Waiting until we can have the 5b needed to buy it. (Run with --disable-purchase-tix-api to disable this feature.)`, true);
120 + do {
121 + await ns.sleep(sleepInterval);
122 + try {
123 + const reserve = options['reserve'] != null ? options['reserve'] : Number(ns.read("reserve.txt") || 0);
124 + success = await tryGetStockMarketAccess(ns, player.money - reserve);
125 + } catch (err) {
126 + log(ns, `WARNING: stockmaster.js Caught (and suppressed) an unexpected error while waiting to buy stock market access:\n` +
127 + (typeof err === 'string' ? err : err.message || JSON.stringify(err)), false, 'warning');
128 + }
129 + } while (!success);
130 + }
131 +
132 + dictSourceFiles = await getActiveSourceFiles(ns); // Find out what source files the user has unlocked
133 + if (!disableShorts && (!(8 in dictSourceFiles) || dictSourceFiles[8] < 2)) {
134 + log(ns, "INFO: Shorting stocks has been disabled (you have not yet unlocked access to shorting)");
135 + disableShorts = true;
136 + }
137 +
138 + allStockSymbols = await getStockSymbols(ns);
139 + allStocks = await initAllStocks(ns);
140 +
141 + let bitnodeMults;
142 + if (5 in dictSourceFiles) bitnodeMults = await tryGetBitNodeMultipliers(ns);
143 + // Assume bitnode mults are 1 if user doesn't have this API access yet
144 + if (!bitnodeMults) bitnodeMults = { FourSigmaMarketDataCost: 1, FourSigmaMarketDataApiCost: 1 };
145 +
146 + if (showMarketSummary) await launchSummaryTail(ns); // Opens a separate script / window to continuously display the Pre4S forecast
147 +
148 + let hudElement = null;
149 + if (!disableHud) {
150 + hudElement = initializeHud();
151 + ns.atExit(() => hudElement.parentElement.parentElement.parentElement.removeChild(hudElement.parentElement.parentElement));
152 + }
153 +
154 + log(ns, `Welcome! Please note: all stock purchases will initially result in a Net (unrealized) Loss. This is not only due to commission, but because each stock has a 'spread' (difference in buy price and sell price). ` +
155 + `This script is designed to buy stocks that are most likely to surpass that loss and turn a profit, but it will take a few minutes to see the progress.\n\n` +
156 + `If you choose to stop the script, make sure you SELL all your stocks (can go 'run ${ns.getScriptName()} --liquidate') to get your money back.\n\nGood luck!\n~ Insight\n\n`)
157 +
158 + let pre4s = true;
159 + while (true) {
160 + try {
161 + const playerStats = await getPlayerInfo(ns);
162 + const reserve = options['reserve'] != null ? options['reserve'] : Number(ns.read("reserve.txt") || 0);
163 + // Check whether we have 4s access yes (once we do, we can stop checking)
164 + if (pre4s) pre4s = !(await checkAccess(ns, 'has4SDataTIXAPI'));
165 + const holdings = await refresh(ns, !pre4s, allStocks, myStocks); // Returns total stock value
166 + const corpus = holdings + playerStats.money; // Corpus means total stocks + cash
167 + const maxHoldings = (1 - fracH) * corpus; // The largest value of stock we could hold without violiating fracH (Fraction to keep as cash)
168 + if (pre4s && !mock && await tryGet4SApi(ns, playerStats, bitnodeMults, corpus * (options['buy-4s-budget'] - fracH) - reserve))
169 + continue; // Start the loop over if we just bought 4S API access
170 + // Be more conservative with our decisions if we don't have 4S data
171 + const thresholdToBuy = pre4s ? options['pre-4s-buy-threshold-return'] : options['buy-threshold'];
172 + const thresholdToSell = pre4s ? options['pre-4s-sell-threshold-return'] : options['sell-threshold'];
173 + if (myStocks.length > 0)
174 + doStatusUpdate(ns, allStocks, myStocks, hudElement);
175 + else if (hudElement) hudElement.innerText = "$0.000 ";
176 + if (pre4s && allStocks[0].priceHistory.length < minTickHistory) {
177 + log(ns, `Building a history of stock prices (${allStocks[0].priceHistory.length}/${minTickHistory})...`);
178 + await ns.sleep(sleepInterval);
179 + continue;
180 + }
181 +
182 + // Sell forecasted-to-underperform shares (worse than some expected return threshold)
183 + let sales = 0;
184 + for (let stk of myStocks) {
185 + if (stk.absReturn() <= thresholdToSell || stk.bullish() && stk.sharesShort > 0 || stk.bearish() && stk.sharesLong > 0) {
186 + if (pre4s && stk.ticksHeld < pre4sMinHoldTime) {
187 + if (!stk.warnedBadPurchase) log(ns, `WARNING: Thinking of selling ${stk.sym} with ER ${formatBP(stk.absReturn())}, but holding out as it was purchased just ${stk.ticksHeld} ticks ago...`);
188 + stk.warnedBadPurchase = true; // Hack to ensure we don't spam this warning
189 + } else {
190 + sales += await doSellAll(ns, stk);
191 + stk.warnedBadPurchase = false;
192 + }
193 + }
194 + }
195 + if (sales > 0) continue; // If we sold anything, loop immediately (no need to sleep) and refresh stats immediately before making purchasing decisions.
196 +
197 + // If we haven't gone above a certain liquidity threshold, don't attempt to buy more stock
198 + // Avoids death-by-a-thousand-commissions before we get super-rich, stocks are capped, and this is no longer an issue
199 + // BUT may mean we miss striking while the iron is hot while waiting to build up more funds.
200 + if (playerStats.money / corpus > fracB) {
201 + // Compute the cash we have to spend (such that spending it all on stock would bring us down to a liquidity of fracH)
202 + let cash = Math.min(playerStats.money - reserve, maxHoldings - holdings);
203 + // If we haven't detected the market cycle (or haven't detected it reliably), assume it might be quite soon and restrict bets to those that can turn a profit in the very-near term.
204 + const estTick = Math.max(detectedCycleTick, marketCycleLength - (!marketCycleDetected ? 10 : inversionAgreementThreshold <= 8 ? 20 : inversionAgreementThreshold <= 10 ? 30 : marketCycleLength));
205 + // Buy shares with cash remaining in hand if exceeding some buy threshold. Prioritize targets whose expected return will cover the ask/bit spread the soonest
206 + for (const stk of allStocks.sort(purchaseOrder)) {
207 + if (cash <= 0) break; // Break if we are out of money (i.e. from prior purchases)
208 + // Do not purchase a stock if it is not forecasted to recover from the ask/bid spread before the next market cycle and potential probability inversion
209 + if (stk.blackoutWindow() >= marketCycleLength - estTick) continue;
210 + if (pre4s && (Math.max(pre4sMinHoldTime, pre4sMinBlackoutWindow) >= marketCycleLength - estTick)) continue;
211 + // Skip if we already own all possible shares in this stock, or if the expected return is below our threshold, or if shorts are disabled and stock is bearish
212 + if (stk.ownedShares() == stk.maxShares || stk.absReturn() <= thresholdToBuy || (disableShorts && stk.bearish())) continue;
213 + // If pre-4s, do not purchase any stock whose last inversion was too recent, or whose probability is too close to 0.5
214 + if (pre4s && (stk.lastInversion < minTickHistory || Math.abs(stk.prob - 0.5) < pre4sBuyThresholdProbability)) continue;
215 +
216 + // Enforce diversification: Don't hold more than x% of our portfolio as a single stock (as corpus increases, this naturally stops being a limiter)
217 + // Inflate our budget / current position value by a factor of stk.spread_pct to avoid repeated micro-buys of a stock due to the buy/ask spread making holdings appear more diversified after purchase
218 + let budget = Math.min(cash, maxHoldings * (diversification + stk.spread_pct) - stk.positionValue() * (1.01 + stk.spread_pct))
219 + let purchasePrice = stk.bullish() ? stk.ask_price : stk.bid_price; // Depends on whether we will be buying a long or short position
220 + let affordableShares = Math.floor((budget - commission) / purchasePrice);
221 + let numShares = Math.min(stk.maxShares - stk.ownedShares(), affordableShares);
222 + if (numShares <= 0) continue;
223 + // Don't buy fewer shares than can beat the comission before the next stock market cycle (after covering the spread), lest the position reverse before we break-even.
224 + let ticksBeforeCycleEnd = marketCycleLength - estTick - stk.timeToCoverTheSpread();
225 + if (ticksBeforeCycleEnd < 1) continue; // We're cutting it too close to the market cycle, position might reverse before we break-even on commission
226 + let estEndOfCycleValue = numShares * purchasePrice * ((stk.absReturn() + 1) ** ticksBeforeCycleEnd - 1); // Expected difference in purchase price and value at next market cycle end
227 + let owned = stk.ownedShares() > 0;
228 + if (estEndOfCycleValue <= 2 * commission)
229 + log(ns, (owned ? '' : `We currently have ${formatNumberShort(stk.ownedShares(), 3, 1)} shares in ${stk.sym} valued at ${formatMoney(stk.positionValue())} ` +
230 + `(${(100 * stk.positionValue() / maxHoldings).toFixed(1)}% of corpus, capped at ${(diversification * 100).toFixed(1)}% by --diversification).\n`) +
231 + `Despite attractive ER of ${formatBP(stk.absReturn())}, ${owned ? 'more ' : ''}${stk.sym} was not bought. ` +
232 + `\nBudget: ${formatMoney(budget)} can only buy ${numShares.toLocaleString('en')} ${owned ? 'more ' : ''}shares @ ${formatMoney(purchasePrice)}. ` +
233 + `\nGiven an estimated ${marketCycleLength - estTick} ticks left in market cycle, less ${stk.timeToCoverTheSpread().toFixed(1)} ticks to cover the spread (${(stk.spread_pct * 100).toFixed(2)}%), ` +
234 + `remaining ${ticksBeforeCycleEnd.toFixed(1)} ticks would only generate ${formatMoney(estEndOfCycleValue)}, which is less than 2x commission (${formatMoney(2 * commission, 3)})`);
235 + else
236 + cash -= await doBuy(ns, stk, numShares);
237 + }
238 + }
239 + } catch (err) {
240 + log(ns, `WARNING: stockmaster.js Caught (and suppressed) an unexpected error in the main loop:\n` +
241 + (typeof err === 'string' ? err : err.message || JSON.stringify(err)), false, 'warning');
242 + }
243 + await ns.sleep(sleepInterval);
244 + }
245 + }
246 +
247 + /** Ram-dodge getting updated player info. Note that this is the only async routine called in the main loop.
248 + * If latency or ram instability is an issue, you may wish to try uncommenting the direct request.
249 + * @param {NS} ns
250 + * @returns {Promise<Player>} */
251 + async function getPlayerInfo(ns) {
252 + return await getNsDataThroughFile(ns, `ns.getPlayer()`);
253 + }
254 +
255 + function getTimeInBitnode() { return Date.now() - resetInfo.lastNodeReset; }
256 +
257 + /* A sorting function to put stocks in the order we should prioritize investing in them */
258 + let purchaseOrder = (a, b) => (Math.ceil(a.timeToCoverTheSpread()) - Math.ceil(b.timeToCoverTheSpread())) || (b.absReturn() - a.absReturn());
259 +
260 + /** @param {NS} ns
261 + * Generic helper for dodging the hefty RAM requirements of stock functions by spawning a temporary script to collect info for us. */
262 + async function getStockInfoDict(ns, stockFunction) {
263 + allStockSymbols ??= await getStockSymbols(ns);
264 + if (allStockSymbols == null) throw new Error(`No WSE API Access yet, this call to ns.stock.${stockFunction} is premature.`);
265 + return await getNsDataThroughFile(ns,
266 + `Object.fromEntries(ns.args.map(sym => [sym, ns.stock.${stockFunction}(sym)]))`,
267 + `/Temp/stock-${stockFunction}.txt`, allStockSymbols);
268 + };
269 +
270 + /** @param {NS} ns **/
271 + async function initAllStocks(ns) {
272 + let dictMaxShares = await getStockInfoDict(ns, 'getMaxShares'); // Only need to get this once, it never changes
273 + return allStockSymbols.map(s => ({
274 + sym: s,
275 + maxShares: dictMaxShares[s], // Value never changes once retrieved
276 + expectedReturn: function () { // How much holdings are expected to appreciate (or depreciate) in the future
277 + // To add conservatism to pre-4s estimates, we reduce the probability by 1 standard deviation without crossing the midpoint
278 + let normalizedProb = (this.prob - 0.5);
279 + let conservativeProb = normalizedProb < 0 ? Math.min(0, normalizedProb + this.probStdDev) : Math.max(0, normalizedProb - this.probStdDev);
280 + return this.vol * conservativeProb;
281 + },
282 + absReturn: function () { return Math.abs(this.expectedReturn()); }, // Appropriate to use when can just as well buy a short position as a long position
283 + bullish: function () { return this.prob > 0.5 },
284 + bearish: function () { return !this.bullish(); },
285 + ownedShares: function () { return this.sharesLong + this.sharesShort; },
286 + owned: function () { return this.ownedShares() > 0; },
287 + positionValueLong: function () { return this.sharesLong * this.bid_price; },
288 + positionValueShort: function () { return this.sharesShort * (2 * this.boughtPriceShort - this.ask_price); }, // Shorts work a bit weird
289 + positionValue: function () { return this.positionValueLong() + this.positionValueShort(); },
290 + // How many stock market ticks must occur at the current expected return before we regain the value lost by the spread between buy and sell prices.
291 + // This can be derived by taking the compound interest formula (future = current * (1 + expected_return) ^ n) and solving for n
292 + timeToCoverTheSpread: function () { return Math.log(this.ask_price / this.bid_price) / Math.log(1 + this.absReturn()); },
293 + // We should not buy this stock within this many ticks of a Market cycle, or we risk being forced to sell due to a probability inversion, and losing money due to the spread
294 + blackoutWindow: function () { return Math.ceil(this.timeToCoverTheSpread()); },
295 + // Pre-4s properties used for forecasting
296 + priceHistory: [],
297 + lastInversion: 0,
298 + }));
299 + }
300 +
301 + /** @param {NS} ns **/
302 + async function refresh(ns, has4s, allStocks, myStocks) {
303 + let holdings = 0;
304 +
305 + // Dodge hefty RAM requirements by spawning a sequence of temporary scripts to collect info for us one function at a time
306 + const dictAskPrices = await getStockInfoDict(ns, 'getAskPrice');
307 + const dictBidPrices = await getStockInfoDict(ns, 'getBidPrice');
308 + const dictVolatilities = !has4s ? null : await getStockInfoDict(ns, 'getVolatility');
309 + const dictForecasts = !has4s ? null : await getStockInfoDict(ns, 'getForecast');
310 + const dictPositions = mock ? null : await getStockInfoDict(ns, 'getPosition');
311 + const ticked = allStocks.some(stk => stk.ask_price != dictAskPrices[stk.sym]); // If any price has changed since our last update, the stock market has "ticked"
312 +
313 + if (ticked) {
314 + if (Date.now() - lastTick < expectedTickTime - sleepInterval) {
315 + if (Date.now() - lastTick < catchUpTickTime - sleepInterval) {
316 + let changedPrices = allStocks.filter(stk => stk.ask_price != dictAskPrices[stk.sym]);
317 + log(ns, `WARNING: Detected a stock market tick after only ${formatDuration(Date.now() - lastTick)}, but expected ~${formatDuration(expectedTickTime)}. ` +
318 + (changedPrices.length >= 33 ? '(All stocks updated)' : `The following ${changedPrices.length} stock prices changed: ${changedPrices.map(stk =>
319 + `${stk.sym} ${formatMoney(stk.ask_price)} -> ${formatMoney(dictAskPrices[stk.sym])}`).join(", ")}`), false, 'warning');
320 + } else
321 + log(ns, `INFO: Detected a rapid stock market tick (${formatDuration(Date.now() - lastTick)}), likely to make up for lag / offline time.`)
322 + }
323 + lastTick = Date.now()
324 + }
325 +
326 + myStocks.length = 0;
327 + for (const stk of allStocks) {
328 + const sym = stk.sym;
329 + stk.ask_price = dictAskPrices[sym]; // The amount we would pay if we bought the stock (higher than 'price')
330 + stk.bid_price = dictBidPrices[sym]; // The amount we would recieve if we sold the stock (lower than 'price')
331 + stk.spread = stk.ask_price - stk.bid_price;
332 + stk.spread_pct = stk.spread / stk.ask_price; // The percentage of value we lose just by buying the stock
333 + stk.price = (stk.ask_price + stk.bid_price) / 2; // = ns.stock.getPrice(sym);
334 + stk.vol = has4s ? dictVolatilities[sym] : stk.vol;
335 + stk.prob = has4s ? dictForecasts[sym] : stk.prob;
336 + stk.probStdDev = has4s ? 0 : stk.probStdDev; // Standard deviation around the est. probability
337 + // Update our current portfolio of owned stock
338 + let [priorLong, priorShort] = [stk.sharesLong, stk.sharesShort];
339 + stk.position = mock ? null : dictPositions[sym];
340 + stk.sharesLong = mock ? (stk.sharesLong || 0) : stk.position[0];
341 + stk.boughtPrice = mock ? (stk.boughtPrice || 0) : stk.position[1];
342 + stk.sharesShort = mock ? (stk.shares_short || 0) : stk.position[2];
343 + stk.boughtPriceShort = mock ? (stk.boughtPrice_short || 0) : stk.position[3];
344 + holdings += stk.positionValue();
345 + if (stk.owned()) myStocks.push(stk); else stk.ticksHeld = 0;
346 + if (ticked) // Increment ticksHeld, or reset it if we have no position in this stock or reversed our position last tick.
347 + stk.ticksHeld = !stk.owned() || (priorLong > 0 && stk.sharesLong == 0) || (priorShort > 0 && stk.sharesShort == 0) ? 0 : 1 + (stk.ticksHeld || 0);
348 + }
349 + if (ticked) await updateForecast(ns, allStocks, has4s); // Logic below only required on market tick
350 + return holdings;
351 + }
352 +
353 + // Historical probability can be inferred from the number of times the stock was recently observed increasing over the total number of observations
354 + const forecast = history => history.reduce((ups, price, idx) => idx == 0 ? 0 : (history[idx - 1] > price ? ups + 1 : ups), 0) / (history.length - 1);
355 + // An "inversion" can be detected if two probabilities are far enough apart and are within "tolerance" of p1 being equal to 1-p2
356 + const tol2 = inversionDetectionTolerance / 2;
357 + const detectInversion = (p1, p2) => ((p1 >= 0.5 + tol2) && (p2 <= 0.5 - tol2) && p2 <= (1 - p1) + inversionDetectionTolerance)
358 + /* Reverse Condition: */ || ((p1 <= 0.5 - tol2) && (p2 >= 0.5 + tol2) && p2 >= (1 - p1) - inversionDetectionTolerance);
359 +
360 + /** @param {NS} ns **/
361 + async function updateForecast(ns, allStocks, has4s) {
362 + const currentHistory = allStocks[0].priceHistory.length;
363 + const prepSummary = showMarketSummary || mock || (!has4s && (currentHistory < minTickHistory || allStocks.filter(stk => stk.owned()).length == 0)); // Decide whether to display the market summary table.
364 + const inversionsDetected = []; // Keep track of individual stocks whose probability has inverted (45% chance of happening each "cycle")
365 + detectedCycleTick = (detectedCycleTick + 1) % marketCycleLength; // Keep track of stock market cycle (which occurs every 75 ticks)
366 + for (const stk of allStocks) {
367 + stk.priceHistory.unshift(stk.price);
368 + if (stk.priceHistory.length > maxTickHistory) // Limit the rolling window size
369 + stk.priceHistory.splice(maxTickHistory, 1);
370 + // Volatility is easy - the largest observed % movement in a single tick
371 + if (!has4s) stk.vol = stk.priceHistory.reduce((max, price, idx) => Math.max(max, idx == 0 ? 0 : Math.abs(stk.priceHistory[idx - 1] - price) / price), 0);
372 + // We want stocks that have the best expected return, averaged over a long window for greater precision, but the game will occasionally invert probabilities
373 + // (45% chance every 75 updates), so we also compute a near-term forecast window to allow for early-detection of inversions so we can ditch our position.
374 + stk.nearTermForecast = forecast(stk.priceHistory.slice(0, nearTermForecastWindowLength));
375 + let preNearTermWindowProb = forecast(stk.priceHistory.slice(nearTermForecastWindowLength, nearTermForecastWindowLength + marketCycleLength)); // Used to detect the probability before the potential inversion event.
376 + // Detect whether it appears as though the probability of this stock has recently undergone an inversion (i.e. prob => 1 - prob)
377 + stk.possibleInversionDetected = has4s ? detectInversion(stk.prob, stk.lastTickProbability || stk.prob) : detectInversion(preNearTermWindowProb, stk.nearTermForecast);
378 + stk.lastTickProbability = stk.prob;
379 + if (stk.possibleInversionDetected) inversionsDetected.push(stk);
380 + }
381 + // Detect whether our auto-detected "stock market cycle" timing should be adjusted based on the number of potential inversions observed
382 + let summary = "";
383 + if (inversionsDetected.length > 0) {
384 + summary += `${inversionsDetected.length} Stocks appear to be reversing their outlook: ${inversionsDetected.map(s => s.sym).join(', ')} (threshold: ${inversionAgreementThreshold})\n`;
385 + if (inversionsDetected.length >= inversionAgreementThreshold && (has4s || currentHistory >= minTickHistory)) { // We believe we have detected the stock market cycle!
386 + const newPredictedCycleTick = has4s ? 0 : nearTermForecastWindowLength; // By the time we've detected it, we're this many ticks past the cycle start
387 + if (detectedCycleTick != newPredictedCycleTick)
388 + log(ns, `Threshold for changing predicted market cycle met (${inversionsDetected.length} >= ${inversionAgreementThreshold}). ` +
389 + `Changing current market tick from ${detectedCycleTick} to ${newPredictedCycleTick}.`);
390 + marketCycleDetected = true;
391 + detectedCycleTick = newPredictedCycleTick;
392 + // Don't adjust this in the future unless we see another day with as much or even more agreement (capped at 14, it seems sometimes our cycles get out of sync with
393 + // actual cycles and we need to reset our clock even after previously determining the cycle with great certainty.)
394 + inversionAgreementThreshold = Math.max(14, inversionsDetected.length);
395 + }
396 + }
397 + // Act on any inversions (if trusted), compute the probability, and prepare the stock summary
398 + for (const stk of allStocks) {
399 + // Don't "trust" (act on) a detected inversion unless it's near the time when we're capable of detecting market cycle start. Avoids most false-positives.
400 + if (stk.possibleInversionDetected && (has4s && detectedCycleTick == 0 ||
401 + (!has4s && (detectedCycleTick >= nearTermForecastWindowLength / 2) && (detectedCycleTick <= nearTermForecastWindowLength + inversionLagTolerance))))
402 + stk.lastInversion = detectedCycleTick; // If we "trust" a probability inversion has occurred, probability will be calculated based on only history since the last inversion.
403 + else
404 + stk.lastInversion++;
405 + // Only take the stock history since after the last inversion to compute the probability of the stock.
406 + const probWindowLength = Math.min(longTermForecastWindowLength, stk.lastInversion);
407 + stk.longTermForecast = forecast(stk.priceHistory.slice(0, probWindowLength));
408 + if (!has4s) {
409 + stk.prob = stk.longTermForecast;
410 + stk.probStdDev = Math.sqrt((stk.prob * (1 - stk.prob)) / probWindowLength);
411 + }
412 + const signalStrength = 1 + (stk.bullish() ? (stk.nearTermForecast > stk.prob ? 1 : 0) + (stk.prob > 0.8 ? 1 : 0) : (stk.nearTermForecast < stk.prob ? 1 : 0) + (stk.prob < 0.2 ? 1 : 0));
413 + if (prepSummary) { // Example: AERO ++ Prob: 54% (t51: 54%, t10: 67%) tLast⇄:190 Vol:0.640% ER: 2.778BP Spread:1.784% ttProfit: 65 Pos: 14.7M long (held 189 ticks)
414 + stk.debugLog = `${stk.sym.padEnd(5, ' ')} ${(stk.bullish() ? '+' : '-').repeat(signalStrength).padEnd(3)} ` +
415 + `Prob:${(stk.prob * 100).toFixed(0).padStart(3)}% (t${probWindowLength.toFixed(0).padStart(2)}:${(stk.longTermForecast * 100).toFixed(0).padStart(3)}%, ` +
416 + `t${Math.min(stk.priceHistory.length, nearTermForecastWindowLength).toFixed(0).padStart(2)}:${(stk.nearTermForecast * 100).toFixed(0).padStart(3)}%) ` +
417 + `tLast⇄:${(stk.lastInversion + 1).toFixed(0).padStart(3)} Vol:${(stk.vol * 100).toFixed(2)}% ER:${formatBP(stk.expectedReturn()).padStart(8)} ` +
418 + `Spread:${(stk.spread_pct * 100).toFixed(2)}% ttProfit:${stk.blackoutWindow().toFixed(0).padStart(3)}`;
419 + if (stk.owned()) stk.debugLog += ` Pos: ${formatNumberShort(stk.ownedShares(), 3, 1)} (${stk.ownedShares() == stk.maxShares ? 'max' :
420 + ((100 * stk.ownedShares() / stk.maxShares).toFixed(0).padStart(2) + '%')}) ${stk.sharesLong > 0 ? 'long ' : 'short'} (held ${stk.ticksHeld} ticks)`;
421 + if (stk.possibleInversionDetected) stk.debugLog += ' ⇄⇄⇄';
422 + }
423 + }
424 + // Print a summary of stocks as of this most recent tick (if enabled)
425 + if (prepSummary) {
426 + summary += `Market day ${detectedCycleTick + 1}${marketCycleDetected ? '' : '?'} of ${marketCycleLength} (${marketCycleDetected ? (100 * inversionAgreementThreshold / 19).toPrecision(2) : '0'}% certain) ` +
427 + `Current Stock Summary and Pre-4S Forecasts (by best payoff-time):\n` + allStocks.sort(purchaseOrder).map(s => s.debugLog).join("\n")
428 + if (showMarketSummary) await updateForecastFile(ns, summary); else log(ns, summary);
429 + }
430 + // Write out a file of stock probabilities so that other scripts can make use of this (e.g. hack orchestrator can manipulate the stock market)
431 + await ns.write('/Temp/stock-probabilities.txt', JSON.stringify(Object.fromEntries(
432 + allStocks.map(stk => [stk.sym, { prob: stk.prob, sharesLong: stk.sharesLong, sharesShort: stk.sharesShort }]))), "w");
433 + }
434 +
435 + // Helpers to display the stock market summary in a separate window.
436 + let summaryFile = '/Temp/stockmarket-summary.txt';
437 + let updateForecastFile = async (ns, summary) => await ns.write(summaryFile, summary, 'w');
438 + let launchSummaryTail = async ns => {
439 + let summaryTailScript = summaryFile.replace('.txt', '-tail.js');
440 + if (await getNsDataThroughFile(ns, `ns.scriptRunning('${summaryTailScript}', ns.getHostname())`, '/Temp/stockmarket-summary-is-running.txt'))
441 + return;
442 + //await getNsDataThroughFile(ns, `ns.scriptKill('${summaryTailScript}', ns.getHostname())`, summaryTailScript.replace('.js', '-kill.js')); // Only needed if we're changing the script below
443 + await runCommand(ns, `ns.disableLog('sleep'); ns.tail(); let lastRead = '';
444 + while (true) {
445 + let read = ns.read('${summaryFile}');
446 + if (lastRead != read) ns.print(lastRead = read);
447 + await ns.sleep(1000);
448 + }`, summaryTailScript);
449 + }
450 +
451 + // Ram-dodging helpers that spawn temporary scripts to buy/sell rather than pay 2.5GB ram per variant
452 + let buyStockWrapper = async (ns, sym, numShares) => await transactStock(ns, sym, numShares, 'buyStock'); // ns.stock.buyStock(sym, numShares);
453 + let buyShortWrapper = async (ns, sym, numShares) => await transactStock(ns, sym, numShares, 'buyShort'); // ns.stock.buyShort(sym, numShares);
454 + let sellStockWrapper = async (ns, sym, numShares) => await transactStock(ns, sym, numShares, 'sellStock'); // ns.stock.sellStock(sym, numShares);
455 + let sellShortWrapper = async (ns, sym, numShares) => await transactStock(ns, sym, numShares, 'sellShort'); // ns.stock.sellShort(sym, numShares);
456 + let transactStock = async (ns, sym, numShares, action) =>
457 + await getNsDataThroughFile(ns, `ns.stock.${action}(ns.args[0], ns.args[1])`, null, [sym, numShares]);
458 +
459 + /** @param {NS} ns
460 + * Automatically buys either a short or long position depending on the outlook of the stock. */
461 + async function doBuy(ns, stk, sharesToBuy) {
462 + // We include -2*commission in the "holdings value" of our stock, but if we make repeated purchases of the same stock, we have to track
463 + // the additional commission somewhere. So only subtract it from our running profit if this isn't our first purchase of this symbol
464 + let price = 0; //price wasn't defined yet.
465 + if (stk.owned())
466 + totalProfit -= commission;
467 + let long = stk.bullish();
468 + let expectedPrice = long ? stk.ask_price : stk.bid_price; // Depends on whether we will be buying a long or short position
469 + log(ns, `INFO: ${long ? 'Buying ' : 'Shorting'} ${formatNumberShort(sharesToBuy, 3, 3).padStart(5)} (` +
470 + `${stk.maxShares == sharesToBuy + stk.ownedShares() ? '@max shares' : `${formatNumberShort(sharesToBuy + stk.ownedShares(), 3, 3).padStart(5)}/${formatNumberShort(stk.maxShares, 3, 3).padStart(5)}`}) ` +
471 + `${stk.sym.padEnd(5)} @ ${formatMoney(expectedPrice).padStart(9)} for ${formatMoney(sharesToBuy * expectedPrice).padStart(9)} (Spread:${(stk.spread_pct * 100).toFixed(2)}% ` +
472 + `ER:${formatBP(stk.expectedReturn()).padStart(8)}) Ticks to Profit: ${stk.timeToCoverTheSpread().toFixed(2)}`, noisy, toastStyle);
473 + try {
474 + price = mock ? expectedPrice : Number(await transactStock(ns, stk.sym, sharesToBuy, long ? 'buyStock' : 'buyShort'));
475 + } catch (err) {
476 + if (long) throw err;
477 + disableShorts = true;
478 + log(ns, `WARN: Failed to short ${stk.sym} (Shorts not available?). Disabling shorts...`, true, 'warning');
479 + return 0;
480 + }
481 + // The rest of this work is for troubleshooting / mock-mode purposes
482 + if (price == 0) {
483 + const playerMoney = (await getPlayerInfo(ns)).money;
484 + if (playerMoney < sharesToBuy * expectedPrice)
485 + log(ns, `WARN: Failed to ${long ? 'buy' : 'short'} ${stk.sym} because money just recently dropped to ${formatMoney(playerMoney)} and we can no longer afford it.`, noisy);
486 + else
487 + log(ns, `ERROR: Failed to ${long ? 'buy' : 'short'} ${stk.sym} @ ${formatMoney(expectedPrice)} (0 was returned) despite having ${formatMoney(playerMoney)}.`, true, 'error');
488 + return 0;
489 + } else if (price != expectedPrice) {
490 + log(ns, `WARNING: ${long ? 'Bought' : 'Shorted'} ${stk.sym} @ ${formatMoney(price)} but expected ${formatMoney(expectedPrice)} (spread: ${formatMoney(stk.spread)})`, false, 'warning');
491 + price = expectedPrice; // Known Bitburner bug for now, short returns "price" instead of "bid_price". Correct this so running profit calcs are correct.
492 + }
493 + if (mock && long) stk.boughtPrice = (stk.boughtPrice * stk.sharesLong + price * sharesToBuy) / (stk.sharesLong + sharesToBuy);
494 + if (mock && !long) stk.boughtPriceShort = (stk.boughtPriceShort * stk.sharesShort + price * sharesToBuy) / (stk.sharesShort + sharesToBuy);
495 + if (long) stk.sharesLong += sharesToBuy; else stk.sharesShort += sharesToBuy; // Maintained for mock mode, otherwise, redundant (overwritten at next refresh)
496 + return sharesToBuy * price + commission; // Return the amount spent on the transaction so it can be subtracted from our cash on hand
497 + }
498 +
499 + /** @param {NS} ns
500 + * Sell our current position in this stock. */
501 + async function doSellAll(ns, stk) {
502 + let long = stk.sharesLong > 0;
503 + if (long && stk.sharesShort > 0) // Detect any issues here - we should always sell one before buying the other.
504 + log(ns, `ERROR: Somehow ended up both ${stk.sharesShort} short and ${stk.sharesLong} long on ${stk.sym}`, true, 'error');
505 + let expectedPrice = long ? stk.bid_price : stk.ask_price; // Depends on whether we will be selling a long or short position
506 + let sharesSold = long ? stk.sharesLong : stk.sharesShort;
507 + let price = mock ? expectedPrice : await transactStock(ns, stk.sym, sharesSold, long ? 'sellStock' : 'sellShort');
508 + const profit = (long ? stk.sharesLong * (price - stk.boughtPrice) : stk.sharesShort * (stk.boughtPriceShort - price)) - 2 * commission;
509 + log(ns, `${profit > 0 ? 'SUCCESS' : 'WARNING'}: Sold all ${formatNumberShort(sharesSold, 3, 3).padStart(5)} ${stk.sym.padEnd(5)} ${long ? ' long' : 'short'} positions ` +
510 + `@ ${formatMoney(price).padStart(9)} for a ` + (profit > 0 ? `PROFIT of ${formatMoney(profit).padStart(9)}` : ` LOSS of ${formatMoney(-profit).padStart(9)}`) + ` after ${stk.ticksHeld} ticks`,
511 + noisy, noisy ? (profit > 0 ? 'success' : 'error') : undefined);
512 + if (price == 0) {
513 + log(ns, `ERROR: Failed to sell ${sharesSold} ${stk.sym} ${long ? 'shares' : 'shorts'} @ ${formatMoney(expectedPrice)} - 0 was returned.`, true, 'error');
514 + return 0;
515 + } else if (price != expectedPrice) {
516 + log(ns, `WARNING: Sold ${stk.sym} ${long ? 'shares' : 'shorts'} @ ${formatMoney(price)} but expected ${formatMoney(expectedPrice)} (spread: ${formatMoney(stk.spread)})`, false, 'warning');
517 + price = expectedPrice; // Known Bitburner bug for now, sellSort returns "price" instead of "ask_price". Correct this so running profit calcs are correct.
518 + }
519 + if (long) stk.sharesLong -= sharesSold; else stk.sharesShort -= sharesSold; // Maintained for mock mode, otherwise, redundant (overwritten at next refresh)
520 + totalProfit += profit;
521 + return price * sharesSold - commission; // Return the amount of money recieved from the transaction
522 + }
523 +
524 + let formatBP = fraction => formatNumberShort(fraction * 100 * 100, 3, 2) + " BP";
525 +
526 + /** Log / tprint / toast helper.
527 + * @param {NS} ns */
528 + let log = (ns, message, tprint = false, toastStyle = "") => {
529 + if (message == lastLog) return;
530 + ns.print(message);
531 + if (tprint) ns.tprint(message);
532 + if (toastStyle) ns.toast(message, toastStyle);
533 + return lastLog = message;
534 + }
535 +
536 + function doStatusUpdate(ns, stocks, myStocks, hudElement = null) {
537 + let maxReturnBP = 10000 * Math.max(...myStocks.map(s => s.absReturn())); // The largest return (in basis points) in our portfolio
538 + let minReturnBP = 10000 * Math.min(...myStocks.map(s => s.absReturn())); // The smallest return (in basis points) in our portfolio
539 + let est_holdings_cost = myStocks.reduce((sum, stk) => sum + (stk.owned() ? commission : 0) +
540 + stk.sharesLong * stk.boughtPrice + stk.sharesShort * stk.boughtPriceShort, 0);
541 + let liquidation_value = myStocks.reduce((sum, stk) => sum - (stk.owned() ? commission : 0) + stk.positionValue(), 0);
542 + let status = `Long ${myStocks.filter(s => s.sharesLong > 0).length}, Short ${myStocks.filter(s => s.sharesShort > 0).length} of ${stocks.length} stocks ` +
543 + (myStocks.length == 0 ? '' : `(ER ${minReturnBP.toFixed(1)}-${maxReturnBP.toFixed(1)} BP) `) +
544 + `Profit: ${formatMoney(totalProfit, 3)} Holdings: ${formatMoney(liquidation_value, 3)} (Cost: ${formatMoney(est_holdings_cost, 3)}) ` +
545 + `Net: ${formatMoney(totalProfit + liquidation_value - est_holdings_cost, 3)}`
546 + log(ns, status);
547 + if (hudElement) hudElement.innerText = formatMoney(liquidation_value, 6, 3);
548 + }
549 +
550 + /** @param {NS} ns **/
551 + async function liquidate(ns) {
552 + allStockSymbols ??= await getStockSymbols(ns);
553 + if (allStockSymbols == null) return; // Nothing to liquidate, no API Access
554 + let totalStocks = 0, totalSharesLong = 0, totalSharesShort = 0, totalRevenue = 0;
555 + const dictPositions = mock ? null : await getStockInfoDict(ns, 'getPosition');
556 + for (const sym of allStockSymbols) {
557 + var [sharesLong, , sharesShort, avgShortCost] = dictPositions[sym];
558 + if (sharesLong + sharesShort == 0) continue;
559 + totalStocks++, totalSharesLong += sharesLong, totalSharesShort += sharesShort;
560 + if (sharesLong > 0) totalRevenue += (await sellStockWrapper(ns, sym, sharesLong)) * sharesLong - commission;
561 + if (sharesShort > 0) totalRevenue += (2 * avgShortCost - (await sellShortWrapper(ns, sym, sharesShort))) * sharesShort - commission;
562 + }
563 + log(ns, `Sold ${totalSharesLong.toLocaleString('en')} long shares and ${totalSharesShort.toLocaleString('en')} short shares ` +
564 + `in ${totalStocks} stocks for ${formatMoney(totalRevenue, 3)}`, true, 'success');
565 + }
566 +
567 + /** @param {NS} ns **/
568 + /** @param {Player} playerStats **/
569 + async function tryGet4SApi(ns, playerStats, bitnodeMults, budget) {
570 + if (await checkAccess(ns, 'has4SDataTIXAPI')) return false; // Only return true if we just bought it
571 + const cost4sData = 1E9 * bitnodeMults.FourSigmaMarketDataCost;
572 + const cost4sApi = 25E9 * bitnodeMults.FourSigmaMarketDataApiCost;
573 + const has4S = await checkAccess(ns, 'has4SData');
574 + const totalCost = (has4S ? 0 : cost4sData) + cost4sApi;
575 + // Liquidate shares if it would allow us to afford 4S API data
576 + if (totalCost > budget) /* Need to reserve some money to invest */
577 + return false;
578 + if (playerStats.money < totalCost)
579 + await liquidate(ns);
580 + if (!has4S) {
581 + if (await tryBuy(ns, 'purchase4SMarketData'))
582 + log(ns, `SUCCESS: Purchased 4SMarketData for ${formatMoney(cost4sData)} ` +
583 + `(At ${formatDuration(getTimeInBitnode())} into BitNode)`, true, 'success');
584 + else
585 + log(ns, 'ERROR attempting to purchase 4SMarketData!', false, 'error');
586 + }
587 + if (await tryBuy(ns, 'purchase4SMarketDataTixApi')) {
588 + log(ns, `SUCCESS: Purchased 4SMarketDataTixApi for ${formatMoney(cost4sApi)} ` +
589 + `(At ${formatDuration(getTimeInBitnode())} into BitNode)`, true, 'success');
590 + return true;
591 + } else {
592 + log(ns, 'ERROR attempting to purchase 4SMarketDataTixApi!', false, 'error');
593 + if (!(5 in dictSourceFiles)) { // If we do not have access to bitnode multipliers, assume the cost is double and try again later
594 + log(ns, 'INFO: Bitnode mults are not available (SF5) - assuming everything is twice as expensive in the current bitnode.');
595 + bitnodeMults.FourSigmaMarketDataCost *= 2;
596 + bitnodeMults.FourSigmaMarketDataApiCost *= 2;
597 + }
598 + }
599 + return false;
600 + }
601 +
602 + /** @param {NS} ns
603 + * @param {"hasWSEAccount"|"hasTIXAPIAccess"|"has4SData"|"has4SDataTIXAPI"} stockFn
604 + * Helper to check for one of the stock access functions */
605 + async function checkAccess(ns, stockFn) {
606 + return await getNsDataThroughFile(ns, `ns.stock.${stockFn}()`)
607 + }
608 +
609 + /** @param {NS} ns
610 + * @param {"purchaseWseAccount"|"purchaseTixApi"|"purchase4SMarketData"|"purchase4SMarketDataTixApi"} stockFn
611 + * Helper to try and buy a stock access. Yes, the code is the same as above, but I wanted to be explicit. */
612 + async function tryBuy(ns, stockFn) {
613 + return await getNsDataThroughFile(ns, `ns.stock.${stockFn}()`)
614 + }
615 +
616 + /** @param {NS} ns
617 + * @param {number} budget - The amount we are willing to spend on WSE and API access
618 + * Tries to purchase access to the stock market **/
619 + async function tryGetStockMarketAccess(ns, budget) {
620 + if (await checkAccess(ns, 'hasTIXAPIAccess')) return true; // Already have access
621 + const costWseAccount = 200E6;
622 + const costTixApi = 5E9;
623 + const hasWSE = await checkAccess(ns, 'hasWSEAccount');
624 + const totalCost = (hasWSE ? 0 : costWseAccount) + costTixApi;
625 + if (totalCost > budget) return false;
626 + if (!hasWSE) {
627 + if (await tryBuy(ns, 'purchaseWseAccount'))
628 + log(ns, `SUCCESS: Purchased a WSE (stockmarket) account for ${formatMoney(costWseAccount)} ` +
629 + `(At ${formatDuration(getTimeInBitnode())} into BitNode)`, true, 'success');
630 + else
631 + log(ns, 'ERROR attempting to purchase WSE account!', false, 'error');
632 + }
633 + if (await tryBuy(ns, 'purchaseTixApi')) {
634 + log(ns, `SUCCESS: Purchased Tix (stockmarket) Api access for ${formatMoney(costTixApi)} ` +
635 + `(At ${formatDuration(getTimeInBitnode())} into BitNode)`, true, 'success');
636 + return true;
637 + } else
638 + log(ns, 'ERROR attempting to purchase Tix Api!', false, 'error');
639 + return false;
640 + }
641 +
642 + function initializeHud() {
643 + const d = eval("document");
644 + let htmlDisplay = d.getElementById("stock-display-1");
645 + if (htmlDisplay !== null) return htmlDisplay;
646 + // Get the custom display elements in HUD.
647 + let customElements = d.getElementById("overview-extra-hook-0").parentElement.parentElement;
648 + // Make a clone of the hook for extra hud elements, and move it up under money
649 + let stockValueTracker = customElements.cloneNode(true);
650 + // Remove any nested elements created by stats.js
651 + stockValueTracker.querySelectorAll("p > p").forEach(el => el.parentElement.removeChild(el));
652 + // Change ids since duplicate id's are invalid
653 + stockValueTracker.querySelectorAll("p").forEach((el, i) => el.id = "stock-display-" + i);
654 + // Get out output element
655 + htmlDisplay = stockValueTracker.querySelector("#stock-display-1");
656 + // Display label and default value
657 + stockValueTracker.querySelectorAll("p")[0].innerText = "Stock";
658 + htmlDisplay.innerText = "$0.000 "
659 + // Insert our element right after Money
660 + customElements.parentElement.insertBefore(stockValueTracker, customElements.parentElement.childNodes[2]);
661 + return htmlDisplay;
662 + }

targets.js

@@ -1,30 +1,6 @@
1 1 /** @param {NS} ns */
2 + // is automatically populated by scan.js
2 3 export var targets = [
3 4 "foodnstuff",
4 - // "iron-gym",
5 - // "joesguns",
6 - // "sigma-cosmetics",
7 - // "hong-fang-tea",
8 - // "nectar-net",
9 - // "silver-helix",
10 - // "computek",
11 - // "neo-net",
12 - // "the-hub",
13 - // "netlink",
14 - // "summit-uni",
15 - // "rothman-uni",
16 - // "rho-construction",
17 - // "johnson-ortho",
18 - // "catalyst",
19 - // "aevum-police",
20 - // "millenium-fitness",
21 5 "n00dles",
22 - // "phantasy",
23 - // "harakiri-sushi",
24 - // "zer0",
25 - // "max-hardware",
26 - // "omega-net",
27 - // "crush-fitness",
28 - // ".",
29 - // "alpha-ent",
30 6 ];

Alyssa Smith revised this gist 1697515517. Go to revision

10 files changed, 288 insertions

_firstboot.js(file created)

@@ -0,0 +1,31 @@
1 + /** @param {NS} ns */
2 + // script to be run after each augment to recover hack level and money
3 + // 1. run scan.js, backdoor n00dles and foodnstuff
4 + // 2. run this script, starts hacking those servers
5 + // 3. once you have 200k, go to aevum, then run alain/casino.js
6 + // 3a. go to tech store, buy darkweb, connect darkweb, buy -a
7 + // 4. run scan.js, update targets.js with any extra servers you can backdoor, make sure you do backdoor them with the links in scan's output
8 + // 5. run purchase.js, update with higher ram allowance (32>2048) if you have enough money
9 + // * run delete-pserv.js if there's an issue with buying the servers
10 + // 6. run init.js, this will boot up stockmaster and stats, as well as starting the hack scripts running across all pservs and home
11 + export async function main(ns) {
12 + const scripts = [
13 + {name: "/alain/stats.js", args:[]},
14 + // {name: "/alain/stockmaster.js", args:[]},
15 + // {name: "/pserv.js", args:[]},
16 + {name: "/local.js", args:[]},
17 + ];
18 + for(const {name,args,waitFor=false} of scripts) {
19 + const pid = ns.run(name,1,...args);
20 + if(pid) {
21 + ns.tprint(`Started ${name} with [${args}]`);
22 + if(waitFor) {
23 + ns.tprint("Waiting for it to exit...");
24 + await awaitByPid(ns,pid);
25 + }
26 + }
27 + else
28 + ns.tprint(`ERROR: Failed to start ${name} with ${args}!`);
29 + await ns.sleep(100);
30 + }
31 + }

_init.js(file created)

@@ -0,0 +1,22 @@
1 + /** @param {NS} ns */
2 + export async function main(ns) {
3 + const scripts = [
4 + {name: "/alain/stats.js", args:[]},
5 + {name: "/alain/stockmaster.js", args:[]},
6 + {name: "/pserv.js", args:[]},
7 + {name: "/local.js", args:[]},
8 + ];
9 + for(const {name,args,waitFor=false} of scripts) {
10 + const pid = ns.run(name,1,...args);
11 + if(pid) {
12 + ns.tprint(`Started ${name} with [${args}]`);
13 + if(waitFor) {
14 + ns.tprint("Waiting for it to exit...");
15 + await awaitByPid(ns,pid);
16 + }
17 + }
18 + else
19 + ns.tprint(`ERROR: Failed to start ${name} with ${args}!`);
20 + await ns.sleep(100);
21 + }
22 + }

_shutdown.js(file created)

@@ -0,0 +1,20 @@
1 + /** @param {NS} ns */
2 + export async function main(ns) {
3 + const scripts = [
4 + {name: "/alain/stockmaster.js", args:["-l"]},
5 + {name: "/alain/kill-all-scripts.js", args:[]},
6 + ];
7 + for(const {name,args,waitFor=false} of scripts) {
8 + const pid = ns.run(name,1,...args);
9 + if(pid) {
10 + ns.tprint(`Started ${name} with [${args}]`);
11 + if(waitFor) {
12 + ns.tprint("Waiting for it to exit...");
13 + await awaitByPid(ns,pid);
14 + }
15 + }
16 + else
17 + ns.tprint(`ERROR: Failed to start ${name} with ${args}!`);
18 + await ns.sleep(100);
19 + }
20 + }

delete-pserv.js(file created)

@@ -0,0 +1,11 @@
1 + export async function main(ns) {
2 + // Iterator we'll use for our loop
3 + let i = 0;
4 +
5 + while (i < ns.getPurchasedServerLimit()) {
6 + let hostname = "pserv-" + i;
7 + ns.deleteServer(hostname)
8 + ++i;
9 + await ns.sleep(100);
10 + }
11 + }

early-hack-template.js(file created)

@@ -0,0 +1,18 @@
1 + import { targets } from "/targets.js";
2 +
3 + export async function main(ns) {
4 + const args = ns.flags([['help', false]]);
5 + let hostname = args._[0];
6 + if(!hostname) {
7 + hostname = targets[Math.floor(Math.random()*targets.length)];
8 + }
9 + while (true) {
10 + if (ns.getServerSecurityLevel(hostname) > (ns.getServerMinSecurityLevel(hostname)+1)) {
11 + await ns.weaken(hostname);
12 + } else if (ns.getServerMoneyAvailable(hostname) < (ns.getServerMaxMoney(hostname)*0.75)) {
13 + await ns.grow(hostname);
14 + } else {
15 + await ns.hack(hostname);
16 + }
17 + }
18 + }

local.js(file created)

@@ -0,0 +1,14 @@
1 + export async function main(ns) {
2 + var i = 0;
3 + var ram = ns.getScriptRam("early-hack-template.js")*100,
4 + total_ram = ns.getServerMaxRam("home")-1024,
5 + used_ram = ns.getServerUsedRam("home")
6 +
7 + ns.tail();
8 +
9 + while (i < ((total_ram-used_ram)/ram)) {
10 + ns.run("early-hack-template.js", 100);
11 + ++i;
12 + await ns.sleep(100);
13 + }
14 + }

pserv.js(file created)

@@ -0,0 +1,19 @@
1 + import { targets } from "/targets.js";
2 +
3 + export async function main(ns) {
4 + // Iterator we'll use for our loop
5 + let i = 0;
6 + ns.tail();
7 +
8 + while (i < ns.getPurchasedServerLimit()) {
9 + let hostname = "pserv-" + i;
10 + // ns.deleteServer(hostname)
11 + // ns.purchaseServer(hostname, 1024)
12 + ns.scp("targets.js", hostname);
13 + ns.scp("early-hack-template.js", hostname);
14 + for (var z = 0; z < Math.floor(ns.getServerMaxRam(hostname)/2.4/10); z++)
15 + ns.exec("early-hack-template.js", hostname, 10, targets[z % targets.length]);
16 + ++i;
17 + await ns.sleep(100);
18 + }
19 + }

purchase.js(file created)

@@ -0,0 +1,12 @@
1 + export async function main(ns) {
2 + // Iterator we'll use for our loop
3 + let i = 0;
4 + ns.tail();
5 +
6 + while (i < ns.getPurchasedServerLimit()) {
7 + let hostname = "pserv-" + i;
8 + ns.purchaseServer(hostname, 32) // update this by powers of 2 as you get more money. i usually have it at 2048
9 + ++i;
10 + await ns.sleep(100);
11 + }
12 + }

scan.js(file created)

@@ -0,0 +1,111 @@
1 + /**
2 + * @param {NS} ns
3 + * @returns interactive server map
4 + */
5 + export function main(ns) {
6 + const factionServers = ["CSEC", "avmnite-02h", "I.I.I.I", "run4theh111z", "w0r1d_d43m0n", "fulcrumassets"],
7 + css = ` <style id="scanCSS">
8 + .serverscan {white-space:pre; color:#ccc; font:14px monospace; line-height: 16px; }
9 + .serverscan .server {color:#080;cursor:pointer;text-decoration:underline}
10 + .serverscan .faction {color:#088}
11 + .serverscan .rooted {color:#6f3}
12 + .serverscan .rooted.faction {color:#0ff}
13 + .serverscan .rooted::before {color:#6f3}
14 + .serverscan .hack {display:inline-block; font:12px monospace}
15 + .serverscan .red {color:red;}
16 + .serverscan .green {color:green;}
17 + .serverscan .backdoor {color:#6f3}
18 + .serverscan .linky {font:12px monospace}
19 + .serverscan .linky > a {cursor:pointer; text-decoration:underline;}
20 + .serverscan .cct {color:#0ff;}
21 + </style>`,
22 + doc = eval("document"),
23 + terminalInsert = html => doc.getElementById("terminal").insertAdjacentHTML('beforeend', `<li>${html}</li>`),
24 + terminalInput = doc.getElementById("terminal-input"),
25 + terminalEventHandlerKey = Object.keys(terminalInput)[1],
26 + setNavCommand = async inputValue => {
27 + terminalInput.value = inputValue
28 + terminalInput[terminalEventHandlerKey].onChange({ target: terminalInput })
29 + terminalInput.focus()
30 + await terminalInput[terminalEventHandlerKey].onKeyDown({ key: 'Enter', preventDefault: () => 0 })
31 + },
32 + myHackLevel = ns.getHackingLevel(),
33 + serverInfo = (serverName) => {
34 + // Costs 2 GB. If you can't don't need backdoor links, uncomment and use the alternate implementations below
35 + return ns.getServer(serverName)
36 + /* return {
37 + requiredHackingSkill: ns.getServerRequiredHackingLevel(serverName),
38 + hasAdminRights: ns.hasRootAccess(serverName),
39 + purchasedByPlayer: serverName.includes('daemon') || serverName.includes('hacknet'),
40 + backdoorInstalled: true // No way of knowing without ns.getServer
41 + } */
42 + },
43 + createServerEntry = serverName => {
44 + let server = serverInfo(serverName),
45 + requiredHackLevel = server.requiredHackingSkill,
46 + rooted = server.hasAdminRights,
47 + canHack = requiredHackLevel <= myHackLevel,
48 + shouldBackdoor = !server?.backdoorInstalled && canHack && serverName != 'home' && rooted && !server.purchasedByPlayer,
49 + contracts = ns.ls(serverName, ".cct")
50 +
51 + return `<span id="${serverName}">`
52 + + `<a class="server${factionServers.includes(serverName) ? " faction" : ""}`
53 + + `${rooted ? " rooted" : ""}">${serverName}</a>`
54 + + (server.purchasedByPlayer ? '' : ` <span class="hack ${(canHack ? 'green' : 'red')} monitor linky">(<a>${requiredHackLevel}</a>)</span>`)
55 + + `${((canHack && !rooted) || shouldBackdoor ? ' <span class="backdoor linky">[<a>backdoor</a>]</span>' : '')}`
56 + + ` ${contracts.map(c => `<span class="cct" title="${c}">@</span>`)}`
57 + + "</span>"
58 + },
59 + buildOutput = (parent = servers[0], prefix = ["\n"]) => {
60 + let output = prefix.join("") + createServerEntry(parent)
61 + for (let i = 0; i < servers.length; i++) {
62 + if (parentByIndex[i] != parent) continue
63 + let newPrefix = prefix.slice()
64 + const appearsAgain = parentByIndex.slice(i + 1).includes(parentByIndex[i]),
65 + lastElementIndex = newPrefix.length - 1
66 +
67 + newPrefix.push(appearsAgain ? "├╴" : "└╴")
68 +
69 + newPrefix[lastElementIndex] = newPrefix[lastElementIndex].replace("├╴", "│ ").replace("└╴", " ")
70 + output += buildOutput(servers[i], newPrefix)
71 + }
72 +
73 + return output
74 + },
75 + ordering = (serverA, serverB) => {
76 + // Sort servers with fewer connections towards the top.
77 + let orderNumber = ns.scan(serverA).length - ns.scan(serverB).length
78 + // Purchased servers to the very top
79 + orderNumber = orderNumber != 0 ? orderNumber
80 + : serverInfo(serverB).purchasedByPlayer - serverInfo(serverA).purchasedByPlayer
81 + // Hack: compare just the first 2 chars to keep purchased servers in order purchased
82 + orderNumber = orderNumber != 0 ? orderNumber
83 + : serverA.slice(0, 2).toLowerCase().localeCompare(serverB.slice(0, 2).toLowerCase())
84 +
85 + return orderNumber
86 + }
87 +
88 + // refresh css (in case it changed)
89 + doc.getElementById("scanCSS")?.remove()
90 + doc.head.insertAdjacentHTML('beforeend', css)
91 + let servers = ["home"],
92 + parentByIndex = [""],
93 + routes = { home: "home" }
94 + for (let server of servers)
95 + for (let oneScanResult of ns.scan(server).sort(ordering))
96 + if (!servers.includes(oneScanResult)) {
97 + const backdoored = serverInfo(oneScanResult)?.backdoorInstalled
98 + servers.push(oneScanResult)
99 + parentByIndex.push(server)
100 + routes[oneScanResult] = backdoored ? "connect " + oneScanResult : routes[server] + ";connect " + oneScanResult
101 + }
102 +
103 + terminalInsert(`<div class="serverscan new">${buildOutput()}</div>`)
104 + doc.querySelectorAll(".serverscan.new .server").forEach(serverEntry => serverEntry
105 + .addEventListener('click', setNavCommand.bind(null, routes[serverEntry.childNodes[0].nodeValue])))
106 + doc.querySelectorAll(".serverscan.new .monitor").forEach(monitorButton => monitorButton
107 + .addEventListener('click', setNavCommand.bind(null, "run monitor.js " + monitorButton.parentNode.childNodes[0].childNodes[0].nodeValue)))
108 + doc.querySelectorAll(".serverscan.new .backdoor").forEach(backdoorButton => backdoorButton
109 + .addEventListener('click', setNavCommand.bind(null, routes[backdoorButton.parentNode.childNodes[0].childNodes[0].nodeValue] + ";run brutessh.exe;run httpworm.exe;run sqlinject.exe;run ftpcrack.exe;run relaysmtp.exe;run nuke.exe;backdoor")))
110 + doc.querySelector(".serverscan.new").classList.remove("new")
111 + }

targets.js(file created)

@@ -0,0 +1,30 @@
1 + /** @param {NS} ns */
2 + export var targets = [
3 + "foodnstuff",
4 + // "iron-gym",
5 + // "joesguns",
6 + // "sigma-cosmetics",
7 + // "hong-fang-tea",
8 + // "nectar-net",
9 + // "silver-helix",
10 + // "computek",
11 + // "neo-net",
12 + // "the-hub",
13 + // "netlink",
14 + // "summit-uni",
15 + // "rothman-uni",
16 + // "rho-construction",
17 + // "johnson-ortho",
18 + // "catalyst",
19 + // "aevum-police",
20 + // "millenium-fitness",
21 + "n00dles",
22 + // "phantasy",
23 + // "harakiri-sushi",
24 + // "zer0",
25 + // "max-hardware",
26 + // "omega-net",
27 + // "crush-fitness",
28 + // ".",
29 + // "alpha-ent",
30 + ];
Newer Older