Skip to content

broomburgo/Narratore

Repository files navigation

Narratore

Narratore is a Swift library that can be used to create and run interactive stories and narrative games.

With Narratore you can create stories using a DSL that allows to focus on the narration, with very few lines of code. In Narratore a story is a Swift Package.

The library also makes it easy to run a story, with a callback-based handler.

Here's a taste of a minimal definition for a game with Narratore:

import Narratore

// ------ Define a game setting ------ //

enum MyGame: Setting {
  enum Generate: Generating {
    static func randomRatio() -> Double {
      Double((0...1000).randomElement()!)/1000
    }
    
    static func uniqueString() -> String {
      UUID().uuidString
    }
  }
  
  struct Message: Messaging {
    var id: String?
    var text: String

    init(id: ID?, text: String) {
      self.id = id
      self.text = text
    }
  }

  struct Tag: Tagging {
    var value: String
    
    init(_ value: String) {
      self.value = value
    }
  }
  
  struct World: Codable {
    var isEnjoyable = true
  }
}

// ------ Write a story ------ //

extension SceneType {
  typealias Game = MyGame
}

extension MyGame: Story {
  static let scenes: [RawScene<MyGame>] = [
    MyFirstScene.raw,
    MySecondScene.Main.raw,
    MySecondScene.Other.raw,
  ]
}

struct MyFirstScene: SceneType {
  typealias Anchor = String

  var steps: Steps {
    "Welcome"
    
    "This is your new game, built with narratore".with(tags: [.init("Let's play some sound effect!")])
    
    check {
      if $0.world.isEnjoyable {
        "Enjoy!"
      }
    }
    
    "Now choose".with(anchor: "We could jump right here from anywhere")
    
    choose { _ in
      "Go to second scene, main path".onSelect {
        "Let's go to the second scene!"
        .with(id: "We can keep track of this message")
        .then {
          .transitionTo(MySecondScene.Main(magicNumber: 42))  
        }
      }

      "Go to second scene, alternate path".onSelect {
        "Going to the alternate path of the second scene".then {
          .transitionTo(MySecondScene.Other())
        }
      }
    }
  }
}

enum MySecondScene {      
  struct Main: SceneType {
    var magicNumber: Int

    var steps: [SceneStep<Self>] {
      "Welcome to the second scene"
      
      if magicNumber == 42 {
        "The magic number is \(magicNumber)"
      } else {
        "The magic number doesn't look right..."
      }
      
      "Hope you'll find this useful!"
    }
  }
  
  struct Other: SceneType {
    var steps: [SceneStep<Self>] {
      "I see you chose the alternate path"
      
      "Bad luck!"
    }
  }
}

// ------ Run the game ------ //

final class MyHandler: Handler {
  typealias Game = MyGame

  func handle(event: Event<MyGame>) {
    if case .gameEnded = event {
      print("Thanks for playing!")
    }
  }
  
  func acknowledge(narration: Narration<MyGame>) async -> Next<MyGame, Void> {
    for message in narration.messages {
      print(message)
      _ = readLine()
    }
    return .advance
  }
  
  func make(choice: Choice<MyGame>) async -> Next<MyGame, Option<MyGame>> {
    for (index, option) in choice.options.enumerated() {
      print(index, option.message)
    }
    
    while true {
      guard
        let captured = readLine(),
        let selected = Int(captured),
        choice.options.indices.contains(selected)
      else {
        print("Invalid input")
        continue
      }
      
      return .advance(with: choice.options[selected])
    }
  }
  
  func answer(request: Player<MyGame>.TextRequest) async -> Next<MyGame, Player<MyGame>.ValidatedText> {
    if let message = request.message {
      print(message)
    }

    guard let text = readLine() else {
      return .replay
    }

    switch request.validate(text) {
    case .valid(let validatedText):
      return .advance(with: validatedText)

    case .invalid(let optionalMessage):
      if let optionalMessage {
        print(optionalMessage)
      }
      return .replay
    }
  }
}

@main
enum Main {
  static func main() async {
    await Runner<MyGame>.init(
      handler: MyHandler.init(),
      status: .init(
        world: .init(),
        scene: MyFirstScene.init()
      )
    ).start()
  }
}

To learn about the detail of each main component of Narratore, check the following docs:

Narratore is designed to be modular and extensible. In fact, each main component can be defined and implemented in a separate Swift package. For example:

  • a specific game setting could be defined in a library;
  • several stories could be created for a certain game setting;
  • a game handler for that setting could be created for each platform (command line, iOS, macOS, Linux...);
  • the final game would mix the game handler with one or more stories.

To learn how to extend Narratore and define modular components, check out Extending Narratore.

The linked docs progressively build a basic game setting, a short story, a simple command-line runner, and some extension, each of which can be found in a companion package called SimpleGame, whose purpose is to show the basics of Narratore in practice via the construction of an actual story that can be run from the command line.

The main purpose of the companion package is to document the features of Narratore; nevertheless, most of its code is generic and reusable, and can be used to create games: please refer to the companion package README to learn how to use it in your projects.

Finally, DSL reference contains a quick reference to the Narratore DSL, that is, the possible commands that one can use to write a story.

Thanks for checking out Narratore, I hope you'll have fun with it!

Requirements

Narratore requires iOS 13 and macOS 10.15, and has no third-party dependencies.

Acknowledgments

Narratore was heavily inspired by Ink, and its initial purpose was to be a similar story creation engine, but with the possibility of defining stories in Swift, instead of using a markup language. Nevertheless, the Ink specification was a strong inspiration for the features of Narratore.