Concept and Implementation
The game begins with a start page where the user gets enough time to read through the instructions.
Upon clicking start the game begins with a sequence of one color; say blue. The color is highlighted with a black stroke. The user is supposed to press the same color. If they get it right they proceed to the next round which appends one more color to the sequence say green and now the player sees two colors blue and green. If they had gotten it wrong on the first try the sequence repeats at the expense of one out of the three lives that the player has. This is to say that the player has got only two passes to get it wrong. The game continues until all the colors have been highlighted and well matched by the players. At the side there is a score tracking progress bar that fills depending on the number of colors that the player gets right. When the player successfully fills the bar by getting all the colors right, the game ends and the player wins. If the player gets it wrong three times the game ends and they have the option to restart.
VIDEO/ IMAGES
ARDUINO
The arduino code is less complicated compared to the p5js code. The connections involved
- 4 led push buttons
- Buzzer
- Jumper wires
SCHEMATIC
For my scematic I used normal push buttons as I could not find the led-push buttons for representation.
ARDUINO CODE
// Pin Definitions const int redPin = 6; const int greenPin = 7; const int bluePin = 8; const int yellowPin = 9; const int buzzerPin = 11; const int buttonPins[] = {2, 3, 4, 5}; // Red, Green, Blue, Yellow const char* colorNames[] = {"red", "green", "blue", "yellow"}; // Frequencies for different colors (in Hz) const int tones[] = {262, 330, 390, 494}; // A4, C5, D5, E5 int lastButtonState[4] = {HIGH, HIGH, HIGH, HIGH}; // For edge detection String colorSequence = ""; // Collects pressed color names void setup() { // LED outputs pinMode(redPin, OUTPUT); pinMode(greenPin, OUTPUT); pinMode(bluePin, OUTPUT); pinMode(yellowPin, OUTPUT); pinMode(buzzerPin, OUTPUT); // Button inputs for (int i = 0; i < 4; i++) { pinMode(buttonPins[i], INPUT_PULLUP); } Serial.begin(9600); } void loop() { for (int i = 0; i < 4; i++) { int currentState = digitalRead(buttonPins[i]); // Detect new button press (from HIGH to LOW) if (lastButtonState[i] == HIGH && currentState == LOW) { // Turn on the LED if (i == 0) digitalWrite(redPin, HIGH); if (i == 1) digitalWrite(greenPin, HIGH); if (i == 2) digitalWrite(bluePin, HIGH); if (i == 3) digitalWrite(yellowPin, HIGH); // Play tone on buzzer tone(buzzerPin, tones[i], 150); // Play for 150 ms // Append to sequence and send it if (colorSequence.length() > 0) { colorSequence += ","; } colorSequence += colorNames[i]; Serial.println(colorSequence); // Send entire sequence } // Turn off LED if button is released if (currentState == HIGH) { if (i == 0) digitalWrite(redPin, LOW); if (i == 1) digitalWrite(greenPin, LOW); if (i == 2) digitalWrite(bluePin, LOW); if (i == 3) digitalWrite(yellowPin, LOW); } lastButtonState[i] = currentState; } delay(50); // Short delay for debouncing }
P5JS
For my p5js I mainly used functions to help with handling of data and processing from the arduino and to display. In the sketch I had html, a webserial library, music(background, correct and incorrect sounds), and the actual p5js sketch code. Some of the notable portions of the p5js code include music handling, serial communication, game start page, button handling and game over page.
Below are the different code portions.
HTML
<!DOCTYPE html> <html lang="en"> <head> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/addons/p5.sound.min.js"></script> <!-- Load the web-serial library --> <script src="p5.web-serial.js"></script> <link rel="stylesheet" type="text/css" href="style.css"> <meta charset="utf-8" /> </head> <body> <main> </main> <script src="sketch.js"></script> </body> </html>
WEBSERIAL LIBRARY
let port, reader, writer; let serialActive = false; async function getPort(baud = 9600) { let port = await navigator.serial.requestPort(); // Wait for the serial port to open. await port.open({ baudRate: baud }); // create read & write streams textDecoder = new TextDecoderStream(); textEncoder = new TextEncoderStream(); readableStreamClosed = port.readable.pipeTo(textDecoder.writable); writableStreamClosed = textEncoder.readable.pipeTo(port.writable); reader = textDecoder.readable .pipeThrough(new TransformStream(new LineBreakTransformer())) .getReader(); writer = textEncoder.writable.getWriter(); return { port, reader, writer }; } class LineBreakTransformer { constructor() { // A container for holding stream data until a new line. this.chunks = ""; } transform(chunk, controller) { // Append new chunks to existing chunks. this.chunks += chunk; // For each line breaks in chunks, send the parsed lines out. const lines = this.chunks.split("\r\n"); this.chunks = lines.pop(); lines.forEach((line) => controller.enqueue(line)); } flush(controller) { // When the stream is closed, flush any remaining chunks out. controller.enqueue(this.chunks); } } async function setUpSerial() { noLoop(); ({ port, reader, writer } = await getPort()); serialActive = true; runSerial(); loop(); } async function runSerial() { try { while (true) { if (typeof readSerial === "undefined") { console.log("No readSerial() function found."); serialActive = false; break; } else { const { value, done } = await reader.read(); if (done) { // Allow the serial port to be closed later. reader.releaseLock(); break; } readSerial(value); } } } catch (e) { console.error(e); } } async function writeSerial(msg) { await writer.write(msg); }
p5js SKETCH
gridSize = 5; squareSize = 100; colors = ['red', 'green', 'blue', 'yellow']; grid = []; sequence = []; playerInput = []; showingSequence = false; showIndex = 0; showTimer = 0; inputEnabled = false; currentRound = 1; gameOver = false; gameStarted = false; serialMessage = ''; messageColor = 'black'; confetti = []; startButton = null; restartButton = null; statusTimer = 0; statusDuration = 2000; roundStartTime = 0; timeLimit = 10000; timeLeft = timeLimit; let bgMusic, correctSound, incorrectSound; lives = 3; function preload() { //loading the music soundFormats('mp3', 'wav'); bgMusic = loadSound('music/background.mp3'); correctSound = loadSound('music/correct.mp3'); incorrectSound = loadSound('music/incorrect.mp3'); } function setup() { createCanvas(gridSize * squareSize + 80, gridSize * squareSize + 60); noStroke(); startButton = createButton('▶ Start Game');//start button styleButton(startButton, width / 2 - 60, height / 2 + 10, '#4CAF50'); startButton.mousePressed(async () => { await setUpSerial();//using the start button to initiate serial comm startGame(); }); bgMusic.setLoop(true); bgMusic.play();// playing the background music throughout } function styleButton(btn, x, y, color) {// styling code function for all the buttons btn.position(x, y); btn.style('font-size', '20px'); btn.style('padding', '10px 20px'); btn.style('background-color', color); btn.style('color', 'white'); btn.style('border', 'none'); btn.style('border-radius', '8px'); } function startGame() { //initializing the game gameStarted = true; startButton.remove();// moving from the start page to the grid initGrid(); nextRound(); } //game loop function draw() { background(220); if (!gameStarted) { drawStartScreen(); return; } if (gameOver) { drawEndScreen(); return; } drawGrid(); drawProgressBar(); drawTimeBar(); drawSidebar(); if (showingSequence && millis() - showTimer > 800) { showTimer = millis(); showIndex++; if (showIndex >= sequence.length) { showingSequence = false; inputEnabled = true; showIndex = 0; roundStartTime = millis(); } } if (inputEnabled) { timeLeft = timeLimit - (millis() - roundStartTime); if (timeLeft <= 0) { handleIncorrect('Time Up!'); } } clearSerialMessageIfDue(); } function drawStartScreen() { drawBackgroundGradient(); fill(30); textAlign(CENTER, CENTER); textFont('Helvetica'); textSize(36); text(' Color Memory Game', width / 2, height / 2 - 80); textSize(20); text('Repeat the color sequence using the physical buttons!', width / 2, height / 2 - 40); } //---------------END SCREEN------------------------------- function drawEndScreen() { drawBackgroundGradient(); drawConfetti(); drawGameOverArt(); textAlign(CENTER, CENTER); fill('#2E8B57'); textFont('Georgia'); textSize(36); text('Game Over!', width / 2, height / 2 - 80); fill(50); textSize(16); text('Press the button to restart', width / 2, height / 2 - 40); if (!restartButton) { restartButton = createButton(' Restart'); restartButton.id('restartBtn'); styleButton(restartButton, width / 2 - 60, height / 2 + 20, '#f44336'); restartButton.mousePressed(() => { restartButton.remove(); restartButton = null; resetGame(); }); } } //--------------------BOTTOM BAR-------------------------- //Displaying the message function updateSerialMessage(msg, color) { serialMessage = msg; messageColor = color; statusTimer = millis(); } //Clearing the message function clearSerialMessageIfDue() { if (millis() - statusTimer > statusDuration && serialMessage !== '') { serialMessage = ''; } } //---------------------SIDE BAR--------------------------------- function drawSidebar() { let barWidth = 60; let barX = width - barWidth; fill(240); rect(barX, 0, barWidth, height); let progress = sequence.length / grid.length;//filling with the progress bar fill('#2196F3'); rect(barX + 10, 20, 40, height * progress); textAlign(CENTER, CENTER); textSize(20); for (let i = 0; i < 3; i++) { let y = height - 40 - i * 30; fill(i < lives ? 'red' : 'lightgray'); text(i < lives ? '❤️' : '', barX + barWidth / 2, y);// representing lives with red heart emojis } } // -------------------TIME BAR--------------------------------------- function drawTimeBar() { let barHeight = 10; let barY = height - 60; let progress = constrain(timeLeft / timeLimit, 0, 1); fill(200); rect(0, barY, width - 80, barHeight); fill(progress < 0.3 ? '#F44336' : progress < 0.6 ? '#FFC107' : '#4CAF50'); rect(0, barY, (width - 80) * progress, barHeight);///changing color depending in the time left } //-------------------PROGRESS BAR----------------------------- function drawProgressBar() { let barHeight = 30; let barY = height - barHeight; let progress = sequence.length > 0 ? playerInput.length / sequence.length : 0; fill(200); rect(0, barY, width - 80, barHeight); // filling the bar proporionally to the progress fill(messageColor === 'green' ? '#4CAF50' : messageColor === 'red' ? '#F44336' : '#2196F3'); rect(0, barY, (width - 80) * progress, barHeight); fill(255); textAlign(CENTER, CENTER); textSize(16); text(serialMessage, (width - 80) / 2, barY + barHeight / 2); } //-------------------GRID SET UP------------------------------------- function drawGrid() { for (let i = 0; i < grid.length; i++) { let sq = grid[i]; let x = sq.x; let y = sq.y; // highlighting square by a black stroke and appending them to the sequence. if (showingSequence && i === sequence[showIndex]) { let sw = 6; strokeWeight(sw); stroke(0); let inset = sw / 2; fill(sq.color); rect(x + inset, y + inset, squareSize - sw, squareSize - sw);//fitting the stroke within the square } else { noStroke(); fill(sq.color); rect(x, y, squareSize, squareSize); } } noStroke(); } function initGrid() { grid = []; for (let row = 0; row < gridSize; row++) { for (let col = 0; col < gridSize; col++) { let availableColors = colors.slice(); if (row > 0) { //avoiding the same colors being next to each other in a row let aboveColor = grid[(row - 1) * gridSize + col].color; availableColors = availableColors.filter(c => c !== aboveColor); } if (col > 0) {//avoiding the same colors being next to each other in a col let leftColor = grid[row * gridSize + (col - 1)].color; availableColors = availableColors.filter(c => c !== leftColor); } grid.push({ x: col * squareSize, y: row * squareSize, color: random(availableColors) }); } } } //---------SERIAL COMMUNICATION MANAGEMENT---------------------------- function readSerial(data) { if (!inputEnabled || gameOver) return; let colorClicked = data.trim().split(',').pop().trim();//reading the serial port for the color pressed and printed out by arduino let expectedIndex = sequence[playerInput.length]; let expectedColor = grid[expectedIndex].color;// checking if the colors match if (colorClicked === expectedColor) { playerInput.push(expectedIndex); correctSound.play(); updateSerialMessage('Correct', 'green');//updating the message. if (playerInput.length === sequence.length) { inputEnabled = false; setTimeout(nextRound, 1000); } } else { handleIncorrect('Incorrect'); } } // checking if the pattern by the player is incorrect function handleIncorrect(message) { incorrectSound.play(); updateSerialMessage(message, 'red'); lives--; playerInput = []; inputEnabled = false; if (lives <= 0) { // they have no more lives end game gameOver = true; spawnConfetti(); } else { setTimeout(replayRound, 1500); } } //playing the next round when the player gets it right function nextRound() { currentRound++; playerInput = []; if (sequence.length >= grid.length) { //checking if all the squares have been matched gameOver = true;// end game if true spawnConfetti(); return; } sequence.push(floor(random(grid.length)));// append one more random square if the game is not over showingSequence = true; showIndex = 0; showTimer = millis(); } // Repeating the round when the player gets it wrong function replayRound() { playerInput = []; showingSequence = true; showIndex = 0; showTimer = millis(); } function drawBackgroundGradient() { for (let y = 0; y < height; y++) { let c = lerpColor(color(255), color(200, 220, 255), map(y, 0, height, 0, 1)); stroke(c); line(0, y, width, y); } } //-----------------CONFETTI FOR GAME OVER------------------------------------ function spawnConfetti() { confetti = []; for (let i = 0; i < 100; i++) { confetti.push({ x: random(width), y: random(-200, 0), speed: random(2, 5), size: random(5, 10), color: random(colors) }); } } function drawConfetti() { for (let c of confetti) { fill(c.color); noStroke(); ellipse(c.x, c.y, c.size); c.y += c.speed; if (c.y > height) { c.y = random(-100, 0); c.x = random(width); } } } function drawGameOverArt() { for (let i = 0; i < sequence.length; i++) { fill(grid[sequence[i]].color); ellipse(50 + (i % 10) * 25, 50 + floor(i / 10) * 25, 20); } } //-------------- RESETTING THE GAME------------------------------------------ function resetGame() { lives = 3; sequence = []; playerInput = []; showingSequence = false; showIndex = 0; inputEnabled = false; currentRound = 1; gameOver = false; timeLeft = timeLimit; serialMessage = ''; messageColor = 'black'; initGrid(); nextRound(); }
Areas I am proud of and areas for future improvement
I am really proud about how the game graphics turned out. That is; buttons, score recording and representation, start page, game over page, restart logic, “lives” handling and the game progression loop. On the hardware arduino side, I was proud of the carpentry and design of the wooden platform that held the push-buttons. However, I think there’s still more that could have been done on the aspect of aesthetics to make my projects more appealing and attractive. I also need to work on communicating instructions more effectively and intuitively. Also it was in my very initial idea to 3D print so that I can automate a robot following the progress of the game. I was limited however because of the high demand of the 3D printers. I hope to one day finish what I have started.