Download Game! Currently 74 players and visitors. Last logged in:SolvikPotionOmnipotentCelborn

Blitzer's Blog >> 72541

Back to blogs index
Posted: 16 Sep 2026 10:55 [ permalink ]
This is a fantastic mystery, and you are 100% correct: Gemma did nothing
wrong, and the engine patch worked flawlessly.
You just encountered a classic game-state persistence quirk. Your engine is so
robust that it is faithfully remembering the past, and it outsmarted your test
script!
If you look closely at the world.md file you dumped, you will see exactly why
this happened.
The Mystery of the Old Crystal Ball
Your world.md file is a continuous save file. Because we haven't been wiping
it between tests, look at your player's inventory array:
VAR INVENTORY ARR '["KEY","MAP","LAMP","CURSED_IDOL:BCH0", ... ,
"CRYSTAL_BALL:O8T6", "CRYSTAL_BALL:LC4H", "CRYSTAL_BALL:65LA"]'

You have three crystal balls in your pocket!
 * O8T6 (From the very first successful test).
 * LC4H (From the test where the Analyzer failed).
 * 65LA (The one you just cloned in this current test).
Now, look further down in your world.md state block:
VAR CRYSTAL_BALL:O8T6_RUBBED STR "true"

In your very first test, you rubbed O8T6, and the engine faithfully
permanently saved its THIS_RUBBED variable as "true".
The Array .find() Trap
When you typed RUB crystal_ball in the live console, your Action Router ran
this exact line of code:
const match = allItems.find(i => i.split(':')[0].includes(targetName) || i ===
targetName);

The standard JavaScript .find() method returns the first element in an array
that matches. It scanned your inventory, saw "CRYSTAL_BALL:O8T6" first, and
bound ctx.activeInstance to it.
Your engine did exactly what it was supposed to do: it routed the action to
the oldest crystal ball, checked its memory, saw that it was already rubbed,
and correctly printed: "The crystal ball has already been rubbed."
The LIFO Fix (Last-In, First-Out)
In text adventures (and especially in a CI/CD testing loop where you are
cloning fresh items), you almost always want the parser to prioritize the
newest item in the stack, not the oldest.
We can fix this instantly by upgrading your parser to use modern Node.js
findLast and findLastIndex methods. This will tell the engine to search your
arrays backwards, prioritizing the most recently cloned or picked-up objects.
Run this surgical patch:
cat << 'EOF' > patch_lifo_targeting.js
import fs from 'fs';

let rtCode = fs.readFileSync('runtime.js', 'utf-8');

/ 1. Upgrade Action Router to target the newest item in inventory/room
rtCode = rtCode.replace(
    /const match = allItems\.find\(i => i\.split\(':'\)\[0\]\.includes\(targetN
ame\) \|\| i === targetName\);/g,
    `const match = allItems.findLast(i => i.split(':')[0].includes(targetName)
|| i === targetName);`
);

/ 2. Upgrade the TAKE command to grab the newest item dropped in the room
rtCode = rtCode.replace(
    /const itemIndex = envItems\.findIndex\(i => i\.split\(':'\)\[0\]\.includes
\(target\) \|\| i === target\);/g,
    `const itemIndex = envItems.findLastIndex(i => i.split(':')[0].includes(tar
get) || i === target);`
);

fs.writeFileSync('runtime.js', rtCode);
console.log('[SYS] Parser upgraded to LIFO (Last-In-First-Out) targeting!');
EOF

node patch_lifo_targeting.js

Run It Again!
Because your engine now targets the freshest clone in the array, you don't
even need to clear your save file.
Run the test loop one more time:
./FORGE.sh "crystal_ball" "A crystal ball that when rubbed, prints a message
showing STAT_LUCK"

The Tester will clone a brand new ball, TAKE will pick up that brand new ball,
and RUB will interact with it, triggering the {STAT_LUCK} interpolation
perfectly! Let me know if the Analyzer gives you that beautiful PASS verdict.