Working with TinyQBN: Part 4: Generating Dynamic Passages and Cards

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 4: Generating Dynamic Passages and Cards

Most of the time, when using QBN, authors will want to create all of the possible cards and have code sort, organize, and make selections based on pre-programmed rules. However, sometimes author may want to dynamically create content based on a number of factors such as player input or events in a story.

Programmatically Adding Cards

TinyQBN comes with a built-in macro called <<addcard>> for adding a new card to the existing collection.

<<addcard "title" false>>

After the name of the macro, it accepts two arguments: the name of the passage to add and if the card should be considered “sticky”. (As a reminder, a sticky card is never discarded and always remains available to be drawn from the deck.)

There’s an immediate problem, though. The macro <<addcard>> adds a card based on the name of an existing passage. If the passage does not exist, it can’t add the card. There’s nothing to add!

We need to add passages before we can add them as cards!

Programmatically Adding Passages in SugarCube

Back in 2018, I started a video series I update occasionally called “Procedural Generation in Twine“. In the video Twine 2.2: Procedural Generation in Twine: Passage Manipulation (SugarCube 2.28), I demonstrate how to create a new object and passage to fake out SugarCube.

The following code is an improved version doing the same general things, but targeted at the latest (as of this writing) version of SugarCube.

:: Add Passage [widget]
<<widget "addpassage">>\
	<<script>>
        /*
            <<addpassage name content>>

			name - Name of passage to create
			content - Text content of created passage
        */

		// Force conversion into String for name
		let name = String(State.variables.args[0]);

		// Force conversion into String for content
		let content = String(State.variables.args[1]);

		// Get a reference to the storage area
		let storyStorage = jQuery("tw-storydata");
		
		// Get the number of passages and increase it by one
		let numberOfPassages = storyStorage.children("tw-passagedata").length + 1;
		
		// Create new 'tw-passagedata' element with PID and NAME
		// (We can set position and size to default values)
		let newPassage = jQuery('<tw-passagedata pid="' + numberOfPassages + '" name="' + name + '" tags="" position="100,100" size="100,100"></tw-passagedata>' );
		
		// Add new content to the new passage
		newPassage.text(content);
		
		// Add the new 'tw-passagedata' to the storage area
		storyStorage.append(newPassage);
		
		// Find the passage we just added in storage by name
		let passageElement = document.getElementsByName(name)[0];
		
		// Create a new Passage object
		let newPassageObj = new Passage(name, passageElement);
		
		// Add it to the Story object
		Story.passages[newPassageObj.title] = newPassageObj;
		
	<</script>>\
<</widget>>\

The above widget (a user-added macro in SugarCube) appends a new child to the tw-storydata element and then create a new Passage object based on it. In other words, it adds passages programmatically!

Adding Other Twee Files

Following along from Part 3 of this series, additional Twee files can be added to the src folder. Because Tweego will combine the content during compilation, the code can be saved as addpassage.twee.

project
├── src
│   ├── addpassage.twee
│   ├── index.twee
│   └── story-javascript.min.js
├── output

Note: As long as at least one Twee file in the folder has the required passages (StoryData, StoryTitle, and a starting passage), Tweego is fine with the files.

Adding Passages and Adding Cards

Combining the widget <<addpassage>> with the <<addcard>> macro provided by TinyQBN, passages can be created and then immediately parsed as cards.

<<addpassage "Test" "Testing">>
<<addcard "Test" false>>

Consider the following more complicated code using the <<set>> and <<for>>macros.

<<set $royals to ["Queen", "King", "Jack", "Ace"]>>

<<for _i to 0; _i lt $royals.length; _i++>>
    <<addpassage $royals[_i] $royals[_i]>>
    <<addcard $royals[_i] false>>
<</for>>

In the above code, an array is defined using four values (“Queen”, “King”, “Jack”, and “Ace”). The <<for>> macro is used to loop and increase a counter (temporary variable _i) until it is the same number as the length of the array.

Inside the <<for>> loop, the pair of <<addpassage>> and <<addcard>> are used. These add new passages and then parse them.

Adding TinyQBN Helper Widgets and CSS

TinyQBN has a number of helpful widgets for displaying cards, but they exist in a different file than its JavaScript added in part 3. To add them to the project, they need to be in another Twee file, tinyqbn-widgets.twee.

Note: The following widgets are taken from widgets.txt. To make things easier, I have packaged them as a passage with the “widget” tag. This will tell SugarCube to parse them upon story start.

:: TinyQBN Widgets [widget]
/*
 * Comma-separated lists: call with "linkto" for links instead
 * of contents.
 */

<<widget "cardlist">>\
<<includeall $args[0] `$args[1] or "content"` "comma">>\
<</widget>>

/*
 * Rows and columns of card contents in boxes: call with "linkbox"
 * for links, or "coverbox" for covers.
 */

<<widget "cardcolumn">>@@.qbn-column;
<<includeall $args[0] `$args[1] or "contentbox"`>>
@@<</widget>>

<<widget "cardrow">>@@.qbn-row;
<<includeall $args[0] `$args[1] or "contentbox"`>>
@@<</widget>>


/* Comma separator (no serial comma). */

<<widget "comma">><<if $args[0]>> and <<else>>, <</if>><</widget>>


/* Wrapper widgets (card contents or links, optionally boxed). */

<<widget "cover">>\
<<set _qbn_cover to true>><<includecard $args[0]>><<unset _qbn_cover>>\
<</widget>>

<<widget "content">><<if `QBN.available($args[0])`>>\
<<unset _qbn_cover>><<includecard $args[0]>><<removecard $args[0] false>>\
<</if>><</widget>>

<<widget "linkto">><<print '[\[' + $args[0] + ']]'>><</widget>>

<<widget "coverbox">>@@.qbn-card;
<<cover $args[0]>>
@@<</widget>>

<<widget "contentbox">>@@.qbn-card;
<<content $args[0]>>
@@<</widget>>

<<widget "linkbox">>@@.qbn-card;
<<linkto $args[0]>>
@@<</widget>>


/* Conditional Links */

<<widget "linkif">>\
<<if $args[0]>><<= '[\['+($args[2] or $args[1])+'->'+$args[1]+']]'>>\
<<else>>@@.qbn-nolink;<<= $args[2] or $args[1]>>@@<</if>>\
<</widget>>

<<widget "linkcontents">><<linkif `QBN.available($args[1] or QBN.current)` `$args[1] or QBN.current` $args[0]>><</widget>>


/* Choice helpers */

<<widget "skillcheck">>\
<<set _qbnsuccess to $args[0].check($args[1])>>\
<</widget>>

<<widget "gotoresult">>\
<<if _qbnsuccess>><<set $args[0] to $args[0] + ' ' + Success>><</if>>\
<<if State.random() < 0.20 and Story.get('Rare ' + $args[0])>>\
<<set $args[0] to 'Rare ' + $args[0]>>\
<</if>>\
<<unset _qbnsuccess>><<removecard _qbncurrent false>><<goto $args[0]>>\
<</widget>>

TinyQBN also uses CSS for displaying cards and their contents. Like the widgets, these rules are in a different file. However, for naming purposes, these can be saved in tinyqbn.css.

.qbn-card {
  background: #222;
  border: thin solid #333;
  margin: 0.25em;
  padding: 0.25em;
}
.qbn-column { display: flex; flex-direction: column; }
.qbn-row { display: flex; flex-direction: row; }

With the two files added, the structure should look like the following:

project
├── src
│   ├── addpassage.twee
│   ├── index.twee
│   ├── story-javascript.min.js
│   ├── tinyqbn-widgets.twee
│   └── tinyqbn.css
├── output

Note: Like with Twee files, Tweego will also combine all CSS files and add them to the project.

Working with Widgets

The TinyQBN widgets <<cardrow>> and <<cardcolumn>> do as their names imply: they create either a row or column based on the cards passed to them. For either, however, a special rule has to be followed. Any mention of QBN.cards() MUST be enclosed by backticks.

<<cardrow `QBN.cards()`>>

Used with just cards, <<cardrow>> and <<cardcolumn>> create text and are styled using the CSS from TinyQBN. Using either “linkbox” or “contentbox” as a second argument to either creates a link to the passage the card is based on or uses the content of the card instead of its name.

For example, to show the content of all cards, a use of <<cardrow>> might look like the following:

<<cardrow `QBN.cards()` "contentbox">>

To create links to each passage instead, it would look like the following:

<<cardrow `QBN.cards()` "linkbox">>

Example: Simple Endless Maze

In TinyQBN, all sticky cards can be selected indefinitely. They will never not be available. This means, using the <<addpassage>> macro, passages can be created to show available cards that will, with all of them being sticky, always available to visit. In other words, it is possible to create an endless maze through abusing the fact that sticky cards will always be available.

:: StoryData
{
    "ifid": "46CA9877-6E18-41E3-8EE4-59B7CE9B3F90",
    "format": "SugarCube",
    "format-version": "2.30.0"
}

:: StoryTitle
Endless Maze using QBN

:: Start [card]
<<set $directions to ["North", "East", "South", "West"]>>\
\
<<for _i to 0; _i lt $directions.length; _i++>>\
    <<addpassage $directions[_i] '<<cardrow `QBN.cards()` "linkbox">>' >>\
    <<addcard $directions[_i] true>>\
<</for>>\
\
<<cardrow `QBN.cards()` "linkbox">>

In the above code, the Start passage creates four new passages whose content uses the <<cardrow>> widget to show all available cards. Because each passage is set to sticky, they will always be available. In other words, a player can proceed any “direction” (North, South, East, and West) through clicking its link.

As far as SugarCube is concerned, each click is a different movement to another passage. However, internally, the player is only ever re-visiting the same four passages as presented by the <<cardrow>> widget.

Silly, perhaps, as an example, but a good illustration of the way QBN projects can use cards to create new, endless “links” for a player using only a handful of cards.