diff --git a/src/CodingContract/Contract.ts b/src/CodingContract/Contract.ts index a2f384561..3b4b966ea 100644 --- a/src/CodingContract/Contract.ts +++ b/src/CodingContract/Contract.ts @@ -1,4 +1,4 @@ -import { FactionName, CodingContractName } from "@enums"; +import { CodingContractName } from "@enums"; import { CodingContractTypes } from "./ContractTypes"; import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver"; @@ -13,7 +13,7 @@ export enum CodingContractRewardType { FactionReputation, FactionReputationAll, CompanyReputation, - Money, // This must always be the last reward type + Money, } // Numeric enum @@ -35,11 +35,9 @@ export type ICodingContractReward = } | { type: CodingContractRewardType.CompanyReputation; - name: string; } | { type: CodingContractRewardType.FactionReputation; - name: FactionName; }; /** diff --git a/src/CodingContract/ContractGenerator.ts b/src/CodingContract/ContractGenerator.ts index 9c2cbbeeb..bb3cb8632 100644 --- a/src/CodingContract/ContractGenerator.ts +++ b/src/CodingContract/ContractGenerator.ts @@ -1,7 +1,6 @@ import { CodingContract, CodingContractRewardType, ICodingContractReward } from "./Contract"; import { CodingContractTypes } from "./ContractTypes"; import { currentNodeMults } from "../BitNode/BitNodeMultipliers"; -import { Factions } from "../Faction/Factions"; import { Player } from "@player"; import { CodingContractName } from "@enums"; import { GetServer, GetAllServers } from "../Server/AllServers"; @@ -85,7 +84,7 @@ export function generateRandomContract(): void { const problemType = getRandomProblemType(maxDif); - const contractFn = getRandomFilename(randServer, reward); + const contractFn = getRandomFilename(randServer); if (contractFn == null) { return; } @@ -104,7 +103,7 @@ export function generateRandomContractOnHome(): void { // Choose random server const serv = Player.getHomeComputer(); - const contractFn = getRandomFilename(serv, reward); + const contractFn = getRandomFilename(serv); if (contractFn == null) { return; } @@ -129,7 +128,8 @@ export const generateDummyContract = (problemType: CodingContractName, server: B interface IGenerateContractParams { problemType?: CodingContractName; server?: string; - fn?: ContractFilePath; + filename?: ContractFilePath; + reward?: ICodingContractReward; } export function generateContract(params: IGenerateContractParams): void { @@ -142,8 +142,8 @@ export function generateContract(params: IGenerateContractParams): void { problemType = getRandomProblemType(); } - // Reward Type - This is always random for now - const reward = getRandomReward(); + // Reward Type + const reward = params.reward ?? getRandomReward(); // Server let server; @@ -159,7 +159,7 @@ export function generateContract(params: IGenerateContractParams): void { return; } - const filename = params.fn ? params.fn : getRandomFilename(server, reward); + const filename = params.filename ? params.filename : getRandomFilename(server); if (filename == null) { return; } @@ -167,32 +167,6 @@ export function generateContract(params: IGenerateContractParams): void { server.addContract(contract); } -// Ensures that a contract's reward type is valid -function sanitizeRewardType(rewardType: CodingContractRewardType): CodingContractRewardType { - let type = rewardType; // Create copy - - const factionsThatAllowHacking = Player.factions.filter((fac) => { - try { - return Factions[fac].getInfo().offerHackingWork; - } catch (e) { - console.error("Error when trying to filter Hacking Factions for Coding Contract Generation", e); - return false; - } - }); - if (type === CodingContractRewardType.CompanyReputation && Object.keys(Player.jobs).length === 0) { - type = - Math.random() < 0.5 ? CodingContractRewardType.FactionReputation : CodingContractRewardType.FactionReputationAll; - } - if (type === CodingContractRewardType.FactionReputation && factionsThatAllowHacking.length === 0) { - type = CodingContractRewardType.Money; - } - if (type === CodingContractRewardType.FactionReputationAll && factionsThatAllowHacking.length === 0) { - type = CodingContractRewardType.Money; - } - - return type; -} - function getRandomProblemType(maxDif = 10): CodingContractName { const problemTypes = Object.values(CodingContractName).filter((x) => CodingContractTypes[x].difficulty <= maxDif); const randIndex = getRandomIntInclusive(0, problemTypes.length - 1); @@ -200,42 +174,17 @@ function getRandomProblemType(maxDif = 10): CodingContractName { return problemTypes[randIndex]; } -function getRandomReward(): ICodingContractReward { +export function getRandomReward(): ICodingContractReward { + const validRewardTypes = [ + CodingContractRewardType.FactionReputation, + CodingContractRewardType.FactionReputationAll, + CodingContractRewardType.CompanyReputation, + ]; // Don't offer money reward by default if BN multiplier is 0 (e.g. BN8) - const rewardTypeUpperBound = - currentNodeMults.CodingContractMoney === 0 ? CodingContractRewardType.Money - 1 : CodingContractRewardType.Money; - const rewardType = sanitizeRewardType(getRandomIntInclusive(0, rewardTypeUpperBound)); - - // Add additional information based on the reward type - const factionsThatAllowHacking = Player.factions.filter((fac) => Factions[fac].getInfo().offerHackingWork); - - switch (rewardType) { - case CodingContractRewardType.FactionReputation: { - // Get a random faction that player is a part of. That - // faction must allow hacking contracts - const numFactions = factionsThatAllowHacking.length; - // This check is unnecessary because sanitizeRewardType ensures that it won't happen. However, I'll still leave - // it here, just in case somebody else changes sanitizeRewardType without taking account of this check. - if (numFactions > 0) { - const randFaction = factionsThatAllowHacking[getRandomIntInclusive(0, numFactions - 1)]; - return { type: rewardType, name: randFaction }; - } - return { type: CodingContractRewardType.Money }; - } - case CodingContractRewardType.CompanyReputation: { - const allJobs = Object.keys(Player.jobs); - // This check is also unnecessary. Check the comment above. - if (allJobs.length > 0) { - return { - type: CodingContractRewardType.CompanyReputation, - name: allJobs[getRandomIntInclusive(0, allJobs.length - 1)], - }; - } - return { type: CodingContractRewardType.Money }; - } - default: - return { type: rewardType }; + if (currentNodeMults.CodingContractMoney > 0) { + validRewardTypes.push(CodingContractRewardType.Money); } + return { type: getRandomIntInclusive(0, validRewardTypes.length - 1) }; } function getRandomServer(): BaseServer | null { @@ -277,16 +226,8 @@ function getRandomAlphanumericString(length: number) { * Callers of this function must return early and not generate a contract when it happens. It likely happens when there * are ~240k contracts on the specified server. */ -export function getRandomFilename( - server: BaseServer, - reward: ICodingContractReward = { type: CodingContractRewardType.Money }, -): ContractFilePath | null { - let contractFn = `contract-${getRandomAlphanumericString(6)}`; - if ("name" in reward) { - // Only alphanumeric characters in the reward name. - contractFn += `-${reward.name.replace(/[^a-zA-Z0-9]/g, "")}`; - } - contractFn += ".cct"; +export function getRandomFilename(server: BaseServer): ContractFilePath | null { + const contractFn = `contract-${getRandomAlphanumericString(6)}.cct`; // Return null if there is a contract with the same name. if (server.contracts.filter((c: CodingContract) => c.fn === contractFn).length) { return null; diff --git a/src/PersonObjects/Player/PlayerObjectGeneralMethods.ts b/src/PersonObjects/Player/PlayerObjectGeneralMethods.ts index 511f34759..a0356c6ec 100644 --- a/src/PersonObjects/Player/PlayerObjectGeneralMethods.ts +++ b/src/PersonObjects/Player/PlayerObjectGeneralMethods.ts @@ -48,7 +48,6 @@ import { SnackbarEvents } from "../../ui/React/Snackbar"; import { achievements } from "../../Achievements/Achievements"; import { isCompanyWork } from "../../Work/CompanyWork"; -import { isMember } from "../../utils/EnumHelper"; import { canAccessBitNodeFeature } from "../../BitNode/BitNodeUtils"; import { AlertEvents } from "../../ui/React/AlertManager"; import { Augmentations } from "../../Augmentation/Augmentations"; @@ -57,6 +56,9 @@ import type { Result } from "@nsdefs"; import type { AchievementId } from "../../Achievements/Types"; import { Infiltration } from "../../Infiltration/Infiltration"; import { recalculateNumberOfOwnedSleeves } from "../Sleeve/SleeveCovenantPurchases"; +import { Player } from "@player"; +import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive"; +import { getRecordKeys } from "../../Types/Record"; export function init(this: PlayerObject): void { /* Initialize Player's home computer */ @@ -495,57 +497,65 @@ export function gainCodingContractReward( reward: ICodingContractReward | null, difficulty = 1, ): string { - if (!reward) return `No reward for this contract`; + if (!reward) { + return `No reward for this contract`; + } switch (reward.type) { case CodingContractRewardType.FactionReputation: { - if (!Factions[reward.name]) { - return this.gainCodingContractReward({ type: CodingContractRewardType.FactionReputationAll }); + const factionsThatAllowHacking = Player.factions.filter((fac) => Factions[fac].getInfo().offerHackingWork); + if (factionsThatAllowHacking.length === 0) { + return this.gainCodingContractReward({ type: CodingContractRewardType.Money }, difficulty); } + const randomFaction = factionsThatAllowHacking[getRandomIntInclusive(0, factionsThatAllowHacking.length - 1)]; const repGain = CONSTANTS.CodingContractBaseFactionRepGain * difficulty; - Factions[reward.name].playerReputation += repGain; - return `Gained ${repGain} faction reputation for ${reward.name}`; + Factions[randomFaction].playerReputation += repGain; + return `Gained ${repGain} faction reputation for ${randomFaction}`; } case CodingContractRewardType.FactionReputationAll: { - const totalGain = CONSTANTS.CodingContractBaseFactionRepGain * difficulty; - - // Ignore Bladeburners and other special factions for this calculation - const specialFactions = [ - FactionName.Bladeburners, - FactionName.ShadowsOfAnarchy, - FactionName.ChurchOfTheMachineGod, - ]; - const factions = this.factions.slice().filter((f) => { - return !specialFactions.includes(f); - }); - - // If the player was only part of the special factions, we'll just give money - if (factions.length == 0) { + const factionsThatAllowHacking = Player.factions.filter((fac) => Factions[fac].getInfo().offerHackingWork); + if (factionsThatAllowHacking.length === 0) { return this.gainCodingContractReward({ type: CodingContractRewardType.Money }, difficulty); } - const gainPerFaction = Math.floor(totalGain / factions.length); - for (const facName of factions) { - if (!Factions[facName]) continue; + const totalGain = CONSTANTS.CodingContractBaseFactionRepGain * difficulty; + const gainPerFaction = Math.floor(totalGain / factionsThatAllowHacking.length); + for (const facName of factionsThatAllowHacking) { Factions[facName].playerReputation += gainPerFaction; } - return `Gained ${gainPerFaction} reputation for each of the following factions: ${factions.join(", ")}`; + return `Gained ${gainPerFaction} reputation for each of the following factions: ${factionsThatAllowHacking.join( + ", ", + )}`; } case CodingContractRewardType.CompanyReputation: { - if (!isMember("CompanyName", reward.name)) { - return this.gainCodingContractReward({ type: CodingContractRewardType.FactionReputationAll }); + const companies = getRecordKeys(Player.jobs); + if (companies.length === 0) { + return this.gainCodingContractReward( + { + type: + Math.random() < 0.5 + ? CodingContractRewardType.FactionReputation + : CodingContractRewardType.FactionReputationAll, + }, + difficulty, + ); } + const randomCompany = companies[getRandomIntInclusive(0, companies.length - 1)]; const repGain = CONSTANTS.CodingContractBaseCompanyRepGain * difficulty; - Companies[reward.name].playerReputation += repGain; - return `Gained ${repGain} company reputation for ${reward.name}`; + Companies[randomCompany].playerReputation += repGain; + return `Gained ${repGain} company reputation for ${randomCompany}`; } - case CodingContractRewardType.Money: - default: { + case CodingContractRewardType.Money: { const moneyGain = CONSTANTS.CodingContractBaseMoneyGain * difficulty * currentNodeMults.CodingContractMoney; this.gainMoney(moneyGain, "codingcontract"); return `Gained ${formatMoney(moneyGain)}`; } + default: { + // Verify type switch statement is exhaustive + const __a: never = reward; + } } + throw new Error("Invalid coding contract reward type"); } export function gotoLocation(this: PlayerObject, to: LocationName): boolean { diff --git a/test/jest/CodingContract/ContractGenerator.test.ts b/test/jest/CodingContract/ContractGenerator.test.ts index bdc979c42..a31e074c2 100644 --- a/test/jest/CodingContract/ContractGenerator.test.ts +++ b/test/jest/CodingContract/ContractGenerator.test.ts @@ -5,12 +5,18 @@ import { generateRandomContract, generateRandomContractOnHome, getRandomFilename, + getRandomReward, } from "../../../src/CodingContract/ContractGenerator"; -import { CodingContractName } from "../../../src/Enums"; +import { CodingContractName, CompanyName, JobField, JobName } from "../../../src/Enums"; import { GetAllServers } from "../../../src/Server/AllServers"; import type { BaseServer } from "../../../src/Server/BaseServer"; import { SpecialServers } from "../../../src/Server/data/SpecialServers"; -import { initGameEnvironment, setupBasicTestingEnvironment } from "../Utilities"; +import { getNS, initGameEnvironment, setupBasicTestingEnvironment } from "../Utilities"; +import { prestigeSourceFile } from "../../../src/Prestige"; +import { CodingContractRewardType } from "../../../src/CodingContract/Contract"; +import { joinFaction } from "../../../src/Faction/FactionHelpers"; +import { Factions } from "../../../src/Faction/Factions"; +import { Companies } from "../../../src/Company/Companies"; beforeAll(() => { initGameEnvironment(); @@ -74,3 +80,124 @@ describe("getRandomFilename", () => { expect(set.size).toStrictEqual(maxIter); }); }); + +describe("getRandomReward", () => { + test("Disable money reward when CodingContractMoney BN multiplier is 0", () => { + Player.bitNodeN = 8; + prestigeSourceFile(true); + for (let i = 0; i < 1000; ++i) { + expect(getRandomReward().type).not.toStrictEqual(CodingContractRewardType.Money); + } + }); + test("Have all valid reward types", () => { + const rewardTypeCounts = [0, 0, 0, 0]; + for (let i = 0; i < 1000; ++i) { + ++rewardTypeCounts[getRandomReward().type]; + } + expect(rewardTypeCounts.length).toStrictEqual(4); + expect(rewardTypeCounts[CodingContractRewardType.FactionReputation]).toBeGreaterThan(0); + expect(rewardTypeCounts[CodingContractRewardType.FactionReputationAll]).toBeGreaterThan(0); + expect(rewardTypeCounts[CodingContractRewardType.CompanyReputation]).toBeGreaterThan(0); + expect(rewardTypeCounts[CodingContractRewardType.Money]).toBeGreaterThan(0); + }); +}); + +describe("Receive correct reward", () => { + test("FactionReputation", () => { + const ns = getNS(); + generateContract({ + problemType: CodingContractName.FindLargestPrimeFactor, + server: SpecialServers.Home, + reward: { type: CodingContractRewardType.FactionReputation }, + }); + joinFaction(Factions.CyberSec); + expect(Factions.CyberSec.playerReputation).toStrictEqual(0); + const contract = Player.getHomeComputer().contracts[0]; + ns.codingcontract.attempt(contract.getAnswer(), contract.fn); + expect(Factions.CyberSec.playerReputation).toBeGreaterThan(0); + }); + test("FactionReputationAll", () => { + const ns = getNS(); + generateContract({ + problemType: CodingContractName.FindLargestPrimeFactor, + server: SpecialServers.Home, + reward: { type: CodingContractRewardType.FactionReputationAll }, + }); + joinFaction(Factions.CyberSec); + joinFaction(Factions.NiteSec); + expect(Factions.CyberSec.playerReputation).toStrictEqual(0); + expect(Factions.NiteSec.playerReputation).toStrictEqual(0); + const contract = Player.getHomeComputer().contracts[0]; + ns.codingcontract.attempt(contract.getAnswer(), contract.fn); + expect(Factions.CyberSec.playerReputation).toBeGreaterThan(0); + expect(Factions.NiteSec.playerReputation).toBeGreaterThan(0); + }); + test("CompanyReputation", () => { + const ns = getNS(); + generateContract({ + problemType: CodingContractName.FindLargestPrimeFactor, + server: SpecialServers.Home, + reward: { type: CodingContractRewardType.CompanyReputation }, + }); + expect(ns.singularity.applyToCompany(CompanyName.JoesGuns, JobField.employee)).toStrictEqual(JobName.employee); + expect(Companies[CompanyName.JoesGuns].playerReputation).toStrictEqual(0); + const contract = Player.getHomeComputer().contracts[0]; + ns.codingcontract.attempt(contract.getAnswer(), contract.fn); + expect(Companies[CompanyName.JoesGuns].playerReputation).toBeGreaterThan(0); + }); + test("Money", () => { + const ns = getNS(); + generateContract({ + problemType: CodingContractName.FindLargestPrimeFactor, + server: SpecialServers.Home, + reward: { type: CodingContractRewardType.Money }, + }); + Player.money = 0; + const contract = Player.getHomeComputer().contracts[0]; + ns.codingcontract.attempt(contract.getAnswer(), contract.fn); + expect(Player.money).toBeGreaterThan(0); + }); +}); + +describe("Receive fallback reward", () => { + test("FactionReputation", () => { + const ns = getNS(); + generateContract({ + problemType: CodingContractName.FindLargestPrimeFactor, + server: SpecialServers.Home, + reward: { type: CodingContractRewardType.FactionReputation }, + }); + expect(Player.factions.length).toStrictEqual(0); + Player.money = 0; + const contract = Player.getHomeComputer().contracts[0]; + ns.codingcontract.attempt(contract.getAnswer(), contract.fn); + expect(Player.money).toBeGreaterThan(0); + }); + test("FactionReputationAll", () => { + const ns = getNS(); + generateContract({ + problemType: CodingContractName.FindLargestPrimeFactor, + server: SpecialServers.Home, + reward: { type: CodingContractRewardType.FactionReputationAll }, + }); + expect(Player.factions.length).toStrictEqual(0); + Player.money = 0; + const contract = Player.getHomeComputer().contracts[0]; + ns.codingcontract.attempt(contract.getAnswer(), contract.fn); + expect(Player.money).toBeGreaterThan(0); + }); + test("CompanyReputation", () => { + const ns = getNS(); + generateContract({ + problemType: CodingContractName.FindLargestPrimeFactor, + server: SpecialServers.Home, + reward: { type: CodingContractRewardType.CompanyReputation }, + }); + expect(Object.keys(Player.jobs).length).toStrictEqual(0); + joinFaction(Factions.CyberSec); + expect(Factions.CyberSec.playerReputation).toStrictEqual(0); + const contract = Player.getHomeComputer().contracts[0]; + ns.codingcontract.attempt(contract.getAnswer(), contract.fn); + expect(Factions.CyberSec.playerReputation).toBeGreaterThan(0); + }); +});