Assignment 5: A Game, Visitors
Goals: Design a game; implement generics and visitors.
Instructions
This assignment is long. Start early.
the names of classes,
the names and types of the fields within classes,
the names, types and order of the arguments to the constructor,
the names, types and order of arguments to methods, or
filenames,
Make sure you follow the style guidelines that we enforce. For now the most important ones are: using spaces instead of tabs, indenting by 4 characters, following the naming conventions (data type names start with a capital letter, names of fields and methods start with a lower case letter), and having spaces before curly braces.
You will submit this assignment by the deadline using the online submission system. You may submit as many times as you wish. Be aware of the fact that close to the deadline the system may slow down to handle many submissions - so try to finish early.
There will be a separate submission for each problem - it makes it easier to grade each problem, and to provide you with feedback for each problem you work on.
The submissions will be organized as follows:
Homework 5 Problem 1: Your game implementation
Homework 5 Problem 2: The Course.java file
Problem 1: November 8th, 10:00pm
Problem 2: November 8th, 10:00pm
Practice Problems
Work out these problems on your own. Save them in an electronic portfolio, so you can show them to your instructor, review them before the exam, use them as a reference when working on the homework assignments.
Problem 20.2 on page 296
Problem 20.8 on page 306
Problem 20.9 on page 306
Problem 21.1 on page 312
Problem 21.3 on page 312
Problem 21.4 on page 315
Problem 21.5 on page 320
Problem 21.6 on page 320
Problem 21.7 on page 320
1 Problem 1 — N Bullets
You are going to design a game called N Bullets, based on the game 10 Bullets.
N Bullets is a game in which ships fly across the screen and the player shoots bullets at them from the center of the bottom of the screen. When a bullet and ship collide, both are instantly destroyed. The bullet, however, is replaced by a group of bullets that fly out to hit other ships. The game ends when the player run out of bullets and no more bullets are on screen.
Your game will use the javalib.funworld library, which provides an implementation of Worlds and bigBang similar to what was used in 1114. Read the documentation carefully for more information.
1.1 Visual Clarification
To get a better idea of what we are looking for, watch this gameplay video; it’s our implementation of the game
1.2 Submission format
You may find it overwhelming to keep all of your code within one file. To make your code more managable, you may submit a zip archive for this assignment instead of a single file. If you are more comfortable submitting as a single file, though, that is also fine.
1.3 World requirements
Your world must have a constructor that just takes an integer, which represents the number of bullets a player has to shoot. That is how your graders will launch your world.
Your game must end when there are no more bullets to fire and there are no bullets left on the screen.
So long as there are bullets left to fire, the player should be able to press the space bar to fire a bullet from the center of the bottom of the screen.
As well as the bullets and the ships, the player must also be able to see how many bullets are left and how many ships have been destroyed so far.
1.4 Ships
Ships should not spawn at every tick. Ships should spawn with some fixed frequency, and a non-zero, random amount of them should spawn at the same time.
Ships should spawn at either the left or right ends of the screen, and then move across the screen.
Ships should not be able to spawn at the very top or very bottom of the screen, but their spawn point along the y-axis should be random.
Two or more identical ships spawning at the same time is fine.
Ships should move with what appears to be smooth motion, and move in a straight line across the screen.
All ships should have the same speed (the magnitude of their velocity).
Ships that have flown past the edge of the screen should be removed from the game.
Ships should be visually represented as a circle with a fixed color and radius.
When a ship is hit by a bullet, it disappears.
If a bullet hits two or more ships simultaneously, all of the ships should disappear.
Ships should not fly over (or under) the bullet left/ships destroyed info.
1.5 Bullets
Bullets should move with what appears to be smooth motion, and move in a straight line across the screen.
All bullets should have the same speed (the magnitude of their velocity). It is fine if bullet speeds vary slightly due to rounding.
Bullets that have flown past the edge of the screen should be removed from the game.
Bullets should be visually represented as a circle with a fixed color; their size is discussed below.
When a bullet collides with a ship, it disappears and "explodes" into many bullets. The initial position of all of them should be the same as that of the destroyed bullet.
When a bullet a player fired hits a ship, it should explode into two bullets. When one of those bullets hits a ship, it should explode into three bullets, etc.
In the nth explosion (where n is 1 in the explosion of the player-fired bullet), for each bullet i it explodes into, 0 <= i <= n, the bullet should fire off at i * (360 / (n + 1)) degrees.
Bullets should grow in size along with n, so the higher up in the chain of explosions the bullet originated from, the bigger it should be. It should stop growing after some explosion, however, so it doesn’t take up too much of the screen.
If two or more bullets hit the same ship simultaneously, all of the bullets should explode.
Bullets may fly over (or under) the bullet left/ships destroyed info
1.6 Java and math
When computing the distance from one point to another, Math.hypot(double x, double y) will likely help.
When computing the x and y components of the bullet’s velocity, Math.cos(double theta), and Math.sin(double theta) will likely help. This theta is measured in radians, and there are 2 * Math.PI radians in a circle.
1.7 Tick-tock On The Clock
This style of programming is called a fluent interface.
As you may have noticed, a lot has to happen in one tick (bullets and ships have to move, collisions need to be handled, ships may be spawned, etc.). To make this manageable, we recommend you design methods that use the current (this) world and produce a new world, where one of those steps have been taken, and a new world is returned. You can then chain those together to in your onTick method. Using this methodology, it should look something like this (with much better naming, of course):
// advance the world by one tick: do step one, then step two, then step three, // and then step four public MyTowerDefenseClass onTick() { return this.doStepOne() .doStepTwo() .doStepThree() .doStepFour(); }
An added benefit of designing this collection of methods is, of course, the ability to test each step of your code independently.
As a note of caution, beware that you’re calling dependent operations in the right order. For example, the order in which you choose to remove off-screen components and handle ship/bullet collisions could affect the results of the game.
1.8 To IList or ILo?
It is up to you whether or not you want to use the IList<T> interface we have recently covered or the ILo* pattern from earlier in the course. The former has the advantage of being able to write and re-use abstractions like foldr, map, filter, etc. easily, while the latter has the advantage of not having to write many function objects.
1.9 Local variables
You will almost certainly want to use local variables for this assignment.
Be cautious, because some programms use local variables as an excuse for not using helper methods. Remember, one task per method.
Let’s say we were computing the average of a list, and returning 0 if the list was empty:
public int average() { if (this.length() == 0) { return 0; } else { return this.sum() / this.length(); } }
The problem, of course, is the length is being computed twice. One way to get around this would be to use a helper method:
public int average() { return this.averageHelper(this.length()); } public int averageHelper(int length) { if (length == 0) { return 0; } else { return this.sum() / length; } }
Why did the programmer choose to compute the sum in the helper and not in the outer method and pass it to the helper?
This is fairly verbose for such a simple operation, however. Instead, we can use a local variable:
public int average() { int length = this.length(); if (length == 0) { return 0; } else { return this.sum() / length; } }
This local variable could have been called something besides length.
As in ISL, local variables should be used to avoid duplicate computation, clarify what expressions mean, and contain simple computations another method wouldn’t need to use. Proper usage of local variables, also like in ISL, is a judgement call.
1.10 A note about randomness
There are two ways to generate random numbers in Java. The easiest is to use Math.random(), which generates a double between 0 (inclusive) and 1 (exclusive). You can multiply this number by some integer to make it bigger, then coerce to an int to produce a random integer in the range you wish. However, this is not easily testable: you’ll get different random values every time.
The better way to generate random numbers is: First, import java.util.Random at the top of your file. Next, create a new Random() object, and use its nextInt(int maxVal) method, which will give you a random integer between zero (inclusive) and maxVal (exclusive).
This is known as a "pseudorandom number generator", since the numbers aren’t really random if they can be reliably repeated...
One of them, to be used for testing, should take in a Random object whose seed value you specify. This way your game will be utterly predictable every single time you test it.
The second constructor should not take in a Random object, but should call the other constructor, and pass along a really random object:
import java.util.Random; class YourWorld { Random rand // The constructor for use in "real" games YourWorld() { this(new Random()); } // The constructor for use in testing, with a specified Random object YourWorld(Random rand) { this.rand = rand; ... } } Now, your tests can be predictable while your game can still be random, and the rest of your code doesn’t need to change at all.
1.11 Suggested constants
While you are welcome to tweak the game to create gameplay you enjoy (so long as you adhere to all of the above specifications), here are the constants we used that we know will generate decent gameplay (as seen in the previously linked video):
Screen width: 500 pixels
Screen height: 300 pixels
Tick rate: 1.0 / 28.0 seconds per frame.
Ship spawn frequency: every 1 seconds
Number of ships to spawn: between 1 and 3 inclusive, uniform distribution
Initial bullet radius: 2 pixels
Radius of bullet after an explosion: 2 pixels larger than radius of the past one
Maximum bullet radius: 10 pixels
Bullet color: pink
Bullet speed: 8 pixels/tick
Ship radius 1 / 30 the screen height
Ship color: cyan
Ship speed: 1 / 2 the bullet speed
Where ships can spawn: Not in the top or bottom seventh of the screen
Font color: black
Font size: 13
1.12 Yikes, This Sure Is A Lot!
Yes, it is, but don’t worry, you can do it. A big part of being able to succeed with this assignment is to keep track of what data represents what, how to organize it, and what you will need to do with it. As always, sticking to the design recipe and fleshing out your wishlist in advance is the best way to go about handling daunting programs. To help you out, we’ve started an informal wishlist of tasks your program must be able to do:
Determine when a single game piece has come into contact with any of a list of game pieces
Move a list of game pieces from their current position to their next positions
Remove game pieces that have left the screen
All of these directly imply that your program also needs to do the following:
Determine when a single game piece has come into contact with another game piece
Move a single game piece from its current position to its next position
Determine when a game has left the screen
To begin, we recommend fleshing out this wishlist and storing it somewhere you can easily access, and determing which classes each task should belong to. Then, you can add the method stubs to each of the classes, and write examples and tests before you begin to program. Give each method its own test function, and comment them out. As you implement each method, you can uncomment that method’s test function, and that way you can test the whole program as you go.
2 Problem 2 — Courses and Prereqs
A Course has a name and prereqs, which is an IList<Course> (see Lecture 15 for the data definition of parametrized lists).
Notation: When you see notation of the form ClassName#methodName(...), it means methodName is a method defined in the class ClassName, and the arguments are left for you to determine. For example, String#substring(...).
Design the Course class and any other necessary classes and interfaces.
Create the IFunc<A, R> interface, which has one method: R apply(A arg); as shown in class.
Implement the visitor pattern for IList<T>s. Your IListVisitor<T, R> interface should be a function, meaning it should extend the IFunc interface —
think carefully what types should be given to IFunc<?, ?> to make this work correctly... Design a DeepestPathLength class, which is a function object that takes a Course. This should be used in a Course#getDeepestPathLength(...) method, which computes the length of the longest path from this course to a course with no prerequisites. You must use the visitor pattern where appropriate. Calling the Course#getDeepestPathLength(...) method on a Course should produce the same value as applying a DeepestPathLength object to it.
Design the IPred<X> interface, which is an IFunc that always returns a Boolean.
Design a HasPrereq class, which is an IPred<Course> This should be used in a Course#hasPrereq(...) method, which determines if a course has a prereq (either an immediate one or a prereq of a prereq) whose name is the same as a given string. Think carefully about the constructor for this class. You must use the visitor pattern when appropriate. Calling the Course#hasPrereq(...) method on a Course should produce the same value as applying a HasPrereq object to it (given that they are both looking for the same name).
2.1 Extra Credit: Ormap
For extra credit, design the Ormap<T> class, which is a list visitor that operates as ormap did in ISL. Think carefully about the constructor for this class.
Re-implement the HasPrereq class, using Ormap whenever appropriate
If you plan on doing the extra credit, your submission to the handin server should either totally complete the above two items or not. Your graders will only grade one version of HasPrereq.