Projekt: Ratespiel in Rust
Das ist dein erstes echtes Projekt. Der Computer denkt sich eine geheime Zahl zwischen 1 und 100 aus. Du rätst. Er sagt dir „zu klein“ oder „zu groß“, bis du richtig liegst. Dann verrät er dir, wie viele Versuche du gebraucht hast.
Das Programm ist klein, aber es nutzt vieles, was du bisher gelernt hast: Variablen, Match, Schleifen und Result. Wir bauen es Schritt für Schritt, und jeder Schritt läuft für sich allein.
Was du baust
Abschnitt betitelt „Was du baust“Wenn du fertig bist, sieht das Spiel auf deinem Computer so aus:
Errate die Zahl! (1 bis 100)Bitte gib deinen Tipp ein:50Zu groß!Bitte gib deinen Tipp ein:halloDas ist keine ganze Zahl. Versuch es noch mal!Bitte gib deinen Tipp ein:25Zu klein!Bitte gib deinen Tipp ein:37Richtig! Du hast 3 Versuche gebraucht.Achte auf zwei freundliche Details. Wenn jemand hallo eintippt, stürzt das Spiel nicht ab. Es fragt einfach noch mal. Und diese ungültige Eingabe zählt nicht als Versuch.
Schritt 1: Das Projekt anlegen
Abschnitt betitelt „Schritt 1: Das Projekt anlegen“Öffne ein Terminal und lege ein neues Cargo-Projekt an:
cargo new guessing_gamecd guessing_gameDer ganze Code auf dieser Seite kommt in src/main.rs. Starten kannst du ihn jederzeit mit:
cargo runFalls du Rust noch nicht installiert hast, schau zuerst bei Erste Schritte vorbei.
Schritt 2: Einen Tipp von der Tastatur lesen
Abschnitt betitelt „Schritt 2: Einen Tipp von der Tastatur lesen“Um zu lesen, was der Spieler eintippt, nimmst du std::io::stdin() und die Methode read_line:
use std::io;
fn main() { println!("Bitte gib deinen Tipp ein:");
let mut guess = String::new(); io::stdin() .read_line(&mut guess) .expect("Zeile konnte nicht gelesen werden");
println!("Dein Tipp: {guess}");}Dieses Programm wartet, bis du etwas eintippst und Enter drückst. Starte es also mit cargo run auf deinem eigenen Rechner. (Der Rust Playground hat keine Tastatureingabe. Deshalb gibt es hier keinen „Ausprobieren“-Button.)
Hier passiert einiges:
String::new()erzeugt einen leeren, wachsenden String, der den Text aufnimmt.read_line(&mut guess)hängt die eingetippte Zeile ans Ende vonguessan. Es braucht&mut, weil es den String verändert. Wenn&mutneu für dich ist, lies Borrowing (Ausleihen).read_linegibt einResultzurück, weil das Lesen der Eingabe fehlschlagen kann..expect(...)bedeutet: „Wenn das schiefgeht, beende das Programm mit dieser Meldung.“ Für den Moment reicht das. Später gehen wir sanfter damit um.
Eine Überraschung: Der Text enthält auch die Enter-Taste. Wenn du 42 eintippst, steht in guess der Wert "42\n". Merk dir das für den nächsten Schritt.
Schritt 3: Den Text in eine Zahl umwandeln
Abschnitt betitelt „Schritt 3: Den Text in eine Zahl umwandeln“Der Spieler hat Text eingegeben, wir wollen aber eine Zahl, die wir vergleichen können. .trim() entfernt Leerzeichen und den Zeilenumbruch an beiden Enden. Danach versucht .parse(), den Text in eine Zahl umzuwandeln.
Das kann schiefgehen. "sieben" ist keine Zahl. Darum gibt parse() ein Result zurück, und wir behandeln mit match beide Fälle:
fn main() { // Stell dir vor, der Spieler hat diese Zeilen eingetippt (jede endet mit Enter). let inputs = ["42\n", " 7 \n", "sieben\n", "-3\n"];
for input in inputs { match input.trim().parse::<u32>() { Ok(num) => println!("{:?} -> die Zahl {num}", input), Err(e) => println!("{:?} -> kein gültiger Tipp ({e})", input), } }}Ausgabe:
"42\n" -> die Zahl 42" 7 \n" -> die Zahl 7"sieben\n" -> kein gültiger Tipp (invalid digit found in string)"-3\n" -> kein gültiger Tipp (invalid digit found in string)Der Text in Klammern, invalid digit found in string („ungültige Ziffer im String gefunden“), ist die Fehlermeldung, die Rust selbst liefert. Deshalb ist sie auf Englisch.
parse::<u32>() sagt Rust, in welchen Typ es umwandeln soll. u32 ist eine vorzeichenlose (also nie negative) ganze Zahl. Darum wird -3 abgelehnt. Genau das wollen wir: Die geheime Zahl liegt immer zwischen 1 und 100.
Im Spiel schreiben wir dieselbe Idee so:
let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => { println!("Das ist keine ganze Zahl. Versuch es noch mal!"); continue; }};Hier kommt der Typ aus let guess: u32, deshalb brauchen wir kein ::<u32>. Klappt das Umwandeln, liefert match die Zahl zurück. Klappt es nicht, geben wir eine freundliche Meldung aus und fragen mit continue noch einmal. (continue funktioniert nur in einer Schleife. Die bauen wir in Schritt 5 ein.)
Warum nicht einfach .expect()? Weil dann schon ein Tippfehler das ganze Spiel abstürzen ließe. Wenn du den Err-Fall behandelst, wird dein Programm angenehm zu benutzen.
Schritt 4: Den Tipp mit der geheimen Zahl vergleichen
Abschnitt betitelt „Schritt 4: Den Tipp mit der geheimen Zahl vergleichen“Jede Zahl hat eine Methode .cmp(). Sie vergleicht zwei Werte und gibt ein std::cmp::Ordering zurück. Das ist ein Enum mit drei Varianten: Less (kleiner), Greater (größer) und Equal (gleich). Ein match darauf liest sich fast wie ein englischer Satz:
use std::cmp::Ordering;
fn main() { let secret: u32 = 42;
for guess in [10, 42, 99] { match guess.cmp(&secret) { Ordering::Less => println!("{guess}: Zu klein!"), Ordering::Greater => println!("{guess}: Zu groß!"), Ordering::Equal => println!("{guess}: Gewonnen!"), } }}Ausgabe:
10: Zu klein!42: Gewonnen!99: Zu groß!Du könntest dasselbe auch mit if guess < secret ... else if ... schreiben. Die match-Version hat einen Vorteil: Der Compiler prüft, ob du alle drei Fälle behandelt hast. Vergisst du einen, kompiliert das Programm nicht.
Schritt 5: Fragen, bis der Tipp stimmt
Abschnitt betitelt „Schritt 5: Fragen, bis der Tipp stimmt“Jetzt packen wir alles in eine loop-Schleife. Sie läuft endlos, bis wir sie mit break verlassen. Außerdem führen wir einen Zähler, attempts, und erhöhen ihn nur, wenn der Tipp eine echte Zahl war.
Im echten Spiel kommt die Eingabe von der Tastatur. Damit eine Version im Playground läuft, nehmen wir eine feste geheime Zahl und eine feste Liste von „eingetippten“ Zeilen. inputs.next() liefert die nächste Zeile oder None, wenn die Liste zu Ende ist:
use std::cmp::Ordering;
fn main() { let secret: u32 = 37; // Stell dir vor, der Spieler hat das eingetippt, eine Zeile pro Eintrag. let mut inputs = ["50", "hallo", "25", "37"].into_iter(); let mut attempts = 0;
println!("Errate die Zahl! (1 bis 100)");
loop { let input = match inputs.next() { Some(line) => line, None => { println!("Keine Tipps mehr. Die Zahl war {secret}."); break; } }; println!("> {input}");
let guess: u32 = match input.trim().parse() { Ok(num) => num, Err(_) => { println!("Das ist keine ganze Zahl. Versuch es noch mal!"); continue; } };
attempts += 1;
match guess.cmp(&secret) { Ordering::Less => println!("Zu klein!"), Ordering::Greater => println!("Zu groß!"), Ordering::Equal => { println!("Richtig! Du hast {attempts} Versuche gebraucht."); break; } } }}Ausgabe:
Errate die Zahl! (1 bis 100)> 50Zu groß!> halloDas ist keine ganze Zahl. Versuch es noch mal!> 25Zu klein!> 37Richtig! Du hast 3 Versuche gebraucht.Ändere ruhig die geheime Zahl oder die Liste der Tipps und starte das Programm noch mal. Setz die richtige Antwort an den Anfang oder lass sie ganz weg und schau, was passiert.
Der Fall None ist wichtig. Er ist die Playground-Version von „der Spieler hat keine Eingabe mehr“. Das echte Spiel braucht dieselbe Idee (siehe Schritt 7).
Schritt 6: Eine zufällige geheime Zahl wählen
Abschnitt betitelt „Schritt 6: Eine zufällige geheime Zahl wählen“Ein Spiel, bei dem die geheime Zahl immer gleich ist, macht nicht lange Spaß. In einem echten Projekt würdest du das rand-Crate nehmen (auf Englisch). Es ist die Standardwahl für Zufallszahlen in Rust. Wie du es benutzt, zeigt die Lektion Zufallszahlen. Du fügst es so hinzu:
cargo add randIn diesem Projekt bleiben wir bei der Standardbibliothek, damit du nichts einrichten musst. Der Trick: Wir fragen die Uhr nach der aktuellen Zeit, nehmen den Nanosekunden-Anteil (eine Zahl von 0 bis 999.999.999, die sich ständig ändert) und quetschen ihn mit % (Rest einer Division) in den Bereich 1 bis 100:
use std::time::{SystemTime, UNIX_EPOCH};
fn secret_number() -> u32 { let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("die Uhr steht vor dem Jahr 1970") .subsec_nanos(); nanos % 100 + 1}
fn main() { let secret = secret_number(); let in_range = (1..=100).contains(&secret); println!("Geheimzahl gewählt. Liegt sie zwischen 1 und 100? {in_range}");}Ausgabe:
Geheimzahl gewählt. Liegt sie zwischen 1 und 100? truenanos % 100 liegt immer zwischen 0 und 99. Mit + 1 landest du also bei 1 bis 100. Wir geben nur aus, ob die Zahl im Bereich liegt. So ist die Ausgabe jedes Mal gleich. Wenn du spicken willst, gib secret selbst aus.
Sei ehrlich zu dir, was das ist: „unvorhersehbar genug für ein Spiel“, aber kein echter Zufall. Nimm so einen Trick nie für Passwörter, Sicherheits-Tokens oder irgendetwas, das jemand erraten wollen könnte. Dafür gibt es richtige Crates.
Schritt 7: Das komplette Spiel
Abschnitt betitelt „Schritt 7: Das komplette Spiel“Hier ist alles zusammen. Diese Version kommt in src/main.rs, und mit cargo run kannst du spielen.
Eine Neuerung: Wir machen ein match auf das Ergebnis von read_line, statt .expect() aufzurufen. read_line gibt Ok(0) zurück, wenn keine Eingabe mehr kommt. Das passiert zum Beispiel, wenn der Spieler Strg+D drückt (unter Windows Strg+Z und dann Enter) oder wenn die Eingabe per Pipe aus einer Datei kommt. Ohne diese Prüfung würde die Schleife bei leerer Eingabe endlos weiterlaufen.
use std::cmp::Ordering;use std::io;use std::time::{SystemTime, UNIX_EPOCH};
fn secret_number() -> u32 { let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("die Uhr steht vor dem Jahr 1970") .subsec_nanos(); nanos % 100 + 1}
fn main() { println!("Errate die Zahl! (1 bis 100)");
let secret = secret_number(); let mut attempts = 0;
loop { println!("Bitte gib deinen Tipp ein:");
let mut guess = String::new(); match io::stdin().read_line(&mut guess) { Ok(0) => { // Ende der Eingabe: Es gibt nichts mehr zu lesen. println!("Keine Eingabe mehr. Die Zahl war {secret}. Tschüss!"); break; } Ok(_) => {} Err(e) => { println!("Deine Eingabe konnte nicht gelesen werden: {e}"); break; } }
let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => { println!("Das ist keine ganze Zahl. Versuch es noch mal!"); continue; } };
attempts += 1;
match guess.cmp(&secret) { Ordering::Less => println!("Zu klein!"), Ordering::Greater => println!("Zu groß!"), Ordering::Equal => { println!("Richtig! Du hast {attempts} Versuche gebraucht."); break; } } }}Wenn du es lieber im Browser ausprobieren willst, nimm die Version aus Schritt 5. Sie spielt eine feste Liste von Tipps durch. 🦀
Häufige Fehler
Abschnitt betitelt „Häufige Fehler“mut beim String vergessen. read_line muss den String verändern. Deshalb muss er veränderbar sein:
use std::io;
fn main() { let guess = String::new(); io::stdin().read_line(&mut guess).expect("Zeile konnte nicht gelesen werden");}error[E0596]: cannot borrow `guess` as mutable, as it is not declared as mutable --> src/main.rs:5:27 |5 | io::stdin().read_line(&mut guess).expect("Zeile konnte nicht gelesen werden"); | ^^^^^^^^^^ cannot borrow as mutable |help: consider changing this to be mutable |4 | let mut guess = String::new(); | +++Der Compiler sagt: „guess kann nicht veränderbar ausgeliehen werden, weil es nicht als veränderbar deklariert ist.“ Die Lösung ist genau sein Vorschlag: let mut guess = String::new();.
Text mit einer Zahl vergleichen. Wenn du das Umwandeln vergisst, ist guess immer noch ein String. Und einen String kannst du nicht mit einem u32 vergleichen:
use std::cmp::Ordering;
fn main() { let secret: u32 = 42; let guess = String::from("50");
match guess.cmp(&secret) { Ordering::Less => println!("Zu klein!"), Ordering::Greater => println!("Zu groß!"), Ordering::Equal => println!("Gewonnen!"), }}error[E0308]: mismatched types --> src/main.rs:7:21 |7 | match guess.cmp(&secret) { | --- ^^^^^^^ expected `&String`, found `&u32` | | | arguments to this method are incorrect„Mismatched types“ heißt „Typen passen nicht zusammen“: Erwartet wurde &String, gefunden hat der Compiler &u32. Rust wandelt nie von selbst zwischen Text und Zahlen um. Erst umwandeln, dann vergleichen.
.trim() vergessen. Das kompiliert zwar, aber jeder Tipp wird abgelehnt, weil der Zeilenumbruch keine Ziffer ist:
fn main() { let typed = "42\n"; println!("{:?}", typed.parse::<u32>()); println!("{:?}", typed.trim().parse::<u32>());}Ausgabe:
Err(ParseIntError { kind: InvalidDigit })Ok(42)Ideen zum Erweitern
Abschnitt betitelt „Ideen zum Erweitern“Wenn dein Spiel läuft, probier dich an diesen Ideen. Jede ist klein:
- Den Bereich prüfen. Ist der Tipp 0 oder größer als 100, gib „Bitte wähle eine Zahl von 1 bis 100“ aus und zähle ihn nicht mit.
- Die Versuche begrenzen. Gib dem Spieler 7 Versuche. Sind sie aufgebraucht, zeig die geheime Zahl und beende das Spiel.
- Noch mal spielen. Wenn der Spieler gewinnt, frag „Noch eine Runde? (j/n)“ und starte eine neue Runde mit einer neuen geheimen Zahl.
- Einen Rekord speichern. Merk dir die wenigsten Versuche über alle Runden und gib sie am Ende aus.
- Wärmere Hinweise geben. Sag „Ganz knapp!“, wenn der Tipp höchstens 3 von der geheimen Zahl entfernt ist.
guess.abs_diff(secret)liefert den Abstand zwischen zwei Zahlen. - Das echte
rand-Crate nehmen. Führecargo add randaus und ersetzesecret_number()mithilfe der Dokumentation auf docs.rs (auf Englisch). - Logik in Funktionen auslagern. Schreib
fn check_guess(guess: u32, secret: u32) -> Orderingund füge Tests dafür hinzu.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“io::stdin().read_line(&mut s)liest eine Zeile mitsamt Zeilenumbruch und gibt einResultzurück.Ok(0)bedeutet: Es kommt keine Eingabe mehr..trim().parse()wandelt Text in eine Zahl um. Es gibt einResultzurück. Behandle es mitmatch, damit ungültige Eingaben freundlich abgefangen werden.a.cmp(&b)gibtOrdering::Less,GreateroderEqualzurück, undmatchsorgt dafür, dass du alle drei Fälle behandelst.loopmitcontinueundbreakhält das Spiel am Laufen, bis der Spieler gewinnt.- Der Uhr-Trick reicht für ein Spiel. Echte Projekte nehmen das
rand-Crate.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Der Spieler tippt 42 und drückt Enter. Was steht in dem String von read_line?
2. Warum nutzt das Spiel match auf parse() statt .expect()?
3. Was bedeutet es, wenn read_line Ok(0) zurückgibt?
4. Welche Werte kann guess.cmp(&secret) zurückgeben?
Punkte: 0 / 4
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Ein Ratespiel programmieren – Das Rust-Buch: die Version dieses Projekts aus dem offiziellen Buch. Sie benutzt das Crate
randund erklärt jede Zeile ausführlich. Stdin::read_line– Doku der Standardbibliothek (auf Englisch): warum der Text an deinenStringangehängt wird, warum der Zeilenumbruch erhalten bleibt und warum am Ende der EingabeOk(0)zurückkommt.str::parse– Doku der Standardbibliothek (auf Englisch): wieparseden Zieltyp aus deiner Typangabe ableitet und wie der Fehler aussieht, wenn der Text keine Zahl ist.std::cmp::Ordering– Doku der Standardbibliothek (auf Englisch): das Enum mitLess/Equal/Greater, dascmpzurückgibt, plus Hilfsmethoden wieis_ltundreverse.- Das Crate
rand– docs.rs (auf Englisch): der richtige Weg zu Zufallszahlen, sobald du bereit für dein erstes Crate bist.