use rand::distr::weighted::WeightedIndex;
use rand::prelude::*;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub fn wedges_from_values<T: Clone>(values: Vec<T>) -> Vec<Wedge<T>> {
values.into_iter().map(Wedge::new).collect()
}
#[must_use]
pub fn wedges_from_tuples<T: Clone>(tuples: Vec<(T, usize)>) -> Vec<Wedge<T>> {
tuples
.into_iter()
.map(|(v, w)| Wedge::new_weighted(v, w))
.collect()
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
#[derive(Debug, PartialEq, Clone)]
pub struct Wedge<T>
where
T: Clone,
{
pub value: T,
pub width: usize,
pub active: bool,
}
impl<T: Clone> Wedge<T> {
pub fn new_weighted(value: T, width: usize) -> Self {
Self {
value,
width,
active: true,
}
}
#[must_use]
pub fn new(value: T) -> Self {
Self {
value,
width: 1,
active: true,
}
}
#[must_use]
pub fn cover(&self) -> Self {
Self {
value: self.value.clone(),
width: self.width,
active: false,
}
}
#[must_use]
pub fn uncover(&self) -> Self {
Self {
value: self.value.clone(),
width: self.width,
active: true,
}
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "type", rename_all = "camelCase"))]
#[derive(Debug, Clone)]
pub struct Spinner<T>
where
T: Clone,
{
wedges: Vec<Wedge<T>>,
weights: Vec<usize>,
}
impl<T: Clone> Spinner<T> {
#[must_use]
pub fn new(wedges: Vec<Wedge<T>>) -> Self {
let weights = wedges.iter().map(|w| w.width).collect();
Self { wedges, weights }
}
#[must_use]
pub fn wedges(&self) -> Vec<Wedge<T>> {
self.wedges.clone()
}
pub fn iter(&self) -> impl Iterator<Item = &Wedge<T>> {
self.wedges.iter()
}
#[must_use]
pub fn spin(&self) -> Option<T> {
if self.wedges.is_empty() {
return None;
}
let mut rng = rand::rng();
let distribution = WeightedIndex::new(&self.weights).ok()?;
let chosen_wedge = self.wedges[distribution.sample(&mut rng)].clone();
if !chosen_wedge.active {
return None;
}
Some(chosen_wedge.value)
}
#[must_use]
pub fn cover_all(&self) -> Spinner<T> {
let all_covered = self.wedges.iter().map(Wedge::cover).collect();
Spinner::new(all_covered)
}
#[must_use]
pub fn uncover_all(&self) -> Spinner<T> {
let uncovered = self.wedges.iter().map(Wedge::uncover).collect();
Spinner::new(uncovered)
}
#[must_use]
pub fn add_wedge(&self, new_wedge: Wedge<T>) -> Spinner<T> {
let mut added = self.wedges.clone();
added.push(new_wedge);
Spinner::new(added)
}
}
impl<T: Clone + PartialEq> Spinner<T> {
#[must_use]
pub fn cover(&self, target_val: &T) -> Spinner<T> {
let wedges = &self.wedges;
let covered = wedges
.iter()
.map(|w| {
if w.value == *target_val {
w.cover()
} else {
w.clone()
}
})
.collect();
Spinner::new(covered)
}
#[must_use]
pub fn uncover(&self, target_val: &T) -> Spinner<T> {
let wedges = &self.wedges;
let uncovered = wedges
.iter()
.map(|w| {
if w.value == *target_val {
w.uncover()
} else {
w.clone()
}
})
.collect();
Spinner::new(uncovered)
}
#[must_use]
pub fn replace_value(&self, match_val: &T, new_val: &T) -> Spinner<T> {
let wedges = &self.wedges;
let updated = wedges
.clone()
.into_iter()
.map(|w| {
if w.value == *match_val {
Wedge::new_weighted(new_val.clone(), w.width)
} else {
w
}
})
.collect();
Spinner::new(updated)
}
#[must_use]
pub fn remove_wedges(&self, value: &T) -> Spinner<T> {
let wedges = &self.wedges;
let shrunken = wedges
.clone()
.into_iter()
.filter(|w| w.value != *value)
.collect();
Spinner::new(shrunken)
}
}
#[cfg(test)]
mod spinner_tests {
use crate::spinners::*;
#[test]
fn wedges_from_values_creates_expected_wedges() {
let wedges = wedges_from_values(vec!["A", "B", "C"]);
assert_eq!(wedges.len(), 3);
assert_eq!(wedges[0], Wedge::new("A"));
assert_eq!(wedges[1], Wedge::new("B"));
assert_eq!(wedges[2], Wedge::new("C"));
}
#[test]
fn wedges_from_tuples_creates_expected_wedges() {
let wedges = wedges_from_tuples(vec![("A", 1), ("B", 2), ("C", 3)]);
assert_eq!(wedges.len(), 3);
assert_eq!(wedges[0], Wedge::new_weighted("A", 1));
assert_eq!(wedges[1], Wedge::new_weighted("B", 2));
assert_eq!(wedges[2], Wedge::new_weighted("C", 3));
}
#[test]
fn can_create_wedges_with_varied_value_types() {
let text_wedge = Wedge::new_weighted("Winner".to_string(), 1);
assert_eq!(text_wedge.value, "Winner");
let numeric = Wedge::new_weighted(10, 1);
assert_eq!(numeric.value, 10);
}
#[test]
fn wedge_new_default_returns_expected_values() {
let bad_one = Wedge::new("Bankrupt!");
assert_eq!(bad_one.width, 1);
assert!(bad_one.active);
assert_eq!(bad_one.value, "Bankrupt!");
}
#[test]
fn can_create_spinners_with_varied_wedge_types() {
let num_wedges = vec![
Wedge::new_weighted(100, 1),
Wedge::new_weighted(200, 1),
Wedge::new_weighted(500, 1),
];
let numeric_spinner = Spinner::new(num_wedges);
assert_eq!(numeric_spinner.wedges.len(), 3);
let text_wedges = vec![
Wedge::new_weighted("Lose a Turn".to_string(), 2),
Wedge::new_weighted("Ahead 4".to_string(), 4),
Wedge::new_weighted("Back 2".to_string(), 4),
];
let text_spinner = Spinner::new(text_wedges);
assert_eq!(text_spinner.wedges.len(), 3);
dbg!(text_spinner);
}
#[test]
fn spin_returns_none_if_no_wedges_in_place() {
let wedges: Vec<Wedge<usize>> = Vec::new();
let spinner = Spinner::new(wedges);
assert!(spinner.spin().is_none());
}
#[test]
fn spin_always_returns_some_if_wedges_in_place() {
let spinner = Spinner::new(vec![
Wedge::new_weighted("Heads", 1),
Wedge::new_weighted("Tails", 1),
]);
for _ in 1..100 {
assert!(spinner.spin().is_some());
}
}
#[test]
fn spin_returns_only_expected_values() {
let spinner = Spinner::new(vec![
Wedge::new_weighted(1, 1),
Wedge::new_weighted(2, 1),
Wedge::new_weighted(3, 1),
]);
for _ in 1..1000 {
assert!((1..=3).contains(&spinner.spin().unwrap()));
}
}
#[test]
fn spin_respects_wedge_weights() {
let spinner = Spinner::new(vec![
Wedge::new_weighted("Heads", 10),
Wedge::new_weighted("Tails", 1),
]);
let mut head_count = 0;
let mut tail_count = 0;
for _ in 1..1000 {
match spinner.spin().unwrap() {
"Heads" => head_count += 1,
"Tails" => tail_count += 1,
_ => panic!("unexpected value returned from spin()"),
}
}
assert!(head_count > tail_count * 6);
}
#[test]
fn spin_returns_none_if_selected_wedge_inactive() {
let spinner = Spinner::new(vec![
Wedge::new("Inactive").cover(),
Wedge::new("Also Inactive").cover(),
]);
for _ in 1..100 {
assert!(spinner.spin().is_none());
}
}
#[test]
fn spinner_cover_inactivates_only_the_right_wedges() {
let spinner = Spinner::new(vec![
Wedge::new_weighted("Red", 2),
Wedge::new_weighted("Blue", 2),
Wedge::new_weighted("Green", 2),
Wedge::new_weighted("Red", 2),
]);
let new_spinner = spinner.cover(&"Red");
for _ in 1..100 {
if let Some(val) = new_spinner.spin() {
assert_ne!(val, "Red");
assert!(["Blue", "Green"].contains(&val));
}
}
}
#[test]
fn spinner_uncover_activates_only_the_right_wedges() {
let spinner = Spinner::new(vec![
Wedge::new_weighted("Red", 2).cover(),
Wedge::new_weighted("Blue", 2).cover(),
Wedge::new_weighted("Green", 2).cover(),
]);
let new_spinner = spinner.uncover(&"Red");
for _ in 1..100 {
if let Some(val) = new_spinner.spin() {
assert_eq!(val, "Red");
}
}
}
#[test]
fn uncover_all_and_cover_all_work_correctly() {
let spinner = Spinner::new(vec![
Wedge::new("Win"),
Wedge::new("Lose"),
Wedge::new("Draw"),
]);
let all_covered = spinner.cover_all();
for _ in 1..100 {
assert!(all_covered.spin().is_none());
}
let all_uncovered = all_covered.uncover_all();
for _ in 1..100 {
assert!(all_uncovered.spin().is_some());
}
}
#[test]
fn can_add_wedge_to_existing_spinner() {
let spinner = Spinner::new(vec![Wedge::new(1), Wedge::new(2)]);
for _ in 1..100 {
if let Some(spin) = spinner.spin() {
assert!([1, 2].contains(&spin));
}
}
let spinner = spinner.add_wedge(Wedge::new(3));
let mut spun_a_3 = false;
for _ in 1..1000 {
if let Some(3) = spinner.spin() {
spun_a_3 = true;
}
}
assert!(
spun_a_3,
"new value not returned from spinner in 1000 spins"
)
}
#[test]
fn can_remove_wedges_matching_value_from_spinner() {
let spinner = Spinner::new(vec![Wedge::new(0), Wedge::new(1), Wedge::new(1)]);
let one_removed = spinner.remove_wedges(&1);
for _ in 1..100 {
match one_removed.spin() {
Some(spin) => assert_eq!(spin, 0),
None => panic!(
"spin should not return None if at least one active wedge is on the spinner"
),
}
}
}
#[test]
fn can_obtain_copy_of_wedges_from_spinner() {
let spinner = Spinner::new(vec![Wedge::new(1), Wedge::new(2)]);
let wedges = spinner.wedges();
let values: Vec<i32> = wedges.iter().map(|w| w.value).collect();
assert_eq!(values, vec![1, 2]);
}
#[test]
fn can_use_iterator_over_spinner_wedges() {
let spinner = Spinner::new(vec![Wedge::new(1), Wedge::new(2)]);
for wedge in spinner.iter() {
assert!((1..=2).contains(&wedge.value));
}
assert_eq!(spinner.iter().count(), 2);
}
#[test]
fn can_replace_values_on_spinner_wedges() {
let rush_albums = Spinner::new(vec![
Wedge::new("2112"),
Wedge::new("Signals"),
Wedge::new("Sheik Yerbouti"), ]);
let rush_albums = rush_albums.replace_value(&"Sheik Yerbouti", &"Power Windows");
for _ in 1..100 {
assert!(["2112", "Signals", "Power Windows"].contains(&rush_albums.spin().unwrap()))
}
}
#[test]
fn replace_value_supports_owned_values() {
let apple = String::from("Apple");
let banana = String::from("Banana");
let spinner = Spinner::new(vec![Wedge::new(apple.clone())]);
let spinner = spinner.replace_value(&apple, &banana);
assert_eq!(spinner.spin().unwrap(), banana);
}
}