use crate::achievement::AchievementContext;
use crate::evaluator::{EvalResult, Evaluator};
use std::collections::HashMap;
use std::fmt::Debug;
use std::time::Duration;
use thiserror::Error;
#[macro_export]
macro_rules! char_map {
($($char:literal => $client_id:expr),* $(,)?) => {{
let mut map = ::std::collections::HashMap::new();
$(
map.insert($char, $client_id);
)*
map
}};
}
#[derive(Debug)]
pub struct SpellingBeeEvaluator {
client_ids: Vec<String>,
max_time: Duration,
}
#[derive(Debug, Error)]
pub enum Error {
#[error("the character '{0}' does not exist in character map")]
UnregisteredCharacter(char),
#[error("the provided word must be at least one character")]
WordTooShort,
#[error("the provided word is too long")]
WordTooLong,
}
type Result<T> = std::result::Result<T, Error>;
impl SpellingBeeEvaluator {
pub fn new(
word: &str,
characters: &HashMap<char, &str>,
time_per_character: Duration,
) -> Result<Self> {
let mut client_ids = Vec::with_capacity(word.len());
if word.is_empty() {
return Err(Error::WordTooShort);
}
if word.len() > 100 {
return Err(Error::WordTooLong);
}
let max_time = time_per_character
.checked_mul(word.len() as u32)
.ok_or(Error::WordTooLong)?;
for char in word.chars().rev() {
let Some(client_id) = characters.get(&char) else {
return Err(Error::UnregisteredCharacter(char));
};
client_ids.push(client_id.to_string());
}
Ok(Self {
client_ids,
max_time,
})
}
}
impl<Player, Metadata, Trigger> Evaluator<Player, Metadata, Trigger> for SpellingBeeEvaluator {
fn evaluate(
&self,
ctx: &AchievementContext<Player, Metadata, Trigger>,
) -> impl Into<EvalResult> {
if ctx.current_bloop.client_id != *self.client_ids.first().unwrap() {
return false;
}
let last_client_ids = ctx
.global_bloops()
.filter(ctx.filter_within_window(self.max_time))
.filter(ctx.filter_current_player())
.take(self.client_ids.len() - 1)
.map(|bloop| &bloop.client_id);
last_client_ids.eq(self.client_ids.iter().skip(1))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bloop::Bloop;
use crate::evaluator::{EvalResult, Evaluator};
use crate::test_utils::{MockPlayer, TestCtxBuilder};
use chrono::Utc;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::Duration;
fn make_bloop(
player: &Arc<RwLock<MockPlayer>>,
client_id: &str,
offset_secs: u64,
) -> Bloop<MockPlayer> {
let recorded_at = Utc::now() - Duration::from_secs(offset_secs);
Bloop::new(player.clone(), client_id.to_string(), recorded_at)
}
#[test]
fn matches_correct_sequence() {
let word = "hey";
let client_map = char_map! {
'h' => "client1",
'e' => "client2",
'y' => "client3"
};
let (player, _) = MockPlayer::builder().build();
let bloops = vec![
make_bloop(&player, "client1", 20),
make_bloop(&player, "client2", 10),
];
let mut builder = TestCtxBuilder::new(make_bloop(&player, "client3", 0)).bloops(bloops);
let evaluator =
SpellingBeeEvaluator::new(word, &client_map, Duration::from_secs(15)).unwrap();
assert_eq!(
evaluator.evaluate(&builder.build()).into(),
EvalResult::AwardSelf
);
}
#[test]
fn matches_correct_sequence_with_prior_bloops() {
let word = "hey";
let client_map = char_map! {
'h' => "client1",
'e' => "client2",
'y' => "client3"
};
let (player, _) = MockPlayer::builder().build();
let bloops = vec![
make_bloop(&player, "client1", 20),
make_bloop(&player, "client2", 10),
make_bloop(&player, "client3", 30),
];
let mut builder = TestCtxBuilder::new(make_bloop(&player, "client3", 0)).bloops(bloops);
let evaluator =
SpellingBeeEvaluator::new(word, &client_map, Duration::from_secs(25)).unwrap();
assert_eq!(
evaluator.evaluate(&builder.build()).into(),
EvalResult::AwardSelf
);
}
#[test]
fn fails_on_wrong_order() {
let word = "hey";
let client_map = char_map! {
'h' => "client1",
'e' => "client2",
'y' => "client3"
};
let (player, _) = MockPlayer::builder().build();
let bloops = vec![
make_bloop(&player, "client1", 20),
make_bloop(&player, "client3", 10),
];
let mut builder = TestCtxBuilder::new(make_bloop(&player, "client2", 0)).bloops(bloops);
let evaluator =
SpellingBeeEvaluator::new(word, &client_map, Duration::from_secs(15)).unwrap();
assert_eq!(
evaluator.evaluate(&builder.build()).into(),
EvalResult::NoAward
);
}
#[test]
fn fails_if_current_id_mismatch() {
let word = "abc";
let client_map = char_map! {
'a' => "client1",
'b' => "client2",
'c' => "client3"
};
let (player, _) = MockPlayer::builder().build();
let bloops = vec![
make_bloop(&player, "client1", 20),
make_bloop(&player, "client2", 10),
];
let mut builder =
TestCtxBuilder::new(make_bloop(&player, "wrong_client", 0)).bloops(bloops);
let evaluator =
SpellingBeeEvaluator::new(word, &client_map, Duration::from_secs(15)).unwrap();
assert_eq!(
evaluator.evaluate(&builder.build()).into(),
EvalResult::NoAward
);
}
#[test]
fn fails_if_too_slow() {
let word = "abc";
let client_map = char_map! {
'a' => "client1",
'b' => "client2",
'c' => "client3"
};
let (player, _) = MockPlayer::builder().build();
let player = player.clone();
let bloops = vec![
make_bloop(&player, "client1", 100),
make_bloop(&player, "client2", 50),
];
let mut builder = TestCtxBuilder::new(make_bloop(&player, "client3", 0)).bloops(bloops);
let evaluator =
SpellingBeeEvaluator::new(word, &client_map, Duration::from_secs(10)).unwrap();
assert_eq!(
evaluator.evaluate(&builder.build()).into(),
EvalResult::NoAward
);
}
#[test]
fn build_fails_if_unregistered_char() {
let client_map = char_map! {
'a' => "client1",
'b' => "client2"
};
let result = SpellingBeeEvaluator::new("abc", &client_map, Duration::from_secs(5));
assert!(matches!(result, Err(Error::UnregisteredCharacter('c'))));
}
#[test]
fn build_fails_if_word_too_short() {
let client_map = HashMap::new();
let result = SpellingBeeEvaluator::new("", &client_map, Duration::from_secs(1));
assert!(matches!(result, Err(Error::WordTooShort)));
}
#[test]
fn build_fails_if_word_too_long() {
let client_map = ('a'..='z').map(|c| (c, "client")).collect();
let long_word = "a".repeat(1_000);
let result = SpellingBeeEvaluator::new(&long_word, &client_map, Duration::from_secs(1));
assert!(matches!(result, Err(Error::WordTooLong)));
}
}