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).
This commit is contained in:
parent
a87fd89762
commit
c2efdcaa86
2 changed files with 117 additions and 79 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue