do() - Action Construct

← Back to Index

Overview

The do() construct is the fundamental building block of NDL. It describes an action performed by an entity in the game world.

Syntax

do($entity, "action_description")

Parameters

ParameterTypeRequiredDescription
entityEntity ReferenceYesThe entity performing the action (with $ prefix)
action_descriptionStringYesNatural language description of the action

Entity Reference Syntax

Entities are referenced with the $ prefix:

  • $"player" - Named player entity
  • $"you" - Second-person reference
  • $"guard" - Named NPC
  • $"goblin" - Named creature
  • $ - Implicit entity (context-dependent)

Examples

Basic Usage

do($"player", "attack")

LLM Output: “You attack with determination.”

Detailed Action Description

do($"you", "swing makeshift club against table's surface")

LLM Output: “You raise the gnarled branch, hefting it awkwardly in your hand. You bring it back over your head, aiming for the tabletop. But as you swing, your momentum falters. The branch veers off course, catching on the edge of the chair.”

With Intention Property

do($"you", "swing makeshift club against table's surface", intention="test club's combat readiness")

LLM Output: “You raise the makeshift club, testing its weight and balance. You bring it down against the table’s surface to gauge its durability.”

Combined with Modifiers

With Manner Modifier (~)

do($"player", "walk") ~ "carefully"

LLM Output: “You carefully make your way forward, watching each step.”

do($"player", "attack") ~ "sword"

LLM Output: “You swing your sword in a wide arc.”

With Properties

do($"player", "speak") intention="intimidate"

LLM Output: “You speak with a menacing tone, your words calculated to strike fear.”

Chaining Actions

do($"player", "attack") -> do($"player", "follow up")

LLM Output: “You attack fiercely, then immediately follow up with another strike.”

Real-World Examples from Screenshots

Example 1: Search Action

From the Gemma 2 test screenshot:

NDL:

search("pile of wood", intention="find makeshift club") -> result()

LLM Output: “Your fingers sift through the rough-hewn logs, searching for something sturdy and manageable. You find it - a thick, gnarled branch, its ends still bearing the marks of the axe. It’s not much, but it’s better than nothing.”

Example 2: Failed Action with System Response

NDL:

do($"you", "swing makeshift club against table's surface", intention="test club's combat readiness")
-> result("failure")
-> system_response("something goes wrong when you try to perform this action")
-> describe()

LLM Output: “You raise the gnarled branch, hefting it awkwardly in your hand. It’s heavier than you anticipated, the rough bark digging into your palm. You bring it back over your head, aiming for the tabletop. But as you swing, your momentum falters. The branch veers off course, catching on the edge of the chair.

With a sickening crack, the makeshift club splinters, breaking into several pieces. The impact sends a shower of splinters flying, scattering across the floor. You stare down at the useless fragments in your hand, your heart sinking.”

Design Philosophy

What, How, Why Structure

From veritasr [04:24]:

“There’s also a sort of recipe on the input, which is the same one you use as a GM for people who don’t know how to play TTRPGs:

  • what (do you do) → do(action)
  • how (do you do it) → ~ manner
  • why (are you doing it) → intention="reason"

The do() construct answers the “what” question.

LLM as Narrator

From veritasr [04:09]:

“Backend processes input and runs logic deciding the result. Then it dynamically generates the NDL prompt, passing in the action chain to the LLM for processing. The end result is what you see here. Basically narrative text.”

The do() construct tells the LLM what happened, not what should happen. The game logic has already made all decisions.

Common Patterns

Combat Actions

do($"player", "attack") ~ "sword" -> target($"goblin") -> roll("hit") -> result("success")

Movement Actions

do($"player", "move") ~ "stealthily" -> target($"door")

Social Actions

do($"player", "persuade") ~ "charming words" -> target($"merchant") -> intention="get discount"

Search/Investigation

do($"player", "search") ~ "carefully" -> target($"room")

Anti-Patterns

Don’t Ask LLM to Decide

❌ do($"player", "try to attack")
✓ do($"player", "attack") -> result("success")
✓ do($"player", "attack") -> result("failure")

The outcome should be determined by game logic before NDL generation.

Don’t Use Vague Descriptions

❌ do($"player", "do something")
✓ do($"player", "swing sword at enemy")

Be specific about the action.

Don’t Omit Entity Reference

❌ do("attack")
✓ do($"player", "attack")

Always specify who is performing the action.

Technical Details

Action Categories

Based on veritasr’s implementation, NDL supports 100+ action types across categories:

  • Physical: 36 actions (movement, combat, manipulation)
  • Mental: 35 actions (thinking, planning, problem-solving)
  • Social: 43+ actions (conversation, persuasion, emotion)

Processing Example

From veritasr’s Director implementation:

Input String:  do($"you", "write some NDL")->wait("response validation")
is_ndl:  True
continue_last:  False
end_scene:  True
transition_scene_type:  both
Actions:  ['do', 'wait']

The parser extracts:

  • Action type: do
  • Entity: $"you"
  • Description: "write some NDL"

Integration with Game Logic

Flow

Game State Change
    ↓
Generate NDL: do($entity, "action")
    ↓
Build Dynamic Prompt
    ↓
Send to LLM
    ↓
Receive Narrative Text
    ↓
Display to Player

Example

Player Input: "I attack the goblin with my sword"
    ↓
Parser: Extract intent, entity, target
    ↓
Game Logic: Roll attack, calculate hit/miss/damage
    ↓
Result: Hit for 8 damage
    ↓
NDL Generator: do($"player", "attack") ~ "sword" -> target($"goblin") -> result("hit") -> damage(8)
    ↓
Prompt Builder: Add context + NDL
    ↓
LLM: "You swing your sword in a powerful arc, the blade catching the goblin in its shoulder. It shrieks in pain as blood flows from the wound."

Model Compatibility

From veritasr [04:21]:

“And this is all on 8B / 9B LLMs. Nothing that really breaks the bank hardware wise.”

The do() construct works reliably on small models:

  • Gemma 8B/9B
  • Llama 3 8B/9B

No need for GPT-4 or large models.


ndl construct action do