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

Fusionen

Jetzt wird es ernst. Legst du ein Plättchen zwischen zwei Ketten, schluckt die größere die kleinere, und über Nacht wechseln ganze Straßenzüge die Farbe. Die größten Aktionäre der geschluckten Kette kassieren eine Prämie, und danach entscheidet jeder, ob er seine Aktien verkauft, tauscht oder behält.

3 Schritte · ca. 70 Minuten · iced 0.14

Eine Karte mit der Überschrift Fusion über dem Spielbrett: Uferblick schluckt Morgenrot, darunter drei Knöpfe zum Verkaufen, Tauschen oder Behalten
So sieht der Baulöwe am Ende dieser Etappe aus.
Schritt 1

Die Großen schlucken die Kleinen

Bisher war es verboten, ein Plättchen zwischen zwei Ketten zu legen. Ab jetzt ist genau das der spannendste Zug im Spiel: eine Fusion. Die größere Kette übernimmt die kleinere, und alle Hotels der kleineren wechseln die Farbe. Eine Ausnahme gibt es: Ketten mit 11 oder mehr Feldern sind sicher. Sie können nicht mehr geschluckt werden, und zwei sichere Ketten kann man deshalb auch nicht verbinden. Die Grenze kommt als Konstante nach oben:

src/spiel.rsunter const KAEUFE_JE_ZUG
pub const KAEUFE_JE_ZUG: u32 = 3;
// Ab dieser Größe kann eine Kette nicht mehr geschluckt werden
pub const SICHER: usize = 11;

In darf_legen fällt das alte Verbot weg. Stattdessen zählen wir, wie viele der angrenzenden Ketten sicher sind:

src/spiel.rsin fn darf_legen
        let ketten = self.ketten_neben(feld);
        // Zwei Ketten auf einmal berühren: Das ist eine Fusion, die kommt später
        if ketten.len() > 1 {        // Zwei sichere Ketten kann niemand zusammenlegen
        let sichere = ketten
            .iter()
            .filter(|&&kette| self.groesse(kette) >= SICHER)
            .count();
        if sichere >= 2 {            return false;
        }

filter(|&&kette| …) hat zwei &, weil iter() Verweise liefert und filter noch einmal einen Verweis darauf. Mit beiden & im Muster packen wir beide aus. In legen kommt die Fusion als erster Fall dazu:

src/spiel.rsin fn legen, ganz unten
        self.brett[feld.index()] = Belegung::Rohbau;
        if let Some(&kette) = ketten.first() {        if ketten.len() > 1 {
            self.fusion(feld, ketten);
        } else if let Some(&kette) = ketten.first() {            self.ausbreiten(feld, kette);

Die Fusion selbst ist in dieser ersten Fassung kurz: Ketten nach Größe sortieren, die größte überlebt, alle anderen werden umgefärbt. Die neuen Methoden kommen vor gruenden:

src/spiel.rsvor // Gründet an der wartenden Stelle …
    // Das Plättchen verbindet mehrere Ketten: Die größte schluckt die anderen
    fn fusion(&mut self, feld: Feld, mut ketten: Vec<Kette>) {
        // Größte zuerst. Bei Gleichstand gewinnt die teurere Kette.
        ketten.sort_by_key(|&kette| Reverse((self.groesse(kette), kette.stufe())));
        let ueberlebende = ketten[0];

        for &tot in &ketten[1..] {
            self.notiere(format!(
                "lässt {} {} schlucken",
                ueberlebende.name(),
                tot.name()
            ));
            self.umfaerben(tot, ueberlebende);
        }
        self.ausbreiten(feld, ueberlebende);
        self.nach_dem_legen();
    }

    // Alle Hotels einer Kette gehören ab jetzt einer anderen
    fn umfaerben(&mut self, alt: Kette, neu: Kette) {
        for belegung in self.brett.iter_mut() {
            if *belegung == Belegung::Hotel(alt) {
                *belegung = Belegung::Hotel(neu);
            }
        }
    }
    // Gründet an der wartenden Stelle eine neue Kette
  • Sortieren nach zwei Dingen. sort_by_key sortiert nach einem Paar aus Größe und Preisstufe. Paare vergleicht Rust Stelle für Stelle: erst die Größe, und nur bei Gleichstand die Stufe. Reverse dreht die Reihenfolge um, damit die größte vorne steht. Dafür braucht es oben use std::cmp::Reverse;.
  • Eine Hausregel. Im Original entscheidet bei gleich großen Ketten der Spieler, der die Fusion auslöst. Wir machen es uns einfacher: Die teurere Kette gewinnt, und bei gleicher Preisstufe die, die zuerst gefunden wurde.
  • iter_mut() geht das Brett durch und erlaubt, jeden Eintrag zu verändern. Mit *belegung greifen wir auf den Wert hinter dem Verweis zu.
  • &ketten[1..] sind alle Ketten außer der ersten, also alle, die geschluckt werden.
src/spiel.rsganz oben
use std::cmp::Reverse;
use rand::seq::SliceRandom;

In der Kettentafel soll man sichere Ketten erkennen:

src/main.rsganz oben und in fn view, Kettentafel
    Art, Belegung, Feld, KAEUFE_JE_ZUG, Kette, Phase, REIHEN, SPALTEN, Spiel, Spieler, Zug,    Art, Belegung, Feld, KAEUFE_JE_ZUG, Kette, Phase, REIHEN, SICHER, SPALTEN, Spiel, Spieler, Zug,⋮            } else {                (format!("{groesse} Felder"), geld(self.spiel.preis(kette)))                // Sichere Ketten kann niemand mehr schlucken
                let wort = if groesse >= SICHER {
                    "sicher"
                } else {
                    "Felder"
                };
                (format!("{groesse} {wort}"), geld(self.spiel.preis(kette)))            };
Probelauf Speichern, dann im Terminal:
cargo run
Leg ein Plättchen zwischen zwei Ketten. Die kleinere färbt sich schlagartig in die Farbe der größeren, und weil die Kette jetzt viel größer ist, wachsen alle ihre Hotels in die Höhe. Im Protokoll steht, wer wen geschluckt hat. Erreicht eine Kette 11 Felder, steht in der Tafel „sicher“.
Schritt 2

Die Prämie

Wer Aktien einer Kette hält, die geschluckt wird, soll nicht leer ausgehen. Die beiden größten Aktionäre bekommen eine Prämie: der größte das Zehnfache des Aktienkurses, der zweitgrößte das Fünffache. Der Kurs zählt dabei, bevor die Kette verschwindet. Es gibt zwei Sonderfälle: Hält nur einer Aktien, bekommt er beide Prämien. Und teilen sich mehrere den ersten Platz, teilen sie sich beide Prämien, und für Platz zwei bleibt nichts. Die Prämie zahlen wir in fusion direkt vor dem Umfärben:

src/spiel.rsin fn fusion
            ));            self.praemien_zahlen(tot);            self.umfaerben(tot, ueberlebende);
src/spiel.rsvor // Alle Hotels einer Kette gehören …
    // Die beiden größten Aktionäre der geschluckten Kette bekommen eine Prämie
    fn praemien_zahlen(&mut self, kette: Kette) {
        let preis = self.preis(kette);
        let bestand: Vec<u32> = self.spieler.iter().map(|s| s.aktien[kette.nr()]).collect();

        let meiste = bestand.iter().copied().max().unwrap_or(0);
        if meiste == 0 {
            return;
        }
        let zweitmeiste = bestand
            .iter()
            .copied()
            .filter(|&b| b < meiste)
            .max()
            .unwrap_or(0);

        let erste: Vec<usize> = (0..bestand.len())
            .filter(|&nr| bestand[nr] == meiste)
            .collect();
        let zweite: Vec<usize> = (0..bestand.len())
            .filter(|&nr| zweitmeiste > 0 && bestand[nr] == zweitmeiste)
            .collect();

        let gross = preis * 10;
        let klein = preis * 5;
        if erste.len() > 1 || zweite.is_empty() {
            // Gleichstand an der Spitze oder nur einer hält Aktien: Alles wird geteilt
            self.auszahlen(&erste, gross + klein);
        } else {
            self.auszahlen(&erste, gross);
            self.auszahlen(&zweite, klein);
        }
    }

    // Teilt eine Summe gerecht auf, aufgerundet auf volle Hundert
    fn auszahlen(&mut self, empfaenger: &[usize], summe: u32) {
        let anteil = (summe / empfaenger.len() as u32).div_ceil(100) * 100;
        for &nr in empfaenger {
            self.spieler[nr].geld += anteil;
            let eintrag = format!("{} erhält {anteil} € Prämie", self.spieler[nr].name);
            self.protokoll.push(eintrag);
        }
    }
    // Alle Hotels einer Kette gehören ab jetzt einer anderen
  • Erst sammeln, dann rechnen. bestand ist eine Liste mit den Aktien jedes Spielers an dieser Kette, in der Reihenfolge der Spieler.
  • Die meisten und die zweitmeisten. max findet den größten Wert. Für den zweitgrößten filtern wir alles heraus, was so groß ist wie der größte, und nehmen davon wieder das Maximum. Weil die Liste leer sein könnte, liefert max ein Option, das unwrap_or(0) auflöst.
  • Wer steht auf dem Treppchen? (0..bestand.len()).filter(…) geht alle Spielernummern durch und behält die, deren Bestand passt. Dabei kann es mehrere Gewinner geben.
  • Aufrunden auf Hundert. div_ceil(100) teilt durch 100 und rundet dabei auf. Mal 100 ergibt den aufgerundeten Betrag. Aus 2.500 € geteilt durch drei, also 833,33 €, werden so 900 €.
Probelauf Speichern, dann im Terminal:
cargo run
Kauf ein paar Aktien einer kleinen Kette und schluck sie dann mit einer größeren. Deine Kasse springt nach oben, und im Protokoll steht, wer wie viel Prämie bekommen hat.
Schritt 3

Verkaufen, tauschen oder behalten

Nach der Prämie hat jeder Aktionär der geschluckten Kette eine Entscheidung zu treffen. Er kann seine Aktien zum aktuellen Kurs an die Bank verkaufen, sie zwei zu eins gegen Aktien der überlebenden Kette tauschen, oder sie behalten und darauf hoffen, dass die Kette später neu gegründet wird. Reihum entscheidet jeder Aktionär, beginnend mit dem, der die Fusion ausgelöst hat.

Damit wird die Fusion zu einer eigenen Phase, und in dieser Phase ist nicht der Spieler am Zug dran, sondern ein Aktionär nach dem anderen. Die Phase merkt sich, wo gelegt wurde und welche Kette wen schluckt. Und die möglichen Entscheidungen werden ein eigenes enum. Die Zeile für Kaufen ändert sich nur optisch: Sobald ein enum einen Fall mit mehreren benannten Feldern hat, schreibt cargo fmt alle solchen Fälle über mehrere Zeilen.

src/spiel.rsin enum Phase und darunter
    // Aktien kaufen, bis zu drei Stück    Kaufen { gekauft: u32 },    Kaufen {
        gekauft: u32,
    },    // Eine Kette wird geschluckt, die Aktionäre entscheiden reihum
    Fusion {
        feld: Feld,
        tot: Kette,
        ueberlebende: Kette,
    },
}

// Was ein Aktionär mit Aktien einer geschluckten Kette macht
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Entscheidung {
    Verkaufen,
    Tauschen,
    Behalten,
}
// Alles, was ein Spieler tun kann
src/spiel.rsin enum Zug
    Kaufen(Kette),    Aktien(Entscheidung),    ZugBeenden,

Bei mehreren geschluckten Ketten auf einmal arbeiten wir sie der Reihe nach ab. Dafür bekommt das Spiel zwei Warteschlangen:

src/spiel.rsin struct Spiel und in fn neu
    pub protokoll: Vec<String>,    // Bei einer Fusion: Diese Ketten werden noch geschluckt
    pub offene_fusionen: Vec<Kette>,
    // Diese Aktionäre müssen noch entscheiden, der vorderste zuerst
    pub entscheider: Vec<usize>,⋮            protokoll: Vec::new(),            offene_fusionen: Vec::new(),
            entscheider: Vec::new(),

Jetzt der Trick, der den Rest des Programms fast unverändert lässt: Wir fragen nicht mehr „wer ist am Zug?“, sondern „wer muss gerade handeln?“. Das ist fast immer der Spieler am Zug, außer während einer Fusion. Weil aktueller und dran das ab jetzt berücksichtigen, funktionieren alle Methoden, die sie benutzen, auch für die Aktionäre:

src/spiel.rsaktueller und dran ersetzen
    // Der Spieler, der gerade dran ist
    pub fn aktueller(&self) -> &Spieler {
        &self.spieler[self.am_zug]
    }

    // Dasselbe zum Verändern
    fn dran(&mut self) -> &mut Spieler {
        &mut self.spieler[self.am_zug]
    }    // Wer gerade handeln muss: Bei einer Fusion entscheiden die Aktionäre reihum
    pub fn handelnder(&self) -> usize {
        match self.phase {
            Phase::Fusion { .. } => self.entscheider[0],
            _ => self.am_zug,
        }
    }

    // Der Spieler, der gerade handeln muss
    pub fn aktueller(&self) -> &Spieler {
        &self.spieler[self.handelnder()]
    }

    // Dasselbe zum Verändern
    fn dran(&mut self) -> &mut Spieler {
        let nr = self.handelnder();
        &mut self.spieler[nr]
    }
    // Schreibt ins Protokoll, was der Spieler am Zug getan hat

In dran merken wir uns die Nummer zuerst in einer Variable. Ein direktes &mut self.spieler[self.handelnder()] ginge nicht, weil Rust dann gleichzeitig verändern und lesen müsste. Nun wird fusion zum Auftakt, und drei neue Methoden übernehmen den Ablauf. Ersetze den Rest von fusion bis vor praemien_zahlen:

src/spiel.rsin fn fusion ab let ueberlebende, bis vor praemien_zahlen
        let ueberlebende = ketten[0];

        for &tot in &ketten[1..] {
            self.notiere(format!(
                "lässt {} {} schlucken",
                ueberlebende.name(),
                tot.name()
            ));
            self.praemien_zahlen(tot);
            self.umfaerben(tot, ueberlebende);
        }
        self.ausbreiten(feld, ueberlebende);
        self.nach_dem_legen();
    }        let ueberlebende = ketten[0];
        self.offene_fusionen = ketten[1..].to_vec();
        self.naechste_fusion(feld, ueberlebende);
    }

    // Nimmt sich die nächste Kette vor, die geschluckt wird
    fn naechste_fusion(&mut self, feld: Feld, ueberlebende: Kette) {
        if self.offene_fusionen.is_empty() {
            self.ausbreiten(feld, ueberlebende);
            self.nach_dem_legen();
            return;
        }
        let tot = self.offene_fusionen.remove(0);
        let eintrag = format!(
            "{}: {} schluckt {}",
            self.spieler[self.am_zug].name,
            ueberlebende.name(),
            tot.name()
        );
        self.protokoll.push(eintrag);
        self.praemien_zahlen(tot);

        // Reihum, beim Spieler am Zug beginnend, entscheidet jeder, der Aktien hat
        let anzahl = self.spieler.len();
        let entscheider: Vec<usize> = (0..anzahl)
            .map(|schritt| (self.am_zug + schritt) % anzahl)
            .filter(|&nr| self.spieler[nr].aktien[tot.nr()] > 0)
            .collect();
        self.entscheider = entscheider;

        if self.entscheider.is_empty() {
            self.umfaerben(tot, ueberlebende);
            self.naechste_fusion(feld, ueberlebende);
        } else {
            self.phase = Phase::Fusion {
                feld,
                tot,
                ueberlebende,
            };
        }
    }

    // Ein Aktionär entscheidet, was mit seinen Aktien der geschluckten Kette passiert
    fn entscheiden(&mut self, wahl: Entscheidung) {
        let Phase::Fusion {
            feld,
            tot,
            ueberlebende,
        } = self.phase
        else {
            return;
        };
        let preis = self.preis(tot);
        let anzahl = self.aktueller().aktien[tot.nr()];

        match wahl {
            Entscheidung::Verkaufen => {
                let spieler = self.dran();
                spieler.geld += anzahl * preis;
                spieler.aktien[tot.nr()] = 0;
                self.bank[tot.nr()] += anzahl;
                self.notiere(format!("verkauft {anzahl} {}", tot.name()));
            }
            Entscheidung::Tauschen => {
                // Zwei alte gegen eine neue, solange die Bank welche hat
                let paare = (anzahl / 2).min(self.bank[ueberlebende.nr()]);
                let spieler = self.dran();
                spieler.aktien[tot.nr()] -= paare * 2;
                spieler.aktien[ueberlebende.nr()] += paare;
                self.bank[tot.nr()] += paare * 2;
                self.bank[ueberlebende.nr()] -= paare;
                self.notiere(format!("tauscht in {paare} {}", ueberlebende.name()));
            }
            Entscheidung::Behalten => {
                self.notiere(format!("behält {anzahl} {}", tot.name()));
            }
        }

        self.entscheider.remove(0);
        if self.entscheider.is_empty() {
            self.umfaerben(tot, ueberlebende);
            self.naechste_fusion(feld, ueberlebende);
        }
    }
    // Die beiden größten Aktionäre der geschluckten Kette bekommen eine Prämie

So läuft eine Fusion jetzt ab:

  • fusion sortiert die Ketten, merkt sich alle zu schluckenden in offene_fusionen und startet mit der ersten.
  • naechste_fusion nimmt die nächste Kette aus der Warteschlange, zahlt die Prämien und stellt die Reihe der Aktionäre auf. Die Rechnung (self.am_zug + schritt) % anzahl zählt reihum ab dem Spieler am Zug, einmal um den Tisch. Hat niemand Aktien, geht es sofort mit der nächsten Kette weiter. Ist keine mehr übrig, breitet sich die Überlebende über das neue Plättchen aus, und der Zug geht mit dem Kaufen weiter.
  • entscheiden setzt eine Wahl um. Beim Tauschen gilt: Für je zwei alte gibt es eine neue Aktie, aber nur so viele, wie die Bank noch hat. min nimmt den kleineren der beiden Werte. Danach ist der nächste Aktionär dran. Wenn alle entschieden haben, wird umgefärbt, und naechste_fusion macht weiter.
Worauf du achten musst naechste_fusion ruft sich selbst auf. Das nennt man Rekursion. Das ist hier ungefährlich, weil die Warteschlange bei jedem Aufruf um eine Kette kürzer wird und irgendwann leer ist. Im Protokoll steht der Name aus self.spieler[self.am_zug] statt über notiere, denn in dem Moment steckt das Spiel mitten im Wechsel zwischen zwei Phasen.

Die Computergegner brauchen noch ihre Auswahl, und ausfuehren leitet die Entscheidung weiter:

src/spiel.rsin fn moegliche_zuege und fn ausfuehren
                zuege.push(Zug::ZugBeenden);
            }            Phase::Fusion { .. } => {
                zuege.push(Zug::Aktien(Entscheidung::Verkaufen));
                zuege.push(Zug::Aktien(Entscheidung::Tauschen));
                zuege.push(Zug::Aktien(Entscheidung::Behalten));
            }        }
        zuege⋮            Zug::Kaufen(kette) => self.kaufen(kette),            Zug::Aktien(wahl) => self.entscheiden(wahl),

Weiter in main.rs. Wir brauchen gleich zwei Dialoge, die sich sehr ähneln: den für die Gründung und einen neuen für die Fusion. Statt alles doppelt zu schreiben, ziehen wir die gemeinsamen Teile in zwei freie Funktionen, einen farbigen Knopf und ein Dialogfenster. Sie kommen vor das Vermessungsfähnchen:

src/main.rsvor // Ein Vermessungsfähnchen …
// Ein farbiger Knopf, der einen Spielzug verschickt
fn farbknopf<'a>(beschriftung: &str, farbe: Color, zug: Zug) -> Element<'a, Message> {
    button(text(beschriftung.to_string()).size(14))
        .padding([8, 12])
        .on_press(Message::Spielzug(zug))
        .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()
            }
        })
        .into()
}

// Eine Karte mit Titel, Frage und Knöpfen über einem dunklen Schleier
fn dialogfenster<'a>(
    farben: Farben,
    titel: &'a str,
    frage: String,
    knoepfe: Row<'a, Message>,
) -> Element<'a, Message> {
    let titel = text(titel)
        .size(24)
        .font(Font {
            weight: font::Weight::Bold,
            ..Font::DEFAULT
        })
        .color(farben.schrift);
    let frage = text(frage)
        .size(15)
        .color(farben.schrift)
        .align_x(text::Alignment::Center);

    let karte = container(
        column![titel, frage, knoepfe]
            .spacing(14)
            .align_x(alignment::Horizontal::Center),
    )
    .padding(24)
    .max_width(640)
    .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()
}
// Ein Vermessungsfähnchen: Hier darfst du bauen

<'a> ist eine Lebensdauer. Sie sagt Rust: Das Dialogfenster lebt so lange wie der Titel und die Knöpfe, die es bekommt. Mehr musst du dafür im Moment nicht wissen. max_width(640) sorgt dafür, dass lange Fragen umbrechen, statt das Fenster zu sprengen. Damit schrumpft der Gründungsdialog auf ein paar Zeilen, und der Fusionsdialog kommt dazu:

src/main.rsfn gruendungsdialog komplett ersetzen, darunter fn fusionsdialog
    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::Spielzug(Zug::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 gruendungsdialog(&self, farben: Farben) -> Element<'_, Message> {
        let mut knoepfe = row![].spacing(8);
        for kette in self.spiel.freie_ketten() {
            let zug = Zug::Gruenden(kette);
            knoepfe = knoepfe.push(farbknopf(kette.name(), kettenfarbe(kette), zug));
        }
        let frage = "Deine Häuser stehen jetzt nebeneinander. Welche Kette gründest du?";
        dialogfenster(farben, "Neue Hotelkette!", frage.to_string(), knoepfe)
    }

    // Das Fenster für deine Aktien einer Kette, die gerade geschluckt wird
    fn fusionsdialog(
        &self,
        farben: Farben,
        tot: Kette,
        ueberlebende: Kette,
    ) -> Element<'_, Message> {
        let anzahl = self.ich().aktien[tot.nr()];
        let preis = self.spiel.preis(tot);
        let frage = format!(
            "{} schluckt {}. Du hast {anzahl} Aktien von {}, eine ist {} wert. Was machst du damit?",
            ueberlebende.name(),
            tot.name(),
            tot.name(),
            geld(preis)
        );

        let verkaufen = format!("Verkaufen für {}", geld(anzahl * preis));
        let tauschen = format!("2 zu 1 in {} tauschen", ueberlebende.name());
        let knoepfe = row![
            farbknopf(
                &verkaufen,
                kettenfarbe(tot),
                Zug::Aktien(Entscheidung::Verkaufen)
            ),
            farbknopf(
                &tauschen,
                kettenfarbe(ueberlebende),
                Zug::Aktien(Entscheidung::Tauschen)
            ),
            farbknopf(
                "Behalten",
                farben.akzent,
                Zug::Aktien(Entscheidung::Behalten)
            ),
        ]
        .spacing(8);
        dialogfenster(farben, "Fusion!", frage, knoepfe)
    }
    fn farben(&self) -> Farben {
src/main.rsin fn view, weiter unten
        let mut ebenen = stack![brett, folie];        if self.mensch_am_zug() && matches!(self.spiel.phase, Phase::Gruenden(_)) {
            ebenen = ebenen.push(self.gruendungsdialog(farben));        if self.mensch_am_zug() {
            match self.spiel.phase {
                Phase::Gruenden(_) => ebenen = ebenen.push(self.gruendungsdialog(farben)),
                Phase::Fusion {
                    tot, ueberlebende, ..
                } => ebenen = ebenen.push(self.fusionsdialog(farben, tot, ueberlebende)),
                _ => {}
            }        }
        ebenen.into()

Das match packt aus der Fusionsphase gleich die beiden Ketten aus. .. steht für „der Rest ist mir egal“, hier das Feld. Oben brauchen wir noch Row und Entscheidung:

src/main.rsganz oben
use iced::widget::{button, canvas, column, container, row, space, stack, text};use iced::widget::{Row, button, canvas, column, container, row, space, stack, text};⋮    Art, Belegung, Feld, KAEUFE_JE_ZUG, Kette, Phase, REIHEN, SICHER, SPALTEN, Spiel, Spieler, Zug,    Art, Belegung, Entscheidung, Feld, KAEUFE_JE_ZUG, Kette, Phase, REIHEN, SICHER, SPALTEN, Spiel,
    Spieler, Zug,
Probelauf Speichern, dann im Terminal:
cargo run
Wird eine Kette geschluckt, von der du Aktien hast, erscheint die Karte „Fusion!“ mit drei Knöpfen. Auch die Touristen müssen jetzt entscheiden, das Protokoll verrät, wie: „Toni verkauft 3 Morgenrot“, „Tine tauscht in 1 Uferblick“. Und weil mensch_am_zug jetzt den Handelnden fragt, wartet das Spiel geduldig, wenn du mitten im Zug eines Gegners entscheiden musst.
Zwischenstand

Der komplette Code nach Etappe 6

Hier ist der vollständige Stand nach Etappe 6. 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, src/ki.rs anzeigen

Cargo.toml

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

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

src/main.rs

mod ki;
mod spiel;

use std::time::Duration;

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

// 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((1280.0, 760.0))
        .antialiasing(true)
        .subscription(Bauloewe::abo)
        .run()
}

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

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

impl Bauloewe {
    fn neu() -> Bauloewe {
        Bauloewe {
            spiel: Spiel::neu(&[
                ("Leo", Art::Mensch),
                ("Toni", Art::Tourist),
                ("Tine", Art::Tourist),
                ("Theo", Art::Tourist),
            ]),
            maus_feld: None,
            nacht: false,
        }
    }

    // Du bist immer Spieler Nummer 0
    fn ich(&self) -> &Spieler {
        &self.spiel.spieler[0]
    }

    fn mensch_am_zug(&self) -> bool {
        self.spiel.aktueller().art == Art::Mensch
    }

    // Ist der Computer dran, tickt alle 0,6 Sekunden ein Takt
    fn abo(&self) -> Subscription<Message> {
        if self.mensch_am_zug() {
            Subscription::none()
        } else {
            time::every(Duration::from_millis(600)).map(|_| Message::KiTakt)
        }
    }

    fn update(&mut self, message: Message) {
        match message {
            Message::MausUeber(feld) => self.maus_feld = feld,
            Message::TagNacht => self.nacht = !self.nacht,
            // Züge vom Menschen zählen nur, wenn er dran ist
            Message::Geklickt(feld) if self.mensch_am_zug() => {
                self.spiel.ausfuehren(Zug::Legen(feld))
            }
            Message::Spielzug(zug) if self.mensch_am_zug() => self.spiel.ausfuehren(zug),
            Message::Geklickt(_) | Message::Spielzug(_) => {}
            // Der Takt für den Computer
            Message::KiTakt => {
                if !self.mensch_am_zug() {
                    let zug = ki::waehle_zug(&self.spiel);
                    self.spiel.ausfuehren(zug);
                }
            }
        }
    }

    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 {
            _ if !self.mensch_am_zug() => format!("{} ist am Zug …", self.spiel.aktueller().name),
            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 kasse = text(format!("Kasse: {}", geld(self.ich().geld)))
            .size(17)
            .font(Font {
                weight: font::Weight::Bold,
                ..Font::DEFAULT
            })
            .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, kasse, 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.ich().hand {
            let darf = self.mensch_am_zug() && self.spiel.darf_legen(feld);
            let plaettchen = button(text(feld.name()).size(16).font(Font::MONOSPACE))
                .padding([8, 14])
                .on_press_maybe(darf.then_some(Message::Geklickt(feld)))
                .style(move |_theme: &Theme, status| {
                    let grund = match status {
                        button::Status::Hovered => farben.rasen_hell,
                        button::Status::Disabled => Color {
                            a: 0.5,
                            ..farben.stein
                        },
                        _ => 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);

        // In der Kaufphase gibt es einen Knopf, der den Zug beendet
        let mut rechts = row![stapel]
            .spacing(16)
            .align_y(alignment::Vertical::Center);
        let ansage = match self.spiel.phase {
            _ if !self.mensch_am_zug() => None,
            Phase::Kaufen { gekauft } => Some(format!(
                "Du darfst noch {} Aktien kaufen",
                KAEUFE_JE_ZUG - gekauft
            )),
            Phase::Legen if !self.spiel.kann_legen() => {
                Some(String::from("Keins deiner Plättchen passt gerade"))
            }
            _ => None,
        };
        if let Some(ansage) = ansage {
            let rest = text(ansage).size(14).color(farben.schrift);
            let fertig = button(text("Zug beenden").size(14))
                .padding([8, 16])
                .on_press(Message::Spielzug(Zug::ZugBeenden))
                .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()
                });
            rechts = rechts.push(rest).push(fertig);
        }

        let fussleiste = row![hand, space::horizontal(), rechts]
            .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);
        let klein = |inhalt: &str, breite: f32| {
            text(inhalt.to_string())
                .size(11)
                .color(farben.beschriftung)
                .width(breite)
        };
        ketten = ketten.push(
            row![
                klein("", 12.0),
                klein("Kette", 90.0),
                klein("Größe", 70.0),
                klein("Kurs", 70.0),
                klein("Du", 30.0),
                klein("Bank", 30.0),
            ]
            .spacing(8),
        );
        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, kurs) = if groesse == 0 {
                (String::from("frei"), String::new())
            } else {
                // Sichere Ketten kann niemand mehr schlucken
                let wort = if groesse >= SICHER {
                    "sicher"
                } else {
                    "Felder"
                };
                (format!("{groesse} {wort}"), geld(self.spiel.preis(kette)))
            };
            let zeile = row![
                punkt,
                text(kette.name()).size(13).color(farben.schrift).width(90),
                text(angabe).size(13).color(farben.schrift).width(70),
                text(kurs).size(13).color(farben.schrift).width(70),
                text(self.ich().aktien[kette.nr()].to_string())
                    .size(13)
                    .color(farben.schrift)
                    .width(30),
                text(self.spiel.bank[kette.nr()].to_string())
                    .size(13)
                    .color(farben.schrift)
                    .width(30),
                button(text("+").size(14))
                    .padding([1, 9])
                    .on_press_maybe(
                        (self.mensch_am_zug() && self.spiel.darf_kaufen(kette))
                            .then_some(Message::Spielzug(Zug::Kaufen(kette)))
                    )
                    .style(move |_theme: &Theme, status| {
                        let grund = match status {
                            button::Status::Disabled => Color {
                                a: 0.25,
                                ..farben.akzent
                            },
                            button::Status::Hovered => aufhellen(farben.akzent, 0.2),
                            _ => farben.akzent,
                        };
                        button::Style {
                            background: Some(Background::Color(grund)),
                            text_color: Color::WHITE,
                            border: Border {
                                radius: 12.0.into(),
                                ..Border::default()
                            },
                            ..button::Style::default()
                        }
                    }),
            ]
            .spacing(8)
            .align_y(alignment::Vertical::Center);
            ketten = ketten.push(zeile);
        }
        let tafel_stil = 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 tafel = container(ketten).padding(14).style(tafel_stil);

        // Links oben: wer mitspielt und was zuletzt passiert ist
        let mut tisch = column![
            text("Am Tisch")
                .size(15)
                .font(Font {
                    weight: font::Weight::Bold,
                    ..Font::DEFAULT
                })
                .color(farben.schrift)
        ]
        .spacing(4);
        for (nr, spieler) in self.spiel.spieler.iter().enumerate() {
            let aktien: u32 = spieler.aktien.iter().sum();
            let zeile = row![
                text(spieler.name.as_str())
                    .size(13)
                    .color(farben.schrift)
                    .width(60),
                text(geld(spieler.geld))
                    .size(13)
                    .color(farben.schrift)
                    .width(80),
                text(format!("Aktien: {aktien}"))
                    .size(13)
                    .color(farben.schrift),
            ]
            .spacing(8);

            // Wer am Zug ist, bekommt einen farbigen Hintergrund
            let am_zug = nr == self.spiel.am_zug;
            let zeile = container(zeile)
                .padding([3, 8])
                .style(move |_theme: &Theme| container::Style {
                    background: am_zug.then_some(Background::Color(Color {
                        a: 0.35,
                        ..farben.akzent
                    })),
                    border: Border {
                        radius: 6.0.into(),
                        ..Border::default()
                    },
                    ..container::Style::default()
                });
            tisch = tisch.push(zeile);
        }
        tisch = tisch.push(text("Zuletzt").size(11).color(farben.beschriftung));
        for eintrag in self.spiel.protokoll.iter().rev().take(5) {
            tisch = tisch.push(text(eintrag.as_str()).size(12).color(farben.schrift));
        }
        let tisch = container(tisch).padding(14).width(270).style(tafel_stil);

        let tafelzeile = row![tisch, 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.mensch_am_zug() {
            match self.spiel.phase {
                Phase::Gruenden(_) => ebenen = ebenen.push(self.gruendungsdialog(farben)),
                Phase::Fusion {
                    tot, ueberlebende, ..
                } => ebenen = ebenen.push(self.fusionsdialog(farben, tot, ueberlebende)),
                _ => {}
            }
        }
        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 zug = Zug::Gruenden(kette);
            knoepfe = knoepfe.push(farbknopf(kette.name(), kettenfarbe(kette), zug));
        }
        let frage = "Deine Häuser stehen jetzt nebeneinander. Welche Kette gründest du?";
        dialogfenster(farben, "Neue Hotelkette!", frage.to_string(), knoepfe)
    }

    // Das Fenster für deine Aktien einer Kette, die gerade geschluckt wird
    fn fusionsdialog(
        &self,
        farben: Farben,
        tot: Kette,
        ueberlebende: Kette,
    ) -> Element<'_, Message> {
        let anzahl = self.ich().aktien[tot.nr()];
        let preis = self.spiel.preis(tot);
        let frage = format!(
            "{} schluckt {}. Du hast {anzahl} Aktien von {}, eine ist {} wert. Was machst du damit?",
            ueberlebende.name(),
            tot.name(),
            tot.name(),
            geld(preis)
        );

        let verkaufen = format!("Verkaufen für {}", geld(anzahl * preis));
        let tauschen = format!("2 zu 1 in {} tauschen", ueberlebende.name());
        let knoepfe = row![
            farbknopf(
                &verkaufen,
                kettenfarbe(tot),
                Zug::Aktien(Entscheidung::Verkaufen)
            ),
            farbknopf(
                &tauschen,
                kettenfarbe(ueberlebende),
                Zug::Aktien(Entscheidung::Tauschen)
            ),
            farbknopf(
                "Behalten",
                farben.akzent,
                Zug::Aktien(Entscheidung::Behalten)
            ),
        ]
        .spacing(8);
        dialogfenster(farben, "Fusion!", frage, knoepfe)
    }

    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.mensch_am_zug() && self.spiel.darf_legen(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.mensch_am_zug() && self.spiel.darf_legen(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 farbiger Knopf, der einen Spielzug verschickt
fn farbknopf<'a>(beschriftung: &str, farbe: Color, zug: Zug) -> Element<'a, Message> {
    button(text(beschriftung.to_string()).size(14))
        .padding([8, 12])
        .on_press(Message::Spielzug(zug))
        .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()
            }
        })
        .into()
}

// Eine Karte mit Titel, Frage und Knöpfen über einem dunklen Schleier
fn dialogfenster<'a>(
    farben: Farben,
    titel: &'a str,
    frage: String,
    knoepfe: Row<'a, Message>,
) -> Element<'a, Message> {
    let titel = text(titel)
        .size(24)
        .font(Font {
            weight: font::Weight::Bold,
            ..Font::DEFAULT
        })
        .color(farben.schrift);
    let frage = text(frage)
        .size(15)
        .color(farben.schrift)
        .align_x(text::Alignment::Center);

    let karte = container(
        column![titel, frage, knoepfe]
            .spacing(14)
            .align_x(alignment::Horizontal::Center),
    )
    .padding(24)
    .max_width(640)
    .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()
}

// 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,
    }
}

// Schreibt Geld wie in Deutschland: 6000 wird zu "6.000 €"
fn geld(betrag: u32) -> String {
    let ziffern = betrag.to_string();
    let mut mit_punkten = String::new();
    for (stelle, ziffer) in ziffern.chars().enumerate() {
        if stelle > 0 && (ziffern.len() - stelle) % 3 == 0 {
            mit_punkten.push('.');
        }
        mit_punkten.push(ziffer);
    }
    format!("{mit_punkten} €")
}

// 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 std::cmp::Reverse;

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;

// Geld und Aktien
pub const STARTGELD: u32 = 6000;
pub const AKTIEN_JE_KETTE: u32 = 25;
pub const KAEUFE_JE_ZUG: u32 = 3;

// Ab dieser Größe kann eine Kette nicht mehr geschluckt werden
pub const SICHER: usize = 11;

#[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,
    ];

    // Die Nummer der Kette: 0 bis 6
    pub fn nr(self) -> usize {
        self as usize
    }

    // Preisstufe: 0 günstig, 1 mittel, 2 teuer
    pub fn stufe(self) -> u32 {
        match self {
            Kette::Zugvogel | Kette::Ankerplatz => 0,
            Kette::Bergfried | Kette::Uferblick | Kette::Morgenrot => 1,
            Kette::Aurora | Kette::Koenigshof => 2,
        }
    }

    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),
}

// Woran das Spiel gerade ist
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Phase {
    // Warten auf ein Plättchen
    Legen,
    // Hier wartet eine neue Kette auf ihren Namen
    Gruenden(Feld),
    // Aktien kaufen, bis zu drei Stück
    Kaufen {
        gekauft: u32,
    },
    // Eine Kette wird geschluckt, die Aktionäre entscheiden reihum
    Fusion {
        feld: Feld,
        tot: Kette,
        ueberlebende: Kette,
    },
}

// Was ein Aktionär mit Aktien einer geschluckten Kette macht
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Entscheidung {
    Verkaufen,
    Tauschen,
    Behalten,
}

// Alles, was ein Spieler tun kann
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Zug {
    Legen(Feld),
    Gruenden(Kette),
    Kaufen(Kette),
    Aktien(Entscheidung),
    ZugBeenden,
}

// Wer da spielt: ein Mensch oder der Computer
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Art {
    Mensch,
    Tourist,
}

// Was einem Spieler gehört
pub struct Spieler {
    pub name: String,
    pub art: Art,
    pub geld: u32,
    pub aktien: [u32; 7],
    pub hand: Vec<Feld>,
}

// Alles, was die Spielregeln wissen müssen
pub struct Spiel {
    pub brett: Vec<Belegung>,
    pub stapel: Vec<Feld>,
    pub phase: Phase,
    pub spieler: Vec<Spieler>,
    // Wer gerade dran ist: die Nummer in der Liste der Spieler
    pub am_zug: usize,
    // Was bisher passiert ist, ein Eintrag pro Handlung
    pub protokoll: Vec<String>,
    // Bei einer Fusion: Diese Ketten werden noch geschluckt
    pub offene_fusionen: Vec<Kette>,
    // Diese Aktionäre müssen noch entscheiden, der vorderste zuerst
    pub entscheider: Vec<usize>,
    // Die Aktien, die noch niemandem gehören
    pub bank: [u32; 7],
}

impl Spiel {
    pub fn neu(teilnehmer: &[(&str, Art)]) -> 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 spieler = teilnehmer
            .iter()
            .map(|&(name, art)| Spieler {
                name: name.to_string(),
                art,
                geld: STARTGELD,
                aktien: [0; 7],
                hand: Vec::new(),
            })
            .collect();

        let mut spiel = Spiel {
            brett: vec![Belegung::Frei; SPALTEN * REIHEN],
            stapel,
            phase: Phase::Legen,
            spieler,
            am_zug: 0,
            protokoll: Vec::new(),
            offene_fusionen: Vec::new(),
            entscheider: Vec::new(),
            bank: [AKTIEN_JE_KETTE; 7],
        };

        // Reihum zieht jeder seine Hand, dann fängt der Erste an
        for nr in 0..spiel.spieler.len() {
            spiel.am_zug = nr;
            spiel.nachziehen();
        }
        spiel.am_zug = 0;
        spiel
    }

    // Wer gerade handeln muss: Bei einer Fusion entscheiden die Aktionäre reihum
    pub fn handelnder(&self) -> usize {
        match self.phase {
            Phase::Fusion { .. } => self.entscheider[0],
            _ => self.am_zug,
        }
    }

    // Der Spieler, der gerade handeln muss
    pub fn aktueller(&self) -> &Spieler {
        &self.spieler[self.handelnder()]
    }

    // Dasselbe zum Verändern
    fn dran(&mut self) -> &mut Spieler {
        let nr = self.handelnder();
        &mut self.spieler[nr]
    }

    // Schreibt ins Protokoll, was der Spieler am Zug getan hat
    fn notiere(&mut self, was: String) {
        let eintrag = format!("{} {was}", self.aktueller().name);
        self.protokoll.push(eintrag);
    }

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

    // Füllt die Hand des Spielers am Zug wieder auf, solange der Stapel reicht
    fn nachziehen(&mut self) {
        let nr = self.am_zug;
        while self.spieler[nr].hand.len() < HANDGROESSE {
            match self.stapel.pop() {
                Some(feld) => self.spieler[nr].hand.push(feld),
                None => break,
            }
        }
        self.spieler[nr]
            .hand
            .sort_by_key(|feld| (feld.spalte, feld.reihe));
    }

    // Darf der Spieler am Zug dieses Plättchen jetzt legen?
    pub fn darf_legen(&self, feld: Feld) -> bool {
        if self.phase != Phase::Legen || !self.aktueller().hand.contains(&feld) {
            return false;
        }
        let ketten = self.ketten_neben(feld);

        // Zwei sichere Ketten kann niemand zusammenlegen
        let sichere = ketten
            .iter()
            .filter(|&&kette| self.groesse(kette) >= SICHER)
            .count();
        if sichere >= 2 {
            return false;
        }
        // Eine neue Kette gründen geht nur, wenn noch eine frei ist
        let gruendet = ketten.is_empty() && self.rohbau_neben(feld);
        !(gruendet && self.freie_ketten().is_empty())
    }

    // Hat der Spieler am Zug überhaupt ein Plättchen, das passt?
    pub fn kann_legen(&self) -> bool {
        self.aktueller()
            .hand
            .iter()
            .any(|&feld| self.darf_legen(feld))
    }

    // 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.darf_legen(feld) {
            return false;
        }
        let ketten = self.ketten_neben(feld);
        let rohbau_daneben = self.rohbau_neben(feld);

        self.dran().hand.retain(|&h| h != feld);
        self.notiere(format!("legt {}", feld.name()));
        self.brett[feld.index()] = Belegung::Rohbau;

        if ketten.len() > 1 {
            self.fusion(feld, ketten);
        } else if let Some(&kette) = ketten.first() {
            self.ausbreiten(feld, kette);
            self.nach_dem_legen();
        } else if rohbau_daneben {
            self.phase = Phase::Gruenden(feld);
        } else {
            self.nach_dem_legen();
        }
        true
    }

    // Das Plättchen verbindet mehrere Ketten: Die größte schluckt die anderen
    fn fusion(&mut self, feld: Feld, mut ketten: Vec<Kette>) {
        // Größte zuerst. Bei Gleichstand gewinnt die teurere Kette.
        ketten.sort_by_key(|&kette| Reverse((self.groesse(kette), kette.stufe())));
        let ueberlebende = ketten[0];
        self.offene_fusionen = ketten[1..].to_vec();
        self.naechste_fusion(feld, ueberlebende);
    }

    // Nimmt sich die nächste Kette vor, die geschluckt wird
    fn naechste_fusion(&mut self, feld: Feld, ueberlebende: Kette) {
        if self.offene_fusionen.is_empty() {
            self.ausbreiten(feld, ueberlebende);
            self.nach_dem_legen();
            return;
        }
        let tot = self.offene_fusionen.remove(0);
        let eintrag = format!(
            "{}: {} schluckt {}",
            self.spieler[self.am_zug].name,
            ueberlebende.name(),
            tot.name()
        );
        self.protokoll.push(eintrag);
        self.praemien_zahlen(tot);

        // Reihum, beim Spieler am Zug beginnend, entscheidet jeder, der Aktien hat
        let anzahl = self.spieler.len();
        let entscheider: Vec<usize> = (0..anzahl)
            .map(|schritt| (self.am_zug + schritt) % anzahl)
            .filter(|&nr| self.spieler[nr].aktien[tot.nr()] > 0)
            .collect();
        self.entscheider = entscheider;

        if self.entscheider.is_empty() {
            self.umfaerben(tot, ueberlebende);
            self.naechste_fusion(feld, ueberlebende);
        } else {
            self.phase = Phase::Fusion {
                feld,
                tot,
                ueberlebende,
            };
        }
    }

    // Ein Aktionär entscheidet, was mit seinen Aktien der geschluckten Kette passiert
    fn entscheiden(&mut self, wahl: Entscheidung) {
        let Phase::Fusion {
            feld,
            tot,
            ueberlebende,
        } = self.phase
        else {
            return;
        };
        let preis = self.preis(tot);
        let anzahl = self.aktueller().aktien[tot.nr()];

        match wahl {
            Entscheidung::Verkaufen => {
                let spieler = self.dran();
                spieler.geld += anzahl * preis;
                spieler.aktien[tot.nr()] = 0;
                self.bank[tot.nr()] += anzahl;
                self.notiere(format!("verkauft {anzahl} {}", tot.name()));
            }
            Entscheidung::Tauschen => {
                // Zwei alte gegen eine neue, solange die Bank welche hat
                let paare = (anzahl / 2).min(self.bank[ueberlebende.nr()]);
                let spieler = self.dran();
                spieler.aktien[tot.nr()] -= paare * 2;
                spieler.aktien[ueberlebende.nr()] += paare;
                self.bank[tot.nr()] += paare * 2;
                self.bank[ueberlebende.nr()] -= paare;
                self.notiere(format!("tauscht in {paare} {}", ueberlebende.name()));
            }
            Entscheidung::Behalten => {
                self.notiere(format!("behält {anzahl} {}", tot.name()));
            }
        }

        self.entscheider.remove(0);
        if self.entscheider.is_empty() {
            self.umfaerben(tot, ueberlebende);
            self.naechste_fusion(feld, ueberlebende);
        }
    }

    // Die beiden größten Aktionäre der geschluckten Kette bekommen eine Prämie
    fn praemien_zahlen(&mut self, kette: Kette) {
        let preis = self.preis(kette);
        let bestand: Vec<u32> = self.spieler.iter().map(|s| s.aktien[kette.nr()]).collect();

        let meiste = bestand.iter().copied().max().unwrap_or(0);
        if meiste == 0 {
            return;
        }
        let zweitmeiste = bestand
            .iter()
            .copied()
            .filter(|&b| b < meiste)
            .max()
            .unwrap_or(0);

        let erste: Vec<usize> = (0..bestand.len())
            .filter(|&nr| bestand[nr] == meiste)
            .collect();
        let zweite: Vec<usize> = (0..bestand.len())
            .filter(|&nr| zweitmeiste > 0 && bestand[nr] == zweitmeiste)
            .collect();

        let gross = preis * 10;
        let klein = preis * 5;
        if erste.len() > 1 || zweite.is_empty() {
            // Gleichstand an der Spitze oder nur einer hält Aktien: Alles wird geteilt
            self.auszahlen(&erste, gross + klein);
        } else {
            self.auszahlen(&erste, gross);
            self.auszahlen(&zweite, klein);
        }
    }

    // Teilt eine Summe gerecht auf, aufgerundet auf volle Hundert
    fn auszahlen(&mut self, empfaenger: &[usize], summe: u32) {
        let anteil = (summe / empfaenger.len() as u32).div_ceil(100) * 100;
        for &nr in empfaenger {
            self.spieler[nr].geld += anteil;
            let eintrag = format!("{} erhält {anteil} € Prämie", self.spieler[nr].name);
            self.protokoll.push(eintrag);
        }
    }

    // Alle Hotels einer Kette gehören ab jetzt einer anderen
    fn umfaerben(&mut self, alt: Kette, neu: Kette) {
        for belegung in self.brett.iter_mut() {
            if *belegung == Belegung::Hotel(alt) {
                *belegung = Belegung::Hotel(neu);
            }
        }
    }

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

        // Wer gründet, bekommt eine Aktie geschenkt
        if self.bank[kette.nr()] > 0 {
            self.bank[kette.nr()] -= 1;
            self.dran().aktien[kette.nr()] += 1;
        }
        self.notiere(format!("gründet {}", kette.name()));
        self.nach_dem_legen();
    }

    // Darf man jetzt eine Aktie dieser Kette kaufen?
    pub fn darf_kaufen(&self, kette: Kette) -> bool {
        let Phase::Kaufen { gekauft } = self.phase else {
            return false;
        };
        let preis = self.preis(kette);
        gekauft < KAEUFE_JE_ZUG
            && preis > 0
            && self.bank[kette.nr()] > 0
            && self.aktueller().geld >= preis
    }

    // Kauft eine Aktie. Nach dem dritten Kauf ist der Zug vorbei.
    pub fn kaufen(&mut self, kette: Kette) {
        if !self.darf_kaufen(kette) {
            return;
        }
        let Phase::Kaufen { gekauft } = self.phase else {
            return;
        };
        let preis = self.preis(kette);
        let spieler = self.dran();
        spieler.geld -= preis;
        spieler.aktien[kette.nr()] += 1;
        self.bank[kette.nr()] -= 1;
        self.notiere(format!("kauft {}", kette.name()));

        if gekauft + 1 == KAEUFE_JE_ZUG {
            self.naechster_zug();
        } else {
            self.phase = Phase::Kaufen {
                gekauft: gekauft + 1,
            };
        }
    }

    // Beendet die Kaufphase und damit den Zug
    pub fn zug_beenden(&mut self) {
        match self.phase {
            Phase::Kaufen { .. } => self.naechster_zug(),
            // Passt kein Plättchen, geht es ohne Legen weiter
            Phase::Legen if !self.kann_legen() => {
                self.notiere(String::from("kann nichts legen"));
                self.nach_dem_legen();
            }
            _ => {}
        }
    }

    // Alle Züge, die der Spieler am Zug gerade machen darf
    pub fn moegliche_zuege(&self) -> Vec<Zug> {
        let mut zuege = Vec::new();
        match self.phase {
            Phase::Legen => {
                for &feld in &self.aktueller().hand {
                    if self.darf_legen(feld) {
                        zuege.push(Zug::Legen(feld));
                    }
                }
                if zuege.is_empty() {
                    zuege.push(Zug::ZugBeenden);
                }
            }
            Phase::Gruenden(_) => {
                for kette in self.freie_ketten() {
                    zuege.push(Zug::Gruenden(kette));
                }
            }
            Phase::Kaufen { .. } => {
                for kette in self.aktive_ketten() {
                    if self.darf_kaufen(kette) {
                        zuege.push(Zug::Kaufen(kette));
                    }
                }
                zuege.push(Zug::ZugBeenden);
            }
            Phase::Fusion { .. } => {
                zuege.push(Zug::Aktien(Entscheidung::Verkaufen));
                zuege.push(Zug::Aktien(Entscheidung::Tauschen));
                zuege.push(Zug::Aktien(Entscheidung::Behalten));
            }
        }
        zuege
    }

    // Führt einen Zug aus, egal ob er vom Menschen oder vom Computer kommt
    pub fn ausfuehren(&mut self, zug: Zug) {
        match zug {
            Zug::Legen(feld) => {
                self.legen(feld);
            }
            Zug::Gruenden(kette) => self.gruenden(kette),
            Zug::Kaufen(kette) => self.kaufen(kette),
            Zug::Aktien(wahl) => self.entscheiden(wahl),
            Zug::ZugBeenden => self.zug_beenden(),
        }
    }

    // Gibt es schon Ketten, darf man Aktien kaufen. Sonst ist der Zug vorbei.
    fn nach_dem_legen(&mut self) {
        if self.aktive_ketten().is_empty() {
            self.naechster_zug();
        } else {
            self.phase = Phase::Kaufen { gekauft: 0 };
        }
    }

    // Nachziehen, und der Nächste ist dran
    fn naechster_zug(&mut self) {
        self.nachziehen();
        self.am_zug = (self.am_zug + 1) % self.spieler.len();
        self.phase = Phase::Legen;
    }

    // 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()
    }

    // Was eine Aktie dieser Kette gerade kostet
    pub fn preis(&self, kette: Kette) -> u32 {
        let grundpreis = match self.groesse(kette) {
            0 => return 0,
            groesse @ 1..=5 => groesse as u32 * 100,
            6..=10 => 600,
            11..=20 => 700,
            21..=30 => 800,
            31..=40 => 900,
            _ => 1000,
        };
        grundpreis + kette.stufe() * 100
    }

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

    // 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()
    }

    // Steht neben diesem Feld ein Rohbau?
    fn rohbau_neben(&self, feld: Feld) -> bool {
        feld.nachbarn()
            .iter()
            .any(|&nachbar| self.belegung(nachbar) == Belegung::Rohbau)
    }

    // 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);
                }
            }
        }
    }
}

src/ki.rs

use rand::seq::IndexedRandom;

use crate::spiel::{Art, Spiel, Zug};

// Welchen Zug macht der Computer, der gerade dran ist?
pub fn waehle_zug(spiel: &Spiel) -> Zug {
    match spiel.aktueller().art {
        Art::Mensch | Art::Tourist => tourist(spiel),
    }
}

// Der Tourist denkt nicht nach: Er nimmt irgendeinen erlaubten Zug
fn tourist(spiel: &Spiel) -> Zug {
    let zuege = spiel.moegliche_zuege();
    zuege
        .choose(&mut rand::rng())
        .copied()
        .unwrap_or(Zug::ZugBeenden)
}
ZusammenfassungBei einer Fusion schluckt die größte Kette die anderen. Sortiert wird mit sort_by_key und Reverse nach einem Paar, das Rust Stelle für Stelle vergleicht. Die größten Aktionäre bekommen Prämien, aufgerundet mit div_ceil. Danach entscheidet reihum jeder Aktionär, und die Frage „wer muss handeln?“ ersetzt „wer ist am Zug?“. Warteschlangen in Vecs und ein wenig Rekursion arbeiten mehrere Fusionen nacheinander ab. Gemeinsame Teile der Oberfläche wandern in eigene Funktionen.
Übungen

Zum Ausprobieren

  1. Früher sicher: Mach Ketten schon ab 8 Feldern sicher. Wie verändert das das Spiel?
  2. Großzügige Bank: Die Prämien sollen doppelt so hoch sein. Welche Zeilen änderst du?
  3. Ein Test für die Prämie: Schreib einen Test, in dem zwei Spieler gleich viele Aktien einer geschluckten Kette haben. Beide müssen die Hälfte beider Prämien bekommen, aufgerundet auf Hundert.
Lösungen anzeigen

1. pub const SICHER: usize = 8;. Ketten werden früher unangreifbar, es gibt weniger Fusionen, und mehr Plättchen werden unspielbar, weil sie zwei sichere Ketten verbinden würden.

2. In praemien_zahlen: let gross = preis * 20; und let klein = preis * 10;.

3. Zum Beispiel so, am Ende von spiel.rs:

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

    #[test]
    fn gleichstand_teilt_beide_praemien() {
        let mut spiel = Spiel::neu(&[("A", Art::Mensch), ("B", Art::Tourist)]);
        let feld = |spalte| Feld { spalte, reihe: 0 };
        spiel.brett[feld(0).index()] = Belegung::Hotel(Kette::Zugvogel);
        spiel.brett[feld(1).index()] = Belegung::Hotel(Kette::Zugvogel);
        spiel.spieler[0].aktien[Kette::Zugvogel.nr()] = 2;
        spiel.spieler[1].aktien[Kette::Zugvogel.nr()] = 2;

        spiel.praemien_zahlen(Kette::Zugvogel);

        // Kurs 200 €: (2.000 € + 1.000 €) / 2 = 1.500 € für jeden
        assert_eq!(spiel.spieler[0].geld, 6000 + 1500);
        assert_eq!(spiel.spieler[1].geld, 6000 + 1500);
    }
}

Der Test darf die private Methode praemien_zahlen aufrufen, weil das Modul tests innerhalb von spiel.rs liegt.

Etappe 6 geschafft 🦈

Das Herz des Spiels schlägt: Ketten wachsen, schlucken sich gegenseitig und machen Aktionäre reich. In der nächsten Etappe bekommt das Spiel ein Ende: tote Plättchen werden ausgetauscht, jemand darf die Partie beenden, und in der Schlussabrechnung zeigt sich, wer der größte Baulöwe ist.

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

☕ Kaffee spendieren