Status: stable. This is the authoritative definition of the Arcturus language surface and its semantics, at the level an author needs to write correct programs. The language is proven in the field: Hibernated 2, a full-length commercial game, is ported and plays to completion, and the two reference games in sections 18 and 19 are the conformance anchors of the whole toolchain. Where the compiler and this document disagree, the document wins: the code gets fixed, or the document is amended in the same commit.
Scope boundary. This document defines the language. The runtime behavior the language drives, the standard library (named Cosmos), the parser, the action pipeline, the banner, and the optional summonable features are defined in 02-cosmos-and-parser.md. The lowering of each construct to z5 is recorded in 03-compiler-pipeline.md and 04-codegen-mapping.md. Where this document says "Cosmos provides X", X is specified in 02.
The worked examples in sections 18 and 19, the Brass Lantern and the iconic Cloak of Darkness, use only constructs defined here and serve as the shared reference programs across all documents.
- One way to read a thing.
changesets any mutable state;istests any boolean; the dot reads any property. - The author describes the world; the compiler handles the machine, and aims for the smallest possible z-code in doing so.
- Structure from indentation. No braces and no
end. - Declarative shape, imperative behavior. Objects are data; behavior hangs
off them in
onhandlers andblockroutines. - Errors at compile time, not surprises at run time.
Arcturus source uses three file extensions, named after the star: .storyarc
for a story (an author's game), .prelude for a core Cosmos library file (the
prelude loaded before the story), and .granule for a summoned module (a
granule on the star's surface). A granule is anything brought in with summon,
whether a third-party extension or an optional Cosmos feature or language pack;
it loads only when summoned. The core Cosmos library is .prelude; everything
opt-in is a .granule. All three are the same language and lex identically; the
extension only signals the file's role.
Source is UTF-8; the compiler maps text to ZSCII at build time.
Comments start with // and run to end of line. There are no block comments.
Identifiers begin with a letter and contain letters, digits, and underscores, and are case sensitive. Convention is lower_snake_case. Reserved words (appendix A) cannot be identifiers.
Indentation defines block structure: an indent opens a body, a dedent closes it. Use a consistent unit, four spaces recommended, and never mix tabs and spaces. An inconsistent indent is a compile error.
Newlines are significant: one statement or declaration per line. A quoted
string may span several physical lines (section 16). One exception continues a
logical line: a line ending in a comma runs on to the next, so a long
comma-separated list (a spans set, an in clause, words, verb synonyms)
can be broken across lines and indented freely. Blank and comment-only lines
between the comma and the continuation are ignored.
- number: a 16-bit signed integer, -32768 to 32767, wrapping arithmetic. No floats.
- text: a string, ZSCII encoded at compile time, with
${ }interpolation. - boolean:
trueorfalse. - object: a reference to a declared object; the literal
nothingis null. - list: an ordered, bounded collection, declared with a capacity.
- block: a routine value (section 11). A property may hold a block, which makes it a computed property.
Conditions must be boolean; if n for a number is a compile error, write
if n > 0. Object presence is tested with is not nothing.
Top-level constructs, in any order: the game metadata block, summon
directives, kind, room, thing, verb, global, constant, block,
and free-standing on rules. A language pack additionally uses the
language-layer declarations language (its self-identifying marker),
direction, particle, pronoun, chain (the words that join several
commands on one line), and noise (the articles the parser knows but
ignores), which map player-typed words to the compiler's fixed properties
and roles (02, sections 8, 8a, 8b, and 14a); and a German
object declares its gender with a bare der, die, or das line, which the
compiler maps to the gender attributes (02, section 14a).
The metadata block sets everything the banner and story header carry:
game
title "The Brass Lantern"
headline "An Interactive Fiction"
author "Stefan"
release 1
serial "260626"
UUID c35b1143-7d7e-47f8-beb3-2637c4c094ab
start hallway
release is the release number (default 1). serial is the six-digit
YYMMDD serial; if omitted the compiler uses the build date. UUID is written
into the story file as an IFID array so IFDB and similar can identify the
game; it is optional but recommended. headline is the subtitle line of the
banner. banner false stops the automatic banner at start: the game prints it
later with print_banner (after a quote box, say), or never.
The banner also names the compiler (Arcturus) and the library
(Cosmos) with their versions; see 02.
Story state comes in three declarations, and the head tells the reader what they are holding:
flag grill_open // boolean state; starts false
flag emergency_power = true // the rare pre-set one
counter grill_pushes // a number that counts; starts 0
counter lives = 3
global motto = "Per aspera ad astra." // the general drawer
global favorite = lantern
constant max_carry = 7
A FLAG holds only true or false, forever: change grill_open to 3 is a
compile error, and no = false is ever written, since a flag starts false
by itself. A COUNTER is a number with the counting mechanics attached:
grill_pushes++
lives--
++ and -- belong to counters alone; everything else, and any other
assignment, keeps the one way to write state, change x to <value> (the
= appears only at the declaration). A GLOBAL is everything else: numbers
that are values rather than counts, object references, and strings (a text
global holds its string and prints as text in ${...} interpolation).
All three are Z-machine globals underneath; the split is for the reader and the compiler, which checks the promise each head makes, and is free to pack flags into bits later without any source change. The Z-machine offers 240; the compiler allocates them and errors only if a program exceeds that.
Two built-in categories introduce objects: room for locations, thing for
everything else. Both are kinds and can be extended.
room <id> [of <kind>]
thing <id> [of <kind>] [in <location>]
thing <id> [of <kind>] in <room>, <room> ...
of <kind> sets the parent kind; in <location> sets the initial tree
position. The body is property settings, on handlers, an optional grains
block (section 14), topic blocks on a character (section 15), and, with
summon.ambience, ambience blocks (02, section 14; docs/05).
An object can also live BACKSTAGE: thing vlad of character in scope places
it in an invisible room whose contents the parser always has in scope, in
every room, light or dark. That is the home of a companion who follows the
player everywhere, of their examinable parts, of anything the game should
always answer for; move x to scope and back stages things at run time (the
seen-but-unreachable chip in a droid's chest). A backstage object is never
listed (its room is never entered), so it defends itself in its own handlers:
make it scenery, or answer on take yourself. The whole mechanism folds
away in a game that stages nothing. The name scope is reserved as a
location only in games that use it.
A fixed object can be in scope in more than one room. The object tree gives each
object a single home, so a second (and third) room is a span: in hall, vault
puts the object in hall and spans it into vault, and a spans a, b, c line
in the body does the same for a scenery object with no single home (a moon seen
from three clearings). The object lives in one room and is referable from every
room it spans. Spanning is for non-movable objects (fixed or scenery); on a
movable object it is ignored, since a carried object's scope follows it. Its
headline uses are a two-sided door (one door object in both rooms it joins) and
wide scenery. A room's exit may name such a door, gating movement on it (02
section 10).
A span target may be a room KIND, not just a named room: spans outside_room
puts the object in scope in every room of that kind. The sun and the sky hang
over every outdoor room, the walls stand in every indoor one, and you name the
kind once instead of listing rooms:
kind outside_room of room // a marker kind: no body needed
room meadow of outside_room
name "Meadow"
...
thing the_sun
scenery
spans outside_room // in scope in meadow and every other outside_room
desc "It blazes overhead."
Every room is known at compile time, so the kind expands to its rooms as you
build; there is no runtime cost beyond the ordinary span. A room of a subkind
counts too (a beach_room of outside_room is an outside room). A kind used only
to tag its instances like this can be declared with no body at all.
A long span is not confined to one line. Every spans line on an object adds
to its set, so a wide scenery object can list its rooms (or kinds) across as many
lines as read well, and a line ending in a comma continues on the next (section
2):
thing river
scenery
spans north_bank, south_bank, ford,
mill_race, weir, millpond
spans estuary
room hallway
name "Hallway"
desc "A bare stone hallway. Worn steps lead down, north."
north cellar
thing lantern in hallway
name "brass lantern"
words brass, lantern, lamp
desc "A battered brass lantern."
switchable
The object identifier (lantern) is the code symbol; the name property is
the printed text. They are different. The words property is a third thing
again: the vocabulary the parser matches, holding the object's nouns and
adjectives as equal entries. name is printed but not typed; words is
typed but not printed. Adjectives are simply words in words (02, section 8).
The player is a seeded object every game already has. The language layer gives
it the standard self-words, so x me, x myself, x yourself (and each
language's own: untersuche dich, examinate) work in every game with no
author code; taking yourself answers its own line, and examining yourself
without a player.desc gets a proper default ("Are we going to admire
ourselves for a while or do we play an adventure game?") rather than an
object's message.
A game augments the player with top-level player. declarations:
player.words olivia, lund
player.desc "You are Olivia Lund, exobiologist."
player.words ADDS to the words already declared (the standard self-words
stay), so the heroine answers to her name and to "me" alike. player.desc
sets the description x me prints, and it takes the computed form like any
text property:
player.desc block
say "You catch a glimpse of yourself: Olivia Lund. Once just an
exobiologist, now a ghost haunting the graveyards of the stars."
Any player property can be set this way (player.name, or a custom flag);
words accumulates, everything else is set with the last declaration winning.
The intro property is an object's initial appearance in a room description.
While the object sits untouched in place, the room lists it with its intro
text, as its own paragraph, instead of the plain "You can see X here." The
moment the player first takes it, Cosmos sets the moved attribute and the
object reverts to the plain listing. intro replaces the whole generated line:
for a container, that includes the (contains ...) contents listing, on the
principle that an author who writes the prose owns the description (mention the
contents in the intro itself if they should show). A fixed or static object is
never taken, so its intro shows for as long as it is in view, which makes
intro the way to write set dressing that reads as prose rather than a list:
thing statue in hall
name "marble statue"
fixed
intro "A marble statue of a forgotten king dominates the room."
Containment is the Z-machine object tree: one parent per object, set with
in, changed with move, read with holds, in, and for each. The tree
is a separate axis from properties and is never reached with the dot.
CAREFUL, INFORM HANDS: Arcturus's move is the SILENT tree operation; it
does no bookkeeping of any kind. The Inform idiom "move lamp to player" (an
acquisition the player should be credited for) is the Cosmos block
gain(lamp): it pays a scored thing's points exactly once and marks it
moved and seen before moving it, the bookkeeping TAKE would have done
(02, section 7). A bare move lamp to player leaves auto-scored points
unreachable and the object's intro un-retired. Rule of thumb: gain when
the player RECEIVES something, teleport when the player ARRIVES somewhere,
move for silent stage management behind the scenes.
Directions are object-valued properties on a room whose value is another
room. north cellar sets north to cellar; it can be changed at run time
(change hallway.north to nothing). Cosmos defines the direction names and
the GO verb that reads them.
Kinds are templates supplying default properties and shared handlers:
kind lamp_kind of thing
switchable
lit false
on switch_on
now self is lit
say "Light floods out."
An instance is declared with thing or room plus of:
thing brass_lantern of lamp_kind in hallway
name "brass lantern"
lit false // overrides the inherited default
A kind roots at thing or room. Inheritance is single parent in v1: a kind
names one parent with of, forming a chain (small_box of container of thing).
Resolution order. An instance inherits every property and handler of its kind
chain. Re-declaring a property overrides the inherited default. For handlers,
the most specific runs first: the instance's own, then its kind's, then each
parent, then the Cosmos default. Each handler either ends, which consumes the
action so no more general handler or default runs, or ends with continue to
pass control to the next handler up the chain.
Multiple-parent composition (a thing that is both a container and a supporter) is a deliberate non-goal for v1; model it as a kind chain, or say so if a real game needs true mixins.
The standard kinds root the tree: thing and room, and of thing the kinds
container, supporter, door, and character. Each is an attribute (obj is container), and each supplies the defaults universal to it: a room is lit, a
door is openable and fixed, a character is animate and refuses being
taken. character is the animate kind for anyone the player addresses, gives to,
or talks to, people and animals and robots alike. What each standard kind
provides is listed in 02 section 10.
The author works with one concept, the property, and the compiler decides its physical storage.
<name> <default> // a property with that default; type from the literal
<name> // shorthand: a boolean property defaulting to true
<name> list <n> // a list property with capacity n, initially empty
<name> block // a computed property; the indented block follows
The declared default's type fixes the property's type program-wide. Using one property as two types is a compile error naming both sites.
Representation is chosen by the compiler in the same whole-program pass that performs dead-code elimination:
- A property whose values are only ever boolean across the whole program
becomes an attribute bit.
if chest is lockedcompiles to a bit test;now chest is opento a bit set. Zero marginal object bytes. - A property holding a number, text, object, list, or block becomes a property slot.
You write the same change ... to ... and is either way; the bit-or-slot
choice is invisible.
Computed properties. A property whose value is a block is evaluated by
running the block when the property is read. The block says its own text;
reading the property (for example when Cosmos prints a room desc) runs the
block, and whatever it says is what the property prints. This is text only: a
computed desc is the headline use, and a computed value property (a number
or object decided at run time) is reported as a compile error, because a read
cannot tell a small value apart from the block's address.
room cellar
name "Cellar"
desc block
if here is lit
say "A damp cellar of black stone."
else
say "Pitch black. You feel cold stone underfoot."
Boundaries the compiler enforces:
- The 48-bit budget. More boolean-only properties than there are attribute bits spills the least-used to one-word slots holding 0 or 1; correct, slightly larger, never visible.
- Declare before you change. Object layout is frozen at build time, so a
property cannot be created at run time.
change ruby.foo to falsewhenrubynever declaredfoois a compile error.addis for lists only (section 7).
Unused declared properties are removed by dead-code elimination.
Cosmos predefines these boolean attributes. Set one by naming it (fixed),
clear it with false (fixed false), test it with is.
| Attribute | Meaning and usage |
|---|---|
fixed |
The object cannot be taken; it stays where it is. take refuses it. |
scenery |
Background detail: still referable for examine, but left out of the room's contents listing and not takeable (gives the scenery line). A game that wants what sits ON or IN scenery holders told anyway opts in once with constant scenery_contents = 1: each such holder then gets its own paragraph ("On the counter you can see a bell and a candle."), the knowledge model deciding per item (PunyInform's OPTIONAL_PRINT_SCENERY_CONTENTS, as a fold: off by default, zero bytes unused). Worked example: examples/features/scenery-contents.storyarc. |
hidden |
Out of scope entirely until cleared: an undiscovered object, neither listed nor referable. Clear it when the object is revealed. |
concealed |
In scope and actable, but omitted from the room's contents listing (present but not spelled out in the description). |
wearable |
Can be worn; the wear verb accepts it. |
worn |
Currently worn. Set by wear, cleared by drop / take_off. Inventory tags it "(worn)". |
lit |
Gives light. On a room, the room is independently lit; on a thing, the thing glows and lights its location. Light is otherwise computed. |
edible |
Can be eaten; the eat verb consumes it rather than refusing. |
named |
A proper-named thing (Linda, Excalibur). Takes no article: ${the noun} and ${a noun} print just the name. |
an |
The indefinite article is "an", not "a". Derived from the name's first letter (a vowel -> an); set an or an false only for an exception (an hour, a unicorn). |
feminine |
Grammatical gender. Drives the Spanish articles and agreement (la lampara, Cogida), the German article (declared there with die, which sets this), and the English "her" pronoun on a character. Spanish derives it from a head noun ending in -a or a reliably feminine suffix; declare it where spelling cannot reveal it (la llave; an English Ruth). Masculine is the unmarked default. |
neutral |
The third German gender, declared there with das (das Buch, "es"). English and Spanish never read it. |
switchable |
Marks a thing the switch verb targets, but the effect is the author's: unlike openable or edible, there is no built-in on/off behavior (the library has no way to know what turning a thing on should do), so give the object on switch_on and on switch_off handlers. Without them, switching it is refused (msg_no_switch). The attribute itself only advertises intent. |
openable |
Can be opened and closed; the open / close verbs apply. |
open |
Currently open (a container or door). Set by open, cleared by close. A closed container hides its contents from scope. |
clear |
A see-through container (a glass jar): its contents are in scope and referable even when closed. An open or clear container exposes its contents; a closed opaque one shields them. |
seen |
Set once the player has been shown an object (a content of an open container, something taken or examined). A closed opaque container still lists the contents the player has seen, so they are not forgotten when put away; contents never seen stay hidden until the box is opened. Cosmos manages this; you rarely set it. The full container knowledge model is in 02, section 5a. |
lockable |
Can be locked and unlocked with a key (lock / unlock). |
locked |
Currently locked; blocks open until unlocked with the matching key. |
scored |
Managed by scoring (section 6a): the compiler sets it on every room and takeable thing; write scored false to exempt one. Set it by hand only in a game without scoring that wants a single classic auto-payer. |
visited |
The room has been entered before (Cosmos sets it on entry). Use it to vary a room's description on return. |
moved |
Set the first time the player takes an object. While clear, the object shows its intro text in a room description instead of the plain listing. |
animate |
An animate agent (a person, animal, robot, or AI). The conversation and give verbs apply only to the animate; the character kind sets it by default, and animate objects refuse being taken. |
component |
This thing is PART OF the thing it sits in (a lever in a machine, a button on a panel; the equivalent of Dialog's #partof). The object tree carries the relation, so the part follows its whole wherever the whole moves; the attribute grants what a plain thing's insides never get: the part is in scope whenever the whole is, take answers that it is part of it (msg_part_of), and it never lists as the whole's contents. Make the part on pull / on push handlers do the machine's work. To detach one in play, clear the attribute and move it. A game with no components pays nothing (any_components). |
The standard kinds are also attributes, set by of <kind> and tested with is <kind>: thing, room, container, supporter, door, character. An object
carries the attribute of every kind in its chain.
| Property | Type | Meaning and usage |
|---|---|---|
name |
text | The printed short name ("brass lantern"). Distinct from the object's id and from words. |
desc |
text | The description shown by examine (and on first look at a room). |
words |
list | The vocabulary the parser matches: the object's nouns and adjectives, as equal entries. Typed but not printed. |
tag |
text | A short state qualifier appended to the object in listings and the inventory: "a fluid canister (full)". Usually computed (tag block); print with show, not say, so it stays inline. The parentheses come from the listing. |
plural |
list | The words that name this object AS PART OF A GROUP (plural coins on each coin): "take coins" acts on every match in scope. Only with summon.plurals (02 section 8; docs/05); ignored otherwise. |
intro |
text | An object's initial appearance in a room, shown as its own paragraph while the object is untouched (moved clear). |
appearance |
text | The paragraph the object ALWAYS owns in a room description, replacing its listing line and never expiring ("The keeper is trimming the wick."): Inform's describe, Dialog's (appearance $). A computed block (appearance block) words it by state; checked before intro; hidden/concealed still suppress. Costs nothing in a game that never sets one. |
capacity |
number | How many objects a container or supporter holds. |
article |
text | The definite article, verbatim, when derivation cannot reach it: article "las" (las tijeras), article "el" (el agua). |
indefinite |
text | The indefinite article, verbatim: indefinite "unas", or an English mass noun with indefinite "some" ("You can see some water here."). |
unseal_with |
object | The object (a key) that locks and unlocks this one (for lockable things). |
arc_image |
number | Optional. A room's picture, named by its resource id (arc_image 8, or a constant that folds to one). Shown on an aware interpreter, ignored on a standard one. Section 6b. |
score, max_score, and turns are runtime globals, not object properties (02
section 2).
Score just works. One line in the game block turns it on:
game
title "Hibernated 2"
scoring
With scoring on, every room pays five points on the first visit and every
takeable thing five points on the first take, automatically: no attributes,
no bookkeeping, no table. The start room and whatever the player starts
holding never pay (nothing is earned by beginning). A room or thing that
should not score opts out with one line:
room broom_closet
scored false
Things a plain take refuses anyway (scenery, fixed, animate, doors) never pay and never count.
For everything the compiler cannot know, the events, there is award, a
statement legal anywhere a statement is (handlers, topic bodies, grains):
on push
award 15
say "The mechanism yields."
Every award site pays EXACTLY ONCE, by construction; a second push is a
silent no-op, and no moved/visited/flag guard is ever written. When one
problem has alternative solutions worth different points, name the pool:
if hacked_it
award 10 for door_solved "outsmarting the blast door"
else
award 5 for door_solved "outsmarting the blast door"
A pool pays once, whichever branch fires first. Its label is author documentation: it names the pool in the source and in the compile ledger, and costs the story file nothing.
MAX_SCORE COMPUTES ITSELF: the sum of every automatic room and thing, every
anonymous award site, and every pool counted once at its maximum. It is
never typed, so it can never drift from the game (no more 355/350). The
compile ledger prints the plan (scoring 6 award sites, 1 pool, 12 auto-scored; max_score 95), which is your scoring table: generated, not
written. The one honest limit: an award that is UNREACHABLE still counts,
because reachability is yours, not the compiler's; the ledger makes such a
site easy to spot.
RANKS, the Infocom ladder, need no numbers either:
ranks
"Cosmic Explorer"
"Interstellar Apprentice"
"Space Archaeologist"
"Savior of the Universe"
The titles spread evenly across the summed max (the last always means full score) and the score verb announces them: "You have scored 55 of a possible 95, which earns you the rank of Interstellar Apprentice." An entry may pin its own threshold, overriding the spread, in either unit:
ranks
"Cosmic Explorer"
"Interstellar Apprentice" at 17 percent
"Slayer of the Prime Unit" at 320 points
"Savior of the Universe" at 100 percent
A PERCENT pin scales with the summed max, so the ladder keeps its shape as the game grows during development; a POINTS pin is the definite value, verbatim, for when a rank must sit exactly at a known threshold. Mix them freely; unpinned titles keep the even spread.
SCORE is the one score verb, Infocom-shaped:
You have scored 55 of a possible 95, in 21 turns, which earns you the rank
of Space Archaeologist.
One care the automatic points ask for: they pay through the verbs, so a
cutscene must pay the same way. Moving the player without walking (a crash
landing, a transit pod) is teleport(dest); handing the player an object
without TAKE (a panel pried open, a mechanism yielding its prize) is
gain(obj). Each pays exactly like the verb would, so no auto-scored
point ever becomes unreachable; a bare move obj to player pays nothing.
Section 5 has the rule of thumb (the move-versus-gain warning), section 7
the statements themselves.
The escape hatch: change score stays legal (penalties, score-as-resource),
but it is off the paved road: hand-changed points play no part in the
computed max. award is the road.
Optional graphics. A room can carry a picture, shown on an interpreter that can display one (Actaea's window) and silently absent everywhere else. The story stays a conformant z5 file that runs unchanged, text-only, on any standard interpreter: an interpreter only decodes bytes its control flow reaches, and the draw sits behind a capability guard a text interpreter never passes. A game that declares no picture is byte-identical to one that never could.
A picture is named by its arc_image id, a resource slot. The id is one number
shared by every target: on a modern system the interpreter loads <id>.png; a
retro build (B12) loads slot <id> in the machine's own format. So there is no
name table to translate down. Write the id as a plain number, or, for
readability, as a constant that folds to one:
constant scene_path = 8
constant scene_church = 1
room opening
name "Forsaken Path"
desc "A path deep in the Black Forest, extending north."
arc_image scene_path
north church
room church
name "Churchyard"
desc "A small stone church, its door ajar. The path leads back south."
arc_image scene_church
south opening
Ids start at 1; 0 is reserved to mean "no picture" (it clears the band). Cosmos
reads the property on room entry, behind the guard, and draws the picture; a
room with no arc_image clears the band, so the picture always matches the
room. Re-looking in the same room does not redraw (it would make a retro target
re-decompress its art for nothing).
Art is authored once as PNGs in one of two shapes, each a whole number of 8-pixel text rows tall so the status bar sits flush under the band:
| Mode | Pixels | Rows | arc_mode |
Look |
|---|---|---|---|---|
| Infocom | 320x72 | 9 | 9 |
The upper third, the classic Arthur style. |
| DAAD | 320x96 | 12 | 12 |
The upper half, the Rabenstein style. |
You declare the mode once, game-wide, with a constant named arc_mode, whose
value is the band height in text rows:
constant arc_mode = 12 // DAAD mode (320x96); 9 for Infocom mode (320x72)
This is deliberate, and it matters for the retro targets: the interpreter learns
the band size from the story, not by measuring a picture. It reserves the band
and lays out the screen (and, on an 8-bit machine, its memory) before any
picture is loaded, so nothing depends on a picture's pixel dimensions. The mode
travels in the draw opcode itself. arc_mode must be 9 or 12; omitted, it
defaults to 9 (Infocom mode). All of a game's pictures share the one mode, so
author your art to match it.
A modern interpreter integer-scales the picture to the window width, which keeps pixel art crisp at any font size; pixel art is the medium that looks best.
The pictures live beside the story, not inside it. During development, point the
interpreter at a directory of numbered PNGs (actaea game.z5 --images art/).
For distribution, the arcimg tool packs them into a single .arcres file (a
zip of the numbered PNGs), which the interpreter reads automatically when it
sits next to the story; the z5 stays a separate file.
arcimg is the third standalone tool, shipped like arcc and actaea
(build/arcimg, a single self-contained file). The two commands of the
modern path:
arcimg prep opening.jpg --id 8 --mode daad -o art/ # art/8.png at 320x96
arcimg pack art/ -o game.arcres # the distributable pack
The full picture workflow, the retro conversions, and which interpreters play the pictures today and next, is its own author guide: docs/07.
Mode-sized PNGs need nothing but the standard library; prep reaches for Pillow
only to resize or convert, and offers a guided install the first time. A worked
example, with its .arcres and heavily commented source, is in
examples/arc_image.
Statements appear inside on handlers, block bodies, and computed
properties.
let introduces a local: let n = 0.
change ... to ... is the universal setter, for a local, a global, or a
property:
change n to n + 1
change score to score + 10
change ruby.desc to "The ruby sits exposed."
now ... is / is not ... is boolean-set sugar: now ruby is lit,
now door is not locked.
move ... to ... is the only tree operation; nothing detaches:
move knife to player
move note to nothing
Three calls elevate move for the set pieces a silent tree operation
would get wrong, each doing the bookkeeping its verb would have done
(all three in 02, section 7). teleport(dest) moves the player without
walking (a crash landing, a transit pod) and describes the arrival.
gain(obj) hands the player an object without TAKE (a panel pried open,
a mechanism yielding its prize); section 5 has the move-versus-gain
warning, and with scoring on both pay exactly like their verbs
(section 6a). convey(vehicle, dest) moves a VEHICLE the player rides
(a boat, a lift, a mine cart): the player sits inside the vehicle in the
object tree, so moving the vehicle carries them, but what a plain move
cannot do is refresh here, the player's cached room, and scope then
still answers for the room left behind (the vehicle trap). convey
moves the vehicle, updates here when the player is aboard, and
describes the arrival, so a self-driving boat is one line in
on each_turn: convey(boat, here.south). See
examples/features/vehicles.storyarc.
The general form is perform: run any action as part of the current turn,
exactly as the player's own command would dispatch it, refusals, handlers,
and messages included (Inform's <<take book>>, Dialog's (try ...)):
perform("take", book) // the full TAKE, "Got it." and all
perform("go", west) // a real move; a direction rides the way slot
perform("give", coin, bob) // two nouns
if perform("open", chest) is 0
say "The chest defies you." // 0 means the action refused
The action name is checked at compile time; the enclosing command's own
operands are restored afterwards (a later AGAIN still repeats what the
player typed), and no extra turn passes: it is one turn's work. Where
teleport and gain exist they stay the better word (they are silent
about the how); perform is for when you want the verb's whole voice.
Costs nothing in a game that never calls it.
add ... to ... and remove ... from ... operate on list properties only:
add "ruby" to ruby.synonyms
remove "old" from chest.synonyms
say prints text or a value followed by a line break; printing a number prints
digits, an object prints its name: say "Score: ${score}.". show prints the
same way but without the trailing line break, for building one line from pieces:
show("You can only go ") then more show/say calls finish the line, the last
one ending it. Both honor the library's paragraph spacing (a pending blank line
is flushed before either prints). Use say to finish a line, show to build
one.
stop ends the current handler or block immediately; in an action handler
that also consumes the action (section 12). continue ends the current
handler and passes the action to the next, more general handler (section 12).
finish ends the game, printing its final message; Cosmos then reports the
final score (the same line SCORE prints) and offers the classic RESTART,
RESTORE, QUIT prompt, answered in the pack's own words (02, section 7).
if, else if, else, by indentation:
if ruby is lit
say "It glows."
else if ruby is hidden
say "You see nothing of note."
else
say "A dull red stone."
while:
while count > 0
say "."
change count to count - 1
for each ... in / of ...:
for each item in player // tree children of an object
for each word in ruby.synonyms // list elements
for each door of room // every instance of a kind
The tree walk is MOVE-SAFE for its own loop object: the next child is noted before the body runs, so emptying a container the obvious way just works, with no drain idiom to learn:
for each x in bucket
move x to here
Moving OTHER objects out of the same parent inside the body remains the author's own risk, as it has been on every Z-machine library.
switch, on a number or a string, with no fall-through; a case may list
several values, and else is the default:
switch reply
case "yes", "y"
say "Good."
case "no", "n"
say "As you wish."
else
say "I did not understand."
switch count
case 0
say "None."
case 1
say "Just one."
else
say "Several."
A number switch compiles to a compact comparison chain. A string switch compiles to equality tests, cheapest when the values are dictionary words (parser tokens), which is the common case for topics and replies.
Arithmetic on numbers: +, -, *, / (integer), mod.
Comparison: <, >, <=, >=.
Equality and identity: is, is not, for numbers, booleans, objects.
Boolean property test: <obj> is <property> and <obj> is not <property>
when the right side names a declared boolean property of the object:
if lantern is lit
if door is not locked
Kind-membership test: <obj> is <kind> and <obj> is not <kind> when the right
side names a kind, testing whether the object is of that kind (any kind in its
chain):
if hook is supporter
if noun is not container
The direction in doubles as a keyword (the containment operator, and the
copula form x is in y). Where only a value can stand, it is the direction:
perform("go", in) and if way is in both read naturally, while
x is in y with an operand after the in stays the tree test.
Predicate test: <value> is <block> and <value> is not <block> when the right
side names a block with exactly one parameter: the block is called with the left
side and the test is its truth (nonzero). So the library's predicates read the
way the attributes do:
if lamp is visible
if coin is not reachable
The block should return 0 or 1; visible(lamp) remains equivalent. Blocks of
any other arity are ordinary values here and keep the call-them-with-parens
error.
Disambiguation: when the right operand is a bare identifier, is is a property
test if it names a declared boolean property, a kind-membership test if it names
a kind, a predicate test if it names a one-parameter block, and otherwise an
equality. A name that is both a boolean property and an object (or a kind and an
object) used with is is a compile-time clash to rename.
Logic: and, or, not, short-circuiting.
Property read with the dot, chainable: ruby.value, hallway.north.name.
Tree tests: player holds lantern, lantern in player.
Built-in references in handler and block bodies: self (the enclosing
object), player, here (the current room), noun and second (the
matched objects), nothing. Cosmos also provides <obj> is visible and
<obj> is reachable (scope rules in 02).
A verb declaration lists the player's words, then grammar lines:
verb "take", "get"
take noun
verb "put"
put noun in noun
put noun on noun
A grammar line is an action name, then slots and literal words. Slots:
noun (one in-scope object), held (a held object), multi (several,
including "all"), text (free text). Bare words such as in, on, with
are literal prepositions. Two-object lines bind noun and second.
A two-noun line may end in reverse, for a verb whose two objects can be typed
in the other order without a preposition, the classic dative: GIVE and SHOW take
both give noun to noun ("give the coin to Bob") and give noun noun reverse
("give Bob the coin"). On a reversed line the first object is the recipient
(second) and the last is the thing (noun), so both orders reach the same
handler with the same roles. The parser splits the two adjacent nouns for you;
reverse needs exactly two noun slots and no preposition between them.
reverse is part of the grammar, not English, so a language pack declares the
reversed lines its language wants: the German pack does, since recipient-first
(gib Bob die Muenze) is the natural dative there.
POSITIONAL GRAMMAR. A line's first name is its action, and the action need not be the same on every line, so a verb's wording can say more than "one noun" or "two nouns around a preposition":
verb "dig", "excavate"
dig
dig noun
dig noun with held
dig in noun with held
verb "look", "l"
look
look noun
look at noun
look_under under noun
look_behind behind noun
A literal may open a line (dig in noun with held), and a leading word may
select the line's own action, so LOOK UNDER BED and LOOK BEHIND BED reach
look_under and look_behind, two ordinary actions with ordinary handlers.
The compiler notices such a verb and matches it positionally: lines are tried
most specific first (most literal words, then, among literal-free lines,
fewest slots), and the first line that fits the typed words wins. Everything
else about the turn is unchanged: slots resolve through the same scoring
matcher, ambiguity still asks, pronouns still bind, and a command no line
accounts for is refused honestly. A quoted literal (dig "in" noun) is the
same as the bare word.
This costs bytes only where it is used: a verb whose lines are the plain
shapes stays on the compact model, and a game with no positional verb compiles
byte-identical to one built before the feature existed. A positional verb
follows three checked rules: at most two slots per line, a literal word
between two slots (adjacent bare nouns belong to reverse, which is a
plain-model feature), and single-word verb synonyms.
EXTENDING THE STANDARD GRAMMAR. The grammar is not a fixed table you write
additions into; it is the sum of every verb declaration in the compile,
Cosmos's and yours alike, and your game is expected to add its own. Three
patterns cover what a game wants:
A new verb is just a declaration plus handlers. The action name is yours to invent; naming it in a grammar line is what creates it:
verb "dig", "excavate"
dig
dig noun
dig noun with held
dig in noun with held
on dig
...noun and second are bound as usual...
A new way to say an old thing reuses the standard action, so every handler and default response already in place answers the new wording too. The line names the standard action, and nothing else is needed:
verb "peruse"
examine noun
A richer shape for a standard verb redeclares it. List the verb's words and every line you want, the standard ones you keep plus your own; for the words it declares, the later declaration wins, so your version replaces the Cosmos one wholesale:
verb "attack", "hit", "break", "kill", "fight", "smash"
attack noun
attack noun with held
The showcase for all of this, including the LOOK extension with its two
wording-selected actions, is examples/features/grammar.storyarc; compile it
and type along. The same patterns hold in any language, because a language
pack's verbs are ordinary declarations too: a German game redeclares grabe
with dig in noun mit noun and the same matcher serves it (02 section 8c).
When several of your lines could fit the same typed command, remember the
matcher's order: the line with more literal words is tried first, declaration
order breaks ties, so you rarely need to think about it; when in doubt, put
the more specific wording first anyway, which reads better in the source.
Standard verbs, including talk-to, come from Cosmos; the full list of standard grammar lines is 02 appendix B, and how input is tokenized and resolved is defined in 02 (the positional matcher in 02 section 8c). This section defines only how you declare a verb and how its grammar names the action your handlers receive.
Direction words are declared the same spirit, mapping vocabulary to a fixed direction property:
direction north "north", "n"
direction northeast "northeast", "ne"
The property name (north, northeast, up, in, ...) is one of the standard
directions and never changes; the quoted words are the player's vocabulary. Like
verbs and messages, direction words are part of the language layer, so a language
pack redeclares them (direction north "norte", "n") and Cosmos ships the English
set. A game rarely writes these; it summons a language, or uses the default
English. Selecting a language is one summon: summon.language "spanish" compiles
that language layer in place of English (02, section 8).
A room's exit is written with this property name, not the word: north cellar,
east door (section 5). So an exit stays in the fixed English name even in a
translated game (east puerta), while the player types the localized word
(este). The same split runs through the language: the fixed identifiers a game's
code uses (thing, room, openable, the direction properties, the grain
actions in section 14) are English; only what the player reads and types is
localized.
A block is a named routine. It takes arguments, may return a value, and is
called from your code:
block points_for(item)
return item.value * 2
block describe_exit(dir)
if here.(dir) is nothing
say "no exit"
else
say "a way ${dir}"
Calling: points_for(ruby), describe_exit(north). Parameters are values
and need no type annotation. Recursion is allowed, bounded by the Z-machine
stack. The 15-locals limit per Z-machine routine is managed by the compiler,
which spills to the stack as needed.
PARENTHESES ONLY WHERE THEY EARN THEIR KEEP: a block (or intrinsic) that
takes no values is called by its bare name, in statement position
(print_banner, describe_room) and in value position alike
(let k = read_key, if any_scored is 1). The bare name resolves as a call
only after every data name (locals, globals, objects, constants,
directions), so story names always win, and naming a block that does take
values is a compile error pointing at the parenthesized form. Parens appear
exactly where arguments do: teleport(wreckage_site), random(6),
quote(5, 29). The same doctrine prefers the English tests over call
shapes: if shard is not moved (never if not (shard is moved); the
grouped form is for genuinely compound conditions), and if chip is in scope or the short chip in box for the tree test, with is not in the
negation.
Blocks also serve as computed property values (section 6) and as grain responses (section 14). A block attached to a property or grain may be named and referenced, or written inline as an indented body.
The split is deliberate: block routines are called by you; on handlers
are entry points the engine fires.
A handler runs when its event fires. Handlers live inside an object or kind
body, where self is that object, or as free-standing top-level rules naming
their object.
Action handlers match a verb and its objects:
on switch_on lantern
on take ruby
on put ruby in chest
To handle a whole kind, match the kind in any slot and refer to the matched
objects with noun and second. Both slots of a two-object verb may be a
specific object, a kind, or a mix:
on take container
say "${The noun} is too heavy to lift."
on put thing in chest // any thing put into the chest
if noun is not ruby
say "Only the ruby fits the slot."
stop
say "The ruby drops in with a click."
Here noun is the object put and second is the chest. The matched object
is always noun (and second for the second slot); test it against a
specific object with is and is not, as in if noun is not ruby.
A handler header may also list alternatives with or, so one handler covers
several specific objects:
on put ruby or ring in chest
say "${The noun} settles into the velvet."
The handler fires when noun is the ruby or the ring and second is the
chest, with noun bound to whichever matched.
Inside an object or kind body, self stands as an operand for the enclosing
object itself, which reads naturally where the object appears in its own
pattern:
thing haystack of container in farm
...
on put noun in self // anything put into THIS haystack
move noun to nothing
say "${The noun} vanishes into the hay."
In a kind body self means each instance, so every barrel of a kind guards
its own number. A free-standing rule has no enclosure and names its object
instead; writing self there is a compile error that says so.
One handler may answer several verbs at once, by listing the verbs separated by commas, so a shared response is written once:
on attack, push, pull
say "It is too far away for this."
stop
Comma joins verbs; or joins operand alternatives. The two combine, and any
operands apply to every listed verb:
on push, pull lever
say "The lever does not budge."
A when guard restricts a handler to a condition: it applies only while the
condition holds, and otherwise defers to the next handler up the chain.
on push slab when player holds crowbar
say "You lever the slab aside."
Default versus override. A matching handler replaces the verb's default behavior, with the most specific winning (the section 5 resolution order). Writing the handler switches the built-in behavior off: when the action fires, your lines run instead of it, and the built-in part only happens if you ask for it back. How the handler ENDS decides how much happens:
-
End it (reach the last line, or
stopearly): your lines are ALL that happens. Anon go westthat only says "LEAVING" prints the word and the player stays in the room. -
End with
continue: your lines happen, THEN the normal action does. The same handler withcontinueas its last line prints "LEAVING" and then the player really walks west. (continuehands the action to the next, more general handler: the kind's, the room's, and finally the Cosmos default, which does the real work.) -
on after <verb>is a separate handler for the third timing: your lines happen AFTER the action has really taken place. The player walks west first, then "The door clicks shut behind you." If the walk never happened (refused, or replaced by a handler that did not continue), the after handler stays silent.
stop on a handler's last line changes nothing: reaching the end blocks
the built-in behavior anyway. stop exists to end the handler from the
MIDDLE of the body, almost always inside an if, when a refusal means the
remaining lines should not run:
on go west
if door is locked
say "The door won't budge."
stop // end here: no movement
say "You slip through."
continue // unlocked: and now the go really happens
on after go west
say "The door clicks shut behind you."
The full ordering is in 02.
The after handler, fully. on after <verb> takes everything an ordinary
handler header takes: comma-separated verb lists, operand patterns, or
alternatives, and when guards, and it lives anywhere a handler lives (an
object, a kind, a room, or free-standing at file level):
on after take when here is vault
say "An alarm begins to wail somewhere above."
on after drop, put
if here is cloakroom
now bar is lit
Two rules govern when it fires. First, the action must have COMPLETED: it
ran, and nothing refused it. Every library refusal (can't see it, it's
fixed, the door is locked) marks the turn refused, and a story handler that
refuses something should do the same by setting the refused global before
it stops. Second, replacing counts as completing: an on take that ends
after printing its own version of the take still completed the action, so
its after handlers run. Only a REFUSED turn silences them.
Within the after pass, handlers resolve exactly like the main ones: most
specific first, and continue passes to the next (the kind's after, the
room's, a free-standing one). An on other catch-all never answers the
after pass; it is for the player's verbs, not for bookkeeping. In a game
with no on after anywhere the whole machinery folds away at compile time
and costs nothing.
on other is the catch-all handler: it fires for any action on the object
that no specific on <verb> handler caught. It is the object's own default,
the least specific of its handlers, running before the action climbs to the
kind, the room, or the Cosmos default; stop consumes the action and
continue passes it on. This is the equivalent of an Inform default: branch:
thing statue
name "marble statue"
on examine
say "A nobleman, nose long since chipped away."
on other
say "The statue suffers your attentions in silence."
Here examine has its own reply and every other verb falls to on other. The
name other always means "anything not otherwise matched": as a verb here, and
as the fallback direction in on go other (02). A specific handler that runs
and ends with continue climbs to the kind, the room, and the defaults; it
does not fall into the same object's on other, so on look / continue
reads as "pass look through untouched". Inside a go handler, way holds
the chosen direction and a bare direction name is comparable against it
(if way is not north), for rules that treat one direction differently.
The full dispatch chain is defined in 02, section 9.
Life-cycle events. Besides the action events named by verbs, Cosmos fires three
events as the game runs, handled with the same on syntax:
on startruns once at the very beginning, BEFORE the banner: this is where everything that must happen before the game proper belongs. Set up the world, arm timers from the outset, choose the screen colours (zcolor.backgroundand friends, so the banner prints on the colours you chose instead of being erased by them), and show an opening the way the Infocom games did, a scene or an epigraph before the title. The banner, then the first room description, follow.on enterruns when the player arrives in a room, as that room's handler, so a room can react to being entered. The name is shared with the ENTER verb, and the owner decides which is meant: on a room it is this arrival event (every hook fires; walking continues), while on a thing it is the ordinary verb handler, consuming like any other, which is what lets a scenery facade redirect ENTER into ateleportwithout the default refusal following.on each_turnruns once per turn, the per-turn daemon. Awhenguard decides when it is awake, and its reach follows scope: a room's runs while the player is there, an object's while it is in scope, a free-standing one every turn.
on each_turn when ruby is hidden
say "Water ticks against stone."
Recurring and delayed behavior beyond every turn uses the after and every
scheduling statements (one-shot and repeating timers); daemons and timers together
are covered in full in 02, section 13.
summon brings an optional Cosmos feature, or your own granule, into the build.
A granule is ordinary Arcturus source (kinds, verbs, blocks, grains) in a
.granule file, loaded only when summoned. There are three forms, which differ
in where the granule is found; the resolution rules and the fork workflow are in
05.
summon.statusline // the bundled feature, always
summon statusline.granule // your copy if present, else bundled
summon "extensions/lockpicking.granule" // an explicit file
- The dotted form (
summon.statusline) always uses the copy that ships inside the compiler. It also carries the non-granule featuresummon.language "<name>", which selects a language pack (a granule that overrides not only the messages and vocabulary but the parser's grammar logic where a language needs it, 02). - The bare filename form (
summon statusline.granule) prefers a copy in the story's directory or a-Ldirectory, and otherwise falls back to the bundled one with a notice. This is how you summon a forked granule by name, and also how you summon a tunedabbreviations.granule(below). - The quoted form is an explicit path, with no bundled fallback.
Text compression is not a summonable feature. The compiler always applies a
standard abbreviation set, so nothing is required to get it. A story can tune the
set to its own text with arcc --make-abbreviations, which writes an
abbreviations.granule beside it; summon that by name (summon abbreviations.granule) to use it in place of the default (02, and 05 section 7).
The granules that ship with Cosmos - extended verbs, the status line, verbose exits, the conversation menu, and debug verbs - are catalogued in 05. Debug is opt-in by the summon alone; there is no separate release build to strip it.
Grains are built-in cheap scenery: words that respond to a few verbs without the cost of a full object. They replace the cheap_scenery pattern and are part of the language, not an import.
A grains block lists grain lines. Each line names the actions it answers, the
scenery words it matches (one or more, joined by or), and a response, which
is a one-line say, a do of a named block, or an indented body. The actions
are named the way an on handler names them, by action (examine, touch,
smell), not by the player's word: they are fixed identifiers, while the scenery
words are the vocabulary the player types and a language pack localizes. In
English the two coincide, so examine reads as both; a Spanish grain still writes
examine "mar", the action in the fixed name and mar in Spanish.
room foyer
name "Foyer of the Opera House"
desc "Red and gold, with glittering chandeliers overhead."
grains
touch, examine "chandeliers" or "hall" say "Pretty nice."
examine "gold" say "Holy crap, that is worth a fortune."
examine "carpet"
say "Threadbare in the corners."
change foyer.noticed to true
examine "ceiling" do describe_ceiling
Grains may also be attached from outside the object's body, which lets extensions or language packs add them:
foyer.grains
examine "molding" or "cornice" say "Ornate plasterwork."
A grain matches when the player's verb resolves to one of the grain's actions and names one of its words, and no real object in scope matches that word. The parser handling of grains is defined in 02. Grains cost only dictionary words and a small table, never an object entry.
A grain word may be reused freely across rooms: "steps" can be set dressing in
the hallway and again in the cellar, each with its own response. The word gets
one dictionary entry, which points at a chain of (grain, owner) pairs, and the
parser answers with the grain whose owner is in scope. When several grains of
the same word are in scope at once (rare: a room and something the player
carries), the first declared wins. For one piece of scenery genuinely visible
from several rooms, a scenery thing with spans (section 5) is still the
better tool: one object, one description, one identity.
A character (a thing that is animate, which the character kind sets) can hold
conversation topics. A topic is one subject the player can raise, together with
the exchange that follows. Topics are inert on their own: a summoned feature
presents them, either through the Infocom-style ask/tell verbs
(summon.infocom_talking) or as a numbered menu (summon.conversations).
The two are mutually exclusive by the compiler: a game summons exactly one,
and switching presentations later is a one-line change. How they are
presented is defined in 02; this section defines the construct.
A topic is declared in the person's body:
topic <subject> "<label>" [words a, b, ...] [when <cond>] [once] [hidden]
<body>
The header parts, with the modifiers in any order:
<subject>is a barename id, local to this person;revealandhideaddress topics by it."<label>"is the line shown in the conversations menu (any expression).words a, b, ...are the words ask/tell match against (ask <person> about <word>). They are optional: a menu-only topic needs none, since the player picks it by number.when <cond>guards visibility; the topic is offered only while the condition holds, evaluated withselfbound to the person.oncemakes the topic one-shot: after it runs, the player cannot raise it again. Code can still bring it back withreveal(below).hiddenstarts the topic out of view, until arevealbrings it in.
By default a topic is repeatable and never leaves on its own: the player can raise it as often as they like. Nothing is needed to keep a topic around; every control below only ever takes one OUT of view. (How often a topic can be raised also depends on the presentation, and the two differ: see the note below.)
Three ways out of view, and when to use which. They differ in who is in control and whether the topic can come back:
- A
whenguard is LIVE STATE: the topic appears and disappears as the condition moves, with no bookkeeping. A topic whose own body changes the state it is guarded on ("ask Vlad to cut the grill" sets the grill open, and the guard was... and not grill_open) therefore vanishes the moment it has run, with noonceneeded, and would return by itself if the state ever reverted. When the story state already encodes what the topic is about, the guard alone is usually the whole answer. hidden/reveal/hideis a MANUAL SWITCH: the author decides the exact moment a topic enters or leaves, from another topic's body or any handler. Revealing is repeatable; use it when no world state naturally expresses "this is now worth raising".onceis a ONE-SHOT: after one telling the player cannot raise it again, regardless of guards. Unlike awhenguard it does not return on its own, and the player can never bring it back, which is the point (a confession the suspect will not repeat, a joke that dies on the second telling). Only the author can stage a return, with arevealin code, for a line that fires again under new circumstances;oncethen retires it once more. Do not use it for topics awhenguard already retires, or the guard becomes irrelevant.
They combine: hidden once is a one-shot that starts out of view, and a when
guard on a once topic gates the single telling.
The body is an ordinary statement block, so any statement is allowed. It adds four conversation forms:
you "..."prints the player's line, auto-quoted and attributed:You: "...".reply "..."prints the person's line, auto-quoted and attributed by name:<Name>: "..."(the person isself).say "..."is plain narration, a stage direction with no speaker or quotes.reveal <subject>brings another of the person's topics into view;hide <subject>takes one out of view.
The speaker labels and the quotation marks live in overridable library blocks
(line_you, line_reply, line_end), so a story or a language pack can restyle
or translate the framing without touching the topics.
A worked fragment:
thing esme of character in tent
name "Madame Esme"
named
topic fortune "your fortune"
you "What do you see for me?"
reply "A long road, and a choice you will not want to make."
reveal road
topic road "the long road" hidden once
you "This road. Where does it lead?"
reply "North, into the dark."
topic charm "the silver charm" words charm, relic when player holds charm
you "What is this charm worth to you?"
reply "More than you have. Keep it close."
Raising fortune, by asking or by picking it, runs the exchange and reveals
road, which then appears (it began hidden); road is once, so it retires
after one telling. The charm topic is offered only while the player holds the
charm, and answers to ask esme about charm or about relic.
A string is written in double quotes and may span physical lines; runs of whitespace, including line breaks, collapse to a single space, so continuation lines may be indented:
desc "A damp cellar of black stone. A squat pedestal stands at its
centre, a rusted lever set into the base."
Because a real line break collapses to a space, a forced line break is written
\n (Arcturus's spelling of Inform's ^); \n\n leaves a blank line, a
paragraph break. A say already ends its line, so \n is only for breaks
within a line of text:
say "Hey\n\nThis is two lines below.\n\n\nAnd this three."
To follow a say with a paragraph break, say it with the par modifier:
say.par "..." prints the text and marks the library's pending break, which
the next output flushes as a single blank line (repeats collapse, docs/02).
Consecutive prose paragraphs are each a say.par line, no bookkeeping
between them. The mirrored par.say "..." puts the break FIRST: the reveal
paragraph appended under existing prose (a first-visit aside, a description
that grows a second paragraph when the state changes). Both compose with a
colour in any order (say.yellow.par, par.say.yellow), and par.say.par
is a free-standing paragraph. The banner manages its own spacing the same
way (a trailing pending break; under a status bar the title sits directly
below the bar), so a story never calls the bare par for routine prose. If
story code reads like Inform new_lines, something is being done wrong.
Interpolation embeds an expression with ${ }; printing an object prints its
name. Article helpers: ${the ruby}, ${a ruby}, and the capitalized
${The ruby}, ${A ruby}; an object with named set takes no article.
Their full behavior is in 02. Escapes: \", \\, \$, and \n.
An object may override its articles outright with the article (definite) and
indefinite properties, for the cases derivation cannot reach: article "las" and indefinite "unas" for las tijeras, article "el" for el agua,
indefinite "some" for an English mass noun. The stored text prints verbatim,
so keep it lowercase and prefer messages that keep such objects mid-sentence:
a hand-set article does not capitalize itself at a sentence start.
An article may carry a grammatical-case tag after a colon, ${the:acc noun} or
${a:dat noun}, for a language whose article inflects for case (German
der/den/dem). The cases are nom, acc (or akk), dat, and gen; with no
tag the case is nominative. English and Spanish ignore the tag, so it costs
nothing there; a language pack's article block reads it (02, section 14a). Only
the definite and indefinite article take a tag.
Screen colours have their own section, 16a, below.
The Z-machine draws in nine standard colours, and Arcturus exposes them by name. The palette, as the Standard defines it (section 8.3.1):
| Name | Number | Colour |
|---|---|---|
default |
1 | the interpreter's own default |
black |
2 | black |
red |
3 | red |
green |
4 | green |
yellow |
5 | yellow |
blue |
6 | blue |
magenta |
7 | magenta (purple) |
cyan |
8 | cyan (light blue) |
white |
9 | white |
(Later revisions of the Standard add interpreter-specific greys; Arcturus supports the portable nine, which every colour interpreter carries, down to the 8-bit machines.)
The zcolor statement sets the base colours, one target per line, usually in
on start:
zcolor.font <colour>: the base text colour. Remembered, so every one-shot colour below restores to it.zcolor.background <colour>: the background. Setting it also repaints the screen, so the new colour covers the whole display rather than only the text printed from then on.zcolor.statusline <colour>: the status bar's text colour (with the statusline granule). The bar draws in it and the base font colour returns after every draw.zcolor.input <colour>: the colour of the text the player types. The command echoes in it, and the base font colour returns the moment the line is entered.
say.<colour> "..." prints one text in that colour and then restores the base
font colour by itself, so an emphasized passage is a single line with no state
to manage and no restore to forget. It composes with interpolation
(say.yellow "${The noun} glows.") and with the par modifier in either
order (say.yellow.par, section 16). Together, the classic Infocom-era look is
four lines and stays out of the prose:
on start
zcolor.font white
zcolor.background black
zcolor.statusline cyan
zcolor.input cyan
say.yellow "For my part I know nothing with any certainty, but the
sight of the stars makes me dream."
say "-- Vincent van Gogh"
Colour support is handled for you, at both ends. The compiler marks the story
as colour-using in the header (Flags 2 bit 6, which interpreters require
before they enable colour at all), and every colour operation checks at run
time whether the interpreter reports colour support (Flags 1 bit 0): on an
interpreter without it, zcolor does nothing and say.<colour> is exactly a
plain say. No author-side guard is ever needed, and a game that never uses
colours pays nothing for the feature. An unknown colour name is a compile
error that lists the palette.
Representative compile-time errors:
- Mutating an undeclared property.
- Property type clash across sites.
- A non-boolean condition (
if n). - Unknown verb or action in a handler header.
- Inconsistent indentation or mixed tabs and spaces.
- A
switchmixing number and string cases. - A name clash between a boolean property and an object used with
is. - A
summonof a missing file or unknown built-in feature.
A complete, winnable game using only constructs defined above. Cosmos supplies the parser, the turn loop, the player, and the everyday verbs, so this file is the world and the few behaviors that differ from the defaults.
game
title "The Brass Lantern"
author "Stefan"
UUID 7f3a9c20-1e44-4b8a-9d51-6c2f0b9a7e10
start hallway
on start
say "A cold draught curls up from somewhere below."
say "You came for the ruby. You should find some light first."
room hallway
name "Hallway"
desc "A bare stone hallway. Worn steps lead down into the dark, north."
north cellar
thing lantern in hallway
name "brass lantern"
words brass, lantern, lamp
desc "A battered brass lantern, switchable if you care to."
switchable
lit false
on switch_on
now self is lit
say "The lantern catches with a soft hiss."
on switch_off
now self is not lit
say "The flame gutters out, and the dark leans in."
room cellar
name "Cellar"
desc "A damp cellar of black stone. A squat pedestal stands at its
centre, a rusted lever set into the base."
south hallway
on enter
if not (player holds lantern and lantern is lit)
say "You grope down the steps, but sense wins over greed,
and you back up into the hallway."
move player to hallway
stop
on each_turn when ruby is hidden
say "Somewhere water ticks against stone, patient and unhurried."
thing pedestal in cellar
name "stone pedestal"
desc "Waist high and cold, a rusted lever set into its base."
fixed
thing lever in cellar
name "rusted lever"
desc "A stubby iron lever, begging to be pulled."
fixed
pulled false
on pull
if lever is pulled
say "It will not give a second time."
stop
now lever is pulled
now ruby is not hidden
change ruby.desc to "The ruby sits exposed, drinking the lantern."
say "The lever grinds down. A panel slides back, and a red gleam
answers the light."
thing ruby in cellar
name "blood ruby"
words red, blood, ruby, gem, jewel
desc "A ruby the size of a plum, drinking the light, giving back fire."
hidden
on take
move ruby to player
say "It is warm in your hand, almost a pulse."
finish "*** You carry the blood ruby home in ${turns} turns ***"
verb "pull", "yank"
pull noun
The benchmark game implemented in nearly every IF system, the natural second
conformance target, and a 1:1 port of Roger Firth's reference implementation
(the PunyInform cloak.inf, which is also the size benchmark, so the content
matches byte for byte in spirit). It exercises darkness, a wearable item that
changes a room's light, a supporter (the hook), a state counter with the
original's two-tier disturbance rules, two award sites self-summing the
classic MAX_SCORE of 2, and a win-or-lose ending.
game
title "Cloak of Darkness"
headline "A basic IF demonstration."
author "Roger Firth"
release 3
serial "221116"
UUID 2a1f8e63-9b07-4c2d-8f3a-5e1d6042b7c9
start foyer
// The classic Cloak of Darkness, a 1:1 port of Roger Firth's reference
// implementation (the PunyInform cloak.inf, release 3): three rooms, three
// objects, two points. The original shows the score on its status line, so
// this port summons one; the two `award 1` sites self-sum the max of 2.
// One knowing divergence, truer to Firth's spec than to his code: an action
// aimed at something unseen in the dark ("x message") disturbs the sawdust
// here, where the Inform parser rejected it before any rule could run.
summon.statusline
counter disturbed
on start
say "Hurrying through the rainswept November night, you're glad to see
the bright lights of the Opera House. It's surprising that there
aren't more people about but, hey, what do you expect in a cheap
demo game...?"
room foyer
name "Foyer of the Opera House"
desc "You are standing in a spacious hall, splendidly decorated in red
and gold, with glittering chandeliers overhead. The entrance from
the street is to the north, and there are doorways south and west."
south bar
west cloakroom
on go north
say "You've only just arrived, and besides, the weather outside
seems to be getting worse."
stop
room cloakroom
name "Cloakroom"
desc "The walls of this small room were clearly once lined with hooks,
though now only one remains. The exit is a door to the east."
east foyer
thing hook of supporter in cloakroom
name "small brass hook"
words small, brass, hook, peg
scenery
on examine
if hook holds cloak
say "It's just a small brass hook, with a cloak hanging on it."
else
say "It's just a small brass hook, screwed to the wall."
stop
thing cloak in player
name "velvet cloak"
words handsome, dark, black, velvet, satin, cloak
desc "A handsome cloak, of velvet trimmed with satin, and slightly
spattered with raindrops. Its blackness is so deep that it
almost seems to suck light from the room."
wearable
worn
// The cloak is the light switch: while it is anywhere on the player the
// bar stays dark, and it may only be put down in the cloakroom. The
// first hang on the hook is worth a point (award pays once by itself).
on drop, put
if here is not cloakroom
say "This isn't the best place to leave a smart cloak lying
around."
stop
continue
on after take
now bar is not lit
on after drop, put
if here is cloakroom
now bar is lit
if second is hook
award 1
room bar
name "Foyer bar"
desc "The bar, much rougher than you'd have guessed after the opulence
of the foyer to the north, is completely empty. There seems to be
some sort of message scrawled in the sawdust on the floor."
north foyer
lit false
// In the dark, going anywhere but north gropes badly (two disturbances,
// instant ruin) and any other action risks one; look and inventory pass
// through untouched, and the meta verbs never reach the room at all
// (out-of-world, as in the original).
on go
if here is not lit
if way is not north
change disturbed to disturbed + 2
say "Blundering around in the dark isn't a good idea!"
stop
continue
on look, inventory
continue
on other
if here is not lit
disturbed++
say "In the dark? You could easily disturb something!"
stop
continue
thing message in bar
name "scrawled message"
words message, sawdust, floor
scenery
on examine
if disturbed < 2
award 1
say "The message, neatly marked in the sawdust, reads..."
finish "*** You have won ***"
else
say "The message has been carelessly trampled, making it
difficult to read. You can just distinguish the words..."
finish "*** You have lost ***"
stop
verb "read"
examine noun
verb "hang"
put noun on noun
Both examples lean on Cosmos for the parser, the turn loop, scope, light, and the everyday verbs; the per-game logic above is all defined in this document. Section 15 of 02 reconciles each example with the Cosmos model in detail.
game, room, thing, kind, verb, of, in, on, after, block,
return, global, flag, counter, constant, let, change, to,
now, is, not,
add, remove, from, move, say, stop, continue, finish, if,
else, while, for, each, switch, case, and, or, holds,
when, self, player, here, noun, second, nothing, true,
false, list, summon, grains, do, title, headline, author,
release, serial, UUID, start, mod, every, topic, you, reply,
reveal, hide.
Grammar slot words (held, multi, text) and the standard direction and
verb names are reserved by Cosmos rather than the core language; see 02.
Informal sketch; INDENT and DEDENT are indentation tokens.
program := { toplevel }
toplevel := game_block | summon | kind_decl | object_decl | verb_decl
| global_decl | constant_decl | block_decl | rule
game_block := "game" INDENT { meta_line } DEDENT
summon := "summon" ( string | id )
| "summon" "." id [ string ]
object_decl := ("room" | "thing") id [ "of" id ] [ "in" id ]
INDENT { member } DEDENT
kind_decl := "kind" id [ "of" id ] INDENT { member } DEDENT
member := property_decl | handler | grains_block
property_decl := id [ value ] | id "list" number | id "block"
INDENT { statement } DEDENT
handler := "on" [ "after" ] event { "," event } [ pattern ]
[ "when" expr ] INDENT { statement } DEDENT
event := id (* a verb or action name, or "other" *)
pattern := { operand | word }
operand := id { "or" id }
grains_block := "grains" INDENT { grain } DEDENT
grain := verbs words ( "say" string | "do" id
| INDENT { statement } DEDENT )
verb_decl := "verb" string { "," string } INDENT { grammar } DEDENT
grammar := id { slot | word }
slot := "noun" | "held" | "multi" | "text"
block_decl := "block" id "(" [ params ] ")" INDENT { statement } DEDENT
global_decl := "global" id "=" expr
flag_decl := "flag" id [ "=" ( "true" | "false" ) ]
counter_decl := "counter" id [ "=" number ]
constant_decl := "constant" id "=" expr
rule := handler
statement := let | change | now | move | add | remove | say
| stop | continue | finish | if | while | for | switch
| return | call
switch := "switch" expr INDENT { case } [ else_case ] DEDENT
case := "case" value { "," value } INDENT { statement } DEDENT
for := "for" "each" id ( "in" | "of" ) expr
INDENT { statement } DEDENT
place := id | expr "." id
expr := (* numbers, strings, booleans, object refs, nothing,
dot access, calls, is / is not, holds, in,
and / or / not, arithmetic and comparison *)