hypertwist/Solvers/brownan/validate_brownan_e2e.py
2026-06-11 06:32:20 +00:00

209 lines
6.8 KiB
Python

"""
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()