Skip to main content

encounter/
practice.rs

1//! Practice specification types: [`PracticeSpec`], [`TurnPolicy`], and [`DurationPolicy`].
2
3use serde::{Deserialize, Serialize};
4
5/// Controls whose turn it is at each beat of a practice.
6#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum TurnPolicy {
9    /// Participants take turns in a fixed rotation.
10    #[default]
11    RoundRobin,
12    /// Each initiating utterance is paired with a response (adjacency pair).
13    AdjacencyPair,
14}
15
16/// Controls how long a practice runs.
17#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum DurationPolicy {
20    /// A single initiator/responder exchange, then the practice ends.
21    #[default]
22    SingleExchange,
23    /// Turn-based scene that continues until resolved or a beat cap is hit.
24    MultiBeat {
25        /// Maximum number of beats before the practice is forced to end.
26        max_beats: usize,
27    },
28    /// The practice runs until a resolution condition is satisfied.
29    UntilResolved,
30}
31
32/// Full specification for a practice, deserialized from a TOML definition file.
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34pub struct PracticeSpec {
35    /// Human-readable name for this practice (e.g. "negotiation").
36    pub name: String,
37    /// Affordance identifiers available within this practice.
38    pub affordances: Vec<String>,
39    /// How turn order is determined. Defaults to [`TurnPolicy::RoundRobin`].
40    #[serde(default)]
41    pub turn_policy: TurnPolicy,
42    /// How long the practice runs. Defaults to [`DurationPolicy::SingleExchange`].
43    #[serde(default)]
44    pub duration_policy: DurationPolicy,
45    /// Raw fabula DSL source for the practice entry condition. Empty means always enterable.
46    #[serde(default)]
47    pub entry_condition_source: String,
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn practice_spec_deserializes() {
56        let s = r#"
57            name = "negotiation"
58            affordances = ["offer", "counter_offer", "accept", "reject"]
59            turn_policy = "adjacency_pair"
60
61            [duration_policy]
62            multi_beat = { max_beats = 10 }
63        "#;
64
65        let spec: PracticeSpec = toml::from_str(s).expect("should deserialize");
66
67        assert_eq!(spec.name, "negotiation");
68        assert_eq!(spec.affordances.len(), 4);
69        assert_eq!(spec.turn_policy, TurnPolicy::AdjacencyPair);
70        assert_eq!(
71            spec.duration_policy,
72            DurationPolicy::MultiBeat { max_beats: 10 }
73        );
74    }
75
76    #[test]
77    fn practice_defaults_to_single_exchange_round_robin() {
78        let s = r#"
79            name = "greeting"
80            affordances = ["wave", "nod"]
81        "#;
82
83        let spec: PracticeSpec = toml::from_str(s).expect("should deserialize");
84
85        assert_eq!(spec.name, "greeting");
86        assert_eq!(spec.affordances.len(), 2);
87        assert_eq!(spec.turn_policy, TurnPolicy::RoundRobin);
88        assert_eq!(spec.duration_policy, DurationPolicy::SingleExchange);
89        assert_eq!(spec.entry_condition_source, "");
90    }
91}