MP10
Text-Based Adventure
Due dates may be found on the schedule.

Changelog

2024-12-06 10:08 CDT
Updated documentation of /transfer to use the field "to" found in hub.py instead of the incorrect "location" previously documented.

This description is quite long. So is both the starting code and the code you turn in.

That said, the code is fairly simple; a little global state and many repetitive if statements. To get to that code, you will first need to decide what the domain tracks and pick a few design alternatives based on the system design.

The entire class on 2024-12-05 was devoted to discussing this MP and the associated project. If you missed that class, you should watch the recording before attempting this MP.

As decided by roughly a ⅔ majority vote of those in class on November 7, our final project will be a distributed text-based adventure game in the spirit of Colossol Cave or Zork.

In this MP you will implement a specific 3-room single-player text-based adventure domain. The project that follows will have you build your own domain and handle multiple players.

Try it out

We intend to keep a full example implementation running at

http://fa24-cs340-adm.cs.illinois.edu:10340/

We invite you to go there and try it out, and also to compare your implementation to it. Note that we have not spent much effort trying to make this error-proof, so it might crash at some point.

1 Initial Files

mp10.zip contains a set of starter files.

You will code in and submit only domain.py.

You can try out your code by running make start and visiting the URL it prints in the browser.

You can run automated tests with make test or python3 -m pytest.

2 Coding Task

You will implement four web API endpoints, all using the POST method:

2.1 /newhub

This is called once during game set-up before any other functions are called.

The request body is the plain-text URL of the hub server.

You should POST to the hub server’s /register endpoint a JSON object with the following fields:

{ "url": whoami # the base URL of your domain server
, "name": "MP10" # the name of your domain
, "description": "An example domain based in Siebel 1404 and its surroundings."
, "items": domain_items # a list of all items you might give a player
}

The items field has the name, description, verb, and (if appropriate) depth fields. The id field will be assigned by the central hub and sent back as part of the reply.

The response from that URL will be a JSON object; it will have a single field error if something goes wrong; otherwise it will contain

{ "id": ... # a number identifying your domain
, "secret": ... # a string used to authenticate your domain
, "items": [...] # a list of item IDs in the same order as the request
}

It’s up to you to remember which item had which id; for example, domain_items[0]['id'] = response_data['items'][0].

The /newhub response should be a JSON object with a single field ok, the value of which is a string to show the user. If something goes wrong instead have a field error with a string to show the user and a non-success status code.

2.2 /arrive

This is called by the central hub server each time a user either (a) logs in or (b) returns to your domain after journeying elsewhere. It establishes the identity of the user, the inventory of the user, and items the user is expected to find your the domain.

The request body is a JSON object with the following fields:

{ "secret": ...    # the same string as from /newhub
, "user": ...      # a number identifying the user
, "owned": [...]   # items from this domain in the user's inventory
, "carried": [...] # items from other domains in the user's inventory
, "dropped": [...] # items the user left in this domain
, "prize": [...]   # items other domains want the user to find in this domain
}

Each dropped item has a location field indicating where it was dropped.

Each prize item has a a depth field indicating how many out-of-domain barriers the user should have to pass to find the item.

The response should have a 200 status code; its content is ignored.

Two approaches to inventory

If you track user inventory yourself, the four lists will help you do this.

If you have the hub track inventory, you still need to track the definitions of each item which are provided in these lists.

2.3 /dropped

This is called by the central hub server each time a user drops an item in your domain.

The request body is a JSON object with the following fields:

{ "secret": ... # the same string as from /newhub
, "user": ...   # a number identifying the user
, "item": ...   # the item being dropped, including id, name, description, and verb
}

The response should be a JSON value indicating your own internal way of tracking where the user was when they dropped the item. This could be a string like "classroom", a numeric identifier like 340, a more complicated object like {"latlon": [40.11, -88.24], "building": "Siebel", "room": 1404}; its implementation is up to you. This same value will be provided in the dropped field of subsequent /arrive calls.

Two approaches to inventory

If you track user inventory yourself, you’ll need to update that here and return a location as well.

If you have the hub track inventory, this can be just a single return of the user’s current location.

Do not call /transfer from /dropped /dropped is called part-way through the hub’s /command processing for a drop item command. That command processing will handle moving the item; all it wants from your domain is the location to move it to.

2.4 /command

This is called by the web user interface each time the user types something.

The request body is a JSON object with the following fields:

{ "user": ...      # a number identifying the user
, "command": [...] # a list of strings, the words that the user typed
}

The response should be text to show the user in response to this command. For simplicity of testing, we provide a set of strings to use as responses in various situations. You are welcome to add additional responses of your own if you would like, but not to edit the ones we provide.

/command returns a simple string response, not a JSON response.

As a special case, if your response begins "$journey " then the web UI will automatically run your response (minus the leading $) as if the user had typed it.

2.5 Item format

Items are represented in interactions with the hub as JSON objects or as integer identifiers. The JSON object will have a subset of the following fields:

{ "name": ...     # a single-word label for the item
, "description": ... # response to "look item"
, "verb": {...}   # a map of {verb: response to "verb item"}
, "id": ...       # integer id used by hub for this item
, "location": ... # a value you returned from /dropped
, "depth": ...    # an integer indicating how hard this should be to find
}

A word means lower-case ASCII letters, digits, and hyphens, but not just digits.

If the item was

{ "name": "widget"
, "verb":
    { "open":"It seems to be stuck."
    , "close":"It's already closed"
    }
}

then the command open widget would return the reply It seems to be stuck.

An item only has depth if it is created and owned by one domain but hosted and found by the user in another domain. When present, depth is a non-negative integer. For this MP, simply place items of a given depth in the specified locations. Additional meaning for depth will be added in the project.

2.6 The MP10 domain

There are three locations in the MP10 domain.

podium down     up west     east classroom             north              south foyer east
Map of domain

2.6.1 The foyer

  • Users start here.
  • Users can read sign here.
  • There is a paper here, which the user can take and then read
  • go east goes to other domains; return "$journey east" to make that happen.
  • go north goes to the classroom

2.6.2 The classroom:

  • Any depth-0 items hosted by this domain go here.
  • south goes to the foyer.
  • down and west both go to the podium.

2.6.3 The podium

  • up and east both go to the classroom.
  • If the cabinet is open, any depth-1 items hosted by this domain go here1 For the project, this would be depth 2, not 1, because to get inside you need to have a depth-1 key. But I released this MP and several dozen students downloaded it and started working on it before I realized this mistake, and for this single-domain MP the meaning of depth isn’t particularly important so I decided to leave it as depth 1 and add this margin note instead of changing the code after I’d released it..
  • There is a cabinet here initially in the locked state.
    • When locked, use key cabinet will change it to closed if they have a depth-1 key owned by this domain but hosted in another domain.
    • When closed, use key cabinet will change it to locked if they have the key.
    • When closed, open cabinet will change it to open.
    • When open, can use switch to change the switch state between down (its initial state) and up
    • When open, can close cabinet to change it to closed.
  • If the switch goes up, the screen changes to password state
    • When the screen is in password state, user can tell screen xyzzy to change it to won state

2.6.4 What the domain tracks

In summary, you will need to track

  • Three locations: foyer, classroom, podium
  • One local item (the paper)
  • One owned item hosted by other domains at depth 1 (the key)
  • A cabinet with three states: locked, closed, open
  • A switch with two states: down, up
  • A screen with two or three states: password, won; but if switch is down, the screen is blank
  • Hosting depth 0 and depth 1 items owned by other domains

We recommend, but not not require, also tracking what locations the user has seen before and using this to choose between two responses when the user enters a location: showing a full description on the first visit (what look would return) and just the name of the location thereafter.

2.6.5 Location-based actions

All commands should be checked against the user’s current location, and possibly against what items are in that location, the user’s inventory, and other game state.

For example:

  • If the user gives a command take whatnot, only move an item named whatnot into the user’s inventory if there’s an item with name whatnot in the user’s current location.

  • If the user gives a command take 43, only move item id 43 into the user’s inventory if there’s an item with id 43 in the user’s current location.

  • If the user gives a command read sign, only respond with the contents of the sign if the user is in a location with a sign.

  • If the user gives a command use key cabinet, only do what that should do if both the user has an item named key in their inventory and there’s a cabinet in the user’s current location.

3 System Design

There are three applications of interest to this game.

  1. The Web User Interface (UI) is written in JavaScript and HTML5 and is provided for you.

    • Displays things to the user
    • Accepts and cleans up user input
    • Routes each user command to the hub or a domain
  2. The Central Hub Server (hub) is written in Python with aiohttp and is provided for you.

    • Tracks user inventory and dropped items
    • Registers domains and users
  3. The Domain Servers (domain) are written in Python with aiohttp by you.

    • Tracks user location and world state
    • Responds to most user commands

You will program a domain server. There are multiple correct ways to do this; the design intentionally allows for multiple ways of handling the same task.

Two inventory designs

How do you track where items are? Here are two options:

  1. Track item locations in the domain code.

    • Initial state is given by your starting code.
    • State is modified by any /arrive and /dropped requests from the hub.
    • Whenever the user tries to use an item, the domain’s state indicates if this is possible.
    • State is modified by user actions.
    • When an item enters or leaves a user’s possession, the hub is informed of this using /transfer
    • The domain never calls /query
  2. Track item locations with the hub.

    • Initial state is set by your starting code and sent to the hub using /transfer when a new user /arrives.
    • Item descriptions are collected from /arrive, but their locations are ignored.
    • Whenever the user tries to use an item, /query is used to find out where it is.
    • When an item enters or leaves a user’s possession, the hub is informed of this using /transfer

Option 1 has more code for tracking item state; Option 2 has more code for knowing if a user typing look at paper2 The UI will turn this into ["look", "paper"] before sending it to /command should return the description of the item named paper or not.

Two command processing philosophies

How do you decide what response to make to a given command? Here are two philosophies; many of you will end up somewhere between these extremes.

  1. In your /command, have one return line for each possible (response, state change) pair. Put each return and its associated state change inside an if that checks for all the criteria that could allow it to happen.

    For example, you might have a bit of code like

    if command == ["read", "paper"] and "paper" in carried_items:
        return Response(text='The paper reads <q>XYZZY</q>')
  2. Build data structures representing all possible actions and state. In your /command, use those data structures to find what changes to make and responses to return.

    For example, you might have a bit of code like

    for item in all_items:
        if item['location'] == 'inventory' and command[1] == item['name']:
            if command[0] in item['verb']:
                return Response(text=item['verb'][command[0]])

Option 1 is easier to get started, but also longer and easier to miss some corner case. Option 2 is more work to set up, but also easier to extend to a larger and more complicated domain. Many of you will end up with some kind of hybrid, using option 2 for common cases and option 1 for special cases.

4 Hub’s Domain-Focussed API

The following API endpoints are exposed by the hub server to each domain server. Each is a POST-method HTTP request with a JSON-formatted request and response.

4.1 /transfer

This endpoint is used to move items in the hub’s item location tracking.

The request body should contain the following fields:

{ "domain": ... # your domain's ID as given during /newhub
, "secret": ... # your domain's secret as given during /newhub
, "user": ...   # the user ID as given in /arrive
, "item": ...   # a numerical item ID
, "to": ...     # where you want the item to go
}

If to is "inventory", the item will be picked up and carried with the user. Any other to value is treated as identifying a particular part of your domain where the item is being stored.

The response will be status 200 {"ok": "item transferred"} on success or a status between 400 and 499 with body {"error": "..."} if the transfer cannot be accomplished.

FAQs about /transfer
If I track my own inventory, do I need to call /transfer?
Yes, each time a user successfully takes an item.
What is the URL I send /transfer to?
The hub URL, with /transfer appended. The hub URL was given as the request to /newhub.
Where do I get the item IDs?

For items from your domain, these are from the "items" response to /register, which was called inside /newhub.

For items from other domains, these are from the "carried" and "prize" entries given to /arrive.

4.2 /query

This endpoint is used to discover what items the hub believes are in a given location.

The request body should contain exactly 4 of the following fields:

{ "domain": ... # your domain's ID as given during /newhub
, "secret": ... # your domain's secret as given during /newhub
, "user": ...   # the user ID as given in /arrive
, "location": ... # where you want to look for items
, "depth": ...  # what depth hosted items you want to be informed of
}

The request must include either location or depth but not both.

If given location "inventory", lists the items the user is carrying.

If given any other location, lists the items that were previously placed there using /transfer.

If given a depth, lists items owned by other domains but hosted in your domain that have that depth.

The response is a list of integer item identifiers. All of the identifiers will be ones that were the id field of an object provided in an earlier /arrive request.

5 About command processing

5.1 Command standardization

The web front-end will edit what the user types as follows:

  1. It will make the text lower-case, remove any non-ASCII characters, and convert to a single space between words.

    For example, Open the door becomes open the door.

  2. If the first word is a prefix of a single known verb, it will expand it to be that verb.

    For example t key becomes take key because take is the only in-game verb that starts with t.

  3. All articles3 Articles: a, an, the and prepositions4 Prepositions: about, above, across, after, against, among, around, at, before, behind, below, beside, between, by, during, for, from, in, inside, into, near, of, off, on, out, over, through, to, toward, under, with, aboard, along, amid, as, beneath, beyond, but, concerning, considering, despite, except, following, like, minus, next, onto, opposite, outside, past, per, plus, regarding, round, save, since, than, till, underneath, unlike, until, upon, versus, via, within, without are removed.

    For example, use the key in the lock of the door and use the key to lock the door both become use key lock door.

  4. A list of alternative phrasings will be replaced with canonical form.

    For example, west becomes go west and take inventory becomes inventory.

  5. If the verb is journey, drop, inventory, score, or region, the message is directed to the hub server.

    Otherwise, the message is directed to the current domain server.

The final value sent to /command will be a list of words. for example, if the user types Open the door, /command will be sent ["open", "door"].

5.2 General-purpose commands

Many commands will be specific to the locations you are creating, but some are more-or-less universal:

look
Displays the description of the user’s current location. Also displays any items that are visible in this location.
look item

Displays the description of that item if it is in the user’s inventory. You may also display that description if the item is in the same location as the user.

For this and other item commands, you must support identifying items by name; we recommend, but do not require, also supporting identifying items by ID.

take item
If the item is in the same location as the user, moves it into the user’s inventory.
drop item
If the item is in the user’s inventory, moves into the same location as the user. This command is handled by the hub, aided by the domain’s /dropped endpoint.
go direction

Moves the user in that direction within the domain, if possible.

If the entered location is one the user has not been to before, displays the description of the new location. If the entered location is one the user has been to before, may either displays the description of the new location or just its name, at your preference. Either way, also displays any items that are visible in this location.

verb item
If the user has that item in their inventory and verb is in the item’s verb field, displays the value of that verb of that item.
inventory
Handled by the hub; shows what the user is carrying.
journey direction
Handled by the hub; changes what domain the user is in.

6 Extra Provided Code

Because this game will involve several computers sending many messages to one another, both server-to-server and browser-to-server, we provide a few extra pieces to make that operate well.

6.1 Shared client

Your domain will send many requests to the same server (the central hub); if handled naively that will result in most of your code’s runtime being spent creating TCP connections. To instead re-use the same connection for many requests, we use aiohttp’s on_startup and on_shutdown to provide a single ClientSession you can reuse for each request.

This means that your requests will look like this:

@routes.post('/example')
async def example(req: Request) -> Response:
    ...
    async with req.app.client.post(url, json={'cs':340,'ex':'ample'}) as response:
          data = await response.json()
          ...
    ...
    return web.json_response(status=500, data={"example":"this code is not done"})

Do not make your own ClientSession; only use the one in app.client.

6.2 CORS

Cross-Origin Resource Sharing, or CORS as it is usually abbreviated, is a set of protocols and rules to guard against certain types of malicious behavior online. Because this game will have a browser visit a page hosted by one server and from that browser send requests to another page, we will run into the limitations of CORS.

Rather than explain the details of this concept, we will simply have the domain servers proclaim that they are willing to be contacted even in ways that CORS protections would usually prevent. We do this with what aiohttp calls a middleware, written and provided by us for you. Its presence should have no impact on how you code.

6.3 Strings to use

For simplicity of testing, we provide a set of strings to use as responses in various situations. You must use these strings as-is in those situations to get full credit on this MP.

These strings are a minimum set. You are welcome to make you domain more forgiving (for example, you could accept computer as a synonym for monitor), more expressive (for example, you could add a use bench action in the foyer), or more expansive (for example, you could add other locations, items, and the like).

7 Testing Your Application

A frontend has been provided for you to test your web application.

To run this front-end, you need to run the hub and your domain. We’ve provided a simple make start script to do this for you. It will print out two URLs, one to vaagicate to in your browser and the other to enter into the text box that shows up on that webpage.

We also have a pytest test suite. It will be used for grading, but it is not fully complete, intentionally only testing a few common sequences of commands. The project will have a more robust set of tests.