#!/usr/bin/env python3 """Generate the exact Magic120Cell sticker-permutation table. The extractor compiles a temporary, headless compatibility copy of the MIT-licensed roice3/Magic120Cell geometry/state engine. It assigns a unique identity to each of the 7,560 sticker slots, applies every cell/sticker move, and records only the changed destination/source pairs. Copyright (c) 2016 Roice Nelson Magic120Cell source license: MIT """ from __future__ import annotations import argparse import base64 import hashlib import json import shutil import struct import subprocess import tempfile import zlib from pathlib import Path CELL_COUNT = 120 STICKERS_PER_CELL = 63 TWIST_STICKER_COUNT = 62 STATE_SIZE = CELL_COUNT * STICKERS_PER_CELL MOVE_COUNT = CELL_COUNT * TWIST_STICKER_COUNT RAW_MAGIC = b"HTM120P1" COMPAT_STDAFX = r""" #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using __int8 = std::int8_t; using __int16 = std::int16_t; using __int32 = std::int32_t; using __int64 = std::int64_t; using BOOL = int; using HDC = void*; using HGLRC = void*; using HWND = void*; #ifndef TRUE #define TRUE 1 #endif #ifndef FALSE #define FALSE 0 #endif """ COMPAT_LOADER_HEADER = r""" #pragma once #include "puzzle/state.h" #include "puzzle/twist.h" class CLoader { public: CLoader(); void saveToFile(int, const CState&, const std::vector&, bool); void saveScrambledFile(const CState&, const std::vector&); bool loadFromFile(int&, CState&, std::vector&); bool loadScrambledFile(CState&, std::vector&); }; """ COMPAT_RENDERER_HEADER = r""" #pragma once #include "vectorND.h" class IRenderable { public: virtual ~IRenderable() = default; virtual void render(const CVector3D& lookFrom, bool forPicking) = 0; }; """ COMPAT_SUPPORT_CPP = r""" #include #include "helper.h" #include "loader.h" void CColor::generateRandom() { m_r = getRandomDouble(1.0); m_g = getRandomDouble(1.0); m_b = getRandomDouble(1.0); m_a = 1.0; } void CColor::setColorHLS(double h, double l, double s, double a) { // Color values are irrelevant to permutation extraction, but state // initialization requires a stable in-range palette. m_r = std::fmod(h + s, 1.0); m_g = l; m_b = std::fmod(h + 0.5, 1.0); m_a = a; } void CColor::lighten() { m_r = std::min(1.0, m_r * 1.1); m_g = std::min(1.0, m_g * 1.1); m_b = std::min(1.0, m_b * 1.1); } CLoader::CLoader() = default; void CLoader::saveToFile(int, const CState&, const std::vector&, bool) {} void CLoader::saveScrambledFile(const CState&, const std::vector&) {} bool CLoader::loadFromFile(int&, CState&, std::vector&) { return false; } bool CLoader::loadScrambledFile(CState&, std::vector&) { return false; } """ EXTRACTOR_CPP = r""" #include #include "magic120Cell.h" namespace { constexpr int CellCount = 120; constexpr int StickersPerCell = 63; constexpr int StateSize = CellCount * StickersPerCell; class CInspectableMagic120Cell final : public CMagic120Cell { public: void setUniqueIdentity() { for (int cell = 0; cell < CellCount; ++cell) { for (int sticker = 0; sticker < StickersPerCell; ++sticker) { m_state.setStickerColorIndex( cell, sticker, cell * StickersPerCell + sticker); } } m_state.commitChanges(); m_twistHistory.clear(); } int getIdentityAt(const int destination) const { return m_state.getStickerColorIndex( destination / StickersPerCell, destination % StickersPerCell); } }; void writeU16(std::ofstream& output, const std::uint16_t value) { output.write(reinterpret_cast(&value), sizeof(value)); } void writeU32(std::ofstream& output, const std::uint32_t value) { output.write(reinterpret_cast(&value), sizeof(value)); } } int main(const int argc, const char* argv[]) { if (argc != 2) { std::cerr << "usage: extractor " << std::endl; return 2; } std::ofstream output(argv[1], std::ios::binary | std::ios::trunc); if (!output) { std::cerr << "unable to open output" << std::endl; return 3; } output.write("HTM120P1", 8); writeU32(output, CellCount); writeU32(output, StickersPerCell); writeU32(output, CellCount * (StickersPerCell - 1)); writeU32(output, StateSize); auto puzzle = std::make_unique(); std::uint64_t totalPairs = 0; std::uint16_t minimumPairs = UINT16_MAX; std::uint16_t maximumPairs = 0; for (int cell = 0; cell < CellCount; ++cell) { for (int sticker = 1; sticker < StickersPerCell; ++sticker) { puzzle->setUniqueIdentity(); STwist twist; twist.m_cell = cell; twist.m_sticker = sticker; twist.m_leftClick = false; twist.m_viewRotation = false; twist.m_slicemask = 1; puzzle->startRotate(twist); puzzle->finishRotate(); std::vector> changed; changed.reserve(256); for (int destination = 0; destination < StateSize; ++destination) { const int source = puzzle->getIdentityAt(destination); if (source != destination) { if (source < 0 || source >= StateSize) { std::cerr << "invalid source identity" << std::endl; return 4; } changed.emplace_back( static_cast(destination), static_cast(source)); } } if (changed.empty() || changed.size() > UINT16_MAX) { std::cerr << "invalid changed-slot count" << std::endl; return 5; } const auto changedCount = static_cast(changed.size()); writeU16(output, changedCount); for (const auto& pair : changed) { writeU16(output, pair.first); writeU16(output, pair.second); } totalPairs += changedCount; minimumPairs = std::min(minimumPairs, changedCount); maximumPairs = std::max(maximumPairs, changedCount); } if ((cell + 1) % 10 == 0) { std::cerr << "extracted cells " << (cell + 1) << " / " << CellCount << std::endl; } } output.flush(); if (!output) { std::cerr << "write failed" << std::endl; return 6; } std::cerr << "moves=" << CellCount * (StickersPerCell - 1) << " pairs=" << totalPairs << " min=" << minimumPairs << " max=" << maximumPairs << std::endl; return 0; } """ SOURCE_FILES = ( "Magic120Cell/workFiles/magic120Cell.cpp", "Magic120Cell/workFiles/puzzle.cpp", "Magic120Cell/workFiles/cell.cpp", "Magic120Cell/workFiles/cellDodec.cpp", "Magic120Cell/workFiles/primitives.cpp", "Magic120Cell/workFiles/settings.cpp", "geometryLib/vectorND.cpp", "geometryLib/polygon.cpp", "geometryLib/gl/displayList.cpp", "geometryLib/puzzle/state.cpp", "geometryLib/puzzle/twist.cpp", "geometryLib/puzzle/twistHistory.cpp", ) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument( "--donor-root", type=Path, default=Path( "/home/dev/src/Workspaces/HyperTwist/mirrors/permissive/" "roice3/Magic120Cell" ), ) parser.add_argument("--output-inl", required=True, type=Path) parser.add_argument("--output-manifest", required=True, type=Path) parser.add_argument("--keep-raw", type=Path) return parser.parse_args() def patch_compatibility_tree(root: Path) -> None: (root / "Magic120Cell/stdafx.h").write_text(COMPAT_STDAFX, encoding="utf-8") (root / "Magic120Cell/workFiles/loader.h").write_text( COMPAT_LOADER_HEADER, encoding="utf-8" ) (root / "geometryLib/renderer.h").write_text( COMPAT_RENDERER_HEADER, encoding="utf-8" ) matrix_text = (root / "geometryLib/matrix4D.h").read_text(encoding="utf-8-sig") matrix_text = matrix_text.replace( "m[i][j] operation= rhs.m[i][j];", "m[i][j] operation ## = rhs.m[i][j];", ) matrix_text = matrix_text.replace( "m[i][j] operation= a;", "m[i][j] operation ## = a;", ) matrix_text = matrix_text.replace( "DO_OP_MATRIX_INPLACE(+);", """for( int i=0; i<4; i++ ) \t\t\tfor( int j=0; j<4; j++ ) \t\t\t\tm[i][j] += rhs.m[i][j]; \t\treturn( *this );""", ) matrix_text = matrix_text.replace( "DO_OP_M_SCALAR_INPLACE(*);", """for( int i=0; i<4; i++ ) \t\t\tfor( int j=0; j<4; j++ ) \t\t\t\tm[i][j] *= a; \t\treturn( *this );""", ) (root / "geometryLib/matrix4D.h").write_text(matrix_text, encoding="utf-8") (root / "geometryLib/matrix4d.h").write_text(matrix_text, encoding="utf-8") (root / "geometryLib/gl/displayList.h").write_text( """#pragma once #include class CDisplayList { public: CDisplayList(); ~CDisplayList(); CDisplayList(const CDisplayList&); CDisplayList& operator=(const CDisplayList&); void start(bool execute = true); void end() const; bool isRecorded() const; bool call() const; void clear(); private: GLuint m_dl; }; """, encoding="utf-8", ) vector_header = root / "geometryLib/vectorND.h" vector_text = vector_header.read_text(encoding="utf-8-sig") vector_text = vector_text.replace( "m_components[i] operation= rhs.m_components[i];", "m_components[i] operation ## = rhs.m_components[i];", ) vector_text = vector_text.replace( "m_components[i] operation= a;", "m_components[i] operation ## = a;", ) old_nan_check = """inline bool isNaN( double value ) { \tint floatType = _fpclass( value ); \treturn \t\t_FPCLASS_SNAN == floatType || \t\t_FPCLASS_QNAN == floatType || \t\t_FPCLASS_NINF == floatType || \t\t_FPCLASS_PINF == floatType; }""" if old_nan_check not in vector_text: raise RuntimeError("expected donor floating-point classification block missing") vector_text = vector_text.replace( old_nan_check, """inline bool isNaN( double value ) { \treturn !std::isfinite( value ); }""", ) vector_header.write_text(vector_text, encoding="utf-8") polygon_path = root / "geometryLib/polygon.cpp" polygon_text = polygon_path.read_text(encoding="cp1252") polygon_text = polygon_text.replace( "s.m_p2 * finiteScale : s.m_p1", "CVector3D( s.m_p2 * finiteScale ) : s.m_p1", ) polygon_text = polygon_text.replace( "s.m_p1 * finiteScale : s.m_p2", "CVector3D( s.m_p1 * finiteScale ) : s.m_p2", ) polygon_path.write_text(polygon_text, encoding="utf-8") replacements = { "Magic120Cell/workFiles/cellDodec.cpp": ( ('#include "CellDodec.h"', '#include "cellDodec.h"'), ("__super::operator = ( rhs );", "Cell::operator = ( rhs );"), ("__super::getSticker( sticker )", "Cell::getSticker( sticker )"), ), "Magic120Cell/workFiles/magic120Cell.cpp": ( ('#include "magic120cell.h"', '#include "magic120Cell.h"'), ("__super::render( lookFrom, forPicking );", "CPuzzle::render( lookFrom, forPicking );"), ("__super::resetView();", "CPuzzle::resetView();"), ), } for relative_path, path_replacements in replacements.items(): path = root / relative_path text = path.read_text(encoding="utf-8-sig") for old, new in path_replacements: if old not in text: raise RuntimeError(f"expected donor source token missing: {old}") text = text.replace(old, new) path.write_text(text, encoding="utf-8") def compile_and_extract(root: Path, raw_output: Path) -> str: support_cpp = root / "compat_support.cpp" extractor_cpp = root / "extractor.cpp" executable = root / "magic120cell-extractor" support_cpp.write_text(COMPAT_SUPPORT_CPP, encoding="utf-8") extractor_cpp.write_text(EXTRACTOR_CPP, encoding="utf-8") command = [ "clang++", "-std=c++14", "-O2", "-g", "-fno-omit-frame-pointer", "-fsanitize=address,undefined", "-ffunction-sections", "-fdata-sections", "-Wno-unknown-pragmas", "-I", str(root / "Magic120Cell"), "-I", str(root / "Magic120Cell/workFiles"), "-I", str(root / "geometryLib"), str(extractor_cpp), str(support_cpp), *(str(root / relative_path) for relative_path in SOURCE_FILES), "-Wl,--gc-sections", "-lGL", "-o", str(executable), ] compile_result = subprocess.run( command, check=False, capture_output=True, text=True ) if compile_result.returncode != 0: raise RuntimeError( "Magic120Cell extractor compile failed:\n" f"{compile_result.stdout}\n{compile_result.stderr}" ) extract_result = subprocess.run( [str(executable), str(raw_output)], check=False, capture_output=True, text=True, ) if extract_result.returncode != 0: raise RuntimeError( "Magic120Cell extractor execution failed:\n" f"exit_code={extract_result.returncode}\n" f"{extract_result.stdout}\n{extract_result.stderr}" ) return extract_result.stderr.strip() def validate_raw_table(raw: bytes) -> dict[str, int]: if len(raw) < 24 or raw[:8] != RAW_MAGIC: raise RuntimeError("raw permutation table has an invalid header") cell_count, sticker_count, move_count, state_size = struct.unpack_from( " len(raw): raise RuntimeError("raw permutation table ended before a move header") pair_count = struct.unpack_from(" len(raw): raise RuntimeError("raw permutation table contains an invalid move") destinations: set[int] = set() sources: set[int] = set() for pair_index in range(pair_count): destination, source = struct.unpack_from( "= state_size or source >= state_size or destination in destinations or source in sources ): raise RuntimeError("raw move is not a bijection over changed slots") destinations.add(destination) sources.add(source) if destinations != sources: raise RuntimeError("raw move does not close over its changed-slot set") cursor += byte_count minimum_pairs = min(minimum_pairs, pair_count) maximum_pairs = max(maximum_pairs, pair_count) total_pairs += pair_count if cursor != len(raw): raise RuntimeError("raw permutation table contains trailing bytes") return { "minimum_pairs_per_move": minimum_pairs, "maximum_pairs_per_move": maximum_pairs, "total_pairs": total_pairs, } def write_inl(path: Path, compressed: bytes, raw_size: int, sha256: str) -> None: path.parent.mkdir(parents=True, exist_ok=True) encoded = base64.b64encode(compressed).decode("ascii") wrapped = "\n".join( f' TEXT("{encoded[index:index + 120]}"),' for index in range(0, len(encoded), 120) ) content = f"""#pragma once // Generated from roice3/Magic120Cell (MIT), Copyright (c) 2016 Roice Nelson. // Raw table SHA-256: {sha256} namespace HyperTwistMagic120CellGenerated {{ constexpr int32 RawPermutationTableSize = {raw_size}; constexpr int32 CompressedPermutationTableSize = {len(compressed)}; constexpr const TCHAR* CompressedPermutationTableBase64Chunks[] = {{ {wrapped} }}; }} """ path.write_text(content, encoding="utf-8", newline="\n") def main() -> int: args = parse_args() donor_root = args.donor_root.resolve() if not (donor_root / "Magic120Cell/workFiles/magic120Cell.cpp").is_file(): raise RuntimeError(f"Magic120Cell donor root is incomplete: {donor_root}") with tempfile.TemporaryDirectory(prefix="hypertwist-magic120cell-") as temporary: compatibility_root = Path(temporary) / "source" shutil.copytree(donor_root, compatibility_root) patch_compatibility_tree(compatibility_root) raw_output = Path(temporary) / "magic120cell-permutations.raw" extractor_summary = compile_and_extract(compatibility_root, raw_output) raw = raw_output.read_bytes() table_stats = validate_raw_table(raw) compressed = zlib.compress(raw, level=9) raw_sha256 = hashlib.sha256(raw).hexdigest() write_inl(args.output_inl, compressed, len(raw), raw_sha256) if args.keep_raw is not None: args.keep_raw.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(raw_output, args.keep_raw) manifest = { "schema_version": "hypertwist/magic120cell-permutation-table/v1", "source_repository": "roice3/Magic120Cell", "source_license": "MIT", "source_copyright": "Copyright (c) 2016 Roice Nelson", "cell_count": CELL_COUNT, "stickers_per_cell": STICKERS_PER_CELL, "state_size": STATE_SIZE, "move_count": MOVE_COUNT, "raw_bytes": len(raw), "compressed_bytes": len(compressed), "raw_sha256": raw_sha256, "extractor_summary": extractor_summary, **table_stats, } args.output_manifest.parent.mkdir(parents=True, exist_ok=True) args.output_manifest.write_text( json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8", ) print(json.dumps(manifest, indent=2, sort_keys=True)) return 0 if __name__ == "__main__": raise SystemExit(main())