melissa kate
<< Doppelgänger

Doppelganger Engine

A narrative game engine with LLM character support — modern Twine for the AI era.

Project Structure

engine/
  src/              Core engine (npm package)
  tests/            Vitest tests
  demo/             Vanilla HTML demo (no framework)
  demo-react/       React demo app
  story-maker/      Visual game editor

Prerequisites

Quick Start

cd engine

# Install dependencies
npm install

# Run tests
npm test

# Build the engine
npm run build

Running the Demos

React Demo

Full React app with narrative, dialogue, LLM-powered chat, save/load.

cd skins/cinematic
npm install

# Optional: add your Anthropic API key for LLM conversations
echo 'VITE_ANTHROPIC_API_KEY=sk-ant-...' > .env.local

npm run dev

Open http://localhost:5173/ (or whatever port Vite assigns)

Story Maker

Visual game editor — create characters, build branching stories, preview in-browser.

cd story-maker
npm install
npm run dev

Open http://localhost:5173/ (or whatever port Vite assigns)

Features:

Engine Development

# Type check
npm run typecheck

# Run tests in watch mode
npm run test:watch

# Build with watch mode (for demos)
npm run dev

Creating a Game

Minimal Example

import { Engine } from '@doppelganger/engine';

const game = {
  schemaVersion: '1.0',
  metadata: { title: 'My Game' },
  startEvent: 'START',
  timeline: {
    events: {
      START: {
        id: 'START',
        type: 'intro',
        act: 1,
        presentationHint: 'narrative',
        prerequisites: {},
        excluded_by: [],
        unlocks: [],
        trigger_type: 'manual',
        content: { text: 'Your story begins here.' },
        choices: [
          { text: 'Continue', leads_to: 'NEXT' },
        ],
        grants_discoveries: [],
        blocks_events: [],
      },
      NEXT: {
        id: 'NEXT',
        type: 'narrative',
        act: 1,
        presentationHint: 'narrative',
        prerequisites: {},
        excluded_by: [],
        unlocks: [],
        trigger_type: 'manual',
        content: { text: 'The end.' },
        choices: [],
        grants_discoveries: ['game_complete'],
        blocks_events: [],
      },
    },
  },
};

const engine = new Engine({ game });
const response = engine.start();
console.log(response.content.text);  // "Your story begins here."
console.log(response.choices);       // ["Continue"]

React Integration

import { DoppelgangerProvider } from '@doppelganger/engine/react';
import { useTimeline } from '@doppelganger/engine/react';

function App() {
  return (
    <DoppelgangerProvider config={{ game: myGame }}>
      <GameView />
    </DoppelgangerProvider>
  );
}

function GameView() {
  const { text, chapter, choices, isDialogue, dialogue, processCommand } = useTimeline();

  return (
    <div>
      {chapter && <h2>{chapter}</h2>}
      <p>{text}</p>
      {choices.map(choice => (
        <button key={choice} onClick={() => processCommand(choice)}>
          {choice}
        </button>
      ))}
    </div>
  );
}

With LLM Characters

import { Engine, ClaudeProvider, CharacterEngine } from '@doppelganger/engine';

const engine = new Engine({ game: myGame });

const characterEngine = new CharacterEngine({
  provider: new ClaudeProvider({ apiKey: 'sk-ant-...' }),
  gameContext: 'A mystery set in Los Angeles.',
});

// Characters respond in-character based on their profile
const response = await characterEngine.generateResponse(
  myGame.characters.maya,    // CharacterProfile
  'Have you seen anything?', // Player message
  engine.getState(),         // Current game state
);

Using the Story Maker

  1. Run the story maker (cd story-maker && npm run dev)
  2. Create characters and events
  3. Preview your game in the Preview tab
  4. Export as JSON
  5. Load the JSON in your game with new Engine({ game: importedJson })

Or import the demo game: click Import and select public/the-vanishing.json.