gioser

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 229TOML 101Markdown 82Shell 24052 Definitions

⎇ main llimphi-voxel/src/casa.rs Rust · 1222 lines · 47.8 KB History Who touched what raw

chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual d8edd9a9c · Sergio ·

1//! **Casas procedurales por tipo**: a diferencia de un [`Mueble`](crate::Mueble)
2//! (modelo fijo), una casa se **genera** — dimensiones, ventanas, techo, chimenea y
3//! detalles varían con una **semilla**, dentro de un **estilo** ([`CasaTipo`]).
4//! Estampá una con [`stamp_casa`]; misma `(tipo, seed)` → misma casa (determinista),
5//! semillas distintas → casas distintas del mismo estilo.
6
7use serde::{Deserialize, Serialize};
8
9use crate::mineral::Mineral;
10use crate::worldgen::Material;
11use llimphi_3d::VoxelGrid;
12
13/// Estilo de casa. Cada uno tiene su gramática de generación.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15pub enum CasaTipo {
16    /// Prehistórica (Flintstones): choza de piedra abovedada, puerta en arco.
17    Flintstone,
18    /// Hobbit: montículo de pasto con puerta y ventana redondas.
19    Hobbit,
20    /// Cabaña de troncos: muros de madera, techo a dos aguas, chimenea de piedra.
21    Cabana,
22    /// Casa moderna: hormigón y grandes ventanales de vidrio, techo plano.
23    Moderna,
24    /// Edificio de departamentos: bloque alto con grilla de ventanas.
25    Edificio,
26    /// Iglú: cúpula de nieve con túnel de entrada.
27    Iglu,
28    /// Palafito: casa de madera sobre pilotes.
29    Palafito,
30    /// Adobe (pueblo): bloques de adobe apilados, techos planos y vigas.
31    Adobe,
32    /// Rascacielos: torre de vidrio y acero (altura variable).
33    Rascacielos,
34    /// Granja: casa con techo a dos aguas + granero.
35    Granja,
36    /// Torre medieval: torreón de piedra con almenas.
37    Torre,
38    /// Faro costero: torre blanca con franjas rojas y linterna que emite luz.
39    Faro,
40    /// Choza tropical: paredes bajas y techo cónico de paja.
41    Choza,
42    /// Tienda: local a pie de calle con toldo y vidriera.
43    Tienda,
44    /// Dúplex suburbano: dos plantas, techo a dos aguas, garaje.
45    Duplex,
46    /// Palacio de cuento: mármol sobre una escalinata, cuatro torres con
47    /// agujas de diamante y una cúpula de diamante con remate de oro — «un
48    /// lindo palacio de diamantes». Va al final: postcard numera las variantes.
49    Palacio,
50    /// Kiosko de jardín abierto: ocho columnas y un techo en domo, de
51    /// malaquita (franjas de cobre y jade) con borde y remate de oro.
52    Kiosko,
53}
54
55impl CasaTipo {
56    pub const ALL: [CasaTipo; 17] = [
57        CasaTipo::Flintstone,
58        CasaTipo::Hobbit,
59        CasaTipo::Cabana,
60        CasaTipo::Moderna,
61        CasaTipo::Edificio,
62        CasaTipo::Iglu,
63        CasaTipo::Palafito,
64        CasaTipo::Adobe,
65        CasaTipo::Rascacielos,
66        CasaTipo::Granja,
67        CasaTipo::Torre,
68        CasaTipo::Faro,
69        CasaTipo::Choza,
70        CasaTipo::Tienda,
71        CasaTipo::Duplex,
72        CasaTipo::Palacio,
73        CasaTipo::Kiosko,
74    ];
75
76    pub fn label(self) -> &'static str {
77        match self {
78            CasaTipo::Flintstone => "flintstone",
79            CasaTipo::Hobbit => "hobbit",
80            CasaTipo::Cabana => "cabaña",
81            CasaTipo::Moderna => "moderna",
82            CasaTipo::Edificio => "edificio",
83            CasaTipo::Iglu => "iglú",
84            CasaTipo::Palafito => "palafito",
85            CasaTipo::Adobe => "adobe",
86            CasaTipo::Rascacielos => "rascacielos",
87            CasaTipo::Granja => "granja",
88            CasaTipo::Torre => "torre",
89            CasaTipo::Faro => "faro",
90            CasaTipo::Choza => "choza",
91            CasaTipo::Tienda => "tienda",
92            CasaTipo::Duplex => "dúplex",
93            CasaTipo::Palacio => "palacio",
94            CasaTipo::Kiosko => "kiosko",
95        }
96    }
97
98    /// Radio de la **huella** en planta (voxels): hasta dónde llega la casa desde
99    /// su centro. Es lo que hay que nivelar y despejar para plantarla a mano
100    /// ([`plantar_casa`]). Generoso: mejor despejar de más que dejar un árbol
101    /// atravesando un muro.
102    pub fn huella(self) -> i32 {
103        match self {
104            // La esquina de la escalinata está a (w+3, d+4) = (19, 16): 25 cubre
105            // el palacio más grande y deja un voxel de aire alrededor.
106            CasaTipo::Palacio => 25,
107            CasaTipo::Kiosko => 9,
108            CasaTipo::Rascacielos | CasaTipo::Granja | CasaTipo::Duplex => 12,
109            _ => 9,
110        }
111    }
112
113    pub fn next(self) -> CasaTipo {
114        let i = CasaTipo::ALL.iter().position(|&t| t == self).unwrap_or(0);
115        CasaTipo::ALL[(i + 1) % CasaTipo::ALL.len()]
116    }
117}
118
119/// **Uso de casas en un bioma**: qué estilo y con qué densidad `[0,1]` aparecen
120/// (una candidata por parcela de la grilla gruesa; el bioma las esparce por semilla
121/// → un pueblo/ciudad procedural, como la flora).
122#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
123pub struct CasaUso {
124    pub tipo: CasaTipo,
125    pub densidad: f32,
126}
127
128/// **Tipo de asentamiento**: preset que rellena la mezcla de casas ([`CasaUso`])
129/// y la densidad urbana de un bioma → distintos pueblos y ciudades de un toque.
130/// Es sólo un atajo de autoría: al aplicarlo, escribe `casas` + `densidad_urbana`.
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
132pub enum PuebloTipo {
133    /// Aldea: puñado de casas rústicas, muy dispersa.
134    Aldea,
135    /// Suburbio: dúplex y casas modernas en densidad media.
136    Suburbio,
137    /// Ciudad: mezcla de edificios y casas, densa.
138    Ciudad,
139    /// Metrópolis: rascacielos y torres de departamentos, muy densa.
140    Metropolis,
141    /// Prehistórico: chozas, flintstones y hobbits, dispersa.
142    Prehistorico,
143    /// Costero: faros, palafitos y chozas junto al agua.
144    Costero,
145    /// Medieval: torres, adobe y cabañas amuralladas.
146    Medieval,
147    /// Rural: granjas y cabañas muy espaciadas.
148    Rural,
149}
150
151impl PuebloTipo {
152    pub const ALL: [PuebloTipo; 8] = [
153        PuebloTipo::Aldea,
154        PuebloTipo::Suburbio,
155        PuebloTipo::Ciudad,
156        PuebloTipo::Metropolis,
157        PuebloTipo::Prehistorico,
158        PuebloTipo::Costero,
159        PuebloTipo::Medieval,
160        PuebloTipo::Rural,
161    ];
162
163    pub fn label(self) -> &'static str {
164        match self {
165            PuebloTipo::Aldea => "aldea",
166            PuebloTipo::Suburbio => "suburbio",
167            PuebloTipo::Ciudad => "ciudad",
168            PuebloTipo::Metropolis => "metrópolis",
169            PuebloTipo::Prehistorico => "prehistórico",
170            PuebloTipo::Costero => "costero",
171            PuebloTipo::Medieval => "medieval",
172            PuebloTipo::Rural => "rural",
173        }
174    }
175
176    pub fn next(self) -> PuebloTipo {
177        let i = PuebloTipo::ALL.iter().position(|&t| t == self).unwrap_or(0);
178        PuebloTipo::ALL[(i + 1) % PuebloTipo::ALL.len()]
179    }
180
181    /// Densidad urbana sugerida `[0,1]`: cuánto se agrupan las casas (cluster fbm).
182    pub fn densidad(self) -> f32 {
183        match self {
184            PuebloTipo::Aldea => 0.25,
185            PuebloTipo::Suburbio => 0.5,
186            PuebloTipo::Ciudad => 0.7,
187            PuebloTipo::Metropolis => 0.9,
188            PuebloTipo::Prehistorico => 0.3,
189            PuebloTipo::Costero => 0.4,
190            PuebloTipo::Medieval => 0.55,
191            PuebloTipo::Rural => 0.15,
192        }
193    }
194
195    /// Mezcla de casas (estilo + probabilidad por parcela) característica del tipo.
196    pub fn casas(self) -> Vec<CasaUso> {
197        let c = |tipo, densidad| CasaUso { tipo, densidad };
198        match self {
199            PuebloTipo::Aldea => vec![
200                c(CasaTipo::Cabana, 0.4),
201                c(CasaTipo::Adobe, 0.3),
202                c(CasaTipo::Granja, 0.2),
203                c(CasaTipo::Choza, 0.2),
204            ],
205            PuebloTipo::Suburbio => vec![
206                c(CasaTipo::Duplex, 0.45),
207                c(CasaTipo::Moderna, 0.4),
208                c(CasaTipo::Tienda, 0.15),
209                c(CasaTipo::Granja, 0.1),
210            ],
211            PuebloTipo::Ciudad => vec![
212                c(CasaTipo::Edificio, 0.45),
213                c(CasaTipo::Moderna, 0.3),
214                c(CasaTipo::Tienda, 0.25),
215                c(CasaTipo::Duplex, 0.2),
216            ],
217            PuebloTipo::Metropolis => vec![
218                c(CasaTipo::Rascacielos, 0.4),
219                c(CasaTipo::Edificio, 0.5),
220                c(CasaTipo::Moderna, 0.2),
221                c(CasaTipo::Tienda, 0.2),
222            ],
223            PuebloTipo::Prehistorico => vec![
224                c(CasaTipo::Flintstone, 0.4),
225                c(CasaTipo::Choza, 0.3),
226                c(CasaTipo::Hobbit, 0.25),
227            ],
228            PuebloTipo::Costero => vec![
229                c(CasaTipo::Palafito, 0.4),
230                c(CasaTipo::Choza, 0.3),
231                c(CasaTipo::Cabana, 0.2),
232                c(CasaTipo::Faro, 0.06),
233            ],
234            PuebloTipo::Medieval => vec![
235                c(CasaTipo::Adobe, 0.4),
236                c(CasaTipo::Cabana, 0.3),
237                c(CasaTipo::Torre, 0.15),
238                c(CasaTipo::Granja, 0.2),
239            ],
240            PuebloTipo::Rural => vec![
241                c(CasaTipo::Granja, 0.4),
242                c(CasaTipo::Cabana, 0.3),
243                c(CasaTipo::Choza, 0.15),
244            ],
245        }
246    }
247}
248
249/// Deriva una semilla de casa estable de una parcela de mundo `(cwx, cwz)` y la
250/// semilla del mundo → cada parcela genera una casa distinta pero determinista.
251pub fn casa_seed(world_seed: u32, cwx: i32, cwz: i32) -> u32 {
252    world_seed
253        .wrapping_add((cwx as u32).wrapping_mul(0x9E37_79B9))
254        .wrapping_add((cwz as u32).wrapping_mul(0x85EB_CA77))
255        ^ 0xCA5A
256}
257
258/// Hash entero determinista `[0, m)` a partir de `(seed, salt)`.
259#[inline]
260fn h(seed: u32, salt: u32, m: u32) -> u32 {
261    let mut x = seed.wrapping_mul(0x9E37_79B9).wrapping_add(salt.wrapping_mul(0x85EB_CA77));
262    x ^= x >> 15;
263    x = x.wrapping_mul(0x2C1B_3C6D);
264    x ^= x >> 13;
265    x % m.max(1)
266}
267
268/// Ruido `[0,1)` determinista para jitter fino.
269#[inline]
270fn hf(seed: u32, a: i32, b: i32) -> f32 {
271    let mut x = seed
272        .wrapping_add((a as u32).wrapping_mul(0x9E37_79B9))
273        .wrapping_add((b as u32).wrapping_mul(0x85EB_CA77));
274    x ^= x >> 15;
275    x = x.wrapping_mul(0x2C1B_3C6D);
276    x ^= x >> 12;
277    (x & 0xFFFF) as f32 / 65536.0
278}
279
280/// **Genera y estampa una casa** del `tipo` con la base (esquina/centro) en
281/// `(x, y, z)`, variada por `seed`. El piso arranca en `y`.
282pub fn stamp_casa(g: &mut VoxelGrid, tipo: CasaTipo, x: u32, y: u32, z: u32, seed: u32) {
283    let bx = x as i32;
284    let by = y as i32;
285    let bz = z as i32;
286    let dim = g.dim();
287    // **Simétrica por construcción**: cada voxel va también a su espejo en `x`
288    // (el frente mira a `+Z`, así que izquierda y derecha son `±dx`). Lo que un
289    // estilo pone de un solo lado —la chimenea, el garaje, el tanque— sale de
290    // los dos, que es como construye la gente; y lo que varía al azar por
291    // columna queda igual a los dos lados porque el último que escribe escribe
292    // en ambos. Ver `toda_casa_es_simetrica_izquierda_derecha`.
293    let mut put = |dx: i32, dy: i32, dz: i32, mat: Material| {
294        for dx in [dx, -dx] {
295            let (px, py, pz) = (bx + dx, by + dy, bz + dz);
296            if px >= 0 && py >= 0 && pz >= 0 && (px as u32) < dim[0] && (py as u32) < dim[1] && (pz as u32) < dim[2] {
297                g.set_rgba(px as u32, py as u32, pz as u32, mat.color(), VoxelGrid::shine_to_alpha(mat.brillo()));
298            }
299        }
300    };
301
302    match tipo {
303        CasaTipo::Flintstone => {
304            // Choza de piedra abovedada, con silueta lumpy y arco de entrada.
305            let w = 4 + h(seed, 1, 3) as i32; // 4..6 (medio ancho)
306            let d = 4 + h(seed, 2, 3) as i32;
307            let base_h = 4 + h(seed, 3, 2) as i32;
308            let piedra = [Material::Arenisca, Material::Grava, Material::Rock][h(seed, 9, 3) as usize];
309            for dx in -w..=w {
310                for dz in -d..=d {
311                    let borde = dx == -w || dx == w || dz == -d || dz == d;
312                    if borde {
313                        // Altura de muro con bulto (piedra irregular).
314                        let bulto = (hf(seed, dx, dz) * 2.5) as i32;
315                        let alto = base_h + bulto;
316                        for dy in 0..alto {
317                            // Arco de puerta en el frente (dz = d).
318                            let puerta = dz == d && dx.abs() <= 1 && dy < 3;
319                            if !puerta {
320                                put(dx, dy, dz, piedra);
321                            }
322                        }
323                    }
324                }
325            }
326            // Techo abovedado (cúpula chata de piedra/paja).
327            let techo = if h(seed, 4, 2) == 0 { Material::Paja } else { piedra };
328            for dx in -w..=w {
329                for dz in -d..=d {
330                    let r = (dx * dx) as f32 / (w * w) as f32 + (dz * dz) as f32 / (d * d) as f32;
331                    let cima = ((1.0 - r) * 3.0) as i32;
332                    if cima >= 0 {
333                        put(dx, base_h + cima, dz, techo);
334                    }
335                }
336            }
337        }
338
339        CasaTipo::Hobbit => {
340            // Montículo de pasto con puerta y ventana redondas.
341            let r = 5 + h(seed, 1, 3) as i32; // radio 5..7
342            let past = [Material::Grass, Material::Musgo, Material::Leaf][h(seed, 8, 3) as usize];
343            for dx in -r..=r {
344                for dy in 0..=r {
345                    for dz in -r..=r {
346                        let dd = dx * dx + dy * dy + dz * dz;
347                        // Cáscara de la media esfera (montículo).
348                        if dd <= r * r && dd > (r - 2) * (r - 2) {
349                            put(dx, dy, dz, past);
350                        }
351                    }
352                }
353            }
354            // Fachada + puerta redonda al frente (dz = r-1..r).
355            let zf = r;
356            for dx in -2..=2 {
357                for dy in 0..4 {
358                    if dx * dx + (dy - 1) * (dy - 1) <= 4 {
359                        put(dx, dy, zf - 1, Material::Ladrillo); // marco de la puerta
360                    }
361                }
362            }
363            for dx in -1..=1 {
364                for dy in 0..3 {
365                    put(dx, dy, zf, Material::Madera); // puerta redonda de madera
366                }
367            }
368            put(3, 2, zf - 1, Material::Vidrio); // ventanita redonda
369            put(-3, 2, zf - 1, Material::Vidrio);
370            // Chimenea.
371            put(0, r + 1, 0, Material::Ladrillo);
372            put(0, r + 2, 0, Material::Ladrillo);
373        }
374
375        CasaTipo::Cabana => {
376            // Cabaña de troncos con techo a dos aguas y chimenea de piedra.
377            let w = 3 + h(seed, 1, 3) as i32; // 3..5
378            let d = 4 + h(seed, 2, 3) as i32; // 4..6
379            let wall = 5;
380            for dy in 0..wall {
381                for dx in -w..=w {
382                    for dz in -d..=d {
383                        let borde = dx == -w || dx == w || dz == -d || dz == d;
384                        if borde {
385                            // Puerta al frente + ventanas.
386                            let puerta = dz == d && dx.abs() <= 1 && dy < 3;
387                            let ventana = (dx == -w || dx == w) && dy == 2 && dz.abs() == d / 2;
388                            let m = if puerta {
389                                continue;
390                            } else if ventana {
391                                Material::Vidrio
392                            } else if dy % 2 == 0 {
393                                Material::Madera // troncos (franjas)
394                            } else {
395                                Material::Adobe
396                            };
397                            put(dx, dy, dz, m);
398                        }
399                    }
400                }
401            }
402            // Techo a dos aguas (cumbrera a lo largo del eje más largo).
403            let teja = if h(seed, 4, 2) == 0 { Material::Teja } else { Material::Pizarra };
404            let ridge_x = w >= d;
405            for dx in -w - 1..=w + 1 {
406                for dz in -d - 1..=d + 1 {
407                    let slope = if ridge_x { d + 1 - dz.abs() } else { w + 1 - dx.abs() }.max(0);
408                    put(dx, wall + slope, dz, teja);
409                }
410            }
411            // Chimenea de piedra a un lado (varía).
412            let cs = if h(seed, 5, 2) == 0 { -w } else { w };
413            for dy in 0..wall + d + 2 {
414                put(cs, dy, -d + 1, Material::Rock);
415            }
416        }
417
418        CasaTipo::Moderna => {
419            // Casa moderna: hormigón + grandes ventanales, techo plano, 1–2 plantas.
420            let w = 4 + h(seed, 1, 3) as i32;
421            let d = 4 + h(seed, 2, 3) as i32;
422            let plantas = 1 + h(seed, 3, 2) as i32; // 1..2
423            let ph = 5; // alto de planta
424            let top = plantas * ph;
425            for dy in 0..top {
426                for dx in -w..=w {
427                    for dz in -d..=d {
428                        let borde = dx == -w || dx == w || dz == -d || dz == d;
429                        if borde {
430                            let piso_line = dy % ph == 0;
431                            // Ventanal: franjas de vidrio entre líneas de piso.
432                            let ventanal = !piso_line && (dy % ph) != ph - 1;
433                            let puerta = dz == d && dx.abs() <= 1 && dy < 3;
434                            if puerta {
435                                continue;
436                            }
437                            let m = if piso_line {
438                                Material::Hormigon
439                            } else if ventanal {
440                                Material::Vidrio
441                            } else {
442                                Material::Yeso
443                            };
444                            put(dx, dy, dz, m);
445                        }
446                    }
447                }
448            }
449            // Losa/techo plano.
450            for dx in -w..=w {
451                for dz in -d..=d {
452                    put(dx, top, dz, Material::Hormigon);
453                }
454            }
455            // Parapeto + a veces un panel solar.
456            if h(seed, 6, 2) == 0 {
457                put(0, top + 1, 0, Material::Azul);
458                put(1, top + 1, 0, Material::Azul);
459            }
460        }
461
462        CasaTipo::Edificio => {
463            // Bloque de departamentos: alto, con grilla de ventanas.
464            let w = 3 + h(seed, 1, 3) as i32;
465            let d = 3 + h(seed, 2, 2) as i32;
466            let pisos = 5 + h(seed, 3, 5) as i32; // 5..9
467            let ph = 3;
468            let top = pisos * ph;
469            let muro = [Material::Ladrillo, Material::Hormigon, Material::Arcilla][h(seed, 7, 3) as usize];
470            for dy in 0..top {
471                for dx in -w..=w {
472                    for dz in -d..=d {
473                        let borde = dx == -w || dx == w || dz == -d || dz == d;
474                        if borde {
475                            // Ventana en cada piso (mitad de la altura), columnas alternas.
476                            let ventana = dy > 0 && dy % ph == 1 && (dx + dz).rem_euclid(2) == 0;
477                            let puerta = dz == d && dx.abs() <= 1 && dy < 2;
478                            if puerta {
479                                continue;
480                            }
481                            put(dx, dy, dz, if ventana { Material::Vidrio } else { muro });
482                        }
483                    }
484                }
485            }
486            for dx in -w..=w {
487                for dz in -d..=d {
488                    put(dx, top, dz, Material::Hormigon); // azotea
489                }
490            }
491            // Tanque/antena.
492            put(-w + 1, top + 1, -d + 1, Material::Acero);
493        }
494
495        CasaTipo::Iglu => {
496            let r = 4 + h(seed, 1, 2) as i32; // 4..5
497            for dx in -r..=r {
498                for dy in 0..=r {
499                    for dz in -r..=r {
500                        let dd = dx * dx + dy * dy + dz * dz;
501                        if dd <= r * r && dd > (r - 2) * (r - 2) {
502                            put(dx, dy, dz, Material::Snow); // cúpula de nieve
503                        }
504                    }
505                }
506            }
507            for dz in r..=r + 2 {
508                for dx in -1..=1 {
509                    for dy in 0..3 {
510                        if dx * dx + (dy - 1) * (dy - 1) <= 2 {
511                            put(dx, dy, dz, Material::Hielo); // túnel
512                        }
513                    }
514                }
515            }
516        }
517
518        CasaTipo::Palafito => {
519            let w = 3 + h(seed, 1, 2) as i32;
520            let d = 3 + h(seed, 2, 2) as i32;
521            let alto = 4;
522            for &(cx, cz) in &[(-w, -d), (w, -d), (-w, d), (w, d)] {
523                for dy in 0..alto {
524                    put(cx, dy, cz, Material::Madera); // pilotes
525                }
526            }
527            for dx in -w..=w {
528                for dz in -d..=d {
529                    put(dx, alto, dz, Material::Madera); // plataforma
530                }
531            }
532            for dy in (alto + 1)..(alto + 5) {
533                for dx in -w..=w {
534                    for dz in -d..=d {
535                        let borde = dx == -w || dx == w || dz == -d || dz == d;
536                        if borde {
537                            if dz == d && dx.abs() <= 1 && dy < alto + 3 {
538                                continue; // puerta
539                            }
540                            let ventana = (dx == -w || dx == w) && dy == alto + 2;
541                            put(dx, dy, dz, if ventana { Material::Vidrio } else { Material::Madera });
542                        }
543                    }
544                }
545            }
546            for dx in (-w - 1)..=(w + 1) {
547                for dz in (-d - 1)..=(d + 1) {
548                    let slope = (d + 1 - dz.abs()).max(0);
549                    put(dx, alto + 5 + slope, dz, Material::Paja);
550                }
551            }
552            for dy in 0..alto {
553                put(0, dy, d + 1, Material::Madera); // escalera
554            }
555        }
556
557        CasaTipo::Adobe => {
558            let w = 3 + h(seed, 1, 3) as i32;
559            let d = 3 + h(seed, 2, 3) as i32;
560            let h1 = 4;
561            for dy in 0..h1 {
562                for dx in -w..=w {
563                    for dz in -d..=d {
564                        let borde = dx == -w || dx == w || dz == -d || dz == d;
565                        if borde {
566                            if dz == d && dx.abs() <= 1 && dy < 3 {
567                                continue; // puerta
568                            }
569                            let ventana = dy == 2 && dx.abs() == w && dz.abs() == d / 2;
570                            put(dx, dy, dz, if ventana { Material::Vidrio } else { Material::Adobe });
571                        }
572                    }
573                }
574            }
575            for dx in -w..=w {
576                for dz in -d..=d {
577                    put(dx, h1, dz, Material::Adobe); // techo plano
578                }
579            }
580            let mut vx = -w;
581            while vx <= w {
582                put(vx, h1 - 1, d + 1, Material::Madera); // vigas que sobresalen
583                put(vx, h1 - 1, -d - 1, Material::Madera);
584                vx += 2;
585            }
586            let ow = (w - 1).max(1);
587            for dy in h1..(h1 + 3) {
588                for dx in -ow..=(ow - 1) {
589                    for dz in -ow..=(ow - 1) {
590                        let borde = dx == -ow || dx == ow - 1 || dz == -ow || dz == ow - 1;
591                        if borde {
592                            put(dx, dy, dz, Material::Arcilla); // segundo cuerpo apilado
593                        }
594                    }
595                }
596            }
597            for dx in -ow..=(ow - 1) {
598                for dz in -ow..=(ow - 1) {
599                    put(dx, h1 + 3, dz, Material::Adobe);
600                }
601            }
602        }
603
604        CasaTipo::Rascacielos => {
605            let ht = 14 + h(seed, 1, 12) as i32; // 14..25
606            let w = 2 + h(seed, 2, 2) as i32; // 2..3
607            for dy in 0..ht {
608                for dx in -w..=w {
609                    for dz in -w..=w {
610                        let borde = dx == -w || dx == w || dz == -w || dz == w;
611                        if borde {
612                            let esquina = dx.abs() == w && dz.abs() == w;
613                            let banda = dy % 4 == 0;
614                            put(dx, dy, dz, if esquina || banda { Material::Acero } else { Material::Vidrio });
615                        }
616                    }
617                }
618            }
619            for dx in -w..=w {
620                for dz in -w..=w {
621                    put(dx, ht, dz, Material::Hormigon);
622                }
623            }
624            put(0, ht + 1, 0, Material::Acero);
625            put(0, ht + 2, 0, Material::Acero);
626        }
627
628        CasaTipo::Granja => {
629            let w = 3i32;
630            let d = 3i32;
631            for dy in 0..5 {
632                for dx in -w..=w {
633                    for dz in -d..=d {
634                        let borde = dx == -w || dx == w || dz == -d || dz == d;
635                        if borde {
636                            if dz == d && dx.abs() <= 1 && dy < 3 {
637                                continue; // puerta
638                            }
639                            let ventana = dy == 2 && dx.abs() == w && dz == 0;
640                            put(dx, dy, dz, if ventana { Material::Vidrio } else { Material::Yeso });
641                        }
642                    }
643                }
644            }
645            for dx in (-w - 1)..=(w + 1) {
646                for dz in (-d - 1)..=(d + 1) {
647                    let slope = (d + 1 - dz.abs()).max(0);
648                    put(dx, 5 + slope, dz, Material::Teja);
649                }
650            }
651            for dy in 0..7 {
652                put(w, dy, -d + 1, Material::Ladrillo); // chimenea
653            }
654            // Granero al costado.
655            let cx = w + 5;
656            for dy in 0..6 {
657                for dx in (cx - 3)..=(cx + 3) {
658                    for dz in -3..=3 {
659                        let borde = dx == cx - 3 || dx == cx + 3 || dz == -3 || dz == 3;
660                        if borde {
661                            if dz == 3 && (dx - cx).abs() <= 1 && dy < 4 {
662                                continue; // portón
663                            }
664                            put(dx, dy, dz, Material::Ladrillo);
665                        }
666                    }
667                }
668            }
669            for dx in (cx - 4)..=(cx + 4) {
670                for dz in -4i32..=4 {
671                    let slope = (4 - dz.abs()).max(0);
672                    put(dx, 6 + slope, dz, Material::Teja);
673                }
674            }
675        }
676
677        CasaTipo::Torre => {
678            // Torreón de piedra: planta cuadrada, alto variable, almenas arriba.
679            let r = 2 + h(seed, 1, 2) as i32; // 2..3
680            let alto = 8 + h(seed, 2, 6) as i32; // 8..13
681            let piedra = [Material::Granito, Material::Arenisca, Material::Basalto][h(seed, 7, 3) as usize];
682            for dy in 0..alto {
683                for dx in -r..=r {
684                    for dz in -r..=r {
685                        let borde = dx == -r || dx == r || dz == -r || dz == r;
686                        if borde {
687                            if dz == r && dx == 0 && dy < 3 {
688                                continue; // portón
689                            }
690                            // Aspilleras: ranuras verticales angostas.
691                            let aspillera = dy > 2 && dy % 3 == 0 && (dx == 0 || dz == 0);
692                            put(dx, dy, dz, if aspillera { Material::Vidrio } else { piedra });
693                        }
694                    }
695                }
696            }
697            // Almenas: dientes alternados en el borde superior.
698            for dx in -r..=r {
699                for dz in -r..=r {
700                    let borde = dx == -r || dx == r || dz == -r || dz == r;
701                    if borde && (dx + dz).rem_euclid(2) == 0 {
702                        put(dx, alto, dz, piedra);
703                    }
704                }
705            }
706        }
707
708        CasaTipo::Faro => {
709            // Faro: torre cilíndrica con franjas blancas y rojas + linterna acristalada.
710            let alto = 12 + h(seed, 1, 6) as i32; // 12..17
711            let r = 3;
712            for dy in 0..alto {
713                let banda = (dy / 3) % 2 == 0;
714                let piel = if banda { Material::Yeso } else { Material::Ladrillo };
715                for dx in -r..=r {
716                    for dz in -r..=r {
717                        // Aproximación de círculo por radio.
718                        let dist2 = dx * dx + dz * dz;
719                        let anillo = dist2 <= r * r && dist2 > (r - 1) * (r - 1);
720                        if anillo {
721                            if dz == r - 0 && dx == 0 && dy < 3 {
722                                continue; // puerta
723                            }
724                            put(dx, dy, dz, piel);
725                        }
726                    }
727                }
728            }
729            // Galería + linterna de vidrio.
730            for dx in -r..=r {
731                for dz in -r..=r {
732                    if dx * dx + dz * dz <= r * r {
733                        put(dx, alto, dz, Material::Acero);
734                    }
735                }
736            }
737            for dy in (alto + 1)..(alto + 4) {
738                for dx in -(r - 1)..=(r - 1) {
739                    for dz in -(r - 1)..=(r - 1) {
740                        let dist2 = dx * dx + dz * dz;
741                        if dist2 <= (r - 1) * (r - 1) && dist2 > (r - 2).max(0) * (r - 2).max(0) {
742                            put(dx, dy, dz, Material::Vidrio);
743                        }
744                    }
745                }
746            }
747            put(0, alto + 4, 0, Material::Bloom); // remate de la linterna
748        }
749
750        CasaTipo::Choza => {
751            // Choza tropical: paredes bajas de adobe/madera + techo cónico de paja.
752            let r = 2 + h(seed, 1, 2) as i32; // 2..3
753            let pared = if h(seed, 5, 2) == 0 { Material::Adobe } else { Material::Madera };
754            for dy in 0..3 {
755                for dx in -r..=r {
756                    for dz in -r..=r {
757                        let borde = dx == -r || dx == r || dz == -r || dz == r;
758                        if borde {
759                            if dz == r && dx == 0 && dy < 2 {
760                                continue; // vano
761                            }
762                            put(dx, dy, dz, pared);
763                        }
764                    }
765                }
766            }
767            // Techo cónico de paja: capas que se cierran hacia la punta.
768            let mut rr = r + 1;
769            let mut dy = 3;
770            while rr >= 0 {
771                for dx in -rr..=rr {
772                    for dz in -rr..=rr {
773                        if dx.abs() == rr || dz.abs() == rr || rr == 0 {
774                            put(dx, dy, dz, Material::Paja);
775                        }
776                    }
777                }
778                rr -= 1;
779                dy += 1;
780            }
781        }
782
783        CasaTipo::Tienda => {
784            // Local a pie de calle: caja baja, vidriera al frente, toldo y cartel.
785            let w = 4 + h(seed, 1, 3) as i32;
786            let d = 3 + h(seed, 2, 2) as i32;
787            let alto = 4;
788            let muro = [Material::Ladrillo, Material::Yeso, Material::Arcilla][h(seed, 7, 3) as usize];
789            for dy in 0..alto {
790                for dx in -w..=w {
791                    for dz in -d..=d {
792                        let borde = dx == -w || dx == w || dz == -d || dz == d;
793                        if borde {
794                            // Frente (dz == d): vidriera grande + puerta.
795                            if dz == d && dy < 3 {
796                                if dx.abs() <= w - 1 {
797                                    put(dx, dy, dz, Material::Vidrio);
798                                }
799                                continue;
800                            }
801                            put(dx, dy, dz, muro);
802                        }
803                    }
804                }
805            }
806            // Techo plano + cartel.
807            for dx in -w..=w {
808                for dz in -d..=d {
809                    put(dx, alto, dz, Material::Hormigon);
810                }
811            }
812            // Toldo: alero rayado sobre la vidriera.
813            let toldo = [Material::Ladrillo, Material::Bloom, Material::Azul][h(seed, 8, 3) as usize];
814            for dx in -w..=w {
815                put(dx, 3, d + 1, if dx.rem_euclid(2) == 0 { toldo } else { Material::Yeso });
816            }
817            // Cartel sobre el toldo.
818            for dx in -(w - 1)..=(w - 1) {
819                put(dx, alto + 1, d, muro);
820            }
821        }
822
823        CasaTipo::Duplex => {
824            // Dúplex suburbano: dos plantas, techo a dos aguas, garaje anexo.
825            let w = 4 + h(seed, 1, 2) as i32;
826            let d = 4 + h(seed, 2, 2) as i32;
827            let alto = 8; // dos plantas de 4
828            let muro = [Material::Yeso, Material::Ladrillo, Material::Arcilla][h(seed, 7, 3) as usize];
829            for dy in 0..alto {
830                for dx in -w..=w {
831                    for dz in -d..=d {
832                        let borde = dx == -w || dx == w || dz == -d || dz == d;
833                        if borde {
834                            let puerta = dz == d && dx.abs() <= 1 && dy < 3;
835                            if puerta {
836                                continue;
837                            }
838                            // Ventanas: una por planta a media altura.
839                            let ventana = (dy == 2 || dy == 6) && (dx.abs() == 2 || dz.abs() == 2);
840                            put(dx, dy, dz, if ventana { Material::Vidrio } else { muro });
841                        }
842                    }
843                }
844            }
845            // Techo a dos aguas de teja.
846            for dx in (-w - 1)..=(w + 1) {
847                for dz in (-d - 1)..=(d + 1) {
848                    let slope = (d + 1 - dz.abs()).max(0);
849                    put(dx, alto + slope, dz, Material::Teja);
850                }
851            }
852            // Garaje anexo bajo.
853            let gx = w + 4;
854            for dy in 0..4 {
855                for dx in (gx - 3)..=(gx + 3) {
856                    for dz in -3..=3 {
857                        let borde = dx == gx - 3 || dx == gx + 3 || dz == -3 || dz == 3;
858                        if borde {
859                            if dz == 3 && (dx - gx).abs() <= 2 && dy < 3 {
860                                continue; // portón del garaje
861                            }
862                            put(dx, dy, dz, muro);
863                        }
864                    }
865                }
866            }
867            for dx in (gx - 3)..=(gx + 3) {
868                for dz in -3..=3 {
869                    put(dx, 4, dz, Material::Hormigon);
870                }
871            }
872        }
873
874        CasaTipo::Palacio => palacio(g, (bx, by, bz), seed),
875        CasaTipo::Kiosko => kiosko(g, (bx, by, bz), seed),
876    }
877}
878
879/// Un color con su brillo, de un material o de un mineral con nombre.
880type Tinte = ([u8; 3], f32);
881
882fn de_mineral(m: Mineral) -> Tinte {
883    (m.color(), m.brillo())
884}
885
886fn de_material(m: Material) -> Tinte {
887    (m.color(), m.brillo())
888}
889
890/// Pone un voxel relativo a la base, si cae en la grilla — y su espejo en `x`,
891/// por la misma regla que el `put` de [`stamp_casa`]: toda construcción es
892/// simétrica izquierda-derecha.
893fn pon(g: &mut VoxelGrid, base: (i32, i32, i32), dx: i32, dy: i32, dz: i32, t: Tinte) {
894    let dim = g.dim();
895    for dx in [dx, -dx] {
896        let (px, py, pz) = (base.0 + dx, base.1 + dy, base.2 + dz);
897        if px >= 0 && py >= 0 && pz >= 0 && (px as u32) < dim[0] && (py as u32) < dim[1] && (pz as u32) < dim[2] {
898            g.set_rgba(px as u32, py as u32, pz as u32, t.0, VoxelGrid::shine_to_alpha(t.1));
899        }
900    }
901}
902
903/// El palacio: escalinata de mármol, cuerpo de mármol con ventanales de
904/// diamante y portal de oro, cuatro torres con aguja de diamante, cúpula de
905/// diamante y remate de oro. El frente mira a `+Z`, como las demás.
906fn palacio(g: &mut VoxelGrid, b: (i32, i32, i32), seed: u32) {
907    let marmol = de_material(Material::Marmol);
908    let diamante = de_mineral(Mineral::Diamante);
909    let oro = de_mineral(Mineral::Oro);
910    // Grande: el rey y la princesa miden unos cuatro voxels, y un palacio que
911    // les llega a la cintura de las torres se lee como una casa con agujas.
912    let w = 15 + h(seed, 1, 2) as i32; // medio ancho del cuerpo: 15..16
913    let d = 12;
914    let alto = 12 + h(seed, 2, 2) as i32; // 12..13
915    // Escalinata: dos gradas de mármol más anchas que el cuerpo, la de abajo
916    // asoma al frente.
917    for (dy, extra, frente) in [(0, 3, 4), (1, 2, 2)] {
918        for dx in -(w + extra)..=(w + extra) {
919            for dz in -(d + extra)..=(d + frente) {
920                pon(g, b, dx, dy, dz, marmol);
921            }
922        }
923    }
924    let y0 = 2;
925    // Cuerpo: muros de mármol; ventanales de diamante a media altura; portal
926    // de tres de ancho con marco de oro.
927    for dy in 0..alto {
928        for dx in -w..=w {
929            for dz in -d..=d {
930                let borde = dx == -w || dx == w || dz == -d || dz == d;
931                if !borde {
932                    continue;
933                }
934                let portal = dz == d && dx.abs() <= 2 && dy < 6;
935                if portal {
936                    continue;
937                }
938                let marco = dz == d && dx.abs() == 3 && dy < 7 || dz == d && dx.abs() <= 3 && dy == 6;
939                // Dos hileras de ventanales: la planta baja y la alta.
940                let fila = (3..=5).contains(&dy) || (8..=10).contains(&dy);
941                let ventana = fila && (dx.rem_euclid(3) == 0 || dz.rem_euclid(3) == 0) && !(dx.abs() == w && dz.abs() == d);
942                let t = if marco { oro } else if ventana { diamante } else { marmol };
943                pon(g, b, dx, y0 + dy, dz, t);
944            }
945        }
946    }
947    // Techo plano de mármol con cornisa de oro.
948    for dx in -(w + 1)..=(w + 1) {
949        for dz in -(d + 1)..=(d + 1) {
950            let cornisa = dx.abs() == w + 1 || dz.abs() == d + 1;
951            pon(g, b, dx, y0 + alto, dz, if cornisa { oro } else { marmol });
952        }
953    }
954    // Cuatro torres en las esquinas, más altas que el cuerpo, con aguja de diamante.
955    let r = 3;
956    let alto_torre = alto + 8;
957    for (sx, sz) in [(-1, -1), (1, -1), (-1, 1), (1, 1)] {
958        let (cx, cz) = (sx * w, sz * d);
959        for dy in 0..alto_torre {
960            for dx in -r..=r {
961                for dz in -r..=r {
962                    if dx * dx + dz * dz <= r * r + 1 {
963                        let t = if dy == alto_torre - 1 { oro } else { marmol };
964                        pon(g, b, cx + dx, y0 + dy, cz + dz, t);
965                    }
966                }
967            }
968        }
969        // Aguja: cono de diamante que se afina en seis escalones.
970        for k in 0..6 {
971            let rr = (r - k / 2).max(0);
972            for dx in -rr..=rr {
973                for dz in -rr..=rr {
974                    if dx * dx + dz * dz <= rr * rr {
975                        pon(g, b, cx + dx, y0 + alto_torre + k, cz + dz, diamante);
976                    }
977                }
978            }
979        }
980        pon(g, b, cx, y0 + alto_torre + 6, cz, oro);
981    }
982    // Cúpula central de diamante sobre el techo, con remate de oro.
983    let rc = 8;
984    let yc = y0 + alto + 1;
985    for dx in -rc..=rc {
986        for dy in 0..=rc {
987            for dz in -rc..=rc {
988                let dd = dx * dx + dy * dy + dz * dz;
989                if dd <= rc * rc && dd > (rc - 1) * (rc - 1) {
990                    pon(g, b, dx, yc + dy, dz, diamante);
991                }
992            }
993        }
994    }
995    pon(g, b, 0, yc + rc + 1, 0, oro);
996    pon(g, b, 0, yc + rc + 2, 0, oro);
997}
998
999/// El kiosko: piso de mármol, ocho columnas y un domo bajo de malaquita (franjas
1000/// de cobre y jade, que es lo que la malaquita es), con borde y remate de oro.
1001/// Abierto: se ve a través y se puede estar adentro.
1002fn kiosko(g: &mut VoxelGrid, b: (i32, i32, i32), seed: u32) {
1003    let marmol = de_material(Material::Marmol);
1004    let oro = de_mineral(Mineral::Oro);
1005    let malaquita = |dy: i32| de_mineral(if dy.rem_euclid(2) == 0 { Mineral::Cobre } else { Mineral::Jade });
1006    let r = 6 + h(seed, 1, 2) as i32; // 6..7
1007    // Piso redondo de mármol, una grada.
1008    for dx in -(r + 1)..=(r + 1) {
1009        for dz in -(r + 1)..=(r + 1) {
1010            if dx * dx + dz * dz <= (r + 1) * (r + 1) {
1011                pon(g, b, dx, 0, dz, marmol);
1012            }
1013        }
1014    }
1015    // Ocho columnas en el borde.
1016    let alto = 6;
1017    for i in 0..8 {
1018        let a = i as f32 * std::f32::consts::TAU / 8.0;
1019        let (cx, cz) = ((a.cos() * r as f32).round() as i32, (a.sin() * r as f32).round() as i32);
1020        for dy in 1..=alto {
1021            pon(g, b, cx, dy, cz, malaquita(dy));
1022        }
1023    }
1024    // Aro de oro arriba de las columnas.
1025    for dx in -r..=r {
1026        for dz in -r..=r {
1027            let dd = dx * dx + dz * dz;
1028            if dd <= r * r && dd > (r - 1) * (r - 1) {
1029                pon(g, b, dx, alto + 1, dz, oro);
1030            }
1031        }
1032    }
1033    // Domo bajo (media esfera achatada) de malaquita en franjas.
1034    for dx in -r..=r {
1035        for dy in 0..=r {
1036            for dz in -r..=r {
1037                let dd = dx * dx + (dy * 2) * (dy * 2) + dz * dz;
1038                if dd <= r * r && dd > (r - 1) * (r - 1) {
1039                    pon(g, b, dx, alto + 2 + dy, dz, malaquita(dy));
1040                }
1041            }
1042        }
1043    }
1044    let cima = alto + 2 + r / 2 + 1;
1045    pon(g, b, 0, cima, 0, oro);
1046    pon(g, b, 0, cima + 1, 0, oro);
1047}
1048
1049/// **Planta una casa a mano** en la columna `(x, z)` de la grilla: busca el
1050/// suelo, despeja lo que haya encima en su [`huella`](CasaTipo::huella) (árboles,
1051/// pasto, otra casa) y rellena lo que falte abajo, así la casa queda apoyada en
1052/// un piso parejo, y después la estampa. Es lo que usa un decorado de escena:
1053/// la generación procedural nivela su propio casco urbano, pero una casa
1054/// puesta en un punto cualquiera cae sobre el relieve que haya.
1055///
1056/// Devuelve la `y` del piso, o `None` si la columna no tiene suelo.
1057pub fn plantar_casa(g: &mut VoxelGrid, tipo: CasaTipo, x: u32, z: u32, seed: u32) -> Option<u32> {
1058    plantar_casa_girada(g, tipo, x, z, seed, 0)
1059}
1060
1061/// Igual que [`plantar_casa`], con el frente girado `giro` cuartos de vuelta
1062/// alrededor de su centro (`0` = a `+Z`, `1` = a `+X`, `2` = a `-Z`, `3` = a
1063/// `-X`). La casa se genera en una grilla aparte y se copia rotada: el
1064/// generador de cada estilo sigue escribiendo «de frente a +Z» sin saber nada.
1065pub fn plantar_casa_girada(g: &mut VoxelGrid, tipo: CasaTipo, x: u32, z: u32, seed: u32, giro: u8) -> Option<u32> {
1066    let dim = g.dim();
1067    if x >= dim[0] || z >= dim[2] {
1068        return None;
1069    }
1070    let suelo = g.height_at(x, z)?;
1071    let piso = suelo + 1;
1072    let r = tipo.huella();
1073    for dx in -r..=r {
1074        for dz in -r..=r {
1075            if dx * dx + dz * dz > r * r {
1076                continue;
1077            }
1078            let (px, pz) = (x as i32 + dx, z as i32 + dz);
1079            if px < 0 || pz < 0 || px as u32 >= dim[0] || pz as u32 >= dim[2] {
1080                continue;
1081            }
1082            let (px, pz) = (px as u32, pz as u32);
1083            // El material del suelo de esa columna, para rellenar con lo mismo.
1084            let tope = g.height_at(px, pz);
1085            let relleno = tope.and_then(|y| g.get(px, y, pz)).unwrap_or([120, 110, 100, 255]);
1086            for y in piso..dim[1] {
1087                g.clear(px, y, pz);
1088            }
1089            let desde = tope.map(|y| y + 1).unwrap_or(0);
1090            for y in desde..piso {
1091                g.set_rgba(px, y, pz, [relleno[0], relleno[1], relleno[2]], relleno[3]);
1092            }
1093        }
1094    }
1095    let giro = giro % 4;
1096    if giro == 0 {
1097        stamp_casa(g, tipo, x, piso, z, seed);
1098        return Some(piso);
1099    }
1100    // Una grilla de la huella entera (más un margen: algún estilo asoma un
1101    // voxel de su huella), con el centro en el medio.
1102    let m = r + 4;
1103    let lado = (2 * m + 1) as u32;
1104    let mut aparte = VoxelGrid::new([lado, dim[1], lado]);
1105    stamp_casa(&mut aparte, tipo, m as u32, piso, m as u32, seed);
1106    for ax in 0..lado {
1107        for az in 0..lado {
1108            let (dx, dz) = (ax as i32 - m, az as i32 - m);
1109            // `+Z` gira a `+X` con un cuarto de vuelta: (dx, dz) → (dz, -dx).
1110            let (rx, rz) = match giro {
1111                1 => (dz, -dx),
1112                2 => (-dx, -dz),
1113                _ => (-dz, dx),
1114            };
1115            let (px, pz) = (x as i32 + rx, z as i32 + rz);
1116            if px < 0 || pz < 0 || px as u32 >= dim[0] || pz as u32 >= dim[2] {
1117                continue;
1118            }
1119            for y in 0..dim[1] {
1120                if let Some(c) = aparte.get(ax, y, az).filter(|c| c[3] > 0) {
1121                    g.set_rgba(px as u32, y, pz as u32, [c[0], c[1], c[2]], c[3]);
1122                }
1123            }
1124        }
1125    }
1126    Some(piso)
1127}
1128
1129#[cfg(test)]
1130mod tests {
1131    use super::*;
1132
1133    fn solids(g: &VoxelGrid, dim: [u32; 3]) -> usize {
1134        let mut n = 0;
1135        for x in 0..dim[0] {
1136            for y in 0..dim[1] {
1137                for z in 0..dim[2] {
1138                    if g.is_solid(x as i32, y as i32, z as i32) {
1139                        n += 1;
1140                    }
1141                }
1142            }
1143        }
1144        n
1145    }
1146
1147    #[test]
1148    fn cada_tipo_genera_una_casa() {
1149        let dim = [40u32, 40, 40];
1150        for tipo in CasaTipo::ALL {
1151            let mut g = VoxelGrid::new(dim);
1152            stamp_casa(&mut g, tipo, 20, 1, 20, 7);
1153            assert!(solids(&g, dim) >= 30, "{} generó muy poco", tipo.label());
1154        }
1155    }
1156
1157    #[test]
1158    fn misma_semilla_misma_casa_distinta_semilla_distinta() {
1159        let dim = [40u32, 40, 40];
1160        let build = |seed| {
1161            let mut g = VoxelGrid::new(dim);
1162            stamp_casa(&mut g, CasaTipo::Cabana, 20, 1, 20, seed);
1163            solids(&g, dim)
1164        };
1165        assert_eq!(build(3), build(3), "misma semilla → misma casa");
1166        // Con muchas semillas, al menos algunas dan tamaños distintos.
1167        let n: std::collections::BTreeSet<usize> = (0..8).map(build).collect();
1168        assert!(n.len() > 1, "distintas semillas dan casas distintas");
1169    }
1170
1171    /// **Toda construcción es simétrica**: el lado izquierdo igual que el
1172    /// derecho, visto de frente (el frente mira a `+Z`, así que el espejo es en
1173    /// `x`). Así construyen los humanos, y el operador lo puso como regla sin
1174    /// excepción: una casa torcida se lee como un error del generador, no como
1175    /// un estilo. Se mide voxel por voxel, color incluido, y con varias semillas
1176    /// porque la asimetría suele venir de una variación al azar.
1177    #[test]
1178    fn toda_casa_es_simetrica_izquierda_derecha() {
1179        let dim = [72u32, 96, 72];
1180        let cx = 36u32;
1181        let mut malas = Vec::new();
1182        for tipo in CasaTipo::ALL {
1183            for seed in 0..6 {
1184                let mut g = VoxelGrid::new(dim);
1185                stamp_casa(&mut g, tipo, cx, 1, 36, seed);
1186                let mut distintos = 0;
1187                for x in 0..dim[0] {
1188                    let espejo = 2 * cx as i64 - x as i64;
1189                    if !(0..dim[0] as i64).contains(&espejo) {
1190                        continue;
1191                    }
1192                    for y in 0..dim[1] {
1193                        for z in 0..dim[2] {
1194                            let a = g.get(x, y, z).filter(|c| c[3] > 0);
1195                            let b = g.get(espejo as u32, y, z).filter(|c| c[3] > 0);
1196                            if a != b {
1197                                distintos += 1;
1198                            }
1199                        }
1200                    }
1201                }
1202                if distintos > 0 {
1203                    malas.push(format!("{} (semilla {seed}): {distintos} voxels sin espejo", tipo.label()));
1204                }
1205            }
1206        }
1207        assert!(malas.is_empty(), "construcciones asimétricas:\n{}", malas.join("\n"));
1208    }
1209
1210    #[test]
1211    fn los_presets_de_pueblo_traen_casas_y_densidad() {
1212        for pt in PuebloTipo::ALL {
1213            let casas = pt.casas();
1214            assert!(!casas.is_empty(), "{} sin casas", pt.label());
1215            assert!(casas.iter().all(|c| c.densidad > 0.0), "{} con densidad 0", pt.label());
1216            let d = pt.densidad();
1217            assert!((0.05..=0.95).contains(&d), "{} densidad fuera de rango: {d}", pt.label());
1218        }
1219        // La metrópolis es más densa que la aldea.
1220        assert!(PuebloTipo::Metropolis.densidad() > PuebloTipo::Aldea.densidad());
1221    }
1222}