feat(dominium): dominium-core — núcleo del simulador de campo medio
- grid — el Sustrato Plano: grilla SoA de 5 capas f32 (materia, psique, poder, oro, degradación), indexada y*width+x. - lemmings — Agentes Vectoriales en SoA: pos_x/y, edad, energia, vector_psi [Orden,Miedo,Curiosidad,Corruptibilidad], accion u8. spawn / swap_remove / nearest (determinista, empate por menor índice). - world — World + las 6 acciones atómicas fijas: Mover (gravedad mental hacia el vecino más afín al psi), Extraer, Sincronizar, Intercambiar, Replicar, Degradar. step_lemming despacha por el byte accion. - params — SimParams (las constantes que los sliders del panel ajustan). Cero deps gráficas — sólo serde (regla inviolable de la spec). 11 tests verdes (acciones verificadas: Mover sigue la materia, Extraer degrada, Replicar engendra, Intercambiar conserva energía, etc.). cargo check --workspace verde. Pendiente dominium: physics (difusión/entropía/cinemática), iso, render-plan, canvas/panel GPUI. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Generated
+7
@@ -3475,6 +3475,13 @@ dependencies = [
|
|||||||
"urlencoding",
|
"urlencoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dominium-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "double-ended-peekable"
|
name = "double-ended-peekable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -141,6 +141,11 @@ members = [
|
|||||||
"crates/modules/shuma/shuma-core",
|
"crates/modules/shuma/shuma-core",
|
||||||
"crates/modules/shuma/shuma-intent",
|
"crates/modules/shuma/shuma-intent",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/dominium/ — Simulador psicológico de campo medio
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/dominium/dominium-core",
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# modules/gioser/ — Landing WASM (chacana + 4 elementos)
|
# modules/gioser/ — Landing WASM (chacana + 4 elementos)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "dominium-core"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
description = "dominium — núcleo del simulador de campo medio: grilla SoA de 5 capas + Lemmings vectoriales + las 6 acciones atómicas. Sin deps gráficas."
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { workspace = true }
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
//! El Sustrato Plano — grilla SoA de 5 capas de `f32`.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Grilla de campos: 5 capas paralelas, cada una `width × height` `f32`,
|
||||||
|
/// indexadas `y * width + x`. Toda la física opera sobre estos arrays
|
||||||
|
/// contiguos (cache-friendly).
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Grid {
|
||||||
|
pub width: usize,
|
||||||
|
pub height: usize,
|
||||||
|
/// Biomasa / energía / alimento disponible.
|
||||||
|
pub materia: Vec<f32>,
|
||||||
|
/// Densidad de información / frecuencia dogmática.
|
||||||
|
pub psique: Vec<f32>,
|
||||||
|
/// Tensión de control / deuda / atractores del Estado Profundo.
|
||||||
|
pub poder: Vec<f32>,
|
||||||
|
/// Materia prima densa intercambiable.
|
||||||
|
pub oro: Vec<f32>,
|
||||||
|
/// Contaminación / cicatrices industriales del suelo.
|
||||||
|
pub degradacion: Vec<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Grid {
|
||||||
|
/// Grilla de `width × height` con todas las capas en cero.
|
||||||
|
pub fn new(width: usize, height: usize) -> Self {
|
||||||
|
let n = width * height;
|
||||||
|
Self {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
materia: vec![0.0; n],
|
||||||
|
psique: vec![0.0; n],
|
||||||
|
poder: vec![0.0; n],
|
||||||
|
oro: vec![0.0; n],
|
||||||
|
degradacion: vec![0.0; n],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cantidad de celdas (`width * height`).
|
||||||
|
pub fn cells(&self) -> usize {
|
||||||
|
self.width * self.height
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Índice plano de `(x, y)`. El caller garantiza bounds válidos.
|
||||||
|
pub fn idx(&self, x: usize, y: usize) -> usize {
|
||||||
|
y * self.width + x
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `true` si `(x, y)` cae dentro de la grilla.
|
||||||
|
pub fn in_bounds(&self, x: i64, y: i64) -> bool {
|
||||||
|
x >= 0 && y >= 0 && (x as usize) < self.width && (y as usize) < self.height
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clampa una coordenada continua a una celda válida.
|
||||||
|
pub fn clamp_cell(&self, x: f32, y: f32) -> (usize, usize) {
|
||||||
|
let cx = (x.floor() as i64).clamp(0, self.width as i64 - 1) as usize;
|
||||||
|
let cy = (y.floor() as i64).clamp(0, self.height as i64 - 1) as usize;
|
||||||
|
(cx, cy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_grid_is_zeroed() {
|
||||||
|
let g = Grid::new(8, 4);
|
||||||
|
assert_eq!(g.cells(), 32);
|
||||||
|
assert!(g.materia.iter().all(|&v| v == 0.0));
|
||||||
|
assert_eq!(g.materia.len(), 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn idx_and_bounds() {
|
||||||
|
let g = Grid::new(10, 5);
|
||||||
|
assert_eq!(g.idx(3, 2), 23);
|
||||||
|
assert!(g.in_bounds(9, 4));
|
||||||
|
assert!(!g.in_bounds(10, 4));
|
||||||
|
assert!(!g.in_bounds(-1, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clamp_cell_keeps_in_range() {
|
||||||
|
let g = Grid::new(10, 10);
|
||||||
|
assert_eq!(g.clamp_cell(-5.0, 3.7), (0, 3));
|
||||||
|
assert_eq!(g.clamp_cell(99.0, 99.0), (9, 9));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
//! Los Agentes Vectoriales — Lemmings en Structure-of-Arrays.
|
||||||
|
//!
|
||||||
|
//! Sin objetos ni punteros por agente: vectores paralelos indexados por
|
||||||
|
//! un `usize` continuo. Datos crudos alineados en caché.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Índices de las cuatro componentes de `vector_psi`.
|
||||||
|
pub const PSI_ORDEN: usize = 0;
|
||||||
|
pub const PSI_MIEDO: usize = 1;
|
||||||
|
pub const PSI_CURIOSIDAD: usize = 2;
|
||||||
|
pub const PSI_CORRUPTIBILIDAD: usize = 3;
|
||||||
|
|
||||||
|
/// Población de Lemmings en SoA. Todos los vectores tienen el mismo largo.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct Lemmings {
|
||||||
|
pub pos_x: Vec<f32>,
|
||||||
|
pub pos_y: Vec<f32>,
|
||||||
|
/// Contador incremental de ticks de vida.
|
||||||
|
pub edad: Vec<u32>,
|
||||||
|
/// Escalar de salud; si llega a 0 el agente muere.
|
||||||
|
pub energia: Vec<f32>,
|
||||||
|
/// Tensores de sesgo interno `[Orden, Miedo, Curiosidad, Corruptibilidad]`.
|
||||||
|
pub vector_psi: Vec<[f32; 4]>,
|
||||||
|
/// Byte discriminador de la máquina de estados (0-5).
|
||||||
|
pub accion: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Lemmings {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.pos_x.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.pos_x.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Instancia un Lemming nuevo (edad 0). Devuelve su índice.
|
||||||
|
pub fn spawn(&mut self, x: f32, y: f32, energia: f32, psi: [f32; 4]) -> usize {
|
||||||
|
let i = self.len();
|
||||||
|
self.pos_x.push(x);
|
||||||
|
self.pos_y.push(y);
|
||||||
|
self.edad.push(0);
|
||||||
|
self.energia.push(energia);
|
||||||
|
self.vector_psi.push(psi);
|
||||||
|
self.accion.push(0);
|
||||||
|
i
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Elimina el Lemming `i` por `swap_remove` — O(1), no preserva el
|
||||||
|
/// orden (el último ocupa el hueco).
|
||||||
|
pub fn remove(&mut self, i: usize) {
|
||||||
|
self.pos_x.swap_remove(i);
|
||||||
|
self.pos_y.swap_remove(i);
|
||||||
|
self.edad.swap_remove(i);
|
||||||
|
self.energia.swap_remove(i);
|
||||||
|
self.vector_psi.swap_remove(i);
|
||||||
|
self.accion.swap_remove(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Distancia euclidiana al cuadrado entre dos Lemmings (sin `sqrt` —
|
||||||
|
/// suficiente para comparar cercanía y bit-exacto).
|
||||||
|
pub fn dist2(&self, a: usize, b: usize) -> f32 {
|
||||||
|
let dx = self.pos_x[a] - self.pos_x[b];
|
||||||
|
let dy = self.pos_y[a] - self.pos_y[b];
|
||||||
|
dx * dx + dy * dy
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Índice del Lemming vivo más cercano a `i` (distinto de `i`), o
|
||||||
|
/// `None` si es el único. Determinista: ante empate gana el menor
|
||||||
|
/// índice.
|
||||||
|
pub fn nearest(&self, i: usize) -> Option<usize> {
|
||||||
|
let mut best: Option<(usize, f32)> = None;
|
||||||
|
for j in 0..self.len() {
|
||||||
|
if j == i {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let d = self.dist2(i, j);
|
||||||
|
if best.map(|(_, bd)| d < bd).unwrap_or(true) {
|
||||||
|
best = Some((j, d));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
best.map(|(j, _)| j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_and_remove() {
|
||||||
|
let mut l = Lemmings::new();
|
||||||
|
let a = l.spawn(1.0, 1.0, 10.0, [0.0; 4]);
|
||||||
|
let _b = l.spawn(2.0, 2.0, 20.0, [0.0; 4]);
|
||||||
|
assert_eq!((a, l.len()), (0, 2));
|
||||||
|
l.remove(a);
|
||||||
|
assert_eq!(l.len(), 1);
|
||||||
|
// swap_remove: el agente "b" ocupa el índice 0.
|
||||||
|
assert_eq!(l.energia[0], 20.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nearest_picks_closest_and_breaks_ties_by_index() {
|
||||||
|
let mut l = Lemmings::new();
|
||||||
|
l.spawn(0.0, 0.0, 1.0, [0.0; 4]); // 0
|
||||||
|
l.spawn(10.0, 0.0, 1.0, [0.0; 4]); // 1 — lejos
|
||||||
|
l.spawn(1.0, 0.0, 1.0, [0.0; 4]); // 2 — cerca de 0
|
||||||
|
assert_eq!(l.nearest(0), Some(2));
|
||||||
|
// Único agente → None.
|
||||||
|
let mut solo = Lemmings::new();
|
||||||
|
solo.spawn(0.0, 0.0, 1.0, [0.0; 4]);
|
||||||
|
assert_eq!(solo.nearest(0), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
//! `dominium-core` — el núcleo lógico del simulador de campo medio.
|
||||||
|
//!
|
||||||
|
//! Laboratorio de complejidad emergente: los agentes (Lemmings) no toman
|
||||||
|
//! decisiones cognitivas — reaccionan mecánicamente a los campos de una
|
||||||
|
//! grilla plana ejecutando una de 6 acciones atómicas fijas. Civilización,
|
||||||
|
//! guerra, fe y poder son patrones emergentes, no algoritmos.
|
||||||
|
//!
|
||||||
|
//! - [`grid`] — el Sustrato Plano: 5 capas SoA de `f32`.
|
||||||
|
//! - [`lemmings`] — los Agentes Vectoriales en Structure-of-Arrays.
|
||||||
|
//! - [`world`] — el `World` + las 6 acciones atómicas (`Action`).
|
||||||
|
//! - [`params`] — `SimParams`, las constantes que los sliders ajustan.
|
||||||
|
//!
|
||||||
|
//! Cero dependencias gráficas (regla inviolable de la spec): sólo `serde`.
|
||||||
|
//! La difusión/entropía/cinemática viven en `dominium-physics`; el
|
||||||
|
//! renderizado isométrico en `dominium-iso` + `dominium-render-plan`.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
pub mod grid;
|
||||||
|
pub mod lemmings;
|
||||||
|
pub mod params;
|
||||||
|
pub mod world;
|
||||||
|
|
||||||
|
pub use grid::Grid;
|
||||||
|
pub use lemmings::Lemmings;
|
||||||
|
pub use params::SimParams;
|
||||||
|
pub use world::{Action, World};
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
//! Constantes globales de la simulación.
|
||||||
|
//!
|
||||||
|
//! Son las que los sliders del Panel de Control alimentan en vivo: cada
|
||||||
|
//! una sintoniza una de las ecuaciones del núcleo.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Parámetros que gobiernan las 6 acciones y el ciclo de vida.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SimParams {
|
||||||
|
/// Velocidad de desplazamiento de `Mover` (celdas por tick).
|
||||||
|
pub move_speed: f32,
|
||||||
|
/// Energía que consume un paso de `Mover`.
|
||||||
|
pub move_cost: f32,
|
||||||
|
/// Cantidad extraída de la celda por `Extraer`.
|
||||||
|
pub extract_rate: f32,
|
||||||
|
/// Degradación añadida al suelo por cada `Extraer`.
|
||||||
|
pub degr_per_extract: f32,
|
||||||
|
/// Tasa de convergencia de `vector_psi` en `Sincronizar` (0-1).
|
||||||
|
pub sync_rate: f32,
|
||||||
|
/// Energía transferida por `Intercambiar`.
|
||||||
|
pub trade_amount: f32,
|
||||||
|
/// Umbral de energía para que `Replicar` dispare.
|
||||||
|
pub replicate_threshold: f32,
|
||||||
|
/// Fracción de la energía del padre que hereda el hijo en `Replicar`.
|
||||||
|
pub child_energy_frac: f32,
|
||||||
|
/// Daño de energía que inflige `Degradar`.
|
||||||
|
pub fight_damage: f32,
|
||||||
|
/// Fracción del daño que el atacante absorbe como energía.
|
||||||
|
pub absorb_frac: f32,
|
||||||
|
/// Umbral de energía bajo el cual el agente se fuerza a `Pelear`.
|
||||||
|
pub desperation_threshold: f32,
|
||||||
|
/// Edad máxima; al superarla el agente muere.
|
||||||
|
pub max_edad: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SimParams {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
move_speed: 1.0,
|
||||||
|
move_cost: 0.10,
|
||||||
|
extract_rate: 1.0,
|
||||||
|
degr_per_extract: 0.05,
|
||||||
|
sync_rate: 0.10,
|
||||||
|
trade_amount: 0.50,
|
||||||
|
replicate_threshold: 50.0,
|
||||||
|
child_energy_frac: 0.30,
|
||||||
|
fight_damage: 5.0,
|
||||||
|
absorb_frac: 0.50,
|
||||||
|
desperation_threshold: 5.0,
|
||||||
|
max_edad: 1000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
//! El mundo: grilla + lemmings, y las 6 acciones atómicas fijas.
|
||||||
|
//!
|
||||||
|
//! Cualquier "profesión" o "rol" del macro es sólo un Lemming ejecutando
|
||||||
|
//! una de estas 6 acciones en un entorno específico.
|
||||||
|
|
||||||
|
use crate::grid::Grid;
|
||||||
|
use crate::lemmings::{Lemmings, PSI_CORRUPTIBILIDAD, PSI_CURIOSIDAD, PSI_MIEDO, PSI_ORDEN};
|
||||||
|
use crate::params::SimParams;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Las 6 acciones atómicas. El byte `accion` del Lemming es uno de estos.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum Action {
|
||||||
|
/// Lee gradientes vecinos, se mueve hacia el óptimo, gasta energía.
|
||||||
|
Mover = 0,
|
||||||
|
/// Resta de la celda actual, suma a su energía, degrada el suelo.
|
||||||
|
Extraer = 1,
|
||||||
|
/// Acerca su `vector_psi` a los campos de la celda actual.
|
||||||
|
Sincronizar = 2,
|
||||||
|
/// Transfiere energía al vecino más cercano.
|
||||||
|
Intercambiar = 3,
|
||||||
|
/// Gasta energía para instanciar un Lemming hijo (edad 0).
|
||||||
|
Replicar = 4,
|
||||||
|
/// Resta energía al vecino más cercano y absorbe una fracción.
|
||||||
|
Degradar = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Action {
|
||||||
|
/// Convierte el byte discriminador. `None` si está fuera de rango.
|
||||||
|
pub fn from_u8(b: u8) -> Option<Action> {
|
||||||
|
match b {
|
||||||
|
0 => Some(Action::Mover),
|
||||||
|
1 => Some(Action::Extraer),
|
||||||
|
2 => Some(Action::Sincronizar),
|
||||||
|
3 => Some(Action::Intercambiar),
|
||||||
|
4 => Some(Action::Replicar),
|
||||||
|
5 => Some(Action::Degradar),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// El estado completo de la simulación.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct World {
|
||||||
|
pub grid: Grid,
|
||||||
|
pub lemmings: Lemmings,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl World {
|
||||||
|
pub fn new(width: usize, height: usize) -> Self {
|
||||||
|
Self { grid: Grid::new(width, height), lemmings: Lemmings::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Celda que ocupa el Lemming `i`.
|
||||||
|
fn cell_of(&self, i: usize) -> usize {
|
||||||
|
let (cx, cy) = self.grid.clamp_cell(self.lemmings.pos_x[i], self.lemmings.pos_y[i]);
|
||||||
|
self.grid.idx(cx, cy)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 0 · Mover — gravedad mental hacia el vecino más afín al `vector_psi`.
|
||||||
|
pub fn act_mover(&mut self, i: usize, p: &SimParams) {
|
||||||
|
let (cx, cy) =
|
||||||
|
self.grid.clamp_cell(self.lemmings.pos_x[i], self.lemmings.pos_y[i]);
|
||||||
|
let psi = self.lemmings.vector_psi[i];
|
||||||
|
let mut best_dir = (0.0f32, 0.0f32);
|
||||||
|
let mut best_score = f32::MIN;
|
||||||
|
for (dx, dy) in [(1i64, 0i64), (-1, 0), (0, 1), (0, -1)] {
|
||||||
|
let (nx, ny) = (cx as i64 + dx, cy as i64 + dy);
|
||||||
|
if !self.grid.in_bounds(nx, ny) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let idx = self.grid.idx(nx as usize, ny as usize);
|
||||||
|
// Orden busca materia, Miedo evita poder, Curiosidad busca
|
||||||
|
// psique, Corruptibilidad busca oro.
|
||||||
|
let score = psi[PSI_ORDEN] * self.grid.materia[idx]
|
||||||
|
- psi[PSI_MIEDO] * self.grid.poder[idx]
|
||||||
|
+ psi[PSI_CURIOSIDAD] * self.grid.psique[idx]
|
||||||
|
+ psi[PSI_CORRUPTIBILIDAD] * self.grid.oro[idx];
|
||||||
|
if score > best_score {
|
||||||
|
best_score = score;
|
||||||
|
best_dir = (dx as f32, dy as f32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let w = self.grid.width as f32 - 1.0;
|
||||||
|
let h = self.grid.height as f32 - 1.0;
|
||||||
|
self.lemmings.pos_x[i] =
|
||||||
|
(self.lemmings.pos_x[i] + best_dir.0 * p.move_speed).clamp(0.0, w);
|
||||||
|
self.lemmings.pos_y[i] =
|
||||||
|
(self.lemmings.pos_y[i] + best_dir.1 * p.move_speed).clamp(0.0, h);
|
||||||
|
self.lemmings.energia[i] -= p.move_cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 1 · Extraer — vacía materia de la celda hacia la energía del agente.
|
||||||
|
pub fn act_extraer(&mut self, i: usize, p: &SimParams) {
|
||||||
|
let idx = self.cell_of(i);
|
||||||
|
let taken = self.grid.materia[idx].min(p.extract_rate).max(0.0);
|
||||||
|
self.grid.materia[idx] -= taken;
|
||||||
|
self.lemmings.energia[i] += taken;
|
||||||
|
self.grid.degradacion[idx] += p.degr_per_extract;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 2 · Sincronizar — el `vector_psi` deriva hacia los campos de la celda.
|
||||||
|
pub fn act_sincronizar(&mut self, i: usize, p: &SimParams) {
|
||||||
|
let idx = self.cell_of(i);
|
||||||
|
let targets = [
|
||||||
|
self.grid.psique[idx],
|
||||||
|
self.grid.poder[idx],
|
||||||
|
self.grid.psique[idx],
|
||||||
|
self.grid.poder[idx],
|
||||||
|
];
|
||||||
|
for k in 0..4 {
|
||||||
|
let v = self.lemmings.vector_psi[i][k];
|
||||||
|
self.lemmings.vector_psi[i][k] = v + (targets[k] - v) * p.sync_rate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 3 · Intercambiar — transfiere energía al vecino más cercano.
|
||||||
|
pub fn act_intercambiar(&mut self, i: usize, p: &SimParams) {
|
||||||
|
let Some(j) = self.lemmings.nearest(i) else { return };
|
||||||
|
let amount = p.trade_amount.min(self.lemmings.energia[i]).max(0.0);
|
||||||
|
self.lemmings.energia[i] -= amount;
|
||||||
|
self.lemmings.energia[j] += amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 4 · Replicar — instancia un hijo con edad 0 en las mismas coordenadas.
|
||||||
|
pub fn act_replicar(&mut self, i: usize, p: &SimParams) {
|
||||||
|
if self.lemmings.energia[i] <= p.replicate_threshold {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cost = self.lemmings.energia[i] * p.child_energy_frac;
|
||||||
|
self.lemmings.energia[i] -= cost;
|
||||||
|
let (x, y) = (self.lemmings.pos_x[i], self.lemmings.pos_y[i]);
|
||||||
|
let psi = self.lemmings.vector_psi[i];
|
||||||
|
self.lemmings.spawn(x, y, cost, psi);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 5 · Degradar (Pelear) — resta energía al vecino y absorbe parte.
|
||||||
|
pub fn act_degradar(&mut self, i: usize, p: &SimParams) {
|
||||||
|
let Some(j) = self.lemmings.nearest(i) else { return };
|
||||||
|
let dmg = p.fight_damage.min(self.lemmings.energia[j]).max(0.0);
|
||||||
|
self.lemmings.energia[j] -= dmg;
|
||||||
|
self.lemmings.energia[i] += dmg * p.absorb_frac;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Despacha la acción del Lemming `i` según su byte `accion`.
|
||||||
|
pub fn step_lemming(&mut self, i: usize, p: &SimParams) {
|
||||||
|
match Action::from_u8(self.lemmings.accion[i]) {
|
||||||
|
Some(Action::Mover) => self.act_mover(i, p),
|
||||||
|
Some(Action::Extraer) => self.act_extraer(i, p),
|
||||||
|
Some(Action::Sincronizar) => self.act_sincronizar(i, p),
|
||||||
|
Some(Action::Intercambiar) => self.act_intercambiar(i, p),
|
||||||
|
Some(Action::Replicar) => self.act_replicar(i, p),
|
||||||
|
Some(Action::Degradar) => self.act_degradar(i, p),
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn world_1x_lemming() -> (World, SimParams) {
|
||||||
|
let mut w = World::new(16, 16);
|
||||||
|
w.lemmings.spawn(8.0, 8.0, 100.0, [1.0, 0.0, 0.0, 0.0]);
|
||||||
|
(w, SimParams::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn action_from_u8_covers_0_to_5() {
|
||||||
|
for b in 0..=5u8 {
|
||||||
|
assert!(Action::from_u8(b).is_some());
|
||||||
|
}
|
||||||
|
assert!(Action::from_u8(6).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mover_heads_toward_higher_materia() {
|
||||||
|
let (mut w, p) = world_1x_lemming();
|
||||||
|
// Materia alta a la derecha de (8,8).
|
||||||
|
let right = w.grid.idx(9, 8);
|
||||||
|
w.grid.materia[right] = 100.0;
|
||||||
|
let x0 = w.lemmings.pos_x[0];
|
||||||
|
w.act_mover(0, &p);
|
||||||
|
assert!(w.lemmings.pos_x[0] > x0, "se movió hacia la materia");
|
||||||
|
assert!(w.lemmings.energia[0] < 100.0, "Mover cuesta energía");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extraer_drains_cell_into_agent_and_degrades() {
|
||||||
|
let (mut w, p) = world_1x_lemming();
|
||||||
|
let idx = w.grid.idx(8, 8);
|
||||||
|
w.grid.materia[idx] = 10.0;
|
||||||
|
w.act_extraer(0, &p);
|
||||||
|
assert!(w.grid.materia[idx] < 10.0);
|
||||||
|
assert!(w.lemmings.energia[0] > 100.0);
|
||||||
|
assert!(w.grid.degradacion[idx] > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replicar_spawns_child_and_costs_energy() {
|
||||||
|
let (mut w, p) = world_1x_lemming(); // energía 100 > umbral 50
|
||||||
|
w.act_replicar(0, &p);
|
||||||
|
assert_eq!(w.lemmings.len(), 2);
|
||||||
|
assert_eq!(w.lemmings.edad[1], 0);
|
||||||
|
assert!(w.lemmings.energia[0] < 100.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn degradar_drains_nearest_and_absorbs() {
|
||||||
|
let mut w = World::new(16, 16);
|
||||||
|
w.lemmings.spawn(8.0, 8.0, 50.0, [0.0; 4]);
|
||||||
|
w.lemmings.spawn(9.0, 8.0, 50.0, [0.0; 4]);
|
||||||
|
let p = SimParams::default();
|
||||||
|
w.act_degradar(0, &p);
|
||||||
|
assert!(w.lemmings.energia[1] < 50.0, "la víctima pierde energía");
|
||||||
|
assert!(w.lemmings.energia[0] > 50.0, "el atacante absorbe");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn intercambiar_conserves_total_energy() {
|
||||||
|
let mut w = World::new(16, 16);
|
||||||
|
w.lemmings.spawn(8.0, 8.0, 30.0, [0.0; 4]);
|
||||||
|
w.lemmings.spawn(9.0, 8.0, 30.0, [0.0; 4]);
|
||||||
|
let p = SimParams::default();
|
||||||
|
w.act_intercambiar(0, &p);
|
||||||
|
let total = w.lemmings.energia[0] + w.lemmings.energia[1];
|
||||||
|
assert!((total - 60.0).abs() < 1e-4, "la energía se conserva");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user