From c2efdcaa8689fda2cc932dc4e160378940e394f8 Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Wed, 10 Jun 2026 23:04:25 +0000 Subject: [PATCH] fix(brownan): fix stdout parser and end-to-end validation harness - 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). --- Solvers/brownan/brownan_solver_wrapper.py | 6 +- Solvers/brownan/validate_brownan_e2e.py | 190 +++++++++++++--------- 2 files changed, 117 insertions(+), 79 deletions(-) diff --git a/Solvers/brownan/brownan_solver_wrapper.py b/Solvers/brownan/brownan_solver_wrapper.py index 3065654..c9be4b8 100644 --- a/Solvers/brownan/brownan_solver_wrapper.py +++ b/Solvers/brownan/brownan_solver_wrapper.py @@ -253,7 +253,11 @@ def solve(cube_input: str, brownan_exe: str = None) -> str: continue if in_solution: if not stripped: - break + continue + # Skip the "Continuing to look..." status line; the actual move + # sequence is on the following line. + if stripped.startswith("Continuing"): + continue for token in stripped.split(): wca = convert_move(token) if wca: diff --git a/Solvers/brownan/validate_brownan_e2e.py b/Solvers/brownan/validate_brownan_e2e.py index 30dcb1c..350fc85 100644 --- a/Solvers/brownan/validate_brownan_e2e.py +++ b/Solvers/brownan/validate_brownan_e2e.py @@ -1,16 +1,23 @@ -#!/usr/bin/env python3 """ End-to-end validation script for the brownan solver wrapper. -Tests brownan and cahidenes solvers against preset scrambled cube facelets. -Validates that both solvers return legal WCA move sequences that kociemba -confirms actually solve the cube. +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: @@ -18,46 +25,71 @@ except ImportError: sys.exit(1) -# Pre-computed scrambled facelet strings (kociemba order: U R F D L B) -# Generated with pycuber using 25-move random scrambles. -TEST_FACELETS = [ - # Scramble 1: R U2 F D R R2 L L' U U D2 L L R2 R2 B B2 F D2 B' F D' U B F' - "DDRBUDLLRBLBURDUFDBBDRFRLRFUDRUDLFLLFRUBLFLFFUFRBBUBUD", - # Scramble 2: L' U2 B2 U' U2 F2 L' L R U2 D' U U' F' R' D' F R' R2 R L2 D2 R D' B' - "RLRRUULURBFULRDRULDRDBFDURFFDDFDBFLUUFBDLRLLLFUBFBBBBD", - # Scramble 3: R2 L R' U U' B' U L2 F R' B2 B F D2 L2 F2 L' L' F D2 B U D2 D2 B2 - "DDURUBRBDLLBRRLBFRBUFFFBBLDRDLUDRFFUFDDRLDUFULBRUBUFLL", - # Scramble 4: U' B2 B2 F2 U2 R R2 F2 F F' D' U2 R' L L R' R2 L D' D2 L L' B U' F - "BUDRUDURUBFFURDFLDFURLFBDFRFRUDDFRRLUDLULBDLRLFLBBLBBB", - # Scramble 5: U L U2 B L D2 B2 D' L2 B L2 R R' B2 R' B2 B D U' U2 L L U R2 U - "UBBDUBDURDRLBRLUUBBFFFFUUDRRLBUDRFFRFRLDLLLLFUDLBBFDRD", -] +# 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 solve_with_cahidenes(facelets, wrapper_path): - """Call the cahidenes solver wrapper and return move list.""" - result = subprocess.run( - ["python.exe", str(wrapper_path), facelets], - capture_output=True, - text=True, - timeout=60, - ) - if result.returncode != 0: - print(f"Cahidenes solver error: {result.stderr.strip()}") - return [] - return result.stdout.strip().split() +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 solve_with_brownan(facelets, wrapper_path): - """Call the brownan solver wrapper and return move list.""" +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"Brownan solver error: {result.stderr.strip()}") + print(f" {wrapper_path.name} error (rc={result.returncode}): {result.stderr.strip()}") return [] output = result.stdout.strip() if not output: @@ -65,8 +97,7 @@ def solve_with_brownan(facelets, wrapper_path): return output.split() -def verify_wca_moves(moves): - """Check that all moves are valid WCA notation.""" +def verify_wca_moves(moves: list[str]) -> bool: faces = set("URFDLB") for m in moves: if not m: @@ -80,28 +111,13 @@ def verify_wca_moves(moves): return True -def verify_solution(facelets, moves): - """Use kociemba to verify that moves solve the facelet string.""" - if not moves: - return False - try: - solution = kociemba.solve(facelets) - # Normalize: same move sequence up to equivalent representations - # For a quick check, just verify kociemba accepts the provided moves - # by concatenating and letting kociemba validate (it raises on invalid). - # Better: apply moves via kociemba pattern-solving? Just check length. - # Simpler heuristic: if moves are valid WCA and solver succeeded, trust it. - return True - except Exception as e: - print(f"kociemba verification error: {e}") - return False - - 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=len(TEST_FACELETS), help="Number of test cases") + 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") @@ -115,50 +131,68 @@ def main(): 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 - count = min(args.count, len(TEST_FACELETS)) - for i in range(count): - facelets = TEST_FACELETS[i] - print(f"\n=== Test {i+1}/{count} ===") + 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}") - # Validate the facelet itself with kociemba + # Sanity-check with kociemba try: canonical = kociemba.solve(facelets) - print(f"Canonical solution ({len(canonical.split())} moves): {canonical}") + print(f"Canonical ({len(canonical.split())} moves): {canonical}") except Exception as e: - print(f"FAIL: Facelet invalid according to kociemba: {e}") + print(f"FAIL: kociemba rejected facelets: {e}") all_passed = False continue - cahidenes_moves = solve_with_cahidenes(facelets, cahidenes_wrapper) - print(f"Cahidenes ({len(cahidenes_moves)} moves): {' '.join(cahidenes_moves)}") + cahidenes_moves = solve_with_wrapper(cahidenes_wrapper, facelets) + brownan_moves = solve_with_wrapper(brownan_wrapper, facelets) - brownan_moves = solve_with_brownan(facelets, brownan_wrapper) + 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 - continue if not verify_wca_moves(brownan_moves): print("FAIL: Brownan returned invalid WCA moves") - all_passed = False - continue - - cahidenes_ok = verify_solution(facelets, cahidenes_moves) - brownan_ok = verify_solution(facelets, brownan_moves) - - if not cahidenes_ok: - print("FAIL: Cahidenes solution failed kociemba verification") - all_passed = False - if not brownan_ok: - print("FAIL: Brownan solution failed kociemba verification") + brn_ok = False all_passed = False - if cahidenes_ok and brownan_ok: - length_diff = abs(len(cahidenes_moves) - len(brownan_moves)) - print(f"PASS: Both solvers valid; length diff = {length_diff}") + 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)