neurodoom 0.6.7

Deterministic no_std Doom engine with semantic and depth perception buffers for AI
Documentation
use neurodoom::engine::ClassicEngine;
use neurodoom::math::*;
use neurodoom::render::SemanticClass;
use neurodoom::rules::PlayerAction;
use neurodoom::world::PeerId;

fn load_wad() -> Vec<u8> {
    let path = std::env::var("DOOM_WAD")
        .unwrap_or_else(|_| "doom1.wad".to_string());
    std::fs::read(&path).unwrap_or_else(|e| panic!("failed to read WAD at {path}: {e}"))
}

#[test]
fn engine_initializes() {
    let wad_data = load_wad();
    let engine = ClassicEngine::new(&wad_data, "E1M1").unwrap();
    assert_eq!(engine.buffer_size(), (SCREENWIDTH, SCREENHEIGHT));
}

#[test]
fn tick_empty_input() {
    let wad_data = load_wad();
    let mut engine = ClassicEngine::new(&wad_data, "E1M1").unwrap();
    engine.tick_single(PeerId(0), PlayerAction::default());
    let nonzero = engine.framebuffer().iter().filter(|&&b| b != 0).count();
    assert!(nonzero > 1000, "framebuffer mostly empty after tick");
}

#[test]
fn tick_350_no_panic() {
    let wad_data = load_wad();
    let mut engine = ClassicEngine::new(&wad_data, "E1M1").unwrap();
    for _ in 0..350 {
        engine.tick_single(PeerId(0), PlayerAction::default());
    }
    assert_eq!(engine.world().tick, 350);
}

#[test]
fn forward_input_moves_player() {
    let wad_data = load_wad();
    let mut engine = ClassicEngine::new(&wad_data, "E1M1").unwrap();

    let initial_pos = engine.world().iter()
        .find(|e| engine.world().is_controlled(e.id))
        .map(|e| (e.x, e.y))
        .unwrap();

    for _ in 0..35 {
        engine.tick_single(PeerId(0), PlayerAction {
            forward_move: 25,
            ..PlayerAction::default()
        });
    }

    let final_pos = engine.world().iter()
        .find(|e| engine.world().is_controlled(e.id))
        .map(|e| (e.x, e.y))
        .unwrap();

    let dx = (final_pos.0 - initial_pos.0).abs();
    let dy = (final_pos.1 - initial_pos.1).abs();
    assert!(dx > FRACUNIT || dy > FRACUNIT, "player should have moved: dx={dx} dy={dy}");
}

#[test]
fn turn_changes_angle() {
    let wad_data = load_wad();
    let mut engine = ClassicEngine::new(&wad_data, "E1M1").unwrap();

    let initial_angle = engine.world().iter()
        .find(|e| engine.world().is_controlled(e.id))
        .map(|e| e.angle)
        .unwrap();

    engine.tick_single(PeerId(0), PlayerAction {
        angle_turn: 512,
        ..PlayerAction::default()
    });

    let new_angle = engine.world().iter()
        .find(|e| engine.world().is_controlled(e.id))
        .map(|e| e.angle)
        .unwrap();

    assert_ne!(initial_angle, new_angle, "angle should change on turn input");
}

#[test]
fn semantic_buffer_has_content() {
    let wad_data = load_wad();
    let mut engine = ClassicEngine::new(&wad_data, "E1M1").unwrap();
    engine.tick_single(PeerId(0), PlayerAction::default());

    let semantic = engine.semantic_buffer();
    assert!(semantic.iter().any(|&c| c == SemanticClass::Wall as u8), "no Wall pixels");
    assert!(semantic.iter().any(|&c| c == SemanticClass::Floor as u8), "no Floor pixels");
    assert!(semantic.iter().any(|&c| c == SemanticClass::Ceiling as u8), "no Ceiling pixels");
}

#[test]
fn depth_buffer_has_content() {
    let wad_data = load_wad();
    let mut engine = ClassicEngine::new(&wad_data, "E1M1").unwrap();
    engine.tick_single(PeerId(0), PlayerAction::default());
    assert!(engine.depth_buffer().iter().any(|&d| d != 0), "depth buffer all zeros");
}