Rust-Werkstatt
Werkstatt · Baulöwe · Etappe 3 von 10

Hotelketten gründen

Jetzt wird aus der Baustelle eine Stadt. Liegen zwei Häuser nebeneinander, gründest du eine Hotelkette, und jede weitere Parzelle daneben lässt sie wachsen. Sieben Ketten gibt es, jede mit eigener Farbe und eigener Dachform, vom Dachgarten bis zur goldenen Turmspitze. Und je größer eine Kette wird, desto höher ragen ihre Türme.

5 Schritte · ca. 75 Minuten · iced 0.14

Das Spielbrett mit sieben Hotelketten in verschiedenen Farben und Dachformen, rechts oben eine Tafel mit den Kettengrößen
So sieht der Baulöwe am Ende dieser Etappe aus.
Schritt 1

Was steht auf dem Feld?

Bisher merkt sich das Spiel in einer Liste, wo gebaut wurde. Das reicht nicht mehr, denn ab jetzt kann auf einem Feld dreierlei stehen: nichts, ein Rohbau oder ein Hotel einer bestimmten Kette. Für so ein „entweder, oder, oder“ ist in Rust das enum da. Wir beginnen mit zwei Möglichkeiten und legen es in spiel.rs vor die Struktur Spiel:

src/spiel.rsvor // Alles, was die Spielregeln wissen müssen
// Was auf einem Feld steht
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Belegung {
    Frei,
    Rohbau,
}
// Alles, was die Spielregeln wissen müssen

Statt einer Liste gebauter Felder bekommt das Spiel ein Brett: eine Liste mit genau 108 Einträgen, einer pro Feld. Damit wir zu jedem Feld seinen Platz in dieser Liste finden, bekommt Feld eine Methode index. Wir zählen dabei Reihe für Reihe durch, wie beim Lesen: 1A ist Platz 0, 12A ist Platz 11, 1B ist Platz 12.

src/spiel.rsin impl Feld, ganz oben
impl Feld {    // Die Nummer des Feldes in einer Liste mit 108 Einträgen
    pub fn index(self) -> usize {
        self.reihe * SPALTEN + self.spalte
    }
    // Der Name wie auf dem Spielplan: "1A" bis "12I"
src/spiel.rsin struct Spiel und in fn neu
pub struct Spiel {    pub gebaut: Vec<Feld>,    pub brett: Vec<Belegung>,⋮        let mut spiel = Spiel {            gebaut: Vec::new(),            brett: vec![Belegung::Frei; SPALTEN * REIHEN],

vec![Belegung::Frei; 108] baut eine Liste, in der 108-mal dasselbe steht. Damit Rust den Wert so oft kopieren darf, trägt Belegung die Markierungen Clone und Copy. Jetzt noch eine Methode zum Nachschauen und die geänderte Zeile in legen:

src/spiel.rsin impl Spiel, vor fn nachziehen
    // Was steht auf diesem Feld?
    pub fn belegung(&self, feld: Feld) -> Belegung {
        self.brett[feld.index()]
    }
    // Füllt die Hand wieder auf, solange der Stapel reicht
src/spiel.rsin fn legen
        self.hand.remove(stelle);        self.gebaut.push(feld);        self.brett[feld.index()] = Belegung::Rohbau;

In main.rs fragen wir beim Zeichnen jetzt nach der Belegung. Weil es bald drei Fälle gibt, nehmen wir gleich ein match statt eines if. Der Code für das Namensschild rückt dabei eine Stufe ein:

src/main.rsganz oben und am Ende von fn zeichne_parzelle
use spiel::{Feld, REIHEN, SPALTEN, Spiel};use spiel::{Belegung, Feld, REIHEN, SPALTEN, Spiel};⋮        if self.spiel.gebaut.contains(&feld) {
            self.zeichne_rohbau(frame, iso, farben, feld, hoehe);
        } else {
            let schild = Text {
                content: feld.name(),
                position: iso.punkt(s + 0.5, r + 0.5, hoehe),
                color: farben.beschriftung,
                size: 11.0.into(),
                font: Font::MONOSPACE,
                align_x: text::Alignment::Center,
                align_y: alignment::Vertical::Center,
                ..Text::default()
            };
            // Als Formen zeichnen, damit Häuser davor die Schrift verdecken
            schild.draw_with(|form, farbe| frame.fill(&form, farbe));
        }        match self.spiel.belegung(feld) {
            Belegung::Rohbau => self.zeichne_rohbau(frame, iso, farben, feld, hoehe),
            Belegung::Frei => {
                let schild = Text {
                    content: feld.name(),
                    position: iso.punkt(s + 0.5, r + 0.5, hoehe),
                    color: farben.beschriftung,
                    size: 11.0.into(),
                    font: Font::MONOSPACE,
                    align_x: text::Alignment::Center,
                    align_y: alignment::Vertical::Center,
                    ..Text::default()
                };
                // Als Formen zeichnen, damit Häuser davor die Schrift verdecken
                schild.draw_with(|form, farbe| frame.fill(&form, farbe));
            }
        }    }
Probelauf Speichern, dann im Terminal:
cargo run
Alles wie vorher. Unter der Haube steht jetzt aber ein Brett statt einer Liste, und das ist genau die Grundlage für die Ketten.
Schritt 2

Ein Bauplan für Häuser

Unser Rohbau ist fest verdrahtet: immer drei Stockwerke, immer Beton. Hotels sollen aber je nach Kette anders aussehen und mit der Kette wachsen. Statt für jede Art Haus eine eigene Zeichenfunktion zu schreiben, beschreiben wir ein Haus durch einen Bauplan und lassen eine einzige Funktion jeden Bauplan zeichnen. Der Bauplan kommt vor Quader:

src/main.rsvor // Ein Quader: von …
// Die Beschreibung eines Hauses: wie hoch, welche Farbe
struct Bauplan {
    stockwerke: usize,
    fassade: Color,
}
// Ein Quader: von (s0, r0) bis (s1, r1) auf dem Brett, von z0 bis z1 in der Höhe

Aus zeichne_rohbau wird zeichne_haus, und die Funktion bekommt den Bauplan als siebten Parameter. An drei Stellen ersetzen wir feste Werte durch Angaben aus dem Plan:

src/main.rsfn zeichne_rohbau wird zu fn zeichne_haus
    fn zeichne_rohbau(    fn zeichne_haus(        &self,
        frame: &mut Frame,
        iso: &Iso,
        farben: &Farben,
        feld: Feld,
        boden: f32,        plan: &Bauplan,    ) {
        let s = feld.spalte as f32;
        let r = feld.reihe as f32;        let stockwerke = 3;        let stockwerke = plan.stockwerke;        ⋮            z1: boden + stockwerke as f32 * STOCKWERK,            farbe: farben.beton,            farbe: plan.fassade,        };        ⋮        ];        frame.fill(&vieleck(&dach), abdunkeln(farben.beton, 0.85));        frame.fill(&vieleck(&dach), abdunkeln(plan.fassade, 0.85));    }

Und in zeichne_parzelle schreiben wir den Bauplan für einen Rohbau:

src/main.rsin fn zeichne_parzelle, im match
        match self.spiel.belegung(feld) {            Belegung::Rohbau => self.zeichne_rohbau(frame, iso, farben, feld, hoehe),            Belegung::Rohbau => {
                let plan = Bauplan {
                    stockwerke: 3,
                    fassade: farben.beton,
                };
                self.zeichne_haus(frame, iso, farben, feld, hoehe, &plan);
            }            Belegung::Frei => {

Das nennt man Umbauen ohne Umzug oder, unter Programmierern, Refactoring: Das Programm tut danach genau dasselbe wie vorher, ist aber für das Kommende vorbereitet.

Probelauf Speichern, dann im Terminal:
cargo run
Die Rohbauten sehen genauso aus wie bisher. Probier zum Spaß im Bauplan stockwerke: 6 aus, und stell es danach wieder auf 3.
Schritt 3

Sieben Hotelketten

Jetzt kommt der Kern des Spiels. Die Regel ist einfach: Legst du ein Plättchen direkt neben einen Rohbau, entsteht eine Hotelkette, und du darfst ihr einen Namen geben. Legst du ein Plättchen neben eine bestehende Kette, wächst sie, und zwar samt aller Rohbauten, die mit ihr verbunden sind. „Neben“ heißt dabei: links, rechts, oben oder unten, nicht schräg.

Es gibt sieben Ketten, jede mit eigenem Namen. Das ist wieder ein klarer Fall für ein enum. Es kommt in spiel.rs vor die Belegung, und die Belegung bekommt ihren dritten Fall, der sich merkt, zu welcher Kette ein Hotel gehört:

src/spiel.rsvor // Was auf einem Feld steht
// Die sieben Hotelketten
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Kette {
    Zugvogel,
    Ankerplatz,
    Bergfried,
    Uferblick,
    Morgenrot,
    Aurora,
    Koenigshof,
}

impl Kette {
    pub const ALLE: [Kette; 7] = [
        Kette::Zugvogel,
        Kette::Ankerplatz,
        Kette::Bergfried,
        Kette::Uferblick,
        Kette::Morgenrot,
        Kette::Aurora,
        Kette::Koenigshof,
    ];

    pub fn name(self) -> &'static str {
        match self {
            Kette::Zugvogel => "Zugvogel",
            Kette::Ankerplatz => "Ankerplatz",
            Kette::Bergfried => "Bergfried",
            Kette::Uferblick => "Uferblick",
            Kette::Morgenrot => "Morgenrot",
            Kette::Aurora => "Aurora",
            Kette::Koenigshof => "Königshof",
        }
    }
}
// Was auf einem Feld steht⋮    Frei,
    Rohbau,    Hotel(Kette),}

ALLE ist eine Konstante innerhalb von impl Kette: eine Liste mit allen sieben Ketten, damit wir sie später in Schleifen durchgehen können. &'static str ist ein Text, der fest im Programm steht und nie verschwindet. Genau das sind die Namen.

Für die Nachbarschaft bekommt Feld eine Methode, die die bis zu vier Nachbarn liefert. Am Rand gibt es weniger, deshalb prüfen wir jede Richtung, bevor wir sie in die Liste packen:

src/spiel.rsin impl Feld, unter fn index
        self.reihe * SPALTEN + self.spalte
    }
    // Die bis zu vier Nachbarn: links, rechts, oben, unten
    pub fn nachbarn(self) -> Vec<Feld> {
        let mut liste = Vec::new();
        if self.spalte > 0 {
            liste.push(Feld {
                spalte: self.spalte - 1,
                reihe: self.reihe,
            });
        }
        if self.spalte + 1 < SPALTEN {
            liste.push(Feld {
                spalte: self.spalte + 1,
                reihe: self.reihe,
            });
        }
        if self.reihe > 0 {
            liste.push(Feld {
                spalte: self.spalte,
                reihe: self.reihe - 1,
            });
        }
        if self.reihe + 1 < REIHEN {
            liste.push(Feld {
                spalte: self.spalte,
                reihe: self.reihe + 1,
            });
        }
        liste
    }
    // Der Name wie auf dem Spielplan: "1A" bis "12I"

Das Spiel muss sich merken, wenn eine Gründung auf deine Entscheidung wartet:

src/spiel.rsin struct Spiel und in fn neu
    pub hand: Vec<Feld>,    // Hier wartet eine neue Kette darauf, dass man ihr einen Namen gibt
    pub gruendung: Option<Feld>,}⋮            hand: Vec::new(),            gruendung: None,        };

Und jetzt die Regeln selbst. legen wird dafür komplett neu geschrieben, und darunter kommen fünf neue Methoden. Ersetze also alles ab dem Kommentar über legen bis zum Ende der Datei:

src/spiel.rsin impl Spiel: fn legen und alles danach ersetzen
    // Legt ein Plättchen aus der Hand. Gibt false zurück, wenn das nicht geht.
    pub fn legen(&mut self, feld: Feld) -> bool {
        let Some(stelle) = self.hand.iter().position(|&h| h == feld) else {
            return false;
        };
        self.hand.remove(stelle);
        self.brett[feld.index()] = Belegung::Rohbau;
        self.nachziehen();
        true
    }
}    // Legt ein Plättchen aus der Hand. Gibt false zurück, wenn das nicht geht.
    pub fn legen(&mut self, feld: Feld) -> bool {
        if self.gruendung.is_some() {
            return false;
        }
        let Some(stelle) = self.hand.iter().position(|&h| h == feld) else {
            return false;
        };

        let ketten = self.ketten_neben(feld);
        let rohbau_daneben = feld
            .nachbarn()
            .iter()
            .any(|&nachbar| self.belegung(nachbar) == Belegung::Rohbau);

        // Zwei Ketten auf einmal berühren: Das ist eine Fusion, die kommt später
        if ketten.len() > 1 {
            return false;
        }
        // Eine neue Kette gründen geht nur, wenn noch eine frei ist
        if ketten.is_empty() && rohbau_daneben && self.freie_ketten().is_empty() {
            return false;
        }

        self.hand.remove(stelle);
        self.brett[feld.index()] = Belegung::Rohbau;

        if let Some(&kette) = ketten.first() {
            self.ausbreiten(feld, kette);
        } else if rohbau_daneben {
            self.gruendung = Some(feld);
        }

        self.nachziehen();
        true
    }

    // Gründet an der wartenden Stelle eine neue Kette
    pub fn gruenden(&mut self, kette: Kette) {
        if !self.freie_ketten().contains(&kette) {
            return;
        }
        if let Some(feld) = self.gruendung.take() {
            self.ausbreiten(feld, kette);
        }
    }

    // Wie viele Felder gehören zu dieser Kette?
    pub fn groesse(&self, kette: Kette) -> usize {
        self.brett
            .iter()
            .filter(|&&belegung| belegung == Belegung::Hotel(kette))
            .count()
    }

    // Alle Ketten, die noch nicht auf dem Brett stehen
    pub fn freie_ketten(&self) -> Vec<Kette> {
        Kette::ALLE
            .into_iter()
            .filter(|&kette| self.groesse(kette) == 0)
            .collect()
    }

    // Welche Ketten grenzen an dieses Feld? Jede nur einmal.
    fn ketten_neben(&self, feld: Feld) -> Vec<Kette> {
        let mut ketten = Vec::new();
        for nachbar in feld.nachbarn() {
            if let Belegung::Hotel(kette) = self.belegung(nachbar) {
                if !ketten.contains(&kette) {
                    ketten.push(kette);
                }
            }
        }
        ketten
    }

    // Färbt ein Feld und alle angrenzenden Rohbauten in der Farbe der Kette
    fn ausbreiten(&mut self, start: Feld, kette: Kette) {
        let mut offen = vec![start];
        while let Some(feld) = offen.pop() {
            self.brett[feld.index()] = Belegung::Hotel(kette);
            for nachbar in feld.nachbarn() {
                if self.belegung(nachbar) == Belegung::Rohbau {
                    offen.push(nachbar);
                }
            }
        }
    }
}

Das ist ein großer Brocken, gehen wir ihn in Ruhe durch:

  • Erst prüfen, dann handeln. legen sammelt zuerst alles, was es wissen muss: welche Ketten angrenzen und ob ein Rohbau daneben steht. any fragt, ob irgendein Nachbar die Bedingung erfüllt. Erst wenn alle Prüfungen bestanden sind, wird das Plättchen wirklich aus der Hand genommen. So kann nie ein halb ausgeführter Zug entstehen.
  • Zwei Ketten auf einmal? Das wäre eine Fusion, die große Kette schluckt die kleine. Das bauen wir in Etappe 6. Bis dahin ist so ein Zug einfach nicht erlaubt.
  • Drei Ausgänge. Grenzt eine Kette an, breitet sie sich aus. Grenzt nur ein Rohbau an, merken wir uns die Gründung und warten auf deine Wahl. Grenzt gar nichts an, bleibt es ein Rohbau.
  • gruenden nimmt die wartende Stelle mit take() aus dem Option heraus. Danach steht dort wieder None, die Gründung ist erledigt.
  • groesse zählt mit filter und count alle Felder einer Kette. freie_ketten filtert aus ALLE diejenigen heraus, die noch nirgends stehen, und collect sammelt sie in einer neuen Liste.
  • ausbreiten ist das Herzstück: Es färbt ein Feld ein und merkt sich alle Rohbau-Nachbarn in einer Liste offen. Solange diese Liste nicht leer ist, nimmt es das nächste Feld heraus, färbt es ein und schaut sich wieder dessen Nachbarn an. So läuft die Farbe durch alle verbundenen Rohbauten wie Wasser durch Rinnen. Man nennt das eine Flutfüllung, das Füllwerkzeug in Malprogrammen funktioniert genauso.
Worauf du achten musst ketten_neben und ausbreiten haben kein pub. Sie sind Hilfsarbeiter der Regeln und gehen main.rs nichts an. Von außen kann man eine Kette also nur über legen und gruenden verändern, und genau dort stehen die Prüfungen. So kann die Oberfläche die Regeln gar nicht aus Versehen umgehen.

Weiter in main.rs. Jede Kette bekommt eine Erkennungsfarbe, und Hotels werden nach einem eigenen Bauplan gezeichnet: Die Fassade ist leicht in der Kettenfarbe getönt, oben läuft ein farbiges Band ums Haus, und die Höhe wächst mit der Kette. Die Wurzel (sqrt) sorgt dafür, dass kleine Ketten schnell in die Höhe gehen, große aber nicht endlos: Zwei Felder ergeben gut 5 Stockwerke, zwölf Felder etwa 8, vierzig Felder knapp 12. Eine kleine Schwankung aus unserer Zufallsfunktion macht die Skyline lebendiger. Das alles kommt unter den Bauplan, der dafür ein Feld band bekommt:

src/main.rsstruct Bauplan und darunter
    fassade: Color,    band: Option<Color>,}
// Wie ein Hotel aussieht: Je größer die Kette, desto höher das Haus
fn hotel_bauplan(kette: Kette, groesse: usize, farben: &Farben, feld: Feld) -> Bauplan {
    let grundhoehe = 3.0 + (groesse as f32).sqrt() * 1.5;
    let schwankung = zufall(feld.index(), 7) % 2;
    Bauplan {
        stockwerke: grundhoehe as usize + schwankung as usize,
        fassade: mischen(farben.beton, kettenfarbe(kette), 0.25),
        band: Some(kettenfarbe(kette)),
    }
}

// Jede Kette hat ihre Erkennungsfarbe
fn kettenfarbe(kette: Kette) -> Color {
    match kette {
        Kette::Zugvogel => Color::from_rgb8(0xf2, 0xb1, 0x34),
        Kette::Ankerplatz => Color::from_rgb8(0x2b, 0xb3, 0xa6),
        Kette::Bergfried => Color::from_rgb8(0xd9, 0x48, 0x3b),
        Kette::Uferblick => Color::from_rgb8(0x3b, 0x7d, 0xd8),
        Kette::Morgenrot => Color::from_rgb8(0xe8, 0x73, 0x8f),
        Kette::Aurora => Color::from_rgb8(0x8e, 0x5b, 0xd6),
        Kette::Koenigshof => Color::from_rgb8(0xc9, 0xa2, 0x27),
    }
}
// Ein Quader: von (s0, r0) bis (s1, r1) auf dem Brett, von z0 bis z1 in der Höhe

mischen ist ein neuer Farbhelfer. Er kommt zu den anderen:

src/main.rsvor // Würfeln ohne Würfel …
// Mischt zwei Farben: anteil 0.0 ist ganz a, 1.0 ist ganz b
fn mischen(a: Color, b: Color, anteil: f32) -> Color {
    Color {
        r: a.r + (b.r - a.r) * anteil,
        g: a.g + (b.g - a.g) * anteil,
        b: a.b + (b.b - a.b) * anteil,
        a: 1.0,
    }
}
// Würfeln ohne Würfel: gleiche Eingabe, immer das gleiche Ergebnis

Das Band zeichnen wir in zeichne_haus direkt vor dem Dach, auf beiden sichtbaren Seiten, rechts wie immer etwas dunkler. if let Some(band) heißt: Nur wenn der Plan ein Band hat, wird es gezeichnet. Rohbauten haben keins.

src/main.rsin fn zeichne_haus, vor dem Flachdach
        // Ein Band in der Farbe der Kette ganz oben um das Haus
        if let Some(band) = plan.band {
            let unten = haus.z1 - 3.5;
            let links = [
                iso.punkt(haus.s0, haus.r1, haus.z1),
                iso.punkt(haus.s1, haus.r1, haus.z1),
                iso.punkt(haus.s1, haus.r1, unten),
                iso.punkt(haus.s0, haus.r1, unten),
            ];
            let rechts = [
                iso.punkt(haus.s1, haus.r0, haus.z1),
                iso.punkt(haus.s1, haus.r1, haus.z1),
                iso.punkt(haus.s1, haus.r1, unten),
                iso.punkt(haus.s1, haus.r0, unten),
            ];
            frame.fill(&vieleck(&links), band);
            frame.fill(&vieleck(&rechts), abdunkeln(band, 0.75));
        }
        // Das Flachdach bekommt einen Rand
src/main.rsin fn zeichne_parzelle, im match
                    stockwerke: 3,
                    fassade: farben.beton,                    band: None,                };
                self.zeichne_haus(frame, iso, farben, feld, hoehe, &plan);
            }            Belegung::Hotel(kette) => {
                let groesse = self.spiel.groesse(kette);
                let plan = hotel_bauplan(kette, groesse, farben, feld);
                self.zeichne_haus(frame, iso, farben, feld, hoehe, &plan);
            }            Belegung::Frei => {

Bleibt die Wahl der Kette. Dafür gibt es einen neuen Brief, und wenn eine Gründung wartet, legen wir ein Auswahlfenster über alles:

src/main.rsganz oben
use iced::widget::canvas::{Frame, Path, Stroke, Text, gradient};use iced::widget::{button, canvas, column, row, space, stack, text};
use iced::{
    Background, Border, Color, Element, Fill, Font, Point, Rectangle, Renderer, Size, Theme,
    alignment, font,
};use iced::widget::{button, canvas, column, container, row, space, stack, text};
use iced::{
    Background, Border, Color, Element, Fill, Font, Point, Rectangle, Renderer, Shadow, Size,
    Theme, Vector, alignment, font,
};use spiel::{Belegung, Feld, REIHEN, SPALTEN, Spiel};use spiel::{Belegung, Feld, Kette, REIHEN, SPALTEN, Spiel};
src/main.rsenum Message und fn update
    TagNacht,    Gruenden(Kette),}⋮            Message::TagNacht => self.nacht = !self.nacht,            Message::Gruenden(kette) => self.spiel.gruenden(kette),        }
src/main.rsin fn view, ganz unten, und darunter eine neue Funktion
        let folie = column![kopfzeile, space::vertical(), fussleiste];        stack![brett, folie].into()        // Wartet eine Gründung, kommt das Auswahlfenster obendrauf
        let mut ebenen = stack![brett, folie];
        if self.spiel.gruendung.is_some() {
            ebenen = ebenen.push(self.gruendungsdialog(farben));
        }
        ebenen.into()    }
    // Das Fenster zum Gründen einer neuen Kette
    fn gruendungsdialog(&self, farben: Farben) -> Element<'_, Message> {
        let mut knoepfe = row![].spacing(8);
        for kette in self.spiel.freie_ketten() {
            let farbe = kettenfarbe(kette);
            let knopf = button(text(kette.name()).size(14))
                .padding([8, 12])
                .on_press(Message::Gruenden(kette))
                .style(move |_theme: &Theme, status| {
                    let grund = match status {
                        button::Status::Hovered => aufhellen(farbe, 0.25),
                        _ => farbe,
                    };
                    button::Style {
                        background: Some(Background::Color(grund)),
                        text_color: Color::WHITE,
                        border: Border {
                            radius: 6.0.into(),
                            ..Border::default()
                        },
                        ..button::Style::default()
                    }
                });
            knoepfe = knoepfe.push(knopf);
        }

        let titel = text("Neue Hotelkette!")
            .size(24)
            .font(Font {
                weight: font::Weight::Bold,
                ..Font::DEFAULT
            })
            .color(farben.schrift);
        let frage = text("Deine Häuser stehen jetzt nebeneinander. Welche Kette gründest du?")
            .size(15)
            .color(farben.schrift);

        let karte = container(
            column![titel, frage, knoepfe]
                .spacing(14)
                .align_x(alignment::Horizontal::Center),
        )
        .padding(24)
        .style(move |_theme: &Theme| container::Style {
            background: Some(Background::Color(farben.tafel)),
            border: Border {
                radius: 12.0.into(),
                ..Border::default()
            },
            shadow: Shadow {
                color: Color::from_rgba(0.0, 0.0, 0.0, 0.35),
                offset: Vector::new(0.0, 8.0),
                blur_radius: 24.0,
            },
            ..container::Style::default()
        });

        // Ein dunkler Schleier über dem Brett, die Karte in der Mitte
        container(karte)
            .center(Fill)
            .style(|_theme: &Theme| container::Style {
                background: Some(Background::Color(Color::from_rgba(0.0, 0.0, 0.0, 0.3))),
                ..container::Style::default()
            })
            .into()
    }
    fn farben(&self) -> Farben {
  • Eine dritte Ebene. stack! lässt sich wie eine Liste verlängern: Mit push kommt nur dann eine Ebene obendrauf, wenn eine Gründung wartet.
  • Die Knöpfe entstehen in einer Schleife über die freien Ketten, jeder in seiner Kettenfarbe. Ein Klick verschickt Message::Gruenden mit genau dieser Kette.
  • Die Karte ist ein container mit Innenabstand, runden Ecken und einem weichen Schatten (Shadow), der sie über dem Brett schweben lässt. Vector::new(0.0, 8.0) verschiebt den Schatten 8 Pixel nach unten, blur_radius macht ihn weich.
  • Der Schleier. Außen herum liegt ein zweiter container, der das ganze Fenster mit halbdurchsichtigem Schwarz abdunkelt und mit center(Fill) die Karte genau in die Mitte setzt.

Zum Schluss eine Farbe für die Karte, bei Tag hell und bei Nacht dunkel:

src/main.rsin struct Farben und in beiden Paletten
    stange: Color,    tafel: Color,}⋮            stange: Color::WHITE,            tafel: Color::from_rgb8(0xfb, 0xf8, 0xf1),⋮            stange: Color::from_rgb8(0xcf, 0xd3, 0xdc),            tafel: Color::from_rgb8(0x1e, 0x22, 0x33),
Probelauf Speichern, dann im Terminal:
cargo run
Leg zwei Plättchen nebeneinander. Beim zweiten erscheint die Karte „Neue Hotelkette!“. Such dir eine Kette aus, und beide Häuser verwandeln sich in Hotels mit farbigem Band. Leg weitere Plättchen daneben, und die Kette wächst, samt aller Rohbauten, die mit ihr verbunden sind. Mit jedem Feld werden die Häuser der Kette ein Stück höher.
Ein halbdurchsichtiger dunkler Schleier über dem Brett, in der Mitte eine helle Karte mit der Überschrift Neue Hotelkette und farbigen Knöpfen für die freien Ketten
Die Gründung: Nur Ketten, die noch frei sind, stehen zur Wahl.
Schritt 4

Sieben Dächer

Farbe allein macht noch keinen Charakter. Jede Kette bekommt deshalb ihre eigene Dachform: das schlichte Flachdach der günstigen Zugvogel-Pensionen, Dachgärten beim Ankerplatz, Burgzinnen beim Bergfried, Funkmasten beim Uferblick, spitze Dächer bei Morgenrot, gestufte Aufbauten wie bei alten Hochhäusern bei Aurora und eine goldene Turmspitze beim Königshof. Die Formen kommen als enum in den Bauplan:

src/main.rsstruct Bauplan und darunter
    band: Option<Color>,    dach: Dach,}
// Die Dachformen der Ketten
#[derive(Clone, Copy)]
enum Dach {
    Flach,
    Garten,
    Zinnen,
    Antenne,
    Pyramide,
    Stufen,
    Turmspitze,
}
// Wie ein Hotel aussieht: Je größer die Kette, desto höher das Haus
src/main.rsin fn hotel_bauplan und darunter
        band: Some(kettenfarbe(kette)),        dach: dachform(kette),    }
}
// Jede Kette hat ihre eigene Dachform
fn dachform(kette: Kette) -> Dach {
    match kette {
        Kette::Zugvogel => Dach::Flach,
        Kette::Ankerplatz => Dach::Garten,
        Kette::Bergfried => Dach::Zinnen,
        Kette::Uferblick => Dach::Antenne,
        Kette::Morgenrot => Dach::Pyramide,
        Kette::Aurora => Dach::Stufen,
        Kette::Koenigshof => Dach::Turmspitze,
    }
}
// Jede Kette hat ihre Erkennungsfarbe
src/main.rsin fn zeichne_parzelle, Bauplan für den Rohbau
                    band: None,                    dach: Dach::Flach,                };

In zeichne_haus ersetzen wir das feste Flachdach durch einen Aufruf, der je nach Dachform zeichnet:

src/main.rsin fn zeichne_haus, ganz unten
            frame.fill(&vieleck(&rechts), abdunkeln(band, 0.75));
        }
        // Das Flachdach bekommt einen Rand
        let innen = 0.05;
        let dach = [
            iso.punkt(haus.s0 + innen, haus.r0 + innen, haus.z1),
            iso.punkt(haus.s1 - innen, haus.r0 + innen, haus.z1),
            iso.punkt(haus.s1 - innen, haus.r1 - innen, haus.z1),
            iso.punkt(haus.s0 + innen, haus.r1 - innen, haus.z1),
        ];
        frame.fill(&vieleck(&dach), abdunkeln(plan.fassade, 0.85));        self.zeichne_dach(frame, iso, &haus, plan);    }

Die neue Methode zeichne_dach kommt ans Ende des zweiten impl Bauloewe-Blocks, direkt dahinter zwei freie Helfer für das Flachdach (das alte, jetzt als eigene Funktion) und für spitze Dächer:

src/main.rsunter fn zeichne_haus, bis vor fn zeichne_sockel
        self.zeichne_dach(frame, iso, &haus, plan);
    }
    // Jede Dachform wird anders gezeichnet
    fn zeichne_dach(&self, frame: &mut Frame, iso: &Iso, haus: &Quader, plan: &Bauplan) {
        let farbe = plan.band.unwrap_or(plan.fassade);
        let mitte_s = (haus.s0 + haus.s1) / 2.0;
        let mitte_r = (haus.r0 + haus.r1) / 2.0;

        match plan.dach {
            Dach::Flach => flachdach(frame, iso, haus),
            Dach::Garten => {
                flachdach(frame, iso, haus);
                let mitte = iso.punkt(mitte_s, mitte_r, haus.z1);
                let busch = Color::from_rgb8(0x5f, 0x9a, 0x44);
                frame.fill(
                    &Path::circle(Point::new(mitte.x - 5.0, mitte.y - 3.0), 4.5),
                    busch,
                );
                frame.fill(
                    &Path::circle(Point::new(mitte.x + 4.0, mitte.y - 1.0), 3.5),
                    aufhellen(busch, 0.2),
                );
            }
            Dach::Zinnen => {
                flachdach(frame, iso, haus);
                let d = 0.12;
                for (s, r) in [
                    (haus.s0, haus.r0),
                    (haus.s1 - d, haus.r0),
                    (haus.s0, haus.r1 - d),
                    (haus.s1 - d, haus.r1 - d),
                ] {
                    let zinne = Quader {
                        s0: s,
                        r0: r,
                        s1: s + d,
                        r1: r + d,
                        z0: haus.z1,
                        z1: haus.z1 + 5.0,
                        farbe: haus.farbe,
                    };
                    zeichne_quader(frame, iso, &zinne);
                }
            }
            Dach::Antenne => {
                flachdach(frame, iso, haus);
                let fuss = iso.punkt(mitte_s, mitte_r, haus.z1);
                let spitze = Point::new(fuss.x, fuss.y - 20.0);
                let mast = Stroke::default()
                    .with_color(abdunkeln(farbe, 0.5))
                    .with_width(1.5);
                frame.stroke(&Path::line(fuss, spitze), mast);
                let lampe = if self.nacht {
                    Color::from_rgb8(0xff, 0x4a, 0x3d)
                } else {
                    abdunkeln(farbe, 0.6)
                };
                frame.fill(&Path::circle(spitze, 2.2), lampe);
            }
            Dach::Pyramide => {
                let spitze = iso.punkt(mitte_s, mitte_r, haus.z1 + 16.0);
                pyramide(frame, iso, haus, spitze, farbe);
            }
            Dach::Stufen => {
                for (stufe, hoehe) in [(0.1, 7.0), (0.2, 13.0)] {
                    let aufsatz = Quader {
                        s0: haus.s0 + stufe,
                        r0: haus.r0 + stufe,
                        s1: haus.s1 - stufe,
                        r1: haus.r1 - stufe,
                        z0: haus.z1,
                        z1: haus.z1 + hoehe,
                        farbe: haus.farbe,
                    };
                    zeichne_quader(frame, iso, &aufsatz);
                }
            }
            Dach::Turmspitze => {
                let sockel = Quader {
                    s0: haus.s0 + 0.15,
                    r0: haus.r0 + 0.15,
                    s1: haus.s1 - 0.15,
                    r1: haus.r1 - 0.15,
                    z0: haus.z1,
                    z1: haus.z1 + 5.0,
                    farbe: haus.farbe,
                };
                zeichne_quader(frame, iso, &sockel);
                let spitze = iso.punkt(mitte_s, mitte_r, sockel.z1 + 26.0);
                pyramide(frame, iso, &sockel, spitze, farbe);
            }
        }
    }}
// Ein Flachdach mit Rand
fn flachdach(frame: &mut Frame, iso: &Iso, haus: &Quader) {
    let innen = 0.05;
    let dach = [
        iso.punkt(haus.s0 + innen, haus.r0 + innen, haus.z1),
        iso.punkt(haus.s1 - innen, haus.r0 + innen, haus.z1),
        iso.punkt(haus.s1 - innen, haus.r1 - innen, haus.z1),
        iso.punkt(haus.s0 + innen, haus.r1 - innen, haus.z1),
    ];
    frame.fill(&vieleck(&dach), abdunkeln(haus.farbe, 0.85));
}

// Ein spitzes Dach: zwei sichtbare Dreiecke von der Oberkante zur Spitze
fn pyramide(frame: &mut Frame, iso: &Iso, unten: &Quader, spitze: Point, farbe: Color) {
    let links = [
        iso.punkt(unten.s0, unten.r1, unten.z1),
        iso.punkt(unten.s1, unten.r1, unten.z1),
        spitze,
    ];
    let rechts = [
        iso.punkt(unten.s1, unten.r0, unten.z1),
        iso.punkt(unten.s1, unten.r1, unten.z1),
        spitze,
    ];
    frame.fill(&vieleck(&links), abdunkeln(farbe, 0.9));
    frame.fill(&vieleck(&rechts), abdunkeln(farbe, 0.65));
}
fn zeichne_sockel(frame: &mut Frame, iso: &Iso, farben: &Farben) {

Keine Angst vor der Länge, jeder Zweig ist ein kleines Bastelprojekt aus bekannten Teilen:

  • Garten: Flachdach plus zwei grüne Kreise als Büsche, leicht versetzt, der hintere dunkler als der vordere.
  • Zinnen: vier kleine Quader auf den Ecken. Sie werden in der Reihenfolge hinten, rechts, links, vorne gezeichnet, also wieder nach dem Maler-Algorithmus.
  • Antenne: ein Mast wie beim Fähnchen, oben ein kleiner Kreis. Nachts wird er zur roten Warnleuchte, wie auf echten Hochhäusern.
  • Pyramide: Von einem spitzen Dach sehen wir zwei Dreiecke, jeweils von den vorderen Oberkanten bis zur Spitze über der Hausmitte. Das erledigt der Helfer pyramide, den auch die Turmspitze benutzt.
  • Stufen: zwei immer schmalere Aufsätze übereinander. Die Schleife geht über Paare aus Einrückung und Höhe.
  • Turmspitze: ein schmaler Sockel und darauf eine sehr hohe, schlanke Pyramide in Gold.

plan.band.unwrap_or(plan.fassade) holt die Bandfarbe aus dem Option, und wenn es keine gibt, nimmt es stattdessen die Fassade. Dächer erscheinen so in der Farbe ihrer Kette.

Probelauf Speichern, dann im Terminal:
cargo run
Gründe mehrere Ketten und schau dir die Dächer an. Schalte auch mal auf Nacht: Die Funkmasten des Uferblicks blinken jetzt rot.
Schritt 5

Die Kettentafel

Wie groß ist eigentlich welche Kette? Das soll man auf einen Blick sehen, ohne Felder zu zählen. Oben rechts kommt dafür eine Tafel hin, mit einem farbigen Punkt, dem Namen und der Größe jeder Kette. Sie entsteht in view, direkt vor der Folie, und die Folie bekommt sie als zweite Zeile:

src/main.rsin fn view, vor let folie
            .align_y(alignment::Vertical::Center);
        // Rechts oben: die Tafel mit allen Ketten
        let mut ketten = column![
            text("Hotelketten")
                .size(15)
                .font(Font {
                    weight: font::Weight::Bold,
                    ..Font::DEFAULT
                })
                .color(farben.schrift)
        ]
        .spacing(5);
        for kette in Kette::ALLE {
            let groesse = self.spiel.groesse(kette);
            let farbe = kettenfarbe(kette);
            let punkt = container(space())
                .width(12)
                .height(12)
                .style(move |_theme: &Theme| container::Style {
                    background: Some(Background::Color(farbe)),
                    border: Border {
                        radius: 6.0.into(),
                        ..Border::default()
                    },
                    ..container::Style::default()
                });
            let angabe = if groesse == 0 {
                String::from("frei")
            } else {
                format!("{groesse} Felder")
            };
            let zeile = row![
                punkt,
                text(kette.name()).size(13).color(farben.schrift).width(90),
                text(angabe).size(13).color(farben.schrift),
            ]
            .spacing(8)
            .align_y(alignment::Vertical::Center);
            ketten = ketten.push(zeile);
        }
        let tafel = container(ketten)
            .padding(14)
            .style(move |_theme: &Theme| container::Style {
                background: Some(Background::Color(Color {
                    a: 0.85,
                    ..farben.tafel
                })),
                border: Border {
                    radius: 10.0.into(),
                    ..Border::default()
                },
                ..container::Style::default()
            });
        let tafelzeile = row![space::horizontal(), tafel].padding([0, 24]);
        let folie = column![kopfzeile, space::vertical(), fussleiste];        let folie = column![kopfzeile, tafelzeile, space::vertical(), fussleiste];
  • Der farbige Punkt ist ein leerer container mit space() als Inhalt, 12 mal 12 Pixel groß, mit Hintergrundfarbe und so stark gerundeten Ecken, dass ein Kreis daraus wird.
  • Feste Breite für den Namen. Mit .width(90) bekommt jeder Name gleich viel Platz, dadurch stehen die Größenangaben ordentlich untereinander wie in einer Tabelle.
  • format!("{groesse} Felder"): Steht der Name einer Variable direkt in den geschweiften Klammern, setzt Rust ihren Wert ein.
  • Halbdurchsichtig. Color { a: 0.85, ..farben.tafel } lässt das Brett ein wenig durch die Tafel schimmern, falls ein hoher Turm dahinter steht.
Probelauf Speichern, dann im Terminal:
cargo run
Oben rechts steht die Kettentafel. Freie Ketten sind als „frei“ markiert. Gründest du eine Kette oder lässt eine wachsen, zählt die Tafel sofort mit.
Die Stadt bei Nacht: sieben Hotelketten mit unterschiedlichen Dächern, leuchtenden Fenstern und roten Warnleuchten auf den Funkmasten
Nachts zeigt die Stadt, was sie hat.
Zwischenstand

Der komplette Code nach Etappe 3

Hier ist der vollständige Stand nach Etappe 3. Nimm ihn zum Vergleichen, wenn irgendwo ein Fehler steckt, den du nicht findest, oder als Einstiegspunkt, wenn du erst bei der nächsten Etappe dazustößt.

Cargo.toml, src/main.rs, src/spiel.rs anzeigen

Cargo.toml

[package]
name = "bauloewe"
version = "0.1.0"
edition = "2024"

[dependencies]
iced = { version = "0.14", features = ["canvas"] }
rand = "0.9"

src/main.rs

mod spiel;

use iced::mouse;
use iced::widget::canvas::{Frame, Path, Stroke, Text, gradient};
use iced::widget::{button, canvas, column, container, row, space, stack, text};
use iced::{
    Background, Border, Color, Element, Fill, Font, Point, Rectangle, Renderer, Shadow, Size,
    Theme, Vector, alignment, font,
};
use spiel::{Belegung, Feld, Kette, REIHEN, SPALTEN, Spiel};

// So groß erscheint ein Feld auf dem Bildschirm
const HALBE_BREITE: f32 = 40.0;
const HALBE_HOEHE: f32 = 20.0;

// Höhen in Pixeln
const PLATTE: f32 = 4.0;
const ANGEHOBEN: f32 = 9.0;
const STOCKWERK: f32 = 13.0;

fn main() -> iced::Result {
    iced::application(Bauloewe::neu, Bauloewe::update, Bauloewe::view)
        .title("Baulöwe")
        .window_size((980.0, 680.0))
        .antialiasing(true)
        .run()
}

struct Bauloewe {
    spiel: Spiel,
    maus_feld: Option<Feld>,
    nacht: bool,
}

#[derive(Debug, Clone)]
enum Message {
    MausUeber(Option<Feld>),
    Geklickt(Feld),
    TagNacht,
    Gruenden(Kette),
}

impl Bauloewe {
    fn neu() -> Bauloewe {
        Bauloewe {
            spiel: Spiel::neu(),
            maus_feld: None,
            nacht: false,
        }
    }

    fn update(&mut self, message: Message) {
        match message {
            Message::MausUeber(feld) => self.maus_feld = feld,
            Message::Geklickt(feld) => {
                self.spiel.legen(feld);
            }
            Message::TagNacht => self.nacht = !self.nacht,
            Message::Gruenden(kette) => self.spiel.gruenden(kette),
        }
    }

    fn view(&self) -> Element<'_, Message> {
        let farben = self.farben();

        let titel = text("BAULÖWE")
            .size(26)
            .font(Font {
                weight: font::Weight::Bold,
                ..Font::DEFAULT
            })
            .color(farben.schrift);

        let hinweis = match self.maus_feld {
            Some(feld) => format!("Feld {}", feld.name()),
            None => String::from("Fahr mit der Maus über das Baugrundstück"),
        };
        let status = text(hinweis).size(15).color(farben.schrift);

        let beschriftung = if self.nacht { "Tagmodus" } else { "Nachtmodus" };
        let schalter = button(text(beschriftung).size(14))
            .padding([6, 16])
            .on_press(Message::TagNacht)
            .style(move |_theme: &Theme, _status| button::Style {
                background: Some(Background::Color(farben.akzent)),
                text_color: Color::WHITE,
                border: Border {
                    radius: 16.0.into(),
                    ..Border::default()
                },
                ..button::Style::default()
            });

        let kopfzeile = row![titel, space::horizontal(), status, schalter]
            .spacing(20)
            .padding([14, 24])
            .align_y(alignment::Vertical::Center);

        let brett = canvas(self).width(Fill).height(Fill);

        // Unten: die Plättchen auf deiner Hand
        let mut hand = row![text("Deine Plättchen:").size(15).color(farben.schrift)]
            .spacing(10)
            .align_y(alignment::Vertical::Center);
        for &feld in &self.spiel.hand {
            let plaettchen = button(text(feld.name()).size(16).font(Font::MONOSPACE))
                .padding([8, 14])
                .on_press(Message::Geklickt(feld))
                .style(move |_theme: &Theme, status| {
                    let grund = match status {
                        button::Status::Hovered => farben.rasen_hell,
                        _ => farben.stein,
                    };
                    button::Style {
                        background: Some(Background::Color(grund)),
                        text_color: farben.schrift,
                        border: Border {
                            radius: 6.0.into(),
                            ..Border::default()
                        },
                        ..button::Style::default()
                    }
                });
            hand = hand.push(plaettchen);
        }

        let stapel = text(format!("Im Stapel: {}", self.spiel.stapel.len()))
            .size(14)
            .color(farben.schrift);

        let fussleiste = row![hand, space::horizontal(), stapel]
            .padding([16, 24])
            .align_y(alignment::Vertical::Center);

        // Kopf- und Fußleiste liegen wie eine Folie über dem Brett
        // Rechts oben: die Tafel mit allen Ketten
        let mut ketten = column![
            text("Hotelketten")
                .size(15)
                .font(Font {
                    weight: font::Weight::Bold,
                    ..Font::DEFAULT
                })
                .color(farben.schrift)
        ]
        .spacing(5);
        for kette in Kette::ALLE {
            let groesse = self.spiel.groesse(kette);
            let farbe = kettenfarbe(kette);
            let punkt = container(space())
                .width(12)
                .height(12)
                .style(move |_theme: &Theme| container::Style {
                    background: Some(Background::Color(farbe)),
                    border: Border {
                        radius: 6.0.into(),
                        ..Border::default()
                    },
                    ..container::Style::default()
                });
            let angabe = if groesse == 0 {
                String::from("frei")
            } else {
                format!("{groesse} Felder")
            };
            let zeile = row![
                punkt,
                text(kette.name()).size(13).color(farben.schrift).width(90),
                text(angabe).size(13).color(farben.schrift),
            ]
            .spacing(8)
            .align_y(alignment::Vertical::Center);
            ketten = ketten.push(zeile);
        }
        let tafel = container(ketten)
            .padding(14)
            .style(move |_theme: &Theme| container::Style {
                background: Some(Background::Color(Color {
                    a: 0.85,
                    ..farben.tafel
                })),
                border: Border {
                    radius: 10.0.into(),
                    ..Border::default()
                },
                ..container::Style::default()
            });
        let tafelzeile = row![space::horizontal(), tafel].padding([0, 24]);

        let folie = column![kopfzeile, tafelzeile, space::vertical(), fussleiste];

        // Wartet eine Gründung, kommt das Auswahlfenster obendrauf
        let mut ebenen = stack![brett, folie];
        if self.spiel.gruendung.is_some() {
            ebenen = ebenen.push(self.gruendungsdialog(farben));
        }
        ebenen.into()
    }

    // Das Fenster zum Gründen einer neuen Kette
    fn gruendungsdialog(&self, farben: Farben) -> Element<'_, Message> {
        let mut knoepfe = row![].spacing(8);
        for kette in self.spiel.freie_ketten() {
            let farbe = kettenfarbe(kette);
            let knopf = button(text(kette.name()).size(14))
                .padding([8, 12])
                .on_press(Message::Gruenden(kette))
                .style(move |_theme: &Theme, status| {
                    let grund = match status {
                        button::Status::Hovered => aufhellen(farbe, 0.25),
                        _ => farbe,
                    };
                    button::Style {
                        background: Some(Background::Color(grund)),
                        text_color: Color::WHITE,
                        border: Border {
                            radius: 6.0.into(),
                            ..Border::default()
                        },
                        ..button::Style::default()
                    }
                });
            knoepfe = knoepfe.push(knopf);
        }

        let titel = text("Neue Hotelkette!")
            .size(24)
            .font(Font {
                weight: font::Weight::Bold,
                ..Font::DEFAULT
            })
            .color(farben.schrift);
        let frage = text("Deine Häuser stehen jetzt nebeneinander. Welche Kette gründest du?")
            .size(15)
            .color(farben.schrift);

        let karte = container(
            column![titel, frage, knoepfe]
                .spacing(14)
                .align_x(alignment::Horizontal::Center),
        )
        .padding(24)
        .style(move |_theme: &Theme| container::Style {
            background: Some(Background::Color(farben.tafel)),
            border: Border {
                radius: 12.0.into(),
                ..Border::default()
            },
            shadow: Shadow {
                color: Color::from_rgba(0.0, 0.0, 0.0, 0.35),
                offset: Vector::new(0.0, 8.0),
                blur_radius: 24.0,
            },
            ..container::Style::default()
        });

        // Ein dunkler Schleier über dem Brett, die Karte in der Mitte
        container(karte)
            .center(Fill)
            .style(|_theme: &Theme| container::Style {
                background: Some(Background::Color(Color::from_rgba(0.0, 0.0, 0.0, 0.3))),
                ..container::Style::default()
            })
            .into()
    }

    fn farben(&self) -> Farben {
        if self.nacht {
            Farben::nacht()
        } else {
            Farben::tag()
        }
    }
}

impl canvas::Program<Message> for Bauloewe {
    type State = ();

    fn update(
        &self,
        _state: &mut Self::State,
        event: &canvas::Event,
        bounds: Rectangle,
        cursor: mouse::Cursor,
    ) -> Option<canvas::Action<Message>> {
        let iso = Iso::neu(bounds.size());
        let feld = cursor
            .position_in(bounds)
            .and_then(|punkt| iso.feld_unter(punkt));

        match event {
            canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
                feld.map(|feld| canvas::Action::publish(Message::Geklickt(feld)))
            }
            canvas::Event::Mouse(_) if feld != self.maus_feld => {
                Some(canvas::Action::publish(Message::MausUeber(feld)))
            }
            _ => None,
        }
    }

    fn draw(
        &self,
        _state: &Self::State,
        renderer: &Renderer,
        _theme: &Theme,
        bounds: Rectangle,
        _cursor: mouse::Cursor,
    ) -> Vec<canvas::Geometry> {
        let mut frame = Frame::new(renderer, bounds.size());
        let farben = self.farben();
        let iso = Iso::neu(bounds.size());

        self.zeichne_himmel(&mut frame, &farben, bounds.size());
        zeichne_sockel(&mut frame, &iso, &farben);

        // Von hinten nach vorne zeichnen, damit Vorderes Hinteres verdeckt
        let mut felder = Vec::new();
        for reihe in 0..REIHEN {
            for spalte in 0..SPALTEN {
                felder.push(Feld { spalte, reihe });
            }
        }
        felder.sort_by_key(|feld| feld.spalte + feld.reihe);

        for feld in felder {
            self.zeichne_parzelle(&mut frame, &iso, &farben, feld);
        }

        vec![frame.into_geometry()]
    }

    fn mouse_interaction(
        &self,
        _state: &Self::State,
        _bounds: Rectangle,
        _cursor: mouse::Cursor,
    ) -> mouse::Interaction {
        match self.maus_feld {
            Some(feld) if self.spiel.hand.contains(&feld) => mouse::Interaction::Pointer,
            _ => mouse::Interaction::default(),
        }
    }
}

// Alles, was auf die Leinwand gezeichnet wird
impl Bauloewe {
    fn zeichne_himmel(&self, frame: &mut Frame, farben: &Farben, groesse: Size) {
        let verlauf = gradient::Linear::new(Point::ORIGIN, Point::new(0.0, groesse.height))
            .add_stop(0.0, farben.himmel_oben)
            .add_stop(1.0, farben.himmel_unten);
        frame.fill_rectangle(Point::ORIGIN, groesse, verlauf);

        if self.nacht {
            // 90 Sterne, jede Nacht an denselben Stellen
            for nr in 0..90 {
                let x = (zufall(nr, 1) % 1000) as f32 / 1000.0;
                let y = (zufall(nr, 2) % 1000) as f32 / 1000.0;
                let punkt = Point::new(x * groesse.width, y * groesse.height * 0.55);
                let helligkeit = 0.3 + (zufall(nr, 3) % 70) as f32 / 100.0;
                frame.fill(
                    &Path::circle(punkt, 1.2),
                    Color {
                        a: helligkeit,
                        ..Color::WHITE
                    },
                );
            }
        } else {
            // Die Sonne: ein weicher Schein aus vielen blassen Kreisen
            let sonne = Point::new(groesse.width * 0.84, groesse.height * 0.2);
            for ring in 0..12 {
                let radius = 30.0 + ring as f32 * 14.0;
                frame.fill(
                    &Path::circle(sonne, radius),
                    Color::from_rgba(1.0, 0.97, 0.85, 0.05),
                );
            }
            frame.fill(
                &Path::circle(sonne, 26.0),
                Color::from_rgb8(0xff, 0xf6, 0xd8),
            );
        }
    }

    fn zeichne_parzelle(&self, frame: &mut Frame, iso: &Iso, farben: &Farben, feld: Feld) {
        let s = feld.spalte as f32;
        let r = feld.reihe as f32;
        let im_plan = self.spiel.hand.contains(&feld);
        let unter_maus = im_plan && self.maus_feld == Some(feld);
        let hoehe = if unter_maus {
            PLATTE + ANGEHOBEN
        } else {
            PLATTE
        };

        // Der Bordstein ist ein flacher Quader
        let rand = 0.1;
        let bordstein = Quader {
            s0: s + rand,
            r0: r + rand,
            s1: s + 1.0 - rand,
            r1: r + 1.0 - rand,
            z0: 0.0,
            z1: hoehe,
            farbe: farben.stein,
        };
        zeichne_quader(frame, iso, &bordstein);

        // Der Rasen liegt als Fläche oben auf dem Bordstein
        let innen = rand + 0.04;
        let rasen = [
            iso.punkt(s + innen, r + innen, hoehe),
            iso.punkt(s + 1.0 - innen, r + innen, hoehe),
            iso.punkt(s + 1.0 - innen, r + 1.0 - innen, hoehe),
            iso.punkt(s + innen, r + 1.0 - innen, hoehe),
        ];
        let gruen = if unter_maus {
            farben.rasen_hell
        } else {
            farben.rasen
        };
        frame.fill(&vieleck(&rasen), gruen);

        if unter_maus {
            let umriss = vieleck(&bordstein.oberseite(iso));
            frame.stroke(
                &umriss,
                Stroke::default().with_color(farben.akzent).with_width(2.0),
            );
        }

        if im_plan {
            zeichne_faehnchen(frame, iso, farben, feld, hoehe);
        }

        match self.spiel.belegung(feld) {
            Belegung::Rohbau => {
                let plan = Bauplan {
                    stockwerke: 3,
                    fassade: farben.beton,
                    band: None,
                    dach: Dach::Flach,
                };
                self.zeichne_haus(frame, iso, farben, feld, hoehe, &plan);
            }
            Belegung::Hotel(kette) => {
                let groesse = self.spiel.groesse(kette);
                let plan = hotel_bauplan(kette, groesse, farben, feld);
                self.zeichne_haus(frame, iso, farben, feld, hoehe, &plan);
            }
            Belegung::Frei => {
                let schild = Text {
                    content: feld.name(),
                    position: iso.punkt(s + 0.5, r + 0.5, hoehe),
                    color: farben.beschriftung,
                    size: 11.0.into(),
                    font: Font::MONOSPACE,
                    align_x: text::Alignment::Center,
                    align_y: alignment::Vertical::Center,
                    ..Text::default()
                };
                // Als Formen zeichnen, damit Häuser davor die Schrift verdecken
                schild.draw_with(|form, farbe| frame.fill(&form, farbe));
            }
        }
    }

    fn zeichne_haus(
        &self,
        frame: &mut Frame,
        iso: &Iso,
        farben: &Farben,
        feld: Feld,
        boden: f32,
        plan: &Bauplan,
    ) {
        let s = feld.spalte as f32;
        let r = feld.reihe as f32;
        let stockwerke = plan.stockwerke;
        let haus = Quader {
            s0: s + 0.2,
            r0: r + 0.2,
            s1: s + 0.8,
            r1: r + 0.8,
            z0: boden,
            z1: boden + stockwerke as f32 * STOCKWERK,
            farbe: plan.fassade,
        };

        // Nachts fällt warmes Licht aus den Fenstern auf den Rasen
        if self.nacht {
            let schein = [
                iso.punkt(s + 0.14, r + 0.14, boden),
                iso.punkt(s + 0.86, r + 0.14, boden),
                iso.punkt(s + 0.86, r + 0.86, boden),
                iso.punkt(s + 0.14, r + 0.86, boden),
            ];
            frame.fill(
                &vieleck(&schein),
                Color {
                    a: 0.18,
                    ..farben.licht
                },
            );
        }

        // Der Schatten fällt nach rechts auf den Rasen
        let schatten = [
            iso.punkt(haus.s1, haus.r0, boden),
            iso.punkt(haus.s1 + 0.12, haus.r0 + 0.08, boden),
            iso.punkt(haus.s1 + 0.12, haus.r1, boden),
            iso.punkt(haus.s1, haus.r1, boden),
        ];
        frame.fill(&vieleck(&schatten), Color::from_rgba(0.0, 0.0, 0.0, 0.25));

        zeichne_quader(frame, iso, &haus);

        // Fenster: pro Stockwerk zwei auf jeder sichtbaren Seite
        for stock in 0..stockwerke {
            let unten = boden + stock as f32 * STOCKWERK + 4.0;
            let oben = unten + 6.0;
            for nr in 0..2 {
                let a = 0.08 + nr as f32 * 0.27;
                let b = a + 0.17;
                let links = [
                    iso.punkt(haus.s0 + a, haus.r1, oben),
                    iso.punkt(haus.s0 + b, haus.r1, oben),
                    iso.punkt(haus.s0 + b, haus.r1, unten),
                    iso.punkt(haus.s0 + a, haus.r1, unten),
                ];
                let rechts = [
                    iso.punkt(haus.s1, haus.r0 + a, oben),
                    iso.punkt(haus.s1, haus.r0 + b, oben),
                    iso.punkt(haus.s1, haus.r0 + b, unten),
                    iso.punkt(haus.s1, haus.r0 + a, unten),
                ];
                for (seite, ecken) in [(0, links), (1, rechts)] {
                    let fenster_nr = stock * 4 + nr * 2 + seite;
                    let brennt =
                        self.nacht && zufall(feld.spalte * 16 + feld.reihe, fenster_nr) % 3 != 0;
                    let farbe = if brennt {
                        farben.licht
                    } else if seite == 0 {
                        farben.fenster
                    } else {
                        abdunkeln(farben.fenster, 0.8)
                    };
                    frame.fill(&vieleck(&ecken), farbe);
                }
            }
        }

        // Ein Band in der Farbe der Kette ganz oben um das Haus
        if let Some(band) = plan.band {
            let unten = haus.z1 - 3.5;
            let links = [
                iso.punkt(haus.s0, haus.r1, haus.z1),
                iso.punkt(haus.s1, haus.r1, haus.z1),
                iso.punkt(haus.s1, haus.r1, unten),
                iso.punkt(haus.s0, haus.r1, unten),
            ];
            let rechts = [
                iso.punkt(haus.s1, haus.r0, haus.z1),
                iso.punkt(haus.s1, haus.r1, haus.z1),
                iso.punkt(haus.s1, haus.r1, unten),
                iso.punkt(haus.s1, haus.r0, unten),
            ];
            frame.fill(&vieleck(&links), band);
            frame.fill(&vieleck(&rechts), abdunkeln(band, 0.75));
        }

        self.zeichne_dach(frame, iso, &haus, plan);
    }

    // Jede Dachform wird anders gezeichnet
    fn zeichne_dach(&self, frame: &mut Frame, iso: &Iso, haus: &Quader, plan: &Bauplan) {
        let farbe = plan.band.unwrap_or(plan.fassade);
        let mitte_s = (haus.s0 + haus.s1) / 2.0;
        let mitte_r = (haus.r0 + haus.r1) / 2.0;

        match plan.dach {
            Dach::Flach => flachdach(frame, iso, haus),
            Dach::Garten => {
                flachdach(frame, iso, haus);
                let mitte = iso.punkt(mitte_s, mitte_r, haus.z1);
                let busch = Color::from_rgb8(0x5f, 0x9a, 0x44);
                frame.fill(
                    &Path::circle(Point::new(mitte.x - 5.0, mitte.y - 3.0), 4.5),
                    busch,
                );
                frame.fill(
                    &Path::circle(Point::new(mitte.x + 4.0, mitte.y - 1.0), 3.5),
                    aufhellen(busch, 0.2),
                );
            }
            Dach::Zinnen => {
                flachdach(frame, iso, haus);
                let d = 0.12;
                for (s, r) in [
                    (haus.s0, haus.r0),
                    (haus.s1 - d, haus.r0),
                    (haus.s0, haus.r1 - d),
                    (haus.s1 - d, haus.r1 - d),
                ] {
                    let zinne = Quader {
                        s0: s,
                        r0: r,
                        s1: s + d,
                        r1: r + d,
                        z0: haus.z1,
                        z1: haus.z1 + 5.0,
                        farbe: haus.farbe,
                    };
                    zeichne_quader(frame, iso, &zinne);
                }
            }
            Dach::Antenne => {
                flachdach(frame, iso, haus);
                let fuss = iso.punkt(mitte_s, mitte_r, haus.z1);
                let spitze = Point::new(fuss.x, fuss.y - 20.0);
                let mast = Stroke::default()
                    .with_color(abdunkeln(farbe, 0.5))
                    .with_width(1.5);
                frame.stroke(&Path::line(fuss, spitze), mast);
                let lampe = if self.nacht {
                    Color::from_rgb8(0xff, 0x4a, 0x3d)
                } else {
                    abdunkeln(farbe, 0.6)
                };
                frame.fill(&Path::circle(spitze, 2.2), lampe);
            }
            Dach::Pyramide => {
                let spitze = iso.punkt(mitte_s, mitte_r, haus.z1 + 16.0);
                pyramide(frame, iso, haus, spitze, farbe);
            }
            Dach::Stufen => {
                for (stufe, hoehe) in [(0.1, 7.0), (0.2, 13.0)] {
                    let aufsatz = Quader {
                        s0: haus.s0 + stufe,
                        r0: haus.r0 + stufe,
                        s1: haus.s1 - stufe,
                        r1: haus.r1 - stufe,
                        z0: haus.z1,
                        z1: haus.z1 + hoehe,
                        farbe: haus.farbe,
                    };
                    zeichne_quader(frame, iso, &aufsatz);
                }
            }
            Dach::Turmspitze => {
                let sockel = Quader {
                    s0: haus.s0 + 0.15,
                    r0: haus.r0 + 0.15,
                    s1: haus.s1 - 0.15,
                    r1: haus.r1 - 0.15,
                    z0: haus.z1,
                    z1: haus.z1 + 5.0,
                    farbe: haus.farbe,
                };
                zeichne_quader(frame, iso, &sockel);
                let spitze = iso.punkt(mitte_s, mitte_r, sockel.z1 + 26.0);
                pyramide(frame, iso, &sockel, spitze, farbe);
            }
        }
    }
}

// Ein Flachdach mit Rand
fn flachdach(frame: &mut Frame, iso: &Iso, haus: &Quader) {
    let innen = 0.05;
    let dach = [
        iso.punkt(haus.s0 + innen, haus.r0 + innen, haus.z1),
        iso.punkt(haus.s1 - innen, haus.r0 + innen, haus.z1),
        iso.punkt(haus.s1 - innen, haus.r1 - innen, haus.z1),
        iso.punkt(haus.s0 + innen, haus.r1 - innen, haus.z1),
    ];
    frame.fill(&vieleck(&dach), abdunkeln(haus.farbe, 0.85));
}

// Ein spitzes Dach: zwei sichtbare Dreiecke von der Oberkante zur Spitze
fn pyramide(frame: &mut Frame, iso: &Iso, unten: &Quader, spitze: Point, farbe: Color) {
    let links = [
        iso.punkt(unten.s0, unten.r1, unten.z1),
        iso.punkt(unten.s1, unten.r1, unten.z1),
        spitze,
    ];
    let rechts = [
        iso.punkt(unten.s1, unten.r0, unten.z1),
        iso.punkt(unten.s1, unten.r1, unten.z1),
        spitze,
    ];
    frame.fill(&vieleck(&links), abdunkeln(farbe, 0.9));
    frame.fill(&vieleck(&rechts), abdunkeln(farbe, 0.65));
}

fn zeichne_sockel(frame: &mut Frame, iso: &Iso, farben: &Farben) {
    let rand = 0.35;
    let (s0, r0) = (-rand, -rand);
    let (s1, r1) = (SPALTEN as f32 + rand, REIHEN as f32 + rand);

    // Ein weicher Schatten unter dem ganzen Brett
    let schatten = [
        iso.punkt(s0, r0, -34.0),
        iso.punkt(s1, r0, -34.0),
        iso.punkt(s1, r1, -34.0),
        iso.punkt(s0, r1, -34.0),
    ];
    frame.fill(&vieleck(&schatten), Color::from_rgba(0.0, 0.0, 0.0, 0.18));

    let sockel = Quader {
        s0,
        r0,
        s1,
        r1,
        z0: -22.0,
        z1: 0.0,
        farbe: farben.strasse,
    };
    zeichne_quader(frame, iso, &sockel);
}

// Ein Vermessungsfähnchen: Hier darfst du bauen
fn zeichne_faehnchen(frame: &mut Frame, iso: &Iso, farben: &Farben, feld: Feld, boden: f32) {
    let fuss = iso.punkt(feld.spalte as f32 + 0.3, feld.reihe as f32 + 0.3, boden);
    let spitze = Point::new(fuss.x, fuss.y - 20.0);
    let stange = Stroke::default().with_color(farben.stange).with_width(1.5);
    frame.stroke(&Path::line(fuss, spitze), stange);

    let fahne = [
        spitze,
        Point::new(spitze.x + 11.0, spitze.y + 3.5),
        Point::new(spitze.x, spitze.y + 7.0),
    ];
    frame.fill(&vieleck(&fahne), farben.akzent);
}

// Rechnet zwischen Brett und Bildschirm hin und her
struct Iso {
    ursprung: Point,
}

impl Iso {
    fn neu(groesse: Size) -> Iso {
        // Die Mitte des Bretts soll in der Mitte der Leinwand liegen
        let mitte_s = SPALTEN as f32 / 2.0;
        let mitte_r = REIHEN as f32 / 2.0;
        let x = groesse.width / 2.0 - (mitte_s - mitte_r) * HALBE_BREITE;
        let y = groesse.height / 2.0 - (mitte_s + mitte_r) * HALBE_HOEHE + 10.0;
        Iso {
            ursprung: Point::new(x, y),
        }
    }

    // Hinweg: Brett (Spalte, Reihe, Höhe) -> Bildschirm
    fn punkt(&self, s: f32, r: f32, z: f32) -> Point {
        Point::new(
            self.ursprung.x + (s - r) * HALBE_BREITE,
            self.ursprung.y + (s + r) * HALBE_HOEHE - z,
        )
    }

    // Rückweg: Bildschirm -> Feld auf dem Brett
    fn feld_unter(&self, punkt: Point) -> Option<Feld> {
        let dx = (punkt.x - self.ursprung.x) / HALBE_BREITE;
        let dy = (punkt.y - self.ursprung.y) / HALBE_HOEHE;
        let s = (dy + dx) / 2.0;
        let r = (dy - dx) / 2.0;
        if s < 0.0 || r < 0.0 {
            return None;
        }
        let spalte = s as usize;
        let reihe = r as usize;
        if spalte < SPALTEN && reihe < REIHEN {
            Some(Feld { spalte, reihe })
        } else {
            None
        }
    }
}

// Die Beschreibung eines Hauses: wie hoch, welche Farbe
struct Bauplan {
    stockwerke: usize,
    fassade: Color,
    band: Option<Color>,
    dach: Dach,
}

// Die Dachformen der Ketten
#[derive(Clone, Copy)]
enum Dach {
    Flach,
    Garten,
    Zinnen,
    Antenne,
    Pyramide,
    Stufen,
    Turmspitze,
}

// Wie ein Hotel aussieht: Je größer die Kette, desto höher das Haus
fn hotel_bauplan(kette: Kette, groesse: usize, farben: &Farben, feld: Feld) -> Bauplan {
    let grundhoehe = 3.0 + (groesse as f32).sqrt() * 1.5;
    let schwankung = zufall(feld.index(), 7) % 2;
    Bauplan {
        stockwerke: grundhoehe as usize + schwankung as usize,
        fassade: mischen(farben.beton, kettenfarbe(kette), 0.25),
        band: Some(kettenfarbe(kette)),
        dach: dachform(kette),
    }
}

// Jede Kette hat ihre eigene Dachform
fn dachform(kette: Kette) -> Dach {
    match kette {
        Kette::Zugvogel => Dach::Flach,
        Kette::Ankerplatz => Dach::Garten,
        Kette::Bergfried => Dach::Zinnen,
        Kette::Uferblick => Dach::Antenne,
        Kette::Morgenrot => Dach::Pyramide,
        Kette::Aurora => Dach::Stufen,
        Kette::Koenigshof => Dach::Turmspitze,
    }
}

// Jede Kette hat ihre Erkennungsfarbe
fn kettenfarbe(kette: Kette) -> Color {
    match kette {
        Kette::Zugvogel => Color::from_rgb8(0xf2, 0xb1, 0x34),
        Kette::Ankerplatz => Color::from_rgb8(0x2b, 0xb3, 0xa6),
        Kette::Bergfried => Color::from_rgb8(0xd9, 0x48, 0x3b),
        Kette::Uferblick => Color::from_rgb8(0x3b, 0x7d, 0xd8),
        Kette::Morgenrot => Color::from_rgb8(0xe8, 0x73, 0x8f),
        Kette::Aurora => Color::from_rgb8(0x8e, 0x5b, 0xd6),
        Kette::Koenigshof => Color::from_rgb8(0xc9, 0xa2, 0x27),
    }
}

// Ein Quader: von (s0, r0) bis (s1, r1) auf dem Brett, von z0 bis z1 in der Höhe
struct Quader {
    s0: f32,
    r0: f32,
    s1: f32,
    r1: f32,
    z0: f32,
    z1: f32,
    farbe: Color,
}

impl Quader {
    fn oberseite(&self, iso: &Iso) -> [Point; 4] {
        [
            iso.punkt(self.s0, self.r0, self.z1),
            iso.punkt(self.s1, self.r0, self.z1),
            iso.punkt(self.s1, self.r1, self.z1),
            iso.punkt(self.s0, self.r1, self.z1),
        ]
    }
}

fn zeichne_quader(frame: &mut Frame, iso: &Iso, q: &Quader) {
    let links = [
        iso.punkt(q.s0, q.r1, q.z1),
        iso.punkt(q.s1, q.r1, q.z1),
        iso.punkt(q.s1, q.r1, q.z0),
        iso.punkt(q.s0, q.r1, q.z0),
    ];
    let rechts = [
        iso.punkt(q.s1, q.r0, q.z1),
        iso.punkt(q.s1, q.r1, q.z1),
        iso.punkt(q.s1, q.r1, q.z0),
        iso.punkt(q.s1, q.r0, q.z0),
    ];
    let oben = q.oberseite(iso);

    // Das Licht kommt von links: links mittel, rechts dunkel, oben am hellsten
    frame.fill(&vieleck(&links), abdunkeln(q.farbe, 0.78));
    frame.fill(&vieleck(&rechts), abdunkeln(q.farbe, 0.58));
    frame.fill(&vieleck(&oben), q.farbe);

    // Eine feine Lichtkante an den beiden vorderen Oberkanten
    let kante = Path::new(|p| {
        p.move_to(oben[3]);
        p.line_to(oben[2]);
        p.line_to(oben[1]);
    });
    frame.stroke(
        &kante,
        Stroke::default()
            .with_color(aufhellen(q.farbe, 0.35))
            .with_width(1.0),
    );
}

// Baut aus einer Liste von Ecken eine geschlossene Fläche
fn vieleck(ecken: &[Point]) -> Path {
    Path::new(|p| {
        p.move_to(ecken[0]);
        for ecke in &ecken[1..] {
            p.line_to(*ecke);
        }
        p.close();
    })
}

fn abdunkeln(farbe: Color, faktor: f32) -> Color {
    Color {
        r: farbe.r * faktor,
        g: farbe.g * faktor,
        b: farbe.b * faktor,
        a: farbe.a,
    }
}

fn aufhellen(farbe: Color, anteil: f32) -> Color {
    Color {
        r: farbe.r + (1.0 - farbe.r) * anteil,
        g: farbe.g + (1.0 - farbe.g) * anteil,
        b: farbe.b + (1.0 - farbe.b) * anteil,
        a: farbe.a,
    }
}

// Mischt zwei Farben: anteil 0.0 ist ganz a, 1.0 ist ganz b
fn mischen(a: Color, b: Color, anteil: f32) -> Color {
    Color {
        r: a.r + (b.r - a.r) * anteil,
        g: a.g + (b.g - a.g) * anteil,
        b: a.b + (b.b - a.b) * anteil,
        a: 1.0,
    }
}

// Würfeln ohne Würfel: gleiche Eingabe, immer das gleiche Ergebnis
fn zufall(a: usize, b: usize) -> u32 {
    let mut h = (a as u32).wrapping_mul(2_654_435_761) ^ (b as u32).wrapping_mul(40_503);
    h ^= h >> 15;
    h = h.wrapping_mul(2_246_822_519);
    h ^= h >> 13;
    h
}

// Alle Farben an einem Ort
#[derive(Clone, Copy)]
struct Farben {
    himmel_oben: Color,
    himmel_unten: Color,
    strasse: Color,
    stein: Color,
    rasen: Color,
    rasen_hell: Color,
    beschriftung: Color,
    akzent: Color,
    schrift: Color,
    beton: Color,
    fenster: Color,
    licht: Color,
    stange: Color,
    tafel: Color,
}

impl Farben {
    fn tag() -> Farben {
        Farben {
            himmel_oben: Color::from_rgb8(0x8e, 0xc5, 0xe8),
            himmel_unten: Color::from_rgb8(0xe9, 0xf2, 0xf7),
            strasse: Color::from_rgb8(0x5a, 0x62, 0x70),
            stein: Color::from_rgb8(0xd9, 0xd4, 0xc7),
            rasen: Color::from_rgb8(0x9c, 0xcb, 0x6b),
            rasen_hell: Color::from_rgb8(0xc4, 0xe6, 0x8f),
            beschriftung: Color::from_rgba8(0x1e, 0x32, 0x14, 0.55),
            akzent: Color::from_rgb8(0xce, 0x5a, 0x1e),
            schrift: Color::from_rgb8(0x1c, 0x2a, 0x3a),
            beton: Color::from_rgb8(0xe8, 0xe2, 0xd6),
            fenster: Color::from_rgb8(0x5b, 0x8d, 0xb8),
            licht: Color::from_rgb8(0xff, 0xd2, 0x7a),
            stange: Color::WHITE,
            tafel: Color::from_rgb8(0xfb, 0xf8, 0xf1),
        }
    }

    fn nacht() -> Farben {
        Farben {
            himmel_oben: Color::from_rgb8(0x0b, 0x10, 0x26),
            himmel_unten: Color::from_rgb8(0x2a, 0x23, 0x50),
            strasse: Color::from_rgb8(0x2a, 0x30, 0x42),
            stein: Color::from_rgb8(0x5c, 0x60, 0x6e),
            rasen: Color::from_rgb8(0x2e, 0x4a, 0x36),
            rasen_hell: Color::from_rgb8(0x4d, 0x7a, 0x4f),
            beschriftung: Color::from_rgba8(0xdc, 0xe6, 0xd2, 0.45),
            akzent: Color::from_rgb8(0xe0, 0x7a, 0x2e),
            schrift: Color::from_rgb8(0xe8, 0xec, 0xf5),
            beton: Color::from_rgb8(0x6d, 0x6a, 0x78),
            fenster: Color::from_rgb8(0x27, 0x32, 0x4a),
            licht: Color::from_rgb8(0xff, 0xd2, 0x7a),
            stange: Color::from_rgb8(0xcf, 0xd3, 0xdc),
            tafel: Color::from_rgb8(0x1e, 0x22, 0x33),
        }
    }
}

src/spiel.rs

use rand::seq::SliceRandom;

// Das Brett: 12 Spalten (1 bis 12) und 9 Reihen (A bis I)
pub const SPALTEN: usize = 12;
pub const REIHEN: usize = 9;

// So viele Plättchen hat man auf der Hand
pub const HANDGROESSE: usize = 6;

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Feld {
    pub spalte: usize,
    pub reihe: usize,
}

impl Feld {
    // Die Nummer des Feldes in einer Liste mit 108 Einträgen
    pub fn index(self) -> usize {
        self.reihe * SPALTEN + self.spalte
    }

    // Die bis zu vier Nachbarn: links, rechts, oben, unten
    pub fn nachbarn(self) -> Vec<Feld> {
        let mut liste = Vec::new();
        if self.spalte > 0 {
            liste.push(Feld {
                spalte: self.spalte - 1,
                reihe: self.reihe,
            });
        }
        if self.spalte + 1 < SPALTEN {
            liste.push(Feld {
                spalte: self.spalte + 1,
                reihe: self.reihe,
            });
        }
        if self.reihe > 0 {
            liste.push(Feld {
                spalte: self.spalte,
                reihe: self.reihe - 1,
            });
        }
        if self.reihe + 1 < REIHEN {
            liste.push(Feld {
                spalte: self.spalte,
                reihe: self.reihe + 1,
            });
        }
        liste
    }

    // Der Name wie auf dem Spielplan: "1A" bis "12I"
    pub fn name(self) -> String {
        let buchstabe = (b'A' + self.reihe as u8) as char;
        format!("{}{}", self.spalte + 1, buchstabe)
    }
}

// Die sieben Hotelketten
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Kette {
    Zugvogel,
    Ankerplatz,
    Bergfried,
    Uferblick,
    Morgenrot,
    Aurora,
    Koenigshof,
}

impl Kette {
    pub const ALLE: [Kette; 7] = [
        Kette::Zugvogel,
        Kette::Ankerplatz,
        Kette::Bergfried,
        Kette::Uferblick,
        Kette::Morgenrot,
        Kette::Aurora,
        Kette::Koenigshof,
    ];

    pub fn name(self) -> &'static str {
        match self {
            Kette::Zugvogel => "Zugvogel",
            Kette::Ankerplatz => "Ankerplatz",
            Kette::Bergfried => "Bergfried",
            Kette::Uferblick => "Uferblick",
            Kette::Morgenrot => "Morgenrot",
            Kette::Aurora => "Aurora",
            Kette::Koenigshof => "Königshof",
        }
    }
}

// Was auf einem Feld steht
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Belegung {
    Frei,
    Rohbau,
    Hotel(Kette),
}

// Alles, was die Spielregeln wissen müssen
pub struct Spiel {
    pub brett: Vec<Belegung>,
    pub stapel: Vec<Feld>,
    pub hand: Vec<Feld>,
    // Hier wartet eine neue Kette darauf, dass man ihr einen Namen gibt
    pub gruendung: Option<Feld>,
}

impl Spiel {
    pub fn neu() -> Spiel {
        // Für jedes Feld ein Plättchen, dann gut mischen
        let mut stapel = Vec::new();
        for reihe in 0..REIHEN {
            for spalte in 0..SPALTEN {
                stapel.push(Feld { spalte, reihe });
            }
        }
        stapel.shuffle(&mut rand::rng());

        let mut spiel = Spiel {
            brett: vec![Belegung::Frei; SPALTEN * REIHEN],
            stapel,
            hand: Vec::new(),
            gruendung: None,
        };
        spiel.nachziehen();
        spiel
    }

    // Was steht auf diesem Feld?
    pub fn belegung(&self, feld: Feld) -> Belegung {
        self.brett[feld.index()]
    }

    // Füllt die Hand wieder auf, solange der Stapel reicht
    pub fn nachziehen(&mut self) {
        while self.hand.len() < HANDGROESSE {
            match self.stapel.pop() {
                Some(feld) => self.hand.push(feld),
                None => break,
            }
        }
        self.hand.sort_by_key(|feld| (feld.spalte, feld.reihe));
    }

    // Legt ein Plättchen aus der Hand. Gibt false zurück, wenn das nicht geht.
    pub fn legen(&mut self, feld: Feld) -> bool {
        if self.gruendung.is_some() {
            return false;
        }
        let Some(stelle) = self.hand.iter().position(|&h| h == feld) else {
            return false;
        };

        let ketten = self.ketten_neben(feld);
        let rohbau_daneben = feld
            .nachbarn()
            .iter()
            .any(|&nachbar| self.belegung(nachbar) == Belegung::Rohbau);

        // Zwei Ketten auf einmal berühren: Das ist eine Fusion, die kommt später
        if ketten.len() > 1 {
            return false;
        }
        // Eine neue Kette gründen geht nur, wenn noch eine frei ist
        if ketten.is_empty() && rohbau_daneben && self.freie_ketten().is_empty() {
            return false;
        }

        self.hand.remove(stelle);
        self.brett[feld.index()] = Belegung::Rohbau;

        if let Some(&kette) = ketten.first() {
            self.ausbreiten(feld, kette);
        } else if rohbau_daneben {
            self.gruendung = Some(feld);
        }

        self.nachziehen();
        true
    }

    // Gründet an der wartenden Stelle eine neue Kette
    pub fn gruenden(&mut self, kette: Kette) {
        if !self.freie_ketten().contains(&kette) {
            return;
        }
        if let Some(feld) = self.gruendung.take() {
            self.ausbreiten(feld, kette);
        }
    }

    // Wie viele Felder gehören zu dieser Kette?
    pub fn groesse(&self, kette: Kette) -> usize {
        self.brett
            .iter()
            .filter(|&&belegung| belegung == Belegung::Hotel(kette))
            .count()
    }

    // Alle Ketten, die noch nicht auf dem Brett stehen
    pub fn freie_ketten(&self) -> Vec<Kette> {
        Kette::ALLE
            .into_iter()
            .filter(|&kette| self.groesse(kette) == 0)
            .collect()
    }

    // Welche Ketten grenzen an dieses Feld? Jede nur einmal.
    fn ketten_neben(&self, feld: Feld) -> Vec<Kette> {
        let mut ketten = Vec::new();
        for nachbar in feld.nachbarn() {
            if let Belegung::Hotel(kette) = self.belegung(nachbar) {
                if !ketten.contains(&kette) {
                    ketten.push(kette);
                }
            }
        }
        ketten
    }

    // Färbt ein Feld und alle angrenzenden Rohbauten in der Farbe der Kette
    fn ausbreiten(&mut self, start: Feld, kette: Kette) {
        let mut offen = vec![start];
        while let Some(feld) = offen.pop() {
            self.brett[feld.index()] = Belegung::Hotel(kette);
            for nachbar in feld.nachbarn() {
                if self.belegung(nachbar) == Belegung::Rohbau {
                    offen.push(nachbar);
                }
            }
        }
    }
}
ZusammenfassungEin enum mit Daten wie Hotel(Kette) merkt sich nicht nur was, sondern auch welches. Das Brett ist eine Liste mit 108 Plätzen, index findet den richtigen. Die Flutfüllung breitet eine Kette über alle verbundenen Felder aus. Ein Bauplan beschreibt ein Haus als Daten, und eine einzige Funktion kann daraus Rohbauten, Hotels und sieben Dachformen zeichnen. Private Hilfsfunktionen sorgen dafür, dass die Oberfläche die Regeln nicht umgehen kann.
Übungen

Zum Ausprobieren

  1. Wolkenkratzer: Lass Hotels doppelt so stark wachsen. Welche Zahl änderst du?
  2. Eigene Handschrift: Gib den Zugvögeln statt des Flachdachs einen Dachgarten. Wie viele Zeilen sind das?
  3. Test für die Flutfüllung: Schreib einen Test: Ein Rohbau auf 1A, ein Rohbau auf 2A, dann legt man 3A neben eine bestehende Kette auf 4A. Hinterher müssen alle vier Felder zur Kette gehören. Tipp: Im Test darfst du brett und hand direkt vorbereiten.
Lösungen anzeigen

1. In hotel_bauplan die 1.5 durch 3.0 ersetzen. Die Wurzel bleibt, damit auch große Ketten noch ins Fenster passen.

2. Eine Zeile in dachform: Kette::Zugvogel => Dach::Garten,. Weil jede Kette ihr Dach an genau einer Stelle bekommt, ist Umgestalten ganz einfach.

3. Ans Ende von spiel.rs:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn kette_schluckt_verbundene_rohbauten() {
        let mut spiel = Spiel::neu();
        let feld = |spalte| Feld { spalte, reihe: 0 };
        spiel.brett[feld(0).index()] = Belegung::Rohbau;
        spiel.brett[feld(1).index()] = Belegung::Rohbau;
        spiel.brett[feld(3).index()] = Belegung::Hotel(Kette::Aurora);
        spiel.hand = vec![feld(2)];

        assert!(spiel.legen(feld(2)));
        assert_eq!(spiel.groesse(Kette::Aurora), 4);
    }
}

feld ist hier eine kleine Closure, die aus einer Spaltennummer ein Feld in Reihe A macht. assert! prüft, dass legen geklappt hat.

Etappe 3 geschafft 🏨

Deine Stadt hat jetzt Hotelketten mit eigenem Gesicht. In der nächsten Etappe kommt Geld ins Spiel: Jede Kette hat einen Aktienkurs, der mit ihrer Größe steigt, und du kannst Anteile kaufen. Wer eine Kette gründet, bekommt eine Aktie geschenkt.

Hat dir das Mitbauen Spaß gemacht? Ein Kaffee hilft, dass die nächste Etappe ein Stück schneller fertig wird.

☕ Kaffee spendieren