examples/adventure/
Un’avventura testuale eseguita con una sequenza di comandi prestabilita.
- 454 righe
- 3 moduli
- riga di comando
- legge un file
Il mondo del gioco è descritto nei dati del programma. Ogni turno riceve lo stato attuale e restituisce quello nuovo, insieme ai messaggi da mostrare. Il programma termina senza chiamare exit: i test possono così verificare anche il controllo delle perdite di memoria, eseguito alla fine di main.
Eseguilo
A gate of black iron, standing open. A path leads north.
A walled yard, gone to grass. Doors east and west, the gate south.
There is a lantern here.
You are carrying nothing.
Cold ovens and a long table. Something glints under it.
There is a key here.
You take the key.
You are carrying: key.
A walled yard, gone to grass. Doors east and west, the gate south.
There is a lantern here.
Shelves to the ceiling, all of them empty. A small door north.
A round room with one locked door in the floor.
The key turns. The door swings up on a stair going down.
7 moves, and the stair is open.
exit 0 · 15 righe
Il programma
# A text adventure, played from a script.
#
# heroes run examples/adventure/main.hero -- examples/adventure/walkthrough.txt
#
# main.hero the program: the script, the transcript, and no `exit`
# game.hero one turn — a state in, a state and some lines out
# world.hero the map: rooms, the ways between them, and what is lying about
#
# **This is the corpus program that does not call `exit`** (author
# instruction), and it is here on purpose. `exit` bypasses
# `hero_runtime_check_leaks()` by design — a program asking to stop now is not
# asking for an audit, and running the gate there would report a leak for every
# live value in every frame the exit unwinds past — but on Darwin arm64 that
# counter is the **only** leak instrument there is. So the corpus keeps a path
# that runs off the end of `main` and crosses the gate, and this is it: every
# refusal below is a printed line and a return, never a status.
#
# The commands come from a file rather than from a terminal, which is what makes
# a game a corpus program at all. `game.play_all` is the same call the tests
# make, so what is tested is what runs.
use game
use world
function main()
given = args()
if len(given) == 0
print("usage: adventure <walkthrough.txt>")
return
match read_file(given[0])
.err e => print("cannot read ", given[0], ": ", e.code)
.ok script => run(script)
function run(script: str)
w = world.make()
lines = commands(script)
turn = game.play_all(w, lines)
for said in turn.said
print(said)
print("")
print(turn.next.moves, " moves, ", ending_of(turn.next))
function ending_of(g: game.Game) -> str
if g.won
return "and the stair is open."
return "and the stair is still shut."
# One command per line, with blanks and `#` comments dropped — the same shape
# every other file in this corpus uses, so a reader who has met one has met all
# of them.
function commands(script: str) -> [str]
out: [str] @= []
for line in split_lines(script)
head = trimmed(line)
if head == "" || head[0] == '#'
continue
out @ out.push(head)
return out
function is_space(c: u8) -> bool
return c == ' ' || c == '\t' || c == 13
function trimmed(line: str) -> str
from: i64 @= 0
while from < line.len() && is_space(line[from])
from @ from + 1
to: i64 @= line.len()
while to > from && is_space(line[to - 1])
to @ to - 1
return line.slice(from: from, to: to)
function split_lines(text: str) -> [str]
out: [str] @= []
start: i64 @= 0
i: i64 @= 0
while i < text.len()
if text[i] == '\n'
out @ out.push(text.slice(from: start, to: i))
start @ i + 1
i @ i + 1
if start < text.len()
out @ out.push(text.slice(from: start, to: text.len()))
return out
test "the walkthrough on disk wins the game"
script = read_file("examples/adventure/walkthrough.txt").must()
turn = game.play_all(world.make(), commands(script))
assert turn.next.won == true
assert ending_of(turn.next) == "and the stair is open."
test "comments and blank lines are not commands"
assert commands("go north\n\n# a note\n take \n") == ["go north", "take"]
assert commands("") == []
game.hero · 216 righe
# One turn: the state before, a line of English, the state after and what to say.
#
# **A turn is a value.** `play` takes a `Game` and returns a `Turn` holding the
# next `Game` and the lines to print — it changes nothing, because a language
# with no aliasing gives no way to change something you were handed. That is the
# shape a state machine takes here, and it is the reason the whole game is
# testable without a terminal: a test plays a list of commands and looks at what
# came back.
use world
record Game
here: str
carrying: [str]
taken: {str: bool}
moves: i64
won: bool
record Turn
said: [str]
next: Game
function begin(w: world.World) -> Game
return Game(here: w.start, carrying: [], taken: {}, moves: 0, won: false)
function is_space(c: u8) -> bool
return c == ' ' || c == '\t' || c == 13
function trimmed(line: str) -> str
from: i64 @= 0
while from < line.len() && is_space(line[from])
from @ from + 1
to: i64 @= line.len()
while to > from && is_space(line[to - 1])
to @ to - 1
return line.slice(from: from, to: to)
function first_word(line: str) -> str
i: i64 @= 0
while i < line.len() && line[i] != ' '
i @ i + 1
return line.slice(from: 0, to: i)
function rest_after(line: str, word: str) -> str
if word.len() >= line.len()
return ""
return trimmed(line.slice(from: word.len(), to: line.len()))
function carrying(g: Game, what: str) -> bool
for one in g.carrying
if one == what
return true
return false
## The turn
function play(w: world.World, g: Game, line: str) -> Turn
command = trimmed(line)
verb = first_word(command)
if verb == "look"
return same(g, describe(w, g))
if verb == "go"
return go(w, g, rest_after(line: command, word: "go"))
if verb == "take"
return take(w, g)
if verb == "bag"
return same(g, [bag_of(g)])
if verb == "open"
return open(g)
if verb == ""
return same(g, [])
return same(g, ["I do not know how to " + verb + "."])
function same(g: Game, said: [str]) -> Turn
return Turn(said: said, next: g)
function describe(w: world.World, g: Game) -> [str]
match world.room_at(w, g.here)
.err _ => return ["You are nowhere, which should not be possible."]
.ok room =>
lines: [str] @= [room.description]
if room.item != "" && g.taken[room.item].is_err()
lines @ lines.push("There is a " + room.item + " here.")
return lines
function go(w: world.World, g: Game, direction: str) -> Turn
if direction == ""
return same(g, ["Go where?"])
match world.through(w, here: g.here, direction: direction)
.err e => return same(g, [e.msg])
.ok there =>
moved = Game(
here: there,
carrying: g.carrying,
taken: g.taken,
moves: g.moves + 1,
won: g.won
)
return Turn(said: describe(w, moved), next: moved)
function take(w: world.World, g: Game) -> Turn
match world.room_at(w, g.here)
.err _ => return same(g, ["There is nothing here."])
.ok room =>
if room.item == "" || !g.taken[room.item].is_err()
return same(g, ["There is nothing to take."])
picked = Game(
here: g.here,
carrying: g.carrying.push(room.item),
taken: with_taken(g.taken, room.item),
moves: g.moves + 1,
won: g.won
)
return Turn(said: ["You take the " + room.item + "."], next: picked)
function with_taken(taken: {str: bool}, item: str) -> {str: bool}
out: {str: bool} @= taken
out[item] @ true
return out
function bag_of(g: Game) -> str
if g.carrying.len() == 0
return "You are carrying nothing."
return "You are carrying: " + join(sort(g.carrying), ", ") + "."
# The one command that ends the game. It needs the key **and** the room, so the
# two mistakes it can catch say different things.
function open(g: Game) -> Turn
if g.here != "vault"
return same(g, ["There is nothing here to open."])
if !carrying(g, "key")
return same(g, ["The door is locked, and you have no key."])
won = Game(
here: g.here,
carrying: g.carrying,
taken: g.taken,
moves: g.moves + 1,
won: true
)
return Turn(said: ["The key turns. The door swings up on a stair going down."], next: won)
# A whole walkthrough, played in order — the shape every test below uses, and
# the shape `main` uses too, so what is tested is what runs.
function play_all(w: world.World, lines: [str]) -> Turn
g: Game @= begin(w)
said: [str] @= []
for line in lines
turn = play(w, g, line)
for one in turn.said
said @ said.push(one)
g @ turn.next
return Turn(said: said, next: g)
test "a new game starts at the start, empty-handed"
w = world.make()
g = begin(w)
assert g.here == w.start
assert g.carrying == []
assert g.moves == 0
assert g.won == false
test "going somewhere describes it, and going nowhere says which ways there are"
w = world.make()
after = play(w, begin(w), "go north")
assert after.next.here == "yard"
assert after.next.moves == 1
refused = play(w, begin(w), "go west")
assert refused.next.here == "gate"
assert refused.next.moves == 0
assert refused.said == ["there is no way west — try north"]
test "an item is taken once, and the room remembers"
w = world.make()
turn = play_all(w, ["go north", "take", "take"])
assert turn.next.carrying == ["lantern"]
assert turn.said[turn.said.len() - 1] == "There is nothing to take."
test "the vault needs the key, and says which half is missing"
w = world.make()
no_key = play_all(w, ["go north", "go west", "go north", "open"])
assert no_key.next.won == false
assert no_key.said[no_key.said.len() - 1] == "The door is locked, and you have no key."
wrong_room = play_all(w, ["open"])
assert wrong_room.said == ["There is nothing here to open."]
test "the game can be won, and the walkthrough is the proof"
w = world.make()
won = play_all(w, ["go north", "go east", "take", "go west", "go west", "go north", "open"])
assert won.next.won == true
assert won.next.carrying == ["key"]
assert won.next.moves == 7
test "a word the game does not know is answered rather than ignored"
w = world.make()
assert play(w, begin(world.make()), "dance").said == ["I do not know how to dance."]
assert play(w, begin(world.make()), " ").said == []
world.hero · 132 righe
# The map: rooms, the ways between them, and what is lying about.
#
# Written in code rather than read from a file, and that is the point of having
# it: every other program in this corpus parses its input, and a world is the
# case where the data *is* the program. A `{str: Room}` keyed by name, each room
# holding a `{str: str}` from a direction to another room's name — so the map is
# a graph with no pointers in it, which is the only kind this language has.
#
# Names are the links. An index would be shorter and a reader would then have to
# hold a numbering in their head to know where `2` goes.
## Error codes
#
# One `constant` per code, so a typo is `error[unknown_name]` here rather
# than a comparison that is quietly false forever (panel 034 R4).
constant ERR_NO_WAY: str
"no_way"
record Room
name: str
description: str
exits: {str: str}
item: str
record World
rooms: {str: Room}
start: str
function make() -> World
rooms: {str: Room} @= {}
rooms["gate"] @ Room(
name: "gate",
description: "A gate of black iron, standing open. A path leads north.",
exits: {"north": "yard"},
item: ""
)
rooms["yard"] @ Room(
name: "yard",
description: "A walled yard, gone to grass. Doors east and west, the gate south.",
exits: {"south": "gate", "east": "kitchen", "west": "library"},
item: "lantern"
)
rooms["kitchen"] @ Room(
name: "kitchen",
description: "Cold ovens and a long table. Something glints under it.",
exits: {"west": "yard"},
item: "key"
)
rooms["library"] @ Room(
name: "library",
description: "Shelves to the ceiling, all of them empty. A small door north.",
exits: {"east": "yard", "north": "vault"},
item: ""
)
rooms["vault"] @ Room(
name: "vault",
description: "A round room with one locked door in the floor.",
exits: {"south": "library"},
item: ""
)
return World(rooms: rooms, start: "gate")
function room_at(w: World, name: str) -> Room?
return w.rooms[name]
# Where `direction` leads from here — or an error naming the ways that do exist,
# which is the whole difference between a game and a guessing game.
function through(w: World, here: str, direction: str) -> str?
room = room_at(w, here)?
match room.exits[direction]
.ok there => return ok(there)
.err _ => return fail(
code: ERR_NO_WAY,
msg: "there is no way " + direction + " — try " + join(sort(keys(room.exits)), ", ")
)
test "every exit leads to a room that exists, and comes back"
w = make()
for name in sort(keys(w.rooms))
room = room_at(w, name).must()
for direction in sort(keys(room.exits))
there = room.exits[direction].must()
assert !room_at(w, there).is_err()
test "the world is connected: every room is reachable from the start"
w = make()
seen = reachable(w)
assert seen.len() == len(w.rooms)
test "a direction that is not there names the ones that are"
w = make()
assert through(w, here: "gate", direction: "north").must() == "yard"
match through(w, here: "gate", direction: "west")
.ok _ => assert false
.err e =>
assert e.code == ERR_NO_WAY
assert e.msg == "there is no way west — try north"
test "the only item worth having is behind two doors"
w = make()
assert room_at(w, "kitchen").must().item == "key"
assert room_at(w, "vault").must().item == ""
# Every room a walker can get to, by breadth-first search over the names. It is
# a test helper that lives here because it is about the map rather than about
# the game — and a world with an unreachable room is a world with a bug in it.
function reachable(w: World) -> [str]
seen: {str: bool} @= {}
seen[w.start] @ true
queue: [str] @= [w.start]
head: i64 @= 0
while head < queue.len()
here = queue[head]
head @ head + 1
match room_at(w, here)
.err _ => continue
.ok room =>
for direction in sort(keys(room.exits))
there = room.exits[direction].must()
if seen[there].is_err()
seen[there] @ true
queue @ queue.push(there)
return sort(keys(seen))
Input
# The short way through, which is the only way through.
look
go north
bag
go east
take
bag
go west
go west
go north
open
impronta sha256 190e1c2b5b2c · 6 file · index.json
Successivo: maze. Tutti gli esempi. ← Precedente: wordbands
Eseguine uno: i quattro comandi. Impara il linguaggio, oppure leggi la specifica completa in un solo file.