Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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!"
Expand Down
37 changes: 32 additions & 5 deletions books/blackwood-horror/actions.zil
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
<COND (,CHAINS-CUT-FLAG
<TELL " To the north, a heavy door stands open, revealing darkness beyond.">)
(T
<TELL " At the far end, a heavy door sealed with chains blocks further passage.">)>
<TELL CR "A doorway leads west back to the entrance hall." CR>)>>
<TELL " At the far end, a [[heavy door->door]] sealed with [[chains]] blocks further passage.">)>
<TELL CR "A doorway leads [[west]] back to the entrance hall." CR>)>>

<ROUTINE BASEMENT-CORRIDOR-FCN (RARG)
<COND (<EQUAL? .RARG ,M-LOOK>
Expand Down Expand Up @@ -140,15 +140,42 @@
<TELL "A file folder labeled 'Patient 189 - CONFIDENTIAL'. Inside are medical records and notes. 'Subject shows extraordinary resistance to pain. Mental state deteriorating. Recommending transfer to isolation wing. Dr. Mordecai has expressed personal interest in this case. Update: Patient transferred to chapel for experimental treatment. Nov 1, 1952.'" CR>
<RTRUE>)>>

"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."

<ROUTINE CABINET-DESC-F (RARG)
<COND (<EQUAL? .RARG ,M-OBJDESC>
<COND (<FSET? ,METAL-CABINET ,OPENBIT>
<TELL "A [[metal cabinet->cabinet]] stands in the shadows, its door hanging wide." CR>
<COND (<FIRST? ,METAL-CABINET>
<PRINT-CONT ,METAL-CABINET>)>)
(T
<TELL "A [[metal cabinet->cabinet]] stands in the shadows, its door slightly ajar." CR>)>
<RTRUE>)>>

<ROUTINE CABINET-F ()
<COND (<VERB? EXAMINE LOOK-INSIDE>
<TELL "The cabinet's glass doors are cracked but still intact. Inside, you can see various medical instruments">
<COND (<AND <VERB? OPEN>
<FSET? ,METAL-CABINET ,OPENBIT>>
<TELL "The cabinet door already hangs wide." CR>
<RTRUE>)
(<VERB? EXAMINE LOOK-INSIDE OPEN SEARCH>
<COND (<FSET? ,METAL-CABINET ,OPENBIT>
<TELL "The cabinet door hangs wide on stiff hinges. The shelves hold rusted medical instruments">)
(T
<FSET ,METAL-CABINET ,OPENBIT>
<FSET ,METAL-CABINET ,TOUCHBIT>
<TELL "You ease the cabinet door wide on its stiff hinges. The shelves hold rusted medical instruments">)>
<COND (<OR <IN? ,SCALPEL ,METAL-CABINET> <IN? ,ETHER-BOTTLE ,METAL-CABINET>>
<TELL ", including">
<TELL ", and among them">
<COND (<IN? ,SCALPEL ,METAL-CABINET> <TELL " a scalpel">)>
<COND (<AND <IN? ,SCALPEL ,METAL-CABINET> <IN? ,ETHER-BOTTLE ,METAL-CABINET>> <TELL " and">)>
<COND (<IN? ,ETHER-BOTTLE ,METAL-CABINET> <TELL " a bottle">)>)>
<TELL "." CR>
<RTRUE>)
(<AND <VERB? CLOSE>
<FSET? ,METAL-CABINET ,OPENBIT>>
<FCLEAR ,METAL-CABINET ,OPENBIT>
<TELL "The door swings back on its warped frame and comes to rest slightly ajar." CR>
<RTRUE>)>>

<ROUTINE HEAVYDOOR-F ()
Expand Down
23 changes: 12 additions & 11 deletions books/blackwood-horror/dungeon.zil
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,15 @@
<ROOM SANITARIUM-GATE
(IN ROOMS)
(DESC "Sanitarium Gate")
(LDESC "The rusted iron gates 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.")
(LDESC "The rusted [[iron gates->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)>

<ROOM SANITARIUM-ENTRANCE
(IN ROOMS)
(DESC "Sanitarium Entrance Hall")
(LDESC "The entrance hall reeks of mildew and decay. A grand 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.")
(LDESC "The entrance hall reeks of mildew and decay. A [[grand staircase->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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)>

<OBJECT OAK-DESK
Expand Down Expand Up @@ -286,7 +286,7 @@
(SYNONYM KEY)
(ADJECTIVE BRASS SMALL)
(DESC "brass key")
(LDESC "A small brass key lies among the scattered papers on the floor, cold to the touch.")
(LDESC "A small [[brass key->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.")>
Expand All @@ -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.")>
Expand All @@ -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)>

<OBJECT SCALPEL
Expand Down Expand Up @@ -335,7 +336,7 @@
(SYNONYM BEDS FRAMES BED FRAME)
(ADJECTIVE RUSTED)
(DESC "bed frames")
(FDESC "Dozens of bed frames line the walls. Most mattresses have rotted away, leaving only rusted springs. Among the debris, you notice a child's crayon drawing pinned to one bedframe—a crude sun, a stick figure, the word 'HOME' in wobbly letters.")
(FDESC "Dozens of bed frames line the walls. Most mattresses have rotted away, leaving only rusted springs. Among the debris, a child's [[crayon drawing]] is pinned to one bedframe.")
(LDESC "Rusted bed frames line the corridor.")
(TEXT "Dozens of bed frames line the walls. The mattresses have rotted away, leaving only rusted springs and metal frames. Some still have restraint straps attached.")>

Expand Down Expand Up @@ -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)>
Expand Down Expand Up @@ -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)
Expand Down
35 changes: 35 additions & 0 deletions books/blackwood-horror/test/test-prose-reveal.zil
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<INSERT-FILE "books/blackwood-horror/blackwood-horror">

<GLOBAL CO <CO-CREATE GO>>

;"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."

<ROUTINE RUN-TEST ()
<ASSERT "Start at Sanitarium Gate" <CO-RESUME ,CO "look" T> <==? ,HERE ,SANITARIUM-GATE>>
<ASSERT-TEXT "retrace" <CO-RESUME ,CO "go back">>
<ASSERT "A refused GO BACK leaves the player in place" <==? ,HERE ,SANITARIUM-GATE>>
<ASSERT-NOT-TEXT "[[" <CO-RESUME ,CO "look">>
<ASSERT "Enter the entrance hall" <CO-RESUME ,CO "north" T> <==? ,HERE ,SANITARIUM-ENTRANCE>>
<ASSERT "GO BACK returns to the gate" <CO-RESUME ,CO "go back" T> <==? ,HERE ,SANITARIUM-GATE>>
<ASSERT "GO BACK again returns to the hall" <CO-RESUME ,CO "walk back" T> <==? ,HERE ,SANITARIUM-ENTRANCE>>

<ASSERT "Go to Operating Theater" <CO-RESUME ,CO "north" T> <==? ,HERE ,OPERATING-THEATER>>
<ASSERT-TEXT "slightly ajar" <CO-RESUME ,CO "look">>
<ASSERT-NOT-TEXT "scalpel" <CO-RESUME ,CO "look">>
<ASSERT-NOT-TEXT "contains" <CO-RESUME ,CO "look">>
<ASSERT "The scalpel cannot be taken unseen" <CO-RESUME ,CO "take scalpel" T> <==? <LOC ,SCALPEL> ,METAL-CABINET>>
<ASSERT-TEXT "scalpel" <CO-RESUME ,CO "examine cabinet">>
<ASSERT "Examining the cabinet opens it" <FSET? ,METAL-CABINET ,OPENBIT>>
<ASSERT-TEXT "rusty scalpel" <CO-RESUME ,CO "look">>
<ASSERT "Take scalpel" <CO-RESUME ,CO "take scalpel" T> <==? <LOC ,SCALPEL> ,ADVENTURER>>
<ASSERT-TEXT "slightly ajar" <CO-RESUME ,CO "close cabinet">>
<ASSERT-NOT-TEXT "ether" <CO-RESUME ,CO "look">>
<ASSERT-TEXT "bottle" <CO-RESUME ,CO "open cabinet">>

<ASSERT "Back to the hall" <CO-RESUME ,CO "back" T> <==? ,HERE ,SANITARIUM-ENTRANCE>>
<ASSERT "Go to Patient Ward" <CO-RESUME ,CO "east" T> <==? ,HERE ,PATIENT-WARD>>
<ASSERT-TEXT "crayon drawing" <CO-RESUME ,CO "look">>
<ASSERT-NOT-TEXT "HOME" <CO-RESUME ,CO "look">>
<ASSERT-TEXT "HOME" <CO-RESUME ,CO "examine drawing">>>
39 changes: 39 additions & 0 deletions docs/WRITING_ADVENTURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
<TELL "A gravel path leads [[north]] to the entrance." CR>
```

- `[[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
Expand Down
69 changes: 66 additions & 3 deletions zilscript/bootstrap.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading