BLADEBURNER: Add API to calculate max upgrade count of skill (#1475)

This commit is contained in:
catloversg 2024-08-17 03:15:20 +07:00 committed by GitHub
parent 3d15413619
commit 289f60d8c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 274 additions and 19 deletions

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [BladeburnerFormulas](./bitburner.bladeburnerformulas.md)
## BladeburnerFormulas interface
Bladeburner formulas
**Signature:**
```typescript
interface BladeburnerFormulas
```
## Methods
| Method | Description |
| --- | --- |
| [skillMaxUpgradeCount(name, level, skillPoints)](./bitburner.bladeburnerformulas.skillmaxupgradecount.md) | Calculate the number of times that you can upgrade a skill. |

@ -0,0 +1,28 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [BladeburnerFormulas](./bitburner.bladeburnerformulas.md) &gt; [skillMaxUpgradeCount](./bitburner.bladeburnerformulas.skillmaxupgradecount.md)
## BladeburnerFormulas.skillMaxUpgradeCount() method
Calculate the number of times that you can upgrade a skill.
**Signature:**
```typescript
skillMaxUpgradeCount(name: string, level: number, skillPoints: number): number;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| name | string | Skill name. It's case-sensitive and must be an exact match. |
| level | number | Skill level. It must be a non-negative number. |
| skillPoints | number | Number of skill points to upgrade the skill. It must be a positive number. |
**Returns:**
number
Number of times that you can upgrade the skill.

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [Formulas](./bitburner.formulas.md) &gt; [bladeburner](./bitburner.formulas.bladeburner.md)
## Formulas.bladeburner property
Bladeburner formulas
**Signature:**
```typescript
bladeburner: BladeburnerFormulas;
```

@ -20,6 +20,7 @@ You need Formulas.exe on your home computer to use this API.
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [bladeburner](./bitburner.formulas.bladeburner.md) | | [BladeburnerFormulas](./bitburner.bladeburnerformulas.md) | Bladeburner formulas |
| [gang](./bitburner.formulas.gang.md) | | [GangFormulas](./bitburner.gangformulas.md) | Gang formulas |
| [hacking](./bitburner.formulas.hacking.md) | | [HackingFormulas](./bitburner.hackingformulas.md) | Hacking formulas |
| [hacknetNodes](./bitburner.formulas.hacknetnodes.md) | | [HacknetNodesFormulas](./bitburner.hacknetnodesformulas.md) | Hacknet Nodes formulas |

@ -36,6 +36,7 @@
| [BitNodeRequirement](./bitburner.bitnoderequirement.md) | Player must be located in this BitNode. |
| [Bladeburner](./bitburner.bladeburner.md) | Bladeburner API |
| [BladeburnerCurAction](./bitburner.bladeburnercuraction.md) | Bladeburner current action. |
| [BladeburnerFormulas](./bitburner.bladeburnerformulas.md) | Bladeburner formulas |
| [BladeburnerRankRequirement](./bitburner.bladeburnerrankrequirement.md) | Player must have at least this rank in the Bladeburner Division. |
| [CityRequirement](./bitburner.cityrequirement.md) | Player must be located in this city. |
| [CodingContract](./bitburner.codingcontract.md) | Coding Contract API |

@ -3,7 +3,7 @@ import type { BladeMultName, BladeSkillName } from "@enums";
import { currentNodeMults } from "../BitNode/BitNodeMultipliers";
import { Bladeburner } from "./Bladeburner";
import { Availability } from "./Types";
import { PositiveInteger, isPositiveInteger } from "../types";
import { PositiveInteger, PositiveNumber, isPositiveInteger } from "../types";
import { PartialRecord, getRecordEntries } from "../Types/Record";
interface SkillParams {
@ -40,9 +40,6 @@ export class Skill {
* The cost of the next level: (baseCost + currentLevel * costInc) * mult. The cost needs to be an integer, so we
* need to use Math.floor or Math.round.
*
* Note: there is no notation for Math.round, so I use \lceil and \rceil as alternatives for non-existent \lround
* and \rround. When you see \lceil and \rceil, it means Math.round, not Math.ceil.
*
* In order to calculate the cost of "count" levels, we need to run a loop. "count" can be a big number, so it's
* infeasible to calculate the cost in that way. We need to find the closed forms of:
*
@ -52,7 +49,7 @@ export class Skill {
* Or:
*
* [2]:
* $$Cost = \sum_{i = CurrentLevel}^{CurrentLevel+Count-1}\lceil ((BaseCost + i \ast CostInc) \ast Mult) \rceil$$
* $$Cost = \sum_{i = CurrentLevel}^{CurrentLevel+Count-1} \mathrm{Round}((BaseCost + i \ast CostInc) \ast Mult)$$
*
* It's really hard to find the closed forms of those two equations, so we switch to these equations:
*
@ -62,7 +59,7 @@ export class Skill {
* Or
*
* [4]:
* $$Cost = \lceil\sum_{i = CurrentLevel}^{CurrentLevel+Count-1} ((BaseCost + i \ast CostInc) \ast Mult) \rceil$$
* $$Cost = \mathrm{Round}(\sum_{i = CurrentLevel}^{CurrentLevel+Count-1} ((BaseCost + i \ast CostInc) \ast Mult))$$
*
* This means that we do the flooring/rounding at the end instead of each iterative step.
*
@ -73,7 +70,7 @@ export class Skill {
*
* The closed form of [4]:
*
* $$Cost = \lceil Count \ast Mult \ast (BaseCost + (CostInc \ast (CurrentLevel + \frac{Count - 1}{2}))) \rceil$$
* $$Cost = \mathrm{Round}(Count \ast Mult \ast (BaseCost + (CostInc \ast (CurrentLevel + \frac{Count - 1}{2}))))$$
*
*/
return Math.round(
@ -83,6 +80,61 @@ export class Skill {
);
}
calculateMaxUpgradeCount(currentLevel: number, cost: PositiveNumber): number {
/**
* Define:
* - x = count
* - a = currentNodeMults.BladeburnerSkillCost
* - b = this.baseCost
* - c = this.costInc
* - d = currentLevel
* - y = cost
*
* We have:
*
* $$ y = \mathrm{Round}(x \ast a \ast (b + c \ast (d + \frac{x - 1}{2})))$$
*
* To simplify the calculation, let's ignore the Math.round part:
*
* $$ y = x \ast a \ast (b + c \ast (d + \frac{x - 1}{2}))$$
*
* Solve for x in terms of y:
*
* Define:
*
* $$ m = -b - c \ast d + \frac{c}{2} $$
*
* $$ Delta = \sqrt{{m ^ 2} + \frac{2 \ast c \ast y}{a}} $$
*
* Solutions:
*
* $$ x_1 = \frac{m + Delta}{c} $$
*
* $$ x_2 = \frac{m - Delta}{c} $$
*
* $a$, $c$ and $y$ are always greater than 0, so $x_2$ is always less than 0. Therefore, $x_1$ is the only
* solution.
*/
const m = -this.baseCost - this.costInc * currentLevel + this.costInc / 2;
const delta = Math.sqrt(m * m + (2 * this.costInc * cost) / currentNodeMults.BladeburnerSkillCost);
const result = Math.round((m + delta) / this.costInc);
/**
* Due to floating-point rounding and edge-cases, we cannot ensure that rounding x_1 will give us the correct
* integer. In other words, we cannot be sure that x_1 is within 0.5 of the integer value we want. However, we can
* be sure that it is within 1 of the value we want, which means that checking the numbers above and below the
* rounded value are sufficient to find our correct integer.
*/
const costOfResultPlus1 = this.calculateCost(currentLevel, (result + 1) as PositiveInteger);
if (costOfResultPlus1 <= cost) {
return result + 1;
}
const costOfResult = this.calculateCost(currentLevel, result as PositiveInteger);
if (costOfResult <= cost) {
return result;
}
return result - 1;
}
canUpgrade(bladeburner: Bladeburner, count = 1): Availability<{ cost: number }> {
const currentLevel = bladeburner.skills[this.name] ?? 0;
if (!isPositiveInteger(count)) {

@ -3,12 +3,12 @@ import React, { useState } from "react";
import { AugmentationName } from "@enums";
import { Player } from "@player";
import { KEY } from "../../utils/helpers/keyCodes";
import { random } from "../utils";
import { BlinkingCursor } from "./BlinkingCursor";
import { interpolate } from "./Difficulty";
import { GameTimer } from "./GameTimer";
import { IMinigameProps } from "./IMinigameProps";
import { KeyHandler } from "./KeyHandler";
import { getRandomArbitrary } from "../../utils/helpers/getRandomArbitrary";
interface Difficulty {
[key: string]: number;
@ -67,7 +67,7 @@ export function BackwardGame(props: IMinigameProps): React.ReactElement {
}
function makeAnswer(difficulty: Difficulty): string {
const length = random(difficulty.min, difficulty.max);
const length = getRandomArbitrary(difficulty.min, difficulty.max);
let answer = "";
for (let i = 0; i < length; i++) {
if (i > 0) answer += " ";

@ -3,12 +3,12 @@ import React, { useState } from "react";
import { AugmentationName } from "@enums";
import { Player } from "@player";
import { KEY } from "../../utils/helpers/keyCodes";
import { random } from "../utils";
import { BlinkingCursor } from "./BlinkingCursor";
import { interpolate } from "./Difficulty";
import { GameTimer } from "./GameTimer";
import { IMinigameProps } from "./IMinigameProps";
import { KeyHandler } from "./KeyHandler";
import { getRandomArbitrary } from "../../utils/helpers/getRandomArbitrary";
interface Difficulty {
[key: string]: number;
@ -35,7 +35,7 @@ function generateLeftSide(difficulty: Difficulty): string {
if (Player.hasAugmentation(AugmentationName.WisdomOfAthena, true)) {
options.splice(0, 1);
}
const length = random(difficulty.min, difficulty.max);
const length = getRandomArbitrary(difficulty.min, difficulty.max);
for (let i = 0; i < length; i++) {
str += options[Math.floor(Math.random() * options.length)];
}

@ -2,11 +2,12 @@ import { Paper, Typography } from "@mui/material";
import React, { useState } from "react";
import { AugmentationName } from "@enums";
import { Player } from "@player";
import { Arrow, downArrowSymbol, getArrow, leftArrowSymbol, random, rightArrowSymbol, upArrowSymbol } from "../utils";
import { Arrow, downArrowSymbol, getArrow, leftArrowSymbol, rightArrowSymbol, upArrowSymbol } from "../utils";
import { interpolate } from "./Difficulty";
import { GameTimer } from "./GameTimer";
import { IMinigameProps } from "./IMinigameProps";
import { KeyHandler } from "./KeyHandler";
import { getRandomArbitrary } from "../../utils/helpers/getRandomArbitrary";
interface Difficulty {
[key: string]: number;
@ -77,7 +78,7 @@ export function CheatCodeGame(props: IMinigameProps): React.ReactElement {
function generateCode(difficulty: Difficulty): Arrow[] {
const arrows: Arrow[] = [leftArrowSymbol, rightArrowSymbol, upArrowSymbol, downArrowSymbol];
const code: Arrow[] = [];
for (let i = 0; i < random(difficulty.min, difficulty.max); i++) {
for (let i = 0; i < getRandomArbitrary(difficulty.min, difficulty.max); i++) {
let arrow = arrows[Math.floor(4 * Math.random())];
while (arrow === code[code.length - 1]) arrow = arrows[Math.floor(4 * Math.random())];
code.push(arrow);

@ -4,12 +4,12 @@ import { Box, Paper, Typography } from "@mui/material";
import { AugmentationName } from "@enums";
import { Player } from "@player";
import { Settings } from "../../Settings/Settings";
import { random } from "../utils";
import { interpolate } from "./Difficulty";
import { GameTimer } from "./GameTimer";
import { IMinigameProps } from "./IMinigameProps";
import { KeyHandler } from "./KeyHandler";
import { isPositiveInteger } from "../../types";
import { getRandomArbitrary } from "../../utils/helpers/getRandomArbitrary";
interface Difficulty {
[key: string]: number;
@ -200,7 +200,7 @@ function generateQuestion(wires: Wire[], difficulty: Difficulty): Question[] {
function generateWires(difficulty: Difficulty): Wire[] {
const wires = [];
const numWires = random(difficulty.wiresmin, difficulty.wiresmax);
const numWires = getRandomArbitrary(difficulty.wiresmin, difficulty.wiresmax);
for (let i = 0; i < numWires; i++) {
const wireColors = [colors[Math.floor(Math.random() * colors.length)]];
if (Math.random() < 0.15) {

@ -2,10 +2,6 @@ import { KEY } from "../utils/helpers/keyCodes";
import { Player } from "@player";
import { AugmentationName } from "@enums";
export function random(min: number, max: number): number {
return Math.random() * (max - min) + min;
}
export const upArrowSymbol = "↑";
export const downArrowSymbol = "↓";
export const leftArrowSymbol = "←";

@ -67,6 +67,7 @@ export const helpers = {
number,
positiveInteger,
positiveSafeInteger,
positiveNumber,
scriptArgs,
runOptions,
spawnOptions,

@ -669,6 +669,9 @@ export const RamCosts: RamCostTree<NSFull> = {
factionGains: 0,
companyGains: 0,
},
bladeburner: {
skillMaxUpgradeCount: 0,
},
},
} as const;

@ -51,6 +51,7 @@ import { findEnumMember } from "../utils/helpers/enum";
import { getEnumHelper } from "../utils/EnumHelper";
import { CompanyPositions } from "../Company/CompanyPositions";
import { findCrime } from "../Crime/CrimeHelpers";
import { Skills } from "../Bladeburner/data/Skills";
export function NetscriptFormulas(): InternalAPI<IFormulas> {
const checkFormulasAccess = function (ctx: NetscriptContext): void {
@ -427,6 +428,22 @@ export function NetscriptFormulas(): InternalAPI<IFormulas> {
return calculateCompanyWorkStats(person, company, position, favor);
},
},
bladeburner: {
skillMaxUpgradeCount: (ctx) => (_name, _level, _skillPoints) => {
checkFormulasAccess(ctx);
const name = getEnumHelper("BladeSkillName").nsGetMember(ctx, _name, "name");
const level = helpers.number(ctx, "level", _level);
if (level < 0) {
throw new Error(`Level must be a non-negative number.`);
}
const skillPoints = helpers.positiveNumber(ctx, "skillPoints", _skillPoints);
const skill = Skills[name];
if (level >= skill.maxLvl) {
return 0;
}
return skill.calculateMaxUpgradeCount(level, skillPoints);
},
},
};
// Removed functions

@ -5110,6 +5110,22 @@ interface GangFormulas {
ascensionMultiplier(points: number): number;
}
/**
* Bladeburner formulas
* @public
*/
interface BladeburnerFormulas {
/**
* Calculate the number of times that you can upgrade a skill.
*
* @param name - Skill name. It's case-sensitive and must be an exact match.
* @param level - Skill level. It must be a non-negative number.
* @param skillPoints - Number of skill points to upgrade the skill. It must be a positive number.
* @returns Number of times that you can upgrade the skill.
*/
skillMaxUpgradeCount(name: string, level: number, skillPoints: number): number;
}
/**
* Formulas API
* @remarks
@ -5134,6 +5150,8 @@ export interface Formulas {
gang: GangFormulas;
/** Work formulas */
work: WorkFormulas;
/** Bladeburner formulas */
bladeburner: BladeburnerFormulas;
}
/** @public */

@ -0,0 +1,7 @@
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random#getting_a_random_number_between_two_values
export function getRandomArbitrary(min: number, max: number): number {
if (min > max) {
throw new Error(`Min is greater than max. Min: ${min}. Max: ${max}.`);
}
return Math.random() * (max - min) + min;
}

@ -0,0 +1,97 @@
import { currentNodeMults } from "../../../src/BitNode/BitNodeMultipliers";
import { Skill } from "../../../src/Bladeburner/Skill";
import { BladeSkillName } from "../../../src/Enums";
import { PositiveInteger, isPositiveInteger, isPositiveNumber } from "../../../src/types";
import { getRandomArbitrary } from "../../../src/utils/helpers/getRandomArbitrary";
import { getRandomIntInclusive } from "../../../src/utils/helpers/getRandomIntInclusive";
const skill = new Skill({
name: BladeSkillName.hyperdrive,
desc: "",
baseCost: 1,
costInc: 1,
mults: {},
});
describe("Test calculateMaxUpgradeCount", function () {
test("errorCount", () => {
let testCaseCount = 0;
let errorCount = 0;
const test1Errors = [];
const test2Errors = [];
for (let i = 0; i < 10; ++i) {
skill.baseCost = getRandomIntInclusive(1, 1000);
for (let j = 0; j < 10; ++j) {
skill.costInc = getRandomArbitrary(1, 1000);
for (let k = 0; k < 10; ++k) {
currentNodeMults.BladeburnerSkillCost = getRandomArbitrary(1, 1000);
for (let m = 0; m < 1e4; ++m) {
const currentLevel = getRandomIntInclusive(0, 1e9);
let count = 0;
let cost = 0;
// Test 1
++testCaseCount;
const expectedCount = getRandomIntInclusive(1, 1e9);
if (!isPositiveInteger(expectedCount)) {
throw new Error(`Invalid expectedCount: ${expectedCount}`);
}
cost = skill.calculateCost(currentLevel, expectedCount);
if (!isPositiveNumber(cost)) {
throw new Error(`Invalid cost: ${cost}`);
}
count = skill.calculateMaxUpgradeCount(currentLevel, cost);
if (expectedCount !== count) {
++errorCount;
test1Errors.push({
baseCost: skill.baseCost,
costInc: skill.costInc,
mult: currentNodeMults.BladeburnerSkillCost,
currentLevel,
cost,
count,
expectedCount,
});
}
// Test 2
++testCaseCount;
const budget = getRandomArbitrary(1e9, 1e50);
if (!isPositiveNumber(budget)) {
throw new Error(`Invalid budget: ${budget}`);
}
count = skill.calculateMaxUpgradeCount(currentLevel, budget);
if (!isPositiveInteger(count)) {
throw new Error(`Invalid count: ${count}`);
}
cost = skill.calculateCost(currentLevel, count);
const costOfCountPlus1 = skill.calculateCost(currentLevel, (count + 1) as PositiveInteger);
if (count !== count + 1 && (budget < cost || budget >= costOfCountPlus1)) {
++errorCount;
test2Errors.push({
baseCost: skill.baseCost,
costInc: skill.costInc,
mult: currentNodeMults.BladeburnerSkillCost,
currentLevel,
count,
budget,
cost,
costOfCountPlus1,
});
}
}
}
}
}
if (errorCount !== 0) {
// There may be hundreds of thousands or even millions of failed test cases, so we only show a limited number of them.
console.error(
`testCaseCount: ${testCaseCount}. errorCount: ${errorCount}. test1Errors: ${JSON.stringify(
test1Errors.slice(0, 1000),
)}. test2Errors: ${JSON.stringify(test2Errors.slice(0, 1000))}`,
);
}
expect(errorCount).toBe(0);
});
});