Files
bitburner-src/src/CodingContract/ContractGenerator.ts
T

232 lines
8.6 KiB
TypeScript

import { CodingContract, CodingContractRewardType, ICodingContractReward } from "./Contract";
import { CodingContractTypes } from "./ContractTypes";
import { currentNodeMults } from "../BitNode/BitNodeMultipliers";
import { Player } from "@player";
import { CodingContractName } from "@enums";
import { GetServer, GetAllServers } from "../Server/AllServers";
import { SpecialServers } from "../Server/data/SpecialServers";
import { Server } from "../Server/Server";
import { BaseServer } from "../Server/BaseServer";
import { getRandomIntInclusive } from "../utils/helpers/getRandomIntInclusive";
import { ContractFilePath, resolveContractFilePath } from "../Paths/ContractFilePath";
import { clampNumber } from "../utils/helpers/clampNumber";
import { getRandomAlphanumericString } from "../utils/StringHelperFunctions";
export function tryGeneratingRandomContract(numberOfTries: number): void {
/**
* We try to generate contracts three times every 10 minutes. 1576800 is the number of tries in 10 years. There is no
* reason to support anything above that. We tested this number (1576800) on a very old machine. It took only ~300ms
* to loop 1576800 times and generate ~10103 contracts on that machine.
*/
numberOfTries = clampNumber(Math.floor(numberOfTries), 0, 1576800);
if (numberOfTries < 1) {
return;
}
let currentNumberOfContracts = GetAllServers().reduce((sum, server) => {
return sum + server.contracts.length;
}, 0);
for (let i = 0; i < numberOfTries; ++i) {
const random = Math.random();
/**
* When currentNumberOfContracts is small, the probability is ~0.25. 25% is the "reasonable" chance of getting a
* contract in normal situations (low currentNumberOfContracts). We have used this probability for a long time as a
* constant before we decide to switch to a new function that is based on currentNumberOfContracts.
*
* This function was chosen due to these characteristics:
* - The probability is exactly 0.25 if currentNumberOfContracts is 0.
* - The probability is ~0.25 if currentNumberOfContracts is small (near 0).
* - The probability approaches 0 when currentNumberOfContracts becomes unusually large:
* - If currentNumberOfContracts is 2500, the probability is 0.23861.
* - If currentNumberOfContracts is 5000, the probability is 0.12462.
* - If currentNumberOfContracts is 7500, the probability is 0.01176.
* - If currentNumberOfContracts is 10000, the probability is 0.0006129.
*
* With this function, we ensure that:
* - The player gets a reasonable amount of contracts in normal situations.
* - If the offline time is unusually large (being offline for years, editing save file, tampering function prototype,
* etc.), the game will not hang when it tries to generate contracts.
*
* These are some data points for reference:
* - 1 month: ~3157 contracts.
* - 3 months: ~6198 contracts.
* - 6 months: ~7231 contracts.
* - 12 months: ~8003 contracts.
* - 2 years: ~8687 contracts.
* - 5 years: ~9506 contracts.
* - 10 years: ~10103 contracts.
* - 25 years: ~10879 contracts.
* - 50 years: ~11461 contracts.
*
* Those numbers mean that if the player does not have any contracts and is online (or loads a save file with
* equivalent offline time) for X months/years, they will have ~Y contracts.
*/
if (random > 100 / (399 + Math.exp(0.0012 * currentNumberOfContracts))) {
continue;
}
generateRandomContract();
++currentNumberOfContracts;
}
}
export function generateRandomContract(): void {
// Choose random server
const randServer = getRandomServer();
if (randServer === null) {
return;
}
// Then select a random reward type. 'Money' will always be the last reward type
const reward = getRandomReward();
// Finally select a random problem type.
// Difficulty is capped to not overwhelm a new player.
const totalSFs = [...Player.sourceFiles].reduce<number>((total, [__bn, lvl]) => (total += lvl), 0);
const maxDif = 2 * totalSFs + 1;
const problemType = getRandomProblemType(maxDif);
const contractFn = getRandomFilename(randServer);
if (contractFn == null) {
return;
}
const contract = new CodingContract(contractFn, problemType, reward);
randServer.addContract(contract);
}
export function generateRandomContractOnHome(): void {
// First select a random problem type
const problemType = getRandomProblemType();
// Then select a random reward type. 'Money' will always be the last reward type
const reward = getRandomReward();
// Choose random server
const serv = Player.getHomeComputer();
const contractFn = getRandomFilename(serv);
if (contractFn == null) {
return;
}
const contract = new CodingContract(contractFn, problemType, reward);
serv.addContract(contract);
}
export const generateDummyContract = (problemType: CodingContractName, server: BaseServer): string | null => {
if (!CodingContractTypes[problemType]) throw new Error(`Invalid problem type: '${problemType}'`);
const contractFn = getRandomFilename(server);
if (contractFn == null) {
return null;
}
const contract = new CodingContract(contractFn, problemType, null);
server.addContract(contract);
return contractFn;
};
interface IGenerateContractParams {
problemType?: CodingContractName;
server?: string;
filename?: ContractFilePath;
reward?: ICodingContractReward;
rewardScaling?: number;
}
export function generateContract(params: IGenerateContractParams): void {
// Problem Type
let problemType;
const problemTypes = Object.keys(CodingContractTypes);
if (params.problemType && problemTypes.includes(params.problemType)) {
problemType = params.problemType;
} else {
problemType = getRandomProblemType();
}
// Reward Type
const reward = params.reward ?? getRandomReward();
// Server
let server;
if (params.server != null) {
server = GetServer(params.server);
if (server == null) {
server = getRandomServer();
}
} else {
server = getRandomServer();
}
if (server === null) {
return;
}
const filename = params.filename ? params.filename : getRandomFilename(server);
if (filename == null) {
return;
}
const contract = new CodingContract(filename, problemType, reward, params.rewardScaling);
server.addContract(contract);
}
function getRandomProblemType(maxDif = 10): CodingContractName {
const problemTypes = Object.values(CodingContractName).filter((x) => CodingContractTypes[x].difficulty <= maxDif);
const randIndex = getRandomIntInclusive(0, problemTypes.length - 1);
return problemTypes[randIndex];
}
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)
if (currentNodeMults.CodingContractMoney > 0) {
validRewardTypes.push(CodingContractRewardType.Money);
}
return { type: getRandomIntInclusive(0, validRewardTypes.length - 1) };
}
function getRandomServer(): BaseServer | null {
const servers = GetAllServers().filter((server: BaseServer) => server.serversOnNetwork.length !== 0);
if (servers.length === 0) {
return null;
}
let randIndex = getRandomIntInclusive(0, servers.length - 1);
let randServer = servers[randIndex];
// An infinite loop shouldn't ever happen, but to be safe we'll use
// a for loop with a limited number of tries
for (let i = 0; i < 200; ++i) {
if (
randServer instanceof Server &&
!randServer.purchasedByPlayer &&
randServer.hostname !== SpecialServers.WorldDaemon
) {
break;
}
randIndex = getRandomIntInclusive(0, servers.length - 1);
randServer = servers[randIndex];
}
return randServer;
}
/**
* This function will return null if the randomized name collides with another contract's name on the specified server.
* 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): 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;
}
const validatedPath = resolveContractFilePath(contractFn);
if (!validatedPath) throw new Error(`Generated contract path could not be validated: ${contractFn}`);
return validatedPath;
}