From aacfcdcb358bd4ace529fce5633fa7db8fd8d73c Mon Sep 17 00:00:00 2001 From: Igor Chernakov Date: Mon, 13 Jul 2026 21:34:49 +0200 Subject: [PATCH 01/37] Replace JSON output with plain text + exit codes in llm.lua MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor llm.lua to output plain text directly to stdout instead of JSON, using exit codes (0 = success, 1 = error) for status signaling. Updates PLAYING.md documentation to reflect new output format and command usage patterns. Adds support for limehouse-killings and blackwood-horror games. Includes minor ZIL fixes (CR→CRLF) and removes redundant bootstrap print statement. --- PLAYING.md | 50 +++++++++------------- books/limehouse-killings/actions.zil | 4 +- books/limehouse-killings/dungeon.zil | 3 +- limehouse.sav.actions | 4 ++ llm.lua | 63 +++++++--------------------- zilscript/bootstrap.lua | 1 - 6 files changed, 43 insertions(+), 82 deletions(-) create mode 100644 limehouse.sav.actions diff --git a/PLAYING.md b/PLAYING.md index fb3e4f1..fdb04a0 100644 --- a/PLAYING.md +++ b/PLAYING.md @@ -23,23 +23,15 @@ lua5.4 llm.lua --action "read leaflet" --save zork1.sav ## Output -Each command returns JSON: - -```json -{ - "ok": true, - "output": "Opening the small mailbox reveals a leaflet.\n", - "room": "West of House", - "savefile": "zork1.sav", - "historyfile": "zork1.sav.actions", - "restored": true -} +Each command returns the game's text output directly (plain text, no JSON): + +``` +Ashworth Manor Gate +The iron gates of Ashworth Manor loom before you... ``` -- `ok` — whether the command executed without engine error -- `output` — the game's text response (ANSI stripped) -- `room` — current room name (if available) -- `restored` — whether state was restored from a previous save +- **Exit code 0** — command executed successfully +- **Exit code 1** — error occurred (stderr shows `ERROR: ...`) ## How State Continuity Works @@ -60,12 +52,8 @@ lua5.4 llm.lua --new-game --save "$SAVE" # Play turns for i in $(seq 1 10); do - # Choose an action based on current game state - RESPONSE=$(lua5.4 llm.lua --action "look" --save "$SAVE") - echo "$RESPONSE" | jq -r '.output' - - # Decide next action, then execute - lua5.4 llm.lua --action "go north" --save "$SAVE" + # Execute a command - output is plain text, exit code 0=success, 1=error + lua5.4 llm.lua --action "look" --save "$SAVE" done ``` @@ -74,7 +62,7 @@ done An agent can play the game in a loop: 1. **Start**: `lua5.4 llm.lua --new-game --save game.sav` -2. **Observe**: Parse the `output` field from the JSON response +2. **Observe**: Read the plain text output 3. **Decide**: Choose the next command based on game state 4. **Act**: `lua5.4 llm.lua --action "" --save game.sav` 5. **Repeat** from step 2 @@ -98,6 +86,8 @@ Pass `--game ` to play different games: | zork1 (default) | `infocom.zork1.zork1` | | lurkinghorror | `infocom.lurkinghorror.h1` | | spellbreaker | `infocom.spellbreaker.z6` | +| limehouse-killings | `books.limehouse-killings.limehouse-killings` | +| blackwood-horror | `books.blackwood-horror.blackwood-horror` | ## Command-Line Reference @@ -110,6 +100,9 @@ Options: --game, -g name Game module (default: zork1) --new-game Start fresh, ignoring existing save --help, -h Show help + +Output: plain text to stdout +Exit codes: 0 = success, 1 = error ``` ## Evaluating Playability @@ -126,19 +119,16 @@ Example evaluation script: ```bash SAVE="eval.sav" -lua5.4 llm.lua --new-game --save "$SAVE" | jq -r '.output' > eval.log +lua5.4 llm.lua --new-game --save "$SAVE" > eval.log for turn in $(seq 1 50); do # In practice, an LLM would decide the action here ACTION="look" - RESULT=$(lua5.4 llm.lua --action "$ACTION" --save "$SAVE") - OUTPUT=$(echo "$RESULT" | jq -r '.output') - ROOM=$(echo "$RESULT" | jq -r '.room') - - echo "Turn $turn [$ROOM]: $OUTPUT" >> eval.log + lua5.4 llm.lua --action "$ACTION" --save "$SAVE" >> eval.log + EXIT_CODE=$? - # Check for game end conditions - if echo "$OUTPUT" | grep -qi "game has ended\|you have died\|score is"; then + # Check for game end conditions or errors + if [ $EXIT_CODE -ne 0 ]; then break fi done diff --git a/books/limehouse-killings/actions.zil b/books/limehouse-killings/actions.zil index c834367..8e8b58e 100644 --- a/books/limehouse-killings/actions.zil +++ b/books/limehouse-killings/actions.zil @@ -320,7 +320,7 @@ ) (T )> - )>> + )>> @@ -345,7 +345,7 @@ ) (T )> - )>> + )>> ; --- Global Object Actions --- diff --git a/books/limehouse-killings/dungeon.zil b/books/limehouse-killings/dungeon.zil index 60edbba..7f741af 100644 --- a/books/limehouse-killings/dungeon.zil +++ b/books/limehouse-killings/dungeon.zil @@ -521,5 +521,4 @@ (IN ASHWORTH-MANOR-GATE) (DESC "you") (SYNONYM DETECTIVE YOU) - (FLAGS ACTORBIT) - (ACTION PLAYER-F)> + (FLAGS NDESCBIT INVISIBLE ACTORBIT)> diff --git a/limehouse.sav.actions b/limehouse.sav.actions new file mode 100644 index 0000000..c1ee00e --- /dev/null +++ b/limehouse.sav.actions @@ -0,0 +1,4 @@ +{"time":1783969734,"game":"limehouse-killings","action":"go north"} +{"time":1783969735,"game":"limehouse-killings","action":"inventory"} +{"time":1783969736,"game":"limehouse-killings","action":"take all"} +{"time":1783969737,"game":"limehouse-killings","action":"examine glass"} diff --git a/llm.lua b/llm.lua index 8193247..0f7e80a 100755 --- a/llm.lua +++ b/llm.lua @@ -84,14 +84,22 @@ local GAMES = { }, spellbreaker = { modules = {"infocom.spellbreaker.z6"} + }, + ["limehouse-killings"] = { + modules = {"books.limehouse-killings.limehouse-killings"} + }, + ["blackwood-horror"] = { + modules = {"books.blackwood-horror.blackwood-horror"} } } local game_name = args.game or "zork1" local game_config = GAMES[game_name] if not game_config then + local available = {} + for k in pairs(GAMES or {}) do table.insert(available, k) end io.write(string.format('{"ok":false,"error":"Unknown game: %s. Available: %s"}\n', - game_name, table.concat(table.keys(GAMES or {}), ", "))) + game_name, table.concat(available, ", "))) os.exit(1) end @@ -213,7 +221,7 @@ local restore local function write_error_and_exit(message) restore() message = tostring(message):gsub('\27%[[0-9;]*m', '') - io.write(string.format('{"ok":false,"error":%s}\n', json_escape(message))) + io.write("ERROR: " .. message .. "\n") os.exit(1) end @@ -405,37 +413,9 @@ if args.action then restore() - -- Output JSON - local response = { - ok = true, - output = output, - room = room, - savefile = savefile, - historyfile = historyfile, - restored = resumed_from_dump or resumed_from_history, - } - if save_err then - response.save_error = save_err - end - if history_err then - response.history_error = history_err - end - - -- Simple JSON serialization - io.write("{") - io.write('"ok":' .. tostring(response.ok) .. ',') - io.write('"output":' .. json_escape(response.output) .. ',') - io.write('"room":' .. json_escape(response.room) .. ',') - io.write('"savefile":' .. json_escape(response.savefile) .. ',') - io.write('"historyfile":' .. json_escape(response.historyfile) .. ',') - io.write('"restored":' .. tostring(response.restored)) - if response.save_error then - io.write(',"save_error":' .. json_escape(response.save_error)) - end - if response.history_error then - io.write(',"history_error":' .. json_escape(response.history_error)) - end - io.write("}\n") + -- Output plain text (exit 0 = success) + io.write(output) + os.exit(0) elseif args.new_game then -- Start a new game and get initial output @@ -469,20 +449,9 @@ elseif args.new_game then restore() - io.write("{") - io.write('"ok":true,') - io.write('"output":' .. json_escape(output) .. ',') - io.write('"room":' .. json_escape(room) .. ',') - io.write('"savefile":' .. json_escape(savefile) .. ',') - io.write('"historyfile":' .. json_escape(historyfile) .. ',') - io.write('"new_game":true') - if save_err then - io.write(',"save_error":' .. json_escape(save_err)) - end - if history_err then - io.write(',"history_error":' .. json_escape(history_err)) - end - io.write("}\n") + -- Output plain text (exit 0 = success) + io.write(output) + os.exit(0) else write_error_and_exit("No action specified. Use --action or --new-game") diff --git a/zilscript/bootstrap.lua b/zilscript/bootstrap.lua index fcd6304..6e653bf 100644 --- a/zilscript/bootstrap.lua +++ b/zilscript/bootstrap.lua @@ -1664,4 +1664,3 @@ end _LLM_RESTORED = false -- === Done === -print("ZIL runtime initialized.") From 2b833e00d71950149e8eee7f5f3459db42de1d3b Mon Sep 17 00:00:00 2001 From: Igor Chernakov Date: Mon, 13 Jul 2026 22:04:43 +0200 Subject: [PATCH 02/37] Preserve vocabulary hyphens; add Limehouse walkthrough Make parser vocabulary robust and add a Limehouse golden-path test. Key changes: - Compiler: preserve exact ZIL word spelling (hyphens) for SYNONYM/ADJECTIVE output (zilscript/compiler/fields.lua) and add unit test. - Bootstrap: restore saved vocabulary/address map for SAVE v2, fix TELL numeric printing and PTSIZE nil guard (zilscript/bootstrap.lua). - Limehouse content: parser-friendly fixes (synonyms/adjectives, guarded one-time flags, OPENBIT on containers, cipher-book puzzle, ASK/TELL topics, ACCUSE syntax, exits). - Tests/Makefile: add tests/test_limehouse_walkthrough.lua, integrate target, and adjust LLM tests. - Docs: strengthen play-as-you-build, vocabulary, and testing guidance in skills/. --- Makefile | 9 +- books/limehouse-killings/actions.zil | 113 +++++++++++-- books/limehouse-killings/dungeon.zil | 152 ++++++++++++++++-- .../02_working_materials_and_design_docs.md | 9 ++ .../03_world_model_and_puzzle_architecture.md | 10 ++ skills/04_content_writing_and_npc_layer.md | 4 + skills/05_zil_implementation_reference.md | 107 +++++++++--- .../06_testing_transcripts_and_debugging.md | 38 +++++ skills/07_workflow_subagents_and_hints_ui.md | 3 + skills/08_packaging_checklists_and_release.md | 3 + skills/README.md | 18 ++- tests/test_compiler.lua | 12 ++ tests/test_limehouse_walkthrough.lua | 70 ++++++++ tests/test_llm.lua | 90 ++++------- zilscript/bootstrap.lua | 38 +++++ zilscript/compiler/fields.lua | 12 +- 16 files changed, 571 insertions(+), 117 deletions(-) create mode 100644 tests/test_limehouse_walkthrough.lua diff --git a/Makefile b/Makefile index ee72313..c258ffc 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # Targets -.PHONY: test test-all test-unit test-integration test-game-startup test-zork1 test-zork2 test-parser test-containers test-directions test-light test-pronouns test-take test-turnbit test-clock test-clock-direct test-assertions test-check-commands test-read-mailbox test-walk-around-house test-horror-helpers test-horror-partial test-horror test-horror-failures test-horror-all test-pure-zil test-simple-new test-insert-file test-let test-save test-llm help llm-new llm-look test-zilch test-flow-control +.PHONY: test test-all test-unit test-integration test-game-startup test-zork1 test-zork2 test-parser test-containers test-directions test-light test-pronouns test-take test-turnbit test-clock test-clock-direct test-assertions test-check-commands test-read-mailbox test-walk-around-house test-horror-helpers test-horror-partial test-horror test-horror-failures test-horror-all test-limehouse-walkthrough test-pure-zil test-simple-new test-insert-file test-let test-save test-llm help llm-new llm-look test-zilch test-flow-control help: @echo "Available targets:" @@ -36,6 +36,7 @@ help: @echo " test-let - Run LET form tests" @echo " test-save - Run save/restore tests" @echo " test-llm - Run LLM persistence tests" + @echo " test-limehouse-walkthrough - Run Limehouse golden-path LLM test" @echo "" @echo "Horror game tests:" @echo " test-horror-helpers - Run horror test helpers" @@ -65,7 +66,7 @@ test-unit: @echo "Running unit tests..." lua tests/run_all.lua -test-integration: test-zork1 test-zork2 test-game-startup test-parser test-horror-all +test-integration: test-zork1 test-zork2 test-game-startup test-parser test-horror-all test-limehouse-walkthrough @echo "All integration tests completed!" test-zork1: @@ -151,6 +152,10 @@ test-llm: @echo "Running LLM persistence tests..." @lua tests/test_llm.lua +test-limehouse-walkthrough: + @echo "Running Limehouse Killings golden-path walkthrough..." + @lua5.4 tests/test_limehouse_walkthrough.lua + test-zilch: @echo "Running ZILCH feature tests..." @lua5.4 run-zil-test.lua zil/test-zilch diff --git a/books/limehouse-killings/actions.zil b/books/limehouse-killings/actions.zil index 8e8b58e..3817cee 100644 --- a/books/limehouse-killings/actions.zil +++ b/books/limehouse-killings/actions.zil @@ -5,19 +5,24 @@ - - > + + + >)> )>> - - > + + + >)> ) ( + + + >)> )>> + ) ( + ) (T @@ -42,8 +49,9 @@ - - > + + + >)> ) ( @@ -58,8 +66,9 @@ - - > + + + >)> )>> ; --- Tool Object Actions --- @@ -126,6 +135,26 @@ )>> + + ) + ( <==? ,CIPHER-STAGE 0>> + + ) + ( <==? ,CIPHER-STAGE 1>> + + ) + ( <==? ,CIPHER-STAGE 2>> + + ) + ( <==? ,CIPHER-STAGE 3>> + ) + (T + + )> + )>> + @@ -147,6 +176,9 @@ + + + >)> ) ( @@ -232,6 +264,7 @@ ) ( + )>> @@ -390,8 +423,27 @@ ) - ( - > + ( + + + ) + ( + + + + >)> + ) + ( + + + + )> + + ) + ( + + ) + ( > ) ( @@ -453,8 +505,18 @@ ) - ( - > + ( + + + ) + ( + + + + + >)> + ) + ( > ) ( @@ -508,8 +570,20 @@ ) - ( - > + ( + + + ) + ( + > + + + + + >)> + + ) + ( > ) ( @@ -574,8 +648,11 @@ ) - ( - > + ( + + + ) + ( > ) ( @@ -912,7 +989,7 @@ - > + <==? ,HERE ,LIBRARY>> @@ -948,6 +1025,8 @@ ; === GAME ENTRY === + + diff --git a/books/limehouse-killings/dungeon.zil b/books/limehouse-killings/dungeon.zil index 7f741af..880a088 100644 --- a/books/limehouse-killings/dungeon.zil +++ b/books/limehouse-killings/dungeon.zil @@ -11,6 +11,7 @@ > > > + > > > @@ -29,6 +30,7 @@ > > > +> ; === ROOMS === @@ -70,6 +72,7 @@ (LDESC "Floor-to-ceiling bookshelves line the walls, their contents ranging from leather-bound classics to modern scientific texts. The fire is cold, but the room retains a scholarly warmth. A doorway leads west back to the entrance hall.") (WEST TO ASHWORTH-ENTRANCE-HALL) (EAST TO SECRET-PASSAGE IF CIPHER-SOLVED) + (SOUTH TO SECRET-PASSAGE IF CIPHER-SOLVED) (FLAGS RLANDBIT ONBIT) (GLOBAL BOOKSHELF READING-DESK FIREPLACE COLORED-MARKERS)> @@ -145,7 +148,8 @@ (DESC "unsent letter") (FDESC "A yellowed envelope lies among the papers on the desk, addressed in a shaking hand.") (LDESC "An unsent letter, its paper yellowed with age. The ink is faded but legible, the words a threat from one man to another. The seal is broken, the wax still bearing the initial 'M.'") - (SYNONYM LETTER NOTE PAPER) + (SYNONYM LETTER NOTE PAPER DEAD-LETTER) + (ADJECTIVE DEAD UNSENT) (FLAGS TAKEBIT READBIT) (ACTION DEAD-LETTER-F)> @@ -154,7 +158,8 @@ (DESC "blood-stained knife") (FDESC "Something glints in the branches near the fountain -- a knife, its blade dark with dried blood.") (LDESC "A sharp blade, its edge stained with dried blood. The handle bears the mark of a surgical instrument, the kind used by doctors and scientists.") - (SYNONYM KNIFE BLADE WEAPON) + (SYNONYM KNIFE BLADE WEAPON BLOOD-STAINED-KNIFE) + (ADJECTIVE BLOOD STAINED) (FLAGS TAKEBIT WEAPONBIT) (ACTION BLOOD-STAINED-KNIFE-F)> @@ -164,7 +169,8 @@ (FDESC "A small locked box sits among the cold ashes in the fireplace, its brass clasp gleaming dully.") (LDESC "A small ornate box, its surface carved with intricate patterns. A keyhole stares up at you, promising secrets within.") (SYNONYM BOX CASE CONTAINER) - (FLAGS CONTBIT) + (ADJECTIVE LOCKED) + (FLAGS CONTBIT SEARCHBIT) (ACTION LOCKED-BOX-F)>