diff --git a/frontend/src/components/PasteRegistration.tsx b/frontend/src/components/PasteRegistration.tsx
new file mode 100644
index 0000000..cf043f8
--- /dev/null
+++ b/frontend/src/components/PasteRegistration.tsx
@@ -0,0 +1,142 @@
+import { Typography } from "@mui/material";
+import Box from "@mui/material/Box";
+import HashIcon from "@mui/icons-material/NumbersRounded";
+import Button from "@mui/material/Button";
+import Snackbar from "@mui/material/Snackbar";
+import Alert from "@mui/material/Alert";
+import { alpha, useTheme } from "@mui/material/styles";
+import TextField from "components/TextField";
+import { useAppDispatch } from "hooks/useAppDispatch";
+import { useAppSelector } from "hooks/useAppSelector";
+import { FC, useState } from "react";
+import { commentsActions } from "store/slices/commentsSlice";
+import { digitCodeActions } from "store/slices/digitCodeSlice";
+import { registrationActions } from "store/slices/registrationSlice";
+import { roundsActions } from "store/slices/roundsSlice";
+import { parse as parseTuringInfo } from "parsing/turing-copy-paste";
+import { parse as parseProblemBook } from "parsing/problem-book";
+
+const PasteRegistration: FC = () => {
+ const dispatch = useAppDispatch();
+ const registration = useAppSelector((state) => state.registration);
+ const [cardText, setCardText] = useState("");
+ const [showNotFound, setShowNotFound] = useState(false);
+ const theme = useTheme();
+
+ function onSubmit() {
+ const problem = parseTuringInfo(cardText) || parseProblemBook(cardText);
+ console.log(problem);
+ if (problem === null) {
+ setShowNotFound(true);
+ return;
+ }
+ dispatch(registrationActions.updateHash(problem.code.toUpperCase()));
+ dispatch(roundsActions.reset());
+ dispatch(commentsActions.reset());
+ dispatch(digitCodeActions.reset());
+ dispatch(registrationActions.fetchDone());
+ dispatch(commentsActions.setCards(problem));
+ }
+
+ if (registration.status !== "new") {
+ return (
+ }
+ value={registration.hash}
+ maxChars={10}
+ customRadius={
+ registration.status === "ready"
+ ? theme.spacing(0, 0, 2, 2)
+ : undefined
+ }
+ />
+ );
+ }
+
+ return (
+ <>
+ {
+ setShowNotFound(false);
+ }}
+ >
+ {
+ setShowNotFound(false);
+ }}
+ severity="error"
+ sx={{ width: "100%" }}
+ variant="filled"
+ >
+ Could not parse the game setup. Did you copy&paste the whole setup
+ from turingmachine.info or
+ the{" "}
+
+ problem book
+
+ ?
+
+
+
+ >
+ );
+};
+
+export default PasteRegistration;
diff --git a/frontend/src/parsing/__test__/problem-book.test.ts b/frontend/src/parsing/__test__/problem-book.test.ts
new file mode 100644
index 0000000..e131464
--- /dev/null
+++ b/frontend/src/parsing/__test__/problem-book.test.ts
@@ -0,0 +1,38 @@
+import { parse } from "parsing/problem-book";
+
+test("C65 6FF P", () => {
+ expect(
+ parse("5,#C65 6FF P,->,5,22,24,27,33,48,O252,O536,O658,O213,O613,O289")
+ ).toStrictEqual({
+ code: "C656FFP",
+ color: 1,
+ ind: [5, 22, 24, 27, 33, 48],
+ crypt: [252, 536, 658, 213, 613, 289],
+ m: 0,
+ });
+});
+
+test("F4D EXI", () => {
+ expect(
+ parse("10,#F4D EXI,->,25,23,24,16,36,4,42,14,B749,B643,B586,B325")
+ ).toStrictEqual({
+ code: "F4DEXI",
+ color: 2,
+ ind: [25, 24, 36, 42],
+ fake: [23, 16, 4, 14],
+ crypt: [749, 643, 586, 325],
+ m: 1,
+ });
+});
+
+test("I51 ZKH K", () => {
+ expect(
+ parse("4,#I51 ZKH K,->,N23,N24,N25,N41,N48,G681,G746,G440,G350,G771")
+ ).toStrictEqual({
+ code: "I51ZKHK",
+ color: 0,
+ ind: [23, 24, 25, 41, 48],
+ crypt: [681, 746, 440, 350, 771],
+ m: 2,
+ });
+});
diff --git a/frontend/src/parsing/__test__/turing-copy-paste.test.ts b/frontend/src/parsing/__test__/turing-copy-paste.test.ts
new file mode 100644
index 0000000..80f4c1c
--- /dev/null
+++ b/frontend/src/parsing/__test__/turing-copy-paste.test.ts
@@ -0,0 +1,102 @@
+import { parse } from "parsing/turing-copy-paste";
+
+test("A518FRM", () => {
+ expect(
+ parse(
+ `op #A51 8FR M Share Get Criteria and Verification cards in the box. A 1 652 B 7 405 C 12 616 D 15 331 E 16 505 Back to Homepage`
+ )
+ ).toStrictEqual({
+ ind: [1, 7, 12, 15, 16],
+ crypt: [652, 405, 616, 331, 505],
+ color: 2,
+ m: 0,
+ code: "A518FRM",
+ });
+});
+
+test("A4BL4O", () => {
+ expect(
+ parse(
+ `op #A4B L4O Share Get Criteria and Verification cards in the box. A 5 578 B 10 376 C 11 566 D 17 618 Back to Homepage`
+ )
+ ).toStrictEqual({
+ ind: [5, 10, 11, 17],
+ crypt: [578, 376, 566, 618],
+ color: 0,
+ m: 0,
+ code: "A4BL4O",
+ });
+});
+
+test("A4BL4O lowercase", () => {
+ expect(
+ parse(
+ `op #A4B L4O Share Get Criteria and Verification cards in the box. A 5 578 B 10 376 C 11 566 D 17 618 Back to Homepage`.toLowerCase()
+ )
+ ).toStrictEqual({
+ ind: [5, 10, 11, 17],
+ crypt: [578, 376, 566, 618],
+ color: 0,
+ m: 0,
+ code: "A4BL4O",
+ });
+});
+
+test("A4BL4O uppercase", () => {
+ expect(
+ parse(
+ `op #A4B L4O Share Get Criteria and Verification cards in the box. A 5 578 B 10 376 C 11 566 D 17 618 Back to Homepage`.toUpperCase()
+ )
+ ).toStrictEqual({
+ ind: [5, 10, 11, 17],
+ crypt: [578, 376, 566, 618],
+ color: 0,
+ m: 0,
+ code: "A4BL4O",
+ });
+});
+
+test("C65 6FF P", () => {
+ expect(
+ parse(
+ `op #C65 6FF P Share Get Criteria and Verification cards in the box. A 5 252 B 22 536 C 24 658 D 27 213 E 33 613 F 48 289 Back to Homepage`
+ )
+ ).toStrictEqual({
+ ind: [5, 22, 24, 27, 33, 48],
+ crypt: [252, 536, 658, 213, 613, 289],
+ color: 1,
+ m: 0,
+ code: "C656FFP",
+ });
+});
+
+test("E52 NBU A", () => {
+ expect(
+ parse(
+ `rulebook page 3 #E52 NBU A Share Get Criteria and Verification cards in the box. a 1721 499 b 2214 296 c 2319 237 d 210 378 e 1815 594 Back t`
+ )
+ ).toStrictEqual({
+ ind: [17, 22, 23, 2, 18],
+ fake: [21, 14, 19, 10, 15],
+ crypt: [499, 296, 237, 378, 594],
+ color: 2,
+ m: 1,
+ code: "E52NBUA",
+ });
+});
+
+//
+
+test("H5K M7S", () => {
+ expect(
+ parse(
+ `Nightmare Mode: see rulebook page 3 #H5K M7S Share Get Criteria and Verification cards in the box. 212171922 A 485 B 537 C 315 D 413 E 614 Back to Homepage`
+ )
+ ).toStrictEqual({
+ ind: [2, 12, 17, 19, 22],
+ crypt: [485, 537, 315, 413, 614],
+ color: 0,
+ m: 2,
+ code: "H5KM7S",
+ });
+});
diff --git a/frontend/src/parsing/problem-book.ts b/frontend/src/parsing/problem-book.ts
new file mode 100644
index 0000000..1eec2da
--- /dev/null
+++ b/frontend/src/parsing/problem-book.ts
@@ -0,0 +1,70 @@
+import { parseCode, getColor, type ParsedGame } from "./util";
+
+export function parse(text: string): ParsedGame | null {
+ const problem = text.split("#")[1];
+ if (!problem) {
+ return null;
+ }
+ const hashEnd = problem.indexOf(",");
+ const code = problem.slice(0, hashEnd).replace(/\s+/g, "");
+ const parsedCode = parseCode(code);
+ if (parsedCode === null) {
+ return null;
+ }
+ const { mode, numVerifiers } = parsedCode;
+ const cardText = problem.split("->,")[1];
+ if (!cardText) {
+ return null;
+ }
+ const cards = cardText.split(",");
+ if (mode === 0) {
+ // CLASSIC
+ const ind = [];
+ for (let i = 0; i < numVerifiers; i += 1) {
+ ind.push(Number(cards[i]));
+ }
+ const crypt = [];
+ for (let i = numVerifiers; i < numVerifiers * 2; i += 1) {
+ crypt.push(Number(cards[i].slice(1)));
+ }
+ const color = getColor(crypt[0]);
+ if (color === null) {
+ return null;
+ }
+ return { ind, m: mode, crypt, code, color };
+ } else if (mode === 1) {
+ // EXTREME
+ const ind = [];
+ const fake = [];
+ for (let i = 0; i < numVerifiers * 2; i += 2) {
+ ind.push(Number(cards[i]));
+ fake.push(Number(cards[i + 1]));
+ }
+ const crypt = [];
+ for (let i = numVerifiers * 2; i < numVerifiers * 3; i += 1) {
+ crypt.push(Number(cards[i].slice(1)));
+ }
+ const color = getColor(crypt[0]);
+ if (color === null) {
+ return null;
+ }
+ return { ind, fake, m: mode, crypt, code, color };
+ } else if (mode === 2) {
+ // NIGHTMARE
+ const ind = [];
+ for (let i = 0; i < numVerifiers; i += 1) {
+ ind.push(Number(cards[i].slice(1)));
+ }
+ const crypt = [];
+ for (let i = numVerifiers; i < numVerifiers * 2; i += 1) {
+ crypt.push(Number(cards[i].slice(1)));
+ }
+ const color = getColor(crypt[0]);
+ if (color === null) {
+ return null;
+ }
+ return { ind, m: mode, crypt, code, color };
+ } else {
+ return null;
+ }
+}
diff --git a/frontend/src/parsing/turing-copy-paste.ts b/frontend/src/parsing/turing-copy-paste.ts
new file mode 100644
index 0000000..4205fe3
--- /dev/null
+++ b/frontend/src/parsing/turing-copy-paste.ts
@@ -0,0 +1,144 @@
+import { range, getColor, parseCode, type ParsedGame } from "./util";
+
+function getCode(parts: string[]) {
+ let start = 0;
+ for (let i = 0; i < parts.length; i += 1) {
+ if (parts[i].startsWith("#")) {
+ start = i;
+ } else if (parts[i].toLowerCase().includes("share")) {
+ return parts.slice(start, i);
+ }
+ }
+ return null;
+}
+
+function classicRegex(verifier: number) {
+ return String.fromCharCode(65 + verifier) + "(\\d{1,2})(\\d{3})";
+}
+
+function parseClassicMatch(match: string[]) {
+ const ind = [];
+ const crypt = [];
+ for (let i = 0; i < match.length; i += 1) {
+ if (i % 2 == 0) {
+ ind.push(Number(match[i]));
+ } else {
+ crypt.push(Number(match[i]));
+ }
+ }
+ return { ind, crypt };
+}
+
+// a 1721 499 b 2214 296 c 2319 237 d 210 378 e 1815 594 B
+function extremeRegex(verifier: number) {
+ return (
+ String.fromCharCode(65 + verifier) +
+ "((\\d)(\\d)|(\\d)(\\d{2})|(\\d{2})(\\d{2}))(\\d{3})(?![0-9])"
+ );
+}
+
+function parseExtremeMatch(match: string[]) {
+ const ind: number[] = [];
+ const fake: number[] = [];
+ const crypt: number[] = [];
+ const validMatches = match.filter((el) => el !== undefined);
+ for (let i = 0; i < validMatches.length; i += 1) {
+ if (i % 4 === 1) {
+ ind.push(Number(validMatches[i]));
+ } else if (i % 4 === 2) {
+ fake.push(Number(validMatches[i]));
+ } else if (i % 4 === 3) {
+ crypt.push(Number(validMatches[i]));
+ }
+ }
+ return { ind, fake, crypt };
+}
+
+function nightmareCryptRegex(verifier: number) {
+ return String.fromCharCode(65 + verifier) + "(\\d{3})";
+}
+
+function parseNightmareInd(text: string, numVerifiers: number) {
+ const indText = text.match(
+ new RegExp(`\\d{${numVerifiers},${numVerifiers * 2}}`)
+ );
+ if (!indText) {
+ return null;
+ }
+ const indTextMatch = indText[0];
+ const numSingleDigit = numVerifiers * 2 - indTextMatch.length;
+ const ind: number[] = [];
+ let currentChar = 0;
+ for (; currentChar < numSingleDigit; currentChar += 1) {
+ ind.push(Number(indTextMatch.slice(currentChar, currentChar + 1)));
+ }
+ for (; currentChar < indTextMatch.length; currentChar += 2) {
+ ind.push(Number(indTextMatch.slice(currentChar, currentChar + 2)));
+ }
+ return ind;
+}
+
+export function parse(text: string): ParsedGame | null {
+ const parts = text.replaceAll(/\s+/g, " ").trim().split(" ");
+ const codeParts = getCode(parts);
+ if (codeParts === null) {
+ return null;
+ }
+ const code = codeParts.join("").slice(1).toUpperCase();
+ const parsedCode = parseCode(code);
+ if (parsedCode === null) {
+ return null;
+ }
+ const { mode, numVerifiers } = parsedCode;
+
+ const textWithoutSpaces = text.replaceAll(/\s+/g, "").toUpperCase();
+ if (mode === 0) {
+ // CLASSIC
+ const match = textWithoutSpaces.match(
+ new RegExp(range(0, numVerifiers).map(classicRegex).join(""))
+ );
+ if (!match) {
+ return null;
+ }
+ const { ind, crypt } = parseClassicMatch(match.slice(1));
+ const color = getColor(crypt[0]);
+ if (color === null) {
+ return null;
+ }
+ return { ind, crypt, color, m: mode, code };
+ } else if (mode === 1) {
+ // EXTREME
+ const match = textWithoutSpaces.match(
+ new RegExp(range(0, numVerifiers).map(extremeRegex).join(""))
+ );
+ if (!match) {
+ return null;
+ }
+ const { ind, fake, crypt } = parseExtremeMatch(match.slice(1));
+ const color = getColor(crypt[0]);
+ if (color === null) {
+ return null;
+ }
+ return { ind, fake, crypt, color, m: mode, code };
+ } else if (mode === 2) {
+ // NIGHTMARE
+ const cryptMatch = textWithoutSpaces.match(
+ new RegExp(range(0, numVerifiers).map(nightmareCryptRegex).join(""))
+ );
+ if (!cryptMatch) {
+ return null;
+ }
+ const crypt = cryptMatch.slice(1).map(Number);
+ const color = getColor(crypt[0]);
+ if (color === null) {
+ return null;
+ }
+ const ind = parseNightmareInd(textWithoutSpaces, numVerifiers);
+ if (ind === null) {
+ return null;
+ }
+ return { ind, color, m: mode, crypt, code };
+ } else {
+ return null;
+ }
+}
diff --git a/frontend/src/parsing/util.ts b/frontend/src/parsing/util.ts
new file mode 100644
index 0000000..9bab548
--- /dev/null
+++ b/frontend/src/parsing/util.ts
@@ -0,0 +1,86 @@
+import type { GameSetup } from "store/slices/commentsSlice";
+
+export type ParsedGame = {
+ code: string;
+} & GameSetup;
+
+// Adapted this from: https://github.com/manurFR/turingmachine/blob/25027290422fe858d99d17801d3a644b467ce732/checkcards.py
+export const CHECK_CARDS = [
+ [
+ 201, 206, 215, 220, 227, 233, 244, 253, 261, 267, 274, 280, 286, 293, 302,
+ 309, 315, 322, 329, 334, 339, 346, 350, 356, 360, 370, 376, 381, 387, 392,
+ 396, 403, 407, 413, 419, 429, 434, 440, 447, 455, 462, 470, 475, 481, 485,
+ 491, 497, 503, 507, 516, 525, 532, 537, 546, 551, 560, 566, 572, 578, 582,
+ 588, 592, 596, 604, 609, 614, 618, 628, 632, 636, 640, 646, 650, 654, 661,
+ 665, 670, 681, 688, 695, 701, 709, 717, 723, 737, 741, 746, 751, 758, 766,
+ 771, 778, 782, 787, 795,
+ ],
+ [
+ 205, 213, 219, 224, 232, 243, 252, 257, 266, 273, 279, 289, 299, 308, 314,
+ 319, 327, 332, 338, 344, 349, 355, 359, 369, 374, 379, 386, 391, 395, 402,
+ 406, 412, 418, 424, 433, 439, 445, 454, 461, 469, 474, 480, 484, 490, 496,
+ 502, 506, 515, 523, 530, 536, 543, 550, 558, 564, 571, 577, 581, 587, 591,
+ 595, 599, 608, 613, 617, 627, 631, 635, 639, 645, 649, 653, 658, 664, 669,
+ 680, 687, 694, 699, 708, 715, 720, 729, 736, 740, 744, 750, 757, 765, 770,
+ 776, 781, 786, 793, 798,
+ ],
+ [
+ 204, 212, 217, 223, 231, 237, 251, 256, 264, 270, 278, 282, 288, 296, 304,
+ 312, 317, 325, 331, 337, 341, 348, 353, 358, 365, 373, 378, 385, 390, 394,
+ 401, 405, 410, 416, 423, 432, 437, 442, 453, 459, 464, 472, 479, 483, 487,
+ 495, 499, 505, 514, 520, 528, 534, 541, 549, 557, 563, 568, 576, 580, 586,
+ 590, 594, 598, 606, 611, 616, 625, 630, 634, 638, 643, 648, 652, 657, 663,
+ 668, 677, 686, 691, 697, 706, 714, 719, 726, 739, 743, 749, 755, 763, 769,
+ 775, 780, 785, 792, 797,
+ ],
+ [
+ 202, 207, 216, 221, 228, 236, 247, 255, 263, 268, 277, 287, 294, 303, 311,
+ 316, 324, 330, 335, 340, 347, 352, 357, 362, 372, 377, 382, 391, 393, 399,
+ 404, 409, 414, 421, 430, 435, 441, 449, 458, 463, 471, 476, 482, 486, 492,
+ 498, 504, 509, 518, 527, 533, 540, 547, 553, 562, 567, 573, 579, 585, 589,
+ 593, 597, 605, 610, 615, 621, 629, 633, 637, 641, 647, 651, 656, 662, 667,
+ 673, 684, 690, 696, 704, 710, 718, 725, 733, 738, 742, 747, 754, 759, 767,
+ 773, 779, 783, 790, 796,
+ ],
+];
+
+export function getColor(checkCard: number) {
+ for (let i of range(0, 4)) {
+ if (CHECK_CARDS[i].includes(checkCard)) {
+ return i;
+ }
+ }
+ return null;
+}
+
+export function range(start: number, end: number): number[] {
+ const result = [];
+ for (let i = start; i < end; i += 1) {
+ result.push(i);
+ }
+ return result;
+}
+
+const MODE_MAP: { [key: string]: number } = {
+ A: 0,
+ B: 0,
+ C: 0,
+ D: 1,
+ E: 1,
+ F: 1,
+ G: 2,
+ H: 2,
+ I: 2,
+};
+
+export function parseCode(code: string) {
+ if (code.length < 2) {
+ return null;
+ }
+ const mode = MODE_MAP[code[0]];
+ if (mode === undefined) {
+ return null;
+ }
+ const numVerifiers = Number(code[1]);
+ return { mode, numVerifiers };
+}
diff --git a/frontend/src/store/slices/commentsSlice.ts b/frontend/src/store/slices/commentsSlice.ts
index 7411516..3337188 100644
--- a/frontend/src/store/slices/commentsSlice.ts
+++ b/frontend/src/store/slices/commentsSlice.ts
@@ -1,5 +1,9 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
-import { CriteriaCard, criteriaCardPool, CryptCard } from "hooks/useCriteriaCard";
+import {
+ CriteriaCard,
+ criteriaCardPool,
+ CryptCard,
+} from "hooks/useCriteriaCard";
const verifiers: Verifier[] = ["A", "B", "C", "D", "E", "F"];
@@ -36,6 +40,13 @@ const createLetters = (
};
export type CommentsState = Comment[];
+export type GameSetup = {
+ ind: number[];
+ crypt: number[];
+ color: number;
+ fake?: number[];
+ m: number;
+};
const initialState: CommentsState = [];
@@ -45,24 +56,18 @@ export const commentsSlice = createSlice({
reducers: {
load: (_, action: PayloadAction) => action.payload,
reset: () => initialState,
- setCards: (
- state,
- action: PayloadAction<{
- ind: number[];
- crypt: number[];
- color: number;
- fake?: number[];
- m?: number;
- }>
- ) => {
+ setCards: (state, action: PayloadAction) => {
const { ind, crypt, color, fake, m } = action.payload;
const nightmare = m === 2;
- const addAdditionalCardAttributes = (card: number, cryptNumber: number) => {
+ const addAdditionalCardAttributes = (
+ card: number,
+ cryptNumber: number
+ ) => {
return {
...criteriaCardPool.find((cc) => cc.id === card)!,
nightmare,
- cryptCard: {id: cryptNumber, color: color} as CryptCard,
+ cryptCard: { id: cryptNumber, color: color } as CryptCard,
};
};
diff --git a/frontend/src/views/Registration.tsx b/frontend/src/views/Registration.tsx
index 21e301c..792f4e5 100644
--- a/frontend/src/views/Registration.tsx
+++ b/frontend/src/views/Registration.tsx
@@ -13,11 +13,12 @@ import { registrationActions } from "store/slices/registrationSlice";
import HashCodeRegistration from "components/HashCodeRegistration";
import ManualRegistration from "components/ManualRegistration";
import { Card } from "@mui/material";
+import PasteRegistration from "components/PasteRegistration";
const Registration: FC = () => {
const dispatch = useAppDispatch();
const registration = useAppSelector((state) => state.registration);
- const [registrationMethod, setRegristationMethod] = useState("manual");
+ const [registrationMethod, setRegristationMethod] = useState("paste");
function changeRegistrationMethod(e: React.ChangeEvent) {
setRegristationMethod((e.target as HTMLInputElement).value);
}
@@ -59,6 +60,7 @@ const Registration: FC = () => {
control={}
label="Manual"
/>
+ } label="Paste" />
}
@@ -75,6 +77,7 @@ const Registration: FC = () => {
)}
+ {registrationMethod === "paste" && }
);
};