neurodoom 0.6.7

Deterministic no_std Doom engine with semantic and depth perception buffers for AI
Documentation
//! Fixed-point arithmetic, Binary Angle Measurement (BAM), and the
//! screen-size constants used across the engine.
//!
//! Matches the conventions of the original Doom C source:
//! `Fixed = int32_t` (16.16 fixed), `Angle = uint32_t` (full circle =
//! `u32::MAX + 1`).

/// 16.16 fixed-point number.
pub type Fixed = i32;

/// Binary Angle Measurement. Full circle = `u32::MAX + 1`.
pub type Angle = u32;

pub const FRACBITS: i32 = 16;
pub const FRACUNIT: Fixed = 1 << FRACBITS;

pub const FINEANGLES: usize = 8192;
pub const FINEMASK: usize = FINEANGLES - 1;
pub const ANGLETOFINESHIFT: u32 = 19;

pub const ANG45: Angle = 0x20000000;
pub const ANG90: Angle = 0x40000000;
pub const ANG180: Angle = 0x80000000;
pub const ANG270: Angle = 0xc0000000;

pub const SCREENWIDTH: usize = 320;
pub const SCREENHEIGHT: usize = 200;
pub const TICRATE: u32 = 35;
pub const MAXPLAYERS: usize = 4;

pub const MELEERANGE: Fixed = 64 * FRACUNIT;
pub const MISSILERANGE: Fixed = 2048 * FRACUNIT;
pub const USERANGE: Fixed = 64 * FRACUNIT;

/// Multiply two 16.16 fixed-point values.
#[inline]
pub fn fixed_mul(a: Fixed, b: Fixed) -> Fixed {
    ((a as i64 * b as i64) >> FRACBITS) as Fixed
}

/// Divide two 16.16 fixed-point values.
/// Returns `i32::MAX` or `i32::MIN` on overflow.
#[inline]
pub fn fixed_div(a: Fixed, b: Fixed) -> Fixed {
    if b == 0 || (a.unsigned_abs() >> 14) >= b.unsigned_abs() {
        return if (a ^ b) < 0 { i32::MIN } else { i32::MAX };
    }
    (((a as i64) << FRACBITS) / b as i64) as Fixed
}

/// Index into `FINESINE` to get sine. The mask keeps the index in
/// `0..FINEANGLES` (`< 8192`), and `FINESINE` has 10240 entries, so the
/// index is always in-bounds and the compiler elides the bounds check.
#[inline(always)]
#[allow(clippy::indexing_slicing)]
pub fn finesine(index: usize) -> Fixed {
    crate::tables::FINESINE[index & FINEMASK]
}

/// Index into `FINESINE` to get cosine (offset by 90°). The mask keeps
/// the index in `0..FINEANGLES`, so `(idx & FINEMASK) + FINEANGLES/4` is
/// always `< 8192 + 2048 = 10240` — in-bounds of the 10240-entry table.
#[inline(always)]
#[allow(clippy::indexing_slicing)]
pub fn finecosine(index: usize) -> Fixed {
    crate::tables::FINESINE[(index & FINEMASK) + FINEANGLES / 4]
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn fixed_mul_identity() {
        assert_eq!(fixed_mul(FRACUNIT, FRACUNIT), FRACUNIT);
    }

    #[test]
    fn fixed_mul_basic() {
        assert_eq!(fixed_mul(2 * FRACUNIT, 3 * FRACUNIT), 6 * FRACUNIT);
    }

    #[test]
    fn fixed_mul_negative() {
        assert_eq!(fixed_mul(-2 * FRACUNIT, 3 * FRACUNIT), -6 * FRACUNIT);
    }

    #[test]
    fn fixed_div_basic() {
        assert_eq!(fixed_div(6 * FRACUNIT, 3 * FRACUNIT), 2 * FRACUNIT);
    }

    #[test]
    fn fixed_div_overflow() {
        let result = fixed_div(i32::MAX, 1);
        assert_eq!(result, i32::MAX);
    }

    #[test]
    fn fixed_div_by_zero() {
        assert_eq!(fixed_div(FRACUNIT, 0), i32::MAX);
    }

    #[test]
    fn trig_sine_zero() {
        // Doom's table has a small positive value at index 0, not exact zero
        assert_eq!(crate::tables::FINESINE[0], 25);
    }

    #[test]
    fn trig_sine_90() {
        // sin(90°) ≈ 1.0 — Doom uses 65535, not 65536 (FRACUNIT)
        assert_eq!(crate::tables::FINESINE[FINEANGLES / 4], 65535);
    }

    #[test]
    fn trig_cosine_zero() {
        // cos(0°) ≈ 1.0 — same as sin(90°)
        assert_eq!(finecosine(0), 65535);
    }

    #[test]
    fn trig_sine_180() {
        // sin(180°) ≈ 0 — small negative in Doom's table
        assert_eq!(crate::tables::FINESINE[FINEANGLES / 2], -25);
    }
}