cd3b41a401
- 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>
143 lines
4.7 KiB
Rust
143 lines
4.7 KiB
Rust
//! El ciclo del motor — un `tick` completo de la simulación.
|
|
//!
|
|
//! Orden fijo: difusión/entropía → evaluación de transiciones → acciones
|
|
//! de los agentes → envejecimiento y cosecha de muertos.
|
|
|
|
use crate::diffuse::diffuse;
|
|
use dominium_core::{SimParams, World};
|
|
|
|
/// Evaluación de transiciones: un agente exhausto se fuerza a `Pelear`.
|
|
fn apply_transitions(world: &mut World, p: &SimParams) {
|
|
for i in 0..world.lemmings.len() {
|
|
if world.lemmings.energia[i] < p.desperation_threshold {
|
|
world.lemmings.accion[i] = 5; // Degradar (Pelear)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Envejece a los agentes y cosecha a los muertos: la energía remanente
|
|
/// de un agente que muere se inyecta como fertilidad (`materia`) en su
|
|
/// celda. Devuelve cuántos murieron.
|
|
fn age_and_reap(world: &mut World, p: &SimParams) -> usize {
|
|
for e in world.lemmings.edad.iter_mut() {
|
|
*e += 1;
|
|
}
|
|
// Recolecta los índices muertos (energía agotada o edad excedida).
|
|
let mut dead: Vec<usize> = (0..world.lemmings.len())
|
|
.filter(|&i| {
|
|
world.lemmings.energia[i] <= 0.0 || world.lemmings.edad[i] > p.max_edad
|
|
})
|
|
.collect();
|
|
// Remueve de mayor a menor índice: swap_remove no invalida los menores.
|
|
dead.sort_unstable_by(|a, b| b.cmp(a));
|
|
let count = dead.len();
|
|
for i in dead {
|
|
let (cx, cy) = world
|
|
.grid
|
|
.clamp_cell(world.lemmings.pos_x[i], world.lemmings.pos_y[i]);
|
|
let idx = world.grid.idx(cx, cy);
|
|
// La energía remanente vuelve a la tierra como biomasa.
|
|
let remnant = world.lemmings.energia[i].max(0.0);
|
|
world.grid.materia[idx] += remnant;
|
|
world.lemmings.remove(i);
|
|
}
|
|
count
|
|
}
|
|
|
|
/// Un paso completo de la simulación.
|
|
pub fn tick(world: &mut World, p: &SimParams) {
|
|
// 1. Difusión y entropía sobre los campos.
|
|
diffuse(&mut world.grid, p);
|
|
// 2. Transiciones de estado forzadas.
|
|
apply_transitions(world, p);
|
|
// 3. Acciones de los agentes. Se fija `n` antes del loop: los hijos
|
|
// que `Replicar` agrega al final NO actúan este tick.
|
|
let n = world.lemmings.len();
|
|
for i in 0..n {
|
|
if i < world.lemmings.len() {
|
|
world.step_lemming(i, p);
|
|
}
|
|
}
|
|
// 4. Envejecer + cosechar muertos.
|
|
age_and_reap(world, p);
|
|
}
|
|
|
|
/// Corre `steps` ticks seguidos.
|
|
pub fn run(world: &mut World, p: &SimParams, steps: usize) {
|
|
for _ in 0..steps {
|
|
tick(world, p);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn exhausted_agent_is_forced_to_fight() {
|
|
let mut w = World::new(8, 8);
|
|
w.lemmings.spawn(4.0, 4.0, 1.0, [0.0; 4]); // energía 1 < umbral 5
|
|
w.lemmings.accion[0] = 0; // Mover
|
|
let p = SimParams::default();
|
|
apply_transitions(&mut w, &p);
|
|
assert_eq!(w.lemmings.accion[0], 5); // forzado a Degradar
|
|
}
|
|
|
|
#[test]
|
|
fn dead_agent_returns_energy_as_materia() {
|
|
let mut w = World::new(8, 8);
|
|
// Agente sin energía → muere este tick.
|
|
w.lemmings.spawn(4.0, 4.0, 0.0, [0.0; 4]);
|
|
let idx = w.grid.idx(4, 4);
|
|
let p = SimParams::default();
|
|
let reaped = age_and_reap(&mut w, &p);
|
|
assert_eq!(reaped, 1);
|
|
assert_eq!(w.lemmings.len(), 0);
|
|
// (energía remanente 0 → materia no sube, pero no panickea)
|
|
assert!(w.grid.materia[idx] >= 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn tick_runs_without_panicking_on_a_populated_world() {
|
|
let mut w = World::new(32, 32);
|
|
for k in 0..20 {
|
|
let x = (k % 8) as f32 + 2.0;
|
|
let y = (k / 8) as f32 + 2.0;
|
|
w.lemmings.spawn(x, y, 30.0, [1.0, 0.2, 0.5, 0.1]);
|
|
w.lemmings.accion[k] = (k % 6) as u8;
|
|
}
|
|
// Sembrar algo de materia.
|
|
for c in w.grid.materia.iter_mut() {
|
|
*c = 5.0;
|
|
}
|
|
let p = SimParams::default();
|
|
run(&mut w, &p, 50);
|
|
// La sim avanzó 50 ticks sin romperse.
|
|
assert!(w.lemmings.edad.iter().all(|&e| e <= 50));
|
|
}
|
|
|
|
#[test]
|
|
fn run_is_deterministic() {
|
|
let build = || {
|
|
let mut w = World::new(16, 16);
|
|
for k in 0..10 {
|
|
w.lemmings.spawn(3.0 + k as f32, 8.0, 40.0, [1.0, 0.0, 0.3, 0.0]);
|
|
w.lemmings.accion[k] = (k % 6) as u8;
|
|
}
|
|
for c in w.grid.materia.iter_mut() {
|
|
*c = 3.0;
|
|
}
|
|
w
|
|
};
|
|
let p = SimParams::default();
|
|
let mut a = build();
|
|
let mut b = build();
|
|
run(&mut a, &p, 30);
|
|
run(&mut b, &p, 30);
|
|
// Mismo input → mismo estado, bit a bit.
|
|
assert_eq!(a.lemmings.pos_x, b.lemmings.pos_x);
|
|
assert_eq!(a.lemmings.energia, b.lemmings.energia);
|
|
assert_eq!(a.grid.materia, b.grid.materia);
|
|
}
|
|
}
|