Fleshed out weighted tables
This commit is contained in:
parent
8a486323df
commit
5c3e3b4af7
|
@ -39,6 +39,7 @@
|
||||||
//! [up]: UserPreferences
|
//! [up]: UserPreferences
|
||||||
|
|
||||||
pub mod user_preferences;
|
pub mod user_preferences;
|
||||||
|
pub mod util;
|
||||||
mod weighted_table;
|
mod weighted_table;
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
|
@ -62,7 +62,7 @@ pub enum ParseError {
|
||||||
/// The prefstring assumes the existance of more pronouns than exist
|
/// The prefstring assumes the existance of more pronouns than exist
|
||||||
///
|
///
|
||||||
/// Often a symptom of a malformed prefstring, this error occurs when the prefstring
|
/// Often a symptom of a malformed prefstring, this error occurs when the prefstring
|
||||||
/// is correctly formatted, but when a [`pronouns_today::WeightedTable`] is formed
|
/// is correctly formatted, but when a [`crate::WeightedTable`] is formed
|
||||||
/// from it, the prefstring refers to a pronoun with an index greater than is in the
|
/// from it, the prefstring refers to a pronoun with an index greater than is in the
|
||||||
/// list. For example, setting the weight of pronoun set number 20 when there's only
|
/// list. For example, setting the weight of pronoun set number 20 when there's only
|
||||||
/// 15 pronoun sets known by the instance.
|
/// 15 pronoun sets known by the instance.
|
||||||
|
@ -70,6 +70,14 @@ pub enum ParseError {
|
||||||
/// This can be caused by reducing the number of available pronouns after a prefstring
|
/// This can be caused by reducing the number of available pronouns after a prefstring
|
||||||
/// has been generated, or by trying to parse a lucky malformed prefstring.
|
/// has been generated, or by trying to parse a lucky malformed prefstring.
|
||||||
PrefstringExceedsPronounCount,
|
PrefstringExceedsPronounCount,
|
||||||
|
|
||||||
|
/// Attempted to roll or generate an empty table
|
||||||
|
///
|
||||||
|
/// Whatever you're doing, you would have ended up with an empty
|
||||||
|
/// [`crate::WeightedTable`], either by evaluating a prefstring that doesn't match any
|
||||||
|
/// pronouns, or by creating an invalid weighted table. Either way, you got this
|
||||||
|
/// error instead.
|
||||||
|
EmptyWeightedTable,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ParseError {
|
impl fmt::Display for ParseError {
|
||||||
|
@ -108,6 +116,9 @@ impl fmt::Display for ParseError {
|
||||||
ParseError::PrefstringExceedsPronounCount => {
|
ParseError::PrefstringExceedsPronounCount => {
|
||||||
write!(f, "The user preferences refers to pronouns beyond those that are known")
|
write!(f, "The user preferences refers to pronouns beyond those that are known")
|
||||||
}
|
}
|
||||||
|
ParseError::EmptyWeightedTable => {
|
||||||
|
write!(f, "Attempted to create or roll an empty roll table")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ pub mod v0;
|
||||||
mod error;
|
mod error;
|
||||||
pub use error::ParseError;
|
pub use error::ParseError;
|
||||||
|
|
||||||
use crate::{InstanceSettings, WeightedTable};
|
use crate::{InstanceSettings, Pronoun, WeightedTable};
|
||||||
|
|
||||||
use data_encoding::BASE32_NOPAD;
|
use data_encoding::BASE32_NOPAD;
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ pub trait Preference {
|
||||||
/// crucial step to randomly selecting a pronoun set based on a user's preferences, as
|
/// crucial step to randomly selecting a pronoun set based on a user's preferences, as
|
||||||
/// any selection is done by using a [`WeightedTable`]. All preference versions must
|
/// any selection is done by using a [`WeightedTable`]. All preference versions must
|
||||||
/// implement this method.
|
/// implement this method.
|
||||||
fn into_weighted_table<'a>(&self, settings: &'a InstanceSettings) -> Result<WeightedTable<'a>, ParseError>;
|
fn into_weighted_table<'a>(&self, settings: &'a InstanceSettings) -> Result<WeightedTable<&'a Pronoun>, ParseError>;
|
||||||
|
|
||||||
/// Parse a given prefstring, after it's extraction from base64
|
/// Parse a given prefstring, after it's extraction from base64
|
||||||
///
|
///
|
||||||
|
@ -78,7 +78,7 @@ pub trait Preference {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Preference for UserPreferences {
|
impl Preference for UserPreferences {
|
||||||
fn into_weighted_table<'a>(&self, settings: &'a InstanceSettings) -> Result<WeightedTable<'a>, ParseError> {
|
fn into_weighted_table<'a>(&self, settings: &'a InstanceSettings) -> Result<WeightedTable<&'a Pronoun>, ParseError> {
|
||||||
match self {
|
match self {
|
||||||
UserPreferences::V0(pref) => pref,
|
UserPreferences::V0(pref) => pref,
|
||||||
}.into_weighted_table(settings)
|
}.into_weighted_table(settings)
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
InstanceSettings,
|
InstanceSettings,
|
||||||
|
Pronoun,
|
||||||
user_preferences::{Preference, ParseError},
|
user_preferences::{Preference, ParseError},
|
||||||
WeightedTable,
|
WeightedTable,
|
||||||
};
|
};
|
||||||
|
@ -23,7 +24,7 @@ pub struct UserPreferencesV0 {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Preference for UserPreferencesV0 {
|
impl Preference for UserPreferencesV0 {
|
||||||
fn into_weighted_table<'a>(&self, settings: &'a InstanceSettings) -> Result<WeightedTable<'a>, ParseError> {
|
fn into_weighted_table<'a>(&self, settings: &'a InstanceSettings) -> Result<WeightedTable<&'a Pronoun>, ParseError> {
|
||||||
let mut remaining_pronouns = settings.pronoun_list.iter();
|
let mut remaining_pronouns = settings.pronoun_list.iter();
|
||||||
let mut weighted_pronouns = HashMap::with_capacity(settings.pronoun_list.len());
|
let mut weighted_pronouns = HashMap::with_capacity(settings.pronoun_list.len());
|
||||||
|
|
||||||
|
@ -74,7 +75,7 @@ impl Preference for UserPreferencesV0 {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(WeightedTable(weighted_pronouns))
|
WeightedTable::new(weighted_pronouns)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ```
|
/// ```
|
||||||
|
@ -168,7 +169,7 @@ impl From<u8> for Command {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{InstanceSettings, WeightedTable};
|
use crate::{InstanceSettings, Pronoun, WeightedTable};
|
||||||
use crate::user_preferences::{Preference, ParseError};
|
use crate::user_preferences::{Preference, ParseError};
|
||||||
use crate::user_preferences::v0::{UserPreferencesV0, Command};
|
use crate::user_preferences::v0::{UserPreferencesV0, Command};
|
||||||
|
|
||||||
|
@ -186,10 +187,11 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_table(actual: WeightedTable, expected: Vec<(&str, u8)>) {
|
fn check_table(actual: WeightedTable<&Pronoun>, expected: Vec<(&str, u8)>) {
|
||||||
let actual_simplified = actual.0.into_iter()
|
let actual_simplified = actual.weights()
|
||||||
.filter(|(pronoun, weight)| *weight > 0)
|
.into_iter()
|
||||||
.map(|(pronoun, weight)| (pronoun.subject_pronoun.as_str(), weight))
|
.filter(|(pronoun, weight)| **weight > 0)
|
||||||
|
.map(|(pronoun, weight)| (pronoun.subject_pronoun.as_str(), *weight))
|
||||||
.collect::<HashMap<&str, u8>>();
|
.collect::<HashMap<&str, u8>>();
|
||||||
let expected_owned = expected.into_iter()
|
let expected_owned = expected.into_iter()
|
||||||
.map(|(pronoun, weight)| (pronoun, weight))
|
.map(|(pronoun, weight)| (pronoun, weight))
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
pub fn pcg64(state: &mut u128) -> u64 {
|
||||||
|
let mut x = *state;
|
||||||
|
pcg64_iterstate(state);
|
||||||
|
let count = (x >> (128 - 6)) as u32; // Highest 6 bits
|
||||||
|
x ^= x >> 35;
|
||||||
|
((x >> 58) as u64).rotate_right(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pcg64_iterstate(state: &mut u128) {
|
||||||
|
const MULTIPLIER: u128 = 0xde92a69f6e2f9f25fd0d90f576075fbd;
|
||||||
|
const INCREMENT: u128 = 621;
|
||||||
|
*state = state.wrapping_mul(MULTIPLIER).wrapping_add(INCREMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pcg64_seed(state: &mut u128, bytes: &[u8]) {
|
||||||
|
for byte in bytes {
|
||||||
|
*state = (*state ^ *byte as u128).rotate_right(8);
|
||||||
|
}
|
||||||
|
pcg64_iterstate(state);
|
||||||
|
}
|
|
@ -1,4 +1,7 @@
|
||||||
use crate::Pronoun;
|
use crate::{
|
||||||
|
user_preferences::ParseError,
|
||||||
|
util::{pcg64, pcg64_seed},
|
||||||
|
};
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
@ -22,14 +25,61 @@ pub const COVID_EPOCH: Date = match Date::from_calendar_date(2020, Month::Januar
|
||||||
/// pronoun. Additional methods are provided to perform this random selection on a weighted list,
|
/// pronoun. Additional methods are provided to perform this random selection on a weighted list,
|
||||||
/// using as a seed both an arbitrary string of bytes and a Date.
|
/// using as a seed both an arbitrary string of bytes and a Date.
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
pub struct WeightedTable<'a>(pub HashMap<&'a Pronoun, u8>);
|
pub struct WeightedTable<Loot: std::cmp::Eq + std::hash::Hash>(HashMap<Loot, u8>);
|
||||||
|
|
||||||
impl<'a> WeightedTable<'a> {
|
impl<Loot: std::cmp::Eq + std::hash::Hash + Copy> WeightedTable<Loot> {
|
||||||
|
|
||||||
|
/// Attempt to generate a new table
|
||||||
|
///
|
||||||
|
/// Accepts an iterator of tuples. Each tupple represents one outcome. The left
|
||||||
|
/// element of the tupple is the value that outcome produces (the table loot, if you
|
||||||
|
/// will), and the right element of the tuple is the weight associated with that
|
||||||
|
/// outcome.
|
||||||
|
///
|
||||||
|
/// Performs a safety check to prevent creating an empty roll table. Raises a parse
|
||||||
|
/// error if the safety check fails. The original tabel can be accessed as a
|
||||||
|
/// [`HashMap`] via the [`WeightedTable::weights()`] method
|
||||||
|
pub fn new(table: impl IntoIterator<Item=(Loot, u8)>) -> Result<Self, ParseError> {
|
||||||
|
let table: HashMap<Loot, u8> = table.into_iter()
|
||||||
|
.filter(|(_, weight)| *weight > 0)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if table.is_empty() {
|
||||||
|
Err(ParseError::EmptyWeightedTable)
|
||||||
|
} else {
|
||||||
|
Ok(WeightedTable(table))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access the orginal table of weights underlying this table
|
||||||
|
///
|
||||||
|
/// Returns a [`HashMap`] mapping pronouns to their respective weights.
|
||||||
|
pub fn weights(&self) -> &HashMap<Loot, u8> {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Produces a table of cumulative weights for the roll table
|
||||||
|
///
|
||||||
|
/// Each entry has the weight of itself as well as all of the previous entries. This
|
||||||
|
/// is provided in a [`Vec`] ordered from lowest weight to highest. This table can be
|
||||||
|
/// used to efficiently perform a custom roll.
|
||||||
|
///
|
||||||
|
/// Two values are returned: The table itself, and the highest value in the table
|
||||||
|
/// (that is, the sum of all of the weights)
|
||||||
|
pub fn rollable_table(&self) -> (Vec<(u64, Loot)>, u64) {
|
||||||
|
let mut cumulative_weights: Vec<(u64, Loot)> = Vec::with_capacity(self.0.len());
|
||||||
|
let mut weight_so_far = 0;
|
||||||
|
for (pronoun, weight) in &self.0 {
|
||||||
|
weight_so_far += *weight as u64;
|
||||||
|
cumulative_weights.push((weight_so_far, *pronoun));
|
||||||
|
}
|
||||||
|
(cumulative_weights, weight_so_far)
|
||||||
|
}
|
||||||
|
|
||||||
/// A shorthand for calling [`WeightedTable::select_on_date()`] with today's date
|
/// A shorthand for calling [`WeightedTable::select_on_date()`] with today's date
|
||||||
///
|
///
|
||||||
/// The date is generated for the system's time and timezone
|
/// The date is generated for the system's time and timezone
|
||||||
pub fn select_today(&self, seed: &[u8]) -> &Pronoun {
|
pub fn select_today(&self, seed: &[u8]) -> Loot {
|
||||||
self.select_on_date(
|
self.select_on_date(
|
||||||
seed,
|
seed,
|
||||||
OffsetDateTime::now_local()
|
OffsetDateTime::now_local()
|
||||||
|
@ -41,7 +91,7 @@ impl<'a> WeightedTable<'a> {
|
||||||
/// Randomly select a pronoun set for a given date and name.
|
/// Randomly select a pronoun set for a given date and name.
|
||||||
///
|
///
|
||||||
/// Is a wrapper for calling [`WeightedTable::select`] with the given date mixed into the seed.
|
/// Is a wrapper for calling [`WeightedTable::select`] with the given date mixed into the seed.
|
||||||
pub fn select_on_date(&self, seed: &[u8], date: Date) -> &Pronoun {
|
pub fn select_on_date(&self, seed: &[u8], date: Date) -> Loot {
|
||||||
let mut new_seed: Vec<u8> = Vec::with_capacity(seed.len() + 4);
|
let mut new_seed: Vec<u8> = Vec::with_capacity(seed.len() + 4);
|
||||||
new_seed.extend(
|
new_seed.extend(
|
||||||
(
|
(
|
||||||
|
@ -59,8 +109,18 @@ impl<'a> WeightedTable<'a> {
|
||||||
/// This function is *pure*, and any randomness is produced internally using PRNG seeded with
|
/// This function is *pure*, and any randomness is produced internally using PRNG seeded with
|
||||||
/// the given date and seed. That is to say, for any given seed, this table must always
|
/// the given date and seed. That is to say, for any given seed, this table must always
|
||||||
/// produce the same pronoun set.
|
/// produce the same pronoun set.
|
||||||
pub fn select(&self, seed: &[u8]) -> &Pronoun {
|
pub fn select(&self, seed: &[u8]) -> Loot {
|
||||||
todo!()
|
let (rollable_table, sum_weights) = self.rollable_table();
|
||||||
|
|
||||||
|
let mut generator: u128 = 131213121312;
|
||||||
|
pcg64_seed(&mut generator, seed);
|
||||||
|
let random = pcg64(&mut generator) % sum_weights;
|
||||||
|
|
||||||
|
rollable_table.iter()
|
||||||
|
.filter(|(weight, _)| random < *weight)
|
||||||
|
.next()
|
||||||
|
.expect("A table was generated with zero entries. This should be impossible.")
|
||||||
|
.1
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue