Week #13 – Final Project Documentation

Concept

I’ve created this project to explore one of Kazakhstan’s cultural traditions  seating around the table (or Dastarkhan in kazakh). This experience alone unpacks different parts of our culture from food to music. To expose the user to those different parts, I’ve created multiple levels to guide the user’s journey in understanding what Dastarkhan is. I’ve built physical room where the table is situated and implemented sensors and buttons around it to support next interactions. Overall, I have 4 levels that user has to complete:

  • First level is about interaction with the buttons.
    I’ve positioned three red buttons around the table to symbolize different seating placements around the table in kazakh culture. Depending on your social status and age, your place around Dastarkhan will be differentTherefore, user had to press different buttons to explore the meaning of the places. When user pressed the button the corresponding image appeared on the P5.

After pressing all three buttons on the physical project, the button to go to next level appears on the P5 screen.

  • Second level is about kazakh music. The user has to find kazakh musical instrument dombyra around the table and press it to trigger the play of the composition in the P5. When the user presses dombyra, the force sensor from Arduino sends value to P5 and the video of a musician playing on Dombyra appears.
  • Third level introduces the user to national dishes on the Dastarkhan. By pressing the arrows on the P5 screen, the user scrolls to different dishes and their explanations.

    There is no interaction between P5 and Arduino at this level. The table rotates on the physical project using the servo motor, while the P5 is guided by the user.
  • Fourth level holds more sentimental meaning, as it uses the pulse sensor. The user has to place their finger on the pulse sensor and press on the heart on the screen. The P5 receives the message that Pulse sensor is active and then it triggers the appearance of pictures of my family and friends.
Videos of Project Interaction

P5 Project

Link to P5 code

Arduino Code

Link to Github

The communication between Arduino and P5 is based on buttons, pulse sensor and force sensor. Values from Arduino are directly sent to the P5 and trigger appearance of certain elements.

What I’m Proud of

I am really proud of my final outcome. It took a lot of time working with the buttons, soldering them and figuring out how to work with them. I spent quite a lot time building physical project and connecting both parts. Moreover, I am proud that I figured out how to code the serial communication, so that multiple sensors could be connected at the same time, while only one of them sends information at a time. I am also proud of aesthetics of my project, since I feel like I could really deliver the atmosphere of Kazakh interior setting and culture. Many Kazakh students came to me sharing that my project really took them to home.

Future Improvements

In the future, I would like to work more on the complexity and depth of interactions between the project and the user. In this project, I only worked out 4 levels of interaction, but there are so many other ways to do it. Also, I would like to work on intuitiveness in experiencing my project. I would like to step aside from my main role of communicating purpose, idea and workings of the project, and leave it to the user himself to figure out. That way I could really achieve high level interaction. Additionally, I would explore more the intersection of traditions and technologies, how are they addressed in today’s world. For now, I just tried to attempt accomplishing this interaction, but there are more sensors and immersive experiences to explore.

Final Project Documentation

Concept

My final project was  an interactive robot which someone can control. I was also inspired by the animation Despicable Me which can explain the design of my robot as a minion. The robot is a simple build with 4 tyres arranged in a + manner to allow the robot to move in all directions and also spin. It also has two hands which move up and down to depict the human hand movement. On movement of the robot the hands also move depending on the direction e.g when moving left the left hand rises. While creating the robot I had a dilemma on how to control the robot. However, I got inspired by previous students work which used the handspose library from ml5.js to control movement using hands. I went on to implement this to control my robot.

Interaction Design
The interaction was using ml5.js hanspose library which picks the users hands position to control the movement of the robot. I divided the screen into 4 sections such that having a hand in each section sends a certain command to Arduino to control the robot; a hand in the top left section moves the robot forward, top right section moves back, bottom left section turns left and bottom right turns right, to stop the robot one has to put both hands on the screen or remove all hands from the screen.

Arduino Code

My Arduino code included commands to control every movement of the robot; the motors, servos and LED’s. It receives data from p5 and depending on the data it executes the command on the robot.

Arduino code on GitHub

p5 Code



My p5 code picks data from user to move the robot. Using the average position of the users hand key points, the code checks where the hands are positions and stores this in as a defined data text which is sent to Arduino. Through serial communication the Arduino is able to process commands on the robot based on what it gets from p5.

Circuit Schematic

For the project I used the Adafruit motor shield for my connections as I initially wanted to use 4 motors. This component turned out to be very beneficial as it reduced the amount of wires that I needed to use if I would have opted for the small motor shield. I also didn’t have to use a breadboard which helped to minimise the size of my robot. I used KiCad to generate the schematic

Project Reflection and Future Improvements

Working on this project was a worthwhile experience. I was a good opportunity to put into work what I have learnt in the course. I had a couple of challenges while coming up with it. I initially wanted to use 4 DC motors for each individual wheel however this had a lot of issues as the robot couldn’t move in some directions. After spending a lot of time trying to fix this I finally got a solution by having to use only two motors and two support wheels which enabled the robot to move better. I also had an issue with picking commands from user. My initial idea was to have the user swipe the hands across the canvas but this had errors because the handspose library was too sensitive which resulted in clashing commands being picked. The project still has space for improvement especially in terms of the user interaction. The user experience can be enhanced by having both hands to control the movement since many users found it challenging to use only one hand. Everyone naturally started using two hands even after reading the instructions which might mean that including a two hand control would be better.All in all, I am proud of the whole project and how I was able to implement it.

User Testing Videos

 

 

 

Final Project: User Testing + IM Showcase

Finalized Concept & Progress

As I began working with p5.js and Arduino, I decided to focus my project on four Armenian symbols: Mount Ararat, the pomegranate, the Armenian rug, and lavash. I removed the duduk (musical instrument) from the project, as recreating it would have taken too much time, and the sounds I could input wouldn’t be similar enough to the actual instrument. Therefore, I chose to focus on these four interactions:

  • Lavash Baking Session:
    This interaction references Armenian flatbread (lavash) and imitates its baking process in a traditional clay oven (tonir). Users can cover the 3D-printed oven with both hands to place the lavash inside. When they release their hands, the lavash reappears on the p5 canvas. This interaction is based on the real-world operation of a tonir, which is typically placed in the ground to bake the lavash beneath the surface. Serial communication includes an LDR sensor from the Arduino side, which sends a signal to p5 to manipulate the lavash’s position inside the oven. An LED sensor also indicates when the lavash is being baked.

  • Breathing in Ararat:

    This interaction centers on Mount Ararat, the iconic peak visible from much of Armenia. As many Armenians dream of seeing Ararat through the clouds, this interaction lets users blow away the clouds to reveal the mountain. Users are encouraged to blow into the 3D-printed mountain, and with enough force, the clouds on the p5 canvas will move away, revealing the peak. I used a DHT22 sensor to detect humidity levels, which are sent to p5 to control the movement of the clouds.

 

  • Coloring the Pomegranate: 

    This interaction draws inspiration from Sergei Parajanov’s iconic film The Color of Pomegranates. To color the pomegranate, users place their finger on the fruit, where a pulse sensor detects their heartbeat. The pomegranate changes color based on the sensor’s readings: lower heart rates result in blue hues, while higher rates produce red tones. Serial communication reads the pulse sensor’s value on the Arduino side and displays it on the p5 canvas, adjusting the pomegranate’s color accordingly.

  • Crafting the Rug: 

    This interaction is entirely based in p5.js. Users can see themselves integrated into the pattern of an Armenian rug, typically displayed on walls for its aesthetic value. I used the following p5.js code as a reference to capture the pixelated camera feed, and by adjusting the RGB values of four main colors (blue, black, red, and beige), I created an image that blends with the rug pattern.

Arduino + p5

Arduino Code on GitHub

p5.js Code:

Reflection & Areas for Improvement

This project challenged me to work with various types of sensors. I encountered several issues with sensors I had never used before, such as the pulse sensor. Initially, it displayed three-digit values that bore no relation to an actual heartbeat. I eventually discovered that the sensor itself is not very reliable, so I implemented constraints to ensure the displayed values resembled a realistic heartbeat. Fortunately, this approach seemed to work during the IM showcase, as users received values around 90-100. Additionally, I had to constantly calibrate the LDR and DHT22 sensor values to suit the specific environment of the showcase.

I believe the visual aspects of the project could be further refined as well. More than that, the interactions could be recorded in p5.js, allowing users to compare how quickly they could “blow the clouds” or “bake the lavash.” This would introduce an element of competition and encourage users to revisit the project repeatedly.

Overall, I am proud of what I accomplished, particularly the variety of interactions I managed to implement within a constrained timeframe. I learned a great deal from this experience and thoroughly enjoyed engaging with users during the IM showcase – hearing their feedback about their favorite interactions was especially rewarding.

Week 14: Final Project Documentation

NYUAD Puzzle Adventure

My final project was inspired by the beauty of the NYU Abu Dhabi campus. I designed a simple game that involved beautiful images of the campus some taken by myself and some from online sources. Initially the game begins with images in 4 square pieces and a player can move and arrange the pieces to make the image whole. The control can be done entirely through a simple controller that I designed. The game keeps track of time in each chapter as the player solves a specific image and the game intentionally designed to loop, Increasing in level of difficulty every time. The level of difficulty increases by increasing need for precision and increasing number of image pieces.

Video Demonstration 

Demonstration 01: Mr. Godbless Osei

Demonstration 02: Mr Aimable Tuyisenge

Demonstration 03: Screen Recording

Implementation

Interaction

Interaction being key in any game design, NYUAD puzzle adventure integrates interaction through a custom controller that is used to move and arrange the image pieces. The controller can be seen below:

Given my game design, I wanted to make a simple controller, So I used four Switches and a Joystick .  I used the switches to change between game states in my game and the joystick to control movement of pieces. The electrical connections the controller can be seen  on a schematic below:

In addition to interaction through the hardware design, I also used sounds that significantly provided cues. One such was a sound that notifies player that two pieces are already attached together. And a celebration rewarding player when an image is completed.

Arduino code

My Arduino code, which was was the heart of the controller involved the following :

1. Reading data from the buttons and the joystick

2. Sending data to P5.js via serial connection.

3. Receiving data from P5.js for the speakers in the controller.

My Arduino code can be see here:

// ------------Buttons pins-------------------------------
int changePin = 3;       
int playPin = 5;   
int instructionsPin = 2;   
int nextPin = 11;   
// ------------Joystick X and Y pins-----------------------
const int xPin = A2; 
const int yPin = A1; 
// ------------Speaker Pin---------------------------------
const int speaker1Pin = 1;
const int speaker2Pin = 4;
// ------------Control Speaker------------------------------
int playOrNot = 0;
const int play1 = 440;
const int play2 = 39;

int xValue = 0;
int yValue = 0;

void setup() 
{
  pinMode(changePin, INPUT);        
  pinMode(instructionsPin, INPUT); 
  pinMode(playPin, INPUT);      
  pinMode(nextPin, INPUT); 
  pinMode(speaker1Pin, OUTPUT);
  pinMode(speaker2Pin, OUTPUT);  
  Serial.begin(9600);           
}

void loop() 
{
  xValue = analogRead(xPin); 
  yValue = analogRead(yPin); 
  int changeState = digitalRead(changePin);
  int playState = digitalRead(playPin);
  int instructionsState = digitalRead(instructionsPin);
  int nextState = digitalRead(nextPin);

  // Serial output in order -> Instructions, Next, Change, Play, X, Y
  Serial.print(instructionsState == HIGH ? "HIGH," : "LOW,");
  Serial.print(nextState == HIGH ? "HIGH," : "LOW,");
  Serial.print(changeState == HIGH ? "HIGH," : "LOW,");
  Serial.print(playState == HIGH ? "HIGH," : "LOW,");
  Serial.print(xValue);
  Serial.print(",");
  Serial.println(yValue);

  if (Serial.available()) 
  {
    playOrNot = Serial.parseInt();
  }
  if (playOrNot == 1) 
  {
    tone(speaker1Pin, play1); 
  } 
  else if (playOrNot == 2)
  {
    tone(speaker2Pin, play2); 
  }
  else
  {
    noTone(speaker1Pin);
    noTone(speaker2Pin);
  }
}

P5.js sketch

My P5.js sketch manages  multiple states such as “WELCOME,” “INSTRUCTIONS,” “PLAY” and uses a serial connection for controller inputs.  Some key features includes

1. Welcome screen: Displays an introductory screen and a short game summary.

2. Controller Integration: Reads serial data for buttons and joystick inputs to navigate states and interact with puzzle pieces.

3. Dynamic Screens and Feedback: Provides visual and audio feedback, including fading hints, a timer, and celebration effects when puzzles are solved.

4. State Management: Transition between screens based on button presses and game progress.

In my code, I also integrated keyboard controls to facilitate debugging.

The P5j.s Sketch can be seen here: -> Fullscreen

Communication 

Data is sent between Arduino and p5.js through a serial connection. Arduino sends data as a string, in a particular order e.g  playButton, value1, value2,…. p5.js listens for this data, splits it into individual pieces, and uses those pieces to control the game states and actions, such as pressing buttons or moving the joystick.

Things I am Proud of

I am proud with how the whole game has turned out. However, some things that really stood out to me are as follows:

1. The controller design:

Designing this controller has helped me understand how complex controllers like those for the Xbox or PS5 work. I could envision my design integrated with many other elements to and become just as complex  as those in market today.

2. The graphics

The graphics used were quite simple, but very appealing in my opinion.

Reflections for Future

Several areas could be improved in this project. Some of them includes:

1. Using different shapes, like jigsaw patterns, instead of simple rectangles. While I initially wanted to use such complex shapes and pattens for my pieces, I failed due to complexity in wrapping images, managing positioning and many other challenges.

2. The controller design could also be improved  by adding sensors like sensors such as touch sensors and adding  to make the controller more interactive. Adding vibration motors would give players a better feel during gameplay.

Week #13 – User Testing Final Project

Unfortunately, the videos from user testing were not saved on my phone.

But, here are the main insights that I’ve gained from user testing:

  • Due to presence of the physical product, all of the users kind of failed to pay attention to the P5 screen on the computer, while it was the component to start interaction. When looking at the project, users immediately wanted to touch buttons and expected some response.
    • The component that attracted the most attention on the physical project was the heart sensor that was placed in front of the table and in the closest reach to the user. This placement was the reason people tried to push the sensor, touch it and expected instant reaction from the physical project or the computer. But since it didn’t occur, they were kind of disappointed.
      • Since to have this interaction with heart sensor, user had to go through multiple levels to reach that level, I had to explain them this information manually. This led me to thinking that maybe having levels was not great decision to build interactive experience with users, since they were not intuitively instructed by the physical project that they will have to go through different levels, therefore they were confused.
        • I also thought that next time, just the interaction with either of the components, such as buttons and sensors can trigger the screen appearance that supports specific interactions. That way there will be no levels, but self-guided exploration of the project.
    • Also, users tried to interact with components that they were not supposed to interact with. For example, users tried to push dishes on the table as if they thought they were buttons. The confusion was supported by the level screen on p5, which encouraged users to explore dishes and the table. In reality, users just needed to press the arrows to go through different dishes. Users also couldn’t understand where to press to switch the dishes,  some pressed on the dishes on the screen or anywhere but not arrows

      • This led me to the idea that I should make more intuitive arrows, so users can easily and directly press on them.
    • Another thing I learned from user testing was that after completing different levels, users didn’t have any clue that the experience has ended. There was just a page with a last level. Therefore, I decided to have a finish page that thanked the user.
      • After implementing it and conducting another user testing, I found out that user needs to refresh page every time he want to go through the project again. Therefore, there should be a refresh button to go to the main page again.
    • Some of the users were also inquiring if there was any interaction with LED chandelier on the top. They tried to touch it or do anything with it, but failed. So, I had to explain that it was just the lighting.
    • I also noticed that the text size for many users was quite small, so they had to come closer and bow to look into the text on the screen, which kind of inconvenient for them.
    • The great confusion was brought by the pulse sensor. When the user reached level 4, he had to place his finger on the pulse sensor and press on the heart. The user expected something big and radical to happen, but in my vision I used the pulse sensor for it’s symbolic and sentimental meaning of listening to your heart, so the pictures of the family appeared. But, the users couldn’t believe that it was the only thing that needed to happen.

 

Final Project – Melody Masters

Concept

Melody Masters is a fun and interactive project that helps people explore and learn about music. It uses Arduino to let users interact with physical buttons  and p5.js to show visual and audio feedback on a screen. By pressing buttons, users can play sounds and see how melodies are made. The idea is to make learning music simple and enjoyable for everyone, whether they are just starting or love experimenting with music. Melody Masters makes learning about music feel like a fun and creative experience. Melody Masters challenges the user by collecting as much points as they can in a minute which helps them with coordinating the location of the note with the note itself. The game has a tutorial that explains where each letter is located so that the user can learn where each note is located. There is also an option for free play where the user can play whatever music they want.

How does it work?

The interaction design of Melody Masters is simple and engaging. Users interact with physical buttons connected to an Arduino board. Each button corresponds to a different musical note, and pressing a button triggers a sound, and lights up the button, the sound is also visualized through animations on the screen using p5.js. The design focuses on making the experience intuitive, so users of all ages can easily play and create melodies. The interaction bridges the gap between touch, sound, and visuals, making it a multisensory experience.

The Arduino code is written to detect button presses and send corresponding signals to the computer via serial communication. Each button is assigned a unique musical note, and when pressed, the Arduino sends a specific code to indicate the note. The code also includes debouncing logic to ensure smooth and reliable input.

Arduino Code

The p5.js code takes input from the Arduino through serial communication and creates visual and audio feedback. The P5 code has the tutorial, game and free play logic. Each logic is different than the other and each component gives the user a different experience.

The Arduino and p5.js communicate via serial communication. When a button is pressed on the Arduino, it sends a unique character (like ‘C’ or ‘D’) over the serial port. The p5.js sketch reads this data, processes it, and triggers the appropriate sound and visual response. The communication ensures a real-time connection, creating an immediate response to user interactions, which makes the experience dynamic and engaging.

What I am proud of?
The way physical button presses translate into both sound and visuals feels intuitive and satisfying. It’s rewarding to see how users can engage with music in such an accessible way.

What can I improve?

What I think I want to improve is that when the audio visualization is being drawn, I can change the colors of the visualization depending on what note is being played for example if it was an E it will become yellow and so on. I want to include this so that the aesthetics of the game matches the aesthetics of the board more.

IM Showcase Interactions

Final Project Documentation

For my final project, it is an extension of my midterm project onto a physical scale. The project is a game where two players have to work cooperatively to “dance” to input the right sequence to defeat the orbs that appear on the screen moving closer to them. In my final project I initially wanted to make a dance pad which user can control using their feet, but after evaluating the amount of material and time left to complete the project, I decided to scale down my project scope. Additionally, I decided to make a game box which players can press as in an arcade rather than using the button on my laptop.

Schematic

Serial Communication

For my project, the serial communication was mostly reliant on the signal from Arduino rather than p5. Since I didn’t attach any output device (LED, buzzer) on the Arduino, I didn’t need p5 to send anything back to Arduino. On the other hand, Arduino would continuously check if any of the buttons that I had made were pressed and if they were pressed, send a send to p5 so the characters moved, or the game state changed. In the future, I would like to add a buzzer to indicate a life loss or when the player advances to the next round to provide game feedback to the player.

p5 Code (Serial Communication)
function readSerial(data) {
  // Send control values (left, right) to Arduino
  if (data && data.length > 6){
    let fromArduino = split(trim(data), ",");
    redUP_arrowPressed = int(fromArduino[0]);
    redDOWN_arrowPressed = int(fromArduino[1]);
    redRIGHT_arrowPressed = int(fromArduino[2]);
    redLEFT_arrowPressed = int(fromArduino[3]); 
    
    blueUP_arrowPressed = int(fromArduino[4]);
    blueDOWN_arrowPressed = int(fromArduino[5]);
    blueRIGHT_arrowPressed = int(fromArduino[6]);
    blueLEFT_arrowPressed = int(fromArduino[7]); 
    
    proceedButton = int(fromArduino[8]);
    yesButton = int(fromArduino[9]);
    noButton = int(fromArduino[10]);
  }
}
Arduino Serial Communication
void loop() {
  // Read the state of the UP_BUTTON (HIGH = not pressed, LOW = pressed)
  int redUPButtonState = digitalRead(redUP_BUTTON); 
  int redDOWNButtonState = digitalRead(redDOWN_BUTTON); 
  int redRIGHTButtonState = digitalRead(redRIGHT_BUTTON); 
  int redLEFTButtonState = digitalRead(redLEFT_BUTTON); 

  int blueUPButtonState = digitalRead(blueUP_BUTTON); 
  int blueDOWNButtonState = digitalRead(blueDOWN_BUTTON); 
  int blueRIGHTButtonState = digitalRead(blueRIGHT_BUTTON); 
  int blueLEFTButtonState = digitalRead(blueLEFT_BUTTON); 

  int proceedButtonState = digitalRead(proceedButton); 
  int yesButtonState = digitalRead(yesButton);
  int noButtonState = digitalRead(noButton); 

  // Print the value (HIGH or LOW) to the Serial Monitor
  Serial.print(redUPButtonState);
  Serial.print(",");
  Serial.print(redDOWNButtonState);
  Serial.print(",");
  Serial.print(redRIGHTButtonState);
  Serial.print(",");
  Serial.print(redLEFTButtonState);

  Serial.print(",");
  Serial.print(blueUPButtonState);
  Serial.print(",");
  Serial.print(blueDOWNButtonState);
  Serial.print(",");
  Serial.print(blueRIGHTButtonState);
  Serial.print(",");
  Serial.print(blueLEFTButtonState);

  Serial.print(",");
  Serial.print(proceedButtonState);
  Serial.print(",");
  Serial.print(yesButtonState);
  Serial.print(",");
  Serial.println(noButtonState);

}
Integration of Serial Communication into P5

Since my p5 were originally mapped to button on the keyboard, all I had to do was replace the variables mapped to the keyboard to the Arduino through the serial communication. Although that sounds easy enough to change, when I was implementing the buttons to change the game states, I ran into a lot of problems with the communication because I had initialized my buttons with INPUT_PULLUP rather than simply INPUT. By initializing the button to INPUT_PULLUP, the button was more sensitive and would remain on an ON-state longer compared to initializing it to INPUT. As I wanted a quicker and smoother response with the buttons, I had to make the program wouldn’t move on to the next sequence when the ON-signal was still being processed between p5 and the Arduino. As such I did a hybrid catch statements to make sure the signal was in the OFF-state before it read another ON-state signal.

Controlling the ON and OFF state
if (!serialCheck) {
    isCheckSerial();
  }

  else {
      
    if (proceedButton == 1) {
      buttonReleased = true; 
    }
    
    if (!musicPlaying){
      playMusic();
      musicPlaying = true;
    }

    if (gameState == 'start' && buttonReleased){
      startScreen();
    }

    else if(gameState == 'friendship warning' && buttonReleased){
      showFriendShipWarning(); 
    }
.
.
.
Integration
function proceedButtonPressed(){
  console.log('gamestate', gameState, proceedButton)

  if (gameState == 'start' && proceedButton == 0){
    gameState = 'friendship warning';
    buttonReleased = false;
    
  }

  else if(gameState == 'friendship warning' && proceedButton == 0){
    gameState = 'tutorial';
    buttonReleased = false;

  }

As for the player controls, it was much easier to replace the mapping from the keyboard to the buttons and the ON-signal wasn’t as noticeable.

player1Moves() {
    fill(this.playerColor);
    if (redUP_arrowPressed == 0) {

// redUP_arrowPressed being an int variable that holds either 1 (OFF) or 0 (ON)
Reflection

Overall, I am really happy with the out of my project and amount of time I put into making sure the visuals and themes were consistent. I am really glad people enjoyed the little game I made, and I hope in the future I’ll get to work on it more and make it less buggy. For future implements, I want to make the orbs on the screen be actual creatures or monsters which the players will have to defeat. Additionally, I hope to highlight the combination which the players will have to press, so I don’t have to directly remind or tell the player to read from left to right. I also wish to build a smooth storyline/ streamline from the starting page to the game. Some people were telling me there were too many instructions to read.

On the physical side, I want to make my game pad less wobbly. Due to the wires under the game pad, when players were hitting the key, the input may not always be registered and made the experience a bit less enjoyable. I would also like to add more game feedback in terms of sound and LED lights that can help signal which orb was just defeat or when they move onto the next round. Since these were small aspects to the game, I didn’t focus on these tasks within the time constraint, but for future implementations, these are topics I can think about adding.

Project

Full Arduino Code
const int redUP_BUTTON = 12;
const int redDOWN_BUTTON = 11;
const int redRIGHT_BUTTON = 10;
const int redLEFT_BUTTON = 9;

const int blueUP_BUTTON = 7;
const int blueDOWN_BUTTON = 6;
const int blueRIGHT_BUTTON = 5;
const int blueLEFT_BUTTON = 4;

const int proceedButton = 13; 
const int yesButton = 3; 
const int noButton = 2; 

void setup() {
  // Start serial communication at 9600 baud
  Serial.begin(9600);

  // Set button pins as input with internal pull-up resistors
  pinMode(redUP_BUTTON, INPUT_PULLUP); 
  pinMode(redDOWN_BUTTON, INPUT_PULLUP);
  pinMode(redRIGHT_BUTTON, INPUT_PULLUP);
  pinMode(redLEFT_BUTTON, INPUT_PULLUP); 

  pinMode(blueUP_BUTTON, INPUT_PULLUP); 
  pinMode(blueDOWN_BUTTON, INPUT_PULLUP);
  pinMode(blueRIGHT_BUTTON, INPUT_PULLUP);
  pinMode(blueLEFT_BUTTON, INPUT_PULLUP); 

  pinMode(proceedButton, INPUT_PULLUP);
  pinMode(yesButton, INPUT_PULLUP);
  pinMode(noButton, INPUT_PULLUP);
}
void loop() {
  // Read the state of the UP_BUTTON (HIGH = not pressed, LOW = pressed)
  int redUPButtonState = digitalRead(redUP_BUTTON); 
  int redDOWNButtonState = digitalRead(redDOWN_BUTTON); 
  int redRIGHTButtonState = digitalRead(redRIGHT_BUTTON); 
  int redLEFTButtonState = digitalRead(redLEFT_BUTTON); 

  int blueUPButtonState = digitalRead(blueUP_BUTTON); 
  int blueDOWNButtonState = digitalRead(blueDOWN_BUTTON); 
  int blueRIGHTButtonState = digitalRead(blueRIGHT_BUTTON); 
  int blueLEFTButtonState = digitalRead(blueLEFT_BUTTON); 

  int proceedButtonState = digitalRead(proceedButton); 
  int yesButtonState = digitalRead(yesButton);
  int noButtonState = digitalRead(noButton); 

  // Print the value (HIGH or LOW) to the Serial Monitor
  Serial.print(redUPButtonState);
  Serial.print(",");
  Serial.print(redDOWNButtonState);
  Serial.print(",");
  Serial.print(redRIGHTButtonState);
  Serial.print(",");
  Serial.print(redLEFTButtonState);

  Serial.print(",");
  Serial.print(blueUPButtonState);
  Serial.print(",");
  Serial.print(blueDOWNButtonState);
  Serial.print(",");
  Serial.print(blueRIGHTButtonState);
  Serial.print(",");
  Serial.print(blueLEFTButtonState);

  Serial.print(",");
  Serial.print(proceedButtonState);
  Serial.print(",");
  Serial.print(yesButtonState);
  Serial.print(",");
  Serial.println(noButtonState);

}
Link to P5: https://editor.p5js.org/yamdn/sketches/1TavUEyVF

Final Project: Formosa!

Concept

The game is inspired by the history of Taiwanese rock, which developed not as a direct product of American cultural imperialism but as a manifestation of our people’s desire to preserve our cultural identity through these modern forms of music; many Taiwanese rock bands write their lyrics in Taiwanese Hokkien and are very involved in social movements outside of their music. These are elements of Taiwanese culture that were suppressed during the martial law era, and the vibrant pop punk/punk rock scene embodies the resilience of our people. Taiwanese rock is an amalgamation of local Taiwanese, Chinese, American, and Japanese influences that reflects our complex history as a colonial subject of many but also our present and future as a country.

The game’s title Formosa! refers to the name “Ilha Formosa,” which was given to Taiwan by Portuguese sailors in the 16th century, meaning “beautiful island.” The song that plays throughout the game is “人間條件 Human Condition” by Fire EX., a band that I consider a symbol of Taiwanese resilience due to their outspoken support of Taiwanese independence and other causes.

I intend for the game to be a light-hearted introduction to this aspect of Taiwan. In the spirit of the wacky, energetic aesthetic of the Scott Pilgrim franchise, the player controls a guitarist who is running late for a gig and has to get there as soon as possible, evading cops and picking up pirated music tapes to gain experience and recharge. My final project is a game centered around a guitar repurposed as a gamepad: each string, when strummed, triggers a specific movement for the protagonist.

How the game works

  • 3 of the guitar’s frets are covered with red strips. These are buttons that players press to either start, jump or attack.
  • Players will have to jump to collect power-ups in the form of vinyls and cassette tapes in order to build up an attack charge (red bar on the top right corner of the screen). The maximum charge value is three.
  • When the attack charge is greater than zero, players can attack to destroy enemies (cops and cop cars). Cops take one attack to destroy while cars take two.
  • When an enemy is destroyed, the score goes up. Contact with enemies will cause the score to go down.
  • Each game lasts 60 seconds. The game automatically ends when the countdown timer reaches zero.

Arduino + p5.js Setup

When players press on a “button” (the red strips on the guitar neck), a circuit is closed between the metal guitar fret and its strings, triggering in-game actions such as jumping and attacking depending on which button is pressed.

The Arduino code simply takes input from the digital pins connected to the three guitar frets; the p5.js code takes this input (0 or 1 depending on whether the circuits are closed or not) and translates it into triggers for character actions.

Arduino code on Github 

The p5.js code has a separate function for each game state — start, instructions, main game and end screen — that is called on in an if-else statement in the main draw function.

The properties of characters and power-ups are set in their individual classes. The hit(entity) function in the Protag class set conditions that are then used to check if the Protag has come into contact with enemies or power-up collectibles in the handleCollisions() and handleAttacks() functions of the main sketch.

Problems Encountered + Solutions

Personally, I’m proud of managing to successfully set up the serial connection between p5.js and Arduino. For a good amount of time, the connection wasn’t working, and I was panicking until I realized that p5.js wasn’t responding as it should be because in all the “if” statements for each in-game action, I put the button state “1” as a string instead of an integer, as they were specified as in the readSerial() function. After simply removing the quotation marks, the serial connection was up and running successfully.

As for the hardware, I also had to figure out new solutions after initial configurations didn’t work as I expected. While i had originally planned to attach 4 wires to individual guitar strings and use a wired conductive guitar pick to create a controller in which strumming each string triggers a different in-game action, this failed to work as all the strings were connected to a metal saddle, which made all the strings one conductive body and therefore could not be utilized for individual actions. I ultimately decided to turn the guitar’s metal frets into the buttons of the controller. I covered three separate frets with copper tape for enhanced visibility and connected them to wires, doing the same for the metal saddle; when the player presses any string against the taped frets, a circuit is completed and a signal is sent from arduino to p5js to trigger 3 actions — start, jump, and attack — depending on which fret is pressed.

Showcase

Future Improvements

The game interface could definitely use a bit for polishing; if I had more time, I’d want to try making more advanced visual and sound effects for when an enemy is attacked or when the protagonist successfully collects a power-up so that players don’t have to keep checking their score and attack charge bar to keep track. It would also be interesting if I could somehow incorporate haptic feedback on the guitar controller– perhaps have the strings vibrate when an attack is used — so that the experience is more immersive and engaging.

FINAL PROJECT – USER TESTING AND FINAL TESTING


Project Documentation

Project Title:

Traffic Light Game


Project Idea:

The original idea was to use LED lights connected to an Arduino to simulate a traffic light. The lights would signal when a car (controlled by the user) should stop or move. However, I transitioned to using p5.js to create a digital simulation of the traffic light and car movement, while the Arduino controlled the car’s behavior through a physical button.


Concept:

The project combines physical hardware (Arduino) and digital visualization (p5.js) to simulate real-world traffic rules:

  • Traffic Lights: Designed in p5.js to change states (green, yellow, red) at timed intervals.
  • Car Movement: Controlled through an Arduino-connected button. Pressing the button sends “MOVE” signals to the p5.js interface, while releasing it sends “STOP.”
  • Strike System: Violations occur when the car moves during a red light or fails to move during green light. A buzzer (Arduino) provides audible feedback for violations.

Step-by-Step Arduino Connection

Components Required:

  • Arduino Uno
  • Breadboard
  • 1 Push Button (Switch)
  • 1 10kΩ Resistor
  • 1 Buzzer
  • Jumper Wires (Male-to-Male)

Step-by-Step Connection:

  1. Connect the Switch:
    • Place the push button (switch) on the breadboard, bridging the middle gap.
    • Connect one leg of the switch to Digital Pin 2 on the Arduino.
    • Connect the same leg of the switch to 5V on the Arduino.
  2. Connect the Resistor:
    • Attach a 10kΩ resistor from the other leg of the switch to the GND rail on the breadboard.
    • This acts as a pull-down resistor, ensuring the switch reads LOW when not pressed.
  3. Connect the Buzzer:
    • Place the buzzer on the breadboard.
    • Connect the positive leg of the buzzer to Digital Pin 8 on the Arduino.
    • Connect the negative leg of the buzzer to the GND rail on the breadboard.
  4. Power Connections:
    • Connect the 5V pin on the Arduino to the breadboard’s + rail.
    • Connect the GND pin on the Arduino to the breadboard’s – rail.

Arduino Connection

The diagram below shows the Arduino and Breadboard connections for the push button and buzzer:

https://drive.google.com/file/d/198DnUSxek9c-3ebID0bJrLljIN0VNa_O/view?usp=drive_link


Code Implementation

1. p5.js Code

The p5.js code handles the simulation of traffic lights, car movement, and strike detection. It also integrates with the Arduino using serial communication to receive “MOVE” and “STOP” signals and send feedback for violations (buzzer activation).

/*
 Course: Introduction to interactive media
 Final Project
 Section: Mang-F2024
 Name: Bismark Buernortey Buer
 Title: Superman Saves
*/

// Declare global variables
let roadY = 0; // Vertical position of the road markings
let gameState = "INSTRUCTIONS"; // Tracks the current state of the game
let carImage, backgroundImage, startSound, gameSound, gameOverSound; // Assets: images and sounds
let gameOverImage, restartImage, quitImage, gameOverBg, startButtonImage; // UI images
let countdownSound; // Countdown sound effect
let carX, carY; // Car position coordinates
let lightState = "green"; // Current state of the traffic light
let strikes = 0; // Counter for traffic violations
let lightTimer = 0; // Timer to track light changes
let isMoving = false; // Boolean flag for car movement (controlled by Arduino)
let violationTimer = 0; // Timer to check violations

let countdown = 3; // Countdown value before the game starts
let countdownStartTime = 0; // Timer start time for countdown
let countdownActive = false; // Flag for countdown state

let serial; // Serial communication object for Arduino
let gracePeriodActive = false; // Flag for grace period after light change
let graceStartTime = 0; // Timer start for grace period

// Preload images and sounds before setup
function preload() {
  carImage = loadImage("car.png");
  backgroundImage = loadImage("background.jpg");
  gameOverImage = loadImage("gameover.png");
  restartImage = loadImage("restart.png");
  quitImage = loadImage("quit.png");
  gameOverBg = loadImage("gameover_bg.jpg");
  startButtonImage = loadImage("start.png");

  startSound = loadSound("start_sound.mp3");
  gameSound = loadSound("gameplay_sound.mp3");
  gameOverSound = loadSound("gameover_sound.mp3");
  countdownSound = loadSound("countdown_go.mp3");
}

// Initial setup for the game
function setup() {
  fullscreen(); // Set fullscreen mode
  createCanvas(windowWidth, windowHeight); // Create a canvas with full window size
  carX = width / 2; // Set car's horizontal position
  carY = height - 200; // Set car's vertical position

  // Initialize serial communication with Arduino
  serial = new p5.SerialPort();
  serial.open("/dev/tty.usbmodem1201"); // serial port
  serial.on("data", serialEvent); // Define event for incoming serial data

  startSound.loop(); // Play start sound on loop
}

// Resize canvas dynamically when the window size changes
function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}

// Main draw loop: controls the game state
function draw() {
  if (gameState === "INSTRUCTIONS") {
    showInstructions(); // Display instructions screen
  } else if (gameState === "COUNTDOWN") {
    showCountdown(); // Display countdown before game starts
  } else if (gameState === "PLAY") {
    playGame(); // Main game logic
  } else if (gameState === "END") {
    endGame(); // End game screen
  }
}

// Display instructions screen
function showInstructions() {
  background(backgroundImage); // Set background
  textAlign(CENTER);
  textSize(32);
  fill("black");
  text("🚦 Traffic Light Game 🚦", width / 2, height / 6); // Title
  textSize(24);
  text("Green: Move | Red: Stop | Yellow: Keep Moving", width / 2, height / 4);
  text("Press and hold the button to stop the car", width / 2, height / 3);
  text("React in time to avoid strikes!", width / 2, height / 2.75);

  image(startButtonImage, width / 2 - 100, height / 2, 200, 100); // Start button

  // Start game on button press
  if (mouseIsPressed && mouseX > width / 2 - 100 && mouseX < width / 2 + 100 && mouseY > height / 2 && mouseY < height / 2 + 100) {
    startSound.stop();
    countdownSound.play();
    countdownStartTime = millis(); // Start countdown timer
    countdownActive = true;
    gameState = "COUNTDOWN";
  }
}

// Show countdown before game starts
function showCountdown() {
  let currentTime = millis();
  let elapsed = Math.floor((currentTime - countdownStartTime) / 1000); // Time passed
  let flashColor = frameCount % 20 < 10 ? color(255, 0, 0) : color(255, 255, 0); // Flashing background effect

  background(flashColor);
  textAlign(CENTER);
  textSize(150);
  fill(255);

  if (elapsed <= 3) {
    countdown = 3 - elapsed;
    text(countdown, width / 2, height / 2); // Show countdown numbers
  } else {
    fill(0, 255, 0);
    text("GO!", width / 2, height / 2); // Show "GO!" when countdown ends

    if (countdownActive) {
      countdownActive = false;
      setTimeout(() => {
        gameSound.loop(); // Start gameplay sound
        gameState = "PLAY";
        lightTimer = millis(); // Start light timer
        violationTimer = millis(); // Start violation timer
        startGracePeriod();
      }, 1000);
    }
  }
}

// Main gameplay logic
function playGame() {
  background("SkyBlue");
  let currentTime = millis();

  updateTrafficLight(currentTime); // Update traffic light state
  updateRoad(); // Draw road
  drawCar(carX, carY); // Draw car
  drawTrafficLight(); // Draw traffic light

  if (isMoving && !gameSound.isPlaying()) gameSound.loop(); // Loop game sound when moving
  else if (!isMoving && gameSound.isPlaying()) gameSound.stop(); // Stop sound if not moving

  // Check for violations every 2 seconds
  if (currentTime - violationTimer >= 2000) {
    checkViolations();
    violationTimer = currentTime;
  }

  fill("black");
  textSize(24);
  text(`Strikes: ${strikes}`, 50, 50); // Display strikes

  // End game after 3 strikes
  if (strikes >= 3) {
    gameSound.stop();
    gameOverSound.play();
    gameState = "END";
  }
}

// Display game over screen
function endGame() {
  background(gameOverBg);
  image(gameOverImage, width / 2 - 150, height / 4, 300, 150);
  image(restartImage, width / 2 - 220, height / 2, 200, 100); // Restart button
  image(quitImage, width / 2 + 20, height / 2, 200, 100); // Quit button

  textAlign(CENTER);
  textSize(24);
  fill("black");
  text("Choose an option:", width / 2, height / 2 - 50);

  // Restart or quit game based on mouse position
  if (mouseIsPressed) {
    if (mouseX > width / 2 - 220 && mouseX < width / 2 - 20 && mouseY > height / 2 && mouseY < height / 2 + 100) {
      restartGame();
    }
    if (mouseX > width / 2 + 20 && mouseX < width / 2 + 220 && mouseY > height / 2 && mouseY < height / 2 + 100) {
      returnToStartPage();
    }
  }
}

// Function to restart the game
function restartGame() {
  gameState = "COUNTDOWN"; // Set game state to countdown
  strikes = 0; // Reset strikes
  lightState = "green"; // Reset traffic light to green
  lightTimer = millis(); // Reset light timer
  violationTimer = millis(); // Reset violation timer
  isMoving = false; // Stop the car movement
  gameOverSound.stop(); // Stop the game over sound
  countdownSound.play(); // Play countdown sound
  countdownStartTime = millis(); // Start countdown timer
  countdownActive = true; // Activate countdown
}

// Function to return to the start page
function returnToStartPage() {
  gameState = "INSTRUCTIONS"; // Return to the instructions screen
  strikes = 0; // Reset strikes
  isMoving = false; // Stop car movement
  lightState = "green"; // Reset traffic light to green
  lightTimer = millis(); // Reset light timer
  violationTimer = millis(); // Reset violation timer
  gameOverSound.stop(); // Stop the game over sound
  startSound.loop(); // Replay the start sound
}

// Function to update the traffic light based on time
function updateTrafficLight(currentTime) {
  if (lightState === "green" && currentTime - lightTimer > 15000) {
    lightState = "yellow"; // Change to yellow after 15 seconds
    lightTimer = millis(); // Reset timer
  } else if (lightState === "yellow" && currentTime - lightTimer > 5000) {
    lightState = "red"; // Change to red after 5 seconds
    lightTimer = millis(); // Reset timer
    startGracePeriod(); // Start grace period for violations
  } else if (lightState === "red" && currentTime - lightTimer > 8000) {
    lightState = "green"; // Change back to green after 8 seconds
    lightTimer = millis(); // Reset timer
    startGracePeriod(); // Start grace period for green light
  }
}

// Function to check for traffic light violations
function checkViolations() {
  let currentTime = millis();
  if (gracePeriodActive && currentTime - graceStartTime < 1000) return; // Skip checks during grace period

  // Add strikes for incorrect actions based on traffic light state
  if (lightState === "green" && !isMoving) addStrike("Didn't move during green!");
  else if (lightState === "red" && isMoving) addStrike("Moved during red!");
  else if (lightState === "yellow" && !isMoving) addStrike("Stopped during yellow!");
}

// Function to handle strikes and send feedback to Arduino
function addStrike(message) {
  strikes++; // Increment strikes count
  console.log(message); // Log the violation message
  serial.write("BUZZER\n"); // Send a buzzer signal to Arduino
}

// Function to draw the traffic light on the screen
function drawTrafficLight() {
  fill("black");
  rect(20, 20, 50, 150, 10); // Draw the traffic light box

  // Draw the red light
  fill(lightState === "red" ? "red" : "gray");
  ellipse(45, 50, 30, 30);

  // Draw the yellow light
  fill(lightState === "yellow" ? "yellow" : "gray");
  ellipse(45, 95, 30, 30);

  // Draw the green light
  fill(lightState === "green" ? "green" : "gray");
  ellipse(45, 140, 30, 30);
}

// Function to draw the car on the screen
function drawCar(x, y) {
  image(carImage, x - 50, y, 100, 150); // Draw the car image centered at (x, y)
}

// Function to update the road movement
function updateRoad() {
  let centerY = height / 2; // Center of the screen vertically
  let centerX = width / 2; // Center of the screen horizontally
  let roadUpOffset = 50; // Width of the road at the top
  let roadDownOffset = 150; // Width of the road at the bottom
  let markingLength = 40; // Length of road markings

  roadY += isMoving ? 5 : 0; // Move road markings downward if car is moving
  if (roadY > markingLength * 2) roadY = 0; // Reset markings position when off-screen

  // Draw the grass background
  noStroke();
  fill("lime");
  rect(0, centerY, width, centerY);

  // Draw the road as a trapezoid
  fill("gray");
  quad(centerX - roadUpOffset, centerY, centerX + roadUpOffset, centerY,
       centerX + roadDownOffset, height, centerX - roadDownOffset, height);

  // Draw dashed road markings
  stroke(255);
  strokeWeight(5);
  for (let i = centerY; i < height; i += markingLength * 2) {
    let y = i + roadY; // Adjust position with road movement
    line(centerX, y, centerX, y + markingLength);
  }
}

// Event function for receiving data from Arduino
function serialEvent() {
  let data = serial.readStringUntil("\n"); // Read serial data line by line
  if (data.trim() === "MOVE") isMoving = true; // Set car to moving if Arduino sends "MOVE"
  if (data.trim() === "STOP") isMoving = false; // Stop car if Arduino sends "STOP"
}

// Function to activate grace period after light changes
function startGracePeriod() {
  gracePeriodActive = true; // Activate grace period
  graceStartTime = millis(); // Set grace period start time
}






















 


2. Arduino Code

The Arduino code reads input from the physical button and sends “MOVE” or “STOP” signals to p5.js via serial communication. Additionally, it listens for the “BUZZER” command from p5.js and activates the buzzer for violations.

 

const int buttonPin = 2;       // Pin for the switch
const int buzzerPin = 8;       // Pin for the buzzer
int buttonState = HIGH;        // Current state of the switch
int lastState = HIGH;          // Last state of the switch
unsigned long lastDebounceTime = 0; // For debounce timing
const unsigned long debounceDelay = 50; // Debounce delay in milliseconds

void setup() {
  pinMode(buttonPin, INPUT_PULLUP); // Use internal pull-up resistor for the switch
  pinMode(buzzerPin, OUTPUT);       // Set buzzer pin as OUTPUT
  Serial.begin(9600);               // Start serial communication at 9600 baud
}

void loop() {
  int currentButtonState = digitalRead(buttonPin);

  // Debounce the button input
  if (currentButtonState != lastState) {
    lastDebounceTime = millis();
  }

  if ((millis() - lastDebounceTime) > debounceDelay) {
    if (currentButtonState != buttonState) {
      buttonState = currentButtonState;

      // Send "MOVE" or "STOP" signal based on button state
      if (buttonState == LOW) {
        Serial.println("MOVE"); // Button pressed
      } else {
        Serial.println("STOP"); // Button released
      }
    }
  }

  // Check for commands coming from p5.js
  if (Serial.available() > 0) {
    String command = Serial.readStringUntil('\n'); // Read command until newline
    command.trim(); // Remove unnecessary whitespace

    if (command == "BUZZER") {
      activateBuzzer(); // Activate buzzer when "BUZZER" command is received
    }
  }

  lastState = currentButtonState; // Update last state
}

// Function to activate the buzzer for 0.5 seconds
void activateBuzzer() {
  digitalWrite(buzzerPin, HIGH);
  delay(500); // Turn buzzer ON for 500 milliseconds
  digitalWrite(buzzerPin, LOW);
}

 


User Testing

Objective:
To test the game’s functionality, usability, and interaction between the Arduino hardware and p5.js interface.

Process:
I asked my roommate to play the game:

  1. He interacted with the physical button to control the car and watched how the car responded to the traffic lights on the screen.
  2. He initially found the yellow light behavior confusing but quickly understood the rules after a second try.

Feedback and Observations:

  • Challenge: Understanding the yellow light instructions.
    • Solution: Clarified instructions on the screen.
  • Positive Feedback: He found the buzzer feedback helpful for identifying violations.

Video:


Final Testing

Objective:
To ensure smooth operation of the system after incorporating feedback from user testing.

Testing Checklist:

  1. Traffic Light Changes: Tested timed transitions between green, yellow, and red lights.
  2. Car Movement: Verified car responded immediately to MOVE/STOP signals from Arduino.
  3. Strike System: Checked that violations were detected correctly, and strikes were incremented.
  4. Buzzer Activation: Ensured the buzzer activated for 0.5 seconds during violations.
  5. Game Over Screen: Verified that the game ended and displayed the correct options after 3 strikes.

Results:

  • All functionalities worked as expected without delays or glitches.
  • User feedback was implemented successfully, resulting in an improved experience.

Videos: 


Conclusion:

The project successfully integrates p5.js for visual simulation with Arduino hardware for user interaction. Combining digital visuals with physical inputs created an interactive and engaging experience that simulates real-world traffic rules.

This is the whole game:

Week 14: Final Game Day

Concept:

Growing up, I have heard a lot about the Royal Bengal Tiger of Bangladesh, which is our sigil for being brave and strong and they live in the Sundarban, which is the largest mangrove forest of the world.

In Bangladesh, we have myths related to Sundarban and its history of man-eating tigers and a spirit/deity, named “Bonbibi” who protects the human from the tigers. An amazing fact, I learned while searching for this project is that both Muslims and Hindus in those certain area, believe in Bonbibi which is very conflicting with the concept of Islam. Yet, she is thought to be the daughter of Prophet Abraham. I did not wanted to miss the chance to let people get introduced to “Sundarban” and the legendary myth.

Inspirations:

While working on this project, I listened to the song named “Bonobibi” by Coke Studio Bangladesh, which says the story of her and connects the life of Bangladeshi to a very deep level. Another book, I took as my inspiration, is Amitav Ghosh‘s “Hungry Tide. I am personally inspired and try to treasure these amazing parts of my culture close to my identity and personality. I am not sure, how much I was able to tell the story through my game project but it was a project, not only for IM course but also me  to navigate ways of spreading my culture to a diverse student body and faculties.

Project Interaction Archive:

Project Box:
Inner Circuit
Right Side View
Top View Of the Project Box

 

 

 

 

 

 

 

 

 

 

 

 

 

Player Interaction: IM Showcase 2024

INTERACTION DESIGN:

Arduino for Physical Interactions
The Arduino serves as the core hardware interface. It reads data from two ultrasonic sensors, a potentiometer, and a physical button. This data is sent to the p5.js sketch via serial communication which to manage gameplay.

The project is divided into two games. To start any of the game player needs to press a physical button. The physical button is connected to Arduino pin 07 which takes the input from the button and allows the p5.js through serial communication to start the game. Pressing the button changes the state (HIGH → LOW or LOW → HIGH) is how Arduino code listens to detect button presses. Using a button is a common practice to start any game, so I used it for my final project as well.

Ultrasonic sensors detect hand proximity for selecting words based on their color. Two sensors were connected to take output for either Yellow or for White Color. I have set the threshold value to 15, so anything that comes below 15cm of distance, the sonar sensors will count them and if the obstacle is detected on the right sensor, the score will go up. To decide which one is dedicated for Yellow and Which one is White, I mapped each ultrasonic sensor to a specific word color in the code. The sensor on the left is dedicated to detecting Yellow, while the one on the right is dedicated to White. This mapping is based on the hardware connection and the logic in the Arduino and p5.js code.

The Arduino identifies which sensor detects an obstacle by reading the distance from the sensors. If the left sensor’s reading goes below the threshold of 15 cm, it corresponds to Yellow, and if the right sensor detects proximity, it corresponds to White. The data is then sent to p5.js via serial communication, which matches the color of the detected word with the respective sensor input to determine if the interaction is correct.

For the second game of collecting flowers, the potentiometer allows players to control the basket horizontally to collect flowers. The potentiometer is connected to an analog pin on the Arduino, which reads its position as a voltage value ranging from 0 to 1023, but I saw in mine it was somewhere around 990 as max value. This raw input is then mapped to the screen width in p5.js, allowing the basket’s movement to correspond seamlessly with the player’s adjustments.  When the basket aligns with a falling flower, the game detects a collision, increasing the score.

 

arduino code:

 // Arduino Code for Sundarbans Challenge Game

// Define Button Pin
#define BUTTON_PIN 7

// Define Ultrasonic Sensor Pins
const int trigPinYellow = 9;
const int echoPinYellow = 10;
const int trigPinWhite = 11;
const int echoPinWhite = 12;

// Define Potentiometer Pin
const int potPin = A0;

// Variables to track button state
bool buttonPressed = false;

void setup() {
  Serial.begin(9600);
  pinMode(BUTTON_PIN, INPUT_PULLUP);

  // Initialize Ultrasonic Sensor Pins
  pinMode(trigPinYellow, OUTPUT);
  pinMode(echoPinYellow, INPUT);
  pinMode(trigPinWhite, OUTPUT);
  pinMode(echoPinWhite, INPUT);
}

void loop() {
  // Read Button State
  bool currentButtonState = digitalRead(BUTTON_PIN) == LOW; // Button pressed when LOW

  if (currentButtonState && !buttonPressed) {
    buttonPressed = true;
    Serial.println("button:pressed");
  } else if (!currentButtonState && buttonPressed) {
    buttonPressed = false;
    Serial.println("button:released");
  }

  // Read distances from both ultrasonic sensors
  long distanceYellow = readUltrasonicDistance(trigPinYellow, echoPinYellow);
  long distanceWhite = readUltrasonicDistance(trigPinWhite, echoPinWhite);

  // Read potentiometer value
  int potValue = analogRead(potPin); // 0 - 1023

  // Send data in "pot:<value>,ultra1:<value>,ultra2:<value>" format
  Serial.print("pot:");
  Serial.print(potValue);
  Serial.print(",ultra1:");
  Serial.print(distanceYellow);
  Serial.print(",ultra2:");
  Serial.println(distanceWhite);

  delay(100); // Update every 100ms
}

long readUltrasonicDistance(int trigPin, int echoPin) {
  // Clear the Trigger Pin
  digitalWrite(trigPin, LOW);
  delayMicroseconds(2);

  // Send a 10 microsecond HIGH pulse to Trigger
  digitalWrite(trigPin, HIGH);
  delayMicroseconds(10);
  digitalWrite(trigPin, LOW);

  // Read the Echo Pin and calculate the duration
  long duration = pulseIn(echoPin, HIGH, 30000); // Timeout after 30ms

  // Calculate distance in centimeters
  long distance = duration * 0.034 / 2;

  // Handle out-of-range measurements
  if (duration == 0) {
    distance = -1; // Indicate out of range
  }

  return distance;
}

Description of Code:

The button, connected to pin 7, uses an internal pull-up resistor to detect when it’s pressed, sending a signal (button:pressed) to the computer. Two ultrasonic sensors measure distance: one for yellow and one for white. These sensors send a pulse and listen for its echo to calculate how close a hand is, allowing players to interact with words based on their color.

The potentiometer, connected to A0, controls the basket in the flower-collecting game by adjusting its position smoothly. All data — button states, distances, and potentiometer readings — is sent to the computer via serial communication in a structured format given as a file from the professor and it is read as serial data in p5.js.

Schematic Diagram: Tinkercad

P5.js Sketch:

P5.js Code with description commented:
// sketch.js 

// ----------------------
// 1. Global Variable Declarations
// ----------------------
let page = "start"; // Initial page
let howToPlayButton, mythologyButton, connectButton, fullscreenButton, backButton; // Buttons
let timer = 45; // Total game time in seconds
let countdown = timer;
let timerStart = false;
let gameOver = false;
let gameResult = "";
let score = 0;
let getReadyBg;
let endgame;



let gameStartInitiated = false; // Flag for transitioning from gameStart to game

// Typewriter effect variables
let typewriterText = "Press the green button to start your adventure!";
let currentText = ""; 
let textIndex = 0;  
let typewriterSpeed = 50; 




// Word display variables
let words = [
  "The", "Sundarbans", "is", "home", "to", "the",
  "man-eater", "Bengal", "tiger.", "Local", "honey", 
  "hunters,", "the", "Mowallis,", "brave", "its", "dangers",
  "to", "collect", "honey,", "often", "facing", "tiger", 
  "encounters.", "Myths", "surround", "these", "tigers,", 
  "people", "can", "only", "stay", "safe", "if", "they",
  "remember", "Bonbibi,", "the", "spirit."
];


let currentWordIndex = 0;
let wordInterval = 8000; // first 5 words every 4s
let fastInterval = 6000; // subsequent words every 2s
let wordDisplay = "";
let wordColor = "white";

// Basket game variables
let basketX, basketY;
let flowers = [];
let vehicles = []; // Water particles for aesthetic effect in basket game


let textToDisplay = "The Sundarbans, is home to the man-eater Bengal tiger. Local honey hunters, the Mowallis, brave its dangers to collect honey, often facing tiger encounters. Myths surround these tigers, people can only stay safe if they remember Bonbibi, the spirit.";


// Background Text for mythology and gameStart
let backgroundText = "In the Sundarbans myth, Bonbibi, the guardian goddess of the forest, defends the local woodcutters and honey gatherers from the malevolent tiger god, Dakshin Rai. The story highlights a young boy, Dukhey, who becomes a victim of a pact between his greedy employer and Dakshin Rai, aiming to sacrifice him to the tiger god. Bonbibi intervenes, rescuing Dukhey and ensuring his safe return, emphasizing her role as a protector and mediator between the natural world and human interests. This myth underscores the intricate balance between exploitation and conservation of the Sundarbans' resources.";

// Variables for serial communication
let latestData = "waiting for data";

// Images
let startBg, nightBg, bonbibi;

// Typewriter interval ID
let typewriterIntervalID;

// Variable to store the setTimeout ID for word transitions
let wordTimeoutID;

// Debugging Mode
let debugMode = false; // Set to false when using actual sensors

// Feedback Variables
let feedbackMessage = "";
let feedbackColor;
let feedbackTimeout;
let wordTouched = false;

// ----------------------
// 2. Function Declarations
// ----------------------

function setPage(newPage) {
  page = newPage;

  if (page === "start") {
    
    if (!bgMusic.isPlaying()) {
      bgMusic.loop(); // Loop the music
      bgMusic.setVolume(0.5); // Adjust volume (0.0 to 1.0)
    }
    
  
    connectButton.show();
    howToPlayButton.show();
    mythologyButton.show();
    fullscreenButton.show(); // Show fullscreen button on Start page
    backButton.hide(); // Hide Back button on Start page
    
  } else if (page === "howToPlay" || page === "mythology") {
    connectButton.hide();
    howToPlayButton.hide();
    mythologyButton.hide();
    fullscreenButton.hide(); // Hide fullscreen on these pages
    backButton.show(); // Show Back button on these pages
  } else if (page === "gameStart") { 
    console.log("Get Ready! Transitioning to game...");
    setTimeout(() => {
      setPage("game"); // Transition to game page after 15 seconds
    }, 18000);
    
    connectButton.hide();
    howToPlayButton.hide();
    mythologyButton.hide();
    fullscreenButton.hide();
    backButton.hide(); // hide Back button on new page
    
  }  else if (page === "secondGameIntro") {
    connectButton.hide();
    howToPlayButton.hide();
    mythologyButton.hide();
    fullscreenButton.hide();
    backButton.hide();
  } else {
    connectButton.hide();
    howToPlayButton.hide();
    mythologyButton.hide();
    fullscreenButton.hide();
    backButton.hide();
  }
}

function toggleFullscreen() {
  let fs = fullscreen();
  fullscreen(!fs);
  console.log(fs ? "Exited fullscreen mode." : "Entered fullscreen mode.");
}

function styleButtons(buttonColors = {}) {
  const defaults = {
    connect: '#855D08',
    howToPlay: '#B77607',
    mythology: '#8C5506',
    fullscreen: '#E1D8D8',
    back: '#555555'
  };

  const colors = { ...defaults, ...buttonColors };

  let buttonStyles = `
    color: rgb(255,255,255);
    font-size: 15px;
    border: none;
    border-radius: 5px;
    padding: 8px 20px;
    text-align: center;
    text-decoration: none;
    display: inline-block;
    margin: 5px;
    cursor: pointer;
    transition: all 0.3s ease;
    box-shadow: 0 4px 6px rgba(0,0,0,0.1);
  `;

  connectButton.style(buttonStyles + `background-color: ${colors.connect};`);
  howToPlayButton.style(buttonStyles + `background-color: ${colors.howToPlay};`);
  mythologyButton.style(buttonStyles + `background-color: ${colors.mythology};`);
  fullscreenButton.style(buttonStyles + `background-color: ${colors.fullscreen};`);
  backButton.style(buttonStyles + `background-color: ${colors.back};`);

  [connectButton, howToPlayButton, mythologyButton, fullscreenButton, backButton].forEach((btn, index) => {
    const baseColor = Object.values(colors)[index];
    btn.mouseOver(() => {
      btn.style('background-color', shadeColor(baseColor, -10));
      btn.style('transform', 'translateY(-2px)');
      btn.style('box-shadow', '0 6px 8px rgba(0,0,0,0.15)');
    });
    btn.mouseOut(() => {
      btn.style('background-color', baseColor);
      btn.style('transform', 'translateY(0px)');
      btn.style('box-shadow', '0 4px 6px rgba(0,0,0,0.1)');
    });
  });
}

function shadeColor(color, percent) {
  let f = parseInt(color.slice(1),16),t=percent<0?0:255,p=percent<0?percent*-1:percent,
  R=f>>16,G=(f>>8)&0x00FF,B=f&0x0000FF;
  return '#'+(0x1000000+(Math.round((t-R)*p)+R)*0x10000+(Math.round((t-G)*p)+G)*0x100+(Math.round((t-B)*p)+B)).toString(16).slice(1);
}


// All preload
//--------------
function preload() {
  startBg = loadImage('start.png', () => console.log("start.png loaded."), () => console.error("Failed to load start.png."));
  nightBg = loadImage('village.png', () => console.log("night.png loaded."), () => console.error("Failed to load night.png."));
  bonbibi = loadImage('bonbibi.png');
  endgame = loadImage('endgame.png');
  
    bgMusic = loadSound('start.mp3', () => console.log("Music loaded!"), () => console.error("Failed to load music."));

}

// setup
function setup() {
  createCanvas(windowWidth, windowHeight);
  textAlign(CENTER,CENTER);

  backButton = createButton('Back');
  backButton.position(width/2 - backButton.width/2, height-100);
  backButton.mousePressed(() => setPage("start"));
  backButton.hide();

  createButtons();
  styleButtons();
  setPage("start");

  feedbackColor = color(255);

  typewriterIntervalID = setInterval(() => {
    if(textIndex < typewriterText.length) {
      currentText += typewriterText[textIndex];
      textIndex++;
    }
  }, typewriterSpeed);
}

// draw
function draw() 

{
  if (page === "gameStart") {
    handleGameStartPage(); // Render the "Get Ready!" page
  }

{
  if(page === "start" && startBg && startBg.width > 0) {
    imageMode(CORNER);
    image(startBg,0,0,width,height);
  } else if((page === "gameStart"||page==="game"||page==="won"||page==="lost"||page==="secondGameIntro"||page==="basketGame") && nightBg && nightBg.width>0) {
    imageMode(CORNER);
    image(nightBg,0,0,width,height);
  } else {
    background(30);
  }

  switch(page) {
    case "start":
      handleStartPage();
      break;
    case "gameStart":
      handleGameStartPage();
      break;
    case "game":
      handleGamePage();
      break;
    case "basketGame":
      handleBasketGame();
      break;
    case "howToPlay":
      handleHowToPlay();
      break;
    case "mythology":
      drawMythologyPage();
      break;
    case "secondGameIntro":
      handleSecondGameIntro();
      break;
    case "won":
    case "lost":
      handleEndPage();
      break;
    default:
      break;
  }

  if(feedbackMessage!==""){
    push();
    let boxWidth=width*0.3;
    let boxHeight=50;
    let boxX=width/2 - boxWidth/2;
    let boxY=height-80;

    fill('#F2D9A4');
    noStroke();
    rect(boxX,boxY,boxWidth,boxHeight,10);

    fill(feedbackColor);
    textSize(24);
    text(feedbackMessage,width/2,boxY+boxHeight/2);
    pop();
  }
}
}

//------------
// createButtons

function createButtons() {
  connectButton = createButton('Connect to Serial');
  connectButton.position(width/2 - connectButton.width/2, height/2 -220);
  connectButton.mousePressed(()=>{
    setUpSerial();
    connectButton.hide();
    console.log("Serial connection initiated.");
  });

  howToPlayButton = createButton('How to Play');
  howToPlayButton.position(width/2 - howToPlayButton.width/2, height/2 +205);
  howToPlayButton.mousePressed(()=>{
    setPage("howToPlay");
    console.log("Navigated to How to Play page.");
  });

  mythologyButton = createButton('Read the Mythology');
  mythologyButton.position(width/2 - mythologyButton.width/2, height/2 +255);
  mythologyButton.mousePressed(()=>{
    setPage("mythology");
    console.log("Navigated to Mythology page.");
  });

  fullscreenButton = createButton('Fullscreen');
  fullscreenButton.mousePressed(toggleFullscreen);

  positionButtons();

  if(page==="start"){
    connectButton.show();
    howToPlayButton.show();
    mythologyButton.show();
    fullscreenButton.show();
  } else {
    connectButton.hide();
    howToPlayButton.hide();
    mythologyButton.hide();
    fullscreenButton.hide();
  }
}

// positionButtons
function positionButtons(){
  let centerX=width/2;
  connectButton.position(centerX - connectButton.width/2, height/2 -220);
  howToPlayButton.position(centerX - howToPlayButton.width/2, height/2 +225);
  mythologyButton.position(centerX - mythologyButton.width/2, height/2 +265);
  fullscreenButton.position(centerX - fullscreenButton.width/2, height/2 +305);
}


function setupBasketGameElements(){
  basketY=height-50;
  basketX=width/2;
  score=0;
  countdown=timer;
  gameOver=false;

  flowers=[];
  vehicles=[];

  for(let x=0;x<=width;x+=10){
    for(let y=-200;y<height;y+=10){
      let v=new Vehicle(x,y);
      vehicles.push(v);
    }
  }

  noCursor();
}

// Page Handling Functions
function handleStartPage(){
  fill(255);
  textSize(48);
  stroke(0);
  strokeWeight(2);
  text("Myths Of Sundarban", width/2, height/13);

  noStroke();
  drawInstructionBox(currentText);
}

function handleGameStartPage() {
  background(0); // Dark background
  fill('#F7E4BA');
  textSize(36);
  stroke(0);
  strokeWeight(2);
  text("Get Ready To Play Using One Hand!", width / 2, height / 2 - 50);

  fill(255);
  textSize(24);
  text("Words will appear in yellow or white.\n", width / 2, height / 2 + 50);
  
  fill(255);
  textSize(24);
  text("You have two ultrasound sensor: Yellow and White", width / 2, height / 2 + 100);
  
    fill(255);
  textSize(24);
  text("If the word is yellow, place your hand in front of the yellow sensor.\n"+
    "If the word is white, place your hand in front of the white sensor.\n"+
    "Respond Quickly and Accurately to score points.", width / 2, height / 2 + 195);
  
  
  fill(255);
  textSize(24);
  text("IF YOU SCORE 20, YOU WILL JUMP TO THE NEXT LEVEL!.", width / 2, height / 2 + 270);
}


function handleGamePage(){
  displayTimerAndScore();
  displaySensorData();

  if (wordDisplay !== "") {
  push();
  textAlign(CENTER, CENTER);
  textSize(64);

  // Choose a background color that contrasts well with yellow and white text
  let backgroundColor = wordColor === "yellow" ? color(0, 0, 0, 100) : color(0, 0, 0, 150); // Semi-transparent dark background
  
  // Border color to match the glowing effect
  let borderColor = wordColor === "yellow" ? color(255, 204, 0) : color(255); // Yellow border for yellow text, white border for white text
  
  // Calculate the width of the text to adjust the background size
  let textWidthValue = textWidth(wordDisplay);
  
  // Draw background rectangle with a border
  fill(backgroundColor);
  noStroke();
  let padding = 20; // Padding around the text
  rectMode(CENTER);
  rect(width / 2, height / 2, textWidthValue + padding, 80); // Adjust rectangle width based on text width
  
  // Draw the border around the background
  stroke(borderColor);
  strokeWeight(6);
  noFill();
  rect(width / 2, height / 2, textWidthValue + padding, 80); // Same size as the background
  
  // Draw the glowing text on top
  fill(wordColor === "yellow" ? color(255, 255, 0) : color(255)); // Glow color for text
  strokeWeight(4);
  stroke(wordColor === "yellow" ? color(255, 255, 0) : color(255)); // Glow color for text
  text(wordDisplay, width / 2, height / 2);
  pop();
}

}

function handleBasketGame(){
 
  background("#094F6D");
  
  noCursor();

  displayTimerAndScore();
  displaySensorData();

  for(let i=0;i<vehicles.length;i++){
    vehicles[i].update();
    vehicles[i].show();
  }

  for(let i=flowers.length-1;i>=0;i--){
    flowers[i].update();
    flowers[i].show();

    if(dist(flowers[i].pos.x,flowers[i].pos.y,basketX,basketY)<40){
      score++;
      flowers.splice(i,1);
    }

    if(flowers[i] && flowers[i].pos.y>height){
      flowers.splice(i,1);
    }
  }

  if(frameCount%30===0){
    let f=new Flower(random(50,width-50),-50,random(30,60),random(TWO_PI),floor(random(6)));
    flowers.push(f);
  }

  drawBasket();
  
 if (score >= 20 && !gameOver) {
    gameOver = true;
    gameResult = "Congratulations! You Win!";
    setPage("won"); // Transition to the winning page
    console.log("Player reached 20 flowers. Game won!");
    return; // Stop further game logic
  }
}

function handleEndPage() {
    background(50); // Set a dark background or whatever fits your game design
   image(endgame, 0, 0, width, height);
  
    textSize(48);
    textAlign(CENTER, CENTER);

    // Calculate rectangle and text positions
    let rectWidth = 800;
    let rectHeight = 100;
    let rectX = (width / 2);
    let rectY = (height / 2) - (rectHeight / 2);

    // Draw a background rectangle for the message
    fill(255, 204, 0); // Bright yellow color for both win and lose
    rect(rectX, rectY, rectWidth, rectHeight, 20); // Rounded corners with a radius of 20

    // Check the game result and set text and colors accordingly
    if (gameResult === "Congratulations! You Win!") {
        fill(0); // Black text for winning message
        text("Congratulations! You Win!", width / 2, height / 2-30);
    } else if (gameResult === "Time's Up! You Lose!") {
        fill(0); // Black text for losing message
        text("Time's Up! You Lose!", width / 2, height / 2-30);
    }

    // Additional UI elements
    textSize(24);
    fill(255); // White color for secondary text
    text("Press 'R' to Restart", width / 2, rectY + rectHeight + 40);
}


function handleHowToPlay(){
  clear();
  background(34,139,34);
  fill(255);
  textSize(32);
  text("How to Play:",width/2,height/5);

  textSize(24);
  text(
    "Words will appear in yellow or white.\n"+
    "If the word is yellow, use the yellow sensor.\n"+
    "If the word is white, use the white sensor.\n"+
    "Respond quickly and accurately to score points.",
    width/2,height/2
  );

  backButton.show();
}

function drawMythologyPage(){
  clear();
  background(34,139,34);
  fill(255);
  textSize(28);
  textAlign(LEFT,TOP);

  text(backgroundText,50,50,width-100,height-100);

  backButton.show();
  textAlign(CENTER,CENTER);
}

function handleSecondGameIntro() {
  // Change background image for this page
  if (bonbibi && bonbibi.width > 0) {
    imageMode(CORNER);
    image(bonbibi, 0, 0, width, height);
  } else {
    background('#7A3B0C'); // Fallback background color
  }

  // Box styling
  let boxWidth = width * 0.8; // Box width (80% of screen width)
  let boxHeight = height * 0.4; // Box height (40% of screen height)
  let boxX = (width - boxWidth) / 2; // Center horizontally
  let boxY = (height - boxHeight) / 2; // Center vertically

  noStroke();
  fill(0, 150); // Semi-transparent background for the box
  rect(boxX, boxY, boxWidth, boxHeight, 10); // Draw the box with rounded corners

  // Text inside the box
  fill(255);
  textSize(24);
  textAlign(CENTER, CENTER);

  // Split the text into vertical lines and display it
  let instructions = [
    "Collect flowers for Bonobibi."," Move basket by rotating the potentiometer.",
    "Press the button to start the basket game."
  ];
  let lineSpacing = 35; // Space between each line
  let textY = boxY + boxHeight / 2 - (instructions.length * lineSpacing) / 2;

  instructions.forEach((line, index) => {
    text(line, width / 2, textY + index * lineSpacing);
  });

  // Title text at the top
  textSize(36);
  stroke(2);
  strokeWeight(4);
  text("Prepare for the Next Challenge!", width / 2, boxY - 40);

  // Hide the back button
  backButton.hide();
}


// drawInstructionBox
function drawInstructionBox(textContent){
  textSize(18);

  let boxWidth=width*0.4;
  let boxHeight=60;
  let boxX=width/2 - boxWidth/2;
  let boxY=height/1.5 - boxHeight/12;

  noStroke();
  fill('rgb(165,88,8)');
  rect(boxX,boxY,boxWidth,boxHeight,10);

  fill(255);
  text(textContent,width/2,boxY+boxHeight/2);
}

function displayTimerAndScore(){
  push();
  textAlign(CENTER,CENTER);
  textSize(24);
  noStroke();
  fill(0,150);
  rectMode(CENTER);
  rect(width/2,50,220,60,10);
  fill(255);
  text("Time: "+countdown+"s | Score: "+score,width/2,50);
  pop();
}

function displaySensorData(){
  push();
  textAlign(LEFT,CENTER);
  textSize(16);
  noStroke();
  fill(0,150);
  rectMode(CORNER);
  rect(20,height-60,320,40,10);
  fill(255);
  text("Latest Data: "+latestData,40,height-40);
  pop();
}

function setupGameElements(){
  currentWordIndex=0;
  wordDisplay="";
  wordColor="white";
  countdown=timer;
  gameOver=false;
  gameResult="";
  score=0;

  wordInterval= 8000;
  console.log("Game elements reset.");

  gameStartInitiated=false;
}

function setNextWord() {
  if (gameOver) return;

  if (score > 20) {
    gameOver = true;
    gameResult = "Congratulations! You Win the First Game!";
    console.log("Score exceeded 20. Transitioning to second game intro...");
    setPage("secondGameIntro");
    return;
  }
  

  // Set the next word and its color
  wordDisplay = words[currentWordIndex];
  wordColor = random(["yellow", "white"]); // Assign color randomly
  currentWordIndex++;
  wordTouched = false; // Reset interaction flag

  // Schedule the next word transition
  wordTimeoutID = setTimeout(setNextWord, currentWordIndex < 5 ? 8000 : 6000);
}


let timerInterval;

function startTimer() {
  // Prevent multiple intervals
  if (timerStart) return;

  timerStart = true;
  clearInterval(timerInterval); // Clear any existing timer interval

  timerInterval = setInterval(() => {
    if (countdown > 0) {
      countdown--; // Decrement the timer
      console.log(`Timer: ${countdown}s`);
    } else {
      clearInterval(timerInterval); // Stop the timer when it reaches zero
      timerStart = false;
      if (!gameOver) {
        gameOver = true;
        gameResult = "Time's Up! You Lose!";
        setPage("lost"); // Go to the game over page
        console.log("Timer ended. Time's up!");
      }
    }
  }, 1000);
}

//--------------
// Debug with keyboard
//---------------

function keyPressed(){
  if(debugMode){
    if(key==='Y'||key==='y'){
      let simulatedData="5,15";
      console.log("Simulated Yellow Sensor Activation:",simulatedData);
      readSerial(simulatedData);
    }

    if(key==='W'||key==='w'){
      let simulatedData="15,5";
      console.log("Simulated White Sensor Activation:",simulatedData);
      readSerial(simulatedData);
    }

    if(key==='B'||key==='b'){
      let simulatedData="ButtonPressed";
      console.log("Simulated Button Press:",simulatedData);
      readSerial(simulatedData);
    }

    if(key==='P'||key==='p'){
      let simulatedData="600";
      console.log("Simulated Potentiometer Activation:",simulatedData);
      readSerial(simulatedData);
    }
  } else {
    if(key==='r'||key==='R'){
      if(page==="secondGameIntro"||page==="won"){
        setupGameElements();
        setPage("start");
        console.log("Game restarted to Start page.");
        currentText="";
        textIndex=0;

        clearInterval(typewriterIntervalID);

        typewriterIntervalID=setInterval(()=>{
          if(textIndex<typewriterText.length){
            currentText+=typewriterText[textIndex];
            textIndex++;
          }
        },typewriterSpeed);
      } else {
        setupGameElements();
        setPage("start");
        console.log("Game restarted to Start page.");
        currentText="";
        textIndex=0;

        clearInterval(typewriterIntervalID);

        typewriterIntervalID=setInterval(()=>{
          if(textIndex<typewriterText.length){
            currentText+=typewriterText[textIndex];
            textIndex++;
          }
        },typewriterSpeed);
      }
    }
  }
}
//------------


//----------
//Window Resize Code
//------------
function windowResized(){
  resizeCanvas(windowWidth,windowHeight);
  positionButtons();
  backButton.position(width/2 - backButton.width/2, height-100);
}

function calcTextSize(baseSize){
  return min(windowWidth,windowHeight)/800 * baseSize;
}

function updateTextSizes(){
  // Can be expanded if needed
}


// class constrauction for driving particles
//---------------------
class Vehicle {
  constructor(x,y){
    this.pos=createVector(x,y);
    this.vel=createVector(0,random(1,3));
    this.acc=createVector(0,0);
  }

  update(){
    this.vel.add(this.acc);
    this.pos.add(this.vel);
    this.acc.mult(0);

    if(this.pos.y>height){
      this.pos.y=0;
      this.pos.x=random(width);
    }
  }

  show(){
    stroke(173,216,230,150);
    strokeWeight(2);
    point(this.pos.x,this.pos.y);
  }
}

let gameSpeed = 2.8; // Default speed multiplier for the game

class Flower {
  constructor(x, y, size, rotation, type) {
    this.pos = createVector(x, y);
    this.size = size;
    this.rotation = rotation;
    this.type = type;
    this.speed = random(3, 6) * gameSpeed; // Adjust speed with multiplier
  }

  update() {
    this.pos.y += this.speed;
  }

  show() {
    push();
    translate(this.pos.x, this.pos.y);
    rotate(this.rotation);
    drawFlower(0, 0, this.size, this.type);
    pop();
  }
}


function drawFlower(x,y,size,type){
  switch(type){
    case 0:
      drawDaisy(x,y,size);
      break;
    case 1:
      drawTulip(x,y,size);
      break;
    case 2:
      drawRose(x,y,size);
      break;
    case 3:
      drawSunflower(x,y,size);
      break;
    case 4:
      drawLily(x,y,size);
      break;
    case 5:
      drawMarigold(x,y,size);
      break;
    default:
      drawDaisy(x,y,size);
      break;
  }
}

function drawDaisy(x,y,size){
  let petalCount=9;
  let petalLength=size;
  let petalWidth=size/3;

  stroke(0);
  fill('#D9E4E6');
  push();
  translate(x,y);
  for(let i=0;i<petalCount;i++){
    rotate(TWO_PI/petalCount);
    ellipse(0,-size/2,petalWidth,petalLength);
  }
  pop();

  fill('#F2F2F2');
  noStroke();
  ellipse(x,y,size/2);
}

function drawTulip(x,y,size){
  let petalCount=6;
  let petalWidth=size/2;

  stroke(0);
  fill('#AEB7FE');
  push();
  translate(x,y);
  for(let i=0;i<petalCount;i++){
    rotate(TWO_PI/petalCount);
    ellipse(0,-size/2,petalWidth,size);
  }
  pop();

  fill('#EDEAE6');
  noStroke();
  ellipse(x,y,size/3);
}

function drawRose(x,y,size){
  let petalCount=10;
  let petalWidth=size/3;

  stroke(0);
  fill('#D87373');
  push();
  translate(x,y);
  for(let i=0;i<petalCount;i++){
    rotate(TWO_PI/petalCount);
    ellipse(0,-size/2,petalWidth,size/1.5);
  }
  pop();

  fill('#F5E6E8');
  noStroke();
  ellipse(x,y,size/4);
}

function drawSunflower(x,y,size){
  let petalCount=20;
  let petalLength=size*1.5;
  let petalWidth=size/2;

  stroke(0);
  fill('#FACA49');
  push();
  translate(x,y);
  for(let i=0;i<petalCount;i++){
    rotate(TWO_PI/petalCount);
    ellipse(0,-size/2,petalWidth,petalLength);
  }
  pop();

  fill('#6E4B1B');
  noStroke();
  ellipse(x,y,size);
}

function drawLily(x,y,size){
  let petalCount=6;
  let petalWidth=size/2;

  stroke(0);
  fill('#998D30');
  push();
  translate(x,y);
  for(let i=0;i<petalCount;i++){
    rotate(TWO_PI/petalCount);
    ellipse(0,-size/2,petalWidth,size);
  }
  pop();

  fill('#FBE7E7');
  noStroke();
  ellipse(x,y,size/4);
}

function drawMarigold(x,y,size){
  let petalCount=12;
  let petalLength=size;
  let petalWidth=size/2;

  stroke(0);
  fill('#F4A263');
  push();
  translate(x,y);
  for(let i=0;i<petalCount;i++){
    rotate(TWO_PI/petalCount);
    ellipse(0,-size/2,petalWidth,petalLength);
  }
  pop();

  fill('#FFC107');
  noStroke();
  ellipse(x,y,size/3);
}

function drawBasket() {
  fill("#F6DC89"); // Set the fill color to a brown, typical of baskets
  rectMode(CENTER);
  rect(basketX, basketY-10, 60, 20, 5); // Main basket shape
  
  // Adding lines to create a woven effect
  for (let i = -30; i <= 30; i += 6) {
    stroke(139, 69, 19); // Darker brown for the weave lines
    line(basketX + i, basketY - 20, basketX + i, basketY); // Vertical lines
  }
  
  for (let j = -10; j <= 0; j += 5) {
    stroke(160, 82, 45); // Lighter brown for a highlight effect
    line(basketX - 30, basketY + j - 10, basketX + 30, basketY + j - 10); // Horizontal lines
  }
  
  noStroke(); // Resetting stroke to default
}
function handleSerialEvent(event) {
  console.log("Handling Serial Event:", event);

  // Parse sensor data
  let keyValues = event.trim().split(',');
  let data = {};
  keyValues.forEach((kv) => {
    let [key, value] = kv.split(':');
    if (key && value !== undefined) data[key.trim()] = value.trim();
  });

  console.log("Parsed Data:", data);

   // Check for the physical button press
  // Check for physical button press
  if (data.button === "pressed") {
    if (page === "start") {
      console.log("Physical button pressed. Transitioning to gameStart page.");
      setPage("gameStart"); // Transition to 'Get Ready' page

      // Add a 3-second delay before transitioning to the 'game' page
      setTimeout(() => {
        setPage("game");
        setupGameElements();
        setNextWord();
        startTimer();
        console.log("Word game started after 10-second delay.");
      }, 15000);
      return;
    }

    if (page === "secondGameIntro") {
      console.log("Physical button pressed. Transitioning to basketGame page.");
      setPage("basketGame");
      setupBasketGameElements();
      startTimer();
      return;
    }
  }


  // Handle Sensor Data (Ultrasonics) for Game 1
  if (page === "game" && !gameOver && !wordTouched) {
    let distanceYellow = parseFloat(data.ultra1);
    let distanceWhite = parseFloat(data.ultra2);

    if (!isNaN(distanceYellow) && !isNaN(distanceWhite)) {
      console.log(`Sensor Readings - Yellow: ${distanceYellow}cm, White: ${distanceWhite}cm`);
      let touchThreshold = 5; // Proximity threshold

      // Check if the correct sensor is touched
      if (wordColor === "yellow" && distanceYellow < touchThreshold) {
        handleCorrectTouch();
      } else if (wordColor === "white" && distanceWhite < touchThreshold) {
        handleCorrectTouch();
      } else if (
        (wordColor === "yellow" && distanceWhite < touchThreshold) ||
        (wordColor === "white" && distanceYellow < touchThreshold)
      ) {
        handleIncorrectTouch();
      }
    } else {
      console.log("Invalid sensor data received.");
    }
  }

  // Handle Potentiometer for Basket Game (Game 2)
  if (page === "basketGame" && !gameOver) {
    let potValue = parseFloat(data.pot);
    console.log("Potentiometer Reading:", potValue);

    if (!isNaN(potValue)) {
      basketX = map(potValue, 0, 1023, 50, width - 50);
      basketX = constrain(basketX, 50, width - 50);
    } else {
      console.log("Invalid potentiometer value:", potValue);
    }
  }
}

// Helper for correct touch
function handleCorrectTouch() {
  score++;
  wordTouched = true;
  feedbackMessage = "Correct!";
  feedbackColor = color(0, 200, 0);

  // Clear the current word and load the next one
  wordDisplay = "";
  clearTimeout(wordTimeoutID); // Cancel any pending word transitions
  setTimeout(() => {
    feedbackMessage = ""; // Clear feedback after 500ms
    setNextWord(); // Transition to the next word
  }, 500); // Allow brief feedback display
}

// Helper for incorrect touch
function handleIncorrectTouch() {
  feedbackMessage = "Incorrect!";
  feedbackColor = color(200, 0, 0);
  wordTouched = true;

  // Clear feedback and reset touch state after a short delay
  setTimeout(() => {
    feedbackMessage = "";
    wordTouched = false; // Allow interaction again
  }, 500);
}



function readSerial(data){
  if(data.trim().length>0){
    latestData=data.trim();
    handleSerialEvent(latestData);
  }
}

Code Explanation: 

As I had two sensors, potentiometer and button along with two levels, I had to maintain the setPage  very carefully. From here, I structured the game into multiple pages like start, gameStart, game, and basketGame. The setPage() function ensures smooth transitions between these stages. For example, when the button is pressed, the game transitions from start to gameStart with a delay before entering the gameplay. I decided to keep my words that will come with yellow or white color randomly, in an array together. To draw the flowers, I generated different types of them following to the styles of flowers found at Sundarban region.

Two sensors are dedicated to detecting hand proximity for selecting yellow and white words. My code receives distance values as ultra1 (Yellow) and ultra2 (White). If a hand comes within the set threshold (15 cm), the game registers the corresponding selection and updates the score. I also kept them on the console log to monitor how are the sensors working.

Feedback messages like “Correct!” or “Incorrect!” are displayed dynamically based on player interactions. These are managed using helper functions like handleCorrectTouch() and handleIncorrectTouch(). The code also includes a timer and score tracker to keep the gameplay engaging and competitive.

The readSerial() and handleSerialEvent() functions handle all incoming data from the Arduino. I used the P5.web-serial.js file given by the Professor.

Aspects that I’m proud of: 

I felt even though, it does not exactly showcase how I wanted to tell the story to my audience, but it was a satisfying play for the audience. Specially, the flower game was tough to finish with the randomness of flower speed and generation, that it was even tough for me to win my own game. I heard feedback from Professor Aya Riad that the flowing collection using potentiometer was very satisfying to play and collect the flowers while the time is running down. Kids enjoyed this one as they liked the visuals and this was also a part of my class assignments. I am glad that I was able to refine my codes to give it a new look.

For the word touching game, I am proud that the concept was unique in this show and was a bit of psychology side that one needs to pay full attention to the color and to their hand movement as well.  Even though I had tons of problems with sensor calibration, I was able to pull it off nicely.

I also got reviews from players who read the whole mythology and listened to the song and asked me about it. It was the biggest win for me that people were really interested to know what is the story behind.

Future Implementations:

I plan to implement all the levels and make it a full fledged game that  everyone can play while learning about the culture and stories of this region with fun. I want to change the ultrasonic distance sensors to some other sensors as they are tough to maintain when two sensors are kept together.