Project 4 - Absolutely Seriously Totally Ecstatically Really Outrageously Incredibly Dope Spacerocks (not Asteroids ®️)

Due Apr 14 - Implement a classic game of space rocks.

Introduction

You have a friend who is really into retro video games. One of their favorites is the Atari classic, Asteroids . Since you are in CS 159 and becoming a super-awesome programmer, you decide to put your skills to the test and create a special version of Asteroids for your friend to play. What a thoughtful gift!

Thus, the goal of this project is to implement your own playable version of Asteroids, called “Generic Space Rock Shooter” (to avoid copyright infringement 🤓). You will need to exercise your growing knowledge of object-oriented concepts, including inheritance, abstract classes, polymorphism, and interfaces.

Your game will look a little something like Dr. Sprague’s version:

Learning Outcomes

  • Interpret UML diagrams that include inheritance, abstract classes, and interfaces
  • Use inheritance and polymorphism to reduce code duplication and simplify implementation
  • Create abstract classes and subclasses that implement abstract methods
  • Create, implement, and use interfaces
  • Write and utilize enum types

Provided Code

We have given you some code to assist you in your game development adventure! THe following files will let you draw and animate objects on the screen, as well as handle coordinates and angles that may be difficult.

StdDraw, GameDriver, and Playable

You will need to download the following files to handle the drawing window and the main game loop:

  1. 📄 StdDraw.java (DO NOT MODIFY) Updated Monday March 27 at noon ‼️
    • All of the graphics for this application will be handled by the StdDraw library developed by Robert Sedgewick and Kevin Wayne at Princeton University. This library is not particularly powerful or full-featured but it makes it easy to develop simple graphical applications in Java.
    • Note: For Spring 2023, we are using patched version of this class that adds a getLastDraw() method. This method returns a String representation of the last thing that was drawn to the screen. This is useful for testing your code. You can use this method to check that your code is drawing the correct things to the screen. For example, if you want to test that your Ship is drawing a triangle, you can check that the last thing drawn to the screen is a triangle. This is a very simple way to test your code.
  2. 📄 GameDriver.java (use for testing)
    • The GameDriver is the entry point for the project. It contains the main method. This class contains helpful constants (that you can use) and a main method which initializes a Playable game and provides the main game loop. Any game that you wish to run must implement the following Playable interface:
  3. 📄 Playable.java (DO NOT MODIFY) Updated Monday March 27 at noon ‼️
    • The Playable interface includes an “update” method that should update the state of all game elements at each time step (called a frame in animation and game design), and a “draw” method that should re-draw game elements after they have been updated. Your code will be tested using the driver above.

GameUtils, Point, Pose, Vector2D

In Asteroids (and many other games), some objects will have a location on the screen (in XY coordinates) as well as an orientation they will face (aka rotation angle). Certain objects will also need to move around and have velocities (represented by a 2D vector).

In the video above, you can see that the ship has a location and orientation. Notice also that sometimes the ship is moving in a direction other than it is facing. This is because the ship has a velocity vector that is different from its orientation.

We have provided the following four classes to help represent these types of data:

  1. 📄 Point.java
    • The Point class is an encapsulation of a two-dimensional location that uses rectangular screen coordinates.
  2. 📄 Pose.java
    • The Pose class is a subclass of Point that includes heading/orientation information (an angular measure).
    • notice especially the Pose.move method which will be useful for moving objects around the screen.
  3. 📄 Vector2D.java
    • The Vector2D class represents a two-dimensional vector. Vectors of this sort may be visualized as arrows with both a direction (or heading) and a length (or magnitude). Vectors may be used in this application to represent both the velocities and accelerations of game elements.
  4. 📄 GameUtils.java
    • Contains utility methods for working with the classes above. In particular, it includes a method for drawing a triangle for a particular Pose (which will you must use when drawing the Ship).
      • note: if you look at the implementation of GameUtils.drawPoseAsTriangle, you will find that it uses StadDraw.polygon internally. Therefore if you're testing Ship, you should take advantage of StdDraw.getLastDraw() to check that the correct polygon was drawn.

Other Provided Files

  1. 📄 AsteroidsGame.java
  2. 📄 TestHelpers.java
  3. 📄 TestStar.java (Updated Thursday 3/30 at 4:15 PM)
  4. 📄 TestNumericDisplay.java (Updated Thursday 3/30 at 4:15 PM)

Example Game

The GameDriver class is organized so that it will support any game that implements the Playable interface. The following two classes provide an example of a very simple “game” that implements Playable and makes use of some of the helper classes provided above:

  1. 📄 BounceGame.java Updated Monday March 27 at noon ‼️
  2. 📄 Ball.java

You can try out BounceGame by changing lines ~38-39 of GameDriver.java as follows:

// Playable game = new AsteroidsGame();
Playable game = new BounceGame();

Game Design

There are a lot of parts that go into creating a Generic Space Rock Shooter game. To help you, we’ve started AsteroidsGame.java for you, which handles the main game loop and simple keyboard input.

Updatable and Drawable

These simple interfaces each require only 1 method (update and draw, respectively). The AsteroidsGame class will use these interfaces to update and draw the game elements. You will need to implement these interfaces in your game elements.

GameElement

The GameElement class is an abstract class that implements the Updatable and Drawable interfaces (this means every GameElement is responsible not only for storing its own data [“state”], but also for [1] maintaining that data via its update method and [2] drawing itself). It also provides a few instance variables that will be useful to all game elements. You will need to extend this class in your concrete game elements. Implement this class based on the UML diagram (getters with no specification below should be simple one-line getters that return the instance variable of the same name).

update()

At each time step, a GameElement should update its position based on its current velocity. A GameElement should also “wrap” around the edges of the game screen when moving:

  • For instance, if (after applying velocity) the element flies off the right side of the screen (its X coordinate is greater than the width of the screen), the X position should be changed to 0 instead to make it re-appear on the left side of the screen.
  • Likewise, if the element flies off the left side of the screen (its X coordinate is less than 0), it should reappear on the right side of the screen.
  • GameElements should also wrap around the top and bottom edges of the screen.

checkCollision(GameElement other)

See Collisions and Scoring below.

Asteroid

The Asteroids must fulfill the following requirements:

  1. Asteroids (and all other non-filled game elements) must be drawn using the default StdDraw pen radius of .002.
  2. The initial location of each asteroid must be generated randomly. Positions must be selected uniformly from the entire game screen.
  3. The direction of motion for each asteroid must be drawn uniformly from the interval [0, 2π).
  4. Asteroids must maintain a constant speed of one pixel per time step.
  5. The location of asteroids must “wrap” at the edges of the game screen. For example, when an asteroid departs from the left side of the screen, it must reappear in the corresponding location on the right side of the screen.
  6. Asteroids must be displayed as open circles.
  7. Asteroids come in three different sizes, each with a different point value, see AsteroidSize below.

AsteroidSize

AsteroidSize is an enum that represents the size of an asteroid. It has three values:

  • SMALL: radius of 10 pixels, worth 200 points when destroyed.
  • MEDIUM: radius of 20 pixels, worth 100 points when destroyed.
  • LARGE: radius of 30 pixels, worth 50 points when destroyed.

AsteroidSize also has a static method called randomSize() that will return a random AsteroidSize based on the probabilities specified in the Initialization section. This method must have a 50% chance of returning AsteroidSize.LARGE and a 25% chance of returning AsteroidSize.SMALL or AsteroidSize.MEDIUM.

Saucer

Enemy saucers must fulfill the following requirements:

  1. Saucers must be displayed as open rectangles with a width of 20 pixels and a height of 10 pixels.
  2. The initial location of each saucer must be generated randomly. Positions must be selected uniformly from the entire game screen.
  3. The initial direction of motion for each saucer must be drawn uniformly from the interval [0, 2π).
  4. Saucers must maintain a constant speed of 2 pixels per time step.
  5. On each time step a saucer must select a new direction of motion with probability .05. The direction of motion must be drawn uniformly from the interval [0, 2π).
  6. The location of the saucer must not “wrap” at the edges of the game screen. Saucers must disappear permanently when they exit the screen.
  7. Saucers are worth 400 points when destroyed.
  8. The radius of a Saucer should be defined based on the details in Collisions and Scoring.

Ship (Updated Thursday 4/6 at 4:40 PM)

The Ship class is a GameElement that represents the player’s ship. It is responsible for drawing the ship and updating its position based on the current velocity . You will need to implement this class based on the UML diagram (getters with no specification below should be simple one-line getters that return the instance variable of the same name).

The player’s ship must also fulfill the following requirements.

  1. The initial pose of the ship must be at the center of the game screen pointing upward.
  2. The ship must be represented as an open isosceles triangle with a base of 10 pixels and a height of 20 pixels. You may use the GameUtils.drawPoseAsTriangle method do draw the ship to the screen.
  3. The movements of the ship must be controlled through the turnLeft(), turnRight(), and thrust() methods.
  4. TURNING: If turnLeft() is called, the ship must be turned .1 radians to the left. If turnRight() is called the ship must be turned .1 radians to the right. Note that you don’t need to worry about keeping heading values within the range 0 to 2π. All of the provided heading-related methods will perform correctly for values outside of this range.
  5. THRUST: If thrust() is called, a thrust must be applied to the ship’s current velocity (note: thrust() should not modify the ship’s position). The ship must be pushed in the direction of its current Pose heading, with a speed of .1 pixels/time step. Because the ship is floating in outer space, this means that its new velocity must now be a combination of its previous velocity plus the thrust. You may use methods from the Vector2D class (consider especially the add method of Vector2D) to help with this.
  6. FRICTION: The ship must slow down over time. Every time update is called (aka one time step), after the ship’s position is updated, the speed of the ship must be decreased by .02 pixels/time step. (hint: speed is defined as the magnitude of velocity)
  7. The location of the ship must “wrap” at the edges of the game screen.
  8. The radius of the ship (because it is not a circular object) should be defined based on the details in Collisions and Scoring.

Bullet

  1. Bullets must be displayed as filled circles with a radius of 1.5 pixels.
  2. Bullets must be created at the current position of the ship each time the user presses the space bar, as handled by AsteroidsGame.handleKeyboardInput().
  3. Bullets must move at a constant speed of 20 pixels per time step.
  4. The direction of the bullet’s motion must match the heading of the ship at the time the bullet is fired.
  5. Bullets must exist for a maximum of 20 time steps (calls to update). After 20 time steps, the bullet should be set to be destroyed.
  6. The location of bullets must also “wrap” at the edges of the game screen.

Stars

  1. Stars must be displayed as closed circles with a radius of 1 pixel.
  2. The initial location of each star must be generated randomly. Positions must be selected uniformly from the entire game screen.
  3. Stars must remain stationary.

Scoreboard (Updated Thursday 3/30 at 4:15 PM)

The text for the player’s score must be left-aligned 10 pixels from the left edge of the game screen and 20 pixels from the top. The numeric score must be prefixed by the string “Score: “. The score must be initialized to 0.

If you look in the constructor for AsteroidsGame.java, you can see how NumericDisplay is used to present the score.

Lives Remaining (Updated Thursday 3/30 at 4:15 PM)

The text for the number of lives remaining must be left-aligned 10 pixels from the left edge of the game screen and 40 pixels from the top. The number of lives must be prefixed by the string “Lives: “. The number of lives remaining must be initialized to 3.

AsteroidsGame (UPDATED Tuesday 4/12 at 3 PM)

The AsteroidsGame class is a Playable class that is responsible for the main game loop and keyboard input. It is also responsible for creating and updating the game elements. We provide a starter version of this class for you, but you will need to implement several methods and modify some of the existing methods. Start by downloading the following file, and then as you proceed through this assignment, work toward your implementation meeting the specifications as outlined below and in the UML diagram.

There are four ArrayLists that handle the objects in the game: drawElements, updateElements, shipAndBullets, and enemies. Objects that are put in drawElements and updateElements should be drawn and updated every time step, respectively. The other two lists, shipAndBullets and enemies are used for collisions. Collisions between game elements should be handled through the methods handleCollisions(), removeDestroyedBullets(), and removeDestroyedEnemies().

This class should also follow the specifications outlined in Game Logic. For instance, AsteroidsGame is also responsible for keeping track of the score and increasing it when enemies are destroyed.

AsteroidsGame() Constructor

Like most constructors, this one initializes its instance variables, but it stops short of creating the game elements. Rather it delegates that responsibility to the startGame() method, which is called by GameDriver. As provided, this class initializes a few of its instance variables, but you will need to ensure all of the instance variables are initialized. Refer to the UML diagram to make sure you have included all the instance variables and methods.

startGame()

This method is responsible for creating the various game elements that are required for the beginning of the game. As you progress through the different parts of the assignment, you will revisit this method to add in new game elements. In Part 1, you need to create the Stars and store references to them to ensure they are drawn (perhaps it would be best to rely on the private helper method here). The game should begin with 100 Stars that are positioned randomly on the screen.

newStars()

This method should create 100 new Stars at random positions on the screen and store their references such that they will be drawn. It should rely on the provided randomXPosition() and randomYPosition() static methods in AsteroidsGame.

newShip()

This method should create a new Ship instance and assign it to the corresponding instance attribute. It should also add the ship to all the appropriate ArrayLists.

addEnemy(Enemy enemy)

This method should add the given enemy to all the appropriate ArrayLists in AsteroidsGame.

newEnemies()

This method should create 10 asteroids of random sizes as determined by the probabilities in AsteroidSize and add them to all the appropriate ArrayLists in AsteroidsGame. (hint: there’s a couple methods that you’ve already written that you could use here!)

handleCollisions()

This method is responsible for checking if objects collide in the game. You should loop through each enemy in the enemies list and check if it collides with the ship or any of the bullets currently in the game. If a collision occurs, both objects should be set to be destroyed.

removeDestroyedBullets()

This method should go through shipAndBullets and check if each bullet needs to be destroyed (it’s destroyed attribute was set to true). If a bullet needs to be destroyed, then it should be removed from all ArrayLists that it was added to.

removeDestroyedEnemies()

This method should go through the enemies list and check if each enemy needs to be destroyed. If an enemy needs to be destroyed, then it should be removed from all ArrayLists that it was added to.

Game Logic

Initialization

When the game starts, the ship must be present on the game screen along with 100 stars, the two numeric displays and 10 asteroids. It is not necessary to guarantee that the the ship is safe from asteroids when the game starts. If the ship appears inside an asteroid, that’s just bad luck for the player.

The size of each asteroid must be determined randomly when it is created. Each asteroid must have a 50% chance of being large and a 25% chance of being small or medium.

There must be no saucers on the initial game screen. A new saucer must be added to the game screen on each time step with a probability of .002. Note that this means there may occasionally be more than one saucer present on the game screen. Again, it is possible for saucers to appear in a location that will cause an immediate collision with the ship.

Collisions and Scoring

For the purposes of collision checking, the game elements can be divided into three categories.

  1. Stars and the numeric display values - These elements are not subject to collisions.
  2. Ship and Bullets - Elements in this category are able to collide with asteroids and saucers.
  3. Asteroids and Saucers - Elements in this category are able to collide with the ship and bullets.

It is not necessary to perform pixel-level collision detection. Instead, you should perform approximate collision detection by associating each game element with an appropriate collision radius.

For the case of the circular elements (Asteroids and Bullets) this should match the radius of the element. For the case of non-circular objects (Ship and Saucers) this radius should be chosen to be half the length of the longest axis of the element. For example, the ship’s longest axis is 20 pixels, so the collision radius should be 10 pixels. A collision should occur whenever two objects are near enough that the circles defined by their collision radii overlap.

Whenever a collision occurs, both elements involved in the collision must be destroyed and removed from the screen. The player’s score must be increased by the point value associated with the Asteroid or Saucer that was involved in the collision.

If the player’s ship is destroyed, and the player has remaining lives, then a new ship should be created at the starting location.

If the player loses all three lives, then no future game updates must occur: the game screen must freeze. The Lives Remaining indicator must read 0 and the score should reflect the final collision that caused the game to end.

Any time the player manages to destroy all of the Asteroids and Saucers on the game screen, a new “level” should begin: the correct number of new asteroids must appear at randomly selected locations. The position of the ship and the position of the stars should not be modified.

The following UML diagrams depict the classes you will need to implement the game:

UML Diagram

Seems too large to include here. Click the button below to view the UML diagram in a new tab.

Larger View

Part 1

  1. Go over the provided files to understand what they do
    • Especially take a look at BounceGame.java and GameDriver.java to see what a game looks like (try running these!)
  2. Make sure GameDriver is set up to use AsteroidsGame and that you have properly downloaded all the provided files to your Eclipse project.
  3. Create the Drawable and Updatable interfaces
  4. Create the GameElement abstract class
    • Implement the update method
  5. Create the Star class
    • Declare the attributes and implement the constructor
    • Implement the draw method
    • In AsteroidsGame:
      1. add code in the newStars() method to create 100 randomly-placed Stars
      2. invoke newStars() in the startGame() method
      3. add code in the draw() method to:
        1. set StdDraw’s pen color to white (read the StdDraw docs or code, or take a peek 👀 at how BounceGame does it)
        2. draw all the Drawables. (👀 Hint: Take a look at how the update() method updates all the Updatables)
      4. ensure that the lines in update() that invoke handleCollisions, removeDestroyedBullets, and removeDestroyedEnemies are commented out for now (you’ll need these in Part 3)
    • Run the game and test your starfield!
  6. Run the provided tests for your Star class.
  7. Create the NumericDisplay class
  8. Run the provided tests for your NumericDisplay class.

Part 2

  1. Create the Ship class stubs
  2. Write the test class for the Ship class (you may want to review the GameUtils methods to support your work here.)
    • Name it ShipTest
  3. Create the Bullet class stubs
  4. Write the test class for the Bullet class
    • Name it BulletTest
  5. Complete the implementation of the Ship class
    • Implement the attributes and constructor
    • Implement the draw method
    • Implement methods to control the ship (turnLeft, turnRight, thrust)
  6. In AsteroidsGame, implement method to add a ship and handle movement
    • You should be able to run the game and test it at this point
    • Add code to AsteroidsGame to shoot a bullet when the spacebar is pressed

Part 3

  1. Create the Enemy abstract class
  2. Create the AsteroidSize enum class that represents the three different sizes and point values
  3. Create the Asteroid class
    • Implement the attributes, constructor, and method stubs
    • Implement the draw method
    • Implement update to have the asteroid move each frame
  4. In AsteroidsGame, implement newEnemies() to add 10 randomly sized asteroids
  5. Add collision handling to AsteroidsGame
    • You will need to check every Enemy to see if it collides with a Bullet or Ship
    • Objects that collide should be destroyed
    • Enemies that are destroyed should update the score
  6. Create the Saucer class

Submitting

As usual you will submit the parts of this assignment to Gradescope. There will be 4 different assignments on Gradescope, dividing this assignment as follows

  1. Part 1 - 🗓️ Monday April 3
    • Submit:
      1. Drawable
      2. Updatable
      3. GameElement
      4. Star
      5. NumericDisplay
    • Autograder:
      1. deducts for checkstyle issues
      2. Your implementation of Star, NumericDisplay, and GameElement pass our unit tests
  2. Part 2A - 🗓️ Wednesday April 5
    • Submit:
      1. ShipTest
      2. BulletTest
    • Autograder:
      1. deducts for checkstyle issues
      2. Runs your ShipTest and BulletTest against our reference solution expecting:
        • 90% coverage of Ship, Bullet
        • all tests passing
  3. Part 2B - 🗓️ Monday April 10
    • Submit:
      1. Ship
      2. Bullet
      3. ShipTest
      4. BulletTest
      5. GameElement
    • Autograder:
      1. deducts for checkstyle issues
      2. Your Tests should have 100% coverage of your implementation, and all of the tests should pass
      3. Your implementation of Ship and Bullet pass our unit tests
  4. Part 3 - 🗓️ Friday April 14
    • Submit:
      1. GameElement
      2. Updatable
      3. Drawable
      4. Star
      5. NumericDisplay
      6. Ship
      7. ShipTest
      8. Bullet
      9. BulletTest
      10. Enemy
      11. Asteroid
      12. Saucer
      13. AsteroidSize
      14. AsteroidsGame
    • Autograder:
      1. deducts for checkstyle issues
      2. Your implementation of the following pass our unit tests:
        • Star
        • NumericDisplay
        • Ship
        • Bullet
        • Asteroid
        • Saucer
        • AsteroidsGame
        • AsteroidSize

Change Log

Many software projects undergo changes over time. This section documents the changes made to this assignment over time. The most recent changes are listed first (this is called “reverse chronological order”).

  1. Add additional method descriptions for AsteroidsGame -
  2. Add reference to AsteroidSize probabilities under AsteroidSize -
  3. Updated Part 3 to list the classes that must pass tests, and added language to Ship thrust -
  4. Revise wording for Ship thrust and friction -
  5. Put instructions for Ship together -
  6. Added hints for Ship radius -
  7. Updated Part 2A language to not require coverage of GameElement, and add guidance on use of existing code in StdDraw, Pose, and GameUtils -
  8. Updated Tests and spec to use textLeft instead -
  9. Update TestStar and TestNumericDisplay to fix issue with JUnit -
  10. Added TestHelpers and due dates for sub-parts -
  11. Update Part 1 -
    • Provide TestStart and TestNumericalDisplay
    • Add instructions about modifications to AsteroidsGame
  12. Publish remaining specs -
  13. Updated `AsteroidsGame.java`
    • provide random helpers
    • provide beginning of `update()`
    • provide beginning of `startGame()` stub
  14. Initial Draft so you can get rolling on Part 1 -
Last modified May 1, 2023: student-sourced updates (1f18b77)