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