Proyek: Game Tebak Angka dengan Rust
Ini proyek sungguhan pertamamu. Komputer memilih sebuah angka rahasia antara 1 dan 100. Kamu menebak. Komputer bilang “terlalu kecil” atau “terlalu besar” sampai tebakanmu benar, lalu memberi tahu berapa kali percobaan yang kamu butuhkan.
Programnya kecil, tapi memakai banyak hal yang sudah kamu pelajari: variabel, match, perulangan (loop), dan Result. Kita akan membangunnya selangkah demi selangkah, dan setiap langkah bisa dijalankan sendiri.
Apa yang akan kamu buat
Section titled “Apa yang akan kamu buat”Kalau sudah selesai, game di komputermu akan terlihat seperti ini:
Tebak angkanya! (1 sampai 100)Ketik tebakanmu:50Terlalu besar!Ketik tebakanmu:haloItu bukan bilangan bulat. Coba lagi!Ketik tebakanmu:25Terlalu kecil!Ketik tebakanmu:37Kamu berhasil dalam 3 percobaan!Perhatikan dua detail yang ramah. Saat pemain mengetik halo, game-nya tidak crash. Ia bertanya lagi. Dan input yang salah itu tidak dihitung sebagai percobaan.
Langkah 1: Buat proyeknya
Section titled “Langkah 1: Buat proyeknya”Buka terminal dan buat proyek Cargo baru:
cargo new guessing_gamecd guessing_gameSemua kode di halaman ini masuk ke src/main.rs. Jalankan kapan saja dengan:
cargo runKalau kamu belum menginstal Rust, baca Mulai dengan Rust dulu.
Langkah 2: Membaca tebakan dari keyboard
Section titled “Langkah 2: Membaca tebakan dari keyboard”Untuk membaca apa yang diketik pemain, pakai std::io::stdin() dan method read_line-nya:
use std::io;
fn main() { println!("Ketik tebakanmu:");
let mut guess = String::new(); io::stdin() .read_line(&mut guess) .expect("Gagal membaca baris");
println!("Tebakanmu: {guess}");}Program ini menunggu kamu mengetik sesuatu dan menekan Enter, jadi jalankan dengan cargo run di komputermu sendiri. (Rust Playground tidak punya input keyboard, karena itu tidak ada tombol “Coba sendiri” di sini.)
Ada beberapa hal yang terjadi:
String::new()membuat sebuah String kosong yang bisa bertambah panjang untuk menampung teksnya.read_line(&mut guess)menambahkan baris yang diketik ke ujungguess. Ia butuh&mutkarena mengubah string-nya. Baca Borrowing kalau&mutmasih baru buatmu.read_linemengembalikanResult, karena membaca input bisa gagal..expect(...)artinya “kalau ini gagal, hentikan program dengan pesan ini”. Untuk sekarang itu tidak masalah; nanti kita akan menanganinya dengan lebih halus.
Satu hal yang mengejutkan: teksnya ikut menyimpan tombol Enter. Kalau kamu mengetik 42, isi guess adalah "42\n". Ingat ini untuk langkah berikutnya.
Langkah 3: Mengubah teks menjadi angka
Section titled “Langkah 3: Mengubah teks menjadi angka”Pemain mengetik teks, padahal kita butuh angka yang bisa dibandingkan. .trim() menghapus spasi dan karakter baris baru di kedua ujung. Lalu .parse() mencoba mengubah teksnya menjadi angka.
Parsing bisa gagal. "tujuh" bukan angka. Jadi parse() mengembalikan Result, dan kita memakai match untuk menangani kedua kemungkinan:
fn main() { // Anggap saja pemain mengetik baris-baris ini (masing-masing diakhiri Enter). let inputs = ["42\n", " 7 \n", "tujuh\n", "-3\n"];
for input in inputs { match input.trim().parse::<u32>() { Ok(num) => println!("{:?} -> angka {num}", input), Err(e) => println!("{:?} -> bukan tebakan yang valid ({e})", input), } }}Output:
"42\n" -> angka 42" 7 \n" -> angka 7"tujuh\n" -> bukan tebakan yang valid (invalid digit found in string)"-3\n" -> bukan tebakan yang valid (invalid digit found in string)Pesan di dalam kurung berasal dari Rust sendiri, jadi tetap dalam bahasa Inggris: “invalid digit found in string” artinya “ada digit yang tidak valid di dalam string”. parse::<u32>() memberi tahu Rust tipe tujuan parsing-nya. u32 adalah bilangan bulat tanpa tanda (tidak pernah negatif), karena itu -3 ditolak. Memang itu yang kita mau: angka rahasianya selalu di antara 1 dan 100.
Di dalam game, kita menulis ide yang sama seperti ini:
let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => { println!("Itu bukan bilangan bulat. Coba lagi!"); continue; }};Di sini tipenya berasal dari let guess: u32, jadi kita tidak perlu ::<u32>. Kalau parsing berhasil, match mengembalikan angkanya. Kalau gagal, kita mencetak pesan yang ramah dan memakai continue untuk bertanya lagi. (continue hanya bisa dipakai di dalam loop, yang akan kita tambahkan di Langkah 5.)
Kenapa tidak pakai .expect() saja? Karena dengan begitu, satu salah ketik saja akan membuat seluruh game crash. Menangani kasus Err adalah cara membuat program yang nyaman dipakai.
Langkah 4: Membandingkan tebakan dengan angka rahasia
Section titled “Langkah 4: Membandingkan tebakan dengan angka rahasia”Setiap angka punya method .cmp(). Method ini membandingkan dua nilai dan mengembalikan std::cmp::Ordering, sebuah enum dengan tiga varian: Less (lebih kecil), Greater (lebih besar), dan Equal (sama). match padanya hampir terbaca seperti kalimat biasa:
use std::cmp::Ordering;
fn main() { let secret: u32 = 42;
for guess in [10, 42, 99] { match guess.cmp(&secret) { Ordering::Less => println!("{guess}: Terlalu kecil!"), Ordering::Greater => println!("{guess}: Terlalu besar!"), Ordering::Equal => println!("{guess}: Kamu menang!"), } }}Output:
10: Terlalu kecil!42: Kamu menang!99: Terlalu besar!Kamu bisa menulis hal yang sama dengan if guess < secret ... else if .... Versi match lebih enak karena compiler memeriksa bahwa kamu sudah menangani ketiga kasus. Lupa satu saja, kodenya tidak akan ter-compile.
Langkah 5: Terus bertanya sampai tebakannya benar
Section titled “Langkah 5: Terus bertanya sampai tebakannya benar”Sekarang kita bungkus semuanya dengan loop. Loop ini berjalan terus sampai kita keluar dengan break. Kita juga menyimpan penghitung, attempts, dan hanya menambahkan 1 kalau tebakannya benar-benar angka.
Di game sungguhan, input datang dari keyboard. Supaya ada versi yang bisa jalan di Playground, kita pakai angka rahasia tetap dan daftar baris “ketikan” yang tetap. inputs.next() memberi baris berikutnya, atau None kalau daftarnya sudah habis:
use std::cmp::Ordering;
fn main() { let secret: u32 = 37; // Anggap saja pemain mengetik ini, satu per baris. let mut inputs = ["50", "halo", "25", "37"].into_iter(); let mut attempts = 0;
println!("Tebak angkanya! (1 sampai 100)");
loop { let input = match inputs.next() { Some(line) => line, None => { println!("Tebakan habis. Angkanya adalah {secret}."); break; } }; println!("> {input}");
let guess: u32 = match input.trim().parse() { Ok(num) => num, Err(_) => { println!("Itu bukan bilangan bulat. Coba lagi!"); continue; } };
attempts += 1;
match guess.cmp(&secret) { Ordering::Less => println!("Terlalu kecil!"), Ordering::Greater => println!("Terlalu besar!"), Ordering::Equal => { println!("Kamu berhasil dalam {attempts} percobaan!"); break; } } }}Output:
Tebak angkanya! (1 sampai 100)> 50Terlalu besar!> haloItu bukan bilangan bulat. Coba lagi!> 25Terlalu kecil!> 37Kamu berhasil dalam 3 percobaan!Coba ganti angka rahasia atau daftar tebakannya, lalu jalankan lagi. Taruh jawaban yang benar di urutan pertama, atau hapus sama sekali, dan lihat apa yang terjadi.
Kasus None itu penting. Itulah versi Playground dari “pemain sudah tidak punya input lagi”, dan game sungguhan juga butuh ide yang sama (lihat Langkah 7).
Langkah 6: Memilih angka rahasia secara acak
Section titled “Langkah 6: Memilih angka rahasia secara acak”Game yang angka rahasianya selalu sama tidak terlalu seru. Di proyek sungguhan, kamu akan memakai crate rand (bahasa Inggris), pilihan standar untuk angka acak di Rust. Pelajaran Angka acak menunjukkan cara memakainya. Kamu menambahkannya dengan:
cargo add randUntuk proyek ini kita tetap memakai standard library saja, jadi kamu tidak perlu setup apa pun. Triknya: tanya jam komputer untuk waktu saat ini, ambil bagian nanodetiknya (angka dari 0 sampai 999.999.999 yang terus berubah), lalu “peras” ke rentang 1 sampai 100 dengan % (sisa bagi):
use std::time::{SystemTime, UNIX_EPOCH};
fn secret_number() -> u32 { let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("jam diatur ke sebelum 1970") .subsec_nanos(); nanos % 100 + 1}
fn main() { let secret = secret_number(); let in_range = (1..=100).contains(&secret); println!("Angka rahasia sudah dipilih. Apakah di antara 1 dan 100? {in_range}");}Output:
Angka rahasia sudah dipilih. Apakah di antara 1 dan 100? truenanos % 100 selalu bernilai 0 sampai 99, jadi setelah ditambah 1 hasilnya 1 sampai 100. Kita hanya mencetak apakah angkanya ada di dalam rentang, jadi output-nya selalu sama. Cetak secret langsung kalau kamu ingin mengintip.
Jujurlah pada dirimu sendiri soal apa ini sebenarnya: ini “cukup sulit ditebak untuk sebuah game”, bukan keacakan yang sesungguhnya. Jangan pernah memakai trik seperti ini untuk password, token keamanan, atau apa pun yang nilainya mungkin coba ditebak orang. Untuk hal-hal seperti itu, pakai crate yang tepat.
Langkah 7: Game lengkapnya
Section titled “Langkah 7: Game lengkapnya”Ini semuanya digabung. Versi inilah yang kamu taruh di src/main.rs dan mainkan dengan cargo run.
Ada satu hal baru: kita memakai match pada hasil read_line, bukan .expect(). read_line mengembalikan Ok(0) saat tidak ada input lagi, misalnya kalau pemain menekan Ctrl+D (di Windows: Ctrl+Z lalu Enter), atau kalau input dikirim lewat pipe dari sebuah file. Tanpa pengecekan itu, loop-nya akan berputar selamanya pada input kosong.
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("jam diatur ke sebelum 1970") .subsec_nanos(); nanos % 100 + 1}
fn main() { println!("Tebak angkanya! (1 sampai 100)");
let secret = secret_number(); let mut attempts = 0;
loop { println!("Ketik tebakanmu:");
let mut guess = String::new(); match io::stdin().read_line(&mut guess) { Ok(0) => { // Input sudah habis: tidak ada lagi yang bisa dibaca. println!("Input habis. Angkanya adalah {secret}. Sampai jumpa!"); break; } Ok(_) => {} Err(e) => { println!("Gagal membaca input-mu: {e}"); break; } }
let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => { println!("Itu bukan bilangan bulat. Coba lagi!"); continue; } };
attempts += 1;
match guess.cmp(&secret) { Ordering::Less => println!("Terlalu kecil!"), Ordering::Greater => println!("Terlalu besar!"), Ordering::Equal => { println!("Kamu berhasil dalam {attempts} percobaan!"); break; } } }}Kalau ingin mencobanya di browser, pakai versi Langkah 5, yang memainkan daftar tebakan tetap. 🦀
Kesalahan umum
Section titled “Kesalahan umum”Lupa menulis mut pada string-nya. read_line perlu mengubah string-nya, jadi string itu harus mutable (dapat diubah):
use std::io;
fn main() { let guess = String::new(); io::stdin().read_line(&mut guess).expect("Gagal membaca baris");}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("Gagal membaca baris"); | ^^^^^^^^^^ cannot borrow as mutable |help: consider changing this to be mutable |4 | let mut guess = String::new(); | +++Compiler bilang guess tidak bisa dipinjam sebagai mutable karena tidak dideklarasikan sebagai mutable. Solusinya persis seperti saran compiler: let mut guess = String::new();.
Membandingkan teks dengan angka. Kalau kamu lupa mem-parse, guess masih berupa String, dan kamu tidak bisa membandingkan String dengan u32:
use std::cmp::Ordering;
fn main() { let secret: u32 = 42; let guess = String::from("50");
match guess.cmp(&secret) { Ordering::Less => println!("Terlalu kecil!"), Ordering::Greater => println!("Terlalu besar!"), Ordering::Equal => println!("Kamu menang!"), }}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” artinya tipenya tidak cocok: method itu mengharapkan &String, tapi menerima &u32. Rust tidak pernah mengonversi antara teks dan angka secara otomatis. Parse dulu, baru bandingkan.
Lupa memanggil .trim(). Kode ini ter-compile, tapi setiap tebakan ditolak, karena karakter baris baru bukan digit:
fn main() { let typed = "42\n"; println!("{:?}", typed.parse::<u32>()); println!("{:?}", typed.trim().parse::<u32>());}Output:
Err(ParseIntError { kind: InvalidDigit })Ok(42)Ide untuk mengembangkannya
Section titled “Ide untuk mengembangkannya”Setelah game-mu berjalan, coba ide-ide ini sendiri. Masing-masing kecil saja:
- Periksa rentangnya. Kalau tebakannya 0 atau lebih dari 100, tulis “Pilih angka dari 1 sampai 100” dan jangan dihitung.
- Batasi jumlah percobaan. Beri pemain 7 kesempatan. Kalau habis, tunjukkan angka rahasianya dan akhiri game.
- Main lagi. Saat pemain menang, tanyakan “Main lagi? (y/n)” dan mulai ronde baru dengan angka rahasia baru.
- Simpan skor terbaik. Ingat jumlah percobaan paling sedikit dari semua ronde dan cetak di akhir.
- Beri petunjuk “hangat”. Tulis “sudah dekat sekali!” saat tebakan berjarak 3 atau kurang dari angka rahasia.
guess.abs_diff(secret)memberi jarak antara dua angka. - Pakai crate
randyang sebenarnya. Jalankancargo add randdan ikuti dokumentasinya di docs.rs (bahasa Inggris) untuk menggantisecret_number(). - Pindahkan logika ke fungsi. Tulis
fn check_guess(guess: u32, secret: u32) -> Orderingdan tambahkan test untuknya.
Ringkasan
Section titled “Ringkasan”io::stdin().read_line(&mut s)membaca satu baris, termasuk karakter baris baru, dan mengembalikanResult.Ok(0)berarti input sudah habis..trim().parse()mengubah teks menjadi angka. HasilnyaResult, jadi pakaimatchuntuk menangani input yang salah dengan ramah.a.cmp(&b)mengembalikanOrdering::Less,Greater, atauEqual, danmatchmemastikan kamu menangani ketiganya.loopdengancontinuedanbreakmembuat game terus berjalan sampai pemain menang.- Trik jam komputer cukup untuk game, tapi proyek sungguhan memakai crate
rand.
Kuis singkat
Section titled “Kuis singkat”Kuis singkat
1. Pemain mengetik 42 lalu menekan Enter. Apa isi string dari read_line?
2. Kenapa game ini memakai match pada parse(), bukan .expect()?
3. Apa artinya kalau read_line mengembalikan Ok(0)?
4. Nilai apa saja yang bisa dikembalikan guess.cmp(&secret)?
Skor: 0 / 4
Bacaan lebih lanjut
Section titled “Bacaan lebih lanjut”- Programming a Guessing Game — The Rust Book (bahasa Inggris): versi resmi proyek ini dari Rust Book. Memakai crate
randdan menjelaskan setiap baris dengan detail. Stdin::read_line— dokumentasi std (bahasa Inggris): kenapa teksnya ditambahkan keString-mu, kenapa karakter baris barunya ikut, dan kenapa ia mengembalikanOk(0)di akhir input.str::parse— dokumentasi std (bahasa Inggris): bagaimanaparsememilih tipe tujuan dari anotasimu, dan seperti apa error-nya kalau teksnya bukan angka.std::cmp::Ordering— dokumentasi std (bahasa Inggris): enumLess/Equal/Greateryang dikembalikancmp, plus fungsi bantu sepertiis_ltdanreverse.- Crate
rand— docs.rs (bahasa Inggris): cara yang benar untuk memilih angka acak, kalau kamu sudah siap menambahkan crate pertamamu.