@cubingcenter/scrambler 0.4.1 → 0.5.1
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.
- package/README.md +21 -0
- package/dist/core/api.d.ts.map +1 -1
- package/dist/core/api.js +6 -1
- package/dist/puzzles/pyraminx/facelets.d.ts.map +1 -1
- package/dist/puzzles/pyraminx/facelets.js +43 -17
- package/dist/puzzles/pyraminx/random-state.d.ts.map +1 -1
- package/dist/puzzles/pyraminx/random-state.js +63 -29
- package/dist/puzzles/pyraminx/scrambler.d.ts.map +1 -1
- package/dist/puzzles/pyraminx/scrambler.js +35 -26
- package/dist/puzzles/pyraminx/solver/search.d.ts +3 -0
- package/dist/puzzles/pyraminx/solver/search.d.ts.map +1 -1
- package/dist/puzzles/pyraminx/solver/search.js +60 -5
- package/dist/puzzles/pyraminx/solver/tables.d.ts +1 -0
- package/dist/puzzles/pyraminx/solver/tables.d.ts.map +1 -1
- package/dist/puzzles/pyraminx/solver/tables.js +3 -0
- package/dist/puzzles/skewb/facelets.d.ts.map +1 -1
- package/dist/puzzles/skewb/facelets.js +46 -23
- package/dist/puzzles/skewb/index.d.ts +2 -1
- package/dist/puzzles/skewb/index.d.ts.map +1 -1
- package/dist/puzzles/skewb/index.js +2 -1
- package/dist/puzzles/skewb/random-state.d.ts +1 -1
- package/dist/puzzles/skewb/random-state.d.ts.map +1 -1
- package/dist/puzzles/skewb/random-state.js +25 -18
- package/dist/puzzles/skewb/scrambler.d.ts.map +1 -1
- package/dist/puzzles/skewb/scrambler.js +27 -16
- package/dist/puzzles/skewb/solver/browser-loader.d.ts +20 -0
- package/dist/puzzles/skewb/solver/browser-loader.d.ts.map +1 -0
- package/dist/puzzles/skewb/solver/browser-loader.js +86 -0
- package/dist/puzzles/skewb/solver/generated/centers-table.bin +0 -0
- package/dist/puzzles/skewb/solver/generated/helpers.bin +0 -0
- package/dist/puzzles/skewb/solver/generated/manifest.json +31 -0
- package/dist/puzzles/skewb/solver/generated/orientation-table.bin +0 -0
- package/dist/puzzles/skewb/solver/generated/permutation-table.bin +0 -0
- package/dist/puzzles/skewb/solver/generated/policy.bin +0 -0
- package/dist/puzzles/skewb/solver/generated-assets.d.ts +10 -0
- package/dist/puzzles/skewb/solver/generated-assets.d.ts.map +1 -0
- package/dist/puzzles/skewb/solver/generated-assets.js +24 -0
- package/dist/puzzles/skewb/solver/index.d.ts +5 -0
- package/dist/puzzles/skewb/solver/index.d.ts.map +1 -0
- package/dist/puzzles/skewb/solver/index.js +4 -0
- package/dist/puzzles/skewb/solver/node-loader.d.ts +10 -0
- package/dist/puzzles/skewb/solver/node-loader.d.ts.map +1 -0
- package/dist/puzzles/skewb/solver/node-loader.js +91 -0
- package/dist/puzzles/skewb/solver/search.d.ts +14 -0
- package/dist/puzzles/skewb/solver/search.d.ts.map +1 -0
- package/dist/puzzles/skewb/solver/search.js +120 -0
- package/dist/puzzles/skewb/solver/table-metadata.d.ts +27 -0
- package/dist/puzzles/skewb/solver/table-metadata.d.ts.map +1 -0
- package/dist/puzzles/skewb/solver/table-metadata.js +26 -0
- package/dist/puzzles/skewb/solver/tables.d.ts +69 -0
- package/dist/puzzles/skewb/solver/tables.d.ts.map +1 -0
- package/dist/puzzles/skewb/solver/tables.js +511 -0
- package/dist/random/prng.d.ts.map +1 -1
- package/dist/random/prng.js +25 -2
- package/dist-cjs/core/api.js +6 -1
- package/dist-cjs/puzzles/pyraminx/facelets.js +43 -17
- package/dist-cjs/puzzles/pyraminx/random-state.js +63 -29
- package/dist-cjs/puzzles/pyraminx/scrambler.js +34 -25
- package/dist-cjs/puzzles/pyraminx/solver/search.js +62 -4
- package/dist-cjs/puzzles/pyraminx/solver/tables.js +4 -0
- package/dist-cjs/puzzles/skewb/facelets.js +46 -23
- package/dist-cjs/puzzles/skewb/index.js +49 -11
- package/dist-cjs/puzzles/skewb/random-state.js +25 -18
- package/dist-cjs/puzzles/skewb/scrambler.js +26 -15
- package/dist-cjs/puzzles/skewb/solver/browser-loader.js +91 -0
- package/dist-cjs/puzzles/skewb/solver/generated/centers-table.bin +0 -0
- package/dist-cjs/puzzles/skewb/solver/generated/helpers.bin +0 -0
- package/dist-cjs/puzzles/skewb/solver/generated/manifest.json +31 -0
- package/dist-cjs/puzzles/skewb/solver/generated/orientation-table.bin +0 -0
- package/dist-cjs/puzzles/skewb/solver/generated/permutation-table.bin +0 -0
- package/dist-cjs/puzzles/skewb/solver/generated/policy.bin +0 -0
- package/dist-cjs/puzzles/skewb/solver/generated-assets.js +60 -0
- package/dist-cjs/puzzles/skewb/solver/index.js +23 -0
- package/dist-cjs/puzzles/skewb/solver/node-loader.js +97 -0
- package/dist-cjs/puzzles/skewb/solver/search.js +130 -0
- package/dist-cjs/puzzles/skewb/solver/table-metadata.js +29 -0
- package/dist-cjs/puzzles/skewb/solver/tables.js +543 -0
- package/dist-cjs/random/prng.js +25 -2
- package/package.json +6 -3
- package/dist/puzzles/skewb/solver.d.ts +0 -11
- package/dist/puzzles/skewb/solver.d.ts.map +0 -1
- package/dist/puzzles/skewb/solver.js +0 -149
- package/dist-cjs/puzzles/skewb/solver.js +0 -160
package/dist/random/prng.js
CHANGED
|
@@ -1,13 +1,36 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
// 4096 bytes per refill (~682 values per syscall).
|
|
3
|
+
const CRYPTO_BUFFER_BYTES = 4096;
|
|
4
|
+
// Shared unseeded source: the crypto refill costs a syscall per buffer, so a
|
|
5
|
+
// per-call source would pay ~one syscall per scramble. Seeded calls stay
|
|
6
|
+
// isolated (deterministic streams must never be shared).
|
|
7
|
+
let sharedCryptoSource = null;
|
|
2
8
|
export function createRandomSource(seed) {
|
|
3
9
|
if (seed === undefined) {
|
|
4
|
-
|
|
10
|
+
sharedCryptoSource ??= new CryptoRandomSource();
|
|
11
|
+
return sharedCryptoSource;
|
|
5
12
|
}
|
|
6
13
|
return new SeededRandomSource(hashSeed(seed));
|
|
7
14
|
}
|
|
8
15
|
class CryptoRandomSource {
|
|
16
|
+
buffer = null;
|
|
17
|
+
offset = 0;
|
|
9
18
|
next() {
|
|
10
|
-
|
|
19
|
+
// Batch the crypto calls: one randomBytes per move is ~2 orders of
|
|
20
|
+
// magnitude slower in Node (syscall per call). Refill lazily.
|
|
21
|
+
if (this.buffer === null || this.offset + 6 > CRYPTO_BUFFER_BYTES) {
|
|
22
|
+
this.buffer = randomBytes(CRYPTO_BUFFER_BYTES);
|
|
23
|
+
this.offset = 0;
|
|
24
|
+
}
|
|
25
|
+
const bytes = this.buffer;
|
|
26
|
+
const offset = this.offset;
|
|
27
|
+
this.offset += 6;
|
|
28
|
+
const value = bytes[offset] * 0x10000000000 +
|
|
29
|
+
bytes[offset + 1] * 0x100000000 +
|
|
30
|
+
bytes[offset + 2] * 0x1000000 +
|
|
31
|
+
bytes[offset + 3] * 0x10000 +
|
|
32
|
+
bytes[offset + 4] * 0x100 +
|
|
33
|
+
bytes[offset + 5];
|
|
11
34
|
return value / 0x1000000000000;
|
|
12
35
|
}
|
|
13
36
|
nextInt(maxExclusive) {
|
package/dist-cjs/core/api.js
CHANGED
|
@@ -71,11 +71,16 @@ async function initializeScrambler(baseUrl, eventId) {
|
|
|
71
71
|
const { loadBrowserTables } = await Promise.resolve().then(() => __importStar(require("../puzzles/pyraminx/solver/browser-loader.js")));
|
|
72
72
|
await loadBrowserTables(baseUrl);
|
|
73
73
|
}
|
|
74
|
+
else if (eventId === "skwb") {
|
|
75
|
+
const { loadBrowserTables } = await Promise.resolve().then(() => __importStar(require("../puzzles/skewb/solver/browser-loader.js")));
|
|
76
|
+
await loadBrowserTables(baseUrl);
|
|
77
|
+
}
|
|
74
78
|
else if (!eventId) {
|
|
75
79
|
await Promise.all([
|
|
76
80
|
Promise.resolve().then(() => __importStar(require("../puzzles/cube2/solver/browser-loader.js"))).then((m) => m.loadBrowserTables(baseUrl)),
|
|
77
81
|
Promise.resolve().then(() => __importStar(require("../puzzles/cube3/solver/browser-loader.js"))).then((m) => m.loadBrowserTables(baseUrl)),
|
|
78
|
-
Promise.resolve().then(() => __importStar(require("../puzzles/pyraminx/solver/browser-loader.js"))).then((m) => m.loadBrowserTables(baseUrl))
|
|
82
|
+
Promise.resolve().then(() => __importStar(require("../puzzles/pyraminx/solver/browser-loader.js"))).then((m) => m.loadBrowserTables(baseUrl)),
|
|
83
|
+
Promise.resolve().then(() => __importStar(require("../puzzles/skewb/solver/browser-loader.js"))).then((m) => m.loadBrowserTables(baseUrl))
|
|
79
84
|
]);
|
|
80
85
|
}
|
|
81
86
|
}
|
|
@@ -5,6 +5,8 @@ exports.toPyraminxFacelets = toPyraminxFacelets;
|
|
|
5
5
|
exports.formatPyraminxFacelets = formatPyraminxFacelets;
|
|
6
6
|
exports.getPyraminxFaceOrder = getPyraminxFaceOrder;
|
|
7
7
|
const FACE_ORDER = ["F", "L", "R", "D"];
|
|
8
|
+
const COLORS = ["F", "L", "R", "D"];
|
|
9
|
+
const COLOR_INDEX = { F: 0, L: 1, R: 2, D: 3 };
|
|
8
10
|
// positionId -> sticker slots (each slot = one triangle cell of the grid).
|
|
9
11
|
// Positions follow the scrambler piece model (moves.ts):
|
|
10
12
|
// edges 0..5 = UL, UR, UB, LR, LB, RB
|
|
@@ -54,6 +56,33 @@ const TIP_PIECE_STICKERS = [
|
|
|
54
56
|
["F", "R", "D"], // 2 R
|
|
55
57
|
["L", "D", "R"], // 3 B
|
|
56
58
|
];
|
|
59
|
+
const EDGE_CELLS = buildCells(EDGE_POSITION_SLOTS, EDGE_PIECE_STICKERS, 6, 2);
|
|
60
|
+
const CENTER_CELLS = buildCells(CENTER_POSITION_SLOTS, CENTER_PIECE_STICKERS, 4, 3);
|
|
61
|
+
const TIP_CELLS = buildCells(TIP_POSITION_SLOTS, TIP_PIECE_STICKERS, 4, 3);
|
|
62
|
+
function buildCells(slots, stickersTable, pieceCount, orientationCount) {
|
|
63
|
+
const cells = [];
|
|
64
|
+
for (let position = 0; position < slots.length; position += 1) {
|
|
65
|
+
const byPiece = [];
|
|
66
|
+
for (let piece = 0; piece < pieceCount; piece += 1) {
|
|
67
|
+
const byOrientation = [];
|
|
68
|
+
for (let orientation = 0; orientation < orientationCount; orientation += 1) {
|
|
69
|
+
const stickers = stickersTable[piece];
|
|
70
|
+
const slotList = slots[position];
|
|
71
|
+
const cell = [];
|
|
72
|
+
for (let index = 0; index < slotList.length; index += 1) {
|
|
73
|
+
const [face, stickerIndex] = slotList[index];
|
|
74
|
+
cell.push(FACE_ORDER.indexOf(face));
|
|
75
|
+
cell.push(stickerIndex);
|
|
76
|
+
cell.push(COLOR_INDEX[stickers[(index - orientation + stickers.length) % stickers.length]]);
|
|
77
|
+
}
|
|
78
|
+
byOrientation.push(cell);
|
|
79
|
+
}
|
|
80
|
+
byPiece.push(byOrientation);
|
|
81
|
+
}
|
|
82
|
+
cells.push(byPiece);
|
|
83
|
+
}
|
|
84
|
+
return cells;
|
|
85
|
+
}
|
|
57
86
|
function createSolvedPyraminxFacelets() {
|
|
58
87
|
return {
|
|
59
88
|
F: Array.from({ length: 9 }, () => "F"),
|
|
@@ -62,36 +91,33 @@ function createSolvedPyraminxFacelets() {
|
|
|
62
91
|
D: Array.from({ length: 9 }, () => "D"),
|
|
63
92
|
};
|
|
64
93
|
}
|
|
94
|
+
// Sparse arrays: every one of the 36 sticker cells is overwritten by the
|
|
95
|
+
// three placement loops below (12 edges + 12 centers + 12 tips), so no fill
|
|
96
|
+
// is needed. Coverage is enforced by test/pyraminx-facelets.test.ts.
|
|
65
97
|
function createEmptyPyraminxFacelets() {
|
|
66
98
|
return {
|
|
67
|
-
F: Array(9)
|
|
68
|
-
L: Array(9)
|
|
69
|
-
R: Array(9)
|
|
70
|
-
D: Array(9)
|
|
99
|
+
F: Array(9),
|
|
100
|
+
L: Array(9),
|
|
101
|
+
R: Array(9),
|
|
102
|
+
D: Array(9),
|
|
71
103
|
};
|
|
72
104
|
}
|
|
73
|
-
function
|
|
74
|
-
for (let
|
|
75
|
-
|
|
76
|
-
const sticker = stickers[(index - orientation + stickers.length) % stickers.length];
|
|
77
|
-
const arr = facelets[face];
|
|
78
|
-
arr[stickerIndex] = sticker;
|
|
105
|
+
function placeCells(faces, cells) {
|
|
106
|
+
for (let cellIndex = 0; cellIndex < cells.length; cellIndex += 3) {
|
|
107
|
+
faces[cells[cellIndex]][cells[cellIndex + 1]] = COLORS[cells[cellIndex + 2]];
|
|
79
108
|
}
|
|
80
109
|
}
|
|
81
110
|
function toPyraminxFacelets(state) {
|
|
82
111
|
const facelets = createEmptyPyraminxFacelets();
|
|
112
|
+
const faces = [facelets.F, facelets.L, facelets.R, facelets.D];
|
|
83
113
|
for (let position = 0; position < 6; position += 1) {
|
|
84
|
-
|
|
85
|
-
const orientation = state.edgesOrient[position];
|
|
86
|
-
placeOrientedStickers(facelets, EDGE_POSITION_SLOTS[position], EDGE_PIECE_STICKERS[piece], orientation);
|
|
114
|
+
placeCells(faces, EDGE_CELLS[position][state.edgesPerm[position]][state.edgesOrient[position]]);
|
|
87
115
|
}
|
|
88
116
|
for (let position = 0; position < 4; position += 1) {
|
|
89
|
-
|
|
90
|
-
placeOrientedStickers(facelets, CENTER_POSITION_SLOTS[position], CENTER_PIECE_STICKERS[position], orientation);
|
|
117
|
+
placeCells(faces, CENTER_CELLS[position][position][state.centers[position]]);
|
|
91
118
|
}
|
|
92
119
|
for (let position = 0; position < 4; position += 1) {
|
|
93
|
-
|
|
94
|
-
placeOrientedStickers(facelets, TIP_POSITION_SLOTS[position], TIP_PIECE_STICKERS[position], orientation);
|
|
120
|
+
placeCells(faces, TIP_CELLS[position][position][state.tips[position]]);
|
|
95
121
|
}
|
|
96
122
|
return facelets;
|
|
97
123
|
}
|
|
@@ -2,41 +2,71 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.generateRandomPyraminxState = generateRandomPyraminxState;
|
|
4
4
|
const index_js_1 = require("../../random/index.js");
|
|
5
|
-
const
|
|
5
|
+
const state_js_1 = require("./state.js");
|
|
6
|
+
// Compact -> full Lehmer index for the 360 even permutations of 6 edges.
|
|
7
|
+
// Generated once: a permutation's parity is the parity of the digit sum of
|
|
8
|
+
// its Lehmer code (each digit counts smaller elements to its right).
|
|
9
|
+
const COMPACT_TO_FULL = new Uint16Array(360);
|
|
10
|
+
(function buildCompactMapping() {
|
|
11
|
+
let compact = 0;
|
|
12
|
+
for (let full = 0; full < 720 && compact < 360; full += 1) {
|
|
13
|
+
let value = full;
|
|
14
|
+
const c5 = (value / 120) | 0;
|
|
15
|
+
value -= c5 * 120;
|
|
16
|
+
const c4 = (value / 24) | 0;
|
|
17
|
+
value -= c4 * 24;
|
|
18
|
+
const c3 = (value / 6) | 0;
|
|
19
|
+
value -= c3 * 6;
|
|
20
|
+
const c2 = (value / 2) | 0;
|
|
21
|
+
value -= c2 * 2;
|
|
22
|
+
const c1 = value;
|
|
23
|
+
if ((c5 + c4 + c3 + c2 + c1) % 2 === 0) {
|
|
24
|
+
COMPACT_TO_FULL[compact] = full;
|
|
25
|
+
compact += 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
})();
|
|
6
29
|
function generateRandomPyraminxState(options = {}) {
|
|
7
30
|
const random = (0, index_js_1.createRandomSource)(options.seed);
|
|
8
31
|
const rejectTrivial = options.rejectTrivial ?? true;
|
|
9
32
|
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
33
|
+
// The state is legal by construction (even permutation parity, even
|
|
34
|
+
// orientation sum, values in range), so only the trivial core check is
|
|
35
|
+
// needed here — no full re-validation.
|
|
10
36
|
const state = createLegalRandomState(random);
|
|
11
|
-
if (!rejectTrivial || (0,
|
|
37
|
+
if (!rejectTrivial || !(0, state_js_1.isSolvedPyraminxState)(state)) {
|
|
12
38
|
return state;
|
|
13
39
|
}
|
|
14
40
|
}
|
|
15
41
|
throw new Error("Unable to generate a non-trivial random Pyraminx state.");
|
|
16
42
|
}
|
|
17
43
|
function createLegalRandomState(random) {
|
|
18
|
-
// 4 random centers
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
44
|
+
// 4 random centers from one uniform value in [0, 81) (4 base-3 digits)
|
|
45
|
+
let centerValue = random.nextInt(81);
|
|
46
|
+
const centers = [0, 0, 0, 0];
|
|
47
|
+
for (let index = 0; index < 4; index += 1) {
|
|
48
|
+
centers[index] = centerValue % 3;
|
|
49
|
+
centerValue = (centerValue / 3) | 0;
|
|
50
|
+
}
|
|
51
|
+
// 4 random tips from one uniform value in [0, 81)
|
|
52
|
+
let tipValue = random.nextInt(81);
|
|
53
|
+
const tips = [0, 0, 0, 0];
|
|
54
|
+
for (let index = 0; index < 4; index += 1) {
|
|
55
|
+
tips[index] = tipValue % 3;
|
|
56
|
+
tipValue = (tipValue / 3) | 0;
|
|
30
57
|
}
|
|
31
|
-
// 6 edges
|
|
32
|
-
const
|
|
58
|
+
// 6 edges permutation, even parity, uniform over the 360 even permutations
|
|
59
|
+
const edgesPerm = permutationFromCompact(random.nextInt(360));
|
|
60
|
+
// 6 edges orientation (sum must be even): 5 free bits + parity digit
|
|
61
|
+
let orientationValue = random.nextInt(32);
|
|
62
|
+
const edgesOrient = [0, 0, 0, 0, 0, 0];
|
|
33
63
|
let sumOrient = 0;
|
|
34
|
-
for (let
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
64
|
+
for (let index = 0; index < 5; index += 1) {
|
|
65
|
+
edgesOrient[index] = orientationValue & 1;
|
|
66
|
+
sumOrient += orientationValue & 1;
|
|
67
|
+
orientationValue >>= 1;
|
|
38
68
|
}
|
|
39
|
-
edgesOrient[5] = sumOrient
|
|
69
|
+
edgesOrient[5] = sumOrient & 1;
|
|
40
70
|
return {
|
|
41
71
|
centers,
|
|
42
72
|
edgesPerm,
|
|
@@ -44,13 +74,17 @@ function createLegalRandomState(random) {
|
|
|
44
74
|
tips
|
|
45
75
|
};
|
|
46
76
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
77
|
+
const FACTORIALS = [1, 1, 2, 6, 24, 120, 720];
|
|
78
|
+
function permutationFromCompact(compact) {
|
|
79
|
+
let full = COMPACT_TO_FULL[compact];
|
|
80
|
+
const permutation = [0, 0, 0, 0, 0, 0];
|
|
81
|
+
const remaining = [0, 1, 2, 3, 4, 5];
|
|
82
|
+
for (let position = 0; position < 6; position += 1) {
|
|
83
|
+
const divisor = FACTORIALS[5 - position];
|
|
84
|
+
const choice = (full / divisor) | 0;
|
|
85
|
+
full -= choice * divisor;
|
|
86
|
+
permutation[position] = remaining[choice];
|
|
87
|
+
remaining.splice(choice, 1);
|
|
54
88
|
}
|
|
55
|
-
return
|
|
89
|
+
return permutation;
|
|
56
90
|
}
|
|
@@ -3,12 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.pyraminxScrambleGenerator = void 0;
|
|
4
4
|
exports.generatePyraminxScramble = generatePyraminxScramble;
|
|
5
5
|
const random_state_js_1 = require("./random-state.js");
|
|
6
|
-
const moves_js_1 = require("./moves.js");
|
|
7
6
|
const index_js_1 = require("./solver/index.js");
|
|
8
7
|
const facelets_js_1 = require("./facelets.js");
|
|
9
8
|
const types_js_1 = require("./types.js");
|
|
10
|
-
|
|
11
|
-
const FACE_TIP_INDEX = { U: 0, L: 1, R: 2, B: 3 };
|
|
9
|
+
const TIP_INVERSE = ["u'", "l'", "r'", "b'"];
|
|
12
10
|
exports.pyraminxScrambleGenerator = {
|
|
13
11
|
info: {
|
|
14
12
|
eventId: "pymx",
|
|
@@ -24,47 +22,58 @@ exports.pyraminxScrambleGenerator = {
|
|
|
24
22
|
};
|
|
25
23
|
async function generatePyraminxScramble(options = {}) {
|
|
26
24
|
const state = (0, random_state_js_1.generateRandomPyraminxState)(options);
|
|
27
|
-
// Solve the core edges and centers
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
// Solve the core edges and centers as move indices (0..7). For index m the
|
|
26
|
+
// inverse is m ^ 1, the face is m >> 1 (0=U, 1=L, 2=R, 3=B) and the
|
|
27
|
+
// direction is m & 1 (1 = clockwise, 0 = counter-clockwise).
|
|
28
|
+
const solution = (0, index_js_1.arePyraminxTablesLoaded)()
|
|
29
|
+
? (0, index_js_1.solvePyraminxIndicesSync)(state)
|
|
30
|
+
: await (0, index_js_1.solvePyraminxIndices)(state);
|
|
31
|
+
const depth = solution.length;
|
|
32
|
+
// Single pass over the solution (reversed): build the scramble move strings
|
|
33
|
+
// while accumulating the tips rotated by the layer moves arithmetically
|
|
34
|
+
// (no state replay, no string parsing).
|
|
35
|
+
const moves = new Array(depth + 4);
|
|
33
36
|
const coreTips = [0, 0, 0, 0];
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
37
|
+
let count = 0;
|
|
38
|
+
for (let index = depth - 1; index >= 0; index -= 1) {
|
|
39
|
+
const m = solution[index] ^ 1;
|
|
40
|
+
moves[count++] = index_js_1.PYRAMINX_MOVES[m];
|
|
41
|
+
const face = m >> 1;
|
|
42
|
+
coreTips[face] = (coreTips[face] + (m & 1 ? 2 : 1)) % 3;
|
|
41
43
|
}
|
|
42
|
-
const
|
|
43
|
-
for (let i = 0; i < 4; i
|
|
44
|
+
const tipMoves = [];
|
|
45
|
+
for (let i = 0; i < 4; i += 1) {
|
|
44
46
|
const delta = (state.tips[i] - coreTips[i] + 3) % 3;
|
|
45
47
|
if (delta === 1) {
|
|
46
|
-
|
|
48
|
+
moves[count++] = types_js_1.PYRAMINX_TIPS[i];
|
|
49
|
+
tipMoves.push(i * 2);
|
|
47
50
|
}
|
|
48
51
|
else if (delta === 2) {
|
|
49
|
-
|
|
52
|
+
moves[count++] = TIP_INVERSE[i];
|
|
53
|
+
tipMoves.push(i * 2 + 1);
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
|
-
|
|
56
|
+
moves.length = count;
|
|
57
|
+
if (!options.skipValidation && !(0, index_js_1.verifyPyraminxScrambleIndices)(state, solution, tipMoves)) {
|
|
53
58
|
throw new Error("Generated Pyraminx scramble does not reproduce the target random state.");
|
|
54
59
|
}
|
|
55
60
|
const facelets = (0, facelets_js_1.toPyraminxFacelets)(state);
|
|
61
|
+
const solutionMoves = new Array(depth);
|
|
62
|
+
for (let index = 0; index < depth; index += 1) {
|
|
63
|
+
solutionMoves[index] = index_js_1.PYRAMINX_MOVES[solution[index]];
|
|
64
|
+
}
|
|
56
65
|
return {
|
|
57
66
|
eventId: "pymx",
|
|
58
|
-
scramble:
|
|
59
|
-
moves
|
|
67
|
+
scramble: moves.join(" "),
|
|
68
|
+
moves,
|
|
60
69
|
quality: "random-state",
|
|
61
70
|
generator: "pyraminx-optimal",
|
|
62
71
|
metadata: {
|
|
63
72
|
state,
|
|
64
73
|
facelets,
|
|
65
74
|
faceletString: (0, facelets_js_1.formatPyraminxFacelets)(facelets),
|
|
66
|
-
solutionMoves
|
|
67
|
-
solutionDepth:
|
|
75
|
+
solutionMoves,
|
|
76
|
+
solutionDepth: depth
|
|
68
77
|
}
|
|
69
78
|
};
|
|
70
79
|
}
|
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.solvePyraminx = solvePyraminx;
|
|
4
4
|
exports.solvePyraminxWithInfo = solvePyraminxWithInfo;
|
|
5
|
+
exports.solvePyraminxIndices = solvePyraminxIndices;
|
|
6
|
+
exports.solvePyraminxIndicesSync = solvePyraminxIndicesSync;
|
|
5
7
|
exports.solvePyraminxSync = solvePyraminxSync;
|
|
6
8
|
exports.verifyPyraminxScramble = verifyPyraminxScramble;
|
|
9
|
+
exports.verifyPyraminxScrambleIndices = verifyPyraminxScrambleIndices;
|
|
7
10
|
const tables_js_1 = require("./tables.js");
|
|
8
11
|
async function solvePyraminx(state) {
|
|
9
12
|
await (0, tables_js_1.ensureTablesLoaded)();
|
|
@@ -13,7 +16,25 @@ async function solvePyraminxWithInfo(state) {
|
|
|
13
16
|
await (0, tables_js_1.ensureTablesLoaded)();
|
|
14
17
|
return solveWithTables(state);
|
|
15
18
|
}
|
|
19
|
+
// Index-based solve for the hot scramble path: returns the solution as move
|
|
20
|
+
// indices (0..7) in walk order. The inverse of index m is m ^ 1 (moves are
|
|
21
|
+
// paired U/U', L/L', R/R', B/B'), its face is m >> 1 and its direction is
|
|
22
|
+
// m & 1 (1 = clockwise, 0 = counter-clockwise).
|
|
23
|
+
async function solvePyraminxIndices(state) {
|
|
24
|
+
await (0, tables_js_1.ensureTablesLoaded)();
|
|
25
|
+
return solveWithTablesIndices(state);
|
|
26
|
+
}
|
|
27
|
+
function solvePyraminxIndicesSync(state) {
|
|
28
|
+
if (!tables_js_1.POLICY_TABLE)
|
|
29
|
+
throw new Error("Pyraminx tables not loaded");
|
|
30
|
+
return solveWithTablesIndices(state);
|
|
31
|
+
}
|
|
16
32
|
function solveWithTables(state) {
|
|
33
|
+
const indices = solveWithTablesIndices(state);
|
|
34
|
+
const moves = indices.map((index) => tables_js_1.PYRAMINX_MOVES[index]);
|
|
35
|
+
return { moves, depth: moves.length };
|
|
36
|
+
}
|
|
37
|
+
function solveWithTablesIndices(state) {
|
|
17
38
|
const centersTable = tables_js_1.CENTERS_TABLE;
|
|
18
39
|
const permTable = tables_js_1.PERMUTATION_TABLE;
|
|
19
40
|
const orientTable = tables_js_1.ORIENTATION_TABLE;
|
|
@@ -23,17 +44,20 @@ function solveWithTables(state) {
|
|
|
23
44
|
let o = (0, tables_js_1.getOrientationIndex)(state.edgesOrient);
|
|
24
45
|
let stateIndex = c + 81 * (p + tables_js_1.PERMUTATION_COUNT * o);
|
|
25
46
|
if (stateIndex === 0)
|
|
26
|
-
return
|
|
47
|
+
return [];
|
|
27
48
|
const moves = [];
|
|
28
49
|
while (stateIndex !== 0) {
|
|
29
|
-
const
|
|
30
|
-
|
|
50
|
+
const bits = stateIndex * 3;
|
|
51
|
+
const byteIndex = bits >> 3;
|
|
52
|
+
const offset = bits & 7;
|
|
53
|
+
const m = ((policy[byteIndex] | ((policy[byteIndex + 1] ?? 0) << 8)) >> offset) & 0x07;
|
|
54
|
+
moves.push(m);
|
|
31
55
|
c = centersTable[c * tables_js_1.MOVE_COUNT + m];
|
|
32
56
|
p = permTable[p * tables_js_1.MOVE_COUNT + m];
|
|
33
57
|
o = orientTable[o * tables_js_1.MOVE_COUNT + m];
|
|
34
58
|
stateIndex = c + 81 * (p + tables_js_1.PERMUTATION_COUNT * o);
|
|
35
59
|
}
|
|
36
|
-
return
|
|
60
|
+
return moves;
|
|
37
61
|
}
|
|
38
62
|
// Synchronous variant for scripts that have already ensured tables are loaded
|
|
39
63
|
function solvePyraminxSync(state) {
|
|
@@ -86,3 +110,37 @@ function verifyPyraminxScramble(state, finalMoves) {
|
|
|
86
110
|
tips[2] === state.tips[2] &&
|
|
87
111
|
tips[3] === state.tips[3]);
|
|
88
112
|
}
|
|
113
|
+
// Numeric verification for the hot scramble path: replays the scramble
|
|
114
|
+
// (solution reversed with m ^ 1 inverses, tip moves as tipIndex * 2 + dir
|
|
115
|
+
// with dir 0 = clockwise, 1 = counter-clockwise) without any string parsing.
|
|
116
|
+
function verifyPyraminxScrambleIndices(state, solution, tipMoves) {
|
|
117
|
+
const centersTable = tables_js_1.CENTERS_TABLE;
|
|
118
|
+
const permTable = tables_js_1.PERMUTATION_TABLE;
|
|
119
|
+
const orientTable = tables_js_1.ORIENTATION_TABLE;
|
|
120
|
+
if (!centersTable || !permTable || !orientTable) {
|
|
121
|
+
throw new Error("Pyraminx solver tables not loaded. Call ensureTablesLoaded() first.");
|
|
122
|
+
}
|
|
123
|
+
let c = 0;
|
|
124
|
+
let p = 0;
|
|
125
|
+
let o = 0;
|
|
126
|
+
const tips = [0, 0, 0, 0];
|
|
127
|
+
for (let index = solution.length - 1; index >= 0; index -= 1) {
|
|
128
|
+
const m = solution[index] ^ 1;
|
|
129
|
+
c = centersTable[c * tables_js_1.MOVE_COUNT + m];
|
|
130
|
+
p = permTable[p * tables_js_1.MOVE_COUNT + m];
|
|
131
|
+
o = orientTable[o * tables_js_1.MOVE_COUNT + m];
|
|
132
|
+
const face = m >> 1;
|
|
133
|
+
tips[face] = (tips[face] + (m & 1 ? 2 : 1)) % 3;
|
|
134
|
+
}
|
|
135
|
+
for (const tipMove of tipMoves) {
|
|
136
|
+
const tipIndex = tipMove >> 1;
|
|
137
|
+
tips[tipIndex] = (tips[tipIndex] + (tipMove & 1 ? 2 : 1)) % 3;
|
|
138
|
+
}
|
|
139
|
+
return (c === (0, tables_js_1.getCentersIndex)(state.centers) &&
|
|
140
|
+
p === (0, tables_js_1.getPermutationCompactIndex)(state.edgesPerm) &&
|
|
141
|
+
o === (0, tables_js_1.getOrientationIndex)(state.edgesOrient) &&
|
|
142
|
+
tips[0] === state.tips[0] &&
|
|
143
|
+
tips[1] === state.tips[1] &&
|
|
144
|
+
tips[2] === state.tips[2] &&
|
|
145
|
+
tips[3] === state.tips[3]);
|
|
146
|
+
}
|
|
@@ -20,6 +20,7 @@ exports.generateOrientationTable = generateOrientationTable;
|
|
|
20
20
|
exports.generatePolicyTable = generatePolicyTable;
|
|
21
21
|
exports.checksum = checksum;
|
|
22
22
|
exports.ensureTablesLoaded = ensureTablesLoaded;
|
|
23
|
+
exports.arePyraminxTablesLoaded = arePyraminxTablesLoaded;
|
|
23
24
|
exports.getPolicyMove = getPolicyMove;
|
|
24
25
|
exports.getCentersTransition = getCentersTransition;
|
|
25
26
|
exports.getPermutationTransition = getPermutationTransition;
|
|
@@ -298,6 +299,9 @@ async function ensureTablesLoaded() {
|
|
|
298
299
|
exports.POLICY_TABLE = tables.policyTable;
|
|
299
300
|
tablesLoaded = true;
|
|
300
301
|
}
|
|
302
|
+
function arePyraminxTablesLoaded() {
|
|
303
|
+
return tablesLoaded;
|
|
304
|
+
}
|
|
301
305
|
function getPolicyMove(stateIndex) {
|
|
302
306
|
if (!exports.POLICY_TABLE)
|
|
303
307
|
throw new Error("Pyraminx solver tables not loaded. Call ensureTablesLoaded() first.");
|
|
@@ -7,6 +7,7 @@ exports.getSkewbFaceOrder = getSkewbFaceOrder;
|
|
|
7
7
|
// Facelets model matching cubingcenter-viz/src/puzzles/skewb.ts
|
|
8
8
|
// Each face has 5 stickers: 0=center, 1=TL,2=TR,3=BL,4=BR (net layout)
|
|
9
9
|
const FACE_ORDER = ["U", "R", "F", "D", "L", "B"];
|
|
10
|
+
const FACE_INDEX = { U: 0, R: 1, F: 2, D: 3, L: 4, B: 5 };
|
|
10
11
|
// Canonical sticker faces per corner position: [UD sticker, RL sticker, FB sticker]
|
|
11
12
|
const CORNER_STICKERS = [
|
|
12
13
|
["U", "R", "F"],
|
|
@@ -29,43 +30,65 @@ const CORNER_SLOTS = [
|
|
|
29
30
|
[["D", 4], ["R", 4], ["B", 3]],
|
|
30
31
|
[["D", 3], ["L", 3], ["B", 4]],
|
|
31
32
|
];
|
|
33
|
+
// For each (position, piece, orientation) the three facelet writes, flattened:
|
|
34
|
+
// face index, sticker slot index and sticker color index, so the hot loop is
|
|
35
|
+
// pure table lookups (no tuple destructuring, no modulo).
|
|
36
|
+
const CORNER_WRITE_COUNT = 8 * 8 * 3 * 3;
|
|
37
|
+
const WRITE_FACE = new Int8Array(CORNER_WRITE_COUNT);
|
|
38
|
+
const WRITE_SLOT = new Int8Array(CORNER_WRITE_COUNT);
|
|
39
|
+
const WRITE_COLOR = new Int8Array(CORNER_WRITE_COUNT);
|
|
40
|
+
for (let pos = 0; pos < 8; pos++) {
|
|
41
|
+
const slots = CORNER_SLOTS[pos];
|
|
42
|
+
for (let piece = 0; piece < 8; piece++) {
|
|
43
|
+
const stickers = CORNER_STICKERS[piece];
|
|
44
|
+
for (let ori = 0; ori < 3; ori++) {
|
|
45
|
+
const base = ((pos * 8 + piece) * 3 + ori) * 3;
|
|
46
|
+
for (let i = 0; i < 3; i++) {
|
|
47
|
+
const [face, idx] = slots[i];
|
|
48
|
+
const color = stickers[(i + 3 - ori) % 3];
|
|
49
|
+
WRITE_FACE[base + i] = FACE_INDEX[face];
|
|
50
|
+
WRITE_SLOT[base + i] = idx;
|
|
51
|
+
WRITE_COLOR[base + i] = FACE_INDEX[color];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
32
56
|
function createSolvedSkewbFacelets() {
|
|
33
57
|
return {
|
|
34
|
-
U:
|
|
35
|
-
R:
|
|
36
|
-
F:
|
|
37
|
-
D:
|
|
38
|
-
L:
|
|
39
|
-
B:
|
|
58
|
+
U: ["U", "U", "U", "U", "U"],
|
|
59
|
+
R: ["R", "R", "R", "R", "R"],
|
|
60
|
+
F: ["F", "F", "F", "F", "F"],
|
|
61
|
+
D: ["D", "D", "D", "D", "D"],
|
|
62
|
+
L: ["L", "L", "L", "L", "L"],
|
|
63
|
+
B: ["B", "B", "B", "B", "B"],
|
|
40
64
|
};
|
|
41
65
|
}
|
|
42
66
|
function createEmptySkewbFacelets() {
|
|
43
67
|
return {
|
|
44
|
-
U:
|
|
45
|
-
R:
|
|
46
|
-
F:
|
|
47
|
-
D:
|
|
48
|
-
L:
|
|
49
|
-
B:
|
|
68
|
+
U: ["?", "?", "?", "?", "?"],
|
|
69
|
+
R: ["?", "?", "?", "?", "?"],
|
|
70
|
+
F: ["?", "?", "?", "?", "?"],
|
|
71
|
+
D: ["?", "?", "?", "?", "?"],
|
|
72
|
+
L: ["?", "?", "?", "?", "?"],
|
|
73
|
+
B: ["?", "?", "?", "?", "?"],
|
|
50
74
|
};
|
|
51
75
|
}
|
|
52
76
|
function toSkewbFacelets(state) {
|
|
53
77
|
const facelets = createEmptySkewbFacelets();
|
|
78
|
+
const faces = [facelets.U, facelets.R, facelets.F, facelets.D, facelets.L, facelets.B];
|
|
79
|
+
const perm = state.corners.permutation;
|
|
80
|
+
const orient = state.corners.orientation;
|
|
81
|
+
const centers = state.centers;
|
|
54
82
|
// centers
|
|
55
83
|
for (let face = 0; face < 6; face++) {
|
|
56
|
-
|
|
57
|
-
facelets[targetFace][0] = FACE_ORDER[state.centers[face]];
|
|
84
|
+
faces[face][0] = FACE_ORDER[centers[face]];
|
|
58
85
|
}
|
|
59
|
-
// corners
|
|
86
|
+
// corners (unrolled 3 writes per position)
|
|
60
87
|
for (let pos = 0; pos < 8; pos++) {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
for (let i = 0; i < 3; i++) {
|
|
66
|
-
const [face, idx] = slots[i];
|
|
67
|
-
facelets[face][idx] = stickers[(i + 3 - ori) % 3];
|
|
68
|
-
}
|
|
88
|
+
const base = ((pos * 8 + perm[pos]) * 3 + orient[pos]) * 3;
|
|
89
|
+
faces[WRITE_FACE[base]][WRITE_SLOT[base]] = FACE_ORDER[WRITE_COLOR[base]];
|
|
90
|
+
faces[WRITE_FACE[base + 1]][WRITE_SLOT[base + 1]] = FACE_ORDER[WRITE_COLOR[base + 1]];
|
|
91
|
+
faces[WRITE_FACE[base + 2]][WRITE_SLOT[base + 2]] = FACE_ORDER[WRITE_COLOR[base + 2]];
|
|
69
92
|
}
|
|
70
93
|
return facelets;
|
|
71
94
|
}
|