叙述者 (Narratore)

Narratore 是一个 Swift 库,可用于创建和运行交互式故事和叙事游戏。

使用 Narratore,您可以使用 DSL 创建故事,该 DSL 允许您专注于叙述,只需编写很少的代码。在 Narratore 中,**故事是一个 Swift 包**。

该库还简化了运行故事的过程,并提供了基于回调的处理程序。

这是一个使用 Narratore 定义游戏的最简示例:

import Foundation
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
  }

  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!")])
    
    DO.check {
      .inCase($0.world.isEnjoyable) {
        .tell { "Enjoy!" }
      }
    }
    
    "Now choose".with(anchor: "We could jump right here from anywhere")
    
    DO.choose { _ in
      "Go to second scene, main path".onSelect {
        .tell {
          "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 {
        .tell {
          "Going to the alternate path of the second scene"
        } then: {
          .transitionTo(MySecondScene_Other())
        }
      }
    }
  }
}

struct MySecondScene_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 MySecondScene_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>(
      handler: MyHandler(),
      status: .init(
        world: .init(),
        scene: MyFirstScene()
      )
    ).start()
  }
}

要了解 Narratore 每个主要组件的详细信息,请查看以下文档:

Narratore 被设计成模块化和可扩展的。事实上,每个主要组件都可以在单独的 Swift 包中定义和实现。例如:

要了解如何扩展 Narratore 并定义模块化组件,请查看 扩展 Narratore

链接的文档逐步构建了一个基本游戏设定、一个短篇故事、一个简单的命令行运行器和一些扩展,每个组件都可以在一个名为 SimpleGame 的配套包中找到,其目的是通过构建一个可以从命令行运行的实际故事来展示 Narratore 的基本原理。

配套包的主要目的是记录 Narratore 的功能;尽管如此,它的大部分代码都是通用的和可重用的,并且可以用于创建游戏:请参阅配套包 README 以了解如何在您的项目中使用它。

感谢您查看 Narratore,希望您玩得开心!

要求

Narratore 需要 iOS 13macOS 10.15,并且没有第三方依赖项。

致谢

Narratore 很大程度上受到了 Ink 的启发,其最初目的是成为一个类似的故事创建引擎,但可以使用 Swift 定义故事,而不是使用标记语言。尽管如此,Ink 规范对于 Narratore 的功能是一个强大的灵感来源。