@houstonp/rubiks-cube 1.5.2 → 2.1.0

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.
Files changed (51) hide show
  1. package/README.md +280 -170
  2. package/package.json +33 -3
  3. package/src/camera/cameraState.js +81 -0
  4. package/src/core.js +447 -0
  5. package/src/cube/animationSlice.js +205 -0
  6. package/src/cube/animationState.js +96 -0
  7. package/src/cube/cubeSettings.js +19 -0
  8. package/src/cube/cubeState.js +285 -139
  9. package/src/cube/stickerState.js +188 -0
  10. package/src/debouncer.js +16 -0
  11. package/src/globals.ts +9 -0
  12. package/src/index.js +621 -0
  13. package/src/settings.js +138 -0
  14. package/src/three/centerPiece.js +44 -0
  15. package/src/three/cornerPiece.js +60 -0
  16. package/src/three/cube.js +492 -0
  17. package/src/three/edgePiece.js +50 -0
  18. package/src/three/sticker.js +37 -0
  19. package/tests/common.js +27 -0
  20. package/tests/cube.five.test.js +126 -0
  21. package/tests/cube.four.test.js +126 -0
  22. package/tests/cube.seven.test.js +126 -0
  23. package/tests/cube.six.test.js +126 -0
  24. package/tests/cube.three.test.js +151 -0
  25. package/tests/cube.two.test.js +125 -0
  26. package/tests/setup.js +36 -0
  27. package/types/camera/cameraState.d.ts +19 -0
  28. package/types/core.d.ts +454 -0
  29. package/types/cube/animationSlice.d.ts +26 -0
  30. package/types/cube/animationState.d.ts +41 -0
  31. package/types/cube/cubeSettings.d.ts +17 -0
  32. package/types/cube/cubeState.d.ts +47 -0
  33. package/types/cube/stickerState.d.ts +21 -0
  34. package/types/debouncer.d.ts +13 -0
  35. package/types/globals.d.ts +7 -0
  36. package/types/index.d.ts +87 -0
  37. package/types/settings.d.ts +38 -0
  38. package/types/three/centerPiece.d.ts +15 -0
  39. package/types/three/cornerPiece.d.ts +24 -0
  40. package/types/three/cube.d.ts +130 -0
  41. package/types/three/edgePiece.d.ts +16 -0
  42. package/types/three/sticker.d.ts +15 -0
  43. package/.prettierrc +0 -7
  44. package/index.js +0 -274
  45. package/src/cube/cube.js +0 -276
  46. package/src/cube/cubeRotation.js +0 -63
  47. package/src/threejs/materials.js +0 -42
  48. package/src/threejs/pieces.js +0 -103
  49. package/src/threejs/stickers.js +0 -48
  50. package/src/utils/debouncer.js +0 -7
  51. package/src/utils/rotation.js +0 -53
@@ -0,0 +1,126 @@
1
+ // @ts-check
2
+ import './setup.js';
3
+ import { describe, it, expect, test } from 'bun:test';
4
+ import { CubeTypes, Movements, Rotations } from '../src/core.js';
5
+ import { toKociemba } from '../src/cube/stickerState.js';
6
+ import { createTestCube, drainUpdates } from './common.js';
7
+
8
+ describe('5x5 Tests', () => {
9
+ it('Valid initial Kociemba state string', () => {
10
+ //Arange
11
+ const cube = createTestCube(CubeTypes.Five);
12
+
13
+ //Act
14
+ const state = toKociemba(cube._cubeInfo.initialStickerState);
15
+
16
+ //Assert
17
+ expect(state.length).toBe(150);
18
+ for (const ch of state) {
19
+ expect('UDLRFB').toContain(ch);
20
+ }
21
+
22
+ const counts = {
23
+ U: 0,
24
+ D: 0,
25
+ L: 0,
26
+ R: 0,
27
+ F: 0,
28
+ B: 0,
29
+ };
30
+ for (const ch of state) {
31
+ const face = /** @type {"R" | "U" | "F" | "L" | "D" | "B"} */ (ch);
32
+ counts[face]++;
33
+ }
34
+ expect(Object.values(counts).every((c) => c === 25)).toBe(true);
35
+ });
36
+
37
+ it('reset method returns the cube to the initial state', () => {
38
+ // Arrange
39
+ const cube = createTestCube(CubeTypes.Five);
40
+ const initialState = toKociemba(cube._cubeInfo.initialStickerState);
41
+ /** @type {string?} */
42
+ let completedState = null;
43
+ /** @type {string?} */
44
+ let failedReason = null;
45
+ cube.movement(
46
+ Movements.Single.R,
47
+ (state) => {
48
+ completedState = state;
49
+ return true;
50
+ },
51
+ (reason) => {
52
+ failedReason = reason;
53
+ return true;
54
+ },
55
+ );
56
+ drainUpdates(cube);
57
+
58
+ // Act
59
+ /** @type {string?} */
60
+ let resetState = null;
61
+ cube.reset((state) => {
62
+ resetState = state;
63
+ return true;
64
+ });
65
+ drainUpdates(cube);
66
+
67
+ // Assert
68
+ expect(failedReason).toBeNull();
69
+ expect(completedState).not.toBeNull();
70
+ expect(completedState).not.toBe(initialState);
71
+ expect(/** @type {string?} **/ (resetState)).toBe(initialState);
72
+ });
73
+
74
+ it('solve', () => {
75
+ // Arrange
76
+ const cube = createTestCube(CubeTypes.Five);
77
+ const scramble =
78
+ "Dw Uw' U' Fw' L Fw R' U2 Rw Dw Fw' L' Lw D Dw U2 Bw' U' D Lw R B Lw2 U D' Uw R' L F B2 Lw2 R2 Dw U2 Rw' Lw' R D2 Fw Uw L' Bw B2 D2 Fw' Rw D U2 Lw' Fw2 Lw' Uw2 B2 Rw' F2 Rw2 L' D F' Rw";
79
+ const scrambleMoves = /** @type {import('../src/core.js').Movement[]} */ (scramble.split(' '));
80
+
81
+ for (const move of scrambleMoves) {
82
+ cube.movement(
83
+ move,
84
+ () => {},
85
+ () => {
86
+ throw new Error('Movement failed unexpectedly');
87
+ },
88
+ );
89
+ drainUpdates(cube);
90
+ }
91
+
92
+ // Act
93
+ const solution =
94
+ "z' y r U b 3r' z' U r2 U' r' U' r z x' 3r U' 3r' z' x' D r' F' r U' U' r' z r U' r' z' y' r U2' r' z x' F' U' r x' l R u' R2' u D' R U r' U' D' x' U' R' U' r2 3r2' 4r U x' L r' U2' r2 3r2' L' D' x L2 U 4r U' r' r' U' r U' U' 4r' U' 3r2 U2' 3r2' 4r2 U r 4r 4r' U' r U U r' 4r r' 4r U' 4r2' r2 U' r' 4r U' r' 4r U2 r U' r2' U2' r2 4r' U' r' 3r U 3r' r2 U r' z' U L 3d' U F R' F' R 3u' D' R U R U' R2' F R' F' R u y R U' R' u' d U' R U' R' u2' R' U R2 U' R' u 3u' U' U' U R U' R' y' R U R' F R' F' R d R U' R' 3u' R' F R F' R U' R' d R U R' u2' R' U' R2 U' R' y' 4d R U' R' u' 3u R U' R' u d' R U R' 3u' U F R' F' R U' d U' R' U R U' R' U' R U' L' U L y' R' U2' R U' U 4d D' L' U L D U R U' R' U R U' R' 4d' U' F' R U R' U' R' F R2 U R' U R U2' R' L' U' L F L' U' L U L F' L2' U L U x2 y'";
95
+ const solutionActions = /** @type {(import('../src/core.js').Movement | import('../src/core.js').Rotation)[]} */ (solution.split(' '));
96
+
97
+ let finalState = null;
98
+ for (const action of solutionActions) {
99
+ if (action.includes('x') || action.includes('y') || action.includes('z')) {
100
+ cube.rotate(
101
+ /** @type {import('../src/core.js').Rotation} */ (action),
102
+ (state) => {
103
+ finalState = state;
104
+ },
105
+ () => {
106
+ throw new Error('Rotation failed unexpectedly');
107
+ },
108
+ );
109
+ } else {
110
+ cube.movement(
111
+ /** @type {import('../src/core.js').Movement} */ (action),
112
+ (state) => {
113
+ finalState = state;
114
+ },
115
+ () => {
116
+ throw new Error('Movement failed unexpectedly');
117
+ },
118
+ );
119
+ }
120
+ drainUpdates(cube);
121
+ }
122
+
123
+ // Assert
124
+ expect(/** @type {string?} **/ (finalState)).toBe(toKociemba(cube._cubeInfo.initialStickerState));
125
+ });
126
+ });
@@ -0,0 +1,126 @@
1
+ // @ts-check
2
+ import './setup.js';
3
+ import { describe, it, expect, test } from 'bun:test';
4
+ import { CubeTypes, Movements, Rotations } from '../src/core.js';
5
+ import { toKociemba } from '../src/cube/stickerState.js';
6
+ import { createTestCube, drainUpdates } from './common.js';
7
+
8
+ describe('4x4 Tests', () => {
9
+ it('Valid initial Kociemba state string', () => {
10
+ //Arange
11
+ const cube = createTestCube(CubeTypes.Four);
12
+
13
+ //Act
14
+ const state = toKociemba(cube._cubeInfo.initialStickerState);
15
+
16
+ //Assert
17
+ expect(state.length).toBe(96);
18
+ for (const ch of state) {
19
+ expect('UDLRFB').toContain(ch);
20
+ }
21
+
22
+ const counts = {
23
+ U: 0,
24
+ D: 0,
25
+ L: 0,
26
+ R: 0,
27
+ F: 0,
28
+ B: 0,
29
+ };
30
+ for (const ch of state) {
31
+ const face = /** @type {"R" | "U" | "F" | "L" | "D" | "B"} */ (ch);
32
+ counts[face]++;
33
+ }
34
+ expect(Object.values(counts).every((c) => c === 16)).toBe(true);
35
+ });
36
+
37
+ it('reset method returns the cube to the initial state', () => {
38
+ // Arrange
39
+ const cube = createTestCube(CubeTypes.Four);
40
+ const initialState = toKociemba(cube._cubeInfo.initialStickerState);
41
+ /** @type {string?} */
42
+ let completedState = null;
43
+ /** @type {string?} */
44
+ let failedReason = null;
45
+ cube.movement(
46
+ Movements.Single.R,
47
+ (state) => {
48
+ completedState = state;
49
+ return true;
50
+ },
51
+ (reason) => {
52
+ failedReason = reason;
53
+ return true;
54
+ },
55
+ );
56
+ drainUpdates(cube);
57
+
58
+ // Act
59
+ /** @type {string?} */
60
+ let resetState = null;
61
+ cube.reset((state) => {
62
+ resetState = state;
63
+ return true;
64
+ });
65
+ drainUpdates(cube);
66
+
67
+ // Assert
68
+ expect(failedReason).toBeNull();
69
+ expect(completedState).not.toBeNull();
70
+ expect(completedState).not.toBe(initialState);
71
+ expect(/** @type {string?} **/ (resetState)).toBe(initialState);
72
+ });
73
+
74
+ it('solve', () => {
75
+ // Arrange
76
+ const cube = createTestCube(CubeTypes.Four);
77
+ const scramble =
78
+ "D2 F D2 F U2 L2 F R2 F2 D2 L2 R B L' B' L' D' F U Uw2 Rw2 L' Fw2 L' F' L' B' Uw2 B Rw2 F' L Uw' B' U Rw2 D U2 Fw' Rw' B' F2 Uw' B2 L'";
79
+ const scrambleMoves = /** @type {import('../src/core.js').Movement[]} */ (scramble.split(' '));
80
+
81
+ for (const move of scrambleMoves) {
82
+ cube.movement(
83
+ move,
84
+ () => {},
85
+ () => {
86
+ throw new Error('Movement failed unexpectedly');
87
+ },
88
+ );
89
+ drainUpdates(cube);
90
+ }
91
+
92
+ // Act
93
+ const solution =
94
+ "z r U' r' F U r' y u' u' U2 l' U2 l z' x' r2' F U2 x' U' U' r2' 3r2 B U' r' F' 3r 3r' U' U' r2 U' r2' 3r2 U' 3r' r U2 r' U' U' r2 U' U' r2' 3r2 U r' U' 3r' r2 U2' r' x' y' x' u' U' R' U' R u D' F D u' R' U' R 3d U' U L' U L d U' R U' R' u' U R U' R' u U' R U' R' u' U' y' R' U' R u U' R U R' L U L' 3d R U' R' U' R U' R' U R U' R' y R U' R' U R U' R' 3d' U' F' U F U' L' U2 R U R' U2 L R' U R' U' R3 U' R' U R U R2' z2";
95
+ const solutionActions = /** @type {(import('../src/core.js').Movement | import('../src/core.js').Rotation)[]} */ (solution.split(' '));
96
+
97
+ let finalState = null;
98
+ for (const action of solutionActions) {
99
+ if (action.includes('x') || action.includes('y') || action.includes('z')) {
100
+ cube.rotate(
101
+ /** @type {import('../src/core.js').Rotation} */ (action),
102
+ (state) => {
103
+ finalState = state;
104
+ },
105
+ () => {
106
+ throw new Error('Rotation failed unexpectedly');
107
+ },
108
+ );
109
+ } else {
110
+ cube.movement(
111
+ /** @type {import('../src/core.js').Movement} */ (action),
112
+ (state) => {
113
+ finalState = state;
114
+ },
115
+ () => {
116
+ throw new Error('Movement failed unexpectedly');
117
+ },
118
+ );
119
+ }
120
+ drainUpdates(cube);
121
+ }
122
+
123
+ // Assert
124
+ expect(/** @type {string?} **/ (finalState)).toBe(toKociemba(cube._cubeInfo.initialStickerState));
125
+ });
126
+ });
@@ -0,0 +1,126 @@
1
+ // @ts-check
2
+ import './setup.js';
3
+ import { describe, it, expect, test } from 'bun:test';
4
+ import { CubeTypes, Movements, Rotations } from '../src/core.js';
5
+ import { toKociemba } from '../src/cube/stickerState.js';
6
+ import { createTestCube, drainUpdates } from './common.js';
7
+
8
+ describe('7x7 Tests', () => {
9
+ it('Valid initial Kociemba state string', () => {
10
+ //Arange
11
+ const cube = createTestCube(CubeTypes.Seven);
12
+
13
+ //Act
14
+ const state = toKociemba(cube._cubeInfo.initialStickerState);
15
+
16
+ //Assert
17
+ expect(state.length).toBe(294);
18
+ for (const ch of state) {
19
+ expect('UDLRFB').toContain(ch);
20
+ }
21
+
22
+ const counts = {
23
+ U: 0,
24
+ D: 0,
25
+ L: 0,
26
+ R: 0,
27
+ F: 0,
28
+ B: 0,
29
+ };
30
+ for (const ch of state) {
31
+ const face = /** @type {"R" | "U" | "F" | "L" | "D" | "B"} */ (ch);
32
+ counts[face]++;
33
+ }
34
+ expect(Object.values(counts).every((c) => c === 49)).toBe(true);
35
+ });
36
+
37
+ it('reset method returns the cube to the initial state', () => {
38
+ // Arrange
39
+ const cube = createTestCube(CubeTypes.Seven);
40
+ const initialState = toKociemba(cube._cubeInfo.initialStickerState);
41
+ /** @type {string?} */
42
+ let completedState = null;
43
+ /** @type {string?} */
44
+ let failedReason = null;
45
+ cube.movement(
46
+ Movements.Single.R,
47
+ (state) => {
48
+ completedState = state;
49
+ return true;
50
+ },
51
+ (reason) => {
52
+ failedReason = reason;
53
+ return true;
54
+ },
55
+ );
56
+ drainUpdates(cube);
57
+
58
+ // Act
59
+ /** @type {string?} */
60
+ let resetState = null;
61
+ cube.reset((state) => {
62
+ resetState = state;
63
+ return true;
64
+ });
65
+ drainUpdates(cube);
66
+
67
+ // Assert
68
+ expect(failedReason).toBeNull();
69
+ expect(completedState).not.toBeNull();
70
+ expect(completedState).not.toBe(initialState);
71
+ expect(/** @type {string?} **/ (resetState)).toBe(initialState);
72
+ });
73
+
74
+ it('solve', () => {
75
+ // Arrange
76
+ const cube = createTestCube(CubeTypes.Seven);
77
+ const scramble =
78
+ "B' Rw' Lw2 3Rw2 Fw' 3Rw' 3Uw2 Lw2 3Lw' 3Rw B Rw2 U 3Bw' 3Fw2 U' Bw F' Uw R' Lw 3Fw' Fw2 R' 3Rw' 3Uw 3Rw' Fw' 3Lw' R2 B Uw' Rw2 Bw Uw' F2 R B Fw' Rw' Bw 3Bw 3Uw2 3Dw' L2 D2 R 3Rw 3Uw2 3Lw' 3Fw Fw2 B' Rw' 3Rw Lw R U' Rw' 3Bw' 3Rw2 3Bw2 Fw U2 Uw B2 Dw' Lw' Dw2 Rw2 Lw2 Dw Rw' D L' 3Bw 3Fw2 D 3Fw2 3Bw' Rw 3Fw2 Uw2 3Fw2 3Rw L' D 3Bw' 3Fw2 3Lw Rw 3Bw D2 3Uw U 3Dw2 3Rw Rw 3Lw2 3Uw'";
79
+ const scrambleMoves = /** @type {import('../src/core.js').Movement[]} */ (scramble.split(' '));
80
+
81
+ for (const move of scrambleMoves) {
82
+ cube.movement(
83
+ move,
84
+ () => {},
85
+ () => {
86
+ throw new Error('Movement failed unexpectedly');
87
+ },
88
+ );
89
+ drainUpdates(cube);
90
+ }
91
+
92
+ // Act
93
+ const solution =
94
+ "x' y 4r 3r' U' 4r' 3r 5r U 5r' U' 5r U F r U r' z' F' 4r U' 4r' 3r r' U' r U' r' U' x' 4r' x' U' r2' 5r2 U x' l2' z U' 3r 4r' z' r' z x' 4r U 4r' 3r U x2' 3r' x' D r2 y' U' x' D' U' r' F r2 3r' r U r' U x 3r' U' 3r U' y x' 3r U 4r' x2 U x' U' 4r' z' U2 4l' U2 4l z U x' U' 3r' z' z U' R R' u' R2 u U' x' x' 4r2 U' U' 4r2' x x U r U' r' U x2 U' x' l' 3l' l U2 3l 4l' l' B' l U x l2 F z' 4l' U2 3r z y' U' x' U 4l x' U' 4r' R' F x' l2' x' D' x' R' L' 3l U 5r2 3r2' x' x' r' L F U 6r2 U' 3l' L x' l2' L U' x' 6r U x' L' U' r' 6r U' 6r' 5r U 4r' U' 3r' U' 4r U' 3r' U 3r U' r' U' 5r' 6r' 6r' 3r r' U' r U' r' U' U2 4r x L U' U' 3r U' U' r 6r' U' 6r2 4r2' 3r2 U' 4r r' U' r' U' r' 4r 3r' U 6r' 4r U' 4r' U' r U r' U x' L' U' 5r2 U' 5r2' U' r U r' 6r l' x' 4r2 3r2' U' 4r2 3r2' 5r 4r' U' 4r U' 3r U' 3r' r U r2' 3r U' 6r' r 3r' 4r U' 4r' U' 3r U 3r' 5r 4r' U' l' 3l r U' r U' U' r2' U' 6r r' U' r 6r' U 5r U 4r' U 4r 5r' U' 4r U' r' U r U' 4r' 3r U' 3r' U' 3r U 3r' U' 3r U' 3r' U' r U' U' r' U' r U' r' U r U' U' r' U' r U 4r 3r' U' r' U 4r' 3r 6r2' U R U 3r 6r' U' U' R' U R U r' R2' 6r U R U' x2' U' R U 5r' x' 3l2' U F U' F' 6r 4r 5r R' U R U' 5r' R U' R' U D R' D' r' 3r2 x' x' D R' D' 4r r' R R' F R F' x' U' R U r 3r2' 5r2 x' R' D R' D' U R U' 3r 4r2' 5r U' R U x r l2 3l2' R' U R U' x' 4r 3r' R' U R U' 4r' x 3r' r 4r' U' R' U 6l' U R U' 4r 6l' U' R' U 6l U R U' 3r' U' R' U 6l' U R U' 4l 3l 3l' l R' U' R U 4r' U' R' U 6l' U U' U R U' l' 3l x' 3r U' U 3r' r U' R' U 6l' U R U' r' z' y' R' F R 6d R' U L U L' D U2 R U R' D' R U' R' U R U' R' y U2 R U' R' U R' F R F' L' U L L' U L U L' U L U' x R' U R' D D R U' R' D D R2 y2 x'";
95
+ const solutionActions = /** @type {(import('../src/core.js').Movement | import('../src/core.js').Rotation)[]} */ (solution.split(' '));
96
+
97
+ let finalState = null;
98
+ for (const action of solutionActions) {
99
+ if (action.includes('x') || action.includes('y') || action.includes('z')) {
100
+ cube.rotate(
101
+ /** @type {import('../src/core.js').Rotation} */ (action),
102
+ (state) => {
103
+ finalState = state;
104
+ },
105
+ () => {
106
+ throw new Error('Rotation failed unexpectedly');
107
+ },
108
+ );
109
+ } else {
110
+ cube.movement(
111
+ /** @type {import('../src/core.js').Movement} */ (action),
112
+ (state) => {
113
+ finalState = state;
114
+ },
115
+ () => {
116
+ throw new Error('Movement failed unexpectedly');
117
+ },
118
+ );
119
+ }
120
+ drainUpdates(cube);
121
+ }
122
+
123
+ // Assert
124
+ expect(/** @type {string?} **/ (finalState)).toBe(toKociemba(cube._cubeInfo.initialStickerState));
125
+ });
126
+ });
@@ -0,0 +1,126 @@
1
+ // @ts-check
2
+ import './setup.js';
3
+ import { describe, it, expect, test } from 'bun:test';
4
+ import { CubeTypes, Movements, Rotations } from '../src/core.js';
5
+ import { toKociemba } from '../src/cube/stickerState.js';
6
+ import { createTestCube, drainUpdates } from './common.js';
7
+
8
+ describe('6x6 Tests', () => {
9
+ it('Valid initial Kociemba state string', () => {
10
+ //Arange
11
+ const cube = createTestCube(CubeTypes.Six);
12
+
13
+ //Act
14
+ const state = toKociemba(cube._cubeInfo.initialStickerState);
15
+
16
+ //Assert
17
+ expect(state.length).toBe(216);
18
+ for (const ch of state) {
19
+ expect('UDLRFB').toContain(ch);
20
+ }
21
+
22
+ const counts = {
23
+ U: 0,
24
+ D: 0,
25
+ L: 0,
26
+ R: 0,
27
+ F: 0,
28
+ B: 0,
29
+ };
30
+ for (const ch of state) {
31
+ const face = /** @type {"R" | "U" | "F" | "L" | "D" | "B"} */ (ch);
32
+ counts[face]++;
33
+ }
34
+ expect(Object.values(counts).every((c) => c === 36)).toBe(true);
35
+ });
36
+
37
+ it('reset method returns the cube to the initial state', () => {
38
+ // Arrange
39
+ const cube = createTestCube(CubeTypes.Six);
40
+ const initialState = toKociemba(cube._cubeInfo.initialStickerState);
41
+ /** @type {string?} */
42
+ let completedState = null;
43
+ /** @type {string?} */
44
+ let failedReason = null;
45
+ cube.movement(
46
+ Movements.Single.R,
47
+ (state) => {
48
+ completedState = state;
49
+ return true;
50
+ },
51
+ (reason) => {
52
+ failedReason = reason;
53
+ return true;
54
+ },
55
+ );
56
+ drainUpdates(cube);
57
+
58
+ // Act
59
+ /** @type {string?} */
60
+ let resetState = null;
61
+ cube.reset((state) => {
62
+ resetState = state;
63
+ return true;
64
+ });
65
+ drainUpdates(cube);
66
+
67
+ // Assert
68
+ expect(failedReason).toBeNull();
69
+ expect(completedState).not.toBeNull();
70
+ expect(completedState).not.toBe(initialState);
71
+ expect(/** @type {string?} **/ (resetState)).toBe(initialState);
72
+ });
73
+
74
+ it('solve', () => {
75
+ // Arrange
76
+ const cube = createTestCube(CubeTypes.Six);
77
+ const scramble =
78
+ "D Fw' R2 F2 Dw' Lw' Rw' F U2 3Uw 3Fw' Bw Rw2 L2 Bw2 B2 Uw 3Rw' Lw Rw2 3Uw2 Dw2 3Fw2 U2 Rw F Bw L 3Rw Dw2 3Fw' Dw2 D Bw' F Fw' D 3Fw' F' R' L B Rw' 3Fw2 U' L2 Lw' D Bw Uw2 3Fw2 Uw2 Bw' Fw U' Fw2 L 3Rw Bw' D' 3Rw' 3Uw2 Lw' U' Dw F R' B Rw U2 B2 Fw' 3Rw R 3Fw2 3Uw2 D' 3Fw' L Dw";
79
+ const scrambleMoves = /** @type {import('../src/core.js').Movement[]} */ (scramble.split(' '));
80
+
81
+ for (const move of scrambleMoves) {
82
+ cube.movement(
83
+ move,
84
+ () => {},
85
+ () => {
86
+ throw new Error('Movement failed unexpectedly');
87
+ },
88
+ );
89
+ drainUpdates(cube);
90
+ }
91
+
92
+ // Act
93
+ const solution =
94
+ "y' U 3r r F' F D' r r' U' r z y z' y' x U r' U' r U' r' U l y x' D U x' 4r2 L 3u x' U' U' r2 D' 3l2' U2 x' r u x U' 4r' F U x' 4l' U2 r U' 4r' z' F' 3r U2' 3r' x U' r' U' 4l R' 3u' 4u 3l' U2 3l 3u R2 u r U' U' r' z x' x' x' x' 2-3l' U2 r' F x' U 3r2' R2 U' r2' x U R' R' U' l U x x L U 3r2' x2' 5r U' 3r 4r' 5r' D 5r U 3r' U' 3r U 5r 3r' U r U r' U r U' r' U r U 2L' 5r 5r' 3r2 U 3r 5r2 3r' U' 3r 5r2' U' 4r r' U' r2 4r' 5r2 r' U' r' U' U' 5r' r3 U r' 5r U 5r2' 4r U' 4r' 5r 3r U' 3r' U 3r U' 3r2' U' U' 3r 4r' r2 U r' U' r U' r2' U 4r U' r' U 5r' r U' U' r U2' r' U2' r U r' 3r U2' 3r' U' r U' 4r r' U r' U' r 4r' z' U' L' U L R U' R' u F R' F' R u' 4u L' U' L' U L2 y' u L' U L 3u' L' U L u' y L' U L 4d L' U L 3u y' y U' y' 3u 3u 4u' 4u' U' y 5d 5d' U' U U' L' U L u' R' F R F' R U' R' 3u' d U' R U' R' u' U2 R U' R' 3u' u R U' R2' U' R y' L' U L R' F R F' R U' R' 3u' R' F R F' R U' R' 3u R U' R' u L' U L u' 4u' R' F R F' R U' R' U' d R U' R' u' 4u R' F R F' R U' R' 4u' u R U2' R2' U' R y' R' U2' R U R' U2' R U' L' U2 L U2' L' U L U R U' R' 5r U' 5r' U2 5r U 5r' U U U' R2' D' R U' R' D R U R' D' R U R' D R U R U' R' U' R 3r2' F2 U2' 3r2 R2' U2' F2 3r2 x2 y";
95
+ const solutionActions = /** @type {(import('../src/core.js').Movement | import('../src/core.js').Rotation)[]} */ (solution.split(' '));
96
+
97
+ let finalState = null;
98
+ for (const action of solutionActions) {
99
+ if (action.includes('x') || action.includes('y') || action.includes('z')) {
100
+ cube.rotate(
101
+ /** @type {import('../src/core.js').Rotation} */ (action),
102
+ (state) => {
103
+ finalState = state;
104
+ },
105
+ () => {
106
+ throw new Error('Rotation failed unexpectedly');
107
+ },
108
+ );
109
+ } else {
110
+ cube.movement(
111
+ /** @type {import('../src/core.js').Movement} */ (action),
112
+ (state) => {
113
+ finalState = state;
114
+ },
115
+ () => {
116
+ throw new Error('Movement failed unexpectedly');
117
+ },
118
+ );
119
+ }
120
+ drainUpdates(cube);
121
+ }
122
+
123
+ // Assert
124
+ expect(/** @type {string?} **/ (finalState)).toBe(toKociemba(cube._cubeInfo.initialStickerState));
125
+ });
126
+ });
@@ -0,0 +1,151 @@
1
+ // @ts-check
2
+ import './setup.js';
3
+ import { describe, it, expect, test } from 'bun:test';
4
+ import { CubeTypes, Movements } from '../src/core.js';
5
+ import { toKociemba } from '../src/cube/stickerState.js';
6
+ import { createTestCube, drainUpdates } from './common.js';
7
+
8
+ describe('3x3 Tests', () => {
9
+ it('Valid initial Kociemba state string', () => {
10
+ //Arange
11
+ const cube = createTestCube(CubeTypes.Three);
12
+
13
+ //Act
14
+ const state = toKociemba(cube._cubeInfo.initialStickerState);
15
+
16
+ //Assert
17
+ expect(state.length).toBe(54);
18
+ for (const ch of state) {
19
+ expect('UDLRFB').toContain(ch);
20
+ }
21
+
22
+ const counts = {
23
+ U: 0,
24
+ D: 0,
25
+ L: 0,
26
+ R: 0,
27
+ F: 0,
28
+ B: 0,
29
+ };
30
+ for (const ch of state) {
31
+ const face = /** @type {"R" | "U" | "F" | "L" | "D" | "B"} */ (ch);
32
+ counts[face]++;
33
+ }
34
+ expect(Object.values(counts).every((c) => c === 9)).toBe(true);
35
+ });
36
+
37
+ it('reset method returns the cube to the initial state', () => {
38
+ // Arrange
39
+ const cube = createTestCube(CubeTypes.Three);
40
+ const initialState = toKociemba(cube._cubeInfo.initialStickerState);
41
+ /** @type {string?} */
42
+ let completedState = null;
43
+ /** @type {string?} */
44
+ let failedReason = null;
45
+ cube.movement(
46
+ Movements.Single.R,
47
+ (state) => {
48
+ completedState = state;
49
+ return true;
50
+ },
51
+ (reason) => {
52
+ failedReason = reason;
53
+ return true;
54
+ },
55
+ );
56
+ drainUpdates(cube);
57
+
58
+ // Act
59
+ /** @type {string?} */
60
+ let resetState = null;
61
+ cube.reset((state) => {
62
+ resetState = state;
63
+ return true;
64
+ });
65
+ drainUpdates(cube);
66
+
67
+ // Assert
68
+ expect(failedReason).toBeNull();
69
+ expect(completedState).not.toBeNull();
70
+ expect(completedState).not.toBe(initialState);
71
+ expect(/** @type {string?} **/ (resetState)).toBe(initialState);
72
+ });
73
+
74
+ it('sexy move matches expected state', () => {
75
+ // Arrange
76
+ const EXPECTED_STATE = 'UULUUFUUFRRUBRRURRFFDFFUFFFDDRDDDDDDBLLLLLLLLBRRBBBBBB';
77
+ const cube = createTestCube(CubeTypes.Three);
78
+ const moves = [Movements.Single.R, Movements.Single.U, Movements.Single.RP, Movements.Single.UP];
79
+
80
+ // Act
81
+ /** @type {string?} */
82
+ let finalState = null;
83
+ for (const move of moves) {
84
+ cube.movement(
85
+ move,
86
+ (state) => {
87
+ finalState = state;
88
+ },
89
+ () => {
90
+ throw new Error('Movement failed unexpectedly');
91
+ },
92
+ );
93
+ drainUpdates(cube);
94
+ }
95
+
96
+ // Assert
97
+ expect(/** @type {string?} **/ (finalState)).toBe(EXPECTED_STATE);
98
+ });
99
+
100
+ it('solve', () => {
101
+ // Arrange
102
+ const cube = createTestCube(CubeTypes.Three);
103
+ const scramble = "D U R2 B2 D' B2 D R2 F2 L2 R B U2 R2 U' B' L D' R U' R";
104
+ const scrambleMoves = /** @type {import('../src/core.js').Movement[]} */ (scramble.split(' '));
105
+
106
+ for (const move of scrambleMoves) {
107
+ cube.movement(
108
+ move,
109
+ () => {},
110
+ () => {
111
+ throw new Error('Movement failed unexpectedly');
112
+ },
113
+ );
114
+ drainUpdates(cube);
115
+ }
116
+
117
+ // Act
118
+ const solution =
119
+ "D L R' F R D2 R U' R' F u R U2' R' U R U' R' U' L U L' D U L' U' L U2 L' U L L F' L' F U R' U2 R U2' R' U R U2' R' U R U2' R' U' R U y'";
120
+ const solutionActions = /** @type {(import('../src/core.js').Movement | import('../src/core.js').Rotation)[]} */ (solution.split(' '));
121
+
122
+ let finalState = null;
123
+ for (const action of solutionActions) {
124
+ if (action.includes('x') || action.includes('y') || action.includes('z')) {
125
+ cube.rotate(
126
+ /** @type {import('../src/core.js').Rotation} */ (action),
127
+ (state) => {
128
+ finalState = state;
129
+ },
130
+ () => {
131
+ throw new Error('Rotation failed unexpectedly');
132
+ },
133
+ );
134
+ } else {
135
+ cube.movement(
136
+ /** @type {import('../src/core.js').Movement} */ (action),
137
+ (state) => {
138
+ finalState = state;
139
+ },
140
+ () => {
141
+ throw new Error('Movement failed unexpectedly');
142
+ },
143
+ );
144
+ }
145
+ drainUpdates(cube);
146
+ }
147
+
148
+ // Assert
149
+ expect(/** @type {string?} **/ (finalState)).toBe(toKociemba(cube._cubeInfo.initialStickerState));
150
+ });
151
+ });