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

Aktien und Geld

Ein Hotel ist schön, eine Beteiligung daran ist besser. Ab heute hat jede Kette einen Aktienkurs, der mit ihrer Größe steigt, und du startest mit 6.000 € in der Kasse. Nach jedem Plättchen darfst du bis zu drei Aktien kaufen, und wer eine Kette gründet, bekommt eine geschenkt. Dafür lernt das Spiel, einen Zug in Phasen zu denken.

3 Schritte · ca. 60 Minuten · iced 0.14

Das größere Spielfenster mit der Kasse in der Kopfzeile, rechts oben eine Tabelle mit Größe, Kurs und Aktienbestand jeder Kette samt Kaufknöpfen, unten der Knopf Zug beenden
So sieht der Baulöwe am Ende dieser Etappe aus.
Schritt 1

Ein Zug in Phasen

Bisher ist ein Zug ein einziger Klick. Im echten Spiel hat ein Zug aber mehrere Abschnitte: erst ein Plättchen legen, dann vielleicht eine Kette gründen, dann Aktien kaufen und zum Schluss nachziehen. In Etappe 3 haben wir die Gründung mit einem Option gelöst. Jetzt kommen weitere Abschnitte dazu, und damit wird es Zeit für ein richtiges Phasen-Modell: ein enum, das sagt, woran das Spiel gerade ist. Es kommt in spiel.rs vor die Struktur Spiel:

src/spiel.rsvor // Alles, was die Spielregeln wissen müssen
// 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 die Spielregeln wissen müssen

Kaufen { gekauft: u32 } ist ein Fall mit einem benannten Feld: Er merkt sich, wie viele Aktien in diesem Zug schon gekauft wurden. Das alte gruendung wird durch die Phase ersetzt:

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

Ein Programm, das so von Phase zu Phase springt, nennt man einen Zustandsautomaten. Die Regel für jede Methode lautet: Erst nachsehen, ob sie in der aktuellen Phase überhaupt erlaubt ist. legen geht nur beim Legen:

src/spiel.rsin fn legen, ganz oben
    pub fn legen(&mut self, feld: Feld) -> bool {        if self.gruendung.is_some() {        if self.phase != Phase::Legen {            return false;
        }

Am Ende von legen ziehen wir nicht mehr sofort nach. Stattdessen geht es in die nächste Phase:

src/spiel.rsin fn legen, ganz unten
        if let Some(&kette) = ketten.first() {
            self.ausbreiten(feld, kette);            self.nach_dem_legen();        } else if rohbau_daneben {            self.gruendung = Some(feld);
        }

        self.nachziehen();            self.phase = Phase::Gruenden(feld);
        } else {
            self.nach_dem_legen();
        }        true
    }

gruenden holt sich das wartende Feld jetzt aus der Phase. Die Zeile let Phase::Gruenden(feld) = self.phase else { return; }; ist wieder ein Türsteher: Nur wenn die Phase wirklich eine Gründung ist, geht es weiter, und dann steht das Feld gleich in feld. Darunter kommen drei neue Methoden:

src/spiel.rsfn gruenden ersetzen, darunter drei neue Methoden
    pub fn gruenden(&mut self, kette: Kette) {
        if !self.freie_ketten().contains(&kette) {
            return;
        }
        if let Some(feld) = self.gruendung.take() {
            self.ausbreiten(feld, kette);
        }
    }    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);
        self.nach_dem_legen();
    }

    // Beendet die Kaufphase und damit den Zug
    pub fn zug_beenden(&mut self) {
        if let Phase::Kaufen { .. } = self.phase {
            self.naechster_zug();
        }
    }

    // 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 Zug beginnt
    fn naechster_zug(&mut self) {
        self.nachziehen();
        self.phase = Phase::Legen;
    }
    // Wie viele Felder gehören zu dieser Kette?
  • zug_beenden ist öffentlich, denn den Knopf dafür drückst du. Es wirkt aber nur in der Kaufphase. Phase::Kaufen { .. } heißt: Kaufphase, egal mit welcher Zahl darin.
  • nach_dem_legen entscheidet, wie es weitergeht: Gibt es noch gar keine Kette, gibt es auch nichts zu kaufen, und der Zug endet sofort.
  • naechster_zug zieht nach und stellt die Phase zurück auf Legen. Ab Etappe 5 ist hier auch der nächste Spieler dran.

Fehlt noch aktive_ketten, das Gegenstück zu freie_ketten:

src/spiel.rsvor // Alle Ketten, die noch nicht auf dem Brett stehen
    // 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

In main.rs gibt es einen neuen Brief, und der Gründungsdialog fragt jetzt die Phase:

src/main.rsganz oben, in enum Message, in fn update
use spiel::{Belegung, Feld, Kette, REIHEN, SPALTEN, Spiel};use spiel::{Belegung, Feld, Kette, Phase, REIHEN, SPALTEN, Spiel};⋮    Gruenden(Kette),    ZugBeenden,}⋮            Message::Gruenden(kette) => self.spiel.gruenden(kette),            Message::ZugBeenden => self.spiel.zug_beenden(),        }
src/main.rsin fn view, weiter unten
        let mut ebenen = stack![brett, folie];        if self.spiel.gruendung.is_some() {        if let Phase::Gruenden(_) = self.spiel.phase {

Die Plättchen in der Fußleiste sollen nur klickbar sein, solange gelegt werden darf. on_press_maybe nimmt ein Option: Bei Some(brief) ist der Knopf aktiv, bei None ausgegraut. darf_legen.then_some(…) macht aus einem Wahrheitswert genau so ein Option. Für den ausgegrauten Zustand bekommt der Stil einen eigenen Fall:

src/main.rsin fn view, Fußleiste
        // Unten: die Plättchen auf deiner Hand        let darf_legen = self.spiel.phase == Phase::Legen;        ⋮                .padding([8, 14])                .on_press(Message::Geklickt(feld))                .on_press_maybe(darf_legen.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,

Und rechts unten erscheint in der Kaufphase der Knopf „Zug beenden“:

src/main.rsin fn view, vor let fussleiste
            .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);
        if let Phase::Kaufen { .. } = self.spiel.phase {
            let fertig = button(text("Zug beenden").size(14))
                .padding([8, 16])
                .on_press(Message::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(fertig);
        }
        let fussleiste = row![hand, space::horizontal(), stapel]        let fussleiste = row![hand, space::horizontal(), rechts]
Probelauf Speichern, dann im Terminal:
cargo run
Solange noch keine Kette steht, spielt sich alles wie bisher. Sobald die erste Kette gegründet ist, werden die Plättchen nach jedem Zug blass, und rechts unten erscheint „Zug beenden“. Erst nach dem Klick ziehst du nach und darfst wieder legen.
Schritt 2

Aktienkurse

Jede Kette hat einen Aktienkurs, und der hängt von zwei Dingen ab. Erstens von der Größe: Je mehr Felder eine Kette hat, desto wertvoller ist sie. Zweitens von der Preisstufe: Zugvogel und Ankerplatz sind günstige Häuser, Aurora und Königshof die Luxusklasse, der Rest liegt dazwischen. Die Stufe kommt in impl Kette:

src/spiel.rsin impl Kette, vor fn name
    // 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 {

Der senkrechte Strich | in einem match-Zweig heißt „oder“. Und nun die Kurstabelle, als Methode von Spiel, weil sie die Größe auf dem Brett braucht:

src/spiel.rsin impl Spiel, vor // Alle Ketten, die schon auf dem Brett stehen
    // 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

Hier stecken gleich drei match-Kniffe drin:

  • 1..=5 ist ein Bereich: alle Zahlen von 1 bis einschließlich 5.
  • groesse @ 1..=5 prüft den Bereich und gibt dem Wert gleichzeitig einen Namen, damit wir damit rechnen können: 2 Felder kosten 200 €, 3 Felder 300 €, und so weiter.
  • 0 => return 0 verlässt die Funktion mitten im match: Eine Kette, die es nicht gibt, hat keinen Kurs. Der Unterstrich _ ganz unten fängt alles ab, was vorher nicht gepasst hat, also 41 Felder und mehr.

Zum Anzeigen brauchen wir Geldbeträge in deutscher Schreibweise mit Tausenderpunkt. Rust kann das nicht von Haus aus, also schreiben wir es selbst, vor mischen:

src/main.rsvor // Mischt zwei Farben …
// 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

enumerate liefert zu jeder Ziffer ihre Position. Vor einer Ziffer kommt ein Punkt, wenn danach noch eine durch drei teilbare Anzahl Ziffern folgt. Bei „6000“ ist das vor der zweiten Ziffer der Fall: Danach kommen noch genau drei.

In der Kettentafel zeigen wir den Kurs als neue Spalte. Weil jetzt zwei Angaben von der Frage abhängen, ob es die Kette gibt, liefert das if gleich ein Paar zurück:

src/main.rsin fn view, in der Schleife der Kettentafel
            let angabe = if groesse == 0 {
                String::from("frei")
            } else {
                format!("{groesse} Felder")
            };
            let zeile = row![
                punkt,
                text(kette.name()).size(13).color(farben.schrift).width(90),
                text(angabe).size(13).color(farben.schrift),            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),            ]
Probelauf Speichern, dann im Terminal:
cargo run
In der Kettentafel steht jetzt neben jeder Kette ihr Kurs. Lass eine Kette wachsen und schau zu, wie der Kurs mit ihr steigt. Eine Königshof-Aktie ist immer 200 € teurer als eine Zugvogel-Aktie gleicher Größe.
Schritt 3

Die Kasse

Jetzt darfst du Aktien kaufen. Jeder Spieler startet mit 6.000 € in der Kasse, und von jeder Kette gibt es 25 Aktien. Pro Zug darfst du höchstens drei kaufen. Diese Zahlen kommen als Konstanten nach oben in spiel.rs:

src/spiel.rsunter const HANDGROESSE
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;

Was einem Spieler gehört, fassen wir in einer Struktur Spieler zusammen. Die Aktien sind ein Array mit sieben Plätzen, einem pro Kette. Damit wir den richtigen Platz finden, bekommt Kette eine Nummer. self as usize verrät die Position eines Falls im enum: Zugvogel ist 0, Königshof ist 6.

src/spiel.rsin impl Kette, ganz oben
    pub const ALLE: [Kette; 7] = [        ⋮    ];
    // Die Nummer der Kette: 0 bis 6
    pub fn nr(self) -> usize {
        self as usize
    }
    // Preisstufe: 0 günstig, 1 mittel, 2 teuer
src/spiel.rsvor // Alles, was die Spielregeln …, und in struct Spiel
// Was einem Spieler gehört
pub struct Spieler {
    pub geld: u32,
    pub aktien: [u32; 7],
}
// Alles, was die Spielregeln wissen müssen⋮    pub phase: Phase,    pub spieler: Spieler,
    // Die Aktien, die noch niemandem gehören
    pub bank: [u32; 7],}
src/spiel.rsin fn neu
            phase: Phase::Legen,            spieler: Spieler {
                geld: STARTGELD,
                aktien: [0; 7],
            },
            bank: [AKTIEN_JE_KETTE; 7],        };

[0; 7] ist ein Array aus sieben Nullen, [AKTIEN_JE_KETTE; 7] eins aus siebenmal 25. Wer eine Kette gründet, bekommt eine Aktie geschenkt, sofern die Bank noch eine hat. Das kommt in gruenden:

src/spiel.rsin fn gruenden
        self.ausbreiten(feld, kette);
        // Wer gründet, bekommt eine Aktie geschenkt
        if self.bank[kette.nr()] > 0 {
            self.bank[kette.nr()] -= 1;
            self.spieler.aktien[kette.nr()] += 1;
        }        self.nach_dem_legen();
    }

Und nun das Kaufen, in zwei Methoden direkt unter gruenden. Die erste prüft nur, die zweite handelt. Diese Trennung hat einen Vorteil: Die Oberfläche kann mit darf_kaufen vorab fragen, ob ein Knopf aktiv sein soll, und benutzt dafür genau dieselbe Regel wie der Kauf selbst.

src/spiel.rsunter fn gruenden
        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.spieler.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;
        };
        self.spieler.geld -= self.preis(kette);
        self.spieler.aktien[kette.nr()] += 1;
        self.bank[kette.nr()] -= 1;

        if gekauft + 1 == KAEUFE_JE_ZUG {
            self.naechster_zug();
        } else {
            self.phase = Phase::Kaufen {
                gekauft: gekauft + 1,
            };
        }
    }
    // Beendet die Kaufphase und damit den Zug

In darf_kaufen werden vier Bedingungen mit && verknüpft, alle müssen stimmen: noch Käufe frei, die Kette existiert, die Bank hat noch Aktien, und das Geld reicht. Nach dem dritten Kauf endet der Zug von selbst.

In main.rs machen wir zuerst das Fenster größer, denn die Tafel bekommt drei neue Spalten und braucht Platz. Dann kommt der Brief fürs Kaufen:

src/main.rsganz oben, in fn main, enum Message, fn update
use spiel::{Belegung, Feld, Kette, Phase, REIHEN, SPALTEN, Spiel};use spiel::{Belegung, Feld, KAEUFE_JE_ZUG, Kette, Phase, REIHEN, SPALTEN, Spiel};⋮        .window_size((980.0, 680.0))        .window_size((1280.0, 760.0))⋮    ZugBeenden,    Kaufen(Kette),}⋮            Message::ZugBeenden => self.spiel.zug_beenden(),            Message::Kaufen(kette) => self.spiel.kaufen(kette),        }

Die Kasse zeigen wir groß in der Kopfzeile:

src/main.rsin fn view, Kopfzeile
        let status = text(hinweis).size(15).color(farben.schrift);        let kasse = text(format!("Kasse: {}", geld(self.spiel.spieler.geld)))
            .size(17)
            .font(Font {
                weight: font::Weight::Bold,
                ..Font::DEFAULT
            })
            .color(farben.schrift);        ⋮        let kopfzeile = row![titel, space::horizontal(), status, schalter]        let kopfzeile = row![titel, space::horizontal(), status, kasse, schalter]

Die Kettentafel bekommt eine Überschriftenzeile und drei neue Spalten: wie viele Aktien du hast, wie viele die Bank noch hat, und einen Kaufknopf. Für die Überschriften bauen wir uns eine kleine Closure klein, weil wir sechsmal dasselbe brauchen, nur mit anderem Text und anderer Breite:

src/main.rsin fn view, Kettentafel
        .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 {        ⋮                text(kurs).size(13).color(farben.schrift).width(70),                text(self.spiel.spieler.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.spiel
                            .darf_kaufen(kette)
                            .then_some(Message::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()
                        }
                    }),            ]

Der Kaufknopf ist nur aktiv, wenn darf_kaufen ja sagt. Sonst verblasst er auf ein Viertel seiner Deckkraft. Zum Schluss sagt die Fußleiste in der Kaufphase, wie viele Käufe noch übrig sind:

src/main.rsin fn view, Fußleiste
        if let Phase::Kaufen { .. } = self.spiel.phase {        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 fertig = button(text("Zug beenden").size(14))        ⋮            rechts = rechts.push(fertig);            rechts = rechts.push(rest).push(fertig);        }
Probelauf Speichern, dann im Terminal:
cargo run
Das Fenster ist größer, oben rechts steht deine Kasse mit 6.000 €. Gründe eine Kette: In der Spalte „Du“ steht sofort eine 1, deine Gründeraktie. In der Kaufphase leuchten die Plus-Knöpfe. Kauf drei Aktien, und der Zug endet von selbst. Wird das Geld knapp, bleiben teure Aktien grau.
Zwischenstand

Der komplette Code nach Etappe 4

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

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

Cargo.toml

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

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

src/main.rs

mod spiel;

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

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

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

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

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

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

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

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

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

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

        let hinweis = match self.maus_feld {
            Some(feld) => format!("Feld {}", feld.name()),
            None => String::from("Fahr mit der Maus über das Baugrundstück"),
        };
        let status = text(hinweis).size(15).color(farben.schrift);
        let kasse = text(format!("Kasse: {}", geld(self.spiel.spieler.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 darf_legen = self.spiel.phase == Phase::Legen;
        let mut hand = row![text("Deine Plättchen:").size(15).color(farben.schrift)]
            .spacing(10)
            .align_y(alignment::Vertical::Center);
        for &feld in &self.spiel.hand {
            let plaettchen = button(text(feld.name()).size(16).font(Font::MONOSPACE))
                .padding([8, 14])
                .on_press_maybe(darf_legen.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);
        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 fertig = button(text("Zug beenden").size(14))
                .padding([8, 16])
                .on_press(Message::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.spiel.spieler.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.spiel
                            .darf_kaufen(kette)
                            .then_some(Message::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 = container(ketten)
            .padding(14)
            .style(move |_theme: &Theme| container::Style {
                background: Some(Background::Color(Color {
                    a: 0.85,
                    ..farben.tafel
                })),
                border: Border {
                    radius: 10.0.into(),
                    ..Border::default()
                },
                ..container::Style::default()
            });
        let tafelzeile = row![space::horizontal(), tafel].padding([0, 24]);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        vec![frame.into_geometry()]
    }

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

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

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

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

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

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

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

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

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

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

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

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

        zeichne_quader(frame, iso, &haus);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// Was einem Spieler gehört
pub struct Spieler {
    pub geld: u32,
    pub aktien: [u32; 7],
}

// Alles, was die Spielregeln wissen müssen
pub struct Spiel {
    pub brett: Vec<Belegung>,
    pub stapel: Vec<Feld>,
    pub hand: Vec<Feld>,
    pub phase: Phase,
    pub spieler: Spieler,
    // Die Aktien, die noch niemandem gehören
    pub bank: [u32; 7],
}

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

        let mut spiel = Spiel {
            brett: vec![Belegung::Frei; SPALTEN * REIHEN],
            stapel,
            hand: Vec::new(),
            phase: Phase::Legen,
            spieler: Spieler {
                geld: STARTGELD,
                aktien: [0; 7],
            },
            bank: [AKTIEN_JE_KETTE; 7],
        };
        spiel.nachziehen();
        spiel
    }

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

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

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

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

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

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

        if let Some(&kette) = ketten.first() {
            self.ausbreiten(feld, kette);
            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.spieler.aktien[kette.nr()] += 1;
        }
        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.spieler.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;
        };
        self.spieler.geld -= self.preis(kette);
        self.spieler.aktien[kette.nr()] += 1;
        self.bank[kette.nr()] -= 1;

        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) {
        if let Phase::Kaufen { .. } = self.phase {
            self.naechster_zug();
        }
    }

    // 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 Zug beginnt
    fn naechster_zug(&mut self) {
        self.nachziehen();
        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()
    }

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

    // Färbt ein Feld und alle angrenzenden Rohbauten in der Farbe der Kette
    fn ausbreiten(&mut self, start: Feld, kette: Kette) {
        let mut offen = vec![start];
        while let Some(feld) = offen.pop() {
            self.brett[feld.index()] = Belegung::Hotel(kette);
            for nachbar in feld.nachbarn() {
                if self.belegung(nachbar) == Belegung::Rohbau {
                    offen.push(nachbar);
                }
            }
        }
    }
}
ZusammenfassungEin Zustandsautomat teilt einen Zug in Phasen, und jede Regel prüft zuerst, ob sie in der aktuellen Phase erlaubt ist. Mit let … else und if let holt man Daten sicher aus einem enum. In match gibt es Bereiche wie 1..=5, Namen mit @ und Alternativen mit |. Prüfen (darf_kaufen) und Handeln (kaufen) sind getrennt, damit Oberfläche und Regeln immer dieselbe Antwort geben. Und on_press_maybe schaltet Knöpfe passend an und aus.
Übungen

Zum Ausprobieren

  1. Reich geboren: Starte mit 10.000 €. Was musst du ändern?
  2. Depotwert: Zeig in der Kopfzeile neben der Kasse auch, was deine Aktien gerade zusammen wert sind. Tipp: Schreib dafür in spiel.rs eine Methode depotwert, die für jede Kette Anzahl mal Kurs rechnet.
  3. Grenzfall: Was passiert, wenn du in der Kaufphase eine Kette kaufen willst, von der die Bank keine Aktien mehr hat? Wo genau im Code wird das verhindert?
Lösungen anzeigen

1. Eine Zeile in spiel.rs: pub const STARTGELD: u32 = 10000;. Zahlen dürfen in Rust übrigens Unterstriche haben, 10_000 ist dasselbe und liest sich leichter.

2. In impl Spiel:

pub fn depotwert(&self) -> u32 {
    Kette::ALLE
        .into_iter()
        .map(|kette| self.spieler.aktien[kette.nr()] * self.preis(kette))
        .sum()
}

map verwandelt jede Kette in einen Betrag, sum zählt alles zusammen. In view kannst du den Text der Kasse dann zum Beispiel so bauen: format!("Kasse: {} · Aktien: {}", geld(…), geld(self.spiel.depotwert())).

3. Nichts, und das ist Absicht. In darf_kaufen steht self.bank[kette.nr()] > 0. Ist die Bank leer, ist die Bedingung falsch, der Knopf bleibt grau, und selbst wenn jemand kaufen direkt aufriefe, würde es sofort mit return aussteigen.

Etappe 4 geschafft 💶

Es gibt Kurse, eine Kasse und eine Bank. Nur spielst du noch allein, und das ist auf Dauer langweilig. In der nächsten Etappe setzen sich Mitspieler an den Tisch: drei Computergegner, die reihum legen und kaufen. Den Anfang macht der Tourist, der nicht viel nachdenkt.

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

☕ Kaffee spendieren