Midterm project – Whack a Mole

For my midterm project, I decided to create my own version of the classic Whack-A-Mole game. The goal was to design an interactive experience where players could test their reflexes by clicking on moles as they randomly appeared on the screen. It was inspired by both traditional arcade versions and mobile games, where I wanted to reach a balance between simple navigation and entertainment.

One of the aspects I’m particularly proud of is the way I structured the game logic. I tried to keep my code modular by organizing key components into functions. This not only made the development process more efficient but also allowed me to tweak game parameters easily. Another feature I really enjoyed implementing was the randomization of mole appearances that ensures that no two games feel exactly the same. The timing and positioning algorithms took some hard work, but I’m happy with how smooth the flow of the game turned out.

How it works: Moles pop up from different holes at random intervals, and the player must click on them to score points. Each mole stays visible for a short duration before disappearing, which requires quick reactions from the player. As the game progresses, the frequency of mole appearances increases, adding a greater challenge over time. To add an element of risk, I implemented a bomb mechanic, meaning that if a player accidentally clicks on a bomb instead of a mole, the game ends immediately. This twist keeps players alert and encourages more precise clicking. The game runs in a continuous loop with constant updates on the screen that track the player’s score. Each successful mole hit adds points, while avoiding bombs becomes crucial for “survival”.

Challenges: One of the biggest issues was making sure the game felt responsive without being overwhelming. Initially, the moles appeared and disappeared too quickly which made it nearly impossible to hit them in time. I had to experiment with different timing settings to find the right balance. Another tricky aspect was collision detection, where I had to make sure that clicks were registered correctly when a mole is hit and not on empty spots or bombs. Debugging this issue took some time, but through testing and refining my hitbox logic, I was able to get it working.

Implementation: The game is built with JavaScript and p5.js, which handle the graphics and interactions. I used free images and sound effects from online stocks to make the game more engaging.  The Game class manages the core mechanics which includes the following:

  • Hole and Object Management – moles and bombs are placed randomly to keep the game unpredictable
  • Scoring System – players earn points for hitting moles, while bombs immediately end the game
  • Difficulty Scaling – as the game progresses, moles appear and disappear faster, making it more challenging
  • Sound Effects and Graphics – Background music, whacking sounds, and animations add to an immersive experienceCode snippets:>>Managing moles and bombs
    if (mouseIsPressed) {
      if (dist(mouseX, mouseY, hole.x, hole.y) < hole.d) {
        mouseIsPressed = false;
        punched = true;
        sounds.whak.play();
        setTimeout(() => {
          punched = false;
        }, 200);
        if (hole.type == "mole") {
          hole.type = "hole";
          game.score += 1;
          game.timer += 30;
        } else {
          sounds.bomb.play();
          gameOver();
        }
      }
    }
    

    >>Registering the mouse clicks

    if (frameCount % (game.difficulty - game.score) == 0) {
      let hole = random(game.holes);
      if (hole.type == "mole") {
        hole.type = "hole";
      } else {
        hole.type = "mole";
      }
      if (random(1) < 0.1) {
        hole.type = "bomb";
        setTimeout(() => {
          hole.type = "hole";
        }, 1000);
      }
    }
    

    >>Game Over logic

    function gameOver() {
      sounds.gameover.play();
      setTimeout(() => {
        sounds.back.stop();
        image(imgs.blast.img, mouseX, mouseY, 250, 250);
        background(20, 100);
        textSize(64);
        text("Game Over", width / 2, height / 2);
        textSize(16);
        text("click anywhere to restart!", width / 2, height - 50);
        textSize(46);
        text(game.score, width / 2, 35);
        state = "gameOver";
      }, 100);
      noLoop();
    }
    
    

    Improvements: Looking forward, there are definitely some areas that I’d like to improve. One feature I would like to add is a progressive difficulty system, where players are challenged at different levels of difficulty. Right now, the game is fun but could benefit from more depth. On top of that, I’d like to upgrade the user interface by adding a “start” and “home” screen, score tracker, and possibly a leaderboard.

    let imgs = {};
    let sounds = {};
    let punched = false;
    let state = "start";
    
    function preload() {
      backImg = loadImage("assets/back.png");
      font = loadFont("assets/Sigmar-Regular.ttf");
      imgs.blast = { img: loadImage("assets/blast.png"), xoff: 0, yoff: 0 };
      imgs.bomb = { img: loadImage("assets/bomb.png"), xoff: 0, yoff: 0 };
      imgs.hammer = { img: loadImage("assets/hammer.png"), xoff: 0, yoff: 0 };
      imgs.hole = { img: loadImage("assets/hole.png"), xoff: 0, yoff: 30 };
      imgs.mole = { img: loadImage("assets/mole.png"), xoff: 0, yoff: 0 };
    
      sounds.bomb = loadSound("sounds/Bomb hit.mp3");
      sounds.back = loadSound("sounds/Game main theme.mp3");
      sounds.gameover = loadSound("sounds/game-over.mp3");
      sounds.whak = loadSound("sounds/Whacking a mole.mp3");
    }
    
    function setup() {
      createCanvas(600, 600);
      imageMode(CENTER);
      textFont(font);
      game = new Game();
      textAlign(CENTER, CENTER);
    }
    
    function draw() {
      image(backImg, width / 2, height / 2, width, height);
      switch (state) {
        case "start":
          sounds.back.stop();
    
          textSize(68);
          fill(255);
          text("WhACK!\na MOLE!", width / 2, height / 2 - 120);
          textSize(16);
          text("press anywhere to start!", width / 2, height - 30);
    
          textSize(26);
          let img = [imgs.hole, imgs.mole, imgs.bomb];
          image(
            img[floor(frameCount / 60) % 3].img,
            width / 2 + img[floor(frameCount / 60) % 3].xoff,
            height / 2 + 150 + img[floor(frameCount / 60) % 3].yoff
          );
          if (mouseIsPressed) {
            mouseIsPressed = false;
            sounds.whak.play();
            state = "game";
          }
          break;
        case "game":
          game.show();
          if (!sounds.back.isPlaying()) sounds.back.play();
          break;
      }
      if (mouseX != 0 && mouseY != 0) {
        push();
        translate(mouseX, mouseY + 10);
        if (punched) {
          rotate(-PI / 2);
        }
        scale(map(game.holes.length, 4, 20, 1, 0.25));
        image(imgs.hammer.img, 0, 0, 150, 150);
        pop();
      }
    }
    
    function mousePressed() {
      if (state == "gameOver") {
        state = "start";
        game = new Game();
        mouseIsPressed = false;
        loop();
      }
    }
    function gameOver() {
      sounds.gameover.play();
      setTimeout(() => {
        sounds.back.stop();
        image(imgs.blast.img, mouseX, mouseY, 250, 250);
        background(20, 100);
        textSize(64);
        text("Game Over", width / 2, height / 2);
        textSize(16);
        text("click anywhere to restart!", width / 2, height - 50);
        textSize(46);
        text(game.score, width / 2, 35);
        state = "gameOver";
      }, 100);
    
      noLoop();
    }
    
    class Game {
      constructor() {
        this.x = 10;
        this.y = height / 2 - 80;
        this.w = width - 20;
        this.h = height / 2 + 70;
    
        this.holesNum = 4;
        this.holes = [];
        this.difficulty = 60;
        this.score = 0;
        this.timer = 4800;
      }
      show() {
        //timer
        if (this.timer > 4800) this.timer = 4800;
        this.timer -= 1.5;
        fill(20, 100);
        rect(10, 5, width - 20, 10);
        fill(255);
        rect(10, 5, map(this.timer, 0, 4800, 0, width - 20), 10);
        if (this.timer < 0) {
          mouseX = width / 2;
          mouseY = height / 2;
          gameOver();
        }
    
        //score
        fill(255);
        textSize(46);
        if (punched) textSize(54);
        text(this.score, width / 2, 35);
    
        if (this.holesNum != this.holes.length) {
          this.holes = this.findHolePositions(1);
        }
        for (let i = 0; i < this.holes.length; i++) {
          push();
          translate(this.holes[i].x, this.holes[i].y);
          scale(this.holes[i].d / 250);
          let img;
          switch (this.holes[i].type) {
            case "hole":
              img = imgs.hole;
              //nothing
              break;
            case "mole":
              img = imgs.mole;
              break;
            case "bomb":
              img = imgs.bomb;
              break;
          }
    
          if (this.holes[i].type == "mole" || this.holes[i].type == "bomb") {
            //check mouse click on mole
            if (mouseIsPressed) {
              if (
                dist(mouseX, mouseY, this.holes[i].x, this.holes[i].y) <
                this.holes[i].d
              ) {
                mouseIsPressed = false;
                punched = true;
                sounds.whak.play();
                setTimeout(() => {
                  punched = false;
                }, 200);
                if (this.holes[i].type == "mole") {
                  this.holes[i].type = "hole";
                  this.score += 1;
                  this.timer += 30;
                } else {
                  sounds.bomb.play();
                  gameOver();
                }
              }
            }
          }
          image(img.img, img.xoff, img.yoff);
          pop();
        }
        if (this.difficulty - this.score < 20) {
          this.difficulty += 30;
          this.holesNum += 1;
        }
    
        if (frameCount % (this.difficulty - this.score) == 0) {
          let hole = random(this.holes);
          if (hole.type == "mole") {
            hole.type = "hole";
          } else {
            hole.type = "mole";
          }
          if (random(1) < 0.1) {
            hole.type = "bomb";
            setTimeout(() => {
              hole.type = "hole";
            }, 1000);
          }
        }
      }
    
      findHolePositions(n, d = 200) {
        let arr = [];
    
        for (let i = 0; i < this.holesNum; i++) {
          let x = random(this.x + d / 2, this.x + this.w - d / 2);
          let y = random(this.y + d / 2, this.y + this.h - d / 2);
          arr.push({ x: x, y: y, d: d, type: "hole" });
        }
        //no hole should overlap
        for (let i = 0; i < arr.length; i++) {
          for (let j = 0; j < arr.length; j++) {
            if (i != j) {
              let d_ = dist(arr[i].x, arr[i].y, arr[j].x, arr[j].y);
              if (d_ < d) {
                n += 1;
                if (n > 50) {
                  n = 0;
                  d *= 0.9;
                  return this.findHolePositions(n, d);
                }
                return this.findHolePositions(n, d);
              }
            }
          }
        }
        return arr;
      }
    }
    

    The Visuals:

Midterm Project – Dodge the Droppings!

Link to fullscreen sketch

Concept

The idea behind my game came from a personal incident, and is intended to be funny and inspired by our very own NYUAD campus. The game setting is such that the player is working under the palm trees outside C2 competing with time to complete their assignment due in 5 minutes. Birds above have started attacking the player, who now needs to dodge the droppings falling from above while at the same time finish their assignment as soon as possible. The game is an adaptation of the classic dodge-style game with an added game element. This added element is a progress bar that represents the player’s assignment progress and it gets filled upon the repeated pressing of the space bar. The main goal is to fill up the progress bar (the win condition), while dodging the droppings falling from above without getting hit by any (getting hit is the lose condition).

Game components

The sketch starts by displaying a home page with instructions on how to play the game. Upon pressing the “Enter” button, the game starts. The images of the keyboard buttons on the instructions are taken from flaticon.com.

The main game screen consists of four elements:

  1. bird
  2. dropping
  3. computer
  4. progress bar (yellow long rectangle on the right).

Bird poop (dropping) falls from where the birds are located at the top of the screen, at regular intervals across random columns in different speeds. The computer is situated at the bottom of the screen and can be moved horizontally to different columns using the left and right keyboard buttons. When the space bar is pressed, the progress bar fills up (red indicates the level of assignment progress). At the top right, there is a timer that shows how much time has elapsed in seconds, and the best time  – which is the lowest time taken to complete the assignment, since the goal is to finish the assignment as quickly as possible without getting hit by a dropping – will be tracked across game plays and displayed on the home page.

I drew the images used for the bird, dropping and computer on Procreate and used a background remover application to make the background transparent. The image in the game background with the palm trees is taken from NYUAD’s website.

game screen
screen shown when the assignment is complete
screen shown when hit by / collided with a dropping
home page with the best time displayed

Implementation

Each of the four main elements (bird, dropping, computer, progress bar) are implemented as a class and have their respective functions, including display, fall, move, fill etc. There is also a Game class that manages game state variables, starts/resets the game and checks for collision between computer and the droppings.

One of the design decisions I made was to divide the canvas into (invisible) columns and have the computer move one column at a time instead of moving continuously, and droppings fall randomly from one of the columns instead of from any random pixel. This way, the user can’t “cheat” by placing the computer at a location where droppings rarely fall and it has to constantly dodge droppings, which I personally felt made the game a bit more challenging and fun.

class Computer {
  constructor() {
    this.x = 0;
    this.y = 480;
    this.alive = true;
    // initially static
    this.direction = 0;
    // place the computer at a random column
    this.col = int(random(0, numCols));
  }
  
  move(dir) {
    // if the right arrow is pressed
    if (dir === 1) {
      // numCols - 1 so that the last column is not accessible
      // last column space is reserved for placing the progress bar
      if (computer.col < numCols - 1) {
        computer.col++;
      }
    // left arrow is pressed
    } else {
      if (computer.col > 0) {
        computer.col--;
      }
    }
  }
  
  display() {
    this.x = colWidth * this.col + leftMargin;
    image(computerImg, this.x, this.y, computerWidth, computerHeight);
  }
}

A new dropping is created for every 1/3 second and its column and speed is randomly chosen. I settled on 1/3 second (frameCount % 20 === 0) among other values to make the game just challenging enough without frustrating the user by bombarding them with too many obstacles.

// create a new dropping 
if (frameCount % 20 === 0) {
  droppings.push(new Dropping());
}

for (let i = droppings.length - 1; i >= 0; i--) {
  let dropping = droppings[i];
  
  // check if any dropping has collided with the computer
  if (this.checkCollision(dropping, computer)) {
    // play sound effect
    blopSound.play();
    // remove the dropping from the array
    droppings.splice(i, 1);
    gameOverFail = true;
    break;
  } else {
    dropping.fall();
    dropping.display();
  }
  
  // remove droppings that have went beyond canvas
  if (dropping.y > height) {
    droppings.splice(i, 1);
  }
}

Problems I ran into

One of the biggest challenges was managing the state of the game such that the game can be restarted and the correct screens based on whether the game is in process, lost or won is shown without fail. I was able to do this by keeping track of three boolean variables:

let gameStarted = false;
let gameOverSuccess = false;
let gameOverFail = false;

...

// switch screens
if (!gameStarted) {
  instructions.display();
} else {
  game.run();
}

if (gameOverSuccess) {
  ...
} else if (gameOverFail) {
  ...
}

I track these variables in the draw() function and change what is shown on screen should any of these variables are modified.

Checking for collision between the computer and droppings was not very challenging as I implemented it as a simple function that checked whether any dropping overlaps in any direction with the computer, but it required configuring of values to ensure that collision was detected when the two appear to touch each other to the visible eye (instead of being triggered as soon as they barely touch) . As I had already tested creating a progress bar that fills upon a key press in the previous week as part of my midterm progress, incorporating it into the game took little time.

Future improvements

I quite like how the game turned out, both in terms of visuals and playing experience. However, one thought that I kept having was how nice it would be to be able to choose the difficulty of the game. I didn’t want to make the game too challenging so I settled on values for the speed and frequency of obstacles that would cater to the general audience, but this means that for certain groups of people who enjoy playing games, my game may appear very trivial. Therefore, one of the main improvements I would be interested in making is creating multiple degrees of difficulty (easy, medium, hard) that the user can choose from. I think this would make the game more engaging and potentially have users come back to the game to succeed in each round, instead of just attempting once.

Midterm concept and outline.

Initial concept

I initially wanted to make a top-down shooter game, I designed a lot of stuff, a collision algorithm for the bullets, obstacles, and enemies, a multitude of weapons with special traits, power ups, an infinitely-expanding map, and a bunch of other interactive elements. I got really frustrated with the implementation and gave up on the idea. I came back a day later and didn’t know whether I should abandon the game or not, so I changed to code to make it a horizontal shooter game,and  below is where i reached, before completetly abandoning the idea, even though I had almost all the logic and algorithms implemented, I just couldn’t work with a project a didn’t love.

Hence the delay of submitting this assignment. 

Current concept

I woke up 2 days after, I saw a spider on top in some corner no one goes to in my house with a small net. I decided it to befriend it and I called it Webster.

This sparked an idea in me, which is to make a game of a spider swinging around the world with a web. Then I drew my initial concept.

The most frightening part

This would probably be the implementation of gravity and the physics of the web/rope. I already implemented them though.

class Spider {
  constructor(x, y) {
    // store position & velocity in p5 vectors
    this.pos = createVector(x, y);
    this.vel = createVector(0, 0);
    // spider radius = 15 is used for collisions
    this.radius = 15;
    // track if spider is attached to rope or not
    this.attached = false;
  }

  update() {
    // apply gravity each frame
    this.vel.add(gravity);

    // if spider is attached, we do some rope physics
    if (this.attached && ropeAnchor) {
      // figure out how far spider is from anchor
      let ropeVec = p5.Vector.sub(ropeAnchor, this.pos);
      let distance = ropeVec.mag();

      // only if rope is stretched beyond rest length do we apply the spring force
      if (distance > ropeRestLength) {
        let stretch = distance - ropeRestLength;
        // hooke's law, f = k * x
        let force = ropeVec.normalize().mult(stretch * ropeK);
        // add that force to our velocity
        this.vel.add(force);
      }
    }

    // apply damping (which is basically air resistance)
    this.vel.mult(damping);

    // move spider according to velocity
    this.pos.add(this.vel);
  }

  show() {
    // draw the spider sprite instead of a circle
    push();
    imageMode(CENTER);
    image(spiderImg, this.pos.x, this.pos.y, this.radius * 2, this.radius * 2);
    pop();
  }
}