use rand::seq::SliceRandom as _;
use crate::{GameResult, gameerror::GameError};
#[derive(Debug, Clone, PartialEq)]
pub struct RefillingPool<T> {
items: Vec<T>,
unused: Vec<usize>,
}
impl<T> RefillingPool<T> {
pub fn new(items: impl IntoIterator<Item = T>) -> GameResult<Self> {
let items: Vec<T> = items.into_iter().collect();
if items.is_empty() {
return Err(GameError::PoolCannotBeEmpty);
}
let mut unused: Vec<usize> = (0..items.len()).collect();
unused.shuffle(&mut rand::rng());
Ok(Self { items, unused })
}
pub fn add(&mut self, item: T) {
self.items.push(item);
self.unused.push(self.items.len() - 1);
self.unused.shuffle(&mut rand::rng());
}
pub fn remove_index(&mut self, index: usize) -> GameResult<T> {
if index >= self.items.len() {
return Err(GameError::InvalidPoolIndex(index, self.items.len()));
}
if self.items.len() == 1 {
return Err(GameError::PoolCannotBeEmpty);
}
self.unused.retain(|&ui| ui != index);
let item = self.items.swap_remove(index);
self.refill();
Ok(item)
}
#[must_use]
pub fn full_size(&self) -> usize {
self.items.len()
}
#[must_use]
pub fn current_size(&self) -> usize {
self.unused.len()
}
fn refill(&mut self) {
self.unused = (0..self.items.len()).collect();
self.unused.shuffle(&mut rand::rng());
}
}
impl<T: PartialEq> RefillingPool<T> {
pub fn remove(&mut self, item: &T) -> Option<T> {
if let Some(index) = self.items.iter().position(|i| i == item) {
self.remove_index(index).ok()
} else {
None
}
}
pub fn index_of(&self, item: &T) -> Option<usize> {
self.items.iter().position(|i| i == item)
}
}
impl<T: Clone> RefillingPool<T> {
#[allow(
clippy::missing_panics_doc,
reason = "cannot panic, refills before pop()"
)]
pub fn draw(&mut self) -> T {
if self.unused.is_empty() {
self.refill();
}
self.items[self.unused.pop().unwrap()].clone()
}
pub fn draw_where<F>(&mut self, mut pred: F) -> Option<T>
where
F: FnMut(&T) -> bool,
{
if self.unused.is_empty() {
self.refill();
}
let passing_unused_idx = self.unused.iter().position(|i| pred(&self.items[*i]))?;
let passing_item_idx = self.unused.swap_remove(passing_unused_idx);
Some(self.items[passing_item_idx].clone())
}
pub fn draw_with_context<C, F>(&mut self, context: &C, mut chooser: F) -> Option<T>
where
F: FnMut(&C, &T) -> bool,
{
if self.unused.is_empty() {
self.refill();
}
let passing_unused_idx = self
.unused
.iter()
.position(|i| chooser(context, &self.items[*i]))?;
let passing_item_idx = self.unused.swap_remove(passing_unused_idx);
Some(self.items[passing_item_idx].clone())
}
pub fn draw_with_context_or_any<C, F>(&mut self, context: &C, chooser: F) -> T
where
F: FnMut(&C, &T) -> bool,
{
self.draw_with_context(context, chooser)
.unwrap_or_else(|| self.draw())
}
}
impl<T: Clone> Iterator for RefillingPool<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
Some(self.draw())
}
}
impl<T: Clone> TryFrom<&[T]> for RefillingPool<T> {
type Error = GameError;
fn try_from(items: &[T]) -> Result<Self, Self::Error> {
RefillingPool::new(items.to_vec())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn creating_pool_with_no_refills_is_error() {
assert!(RefillingPool::<()>::new([]).is_err_and(|e| e.eq(&GameError::PoolCannotBeEmpty)));
}
#[test]
fn removing_last_item_is_error() {
let mut pool = RefillingPool::new([1]).unwrap();
assert!(
pool.remove_index(0)
.is_err_and(|e| e.eq(&GameError::PoolCannotBeEmpty))
);
}
#[test]
fn pool_full_and_current_sizes_correct_after_draws_and_refill() {
let mut pool = RefillingPool::new([1, 2, 3]).unwrap();
assert_eq!(dbg!(pool.full_size()), 3);
assert_eq!(dbg!(pool.current_size()), 3);
pool.draw();
assert_eq!(pool.full_size(), 3);
assert_eq!(pool.current_size(), 2);
pool.draw();
pool.draw();
assert_eq!(dbg!(pool.full_size()), 3);
assert_eq!(dbg!(pool.current_size()), 0);
pool.draw();
assert_eq!(pool.current_size(), 2);
}
#[test]
fn test_refilling_pool_add() {
let mut pool = RefillingPool::new([1, 2, 3]).unwrap();
for _ in 1..=300 {
assert!([1, 2, 3].contains(&pool.draw()))
}
pool.add(4);
assert_eq!(pool.full_size(), 4);
for _ in 1..=300 {
assert!([1, 2, 3, 4].contains(&pool.draw()))
}
}
#[test]
fn test_refilling_pool_remove_index() {
let mut pool = RefillingPool::new([1, 2, 3]).unwrap();
let _ = pool.remove_index(0);
assert_eq!(pool.full_size(), 2);
assert_eq!(pool.current_size(), 2);
assert!(pool.take(500).all(|item| item != 1));
}
#[test]
fn test_refilling_pool_index_of() {
let pool = RefillingPool::new([1, 2, 3]).unwrap();
assert_eq!(pool.index_of(&1), Some(0));
assert_eq!(pool.index_of(&2), Some(1));
assert_eq!(pool.index_of(&3), Some(2));
assert_eq!(pool.index_of(&4), None);
}
#[test]
fn test_refilling_pool_remove() {
let mut pool = RefillingPool::new([1, 2, 3]).unwrap();
let _ = pool.remove(&1);
assert_eq!(pool.full_size(), 2);
assert_eq!(pool.current_size(), 2);
assert!(pool.take(500).all(|item| item != 1));
}
}