BUGFIX: Coding contract UI does not handle error properly when answer format is invalid (#2171)

This commit is contained in:
catloversg
2025-06-02 16:33:57 +07:00
committed by GitHub
parent 67017c5782
commit 856ce9a5c9
4 changed files with 132 additions and 61 deletions

View File

@@ -5,6 +5,7 @@ import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver
import { CodingContractEvent } from "../ui/React/CodingContractModal";
import { ContractFilePath, resolveContractFilePath } from "../Paths/ContractFilePath";
import { assertObject } from "../utils/TypeAssertion";
import { Result } from "../types";
// Numeric enum
/** Enum representing the different types of rewards a Coding Contract can give */
@@ -21,6 +22,7 @@ export enum CodingContractResult {
Success,
Failure,
Cancelled,
InvalidFormat,
}
/** A class that represents the type of reward a contract gives */
@@ -67,7 +69,9 @@ export class CodingContract {
reward: ICodingContractReward | null = null,
) {
const path = resolveContractFilePath(fn);
if (!path) throw new Error(`Bad file path while creating a coding contract: ${fn}`);
if (!path) {
throw new Error(`Bad file path while creating a coding contract: ${fn}`);
}
if (!CodingContractTypes[type]) {
throw new Error(`Error: invalid contract type: ${type} please contact developer`);
}
@@ -100,33 +104,59 @@ export class CodingContract {
}
/** 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);
isValid(answer: unknown): Result<{ answer: unknown }> {
if (typeof answer === "string") {
try {
answer = CodingContractTypes[this.type].convertAnswer(answer);
} catch (error) {
return {
success: false,
message: `The answer is not in the right format for contract '${this.type}'. Reason: ${
error instanceof Error ? error.message : String(error)
}`,
};
}
}
const result = CodingContractTypes[this.type].validateAnswer(answer);
if (!result) {
return {
success: false,
message: `The answer is not in the right format for contract '${this.type}'. Got: ${answer}`,
};
}
return { success: true, 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);
isSolution(solution: unknown): {
result: Exclude<CodingContractResult, CodingContractResult.Cancelled>;
message?: string;
} {
const validationResult = this.isValid(solution);
if (!validationResult.success) {
return { result: CodingContractResult.InvalidFormat, message: validationResult.message };
}
/**
* We sometimes need to convert the given solution by calling CodingContractType.convertAnswer() (e.g., Square Root
* contract) before using it. The conversion is done in CodingContract.isValid().
*/
solution = validationResult.answer;
return {
result: CodingContractTypes[this.type].solver(this.state, solution)
? CodingContractResult.Success
: CodingContractResult.Failure,
};
}
/** Creates a popup to prompt the player to solve the problem */
async prompt(): Promise<CodingContractResult> {
return new Promise<CodingContractResult>((resolve) => {
async prompt(): Promise<{ result: CodingContractResult; message?: string }> {
return new Promise((resolve) => {
CodingContractEvent.emit({
c: this,
onClose: () => {
resolve(CodingContractResult.Cancelled);
resolve({ result: CodingContractResult.Cancelled });
},
onAttempt: (val: string) => {
if (this.isSolution(val)) {
resolve(CodingContractResult.Success);
} else {
resolve(CodingContractResult.Failure);
}
resolve(this.isSolution(val));
},
});
});

View File

@@ -1,5 +1,5 @@
import { Player } from "@player";
import { CodingContract } from "../CodingContract/Contract";
import { CodingContract, CodingContractResult } from "../CodingContract/Contract";
import { CodingContractObject, CodingContract as ICodingContract } from "@nsdefs";
import { InternalAPI, NetscriptContext } from "../Netscript/APIWrapper";
import { helpers } from "../Netscript/NetscriptHelpers";
@@ -7,6 +7,7 @@ import { CodingContractName } from "@enums";
import { generateDummyContract } from "../CodingContract/ContractGenerator";
import { isCodingContractName } from "../CodingContract/ContractTypes";
import { type BaseServer } from "../Server/BaseServer";
import { exceptionAlert } from "../utils/helpers/exceptionAlert";
export function NetscriptCodingContract(): InternalAPI<ICodingContract> {
const getCodingContract = function (ctx: NetscriptContext, hostname: string, filename: string): CodingContract {
@@ -25,26 +26,51 @@ export function NetscriptCodingContract(): InternalAPI<ICodingContract> {
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;
const validationResult = contract.isValid(answer);
if (!validationResult.success) {
throw helpers.errorMessage(ctx, validationResult.message);
}
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.`,
);
const resultOfCheckingSolution = contract.isSolution(answer);
switch (resultOfCheckingSolution.result) {
case CodingContractResult.Success: {
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;
}
/**
* This should never happen. If the answer format is invalid, it should already be handled by the call to
* contract.isValid() above.
*/
case CodingContractResult.InvalidFormat: {
exceptionAlert(
new Error(
`contract.isSolution() returns unexpected InvalidFormat result. Type: ${contract.type}. Answer: ${answer}`,
),
true,
);
return "";
}
case CodingContractResult.Failure: {
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 "";
}
default: {
const __: never = resultOfCheckingSolution.result;
}
}
return "";
}
@@ -53,15 +79,8 @@ export function NetscriptCodingContract(): InternalAPI<ICodingContract> {
const filename = helpers.string(ctx, "filename", _filename);
const host = _host ? helpers.string(ctx, "host", _host) : ctx.workerScript.hostname;
const contract = getCodingContract(ctx, host, filename);
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, host);
return attemptContract(ctx, serv, contract, answer);
const server = helpers.getServer(ctx, host);
return attemptContract(ctx, server, contract, answer);
},
getContractType: (ctx) => (_filename, _host?) => {
const filename = helpers.string(ctx, "filename", _filename);

View File

@@ -503,40 +503,51 @@ export class Terminal {
return this.error("There's already a Coding Contract in Progress");
}
const serv = Player.getCurrentServer();
const contract = serv.getContract(contractPath);
if (!contract) return this.error("No such contract");
const server = Player.getCurrentServer();
const contract = server.getContract(contractPath);
if (!contract) {
return this.error("No such contract");
}
this.contractOpen = true;
const res = await contract.prompt();
const promptResult = await contract.prompt();
//Check if the contract still exists by the time the promise is fulfilled
if (serv.getContract(contractPath) == null) {
if (server.getContract(contractPath) == null) {
this.contractOpen = false;
return this.error("Contract no longer exists (Was it solved by a script?)");
}
switch (res) {
switch (promptResult.result) {
case CodingContractResult.Success:
if (contract.reward !== null) {
const reward = Player.gainCodingContractReward(contract.reward, contract.getDifficulty());
this.print(`Contract SUCCESS - ${reward}`);
}
serv.removeContract(contract);
server.removeContract(contract);
break;
case CodingContractResult.InvalidFormat:
this.error(
`Contract FAILED - ${
promptResult.message ?? `The answer is not in the right format for contract '${contract.type}'`
}`,
);
break;
case CodingContractResult.Failure:
++contract.tries;
if (contract.tries >= contract.getMaxNumTries()) {
this.error("Contract FAILED - Contract is now self-destructing");
serv.removeContract(contract);
server.removeContract(contract);
} else {
this.error(`Contract FAILED - ${contract.getMaxNumTries() - contract.tries} tries remaining`);
}
break;
case CodingContractResult.Cancelled:
default:
this.print("Contract cancelled");
break;
default: {
const __: never = promptResult.result;
}
}
this.contractOpen = false;
}

View File

@@ -24,22 +24,26 @@ export function CodingContractModal(): React.ReactElement {
const [answer, setAnswer] = useState("");
useEffect(() => {
CodingContractEvent.subscribe((props) => setContract(props));
});
return CodingContractEvent.subscribe((props) => setContract(props));
}, []);
useEffect(() => {
return () => {
contract?.onClose();
};
}, [contract]);
if (contract === null) return <></>;
if (contract === null) {
return <></>;
}
function onChange(event: React.ChangeEvent<HTMLInputElement>): void {
setAnswer(event.target.value);
}
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
if (contract === null) return;
if (contract === null) {
return;
}
const value = event.currentTarget.value;
if (event.key === KEY.ENTER && value !== "") {
@@ -51,15 +55,22 @@ export function CodingContractModal(): React.ReactElement {
}
function close(): void {
if (contract === null) return;
if (contract === null) {
return;
}
contract.onClose();
setContract(null);
}
const contractType = CodingContractTypes[contract.c.type];
const description = [];
for (const [i, value] of contractType.desc(contract.c.getData()).split("\n").entries())
description.push(<span key={i} dangerouslySetInnerHTML={{ __html: value + "<br />" }}></span>);
for (const [i, value] of contractType.desc(contract.c.getData()).split("\n").entries()) {
description.push(
<span key={i}>
{value} <br />
</span>,
);
}
return (
<Modal open={contract !== null} onClose={close}>
<CopyableText variant="h4" value={contract.c.type} />