Final Project: Battle of the Card Bots
Battle of the Card Bots
Tu Quynh Vuu
Interaction Lab: Gohai
The concept of “Battle of the Card Bots” is a two-player robot fight inside an arena, where each robot is user controlled through the joysticks. The primary objective of the game is to maneuver the bots and use the needle attached to its front to pop the opponent’s balloon (clipped on the back of each bot). The first bot to pop their opponent’s balloon wins the game! As they are called “card bots,” the theme is relative to the deck of cards, with colors of black and red being the overarching theme. The red robot is “club” while the black robot is “spade,” each have engravings of their card symbol on the back of their heads. To kickstart the entire game, the users refer to the Processing sketch where they navigate through a start page, instruction page, countdown screen, and playtime screen; depending on the result of the game once the 80 second playtime timer ends, there is a game over screen, and 3 corresponding screens (tie, spade wins, club wins). The countdown is 10 seconds, in which the users can test out the joystick controls of their respective robot, and each playtime round only lasts 80 seconds! The color of the LED strip inside of the arena will also fluctuate depending on which processing screen the users are looking at.
Regarding the preparatory research and the proposal essay for this project, I displayed my interest in making robots from the start. Although I discussed a different method for the construction of my robots in my proposal essay, such as making more limbs for each bot, having them knock each other outside of the arena, and using a bluetooth module rather than wires. The initial construction for my robots will be discussed later on in this documentation. I decided to change many aspects of my original plan along the way to accommodate for time constraints, avoiding massive complications, and for a more simplistic yet entertaining game! I was set on using joysticks to control the movements of the robot, and rather than making it have many limbs, I decided to make its movements solely rely on wheels attached to servo motors. The movements of the wheels controlled by the joystick include going forward, backward, left and right.
For user-testing day, I had the two robots ready and set up, with full motor control through the joystick. I also made a makeshift arena out of wood scraps from the fabrication lab, and transferred all the other components (arduinos, breadboards, wires, cables) to the top of it, with the wires hanging along the sides. I received plenty of great feedback from users who tested the project, including what things I should do for my processing sketch (I had nothing on processing during user-testing), change the angle placement of the needle for easier balloon popping, placement of the wires connected to the robots, and more. As the base foundation of my project was already laid down, I took into consideration everyone’s feedback and cultivated those ideas into reality for the final project. I also sharpened the needle on top of adjusting the angle it was placed, as they were quite dull and needed a lot of force to pop the balloons. For the processing sketch, I made it into something that was a lot like a game page, with a start screen, instructions, countdown, playtime, and end results. My processing sketch also had music (instrumental version of luigi’s mansion theme song) to make the environment more fun. One of the most prominent suggestions for my arena was to develop it into a larger theme, which is why I decided to go with the deck of cards theme as my robots were already built on that basis.
Initially, I planned for the robots to be on a larger scale than they were for the final result. I wanted fully functional robots with arms and legs that moved on command, however, there were too many components to cultivate this idea, as it would fry the arduino board and was much too time consuming. I discussed my ideas with my professor, and he suggested simplifying it to fit in the time crunch while keeping the main objectives. Cutting down the number of servo motors used for each robot, I decided to go with wheels for movement, rather than making legs with joints. To do this, I had to alter the mechanics inside the servos as they were only capable of 180 degree movement, and I needed it to be 360 degree movement. I altered two of the servos I had on hand, while I ordered more 360 degree servos on Taobao. I also ordered materials from Taobao including various types of wheels (to see which ones accommodate the robots best). For the power source, I connected an external 5v power supply to the breadboard in order to not overload the arduino board. Each robot has its own arduino board and breadboard, but they servo motors share the same power source.
The first prototype for the robot was still quite large as the main platform could not be held up by only two servo motors. I made the model larger as I wanted each robot to hold its own arduino and breadboard, since I was deciding on making it bluetooth controlled. I designed a 3 layer stack for the robot, with the first base holding the arduino, second base holding the breadboard, and the third for a cover. I used the laser cutter to cut out these parts on 3mm clear acrylic. However, this idea was not quite sustainable since the arduinos would need to be powered by a battery attached to the robot, making it extremely difficult to maneuver due to the weight. I decided to abandon the bluetooth idea since it was quite far-fetched for the amount of time I had to complete this project. So, my professor and I discussed a more simplified solution, which was to build an overhanging platform to hold the arduino, breadboard, wires, and such materials, so that only several cables would hang down to connect the bots. This change in ideas was extremely helpful and allowed me to make the robots much smaller and simpler. The idea of using balloons and needles for the main “goal” of this robot fight was not developed until later on in the project. I chose to do balloon popping because I was inspired by my midterm project which used a needle and balloon mechanism. I also thought this gameplay would be much more interesting than knocking the robots outside the arena as this could possibly damage the bots a lot, and having merely the health bar decrease did not have the same immediate effect of popping balloons.
The second prototype actually ended up being the final prototype for the bots. I decided to only use one base for the bot, and two stands propping up its head. I designed the parts and laser cut it on 3mm colored acrylic (black and red) with the clear acrylic stands being reused from my first prototype. The two servo motors were glued together back to back onto the base, with the acrylic stands propped in front and in the back of the motors. The measurements for this were quite precise to ensure that the motors would fit well between the stands. For the head of the robot, it is propped up but the acrylic stands (with rectangular holes at the bottom of the head for assemblance). The base of the bot is 9cm by 6cm, the head is 4cm all around, and the collective height of the bot is 11cm. The wheels are 3cm in diameter and were glued onto the cut horns of the servo motor, which was also glued onto the servo itself for durability. Prior to the assembling of the robots, I tested out the wheels and made sure the code worked. This aspect was a little tricky as I had to connect the joystick to the servos, and calculate which angles allowed movement forward, backward, left, and right. To do this, I had to define the midpoints of the joystick itself, and used if and for loops to have it move in different directions at different values. There were certain times where the wheels would start moving on its own, so in my code, I altered it to detaching the servos and reattaching it once the joystick was in command. After the code successfully worked for the movement of the bots at all parameters, all that was left to do was attach the needles onto the robots. In the final prototype of this bot, I used residual materials from laser-cut parts to make the bots look more pleasing and glued the needle between those parts to ensure they would stay at an angle for the balloon popping. As I needed longer wires, I cut extra wires provided by the lab and soldered them onto the shorter M/M cables. The entire coding process of this project was not extremely difficult as I implemented all the knowledge I’ve learned from this semester and last semester, however, it does take a hefty amount of trial and error.
For the arena of the robot fight, I turned my makeshift wooden scrap arena from user testing day into a more developed one. I designed the parts on cuttle.xyz for laser cutting, with most of the parts being 60cm by 10cm. The cross shape based that overhangs on top of the arena is designed to hold the arduinos, breadboard, wires, and more. I also designed aesthetic card-themed borders around the edges to hide the wiring and cables. Keeping the theme in mind, everything was cut on black and red acrylic, and each leg of the stand (there are 4) has the card symbols engraved on the sides. Because 3mm acrylic does not hold up well standing on its own, my professor assisted me in building a wooden base where I used to prop up the rest of the laser cut acrylic materials. This assured durability for the arena stand, and I also used residual materials to enclose each leg of the arena, making sure that most of the wiring was hidden. The arena floor was an entire 60cm by 45cm acrylic board, and the overhanging part was simply propped up on it. The transfer of the cables, arduinos, and breadboards from the makeshift arena onto the polished one was a little difficult as I took careful measure to make sure nothing fell apart. This process was tricky as all of the wiring is essentially connected together, so I did have to disconnect several wires in order to get the placements accurate. After the computational systems were installed onto the arena, I looped the robots’ servo wires into two opposing legs of the arena, and the joysticks into the other two legs. I also noticed that the lighting was quite dim on the arena due to the overhanging material, so I decided to implement the neopixel LED strip underneath the wooden base of the arena. The assemblance of these acrylic parts was entirely done with the help of hot glue and patience.
On the processing side of this project, I created a game-like format similar to the ones in arcade games. It includes a start page, instruction page, countdown screen, playtime screen, and game over screen with corresponding ones depending on the result of the game (tie, spade wins, club wins). I have had prior experience in coding visuals so the construction of these screens was not too difficult. I also connected each screen to the neopixel (powered by arduino), where the colors would change depending on the processing screen you were on. For instance, once the countdown timer ended and the screen switched to play time, the lights would turn red. I also implemented music to match the theme of the game, which was the instrumental version of Luigi’s mansion theme song. One hindrance of the processing side is that the ending screens for a tie, spade winning, and club winning are controlled by keyboard buttons rather than when the balloon actually pops. This is due to time constraints so I could not add a sensor to detect the balloon popping. The processing code relies on states, where the screens are based on the declaration of these states. The screens are full functions with its relative components inside, allowing for ease of access in switching to different screens depending on which part of the game the user is on.
Overall, the main goal of this project was to create a fun and interesting experience for the audience as they play Battle of the Card Bots. I would say that this project was extremely successful and a fundamental stepping stone of my major in IMA. I have always wanted to develop robots, and this project was a perfect opportunity to cultivate this idea. The audience for my project particularly enjoyed the entire experience, and everyone gave remarks on how developed, amazing, and fun it was ! If I had more time on this project, I would definitely add sensors to detect the balloon popping, and interconnect it with my processing sketch to have the ending screens show up as a reaction. I would also like to add more sound effects such as the annunciation of the countdown, rather than just having one song play the entire time. From this project, I have learned that it is better to simplify things rather than overcomplicating them, but maintaining the main aspects of what makes the game fun. I also presented this project for the IMA show, where I received tons of positive feedback from both students, staff, and outside visitors. The game was played so much that one of the servo motors connected to a wheel on the robot actually broke. In all, I was extremely impressed with my own capabilities and pleased with the result of this project as I spent most of my time in the lab trying to perfect it. I look forward to cultivating future projects in the IMA field. 🙂
Processing Code
String gameState; String time = "010"; int t; int interval = 10; int interval1 = 87; PImage p; int suitSize = 50; int padding = 20; boolean firstRun = true; int startTime; import ddf.minim.*; import ddf.minim.effects.*; Minim minim; AudioPlayer song; import processing.serial.*; Serial serialPort; int NUM_LEDS = 60; // How many LEDs in your strip? color[] leds = new color[NUM_LEDS]; // array of one color for each pixel void setup(){ size(2000, 1200); gameState = "START"; frameRate(30); printArray(Serial.list()); serialPort = new Serial (this, "COM7", 115200); minim = new Minim(this); song = minim.loadFile("luigismansion.mp3", 2048); } void draw(){ background(255); if (gameState == "START"){ startGame(); song.loop(); for (int i=0; i < NUM_LEDS; i++){ leds[i] = color(255); } } else if (gameState == "INSTRUCT"){ instructGame(); for (int i=0; i < NUM_LEDS; i++){ leds[i] = color(201, 2, 118); } } else if (gameState == "PLAY"){ playGame(); for (int i=0; i < NUM_LEDS; i++){ leds[i] = color(random(50, 255), random(150, 255), random(200, 255)); } } else if (gameState == "PLAY1"){ playGame1(); for (int i=0; i < NUM_LEDS; i++){ leds[i] = color(random(200, 255), random(0, 20), random(0, 20)); } } else if (gameState == "SPADEWIN"){ spadewinGame(); for (int i=0; i < NUM_LEDS; i++){ leds[i] = color(129, 9, 9); } } else if (gameState == "CLUBWIN"){ clubwinGame(); for (int i=0; i < NUM_LEDS; i++){ leds[i] = color(255); } } else if (gameState == "TIE"){ tieGame(); for (int i=0; i < NUM_LEDS; i++){ leds[i] = color(255, 137, 126); } } else if (gameState == "END"){ endGame(); for (int i=0; i < NUM_LEDS; i++){ leds[i] = color(129, 11, 0); } } sendColors(); } void startGame(){ background(0); fill(255, 0, 0); circle(random(width), random(height), random(10, 45)); square(random(width), random(height), random(10, 45)); fill(255); circle(random(width), random(height), random(10, 45)); square(random(width), random(height), random(10, 45)); fill(165, 20, 20); circle(random(width), random(height), random(10, 45)); square(random(width), random(height), random(10, 45)); rect(0, 0, 2000, 130); rect(0, 1000, 2000, 80); fill(142, 132, 132); square(random(width), random(height), random(10, 45)); fill(255, 0, 0); textAlign(CENTER); textSize(150); text("BATTLE OF THE CARD BOTS", width/2, height/2-150); rect(width/2-150, height/2-40, 400, 170); fill(0); textAlign(CENTER); textSize(40); text("PRESS TO START", width/2+50, height/2+50); if (mousePressed == true){ gameState = "INSTRUCT"; } } void instructGame(){ background(0); fill(142, 20, 9); rect(0, 0, 2000, 130); rect(0, 1000, 2000, 80); drawSpade(300, 200, 80); drawClub(1600, 200, 80); drawSpade(150, 500, 80); drawClub(1850, 500, 80); drawSpade(300, 800, 80); drawClub(1600, 800, 80); textAlign(CENTER); textSize(95); fill(255); text("HOW TO PLAY", width/2, height/2-300); fill(255, 0, 0); textSize(50); text("1) Each Player pick up a joystick!", width/2, height/2-200); text("2) Control your bot to pop each other's balloon", width/2, height/2-100); text("3) You have 80 seconds of playtime", width/2, height/2); text("4) Goodluck and Have Fun!", width/2, height/2+100); rect(width/2-250, height/2+200, 500, 70); fill(255); textSize(35); text("Tap the Space Bar to Start", width/2, height/2+250); if (keyPressed == true){ gameState = "PLAY"; } } void playGame(){ if (firstRun) { firstRun = false; startTime = int(millis()/1000); } background(255, 0, 0); //noStroke(); fill(0); for(int i=0;i<width/20;i++) for(int j=0;j<height/20;j++) if((i+j)%2==0) rect(20*i,20*j,20,20); p=get(); textSize(120); fill(255); rect(width/2-390, height/2-380, 780, 260); fill(0); rect(width/2-360, height/2-350, 710, 200); fill(255); text("COUNTDOWN", width/2, height/2-200); fill(0); rect(width/2-100, height/2-60, 200, 120); interval = 10; t = startTime + interval-int(millis()/1000); time = nf(t, 1); if (t > 0){ fill(255); textSize(95); text(time, width/2, height/2+30); } if (t == 0){ //println("GAME OVER"); //interval+=10; fill(255); textSize(95); text("GO!", width/2, height/2+30); } if(t == -1){ firstRun = true; gameState = "PLAY1"; } } void playGame1(){ if (firstRun) { firstRun = false; startTime = int(millis()/1000); } background(0); for (int x = 0; x < width; x += suitSize + padding) { for (int y = 0; y < height; y += suitSize + padding) { int suit = (x + y) % 4; switch (suit) { case 0: drawSpade(x, y, suitSize); break; case 2: drawClub(x, y, suitSize); break; } } } textSize(120); fill(255, 0, 0); rect(width/2-360, height/2-380, 730, 270); fill(0); rect(width/2-300, height/2-350, 600, 200); fill(255); text("PLAYTIME", width/2, height/2-200); fill(0); rect(width/2-100, height/2-60, 200, 120); interval1 = 80; t = startTime + interval1-int(millis()/1000); time = nf(t, 1); fill(255); textSize(95); text(time, width/2, height/2+30); if (t == 0){ firstRun = true; gameState = "END"; } } void spadewinGame(){ background(0); for (int x = 0; x < width; x += suitSize + padding) { for (int y = 0; y < height; y += suitSize + padding) { int suit = (x + y) % 4; switch (suit) { case 0: noStroke(); drawSpade(x, y, suitSize); break; } } } textSize(120); fill(255, 0, 0); rect(width/2-530, height/2-380, 1120, 260); fill(0); rect(width/2-500, height/2-350, 1060, 200); fill(255); text("SPADE BOT WINS!", width/2, height/2-200); fill(255); rect(width/2-120, height/2-80, 240, 160); fill(255, 0, 0); rect(width/2-100, height/2-60, 200, 120); fill(0); textSize(40); text("Play Again!", width/2, height/2+5); } void clubwinGame(){ background(255, 0, 0); for (int x = 0; x < width; x += suitSize + padding) { for (int y = 0; y < height; y += suitSize + padding) { int suit = (x + y) % 4; switch (suit) { case 2: drawClub(x, y, suitSize); break; } } } textSize(120); fill(0); rect(width/2-530, height/2-380, 1060, 260); fill(255, 0, 0); rect(width/2-500, height/2-350, 1000, 200); fill(255); text("CLUB BOT WINS!", width/2, height/2-200); fill(0); rect(width/2-120, height/2-80, 240, 160); fill(255); rect(width/2-100, height/2-60, 200, 120); fill(0); textSize(40); text("Play Again!", width/2, height/2+5); } void tieGame(){ background(20, 100, 150); background(0); for (int x = 0; x < width; x += suitSize + padding) { for (int y = 0; y < height; y += suitSize + padding) { int suit = (x + y) % 3; switch (suit) { case 0: drawClub(x, y, suitSize); break; case 2: drawSpade(x, y, suitSize); break; } } } textSize(120); fill(255, 0, 0); rect(width/2-330, height/2-380, 660, 260); fill(0); rect(width/2-300, height/2-350, 600, 200); fill(255); text("IT'S A TIE!", width/2, height/2-200); fill(255); rect(width/2-120, height/2-80, 240, 160); fill(255, 0, 0); rect(width/2-100, height/2-60, 200, 120); fill(0); textSize(40); text("Play Again!", width/2, height/2+5); } void endGame(){ background(0); fill(255, 0, 0); circle(random(width), random(height), random(10, 45)); square(random(width), random(height), random(10, 45)); fill(255); circle(random(width), random(height), random(10, 45)); square(random(width), random(height), random(10, 45)); fill(165, 20, 20); circle(random(width), random(height), random(10, 45)); square(random(width), random(height), random(10, 45)); rect(0, 0, 2000, 130); rect(0, 1000, 2000, 80); fill(142, 132, 132); square(random(width), random(height), random(10, 45)); fill(255, 0, 0); textAlign(CENTER); textSize(150); text("GAME OVER", width/2, height/2); } void keyPressed() { if ( key == 's' ){ gameState = "SPADEWIN"; } else if (key == 'c'){ gameState = "CLUBWIN"; } else if (key == 't'){ gameState = "TIE"; } } void drawSpade(float x, float y, float size) { float width = size * 0.7; float height = size * 1.2; pushMatrix(); translate(x, y); scale(size / 100.0); fill(255, 0, 0); beginShape(); vertex(50, 0); bezierVertex(14-5, 0+30, 0-5, 40+30, 50, 85); bezierVertex(100+5, 40+40, 86, 0+40, 50, 10); endShape(); rect(40, 65, 18, 35); rect(35, 95, 30, 7); popMatrix(); } void drawClub(float x, float y, float size) { float width = size; float height = size; pushMatrix(); translate(x, y); scale(size / 100.0); noStroke(); fill(255); ellipse(50, 30, 60, 60); ellipse(25, 50, 60, 65); ellipse(75, 50, 60, 65); rect(40, 65, 18, 35); rect(35, 95, 30, 10); popMatrix(); } void sendColors() { byte[] out = new byte[NUM_LEDS*3]; for (int i=0; i < NUM_LEDS; i++) { out[i*3] = (byte)(floor(red(leds[i])) >> 1); if (i == 0) { out[0] |= 1 << 7; } out[i*3+1] = (byte)(floor(green(leds[i])) >> 1); out[i*3+2] = (byte)(floor(blue(leds[i])) >> 1); } serialPort.write(out); }
Arduino Code (Club Bot)
#include <Servo.h>
Servo myservo1;
Servo myservo2;
int JoyStick_X = 0; //x
int JoyStick_Y = 1; //y
int JoyStick_Z = 5; //key
#include <FastLED.h>
#define NUM_LEDS 60
#define DATA_PIN 6
CRGB leds[NUM_LEDS];
int next_led = 0;
byte next_col = 0;
byte next_rgb[3];
void setup() {
myservo1.attach(2);
myservo2.attach(3);
pinMode(JoyStick_Z, INPUT);
Serial.begin(115200); // 9600 bps
FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS);
FastLED.setBrightness(50);
leds[0] = CRGB::Red;
FastLED.show();
delay(1000);
leds[0] = CRGB::Black;
FastLED.show();
}
void loop() {
while (Serial.available()) {
char in = Serial.read();
if (in & 0x80) {
next_led = 0;
next_col = 0;
}
if (next_led < NUM_LEDS) {
next_rgb[next_col] = in << 1;
next_col++;
if (next_col == 3) {
leds[next_led] = CRGB(next_rgb[0], next_rgb[1], next_rgb[2]);
next_led++;
next_col = 0;
}
}
if (next_led == NUM_LEDS) {
FastLED.show();
next_led++;
}
}
int x,y,z;
x=analogRead(JoyStick_X);
y=analogRead(JoyStick_Y);
z=digitalRead(JoyStick_Z);
Serial.print(x ,DEC);
Serial.print(",");
Serial.print(y ,DEC);
Serial.print(",");
Serial.println(z ,DEC);
int midX = 522;
int midY = 502;
int dX = x-midX;
int dY = y-midY;
if (abs(dX) < 10 && abs(dY) < 10) {
Serial.println("idle");
myservo1.detach();
myservo2.detach();
} else if (abs(dX) > abs(dY)) {
Serial.println("left or right");
myservo1.attach(2); // attaches the servo on pin 9 to the servo object
myservo2.attach(3);
myservo1.write(map(x, 0, 1023, 0, 180));
myservo2.write(map(x, 0, 1023, 0, 180));
} else {
Serial.println("up or down");
myservo1.attach(2); // attaches the servo on pin 9 to the servo object
myservo2.attach(3);
myservo1.write(map(y, 0, 1023, 0, 180));
myservo2.write(map(y, 0, 1023, 180, 0));
}
}
#include <Servo.h>
Servo myservo1;
Servo myservo2;
int JoyStick_X = 0; //x
int JoyStick_Y = 1; //y
int JoyStick_Z = 5; //key
void setup() {
myservo1.attach(2); // attaches the servo on pin 9 to the servo object
myservo2.attach(3);
pinMode(JoyStick_Z, INPUT);
Serial.begin(9600); // 9600 bps
}
void loop() {
int x,y,z;
x=analogRead(JoyStick_X);
y=analogRead(JoyStick_Y);
z=digitalRead(JoyStick_Z);
Serial.print(x ,DEC);
Serial.print(",");
Serial.print(y ,DEC);
Serial.print(",");
Serial.println(z ,DEC);
int midX = 512;
int midY = 487;
int dX = x-midX;
int dY = y-midY;
if (abs(dX) < 10 && abs(dY) < 10) {
Serial.println("idle");
myservo1.detach();
myservo2.detach();
} else if (abs(dX) > abs(dY)) {
Serial.println("left or right");
myservo1.attach(2); // attaches the servo on pin 9 to the servo object
myservo2.attach(3);
myservo1.write(map(x, 0, 1023, 0, 180));
myservo2.write(map(x, 0, 1023, 0, 180));
} else {
Serial.println("up or down");
myservo1.attach(2); // attaches the servo on pin 9 to the servo object
myservo2.attach(3);
myservo1.write(map(y, 0, 1023, 0, 180));
myservo2.write(map(y, 0, 1023, 180, 0));
}
}