Final Project: Defuse Dash

Concept

The project, “Defuse Dash,” is an interactive game where players must solve physical and digital puzzles to “defuse” a bomb. The game uses an Arduino setup with various sensors and inputs, including buttons, potentiometers, adafruit trellis, and an ultrasonic distance sensor, integrated with a p5.js visual and interaction interface. The game aims to teach principles of problem-solving under pressure, as players must complete various tasks within time limits to defuse a virtual bomb.

Images

Project Interaction

Visual and Physical Setup

Players are presented with a physical board containing:

  • A series of buttons connected to LEDs that light up when pressed.
  • A potentiometer used to adjust resistance values.
  • An ultrasonic distance sensor to measure and input distances.
  • A keypad for code entry.

The p5.js interface displays:

  • Task instructions and success criteria.
  • A visual representation of the “bomb,” including a countdown timer.
  • Feedback sections that show task outcomes and game progress.

Game Flow

The game progresses through a series of tasks such as adjusting distances, matching potentiometer values, entering sequences through buttons, and inputting codes on a keypad. Each task completion is visually indicated, moving the player closer to defusing the bomb.

Implementation

Hardware Components

  • Buttons and LEDs: Create a direct interaction where pressing a button shows immediate visual feedback through an LED.
  • Potentiometer: Adjusts to match a required voltage displayed on the p5.js interface.
  • Ultrasonic Sensor: Measures how far away a player is from the sensor, requiring players to physically move to match the required distance.
  • Adafruit Trellis Keypad: Create a direct interaction where pressing a button shows immediate visual feedback through an LED.

Software Components

  • p5.js Visuals: Animates tasks, displays real-time data, and provides a graphical countdown timer and game status.
  • Serial Communication: Handles data transmission between the Arduino and the p5.js interface, using serial commands to start tasks and send results.

Interaction Design

The interaction design focuses on integrating tactile and visual feedback to enhance engagement:

  • Tactile: Feeling the buttons click and adjusting the potentiometer gives a hands-on experience.
  • Visual: Immediate updates on the interface when tasks are completed successfully or fail.

Communication Between Arduino and p5.js

The communication protocol is straightforward:

  • Sending Commands: p5.js sends numeric codes corresponding to different tasks.
  • Receiving Data: Arduino sends back results as comma-separated strings that p5.js parses and uses to update the game state.

Arduino

The Arduino sketch controls every hardware-related action that’s essential to gameplay. this sketch is designed to track inputs from a variety of sensors, including buttons via digital pins, potentiometers, adafruit trellis, and ultrasonic distance sensors. In order to guarantee accurate input detection, it manages button debouncing. It also regulates outputs, such as button and keypad LEDs, to give the player real-time feedback depending on the logic and status of the game.

For each cycle of the loop() function, the sketch checks the status of connected devices, updates the game state based on player interactions, and sends crucial gameplay data to the p5.js application over serial communication. This includes values like distance measurements, potentiometer levels, button sequence inputs, and keypad inputs, which are vital for progressing through the game’s challenges.

Code

#include <string.h>

// Define task states using an enum
enum Task {
  TASK_DISTANCE_SENSING,
  TASK_POTENTIOMETER_ADJUSTMENT,
  TASK_BUTTON_SEQUENCE,
  TASK_ADAFRUIT_KEYPAD,
  TASK_DEFUSED_SOUND,
  TASK_EXPLODED_SOUND
};

// Variables to store the current task and sensor values
volatile Task currentTask = TASK_DISTANCE_SENSING;
const int buttonPins[] = { 2, 3, 4, 5 };  // Pins for buttons
const int LEDpins[] = {10, 11, 12, 13}; // Pins for LED buttons
const int potPin = A0;
const int trigPin = 6; 
const int echoPin = 7;

// Variables to store sensor data
bool buttonStates[4] = { 0 };
int potValue = 0;
int distance = 0;

// Length of the button press sequence
const int sequenceLength = 5;   
// Array to store the button sequence     
int buttonSequence[sequenceLength]; 
// Index to keep track of the current position in the sequence 
int sequenceIndex = 0;               
bool lastButtonState[4] = { LOW, LOW, LOW, LOW };

String taskCommand = "0";
int taskNumber = 0;
// Initialize an empty string to hold the sequence
String sequenceString = "";  

#include <Wire.h>
#include "Adafruit_Trellis.h"

#define MOMENTARY 0
#define LATCHING 1
// set the mode here
#define MODE MOMENTARY

Adafruit_Trellis matrix0 = Adafruit_Trellis();
Adafruit_TrellisSet trellis = Adafruit_TrellisSet(&matrix0);

#define NUMTRELLIS 1

#define numKeys (NUMTRELLIS * 16)

#define INTPIN A2

int passcode[5];
int passcodeIndex = 0;
// Initialize an empty string for the passcode
String passcodeString = "";  

void setup() {
  Serial.begin(9600);  
  pinMode(trigPin, OUTPUT);
  pinMode(echoPin, INPUT);
  for (int i = 0; i < 4; i++) {
    // Set button pins with internal pull-up resistors
    pinMode(buttonPins[i], INPUT_PULLUP);  
    pinMode(LEDpins[i], OUTPUT);
  }
  pinMode(buzzerPin, OUTPUT);
  pinMode(INTPIN, INPUT);
  digitalWrite(INTPIN, HIGH);
  trellis.begin(0x70);
  // light up all the LEDs in order
  for (uint8_t i = 0; i < numKeys; i++) {
    trellis.setLED(i);
    trellis.writeDisplay();
    delay(50);
  }
  // then turn them off
  for (uint8_t i = 0; i < numKeys; i++) {
    trellis.clrLED(i);
    trellis.writeDisplay();
    delay(50);
  }
}


void loop() {
  if (Serial.available() > 0) {
    // Read the next command until a newline character
    taskCommand = Serial.readStringUntil('\n');  
    if (taskCommand.length() > 0) {
      taskNumber = taskCommand.toInt();
      if (taskNumber == 0 || taskNumber == 1 || taskNumber == 2) {
        currentTask = TASK_DISTANCE_SENSING;
      } else if (taskNumber == 3 || taskNumber == 4 || taskNumber == 5) {
        currentTask = TASK_POTENTIOMETER_ADJUSTMENT;
      } else if (taskNumber == 6) {
        currentTask = TASK_BUTTON_SEQUENCE;
      } else if (taskNumber == 7) {
        currentTask = TASK_ADAFRUIT_KEYPAD;
      } else {
        currentTask = static_cast<Task>(taskNumber);  
      }
      // Function to execute tasks
      executeTask(currentTask); 
    }
  }
}

void executeTask(Task task) {
  switch (task) {
    case TASK_DISTANCE_SENSING:
      distance = measureDistance();
      Serial.println(distance);
      break;
    case TASK_POTENTIOMETER_ADJUSTMENT:
      potValue = analogRead(potPin);
      Serial.println(potValue);
      break;
    case TASK_BUTTON_SEQUENCE:
      readButtons();
      break;
    case TASK_ADAFRUIT_KEYPAD:
      readKeypad();
      break;
    default:
      break;
  }
}


// Function to measure distance using an ultrasonic sensor
int measureDistance() {
  digitalWrite(trigPin, LOW);
  delayMicroseconds(2);
  digitalWrite(trigPin, HIGH);
  delayMicroseconds(10);
  digitalWrite(trigPin, LOW);
  // 38ms timeout for a max distance
  long duration = pulseIn(echoPin, HIGH, 38000);  
  // Calculate and return the distance
  return duration * 0.034 / 2;                    
}

// Function to read and report button states
void readButtons() {
  for (int i = 0; i < 4; i++) {
    // Invert because INPUT_PULLUP
    bool currentButtonState = !digitalRead(buttonPins[i]);  

    digitalWrite(LEDpins[i], currentButtonState ? HIGH : LOW);

    // Check if there is a change and the button is pressed
    if (currentButtonState && currentButtonState != lastButtonState[i] && sequenceIndex < sequenceLength) {
      // Store button number (1-based index)
      buttonSequence[sequenceIndex] = i + 1;  
      if (sequenceIndex > 0) {
        sequenceString += ",";  
      }
      // Add the button number to the string
      sequenceString += String(buttonSequence[sequenceIndex]);  
      sequenceIndex++;
    }

    // Update the last button state
    lastButtonState[i] = currentButtonState;
  }

  // Check if the sequence is complete
  if (sequenceIndex == sequenceLength) {
    // Send the complete sequence string
    Serial.println(sequenceString);  
    sequenceString = "";
    // Reset the sequence index to start new sequence capture
    sequenceIndex = 0;
  }
}

void readKeypad() {
  // 30ms delay is required, don't remove me!
  delay(30);  

  if (MODE == MOMENTARY) {
    // If a button was just pressed or released...
    if (trellis.readSwitches()) {

      // Go through every button
      for (uint8_t i = 0; i < numKeys; i++) {
        // If it was pressed, turn it on
        if (trellis.justPressed(i)) {
          trellis.setLED(i);
          trellis.writeDisplay();

          if (passcodeIndex < 5) {
            passcode[passcodeIndex] = i+1;
            if (passcodeIndex > 0) {
              passcodeString += ","; 
            }
            // Add the key index to the string
            passcodeString += String(passcode[passcodeIndex]);  
            passcodeIndex++;
          }

          // If it was released, turn it off
          if (trellis.justReleased(i)) {
            trellis.clrLED(i);
            trellis.writeDisplay();
          }
        }
      }

      // Check if the passcode is complete
      if (passcodeIndex == 5) {
        // Send the complete passcode string
        Serial.println(passcodeString); 
        // Reset for next input 
        passcodeString = "";
        passcodeIndex = 0;  
      }
    }
  }
}

P5.js

The p5.js sketch is crafted to enrich the player’s experience with engaging visuals and responsive game dynamics. It visually represents the game state on a web interface, animating elements like timers, sensor data displays, and game status messages. This sketch plays a critical role in bridging the physical and digital aspects of the game by interpreting and displaying data received from the Arduino.

The sketch manages different game states, such as starting the game, transitioning between tasks, and handling win-or-lose conditions. Each state is visually distinct and designed to keep the player informed and engaged.

Code

Points of Pride

  • One of the aspects I’m particularly proud of is the seamless integration and synchronization between the Arduino and p5.js environments, which facilitated a dynamic and interactive experience.
  • The development of the narrative-driven game logic within p5.js, which utilized the sensor inputs from Arduino to unfold a storyline, is another high point. This narrative approach significantly enhanced user engagement, making the technical aspects of the project more relatable and enjoyable.
  • The use of creative riddles that required players to interpret clues and respond with physical interactions, such as adjusting distances and entering sequences, added an educational layer that subtly introduced users to concepts of measurement, spatial awareness, and logical sequencing.
  • Despite not initially being confident in my soldering skills, I successfully soldered numerous components, and they functioned perfectly. This experience not only enhanced my abilities but also boosted my confidence, affirming that I am now proficient in soldering.

Video(Before the Showcase)

Video(During the showcase)

Future Improvements

  • Interactivity Enhancements: One exciting direction for future development is enhancing the game’s interactivity by introducing multiplayer features. This could involve allowing multiple players to interact with the same game setup simultaneously, adding a competitive element by having players complete tasks in sequence, against a timer, or against each other.
  • Leaderboard/Scoreboard: Integrating score tracking could significantly extend the game’s appeal and replayability. A leaderboard system where players can register their scores and compare them with other players.

Leave a Reply