WeChall - Rubik's Cube

Challenge

页面展示一个 3D 魔方(需 JavaScript + YUI 渲染),noscript fallback 里有一段 54 位数字 cubestring。 可以通过 api.php?move=<notation> 接口提交魔方转动,将魔方还原为 solved 状态。

Solution

核心难点在于 WeChall 的 cubestring 编码与标准 kociemba facelet 编码之间的映射。

1. Cubestring 格式

54 位数字,面序为 [U, L, F, R, B, D](每面 9 位),数字 1-6 代表 6 种颜色。 Solved 状态 = 111111111222222222333333333444444444555555555666666666

noscript 标签中直接暴露当前状态:

1
2
3
<noscript id="cubestring"
>225311553624124563661633443251441126415453262654265133</noscript
>

2. 服务器端 Cube 语义

通过逆向 rubik.js 中的 CUBIE_MOVEMENTS 和移植 cube.php 的 move 函数,确认服务器使用以下面序和转动语义:

  • 面序:U(0-8) L(9-17) F(18-26) R(27-35) B(36-44) D(45-53)
  • 每面内部为行主序(row-major),无翻转
  • 转动通过 front() + Y()/X() 组合实现(right = Y front Y'up = X' front X 等)

3. Kociemba 映射

kociemba 的面序为 [U, R, F, D, L, B]。映射 = 面序重排,面内恒等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
KOC_ORDER = 'URFDLB'
SERVER_ORDER = 'ULFRBD'
M = []
for face in SERVER_ORDER:
kf = KOC_ORDER.index(face) * 9
M.extend(range(kf, kf + 9))

def cs_to_kociemba(cs):
out = ['?'] * 54
for i, ch in enumerate(cs):
out[M[i]] = ch
return ''.join(out)

def digit_to_letter(facelet):
centers = {i: facelet[i] for i in (4, 13, 22, 31, 40, 49)}
faces = {4: 'U', 13: 'R', 22: 'F', 31: 'D', 40: 'L', 49: 'B'}
d2l = {centers[i]: faces[i] for i in centers}
return ''.join(d2l[c] for c in facelet)

4. 完整 solve 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#!/usr/bin/env python3
"""WeChall Rubik's Cube solver: cubestring -> kociemba -> submit moves."""
import re
import urllib.request
import time

COOKIE = 'WC=YOUR_COOKIE_HERE'
BASE = 'https://www.wechall.net/en/challenge/space/rubikcube/api.php?move='
INDEX = 'https://www.wechall.net/en/challenge/space/rubikcube/index.php'

KOC_ORDER = 'URFDLB'
SERVER_ORDER = 'ULFRBD'
M = []
for face in SERVER_ORDER:
kf = KOC_ORDER.index(face) * 9
M.extend(range(kf, kf + 9))

def cs_to_kociemba(cs):
out = ['?'] * 54
for i, ch in enumerate(cs):
out[M[i]] = ch
return ''.join(out)

def digit_to_letter(facelet):
centers = {i: facelet[i] for i in (4, 13, 22, 31, 40, 49)}
faces = {4: 'U', 13: 'R', 22: 'F', 31: 'D', 40: 'L', 49: 'B'}
d2l = {centers[i]: faces[i] for i in centers}
return ''.join(d2l[c] for c in facelet)

# --- server Cube (ported from cube.php) ---

def rotate(cs, count=1):
moves = {0: 2, 2: 8, 8: 6, 6: 0, 1: 5, 5: 7, 7: 3, 3: 1}
out = cs
for _ in range(count):
ocube = out
lst = list(out)
for k, v in moves.items():
lst[v] = ocube[k]
out = ''.join(lst)
return out

class Cube:
def __init__(self, cube):
self.cube = cube

def move(self, moves):
for mv in moves.split(' '):
if mv == '':
continue
count = 3 if "'" in mv else (2 if '2' in mv else 1)
mv = mv[0]
funcs = {'F': 'front', 'R': 'right', 'B': 'back', 'U': 'up',
'L': 'left', 'D': 'down'}
if mv in funcs:
for _ in range(count):
getattr(self, funcs[mv])()

def isSolved(self):
a = sorted([self.cube[i*9:(i+1)*9] for i in range(6)])
b = sorted([str(i+1)*9 for i in range(6)])
return a == b

def front(self):
c = self.cube
m = {6: 27, 7: 30, 8: 33, 27: 47, 30: 46, 33: 45,
45: 11, 46: 14, 47: 17, 11: 8, 14: 7, 17: 6}
lst = list(c)
for k, v in m.items():
lst[v] = c[k]
lst[18:27] = rotate(''.join(lst[18:27]))
self.cube = ''.join(lst)

def Y(self):
self.cube = (rotate(self.cube[0:9], 1) +
self.cube[18:27] + self.cube[27:36] +
self.cube[36:45] + self.cube[9:18] +
rotate(self.cube[45:54], 3))

def X(self):
self.cube = (self.cube[2*9:3*9] +
rotate(self.cube[1*9:2*9], 3) +
self.cube[5*9:6*9] +
rotate(self.cube[3*9:4*9], 1) +
rotate(self.cube[0*9:1*9], 2) +
rotate(self.cube[4*9:5*9], 2))

def right(self):
self.Y(); self.front(); self.Y(); self.Y(); self.Y()

def back(self):
self.Y(); self.Y(); self.front(); self.Y(); self.Y()

def left(self):
self.Y(); self.Y(); self.Y(); self.front(); self.Y()

def up(self):
self.X(); self.X(); self.X(); self.front(); self.X()

def down(self):
self.X(); self.front(); self.X(); self.X(); self.X()

# --- main ---

import kociemba

# 1. Fetch current cubestring
req = urllib.request.Request(INDEX, headers={'Cookie': COOKIE})
body = urllib.request.urlopen(req, timeout=30).read().decode()
cs = re.search(r'<noscript id="cubestring">([0-9]+)</noscript>', body).group(1)
print(f"Cubestring: {cs}")

# 2. Convert to kociemba facelet and solve
kf = digit_to_letter(cs_to_kociemba(cs))
sol = kociemba.solve(kf)
moves = sol.split()
print(f"Solution ({len(moves)} moves): {sol}")

# 3. Verify locally
c = Cube(cs)
c.move(sol)
assert c.isSolved(), "Local verification failed!"

# 4. Submit moves to server
for i, mv in enumerate(moves):
url = BASE + mv.replace("'", "%27")
req = urllib.request.Request(url, headers={'Cookie': COOKIE})
resp = urllib.request.urlopen(req, timeout=30).read().decode()
text = re.sub(r'<[^>]+>', ' ', resp)
print(f" [{i+1}/{len(moves)}] {mv} -> {' '.join(text.split())[:80]}")
if 'solved' in resp.lower():
print("*** SOLVED! ***")
break
time.sleep(0.3)

# 5. Verify on /en/challs
req = urllib.request.Request('https://www.wechall.net/en/challs',
headers={'Cookie': COOKIE})
challs = urllib.request.urlopen(req, timeout=30).read().decode()
idx = challs.find('Rubik')
if idx >= 0 and 'wc_chall_solved' in challs[idx-300:idx+300]:
print("Confirmed: Rubik's Cube SOLVED on WeChall!")