Furious Flying Fish

Summary of Deliverables

By the end of this, here’s what you’ll need to submit to Gradescope:

  • arena.py
  • fish.py
  • target.py
  • furious_fish.py
  • readme_FFF.txt

0. Getting Started

All files should be available to you in the “Furious Flying Fish” assignment on Codio. If you need to download the starter files again, you can do so here.

Read through the classes and function stubs provided, along with the comments. You will be using several different classes to implement the game. Namely:

  • fish.py represents the fish projectile you launch in the game
  • target.py represents a single target to be hit by the fish in the game.
  • arena.py represents the domain in which the game takes place. This class handles the bulk of the game logic.
  • furious_fish.java represents the overall game. We have provided all the code that goes in this class, but you should understand what every function call does. Do NOT modify anything in this class, just read through it.

A. Goals

The purpose of this homework is to do a deeper dive into object-oriented programming.

At the end of the assignment, you will have produced a game called Furious Flying Fishes (our take on Angry Birds). Furious Flying Fish has a heroic fish protagonist named Nemo, moving targets, targets with multiple hit points, and a limited number of fish launches. We have provided an example video of how your game might look when it is complete here. Each class acts as the blueprint for distinct, crucial parts of our overall game. As you are implementing this assignment, keep in mind that we are having objects interact with each other; they do not exist in isolation. Your fish object, nemo, will interact with your target objects by flying into and hitting them, and your fish and target objects operate within the Arena object which controls the game.


B. Advice

Work on the program in the order of the directions. Do not try to do things out of order, as that will likely add to your confusion. You will start with game elements like the fish and work backwards to the drawing and the loop.


1. Class Constructors


We will begin the Furious Flying Fish assignment by implementing the constructor for each of our game components - fish.py, target.py, and arena.py. This will allow us to properly instantiate them by parsing a level-description text file as you have seen in previous lectures.

A. Fish Constructor

The constructor for the Fish class should initialize all of the fish’s instance variables. Its position, radius, and number of throws (how many times the fish can be launched before the game ends) are passed to the constructor. The fish’s velocity components should both be set to zero.


B. Target Constructor

The constructor for the Target class takes in the canvas’s scale, and the target’s position, radius, velocity components, and number of hit points. These attributes should be initialized accordingly. The Targets in this version of the game move across the screen, and take more than one hit to completely destroy, which is why we will need velocity and hit point variables. We will discuss how to use these variables in later sections of the writeup, just make sure you initialize them in this constructor. Also, initialize hit_this_shot to False.


C. Arena Constructor

The Arena class’s constructor is where you will parse the level description file passed as the second command line argument. As the comments in the base code note, you will read several pieces of information about the game’s initial state from the input file. The data in the file will be ordered as such, where targets[0] is the first target, targets[1] is the second, and so on:

num_targets scale
nemo.num_throws_remaining
targets[0].x_pos targets[0].y_pos targets[0].radius targets[0].x_vel targets[0].y_vel targets[0].hit_points
targets[1].x_pos targets[1].y_pos targets[1].radius targets[1].x_vel targets[1].y_vel targets[1].hit_points

The first line of information contains data about the Arena. Make sure to call penndraw.set_scale() with the scale read in the file (so that coordinates range from 0 to the specified scale), in addition to setting Arena’s member variables.

The Arena constructor should initialize nemo’s position to the position (1,1), with a radius of 0.25, and with a number of throws equal to the amount indicated in the file. The number given for num_targets will tell you how many lines there will be with target data in the file; you should use this when allocating and instantiating the values of your Target list. Note that the .close() function on an open file object closes the file. As well, make sure to set mouse_listening_mode and mouse_was_pressed_last_update to the correct boolean value in your constructor.


2. Draw Functions


Next in this project we will implement the draw() functions of the Fish, Target, and Arena classes. This will let us “visually debug” our constructors to ensure our objects are initialized with the right properties.


A. Drawing the Fish

You should draw the fish so that its body should appear as a circle centered at the fish’s position, with a radius equal to the fish’s radius. Additionally, draw a triangular tail and a circular eyes somewhere on the fish’s body to give it more visual detail. You may add more body parts and details as you wish, but they won’t be worth any additional credit. You should also draw the fish’s remaining throws as text at a position 0.1 units above the fish’s body (not overlapping it).


B. Drawing the Fish’s Launch Velocity

In addition to drawing the fish, you will implement the fish’s draw_velocity() function to pre-visualize the fish’s launch velocity when the player clicks and drags (to be implemented in a future section). All draw_velocity() needs to do is draw a line from the fish’s position to a point equivalent to the fish’s center after a timestep of 1. Since the new fish’s position will change after one timestep, you can represent the new position as $p_{new} = p_{old} + velocity$.


C. Drawing a Target

Each target should be drawn as a circle centered at the target’s position, with a radius equal to the target’s radius. The target’s current hit points should be drawn as text centered at the target’s position. A target’s draw function should only draw anything if the target’s hit points are above zero.


D. Drawing the Arena

The Arena’s drawing function will be a little more involved than the Fish’s or Target’s. The Arena is the object in the game engine that stores the fish and targets, so it will invoke their respective draw functions to draw them. Thus, Arena’s draw() function will perform the following operations:

  • Clear the screen with penndraw.clear()
  • Iterate over the list of Targets and draw each one
  • Draw the Fish
  • If the Arena’s mouse_was_pressed_last_update variable has a value of True and mouse_listening_mode is True, draw the fish’s velocity
  • Call penndraw.advance() to draw everything to the screen

E. Checkpoint

Run furious_fish.py and give it the command-line argument target_file_1.txt. At this point, you should see the fish and targets being drawn on your screen, but nothing will move and nothing will respond to mouse input (yet).

This should produce the following drawing:

If your drawing does not look like this, try to debug your draw() functions.


3. Updating the Components


A. Updating the Targets

The first thing we’re going to do in this section is actually add a little code to Arena’s update function. Iterate over the list of Targets and call update() on each Target. You should pass time_step to each invocation of Target.update(). We will implement the rest of Arena.update() later in this section. We do this so that our code actually calls Target.update() somewhere so we can test whether our implementation of it works after this step.

Now, we will actually implement Target.update(). This function should do the following:

  1. Increment the target’s x-position by its x-velocity times the input time step.
  2. Increment the target’s y-position by its y-velocity times the input time step.
  3. Check whether the target’s x-position is less than zero or greater than the Arena’s scale (the scale is the maximum x coord that is still on the screen), accounting for the target’s radius in this calculation. The target should be completely offscreen to the left or right for this check to evaluate to true; you should not just check the target’s center. If the target is offscreen horizontally, then you should set the target’s x_pos such that it wraps around to the other side of the screen, also accounting for the target’s radius. For example, if the target’s radius were 0.5 and its x-position were 10.5, it would be offscreen to the right (since the scale of the screen is 10 from our example file). So, that target’s position would be set to -0.5 in order to wrap it around to the left side of the screen, while making it just offscreen so it doesn’t visually teleport, but smoothly move offscreen then onscreen again. Refer to the video provided in the base code for a visualization of this occurring.
  4. Check the target’s y-position against 0 and the Arena’s scale (we use scale for both the x and y position check since the canvas is a square), accounting for radius. Then, perform the same behavior as you did for the x axis check above, but for the y axis.

B. Checkpoint: Moving Targets

Run furious_fish.py from the terminal, and give it the command-line argument target_file_1.txt. If you’ve correctly implemented Target.update(), you will see the targets move like they do in the example video of Furious Flying Fish.


C. Updating the Fish

Implementing Fish.update() is very similar to the update() method we wrote for the ball class in lecture. This function should simply increment the fish’s x and y position by its x or y velocity multiplied by the input time step. Then, it should update the fish’s y velocity by subtracting from it the value 0.25 * timeStep to represent gravity dragging the fish down over time. We won’t be able to test this function until we’ve completed Arena.update(), however.

\(p^{t+1}_x = p^{t}_x + v^{t}_x * \text{timeStep}\) \(p^{t+1}_y = p^{t}_y + v^{t}_y * \text{timeStep}\) \(v^{t+1}_y = v^{t}_y - 0.25*\text{timeStep}\)


D. Updating the Arena Part 1: Mouse-listening mode

You’ve already implemented the portion of Arena.update() that updates each Target. Now you will implement the portion that handles mouse-listening mode, and the portion that handles Fish-flight mode. We have provided comments inside Arena.update() that outline what to do, but we will further explain the process here. When the game is in mouse-listening mode, it has to handle two possible states. The first is when the player is currently pressing a mouse button, indicated by penndraw.mouse_pressed() returning True. In this state, the Arena should set mouse_was_pressed_last_update to True, and invoke Fish.set_velocity_from_mouse_pos(). We will implement that function in the next section. The second state is when the player has just released the mouse button, indicated by penndraw.mouse_pressed() returning False and mouse_was_pressed_last_update being true. In this state, mouse_was_pressed_last_update should be set to False, mouse_listening_mode should be set to False, and the fish should decrement its throw counter. To decrement the fish’s throws, you should implement Fish.decrement_throws().


E. Setting the Fish’s Initial Velocity

We will take a detour in our code for a moment in order to implement Fish.set_set_velocity_from_mouse_pos(), which will allow us to update the fish’s initial velocity in mouse-listening mode. This function should get the mouse x-position and y-position, and compute the distance from that position to the fish’s current position. Then, it should set the fish’s current velocity to equal that distance in both dimensions.

x_vel <- fish's x-position - mouse's x-position
y_vel <- fish's y-position - mouse's y-position

F. Checkpoint: Mouse-Listening Mode

If you run your program with the same command-line argument as before, you should now be able to click and drag, and a line representing your fish’s initial velocity should be drawn when you do so. Once you release the mouse, the game will become non-interactive as you have not yet implemented Fish-flight mode in Arena.update(). For this checkpoint to work, make sure that mouse_listening_mode is set to True in the Arena constructor.


G. Updating the Arena Part 2: Fish-flight mode

The comments in Arena.update() indicate where you should handle fish-flight mode: in the else statement provided in the base code of Arena.update(). This portion of the function should do three things (some of which are functions we have not yet implemented):

  1. Call the fish’s update function
  2. Iterate over the list of Targets and call Fish.test_and_handle_collision() on each Target. The eventual result of this will be to have the fish check if it currently overlaps each target, and decrease that target’s hit points by 1 if it does overlap. The target’s hit point should decrease after the fish is offscreen and not at the moment where it hits the target.
  3. Check if the fish is offscreen by calling Arena.fish_is_off_screen(), and if that evaluates to True, iterate over each Target, and if the target is hit this round, decrease its health and reset its hit value to false for the next round. Finally, you should be calling nemo.reset() and setting mouse_listening_mode to True. The eventual result of this will be to reset the game back to mouse-listening mode when the fish flies offscreen.

H. Handling Fish-Target Collision

First, we will implement Fish.distance(), which is a helper function that computes the distance between two points (x1, y1) and (x2, y2). You will use the Pythagorean Theorem to implement this function to find the distance between two points. This function will be called from Fish.test_and_handle_collision().

We will now implement Fish.test_and_handle_collision(). The first thing this function should do is check if the input Target has more than zero hit points. If it does, then the fish is allowed to test for intersection against it, as the target still exists in the playing arena. The fish should now check if the distance between its center and the target’s center is less than the sum of its radius and the target’s radius. If this is true, then you should set the target’s hit_this_shot value


I. Checking if the Fish is Offscreen

To implement Fish_is_off_screen() in the Arena class, you’ll need to perform three checks.

  1. Is the fish below the screen bottom? This will be true when the fish’s y position plus its radius are less than zero.
  2. Is the fish beyond the left side of the screen? This will be true when the fish’s x-position plus its radius are less than zero.
  3. Is the fish beyond the right side of the screen? This will be true when the fish’s x-position minus its radius are greater than the Arena’s scale. (Since the scale is the largest x-coordinate that is still on the screen)

We do not check if the fish is above the top of the screen since the fish is subject to gravity and will fall back down eventually, so the fish is allowed to continue to travel (and not reset) if it goes above the top of the screen.

If any of the three conditions described above are true, then this function should return True. If they are all false, then it should return False. Notably, you should be able to evaluate your function’s return value without using any if statements. If you use any if statements in this function, you will lose points.


J. Checkpoint: Testing Your Gameplay So Far

If you run your game with target_file_1.txt as input, you should be able to launch your fish by clicking and dragging, and have it decrease the number of throws remaining. However, targets don’t lose any hit points yet even when your fish intersects with them.


K. Using the Target’s is_hit boolean

We will now update Fish.test_and_handle_collision() so that it only modifies the input Target if the fish has not yet collided with it in this fish-launch. To do this, we will make use of the hit_this_shot boolean for the Target. The fish should set that Target’s boolean value to True inside the if statement in your test_and_handle_collisions function.


L. Checkpoint: Testing Your Gameplay Again

When you run your game, you should now be able to properly decrease a Target’s hit points by at most 1 per fish launch. You’re almost done with the project at this point! All you have to do is check whether or not the game should end, which you’ll do in the next section.


4. Wrapping Up


If you examine furious_fish.py, you will see that its while loop continues as long as the Arena’s game_over() function returns False. You will now implement this function so that your game can end in one of two ways: the player can win, or they can lose.


A. Winning the Game

Now that you’ve handled what happens when the Fish hits a Target, you should modify Arena.update() so that it filters out (e.g. removes) any Target objects from the list that have zero hitpoints remaining. This will simplify the process of checking whether or not the player has won the game.

Victory occurs when all targets are broken. Implement Arena.did_player_win(). This is a helper function that determines if the state of the game’s variables are such that the player has won. This function will return True if and only if there are no remaining Target objects in the list targets. In all other scenarios, this function will return False.


B. Losing the Game

You will also have to check if the player has lost the game, which occurs when they are out of fish throws. Implement Arena.did_player_lose(). This is a helper function that returns True if the game is in mouse-listening mode and the fish’s remaining throw count has reached 0. We add the condition that the game must be in mouse-listening mode in order to allow the player to win on their very last throw; if the player lost immediately when the throw count reached 0, they’d never be able to launch their fish for their very last time as the game would immediately end without giving the fish a chance to collide with any remaining targets.


C. Ending the Game

Now that you’ve implemented the two helper functions, go ahead and implement Arena.game_over(). This function should return True when either the win or loss condition is True.

Now you must also implement Arena.draw_game_complete_screen(). This will clear the screen and draw a message that depends on whether the player has won or lost. If the player has won the game, it should draw text on screen that says “You Win!”. If the player has lost, it should draw text that says “You have lost…”.


D. Checkpoint: Testing the Game

Run your game as you have before. If you can reach both the win screen and the lose screen by playing, then congratulations! You have finished coding Furious Flying Fish!


5. Extra Credit


Implement any of these features to earn extra points!

Detailed Drawing (1 points)

Draw the targets with more detail than just a plain penndraw circle, using additional PennDraw shapes. Using images alone in place of circles will not count for extra credit.

Bouncing Fish (1-2 points)

Have the fish-ball bounce off of targets rather than moving through them. This will require you to reflect the fish’s velocity about the vector between the target’s center and the fish’s center. Feel free to look up how to reflect one vector about another. For an additional point, have a target change its velocity direction to match that of the fish’s bounce velocity, but in the opposite direction.

Force Fields (2 points)

Using penndraw’s keyboard input capabilities, allow the user to hold the I or O key while clicking to place a force field that either pulls the fish-ball in (I) or pushes it out (O). The fish’s initial velocity should only be updated while the mouse is held when no keyboard key is pressed. Likewise, the user should be able to press the R key to reset the game to mouse_listening_mode and reset the fish, should it get caught in the force fields. You should represent force fields as small penndraw circles; orange for pull-fields and purple for push-fields.

Changing Target Appearance with Health (1 point)

As a target’s HP decreases, it should change appearance. For example, you might change its color, decrease its radius, or have more and more cracks appear on its surface.


6. Readme and Submission


A. Readme

Make sure to fill out the readme provided with the base code before you submit!


B. Submission

You need to upload arena.py, fish.py, target.py, furious_fish.py, and readme_FFF.txt. Remember that your filenames should match the required names exactly, including capitalization.

If you used images for any of your drawing, you’ll need to submit those image files as well in order for your program to run correctly.