use std::sync::atomic::{AtomicU64, Ordering};
use crate::engine::boundary::{BoundaryContext, BoundaryResource};
use crate::engine::entity::Entity;
use crate::engine::error::ECSResult;
use crate::engine::types::ChannelID;
use crate::engine::worker_stage::WorkerStage;
use super::geometry::GridGeometry;
use super::SpaceError;
pub struct GridSpace2D {
geometry: GridGeometry,
channels: [ChannelID; 1],
deterministic: bool,
stage: WorkerStage<(Entity, u32, u32)>,
scratch: Vec<(Entity, u32, u32)>,
counts: Vec<u32>,
cell_starts: Vec<u32>,
occupants: Vec<Entity>,
claims: GridClaims,
}
impl GridSpace2D {
pub fn new(geometry: GridGeometry, channel: ChannelID) -> Result<Self, SpaceError> {
geometry.validate()?;
let total_cells = geometry.total_cells();
Ok(Self {
geometry,
channels: [channel],
deterministic: true,
stage: WorkerStage::new(),
scratch: Vec::new(),
counts: vec![0; total_cells],
cell_starts: vec![0; total_cells + 1],
occupants: Vec::new(),
claims: GridClaims::new(geometry.cols(), geometry.rows()),
})
}
#[must_use]
pub fn with_determinism(mut self, deterministic: bool) -> Self {
self.deterministic = deterministic;
self
}
#[inline]
pub fn channel(&self) -> ChannelID {
self.channels[0]
}
#[inline]
pub fn geometry(&self) -> &GridGeometry {
&self.geometry
}
#[inline]
pub fn stage(&self, entity: Entity, col: u32, row: u32) {
self.stage.push((entity, col, row));
}
#[inline]
pub fn len(&self) -> usize {
self.occupants.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.occupants.is_empty()
}
pub fn occupants(&self, col: u32, row: u32) -> &[Entity] {
if col >= self.geometry.cols() || row >= self.geometry.rows() {
return &[];
}
let cell = self.geometry.cell_index(col, row) as usize;
let start = self.cell_starts[cell] as usize;
let end = self.cell_starts[cell + 1] as usize;
&self.occupants[start..end]
}
#[inline]
pub fn count(&self, col: u32, row: u32) -> usize {
self.occupants(col, row).len()
}
#[inline]
pub fn is_cell_empty(&self, col: u32, row: u32) -> bool {
self.count(col, row) == 0
}
pub fn moore_neighborhood(
&self,
col: u32,
row: u32,
radius: u32,
) -> impl Iterator<Item = (u32, u32)> + '_ {
let radius = radius as i64;
let centre = (col as i64, row as i64);
(-radius..=radius)
.flat_map(move |dr| (-radius..=radius).map(move |dc| (dc, dr)))
.filter(|&(dc, dr)| dc != 0 || dr != 0)
.filter_map(move |(dc, dr)| self.geometry.wrap_cell(centre.0 + dc, centre.1 + dr))
}
pub fn von_neumann_neighborhood(
&self,
col: u32,
row: u32,
radius: u32,
) -> impl Iterator<Item = (u32, u32)> + '_ {
let radius = radius as i64;
let centre = (col as i64, row as i64);
(-radius..=radius)
.flat_map(move |dr| (-radius..=radius).map(move |dc| (dc, dr)))
.filter(move |&(dc, dr)| (dc != 0 || dr != 0) && dc.abs() + dr.abs() <= radius)
.filter_map(move |(dc, dr)| self.geometry.wrap_cell(centre.0 + dc, centre.1 + dr))
}
#[inline]
pub fn claims(&self) -> &GridClaims {
&self.claims
}
fn rebuild(&mut self) {
self.scratch.clear();
self.stage.drain_into(&mut self.scratch);
let cols = self.geometry.cols();
let rows = self.geometry.rows();
for item in &mut self.scratch {
item.1 = item.1.min(cols - 1);
item.2 = item.2.min(rows - 1);
}
let total_cells = self.geometry.total_cells();
self.counts.clear();
self.counts.resize(total_cells, 0);
for &(_, col, row) in &self.scratch {
self.counts[self.geometry.cell_index(col, row) as usize] += 1;
}
self.cell_starts.clear();
self.cell_starts.resize(total_cells + 1, 0);
for cell in 0..total_cells {
self.cell_starts[cell + 1] = self.cell_starts[cell] + self.counts[cell];
}
self.occupants.clear();
self.occupants
.resize(self.scratch.len(), Entity::PLACEHOLDER);
let mut cursor: Vec<u32> = self.cell_starts[..total_cells].to_vec();
for &(entity, col, row) in &self.scratch {
let cell = self.geometry.cell_index(col, row) as usize;
let slot = cursor[cell] as usize;
self.occupants[slot] = entity;
cursor[cell] += 1;
}
if self.deterministic {
for cell in 0..total_cells {
let start = self.cell_starts[cell] as usize;
let end = self.cell_starts[cell + 1] as usize;
self.occupants[start..end].sort_unstable_by_key(|entity| entity.to_raw());
}
}
}
}
impl BoundaryResource for GridSpace2D {
fn name(&self) -> &str {
"GridSpace2D"
}
fn channels(&self) -> &[ChannelID] {
&self.channels
}
fn begin_tick(&mut self, _ctx: &mut BoundaryContext<'_>) -> ECSResult<()> {
self.stage.clear();
self.claims.clear();
Ok(())
}
fn finalise(
&mut self,
_ctx: &mut BoundaryContext<'_>,
channels: &[ChannelID],
) -> ECSResult<()> {
if channels.contains(&self.channels[0]) {
self.rebuild();
}
Ok(())
}
fn end_tick(&mut self, _ctx: &mut BoundaryContext<'_>) -> ECSResult<()> {
Ok(())
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
pub struct GridClaims {
cols: u32,
rows: u32,
cells: Vec<AtomicU64>,
}
impl GridClaims {
pub fn new(cols: u32, rows: u32) -> Self {
let total = cols as usize * rows as usize;
Self {
cols,
rows,
cells: (0..total)
.map(|_| AtomicU64::new(Entity::PLACEHOLDER.to_raw()))
.collect(),
}
}
#[inline]
fn index(&self, col: u32, row: u32) -> Option<usize> {
(col < self.cols && row < self.rows).then(|| (row * self.cols + col) as usize)
}
#[inline]
pub fn bid(&self, col: u32, row: u32, entity: Entity) {
if let Some(index) = self.index(col, row) {
self.cells[index].fetch_min(entity.to_raw(), Ordering::Relaxed);
}
}
#[inline]
pub fn winner(&self, col: u32, row: u32) -> Option<Entity> {
let index = self.index(col, row)?;
let raw = self.cells[index].load(Ordering::Relaxed);
(raw != Entity::PLACEHOLDER.to_raw()).then(|| Entity::from_raw(raw))
}
#[inline]
pub fn won(&self, col: u32, row: u32, entity: Entity) -> bool {
self.winner(col, row) == Some(entity)
}
pub fn clear(&self) {
let unclaimed = Entity::PLACEHOLDER.to_raw();
for cell in &self.cells {
cell.store(unclaimed, Ordering::Relaxed);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entity(raw: u64) -> Entity {
Entity::from_raw(raw)
}
fn grid() -> GridSpace2D {
GridSpace2D::new(GridGeometry::cells(10, 10, true), 3).unwrap()
}
#[test]
fn occupancy_snapshot_counts_and_sorts() {
let mut g = grid();
g.stage(entity(5), 2, 3);
g.stage(entity(1), 2, 3);
g.stage(entity(9), 7, 7);
g.rebuild();
assert_eq!(g.len(), 3);
assert_eq!(g.count(2, 3), 2);
assert!(g.is_cell_empty(0, 0));
let ids: Vec<u64> = g.occupants(2, 3).iter().map(|e| e.to_raw()).collect();
assert_eq!(ids, vec![1, 5], "occupants sorted by entity id");
}
#[test]
fn neighborhoods_respect_topology() {
let g = grid();
assert_eq!(g.moore_neighborhood(0, 0, 1).count(), 8);
assert_eq!(g.von_neumann_neighborhood(0, 0, 1).count(), 4);
let bounded = GridSpace2D::new(GridGeometry::cells(10, 10, false), 3).unwrap();
assert_eq!(bounded.moore_neighborhood(0, 0, 1).count(), 3);
assert_eq!(bounded.von_neumann_neighborhood(0, 0, 1).count(), 2);
}
#[test]
fn claims_lowest_entity_wins_regardless_of_bid_order() {
let claims = GridClaims::new(4, 4);
for &raw in &[900u64, 3, 512, 44] {
claims.bid(1, 1, entity(raw));
}
assert_eq!(claims.winner(1, 1), Some(entity(3)));
assert!(claims.won(1, 1, entity(3)));
assert!(!claims.won(1, 1, entity(44)));
assert_eq!(claims.winner(2, 2), None);
claims.clear();
assert_eq!(claims.winner(1, 1), None);
}
#[test]
fn parallel_bids_are_deterministic() {
let claims = GridClaims::new(8, 8);
rayon::scope(|s| {
for raw in 1..=512u64 {
let claims = &claims;
s.spawn(move |_| {
claims.bid((raw % 8) as u32, ((raw / 8) % 8) as u32, entity(raw));
});
}
});
for row in 0..8u32 {
for col in 0..8u32 {
let expected = (1..=512u64)
.filter(|raw| (raw % 8) as u32 == col && ((raw / 8) % 8) as u32 == row)
.min()
.map(entity);
assert_eq!(claims.winner(col, row), expected);
}
}
}
}