Shape Composition Guide

Shape Composition Guide — Advanced Interactive Fiction

Shape Composition Guide

Composing and nesting story shapes like recursive functions

1. Shapes Are Functions

You already know the ten basic shapes. Now here is the insight that unlocks everything else: each shape is a function.

A function has three things: an entry point, internal logic, and an exit point. A shape is exactly the same. A Diamond has one way in (the neck), internal branching, and one way out (the gather). A Hub-and-Spoke has one way in (the hub), internal spoke-visits, and one way out (the exit gate). Every shape, no matter how complex inside, presents the same simple interface to the outside world: arrow in, arrow out.

This means you can put any shape inside any other shape. Wherever a shape expects "something happens here," you can slot in an entire sub-shape. The outer shape does not need to know what the inner shape does -- it only needs the entry and exit to line up.

Ink Mechanic: Tunnels

In Ink, the mechanic that makes "shapes as functions" literal is the tunnel. Instead of a plain divert (-> knot), you write -> knot -> -- this calls the knot like a function. Inside that knot, ->-> means "return to wherever I was called from." This is exactly how you nest one shape inside another: the outer shape tunnels into the inner shape, and the inner shape returns when it finishes.

SHAPE SLOT ANY SHAPE diamond / gauntlet / QBN / ... internal logic is encapsulated ENTRY -> shape -> EXIT ->->
Every shape presents the same interface: one entry, one exit. The inside can be anything.

When you see a shape this way, composition becomes natural. A Hub-and-Spoke where each spoke is a Diamond? That is a function (the hub) calling three sub-functions (the diamonds). A Gauntlet where each gate uses QBN logic? That is a linear sequence of function calls, each one internally using state-based selection.

The rest of this guide gives you five concrete recipes for composing shapes, each with a diagram and working Ink code you can paste into Inky.

2. Composition Recipes

Recipe 1: Diamond inside Hub-and-Spoke

The most common composition: a central hub with gated choices, where each spoke is a self-contained Diamond (branch, explore, converge). The player keeps returning to the hub until all spokes are visited, then exits.

This is where tunnels shine. The hub tunnels into each spoke-diamond with -> spoke ->, and each diamond ends with ->-> to return to the hub. The hub does not need to know what happens inside the spoke -- it just calls and resumes.

HUB sticky -> spoke_a -> -> spoke_b -> -> spoke_c -> SPOKE A (Diamond) ->-> SPOKE B (Diamond) ->-> SPOKE C (Diamond) ->->
Macro: Hub-and-Spoke. Each spoke is a Diamond that tunnels back to the hub via ->->.
ink
VAR visited_market = false
VAR visited_shrine = false
VAR visited_docks = false

-> hub

=== hub ===
You stand in the town square. Paths lead in every direction.
+ {not visited_market} [Head to the market] -> spoke_market ->
+ {not visited_shrine} [Visit the shrine] -> spoke_shrine ->
+ {not visited_docks} [Walk to the docks] -> spoke_docks ->
+ {visited_market and visited_shrine and visited_docks} [Leave town] -> finale
- -> hub

=== spoke_market ===
The market is a riot of color and noise.
* [Browse the weapon stalls]
    You find a short blade with a chipped edge. Cheap but functional.
* [Check the food vendors]
    Dried fish and hard bread. Trail rations.
* [Talk to the old merchant]
    She tells you about strange lights on the northern road.
- ~ visited_market = true
You pocket what you need and head back to the square.
->->

=== spoke_shrine ===
Incense smoke curls around stone pillars.
* [Pray at the altar]
    A warmth fills your chest. You feel watched, but not threatened.
* [Read the inscriptions]
    The old language. Something about a seal, and a key beneath water.
* [Leave an offering]
    You set down a coin. The flame on the altar flickers blue.
- ~ visited_shrine = true
You step back into daylight.
->->

=== spoke_docks ===
Salt air, creaking wood, the cry of gulls.
* [Ask the fishermen about the northern lights]
    "Bad business," one mutters. "Fish been dying since it started."
* [Check the boats]
    One skiff has a fresh hull. Someone is planning to leave.
* [Search the warehouse]
    Crates stamped with a sigil you do not recognize.
- ~ visited_docks = true
You walk back along the harbour wall.
->->

=== finale ===
With the town explored, you take the northern road.
-> END
Key Insight

Notice how the hub uses -> spoke_market -> (tunnel syntax) instead of -> spoke_market. Each spoke ends with ->-> instead of -> hub. This means the spokes are truly self-contained functions -- they do not need to know the hub exists. You could reuse them inside a different outer shape without changing a line.

Recipe 2: Gauntlet with QBN Gates

A Gauntlet is a linear sequence of stations where you either pass or fail each gate. Standard gauntlets use simple conditions. This recipe makes each gate use QBN logic -- the available "clean" paths through each gate depend on accumulated LIST state, so the same gauntlet plays differently based on what you have collected.

GAUNTLET WITH QBN GATES LIST Traits = (charm), blade, (lore) S1 ? {charm} always S2 ? {blade} always S3 entry exit Clean path (state-gated) Costly path (always open)
Each gate checks LIST membership. Different state combinations unlock different clean paths.
ink
// In a real story, traits would be earned from earlier scenes.
// Parens = "starts in the set." Blade is defined but not yet earned.
LIST Traits = (charm), blade, (lore)
VAR health = 3

-> station_1

=== station_1 ===
The road narrows to a bridge. A toll collector blocks the way.
* {Traits ? charm} [Charm your way past]
    "Well aren't you a delight." She waves you through with a grin.
    -> station_2
* {Traits ? blade} [Flash your weapon]
    She steps aside without a word. Professional courtesy.
    -> station_2
* [Pay the toll] // always available
    You hand over your last coin. She bites it and nods.
    -> station_2
* [Try to shove past] // costly: always available
    She trips you and takes two coins. You limp onward.
    ~ health = health - 1
    -> station_2

=== station_2 ===
A collapsed section of road. Rubble everywhere.
* {Traits ? lore} [Recall the old shortcut from the archives]
    The side path is right where the map said. Clean and easy.
    -> station_3
* {Traits ? blade} [Hack through the undergrowth]
    Slow work, but your blade makes it possible.
    -> station_3
* [Climb over the rubble] // costly: always available
    You scrape your hands raw but make it through.
    ~ health = health - 1
    -> station_3

=== station_3 ===
The final gate: an iron door set into the hillside.
* {Traits ? lore} [Read the inscription and speak the passphrase]
    The door swings open silently.
    -> gauntlet_end
* {Traits ? charm} [Convince the guard behind the slot]
    "Password?" "Would you believe I forgot?" A pause. The bolt slides.
    -> gauntlet_end
* [Force the door] // costly: always available
    You throw your weight against it. Something in your shoulder pops.
    ~ health = health - 1
    -> gauntlet_end

=== gauntlet_end ===
{health >= 3:You emerge unscathed. Every gate had a clean answer.}
{health == 2:Battered but standing. One gate cost you.}
{health == 1:Barely alive. The gauntlet took its toll.}
{health <= 0:You collapse just past the threshold. But you made it.}
-> END
Design Note

The QBN layer means this gauntlet has high replay value. A player with charm and lore experiences a very different run than one with blade alone. The gauntlet feels different each time, but the structure is always linear. The shape (gauntlet) and the content-selection (QBN) are independent layers.

In this example, (charm) and (lore) start in the set (parentheses = initialized), so those gates are open. blade is defined but not yet earned -- its gates stay locked. In a full game, traits would be earned from earlier scenes via ~ Traits += blade.

Recipe 3: Branch-and-Bottleneck Spine with Storylet Chapters

The macro structure is a branch-and-bottleneck with three acts. Each act is a Diamond: choices fan out, then converge to a bottleneck before the next act begins. But within each diamond branch, the specific content is selected via QBN -- which scenes you actually read depend on variables accumulated from previous acts.

This gives you the best of both worlds: clear dramatic structure (three-act diamond spine) with high replayability (QBN-selected content within each branch).

ACT 1 (Diamond) QBN QBN bottleneck ACT 2 (Diamond) QBN QBN bottleneck ACT 3 (Diamond) QBN QBN bottleneck = QBN-selected content (state picks which text you see)
Three-act diamond spine. Branch content within each act is QBN-selected based on accumulated state.
ink
VAR reputation = 0
VAR has_artifact = false
VAR knows_secret = false

-> act_1

=== act_1 ===
The council chamber. Two factions demand your attention.
* [Side with the merchants]
    ~ reputation = reputation + 1
    {knows_secret:
        You leverage what you know. The merchants owe you double now.
        ~ reputation = reputation + 1
    }
    {not knows_secret:
        A straightforward deal. Coin for loyalty.
    }
    The merchants nod. Commerce will flow.
* [Side with the scholars]
    ~ knows_secret = true
    {reputation > 0:
        Your standing makes the scholars trust you faster.
        They share the deep archives, not just the public ones.
        ~ has_artifact = true
    }
    {reputation <= 0:
        The scholars accept you cautiously. Basic access only.
    }
    Knowledge is its own currency.
- // Act 1 bottleneck
The council adjourns. Word of your choice spreads.
-> act_2

=== act_2 ===
A month later. The northern border is under threat.
* [Lead the defense yourself]
    ~ reputation = reputation + 1
    {has_artifact:
        The artifact hums in your pack. The enemy scouts
        turn back before they even reach the wall.
    }
    {not has_artifact:
        Hard fighting. You hold the line, but barely.
    }
    The border holds. Your name is spoken with respect.
* [Send diplomats instead]
    {knows_secret:
        Your knowledge of the enemy's internal politics
        gives the diplomats exactly the right leverage.
    }
    {not knows_secret:
        The diplomats do their best. A fragile truce.
    }
    ~ knows_secret = true
    Peace, for now. But peace has a price.
- // Act 2 bottleneck
Winter comes. The real test approaches.
-> act_3

=== act_3 ===
The throne room. A decision that cannot be undone.
* [Claim the throne]
    {reputation >= 2:
        The crowd cheers. You have earned this.
    }
    {reputation == 1:
        Mixed reactions. You have supporters, but also doubters.
    }
    {reputation <= 0:
        Silence. You sit on a throne no one wanted you to have.
    }
    {has_artifact: The artifact gleams on the armrest. A symbol of legitimacy.}
    The crown is heavy.
* [Refuse the throne]
    {knows_secret:
        You reveal what you know. The truth changes everything.
        A better candidate emerges from the chaos.
    }
    {not knows_secret:
        You step aside with grace. The council will choose.
    }
    {reputation >= 2: They beg you to reconsider. You hold firm.}
    Freedom is its own crown.
- // Act 3 bottleneck: converged ending
The story ends, but the shape of it was yours.
-> END
Two Layers Working Together

The diamond structure is visible in the choices and gathers -- two options fan out, then converge at the bottleneck. The QBN layer is visible in the conditional text blocks inside each branch -- {has_artifact: ...} and {reputation > 0: ...} select different scene content based on state. Same structure, different content every playthrough.

Recipe 4: Recursive Diamond (Diamond within Diamond)

A Diamond where one of the branches is itself a Diamond. The outer diamond asks "what do you do?" and one answer leads to a sub-decision with its own branching and convergence before rejoining the outer gather.

This is useful when one choice is genuinely more complex than the others -- it deserves exploration, not just a single line of text. The tunnel mechanic keeps it clean: the outer diamond tunnels into the inner diamond, the inner diamond converges and returns.

OUTER DIAMOND neck A C INNER DIAMOND (B) -> inner -> b1 b2 b3 ->-> foot
Branch B of the outer diamond is itself a diamond. Tunnel in, converge, tunnel out.
ink
VAR found_clue = false

-> outer_diamond

=== outer_diamond ===
The detective surveys the crime scene. Three leads.
* [Check the victim's pockets]
    A receipt from a bar downtown. Timestamped last night.
* [Examine the locked room] -> inner_diamond ->
* [Interview the neighbor]
    "I heard shouting around midnight. Two voices, maybe three."
- // Outer gather
{found_clue: A piece of the puzzle clicks into place.}
The detective pockets the notebook and heads to the precinct.
-> END

=== inner_diamond ===
The room is sealed from the inside. How?
* [Check the windows]
    One window has a fresh scratch on the latch. Opened recently,
    then closed again from outside with a wire.
    ~ found_clue = true
* [Examine the lock mechanism]
    Standard deadbolt. No sign of picking. But the strike plate
    has been shimmed -- the door could close without locking.
    ~ found_clue = true
* [Look at the ventilation]
    The vent cover is loose. Too small for a person, but not
    for a hand reaching through to turn the deadbolt.
    ~ found_clue = true
- // Inner gather
The locked room is not so locked after all.
->->
When to Use This

Recursive diamonds work when one branch genuinely has more depth than the others. If all three branches need sub-diamonds, you probably want Hub-and-Spoke instead (Recipe 1). Two levels of nesting is usually the maximum before the reader loses the thread. If you need three levels, reconsider your structure.

Recipe 5: Loop-and-Grow feeding a Foldback

A Loop-and-Grow is a location you keep visiting -- each time, new options appear based on what you have done before. Sticky choices (+) mean old options stay available; once-only choices (*) get consumed. State accumulates across visits.

A Foldback is a dramatic scene where the path differs based on your history, but the outcome converges to a single point. The loop feeds the foldback: what you did in the loop determines how the foldback plays out.

LOOP-AND-GROW tavern + drink + listen * gamble * fight trust++ / rumors++ / goodwill++ BREAK: visits >= 3 FOLDBACK {high trust} {low trust} end same outcome, different path
Loop accumulates state, then the foldback reads that state to vary the path to a converged outcome.
ink
VAR trust = 0
VAR rumors = 0
VAR goodwill = 0
VAR visits = 0

-> tavern

=== tavern ===
~ visits = visits + 1
{visits == 1: You push through the door. Smoke and noise.}
{visits == 2: The barkeep nods. You're becoming a regular.}
{visits >= 3: The usual seat is empty. Waiting for you.}

+ [Buy a round for the house]
    ~ goodwill = goodwill + 1
    ~ trust = trust + 1
    Cheers erupt. A few strangers raise their mugs your way.
    -> tavern_check
+ [Sit in the corner and listen]
    ~ rumors = rumors + 1
    Fragments of conversation drift over. The old mine. The missing girl.
    {rumors >= 2: A pattern is forming in what you hear.}
    -> tavern_check
* [Challenge the scarred man to cards]
    ~ goodwill = goodwill + 1
    He grins. You win a hand, lose two. But you learn how he thinks.
    ~ trust = trust + 1
    -> tavern_check
* [Break up the bar fight]
    ~ trust = trust + 2
    You pull them apart. The smaller one thanks you. The bigger one glares.
    -> tavern_check

= tavern_check
{visits >= 3:
    The door slams open. "They found a body at the mine."
    -> foldback
}
The night wears on.
-> tavern

=== foldback ===
You follow the crowd to the mine entrance. Torchlight. Silence.
{trust >= 3:
    The miners let you through. "You're alright," one says.
    Inside, you find the foreman, pale and shaking.
    "She went down there. Three days ago. I should have said something."
    {rumors >= 2:
        The fragments connect. You know which shaft. You know why.
        "Follow me," you say, and they do.
    }
    {rumors < 2:
        You do not know enough. But you go anyway.
        The foreman leads. You follow blind.
    }
}
{trust < 3:
    The miners block the entrance. "Who are you?"
    {goodwill >= 2:
        You call in favors. Enough goodwill to get waved through.
        The miners step aside. You enter alone.
    }
    {goodwill < 2:
        You argue. They shove you back.
        You find another way in -- the old ventilation shaft,
        crawling through dust and dark.
    }
}
- // Foldback convergence
Either way, you reach the bottom. Either way, you find her.
The path was different. The destination was always the same.
-> END
Sticky vs Once-Only in Loops

The loop uses + (sticky) for repeatable actions like drinking and listening, and * (once-only) for unique events like the card game and the fight. This means early visits have more options, and later visits narrow down -- the loop naturally evolves without you writing special logic for each visit.

3. The Meta-Pattern

Here is the thing that makes composition click: the same shapes recur at every zoom level.

The Story Structure Atlas (the companion to this guide) demonstrates this directly. Its macro structure is a QBN -- accumulated state from Act 1 and Act 2 determines which ending you reach in Act 3. But zoom in:

The macro shape (QBN) contains acts. Each act is a different shape. Each shape might contain sub-shapes. The same ten building blocks combine and recombine at every scale, just as a few functions compose into programs of arbitrary complexity.

Composition Cheat Sheet

Not every combination works equally well. Here is a practical grid. Rows are the outer shape, columns are the inner shape. Each cell says whether the pairing works and why (or why not).

Outer / Inner Diamond Gauntlet QBN Hub-Spoke Loop Foldback
Hub-and-Spoke ✓ classic ✓ trial spokes ✓ storylets ✗ confusing ~ needs exit ✓ climactic spoke
Diamond ✓ sub-decision ~ rare ✓ rich branches ✗ pacing ✗ trap ~ one branch
Gauntlet ✓ gate choices ✗ redundant ✓ smart gates ✗ breaks flow ✗ stalls ~ final gate
Loop-and-Grow ✓ visit events ~ special event ✓ natural fit ✗ lost ✗ maze ✓ classic exit
QBN ✓ storylet shape ✓ challenge lot ~ opaque ✓ exploration ~ careful ✓ dramatic end
Foldback ✓ path variety ✓ trial paths ✓ history matters ✗ dilutes ✗ kills urgency ✗ redundant
Reading the Grid

= works well, with a one-phrase explanation of why. ~ = possible but requires care. = usually a bad idea, with a reason. The most reliable pairings: Diamond inside anything, QBN as a content layer inside anything, Foldback as a climactic exit from anything.

4. Vibe-Coding Composed Shapes

When using AI to help generate interactive fiction, specificity beats vagueness. These prompt templates encode the composition patterns from this guide so the AI knows exactly what structure you want.

Prompt 1: Hub-and-Spoke with Inner Shapes
Build me a hub-and-spoke in Ink where each spoke uses [SHAPE].

The hub is [LOCATION]. It tracks [STATE VARIABLE] across visits.
Use sticky choices (+) for the hub so the player returns after each spoke.
Use tunnels (-> spoke ->) so spokes are self-contained.

Spoke topics:
1. [TOPIC A] — uses [SHAPE] internally
2. [TOPIC B] — uses [SHAPE] internally
3. [TOPIC C] — uses [SHAPE] internally

Exit condition: [WHAT TRIGGERS THE EXIT].
After all spokes, divert to [FINALE KNOT].
Prompt 2: Multi-Act Branch-and-Bottleneck
Create a 3-act branch-and-bottleneck story in Ink.
Each act is a diamond: 2-3 choices that converge to a bottleneck.

Inside each act, use [SHAPE] to select content. For example,
use conditional text blocks ({variable: text}) to vary
what the player reads based on accumulated state.

Track these variables across all acts:
- [VARIABLE 1] (type: [number/boolean/LIST])
- [VARIABLE 2] (type: [number/boolean/LIST])

Act 1: [SETTING/SITUATION]
Act 2: [SETTING/SITUATION]
Act 3: [SETTING/SITUATION]

The bottleneck between acts should feel like a natural
scene transition, not an arbitrary reset.
Prompt 3: Generic Nested Composition
I need a [OUTER SHAPE] where the [COMPONENT] is a [INNER SHAPE].

Outer shape: [OUTER SHAPE NAME]
- Entry state: [WHAT THE PLAYER KNOWS/HAS WHEN ENTERING]
- Exit state: [WHAT SHOULD BE TRUE WHEN LEAVING]

Inner shape: [INNER SHAPE NAME]
- Placed at: [WHERE IN THE OUTER SHAPE — e.g., "spoke 2", "branch B", "gate 3"]
- Uses tunnel syntax: -> [inner_knot] -> with ->-> return
- Internal choices: [NUMBER] options
- Modifies: [WHAT VARIABLES IT CHANGES]

Ensure all paths through the inner shape converge before
returning to the outer shape. Use gathers (-) for convergence.
Why Templates Work

These templates work because they give the AI three things it needs: the structural pattern (which shapes, where), the state contract (what variables flow in and out), and the content seeds (topics, settings, situations). Without the structure, you get a formless story. Without the state contract, you get shapes that do not connect. Without the content seeds, you get generic placeholder text.

Pause & Reflect

Composition is a worldview. When you nest a diamond inside a hub-and-spoke, you’re saying “big choices contain smaller choices.” When you put a gauntlet inside a QBN, you’re saying “your identity determines your challenges.” Look at the 5 recipes above. Which composition pattern matches a system you interact with in real life? (School, healthcare, social media, hiring...)

Recursion is power — and responsibility. If shapes can contain other shapes infinitely, who decides when to stop nesting? The author. That means you decide how much complexity a reader faces. When is deep nesting a gift (richness, discovery) and when is it a trap (confusion, overwhelm)? Think about a time you felt lost in a system with too many layers. What would simplifying the shape have changed?

What composition would you never see in a commercial game — but should exist? Most shipped games use branch-and-bottleneck because it’s efficient. But what if your story’s meaning requires a composition that’s “wasteful” — content most players will never see? Is that waste, or is it generosity? What story would justify building something most readers will only partially experience?

Ink Syntax Quick Reference

=== knot ===Define a knot (major section)
= stitchDefine a stitch (sub-section within a knot)
* [Choice text]Once-only choice (consumed after use)
+ [Choice text]Sticky choice (remains available)
-Gather (convergence point)
-> knotDivert (go to knot, one-way)
-> knot ->Tunnel (call knot like a function)
->->Return from tunnel (like "return" in a function)
-> ENDEnd the story
VAR x = 0Declare a global variable
~ x = x + 1Assignment / mutation
{condition: text}Conditional text (inline)
{x > 3: text}Conditional with comparison
* {condition} [Choice]Gated choice (only shown if condition is true)
LIST States = a, b, cDefine a LIST (set of flags, file-scope only)
~ States += aAdd a value to a LIST
{States ? a}Test if LIST contains a value (also: States has a)
{first|second|third}Sequence (shows first, then second, then third, stays on third)
{&first|second|third}Cycle (loops: first, second, third, first, ...)
{~first|second|third}Shuffle (random order each time)
{!first|second|third}Once-only (shows each in order, then blank)