Info Assignments Office Hours Hall of Fame Notes
Info Assignments Office Hours Hall of Fame Notes

API Adventures

Assigned: 2020-09-16
Due Date: 2020-09-22 by 11:59PM U.S. Central Time

Assignment

Get your copy of the assignment here: https://classroom.github.com/a/ovabUQ0B

You may encounter "Non-managed pom.xml file found". You should select "Add as Maven Project"

This week's assignment is another adventure game, but we've made it more interesting by creating a website that will allow you to play your very own game visually! You'll be able to return not just text, but videos & images by URL, so you can add some extra visual flair to your game.

Note: We strongly recommend starting on this assignment early. A lot of your work will be refactoring your last week's implementation to work with our new requirements. Our suggested day-to-day plan is to spend the first couple days making those refactors to your implementation, then spend a day or two working on the API, and the last day working on the database/cleaning up your code. This assignment is much more manageable if you work on it a bit each day rather than tackling it all at once :) And don't forget to ask questions & take notes in code review!

Gradescope

There is no autograder for this assignment, but you still must submit your GitHub repository to Gradescope.

Goals

Instructions

Setup

After you've cloned your new repo, please move all of the files over from your previous week's adventure into this week's. That includes any other packages you might have made. For example, if your previous week's assignment looked like this:

- src
  - main
    - java
      - student
        - adventure
          - ClassA.java
          - ClassB.java
        - cli
          - ClassC.java
  - resources
    - something.json
 

... then this week's should look like:

- src
  - main
    - java
      - student
        - adventure
          - ClassA.java
          - ClassB.java
        - cli
          - ClassC.java
        - server
          - AdventureService.java
          ...
  - resources
    - something.json
    - client.cert
    ...

Basically, you're merging the directory structure.

Background

Last week, we asked you to build a text adventure game, but we also asked you to keep the engine as a discrete unit from the console interaction code. As you can imagine, there are tons of ways to play an adventure game. Some adventure games have fully-fledged 3D graphics, some are on 2D maps w/ sprites. While we're not going quite as far as these options, we are going to introduce a new UI to play your game with support for image/audio feedback! The best part: you'll be able to play your game right from the browser! We've created an Adventure website that supports playing a game through an Adventure API.

Wait, what is an API? API stands for Application Programming Interface, that is, an "interface" that allows two different applications to interact programmatically. You have never seen our Adventure website before, and your program currently does not support it. However, if we define an API contract between your game engine and our website, the two can work together to create a really cool experience!

We're not leaving you in the dust when it comes to creating this API: we've set up a local server for you to run that handles requests from the website, and we've written out all the methods you'll have to fill in. Your task is to define the interactions with your game engine.

Part 0 - Improving your Adventure Game Engine

After code review, you should have received some advice about your game engine's design. We recommend you start by implementing this feedback, as it will very likely help you define the API interactions later on. Things to be paying attention to:

Also don't forget to finish your custom feature this week!

Part 1 - Understanding the Provided Code

After you have moved your student.adventure package into the new project repo, you may have noticed a student.server package. This is where the API is defined: it has a server, a resource with URL paths, a service, and some POJOs. Let's go through what each of these are so you're familiar when you see them:

AdventureServer

This class holds the server for running your API locally. You are free to look through it, but it should be 100% set up for you, and any changes might interfere with the normal behavior of the API. The server is hosted on https://0.0.0.0:8080, which is port 8080 on your local machine. The base URL for the API is https://0.0.0.0:8080/adventure/v1; that is to say, anytime we talk about an API path, it's always prefixed with this. The website knows how to work with exactly this URL, so please don't change it!

AdventureResource

This class holds methods that allow the API to access your "resource". Resources are to REST APIs as objects are to Object Oriented Programming: they represent a store of data with some methods to operate on said data. Because our API is a REST HTTP API, we invoke these methods through visiting certain URLs. That's how the website can get data from your API without having to run Java: it only needs to know the URL to call, and the server will package a response that the website can handle.

You'll see some methods with documentation explaining what they do, but you'll also see some @ annotations above all of them.

@GET/@POST are HTTP verbs that respectively mean "access" a resource and "add to/change" a resource. When you see one of these above a method, it means the client needs to use that verb to call the method.

@Path represents the relative URL path a client needs to visit to call this method. Remember, all of these paths are prefixed with the base path: in our case, https://0.0.0.0:8080/adventure/v1/. So if you saw a method like

@GET
@Path("/ping")
public String ping() {
  return "pong";
}

...then that method would be invoked if the client sent a GET request to https://0.0.0.0:8080/adventure/v1/ping.

Lastly, you'll see some @Produces and @Consumes annotations. These say that the method should expect the request & response, respectively, to be of the specified type. We only use JSON to transfer our data, so you'll only see us using @Produces(MediaType.APPLICATION_JSON) and/or @Consumes(MediaType.APPLICATION_JSON) on our API methods.

You will not need to make many changes in here, but we will ask you to fill in the ping() method with the response "pong". You will also see we use AdventureService here.

AdventureService

This is a Java interface that defines a basic contract for your Adventure API on the Java side of things. The job of an interface class in Java is to force any class that uses it to implement the listed methods. You have certainly seen a very popular interface in Java before: List! Interfaces allow us to treat different objects the same way when it comes to these methods. That's why I can write

List<String> strings = new ArrayList<>();
// or
List<String> strings = new LinkedList<>();

string.add("Hi there!");

The add method is part of the List interface, and if I changed what type of underlying list I was using, it would not make me go back and change the calls to add.

We didn't force your game engine to conform to a certain class structure, so the biggest task of this assignment is for you to implement this interface and create your own Adventure service that works with our API.

Part 2 - Implementing the API

Just like last week, you are free to implement the API however you can, but because the website is static, you will need to make sure your API implementation follows the API contract. You should start out by implementing AdventureService. Your class should look like

public SomeAdventureService implements AdventureService {
  ...
}

You may notice that IntelliJ gives you a red squiggly line under the first line. If you recall, a class that implements an interface must define all the methods in the base interface. You can right click the red line, Show Context Actions > Implement methods, and it will automatically implement basic versions of the methods for you.

Your API must support multiple instances of the game, such that two clients could play a game at the same time. Lucky for you, web servers handle this very nicely outright, but your service will need a way to store a game instance for each instance ID. The instance ID comes from the URL, and the AdventureResource extracts it for you, and passes it to your service through methods.

Here are the methods you must implement, with a description:

void reset()

This method should clear out any instances of your adventure game, and reset the instance ID back to zero.

int newGame() throws AdventureException

This method is called when we want to create a new instance of a game. You should instantiate a new instance of the game, assign an id to it, and store it by its id.

GameStatus getGame(int id)

This method returns the current status for game instance with the ID of id. This includes what should be displayed to the user, what commands are available, whether or not the last action was an error, the instance id, and any additional game state the user needs. The GameStatus documentation more precisely explains its fields.

boolean destroyGame(int id)

This method destroys a game instance with the ID of id, clearing it from your service permanently.

void executeCommand(int id, Command command)

This method informs the game instance with the ID of id that a command has been issued to it. The command should be handled by the game instance, and the state for that game should be updated. The new state will be returned to the client the in the same way that getGame does.

You must find a way to implement these five methods to work with your game engine in order for the API to properly work.

Testing the API

To check to see if everything is working as planned, you should just go to the src/main/java/student/Main.java class and run main. Then, visit https://localhost:8080/adventure/v1/ping. You should see something saying "Your connection is not private. danger blah blah security blah blah risk blah ...". You need to click "Advanced/Show Details" and proceed to "localhost (unsafe)/Accept the risk and continue/Visit this website". Then you'll see a blank screen or some text; this means you are ready to begin testing out the UI. To see more about why your browser is warning you, see the section on "More about servers, ..., SSL, ...".

To test with the UI, visit https://courses.engr.illinois.edu/cs126/fa2020/adventure/. It should automatically recognize your server and start a new instance if you've implemented the service correctly.

Part 3 - Leaderboards and a Database

At the end of the day, you have created a game; now it's time to reward your players with high scores! The problem is that your current instances are somewhat ephemeral; that is to say, once the game exits/ends normally, you lose all of the data from that game, which is no good for high scores.

Enter databases! Using a database, we can persist high scores and retrieve them to show the client. You've spent some time learning about how SQL databases work, and now you get to apply it.

Creating the Database

Your database will be a local, file-based SQLite database to minimize setup. If you follow these steps, you should have no issues getting the database set up:

  1. In IntelliJ, navigate to View > Tool Windows > Database
  2. Hit the + button on the left, and select SQLite.
  3. For the file of the datasource, hit the + button, and create the database file at src/main/resources with the name adventure.db.
  4. You may need to click on the Driver: SQLite link to download the SQLite driver. If so, do this and come back to the main window.
  5. Click Test Connection. If all is well, it should show a green checkmark, and you should be able to start working!
  6. Later on, once you've added tables/values, you can come back to this database panel, and it should show the tables/schema.

Database Specification

You will be using JDBC to interact with your database. There are generally two things you will be doing with JDBC: connecting once at the beginning of your service and executing queries throughout the game's runtime.

Connect with JDBC

If you followed the steps above, you should be able to do this without a hitch!

private final static String DATABASE_URL = "jdbc:sqlite:src/main/resources/adventure.db";
private final Connection dbConnection;

...
    
dbConnection = DriverManager.getConnection(DATABASE_URL);

And that's it! You are connected to the database, and you'll want to use the Connection object you get throughout your program to work with SQL.

Execute SQL with JDBC

Statement stmt = dbConnection.createStatement();
ResultSet results;
if (stmt.execute("SELECT name, score FROM leaderboard_pjg4")) {
    results = stmt.getResultSet();
} else {
    // Fail somehow
}

// Now work with the results set, if applicable.

In this case, I executed SELECT * FROM leaderboard, but this could have been any valid SQL statement. As a result, I get a ResultSet. Here is a helpful link on how to use a ResultSet to extract the returned values.

Database Requirements

Your adventure game must interact with the database in the following ways:

Your leaderboard is global for your game. That means that there isn't one leaderboard per instance; they all play on the same leaderboard.

Deliverables and Grading

For the project to be considered complete, your submission must (in the order we suggest you tackle this):

As always, many points on this assignment are made up of aspects of your physical code and your design. Keep using correct style conventions according to the Google Java Style Guide, and think about how you can make a large application like this modular.

We're also paying a lot of attention to Object Decomposition on this assignment, as you will be working with a variety of objects, and you have the chance to improve your designs from last week. Make sure you are placing methods in the classes where they make the most sense, and also make sure data flows through your objects in a clean, extensible manner.

Assignment Rubric

Click here to view ## Note! New items in bold ### Readability and flexibility of code
  • Modularity: each method should perform one distinct task
  • It should be easy to read through each method, follow its control flow, and verify its correctness
  • The code should be flexible/ready for change (no magic numbers, no violations of DRY)

Object decomposition

  • Choice of classes to create

    • Classes should be coherent units and have clear purposes

    • Classes should not be too “big” or be responsible for too much functionality

      • If a “big” class contains things which aren’t closely related, it may be appropriate to break off part of it into a new class. (This is similar to decomposing long functions into modular units.)
    • If a group of global/static functions are all operating on the same data/object, they should probably be bundled together into a class

    • As an example, the information stored in parallel arrays is often closely related, so it’s usually good design to group the information into a class and combine the parallel arrays into a single array

  • Placement of methods

    • Functionality should be placed in the class that it's most related to
  • Member variables stored by each class

    • Classes should store their data using the most intuitive data structure
    • No "missing" member variables
    • No member variables which should be local variables
    • No redundancy / storing multiple copies of the same data in different formats
  • Encapsulation

    • Appropriate access modifiers
    • Member variables should generally only modified by member functions in the same class
    • The interface of a class should be intuitive/abstract, and external code should only interact with the class via the interface
      • By intuitive, we mean that it should be easy to understand and use the class, and there shouldn’t be any hidden assumptions about how the class should be used
      • By abstract, we mean that an external client shouldn’t need to worry about the internal details of the class
    • No unnecessary getters/setters
    • In Java, getters should not return a mutable member variable

Documentation

  • Specifications
    • Specifications are required for all functions which are part of the public interface of a class
    • Specifications should precisely describe the inputs and outputs of a function, and should also describe what the function does (e.g. mutating state of object)
    • Specifications should also be formatted properly (e.g. follow Javadoc style for Java assignments)
  • Inline comments should not describe things which are obvious from the code, and should describe things which need clarification

Naming

  • Semantics: names should effectively describe the entities they represent; they should be unambiguous and leave no potential for misinterpretation. However, they should not be too verbose.
  • Style: names should follow the Google Java/C++ Style Guide

Layout

  • Spacing should be readable and consistent; your code should look professional
    • Vertical whitespace should be meaningful
      • Vertical whitespace can help create paragraphs
      • Having 2+ empty lines in a row, or empty lines at the beginning or end of files, is usually a waste of space and looks inconsistent
    • Horizontal whitespace should be present where required by the Google Style Guide
  • Lines should all be under 100 characters; no horizontal scrolling should be necessary

Testing

  • You should make sure all types of inputs and outputs are tested.
    • If category A of inputs is likely to cause different behavior than category B, then you should test inputs from both categories
  • Boundary/edge cases often cause different/unexpected behavior, and thus, they should be tested
  • Your tests should ensure that the application state is correctly updated. For example, if a function mutates an object, you can’t just check that the return value is correct. You must also ensure that the object reflects those changes.
  • Your tests should cover all of the functionality that you’ve implemented. In other words, every line of code should be exercised by some test case, unless the assignment documentation says otherwise
  • Each individual test case should only serve one coherent purpose. Individual test cases should not have assertions testing unrelated things
  • Your tests, like your code, should be organized and easy to understand. This includes:
    • Easy to verify thoroughness / all possibilities covered
    • Easy to verify the correctness of each test case
    • Clear categories of test cases, where similar tests are grouped together
      • In Java, this can be accomplished by inserting comments to separate groups
    • Test case names make the purpose of each test case clear
      • Here is one possible way to name test methods in JUnit
    • Different test files for different source files

Process

  • Commit modularity
    • Code should be checked-in periodically/progressively in logical chunks
    • Unrelated changes should not be bundled in the same commit
  • Commit messages
    • Should concisely and accurately describe the changes made
    • Should have a consistent style and look professional
      • First word of the message should be a verb, and it should be capitalized

Presentation

  • Arrived on time with all necessary materials and ready to go
  • Good selection of topics to focus on
  • Logical order of presentation
  • Appropriate pacing and engagement of the fellow students
  • Speaking loud enough and enunciating clearly

Participation

  • Each student should contribute at least one meaningful comment or question for every other student who presents in his/her code review
  • Students must behave respectfully to moderator and other students

Weightings

Your grades for each section of the rubric will be weighted as follows:

  • Readability and flexibility of code (15%)
  • Object decomposition (20%)
  • Documentation (10%)
  • Naming (10%)
  • Layout (5%)
  • Testing (20%)
  • Process (10%)
  • Presentation (5%)
  • Participation (5%)