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

Mitspieler und der Tourist

Heute bekommst du Gesellschaft. Erst spielen vier Menschen reihum am selben Rechner, dann übernehmen Computergegner drei der Plätze. Dafür lernt das Spiel, jede Handlung als Wert zu beschreiben, und der erste Gegner zieht im Takt: der Tourist, der sich einfach irgendeinen erlaubten Zug aussucht.

4 Schritte · ca. 75 Minuten · iced 0.14 mit tokio

Das Spielfenster mit drei Computergegnern in der Tafel Am Tisch und einem laufenden Protokoll
So sieht der Baulöwe am Ende dieser Etappe aus.
Schritt 1

Vier an einem Tisch

Ein Wirtschaftsspiel allein ist wie Monopoly ohne Gegner. Bevor die Computer Platz nehmen, bauen wir das Spiel so um, dass mehrere Menschen reihum am selben Rechner spielen können. Dafür wandert die Hand in den Spieler, denn jeder hat seine eigene, und jeder bekommt einen Namen:

src/spiel.rsstruct Spieler und struct Spiel
pub struct Spieler {    pub name: String,    pub geld: u32,
    pub aktien: [u32; 7],    pub hand: Vec<Feld>,}⋮    pub stapel: Vec<Feld>,    pub hand: Vec<Feld>,    pub phase: Phase,    pub spieler: Spieler,    pub spieler: Vec<Spieler>,
    // Wer gerade dran ist: die Nummer in der Liste der Spieler
    pub am_zug: usize,

Spiel::neu bekommt jetzt eine Liste von Namen und macht daraus Spieler. Ersetze die ganze Funktion und füge darunter zwei kleine Helfer ein:

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

        let mut spiel = Spiel {
            brett: vec![Belegung::Frei; SPALTEN * REIHEN],
            stapel,
            hand: Vec::new(),
            phase: Phase::Legen,
            spieler: Spieler {
                geld: STARTGELD,
                aktien: [0; 7],
            },
            bank: [AKTIEN_JE_KETTE; 7],
        };
        spiel.nachziehen();
        spiel
    }    pub fn neu(namen: &[&str]) -> 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 = namen
            .iter()
            .map(|name| Spieler {
                name: name.to_string(),
                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,
            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
    }

    // 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]
    }
    // Was steht auf diesem Feld?
  • namen: &[&str] ist ein Slice: ein Blick auf eine Liste, egal wie lang sie ist. So kann man Spiel::neu mit zwei, vier oder sechs Namen aufrufen.
  • map und collect verwandeln jeden Namen in einen Spieler und sammeln sie in einem Vec. Welchen Typ collect bauen soll, weiß Rust, weil das Ergebnis im Feld spieler landet.
  • Reihum ziehen. nachziehen füllt immer die Hand des Spielers am Zug. Also setzen wir am_zug nacheinander auf jeden Spieler und ziehen.
  • aktueller und dran liefern den Spieler am Zug, einmal zum Lesen (&), einmal zum Verändern (&mut). Die veränderbare Variante bleibt privat, damit nur die Regeln Spieler verändern können.

nachziehen arbeitet ab jetzt mit der Hand des Spielers am Zug und wird privat, denn von außen braucht es niemand mehr:

src/spiel.rsfn nachziehen komplett ersetzen
    // Füllt die Hand wieder auf, solange der Stapel reicht
    pub fn nachziehen(&mut self) {
        while self.hand.len() < HANDGROESSE {
            match self.stapel.pop() {
                Some(feld) => self.hand.push(feld),
                None => break,
            }
        }
        self.hand.sort_by_key(|feld| (feld.spalte, feld.reihe));
    }    // 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));
    }
    // Legt ein Plättchen aus der Hand. Gibt false zurück, wenn das nicht geht.

Warum nicht einfach self.dran().hand.push(feld)? Weil wir in derselben Zeile auch self.stapel verändern. Rust erlaubt nicht, dass zwei veränderbare Zugriffe auf self gleichzeitig laufen, wenn einer davon über eine Methode geht. Über self.spieler[nr] sieht Rust dagegen, dass es zwei verschiedene Felder sind. Überall sonst ersetzen wir die alte Hand und das alte Geld durch den Spieler am Zug:

src/spiel.rsin fn legen, fn gruenden, fn darf_kaufen, fn kaufen
        let Some(stelle) = self.hand.iter().position(|&h| h == feld) else {        let Some(stelle) = self.aktueller().hand.iter().position(|&h| h == feld) else {        ⋮        self.hand.remove(stelle);        self.dran().hand.remove(stelle);        ⋮            self.spieler.aktien[kette.nr()] += 1;            self.dran().aktien[kette.nr()] += 1;        ⋮            && self.spieler.geld >= preis            && self.aktueller().geld >= preis        ⋮        self.spieler.geld -= self.preis(kette);
        self.spieler.aktien[kette.nr()] += 1;        let preis = self.preis(kette);
        let spieler = self.dran();
        spieler.geld -= preis;
        spieler.aktien[kette.nr()] += 1;

Beim Kaufen rechnen wir den Preis zuerst aus und holen uns erst danach den Spieler zum Verändern. Andersherum würde Rust sich beschweren, denn während wir den Spieler verändern, dürfen wir nicht gleichzeitig self.preis fragen. Und dann das Wichtigste: Nach jedem Zug ist der Nächste dran. Der Rest beim Teilen (%) sorgt dafür, dass nach dem Letzten wieder der Erste kommt:

src/spiel.rsfn naechster_zug
    // Nachziehen, und der nächste Zug beginnt    // 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;
    }

In main.rs legen wir vier Spieler an und zeigen immer die Hand und die Kasse dessen, der gerade dran ist:

src/main.rsin fn neu
            spiel: Spiel::neu(),            spiel: Spiel::neu(&["Anna", "Ben", "Carla", "Dario"]),
src/main.rsin fn view
        let kasse = text(format!("Kasse: {}", geld(self.spiel.spieler.geld)))        let spieler = self.spiel.aktueller();
        let kasse = text(format!("{} · Kasse: {}", spieler.name, geld(spieler.geld)))        ⋮        for &feld in &self.spiel.hand {        for &feld in &self.spiel.aktueller().hand {        ⋮                text(self.spiel.spieler.aktien[kette.nr()].to_string())                text(self.spiel.aktueller().aktien[kette.nr()].to_string())
src/main.rsin fn mouse_interaction und fn zeichne_parzelle
            Some(feld) if self.spiel.hand.contains(&feld) => mouse::Interaction::Pointer,            Some(feld) if self.spiel.aktueller().hand.contains(&feld) => {
                mouse::Interaction::Pointer
            }        ⋮        let im_plan = self.spiel.hand.contains(&feld);        let im_plan = self.spiel.aktueller().hand.contains(&feld);
Probelauf Speichern, dann im Terminal:
cargo run
Oben rechts steht jetzt, wer dran ist, zum Beispiel „Anna · Kasse: 6.000 €“. Nach jedem Zug wechseln Name, Kasse, Hand und Fähnchen. Hol drei Leute an den Rechner und spielt eine Runde, oder spiel alle vier selbst.
Schritt 2

Wer sitzt am Tisch?

Mit vier Spielern verliert man schnell den Überblick. Links oben kommt deshalb eine Tafel hin: alle Spieler mit Kasse und Aktien, der aktuelle farbig hinterlegt, und darunter ein kleines Protokoll der letzten Handlungen. Das Protokoll ist eine Liste von Texten im Spiel:

src/spiel.rsin struct Spiel und in fn neu
    pub am_zug: usize,    // Was bisher passiert ist, ein Eintrag pro Handlung
    pub protokoll: Vec<String>,⋮            am_zug: 0,            protokoll: Vec::new(),
src/spiel.rsunter fn dran
        &mut self.spieler[self.am_zug]
    }
    // 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?

Dreimal wird notiert: beim Legen, beim Gründen und beim Kaufen.

src/spiel.rsin fn legen, fn gruenden und fn kaufen
        self.dran().hand.remove(stelle);        self.notiere(format!("legt {}", feld.name()));        ⋮            self.dran().aktien[kette.nr()] += 1;
        }        self.notiere(format!("gründet {}", kette.name()));        ⋮        self.bank[kette.nr()] -= 1;        self.notiere(format!("kauft {}", kette.name()));

In view bekommt die Tafel rechts einen Nachbarn links. Weil beide Tafeln gleich aussehen sollen, ziehen wir den Stil in eine Variable tafel_stil. Das geht, weil die Closure nur farben einfängt, und das ist Copy. Also ist auch die Closure selbst kopierbar und lässt sich zweimal verwenden:

src/main.rsin fn view, Ende der Kettentafel
        let tafel = container(ketten)
            .padding(14)
            .style(move |_theme: &Theme| container::Style {
                background: Some(Background::Color(Color {
                    a: 0.85,
                    ..farben.tafel
                })),
                border: Border {
                    radius: 10.0.into(),
                    ..Border::default()
                },
                ..container::Style::default()
            });
        let tafelzeile = row![space::horizontal(), tafel].padding([0, 24]);        let 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]);
  • iter().enumerate() liefert zu jedem Spieler seine Nummer. So erkennen wir, wer am Zug ist.
  • spieler.aktien.iter().sum() zählt alle Aktien zusammen. Rust muss dabei wissen, welcher Zahlentyp herauskommt, deshalb steht : u32 an der Variable.
  • am_zug.then_some(…) gibt dem Hintergrund nur dann eine Farbe, wenn der Spieler dran ist. Sonst ist er None, also durchsichtig.
  • iter().rev().take(5) geht das Protokoll rückwärts durch und nimmt nur die fünf neuesten Einträge. Der neueste steht damit oben.
Probelauf Speichern, dann im Terminal:
cargo run
Links oben sitzen jetzt Anna, Ben, Carla und Dario am Tisch. Wer dran ist, ist orange hinterlegt. Darunter läuft das Protokoll mit: „Ben legt 4C“, „Ben gründet Aurora“, „Ben kauft Aurora“.
Schritt 3

Züge als Daten

Bald sollen Computer mitspielen. Ein Computergegner muss aber wissen, was er überhaupt tun darf, und er muss es tun können, ohne auf Knöpfe zu klicken. Dafür machen wir aus jeder Handlung einen Wert: ein enum Zug. Es kommt vor Spieler:

src/spiel.rsvor // Was einem Spieler gehört
// Alles, was ein Spieler tun kann
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Zug {
    Legen(Feld),
    Gruenden(Kette),
    Kaufen(Kette),
    ZugBeenden,
}
// Was einem Spieler gehört

Die Prüfungen aus legen ziehen in eine eigene Methode darf_legen um, genau wie beim Kaufen. So können Oberfläche und Computer fragen, bevor sie handeln. Ersetze legen komplett durch diese drei Methoden:

src/spiel.rsfn legen komplett ersetzen
    // 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.phase != Phase::Legen {
            return false;
        }
        let Some(stelle) = self.aktueller().hand.iter().position(|&h| h == feld) else {
            return false;
        };

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

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

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

        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
    }    // 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 Ketten auf einmal berühren: Das ist eine Fusion, die kommt später
        if ketten.len() > 1 {
            return false;
        }
        // Eine neue Kette gründen geht nur, wenn noch eine frei ist
        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 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
    }
    // Gründet an der wartenden Stelle eine neue Kette

legen ist dadurch kürzer geworden: Erst fragt es darf_legen, dann handelt es. Statt die Position in der Hand zu suchen, entfernen wir das Plättchen mit retain, das alle anderen behält. Der Nachbar-Rohbau wird jetzt an zwei Stellen gebraucht und bekommt deshalb eine eigene Methode:

src/spiel.rsvor // Welche Ketten grenzen an dieses Feld?
    // 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.

Neu ist auch ein Sonderfall: Was, wenn kein einziges Plättchen passt? Dann darfst du den Zug beenden, ohne zu legen. Und damit Oberfläche und Computer denselben Eingang benutzen, gibt es ausfuehren, das jeden Zug an die passende Methode weiterreicht:

src/spiel.rsfn zug_beenden ersetzen, darunter fn ausfuehren
    pub fn zug_beenden(&mut self) {
        if let Phase::Kaufen { .. } = self.phase {
            self.naechster_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();
            }
            _ => {}
        }
    }

    // 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::ZugBeenden => self.zug_beenden(),
        }
    }
    // Gibt es schon Ketten, darf man Aktien kaufen. Sonst ist der Zug vorbei.

Das match in zug_beenden hat einen Zweig mit Bedingung: Phase::Legen if !self.kann_legen() greift nur beim Legen, und nur wenn nichts passt. Der Zweig _ => {} tut in allen anderen Fällen nichts.

In main.rs ersetzen drei Briefe durch einen einzigen: Message::Spielzug transportiert einen ganzen Zug.

src/main.rsganz oben, enum Message, fn update
use spiel::{Belegung, Feld, KAEUFE_JE_ZUG, Kette, Phase, REIHEN, SPALTEN, Spiel};use spiel::{Belegung, Feld, KAEUFE_JE_ZUG, Kette, Phase, REIHEN, SPALTEN, Spiel, Zug};⋮    Gruenden(Kette),
    ZugBeenden,
    Kaufen(Kette),    Spielzug(Zug),}⋮            Message::Geklickt(feld) => {
                self.spiel.legen(feld);
            }            Message::Geklickt(feld) => self.spiel.ausfuehren(Zug::Legen(feld)),            Message::TagNacht => self.nacht = !self.nacht,            Message::Gruenden(kette) => self.spiel.gruenden(kette),
            Message::ZugBeenden => self.spiel.zug_beenden(),
            Message::Kaufen(kette) => self.spiel.kaufen(kette),            Message::Spielzug(zug) => self.spiel.ausfuehren(zug),

Die drei Knöpfe schicken jetzt ihren Zug verpackt in Message::Spielzug:

src/main.rsin fn view und fn gruendungsdialog: die on_press-Zeilen
                .on_press(Message::ZugBeenden)                .on_press(Message::Spielzug(Zug::ZugBeenden))        ⋮                            .then_some(Message::Kaufen(kette))                            .then_some(Message::Spielzug(Zug::Kaufen(kette)))        ⋮                .on_press(Message::Gruenden(kette))                .on_press(Message::Spielzug(Zug::Gruenden(kette)))

Und jetzt zahlt sich darf_legen sofort aus: Plättchen-Knöpfe, Fähnchen und Mauszeiger fragen nicht mehr nur „liegt es auf der Hand?“, sondern „darf es jetzt gelegt werden?“. Ein Plättchen, das zwei Ketten verbinden würde, bekommt so gar kein Fähnchen mehr:

src/main.rsin fn view, Fußleiste
        let darf_legen = self.spiel.phase == Phase::Legen;        ⋮                .on_press_maybe(darf_legen.then_some(Message::Geklickt(feld)))                .on_press_maybe(
                    self.spiel
                        .darf_legen(feld)
                        .then_some(Message::Geklickt(feld)),
                )
src/main.rsin fn view, die Ansage vor dem Knopf „Zug beenden“
        if let Phase::Kaufen { gekauft } = self.spiel.phase {
            let rest = text(format!(
                "Du darfst noch {} Aktien kaufen",
                KAEUFE_JE_ZUG - gekauft
            ))
            .size(14)
            .color(farben.schrift);        let ansage = match self.spiel.phase {
            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))
src/main.rsin fn mouse_interaction und fn zeichne_parzelle
            Some(feld) if self.spiel.aktueller().hand.contains(&feld) => {
                mouse::Interaction::Pointer
            }            Some(feld) if self.spiel.darf_legen(feld) => mouse::Interaction::Pointer,        ⋮        let im_plan = self.spiel.aktueller().hand.contains(&feld);        let im_plan = self.spiel.darf_legen(feld);
Probelauf Speichern, dann im Terminal:
cargo run
Es spielt sich wie vorher. Achte auf die Fähnchen: In der Kaufphase verschwinden sie, weil dann gerade nicht gelegt werden darf. Und würde ein Plättchen zwei Ketten berühren, bleibt es grau. Passt gar keins, steht unten „Keins deiner Plättchen passt gerade“ mit dem Knopf „Zug beenden“.
Schritt 4

Der Tourist

Zeit für den ersten Computergegner. Er heißt Tourist, weil er nicht viel nachdenkt: Er schaut sich an, welche Züge erlaubt sind, und nimmt irgendeinen. Das klingt dumm, ist aber ein wichtiger erster Schritt, denn er prüft die ganze Maschinerie, und später messen sich die klügeren Gegner an ihm. Zuerst muss das Spiel wissen, wer Mensch ist und wer nicht:

src/spiel.rsvor // Was einem Spieler gehört, und in struct Spieler
// 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,
src/spiel.rsin fn neu
    pub fn neu(namen: &[&str]) -> Spiel {    pub fn neu(teilnehmer: &[(&str, Art)]) -> Spiel {        ⋮        let spieler = namen
            .iter()
            .map(|name| Spieler {        let spieler = teilnehmer
            .iter()
            .map(|&(name, art)| Spieler {                name: name.to_string(),                art,

|&(name, art)| packt jedes Paar der Liste gleich in zwei Namen aus. Jetzt die wichtigste Frage für jeden Computergegner: Was darf ich gerade tun? Die Antwort liefert eine Methode, die alle erlaubten Züge sammelt. Sie kommt direkt vor ausfuehren:

src/spiel.rsvor // Führt einen Zug aus …
    // 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);
            }
        }
        zuege
    }
    // Führt einen Zug aus, egal ob er vom Menschen oder vom Computer kommt

Je nach Phase sammelt sie Plättchen, die passen, freie Ketten zum Gründen oder Aktien, die man kaufen kann. Beim Kaufen ist „aufhören“ immer auch eine Möglichkeit.

Die Computergegner bekommen ein eigenes Modul src/ki.rs. Das Kürzel steht für künstliche Intelligenz, auch wenn der Tourist davon nicht viel hat:

src/ki.rsneue Datei neben main.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)
}
  • use crate::spiel::…: crate ist die Wurzel unseres Programms. Von ki.rs aus erreichen wir so das Modul spiel.
  • waehle_zug ist der einzige Eingang. Es schaut, welche Art von Spieler dran ist, und fragt den passenden Denker. Noch gibt es nur den Touristen. In Etappe 8 und 9 kommen hier zwei weitere Zweige dazu.
  • choose kommt von IndexedRandom und zieht ein zufälliges Element aus einer Liste. Weil die Liste leer sein könnte, liefert es ein Option. copied macht aus dem Verweis eine Kopie, und unwrap_or nimmt zur Not „Zug beenden“.

Damit der Computer nicht in einem Wimpernschlag durchspielt, soll er im Takt ziehen: alle 0,6 Sekunden eine Handlung. Einen Takt liefert iced über eine Subscription, ein Abonnement auf Ereignisse von außen. Die Uhr dafür braucht einen Unterbau für nebenläufige Aufgaben, und iced kann dafür tokio nutzen. Dieses Feature schalten wir zu:

cargo add iced@0.14 --features canvas,tokio

In main.rs melden wir das neue Modul an und holen uns Uhr und Abo:

src/main.rsganz oben
mod ki;mod spiel;
use std::time::Duration;
use iced::mouse;⋮    Theme, Vector, alignment, font,    Subscription, Theme, Vector, alignment, font, time,};use spiel::{Belegung, Feld, KAEUFE_JE_ZUG, Kette, Phase, REIHEN, SPALTEN, Spiel, Zug};use spiel::{
    Art, Belegung, Feld, KAEUFE_JE_ZUG, Kette, Phase, REIHEN, SPALTEN, Spiel, Spieler, Zug,
};
src/main.rsin fn main und fn neu
        .antialiasing(true)        .subscription(Bauloewe::abo)        .run()⋮            spiel: Spiel::neu(&["Anna", "Ben", "Carla", "Dario"]),            spiel: Spiel::neu(&[
                ("Leo", Art::Mensch),
                ("Toni", Art::Tourist),
                ("Tine", Art::Tourist),
                ("Theo", Art::Tourist),
            ]),

Du spielst als Leo, der Baulöwe, gegen Toni, Tine und Theo. Jetzt die drei Helfer, die main.rs ab sofort ständig braucht. Sie kommen in impl Bauloewe vor update:

src/main.rsin impl Bauloewe, vor fn update
    // 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) {

abo gibt iced bei jeder Änderung Bescheid, welche Abos gerade gelten sollen. Ist der Mensch dran, gibt es keins. Ist der Computer dran, schickt time::every alle 600 Millisekunden ein Signal, das map in einen Brief KiTakt verwandelt. Der Brief kommt ins enum, und update bekommt zwei Wächter:

src/main.rsenum Message und fn update
    Spielzug(Zug),    KiTakt,}⋮            Message::MausUeber(feld) => self.maus_feld = feld,            Message::Geklickt(feld) => self.spiel.ausfuehren(Zug::Legen(feld)),            Message::TagNacht => self.nacht = !self.nacht,            Message::Spielzug(zug) => self.spiel.ausfuehren(zug),            // 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);
                }
            }        }

Die Zweige mit if self.mensch_am_zug() lassen Klicks nur durch, wenn du dran bist. Die Zeile darunter fängt dieselben Briefe in allen anderen Fällen ab und tut nichts, denn ein match muss jeden Fall behandeln. Bei KiTakt fragt das Programm den Computer nach seinem Zug und führt ihn aus, über genau denselben Eingang wie deine Klicks.

Bleibt die Oberfläche. Sie zeigt ab jetzt immer deine Sicht: deine Hand, deine Kasse, deine Aktien. Und alles Klickbare ist nur aktiv, wenn du dran bist:

src/main.rsin fn view, Kopfzeile
        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()),        ⋮        let spieler = self.spiel.aktueller();
        let kasse = text(format!("{} · Kasse: {}", spieler.name, geld(spieler.geld)))        let kasse = text(format!("Kasse: {}", geld(self.ich().geld)))

Der erste Zweig im match ist ein Trick: _ passt auf alles, und die Bedingung dahinter entscheidet. Ist der Computer dran, gewinnt dieser Zweig, egal wo die Maus ist.

src/main.rsin fn view, Fußleiste und Kettentafel
        for &feld in &self.spiel.aktueller().hand {        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(
                    self.spiel
                        .darf_legen(feld)
                        .then_some(Message::Geklickt(feld)),
                )                .on_press_maybe(darf.then_some(Message::Geklickt(feld)))        ⋮        let ansage = match self.spiel.phase {            _ if !self.mensch_am_zug() => None,            Phase::Kaufen { gekauft } => Some(format!(        ⋮                text(self.spiel.aktueller().aktien[kette.nr()].to_string())                text(self.ich().aktien[kette.nr()].to_string())        ⋮                        self.spiel
                            .darf_kaufen(kette)                        (self.mensch_am_zug() && self.spiel.darf_kaufen(kette))                            .then_some(Message::Spielzug(Zug::Kaufen(kette)))
src/main.rsin fn view, Gründungsdialog, und im Canvas
        if let Phase::Gruenden(_) = self.spiel.phase {        if self.mensch_am_zug() && matches!(self.spiel.phase, Phase::Gruenden(_)) {        ⋮            Some(feld) if self.spiel.darf_legen(feld) => mouse::Interaction::Pointer,            Some(feld) if self.mensch_am_zug() && self.spiel.darf_legen(feld) => {
                mouse::Interaction::Pointer
            }        ⋮        let im_plan = self.spiel.darf_legen(feld);        let im_plan = self.mensch_am_zug() && self.spiel.darf_legen(feld);

matches! prüft, ob ein Wert auf ein Muster passt, und liefert einfach wahr oder falsch. Damit lässt sich die Musterprüfung mit && verknüpfen.

Probelauf Speichern, dann im Terminal:
cargo run
Du bist Leo und fängst an. Leg dein Plättchen, kauf vielleicht etwas, beende den Zug, und dann lehn dich zurück: Toni, Tine und Theo legen, gründen und kaufen im Takt, und das Protokoll links schreibt mit. Danach bist du wieder dran. Die Touristen kaufen wild durcheinander. Mal sehen, wie lange sie mit ihrem Geld auskommen.
Das Spielfenster mit der Tafel Am Tisch links oben, in der Leo, Toni, Tine und Theo mit Kasse und Aktien stehen, darunter das Protokoll. Rechts die Kettentafel.
Leo gegen drei Touristen.
Zwischenstand

Der komplette Code nach Etappe 5

Hier ist der vollständige Stand nach Etappe 5. 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::{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, Feld, KAEUFE_JE_ZUG, Kette, Phase, REIHEN, 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 {
                (format!("{groesse} Felder"), 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() && matches!(self.spiel.phase, Phase::Gruenden(_)) {
            ebenen = ebenen.push(self.gruendungsdialog(farben));
        }
        ebenen.into()
    }

    // Das Fenster zum Gründen einer neuen Kette
    fn gruendungsdialog(&self, farben: Farben) -> Element<'_, Message> {
        let mut knoepfe = row![].spacing(8);
        for kette in self.spiel.freie_ketten() {
            let farbe = kettenfarbe(kette);
            let knopf = button(text(kette.name()).size(14))
                .padding([8, 12])
                .on_press(Message::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 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 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 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;

#[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 },
}

// Alles, was ein Spieler tun kann
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Zug {
    Legen(Feld),
    Gruenden(Kette),
    Kaufen(Kette),
    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>,
    // 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(),
            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
    }

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

    // 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 Ketten auf einmal berühren: Das ist eine Fusion, die kommt später
        if ketten.len() > 1 {
            return false;
        }
        // Eine neue Kette gründen geht nur, wenn noch eine frei ist
        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 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
    }

    // 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);
            }
        }
        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::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)
}
ZusammenfassungDie Hand gehört jetzt zum Spieler, und am_zug geht reihum, mit % wieder von vorn. Jede Handlung ist ein Wert vom Typ Zug, moegliche_zuege sammelt alle erlaubten, und ausfuehren ist der gemeinsame Eingang für Mensch und Computer. Prüfen (darf_legen) und Handeln (legen) sind getrennt. Eine Subscription liefert den Takt für den Computer, und match-Zweige mit Bedingung sorgen dafür, dass du nur klicken kannst, wenn du dran bist.
Übungen

Zum Ausprobieren

  1. Dein Name: Spiel unter deinem eigenen Namen statt als Leo.
  2. Turbo: Die Touristen sollen fünfmal so schnell ziehen. Und wie viele Gegner verträgt der Tisch? Probier es mit fünf.
  3. Computer gegen Computer: Mach aus Leo einen Touristen und schau einfach zu. Was passiert, wenn der Stapel leer ist? (Keine Sorge, das reparieren wir in Etappe 7.)
Lösungen anzeigen

1. In Bauloewe::neu den Namen im ersten Paar ändern. Die Tafel zeigt ihn sofort an, das Protokoll auch.

2. In abo aus 600 120 machen. Einen fünften Gegner bekommst du mit einer weiteren Zeile wie ("Tessa", Art::Tourist),. Weil alles mit self.spieler.len() rechnet, funktioniert der Rest von selbst. Nur die Tafel wird etwas länger.

3. Mit ("Leo", Art::Tourist) spielt der Computer gegen sich selbst, denn mensch_am_zug ist dann nie wahr und das Abo tickt immer. Ist der Stapel leer und niemand kann mehr legen, sagt jeder nur noch „kann nichts legen“, und das Spiel endet nie. Genau das werden wir mit einer richtigen Schlussabrechnung lösen.

Etappe 5 geschafft 🎲

Du spielst gegen den Computer! Noch denkt er nicht nach, aber die Maschinerie steht: erlaubte Züge sammeln, einen auswählen, ausführen. In der nächsten Etappe kommt die Regel, die das Spiel spannend macht: Stoßen zwei Ketten zusammen, schluckt die große die kleine, und die Aktionäre der kleinen bekommen eine Prämie.

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

☕ Kaffee spendieren