deepstrike_core/orchestration/
tournament.rs1use crate::types::error::{DeepStrikeError, Result};
20
21pub type EntrantId = String;
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct Match {
27 pub id: u32,
29 pub left: EntrantId,
30 pub right: EntrantId,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum TournamentAction {
36 JudgeRound { round: u32, matches: Vec<Match> },
39 Done { winner: EntrantId, rounds_used: u32 },
41}
42
43#[derive(Debug)]
45pub struct Tournament {
46 survivors: Vec<EntrantId>,
48 pending: Vec<Match>,
50 bye: Option<EntrantId>,
52 round: u32,
54 done: bool,
55}
56
57impl Tournament {
58 pub fn new(entrants: Vec<EntrantId>) -> Result<Self> {
60 if entrants.is_empty() {
61 return Err(DeepStrikeError::InvalidConfig(
62 "tournament requires at least one entrant".into(),
63 ));
64 }
65 Ok(Self {
66 survivors: entrants,
67 pending: Vec::new(),
68 bye: None,
69 round: 0,
70 done: false,
71 })
72 }
73
74 pub fn start(&mut self) -> TournamentAction {
77 self.emit_round_or_done()
78 }
79
80 pub fn feed_round(&mut self, winners: Vec<EntrantId>) -> Result<TournamentAction> {
84 if self.done {
85 return Err(DeepStrikeError::InvalidConfig(
86 "tournament already complete".into(),
87 ));
88 }
89 if winners.len() != self.pending.len() {
90 return Err(DeepStrikeError::InvalidConfig(format!(
91 "expected {} winner(s) for round {}, got {}",
92 self.pending.len(),
93 self.round,
94 winners.len()
95 )));
96 }
97 for (w, m) in winners.iter().zip(&self.pending) {
98 if w != &m.left && w != &m.right {
99 return Err(DeepStrikeError::InvalidConfig(format!(
100 "winner '{w}' is not a participant in match {}",
101 m.id
102 )));
103 }
104 }
105
106 let mut next = winners;
107 if let Some(bye) = self.bye.take() {
108 next.push(bye);
109 }
110 self.survivors = next;
111 self.pending.clear();
112 Ok(self.emit_round_or_done())
113 }
114
115 fn emit_round_or_done(&mut self) -> TournamentAction {
117 if self.survivors.len() == 1 {
118 self.done = true;
119 return TournamentAction::Done {
120 winner: self.survivors[0].clone(),
121 rounds_used: self.round,
122 };
123 }
124
125 self.round += 1;
126 let mut matches = Vec::with_capacity(self.survivors.len() / 2);
127 let mut i = 0;
128 while i + 1 < self.survivors.len() {
129 matches.push(Match {
130 id: (i / 2) as u32,
131 left: self.survivors[i].clone(),
132 right: self.survivors[i + 1].clone(),
133 });
134 i += 2;
135 }
136 self.bye = if self.survivors.len() % 2 == 1 {
138 self.survivors.last().cloned()
139 } else {
140 None
141 };
142 self.pending = matches.clone();
143 TournamentAction::JudgeRound {
144 round: self.round,
145 matches,
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 fn ids(xs: &[&str]) -> Vec<EntrantId> {
155 xs.iter().map(|s| s.to_string()).collect()
156 }
157
158 #[test]
159 fn empty_entrants_is_error() {
160 assert!(Tournament::new(vec![]).is_err());
161 }
162
163 #[test]
164 fn single_entrant_wins_immediately() {
165 let mut t = Tournament::new(ids(&["a"])).unwrap();
166 match t.start() {
167 TournamentAction::Done {
168 winner,
169 rounds_used,
170 } => {
171 assert_eq!(winner, "a");
172 assert_eq!(rounds_used, 0);
173 }
174 _ => panic!("expected immediate Done"),
175 }
176 }
177
178 #[test]
179 fn two_entrants_one_round() {
180 let mut t = Tournament::new(ids(&["a", "b"])).unwrap();
181 match t.start() {
182 TournamentAction::JudgeRound { round, matches } => {
183 assert_eq!(round, 1);
184 assert_eq!(matches.len(), 1);
185 assert_eq!(
186 matches[0],
187 Match {
188 id: 0,
189 left: "a".into(),
190 right: "b".into()
191 }
192 );
193 }
194 _ => panic!("expected JudgeRound"),
195 }
196 match t.feed_round(ids(&["b"])).unwrap() {
197 TournamentAction::Done {
198 winner,
199 rounds_used,
200 } => {
201 assert_eq!(winner, "b");
202 assert_eq!(rounds_used, 1);
203 }
204 _ => panic!("expected Done"),
205 }
206 }
207
208 #[test]
209 fn four_entrants_two_rounds() {
210 let mut t = Tournament::new(ids(&["a", "b", "c", "d"])).unwrap();
211 let r1 = t.start();
212 match r1 {
213 TournamentAction::JudgeRound { round, matches } => {
214 assert_eq!(round, 1);
215 assert_eq!(matches.len(), 2);
216 }
217 _ => panic!(),
218 }
219 let r2 = t.feed_round(ids(&["a", "d"])).unwrap();
221 match r2 {
222 TournamentAction::JudgeRound { round, matches } => {
223 assert_eq!(round, 2);
224 assert_eq!(matches.len(), 1);
225 assert_eq!(
226 matches[0],
227 Match {
228 id: 0,
229 left: "a".into(),
230 right: "d".into()
231 }
232 );
233 }
234 _ => panic!(),
235 }
236 match t.feed_round(ids(&["d"])).unwrap() {
237 TournamentAction::Done {
238 winner,
239 rounds_used,
240 } => {
241 assert_eq!(winner, "d");
242 assert_eq!(rounds_used, 2);
243 }
244 _ => panic!(),
245 }
246 }
247
248 #[test]
249 fn three_entrants_bye_advances() {
250 let mut t = Tournament::new(ids(&["a", "b", "c"])).unwrap();
251 match t.start() {
252 TournamentAction::JudgeRound { round, matches } => {
253 assert_eq!(round, 1);
254 assert_eq!(matches.len(), 1);
256 assert_eq!(
257 matches[0],
258 Match {
259 id: 0,
260 left: "a".into(),
261 right: "b".into()
262 }
263 );
264 }
265 _ => panic!(),
266 }
267 match t.feed_round(ids(&["a"])).unwrap() {
269 TournamentAction::JudgeRound { round, matches } => {
270 assert_eq!(round, 2);
271 assert_eq!(
272 matches[0],
273 Match {
274 id: 0,
275 left: "a".into(),
276 right: "c".into()
277 }
278 );
279 }
280 _ => panic!(),
281 }
282 match t.feed_round(ids(&["c"])).unwrap() {
283 TournamentAction::Done {
284 winner,
285 rounds_used,
286 } => {
287 assert_eq!(winner, "c");
288 assert_eq!(rounds_used, 2);
289 }
290 _ => panic!(),
291 }
292 }
293
294 #[test]
295 fn eight_entrants_three_rounds() {
296 let mut t = Tournament::new(ids(&["1", "2", "3", "4", "5", "6", "7", "8"])).unwrap();
297 let mut action = t.start();
298 let mut last_round = 0;
299 loop {
300 match action {
301 TournamentAction::JudgeRound { round, matches } => {
302 last_round = round;
303 let winners: Vec<EntrantId> = matches.iter().map(|m| m.left.clone()).collect();
305 action = t.feed_round(winners).unwrap();
306 }
307 TournamentAction::Done {
308 winner,
309 rounds_used,
310 } => {
311 assert_eq!(winner, "1");
312 assert_eq!(rounds_used, 3);
313 assert_eq!(last_round, 3);
314 break;
315 }
316 }
317 }
318 }
319
320 #[test]
321 fn wrong_winner_count_is_error() {
322 let mut t = Tournament::new(ids(&["a", "b", "c", "d"])).unwrap();
323 t.start();
324 assert!(t.feed_round(ids(&["a"])).is_err());
326 }
327
328 #[test]
329 fn winner_not_in_match_is_error() {
330 let mut t = Tournament::new(ids(&["a", "b"])).unwrap();
331 t.start();
332 assert!(t.feed_round(ids(&["zzz"])).is_err());
333 }
334
335 #[test]
336 fn feed_after_done_is_error() {
337 let mut t = Tournament::new(ids(&["a", "b"])).unwrap();
338 t.start();
339 t.feed_round(ids(&["a"])).unwrap();
340 assert!(t.feed_round(ids(&["a"])).is_err());
341 }
342}