gioser

tawasuyu/brahman

Rust 521TOML 202Markdown 57JSON 197505 Definiciones

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>

cd3b41a4018e3e4951ffe064e1f7e03cac4b2a77 · por sergio · (hace 5 meses) · padre d1727b137 · ver el árbol en este commit

Delta semántico · 19

crates/modules/dominium/dominium-core/src/params.rs

crates/modules/dominium/dominium-physics/src/diffuse.rs

crates/modules/dominium/dominium-physics/src/lib.rs

crates/modules/dominium/dominium-physics/src/tick.rs

8 archivos · +358 −0

diff --git a/Cargo.lock b/Cargo.lock
index 3ffdf09..23fd070 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3482,6 +3482,13 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "dominium-physics"
+version = "0.1.0"
+dependencies = [
+ "dominium-core",
+]
+
 [[package]]
 name = "double-ended-peekable"
 version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index 4097ab9..3f83bd0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -145,6 +145,7 @@ members = [
     # modules/dominium/ — Simulador psicológico de campo medio
     # ============================================================
     "crates/modules/dominium/dominium-core",
+    "crates/modules/dominium/dominium-physics",
 
     # ============================================================
     # modules/gioser/ — Landing WASM (chacana + 4 elementos)
diff --git a/crates/modules/dominium/dominium-core/src/params.rs b/crates/modules/dominium/dominium-core/src/params.rs
index c8c0e4b..a7f3b4b 100644
--- a/crates/modules/dominium/dominium-core/src/params.rs
+++ b/crates/modules/dominium/dominium-core/src/params.rs
@@ -32,6 +32,10 @@ pub struct SimParams {
     pub desperation_threshold: f32,
     /// Edad máxima; al superarla el agente muere.
     pub max_edad: u32,
+    /// Fracción que cada celda difunde hacia sus 4 vecinas por tick (0-1).
+    pub diffusion_rate: f32,
+    /// Tasa de pérdida natural (entropía) de los campos por tick (0-1).
+    pub entropy_rate: f32,
 }
 
 impl Default for SimParams {
@@ -49,6 +53,8 @@ impl Default for SimParams {
             absorb_frac: 0.50,
             desperation_threshold: 5.0,
             max_edad: 1000,
+            diffusion_rate: 0.10,
+            entropy_rate: 0.01,
         }
     }
 }
diff --git a/crates/modules/dominium/dominium-physics/Cargo.toml b/crates/modules/dominium/dominium-physics/Cargo.toml
new file mode 100644
index 0000000..1b24654
--- /dev/null
+++ b/crates/modules/dominium/dominium-physics/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "dominium-physics"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+authors.workspace = true
+publish.workspace = true
+description = "dominium — ciclo del motor: difusión + entropía de los campos + tick completo (transiciones, acciones, envejecimiento). Determinista bit-exacto."
+
+[dependencies]
+dominium-core = { path = "../dominium-core" }
diff --git a/crates/modules/dominium/dominium-physics/src/diffuse.rs b/crates/modules/dominium/dominium-physics/src/diffuse.rs
new file mode 100644
index 0000000..a2c3c1a
--- /dev/null
+++ b/crates/modules/dominium/dominium-physics/src/diffuse.rs
@@ -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);
+    }
+}
diff --git a/crates/modules/dominium/dominium-physics/src/lib.rs b/crates/modules/dominium/dominium-physics/src/lib.rs
new file mode 100644
index 0000000..8510470
--- /dev/null
+++ b/crates/modules/dominium/dominium-physics/src/lib.rs
@@ -0,0 +1,17 @@
+//! `dominium-physics` — el ciclo del motor de simulación.
+//!
+//! - [`diffuse`] — difusión + entropía de los campos de la grilla.
+//! - [`tick`] — un paso completo: difusión → transiciones → acciones →
+//!   envejecimiento/cosecha. [`tick::run`] corre N pasos.
+//!
+//! Determinista bit-exacto: sólo aritmética f32 en orden fijo, sin
+//! HashMap iteration ni reducciones paralelas. Mismo seed → mismo estado
+//! en cualquier plataforma.
+
+#![forbid(unsafe_code)]
+
+pub mod diffuse;
+pub mod tick;
+
+pub use diffuse::diffuse;
+pub use tick::{run, tick};
diff --git a/crates/modules/dominium/dominium-physics/src/tick.rs b/crates/modules/dominium/dominium-physics/src/tick.rs
new file mode 100644
index 0000000..10e1014
--- /dev/null
+++ b/crates/modules/dominium/dominium-physics/src/tick.rs
@@ -0,0 +1,142 @@
+//! 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);
+    }
+}
diff --git a/vamos.txt b/vamos.txt
index 09e4d9b..5dd87df 100644
--- a/vamos.txt
+++ b/vamos.txt
@@ -820,3 +820,80 @@
   ./target/debug/sandokan stop <card-id>
 
 
+
+
+
+
+● Comandos de prueba
+
+  target/ está en 22 GB — pero tranquilo: está gitignored, el repo git
+  real son 96 MB. El "desborde" es disco, no el repositorio. Más abajo
+  el cargo clean.
+
+  Todo de una
+
+  cargo check --workspace        # compila los ~145 crates
+  cargo test  --workspace        # corre TODOS los tests (~270)
+
+  sandokan — el ejecutable interactivo
+
+  cargo build -p sandokan-cli
+  ./target/debug/sandokan daemon &                 # deja el daemon
+  corriendo
+  ./target/debug/sandokan run /bin/sleep 300       # imprime un card_id
+  ./target/debug/sandokan list                     # lo ves activo
+  ./target/debug/sandokan status   <card-id>       # → Running
+  ./target/debug/sandokan telemetry <card-id>      # → mem / nproc
+  ./target/debug/sandokan stop     <card-id>       # → detenido
+  ./target/debug/sandokan status   <card-id>       # → Killed
+
+  Tests por área (lo construido esta sesión)
+
+  # Fase A — foundations
+  cargo test -p arje-brain-rules -p arje-brain-cognitive -p
+  arje-brain-audit \
+             -p sandokan-lifecycle -p arje-incarnate
+
+  # Fase B — orquestador + discovery
+  cargo test -p sandokan-core -p sandokan-local -p sandokan-daemon -p
+  sandokan \
+             -p brahman-dht -p brahman-card-discovery -p
+  brahman-ssh-multiplex
+
+  # Fase F — pineal (6 charts)
+  cargo test -p pineal-render -p pineal-export -p pineal-heatmap \
+             -p pineal-polar -p pineal-treemap -p pineal-flow -p
+  pineal-mesh
+
+  # Apps Fase C — núcleos
+  cargo test -p fana-core -p fana-graph -p fana-store -p fana-semantic
+    # fana
+  cargo test -p shuma-intent
+   # shuma shell
+  cargo test -p verbo-core -p verbo-mock
+   # verbo
+  cargo test -p dominium-core
+   # dominium
+
+  Validación arje (boot — ya existía)
+
+  seeds/validate.sh seeds/arje-minimal.card.json
+
+  ⚠  Disco — cargo clean
+
+  target/ = 22 GB y va a seguir creciendo (gpui, libp2p, russh son
+  pesados). Cuando quieras recuperar ese espacio:
+
+  cargo clean         # borra target/ entero — recupera ~22 GB
+
+  Tras eso, el próximo cargo build recompila todo desde cero (lento,
+  ~varios min). Recomendación: corré tus pruebas, y cuando termines una
+   tanda, cargo clean. No afecta nada del código ni del repo — sólo
+  artefactos de compilación.
+
+  ¿Querés que lo corra yo ahora (cargo clean), o lo dejás para cuando
+  termines de probar esta tanda? Si lo corro ahora, tu próxima prueba
+  recompila de cero.
+
+
+