Working with TinyQBN: Part 2: Writing QBN Stories Using Twine 2 Example

Working with TinyQBN

TinyQBN is a JavaScript library and set of SugarCube widgets created by Joshua Grams for working with quality-based narrative (QBN) structures in Twine 2.

Part 2: Writing QBN Stories Using Twine 2 Example

Notation Issue: Twine 2 and Twee

Twine 2 only produces HTML. This makes trying to examine the structure of a Twine 2 story very difficult, as its parts are encoded within HTML elements and attributes. In Twine 1, there were two output formats: HTML and another called Twee. While technically invented before Twine 1 was created, Twee was dropped as an input and output format option with the transition to Twine 2 (Cox, 2019). However, many authors have continued to use it with tools called twee compilers like Tweego (Edwards, 2020) and Extwee (Cox, 2019) that combine story formats and Twee code into HTML files. To help with converting an existing story in Twine 2, McCollum (2015) and de Marco (2017) have also created story formats that convert HTML from within Twine 2 into Twee (1) code.

Because of the difficulty in parsing HTML to understand a story, the Twine Cookbook established the use of Twee code examples to represent passages and stories for greater readability and accessibility (Twine Committee, 2017). This guide follows the convention established by the editors of the Twine Cookbook of representing stories and passages using the Twee 3 Specification instead of HTML (Twine Committee, 2019).

This preface serves as an explanation and warning. While Twee is easier to understand, there is no easy way to convert its data into a format Twine 2 can understand outside of HTML. Part 3 will cover working with Twee compilers more at length, but this part will represent passages using Twee instead of its HTML equivalent.

Revisiting Card, Hand, and Deck

In Part 1, the metaphor of a card was introduced. In TinyQBN, a passage is considered a card when its tags contains the String value “card” or “sticky-card”.

Example Passage-Card

:: Erista [card]
I'm a card!

Once “card” or “sticky-card” are added, other expressions can also be used as additional tags to a passage. These are used with qualities (values) to determine a card’s availability.

Comparison Expressions

Comparison expressions are written using a version of the conditional statement shorthand used by SugarCube.

ShorthandComparison Operation
eq==
neq!=
lt<
gt>
lte<=
gte>=
eqvarvariable == variable
neqvarvariable != variable
ltvarvariable < variable
gtvarvariable > variable
ltevarvariable <= variable
gtevarvariable >= variable
Comparison Expression Notation

Example Passage-Card with Comparison Expression

:: Start
<<set $rubies to 3>>

:: Erista [card rubies-eq-3]
I'm a card!

Requirement Expressions

To check if a variable exists, the expression pattern “req-variable” is also possible. In TinyQBN, this can be used to test for Boolean variables (regardless of value) or for the presence of a variable in the story without comparing its value to another.

Example Passage-Card with Requirement Expression

:: Erista [card req-rubies]
The kingdom of Erista.

Availability

A card is considered available if all of its expressions evaluate to true.

The method QBN.available(cardName) can be used to check if an individual card is available or not based on its name. When cards are generated based on passage names (if they have the “card” or “sticky-card” tag) when the story starts, these can be used to check the passage-card’s availability.

Drawing

TinyQBN considers all cards in a story to be part of a larger pool called a deck. During a process called drawing, cards are selected from the deck based on their current availability.

When drawn, cards can be placed in a collection called a hand. In TinyQBN and other libraries, a hand is a subset of a deck up to a particular size. In the cases of dozens or more cards as part of a story, this allows for working with a smaller collection, if wanted.

The method QBN.cards(limit) in TinyQBN returns an Array of random passage-cards names as drawn from the story deck to a size based on limit.

Converting Branching Patterns to QBN

The following examples are based on the TinyQBN Project Generator, which generates Twine 2 HTML files with the correct Story JavaScript and SugarCube widgets pre-loaded. Once created, these can be imported directly into Twine 2 using its “Import From File” functionality from the Story Listing screen.

The “TinyQBN widgets” passage is not included in the following Twee code examples. Its inclusion should be assumed whenever widget functionality is mentioned.

Location as Card

One of the most common narrative structures using passages in Twine is to have each be a location in a story. For example, a central room could have links to passages for rooms for each cardinal direction.

Example Central-Location Passages

:: CentralRoom
[[North]]
[[East]]
[[South]]
[[West]]

:: North

:: South

:: West

:: East

In Twine 2, such a branching, location-based structure is very common. A player would arrive at the Central Room passage and then branch off to one of the other rooms.

However, in order to give a player access to each room from the others, links would need to be included in every single location to connect it to all the others.

Example Inter-Location Passages

:: CentralRoom
[[North]]
[[East]]
[[South]]
[[West]]

:: North
[[CentralRoom]]
[[East]]
[[South]]
[[West]]

:: South
[[CentralRoom]]
[[North]]
[[East]]

[[West]]

:: West
[[CentralRoom]]
[[North]]
[[East]]
[[South]]

:: East
[[CentralRoom]]
[[North]]

[[South]]
[[West]]

Beginning to translate this branching structure into a card-based one would require two major changes: (1) disconnecting all links and (2) converting each into a passage-card.

Example Location Passages-Cards

:: CentralRoom [card]


:: North [card]


:: South [card]


:: West [card]


:: East [card]

To help with listing cards, TinyQBN provides a useful widget called <<cardrow>>. It accepts two arguments: what cards to show and how to show them. The method QBN.cards(limit) returns all available cards.

TinyQBN defines two different ways to show cards:

  • contentbox: used for showing a cover, if a card has one
  • linkbox: creates a button-like visual for each card

Example Location Passages-Cards Using QBN.cards()

:: CentralRoom [card]
<<cardrow `QBN.cards()` "linkbox">>

:: North [card]
<<cardrow `QBN.cards()` "linkbox">>

:: South [card]
<<cardrow `QBN.cards()` "linkbox">>

:: West [card]
<<cardrow `QBN.cards()` "linkbox">>

:: East [card]
<<cardrow `QBN.cards()` "linkbox">>

Using cards instead of links, each passage can be accessed from any other using the widget <<cardrow>>. However, TinyQBN only considers a card available if it has not be used previously within a current session. In other words, in the new code, visiting a location would remove its availability.

If this was not desired, each could be updated with “sticky-card”, signaling to TinyQBN that its expressions should continue to be checked for availability even after being used.

Example Sticky Card Location Passages

:: CentralRoom [sticky-card]
<<cardrow `QBN.cards()` "linkbox">>

:: North [sticky-card]
<<cardrow `QBN.cards()` "linkbox">>

:: South [sticky-card]
<<cardrow `QBN.cards()` "linkbox">>

:: West [sticky-card]
<<cardrow `QBN.cards()` "linkbox">>

:: East [sticky-card]
<<cardrow `QBN.cards()` "linkbox">>

The method QBN.cards(limit) returns cards through random selection. This means that each usage generates a new, random Array of available cards. To provide more consistency, these can be sorted: <<cardrow `QBN.cards().sort(QBN.alphabetically)` “linkbox”>>

Example Sticky Card Location Sorted Passages

:: CentralRoom [sticky-card]
<<cardrow `QBN.cards().sort(QBN.alphabetically)` "linkbox">>

:: North [sticky-card]
<<cardrow `QBN.cards().sort(QBN.alphabetically)` "linkbox">>

:: South [sticky-card]
<<cardrow `QBN.cards().sort(QBN.alphabetically)` "linkbox">>

:: West [sticky-card]
<<cardrow `QBN.cards().sort(QBN.alphabetically)` "linkbox">>

:: East [sticky-card]
<<cardrow `QBN.cards().sort(QBN.alphabetically)` "linkbox">>

Finally, within the new code, every location, including the current one, would be available. Players would rarely need to travel to a location they are current at, so a new quality, location is needed to check.

A new requirement for each card is that location not be equal to current passage name. This may seem strange, but it create a rule that travel to the current passage is not possible through using, in SugarCube, the function passage().

Example Sticky Card Location Sorted Passages Excluding Current One

:: CentralRoom[sticky-card req-location-neq-CentralRoom]
<<set $location to passage()>>
<<cardrow `QBN.cards().sort(QBN.alphabetically)` "linkbox">>

:: North[sticky-card req-location-neq-North]
<<set $location to passage()>>
<<cardrow `QBN.cards().sort(QBN.alphabetically)` "linkbox">>

:: South[sticky-card req-location-neq-South]
<<set $location to passage()>>
<<cardrow `QBN.cards().sort(QBN.alphabetically)` "linkbox">>

:: West[sticky-card req-location-neq-West]
<<set $location to passage()>>
<<cardrow `QBN.cards().sort(QBN.alphabetically)` "linkbox">>

:: East[sticky-card req-location-neq-East]
<<set $location to passage()>>
<<cardrow `QBN.cards().sort(QBN.alphabetically)` "linkbox">>

Note: Twine 2 encodes tags as part of attributes in HTML and uses spaces as a delimiter. In other words, individual tags cannot contain spaces. Twine 2 automatically converts any spaces into hyphens.

The existing code can be optimized through using the <<include>> macro in SugarCube, moving the repetitive code into its own passage.

Optimized Example Sticky Card Location Sorted Passages Excluding Current One

:: CentralRoom[sticky-card req-location-neq-CentralRoom]
<<include "LocationCheck">>

:: North[sticky-card req-location-neq-North]
<<include "LocationCheck">>

:: South[sticky-card req-location-neq-South]
<<include "LocationCheck">>

:: West[sticky-card req-location-neq-West]
<<include "LocationCheck">>

:: East[sticky-card req-location-neq-East]
<<include "LocationCheck">>

:: LocationCheck
<<set $location to passage()>>\
<<cardrow `QBN.cards().sort(QBN.alphabetically)` "linkbox">>\

Locations and Keys

A common branching structure is to use a “key” to access a new location in a story. For example, consider the following code taken from the Lock and Key (Variable) entry in the Twine Cookbook.

Example Location and Key in SugarCube with Variable

:: StoryTitle
Lock and Key: Variable in SugarCube

:: Start
<<set $key to false>>

Rooms:
[[Back Room]]
[[Front Room]]

:: Back Room
<<if $key is false>>
    Items:
    <<linkreplace "Pick up the key">><<set $key to true>>You have a key.<</linkreplace>>
<<else>>
    There is nothing here.
<</if>>

Rooms:
[[Front Room]]

:: Front Room
<<if $key is true>>
    [[Exit]]
<<else>>
    Locked Door
<</if>>

Rooms:
[[Back Room]]

:: Exit
You found the key and went through the door!

In the above code, the passages Back Room and Front Room link to each other. The passage Exit is only accessible if a key is picked up.

Converted into QBN terms, the passages Back Room and Front Room could become cards. So too could Exit, with a requirement that $key is a certain value.

Example Location and Key Using Cards

:: StoryTitle
Lock and Key: Variable in SugarCube

:: Start
<<set $key to false>>
<<include "LocationCheck">>

:: Front-Room [sticky-card req-not-passage-Front-Room]
<<include "LocationCheck">>

::Back-Room [sticky-card req-not-passage-Back-Room]
<<include "LocationCheck">>

<<if $key>>
	There is nothing here.
<<else>>
	Items:
	<<linkreplace "Pick up the key">><<set $key to true>>You have a key.<</linkreplace>>
<</if>>

:: LocationCheck
<<cardrow `QBN.cards().sort(QBN.alphabetically)` "linkbox">>\

:: Exit [sticky-card req-key req-passage-Front-Room]
You found the key and went through the door!

:: Locked-Door [sticky-card req-not-key req-passage-Front-Room]
It won't open.

[[Continue->Front-Room]]

Update: Josh Grams added the above, updated code. It uses the “passage” form of expressions to test if the current passage is another value. In this code, each location now also checks if the currently shown passage is itself.

References

Cox, D. (2019). An Oral History of Twee. Digital Ephemera. Retrieved from https://videlais.com/2019/06/08/an-oral-history-of-twee/

Cox. D. (2019). Extwee. GitHub. Retrieved from https://github.com/videlais/extwee

Edwards, T. M. (2020). Tweego. Retrieved from https://www.motoslave.net/tweego/

de Marco, M. C. (2017). About Entwee. Retrieved from https://www.mcdemarco.net/tools/entwee/

McCollum, M. (2015). Entweedle. Retrieved from http://www.maximumverbosity.net/twine/Entweedle/

Twine Committee. (2017). Twine Cookbook. Retrieved from https://twinery.org/cookbook/

Twine Committee. (2019). Twee 3 Specification. Twine-Specs. GitHub. Retrieved from https://github.com/iftechfoundation/twine-specs/blob/master/twee-3-specification.md