diff --git a/src/CodingContracts.ts b/src/CodingContracts.ts index f4ad1592c..6fe9c2bc6 100644 --- a/src/CodingContracts.ts +++ b/src/CodingContracts.ts @@ -1,20 +1,12 @@ import type { FactionName } from "@enums"; -import { codingContractTypesMetadata, type CodingContractType } from "./data/codingcontracttypes"; +import { codingContractTypesMetadata } 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"; /* Contract Types */ -export const CodingContractTypes: Record> = {}; - -for (const md of codingContractTypesMetadata) { - // Because functions are contravariant with their parameters, we can't - // consider arbitrary CodingContractTypes as CodingContractType - // directly. However, we do want that as the final type, to enforce that the - // state and data are unknown. So we cast through CodingContractType. - CodingContractTypes[md.name] = md as CodingContractType; -} +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 */ diff --git a/src/data/codingcontracttypes.ts b/src/data/codingcontracttypes.ts index e98a4d86e..b667472b5 100644 --- a/src/data/codingcontracttypes.ts +++ b/src/data/codingcontracttypes.ts @@ -1,10 +1,13 @@ import { getRandomIntInclusive } from "../utils/helpers/getRandomIntInclusive"; +import { randomBigIntExclusive } from "../utils/helpers/randomBigIntExclusive"; import { comprGenChar, comprLZGenerate, comprLZEncode, comprLZDecode } from "../utils/CompressionContracts"; import { HammingEncode, HammingDecode, HammingEncodeProperly } from "../utils/HammingCodeTools"; import { filterTruthy } from "../utils/helpers/ArrayHelpers"; -export interface CodingContractType { +// This is the base interface, but should not be used for +// typechecking individual entries. Use the two types below for that. +interface CodingContractType { /** * Function that returns a string with the problem's description. * Requires the 'data' of a Contract as input @@ -31,6 +34,14 @@ export interface CodingContractType { solver: (state: State, answer: string) => boolean; } +// This simple alias uses State == Data, and omits getData since it won't be used in this case. +type CodingContractSimpleType = Omit, "getData">; + +// This alias has unique State and Data, and requires getData. +type CodingContractComplexType = Omit, "getData"> & { + getData: (state: State) => Data; +}; + /* Helper functions for Coding Contract implementations */ function removeBracketsFromArrayString(str: string): string { let strCpy: string = str; @@ -67,7 +78,10 @@ function convert2DArrayToString(arr: number[][]): string { return components.join(",").replace(/\s/g, ""); } -export const codingContractTypesMetadata = [ +// Because functions are contravariant with their parameters, we can't consider +// arbitrary CodingContractTypes as CodingContractType directly. +// So we use CodingContractType here, which at least validates the shape. +export const codingContractTypesMetadata: CodingContractType[] = [ { desc: (n: number): string => { return ["A prime factor is a factor that is a prime number.", `What is the largest prime factor of ${n}?`].join( @@ -91,7 +105,7 @@ export const codingContractTypesMetadata = [ return (n === 1 ? fac - 1 : n) === parseInt(ans, 10); }, - } satisfies CodingContractType, + } satisfies CodingContractSimpleType, { desc: (n: number[]): string => { return [ @@ -121,7 +135,7 @@ export const codingContractTypesMetadata = [ return parseInt(ans, 10) === Math.max(...nums); }, - } satisfies CodingContractType, + } satisfies CodingContractSimpleType, { desc: (n: number): string => { return [ @@ -152,7 +166,7 @@ export const codingContractTypesMetadata = [ return ways[data] === parseInt(ans, 10); }, - } satisfies CodingContractType, + } satisfies CodingContractSimpleType, { desc: (data: [number, number[]]): string => { const n: number = data[0]; @@ -196,7 +210,7 @@ export const codingContractTypesMetadata = [ } return ways[n] === parseInt(ans, 10); }, - } satisfies CodingContractType<[number, number[]]>, + } satisfies CodingContractSimpleType<[number, number[]]>, { desc: (n: number[][]): string => { let d: string = [ @@ -320,7 +334,7 @@ export const codingContractTypesMetadata = [ return true; }, - } satisfies CodingContractType, + } satisfies CodingContractSimpleType, { desc: (arr: number[]): string => { return [ @@ -362,7 +376,7 @@ export const codingContractTypesMetadata = [ const solution: boolean = i === n; return (ans === "1" && solution) || (ans === "0" && !solution); }, - } satisfies CodingContractType, + } satisfies CodingContractSimpleType, { desc: (arr: number[]): string => { return [ @@ -418,7 +432,7 @@ export const codingContractTypesMetadata = [ } return jumps === parseInt(ans, 10); }, - } satisfies CodingContractType, + } satisfies CodingContractSimpleType, { desc: (arr: number[][]): string => { return [ @@ -472,7 +486,7 @@ export const codingContractTypesMetadata = [ return sanitizedResult === sanitizedAns || sanitizedResult === removeBracketsFromArrayString(sanitizedAns); }, - } satisfies CodingContractType, + } satisfies CodingContractSimpleType, { desc: (data: string): string => { return [ @@ -537,7 +551,7 @@ export const codingContractTypesMetadata = [ return true; }, - } satisfies CodingContractType, + } satisfies CodingContractSimpleType, { desc: (data: number[]): string => { return [ @@ -573,7 +587,7 @@ export const codingContractTypesMetadata = [ return maxSoFar.toString() === ans; }, - } satisfies CodingContractType, + } satisfies CodingContractSimpleType, { desc: (data: number[]): string => { return [ @@ -608,7 +622,7 @@ export const codingContractTypesMetadata = [ return profit.toString() === ans; }, - } satisfies CodingContractType, + } satisfies CodingContractSimpleType, { desc: (data: number[]): string => { return [ @@ -649,7 +663,7 @@ export const codingContractTypesMetadata = [ return release2.toString() === ans; }, - } satisfies CodingContractType, + } satisfies CodingContractSimpleType, { desc: (data: [number, number[]]): string => { const k = data[0]; @@ -718,7 +732,7 @@ export const codingContractTypesMetadata = [ return parseInt(ans) === rele[k]; }, - } satisfies CodingContractType<[number, number[]]>, + } satisfies CodingContractSimpleType<[number, number[]]>, { desc: (data: number[][]): string => { function createTriangleRecurse(data: number[][], level = 0): string { @@ -784,7 +798,7 @@ export const codingContractTypesMetadata = [ return dp[0] === parseInt(ans); }, - } satisfies CodingContractType, + } satisfies CodingContractSimpleType, { desc: (data: number[]): string => { const numRows = data[0]; @@ -826,7 +840,7 @@ export const codingContractTypesMetadata = [ return parseInt(ans) === currentRow[n - 1]; }, - } satisfies CodingContractType, + } satisfies CodingContractSimpleType, { desc: (data: number[][]): string => { let gridString = ""; @@ -897,7 +911,7 @@ export const codingContractTypesMetadata = [ return obstacleGrid[obstacleGrid.length - 1][obstacleGrid[0].length - 1] === parseInt(ans); }, - } satisfies CodingContractType, + } satisfies CodingContractSimpleType, { name: "Shortest Path in a Grid", desc: (data: number[][]): string => { @@ -1024,7 +1038,7 @@ export const codingContractTypesMetadata = [ // Path was valid, finally verify that the answer path brought us to the end coordinates return ansY == dstY && ansX == dstX; }, - } satisfies CodingContractType, + } satisfies CodingContractSimpleType, { desc: (data: string): string => { return [ @@ -1131,7 +1145,7 @@ export const codingContractTypesMetadata = [ return true; }, - } satisfies CodingContractType, + } satisfies CodingContractSimpleType, { desc: (data: [string, number]): string => { const digits: string = data[0]; @@ -1243,7 +1257,7 @@ export const codingContractTypesMetadata = [ return true; }, - } satisfies CodingContractType<[string, number]>, + } satisfies CodingContractSimpleType<[string, number]>, { name: "HammingCodes: Integer to Encoded Binary", difficulty: 5, @@ -1275,7 +1289,7 @@ export const codingContractTypesMetadata = [ solver: (data: number, ans: string): boolean => { return ans === HammingEncode(data); }, - } satisfies CodingContractType, + } satisfies CodingContractSimpleType, { name: "HammingCodes: Encoded Binary to Integer", difficulty: 8, @@ -1318,7 +1332,7 @@ export const codingContractTypesMetadata = [ solver: (data: string, ans: string): boolean => { return parseInt(ans, 10) === HammingDecode(data); }, - } satisfies CodingContractType, + } satisfies CodingContractSimpleType, { name: "Proper 2-Coloring of a Graph", difficulty: 7, @@ -1469,7 +1483,7 @@ export const codingContractTypesMetadata = [ //Return false if the coloring is the wrong size else return false; }, - } satisfies CodingContractType<[number, [number, number][]]>, + } satisfies CodingContractSimpleType<[number, [number, number][]]>, { name: "Compression I: RLE Compression", difficulty: 2, @@ -1547,7 +1561,7 @@ export const codingContractTypesMetadata = [ return ans.length <= length; }, - } satisfies CodingContractType, + } satisfies CodingContractSimpleType, { name: "Compression II: LZ Decompression", difficulty: 4, @@ -1581,7 +1595,7 @@ export const codingContractTypesMetadata = [ solver: (compr: string, ans: string): boolean => { return ans === comprLZDecode(compr); }, - } satisfies CodingContractType, + } satisfies CodingContractSimpleType, { name: "Compression III: LZ Compression", difficulty: 10, @@ -1618,7 +1632,7 @@ export const codingContractTypesMetadata = [ solver: (plain: string, ans: string): boolean => { return comprLZDecode(ans) === plain && ans.length <= comprLZEncode(plain).length; }, - } satisfies CodingContractType, + } satisfies CodingContractSimpleType, { desc: (data: [string, number]): string => { return [ @@ -1680,7 +1694,7 @@ export const codingContractTypesMetadata = [ .join(""); return cipher === ans; }, - } satisfies CodingContractType<[string, number]>, + } satisfies CodingContractSimpleType<[string, number]>, { desc: (data: [string, string]): string => { return [ @@ -1848,5 +1862,40 @@ export const codingContractTypesMetadata = [ .join(""); return cipher === ans; }, - } satisfies CodingContractType<[string, string]>, -] as const; + } satisfies CodingContractSimpleType<[string, string]>, + { + name: "Square Root", + difficulty: 5, + desc(data: bigint): string { + return `You are given a ~200 digit BigInt. Find the square root of this number, to the nearest integer. +Hint: If you are having trouble, you might consult https://en.wikipedia.org/wiki/Methods_of_computing_square_roots + +Input number: +${data}`; + }, + generate(): [string, string] { + const half = BigInt(2 ** 332); + // We will square this, meaning the result won't be uniformly distributed anymore. + // That's OK, we never claimed that (just that it would be random). + // We cap the low end to 2^332 so that the problem input is always in the range [2^664, 2^666) which is 200-201 digits. + const ans = randomBigIntExclusive(half) + half; + let offset: bigint; + // The numbers x for which round(sqrt(x)) = n are the integer range [n^2 - n + 1, n^2 + n + 1) + if (Math.random() >= 0.5) { + // Half the time, we will test the edge cases + offset = Math.random() >= 0.5 ? ans : 1n - ans; + } else { + offset = randomBigIntExclusive(ans + ans) + 1n - ans; + } + // Bigints can't be JSON serialized, so we use strings. + return [ans.toString(), offset.toString()]; + }, + getData(state: [string, string]): bigint { + const ans = BigInt(state[0]); + return ans * ans + BigInt(state[1]); + }, + solver(state: [string, string], ans: string): boolean { + return state[0] === ans; + }, + } satisfies CodingContractComplexType, +]; diff --git a/src/utils/helpers/randomBigIntExclusive.ts b/src/utils/helpers/randomBigIntExclusive.ts new file mode 100644 index 000000000..da420d1ed --- /dev/null +++ b/src/utils/helpers/randomBigIntExclusive.ts @@ -0,0 +1,38 @@ +/** + * Return a uniform random number in [0, size). + * Adjusting the range can be done with addition. Because bigints are more + * expensive, it makes more sense to have the 0-based version as a primitive. + */ +export function randomBigIntExclusive(size: bigint): bigint { + // Sadly, the easiest/most efficient way to operate on bigints bitwise is to + // deal with the hex string representation. In particular, this is the + // fastest general way to find the size (number of bits) of the bigint. + const bits = size.toString(16); + if (bits.length <= 12) { + // bigint is at most 48 bits. We can resolve this with a single random() + // call, and that will save special cases below. + return BigInt(Math.floor(Math.random() * Number(size))); + } + // We add 1 to the highest-order random digits, to ensure we cover the + // entire range. If we end up getting a number that is too big we toss it + // and try again. This seems wasteful, but it's actually one of the only + // ways to get a truly uniform result. Since the high-order part is > 2^44 + // by the nature of it having 12 hex digits, we will need to restart at + // *most* 2^-44 of the time, in the pathological cases. + const highpart = parseInt(bits.slice(0, 12), 16) + 1; + let result: bigint; + do { + let str = "0x" + Math.floor(Math.random() * highpart).toString(16); + let i = 12; + for (; i + 12 < bits.length; i += 12) { + str += Math.floor(Math.random() * 2 ** 48).toString(16); + } + // Have to be careful to avoid 32-bit integer shift limit + // By the logic above, this shift will always be > 0, and so we're always + // adding at least one hex digit. + const halfmul = 1 << (2 * (bits.length - i)); + str += Math.floor(Math.random() * halfmul * halfmul).toString(16); + result = BigInt(str); + } while (result >= size); + return result; +}