""" End-to-end validation script for the brownan solver wrapper. Generates random scrambles with pycuber, feeds the resulting 54-character kociemba-order facelet string to both the cahidenes and brownan wrappers, then verifies each solution actually solves the cube by re-applying it in pycuber. """ import argparse import random import subprocess import sys from pathlib import Path from typing import Any # Map pycuber colour names to single uppercase letters used in kociemba facelets. # The wrapper only requires that the 6 centres are distinct, so any consistent # mapping works. PYCUBER_TO_CHAR = { 'red': 'L', 'orange': 'R', 'green': 'F', 'blue': 'B', 'white': 'D', 'yellow': 'U', } FACES = ["U", "D", "F", "B", "L", "R"] SUFFIXES = ["", "'", "2"] N_SCRAMBLES = 10 SCRAMBLE_DEPTH = 12 SCRIPT_DIR = Path(__file__).resolve().parent def facelet_string_from_cube(cube: Any) -> str: """Build a 54-char kociemba-order facelet string from a pycuber Cube.""" out = [] for f in "URFDLB": face = cube.get_face(f) for row in face: for cell in row: out.append(PYCUBER_TO_CHAR[cell.colour]) return "".join(out) def random_scramble(depth: int = SCRAMBLE_DEPTH) -> str: moves = [] previous_face = None for _ in range(depth): available_faces = [face for face in FACES if face != previous_face] face = random.choice(available_faces) moves.append(face + random.choice(SUFFIXES)) previous_face = face return " ".join(moves) def is_solved(cube: Any) -> bool: for f in "URFDLB": face = cube.get_face(f) colours = {cell.colour for row in face for cell in row} if len(colours) != 1: return False return True def solve_with_wrapper(wrapper_path: Path, facelets: str) -> list[str]: result = subprocess.run( [sys.executable, str(wrapper_path), facelets], capture_output=True, text=True, timeout=300, cwd=str(wrapper_path.parent), ) if result.returncode != 0: print(f" {wrapper_path.name} error (rc={result.returncode}): {result.stderr.strip()}") return [] output = result.stdout.strip() if not output: return [] return output.split() def verify_wca_moves(moves: list[str]) -> bool: faces = set("URFDLB") for m in moves: if not m: return False if m[0] not in faces: return False if len(m) == 2 and m[1] not in ("'", "2"): return False if len(m) > 2: return False return True def main(): parser = argparse.ArgumentParser(description="Validate brownan solver end-to-end") parser.add_argument("--brownan-wrapper", default=None, help="Path to brownan_solver_wrapper.py") parser.add_argument("--cahidenes-wrapper", default=None, help="Path to cahidenes_solver_wrapper.py") parser.add_argument("--count", type=int, default=N_SCRAMBLES, help="Number of random scrambles to test") parser.add_argument("--depth", type=int, default=SCRAMBLE_DEPTH, help="Scramble depth") parser.add_argument("--seed", type=int, default=None, help="Random seed for reproducibility") args = parser.parse_args() try: from pycuber import Cube, Formula except ImportError: print("ERROR: pycuber package not installed. Run: pip install pycuber") sys.exit(1) try: import kociemba except ImportError: print("ERROR: kociemba package not installed. Run: pip install kociemba") sys.exit(1) solver_candidates = [ SCRIPT_DIR, SCRIPT_DIR.parent.parent / "UnrealHyperTwist" / "Binaries" / "Win64" / "Solvers", Path("C:/HyperTwist/UnrealHyperTwist/Binaries/Win64/Solvers"), ] def resolve_default_wrapper(filename: str) -> Path: for directory in solver_candidates: candidate = directory / filename if candidate.exists(): return candidate return solver_candidates[0] / filename brownan_wrapper = Path(args.brownan_wrapper) if args.brownan_wrapper else resolve_default_wrapper("brownan_solver_wrapper.py") cahidenes_wrapper = Path(args.cahidenes_wrapper) if args.cahidenes_wrapper else resolve_default_wrapper("cahidenes_solver_wrapper.py") if not brownan_wrapper.exists(): print(f"ERROR: Brownan wrapper not found at {brownan_wrapper}") sys.exit(1) if not cahidenes_wrapper.exists(): print(f"ERROR: Cahidenes wrapper not found at {cahidenes_wrapper}") sys.exit(1) if args.seed is not None: random.seed(args.seed) all_passed = True for i in range(args.count): print(f"\n=== Test {i+1}/{args.count} ===") scramble = random_scramble(args.depth) print(f"Scramble: {scramble}") c = Cube() c(Formula(scramble)) facelets = facelet_string_from_cube(c) print(f"Facelets: {facelets}") # Sanity-check with kociemba try: canonical = kociemba.solve(facelets) print(f"Canonical ({len(canonical.split())} moves): {canonical}") except Exception as e: print(f"FAIL: kociemba rejected facelets: {e}") all_passed = False continue cahidenes_moves = solve_with_wrapper(cahidenes_wrapper, facelets) brownan_moves = solve_with_wrapper(brownan_wrapper, facelets) print(f"Cahidenes ({len(cahidenes_moves)} moves): {' '.join(cahidenes_moves)}") print(f"Brownan ({len(brownan_moves)} moves): {' '.join(brownan_moves)}") cah_ok = True brn_ok = True if not verify_wca_moves(cahidenes_moves): print("FAIL: Cahidenes returned invalid WCA moves") cah_ok = False all_passed = False if not verify_wca_moves(brownan_moves): print("FAIL: Brownan returned invalid WCA moves") brn_ok = False all_passed = False if cah_ok: check = Cube() check(Formula(scramble)) check(Formula(" ".join(cahidenes_moves))) if not is_solved(check): print("FAIL: Cahidenes solution did not solve the cube") cah_ok = False all_passed = False else: print("PASS: Cahidenes solution verified") if brn_ok: check = Cube() check(Formula(scramble)) check(Formula(" ".join(brownan_moves))) if not is_solved(check): print("FAIL: Brownan solution did not solve the cube") brn_ok = False all_passed = False else: print("PASS: Brownan solution verified") print("\n" + ("ALL TESTS PASSED" if all_passed else "SOME TESTS FAILED")) sys.exit(0 if all_passed else 1) if __name__ == "__main__": main()