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/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
@@ -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
+