Sinks, Softlocks, and Escape Hatches

Valid Sinks (Intentional Endpoints)

A sink node is an intentional structural endpoint defined by the author. Sinks represent narrative completion—they may be “good” or “bad” endings, but they are complete.

Characteristics:

  • Explicitly marked in structural domain: domain.sink_nodes

  • Has rendering content (death scene, victory text, etc.)

  • May trigger achievements, statistics, replay prompts

  • Represents fulfilled author intent

Example: Death as Valid Sink:

.. code-block:: yaml

dragon_encounter:
  choices:
    - fight_with_sword:
        requires: has_sword
        leads_to: dragon_battle
    - fight_barehanded:
        requires: null  # Always available
        leads_to: heroic_death  # Sink node
        metadata:
          achievement: "Foolish Bravery"
          can_replay: true

The player chose certain death. This is narratively complete and satisfying (in a dark way). The author intended this path.

Softlocks (Unintentional Dead Ends)

A softlock occurs when no forward progress is possible due to unsatisfied requirements and no valid sink is reachable.

Characteristics:

  • NOT marked as sink (unintentional)

  • No valid outgoing edges satisfy requirements

  • No rendering content (structural gap)

  • Represents authoring error or unexpected state

Example: Accidental Softlock:

.. code-block:: yaml

dragon_cave:
  choices:
    - fight_dragon:
        requires: has_sword  # Player doesn't have sword
        # No other options!

Player is stuck. No death scene, no content, no way forward. This is a bug.

Prevention Strategy

Forward progress guarantee:

At every non-sink node, at least one of the following must be true:

1. At least one outgoing edge is currently satisfiable
2. At least one requirement can be provisioned within narrative rules
3. A reset affordance is available as escape hatch

PLANNING phase responsibilities:

.. code-block:: python

def ensure_forward_progress(cursor, domain):
    # Check 1: Any edges currently satisfiable?
    if has_satisfiable_edge(cursor):
        return True
    
    # Check 2: Can we provision to satisfy an edge?
    if can_provision_for_any_edge(cursor, domain):
        provision_and_mark_available(cursor)
        return True
    
    # Check 3: Is reset allowed?
    if domain.allows_reset:
        provision_reset_affordance(cursor)
        return True
    
    # True softlock: fail loudly
    raise SoftlockError(f"No forward progress from {cursor}")

Escape Hatches: Reset Affordances

Like “unstuck” commands in 3D games, reset affordances provide emergency exits from softlock situations.

Implementation:

.. code-block:: python

class ResetAffordance(Affordance):
    """Emergency escape hatch for stuck players."""
    
    def available(self, ns: NS) -> bool:
        # Only show if no other valid choices
        cursor = ns["cursor"]
        other_choices = [
            e for e in cursor.edges_out(ChoiceEdge)
            if e.available(ns) and e != self
        ]
        return len(other_choices) == 0
    
    def execute(self, ctx: Context) -> Node:
        # Return to last checkpoint
        return ctx.graph.get_last_checkpoint()

When to use:

  • Testing/debugging: always enable during development

  • Published stories: use sparingly, signals authoring gap

  • Procedural content: may be necessary due to generation limits

  • Player agency: some authors embrace “you can always restart”

Author control:

.. code-block:: yaml

chapter_domain:
  softlock_prevention:
    allow_reset: true
    checkpoint_strategy: "scene_entry"  # or "manual_save"
    reset_message: "Return to cave entrance?"

Design Philosophy

Intentional failure is narrative: Death, capture, betrayal—these are sinks, not softlocks. They complete a story arc (even if tragic).

Unintentional blocking is a bug: “You need a sword but can’t get one” is a softlock. This breaks flow and should never ship.

The superposition view: The fabula contains all possible threads. Some threads end in sinks (intentional). Softlocks are threads that end in void (unintentional). Planning ensures every thread from source→{any sink} is navigable.

See Also

  • :ref:resolution-frontier – Forward-looking planning

  • :ref:structural-domains – Chapter/scene/book stacking

  • :ref:dependency-resolution – Provisioning strategies