A2: Adventure

Deadline: Friday, 03/09/18, 11:59 pm

This assignment is to be done as individuals, not with partners nor with teams.

In this assignment, you will develop a text adventure game (TAG), also known as interactive fiction. The characteristic elements of TAGs include gameplay driven by exploration and puzzle-solving, and a text-based interface in which users type natural-language commands and the game responds with text. The seminal work in this genre is the Colossal Cave Adventure.

Exercise: An interactive example is worth a thousand words. So before reading any further in this writeup, spend just a few minutes playing this online version of Colossal Cave Adventure. And don't worry; that game is far bigger than what you will build in this assignment.

How to get started: Begin by reading this entire writeup and making sure you have a good understanding of it. The next thing you should do is spend a good bit of time, maybe a whole day, sketching out on paper how you're going to solve this assignment. Your focus should be on identifying what features you need to implement, what data structures and functions you'll need for those features, and how you'll go about testing those data structures and functions during the process of implementing them. Waiting until the very end to test is a recipe for disaster!

Table of Contents:

Introduction

You are to implement a game engine that could be used to play many adventures. Here, the game engine is an OCaml program that implements the gameplay and user interface. An adventure is a data file that is input by the game engine and describes a particular gaming experience: exploring a cave, hitchhiking on a spaceship, finding the missing pages of a powerful magical book, etc. This factoring of responsibility between the engine and input file is known as data driven design in games.

The gameplay of TAGs is based on an adventurer moving between rooms. Rooms might represent actual rooms, or they might be more abstract—for example, a room might be an interesting location in a forest. Each room also has a text description associated with it. Some rooms have items in them. These items can be taken by the adventurer and carried to another location, where they can then be dropped. The adventurer begins the game in a predetermined starting room, possibly with some predetermined items.

The player does not so much win a TAG as complete the TAG by accomplishing various tasks: exploring the entire map of rooms and corridors, finding items, moving items to specified locations, etc. To indicate the player's progress toward completion, a TAG gives a player a numeric score. The TAG also tracks the number of turns taken by the player. Savvy players attempt to achieve the highest score with the lowest number of turns.

Your task is to develop a game engine and to write a small adventure of your own.

Assignment information

Objectives.

Recommended reading.

Caution: There is a chapter on JSON in RWO, but you are probably better off ignoring it. The features used in that chapter are more advanced than what you need, hence might be more confusing than helpful for this assignment. The ATDgen library and tool at the end of that chapter are not permitted for use on this assignment, because using them would preclude some of the list and tree processing that we want you to learn from this assignment. Note that the Core library used in that book is not supported in this course and will cause your code to fail make check.

Requirements.

  1. Your engine must satisfy the requirements stated below.

  2. You must submit your own, original small adventure file.

  3. You must submit an OUnit test suite, as described below.

  4. Your code must be written with good style and be well documented.

  5. You must submit an overview document, as described below.

What we provide. In the release code on the course website you will find these files:

Grading issues.

Prohibited OCaml features. You may not use imperative data structures, including refs, arrays, mutable fields, and the Bytes and Hashtbl modules. Strings are allowed, but the deprecated functions on them in the String module are not, because those functions provide imperative features. The Map module is not imperative, hence is permitted. Your solutions may not require linking any additional libraries/packages beyond OUnit, Yojson, Str, and ANSITerminal. You may and in fact must use I/O functions provided by the Pervasives module, even though they cause side effects, to implement your user interface.

Part 0: Makefile

As usual, we provide a makefile.

Part 1: Game engine

Implement a game engine that provides the following functionality. The release code provides the following files to get you started:

You are permitted to change the .ml files. The existing code in the .mli files may not be changed, unless otherwise specified, because those files declare the names and types against which the course staff will test your engine. You are permitted to add new declarations to the .mli files, because additional names and types the course staff is unaware of will not impair our testing. In fact, you will almost certainly want to declare new names and types in state.mli.

Interface. The interface to a TAG is based on the player issuing text commands to a prompt; the game replies with more text and a new prompt, and so on. Thus, the interface is a kind of read-eval-print-loop (REPL), much like utop. For this assignment, commands will generally be two-word phrases of the form "VERB OBJECT". We leave the design of the user interface up to your own creativity. In grading, we will not be strictly comparing your user interface's text output against expected output, so you have freedom in designing the interface.

All the console I/O functions you need are in the Pervasives module. Some people might want to investigate the Printf module for output, but it's not necessary. The Scanf module is overkill for input in this assignment; in fact the read_line function in Pervasives is probably all you need. For parsing player commands, you are welcome to use the OCaml Str library, but it's not necessary; the standard library String module suffices.

Your user interface must be implemented entirely within main.ml or any supporting modules you design yourself. It may not be implemented in state.ml. As the specification of do' in state.mli says, that function may not have any side effects, especially not printing nor terminating. There are a couple reasons for this restriction. First, we want you to think of State as being purely functional. Second, games are often designed using the Model-View-Controller design pattern. Here, State is the model, hence it should not be implementing any of the user interface. Indeed, you should imagine your State module being usable equally well by other programmers implementing graphical user interfaces, which have no need for printing of text responses. So instead of printing inside State, you can add functions to the State interface to help Main figure out what to print.

Commands. Your engine is required to support the following commands:

Input from the player must be handled as case-insensitive. For example, "go north" and "gO NoRtH" should be recognized and treated as the same. Nonetheless, the room descriptions, directions and item names in the adventure file are case sensitive and should be displayed by the engine with their proper case. For example, if the adventure file defines an item named "Lambda", then when that name is displayed it should be "Lambda" and not "lambda". But the player is permitted to enter "take lambda" to pick it up.

A turn is any successfully completed issue of the commands "go" (or any of its shorthand forms), "take", or "drop". For example, "go north" counts as a turn only if the current room permits exit to the north. At the beginning of play, the current number of turns is zero.

Scoring. The player's score is determined by these rules:

The winning score for a game is sum of all the item points plus the sum of all the room points, regardless of whether it's actually possible to earn those points (i.e., maybe a room is unreachable), or whether the points are negative (hence decrease the sum). If the player does earn the winning score possible for the adventure, the game engine informs the player that they have completed the adventure using a message specified in the adventure file, but the player is still allowed to interact with the game afterwards—the game doesn't automatically quit. So the player's score might go down or up again. After the completion message has been displayed once, the engine does not have to display it again.

Robustness. We want you to imagine that you are writing the game engine as a product that you ship to customers. The customers then additionally acquire adventure files that they want to play, and those files could be authored by someone other than you. In that scenario, you want to make sure that the customers do not blame you for errors that are not your own. That is, the game engine itself should not exhibit any errors, but if it detects an error in an adventure file, it should correctly blame that file (and by association its developer rather than you).

So your game engine may not terminate abnormally, meaning it may not raise an unhandled exception, nor may it go into an infinite loop that would prevent the player from entering a command. But if the adventure file itself contains errors, your engine is permitted to print an error message blaming the adventure file, then terminate normally.

Adventure files. Adventure files are formatted in JSON. We provide a couple example adventure files in the release code. Note that the JSON property names in that file may not be changed. Also note that many of the property values are strings, which are case sensitive and can contain arbitrary characters (including whitespace). An adventure file contains six main entries:

Adventure files will never have huge maps in them; as a rough estimate there might be on the order of 100 rooms and 100 items.

For those who desire additional precision, we provide a JSON schema for adventure files in schema.json. Your game engine is required to be compatible with this schema, which defines the required elements of the file and their names. It is possible for you to extend this schema to add more functionality to your engine, but you must make sure that you don't remove any properties or objects and that you maintain support for adventures that don't have your additional features. Otherwise, the course staff will be unable to grade your submission using our test suite of adventures. Using a JSON schema validator, you can check the well-formedness of an adventure file again the schema.

There are many errors that could exist in an adventure file, however, that cannot be detected by a schema validator. For example, a room could have an exit to a non-existent room, or two items could have the same name, or a non-existent item could be given in the starting inventory, or there could be two exits with the same name, etc. For these and other such errors that are the fault of the adventure file creator (as discussed above under the heading "Robustness") your game engine may not itself exhibit any errors, but it may blame the adventure file and terminate normally.

Your engine is not responsible for detecting adventure file errors or for attempting to continue gameplay beyond the point it happens to detect an error, although your engine may not raise unhandled exceptions or go into an infinite loop, as discussed above under "Robustness."

JSON. We recommend using the Yojson library's Basic module for parsing JSON adventure files. Although you are welcome to use any functionality provided by the library, we suggest concentrating your attention as follows:

We provide a small tutorial on Yojson in this OCaml file, which uses this JSON file as an example.

Plan of attack. Here's one way you might approach implementation of your game engine.

  1. Implement the loading of an adventure file into a json-typed value.

  2. Implement converting that JSON into OCaml types, including designing the types. Do some interactive testing in utop to check whether it looks like the conversion is being done right.

  3. Implement the game state, including producing the initial state, and all the required functions except do'. Write an OUnit test suite using those required functions to make sure the initial state is correct for some small adventure files.

  4. Implement parsing of a string into a verb and object (if any), including designing a type to represent commands. Do interactive testing in utop to make sure all the commands that need to be supported are being correctly parsed.

  5. Implement do' and write a OUnit test suite for some test adventure files.

  6. Implement the REPL, test it interactively, and get a non-3110 friend to playtest it for you.

Tasks 2 and 5 are probably the hardest, but YMMV.

Part 2: Testing

There are two aspects to testing this assignment. The first is to create an OUnit test suite in state_test.ml. The second is to playtest your REPL.

Gameplay will be only a small part of the grading. But bear in mind that really poor gameplay could make it difficult or impossible for the grader to assess your submission.

Part 3: Your own adventure

Write and submit your own adventure that a grader will play using your own engine. Your adventure must have at least 5 rooms and 3 items. If your engine is somehow non-operable, the grader will attempt to play it using the staff's own engine.

We ask you to do this for two reasons. First, it will help you understand the adventure file format and implement your game engine. (So you need not do this part last!) Second, it provides you with an opportunity for some creativity, which we encourage but will not assess as part of your grade. If there are some really great adventures submitted, we will consider making them available for other students to play.

Part 4: Overview document

You are to turn in an overview document as described by this handout on overview documents. Think of this is an expanded version of the feedback document you wrote for A1. The handout also provides an example overview document for an old assignment. The main purpose of the overview document is to cause you to reflect on the programming experience you get out of each assignment.

What to turn in

Submit files with these names on CMS:

Assessment

The course staff will assess the following aspects of your solution. The percentages shown are the approximate weight we expect each to receive.

Karma

You are highly encouraged to go beyond the minimal requirements for this assignment as described above. But, no matter what you implement, be sure to maintain compatibility with the basic adventure file format and required commands. Note that it should be possible to add additional information (e.g., properties and objects) to the JSON file and still remain compatible with the required schema. Your karma implementation must not cause you to change or violate the specifications of any of the functions in the provided interfaces. It's true that might rule out some cool features that you have in mind, but we hope you understand that the course staff must be able to run your solution through our autograder with those interfaces.

Experienced Adventurer:

Seasoned Adventurer:

Master Adventurer:


Acknowledgement: Adapted from Prof. John Estell (Ohio Northern University).