@wodzik/cubecore 0.1.9 → 0.1.11

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 CHANGED
@@ -13,8 +13,8 @@ headless packages also run in Node / Bun / workers.
13
13
 
14
14
  | package | what |
15
15
  |---|---|
16
- | `@wodzik/cubecore/core` | state (sticker permutation + centre spins), notation (parse / format / invert / simplify / mirror, `(…)3`, `[A, B]`, `.` pauses, comments, source ranges), metrics, 24 frames and colour neutrality, check primitives, method engine (`Method`, `MethodTracker`), masks, pieces view (Kociemba), 15-char state codec, facelet strings, face turns as a smart cube reports them (`toFaceTurns`, `OrientationTracker`), live move log (`MoveCollapser`: R R → R2, R L' → M), following a scramble / algorithm (`SequenceTracker`) |
17
- | `@wodzik/cubecore/cfop`, `roux`, `zz`, `petrus`, `lbl` | methods: stages, their checks, masks, trainer stages; CFOP: OLL / PLL recognition (colour neutral, speedcubedb numbering and names; which case came up after F2L / OLL in a solve) |
16
+ | `@wodzik/cubecore/core` | state (sticker permutation + centre spins), notation (parse / format / invert / simplify / mirror, `(…)3`, `[A, B]`, `.` pauses, comments, source ranges), metrics, 24 frames and colour neutrality, check primitives, method engine (`Method`, `MethodTracker`), masks, pieces view (Kociemba), 15-char state codec, facelet strings, face turns as a smart cube reports them (`toFaceTurns`, `OrientationTracker`), live move log (`MoveCollapser`: R R → R2, R L' → M), following a scramble / algorithm (`SequenceTracker`), which algorithm moves did — by effect, any notation (`AlgMatcher`) |
17
+ | `@wodzik/cubecore/cfop`, `roux`, `zz`, `petrus`, `lbl` | methods: stages, their checks, masks, trainer stages; CFOP: F2L / OLL / PLL recognition (colour neutral, speedcubedb numbering and names; which case came up after F2L / OLL in a solve), F2L tables from any set (`F2LCaseTable` — e.g. advanced F2L, a piece stuck in another slot) |
18
18
  | `@wodzik/cubecore/methods` | all methods together, masks by name |
19
19
  | `@wodzik/cubecore/solve` | two-phase solver (≤ 21 moves, ms), optimal stage solvers (cross, EOCross, xcross, xxcross, slot, Roux blocks), random-state scrambles with presets, trainer scrambles (a stage in exactly N moves), from the cube's current state, any face; Web Worker with a Promise API; tables kept in IndexedDB |
20
20
  | `@wodzik/cubecore/timeline` | recordings, replay clock (moves end at their recorded time), stage timings, sections for progress bars, pause compression, recording and share codecs |
@@ -5,3 +5,6 @@ export * from "./simulated.js";
5
5
  export * from "./cubeSkins.js";
6
6
  export * from "./grips.js";
7
7
  export * from "./rotations.js";
8
+ export * from "./timer.js";
9
+ /** The MAC a cube's connection found (kept per device — QiYi's handshake needs it; offer it back when asking the user for one). */
10
+ export { getCachedMacForDevice, removeCachedMacForDevice } from "./vendor/smartcube-web-bluetooth/smartcube/attachment/address-hints.js";
@@ -5,3 +5,6 @@ export * from "./simulated.js";
5
5
  export * from "./cubeSkins.js";
6
6
  export * from "./grips.js";
7
7
  export * from "./rotations.js";
8
+ export * from "./timer.js";
9
+ /** The MAC a cube's connection found (kept per device — QiYi's handshake needs it; offer it back when asking the user for one). */
10
+ export { getCachedMacForDevice, removeCachedMacForDevice } from "./vendor/smartcube-web-bluetooth/smartcube/attachment/address-hints.js";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The GAN smart timer (Halo / Smart Timer) over Bluetooth — hands on / get
3
+ * set / running / stopped, and the recorded times — from the vendored
4
+ * smartcube-web-bluetooth.
5
+ *
6
+ * const timer = await connectGanTimer(); // needs a user gesture
7
+ * timer.events$.subscribe((e) => { if (e.state === GanTimerState.STOPPED) … });
8
+ */
9
+ export { connectGanTimer, GanTimerState, makeTime, makeTimeFromTimestamp } from "./vendor/smartcube-web-bluetooth/gan-smart-timer.js";
10
+ export type { GanTimerConnection, GanTimerEvent, GanTimerTime, GanTimerRecordedTimes } from "./vendor/smartcube-web-bluetooth/gan-smart-timer.js";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The GAN smart timer (Halo / Smart Timer) over Bluetooth — hands on / get
3
+ * set / running / stopped, and the recorded times — from the vendored
4
+ * smartcube-web-bluetooth.
5
+ *
6
+ * const timer = await connectGanTimer(); // needs a user gesture
7
+ * timer.events$.subscribe((e) => { if (e.state === GanTimerState.STOPPED) … });
8
+ */
9
+ export { connectGanTimer, GanTimerState, makeTime, makeTimeFromTimestamp } from "./vendor/smartcube-web-bluetooth/gan-smart-timer.js";
package/cfop/f2l.d.ts CHANGED
@@ -26,3 +26,33 @@ export declare function isStandardF2L(s: State, slot: F2LSlot): boolean;
26
26
  * (not one of the 41).
27
27
  */
28
28
  export declare function recognizeF2L(s: State, slot: F2LSlot): F2LMatch | "solved" | null;
29
+ /** A case of an F2L set, for one slot: its algorithm as held (cross on D, inserting into that slot). */
30
+ export interface F2LCaseSource {
31
+ id: string;
32
+ alg: string;
33
+ }
34
+ export interface F2LSetMatch {
35
+ id: string;
36
+ slot: F2LSlot;
37
+ /** The U turn before the algorithm (moves only pieces in the top layer). */
38
+ preAuf: string;
39
+ alg: string;
40
+ }
41
+ /**
42
+ * Recognition for any set of F2L cases — e.g. "advanced F2L", where a piece
43
+ * of the pair is stuck in another slot. The case's state is its algorithm
44
+ * undone on a solved cube, and a case is told by where the slot's pair is
45
+ * (anywhere — top layer or any slot) and how it's turned, up to a U turn
46
+ * (only pieces in the top layer move with it). Cases the set lists twice
47
+ * under the same pair position keep the first; `duplicates` names the rest.
48
+ */
49
+ export declare class F2LCaseTable {
50
+ readonly slot: F2LSlot;
51
+ private readonly table;
52
+ readonly duplicates: string[];
53
+ constructor(slot: F2LSlot, cases: readonly F2LCaseSource[]);
54
+ get size(): number;
55
+ recognize(s: State): F2LSetMatch | null;
56
+ }
57
+ /** Is a piece of the slot's pair in another slot (not the top layer, not its own) — an "advanced F2L" case? */
58
+ export declare function isTrappedF2L(s: State, slot: F2LSlot): boolean;
package/cfop/f2l.js CHANGED
@@ -86,3 +86,55 @@ export function recognizeF2L(s, slot) {
86
86
  }) ?? "";
87
87
  return { id: e.kase.id, group: e.kase.group, slot, preAuf, alg: e.kase.algs[slot] };
88
88
  }
89
+ /**
90
+ * Recognition for any set of F2L cases — e.g. "advanced F2L", where a piece
91
+ * of the pair is stuck in another slot. The case's state is its algorithm
92
+ * undone on a solved cube, and a case is told by where the slot's pair is
93
+ * (anywhere — top layer or any slot) and how it's turned, up to a U turn
94
+ * (only pieces in the top layer move with it). Cases the set lists twice
95
+ * under the same pair position keep the first; `duplicates` names the rest.
96
+ */
97
+ export class F2LCaseTable {
98
+ slot;
99
+ table = new Map();
100
+ duplicates = [];
101
+ constructor(slot, cases) {
102
+ this.slot = slot;
103
+ for (const source of cases) {
104
+ let s;
105
+ try {
106
+ s = applyMoves(solvedState(), invert(parseAlg(source.alg)));
107
+ }
108
+ catch {
109
+ continue;
110
+ }
111
+ const k = key(s, slot);
112
+ const p = pairOf(s, slot);
113
+ if (!k || !p)
114
+ continue;
115
+ if (this.table.has(k))
116
+ this.duplicates.push(source.id);
117
+ else
118
+ this.table.set(k, { source, raw: pairText(p) });
119
+ }
120
+ }
121
+ get size() {
122
+ return this.table.size;
123
+ }
124
+ recognize(s) {
125
+ const k = key(s, this.slot);
126
+ const e = k ? this.table.get(k) : undefined;
127
+ if (!e)
128
+ return null;
129
+ const preAuf = AUFS.find((u) => {
130
+ const q = pairOf(u ? applyMoves(s, u) : s, this.slot);
131
+ return q !== null && pairText(q) === e.raw;
132
+ }) ?? "";
133
+ return { id: e.source.id, slot: this.slot, preAuf, alg: e.source.alg };
134
+ }
135
+ }
136
+ /** Is a piece of the slot's pair in another slot (not the top layer, not its own) — an "advanced F2L" case? */
137
+ export function isTrappedF2L(s, slot) {
138
+ const p = pairOf(s, slot);
139
+ return !!p && !isStandardF2L(s, slot);
140
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Which algorithm was done — by what it does, not how it's written.
3
+ *
4
+ * An algorithm's EFFECT is the permutation its face turns make, relative to
5
+ * the centres (as a smart cube reports them — toFaceTurns): M' = r R' =
6
+ * R' L; L R' = R' L (turns on one axis commute); U U = U2 = U2';
7
+ * rotations don't count (only where the pieces go relative to the
8
+ * centres). So two writings of one algorithm are one effect, and a solve's
9
+ * moves match it however they were done — with slips undone, in any order
10
+ * on an axis.
11
+ *
12
+ * const m = new AlgMatcher<string>();
13
+ * m.add("R U R' U' R' F R2 U' R' U' R U R' F'", "T-perm");
14
+ * m.matchSuffix(solveMoves) // { data: "T-perm", start: 12, preAuf: "U", postAuf: "" } — moves 0–11 were a setup
15
+ *
16
+ * Up to a turn of the AUF face (U) before and after: an algorithm done from
17
+ * another angle, or finished with an AUF, is the same algorithm.
18
+ */
19
+ import { type Face } from "./geometry.js";
20
+ import { type Move } from "./moves.js";
21
+ export interface AlgMatch<T> {
22
+ data: T;
23
+ /** Index of the first move of the match; the moves before it are a setup. */
24
+ start: number;
25
+ /** The AUF turn done before the algorithm ("" / "U" / "U2" / "U'"), as the moves were held. */
26
+ preAuf: string;
27
+ /** The AUF turn after it. */
28
+ postAuf: string;
29
+ }
30
+ /** The effect of `moves` (any notation) as a key: where every sticker goes, relative to the centres. */
31
+ export declare function effectKey(moves: readonly Move[] | string): string;
32
+ export declare class AlgMatcher<T> {
33
+ private readonly effects;
34
+ private readonly aufs;
35
+ private readonly longest;
36
+ /** @param aufFace the face turned for AUF (U for a last layer, or F2L's top). */
37
+ constructor(aufFace?: Face);
38
+ /** An algorithm and what it stands for; the first added wins a shared effect. Returns false when it can't be read. */
39
+ add(alg: string, data: T): boolean;
40
+ get size(): number;
41
+ /** The algorithm whose effect is exactly that of `moves` (any notation), or null. */
42
+ match(moves: readonly Move[] | string): Omit<AlgMatch<T>, "start"> | null;
43
+ /**
44
+ * The longest ending of `moves` that is one of the algorithms — what was
45
+ * done last, and where it began (the moves before it: a setup, an
46
+ * extraction…). Endings up to a few moves longer than the longest
47
+ * algorithm are tried (slips, AUFs inside). Null when none is.
48
+ */
49
+ matchSuffix(moves: readonly Move[] | string, slack?: number): AlgMatch<T> | null;
50
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Which algorithm was done — by what it does, not how it's written.
3
+ *
4
+ * An algorithm's EFFECT is the permutation its face turns make, relative to
5
+ * the centres (as a smart cube reports them — toFaceTurns): M' = r R' =
6
+ * R' L; L R' = R' L (turns on one axis commute); U U = U2 = U2';
7
+ * rotations don't count (only where the pieces go relative to the
8
+ * centres). So two writings of one algorithm are one effect, and a solve's
9
+ * moves match it however they were done — with slips undone, in any order
10
+ * on an axis.
11
+ *
12
+ * const m = new AlgMatcher<string>();
13
+ * m.add("R U R' U' R' F R2 U' R' U' R U R' F'", "T-perm");
14
+ * m.matchSuffix(solveMoves) // { data: "T-perm", start: 12, preAuf: "U", postAuf: "" } — moves 0–11 were a setup
15
+ *
16
+ * Up to a turn of the AUF face (U) before and after: an algorithm done from
17
+ * another angle, or finished with an AUF, is the same algorithm.
18
+ */
19
+ import { formatMove } from "./moves.js";
20
+ import { parseAlg } from "./notation.js";
21
+ import { OrientationTracker, toFaceTurns } from "./physical.js";
22
+ import { applyMoves, solvedState } from "./state.js";
23
+ const key = (s) => String.fromCharCode(...s);
24
+ /** The effect of `moves` (any notation) as a key: where every sticker goes, relative to the centres. */
25
+ export function effectKey(moves) {
26
+ return key(applyMoves(solvedState(), toFaceTurns(moves).moves));
27
+ }
28
+ export class AlgMatcher {
29
+ effects = new Map();
30
+ aufs;
31
+ longest = { moves: 0 };
32
+ /** @param aufFace the face turned for AUF (U for a last layer, or F2L's top). */
33
+ constructor(aufFace = "U") {
34
+ this.aufs = ["", aufFace, `${aufFace}2`, `${aufFace}'`];
35
+ }
36
+ /** An algorithm and what it stands for; the first added wins a shared effect. Returns false when it can't be read. */
37
+ add(alg, data) {
38
+ let faceTurns;
39
+ try {
40
+ faceTurns = toFaceTurns(parseAlg(alg.replace(/[()]/g, " "))).moves;
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ if (faceTurns.length === 0)
46
+ return false;
47
+ this.longest.moves = Math.max(this.longest.moves, faceTurns.length);
48
+ const body = faceTurns.map(formatMove).join(" ");
49
+ for (const preAuf of this.aufs)
50
+ for (const postAuf of this.aufs) {
51
+ const k = key(applyMoves(solvedState(), [preAuf, body, postAuf].filter(Boolean).join(" ")));
52
+ if (!this.effects.has(k))
53
+ this.effects.set(k, { data, preAuf, postAuf });
54
+ }
55
+ return true;
56
+ }
57
+ get size() {
58
+ return this.effects.size;
59
+ }
60
+ /** The algorithm whose effect is exactly that of `moves` (any notation), or null. */
61
+ match(moves) {
62
+ const e = this.effects.get(effectKey(moves));
63
+ return e ? { ...e } : null;
64
+ }
65
+ /**
66
+ * The longest ending of `moves` that is one of the algorithms — what was
67
+ * done last, and where it began (the moves before it: a setup, an
68
+ * extraction…). Endings up to a few moves longer than the longest
69
+ * algorithm are tried (slips, AUFs inside). Null when none is.
70
+ */
71
+ matchSuffix(moves, slack = 6) {
72
+ const list = typeof moves === "string" ? parseAlg(moves) : moves;
73
+ // One tracker over the whole list: a rotation or wide move keeps holding for the moves after it.
74
+ const t = new OrientationTracker();
75
+ const faceTurns = list.map((m) => t.push(m));
76
+ const from = Math.max(0, list.length - (this.longest.moves + slack));
77
+ for (let start = from; start < list.length; start++) {
78
+ const tail = faceTurns.slice(start).flat();
79
+ const e = this.effects.get(key(applyMoves(solvedState(), tail)));
80
+ if (e)
81
+ return { ...e, start };
82
+ }
83
+ return null;
84
+ }
85
+ }
package/core/index.d.ts CHANGED
@@ -17,3 +17,4 @@ export * from "./sequence.js";
17
17
  export * from "./practice.js";
18
18
  export * from "./arrows.js";
19
19
  export * from "./stages.js";
20
+ export * from "./algMatch.js";
package/core/index.js CHANGED
@@ -17,3 +17,4 @@ export * from "./sequence.js";
17
17
  export * from "./practice.js";
18
18
  export * from "./arrows.js";
19
19
  export * from "./stages.js";
20
+ export * from "./algMatch.js";
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.9",
6
+ "version": "0.1.11",
7
7
  "description": "A 3×3×3 cube library for the web, built around smart (Bluetooth) cubes: state, notation, methods, solvers and scrambles, a three.js renderer with skins, pictures, web components, React bindings.",
8
8
  "license": "MPL-2.0",
9
9
  "author": "Tomasz Wodzikowski <wodziszczakow@gmail.com>",