const GAMMA: u64 = 0x9E37_79B9_7F4A_7C15;
#[must_use]
pub fn hash_u64(seed: u64, index: u64) -> u64 {
let mut z = seed.wrapping_add(index.wrapping_mul(GAMMA));
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
#[must_use]
pub fn hash_unit(seed: u64, index: u64) -> f64 {
#[allow(clippy::cast_precision_loss)]
{
(hash_u64(seed, index) >> 11) as f64 / (1u64 << 53) as f64
}
}
#[must_use]
pub fn salt(seed: u64, n: u64) -> u64 {
seed ^ n.wrapping_mul(0xD1B5_4A32_D192_ED03)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_matches_the_reference_vectors() {
assert_eq!(hash_u64(0, 1), 0xE220_A839_7B1D_CDAF);
assert_eq!(hash_u64(0, 2), 0x6E78_9E6A_A1B9_65F4);
assert_eq!(hash_u64(0, 3), 0x06C4_5D18_8009_454F);
}
#[test]
fn stepping_the_state_is_hashing_the_index() {
let seed: u64 = 0x1234_5678_9ABC_DEF0;
let mut state = seed;
for index in 1..64 {
state = state.wrapping_add(GAMMA);
let mut z = state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
assert_eq!(z ^ (z >> 31), hash_u64(seed, index), "index {index}");
}
}
#[test]
fn a_unit_value_stays_inside_its_range() {
for index in 0..10_000 {
let unit = hash_unit(0xDEAD_BEEF, index);
assert!((0.0..1.0).contains(&unit), "index {index} gave {unit}");
}
}
#[test]
fn the_unit_values_spread_across_the_range() {
let mut buckets = [0u32; 10];
for index in 0..10_000 {
let unit = hash_unit(1, index);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let bucket = (unit * 10.0) as usize;
buckets[bucket.min(9)] += 1;
}
for (bucket, &count) in buckets.iter().enumerate() {
assert!(
(800..1200).contains(&count),
"bucket {bucket} holds {count} of 10000, which is lopsided"
);
}
}
#[test]
fn neighbouring_indices_are_not_neighbouring_values() {
let mut close = 0;
for index in 0..1_000 {
let a = hash_unit(7, index);
let b = hash_unit(7, index + 1);
if (a - b).abs() < 0.01 {
close += 1;
}
}
assert!(
close < 50,
"{close} of 1000 consecutive pairs landed within 1% of each other — \
about 20 is what independence looks like, and a ramp would give 1000"
);
}
#[test]
fn a_different_seed_is_a_different_sequence() {
let same = (0..1_000)
.filter(|&i| hash_u64(1, i) == hash_u64(2, i))
.count();
assert_eq!(same, 0, "{same} indices collided between two seeds");
}
#[test]
fn salting_deals_a_new_hand_each_run() {
let seed: u64 = 0xABCD_1234_5678_9EF0;
assert_eq!(salt(seed, 0), seed, "the first run is the seed as given");
let mut seen = std::collections::HashSet::new();
for run in 0..1_000 {
assert!(seen.insert(salt(seed, run)), "run {run} repeated a seed");
}
}
}