1. Camera Coordinate System

Bilateral Coordination Test · Sessions 7–9 → Session 17

v1 — Prototype
Session 7, Iteration 1

The camera was flipped, and I tried to flip it again in code. This double-flip was a mistake — the dots appeared in the wrong place on screen.

// WRONG — double mirror
ctx.save();
ctx.scale(-1, 1);
ctx.translate(-canvas.width, 0);
// draw dots here
ctx.restore();

Result: my hand appeared on the wrong side of the screen.

v2 — Diagnosis
Session 7, Iterations 2–4

I tried different fixes, but they all failed. The problem: the browser camera was already flipped by default, but I didn't understand that yet.

// STILL WRONG — still fighting
// the symptom not the cause
video.style.transform = 'scaleX(-1)';
ctx.scale(-1, 1);
// Now triple-mirrored

Result: each fix made it worse. The problem was unfixed after 3 attempts.

v3 — Final
Session 7, Iteration 5 → all future sessions

I realized the browser flips the camera by default. So I flipped the video in CSS, then adjusted the math to match. Simple once I understood the root cause.

/* CSS — mirror video only */
video { transform: scaleX(-1); }

// JS — adjust coordinate math
// Don't mirror the canvas at all
const x = (1 - landmarks[8].x) * width;
const y = landmarks[8].y * height;

// landmarks[8] = index fingertip
// .x is 0-1 from camera's perspective
// (1-x) flips it to match mirrored video

Result: worked perfectly on all devices. I reused this exact pattern later with no issues.

2. GAME_STANDARD Phase Architecture

All Tests · Session 8 → Sessions 12–17

v1 — Ad Hoc
Sessions 2–7

Each test had its own way of tracking what's happening (welcome screen, practice, test, results). No two tests worked the same way. Every new test meant starting from scratch.

// Session 2: no phase variable at all
// Just boolean flags
let gameStarted = false;
let testRunning = false;
let showingResults = false;

// Unclear what combination of
// these flags means what state

Result: every test worked differently. Confusing to debug and maintain.

v2 — Standard Created
Session 8 — GAME_STANDARD.md

I created a standard way for all tests to track their state: welcome → practice → test → results. All tests now use the same structure and naming.

// GAME_STANDARD — single source of truth
let phase = 'welcome';
// Valid values: welcome | practice
//               test | results

function setPhase(p) {
  phase = p;
  document.body.dataset.phase = p;
  // CSS hides/shows sections via
  // [data-phase="welcome"] .welcome-screen
}

// Ready screen = UI overlay only
// phase stays 'practice' during countdown

Result: every new test now uses the same structure. Much easier to build and debug.

v3 — Updated
Session 17 — Part 8 added

Added more rules to the standard: how buttons work on the results screen, what happens when someone closes the tab. Updated all tests to follow the new rules.

/* GAME_STANDARD Part 8 — Results Buttons */
/* Primary: "Play Again" — resets to welcome */
/* Secondary: "View History" — goes to history.html */
/* Exit modal: shown on back navigation */

.results-actions {
  display: flex;
  gap: 0.875rem;
  justify-content: center;
  margin-top: 2rem;
}

/* Exit confirmation prevents data loss */
window.addEventListener('beforeunload', (e) => {
  if (phase === 'test') {
    e.preventDefault();
    return e.returnValue = '';
  }
});

Result: all tests now handle buttons and exits the same way. Consistent user experience.

3. Bilateral Symmetry Measurement

Bilateral Coordination Test · Sessions 8–9 → Session 16

v1 — Total Score
Sessions 7–8, Prototype

Just count the dots. It doesn't matter which hand collects them. Simple to build, but doesn't actually measure what a bilateral test should measure.

// v1 — total score only
let totalScore = 0;

function collectDot(dot) {
  totalScore++;
  // No hand attribution
  // No way to know asymmetry
}

Result: someone using only their right hand would have the same score as someone using both hands equally. That's not useful.

v2 — Per-Hand
Session 8 — Record of Resistance #9

Track which hand collects each dot. The main number is now the symmetry ratio: how balanced are the hands? A 96% ratio means both hands worked equally.

// v2 — per-hand scoring
let leftScore = 0;
let rightScore = 0;

function collectDot(dot, hand) {
  if (hand === 'Left')  leftScore++;
  if (hand === 'Right') rightScore++;
}

// Symmetry: how balanced are the hands?
// min/max = 1.0 at perfect symmetry
function getSymmetry() {
  if (!leftScore || !rightScore) return 0;
  return Math.min(leftScore, rightScore) /
         Math.max(leftScore, rightScore);
}

Result: now I can see if someone's using both hands equally or favoring one side.

v3 — With Graph
Session 16 — Chart.js added

Users wanted to see their progress over time. Added graphs showing left vs. right hand performance. Blue for left hand, red for right hand.

// v3 — Chart.js visualization
const chart = new Chart(ctx, {
  type: 'line',
  data: {
    labels: dates,
    datasets: [
      {
        label: 'Left Hand',
        data: leftScores,
        borderColor: '#3b82f6', // blue
        backgroundColor: 'rgba(59,130,246,0.08)'
      },
      {
        label: 'Right Hand',
        data: rightScores,
        borderColor: '#dc2626', // red
        backgroundColor: 'rgba(220,38,38,0.08)'
      }
    ]
  }
});

// Toggle: symmetry % view
function showSymmetryView() {
  const symmetry = leftScores.map((l, i) =>
    Math.round(Math.min(l, rightScores[i]) /
    Math.max(l, rightScores[i]) * 100)
  );
  chart.data.datasets[0].data = symmetry;
  chart.update();
}

Result: users can track how their hand coordination improves over time. That's what Baseline is all about.

4. Row-Level Security Policy

Supabase Backend · Session 10–11 → Session 19

v1 — No RLS
Sessions 10–12, First Integration

The database had security enabled but no rules written. Data couldn't be saved or loaded. It looked like it was working, but nothing was actually stored.

-- v1: RLS enabled but no policies
-- Result: all queries silently fail
-- Table appears empty to all users

-- No INSERT policy → results not saved
-- No SELECT policy → history shows nothing
-- Bug was invisible because no error thrown

Result: the history page always showed no data, even though I thought I saved results.

v2 — Broken Policy
Sessions 13–18, First Policy Attempt

I added security rules, but I used the wrong data type. The rules looked correct but actually didn't work.

-- v2: policies written but type mismatch
-- user_id column is TEXT
-- auth.uid() returns UUID
-- No implicit cast → policy always fails

CREATE POLICY "user_sees_own_data"
  ON test_results FOR SELECT
  USING (auth.uid() = user_id);
  -- ↑ UUID vs TEXT — false every time
  -- No error thrown — just empty results

-- Same broken pattern on all 3 tables:
-- test_results, users, sessions

Result: the data looked protected, but it wasn't. The professor caught this in a security review.

v3 — Fixed (Session 19)
Session 19 — Security Hardening

Fixed the data type mismatch. Now the rules actually work and each user only sees their own data.

-- v3: explicit cast fixes the type mismatch
-- auth.uid() → UUID
-- user_id → TEXT needs ::uuid cast

CREATE POLICY "user_sees_own_results"
  ON test_results FOR SELECT
  USING (auth.uid() = user_id::uuid);
  --                   ↑ explicit cast

CREATE POLICY "user_inserts_own_results"
  ON test_results FOR INSERT
  WITH CHECK (auth.uid() = user_id::uuid);

-- Applied to all 3 tables:
-- test_results, users, sessions

-- Verified: each user now sees ONLY
-- their own rows. Isolation confirmed.

Result: each user now sees only their own data. Secure.

5. Understanding Before Building — Backend Architecture

Supabase Integration · Session 11 → Sessions 12–19

v1 — Copy/Paste Integration
Session 11, Initial Response

I could have just copied working code into my project without understanding it.

// WRONG APPROACH — cargo cult integration
// Paste code without understanding
// Learn through trial and error

Result: it would work, but I wouldn't understand why. Future bugs would be impossible to fix.

v2 — Conceptual Understanding
Session 11, Deliberate Pause

Instead, I paused and asked myself: how does a backend work? I spent time learning the basic idea: frontend sends data → backend receives it → database stores it. Only then did I build it. Took 30 extra minutes but was worth it.

// CORRECT APPROACH — understand first
// Frontend sends test results to API
// API authenticates user + passes to database
// Database stores result with user_id
// RLS policy ensures users only see their data
//
// Three-tier model becomes your mental model
// Future bugs become explicable

Result: I understood how the whole system works. Types matter. Security policies matter. This understanding stuck with me.

v3 — Applied to Security Hardening
Session 19, Security Fix

Months later in Session 19, the professor found a security bug. Because I understood the backend from Session 11, I could diagnose it instantly: the data types didn't match. Once I fixed it, everything worked. Without that Session 11 understanding, the bug would have been invisible.

-- Fixed RLS policy (Session 19)
CREATE POLICY "user_sees_own_data"
  ON test_results FOR SELECT
  USING (auth.uid() = user_id::uuid);
  --                   ↑ UUID cast

-- This fix was possible because I understood
-- the architecture from Session 11

Result: Fixed in minutes. That's what understanding does for you.

6. Time-Domain Peak Detection for Transient Events

Rhythm Synchronization Test · Session 15 → Sessions 16–17

v1 — Frequency-Domain Analysis
Session 15, First Attempt

I tried to detect taps by looking at the frequencies in the audio. This is how you identify a note or sound type.

// WRONG for transient detection
analyser.getByteFrequencyData(dataArray);
let hasClap = dataArray.slice(80, 120) // percussion freq band
  .reduce((max, val) => val > max ? val : max) > 50;
// Problem: short taps don't build frequency content

Result: it didn't work. The tap was too fast and quiet for frequency analysis to pick up.

v2 — Time-Domain Peak Detection
Session 15, Root Cause Analysis

I realized a tap is just a loud moment in the audio. I switched to looking at the raw sound wave and finding moments that are really loud.

// CORRECT for transient detection
analyser.getByteTimeDomainData(dataArray);
let maxVal = 0;
for (let i = 0; i < dataArray.length; i++) {
  const val = Math.abs(dataArray[i] - 128) / 128.0;
  if (val > maxVal) maxVal = val;
}
// Threshold: 0.04 (normalized amplitude)
// Lockout: 200ms to prevent double-counts
if (maxVal > 0.04 && now - lastTapTime > 200) {
  recordTap(now);
  lastTapTime = now;
}

Result: worked instantly. Users could tap and see the system respond.

v3 — Applied to All Audio Features
Sessions 16–17, Reaction Time Refinement

I used the same approach for every test that needed to detect sound. It works reliably even with background noise or different microphones.

// Same peak detection pattern used for:
// - Rhythm sync tap detection
// - Reaction time audio cue confirmation
// - Future attention/auditory tests
//
// Principle: transient events (taps, clicks, alerts)
// use time-domain analysis; sustained tones use
// frequency-domain analysis

Result: every test that needs sound detection uses the same reliable method.

7. Centralized State Machine Over Scattered Function Scope

Rhythm Synchronization Test · Session 15 → Sessions 17–19

v1 — Scattered Functions
Session 15, First Working Version

I wrote functions scattered throughout the code. The buttons couldn't find them. The Start button was broken.

// SCATTERED approach — functions lost




Result: buttons didn't work. Everything felt fragile.

v2 — Centralized Object
Session 15, State Refactor

I put everything into one object called `app`. All the state lives there. All the functions live there. Buttons can find them instantly.

// CENTRALIZED approach — single source of truth
const app = {
  // State
  currentState: 'welcome',
  stageIndex: 0,
  tapsDetected: 0,

  // Methods
  startPractice() { ... },
  recordTap(tapTime) { ... },
  setState(newState) { ... }
};


Result: all buttons work. Everything is clear and organized.

v3 — Applied to All Tests
Sessions 16–17, Consistency Across Platform

All three tests now use the same pattern. Every test has its own `app` object that works the same way. Makes them easy to understand and fix.

// Consistent pattern across all tests:
// const app = {
//   phase: 'welcome',
//   startGame: function() { ... },
//   saveResults: function() { ... }
// }
//
// Benefits:
// - Debugging is straightforward (print app to see all state)
// - Events are predictable (everything goes through app methods)
// - No scope conflicts (all state in one place)
// - Scales to complex games (inventory, score tracking, multiplayer)

Result: the platform is predictable and easy to maintain. Adding features and fixing bugs is straightforward.