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.
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.
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.
// 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:
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.
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.cargo run
Äußerlich ändert sich nichts. Rust warnt, dass krebs und
nachbarn nie gelesen werden. Keine Sorge, das kommt.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:
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:
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.rngist 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 Felderkrebsauftrue.
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.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.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:
.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 % BREITEist die Spalte,i / BREITEdie 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..=1zählt von −1 bis einschließlich 1. Das=macht den Unterschied zu0..9, das vor der 9 aufhört. - Die Kombination 0 und 0 ist das Feld selbst.
continueheiß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.
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.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.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:
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.
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.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:
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:
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 … elsewä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:
} } 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.
_ 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!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.
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),
}
}
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 _.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.
- Krebsplage: Stell
KREBSEauf 40. Wie fühlt sich das Spiel an? Und was passiert, wenn du es auf 82 stellst? - Eckenzählung: Wie viele Nummern liefert
nachbarn(0), wie vielenachbarn(4)und wie vielenachbarn(40)? Überleg es dir erst, dann prüf es, zum Beispiel mitprintln!("{:?}", nachbarn(0));am Anfang vonmain. - Spickzettel: Lass in
feld_ansichtdie 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