All Posts

week 11

Piano Tiles: Hand and Foot

  1. overview

A rhythm-game where you must hit tiles falling in four columns using Arduino-connected hand (red) and foot (blue) buttons. Complete the Crazy Frog song to win; miss 3 times and you lose.

2. Arduino

Buttons: 4 hand inputs (pins 2–5), 4 foot inputs (pins 6–9)
Wiring: Buttons wired to +5 V with external pull-downs to GND

3. p5.js

Tile engine:

-Spawns one tile at a time per a predefined ;pattern[]’ of ‘{col, isFoot}’ steps
-Draws tile in its column, colored by ‘isFoot’ (blue) or hand (red)
-Moves tile downward at speed chosen by difficulty

-Lose if lives hit zero; win if you clear the last tile before the sample ends

Final Project: Inside Out Runner Game

For this final project, I built an interactive game that represents the mental world of a child’s mind—heavily inspired by Inside Out. The digital part of the game was made using p5.js, and it connects to three physical Arduino setups: two for the moving characters (Joy and Sadness) and one for the glowing memory balls.

You play by dodging obstacles and collecting star-shaped powerups. There are two types of obstacles: the memory bridge (which the player slides under) and the memory ball cluster (which you jump over). As the player moves through the level, their real-life emotions move along a race track: Joy advances when the player collects powerups, and Sadness moves forward every time an obstacle is hit.

If Sadness reaches the end first (if user hit 3 obstacles), a memory ball glows blue. If Joy wins (3 powerups), it glows yellow.


Hardware + Fabrication:

  • I designed the track pattern in Illustrator and laser-cut it .

  • I downloaded STL files of Joy and Sadness from Printables, 3D printed them, and painted them with acrylic paint. Then i laser cut a stand and glued them to it so that they were on top of the arduino boards that were connected to the wheels.

  • I used SolidWorks to model the memory ball, 3D printed it, and added LEDs connected to breadboards inside then connected the 2 breadboards to a single arduino.

  •  


Code + Arduino Integration:

At first, I wanted to send letters (‘O’, ‘P’, etc.) from p5 to Arduino to trigger events, but it just wasn’t reliable. After about 8 hours of trial and error, I figured out that using numbers instead of characters made the communication consistent.

Another issue was the Arduinos themselves. Because the boards are inside the moving characters, they were getting damaged by all the motion and collisions. They’d randomly stop working even though the wiring was fine. I had to replace the connections multiple times, and one of the boards is still a little unstable.


Arduino Code – Joy & Sadness Movement (Boards 1 & 2):

const int STBY = 6;

// Motor A
const int AIN1 = 4;
const int AIN2 = 5;
const int PWMA = 3;

// Motor B
const int BIN1 = 7;
const int BIN2 = 8;
const int PWMB = 9;

void setup() {
  Serial.begin(9600);

  pinMode(STBY, OUTPUT);
  pinMode(AIN1, OUTPUT);
  pinMode(AIN2, OUTPUT);
  pinMode(PWMA, OUTPUT);
  pinMode(BIN1, OUTPUT);
  pinMode(BIN2, OUTPUT);
  pinMode(PWMB, OUTPUT);

  digitalWrite(STBY, HIGH); // Enable motor driver
}

void loop() {
  if (Serial.available()) {
    char command = Serial.read();

    if (command == 0 || command == 1) {
      moveForward(200); // Set speed here (0–255)
      delay(1000);       // Adjust this value to travel the desired cm (experiment to tune)
      stopMotors();
    }
  }
}

void moveForward(int speed) {
  // Motor A forward
  digitalWrite(AIN1, HIGH);
  digitalWrite(AIN2, LOW);
  analogWrite(PWMA, speed);

  // Motor B forward
  digitalWrite(BIN1, HIGH);
  digitalWrite(BIN2, LOW);
  analogWrite(PWMB, speed);
}

void stopMotors() {
  // Stop Motor A
  analogWrite(PWMA, 0);
  digitalWrite(AIN1, LOW);
  digitalWrite(AIN2, LOW);

  // Stop Motor B
  analogWrite(PWMB, 0);
  digitalWrite(BIN1, LOW);
  digitalWrite(BIN2, LOW);
}

 


Arduino Code – Memory Ball LEDs (Board 3):

// Pins for Motor Driver (L298N or similar)
#define MOTOR1_PIN1 2
#define MOTOR1_PIN2 3
#define MOTOR2_PIN1 4
#define MOTOR2_PIN2 5

void setup() {
  Serial.begin(9600);
  pinMode(MOTOR1_PIN1, OUTPUT);
  pinMode(MOTOR1_PIN2, OUTPUT);
  pinMode(MOTOR2_PIN1, OUTPUT);
  pinMode(MOTOR2_PIN2, OUTPUT);
}

void loop() {
  if (Serial.available()) {
    char cmd = Serial.read();
    if (cmd == 'O') { // 'O' for Obstacle
      // Move both motors forward for 1 second
      digitalWrite(MOTOR1_PIN1, HIGH);
      digitalWrite(MOTOR1_PIN2, LOW);
      digitalWrite(MOTOR2_PIN1, HIGH);
      digitalWrite(MOTOR2_PIN2, LOW);
      delay(1000);
      stopMotors();
    }
  }
}

void stopMotors() {
  digitalWrite(MOTOR1_PIN1, LOW);
  digitalWrite(MOTOR1_PIN2, LOW);
  digitalWrite(MOTOR2_PIN1, LOW);
  digitalWrite(MOTOR2_PIN2, LOW);
}


The p5.js Sketch:

The sketch handles the game logic—collision detection, button controls, score tracking, and serial communication with  Arduinos.

  • Character controller with jump/slide mechanics

  • Obstacle spawning at set intervals

  • Collision detection that updates the win count for Joy or Sadness

  • Serial output that sends a number to each board depending on the in-game event


What I’m Most Proud Of:

Definitely the connection between digital and physical. Seeing Joy or Sadness physically move when something happens in the game is so rewarding. It brings the internal emotional world of the game into the real world.

Also, figuring out how to manage serial communication across 3 Arduinos (despite many boards dying on me) felt like a huge accomplishment.


If I Had More Time:

  • Improve the Arduino casing so that the wires don’t come loose so easily

  • Add a small screen showing who’s winning in real-time

P5 Code: 

let port1, port2;
let connectBtn1, connectBtn2;
let player;
let obstacles = [];
let laneWidth;
let lanes = [0, 1, 2];
let score = 0;
let gameSpeed = 5;
let obstacleSpawnCounter = 0;
let bg1, bg2;
let gameState = "start";
let startButton;
let powerUps = [];
let powerUpSpawnCounter = 0;
let gameMusic;
let obstacleHits = 0;
let powerUpHits = 0;
const MAX_HITS = 3;

function preload() {
  bg2 = loadImage("background.png");
  bg1 = loadImage("headquarters.png");
  gameMusic = loadSound("Song.mp3");
}

function setup() {
  createCanvas(windowWidth, windowHeight);
  laneWidth = width / 3;
  setupGame();

  port1 = createSerial();
  port2 = createSerial();

  startButton = createButton("Start Game");
  startButton.position(width / 2 - 50, height / 2 + 40);
  startButton.size(100, 40);
  startButton.mousePressed(() => {
    gameState = "playing";
    startButton.hide();
  });

  connectBtn1 = createButton("Connect Arduino 1 (Obstacle)");
  connectBtn1.position(50, 50);
  connectBtn1.mousePressed(() => port1.open(9600));

  connectBtn2 = createButton("Connect Arduino 2 (Powerup)");
  connectBtn2.position(50, 100);
  connectBtn2.mousePressed(() => port2.open(9600));
}

function setupGame() {
  player = new Player();
  obstacles = [];
  score = 0;
  gameSpeed = 5;
  obstacleSpawnCounter = 0;
  powerUps = [];
  powerUpSpawnCounter = 0;
  obstacleHits = 0;
  powerUpHits = 0;

  if (gameState === "start" && startButton) {
    startButton.show();
  }
}

function draw() {
  background(bg2);

  if (gameState === "start") {
    drawStartScreen();
  } else if (gameState === "playing") {
    if (!gameMusic.isPlaying()) {
      gameMusic.loop();
    }
    runGame();
  } else if (gameState === "gameover") {
    if (gameMusic.isPlaying()) {
      gameMusic.stop();
    }
    drawGameOverScreen();
  }
}

function drawStartScreen() {
  fill(255);
  background(bg1);
  textAlign(CENTER, CENTER);
  textSize(48);
  text("INSIDE OUT RUNNER", width / 2, height / 2 - 120);

  textSize(24);
  text("Instructions:", width / 2, height / 2 - 50);
  textSize(20);
  text("Up Arrow: Jump over Memory Ball Cluster (obstacle)", width / 2, height / 2 - 15);
  text("Down Arrow: Slide under Memory Bridge (obstacle)", width / 2, height / 2 + 5);
  text("Left/Right Arrows: Move between the 3 lanes", width / 2, height / 2 + 25);

  textSize(24);
  text("Objective:", width / 2, height / 2 + 100);
  textSize(20);
  text("Collect 3 stars for Joy to win!", width / 2, height / 2 + 130);
  text("Hit 3 obstacles for Sadness to win!", width / 2, height / 2 + 150);
}

function drawGameOverScreen() {
  background(bg1);
  fill(255, 50, 50);
  textAlign(CENTER, CENTER);
  textSize(48);
  text("Game Over", width / 2, height / 2 - 40);
  textSize(24);
  text("Final Score: " + floor(score / 10), width / 2, height / 2 + 10);
  text("Press R to Restart", width / 2, height / 2 + 50);

  if (obstacleHits >= 3) {
    text("Sadness Wins!", width / 2, height / 2 + 80);
  } else if (powerUpHits >= 3) {
    text("Joy Wins!", width / 2, height / 2 + 80);
  }
}

function runGame() {
  gameSpeed += 0.0005;
  background(bg2);

  player.update();
  player.show();

  obstacleSpawnCounter++;
  if (obstacleSpawnCounter > max(90 - gameSpeed * 5, 40)) {
    obstacles.push(new Obstacle());
    obstacleSpawnCounter = 0;
  }

  for (let i = obstacles.length - 1; i >= 0; i--) {
    if (obstacles[i].offscreen()) {
      obstacles.splice(i, 1);
      continue;
    }
    obstacles[i].move();
    obstacles[i].show();

    if (obstacles[i].hits(player)) {
      obstacleHits++;
      obstacles.splice(i, 1);
      if (port1 && port1.opened()) {
        port1.write(0);
      }
      if (obstacleHits >= MAX_HITS) {
        gameState = "gameover";
      }
    }
  }

  powerUpSpawnCounter++;
  if (powerUpSpawnCounter > 300) {
    powerUps.push(new PowerUp());
    powerUpSpawnCounter = 0;
  }

  for (let i = powerUps.length - 1; i >= 0; i--) {
    if (powerUps[i].offscreen()) {
      powerUps.splice(i, 1);
      continue;
    }
    powerUps[i].move();
    powerUps[i].show();

    if (powerUps[i].hits(player)) {
      powerUpHits++;
      powerUps.splice(i, 1);
      score += 100;
      if (port2 && port2.opened()) {
        port2.write(1);
      }
      if (powerUpHits >= MAX_HITS) {
        gameState = "gameover";
      }
    }
  }

  score += 1;
  fill(255);
  textSize(24);
  textAlign(LEFT);
  text("Score: " + floor(score / 10), 10, 30);

  textAlign(RIGHT);
  text("Obstacles: " + obstacleHits + "/3", width - 10, 30);
  text("Powerups: " + powerUpHits + "/3", width - 10, 60);
  textAlign(LEFT);
}

function keyPressed() {
  if (gameState === "start" && key === " ") {
    gameState = "playing";
  } else if (gameState === "gameover" && (key === "r" || key === "R")) {
    setupGame();
    gameState = "playing";
  }

  if (gameState === "playing") {
    if (keyCode === LEFT_ARROW) {
      player.move(-1);
    } else if (keyCode === RIGHT_ARROW) {
      player.move(1);
    } else if (keyCode === UP_ARROW) {
      player.jump();
    } else if (keyCode === DOWN_ARROW) {
      player.slide();
    }
  }
}

class Player {
  constructor() {
    this.lane = 1;
    this.x = laneWidth * this.lane + laneWidth / 2;
    this.baseY = height - 50;
    this.y = this.baseY;
    this.r = 20;
    this.gravity = 1;
    this.velocity = 0;
    this.isJumping = false;
    this.isSliding = false;
    this.slideTimer = 0;
  }

  show() {
    fill(255);
    noStroke();
    if (this.isSliding) {
      ellipse(this.x, this.y + this.r / 2, this.r * 2, this.r);
    } else {
      ellipse(this.x, this.y, this.r * 2);
    }
  }

  move(dir) {
    this.lane += dir;
    this.lane = constrain(this.lane, 0, 2);
    this.x = laneWidth * this.lane + laneWidth / 2;
  }

  jump() {
    if (!this.isJumping && !this.isSliding) {
      this.velocity = -20;
      this.isJumping = true;
    }
  }

  slide() {
    if (!this.isJumping && !this.isSliding) {
      this.isSliding = true;
      this.slideTimer = 30;
    }
  }

  update() {
    this.y += this.velocity;
    this.velocity += this.gravity;

    if (this.y >= this.baseY) {
      this.y = this.baseY;
      this.velocity = 0;
      this.isJumping = false;
    }

    if (this.isSliding) {
      this.slideTimer--;
      if (this.slideTimer <= 0) {
        this.isSliding = false;
      }
    }
  }
}

function drawEmotionCloud(x, y) {
  const colors = ['#A066FF', '#FF6666', '#FFFF66', '#66FF66', '#66CCFF'];
  for (let i = 0; i < 5; i++) {
    fill(colors[i % colors.length]);
    noStroke();
    ellipse(x + i * 20, y + sin(i) * 8, 30, 30);
  }
}

function drawMemoryBridge(x, y) {
  fill("purple");
  beginShape();
  for (let dx = -60; dx <= 60; dx += 5) {
    let dy = -20 * sin(PI * (dx + 60) / 120);
    vertex(x + dx, y + dy);
  }
  for (let dx = 60; dx >= -60; dx -= 5) {
    let dy = -10 * sin(PI * (dx + 60) / 120);
    vertex(x + dx, y + dy + 20);
  }
  endShape(CLOSE);

  stroke(0);
  strokeWeight(2);
  for (let dx = -50; dx <= 50; dx += 20) {
    let dy = -20 * sin(PI * (dx + 60) / 120);
    line(x + dx, y + dy, x + dx, y + dy - 10);
  }

  noFill();
  strokeWeight(2);
  beginShape();
  for (let dx = -50; dx <= 50; dx += 5) {
    let dy = -20 * sin(PI * (dx + 60) / 120);
    vertex(x + dx, y + dy - 10);
  }
  endShape();
}

// --- Obstacle Class ---
class Obstacle {
  constructor() {
    this.lane = random(lanes);
    this.x = laneWidth * this.lane + laneWidth / 2;
    this.y = 350;
    if (random(1) < 0.5) {
      this.type = "high";
    } else {
      this.type = "low";
    }
    this.color = color("blue");
  }

  move() {
    this.y += gameSpeed;
  }

  show() {
    if (this.type === "low") {
      drawMemoryBridge(this.x, this.y);
    } else {
      drawEmotionCloud(this.x, this.y);
    }
  }

  hits(player) {
    let playerRadius = player.isSliding ? player.r * 0.5 : player.r;
    let px = player.x;
    let py = player.isSliding ? player.y + player.r / 2 : player.y;

    let collisionX = abs(this.x - px) < 60 / 2 + playerRadius;
    let collisionY = abs(this.y - py) < 40 / 2 + playerRadius;

    if (collisionX && collisionY) {
      if (this.type === "high" && !player.isJumping) return true;
      if (this.type === "low" && !player.isSliding) return true;
    }
    return false;
  }

  offscreen() {
    return this.y > height + 40;
  }
}

// --- PowerUp Class ---
class PowerUp {
  constructor() {
    this.lane = random(lanes);
    this.x = laneWidth * this.lane + laneWidth / 2;
    this.y = 350;
    this.size = 30;
    this.color = color(255, 174, 66);
  }

  move() {
    this.y += gameSpeed;
  }

  show() {
    push();
    fill(this.color);
    noStroke();
    translate(this.x, this.y);
    beginShape();
    for (let i = 0; i < 10; i++) {
      let angle = TWO_PI * i / 10;
      let r = i % 2 === 0 ? this.size : this.size / 2;
      let sx = cos(angle) * r;
      let sy = sin(angle) * r;
      vertex(sx, sy);
    }
    endShape(CLOSE);
    pop();
  }

  offscreen() {
    return this.y > height + this.size;
  }

  hits(player) {
    let d = dist(this.x, this.y, player.x, player.y);
    return d < this.size + player.r;
  }
}

 

Final project testing

For this step of the project, I focused on user testing and started making some updates based on how people interacted with the game. The concept is a physical memory-based game where two characters move forward when the player makes a correct choice, but if they hit an obstacle, a glowing memory ball (controlled by another Arduino) lights up.

Right now, I’m still working on the Arduino connection between the boards. Each moving character has its own Arduino, and I’ve written code that lets them move forward when they receive a command — but syncing the movement between the two boards and handling serial communication properly is still a work in progress. I’m also still refining how everything connects with the third Arduino, which controls the obstacle LED reaction in the memory balls.

During testing, I noticed that sometimes one board would move while the other didn’t, or the LED wouldn’t react consistently. So I’ve been going back into the code and checking the timing, delays, and serial input handling. It’s been a bit tricky trying to get all three Arduinos to communicate clearly without any delays or missed signals.

This week, I’m focusing on:

**Finalizing the Arduino code for the two moving boards
**Getting the serial communication between the three Arduinos to work smoothly

The part I’m most proud of so far is how the basic movement is already working and how the obstacle-triggered lights bring the whole thing to life. Once the connection part is sorted out, I think the physical interactions will feel much more natural and immersive.

Week 11

For this assignment, Izza and I worked together to do the 3 tasks.

Task 1: 

For this task, we used a potentiometer to control whether the ellipse was moving to the left or the right on the horizontal axis. We did this by mapping the values of the potentiometer (0-1023) on the horizontal axis. Then, when we turned the potentiometer the value would translate to a position on the horizontal axis that the ball would move to. We had some difficulties with the delay between the arduino and p5js as sometimes we’d have to wait a couple seconds before the code would update in p5. Here is the code for the arduino:

void setup() {

  Serial.begin(9600);

}




// Read potentiometer value (0–1023) and sends to p5js

void loop() {

  int sensorValue = analogRead(A0);

  Serial.println(sensorValue);

  delay(1);

}

 

Task 2:

For this task, we had to do something that controlled the brightness of an LED on the arduino breadboard through p5js. So, we decided to create a dropdown for the user to pick between 1-10 to control the brightness of the LED with 1 being off and 10 being maximum brightness. We then mapped that to the 0-255 range for the brightness of an LED and sent that to the arduino which would control how brightly the LED would light up for a few seconds. On the arduino we simply had one bulb connected to digital pin 9. The arduino code can be seen below:

void setup() {

  Serial.begin(9600);

  pinMode(9, OUTPUT);

}




//gets the serial converted value from p5js

void loop() {

  if (Serial.available() > 0) {

    int brightness = Serial.parseInt(); 

    brightness = constrain(brightness, 0, 255); //make sure the value isn't out of range

    analogWrite(9, brightness);

  }

}

 

Task 3:

In this task, we had to take already existing code and alter it such that every time the ball bounced, one LED light on the arduino lit up, and the wind was controlled by an analog sensor. For controlling our wind, we used a potentiometer once again as we could make it such that values above 512 would move the ball to the east (right) and values below 512 would move the ball towards the west (left). On the arduino, we connected a potentiometer at analog pin A0 and an LED light on digital pin 9. We then used p5js to recieve that serial input from the potentiometer and map it to the wind. Whether it bounced being true or false is also what makes the LED light up. Once again, we did experience a delay between the potentiometer value and the wind in p5. The arduino code can be seen below:

const int potPin = A0;

const int ledPin = 9;

bool ledOn = false;

unsigned long ledTimer = 0;

const int ledDuration = 100;




void setup() {

  Serial.begin(9600);

  pinMode(ledPin, OUTPUT);

}




void loop() {

  // Read potentiometer and send value

  int potValue = analogRead(potPin);

  Serial.println(potValue);




  // If LED was turned on recently, turn it off after some time

  if (ledOn && millis() - ledTimer > ledDuration) {

    digitalWrite(ledPin, LOW);

    ledOn = false;

  }




  // recieve signal on whether the ball bounced from p5.js

  if (Serial.available()) {

    String input = Serial.readStringUntil('\n');

    input.trim();




    if (input == "bounce") {

      digitalWrite(ledPin, HIGH);

      ledOn = true;

      ledTimer = millis();

    }

  }




  delay(10); // Slight delay for stability

}

 

Lastly, here is the link to the video showing the LED light up and the ball being “blown” away by the value sent from the potentiometer:

https://drive.google.com/file/d/140pGv-9DMPd1gCa1xMn_LR3pR_pphx47/view?usp=sharing

Week 14 – Final Project Show Case:

Video:

Fixes made:

The p5 program had issues with counting the score and incrementing it. Turns out, there was a Upper case – lower case difference between the Serial.print by arduino and what was being expected by the P5 program. This in return rendered no score on the P5 canvas. This was identified and fixed before the showcase.

The P5 canvas also had text mis-alignment, which was worked towards as well.

In addition to it, the bearing  kept on falling off, this was then fixed by sharpening the wooden stick using a giant sharpner Mr Dustin got me. Using it, the diameter was reduced enough to fit into the hole inside the bearing!

Drilling a pilot hole to put a M3 screen through was hard, hence didn’t take the risk, but implementation of that surely would have made sure it never came off.

Feedback from the audience:

The audience loved the action packed mechanical project. The best part was the frustration and the eagerness to compete. The concept of timer garnered total attention, and moving mechanism alongside trajectory of markers and tape made it unique in its on way. Some people did suggest going for more easier level, which was taken into consideration.

The open-rounded lid as stationary holder was used. Maybe in the future, one with bigger diameter can be used to facilitate the user in their gameplay.

So far, what stood out for me as complement, was the genuineness  the project had in terms of how it was built and was structured. I take pride in getting my hands dirty and full of splinters and hot glue, and hence, I found it to be an overall success.

Of course the release mechanism would have taken to it to next level, and that is something I will be working towards over the Summers!

 

Final Project Documentation

Concept

I was thinking long and hard about the final project that would be a great finale to all of the things we have learned during the semester. I have decided I want to make a robot. Robot is a broad term and I had to decide the purpose of mine, and since I wanted to create something that is innovative, fun and creative I decided to make a maze solving robot. The initial plans were to make the robot go through the maze on its own and have the user just set up the maze, but we will get to why it didn’t work out like that later. Instead of the robot solving the maze on its own, now its the user who is in control and trying to go through it “blind”, precisely using the ultrasonic sensors as guides. The user that controls the robot does not see the maze and is solving it based just on the sensors, while their friend rearranges the maze between sessions in order to make the game fun and interesting throughout the session.

Video of user interaction with the project
Arduino code
const int AIN1 = 13;
const int AIN2 = 12;
const int PWMA = 11;

const int PWMB = 10;
const int BIN2 = 9;
const int BIN1 = 8;

const int trigPinFront = 6;
const int echoPinFront = 5;

const int trigPinLeft = A0;
const int echoPinLeft = 2;

const int trigPinRight = 4;
const int echoPinRight = 3;

unsigned long lastEchoTime = 0;
const unsigned long echoInterval = 300;

void setup() {
  Serial.begin(9600);

  pinMode(AIN1, OUTPUT); 
  pinMode(AIN2, OUTPUT); 
  pinMode(PWMA, OUTPUT);
  pinMode(BIN1, OUTPUT); 
  pinMode(BIN2, OUTPUT); 
  pinMode(PWMB, OUTPUT);
  pinMode(trigPinFront, OUTPUT); 
  pinMode(echoPinFront, INPUT);
  pinMode(trigPinLeft, OUTPUT); 
  pinMode(echoPinLeft, INPUT);
  pinMode(trigPinRight, OUTPUT); 
  pinMode(echoPinRight, INPUT);

  Serial.println("READY");
}

void loop() {
  if (Serial.available()) {
    char command = Serial.read();

    //Resppond to command to move the robot
    switch (command) {
      case 'F':
        leftMotor(50); rightMotor(-50);
        delay(1000);
        leftMotor(0); rightMotor(0);
        break;
      case 'B':
        leftMotor(-50); rightMotor(50);
        delay(1000);
        leftMotor(0); rightMotor(0);
        break;
      case 'L':
        leftMotor(200); rightMotor(200);
        delay(300);
        leftMotor(200); rightMotor(200);
        delay(300);
        leftMotor(0); rightMotor(0);
        break;
      case 'R':
        leftMotor(-200); rightMotor(-200);
        delay(300);
        leftMotor(-200); rightMotor(-200);
        delay(300);
        leftMotor(0); rightMotor(0);
        break;
      case 'S':
        leftMotor(0); rightMotor(0);
        break;
    }
  }

  //Send distance data to the serial
  unsigned long currentTime = millis();
  if (currentTime - lastEchoTime > echoInterval) {
    float front = getDistance(trigPinFront, echoPinFront);
    float left = getDistance(trigPinLeft, echoPinLeft);
    float right = getDistance(trigPinRight, echoPinRight);

    Serial.print("ECHO,F,"); Serial.println(front);
    Serial.print("ECHO,L,"); Serial.println(left);
    Serial.print("ECHO,R,"); Serial.println(right);

    lastEchoTime = currentTime;
  }
}

//Logic for controling the movement of the right and left motor
void rightMotor(int motorSpeed) {
  if (motorSpeed > 0) {
    digitalWrite(AIN1, HIGH);
    digitalWrite(AIN2, LOW);
  } else if (motorSpeed < 0) {
    digitalWrite(AIN1, LOW);
    digitalWrite(AIN2, HIGH);
  } else {
    digitalWrite(AIN1, LOW);
    digitalWrite(AIN2, LOW);
  }
  analogWrite(PWMA, abs(motorSpeed));
}

void leftMotor(int motorSpeed) {
  if (motorSpeed > 0) {
    digitalWrite(BIN1, HIGH);
    digitalWrite(BIN2, LOW);
  } else if (motorSpeed < 0) {
    digitalWrite(BIN1, LOW);
    digitalWrite(BIN2, HIGH);
  } else {
    digitalWrite(BIN1, LOW);
    digitalWrite(BIN2, LOW);
  }
  analogWrite(PWMB, abs(motorSpeed));
}

//Logic for measuring distance
float getDistance(int trigPin, int echoPin) {
  digitalWrite(trigPin, LOW);
  delayMicroseconds(2);
  digitalWrite(trigPin, HIGH);
  delayMicroseconds(10);
  digitalWrite(trigPin, LOW);

  long duration = pulseIn(echoPin, HIGH);
  float distance = duration / 148.0;
  return distance;
}

The Arduinos main purpose is to handle motor move meant as well as use the data from the distance sensors and send them to p5. For the movement it takes data from p5 which the user enters by pressing buttons on the keyboard and translates them to motor movement which imitates the movement on screen. The data from the 3 ultrasonic sensors is picked up with the Arduino and sent to the serial in order to be picked up by p5.

The p5 code takes the echo values that the Arduino sends and uses that date to draw the “echo lines” which the user will use to “see” the maze with the walls being visible every now and then if in range. P5 is also used to take user input and send it to the Arduino which translates it to movement. It also has code that serves as main connection from the Arduino to p5.

Here is the schematic of the circuit. One of the most challenging parts of this project was connecting all the wires and making sure they wouldn’t disconnect during transportation and during the showcase. I kept all the wires as far apart from each other as possible and made sure everything that could move them is nicely secured to the plate.

The making of the project

Making this project was a journey. What seemed to be a straight forward project turned out to be a 2 week long process of trial and error until I got the final result.

As I have mentioned above in the beginning the idea was to make the robot go through the maze on its own and have the user just set up the maze.

This is one of the photos of the early stages of development of the robot. As you can see it looks so much different than the final product. This was the part of the project where I was focusing on just getting the movement and some reading from the sensors.

After I managed to get the movement done with the cable attached and with sending commands through my laptop I was ready to move on to the next phase which was adding 2 more sensors and having the robot move on its own. But before I could even do that I wanted to start working on the maze. The base of them maze was 120cm wide and 180cm tall, so if I put it up it would be roughly the same size as me. I also had to make the walls of the maze which were 20cm each in order to get picked up by the sensors on the robot. I also created temporary walls that could be moved by the users to give more interactivity to the project. This turned out to be much more of a time consuming and painful process than I thought because I had to use scraps of cardboard and make sure each peace is not only 20cm tall, but also that the cut on the side of the piece is straight enough so it can stick to the other piece. After that was done, testing for the autonomous movement was ready to start.

The movement seems to be alright, but if you watched carefully in the beginning the paperclip that was used as the 3rd wheel got a bit stuck on the cardboard. At the time of the recording of this video I didn’t think that would be an issue, but damn was I wrong. After more testing something scary started happening. The paperclip wouldn’t only get stuck a bit and make the robot slow down, it would actually fly right off the robot every time it got stuck. Also other problems came up, such as the robot constantly resetting in place every time it would start without a cable attached to it and also the third sensor not reading anything. So lets go through one problem at a time.

The problem with the robot resetting was very easy to debug and fix. The main reason something like that would be happening only when the cable is not plugged would mean something with the power is not alright. At the time I was using 4 1.5V batteries to power the motors of the robot as well as the Arduino which proved to be insufficient.  The fix was to connect a 9V battery to the motors to allow the 1.5V batteries to be used just by the Arduino which fixed the problem. The next problem was the reading of the ultrasonic sensors. from all the wiring I have ran out of digital pins and had only one digital and one analog pin left for the sensor. After searching the internet I read that it should be fine since the analog pins can behave as digital, but my readings were still just 0. After talking with the professor who went through the source code of the Arduino he discovered that “Indeed there is a conversion table for pin numbers to their internal representations (bits in a port) and the table only includes the digital pins!”. The fix that the professor suggested and which ended up working is plugging the Echo pin in the analog one and the Trig in the digital. This helped me move on with the project and I would like to thank professor Shiloh one more time for saving me countless hours debugging!

Back to the movement issue. Because the paperclip kept getting stuck and flying off I had decided to remove it completely and instead use a wheel in the back which would act as support and allow the robot to move through all the bumps in the floor without a problem, or so I thought. After cutting the acrylic and getting the wheel in place I spent 2 hours trying to get the acrylic to stick to the base of the robot and in the process I superglued my finger to my phone which was not a fun experience at all! When I managed that I started the robot up and all seemed fine until I decided to try it out on the maze. Not only was the robot not detecting the walls, it was also not turning at all. After another countless hours of debugging here is what happened.

First of all the ultrasonic sensors are very unreliable which I found was the reason the robot wasn’t seeing the walls. Sometimes the reading would be 25 inches and would suddenly jump to 250inches which was impossible because it would mean the walls were too far apart. This was the main reason I decided to switch from an autonomous robot to the one that is controlled by the user. As for the movement, since the wheel was made out of rubber it created friction with the cardboard an it didn’t allow the robot to make a turn. It took me a lot of trial and error to realize the problem and come up with somewhat of the solution. I taped the the bottom up with clear duct tape which was slipping on the cardboard and allowed turning. The problem with this was the mentioned slipping, as one press of the button would make the robot go 360. I put in tape on the bottom of the cardboard which would stop that, but would also sometimes stop the turning mid way. In retrospect I would have saved myself so much trouble if I just let go of the idea of the cardboard floor!

And so we come to the final design of the robot and the maze.

Areas of improvement

There are definitely some areas that could be worked on more in order to make the robot more functional. The first and most obvious one would be changing the cardboard floor with something else, perhaps something which doesn’t have bumps in it and wouldn’t create too much friction. Another thing is removing the back wheel and adding something else in its place, something that allows it to spin in place, but creates enough friction so the robot is not just spinning around in circles. I would also add an instructions page on the p5 as I have realized some users were confused on what to do when they approached the computer. Also I would try to find an alternative to the ultrasonic sensors and use something much more reliable which would allow the autonomous movement of the robot.

Things I am proud of and conclusion

I am honestly proud of the whole project. I think it is a great reflection of the things we have learned during the semester of Intro to IM and a great representation of how far we have come. When I started the course I didn’t even know how to connect wires to bake an LED light up, and here I am 14 weeks later making a robot that drives around the maze. Even though the journey to the end product was a bumpy one, I am grateful for everything I have learned in the process, every wire I had to cut and put back 20 times, all the sensors I went through to find the ones that work, all of it taught me valuable lessons and I am excited to start with new projects in the future. Thank you for reading through my journey through this class and I hope I will have a chance to write a blog again when I start my next project!

Week 14 – Final Project Blog

Concept and Implementation

The game begins with a start page where the user gets enough time to read through the instructions.

Upon clicking start the game begins with a sequence of one color; say blue. The color is highlighted with a black stroke. The user is supposed to press the same color. If they get it right they proceed to the next round which appends one more color to the sequence say green and now the player sees two colors blue and green. If they had gotten it wrong on the first try the sequence repeats at the expense of one out of the three lives that the player has. This is to say that the player has got only two passes to get it wrong. The game continues until all the colors have been highlighted and well matched by the players. At the side there is a score tracking progress bar that fills depending on the number of colors that the player gets right. When the player successfully fills the bar by getting all the colors right, the game ends and the player wins. If the player gets it wrong three times the game ends and they have the option to restart.

VIDEO/ IMAGES

 

ARDUINO

The arduino code is less complicated compared to the p5js code. The connections involved 

  • 4 led push buttons
  • Buzzer
  • Jumper wires

SCHEMATIC

For my scematic I used normal push buttons as I could not find the led-push buttons for representation.

ARDUINO CODE

// Pin Definitions
const int redPin = 6;
const int greenPin = 7;
const int bluePin = 8;
const int yellowPin = 9;
const int buzzerPin = 11;

const int buttonPins[] = {2, 3, 4, 5};  // Red, Green, Blue, Yellow
const char* colorNames[] = {"red", "green", "blue", "yellow"};

// Frequencies for different colors (in Hz)
const int tones[] = {262, 330, 390, 494}; // A4, C5, D5, E5

int lastButtonState[4] = {HIGH, HIGH, HIGH, HIGH};  // For edge detection
String colorSequence = "";  // Collects pressed color names

void setup() {
  // LED outputs
  pinMode(redPin, OUTPUT);
  pinMode(greenPin, OUTPUT);
  pinMode(bluePin, OUTPUT);
  pinMode(yellowPin, OUTPUT);
  pinMode(buzzerPin, OUTPUT);

  // Button inputs
  for (int i = 0; i < 4; i++) {
    pinMode(buttonPins[i], INPUT_PULLUP);
  }

  Serial.begin(9600);
}

void loop() {
  for (int i = 0; i < 4; i++) {
    int currentState = digitalRead(buttonPins[i]);

    // Detect new button press (from HIGH to LOW)
    if (lastButtonState[i] == HIGH && currentState == LOW) {
      // Turn on the LED
      if (i == 0) digitalWrite(redPin, HIGH);
      if (i == 1) digitalWrite(greenPin, HIGH);
      if (i == 2) digitalWrite(bluePin, HIGH);
      if (i == 3) digitalWrite(yellowPin, HIGH);

      // Play tone on buzzer
      tone(buzzerPin, tones[i], 150);  // Play for 150 ms

      // Append to sequence and send it
      if (colorSequence.length() > 0) {
        colorSequence += ",";
      }
      colorSequence += colorNames[i];

      Serial.println(colorSequence);  // Send entire sequence
    }

    // Turn off LED if button is released
    if (currentState == HIGH) {
      if (i == 0) digitalWrite(redPin, LOW);
      if (i == 1) digitalWrite(greenPin, LOW);
      if (i == 2) digitalWrite(bluePin, LOW);
      if (i == 3) digitalWrite(yellowPin, LOW);
    }

    lastButtonState[i] = currentState;
  }

  delay(50);  // Short delay for debouncing
}
P5JS 

For my p5js I mainly used functions to help with handling of data and processing from the arduino and to display. In the sketch I had html, a webserial library, music(background, correct and incorrect sounds), and the actual p5js sketch code. Some of the notable portions of the p5js code include music handling, serial communication, game start page, button handling and game over page.

Below are the different code portions.

HTML

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/addons/p5.sound.min.js"></script>
    
    <!-- Load the web-serial library -->
    <script src="p5.web-serial.js"></script>
    
    <link rel="stylesheet" type="text/css" href="style.css">
    <meta charset="utf-8" />

  </head>
  <body>
    <main>
    </main>
    <script src="sketch.js"></script>
  </body>
</html>

WEBSERIAL LIBRARY

let port, reader, writer;
let serialActive = false;

async function getPort(baud = 9600) {
  let port = await navigator.serial.requestPort();

  // Wait for the serial port to open.
  await port.open({ baudRate: baud });

  // create read & write streams
  textDecoder = new TextDecoderStream();
  textEncoder = new TextEncoderStream();
  readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
  writableStreamClosed = textEncoder.readable.pipeTo(port.writable);

  reader = textDecoder.readable
    .pipeThrough(new TransformStream(new LineBreakTransformer()))
    .getReader();
  writer = textEncoder.writable.getWriter();

  return { port, reader, writer };
}

class LineBreakTransformer {
  constructor() {
    // A container for holding stream data until a new line.
    this.chunks = "";
  }

  transform(chunk, controller) {
    // Append new chunks to existing chunks.
    this.chunks += chunk;
    // For each line breaks in chunks, send the parsed lines out.
    const lines = this.chunks.split("\r\n");
    this.chunks = lines.pop();
    lines.forEach((line) => controller.enqueue(line));
  }

  flush(controller) {
    // When the stream is closed, flush any remaining chunks out.
    controller.enqueue(this.chunks);
  }
}

async function setUpSerial() {
  noLoop();
  ({ port, reader, writer } = await getPort());
  serialActive = true;
  runSerial();
  loop();
}

async function runSerial() {
  try {
    while (true) {
      if (typeof readSerial === "undefined") {
        console.log("No readSerial() function found.");
        serialActive = false;
        break;
      } else {
        const { value, done } = await reader.read();
        if (done) {
          // Allow the serial port to be closed later.
          reader.releaseLock();
          break;
        }
        readSerial(value);
      }
    }
  } catch (e) {
    console.error(e);
  }
}

async function writeSerial(msg) {
  await writer.write(msg);
}

p5js SKETCH

gridSize = 5;
squareSize = 100;
colors = ['red', 'green', 'blue', 'yellow'];
grid = [];
sequence = [];
playerInput = [];
showingSequence = false;
showIndex = 0;
showTimer = 0;
inputEnabled = false;
currentRound = 1;
gameOver = false;
gameStarted = false;

serialMessage = '';
messageColor = 'black';
confetti = [];
startButton = null;
restartButton = null;

statusTimer = 0;
statusDuration = 2000;

roundStartTime = 0;
timeLimit = 10000;
timeLeft = timeLimit;

let bgMusic, correctSound, incorrectSound;

lives = 3;

function preload() { //loading the music
  soundFormats('mp3', 'wav');
  bgMusic = loadSound('music/background.mp3');
  correctSound = loadSound('music/correct.mp3');
  incorrectSound = loadSound('music/incorrect.mp3');
}

function setup() {
  createCanvas(gridSize * squareSize + 80, gridSize * squareSize + 60);
  noStroke();

  startButton = createButton('▶ Start Game');//start button
  styleButton(startButton, width / 2 - 60, height / 2 + 10, '#4CAF50');
  startButton.mousePressed(async () => {
    await setUpSerial();//using the start button to initiate serial comm
    startGame();
  });

  bgMusic.setLoop(true);
  bgMusic.play();// playing the background music throughout
}

function styleButton(btn, x, y, color) {// styling code function for all the buttons
  btn.position(x, y);
  btn.style('font-size', '20px');
  btn.style('padding', '10px 20px');
  btn.style('background-color', color);
  btn.style('color', 'white');
  btn.style('border', 'none');
  btn.style('border-radius', '8px');
}

function startGame() { //initializing the game
  gameStarted = true;
  startButton.remove();// moving from the start page to the grid
  initGrid();
  nextRound();
}


//game loop
function draw() {
  background(220);

  if (!gameStarted) {
    drawStartScreen(); 
    return;
  }

  if (gameOver) {
    drawEndScreen();
    return;
  }

  drawGrid();
  drawProgressBar();
  drawTimeBar();
  drawSidebar();

  if (showingSequence && millis() - showTimer > 800) {
    showTimer = millis();
    showIndex++;
    if (showIndex >= sequence.length) {
      showingSequence = false;
      inputEnabled = true;
      showIndex = 0;
      roundStartTime = millis();
    }
  }

  if (inputEnabled) {
    timeLeft = timeLimit - (millis() - roundStartTime);
    if (timeLeft <= 0) {
      handleIncorrect('Time Up!');
    }
  }

  clearSerialMessageIfDue();
}

function drawStartScreen() {
  drawBackgroundGradient();
  fill(30);
  textAlign(CENTER, CENTER);
  textFont('Helvetica');
  textSize(36);
  text(' Color Memory Game', width / 2, height / 2 - 80);
  textSize(20);
  text('Repeat the color sequence using the physical buttons!', width / 2, height / 2 - 40);
}


//---------------END SCREEN-------------------------------
function drawEndScreen() {
  drawBackgroundGradient();
  drawConfetti();
  drawGameOverArt();
  textAlign(CENTER, CENTER);
  fill('#2E8B57');
  textFont('Georgia');
  textSize(36);
  text('Game Over!', width / 2, height / 2 - 80);
  fill(50);
  textSize(16);
  text('Press the button to restart', width / 2, height / 2 - 40);
  if (!restartButton) {
    restartButton = createButton(' Restart');
    restartButton.id('restartBtn');
    styleButton(restartButton, width / 2 - 60, height / 2 + 20, '#f44336');
    restartButton.mousePressed(() => {
      restartButton.remove();
      restartButton = null;
      resetGame();
    });
  }
}

//--------------------BOTTOM BAR--------------------------
//Displaying the message
function updateSerialMessage(msg, color) {
  serialMessage = msg;
  messageColor = color;
  statusTimer = millis();
}
//Clearing the message
function clearSerialMessageIfDue() {
  if (millis() - statusTimer > statusDuration && serialMessage !== '') {
    serialMessage = '';
  }
}

//---------------------SIDE BAR---------------------------------
function drawSidebar() {
  let barWidth = 60;
  let barX = width - barWidth;
  fill(240);
  rect(barX, 0, barWidth, height);

  let progress = sequence.length / grid.length;//filling with the progress bar
  fill('#2196F3');
  rect(barX + 10, 20, 40, height * progress);

  textAlign(CENTER, CENTER);
  textSize(20);
  for (let i = 0; i < 3; i++) {
    let y = height - 40 - i * 30;
    fill(i < lives ? 'red' : 'lightgray');
    text(i < lives ? '❤️' : '', barX + barWidth / 2, y);// representing lives with red heart emojis
  }
}

// -------------------TIME BAR---------------------------------------
function drawTimeBar() {
  let barHeight = 10;
  let barY = height - 60;
  let progress = constrain(timeLeft / timeLimit, 0, 1); 

  fill(200);
  rect(0, barY, width - 80, barHeight);

  fill(progress < 0.3 ? '#F44336' : progress < 0.6 ? '#FFC107' : '#4CAF50');
  rect(0, barY, (width - 80) * progress, barHeight);///changing color depending in the time left
}


//-------------------PROGRESS BAR-----------------------------
function drawProgressBar() {
  let barHeight = 30;
  let barY = height - barHeight;
  let progress = sequence.length > 0 ? playerInput.length / sequence.length : 0;

  fill(200);
  rect(0, barY, width - 80, barHeight); // filling the bar proporionally to the progress

  fill(messageColor === 'green' ? '#4CAF50' : messageColor === 'red' ? '#F44336' : '#2196F3');
  rect(0, barY, (width - 80) * progress, barHeight);

  fill(255);
  textAlign(CENTER, CENTER);
  textSize(16);
  text(serialMessage, (width - 80) / 2, barY + barHeight / 2);
}

//-------------------GRID SET UP-------------------------------------
function drawGrid() {
  for (let i = 0; i < grid.length; i++) {
    let sq = grid[i];
    let x = sq.x;
    let y = sq.y;
// highlighting square by a black stroke and appending them to the sequence.
    if (showingSequence && i === sequence[showIndex]) {
      let sw = 6;
      strokeWeight(sw);
      stroke(0);
      let inset = sw / 2;
      fill(sq.color);
      rect(x + inset, y + inset, squareSize - sw, squareSize - sw);//fitting the stroke within the square
    } else {
      noStroke();
      fill(sq.color);
      rect(x, y, squareSize, squareSize);
    }
  }
  noStroke();
}

function initGrid() {
  grid = [];
  for (let row = 0; row < gridSize; row++) {
    for (let col = 0; col < gridSize; col++) {
      let availableColors = colors.slice();
      if (row > 0) { //avoiding the same colors being next to each other in a row
        let aboveColor = grid[(row - 1) * gridSize + col].color;
        availableColors = availableColors.filter(c => c !== aboveColor);
      }
      if (col > 0) {//avoiding the same colors being next to each other in a col
        let leftColor = grid[row * gridSize + (col - 1)].color;
        availableColors = availableColors.filter(c => c !== leftColor);
      }
      grid.push({ x: col * squareSize, y: row * squareSize, color: random(availableColors) });
    }
  }
}

//---------SERIAL COMMUNICATION MANAGEMENT----------------------------
function readSerial(data) {
  if (!inputEnabled || gameOver) return;
  let colorClicked = data.trim().split(',').pop().trim();//reading the serial port for the color pressed and printed out by arduino
  let expectedIndex = sequence[playerInput.length];
  let expectedColor = grid[expectedIndex].color;// checking if the colors match
  if (colorClicked === expectedColor) {
    playerInput.push(expectedIndex);
    correctSound.play();
    updateSerialMessage('Correct', 'green');//updating the message.
    if (playerInput.length === sequence.length) {
      inputEnabled = false;
      setTimeout(nextRound, 1000);
    }
  } else {
    handleIncorrect('Incorrect');
  }
}


// checking if the pattern by the player is incorrect
function handleIncorrect(message) {
  incorrectSound.play();
  updateSerialMessage(message, 'red');
  lives--;
  playerInput = [];
  inputEnabled = false;
  if (lives <= 0) { // they have no more lives end game
    gameOver = true;
    spawnConfetti();
  } else {
    setTimeout(replayRound, 1500);
  }
}


//playing the next round when the player gets it right
function nextRound() {
  currentRound++;
  playerInput = [];
  if (sequence.length >= grid.length) { //checking if all the squares have been matched
    gameOver = true;// end game if true
    spawnConfetti();
    return;
  }
  sequence.push(floor(random(grid.length)));// append one more random square if the game is not over
  showingSequence = true;
  showIndex = 0;
  showTimer = millis();
}
// Repeating the round when the player gets it wrong
function replayRound() {
  playerInput = [];
  showingSequence = true;
  showIndex = 0;
  showTimer = millis();
}

function drawBackgroundGradient() {
  for (let y = 0; y < height; y++) {
    let c = lerpColor(color(255), color(200, 220, 255), map(y, 0, height, 0, 1));
    stroke(c);
    line(0, y, width, y);
  }
}


//-----------------CONFETTI FOR GAME OVER------------------------------------
function spawnConfetti() {
  confetti = [];
  for (let i = 0; i < 100; i++) {
    confetti.push({
      x: random(width),
      y: random(-200, 0),
      speed: random(2, 5),
      size: random(5, 10),
      color: random(colors)
    });
  }
}


function drawConfetti() {
  for (let c of confetti) {
    fill(c.color);
    noStroke();
    ellipse(c.x, c.y, c.size);
    c.y += c.speed;
    if (c.y > height) {
      c.y = random(-100, 0);
      c.x = random(width);
    }
  }
}

function drawGameOverArt() {
  for (let i = 0; i < sequence.length; i++) {
    fill(grid[sequence[i]].color);
    ellipse(50 + (i % 10) * 25, 50 + floor(i / 10) * 25, 20);
  }
}

//-------------- RESETTING THE GAME------------------------------------------
function resetGame() {
  lives = 3;
  sequence = [];
  playerInput = [];
  showingSequence = false;
  showIndex = 0;
  inputEnabled = false;
  currentRound = 1;
  gameOver = false;
  timeLeft = timeLimit;
  serialMessage = '';
  messageColor = 'black';
  initGrid();
  nextRound();
}
Areas I am proud of and areas for future improvement

I am really proud about how the game graphics turned out. That is; buttons, score recording and representation, start page, game over page, restart logic, “lives” handling and the game progression loop. On the hardware arduino side, I was proud of the carpentry and design of the wooden platform that held the push-buttons.  However, I think there’s still more that could have been done on the aspect of aesthetics to make my projects more appealing and attractive. I also need to work on communicating instructions more effectively and intuitively. Also it was in my very initial idea to 3D print so that I can automate a robot following the progress of the game. I was limited however because of the high demand of the 3D printers. I hope to one day finish what I have started.

Week 13 – User Testing and Interaction

Video Demo:

note- instructions were provided in this video, after the user was unable to lock the target. In all fairness, such intricacy was bounded to game rules and hence was required to be briefed so that he can continue operating it.

Initial Impression:

The initial Impression of the project was awe-inspiring for most of the audience, especially for the male audience. One can sense the ‘awesomeness’ – quoting my friend by merely looking at the rough-tough built of the design. For the female audience, the lack of color-scheming made it less appealing. This was concluded after asking 8 of my classmates to test out my project.

Haris for instance was thrilled to see the project and idea come to life! The very idea of gamifying putting things back into their place with scoring mechanism was a huge plus point according to him. Overall, the functional and responsive piece of wood, cardboard, and PVC pipe did pique their curiosity. Unlike conventional design mechanism, this was something different. I am grateful to professor for approving and allowing this project. A mix of Cornhole game and concept of  re-racking of equipment after its use. To make it appropriate for the settings, the launcher launches out markers and scotch tape into the pencil holder – with a button attached to the bottom.

Areas with successful interaction:

The major area of success was the button movement and LED correspondence to the game state. The game when in MENU state, turns on the red LED. When start button pressed, it triggers the game to start both in P5.js and inside Arduino program. This turns on the blue LED. This is when the user can control the movement of the canon. The button placement reflects the direction of movement, thus users had no problem interacting with the system.

Confusion and explanation needed:

However, the confusion arose when they were unable to lock and shoot. Provided that instructions were not given regarding game rule, they continued to either move the launcher back and forth, or tried fiddling around with the inner barrel.

Conclusion:

Label the buttons and LED to allow for smoother user interface!

Week 13 – User Testing

People Experience

I had people try out my game without prior instruction. At first people did not understand how to play the game but eventually after two tries people got the hang of it. It was particularly challenging at first because of the push buttons that took really long to read data(must have been faulty). 

Upon getting the hang of it, I could see the excitement as the game is really challenging for the users and therefore very exciting.

Some of the areas of confusion came when the users did not read the basic instructions on the game flow but when they did it became all so easy to understand what is happening between the controls and the experience on the screen 

Effective aspects and areas to improve.

I am particularly proud of the fact that the game did not crash or have an unexpected breakdown. I am also proud that the communication was well mapped at most times between the arduino and p5js. I was able to channel all the data from randomly pressed push-buttons to p5js to match the sequence which I found just amazing. Also the synchronization between the arduino push-buttons and the p5js sounds made when the user gets it right was really challenging but when I got it done it was really fulfilling.

There are still some areas of improvement however. I noticed that after playing for a long time the game began to lag. This was indication that I needed a better way to map the data from arduino to p5js since as the game progressed data being mapped became more and more. Additionally, I needed to make the time more proportional to the number of colors expected to be matched by the player. I thought it would be a great challenge for the players but the limited time made it even more difficult(doable though). I could consider making the timing more lenient so as to have more people try to finish.

Areas the need to explain

Most people started the game without understanding the instructions. Most people thought that they were supposed to press the push-buttons immediately when the colors were displayed on the screen but that was not the case. I therefore had to explain to most people to wait for the sequence to completely finish before they could begin pressing the push buttons. I think to solve this problem for first timers playing the game, the best thing was to make the instructions more clear on the screen rather than printed out as I had planned.

Week 12 – Final Proposal and Progress

Finalized concept for the project;

Even though I was unsettled at first I finally decided to go with the color matching game where the user observes random color patterns on p5js and tries to match them by pressing the same sequence of colors on a set of push buttons embedded on a wooden platform. 

Communication between p5js and arduino;

The communication between p5js and arduino begins with a handshake which is initialized by the library we used in class – p5-web-serial(). The p5js generates random colors in increasing sequence and waits for response from the arduino. Arduino sends feedback in terms of the color that has been pressed if it is green for instance,… the  p5 reads the serial which has the color pressed in arduino printed out. If the color read matches with the expected color then p5js reads it as correct  and proceeds to the next round. If not, the p5js reads it as wrong and repeats the same sequence. For multiple colors in a long sequence the arduino prints out the colors pressed in an array that is matched against that which has been displayed in p5js. And the same logic of checking for correctness is repeated.

 Images of progress