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 your app’s 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.
As usual, deadlines vary based on your deadline group. MP4 is due at:
-
11:59 PM on Sunday, 04/12/2020 for the Blue Group: all labs starting at 3 PM or earlier
-
11:59 PM on Monday, 04/13/2020 for the Orange Group: all labs starting at 4 PM or later
Additionally, 10% of your grade is for submitting code that earns at least 40 points by 11:59 PM on Sunday 04/05/2020 (Blue) or Monday 04/06/2020 (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 remote update.
However, since you’ve modified GameActivity.java
yourself, we need to distribute
that separately.
2.1. Obtaining MP4
-
Get the remote code. Select VCS | Git | Fetch from the menu to download the remote commit. However, it still needs to be merged with your work.
-
Merge the changes! Now we need to merge the changes from your computer with our remote repository, so go back to the menu and choose VCS | Git | Merge Changes. Check the
remotes/release/mp4
branch you just fetched and press Merge! The Version Control pane will appear to inform you of items updated from the server. -
Update
grade.yaml
. Change thecheckpoint
setting to4
, then do File | Sync Project with Gradle Files. -
Get the new
GameActivity
. When ready, copy-paste our updatedGameActivity
code over your version. You can always refer back to your previous commits' work through the GitHub web site.
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
Maps are very useful data structures that 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). 1
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. Accessing Resources
To make it easier to translate apps into different languages, Android considers it good practice
to put user-facing strings of text in string resources in res/values/strings.xml
rather than hardcoding them as string literals in your Java files.
We don’t impose that as a requirement, but you will find it helpful
to be able to access string and string array resources.
The object returned by the getResources
function of activities grants access to the app’s
resources. The value of string resources identified by their R.string
constants
2
can be accessed with getString
:
// Assume the context variable holds an Android Context object
// Activity objects are their own contexts
String appName = context.getResources().getString(R.string.app_name);
More relevant to the MP, resource arrays can be accessed with getStringArray
or getIntArray
by their R.array
constant:
String[] teamNames = context.getResources().getStringArray(R.array.team_choices);
If you’re curious, you can see Android’s official
Resources
documentation
for more information.
3.3. What is a Websocket?
In Checkpoints 2 and 3, we 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.4. 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.4.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.4.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.4.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.4.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.4.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.4.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.5. 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.5.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.5.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.5.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 virtual office hours or post on the forum when stuck.
4.1. 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 likeonLocationUpdate
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.
If you want to start earning points immediately, you can skip to the next section and start implementing
those. Alternatively, you can go through this section to set up GameActivity
to use them now
so you can test your gameplay logic in the emulator if you like.
4.1.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 3; the last parameter, context
, can be the activity itself
4.
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 onLocationUpdate
function.
To make the user’s movements affect the game and be sent to the server, you will need to fill
in onLocationUpdate
:
4.1.2. onLocationUpdate
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.
This section is tested by testGameActivityIntegration
. However, since the app depends on the
logic classes to work properly, the test will only pass once you’ve also completed most of the checkpoint.
If you’re not sure whether you successfully connected
GameActivity
to Game
functions, add print statements 5 to trace
the flow of execution to make sure Game
functions are being entered when the test suite expects
things to be happening.
4.2. Target Mode Gameplay
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.
You can get the team colors array resource as an array storing
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.
After making the class handle your user’s movements, testTargetMode
will pass.
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, testTargetModeMultiplePlayers
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.3. Area Mode Gameplay
This checkpoint provides much more starter code for target mode than area mode, so you may prefer to finish Target Mode Gameplay first for an example.
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 (tested by
testAreaModeLoading
) is responsible for loading the area configuration, cell ownerships, and the player’s last capture from the JSON. It should render the area grid 6 and fill in captured cells with the capturing team’s color. 7 -
locationUpdated
(tested bytestAreaModeMovement
) 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
(tested bytestAreaModeMultiplePlayers
) 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.
getTeamScore
will be tested in the next section.
4.4. Scoring
You should complete the Target Mode Gameplay and Area Mode Gameplay 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.
You can then take advantage of getTeamScore
to implement getWinningTeam
in the abstract Game
class. The winner is the team with the most points.
After completing these tasks, testScoring
will pass.
4.5. Game Over
You should complete all previous sections before starting this one.
Our provided code handles changes the paused and running game states.
The gameState
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 array from Accessing Resources.
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 8
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:
-
15 points for target mode setup and movement
-
10 points for handling target mode messages
-
10 points for setting up area mode games
-
10 points for handling area mode player movement
-
10 points for handling area mode messages
-
10 points for
getTeamScore
andgetWinningTeam
-
10 points for connecting game logic to the activity
-
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 the end of 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 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!