-
Notifications
You must be signed in to change notification settings - Fork 46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[cubing.js issue] Detect partial solve #317
Comments
You're not missing anything. There are two challenges here:
The naïve implementation of this might take 24 × 24 = 576 comparisons, which isn't great. So what I would do is build a table or all solved "color orientations" normalized to standard orientation, and use any piece to normalize the candidates for import { Alg, AlgBuilder, Move } from "cubing/alg";
import {
KPattern,
type KPatternData,
type KPatternOrbitData,
KTransformation,
} from "cubing/kpuzzle";
import { cube3x3x3 } from "cubing/puzzles";
enum PieceMask {
Regular,
Ignore,
}
const R = PieceMask.Regular;
const I = PieceMask.Ignore;
type PatternMask = Record<string /* orbit name */, PieceMask[]>;
const rouxSecondBlockPatternMask: PatternMask = {
EDGES: [I, I, I, I, I, R, I, R, R, R, R, R],
CORNERS: [I, I, I, I, R, R, R, R],
CENTERS: [I, R, I, R, I, I],
};
const IGNORED_PIECE_VALUE = 99; // TODO: This should really be set to the lowest otherwise unused piece number in the orbit.
function applyPatternMask(pattern: KPattern, mask: PatternMask): KPattern {
const newPatternData: KPatternData = {};
for (const orbitDefinition of kpuzzle.definition.orbits) {
const patternOrbit = pattern.patternData[orbitDefinition.orbitName];
const maskOrbit = mask[orbitDefinition.orbitName];
const newOrbitData: KPatternOrbitData & { orientationMod: number[] } = {
pieces: [],
orientation: [],
orientationMod: [],
};
for (let i = 0; i < orbitDefinition.numPieces; i++) {
switch (maskOrbit[i]) {
case PieceMask.Regular: {
newOrbitData.pieces.push(patternOrbit.pieces[i]);
newOrbitData.orientation.push(patternOrbit.orientation[i]);
newOrbitData.orientationMod.push(
patternOrbit.orientationMod?.[i] ?? 0,
);
break;
}
case PieceMask.Ignore: {
newOrbitData.pieces.push(IGNORED_PIECE_VALUE);
newOrbitData.orientation.push(0);
newOrbitData.orientationMod.push(1);
break;
}
default: {
throw new Error("Unrecognized `PieceMaskAction` value.");
}
}
}
newPatternData[orbitDefinition.orbitName] = newOrbitData;
}
return new KPattern(pattern.kpuzzle, newPatternData);
}
const kpuzzle = await cube3x3x3.kpuzzle();
const cubeOrientations: {
inverseTransformation: KTransformation;
algToNormalize: Alg;
}[] = [];
for (const moveToSetU of [
null,
new Move("x"),
new Move("x2"),
new Move("x'"),
new Move("z"),
new Move("z'"),
]) {
for (const moveToSetF of [
null,
new Move("y"),
new Move("y2"),
new Move("y'"),
]) {
const algBuilder: AlgBuilder = new AlgBuilder();
if (moveToSetU) {
algBuilder.push(moveToSetU);
}
if (moveToSetF) {
algBuilder.push(moveToSetF);
}
const algToNormalize = algBuilder.toAlg();
const inverseTransformation = kpuzzle.algToTransformation(algToNormalize);
cubeOrientations.push({
inverseTransformation,
algToNormalize,
});
}
}
const orientedSolvedPattern: KPattern = kpuzzle.defaultPattern();
const solvedPatternsByDRF: Record<
number /* DRF piece */,
Record<number /* DRF orientation */, KPattern>
> = {};
const DRF_ORBIT = "CORNERS";
const DRF_INDEX = 4;
// Assumes DRF is a piece with known piece number and orientation.
function extractDRFCoordinates(pattern: KPattern): {
pieceDRF: number;
orientationDRF: number;
} {
const orbitData = pattern.patternData[DRF_ORBIT];
if ((orbitData.orientationMod?.[DRF_INDEX] ?? 0) !== 0) {
throw new Error("Unexpected partially known orientation");
}
return {
pieceDRF: orbitData.pieces[DRF_INDEX],
orientationDRF: orbitData.orientation[DRF_INDEX],
};
}
for (const cubeOrientation of cubeOrientations) {
const orientedPattern = orientedSolvedPattern.applyTransformation(
cubeOrientation.inverseTransformation,
);
const maskedPattern = applyPatternMask(
orientedPattern,
rouxSecondBlockPatternMask,
);
const { pieceDRF, orientationDRF } = extractDRFCoordinates(orientedPattern);
const byOrientation = (solvedPatternsByDRF[pieceDRF] ??= {});
byOrientation[orientationDRF] = maskedPattern;
}
function isRouxSecondBlockSolved(
candidateFull3x3x3Pattern: KPattern,
): { isSolved: false } | { isSolved: true; algToNormalize: Alg } {
for (const cubeOrientation of cubeOrientations) {
const reorientedCandidate = candidateFull3x3x3Pattern.applyTransformation(
cubeOrientation.inverseTransformation,
);
const candidateMasked = applyPatternMask(
reorientedCandidate,
rouxSecondBlockPatternMask,
);
const { pieceDRF, orientationDRF } =
extractDRFCoordinates(reorientedCandidate);
const solvedPatternByDRF = solvedPatternsByDRF[pieceDRF][orientationDRF];
if (candidateMasked.isIdentical(solvedPatternByDRF)) {
const { algToNormalize } = cubeOrientation;
return { isSolved: true, algToNormalize };
}
}
return { isSolved: false };
}
function test(candidate: KPattern) {
const isSolvedInfo = isRouxSecondBlockSolved(candidate);
if (isSolvedInfo.isSolved) {
console.log(
`Solved, orient using: ${
isSolvedInfo.algToNormalize.experimentalIsEmpty()
? "(empty alg)"
: isSolvedInfo.algToNormalize
}`,
);
} else {
console.log("Unsolved");
}
}
test(orientedSolvedPattern.applyAlg("U L F R B D")); // Prints: "Unsolved"
test(orientedSolvedPattern.applyAlg("y b U B' U F R2 F' y'")); // Prints: "Solved, orient using: (empty alg)"
test(orientedSolvedPattern.applyAlg("M' U'")); // Prints: "Solved, orient using: (empty alg)"
test(orientedSolvedPattern.applyAlg("F")); // Prints: "Solved, orient using: x"
test(orientedSolvedPattern.applyAlg("b U B' U F R2 F'")); // Prints: "Solved, orient using: y"
test(orientedSolvedPattern.applyAlg("[S', L]")); // Prints: "Solved, orient using: z y" This obviously is a lot of work, but it has most of the ideas I expect to use to implement this in |
Oh wow, I didn't expect a full-blown solution as an answer! You're a star, thanks! I think this all makes sense to me, except for the questions below. I can also simplify this a bit for my use case as I know the cube orientation beforehand, so I don't need to iterate over all orientations to check the solution. But this is great to understand better how everything fits together!
|
We're using it as a lookup table for how to undo a transformation. I admit I haven't 100% thought this through, but this is the direction that gives the correct result.
Yeah, if you want to enforce a certain orientation for blocks you can remove some of the orientations. In any case, this code motivated me to finally put together the missing pieces for https://experiments.cubing.net/cubing.js/live-reconstruction/ — it has some way to go, but turns out it's quite practical to implement pattern detection across multiple solving methods for us now! |
I've been struggling with this code for the last few days as I couldn't quite make it do what I wanted. And I now know why. Let's say you are in "x2 y" orientation, so yellow top, red front, on a solved cube. If you now apply an R move, it's immediately apparent that second block isn't solved anymore. However, the Now this is correct, however, it's not useful for a trainer. When solving Roux, you decide on an orientation during inspection and then never rotate. So what I really want is to limit the solved patterns to those that correspond to the chosen cube orientation. And that's exactly one pattern. However, since slice moves can make the virtual cube think it was rotated (e.g. M or M' can apply a virtual x or x' rotation), I also need to consider the same pattern mask rotated by x, x' and x2 (in case of Roux specifically where M slices are prevalent). So I ended up writing a function that can permute the pattern mask (and the stickering mask) by a given The end result is much simpler for my use so I'm sharing it here in case someone else can make use of it too. Note that since parsing a stickering mask isn't exposed, I had to write that code again (which isn't included here). export interface IPuzzle {
setup(
cubeOrientation: CubeOrientation,
stickeringMask: StickeringMaskData,
patternMask: PatternMask,
additionalSolveOrientations?: Alg[]): void;
setAlg(alg: Alg): void;
isCaseSolved(pattern: KPattern): boolean;
}
export class Puzzle implements IPuzzle {
readonly player: TwistyPlayer;
readonly kpuzzle: KPuzzle;
cubeOrientation: CubeOrientation;
patternMasks: PatternMask[] = [];
solvedPatterns: KPattern[] = [];
constructor(player: TwistyPlayer, kpuzzle: KPuzzle) {
this.player = player;
this.kpuzzle = kpuzzle;
this.cubeOrientation = new CubeOrientation(new Alg(), this.kpuzzle);
}
setup(cubeOrientation: CubeOrientation, stickeringMask: StickeringMaskData, patternMask: PatternMask, additionalSolveOrientations?: Alg[]) {
this.cubeOrientation = cubeOrientation;
this.patternMasks = [];
this.solvedPatterns = [];
var newMask = permuteOrbitArrays(this.kpuzzle, stickeringMask, cubeOrientation.inverseTransformation);
this.player.experimentalModel.twistySceneModel.stickeringMaskRequest.set(serializeStickeringMask(newMask));
// Default pattern + cube orientation
const reorientedDefaultPattern = this.kpuzzle.defaultPattern().applyTransformation(cubeOrientation.inverseTransformation);
this.patternMasks.push(patternMask);
this.solvedPatterns.push(applyPatternMask(this.kpuzzle, reorientedDefaultPattern, patternMask));
// Slice moves can reorient the virtual cube, e.g. M & M' can make it look like we applied additional x rotations.
// Calculate the solved pattern for each of these additional rotations here
if (additionalSolveOrientations) {
for (const overlay of additionalSolveOrientations) {
const transform = this.kpuzzle.algToTransformation(overlay);
const pattern = reorientedDefaultPattern.applyTransformation(transform);
const orientedMask = permuteOrbitArrays(this.kpuzzle, patternMask, transform);
this.patternMasks.push(orientedMask);
this.solvedPatterns.push(applyPatternMask(this.kpuzzle, pattern, orientedMask));
}
}
}
setAlg(alg: Alg): void {
this.player.alg = "";
const fullAlg = this.cubeOrientation.algToNormalize.concat(alg);
this.player.alg = fullAlg;
}
isCaseSolved(pattern: KPattern): boolean {
for (var i = 0; i < this.patternMasks.length; ++i) {
const maskedPattern = applyPatternMask(this.kpuzzle, pattern, this.patternMasks[i]);
if (maskedPattern.isIdentical(this.solvedPatterns[i])) {
return true;
}
}
return false;
}
} Permutation is done with this: function permuteArray<T>(array: T[], permutations: number[]): T[] {
const newArray = [...array];
for (let i = 0; i < permutations.length; ++i) {
newArray[i] = array[permutations[i]];
}
return newArray;
}
export function forAllOrbits(kpuzzle: KPuzzle, callback: (name: string) => void) {
for (const orbit of kpuzzle.definition.orbits) {
callback(orbit.orbitName);
}
}
type ArrayByOrbit<T> = Record<string, T[]>;
export function permuteOrbitArrays<T>(kpuzzle: KPuzzle, data: ArrayByOrbit<T>, transformation: KTransformation): ArrayByOrbit<T> {
const inverseTransformation = transformation.invert();
var outData = Object.assign({}, data);
forAllOrbits(kpuzzle, (name: string) => {
const entry = data[name];
const permutation = inverseTransformation.transformationData[name].permutation;
outData[name] = permuteArray(entry, permutation);
});
return outData;
} |
Steps to reproduce the issue
Hi,
My issue might actually be a non-issue and purely due to lack of understanding on the internals of cubing.js. More documentation around
KPattern
andKTransformation
would help immensely for first timers! (or maybe I just didn't find it?)I'm currently writing a case trainer for Roux Second Block and as part of that I want to detect when the case solved. And I'm not sure what the best way to do that is. There's an exported
experimentalIs3x3x3Solved
function but that checks the entire cube. I just care about the pieces that make up first and second block.Worth noting that I want to support different orientations as well, so have a setup alg like
x2 y
prepended to the case.I thought that I could just compare whether some of the
pieces
in the KPattern's data correspond to their index in the array to check positioning (and check orientation for 0), but that only works in some orientations. For example, both of these patterns are solved, the former is default green front, white top, the latter is the same with ay
move applied, so red front, white top:The pattern is very different but the cube is solved in both cases.
Is there an easy way to check the solved state of specific pieces while respecting orientation? I saw that internally you use
normalize3x3x3Orientation
before checking the solved state, so maybe I need that? But it's not exported.Thanks!
Edit: Maybe this should have been a feature request instead?
Observed behaviour
🖼 Screenshots
No response
Expected behaviour
Environment
Additional info
No response
The text was updated successfully, but these errors were encountered: