h48

A prototype for an optimal Rubik's cube solver, work in progress.
git clone https://git.tronto.net/h48
Download | Log | Files | Refs | README | LICENSE

commit 9589072687726dd51776b0e952625ae1996f4721
parent dc5e8188a7e29909cbdf661e9b2a80700fe0491d
Author: Sebastiano Tronto <sebastiano@tronto.net>
Date:   Wed, 16 Apr 2025 09:56:55 +0200

Added rudimentary QT UI

Diffstat:
M.gitignore | 3++-
MMakefile | 10++++++++--
Aqt/CMakeLists.txt | 27+++++++++++++++++++++++++++
Aqt/Makefile | 15+++++++++++++++
Aqt/NissyMain.qml | 261+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aqt/adapter.cpp | 140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aqt/adapter.h | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Aqt/main.cpp | 16++++++++++++++++
8 files changed, 521 insertions(+), 3 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -9,11 +9,12 @@ utils/.DS_Store perf.data perf.data.old run +nissyqt +qt/build debugrun shell/lasttest.out shell/lasttest.err tables/* -tables-old*/* test/*/runtest test/.DS_Store test/run diff --git a/Makefile b/Makefile @@ -18,7 +18,7 @@ debugnissy.o: ${CC} ${MACROS} ${DBGFLAGS} -c -o debugnissy.o src/nissy.c clean: - rm -rf *.out *.o *.s *.so run debugrun + rm -rf *.out *.o *.s *.so run debugrun nissyqt qt/build test: debugnissy.o CC="${CC} ${MACROS} ${DBGFLAGS}" OBJ=debugnissy.o ./test/test.sh @@ -46,4 +46,10 @@ python: nissy.o ${CC} ${CFLAGS} -shared ${PYTHON3_INCLUDES} -o nissy_python_module.so \ nissy.o python/nissy_module.c -.PHONY: all clean test tool debugtool shell debugshell shelltest python +qt: nissy.o + mkdir -p qt/build + cd qt && cmake . -B build + cd qt/build && make + cp qt/build/appnissyqt ./nissyqt + +.PHONY: all clean test tool debugtool shell debugshell shelltest python qt diff --git a/qt/CMakeLists.txt b/qt/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.16) + +project(nissyqt VERSION 0.1 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_STANDARD 20) + +find_package(Qt6 REQUIRED COMPONENTS Quick Concurrent) + +qt_standard_project_setup(REQUIRES 6.5) + +qt_add_executable(appnissyqt + main.cpp + adapter.cpp adapter.h + ../cpp/nissy.h ../cpp/nissy.cpp + ../nissy.o +) + +qt_add_qml_module(appnissyqt + URI nissyqt + VERSION 1.0 + QML_FILES NissyMain.qml +) + +target_link_libraries(appnissyqt + PRIVATE Qt6::Quick +) diff --git a/qt/Makefile b/qt/Makefile @@ -0,0 +1,15 @@ +all: nissyqt + +nissyqt: + mkdir -p build + cmake . -B build + cd build && make + cp build/appnissyqt ./run + +run: + QT_LOGGING_RULES="*.debug=true; qt.*.debug=false" ./run + +clean: + rm -rf run build + +.PHONY: all nissyqt run clean diff --git a/qt/NissyMain.qml b/qt/NissyMain.qml @@ -0,0 +1,261 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Window { + id: mainWindow + + width: 800 + height: 600 + visible: true + title: "Nissy 3.0 - Preview" + + SplitView { + id: splitView + anchors.fill: parent + orientation: Qt.Vertical + + handle: Rectangle { + implicitHeight: 3 + color: SplitHandle.hovered ? "black" : "#AAAAAA" + } + + component MyScrollBar: ScrollBar { + orientation: Qt.Vertical + size: parent.height + policy: ScrollBar.AlwaysOn + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + contentItem: Rectangle { + implicitWidth: 4 + radius: implicitWidth/2 + color: "black" + } + background: Rectangle { + implicitWidth: 4 + radius: implicitWidth/2 + color: "#AAAAAA" + } + } + + ColumnLayout { + id: mainArea + + property alias scramble: scrambleRow.scramble + property alias minmoves: solverCfg.minmoves + property alias maxmoves: solverCfg.maxmoves + property alias maxsolutions: solverCfg.maxsolutions + property alias sols: sols.text + property alias solsHeader: solsHeader.text + + property bool solutionsLoading: false + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: logView.top + anchors.margins: 6 + spacing: 10 + + SplitView.minimumHeight: 180 + SplitView.preferredHeight: 300 + + component Separator: Rectangle { + height: 1 + Layout.fillWidth: true + color: "black" + } + + ColumnLayout { + id: scrambleRow + + property alias scramble: scrambleRowLayout.scramble + + RowLayout { + id: scrambleRowLayout + + property alias scramble: scrambleEditor.text + + spacing: 6 + + TextField { + id: scrambleEditor + + placeholderText: "Enter scramble here" + Layout.fillWidth: true + padding: 4 + + readonly property bool empty: text.trim().length == 0 + readonly property bool valid: NissyAdapter.isValidScramble(text) + + onAccepted: if (!empty && valid) submitScramble() + } + + Button { + id: solveButton + + enabled: !scrambleEditor.empty && scrambleEditor.valid && + !mainArea.solutionsLoading + text: "Solve!" + + onPressed: submitScramble() + } + } + + Label { + id: invalidScrambleWarning + text: scrambleEditor.empty || scrambleEditor.valid ? + "" : "Invalid Scramble" + } + } + + Separator {} + + ColumnLayout { + id: solverCfg + property alias minmoves: minMaxRow.min + property alias maxmoves: minMaxRow.max + property alias maxsolutions: maxOptimal.maxsolutions + + RowLayout { + id: minMaxRow + + property alias min: slider.min + property alias max: slider.max + + Rectangle { + width: 100 + height: 20 + Label { text: "Min moves: " + slider.min } + } + RangeSlider { + id: slider + from: 0 + to: 20 + first.value: from + second.value: to + stepSize: 1 + snapMode: RangeSlider.SnapAlways + + readonly property int min: Math.round(first.value) + readonly property int max: Math.round(second.value) + } + Rectangle { + width: 100 + height: 20 + Label { text: "Max moves: " + slider.max } + } + } + + RowLayout { + id: maxOptimal + + property alias maxsolutions: maxSolsRect.maxsolutions + + Rectangle { + width: 220 + height: 20 + Label { text: "Maximum number of solutions:" } + } + Rectangle { + id: maxSolsRect + + property int maxsolutions: parseInt(textField.text) + + width: 35 + height: 20 + + TextField { + id: textField + + width: parent.width + text: "1" + validator: IntValidator{ bottom: 1; top: 999; } + + onAccepted: submitScramble() + } + } + } + } + + Separator {} + + Label { id: solsHeader } + + ScrollView { + Layout.fillHeight: true + Layout.fillWidth: true + Layout.bottomMargin: 10 + ScrollBar.vertical: MyScrollBar {} + + TextEdit { + id: sols + readOnly: true + font.family: "Monospace" + } + } + } + + ScrollView { + id: logView + + property alias text: logText.text + + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: 6 + + SplitView.preferredHeight: 300 + + background: Rectangle { + color: "#404040" + radius: 4 + } + + ScrollBar.vertical: MyScrollBar { + id: scrollBar + position: 1.0 - size + } + Label { + id: logText + + font.family: "Monospace" + color: "white" + } + } + } + + function submitScramble() { + mainArea.solutionsLoading = true; + mainArea.solsHeader = "Loading solutions..." + mainArea.sols = "" + logView.text = "" + NissyAdapter.requestSolve( + mainArea.scramble, + "h48h3k2", + mainArea.minmoves, + mainArea.maxmoves, + mainArea.maxsolutions, + -1 + ) + } + + Connections { + target: NissyAdapter + function onSolutionsReady(header, sols) { + mainArea.solutionsLoading = false + mainArea.solsHeader = header + mainArea.sols = sols + } + function onSolverError(msg) { + mainArea.solutionsLoading = false + mainArea.solusHeader = msg + mainArea.sols = "" + } + function onAppendLog(msg) { + logView.text += msg + } + } +} diff --git a/qt/adapter.cpp b/qt/adapter.cpp @@ -0,0 +1,140 @@ +#include "adapter.h" + +#include <fstream> +#include <sstream> +#include <string> +#include <vector> +#include <QDebug> +#include <QtConcurrent/QtConcurrent> + +void logWrapper(const char *str, void *data) +{ + auto f = *reinterpret_cast<std::function<void(const char*)>*>(data); + f(str); +} + +NissyAdapter::NissyAdapter() +{ + std::vector<std::string> solverNames { + "h48h3k2" + }; + + for (auto s : solverNames) + initSolver(s); + + writeLog = [&](const char *str) { + emit appendLog(QString::fromStdString(str)); + }; + + nissy::set_logger(&logWrapper, &writeLog); +} + +void NissyAdapter::initSolver(const std::string& s) { + auto se = nissy::solver::get(s); + if (std::holds_alternative<nissy::error>(se)) { + qDebug("Error loading solver!"); + return; + } + auto ss = std::get<nissy::solver>(se); + solvers.push_back(ss); +} + +bool NissyAdapter::loadSolverData(nissy::solver& solver) { + if (solver.data_checked) + return true; + + std::filesystem::path filePath("./tables/" + solver.id); + if (!std::filesystem::exists(filePath)) { + auto err = solver.generate_data(); + if (!err.ok()) { + emit solverError(QString("Error generating data!")); + return false; + } + std::filesystem::create_directory("./tables/"); + std::ofstream ofs(filePath, std::ios::binary); + ofs.write(reinterpret_cast<char *>(solver.data.data()), + solver.size); + ofs.close(); + } else { + std::ifstream ifs(filePath, std::ios::binary); + solver.read_data(ifs); + ifs.close(); + } + + if (!solver.check_data().ok()) { + emit solverError(QString("Error reading data!")); + return false; + } + + return true; +} + +Q_INVOKABLE bool NissyAdapter::isValidScramble(QString qscr) +{ + nissy::cube c; + return c.move(qscr.toStdString()).ok(); +} + +Q_INVOKABLE void NissyAdapter::requestSolve( + QString scramble, + QString solver, + int minmoves, + int maxmoves, + int maxsolutions, + int optimal +) +{ + nissy::cube c; + if (!c.move(scramble.toStdString()).ok()) { + emit solverError(QString("Unexpected error: invalid scramble")); + return; + } + + nissy::solver *ss = nullptr; + for (auto& s : solvers) + if (s.name == solver) + ss = &s; + if (ss == nullptr) { + std::string msg = "Error: solver '" + solver.toStdString() + + "' not available"; + emit solverError(QString::fromStdString(msg)); + return; + } + + SolveOptions opts{c, ss, (unsigned)minmoves, (unsigned)maxmoves, + (unsigned)maxsolutions, optimal}; + auto _ = QtConcurrent::run(&NissyAdapter::startSolve, this, opts); + return; +} + +void NissyAdapter::startSolve(SolveOptions opts) +{ + loadSolverData(*opts.solver); + + auto result = opts.solver->solve(opts.cube, nissy::nissflag::NORMAL, + opts.minmoves, opts.maxmoves, opts.maxsolutions, opts.optimal, 8); + + if (!result.err.ok()) { + std::string msg = "Error computing solutions: " + + std::to_string(result.err.value); + emit solverError(QString::fromStdString(msg)); + return; + } + + auto& sols = result.solutions; + if (sols.size() == 0) { + emit solutionsReady("No solution found", ""); + } else { + std::stringstream hs; + hs << "Found " << sols.size() << " solution" + << (sols.size() > 1 ? "s:" : ":"); + + std::stringstream ss; + for (auto s : sols) { + auto n = nissy::count_moves(s).value; + ss << s << "(" << n << ")" << std::endl; // TODO: remove last newline + } + emit solutionsReady(QString::fromStdString(hs.str()), + QString::fromStdString(ss.str())); + } +} diff --git a/qt/adapter.h b/qt/adapter.h @@ -0,0 +1,52 @@ +#ifndef ADAPTER_H +#define ADAPTER_H + +#include "../cpp/nissy.h" + +#include <map> +#include <string> +#include <QObject> +#include <QtQmlIntegration> + +struct SolveOptions { + nissy::cube cube; + nissy::solver *solver; + unsigned minmoves; + unsigned maxmoves; + unsigned maxsolutions; + int optimal; +}; + +class NissyAdapter : public QObject { + Q_OBJECT + QML_SINGLETON + QML_ELEMENT + +public: + NissyAdapter(); + + Q_INVOKABLE bool isValidScramble(QString); + Q_INVOKABLE void requestSolve( + QString scramble, + QString solver, + int minmoves, + int maxmoves, + int maxsolutions, + int optimal + ); + +signals: + void solutionsReady(QString, QString); + void solverError(QString); + void appendLog(QString); + +private: + std::vector<nissy::solver> solvers; + std::function<void(const char*)> writeLog; + + void initSolver(const std::string&); + void startSolve(SolveOptions); + bool loadSolverData(nissy::solver&); +}; + +#endif diff --git a/qt/main.cpp b/qt/main.cpp @@ -0,0 +1,16 @@ +#include "adapter.h" + +#include <QGuiApplication> +#include <QQmlApplicationEngine> + +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + QQmlApplicationEngine engine; + QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, + &app, []() { QCoreApplication::exit(-1); }, Qt::QueuedConnection); + + engine.loadFromModule("nissyqt", "NissyMain"); + + return app.exec(); +}