Lighthouse: Text Adventure Engine
EasyAI-assisted coding · AI-Assisted Feature Work
Lighthouse: Text Adventure Engine
You've joined the small team behind Lighthouse, a text adventure that ships inside a mobile reading app. The engine parses what the player types, runs it against a five-room world, and replies with a line of text. Players have filed two bugs, and the mobile team needs a carry limit and save games before the next release.
Codebase
adventure/config.py: start room, carry limit, verb synonyms, direction words, exit order.adventure/world.py: static rooms, exits, items (with weights) and the locked trapdoor. Known-good data.adventure/parser.py: turns raw input into aCommand(verb, noun).adventure/state.py:GameState(current room, inventory, items per room, flags) andfind_item.adventure/commands.py: one handler per verb. Each returns the reply text.adventure/engine.py:Game, which parses input and routes it to a handler.adventure/persistence.py: save/load stubs (Part 3).adventure/api.py: therun(operations)entrypoint.tests/: existing unit tests.test_case_insensitiveandtest_take_twicefail today.
How your code is run
run(operations: list[list]) -> list starts one new game and returns exactly one output per operation.
| Operation | Arguments | Returns |
|---|---|---|
cmd | text (what the player typed) | the reply string |
save | none | a save dict (Part 3), "error: not supported" today |
load | data (a save dict) | "ok" or "error: <reason>" (Part 3), "error: not supported" today |
Input rules. Commands are case-insensitive. Extra spaces are ignored, and the words the, a, an are dropped wherever they appear. The noun is whatever is left after the verb, in lowercase, single-spaced. An item is referred to by its name or one of its aliases (world.py).
Replies (<name> is the item's name from world.py, <noun> is the noun as normalized):
| Input | Reply |
|---|---|
look, l | <Title>. <description> Items: <names, in room order, or none>. Exits: <directions in order north, south, east, west, up, down>. |
go <dir>, <dir>, n s e w u d | the new room's look text; You can't go that way.; The trapdoor is locked.; Go where? with no direction |
take <item>, get <item> | Taken: <name>.; There is no <noun> here.; The <name> is fixed in place.; Take what? |
take all | Taken: <names in room order>. (fixed items are skipped); There is nothing to take. |
drop <item> | Dropped: <name>. (the item goes to the end of the room's list); You aren't carrying any <noun>.; Drop what? |
inventory, inv, i | You are carrying: <names in pickup order>. or You are carrying nothing. |
examine <item>, x <item> | the item's description (room or inventory); You don't see any <noun> here.; Examine what? |
unlock trapdoor | You unlock the trapdoor.; You need a key. (no brass key held); It's already unlocked.; You can't unlock that. (any other noun, or not in the Lighthouse) |
| anything else | I don't understand that. |
Worked example
[["cmd", "look"], ["cmd", "go north"], ["cmd", "get the lamp"], ["cmd", "examine lantern"]]returns
["Beach. Waves fold over grey sand. Items: shell, driftwood. Exits: north.",
"Cliff Path. A narrow path climbs the cliff. Items: lantern, rope. Exits: north, south.",
"Taken: lantern.",
"A storm lantern with a little oil left."]Part 1: Player bug reports
Goal: fix both reports.
- "Typing
TAKE Lanternsays *I don't understand that.*" Every rule above must work regardless of letter case, for verbs, directions, articles and item names. Replies that echo<noun>use the lowercase form (take ROPEon the beach repliesThere is no rope here.). - "Taking something I already have gives me a second copy."
take <item>for an item you are already carrying must replyYou already have the <name>.and change nothing. This check comes before the fixed-in-place check.
Part 2: Carry limit
Goal: enforce config.MAX_CARRY_WEIGHT (10) using the item weights in world.py.
- You may hold items whose weights add up to at most 10 (exactly 10 is allowed).
take <item>that would go over the limit repliesThe <name> is too heavy.and changes nothing. Check order fortake <item>: missing noun, not found, already carried, fixed in place, too heavy.take allgoes through the room's items in order, taking each one that still fits and skipping fixed items. The reply isTaken: <names>.if anything was taken, followed by (after one space)Too heavy: <names>.if anything didn't fit. If neither applies:There is nothing to take.inventorynow ends with the carried weight:You are carrying: lantern, rope (5/10).andYou are carrying nothing (0/10).
Example (in the Lighthouse, carrying nothing): take all replies Taken: brass key, logbook. Too heavy: anvil.
Part 3: Save and load
Goal: implement persistence.to_save and persistence.from_save so save and load work.
savereturns a dict:{"version": 1, "room": <room id>, "inventory": [<item ids in pickup order>], "rooms": {<every room id>: [<item ids in room order>]}, "flags": {"trapdoor_unlocked": <bool>}}. Ids are the keys ofROOMSandITEMSinworld.py(for example"brass_key","lamp_room").- A save is a snapshot: commands run after
savemust not change a dict it already returned. loadreplaces the whole game with the save's state and returns"ok". Commands after it carry on from that state, and saving straight away returns an equal dict.loadvalidates in this order and returns the first error, leaving the current game untouched:versionis not1(or the save isn't a dict):"error: unsupported version"roomisn't a room id:"error: unknown room '<room>'"inventoryisn't a list,roomsdoesn't have exactly the five room ids with a list for each, any id is unknown, any item appears zero or several times overall, or a fixed item is in the inventory:"error: corrupt save"- inventory weight is over the carry limit:
"error: inventory too heavy" - Older saves (written before the trapdoor shipped) have no
flagskey. Treat a missing key, or a missing flag, asfalse.
Notes
world.pyandconfig.pyare known-good. The bugs are in the code that uses them.- Use the AI assistant freely, but check its changes against the reply table above.
- Later parts must not break earlier ones. Hidden tests re-check earlier behaviour.
Example 1
- Input:
- [["cmd", "TAKE Shell"], ["cmd", "take shell"], ["cmd", "drop shell"]]
- Output:
- ["Taken: shell.", "You already have the shell.", "Dropped: shell."]
- Explanation
- Part 1: commands are case-insensitive, and taking something you already carry changes nothing.
Example 2
- Input:
- [["cmd", "n"], ["cmd", "n"], ["cmd", "take all"], ["cmd", "inventory"]]
- Output:
- ["Cliff Path. A narrow path climbs the cliff. Items: lantern, rope. Exits: north, south.", "Lighthouse. A cold round room at the foot of a spiral stair. A trapdoor is set in the floor. Items: brass key, logbook, anvil. Exits: south, up, down.", "Taken: brass key, logbook. Too heavy: anvil.", "You are carrying: brass key, logbook (3/10)."]
- Explanation
- Part 2: the anvil weighs 12, over the limit of 10, so take all skips it and says so.
Example 3
- Input:
- [["cmd", "take shell"], ["save"], ["cmd", "drop shell"], ["load", "<the saved dict>"], ["cmd", "inventory"]]
- Output:
- ["Taken: shell.", "<save dict: room beach, inventory [shell], beach holds [driftwood]>", "Dropped: shell.", "ok", "You are carrying: shell (1/10)."]
- Explanation
- Part 3: a save is a snapshot, so it still describes the moment it was taken after later commands.
Guidelines
- Python 3 standard library only
- Commands are case-insensitive; replies echo nouns in lowercase
- Item weights are small integers; the carry limit is 10 and exactly 10 is allowed
- Saves are plain JSON-serializable dicts
- Every operation returns exactly one value; errors are "error: ..." strings