MISC: Change how coding contract rewards are randomized (#2490)

This commit is contained in:
catloversg
2026-02-12 01:34:59 +07:00
committed by GitHub
parent 62f7501b43
commit 638c791047
4 changed files with 189 additions and 113 deletions

View File

@@ -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;
};
/**

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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);
});
});