Download Game! Currently 96 players. Last logged in:SerkeiiLysanderOmrilBaroque

Blitzer's Blog >> 72937

Back to blogs index
Posted: 04 Oct 2026 14:26 [ permalink ]
Building an idempotent, parallel extraction pipeline using map-reduce
techniques for large texts like novels is exactly how production pipelines
operate. We can certainly architect the document sharding and parallel worker
distribution later.
For now, here is the middleware compiler that translates Gemma's standard JSON
output into the compact, executable JSON-Lisp AST format that your MUD and
Canvas engines consume.
This compiler performs a single pass over the extracted nodes. Because the LLM
only outputs dimensions (w, h), the compiler must dynamically resolve the
Euclidean geometry, assigning x and y origins by tracing the graph from a
starting anchor point and calculating offsets based on the exit directions.
The Middleware Compiler Script
 * Compiles LLM JSON output into a spatial JSON-Lisp AST.
 * @param {Object} extractedJson - The sanitized JSON from Gemma.
 * @returns {Array} - The JSON-Lisp "zones" branch.
 */
function compileToAST(extractedJson) {
    const roomMap = {};
    extractedJson.zones.forEach(z => roomMap[z.id] = z);

    const layout = {};
    const gap = 3; // The distance (in grid tiles) between connected rooms

    // Recursive graph traversal to plot room coordinates
    function plotRoom(id, x, y) {
        if (layout[id]) return; // Already plotted
        
        const room = roomMap[id];
        if (!room) return;
        
        layout[id] = { x, y, w: room.grid.w, h: room.grid.h };

        // Calculate neighbor coordinates based on exit directions
        if (room.exits) {
            room.exits.forEach(exit => {
                if (layout[exit.target_id]) return;
                
                const target = roomMap[exit.target_id];
                if (!target) return;

                let tx = x, ty = y;
                
                // Center the rooms against each other and apply the
directional gap
                if (exit.direction === 'north') {
                    tx = x + Math.floor((room.grid.w - target.grid.w) / 2);
                    ty = y - target.grid.h - gap;
                } else if (exit.direction === 'south') {
                    tx = x + Math.floor((room.grid.w - target.grid.w) / 2);
                    ty = y + room.grid.h + gap;
                } else if (exit.direction === 'east') {
                    tx = x + room.grid.w + gap;
                    ty = y + Math.floor((room.grid.h - target.grid.h) / 2);
                } else if (exit.direction === 'west') {
                    tx = x - target.grid.w - gap;
                    ty = y + Math.floor((room.grid.h - target.grid.h) / 2);
                }
                
                plotRoom(exit.target_id, tx, ty);
            });
        }
    }

    // Anchor the first room at an arbitrary center point on the Canvas
    if (extractedJson.zones.length > 0) {
        plotRoom(extractedJson.zones[0].id, 15, 15);
    }

    // Fallback: If Gemma hallucinates a disconnected room, plot it off to the
side
    extractedJson.zones.forEach((z, index) => {
        if (!layout[z.id]) {
            plotRoom(z.id, 15 + (index * 10), 15 + (index * 10));
        }
    });

    // Translate the plotted data into the final JSON-Lisp array structure
    const astZones = [ "zones" ];
    
    extractedJson.zones.forEach(z => {
        const exitsArray = z.exits ? z.exits.map(e => [
            "exit", 
            e.direction, 
            e.target_id, 
            { "desc": `A passage leading ${e.direction}`, "type": "walkable" }
        ]) : [];

        astZones.push([
            "room",
            z.id,
            {
                "name": z.name,
                "description": z.description,
                "grid": layout[z.id]
            },
            exitsArray
        ]);
    });

    return astZones;
}

How to Hook It Up
In your prototype UI, inside the btn-extract click handler (which currently
calls mountWorld), you can wire this directly to your Gemma pipeline:
btnExtract.addEventListener('click', async () => {
    btnExtract.textContent = "Extracting... (Contacting Gemma)";
    btnExtract.disabled = true;

    const textToScan = document.getElementById('book-input').value;
    
    // 1. Run the LLM Extraction
    const llmJson = await extractTopologyPass(textToScan);
    
    // 2. Compile to JSON-Lisp AST
    const generatedZones = compileToAST(llmJson);
    
    // 3. Mount it into the mock engine state
    mockAST[2] = generatedZones; // Overwrite the hardcoded zones with live
data
    
    // 4. Start the Engine
    mountWorld();
});

Because this compiler standardizes the output, it guarantees that no matter
what text you feed the pipeline, the resulting AST maps flawlessly to both
your 2D grid logic and your text-based MUD parser. The data can immediately be
serialized using your AST-MD formats and synced via your CRDT relays.
Should we map out the chunking/sharding strategy for large files next, or
would you prefer to draft Pass 2 (Entity & Event Extraction) for this
single-page window first?