Documentation
¶
Overview ¶
Package troupe reads files in the Troupe format, checks them, and computes what they mean, as the Troupe specification defines.
ReadStory, ReadSession and ReadPackage read the three forms of a file, Package.ReadSessions reads a package's sessions, and Check runs the checks reading leaves. A document with any diagnostic doesn't conform, and serves only to report them. Package.Open streams a package's other files, such as its images, and ReadPackageStory reads its story.json stored apart from it. Story.FileFields yields the fields that hold a file, and ReplaceFileFields sets them in a document's JSON, keeping every other byte.
For an editor, Story.CheckID checks the ID of an object being added, and Story.RenameID makes the JSON Patch that renames one with every reference to it.
NewPlay makes the play of a session: a story's scenario as the session plays it. NewScenarioPlay makes a scenario's play on its own. A play gives its cast, the worlds in use, its mode, what it calls the narrator, the state keys its changes can write, and its states: Initial, StateAt for a message, and Apply, which gives a message's state from its parent's, so a caller that keeps each message's state never replays a path. CheckChanges judges a message's changes before they are recorded. A State marshals to JSON, and UnmarshalState restores it. From a message's parent's state, KnownBy tells who knows each of its parts and RenderParts shows them. Render resolves the macros in author text, and PointAt builds the point where the next message is written, where Point.Eval evaluates conditions and Point.Active gives the entries active. Root makes the root a session starts with from an opening, and Escape writes a player's or a model's words where macros resolve.
The typed model mirrors documents as written (Format 2.2). An absent field is its zero value, or nil where the zero value means something, and the spec's defaults are left to the operations that apply them; field comments give them. Fields hold strings where the spec has a fixed set of values. In a document that fails the schema, a value of the wrong type reads as absent, and so does a list holding an item of the wrong type. Marshaled to JSON, the model is written as the spec writes it, so a caller can write the messages and characters it makes, and a conforming document marshals as the same JSON.
The package does no I/O, and is deterministic: randomness comes from a source the caller passes. Plays, states and points are safe for concurrent use, and the package only reads the documents it is given, so a story or session read once can serve any number of plays, checks and calls at once.
Index ¶
- Constants
- Variables
- func Escape(text string) string
- func ReadPackage(r io.ReaderAt, size int64) (*Package, []Diagnostic, error)
- func ReadPackageStory(r io.Reader, files []string) (*Story, []Diagnostic, error)
- func ReadSession(r io.Reader) (*Session, []Diagnostic, error)
- func ReadStory(r io.Reader) (*Story, []Diagnostic, error)
- func ReplaceFileFields(doc []byte, values map[string]string) ([]byte, error)
- type ActiveEntry
- type Boundaries
- type CastMember
- type Change
- type Character
- type Code
- type Diagnostic
- type Entry
- type FileField
- type Keys
- type Line
- type Location
- type Member
- type Message
- type Opening
- type Package
- type Part
- type Play
- func (pl *Play) Apply(before State, m *Message) State
- func (pl *Play) Cast() []Member
- func (pl *Play) CheckChanges(before State, changes []Change) []Diagnostic
- func (pl *Play) Initial() State
- func (pl *Play) KnownBy(before State, m *Message) [][]string
- func (pl *Play) Mode() string
- func (pl *Play) NarratorName() string
- func (pl *Play) PointAt(before State, recent []Message, char string) *Point
- func (pl *Play) Render(text string, c RenderContext, r *rand.Rand) string
- func (pl *Play) RenderParts(before State, m *Message, r *rand.Rand) []Part
- func (pl *Play) Root(opening string, r *rand.Rand) (Message, error)
- func (pl *Play) StateAt(id string) (State, error)
- func (pl *Play) StateKeys() []StateKey
- func (pl *Play) UnmarshalState(data []byte) (State, error)
- func (pl *Play) Worlds() []*World
- type Point
- type RenderContext
- type Scenario
- type Session
- type SessionHeader
- type State
- type StateKey
- type Story
- type Variable
- type World
Examples ¶
Constants ¶
const Version = "0.15"
Version is the Troupe version this module reads.
Variables ¶
var ( // ErrBadPointer is for a pointer that names no list or object they take. ErrBadPointer = errors.New("bad pointer") // ErrIDInvalid is for an ID that doesn't fit the grammar or is reserved // (Format 2.3). ErrIDInvalid = errors.New("not a valid ID") // ErrIDTaken is for an ID another object has where it must be unique // (Format 2.3). ErrIDTaken = errors.New("the ID is taken") )
The errors CheckID's and RenameID's errors wrap, one for each way they refuse.
var ErrStateFormat = errors.New("the stored state is in a format this release doesn't read")
ErrStateFormat is what UnmarshalState's error wraps for a state stored in a format this release doesn't read, as another Troupe version's or an earlier release's may be. Apply computes it again.
Functions ¶
func Escape ¶
Escape returns text written so that, where macros resolve, it resolves to exactly text: each \ as \\ and each {{ as \{{ (Format 9). A writer escapes a player's or a model's words before putting them into a session's characters, worlds or entries, in the fields where macros resolve, such as a description or an entry's text, but not in those used as written, such as a name.
func ReadPackage ¶
ReadPackage reads a package, the ZIP archive of size bytes in r, and its story, as a story reader does, leaving its sessions to ReadSessions (Format 12.3, 15.2). It reads every entry section 12.3 allows, so data that can't be read is UNREADABLE wherever it is. Its results are those of ReadStory, for the package as a story reader reads it: the package is nil when the archive is UNREADABLE, or its story.json UNREADABLE or UNSUPPORTED_VERSION, and its diagnostics are then only those. Package.Open and Package.ReadSessions read r again.
Before it judges the limit on entries (Format 14), it parses the archive's central directory, which takes memory proportional to size, so a caller accepting archives from others caps size.
func ReadPackageStory ¶
ReadPackageStory reads story.json as a package holds it, without the package. Files are the paths of the package's files, such as Package.Files and Package.Sessions, and its package paths must name one of them or story.json (Format 12.2, 12.3). Its results are those ReadPackage gives for that story.json, so every diagnostic's File is "story.json".
Example ¶
A package's story.json, stored apart from its ZIP, reads back given the paths of the package's files, such as Package.Files.
package main
import (
"fmt"
"log"
"strings"
"github.com/TroupesAI/troupe-go"
)
func main() {
doc := `{"troupe": "0.15", "kind": "story", "cover": "images/cover.png"}`
st, diags, err := troupe.ReadPackageStory(strings.NewReader(doc), []string{"images/cover.png"})
if err != nil {
log.Fatal(err)
}
fmt.Println(st.Cover, len(diags))
// Without that file, the path names nothing.
_, diags, err = troupe.ReadPackageStory(strings.NewReader(doc), nil)
if err != nil {
log.Fatal(err)
}
for _, d := range diags {
fmt.Println(d.Code, d.File, d.Location())
}
}
Output: images/cover.png 0 UNREADABLE story.json /cover
func ReadSession ¶
func ReadSession(r io.Reader) (*Session, []Diagnostic, error)
ReadSession reads a session file, line by line (Format 11). Its results are those of ReadStory, for the whole file: the session is nil when any line is UNREADABLE, or its header UNSUPPORTED_VERSION, and its diagnostics are then only those.
func ReadStory ¶
func ReadStory(r io.Reader) (*Story, []Diagnostic, error)
ReadStory reads a story file (Format 3, 12.1). It returns the diagnostics of UNREADABLE, UNSUPPORTED_VERSION, SCHEMA and UNKNOWN_FIELD, sorted by location. The story is nil when it is UNREADABLE or UNSUPPORTED_VERSION; with other diagnostics it serves only to report them. The error is r's, when reading it fails.
Example ¶
Reading a story reports what the file breaks, and Check what it means that doesn't hold, here a cast member naming no character.
package main
import (
"fmt"
"log"
"strings"
"github.com/TroupesAI/troupe-go"
)
func main() {
st, diags, err := troupe.ReadStory(strings.NewReader(`{"troupe": "0.15", "kind": "story",
"characters": [{"id": "kai", "name": "Kai", "description": "A thief."}],
"scenarios": [{"id": "heist", "cast": [{}, {"character": "kai"}, {"character": "ghost"}]}]}`))
if err != nil {
log.Fatal(err)
}
diags = append(diags, troupe.Check(st, nil)...)
for _, d := range diags {
fmt.Println(d.Code, d.Location())
}
}
Output: UNKNOWN_CHARACTER /scenarios/0/cast/2/character
func ReplaceFileFields ¶
ReplaceFileFields returns doc, a document's JSON, with the fields that hold a file at the pointers in values, as FileFields gives them, set to their values. Every other byte is kept, so every ext stays as written (Format 13). Its error is for doc that isn't JSON, a pointer that names no string in doc, or a value that isn't an https: URL, a data: URI or a package path as Format 12.2 allows them.
Example ¶
On import, a file a story holds as a data: URI can be stored apart, and its field set to the file's path, with every other byte kept as written.
package main
import (
"bytes"
"fmt"
"log"
"github.com/TroupesAI/troupe-go"
)
func main() {
doc := []byte(`{"troupe": "0.15", "kind": "story", "cover": "data:image/png;base64,iVBORw0KGgo=", "ext": {"x.y": [1, 2.0]}}`)
st, _, err := troupe.ReadStory(bytes.NewReader(doc))
if err != nil {
log.Fatal(err)
}
paths := map[string]string{}
for f := range st.FileFields() {
if f.MediaType != "" {
fmt.Println(f.Pointer, f.MediaType, len(f.Data), "bytes")
paths[f.Pointer] = "images/cover.png"
}
}
doc, err = troupe.ReplaceFileFields(doc, paths)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(doc))
}
Output: /cover image/png 8 bytes {"troupe": "0.15", "kind": "story", "cover": "images/cover.png", "ext": {"x.y": [1, 2.0]}}
Types ¶
type ActiveEntry ¶
type ActiveEntry struct {
// Owner is the entry's owner: a character, world or scenario ID, or
// "session" for a session entry. Render and Eval take it as the owner,
// and the entry's reference is Owner, "/" and its ID (Format 2.4).
Owner string
Entry Entry
// Kept reports whether the entry's sticky keeps it active from a message
// that records it, so the message being written doesn't record it in
// activated again (Format 11.4).
Kept bool
}
ActiveEntry is an entry active for the message being written, or one whose retrieval would activate it (Format 7.2).
type Boundaries ¶
type Boundaries struct {
Lines []string `json:"lines,omitempty"`
Veils []string `json:"veils,omitempty"`
Ext jsontext.Value `json:"ext,omitzero"`
}
Boundaries are the players' lines and veils (Format 11).
type CastMember ¶
type CastMember struct {
Character string `json:"character,omitempty"` // "" for a seat
Player bool `json:"player,omitzero"`
Notes string `json:"notes,omitempty"`
Spoiler bool `json:"spoiler,omitzero"`
Ext jsontext.Value `json:"ext,omitzero"`
}
CastMember is a member of a scenario's cast (Format 6.1).
type Change ¶
type Change struct {
By string `json:"by"` // "opening", "player" or "model"
Set map[string]jsontext.Value `json:"set"`
Ext jsontext.Value `json:"ext,omitzero"`
}
Change is a set of state changes a message records (Format 11).
type Character ¶
type Character struct {
ID string `json:"id"`
Name string `json:"name"`
Aliases []string `json:"aliases,omitempty"`
Description string `json:"description"`
Summary string `json:"summary,omitempty"`
Examples [][]Line `json:"examples,omitempty"`
Entries []Entry `json:"entries,omitempty"`
Variables []Variable `json:"variables,omitempty"`
Avatar string `json:"avatar,omitempty"`
Ext jsontext.Value `json:"ext,omitzero"`
}
Character is a character (Format 4).
type Code ¶
type Code string
Code is the code of a check (Format 15.3), or of a rule CheckChanges judges.
const ( CodeUnreadable Code = "UNREADABLE" CodeUnsupportedVersion Code = "UNSUPPORTED_VERSION" CodeSchema Code = "SCHEMA" CodeUnknownField Code = "UNKNOWN_FIELD" CodeDuplicateID Code = "DUPLICATE_ID" CodeUnknownScenario Code = "UNKNOWN_SCENARIO" CodeUnknownCharacter Code = "UNKNOWN_CHARACTER" CodeUnknownWorld Code = "UNKNOWN_WORLD" CodeUnknownLocation Code = "UNKNOWN_LOCATION" CodeUnknownOpening Code = "UNKNOWN_OPENING" CodeUnknownEntry Code = "UNKNOWN_ENTRY" CodeUnknownStateKey Code = "UNKNOWN_STATE_KEY" CodeInvalidValue Code = "INVALID_VALUE" CodeUnknownMessage Code = "UNKNOWN_MESSAGE" CodeInvalidPlayer Code = "INVALID_PLAYER" CodeUnknownMacro Code = "UNKNOWN_MACRO" CodeRegexError Code = "REGEX_ERROR" CodeCELError Code = "CEL_ERROR" )
The codes of Format 15.3. Reading reports the first four, and Check the others.
type Diagnostic ¶
type Diagnostic struct {
Code Code
// File is the path within a package; "" outside one, or when the
// diagnostic is about a ZIP entry whose name isn't a package path.
File string
// Line is the line of a session, from 1 for its header; 0 in a story and
// from CheckChanges.
Line int
// Message is the message's ID, when its line has a valid one.
Message string
// Pointer is a JSON Pointer within the document, or for CheckChanges
// within the message.
Pointer string
}
Diagnostic is a check a document fails, or a rule a change CheckChanges judges breaks, and where. It never holds text from the document other than the names in its location.
func Check ¶
func Check(st *Story, s *Session) []Diagnostic
Check runs the checks of Format 15.3 that reading leaves: IDs, references, state keys and values, messages and players. st is a story that read without UNREADABLE, and s, when not nil, a session played with it.
The story is checked once for each of its scenarios, as written and played without players, whether or not a session comes with it. A session is then checked against its scenario (conformance README section 2). When its scenario names none, it is reported only as UNKNOWN_SCENARIO, and not at all when it is missing, which is the schema's (Format 15.3). Diagnostics are sorted as reading's are, and their File is empty.
func (Diagnostic) Location ¶
func (d Diagnostic) Location() string
Location gives where the diagnostic is in the conformance suite's form, without the file: "/scenarios/0/when" in a story, "header/players" in a session's header, and "message:3/changes/0" in a message. A message line without a valid ID is "line:<n>" instead, and a diagnostic from CheckChanges is its pointer within the message.
type Entry ¶
type Entry struct {
ID string `json:"id"`
Text string `json:"text"`
Title string `json:"title,omitempty"`
Keywords []string `json:"keywords,omitempty"`
Retrievable bool `json:"retrievable,omitzero"`
When string `json:"when,omitempty"`
ScanDepth *int `json:"scanDepth,omitzero"` // nil for 4
Once bool `json:"once,omitzero"`
Sticky int64 `json:"sticky,omitzero"` // 0 for none
Cooldown int64 `json:"cooldown,omitzero"` // 0 for none
Chance *int `json:"chance,omitzero"` // a percent; nil for 100
Group string `json:"group,omitempty"`
Weight int `json:"weight,omitzero"` // 0 for 1
Priority *int `json:"priority,omitzero"` // nil for 50
Position string `json:"position,omitempty"` // "system" or "context"; "" for context, unless Depth is set
Depth *int64 `json:"depth,omitzero"`
KnownBy []string `json:"knownBy,omitempty"` // nil for everyone
Disabled bool `json:"disabled,omitzero"`
Ext jsontext.Value `json:"ext,omitzero"`
}
Entry is an entry (Format 7).
type FileField ¶
type FileField struct {
Pointer string // the field, as a JSON Pointer into its document
URL string // an https: URL
Path string // a package path
MediaType string // a data: URI's media type, such as "image/png"
Data []byte // a data: URI's content, decoded
}
FileField is a field that holds a file, and its value: an https: URL, a package path or a data: URI, whichever sets its fields (Format 12.2). A data: URI's content isn't checked against its media type: readers leave that to the app that shows it.
type Keys ¶
Keys is what a keyed variable has one value for (Format 8.3). Its zero value means the variable isn't keyed.
func (Keys) MarshalJSON ¶
MarshalJSON writes k as per is written: "cast", "*" or the list.
func (*Keys) UnmarshalJSON ¶
UnmarshalJSON reads k as per is written.
type Line ¶
type Line struct {
Speaker string `json:"speaker,omitempty"` // "char", "player" or a character ID; "" for narration
Text string `json:"text"`
Ext jsontext.Value `json:"ext,omitzero"`
}
Line is a line of a character's example exchange (Format 4).
type Location ¶
type Location struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Parent string `json:"parent,omitempty"`
Image string `json:"image,omitempty"`
Ext jsontext.Value `json:"ext,omitzero"`
}
Location is a place in a world (Format 5).
type Member ¶
type Member struct {
// Character is the member's character, from the story or the session's
// header; nil for a seat without one, which only a scenario played on
// its own has.
Character *Character
// Seat reports whether a person fills the member: a member without a
// character, filled by a character the person brings, one marked player,
// or a new seat after the scenario cast.
Seat bool
// Player reports whether a person plays the character: players lists
// it. A model plays the others.
Player bool
// Notes and Spoiler are the scenario's for the member; a new seat and a
// joiner have none. Notes render with the member's character as
// {{char}}, and the scenario as owner (Format 9). State.Spoilers tells
// whether a spoiler still is one.
Notes string
Spoiler bool
}
Member is a member of a play's cast (Format 6.1, 11.2).
type Message ¶
type Message struct {
ID string `json:"id"`
Parent string `json:"parent,omitempty"`
Opening string `json:"opening,omitempty"`
From string `json:"from"` // "player", "model" or "system"
Char string `json:"char,omitempty"`
Content []Part `json:"content"`
At string `json:"at,omitempty"` // RFC 3339, with its offset as written
Changes []Change `json:"changes,omitempty"`
Activated []string `json:"activated,omitempty"`
Entries []Entry `json:"entries,omitempty"`
Hidden bool `json:"hidden,omitzero"`
Ext jsontext.Value `json:"ext,omitzero"`
}
Message is a line of a session after its header (Format 11).
type Opening ¶
type Opening struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Set map[string]jsontext.Value `json:"set,omitempty"`
Content []Part `json:"content,omitempty"`
Ext jsontext.Value `json:"ext,omitzero"`
}
Opening is a way a scenario starts (Format 6.3).
type Package ¶
type Package struct {
Story *Story
// StoryJSON is story.json as written, which a writer rewrites to keep
// every ext unchanged (Format 13).
StoryJSON []byte
// Sessions are the paths of the package's sessions, sorted, such as
// "sessions/a.troupe-session.jsonl". ReadSessions reads them.
Sessions []string
// Files are the paths of the package's files other than story.json and
// its sessions, sorted. They include files nothing references, which a
// writer keeps (Format 12.3).
Files []string
// contains filtered or unexported fields
}
Package is a package: its story, its sessions and its other files (Format 12.3).
func (*Package) Open ¶
func (p *Package) Open(path string) (io.ReadCloser, error)
Open opens the file of the package at path, such as one of Files, to read its content as a stream. It reads from the io.ReaderAt given to ReadPackage, which must stay unchanged, and is safe for concurrent use. Its error is an fs.ErrNotExist when path names no file of the package.
func (*Package) ReadSessions ¶
func (p *Package) ReadSessions() (map[string]*Session, []Diagnostic, error)
ReadSessions reads the package's sessions, by path, as a session reader does (Format 15.2). Its diagnostics are the package's at that level, ReadPackage's and the sessions', so they replace ReadPackage's. When any session is UNREADABLE or UNSUPPORTED_VERSION, the package fails whole: the sessions are nil, and the diagnostics only those (Format 15.3). The error is r's, when reading it fails.
type Part ¶
type Part struct {
Type string `json:"type,omitempty"` // "" or "text" for text, "image" for an image
Text string `json:"text"`
Src string `json:"src,omitempty"`
Alt string `json:"alt,omitempty"`
Speaker string `json:"speaker,omitempty"` // "" for narration
KnownBy []string `json:"knownBy,omitempty"` // nil for the witness rule (Format 11.5)
Ext jsontext.Value `json:"ext,omitzero"`
}
Part is a part of a message or an opening's content (Format 11). Src and Alt are set only for an image part, and Text only for a text part.
func (Part) MarshalJSON ¶
MarshalJSON writes p with the fields its type has: text for a text part, and src and alt for an image part.
type Play ¶
type Play struct {
// contains filtered or unexported fields
}
Play is a story's scenario as a session plays it, or played on its own. The states it gives are its own: a method that takes a State takes one this play gave. Every method about a message other than StateAt takes the state before it first, so a caller always passes the parent's state.
Example ¶
A play gives the state at each message from its parent's, and evaluates conditions where the next message is being written.
package main
import (
"fmt"
"log"
"strings"
"github.com/TroupesAI/troupe-go"
)
func main() {
st, _, err := troupe.ReadStory(strings.NewReader(`{"troupe": "0.15", "kind": "story",
"characters": [{"id": "kai", "name": "Kai", "description": "A thief."}],
"scenarios": [{"id": "heist", "cast": [{}, {"character": "kai"}],
"variables": [{"id": "bell", "type": "int", "by": ["player"], "description": "The last bell rung."}]}]}`))
if err != nil {
log.Fatal(err)
}
s, _, err := troupe.ReadSession(strings.NewReader(
`{"troupe": "0.15", "kind": "session", "scenario": "heist", "characters": [{"id": "mara", "name": "Mara", "description": "A smuggler."}], "players": ["mara"]}
{"id": "1", "from": "player", "content": [{"speaker": "mara", "text": "We wait for the bell."}], "changes": [{"by": "player", "set": {"state.bell": 7}}]}`))
if err != nil {
log.Fatal(err)
}
play, err := troupe.NewPlay(st, s)
if err != nil {
log.Fatal(err)
}
// A caller that keeps each message's state applies only the next one.
state := play.Initial()
for i := range s.Messages {
state = play.Apply(state, &s.Messages[i])
}
bell, _ := state.Get("state.bell")
fmt.Println("bell:", bell)
// The message being written is for Kai, after message 1.
point := play.PointAt(play.Initial(), s.Messages, "kai")
ok, err := point.Eval("state.bell >= 7 && hit('bell')", "heist", 4)
fmt.Println(ok, err)
}
Output: bell: 7 true <nil>
func NewPlay ¶
NewPlay returns the play of session s, played with story st. Without s, it returns the play of st's first scenario on its own, as NewScenarioPlay does. A session without messages, such as one read from its header alone, serves to compute states with Apply.
It fails when st has no scenario, or s's scenario isn't one of st's. st and s are documents that pass Check; the results for others are only bounded, since the spec gives them no meaning (Format 15.1).
func NewScenarioPlay ¶
NewScenarioPlay returns the play of st's scenario with the ID scenario, played on its own: its seats are unfilled, so it has no players (conformance README section 2). It is the scenario as a person starting a session sees it, before choosing whom to play.
It fails when st has no such scenario.
func (*Play) Apply ¶
Apply returns the state after a message, given the state of this play before it: its parent's, or the initial state for a root. That is the state after the message's changes (Format 8.6), with what else depends on the path, such as the rounds so far, the player's character {{player}} means, the session entries in use, the rounds entries were recorded as activated in and who has been present (Format 6.1, 7.2, 9, 10.1, 11.3). Applying the messages of a path in turn gives the state at its last, so a caller that keeps each message's state never replays a path.
func (*Play) Cast ¶
Cast returns the play's cast, in cast order (Format 6.1, 11.2). In a session, that is the scenario cast with its seats filled by the players' header characters, then the new seats, then the joiners; a seat no player fills is left out. A scenario played on its own has no players, so it lists every member as written, seats included, with the default of one seat and every story character: what a person starting a session can choose from. Its seats aren't in the cast its states have (conformance README section 2).
func (*Play) CheckChanges ¶
func (pl *Play) CheckChanges(before State, changes []Change) []Diagnostic
CheckChanges judges the changes of a message not yet recorded, given before, the state at its parent. Each state key is judged as Check judges a recorded change's (Format 8.5), and against its variable's by, maxStep and oneWay, which Format 8.1 leaves to whoever records a change: the scene has no by, and an opening's change is held to none of them. Each change is judged against the state the ones before it leave. oneWay compares a state key's value there with the one the change gives it. maxStep bounds the sum of the moves of the message's changes by "model", either way, each from the state key's value before the change to the one it gives it, and is reported at the last that sets the state key validly. Values are compared normalized, an absent key of a "*" map counting as the default (Format 8.3, 8.6).
It returns a Diagnostic for each rule a state key breaks, sorted as Check's are, with Line 0 and the state key's Pointer within the message, such as "/changes/0/set/state.bell". Changes it accepts get none from Check once recorded as given; a caller that drops a refused change judges the rest again. Each change's by and set are as the schema requires, and only a root's first change is by "opening" (Format 11.1).
func (*Play) KnownBy ¶
KnownBy returns who knows each part of message m (Format 7.4, 11.5): the IDs of the characters who know it, and "narrator" when the narrator does, in code point order. before is this play's state before m: its parent's, or the initial state for a root.
A part is known by its speaker, by the characters present after m's changes and, unless m is a root, by those present before them: the witness rule. A part's knownBy replaces the rule with exactly the listed characters. The narrator knows a part unless its knownBy leaves the narrator out, and knows every part in narrator mode. Nobody knows a hidden message's parts (Format 11.4).
func (*Play) NarratorName ¶
NarratorName returns what text calls the narrator: the scenario's narratorName, or "the narrator" (Format 6).
func (*Play) PointAt ¶
PointAt returns the evaluation point at the last of recent, for a message written for char, or for the narrator when char is "". recent are the last messages of the path to the point, oldest first, and before is this play's state before the first of them: the initial state when it is a root. recent needs only as many messages as the deepest scan reaches: at most 100 that aren't hidden and have content (Format 7.2). With no messages, the point is before any message, at before.
Building a point does the work every condition at it shares, so a caller evaluating many conditions builds it once.
func (*Play) Render ¶
Render returns author text with its macros resolved (Format 9): the texts of a story, and of a session's characters, worlds and entries, where macros resolve. A message part's text is RenderParts's to show, since a player's or model's words are used as written. r chooses each pick's option, so rendering is reproducible from the caller's source.
Text between {{ and }} that is no macro, or that names an undeclared variable, is left as written; a declared variable with nothing to show renders as nothing.
func (*Play) RenderParts ¶
RenderParts returns m's parts as they are shown (Format 9), given this play's state before m, as KnownBy and Apply take it. In a root made from an opening, their text and alt resolve at the root, with each part's speaker as {{char}} and own meaning state; r chooses a pick's option, though a root has its picks chosen already (Format 11.1). Any other message is a player's or a model's words, used as written, so no state leaks through model output or prompt attacks (Format Appendix C).
func (*Play) Root ¶
Root returns the root a session starts with when it plays the scenario's opening with the ID opening (Format 11.1): a system message naming the opening. Its content is the opening's as written, with each pick replaced by the option r chooses, escaped, or empty when the opening has none. When the opening has a set, the root records it as its first change, by "opening", without the state keys of each cast member marked player that players doesn't list, its character's variables and its key of "cast" variables, and with the players' characters added to the scene.present it sets. When no state key is left, it records no change. The caller gives the root its ID, and its at.
It fails when the scenario has no such opening.
func (*Play) StateAt ¶
StateAt returns the state at the session's message with the ID id, after its changes: those of every message on the path from its root, in order (Format 8.6).
func (*Play) StateKeys ¶
StateKeys returns the state keys the play's changes and its openings' set can write (Format 6.3, 8.5): scene.location and scene.present, then each variable in use, the scenario's, its worlds' and its characters', with a keyed variable's keys in their order, as State.All yields them. In a scenario played on its own, a "cast" variable's keys also include the characters of seats marked player, which its openings may set though its states leave them out (Format 8.5); a root leaves them out of a session where no player fills the seat (Format 11.1).
It offers what is valid, and Check still judges what is written: for a draft that fails Check, it gives the state keys of what reads, each once.
Example ¶
An editor offers the state keys an opening's set can write, each with the variable that says how to enter its value.
package main
import (
"fmt"
"log"
"strings"
"github.com/TroupesAI/troupe-go"
)
func main() {
st, _, err := troupe.ReadStory(strings.NewReader(`{"troupe": "0.15", "kind": "story",
"characters": [{"id": "kai", "name": "Kai", "description": "A thief.",
"variables": [{"id": "nerve", "type": "int", "min": 0, "max": 5, "description": "How steady Kai is."}]}],
"scenarios": [{"id": "heist", "cast": [{}, {"character": "kai"}],
"variables": [
{"id": "bell", "type": "int", "description": "The last bell rung."},
{"id": "trust", "type": "int", "per": "cast", "description": "Trust in the player."},
{"id": "loot", "type": "int", "per": "*", "description": "What the crew carries."}]}]}`))
if err != nil {
log.Fatal(err)
}
play, err := troupe.NewScenarioPlay(st, "heist")
if err != nil {
log.Fatal(err)
}
for _, k := range play.StateKeys() {
switch {
case k.Variable == nil:
fmt.Println(k.Name)
case k.Key == "*":
fmt.Println(k.Name+".<any ID>", k.Variable.Type, k.Owner)
default:
fmt.Println(k.Name, k.Variable.Type, k.Owner)
}
}
}
Output: scene.location scene.present state.bell int heist state.trust.kai int heist state.loot.<any ID> int heist state.kai.nerve int kai
func (*Play) UnmarshalState ¶
UnmarshalState returns the state that State.MarshalJSON wrote as data. It is this play's state as it was stored, but for what the play has added since: a state key the stored state doesn't hold, such as one of a joiner the session's header has added, holds its initial value (Format 8.3).
It fails when data isn't a stored state, or holds a state key or value that isn't valid in this play (Format 8.5), and wraps ErrStateFormat when the state is stored in a format this release doesn't read.
type Point ¶
type Point struct {
// contains filtered or unexported fields
}
Point is where conditions are evaluated: the parent of the message being written, for a character or the narrator (Format 10.1).
func (*Point) Active ¶
func (p *Point) Active(retrieved []string, r *rand.Rand) []ActiveEntry
Active returns the entries active for the message being written at p (Format 7.2), in the order of their owners, the scenario, its worlds, the characters of the cast and the session, and each owner's in its order. The entries in use are the scenario's, its worlds', those of the characters present or the message is written for, and the session's; of these, those whoever the message is for knows (Format 7.4).
An entry with keywords activates when one matches its scanned text, a retrievable one when retrieved names its reference, one with both by either, and one with neither always, while its when holds. retrieved are the references of the entries the caller retrieved, of those Retrievable gives; any other reference changes nothing. The rounds of the messages that record an entry in activated time it: sticky keeps it active, and once and cooldown keep it from activating again. Of the entries that would activate, group keeps one of each owner's group, of the highest priority, chosen by weight among equals, where an entry sticky keeps wins against equals that aren't kept, and then chance lets each through, so a chosen entry that fails it leaves its group empty. r draws for weight and chance, so activation is reproducible from the caller's source.
Which active entries reach the model is the caller's to choose, by their priority, and where each goes by its position or depth (Format 7.3). The message being written records those that reach it in activated, apart from those Kept; one whose text renders as nothing or only whitespace reaches nothing (Format 11.4).
Example ¶
A session starts from an opening's root. For each message, the caller restores the stored state before the messages a scan reaches, builds the point after them, and gets the entries active for the message being written.
package main
import (
"bytes"
"encoding/json/v2"
"fmt"
"log"
"math/rand/v2"
"strings"
"github.com/TroupesAI/troupe-go"
)
func main() {
st, _, err := troupe.ReadStory(strings.NewReader(`{"troupe": "0.15", "kind": "story",
"characters": [{"id": "kai", "name": "Kai", "description": "A thief.",
"entries": [{"id": "fear", "text": "Kai fears heights.", "keywords": ["/roofs?/"], "sticky": 2}]}],
"scenarios": [{"id": "heist", "cast": [{}, {"character": "kai"}],
"openings": [{"id": "rooftop", "set": {"scene.present": ["kai"]},
"content": [{"text": "Rain on the {{pick::roof::slates}}."}]}]}]}`))
if err != nil {
log.Fatal(err)
}
// The player's character, as a person typed it: its description is
// escaped, since macros resolve there.
header, err := json.Marshal(troupe.SessionHeader{
Troupe: troupe.Version,
Scenario: "heist",
Characters: []troupe.Character{{
ID: "mara", Name: "Mara", Description: troupe.Escape("A smuggler {{who never pays}}."),
}},
Players: []string{"mara"},
})
if err != nil {
log.Fatal(err)
}
s, _, err := troupe.ReadSession(bytes.NewReader(header))
if err != nil {
log.Fatal(err)
}
play, err := troupe.NewPlay(st, s)
if err != nil {
log.Fatal(err)
}
r := rand.New(rand.NewPCG(1, 2)) //nolint:gosec // G404: a seeded source makes the example reproducible
root, err := play.Root("rooftop", r)
if err != nil {
log.Fatal(err)
}
root.ID = "1"
fmt.Println(root.Content[0].Text, string(root.Changes[0].Set["scene.present"]))
stored, err := json.Marshal(play.Apply(play.Initial(), &root))
if err != nil {
log.Fatal(err)
}
// Message 2 is written for Kai, after the root, which the scan reaches.
reply := troupe.Message{ID: "2", Parent: "1", From: "model", Char: "kai", Content: []troupe.Part{{Text: "Kai grips the ledge."}}}
for _, a := range play.PointAt(play.Initial(), []troupe.Message{root}, "kai").Active(nil, r) {
fmt.Println(a.Owner+"/"+a.Entry.ID, a.Kept)
if !a.Kept {
reply.Activated = append(reply.Activated, a.Owner+"/"+a.Entry.ID)
}
}
// Message 3 is written for Kai too, after message 2, from the root's
// stored state.
before, err := play.UnmarshalState(stored)
if err != nil {
log.Fatal(err)
}
for _, a := range play.PointAt(before, []troupe.Message{reply}, "kai").Active(nil, r) {
fmt.Println(a.Owner+"/"+a.Entry.ID, a.Kept)
}
}
Output: Rain on the roof. ["kai","mara"] kai/fear false kai/fear true
func (*Point) Eval ¶
Eval evaluates the condition expr of an entry owned by self, a character, world or scenario ID, or "session" for a session entry, which scans scanDepth messages: the entry's, 4 when it has none (Format 7.2, 10). It returns an error only when expr is a CEL_ERROR; a runtime error makes the condition false.
func (*Point) Retrievable ¶
func (p *Point) Retrievable() []ActiveEntry
Retrievable returns the retrievable entries that retrieving would activate at p, in Active's order: those that activate when retrieved and not otherwise, and that can win their group, since no entry of it active without retrieval has a higher priority, or an equal one and is kept (Format 7.2). chance is drawn only once they are retrieved. The caller retrieves those that are relevant, by their title or text, and passes their references to Active.
type RenderContext ¶
type RenderContext struct {
// State is the state the macros read, a state of the play: at the
// message the text is in, or elsewhere at the evaluation point, the
// session's head, or the initial state when there is no head (Format 9).
// {{player}} comes from it too.
State State
// Char is the character {{char}} means: a part's speaker, the character
// the text belongs to, or the one the message being written is for. ""
// means none.
Char string
// Narrator reports whether the text is for a message written for the
// narrator, so {{char}}, without Char, renders as NarratorName gives it.
Narrator bool
// Owner is the text's owner, which own means: a character, world or
// scenario ID, or "session" or "" for a session entry, which has none
// (Format 8.2).
Owner string
}
RenderContext is where author text is rendered (Format 9).
type Scenario ¶
type Scenario struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Tagline string `json:"tagline,omitempty"`
Premise string `json:"premise,omitempty"`
Briefing string `json:"briefing,omitempty"`
Mode string `json:"mode,omitempty"` // "characters" or "narrator"; "" for characters
NarratorName string `json:"narratorName,omitempty"` // "" for "the narrator"
Cast []CastMember `json:"cast,omitempty"`
// Worlds is nil when absent, which means every story world, and points to
// an empty list for none.
Worlds *[]string `json:"worlds,omitzero"`
Openings []Opening `json:"openings,omitempty"`
Entries []Entry `json:"entries,omitempty"`
Variables []Variable `json:"variables,omitempty"`
Ext jsontext.Value `json:"ext,omitzero"`
}
Scenario is a scenario (Format 6).
type Session ¶
type Session struct {
Header SessionHeader
Messages []Message // in line order
}
Session is a session: its header line, then its messages (Format 11).
type SessionHeader ¶
type SessionHeader struct {
Schema string `json:"$schema,omitempty"`
Troupe string `json:"troupe"`
Name string `json:"name,omitempty"`
Story string `json:"story,omitempty"`
Boundaries Boundaries `json:"boundaries,omitzero"`
Scenario string `json:"scenario"`
Characters []Character `json:"characters,omitempty"`
Players []string `json:"players,omitempty"`
Worlds []World `json:"worlds,omitempty"`
Head string `json:"head,omitempty"`
Ext jsontext.Value `json:"ext,omitzero"`
}
SessionHeader is the first line of a session (Format 11).
func (SessionHeader) MarshalJSON ¶
func (h SessionHeader) MarshalJSON() ([]byte, error)
MarshalJSON writes h as a session's header line, its kind included, with the members of maps sorted, as Story.MarshalJSON does.
type State ¶
type State struct {
// contains filtered or unexported fields
}
State is the state at a point of a play (Format 8.6). The zero State holds nothing.
func (State) All ¶
All yields every state key the state holds with its value, as Get gives them: scene.location and scene.present, then each variable in use, the scenario's, its worlds' and its characters', with a keyed variable's keys in their order: the cast's, the list's, or for a "*" map, code point order (Format 8.3).
func (State) Get ¶
Get returns the value at key, a state key (Format 8.5): "scene.location", "scene.present", a variable's full name, or a keyed variable's full name and one of its keys, such as "state.trust.kai". The value is a bool, an int64, a string, or a []string for a set and for scene.present. It reports false when the state holds nothing at key, as for a key a "*" map doesn't have.
func (State) MarshalJSON ¶
MarshalJSON returns s as JSON that UnmarshalState reads back: every state key and value s holds, and what else it keeps that depends on the path, such as the rounds, the session entries in use, when entries were recorded as activated and who has been present. The JSON names this module's Troupe version; a later release reads it back or wraps ErrStateFormat. No string in it holds U+0000, so a database's JSON type that refuses it, such as PostgreSQL's jsonb, can hold it.
It fails for the zero State.
func (State) Spoilers ¶
Spoilers returns the characters of the cast members marked spoiler that are still spoilers for players at s, in cast order: those that no state at a message on its path, the root's included, has present (Format 6.1). The cast starts present, so only a root that leaves a member out of scene.present, such as through an opening's set, keeps it a spoiler.
type StateKey ¶
type StateKey struct {
// Name is the state key: "scene.location", "scene.present", a variable's
// full name (Format 8.2), or a keyed variable's full name and one of its
// keys, such as "state.trust.kai". For a "*" map, whose keys can't be
// listed, it is the variable's full name.
Name string
// Key is a keyed variable's key: a cast character's ID for "cast", or one
// of its list, and "*" for a "*" map, where any author ID other than char
// and player is a key, written after Name and a dot (Format 8.3). It is ""
// for the scene and for a variable that isn't keyed.
Key string
// Variable is the variable the state key writes, which gives its type and
// values; nil for the scene: scene.location, a location ID or "", and
// scene.present, the characters present (Format 8.4).
Variable *Variable
// Owner is the variable's owner, the scenario's, a world's or a
// character's ID; "" for the scene.
Owner string
}
StateKey is a state key a play's changes can write (Format 8.5).
type Story ¶
type Story struct {
Schema string `json:"$schema,omitempty"`
Troupe string `json:"troupe"`
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Tagline string `json:"tagline,omitempty"`
Version string `json:"version,omitempty"`
Authors []string `json:"authors,omitempty"`
Credits string `json:"credits,omitempty"`
Language string `json:"language,omitempty"`
Tags []string `json:"tags,omitempty"`
Rating string `json:"rating,omitempty"` // "general", "mature" or "explicit"; "" for unrated, which may be explicit
// Warnings is nil when absent, which says nothing about the content, and
// points to an empty list when no warning applies.
Warnings *[]string `json:"warnings,omitzero"`
Cover string `json:"cover,omitempty"`
Characters []Character `json:"characters,omitempty"`
Worlds []World `json:"worlds,omitempty"`
Scenarios []Scenario `json:"scenarios,omitempty"`
Ext jsontext.Value `json:"ext,omitzero"`
}
Story is a story (Format 3).
func (*Story) CheckID ¶
CheckID reports whether an object added to the list at the JSON Pointer list can take the ID id (Format 2.3). The list is "/characters", "/worlds", "/scenarios", a world's "/worlds/0/locations", or the entries, variables or openings of a character, world or scenario, such as "/scenarios/0/openings". The error wraps ErrBadPointer for a pointer that names no such list, ErrIDInvalid, or ErrIDTaken when an object the list's objects must be unique among has id.
func (*Story) FileFields ¶
FileFields yields the story's fields that hold a file: its cover, then its characters' avatars, its locations' images and the image parts of its openings. It decodes each data: URI as it yields it, so only one is in memory at a time. In a story that fails the schema, a value of none of the three forms is left out.
func (Story) MarshalJSON ¶
MarshalJSON writes st as a story document, its kind included, with the members of maps such as an opening's set sorted, so the same story always gives the same bytes.
func (*Story) RenameID ¶
RenameID returns the JSON Patch (RFC 6902) that gives the object at the JSON Pointer object the ID id, and rewrites every reference to it in the story, unless another object shares its ID; for the ID the object has, an empty one. The object is a character, world or scenario, such as "/characters/0", a world's location, or an entry, variable or opening, such as "/worlds/1/locations/2" or "/scenarios/0/variables/3". The patch moves a member it renames to the end of its object.
The references are the cast members, scenario worlds, parents, knownBy and speakers that name it, the state keys of openings' set and the scene values they write, and the names macros and conditions read, each resolved as Format 9 and 10 resolve it in the scenarios that use it. A keyed variable's key that is a renamed character's ID, as it is written in per, initial, set, macros and conditions, is renamed with it, since it shows as the character's name and char looks it up (Format 9, 10.1). In conditions, a string literal is a reference when it is compared with char, player, self, scenario, opening, scene.location, scene.present, scene.within or players.
It also returns pointers, into the story as it is, to the texts, conditions and values it leaves as written, whole, since rewriting them could change what they mean:
- a string value that is a renamed character's or location's ID, which shows as its name (Format 9): an enum's or set's values or labels, an initial value, or a value an opening sets, and a string literal equal to it that a condition uses any other way;
- a name in text or a condition of a character or world that refers to the object in one scenario that uses it, and to something else in another;
- a string literal that refers to the object but is written with escapes or as a raw string, a name that does but is written between backticks, a condition that doesn't parse and has the ID as a word, and a key whose renamed form its object already has;
- a renamed character's key of a list or "*" variable, wherever it is written, when the story already has the new ID as a word, since it may be a key of the same variable, which the two would merge into.
When another object shares the object's ID, which is DUPLICATE_ID (Format 15.3), what a reference to the ID refers to is the author's to say, so the patch only gives the object the ID, and every reference that could refer to the object, were it alone to have the ID, is left as written, and returned.
The error wraps ErrBadPointer for a pointer that names no such object, and otherwise ErrIDInvalid or ErrIDTaken as CheckID's does. In a story that fails the schema, a value that reads as absent is left as written, and not returned. A session played with the story isn't changed, so its references to the old ID name nothing.
Example ¶
An editor renames a character, and every reference to it, with a JSON Patch, which a patch library applies to the stored story.json.
package main
import (
"errors"
"fmt"
"log"
"strings"
"github.com/TroupesAI/troupe-go"
)
func main() {
st, _, err := troupe.ReadStory(strings.NewReader(`{"troupe": "0.15", "kind": "story",
"characters": [{"id": "kai_moreau", "name": "Kai Moreau", "description": "A thief."}],
"scenarios": [{"id": "heist", "cast": [{}, {"character": "kai_moreau"}],
"entries": [{"id": "watch", "text": "The guard is near.", "when": "'kai_moreau' in scene.present"}]}]}`))
if err != nil {
log.Fatal(err)
}
if err := st.CheckID("/characters", "kai_moreau"); errors.Is(err, troupe.ErrIDTaken) {
fmt.Println("taken")
}
patch, left, err := st.RenameID("/characters/0", "kai")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(patch), left)
}
Output: taken [{"op":"add","path":"/characters/0/id","value":"kai"},{"op":"replace","path":"/scenarios/0/cast/1/character","value":"kai"},{"op":"replace","path":"/scenarios/0/entries/0/when","value":"'kai' in scene.present"}] []
type Variable ¶
type Variable struct {
ID string `json:"id"`
Type string `json:"type"` // "bool", "int", "string", "enum" or "set"
Values []string `json:"values,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
Min *int64 `json:"min,omitzero"`
Max *int64 `json:"max,omitzero"`
Initial jsontext.Value `json:"initial,omitzero"` // nil for the type's default
Per Keys `json:"per,omitzero"`
Description string `json:"description"`
By []string `json:"by,omitempty"` // "player" and "model"
MaxStep int64 `json:"maxStep,omitzero"`
OneWay bool `json:"oneWay,omitzero"`
Spoiler bool `json:"spoiler,omitzero"`
Ext jsontext.Value `json:"ext,omitzero"`
}
Variable is a variable's declaration (Format 8).
type World ¶
type World struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Locations []Location `json:"locations,omitempty"`
Entries []Entry `json:"entries,omitempty"`
Variables []Variable `json:"variables,omitempty"`
Ext jsontext.Value `json:"ext,omitzero"`
}
World is a world (Format 5).
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
condition
Package condition compiles and evaluates conditions: CEL expressions, narrowed by Format 10.2.
|
Package condition compiles and evaluates conditions: CEL expressions, narrowed by Format 10.2. |
|
ijson
Package ijson reads I-JSON (RFC 7493) as Format 2.2 requires, into the generic tree the schema validator takes.
|
Package ijson reads I-JSON (RFC 7493) as Format 2.2 requires, into the generic tree the schema validator takes. |
|
keywords
Package keywords matches entry keywords against text (Format 7.2).
|
Package keywords matches entry keywords against text (Format 7.2). |
|
macro
Package macro parses text with macros (Format 9).
|
Package macro parses text with macros (Format 9). |
|
regex
Package regex compiles regex keywords: patterns in the syntax ECMAScript and RE2 share, with ECMAScript's meaning under the u flag (Format 14).
|
Package regex compiles regex keywords: patterns in the syntax ECMAScript and RE2 share, with ECMAScript's meaning under the u flag (Format 14). |