Add facelet-to-brownan 120-char conversion in brownan solver wrapper
This commit is contained in:
parent
65614be283
commit
93e3943827
1 changed files with 142 additions and 14 deletions
|
|
@ -2,9 +2,8 @@
|
|||
"""
|
||||
HyperTwist IPC wrapper for brownan GPL Rubik's Cube solver oracle.
|
||||
|
||||
Input: 120-character brownan raw cube string (NOT 54-char facelet).
|
||||
Brownan uses a cubie-based format documented at:
|
||||
https://github.com/brownan/Rubiks-Cube-Solver
|
||||
Input: Either a 54-character facelet string (kociemba order: U R F D L B)
|
||||
or a 120-character brownan raw cube string.
|
||||
|
||||
Output: Space-separated WCA move notation (U, D, F, B, L, R, with ' and 2 suffixes).
|
||||
Brownan native output uses faces F, T, L, B, D, R where T=U in WCA.
|
||||
|
|
@ -62,12 +61,10 @@ def ensure_tables(brownan_exe: str) -> bool:
|
|||
|
||||
def convert_move(brownan_move: str) -> str:
|
||||
"""Convert a single brownan move token to WCA notation."""
|
||||
# brownan tokens look like: "F ", "T' ", "2L "
|
||||
token = brownan_move.strip()
|
||||
if not token:
|
||||
return ""
|
||||
|
||||
# Handle double-turn prefix
|
||||
if token.startswith("2"):
|
||||
face = token[1]
|
||||
suffix = "2"
|
||||
|
|
@ -82,10 +79,144 @@ def convert_move(brownan_move: str) -> str:
|
|||
return f"{wca_face}{suffix}"
|
||||
|
||||
|
||||
def solve(raw_cube: str, brownan_exe: str = None) -> str:
|
||||
"""Solve a cube from a brownan 120-character raw string."""
|
||||
if len(raw_cube) != 120:
|
||||
print(f"ERROR: Brownan raw cube string must be 120 characters, got {len(raw_cube)}", file=sys.stderr)
|
||||
def facelet_to_brownan_raw(facelet: str) -> str:
|
||||
"""Convert a 54-character kociemba facelet string to brownan 120-char raw format."""
|
||||
if len(facelet) != 54:
|
||||
raise ValueError(f"Facelet string must be 54 characters, got {len(facelet)}")
|
||||
|
||||
# Map input face labels/colors to brownan's fixed color scheme based on centers.
|
||||
# Brownan fixed centers: U=r, L=b, F=w, R=g, B=y, D=o
|
||||
color_map = {
|
||||
facelet[4]: "r", # U center
|
||||
facelet[13]: "g", # R center
|
||||
facelet[22]: "w", # F center
|
||||
facelet[31]: "o", # D center
|
||||
facelet[40]: "b", # L center
|
||||
facelet[49]: "y", # B center
|
||||
}
|
||||
|
||||
if len(set(color_map.keys())) != 6:
|
||||
raise ValueError("Invalid facelet string: center pieces are not all distinct")
|
||||
|
||||
def remap(s: str) -> str:
|
||||
return "".join(color_map.get(c, c) for c in s)
|
||||
|
||||
# Build 48-position brownan layout string (1-indexed internally).
|
||||
# brownan layout (centers fixed):
|
||||
# 1 2 3
|
||||
# 4 R 5
|
||||
# 6 7 8
|
||||
# 9 10 11 12 13 14 15 16 17 18 19 20
|
||||
# 21 B 22 23 W 24 25 G 26 27 Y 28
|
||||
# 29 30 31 32 33 34 35 36 37 38 39 40
|
||||
# 41 42 43
|
||||
# 44 O 45
|
||||
# 46 47 48
|
||||
layout = [""] * 49
|
||||
|
||||
u = remap(facelet[0:9])
|
||||
layout[1], layout[2], layout[3] = u[0], u[1], u[2]
|
||||
layout[4], layout[5] = u[3], u[5]
|
||||
layout[6], layout[7], layout[8] = u[6], u[7], u[8]
|
||||
|
||||
r = remap(facelet[9:18])
|
||||
layout[15], layout[16], layout[17] = r[0], r[1], r[2]
|
||||
layout[25], layout[26] = r[3], r[5]
|
||||
layout[35], layout[36], layout[37] = r[6], r[7], r[8]
|
||||
|
||||
f = remap(facelet[18:27])
|
||||
layout[12], layout[13], layout[14] = f[0], f[1], f[2]
|
||||
layout[23], layout[24] = f[3], f[5]
|
||||
layout[32], layout[33], layout[34] = f[6], f[7], f[8]
|
||||
|
||||
d = remap(facelet[27:36])
|
||||
layout[41], layout[42], layout[43] = d[0], d[1], d[2]
|
||||
layout[44], layout[45] = d[3], d[5]
|
||||
layout[46], layout[47], layout[48] = d[6], d[7], d[8]
|
||||
|
||||
l = remap(facelet[36:45])
|
||||
layout[9], layout[10], layout[11] = l[0], l[1], l[2]
|
||||
layout[21], layout[22] = l[3], l[5]
|
||||
layout[29], layout[30], layout[31] = l[6], l[7], l[8]
|
||||
|
||||
# Back face may need mirroring/rotation in the unfolded net.
|
||||
# Direct mapping is used here; adjust if brownan rejects valid cubes.
|
||||
b = remap(facelet[45:54])
|
||||
layout[18], layout[19], layout[20] = b[0], b[1], b[2]
|
||||
layout[27], layout[28] = b[3], b[5]
|
||||
layout[38], layout[39], layout[40] = b[6], b[7], b[8]
|
||||
|
||||
# Original cube_convert.py prepends '@' because the author used wrong indexes.
|
||||
cc = "@" + "".join(layout[1:49])
|
||||
|
||||
# Extract 20 cubies exactly as cube_convert.py does.
|
||||
cubies = [
|
||||
"nn%s%s%sn" % (cc[29], cc[40], cc[46]),
|
||||
"nnn%s%sn" % (cc[39], cc[47]),
|
||||
"nnn%s%s%s" % (cc[38], cc[48], cc[37]),
|
||||
"nn%s%snn" % (cc[21], cc[28]),
|
||||
"nnn%sn%s" % (cc[27], cc[26]),
|
||||
"n%s%s%snn" % (cc[1], cc[9], cc[20]),
|
||||
"n%sn%snn" % (cc[2], cc[19]),
|
||||
"n%sn%sn%s" % (cc[3], cc[18], cc[17]),
|
||||
"nn%sn%sn" % (cc[30], cc[44]),
|
||||
"nnnn%s%s" % (cc[45], cc[36]),
|
||||
"n%s%snnn" % (cc[4], cc[10]),
|
||||
"n%snnn%s" % (cc[5], cc[16]),
|
||||
"%sn%sn%sn" % (cc[32], cc[31], cc[41]),
|
||||
"%snnn%sn" % (cc[33], cc[42]),
|
||||
"%snnn%s%s" % (cc[34], cc[43], cc[35]),
|
||||
"%sn%snnn" % (cc[23], cc[22]),
|
||||
"%snnnn%s" % (cc[24], cc[25]),
|
||||
"%s%s%snnn" % (cc[12], cc[6], cc[11]),
|
||||
"%s%snnnn" % (cc[13], cc[7]),
|
||||
"%s%snnn%s" % (cc[14], cc[8], cc[15]),
|
||||
]
|
||||
|
||||
def select(pred):
|
||||
matches = [c for c in cubies if pred(c)]
|
||||
if not matches:
|
||||
raise ValueError("Invalid cube: not all cubies accounted for")
|
||||
return matches[0]
|
||||
|
||||
fc = ""
|
||||
fc += select(lambda a: "y" in a and "b" in a and "o" in a)
|
||||
fc += select(lambda a: "y" in a and "o" in a and a.count("n") == 4)
|
||||
fc += select(lambda a: "y" in a and "o" in a and "g" in a)
|
||||
fc += select(lambda a: "y" in a and "b" in a and a.count("n") == 4)
|
||||
fc += select(lambda a: "y" in a and "g" in a and a.count("n") == 4)
|
||||
fc += select(lambda a: "y" in a and "b" in a and "r" in a)
|
||||
fc += select(lambda a: "y" in a and "r" in a and a.count("n") == 4)
|
||||
fc += select(lambda a: "y" in a and "r" in a and "g" in a)
|
||||
fc += select(lambda a: "o" in a and "b" in a and a.count("n") == 4)
|
||||
fc += select(lambda a: "o" in a and "g" in a and a.count("n") == 4)
|
||||
fc += select(lambda a: "b" in a and "r" in a and a.count("n") == 4)
|
||||
fc += select(lambda a: "g" in a and "r" in a and a.count("n") == 4)
|
||||
fc += select(lambda a: "w" in a and "b" in a and "o" in a)
|
||||
fc += select(lambda a: "w" in a and "o" in a and a.count("n") == 4)
|
||||
fc += select(lambda a: "w" in a and "o" in a and "g" in a)
|
||||
fc += select(lambda a: "w" in a and "b" in a and a.count("n") == 4)
|
||||
fc += select(lambda a: "w" in a and "g" in a and a.count("n") == 4)
|
||||
fc += select(lambda a: "w" in a and "b" in a and "r" in a)
|
||||
fc += select(lambda a: "w" in a and "r" in a and a.count("n") == 4)
|
||||
fc += select(lambda a: "w" in a and "r" in a and "g" in a)
|
||||
|
||||
if len(fc) != 120:
|
||||
raise ValueError(f"Conversion error: final code length is {len(fc)}, expected 120")
|
||||
|
||||
return fc
|
||||
|
||||
|
||||
def solve(cube_input: str, brownan_exe: str = None) -> str:
|
||||
"""Solve a cube from either a 54-char facelet or 120-char brownan raw string."""
|
||||
cube_input = cube_input.strip()
|
||||
|
||||
if len(cube_input) == 54:
|
||||
raw_cube = facelet_to_brownan_raw(cube_input)
|
||||
elif len(cube_input) == 120:
|
||||
raw_cube = cube_input
|
||||
else:
|
||||
print(f"ERROR: Input must be 54-char facelet or 120-char brownan raw, got {len(cube_input)}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if brownan_exe is None:
|
||||
|
|
@ -94,7 +225,6 @@ def solve(raw_cube: str, brownan_exe: str = None) -> str:
|
|||
print("ERROR: brownan-solver executable not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Optionally warn about missing tables (non-fatal)
|
||||
ensure_tables(brownan_exe)
|
||||
|
||||
exe_dir = os.path.dirname(os.path.abspath(brownan_exe))
|
||||
|
|
@ -113,7 +243,6 @@ def solve(raw_cube: str, brownan_exe: str = None) -> str:
|
|||
print(f"ERROR: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# brownan prints "Solution found!" and then moves, may continue searching deeper
|
||||
lines = result.stdout.splitlines()
|
||||
in_solution = False
|
||||
moves = []
|
||||
|
|
@ -125,7 +254,6 @@ def solve(raw_cube: str, brownan_exe: str = None) -> str:
|
|||
if in_solution:
|
||||
if not stripped:
|
||||
break
|
||||
# A solution line is a sequence of move tokens like "F T' 2L ..."
|
||||
for token in stripped.split():
|
||||
wca = convert_move(token)
|
||||
if wca:
|
||||
|
|
@ -137,9 +265,9 @@ def solve(raw_cube: str, brownan_exe: str = None) -> str:
|
|||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="HyperTwist Brownan Solver Wrapper")
|
||||
parser.add_argument("raw_cube", help="120-character brownan raw cube string")
|
||||
parser.add_argument("cube_input", help="54-char facelet string or 120-char brownan raw string")
|
||||
parser.add_argument("--exe", help="Path to brownan-solver executable")
|
||||
args = parser.parse_args()
|
||||
|
||||
result = solve(args.raw_cube.strip(), args.exe)
|
||||
result = solve(args.cube_input, args.exe)
|
||||
print(result)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue