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:
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