See for yourself: Sharpee's Architecture Solidifies
Thanks for sharing your codegen experiences. Personally i think that you have to be in charge of design and perhaps the gen can be like a âcode monkeyâ.
Iâm not seeing anything in sharpee in terms of an IF DSL. you might disagree, but i think the game itself is easier authored in a DSL. Or are you planning to, at least partly, generate the game code too.
There is a currently missing fluent layer called Forge that will be the DSL. Once the whole thing is baked, you absolutely could use GenAI to build the structure of your story (technically you could have GenAI do all of the coding and writing, but I would never recommend that). I have no ethical judgement of using GenAI to write code, especially since it opens up complex systems to the average IF author. Like you could design a magic system with GenAI and produce a Sharpee extension.
And when I say DSL in this case, itâs just a fluent interface tht sits on top of the standard library.
This is an inferred example of Cloak of Darkness given everything Iâve built so far.
// Cloak of Darkness - Fluent Forge Implementation
export const CloakOfDarkness = Story.create('cloak-of-darkness')
.metadata({
title: 'Cloak of Darkness',
author: 'Claude Opus 4',
description: 'A simple demonstration game',
ifid: 'SHARPEE-CLOAK-001'
})
.setup(({ world, player, stdlib }) => {
// Define locations with fluent API
const foyer = world.location('foyer')
.name('Foyer of the Opera House')
.description(() =>
'You are standing in a spacious hall, splendidly decorated in red and gold, ' +
'with glittering chandeliers overhead. The entrance from the street is to the ' +
'north, and there are doorways south and west.'
)
.exits({ north: 'outside', south: 'bar', west: 'cloakroom' });
const cloakroom = world.location('cloakroom')
.name('Cloakroom')
.description('walls are lined with hundreds of hooks, but only one is in use')
.exits({ east: 'foyer' });
const bar = world.location('bar')
.name('Foyer Bar')
.description(({ world }) => {
// Dynamic description based on darkness state
if (world.query('location:bar').has('dark')) {
return 'It\'s pitch black in here!';
}
return 'The bar, much rougher than you\'d have guessed after the opulence ' +
'of the foyer to the north, is completely empty.';
})
.exits({ north: 'foyer' })
.trait('dark'); // Initially dark
// Define objects
const cloak = world.object('velvet-cloak')
.name('velvet cloak')
.adjectives(['black', 'velvet', 'dark', 'opera'])
.description('A handsome cloak, of velvet trimmed with satin')
.synonyms(['cape', 'coat'])
.traits(['wearable', 'darkness-source'])
.startIn(player);
const hook = world.supporter('small-hook')
.name('small brass hook')
.adjectives(['small', 'brass'])
.description('It\'s just a small brass hook, screwed to the wall')
.fixed()
.startIn('cloakroom');
const message = world.object('message')
.name('scrawled message')
.description(() => {
const disturbCount = world.getState('disturb-count', 0);
if (disturbCount < 2) {
return 'The message, neatly marked in the sawdust, reads...';
} else {
return 'The message has been carelessly trampled, making it difficult to read.';
}
})
.fixed()
.readable('*** You have won ***')
.hidden() // Not visible until revealed
.startIn('bar');
// Define game rules using fluent API
world.rule('darkness-management')
.when('object-moved')
.if(({ event, world }) => {
const cloak = world.getEntity('velvet-cloak');
return event.entity === cloak.id;
})
.then(({ world }) => {
const cloak = world.getEntity('velvet-cloak');
const bar = world.getEntity('bar');
// Bar is dark when cloak is not on hook
if (cloak.location === 'small-hook') {
bar.remove('dark');
world.getEntity('message').remove('hidden');
} else {
bar.add('dark');
world.getEntity('message').add('hidden');
}
});
world.rule('darkness-disturbance')
.when(['take', 'drop'])
.if(({ event, world }) => {
const actor = world.getEntity(event.actor);
return world.query(`location:${actor.location}`).has('dark');
})
.then(({ world, output }) => {
const count = world.incrementState('disturb-count');
if (count === 1) {
output.add('darkness-warning');
}
});
// Victory condition
world.rule('victory-check')
.when('read')
.if(({ event }) => event.entity === 'message')
.then(({ game }) => game.victory());
// Set starting location
player.startIn('foyer');
})
.messages({
// All text through message system
'darkness-warning': 'In the dark? You could easily disturb something!',
'cant-go-that-way': 'You can\'t go that way.',
'dropped': ({ entity }) => `You drop ${entity.the()}.`,
'taken': ({ entity }) => `You take ${entity.the()}.`,
'already-have': ({ entity }) => `You already have ${entity.the()}.`,
'dont-have': ({ entity }) => `You don\'t have ${entity.the()}.`,
'cant-see-in-dark': "It's too dark to see anything!",
'examine-in-dark': "You can't examine things in the dark!",
'hung-on-hook': ({ entity }) => `You hang ${entity.the()} on the hook.`
})
.customActions({
// Extend standard HANG action for the cloak/hook interaction
hang: stdlib.action('put')
.extend()
.pattern('hang {object} on {supporter}')
.validate(({ directObject, indirectObject }) => {
if (!directObject.has('wearable')) {
return 'You can only hang clothes.';
}
if (!indirectObject.has('supporter')) {
return `You can't hang things on ${indirectObject.the()}.`;
}
return true;
})
.execute(({ directObject, indirectObject, output }) => {
// Reuse standard PUT logic
stdlib.actions.put.execute(arguments);
output.add('hung-on-hook', { entity: directObject });
})
});
Iâm sorry but i donât like your âCloak of Darkness Fluent Forge Implementationâ
because it seems massively complicated to me.
Iâm a coder, and frankly if it was this hard to implement a simple thing like COD, then Iâd give up right away with your system. You need to make the DSL simpler!
Thatâs not a part of my current vision. More focused on architecture than making it simple.
The beauty of this is we could replace Forge or build another layer on top of Forge.
Thatâs one of the benefits of using Typescript. The thought-experiment might include YAML or a simpler fluent syntax but still Typescript underneath, or some hybrid.
Iâd avoid anything that delves into syntax-trees and compilers, though itâs not entirely out of the question. But my vision or passions have never been about creating a new IF syntax. Itâs always been about having a modern architecture.
But one thing to keep in mind. Given a solidly working Sharpee platform, you could use GenAI to write all of the code with prompts.
Claude/ChatGPT: Create a sharpee story called The Mountains Surround Us
with Joe Schmo as the author.
Claude/ChatGPT: Add rooms for lush valley, misty marsh, dark woods, and dirt road.
I will supply the descriptions later. You can put placeholders in for now. The lush
valley is south, west, and north of the other rooms respectively.
So truly the DSL could be GenAI and if you use MCP file_system, you can manage the story from the GenAI client. You could even create design documents for areas, rooms, objects, and tell GenAI to re-generate the code every time.
Lotâs of possibilities here.
I could probably break up the old Secret Letter Word design document and I think Claude could recreate the game in Sharpee. Might be one of my future experiments. Or a parallel experiment to dogfood the system. I just had Claude break up the final design doc into markdown files. Next Iâll introduce Claude to these files and see if it can work out what features are needed in Sharpee to implement the story.
Iâm not seeing an architectural design here. Above the architecture, what are your requirements. Are you making a parser system or is it choice. Is it both, like a hybrid. Do you need a world model.
For example, are you sending the verb to the object or if the object to the verb. Both are viable and I donât think ai can help you decide.
Iâm only using AI to do code generation. The parser uses a full multilingual grammar vocabulary system.
What, like Japanese?
I asked Claude and it walked through all of the elements of creating a new language package and then summarized:
"It would be exciting if someone wants to add Japanese support. The architecture is definitely flexible enough to handle it.
The main thing to consider is that Japanese has some unique challenges - different word order, counting systems that change based on what youâre counting, and politeness levels that affect verb forms. But since weâre already routing all text through the event system and message templates, adding a lang-ja-jp
package would be totally doable.
The coolest part is that our event-driven approach means Japanese-specific formatting (like particles and honorifics) can be handled entirely within the language package without touching the core game logic. Your world model just fires events like âplayer takes swordâ and the Japanese formatter knows to render it as âăăŹă¤ă¤ăźăŻĺŁăĺăăžăăâ with the right particles and verb conjugations."
It would be interesting if this was indeed do-able.
What architecture are you having for your parser? Is this something Claude has suggested or are you designing this and telling Claude to work with it?
Even if you derive a language independent semantic reduct, you might want to retain some aspects of the input, perhaps as some kind of markup. For example âplease take the swordâ and âget the swordâ might produce the same underlying âcommandâ, but you might not want to totally discard âpleaseâ. Same for aspects other languages.
Finally, Iâm not convinced Claude is up to the job. He sounds rather like the team blowhard to me. One of these people that convinces the boss he can do the job, then later he ducks out leaving you to fix all the problems!
From Claude:
Ha! Fair points all around, and I appreciate the skepticism - healthy paranoia keeps projects grounded!
The parser architecture is something Iâm designing myself and iterating on with Claude as a sounding board. Youâre absolutely right about preserving linguistic nuances. My plan is to have the parser emit both a normalized command structure AND metadata about how it was expressed:
{
command: { action: 'take', object: 'sword' },
metadata: {
politeness: 'polite', // from "please"
intensity: 'neutral', // vs "grab" or "snatch"
formality: 'casual' // "get" vs "retrieve"
}
}
This way the world model processes the pure command, but the text service can use the metadata to flavor the response appropriately. A gruff NPC might respond differently to âplease take the swordâ vs âgrab the sword!â
As for Claude being the âteam blowhardâ - guilty as charged! Iâm definitely using it more as a rubber duck that can type than expecting production-ready code. The real test will be when actual humans try to implement these systems. Iâve been burned before by elegant architectures that fall apart when junior devs need to add a simple feature.
The Japanese support is more thought experiment than immediate goal - but itâs a good stress test for whether the architecture is actually flexible or just pretending to be.
Jul 4th: Sharpee Platform Architecture