পাঠ ২

একটি Guessing Game বানানো

Programming a Guessing Game

এই অধ্যায়ে আমরা একটা ছোট কিন্তু সম্পূর্ণ Rust program বানাব — একটা guessing game। Program ১ থেকে ১০০-এর মধ্যে একটা random number বেছে নেবে, তারপর user-কে guess করতে বলবে। guess বড় না ছোট না সঠিক — সেটা জানিয়ে দেবে। এই একটা example-এ let, match, function, external crate, error handling — অনেক কিছু একসাথে দেখব। পরের অধ্যায়গুলোতে এক একটা concept আলাদা করে গভীরে যাব।

Project setup

আগের অধ্যায়ে শেখা cargo new দিয়ে নতুন project শুরু করো:

terminalbash
$ cargo new guessing_game
$ cd guessing_game

Cargo যা তৈরি করেছে — Cargo.toml এবং src/main.rs:

Cargo.tomltoml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"

[dependencies]
src/main.rsrust
fn main() {
    println!("Hello, world!");
}

cargo run চালিয়ে দেখো — "Hello, world!" print হচ্ছে কিনা। এই অধ্যায়ের সব code আমরা src/main.rs-এ লিখব, ধাপে ধাপে।

User-এর input নেওয়া

প্রথমে user-কে একটা guess type করতে দিতে হবে এবং সেটা read করতে হবে। নিচের code পুরোটা src/main.rs-এ replace করো:

src/main.rsrust
use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

এই কয়েকটা line-এ অনেক কিছু চলছে। ভেঙে দেখি।

use std::io;

User input নিতে এবং print করতে standard library-এর io module দরকার। use std::io; দিয়ে এটাকে scope-এ এনে আনলাম।

Rust default-এ একটা ছোট set — prelude — সব program-এ auto-import করে। এর বাইরে কিছু লাগলে use statement দিয়ে explicitly আনতে হয়।std::io prelude-এ নেই, তাই import করতে হলো।

Variable দিয়ে value রাখা

User-এর input রাখার জন্য একটা variable লাগবে:

let mut guess = String::new();

let দিয়ে variable বানাই। Rust-এ variable default-এ immutable — মান একবার দিলে বদলানো যায় না। মান বদলাতে চাইলে mut keyword যোগ করতে হয়। এখানে user input read করার সময় string-এ append হবে, তাই mut দরকার।

String::new() একটা নতুন, খালি String instance return করে। String হলো standard library-র UTF-8 encoded, growable text type। :: দিয়ে বোঝানো হচ্ছে new হলো String type-এর একটা associated function — type-এর সাথে যুক্ত function (অন্য language-এ "static method" নামে চেনা যায়)। অনেক type-এই new নামে একটা constructor-জাতীয় function থাকে।

stdin().read_line() এবং reference

io::stdin()
    .read_line(&mut guess)
    .expect("Failed to read line");

io::stdin() standard input-এর একটা handle return করে। তার উপর .read_line() call করলে user-এর type করা টেক্সট আমাদের string-এ যোগ হয়।

লক্ষ্য করো — &mut guess। এখানে & মানে এটা একটা reference — আমরা guess-এর data copy না করেই অন্য জায়গাকে সেটা read/write করার অনুমতি দিচ্ছি। Reference-ও variable-এর মতো default-এ immutable, তাই mutate করতে চাইলে &mut লিখতে হয়। Reference Rust-এর সবচেয়ে গুরুত্বপূর্ণ feature-গুলোর একটা — Chapter 4-এ বিস্তারিত আছে।

Result দিয়ে error handle করা

.read_line() শুধু string update করে না — এটা একটা Result value-ও return করে। Result একটা enum (chapter ৬-এ বিস্তারিত), যার দুটো variant: Ok (সফল) ও Err (failed)।

.expect("...") call করলে — value Err হলে program crash করবে এবং আমাদের দেওয়া message print করবে; Ok হলে ভিতরের value return করবে। এখানে আমরা শুধু crash করাচ্ছি; recover করার পদ্ধতি chapter ৯-এ।

.expect() না দিলে compile তো হবে, কিন্তু warning দেবে যে Result ignore করা হচ্ছে।

println! placeholder

println!("You guessed: {guess}");

{guess} হলো একটা placeholder — variable-এর নাম সরাসরি curly bracket-এর মধ্যে দিলেই value print হয়। Expression print করতে চাইলে empty {} ব্যবহার করে comma-separated list দিতে হয়:

let x = 5;
let y = 10;

println!("x = {x} and y + 2 = {}", y + 2);
// Output: x = 5 and y + 2 = 12

প্রথম পরীক্ষা

terminalbash
$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.44s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6

এ পর্যন্ত — keyboard থেকে input নিয়ে print করা হলো।

Random secret number generate করা

এখন একটা secret number লাগবে — প্রতিবার play করলে আলাদা হবে। Rust-এর standard library-তে random number generation নেই। কিন্তু rand নামে একটা official crate আছে।

Cargo.toml-এ dependency যোগ

Cargo.toml খুলে [dependencies] heading-এর নিচে এই line যোগ করো:

Cargo.tomltoml
[dependencies]
rand = "0.8.5"

0.8.5 হলো version specifier। আসলে এটা ^0.8.5-এর shorthand — মানে "0.8.5 বা তার চেয়ে নতুন, কিন্তু 0.9.0-এর নিচে"। এটা Semantic Versioning (SemVer) অনুসরণ করে — 0.8.x version-গুলো API-compatible ধরা হয়।

এখন cargo build চালাও:

terminalbash
$ cargo build
  Updating crates.io index
   Locking 15 packages to latest Rust 1.85.0 compatible versions
    Adding rand v0.8.5 (available: v0.9.0)
 Compiling proc-macro2 v1.0.93
 Compiling unicode-ident v1.0.17
 ... (অনেক crate compile হচ্ছে)
 Compiling rand v0.8.5
 Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
  Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.48s

Cargo crates.io registry থেকে rand এবং তার dependencies download করেছে, তারপর সব compile করেছে। দ্বিতীয়বার cargo build চালালে — কোনো change নেই বলে — কিছুই recompile হবে না।

Cargo.lock — reproducible build

প্রথমবার build করার পর Cargo.lock file তৈরি হয়েছে। এটা দেখায় ঠিক কোন version-এর কোন crate use করা হয়েছে। পরের build-এ Cargo এই file-ই follow করে — যাতে নতুন version এসেও তোমার build break না হয়।

Update করতে চাইলে cargo update:

terminalbash
$ cargo update
    Updating crates.io index
     Locking 1 package to latest Rust 1.85.0 compatible version
    Updating rand v0.8.5 -> v0.8.6 (available: v0.999.0)

লক্ষ্য করো — 0.999.0 available থাকলেও Cargo নেবে না, কারণ আমাদের specifier 0.9.0-এর নিচে সীমাবদ্ধ। বড় version upgrade করতে চাইলে Cargo.toml-এ specifier বদলাতে হবে।

Cargo.lock source control-এ commit করতে হয় — এটা team-এর সবার build consistent রাখে।

Random number generate করার code

src/main.rsrust
use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

use rand::Rng;Rng একটা trait (chapter ১০-এ আসবে)। Trait হলো method-এর set; method ব্যবহার করতে হলে trait scope-এ থাকতে হবে।

rand::thread_rng() current thread-এর জন্য একটা random number generator return করে — operating system এটাকে seed করে। তার উপর .gen_range(1..=100) call করলে ১ থেকে ১০০ (inclusive) এর মধ্যে একটা random number আসে। 1..=100 হলো range expression — ..= মানে দু'প্রান্তই inclusive।

কোন crate-এর কোন trait/method use করতে হবে — সেটা আগে থেকে জানার দরকার নেই। cargo doc --open চালালে browser-এ তোমার সব dependency-এর local documentation খুলবে।

এই stage-এ secret number print করছি — শুধু testing-এর জন্য। আসল game-এ এটা থাকবে না।

Guess-এর সাথে compare করা

এখন guess-কে secret_number-এর সাথে compare করতে হবে:

src/main.rsrust
use std::cmp::Ordering;
use std::io;

use rand::Rng;

fn main() {
    // ... আগের code ...

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

std::cmp::Ordering আরেকটা enum — variant: Less, Greater, Equal। এই তিনটাই দুটো value compare-এর সম্ভাব্য outcome।

.cmp() method দুটো value তুলনা করে এবং একটা Ordering variant return করে।

match expression-এ pattern অনুযায়ী কোড চালানো হয়। প্রতিটা arm-এ একটা pattern এবং সেই pattern match করলে চালানোর কোড। Rust top থেকে নিচে check করে — প্রথম যে arm match করে, সেটার code চালায়। উদাহরণ — guess 50, secret 38: cmp Greater return করে, প্রথম arm Less match হবে না, দ্বিতীয় arm Greater match — "Too big!" print হবে।

Type mismatch error

এই code এখনই cargo build করলে error আসবে:

compile errortext
error[E0308]: mismatched types
  --> src/main.rs:23:21
   |
23 |     match guess.cmp(&secret_number) {
   |                 --- ^^^^^^^^^^^^^^ expected `&String`, found `&{integer}`
   |                 |
   |                 arguments to this method are incorrect

কারণ — guess হলো String, আর secret_number একটা integer। Rust string ও integer-কে compare করতে দেবে না।

secret_number-এর type explicit বলিনি — Rust default-এ i32 ধরে নিয়েছে। আমরা এটাকে u32 (unsigned 32-bit integer) করতে চাই, এবং তার জন্য guess-কে string থেকে number-এ convert করতে হবে।

Shadowing দিয়ে type convert

read_line-এর ঠিক পরে এই line যোগ করো:

let guess: u32 = guess.trim().parse().expect("Please type a number!");

আমরা একটা নতুন variable বানাচ্ছি যার নাম-ও guess! এটাকে বলে shadowing — পুরোনো variable-কে নতুনটা ঢেকে দেয়। সাধারণত type convert করার সময় এটা use করা হয়, যাতে দুটো আলাদা নাম (যেমন guess_strguess) লাগে না।

.trim() string-এর শুরু/শেষ থেকে whitespace (newline, space) সরায় — user enter চাপলে যে \\n যোগ হয়, সেটা আগে clean করতে হয়।

.parse() string-কে অন্য type-এ convert করে। let guess: u32 দিয়ে আমরা type annotate করেছি, তাই Rust জানে কোন type-এ parse করতে হবে।

parse() ও একটা Result return করে — input invalid হলে fail করতে পারে। .expect() দিয়ে আপাতত fail হলে crash করছি।

এখন cargo run চালিয়ে দেখো:

terminalbash
$ cargo run
   Compiling guessing_game v0.1.0
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
  76
You guessed: 76
Too big!

Loop দিয়ে multiple guess

User-কে একবার-এর বেশি guess করতে দিতে loop keyword use করি — infinite loop:

src/main.rsrust
loop {
    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    let guess: u32 = guess.trim().parse().expect("Please type a number!");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

User এখন বারবার guess করতে পারবে। কিন্তু game শেষ হবে না — সঠিক guess করলেও।

সঠিক guess-এ break

Ordering::Equal arm-এ break যোগ করি:

match guess.cmp(&secret_number) {
    Ordering::Less => println!("Too small!"),
    Ordering::Greater => println!("Too big!"),
    Ordering::Equal => {
        println!("You win!");
        break;
    }
}

break loop থেকে বেরিয়ে যায়। যেহেতু main-এ এটাই শেষ statement, loop শেষ হলে program-ও শেষ।

Invalid input handle করা

এখনো একটা সমস্যা — user যদি number না দিয়ে "foo" type করে, parse fail করে এবং expect program crash করায়। আমরা চাই — invalid হলে শুধু আবার জিজ্ঞেস করো।

expect-এর জায়গায় আরেকটা match:

let guess: u32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => continue,
};

parse fail করলে Err(_) arm match হবে — underscore _ মানে "যেকোনো error, value-এর কথা ভাবছি না"। continue loop-এর পরবর্তী iteration-এ চলে যায়।

parse success হলে Ok(num) match হবে, এবং num value আমাদের নতুন guess variable-এ bind হবে।

চূড়ান্ত code

সবশেষে — secret number print করার line-টা সরিয়ে দাও (game-এর জন্য spoiler!)। final program:

src/main.rsrust
use std::cmp::Ordering;
use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {guess}");

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

এই কয়েক লাইনে আমরা ব্যবহার করেছি — let, match, function, external crate, mutable reference, shadowing, error handling, loop, break, continue, enum (Result, Ordering), trait (Rng) — Rust-এর প্রায় সব core concept-এর ছোঁয়া।

এই অধ্যায় থেকে যা শিখলে

  • use দিয়ে module/trait scope-এ আনা।
  • let immutable, let mut mutable; shadowing দিয়ে নাম reuse করা।
  • String::new(), io::stdin().read_line(&mut s) দিয়ে input; reference (&, &mut) কী।
  • Result enum (Ok/Err); .expect() দিয়ে crash বা match দিয়ে handle।
  • Cargo.toml-এ external crate যোগ; Cargo.lock reproducible build নিশ্চিত করে।
  • rand::thread_rng().gen_range(1..=100) দিয়ে random number; ..= inclusive range।
  • Ordering enum, .cmp(), match দিয়ে comparison।
  • loop, break, continue দিয়ে control flow; {var} placeholder দিয়ে print।