nissy-core

The "engine" of nissy, including the H48 optimal solver.
git clone https://git.tronto.net/nissy-core
Download | Log | Files | Refs | README | LICENSE

commit 37a2ebd4bffa99d465396fde0acfc52284ee3b7c
parent dc47c318bd7272099ec8a66ddc48810f91fa52b9
Author: Sebastiano Tronto <sebastiano@tronto.net>
Date:   Fri, 30 May 2025 14:06:34 +0200

More improvements to web solver

Diffstat:
Msrc/solvers/h48/solve.h | 16++++++++--------
Mweb/adapter.cpp | 5+++--
Mweb/http/index.html | 77+++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mweb/http/nissyapp.mjs | 94++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mweb/http/worker.mjs | 28+++++++++++++++++++++++++---
5 files changed, 176 insertions(+), 44 deletions(-)

diff --git a/src/solvers/h48/solve.h b/src/solvers/h48/solve.h @@ -417,8 +417,8 @@ solve_h48( ) { int i, ntasks, eoesep_table_index; - bool td, fp; - _Atomic int status; + bool td; + _Atomic int status, prev_status; size_t lastused; int8_t d; dfsarg_solve_h48_t arg[THREADS]; @@ -556,7 +556,6 @@ solve_h48( /* Log solutions and handle pause / stop / resume */ if (poll_status != NULL && d >= 15 && NISSY_CANSLEEP) { td = false; - fp = true; while (!td && status != NISSY_STATUS_STOP) { msleep(BASE_SLEEP_TIME); @@ -565,13 +564,14 @@ solve_h48( lastused = sollist.used; pthread_mutex_unlock(&solutions_mutex); + prev_status = status; status = poll_status(poll_status_data); - if (status == NISSY_STATUS_PAUSE && fp) { - LOG("[H48 solve] Paused\n"); - fp = false; + if (status != prev_status) { + if (status == NISSY_STATUS_PAUSE) + LOG("[H48 solve] Paused\n"); + if (status == NISSY_STATUS_RUN) + LOG("[H48 solve] Resumed\n"); } - if (status == NISSY_STATUS_RUN) - fp = true; for (td = true, i = 0; i < threads; i++) td = td && arg[i].thread_done; diff --git a/web/adapter.cpp b/web/adapter.cpp @@ -145,7 +145,7 @@ bool init_solver_generate(const std::string& name) } else { log("Error storing the data (the solver is usable, " "but the data will have to be re-generated next " - "time you want to use it)"); + "time you want to use it)\n"); } return true; @@ -179,7 +179,7 @@ nissy::solver::solve_result solve(std::string name, } return loaded_solvers.at(name).solve(cube, nissflag, minmoves, - maxmoves, maxsols, optimal, threads, NULL, &poll_status_id); + maxmoves, maxsols, optimal, threads, poll_status, &poll_status_id); } EMSCRIPTEN_BINDINGS(Nissy) @@ -194,6 +194,7 @@ EMSCRIPTEN_BINDINGS(Nissy) emscripten::class_<nissy::error>("Error") .function("ok", &nissy::error::ok) + .property("value", &nissy::error::value) .class_property("unsolvableWarning", &nissy::error::UNSOLVABLE_WARNING) .class_property("unsolvableError", diff --git a/web/http/index.html b/web/http/index.html @@ -1,13 +1,16 @@ <!doctype html> <html lang="en-US"> - <head> - <meta charset="utf-8" /> - <meta name="viewport" content="width=device-width" /> - <title>Nissy - H48 solver POC</title> - <script type="module" src="./nissyapp.mjs"></script> - </head> - <body> - <input id="scrambleText" placeholder="Type the scramble here..."> +<head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width" /> + <title>Nissy - H48 solver POC</title> + <script type="module" src="./nissyapp.mjs"></script> +</head> + +<body> + + <div id="scrambleRaw"> + <input id="scrambleText" placeholder="Type the scramble here..." /> <select id="solverSelector"> <option value="h48h3k2" selected="selected"> h48 h=4 k=2 (300 Mb) - light @@ -17,22 +20,44 @@ </option> </select> <button id="solveButton">Solve</button> - <div id="confirmDownload" style="display:none"> - <p id="confirmDownloadText"> - For this solver to work, a large data table is required. - You can either download it or generate it locally. - </p> - <p id="confirmDownloadExtraText"> - In either case, this data will be stored locally for future use. You - can delete it at any time by clearing your browser's data. - </p> - <button id="confirmDownloadCancel">Cancel</button> - <button id="confirmDownloadConfirm">Download</button> - <button id="confirmDownloadGenerate">Generate locally</button> - </div> - <p id="resultsLabel"></p> - <p id="results"></p> - <button id="toggleShowLog">Show nissy log messages</button> - <p id="logPane" style="display:none"></p> - </body> + <button id="pauseResumeButton" disabled>Pause</button> + <button id="cancelSolveButton" disabled>Cancel</button> + </div> + + <div id="solverConfiguration"> + <label id="minLabel" for="minSlider">Minimum moves: 0</label><br /> + <input id="minMovesSlider" name="minSlider" + type="range" min="0" max="20" value="0" /> + <br /> + <label id="maxLabel" for="maxSlider">Maximum moves: 20</label><br /> + <input id="maxMovesSlider" name="maxSlider" + type="range" min="0" max="20" value="20" /> + <br /> + <label for="maxSolutions">Limit number of solutions to</label> + <input id="maxSolutions" name="maxSolutions" + type="number" min="1" max="999" value="1"> + </div> + + <div id="confirmDownload" style="display:none"> + <p id="confirmDownloadText"> + For this solver to work, a large data table is required. + You can either download it or generate it locally. + </p> + <p id="confirmDownloadExtraText"> + In either case, this data will be stored locally for future use. You + can delete it at any time by clearing your browser's data. + </p> + <button id="confirmDownloadCancel">Cancel</button> + <button id="confirmDownloadConfirm">Download</button> + <button id="confirmDownloadGenerate">Generate locally</button> + </div> + + + <p id="resultsLabel"></p> + <p id="results"></p> + <button id="toggleShowLog">Show nissy log messages</button> + <p id="logPane" style="display:none"></p> + +</body> + </html> diff --git a/web/http/nissyapp.mjs b/web/http/nissyapp.mjs @@ -3,16 +3,31 @@ // to this folder when running ./build web. var solveButton = document.getElementById("solveButton"); +var pauseResumeButton = document.getElementById("pauseResumeButton"); +var cancelSolveButton = document.getElementById("cancelSolveButton"); var scrField = document.getElementById("scrambleText"); var resultsLabel = document.getElementById("resultsLabel"); var resultsText = document.getElementById("results"); var solverSelector = document.getElementById("solverSelector"); var toggleLog = document.getElementById("toggleShowLog"); var logPane = document.getElementById("logPane"); +var minSlider = document.getElementById("minMovesSlider"); +var maxSlider = document.getElementById("maxMovesSlider"); +var minLabel = document.getElementById("minLabel"); +var maxLabel = document.getElementById("maxLabel"); +var maxSolsInput = document.getElementById("maxSolutions"); +var solveStatus = "run"; // For now this is global var lastCallbackId = 0; var callbacks = new Map(); // Values: { f: function, arg: object } var worker = new Worker("./worker.mjs", { type: "module" }); + +// Periodically send status updates to the worker + +const sendStatusUpdateToWorker = () => + worker.postMessage({command: "update status", id: -1, arg: solveStatus }); +setInterval(() => sendStatusUpdateToWorker(), 500); + worker.onmessage = (event) => { if (event.data.command == "log") { logPane.innerText += event.data.object; @@ -32,8 +47,48 @@ function updateResults(label, results, enable) { resultsText.innerText = results; solveButton.disabled = !enable; solverSelector.disabled = !enable; + minSlider.disabled = !enable; + maxSlider.disabled = !enable; + maxSolsInput.disabled = !enable; } +scrField.addEventListener("input", (e) => { + const scramble = scrField.value; + + const callbackId = ++lastCallbackId; + callbacks.set(callbackId, { + f: (callbackArg, validateResult) => { + if (validateResult) { + scrField.style.border = ""; + } else { + scrField.style.border = "2px solid red"; + } + }, + arg: scramble + }); + + worker.postMessage({ + command: "validate scramble", + id: callbackId, + arg: scramble + }); +}); + +minSlider.addEventListener("input", () => + minLabel.innerText = "Minimum moves: " + minSlider.value); + +maxSlider.addEventListener("input", () => + maxLabel.innerText = "Maximum moves: " + maxSlider.value); + +maxSolsInput.addEventListener("keyup", () => { + if (maxSolsInput.value != "") { + if (parseInt(maxSolsInput.value) < parseInt(maxSolsInput.min)) + maxSolsInput.value = maxSolsInput.min; + if (parseInt(maxSolsInput.value) > parseInt(maxSolsInput.max)) + maxSolsInput.value = maxSolsInput.max; + } +}); + var logVisible = false; toggleLog.addEventListener("click", () => { logVisible = !logVisible; @@ -67,7 +122,7 @@ solveButton.addEventListener("click", () => { worker.postMessage({ command: "validate scramble", - id: lastCallbackId, + id: callbackId, arg: scramble }); }); @@ -110,7 +165,10 @@ function askDownloadThenSolve(solver, scramble) { updateResults("", "", false); confirmDiv.style.display = "block"; - cancel.addEventListener("click", cleanup); + cancel.addEventListener("click", () => { + cleanup(); + updateResults("", "", true); + }); download.addEventListener("click", () => { cleanup(); @@ -153,12 +211,38 @@ function downloadOrGenerateThenSolve(solver, scramble, download) { }); } +pauseResumeButton.addEventListener("click", (e) => { + if (pauseResumeButton.innerText == "Pause") { + solveStatus = "pause"; + pauseResumeButton.innerText = "Resume"; + updateResults("Solver paused - click Resume to continue", "", false); + } else { + solveStatus = "run"; + pauseResumeButton.innerText = "Pause"; + updateResults("Solving...", "", false); + } +}); + +cancelSolveButton.addEventListener("click", (e) => { + solveStatus = "stop"; + pauseResumeButton.innerText = "Pause"; +}); + function startSolve(solver, scramble) { updateResults("Solving...", "", false); const callbackId = ++lastCallbackId; + pauseResumeButton.disabled = false; + cancelSolveButton.disabled = false; + solveStatus = "run"; + callbacks.set(callbackId, { f: (callbackArg, solveResult) => { + pauseResumeButton.disabled = true; + cancelSolveButton.disabled = true; + pauseResumeButton.innerText = "Pause"; + solveStatus = "stop"; + if (solveResult.success) { const n = solveResult.solutions.length; const label = n == 0 ? "No solution found" : @@ -178,9 +262,9 @@ function startSolve(solver, scramble) { arg: { solver: solver, scramble: scramble, - minmoves: 0, - maxmoves: 20, - maxsolutions: 1, + minmoves: minSlider.value, + maxmoves: maxSlider.value, + maxsolutions: maxSolsInput.value, optimal: 20, threads: window.navigator.hardwareConcurrency, } diff --git a/web/http/worker.mjs b/web/http/worker.mjs @@ -4,12 +4,19 @@ const nissy = await Nissy(); const log = (msg) => postMessage({ command: "log", id: -1, object: msg }); nissy.setLogger(nissy._addCallbackFunction(log)); +var solveStatus = nissy.statusRUN; // For now this is a global variable +const pollStatusCallback = nissy._addCallbackFunction(() => { +console.log("Calling pollstatus, returning " + solveStatus); +return solveStatus; +}); + const commands = [ { name: "load solver data", exec: loadSolverDataFromStorage }, { name: "download solver data", exec: downloadSolverData }, { name: "generate solver data", exec: generateSolverData }, { name: "validate scramble", exec: validateScramble }, { name: "solve", exec: solve }, + { name: "update status", exec: updateStatus }, ]; // Message structure: @@ -161,10 +168,11 @@ async function solve(id, arg) { return; } + solveStatus = nissy.statusRUN; + var cube = new nissy.Cube(); cube.move(arg.scramble); - // TODO: error handling here? const nissFlag = ((str) => { switch (str) { case "inverse": return nissy.NissFlag.inverse; @@ -176,15 +184,29 @@ async function solve(id, arg) { })(arg.nissFlag); const result = await nissy.solve(arg.solver, cube, nissFlag, arg.minmoves, - arg.maxmoves, arg.maxsolutions, arg.optimal, arg.threads, id); + arg.maxmoves, arg.maxsolutions, arg.optimal, arg.threads, + pollStatusCallback); if (result.err.ok()) { - //TODO: add solution lenght? var solutions = result.solutions == "" ? [] : result.solutions.split("\n"); solutions.pop(); // Solver always returns string ending in newline + for (var i = 0; i < solutions.length; i++) { + const movecount = await nissy.countMoves(solutions[i]); + solutions[i] += " (" + movecount.value + ")"; + } async_return(true, solutions); } else { async_return(false, [], "Error while solving (error " + result.err.value + ")"); } }; + +function updateStatus(id, arg) { + if (arg == "stop") { + solveStatus = nissy.statusSTOP; + } else if (arg == "pause") { + solveStatus = nissy.statusPAUSE; + } else { + solveStatus = nissy.statusRUN; + } +};