tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
1//! `sim` — **simulación de la ley [`Fluir`](crate::LeyKind::Fluir)**: lo que hace
2//! que el agua sea *líquida*. Un autómata celular sobre la ocupación de un
3//! [`VoxelGrid`]: cada celda de agua **cae con gravedad**, **se esparce a vecinos
4//! horizontales** y **cae por las cornisas** (cascadas). Conserva la masa (sólo
5//! reubica celdas) y **converge** (no slosh perpetuo): la lateral sólo procede si el
6//! agua puede descender o si hay *carga* (agua encima), así una torre se desploma en
7//! charco y un manto de 1 de profundidad se queda quieto.
8//!
9//! Es contenido puro de CPU: no toca GPU. La app aplica los cambios al grid
10//! (`set`/`clear`) y sube lo *dirty* con [`VoxelRenderer::sync`]
11//! (llimphi_3d::VoxelRenderer::sync). El paso es determinista (sin azar): mismas
12//! condiciones → misma evolución.
13
14use crate::ecuacion::{FieldDef, FieldEngine, Program};
15use llimphi_3d::VoxelGrid;
16
17const AIR: u8 = 0;
18const SOLID: u8 = 1;
19const WATER: u8 = 2;
20
21/// Un nuevo estado de una celda producido por [`WaterSim::step`]: `(pos, estado)`
22/// con `estado` ∈ {[`AIR`], [`WATER`]}. La app lo traduce a `clear`/`set` en el grid.
23pub type CellChange = ([u32; 3], u8);
24
25/// Estado de aire de una celda (para que el caller distinga sin exponer constantes).
26pub const CELL_AIR: u8 = AIR;
27/// Estado de agua de una celda.
28pub const CELL_WATER: u8 = WATER;
29
30/// Autómata celular del agua sobre la ocupación de un grid.
31pub struct WaterSim {
32 dim: [u32; 3],
33 /// Ocupación: 0 aire, 1 sólido (terreno), 2 agua.
34 occ: Vec<u8>,
35 /// Lock por celda dentro de un paso (evita mover dos veces / oscilar).
36 moved: Vec<bool>,
37 /// Índices tocados en el paso (para limpiar `moved` barato).
38 touched: Vec<usize>,
39 /// Fila de agua más alta (tope del escaneo; el agua nunca sube).
40 top: u32,
41 /// Alterna el orden de los vecinos cada paso (simetría).
42 parity: u32,
43 /// Parámetros de la ley Fluir. `gravedad` `[0,1]` = ritmo de caída (1 = cae cada
44 /// paso; menor = más viscoso, cae a tirones). `horizontal` `[0,1]` = ritmo de
45 /// esparcido lateral (0 = no se esparce → líquido espeso que se apila; 1 = se
46 /// esparce cada paso → líquido fino que se nivela).
47 gravedad: f32,
48 horizontal: f32,
49 /// Acumuladores deterministas que convierten los ritmos `[0,1]` en "actúa este
50 /// paso sí/no" sin azar (no hay `Math.random`).
51 g_acc: f32,
52 h_acc: f32,
53}
54
55impl WaterSim {
56 /// Clasifica un grid: las celdas cuyo color es exactamente `agua` son agua; el
57 /// resto de las sólidas, terreno. El agua del worldgen se pinta con el color de
58 /// paleta sin grano, así el match exacto la identifica.
59 pub fn from_grid(grid: &VoxelGrid, agua: [u8; 3]) -> Self {
60 Self::with_params(grid, agua, 1.0, 1.0)
61 }
62
63 /// Como [`from_grid`](Self::from_grid) pero con los parámetros de la ley Fluir
64 /// (`gravedad`, `horizontal`) que dicta el material líquido.
65 pub fn with_params(grid: &VoxelGrid, agua: [u8; 3], gravedad: f32, horizontal: f32) -> Self {
66 let dim = grid.dim();
67 let n = (dim[0] * dim[1] * dim[2]) as usize;
68 let mut occ = vec![AIR; n];
69 let mut top = 0u32;
70 for z in 0..dim[2] {
71 for y in 0..dim[1] {
72 for x in 0..dim[0] {
73 if let Some(c) = grid.get(x, y, z) {
74 if c[3] > 0 {
75 let i = (x + y * dim[0] + z * dim[0] * dim[1]) as usize;
76 if [c[0], c[1], c[2]] == agua {
77 occ[i] = WATER;
78 top = top.max(y);
79 } else {
80 occ[i] = SOLID;
81 }
82 }
83 }
84 }
85 }
86 }
87 Self {
88 dim,
89 occ,
90 moved: vec![false; n],
91 touched: Vec::new(),
92 top,
93 parity: 0,
94 gravedad: gravedad.clamp(0.05, 1.0),
95 horizontal: horizontal.clamp(0.0, 1.0),
96 g_acc: 0.0,
97 h_acc: 0.0,
98 }
99 }
100
101 /// Cantidad de celdas de agua (conservada por `step`).
102 pub fn water_count(&self) -> usize {
103 self.occ.iter().filter(|&&c| c == WATER).count()
104 }
105
106 #[inline]
107 fn idx(&self, x: u32, y: u32, z: u32) -> usize {
108 (x + y * self.dim[0] + z * self.dim[0] * self.dim[1]) as usize
109 }
110
111 /// Ocupación en `(x,y,z)` con bordes: fuera en X/Z o bajo el piso = sólido
112 /// (muros + suelo); por encima del tope vertical = aire.
113 #[inline]
114 fn at(&self, x: i32, y: i32, z: i32) -> u8 {
115 if x < 0 || z < 0 || x >= self.dim[0] as i32 || z >= self.dim[2] as i32 || y < 0 {
116 return SOLID;
117 }
118 if y >= self.dim[1] as i32 {
119 return AIR;
120 }
121 self.occ[self.idx(x as u32, y as u32, z as u32)]
122 }
123
124 #[inline]
125 fn is_free(&self, x: i32, y: i32, z: i32) -> bool {
126 // Libre = aire dentro del grid (no muro, no agua, no tope).
127 x >= 0
128 && z >= 0
129 && y >= 0
130 && x < self.dim[0] as i32
131 && y < self.dim[1] as i32
132 && z < self.dim[2] as i32
133 && self.occ[self.idx(x as u32, y as u32, z as u32)] == AIR
134 && !self.moved[self.idx(x as u32, y as u32, z as u32)]
135 }
136
137 /// Avanza un paso del autómata. Devuelve las celdas que cambiaron de estado
138 /// (para subir sólo eso al grid). Lista vacía = el agua se asentó.
139 pub fn step(&mut self) -> Vec<CellChange> {
140 // Limpiar el lock del paso anterior.
141 for &i in &self.touched {
142 self.moved[i] = false;
143 }
144 self.touched.clear();
145 let mut changes: Vec<CellChange> = Vec::new();
146
147 // Ritmos → "actúa este paso" deterministas (acumulador, sin azar). Con
148 // gravedad/horizontal = 1 actúan siempre (comportamiento base); más bajos,
149 // a tirones → líquido más viscoso/espeso.
150 self.g_acc += self.gravedad;
151 let fall = self.g_acc >= 1.0;
152 if fall {
153 self.g_acc -= 1.0;
154 }
155 self.h_acc += self.horizontal;
156 let spread = self.h_acc >= 1.0;
157 if spread {
158 self.h_acc -= 1.0;
159 }
160
161 // Orden de vecinos horizontales, rotado por paridad (simetría).
162 let dirs0: [(i32, i32); 4] = [(1, 0), (-1, 0), (0, 1), (0, -1)];
163 let rot = (self.parity % 4) as usize;
164 let mut dirs = [(0, 0); 4];
165 for k in 0..4 {
166 dirs[k] = dirs0[(k + rot) % 4];
167 }
168
169 let top = self.top.min(self.dim[1] - 1);
170 for z in 0..self.dim[2] as i32 {
171 for y in 0..=top as i32 {
172 for x in 0..self.dim[0] as i32 {
173 let i = self.idx(x as u32, y as u32, z as u32);
174 if self.occ[i] != WATER || self.moved[i] {
175 continue;
176 }
177 // 1) Caer: si abajo hay aire libre (gatea la gravedad).
178 if fall && self.is_free(x, y - 1, z) {
179 self.relocate(i, (x, y - 1, z), &mut changes);
180 continue;
181 }
182 // El componente horizontal (diagonal y lateral) gatea por `spread`.
183 if !spread {
184 continue;
185 }
186 // 2) Caer en diagonal (flujo / cornisa → cascada): vecino libre
187 // cuya celda de abajo también está libre.
188 let mut done = false;
189 for (dx, dz) in dirs {
190 if self.is_free(x + dx, y, z + dz) && self.at(x + dx, y - 1, z + dz) == AIR {
191 self.relocate(i, (x + dx, y, z + dz), &mut changes);
192 done = true;
193 break;
194 }
195 }
196 if done {
197 continue;
198 }
199 // 3) Esparcir lateral SÓLO si hay carga (agua justo debajo): una
200 // torre se desploma en charco; un manto de 1 se queda quieto.
201 if self.at(x, y - 1, z) == WATER {
202 for (dx, dz) in dirs {
203 if self.is_free(x + dx, y, z + dz) {
204 self.relocate(i, (x + dx, y, z + dz), &mut changes);
205 break;
206 }
207 }
208 }
209 }
210 }
211 }
212 self.parity = self.parity.wrapping_add(1);
213 changes
214 }
215
216 /// Mueve el agua de `from` (índice) a `to` (coord), registrando los cambios y
217 /// bloqueando el destino para el resto del paso.
218 fn relocate(&mut self, from: usize, to: (i32, i32, i32), changes: &mut Vec<CellChange>) {
219 let (tx, ty, tz) = (to.0 as u32, to.1 as u32, to.2 as u32);
220 let ti = self.idx(tx, ty, tz);
221 self.occ[from] = AIR;
222 self.occ[ti] = WATER;
223 self.moved[ti] = true;
224 self.touched.push(ti);
225 // Posición de origen.
226 let fz = from as u32 / (self.dim[0] * self.dim[1]);
227 let rem = from as u32 % (self.dim[0] * self.dim[1]);
228 let fy = rem / self.dim[0];
229 let fx = rem % self.dim[0];
230 changes.push(([fx, fy, fz], AIR));
231 changes.push(([tx, ty, tz], WATER));
232 }
233}
234
235/// Simulación de la ley [`Crecer`](crate::LeyKind::Crecer): una planta (material con
236/// rol objeto) **crece de abajo hacia arriba** a su velocidad. Toma las celdas del
237/// objeto en el grid, las esconde, y las va revelando en orden de altura. La
238/// `velocidad` escala cuántas celdas se revelan por paso (a `1.0`, la planta entera
239/// brota en ~[`GROW_TICKS`] pasos sin importar su tamaño).
240pub struct GrowthSim {
241 /// Celdas de la planta `(pos, color)`, ordenadas por altura ascendente.
242 cells: Vec<([u32; 3], [u8; 3])>,
243 /// Cuántas reveladas hasta ahora.
244 idx: usize,
245 /// Acumulador de la tasa de revelado (determinista).
246 acc: f32,
247 /// Celdas reveladas por paso (derivada de la velocidad y el tamaño).
248 rate: f32,
249}
250
251/// Pasos en los que una planta crece del todo a velocidad `1.0` (~2 s a 30 fps).
252pub const GROW_TICKS: f32 = 60.0;
253
254impl GrowthSim {
255 /// Junta las celdas del color de la planta y las ordena por altura. `velocidad`
256 /// viene de la ley Crecer del material.
257 pub fn from_grid(grid: &VoxelGrid, planta: [u8; 3], velocidad: f32) -> Self {
258 let dim = grid.dim();
259 let mut cells = Vec::new();
260 for z in 0..dim[2] {
261 for y in 0..dim[1] {
262 for x in 0..dim[0] {
263 if let Some(c) = grid.get(x, y, z) {
264 if c[3] > 0 && [c[0], c[1], c[2]] == planta {
265 cells.push(([x, y, z], planta));
266 }
267 }
268 }
269 }
270 }
271 // Orden por altura (y), luego x,z para determinismo.
272 cells.sort_by_key(|(p, _)| (p[1], p[0], p[2]));
273 let len = cells.len().max(1) as f32;
274 let rate = (velocidad.max(0.0) * len / GROW_TICKS).max(0.05);
275 Self { cells, idx: 0, acc: 0.0, rate }
276 }
277
278 /// Las celdas de la planta (para esconderlas al arrancar el crecimiento).
279 pub fn cells(&self) -> &[([u32; 3], [u8; 3])] {
280 &self.cells
281 }
282
283 /// `true` si la planta ya terminó de crecer.
284 pub fn done(&self) -> bool {
285 self.idx >= self.cells.len()
286 }
287
288 /// Revela el siguiente lote de celdas (según la velocidad). Devuelve las celdas a
289 /// dibujar este paso `(pos, color)`. Vacío cuando ya creció del todo.
290 pub fn step(&mut self) -> Vec<([u32; 3], [u8; 3])> {
291 if self.done() {
292 return Vec::new();
293 }
294 self.acc += self.rate;
295 let n = self.acc.floor() as usize;
296 self.acc -= n as f32;
297 let mut out = Vec::new();
298 for _ in 0..n {
299 if self.idx < self.cells.len() {
300 out.push(self.cells[self.idx]);
301 self.idx += 1;
302 }
303 }
304 out
305 }
306}
307
308/// Simulación de una ley [`Ecuacion`](crate::LeyKind::Ecuacion) **sobre un material
309/// real del mundo**: corre el sistema de ecuaciones de campo en las celdas del material
310/// y las **recolorea** por el valor del campo visible — el material queda donde está,
311/// "vivo" según su ecuación (a diferencia de [`WaterSim`], que mueve celdas).
312///
313/// Clasifica las celdas por **proximidad de color** al color base (± `tol` por canal),
314/// para alcanzar también materiales con *grano* (el terreno varía su color; el agua no).
315/// Materiales de color parecido pueden mezclarse — es una aproximación de preview.
316pub struct EcuacionSim {
317 engine: FieldEngine,
318 /// Celdas del material (posición fija; se clasifican una vez).
319 cells: Vec<[u32; 3]>,
320 /// Color base del material (para modular el brillo).
321 base: [u8; 3],
322 /// Campo que tiñe el color.
323 vis: usize,
324 /// Rango del campo visible (para normalizar).
325 fmin: f32,
326 fmax: f32,
327 /// Último color emitido por celda (para subir sólo lo que cambió).
328 last: Vec<[u8; 3]>,
329}
330
331impl EcuacionSim {
332 /// Clasifica las celdas cercanas a `material` (± `tol` por canal), arma el motor de
333 /// campo sobre el grid y siembra una perturbación determinista por celda (para que
334 /// la ecuación arranque con estructura). `vis` es el campo que se pinta.
335 pub fn from_grid(
336 grid: &VoxelGrid,
337 material: [u8; 3],
338 tol: i32,
339 campos: Vec<FieldDef>,
340 vis: usize,
341 ) -> Self {
342 let dim = grid.dim();
343 let mut cells = Vec::new();
344 for z in 0..dim[2] {
345 for y in 0..dim[1] {
346 for x in 0..dim[0] {
347 if let Some(c) = grid.get(x, y, z) {
348 if c[3] > 0 && cerca([c[0], c[1], c[2]], material, tol) {
349 cells.push([x, y, z]);
350 }
351 }
352 }
353 }
354 }
355 let vis = vis.min(campos.len().saturating_sub(1));
356 let (fmin, fmax) = campos.get(vis).map(|d| (d.min, d.max)).unwrap_or((0.0, 1.0));
357 let mut engine = FieldEngine::new(dim, campos);
358 let nf = engine.fields().len();
359 for &[x, y, z] in &cells {
360 for f in 0..nf {
361 let (mn, mx) = {
362 let d = &engine.fields()[f];
363 (d.min, d.max)
364 };
365 let h = hash01(x, y, z, f as u32);
366 engine.set(f as u16, x, y, z, mn + h * 0.6 * (mx - mn));
367 }
368 }
369 let last = vec![[0u8; 3]; cells.len()];
370 Self { engine, cells, base: material, vis, fmin, fmax, last }
371 }
372
373 /// Cantidad de celdas del material clasificadas.
374 pub fn cell_count(&self) -> usize {
375 self.cells.len()
376 }
377
378 /// Avanza un paso del campo y devuelve las celdas cuyo **color cambió**
379 /// `(pos, color)`, para subir sólo eso a la GPU. El color modula el brillo del
380 /// color base por el valor normalizado del campo visible (preserva el tono).
381 pub fn step(&mut self, program: &Program, params: &[f32]) -> Vec<([u32; 3], [u8; 3])> {
382 self.engine.step(program, params, 1.0);
383 let span = (self.fmax - self.fmin).max(1e-6);
384 let mut out = Vec::new();
385 for (i, &[x, y, z]) in self.cells.iter().enumerate() {
386 let t = ((self.engine.get(self.vis as u16, x, y, z) - self.fmin) / span).clamp(0.0, 1.0);
387 let k = 0.35 + 0.65 * t;
388 let col = [
389 (self.base[0] as f32 * k) as u8,
390 (self.base[1] as f32 * k) as u8,
391 (self.base[2] as f32 * k) as u8,
392 ];
393 if col != self.last[i] {
394 self.last[i] = col;
395 out.push(([x, y, z], col));
396 }
397 }
398 out
399 }
400}
401
402/// `true` si `c` está a ≤ `tol` de `base` en los tres canales.
403fn cerca(c: [u8; 3], base: [u8; 3], tol: i32) -> bool {
404 (c[0] as i32 - base[0] as i32).abs() <= tol
405 && (c[1] as i32 - base[1] as i32).abs() <= tol
406 && (c[2] as i32 - base[2] as i32).abs() <= tol
407}
408
409/// Hash determinista → `[0,1)` de una celda + índice de campo (sin azar; para sembrar).
410fn hash01(x: u32, y: u32, z: u32, f: u32) -> f32 {
411 let mut h = x
412 .wrapping_mul(0x9E37_79B1)
413 ^ y.wrapping_mul(0x85EB_CA77)
414 ^ z.wrapping_mul(0xC2B2_AE3D)
415 ^ f.wrapping_mul(0x27D4_EB2F);
416 h ^= h >> 15;
417 h = h.wrapping_mul(0x2C1B_3C6D);
418 h ^= h >> 12;
419 (h & 0x00FF_FFFF) as f32 / (0x0100_0000 as f32)
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425 use crate::Material;
426
427 /// Grid con piso sólido y un poco de agua arriba, en una columna.
428 fn grid_con_columna(dim: [u32; 3], agua: [u8; 3], col_x: u32, col_z: u32, altura: u32) -> VoxelGrid {
429 let mut g = VoxelGrid::new(dim);
430 // Piso en y=0.
431 for z in 0..dim[2] {
432 for x in 0..dim[0] {
433 g.set(x, 0, z, Material::Rock.color());
434 }
435 }
436 // Columna de agua sobre el piso.
437 for k in 0..altura {
438 g.set(col_x, 1 + k, col_z, agua);
439 }
440 g.reset_dirty();
441 g
442 }
443
444 #[test]
445 fn ecuacion_sim_clasifica_y_recolorea() {
446 use crate::ecuacion::{Assign, Expr, FieldDef, Program, Symbols};
447 // Grid con un parche plano de un material (color exacto, como el agua).
448 let dim = [12, 1, 12];
449 let mut g = VoxelGrid::new(dim);
450 let mat = [120, 160, 80];
451 for z in 3..9 {
452 for x in 3..9 {
453 g.set(x, 0, z, mat);
454 }
455 }
456 g.reset_dirty();
457 let sym = Symbols { campos: vec!["t".into()], params: vec!["k".into()] };
458 let e = Expr::parse("k * lap(t)", &sym).unwrap();
459 let prog = Program::compile(&[Assign { campo: 0, expr: e }]);
460 let mut sim = EcuacionSim::from_grid(&g, mat, 8, vec![FieldDef::new("t", 0.0, 0.0, 1.0)], 0);
461 assert_eq!(sim.cell_count(), 36, "clasificó el parche 6×6");
462 let mut changed = 0;
463 for _ in 0..30 {
464 changed += sim.step(&prog, &[0.2]).len();
465 }
466 assert!(changed > 0, "la ecuación recoloreó celdas del material");
467 }
468
469 #[test]
470 fn el_agua_cae_hasta_el_piso() {
471 let agua = Material::Water.color();
472 let dim = [8, 16, 8];
473 let mut g = VoxelGrid::new(dim);
474 for z in 0..dim[2] {
475 for x in 0..dim[0] {
476 g.set(x, 0, z, Material::Rock.color());
477 }
478 }
479 g.set(3, 10, 3, agua); // una gota flotando
480 g.reset_dirty();
481 let mut sim = WaterSim::from_grid(&g, agua);
482 assert_eq!(sim.water_count(), 1);
483 for _ in 0..30 {
484 sim.step();
485 }
486 // La gota terminó descansando sobre el piso (y=1) — no flotando.
487 assert_eq!(sim.water_count(), 1, "no se pierde ni duplica agua");
488 assert_eq!(sim.at(3, 1, 3), CELL_WATER, "el agua quedó sobre el piso");
489 assert_eq!(sim.at(3, 10, 3), CELL_AIR, "ya no flota");
490 }
491
492 #[test]
493 fn la_columna_se_esparce_y_se_asienta() {
494 let agua = Material::Water.color();
495 let dim = [16, 20, 16];
496 let g = grid_con_columna(dim, agua, 8, 8, 10); // torre de 10 de agua
497 let mut sim = WaterSim::from_grid(&g, agua);
498 let n0 = sim.water_count();
499
500 let mut last_changes = 1;
501 let mut steps = 0;
502 while last_changes > 0 && steps < 2000 {
503 last_changes = sim.step().len();
504 steps += 1;
505 }
506 // Conserva masa y converge (deja de moverse) en tiempo finito.
507 assert_eq!(sim.water_count(), n0, "masa conservada");
508 assert!(steps < 2000, "se asentó (sin slosh perpetuo) en {steps} pasos");
509
510 // Se esparció: el charco final es más ancho que la columna inicial (1 celda).
511 let mut ancho = 0;
512 for x in 0..dim[0] as i32 {
513 if sim.at(x, 1, 8) == CELL_WATER {
514 ancho += 1;
515 }
516 }
517 assert!(ancho > 1, "el agua se esparció horizontalmente (ancho={ancho})");
518 }
519
520 #[test]
521 fn horizontal_cero_no_se_esparce_espeso() {
522 // Líquido espeso (horizontal=0): cae pero NO se esparce → se apila en columna.
523 let agua = Material::Water.color();
524 let dim = [16, 20, 16];
525 let g = grid_con_columna(dim, agua, 8, 8, 10);
526 let mut sim = WaterSim::with_params(&g, agua, 1.0, 0.0);
527 for _ in 0..200 {
528 sim.step();
529 }
530 let mut ancho = 0;
531 for x in 0..dim[0] as i32 {
532 if sim.at(x, 1, 8) == CELL_WATER {
533 ancho += 1;
534 }
535 }
536 assert_eq!(ancho, 1, "sin componente horizontal el líquido no se esparce");
537 }
538
539 #[test]
540 fn la_planta_crece_de_abajo_hacia_arriba() {
541 // Una columna de "planta" (color cactus) de 6 de alto sobre un piso.
542 let planta = Material::Cactus.color();
543 let dim = [8, 16, 8];
544 let mut g = VoxelGrid::new(dim);
545 for k in 0..6 {
546 g.set(4, 1 + k, 4, planta);
547 }
548 g.reset_dirty();
549 let mut sim = GrowthSim::from_grid(&g, planta, 1.0);
550 assert_eq!(sim.cells().len(), 6);
551
552 // Revelar paso a paso: cada celda nueva está a una altura >= la anterior.
553 let mut revealed: Vec<u32> = Vec::new();
554 let mut steps = 0;
555 while !sim.done() && steps < 1000 {
556 for (pos, _) in sim.step() {
557 revealed.push(pos[1]);
558 }
559 steps += 1;
560 }
561 assert_eq!(revealed.len(), 6, "se revelaron todas las celdas");
562 // Monótono creciente en altura (crece hacia arriba).
563 assert!(revealed.windows(2).all(|w| w[0] <= w[1]), "crece de abajo hacia arriba: {revealed:?}");
564 assert_eq!(revealed[0], 1, "empieza por la base");
565 }
566
567 #[test]
568 fn cae_por_una_cornisa_cascada() {
569 // Una repisa (escalón) con agua arriba: debe caer por el borde.
570 let agua = Material::Water.color();
571 let dim = [16, 16, 8];
572 let mut g = VoxelGrid::new(dim);
573 // Piso bajo en y=0 en todo; una repisa sólida en y=1..=5 sobre la mitad x<8.
574 for z in 0..dim[2] {
575 for x in 0..dim[0] {
576 g.set(x, 0, z, Material::Rock.color());
577 }
578 }
579 for z in 0..dim[2] {
580 for x in 0..8 {
581 for y in 1..=5 {
582 g.set(x, y, z, Material::Rock.color());
583 }
584 }
585 }
586 // Agua sobre la repisa, cerca del borde (x=7).
587 for z in 0..dim[2] {
588 g.set(7, 6, z, agua);
589 }
590 g.reset_dirty();
591 let mut sim = WaterSim::from_grid(&g, agua);
592 let n0 = sim.water_count();
593 for _ in 0..60 {
594 sim.step();
595 }
596 assert_eq!(sim.water_count(), n0, "masa conservada al caer");
597 // Algo de agua llegó al piso bajo (y=1) del lado sin repisa (x>=8).
598 let mut llego_abajo = false;
599 for z in 0..dim[2] as i32 {
600 for x in 8..dim[0] as i32 {
601 if sim.at(x, 1, z) == CELL_WATER {
602 llego_abajo = true;
603 }
604 }
605 }
606 assert!(llego_abajo, "el agua cayó por la cornisa hasta el nivel bajo");
607 }
608}