CODINGCONTRACT: Add support for other answer formats (#1892)

This commit is contained in:
G4mingJon4s
2025-01-26 18:35:04 +01:00
committed by GitHub
parent b161142796
commit ffae0045a4
11 changed files with 544 additions and 401 deletions

View File

@@ -1,12 +1,9 @@
import {
CodingContract,
CodingContractRewardType,
CodingContractTypes,
ICodingContractReward,
} from "./CodingContracts";
import { CodingContract, CodingContractRewardType, ICodingContractReward } from "./CodingContracts";
import { CodingContractTypes } from "./data/codingcontracttypes";
import { currentNodeMults } from "./BitNode/BitNodeMultipliers";
import { Factions } from "./Faction/Factions";
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";
@@ -104,7 +101,7 @@ export function generateRandomContractOnHome(): void {
serv.addContract(contract);
}
export const generateDummyContract = (problemType: string): string => {
export const generateDummyContract = (problemType: CodingContractName): string => {
if (!CodingContractTypes[problemType]) throw new Error(`Invalid problem type: '${problemType}'`);
const serv = Player.getHomeComputer();
@@ -116,7 +113,7 @@ export const generateDummyContract = (problemType: string): string => {
};
interface IGenerateContractParams {
problemType?: string;
problemType?: CodingContractName;
server?: string;
fn?: ContractFilePath;
}
@@ -176,8 +173,8 @@ function sanitizeRewardType(rewardType: CodingContractRewardType): CodingContrac
return type;
}
function getRandomProblemType(): string {
const problemTypes = Object.keys(CodingContractTypes);
function getRandomProblemType(): CodingContractName {
const problemTypes = Object.values(CodingContractName);
const randIndex = getRandomIntInclusive(0, problemTypes.length - 1);
return problemTypes[randIndex];

View File

@@ -1,14 +1,11 @@
import type { FactionName } from "@enums";
import { codingContractTypesMetadata } from "./data/codingcontracttypes";
import { FactionName, CodingContractName } from "@enums";
import { CodingContractTypes } from "./data/codingcontracttypes";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "./utils/JSONReviver";
import { CodingContractEvent } from "./ui/React/CodingContractModal";
import { ContractFilePath, resolveContractFilePath } from "./Paths/ContractFilePath";
import { assertObject } from "./utils/TypeAssertion";
/* Contract Types */
export const CodingContractTypes = Object.fromEntries(codingContractTypesMetadata.map((x) => [x.name, x]));
// Numeric enum
/** Enum representing the different types of rewards a Coding Contract can give */
export enum CodingContractRewardType {
@@ -62,9 +59,13 @@ export class CodingContract {
tries = 0;
/* String representing the contract's type. Must match type in ContractTypes */
type: string;
type: CodingContractName;
constructor(fn = "default.cct", type = "Find Largest Prime Factor", reward: ICodingContractReward | null = null) {
constructor(
fn = "default.cct",
type = CodingContractName.FindLargestPrimeFactor,
reward: ICodingContractReward | null = null,
) {
const path = resolveContractFilePath(fn);
if (!path) throw new Error(`Bad file path while creating a coding contract: ${fn}`);
if (!CodingContractTypes[type]) {
@@ -94,12 +95,22 @@ export class CodingContract {
return CodingContractTypes[this.type].numTries ?? 10;
}
getType(): string {
getType(): CodingContractName {
return this.type;
}
isSolution(solution: string): boolean {
return CodingContractTypes[this.type].solver(this.state, solution);
/** Checks if the answer is in the correct format. */
isValid(answer: unknown): boolean {
if (typeof answer === "string") answer = CodingContractTypes[this.type].convertAnswer(answer);
return CodingContractTypes[this.type].validateAnswer(answer);
}
isSolution(solution: unknown): boolean {
const type = CodingContractTypes[this.type];
if (typeof solution === "string") solution = type.convertAnswer(solution);
if (!this.isValid(solution)) return false;
return type.solver(this.state, solution);
}
/** Creates a popup to prompt the player to solve the problem */

View File

@@ -10,12 +10,15 @@ import Button from "@mui/material/Button";
import Select, { SelectChangeEvent } from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem";
import { generateContract, generateRandomContract, generateRandomContractOnHome } from "../../CodingContractGenerator";
import { CodingContractTypes } from "../../CodingContracts";
import { isCodingContractName } from "../../data/codingcontracttypes";
import { CodingContractName } from "@enums";
export function CodingContractsDev(): React.ReactElement {
const [codingcontract, setCodingcontract] = useState("Find Largest Prime Factor");
const [codingcontract, setCodingcontract] = useState(CodingContractName.FindLargestPrimeFactor);
function setCodingcontractDropdown(event: SelectChangeEvent): void {
setCodingcontract(event.target.value);
const value = event.target.value;
if (!isCodingContractName(value)) return;
setCodingcontract(value);
}
function specificContract(): void {
@@ -42,9 +45,9 @@ export function CodingContractsDev(): React.ReactElement {
<tr>
<td>
<Select onChange={setCodingcontractDropdown} value={codingcontract}>
{Object.values(CodingContractTypes).map((cc) => (
<MenuItem key={cc.name} value={cc.name}>
{cc.name}
{Object.values(CodingContractName).map((name) => (
<MenuItem key={name} value={name}>
{name}
</MenuItem>
))}
</Select>

View File

@@ -14,3 +14,4 @@ export * from "./Programs/Enums";
export * from "./StockMarket/Enums";
export * from "./ui/Enums";
export * from "./Work/Enums";
export { CodingContractName } from "./data/codingcontracttypes";

View File

@@ -333,6 +333,7 @@ const codingcontract = {
attempt: RamCostConstants.CodingContractBase,
getContractType: RamCostConstants.CodingContractBase / 2,
getData: RamCostConstants.CodingContractBase / 2,
getContract: RamCostConstants.CodingContractBase * (3 / 2),
getDescription: RamCostConstants.CodingContractBase / 2,
getNumTriesRemaining: RamCostConstants.CodingContractBase / 5,
createDummyContract: RamCostConstants.CodingContractBase / 5,

View File

@@ -14,6 +14,7 @@ import { Terminal } from "./Terminal";
import { Player } from "@player";
import {
CityName,
CodingContractName,
CompletedProgramName,
CrimeType,
FactionWorkType,
@@ -123,6 +124,7 @@ export const enums: NSEnums = {
ToastVariant,
UniversityClassType,
CompanyName,
CodingContractName,
};
for (const val of Object.values(enums)) Object.freeze(val);
Object.freeze(enums);

View File

@@ -1,10 +1,12 @@
import { Player } from "@player";
import { CodingContract } from "../CodingContracts";
import { CodingContract as ICodingContract } from "@nsdefs";
import { CodingContractObject, CodingContract as ICodingContract } from "@nsdefs";
import { InternalAPI, NetscriptContext } from "../Netscript/APIWrapper";
import { helpers } from "../Netscript/NetscriptHelpers";
import { codingContractTypesMetadata } from "../data/codingcontracttypes";
import { CodingContractName } from "@enums";
import { generateDummyContract } from "../CodingContractGenerator";
import { isCodingContractName } from "../data/codingcontracttypes";
import { type BaseServer } from "../Server/BaseServer";
export function NetscriptCodingContract(): InternalAPI<ICodingContract> {
const getCodingContract = function (ctx: NetscriptContext, hostname: string, filename: string): CodingContract {
@@ -17,43 +19,49 @@ export function NetscriptCodingContract(): InternalAPI<ICodingContract> {
return contract;
};
function attemptContract(
ctx: NetscriptContext,
server: BaseServer,
contract: CodingContract,
answer: unknown,
): string {
if (contract.isSolution(answer)) {
const reward = Player.gainCodingContractReward(contract.reward, contract.getDifficulty());
helpers.log(ctx, () => `Successfully completed Coding Contract '${contract.fn}'. Reward: ${reward}`);
server.removeContract(contract.fn);
return reward;
}
if (++contract.tries >= contract.getMaxNumTries()) {
helpers.log(ctx, () => `Coding Contract attempt '${contract.fn}' failed. Contract is now self-destructing`);
server.removeContract(contract.fn);
} else {
helpers.log(
ctx,
() =>
`Coding Contract attempt '${contract.fn}' failed. ${
contract.getMaxNumTries() - contract.tries
} attempt(s) remaining.`,
);
}
return "";
}
return {
attempt: (ctx) => (answer, _filename, _hostname?) => {
const filename = helpers.string(ctx, "filename", _filename);
const hostname = _hostname ? helpers.string(ctx, "hostname", _hostname) : ctx.workerScript.hostname;
const contract = getCodingContract(ctx, hostname, filename);
if (typeof answer !== "number" && typeof answer !== "string" && !Array.isArray(answer))
throw new Error("The answer provided was not a number, string, or array");
// Convert answer to string.
// Todo: better typing for contracts, don't do this weird string conversion of the player answer
const answerStr = typeof answer === "string" ? answer : JSON.stringify(answer);
const creward = contract.reward;
if (!contract.isValid(answer))
throw helpers.errorMessage(
ctx,
`Answer is not in the right format for contract '${contract.type}'. Got: ${answer}`,
);
const serv = helpers.getServer(ctx, hostname);
if (contract.isSolution(answerStr)) {
const reward = Player.gainCodingContractReward(creward, contract.getDifficulty());
helpers.log(ctx, () => `Successfully completed Coding Contract '${filename}'. Reward: ${reward}`);
serv.removeContract(filename);
return reward;
} else {
++contract.tries;
if (contract.tries >= contract.getMaxNumTries()) {
helpers.log(ctx, () => `Coding Contract attempt '${filename}' failed. Contract is now self-destructing`);
serv.removeContract(filename);
} else {
helpers.log(
ctx,
() =>
`Coding Contract attempt '${filename}' failed. ${
contract.getMaxNumTries() - contract.tries
} attempts remaining.`,
);
}
return "";
}
return attemptContract(ctx, serv, contract, answer);
},
getContractType: (ctx) => (_filename, _hostname?) => {
const filename = helpers.string(ctx, "filename", _filename);
@@ -65,8 +73,29 @@ export function NetscriptCodingContract(): InternalAPI<ICodingContract> {
const filename = helpers.string(ctx, "filename", _filename);
const hostname = _hostname ? helpers.string(ctx, "hostname", _hostname) : ctx.workerScript.hostname;
const contract = getCodingContract(ctx, hostname, filename);
return structuredClone(contract.getData());
},
getContract: (ctx) => (_filename, _hostname?) => {
const filename = helpers.string(ctx, "filename", _filename);
const hostname = _hostname ? helpers.string(ctx, "hostname", _hostname) : ctx.workerScript.hostname;
const server = helpers.getServer(ctx, hostname);
const contract = getCodingContract(ctx, hostname, filename);
// asserting type here is required, since it is not feasible to properly type getData
return {
type: contract.type,
data: contract.getData(),
submit: (answer: unknown) => {
helpers.checkEnvFlags(ctx);
return attemptContract(ctx, server, contract, answer);
},
description: contract.getDescription(),
numTriesRemaining: () => {
helpers.checkEnvFlags(ctx);
return contract.getMaxNumTries() - contract.tries;
},
} as CodingContractObject;
},
getDescription: (ctx) => (_filename, _hostname?) => {
const filename = helpers.string(ctx, "filename", _filename);
const hostname = _hostname ? helpers.string(ctx, "hostname", _hostname) : ctx.workerScript.hostname;
@@ -81,8 +110,10 @@ export function NetscriptCodingContract(): InternalAPI<ICodingContract> {
},
createDummyContract: (ctx) => (_type) => {
const type = helpers.string(ctx, "type", _type);
if (!isCodingContractName(type))
return helpers.errorMessage(ctx, `The given type is not a valid contract type. Got '${type}'`);
return generateDummyContract(type);
},
getContractTypes: () => () => codingContractTypesMetadata.map((c) => c.name),
getContractTypes: () => () => Object.values(CodingContractName),
};
}

View File

@@ -17,13 +17,6 @@ interface Skills {
intelligence: number;
}
// TODO: provide same treatment to CodingContractData as for SleeveTask (actual types)
/**
* Coding contract data will differ depending on coding contract.
* @public
*/
type CodingContractData = any;
/** @public */
type ScriptArg = string | number | boolean;
@@ -3864,7 +3857,9 @@ export interface CodingContract {
*
* @example
* ```js
* const reward = ns.codingcontract.attempt(yourSolution, filename, hostname);
* const reward = ns.codingcontract.attempt("[solution, as, a, string]", filename, hostname);
* // or
* const reward = ns.codingcontract.attempt(["answer", "as", "an", "array"], filename, hostname);
* if (reward) {
* ns.tprint(`Contract solved successfully! Reward: ${reward}`);
* } else {
@@ -3872,13 +3867,13 @@ export interface CodingContract {
* }
* ```
*
* @param answer - Attempted solution for the contract.
* @param answer - Attempted solution for the contract. This can be a string formatted like submitting manually, or the answer in the format of the specific contract type.
* @param filename - Filename of the contract.
* @param host - Hostname of the server containing the contract. Optional. Defaults to current server if not
* provided.
* @returns A reward description string on success, or an empty string on failure.
*/
attempt(answer: string | number | any[], filename: string, host?: string): string;
attempt(answer: any, filename: string, host?: string): string;
/**
* Get the type of a coding contract.
@@ -3892,7 +3887,7 @@ export interface CodingContract {
* @param host - Hostname of the server containing the contract. Optional. Defaults to current server if not provided.
* @returns Name describing the type of problem posed by the Coding Contract.
*/
getContractType(filename: string, host?: string): string;
getContractType(filename: string, host?: string): `${CodingContractName}`;
/**
* Get the description.
@@ -3920,7 +3915,31 @@ export interface CodingContract {
* @param host - Host of the server containing the contract. Optional. Defaults to current server if not provided.
* @returns The specified contracts data, data type depends on contract type.
*/
getData(filename: string, host?: string): CodingContractData;
getData(filename: string, host?: string): any;
/**
* Get various data about a specific contract.
* @remarks
* RAM cost: 15 GB
*
* The returned object includes the type, data, description as well as methods for getting the number of tries remaining and submitting your answer.
* Depending on the type of the contract, the data is typed differently.
* Using type-narrowing, you can get the correct type of the data:
*
* @example
* ```js
* const contract = ns.codingcontract.getContract(fileName, hostName);
* if (contract.type === ns.enums.CodingContractName.FindLargestPrimeFactor) {
* const data = contract.data;
* // ^? data: number
* }
* ```
*
* @param filename - Filename of the contract.
* @param host - Host of the server containing the contract. Optional. Default to the current server if not provided.
* @returns An object containing various data about the contract specified.
*/
getContract(filename: string, host?: string): CodingContractObject;
/**
* Get the number of attempts remaining.
@@ -3952,7 +3971,7 @@ export interface CodingContract {
* @remarks
* RAM cost: 0 GB
*/
getContractTypes(): string[];
getContractTypes(): `${CodingContractName}`[];
}
/**
@@ -8331,6 +8350,85 @@ declare enum CompanyName {
NoodleBar = "Noodle Bar",
}
declare enum CodingContractName {
FindLargestPrimeFactor = "Find Largest Prime Factor",
SubarrayWithMaximumSum = "Subarray with Maximum Sum",
TotalWaysToSum = "Total Ways to Sum",
TotalWaysToSumII = "Total Ways to Sum II",
SpiralizeMatrix = "Spiralize Matrix",
ArrayJumpingGame = "Array Jumping Game",
ArrayJumpingGameII = "Array Jumping Game II",
MergeOverlappingIntervals = "Merge Overlapping Intervals",
GenerateIPAddresses = "Generate IP Addresses",
AlgorithmicStockTraderI = "Algorithmic Stock Trader I",
AlgorithmicStockTraderII = "Algorithmic Stock Trader II",
AlgorithmicStockTraderIII = "Algorithmic Stock Trader III",
AlgorithmicStockTraderIV = "Algorithmic Stock Trader IV",
MinimumPathSumInATriangle = "Minimum Path Sum in a Triangle",
UniquePathsInAGridI = "Unique Paths in a Grid I",
UniquePathsInAGridII = "Unique Paths in a Grid II",
ShortestPathInAGrid = "Shortest Path in a Grid",
SanitizeParenthesesInExpression = "Sanitize Parentheses in Expression",
FindAllValidMathExpressions = "Find All Valid Math Expressions",
HammingCodesIntegerToEncodedBinary = "HammingCodes: Integer to Encoded Binary",
HammingCodesEncodedBinaryToInteger = "HammingCodes: Encoded Binary to Integer",
Proper2ColoringOfAGraph = "Proper 2-Coloring of a Graph",
CompressionIRLECompression = "Compression I: RLE Compression",
CompressionIILZDecompression = "Compression II: LZ Decompression",
CompressionIIILZCompression = "Compression III: LZ Compression",
EncryptionICaesarCipher = "Encryption I: Caesar Cipher",
EncryptionIIVigenereCipher = "Encryption II: Vigenère Cipher",
SquareRoot = "Square Root",
}
export type CodingContractSignatures = {
[CodingContractName.FindLargestPrimeFactor]: [number, number];
[CodingContractName.SubarrayWithMaximumSum]: [number[], number];
[CodingContractName.TotalWaysToSum]: [number, number];
[CodingContractName.TotalWaysToSumII]: [[number, number[]], number];
[CodingContractName.SpiralizeMatrix]: [number[][], number[]];
[CodingContractName.ArrayJumpingGame]: [number[], 1 | 0];
[CodingContractName.ArrayJumpingGameII]: [number[], number];
[CodingContractName.MergeOverlappingIntervals]: [[number, number][], [number, number][]];
[CodingContractName.GenerateIPAddresses]: [string, string[]];
[CodingContractName.AlgorithmicStockTraderI]: [number[], number];
[CodingContractName.AlgorithmicStockTraderII]: [number[], number];
[CodingContractName.AlgorithmicStockTraderIII]: [number[], number];
[CodingContractName.AlgorithmicStockTraderIV]: [[number, number[]], number];
[CodingContractName.MinimumPathSumInATriangle]: [number[][], number];
[CodingContractName.UniquePathsInAGridI]: [[number, number], number];
[CodingContractName.UniquePathsInAGridII]: [(1 | 0)[][], number];
[CodingContractName.ShortestPathInAGrid]: [(1 | 0)[][], string];
[CodingContractName.SanitizeParenthesesInExpression]: [string, string[]];
[CodingContractName.FindAllValidMathExpressions]: [[string, number], string[]];
[CodingContractName.HammingCodesIntegerToEncodedBinary]: [number, string];
[CodingContractName.HammingCodesEncodedBinaryToInteger]: [string, number];
[CodingContractName.Proper2ColoringOfAGraph]: [[number, [number, number][]], (1 | 0)[]];
[CodingContractName.CompressionIRLECompression]: [string, string];
[CodingContractName.CompressionIILZDecompression]: [string, string];
[CodingContractName.CompressionIIILZCompression]: [string, string];
[CodingContractName.EncryptionICaesarCipher]: [[string, number], string];
[CodingContractName.EncryptionIIVigenereCipher]: [[string, string], string];
[CodingContractName.SquareRoot]: [bigint, bigint, [string, string]];
};
export type CodingContractData<T extends string> = T extends `${keyof CodingContractSignatures}`
? CodingContractSignatures[T][0]
: any;
export type CodingContractAnswer<T extends string> = T extends `${keyof CodingContractSignatures}`
? CodingContractSignatures[T][1]
: any;
export type CodingContractObject = {
[T in keyof CodingContractSignatures]: {
type: T;
data: CodingContractSignatures[T][0];
submit: (answer: CodingContractSignatures[T][1] | string) => string;
description: string;
numTriesRemaining: () => number;
};
}[keyof CodingContractSignatures];
/** @public */
export type NSEnums = {
CityName: typeof CityName;
@@ -8343,6 +8441,7 @@ export type NSEnums = {
ToastVariant: typeof ToastVariant;
UniversityClassType: typeof UniversityClassType;
CompanyName: typeof CompanyName;
CodingContractName: typeof CodingContractName;
};
/**

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,8 @@
import React, { useState, useEffect } from "react";
import { KEY } from "../../utils/helpers/keyCodes";
import { CodingContract, CodingContractTypes } from "../../CodingContracts";
import { CodingContract } from "../../CodingContracts";
import { CodingContractTypes } from "../../data/codingcontracttypes";
import { CopyableText } from "./CopyableText";
import { Modal } from "./Modal";
import { EventEmitter } from "../../utils/EventEmitter";

View File

@@ -12,7 +12,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { Player } from "@player";
import { AugmentationName, LocationName } from "@enums";
import { AugmentationName, CodingContractName, LocationName } from "@enums";
import { AddToAllServers, createUniqueRandomIp, GetAllServers, GetServer, renameServer } from "../Server/AllServers";
import { StockMarket } from "../StockMarket/StockMarket";
import { AwardNFG, v1APIBreak } from "./v1APIBreak";
@@ -168,8 +168,8 @@ export function evaluateVersionCompatibility(ver: string | number): void {
for (const contract of server.contracts) {
//Rename old "HammingCodes: Integer to encoded Binary" contracts
//to "HammingCodes: Integer to Encoded Binary"
if (contract.type == "HammingCodes: Integer to encoded Binary") {
contract.type = "HammingCodes: Integer to Encoded Binary";
if ((contract.type as string) == "HammingCodes: Integer to encoded Binary") {
contract.type = CodingContractName.HammingCodesIntegerToEncodedBinary;
}
}
}