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 pub fn is_done(&self) -> bool {
150 self.done
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 fn ids(xs: &[&str]) -> Vec<EntrantId> {
159 xs.iter().map(|s| s.to_string()).collect()
160 }
161
162 #[test]
163 fn empty_entrants_is_error() {
164 assert!(Tournament::new(vec![]).is_err());
165 }
166
167 #[test]
168 fn single_entrant_wins_immediately() {
169 let mut t = Tournament::new(ids(&["a"])).unwrap();
170 match t.start() {
171 TournamentAction::Done {
172 winner,
173 rounds_used,
174 } => {
175 assert_eq!(winner, "a");
176 assert_eq!(rounds_used, 0);
177 }
178 _ => panic!("expected immediate Done"),
179 }
180 assert!(t.is_done());
181 }
182
183 #[test]
184 fn two_entrants_one_round() {
185 let mut t = Tournament::new(ids(&["a", "b"])).unwrap();
186 match t.start() {
187 TournamentAction::JudgeRound { round, matches } => {
188 assert_eq!(round, 1);
189 assert_eq!(matches.len(), 1);
190 assert_eq!(
191 matches[0],
192 Match {
193 id: 0,
194 left: "a".into(),
195 right: "b".into()
196 }
197 );
198 }
199 _ => panic!("expected JudgeRound"),
200 }
201 match t.feed_round(ids(&["b"])).unwrap() {
202 TournamentAction::Done {
203 winner,
204 rounds_used,
205 } => {
206 assert_eq!(winner, "b");
207 assert_eq!(rounds_used, 1);
208 }
209 _ => panic!("expected Done"),
210 }
211 }
212
213 #[test]
214 fn four_entrants_two_rounds() {
215 let mut t = Tournament::new(ids(&["a", "b", "c", "d"])).unwrap();
216 let r1 = t.start();
217 match r1 {
218 TournamentAction::JudgeRound { round, matches } => {
219 assert_eq!(round, 1);
220 assert_eq!(matches.len(), 2);
221 }
222 _ => panic!(),
223 }
224 let r2 = t.feed_round(ids(&["a", "d"])).unwrap();
226 match r2 {
227 TournamentAction::JudgeRound { round, matches } => {
228 assert_eq!(round, 2);
229 assert_eq!(matches.len(), 1);
230 assert_eq!(
231 matches[0],
232 Match {
233 id: 0,
234 left: "a".into(),
235 right: "d".into()
236 }
237 );
238 }
239 _ => panic!(),
240 }
241 match t.feed_round(ids(&["d"])).unwrap() {
242 TournamentAction::Done {
243 winner,
244 rounds_used,
245 } => {
246 assert_eq!(winner, "d");
247 assert_eq!(rounds_used, 2);
248 }
249 _ => panic!(),
250 }
251 }
252
253 #[test]
254 fn three_entrants_bye_advances() {
255 let mut t = Tournament::new(ids(&["a", "b", "c"])).unwrap();
256 match t.start() {
257 TournamentAction::JudgeRound { round, matches } => {
258 assert_eq!(round, 1);
259 assert_eq!(matches.len(), 1);
261 assert_eq!(
262 matches[0],
263 Match {
264 id: 0,
265 left: "a".into(),
266 right: "b".into()
267 }
268 );
269 }
270 _ => panic!(),
271 }
272 match t.feed_round(ids(&["a"])).unwrap() {
274 TournamentAction::JudgeRound { round, matches } => {
275 assert_eq!(round, 2);
276 assert_eq!(
277 matches[0],
278 Match {
279 id: 0,
280 left: "a".into(),
281 right: "c".into()
282 }
283 );
284 }
285 _ => panic!(),
286 }
287 match t.feed_round(ids(&["c"])).unwrap() {
288 TournamentAction::Done {
289 winner,
290 rounds_used,
291 } => {
292 assert_eq!(winner, "c");
293 assert_eq!(rounds_used, 2);
294 }
295 _ => panic!(),
296 }
297 }
298
299 #[test]
300 fn eight_entrants_three_rounds() {
301 let mut t = Tournament::new(ids(&["1", "2", "3", "4", "5", "6", "7", "8"])).unwrap();
302 let mut action = t.start();
303 let mut last_round = 0;
304 loop {
305 match action {
306 TournamentAction::JudgeRound { round, matches } => {
307 last_round = round;
308 let winners: Vec<EntrantId> = matches.iter().map(|m| m.left.clone()).collect();
310 action = t.feed_round(winners).unwrap();
311 }
312 TournamentAction::Done {
313 winner,
314 rounds_used,
315 } => {
316 assert_eq!(winner, "1");
317 assert_eq!(rounds_used, 3);
318 assert_eq!(last_round, 3);
319 break;
320 }
321 }
322 }
323 }
324
325 #[test]
326 fn wrong_winner_count_is_error() {
327 let mut t = Tournament::new(ids(&["a", "b", "c", "d"])).unwrap();
328 t.start();
329 assert!(t.feed_round(ids(&["a"])).is_err());
331 }
332
333 #[test]
334 fn winner_not_in_match_is_error() {
335 let mut t = Tournament::new(ids(&["a", "b"])).unwrap();
336 t.start();
337 assert!(t.feed_round(ids(&["zzz"])).is_err());
338 }
339
340 #[test]
341 fn feed_after_done_is_error() {
342 let mut t = Tournament::new(ids(&["a", "b"])).unwrap();
343 t.start();
344 t.feed_round(ids(&["a"])).unwrap();
345 assert!(t.feed_round(ids(&["a"])).is_err());
346 }
347}