Furious Flying Fish
Summary of Deliverables
By the end of this, here's what you'll need to submit to Gradescope:
arena.pyfish.pytarget.pyfurious_fish.pyreadme_FFF.txt
0. Getting Started
All files should be available to you in the "Furious Flying Fish" assignment on Codio.
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.pyrepresents the fish projectile you launch in the gametarget.pyrepresents a single target to be hit by the fish in the game.arena.pyrepresents the domain in which the game takes place. This file contains a class that handles the bulk of the game logic.furious_fish.pyrepresents the overall game. We have provided all the code that goes in this file, but you should understand what every function call does. Do NOT modify anything in this file, 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.
We start by writing __init__ methods for the Fish and Target classes. Then, we write an __init__ method for the Arena class, which is a representation of all of the pieces of the game—including a Fish and a list of Target objects. After all three __init__ methods are written, we can move on to write methods that draw the objects.
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. This file contains the information necessary to create a Fish, which we'll call nemo, and a list of Target objects with different starting positions, velocities, and hit point counts.
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
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.
This will require a line that looks like this:
self.nemo = Fish(1, 1, 0.25, num_throws)
- We call the
Fish.__init__method by writingFish(...) - We use the
selfkeyword here because we are trying to save this newFishobject as an attribute of theArenawe are initializing. nemois a variable name like any other, soself.nemois thenemoattribute that belongs to theArenaobject we're currently creating.
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 eye 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'smouse_was_pressed_last_updatevariable has a value ofTrueandmouse_listening_modeisTrue, 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:
- Increment the target's x-position by its x-velocity times the input time step.
- Increment the target's y-position by its y-velocity times the input time step.
- 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'sx_possuch 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. - Check the target's y-position against 0 and the
Arena's scale (we usescalefor 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 * time_step 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_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):
- Call the fish's update function
- Iterate over the list of
Targets and callFish.test_and_handle_collision()on eachTarget. 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. - Check if the fish is offscreen by calling
Arena.fish_is_off_screen(), and if that evaluates toTrue, iterate over eachTarget, and if the target is hit this round, decrease its health and reset its hit value to false for the next round. Additionally, callnemo.reset()and setmouse_listening_modetoTrue. 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
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, and if it has not yet been hit this flight, then the fish is allowed to test for intersection against it. The fish should 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. Use the Pythagorean Theorem to implement this functionality.
$$\text{distance} = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}$$
If the distance between the centers of the objects is less than the sum of their radii, then you should set the target's hit_this_shot value to True.
I. Checking if the Fish is Offscreen
To implement fish_is_off_screen() in the Arena class, you'll need to perform three checks.
- Is the fish below the screen bottom? This will be true when the fish's y position plus its radius are less than zero.
- 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.
- 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.
You should also be able to properly decrease a Target's hit points by at most 1 per fish launch after the fish has fallen off the screen. 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 point)
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 point)
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.
Different Difficulties (1 point)
Create three different level specification files: an easy, a medium, and a hard one. While the game is running, the user can press the w key to restart the game in the next highest difficulty or the s key to restart the game in the next lowest difficulty. When playing on the hardest/easiest difficulty, the corresponding button presses should have no effect.
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.