///|
pub struct Choice {
id : String
text : String
target : String
} derive(Eq, @debug.Debug)
///|
pub extend Choice with Eq::{equal, not_equal}
///|
pub extend Choice with @debug.Debug::{to_repr}
///|
pub(all) enum Frame {
Line(String, String)
Choices(Array[Choice])
Ended(String)
} derive(Eq, @debug.Debug)
///|
pub extend Frame with Eq::{equal, not_equal}
///|
pub extend Frame with @debug.Debug::{to_repr}
///|
pub struct Event {
sequence : Int32
name : String
value : Value
} derive(Eq, @debug.Debug)
///|
pub extend Event with Eq::{equal, not_equal}
///|
pub extend Event with @debug.Debug::{to_repr}
///|
pub struct Session {
priv story : Story
priv vars : Map[String, Value]
priv pc : Int32
priv current : Frame
priv delivered : Array[Event]
priv sequence : Int32
priv actions : Array[String]
}
///|
pub fn Session::frame(self : Session) -> Frame {
match self.current {
Choices(cs) => Choices(cs.copy())
f => f
}
}
///|
pub fn Session::variables(self : Session) -> Map[String, Value] {
self.vars.copy()
}
///|
pub fn Session::events(self : Session) -> Array[Event] {
self.delivered.copy()
}
///|
pub fn Session::node(self : Session) -> String {
self.story.code[self.pc].node
}
///|
pub fn Session::source_line(self : Session) -> Int32 {
self.story.code[self.pc].line
}
///|
pub fn Session::action_count(self : Session) -> Int32 {
self.actions.length()
}
///|
fn enabled(e : Expr, vars : Map[String, Value]) -> Bool raise StoryError {
match eval(e, vars) {
Flag(b) => b
_ => raise Runtime("type", "expected boolean")
}
}
///|
fn target(story : Story, label : String) -> Int32 raise StoryError {
match story.labels.get(label) {
Some(i) => i
None => raise Runtime("target", "missing node " + label)
}
}
///|
fn drain(
story : Story,
vars : Map[String, Value],
begin : Int32,
actions : Array[String],
initial_sequence : Int32,
) -> Session raise StoryError {
let mut pc = begin
let mut sequence = initial_sequence
let events : Array[Event] = []
let mut steps = 0
let mut current = Ended("")
while true {
if steps >= 512 {
raise Limit("more than 512 automatic instructions in one transition")
}
steps = steps + 1
if pc < 0 || pc >= story.code.length() {
raise Runtime("cursor", "execution left the story")
}
let ins = story.code[pc]
match ins.op {
Say(speaker, text) => {
current = Line(speaker, template(text, vars, ins.line))
break
}
Pick(_, _, _, _) => {
let choices = []
let mut i = pc
while i < story.code.length() {
guard story.code[i].op is Pick(id, text, dest, condition) else {
break
}
if enabled(condition, vars) {
choices.push({
id,
text: template(text, vars, story.code[i].line),
target: dest,
})
}
i = i + 1
}
if choices.is_empty() {
raise Runtime("blocked", "all choices are hidden at " + ins.node)
}
current = Choices(choices)
break
}
Assign(name, e) => {
vars[name] = eval(e, vars)
pc = pc + 1
}
Emit(name, e) => {
let value = eval(e, vars)
sequence = sequence + 1
events.push({ sequence, name, value, })
pc = pc + 1
}
Jump(label) => pc = target(story, label)
Branch(e, label) =>
pc = if enabled(e, vars) { target(story, label) } else { pc + 1 }
End(ending) => {
current = Ended(ending)
break
}
}
}
{ story, vars, pc, current, delivered: events, sequence, actions, }
}
///|
/// Evaluate until the first line, choice group or ending. This performs no I/O.
pub fn start(story : Story) -> Session raise StoryError {
drain(story, story.initial.copy(), 0, [], 0)
}
///|
fn Session::next_actions(
self : Session,
action : String,
) -> Array[String] raise StoryError {
if self.actions.length() >= 10000 {
raise Limit("session exceeds 10000 player actions")
}
let actions = self.actions.copy()
actions.push(action)
actions
}
///|
/// Immutable transition: failure never changes self, including events and journal.
pub fn Session::advance(self : Session) -> Session raise StoryError {
guard self.current is Line(_, _) else {
raise Runtime("action", "advance requires a dialogue line")
}
drain(
self.story,
self.vars.copy(),
self.pc + 1,
self.next_actions(""),
self.sequence,
)
}
///|
/// Select by stable identifier, never by the visible list position.
pub fn Session::choose(self : Session, id : String) -> Session raise StoryError {
guard self.current is Choices(cs) else {
raise Runtime("action", "choose requires a choice group")
}
let mut destination : String? = None
for c in cs {
if c.id == id {
destination = Some(c.target)
break
}
}
guard destination is Some(label) else {
raise Runtime("choice", "unknown or hidden choice " + id)
}
drain(
self.story,
self.vars.copy(),
target(self.story, label),
self.next_actions(id),
self.sequence,
)
}