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:
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;
+ }
+};