@cubingcenter/scrambler 0.7.0 → 0.8.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 (126) hide show
  1. package/README.md +15 -4
  2. package/dist/core/api.d.ts.map +1 -1
  3. package/dist/core/api.js +10 -2
  4. package/dist/puzzles/cube2/solver/browser-loader.d.ts +4 -7
  5. package/dist/puzzles/cube2/solver/browser-loader.d.ts.map +1 -1
  6. package/dist/puzzles/cube2/solver/browser-loader.js +42 -14
  7. package/dist/puzzles/cube2/solver/generated-assets.d.ts.map +1 -1
  8. package/dist/puzzles/cube2/solver/generated-assets.js +8 -3
  9. package/dist/puzzles/cube3/solver/browser-loader.d.ts.map +1 -1
  10. package/dist/puzzles/cube3/solver/browser-loader.js +42 -20
  11. package/dist/puzzles/cube3/solver/generated-assets.d.ts.map +1 -1
  12. package/dist/puzzles/cube3/solver/generated-assets.js +9 -3
  13. package/dist/puzzles/cube3/solver/phase1.d.ts +19 -0
  14. package/dist/puzzles/cube3/solver/phase1.d.ts.map +1 -1
  15. package/dist/puzzles/cube3/solver/phase1.js +29 -6
  16. package/dist/puzzles/cube3/solver/phase2.d.ts +28 -0
  17. package/dist/puzzles/cube3/solver/phase2.d.ts.map +1 -1
  18. package/dist/puzzles/cube3/solver/phase2.js +56 -15
  19. package/dist/puzzles/cube3/solver/tables.d.ts.map +1 -1
  20. package/dist/puzzles/cube3/solver/tables.js +14 -12
  21. package/dist/puzzles/cube4/browser-worker-pool.d.ts +82 -0
  22. package/dist/puzzles/cube4/browser-worker-pool.d.ts.map +1 -0
  23. package/dist/puzzles/cube4/browser-worker-pool.js +205 -0
  24. package/dist/puzzles/cube4/browser-worker.d.ts +2 -0
  25. package/dist/puzzles/cube4/browser-worker.d.ts.map +1 -0
  26. package/dist/puzzles/cube4/browser-worker.js +31 -0
  27. package/dist/puzzles/cube4/index.d.ts +7 -0
  28. package/dist/puzzles/cube4/index.d.ts.map +1 -1
  29. package/dist/puzzles/cube4/index.js +4 -0
  30. package/dist/puzzles/cube4/moves.d.ts.map +1 -1
  31. package/dist/puzzles/cube4/moves.js +122 -30
  32. package/dist/puzzles/cube4/reduction/browser-loader.d.ts +26 -1
  33. package/dist/puzzles/cube4/reduction/browser-loader.d.ts.map +1 -1
  34. package/dist/puzzles/cube4/reduction/browser-loader.js +212 -48
  35. package/dist/puzzles/cube4/reduction/center2.d.ts +24 -0
  36. package/dist/puzzles/cube4/reduction/center2.d.ts.map +1 -1
  37. package/dist/puzzles/cube4/reduction/center2.js +99 -1
  38. package/dist/puzzles/cube4/reduction/finish3.d.ts +53 -0
  39. package/dist/puzzles/cube4/reduction/finish3.d.ts.map +1 -0
  40. package/dist/puzzles/cube4/reduction/finish3.js +393 -0
  41. package/dist/puzzles/cube4/reduction/generated/hc-manifest.json +6 -0
  42. package/dist/puzzles/cube4/reduction/hc-pdb.d.ts +5 -3
  43. package/dist/puzzles/cube4/reduction/hc-pdb.d.ts.map +1 -1
  44. package/dist/puzzles/cube4/reduction/hc-pdb.js +30 -12
  45. package/dist/puzzles/cube4/reduction/node-loader.d.ts +10 -1
  46. package/dist/puzzles/cube4/reduction/node-loader.d.ts.map +1 -1
  47. package/dist/puzzles/cube4/reduction/node-loader.js +170 -36
  48. package/dist/puzzles/cube4/reduction/pair-transversal.d.ts +28 -0
  49. package/dist/puzzles/cube4/reduction/pair-transversal.d.ts.map +1 -0
  50. package/dist/puzzles/cube4/reduction/pair-transversal.js +107 -0
  51. package/dist/puzzles/cube4/reduction/phase3-search.d.ts +33 -30
  52. package/dist/puzzles/cube4/reduction/phase3-search.d.ts.map +1 -1
  53. package/dist/puzzles/cube4/reduction/phase3-search.js +211 -104
  54. package/dist/puzzles/cube4/reduction/rotation-fix.d.ts +28 -0
  55. package/dist/puzzles/cube4/reduction/rotation-fix.d.ts.map +1 -0
  56. package/dist/puzzles/cube4/reduction/rotation-fix.js +212 -0
  57. package/dist/puzzles/cube4/reduction/solve.d.ts +72 -13
  58. package/dist/puzzles/cube4/reduction/solve.d.ts.map +1 -1
  59. package/dist/puzzles/cube4/reduction/solve.js +374 -92
  60. package/dist/puzzles/cube4/scramble-cache.d.ts +93 -0
  61. package/dist/puzzles/cube4/scramble-cache.d.ts.map +1 -0
  62. package/dist/puzzles/cube4/scramble-cache.js +150 -0
  63. package/dist/puzzles/cube4/scrambler.d.ts +112 -18
  64. package/dist/puzzles/cube4/scrambler.d.ts.map +1 -1
  65. package/dist/puzzles/cube4/scrambler.js +271 -83
  66. package/dist/puzzles/cube4/worker-pool.d.ts +139 -0
  67. package/dist/puzzles/cube4/worker-pool.d.ts.map +1 -0
  68. package/dist/puzzles/cube4/worker-pool.js +405 -0
  69. package/dist/puzzles/cube4/worker-protocol.d.ts +58 -0
  70. package/dist/puzzles/cube4/worker-protocol.d.ts.map +1 -0
  71. package/dist/puzzles/cube4/worker-protocol.js +58 -0
  72. package/dist/puzzles/cube4/worker.d.ts +26 -0
  73. package/dist/puzzles/cube4/worker.d.ts.map +1 -0
  74. package/dist/puzzles/cube4/worker.js +49 -0
  75. package/dist/puzzles/pyraminx/solver/browser-loader.d.ts +4 -8
  76. package/dist/puzzles/pyraminx/solver/browser-loader.d.ts.map +1 -1
  77. package/dist/puzzles/pyraminx/solver/browser-loader.js +44 -15
  78. package/dist/puzzles/pyraminx/solver/generated-assets.d.ts.map +1 -1
  79. package/dist/puzzles/pyraminx/solver/generated-assets.js +8 -3
  80. package/dist/puzzles/shared/compressed-fetch.d.ts +37 -1
  81. package/dist/puzzles/shared/compressed-fetch.d.ts.map +1 -1
  82. package/dist/puzzles/shared/compressed-fetch.js +226 -31
  83. package/dist/puzzles/shared/load-once.d.ts +16 -0
  84. package/dist/puzzles/shared/load-once.d.ts.map +1 -0
  85. package/dist/puzzles/shared/load-once.js +11 -0
  86. package/dist/puzzles/skewb/solver/browser-loader.d.ts +4 -11
  87. package/dist/puzzles/skewb/solver/browser-loader.d.ts.map +1 -1
  88. package/dist/puzzles/skewb/solver/browser-loader.js +44 -16
  89. package/dist/puzzles/skewb/solver/generated-assets.d.ts.map +1 -1
  90. package/dist/puzzles/skewb/solver/generated-assets.js +8 -3
  91. package/dist/puzzles/sq1/solver/browser-loader.d.ts.map +1 -1
  92. package/dist/puzzles/sq1/solver/browser-loader.js +58 -26
  93. package/dist-cjs/core/api.js +10 -2
  94. package/dist-cjs/puzzles/cube2/solver/browser-loader.js +42 -14
  95. package/dist-cjs/puzzles/cube2/solver/generated-assets.js +8 -3
  96. package/dist-cjs/puzzles/cube3/solver/browser-loader.js +42 -20
  97. package/dist-cjs/puzzles/cube3/solver/generated-assets.js +9 -3
  98. package/dist-cjs/puzzles/cube3/solver/phase1.js +29 -6
  99. package/dist-cjs/puzzles/cube3/solver/phase2.js +56 -15
  100. package/dist-cjs/puzzles/cube3/solver/tables.js +14 -12
  101. package/dist-cjs/puzzles/cube4/browser-worker-pool.js +211 -0
  102. package/dist-cjs/puzzles/cube4/index.js +21 -1
  103. package/dist-cjs/puzzles/cube4/moves.js +122 -30
  104. package/dist-cjs/puzzles/cube4/reduction/browser-loader.js +212 -48
  105. package/dist-cjs/puzzles/cube4/reduction/center2.js +102 -2
  106. package/dist-cjs/puzzles/cube4/reduction/finish3.js +398 -0
  107. package/dist-cjs/puzzles/cube4/reduction/generated/hc-manifest.json +6 -0
  108. package/dist-cjs/puzzles/cube4/reduction/hc-pdb.js +31 -12
  109. package/dist-cjs/puzzles/cube4/reduction/node-loader.js +171 -36
  110. package/dist-cjs/puzzles/cube4/reduction/pair-transversal.js +112 -0
  111. package/dist-cjs/puzzles/cube4/reduction/phase3-search.js +211 -104
  112. package/dist-cjs/puzzles/cube4/reduction/rotation-fix.js +216 -0
  113. package/dist-cjs/puzzles/cube4/reduction/solve.js +374 -88
  114. package/dist-cjs/puzzles/cube4/scramble-cache.js +156 -0
  115. package/dist-cjs/puzzles/cube4/scrambler.js +271 -80
  116. package/dist-cjs/puzzles/cube4/worker-pool.js +447 -0
  117. package/dist-cjs/puzzles/cube4/worker-protocol.js +62 -0
  118. package/dist-cjs/puzzles/pyraminx/solver/browser-loader.js +44 -15
  119. package/dist-cjs/puzzles/pyraminx/solver/generated-assets.js +8 -3
  120. package/dist-cjs/puzzles/shared/compressed-fetch.js +229 -31
  121. package/dist-cjs/puzzles/shared/load-once.js +14 -0
  122. package/dist-cjs/puzzles/skewb/solver/browser-loader.js +44 -16
  123. package/dist-cjs/puzzles/skewb/solver/generated-assets.js +8 -3
  124. package/dist-cjs/puzzles/sq1/solver/browser-loader.js +58 -26
  125. package/docs/development.md +19 -7
  126. package/package.json +4 -3
@@ -1,20 +1,59 @@
1
1
  "use strict";
2
2
  /**
3
3
  * Node loader for the cube4 reduction tables (the committed .bin files).
4
- * The manifests are per-phase (`center1-manifest.json`, ...).
4
+ * The manifests are per-phase (`center1-manifest.json`, ...) plus the shared
5
+ * `hc-manifest.json`, which also seals the optional `c3_pair_dist.bin`.
6
+ *
7
+ * Every .bin read is verified: sha256 checksum AND expected byte length
8
+ * (derived from the manifest `range` + the known packing of each table).
9
+ * All integrity errors name the file and the expected/actual sizes so a
10
+ * corrupted download fails loudly instead of silently producing divergent
11
+ * scrambles. browser-loader.ts performs the identical checks.
5
12
  */
6
13
  var __importDefault = (this && this.__importDefault) || function (mod) {
7
14
  return (mod && mod.__esModule) ? mod : { "default": mod };
8
15
  };
9
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.estimateCube4ReductionTableBytes = estimateCube4ReductionTableBytes;
10
18
  exports.loadCube4ReductionTables = loadCube4ReductionTables;
11
19
  const node_fs_1 = require("node:fs");
12
20
  const node_module_1 = require("node:module");
13
21
  const node_path_1 = __importDefault(require("node:path"));
14
22
  const node_crypto_1 = require("node:crypto");
23
+ const center3_js_1 = require("./center3.js");
15
24
  const edge3_js_1 = require("./edge3.js");
16
25
  const hc_pdb_js_1 = require("./hc-pdb.js");
26
+ const TABLE_MEMORY_BUDGET_BYTES = 150 * 1024 * 1024;
27
+ /** Estimated committed table bytes per solver instance (excludes JS objects). */
28
+ function estimateCube4ReductionTableBytes(tables) {
29
+ return (tables.center1.distance.byteLength +
30
+ tables.center1.moveMaps.byteLength +
31
+ tables.center2.ctMove.byteLength +
32
+ tables.center2.rlMove.byteLength +
33
+ tables.center2.pruning.byteLength +
34
+ (tables.center2.pruningWide?.byteLength ?? 0) +
35
+ tables.center3.moveTable.byteLength +
36
+ tables.center3.pruning.byteLength +
37
+ tables.edge3.distances.byteLength +
38
+ (tables.hc?.moves.byteLength ?? 0) +
39
+ (tables.hc?.distances.byteLength ?? 0) +
40
+ (tables.hc?.coordinates.byteLength ?? 0) +
41
+ (tables.c3PairDist?.byteLength ?? 0));
42
+ }
43
+ function assertCube4ReductionTableBudget(tables) {
44
+ const bytes = estimateCube4ReductionTableBytes(tables);
45
+ if (bytes > TABLE_MEMORY_BUDGET_BYTES) {
46
+ throw new Error(`Cube4 reduction tables use ${(bytes / (1024 * 1024)).toFixed(1)} MiB, above the ` +
47
+ `${(TABLE_MEMORY_BUDGET_BYTES / (1024 * 1024)).toFixed(0)} MiB table budget.`);
48
+ }
49
+ }
17
50
  const GENERATED_DIRECTORY = "puzzles/cube4/reduction/generated";
51
+ /**
52
+ * Byte length of `c3_pair_dist.bin`: CENTER3_STATES x 144 nibble entries
53
+ * (index = ct3 * 144 + f * 12 + h, see phase3-search.ts), nibble-packed.
54
+ * The same constant is enforced in browser-loader.ts.
55
+ */
56
+ const C3_PAIR_DIST_BYTES = (center3_js_1.CENTER3_STATES * 144) / 2;
18
57
  function resolveGeneratedAssetDirectory() {
19
58
  const envDirectory = process.env.CUBINGCENTER_SCRAMBLER_TABLES_DIR;
20
59
  if (envDirectory && (0, node_fs_1.existsSync)(node_path_1.default.join(envDirectory, "center1-manifest.json"))) {
@@ -47,25 +86,97 @@ function getAssetDirectoryCandidates() {
47
86
  function sha256(buffer) {
48
87
  return (0, node_crypto_1.createHash)("sha256").update(buffer).digest("hex");
49
88
  }
50
- function readChecked(file, expectedChecksum) {
51
- const buffer = (0, node_fs_1.readFileSync)(file);
52
- if (sha256(buffer) !== expectedChecksum) {
53
- throw new Error(`Table checksum mismatch: ${file}`);
89
+ function readManifest(file) {
90
+ let text;
91
+ try {
92
+ text = (0, node_fs_1.readFileSync)(file, "utf8");
93
+ }
94
+ catch (error) {
95
+ if (error.code === "ENOENT") {
96
+ throw new Error(`Table manifest missing: ${file} — run pnpm cube4:center1:tables etc. to regenerate the tables.`);
97
+ }
98
+ throw error;
99
+ }
100
+ try {
101
+ return JSON.parse(text);
102
+ }
103
+ catch (error) {
104
+ // Always name the file: a bare JSON.parse error is unactionable.
105
+ throw new Error(`Invalid table manifest JSON: ${file}: ${error instanceof Error ? error.message : String(error)}`);
106
+ }
107
+ }
108
+ function requireEntry(manifest, manifestPath, label) {
109
+ const entry = manifest[label];
110
+ if (!entry || typeof entry.file !== "string" || typeof entry.checksum !== "string") {
111
+ throw new Error(`Invalid table manifest ${manifestPath}: entry "${label}" must declare { file, checksum } — ` +
112
+ "run pnpm cube4:hc:tables etc. to regenerate the manifest.");
113
+ }
114
+ return entry;
115
+ }
116
+ /**
117
+ * Expected byte length of a table file from its manifest entry.
118
+ * `packing` matches how each table is serialized (see the load* functions).
119
+ */
120
+ function expectedBytes(entry, packing) {
121
+ if (typeof entry.bytes === "number")
122
+ return entry.bytes;
123
+ if (typeof entry.range !== "number")
124
+ return undefined;
125
+ if (packing === "byte")
126
+ return entry.range;
127
+ if (packing === "nibble")
128
+ return Math.ceil(entry.range / 2);
129
+ return entry.range * 4;
130
+ }
131
+ function readChecked(file, expectedChecksum, expectedLength) {
132
+ let buffer;
133
+ try {
134
+ buffer = (0, node_fs_1.readFileSync)(file);
135
+ }
136
+ catch (error) {
137
+ if (error.code === "ENOENT") {
138
+ throw new Error(`Table file missing: ${file}` +
139
+ (expectedLength !== undefined ? ` (expected ${expectedLength} bytes)` : "") +
140
+ " — run pnpm cube4:center1:tables etc. to regenerate the tables.");
141
+ }
142
+ throw error;
143
+ }
144
+ if (expectedLength !== undefined && buffer.length !== expectedLength) {
145
+ throw new Error(`Table size mismatch: ${file} (expected ${expectedLength} bytes, got ${buffer.length})`);
146
+ }
147
+ const actual = sha256(buffer);
148
+ if (actual !== expectedChecksum) {
149
+ throw new Error(`Table checksum mismatch: ${file} (expected sha256 ${expectedChecksum}, got ${actual}, ${buffer.length} bytes)`);
54
150
  }
55
151
  return buffer;
56
152
  }
153
+ function readCheckedEntry(file, entry, packing) {
154
+ return readChecked(file, entry.checksum, expectedBytes(entry, packing));
155
+ }
57
156
  function readInt32(buffer) {
58
157
  return new Int32Array(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
59
158
  }
60
159
  function loadCenter1(assetDirectory) {
61
- const manifest = JSON.parse((0, node_fs_1.readFileSync)(node_path_1.default.join(assetDirectory, "center1-manifest.json"), "utf8"));
160
+ const manifestPath = node_path_1.default.join(assetDirectory, "center1-manifest.json");
161
+ const manifest = readManifest(manifestPath);
162
+ const distanceTable = requireEntry(manifest, manifestPath, "distanceTable");
163
+ const moveMaps = requireEntry(manifest, manifestPath, "moveMaps");
62
164
  return {
63
- distance: new Uint8Array(readChecked(node_path_1.default.join(assetDirectory, manifest.distanceTable.file), manifest.distanceTable.checksum)),
64
- moveMaps: readInt32(readChecked(node_path_1.default.join(assetDirectory, manifest.moveMaps.file), manifest.moveMaps.checksum))
165
+ distance: new Uint8Array(readCheckedEntry(node_path_1.default.join(assetDirectory, distanceTable.file), distanceTable, "nibble")),
166
+ moveMaps: readInt32(readCheckedEntry(node_path_1.default.join(assetDirectory, moveMaps.file), moveMaps, "int32"))
65
167
  };
66
168
  }
67
169
  function loadCenter2(assetDirectory) {
68
- const manifest = JSON.parse((0, node_fs_1.readFileSync)(node_path_1.default.join(assetDirectory, "center2-manifest.json"), "utf8"));
170
+ const manifestPath = node_path_1.default.join(assetDirectory, "center2-manifest.json");
171
+ const manifest = readManifest(manifestPath);
172
+ const ctMove = requireEntry(manifest, manifestPath, "ctMove");
173
+ const rlMove = requireEntry(manifest, manifestPath, "rlMove");
174
+ const pruningTable = requireEntry(manifest, manifestPath, "pruningTable");
175
+ // Tolerates older assets without the wide table (wide searches throw
176
+ // a clear error only when actually requested).
177
+ const pruningWideEntry = manifest.pruningWideTable
178
+ ? requireEntry(manifest, manifestPath, "pruningWideTable")
179
+ : undefined;
69
180
  function unpackPruning(buffer) {
70
181
  const pruning = new Uint8Array(450450);
71
182
  for (let i = 0; i < 450450; i++) {
@@ -74,53 +185,73 @@ function loadCenter2(assetDirectory) {
74
185
  }
75
186
  return pruning;
76
187
  }
77
- const pruningBuffer = readChecked(node_path_1.default.join(assetDirectory, manifest.pruningTable.file), manifest.pruningTable.checksum);
188
+ const pruningBuffer = readCheckedEntry(node_path_1.default.join(assetDirectory, pruningTable.file), pruningTable, "nibble");
78
189
  const tables = {
79
- ctMove: readInt32(readChecked(node_path_1.default.join(assetDirectory, manifest.ctMove.file), manifest.ctMove.checksum)),
80
- rlMove: readInt32(readChecked(node_path_1.default.join(assetDirectory, manifest.rlMove.file), manifest.rlMove.checksum)),
190
+ ctMove: readInt32(readCheckedEntry(node_path_1.default.join(assetDirectory, ctMove.file), ctMove, "int32")),
191
+ rlMove: readInt32(readCheckedEntry(node_path_1.default.join(assetDirectory, rlMove.file), rlMove, "int32")),
81
192
  pruning: unpackPruning(pruningBuffer)
82
193
  };
83
- // Tolerates older assets without the wide table (wide searches throw
84
- // a clear error only when actually requested).
85
- if (manifest.pruningWideTable) {
86
- const wideBuffer = readChecked(node_path_1.default.join(assetDirectory, manifest.pruningWideTable.file), manifest.pruningWideTable.checksum);
194
+ if (pruningWideEntry) {
195
+ const wideBuffer = readCheckedEntry(node_path_1.default.join(assetDirectory, pruningWideEntry.file), pruningWideEntry, "nibble");
87
196
  tables.pruningWide = unpackPruning(wideBuffer);
88
197
  }
89
198
  return tables;
90
199
  }
91
200
  function loadCenter3(assetDirectory) {
92
- const manifest = JSON.parse((0, node_fs_1.readFileSync)(node_path_1.default.join(assetDirectory, "center3-manifest.json"), "utf8"));
201
+ const manifestPath = node_path_1.default.join(assetDirectory, "center3-manifest.json");
202
+ const manifest = readManifest(manifestPath);
203
+ const moveTable = requireEntry(manifest, manifestPath, "moveTable");
204
+ const pruningTable = requireEntry(manifest, manifestPath, "pruningTable");
93
205
  return {
94
- moveTable: readInt32(readChecked(node_path_1.default.join(assetDirectory, manifest.moveTable.file), manifest.moveTable.checksum)),
95
- pruning: new Uint8Array(readChecked(node_path_1.default.join(assetDirectory, manifest.pruningTable.file), manifest.pruningTable.checksum))
206
+ moveTable: readInt32(readCheckedEntry(node_path_1.default.join(assetDirectory, moveTable.file), moveTable, "int32")),
207
+ pruning: new Uint8Array(readCheckedEntry(node_path_1.default.join(assetDirectory, pruningTable.file), pruningTable, "byte"))
96
208
  };
97
209
  }
98
210
  function loadEdge3(assetDirectory) {
99
- const manifest = JSON.parse((0, node_fs_1.readFileSync)(node_path_1.default.join(assetDirectory, "edge3-manifest.json"), "utf8"));
211
+ const manifestPath = node_path_1.default.join(assetDirectory, "edge3-manifest.json");
212
+ const manifest = readManifest(manifestPath);
213
+ const distances = requireEntry(manifest, manifestPath, "distances");
214
+ // `range` here already counts packed nibble BYTES (classCount / 2), unlike
215
+ // the other manifests — derive the length from classCount instead.
216
+ const classCount = manifest.classCount;
217
+ const length = typeof classCount === "number" ? Math.ceil(classCount / 2) : expectedBytes(distances, "byte");
100
218
  // Keep the nibble-packed distances as-is (15.5MB instead of 31MB): every
101
219
  // reader (phase3 getPrun, solve.ts, edge3DistanceLookup) handles both
102
220
  // formats, and the smaller working set halves edge-table DRAM traffic.
103
- const distances = new Uint8Array(readChecked(node_path_1.default.join(assetDirectory, manifest.distances.file), manifest.distances.checksum));
104
- return { distances, distanceOfEdge: (0, edge3_js_1.edge3DistanceLookup)(distances) };
221
+ const buffer = readChecked(node_path_1.default.join(assetDirectory, distances.file), distances.checksum, length);
222
+ const table = new Uint8Array(buffer);
223
+ return { distances: table, distanceOfEdge: (0, edge3_js_1.edge3DistanceLookup)(table) };
105
224
  }
106
- function loadHc(assetDirectory) {
225
+ function loadHc(assetDirectory, manifest) {
107
226
  const manifestPath = node_path_1.default.join(assetDirectory, "hc-manifest.json");
108
- if (!(0, node_fs_1.existsSync)(manifestPath))
109
- return undefined;
110
- const manifest = JSON.parse((0, node_fs_1.readFileSync)(manifestPath, "utf8"));
111
- const packed = new Uint8Array(readChecked(node_path_1.default.join(assetDirectory, manifest.distances.file), manifest.distances.checksum));
227
+ const distances = requireEntry(manifest, manifestPath, "distances");
228
+ const packed = new Uint8Array(readCheckedEntry(node_path_1.default.join(assetDirectory, distances.file), distances, "nibble"));
112
229
  const dense = (0, hc_pdb_js_1.buildHcDense)(packed);
113
- if (manifest.moveTable) {
114
- const moveBuffer = readChecked(node_path_1.default.join(assetDirectory, manifest.moveTable.file), manifest.moveTable.checksum);
230
+ if (manifest.moveTable !== undefined) {
231
+ const moveTable = requireEntry(manifest, manifestPath, "moveTable");
232
+ const moveBuffer = readCheckedEntry(node_path_1.default.join(assetDirectory, moveTable.file), moveTable, "int32");
115
233
  dense.moves = new Int32Array(moveBuffer.buffer.slice(moveBuffer.byteOffset, moveBuffer.byteOffset + moveBuffer.byteLength));
116
234
  }
117
235
  return dense;
118
236
  }
119
- function loadC3Pair(assetDirectory) {
237
+ function loadC3Pair(assetDirectory, hcManifest) {
120
238
  const filePath = node_path_1.default.join(assetDirectory, "c3_pair_dist.bin");
121
- if (!(0, node_fs_1.existsSync)(filePath))
239
+ const manifestPath = node_path_1.default.join(assetDirectory, "hc-manifest.json");
240
+ if (!hcManifest || hcManifest.c3PairDist === undefined) {
241
+ // Optional table: absent = not deployed (fine). Present but unsealed =
242
+ // the historical silent-corruption hole — refuse to load it unchecked.
243
+ if ((0, node_fs_1.existsSync)(filePath)) {
244
+ throw new Error(`${filePath} exists but ${manifestPath} has no "c3PairDist" checksum entry — refusing to load it ` +
245
+ "unchecked. Regenerate the manifest: pnpm cube4:hc:tables.");
246
+ }
122
247
  return undefined;
123
- return new Uint8Array((0, node_fs_1.readFileSync)(filePath));
248
+ }
249
+ const checked = requireEntry(hcManifest, manifestPath, "c3PairDist");
250
+ if (typeof checked.bytes === "number" && checked.bytes !== C3_PAIR_DIST_BYTES) {
251
+ throw new Error(`Invalid table manifest ${manifestPath}: c3PairDist.bytes is ${checked.bytes}, expected ` +
252
+ `${C3_PAIR_DIST_BYTES} — run pnpm cube4:hc:tables to regenerate it.`);
253
+ }
254
+ return new Uint8Array(readChecked(node_path_1.default.join(assetDirectory, checked.file), checked.checksum, C3_PAIR_DIST_BYTES));
124
255
  }
125
256
  /** Load all cube4 reduction tables (throws if the generated assets are missing). */
126
257
  function loadCube4ReductionTables() {
@@ -134,11 +265,15 @@ function loadCube4ReductionTables() {
134
265
  center3: loadCenter3(assetDirectory),
135
266
  edge3: loadEdge3(assetDirectory)
136
267
  };
137
- const hc = loadHc(assetDirectory);
138
- if (hc)
139
- tables.hc = hc;
140
- const c3Pair = loadC3Pair(assetDirectory);
268
+ // Shared manifest for the two optional PDB tables; absent = not deployed.
269
+ const hcManifestPath = node_path_1.default.join(assetDirectory, "hc-manifest.json");
270
+ const hcManifest = (0, node_fs_1.existsSync)(hcManifestPath) ? readManifest(hcManifestPath) : undefined;
271
+ if (hcManifest) {
272
+ tables.hc = loadHc(assetDirectory, hcManifest);
273
+ }
274
+ const c3Pair = loadC3Pair(assetDirectory, hcManifest);
141
275
  if (c3Pair)
142
276
  tables.c3PairDist = c3Pair;
277
+ assertCube4ReductionTableBudget(tables);
143
278
  return tables;
144
279
  }
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ /**
3
+ * Phase-2 wing-transversal pruning (two-pair PDB).
4
+ *
5
+ * A phase-2 goal is only usable when the wings are "transversal": every
6
+ * pair has exactly one wing in the second-half positions
7
+ * (`PAIRING_SECOND_HALVES`). Only ~0.1-0.25% of center goals pass, so the
8
+ * phase-2 search used to enumerate millions of center goals to find the
9
+ * transversal ones. This table bounds how many phase-2 moves two given pairs
10
+ * need to become transversal, so the search can prune branches that cannot
11
+ * end transversal within the remaining depth.
12
+ *
13
+ * State: the unordered positions of the two wings of pair X and of pair Y
14
+ * (276 × 276 index; overlapping positions are unreachable). The predicate
15
+ * only depends on positions, so ONE table serves every pair-of-pairs.
16
+ * Exact BFS distances under the given phase-2 move prefix (the search set),
17
+ * hence admissible; generated at runtime (76,176 bytes, a few ms).
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.PAIR_INDEX = exports.PAIR_STATES = void 0;
21
+ exports.getWingPositionMoves = getWingPositionMoves;
22
+ exports.getTransversalPairPdb = getTransversalPairPdb;
23
+ const center2_js_1 = require("./center2.js");
24
+ const pairing_coordinate_js_1 = require("./pairing-coordinate.js");
25
+ exports.PAIR_STATES = 276;
26
+ /** Unordered position pair (a != b) → 0..275, as a flat 24×24 table. */
27
+ exports.PAIR_INDEX = (() => {
28
+ const index = new Uint16Array(24 * 24).fill(0xffff);
29
+ let k = 0;
30
+ for (let a = 0; a < 24; a++) {
31
+ for (let b = a + 1; b < 24; b++) {
32
+ index[a * 24 + b] = k;
33
+ index[b * 24 + a] = k;
34
+ k++;
35
+ }
36
+ }
37
+ return index;
38
+ })();
39
+ const PAIR_A = new Uint8Array(exports.PAIR_STATES);
40
+ const PAIR_B = new Uint8Array(exports.PAIR_STATES);
41
+ for (let a = 0; a < 24; a++) {
42
+ for (let b = a + 1; b < 24; b++) {
43
+ const k = exports.PAIR_INDEX[a * 24 + b];
44
+ PAIR_A[k] = a;
45
+ PAIR_B[k] = b;
46
+ }
47
+ }
48
+ const IS_SECOND = new Uint8Array(24);
49
+ for (const p of pairing_coordinate_js_1.PAIRING_SECOND_HALVES)
50
+ IS_SECOND[p] = 1;
51
+ /**
52
+ * Wing position maps: `posMove[m * 24 + p]` = where the wing at position p
53
+ * goes under CENTER2_MOVES[m] (inverse of the `wp` gather maps).
54
+ */
55
+ function getWingPositionMoves() {
56
+ if (cachedPosMove)
57
+ return cachedPosMove;
58
+ const gather = (0, center2_js_1.getWingMoveMaps)();
59
+ const posMove = new Uint8Array(center2_js_1.CENTER2_MOVES.length * 24);
60
+ for (let m = 0; m < center2_js_1.CENTER2_MOVES.length; m++) {
61
+ for (let i = 0; i < 24; i++)
62
+ posMove[m * 24 + gather[m * 24 + i]] = i;
63
+ }
64
+ cachedPosMove = posMove;
65
+ return posMove;
66
+ }
67
+ let cachedPosMove;
68
+ const cache = new Map();
69
+ /** Two-pair transversal distances under the first `moveCount` CENTER2_MOVES. */
70
+ function getTransversalPairPdb(moveCount) {
71
+ const hit = cache.get(moveCount);
72
+ if (hit)
73
+ return hit;
74
+ const posMove = getWingPositionMoves();
75
+ const n = exports.PAIR_STATES * exports.PAIR_STATES;
76
+ const dist = new Uint8Array(n).fill(0xff);
77
+ const queue = new Int32Array(n);
78
+ let tail = 0;
79
+ for (let x = 0; x < exports.PAIR_STATES; x++) {
80
+ if (IS_SECOND[PAIR_A[x]] === IS_SECOND[PAIR_B[x]])
81
+ continue;
82
+ for (let y = 0; y < exports.PAIR_STATES; y++) {
83
+ if (IS_SECOND[PAIR_A[y]] === IS_SECOND[PAIR_B[y]])
84
+ continue;
85
+ const ya = PAIR_A[y];
86
+ const yb = PAIR_B[y];
87
+ if (ya === PAIR_A[x] || ya === PAIR_B[x] || yb === PAIR_A[x] || yb === PAIR_B[x])
88
+ continue;
89
+ const s = x * exports.PAIR_STATES + y;
90
+ dist[s] = 0;
91
+ queue[tail++] = s;
92
+ }
93
+ }
94
+ for (let head = 0; head < tail; head++) {
95
+ const s = queue[head];
96
+ const x = (s / exports.PAIR_STATES) | 0;
97
+ const y = s - x * exports.PAIR_STATES;
98
+ const d = dist[s] + 1;
99
+ for (let m = 0; m < moveCount; m++) {
100
+ const row = m * 24;
101
+ const nx = exports.PAIR_INDEX[posMove[row + PAIR_A[x]] * 24 + posMove[row + PAIR_B[x]]];
102
+ const ny = exports.PAIR_INDEX[posMove[row + PAIR_A[y]] * 24 + posMove[row + PAIR_B[y]]];
103
+ const t = nx * exports.PAIR_STATES + ny;
104
+ if (dist[t] === 0xff) {
105
+ dist[t] = d;
106
+ queue[tail++] = t;
107
+ }
108
+ }
109
+ }
110
+ cache.set(moveCount, dist);
111
+ return dist;
112
+ }