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

Krebse verstecken – das Watt fängt an zu zählen

Bisher ist unser Watt harmlos: Man kann überall hintreten. Heute verstecken wir zehn Krebse unter dem Sand, und zwar jedes Mal an anderen Stellen. Dafür holen wir uns zum ersten Mal eine fremde Bibliothek ins Projekt, die Zufallszahlen erzeugt. Und dann bringen wir jedem Feld bei, seine Nachbarn zu zählen. Am Ende dieser Etappe erscheinen beim Aufdecken die berühmten Zahlen.

5 Schritte · ca. 45 Minuten · baut auf Etappe 2 auf

Das Watt mit aufgedeckten Feldern, Zahlen in verschiedenen Farben und roten Feldern, unter denen Krebse saßen
So sieht der Wattläufer am Ende dieser Etappe aus.
Vorab

Bevor wir loslegen

Wir machen da weiter, wo Etappe 2 aufgehört hat. Falls du neu einsteigst: Den kompletten Stand findest du am Ende von Etappe 2.

Heute stecken zwei Aufgaben in einer. Erstens: Wo sind die Krebse? Das entscheidet der Zufall, ganz am Anfang, wenn das Watt angelegt wird. Zweitens: Was verrät die Zahl? Jedes Feld zählt, wie viele Krebse in den acht Feldern rundherum stecken. Diese Zahl rechnen wir einmal beim Anlegen aus und merken sie uns, denn sie ändert sich während des Spiels nie.

In dieser Etappe darfst du übrigens noch ungestraft auf Krebse treten. Das Feld wird dann einfach rot. Dass ein Krebs das Spiel beendet, bauen wir in Etappe 4.

Schritt 1

Das Feld weiß mehr

Ein Feld muss sich jetzt zwei neue Dinge merken: ob ein Krebs darunter steckt und wie viele Krebse um es herum sitzen. Für das erste nehmen wir bool, einen Wert, der nur true (wahr) oder false (falsch) sein kann. Für das zweite u8, eine kleine ganze Zahl von 0 bis 255. Mehr als acht Nachbarn kann ein Feld nicht haben, da reicht das locker.

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

Weil ein Feld jetzt drei Angaben hat, müssen wir in neu auch alle drei ausfüllen. Die Zeile würde dadurch sehr lang, also bauen wir das Muster-Feld vorher in Ruhe zusammen und geben ihm einen Namen:

src/main.rsin impl Watt, Funktion neu
    fn neu() -> Self {        let felder = vec![Feld { zustand: Zustand::Verdeckt }; BREITE * HOEHE];        let leer = Feld {
            krebs: false,
            nachbarn: 0,
            zustand: Zustand::Verdeckt,
        };
        let felder = vec![leer; BREITE * HOEHE];        Watt { felder }
    }

leer ist ein Feld ohne Krebs, ohne Krebs-Nachbarn und noch verdeckt. Davon machen wir 81 Kopien. Die Krebse verteilen wir im nächsten Schritt.

Worauf du achten musst Rust ist bei structs sehr genau: Wenn du ein Feld anlegst, musst du alle Angaben ausfüllen. Vergisst du eine, meldet Rust missing field `nachbarn` (oder welches auch immer fehlt). Das schützt dich vor halb fertigen Daten.
Probelauf Speichern, dann im Terminal:
cargo run
Äußerlich ändert sich nichts. Rust warnt, dass krebs und nachbarn nie gelesen werden. Keine Sorge, das kommt.
Schritt 2

Der Zufall kommt ins Spiel

Wo die Krebse sitzen, soll jedes Spiel anders sein. Wir brauchen also Zufallszahlen. Rust selbst bringt dafür nichts mit, aber es gibt eine sehr bekannte Bibliothek namens rand. In der Rust-Welt heißen solche Bibliotheken Crates, zu Deutsch Kisten. Du holst sie genauso wie iced:

cargo add rand@0.10

In der Cargo.toml steht jetzt eine zweite Abhängigkeit:

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

[dependencies]
iced = "0.14"
rand = "0.10"

Wie viele Krebse es gibt, legen wir als Konstante fest:

src/main.rsbei HOEHE
const HOEHE: usize = 9;// So viele Krebse verstecken sich im Watt
const KREBSE: usize = 10;

Und jetzt das eigentliche Verstecken in neu. Aus let felder wird dabei let mut felder, denn wir wollen die Liste nach dem Anlegen noch verändern:

src/main.rsin impl Watt, Funktion neu
            zustand: Zustand::Verdeckt,
        };        let felder = vec![leer; BREITE * HOEHE];        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;
        }        Watt { felder }

Was passiert hier?

  • rand::rng() holt uns einen Zufallsgenerator, eine Art Würfelbecher. rng ist die übliche Abkürzung dafür (random number generator).
  • rand::seq::index::sample(&mut rng, 81, 10) zieht mit diesem Würfelbecher 10 verschiedene Zahlen zwischen 0 und 80, wie bei einer Lottoziehung: Keine Zahl kommt doppelt vor. Genau das brauchen wir, denn unter einem Feld soll nicht zweimal ein Krebs stecken.
  • Die for-Schleife geht die gezogenen Nummern durch und setzt bei jedem dieser Felder krebs auf true.
Worauf du achten musst Der Name rand::seq::index::sample ist lang. Er beschreibt den Weg durch die Bibliothek, wie ein Pfad durch Ordner: in rand, darin in seq, darin in index, und dort liegt die Funktion sample. Jeder Doppelpunkt-Doppelpunkt ist ein Schritt tiefer. Tipp ihn sorgfältig ab. Und der erste Start dauert wieder etwas länger, weil cargo die neue Kiste erst herunterlädt und übersetzt.
Probelauf Speichern, dann im Terminal:
cargo run
Du siehst noch keinen Unterschied, denn die Krebse sind ja versteckt, und anzeigen tun wir sie noch nicht. Die Warnung zu krebs ist aber weg, denn wir benutzen das Feld jetzt. Die zu nachbarn bleibt noch ein bisschen.
Schritt 3

Wer sind meine Nachbarn?

Bevor ein Feld seine Krebs-Nachbarn zählen kann, muss es wissen, wer seine Nachbarn überhaupt sind. Das klingt einfach: die acht Felder rundherum. Aber am Rand stimmt das nicht mehr. Ein Feld in der Ecke hat nur drei Nachbarn, eines am Rand fünf. Wir schreiben eine Funktion, die für jedes Feld die richtige Nachbarliste ausrechnet. Sie kommt ganz ans Ende der Datei, außerhalb von impl Watt:

src/main.rsganz am Ende der Datei
            .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
}

Das ist die kniffligste Funktion bisher, deshalb in Ruhe:

  • Zuerst rechnen wir aus der Feldnummer die Position aus: i % BREITE ist die Spalte, i / BREITE die Reihe. Das kennst du schon aus der Notiz in Etappe 2.
  • Die Nachbarn liegen immer eine Spalte oder Reihe daneben, also bei −1, 0 oder +1. Die zwei Schleifen gehen alle neun Kombinationen durch: -1..=1 zählt von −1 bis einschließlich 1. Das = macht den Unterschied zu 0..9, das vor der 9 aufhört.
  • Die Kombination 0 und 0 ist das Feld selbst. continue heißt: „Überspring den Rest und mach mit der nächsten Runde weiter.“
  • Dann prüfen wir, ob die Nachbarposition überhaupt noch im Watt liegt. Links vom linken Rand wäre die Spalte −1, und die gibt es nicht. Nur Positionen, die drin liegen, rechnen wir in eine Feldnummer um und legen sie in die Liste.
Warum i32 und nicht usize? usize kann nicht negativ werden. Wenn wir bei Spalte 0 eins abziehen, gäbe es mit usize einen Fehler, und das Programm würde abstürzen. Deshalb rechnen wir hier mit i32, einer ganzen Zahl, die auch negativ sein darf. as i32 wandelt um, und am Ende wandeln wir mit as usize wieder zurück, wenn wir sicher sind, dass die Zahl nicht negativ ist.
Probelauf Speichern, dann im Terminal:
cargo run
Äußerlich passiert nichts, und Rust meldet, dass die Funktion nachbarn nie benutzt wird (neben der alten Warnung zum Feld nachbarn). Wir haben ein Werkzeug gebaut, aber noch nicht in die Hand genommen.
Schritt 4

Krebse zählen

Jetzt nehmen wir das Werkzeug in die Hand. Direkt nachdem die Krebse versteckt sind, gehen wir einmal über das ganze Watt und lassen jedes Feld seine Krebs-Nachbarn zählen:

src/main.rsin impl Watt, Funktion neu
        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 }

Wieder zwei Schleifen ineinander. Die äußere geht über alle Felder. Für jedes Feld beginnen wir mit anzahl = 0, gehen dann alle seine Nachbarn durch, und immer wenn unter einem Nachbarn ein Krebs steckt, zählen wir eins hoch. anzahl += 1 ist die Kurzform von anzahl = anzahl + 1. Zum Schluss schreiben wir das Ergebnis ins Feld.

Woher weiß Rust eigentlich, dass anzahl ein u8 sein soll? Wir haben es nirgends hingeschrieben. Rust sieht aber, dass wir anzahl am Ende in felder[i].nachbarn speichern, und das ist ein u8. Also muss anzahl auch einer sein. Diese Fähigkeit, Typen selbst herauszufinden, heißt Typinferenz.

Probelauf Speichern, dann im Terminal:
cargo run
Immer noch keine sichtbare Änderung, aber im Terminal sind jetzt alle Warnungen verschwunden. Unser Watt kennt jetzt die Lage aller Krebse und für jedes Feld die richtige Zahl. Es behält sie nur noch für sich.
Schritt 5

Die Zahlen erscheinen

Endlich zeigen wir, was das Watt weiß. Wenn ein Feld aufgedeckt ist, gibt es drei Möglichkeiten: Es steckt ein Krebs darunter, dann soll es rot werden. Es hat Krebs-Nachbarn, dann soll die Anzahl draufstehen. Oder es hat keine, dann bleibt es einfach leer. Zuerst die Farbe für die Krebsfelder:

src/main.rsbei den Farben
const NASS: Color = Color::from_rgb8(0xb9, 0xa8, 0x86);const ROT: Color = Color::from_rgb8(0xd9, 0x4a, 0x2b);

Jetzt der Umbau in feld_ansicht. Die Farbwahl bekommt eine dritte Möglichkeit, und statt eines leeren Textes kommt ein passender Inhalt in den Container:

src/main.rsin der Funktion feld_ansicht
        let verdeckt = feld.zustand == Zustand::Verdeckt;        let grund = if verdeckt { SAND } else { NASS };

        let kachel = container(text(""))
            .width(FELD)
            .height(FELD)        let grund = if verdeckt {
            SAND
        } else if feld.krebs {
            ROT
        } else {
            NASS
        };

        let inhalt = if !verdeckt && !feld.krebs && feld.nachbarn > 0 {
            text(feld.nachbarn).size(20).color(zahlfarbe(feld.nachbarn))
        } else {
            text("")
        };

        let kachel = container(inhalt)
            .center(FELD)            .style(move |_theme: &Theme| container::Style {

Drei Dinge sind neu:

  • if … else if … else wählt jetzt zwischen drei Farben: verdeckt ist Sand, aufgedeckt mit Krebs ist rot, sonst nasser Sand.
  • Die Bedingung für eine Zahl liest sich so: „nicht verdeckt und kein Krebs und mehr als null Nachbarn“. Das ! heißt „nicht“, das && heißt „und“. Nur wenn alles stimmt, zeigen wir die Zahl, 20 Pixel groß und in ihrer eigenen Farbe.
  • .center(FELD) ersetzt .width(FELD) und .height(FELD): Es macht den Container 44 mal 44 Pixel groß und stellt den Inhalt genau in die Mitte. Sonst säße die Zahl oben links in der Ecke.

Fehlt nur noch zahlfarbe. Die kommt wieder ans Ende der Datei:

src/main.rsganz am Ende der Datei
        }
    }
    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),
    }
}

Hier sehen wir match mit Zahlen statt mit einem enum. Das _ in der letzten Zeile bedeutet „alles andere“, also 4, 5, 6, 7 und 8. Die sind so selten, dass sie sich eine Farbe teilen dürfen. Die Eins bekommt übrigens unser Meeresblau: Konstanten kann man auch an anderen Stellen wiederverwenden.

Worauf du achten musst Ohne das _ würde Rust sich weigern: Ein u8 kann ja auch 4 oder 200 sein, und match muss für jeden möglichen Wert wissen, was zu tun ist. Rust prüft das genau und meldet sonst non-exhaustive patterns. Probier es ruhig aus!
Probelauf Speichern, dann im Terminal:
cargo run
Jetzt wird's ein Spiel! Klick dich durchs Watt: Auf den meisten Feldern erscheinen farbige Zahlen, manche bleiben leer, und wenn du Pech hast, wird ein Feld rot, denn da saß ein Krebs. Schließ das Fenster und starte neu: Die Krebse sitzen jetzt woanders.
Das Watt mit Zahlen und roten Krebsfeldern
Stand nach Schritt 5
Zwischenstand

Der komplette Code nach Etappe 3

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

Cargo.toml und src/main.rs anzeigen

Cargo.toml

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

[dependencies]
iced = "0.14"
rand = "0.10"

src/main.rs

use iced::widget::{column, container, mouse_area, row, text};
use iced::{mouse, Background, Border, 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;

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))
        .run()
}

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

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

// Das ganze Watt: eine Liste aller Felder.
struct Watt {
    felder: Vec<Feld>,
}

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

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

    fn update(&mut self, message: Message) {
        match message {
            Message::Aufdecken(i) => {
                self.felder[i].zustand = Zustand::Aufgedeckt;
            }
        }
    }

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

        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![titel, 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::Verdeckt;
        let grund = if verdeckt {
            SAND
        } else if feld.krebs {
            ROT
        } else {
            NASS
        };

        let inhalt = if !verdeckt && !feld.krebs && feld.nachbarn > 0 {
            text(feld.nachbarn).size(20).color(zahlfarbe(feld.nachbarn))
        } else {
            text("")
        };

        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))
            .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),
    }
}
ZusammenfassungFremde Bibliotheken heißen in Rust Crates und kommen mit cargo add ins Projekt, zum Beispiel rand für Zufallszahlen. Mit bool speichern wir Ja/Nein-Werte, mit u8 und i32 ganze Zahlen, wobei nur i32 negativ werden darf. for-Schleifen mit continue und Bedingungen mit ! und && helfen beim Durchsuchen, und match muss immer alle Fälle abdecken, notfalls mit _.
Ü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. Krebsplage: Stell KREBSE auf 40. Wie fühlt sich das Spiel an? Und was passiert, wenn du es auf 82 stellst?
  2. Eckenzählung: Wie viele Nummern liefert nachbarn(0), wie viele nachbarn(4) und wie viele nachbarn(40)? Überleg es dir erst, dann prüf es, zum Beispiel mit println!("{:?}", nachbarn(0)); am Anfang von main.
  3. Spickzettel: Lass in feld_ansicht die Zahlen auch auf verdeckten Feldern erscheinen. Welches einzelne Zeichen musst du dafür entfernen?
Lösungen anzeigen

1. Bei 40 Krebsen ist fast jedes zweite Feld gefährlich, und man tritt ständig auf einen. Bei 82 stürzt das Programm beim Start ab: sample soll 82 verschiedene Zahlen aus 81 ziehen, und das geht nicht. Die Fehlermeldung sagt sinngemäß, dass mehr gezogen werden soll, als es gibt. Das zeigt, warum Konstanten mit Verstand gewählt werden wollen.

2. nachbarn(0) liefert 3 Nummern (die Ecke oben links: 1, 9 und 10), nachbarn(4) liefert 5 (Mitte des oberen Randes), nachbarn(40) liefert 8 (genau die Mitte). Das {:?} in println! gibt die ganze Liste aus.

3. Das ! vor verdeckt in der Bedingung. Dann steht dort verdeckt && …, also nur noch bei verdeckten Feldern. Wenn du stattdessen die ganze Bedingung !verdeckt && weglässt, erscheinen die Zahlen überall. So oder so: ein prima Spickzettel, aber schnell wieder rückgängig machen!

Etappe 3 geschafft 🦀

Unter dem Sand stecken jetzt Krebse, jedes Spiel an anderen Stellen, und das Watt zeigt beim Aufdecken die richtigen Zahlen. In der nächsten Etappe sorgen wir dafür, dass sich leere Flächen von allein aufdecken, und ein Krebs beendet das Spiel.

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

☕ Kaffee spendieren