Warning: Outdated Content
This MP is from a previous version of CS 125. Click here to access the latest version.
MP4: Polymorphism
In MP3 you made it possible to create multiplayer games. In this final checkpoint you’ll finish the app by connecting gameplay to the server.
In MP0 and MP1 you implemented singleplayer, local gameplay for target mode and area mode by writing code directly in the game activity. Now it’s time to expand that logic to work with multiplayer games, where data about other players is coming in from the server and data about the user needs to be sent to the server.
In lecture, you learned about polymorphism, which makes it easier to share behavior between classes. To manage the complexity of multiplayer gameplay logic, you will apply polymorphism to create a class to manage each kind of game. You will organize the codebase and consolidate related logic by refactoring the gameplay logic out of the game activity and into those new classes.
A screencast with an overview of the checkpoint is available 1:
Please note that the organization of the Your Goal section was improved since the recording, so section numbers and ordering will not match exactly.
As usual, deadlines vary based on your deadline group. MP4 is due at:
-
8 PM on Sunday, 11/17/2019 for the Blue Group: all labs starting at 2 PM or earlier
-
8 PM on Monday, 11/18/2019 for the Orange Group: all labs starting at 3 PM or later
Additionally, 10% of your grade is for submitting code that earns at least 40 points by 8 PM on Sunday 11/10/2019 (Blue) or Monday 11/11/2019 (Orange). Late submissions are subject to the MP late submission policy.
1. Learning Objectives
In Checkpoint 4 you will:
-
Organize your codebase using polymorphism
-
Generalize your existing algorithms to more complex cases
We will also continue to exercise skills used previously, especially object-oriented programming fundamentals and refactoring.
2. Assignment Structure
Updated Javadoc is available.
As usual, we distribute the Checkpoint 4 updates in a patch.
Since this patch includes changes to the main
source set like Checkpoint 3 did,
an extra step is needed to ensure the changes are committed with the rest of your work.
2.1. Obtaining MP4
To apply the patch, follow these steps:
-
Ensure that all your work is committed, so that none of your files' names appear green or blue in the Project pane.
-
Download the MP4 patch file.
-
In Android Studio, select the VCS | Apply Patch menu item.
-
Select the downloaded patch.
-
Change the Name dropdown to Default Changelist. 2
-
Press OK to apply the patch.
-
Change the
checkpoint
setting ingrade.yaml
to4
. -
Select the File | Sync Project with Gradle Files menu item.
We also provide much of an updated GameActivity
file, but you will need to apply it separately
because a patch would likely not be compatible with previous work in that file.
To get that part of the starter code, copy-paste
this Gist
over your GameActivity
. The work you did in that file is still accessible on GitHub or through
Git | Show History; you will want to refer to your previous code as you refactor that logic
into separate files.
If the patch didn’t apply fully, you may get compilation errors in the new GameActivity
.
Those can be fixed by opening the activity_game.xml
layout resource, switching to Text view,
and pasting this Gist over
the existing contents.
You can test your Checkpoint 4 work by running the new Test Checkpoint 4 run configuration. You can run the local autograder with the Grade run configuration.
2.2. Late Submissions
Like in previous checkpoints, enabling useProvided
causes the app to use our provided
components. For Checkpoint 4, the only past components you need are AreaDivider
and Target
,
but in addition to those we also provide the launch activity, the dashboard/main activity,
and the completed game setup activity so that your app can work. Unlike when using provided
components for working on Checkpoint 3, we do not replace any part of your GameActivity
.
As always, you can go back and make late submissions to previous checkpoints by changing
checkpoint
. Past test suites are forward-compatible with this checkpoint’s reorganization.
3. Tools and Resources
This section provides background and documentation you will need to achieve Your Goal.
3.1. Maps
You learned about one of the most common data structures in MP3: the list, which stores an ordered variable-length collection of items. Another extremely useful data structure is the map. Maps store associations between two kinds of things, allowing a value to be looked up by a key. For example:
-
University software might use a map to associate
Student
objects (values) with NetIDString
s (keys). -
Forum software might look up users' display names (values) by usernames (keys).
-
A dictionary allows you to look up the definition (value) of a word (key). 3
Like lists, declaring a map variable involves angle bracket syntax. Since the type of values in the map can be different from the type of keys, you need to specify the key type and the value type. For example, this code declares and initializes a map from strings to integers:
Map<String, Integer> capacities = new HashMap<>();
The empty angle brackets on the right side indicates that the actual map created holds the same kinds of keys and values as the variable is declared to associate.
The most commonly useful functions defined by the Map
interface are:
-
put
, which takes a key and value, adding or replacing the value associated with that key -
get
, which returns the value associated with the given key (or null if the key is absent) -
remove
, which removes the association involving the given key
For example:
capacities.put("Foellinger", 2500);
capacities.put("University Hall", 1000);
System.out.println(capacities.get("Foellinger")); // Prints 2500
capacities.put("Foellinger", 1750);
System.out.println(capacities.get("Foellinger")); // Now prints 1750
capacities.remove("University Hall");
System.out.println(capacities.get("University Hall") == null); // Prints true
The associations in a map can be iterated over by using the collection returned by entrySet
with the enhanced for loop:
for (Map.Entry<String, Integer> entry : capacities.entrySet()) {
// The type names in the angle brackets should match the types in the map
// The current key is entry.getKey()
// The current value is entry.getValue()
// Do something with the key and value?
}
Alternatively, you can get an iterable collection of just the keys with keySet
or of just the values with values
.
3.2. What is a Websocket?
In Checkpoints 2 and 3, you made web requests to get data from or submit data to the server. HTTP requests work well for one-time requests like we’ve done so far, but to continually get the newest data, the client would have to keep asking the server over and over again, which is inefficient.
Websockets allow the client and server to maintain a bidirectional connection. The client can send additional messages to the server without the overhead of a new request, and the server can send messages to the client immediately as events occur.
The websocket protocol allows any kind of data to be transferred. We will continue to use JSON
objects to represent the messages/updates in the game. So when you need to send an update to
the server, you will build a Gson JsonObject
and pass it to our function that sends the JSON
to the server. When the server sends an update to your app, a handler in your code will be called
and passed the JsonObject
, which you can read data from
like you did in Checkpoint 2.
3.3. Messages We Send
This section shows the structure of every message sent by our server. Some of it is processed by our provided code, but your code is responsible for some parts.
Since all websocket messages are turned into JsonObject
s by our provided code,
there needs to be some way to tell what kind of update each message is. Our convention for
this app is that every websocket message has a string type
property specifying what kind of
event it represents.
You don’t need to and probably don’t want to read this kind of dense API documentation from start to finish. Instead, remember what kind of information this section has and refer to it when necessary.
3.3.1. full
When your app enters a game, the first message the server sends to it via the
websocket is a full
-type update, which includes everything about the game as
it stands at that moment. That data will be useful for loading the progress already made in the
game. It has these properties:
-
owner
(string) is the email of the game’s creator/owner -
state
(integer) is theGameStateID
code for the game state -
mode
(string) is the game mode, either "area" or "target" -
players
(array) is the list of players involved in or invited to the game, each of which is an object with these properties:-
email
(string) is the player’s email -
team
(integer) is theTeamID
code for the player’s team/role -
state
(integer) is thePlayerStateID
code for the player -
lastLatitude
andlastLongitude
(doubles) are the player’s last known location, only present if the player is currently playing the game and their phone has sent a location update -
path
(array) is the ordered list of objectives captured by the player, each of which is an object with these properties:-
Target mode only:
id
(string) is the unique ID of the target -
Target mode only:
latitude
andlongitude
(doubles) are the position of the target -
Area mode only:
x
andy
are theAreaDivider
-style cell indexes of the cell
-
-
-
Target mode only:
proximityThreshold
(integer) is the proximity threshold of the game in meters -
Target mode only:
targets
(array) is the list of all targets in the game, each of which is an object with these properties:-
id
(string) is the unique ID of the target -
latitude
andlongitude
(doubles) are the position of the target -
team
(integer) is theTeamID
code of the team that captured the target, orTeamID.OBSERVER
if not captured yet
-
-
Area mode only:
areaNorth
,areaEast
,areaSouth,
andareaWest
are the latitude/longitude of the boundaries of the area -
Area mode only:
cellSize
(integer) is the requested cell size in meters -
Area mode only:
cells
(array) is the list of captured cells, each of which is an object with these properties:-
x
andy
(integers) are theAreaDivider
-style cell indexes -
email
(string) is the email of the player who captured the cell -
team
(integer) is theTeamID
code of the team that captured the cell
-
You may find this example target mode update and example area mode update helpful.
3.3.2. gameState
When the game owner changes the game state (paused vs. running vs. ended), a gameState
-type
update is sent with this property:
-
state
(integer) is theGameStateID
code for the new game state
An example update is available.
3.3.3. playerLocation
When another player’s phone reports that they moved, the server relays that position change
with a playerLocation
-type update, which has these properties:
-
email
(string) is the moved player’s email -
lastLatitude
andlastLongitude
(doubles) are the player’s new location
3.3.4. playerExit
When another player exits the game activity—stops actively playing the game—the
server relays that change with a playerExit
-type event, which has this property:
-
email
(string) is the disconnected player’s email
3.3.5. playerTargetVisit
When another player in a target mode game captures a target, a playerTargetVisit
-type
update is sent, which has these properties:
-
email
(string) is the capturing player’s email -
team
(integer) is theTeamID
code for the capturing player’s team -
targetId
(string) is the unique ID of the captured target
You may find this example update helpful.
3.3.6. playerCellCapture
When another player in an area mode game captures a target, a playerCellCapture
-type
update is sent, which has these properties:
-
email
(string) is the capturing player’s email -
team
(integer) is theTeamID
code for the capturing player’s team -
x
andy
(integers) are theAreaDivider
-style indexes of the captured cell
You may find this example update helpful.
3.4. Messages You Send
When your app detects, based on changes in location, that the user has affected the game, the event should be reported to the server. This only needs to be done when the user is a player, since observers can’t affect the game.
Like messages from the server to your client, all these updates should include a type
property
specifying the kind of event.
3.4.1. locationUpdate
When the player’s phone reports a location update, it should be sent to the server so other users can see the updated location on their map. The update should also have these properties:
-
latitude
andlongitude
(doubles) are the phone’s current location
You may find this example update helpful.
3.4.2. targetVisit
When the player captures a target in a target mode game, a targetVisit
-type update should be
sent to the server with this property:
-
targetId
(string) is the unique ID of the captured target
An example update is available.
3.4.3. cellCapture
When the player captures a cell in an area mode game, a cellCapture
-type update should be sent
to the server with these properties:
-
x
andy
(integers) are theAreaDivider
-style indexes of the captured cell
An example update is available.
4. Your Goal
When you’re finished with Checkpoint 4, the game activity will support multiplayer games in both target mode and area mode! Other players' movements and objective captures will be displayed and the user’s movements will update the game information on the server when the game is running. The scores will be shown below the game map and be continuously updated as the user and other players capture objectives. The game state (paused vs. running) will be displayed and the game owner will have UI to change it or end the game. When the game is ended, the winning team will be displayed in a popup.
MP4 may sound scary at first—there are several new moving parts—so start early and take it one step at a time. Fortunately, you have your previous code to refer to for help. Feel free to come to office hours or post on the forum when stuck.
Unless otherwise specified, the following tasks can be done in any order.
4.1. Connecting
After you paste in the new GameActivity
starter code from Obtaining MP4,
the game activity will have several useful functions but will only show an empty map.
To start getting the user in the multiplayer game, the activity will need to connect to the
websocket for the game. We have provided the connectWebSocket
function to start that process,
but it relies on the game ID being stored in the gameId
instance variable.
Recall from MP2’s MainActivity
Enter buttons that the game ID
is passed to the game activity in the game
extra of the intent.
You need to fill in GameActivity
's onCreate
to store the game ID,
retrieved from the intent, in the gameId
instance variable
then call the connectWebSocket
function to connect to the websocket.
After completing this task, testWebSocket
will pass. When the server sends an update via
websocket, the message will be passed to your receivedData
function, which you will complete
in later sections.
4.2. Game State
You will need to finish the previous section before starting this one.
The full
update sent by the server shortly after the websocket is connected
specifies the current game state, either paused or running. The updateGameState
helper
function, which you need to fill in, is responsible for recording that new state in the
gameState
instance variable and updating the UI to match.
The state code it’s passed can be checked against
GameStateID
constants 4 to determine the current state.
We provided two views whose text you will need to update:
-
The
gameState
label should say "Running" when the game is running and "Paused" when it’s paused -
The
pauseUnpauseGame
button’s text should be "Pause" when the game is running and "Resume" when it’s paused
After implementing updateGameState
, you need to fill in the full
case of the receivedData
function to call that helper function, passing it the current game state.
Ignore the comment about the game instance variable for now—we’ll come back to that
in the next section.
We have provided UI and code that allows the game owner to pause and resume the game.
These actions generate a gameState
update sent to all participants
in the game via websocket. To keep the game state label continuously up to date, fill in the
gameState
case of receivedData
to pass the new state to your helper function.
Don’t worry about detecting game-over yet: that will be handled at the end of the checkpoint.
After completing this task, testGameStateDisplay
will pass.
4.3. Using Game
Subclasses
Putting game logic for both game modes directly in GameActivity
makes that one class
responsible for a lot. Rather than using if statements in several places, it would be nice if
the activity could trigger appropriate gameplay logic without always needing to check the game mode.
This can be accomplished by taking advantage of polymorphism: a game object can be notified
through a consistent interface of events that affect the game.
We have provided an abstract
Game
class
that represents a multiplayer game. It handles behavior used in all games, like showing circles
on the map at the locations of other players, and provides helper functions that will be useful
for implementing game-specific subclasses. Mode-specific logic will go in the overrides of four
methods:
-
The constructor is responsible for loading the current progress of the game and rendering that on the map.
-
locationUpdated
updates the running game according to the user’s movements, much likeupdateLocation
from the oldGameActivity
but specific to one game mode. When the player’s movements cause something to happen, it updates appropriate instance variables, draws on the map, and sends updates to the server. -
handleMessage
updates the game progress and map according to an update from the server about another player’s activity. -
getTeamScore
returns how many objectives the given team has captured so far. This will be used for scoring near the end of the checkpoint.
We’re not going to implement those quite yet, but we will set up GameActivity
to use them
so you can test your gameplay logic in the emulator if you like.
4.3.1. Connecting GameActivity
to Game
The app only knows which subclass is needed once the full
update is received to specify the
game mode. Fill in the other part of that case in receivedData
to initialize the
game
instance variable with a new TargetGame
or AreaGame
as appropriate for the mode.
You have variables for almost all the constructor parameters 5; the last parameter, context
, can be the activity itself
6.
Once the game object is set, other parts of the activity code can use it without needing to
care about the specific game mode.
The activity itself handles the full
update and gameState
updates, but all others have to do
with gameplay and should be handled by the game object. Fill in the default case in
receivedData
to call the game object’s handleMessage
function with the received update.
When the phone moves, GameActivity
is notified and calls its own updateLocation
function.
To make the user’s movements affect the game and be sent to the server, you will need to fill
in updateLocation
:
4.3.2. updateLocation
Logic
As noted in the comments provided inside that function, observers only watch the game and do not affect it. So if the user’s role in the game is Observer, the function should return before doing anything interesting. The game object provides a method that will be helpful for checking this.
So that other players' maps show your user’s location, set up a
locationUpdate
update that the provided code can transmit over the
websocket.
Movements shouldn’t affect a paused game, so only if the game is in the running state,
call locationUpdated
on the game object.
After completing this function, testLocationUpdates
will pass. Other work in this section will
be tested indirectly by later sections. If you’re not sure whether you successfully connected
GameActivity
to Game
functions, add print statements 7 to trace
the flow of execution to make sure Game
functions are being entered when the test suite expects
things to be happening.
4.4. Target Mode Gameplay
A visual explanation of this section is available 8:
We have provided a partially complete
TargetGame
class
that represents a multiplayer target mode game. Your job is to fill out the missing parts to
make target mode games work.
In addition to calling the Game
constructor with super
, TargetGame
's constructor
loads targets and paths from the JSON, storing them in instance variables and drawing them.
It stores all targets in the targets
map variable, looked up by the unique ID
assigned to each by the server. Each player’s path is a list of the IDs of the targets they captured,
stored as a List<String>
as a value of the playerPaths
map variable.
The data loading is correct, but the drawing depends on the
addLineSegment
helper function which you need to implement.
Getting the team colors array resource is very similar to how you
got the names resource in MP2 except that colors are stored as
int
s, so team_colors
is an integer array resource accessible with getIntArray
:
getContext().getResources().getIntArray(R.array.team_colors)
As before, you can index the array using a team ID: the team
parameter passed to your function.
To make the user’s movements affect the game, you will need to put target mode gameplay logic
in locationUpdated
. You will probably not want to use TargetVisitChecker
, but the
overall approach is the same as in Checkpoint 0:
-
Iterate over
targets
(see Maps) to find a target that’s within the proximity threshold. We suggest organizing the rest of the logic into thetryClaimTarget
helper function which can focus on just one target. -
Make sure the target isn’t already captured by any team.
-
If the player has captured a target already, check the hypothetical new line for crosses with existing lines from any player’s path. Here the
playerPaths
map will be helpful. -
If the snake rule is satisfied, capture the target.
-
Your
Target
class can change the marker’s color for you. -
The provided
extendPlayerPath
function can update the instance variables and add a line. -
To notify the server of the capture, build a
targetVisit
update and send it with the protectedsendMessage
function.
-
Since Game
subclasses should work in isolation from the app and Firebase, they should not
use FirebaseAuth
to get the player’s email. Instead, Game
provides a protected getEmail
function to retrieve the email passed to the constructor.
To show captures made by other players, you will need to add a little logic to handleMessage
.
The case that deals with playerTargetVisit
updates has some
provided code to get the properties of the update. You need to use those to change the
target’s marker color and extend the player’s path.
After completing this work, testMultiplePlayersTargetMode
will pass. We’ll come back to
getTeamScore
later. You can delete the fairly gross TargetVisitChecker
class now that
target mode gameplay is handled in a nicer way—previous checkpoints' test suites are
forward-compatible.
4.5. Area Mode Gameplay
The MP4 patch includes much more starter code for target mode than area mode, so you may prefer to finish Target Mode Gameplay first for an example.
A visual explanation of this section is available 9:
The
AreaGame
class
is responsible for multiplayer area mode games. It has the same
public functions as TargetGame
, but with the different rules for that game mode, the implementations
will be different. Specifically, you need to implement this logic:
-
The constructor is responsible for loading the area configuration, cell ownerships, and the player’s last capture from the JSON. It should render the area grid 10 and fill in captured cells with the capturing team’s color. 11
-
locationUpdated
is responsible for detecting, displaying, and reporting area mode updates made by the player. If the user entered an uncaptured cell satisfying the area mode snake rule, it should:-
record the change in your instance variables,
-
add a polygon on the cell colored with the player’s team color, and
-
send a
cellCapture
update to the server.
-
-
handleMessage
is responsible for showing cell captures made by other players, which it is notified of byplayerCellCapture
updates. When that happens, instance variables should be updated and a colored polygon should be added to show the capture. Other kinds of updates should be delegated to the superclass.
Much of this logic can be reused from or based on the area mode logic you wrote in Checkpoint 1. You may not assume, however, that the user is entering the game for the first time—your constructor will need to load the existing game progress, which may include a previous capture already made by the user.
After completing this work, testMultiplePlayersAreaMode
will pass. getTeamScore
will be
tested in the next section.
4.6. Scoring
You should complete all previous sections before starting this one.
Before the game can determine a winner, it will need to have a concept of score.
We define a team’s score as the number of objectives—targets or cells—it has
captured. Fill in the getTeamScore
implementation of both Game
subclasses to count
the given team’s captured objectives according to the current values in their instance variables.
We have provided a gameScores
label in the game activity layout to show the scores.
Fill in the updateScores
helper function in GameActivity
to set that label’s text according
to the scores of all four teams according to the game object.
That label should be kept up to date with the game, so you will need to call that helper function from several places. New score information may be available:
-
When initial information about the game is received
-
After an update is received from the server
-
After the player moves in a running game
After completing these tasks, testScoring
will pass.
4.7. Game Over
You should complete all previous sections before starting this one.
When handling Game State you took care of the paused and running states. That update is also sent when the game is ended by the owner. In that case, you need to show a popup/dialog stating the winning team.
The winning team is the one with the most points as reported by the game object’s getTeamScore
function. Ties are not tested and you may do anything you think is reasonable in that case.
You can look up a team name by team ID using the team_choices
resource.
To show a popup, create and show an
AlertDialog
similar to the example in the provided endGame
function. The message should state the winner,
e.g. "Red wins!", and dismissing the dialog should finish the activity. You only need
one button 12
and it can say anything you like.
You can register a handler with setOnDismissListener
that will run even if the user taps
outside the dialog to close it:
builder.setOnDismissListener(unused -> /* your code here */)
After completing this task, testGameOver
will pass. Well done!
5. Grading
MP4 is worth 100 points total, broken down as follows:
-
5 points for connecting to the game’s websocket
-
5 points for displaying the game state
-
5 points for sending location updates to the server when appropriate
-
25 points for multiplayer target mode gameplay
-
25 points for multiplayer area mode gameplay
-
10 points for scoring (
getTeamScore
implementations and score display) -
5 points for the game-over popup
-
10 points for passing
checkstyle
inspection -
10 points for submitting code that earns at least 40 points by 8 PM on your early deadline day
Your app will be tested by Checkpoint4Test
. Understanding the details of how the tests work
is not necessary, but reading what checks it makes may help you understand what your code
is supposed to do.
After submitting, always check that your commit appeared on the official MP grades page with the score you expected. Investigate and/or get help immediately if something seems to be wrong.
6. Cheating
The cheating policies in the syllabus continue to apply. You may of course copy and use all the code we provided to you, but for the parts we expect you to complete, submitting work done by anyone else is unacceptable. We will check all submissions from every checkpoint for plagiarism.
7. Epilogue
Congratulations! You have completed the Machine Project. Campus Snake 125 should now be fully functional. If deployed onto a physical phone, it can actually work; you can go outside and play the game!
Over the course of this project, you exercised many concepts learned in lecture and learned several important software engineering principles. Being immersed in Android app development prepared you for your final project, for which you can build any Android app you like—no specification, no test suites, no limitations. The world is yours!