From 0161b78a416b4499dea4ffa8286ff9f2766abf06 Mon Sep 17 00:00:00 2001 From: Igor Chernakov Date: Tue, 29 Sep 2026 14:29:05 +0200 Subject: [PATCH] Prose links, GO BACK, and descriptions that withhold what must be examined Stories mark tappable words Twine-style ([[label]] or [[label->target]]); hosts that set PROSE_LINKS receive the markup, all others the plain label. GO BACK retraces the last step for every story, from the bootstrap. Blackwood Horror no longer quotes the drawing, the vial or the ledger in a room description, and its cabinet lists its contents only once opened. Co-Authored-By: Claude Fable 5.1 --- Makefile | 1 + books/blackwood-horror/actions.zil | 37 ++++++++-- books/blackwood-horror/dungeon.zil | 23 ++++--- .../test/test-prose-reveal.zil | 35 ++++++++++ docs/WRITING_ADVENTURES.md | 39 +++++++++++ zilscript/bootstrap.lua | 69 ++++++++++++++++++- 6 files changed, 185 insertions(+), 19 deletions(-) create mode 100644 books/blackwood-horror/test/test-prose-reveal.zil diff --git a/Makefile b/Makefile index ecb5382..25ba1f6 100644 --- a/Makefile +++ b/Makefile @@ -197,6 +197,7 @@ test-horror-playtest-regressions: @lua5.4 run-zil-test.lua books/blackwood-horror/test/test-playtest-scenery @lua5.4 run-zil-test.lua books/blackwood-horror/test/test-playtest-lore @lua5.4 run-zil-test.lua books/blackwood-horror/test/test-playtest-systems + @lua5.4 run-zil-test.lua books/blackwood-horror/test/test-prose-reveal test-horror-all: test-horror-helpers test-horror-partial test-horror-failures test-horror-playtest-regressions test-horror @echo "All horror tests completed!" diff --git a/books/blackwood-horror/actions.zil b/books/blackwood-horror/actions.zil index 49a2a56..3637e70 100644 --- a/books/blackwood-horror/actions.zil +++ b/books/blackwood-horror/actions.zil @@ -27,8 +27,8 @@ ) (T - )> - )>> + door]] sealed with [[chains]] blocks further passage.">)> + )>> @@ -140,15 +140,42 @@ )>> +"The cabinet keeps its contents to itself until the player looks: its door is +ajar, so examining it swings the door wide, as opening it does." + + + + cabinet]] stands in the shadows, its door hanging wide." CR> + + )>) + (T + cabinet]] stands in the shadows, its door slightly ajar." CR>)> + )>> + - + + > + + ) + ( + + ) + (T + + + )> > - + )> > )> )>)> + ) + ( + > + + )>> gate]] of the abandoned sanitarium stand open, their bars red with rust. The structure looms against the darkening sky, its windows like hollow eye sockets. A gravel path leads [[north]] to the entrance.") (NORTH TO SANITARIUM-ENTRANCE) (FLAGS RLANDBIT ONBIT) (GLOBAL SANITARIUM-BUILDING DEAD-OAK-TREE)> @@ -49,7 +49,7 @@ staircase]] ascends to darkness in the east. To the [[west]], a doorway leads to what might have been a reception area. A narrow staircase descends [[into the basement->down]].") (SOUTH TO SANITARIUM-GATE) (WEST TO RECEPTION-ROOM) (NORTH TO OPERATING-THEATER) @@ -245,7 +245,7 @@ (SYNONYM PLAQUE BRASS SIGN) (ADJECTIVE BRASS CORRODED) (DESC "brass plaque") - (LDESC "A corroded brass plaque hangs askew on the gate.") + (LDESC "A corroded [[brass plaque->plaque]] hangs askew on the gate.") (FLAGS READBIT) (TEXT "The plaque reads: 'Blackwood Sanitarium - Est. 1898 - Closed by Order 1952'") (SIZE 5) @@ -256,7 +256,7 @@ (SYNONYM DOOR) (ADJECTIVE HALF-OPEN THEATER OPERATING) (DESC "door to the operating theater") - (LDESC "To the north, a door stands half-open, revealing an operating theater beyond.") + (LDESC "To the [[north]], a [[door]] stands half-open, revealing an operating theater beyond.") (ACTION THEATER-DOOR-F)> key]] lies among the scattered papers on the floor, cold to the touch.") (FLAGS TAKEBIT) (SIZE 2) (TEXT "A small brass key with the number '3' engraved on its head. It's ice cold despite being indoors.")> @@ -296,7 +296,7 @@ (SYNONYM TABLE) (ADJECTIVE OPERATING STAINED) (DESC "operating table") - (FDESC "Tiers of wooden benches circle a central operating table. Cold metal trays sit abandoned on carts. A single overhead lamp, long dead, still points down at the table like an accusation.") + (FDESC "Tiers of wooden benches circle a central [[operating table->table]]. Cold metal trays sit abandoned on carts. A single overhead lamp, long dead, still points down at the table like an accusation.") (LDESC "A stained operating table dominates the center of the room.") (FLAGS SURFACEBIT CONTBIT OPENBIT) (TEXT "The operating table is covered in dark brown stains that you hope are just rust. Leather restraints dangle from all four corners. Deep gouges mar the metal surface, as if someone struggled violently against the bindings.")> @@ -306,8 +306,9 @@ (SYNONYM CABINET) (ADJECTIVE METAL MEDICAL) (DESC "metal cabinet") - (LDESC "A metal cabinet stands in the shadows, its door slightly ajar.") - (FLAGS CONTBIT OPENBIT TRANSBIT) + (DESCFCN CABINET-DESC-F) + (FLAGS CONTBIT) + (CAPACITY 10) (ACTION CABINET-F)> @@ -400,7 +401,7 @@ (ADJECTIVE STRANGE GLOWING) (DESC "vial of serum") (FDESC "Inside the refrigerated drawer, a glass vial glows with an eerie luminescence.") - (LDESC "A glass vial containing luminescent liquid. The label reads 'Compound 237 - DO NOT USE'") + (LDESC "A glass vial of luminescent liquid, its label too small to read at a glance.") (FLAGS TAKEBIT) (SIZE 4) (ACTION SERUM-F)> @@ -1058,7 +1059,7 @@ (SYNONYM LEDGER BOOK) (ADJECTIVE PATIENT LEATHER) (DESC "patient ledger") - (LDESC "A leather-bound ledger with names and dates. The final entry reads: 'Patient 237 - Treatment discontinued. Subject expired during procedure. Dr. Mordecai.'") + (LDESC "A leather-bound ledger, swollen with damp, lies closed.") (FLAGS READBIT TAKEBIT) (TEXT "Patient 237 - Treatment discontinued. Subject expired during procedure. Dr. Mordecai.") (SIZE 8) diff --git a/books/blackwood-horror/test/test-prose-reveal.zil b/books/blackwood-horror/test/test-prose-reveal.zil new file mode 100644 index 0000000..3ce07ae --- /dev/null +++ b/books/blackwood-horror/test/test-prose-reveal.zil @@ -0,0 +1,35 @@ + + +> + +;"What is written on a thing, or shut inside one, is learned by examining it + (Zork I prints the leaflet only when it is read, and lists the mailbox's + contents only once it is open). GO BACK retraces the last step." + + <==? ,HERE ,SANITARIUM-GATE>> + > + > + > + <==? ,HERE ,SANITARIUM-ENTRANCE>> + <==? ,HERE ,SANITARIUM-GATE>> + <==? ,HERE ,SANITARIUM-ENTRANCE>> + + <==? ,HERE ,OPERATING-THEATER>> + > + > + > + <==? ,METAL-CABINET>> + > + > + > + <==? ,ADVENTURER>> + > + > + > + + <==? ,HERE ,SANITARIUM-ENTRANCE>> + <==? ,HERE ,PATIENT-WARD>> + > + > + >> diff --git a/docs/WRITING_ADVENTURES.md b/docs/WRITING_ADVENTURES.md index bbb50ea..11c7338 100644 --- a/docs/WRITING_ADVENTURES.md +++ b/docs/WRITING_ADVENTURES.md @@ -647,6 +647,45 @@ Indicators: - `N ,var` — print a number - `A ,obj` — print with article ("a"/"an") +### Prose Links (Tappable Words) + +Mark the words a reader may act on with double brackets, as Twine does: + +```zil +(LDESC "A corroded [[brass plaque->plaque]] hangs askew on the gate.") + +``` + +- `[[label]]` — the label is both what is printed and what the parser is + given. Use it when the prose already ends in the object's noun + (`[[crayon drawing]]`) or names a direction (`[[north]]`). +- `[[label->target]]` — prints the label; the target is the parser's word + for the object (`[[iron gates->gate]]`) or a direction + (`[[into the basement->down]]`). + +A host that renders links (AdventureArena underlines them with a dashed +rule and offers the object's verbs on tap) sets `PROSE_LINKS` before the +bootstrap runs and receives the markup. Every other host, and every +transcript test, reads the plain label, so `ASSERT-TEXT` needs no change. + +Keep a link inside one string, and never put one in a room's `DESC`: the +room name is matched exactly. Link what the player can act on now — do not +link an object the description is about to withhold. + +### Withholding What Must Be Examined + +A description says what a glance takes in. What is written on a thing, or +shut inside one, is learned by examining, reading or opening it — Zork I +prints the leaflet only when it is read and lists the mailbox's contents +only once it is open. Do not quote a note, a label or a drawing in `FDESC` +or `LDESC`, and do not give a closed container `OPENBIT` or `TRANSBIT`. + +### Going Back + +`GO BACK` (also `WALK BACK`, `BACK`) retraces the player's last step. The +bootstrap handles it for every story by walking the exit that leads to the +room the player came from; one-way passages have no way back. + ### Object Movement ```zil diff --git a/zilscript/bootstrap.lua b/zilscript/bootstrap.lua index 03e2418..98cd466 100644 --- a/zilscript/bootstrap.lua +++ b/zilscript/bootstrap.lua @@ -909,14 +909,30 @@ function ZIL_UNWRAP_RETURN(value) return value end +-- Prose links. A story marks the words a reader may act on, Twine-style: +-- "[[crayon drawing]]", or "[[heavy door->door]]" when the parser's word for +-- the thing differs from the prose. A host that renders links sets +-- PROSE_LINKS and receives the markup; every other host, and every +-- transcript test, reads the plain label. +local function prose_links(text) + if rawget(_G, "PROSE_LINKS") or not text:find("[[", 1, true) then return text end + return (text:gsub("%[%[(.-)%]%]", function(link) + return link:match("^(.-)%->") or link + end)) +end + local function io_write(...) -- Check if io_write was overridden globally (for tests) if _G.io_write then - return _G.io_write(...) + local parts = table.pack(...) + for i = 1, parts.n do + if type(parts[i]) == "string" then parts[i] = prose_links(parts[i]) end + end + return _G.io_write(table.unpack(parts, 1, parts.n)) end -- Default: buffered mode for coroutine-based games for i = 1, select("#", ...) do - table.insert(output_buffer, tostring(select(i, ...))) + table.insert(output_buffer, prose_links(tostring(select(i, ...)))) end end @@ -1100,8 +1116,47 @@ local function route_response(input) return false end +-- "Go back" retraces the last step. The rooms are numbers in globals, so +-- they are saved, restored and restarted with the rest of the story. +RETRACE_HERE = RETRACE_HERE or 0 +RETRACE_FROM = RETRACE_FROM or 0 + +local RETRACE_VERBS = { go = true, walk = true, run = true, head = true, step = true, turn = true } + +local function remember_room() + local here = rawget(_G, "HERE") + if type(here) ~= "number" or here == RETRACE_HERE then return end + RETRACE_FROM, RETRACE_HERE = RETRACE_HERE, here +end + +-- The direction that leads to the room the player came from, as a command. +-- One-way passages and routines that decide an exit as it is walked have no +-- way back. Returns nil and what to tell the player when there is none; a +-- bare "back" is then left to the story, which may have its own verb. +local function retrace(input) + local verb, rest = input:lower():match("^%s*(%a+)%s+(%a+)[%s%.!]*$") + local bare = input:lower():match("^%s*back[%s%.!]*$") ~= nil + if not bare and not (RETRACE_VERBS[verb] and rest == "back") then return input end + local here = rawget(_G, "HERE") + if type(here) == "number" and RETRACE_FROM ~= 0 then + local names = {} + for name in pairs(_DIRECTIONS) do table.insert(names, name) end + table.sort(names) + for _, name in ipairs(names) do + local pp = GETPT(here, _DIRECTIONS[name]) + local size = pp and PTSIZE(pp) + if (size == 1 or size == 4 or size == 5) and GETB(pp, REXIT) == RETRACE_FROM then + return name:lower() + end + end + end + if bare then return input end + return nil, "You can't retrace your steps from here.\n" +end + -- Modified READ to yield with output function READ(inbuf, parse) + remember_room() -- for k, v in pairs(_G) do -- if type(v) == 'boolean' then print(k, type(v)) end -- end @@ -1118,7 +1173,15 @@ function READ(inbuf, parse) if not s then os.exit(0) end - + if type(s) == "string" then + local command, refusal = retrace(s) + if not command then + s = coroutine.yield(refusal) + goto restart_read + end + s = command + end + local p = {} for pos, word in s:gmatch("()(%S+)") do -- Z-machine truncates dictionary words to 6 characters