From 744935b18ec9a5d67a96804cbb2c6cc49b85fac3 Mon Sep 17 00:00:00 2001 From: mstiMarcel Date: Fri, 18 Dec 2020 14:00:15 +0100 Subject: [PATCH] Started work on matrix generation --- package-lock.json | 146 +++++++++-------- package.json | 2 + src/game/MatrixGeneration.test.ts | 24 +++ src/game/MatrixGeneration.ts | 252 ++++++++++++++++++++++++++++++ 4 files changed, 358 insertions(+), 66 deletions(-) create mode 100644 src/game/MatrixGeneration.test.ts create mode 100644 src/game/MatrixGeneration.ts diff --git a/package-lock.json b/package-lock.json index 4291019..20fe0b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1233,6 +1233,11 @@ "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", "dev": true }, + "@types/seedrandom": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.28.tgz", + "integrity": "sha512-SMA+fUwULwK7sd/ZJicUztiPs8F1yCPwF3O23Z9uQ32ME5Ha0NmDK9+QTsYE4O2tHXChzXomSWWeIhCnoN1LqA==" + }, "@types/serve-static": { "version": "1.13.8", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.8.tgz", @@ -1767,26 +1772,6 @@ } } }, - "fork-ts-checker-webpack-plugin-v5": { - "version": "npm:fork-ts-checker-webpack-plugin@5.2.1", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.1.tgz", - "integrity": "sha512-SVi+ZAQOGbtAsUWrZvGzz38ga2YqjWvca1pXQFUArIVXqli0lLoDQ8uS0wg0kSpcwpZmaW5jVCZXQebkyUQSsw==", - "dev": true, - "optional": true, - "requires": { - "@babel/code-frame": "^7.8.3", - "@types/json-schema": "^7.0.5", - "chalk": "^4.1.0", - "cosmiconfig": "^6.0.0", - "deepmerge": "^4.2.2", - "fs-extra": "^9.0.0", - "memfs": "^3.1.2", - "minimatch": "^3.0.4", - "schema-utils": "2.7.0", - "semver": "^7.3.2", - "tapable": "^1.0.0" - } - }, "glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", @@ -1888,18 +1873,6 @@ } } }, - "schema-utils": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", - "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", - "dev": true, - "optional": true, - "requires": { - "@types/json-schema": "^7.0.4", - "ajv": "^6.12.2", - "ajv-keywords": "^3.4.1" - } - }, "slash": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", @@ -2205,16 +2178,6 @@ } } }, - "json5": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", - "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", - "dev": true, - "optional": true, - "requires": { - "minimist": "^1.2.5" - } - }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -2224,18 +2187,6 @@ "graceful-fs": "^4.1.6" } }, - "loader-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", - "dev": true, - "optional": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -2391,18 +2342,6 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true }, - "vue-loader-v16": { - "version": "npm:vue-loader@16.1.1", - "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.1.tgz", - "integrity": "sha512-wz/+HFg/3SBayHWAlZXARcnDTl3VOChrfW9YnxvAweiuyKX/7IGx1ad/4yJHmwhgWlOVYMAbTiI7GV8G33PfGQ==", - "dev": true, - "optional": true, - "requires": { - "chalk": "^4.1.0", - "hash-sum": "^2.0.0", - "loader-utils": "^2.0.0" - } - }, "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -6935,6 +6874,40 @@ } } }, + "fork-ts-checker-webpack-plugin-v5": { + "version": "npm:fork-ts-checker-webpack-plugin@5.2.1", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.1.tgz", + "integrity": "sha512-SVi+ZAQOGbtAsUWrZvGzz38ga2YqjWvca1pXQFUArIVXqli0lLoDQ8uS0wg0kSpcwpZmaW5jVCZXQebkyUQSsw==", + "dev": true, + "optional": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@types/json-schema": "^7.0.5", + "chalk": "^4.1.0", + "cosmiconfig": "^6.0.0", + "deepmerge": "^4.2.2", + "fs-extra": "^9.0.0", + "memfs": "^3.1.2", + "minimatch": "^3.0.4", + "schema-utils": "2.7.0", + "semver": "^7.3.2", + "tapable": "^1.0.0" + }, + "dependencies": { + "schema-utils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", + "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", + "dev": true, + "optional": true, + "requires": { + "@types/json-schema": "^7.0.4", + "ajv": "^6.12.2", + "ajv-keywords": "^3.4.1" + } + } + } + }, "form-data": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", @@ -12925,6 +12898,11 @@ } } }, + "seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" + }, "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -15089,6 +15067,42 @@ } } }, + "vue-loader-v16": { + "version": "npm:vue-loader@16.1.2", + "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.2.tgz", + "integrity": "sha512-8QTxh+Fd+HB6fiL52iEVLKqE9N1JSlMXLR92Ijm6g8PZrwIxckgpqjPDWRP5TWxdiPaHR+alUWsnu1ShQOwt+Q==", + "dev": true, + "optional": true, + "requires": { + "chalk": "^4.1.0", + "hash-sum": "^2.0.0", + "loader-utils": "^2.0.0" + }, + "dependencies": { + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "optional": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "optional": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + } + } + }, "vue-style-loader": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz", diff --git a/package.json b/package.json index 8222ad1..70e3a7b 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "testURL": "http://localhost/" }, "dependencies": { + "@types/seedrandom": "^2.4.28", + "seedrandom": "^3.0.5", "vue": "^3.0.0" }, "devDependencies": { diff --git a/src/game/MatrixGeneration.test.ts b/src/game/MatrixGeneration.test.ts new file mode 100644 index 0000000..8024fe0 --- /dev/null +++ b/src/game/MatrixGeneration.test.ts @@ -0,0 +1,24 @@ +import "jest" +import seedrandom from "seedrandom"; +import {generateGameConfig} from "./MatrixGeneration"; +import GameConfiguration from "./GameConfiguration"; + +describe("MatrixGen", () => { + + test("random generator works", () => { + const seededRNG = seedrandom("Testseed"); + expect(seededRNG()).toEqual(0.026130760816951273); + expect(seededRNG()).toEqual(0.24232428305919648); + }); + + test("basic game config creation", () =>{ + const gameConfig : GameConfiguration = generateGameConfig(1, "Testseed"); + expect(gameConfig).not.toBeNull; + expect(gameConfig.matrix).not.toBeNull; + console.log(gameConfig.matrix); + expect(gameConfig.maxBufferLength).not.toBeNull; + expect(gameConfig.sequences).not.toBeNull; + console.log(gameConfig.sequences); + expect(gameConfig.timeout).not.toBeNull; + }); +}); diff --git a/src/game/MatrixGeneration.ts b/src/game/MatrixGeneration.ts new file mode 100644 index 0000000..b803e13 --- /dev/null +++ b/src/game/MatrixGeneration.ts @@ -0,0 +1,252 @@ +import GameConfiguration from "./GameConfiguration"; + +import seedrandom from "seedrandom"; + +const minMatrixSize = 3; +const maxMatrixSize = 8; + +const minNumberSequences = 2; +const maxNumberSequences = 7; +const minSequenceLength = 2; + +const minBufferSize = 4; +const maxBufferSize = 8; + +const stepsUntilMatrixSizeIncreased = 10; +const stepsUntilSequenceNumberIncreased = 3; + +const defaultTimeout = 60_000; + +//TODO see how many different values are there +//TODO see how the possible values are defined +const numberDifferentMatrixValues = 5; +const matrixValues = ["A0", "E9", "4C", "8B", "6F"]; +const emptyMatrixValue = " "; + +/** + * Generates a game configuration for the current level given the seed which is used + * for the PRNG. + * + * The generated game configuration, see {@link GameConfiguration}, contains + * a matrix with random values with a size which corresponds to the current level, + * the sequences which are generated randomly and have to be found in the matrix, + * the length of the buffer which contains the chosen values from the matrix, + * the timeout which is the time a user has to solve the current level. + * + * @param level The current game level (corresponds directly to the difficulty) + * @param seed The seed used to compute the random values of the marix and sequences + */ +export function generateGameConfig(level: number, seed: string): GameConfiguration { + if (level < 1) { + throw new RangeError("The level has to be at least 1."); + } + //the levels probably start at 1, however starting with 0 makes things smoother here + level--; + + const seededRNG = seedrandom(seed); + const matrixSize: number = getMatrixSize(level); + const numberOfSequences: number = getNumberOfSequences(level); + + const matrix: string[] = generateRandomMatrix(matrixSize, seededRNG); + const maxBufferLength: number = computeMaxBufferLength(level); + const sequences: string[][] = generateSequences(matrix, numberOfSequences, maxBufferLength, seededRNG); + const result: GameConfiguration = { + matrix: matrix, + sequences: sequences, + maxBufferLength: maxBufferLength, + timeout: defaultTimeout + }; + + return result; +} + +function computeMaxBufferLength(level: number): number { + //TODO not sure what might be a good idea + //see getNumberOfSequences: every time the number of sequences is increased, + //the buffer size is set to the minimum + //if the number of sequences is not increased, the buffer length is increased + let result: number = minBufferSize; + + if (matrixSizeWasIncreased(level)) { + //currently it was still possible to increase the matrix size every x level + const intervalLevel: number = level % stepsUntilMatrixSizeIncreased; + result += intervalLevel % stepsUntilSequenceNumberIncreased; + result = Math.min(result, maxBufferSize); + } + else { + //the matrix size could not be increased anymore, therefore the number of + //the maximum buffer length has to be changed differently + result = maxBufferSize; + } + + return result; +} + +function generateSequences(matrix: string[], numberOfSequences: number, maxBufferLength: number, seededRNG: seedrandom.prng): string[][] { + const result: string[][] = new Array(numberOfSequences); + for (let i = 0; i < numberOfSequences; i++) { + const currentSequenceLength: number = getRandomInt(minSequenceLength, maxBufferLength, seededRNG); + result[i] = new Array(currentSequenceLength); + fillSequence(result[i], matrix, seededRNG); + } + return result; +} + +function fillSequence(sequence: string[], matrix: string[], seededRNG: seedrandom.prng): void { + const matrixSize = Math.sqrt(matrix.length); + const matrixCopy: string[] = Object.assign([], matrix); + const sequenceLength = sequence.length; + + let currentLineNumber = 0; + let currentColumnNumber = 0; + let searchInLine = true; + //always start in the first line + for (let i = 0; i < sequenceLength; i++) { + let matrixIndex = -1; + if (searchInLine) { + while (matrixIndex == -1) { + currentColumnNumber = getRandomInt(0, matrixSize - 1, seededRNG); + matrixIndex = getMatrixIndex(matrixSize, currentLineNumber, currentColumnNumber); + if (matrixCopy[matrixIndex] == emptyMatrixValue) { + matrixIndex = -1; + } + + if (i < sequenceLength - 1 && numberValuesLeftColumn(matrixCopy, currentColumnNumber) < 2) { + //there needs to be 2 values left in the chosen column + //one for the current sequence value and one for the next + matrixIndex = -1; + } + } + searchInLine = false; + } + else { + while (matrixIndex == -1) { + currentLineNumber = getRandomInt(0, matrixSize - 1, seededRNG); + + matrixIndex = getMatrixIndex(matrixSize, currentLineNumber, currentColumnNumber); + if (matrixCopy[matrixIndex] == emptyMatrixValue) { + matrixIndex = -1; + } + + //ensure the chosen column has values left which can be selected + if (i < sequenceLength - 1 && numberValuesLeftLine(matrixCopy, currentLineNumber) < 2) { + //there needs to be 2 values left in the chosen line + //one for the current sequence value and one for the next + matrixIndex = -1; + } + + } + searchInLine = true; + } + + matrixIndex = getMatrixIndex(matrixSize, currentLineNumber, currentColumnNumber); + sequence[i] = matrix[matrixIndex]; + matrixCopy[matrixIndex] = emptyMatrixValue; + } +} + +function numberValuesLeftColumn(matrixCopy: string[], columnNumber: number): number { + const matrixSize = Math.sqrt(matrixCopy.length); + let valuesLeft = 0; + for (let i = 0; i < matrixSize; i++) { + const matrixIndex = getMatrixIndex(matrixSize, i, columnNumber); + if (matrixCopy[matrixIndex] != emptyMatrixValue) { + valuesLeft++; + } + } + + return valuesLeft; +} + +function numberValuesLeftLine(matrixCopy: string[], lineNumber: number): number { + const matrixSize = Math.sqrt(matrixCopy.length); + let valuesLeft = 0; + for (let i = 0; i < matrixSize; i++) { + const matrixIndex = getMatrixIndex(matrixSize, lineNumber, i); + if (matrixCopy[matrixIndex] != emptyMatrixValue) { + valuesLeft++; + } + } + + return valuesLeft; +} + +function getMatrixIndex(matrixSize: number, lineNumber: number, columnNumber: number): number { + return (lineNumber * matrixSize) + columnNumber; +} + +function generateRandomMatrix(matrixSize: number, seededRNG: seedrandom.prng): string[] { + const result: string[] = new Array(matrixSize * matrixSize); + + for (let i = 0; i < (matrixSize * matrixSize); i++) { + result[i] = randomValue(seededRNG); + } + + return result; +} + +/** + * Returns a random integer in the given interval. Both interval limits are inclusive + * + * @param min The minimum number to be returned (inclusive) + * @param max The maximum number to be returned (inclusive) + * @param seededRNG The PRNG given by seedrandom + */ +function getRandomInt(min: number, max: number, seededRNG: seedrandom.prng): number { + //taken from https://stackoverflow.com/questions/1527803/generating-random-whole-numbers-in-javascript-in-a-specific-range + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(seededRNG() * (max - min + 1)) + min; +} + +function randomValue(seededRNG: seedrandom.prng): string { + return matrixValues[getRandomInt(0, numberDifferentMatrixValues - 1, seededRNG)]; +} + +/** + * Increase the matrix size every x levels up to a maximum size + * + * x is defined in StepsUntilMatrixSizeIncreased + * + * @param level The current game level (corresponds directly to the difficulty) + */ +//TODO see if this is a clever way to increase the matrix size +function getMatrixSize(level: number): number { + const size: number = minMatrixSize + Math.floor(level / stepsUntilMatrixSizeIncreased); + return Math.min(size, maxMatrixSize); +} + +/** + * Computes the number of sequences for the given level. + * + * @param level The current game level (corresponds directly to the difficulty) + */ +//TODO It would suffice to give the level as param, as the matrix size can be computed +//What is the best practice in this context? +function getNumberOfSequences(level: number): number { + //TODO not sure what might be a good idea + let result: number = minNumberSequences; + + if (matrixSizeWasIncreased(level)) { + //currently it was still possible to increase the matrix size every x level + const intervalLevel: number = level % stepsUntilMatrixSizeIncreased; + result += Math.floor(intervalLevel / stepsUntilSequenceNumberIncreased); + result = Math.min(result, maxNumberSequences); + } + else { + //the matrix size could not be increased anymore, therefore the number of + //sequences has to be changed differently + result = maxNumberSequences; + } + + return result; +} + +/** + * Currently it was still possible to increase the matrix size every x level + * + * @param level The current game level (corresponds directly to the difficulty) + */ +function matrixSizeWasIncreased(level: number): boolean { + return level < maxMatrixSize * (stepsUntilMatrixSizeIncreased + 1); +}