feat(dominium): dominium-physics — ciclo del motor (difusión + tick)
- diffuse — ecuación de fluidos discreta sobre los 3 campos dinámicos (materia/psique/poder): cada celda intercambia con sus 4 vecinas + entropía. Buffer de lectura separado (lee estado viejo). oro y degradacion no difunden. - tick — un paso completo: difusión → transiciones (agente exhausto se fuerza a Pelear) → acciones de los agentes → envejecimiento + cosecha (la energía del muerto vuelve como materia/fertilidad). run() corre N. Determinista bit-exacto: aritmética f32 en orden fijo, sin HashMap ni reducciones paralelas. Test `run_is_deterministic` verifica que mismo input → mismo estado bit a bit. 7 tests verdes. cargo check --workspace verde. dominium ya CORRE (core + physics = simulación funcional). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
//! Difusión y entropía de los campos de la grilla.
|
||||
//!
|
||||
//! Ecuación de fluidos discreta: cada celda intercambia una fracción de
|
||||
//! su valor con sus 4 vecinas, y luego pierde una fracción al ambiente
|
||||
//! (entropía). Difunden los 3 campos dinámicos — materia, psique,
|
||||
//! poder. `oro` (materia sólida) y `degradacion` (cicatriz permanente)
|
||||
//! no difunden.
|
||||
|
||||
use dominium_core::{Grid, SimParams};
|
||||
|
||||
/// Difunde una sola capa: `new[c] = c + rate·(media_vecinos − c)`, y luego
|
||||
/// aplica la entropía. Usa un buffer de lectura separado (la difusión
|
||||
/// debe leer el estado viejo).
|
||||
fn diffuse_layer(layer: &mut [f32], width: usize, height: usize, rate: f32, entropy: f32) {
|
||||
let old = layer.to_vec();
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
let c = y * width + x;
|
||||
let mut sum = 0.0f32;
|
||||
let mut count = 0.0f32;
|
||||
// 4-vecindad (von Neumann), bordes sin wrap.
|
||||
if x > 0 {
|
||||
sum += old[c - 1];
|
||||
count += 1.0;
|
||||
}
|
||||
if x + 1 < width {
|
||||
sum += old[c + 1];
|
||||
count += 1.0;
|
||||
}
|
||||
if y > 0 {
|
||||
sum += old[c - width];
|
||||
count += 1.0;
|
||||
}
|
||||
if y + 1 < height {
|
||||
sum += old[c + width];
|
||||
count += 1.0;
|
||||
}
|
||||
let neighbor_avg = if count > 0.0 { sum / count } else { old[c] };
|
||||
let diffused = old[c] + rate * (neighbor_avg - old[c]);
|
||||
layer[c] = diffused * (1.0 - entropy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Aplica un paso de difusión + entropía a los 3 campos dinámicos.
|
||||
pub fn diffuse(grid: &mut Grid, p: &SimParams) {
|
||||
let (w, h) = (grid.width, grid.height);
|
||||
let (rate, ent) = (p.diffusion_rate, p.entropy_rate);
|
||||
diffuse_layer(&mut grid.materia, w, h, rate, ent);
|
||||
diffuse_layer(&mut grid.psique, w, h, rate, ent);
|
||||
diffuse_layer(&mut grid.poder, w, h, rate, ent);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn diffusion_spreads_a_spike_to_neighbors() {
|
||||
let mut g = Grid::new(5, 5);
|
||||
let center = g.idx(2, 2);
|
||||
g.materia[center] = 100.0;
|
||||
let p = SimParams::default();
|
||||
diffuse(&mut g, &p);
|
||||
// El pico bajó; las vecinas subieron desde 0.
|
||||
assert!(g.materia[center] < 100.0);
|
||||
assert!(g.materia[g.idx(1, 2)] > 0.0);
|
||||
assert!(g.materia[g.idx(3, 2)] > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entropy_decays_a_uniform_field() {
|
||||
let mut g = Grid::new(4, 4);
|
||||
for v in g.psique.iter_mut() {
|
||||
*v = 10.0;
|
||||
}
|
||||
let p = SimParams::default();
|
||||
diffuse(&mut g, &p);
|
||||
// Campo uniforme: la difusión no cambia nada, pero la entropía sí.
|
||||
for &v in &g.psique {
|
||||
assert!(v < 10.0 && v > 9.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diffusion_conserves_mass_minus_entropy() {
|
||||
let mut g = Grid::new(6, 6);
|
||||
let c = g.idx(3, 3);
|
||||
g.materia[c] = 60.0;
|
||||
let total_before: f32 = g.materia.iter().sum();
|
||||
let mut p = SimParams::default();
|
||||
p.entropy_rate = 0.0; // sin pérdida → masa conservada
|
||||
diffuse(&mut g, &p);
|
||||
let total_after: f32 = g.materia.iter().sum();
|
||||
assert!((total_before - total_after).abs() < 1e-2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user