Rust-Werkstatt
Werkstatt · Wattläufer · Etappe 6 von 6

Feinschliff mit Ferris

Der Wattläufer ist schon ein vollständiges Spiel. Heute machen wir daraus eines, das man gern spielt: Ein Knopf startet ein neues Spiel, ohne das Programm neu zu starten. Der erste Schritt ins Watt ist immer sicher, so wie bei den großen Vorbildern. Und statt roter Felder zeigt das Watt beim Verlieren, wer da eigentlich zwickt: Ferris, das Rust-Maskottchen.

4 Schritte · ca. 35 Minuten · baut auf Etappe 5 auf · die letzte Etappe

Der fertige Wattläufer: Überschrift mit Knopf Neues Spiel, Statuszeile und ein Spielfeld mit Zahlen und Fähnchen
So sieht der Wattläufer am Ende dieser Etappe aus.
Vorab

Bevor wir loslegen

Wir machen da weiter, wo Etappe 5 aufgehört hat. Den kompletten Stand findest du am Ende von Etappe 5.

Diese Etappe besteht aus kleinen Verbesserungen, die jede für sich wenig Code brauchen, das Spiel aber spürbar schöner machen. Genau so sieht die Arbeit an echten Programmen oft aus: Wenn das Grundgerüst steht, beginnt die Politur. Und manchmal steckt in einer kleinen Verbesserung eine größere Umbauarbeit, so wie in Schritt 2.

Schritt 1

Ein Knopf für ein neues Spiel

Bisher musst du nach jedem Spiel das Fenster schließen und neu starten. Ein Knopf oben rechts soll das erledigen. iced hat dafür das Widget button. Außerdem brauchen wir Center, um Überschrift und Knopf auf eine Höhe zu bringen:

src/main.rsganz oben, die ersten beiden Zeilen
use iced::widget::{column, container, mouse_area, row, svg, text};
use iced::{mouse, Background, Border, Color, Element, Fill, Font, Theme};use iced::widget::{button, column, container, mouse_area, row, svg, text};
use iced::{mouse, Background, Border, Center, Color, Element, Fill, Font, Theme};

Ein Klick auf den Knopf wird zu einem neuen Brief. Er braucht keine Feldnummer, denn es geht ja ums ganze Spiel:

src/main.rsdas enum Message
    Markieren(usize),    NeuesSpiel,}

Und update weiß, was bei diesem Brief zu tun ist:

src/main.rsin der Funktion update, als erster Zweig
    fn update(&mut self, message: Message) {
        match message {            Message::NeuesSpiel => *self = Watt::neu(),            Message::Aufdecken(i) => {

Eine einzige Zeile, und doch steckt eine hübsche Idee darin. Watt::neu() baut ein komplett frisches Watt, genau wie beim Programmstart. Und *self = … ersetzt unser bisheriges Watt vollständig durch das neue. Das Sternchen heißt: „Nicht die Ausleihe überschreiben, sondern das, worauf sie zeigt.“ Alle Felder, der Spielstand, einfach alles ist danach wie neu. Kurze Zweige ohne geschweifte Klammern sind übrigens erlaubt, wenn sie nur aus einem Ausdruck bestehen.

Jetzt bauen wir den Knopf in view und stellen ihn neben die Überschrift:

src/main.rsin der Funktion view
        let titel = text("WATTLÄUFER").size(22).color(SCHAUM);        let neu_knopf = button(text("Neues Spiel"))
            .on_press(Message::NeuesSpiel)
            .style(|_theme: &Theme, _status| button::Style {
                background: Some(Background::Color(SAND)),
                text_color: MEER,
                border: Border {
                    radius: 6.0.into(),
                    ..Border::default()
                },
                ..button::Style::default()
            });

        let kopf = row![titel.width(Fill), neu_knopf].align_y(Center);    ⋮        let inhalt = column![titel, text(status).color(SCHAUM), watt].spacing(14);        let inhalt = column![kopf, text(status).color(SCHAUM), watt].spacing(14);

Beim Knopf kennst du fast alles schon: .on_press(…) sagt, welcher Brief beim Klick verschickt wird, und .style(…) gestaltet ihn in Sandfarbe mit blauer Schrift. Die Closure bekommt hier zwei Dinge: das Farbschema und den Zustand des Knopfes, zum Beispiel ob gerade die Maus darüber schwebt. Beides brauchen wir nicht, daher die Unterstriche.

In der Kopfzeile bekommt der Titel .width(Fill): Er nimmt sich allen Platz, den der Knopf übrig lässt, und schiebt ihn damit ganz nach rechts. .align_y(Center) stellt beide auf dieselbe Höhe.

Probelauf Speichern, dann im Terminal:
cargo run
Oben rechts sitzt ein sandfarbener Knopf Neues Spiel. Tritt auf einen Krebs, klick den Knopf, und schon liegt ein frisches Watt vor dir, mit neu versteckten Krebsen.
Schritt 2

Der erste Schritt ist immer sicher

Kennst du das? Erster Klick, und schon zwickt es. Das ist nicht fair, denn beim ersten Klick kann man noch gar nichts wissen. Die bekannten Minesweeper-Spiele lösen das mit einem Trick, den wir übernehmen: Die Krebse werden erst versteckt, nachdem du zum ersten Mal geklickt hast, und zwar nie auf diesem Feld und nie direkt daneben. So öffnet der erste Klick immer eine Fläche.

Dafür muss sich das Watt merken, ob der erste Schritt noch bevorsteht:

src/main.rsdas struct Watt
struct Watt {
    felder: Vec<Feld>,
    spiel: Spiel,    erster_schritt: bool,}

Jetzt kommt der Umbau. Das Verstecken und Zählen wandert aus neu heraus. In neu bleibt nur das leere Watt übrig. Weil wir die Liste dort nicht mehr verändern, wird aus let mut felder wieder let felder:

src/main.rsin impl Watt, Funktion neu
            zustand: Zustand::Verdeckt,
        };        let mut felder = vec![leer; BREITE * HOEHE];

        // Zehn verschiedene Feldnummern auslosen und dort Krebse verstecken
        let mut rng = rand::rng();
        for i in rand::seq::index::sample(&mut rng, BREITE * HOEHE, KREBSE) {
            felder[i].krebs = true;
        }

        // Für jedes Feld zählen, wie viele Krebse in den Nachbarfeldern stecken
        for i in 0..felder.len() {
            let mut anzahl = 0;
            for n in nachbarn(i) {
                if felder[n].krebs {
                    anzahl += 1;
                }
            }
            felder[i].nachbarn = anzahl;
        }

        Watt { felder, spiel: Spiel::Laeuft }        let felder = vec![leer; BREITE * HOEHE];
        Watt {
            felder,
            spiel: Spiel::Laeuft,
            erster_schritt: true,
        }    }

Das Watt { … } am Ende ist auf mehrere Zeilen verteilt, weil es drei Angaben sind. felder, allein ist eine Abkürzung für felder: felder,, also „das Feld felder bekommt den Wert der Variablen felder“.

Das Verstecken und Zählen bekommt eine eigene Funktion, direkt hinter neu. Das meiste davon kennst du aus Etappe 3, nur heißt es jetzt self.felder statt felder:

src/main.rsin impl Watt, direkt hinter der Funktion neu
            erster_schritt: true,
        }
    }    // Versteckt die Krebse, aber nie auf dem Startfeld oder direkt daneben.
    fn krebse_verstecken(&mut self, start: usize) {
        let mut sicher = nachbarn(start);
        sicher.push(start);

        let erlaubt: Vec<usize> = (0..BREITE * HOEHE)
            .filter(|i| !sicher.contains(i))
            .collect();

        let mut rng = rand::rng();
        for k in rand::seq::index::sample(&mut rng, erlaubt.len(), KREBSE) {
            self.felder[erlaubt[k]].krebs = true;
        }

        // Für jedes Feld zählen, wie viele Krebse in den Nachbarfeldern stecken
        for i in 0..self.felder.len() {
            let mut anzahl = 0;
            for n in nachbarn(i) {
                if self.felder[n].krebs {
                    anzahl += 1;
                }
            }
            self.felder[i].nachbarn = anzahl;
        }
    }

Neu ist der Anfang:

  • sicher ist die Liste der geschützten Felder: alle Nachbarn des Startfelds, und mit push hängen wir das Startfeld selbst noch an.
  • erlaubt sind alle Feldnummern, die nicht in sicher stehen. (0..BREITE * HOEHE) sind alle Nummern von 0 bis 80, .filter(…) behält nur die erlaubten, und .collect() sammelt sie in einem neuen Vec. Die Typangabe Vec<usize> sagt collect, was für eine Sammlung es bauen soll.
  • Dann losen wir wie gehabt zehn aus, aber nicht direkt Feldnummern, sondern Plätze in der Liste erlaubt. erlaubt[k] ist die Feldnummer, die auf Platz k steht.

Bleibt, die neue Funktion beim ersten Klick aufzurufen. Das passiert in update, direkt nach der Prüfung, ob das Feld verdeckt ist:

src/main.rsin der Funktion update, Zweig Aufdecken
                if self.felder[i].zustand != Zustand::Verdeckt {
                    return; // Fähnchen oder schon offen: nicht aufdecken
                }                if self.erster_schritt {
                    self.krebse_verstecken(i);
                    self.erster_schritt = false;
                }                if self.felder[i].krebs {
Worauf du achten musst Die Reihenfolge ist wichtig: Die Krebse müssen versteckt werden, bevor wir fragen, ob unter dem Feld ein Krebs steckt. Stünde der neue Block weiter unten, würde der erste Klick auf ein garantiert krebsfreies Watt treffen, und danach wären die Krebse zwar da, aber zu spät.
Probelauf Speichern, dann im Terminal:
cargo run
Starte ein paar Spiele mit dem Knopf und klick jedes Mal zuerst irgendwohin, auch in eine Ecke: Der erste Klick öffnet immer eine Fläche und trifft nie einen Krebs.
Schritt 3

Ferris zieht ins Watt

Wer zwickt da eigentlich? Bisher nur ein rotes Feld. Jetzt bekommt der Krebs ein Gesicht: Ferris, das freundliche Rust-Maskottchen. Ferris ist gemeinfrei, jeder darf ihn benutzen. Und nur das Feld, auf das du getreten bist, soll rot leuchten. Die anderen Krebse zeigen sich auf nassem Sand.

Zuerst brauchen wir Ferris als Datei. Leg in deinem Projektordner, also neben src, einen neuen Ordner assets an. Dann lade Ferris als SVG herunter und speichere ihn dort als ferris.svg. Dein Projekt sieht dann so aus:

wattlaeufer/
├── assets/
│   └── ferris.svg
├── src/
│   └── main.rs
└── Cargo.toml

Beim Fähnchen haben wir die Zeichnung direkt in den Code geschrieben. Ferris ist dafür zu groß, er steht in einer eigenen Datei. Mit include_bytes! holt Rust die Datei beim Übersetzen ins Programm hinein:

src/main.rshinter der Konstanten FAHNE
  <ellipse cx="7" cy="21" rx="5" ry="1.5" fill="#8a6a3a"/>
</svg>"##;// Ferris, das Rust-Maskottchen, aus der Datei assets/ferris.svg
const FERRIS: &[u8] = include_bytes!("../assets/ferris.svg");

Der Pfad ist relativ zu src/main.rs: .. heißt „einen Ordner hoch“, dann in assets zu ferris.svg. Das Schöne: Ferris steckt danach im fertigen Programm selbst. Man muss die Datei nicht mitliefern.

Jetzt muss sich das Watt merken, auf welches Feld du getreten bist. Aber solange du auf keins getreten bist, gibt es auch kein solches Feld. Für „vielleicht ein Wert, vielleicht keiner“ hat Rust Option:

src/main.rsdas struct Watt
    spiel: Spiel,
    erster_schritt: bool,    getroffen: Option<usize>,}

Option<usize> ist entweder Some(12), „ja, Feld 12“, oder None, „keins“. Mehr zu Option: Buch, Kapitel 14. Am Anfang ist es natürlich None:

src/main.rsam Ende der Funktion neu
            spiel: Spiel::Laeuft,
            erster_schritt: true,            getroffen: None,        }

Beim Tritt auf einen Krebs merken wir uns das Feld:

src/main.rsin der Funktion update, Zweig Aufdecken
                if self.felder[i].krebs {
                    self.spiel = Spiel::Verloren;                    self.getroffen = Some(i);                    self.alle_krebse_zeigen();

Und in feld_ansicht zeigen wir das Ganze an. Rot wird jetzt nur noch das getroffene Feld, und auf jedem aufgedeckten Krebsfeld sitzt Ferris:

src/main.rsin der Funktion feld_ansicht
        let grund = if verdeckt {
            SAND        } else if feld.krebs {
            ROT        } else if self.getroffen == Some(i) {
            ROT        } else {
            NASS
        };    ⋮            svg(svg::Handle::from_memory(FAHNE)).width(26).height(26).into()        } else if !verdeckt && feld.krebs {
            svg(svg::Handle::from_memory(FERRIS)).width(34).into()        } else if !verdeckt && !feld.krebs && feld.nachbarn > 0 {
Worauf du achten musst Wenn Rust meldet couldn't read `src/../assets/ferris.svg`, liegt die Datei nicht an der richtigen Stelle oder heißt anders. Der Ordner assets muss direkt im Projektordner liegen, nicht in src. Und weil include_bytes! beim Übersetzen arbeitet, ist das ein Fehler beim Kompilieren, nicht erst beim Spielen. Das ist gut so: Ein fehlender Ferris fällt sofort auf.
Probelauf Speichern, dann im Terminal:
cargo run
Tritt absichtlich auf einen Krebs: Das getroffene Feld leuchtet rot, und überall, wo Krebse steckten, sitzt jetzt Ferris im nassen Sand.
Verlorene Partie: überall Ferris, das getroffene Feld rot
Autsch! Ferris hat zugezwickt.
Schritt 4

Das Fenster bleibt, wie es ist

Die letzte Kleinigkeit: Unser Watt hat eine feste Größe. Zieht man das Fenster größer, entsteht nur leeres Meer am Rand, zieht man es kleiner, wird das Watt abgeschnitten. Also verbieten wir das Ändern der Fenstergröße einfach:

src/main.rsin fn main()
        .window_size((460.0, 560.0))        .resizable(false)        .run()

Eine Zeile, noch ein Waggon am Zug, fertig. Manche Linux-Fenstermanager halten sich übrigens nicht an diesen Wunsch. Das ist kein Fehler in deinem Programm.

Probelauf Speichern, dann im Terminal:
cargo run
Versuch, das Fenster an der Ecke größer zu ziehen: Es bleibt, wie es ist. Und damit ist der Wattläufer fertig! Spiel ruhig eine Runde. Du hast sie dir verdient.
Der fertige Wattläufer
Der fertige Wattläufer
Zwischenstand

Der komplette Code nach Etappe 6

Das ist der komplette, fertige Wattläufer: gut 300 Zeilen Rust, Kommentare und Leerzeilen mitgezählt. Heb ihn gut auf, er ist ein prima Startpunkt für eigene Ideen.

Cargo.toml und src/main.rs anzeigen

Cargo.toml

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

[dependencies]
iced = { version = "0.14", features = ["svg"] }
rand = "0.10"

src/main.rs

use iced::widget::{button, column, container, mouse_area, row, svg, text};
use iced::{mouse, Background, Border, Center, Color, Element, Fill, Font, Theme};

// Die Farben des Watts
const MEER: Color = Color::from_rgb8(0x1f, 0x4e, 0x6b);
const SCHAUM: Color = Color::from_rgb8(0xf4, 0xf1, 0xe8);
const SAND: Color = Color::from_rgb8(0xe8, 0xc9, 0x8f);
const SAND_KANTE: Color = Color::from_rgb8(0xc9, 0xa4, 0x62);
const NASS: Color = Color::from_rgb8(0xb9, 0xa8, 0x86);
const ROT: Color = Color::from_rgb8(0xd9, 0x4a, 0x2b);

// So groß ist ein Feld, in Pixeln
const FELD: f32 = 44.0;

// So groß ist das Watt
const BREITE: usize = 9;
const HOEHE: usize = 9;

// So viele Krebse verstecken sich im Watt
const KREBSE: usize = 10;

// Ein kleines Warnfähnchen als SVG-Grafik
const FAHNE: &[u8] = br##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
  <rect x="6" y="3" width="2" height="18" fill="#3b2a1a"/>
  <path d="M8 4 L19 8 L8 12 Z" fill="#d94a2b"/>
  <ellipse cx="7" cy="21" rx="5" ry="1.5" fill="#8a6a3a"/>
</svg>"##;

// Ferris, das Rust-Maskottchen, aus der Datei assets/ferris.svg
const FERRIS: &[u8] = include_bytes!("../assets/ferris.svg");

fn main() -> iced::Result {
    iced::application(Watt::neu, Watt::update, Watt::view)
        .title("Wattläufer")
        .default_font(Font::MONOSPACE)
        .window_size((460.0, 560.0))
        .resizable(false)
        .run()
}

// Ein Feld ist entweder noch verdeckt oder schon aufgedeckt.
#[derive(Clone, Copy, PartialEq)]
enum Zustand {
    Verdeckt,
    Markiert,
    Aufgedeckt,
}

// Alles, was wir über ein einzelnes Feld wissen.
#[derive(Clone, Copy)]
struct Feld {
    krebs: bool,
    nachbarn: u8,
    zustand: Zustand,
}

// Läuft das Spiel noch, oder ist es vorbei?
#[derive(PartialEq)]
enum Spiel {
    Laeuft,
    Gewonnen,
    Verloren,
}

// Das ganze Watt: eine Liste aller Felder.
struct Watt {
    felder: Vec<Feld>,
    spiel: Spiel,
    erster_schritt: bool,
    getroffen: Option<usize>,
}

#[derive(Debug, Clone)]
enum Message {
    Aufdecken(usize),
    Markieren(usize),
    NeuesSpiel,
}

impl Watt {
    // Legt ein neues Watt an, in dem alle Felder verdeckt sind.
    fn neu() -> Self {
        let leer = Feld {
            krebs: false,
            nachbarn: 0,
            zustand: Zustand::Verdeckt,
        };
        let felder = vec![leer; BREITE * HOEHE];
        Watt {
            felder,
            spiel: Spiel::Laeuft,
            erster_schritt: true,
            getroffen: None,
        }
    }

    // Versteckt die Krebse, aber nie auf dem Startfeld oder direkt daneben.
    fn krebse_verstecken(&mut self, start: usize) {
        let mut sicher = nachbarn(start);
        sicher.push(start);

        let erlaubt: Vec<usize> = (0..BREITE * HOEHE)
            .filter(|i| !sicher.contains(i))
            .collect();

        let mut rng = rand::rng();
        for k in rand::seq::index::sample(&mut rng, erlaubt.len(), KREBSE) {
            self.felder[erlaubt[k]].krebs = true;
        }

        // Für jedes Feld zählen, wie viele Krebse in den Nachbarfeldern stecken
        for i in 0..self.felder.len() {
            let mut anzahl = 0;
            for n in nachbarn(i) {
                if self.felder[n].krebs {
                    anzahl += 1;
                }
            }
            self.felder[i].nachbarn = anzahl;
        }
    }

    fn update(&mut self, message: Message) {
        match message {
            Message::NeuesSpiel => *self = Watt::neu(),
            Message::Aufdecken(i) => {
                if self.spiel != Spiel::Laeuft {
                    return; // Spiel vorbei: Klicks werden ignoriert
                }
                if self.felder[i].zustand != Zustand::Verdeckt {
                    return; // Fähnchen oder schon offen: nicht aufdecken
                }
                if self.erster_schritt {
                    self.krebse_verstecken(i);
                    self.erster_schritt = false;
                }
                if self.felder[i].krebs {
                    self.spiel = Spiel::Verloren;
                    self.getroffen = Some(i);
                    self.alle_krebse_zeigen();
                } else {
                    self.aufdecken(i);
                    if self.alles_sicher_aufgedeckt() {
                        self.spiel = Spiel::Gewonnen;
                    }
                }
            }
            Message::Markieren(i) => {
                if self.spiel != Spiel::Laeuft {
                    return;
                }
                let feld = &mut self.felder[i];
                feld.zustand = match feld.zustand {
                    Zustand::Verdeckt => Zustand::Markiert,
                    Zustand::Markiert => Zustand::Verdeckt,
                    Zustand::Aufgedeckt => Zustand::Aufgedeckt,
                };
            }
        }
    }

    // Deckt Feld i auf. Hat es keine Krebs-Nachbarn, deckt es auch alle Nachbarn auf.
    fn aufdecken(&mut self, i: usize) {
        if self.felder[i].zustand != Zustand::Verdeckt {
            return; // schon aufgedeckt: nichts zu tun
        }
        self.felder[i].zustand = Zustand::Aufgedeckt;

        if self.felder[i].nachbarn == 0 && !self.felder[i].krebs {
            for n in nachbarn(i) {
                self.aufdecken(n);
            }
        }
    }

    // Deckt alle Krebse auf, damit man sieht, wo sie steckten.
    fn alle_krebse_zeigen(&mut self) {
        for feld in &mut self.felder {
            if feld.krebs {
                feld.zustand = Zustand::Aufgedeckt;
            }
        }
    }

    // Gewonnen hat, wer jedes Feld ohne Krebs aufgedeckt hat.
    fn alles_sicher_aufgedeckt(&self) -> bool {
        self.felder
            .iter()
            .all(|feld| feld.krebs || feld.zustand == Zustand::Aufgedeckt)
    }

    fn view(&self) -> Element<'_, Message> {
        let titel = text("WATTLÄUFER").size(22).color(SCHAUM);

        let neu_knopf = button(text("Neues Spiel"))
            .on_press(Message::NeuesSpiel)
            .style(|_theme: &Theme, _status| button::Style {
                background: Some(Background::Color(SAND)),
                text_color: MEER,
                border: Border {
                    radius: 6.0.into(),
                    ..Border::default()
                },
                ..button::Style::default()
            });

        let kopf = row![titel.width(Fill), neu_knopf].align_y(Center);

        let fahnen = self
            .felder
            .iter()
            .filter(|feld| feld.zustand == Zustand::Markiert)
            .count();

        let status = match self.spiel {
            Spiel::Laeuft => format!("Krebse im Watt: {}", KREBSE as i32 - fahnen as i32),
            Spiel::Gewonnen => String::from("Geschafft! Trockene Füße."),
            Spiel::Verloren => String::from("Autsch! Gezwickt."),
        };

        let mut watt = column![].spacing(3);
        for y in 0..HOEHE {
            let mut reihe = row![].spacing(3);
            for x in 0..BREITE {
                let i = y * BREITE + x;
                reihe = reihe.push(self.feld_ansicht(i));
            }
            watt = watt.push(reihe);
        }

        let inhalt = column![kopf, text(status).color(SCHAUM), watt].spacing(14);

        container(inhalt)
            .padding(20)
            .width(Fill)
            .height(Fill)
            .style(|_theme: &Theme| container::Style {
                background: Some(Background::Color(MEER)),
                ..container::Style::default()
            })
            .into()
    }

    // Zeichnet das Feld Nummer i, je nachdem, in welchem Zustand es ist.
    fn feld_ansicht(&self, i: usize) -> Element<'_, Message> {
        let feld = self.felder[i];
        let verdeckt = feld.zustand != Zustand::Aufgedeckt;
        let grund = if verdeckt {
            SAND
        } else if self.getroffen == Some(i) {
            ROT
        } else {
            NASS
        };

        let inhalt: Element<'_, Message> = if feld.zustand == Zustand::Markiert {
            svg(svg::Handle::from_memory(FAHNE)).width(26).height(26).into()
        } else if !verdeckt && feld.krebs {
            svg(svg::Handle::from_memory(FERRIS)).width(34).into()
        } else if !verdeckt && !feld.krebs && feld.nachbarn > 0 {
            text(feld.nachbarn).size(20).color(zahlfarbe(feld.nachbarn)).into()
        } else {
            text("").into()
        };

        let kachel = container(inhalt)
            .center(FELD)
            .style(move |_theme: &Theme| container::Style {
                background: Some(Background::Color(grund)),
                border: Border {
                    color: SAND_KANTE,
                    width: if verdeckt { 2.0 } else { 0.0 },
                    radius: 6.0.into(),
                },
                ..container::Style::default()
            });

        mouse_area(kachel)
            .on_press(Message::Aufdecken(i))
            .on_right_press(Message::Markieren(i))
            .interaction(mouse::Interaction::Pointer)
            .into()
    }
}

// Liefert die Nummern aller Nachbarfelder von Feld i (höchstens acht).
fn nachbarn(i: usize) -> Vec<usize> {
    let x = (i % BREITE) as i32;
    let y = (i / BREITE) as i32;
    let mut liste = Vec::new();

    for dy in -1..=1 {
        for dx in -1..=1 {
            if dx == 0 && dy == 0 {
                continue; // das Feld selbst ist nicht sein eigener Nachbar
            }
            let nx = x + dx;
            let ny = y + dy;
            if nx >= 0 && nx < BREITE as i32 && ny >= 0 && ny < HOEHE as i32 {
                liste.push(ny as usize * BREITE + nx as usize);
            }
        }
    }
    liste
}

// Jede Zahl bekommt ihre eigene Farbe, damit man sie schneller erkennt.
fn zahlfarbe(anzahl: u8) -> Color {
    match anzahl {
        1 => MEER,
        2 => Color::from_rgb8(0x2e, 0x7d, 0x4f),
        3 => Color::from_rgb8(0xc2, 0x3b, 0x22),
        _ => Color::from_rgb8(0x5a, 0x2a, 0x6e),
    }
}
ZusammenfassungEin button verschickt beim Klick eine Nachricht, und *self = Watt::neu() setzt den ganzen Zustand auf einmal zurück. Mit .filter() und .collect() baut man aus einer Liste eine neue. include_bytes! holt Dateien schon beim Übersetzen ins Programm. Option steht für „vielleicht ein Wert, vielleicht keiner“. Und manchmal bedeutet eine kleine Verbesserung, Code umzuziehen: vom Programmstart in den ersten Klick.
Ausblick

Wie geht es weiter?

Der Wattläufer ist fertig, aber ein Spiel ist nie wirklich fertig. Hier ein paar Ideen, mit denen du ihn allein weiterbauen kannst. Sie sind nach Schwierigkeit sortiert, und für jede hast du in diesen sechs Etappen schon das Werkzeug kennengelernt.

  • Schwierigkeitsstufen: drei Knöpfe für „Strand“ (8 Krebse), „Watt“ (10) und „Priel“ (15). Tipp: Aus der Konstanten KREBSE wird ein Feld in Watt, und NeuesSpiel bekommt die Anzahl als Wert mit, so wie Aufdecken die Feldnummer.
  • Aufdecken per Doppelklick: Ein Doppelklick auf eine Zahl deckt alle Nachbarn ohne Fähnchen auf, wenn genau so viele Fähnchen drumherum stehen, wie die Zahl sagt. Profis spielen so. mouse_area hat dafür on_double_click.
  • Eine Stoppuhr: Wie schnell schaffst du das Watt? Dafür brauchst du etwas, das jede Sekunde von selbst eine Nachricht schickt. In iced heißt das Subscription, und im Buch (Kapitel 28) lernst du, wie das geht.
Übungen

Zum Ausprobieren

Die Übungen sind freiwillig, aber sie helfen enorm. Mach nach jeder Übung einen Probelauf, und stell danach den alten Zustand wieder her, damit die nächste Etappe bei dir genauso weitergeht wie bei allen anderen.

  1. Größeres Watt: Mach das Watt 12 mal 12 Felder groß mit 20 Krebsen. Welche Stellen musst du ändern? Denk an das Fenster!
  2. Warum nur zehn? In krebse_verstecken losen wir aus erlaubt, nicht aus allen 81 Feldern. Wie viele Einträge hat erlaubt, wenn der erste Klick in der Mitte ist, und wie viele bei einem Klick in die Ecke?
  3. Siegesfarbe: Lass die Statuszeile bei einem Sieg nicht in SCHAUM, sondern in SAND leuchten. Tipp: Du brauchst eine Variable für die Farbe, genau wie grund in feld_ansicht.
Lösungen anzeigen

1. BREITE und HOEHE auf 12, KREBSE auf 20, und die Fenstergröße in main muss wachsen: 12 Felder mal 44 Pixel plus 11 Lücken mal 3 Pixel plus 40 Pixel Rand ergibt 601 Pixel Breite, also zum Beispiel (601.0, 700.0). Alles andere rechnet sich aus den Konstanten von selbst, das ist der Lohn für die ordentliche Vorarbeit.

2. In der Mitte hat das Startfeld 8 Nachbarn, zusammen sind 9 Felder gesperrt, bleiben 72 erlaubte. In der Ecke hat es nur 3 Nachbarn, gesperrt sind 4, bleiben 77.

3. In view: let statusfarbe = if self.spiel == Spiel::Gewonnen { SAND } else { SCHAUM }; und dann text(status).color(statusfarbe) in der Spalte. Weil Spiel PartialEq hat, darf man es mit == vergleichen.

Der Wattläufer ist fertig! 🦀🏆

Aus einer leeren Datei ist in sechs Etappen ein richtiges Spiel geworden, mit Zufall, Rekursion, Grafiken und einem Maskottchen, das zwickt. Wenn du Lust auf mehr hast: Im Fährmann bauen wir als Nächstes ein echtes Werkzeug, einen Dateimanager mit FTP und SFTP.

Hat dir der Wattläufer Spaß gemacht? Ein Kaffee hilft, dass die nächste Werkstatt ein Stück schneller fertig wird.

☕ Kaffee spendieren