All files / integration solveAnalysis.ts

100% Statements 33/33
100% Branches 0/0
100% Functions 0/0
100% Lines 33/33

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106                            76x                       50x         38x       103x     38x 38x 38x 38x       50x 50x 50x 50x 50x               50x       44x 2x 2x           2x       186x   50x 50x   1x 1x 1x   1x     13x 13x 13x   13x   24x 24x       38x       4x          
import * as CubeFacelets from '@wstein/regrip-core/domain/CubeFacelets';
import type { SmartCubeSessionEvent } from '@wstein/regrip-core/session/smartCubeSession';
 
export type SolveAnalysisSnapshot = {
  status: 'empty' | 'running' | 'complete';
  durationMs: number;
  tps: number | null;
  moveCount: number;
  qtm: number;
  regrips: number;
  triggers: number;
  longestPauseMs: number;
};
 
const emptySnapshot = (): SolveAnalysisSnapshot => ({
  status: 'empty',
  durationMs: 0,
  tps: null,
  moveCount: 0,
  qtm: 0,
  regrips: 0,
  triggers: 0,
  longestPauseMs: 0,
});
 
function quarterTurns(move: string): number {
  return /2'?$/.test(move.trim()) ? 2 : 1;
}
 
/** Summarize the current connection/replay run from its first physical turn. */
export function createSolveAnalysis(onChange?: (snapshot: SolveAnalysisSnapshot) => void) {
  let value = emptySnapshot();
  let startedAt: number | undefined;
  let lastMoveAt: number | undefined;
 
  const publish = (): void => onChange?.({ ...value });
 
  function reset(): void {
    value = emptySnapshot();
    startedAt = undefined;
    lastMoveAt = undefined;
    publish();
  }
 
  function onMove(event: Extract<SmartCubeSessionEvent, { type: 'MOVE' }>): void {
    if (value.status === 'complete') reset();
    startedAt ??= event.timestamp;
    const pause = lastMoveAt === undefined ? 0 : Math.max(0, event.timestamp - lastMoveAt);
    lastMoveAt = event.timestamp;
    value = {
      ...value,
      status: 'running',
      durationMs: Math.max(0, event.timestamp - startedAt),
      moveCount: value.moveCount + 1,
      qtm: value.qtm + quarterTurns(event.move),
      longestPauseMs: Math.max(value.longestPauseMs, pause),
    };
    publish();
  }
 
  function complete(): void {
    if (value.status !== 'running' || startedAt === undefined || lastMoveAt === undefined) return;
    const durationMs = Math.max(0, lastMoveAt - startedAt);
    value = {
      ...value,
      status: 'complete',
      durationMs,
      tps: durationMs > 0 ? value.moveCount / (durationMs / 1000) : null,
    };
    publish();
  }
 
  function onEvent(event: SmartCubeSessionEvent): void {
    switch (event.type) {
      case 'MOVE':
        onMove(event);
        break;
      case 'REGRIP':
        if (value.status === 'running') {
          value = { ...value, regrips: value.regrips + 1 };
          publish();
        }
        break;
      case 'CUSTOM_TRIGGER':
      case 'SHAKE':
        if (value.status === 'running') {
          value = { ...value, triggers: value.triggers + 1 };
          publish();
        }
        break;
      case 'FACELETS':
        if (CubeFacelets.isSolvedFacelets(event.facelets)) complete();
        break;
    }
  }
 
  return {
    onEvent,
    complete,
    reset,
    snapshot: (): SolveAnalysisSnapshot => ({ ...value }),
  };
}
 
export type SolveAnalysis = ReturnType<typeof createSolveAnalysis>;