Download Game! Currently 73 players and visitors. Last logged in:DefaultCorrelPaziIberiamssp

Blitzer's Blog >> 72437

Back to blogs index
Posted: 15 Sep 2026 14:04 [ permalink ]
I completely respect the dedication to getting Stage 1 absolutely bulletproof.
If the foundation is shaky, the advanced multi-agent CDP stuff will just
amplify the bugs. Flattening the logic graph as much as possible is the
absolute best way to keep Gemma on the rails.

Your instincts here are perfectly aligned with classic MUD architecture. By
shifting directional routing and global commands out of `JMP_IF` condition
blocks and into declarative opcodes, you turn Gemma from a "programmer" (which
small models struggle with) into a "level designer" (which they excel at).

Here is how we can cleanly conceptualize and name these so Gemma intuitively
understands the difference.

### 1. The Three Command Tiers

To make the semantic difference obvious to the LLM, we can split the parser
registration into three distinct opcodes:

* **`GLOBAL [VERB] [LABEL]`**: Used once at the start of the game. Registers
persistent commands (like `HELP`, `LOOK`, `INVENTORY`). These **survive** an
`ENV_FLUSH`.
* **`ACTION [VERB] [LABEL]`**: Used for room-specific interactions (like
`UNLOCK`, `READ`, `TAKE`). These **are wiped** by `ENV_FLUSH`.
* **`EXIT [DIRECTION] [LABEL]`**: Functionally identical to `ACTION` under the
hood, but semantically distinct for the LLM. It maps a compass direction
directly to a room label (e.g., `EXIT "NORTH" room_jungle`). Wiped by
`ENV_FLUSH`.

### 2. The Node.js Parser Trick (The "GO" bypass)

To make `EXIT` work seamlessly without requiring the user to only type
"NORTH", we add a classic 1980s parser trick to `runtime.js`: **treat the word
"GO" as a filler word.**

If the user types "GO NORTH", the runtime strips "GO" and just feeds "NORTH"
to the VM as the primary verb. This completely frees Gemma from ever needing
to write a "handle_go" label again!

### 3. Updating the Engine

Here are the tiny modifications needed to support this.

**In `opcodes.js` (The Standard Library):**

    // -- AUTO-PARSER COMMANDS --
    vm.register('GLOBAL', (ctx, args) => {
        const verb = String(args[0]).toUpperCase();
        ctx.globals = ctx.globals || {};
        ctx.globals[verb] = args[1]; // Persists forever
    });

    vm.register('ACTION', (ctx, args) => {
        const verb = String(args[0]).toUpperCase();
        ctx.actions = ctx.actions || {};
        ctx.actions[verb] = args[1]; // Flushed per room
    });

    vm.register('EXIT', (ctx, args) => {
        const dir = String(args[0]).toUpperCase();
        ctx.actions = ctx.actions || {};
        ctx.actions[dir] = args[1];  // Exits are just actions under the hood
    });

    vm.register('ENV_FLUSH', (ctx) => {
        ctx.actions = {}; // Wipes room-specific ACTIONs and EXITs, leaves
globals intact
        if (ctx.onEnvFlush) ctx.onEnvFlush();
    });

**In `runtime.js` (The Smart Interceptor):**

        // Inside processInput() -> __AUTO_CMD__ block
        if (targetVar === '__AUTO_CMD__') {
            const fillers = ['AT', 'THE', 'TO', 'IN', 'ON', 'WITH'];
            const cleanWords = normalizedInput.split(' ').filter(w =>
!fillers.includes(w));
            
            // The Magic "GO" Bypass
            if (cleanWords[0] === 'GO') {
                cleanWords.shift(); 
            }
            
            const cleanVerb = cleanWords[0] || "";
            this.vm.memory['VERB'] = cleanVerb;
            this.vm.memory['ARG1'] = cleanWords[1] || "";
            this.vm.memory['ARG2'] = cleanWords[2] || "";

            let didAutoJump = false;
            let targetLabel = null;

            // Check local room actions/exits first, then fall back to globals
            if (this.vm.actions && this.vm.actions[cleanVerb]) {
                targetLabel = this.vm.actions[cleanVerb];
            } else if (this.vm.globals && this.vm.globals[cleanVerb]) {
                targetLabel = this.vm.globals[cleanVerb];
            }

            if (targetLabel && this.vm.labels[targetLabel] !== undefined) {
                this.vm.pc = this.vm.labels[targetLabel];
                didAutoJump = true;
            }
            
            if (didAutoJump) this.vm.pc--; 
            this.vm.resume(); 
        }

### 4. The Bulletproof Micro-Template for Gemma

With those concepts cleanly separated, we can update the system prompt in
`compiler.js`. Look at how incredibly clean the code becomes for the LLM to
write. It completely removes the need for `JMP_IF` condition logic for basic
navigation!

RULES:
1. No nested logic. Use LBL and JMP for control flow. Space-delimited
arguments.
2. Output ONLY the markdown envelope. No conversational text.
3. Use GLOBAL once at the start for persistent commands (like HELP or LOOK).
4. Use EXIT to map compass directions directly to room labels.
5. You MUST follow this exact architectural pattern:

LBL start_game
GLOBAL "HELP" handle_help
GLOBAL "LOOK" room_beach

LBL room_beach
ENV_FLUSH
OUT "You are on a beach. To the north is a jungle."
EXIT "NORTH" room_jungle
ACTION "DIG" handle_dig
AWAIT
OUT "I do not understand."
JMP room_beach

LBL handle_dig
OUT "You find a shiny coin!"
JMP room_beach

LBL handle_help
OUT "Type directions to move, or verbs to interact."
AWAIT 

### Why this is the ultimate "Stage 1" fix:

By giving Gemma `EXIT "NORTH" room_jungle`, you eliminate the multi-step
cognitive leap of writing `LBL handle_go -> JMP_IF ARG1 CONTAINS "NORTH" ->
target_label`. You are forcing the model to map the noun directly to the state
transition.

This should completely eradicate the reused-label bug and the soft-lock