/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.
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
.
You will implement four web API endpoints, all using the POST method:
/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.
/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.
/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.
/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.
/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.
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.
There are three locations in the MP10 domain.
read sign
here.take
and then read
go east
goes to other domains; return "$journey east"
to make that happen.go north
goes to the classroomsouth
goes to the foyer.down
and west
both go to the podium.up
and east
both go to the classroom.use key cabinet
will change it to closed if they have a depth-1 key
owned by this domain but hosted in another domain.use key cabinet
will change it to locked if they have the key
.open cabinet
will change it to open.use switch
to change the switch state between down (its initial state) and upclose cabinet
to change it to closed.tell screen xyzzy
to change it to won stateIn summary, you will need to track
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.
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.
There are three applications of interest to this game.
The Web User Interface (UI) is written in JavaScript and HTML5 and is provided for you.
The Central Hub Server (hub) is written in Python with aiohttp and is provided for you.
The Domain Servers (domain) are written in Python with aiohttp by you.
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.
How do you track where items are? Here are two options:
Track item locations in the domain code.
/arrive
and /dropped
requests from the hub./transfer
/query
Track item locations with the hub.
/transfer
when a new user /arrive
s./arrive
, but their locations are ignored./query
is used to find out where it is./transfer
Option 1 has more code for tracking item state; Option 2 has more code for knowing if a user typing look at paper
2 The UI will turn this into ["look", "paper"]
before sending it to /command
should return the description of the item named paper
or not.
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.
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>')
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.
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.
/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.
/transfer
/transfer
?take
s an item.
/transfer
to?/transfer
appended. The hub URL was given as the request to /newhub
.
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
.
/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.
The web front-end will edit what the user types as follows:
It will make the text lower-case, remove any non-ASCII characters, and convert to a single space between words.
For example,
becomes Open the door
.open the door
If the first word is a prefix of a single known verb, it will expand it to be that verb.
For example
becomes t key
because take key
is the only in-game verb that starts with take
.t
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,
and use the key in the lock of the door
both become use the key to lock the door
.use key lock door
A list of alternative phrasings will be replaced with canonical form.
For example,
becomes west
and go west
becomes take inventory
.inventory
If the verb is
, journey
, drop
, inventory
, or score
, the message is directed to the hub server.region
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"]
.
Many commands will be specific to the locations you are creating, but some are more-or-less universal:
look
look
itemDisplays 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
itemdrop
item/dropped
endpoint.
go
directionMoves 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
field, displays the value of that verb of that item.
inventory
journey
directionBecause 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.
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.
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).
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.