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                            74x                       47x         37x       98x     37x 37x 37x 37x       47x 47x 47x 47x 47x               47x       42x 2x 2x           2x       178x   47x 47x   1x 1x 1x   1x     12x 12x 12x   12x   23x 23x       37x       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>;