All the code examples in this document are taken from the
SimpleStory
module of the companion package. The structure of the story written in that module is purposely contrived, in order to show several features ofNarratore
.
The defining feature of Narratore
is that a story is a Swift Package. A key design choice in Narratore
was to avoid representing stories in loosely typed formats, and take advantage of the full power of the Swift compiler to produce stories with state, choices, branching paths et cetera. To be able to write stories in a "natural" format, that doesn't look too much like code, Narratore
makes heavy use of @resultBuilder
to define a simple DSL that can help focusing on the narration itself. This document provides a summary of the main features of Narratore
when it comes to writing stories, and to do that the document follows an actual story that was written to showcase some of these features. You can check the story yourself: it's fully contained in the SimpleStory
module of the companion package.
Story
in Narratore
is a protocol that derives from Setting
(described in Defining a game setting) and adds a static var scenes: [RawScene<Self>] { get }
property. This is required for decoding reasons: when a story file is deserialized, Narratore
looks for the scenes defined in the property in order to deserialize each scene. We'll see later what a RawScene
is, but for now let's take a look a the basic building blocks of a Story
, that is, Scene
and SceneStep
.
A Scene
is a type conforming to the protocol SceneType
. Essentially, a Scene
is an actual piece of the story, it might be associated to a location, an episode, or even a simple character, and, other than Sendable
, it must be Codable
, and Hashable
, because it's associated to some state that will be serialized and deserialized, and each instance of it will be uniquely identified by its hash value, so the steps of a scene can be cached and don't need to be computed each time. For example, SimpleStory
declares the following scene:
struct Car: SceneType {
var steps: Steps {
...
}
}
Normally, SceneType
would require a typealias Game = ...
declaration, but within a single Story
it's simply possible to automatically add it to all Scene
s:
extension SceneType {
typealias Game = SimpleStory
}
For deserialization consistency, it's important that the type name of each scene is unique. Swift doesn't allow types to have the same name in a single package, but it's possible ot use namespaces to declare distinct types with the same name, for example:
enum Bar {
struct Foo {}
}
enum Baz {
struct Foo {}
}
Bar.Foo
and Baz.Foo
are distinct types, but the string description of the type is the same, that is, "Foo"
and this is not allowed, because Narratore
uses such string description to identify if a specific type can be deserialized from some data. Thus, Narratore
requires that each type has a fully unique name.
To organize scenes, an option is to use a prefix in the scene name, for example
In some cases, it might be convenient to group multiple scenes into a single namespace, because they're all related, and the namespace could include some convenient declarations that are related to all scenes:
enum Bookshop {
static let scenes: [RawScene<SimpleStory>] = [
Bookshop_Main.raw,
Bookshop_ShowPhoto.raw,
...
]
}
struct Bookshop_Main: SceneType {
enum Anchor: Codable, Hashable {
case askQuestions
}
enum Status: Codable {
case regular
case trashed
}
var status: Status = .regular
...
}
struct Bookshop_ShowPhoto: SceneType {
...
}
The Bookshop_Main
scene has a status
property that defines the state of the bookshop: regular
when it's in its normal state, and trashed
when it was messed up by someone. The scene is the same, but it has 2 possible states, which will work almost as separate scenes, as we will see, thanks to the fact that the scene is Hashable
(it's uniquely identified based on its state), and Codable
(deserialization will have a different effect when restoring it from storage).
The Scene
is the fundamental building block of a Narratore
story: it's its "Lego piece", so to speak. One could define a Story
with a single Scene
and a (very) long list of steps, but it's often convenient to split the story in Scene
s that have their own separate state.
A Scene
provides a linear list of steps, whose actual content depends on the current state of the Scene
. This can be easily seen by just looking at the definition of the Scene
protocol:
public protocol SceneType: Codable, Hashable, Sendable {
associatedtype Game: Story
associatedtype Anchor: Codable, Hashable, Sendable = Never
@SceneBuilder<Self>
var steps: Steps { get }
}
A Scene
is associated to a Game: Story
and an Anchor
that defaults to Never
(it's essentially "optional" then, because if it's not defined for a Scene
, it will be assumed to be non-existent). Also, Scene
must define a var steps: Steps { get }
computed property, that returns a list of SceneStep
and depends on the state of the scene (Steps
is a typealias
).
In theory, one could extend their World
type with the SceneType
protocol, and have all scenes in the game depend on the state of the World
itself: while this could be a good idea for a very simple and short story, it's probably better to still split the story in several Scene
s, that could then be referenced from the World
if needed (being Codable
, they can be put in World
properties).
The key requirement for a Scene
is to provide a computed property that returns a [SceneStep<Self>]
, and the types involved are pretty simple, so let's describe them in some detail:
public struct SceneStep<Scene: SceneType>: Sendable {
public init(anchor: Scene.Anchor? = nil, getStep: GetStep<Scene.Game>) {
...
}
...
}
public struct GetStep<Game: Setting>: Sendable {
public init(_ run: @escaping @Sendable (Context<Game>) -> Step<Game>) {
...
}
...
}
public struct Context<Game: Setting>: Sendable {
public let generate: Generate<Game>
public let script: Script<Game>
public let world: Game.World
}
public struct Step<Game: Setting>: Sendable {
public init(apply: @escaping @Sendable (inout Info<Game>, Handling<Game>) async -> Outcome<Game>) {
...
}
...
}
public struct Info<Game: Setting>: Codable, Sendable {
public internal(set) var script: Script<Game>
public internal(set) var world: Game.World
}
public struct Handling<Game: Setting>: Sendable {
public init(
acknowledgeNarration: @escaping @Sendable (Player<Game>.Narration) async -> Next<Game, Void>,
makeChoice: @escaping @Sendable (Player<Game>.Choice) async -> Next<Game, Player<Game>.Option>,
answerRequest: @escaping @Sendable (Player<Game>.TextRequest) async -> Next<Game, Player<Game>.ValidatedText>,
handleEvent: @escaping @Sendable (Player<Game>.Event) -> Void
) {
...
}
...
}
public typealias Outcome<Game: Setting> = Next<Game, SceneChange<Game>?>.Action
public struct Next<Game: Setting, Advancement: Sendable>: Sendable {
public var action: Action
public var update: Update<Game>?
public init(action: Action, update: Update<Game>?) {
self.action = action
self.update = update
}
...
public enum Action: Sendable {
case advance(Advancement)
case replay
case stop
}
}
public typealias Update<Game: Setting> = @Sendable (inout Game.World) async -> Void
Basically, SceneStep
requires a GetStep
, and GetStep
requires a function from Context
to Step
. The Context
contains a generator defined from the Setting.Generate
type, plus an (immutable) value of the current script of the story, and the state of the game world. Step
is created from a function that takes:
- a mutable value of the pair script+state (the
Info
); - a
Handling
value, that captures the logic of theHandler
defined for the game;
The Step
function then, returns an Outcome
, that describes the next action Narratore
should take (check Running the game for more details on Outcome
and some of the other types described).
It's perfectly possible to create a full story just by creating values of the types defined above: this value-based approach makes the creation of a story in Narratore
extremely flexible. But Narratore
also defines a DSL to handle these values - and combine them into a full story – that allows to basically forget about the high-level specifics and focus on the narration itself.
The next section will describe the various components of this DSL.
It's important to understand how narratore represents the state of the story. The current state of the story, as mentioned above, is represented via the Context
type
public struct Context<Game: Setting>: Sendable {
public let generate: Generate<Game>
public let script: Script<Game>
public let world: Game.World
}
where:
generate
exposes all generation functions provided to theGame: Setting
;script
represents the story so far, so all messages sent to the player, plus all additional metadata, if available;world
is the current value of theGame.World
, as defined in theGame: Setting
.
Due to the fact that it's possible to assign unique identifiers to the Game.Message
, whose "seen count" is kept track of by Script
, it's technically possible to structure a complex state of the game without even having a world
: the full script plus the count of observed messages and metadata can potentially be enough to represent even a complex state of the game world. But in general it can be useful to define a specific Game.World
type, that could contain information based on classic patterns (attributes of the player, items and inventory, found leads, discovered locations et cetera).
Every single Step
in a story can be customized based on the Context
and, as we'll see, a Step
could actually simply be an action that updates the Game.World
. The Script
, on the other hand, cannot be updated by a step in the story, and it's exclusively updated by Narratore
itself.
The var steps: Steps
computed property of a Scene
can be augmented with the @SceneBuilder
result builder, that allows to create stories in a natural way. This result builder provides all the expected build_
functions, like buildOptional
, buildEither
and buildArray
, so the composition can be customized based on the Parent
scene that's passed into the function.
In addition to @SceneBuilder
there are other result builders used in the DSL: I'll describe them in detail when needed. But everything starts with simple String
literals.
The available DSL functions are defined as static members of the DO
type, that acts as a namespace for easy access to the functions via autocompletion of DO.
.
DO
is defined as
enum DO<Scene: SceneType> {
...
}
so it's already parametrized on the Scene
.
@SceneBuilder
declares, among other things, the following function:
public static func buildExpression(_ expression: String) -> Component {
...
}
Thus, a SceneStep
can be simply built from a String
. You can take a look at the beginning of the steps
property of Car
scene:
struct Car: SceneType {
var steps: Steps {
...
"You wake up from an unusual dream"
"You were under the sea, walking on the ground as if there was no pull to the surface, nor any resistance from the water itself"
"But you were definitely under the sea, fishes and everything, and the light of the sun reflected on the shimmering water surface, creating a dream-like movement"
...
}
...
}
Simple string literals will be turned into SceneStep
by the SceneBuilder
. Now consider instead the beginning scene Bookshop.Main
:
struct Bookshop_Main: SceneType {
...
var steps: Steps {
switch status {
case .regular:
"The bookshop is barely lit, with some fake candles on the top shelves projecting a faint, shimmering light"
"There's lots of bookshelves, some full of books, some almost empty"
...
}
...
}
Thanks to the powers of the @resultBuilder
, we can switch
over the state of the scene, and produce a sequence of steps that depends on it. Given a uniquely identified state of the scene (remember that Scene
is Hashable
), the sequence of steps must always be the same: but remember that SceneStep
wraps a GetStep
instance, that in turn wraps a function from the game Context
to a Step
, so the specific Step
that is produced from a SceneStep
can actually change, but only a single Step
will eventually be produced (the Step
can actually be a skip
one, as we'll see in a moment).
Finally, you can usually add some properties to the message or narration step represented by a String
, thanks to the .with
extension function defined on it, for example:
"A corpse must be involved in this".with(id: .didSpeculatedAboutTheCorpse)
Internally, a Step
can be constructed (among other things) from a Narration
, which is type that holds an array of Game.Message
, the basic way in which Narratore
communicates some message to the player. This means that a single step can be actually constituted by multiple messages, and the list or content of the messages can depend on the Context
(that is, the state of the story).
The fact that a step can contain multiple message means that the story will be actually considered "advanced by a step" only if all messages are acknowledged by the player: this also applies to state restoration, that is, if a game is restored from a Status
value (check Running the game for more details). This can be convenient: grouping messages that are thematically connected (like in a conversation or a description) can be useful because we might want to restore the state of the game at the beginning of that portion of story, in order to give the player the required context.
If you want to group some messages, and/or make them depend on the state of the story, you can use the DO.tell
function. For example, consider this portion of Bookshop_Main
:
DO.tell {
if !$0.script.didNarrate(.didMeetTheOwner) {
"You don't see people in the store"
"You're quite sure, because you easily see through the empty shelves"
...
"An old man emerges from the desk, behind a pile of books that might or might not be about botanics"
"'Yes?'".with(id: .didMeetTheOwner)
}
}
This uses a classic pattern: if a certain message was acknowledged (the message with id == .didMeetTheOwner
) don't narrate that section again.
The DO.tell
function requires a closure of type (Context<Game>) -> [Game.Message]
, so it takes the game Context
as input, and must return a list of Game.Message
. But DO.tell
uses @MessagesBuilder
so we can define the messages with the regular DSL, with all the regular build_
functions.
Narratore
actually provides 2 separate tell
functions, depending on the context of the function where tell
is called: this allows for a consistent experience, where it's almost always possible to use tell
to group some messages. But the Context
input to the closure is only present when DO.tell
is called at the first level of a @SceneBuilder
: in any other case, a Context
will already be present, so it's not repeated. The other tell
function can be used as .tell
in cases where a single Step
is expected: Step.tell
is a possible constructor.
Within tell
we can also use skip()
to avoid sending messages in certain code paths, useful in case of a switch
, for example in Street_Main
:
...
"Or have weird thoughts"
"Or dreams"
switch theCreatureLookedLike {
case .anAlienBeing?:
"For example, that alien being you dreamed about"
case .aFish?:
"Like of weird fishes"
case .aDarkShadow?:
"Dark dreams, of dark shadows"
default:
skip()
}
"But you're still a well-rounded person"
"Easy to talk to"
...
You can attach an optional update:
closure to a tell
call, in order to strongly associate to that block of messages a change in the Game.World
, for example:
tell {
"You did notice an apartment block, next to the grocery store"
"You should go take a look"
} update: {
$0.didDiscover(.apartment7)
}
The DO.then
function can be used to "jump" to another scene, or to a different place in the same scene, so that the narration continues from there. Jumping to other scenes can be done for several reasons:
- simple narration grouping;
- getting different outcomes from a choice;
- branching out in certain conditions;
- skipping ahead a section of a scene;
- "looping back" in the same scene or in a different one, previously encountered.
The DO.then
function is the basic mechanism with which you can build a story with several sceneing paths, that can also merge together. It's a DSL function, but it's also declared as an optional trailing closure of the tell
function, so it's possible to have a narration step alongside the scene jump.
Internally, Narratore
will keep track of the current scene situation with a stack of scenes: it's possible, in fact, to define scene jumps in a way that allows for "running through" a scene, and then going back to the previous one, at the very next step after the jump. Also, when jumping from a scene to another, it's possible to jump to a specific point in the scene, thanks to the scene Anchor
type.
The DO.then
function requires a SceneChange
value, and there are 3 types of scene changes: let's take a look at them.
This scene change will append a new scene on top of the scene stack, so the narration will continue from the start of the new scene, or from a certain step described by a specific Anchor
value. When the scene ends, it will be removed from the stack, and the narration will continue in the previous scene, from the very next step after the one where the jump occurred.
SceneChange.runThrough
can be used to narrate optional sections of the story, or to narrate a section right before a jump that you want to define in the starting scene.
Use this to replace the scene on top of the stack with another scene: when the new scene ends, it will be removed from the stack and the narration will continue from where it was sceneed from. You should consider SceneChange.replaceWith
as the "default option", to be used in all cases where there is no specific need for particular types of scene transition.
This scene change completely replaces the scene stack with a new one that only contains the scene to which the narration is jumping: this will discard the entire stack, so all previous runThrough
scene changes will be essentially ignored.
Use this for a hard narration changes, for example if the story should go to the ending scene, or for unrecoverable change in some condition in the story, for example if the main character is traveling to another place, or some major change in the world occurs, like a substantial shift in time that would make all previous narration jumps obsolete.
This is conceptually similar to a return
in a Swift function.
Other than advancing the narration, the other main player interaction is making choices. The DSL allows for an expressive way to describe choices (and their consequences), including the possibility to make the available options depend on the script or state of the world.
It starts with the DO.choose
function, that takes a closure enhanced with the @OptionsBuilder
result builder; the closure will also take the Context<Game>
as input, so the available options could depend on the current state the game.
Within the closure passed to choose
we must describe the options that will be presented on the player, with the usual @resultBuilder
features, that is, if-else
scenes, switch
, for-in
and so on. In the end, the @resultBuilder
will need to build an array of options (of type Option<Game>
), which could depend on several conditions: please note that if the conditions produce zero options, the Runner
will send an error to its Handler
and the game will stop.
The DSL allows for building an option by simply writing a String
, with the text that will be presented as option, and calling the onSelect
function on it, for example:
"You look at the photograph"
DO.choose { _ in
"A man".onSelect {
...
}
"A woman".onSelect {
...
}
}
Within the onSelect
closure, __a single stepmust be defined__: this will keep the narration linear, because the
choosefunction will define a single step, that includes both the choice and the result of the choice. But thanks to the
.tellfunction, optionally enchanced by the
then:` trailing closure, it's actually possible to produce multiple messages within the context of an option, and also to jump to another scene, or within the same scene but in a different place. Here's some examples:
"You try to remember what the creature looked like.."
DO.choose { _ in
"Some kind of alien?".onSelect {
.tell {
"...then it comes to your mind: it was some kind of alien being"
"An alien 'entity' could describe it better"
"Eerie, otherworldly"
"You sure have a great imagination, and a great knowledge of eldritch words"
"Including 'eldritch'"
}
}
"Looked like a fish!".onSelect {
.tell {
"...and, unsurprisingly, it looked like a large fish"
"You don't know much about fish: you barely know that there's a distinction between saltwater and freshwater"
"Maybe, in the future, if you see a picture of that particular fish, the dream will come back to your mind"
"But for now, better not to linger"
}
}
"I don't know..".onSelect {
.tell {
"...but you really don't"
"You think about some kind of formless dark shadow"
"But you don't struggle that much: it was just a dream, no use in wasting mental energy in trying to remember what naturally fades away"
}
}
}
"The door is locked".with {
$0.wasTheDoorClosed = true
}
DO.choose(.atTheDoor) {
if $0.world.wasTheKeyFound {
"Open the door with the key".onSelect {
.tell {
"You put the key in the locket and turn it counterclockwise"
"The door unlocks"
"You feel happy, and enter the apartment"
} then: {
.transitionTo(TheApartment())
}
}
}
switch scene.breakTheDoorCounter {
case 0:
"Try to break down the door".onSelect {
.tell {
"You try break down the door with a push"
"The door doesn't bulge"
} then: {
.replaceWith(self.updating { $0.breakTheDoorCounter = 1 }, at: .atTheDoor)
}
}
case 1:
"Try to break down the door again".onSelect {
.tell {
"You try again"
"You're pushing as hard as you can, but your \"build\" is not exactly one of a door-breaker"
} then: {
.replaceWith(self.updating { $0.breakTheDoorCounter = 2 }, at: .atTheDoor)
}
}
default:
"Look for help".onSelect {
.tell {
"You simply can't break the door down"
"Maybe you should look for help"
"It's going to be weird to ask someone to help you break into an apartment"
"But you don't see many alternatives"
} then: {
.replaceWith(LookForHelp())
}
}
}
}
"What will you do?".with(anchor: .whatToDo)
DO.choose {
let (they, _, _) = $0.world.targetPersonPronoun
if scene.didLookAroundOnce {
"Look around some more".onSelect {
.tell {
"You take another look around"
} then: {
.replaceWith(LookAround())
}
}
} else {
"Look around".onSelect {
"You take a look around".then {
.replaceWith(LookAround())
}
}
}
if !scene.didAskAboutTheTarget {
"Ask about the target".onSelect {
.tell {
"You ask the woman at the checkout about the person you're looking for"
} then: {
.replaceWith(AskAboutTheTarget())
}
}
}
if scene.didAskAboutTheTarget, scene.didNoticeMissingBeans {
"Ask about the beans".onSelect {
.tell {
"'Did \(they) buy all the beans in the store?'"
} then: {
.replaceWith(AskAboutTheBeans())
}
}
}
"Get our of here".onSelect {
.tell {
"You decide to leave the grocery store"
"'Goodbye'"
}
}
}
Note that, in the last example, with the "Get our of here"
option, after the .tell
the narration will simply proceed with the very next step after the DO.choose
.
Please also note that, as explained earlier, the DO.choose
function will describe a single step, that includes the choice and the effect of the choice: this means that, before updating the state of the story and the world, the full effect of the choice must take place. For example, if you're persisting the story Status
with the callback handle(event:)
with event .statusUpdated
(see Running the game for more details), the callback will actually be called only after the full effect of the choice has taken place. If you need to immediately persist the choice after it's made, simply return a "jump" step with the then:
trailing closure.
Use can use the DO.check
function to create a Step
that depends on the current Context<Game>
. The closure passed to the check
function must return a single step: Narratore
doesn't allows for branching paths within a single Scene
, but you can use the .tell ... then:
trailing closure (that requires a SceneChange
) to "scene out" from a specific code path of a check
function.
Since the closure passed to DO.check
must return a single Step
, it's possible to take advantage of the autocompletion obtained by writing just .
, that will list the possible Step
constructors.
Even building a single Step
can be good enough in several situations, in fact you can combine DO.check
with the .tell
function to generate a more complex set of conditions and outcomes, for example:
DO.check {
if $0.world.hasDiscovered(.apartment7) {
.tell {
"It's likely the key to the apartment in that apartment block"
}
} else {
.tell {
"You did notice an apartment block, next to the grocery store"
"You should go take a look"
} update: {
$0.didDiscover(.apartment7)
}
}
}
Also, thanks to the then:
trailing closure on the .tell
function, it's possible to send a message with a SceneChange
attached:
DO.check {
if $0.script.narrated[.didThinkAboutTheBookshopFeelings, default: 0] >= 3 {
.tell {
"..."
} then: {
.replaceWith(TheFeelingShop.self, scene: scene)
}
} else {
.tell {
"Let's discard this thought.."
"..."
"...for now"
}
}
}
You can create all types of Step
s in the DO.check
function, including a choice step with choose
:
DO.check {
.inCase($0.world.theCreatureLookedLike == .aDarkShadow) {
.choose {
"It was just a dream".onSelect {
.tell {
"Yes, it was".with
} update: {
$0.increaseMentalHealth()
}
}
"Maybe... not?".onSelect {
.tell {
"Maybe not"
"These thoughts are not helping..."
} update: {
$0.decreaseMentalHealth()
}
}
}
}
}
Note the .inCase
constructor for Step
: this is a simple shorthand for
if condition {
someStep
} else {
.skip
}
useful in cases where there's not going to be a Step
in the else
branch.
In all uncovered code paths, internally, the DO.check
function will produce a .skip
step, and it can also be used manually in case of case
or default
scenes in a switch
that would ideally return with break
.
The DO.update
function can be used to update the state of the Game.World
, and it's also available as an optional trailing closure on tell
and on the with
function of String
. For example
"The rain is thin but persistent"
DO.update {
$0.didDiscover(.bookshop)
$0.didDiscover(.groceryStore)
}
"You look around".with(anchor: .backInStreet)
or
.tell {
"Yes you are, what was I thinking?"
"Occasionally, you might have a weird dream or two"
"But you like the normal world"
"No alternate realities for you"
"And that helps a lot with your line of work"
} update: {
$0.areYouWeird = true
}
or
"The door is locked".with {
$0.wasTheDoorClosed = true
}
In several contexts you'll be able to use the skip
function to simply skip a narration step in specific conditions. You can also use skip
at the root level of a scene to create a step that only acts as "marker", associating it to an Anchor
to be able to jump to that point in the scene, with DO.skip(anchor: ...)
.
Use the group
function to create an array of scene steps – using usual @SceneBuilder
– that can be reused between scenes: this is usually done via a generic function, declared outside a scene (thus, with a generic parameter constrained to be a Scene
), that can be freely called inside scenes, in order to reuse steps.
group
is a lightweight alternative to creating a whole scene and runThrough
it in several other scenes, and here's an example of a generic function that uses group
to create a check that's designed to be run in several scenes, several times during the story:
extension DO where Scene.Game == SimpleStory {
static func checkMentalHealth() -> SceneStep<Scene> {
DO.check {
switch $0.world.mentalHealth {
case 0:
.tell {
"Suddenly, you feel agitated and paranoid"
"You're senses are leaving you..."
"It's like falling asleep..."
} then: {
.transitionTo(PassedOut())
}
case 1 where !$0.script.didNarrate(.gotToMentalHealth1):
.tell {
"You feel confused"
"It seems like you're sweating"
"You're hands are shaking a bit".with(id: .gotToMentalHealth1)
}
case 2 where !$0.script.didNarrate(.gotToMentalHealth2):
.tell {
"You feel a little disoriented"
"It seems like someone is following you"
"Or maybe you're being watched"
"You can't say, but you better watch you back".with(id: .gotToMentalHealth2)
}
case 3 where !$0.script.didNarrate(.gotToMentalHealth3):
.tell {
"You feel a little dizzy"
"Maybe you're just tired"
"Let's hope this case ends soon".with(id: .gotToMentalHealth3)
}
default:
.skip
}
}
}
}
This can be used as follows:
"It almost looks like someone did this on purpose"
DO.check {
.inCase($0.world.didDepleteTheBatteryFaster) {
.tell {
"Your smartphone battery runs off"
"You're left in the dark"
"You can only feel the walls, while looking for the door"
} update: {
$0.decreaseMentalHealth()
}
}
}
DO.checkMentalHealth()
"You keep going"
It's expected that custom functions that can be used in the steps
DSL are defined as extensions of the DO
type, constrained to a specific Scene.Game
type.
This can be used to ask the player to enter some text. In SimpleStory
this is used to ask for the player's name at the start of the first scene of the game:
struct Car: SceneType {
var steps: Steps {
DO.requestText {
"What's your name?"
} validate: { text in
if !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
.valid(.init(text: text))
} else {
.invalid("[Invalid input, please retry]")
}
} ifValid: { _, validated in
.tell {
"\(validated.text)? I like it"
"OK, let's start"
}
}
...
}
}
It's requires a closure to pass the message with the question that will be asked before entering the text (it can be nil
), plus 2 additional closures, one to check if the message is valid (or, if invalid, automatically ask the player for a different text, with some optional message), and one to produce a Step
that will be executed if it's valid: note that the validated text is passed to the ifValid:
closure.
Typically, declarations in a Narratore
story package are not public
, because they're not supposed to be referenced from outside the module, save for the Setting
itself: once the story starts, the story package should run itself automatically, so the scenese don't need to be public
.
But a Runner
, to start from the begining, will need the initial scene. In order to provide the scene without exposing the details outside the package, it's possible to add this declaration to a story package:
/// assuming that the `Setting` is `MyStory`, and that the first scene is `InitialScene`
extension MyStory {
public static func initialScene() -> some SceneType<Self> {
InitialScene()
}
}
Since MyStory
is public
, the Runner
can be initialized with MyStory.initialScene()
.