Skip to content

Commit

Permalink
Only analyse solve on demand
Browse files Browse the repository at this point in the history
  • Loading branch information
simonkellly committed May 24, 2024
1 parent bea7833 commit 422f840
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 45 deletions.
7 changes: 5 additions & 2 deletions src/lib/db.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Dexie, { Table } from 'dexie';
import { GanCubeMove } from 'gan-web-bluetooth';

export enum Penalty {
SOLVED = 0,
PLUS_TWO = 1,
DNF = 2,
}
Expand All @@ -9,9 +11,10 @@ export interface Solve {
id?: number;
timeStamp: number;
time: number;
now: number;
scramble: string;
solution: string;
parsed: string[];
solution: GanCubeMove[];
parsed?: string[];
penalty?: Penalty;
}

Expand Down
74 changes: 71 additions & 3 deletions src/lib/dnfAnalyser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { Alg } from 'cubing/alg';
import { KPattern, KTransformation } from 'cubing/kpuzzle';
import { cube3x3x3 } from 'cubing/puzzles';
import { extractAlgs } from './solutionParser';
import { Penalty } from './db';

export const SOLVED = 'Solved';
export const PLUS_TWO = '+2';
export const DNF = 'DNF';

function checkIsSolved(pattern: KPattern) {
return pattern.experimentalIsSolved({
Expand Down Expand Up @@ -90,6 +93,8 @@ async function check1MoveDnf(

const newAnalysis = await extractAlgs(solutionWithMove);
const brokenAlgIdx = newAnalysis.findIndex(a => a[2] > solutionIdx);
if (solutionIdx == solution.length) return PLUS_TWO;
console.log('Broken alg:', brokenAlgIdx, newAnalysis, solutionIdx);
return algs[brokenAlgIdx] + ' -> ' + newAnalysis[brokenAlgIdx][0];
}
}
Expand Down Expand Up @@ -126,13 +131,78 @@ function checkWrongOrderAlg(scramble: KPattern, algs: string[]) {
return false;
}

function checkPlusTwo(solvedState: KTransformation, solvedPattern: KPattern) {
const corners = solvedState.transformationData['CORNERS'];
const edges = solvedState.transformationData['EDGES'];

let cornerCount = 0;
for (let i = 0; i < 8; i++) {
const positionMatches = corners.permutation[i] == i;
const orientationMatches = corners.orientationDelta[i] == 0;

if (positionMatches && orientationMatches) cornerCount++;
}

let edgeCount = 0;
for (let i = 0; i < 12; i++) {
const positionMatches = edges.permutation[i] == i;
const orientationMatches = edges.orientationDelta[i] == 0;

if (positionMatches && orientationMatches) edgeCount++;
}

const isOneMove = cornerCount == 4 && edgeCount == 8;
if (!isOneMove) {
console.log('Not one move from state');
return false;
}

const moves = ['U', 'D', 'R', 'L', 'F', 'B'];

const checkedState = solvedPattern;
for (const move of moves) {
let moveState = checkedState;
for (let attempt = 0; attempt < 3; attempt++) {
moveState = moveState.applyMove(move);

const isSolved = checkIsSolved(moveState);

if (isSolved) {
return PLUS_TWO;
}
}
}
return false;

}

export async function penaltyChecker(scramble: string, solution: string): Promise<Penalty> {
const puzzle = await cube3x3x3.kpuzzle();

const scrambleTransformation = puzzle.algToTransformation(scramble);
const solutionTransformation = puzzle.algToTransformation(solution);

const totalTransformation = scrambleTransformation.applyTransformation(
solutionTransformation
);
const totalPattern = totalTransformation.toKPattern();
const isSolved = checkIsSolved(totalPattern);

if (isSolved) return Penalty.SOLVED;

const plusTwo = checkPlusTwo(totalTransformation, totalPattern);
if (plusTwo) return Penalty.PLUS_TWO;

return Penalty.DNF;
}

// TODO: If parsing algs failed, reverse scramble and go from other side
// TODO: Check if a wrong alg was done instead of a correct one
// TODO: Check for missed flips and twists
export async function dnfAnalyser(
scramble: string,
solution: string,
fullAnalysis: boolean = true
algs: Awaited<ReturnType<typeof extractAlgs>>
) {
const puzzle = await cube3x3x3.kpuzzle();

Expand All @@ -148,9 +218,7 @@ export async function dnfAnalyser(

if (isSolved) return SOLVED;

if (!fullAnalysis) return 'Analysis disabled';

const algs = await extractAlgs(solution.split(' '));
const isInverseAlg = checkInverseAlg(
scramblePattern,
algs.map(a => a[0])
Expand Down
40 changes: 31 additions & 9 deletions src/timer/resultsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { ScrollArea } from '@/components/ui/scroll-area';
import { Penalty, Solve, db } from '@/lib/db';
import { dnfAnalyser } from '@/lib/dnfAnalyser';
import { extractAlgs } from '@/lib/solutionParser';

function convertTimeToText(time: number) {
if (time == -1) return 'DNF';
Expand Down Expand Up @@ -52,22 +53,36 @@ function SolveDialog({
}) {
const [analysis, setAnalysis] = useState<string | undefined>();

const analyse = () => {
dnfAnalyser(solve.scramble, solve.solution).then(res => {
setAnalysis(res);
});
const analyse = async () => {
const moves = solve.solution.map(s => s.move);
const algs = await extractAlgs(moves);
const analysis = await dnfAnalyser(solve.scramble, moves.join(' '), algs);
setAnalysis(analysis);
db.solves.update(solve.id, { parsed: algs.map(([alg]) => alg) });
};

const deleteSolve = () => {
db.solves.delete(solve.id);
close(false);
}

const solutionStr = solve.solution.map(s => s.move).join(' ');

const timeText =
solve.penalty == Penalty.DNF
? `DNF(${convertTimeToText(solve.time)})`
: convertSolveToText(solve);

const twistyUrl = 'https://alpha.twizzle.net/edit/?' + new URLSearchParams({
"setup-alg": solve.scramble,
"alg": solve.parsed?.join('\n') || solutionStr,
}).toString();

return (
<Dialog open={true} onOpenChange={close}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Solve #{idx}</DialogTitle>
<DialogTitle>Solve #{idx + 1}</DialogTitle>
<DialogDescription>
{new Date(solve.timeStamp).toLocaleString()}
</DialogDescription>
Expand All @@ -80,24 +95,31 @@ function SolveDialog({
className="w-full h-32 mx-auto"
/>
<DrawScramble
scramble={solve.scramble + ' ' + solve.solution}
scramble={solve.scramble + ' ' + solutionStr}
className="w-full h-32 mx-auto"
/>
</div>
<ul className="rounded-md border p-2">
<li className="font-medium">Algs in solve:</li>
<ScrollArea className="h-64">
{solve.parsed.map((alg, i) => (
{!solve.parsed && solutionStr}
{solve.parsed && solve.parsed.map((alg, i) => (
<li key={i + ' ' + alg}>{alg}</li>
))}
</ScrollArea>
</ul>
{analysis && <p className="font-medium">{analysis}</p>}
<DialogFooter>
<Button variant="secondary" type="submit" onClick={analyse}>

<Button type="submit" asChild>
<a href={twistyUrl}>
Twisty
</a>
</Button>
<Button variant="secondary" type="button" onClick={analyse}>
Analyse
</Button>
<Button variant="destructive" type="submit">
<Button variant="destructive" type="submit" onClick={deleteSolve}>
Delete
</Button>
</DialogFooter>
Expand Down
45 changes: 14 additions & 31 deletions src/timer/useCubeTimer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ import {
GanCubeEvent,
GanCubeMove,
cubeTimestampLinearFit,
now,
} from 'gan-web-bluetooth';
import { useCallback, useEffect, useRef } from 'react';
import { useStopwatch } from 'react-use-precision-timer';
import { Key } from 'ts-key-enum';
import { useToast } from '@/components/ui/use-toast';
import { Solve, db } from '@/lib/db';
import { SOLVED, dnfAnalyser } from '@/lib/dnfAnalyser';
import { penaltyChecker } from '@/lib/dnfAnalyser';
import { CubeStore } from '@/lib/smartCube';
import { extractAlgs } from '@/lib/solutionParser';
import { shouldIgnoreEvent } from '@/lib/utils';
import { TimerStore } from './timerStore';

Expand Down Expand Up @@ -118,54 +117,38 @@ export default function useCubeTimer() {
const moves = useRef<GanCubeMove[]>([]);

const cube = useStore(CubeStore, state => state.cube);
const { toast } = useToast();

const startSolve = useCallback(() => {
moves.current = [];
}, []);

const finishSolve = useCallback(async () => {
const endTime = stopwatch.getElapsedRunningTime();
const solutionMoves = cubeTimestampLinearFit(moves.current);
const fullMoves = CubeStore.state.lastMoves;
const solutionMoves = fullMoves
? moves.current.length > fullMoves.length
? moves.current
: cubeTimestampLinearFit(fullMoves).slice(-moves.current.length)
: moves.current;
const solution = solutionMoves.map(move => move.move);
const solutionStr = solution.join(' ');

console.log('Solution:', solution.join(' '));

const algs = await extractAlgs(solution);

console.log('Scramble:', TimerStore.state.originalScramble);
let last = solutionMoves.length > 0 ? solutionMoves[0].cubeTimestamp : 0;
console.table(
algs.map(([alg, comment, idx]) => {
const ms = solutionMoves[idx].cubeTimestamp - last;
const time = (ms / 1000).toFixed(2);
last = solutionMoves[idx].cubeTimestamp;
return [alg + comment, time];
})
);
const penalty = await penaltyChecker(TimerStore.state.originalScramble, solutionStr);

newScramble();

const solutionStr = solution.join(' ');
console.log(solutionStr);
const solve = {
time: endTime,
timeStamp: Date.now(),
now: now(),
scramble: TimerStore.state.originalScramble,
solution: solutionStr,
parsed: algs.map(([alg]) => alg),
solution: solutionMoves,
penalty,
} as Solve;

await db.solves.add(solve);

const dnfAnalysis = await dnfAnalyser(solve.scramble, solve.solution);
if (dnfAnalysis !== SOLVED) {
toast({
title: 'DNF',
description: dnfAnalysis,
});
}
}, [stopwatch, toast]);
}, [stopwatch]);

const updateStateFromSpaceBar = useCallback(
(holdingDown: boolean) => {
Expand Down

0 comments on commit 422f840

Please sign in to comment.