mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-16 06:18:42 +02:00
MISC: Change how coding contract rewards are randomized (#2490)
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user