From 8d1cfb2d9741d37bb33bca65e71bef30ebe0be69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C4=8Cahuk?= Date: Tue, 22 Oct 2024 05:33:56 +0200 Subject: [PATCH] BLADEBURNER: Test cover action completion (#1695) --- .eslintignore | 1 + .gitignore | 1 + .prettierignore | 1 + test/jest/Bladeburner/Actions.test.ts | 336 ++++++++++++++++++++++++++ 4 files changed, 339 insertions(+) create mode 100644 test/jest/Bladeburner/Actions.test.ts diff --git a/.eslintignore b/.eslintignore index 50b2e676b..dbf2a1896 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,6 +7,7 @@ node_modules/ dist input assets +coverage doc markdown electron diff --git a/.gitignore b/.gitignore index 0a281181b..e5b8084ea 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .vscode Changelog.txt Netburner.txt +/coverage /doc/build /node_modules /electron/node_modules diff --git a/.prettierignore b/.prettierignore index b4eee41ed..89fa4adf4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,6 +3,7 @@ package.json dist doc/build doc/source +coverage .build .package .app diff --git a/test/jest/Bladeburner/Actions.test.ts b/test/jest/Bladeburner/Actions.test.ts new file mode 100644 index 000000000..42b76abcf --- /dev/null +++ b/test/jest/Bladeburner/Actions.test.ts @@ -0,0 +1,336 @@ +import { Bladeburner } from "../../../src/Bladeburner/Bladeburner"; +import { PlayerObject } from "../../../src/PersonObjects/Player/PlayerObject"; +import { Player, setPlayer } from "@player"; +import { BlackOperation, Contract, GeneralAction, Operation } from "../../../src/Bladeburner/Actions"; +import { + BladeburnerActionType, + BladeburnerContractName, + BladeburnerGeneralActionName, + BladeburnerOperationName, + CityName, + CrimeType, +} from "@enums"; +import { FormatsNeedToChange } from "../../../src/ui/formatNumber"; +import { CrimeWork } from "../../../src/Work/CrimeWork"; +import type { Action, ActionIdentifier } from "../../../src/Bladeburner/Types"; +import type { Skills } from "@nsdefs"; +import { BlackOperations } from "../../../src/Bladeburner/data/BlackOperations"; + +describe("Bladeburner Actions", () => { + const SampleContract = Contract.createId(BladeburnerContractName.Tracking); + const SampleGeneralAction = GeneralAction.createId(BladeburnerGeneralActionName.Diplomacy); + const SampleOperation = Operation.createId(BladeburnerOperationName.Assassination); + const SampleBlackOp = BlackOperations["Operation Centurion"].id; + + const ENOUGH_TIME_TO_FINISH_ACTION = 1e5; + const BASE_STAT_EXP = 1e6; + + let bb: Bladeburner; + + const cities = Object.keys(new Bladeburner().cities); + + const contracts = Object.values(new Bladeburner().contracts); + const operations = Object.values(new Bladeburner().operations); + const nonGeneralActions = [contracts, operations, Object.values(BlackOperations)].flat(); + + describe("Without Simulacrum", () => { + it("Starting an action cancels player's work immediately", () => { + Player.startWork(new CrimeWork({ crimeType: CrimeType.assassination, singularity: false })); + start(SampleGeneralAction); + expect(Player.currentWork).toBeNull(); + }); + }); + + describe("Upon successful completion", () => { + /** Repetitive snapshot declarations in most tests below */ + let pop, before, after; + + describe(BladeburnerGeneralActionName.Training, () => { + const train = GeneralAction.createId(BladeburnerGeneralActionName.Training); + + it("increases max stamina", () => { + before = bb.maxStamina; + complete(train); + expect(bb.maxStamina).toBeGreaterThan(before); + }); + + it.each(<(keyof Skills)[]>["strength", "dexterity", "agility"])("awards %s exp", (stat: keyof Skills) => { + before = Player.exp[stat]; + complete(train); + expect(Player.exp[stat]).toBeGreaterThan(before); + }); + }); + + describe(BladeburnerGeneralActionName.HyperbolicRegen, () => { + const regen = GeneralAction.createId(BladeburnerGeneralActionName.HyperbolicRegen); + + it("heals the player", () => { + Player.takeDamage(Player.hp.max / 2); + before = Player.hp.current; + complete(regen); + expect(Player.hp.current).toBeGreaterThan(before); + }); + + it("regains stamina", () => { + bb.stamina = 0; + complete(regen); + expect(bb.stamina).toBeGreaterThan(0); + }); + }); + + describe(BladeburnerGeneralActionName.Diplomacy, () => { + const diplomacy = GeneralAction.createId(BladeburnerGeneralActionName.Diplomacy); + + it("mildly reduces chaos in the current city", () => { + allCitiesHighChaos(); + let { chaos } = bb.getCurrentCity(); + complete(diplomacy); + expect(bb.getCurrentCity().chaos).toBeGreaterThan(chaos * 0.9); + expect(bb.getCurrentCity().chaos).toBeLessThan(chaos); + }); + + it("effect scales significantly with player charisma", () => { + Player.gainCharismaExp(1e500); + allCitiesHighChaos(); + complete(diplomacy); + expect(bb.getCurrentCity().chaos).toBe(0); + }); + + it("does NOT affect chaos in other cities", () => { + const otherCity = cities.find((c) => c !== bb.getCurrentCity().name); + /** Testing against a guaranteed 0-chaos level of charisma */ + Player.gainCharismaExp(1e500); + allCitiesHighChaos(); + complete(diplomacy); + expect(bb.cities[otherCity].chaos).toBeGreaterThan(0); + }); + }); + + describe(BladeburnerGeneralActionName.FieldAnalysis, () => { + const fa = GeneralAction.createId(BladeburnerGeneralActionName.FieldAnalysis); + + it("improves population estimate", () => { + ({ pop, popEst: before } = bb.getCurrentCity()); + complete(fa); + ({ popEst: after } = bb.getCurrentCity()); + expect(Math.abs(after - pop)).toBeLessThan(Math.abs(before - pop)); + }); + + it.each(<(keyof Skills)[]>["hacking", "charisma"])("awards %s exp", (stat: keyof Skills) => { + before = Player.exp[stat]; + complete(fa, forceSuccess); + expect(Player.exp[stat]).toBeGreaterThan(before); + }); + + it("provides a minor increase in rank", () => { + before = bb.rank; + complete(fa, forceSuccess); + expect(bb.rank).toBeGreaterThan(before); + }); + }); + + describe.each([SampleContract, SampleOperation, BlackOperations["Operation Archangel"].id])( + "non-general actions increase rank", + (id) => { + it(`${id.type}`, () => { + before = bb.rank; + complete(id, forceSuccess); + expect(bb.rank).toBeGreaterThan(before); + }); + }, + ); + + describe("non-general actions increase rank", () => { + let beforeMinor, minorGain, beforeMajor, majorGain; + + it.each([ + { major: SampleBlackOp, minor: SampleOperation }, + { major: SampleOperation, minor: SampleContract }, + ])("$major.type reward significantly more rank than $minor.type", ({ major, minor }) => { + beforeMinor = bb.rank; + complete(minor, forceSuccess); + minorGain = bb.rank - beforeMinor; + beforeMajor = bb.rank; + complete(major, forceSuccess); + majorGain = bb.rank - beforeMajor; + expect(majorGain).toBeGreaterThan(minorGain); + }); + }); + + describe(BladeburnerGeneralActionName.InciteViolence, () => { + const iv = GeneralAction.createId(BladeburnerGeneralActionName.InciteViolence); + let chaos; + + it("generates available contracts", () => { + const { count } = bb.getActionObject(SampleContract); + complete(iv, forceSuccess); + expect(bb.getActionObject(SampleContract).count).toBeGreaterThan(count); + }); + + it("generates available operations", () => { + const { count } = bb.getActionObject(SampleOperation); + complete(iv, forceSuccess); + expect(bb.getActionObject(SampleOperation).count).toBeGreaterThan(count); + }); + + /** Relates to all issues mentioned in PR-1586: + * - changing chaos rate of incite violence + * - having chaos rate affect only one city + */ + it.each(cities)("SIGNIFICANTLY increases chaos in all cities when chaos is LOW: %s", (city: CityName) => { + ({ chaos } = bb.cities[city]); + complete(iv, forceSuccess); + expect(bb.cities[city].chaos).toBeGreaterThan(chaos * 2); + }); + + /** Relates to all issues mentioned in PR-1586: + * - changing chaos rate of incite violence + * - having chaos rate affect only one city + */ + it.each(cities)("MILDLY increases chaos in all cities when chaos is HIGH: %s", (city: CityName) => { + allCitiesHighChaos(); + ({ chaos } = bb.cities[city]); + complete(iv, forceSuccess); + expect(bb.cities[city].chaos).toBeGreaterThan(chaos * 1.05); + }); + }); + + describe(BladeburnerGeneralActionName.Recruitment, () => { + const recruit = GeneralAction.createId(BladeburnerGeneralActionName.Recruitment); + + it("awards charisma exp", () => { + before = Player.exp.charisma; + complete(recruit, forceSuccess); + expect(Player.exp.charisma).toBeGreaterThan(before); + }); + + it("hires team member", () => { + complete(recruit, forceSuccess); + expect(bb.teamSize).toBeGreaterThan(0); + }); + }); + + describe.each(contracts.map(({ id }) => ({ id })))("$id.name", ({ id }) => { + it("all contracts award money", () => { + before = Player.money; + complete(id, forceSuccess); + expect(Player.money).toBeGreaterThan(before); + }); + }); + + /** Stat EXP check for all actions */ + /** Checking all of them to avoid regressions */ + describe.each(nonGeneralActions.flatMap(actionIdWithIndividualStat))("$id.name", ({ id, stat }) => { + it(`awards ${stat} exp`, () => { + before = Player.exp[stat]; + complete(id, forceSuccess); + expect(Player.exp[stat]).toBeGreaterThan(before); + }); + }); + }); + + describe("Upon failed completion", () => { + let before; + + describe.each([SampleOperation, SampleBlackOp])("operations and black operations decrease rank", (id) => { + it(`${id.type}`, () => { + before = bb.rank; + complete(id, forceFailure); + expect(bb.rank).toBeLessThan(before); + }); + }); + }); + + it("have a minimum duration of 1 second", () => { + complete(SampleContract); + expect(bb.actionTimeToComplete).toBeGreaterThanOrEqual(1); + }); + + beforeAll(() => { + /* Initialise Formatters. Dependency of Bladeburner Logs/Console */ + FormatsNeedToChange.emit(); + }); + + beforeEach(() => { + setPlayer(new PlayerObject()); + + /** Need BN5 to receive Int EXP */ + Player.sourceFiles.set(5, 3); + + if (initBladeburner(Player)) { + bb = Player.bladeburner; + bb.clearConsole(); + } + + basicStats(); + }); + + function initBladeburner(player: PlayerObject): player is PlayerObject & { bladeburner: Bladeburner } { + player.startBladeburner(); + return true; + } + + function basicStats() { + bb.rank = 1; + bb.changeRank(Player, 400e3); + Player.gainStrengthExp(BASE_STAT_EXP); + Player.gainDefenseExp(BASE_STAT_EXP); + Player.gainAgilityExp(BASE_STAT_EXP); + Player.gainDexterityExp(BASE_STAT_EXP); + bb.calculateMaxStamina(); + + bb.stamina = bb.maxStamina; + + resetCity(); + } + + function resetCity() { + bb.cities[bb.city].chaos = 0; + bb.cities[bb.city].comms = 100; + bb.cities[bb.city].pop = 1e9; + + /** Disable random event */ + bb.randomEventCounter = Infinity; + } + + function allCitiesHighChaos() { + for (const city of Object.values(bb.cities)) { + city.chaos = 1e12; + } + } + + function complete(id: ActionIdentifier, modifySuccessRate?: typeof forceSuccess | typeof forceFailure) { + start(id); + if (modifySuccessRate) modifySuccessRate(id); + finish(); + } + + function forceSuccess(id: ActionIdentifier) { + const action = bb.getActionObject(id); + const success = jest.spyOn(action, "getSuccessChance"); + success.mockReturnValueOnce(1); + } + + function forceFailure() { + bb.stamina = 0; + } + + function start(id: ActionIdentifier) { + const action = bb.getActionObject(id); + if ("count" in action) action.count = 1; + if (action.type === BladeburnerActionType.Operation) action.autoLevel = true; + if (id.type === "Black Operations") bb.numBlackOpsComplete = (action).n; + bb.startAction(id); + } + + function finish() { + bb.processAction(ENOUGH_TIME_TO_FINISH_ACTION); + bb.calculateMaxStamina(); + } + + function actionIdWithIndividualStat(action: Action) { + return Object.entries(action.weights) + .filter(([__, value]) => value > 0) + .map(([stat]) => ({ id: action.id, stat } as { id: ActionIdentifier; stat: keyof Skills })); + } +});