As with the previous and future tasks, there are two components to this task.
To reiterate, both the Problem Set and Game Features - Learning Objective portions of this document MUST be completed to achieve the Learning Objective points and pass the course. The Game Features - Comprehensive Understanding is not needed for the Learning Objective requirement, but completing/not completing it will directly impact your grade through CU points. Each Problem Set will carry 20 LO points, while each Game Engine Task will carry 30 LO points and 50 CU points.
You should complete the components below in the order they are listed; you will not be able to earn credit for the following component(s) until you complete the previous ones with all available points.
Coding Task 1 assesses Unit Testing and Classes. Both the Problem Set and Game Engine Task will focus on these topics.
The content of the Learning Objective is primarily used in the Game Engine by the sample game. To play the sample
game, during and after completing this Learning Objective, navigate to the class app.Configuration
and
change GAME to "sample game".
As with the previous task, one of our TAs has prepared a video which covers the requirements for this task. Again, this content is optional, but we strongly recommend watching the video to get started, especially if you're struggling with the game features. The video can be found at https://www.youtube.com/watch?v=XVykxZlLLHM.
Problem Set GitHub Repository link: https://github.com/CSE-116/ProblemSet-1
Once you have the project opened in IntelliJ, you'll see a src folder containing 2 Java packages named problem and tests.
To submit your project, run problem.Zipper
, which will create a zip file containing the problem set,
and submit it to Autolab.
You should read through all the following sections before you begin to implement the methods from the specification. Note that you will not receive feedback on the correctness of these methods until you've completed the testing component of this task (see the "Autolab Feedback" section for more details). However, you must at least create every class/method from this specification to receive feedback. You can "stub out" these methods by having them always return a fixed value, but they must exist so the grading code, and your tests, can compile and run.
For this Problem Set, you will implement the following functionality. Note that all of the methods are non-static.
Location
: Create a class in the problem
package named Location
. This
class will represent a 2-dimensional point representing the location of a shape. This class should implement the
following methods:
double
s representing the x and y
location, respectively.
getX
: create a method named getX
which takes no arguments and returns a
double
. This method should return the x location.
getY
: create a method named getY
which takes no arguments and returns a
double
. This method should return the y location.
setX
: create a method named setX
which takes a single double
and
returns void
. This method should set the x component of the location to the input amount.
setY
: create a method named setY
which takes a single double
and
returns void
. This method should set the y component of the location to the input amount.
Circle
: Create a class in the problem
package named Circle
. This class
will represent a circle based on a 2-dimensional location and a radius. This class should implement the
following methods:
Location
object and a
double
, in that order. These represent the location of the circle and its radius.
getRadius
: create a method named getRadius
which takes no arguments and
returns a double
. This method should return the radius of the circle.
setRadius
: create a method named setRadius
which takes a single
double
and returns void
. This method should set the radius of the circle to
the input value.
getLocation
: create a method named getLocation
which takes no arguments and
returns a (reference to a) Location
object. This method should return the location of the
circle.
setLocation
: create a method named setLocation
which takes a single (reference
to a) Location
object and returns void
. This method should set the location of
the circle to the input value.
getArea
: create a method named getArea
which takes no arguments and returns a
double
. This method should calculate and return the area of the circle.
πr2
, where
r
is the radius of the circle.
Math.PI
as the value of π. Nothing needs to be imported to do so.
CircleEngine
: Create a class in the problem
package named CircleEngine
.
This class will contain methods for using and manipulating Circle
s. This class should implement the
following methods:
detectCollision
: create a method named detectCollision
which takes two
(references to) Circle
objects and returns a boolean. It will return true if the circles
are overlapping and false otherwise.
√((x1-x2)2 + (y1-y2)2)
.
doubleRadius
: create a method named doubleRadius
which takes an
ArrayList
of Circle
s and returns void
. This method should iterate
over the input list and double the radius of each circle, using the appropriate setter.
Write JUnit tests for the following methods from the specification in the tests.TestProblemSet1
class.
These tests should be annotated with @Test
.
Location
Location
object.
Circle
Circle
object.
getArea
method, asserting that the calculating is correcly performed on
circles of varying radii.
CircleEngine
TestProblemSet1
class. They are likely commented out, and must be uncommented for you to
use
them. You can uncomment multiple lines by selecting the lines and pressing ctrl+/ (command+/ on mac).
You
are encouraged to use these tests to help you write the methods they are testing.
Implement the methods from the specification. You may wish to complete the Testing Requirements before you begin implementation, and it's suggested that you run these tests as you implement the methods.
Feedback from Autolab will be given in several phases. If you don't complete a phase, then feedback for the following phase(s) will not be given. You must complete all phases to earn the required score of 20 LOs. For the problem set, the phases will be as follows:
Once you have successfully completed all three phases, you will have completed this Problem Set and Autolab will confirm this with a score of 20 LOs.
You will continue to build functionality for the Game Engine in this component of the task. This will be done on top of the code written for the previous task; you should not reclone or remove said code for this task or any future Game Features components.
As with the previous task, there are two parts to the Game Features component: the Learning Objective and Comprehensive Understanding. The Learning Objective portion must be completed with a score of 30 LOs before the Comprehensive Understanding unlocks. Both portions of the Game Features will be submitted to the same assignment on Autolab.
For this Learning Objective portion, you will be writing code in the following classes which already exist:
PhysicsEngine
, located in the app.gameengine.model.physics
package.
Wall
, located in the app.games.commonobjects
package.
TestTask1
, located in the app.tests
package.
If you do not have the TestTask1
class in your app.tests
package, you should create it. We
have provided some tests for the Wall
class for you, which can be accessed at this link:
TestTask1.java.
If you already have the TestTask1
class, you already have these tests and do not need to add them to
the file.
Additionally, in the app.tests
package, create a new class called TestUtils
.
In this task, you will continue to add feature to the game engine project that you started in the previous task.
The coordinate system use for computer graphics might be different than you're used to. We'll use the standard coordinate system that is typically used in video games (and anything involving rendering to a screen).
The origin of the system is at the top-left of the screen/window/level with positive x going right and positive y going down. This image shows the same information as the one above, but with the coordinate system labeling all the tiles in the game. You can see that "G" (The goal) is at location (6, 2).
Each object within the game has several important properties to consider, namely a location and a hitbox, which has its own location and dimensions. These quantities are each represented as 2-dimensional vectors (x, y). All objects and hitboxes are rectangles, and their location is the location of their upper left corner. Their dimensions are the width and height of the rectangle. For example, the goal (G on the grid above) has a location of (6, 2), since that's the location of its upper-left corner, and it has dimensions (1, 1) since it's exactly 1 tile in size. Locations and dimensions do not have to be whole numbers.
Object locations and dimensions also do not necessarily have to be aligned with the visuals of that object. In the image above, the green dot represents the location of that object, the blue outline represents the size of the sprite image that is being rendered for that object, and the red outline represents the size of its hitbox. Having a hitbox smaller than the graphical sprite allows for more precise physics and collision, but means that extra care has to be taken when dealing with those properties. Pressing F4 in game shows overlays of each object's hitbox, which can be helpful for debugging.
You should read through all the following sections before you begin to implement the methods from the specification. Note that you will not receive feedback on the correctness of these methods until you've completed the testing component of this task (see the "Autolab Feedback" section for more details). However, you must at least create every class/method from this specification to receive feedback. You can "stub out" these methods by having them always return a fixed value, but they must exist so the grading code, and your tests, can compile and run.
For the Learning Objective portion of the Game Features component for this task, you will implement the following functionality:
Note: It is suggested that you follow this specification in order for your implementation. However, you may
find it easier to complete the detectCollision
method before the getOverlap
method.
PhysicsEngine
class:
updateObject
updateObject
method. This method should update the location of
the DynamicGameObject
based on the velocity and amount of elapsed time.
DynamicGameObject
class to
achieve this functionality, namely getVelocity
, getLocation
, and
setLocation
. These methods are already provided for you.
getX
and getY
methods on the
Vector2D
object returned by getVelocity
.
getOverlap
getOverlap
method. This method should return a
double
representing the minimum overlapping distance of the two
Hitbox
objects from the parameters.
Wall.collideWithDynamicObject
method, so referring to those
descriptions and images may be helpful.
Hitbox
, note the following:
Hitbox
using the getLocation
method, which returns a
Vector2D
containing the x and y coordinate of said location.
Hitbox
using the
getDimensions
method, which returns a Vector2D
containing the
width (in the x-component) and height (in the y-component) of the Hitbox.
Hitbox
objects. This means you must calculate the amount the two hitboxes are overlapping in both the x
and y direction, then return the smaller of the two.
getOverlap
on these Walls' hitboxes would return 0.25.
detectCollision
detectCollision
method. This method should return a
boolean
that is true if the two Hitboxes are colliding and false otherwise.
getOverlap
applies here as well. In the same
example, calling detectCollision
on those two Walls would return true, as they are
colliding by 0.25 which is greater than 0.
Wall
class:
collideWithDynamicObject
collideWithDynamicObject
method. This method should adjust the
location of the DynamicGameObject
passed in the parameter to ensure that they are
no longer overlapping, and zero the velocity in that direction.
DynamicGameObject
. All state in the Wall
should stay the same.
PhysicsEngine.getOverlap
method. Thus, you should calculate the overlap based on
the Wall
's and DynamicGameObject
's hitboxes, which can be obtained
with the getHitbox
method. You should be able to reuse much of the same logic for
this method.
setLocation
with
otherObject.getLocation().getX() - 0.5
rather than
otherObject.getHitbox().getLocation().getX() - 0.5
. Note that this is only for the
position update, and you should still use the hitbox's position to calculate the amount to
move).
When working with more complex structures such as classes and data structures, it can be tedious to constantly compare two such instances as is often necessary in testing. Thus, the Learning Objective component will typically begin with writing a Testing Utility method relevant to the task. Writing this method is required to pass the Learning Objective, and it is strongly encouraged that you use this method in your testing.
In the app.tests.TestUtils
class, create a static method named
comparePlayers
that takes in (references to) two Player
s and returns void. This method should contain JUnit asserts
that verify the two Player
s passed in have identical state, failing if they do not.
Specifically, you must assert that the location and velocity vectors of both objects are equal. You should use the
respective getters to access these instance variables. However, you will not find these getters in the
Player
class, rather they will be contained in the
DynamicGameObject
class in the app.gameengine.model.gameobjects
package. This is because Player
extends DynamicGameObject
and thus has access to all of these methods. This is called
inheritance, and more details on inheritance will come later in this course.
Note that this method is not a test, and thus should not have the @Test
annotation. Rather, this is a
static method intended to be used in your testing.
Write JUnit tests for the following methods from the specification in the TestTask1
class. These tests
should be annotated with @Test
:
PhysicsEngine
updateObject
Player
objects, one whose location and
velocity you manually update, and one that is updated by the updateObject
method.
This way, you can make use of the comparePlayers
method.
detectCollision
getOverlap
getOverlap
method. Tests are provided for you in the
TestTask1
class, which you may use to help you write the method itself.
Wall
collideWithDynamicObject
method. Tests are provided for you in
the TestTask1
class, which you may use to help you write the method itself.
Implement the methods from the specification. You may wish to complete the Testing Requirements before you begin implementation, and it's suggested that you run these tests as you implement the methods.
Feedback for the Learning Objective component of the Game Features will be given in several phases. If you don't complete a phase, then feedback for the following phase(s) will not be given. You must complete all the following phases to earn the required score of 30 LOs to complete this task. The phases for the Learning Objective are as follows:
Once you have successfully passed all four phases, you will have completed the Learning Objective component of the Game Features, and Autolab will confirm this with a score of 30 LOs.
For the Comprehensive Understanding portion of this task, you will be implementing functionality in the
SnakeLevel
class (from the app.games.snake
package) such that the Snake game is fully
functional. You can access the Snake game by changing the GAME
constant in
app.Configuration
to "Snake"
.
Each of the methods you implement will have an associated CU count that you will earn upon passing our tests for that method. Partially implemented methods may result in some partial credit being earned, depending on the method.
You don't need to understand all of the internal Snake code; however, you should expect to read and understand some of the internal Javadoc comments while completing this task. Not all background information will be provided in this document.
SnakeLevel
class, implement the following methods:
wallOffBoundary
(10 CUs)
SnakeWall
objects.
SnakeWall
s. Adding a
SnakeWall
to the level can be done by accessing and modifying the ArrayList containing the level's
StaticGameObjects
, just as it is done in the LevelParser
class.
spawnFood
(10 CUs)
SnakeFood
should be added to the level's
StaticGameObjects
ArrayList, as well as the ArrayList named food
in the
SnakeLevel
class.
SnakeFood
should be at a random tile-aligned coordinate, with the
following restrictions:
SnakeBody
object from the tail
ArrayList is currently occupying.
SnakeFood
object from the food
ArrayList is currently occupying.
Randomizer
class from the app.gameengine.utils
package.
randomIntVector2D
method that takes in two parameters.
advanceLevel
method on the Game
object stored internally in this class (this
object can be accessed with this.game
) and return immediately, doing nothing else.
lengthenSnake
(10 CUs)
SnakeBody
objects will be added to the tail
ArrayList, as well as the level's StaticGameObject
ArrayList. The number of
SnakeBody
s to be added to the list will be dependent on the value of the
lengthIncrease
instance variable.
SnakeBody
object, the location of this object should be decided as follows:
tail
ArrayList is empty, the new SnakeBody
object should be
positioned such that it is directly behind the head (which is returned by
this.getPlayer()
). This can be determined by looking at the head's location and
orientation vectors.
tail
ArrayList is not empty, the new SnakeBody
object should be
positioned such that it shares the location with the first SnakeBody
object in the
tail
ArrayList
(the object at index 0).
SnakeBody
is added to the tail
ArrayList
in this
method, it should be added to the front of the ArrayList (prepended) rather than the end. Recall that
calling add()
with only one parameter defaults to adding to the end of the list
(appending).
spawnSnake
(10 CUs)
SnakeBody
objects to the level in the same manner as
lengthenSnake
, but the number to be added should be based off of the value of the
startingLength
instance variable instead. Note that you should add new
SnakeBody
objects to both the tail
ArrayList and to the level's list of
StaticGameObject
s.
startingLength
is based off of the total length of the snake including the
head, rather than just the length of the tail. Adding startingLength
new
SnakeBody
elements will result in an incorrect length.
tail
ArrayList
is empty initially when this method is
called.
update
(10 CUs)
processInput
with a dt of 0 on the keyboardControls
instance variable, to trigger any keyboard inputs since the last update.
SnakeBody
object at index 0 is now at the location that the
SnakeBody
object at index 1 previously occupied and so on. The SnakeBody
at
the last index of the tail
ArrayList should have what was previously the location of the
Player.
setLocation
on SnakeBody
objects. However,
this will currently not have any effect. In order for it to take effect, you must remove a method
from the StaticGameObject
class. This method is unnecessary, and its inclusion or
deletion should not affect any of your other coding tasks.
app.gameengine.model.gameobjects.StaticGameObject
class, and delete or
comment out the setLocation
method within that class. This will allow you to freely
change the location of SnakeBody
objects.
SnakeBody
object stays in place except for the
last. If you choose, you can only modify the location of that final segment to the proper place directly
behind the player.
tail
ArrayList, the Player's location should then be
incremented by the Player's orientation. Remember that the Player can be accessed with
this.getPlayer()
.
tail
contains
SnakeBody
s with locations [(0,0), (1, 0), (2, 0), (3, 0)], the Player's location would be
set to (4, 1) and tail
will contain SnakeBody
s with locations [(1, 0), (2, 0),
(3, 0), (4, 0)]. Repeating this (assuming orientation is unchanged) would result in the Player's
location being set to (4, 2) and
tail
containing SnakeBody
s with locations [(2, 0), (3, 0), (4, 0), (4, 1)].
SnakeBody
, ensure
that added objects are added to the level's StaticGameObjects
ArrayList as well as the
tail
ArrayList. Removed objects must be removed from the tail
ArrayList and
have the destroy()
method called on them. This method can (and probably should) be
implemented without adding or removing SnakeBody
objects.
The Comprehensive Understanding tests will unlock once you have earned 30 LOs on the assignment. These will be run in a single phase for correctness. For each test you successfully pass, you will earn a varying amount of CUs with a total maximum score of 50 CUs.
There is no required testing component for the Comprehensive Understanding; however, you are expected to test your code nonetheless as the Autolab feedback is unlikely to be sufficient for debugging.