use std::fmt;
use std::str::FromStr;
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PlayerId([u8; PlayerId::BYTES]);
impl PlayerId {
pub const BYTES: usize = 16;
#[must_use]
pub const fn from_bytes(bytes: [u8; Self::BYTES]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn to_bytes(self) -> [u8; Self::BYTES] {
self.0
}
#[must_use]
pub fn from_seed(seed: u64) -> Self {
let mut bytes = [0u8; Self::BYTES];
let (high, low) = bytes.split_at_mut(Self::BYTES / 2);
high.copy_from_slice(&crate::rand::hash_u64(seed, 0).to_be_bytes());
low.copy_from_slice(&crate::rand::hash_u64(seed, 1).to_be_bytes());
Self(bytes)
}
}
impl fmt::Display for PlayerId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for byte in self.0 {
write!(f, "{byte:02x}")?;
}
Ok(())
}
}
impl fmt::Debug for PlayerId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "PlayerId({self})")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParsePlayerIdError {
Length(usize),
NotHex(char),
}
impl fmt::Display for ParsePlayerIdError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Length(len) => write!(
f,
"a player id is {} hex digits, not {len}",
PlayerId::BYTES * 2
),
Self::NotHex(c) => write!(f, "{c:?} is not a hex digit"),
}
}
}
impl std::error::Error for ParsePlayerIdError {}
impl FromStr for PlayerId {
type Err = ParsePlayerIdError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
let digits: Vec<char> = text.chars().collect();
if digits.len() != Self::BYTES * 2 {
return Err(ParsePlayerIdError::Length(digits.len()));
}
let digit = |c: char| {
c.to_digit(16)
.and_then(|value| u8::try_from(value).ok())
.ok_or(ParsePlayerIdError::NotHex(c))
};
let mut bytes = [0u8; Self::BYTES];
let (pairs, _) = digits.as_chunks::<2>();
for (byte, &[high, low]) in bytes.iter_mut().zip(pairs) {
*byte = (digit(high)? << 4) | digit(low)?;
}
Ok(Self(bytes))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_printed_form_reads_back_to_the_same_id() {
let id = PlayerId::from_bytes(std::array::from_fn(|i| (i as u8) * 17));
let text = id.to_string();
assert_eq!(text, "00112233445566778899aabbccddeeff");
assert_eq!(text.parse::<PlayerId>(), Ok(id));
assert_eq!(text.to_uppercase().parse::<PlayerId>(), Ok(id));
assert_eq!(format!("{id:?}"), format!("PlayerId({text})"));
}
#[test]
fn a_string_that_is_not_an_id_is_refused() {
let short = "0".repeat(31);
assert_eq!(
short.parse::<PlayerId>(),
Err(ParsePlayerIdError::Length(31))
);
assert_eq!(
"0".repeat(33).parse::<PlayerId>(),
Err(ParsePlayerIdError::Length(33))
);
let not_hex = format!("{}g", "0".repeat(31));
assert_eq!(
not_hex.parse::<PlayerId>(),
Err(ParsePlayerIdError::NotHex('g'))
);
}
#[test]
fn a_seed_gives_the_same_id_every_time_and_another_seed_another() {
assert_eq!(PlayerId::from_seed(7), PlayerId::from_seed(7));
let ids: std::collections::HashSet<PlayerId> = (0..64).map(PlayerId::from_seed).collect();
assert_eq!(ids.len(), 64, "two seeds gave one id");
let bytes = PlayerId::from_seed(7).to_bytes();
let (high, low) = bytes.split_at(PlayerId::BYTES / 2);
assert_ne!(high, low, "both halves came from one hash");
assert!(low.iter().any(|&byte| byte != 0), "the low half is empty");
}
}