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 = (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 + + + + +● 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 # → Running + ./target/debug/sandokan telemetry # → mem / nproc + ./target/debug/sandokan stop # → detenido + ./target/debug/sandokan status # → 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. + + +