-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathRubiksCube.py
378 lines (293 loc) · 13.1 KB
/
RubiksCube.py
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
"""
Rubik's Cube Simulator
This module implements a Rubik's Cube simulator using a group theory approach to represent the cube and its operations
using matrix algebra. The cube is represented as a 1D numpy array, and operations on the cube are represented as permutation
arrays. This allows for efficient and mathematically rigorous manipulation of the cube's state.
Classes:
RubiksCube: A class to represent and manipulate a Rubik's Cube, including methods for rotating, scrambling, undoing,
redoing moves, and checking if the cube is solved. It also supports saving and loading the cube's state.
Functions:
_site_pairs(S: int) -> Tuple[List[np.ndarray], List[np.ndarray]]: Generates pairs of site indices for rotating planes
about the x or z axes.
_create_rotation_operators(S: int) -> np.ndarray: Constructs rotation operators along the x, y, and z axes.
_create_win_permutations(rotation_operators: np.ndarray) -> np.ndarray: Generates permutation operators for solved states.
"""
import numpy as np
from copy import deepcopy
from typing import Optional, List, Tuple, Dict
class RubiksCube:
def __init__(self, S: int, num_scrambles: int = 0, seed: Optional[int] = None):
"""
Initializes the Rubik's Cube.
Parameters:
S (int): The size of the cube (e.g., 3 for a standard 3x3x3 cube).
num_scrambles (int): Number of random rotations to scramble the cube.
seed (Optional[int]): Random seed for reproducibility.
"""
self.size: int = S
self.base_seed: Optional[int] = seed
self._rotation_operators: np.ndarray = _create_rotation_operators(self.size)
self.solved_permutations: np.ndarray = _create_win_permutations(self._rotation_operators)
self.current_state: np.ndarray = np.arange(6 * self.size ** 2)
self.all_operations: List[Tuple[str, Tuple[int, int, int]]] = []
self.undone_operations: List[Tuple[str, Tuple[int, int, int]]] = []
self.undo_count: int = 0
self.check_count: int = 0
self.scramble_count: int = 0
self.saved_state: Optional[Dict[str, np.ndarray, List[List[Tuple[str, Tuple[int, int, int]]]], int]] = None
if num_scrambles > 0:
self.scramble(num_scrambles)
def scramble(self, num_scrambles: int) -> None:
"""
Scrambles the cube with a specified number of random rotations.
Parameters:
num_scrambles (int): Number of random rotations to scramble the cube.
"""
if self.base_seed is not None:
scramble_seed = self.base_seed + self.scramble_count
np.random.seed(scramble_seed)
self.scramble_count += 1
for _ in range(num_scrambles):
axis = np.random.choice([0, 1, 2])
plane = np.random.randint(0, self.size)
direction = np.random.choice([0, 1])
self._operate(axis, plane, direction)
self.all_operations.append(('s', (axis, plane, direction)))
def rotate(self, axis: int, plane: int, direction: int) -> None:
"""
Performs a single rotation operation on the cube.
Parameters:
axis (int): Axis of rotation (0 for x, 1 for y, 2 for z).
plane (int): Index of the plane to rotate.
direction (int): Direction of rotation (0 for counter-clockwise, 1 for clockwise).
Raises:
ValueError: If the axis, plane, or direction is invalid.
"""
if axis not in [0, 1, 2]:
raise ValueError(f"Invalid axis index: {axis}. Options are 0, 1, 2")
if plane not in range(self.size):
raise ValueError(
f"Invalid plane index: {plane}. Options are in closed integer interval [0, {self.size - 1}]")
if direction not in [0, 1]:
raise ValueError(f"Invalid direction: {direction}. Options are 0, 1")
self._operate(axis, plane, direction)
self.all_operations.append(('r', (axis, plane, direction)))
self.undone_operations.clear()
def undo(self, num_moves: int = 1) -> None:
"""
Undoes the specified number of moves.
Parameters:
num_moves (int): Number of moves to undo. Defaults to 1.
"""
if len(self.all_operations) == 0:
return
num_moves = min(num_moves, len(self.all_operations))
for _ in range(num_moves):
last_move = self.all_operations.pop()
move_type, (axis, plane, direction) = last_move
self._operate(axis, plane, 1 - direction)
self.undone_operations.append(last_move)
self.undo_count += 1
def redo(self, num_moves: int = 1) -> None:
"""
Redoes the specified number of undone moves.
Parameters:
num_moves (int): Number of moves to redo. Defaults to 1.
"""
if len(self.undone_operations) == 0:
return
num_moves = min(num_moves, len(self.undone_operations))
for _ in range(num_moves):
last_undone_move = self.undone_operations.pop()
move_type, (axis, plane, direction) = last_undone_move
self._operate(axis, plane, direction)
self.all_operations.append(last_undone_move)
def check(self) -> bool:
"""
Checks if the cube is in a solved state.
Returns:
bool: True if the cube is solved, False otherwise.
"""
self.check_count += 1
return np.any(np.all(self.solved_permutations == self.current_state, axis=1))
def reset(self) -> None:
"""
Resets the cube to the solved state and clears all operation logs.
"""
self.current_state = np.arange(6 * self.size ** 2)
self.all_operations = []
self.undone_operations = []
self.check_count = 0
self.undo_count = 0
def save_state(self) -> None:
"""
Saves the current state of the cube, including all logs and count variables.
"""
self.saved_state = {
'current_state': deepcopy(self.current_state),
'all_operations': deepcopy(self.all_operations),
'undone_operations': deepcopy(self.undone_operations),
'undo_count': self.undo_count,
'check_count': self.check_count,
'scramble_count': self.scramble_count
}
def load_state(self) -> None:
"""
Loads the saved state of the cube, including all logs and count variables.
Raises:
ValueError: If there is no saved state to load.
"""
if self.saved_state is None:
raise ValueError("No saved state to load.")
self.current_state = deepcopy(self.saved_state['current_state'])
self.all_operations = deepcopy(self.saved_state['all_operations'])
self.undone_operations = deepcopy(self.saved_state['undone_operations'])
self.undo_count = self.saved_state['undo_count']
self.check_count = self.saved_state['check_count']
self.scramble_count = self.saved_state['scramble_count']
def _operate(self, axis: int, plane: int, direction: int) -> None:
"""
Performs a rotation operation on the cube without validation.
Parameters:
axis (int): Axis of rotation (0 for x, 1 for y, 2 for z).
plane (int): Index of the plane to rotate.
direction (int): Direction of rotation (0 for counter-clockwise, 1 for clockwise).
"""
rotation_operator = self._rotation_operators[axis, direction, plane]
self.current_state = self.current_state[rotation_operator]
def _site_pairs(S: int) -> Tuple[List[np.ndarray], List[np.ndarray]]:
"""
Generate pairs of sites for z-face and x-face swaps for a Rubik's Cube of size S.
Parameters:
S (int): The size of the Rubik's Cube (number of squares along one edge).
Returns:
Tuple[List[np.ndarray], List[np.ndarray]]: Two lists containing arrays of site pairs for z-face and x-face swaps.
"""
z_face_swaps = np.array([[2, 0], [0, 3], [3, 1], [1, 2]])
x_face_swaps = np.array([[2, 5], [5, 3], [3, 4], [4, 2]])
site_pairs_z = []
site_pairs_x = []
for s in range(S):
these_z_swaps = []
these_x_swaps = []
for i in range(S):
for j in range(4):
pair_z = z_face_swaps[j]
pair_x = x_face_swaps[j]
these_z_swaps.append([[pair_z[0], s, i],
[pair_z[1], s, i]])
these_x_swaps.append([[pair_x[0], i, s],
[pair_x[1], i, s]])
if s == 0:
for p in range(S):
for q in range(S):
these_z_swaps.append([[4, p, q],
[4, q, p]])
these_x_swaps.append([[0, p, q],
[0, q, p]])
if s == S - 1:
for p in range(S):
for q in range(S):
these_z_swaps.append([[5, p, q],
[5, q, p]])
these_x_swaps.append([[1, p, q],
[1, q, p]])
site_pairs_z.append(np.array(these_z_swaps))
site_pairs_x.append(np.array(these_x_swaps))
return site_pairs_z, site_pairs_x
def _create_rotation_operators(S: int) -> np.ndarray:
"""
Create rotation operators for a Rubik's Cube of size S.
Parameters:
S (int): The size of the Rubik's Cube (number of squares along one edge).
Returns:
np.ndarray: A 3D array containing rotation operators and their inverses.
"""
def get_idx(f: int, r: int, c: int) -> int:
return f * S**2 + r * S + c
num_squares = 6 * S ** 2
site_pairs_z, site_pairs_x = _site_pairs(S)
Z = []
X = []
for n in range(S):
site_pairs_z_n = site_pairs_z[n]
site_pairs_x_n = site_pairs_x[n]
idx_pairs_z = []
idx_pairs_x = []
used_idxs_z = []
used_idxs_x = []
for this_pair in site_pairs_z_n:
f1, r1, c1 = this_pair[0]
f2, r2, c2 = this_pair[1]
idx1 = get_idx(f1, r1, c1)
idx2 = get_idx(f2, r2, c2)
idx_pairs_z.append([idx1, idx2])
if idx1 not in used_idxs_z:
used_idxs_z.append(idx1)
if idx2 not in used_idxs_z:
used_idxs_z.append(idx2)
for this_pair in site_pairs_x_n:
f1, r1, c1 = this_pair[0]
f2, r2, c2 = this_pair[1]
idx1 = get_idx(f1, r1, c1)
idx2 = get_idx(f2, r2, c2)
idx_pairs_x.append([idx1, idx2])
if idx1 not in used_idxs_x:
used_idxs_x.append(idx1)
if idx2 not in used_idxs_x:
used_idxs_x.append(idx2)
used_idxs_z = np.sort(np.array(used_idxs_z))
used_idxs_x = np.sort(np.array(used_idxs_x))
self_inds_z = np.delete(np.arange(num_squares), used_idxs_z)
self_inds_x = np.delete(np.arange(num_squares), used_idxs_x)
idx_pairs_z.extend(np.column_stack((self_inds_z, self_inds_z)).tolist())
idx_pairs_x.extend(np.column_stack((self_inds_x, self_inds_x)).tolist())
idx_pairs_z = np.array(idx_pairs_z).T
idx_pairs_x = np.array(idx_pairs_x).T
permuted_columns_z = idx_pairs_z[1][np.argsort(idx_pairs_z[0])]
permuted_columns_x = idx_pairs_x[1][np.argsort(idx_pairs_x[0])]
Z.append(permuted_columns_z)
X.append(permuted_columns_x)
Z, X = np.array(Z), np.array(X)
Y = []
Rx = np.arange(6 * S ** 2)
for i in range(S):
Rx = Rx[X[i]]
for i in range(S):
Y.append(Rx[Z[i][np.argsort(Rx)]])
X_inv = [np.argsort(X_i) for X_i in X]
Y_inv = [np.argsort(Y_i) for Y_i in Y]
Z_inv = [np.argsort(Z_i) for Z_i in Z]
rotation_operators = np.array([[X, X_inv], [Y, Y_inv], [Z, Z_inv]])
return rotation_operators
def _create_win_permutations(rotation_operators: np.ndarray) -> np.ndarray:
"""
Create winning permutations for a Rubik's Cube using the rotation operators.
Parameters:
rotation_operators (np.ndarray): A 3D array containing rotation operators and their inverses.
Returns:
np.ndarray: A 2D array containing winning permutations.
"""
X, Y, Z = rotation_operators[0][0], rotation_operators[1][0], rotation_operators[2][0]
S = len(X)
num_squares = rotation_operators.shape[-1]
I = np.arange(num_squares)
Rx = np.arange(num_squares)
Ry = np.arange(num_squares)
Rz = np.arange(num_squares)
for i in range(S):
Rx = Rx[X[i]]
Ry = Ry[Y[i]]
Rz = Rz[Z[i]]
permutation_operators = []
axis_rotations = [I, Rx[Rx], Rx, np.argsort(Rx), Ry, np.argsort(Ry)]
for permutation_operator in axis_rotations:
permutation_operators.append(permutation_operator)
permutation_operators.append(permutation_operator[Rz])
permutation_operators.append(permutation_operator[Rz[Rz]])
permutation_operators.append(permutation_operator[Rz[Rz[Rz]]])
return np.array(permutation_operators)
def main():
pass
if __name__ == '__main__':
main()