use std::collections::HashMap;
use voxel_light::{propagate, remove, LightEngine, LightUpdate, VoxelAccess, VoxelInfo};
struct MockWorld {
blocks: HashMap<(i32, i32, i32), VoxelInfo>,
}
impl MockWorld {
fn new() -> Self {
Self {
blocks: HashMap::new(),
}
}
fn air_world(radius: i32) -> Self {
let mut world = Self::new();
for x in -radius..=radius {
for y in -radius..=radius {
for z in -radius..=radius {
world.set(x, y, z, VoxelInfo {
transparent: true,
block_light: 0,
emission: 0,
});
}
}
}
world
}
fn set(&mut self, x: i32, y: i32, z: i32, info: VoxelInfo) {
self.blocks.insert((x, y, z), info);
}
fn set_solid(&mut self, x: i32, y: i32, z: i32) {
self.set(x, y, z, VoxelInfo {
transparent: false,
block_light: 0,
emission: 0,
});
}
fn apply_updates(&mut self, updates: &[LightUpdate]) {
for u in updates {
if let Some(voxel) = self.blocks.get_mut(&(u.x, u.y, u.z)) {
voxel.block_light = u.level;
}
}
}
fn get_light(&self, x: i32, y: i32, z: i32) -> u8 {
self.blocks
.get(&(x, y, z))
.map(|v| v.block_light)
.unwrap_or(0)
}
}
impl VoxelAccess for MockWorld {
fn get_voxel(&self, x: i32, y: i32, z: i32) -> Option<VoxelInfo> {
self.blocks.get(&(x, y, z)).copied()
}
}
#[test]
fn propagate_in_empty_world() {
let world = MockWorld::air_world(16);
let updates = propagate(&world, [0, 0, 0], 14);
let source_update = updates.iter().find(|u| u.pos() == [0, 0, 0]).unwrap();
assert_eq!(source_update.level, 14);
let neighbor = updates.iter().find(|u| u.pos() == [1, 0, 0]).unwrap();
assert_eq!(neighbor.level, 13);
let far = updates.iter().find(|u| u.pos() == [13, 0, 0]).unwrap();
assert_eq!(far.level, 1);
assert!(updates.iter().find(|u| u.pos() == [14, 0, 0]).is_none());
}
#[test]
fn propagate_blocked_by_opaque() {
let mut world = MockWorld::air_world(16);
for y in -15..=15 {
for z in -15..=15 {
world.set_solid(2, y, z);
}
}
let updates = propagate(&world, [0, 0, 0], 14);
let before_wall = updates.iter().find(|u| u.pos() == [1, 0, 0]).unwrap();
assert_eq!(before_wall.level, 13);
assert!(updates.iter().find(|u| u.x == 2 && u.y == 0 && u.z == 0).is_none());
assert!(updates.iter().find(|u| u.pos() == [3, 0, 0]).is_none());
}
#[test]
fn propagate_stops_at_world_boundary() {
let mut world = MockWorld::new();
world.set(0, 0, 0, VoxelInfo { transparent: true, block_light: 0, emission: 14 });
world.set(1, 0, 0, VoxelInfo { transparent: true, block_light: 0, emission: 0 });
world.set(2, 0, 0, VoxelInfo { transparent: true, block_light: 0, emission: 0 });
let updates = propagate(&world, [0, 0, 0], 14);
assert!(updates.iter().any(|u| u.pos() == [0, 0, 0] && u.level == 14));
assert!(updates.iter().any(|u| u.pos() == [1, 0, 0] && u.level == 13));
assert!(updates.iter().any(|u| u.pos() == [2, 0, 0] && u.level == 12));
assert!(updates.iter().all(|u| u.pos() != [3, 0, 0]));
}
#[test]
fn propagate_does_not_overwrite_brighter() {
let mut world = MockWorld::air_world(16);
world.set(3, 0, 0, VoxelInfo { transparent: true, block_light: 14, emission: 0 });
let updates = propagate(&world, [0, 0, 0], 14);
let at_3 = updates.iter().find(|u| u.pos() == [3, 0, 0]);
assert!(at_3.is_none());
}
#[test]
fn propagate_level_zero_returns_empty() {
let world = MockWorld::air_world(5);
let updates = propagate(&world, [0, 0, 0], 0);
assert!(updates.is_empty());
}
#[test]
fn propagate_diamond_shape() {
let world = MockWorld::air_world(5);
let updates = propagate(&world, [0, 0, 0], 3);
for u in &updates {
let dist = u.x.unsigned_abs() + u.y.unsigned_abs() + u.z.unsigned_abs();
assert!(dist <= 2, "update at distance {dist} with level {}", u.level);
assert_eq!(u.level, 3 - dist as u8);
}
}
#[test]
fn remove_single_source() {
let mut world = MockWorld::air_world(16);
let updates = propagate(&world, [0, 0, 0], 14);
world.apply_updates(&updates);
let removal_updates = remove(&world, [0, 0, 0]);
assert!(!removal_updates.is_empty());
world.apply_updates(&removal_updates);
assert_eq!(world.get_light(0, 0, 0), 0);
assert_eq!(world.get_light(1, 0, 0), 0);
assert_eq!(world.get_light(5, 0, 0), 0);
assert_eq!(world.get_light(13, 0, 0), 0);
}
#[test]
fn remove_with_surviving_source() {
let mut world = MockWorld::air_world(20);
let updates1 = propagate(&world, [0, 0, 0], 14);
world.apply_updates(&updates1);
let updates2 = propagate(&world, [10, 0, 0], 14);
world.apply_updates(&updates2);
world.set(0, 0, 0, VoxelInfo { transparent: true, block_light: 14, emission: 14 });
world.set(10, 0, 0, VoxelInfo { transparent: true, block_light: 14, emission: 14 });
let removal_updates = remove(&world, [0, 0, 0]);
world.apply_updates(&removal_updates);
assert_eq!(world.get_light(10, 0, 0), 14);
assert_eq!(world.get_light(9, 0, 0), 13);
assert_eq!(world.get_light(11, 0, 0), 13);
assert_eq!(world.get_light(0, 0, 0), 4);
}
#[test]
fn remove_adjacent_to_another_emitter() {
let mut world = MockWorld::air_world(16);
world.set(0, 0, 0, VoxelInfo { transparent: true, block_light: 0, emission: 14 });
world.set(1, 0, 0, VoxelInfo { transparent: true, block_light: 0, emission: 14 });
let u1 = propagate(&world, [0, 0, 0], 14);
world.apply_updates(&u1);
let u2 = propagate(&world, [1, 0, 0], 14);
world.apply_updates(&u2);
let removal_updates = remove(&world, [0, 0, 0]);
world.apply_updates(&removal_updates);
assert_eq!(world.get_light(1, 0, 0), 14);
assert_eq!(world.get_light(0, 0, 0), 13);
assert_eq!(world.get_light(-1, 0, 0), 12);
}
#[test]
fn engine_place_and_remove() {
let mut world = MockWorld::air_world(16);
let mut engine = LightEngine::new();
let updates = engine.place_light(&world, [0, 0, 0], 14);
world.apply_updates(&updates);
assert_eq!(world.get_light(0, 0, 0), 14);
assert_eq!(world.get_light(5, 0, 0), 9);
world.set(0, 0, 0, VoxelInfo { transparent: true, block_light: 14, emission: 14 });
let removal = engine.remove_light(&world, [0, 0, 0]);
world.apply_updates(&removal);
assert_eq!(world.get_light(0, 0, 0), 0);
assert_eq!(world.get_light(5, 0, 0), 0);
}
#[test]
fn engine_block_removed_fills_gap() {
let mut world = MockWorld::air_world(16);
let mut engine = LightEngine::new();
let updates = engine.place_light(&world, [0, 0, 0], 14);
world.apply_updates(&updates);
world.set_solid(3, 0, 0);
world.set(3, 0, 0, VoxelInfo { transparent: true, block_light: 0, emission: 0 });
let fill_updates = engine.block_removed(&world, [3, 0, 0]);
world.apply_updates(&fill_updates);
assert_eq!(world.get_light(3, 0, 0), 11);
}
struct ChunkedWorld {
chunks: HashMap<(i32, i32, i32), HashMap<(usize, usize, usize), VoxelInfo>>,
}
impl ChunkedWorld {
fn new() -> Self {
Self { chunks: HashMap::new() }
}
fn fill_chunk(&mut self, cx: i32, cy: i32, cz: i32) {
let mut local = HashMap::new();
for x in 0..16 {
for y in 0..16 {
for z in 0..16 {
local.insert((x, y, z), VoxelInfo {
transparent: true,
block_light: 0,
emission: 0,
});
}
}
}
self.chunks.insert((cx, cy, cz), local);
}
fn set_world(&mut self, wx: i32, wy: i32, wz: i32, info: VoxelInfo) {
let cx = wx.div_euclid(16);
let cy = wy.div_euclid(16);
let cz = wz.div_euclid(16);
let lx = wx.rem_euclid(16) as usize;
let ly = wy.rem_euclid(16) as usize;
let lz = wz.rem_euclid(16) as usize;
if let Some(chunk) = self.chunks.get_mut(&(cx, cy, cz)) {
chunk.insert((lx, ly, lz), info);
}
}
fn apply_updates(&mut self, updates: &[LightUpdate]) {
for u in updates {
let cx = u.x.div_euclid(16);
let cy = u.y.div_euclid(16);
let cz = u.z.div_euclid(16);
let lx = u.x.rem_euclid(16) as usize;
let ly = u.y.rem_euclid(16) as usize;
let lz = u.z.rem_euclid(16) as usize;
if let Some(chunk) = self.chunks.get_mut(&(cx, cy, cz)) {
if let Some(voxel) = chunk.get_mut(&(lx, ly, lz)) {
voxel.block_light = u.level;
}
}
}
}
fn get_light(&self, wx: i32, wy: i32, wz: i32) -> u8 {
let cx = wx.div_euclid(16);
let cy = wy.div_euclid(16);
let cz = wz.div_euclid(16);
let lx = wx.rem_euclid(16) as usize;
let ly = wy.rem_euclid(16) as usize;
let lz = wz.rem_euclid(16) as usize;
self.chunks
.get(&(cx, cy, cz))
.and_then(|c| c.get(&(lx, ly, lz)))
.map(|v| v.block_light)
.unwrap_or(0)
}
}
impl VoxelAccess for ChunkedWorld {
fn get_voxel(&self, x: i32, y: i32, z: i32) -> Option<VoxelInfo> {
let cx = x.div_euclid(16);
let cy = y.div_euclid(16);
let cz = z.div_euclid(16);
let lx = x.rem_euclid(16) as usize;
let ly = y.rem_euclid(16) as usize;
let lz = z.rem_euclid(16) as usize;
self.chunks.get(&(cx, cy, cz))?.get(&(lx, ly, lz)).copied()
}
}
#[test]
fn cross_chunk_propagation() {
let mut world = ChunkedWorld::new();
world.fill_chunk(0, 0, 0);
world.fill_chunk(1, 0, 0);
let updates = propagate(&world, [15, 8, 8], 14);
world.apply_updates(&updates);
assert_eq!(world.get_light(16, 8, 8), 13);
assert_eq!(world.get_light(17, 8, 8), 12);
}
#[test]
fn cross_chunk_negative_boundary() {
let mut world = ChunkedWorld::new();
world.fill_chunk(0, 0, 0);
world.fill_chunk(-1, 0, 0);
let updates = propagate(&world, [0, 8, 8], 14);
world.apply_updates(&updates);
assert_eq!(world.get_light(-1, 8, 8), 13);
assert_eq!(world.get_light(-2, 8, 8), 12);
}
#[test]
fn missing_neighbor_chunk_stops_propagation() {
let mut world = ChunkedWorld::new();
world.fill_chunk(0, 0, 0);
let updates = propagate(&world, [15, 8, 8], 14);
world.apply_updates(&updates);
assert_eq!(world.get_light(15, 8, 8), 14);
assert!(updates.iter().all(|u| u.x < 16));
}
#[test]
fn cross_chunk_removal() {
let mut world = ChunkedWorld::new();
world.fill_chunk(0, 0, 0);
world.fill_chunk(1, 0, 0);
let updates = propagate(&world, [15, 8, 8], 14);
world.apply_updates(&updates);
world.set_world(15, 8, 8, VoxelInfo { transparent: true, block_light: 14, emission: 14 });
let removal = remove(&world, [15, 8, 8]);
world.apply_updates(&removal);
assert_eq!(world.get_light(15, 8, 8), 0);
assert_eq!(world.get_light(16, 8, 8), 0);
assert_eq!(world.get_light(14, 8, 8), 0);
}