Beginner Projects
Number Guessing Game
A number guessing game manages a stateful interactive loop where a player attempts to guess a secret target number within a bounded range (min..=max), receiving directional feedback (TooLow, TooHigh, Correct) while narrowing the valid search bounds and tracking attempt history.
Game State Machine & Feedback Loop:
[ Initialize `GameSession { secret, low: 1, high: 100, max_attempts: 7 }` ]
|
v
+---> [ Player Submits Guess Input (`&str`) ]
| |
| Parse `u32` & Validate Range (`low..=high`)
| |
| `guess.cmp(&secret)` (`Ordering`)
| ├── `Less` ---> Update `low = low.max(guess + 1)` (`TooLow`)
| ├── `Greater` ---> Update `high = high.min(guess - 1)`(`TooHigh`)
| └── `Equal` ---> Return `TurnOutcome::Won`!
+--- (Repeat while `attempts < max_attempts`)
Separating Pure Game Engine State from Terminal I/O
Designing the game around a pure GameSession struct (separate from stdin().read_line()) makes every transition—out-of-range guesses, remaining attempt limits, dynamic hint narrowing, and win/loss conditions—100% deterministic and unit-testable:
| Component | Type | Responsibility |
|---|---|---|
GuessError |
Enum | Reports non-numeric input (InvalidNumber) or guesses outside [min..=max] (OutOfBounds) without consuming an attempt! |
TurnOutcome |
Enum | Reports TooLow, TooHigh, Won { attempts }, or Lost { secret }. |
GameSession |
Struct | Tracks secret, dynamic narrowed hint window [hint_low..=hint_high], max_attempts, and history: Vec<u32>. |
Complete Runnable Implementation
use std::cmp::Ordering;
#[derive(Debug, PartialEq, Eq)]
pub enum GuessError {
EmptyInput,
InvalidNumber(String),
OutOfBounds { guess: u32, min: u32, max: u32 },
}
#[derive(Debug, PartialEq, Eq)]
pub enum TurnOutcome {
TooLow {
remaining_attempts: usize,
hint_range: (u32, u32),
},
TooHigh {
remaining_attempts: usize,
hint_range: (u32, u32),
},
Won {
attempts_used: usize,
},
Lost {
secret: u32,
},
}
pub struct GameSession {
secret: u32,
min: u32,
max: u32,
hint_low: u32,
hint_high: u32,
max_attempts: usize,
history: Vec<u32>,
finished: bool,
}
impl GameSession {
pub fn new(min: u32, max: u32, secret: u32, max_attempts: usize) -> Self {
assert!(min <= secret && secret <= max, "secret must be within min..=max");
assert!(max_attempts > 0, "max_attempts must be > 0");
Self {
secret,
min,
max,
hint_low: min,
hint_high: max,
max_attempts,
history: Vec::with_capacity(max_attempts),
finished: false,
}
}
pub fn submit_str(&mut self, raw: &str) -> Result<TurnOutcome, GuessError> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err(GuessError::EmptyInput);
}
let guess = trimmed
.parse::<u32>()
.map_err(|_| GuessError::InvalidNumber(trimmed.to_string()))?;
if !(self.min..=self.max).contains(&guess) {
return Err(GuessError::OutOfBounds {
guess,
min: self.min,
max: self.max,
});
}
self.history.push(guess);
let attempts_used = self.history.len();
let remaining = self.max_attempts.saturating_sub(attempts_used);
match guess.cmp(&self.secret) {
Ordering::Equal => {
self.finished = true;
Ok(TurnOutcome::Won { attempts_used })
}
Ordering::Less => {
if remaining == 0 {
self.finished = true;
Ok(TurnOutcome::Lost { secret: self.secret })
} else {
self.hint_low = self.hint_low.max(guess + 1);
Ok(TurnOutcome::TooLow {
remaining_attempts: remaining,
hint_range: (self.hint_low, self.hint_high),
})
}
}
Ordering::Greater => {
if remaining == 0 {
self.finished = true;
Ok(TurnOutcome::Lost { secret: self.secret })
} else {
self.hint_high = self.hint_high.min(guess - 1);
Ok(TurnOutcome::TooHigh {
remaining_attempts: remaining,
hint_range: (self.hint_low, self.hint_high),
})
}
}
}
}
pub fn history(&self) -> &[u32] {
&self.history
}
}
fn main() {
let mut game = GameSession::new(1, 100, 42, 5);
// 1. Invalid or out-of-range inputs return Err without wasting an attempt:
assert_eq!(
game.submit_str("abc"),
Err(GuessError::InvalidNumber("abc".to_string()))
);
assert_eq!(
game.submit_str("150"),
Err(GuessError::OutOfBounds {
guess: 150,
min: 1,
max: 100
})
);
assert_eq!(game.history().len(), 0);
// 2. Binary-search style gameplay narrowing hint_range:
let r1 = game.submit_str("50").unwrap();
println!("Guess 50 -> {r1:?}");
let r2 = game.submit_str("25").unwrap();
println!("Guess 25 -> {r2:?}");
let r3 = game.submit_str("42").unwrap();
println!("Guess 42 -> {r3:?}");
println!("Guess history: {:?}", game.history());
}
Guess 50 -> TooHigh { remaining_attempts: 4, hint_range: (1, 49) }
Guess 25 -> TooLow { remaining_attempts: 3, hint_range: (26, 49) }
Guess 42 -> Won { attempts_used: 3 }
Guess history: [50, 25, 42]