From 4f891991e92db7996af565a2584c5c707d994da0 Mon Sep 17 00:00:00 2001 From: Alexander Zibert Date: Sun, 29 Oct 2023 21:26:30 +0100 Subject: [PATCH] implement basic wasm integration --- .gitignore | 3 +- Makefile | 2 + README.md | 11 ++++ cards.hpp | 16 +++++- frontend/index.html | 12 +++++ frontend/main.js | 98 +++++++++++++++++++++++++++++++++ main.cpp | 119 ---------------------------------------- solver.hpp | 128 +++++++++++++++++++++++++++++++++++++++++++- verifier.hpp | 1 - wasm.cpp | 59 ++++++++++++++++++++ 10 files changed, 326 insertions(+), 123 deletions(-) create mode 100644 frontend/index.html create mode 100644 frontend/main.js create mode 100644 wasm.cpp diff --git a/.gitignore b/.gitignore index 16b9d9c..731b4a0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /a.out /.vscode -/test.out \ No newline at end of file +/test.out +/build diff --git a/Makefile b/Makefile index 1bb9010..1ba508c 100644 --- a/Makefile +++ b/Makefile @@ -2,3 +2,5 @@ all: clang++ -std=c++2b -O2 -Wall main.cpp doctest: clang++ -std=c++2b -O2 -Wall test/*.cpp -o test.out +wasm: + em++ wasm.cpp -O2 -s WASM=1 -o build/wasmWrapper.js diff --git a/README.md b/README.md index 4f040ea..8d75145 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,19 @@ This is a solver for the board game: ## Building +### CLI Build + Just run `make` in the root directory. Then you can run the binary `./a.out`. +### WASM Build + +The WASM build requires a working [emscripten toolchain](https://emscripten.org/docs/getting_started/downloads.html#installation-instructions-using-the-emsdk-recommended). + +Afterwards, you can run `make wasm` to build. + +Next, you need to serve these files. You can run `python -m http.server 9000` in the root directory to serve the files with a simple python http server. +Then, the frontend is accessible under `http://localhost:9000/frontend/`. As of now there only is some output in the JS console. + ## Tests Run `make doctest` in the root directory. Then you can run the binary `./test.out`. diff --git a/cards.hpp b/cards.hpp index 6caf443..63c11a5 100644 --- a/cards.hpp +++ b/cards.hpp @@ -183,4 +183,18 @@ const auto c48 = card_t{blue_less_than_yellow, blue_equal_to_yellow, yellow_greater_than_purple}; // card XX -const auto v = verifier_t{"", [](const code_t &code) { return true; }}; \ No newline at end of file +const auto v = verifier_t{"", [](const code_t &code) { return true; }}; + +const auto all_cards = []() { + auto result = std::vector{49}; + result[2] = c2; + result[12] = c12; + result[16] = c16; + result[23] = c23; + result[24] = c24; + result[33] = c33; + result[35] = c35; + result[45] = c45; + result[48] = c48; + return result; +}(); diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..1ba4b0d --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + Turing Machine + + + + + + diff --git a/frontend/main.js b/frontend/main.js new file mode 100644 index 0000000..ff2a009 --- /dev/null +++ b/frontend/main.js @@ -0,0 +1,98 @@ +function main(Module) { + const { memory, solve_wasm } = Module.asm; + console.log(memory.buffer); + const numCards = 6; + const numQueries = 0; + const inputValues = [numCards, 2, 12, 24, 33, 35, 45, numQueries]; + let offset = 0; + const input = new Uint8Array(memory.buffer, offset, inputValues.length); + input.set(inputValues); + offset += input.byteLength; + + const output = new Uint8Array(memory.buffer, offset, 1000); + offset += output.byteLength; + + const start = new Date(); + solve_wasm(input.byteOffset, output.byteOffset); + console.log("Solving took", new Date() - start, "ms"); + offset = 0; + const numCodes = output[offset]; + offset += 1; + const codes = []; + for (let i = 0; i < numCodes; i += 1) { + codes.push( + String(output[offset]) + output[offset + 1] + output[offset + 2] + ); + offset += 3; + } + console.log(codes); + + const possibleVerifiers = []; + for (let i = 0; i < numCards; i += 1) { + const numVerifiers = output[offset]; + offset += 1; + possibleVerifiers.push([]); + for (let j = 0; j < numVerifiers; j += 1) { + const verifier = output[offset]; + offset += 1; + possibleVerifiers[i].push(verifier); + } + } + console.log(possibleVerifiers); + + const possibleLetters = []; + for (let i = 0; i < numCards; i += 1) { + const numLetters = output[offset]; + offset += 1; + possibleLetters.push([]); + for (let j = 0; j < numLetters; j += 1) { + const letter = output[offset]; + offset += 1; + possibleLetters[i].push(letter); + } + } + console.log(possibleLetters); +} + +window.Module = { + preRun: [], + postRun: [main], + print: (function () { + return function (text) { + console.log(text); + }; + })(), + setStatus: (text) => { + if (!Module.setStatus.last) + Module.setStatus.last = { time: Date.now(), text: "" }; + if (text === Module.setStatus.last.text) return; + var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/); + var now = Date.now(); + if (m && now - Module.setStatus.last.time < 30) return; // if this is a progress update, skip it if too soon + Module.setStatus.last.time = now; + Module.setStatus.last.text = text; + console.log(text); + }, + totalDependencies: 0, + monitorRunDependencies: (left) => { + this.totalDependencies = Math.max(this.totalDependencies, left); + Module.setStatus( + left + ? "Preparing... (" + + (this.totalDependencies - left) + + "/" + + this.totalDependencies + + ")" + : "All downloads complete." + ); + }, +}; +Module.setStatus("Downloading..."); +window.onerror = (event) => { + // TODO: do not warn on ok events like simulating an infinite loop or exitStatus + Module.setStatus("Exception thrown, see JavaScript console"); + spinnerElement.style.display = "none"; + Module.setStatus = (text) => { + if (text) console.error("[post-exception status] " + text); + }; +}; diff --git a/main.cpp b/main.cpp index 6b89632..1b142cf 100644 --- a/main.cpp +++ b/main.cpp @@ -9,125 +9,6 @@ #include "solver.hpp" #include "verifier.hpp" -struct query_t { - code_t code; - char verifierIdx; - bool result; -}; - -const auto initialize_verifier_sets = [](const std::vector &game, - bool nightmareMode) { - auto matchingVerifiers = std::vector>{game.size()}; - auto nightmareSet = std::vector{}; - for (size_t i = 0; i < game.size(); i += 1) { - nightmareSet.push_back('A' + i); - } - for (size_t i = 0; i < game.size(); i += 1) { - if (nightmareMode) { - matchingVerifiers[i] = nightmareSet; - } else { - matchingVerifiers[i].push_back('A' + i); - } - } - return matchingVerifiers; -}; - -struct result_t { - std::set possibleCodes; - std::vector> possibleVerifiers; - std::vector> possibleMatches; -}; - -const auto solve = [](const std::vector &game, - const std::vector &queries) { - // Step 1: Calculate all possible verifier combinations of the game that lead - // to a unique solution with no redundant verifiers - auto possibleCombinations = std::vector>{}; - auto consumer = [&possibleCombinations]( - const std::vector &verifierCombination) { - if (has_single_solution(verifierCombination) && - !has_redundant_information(verifierCombination)) { - possibleCombinations.push_back(verifierCombination); - } - }; - cartesianProduct(game, consumer); - - // Step 2: Use the queries to match verifier cards with machine slots ('A', - // 'B', 'C', ...). - - auto solverResult = result_t{}; - solverResult.possibleMatches.resize(game.size()); - solverResult.possibleVerifiers.resize(game.size()); - for (size_t i = 0; i < possibleCombinations.size(); i += 1) { - const auto &possibleCombination = possibleCombinations[i]; - auto possibleMatchings = initialize_verifier_sets(game, true); - // remove all matchings which give a different result - for (const auto &query : queries) { - for (size_t cardIdx = 0; cardIdx < possibleCombination.size(); - cardIdx += 1) { - const auto &verifier = possibleCombination[cardIdx]; - auto &possibleMatching = possibleMatchings[cardIdx]; - if (verifier.isValid(query.code) != query.result) { - possibleMatching.erase(std::remove(possibleMatching.begin(), - possibleMatching.end(), - query.verifierIdx), - possibleMatching.end()); - } - } - } - // go through all remaining combinations - auto possibleMatchingCombinations = std::vector>{}; - cartesianProduct(possibleMatchings, [&possibleMatchingCombinations]( - const std::vector &comb) { - // TODO: the performance can be improved if the the branch is not further - // followed if a duplicate is encountered, however then we cannot just - // reuse cartesianProduct - auto uniqueSet = std::vector{}; - uniqueSet.resize(comb.size(), false); - - auto count = 0; - for (const auto el : comb) { - if (!uniqueSet[el - 'A']) { - count += 1; - } - uniqueSet[el - 'A'] = true; - } - if (count != comb.size()) { - return; - } - possibleMatchingCombinations.push_back(comb); - }); - if (possibleMatchingCombinations.size() == 0) { - continue; - } - - // build result - const auto solution = get_solution(possibleCombination); - const auto solutionIdx = find_solution_idx(solution); - const auto code = human_codes[solutionIdx]; - solverResult.possibleCodes.insert(code); - - for (const auto &possibleMatchingCombination : - possibleMatchingCombinations) { - for (size_t i = 0; i < possibleMatchingCombination.size(); i += 1) { - solverResult.possibleMatches[i].insert(possibleMatchingCombination[i]); - } - } - - for (size_t cardIdx = 0; cardIdx < game.size(); cardIdx += 1) { - const auto &card = game[cardIdx]; - for (size_t verifierIdx = 0; verifierIdx < card.size(); - verifierIdx += 1) { - const auto &verifier = card[verifierIdx]; - if (verifier.name == possibleCombination[cardIdx].name) { - solverResult.possibleVerifiers[cardIdx].insert(verifierIdx); - } - } - } - } - return solverResult; -}; - int main() { const auto game = std::vector{c2, c12, c24, c33, c35, c45}; auto result = solve(game, {{{1, 2, 3}, 'A', true}, diff --git a/solver.hpp b/solver.hpp index 2b8faf7..76ff528 100644 --- a/solver.hpp +++ b/solver.hpp @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include + +#include "cards.hpp" +#include "cartesian.hpp" +#include "code.hpp" #include "verifier.hpp" #include @@ -43,4 +50,123 @@ const auto find_solution_idx = [](const code_mask_t &mask) { } } return mask.size() + 1; -}; \ No newline at end of file +}; + +const auto initialize_verifier_sets = [](const std::vector &game, + bool nightmareMode) { + auto matchingVerifiers = std::vector>{game.size()}; + auto nightmareSet = std::vector{}; + for (size_t i = 0; i < game.size(); i += 1) { + nightmareSet.push_back('A' + i); + } + for (size_t i = 0; i < game.size(); i += 1) { + if (nightmareMode) { + matchingVerifiers[i] = nightmareSet; + } else { + matchingVerifiers[i].push_back('A' + i); + } + } + return matchingVerifiers; +}; + +struct query_t { + code_t code; + char verifierIdx; + bool result; +}; + +struct result_t { + std::set possibleCodes; + std::vector> possibleVerifiers; + std::vector> possibleMatches; +}; + +const auto solve = [](const std::vector &game, + const std::vector &queries) { + // Step 1: Calculate all possible verifier combinations of the game that lead + // to a unique solution with no redundant verifiers + auto possibleCombinations = std::vector>{}; + auto consumer = [&possibleCombinations]( + const std::vector &verifierCombination) { + if (has_single_solution(verifierCombination) && + !has_redundant_information(verifierCombination)) { + possibleCombinations.push_back(verifierCombination); + } + }; + cartesianProduct(game, consumer); + + // Step 2: Use the queries to match verifier cards with machine slots ('A', + // 'B', 'C', ...). + + auto solverResult = result_t{}; + solverResult.possibleMatches.resize(game.size()); + solverResult.possibleVerifiers.resize(game.size()); + for (size_t i = 0; i < possibleCombinations.size(); i += 1) { + const auto &possibleCombination = possibleCombinations[i]; + auto possibleMatchings = initialize_verifier_sets(game, true); + // remove all matchings which give a different result + for (const auto &query : queries) { + for (size_t cardIdx = 0; cardIdx < possibleCombination.size(); + cardIdx += 1) { + const auto &verifier = possibleCombination[cardIdx]; + auto &possibleMatching = possibleMatchings[cardIdx]; + if (verifier.isValid(query.code) != query.result) { + possibleMatching.erase(std::remove(possibleMatching.begin(), + possibleMatching.end(), + query.verifierIdx), + possibleMatching.end()); + } + } + } + // go through all remaining combinations + auto possibleMatchingCombinations = std::vector>{}; + cartesianProduct(possibleMatchings, [&possibleMatchingCombinations]( + const std::vector &comb) { + // TODO: the performance can be improved if the the branch is not further + // followed if a duplicate is encountered, however then we cannot just + // reuse cartesianProduct + auto uniqueSet = std::vector{}; + uniqueSet.resize(comb.size(), false); + + auto count = 0; + for (const auto el : comb) { + if (!uniqueSet[el - 'A']) { + count += 1; + } + uniqueSet[el - 'A'] = true; + } + if (count != comb.size()) { + return; + } + possibleMatchingCombinations.push_back(comb); + }); + if (possibleMatchingCombinations.size() == 0) { + continue; + } + + // build result + const auto solution = get_solution(possibleCombination); + const auto solutionIdx = find_solution_idx(solution); + const auto code = human_codes[solutionIdx]; + solverResult.possibleCodes.insert(code); + + for (const auto &possibleMatchingCombination : + possibleMatchingCombinations) { + for (size_t i = 0; i < possibleMatchingCombination.size(); i += 1) { + solverResult.possibleMatches[i].insert(possibleMatchingCombination[i]); + } + } + + for (size_t cardIdx = 0; cardIdx < game.size(); cardIdx += 1) { + const auto &card = game[cardIdx]; + for (size_t verifierIdx = 0; verifierIdx < card.size(); + verifierIdx += 1) { + const auto &verifier = card[verifierIdx]; + if (verifier.name == possibleCombination[cardIdx].name) { + solverResult.possibleVerifiers[cardIdx].insert(verifierIdx); + } + } + } + } + return solverResult; +}; diff --git a/verifier.hpp b/verifier.hpp index a65343c..dd26c5b 100644 --- a/verifier.hpp +++ b/verifier.hpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include "code.hpp" diff --git a/wasm.cpp b/wasm.cpp new file mode 100644 index 0000000..ee2a954 --- /dev/null +++ b/wasm.cpp @@ -0,0 +1,59 @@ +#include "solver.hpp" +#include +#include + +extern "C" EMSCRIPTEN_KEEPALIVE void solve_wasm(uint8_t *input, + uint8_t *output) { + // input parsing + size_t offset = 0; + auto numCards = input[offset]; + offset += 1; + auto cards = std::vector{numCards}; + for (uint8_t i = 0; i < numCards; i += 1) { + cards[i] = all_cards[input[offset]]; + offset += 1; + } + + auto numQueries = input[offset]; + offset += 1; + + auto queries = std::vector{numQueries}; + for (size_t i = 0; i < numQueries; numQueries += 1) { + queries.push_back({{input[offset], input[offset + 1], input[offset + 2]}, + (char)input[offset + 3], + (bool)input[offset + 4]}); + offset += 5; + } + + auto result = solve(cards, queries); + offset = 0; + // transform result + + // 1. N_CODES,[codes] + output[offset] = (uint8_t)result.possibleCodes.size(); + offset += 1; + for (const auto &code : result.possibleCodes) { + for (const auto &c : code) { + output[offset] = c - '0'; + offset += 1; + } + } + // 2. [(N_CARDS, [cards])] + for (const auto &card : result.possibleVerifiers) { + output[offset] = (uint8_t)card.size(); + offset += 1; + for (const auto &verifierIdx : card) { + output[offset] = verifierIdx; + offset += 1; + } + } + // 3. [(N_LETTERS, [letters])] + for (const auto &possibleMatch : result.possibleMatches) { + output[offset] = (uint8_t)possibleMatch.size(); + offset += 1; + for (auto letter : possibleMatch) { + output[offset] = letter - 'A'; + offset += 1; + } + } +}