encounter/resolution/
multi_beat.rs1use crate::affordance::CatalogEntry;
4use crate::practice::{DurationPolicy, PracticeSpec, TurnPolicy};
5use crate::scoring::{AcceptanceEval, ActionScorer};
6use crate::types::{Beat, Effect, EncounterResult};
7
8pub struct MultiBeat;
12
13impl MultiBeat {
14 pub fn resolve<P: Clone>(
18 &self,
19 participants: &[String],
20 practice: &PracticeSpec,
21 catalog: &[CatalogEntry<P>],
22 scorer: &dyn ActionScorer<P>,
23 acceptance: &dyn AcceptanceEval<P>,
24 ) -> EncounterResult {
25 let max_beats = match practice.duration_policy {
26 DurationPolicy::MultiBeat { max_beats } => max_beats,
27 DurationPolicy::SingleExchange => 1,
28 DurationPolicy::UntilResolved => usize::MAX,
29 };
30
31 let mut result = EncounterResult::new(participants.to_vec(), Some(practice.name.clone()));
32
33 if participants.is_empty() {
34 return result;
35 }
36
37 let allowed: Vec<CatalogEntry<P>> = catalog
39 .iter()
40 .filter(|e| practice.affordances.contains(&e.spec.name))
41 .cloned()
42 .collect();
43
44 let mut speaker_idx = 0usize;
45
46 for _beat_num in 0..max_beats {
47 let speaker = &participants[speaker_idx % participants.len()];
48 let responder_idx = (speaker_idx + 1) % participants.len();
49 let responder = &participants[responder_idx];
50
51 let scored = scorer.score_actions(speaker, &allowed, participants);
52 let Some(best) = scored.iter().max_by(|a, b| {
53 a.score
54 .partial_cmp(&b.score)
55 .unwrap_or(std::cmp::Ordering::Equal)
56 }) else {
57 break;
58 };
59
60 let accepted = acceptance.evaluate(responder, best);
61 let effects = if accepted {
62 best.entry.spec.effects_on_accept.clone()
63 } else {
64 best.entry.spec.effects_on_reject.clone()
65 };
66
67 let exit_requested = effects
68 .iter()
69 .any(|e| matches!(e, Effect::PracticeExit { .. }));
70
71 let beat = Beat {
72 actor: speaker.clone(),
73 action: best.entry.spec.name.clone(),
74 accepted,
75 effects,
76 };
77 result.push_beat(beat);
78
79 if let Some(esc) = crate::escalation::check_escalation(
81 result.beats.last().unwrap(),
82 result.beats.len() - 1,
83 ) {
84 result.escalation_requested = true;
85 result.escalation_requests.push(esc);
86 }
87
88 if exit_requested {
89 break;
90 }
91
92 speaker_idx = match practice.turn_policy {
93 TurnPolicy::RoundRobin => speaker_idx + 1,
94 TurnPolicy::AdjacencyPair => responder_idx,
95 };
96 }
97 result
98 }
99}