Skip to main content

shuyi_core/bots/playtest/
targets.rs

1//! Playtest targets by tier: what each entrant should achieve on a stage
2//! of each of the game's tiers, turned into the [`Check`]s of one stage.
3
4use alloc::collections::{BTreeMap, BTreeSet};
5use alloc::string::String;
6use alloc::vec::Vec;
7
8use serde::{Deserialize, Serialize};
9
10use super::{Band, Check, Condition, Want};
11
12/// What each entrant should achieve, over the game's own tier type `T`
13/// (whatever its stages are labelled with). [`Targets::checks`] gives a
14/// stage's checks from it.
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
17#[serde(
18    deny_unknown_fields,
19    bound(
20        serialize = "T: Ord + Serialize",
21        deserialize = "T: Ord + Deserialize<'de>"
22    )
23)]
24pub struct Targets<T: Ord> {
25    /// What makes a try a success, per entrant.
26    pub success: BTreeMap<String, Vec<Condition>>,
27    /// The share of successes wanted, per tier and entrant.
28    #[serde(default)]
29    pub bands: BTreeMap<T, BTreeMap<String, Band>>,
30    /// The share under which a run is blocked, in percent, per entrant
31    /// that has one.
32    #[serde(default)]
33    pub floors: BTreeMap<String, u32>,
34    /// Medians compared between two entrants, on a stage of any tier.
35    #[serde(default)]
36    pub medians: Vec<MedianTarget>,
37}
38
39/// A [`Check::Median`] wanted wherever both entrants play.
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
42#[serde(deny_unknown_fields)]
43pub struct MedianTarget {
44    /// The entrant checked.
45    pub entrant: String,
46    /// The measure.
47    pub measure: String,
48    /// Which tries count.
49    #[serde(default)]
50    pub among: Vec<Condition>,
51    /// Which way it should lean.
52    pub want: Want,
53    /// The entrant compared with.
54    pub than: String,
55}
56
57impl<T: Ord> Targets<T> {
58    /// The checks of a stage of `tier` that `entrants` play: a
59    /// [`Check::Rate`] per entrant, in the order given, that has both a
60    /// success rule and a band for `tier`, then each median, in file
61    /// order, whose two entrants both play. Every check names only
62    /// entrants of `entrants`.
63    pub fn checks(&self, tier: &T, entrants: &[&str]) -> Vec<Check> {
64        let bands = self.bands.get(tier);
65        let rates = entrants.iter().filter_map(|&name| {
66            let success = self.success.get(name)?;
67            let band = bands?.get(name)?;
68            Some(Check::Rate {
69                entrant: name.into(),
70                success: success.clone(),
71                band: *band,
72                floor: self.floors.get(name).copied(),
73            })
74        });
75        let medians = self
76            .medians
77            .iter()
78            .filter(|m| {
79                entrants.contains(&m.entrant.as_str()) && entrants.contains(&m.than.as_str())
80            })
81            .map(|m| Check::Median {
82                entrant: m.entrant.clone(),
83                measure: m.measure.clone(),
84                among: m.among.clone(),
85                want: m.want,
86                than: m.than.clone(),
87            });
88        rates.chain(medians).collect()
89    }
90
91    /// The names the targets use that aren't in `known`, sorted, each
92    /// once: a misspelt entrant would otherwise just get no check.
93    pub fn unknown_entrants(&self, known: &[&str]) -> Vec<String> {
94        let named = self
95            .success
96            .keys()
97            .chain(self.bands.values().flat_map(BTreeMap::keys))
98            .chain(self.floors.keys())
99            .chain(self.medians.iter().flat_map(|m| [&m.entrant, &m.than]));
100        named
101            .filter(|name| !known.contains(&name.as_str()))
102            .cloned()
103            .collect::<BTreeSet<String>>()
104            .into_iter()
105            .collect()
106    }
107}