Files
brahman/crates/apps/dominium/src/main.rs
T
sergio f46c7b435f feat(dominium): backend GPUI + app — ventana viva del simulador
dominium-canvas-gpui: Element que pinta un RenderPlan como quads,
centrado en sus bounds (rgba→hsla, único crate que toca gpui).

app dominium: compone core→physics→iso→render-plan→canvas en una
ventana GPUI con bucle de simulación de fondo (~11 tps), panel de
estadísticas, controles play/pausa + re-sembrar, y re-siembra
automática al colapso poblacional.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:22:35 +00:00

311 lines
9.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `dominium` — la ventana viva del simulador de campo medio.
//!
//! Compone toda la cadena de dominium en un app GPUI:
//!
//! ```text
//! dominium-core ─► dominium-physics ─► dominium-iso ─►
//! dominium-render-plan ─► dominium-canvas-gpui ─► [esta ventana]
//! ```
//!
//! Un bucle de fondo avanza la simulación ~11 veces por segundo; cada
//! tick reconstruye la maqueta isométrica y la repinta. El panel
//! derecho muestra las estadísticas agregadas y dos controles
//! (play/pausa, re-sembrar). Cuando la población colapsa, el mundo se
//! re-siembra solo: la demo nunca se queda en negro.
use std::time::Duration;
use dominium_canvas_gpui::DominiumCanvas;
use dominium_core::{SimParams, World};
use dominium_iso::{IsoProjector, ZWeights};
use dominium_physics::tick;
use dominium_render_plan::{build_plan, PlanConfig};
use gpui::{
div, hsla, prelude::*, px, Context, IntoElement, Render, SharedString, Window,
};
use nahual_launcher::launch_app;
use nahual_theme::Theme;
/// Lado de la grilla cuadrada del mundo.
const GRID: usize = 40;
/// Población inicial de Lemmings.
const LEMMINGS: usize = 50;
/// Periodo del bucle de simulación.
const TICK_MS: u64 = 90;
/// PRNG mínimo (LCG de 64 bits) — siembra reproducible sin dependencias.
struct Lcg(u64);
impl Lcg {
fn new(seed: u64) -> Self {
Self(seed)
}
fn next_u32(&mut self) -> u32 {
// Constantes de Knuth (MMIX).
self.0 = self
.0
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(self.0 >> 33) as u32
}
/// Flotante uniforme en `[0, 1)`.
fn next_f32(&mut self) -> f32 {
(self.next_u32() >> 8) as f32 / (1u32 << 24) as f32
}
}
/// Siembra un mundo: continentes de `materia`, vetas de `oro`, niebla de
/// `psique` y una población de Lemmings con sesgos y acciones variadas.
fn seed(seed: u64) -> World {
let mut w = World::new(GRID, GRID);
let mut rng = Lcg::new(seed);
for cy in 0..GRID {
for cx in 0..GRID {
let idx = w.grid.idx(cx, cy);
// m² concentra la materia en parches → aspecto de continentes.
let m = rng.next_f32();
w.grid.materia[idx] = m * m * 60.0;
if rng.next_f32() > 0.92 {
w.grid.oro[idx] = rng.next_f32() * 40.0;
}
w.grid.psique[idx] = rng.next_f32() * 12.0;
}
}
for _ in 0..LEMMINGS {
let x = rng.next_f32() * (GRID as f32 - 1.0);
let y = rng.next_f32() * (GRID as f32 - 1.0);
let psi = [
rng.next_f32(),
rng.next_f32(),
rng.next_f32(),
rng.next_f32(),
];
let i = w.lemmings.spawn(x, y, 30.0 + rng.next_f32() * 40.0, psi);
w.lemmings.accion[i] = (rng.next_u32() % 6) as u8;
}
w
}
/// Estadísticas agregadas de un instante de la simulación.
struct Stats {
poblacion: usize,
materia: f32,
oro: f32,
energia: f32,
}
/// El estado del simulador y su presentación.
struct Sim {
world: World,
params: SimParams,
iso: IsoProjector,
weights: ZWeights,
cfg: PlanConfig,
running: bool,
/// Ticks transcurridos en la época actual.
tick: u64,
/// Cuántas veces se re-sembró el mundo (colapso poblacional).
epoch: u64,
/// Semilla rodante para cada re-siembra.
rng_seed: u64,
}
impl Sim {
fn new(cx: &mut Context<Self>) -> Self {
let rng_seed = 0xD0_31_31_07;
let sim = Self {
world: seed(rng_seed),
params: SimParams::default(),
iso: IsoProjector::new(12.0, 0.05),
weights: ZWeights::default(),
cfg: PlanConfig {
tile: 15.0,
lemming_size: 8.0,
lemming_lift: 0.7,
palette: Default::default(),
},
running: true,
tick: 0,
epoch: 0,
rng_seed,
};
sim.start_loop(cx);
sim
}
/// Lanza el bucle de fondo que avanza la simulación.
fn start_loop(&self, cx: &mut Context<Self>) {
cx.spawn(async move |this, cx| loop {
cx.background_executor()
.timer(Duration::from_millis(TICK_MS))
.await;
let alive = this.update(cx, |sim, cx| {
if sim.running {
sim.advance();
cx.notify();
}
});
if alive.is_err() {
break; // la entidad murió → ventana cerrada.
}
})
.detach();
}
/// Un paso de simulación; re-siembra si la población colapsa.
fn advance(&mut self) {
tick(&mut self.world, &self.params);
self.tick += 1;
if self.world.lemmings.is_empty() {
self.epoch += 1;
self.rng_seed = self.rng_seed.wrapping_mul(2862933555777941757).wrapping_add(1);
self.world = seed(self.rng_seed);
self.tick = 0;
}
}
/// Re-siembra el mundo a mano (botón ↺).
fn reseed(&mut self) {
self.rng_seed = self.rng_seed.wrapping_add(0x9E3779B9);
self.world = seed(self.rng_seed);
self.tick = 0;
self.epoch += 1;
}
/// Calcula las estadísticas del instante actual.
fn stats(&self) -> Stats {
let g = &self.world.grid;
Stats {
poblacion: self.world.lemmings.len(),
materia: g.materia.iter().sum(),
oro: g.oro.iter().sum(),
energia: self.world.lemmings.energia.iter().sum(),
}
}
}
/// Fila etiqueta/valor del panel de estadísticas.
fn stat_row(label: &str, value: String, theme: &Theme) -> impl IntoElement {
div()
.flex()
.flex_row()
.justify_between()
.child(div().text_color(theme.fg_muted).child(SharedString::from(label.to_string())))
.child(div().text_color(theme.fg_text).child(SharedString::from(value)))
}
impl Render for Sim {
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = Theme::global(cx).clone();
let panel = hsla(220.0 / 360.0, 0.18, 0.10, 1.0);
let chip = hsla(220.0 / 360.0, 0.16, 0.16, 1.0);
let canvas_bg = hsla(220.0 / 360.0, 0.22, 0.06, 1.0);
let accent = theme.accent;
let stats = self.stats();
// --- Barra de estado ---
let estado = if self.running { "● corriendo" } else { "‖ en pausa" };
let status = div()
.h(px(34.))
.flex()
.flex_row()
.items_center()
.justify_between()
.px(px(14.))
.bg(panel)
.text_color(theme.fg_text)
.child(SharedString::from(format!(
"dominium · campo medio · época {} · tick {}",
self.epoch, self.tick
)))
.child(div().text_color(accent).child(SharedString::from(estado.to_string())));
// --- Maqueta isométrica ---
let plan = build_plan(&self.world, &self.iso, &self.weights, &self.cfg);
let canvas = div()
.flex_1()
.overflow_hidden()
.child(DominiumCanvas::new(plan).background(canvas_bg));
// --- Botones de control ---
let play_label = if self.running { "‖ Pausar" } else { "▶ Reanudar" };
let play = div()
.id("play")
.px(px(10.))
.py(px(7.))
.bg(chip)
.rounded(px(5.))
.text_color(theme.fg_text)
.cursor_pointer()
.hover(|s| s.bg(theme.bg_row_hover))
.child(SharedString::from(play_label.to_string()))
.on_click(cx.listener(|sim, _ev, _w, cx| {
sim.running = !sim.running;
cx.notify();
}));
let reset = div()
.id("reset")
.px(px(10.))
.py(px(7.))
.bg(chip)
.rounded(px(5.))
.text_color(theme.fg_text)
.cursor_pointer()
.hover(|s| s.bg(theme.bg_row_hover))
.child("↺ Re-sembrar")
.on_click(cx.listener(|sim, _ev, _w, cx| {
sim.reseed();
cx.notify();
}));
// --- Panel de estadísticas ---
let side = div()
.w(px(216.))
.flex()
.flex_col()
.gap(px(10.))
.p(px(12.))
.bg(panel)
.text_color(theme.fg_text)
.child(div().text_color(theme.fg_muted).child("[SIM]"))
.child(play)
.child(reset)
.child(div().h(px(1.)).bg(theme.border))
.child(stat_row("Población", format!("{}", stats.poblacion), &theme))
.child(stat_row("Materia", format!("{:.0}", stats.materia), &theme))
.child(stat_row("Oro", format!("{:.0}", stats.oro), &theme))
.child(stat_row("Energía", format!("{:.0}", stats.energia), &theme))
.child(div().h(px(1.)).bg(theme.border))
.child(
div()
.text_color(theme.fg_muted)
.child(SharedString::from(format!("grilla {GRID}×{GRID}"))),
)
.child(
div()
.text_color(theme.fg_muted)
.child("relieve = materia (Z)"),
);
// --- Composición ---
div()
.size_full()
.flex()
.flex_col()
.bg(theme.bg_app)
.child(status)
.child(
div()
.flex()
.flex_row()
.flex_1()
.child(canvas)
.child(side),
)
}
}
fn main() {
launch_app("brahman · dominium", (1120., 720.), Sim::new);
}