troupe

package module
v0.0.0-...-98955c7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 3, 2026 License: Apache-2.0 Imports: 32 Imported by: 0

README

troupe-go

A Go library for Troupe, an open file format for AI roleplay. It reads Troupe files, checks them, and computes what they mean: the state at a message, how macros render, how conditions evaluate and who knows what.

Status: in development, targeting Troupe 0.15, the spec's current draft. It passes the whole conformance suite; the plan says what is still to come.

Scope

In scope: everything the spec defines.

  • Reading stories, packages and sessions: I-JSON, the limits, versions, the schemas and unknown fields (Format 2.2, 11, 12, 13, 14).
  • The checks and their codes (Format 15.3).
  • The state at a message (Format 8.6).
  • Macros (Format 9) and conditions in CEL (Format 10).
  • Entry activation (Format 7.2) and knowledge (Format 7.4, 11.5).
  • Writing: roots made from openings (Format 11.1), escaping (Format 9), files written back with every ext unchanged (Format 13), and IDs checked as Format 2.3 requires and renamed with their references.

Out of scope: everything the spec leaves to apps (Format 1.1). That includes prompt assembly, choosing which active entries reach a model, model calls, storage and UI.

Rules

  • The spec decides. When the code and the spec disagree, the code is wrong. When the spec is wrong or unclear, it's fixed upstream, never worked around here.
  • No I/O. The library doesn't open files or network connections, and it doesn't read the environment. Callers pass the data in.
  • Deterministic. Randomness, such as {{pick}} and chance, comes from a source the caller supplies, so every result can be reproduced.
  • Bounded. The limits of Format 14 are enforced while reading, before any expensive work, so hostile input can't exhaust memory or CPU.
  • Content stays private. Errors and diagnostics carry codes, locations and IDs, never text from a document, since apps log them.

Conformance

The spec's conformance suite is the main test. The suite, the schemas and the examples some cases read are copied into this repository from a pinned spec commit. A runner executes every case through the public API. CI fails when a case fails, or when the copy no longer matches its commit.

Versioning

Before 1.0 there are no releases: callers require a pushed commit as a pseudo-version. From 1.0, releases are tagged v<spec major>.<spec minor>.<patch>, so v1.0.2 reads Troupe 1.0.

Development

The tools are pinned in mise.toml. Run mise install, then:

Task Does
task lint Lints the code and checks dependencies for vulnerabilities
task test Runs the tests, the conformance suite included
task check Runs what CI runs: lint, tests and the drift check
task spec REF=<tag or commit> Replaces the copy in spec/ with that commit's schemas, suite and examples
task spec:local Copies them from a checkout at ../troupe, to try a spec change before it's pushed. The drift check fails until the copy comes from a pushed commit

Never edit spec/ by hand. spec/REF names the commit it comes from.

License

Apache-2.0, apart from spec/examples/, the spec's worked examples copied from the spec repository, which are CC BY 4.0 as they are there.

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

Examples

Constants

View Source
const Version = "0.15"

Version is the Troupe version this module reads.

Variables

View Source
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.

View Source
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

func Escape(text string) string

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

func ReadPackage(r io.ReaderAt, size int64) (*Package, []Diagnostic, error)

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

func ReadPackageStory(r io.Reader, files []string) (*Story, []Diagnostic, error)

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

func ReplaceFileFields(doc []byte, values map[string]string) ([]byte, error)

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.

const (
	CodeBy      Code = "BY"
	CodeMaxStep Code = "MAX_STEP"
	CodeOneWay  Code = "ONE_WAY"
)

The codes of a change that breaks its variable's by, maxStep or oneWay, which no check of Format 15.3 compares changes with, and only CheckChanges reports (Format 8.1).

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

type Keys struct {
	Cast bool     // "cast"
	Any  bool     // "*"
	List []string // the keys listed
}

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

func (k Keys) MarshalJSON() ([]byte, error)

MarshalJSON writes k as per is written: "cast", "*" or the list.

func (*Keys) UnmarshalJSON

func (k *Keys) UnmarshalJSON(data []byte) error

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

func (p Part) MarshalJSON() ([]byte, error)

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

func NewPlay(st *Story, s *Session) (*Play, error)

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

func NewScenarioPlay(st *Story, scenario string) (*Play, error)

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

func (pl *Play) Apply(before State, m *Message) State

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

func (pl *Play) Cast() []Member

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) Initial

func (pl *Play) Initial() State

Initial returns the state before any message (Format 8.6).

func (*Play) KnownBy

func (pl *Play) KnownBy(before State, m *Message) [][]string

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) Mode

func (pl *Play) Mode() string

Mode returns the scenario's mode: "characters" or "narrator" (Format 6.2).

func (*Play) NarratorName

func (pl *Play) NarratorName() string

NarratorName returns what text calls the narrator: the scenario's narratorName, or "the narrator" (Format 6).

func (*Play) PointAt

func (pl *Play) PointAt(before State, recent []Message, char string) *Point

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

func (pl *Play) Render(text string, c RenderContext, r *rand.Rand) string

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

func (pl *Play) RenderParts(before State, m *Message, r *rand.Rand) []Part

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

func (pl *Play) Root(opening string, r *rand.Rand) (Message, error)

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

func (pl *Play) StateAt(id string) (State, error)

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

func (pl *Play) StateKeys() []StateKey

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

func (pl *Play) UnmarshalState(data []byte) (State, error)

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.

func (*Play) Worlds

func (pl *Play) Worlds() []*World

Worlds returns the worlds in use, in order: the scenario's worlds, or every story world when its worlds is absent, then the session's (Format 6.4).

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

func (p *Point) Eval(expr, self string, scanDepth int) (bool, error)

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.

func (*Point) State

func (p *Point) State() State

State returns the state at the point.

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

func (s State) All() iter.Seq2[string, any]

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

func (s State) Get(key string) (any, bool)

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

func (s State) MarshalJSON() ([]byte, error)

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

func (s State) Spoilers() []string

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

func (st *Story) CheckID(list, id string) error

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

func (st *Story) FileFields() iter.Seq[FileField]

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

func (st Story) MarshalJSON() ([]byte, error)

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

func (st *Story) RenameID(object, id string) (jsontext.Value, []string, error)

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).

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).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL