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 3ab1012ea8c55f82812b5998fd6b4fdf53dea70e
parent 7b8c1e4634784b57bb6223cd780d5c67f5381d43
Author: Sebastiano Tronto <sebastiano@tronto.net>
Date:   Mon, 14 Apr 2025 14:00:44 +0200

Improved C++ API and added info to README

Diffstat:
MREADME.md | 73++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Acpp/examples/move_convert.cpp | 24++++++++++++++++++++++++
Acpp/examples/solve_h48h3k2.cpp | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcpp/nissy.cpp | 293+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mcpp/nissy.h | 194++++++++++++++++++++++++++++++++++---------------------------------------------
Rpython/example.py -> python/examples/solve.py | 0
6 files changed, 415 insertions(+), 260 deletions(-)

diff --git a/README.md b/README.md @@ -139,22 +139,61 @@ UL1 UB0 BL0 FR1 DF0 UR1 DB0 FL0 DR1 DL1 UF0 BR1 DFR2 DBR0 DFL2 UFR2 UBL2 UFL0 UB To solve a cube you can use: ``` -$ ./run solve -solver "h48h0k4" -n 1 -M 4 -cube "JLQWSVUH=ZLCUABGIVTKH" -Found 0 solutions, searching at depth 0 -Found 0 solutions, searching at depth 1 -Found 0 solutions, searching at depth 2 -Found 0 solutions, searching at depth 3 -Solution found: F' U R -F' U R +$ ./run solve -solver h48h0k4 -n 1 -M 4 -cube "JLQWSVUH=ZLCUABGIVTKH" +F' U' R +``` + +Or alternatively: + +``` +$ ./run solve_scramble -solver h48h0k4 -n 1 -M 4 -moves "R' U' F" +F' U' R ``` For a full list of available command, use `run help`. -## Running commands from a Python shell +## Using this software as a library + +This tool has been developed as a library, so it can be easily included +in other programs. For this reason, some bindings for languages other +than C are available. + +The API is documented in the public header file `src/nissy.h`. + +### C + +To use this in a C project, simly include `src/nissy.h`. The tools +in the `tools/` folder are a good example for how to use this. + +NOTE: this project is developed using the C11 standard. If you are using +an older version of C, you can write your own header file. + +### C++ + +The `cpp` folder contains a C++ header `nissy.h` (C++20 standard) and an +implementation file `nissy.cpp`. This interface wraps the calls to the +C functions in an object-oriented C++ interface for more convenient use. -There is a work-in-progress python module available. To build it you need -the Python development headers installed. You can check this from the output -of `./configure.sh`: +The `cpp/examples` folder contains some examples for how to use this +interface. You can build them with e.g. + +``` +g++ -std=c++20 cpp/nissy.cpp cpp/examples/solve_h48h3k2.cpp nissy.o +``` + +NOTE: If you prefer to use a C-style API, you'll have to write +your own header, similar to the `extern "C" {}` block at the top of +`cpp/nissy.cpp`. The C header `src/nissy.h` cannot be compiled as C++, +as it uses features of C that are not compatible with it. + +### Python + +The `python` folder contains a Python module. The API provided by +this module follows the C API quite closely, except its functions +sometimes return strings instead of writing to `char *` buffers. + +To build the Python module you need the Python development headers +installed. You can check this from the output of `./configure.sh`: ``` $ ./configure.sh @@ -182,14 +221,18 @@ From here you can call the library functions directly, for example: 'ASTUGFBH=DACXEZGBLIKF' ``` -Some methods have a different signature in the Pythn module than in -libnissy: for example `solve()` returns the solutions as a list of -strings instead of writing them to a return parameter buffer. +The `python/examples` folder contains some examples, that you +can run for example with: + +``` +$ python python/examples/solve.py +``` + You can access the documentation for the Python module from within a Python interpreter with `help(nissy)`. Cross-check this documentation with the comments in nissy.h for more details. -Please note: the support for the Python module is still rudimentary. +NOTE: Support for the Python module is still rudimentary. ## Cube formats diff --git a/cpp/examples/move_convert.cpp b/cpp/examples/move_convert.cpp @@ -0,0 +1,24 @@ +/* A simple example showing how to move a cube and print it in H48 format. */ + +#include "../nissy.h" +#include <iostream> + +int main() { + nissy::cube c; + if (!c.move("R' U' F").ok()) { + std::cout << "Error moving the cube!" << std::endl; + return 1; + } + + auto string_or_error = c.to_string("H48"); + if (std::holds_alternative<nissy::error>(string_or_error)) { + std::cout << "Error converting cube!" << std::endl; + return 1; + } + + auto str = std::get<std::string>(string_or_error); + std::cout << "Cube in H48 format after R' U' F:" << std::endl + << str << std::endl; + + return 0; +} diff --git a/cpp/examples/solve_h48h3k2.cpp b/cpp/examples/solve_h48h3k2.cpp @@ -0,0 +1,91 @@ +/* Simple demo for an H48 solver */ + +#include "../nissy.h" +#include <filesystem> +#include <fstream> +#include <iostream> +#include <string> + +extern "C" { + long long nissy_setlogger(void (*)(const char *)); +} + +int main() { + // Get verbose output + nissy_setlogger([](const char* s) { std::cout << s; }); + + // Get the scramble from the user + std::cout << "Enter scramble: "; + std::string scramble; + std::getline(std::cin, scramble); + + // Apply scramble to a solved cube + nissy::cube c; + if (!c.move(scramble).ok()) { + std::cout << "Invalid scramble!" << std::endl; + return 1; + } + + // Ask for a limit on the solution's length + int maxmoves; + std::cout << "Maximum number of moves: "; + std::cin >> maxmoves; + + // Load the solver + auto h48h3k2 = std::get<nissy::solver>(nissy::solver::get("h48h3k2")); + std::filesystem::path tableDir("tables"); + std::filesystem::create_directories(tableDir); // Ignored if dir exists + std::filesystem::path tableFile("tables/" + h48h3k2.id); + + // If the table is not present, generate it + if (!std::filesystem::exists(tableFile)) { + std::cout << "Data for h48h3k2 solver was not found, " + << "generating it..." << std::endl; + auto err = h48h3k2.generate_data(); + if (!err.ok()) { + std::cout << "Unexpected error! Error code: " + << err.value << std::endl; + return 1; + } + std::ofstream ofs(tableFile, std::ios::binary); + ofs.write(reinterpret_cast<char *>(h48h3k2.data.data()), + h48h3k2.size); + std::cout << "Table generated and written to " + << tableFile << std::endl; + ofs.close(); + } + // Otherwise read it from file + else { + std::cout << "Loading data table from file" << std::endl; + std::ifstream ifs(tableFile, std::ios::binary); + h48h3k2.read_data(ifs); + ifs.close(); + std::cout << "Data loaded, checking table..." << std::endl; + auto err = h48h3k2.check_data(); + if (!err.ok()) { + std::cout << "Error reading data table from file! " + << "Error code: " << err.value << std::endl; + return 1; + } + std::cout << "Data table loaded" << std::endl; + } + + // Solve + auto solve_result = h48h3k2.solve(c, nissy::nissflag::NORMAL, + 0, maxmoves, 1, -1, 8); + + // Write the result + if (!solve_result.err.ok()) { + std::cout << "Error solving! Error code: " << + solve_result.err.value << std::endl; + return 1; + } + + if (solve_result.solutions.size() == 0) + std::cout << "No solution found!" << std::endl; + else + std::cout << "Solution: " << + solve_result.solutions[0] << std::endl; + + return 0; +} diff --git a/cpp/nissy.cpp b/cpp/nissy.cpp @@ -6,6 +6,8 @@ TODO: add more documentation (here and in README.md) #include "./nissy.h" +#include <fstream> + extern "C" { long long nissy_compose(const char *, const char *, char *); long long nissy_inverse(const char *, char *); @@ -26,191 +28,214 @@ extern "C" { } namespace nissy { - namespace utils { - void fixstr(std::string& str) - { - size_t n = str.find('\0'); - str.resize(n); - } - std::variant<cube_t, error_t> - cube_or_error(const char *r, long long e) - { - if (e < 0) - return error_t{e}; - return cube_t{r}; - } + /* Definition of constants with const qualifier */ + const nissflag nissflag::NORMAL{1}; + const nissflag nissflag::INVERSE{2}; + const nissflag nissflag::MIXED{4}; + const nissflag nissflag::LINEAR{3}; + const nissflag nissflag::ALL{7}; + + const error error::OK{0}; + const error error::UNSOLVABLE{-1}; + const error error::INVALID_CUBE{-10}; + const error error::UNSOLVABLE_CUBE{-11}; + const error error::INVALID_MOVES{-20}; + const error error::INVALID_TRANS{-30}; + const error error::INVALID_FORMAT{-40}; + const error error::INVALID_SOLVER{-50}; + const error error::NULL_POINTER{-60}; + const error error::BUFFER_SIZE{-61}; + const error error::DATA{-70}; + const error error::OPTIONS{-80}; + const error error::UNKNOWN{-999}; + + namespace size { + constexpr size_t B32 = 22; + constexpr size_t H48 = 88; + constexpr size_t CUBE_MAX = H48; + constexpr size_t TRANSFORMATION = 12; + constexpr size_t DATAID = 255; + } - std::variant<std::string, error_t> - string_or_error(const char *cstr, long long e) - { - if (e < 0) - return error_t{e}; + bool error::ok() const { return value >= 0; } - std::string str{cstr}; - fixstr(str); - return str; - } + cube::cube() {} - std::function<void(const char *)> - char_str_fn(std::function<void(std::string&)> f) - { - return [&](const char *cc){ - std::string str{cc}; - f(str); - }; - } - } - - std::variant<cube_t, error_t> - compose(const cube_t& cube, const cube_t& permutation) + error cube::move(const std::string& moves) { char result[size::B32]; - - auto error = nissy_compose(cube.value.c_str(), - permutation.value.c_str(), result); - - return utils::cube_or_error(result, error); + long long err = nissy_applymoves( + m_b32.c_str(), moves.c_str(), result); + if (err < 0) + return error{err}; + m_b32 = result; + return error::OK; } - std::variant<cube_t, error_t> - inverse(const cube_t& cube) + error cube::transform(const std::string& trans) { char result[size::B32]; - - auto error = nissy_inverse(cube.value.c_str(), result); - - return utils::cube_or_error(result, error); + long long err = nissy_applytrans( + m_b32.c_str(), trans.c_str(), result); + if (err < 0) + return error{err}; + m_b32 = result; + return error::OK; } - std::variant<cube_t, error_t> - applymoves(const cube_t& cube, const moves_t& moves) + void cube::invert() { char result[size::B32]; - - auto error = nissy_applymoves(cube.value.c_str(), - moves.value.c_str(), result); - - return utils::cube_or_error(result, error); + nissy_inverse(m_b32.c_str(), result); + m_b32 = result; } - std::variant<cube_t, error_t> - applytrans(const cube_t& cube, const trans_t& trans) + void cube::compose(const cube& other) { char result[size::B32]; + nissy_compose( + m_b32.c_str(), other.to_string().c_str(), result); + m_b32 = result; + } - auto error = nissy_applytrans(cube.value.c_str(), - trans.value.c_str(), result); + std::string cube::to_string() const { return m_b32; } - return utils::cube_or_error(result, error); + std::variant<std::string, error> + cube::to_string(const std::string& format) const + { + char result[size::CUBE_MAX]; + auto err = nissy_convert("B32", format.c_str(), + m_b32.c_str(), size::CUBE_MAX, result); + if (err < 0) + return error{err}; + else + return result; } - std::variant<std::string, error_t> - convert(const cube_format_t& format_in, - const cube_format_t& format_out, const std::string& cube_string) + std::variant<cube, error> + cube::from_string(const std::string& str) { - char result[size::CUBE_MAX]; + return from_string(str, "B32"); + } - auto error = nissy_convert(format_in.value.c_str(), - format_out.value.c_str(), cube_string.c_str(), - size::CUBE_MAX, result); + std::variant<cube, error> + cube::from_string(const std::string& str, const std::string& format) + { + char result[size::B32]; + cube c; + auto err = nissy_convert(format.c_str(), + "B32", c.m_b32.c_str(), size::B32, result); + if (err < 0) + return error{err}; + c.m_b32 = result; + return c; + } - return utils::string_or_error(result, error); + std::variant<cube, error> + cube::get(long long ep, long long eo, long long cp, long long co) + { + return get(ep, eo, cp, co, "fix"); } - std::variant<cube_t, error_t> - getcube(long long ep, long long eo, long long cp, long long co, + std::variant<cube, error> + cube::get(long long ep, long long eo, long long cp, long long co, const std::string& options) { char result[size::B32]; - - auto error = nissy_getcube(ep, eo, cp, co, options.c_str(), - result); - - return utils::cube_or_error(result, error); + cube c; + auto err = nissy_getcube( + ep, eo, cp, co, options.c_str(), result); + if (err < 0) + return error{err}; + c.m_b32 = result; + return c; } - std::variant<std::pair<size_t, std::string>, error_t> - solverinfo(const solver_t& solver) + error solver::generate_data() { - char dataid[size::DATAID]; - - auto error = nissy_solverinfo(solver.value.c_str(), dataid); - if (error < 0) - return error_t{error}; - - return std::pair{error, dataid}; + data.resize(size); + auto err = nissy_gendata(name.c_str(), + size, reinterpret_cast<char *>(data.data())); + return error{err}; } - std::variant<solver_data_t, error_t> - gendata(const solver_t& solver) + void solver::read_data(std::ifstream& ifs) { - char dataid[size::DATAID]; - - auto error = nissy_solverinfo(solver.value.c_str(), dataid); - if (error < 0) - return error_t{error}; - - size_t sz = error; - std::vector<std::byte> buffer(sz); - error = nissy_gendata(solver.value.c_str(), sz, - reinterpret_cast<char *>(buffer.data())); - if (error < 0) - return error_t{error}; - - return solver_data_t{buffer}; + data.resize(size); + ifs.read(reinterpret_cast<char *>(data.data()), size); } - std::optional<error_t> - checkdata(const solver_data_t& data) + error solver::check_data() const { - auto error = nissy_checkdata(data.value.size(), - reinterpret_cast<const char *>(data.value.data())); - - if (error < 0) - return error_t{error}; - return std::nullopt; + auto err = nissy_checkdata(data.size(), + reinterpret_cast<const char *>(data.data())); + return error{err}; } - std::variant<solve_result_t, error_t> - solve(const cube_t& cube, const solver_t& solver, - nissflag_t niss, unsigned minmoves, unsigned maxmoves, - unsigned maxsolutions, int optimal, int threads, - const solver_data_t& data) + void solver::unload_data() { - const size_t len = 3 * (maxmoves+1) * maxsolutions; - char csols[len]; - long long cstats[size::SOLVE_STATS]; - - auto error = nissy_solve(cube.value.c_str(), - solver.value.c_str(), niss.value, minmoves, maxmoves, - maxsolutions, optimal, threads, data.value.size(), - reinterpret_cast<const char *>(data.value.data()), - len, csols, cstats); + data.resize(0); + } - if (error < 0) - return error_t{error}; + solver::solve_result + solver::solve(const cube& cube, nissflag niss, unsigned minmoves, + unsigned maxmoves, unsigned maxsols, int optimal, int threads) + { + const size_t len = 3 * (maxmoves+1) * maxsols; + std::vector<char> csols(len); + solver::solve_result result; + + auto err = nissy_solve(cube.to_string().c_str(), + name.c_str(), niss.value, minmoves, maxmoves, maxsols, + optimal, threads, data.size(), + reinterpret_cast<const char *>(data.data()), len, + csols.data(), result.stats.data()); + result.err = error{err}; + + if (err < 0) + return result; + + // TODO: is this special case actually needed? Remove if not + if (err == 0) { + result.solutions = {}; + return result; + } - std::vector<std::string> sols; - std::string_view strsols(csols); + std::string_view strsols(csols.data()); for (auto r : strsols | std::views::split('\n')) - sols.push_back(std::string{r.begin(), r.end()}); + if (r.begin() != r.end()) + result.solutions.push_back( + std::string{r.begin(), r.end()}); - return solve_result_t{sols, std::to_array(cstats)}; + return result; } - std::variant<unsigned, error_t> - countmoves(const moves_t& moves) + std::variant<solver, error> solver::get(const std::string& name) + { + char dataid[size::DATAID]; + auto err = nissy_solverinfo(name.c_str(), dataid); + if (err < 0) + return error{err}; + solver s(name); + s.size = (unsigned)err; + s.id = std::string{dataid}; + return s; + } + + solver::solver(const std::string& str) : name{str} {} + + std::variant<unsigned, error> + count_moves(const std::string& moves) { - auto error = nissy_countmoves(moves.value.c_str()); - if (error < 0) - return error_t{error}; - return (unsigned)error; + auto err = nissy_countmoves(moves.c_str()); + if (err < 0) + return error{err}; + return (unsigned)err; } - void setlogger(std::function<void(std::string&)> log) + void set_logger(const std::function<void(const char *)>& log) { - nissy_setlogger( - utils::char_str_fn(log).target<void(const char *)>()); + nissy_setlogger(log.target<void(const char *)>()); } } diff --git a/cpp/nissy.h b/cpp/nissy.h @@ -15,119 +15,91 @@ C++20 header file for nissy. #include <vector> namespace nissy { - /* Some constants for size for I/O buffers */ - namespace size { - constexpr size_t B32 = 22; - constexpr size_t H48 = 88; - constexpr size_t CUBE_MAX = H48; - constexpr size_t TRANSFORMATION = 12; - constexpr size_t SOLVE_STATS = 10; - constexpr size_t DATAID = 255; - } - - /* Some structs definitions for better type safety */ - struct nissflag_t { unsigned value; }; - struct error_t { long long value; }; - struct cube_t { std::string value; }; - struct moves_t { std::string value; }; - struct trans_t { std::string value; }; - struct cube_format_t { std::string value; }; - struct solver_t { std::string value; }; - struct solver_data_t { std::vector<std::byte> value; }; - struct solve_result_t { - std::vector<std::string> solutions; - std::array<long long, size::SOLVE_STATS> stats; + + class nissflag { + public: + unsigned value; + + static const nissflag NORMAL; + static const nissflag INVERSE; + static const nissflag MIXED; + static const nissflag LINEAR; + static const nissflag ALL; + }; + + class error { + public: + long long value; + bool ok() const; + + static const error OK; + static const error UNSOLVABLE; + static const error INVALID_CUBE; + static const error UNSOLVABLE_CUBE; + static const error INVALID_MOVES; + static const error INVALID_TRANS; + static const error INVALID_FORMAT; + static const error INVALID_SOLVER; + static const error NULL_POINTER; + static const error BUFFER_SIZE; + static const error DATA; + static const error OPTIONS; + static const error UNKNOWN; + }; + + class cube { + public: + cube(); + error move(const std::string&); + error transform(const std::string&); + void invert(); + void compose(const cube&); + std::string to_string() const; + std::variant<std::string, error> to_string( + const std::string& format) const; + + static std::variant<cube, error> from_string( + const std::string&); + static std::variant<cube, error> from_string( + const std::string& str, const std::string& format); + static std::variant<cube, error> get( + long long ep, long long eo, long long cp, long long co); + static std::variant<cube, error> get( + long long ep, long long eo, long long cp, long long co, + const std::string& options); + + private: + std::string m_b32{"ABCDEFGH=ABCDEFGHIJKL"}; + }; + + class solver { + public: + struct solve_result { + error err; + std::vector<std::string> solutions; + std::array<long long, 10> stats; + }; + + const std::string name; + size_t size; + std::string id; + std::vector<std::byte> data; + + error generate_data(); + void read_data(std::ifstream&); + error check_data() const; + void unload_data(); + solve_result solve(const cube&, nissflag, unsigned minmoves, + unsigned maxmoves, unsigned maxsols, int optimal, + int threads); + + static std::variant<solver, error> get(const std::string&); + private: + solver(const std::string& name); }; - /* Flags for NISS options */ - namespace nissflag { - constexpr nissflag_t NORMAL{1}; - constexpr nissflag_t INVERSE{2}; - constexpr nissflag_t MIXED{4}; - constexpr nissflag_t LINEAR{NORMAL.value | INVERSE.value}; - constexpr nissflag_t ALL{LINEAR.value | MIXED.value}; - } - - /* Error codes */ - namespace error { - constexpr error_t UNSOLVABLE{-1}; - constexpr error_t INVALID_CUBE{-10}; - constexpr error_t UNSOLVABLE_CUBE{-11}; - constexpr error_t INVALID_MOVES{-20}; - constexpr error_t INVALID_TRANS{-30}; - constexpr error_t INVALID_FORMAT{-40}; - constexpr error_t INVALID_SOLVER{-50}; - constexpr error_t NULL_POINTER{-60}; - constexpr error_t BUFFER_SIZE{-61}; - constexpr error_t DATA{-70}; - constexpr error_t OPTIONS{-80}; - constexpr error_t UNKNOWN{-999}; - } - - /* Cube constants */ - namespace cube { - const cube_t SOLVED{"ABCDEFGH=ABCDEFGHIJKL"}; - } - - std::variant<cube_t, error_t> inverse( - const cube_t& cube - ); - - std::variant<cube_t, error_t> applymoves( - const cube_t& cube, - const moves_t& moves - ); - - std::variant<cube_t, error_t> applytrans( - const cube_t& cube, - const trans_t& trans - ); - - std::variant<std::string, error_t> convert( - const cube_format_t& format_in, - const cube_format_t& format_out, - const std::string& cube_string - ); - - std::variant<cube_t, error_t> getcube( - long long ep, - long long eo, - long long cp, - long long co, - const std::string& options - ); - - std::variant<std::pair<size_t, std::string>, error_t> solverinfo( - const solver_t& solver - ); - - std::variant<solver_data_t, error_t> gendata( - const solver_t& solver - ); - - std::optional<error_t> checkdata( - const solver_data_t& data - ); - - std::variant<solve_result_t, error_t> solve( - const cube_t& cube, - const solver_t& solver, - nissflag_t niss, - unsigned minmoves, - unsigned maxmoves, - unsigned maxsolutions, - int optimal, - int threads, - const solver_data_t& data - ); - - std::variant<unsigned, error_t> countmoves( - const moves_t& moves - ); - - void setlogger( - std::function<void(std::string&)> log - ); + std::variant<unsigned, error> count_moves(const std::string&); + void set_logger(const std::function<void(const char*)>&); } #endif diff --git a/python/example.py b/python/examples/solve.py