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

  1. 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.

  2. 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.

  3. Update grade.yaml. Change the checkpoint setting to 4, then do File | Sync Project with Gradle Files.

  4. Get the new GameActivity. When ready, copy-paste our updated GameActivity 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 NetID Strings (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

Java map objects implement the Map interface. The most commonly used implementation is HashMap.

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 JsonObjects 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 the GameStateID 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 the TeamID code for the player’s team/role

    • state (integer) is the PlayerStateID code for the player

    • lastLatitude and lastLongitude (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 and longitude (doubles) are the position of the target

      • Area mode only: x and y are the AreaDivider-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 and longitude (doubles) are the position of the target

    • team (integer) is the TeamID code of the team that captured the target, or TeamID.OBSERVER if not captured yet

  • Area mode only: areaNorth, areaEast, areaSouth, and areaWest 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 and y (integers) are the AreaDivider-style cell indexes

    • email (string) is the email of the player who captured the cell

    • team (integer) is the TeamID 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 the GameStateID 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 and lastLongitude (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 the TeamID 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 the TeamID code for the capturing player’s team

  • x and y (integers) are the AreaDivider-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 and longitude (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 and y (integers) are the AreaDivider-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 like onLocationUpdate from the old GameActivity 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 ints, 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:

  1. Iterate over targets (see Maps) to find a target that’s within the proximity threshold. We suggest organizing the rest of the logic into the tryClaimTarget helper function which can focus on just one target.

  2. Make sure the target isn’t already captured by any team.

  3. 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.

  4. If the snake rule is satisfied, capture the target.

    1. Your Target class can change the marker’s color for you.

    2. The provided extendPlayerPath function can update the instance variables and add a line.

    3. To notify the server of the capture, build a targetVisit update and send it with the protected sendMessage 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 by testAreaModeMovement) 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:

    1. record the change in your instance variables,

    2. add a polygon on the cell colored with the player’s team color, and

    3. send a cellCapture update to the server.

  • handleMessage (tested by testAreaModeMultiplePlayers) is responsible for showing cell captures made by other players, which it is notified of by playerCellCapture 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 and getWinningTeam

  • 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!


Created 7/14/2020
Updated 7/14/2020
Commit 2091ea3 // History // View
Built 7/14/2020 @ 16:28 EDT