Week 10 - PC app and programming

Task #10

The task for the tenth week is to program a simple PC app and connect it to a circuit with I/O devices.

Escape Lava Chicken

I do not have much experience with PC apps, so, I chose to do a simple game with Processing and Arduino. The idea is I will control a player in the game using buttons connected to Arduino. The commands will be send over serial line and visualization and the game itself will run on PC.

📟 Arduino
I connected four buttons and one red LED to Arduino Nano board. The wiring is similar as in Week 7. Just the used pins are different. A serial communication is opened to communicate with the Processing game. The buttons seconds commands: UP, DOWN, LEFT, RIGHT.

const int ledPin = 2;
const int buttonRight = 3;
const int buttonLeft = 4;
const int buttonDown = 5;
const int buttonUp = 6;

bool gameOver = false;

void setup() {
    pinMode(ledPin, OUTPUT);
    digitalWrite(ledPin, LOW);

    pinMode(buttonRight, INPUT_PULLUP);
    pinMode(buttonLeft, INPUT_PULLUP);
    pinMode(buttonDown, INPUT_PULLUP);
    pinMode(buttonUp, INPUT_PULLUP);

    Serial.begin(9600);
}

void loop() {
    if (Serial.available()) {
    String incoming = Serial.readStringUntil('\n');
    incoming.trim();
    if (incoming == "GAME_OVER") {
        gameOver = true;
        digitalWrite(ledPin, HIGH);
    }
    }

    if (!gameOver) {
    if (digitalRead(buttonRight) == LOW) {
        Serial.println("RIGHT");
        delay(200);
    }
    if (digitalRead(buttonLeft) == LOW) {
        Serial.println("LEFT");
        delay(200);
    }
    if (digitalRead(buttonDown) == LOW) {
        Serial.println("DOWN");
        delay(200);
    }
    if (digitalRead(buttonUp) == LOW) {
        Serial.println("UP");
        delay(200);
    }
    } else {
    // If game over and button pressed → send RESET
    if (digitalRead(buttonRight) == LOW ||
        digitalRead(buttonLeft) == LOW ||
        digitalRead(buttonDown) == LOW ||
        digitalRead(buttonUp) == LOW) {
        Serial.println("RESET");
        gameOver = false;
        digitalWrite(ledPin, LOW);
        delay(500); // prevent multiple resets
    }
    }
}

💻 Processing
The Processing code implements a 10x10 tile grid and a player - Chicken. The player can move freely in the grid and is controlled by the Arduino buttons (UP, DOWN, LEFT, RIGHT). There are three type of tiles stone (black), melting (yellow) and lava (red) . The goal of the game is to escape lava (red tiles) and survive the longest. The time is measured and highest score is shown after the player burns in lava. The game can be restarted. I also downloaded free music and sounds to make the game more thrilling. The Processing code can be seen below.

import processing.serial.*;
import processing.sound.*;

Serial myPort;

final int gridSize = 10;
final int tileSize = 50;
color[][] gridColors = new color[gridSize][gridSize];

int playerX = 5;
int playerY = 5;

boolean gameOver = false;
boolean welcomeScreen = true; // Flag for the welcome screen
int startTime;
int endTime;
int highestScore = 0;
String bestTime = "";

PImage safeTexture;
PImage warningTexture;
PImage[] lavaTextures;

// Sounds
SoundFile fireSound;
SoundFile deathSound;
SoundFile backgroundMusic;

void settings() {
    size(gridSize * tileSize, gridSize * tileSize);
}

void setup() {
    printArray(Serial.list());
    myPort = new Serial(this, Serial.list()[2], 9600);
    myPort.bufferUntil('\n');

    safeTexture = createSafeTexture();
    warningTexture = createWarningTexture();
    lavaTextures = createLavaTextures();
    
    fireSound = new SoundFile(this, "fire.aiff");
    deathSound = new SoundFile(this, "death.wav");
    backgroundMusic = new SoundFile(this, "drama.mp3");

    backgroundMusic.loop();
}

void draw() {
    background(30);

    if (welcomeScreen) {
    showWelcomeScreen();  // Show the welcome screen
    } else if (!gameOver) {
    updateTiles();
    drawGrid();
    drawPlayer();
    detectDanger();
    showTimer();
    } else {
    showGameOver();
        // Blocking delay for 2 seconds
        delay(1000);  // 2000 milliseconds = 2 seconds
    }
}

void updateTiles() {
    if (frameCount % 15 == 0) { // faster tile change
    int randX = int(random(gridSize));
    int randY = int(random(gridSize));
    if (gridColors[randX][randY] == color(0, 0, 0)){
        gridColors[randX][randY] = color(255, 255, 0); // warning
    } else if (gridColors[randX][randY] == color(255, 255, 0)) {
        gridColors[randX][randY] = color(255, 0, 0);   // lava
    } else {
        float r = random(1);
        if (r>0.9) {
            gridColors[randX][randY] = color(0, 0, 0); // back to stone
        }
    }
    }
}

void drawGrid() {
    for (int i = 0; i < gridSize; i++) {
    for (int j = 0; j < gridSize; j++) {
        color tileColor = gridColors[i][j];
        
        if (tileColor == color(255, 255, 0)) {
        image(warningTexture, i * tileSize, j * tileSize);
        } else if (tileColor == color(255, 0, 0)) {
        int frame = int(frameCount/5) % lavaTextures.length;
        image(lavaTextures[frame], i * tileSize, j * tileSize);
        } else {
        image(safeTexture, i * tileSize, j * tileSize);
        }
        
        stroke(100);
        noFill();
        rect(i * tileSize, j * tileSize, tileSize, tileSize);
    }
    }
}

void drawPlayer() {
    float px = playerX * tileSize + tileSize/2;
    float py = playerY * tileSize + tileSize/2;

    // Draw chicken (simple)
    pushMatrix();
    translate(px, py);
    noStroke();
    
    // Legs
    stroke(255, 150, 0); // Orange color for legs
    strokeWeight(3);
    line(-5, 15, -5, 20); // Left leg
    line(5, 15, 5, 20);   // Right leg
    
    noStroke();
    
    // Body
    fill(255, 255, 255);
    rectMode(CENTER);
    rect(0, 0, tileSize*0.6, tileSize*0.6);
    
    
    // Head (smaller block)
    fill(255, 0, 0);
    rect(0, -tileSize*0.35, tileSize*0.4, tileSize*0.1); // Head
    rectMode(CORNER);
    
    // Beak
    fill(255, 150, 0);
    triangle(10, -5, 15, 0, 10, 5);
    
    // Eye
    fill(0);
    ellipse(5, -8, 5, 5);
    
    popMatrix();
    
    // Check if standing on lava
    if (gridColors[playerX][playerY] == color(255, 0, 0)) {
    gameOver = true;
    endTime = millis();
    myPort.write("GAME_OVER\n");
    deathSound.play();
    backgroundMusic.stop();
    }
}


void serialEvent(Serial p) {
    String input = p.readStringUntil('\n');
    input = trim(input);

    if (welcomeScreen) {
    // Once any button is pressed, transition to the game
    welcomeScreen = false;
    resetGame(true);
    } else if (!gameOver) {
    if (input.equals("RIGHT")) {
        playerX = min(playerX + 1, gridSize - 1);
        fireSound.play();
    }
    if (input.equals("LEFT")) {
        playerX = max(playerX - 1, 0);
        fireSound.play();
    }
    if (input.equals("DOWN")) {
        playerY = min(playerY + 1, gridSize - 1);
        fireSound.play();
    }
    if (input.equals("UP")) {
        playerY = max(playerY - 1, 0);
        fireSound.play();
    }
    } else {
    // Restart if any button is pressed
    resetGame(false);
    }
}

void resetGame(boolean onStart) {
    playerX = 4;
    playerY = 4;
    gameOver = false;
    startTime = millis();
    if (!onStart) {
    backgroundMusic.loop();
    }
    
    // Clear all tiles
    for (int i = 0; i < gridSize; i++) {
    for (int j = 0; j < gridSize; j++) {
        gridColors[i][j] = color(0);
    }
    }
    
    myPort.write("RESET\n");
}

void showGameOver() {
    background(0);
    textAlign(CENTER, CENTER);
    fill(255, 50, 50);
    textSize(24);
    int score = (endTime - startTime);
    if (score > highestScore) {
    highestScore = score;
    bestTime = (score/1000) + "." + ((score%1000)/10);
    }
    text("🔥 GAME OVER! 🔥\nYou survived " + (score/1000) + "." + ((score%1000)/10) + " seconds.\nBest time " + bestTime + " seconds\nPress any button to restart!", width/2, height/2);
}

void showTimer() {
    fill(255);
    textAlign(LEFT, TOP);
    textSize(16);
    int currentTime = millis();
    float seconds = (currentTime - startTime) / 1000f;
    text("Time: " + nf(seconds, 0, 1) + "s", 10, 10);
}

// --- Textures --- 
PImage createSafeTexture() {
    PImage img = createImage(tileSize, tileSize, RGB);
    img.loadPixels();
    for (int i = 0; i < img.pixels.length; i++) {
    float noiseVal = noise(i * 0.01f);
    img.pixels[i] = color(20 + noiseVal*20, 20 + noiseVal*20, 20 + noiseVal*20);
    }
    img.updatePixels();
    return img;
}

PImage createWarningTexture() {
    PImage img = createImage(tileSize, tileSize, RGB);
    img.loadPixels();
    for (int i = 0; i < img.pixels.length; i++) {
    float noiseVal = noise(i * 0.01f);
    img.pixels[i] = color(255, 255, 150 + noiseVal*50);
    }
    img.updatePixels();
    return img;
}

PImage[] createLavaTextures() {
    PImage[] frames = new PImage[4];
    for (int f = 0; f < frames.length; f++) {
    PImage img = createImage(tileSize, tileSize, RGB);
    img.loadPixels();
    for (int i = 0; i < img.pixels.length; i++) {
        float noiseVal = noise(i * 0.01f + f*0.1f);
        img.pixels[i] = color(255, noiseVal*100, 0);
    }
    img.updatePixels();
    frames[f] = img;
    }
    return frames;
}

void detectDanger() {
    boolean danger = false;

    // Check only the vertical and horizontal neighbors (not diagonals)
    int[] dx = {-1, 1, 0, 0};  // Left, Right, Up, Down
    int[] dy = {0, 0, -1, 1};  // Left, Right, Up, Down

    for (int i = 0; i < dx.length; i++) {
    int nx = playerX + dx[i];
    int ny = playerY + dy[i];
    
    // Make sure the neighbor is within the grid boundaries
    if (nx >= 0 && nx < gridSize && ny >= 0 && ny < gridSize) {
        // Check if the neighboring tile is a lava tile (red color)
        if (gridColors[nx][ny] == color(255, 0, 0)) {
        danger = true;
        }
    }
    }

    // If danger is true (i.e., the player is next to lava), show danger effect
    if (danger) {
    fill(255, 0, 0, 50);  // Red color with transparency for the danger effect
    rect(0, 0, width, height);  // Overlay danger effect on screen
    }
}

void showWelcomeScreen() {
    background(0);
    textAlign(CENTER, CENTER);
    fill(255, 204, 0);
    textSize(40);
    text("Escape Lava Chicken", width/2, height/3);
    
    fill(255);
    textSize(24);
    text("Press any button to start", width/2, height/2);
}

🎮 Gameplay
A short video showing how the game is played.