Add brownan solver wrapper with native-to-WCA move conversion

This commit is contained in:
axiomlogicnexus 2026-06-10 20:56:22 +00:00
parent 6c7b94351a
commit 65614be283

View file

@ -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)