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

Amazing Adventures

Assigned: 2020-02-05
Due Date: 2019-02-11 by 11:59PM U.S. Central Time

Assignment

Write a simple text-based "adventure game" that takes a description of the world in JSON and lets a user interactively navigate through the world. The game should print to and take input from the console.

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

The rubric can be found here.

Gradescope

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

Goals

Getting Started

Make sure IntelliJ recognizes the project as a Maven project. Jackson and JUnit are already listed as dependencies in the pom.xml file.

The JSON describing the game world can be found here: https://courses.grainger.illinois.edu/cs126/sp2020/resources/siebel.json

You should store JSON files in the src/main/resources/ folder.

You should avoid copy-pasting the entire JSON file into your code. You may hand-write small snippets of JSON for testing purposes and store them in your tests, or write your own JSON files and load them into your tests.

Part 1: Managing the JSON

You'll be using Jackson to deserialize JSON into objects representing the game world. You will need to be able to allow the user to enter a filepath to the JSON file so that they can play a different JSON adventure if they wish. If the user doesn't provide a filepath, default to the siebel.json file.

More on JSON and Jackson

Background Info

Serializing (or marshalling) is the process of converting objects in code into a different representation for storage, whether that's JSON, YAML, binary data, or something else. Deserializing (or unmarshalling) is the reverse: converting serialized data into actual objects in your code. In this case, we are transmitting serialized data about an adventure map to you, so that you can deserialize it into Java objects for processing.

A schema is a definition of how to represent some data. For example, if we want to describe a course at UIUC in words we might say the following:

  • A course at UIUC is an object
    • It has a department, which is a string
    • It has a number, which is an integer
    • It might be difficult, which is represented as a boolean
    • It might be time consuming, which is represented as a boolean
    • It has some professors who teach the course, represented as an array of strings

There is a natural translation between a schema describing objects stored in JSON and objects in Java.

JSON

The adventure map is stored as JSON, a data format popularized by the JavaScript programming language. Here's an example of some JSON, representing a group of courses at UIUC:

{
    "courses": [
        {
            "department": "Computer Science",
            "number": 126,
            "isDifficult": false,
            "isTimeConsuming": true,
            "professors": ["Craig Zilles", "Carl Evans", "Michael Woodley"]
        },
        {
            "department": "Computer Science",
            "number": 421,
            "isDifficult": true,
            "isTimeConsuming": false,
            "professors": ["Elsa Gunter", "Mattox Beckman"]
        }
    ]
}

Things to note about syntactically valid JSON:

  • The contents are wrapped in an outermost pair of curly braces: { and }
  • Keys are always quoted strings
  • Values can be numbers, strings, arrays, booleans, or objects
  • For arrays and objects, you must omit the very last comma

The JSON we're providing to you is already valid, so you don't need to worry about any of this unless you want to edit or write your own JSON for testing purposes. If you want to check that your JSON is valid, you can use a JSON validator like this one.

Jackson

Rather than having you parse JSON all by yourself, you're going to use the Jackson library to do it for you. Jackson is already included in your project; all you have to do is import and use it.

If we have some JSON like above, we need to define some Java classes that match the schema so that Jackson can deserialize the JSON into instances of those objects.

First, we define the outermost class with its fields:

public class CourseExplorer {
    private List<Course> courses;

    // -- Constructor and getter/setter methods omitted for brevity --
}

Then we define the inner class, with its own fields:

public class Course {
    private String department;
    private int number;
    private boolean isDifficult;
    private boolean isTimeConsuming;
    private String[] professors;

    // -- Constructor and getter/setter methods omitted for brevity --
}

Finally, we deserialize our JSON into an instance of CourseExplorer:

From a file (courses.json):
File file = new File("src/main/resources/courses.json");
CourseExplorer explorer = new ObjectMapper().readValue(file, CourseExplorer.class);
From a string:
String myJson = "...";
CourseExplorer explorer = new ObjectMapper().readValue(myJson, CourseExplorer.class);

Note that you can define your collections as either Object[] or List<Object> at your preference, and Jackson will convert it to the appropriate format.

If you need more documentation and examples on how to use Jackson, see the User's Guide.

JUnit's @Before

As with the previous assignment, we expect you to write unit tests as you develop. We're going to introduce a new feature of JUnit to make writing some kinds of tests easier for you: the @Before annotation.

Here's the motivation: your unit tests should be self contained so that running one test doesn't influence the results of another. When you have objects that are shared between tests, that can be hard to guarantee. When you apply the @Before annotation to a function in your testing class, it tells JUnit that it should run that function once before each and every one of your unit tests, so we can use an @Before-annotated method to reset any necessary state between test cases.

Specifically, this is great for tests involving Jackson:

public class CourseExplorerTest {
    private ObjectMapper mapper;

    @Before
    public void setUp() {
        mapper = new ObjectMapper();
    }

    @Test
    public void testCaseOne() {
        mapper.readValue(/* ... */);
    }

    @Test
    public void testCaseTwo() {
        mapper.readValue(/* ... */);
    }
}

Now we don't have to repeat the line ObjectMapper mapper = new ObjectMapper() in all of our tests, and the ObjectMapper instance will be reset before each and every one of our unit tests. For more information, see JUnit's documentation.

JSON Schema

Your project should accept the description of the game world using the following schema:

Direction := {
	directionName       : String
	room                : String 		// Name of the room this direction points to
}

Room := {
	name                : String
	description         : String
	directions          : Direction[]
	items               : String[]		// Not used for this assignment; items in the room.
}

Layout := {
	startingRoom        : String 		// The name of the start room for the game
	endingRoom          : String		// The name of the end room for the game
	rooms               : Room[]
}

Feel free to use any tools that convert the schema into Java code. For example, here is a great tool: http://www.jsonschema2pojo.org/. The options should be:

Part 2: Implementing the Game Engine

Output

Starting with the startingRoom, your game should display a message describing the player's current environment. This should include three parts, with a newline separating each:

  1. The current room's description, printed verbatim
  2. If the room is the endingRoom, print a message telling the player that they've reached the final room, and exit the program.
  3. A list of directions that the player can move.

Input

Once this message is written to the console, your program should read a line of input from the user.

  1. If that line is either of the words quit or exit, then you should exit the program.
  2. If that line starts with go some_direction, and some_direction is the name of one of the directions they can travel from that room, you should move the player in that direction and repeat the output/input process. If some_direction does not match any of the possible directions, you should print I can't go some_direction!
  3. If the user inputs something that isn't one of the commands above, you should print out I don't understand , followed by what the user typed surrounded in single quotes.

Your code should be able to handle any case of input commands, but when echoing back a user's input, it should use the original capitalization.

Example

You are on Matthews, outside the Siebel Center
Your journey begins here
From here, you can go: East
> go EAST
You are in the west entry of Siebel Center. You can see the elevator, the ACM office, and hallways to the north and east.
From here, you can go: West, Northeast, North, or East
> GO NoRtH
You are in the north hallway. You can see Siebel 1105 and Siebel 1112.
From here, you can go: South
> go TO HECK!
I can’t go TO HECK!
You are in the north hallway. You can see Siebel 1105 and Siebel 1112.
From here, you can go: South
> gophers ARE tasty!
I don’t understand ’gophers ARE tasty!’
You are in the north hallway. You can see Siebel 1105 and Siebel 1112.
From here, you can go: South
> go South
You are in the west entry of Siebel Center. You can see the elevator, the ACM office, and hallways to the north and east.
From here, you can go: West, Northeast, North, or East
> EXIT

Process finished with exit code 0

Deliverables and Grading

The primary deliverable for this assignment is the game program. The requirements for the program are listed below:

  1. At run time, parse JSON from a file.
  2. Correctly display the description/direction options for the current room.
  3. Handle the user-directed navigation between rooms.
  4. Start the game from the correct startingRoom, and end the game when the endingRoom is reached.
  5. Allow users to specify an alternate JSON file on the command line to get the room descriptions. Gracefully detect and handle the case where they're provided a bad filepath.

Other things we will take into consideration:

You are still expected to write unit tests for this project. Some parts of this project might be difficult to test; therefore, your challenge is to figure out how to structure your code in such a way that much of it can be tested.

Try to make commits at regular intervals. You want to leave a clear trail of discrete pieces of progress that you've made as you work. Commits are cheap! Make lots of them. Once again, try to involve testing in your code development process; you don't have to write all of your tests before the code, but you should try to develop your code alongside your tests and vice versa.

Use your best judgement to write the cleanest code you can; make sure you can justify any design decisions you make in code review.

This is a test of your problem-solving skills: if you don't know how to do something (for example, setting command line arguments in IntelliJ), you should try your favorite search engine. (Make sure you follow our policy on citing online code.) If you're still stuck, consider Piazza and/or Office Hours.