CCT: Add new Square Root coding contract (#1656)

To make this simpler, there is now a general-purpose util for getting
random bigints. I don't know if anyone else will use it, but it makes
this code simpler.

I also rejiggered the type-checking on contracts a little more, because
the previous refactor didn't quite catch all the mistakes that could be
made.
This commit is contained in:
David Walker 2024-09-30 20:18:58 -07:00 committed by GitHub
parent 32e00aff7a
commit bedd398789
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 119 additions and 40 deletions

@ -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<string, CodingContractType<unknown>> = {};
for (const md of codingContractTypesMetadata) {
// Because functions are contravariant with their parameters, we can't
// consider arbitrary CodingContractTypes as CodingContractType<unknown>
// directly. However, we do want that as the final type, to enforce that the
// state and data are unknown. So we cast through CodingContractType<any>.
CodingContractTypes[md.name] = md as CodingContractType<any>;
}
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 */

@ -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<Data, State = Data> {
// This is the base interface, but should not be used for
// typechecking individual entries. Use the two types below for that.
interface CodingContractType<Data, State = Data> {
/**
* 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<Data, State = Data> {
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<Data> = Omit<CodingContractType<Data, Data>, "getData">;
// This alias has unique State and Data, and requires getData.
type CodingContractComplexType<Data, State> = Omit<CodingContractType<Data, State>, "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<unknown> directly.
// So we use CodingContractType<any> here, which at least validates the shape.
export const codingContractTypesMetadata: CodingContractType<any>[] = [
{
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<number>,
} satisfies CodingContractSimpleType<number>,
{
desc: (n: number[]): string => {
return [
@ -121,7 +135,7 @@ export const codingContractTypesMetadata = [
return parseInt(ans, 10) === Math.max(...nums);
},
} satisfies CodingContractType<number[]>,
} satisfies CodingContractSimpleType<number[]>,
{
desc: (n: number): string => {
return [
@ -152,7 +166,7 @@ export const codingContractTypesMetadata = [
return ways[data] === parseInt(ans, 10);
},
} satisfies CodingContractType<number>,
} satisfies CodingContractSimpleType<number>,
{
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<number[][]>,
} satisfies CodingContractSimpleType<number[][]>,
{
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<number[]>,
} satisfies CodingContractSimpleType<number[]>,
{
desc: (arr: number[]): string => {
return [
@ -418,7 +432,7 @@ export const codingContractTypesMetadata = [
}
return jumps === parseInt(ans, 10);
},
} satisfies CodingContractType<number[]>,
} satisfies CodingContractSimpleType<number[]>,
{
desc: (arr: number[][]): string => {
return [
@ -472,7 +486,7 @@ export const codingContractTypesMetadata = [
return sanitizedResult === sanitizedAns || sanitizedResult === removeBracketsFromArrayString(sanitizedAns);
},
} satisfies CodingContractType<number[][]>,
} satisfies CodingContractSimpleType<number[][]>,
{
desc: (data: string): string => {
return [
@ -537,7 +551,7 @@ export const codingContractTypesMetadata = [
return true;
},
} satisfies CodingContractType<string>,
} satisfies CodingContractSimpleType<string>,
{
desc: (data: number[]): string => {
return [
@ -573,7 +587,7 @@ export const codingContractTypesMetadata = [
return maxSoFar.toString() === ans;
},
} satisfies CodingContractType<number[]>,
} satisfies CodingContractSimpleType<number[]>,
{
desc: (data: number[]): string => {
return [
@ -608,7 +622,7 @@ export const codingContractTypesMetadata = [
return profit.toString() === ans;
},
} satisfies CodingContractType<number[]>,
} satisfies CodingContractSimpleType<number[]>,
{
desc: (data: number[]): string => {
return [
@ -649,7 +663,7 @@ export const codingContractTypesMetadata = [
return release2.toString() === ans;
},
} satisfies CodingContractType<number[]>,
} satisfies CodingContractSimpleType<number[]>,
{
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<number[][]>,
} satisfies CodingContractSimpleType<number[][]>,
{
desc: (data: number[]): string => {
const numRows = data[0];
@ -826,7 +840,7 @@ export const codingContractTypesMetadata = [
return parseInt(ans) === currentRow[n - 1];
},
} satisfies CodingContractType<number[]>,
} satisfies CodingContractSimpleType<number[]>,
{
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<number[][]>,
} satisfies CodingContractSimpleType<number[][]>,
{
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<number[][]>,
} satisfies CodingContractSimpleType<number[][]>,
{
desc: (data: string): string => {
return [
@ -1131,7 +1145,7 @@ export const codingContractTypesMetadata = [
return true;
},
} satisfies CodingContractType<string>,
} satisfies CodingContractSimpleType<string>,
{
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<number>,
} satisfies CodingContractSimpleType<number>,
{
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<string>,
} satisfies CodingContractSimpleType<string>,
{
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<string>,
} satisfies CodingContractSimpleType<string>,
{
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<string>,
} satisfies CodingContractSimpleType<string>,
{
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<string>,
} satisfies CodingContractSimpleType<string>,
{
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<bigint, [string, string]>,
];

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