bloop_server_framework/evaluator/
spelling_bee.rs1use crate::achievement::AchievementContext;
2use crate::evaluator::{EvalResult, Evaluator};
3use std::collections::HashMap;
4use std::fmt::Debug;
5use std::time::Duration;
6use thiserror::Error;
7
8#[macro_export]
23macro_rules! char_map {
24 ($($char:literal => $client_id:expr),* $(,)?) => {{
25 let mut map = ::std::collections::HashMap::new();
26 $(
27 map.insert($char, $client_id);
28 )*
29 map
30 }};
31}
32
33#[derive(Debug)]
40pub struct SpellingBeeEvaluator {
41 client_ids: Vec<String>,
42 max_time: Duration,
43}
44
45#[derive(Debug, Error)]
46pub enum Error {
47 #[error("the character '{0}' does not exist in character map")]
48 UnregisteredCharacter(char),
49 #[error("the provided word must be at least one character")]
50 WordTooShort,
51 #[error("the provided word is too long")]
52 WordTooLong,
53}
54
55type Result<T> = std::result::Result<T, Error>;
56
57impl SpellingBeeEvaluator {
58 pub fn new(
81 word: &str,
82 characters: &HashMap<char, &str>,
83 time_per_character: Duration,
84 ) -> Result<Self> {
85 let mut client_ids = Vec::with_capacity(word.len());
86
87 if word.is_empty() {
88 return Err(Error::WordTooShort);
89 }
90
91 if word.len() > 100 {
92 return Err(Error::WordTooLong);
93 }
94
95 let max_time = time_per_character
96 .checked_mul(word.len() as u32)
97 .ok_or(Error::WordTooLong)?;
98
99 for char in word.chars().rev() {
100 let Some(client_id) = characters.get(&char) else {
101 return Err(Error::UnregisteredCharacter(char));
102 };
103
104 client_ids.push(client_id.to_string());
105 }
106
107 Ok(Self {
108 client_ids,
109 max_time,
110 })
111 }
112}
113
114impl<Player, Metadata, Trigger> Evaluator<Player, Metadata, Trigger> for SpellingBeeEvaluator {
115 fn evaluate(
120 &self,
121 ctx: &AchievementContext<Player, Metadata, Trigger>,
122 ) -> impl Into<EvalResult> {
123 if ctx.current_bloop.client_id != *self.client_ids.first().unwrap() {
124 return false;
125 }
126
127 let last_client_ids = ctx
128 .global_bloops()
129 .filter(ctx.filter_within_window(self.max_time))
130 .filter(ctx.filter_current_player())
131 .take(self.client_ids.len())
132 .map(|bloop| &bloop.client_id);
133
134 last_client_ids.eq(self.client_ids.iter().skip(1))
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::bloop::Bloop;
142 use crate::char_map;
143 use crate::evaluator::{EvalResult, Evaluator};
144 use crate::test_utils::{MockPlayer, TestCtxBuilder};
145 use chrono::Utc;
146 use std::collections::HashMap;
147 use std::sync::{Arc, RwLock};
148 use std::time::Duration;
149
150 fn make_bloop(
151 player: &Arc<RwLock<MockPlayer>>,
152 client_id: &str,
153 offset_secs: u64,
154 ) -> Bloop<MockPlayer> {
155 let recorded_at = Utc::now() - Duration::from_secs(offset_secs);
156 Bloop::new(player.clone(), client_id.to_string(), recorded_at)
157 }
158
159 #[test]
160 fn matches_correct_sequence() {
161 let word = "hey";
162 let client_map = char_map! {
163 'h' => "client1",
164 'e' => "client2",
165 'y' => "client3"
166 };
167
168 let (player, _) = MockPlayer::builder().build();
169 let bloops = vec![
170 make_bloop(&player, "client1", 20),
171 make_bloop(&player, "client2", 10),
172 ];
173 let mut builder = TestCtxBuilder::new(make_bloop(&player, "client3", 0)).bloops(bloops);
174
175 let evaluator =
176 SpellingBeeEvaluator::new(word, &client_map, Duration::from_secs(15)).unwrap();
177 assert_eq!(
178 evaluator.evaluate(&builder.build()).into(),
179 EvalResult::AwardSelf
180 );
181 }
182
183 #[test]
184 fn fails_on_wrong_order() {
185 let word = "hey";
186 let client_map = char_map! {
187 'h' => "client1",
188 'e' => "client2",
189 'y' => "client3"
190 };
191
192 let (player, _) = MockPlayer::builder().build();
193 let bloops = vec![
194 make_bloop(&player, "client1", 20),
195 make_bloop(&player, "client3", 10),
196 ];
197
198 let mut builder = TestCtxBuilder::new(make_bloop(&player, "client2", 0)).bloops(bloops);
199
200 let evaluator =
201 SpellingBeeEvaluator::new(word, &client_map, Duration::from_secs(15)).unwrap();
202 assert_eq!(
203 evaluator.evaluate(&builder.build()).into(),
204 EvalResult::NoAward
205 );
206 }
207
208 #[test]
209 fn fails_if_current_id_mismatch() {
210 let word = "abc";
211 let client_map = char_map! {
212 'a' => "client1",
213 'b' => "client2",
214 'c' => "client3"
215 };
216
217 let (player, _) = MockPlayer::builder().build();
218 let bloops = vec![
219 make_bloop(&player, "client1", 20),
220 make_bloop(&player, "client2", 10),
221 ];
222
223 let mut builder =
224 TestCtxBuilder::new(make_bloop(&player, "wrong_client", 0)).bloops(bloops);
225
226 let evaluator =
227 SpellingBeeEvaluator::new(word, &client_map, Duration::from_secs(15)).unwrap();
228 assert_eq!(
229 evaluator.evaluate(&builder.build()).into(),
230 EvalResult::NoAward
231 );
232 }
233
234 #[test]
235 fn fails_if_too_slow() {
236 let word = "abc";
237 let client_map = char_map! {
238 'a' => "client1",
239 'b' => "client2",
240 'c' => "client3"
241 };
242
243 let (player, _) = MockPlayer::builder().build();
244 let player = player.clone();
245 let bloops = vec![
246 make_bloop(&player, "client1", 100),
247 make_bloop(&player, "client2", 50),
248 ];
249
250 let mut builder = TestCtxBuilder::new(make_bloop(&player, "client3", 0)).bloops(bloops);
251
252 let evaluator =
253 SpellingBeeEvaluator::new(word, &client_map, Duration::from_secs(10)).unwrap();
254 assert_eq!(
255 evaluator.evaluate(&builder.build()).into(),
256 EvalResult::NoAward
257 );
258 }
259
260 #[test]
261 fn build_fails_if_unregistered_char() {
262 let client_map = char_map! {
263 'a' => "client1",
264 'b' => "client2"
265 };
266
267 let result = SpellingBeeEvaluator::new("abc", &client_map, Duration::from_secs(5));
268 assert!(matches!(result, Err(Error::UnregisteredCharacter('c'))));
269 }
270
271 #[test]
272 fn build_fails_if_word_too_short() {
273 let client_map = HashMap::new();
274
275 let result = SpellingBeeEvaluator::new("", &client_map, Duration::from_secs(1));
276 assert!(matches!(result, Err(Error::WordTooShort)));
277 }
278
279 #[test]
280 fn build_fails_if_word_too_long() {
281 let client_map = ('a'..='z').map(|c| (c, "client")).collect();
282 let long_word: String = std::iter::repeat('a').take(1_000).collect();
283
284 let result = SpellingBeeEvaluator::new(&long_word, &client_map, Duration::from_secs(1));
285 assert!(matches!(result, Err(Error::WordTooLong)));
286 }
287}