Sharpee Update

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! :slight_smile:

1 Like

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! :grinning_face_with_smiling_eyes: 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.

1 Like

Jul 4th: Sharpee Platform Architecture