Something went wrong. Try again.
HEXWORK - A Lantern in the Deep: 3D ASCII terminal roguelike RPG in Rust.
Entirely designed and built by Muse Spark 1.3 to evaluate if I like the model.
Something went wrong. Try again.
6.4 kB · 206 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207//! Checked millimetre geometry for the v5 simulation.//!//! Positions reserve one voxel at either end of the i32 cell domain. Negative//! occupancy uses Euclidean floor, never rounding. Floating point is an output//! boundary only; it must not feed canonical simulation state.
pub const VOXEL_MM: i64 = 1_000;pub const MIN_POSITION_MM: i64 = (i32::MIN as i64 + 1) * VOXEL_MM;pub const MAX_POSITION_MM: i64 = (i32::MAX as i64 - 1) * VOXEL_MM + 999;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]pub enum SpatialError { OutOfBounds, Overflow, InvalidExtent,}
impl std::fmt::Display for SpatialError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { Self::OutOfBounds => "position outside canonical world bounds", Self::Overflow => "canonical geometry arithmetic overflow", Self::InvalidExtent => "body extent must be positive and bounded", }) }}
impl std::error::Error for SpatialError {}
/// A validated feet position. Coordinates cannot be mutated without validation.#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]pub struct FixedVec3([i64; 3]);
impl FixedVec3 { pub fn new(mm: [i64; 3]) -> Result<Self, SpatialError> { if mm .iter() .all(|v| (MIN_POSITION_MM..=MAX_POSITION_MM).contains(v)) { Ok(Self(mm)) } else { Err(SpatialError::OutOfBounds) } }
pub const fn millimetres(self) -> [i64; 3] { self.0 }
pub fn from_cell_feet(cell: [i32; 3]) -> Result<Self, SpatialError> { Self::new([ i64::from(cell[0]) * VOXEL_MM + 500, i64::from(cell[1]) * VOXEL_MM + 500, i64::from(cell[2]) * VOXEL_MM, ]) }
pub fn cell(self) -> [i32; 3] { self.0.map(|v| v.div_euclid(VOXEL_MM) as i32) }
pub fn checked_translate(self, delta_mm: [i64; 3]) -> Result<Self, SpatialError> { let mut result = self.0; for axis in 0..3 { result[axis] = result[axis] .checked_add(delta_mm[axis]) .ok_or(SpatialError::Overflow)?; } Self::new(result) }
pub fn squared_distance(self, other: Self) -> Result<i128, SpatialError> { let mut sum = 0i128; for axis in 0..3 { let d = i128::from(self.0[axis]) - i128::from(other.0[axis]); sum = sum .checked_add(d.checked_mul(d).ok_or(SpatialError::Overflow)?) .ok_or(SpatialError::Overflow)?; } Ok(sum) }
/// Lossy metres for rendering only. Never use this for occupancy or rules. pub fn presentation_metres(self) -> [f32; 3] { self.0.map(|v| v as f32 / VOXEL_MM as f32) }}
/// Upright axis-aligned box, not a cylinder; origin is centred XY at feet Z.#[derive(Clone, Copy, Debug, PartialEq, Eq)]pub struct BodyExtents { half_width_mm: i64, half_depth_mm: i64, height_mm: i64,}
impl BodyExtents { pub const PLAYER: Self = Self { half_width_mm: 300, half_depth_mm: 300, height_mm: 1_800, };
pub fn new( half_width_mm: i64, half_depth_mm: i64, height_mm: i64, ) -> Result<Self, SpatialError> { if [half_width_mm, half_depth_mm, height_mm] .iter() .any(|v| *v <= 0 || *v > MAX_POSITION_MM) { return Err(SpatialError::InvalidExtent); } Ok(Self { half_width_mm, half_depth_mm, height_mm, }) }
pub const fn millimetres(self) -> [i64; 3] { [self.half_width_mm, self.half_depth_mm, self.height_mm] }
pub fn bounds(self, feet: FixedVec3) -> Result<(FixedVec3, FixedVec3), SpatialError> { Ok(( feet.checked_translate([-self.half_width_mm, -self.half_depth_mm, 0])?, feet.checked_translate([self.half_width_mm, self.half_depth_mm, self.height_mm])?, )) }}
#[cfg(test)]mod tests { use super::*;
// Literal expected values are independent of the production floor helper. #[test] fn fixed_spatial_reference_vectors() { for (mm, cell) in [ (-2001, -3), (-2000, -2), (-1001, -2), (-1000, -1), (-999, -1), (-1, -1), (0, 0), (999, 0), (1000, 1), ] { assert_eq!(FixedVec3::new([mm, mm, mm]).unwrap().cell(), [cell; 3]); } assert_eq!( FixedVec3::from_cell_feet([-2, 3, -1]) .unwrap() .millimetres(), [-1500, 3500, -1000] ); let a = FixedVec3::new([-1000, 2000, -3000]).unwrap(); let b = FixedVec3::new([2000, -2000, 9000]).unwrap(); assert_eq!(a.squared_distance(b), Ok(169_000_000)); assert_eq!(MIN_POSITION_MM, -2_147_483_647_000); assert_eq!(MAX_POSITION_MM, 2_147_483_646_999); }
#[test] fn fixed_spatial_bounds_and_overflow() { assert!(FixedVec3::new([MIN_POSITION_MM; 3]).is_ok()); assert!(FixedVec3::new([MAX_POSITION_MM; 3]).is_ok()); assert_eq!( FixedVec3::new([MIN_POSITION_MM - 1; 3]), Err(SpatialError::OutOfBounds) ); assert_eq!( FixedVec3::from_cell_feet([i32::MAX, 0, 0]), Err(SpatialError::OutOfBounds) ); let max = FixedVec3::new([MAX_POSITION_MM; 3]).unwrap(); assert_eq!( max.checked_translate([1, 0, 0]), Err(SpatialError::OutOfBounds) ); assert_eq!( max.checked_translate([i64::MAX, 0, 0]), Err(SpatialError::Overflow) ); assert!(max .squared_distance(FixedVec3::new([MIN_POSITION_MM; 3]).unwrap()) .is_ok()); assert!(BodyExtents::PLAYER.bounds(max).is_err()); assert_eq!( BodyExtents::new(0, 300, 1800), Err(SpatialError::InvalidExtent) ); }
#[test] fn fixed_body_feet_bounds() { let (min, max) = BodyExtents::PLAYER .bounds(FixedVec3::new([500, 500, 0]).unwrap()) .unwrap(); assert_eq!(min.millimetres(), [200, 200, 0]); assert_eq!(max.millimetres(), [800, 800, 1800]); }}