diff --git a/Solvers/brownan/brownan_solver_wrapper.py b/Solvers/brownan/brownan_solver_wrapper.py new file mode 100644 index 0000000..8b63c56 --- /dev/null +++ b/Solvers/brownan/brownan_solver_wrapper.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +""" +HyperTwist IPC wrapper for brownan GPL Rubik's Cube solver oracle. + +Input: 120-character brownan raw cube string (NOT 54-char facelet). + Brownan uses a cubie-based format documented at: + https://github.com/brownan/Rubiks-Cube-Solver + +Output: Space-separated WCA move notation (U, D, F, B, L, R, with ' and 2 suffixes). + Brownan native output uses faces F, T, L, B, D, R where T=U in WCA. +""" +import argparse +import os +import subprocess +import sys + + +# Map brownan native face letters to WCA face letters +BROWNAN_TO_WCA = { + "T": "U", # Top in brownan = Up in WCA + "D": "D", + "F": "F", + "B": "B", + "L": "L", + "R": "R", +} + + +def find_brownan_exe(): + """Locate the brownan solver executable next to this wrapper.""" + script_dir = os.path.dirname(os.path.abspath(__file__)) + candidates = [ + os.path.join(script_dir, "..", "..", "UnrealHyperTwist", "Binaries", "Win64", "Solvers", "brownan-solver.exe"), + os.path.join(script_dir, "brownan-solver.exe"), + os.path.join(script_dir, "..", "brownan-solver.exe"), + "brownan-solver.exe", + "brownan-solver", + ] + for candidate in candidates: + resolved = os.path.abspath(candidate) + if os.path.isfile(resolved): + return resolved + return None + + +def ensure_tables(brownan_exe: str) -> bool: + """Check that brownan heuristic tables exist next to the executable.""" + exe_dir = os.path.dirname(os.path.abspath(brownan_exe)) + tables = ["table_corner.rht", "table_edge1.rht", "table_edge2.rht"] + missing = [t for t in tables if not os.path.exists(os.path.join(exe_dir, t))] + if missing: + print( + f"WARNING: Brownan tables missing: {missing}. " + "Solving without tables will be very slow. " + "Run table generation first by invoking brownan-solver.exe interactively " + "and selecting options 2, 3, and 4.", + file=sys.stderr, + ) + return False + return True + + +def convert_move(brownan_move: str) -> str: + """Convert a single brownan move token to WCA notation.""" + # brownan tokens look like: "F ", "T' ", "2L " + token = brownan_move.strip() + if not token: + return "" + + # Handle double-turn prefix + if token.startswith("2"): + face = token[1] + suffix = "2" + elif token.endswith("'"): + face = token[0] + suffix = "'" + else: + face = token[0] + suffix = "" + + wca_face = BROWNAN_TO_WCA.get(face, face) + return f"{wca_face}{suffix}" + + +def solve(raw_cube: str, brownan_exe: str = None) -> str: + """Solve a cube from a brownan 120-character raw string.""" + if len(raw_cube) != 120: + print(f"ERROR: Brownan raw cube string must be 120 characters, got {len(raw_cube)}", file=sys.stderr) + sys.exit(1) + + if brownan_exe is None: + brownan_exe = find_brownan_exe() + if not brownan_exe or not os.path.exists(brownan_exe): + print("ERROR: brownan-solver executable not found", file=sys.stderr) + sys.exit(1) + + # Optionally warn about missing tables (non-fatal) + ensure_tables(brownan_exe) + + exe_dir = os.path.dirname(os.path.abspath(brownan_exe)) + try: + result = subprocess.run( + [brownan_exe, raw_cube], + cwd=exe_dir, + text=True, + capture_output=True, + timeout=300, + ) + except subprocess.TimeoutExpired: + print("ERROR: Brownan solver timed out (consider generating heuristic tables)", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"ERROR: {e}", file=sys.stderr) + sys.exit(1) + + # brownan prints "Solution found!" and then moves, may continue searching deeper + lines = result.stdout.splitlines() + in_solution = False + moves = [] + for line in lines: + stripped = line.strip() + if stripped == "Solution found!": + in_solution = True + continue + if in_solution: + if not stripped: + break + # A solution line is a sequence of move tokens like "F T' 2L ..." + for token in stripped.split(): + wca = convert_move(token) + if wca: + moves.append(wca) + break + + return " ".join(moves) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="HyperTwist Brownan Solver Wrapper") + parser.add_argument("raw_cube", help="120-character brownan raw cube string") + parser.add_argument("--exe", help="Path to brownan-solver executable") + args = parser.parse_args() + + result = solve(args.raw_cube.strip(), args.exe) + print(result)