cube-model 1.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cube_model/__init__.py +86 -0
- cube_model/action.py +189 -0
- cube_model/model.py +99 -0
- cube_model/move.py +167 -0
- cube_model/navigation.py +155 -0
- cube_model/new_cube.py +299 -0
- cube_model/py.typed +0 -0
- cube_model/reachable.py +194 -0
- cube_model/rotate.py +104 -0
- cube_model/utils.py +58 -0
- cube_model-1.0.3.dist-info/METADATA +45 -0
- cube_model-1.0.3.dist-info/RECORD +13 -0
- cube_model-1.0.3.dist-info/WHEEL +4 -0
cube_model/new_cube.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
'''Construct a Cube from explicit corner and edge color sequences.'''
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from random import randrange, shuffle
|
|
5
|
+
|
|
6
|
+
from .model import (
|
|
7
|
+
Color,
|
|
8
|
+
CornerSticker,
|
|
9
|
+
Cube,
|
|
10
|
+
EdgeSticker,
|
|
11
|
+
Side,
|
|
12
|
+
)
|
|
13
|
+
from .utils import even_permutation, rand_elt, to_radix
|
|
14
|
+
|
|
15
|
+
_OPPOSITE: dict[Color, Color] = {
|
|
16
|
+
Color.WHITE: Color.YELLOW,
|
|
17
|
+
Color.YELLOW: Color.WHITE,
|
|
18
|
+
Color.RED: Color.ORANGE,
|
|
19
|
+
Color.ORANGE: Color.RED,
|
|
20
|
+
Color.BLUE: Color.GREEN,
|
|
21
|
+
Color.GREEN: Color.BLUE,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
def new_cube(
|
|
25
|
+
corners: Sequence[tuple[Color, Color, Color]],
|
|
26
|
+
edges: Sequence[tuple[Color, Color]],
|
|
27
|
+
front_color: Color,
|
|
28
|
+
top_color: Color,
|
|
29
|
+
initial: Cube | None = None,
|
|
30
|
+
) -> Cube:
|
|
31
|
+
'''Construct a Cube from explicit corner and edge color sequences.
|
|
32
|
+
|
|
33
|
+
corners is 8 tuples: the 4 front-face corners starting at home
|
|
34
|
+
clockwise, then the 4 back-face corners starting at the corner
|
|
35
|
+
opposite home clockwise when facing the back. The first element
|
|
36
|
+
of each tuple is the color of the sticker on the front or back
|
|
37
|
+
face.
|
|
38
|
+
|
|
39
|
+
edges is 12 tuples: 4 front-face edges starting just after home
|
|
40
|
+
clockwise, then 4 middle-layer edges starting next to the home
|
|
41
|
+
corner then clockwise when facing the front, then 4 back-face
|
|
42
|
+
edges starting just after the corner opposite home clockwise when
|
|
43
|
+
facing the back. The first element of each tuple is the color of
|
|
44
|
+
the sticker touching the front or back face, or the leading
|
|
45
|
+
sticker for the middle layer.
|
|
46
|
+
|
|
47
|
+
If initial is supplied, its sticker objects are reused instead of
|
|
48
|
+
allocating new ones. Each directed sticker is uniquely identified
|
|
49
|
+
by (sticker.color, sticker.other.color).
|
|
50
|
+
'''
|
|
51
|
+
existing_edges: dict[tuple[Color, Color], EdgeSticker]
|
|
52
|
+
existing_corners: dict[tuple[Color, Color], CornerSticker]
|
|
53
|
+
if initial is not None:
|
|
54
|
+
existing_edges = {
|
|
55
|
+
(es.color, es.other.color): es
|
|
56
|
+
for es in initial.next_corner.keys()
|
|
57
|
+
}
|
|
58
|
+
existing_corners = {
|
|
59
|
+
(cs.color, cs.other.color): cs
|
|
60
|
+
for cs in initial.next_edge.keys()
|
|
61
|
+
}
|
|
62
|
+
else:
|
|
63
|
+
existing_edges = {}
|
|
64
|
+
existing_corners = {}
|
|
65
|
+
|
|
66
|
+
edge_stickers: dict[tuple[Color, Color], EdgeSticker] = {}
|
|
67
|
+
corner_stickers: dict[tuple[Color, Color], CornerSticker] = {}
|
|
68
|
+
|
|
69
|
+
def make_edge(a: Color, b: Color) -> None:
|
|
70
|
+
'''Create or reuse one edge cubie with two stickers.'''
|
|
71
|
+
if (a, b) in existing_edges:
|
|
72
|
+
s_ab: EdgeSticker = existing_edges[(a, b)]
|
|
73
|
+
s_ba: EdgeSticker = existing_edges[(b, a)]
|
|
74
|
+
else:
|
|
75
|
+
s_ab = EdgeSticker(a)
|
|
76
|
+
s_ba = EdgeSticker(b)
|
|
77
|
+
s_ab._rewire(s_ba)
|
|
78
|
+
s_ba._rewire(s_ab)
|
|
79
|
+
edge_stickers[(a, b)] = s_ab
|
|
80
|
+
edge_stickers[(b, a)] = s_ba
|
|
81
|
+
|
|
82
|
+
et: tuple[Color, Color]
|
|
83
|
+
for et in edges:
|
|
84
|
+
make_edge(*et)
|
|
85
|
+
|
|
86
|
+
next_edge: dict[CornerSticker, EdgeSticker] = {}
|
|
87
|
+
next_corner: dict[EdgeSticker, CornerSticker] = {}
|
|
88
|
+
|
|
89
|
+
def make_corner(a: Color, b: Color, c: Color) -> CornerSticker:
|
|
90
|
+
'''Create or reuse a corner cubie with three stickers. Return
|
|
91
|
+
the first sticker of the cubie.'''
|
|
92
|
+
if (a, b) in existing_corners:
|
|
93
|
+
s: tuple[CornerSticker, CornerSticker, CornerSticker] = (
|
|
94
|
+
existing_corners[(a, b)],
|
|
95
|
+
existing_corners[(b, c)],
|
|
96
|
+
existing_corners[(c, a)],
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
s = (
|
|
100
|
+
CornerSticker(a),
|
|
101
|
+
CornerSticker(b),
|
|
102
|
+
CornerSticker(c),
|
|
103
|
+
)
|
|
104
|
+
s[0]._rewire(s[1])
|
|
105
|
+
s[1]._rewire(s[2])
|
|
106
|
+
s[2]._rewire(s[0])
|
|
107
|
+
corner_stickers[(a, b)] = s[0]
|
|
108
|
+
corner_stickers[(b, c)] = s[1]
|
|
109
|
+
corner_stickers[(c, a)] = s[2]
|
|
110
|
+
return s[0]
|
|
111
|
+
|
|
112
|
+
home: CornerSticker = make_corner(*corners[0])
|
|
113
|
+
ct: tuple[Color, Color, Color]
|
|
114
|
+
for ct in corners[1:]:
|
|
115
|
+
make_corner(*ct)
|
|
116
|
+
|
|
117
|
+
cs: CornerSticker
|
|
118
|
+
es: EdgeSticker
|
|
119
|
+
i: int
|
|
120
|
+
|
|
121
|
+
# wire next links along front face
|
|
122
|
+
for i in range(4):
|
|
123
|
+
ct = corners[i]
|
|
124
|
+
cs = corner_stickers[ct[:2]]
|
|
125
|
+
|
|
126
|
+
et = edges[i]
|
|
127
|
+
es = edge_stickers[et]
|
|
128
|
+
next_edge[cs] = es
|
|
129
|
+
next_corner[es.other] = cs.other.other
|
|
130
|
+
|
|
131
|
+
et = edges[(i + 3) % 4]
|
|
132
|
+
es = edge_stickers[et]
|
|
133
|
+
next_edge[cs.other] = es.other
|
|
134
|
+
next_corner[es] = cs
|
|
135
|
+
|
|
136
|
+
et = edges[i + 4]
|
|
137
|
+
es = edge_stickers[et]
|
|
138
|
+
next_edge[cs.other.other] = es
|
|
139
|
+
next_corner[es.other] = cs.other
|
|
140
|
+
|
|
141
|
+
# wire next links along back face
|
|
142
|
+
for i in range(4, 8):
|
|
143
|
+
ct = corners[i]
|
|
144
|
+
cs = corner_stickers[ct[:2]]
|
|
145
|
+
|
|
146
|
+
et = edges[i + 4]
|
|
147
|
+
es = edge_stickers[et]
|
|
148
|
+
next_edge[cs] = es
|
|
149
|
+
next_corner[es.other] = cs.other.other
|
|
150
|
+
|
|
151
|
+
et = edges[(i + 3) % 4 + 8]
|
|
152
|
+
es = edge_stickers[et]
|
|
153
|
+
next_edge[cs.other] = es.other
|
|
154
|
+
next_corner[es] = cs
|
|
155
|
+
|
|
156
|
+
et = edges[3 * i % 4 + 4]
|
|
157
|
+
es = edge_stickers[et]
|
|
158
|
+
next_edge[cs.other.other] = es.other
|
|
159
|
+
next_corner[es] = cs.other
|
|
160
|
+
|
|
161
|
+
return Cube(
|
|
162
|
+
home=home,
|
|
163
|
+
front_color=front_color,
|
|
164
|
+
top_color=top_color,
|
|
165
|
+
next_edge=next_edge,
|
|
166
|
+
next_corner=next_corner,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Solved-state sequences.
|
|
170
|
+
# Corners: front face CW from home, then back face CW facing back
|
|
171
|
+
# from opposite-home. Tuple[0] is the front or back face sticker.
|
|
172
|
+
_SOLVED_CORNERS: list[tuple[Color, Color, Color]] = [
|
|
173
|
+
(Color.GREEN, Color.ORANGE, Color.WHITE),
|
|
174
|
+
(Color.GREEN, Color.WHITE, Color.RED),
|
|
175
|
+
(Color.GREEN, Color.RED, Color.YELLOW),
|
|
176
|
+
(Color.GREEN, Color.YELLOW, Color.ORANGE),
|
|
177
|
+
(Color.BLUE, Color.WHITE, Color.ORANGE),
|
|
178
|
+
(Color.BLUE, Color.ORANGE, Color.YELLOW),
|
|
179
|
+
(Color.BLUE, Color.YELLOW, Color.RED),
|
|
180
|
+
(Color.BLUE, Color.RED, Color.WHITE),
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
# Edges: front face CW after home, middle layer CW facing front from
|
|
184
|
+
# next-to-home, back face CW after opposite-home. Tuple[0] is the
|
|
185
|
+
# front/back sticker or the leading middle-layer sticker.
|
|
186
|
+
_SOLVED_EDGES: list[tuple[Color, Color]] = [
|
|
187
|
+
(Color.GREEN, Color.WHITE),
|
|
188
|
+
(Color.GREEN, Color.RED),
|
|
189
|
+
(Color.GREEN, Color.YELLOW),
|
|
190
|
+
(Color.GREEN, Color.ORANGE),
|
|
191
|
+
(Color.WHITE, Color.ORANGE),
|
|
192
|
+
(Color.RED, Color.WHITE),
|
|
193
|
+
(Color.YELLOW, Color.RED),
|
|
194
|
+
(Color.ORANGE, Color.YELLOW),
|
|
195
|
+
(Color.BLUE, Color.ORANGE),
|
|
196
|
+
(Color.BLUE, Color.YELLOW),
|
|
197
|
+
(Color.BLUE, Color.RED),
|
|
198
|
+
(Color.BLUE, Color.WHITE),
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
_ALL_COLORS: list[Color] = list(Color)
|
|
202
|
+
|
|
203
|
+
_GB: frozenset[Color] = frozenset({Color.GREEN, Color.BLUE })
|
|
204
|
+
_OR: frozenset[Color] = frozenset({Color.ORANGE, Color.RED })
|
|
205
|
+
_WY: frozenset[Color] = frozenset({Color.WHITE, Color.YELLOW})
|
|
206
|
+
|
|
207
|
+
def center_parity_even(front_color: Color, top_color: Color) -> bool:
|
|
208
|
+
'''Return True if the center orientation is an even permutation.
|
|
209
|
+
|
|
210
|
+
The center permutation is even when the front and top colors are in
|
|
211
|
+
one of the three cyclic-neighbor pairs: (GB, WY), (WY, OR), (OR, GB),
|
|
212
|
+
where GB = {Green, Blue}, WY = {White, Yellow}, OR = {Orange, Red}.
|
|
213
|
+
'''
|
|
214
|
+
return (
|
|
215
|
+
front_color in _GB and top_color in _WY
|
|
216
|
+
or front_color in _WY and top_color in _OR
|
|
217
|
+
or front_color in _OR and top_color in _GB)
|
|
218
|
+
|
|
219
|
+
def solved(initial: Cube | None = None) -> Cube:
|
|
220
|
+
'''Construct a solved cube.
|
|
221
|
+
|
|
222
|
+
If initial is supplied, reuse its sticker objects instead of
|
|
223
|
+
allocating new ones.
|
|
224
|
+
'''
|
|
225
|
+
return new_cube(
|
|
226
|
+
corners=_SOLVED_CORNERS,
|
|
227
|
+
edges=_SOLVED_EDGES,
|
|
228
|
+
front_color=Color.GREEN,
|
|
229
|
+
top_color=Color.WHITE,
|
|
230
|
+
initial=initial,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def shuffled(initial: Cube | None = None) -> Cube:
|
|
234
|
+
'''Construct a solvable randomly shuffled cube.
|
|
235
|
+
|
|
236
|
+
The front color is chosen uniformly at random. To ensure a valid
|
|
237
|
+
cube, the top color is chosen uniformly at random only from the
|
|
238
|
+
four colors that are neither the front color nor its opposite.
|
|
239
|
+
|
|
240
|
+
Corners and edges are permuted randomly. To ensure solvability, the
|
|
241
|
+
first two edges are transposed if needed so that the corner, edge,
|
|
242
|
+
and center permutation parities satisfy the three-way XOR invariant.
|
|
243
|
+
|
|
244
|
+
Edge orientations: a random integer in [0, 2**11) determines which
|
|
245
|
+
of the first 11 edges are flipped. To ensure solvability, the 12th
|
|
246
|
+
is flipped if needed to keep the total number of flips even.
|
|
247
|
+
|
|
248
|
+
Corner orientations: a random integer in [0, 3**7) is interpreted
|
|
249
|
+
in base 3 to twist the first 7 corners. To ensure solvability,
|
|
250
|
+
the 8th corner is twisted so that the total twist sum is
|
|
251
|
+
divisible by 3.
|
|
252
|
+
|
|
253
|
+
If initial is supplied, its sticker objects are reused.
|
|
254
|
+
|
|
255
|
+
'''
|
|
256
|
+
front_color: Color = rand_elt(_ALL_COLORS)
|
|
257
|
+
top_color: Color = rand_elt([c for c in _ALL_COLORS
|
|
258
|
+
if c is not front_color
|
|
259
|
+
and c is not _OPPOSITE[front_color]])
|
|
260
|
+
|
|
261
|
+
corners: list[tuple[Color, Color, Color]] = list(_SOLVED_CORNERS)
|
|
262
|
+
edges: list[tuple[Color, Color]] = list(_SOLVED_EDGES)
|
|
263
|
+
|
|
264
|
+
shuffle(corners)
|
|
265
|
+
shuffle(edges)
|
|
266
|
+
|
|
267
|
+
corner_even: bool = even_permutation(corners, _SOLVED_CORNERS)
|
|
268
|
+
edge_even: bool = even_permutation(edges, _SOLVED_EDGES)
|
|
269
|
+
center_even: bool = center_parity_even(front_color, top_color)
|
|
270
|
+
if not (corner_even ^ edge_even ^ center_even):
|
|
271
|
+
edges[0], edges[1] = edges[1], edges[0]
|
|
272
|
+
|
|
273
|
+
edge_bits: int = randrange(2 ** 11)
|
|
274
|
+
flips: int = 0
|
|
275
|
+
for i in range(11):
|
|
276
|
+
if edge_bits & (1 << i):
|
|
277
|
+
edges[i] = (edges[i][1], edges[i][0])
|
|
278
|
+
flips += 1
|
|
279
|
+
if flips % 2 == 1:
|
|
280
|
+
edges[11] = (edges[11][1], edges[11][0])
|
|
281
|
+
|
|
282
|
+
twists: list[int] = list(to_radix(3, randrange(3 ** 7)))
|
|
283
|
+
twists.append(2 * sum(twists) % 3)
|
|
284
|
+
for i, twist in enumerate(twists):
|
|
285
|
+
match twist:
|
|
286
|
+
case 1:
|
|
287
|
+
a, b, c = corners[i]
|
|
288
|
+
corners[i] = (c, a, b)
|
|
289
|
+
case 2:
|
|
290
|
+
a, b, c = corners[i]
|
|
291
|
+
corners[i] = (b, c, a)
|
|
292
|
+
|
|
293
|
+
return new_cube(
|
|
294
|
+
corners=corners,
|
|
295
|
+
edges=edges,
|
|
296
|
+
front_color=front_color,
|
|
297
|
+
top_color=top_color,
|
|
298
|
+
initial=initial,
|
|
299
|
+
)
|
cube_model/py.typed
ADDED
|
File without changes
|
cube_model/reachable.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
'''Reachability test for the cube.
|
|
2
|
+
|
|
3
|
+
A cube position is reachable from the solved position by face moves
|
|
4
|
+
if and only if the three criteria checked here are all satisfied.
|
|
5
|
+
'''
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Iterator
|
|
10
|
+
|
|
11
|
+
from .new_cube import solved, center_parity_even
|
|
12
|
+
from .model import (
|
|
13
|
+
Color,
|
|
14
|
+
CornerSticker,
|
|
15
|
+
Cube,
|
|
16
|
+
EdgeSticker,
|
|
17
|
+
Side,
|
|
18
|
+
)
|
|
19
|
+
from .navigation import (
|
|
20
|
+
corner_on_side,
|
|
21
|
+
side_color,
|
|
22
|
+
side_corners,
|
|
23
|
+
sticker_side,
|
|
24
|
+
)
|
|
25
|
+
from .utils import even_permutation
|
|
26
|
+
|
|
27
|
+
_PRIORITY: list[Color] = [
|
|
28
|
+
Color.BLUE, Color.GREEN, Color.WHITE, Color.YELLOW,
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
_WY: frozenset[Color] = frozenset({Color.WHITE, Color.YELLOW})
|
|
32
|
+
|
|
33
|
+
# Frozenset of the three colors of a corner cubie.
|
|
34
|
+
type _Cubie3 = frozenset[Color]
|
|
35
|
+
# Frozenset of the two colors of an edge cubie.
|
|
36
|
+
type _Cubie2 = frozenset[Color]
|
|
37
|
+
|
|
38
|
+
def _all_corners(cube: Cube) -> Iterator[CornerSticker]:
|
|
39
|
+
'''Yield one sticker per corner cubie via navigation.
|
|
40
|
+
|
|
41
|
+
Yields the 4 front-face corners followed by the 4 back-face corners.
|
|
42
|
+
'''
|
|
43
|
+
yield from side_corners(cube, cube.home)
|
|
44
|
+
yield from side_corners(cube, corner_on_side(cube, Side.BACK))
|
|
45
|
+
|
|
46
|
+
def _all_edges(cube: Cube) -> Iterator[EdgeSticker]:
|
|
47
|
+
'''Yield one sticker per edge cubie via navigation.
|
|
48
|
+
|
|
49
|
+
For front-face corners: N gives the 4 front-ring edges,
|
|
50
|
+
OON gives the 4 middle-layer edges.
|
|
51
|
+
For back-face corners: N gives the 4 back-ring edges.
|
|
52
|
+
'''
|
|
53
|
+
front: list[CornerSticker] = side_corners(cube, cube.home)
|
|
54
|
+
back: list[CornerSticker] = side_corners(
|
|
55
|
+
cube, corner_on_side(cube, Side.BACK),
|
|
56
|
+
)
|
|
57
|
+
for c in front:
|
|
58
|
+
yield cube.next_edge[c]
|
|
59
|
+
for c in front:
|
|
60
|
+
yield cube.next_edge[c.other.other]
|
|
61
|
+
for c in back:
|
|
62
|
+
yield cube.next_edge[c]
|
|
63
|
+
|
|
64
|
+
def _corner_cubie(cs: CornerSticker) -> _Cubie3:
|
|
65
|
+
'''Return the frozenset of colors of a corner cubie.'''
|
|
66
|
+
return frozenset({cs.color, cs.other.color, cs.other.other.color})
|
|
67
|
+
|
|
68
|
+
def _edge_cubie(es: EdgeSticker) -> _Cubie2:
|
|
69
|
+
'''Return the frozenset of colors of an edge cubie.'''
|
|
70
|
+
return frozenset({es.color, es.other.color})
|
|
71
|
+
|
|
72
|
+
_ref: Cube = solved()
|
|
73
|
+
_SOLVED_CORNERS: list[_Cubie3] = [
|
|
74
|
+
_corner_cubie(cs) for cs in _all_corners(_ref)
|
|
75
|
+
]
|
|
76
|
+
_SOLVED_EDGES: list[_Cubie2] = [
|
|
77
|
+
_edge_cubie(es) for es in _all_edges(_ref)
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
def locations_ok(cube: Cube) -> bool:
|
|
81
|
+
'''Return True if the corner, edge, and center permutation parities
|
|
82
|
+
satisfy the reachability invariant.
|
|
83
|
+
|
|
84
|
+
Every face move flips all three parities simultaneously, so their
|
|
85
|
+
XOR is conserved. In the solved state all three are even, so XOR is
|
|
86
|
+
True; locations_ok checks that the XOR remains True.
|
|
87
|
+
'''
|
|
88
|
+
corner_even: bool = even_permutation(
|
|
89
|
+
[_corner_cubie(cs) for cs in _all_corners(cube)],
|
|
90
|
+
_SOLVED_CORNERS,
|
|
91
|
+
)
|
|
92
|
+
edge_even: bool = even_permutation(
|
|
93
|
+
[_edge_cubie(es) for es in _all_edges(cube)],
|
|
94
|
+
_SOLVED_EDGES,
|
|
95
|
+
)
|
|
96
|
+
center_even: bool = center_parity_even(cube.front_color, cube.top_color)
|
|
97
|
+
return corner_even ^ edge_even ^ center_even
|
|
98
|
+
|
|
99
|
+
def _rep_sticker(es: EdgeSticker) -> EdgeSticker:
|
|
100
|
+
'''Return the representative sticker of an edge cubie.
|
|
101
|
+
|
|
102
|
+
The representative sticker is the one whose color appears earliest
|
|
103
|
+
in the priority order: blue, green, white, yellow.
|
|
104
|
+
'''
|
|
105
|
+
for color in _PRIORITY:
|
|
106
|
+
if es.color == color:
|
|
107
|
+
return es
|
|
108
|
+
if es.other.color == color:
|
|
109
|
+
return es.other
|
|
110
|
+
raise ValueError(
|
|
111
|
+
f'no priority color found in edge cubie: '
|
|
112
|
+
f'{es.color}, {es.other.color}'
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def _rep_face(cube: Cube, es: EdgeSticker) -> Side:
|
|
116
|
+
'''Return the representative face of an edge cubie.
|
|
117
|
+
|
|
118
|
+
The representative face is the one of the two faces on which the
|
|
119
|
+
cubie's stickers lie whose center color appears earliest in the
|
|
120
|
+
priority order: blue, green, white, yellow.
|
|
121
|
+
'''
|
|
122
|
+
sides: list[Side] = [
|
|
123
|
+
sticker_side(cube, es),
|
|
124
|
+
sticker_side(cube, es.other),
|
|
125
|
+
]
|
|
126
|
+
for color in _PRIORITY:
|
|
127
|
+
for side in sides:
|
|
128
|
+
if side_color(cube, side) == color:
|
|
129
|
+
return side
|
|
130
|
+
raise ValueError(
|
|
131
|
+
f'no priority color found among faces of edge cubie: '
|
|
132
|
+
f'{es.color}, {es.other.color}'
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def edge_flips_ok(cube: Cube) -> bool:
|
|
136
|
+
'''Return True if the number of flipped edge cubies is even.
|
|
137
|
+
|
|
138
|
+
An edge cubie is flipped if its representative sticker is not on
|
|
139
|
+
its representative face.
|
|
140
|
+
'''
|
|
141
|
+
flipped: int = sum(
|
|
142
|
+
1
|
|
143
|
+
for es in _all_edges(cube)
|
|
144
|
+
if sticker_side(cube, _rep_sticker(es)) != _rep_face(cube, es)
|
|
145
|
+
)
|
|
146
|
+
return flipped % 2 == 0
|
|
147
|
+
|
|
148
|
+
def _wy_sticker(cs: CornerSticker) -> CornerSticker:
|
|
149
|
+
'''Return the white-or-yellow sticker of a corner cubie.'''
|
|
150
|
+
s: CornerSticker = cs
|
|
151
|
+
for _ in range(3):
|
|
152
|
+
if s.color in _WY:
|
|
153
|
+
return s
|
|
154
|
+
s = s.other
|
|
155
|
+
raise ValueError(
|
|
156
|
+
f'no white/yellow sticker in corner cubie: '
|
|
157
|
+
f'{cs.color}, {cs.other.color}, {cs.other.other.color}'
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def _corner_rotation(cube: Cube, cs: CornerSticker) -> int:
|
|
161
|
+
'''Return the rotation number (0, 1, or 2) of a corner cubie.
|
|
162
|
+
|
|
163
|
+
Let r be the white-or-yellow sticker of the cubie.
|
|
164
|
+
If r is on a white/yellow face, rotation is 0.
|
|
165
|
+
If r.other is on a white/yellow face, rotation is 1.
|
|
166
|
+
Otherwise rotation is 2.
|
|
167
|
+
'''
|
|
168
|
+
r: CornerSticker = _wy_sticker(cs)
|
|
169
|
+
if side_color(cube, sticker_side(cube, r)) in _WY:
|
|
170
|
+
return 0
|
|
171
|
+
if side_color(cube, sticker_side(cube, r.other)) in _WY:
|
|
172
|
+
return 1
|
|
173
|
+
return 2
|
|
174
|
+
|
|
175
|
+
def corner_rotations_ok(cube: Cube) -> bool:
|
|
176
|
+
'''Return True if the sum of corner rotation numbers is divisible by 3.'''
|
|
177
|
+
total: int = sum(
|
|
178
|
+
_corner_rotation(cube, cs) for cs in _all_corners(cube)
|
|
179
|
+
)
|
|
180
|
+
return total % 3 == 0
|
|
181
|
+
|
|
182
|
+
def reachable(cube: Cube) -> bool:
|
|
183
|
+
'''Return True if the cube is reachable from the solved position by moves.
|
|
184
|
+
|
|
185
|
+
A cube is reachable if and only if:
|
|
186
|
+
- corner and edge permutations have the same parity,
|
|
187
|
+
- the number of flipped edge cubies is even, and
|
|
188
|
+
- the sum of corner rotation numbers is divisible by three.
|
|
189
|
+
'''
|
|
190
|
+
return (
|
|
191
|
+
locations_ok(cube)
|
|
192
|
+
and edge_flips_ok(cube)
|
|
193
|
+
and corner_rotations_ok(cube)
|
|
194
|
+
)
|
cube_model/rotate.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'''Rigid rotation of the cube.'''
|
|
2
|
+
|
|
3
|
+
from .new_cube import solved
|
|
4
|
+
from .model import (
|
|
5
|
+
Color,
|
|
6
|
+
Cube,
|
|
7
|
+
Side,
|
|
8
|
+
opp_side,
|
|
9
|
+
shallow_copy,
|
|
10
|
+
)
|
|
11
|
+
from .move import Move, Multiplicity, invert
|
|
12
|
+
from .navigation import (
|
|
13
|
+
Nav,
|
|
14
|
+
nav_cc,
|
|
15
|
+
parse_navs,
|
|
16
|
+
side_color,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
HOME_TRANSITIONS: dict[Move, list[Nav]] = {
|
|
20
|
+
Move(Side.FRONT, Multiplicity.CW): parse_navs('ONNO'),
|
|
21
|
+
Move(Side.FRONT, Multiplicity.CCW): parse_navs('NN'),
|
|
22
|
+
Move(Side.FRONT, Multiplicity.TWO): parse_navs('NNNN'),
|
|
23
|
+
Move(Side.RIGHT, Multiplicity.CW): parse_navs('ONNOO'),
|
|
24
|
+
Move(Side.RIGHT, Multiplicity.CCW): parse_navs('OONN'),
|
|
25
|
+
Move(Side.RIGHT, Multiplicity.TWO): parse_navs('ONNNNOO'),
|
|
26
|
+
Move(Side.TOP, Multiplicity.CW): parse_navs('NNOO'),
|
|
27
|
+
Move(Side.TOP, Multiplicity.CCW): parse_navs('OONNO'),
|
|
28
|
+
Move(Side.TOP, Multiplicity.TWO): parse_navs('OONNNNO'),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for _side in (Side.LEFT, Side.BOTTOM, Side.BACK):
|
|
32
|
+
for _mult in Multiplicity:
|
|
33
|
+
HOME_TRANSITIONS[Move(_side, _mult)] = HOME_TRANSITIONS[
|
|
34
|
+
Move(opp_side[_side], invert[_mult])
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
COLOR_TRANSITIONS: dict[tuple[Move, Color, Color], tuple[Color, Color]] = {}
|
|
38
|
+
|
|
39
|
+
def _build_color_transitions() -> None:
|
|
40
|
+
'''Populate COLOR_TRANSITIONS dynamically using side_color.'''
|
|
41
|
+
dummy: Cube = solved()
|
|
42
|
+
opp_color: dict[Color, Color] = {
|
|
43
|
+
side_color(dummy, side): side_color(dummy, opp)
|
|
44
|
+
for side, opp in opp_side.items()
|
|
45
|
+
}
|
|
46
|
+
m_f_cw = Move(Side.FRONT, Multiplicity.CW)
|
|
47
|
+
m_f_ccw = Move(Side.FRONT, Multiplicity.CCW)
|
|
48
|
+
m_f_two = Move(Side.FRONT, Multiplicity.TWO)
|
|
49
|
+
m_r_cw = Move(Side.RIGHT, Multiplicity.CW)
|
|
50
|
+
m_r_ccw = Move(Side.RIGHT, Multiplicity.CCW)
|
|
51
|
+
m_r_two = Move(Side.RIGHT, Multiplicity.TWO)
|
|
52
|
+
m_t_cw = Move(Side.TOP, Multiplicity.CW)
|
|
53
|
+
m_t_ccw = Move(Side.TOP, Multiplicity.CCW)
|
|
54
|
+
m_t_two = Move(Side.TOP, Multiplicity.TWO)
|
|
55
|
+
for f in Color:
|
|
56
|
+
for t in Color:
|
|
57
|
+
if f == t or t == opp_color[f]:
|
|
58
|
+
continue
|
|
59
|
+
dummy.front_color = f
|
|
60
|
+
dummy.top_color = t
|
|
61
|
+
left_c: Color = side_color(dummy, Side.LEFT)
|
|
62
|
+
right_c: Color = side_color(dummy, Side.RIGHT)
|
|
63
|
+
bottom_c: Color = side_color(dummy, Side.BOTTOM)
|
|
64
|
+
back_c: Color = side_color(dummy, Side.BACK)
|
|
65
|
+
COLOR_TRANSITIONS[(m_f_cw, f, t)] = (f, left_c)
|
|
66
|
+
COLOR_TRANSITIONS[(m_f_ccw, f, t)] = (f, right_c)
|
|
67
|
+
COLOR_TRANSITIONS[(m_f_two, f, t)] = (f, bottom_c)
|
|
68
|
+
COLOR_TRANSITIONS[(m_r_cw, f, t)] = (bottom_c, f)
|
|
69
|
+
COLOR_TRANSITIONS[(m_r_ccw, f, t)] = (t, back_c)
|
|
70
|
+
COLOR_TRANSITIONS[(m_r_two, f, t)] = (back_c, bottom_c)
|
|
71
|
+
COLOR_TRANSITIONS[(m_t_cw, f, t)] = (right_c, t)
|
|
72
|
+
COLOR_TRANSITIONS[(m_t_ccw, f, t)] = (left_c, t)
|
|
73
|
+
COLOR_TRANSITIONS[(m_t_two, f, t)] = (back_c, t)
|
|
74
|
+
for side in (Side.LEFT, Side.BOTTOM, Side.BACK):
|
|
75
|
+
for mult in Multiplicity:
|
|
76
|
+
opp: Side = opp_side[side]
|
|
77
|
+
inv: Multiplicity = invert[mult]
|
|
78
|
+
for f in Color:
|
|
79
|
+
for t in Color:
|
|
80
|
+
if f == t or t == opp_color[f]:
|
|
81
|
+
continue
|
|
82
|
+
m: Move = Move(side, mult)
|
|
83
|
+
opp_m: Move = Move(opp, inv)
|
|
84
|
+
COLOR_TRANSITIONS[(m, f, t)] = (
|
|
85
|
+
COLOR_TRANSITIONS[(opp_m, f, t)]
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
_build_color_transitions()
|
|
89
|
+
|
|
90
|
+
def rotate(move: Move, cube: Cube) -> None:
|
|
91
|
+
'''Rotate the cube rigidly in-place.'''
|
|
92
|
+
steps: list[Nav] = HOME_TRANSITIONS[move]
|
|
93
|
+
cube.home = nav_cc(steps, cube, cube.home)
|
|
94
|
+
new_f, new_t = COLOR_TRANSITIONS[
|
|
95
|
+
(move, cube.front_color, cube.top_color)
|
|
96
|
+
]
|
|
97
|
+
cube.front_color = new_f
|
|
98
|
+
cube.top_color = new_t
|
|
99
|
+
|
|
100
|
+
def rotated(move: Move, cube: Cube) -> Cube:
|
|
101
|
+
'''Return a new cube after rotating it rigidly.'''
|
|
102
|
+
new_cube: Cube = shallow_copy(cube)
|
|
103
|
+
rotate(move, new_cube)
|
|
104
|
+
return new_cube
|
cube_model/utils.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'''Permutation utilities for cube analysis.'''
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Hashable, Iterator, Sequence
|
|
6
|
+
from random import randrange
|
|
7
|
+
|
|
8
|
+
def permutation_cycles[T: Hashable](
|
|
9
|
+
start: Sequence[T],
|
|
10
|
+
end: Sequence[T],
|
|
11
|
+
) -> list[list[T]]:
|
|
12
|
+
'''Return the cycle decomposition of the permutation from start to end.
|
|
13
|
+
|
|
14
|
+
Both sequences must contain the same set of distinct elements.
|
|
15
|
+
The returned cycles are disjoint and cover all elements that move.
|
|
16
|
+
Fixed points are omitted.
|
|
17
|
+
'''
|
|
18
|
+
index: dict[T, int] = {v: i for i, v in enumerate(end)}
|
|
19
|
+
visited: set[int] = set()
|
|
20
|
+
cycles: list[list[T]] = []
|
|
21
|
+
for i, v in enumerate(start):
|
|
22
|
+
if i in visited:
|
|
23
|
+
continue
|
|
24
|
+
j: int = index[v]
|
|
25
|
+
if i == j:
|
|
26
|
+
visited.add(i)
|
|
27
|
+
continue
|
|
28
|
+
cycle: list[T] = []
|
|
29
|
+
k: int = i
|
|
30
|
+
while k not in visited:
|
|
31
|
+
visited.add(k)
|
|
32
|
+
cycle.append(start[k])
|
|
33
|
+
k = index[start[k]]
|
|
34
|
+
cycles.append(cycle)
|
|
35
|
+
return cycles
|
|
36
|
+
|
|
37
|
+
def even_permutation[T: Hashable](
|
|
38
|
+
start: Sequence[T],
|
|
39
|
+
end: Sequence[T],
|
|
40
|
+
) -> bool:
|
|
41
|
+
'''Return True if the permutation from start to end is even.
|
|
42
|
+
|
|
43
|
+
A permutation is even if the number of cycles of even length is even.
|
|
44
|
+
'''
|
|
45
|
+
cycles: list[list[T]] = permutation_cycles(start, end)
|
|
46
|
+
even_count: int = sum(
|
|
47
|
+
1 for c in cycles if len(c) % 2 == 0
|
|
48
|
+
)
|
|
49
|
+
return even_count % 2 == 0
|
|
50
|
+
|
|
51
|
+
def to_radix(base: int, n: int) -> Iterator[int]:
|
|
52
|
+
'''Little-endian representation of n in the given base'''
|
|
53
|
+
while n > 0:
|
|
54
|
+
yield n % base
|
|
55
|
+
n //= base
|
|
56
|
+
|
|
57
|
+
def rand_elt[T](seq: Sequence[T]) -> T:
|
|
58
|
+
return seq[randrange(len(seq))]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cube-model
|
|
3
|
+
Version: 1.0.3
|
|
4
|
+
Summary: Orientation-independent cube model in strictly typed Python
|
|
5
|
+
Author: Yitzchak Gale
|
|
6
|
+
Author-email: gale@sefer.org
|
|
7
|
+
Requires-Python: >=3.13,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
11
|
+
Project-URL: Homepage, https://github.com/ygale/cube-model
|
|
12
|
+
Project-URL: Repository, https://github.com/ygale/cube-model
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# cube-model
|
|
16
|
+
|
|
17
|
+
An orientation-independent cube model in strictly typed Python.
|
|
18
|
+
|
|
19
|
+
## Highlights
|
|
20
|
+
|
|
21
|
+
- Strict typing (`mypy --strict`)
|
|
22
|
+
- Orientation-independent cube state
|
|
23
|
+
- `Color` and `Side` are enums
|
|
24
|
+
- `CornerSticker` objects form circular linked lists of size 3
|
|
25
|
+
- `EdgeSticker` objects form circular linked lists of size 2
|
|
26
|
+
- `Cube.next_edge` and `Cube.next_corner` encode clockwise sticker order on faces
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install cube-model
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Example
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from cube_model import Color, Move, Multiplicity, Side, solved, move
|
|
38
|
+
|
|
39
|
+
cube = solved()
|
|
40
|
+
assert cube.front_color is Color.GREEN
|
|
41
|
+
|
|
42
|
+
move(Move(Side.FRONT, Multiplicity.CW), cube)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
|