Tapestry
Source Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Tapestry

Tapestry is an experimental interactive fiction engine, with the goal of creating game stories for any game genre — independent of a game’s graphics.

Why Tapestry? Modern game engines – while providing amazing support for graphics and gameplay – don’t have great tools for building stories. Game scripting is usually deeply integrated into the engine. And story development, while simple at the start, quickly becomes time consuming and error prone.

Developers should be able to work on the logic of their games separate from the visual elements whenever possible. This enables quick iteration for commonplace tasks such as creating dialog, puzzles, and quests.

Tapestry aims to do just that.

Working Example

This is a port of the Cloak of Darkness. All Tapestry stories are a mixture of English descriptions, and yaml style commands. The English sections describe the game world, and the commands describe the story’s behavior.

# -----------------------------------
# The 'Cloak of Darkness' was originally written by Roger Firth.
# Its been implemented in many IF systems over the years.
# This one is ported from the Inform7 version.
---
Define scene:requires:
  - "cloak"
  - - "tapestry"  # a library of shared object types and behaviors.
    - "scoring"   # definitions used for tracking a game's score.
---
The title of the story is "The Cloak of Darkness."
The story has the headline "An example story."
The story has the max score 2.

# -----------------------------------
# The Foyer:
# -----------------------------------
The Foyer of the Opera House is a room. You are in the foyer. The description of the Foyer is "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 east."

The entrance is a scenery door in the foyer. North from the foyer is the entrance. Through the entrance is the Street. Instead of traveling through the entrance:
  - Say: "You've only just arrived and besides the weather outside is terrible."

# -----------------------------------
# The Cloakroom:
# -----------------------------------
East from the Foyer is the Cloakroom. The Cloakroom has the description "In the Opera's better days the walls of this small room must have been lined with coat hooks. Now, only one remains. The exit is a door to the west." 

# The description of the hook depends on the number of things hanging on it. This uses a special "template" syntax to achieve that.
In the Cloakroom is a scenery supporter called the small brass hook. The hook has the description "A brass hook{if children_of: .hook} with {children_of: .hook | print_inline_objects!} hanging on it{else} screwed into the wall{end}."

# Understandings change the game's input parser, and allow a player to refer to objects in new ways. Without these, a player could only have typed: > put cloak on hook.
Understand "peg" as the hook.
Understand "hang [objects] on/onto [objects]" as storing.

# -----------------------------------
# The Foyer Bar
# -----------------------------------
South from the Foyer is the Foyer Bar. The bar is unlit. The scrawled message is scenery in the bar. The description of the bar is "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."

Neatness is a kind of aspect. The neatnesses are neat, scuffed, and trampled.
Messages are a kind of thing. Messages have a neatness. The scrawled message is a message.
Understand "floor" or "sawdust" as the message.

Instead of examining the message:
  - # Objects can be referred to in commands, by starting their name with a '#'.
    Increase: "#story.score"
  - Say: "The message, {if .message.neat}neatly {end}marked in the sawdust, reads..."
  - EndGame finally:
      FromBool: true

# When multiple rules for the same action exist, later rules override earlier rules.
Instead of examining the message:
  - If:do:
    - "#message.trampled"
    - - Say: "The message has been carelessly trampled, making it difficult to read. You can just distinguish the words..."
      - EndGame saying:
          FromText: "You have lost"

Instead of going:
  - If:do:
    - Is all:
      - Is:matching:text:
        - "#story.location"
        - "equal_to"
        - "#bar"
      - "#bar.unlit"
      - Is:matching:text:
        - # Rules have variables, used to convey information about the action occurring.
          # Variables are indicated with the '@' sign. 
          # In this case, the direction the player is trying to move.
          "@direction"
        - "other_than"
        - "#north"
    - - Set:state:
        - "#message"
        - "trampled"
      - Say: "Blundering around in the dark isn't a good idea!"

# Block actions other than "going" when in the darkened bar:
Instead of running an action:
  - If:do:
    - Is all:
      - Is:matching:text:
          - "#story.location"
          - "equal_to"
          - "#bar"
      - Is:matching:text:
        - "@action"
        - "other_than"
        - "going"
      - "#bar.unlit"
    - - # Increase neatness, clamp so that it doesn't go past 'trampled'
        Increase:state:clamp:
          - "#message"
          - "neatness"
          - true
      - Say: "In the dark!? You might disturb something!"

# -----------------------------------
# The Cloak of Darkness
# -----------------------------------
You are wearing a velvet cloak. The description of the cloak is "A handsome cloak made of velvet trimmed with satin. Its blackness is so deep that it seems to suck light from the room." Understand "dark", "black", or "satin" as the cloak.

After taking the cloak:
  - Set:state:
    - "#bar"
    - "unlit"

After dropping the cloak:
  - Set:state:
    - "#bar"
    - "lit"

After storing the cloak:
  - Set:state:
    - "#bar"
    - "lit"
  - Increase: "#story.score"

Instead of dropping the cloak:
  - If:do:
    - Is:matching:text:
      - "#story.location"
      - "other_than"
      - "#cloakroom"
    - - Say: "This isn't the best place to leave such a nice piece of clothing."

Instead of storing the cloak:
  - If:do:
    - Is:matching:text:
      - "#story.location"
      - "other_than"
      - "#cloakroom"
    - - Say: "This isn't the best place to leave such a nice piece of clothing."

# -----------------------------------
# Tests for the story.
#
# Note: Tapestry files contain two formats: a plain-English format, and a command format.
# Three dashes all on one line switch between the two modes.
# The next three dashes switches to command format to define some tests.
# -----------------------------------
---
- # Dropping the cloak should give you points, but shouldn't let you win the game.
  Define test:do:
  - "cloak of dropping"
  - - StartGame actor:quietly:
      - FromText: "#player.pawn"
      - FromBool: true
    - Fabricate input: "e; take off cloak; drop cloak; w; s; x message"
    - Expect: "#story.completed"
    - Expect:
        Is:matching:num:
        - "#story.score"
        - "equal_to"
        - 1

- # Hanging the cloak on the hook. That's where it's at.
  Define test:do:
  - "cloak of perfection"
  - - StartGame actor:quietly:
      - FromText: "#player.pawn"
      - FromBool: true
    - Fabricate input: "e; take off cloak; put cloak on peg; w; s; x message"
    - Expect: "#story.completed"
    - Expect:
        Is:matching:num:
        - "#story.score"
        - "equal_to"
        - 2

- # Disturbing the message should fail the game.
  Define test:do:
  - "cloak of failure"
  - - StartGame actor:quietly:
      - FromText: "#player.pawn"
      - FromBool: true
    - Fabricate input: "s; jump; jump; n; e; take off cloak; drop cloak; w; s; x message"
    - # Game should be finished, but not completed.
      Expect: "#story.concluded"
    - # Dropping the cloak shouldn't have earned you any points.
      Expect:
        Is:matching:num:
        - "#story.score"
        - "equal_to"
        - 0

Building this with Tapestry produces a playable story and a SQLite database containing all of the objects in the game and their interactions. You can play the story here:
Play ↗

Relation to interactive fiction

The ability to develop a game without graphics is, to my mind, a lot like the original text adventures.

Tapestry is therefore inspired by the world of interactive fiction and owes a lot in particular to Inform 7. For that reason, the default game world for Tapestry attempts to provide a similar set of game rules as Inform. And, Tapestry tries to provide a similar ( if less extensive ) way to model a game world using English-like sentences.

It is not a goal to match Inform’s amazing natural language programming environment. ( Nor is it a goal to run on z-machines. ) It is however a goal to be able to play some “Inform-like” stories with similar results. The hope is to bring interactive fiction into any game genre.