1use alloc::boxed::Box;
40use alloc::collections::BTreeMap;
41use alloc::format;
42use alloc::string::{String, ToString};
43use alloc::vec::Vec;
44use core::fmt::{self, Write as _};
45use core::ops::Range;
46
47use serde::{Deserialize, Serialize};
48
49use super::{Bot, PlayError, Report, Sides, play_observed};
50
51mod targets;
52
53pub use targets::{MedianTarget, Targets};
54
55pub type Measures = BTreeMap<String, i64>;
58
59pub trait Measure<R: Sides> {
61 fn observe(&mut self, rules: &R, command: &R::Command, events: &[R::Event]);
64
65 fn finish(self: Box<Self>, report: &Report<R>, end: &R) -> Measures;
67}
68
69pub trait Entrant<S, R: Sides> {
72 fn name(&self) -> &str;
74
75 fn start(&self, stage: &S, seed: u64) -> R;
78
79 fn bots(&self, seed: u64) -> Vec<(R::Side, Box<dyn Bot<R>>)>;
81
82 fn measure(&self) -> Box<dyn Measure<R>>;
84
85 fn record(&self, seed: u64, commands: &[R::Command]) -> Option<String> {
89 let _ = (seed, commands);
90 None
91 }
92}
93
94pub const UNFINISHED: &str = "unfinished";
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
102#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
103#[non_exhaustive]
104pub struct Tries {
105 pub count: u32,
107 pub seed: u64,
109 pub turns: u32,
111}
112
113impl Tries {
114 #[must_use]
116 pub const fn new(count: u32, seed: u64, turns: u32) -> Self {
117 Self { count, seed, turns }
118 }
119
120 pub fn seeds(&self) -> Range<u64> {
122 self.seed..self.seed.saturating_add(u64::from(self.count))
123 }
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
129#[serde(deny_unknown_fields)]
130pub struct Tried {
131 pub seed: u64,
133 pub outcome: Option<String>,
136 pub turns: u32,
138 pub measures: Measures,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
146#[non_exhaustive]
147pub struct PlayedTry {
148 pub tried: Tried,
150 pub commands: u32,
152 pub record: Option<String>,
154}
155
156impl PlayedTry {
157 #[must_use]
159 pub fn new(tried: Tried, commands: u32) -> Self {
160 Self {
161 tried,
162 commands,
163 record: None,
164 }
165 }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
171#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
172#[serde(deny_unknown_fields)]
173#[non_exhaustive]
174pub struct Condition {
175 pub measure: String,
177 #[serde(default)]
179 pub min: Option<i64>,
180 #[serde(default)]
182 pub max: Option<i64>,
183}
184
185impl Condition {
186 pub fn new(measure: impl Into<String>) -> Self {
190 Self {
191 measure: measure.into(),
192 min: None,
193 max: None,
194 }
195 }
196
197 #[must_use]
199 pub fn with_min(self, min: Option<i64>) -> Self {
200 Self { min, ..self }
201 }
202
203 #[must_use]
205 pub fn with_max(self, max: Option<i64>) -> Self {
206 Self { max, ..self }
207 }
208
209 pub fn met_by(&self, measures: &Measures) -> bool {
211 measures.get(&self.measure).is_some_and(|&value| {
212 self.min.is_none_or(|min| value >= min) && self.max.is_none_or(|max| value <= max)
213 })
214 }
215}
216
217fn meets(conditions: &[Condition], measures: &Measures) -> bool {
219 conditions.iter().all(|c| c.met_by(measures))
220}
221
222#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
224#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
225#[serde(deny_unknown_fields)]
226pub struct Band {
227 pub min: u32,
229 pub max: u32,
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
235#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
236pub enum RateVerdict {
237 Below,
239 Within,
241 Above,
243}
244
245impl Band {
246 pub fn verdict(self, successes: u32, count: u32) -> RateVerdict {
248 let share = u64::from(successes) * 100;
249 if share < u64::from(self.min) * u64::from(count) {
250 RateVerdict::Below
251 } else if share > u64::from(self.max) * u64::from(count) {
252 RateVerdict::Above
253 } else {
254 RateVerdict::Within
255 }
256 }
257}
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
261#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
262pub enum Want {
263 Lower,
265 Higher,
267}
268
269#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
271#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
272pub enum MedianVerdict {
273 Pass,
275 Fail,
277 Unknown,
279}
280
281#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
283#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
284#[non_exhaustive]
285pub enum Check {
286 #[non_exhaustive]
289 Rate {
290 entrant: String,
292 success: Vec<Condition>,
294 band: Band,
296 #[serde(default)]
298 floor: Option<u32>,
299 },
300 #[non_exhaustive]
303 Median {
304 entrant: String,
306 measure: String,
308 #[serde(default)]
310 among: Vec<Condition>,
311 want: Want,
313 than: String,
315 },
316}
317
318impl Check {
319 pub fn rate(entrant: impl Into<String>, success: Vec<Condition>, band: Band) -> Self {
323 Check::Rate {
324 entrant: entrant.into(),
325 success,
326 band,
327 floor: None,
328 }
329 }
330
331 pub fn median(
335 entrant: impl Into<String>,
336 measure: impl Into<String>,
337 want: Want,
338 than: impl Into<String>,
339 ) -> Self {
340 Check::Median {
341 entrant: entrant.into(),
342 measure: measure.into(),
343 among: Vec::new(),
344 want,
345 than: than.into(),
346 }
347 }
348
349 #[must_use]
352 pub fn with_floor(mut self, floor: Option<u32>) -> Self {
353 if let Check::Rate { floor: f, .. } = &mut self {
354 *f = floor;
355 }
356 self
357 }
358
359 #[must_use]
362 pub fn with_among(mut self, among: Vec<Condition>) -> Self {
363 if let Check::Median { among: a, .. } = &mut self {
364 *a = among;
365 }
366 self
367 }
368
369 pub fn entrants(&self) -> Vec<&str> {
372 match self {
373 Check::Rate { entrant, .. } => alloc::vec![entrant.as_str()],
374 Check::Median { entrant, than, .. } => alloc::vec![entrant.as_str(), than.as_str()],
375 }
376 }
377}
378
379#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
381#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
382pub enum Found {
383 Rate {
385 successes: u32,
387 count: u32,
389 verdict: RateVerdict,
391 blocked: bool,
393 },
394 Median {
396 median: Option<i64>,
398 than: Option<i64>,
400 verdict: MedianVerdict,
402 },
403}
404
405#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
407#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
408#[serde(deny_unknown_fields)]
409pub struct Judged {
410 pub check: Check,
412 pub found: Found,
414}
415
416#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
418#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
419#[serde(deny_unknown_fields)]
420pub struct EntrantTries {
421 pub name: String,
423 pub tries: Vec<Tried>,
425}
426
427#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
430#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
431#[serde(deny_unknown_fields)]
432pub struct PlaytestReport {
433 pub entrants: Vec<EntrantTries>,
435 pub checks: Vec<Judged>,
437 pub blocked: bool,
439}
440
441#[derive(Debug, Clone, PartialEq, Eq)]
444#[non_exhaustive]
445pub struct UnknownEntrant {
446 pub check: usize,
448 pub entrant: String,
450}
451
452impl UnknownEntrant {
453 pub fn new(check: usize, entrant: impl Into<String>) -> Self {
455 Self {
456 check,
457 entrant: entrant.into(),
458 }
459 }
460}
461
462impl fmt::Display for UnknownEntrant {
463 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
464 write!(
465 f,
466 "check {} names `{}`, who isn't in the run",
467 self.check + 1,
468 self.entrant
469 )
470 }
471}
472
473#[non_exhaustive]
475pub enum PlaytestError<R: Sides> {
476 UnknownEntrant(UnknownEntrant),
478 #[non_exhaustive]
481 Play {
482 entrant: String,
484 error: PlayError<R>,
486 },
487}
488
489impl<R: Sides> PlaytestError<R> {
490 pub fn play(entrant: impl Into<String>, error: PlayError<R>) -> Self {
492 PlaytestError::Play {
493 entrant: entrant.into(),
494 error,
495 }
496 }
497}
498
499impl<R: Sides> From<UnknownEntrant> for PlaytestError<R> {
500 fn from(e: UnknownEntrant) -> Self {
501 PlaytestError::UnknownEntrant(e)
502 }
503}
504
505impl<R: Sides> fmt::Debug for PlaytestError<R>
506where
507 R::Command: fmt::Debug,
508 R::Error: fmt::Debug,
509{
510 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
511 match self {
512 PlaytestError::UnknownEntrant(e) => f.debug_tuple("UnknownEntrant").field(e).finish(),
513 PlaytestError::Play { entrant, error } => f
514 .debug_struct("Play")
515 .field("entrant", entrant)
516 .field("error", error)
517 .finish(),
518 }
519 }
520}
521
522impl<R: Sides> fmt::Display for PlaytestError<R>
523where
524 R::Command: fmt::Debug,
525 R::Error: fmt::Display,
526{
527 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
528 match self {
529 PlaytestError::UnknownEntrant(e) => e.fmt(f),
530 PlaytestError::Play { entrant, error } => write!(f, "{entrant}: {error}"),
531 }
532 }
533}
534
535fn unknown_entrant(names: &[&str], checks: &[Check]) -> Option<UnknownEntrant> {
537 checks.iter().enumerate().find_map(|(i, check)| {
538 check
539 .entrants()
540 .into_iter()
541 .find(|name| !names.contains(name))
542 .map(|name| UnknownEntrant {
543 check: i,
544 entrant: name.to_string(),
545 })
546 })
547}
548
549pub fn play_tries<S, R: Sides>(
552 stage: &S,
553 entrant: &dyn Entrant<S, R>,
554 seeds: Range<u64>,
555 turns: u32,
556) -> Result<Vec<Tried>, PlayError<R>>
557where
558 R::Command: Clone,
559 R::Outcome: fmt::Debug,
560{
561 seeds
562 .map(|seed| tried(stage, entrant, seed, turns).map(|(tried, _)| tried))
563 .collect()
564}
565
566pub fn play_try<S, R: Sides>(
570 stage: &S,
571 entrant: &dyn Entrant<S, R>,
572 seed: u64,
573 turns: u32,
574) -> Result<PlayedTry, PlayError<R>>
575where
576 R::Command: Clone,
577 R::Outcome: fmt::Debug,
578{
579 let (tried, report) = tried(stage, entrant, seed, turns)?;
580 Ok(PlayedTry {
581 tried,
582 commands: u32::try_from(report.commands.len()).unwrap_or(u32::MAX),
583 record: entrant.record(seed, &report.commands),
584 })
585}
586
587fn tried<S, R: Sides>(
589 stage: &S,
590 entrant: &dyn Entrant<S, R>,
591 seed: u64,
592 turns: u32,
593) -> Result<(Tried, Report<R>), PlayError<R>>
594where
595 R::Command: Clone,
596 R::Outcome: fmt::Debug,
597{
598 let mut rules = entrant.start(stage, seed);
599 let mut bots = entrant.bots(seed);
600 let mut by_side: Vec<(R::Side, &mut dyn Bot<R>)> = bots
601 .iter_mut()
602 .map(|(side, bot)| (*side, bot.as_mut() as &mut dyn Bot<R>))
603 .collect();
604 let mut measure = entrant.measure();
605 let report = play_observed(&mut rules, &mut by_side, turns, |r, c, e| {
606 measure.observe(r, c, e);
607 })?;
608 let tried = Tried {
609 seed,
610 outcome: report.outcome.as_ref().map(|o| format!("{o:?}")),
611 turns: report.turns,
612 measures: measure.finish(&report, &rules),
613 };
614 Ok((tried, report))
615}
616
617fn median(mut values: Vec<i64>) -> Option<i64> {
619 values.sort_unstable();
620 let middle = values.len().checked_sub(1)? / 2;
621 values.get(middle).copied()
622}
623
624fn median_of(tries: &[Tried], measure: &str, among: &[Condition]) -> Option<i64> {
627 median(
628 tries
629 .iter()
630 .filter(|t| meets(among, &t.measures))
631 .filter_map(|t| t.measures.get(measure).copied())
632 .collect(),
633 )
634}
635
636pub fn judge(
639 tries: Vec<(String, Vec<Tried>)>,
640 checks: &[Check],
641) -> Result<PlaytestReport, UnknownEntrant> {
642 let mut entrants: Vec<EntrantTries> = Vec::new();
643 for (name, mut part) in tries {
644 if let Some(e) = entrants.iter_mut().find(|e| e.name == name) {
645 e.tries.append(&mut part);
646 } else {
647 entrants.push(EntrantTries { name, tries: part });
648 }
649 }
650 for e in &mut entrants {
651 e.tries.sort_by_key(|t| t.seed);
652 }
653 let names: Vec<&str> = entrants.iter().map(|e| e.name.as_str()).collect();
654 if let Some(e) = unknown_entrant(&names, checks) {
655 return Err(e);
656 }
657 let of = |name: &str| -> &[Tried] {
658 entrants
659 .iter()
660 .find(|e| e.name == name)
661 .map_or(&[], |e| &e.tries)
662 };
663 let checks: Vec<Judged> = checks
664 .iter()
665 .map(|check| {
666 let found = match check {
667 Check::Rate {
668 entrant,
669 success,
670 band,
671 floor,
672 } => {
673 let tries = of(entrant);
674 let count = u32::try_from(tries.len()).unwrap_or(u32::MAX);
675 let successes = tries.iter().filter(|t| meets(success, &t.measures)).count();
676 let successes = u32::try_from(successes).unwrap_or(u32::MAX);
677 let blocked = floor.is_some_and(|floor| {
678 u64::from(successes) * 100 < u64::from(floor) * u64::from(count)
679 });
680 Found::Rate {
681 successes,
682 count,
683 verdict: band.verdict(successes, count),
684 blocked,
685 }
686 }
687 Check::Median {
688 entrant,
689 measure,
690 among,
691 want,
692 than,
693 } => {
694 let median = median_of(of(entrant), measure, among);
695 let other = median_of(of(than), measure, among);
696 let verdict = match (median, other, want) {
697 (Some(m), Some(o), Want::Lower) if m < o => MedianVerdict::Pass,
698 (Some(m), Some(o), Want::Higher) if m > o => MedianVerdict::Pass,
699 (Some(_), Some(_), _) => MedianVerdict::Fail,
700 _ => MedianVerdict::Unknown,
701 };
702 Found::Median {
703 median,
704 than: other,
705 verdict,
706 }
707 }
708 };
709 Judged {
710 check: check.clone(),
711 found,
712 }
713 })
714 .collect();
715 let blocked = checks
716 .iter()
717 .any(|j| matches!(j.found, Found::Rate { blocked: true, .. }));
718 Ok(PlaytestReport {
719 entrants,
720 checks,
721 blocked,
722 })
723}
724
725pub fn run<S, R: Sides>(
729 stage: &S,
730 entrants: &[&dyn Entrant<S, R>],
731 tries: &Tries,
732 checks: &[Check],
733) -> Result<PlaytestReport, PlaytestError<R>>
734where
735 R::Command: Clone,
736 R::Outcome: fmt::Debug,
737{
738 let names: Vec<&str> = entrants.iter().map(|e| e.name()).collect();
739 if let Some(e) = unknown_entrant(&names, checks) {
740 return Err(e.into());
741 }
742 let mut played = Vec::new();
743 for entrant in entrants {
744 let name = entrant.name().to_string();
745 match play_tries(stage, *entrant, tries.seeds(), tries.turns) {
746 Ok(t) => played.push((name, t)),
747 Err(error) => {
748 return Err(PlaytestError::Play {
749 entrant: name,
750 error,
751 });
752 }
753 }
754 }
755 Ok(judge(played, checks)?)
756}
757
758fn or_dash(n: Option<i64>) -> String {
760 n.map_or_else(|| "–".to_string(), |n| n.to_string())
761}
762
763impl PlaytestReport {
764 fn line(judged: &Judged, width: usize) -> (String, String) {
766 match (&judged.check, &judged.found) {
767 (
768 Check::Rate {
769 entrant,
770 band,
771 floor,
772 ..
773 },
774 Found::Rate {
775 successes,
776 count,
777 verdict,
778 blocked,
779 },
780 ) => {
781 let mark = match verdict {
782 RateVerdict::Within => "✓",
783 RateVerdict::Below => "✗ too hard",
784 RateVerdict::Above => "⚠ too easy",
785 };
786 let mut how = format!("band {}–{}% {mark}", band.min, band.max);
787 if *blocked {
788 let _ = write!(how, " · blocked: under the {}% floor", floor.unwrap_or(0));
790 }
791 (
792 format!("{entrant:<width$} · {successes}/{count} succeed"),
793 how,
794 )
795 }
796 (
797 Check::Median {
798 entrant,
799 measure,
800 want,
801 than: other,
802 ..
803 },
804 Found::Median {
805 median,
806 than,
807 verdict,
808 },
809 ) => {
810 let mark = match verdict {
811 MedianVerdict::Pass => "✓",
812 MedianVerdict::Fail => "✗",
813 MedianVerdict::Unknown => "–",
814 };
815 let want = match want {
816 Want::Lower => "lower",
817 Want::Higher => "higher",
818 };
819 (
820 format!(
821 "{entrant:<width$} · {measure} median {} ({other} {})",
822 or_dash(*median),
823 or_dash(*than)
824 ),
825 format!("{want} {mark}"),
826 )
827 }
828 (check, found) => (format!("{check:?}"), format!("{found:?}")),
830 }
831 }
832
833 pub fn brief(&self) -> String {
836 let mut out = String::new();
837 let _ = self.write_table(&mut out, false);
839 out
840 }
841
842 fn write_table(&self, f: &mut impl fmt::Write, tries: bool) -> fmt::Result {
844 let first = self.entrants.first().map_or(&[][..], |e| &e.tries);
845 match (first.first(), first.last()) {
846 (Some(a), Some(b)) => {
847 writeln!(f, "{} tries (seeds {}–{})", first.len(), a.seed, b.seed)?;
848 }
849 _ => writeln!(f, "0 tries")?,
850 }
851 let width = self
852 .checks
853 .iter()
854 .map(|j| match &j.check {
855 Check::Rate { entrant, .. } | Check::Median { entrant, .. } => {
856 entrant.chars().count()
857 }
858 })
859 .max()
860 .unwrap_or(0);
861 let lines: Vec<(String, String)> =
862 self.checks.iter().map(|j| Self::line(j, width)).collect();
863 let left = lines
864 .iter()
865 .map(|(l, _)| l.chars().count())
866 .max()
867 .unwrap_or(0);
868 if !lines.is_empty() {
869 writeln!(f)?;
870 }
871 for (what, how) in &lines {
872 let pad = left - what.chars().count() + 4;
873 writeln!(f, "{what}{:pad$}{how}", "")?;
874 }
875 if !tries {
876 return Ok(());
877 }
878 if self.entrants.iter().any(|e| !e.tries.is_empty()) {
879 writeln!(f)?;
880 }
881 for entrant in &self.entrants {
882 for t in &entrant.tries {
883 write!(
884 f,
885 "try {} · {} · {} · turns {}",
886 t.seed,
887 entrant.name,
888 t.outcome.as_deref().unwrap_or(UNFINISHED),
889 t.turns
890 )?;
891 for (name, value) in &t.measures {
892 write!(f, " · {name} {value}")?;
893 }
894 writeln!(f)?;
895 }
896 }
897 Ok(())
898 }
899}
900
901impl fmt::Display for PlaytestReport {
904 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
905 self.write_table(f, true)
906 }
907}
908
909#[cfg(test)]
910mod tests;