Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,17 @@ Push a package-specific tag matching the version in `package.json`, such as `cor
release workflow tests the core, builds and smoke-tests one tarball, retains it as a workflow
artifact for download, and does not publish it to a package registry. Use the matching section of
[CHANGELOG.md](CHANGELOG.md) for the GitHub Release body.

### Center orientation in color facelets

`CubeFacelets.patternDataToFacelets` returns an error for nonzero center
orientations; `patternDataToFaceletsExn` throws. A 54-letter color facelet string
cannot carry picture-cube or supercube center twists. Canonical center positions
are required as before.

Decoding color facelets sets center `orientationMod` to 1, meaning that center
orientation is untracked, rather than known to be zero. Ordinary cubing.js
3×3 default patterns use this convention too, so standard face turns remain
convertible. Keep the original pattern when tracked center orientation matters;
this API does not provide a lossless picture-cube round trip. The Console's
CP/CO/EP/EO and Orbit64 color-state exports likewise do not encode center twists.
6 changes: 4 additions & 2 deletions packages/core/src/domain/CubeFacelets.res
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ let validateCubieState = (cornerPieces, cornerOri, edgePieces, edgeOri): result<
Ok()
}

let centersOriented = (pd: patternData) =>
let centerPositionsCanonical = (pd: patternData) =>
pd.centers.pieces->Array.length == 6 &&
pd.centers.orientation->Array.length == 6 &&
pd.centers.pieces->Array.everyWithIndex((p, i) => p == i)
Expand All @@ -213,8 +213,10 @@ let toReid333Struct = (pd: patternData): result<
(array<string>, array<string>, array<string>),
string,
> =>
if !centersOriented(pd) {
if !centerPositionsCanonical(pd) {
Error("non-oriented puzzles are not supported")
} else if pd.centers.orientation->Array.some(value => value != 0) {
Error("center twists cannot be represented by color facelets")
} else {
switch validateCubieState(
pd.corners.pieces,
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/domain/CubeFacelets.resi
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ let solvedFacelets: string
@genType
let isSolvedFacelets: string => bool

/** Encode validated pattern data, returning `Error` for malformed or impossible orbit data. */
/**
Encode canonical-center color facelets. Returns `Error` for malformed or impossible
cubie data and nonzero center orientations, which a color string cannot represent.
Zero values with `orientationMod: [1, ...]` mean untracked centers, not known twists.
This is not a lossless picture-cube/supercube state encoding.
*/
@genType
let patternDataToFacelets: patternData => result<string, string>

Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/domain/CubeFacelets_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,22 @@ describe("patternDataToFacelets", () => {
t->expect(CubeFacelets.patternDataToFacelets(pd)->Result.isError)->Expect.toBe(true)
})

test("rejects every center twist instead of dropping picture-cube state", t => {
for center in 0 to 5 {
for twist in 1 to 3 {
let orientation = Array.make(~length=6, 0)
orientation->Array.setUnsafe(center, twist)
let pd = {
...solvedPatternData,
centers: {...solvedPatternData.centers, orientation},
}
t
->expect(CubeFacelets.patternDataToFacelets(pd))
->Expect.toEqual(Error("center twists cannot be represented by color facelets"))
}
}
})

test("returns an error instead of throwing for malformed orbit arrays", t => {
let malformed: CubeFacelets.patternData = {
corners: {pieces: [], orientation: []},
Expand Down
38 changes: 38 additions & 0 deletions src/adapters/cubing/patternReconciler.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { describe, expect, it } from 'vitest';
import { KPattern } from 'cubing/kpuzzle';
import { cube3x3x3 } from 'cubing/puzzles';

import * as CubeFacelets from '@wstein/regrip-core/domain/CubeFacelets';
import { createPatternReconciler } from './patternReconciler';
Expand Down Expand Up @@ -38,6 +40,42 @@ describe('Cubing pattern reconciler', () => {
}
});

it('keeps ordinary cubing.js moves with untracked centers convertible', async () => {
const puzzle = await cube3x3x3.kpuzzle();
await kpuzzleReady;
for (const initial of [puzzle.defaultPattern(), faceletsToPattern(solvedFacelets)]) {
for (const face of ['U', 'R', 'F', 'D', 'L', 'B']) {
for (const suffix of ['', "'", '2']) {
const moved = initial.applyMove(face + suffix);
expect(moved.patternData.CENTERS.orientationMod).toEqual([1, 1, 1, 1, 1, 1]);
expect(moved.patternData.CENTERS.orientation).toEqual([0, 0, 0, 0, 0, 0]);
expect(patternToFacelets(moved)).toBe(
CubeFacelets.patternDataToFaceletsExn(
CubeFacelets.applyMove(
CubeFacelets.faceletsToPatternDataExn(solvedFacelets),
face + suffix,
)!,
),
);
}
}
}
});

it('rejects a known picture-cube center twist produced by a face turn', async () => {
const puzzle = await cube3x3x3.kpuzzle();
const data = puzzle.defaultPattern().patternData;
const picture = new KPattern(puzzle, {
...data,
CENTERS: { pieces: [...data.CENTERS.pieces], orientation: [0, 0, 0, 0, 0, 0] },
});
const moved = picture.applyMove('R');
expect(moved.patternData.CENTERS.orientation).toEqual([0, 0, 0, 1, 0, 0]);
expect(() => patternToFacelets(moved)).toThrow(
'center twists cannot be represented by color facelets',
);
});

it('does not request a player reset when a facelet snapshot matches applied moves', async () => {
await kpuzzleReady;
const reconciler = createPatternReconciler();
Expand Down
4 changes: 4 additions & 0 deletions src/adapters/cubing/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ const kpuzzleReady: Promise<void> = cube3x3x3.kpuzzle().then((v) => {
/**
* Convert cubing.js KPattern object to the facelets string in the Kociemba notation
* @param pattern Source KPattern object
* Default and facelet-derived cubing.js patterns have untracked centers
* (`orientationMod: 1`); ordinary face turns remain supported. Known center
* twists in picture-cube patterns cannot be encoded as color facelets.
* @returns String representing cube facelets in the Kociemba notation
* @throws if center positions are noncanonical or a center orientation is nonzero
*/
function patternToFacelets(pattern: KPattern): string {
return CubeFacelets.patternDataToFaceletsExn(asCorePatternData(pattern.patternData));
Expand Down
Loading