Assignment 6: Imitation is Flattery

For my midterm project, I decided to recycle one of my weekly assignment’s idea. Instead of starting from scratch and making a half-done game or experience, I chose to work and improve on my previous work, which is Om-Nom-Nom! Of course, I’ll rename it and give credit to the creator, Pac-man, which Toru Iwatani designed. Now, it’s my turn to ATTEMPT in recreating this masterpiece and giving the users a real experience of Pac-Man.

Now in Week 6, I learnt a lot in the JavaScript language, from adding sprites to create animation to enabling computer vision to detect our facial features. Nothing too complex, I decided to use basic sprites of the Pac-Man avatar I found on the internet. I also decided to use a maze image from the internet for my game, but then I realised it wasn’t as easy as I thought.

I ended up creating a home screen for the game, and separate code just for the game. I didn’t, in fact, use sprites and just ended up creating my own ghosts and pacman avatar using shapes.

This technically isn’t an original idea, and yes I used tons of sources ranging from YouTube videos to ChatGPT, but I learned a lot from this lesson. Such as, how to make the blinking power-ups and how they increase the score. That’s a part of the code I’m actually proud of here. I’m also very content with the “button” situation I created here, where instead of using the code actually creating buttons, I just created a shape with text on it, and if the mouse if clicked within that shape, the screen changes.

function homescreen() {
  image(homepage,0,-45,width,height+85)
  
  stroke("white")
  strokeWeight("5")
  fill("red")
  rect(255,78,360,62,30,30,30,30)
  fill("white")
  noStroke()
  textSize(60)
  textStyle(BOLD)
  textFont('Courier New');
  text("HOW TO",330,125)
  
  fill("black")
  rect(713,349,40)
  
  fill("blue")
  stroke("white")
  strokeWeight("5")
  rect(305,450,260,62,30,30,30,30)
  fill("white")
  noStroke()
  textSize(65)
  textStyle(BOLD)
  textFont('Courier New');
  text("PLAY",350,500)
  
}


function draw() {
  
   gamestate = homescreen();
  
  if (mouseX > 255 && mouseX < 615 && mouseY > 78 && mouseY < 140) 
  { 
    stroke("white")
    strokeWeight("5")
    fill("blue")
    rect(255,78,360,62,30,30,30,30)
    fill("green")
    noStroke()
    textSize(60)
    textStyle(BOLD)
    textFont('Courier New');
    text("HOW TO",330,125)
    
  }
   if (mouseX > 305 && mouseX < 565 && mouseY > 450 && mouseY < 512) 
  { 
    fill("red")
    stroke("white")
    strokeWeight("5")
    rect(305,450,260,62,30,30,30,30)
    fill("green")
    noStroke()
    textSize(65)
    textStyle(BOLD)
    textFont('Courier New');
    text("PLAY",350,500)
    
  } 
}


  function mouseClicked() //when the mouse is clicked 
{ 
  if (mouseX > 255 && mouseX < 615 && mouseY > 78 && mouseY < 140) 
  {
    gamestate = howTo();
  }
  
  if (mouseX > 305 && mouseX < 565 && mouseY > 450 && mouseY < 512) 
  {
    gamestate = drawMaze();
  }
}

Another part of my code I really like is how I made my pac-man actually change direction according to the key pressed. Such as, when I press the UP key, the character faces upward.

class Pacman {
  constructor(x, y, diameter) {
    this.x = x;
    this.y = y;
    this.d = diameter;
  }
  
  show() {
    fill(220, 220, 50);
    let theta = PI/3*sq(sin(thetaoff))
    if(speedY < 0) {
    arc(this.x, this.y, this.d, this.d, -theta - PI/6, theta + 7*PI/6); 
      } else if(speedY > 0) {
          arc(this.x, this.y, this.d, this.d, -7*PI/6 - theta, theta + PI/6);
      } else if(speedX < 0){
          arc(this.x, this.y, this.d, this.d, theta + PI, -theta + PI);
      } else if(speedX > 0){
          arc(this.x, this.y, this.d, this.d, theta, -theta);
      } else {
          if(dir == 0) {
            arc(this.x, this.y, this.d, this.d, -theta - PI/6, theta + 7*PI/6); 
          } else if(dir == 1) {
              arc(this.x, this.y, this.d, this.d, -7*PI/6 - theta, theta + PI/6);
          } else if(dir == 2){
              arc(this.x, this.y, this.d, this.d, theta + PI, -theta + PI);
          } else if(dir == 3){
              arc(this.x, this.y, this.d, this.d, theta, -theta);
          } else {
              arc(this.x, this.y, this.d, this.d, theta, -theta);
          }
      }
    thetaoff += 0.1;
  }
  
  move() {
    checkNeighbors(this.x, this.y, neighbors);
    if(this.y % w == 0 && this.x % w == 0) {
      if(neighbors[3] || neighbors[1]) {
        speedX = 0;   
      }
      if(neighbors[0] || neighbors[2]) {
        speedY = 0;   
      }
      if(dir == 2 && neighbors[3] == false){
        speedX = -w/10;
        speedY = 0;
      } 
      if(dir == 3 && neighbors[1] == false){
        speedX = w/10;
        speedY = 0;
      } 
      if(dir == 0 && neighbors[0] == false){
        speedY = -w/10;
        speedX = 0;
        } 
      if(dir == 1 && neighbors[2] == false) {
        speedY = w/10;
        speedX = 0;
      }
  }
      this.x += speedX;
      this.y += speedY;
    //looping the pacman through the canvas
    if(this.x < - w/2) {
      this.x = width + w/2;
    }
    if(this.x > width + w/2) {
      this.x = -w/2;
    }
    if(this.y < - w/2) {
      this.y = height + w/2;
    }
    if(this.y > height + w/2) {
      this.y = -w/2;
    }
  } 
}

A problem I ran into was integrating the sprites. I wasn’t sure how to use a loop, just like how we did with that one “walking” animation we did in class, hence I opted to use basic shapes like circles and used mathematical logic to get the proper shape and animation for the pacman and ghosts.

Overall, I think this a big improvement compared to the weekly assignment I submitted, but I do intend on improving this.

Rama’s Midterm: Tetris

The Idea

For my Midterm I had my take on the Tetris game which I’ve been a fan of for years. I want to maintain the arcade/video game feel to it so I kept that in mind while creating my game. It consists of various components including shapes, grids, timers, and user input handling. The game aims to control the falling tetrominoes, rotating and moving them to form complete horizontal lines to clear rows. Once 5 rows have been cleared, the game levels up and the tetrominoes fall faster giving the player less time to find a good fit to clear the rows.

How It Works and Highlights

The project leverages object-oriented programming principles to organize code into manageable classes such as Tetris, Timer, and T-Grid. This modular approach enhances code readability and maintainability. The game mechanics are well-implemented, with smooth tetromino movement, collision detection, and row-clearing functionality. The user interface is intuitive, providing clear visual feedback through colorful shapes and text. The inclusion of background music and sound effects enhances the overall gaming experience. I created the background image and the first-page using elements on Canva.

displayGrid(pg, x, y, w, h, pallette) {
    var nx = this.tGrid.nx;
    var ny = this.tGrid.ny;
    var cw = w / nx;
    var ch = h / ny;
    
    // Render background
    for (var gy = 0; gy < ny; gy++) {
        for (var gx = 0; gx < nx; gx++) {
            var cx = x + gx * cw;
            var cy = y + gy * ch;
            pg.stroke(210);
            if ((gx & 1) == 1) {
                pg.fill(250);
            } else {
                pg.fill(240);
            }
            pg.rect(cx, cy, cw, ch);
        }
    }
    
    // Render foreground (tetrominoes)
    for (var gy = 0; gy < ny; gy++) {
        for (var gx = 0; gx < nx; gx++) {
            var cx = x + gx * cw;
            var cy = y + gy * ch;
            var valGrid = this.tGrid.getGridVal(gx, gy);
            if (valGrid > 0) {
                pg.stroke(0);
                var rgb = pallette[valGrid % pallette.length];
                pg.fill(rgb[0], rgb[1], rgb[2]);
                pg.rect(cx, cy, cw, ch);
            }
        }
    }
    
    // Render active tetromino shape
    var ks = this.tGrid.shapeSize;
    var kr = ceil(this.tGrid.shapeSize / 2.0);
    for (var ky = 0; ky < ks; ky++) {
        for (var kx = 0; kx < ks; kx++) {
            var gx = this.tGrid.sx + kx - kr;
            var gy = this.tGrid.sy + ky - kr;
            var cx = x + gx * cw;
            var cy = y + gy * ch;
            var valShape = this.tGrid.getShapeVal(kx, ky);
            if (valShape != 0) {
                pg.stroke(0);
                var rgb = pallette[valShape % pallette.length];
                pg.fill(rgb[0], rgb[1], rgb[2]);
                pg.rect(cx, cy, cw, ch);
            }
        }
    }
}

One really cool part of the code is how it draws the game grid. It splits the screen into smaller squares to represent each cell of the grid. Then, it fills these squares with colors to show the background, the falling shapes, and the shapes that have already landed. It does this by going through each cell of the grid and deciding what color it should be based on the game’s state. This method makes sure everything looks neat and organized on the screen, giving players a clear view of the game.

Areas for Improvement and Challenges

One area for improvement could be enhancing the visual appeal of the game by adding animations for tetromino movements and row clearing. Additionally, implementing more advanced gameplay features such as different game modes, power-ups, or multiplayer functionality could increase player engagement. Some challenges were adding a sound effect once every tetromino lands but I had several issues with it, also I was not able to get the tetromino count to stop once the game was over.

Design Inspiration

I took inspiration from EA’s Tetris mobile app game, here’s how it looks:

And here’s mine:

Credits

Sound Track: https://www.youtube.com/watch?v=NmCCQxVBfyM

Main Menu page: https://www.canva.com/

Code :https://www.youtube.com/@easywebsify

Additional Code Assistance: Chat GPT.

Final Sketch

 

Midterm Project – Shaikha AlKaabi

 

When I first started working on my midterm project, I really wanted to make a game about planting flowers. It sounded fun and I thought it would be something different. But as I kept working on it and thinking more about how it would work, it started to feel less like a game and more like a simulator for planting. It wasn’t as fun as I had hoped it would be, and that was really important to me. So, I decided to change it.

I switched my idea to “Back and Forth.” At first, I thought about making a volleyball game. It seemed like a good idea because it was about moving back and forth, which is what I wanted. But the more I thought about it, the more I realized that making it feel realistic and fun at the same time was going to be really hard. So, I started looking for something similar but a bit simpler, and that’s when I landed on a ping-pong game. Ping-pong still had that back and forth action which I liked, but it felt more doable and still really fun. That’s how I ended up with the idea for my project.


Challenges:

The tricky  part was figuring out how to make the ball start from the center of the screen. But then I remembered the width /2 and height/2 that we used to center things and I used that to position the ball at the center whenever it went out of the screen:

reset() { // Reset the ball’s position and speed this.x = width / 2; this.y = height / 2; this.xSpeed = random([-5, 5]); // Increased speed this.ySpeed = random([-5, 5]); // Increased speed }

Overall, I’m quite happy with my game.

let leftPaddle;
let rightPaddle;
let ball;
let leftScore = 0;
let rightScore = 0;
let gameState = "start";
let tableColor;
let startScreenImage;
let gameOverSound;
let paddleHitSound;

function preload() {
  startScreenImage = loadImage('background-image.jpg');
  gameOverSound = loadSound('game over.mp3');
  paddleHitSound = loadSound('paddle.mp3');
}

function setup() {
  createCanvas(800, 400);
  textAlign(CENTER, CENTER);
  textSize(40);
  leftPaddle = new Paddle(true);
  rightPaddle = new Paddle(false);
  ball = new Ball();
  tableColor = color(138, 43, 226);
}

function draw() {
  background(tableColor);
  drawNet();
  if (gameState === "start") {
    showStartScreen();
  } else if (gameState === "play") {
    leftPaddle.show();
    rightPaddle.show();
    ball.show();
    leftPaddle.update();
    rightPaddle.update();
    ball.update();
    ball.checkPaddleCollision(leftPaddle);
    ball.checkPaddleCollision(rightPaddle);
    ball.checkScore();
    drawScore();
  } else if (gameState === "gameover") {
    showGameOverScreen();
  }
}

function keyPressed() {
  // Start the game when ENTER is pressed
  if (keyCode === ENTER && gameState === "start") {
    gameState = "play";
  }
  // Reset the game when ENTER is pressed after game over
  else if (keyCode === ENTER && gameState === "gameover") {
    gameState = "start";
    leftScore = 0;
    rightScore = 0;
    ball.reset();
  }

  // Control the right paddle with arrow keys
  if (keyCode === UP_ARROW) {
    rightPaddle.ySpeed = -5;
  } else if (keyCode === DOWN_ARROW) {
    rightPaddle.ySpeed = 5;
  }

  // Control the left paddle with 'W' and 'S' keys
  if (key === 'w' || key === 'W') {
    leftPaddle.ySpeed = -5;
  } else if (key === 's' || key === 'S') {
    leftPaddle.ySpeed = 5;
  }
}

function keyReleased() {
  // Stop the left paddle when 'W' or 'S' key is released
  if (key === 'w' || key === 'W' || key === 's' || key === 'S') {
    leftPaddle.ySpeed = 0;
  }

  // Stop the right paddle when arrow keys are released
  if (keyCode === UP_ARROW || keyCode === DOWN_ARROW) {
    rightPaddle.ySpeed = 0;
  }
}


function keyReleased() {
  // Stop the left paddle when 'W' or 'S' key is released
  if (key === 'W' || key === 'S') {
    leftPaddle.ySpeed = 0;
  }

  // Stop the right paddle when arrow keys are released
  if (keyCode === UP_ARROW || keyCode === DOWN_ARROW) {
    rightPaddle.ySpeed = 0;
  }
}

function drawNet() {
  // Draw the net in the middle of the table
  stroke(255);
  strokeWeight(2);
  for (let i = 0; i < height; i += 20) {
    line(width / 2, i, width / 2, i + 10);
  }
}

function drawScore() {
  // Display the scores below the player names with text indicating it is the score
  fill(255);
  textSize(20);
  textFont("Courier New");
  textAlign(CENTER, CENTER);
  
  // Player 1 score
  stroke(0)
  text("Player 1", width / 4, 90);
  text("Score: " + leftScore, width / 4, 110);

  // Player 2 score
  stroke(0)
  text("Player 2", (3 * width) / 4, 90);
  text("Score: " + rightScore, (3 * width) / 4, 110);
}


function showStartScreen() {
  // Display the start screen with instructions
  image(startScreenImage, 0, 0, width, height);
  fill(138, 43, 226);
  stroke(255); // Outline color
  strokeWeight(2); // Outline weight
  textFont("Courier New");
  textStyle(BOLD);
  textSize(40);
  text("Back and Forth", width / 2, height / 2 - 100);
  textSize(20);
  text("Press ENTER to Start", width / 2, height / 2 + 50);
  textSize(15);
  text("Press 'W' to move the left paddle up and 'S' to move it down", width / 2, height / 2 + 125);
  textSize(15);
  text("Press Upward-Arrow to move the right paddle up and Down-Arrow to move it down", width / 2, height / 2 + 150);
}

function showGameOverScreen() {
  // Display the game over screen with the winning player's name
  let winner = leftScore > rightScore ? "Player 1" : "Player 2";
  fill(255);
  stroke(0); // Outline color
  strokeWeight(5); // Outline weight
  textFont("Courier New");
  textSize(40);
  text(winner + " Wins!", width / 2, height / 2 - 40);
  textSize(20);
  text("Press ENTER to Play Again", width / 2, height / 2 + 40);
}

class Paddle {
  constructor(isLeft) {
    this.w = 10;
    this.h = 80;
    this.y = height / 2 - this.h / 2;
    if (isLeft) {
      this.x = 20;
    } else {
      this.x = width - 30;
    }
    this.ySpeed = 0;
  }

  show() {
    // Draw the paddle
    fill(255);
    rect(this.x, this.y, this.w, this.h);
  }

  update() {
    // Update the paddle's position based on key inputs
    this.y += this.ySpeed;
    this.y = constrain(this.y, 0, height - this.h);
  }
}

class Ball {
  constructor() {
    this.reset();
  }

  reset() {
    // Reset the ball's position and speed
    this.x = width / 2;
    this.y = height / 2;
    this.xSpeed = random([-5, 5]); // Increased speed
    this.ySpeed = random([-5, 5]); // Increased speed
  }

  show() {
    // Draw the ball
    fill(255);
    ellipse(this.x, this.y, 10, 10);
  }

  update() {
    // Update the ball's position
    this.x += this.xSpeed;
    this.y += this.ySpeed;
    if (this.y < 0 || this.y > height) {
      this.ySpeed *= -1;
    }
  }

  checkPaddleCollision(paddle) {
    // Check for collision with paddles and change direction
    if (this.x - 5 < paddle.x + paddle.w && this.x + 5 > paddle.x && this.y - 5 < paddle.y + paddle.h && this.y + 5 > paddle.y) {
      this.xSpeed *= -1;
      paddleHitSound.play();
    }
  }

  checkScore() {
    // Check for scoring and end game condition
    if (this.x < 0) {
      rightScore++;
      this.reset();
      gameOverSound.play();
    } else if (this.x > width) {
      leftScore++;
      this.reset();
      gameOverSound.play();
    }
    if (leftScore >= 5 || rightScore >= 5) {
      gameState = "gameover";
    }
  }
}

 

Week 6: Midterm Project – Save the Butterfly

Concept 

As I shared in last week’s blog post, I wanted my project to be a gamified story. I also wanted to center it around a butterfly, an extension of a theme I have been committed to for the past few weeks. Additionally, the narrative created would convey my own desire to reunite with my family – a goal that I hope to achieve eventually in my lifetime. This could be seen in the final scene unlocked if the player passes the levels successfully. The butterfly returns to her family of four, which is the number of members in my own family. The storyline and flow of the game go like this:

    1. A butterfly finds herself lost in the city on a rainy day. She flutters through the window of the main player’s room.
    2. the player is prompted to help the butterfly find her way back to her home and family, going through game levels in the city and forest and avoiding (jumping over) obstacles to preserve their health. Here the player has a chance to replenish their health by collecting potions.
    3. If the player manages to successfully complete the mission, they unlock the final scene, in which the butterfly is finally reunited with her family.
    4. If the player loses, they are prompted to restart the game.

In the making of this, I emphasized the animations for an increased focus on the narrative component over the game one. I spent a lot of time playing with different parameters and finding ways to control time and the movement of sprites in response to changes in their environment. The storyboarding I had done last week greatly aided in visualizing how I wanted the animations to eventually look.

Implementation and Parts I am Most Proud of

In terms of execution, I implemented everything as a class based on the rough UML structure I sketched out in my last blog. The main Gameclass had methods for each level and its attributes were objects instantiated from the Player, Butterfly, EnemyPotion, and HealthBar classes. Certain classes were also abstracted from parent classes using inheritance (such as the Playerclass inheriting from a parent Sprite class that has basic attributes and methods shared between all its child classes). Each level/scene is separated by overlays, where the user is prompted to click anywhere on the screen to continue to the next stage of the experience. In terms of assets, all sounds, images, and fonts were sourced from the following open-source community/free platforms:

  1. https://freesound.org/
  2. https://opengameart.org/
  3. https://www.free-stock-music.com/
  4. https://www.dafont.com/

For the mechanics of the game, the Player sprite is placed at the lower left corner of the screen. Its main movement, jumping, is only triggered when the player presses the Space bar. Jumping occurs by setting the vertical velocity to a certain jump power attribute. As the player falls down, the player’s velocity is incremented by an acceleration due to gravity attribute. The player also has the ability to double jump once while in air, which comes in handy if a flock of enemies is headed its way. In terms of the collision detection mechanism, a collision occurs when the distance between the center of the player and that of an enemy object is less than the sum of their respective radii (minus a certain amount to account for the free pixels in the sprite images). Below is the code for the collision detection mechanism, which is a Player class method:

  detectCollision(obj, offset=30) {
    if (!obj.collided) {
      // get distance between the center of the character and that of the enemy object
      let objHeight = obj.h;
      let objWidth = obj.w;
      let playerWidth = this.w;
      let playerHeight = this.h;
      
      // update height and width based on resized parameters if the player/object was resized 
      if (obj.resize) {
        objHeight = obj.resize_y;
        objWidth = obj.resize_x;
      }

      if (this.resize) {
        playerWidth = this.resize_x;
        playerHeight = this.resize_y;
      }
      let centerX = this.x + playerWidth / 2;
      let centerY = this.y + playerHeight / 2;

      let d = dist(
        centerX,
        centerY,
        obj.x + objWidth / 2,
        obj.y + objHeight / 2
      );
      
      // collision detected
      // distance is less than the sum of objects' radii
      // minus a distance to account for free pixels in the sprite images

      if (d < playerWidth / 2 + objWidth / 2 - offset) {
        if (!obj.potion) { // lose health if the object is an enemy
          loseHealth.play();
          this.currHealth -= obj.damage; 
          
        } else if (obj.potion && this.currHealth < game.healthBar.maxHealth) { // regain health if the object is a potion 
          gainHealth.play();
          this.currHealth += 1;
        }
        obj.collided = true; // set object collided to true
        return true; // return true if collision is detected 
      }
    } else {
      return false; // return false if collision is not detected 
    }
  }
}

An implementation highlight that I think is worth noting is the use of the smoothing algorithm in simulating the butterfly’s movement in the first scene as she makes her way from the window to the desk. This was implemented as part of the Butterfly class.

move(targetX, targetY) {
   // move butterfly toward destination -- smoothing algorithm
   if (this.moving) {
     this.x += (targetX - this.x) * 0.009;
     this.y += (targetY - this.y) * 0.007;
   }

There are quite a few things that I am proud of in the implementation of this project. The first is the emotional feel of the overall experience. I made sure that the combination of animations created, the sounds chosen, the font, and the color palettes – going from night to day and from city to nature – created a wholesome backdrop to the game engulfed within. I also am proud that I made sure to bring to life the storyboard from last week’s blog. Finally, more than anything, I loved working on the animated start and final scenes (code shown below) and how the flow from animation to instructions to game and then to animation again (with transition overlays in between) turned out to be quite seamless.

  firstScene() {
    // show background of the scene 
    image(room[this.firstSceneAnimationStep], 0, 0, width, height);
    // change the background based on frameCount to show animated changes in the player's room
    if (frameCount % 7 == 0) {
      this.firstSceneAnimationStep = (this.firstSceneAnimationStep + 1) % 6;
    }
    // show player
    this.player.show();
    // show butterfly, passing in rotation paramaters 
    this.butterfly.show(100, 170);
    
    // player faces the butterfly once it is 40 pixels from the point (100, 170)
    if (dist(this.butterfly.x, this.butterfly.y, 100, 170) < 40) {
      this.player.dir = 1; 
      // move player toward butterfly once it lands on the desk
      if (
        dist(this.player.x, this.player.y, this.butterfly.x, this.butterfly.y) >
        50
      )
        this.player.x -= 0.6;
      else { // once the player is close to the butterfly, display instructions overlay 
        this.gameMode = 1;
        let text = `This poor butterfly seems to be far away from home! 
You have to help her find her way back to her family!

The first step on your journey is to go through the city. 
Beware the obstacles on your way. 

Press the Space bar to jump. 
Collect potions to replenish your health. 

Click anywhere on the screen if you are 
ready to embark on the journey!`;
        twinkleSound.play(); // play twinkle sound
        this.overlay(text);
      }
    }
  }

  finalScene() {
    // display the flower field background
    image(flowerField, 0, 0, width, height);
    this.player.dir = 3; // change direction so that the player' front side is facing the flower field
    this.levelButterfly.dir = 3;

    // resize the butterfly and player to show advancing movement 
    if (frameCount % 10 == 0) {
      this.player.resize_x -= 4;
      this.player.resize_y = this.player.resize_x / 0.75;
    }
    if (frameCount % 15 == 0) {
      this.levelButterfly.resize_x -= 1.5;
      this.levelButterfly.resize_y = this.levelButterfly.resize_x * 2;
    }


    this.resizeObject(this.player);
    this.resizeObject(this.levelButterfly);
    
    // show background butterflies 
    for (let i = 0; i < 4; i++) {
      this.butterflyFamily[i].show();
    }
    
    // stop the animation once the player's y position is less than 
    // 255 pixels 
    if (this.player.y <= 225) {
      this.player.moving = false;
      this.levelButterfly.moving = false;
      // change into overlay, prompting the player to restart the game
      this.gameMode = 5;
      let text = `Click anywhere to restart the game!`;
      twinkleSound.play();
      this.overlay(text);
      noLoop();
    }
    
    // move player and butterfly diagonally across the screen to move 
    // upward through the field 
    this.player.moveDiagonally();
    this.levelButterfly.moveDiagonally();
  }
Challenges Encountered and Proposed Improvements

One of the challenges I encountered was during the implementation of the final scene animation, where the main player and the butterfly had to be iteratively resized to create the animation of moving into the distance. I found that using the resize() method consecutively blurred the images and I, thus, had to find another way to resize them. After some googling, I found a way to resize the image by creating a resized image object and copying the pixels into the resized image as a way to avoid calling the resize() method:

resizeObject(obj) {
  // scale with copy -- 
https://stackoverflow.com/questions/72368646/images-blur-with-resize-with-p5-js
  
  // create an image object with the resized parameters
  let resizedImg = createImage(int(obj.resize_x), int(obj.resize_y));
  
  // get the image to resize from the object's sprites array 
  let srcImg = obj.sprites[obj.dir][obj.step];
  
  // copy the pixels from the source image to the resized image 
  resizedImg.copy(
    srcImg,
    0,
    0,
    srcImg.width,
    srcImg.height,
    0,
    0,
    obj.resize_x,
    obj.resize_y
  );
  
  // rotate object if needed and display the resized image
  if (obj.rotated) {
    push();
    translate(obj.x, obj.y);
    rotate(radians(obj.rotationAngle));
    image(resizedImg, 0, 0);
    pop();
  } else { 
    image(resizedImg, obj.x, obj.y);
  }
}

Another challenge was the length of the Enemy arrays created in the constructor() of the game class for each level as it modulated the duration of each level. The more enemies there are in a level, the longer its duration, as the condition for termination was when a particular level Enemy array became empty. However, I found that the more enemies there were in a level, the more slow and laggy the movements became, possibly due to the number of objects that had to be drawn on the screen. I attempted to fix this by ensuring that objects are only drawn when they are within the screen bounds as their locations are initialized randomly off-screen. While this helped a little, the problem remained. So a future improvement could be to look into this further and perhaps choose lighter/smaller sprites to display or have a different initialization mechanism.

Additionally, here are a few other ideas to elevate the current version a little more:

  • Add some more levels, perhaps in between the city and the forest (e.g. a suburban in-between area/ or a highway).
  • Add different types of potions with varying degrees of health replenishment (the stronger the potion, the rarer it is). This should be accompanied by an increase in the difficulty of the game, e.g. more enemy sprites, faster enemy sprite movements, or an increase in the damage attribute of certain sprites.
  • Add some feedback, such as a jitter, when a collision occurs with an enemy object. An extension of this would be the possibility of annihilating an enemy if the Player sprite jumps on its head (the same way Gombas are annihilated in Super Mario).
Final Sketch

Midterm Project Presentation/Documentation – Jihad Jammal

Concept:

Taking a fresh and creative approach to the beloved Snake game format, my concept introduces players to a comically troublesome dog, embarking on a homework-eating spree. This idea reinvents the classic game mechanic, where instead of a snake that grows with each item consumed, we have a dog that enlarges with every piece of homework it swallows. This creative twist not only injects a dose of humor into the gameplay but also layers in strategic depth and a ticking clock element, with a 30-second time limit creating a sense of urgency.

The concept of this game particularly appeals to me due to my personal fascination with those almost mythical stories we’ve all heard growing up—tales of pets, especially dogs, eating homework and becoming the convenient scapegoat for undone assignments. There’s something universally relatable and humorously nostalgic about the idea, regardless of how fanciful these excuses might have seemed. I was inspired to bring these whimsical narratives to life through gameplay, transforming an age-old excuse into an interactive and entertaining experience.

By integrating this playful storyline with the mechanics of the growing dog within a limited timeframe, I aim to capture not just the essence of these stories but also their inherent humor and the frantic, often comical, attempts to salvage what’s left of the homework before it’s too late. It’s a nod to those shared experiences, a blend of reality and exaggeration, that many of us can chuckle at in hindsight.

Embed sketch:

Include code snippets and one or more images

 

function draw() {
    switch (gameState) {
        case "start":
            drawStartScreen();
            break;
        case "play":
        // Start the looped sound if it's not already playing and the game state just switched to "play"
      if (!gameLoopSound.isPlaying()) {
        gameLoopSound.loop();
        gameLoopSound.setVolume(0.5); // Set volume to 50%
      }
            drawPlayScreen();
            // Calculate time left here
            let timePassed = (millis() - timerStartTime) / 1000;
            let timeLeft = max(30 - timePassed, 0);
            
            // Display the timer
            fill(0);
            noStroke();
            textSize(16);
            textAlign(RIGHT, BOTTOM);
            text("Time left: " + timeLeft.toFixed(1), width - 10, height - 10);

            if (timeLeft <= 0) {
                gameState = "gameover";
              if (gameLoopSound.isPlaying()) {
          gameLoopSound.stop();
        }
            }

            // Check if the dog collides with the square and handle scoring
            checkCollisionsAndScore();

            // Display the current score
            displayScore();

            // Handle player movement
            handleMovement();
            break;
        case "instructions":
            drawInstructionsScreen();
            break;
        case "credits":
            drawCreditsScreen();
            break;
        case "gameover":
            drawGameOverScreen();
            break;
    }
}

 

 

Describe how your project works and what parts you’re proud of (e.g. good technical decisions, good game design):

My approach to this project was focused on crafting an engaging and enjoyable game loop that balanced challenge and playability. I was acutely aware that the dog’s speed couldn’t start off too fast, as this would alienate new players, nor could it become too slow as the timer neared its end, as this would sap the excitement from the gameplay. Achieving this balance was critical in keeping players engaged and encouraging them to improve over time. Additionally, I aimed to create a cohesive and immersive game environment. From the main menu’s view of the backyard to the transition into a bird’s-eye view of the entire backyard upon starting the game, players are drawn into a world that feels both expansive and detailed. This seamless movement into the game’s space was designed to make players feel as if they’ve stepped into the backyard themselves, observing the chaos from above.

Furthermore, I’m particularly proud of how I tackled the game’s technical and design challenges. After some experimentation, I managed to modularize many of the game’s features, breaking them down into separate functions that could be easily managed and updated. This was a significant departure from my initial approach, which had all the functionalities crammed into the draw function—a classic case of spaghetti code. This early version was overwhelming and led to analysis paralysis, making debugging a tedious chore. However, by compartmentalizing these features, I not only streamlined the development process but also significantly improved the game’s performance and my ability to debug and expand on the game. This decision towards modular design has been a game-changer, allowing for cleaner code, easier maintenance, and the flexibility to add new features or tweak existing ones with minimal hassle. It’s a technical achievement that has had a profound impact on both the development experience and the overall quality of the game.

Below is some of the modularized code:

function displayGameInfo() {
    let timePassed = (millis() - timerStartTime) / 1000;
    let timeLeft = max(30 - timePassed, 0);
    fill(0);
    noStroke();
    textSize(16);
    textAlign(RIGHT, BOTTOM);
    text("Time left: " + timeLeft.toFixed(1), width - 10, height - 10);

    checkCollisionsAndScore();
    displayScore();
    handleMovement();
}

function checkCollisionsAndScore() {
let hit = rectRectCollision(dog.x - dog.img.width / 2 * dog.scale, dog.y - dog.img.height / 2 * dog.scale, dog.img.width * dog.scale, dog.img.height * dog.scale, whiteSquare.x, whiteSquare.y, whiteSquare.size, whiteSquare.size);
  if (hit) {
    dog.growAndSlow();
    whiteSquare.respawn();
    score++;

    // Play collision sound at 30% volume
    collisionSound.setVolume(0.3); // Set volume to 30%
    collisionSound.play();
  }
}

function displayScore() {
    fill(0);
    stroke(255);
    strokeWeight(2);
    textSize(16);
    textAlign(RIGHT, TOP);
    text("Score: " + score, width - 10, 10);
}

function handleMovement() {
    if (keyIsDown(LEFT_ARROW)) dog.move(-1, 0);
    if (keyIsDown(RIGHT_ARROW)) dog.move(1, 0);
    if (keyIsDown(UP_ARROW)) dog.move(0, -1);
    if (keyIsDown(DOWN_ARROW)) dog.move(0, 1);
}

function draw() {
    switch (gameState) {
        case "start":
            drawStartScreen();
            break;
        case "play":
        // Start the looped sound if it's not already playing and the game state just switched to "play"
      if (!gameLoopSound.isPlaying()) {
        gameLoopSound.loop();
        gameLoopSound.setVolume(0.5); // Set volume to 50%
      }
            drawPlayScreen();
            // Calculate time left here
            let timePassed = (millis() - timerStartTime) / 1000;
            let timeLeft = max(30 - timePassed, 0);
            
            // Display the timer
            fill(0);
            noStroke();
            textSize(16);
            textAlign(RIGHT, BOTTOM);
            text("Time left: " + timeLeft.toFixed(1), width - 10, height - 10);

            if (timeLeft <= 0) {
                gameState = "gameover";
              if (gameLoopSound.isPlaying()) {
          gameLoopSound.stop();
        }
            }

            // Check if the dog collides with the square and handle scoring
            checkCollisionsAndScore();

            // Display the current score
            displayScore();

            // Handle player movement
            handleMovement();
            break;
        case "instructions":
            drawInstructionsScreen();
            break;
        case "credits":
            drawCreditsScreen();
            break;
        case "gameover":
            drawGameOverScreen();
            break;
    }
}

 

Describe some areas for improvement and problems that you ran into (resolved or otherwise):

Reflecting on the project, I see a few avenues for enhancement that could elevate the gameplay experience and address some challenges encountered along the way. In future iterations, I’d love to delve deeper into refining the game’s core loop. Introducing an in-game store where players can buy stat or ability upgrades seems like an exciting direction. Coupling this with the addition of random power-ups, like speed boosts, time extensions, and score multipliers appearing throughout the 30-second gameplay window, would inject more depth and variability into the strategies players might employ. Given the tight time frame players are working with, these elements could introduce pivotal decision-making moments that are both thrilling and consequential, potentially transforming an average round into an extraordinary one.

On the technical side, one particular hurdle I had to navigate involved the game’s scalability, specifically related to the dog’s growing image. As the dog expanded, it reached a point where it would cause the site or the p5.js environment to crash. Despite my efforts, pinning down the precise cause was challenging, leading me to theorize that it stemmed from an excessive strain on resources at any given moment. Under the pressure of deadlines, I opted for a temporary workaround by imposing a maximum size limit on the dog’s growth. This solution, while effective in preventing crashes, is admittedly more of a band-aid than a cure. Moving forward, dedicating time to resolve this issue comprehensively would not only enhance performance but also ensure that the gameplay’s immersive experience remains uninterrupted and fluid for all players.

Credits:

Sounds:

  • https://pixabay.com/sound-effects/dog-barking-70772/
  • https://pixabay.com/sound-effects/crunchy-paper-33625/
  • https://pixabay.com/sound-effects/merx-market-song-33936/

Art:

  • https://www.freepik.com/free-vector/sticker-template-dog-cartoon-character_20496955.htm#fromView=search&page=1&position=44&uuid=59df1125-5f43-4d72-9319-72464f25d11c
  • https://www.pinterest.com/pin/4433299622478035/
  • https://www.freepik.com/free-vector/scene-backyard-with-fence_24552372.htm
  • https://www.freepik.com/free-vector/seamless-textured-grass-natural-grass-pattern_11930799.htm#query=cartoon%20grass%20texture&position=0&from_view=keyword&track=ais&uuid=7951681d-d20d-4bad-8ce4-d50239a26e77

Code Assistance:

Chatgpt – https://chat.openai.com/

 

Full Code:

let dog; // This will be an instance of MovableImage
let bgImage; // This variable will hold your background image
let startScreenImage; // This variable will hold your start screen image
let gameState = "start"; // "start" for the start screen, "play" for the gameplay
let whiteSquare; // Instance of WhiteSquare
let customFont; // Variable for the custom font
let score = 0; // Tracks the number of times the dog collides with the white square
let hwImage; // This will hold the image of the homework paper
let timerStartTime;
// Global variables for button dimensions
let buttonWidth = 100;
let buttonHeight = 40;
let gameLoopSound;
let collisionSound;


class WhiteSquare {
constructor() {
    this.size = 50; // This can be adjusted based on the actual size of your image
    this.x = 0;
    this.y = 0;
    this.respawn();
  }

  display() {
    // Use the homework image instead of a white rectangle
    image(hwImage, this.x, this.y, this.size, this.size);
  }

  respawn() {
    // Cap the additional distance increase once the score reaches 32
    let cappedScore = Math.min(score, 32);
    let additionalDistance = cappedScore * 5; // The distance increase caps when the score reaches 32
    let minDistance = 100 + additionalDistance; // Base min distance + additional based on capped score

    // Attempt to respawn the square until it's far enough from the dog
    do {
      this.x = random(this.size, width - this.size);
      this.y = random(this.size, height - this.size);
    } while (this.isTooCloseToDog(minDistance));
  }

  isTooCloseToDog(minDistance) {
    if (dog && dog.x !== undefined && dog.y !== undefined) {
      let distance = dist(this.x, this.y, dog.x, dog.y);
      return distance < minDistance;
    }
    return false; // Default to false if dog is not initialized
  }
}


class MovableImage {
  constructor(image, x, y, scale = 1) {
    this.img = image;
    this.x = x;
    this.y = y;
    this.scale = scale; // Added a scale property
    this.speed = 5; // Initial speed - starting with a reasonable speed
  }

  display() {
    // Check if the image is within the bounds of the canvas
    this.x = constrain(this.x, 0, width);
    this.y = constrain(this.y, 0, height);
    
    push();
    translate(this.x, this.y);
    scale(this.scale); // Apply the scaling factor
    image(this.img, 0, 0);
    pop();
  }

  move(stepX, stepY) {
    this.x += stepX * this.speed;
    this.y += stepY * this.speed;
  }

  // Function to set the scale and adjust speed
 growAndSlow() {
   
const maxScale = 0.25; // Adjust this value as needed for balance and performance

  // Only grow if below the maximum scale limit
  if (this.scale < maxScale) {
    this.scale *= 1.025; // Grow by 2.5%
  }

    // Only reduce speed if the score is less than 13
    if (score < 13) {
      this.speed = max(1, this.speed * 0.95); // Reduce speed by 5%, no less than 1
    }
 }

}

function preload() {
  dogImage = loadImage('images/dog1.png');
  bgImage = loadImage('background2.jpg');
  startScreenImage = loadImage('backyard.jpeg');
  customFont = loadFont('MadimiOne-Regular.ttf'); // Load the custom font
  hwImage = loadImage('HW.png'); // Load the image of the homework paper
  buttonSound = loadSound('woof.mp3'); // Make sure the path to your mp3 is correct
    gameLoopSound = loadSound('game_loop_2.mp3'); // Adjust path as needed
    collisionSound = loadSound('paper.mp3'); // Adjust path as needed




}

function setup() {
  createCanvas(400, 400);
  dog = new MovableImage(dogImage, width / 2, height / 2, 0.009);
  whiteSquare = new WhiteSquare();

  imageMode(CENTER);
  textFont(customFont); // Set the custom font for all text
  
  
  // Define button properties
  buttonWidth = 100;
  buttonHeight = 40;
  restartButtonX = width / 2 - buttonWidth / 2;
  restartButtonY = height / 2 + 20;
  menuButtonX = width / 2 - buttonWidth / 2;
  menuButtonY = height / 2 + 70; // Positioned below the restart button

}

function rectRectCollision(x1, y1, w1, h1, x2, y2, w2, h2) {
  return x1 < x2 + w2 &&
         x1 + w1 > x2 &&
         y1 < y2 + h2 &&
         y1 + h1 > y2;
}

function isMouseOverButton(x, y) {
  return mouseX >= x && mouseX <= x + buttonWidth &&
         mouseY >= y && mouseY <= y + buttonHeight;
}

function drawButton(label, x, y) {
  fill(100); // Button color
  stroke(0); // Button outline color
  strokeWeight(2); // Button outline weight
  rect(x, y, buttonWidth, buttonHeight, 5); // Draw the button with rounded corners
  noStroke(); // Disable outline for the text
  fill(255); // Text color
  textSize(16); // Text size
  textAlign(CENTER, CENTER);
  text(label, x + buttonWidth / 2, y + buttonHeight / 2); // Draw the text centered on the button
}

function mousePressed() {
    let playButtonX = width / 2 - buttonWidth / 2;
    let playButtonY = height / 2 + 20;
    let instructionsButtonX = width / 2 - buttonWidth / 2;
    let instructionsButtonY = playButtonY + 70; // Positioned below the "Play" button
    let creditsButtonX = width / 2 - buttonWidth / 2;
    let creditsButtonY = instructionsButtonY + 50; // Positioned below the "Instructions" button

    if (gameState === "start") {
        if (isMouseOverButton(playButtonX, playButtonY)) {
            gameState = "play";
            timerStartTime = millis();
            score = 0;
            dog = new MovableImage(dogImage, width / 2, height / 2, 0.009);
            whiteSquare = new WhiteSquare();
        } else if (isMouseOverButton(instructionsButtonX, instructionsButtonY)) {
            gameState = "instructions";
        } else if (isMouseOverButton(creditsButtonX, creditsButtonY)) {
            gameState = "credits";
        }
    } else if ((gameState === "instructions" || gameState === "credits") && isMouseOverButton(menuButtonX, menuButtonY)) {
        gameState = "start";
    } else if (gameState === "gameover") {
        if (isMouseOverButton(menuButtonX, menuButtonY)) {
            gameState = "start";
        }
        let restartButtonX = width / 2 - buttonWidth / 2;
        let restartButtonY = playButtonY;
        if (isMouseOverButton(restartButtonX, restartButtonY)) {
            gameState = "play";
            timerStartTime = millis();
            score = 0;
            dog = new MovableImage(dogImage, width / 2, height / 2, 0.009);
            whiteSquare = new WhiteSquare();
        }
    }
    if (isMouseOverButton(playButtonX, playButtonY) || 
      isMouseOverButton(instructionsButtonX, instructionsButtonY) || 
      isMouseOverButton(creditsButtonX, creditsButtonY) || 
      isMouseOverButton(menuButtonX, menuButtonY) || 
      isMouseOverButton(restartButtonX, restartButtonY)) {
    buttonSound.play();
  }
}

function drawGameOverScreen() {
  background(0); // You can change the background color
  fill(255);
  textSize(32);
  textAlign(CENTER, CENTER);
  text("Game Over!", width / 2, height / 2 - 60);
  textSize(24);
  text("Your Score: " + score, width / 2, height / 2 - 20);
  

  // Draw the "Main Menu" button
  drawButton("Main Menu", menuButtonX, menuButtonY);
  
    // Draw the "Restart" button
  drawButton("Restart", restartButtonX, restartButtonY);
}


// Global variables for new button positions (adjust the Y positions as needed)
let instructionsButtonY = 267.5; // Positioned below the "Play" button
let creditsButtonY = 315; // Positioned below the "Instructions" button

function drawBackgroundAndTitle() {
    image(startScreenImage, 200, 200, width, height); // Repeated background and title setup
    textSize(30);
    textAlign(CENTER, CENTER);
    stroke(0); // Black outline
    strokeWeight(4); // Thickness of the outline
    fill(255); // White fill for the text
    text("The Dog ate my Homework!", width / 2, height / 2 - 20);
}

function drawStartScreen() {
    drawBackgroundAndTitle();
    drawButton("Instructions", width / 2 - buttonWidth / 2, instructionsButtonY);
    drawButton("Credits", width / 2 - buttonWidth / 2, creditsButtonY);
    drawButton("Play", width / 2 - buttonWidth / 2, height / 2 + 20);
}

function drawPlayScreen() {
    image(bgImage, 200, 200, width, height);
    dog.display();
    whiteSquare.display();
    displayGameInfo();
}

function drawInstructionsScreen() {
    image(startScreenImage, 200, 200, width, height); // Repeated background and title setup
    fill(255);
    textSize(16);
    stroke(0); // Black outline
    strokeWeight(4); // Thickness of the outline
    textAlign(CENTER, CENTER);
    text("The aim of the game is to eat as much of your owner's homework in 30 seconds before you get caught. Use the arrow keys to move around.", width / 8, height / 5, 300, 200); // Adjust the box size if needed
    drawButton("Main Menu", menuButtonX, menuButtonY);
}

function drawCreditsScreen() {
   image(startScreenImage, 200, 200, width, height); // Repeated background and title setup
    fill(255);
    textSize(16);
    stroke(0); // Black outline
    strokeWeight(4); // Thickness of the outline
    textAlign(CENTER, CENTER);
    text("Credits:\nGame Design: Jihad\nProgramming: Jihad, Chatgpt \nArtwork: brgfx, babysofja  \nSound: Pixabay",
      width / 8, height / 5, 300, 200); // The text box's width and height
    drawButton("Main Menu", menuButtonX, menuButtonY); // Adjust the Y position to place it below the credits text
}


function displayGameInfo() {
    let timePassed = (millis() - timerStartTime) / 1000;
    let timeLeft = max(30 - timePassed, 0);
    fill(0);
    noStroke();
    textSize(16);
    textAlign(RIGHT, BOTTOM);
    text("Time left: " + timeLeft.toFixed(1), width - 10, height - 10);

    checkCollisionsAndScore();
    displayScore();
    handleMovement();
}

function checkCollisionsAndScore() {
let hit = rectRectCollision(dog.x - dog.img.width / 2 * dog.scale, dog.y - dog.img.height / 2 * dog.scale, dog.img.width * dog.scale, dog.img.height * dog.scale, whiteSquare.x, whiteSquare.y, whiteSquare.size, whiteSquare.size);
  if (hit) {
    dog.growAndSlow();
    whiteSquare.respawn();
    score++;

    // Play collision sound at 30% volume
    collisionSound.setVolume(0.3); // Set volume to 30%
    collisionSound.play();
  }
}

function displayScore() {
    fill(0);
    stroke(255);
    strokeWeight(2);
    textSize(16);
    textAlign(RIGHT, TOP);
    text("Score: " + score, width - 10, 10);
}

function handleMovement() {
    if (keyIsDown(LEFT_ARROW)) dog.move(-1, 0);
    if (keyIsDown(RIGHT_ARROW)) dog.move(1, 0);
    if (keyIsDown(UP_ARROW)) dog.move(0, -1);
    if (keyIsDown(DOWN_ARROW)) dog.move(0, 1);
}

function draw() {
    switch (gameState) {
        case "start":
            drawStartScreen();
            break;
        case "play":
        // Start the looped sound if it's not already playing and the game state just switched to "play"
      if (!gameLoopSound.isPlaying()) {
        gameLoopSound.loop();
        gameLoopSound.setVolume(0.5); // Set volume to 50%
      }
            drawPlayScreen();
            // Calculate time left here
            let timePassed = (millis() - timerStartTime) / 1000;
            let timeLeft = max(30 - timePassed, 0);
            
            // Display the timer
            fill(0);
            noStroke();
            textSize(16);
            textAlign(RIGHT, BOTTOM);
            text("Time left: " + timeLeft.toFixed(1), width - 10, height - 10);

            if (timeLeft <= 0) {
                gameState = "gameover";
              if (gameLoopSound.isPlaying()) {
          gameLoopSound.stop();
        }
            }

            // Check if the dog collides with the square and handle scoring
            checkCollisionsAndScore();

            // Display the current score
            displayScore();

            // Handle player movement
            handleMovement();
            break;
        case "instructions":
            drawInstructionsScreen();
            break;
        case "credits":
            drawCreditsScreen();
            break;
        case "gameover":
            drawGameOverScreen();
            break;
    }
}

 

Midterm Project: Dungeon Maze

Concept

This dungeon-based maze game’s premise centers on an exciting and dynamic challenge where players are sent into several mazes that are produced by an algorithm, each with its own layout and set of coins and teleporters.

The game’s main goal, set against an engrossing dungeon backdrop, requires players to maneuver tactically through the maze, collecting gold coins while racing against the clock to locate the exit. The game uses teleporters as a game-changing element to enhance depth and complexity. Players can quickly go to different mazes, which resets the maze’s layout and offers unexpected twists. This makes the ‘Dungeon Master’ change their strategy.

In addition, an interesting limited vision feature simulates the real difficulty of traversing a dimly lit dungeon by preventing players from seeing beyond their immediate surroundings and forcing them to depend solely on memory and spatial awareness in order to advance. A fascinating and unpredictable journey that tests players’ intelligence and flexibility through the mix of random maze creation and other mechanisms like timers and limited vision, making every playthrough a unique experience.

Sketch

Snapshots of the Game

Elements of Pride

The choices made in the design of features like the drawLimitedVision effect, the generateRandomMaze function, and the adaptive design for fullscreen and windowed modes are a demonstration of good game designs and good technical decisions.

generateRandomMaze() {
    let maze = new Array(this.rows);
    for (let y = 0; y < this.rows; y++) {
      maze[y] = new Array(this.cols).fill('#');
    }
    const carvePath = (x, y) => {
      const directions = [[1, 0], [-1, 0], [0, 1], [0, -1]];
      this.shuffle(directions);

      directions.forEach(([dx, dy]) => {
        const nx = x + 2 * dx, ny = y + 2 * dy;
        if (nx > 0 && nx < this.cols - 1 && ny > 0 && ny < this.rows - 1 && maze[ny][nx] === '#') {
          maze[ny][nx] = ' ';
          maze[y + dy][x + dx] = ' ';
          carvePath(nx, ny);
        }
      });
    };

    const startX = 2 * Math.floor(Math.random() * Math.floor((this.cols - 2) / 4)) + 1;
    const startY = 2 * Math.floor(Math.random() * Math.floor((this.rows - 2) / 4)) + 1;
    maze[startY][startX] = ' ';
    carvePath(startX, startY);
    let coinCount = Math.floor(Math.random() * (12 - 8 + 1)) + 8; // 8 to 12 coins
    let teleporterCount = Math.floor(Math.random() * (2 - 1 + 1)) + 1; // 1 to 2 teleporters
    maze[this.rows - 2][this.cols - 2] = 'E';
    this.addRandomItems(maze, 'C', coinCount);
    this.addRandomItems(maze, 'T', teleporterCount);

    return maze;
  }

The generateRandomMaze function is a cornerstone of the game’s dynamic environment, creating a new, solvable maze for each session or level. The function uses a recursive backtracking algorithm, it selects a random starting point within the maze and carves out paths. This is done by moving two cells at a time in a chosen direction (to avoid creating isolated cells) and removing walls between cells to create a path. This process repeats recursively for each new cell reached that has all its surrounding cells intact.

Once the maze is generated, the function strategically places coins (‘C’) and teleporters (‘T’) within the maze. Coins act as collectibles, and teleporters serve as transport points to new mazes. An exit (‘E’) is placed at a predefined location, typically far from the start, ensuring that players must navigate through the maze to find it.

function drawLimitedVision(playerX,playerY,visibilityRadius) {

  // Draw a radial gradient from transparent to dark
  let radialGradient = drawingContext.createRadialGradient(playerX, playerY, 0, playerX, playerY, visibilityRadius);
  radialGradient.addColorStop(0, 'rgba(0,0,0,0)'); // Completely transparent at the center
  radialGradient.addColorStop(1, 'rgba(0,0,0,0.8)'); // More opaque towards the edges

  drawingContext.fillStyle = radialGradient;
  drawingContext.fillRect(0, 0, width, height);
}

The drawLimitedVision function enhances the game’s atmosphere by simulating a limited field of vision for the player, similar to carrying a light source in a dark dungeon. The function creates a radial gradient centered on the player’s current position. This gradient transitions from transparent (at the center) to opaque (at the edges) obscures parts of the maze not immediately near the player.

function calculateCellSize() {
  cellSize = min(width, height) / max(mazeColumns, mazeRows);
}

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  calculateCellSize();
}

The code includes mechanisms to adapt to fullscreen and windowed modes. The calculateCellSize function dynamically calculates the size of each maze cell based on the current dimensions of the game window. This ensures that the maze scales correctly when switching between fullscreen and windowed modes, maintaining consistent gameplay and visual quality. The windowResized function listens for changes in the window size (including toggling fullscreen mode) and adjusts the canvas size accordingly.

Difficulties

One of the trickier parts of developing the game was actually implementing the limited vision functionality. Developing a smooth, radial-gradient of light that would dynamically follow the player’s motions and provide vision around them while keeping the remainder of the maze in complete darkness was the challenge. It took several tries to get this effect just right so that it seemed natural and didn’t interfere with gameplay by being too dark or changing too quickly.

There were further difficulties when trying to get the game to smoothly switch between fullscreen and regular window modes. Flexible design was necessary to make sure that the maze, player character, and user interface (UI) components all scaled and remained proportionate across a range of screen sizes and aspect ratios. This flexibility was essential to maintaining the game’s playability and graphic quality in any viewing mode.

Improvements

A possible area for enhancement is the visual design and user interface. For example, adding more complex visuals to the player’s character, treasures, and maze walls might greatly improve the game’s visual attractiveness. Furthermore, adding more variation to the maze layouts and obstacles—like traps or enemies—could add new and unique difficulties and amplify the action.

The project could also benefit from a more advanced level progression system, where the difficulty gradually increases through levels, possibly by enlarging the maze, decreasing the time limit, or increasing the required percentage of coins to collect before exiting.

 

Full Code

Midterm Assignment – A Blast to the Past!

My Midterm Assignment can be described in the following way: “A computer geeks dream”. Jokes aside, this is an Idea I have had on mind for a long time and I decided that this is the right time for me to make it reality. Let’s dive deep into it:

Ideation:

As the midterm assignment was approaching I was a little bit scared of what I should do. I heard people around me all say they are going to make a game or some visual artistic piece but I wanted to do something different. Then I realized, what is my first experience with computers and technology? The answer was clear and simple: WINDOWS XP (My first computer with a Pentium 3 and 1GB of ram Is one I will always remember)

Concept

The concept was pretty simple. Make a small Windows XP emulator that will include the original icons and sounds as well as display the infamous windows errors which don’t make sense (for example: Keyboard not found: Press some button – like how am I supposed to press a button on the keyboard if it’s disconnected???). Before Explaining the technical side, I will let you explore the project a little bit.

Execution

This assignment was done in a way that was very thought of and self reflective based on the readings we have done for the class in the past month. It is focused on human centered design as well as it incorporates many techniques like the obviousness of the design and the “lack of need for instructions”. It starts of simple with a power button which is familiar to everyone as well as the whole windows ecosystem which is easy to figure out. The icons have been made draggable, blue when clicked and with the original click sound, just like a real windows XP. For organization purposes they have been divided in different classes, as shown below:

class Icon {
  constructor(image, label, x, y, width, height) {
    this.image = image;
    this.label = label;
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
    this.isHighlighted = false;
    this.isDragging = false; // New property to track dragging state
    this.offsetX = 0; // Offset between mouse and icon x position during drag
    this.offsetY = 0; // Offset between mouse and icon y position during drag
    this.clickCount = 0;
  }

  draw() {
    push();
    if (this.isHighlighted || this.isDragging) {
      tint(0, 0, 255); // Tint for highlight
      fill("blue"); // Color for highlighted text
    } else {
      noTint();
      fill(255); // Default text color
    }

    image(this.image, this.x, this.y, this.width, this.height);
    text(this.label, this.x, this.y + this.height + 10); // Adjust text position below the icon
    pop();
  }

  contains(px, py) {
    return (
      px >= this.x &&
      px <= this.x + this.width &&
      py >= this.y &&
      py <= this.y + this.height
    );
  }

  click() {
    if (this.contains(mouseX, mouseY)) {
      this.isHighlighted = true;
      clickSound.play();

      // Check if the current icon is the last clicked icon
      if (lastIconClicked === this.label) {
        this.clickCount++; // Increment only if the same icon is clicked consecutively
      } else {
        // Reset all icons' click counts if a different icon is clicked
        icons.forEach((icon) => (icon.clickCount = 0));
        this.clickCount = 1;
        lastIconClicked = this.label; // Update the last clicked icon label
      }

      console.log(
        `Clicked on Icon: ${this.label}, Click Count: ${this.clickCount}`
      );

      // Check if the icon is clicked two times in a row
      if (this.clickCount % 2 == 0 && this.label === "midterm.exe") {
        errorImageLogic();
        this.clickCount = 0; // Reset click count after opening the icon
      } else if (this.clickCount % 2 == 0 && this.label === "My Computer") {
        myComputerLogic();
        this.clickCount = 0; // Reset click count after opening the icon
      } else if (this.clickCount % 2 == 0 && this.label === "Recycle Bin") {
        recycleBinLogic();
        this.clickCount = 0; // Reset click count after opening the icon
      } else if (this.clickCount == 1 && this.label === "errorImage") {
        spawnErrorImages();
        this.clickCount = 0; // Reset click count after spawning error images
      }
    } else {
      this.isHighlighted = false;
    }
  }

  mousePressed() {
    if (this.contains(mouseX, mouseY)) {
      this.isDragging = true;
      this.offsetX = this.x - mouseX;
      this.offsetY = this.y - mouseY;
    }
  }

  mouseReleased() {
    this.isDragging = false;
  }

  mouseDragged() {
    if (this.isDragging) {
      this.x = mouseX + this.offsetX;
      this.y = mouseY + this.offsetY;
    }
  }
}

Shown above is the class for the Icons which makes it easy to add them or take them away.

class Window {
  constructor(image, width, height) {
    this.image = image;
    this.width = width;
    this.height = height;
    this.x = 50;
    this.y = 50;
    this.closeButtonSize = 50; // Size of the close button
  }

  draw() {
    image(this.image, this.x, this.y, this.width, this.height);
  }

  clicked(mx, my) {
    // Check if close button is clicked
    if (
      mx > this.x + this.width - this.closeButtonSize &&
      mx < this.x + this.width &&
      my > this.y &&
      my < this.y + this.closeButtonSize
    ) {
      clickSound.play();
      specialWindow = null; // Close the window
    }

    // Check if the bottom half of the window is clicked
    if (
      this.image === errorImage &&
      mx >= this.x &&
      mx <= this.x + this.width &&
      my >= this.y + this.height / 2 &&
      my <= this.y + this.height
    ) {
      errorShouldSpawn = true;
      // Logic for clicking the bottom half of the error window
      for (let i = 0; i < 40; i++) {
        setTimeout(() => {
          spawnErrorImages();
        }, i * 100); // spawn more error windows over time
      }

      // Transition to blue screen after 2 seconds
      setTimeout(() => {
        loop();
        currentState = State.BLUE_SCREEN;
      }, 4000);
    }
  }
}

Shown above is the code for the Window class which displays the windows where the icons are clicked and also controls the behavior of the error prompts.

Challenges and closing statement

The main challenges included the clicking of the buttons and dragging them around as well as the behavior of the error messages shown.

Special thanks to Pi and Professor Aaron Sherwood for the help throughout the whole execution of the project. You guys really motivated me to make my childhood dream come true. If you showed this to 5 year old Darko and said he made that he would literally be so excited.

At the same time, thank YOU as a reader for “wasting” 5 minutes of your day to appreciate my work. Remember to put a smile on your face and appreciate very day as much as you can. Spread love with your family, friends, professors and even strangers cause you never know what tomorrow brings.

Darko Loves You <3

Stay Safe!

MidTerm Project – Khalifa Alshamsi

Concept and Gameplay

“UFO Escape” transports players into the role of a UFO pilot tasked with the critical mission of avoiding collision with perilously floating rocks in space. The game unfolds across a dynamic background that transitions from the tranquility of space to the adrenaline-pumping danger zone filled with obstacles. Players must use the arrow keys to maneuver their spacecraft, navigating the challenging asteroid field. Each successful dodge rewards the player with points, elevating the stakes and the satisfaction derived from setting new high scores.

Technical Achievements and Design Decisions

In my opinion, one of the game’s standout features is its seamless integration of visual and auditory elements to create an immersive experience, which was the vibe I was going for. The use of distinct background images for different game states — a peaceful space scene for the menu and game over screens and a more in-the-field backdrop for gameplay.

// Displays the space background image only during menu and game over states but displays a different image during gameplay
if (gameState === "MENU" || gameState === "GAME OVER") {
  background(bgImage);
} else if (gameState === "PLAYING") {
  background(gameplayBgImage);
}

The transition between these states is goofy because of its old-school UFO soundtrack that loops during gameplay. This adds a layer of nostalgia that brings me back to the old free online games I used to play growing up in school using the school computers. The soundtrack then abruptly halts upon the game’s end, marking the player’s failure and the game’s conclusion.

Another area of pride would be collision detection, which not only ensures that the game is playable but also maintains a level of difficulty that is engaging without being frustrating, but it was highly frustrating to tune it properly to respond.

// Function to detect collision with obstacles
collidesWith(obstacle) {
  let closestX = constrain(obstacle.x, this.x - this.width / 3, this.x + this.width / 3);
  let closestY = constrain(obstacle.y, this.y - this.height / 3, this.y + this.height / 3);
  let distanceX = obstacle.x - closestX;
  let distanceY = obstacle.y - closestY;
  let distanceSquared = (distanceX * distanceX) + (distanceY * distanceY);
  return distanceSquared < (obstacle.radius * obstacle.radius); // Checks if distance is less than obstacle radius
}

The game accurately determines collisions by calculating the closest points between the UFO and the incoming asteroids. This method considers the actual positions and dimensions of both objects, offering a realistic and, for the most part, precise detection mechanism that enhances the player’s experience.

Areas for Improvement and Challenges

While “UFO Escape” is the first game I got to create, and I am extremely proud of it, certain aspects remain ripe for enhancement. The collision detection system, though effective, could be refined to accommodate the irregular shapes of the rocks more accurately.

Another challenge faced during development was ensuring the game’s performance remained smooth despite the increasing number of obstacles. This issue kept bugging the game, making the rocks not properly collide with the UFO ship. This issue was largely mitigated through optimization techniques, such as removing off-screen obstacles from the array to free up resources.

Conclusion

“UFO Escape” embodies the spirit of classic arcade games, but I would say with a bit more graphics. Its development journey was filled with learning opportunities, from refining collision detection algorithms so many times that I lost count to creating an engaging user interface.

Sketch:

Script:

// Variables for game assets
let bgImage; // Variable to hold the background image for menu and game over screens
let bgMusic; // Variable to hold the background music for gameplay
let player; // Player object
let obstacles = []; // Array to store obstacles
let gameSpeed = 6; // Speed at which obstacles move
let score = 0; // Player's score
let gameState = "MENU"; // Initial game state; that is "MENU", "PLAYING", or "GAME OVER"
let rockImage; // Variable to hold the rock image

// Preload function to load game assets before the game starts
function preload() {
  bgImage = loadImage('space.png'); // Loads the background image
    gameplayBgImage = loadImage('gameplay.jpg'); // Loads the gameplay background image
  bgMusic = loadSound('gameplaysound.mp3'); // Loads the background music
  rockImage = loadImage('rock-2.png'); // Loads the rock image
}

// The setup function to initialize the game
function setup() {
  createCanvas(400, 600); // Size of the game canvas
  player = new Player(); // Initializes the player object
  textAlign(CENTER, CENTER); // Setting text alignment for drawing text
}

// Draw function called repeatedly to render the game
function draw() {
  // Displays the space background image only during menu and game over states but displays a different image during gameplay
  if (gameState === "MENU" || gameState === "GAME OVER") {
    background(bgImage);
  } else if (gameState === "PLAYING") {
    background(gameplayBgImage);
  }

  // Handles game state transitions
  if (gameState === "MENU") {
    drawMenu();
  } else if (gameState === "PLAYING") {
    if (!bgMusic.isPlaying()) {
      bgMusic.loop(); // Looping the background music during gameplay
    }
    playGame();
  } else if (gameState === "GAME OVER") {
    bgMusic.stop(); // Stops the music on game over
    drawGameOver();
  }
}

// Function to display the game menu
function drawMenu() {
  fill(255,0 ,0);
  textSize(24);
  text("UFO Escape", width / 2, height / 2.5);
  textSize(16);
  fill(255);
  text("Avoid the rocks!\nUse ARROW KEYS to move\nPress ENTER to start", width / 2, height / 2);
}

// Function to handle gameplay logic
function playGame() {
  fill(255);
  textSize(16);
  text(`Score: ${score}`, width / 2, 30);

  player.show(); // Displays the player
  player.move(); // Moves the player based on key inputs

  // Adding a new obstacle at intervals
  if (frameCount % 120 == 0) {
    obstacles.push(new Obstacle());
  }

  // Updates and displays obstacles
  for (let i = obstacles.length - 1; i >= 0; i--) {
    obstacles[i].show();
    obstacles[i].update();
    // Checks for collisions
    if (player.collidesWith(obstacles[i])) {
      gameState = "GAME OVER";
    }
    // Removes obstacles that have moved off the screen and increment score
    if (obstacles[i].y > height) {
      obstacles.splice(i, 1);
      score++;
    }
  }
}

// Function to display the game over screen
function drawGameOver() {
  fill(255, 0, 0);
  textSize(32);
  text("Game Over", width / 2, height / 2);
  textSize(16);
  fill(255);
  text("Press ENTER to restart", width / 2, height / 2 + 40);
}

// Function to handle key presses
function keyPressed() {
  // Starts the game or restart after game over when ENTER is pressed
  if (keyCode === ENTER || keyCode === RETURN) {
    if (gameState === "MENU" || gameState === "GAME OVER") {
      gameState = "PLAYING";
      resetGame();
    }
  }
}

// Function to reset the game to its initial state
function resetGame() {
  obstacles = []; // Clear existing obstacles
  score = 0; // Reset score
  player = new Player(); // Reinitialize the player
}

// Player class
class Player {
  constructor() {
    this.width = 60; // Width of the UFO
    this.height = 30; // Height of the UFO
    this.x = width / 2; // Starting x position
    this.y = height - 100; // Starting y position
  }

  // Function to display the UFO
  show() {
    fill(255); // Sets color to white
    rectMode(CENTER);
    rect(this.x, this.y, this.width, this.height, 20); // Draws the UFO's body
    fill(255, 0, 0); // Sets the glass color to red
    arc(this.x, this.y - this.height / 4, this.width / 2, this.height / 1, PI, 0, CHORD); // Draws the glass
  }

  // Function to move the player based on arrow key inputs
  move() {
    if (keyIsDown(LEFT_ARROW)) {
      this.x -= 5; // Moves left
    }
    if (keyIsDown(RIGHT_ARROW)) {
      this.x += 5; // Moves right
    }
    if (keyIsDown(UP_ARROW)) {
      this.y -= 5; // Moves up
    }
    if (keyIsDown(DOWN_ARROW)) {
      this.y += 5; // Moves down
    }
    this.x = constrain(this.x, 0, width); // Keeps player within horizontal bounds
    this.y = constrain(this.y, 0, height); // Keep player within vertical bounds
    //This is only so the player cannot exist the canvas
  }

  // Function to detect collision with obstacles
  collidesWith(obstacle) {
    let closestX = constrain(obstacle.x, this.x - this.width / 3, this.x + this.width / 3);
    let closestY = constrain(obstacle.y, this.y - this.height / 3, this.y + this.height / 3);
    let distanceX = obstacle.x - closestX;
    let distanceY = obstacle.y - closestY;
    let distanceSquared = (distanceX * distanceX) + (distanceY * distanceY);
    return distanceSquared < (obstacle.radius * obstacle.radius); // Checks if distance is less than obstacle radius
  }
}

// Obstacle class
class Obstacle {
  constructor() {
    this.radius = random(15, 30); // Random radius for obstacle
    this.x = random(this.radius, width - this.radius); // Random x position
    this.y = -this.radius; // Starts off-screen so it looks like its coming towards you
  }

  // Function to display the rocks
  show() {
    image(rockImage, this.x, this.y, this.radius * 2, this.radius * 2); // Draws them as a circle
  }

  // Function to update obstacles position
  update() {
    this.y += gameSpeed; // Moves obstacles down the screen
  }
}

 

Midterm Project – NewJeans’ Escape

Concept

Something I always love talking about is my obsession with music. I love collecting cassette tapes, vinyl records, and CDs. I ensure everyone knows about this because I find it so cool. Among the thousands of artists I listen to, only a couple stick out, and among these artists is NewJeans, a group that is made up of 5 members. in 2023’s July, NewJeans made a comeback after almost six months of releasing no music. With that came my all-time favorite music video of theirs, where they collaborated with Cartoon Network’s PowerPuffGirls, called NewJeans by NewJeans. This song is a testament to the creativity and unique musical style that NewJeans is known for. Its distinctive arrangement and innovative lyrics, along with the exciting collaboration, made “New Jeans” another successful hit for the group.

In this music video, there’s a particular scene where the girls are turned into a video game. When I heard that I would be able to make a game-like sketch for my midterm project, my mind immediately went to that scene. Of course, at first, I wanted to make more of something that resembles one of my favorite arcade games, Dance Dance Revolution, but after I spent countless hours trying my hardest to make it work, I just stumbled into too many obstacles that made me realize that I would not have the time to figure it out. So, I went back to my previous idea: To use the concept from the music video. However, I wanted to get creative with the structure of it. I decided to go with a “Choose your Character” title screen, which seemed to be the most crucial part for me. Then for the actual playing part, I wanted their song to be a part of it, so I made it so that the player could win solely by surviving the flying obstacles for the entirety of the song.

Check out the music video!!

 

This is the part of the music video I was inspired by:


Design

Since the character designs were already avaliable, I did not have a hard time picking the character design. However, I could not find a high quality PNG of the charactes so I had to retrce it but it was not challenging at all.

Challenges

Putting NewJeans’ Escape together forced me to face many challenges:
  1. The “Choose Your Character” Disaster: This was primarily the most significant part of the whole game for me. I was really passionate about this, and I would not continue if it had not worked. After countless hours of searching online and watching useless YouTube videos, I turned to ChatGPT, who taught me to use an Index, which I am very, very proud of myself for getting the hang of. I used the mouseClicked(); function to check the position of the mouse and which character it corresponds to and I stored that character into a variable i named “characterIndex” which displays in the ‘play’ state and once the player chooses the character, the game state will change to ‘play’.

I’m very proud of myself for getting it figured out.

// Function to handle mouse clicks
function mouseClicked() {
  if (gameState === "start") {
    // Check if mouse click is on a character
    for (let i = 0; i < 5; i++) {
      if (mouseX > (width / 5) * i && mouseX < (width / 5) * (i + 1)) {
        characterIndex = i; // Set selected character
        gameState = "play"; // Change game state to play
        break;
      }
    }
  }

 

2. The Time Loss Incident: “I just wanted a timer to be displayed at the top right of the screen. Is that too much to ask?” said Rashed as he was on the brink of quitting. Having a timer that would countdown with the song, for some reason, was not cooperating with what I wanted to do. I wanted to directly connect it to the music to have it automatically calculate when the music would start and countdown till its end, but I could not figure it out. I decided to code the timer separately instead of figuring that out and that’s what I did. It turns out this was the most straightforward way, and I wish I had thought of this before where I had two variables, “timeLeft” and “long duration,” which both equal 108 (The duration of the song in seconds), and had them decrease by the current time of the song.

Very proud of myself for not giving up.

// Display remaining time
timeLeft = songDuration - song.currentTime();
let minutes = Math.floor(timeLeft / 60);
let seconds = Math.floor(timeLeft % 60);
let timeString = nf(minutes, 2) + ":" + nf(seconds, 2);
textAlign(RIGHT);
text("Time: " + timeString, width - 10, 30);

 

3. The Collision Colliding Collision: The only thing I wanted was for when the objects hit the player, lives would decrease, but in so many tries, for some reason, it would not work well. No matter hoe many times I try, nothing would work. until I found the miracle of the dist(); function. The function calculates the distance between two points. The coordinates(obj.x + 75, obj.y + 50) represent the center of the current object (adjusted for its size, this was torture). The coordinates(80, characterY + 30) represent the center of the character (adjusted for its size, torture part. 2). This means that if the distance is less than 30 pixels (which represents a collision radius), it means the object and the character have collided.

 

objects.push(createObject());
 }
 for (let obj of objects) {
 obj.x -= objectSpeed; // Move object from right to left
 // Draw the object (evil bunny)
 image(evilBunny, obj.x, obj.y, 150, 100);

 // Check collision with character
 if (dist(obj.x + 75, obj.y + 50, 80, characterY + 30) < 30) { 
   lives--; // If collision happens, decrease lives
   objects.splice(objects.indexOf(obj), 1); // Remove object

 

The Game

FullScreen Link

 

Overall

I am very, very proud of myself and very happy with how this turned out. I hope to continue working on my skills, and one day, I could code a rhythm game.

Midterm Project – Stefania Petre

Approximately ten years ago, a solider has fallen: they took down Flappy Bird. A game which combines simplicity and effectiveness. For my Midterm Project, I have decided to make my own version of it called Flappy Falcon. For this, I needed the code, some pictures and sounds.

The idea behind it is that I wanted to create the NYUAD version of the game, which would have the iconic colors of the University and our mascott.

Code: The code was pretty easy to make because we have already learnt everything that I had to do for it in class. I had to make adjustments along the way, as for some reason the pipes were the hardest thing to code because I faced some issues with them along the way:

– the falcon wouldn’t pass the pipes;

-the moment the falcon would touch the pipes even so slightly it would be game over ;

Pictures: In regards to the media, I came across another problem: the falcon wouldn’t upload and it was showing me an error. After some minutes of thinking of a solution, I got it to work.

Sounds: For the sounds I have uploaded a bird background noise, which signifies the falcon, but another classmate of mine interpreted it as the bird sounds that we hear everyday on campus (thanks sm).

//Flappy Falcon Game
//by Stefania Petre for Introduction Into Interactive Media

let bird;
let pipes = [];
let gameStarted = false;
let score = 0;
let startButton;
let backgroundImage;
let flappySound;
let birdImage;

function preload() {
  backgroundImage = loadImage("background.png"); 
  flappySound = loadSound("sound.mp3"); 
  birdImage = loadImage("bird.png"); 
}

function setup() {
  createCanvas(400, 600);
  bird = new Bird();
  startButton = createButton("Start");
  startButton.position(width / 2.2, height / 1.9);
  startButton.mousePressed(startGame);
}

function draw() {
  if (gameStarted) {
    playGame();
  } else {
    showStartScreen();
  }
}

function playGame() {
  background(94, 38, 163);

  bird.update();
  bird.show();

  updateAndShowPipes();

  showScore();
}

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

    if (pipes[i].hits(bird)) {
      gameOver();
    }

    if (pipes[i].passes(bird)) {
      score++;
    }

    if (pipes[i].offscreen()) {
      pipes.splice(i, 1);
    }
  }

  if (frameCount % 100 === 0) {
    pipes.push(new Pipe());
  }
}

function showScore() {
  textSize(32);
  fill(255);
  text(score, width / 2, 50);
}

function showStartScreen() {
  image(backgroundImage, 0, 0, width, height);
  textSize(32);
  fill(255);
  textAlign(CENTER, CENTER);
  text("Flappy Falcon", width / 2, height / 3);
  textSize(16);
  text("Press Start to Experience Nostalgia!", width / 2, height / 2.2);
  score = 0;
}

function keyPressed() {
  if (key === " " && gameStarted) {
    bird.jump();
  }
}

function startGame() {
  gameStarted = true;
  pipes = [];
  startButton.hide();
  flappySound.play();
}

function gameOver() {
  gameStarted = false;
  startButton.show();
}

class Bird {
  constructor() {
    this.y = height / 2;
    this.x = 64;
    this.gravity = 0.6;
    this.lift = -15;
    this.velocity = 0;
    this.size = 50;
  }

  show() {
    image(birdImage, this.x, this.y, this.size, this.size); // Draw the bird image
  }

  update() {
    this.velocity += this.gravity;
    this.velocity *= 0.9;
    this.y += this.velocity;

    this.y = constrain(this.y, 0, height);
  }

  jump() {
    this.velocity += this.lift;
  }
}

class Pipe {
  constructor() {
    this.spacing = 200; // Increased gap between pipes
    this.top = random(height - this.spacing);
    this.bottom = height - (this.top + this.spacing);
    this.x = width;
    this.w = 40;
    this.speed = 2;
    this.passed = false;
  }

  show() {
    fill(0); // Set color to black
    rect(this.x, 0, this.w, this.top);
    rect(this.x, height - this.bottom, this.w, this.bottom);
  }

  update() {
    this.x -= this.speed;
  }

  offscreen() {
    return this.x < -this.w;
  }

  hits(bird) {
    return (
      bird.x + bird.size / 2 > this.x &&
      bird.x - bird.size / 2 < this.x + this.w &&
      (bird.y - bird.size / 2 < this.top ||
        bird.y + bird.size / 2 > height - this.bottom)
    );
  }

  passes(bird) {
    if (this.x < bird.x && !this.passed) {
      this.passed = true;
      return true;
    }
    return false;
  }
}

Overall, I completed every single requirement for the game which I would say I am happy about!