01 March 2026

Analog Team 15 Bop It

 We were inspired mainly by Bop It and Simon Says, where there are color/light and sound indicators for what the player should input to play the game. Yellow indicates a button press, blue indicates turning the potentiometer, and purple indicates to shake the circuit playground. White indicates it is your turn to place the inputs, green means you got it right, red means you got it wrong. You will see a timer ring light up on how long you have to make the inputs before you automatically lose from time loss. All losses restart you. It starts with 3 inputs at a time, after getting it correct 3 times, it will get harder moving to 4 inputs, which after another 3 correct iterations moves you to 5 inputs, and so on. You will also notice the circuit playground lights up with the color input you trigger based on your action to affirm the input was successful. The goal is to get as far as possible without failing.

DIG3602 Bop It Circuit Playground Demonstration


#include <Adafruit_CircuitPlayground.h>

// ----------------------------
// Game design constants
// ----------------------------

const int NUM_PIXELS = 10;

// Actions:
// 0 = POT turn
// 1 = BUTTON press
// 2 = SHAKE
const int ACTION_COUNT = 3;

const int ROUNDS_PER_LEVEL = 3;

// Timing
const int SHOW_STEP_MS = 450;
const int SHOW_GAP_MS  = 180;
const int YOUR_TURN_MS = 550;

// Per step time limit
const int INPUT_TIMEOUT_MS = 12000;

// ----------------------------
// POT tuning (NEW: DELTA-FROM-START)
// ----------------------------
//
// POT action triggers if the knob moves far enough away from where it started
// at the beginning of the step. No centering required.
//
// If it still feels hard to trigger: LOWER POT_DELTA_TRIGGER (try 60–120)
// If it triggers too easily: RAISE it (try 140–220)
const int POT_DELTA_TRIGGER = 220;

// Require a few consecutive reads beyond threshold to avoid noise triggers
const int POT_CONSECUTIVE_HITS_REQUIRED = 3;

// Optional grace period at the start of each step to let readings settle
const int POT_STEP_GRACE_MS = 150;

// ----------------------------
// Shake tuning
// ----------------------------

const float SHAKE_JERK_THRESHOLD = 6.0;
const int   SHAKE_REQUIRED_HITS  = 2;
const int   SHAKE_HIT_WINDOW_MS  = 450;
const int   SHAKE_COOLDOWN_MS    = 500;

// Prompt positions
const int PIXEL_FOR_POT    = 2;
const int PIXEL_FOR_BUTTON = 7;
const int PIXEL_FOR_SHAKE  = 0;

// Tones for each action
const int TONE_FOR_POT    = 440;
const int TONE_FOR_BUTTON = 660;
const int TONE_FOR_SHAKE  = 880;

// ----------------------------
// State variables
// ----------------------------

int level = 1;

const int MAX_SEQ_LEN = 20;
int sequence[MAX_SEQ_LEN];

// Shake state
unsigned long lastShakeTime = 0;
float lastAccelMag = 0.0;
int shakeHitCount = 0;
unsigned long firstShakeHitTime = 0;

// POT state (per step)
int potStartValue = 0;
bool potUsedThisStep = false;
int potHitStreak = 0;

// ----------------------------
// Helpers
// ----------------------------

void setAllPixels(uint8_t r, uint8_t g, uint8_t b) {
  for (int i = 0; i < NUM_PIXELS; i++) {
    CircuitPlayground.setPixelColor(i, r, g, b);
  }
}

void clearPixels() {
  setAllPixels(0, 0, 0);
}

bool isGameEnabled() {
  return CircuitPlayground.slideSwitch();
}

void showYourTurn() {
  setAllPixels(255, 255, 255);
  delay(YOUR_TURN_MS);
  clearPixels();
}

void showSuccess() {
  setAllPixels(0, 255, 0);
  CircuitPlayground.playTone(880, 140);
  delay(300);
  clearPixels();
}

void showFailure() {
  setAllPixels(255, 0, 0);
  CircuitPlayground.playTone(220, 300);
  delay(600);
  clearPixels();
}

void confirmActionFeedback(int action) {
  if (action == 0) {            // POT
    setAllPixels(0, 80, 255);
    CircuitPlayground.playTone(520, 80);
  } else if (action == 1) {     // BUTTON
    setAllPixels(255, 140, 0);
    CircuitPlayground.playTone(720, 80);
  } else {                      // SHAKE
    setAllPixels(180, 0, 255);
    CircuitPlayground.playTone(920, 80);
  }
  delay(180);
  clearPixels();
}

void showPromptStep(int action) {
  clearPixels();

  int pixelIndex = 0;
  int toneHz = 440;

  if (action == 0) {
    pixelIndex = PIXEL_FOR_POT;
    toneHz = TONE_FOR_POT;
    CircuitPlayground.setPixelColor(pixelIndex, 0, 80, 255);
  } else if (action == 1) {
    pixelIndex = PIXEL_FOR_BUTTON;
    toneHz = TONE_FOR_BUTTON;
    CircuitPlayground.setPixelColor(pixelIndex, 255, 140, 0);
  } else {
    pixelIndex = PIXEL_FOR_SHAKE;
    toneHz = TONE_FOR_SHAKE;
    CircuitPlayground.setPixelColor(pixelIndex, 180, 0, 255);
  }

  CircuitPlayground.playTone(toneHz, SHOW_STEP_MS);
  delay(SHOW_STEP_MS);

  clearPixels();
  delay(SHOW_GAP_MS);
}

void generateSequence(int length) {
  for (int i = 0; i < length; i++) {
    sequence[i] = random(ACTION_COUNT);
  }
}

void showSequence(int length) {
  for (int i = 0; i < length; i++) {
    showPromptStep(sequence[i]);
  }
}

// ----------------------------
// Visual timer ring
// ----------------------------
void showTimerRing(unsigned long elapsedMs) {
  float fractionLeft = 1.0 - (float)elapsedMs / (float)INPUT_TIMEOUT_MS;
  if (fractionLeft < 0) fractionLeft = 0;

  int pixelsOn = (int)(fractionLeft * NUM_PIXELS + 0.5);

  clearPixels();
  for (int i = 0; i < pixelsOn; i++) {
    CircuitPlayground.setPixelColor(i, 30, 30, 30);
  }
}

// ----------------------------
// Shake detector
// ----------------------------
bool detectShakeEvent() {
  if (millis() - lastShakeTime < SHAKE_COOLDOWN_MS) return false;

  float ax = CircuitPlayground.motionX();
  float ay = CircuitPlayground.motionY();
  float az = CircuitPlayground.motionZ();

  float mag = sqrt(ax * ax + ay * ay + az * az);

  float jerk = fabs(mag - lastAccelMag);
  lastAccelMag = mag;

  if (jerk > SHAKE_JERK_THRESHOLD) {
    if (shakeHitCount == 0 || (millis() - firstShakeHitTime) > SHAKE_HIT_WINDOW_MS) {
      shakeHitCount = 0;
      firstShakeHitTime = millis();
    }

    shakeHitCount++;

    if (shakeHitCount >= SHAKE_REQUIRED_HITS) {
      shakeHitCount = 0;
      lastShakeTime = millis();
      return true;
    }
  }

  if (shakeHitCount > 0 && (millis() - firstShakeHitTime) > SHAKE_HIT_WINDOW_MS) {
    shakeHitCount = 0;
  }

  return false;
}

// ----------------------------
// POT: initialize per step
// ----------------------------
void resetPotForStep() {
  // average a few reads for a stable start value
  long sum = 0;
  for (int i = 0; i < 8; i++) {
    sum += analogRead(A1);
    delay(2);
  }
  potStartValue = (int)(sum / 8);

  potUsedThisStep = false;
  potHitStreak = 0;

  // Optional: print start value for debugging
  Serial.print("Pot start = ");
  Serial.println(potStartValue);
}

// ----------------------------
// POT: detect deliberate twist away from start value
// ----------------------------
bool detectPotAction(unsigned long stepStartMs) {
  if (potUsedThisStep) return false;

  // Small grace period to avoid immediate false triggers
  if (millis() - stepStartMs < POT_STEP_GRACE_MS) {
    potHitStreak = 0;
    return false;
  }

  int v = analogRead(A1);
  int delta = abs(v - potStartValue);

  // Debug line (comment out later if you want)
  // Serial.print("Pot v="); Serial.print(v); Serial.print(" delta="); Serial.println(delta);

  if (delta >= POT_DELTA_TRIGGER) {
    potHitStreak++;
    if (potHitStreak >= POT_CONSECUTIVE_HITS_REQUIRED) {
      potUsedThisStep = true;
      potHitStreak = 0;
      return true;
    }
  } else {
    potHitStreak = 0;
  }

  return false;
}

// ----------------------------
// Read ONE action with timeout.
// Returns 0/1/2 for detected actions, -1 for timeout.
// ----------------------------
int getPlayerActionWithTimeout() {
  resetPotForStep();
  unsigned long stepStart = millis();

  while ((millis() - stepStart) < INPUT_TIMEOUT_MS) {
    if (!isGameEnabled()) return -1;

    unsigned long elapsed = millis() - stepStart;
    showTimerRing(elapsed);

    // POT
    if (detectPotAction(stepStart)) {
      Serial.println("Detected: POT");
      confirmActionFeedback(0);
      return 0;
    }

    // BUTTON
    if (CircuitPlayground.leftButton()) {
      while (CircuitPlayground.leftButton()) delay(5);
      Serial.println("Detected: BUTTON");
      confirmActionFeedback(1);
      return 1;
    }

    // SHAKE
    if (detectShakeEvent()) {
      Serial.println("Detected: SHAKE");
      confirmActionFeedback(2);
      return 2;
    }

    delay(10);
  }

  Serial.println("Result: TIMEOUT");
  clearPixels();
  return -1;
}

// ----------------------------
// One full round
// ----------------------------
bool runRound(int seqLength) {
  showSequence(seqLength);
  if (!isGameEnabled()) return false;

  showYourTurn();

  for (int i = 0; i < seqLength; i++) {
    Serial.print("Step ");
    Serial.print(i);
    Serial.print(" expected = ");
    Serial.println(sequence[i]);

    int playerAction = getPlayerActionWithTimeout();

    if (playerAction == -1) {
      Serial.println("FAIL: timeout");
      return false;
    }

    Serial.print("Got = ");
    Serial.println(playerAction);

    if (playerAction != sequence[i]) {
      Serial.println("FAIL: wrong input");
      return false;
    }

    Serial.println("OK");
  }

  return true;
}

// ----------------------------
// Setup
// ----------------------------
void setup() {
  CircuitPlayground.begin();
  Serial.begin(9600);

  randomSeed(analogRead(A1));

  float ax = CircuitPlayground.motionX();
  float ay = CircuitPlayground.motionY();
  float az = CircuitPlayground.motionZ();
  lastAccelMag = sqrt(ax * ax + ay * ay + az * az);

  clearPixels();
  Serial.println("Game ready.");
}

// ----------------------------
// Main loop
// ----------------------------
void loop() {
  if (!isGameEnabled()) {
    clearPixels();
    delay(50);
    return;
  }

  int seqLength = level + 2;
  if (seqLength > MAX_SEQ_LEN) seqLength = MAX_SEQ_LEN;

  for (int round = 0; round < ROUNDS_PER_LEVEL; round++) {
    generateSequence(seqLength);

    bool success = runRound(seqLength);

    if (success) {
      showSuccess();
    } else {
      showFailure();
      level = 1;
      delay(800);
      return;
    }

    delay(350);
  }

  level++;

  setAllPixels(0, 255, 0);
  CircuitPlayground.playTone(988, 120);
  delay(250);
  clearPixels();

  delay(400);
}




No comments:

Post a Comment

Note: Only a member of this blog may post a comment.