This script is where the magic happens. By using a deterministic Node.js
script to build the FSO opcodes, we completely eliminate the chance of the LLM
falling through a wall, forgetting an AWAIT, or creating one-way topological
traps.
The hardest challenge in text adventures is Bidirectional Compass Mapping (if
the Jungle is NORTH of the Beach, the Beach MUST be SOUTH of the Jungle). We
are going to solve this using a "Compass Router" algorithm that dynamically
assigns available opposite pairs.
Here is the complete graph-builder.js script.
graph-builder.js
Save this file in your /mnt/mesh_shared/src/ast-vm/ directory:
import fs from 'fs';
/ The logical opposing pairs for bidirectional routing
const COMPASS_PAIRS = [
{ fwd: 'NORTH', rev: 'SOUTH' },
{ fwd: 'EAST', rev: 'WEST' },
{ fwd: 'UP', rev: 'DOWN' },
{ fwd: 'IN', rev: 'OUT' },
// Backups if the room gets highly connected
{ fwd: 'NORTHEAST', rev: 'SOUTHWEST' },
{ fwd: 'NORTHWEST', rev: 'SOUTHEAST' }
];
function parseCartographer(input) {
const rooms = {};
const blocks = input.split('---').map(b => b.trim()).filter(Boolean);
for (const block of blocks) {
const lines = block.split('
');
let id = '', desc = '', exits = [];
for (const line of lines) {
if (line.startsWith('ROOM:')) id = line.replace('ROOM:',
'').trim();
if (line.startsWith('DESC:')) desc = line.replace('DESC:',
'').trim();
if (line.startsWith('EXITS:')) {
const exitsRaw = line.replace('EXITS:', '');
exits = exitsRaw.split(',').map(e => e.trim()).filter(Boolean);
}
}
if (id) {
rooms[id] = { id, desc, exits, routes: {} };
}
}
return rooms;
}
function assignCompassDirections(rooms) {
// Keep track of assigned connections to enforce bidirectionality
const assignedLinks = new Set();
for (const [id, room] of Object.entries(rooms)) {
for (const targetId of room.exits) {
// Skip if the target room doesn't actually exist
if (!rooms[targetId]) continue;
// Skip if we already mapped this connection from the other side
const linkKey1 = `${id}->${targetId}`;
const linkKey2 = `${targetId}->${id}`;
if (assignedLinks.has(linkKey1)) continue;
// Find an available compass pair
const targetRoom = rooms[targetId];
const usedInRoomA = Object.keys(room.routes);
const usedInRoomB = Object.keys(targetRoom.routes);
let chosenPair = COMPASS_PAIRS.find(pair =>
!usedInRoomA.includes(pair.fwd) && !usedInRoomB.includes(pair.r
ev)
);
// Fallback just in case a room has crazy connectivity
if (!chosenPair) chosenPair = { fwd: 'PATH', rev: 'PATH' };
// Assign the forward route
room.routes[chosenPair.fwd] = targetId;
assignedLinks.add(linkKey1);
// Enforce the reverse route automatically (no traps!)
targetRoom.routes[chosenPair.rev] = id;
assignedLinks.add(linkKey2);
}
}
}
function buildFSO(rooms) {
let fsoCode = `LBL start_game
GLOBAL "HELP" handle_help
`;
// Set the first room dynamically as the spawn point
const firstRoomId = Object.keys(rooms)[0];
if (firstRoomId) fsoCode += `JMP room_${firstRoomId.toLowerCase()}
`;
for (const [id, room] of Object.entries(rooms)) {
const lblName = `room_${id.toLowerCase()}`;
fsoCode += `LBL ${lblName}
`;
fsoCode += `ENV_FLUSH
`;
// Escape quotes to prevent compilation crashes
const safeDesc = room.desc.replace(/"/g, '\\"');
fsoCode += `DESC "${safeDesc}"
`;
for (const [direction, targetId] of Object.entries(room.routes)) {
fsoCode += `EXIT "${direction}" room_${targetId.toLowerCase()}
`;
}
fsoCode += `AWAIT
`;
}
// Add the global help handler
fsoCode += `LBL handle_help
`;
fsoCode += `OUT "Type directions (like NORTH or EAST) to move around."
`;
fsoCode += `AWAIT
`;
return `\`\`\`fso-state
VAR INVENTORY ARR '[]'
\`\`\`
\`\`\`fso-code
${fsoCode.trim()}
\`\`\``;
}
/ CLI Execution
const inputFile = process.argv[2] || 'map.txt';
if (!fs.existsSync(inputFile)) {
console.error(`[FAULT] Could not find ${inputFile}`);
process.exit(1);
}
const rawText = fs.readFileSync(inputFile, 'utf-8');
const rooms = parseCartographer(rawText);
assignCompassDirections(rooms);
const outputMd = buildFSO(rooms);