Files
scrabble-with-numbers/board-shared/src/deck.rs

167 lines
4.7 KiB
Rust

use crate::tile::Operator;
use enum_map::EnumMap;
use rand::Rng;
type DeckSize = u16;
type DigitDeck = [DeckSize; 19];
/// When a deck is empty, new tiles cannot be retrieved.
pub type EmptyDeckError = ();
/// A entire deck of tiles.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Deck {
/// The digits and their current count.
digits: DigitDeck,
/// The operators and their current count.
operators: EnumMap<Operator, DeckSize>,
}
impl Deck {
pub fn new_complete() -> Self {
let mut deck = Self::default();
deck.add_operator_times(Operator::Add, 10);
deck.add_operator_times(Operator::Subtract, 10);
deck.add_operator_times(Operator::Multiply, 6);
deck.add_operator_times(Operator::Divide, 4);
for digit in -9..0 {
deck.add_digit_times(digit, 2);
}
for digit in 0..=9 {
deck.add_digit_times(digit, 8);
}
deck
}
/// Adds a single digit to the deck.
pub fn add_digit(&mut self, digit: i8) {
self.add_digit_times(digit, 1)
}
/// Adds a digit multiple times to the deck.
pub fn add_digit_times(&mut self, digit: i8, times: DeckSize) {
self.digits[Deck::digit_index(digit)] += times;
}
/// Adds a single operator to the deck.
pub fn add_operator(&mut self, operator: Operator) {
self.add_operator_times(operator, 1)
}
/// Adds an operator multiple times to the deck.
pub fn add_operator_times(&mut self, operator: Operator, times: DeckSize) {
self.operators[operator] += times;
}
/// Gets the index of a digit in the digit deck.
fn digit_index(digit: i8) -> usize {
(digit + 9) as usize
}
}
/// A deck of tiles that can be chosen at random.
#[derive(Debug, Clone, Default)]
pub struct RngDeck {
deck: Deck,
rng: rand::rngs::ThreadRng,
}
impl RngDeck {
pub fn new_complete() -> Self {
Self {
deck: Deck::new_complete(),
rng: rand::thread_rng(),
}
}
/// Gets a random tile from the deck and remove it from the deck.
pub fn rand_digit(&mut self) -> Option<i8> {
let sum = self.deck.digits.iter().sum();
Self::select_rng(&mut self.rng, sum, self.deck.digits.iter_mut().enumerate())
.map(|n| n as i8 - 9)
}
/// Gets a random operator from the deck and remove it from the deck.
pub fn rand_operator(&mut self) -> Option<Operator> {
let sum = self.deck.operators.values().sum();
Self::select_rng(&mut self.rng, sum, self.deck.operators.iter_mut())
}
/// Selects a random item from an iterator of (item, count) pairs.
/// The count is decremented by one if the item is selected.
fn select_rng<'a, T>(
rng: &mut rand::rngs::ThreadRng,
sum: DeckSize,
it: impl Iterator<Item = (T, &'a mut DeckSize)>,
) -> Option<T> {
if sum == 0 {
return None;
}
let mut threshold = rng.gen_range(1..=sum);
for (item, count) in it {
threshold = threshold.saturating_sub(*count);
if threshold == 0 {
*count -= 1;
return Some(item);
}
}
unreachable!()
}
}
impl PartialEq for RngDeck {
fn eq(&self, other: &Self) -> bool {
self.deck == other.deck
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_empty() {
let mut deck = RngDeck::default();
assert_eq!(deck.rand_digit(), None);
assert_eq!(deck.rand_operator(), None);
}
#[test]
fn one_digit() {
let mut deck = RngDeck::default();
deck.deck.add_digit(1);
assert_eq!(deck.rand_digit(), Some(1));
assert_eq!(deck.rand_operator(), None);
assert_eq!(deck.rand_digit(), None);
}
#[test]
fn one_operator() {
let mut deck = RngDeck::default();
deck.deck.add_operator(Operator::Multiply);
assert_eq!(deck.rand_digit(), None);
assert_eq!(deck.rand_operator(), Some(Operator::Multiply));
assert_eq!(deck.rand_operator(), None);
}
#[test]
fn respect_proportion() {
let mut deck = RngDeck::default();
deck.deck.add_digit_times(-4, 2);
deck.deck.add_digit_times(3, 7);
deck.deck.add_digit_times(7, 3);
let mut actual = DigitDeck::default();
while let Some(digit) = deck.rand_digit() {
actual[Deck::digit_index(digit)] += 1;
}
let mut excepted = DigitDeck::default();
excepted[Deck::digit_index(-4)] = 2;
excepted[Deck::digit_index(3)] = 7;
excepted[Deck::digit_index(7)] = 3;
assert_eq!(actual, excepted);
}
}