- brownan_solver_wrapper.py: skip the 'Continuing to look...' status line that appears after 'Solution found!' so the actual move sequence is parsed instead of garbage tokens. - validate_brownan_e2e.py: replace static TEST_FACELETS with pycuber-driven random scramble generation; verify each solver's output by re-applying it to the scrambled pycuber cube instead of relying on kociemba alone. - Confirmed brownan binary table loading now succeeds after rebuilding with binary fopen flags (addresses Windows text-mode 0x1A EOF issue).
202 lines
6.4 KiB
Python
202 lines
6.4 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 os
|
|
import random
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
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)
|
|
|
|
|
|
# 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
|
|
|
|
|
|
def facelet_string_from_cube(cube: Cube) -> 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:
|
|
return " ".join(random.choice(FACES) + random.choice(SUFFIXES) for _ in range(depth))
|
|
|
|
|
|
def inverse_moves(moves_str: str) -> list[str]:
|
|
rev = list(reversed(moves_str.strip().split()))
|
|
out = []
|
|
for m in rev:
|
|
if m.endswith("'"):
|
|
out.append(m[:-1])
|
|
elif m.endswith("2"):
|
|
out.append(m)
|
|
else:
|
|
out.append(m + "'")
|
|
return out
|
|
|
|
|
|
def is_solved(cube: Cube) -> 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(
|
|
["python.exe", 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()
|
|
|
|
solvers_dir = Path("C:/HyperTwist/UnrealHyperTwist/Binaries/Win64/Solvers")
|
|
brownan_wrapper = Path(args.brownan_wrapper) if args.brownan_wrapper else solvers_dir / "brownan_solver_wrapper.py"
|
|
cahidenes_wrapper = Path(args.cahidenes_wrapper) if args.cahidenes_wrapper else solvers_dir / "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()
|