Skip to content

Latest commit

 

History

History
165 lines (118 loc) · 9.52 KB

DEFINING_A_GAME_SETTING.md

File metadata and controls

165 lines (118 loc) · 9.52 KB

Defining a game setting

All the code examples in this document are taken from the SimpleSetting module of the companion package.

A game Setting declares the types used to a make a specific story unique. In order to define a Game in Narratore, we need to declare a game type, and make it conform to Setting:

public enum SimpleSetting: Setting {
  ...
}

The game type can be an enum, because it's only expected to declare some associated types, and no state.

Setting requires the definition of 4 associated types:

  • Generate: a type designed to generate values, for example random numbers;
  • Message: the fundamental communication mechanism between Narratore and the player;
  • Tag: additional metadata that can be attached to each step in the narration;
  • World: use this type to define the state of your game; for example, it could contain the character's attributes, the inventory, experience points, but also the state of the world, events that happened, cases solved, other characters et cetera.

Each associated type has additional requirements, let's see them in detail.

Generate

The purpose of the Generate type is to provide a game Setting with an integrated system to generate values (for example, random numbers). The type must conform to the Generate protocol, that currently only requires a static function to produce a random ratio between 0 and 1, and a static function to produce a unique string: the protocol could be expanded in the future with extra requirements, like a function to generate progressive integers, or a function to hash a string.

For testing purposes, it can be useful to give a Generate type some way to fix the values that are going to be produced, for example to control randomness.

Here's possible Generate definition for our SimpleSetting:

public enum SimpleSetting: Setting {
  ...
  public enum Generate: Generating {
    public static var getFixedRandomRatio: (() -> Double)? = nil
    public static var getFixedUniqueString: (() -> String)? = nil

    public static func randomRatio() -> Double {
      getFixedRandomRatio?() ?? Double((0...1000).randomElement()!)/1000
    }
    
    public static func uniqueString() -> String {
      getFixedUniqueString?() ?? UUID().uuidString
    }
  }  
}

Message

A message can be as simple as a String, but thanks to the fact that in Narratore a Message is a generic type, it's possible to obtain more sophisticated results, for example attaching the message to a character, or handling story localization in a convenient way.

The Message associated type is expected to conform to the Messaging protocol, that declares some requirements:

  • it must be Codable & CustomStringConvertible;
  • it must define an ID associated type;
  • it must have 2 properties, text: String and id: ID?;
  • it must be constructible with a specific initializer.

Thanks to these requirements, any Message type will support the DSL utilities described in Writing a story. Also, Narratore provides a default implementation for description that simply returns the value of the text property.

In order to continue defining our SimpleSetting, let's define a SimpleMessage as the minimal type that can conform to Messaging

public enum SimpleSetting: Setting {
  ...  
  public struct Message: Messaging {
    public var id: ID?
    public var text: String
    
    public init(id: ID?, text: String) {
      self.id = id
      self.text = text
    }
    
    public struct ID: Hashable & Codable & ExpressibleByStringLiteral & CustomStringConvertible {
      public var description: String
      
      public init(stringLiteral value: String) {
        self.description = value
      }
    }
  }
  ...
}

Instead of simply using String for the associatedtype ID, we defined a basic ID type that can essentially be created from and transformed to a String. The advantage of specifying a type is that we'll be able to extend this and give it more power if it's needed. Defining a specific type for something instead of using a typealias is the more flexible option, but it requires a small amount of boilerplate (ID in fact is essentially wrapping of String not much more).

Tag

Each narration step in Narratore can be assigned zero or more Tags, that represents additional metadata to take into account when that narration step is received by the Handler (see Running the game for more details). For example, a Tag can be associated with showing some image in the game, or playing a sound, or modifying the font or the message text, or can be used to start a timer or attach some additional information that's relevant to the state of the game in general, but doesn't affect the way some particular narration step is communicated to the player.

The Tag type must conform to the Tagging protocol, that makes it Hashable and Codable, and requires a shouldObserve: Bool property: if shouldObserve is true the tag will recorded in the global state of the game, and the count of observations for that tag can always be accessed from the Script type.

Let's add a simple Tag definition to SimpleSetting:

public enum SimpleSetting: Setting {
  ...
  public struct Tag: Tagging & CustomStringConvertible {
    public var value: String
    public var shouldObserve: Bool
    
    public init(_ value: String, shouldObserve: Bool = false) {
      self.value = value
      self.shouldObserve = shouldObserve
    }
    
    public var description: String {
      value
    }
  }
  ...  
}

World

The World type should represent the state of the game world. A game in Narratore must be started with an initial value for World, and it can be easily changed and manipulated during the course of the story, or even by the game engine itself, in case we need to make changes when running a game: for example, if a game has an inventory, and the player can interact with it outside of the story, we can reflect this change to the game world in the Handler (see Running the game).

The only requirement for World is to be Codable, because Narratore expects the full state of the game (including World) to be serialized when running the game, so that after exiting and entering it again, such state can be restored.

World is an important part of Narratore, but there's no need to record everything in it: in fact, Narratore already records many aspects of the story, for example the messages a player has read, or the observed tags: in order to see how to keep track of the game state outside of World, please check Writing a story.

Ideally, a World is very specific to a certain type of game setting: for example, in a role playing game we would expect to have the player's attributes, experience and health defined in the world, while in a visual novel we would probably care more about the kind of relationships the main character has with other characters. But for now, let's define a World for our SimpleSetting that's sufficiently generic to be able to contain all sorts of information:

public enum SimpleSetting: Setting {
  public struct World: Codable {
    public var value: [Key: Value] = [:]
    public var list: [Key: [Value]] = [:]
    
    public init() {}

    public struct Key: ExpressibleByStringLiteral & CustomStringConvertible & Codable & Equatable & Hashable {
      public var description: String
      public init(stringLiteral value: String) {
        description = value
      }
    }

    public struct Value: ExpressibleByStringLiteral & CustomStringConvertible & Codable & Equatable {
      public var description: String
      public init(stringLiteral value: String) {
        description = value
      }
    }
  }
  ...
}

Like we did with the Message.ID, we defined specific types for the Keys and the Values recorded in the World dictionaries – even if they're simple wrappers for a String – in order to achieve more flexibility in later usage.

Scenes

To be runnable, a game in Narratore must conform to the Story protocol. Story inherits from Setting and adds a requirement for a scenes property.

There is one fundamental grouping mechanism for a story in Narratore: the Scene, which is represented via the SceneType protocol. A Scene is a linear portion of the story, defined via a series of narration steps, and a story is made by a set of Scenes. More details can be found in Writing a story, but the relevant part here is that Narratore must be able to reference all scenes statically, for deserialization reasons: when restoring a previously started game, Narratore must be able to decode each Scene from the serialized Data, and to do that it must look into the catalog of scenes declared in the scenes property of the game Story.

The type of the required scenes property is [RawScene<Self>]: a RawScene<Game> is a "raw" representation of a Codable scene, and encapsulates the decoding mechanism for a specific scene. When writing a story in Narratore, it's expected that when a Scene is added, the _Raw version should also be added to the scenes property of the Game.

The separation between Setting and Story allows to create reusable Settings, from which multiple Storyes can be created (check Extending Narratore for more details) so, for now, the SimpleSetting definition is complete; now, let's see how to write a story in Writing a story.