Skip to main content

encounter/
value_argument.rs

1//! DF-style value argument resolution.
2//!
3//! Characters argue about Schwartz values; the winner shifts the loser's value
4//! based on the conviction gap and the defender's openness to persuasion.
5
6/// Input to a value argument resolution.
7#[derive(Debug, Clone)]
8pub struct ValueArgumentInput {
9    /// Name of the character initiating the argument.
10    pub attacker: String,
11    /// Name of the character defending their position.
12    pub defender: String,
13    /// The Schwartz value being contested.
14    pub value_at_stake: String,
15    /// How strongly the attacker holds their position. Range: `[0.0, 1.0]`.
16    pub attacker_conviction: f64,
17    /// How strongly the defender holds their position. Range: `[0.0, 1.0]`.
18    pub defender_conviction: f64,
19    /// How open the defender is to changing their position. Range: `[0.0, 1.0]`.
20    pub defender_openness: f64,
21}
22
23/// Output of a value argument resolution.
24#[derive(Debug, Clone)]
25pub struct ValueArgumentResult {
26    /// Name of the character who won the argument.
27    pub winner: String,
28    /// Name of the character who lost the argument.
29    pub loser: String,
30    /// The Schwartz value that was contested.
31    pub value_at_stake: String,
32    /// Magnitude of the shift applied to the loser's value. Range: `[0.0, 1.0]`.
33    pub loser_value_shift: f64,
34    /// Small self-reinforcement applied to the winner's value. Range: `[0.0, 0.1]`.
35    pub winner_value_shift: f64,
36}
37
38/// Resolve a value argument.
39///
40/// The character with higher conviction wins. On a tie, the attacker wins.
41///
42/// - Loser shift = `conviction_gap × defender_openness`, clamped to `[0.0, 1.0]`.
43/// - Winner self-reinforcement = `conviction_gap × 0.1`, clamped to `[0.0, 0.1]`.
44pub fn resolve_value_argument(input: &ValueArgumentInput) -> ValueArgumentResult {
45    let attacker_wins = input.attacker_conviction >= input.defender_conviction;
46
47    let (winner, loser) = if attacker_wins {
48        (input.attacker.clone(), input.defender.clone())
49    } else {
50        (input.defender.clone(), input.attacker.clone())
51    };
52
53    let conviction_gap = (input.attacker_conviction - input.defender_conviction).abs();
54
55    let loser_value_shift = (conviction_gap * input.defender_openness).clamp(0.0, 1.0);
56
57    let winner_value_shift = (conviction_gap * 0.1).clamp(0.0, 0.1);
58
59    ValueArgumentResult {
60        winner,
61        loser,
62        value_at_stake: input.value_at_stake.clone(),
63        loser_value_shift,
64        winner_value_shift,
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn winner_shifts_loser_value() {
74        let input = ValueArgumentInput {
75            attacker: "Alice".to_string(),
76            defender: "Bob".to_string(),
77            value_at_stake: "Benevolence".to_string(),
78            attacker_conviction: 0.8,
79            defender_conviction: 0.4,
80            defender_openness: 0.6,
81        };
82        let result = resolve_value_argument(&input);
83        assert_eq!(result.winner, "Alice");
84        assert!(result.loser_value_shift > 0.0);
85        assert!(result.loser_value_shift <= 1.0);
86    }
87
88    #[test]
89    fn defender_wins_when_more_convinced() {
90        let input = ValueArgumentInput {
91            attacker: "Alice".to_string(),
92            defender: "Bob".to_string(),
93            value_at_stake: "Power".to_string(),
94            attacker_conviction: 0.3,
95            defender_conviction: 0.9,
96            defender_openness: 0.5,
97        };
98        let result = resolve_value_argument(&input);
99        assert_eq!(result.winner, "Bob");
100    }
101
102    #[test]
103    fn winner_gets_small_self_reinforcement() {
104        let input = ValueArgumentInput {
105            attacker: "Alice".to_string(),
106            defender: "Bob".to_string(),
107            value_at_stake: "Security".to_string(),
108            attacker_conviction: 0.8,
109            defender_conviction: 0.4,
110            defender_openness: 0.6,
111        };
112        let result = resolve_value_argument(&input);
113        assert!(result.winner_value_shift > 0.0);
114        assert!(result.winner_value_shift < result.loser_value_shift);
115    }
116
117    #[test]
118    fn equal_conviction_favors_attacker() {
119        let input = ValueArgumentInput {
120            attacker: "Alice".to_string(),
121            defender: "Bob".to_string(),
122            value_at_stake: "Tradition".to_string(),
123            attacker_conviction: 0.5,
124            defender_conviction: 0.5,
125            defender_openness: 0.8,
126        };
127        let result = resolve_value_argument(&input);
128        assert_eq!(result.winner, "Alice");
129        assert_eq!(result.loser_value_shift, 0.0);
130    }
131}