Midterm Project – Tank Game

Inspiration

I remember one of the game I played when I was a child having a small arcade handle. There was a specific tank game that I remember to be really difficult to even reach the fifth level because of the small screen and the aggressive attack from the enemies. Below is an image of the game I mentioned:Therefore, I want to replicate this game as my midterm project using p5.js.

Character design:

There are only 2 types of characters in the game: the Player and the Enemies. As similar to the inspiration game, I made the player to have the exact design. It has 2 rows and a body and a head to shoot the bullets. The layout and the head position will change based on the key arrows. Below is the illustration:Next is the enemy design. Instead of having the same enemy design, I made the enemies to have a plus sign. This is because the enemies will not move from the their position. However, I want to make it more difficult to win, so I made the enemies to have the ability to change the shooting direction based on the player’s position.

Below is the code snippet for the changing of enemies’ shooting direction:

//change the bullet direction based on the player's position
for (let i = 0; i < enemiesList.length; i++) {
  if (player.y == int(enemiesList[i].y)) {
    if (player.x < enemiesList[i].x) {
      enemiesList[i].direction = 3;
    } else {
      enemiesList[i].direction = 1;
    }
  }
  if (player.x == int(enemiesList[i].x)) {
    if (player.y < enemiesList[i].y) {
      enemiesList[i].direction = 0;
    } else {
      enemiesList[i].direction = 2;
    }
  }
}

Game structure:

Initially, the first thing the user will see is the Main game page. The page includes a “Start” button and a button for instruction. It looks like below:

The player can use the arrow keys to move around and use the space bar to shoot at the enemies. The game goal is not to obtain to highest score but to survive as long as possible. Below is the main screen game, it includes player, enemies and level information:

Finally, if the player’s character is hit by the enemy’s bullet, it will display the “Game Over” screen and display the highest level achieved:

Below is the code snippet for the Player, Enemy and Bullet objects:

class Player {
  constructor(x, y, w) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.direction = 0;
    this.layout = 1;
  }
}

class Enemy {
  constructor(x, y, w) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.direction = int(random(3));
  }
}

class Bullet{
  constructor(x, y, w, direction, rectWidth, colorCode){
    this.x = x;
    this.y = y;
    this.w = w;
    this.direction = direction;
    this.rectWidth = rectWidth;
    this.colorCode = colorCode;
  }
}

Design and Sounds:

Since this is similar to an arcade game, I chose and the characters’ design to be built up from blocks of squares. Because of this, I chose to the fonts to have the similar effect, made up from different squares. I uploaded a font file from the Google Fonts (Silk).

For sound, there are 4 different sound effects. The first one is the background music that will play in throughout the game play experience. The second one is the shooting sound when the user hits the space bar. The third one is when the enemy is hit by the bullet. Lastly, there is a losing sound effect if the player is hit.

Below is the code snippet for all the sounds used:

sound = loadSound("sound/shoot.wav");
hit = loadSound("sound/hit.mp3");
bgSound = loadSound("sound/bg.mp3");
loseSound = loadSound("sound/lose.mp3");

Obstacles and Improvements:

It is really difficult to know the exact position of the characters to control when they are hit. Currently it only works partially. This means that even though the system recognizes that the player is hit, it takes a bit of time for the screen to change. My thought is that my math for coordinate checking is wrong. However, I tried to changes the coordinates multiple times but it does not seem to work.

Another thing that is really difficult to do is the enemies are spawned overlap with the player’s position. Sometimes, this make an immediate lose game because the user does not have enough time to move. I tried to but conditions but it does not seem to work.

For improvements, I want to make the enemies to be more interactive. I want them to move around according to the player’s position and shoot towards them. The current version only target the player partially.

Game link:

Fullscreen link

Midterm | Pyro Dancer Release

Game Description

Pyro Dancer is an endless-runner 2D game with heavy inspirations from notable titles, such as Castlevania and Metroid inspire the game. Paired with 8-bit sound and theme by Haynes, Pyro Dancer tries to mimic the retro feel of older games.

TOP |Castlevania -Grimoire of Souls- Official Website | KONAMI

Castlevania.

The player assumes the role of a knight, invading the evil castle. Upon entering the castle, he is greeted by dozens of fireballs. To win the game, the player must dodge the fireball and achieve 500 score points. If the player runs out of health points before reaching the score, they will die.

Development Process.

Knight Sprite

Sketches, Ideas, Drawings!

The game uses sprites, images, and the p5 Play library. All assets used are linked in this document, please refer to it for further information.  The library is extensively used to allow better control over the sprites and performance.

In the beginning, I sketched, drew, and created a map of my code. Generally, it is split into three parts: a) Hero class, which functions as a class that generates every component of the player’s character.

class Hero {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    //HEALTH
    this.heroHealth = 100;
    this.healthbar;
    //HERO
    this.hero;
    //BARRIER
    this.barrier;
    this.barrier2;
    this.barrierX = this.x;
  }

b) Fireball class, which serves as the group to generate, spawn, and display the fireball obstacles.

class Fireballs {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.amount;
    this.fireballs = new Group();
    this.fireball;
    this.fireballAni;
  }

c) Main sketch, which encompasses both previous classes and displays the game properly. In this file, many functions initialize, check, and update the game. The game itself is split into three parts: The main menu, the Game Screen, and the Victory / Death Screen.

Main Screen

Game Screen

Death Screen

Challenges & Difficulties

Figuring out how the sprite system works in the p5 Play library was quite a challenge, however, it helped me a ton when configuring the fireball and character. But because of the way they draw sprites, this also made a huge problem when it comes to display updates.

I tried to code the game to allow fullscreen mode, but it never really worked properly. However, the game is coded so that it fits any kind of display resolution, but a manual refresh is needed to properly display the elements inside. I suspect this is mostly because of how spaghettified my code has become to the point that I could not re-arrange ways to update the display.

Conclusion

I truly enjoyed the process of making games with p5 js. It is not the best tool to make one, but it does have some unique capabilities that are interesting to explore. However, at some point, I became frustrated and named a function psychopathically:

function butcherSprites() {
  //remove all sprites
  player.hero.remove();
  obstacle.fireballs.removeAll();
  player.healthbar.opacity = 0;
  homeButton.opacity = 1;
}
Try out the game here!

p5.js Web Editor | Pyro Dancer – Release (p5js.org) [Display with Code]

Pyro Dancer – Release (p5js.org) [Windowed Fullscreen]

Midterm Project: Space Navigator by Sihyun Kim

Final outcome:

Link to fullscreen:

https://editor.p5js.org/sihyunkim/full/o-b8ZVYnT

Concept of the Game

If someone asks me what my childhood favorite game was, I will say that my favorite game was Flappy Bird without any doubts. As a child, I found avoiding obstacles by controlling the bird up and down along with the increase in speed as the game progressed very intriguing and fun. Hence, for this midterm project, I wanted to create something working with the same logic but in a different style. So, inspired by my childhood favorite game- Flappy Bird, I created a game called “Space Navigator”. As its name suggests, the theme of the game is space exploration. The player is part of an astronaut team sent from Earth to explore Saturn. The mission of the player is simple. All the player has to do is avoid collision with the moving meteoroids on the way to Saturn and follow the path of the stars to arrive on Saturn.  

The images below are the concept image I drew while conceptualizing my project and the transparent images I utilized for my project. I wanted my midterm project to look cute and round. So, I searched for lots of images in the Canva. And ended up using the round-ish images below. Also, I wanted to give people the feeling of childish wonder. So, I intentionally chose a piece of music (video below) from a Nintendo game called Captain Toad: Treasure Tracker. I found this music to be a great fit to be  background music for my game as this music is not too loud but still not boring. Most importantly, this song gave me that feeling of “childish wonder”. So,  I decided to use this. 

Concept Image I drew for my project before working on it:

Images obtained from Canva that I have utilized for the game


The background music for the game:

How the game works: 

The game is quite straightforward. As mentioned above, all what player has to do is to avoid the meteoroids and follow the stars as much as he/ she can. Also, the player has to be careful to not touch the bottom and top boundaries to not lose track of the orbit.  And this is done by controlling the rocket. There are two modes in which the player could choose: Voice mode and key mode. As the modes’ names suggest, the voice mode enables the player to control his/her rocket using his/her voice volume. The voice volume is mapped to the adjustment of the velocity of the meteoroids. The higher the voice volume of the player is, the higher the rocket arises. The key mode is the same as the Voice Mode except that this mode involves controlling the rocket using the keyboard’s space bar instead of the player’s voice volume. The better the player plays the game, the faster the game goes. After “collecting” 15 stars, the game will end with the text that the player arrived at Saturn will appear. 

Explanation of codes for the game:

I have three essential components in my game: rocket, meteoroid, and stars. And I made  Meteoroids(), Rocket(), and Stars() classes for these components of the game. First of all, as the name suggests, the Meteoroids() class is responsible for making the moving meteoroids (obstacles) of the game and checking if they collide with the rocket. Inside this class, there are three functions: update(), show(), and checkCollision(). The update function is responsible to animate the meteoroid while the show function is responsible for depicting the meteoroid itself in the screen. Checkcollision is a function that does check Collision with the rocket. The logic behind the Checkcollision function follows the equation of the circle. Whenever collision with a rocket is detected, the flag named winningState changes to “lost”, indicating that the player lost. 

Next, the Rocket() class is responsible for properties related to the rocket that the player controls. In this class, there are four functions: gravity(), move(), update(), show(), and boundary check(). The gravity function stimulates gravity affecting the rocket. Meaning, that if the player does nothing, the gravity function will let the rocket move down. And move function is responsible for adjusting the velocity of the rocket based on the user’s voice volume or the space bar input. Using an if-else statement and a flag named “keycode”, I made sure that the rocket would only be controlled using the voice volume in voice mode and controlled by pressing the space bar in key mode. The update function is responsible for adding the adjustment from the move function to the y position of the rocket, resulting in the animation of a rocket being moved up or down. And show function is responsible for depicting the rocket in the function. The boundaryCheck() function checks if the rocket touched the top or bottom boundary by checking the position of the rocket and the coordinates of the boundaries. If the rocket touches the boundary, two flags, winningState and touchedBoundary, are changed. 

Thirdly, the Stars() class is the class responsible for generating the stats that will guide the rocket. Like the other two classes, the star function has show() and update() function which allows the stars to be animated and shown. One unique thing about the Stars class is that it has two collision check functions- one with the meteoroids and the other with the star. This is because I had to make sure that the stars did not overlap with the meteoroids and use collision detection with stars to know when to score the player. 

In the main sketch, codes related to ensuring the correct procedure of the game, global variables, and functions such as windowResized(), preload(), setup(), keyPressed(), and mousePressed() are located. Basically, windowResized() is responsible for resizing the game screen for the fullscreen mode, preload() is responsible for loading the images, fonts, and files for the game. In setup() function, I have put some properties of the game that should not be redrawn every time (e.g. background music).  The keyPressed() function is responsible for triggering the boost for the rocket in key mode when the space bar is pressed and letting fullscreen be activated/ deactivated when the “f” or “F” key is pressed. 

The mousePressed() is responsible for all conditions related to the mouse press. All the buttons in the work game work due to flags such as gameState being triggered in this mousePressed() function.  the draw() function handles the procedural aspects of the game, and the game’s state changes based on the value of the gameState variable. The use of if-else statements ensures that different screens are displayed depending on the current state of the game. I think this approach allows seamless transitions between various screens, guiding players through different gameplay stages.

Proud Things from the Project

The part which I am most proud of is also part of the star class. Particularly, I am proud of the code snippets attached below, which are responsible for generating stars. The first code snippet is the code of the checkCollision() method in the Stars class. As the first code snippet shows, the logic of a star’s detecting collision with the meteoroids involves the equation of the circle. It checks if the distance between the meteoroid and the star is greater than the sum of the size of the star and the radius of the meteoroid. The output of checkCollision() inside the star class is a boolean variable. It returns true if there was a collision detected and false if there was no collision. The second code snippet is the code snippet located in the main sketch, which simply ensures that the stars are making sure that stars are not being generated at the same position as the meteoroids. The logic of this code snippet using a while-loop is that it keeps generating new Star objects until the checkCollision() method of the Star object returns false. Then, we add the Star object into the array of the Star object. There will be only one object being pushed into an array at a time as we do not technically “save” the generated Star object in the while loop if it overlaps with the meteoroid object.

Code snippet of checkCollision() inside the Star class:

// method to check for collisions with meteoroids
checkCollision(meteoroids) {
  for (let i = 0; i < meteoroids.length; i++) {
    let distX = this.position.x - meteoroids[i].position.x;
    let distY = this.position.y - meteoroids[i].position.y;
    let distance = sqrt(distX * distX + distY * distY);
    if (distance < this.size + meteoroids[i].radius) {
      return true; // collision detected
    }
  }
  return false; // no collision detected
}
//method to check for collisions with the rocket
checkCollisionRocket(other,music) {
  let distX = this.position.x - other.position.x;
  let distY = this.position.y - other.position.y;
  let distance = sqrt(distX * distX + distY * distY);
  if (distance < this.radius + other.radius) {
    metRocket = true;
    music.play()
  }
}

The while-loop that ensures generated star doesn’t collide with meteoroids:

//checking for collision with stars
while (collision) {
  starX = windowWidth + 100;
  starY = random(windowWidth * 0.125, windowWidth * 0.875);
  newStar = new Stars(starX, starY); // Regenerate star
  collision = newStar.checkCollision(meteoroids);
}
//adding the created new Star object to the array if it does not collide with any meteoroids
stars.push(newStar);

I was just so proud of this part of my project because I felt like this was the hardest part of my project. I think that the collision check that the stars have is way more complicated than the collision check that the meteoroids have, because the stars have to consider where would the meteoroids be placed and make sure that stars are not being generated there. I thought using a while-loop that only exists when a star object does not collide with the meteoroid for this part of coding was a very simple but wise decision.  Visual-wise, I just find my output so cute and gives that sense of childish wonder. So, I am very proud of my intuitions(?) to choose the images and fonts  I have utilized. Also, I am proud of the little details I added to this project such as the sound effects when the buttons are clicked or when the mission failed/succeded. While they are very subtle, I think they altogether enhanced the quality of my work. So, I am very satisfied with my output. 

Areas of improvement and problems that you ran into: 

In the future, I think I can improve the game by adding more features. For instance, I could make the player have multiple “lives” per session of the game to enable the player to easily succeed in the mission. I could maybe also consider enabling a two-player key mode, where two players “race” to reach Saturn first. I think it will be also interesting if I allow the player to choose the level of difficulty in the first place or choose the types of obstacles that the player will encounter (e.g. aliens, meteoroids, or shooting stars).

Other than this, there weren’t lots of problems that I ran into, and I was able to resolve these as well.  One of the biggest problems I encountered was to ensure that stars were generated in a random place that didn’t overlap with the meteoroids. 

However, I was able to solve this problem by using a while loop (further explanation is explained above) and the solution to this problem became my proudest part of the project. Another problem I encountered was a very mysterious problem that I still don’t get why it was solved. I tried to load images but it didn’t work out well. I first thought that I perhaps made some spelling mistakes or syntax errors when writing the code. But, it turns out that I didn’t make one. After a few minutes of debugging, I just duplicated the project itself. The duplicated project just loaded the images properly when I did not do anything after duplicating it. I honestly still don’t get why the problem was caused and how it was resolved only through “duplicating” the project. 

Overall

As usual, I loved working on this project. This project was very fun to create and it improved my coding skills. I am very satisfied with the outcome, but I think it could have been improved by adding more modes or features as I have mentioned above. 

Thank you so much for reading my blog post 🙂 

 

Midterm Project – Rotten Rescue

Concept:

The initial concept of this game was to be something different and unusual of the games that we already see where the “good” item is collected and the bad item is supposed to be avoided. But since I am using apples, I decided to relate this a bit to physics and the Isaac Newton apple incident that led to him discovering gravity… This makes the game more interesting and leaves a chance to improve it and make it a bit educational as well.

Full screen display of the game: https://editor.p5js.org/jana_mohsen/full/jEKauH5OR

Game:


Design:

In this game, players maneuver a trash can to catch rotten apples while avoiding the clean ones. The game is structured into several screens: an introduction, instructions, the main game, and a game-over sequence. There are multiple classes used like the Apple Class that regulates both the types of apples, the Trashcan Class which ensures the moevments of the trashcan on the horizontal axis and lastly, the Bubbles Class which is just used to regulate the number, size and speed of apples that are being displayed in the game over page.

The most important part of the code are the functions for each page of the game. The (displayIntro()) function displays the introduction page, which greets with the player with the game title and a start button. Then the second function: (displayInstructions()) teaches the player the “how-to-play” guidelines, respectively, with options to start or proceed to the game. It also allows the player to input the game before starting to play.

During the active game phase, apples of two types—“clean” and “rotten”—descend from the screen’s top. Players control the trash can, moving it left or right to catch rotten apples while avoiding the clean ones. The game keeps score and includes a countdown timer, enhancing the challenge. It manages the player’s interaction with the game through the trash can’s movements when they press keys, allowing the player to move the trash can left or right to catch falling apples, facilitated by the trashCan.display() and trashCan.move() methods.

Within its cycle, runGame() also takes charge of spawning new apples at fixed intervals, alternating between “clean” and “rotten” types, which are then added to an array for tracking. As time progresses, it continuously updates the player’s score by incrementing by 10 based on the type of apples caught and counts down the game timer, implementing a sense of urgency and challenge. The function diligently checks for end-game scenarios, such as the depletion of the timer or accumulation of too many clean apples, transitioning to a game-over state if any conditions are met. Moreover, within this loop, each apple’s position is updated, and its status is checked to determine if it has been successfully caught by the trash can or missed. Apples that are caught or fall off the screen are removed from the gameplay loop, ensuring the game’s state remains current.

When the game ends, either from catching more than 2 clean apples or because the timer ended, the game trasitions to a game over page where the players final score appears. Finally, the game employs a “bubble” effect on the game-over screen for visual appeal, where bubbles generated by the createBubbles() function move around the screen.

Challenging part: 

The most interesting and challenging part of this game is creating the logic behind it. The rest is just assembling buttons, images and creating a nice aesthetic to the game.

function runGame() {
    inputName.hide();
  // Set Instruction.jpeg as the background
    image(imgInstructions, 0, 0, width, height); 

    trashCan.display();
    trashCan.move();
  // Displays score and timer at the top left
    displayScore(); 

    // Logic for apples falling and being collected or missed
    if (frameCount % 60 === 0) {
        let type = random(['clean', 'dirty']);
        apples.push(new Apple(type));
    }

    for (let i = apples.length - 1; i >= 0; i--) {
        apples[i].move();
        apples[i].display();

        if (apples[i].y + apples[i].size > trashCan.y && apples[i].x > trashCan.x && apples[i].x < trashCan.x + trashCan.width) {
            if (apples[i].type === 'dirty') {
                score += 10;
            } else {
                redApplesCollected++;
                if (redApplesCollected >= 3) {
                    currentPage = 'gameOver';
                  // Create bubbles for game over animation
                    createBubbles(20); 
                    break;
                }
            }
          // Remove collected or missed apple from array
            apples.splice(i, 1); 
        } else if (apples[i].y > height) {
          // Remove apple that falls off screen
            apples.splice(i, 1); 
        }
    }

    // Timer countdown and check for game over condition
    if (frameCount % 60 === 0 && gameTimer > 0) {
        gameTimer--;
    } else if (gameTimer === 0) {
        currentPage = 'gameOver'; // Time's up
        createBubbles(20); // Prepare game over screen
    }

    // Drawing and positioning Start Over and Back buttons
    drawGameButtons();
}

The main issue of the project was developing the logic for this game, which proved to be the most major challenge during development. The complex interactions between the various components of the game—like the falling apples, the moving garbage can, and the score system—needed to be carefully thought out and executed. To guarantee seamless gaming and compliance with the game’s regulations, the logic controlling the game’s behavior has to be strong and effective. In order to guarantee precise and reliable performance, handling edge circumstances such as identifying collisions between apples and the trash can or controlling the game’s timing required careful coding and testing.

The code consists of many parts. The trashcan is displayed and moved by the function in response to the players key press. It also shows the amount of game time left and updates the player’s score. The reasoning behind the apples falling is the most important aspect of the function. At regular intervals, it produces red apples, randomly classifying them as normal or rotten. The apple array is then iterated over, with each apple being moved and displayed as the program looks for collisions with the trash can. If an apple is detected, the apple is taken out of the array and the score is changed appropriately. On the other hand, if an apple is missed, it is removed, and if the player has missed too many and it is rotten, the game ends. Lastly, the function controls the game timer, which starts a countdown every second and enters the game over state when the allotted time is up. It also draws and arranges buttons so that you can go back to the main menu or resume the game. In order to create a compelling gaming experience, this function coordinates the interplay between game objects and player input, encapsulating the fundamental gameplay logic.

Limitations and improvements:

The game can have better ways to make it a bit competitive like adding a high score board to show the scores of all the players. Moreover, the amount of apples is random so it might be unfair between players since one can have 10 rotten apples and another only 5. All of these issues are seen as parts to be improved to make the game more appealing and competitive.

Game visuals: 

Introduction page:

Instructions page:

Game page:

Game over page:

Assignment 4: UAE Population Map Visualization

For this week’s assignment, we were tasked with either making some sort of data visualization, or creating a generative text output. I decided to make a visualization of the population of some of the major cities of the UAE on a map of the country.

I started by retrieving a map of UAE from Google Images, uploading it to my sketch, then loading it in the setup and resizing it to fit my canvas. To obtain the canvas coordinates of each city I used the following function to print mouse location to the console (which I commented out in the final sketch):

print(mouseX, mouseY);

Afterwards, I used ChatGPT to obtain an estimate of each city’s population (to save time) along with the coordinates I obtained earlier to create an array with each city’s position and population:

let cities = [
  { name: "Abu Dhabi", x: 440, y: 240, population: "1,480,000" },
  { name: "Al Ain", x: 625, y: 285, population: "656,000" },
  { name: "Madinat Zayed", x: 333, y: 335, population: "10,000" },
  { name: "Dubai", x: 575, y: 150, population: "3,380,000" },
  { name: "Sharjah", x: 600, y: 135, population: "1,400,000" },
  { name: "Khor Fakkan", x: 730, y: 140, population: "33,575" },
  { name: "Ajman", x: 610, y: 130, population: "500,000" },
  { name: "Fujairah", x: 720, y: 170, population: "230,000" },
  { name: "Dibba Al Hisn", x: 720, y: 105, population: "26,395" },
  { name: "Umm Al Quwain", x: 620, y: 115, population: "80,000" },
  { name: "Ras Al Khaimah", x: 670, y: 85, population: "350,000" },
  { name: "Ar Rams", x: 690, y: 70, population: "16,000" }
];

Instead of a traditional for loop, I used a forEach loop that I had seen online to draw a point for each location in the array and display the population when the user’s mouse hovers on each point, and this was my final result:

Looking to the future, I guess I could use a better map and add more cities with more accurate population estimates.

Assignment 2: Cloudy Day

For this assignment, we were tasked with using either a for loop or a while loop to make a simple work of art. I decided to make a UAE flag with some clouds moving in the background using loops.

I started by randomizing 5 starting positions for the clouds to make them different every time the program is run.

let cloudPositions = []; // Array to hold the positions of clouds

function setup() {
  createCanvas(800, 600);
  
  // Create 5 clouds at random positions and add them to the array
  for (let i = 0; i < 5; i++) { 
    cloudPositions.push(random(-50, width - 50));
  }
}

After that I used another for loop to move the clouds in the using the drawCloud() function I wrote:

fill('white');
for (let i = 0; i < cloudPositions.length; i++) {
  drawCloud(cloudPositions[i], 100 + i * 80); // Drawing each cloud at a different vertical position
  cloudPositions[i] += 1; // Moving the cloud to the right
  if (cloudPositions[i] > width) { // Reset cloud position after moving off screen
    cloudPositions[i] = -100;
  }
}

For reference, this is my drawCloud function, it uses multiple ellipses to make a cloud shape:

function drawCloud(x, y) {
  ellipse(x, y, 60, 60);
  ellipse(x + 20, y - 20, 70, 70);
  ellipse(x + 40, y, 50, 50);
  ellipse(x + 60, y - 10, 60, 60);
}

Overall, this is my final art piece:

Looking to the future, this artwork seems very plain, I could add some more elements such as a sun or some buildings.

Lord of The Maze – Midterm – Dachi Tarughishvili

Sketch (Fullscreen) https://editor.p5js.org/dt2307/full/vrBxuAsfN

(I have embedded here just in case but please open it in separate tab for everything to properly work (most importantly audio))

Idea: Lord of the Rings inspired game, where a player takes initiative to escape the maze, avoid the orcs and flying monster, find the ring and reach the mount of doom

Project summary: This game takes inspiration from the epic fantasy novel The Lord of the Rings, which my previous project Eye of Sauron was also based on. In this maze-style game, the main character Frodo must navigate through a maze and reach Mount Doom, the volcanic location in Mordor where the powerful ring was forged and the only place it can be destroyed. Roaming orcs patrol the maze pathways, which Frodo must avoid. Coming into direct contact with an orc reduces Frodo’s health. If Frodo loses all three health points, the game is over. If Frodo successfully reaches Mount Doom in time, the player wins the game and an image is displayed. The goal is to guide Frodo through the maze while evading orcs in order to make it to Mount Doom and destroy the ring. However, there is a catch, if you reach mount of doom without obtaining the ring, flying monster will start chasing you and at that point you should get the ring as soon ass possible to evade it. Once you capture the ring, Sauron animation will be displayed (based on my previous project with the perlin noise after Coding Train intro). After that you can see game become more gloomy as colors start to change, background included. Fortunately, due to magical powers of the ring you are granted an invisibility buff which lasts for certain amount of time. The visual cue is there for player by reducing Frodo’s transparency as well as audio cue which gets more frequent with more pulses indicating when you are gonna run out. Finally, you are able to reach mount of doom and destroy the ring if you get through the remaining orcs!

Inspiration: this game is inspired by lord of the rings movies (books):  The Fellowship of the Ring (2001), The Two Towers (2002), and The Return of the King (2003). I want to recreate an experience where player gets to have their own journey, traversing long distance, making strategic choices, avoiding the danger and reaching destination similar to what happens in the movies.

Visuals: the maze itself is black on green canvas. Characters have their own images (orc, frodo, mount of doom etc.). They are in pixel art style to give players a nostalgic feeling which also makes whole game work on this platform much smoother. The main menu screen as well as instructions and game won game over screen are AI generated, but text on top is using custom font.

Process and Challenges: I made sure to utilize an object-oriented approach. There were several development hurdles. Firstly, after designing the maze layout and slowly incorporating it into code to test functionality, I struggled greatly with collision detection (characters could access the maze improperly from certain sides) which took substantial time to correct. Additionally, programming the repetitive orc movements to patrol the maze appropriately relied heavily on trial-and-error to determine optimal pathways. (And lots of Googling!). Last few days, I also added sounds which were not too difficult but took some time to pick right soundtracks and make it working. Volume slider was a bit tricky as I head to read documentation online because I did not like the way its default behavior worked. I also added countdown which lets player see their current time as well as total time they took to beat the challenge. Additionally, I fixed issue with ring, and volume slider being displayed over game over screen and such. I added even more soundtracks, for getting the ring and spawning the ring. Moreover, I implemented features such as flying monster which spawns and moves towards frodo if he goes to mount of doom without picking up the ring. Upon picking up the ring, I added a feature based on my last project where eye of sauron animation gets displayed (which was done using perlin noise). This comes with change in background as well as another feature – Invisibility. In simple terms, frodo becomes more transparent visually, a sound for invisibility starts playing and in specific timeframe he is immune to enemies. I added another orc near ring to make getting it more challenging. Last but not least, ring gets spawned only if Frodo reaches certain area in the map, to ensure that player can’t just camp at base and wait for ring to spawn if there was a timer instead, making game much simpler.

Here are some development progress pictures (I have not included every one of them) :

Code:

I have separate JS classes for different functions of the game.

Lets go over most of them (briefly, more details are in code comments):

Drawing UI Class: takes care of top bar with health, volume and timer texts.

function drawUI() {
  // Draw Health text
  fill(255);
  textSize(14);
  noStroke();
  text("Lives: " + playerHealth, 55, 11);

  // Draw Volume text
  fill(255);
  textSize(14);
  noStroke();
  text("Volume:", 150, 11);
  // Make sure volume slider is visible
  volumeSlider.style('display', 'block');

  // Draw Timer
  fill(255);
  textSize(14);
  text("Time: " + playTime.toFixed(1) + "s", width - 60, 11);

  // Set volume based on slider value
  initialVolume = volumeSlider.value();
  backgroundMusic.setVolume(initialVolume);
}

Orc class: takes care of spawning as well as moving orcs (also makes sure they don’t go in the maze)

class Orc {
  constructor(pointA, pointB, spawn) {
    this.pointA = pointA; //start
    this.pointB = pointB; //end
    this.size = 20;
    this.speed = 1.2;

    //initial spawn
    this.x = spawn.x;
    this.y = spawn.y;

    // target
    this.currentTarget = this.pointA;
  }

  display() {
    image(orcImg, this.x, this.y, this.size, this.size);
  }

  move() {
    let dx = this.currentTarget.x - this.x;
    let dy = this.currentTarget.y - this.y;
    let length = sqrt(dx * dx + dy * dy); //direction vector

    if (length > 0) {
      dx /= length; //normalize vector for consistent speed
      dy /= length;
      
      //calculate new position
      let newPosX = this.x + dx * this.speed;
      let newPosY = this.y + dy * this.speed;

      if ( //if new position is in bound and does not collide with walls
        newPosX > 0 &&
        newPosX < width - this.size &&
        newPosY > 0 &&
        newPosY < height - this.size &&
        maze[getRow(newPosY)][getCol(newPosX)] !== '#'
      ) {
        this.x = newPosX;
        this.y = newPosY;

        // check if orc reached target
        if (dist(this.x, this.y, this.currentTarget.x, this.currentTarget.y) < this.speed) {
          // switch points
          this.currentTarget = this.currentTarget === this.pointA ? this.pointB : this.pointA;
        }
      }
    }
  }
}

function generateLevel() {
  orcs = [];
  orcs.push(new Orc({ x: 28, y: 350 }, { x: 28, y: 180 }, { x: 28, y: 180 }));
  orcs.push(new Orc({ x: 605, y: 100 }, { x: 605, y: 400 }, { x: 605, y: 180 }));
  orcs.push(new Orc({ x: 452, y: 420 }, { x: 452, y: 250 }, { x: 452, y: 250 }));
  orcs.push(new Orc({ x: 260, y: 605 }, { x: 455, y: 605 }, { x: 455, y: 605 }));
  orcs.push(new Orc({ x: 300, y: 100 }, { x: 200, y: 100 }, { x: 200, y: 100 }));
  // orcs and their pathways
}

Player class: initializes player, as well as deals with maze collision and invisibility buff.

class Player {
  constructor() {
    this.size = 20; 
    this.speed = 3;
    this.spawn();
  }

  display() {
    if (millis() < invincibleUntil) {
      tint(255, 63); //25% transparency
    } else {
      tint(255, 255); 
    }
    image(playerImg, this.x, this.y, this.size, this.size);
  }
  move() {
    
    if (eyeOfSauronActive) { //cant move if active
      return; 
    }
    let newX = this.x;
    let newY = this.y;
    //movement
    if (keyIsDown(LEFT_ARROW) && this.x > 0) {
      newX -= this.speed;
    } else if (keyIsDown(RIGHT_ARROW) && this.x < width - this.size) {
      newX += this.speed;
    }
    if (keyIsDown(UP_ARROW) && this.y > 0) {
      newY -= this.speed;
    } else if (keyIsDown(DOWN_ARROW) && this.y < height - this.size) {
      newY += this.speed;
    }

    if (!this.collidesWithWall(newX, this.y) && !this.collidesWithWall(newX, newY) && !this.collidesWithWall(this.x, newY) && !this.collidesWithWall(newX, newY)) {
      this.x = newX;
      this.y = newY; //updates if there are no collisions with walls
    }
  }
  //collision
  collidesWithWall(x, y) {
    
    //calculates grid indices with helpers
    let left = getCol(x);
    let right = getCol(x + this.size - 1);
    let top = getRow(y);
    let bottom = getRow(y + this.size - 1);
    
    //checks if any grids around player position has # (meaning wall)
    return ( //returns true if collision happens if not false ( or conditions)
      maze[top][left] === '#' ||
      maze[top][right] === '#' ||
      maze[bottom][left] === '#' ||
      maze[bottom][right] === '#'
    );
  }
    //initial spawn
  spawn() {
    this.x = 30;
    this.y = 30;
  }
}

Ring Class: takes care of spawning golden ring as well as checking its collision for player and determining invisibility buff time

const ringSpawnLocation = { row: 10, col: 20 }; //properties
let goldenRingRadius = 10;

function checkRingCollision() {
  if (goldenRing && dist(player.x, player.y, goldenRing.x, goldenRing.y) < goldenRing.size) { //if ring exists and distance is less than rings radius
    //activate sauron and invis buff sequence
    collidedWithRing = true;
    eyeOfSauronActive = true;
    invincibleUntil = millis() + 40000; // make Frodo invincible for 30 seconds
    monsterSpawned = false;
     setTimeout(() => {
      invisibilitySound.play();
    }, 20000);
    if (!sauronSound.isPlaying() && !sauronSoundStarted) {
      sauronSound.play();
      sauronSoundStarted = true;
      goldenRing = null;
    }
  }
}

//creating golden ring
function createGoldenRing(x, y, size) {
  return {
    x,
    y,
    size,
  };
}



//drawing golden ring
function drawGoldenRing() {
  
  image(ringImage, goldenRing.x - goldenRingRadius, goldenRing.y - goldenRingRadius, goldenRingRadius * 2, goldenRingRadius * 2);
}

Game Management Class:  takes care of different game states, main menu state, game win, gameplay, gameover etc. It also displays main menu, instructions and helps with clearing as well as reloading objects and variables upon restart

function startGame() {
  gameStarted = true;
  this.remove(); // remove the Start Game button
  volumeSlider.style('display', 'block'); //displays volume slider

}

function returnToMainMenu() {
  currentScreen = 'mainMenu';
  backButton.hide(); // hide the back button when returning to main menu
}

function showInstructions() {
  currentScreen = 'instructions';
  backButton.show(); // show the back button when instructions are visible
}


function createRestartButton() {
  if (restartButton) {
    restartButton.remove(); // ensure any existing button is removed
  }
  let buttonWidth = 140; 
  let buttonHeight = 35; 
  let buttonX = (width - buttonWidth) / 2; // 
  let buttonY = 200; 
  restartButton = createButton('');
  restartButton.position(buttonX, buttonY);
  restartButton.size(buttonWidth, buttonHeight);
  restartButton.style('background-color', 'transparent');
  restartButton.style('border', 'none'); 
  restartButton.style('cursor', 'pointer');
  restartButton.mousePressed(restartGame);

  // change cursor on hover
  restartButton.mouseOver(() => restartButton.style('cursor', 'pointer'));
}


function gameWin() {
  gameState = 'win';
  backgroundMusic.stop();
  
  //  the game win image
  background(gameWinImg);

  //  text on top of the image
  textSize(31);
  stroke(0);
  strokeWeight(4);
  fill(255); 
  textAlign(CENTER, CENTER);
  text("You've reached the Mount of Doom!", width / 2, height / 2 -50);
  text("Journey Length: " + playTime.toFixed(1) + " seconds", width / 2, height / 2);

  winSound.play();
  if (monsterSound.isPlaying()) {
    monsterSound.stop();
  }
  volumeSlider.style('display', 'none');
  
  createRestartButton();
  noLoop(); //game pause
}


function gameOver() {
  gameState = 'gameOver';
  backgroundMusic.stop();
  
  // the game over image
  background(gameOverImg);
  
  //  text on top of the image
  textSize(45);
  stroke(0);
  strokeWeight(4);
  fill(255); 
  textAlign(CENTER, CENTER);
  text("Game Over!", width / 2, 100);
  text("Survival Time: " + playTime.toFixed(1) + " seconds", width / 2, 150);

  gameoverSound.play();
  if (monsterSound.isPlaying()) {
    monsterSound.stop();
  }
  
  volumeSlider.style('display', 'none');

  createRestartButton();
  noLoop(); // pause
}


function restartGame() {
  // stop sounds
  if (gameoverSound.isPlaying()) {
    gameoverSound.stop();
  }
  if (backgroundMusic.isPlaying()) {
    backgroundMusic.stop();
  }
  if (monsterSound.isPlaying()) {
    monsterSound.stop();
  }
  if (dyingSound.isPlaying()) {  
    dyingSound.stop();
  }
  if (sauronSound.isPlaying()) {
    sauronSound.stop();
  }
  if (invisibilitySound.isPlaying()) {
    invisibilitySound.stop();
  }
  if (winSound.isPlaying()) {
    winSound.stop();
  }

  // remove the restart button if it exists
  if (restartButton) {
    restartButton.remove();
    restartButton = null;
  }

  // reset the game state and variables for a new game
  resetGameState();

  // reset startTime to the current time to restart the timer
  startTime = millis();

  // ensure the game loop is running if it was stopped
  loop();
}



function resetGameState() {
  // reset game flags and variables
  gameStarted = true;
  gameState = 'playing';
  playerHealth = 3;
  playTime = 0;
  monsterSpawned = false;
  collidedWithRing = false;
  goldenRingSpawned = false;
  eyeOfSauronActive = false;
  eyeOfSauronDeactivated = false;
  eyeSize = 15;
  currentLevel = 1;
  
  // reset positions and states of game entities
  player.spawn();
  orcs = []; // clear existing orcs
  generateLevel(); // repopulate the orcs array

  if (volumeSlider) {
    volumeSlider.remove(); // ensure existing slider is removed before creating a new one
  }
  
  //new slider
  volumeSlider = createSlider(0, 1, 1, 0.01); 
  volumeSlider.position(180, 1.5);
  volumeSlider.style('width', '100px');
  volumeSlider.style('color', 'black');
  volumeSlider.style('outline', 'none');
  volumeSlider.style('background', '#white');
  volumeSlider.style('opacity', '0.7');
  volumeSlider.input(() => backgroundMusic.setVolume(volumeSlider.value()));

  // reset the background music volume and play it if not already playing
  backgroundMusic.setVolume(1); // set initial volume
  if (!backgroundMusic.isPlaying()) {
    backgroundMusic.loop();
  }

  // ensure the game loop is running if it was stopped
  loop();
}

Maze class: takes care of maze layout as well as drawing maze. There are two layouts, first one is official game one and second one is for quick testing. It uses helper functions to divide canvas into grids and then draws a maze if it finds # in a grid. It is using a graphic which uses a wall texture and for other places in grids we have grass texture.

let maze = [
  "##########################",
  "#        #   # #   #   # #",
  "# #### # # #   # # # # # #",
  "#   #  ##### ### # # # # #",
  "# #### #     #   # # # # #",
  "#      # # # # ### # # # #",
  "#####    # # # # # # # # #",
  "#   #  ### ### # # # # # #",
  "# # #  # #     # # # #   #",
  "# # # ## ####### ### ### #",
  "# #      #         # # # #",
  "# ################ # # # #",
  "#        #   #     # # # #",
  "# ######## ### ### # # # #",
  "#        # # # # # # # # #",
  "######## # #   # # # # # #",
  "# #    # # # ### # # # # #",
  "#    #   # # #     # # # #",
  "# ## ##### # # ##### # # #",
  "# #  #       #   #   #   #",
  "# #  # ######### # ### ###",
  "# ####   # #   # # # #   #",
  "# #  # # # #   # # # # ###",
  "# ## ### # # ### # # #    ",
  "#        #         #      ",
  "##########################",
];


// let maze = [
//   "                          ",
//   "#        #   # #   #   # #",
//   "# #### # # #   # # # # # #",
//   "#   #  ##### ### # # # # #",
//   "# #### #     #   # # # # #",
//   "#      # # # # ### # # # #",
//   "#####    # # # # # # # # #",
//   "#   #  ### ### # # # # # #",
//   "# # #  # #     # # # #   #",
//   "# # # ## ####### ### ### #",
//   "# #      #         # # # #",
//   "# ################ # # # #",
//   "#        #   #     # # # #",
//   "# ######## ### ### # # # #",
//   "#        # # # # # # # # #",
//   "######## # #   # # # # # #",
//   "# #    # # # ### # # # # #",
//   "#    #   # # #     # # # #",
//   "# ## ##### # # ##### # # #",
//   "# #  #       #   #   #   #",
//   "# #  # ######### # ### ###",
//   "# ####   # #   # # # #   #",
//   "# #  # # # #   # # # # ###",
//   "# ## ### # # ### # # #    ",
//   "#        #         #      ",
//   "##########################",
// ];

// helper functions for row and col 
function getRow(y) {
  return floor(y / 30);
}

function getCol(x) {
  return floor(x / 30);
}

function drawMaze() {
  for (let i = 0; i < maze.length; i++) {
    for (let j = 0; j < maze[i].length; j++) {
      let x = j * 25;
      let y = i * 25;
      if (maze[i][j] === '#' && !eyeOfSauronActive) {
        // Draw wall texture only if Eye of Sauron is not active
        image(wallBuffer, x, y, 25, 25, x, y, 25, 25);
      } else if (drawGrass && maze[i][j] !== '#') {
        // Draw grass texture over the green areas (paths) if drawGrass is true
        // and the current cell is not a wall.
        image(grassBuffer, x, y, 25, 25, x, y, 25, 25);
      }
    }
  }
}

MountDoom Class: creates mount doom, uses a function for tracking and moving monster towards Frodo as well as a function which determines if Frodo is inside mount of doom range.

function moveMonsterTowardsFrodo() {
  let dx = player.x - monsterX;
  let dy = player.y - monsterY;
  let angle = atan2(dy, dx); //angle between monster and player
  monsterX += monsterSpeed * cos(angle);
  monsterY += monsterSpeed * sin(angle);
  //update monster position based on calculated angle
}


class MountOfDoom {
  constructor() {
    this.x = width - 75;
    this.y = height - 95;
    this.size = 75;
  }
}


function createMountOfDoom() {
  return new MountOfDoom();
}


function playerReachedMountOfDoom() {
  return (
    !monsterSpawned && //monster has not spawned and its in bounds
    player.x + player.size > mountOfDoom.x &&
    player.x < mountOfDoom.x + mountOfDoom.size &&
    player.y + player.size > mountOfDoom.y &&
    player.y < mountOfDoom.y + mountOfDoom.size
  );
}

This is Eye of Sauron class:

it takes care of Eye of Sauron animation (used from one of the previous projects). This is drawn using various perlin noise loops. It also has activation and eye increase rate after predetermined time period. (It times well with audio e.g. death = engulfed in darkness).

let orange = 165; // clicking color variable
let size_t = 100; // clicking size variable

function drawEyeOfSauron() {
  background(0, 0, 0, 3);
  push();
  translate(width / 2, height / 2);
  let noiseMax = 5; // fixed value for spikiness
  let alphaValue = 400;

  
  eyeSize += 0.05;

  // outer shape
  stroke(255, 10, 0, alphaValue);
  noFill();
  beginShape();
  for (let a = 0; a < TWO_PI; a += 0.1) {
    let xoff = map(10 * cos(a + phase), -1, 1, 0, noiseMax);
    let yoff = map(sin(a + phase), -1, 1, 0, noiseMax);
    let r = map(noise(xoff, yoff, zoff), 0, 1, 100, 220) * (eyeSize / 20); // scale based on eyesize
    let x = r * cos(a);
    let y = r * sin(a);
    vertex(x, y);
  }
  endShape(CLOSE);

  // orange glow for the first outer shape
  fill(255, orange, 0, alphaValue * 0.5); // lower transp
  beginShape();
  for (let a = 0; a < TWO_PI; a += 0.1) {
    let xoff = map(8 * cos(a + phase), -1, 1, 0, noiseMax);
    let yoff = map(8 * sin(a + phase), -1, 1, 0, noiseMax);
    let r = map(noise(xoff, yoff, zoff), 0, 1, 0, size_t) * (eyeSize / 20); // scale based on eyesize
    let x = r * cos(a);
    let y = r * sin(a);
    vertex(x, y);
  }
  endShape(CLOSE);

  // second glow
  fill(255, 165, 0, alphaValue * 0.5);
  beginShape();
  for (let a = 0; a < TWO_PI; a += 0.1) {
    let xoff = map(10 * cos(a + phase + 1), -1, 1, 0, noiseMax); // different phase
    let yoff = map(10 * sin(a + phase + 1), -1, 1, 0, noiseMax);
    let r = map(noise(xoff, yoff, zoff), 0, 1, 50, 220) * (eyeSize / 20); // scale based on eyesize
    let x = r * cos(a);
    let y = r * sin(a);
    vertex(x, y);
  }
  endShape(CLOSE);

  // inner pupil black which is a vertical ellipse
  fill(0); // black
  beginShape();
  for (let a = 0; a < TWO_PI; a += 0.1) {
    let xoff = map(5 * cos(a + phase), -1, 1, 0, noiseMax);
    let yoff = map(5 * sin(a + phase), -1, 1, 0, noiseMax);
    let rx = map(noise(xoff, yoff, zoff), 0, 1, 5, 20) * (eyeSize / 20); // scale based on eyesize
    let ry = map(noise(yoff, xoff, zoff), 0, 1, 50, 120) * (eyeSize / 20); // scale based on eyesize
    let x = rx * cos(a);
    let y = ry * sin(a);
    vertex(x, y);
  }
  endShape(CLOSE);

  zoff += 0.008;
  phase += 0.008;
 
  if (eyeOfSauronActive && sauronSound.isPlaying()) {
    let timeRemaining = sauronSound.duration() - sauronSound.currentTime();
    if (timeRemaining < 0.7) { 
      eyeSize += 50;
    }
  }

  pop();
}

And lastly, the code I am most proud, where everything comes together is my sketch code:

This is where, variables, preload and setup is made. You can see detailed list in the code but in summary it takes care of initializing objects, creating buttons, slider as well as separate graphic for textures.

The next section is draw function which has different if conditions for different states. For example, if game has not started are we in instructions or main menu. We also have additional drawings, game state function, and references to previous classes to make everything initialize and work well together. Getting everything work well together was through multiple hours of trial and error but eventually the experience created was pretty fluid with no significant performance or visual bugs.

//Variables

//Time
let startTime; 
let playTime = 0;
let mountOfDoomTime = 0;

//Objects
let playerImg, orcImg, mountOfDoomImg;
let player, orcs, playerHealth, mountOfDoom;

//Audio

let volumeSlider;
let winSound;
let backgroundMusicStarted = false;

//ring
let goldenRing;
let goldenRingSpawned = false;
let collidedWithRing = false;

//sauron
let eyeSize = 15;
let eyeOfSauronActive = false;
let isSauronSoundLowered = false;
let eyeOfSauronDeactivated = false;
let zoff = 0;
let phase = 0;
let noiseMax = 0;
let sauronSoundStarted = false;

//monster
let monsterImg;
let monsterSpawned = false;
let monsterSpeed = 0.3;
let monsterX, monsterY;
let monsterCheck = false;
let monsterSizeMultiplier = 0.2;

//buff
let invincibleUntil = 0;

//state 
let gameWinImg, gameOverImg;
let gameState = 'playing'; 
let gameStarted = false;

//font
let pixelFont;

//for managing 
let restartButton;
let mainmenu;
let currentScreen = 'mainMenu';
let newBackgroundImg;

//Maze Management Misc
let drawGrass = true;

//Preload

function preload() {
  playerImg = loadImage('frodo.png');
  orcImg = loadImage('orc.png');
  mountOfDoomImg = loadImage('volcano.png');
  backgroundMusic = loadSound('lotr.mp3');
  dyingSound = loadSound('dying.mp3');
  gameoverSound = loadSound('gameoversound.mp3');
  winSound = loadSound('win.mp3');//all the sounds
  ringImage = loadImage('ring.png');
  sauronSound = loadSound("sauron.mp3")
  monsterImg = loadImage('monster.gif');
  invisibilitySound = loadSound('invisible.mp3');
  ringSpawnSound = loadSound('ringspawn.mp3');
  monsterSound = loadSound('monster.mp3');
  gameWinImg = loadImage('game_won.png');
  gameOverImg = loadImage('game_over.png');
  pixelFont = loadFont('alagard.ttf');
  mainmenu = loadImage('mainmenu.png')
  newBackgroundImg = loadImage('instructions.png');
  grassTexture = loadImage('grass.jpeg');
  wallTexture = loadImage('wall.jpg');
  
}

//Safari Bug (audio does not autoplay, not a problem on chromium)
function keyPressed() {
  // start background music when a key is pressed
  if (!backgroundMusicStarted) {
    backgroundMusicStarted = true;
    backgroundMusic.play();
  }
}

//Setup

function setup() {
  textFont(pixelFont); //using external font
  frameRate(60);
  startTime = millis(); //for calculating journey time in the end
  let initialVolume = 1;
  createCanvas(650, 650);
  generateLevel(); // ?
  player = new Player(); //initialize player
  playerHealth = 3; //initialize player health
  window.addEventListener('keydown', function (e) {
    if (e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
      e.preventDefault(); // prevent default arrow key behavior for safari bug (moving screen)
    }
  });
  
  //monster
  monsterX = width / 2;
  monsterY = height / 2;
 
  mountOfDoom = createMountOfDoom();
  
  //vol slider
  volumeSlider = createSlider(0, 1, initialVolume, 0.01);
  volumeSlider.position(180, 1.5);
  volumeSlider.style('width', '100px');
  volumeSlider.style('color', 'black');
  volumeSlider.style('outline', 'none');
  volumeSlider.style('background', '#white');
  volumeSlider.style('outline', 'none');
  volumeSlider.style('opacity', '0.7');
  volumeSlider.style('transition', 'opacity .2s');


  // mouse over effect
  volumeSlider.mouseOver(() => {
    volumeSlider.style('opacity', '1');
  });

  volumeSlider.mouseOut(() => {
    volumeSlider.style('opacity', '0.7');
  });

  // webkit for browsers
  volumeSlider.style('::-webkit-slider-thumb', 'width: 25px; height: 25px; background: #04AA6D; cursor: pointer;');
  
  volumeSlider.style('display', 'none');

  backgroundMusic.loop();
  
  //start button
  let startButton = createButton(''); //empty because i couldnt get button itself have external font, so it realies on empty button and actual clickable text superimposed on it
  let buttonWidth = 140; 
  let buttonHeight = 35; 
  let buttonX = (width - buttonWidth) / 2; // center the button horizontally
  let buttonY = 175; 
  startButton.position(buttonX, buttonY);
  startButton.size(buttonWidth, buttonHeight);
  startButton.style('background-color', 'transparent');
  startButton.style('border', 'none'); // no border
  startButton.style('cursor', 'pointer');

  // start game on click
  startButton.mousePressed(startGame);
  
  // question, instruciton button
  
  questionMarkButton = createButton('?'); 
  questionMarkButton.position(width/2-45, 15); 
  questionMarkButton.style('background-color', 'transparent');
  questionMarkButton.style('border', 'none');
  questionMarkButton.style('color', '#FFFFFF'); // text color
  questionMarkButton.style('font-size', '50px'); // size 
  questionMarkButton.style('background-color', 'black'); //  background color 
  questionMarkButton.style('color', '#FFFFFF');  //question mark color
  questionMarkButton.style('padding', '7px 35px'); // pading
  questionMarkButton.style('border-radius', '5px'); // rounded corners

  // managing mouse over effect
  questionMarkButton.mousePressed(showInstructions);

  questionMarkButton.mouseOver(() => questionMarkButton.style('color', '#FFC109')); // change color on hover
  questionMarkButton.mouseOut(() => questionMarkButton.style('color', '#FFFFFF')); // revert color on mouse not hovering

  // arrow button 
  backButton = createButton('←'); 
  backButton.position(10, 10); 
  backButton.mousePressed(returnToMainMenu);
  backButton.hide(); // hide it initially
  
  //buffer for creating another canvas instance
  grassBuffer = createGraphics(width, height);
  
  // grass texture with reduced transparency
  grassBuffer.tint(255, 100); // a bit transparent for visuals
  grassBuffer.image(grassTexture, 0, 0, width, height);
  
  // buffer for the wall texture
  wallBuffer = createGraphics(width, height);

  // scale the buffer before drawing the image
  wallBuffer.push(); // save
  wallBuffer.scale(2); // scale up
  wallBuffer.image(wallTexture, 0, 0, width * 2, height * 2); 
  wallBuffer.pop(); // restore
  
}

//Draw

function draw() {
  if (!gameStarted) { //for menu and instructions
    if (currentScreen === 'mainMenu') { //main menu
      
      background(mainmenu); //background image
      //text properties
      textSize(38);
      stroke(0);
      strokeWeight(5);
      textAlign(CENTER, CENTER);
      text('Lord of the Maze', width / 2, 150);

      textSize(24); //start
      let startGameText = "Begin Journey!";
      let startGameWidth = textWidth(startGameText);
      let startGameX = width / 2 - startGameWidth / 2;
      let startGameY = 180 - 2; // y position of text
      let startGameHeight = 24; // height of the text

      // mouse hover detection based on text position and size
      if (mouseX >= startGameX && mouseX <= startGameX + startGameWidth && mouseY >= startGameY && mouseY <= startGameY + startGameHeight) {
        fill("#FFC109"); // change color to indicate hover
      } else {
        fill("#FFFFFF"); // defauult color
      }

      textAlign(CENTER, CENTER);
      text(startGameText, width / 2, 180 + 12); // draw begin jorney text
      questionMarkButton.show();

    } else if (currentScreen === 'instructions') {
      background(newBackgroundImg); // show the instructions background
      fill(255); //  text color
      textSize(20); //  text size
      textAlign(CENTER, CENTER);
      text("Oh valiant traveler, embroiled in a quest most dire: \n to traverse the winding labyrinths of Middle-earth and \n consign the accursed One Ring to the molten depths  of Mount Doom. \n Be forewarned, the path is fraught with peril, \n and the all-seeing Eye of Sauron ever seeks to ensnare thee. \n \nEmploy the sacred Arrow Keys \n to navigate the maze's enigmatic corridors. \n Each stride shall bring thee closer to thy destiny or doom. \n Avoid orcs! Find one ring and reach Mount of Doom", width / 2, 130);
      questionMarkButton.hide();
      
    }
  } else {
      if (gameState === 'playing') {
        questionMarkButton.hide();
      // change background color based on whether the Eye of Sauron has deactivated
      if (!eyeOfSauronActive && !eyeOfSauronDeactivated) {
        background(178, 223, 138); // original color before Eye of Sauron appears
      } else if (eyeOfSauronDeactivated) {
        background(210, 180, 140); // new color after Eye of Sauron disappears
      }
        
        if (!eyeOfSauronActive) {
          image(mountOfDoomImg, mountOfDoom.x, mountOfDoom.y, mountOfDoom.size, mountOfDoom.size);
        }
      
      //more drawing
      drawMaze();
      playTime = (millis() - startTime) / 1000;
      drawUI();
      player.move();
      player.display();

      //stoo invisibility buff sound
      if (millis() >= invincibleUntil && invisibilitySound.isPlaying()) {
        invisibilitySound.stop();
      }

      noTint();
      orcs.forEach(orc => {
        orc.move();
        orc.display();
      });

      checkCollisions();

      if (playerReachedMountOfDoom()) {
        if (!collidedWithRing) {
          // spawn monster in the center only if they still havent collected ring
          monsterSpawned = true;
          monsterCheck = true;

        } else {

          gameWin();
          volumeSlider.remove();
        }
      }

    let newMonsterWidth = monsterImg.width * monsterSizeMultiplier;
    let newMonsterHeight = monsterImg.height * monsterSizeMultiplier;

    // draw the monster 
    if (monsterSpawned) {
      if (!monsterSound.isPlaying()) {
        monsterSound.loop(); 
      }

      moveMonsterTowardsFrodo();

      let newMonsterWidth = monsterImg.width * monsterSizeMultiplier;
      let newMonsterHeight = monsterImg.height * monsterSizeMultiplier;
      image(monsterImg, monsterX, monsterY, newMonsterWidth, newMonsterHeight);
      
      //monster touches frodo looses

      if (dist(player.x, player.y, monsterX, monsterY) < player.size) {
        gameOver();
      }
    } else {
      if (monsterSound.isPlaying()) {
        monsterSound.stop();
      }
    }
    
    //golden ring

    if (goldenRingSpawned && gameState === 'playing' && goldenRing != null) {
      drawGoldenRing();
      checkRingCollision();
    }

      if (orcs.length == 0) {
        currentLevel++;
        generateLevel();
      }
      
       

    //golden ring and specific location
        
      if (!goldenRingSpawned && getRow(player.y) === ringSpawnLocation.row && getCol(player.x) === ringSpawnLocation.col) {
        goldenRing = createGoldenRing(width / 2 + 138, height / 2 - 110, 15);
        goldenRingSpawned = true;
        ringSpawnSound.play(); 
      }


    //eye of sauron and managing music
      if (eyeOfSauronActive ) {
          drawGrass = false; // to not draw grass during eye of sauron animation
          drawEyeOfSauron();
          if (!sauronSound.isPlaying() && sauronSoundStarted) {
            eyeOfSauronActive = false;
            sauronSoundStarted = false;
            eyeOfSauronDeactivated = true; 
            backgroundMusic.setVolume(initialVolume);
          }
        } else {
          drawGrass = true; // resume drawing grass when eye of sauron is not active
        }
    //another game over condition 
      if (playerHealth <= 0) {
        gameOver();
      }
        
        //restart button
        
         if (gameState === 'gameOver' || gameState === 'win') {
    fill(255); 
    textAlign(CENTER, CENTER);
    textSize(24); 
    textFont(pixelFont); 
    text("Restart Game", width / 2, 200); // y position to match the button's
     }


    } 
  }
}


//Collisions


function checkCollisions() {
  if (millis() < invincibleUntil) {
    return; // skip collision check if Frodo is invincible
  }

  orcs.forEach(orc => {
    if ( //orc dimensions
      player.x < orc.x + orc.size &&
      player.x + player.size > orc.x &&
      player.y < orc.y + orc.size &&
      player.y + player.size > orc.y
    ) {
      playerHealth--; //substract player health
      player.spawn(); //respawn
      dyingSound.play(); //play death sound
    }
  });
}

//Helper Functions


function getRow(y) { //convert y into row index (25 units for maze)
  return floor(y / 25);
}

function getCol(x) { //convert x into column index (25 units for maze)
  return floor(x / 25);
}

Conclusion and Future Considerations

In the end, I am very happy with how things turned out. Despite numerous problems and many more solutions and trials and errors, I developed a project that stands strong in almost every department we studied – there is a bit of everything. I hope it did at least some level of justice to inspiration source and achieved a design aesthetic that is consistent throughout. The scope of the project, will initially seemed simple, actually turned out to be much more complex when I started working on it, as there are lots of moving elements (literally and figuratively). This does not mean that it’s perfect of course. There are some improvements and suggestions to be made. For example, I could potentially add more monster types and more interactions and hidden surprises. The scale of the maze could be larger as well. Additionally, this is only one part of Hero’s journey. Story could be extended to what happens after reaching mount of doom. This calls for additional level. Moreover, the maze is fixed and static. It would be interesting to try procedural maze generation technique, so it is a unique maze each time game is loaded. On a final note, I hope you enjoy my game and I will definitely expand it in the future.

 

Week 5 Reading Response: Computer Vision

I am not a programmer or a videographer, so I’ll mostly speak about the first topic, Computer Vision in Interactive Art.

I was really impressed by Krueger’s Videoplace. It seemed to be a highly interactive Computer Vision piece. I found it especially interesting that this project is actually quite old, older even than the computer mouse. This surprised me as I thought that computer vision, at least the kind that could track poses accurately, was relatively new. It’s pretty amazing that the piece was working even in 2006.

Also, the part about computer vision and interactive art based on it being a response to increasing surveillance really stood out to me. Art has often been a response to various kinds of troubles or challenges faced by society, and is often a means of coping with societal trauma. Therefore, it is no surprise that the increased surveillance following 9/11 and the War on Terror, especially machine-based surveillance, triggered an outpouring of interactive artpieces based on computer vision. Lozano-Hemmer’s Standards and Double Standards is probably the best example of this, as to me, it represents that surveillance makes people more distant from each other (represented by the “absent crowd”) while enforcing a sense of oppression and authority (represented by the belts).

Rokeby’s Sorting Daemon was another particularly interesting project, especially after I visited his website to understand how the composites were made. On the left side of the composite are the faces, sorted from dark skin to fair, left to right. The right side meanwhile captures details of their clothes, sorted into mosaics by similar color. I found it to be a rather fitting representation of the profiling of people, which is often racial and unfairly targets racial minorities and immigrants who appear more similar to the stereotypical “terrorist” or “criminal”. It is also a visual representation of the dehumanization of people into datapoints that are monitored by the state.

Overall, this was a very interesting reading about the history of Computer Vision-based art. While I regret not being able to understand the technical aspects better, I would say this was quite a well-written article, which simplified many complex concepts of this old, yet cutting-edge, field.

Week 5 Reading | To the Moon and Back from Our Eyes.

Around fifty-five years ago, Apollo 11 reached the moon and back to Earth. Interestingly, the computers used to guide the spacecraft only required 4 kilobytes of memory. While it may seem small by today’s standards, it was a super machine that allowed precise and complex calculations for the journey.

“That’s one small step for man, one giant leap for mankind.” -Armstrong

Levin presented us with findings over thirty years later on the advancements humanity has made regarding computers. With more capacity, they are now able to track things in real-time and output images that are not just static but interactive. This reminds me of the breakthrough that Apple made with its Apple Vision and Vision Pro. The device is a major display (no pun intended) of what computer vision has become. While it may seem lesser compared to our phones (with its low battery capacity and weight!), it also made me reimagine the future when these devices are common occurrences in daily lives.

Remember when the first iPhone was introduced by Steve Jobs: people made fun of touchscreens. Today, it is indistinguishable from our daily lives. Indeed, today Apple Vision is mostly perceived as redundant to our smartphones.  But unless someone took up the mantle and challenged the default, no invention would ever be made.

References

To the Moon and Back on 4KB of Memory – Metro Weekly

The Four Computers That Flew Humans To The Moon (youtube.com)

Levin, G. “Computer Vision for Artists and Designers: Pedagogic Tools and Techniques for Novice
Programmers”. Journal of Artificial Intelligence and Society, Vol. 20.4. Springer Verlag, 2006.

Week 5 Assignment: Midterm Progress Report

Organ Defenders (WIP)

Concept

For this project, I was inspired mostly by Namco’s Galaga (image attached) and other games in the shoot ’em up genre. In the game, there are waves of enemies that you have to shoot down to score points. Being shot by the enemy or colliding into them makes you lose a life. Finally, there are some enemies that have special attacks that don’t kill you outright but make it harder for you to survive the wave.

A game of Galaga. The player controls a spaceship that is trying to dodge attacks from enemy spaceships. The boss spaceship has a tractor beam that spreads out in front of itself towards the player.

Galaga

As the Biology major of this class, I thought of combining the gameplay of Galaga with a concept based on our body’s immune system in order to make something new. Thus, Organ Defenders (would appreciate suggestions on the name) was born.

Design

This can be divided into three aspects, the game design, the code structure, and the visual design.

Game Design: I mostly went with the tried and true format of Galaga. The player has four lives and can control a white blood cell in the x-axis at the bottom of the screen. The player will be able to shoot bullets of enzymes at the enemies to bring them down and score points.

Waves of bacteria and viruses keep generating at the top and moving downwards (I might change this to right-to-left later). I may give some of the bacteria the ability to shoot toxins later, but for now, colliding with a bacterium makes the player lose a life. Viruses won’t directly kill the player but will disable the gun control, thereby not being able to earn any points.

As mentioned before, score is earned based off of how many bacteria/viruses have been killed. If I have the time, I might create a hard mode, where the higher the number of bacteria/viruses that are allowed to leave the bottom of the screen, the higher the number of new bacteria that are generated at the top. This will attempt to simulate infection.

Additionally, I plan to include three different kinds of timed powerups that can also help the player in challenging situations. Again, the hard mode might include some kind of cost to use the powerups more than once, to prevent spamming. The powerups I am currently planning for will be Antibody Rush (freezes the bacteria/viruses in place), Cytokine Boost (the player will be able to shoot more projectiles per shot), and Complement Bomb (basically nukes everything on the screen).

Code Structure: I plan to have 4 classes: the white blood cell, bacteria, virus, and the bullet. Here’s an example of how the classes will look, using my Bacteria class.

class Bacteria {
  constructor(
    initX,
    initY,
    radius,
    speed
  ) {
    this.x = initX;
    this.y = initY;
    this.r = radius
    this.moveY = speed;
  }
  
  display(){
    fill("green")
    ellipse(this.x, this.y, 2 * this.r);
  }
  
  move(){
    this.y += this.moveY
  }
  
  collide(other){
    return dist(this.x, this.y, other.x, other.y) <= this.r + other.r
  }
  
  wallCollide(){
    return (this.y > height)
  }
}

Visual Design: While it is a bit early to finalize the visual design before most of the gameplay is ready, I can still talk about it briefly. I plan to use Creative Commons-licensed sprites for the white blood cell, bacteria, and viruses. I might use spritesheets to show some animation, but I am not sure whether that will add to the game’s visual aspect as things might be going too fast anyway for a player to enjoy animations. At the most, I might include animations for when the white blood cell shoots a projectile.

Frightening Aspects / Challenges

There are quite a few aspects that seem daunting now:

  • Actually creating a desirable core gameplay loop, something that will encourage the player to keep playing
  • The powerups seem like a hassle to implement, especially the way I’m planning to implement them. Also, it will be challenging to show the timer of how much time is left before the next use of the projectile.
  • Timing of the animations will also need to be fine-tuned to avoid distracting the player and detracting from the game experience.
  • I also need to find a way to have the bacteria not clump together, which would unfairly make the player lose more lives. I tried for a bit to use while loops to keep regenerating random positions until there was a non-colliding one, but it didn’t seem to work properly.

Risk Prevention

  • I’ll have to work on extensive playtesting to resolve bugs. I also aim to have my roommates/friends playtest it to fine-tune the UI/UX.
  • Implementing the powerups will require me to learn some new skills.
  • I should have a core gameplay loop ready, along with visuals and SFX before I attempt to include the powerups, so that in the worst-case scenario, I still have a ready project.
  • Regarding the bacteria clumping together, I need to either find a way to unclump them or give the player i-frames (invincibility frames) upon losing a life. I might include the i-frames anyway, but will need to find a way to indicate that to the player (in fact, my prototype currently has half a second of i-frames, clearly not enough).
  • I should also make my code more responsive and check for any in-built functions I use that may respond unexpectedly on other systems.

Midterm Prototype

Finally, here’s the prototype. So far, only the player movements, the bacteria collisions, and the game over/game restart logic have been put in place.