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.
#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.