The final post before freedom. Yay!
For my final project, I built a game with the final objective to open a locked box and get FREEE CANDY!!!!!!1!!!1!ONE! 😀
My game consisted in three main parts:
- The box
- The card reader
- The laptop
First, the box. I built it out of acryllic. It took forever to build. It was a bit nightmareish at times. BUT IT WAS WORTH IT. The box had four buttons numbered 1-4 to input passwords, four red LEDs to indicate password progression (i.e. two LEDs light up once two numbers have been punched in), an RGB LED to signal a wrong or right password, and a servo to lock the box. And, of course, a Trinket Pro microcontroller (because it is tinier and cuter than the RedBoard, and thus fits in better). A right password makes the servo move and allow the lid of the box to slide open.
But the box is not just as simple as that. Using serial communication, I added two functions to the box. First, I added a second “right” password that, instead of opening the box, would send information to the main program in the laptop to advance the game. Second, I turned the password “4444” (as well as any button press after the box has been opened) into a signal to reset the game for another player to start from the beginning.
Second, the card reader. It has a RFID shield to read cards, and a mystery button to inpute passwords in morse code (because I’m evil). Given my lack of a better button/time/acryllic glue/motivation, I built the reader using the plastic thingy that came with our RedBoard, a breadboard, and some acryllic to make it look nicer. All put together using hot glue YAY.
The card reader also uses serial communication for two purposes. First, it sends information on the cards that are being read to the laptop. Second, it sends dots and dashes when the morse code button is pressed in order for the main program to read the sequence as a message/password in Morse code. (BONUS: the thing actually beeps when using Morse Code yay!)
And finally, the laptop. It contains the Processing sketch that runs most of the game. The sketch shows a series of strings in the screen, which provide instructions and clues that eventually lead to the final password to open the box. The game is advanced when it receives different kinds of input by the player: entering text using the keyboard, entering a password in the box (which sends it to the sketch through serial), or entering a password using morse code. Additionally, when a card is read, additional clues or information are displayed in the screen if it’s already time to use them (else, the screen will show an “encrypted” file consisting in a string of random characters). The cards don’t actually hold any files. Rather, the program shows a “file” based on the card’s unique ID, which is read and sent serially to Processing.
For the text, I created a Type class in order to produce the “typing” effect of the text appearing on the screen. While not perfect, it kinda has the aesthetic of old computers. I later learnt that the text() function has the ability to wrap text around edges, which might have made the whole thing easier to code. Oops.
And that’s kind of it. From the technical point of view, there’s not much more to add. The rest of the work was actually thinking out what kind of clues and puzzles I’d present the users (so, the game design) and making the game as understandable as I could int he time I had. I’m happy that people actively enjoyed the project, despite being admittedly somewhat confusing at times. 😀
Here’s the code:
#include <Servo.h> int led4 = 12; int led3 = 13; int led2 = A0; int led1 = A1; int ledR = 9; int ledG = 10; int ledB = 11; int but1 = 8; int but2 = 5; int but3 = 4; int but4 = 3; boolean state1, state2, state3, state4; boolean prev1, prev2, prev3, prev4; int color = 0; String code = {}; String pass1 = "1123"; Servo lock; void setup() { // Define I/O pinMode(led1, OUTPUT); pinMode(led2, OUTPUT); pinMode(led3, OUTPUT); pinMode(led4, OUTPUT); pinMode(ledR, OUTPUT); pinMode(ledG, OUTPUT); pinMode(ledB, OUTPUT); pinMode(but1, INPUT); pinMode(but2, INPUT); pinMode(but3, INPUT); pinMode(but4, INPUT); lock.attach(6); // Attach lock servo to pin lock.write(0); //Lock box // Set up initial state of lock buttons state1 = digitalRead(but1); prev1 = state1; state2 = digitalRead(but2); prev2 = state2; state3 = digitalRead(but3); prev3 = state3; state4 = digitalRead(but4); prev4 = state4; Serial.begin(9600); } void loop() { // Read passcode: //Read buttons state1 = digitalRead(but1); state2 = digitalRead(but2); state3 = digitalRead(but3); state4 = digitalRead(but4); // If button changed (is pressed), add button's number to code if (state1 != prev1) { // Delay and re-read to sniff out false button presses! delay(100); //Wait a bit state1 = digitalRead(but1); //read the state again if (state1 != prev1) { //If the state is STILL different... code += "1"; //...you can rule out an accidental double-press! } } if (state2 != prev2) { delay(100); state2 = digitalRead(but2); if (state2 != prev2) { code += "2"; } } if (state3 != prev3) { delay(100); state3 = digitalRead(but3); if (state3 != prev3) { code += "3"; } } if (state4 != prev4) { delay(100); state4 = digitalRead(but4); if (state4 != prev4) { code += "4"; } } // Turn # of LEDs equal to code's current length switch (code.length()) { case 0: digitalWrite(led1, LOW); digitalWrite(led2, LOW); digitalWrite(led3, LOW); digitalWrite(led4, LOW); break; case 1: digitalWrite(led1, HIGH); digitalWrite(led2, LOW); digitalWrite(led3, LOW); digitalWrite(led4, LOW); break; case 2: digitalWrite(led1, HIGH); digitalWrite(led2, HIGH); digitalWrite(led3, LOW); digitalWrite(led4, LOW); break; case 3: digitalWrite(led1, HIGH); digitalWrite(led2, HIGH); digitalWrite(led3, HIGH); digitalWrite(led4, LOW); break; case 4: digitalWrite(led1, HIGH); digitalWrite(led2, HIGH); digitalWrite(led3, HIGH); digitalWrite(led4, HIGH); break; } // Set previous states to current states for next reading prev1 = state1; prev2 = state2; prev3 = state3; prev4 = state4; // Password check part. This includes serial communication! if (code.length() != 4) { // Lock by default if code is incomplete lock.write(0); switch (color) { case 0: digitalWrite(ledR, LOW); digitalWrite(ledG, LOW); digitalWrite(ledB, LOW); break; case 1: digitalWrite(ledR, LOW); digitalWrite(ledG, LOW); digitalWrite(ledB, HIGH); break; } if (code.length() > 4) { code.remove(0); color = 0; Serial.println("0000"); } } else { if (code != "1431") { // If code is wrong, flash RGB LED red if (code == pass1) { digitalWrite(ledR, LOW); digitalWrite(ledG, LOW); digitalWrite(ledB, HIGH); } else { digitalWrite(ledR, HIGH); digitalWrite(ledG, LOW); digitalWrite(ledB, LOW); } Serial.println(code); code.remove(0); // Empty code delay(1000); // delay for feedback } else { // If code is right, turn RGB green and unlock digitalWrite(ledR, LOW); digitalWrite(ledG, HIGH); digitalWrite(ledB, LOW); lock.write(180); } } if (Serial.available() > 0) { String in = Serial.readString(); if (in == "blue") { color = 1; } } }
//RFID Libraries #include <Wire.h> #include <Adafruit_PN532.h> // If using the breakout or shield with I2C, define just the pins connected // to the IRQ and reset lines. Use the values below (2, 3) for the shield! #define PN532_IRQ (2) #define PN532_RESET (3) // Not connected by default on the NFC Shield // Use this line for a breakout or shield with an I2C connection: Adafruit_PN532 nfc(PN532_IRQ, PN532_RESET); //Global variables //Booleans to register morse button state boolean pressed = false; boolean prevPressed = false; long tick = 0; //tick is used to count length of beep long lastID = 0; //Last ID read by RFID long lastCard = 0; const long morseID = 28321276; void setup() { //Morse set up pinMode(12, INPUT); //Morse code goes in pinMode(6, OUTPUT); //Beep sounds yay Serial.begin(9600); //Set up RFID reader shield nfc.begin(); uint32_t versiondata = nfc.getFirmwareVersion(); if (! versiondata) { Serial.print("Didn't find PN53x board"); while (1); // halt } // configure board to read RFID tags nfc.SAMConfig(); } void loop() { //RFID read int thisID; int success; uint8_t uid[] = { 0, 0, 0, 0, 0, 0, 0 }; // Buffer to store the returned UID uint8_t uidLength; // Length of the UID (4 or 7 bytes depending on ISO14443A card type) // Wait for an ISO14443A type cards (Mifare, etc.). When one is found // 'uid' will be populated with the UID, and uidLength will indicate // if the uid is 4 bytes (Mifare Classic) or 7 bytes (Mifare Ultralight) success = nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength); if (success) { // Display some basic information about the card // if (uidLength == 4) // { // We probably have a Mifare Classic card ... unsigned long cardid = uid[0]; cardid <<= 8; cardid |= uid[1]; cardid <<= 8; cardid |= uid[2]; cardid <<= 8; cardid |= uid[3]; thisID = cardid; lastCard = cardid; if (thisID != lastID) { Serial.println(cardid); // print the card number } lastID = thisID; // } } //Morse read // If the morse card is inserted: if (lastCard == morseID) { pressed = digitalRead(12); //Read morse button //Beep while pressed if (pressed == true) { tone(6, 440); } else { noTone(6); } //If this is a new press... if (pressed == true && prevPressed != pressed) { tick = millis(); //...start counting time //Else, if this is the end of a press... } else if (pressed == false && prevPressed != pressed) { //...stop counting and compare: if (millis() - tick > 250) { //Over 0.25 seconds = dash Serial.println("-"); //Send dash serially } else if (millis() - tick > 50) { //Else, over 0.05 = dot Serial.println("."); //Send dot serially //Presses shorter than 50ms are considered mistakes and are ignored } } prevPressed = pressed; //Store previous state of the button } }
import gab.opencv.*; import java.awt.Rectangle; import processing.video.*; import processing.serial.*; import ddf.minim.*; import ddf.minim.analysis.*; import ddf.minim.effects.*; import ddf.minim.signals.*; import ddf.minim.spi.*; import ddf.minim.ugens.*; Minim minim; AudioOutput out; Serial morse; Serial box; Capture capture; OpenCV opencv; Rectangle[] faces = {}; String random1, random2, random3; int textSize = 24; String welcome = "Hello, and welcome to my final Intro to Interactive Media project. " + "There's a box with candy next to the computer. If you can solve my puzzles and open it, " + "you may take a candy!\n\nOkay, are you ready?\n\n(You can input answers when the " + "screen is green using the keyboard and ENTER. Try answering \"yes\")"; String clue1 = "You can read the cards' contents using the card reader. Most of them " + "are encrypted, but you should find your first clue in one of 'em...\n\n(A blue screen " + "means you should input a password using the box's buttons.)"; String clue2 = "The Fibonacci sequence, very well. Solving puzzles might decrypt " + "your encrypted cards, leading you to the next clue.\n\nYay\n\nNow tell me... the " + "complement of green is..."; String clue3 = "You should have heard of this code. It's used quiiite often for " + "distress signals. But this time around you should pull an Ali Baba and ask " + "your Sesame to...\n\n(Answer is checked two seconds after last input)"; String clue4 = "You are almost there! The one last thing you need to know is that " + "the answer is the death of the witch.\n\n(Input your answer in the box to open it!)"; String[] text = {welcome, clue1, clue2, clue3, clue4}; String joan = "Joan of Arc\n(1412 - 1431)\n\nJoan of Arc is a heroine of France who led the French " + "against the English during the Hundred Years' war. Originally a peasant, she was eventually " + "canonized by the Catholic Church as a saint. This, however, happened centuries after her death, " + "as at the time she was considered heretic. The English captured her in 1430 and burnt her at the " + "stake one year later, just like a witch..."; char[] input = {}; char[] morseIn = {}; int stage = 0; int cardClue = 0; int morseTime = 0; boolean green = false; // When true, user input is accepted boolean sos = false; int sosCount = 0; PImage spiral; PImage morseCode; String spiralt = "The Golden Spiral is related to the mathematical constant Phi, which can also be found in a famous sequence."; String colorClue = "The RGB color system is an additive system, which uses red, green and " + "blue as primary colors. The complements to those colors are the primary colors in " + "a substractive system: CMY correspond to cyan, m#$%@$% and yellow"; String cheat1 = "This person tried to cheat me by using their room key in my card reader. Meanie. :'c"; String cheat2 = "Pls stop cheating or I won't give any candy to you and I'll be sad kthxbai. :'c"; String pass1 = "1123"; ArrayList<Type> types = new ArrayList<Type>(); void setup() { fullScreen(); // Set up audio minim = new Minim(this); out = minim.getLineOut(); out.setTempo(120); // Set up serial ports morse = new Serial(this, "COM3", 9600); morse.bufferUntil('\n'); box = new Serial(this, "COM4", 9600); box.bufferUntil('\n'); // Set up text to be typed for (int q = 0; q < text.length; q ++) { types.add(new Type(text[q])); } // Set up images spiral = loadImage("GoldenSpiral.gif"); morseCode = loadImage("morsecode.jpg"); // Set up noise strings char[] randChar1 = {}; for (int q = 0; q < 200; q ++) { randChar1 = append(randChar1, randomChar()); } random1 = charToString(randChar1); char[] randChar2 = {}; for (int q = 0; q < 200; q ++) { randChar2 = append(randChar2, randomChar()); } random2 = charToString(randChar2); char[] randChar3 = {}; for (int q = 0; q < 200; q ++) { randChar3 = append(randChar3, randomChar()); } random3 = charToString(randChar3); // Set up easter egg capture capture = new Capture(this, 640, 480); capture.start(); // set up easter egg OpenCV opencv = new OpenCV(this, capture); opencv.loadCascade(OpenCV.CASCADE_FRONTALFACE); } void draw() { background(0); fill(255); strokeWeight(1); green = false; if (sos) { if (sosCount < 120) { background(random(0, 255), random(0, 255), random(0, 255)); textSize(random(12, 72)); text("1", random(0, width), random(0, height)); text("4", random(0, width), random(0, height)); text("3", random(0, width), random(0, height)); text("1", random(0, width), random(0, height)); text("1 4", random(0, width), random(0, height)); text("4 3", random(0, width), random(0, height)); text("3 1", random(0, width), random(0, height)); text("1 4 3", random(0, width), random(0, height)); text("4 3 1", random(0, width), random(0, height)); text("1 4 3 1", random(0, width), random(0, height)); sosCount ++; } else { sos = false; sosCount = 0; } } else { if (types.get(stage).trigger) { switch (stage) { case 0: background(0, 128, 0); green = true; for (int q = 0; q < input.length; q ++) { textSize(textSize); text(input[q], 40 + q*textSize, 350); } break; case 1: background(0, 0, 255); switch (cardClue) { case 0: break; case 1: textSize(16); text(spiralt, 200, 350, 500, 400); image(invert(spiral), 200, 400); break; case 2: textSize(16); text(random1, 200, 350, 500, 400); break; case 3: textSize(16); text(random2, 200, 350, 500, 400); break; case 4: textSize(16); text(random3, 200, 350, 500, 400); break; case 5: textSize(16); image(capture, width/2 -300, 350); opencv.loadImage(capture); Rectangle[] faces = opencv.detect(); for (int i = 0; i < faces.length; i ++) { pushMatrix(); translate(width/2 - 300, 350); sadFace(faces[i].x, faces[i].y, faces[i].width, faces[i].height); popMatrix(); } text(cheat1, 100, 350, 300, 450); text(cheat2, 100, 500, 300, 600); break; } break; case 2: background(0, 128, 0); green = true; for (int q = 0; q < input.length; q ++) { textSize(textSize); text(input[q], 40 + q*textSize, 350); } switch (cardClue) { case 0: break; case 1: textSize(16); text(spiralt, 200, 350, 500, 400); image(invert(spiral), 200, 400); break; case 2: textSize(16); text(random1, 200, 350, 500, 400); break; case 3: textSize(16); text(colorClue, 200, 450, 500, 400); colorChart(800, 450); break; case 4: textSize(16); text(random3, 200, 350, 500, 400); break; case 5: textSize(16); image(capture, width/2 -300, 350); opencv.loadImage(capture); Rectangle[] faces = opencv.detect(); for (int i = 0; i < faces.length; i ++) { pushMatrix(); translate(width/2 - 300, 350); sadFace(faces[i].x, faces[i].y, faces[i].width, faces[i].height); popMatrix(); } text(cheat1, 100, 350, 300, 450); text(cheat2, 100, 500, 300, 600); break; } break; case 3: switch (cardClue) { case 0: break; case 1: textSize(16); text(spiralt, 200, 350, 500, 400); image(invert(spiral), 200, 400); break; case 2: textSize(16); image(morseCode, 200, 400); break; case 3: textSize(16); text(colorClue, 200, 350, 500, 400); colorChart(800, 350); break; case 4: textSize(16); text(random3, 200, 350, 500, 400); break; case 5: textSize(16); image(capture, width/2 -300, 350); opencv.loadImage(capture); Rectangle[] faces = opencv.detect(); for (int i = 0; i < faces.length; i ++) { pushMatrix(); translate(width/2 - 300, 350); sadFace(faces[i].x, faces[i].y, faces[i].width, faces[i].height); popMatrix(); } text(cheat1, 100, 350, 300, 450); text(cheat2, 100, 500, 300, 600); break; } break; case 4: background(0, 0, 255); switch (cardClue) { case 0: break; case 1: textSize(16); text(spiralt, 200, 350, 500, 400); image(invert(spiral), 200, 400); break; case 2: textSize(16); image(morseCode, 200, 400); break; case 3: textSize(16); text(colorClue, 200, 350, 500, 400); colorChart(800, 350); break; case 4: textSize(16); text(joan, 200, 350, width - 200, height - 200); break; case 5: textSize(16); image(capture, width/2 -300, 350); opencv.loadImage(capture); Rectangle[] faces = opencv.detect(); for (int i = 0; i < faces.length; i ++) { pushMatrix(); translate(width/2 - 300, 350); sadFace(faces[i].x, faces[i].y, faces[i].width, faces[i].height); popMatrix(); } text(cheat1, 100, 350, 300, 450); text(cheat2, 100, 500, 300, 600); break; } break; } } strokeWeight(1); fill(255); types.get(stage).type(40, 40, textSize, 2); if (morseTime > 0) { morseTime --; } else { if (charToString(morseIn).equals("---.--..-.")) { stage = 4; } else if (charToString(morseIn).equals("...---...")) { sos = true; } else if (morseIn.length > 0) { background(255, 0, 0); } for (int q = morseIn.length - 1; q >= 0; q --) { morseIn = shorten(morseIn); } } } } void keyPressed() { if (green == false) { return; } if (key != BACKSPACE) { if (keyCode != 16 && key != ENTER) { input = append(input, key); } else if (key == ENTER) { switch (stage) { case 0: //println(charToString(input)); if (trim(charToString(input)).equalsIgnoreCase("yes")) { stage = 1; } break; case 2: if (trim(charToString(input)).equalsIgnoreCase("magenta")) { stage = 3; } } for (int q = input.length - 1; q >= 0; q --) { input = shorten(input); } } } else if (input.length > 0) { input = shorten(input); } } String charToString(char[] c) { if (c.length <= 0) { String voidResult = " "; return voidResult; } else { String result = str(c[0]); for (int q = 1; q < c.length; q ++) { result = result.concat(str(c[q])); } return result; } } void captureEvent(Capture c) { c.read(); } class Type { int counter; int typeCount; int length; String content; String originalContent; int rectCount = 0; boolean drawRect = true; boolean trigger = false; Type(String s) { typeCount = 0; counter = 0; content = s; length = s.length(); originalContent = s; } void type(float x, float y, float size, int period) { float xPos = x; float yPos = y; textSize(size); for (int q = 0; q <= counter; q ++) { char c = content.charAt(q); if (c == '\n' || c == '\r') { xPos = x; yPos += size + 1; continue; } if (xPos + size < width - size) { text(c, xPos, yPos); xPos += size; } else { if (c != ' ' && content.charAt(q-1) != ' ') { text('-', xPos, yPos); } xPos = x; yPos += size + 1; text(c, xPos, yPos); xPos += size; } } if (drawRect) { noStroke(); rect(xPos + 1, yPos - size, size - 2, size); } typeCount = (typeCount + 1) % period; rectCount = (rectCount + 1) % 15; if (rectCount == 0) { drawRect = !drawRect; } if (typeCount == 0) { if (counter < length - 1) { if (content.charAt(counter) != ' ' && content.charAt(counter) != '\r' && content.charAt(counter) != '\n') { out.pauseNotes(); out.playNote(0, 0.05, "C4"); out.resumeNotes(); } counter ++; } else { trigger = true; } } } } PImage invert(PImage im) { PImage im2 = createImage(im.width, im.height, ARGB); im2.loadPixels(); for (int q = 0; q < im.pixels.length; q ++) { float r = 255 - red(im.pixels[q]); float g = 255 - green(im.pixels[q]); float b = 255 - blue(im.pixels[q]); float a = alpha(im.pixels[q]); im2.pixels[q] = color(r, g, b, a); } im2.updatePixels(); return im2; } char randomChar() { char[] chars = {'a', 'A', 'b', 'B', 'c', 'C', 'd', 'D', 'e', 'E', 'f', 'F', 'g', 'G', 'h', 'H', 'i', 'I', 'j', 'J', 'k', 'K', 'l', 'L', 'm', 'M', 'n', 'N', 'o', 'O', 'p', 'P', 'q', 'Q', 'r', 'R', 's', 'S', 't', 'T', 'u', 'U', 'v', 'V', 'w', 'W', 'x', 'X', 'y', 'Y', 'z', 'Z', ' ', '?', '!', '.', '-', '#', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; int ind = int(random(0, chars.length)); return chars[ind]; } void colorChart(float x, float y) { pushMatrix(); translate(x, y); stroke(0); fill(255); rect(0, 0, 300, 200); noStroke(); fill(255, 0, 0, 128); ellipse(75, 50, 75, 75); fill(0, 255, 255, 128); ellipse(100, 50, 75, 75); fill(0, 0, 255, 128); ellipse(200, 50, 75, 75); fill(255, 255, 0, 128); ellipse(225, 50, 75, 75); fill(0, 255, 0, 128); ellipse(138, 150, 75, 75); fill(255, 0, 255, 128); ellipse(162, 150, 75, 75); popMatrix(); } void reset() { testStage = 0; stage = 0; cardClue = 0; green = false; for (Type type : types) { type.counter = 0; type.typeCount = 0; type.trigger = false; } } void sadFace(float x, float y, float w, float h) { stroke(255, 0, 0); strokeWeight(5); pushMatrix(); translate(x, y); scale(w/60, h/60); line(20, 0, 20, 20); line(40, 0, 40, 20); line(0, 40, 60, 40); line(0, 40, 0, 60); line(60, 40, 60, 60); popMatrix(); } void serialEvent(Serial port) { println("SERIAL"); if (port == morse) { // Morse and RFID communication String in = port.readString(); if (in.length() == 3 && stage >= 3) { morseIn = append(morseIn, in.charAt(0)); morseTime = 120; } else if (trim(in).equals("1441256961")) { //Fibonacci cardClue = 1; //println(yay); } else if (trim(in).equals("28321276")) { //Morse cardClue = 2; //println(yay); } else if (trim(in).equals("3442456957")) { //Card 3 (green) cardClue = 3; //println(yay); // } else if (trim(in).equals("621153299")) { //Card 4 (white) cardClue = 4; //println(yay); } else { cardClue = 5; } } else if (port == box) { //Box communication String in = trim(port.readString()); print(in + " - "); print(pass1); if (in.equals("0000") || in.equals("4444")) { reset(); } if (in.equals(pass1)) { port.write("blue"); stage = 2; } } }
(And now, I’m free)