rock_paper_scissors/lib.rs
1//! # Rock-Paper-Scissors Game Library
2//!
3//! Welcome to the **Rock-Paper-Scissors** game library, a simple and flexible Rust-based crate for implementing the classic "Rock, Paper, Scissors" game.
4//! This crate is designed to simplify the development of both console-based and programmatic versions of the game, featuring customizable rules,
5//! user input handling, game settings, and score tracking utilities.
6//!
7//! ## Key Features
8//!
9//! - **Move Management**: Enum-based moves (`Rock`, `Paper`, `Scissors`) for safe and clear gameplay logic.
10//! - **Game Logic**: Easily determine winners for rounds and track scores across an entire game session.
11//! - **Settings and Customization**: Includes prebuilt and customizable configurations, such as "first to X wins".
12//! - **Error Handling**: Built-in validations to ensure valid moves and game states.
13//! - **Randomization**: Utilities for generating random enemy moves.
14//!
15//! ## Core Components
16//!
17//! 1. **MoveType Enum**
18//! Represents the possible game moves: `Rock`, `Paper`, or `Scissors`. Includes utilities for generating randomized moves and representing moves as strings.
19//!
20//! 2. **Winner Enum**
21//! Encapsulates the result of each round, allowing easily distinguishable states: `User`, `Enemy`, or `Tie`.
22//!
23//! 3. **PlayerMoves Struct**
24//! Captures the moves made by the user and the opponent in a single round. Provides functionality to determine the winner of that round.
25//!
26//! 4. **Scores Struct**
27//! Tracks the cumulative scores of a session and provides methods to check if there's an overall winner based on predefined game settings.
28//!
29//! 5. **GameSettings Struct**
30//! Offers customizable configurations for game-winning conditions, such as "first to 3 wins" or other scenarios.
31//!
32//! ## Example Usage
33//! Create and run a short game loop of "Rock, Paper, Scissors":
34//!
35//! ```no_run
36//! use rock_paper_scissors::{PlayerMoves, Scores, Winner, GameSettings};
37//!
38//! fn main() {
39//! let mut scores = Scores::new();
40//! let game_settings = GameSettings::from_first_to(3);
41//!
42//! while scores.check_for_winner(&game_settings).is_err() {
43//! let player_moves = PlayerMoves::build_from_input();
44//! let round_winner = player_moves.check_who_wins_round();
45//!
46//! match round_winner {
47//! Winner::User => scores.user_wins += 1,
48//! Winner::Enemy => scores.enemy_wins += 1,
49//! Winner::Tie => println!("It's a tie!"),
50//! }
51//!
52//! println!("Scores -> User: {}, Enemy: {}", scores.user_wins, scores.enemy_wins);
53//! }
54//!
55//! let game_winner = scores.check_for_winner(&game_settings).unwrap();
56//! println!("Game Winner: {}", game_winner.convert_to_string());
57//! }
58//! ```
59//!
60//! ## Crate Philosophy
61//! This library focuses on simplicity and flexibility, making it perfect for both beginner and intermediate Rust developers learning game development. It
62//! provides clear interfaces and utilities while ensuring essential safeguards to avoid runtime errors.
63//!
64//! ## Contributing
65//! Contributions such as bug fixing, feature additions, and code improvements are welcome! Please read the [contribution guidelines](#) for more details.
66
67use rand::Rng;
68use std::io;
69
70/// # Winner enum
71///
72/// Represents the different results of a game round.
73///
74/// - `Winner::Tie`: Indicates a tie between the user and the enemy.
75/// - `Winner::User`: Indicates that the user has won the round.
76/// - `Winner::Enemy`: Indicates that the enemy has won the round.
77///
78/// # Examples
79///
80/// ```rust
81/// use rock_paper_scissors::Winner;
82///
83/// let winner = Winner::User;
84/// assert_eq!(winner.convert_to_string(), "User");
85/// ```
86#[derive(Debug, PartialEq)]
87pub enum Winner {
88 Tie,
89 User,
90 Enemy,
91}
92
93
94impl Winner {
95 /// Converts a `Winner` to a human-readable `String`.
96 ///
97 /// # Examples
98 ///
99 /// ```rust
100 /// use rock_paper_scissors::Winner;
101 ///
102 /// let winner = Winner::Enemy;
103 /// assert_eq!(winner.convert_to_string(), String::from("Enemy"));
104 /// ```
105 pub fn convert_to_string(&self) -> String {
106 match self {
107 Self::Tie => "Tie".to_string(),
108 Self::User => "User".to_string(),
109 Self::Enemy => "Enemy".to_string(),
110 }
111 }
112}
113
114/// # MoveType Enum
115///
116/// Represents the possible moves in the game, providing safety and clarity in game logic.
117/// Now includes a `None` variant for uninitialized or invalid move states.
118///
119/// ## Variants
120///
121/// - `MoveType::Rock`: The "Rock" move.
122/// - `MoveType::Paper`: The "Paper" move.
123/// - `MoveType::Scissors`: The "Scissors" move.
124/// - `MoveType::None`: A default state to handle uninitialized or invalid moves.
125///
126/// ## Key Features
127///
128/// - **Randomized Moves**: Generates a random move using `random_move()`.
129/// - **String Conversion**: Converts moves to a user-friendly `String` using `convert_to_string()`.
130/// - **Controlled User Input**: Converts valid input into a `MoveType` using `from_user_input()`.
131///
132/// ## Examples
133///
134/// ### Create a MoveType
135///
136/// ```rust
137/// use rock_paper_scissors::MoveType;
138///
139/// let move_type = MoveType::Rock;
140/// assert_eq!(move_type.convert_to_string(), "Rock");
141/// ```
142///
143/// ### Generate a Random Move
144///
145/// ```rust
146/// use rock_paper_scissors::MoveType;
147///
148/// let random_move = MoveType::random_move();
149/// assert!(matches!(random_move, MoveType::Rock | MoveType::Paper | MoveType::Scissors));
150/// ```
151///
152/// ### Handle Uninitialized Moves
153///
154/// The `None` variant is helpful when moves are not immediately set:
155///
156/// ```rust
157/// use rock_paper_scissors::MoveType;
158///
159/// let move_type = MoveType::None;
160/// assert_eq!(move_type.convert_to_string(), "None");
161/// ```
162///
163/// ### Convert User Input
164///
165/// ```no_run
166/// use rock_paper_scissors::MoveType;
167///
168/// println!("Enter your move: 1 (Rock), 2 (Paper), or 3 (Scissors)");
169/// let user_move = MoveType::from_user_input();
170///
171/// match user_move {
172/// Ok(move_type) => println!("You chose: {}", move_type.convert_to_string()),
173/// Err(err) => println!("Error: {}", err),
174/// }
175/// ```
176#[derive(Debug, PartialEq)]
177pub enum MoveType {
178 Rock,
179 Paper,
180 Scissors,
181 None,
182}
183
184impl MoveType {
185 /// Generates a random `MoveType` (one of `Rock`, `Paper`, or `Scissors`).
186 ///
187 /// # Examples
188 ///
189 /// ```rust
190 /// use rock_paper_scissors::MoveType;
191 ///
192 /// let random_move = MoveType::random_move();
193 /// assert!(matches!(random_move, MoveType::Rock | MoveType::Paper | MoveType::Scissors));
194 /// ```
195 pub fn random_move() -> MoveType {
196 let rand_num = rand::rng().random_range(1..=3);
197
198 match rand_num {
199 1 => MoveType::Rock,
200 2 => MoveType::Paper,
201 _ => MoveType::Scissors,
202 }
203 }
204
205 /// Converts the `MoveType` to a human-readable `String`.
206 ///
207 /// # Examples
208 ///
209 /// ```rust
210 /// use rock_paper_scissors::MoveType;
211 ///
212 /// let move_type = MoveType::Scissors;
213 /// assert_eq!(move_type.convert_to_string(), "Scissors");
214 /// ```
215 pub fn convert_to_string(&self) -> String {
216 match self {
217 Self::Rock => "Rock".to_string(),
218 Self::Paper => "Paper".to_string(),
219 Self::Scissors => "Scissors".to_string(),
220 Self::None => "None".to_string(),
221 }
222 }
223
224 /// # Gets the User's Move
225 ///
226 /// Handles user input from the console, validates it, and converts it into a `MoveType`.
227 ///
228 /// The user is prompted to enter a number corresponding to their move:
229 /// - `1` for `Rock`
230 /// - `2` for `Paper`
231 /// - `3` for `Scissors`
232 ///
233 /// The function will ensure valid input by repeatedly asking for input until a valid value is provided.
234 ///
235 /// # Examples
236 ///
237 /// ```no_run
238 /// use rock_paper_scissors::MoveType;
239 ///
240 /// println!("Enter your move: (1 = Rock, 2 = Paper, 3 = Scissors)");
241 /// let user_move = MoveType::from_user_input();
242 ///
243 /// assert!(matches!(user_move, Ok(MoveType::Rock) | Ok(MoveType::Paper) | Ok(MoveType::Scissors)));
244 /// ```
245 ///
246 /// # Behavior
247 ///
248 /// When the user provides invalid input (e.g., letters or numbers outside the valid range),
249 /// the function will re-prompt for valid input until it is received
250 pub fn from_user_input() -> Result<MoveType, String> {
251 println!("Enter your move: (1 = Rock, 2 = Paper, 3 = Scissors)");
252 let mut user_input = String::new();
253
254 io::stdin()
255 .read_line(&mut user_input)
256 .expect("Failed to read line");
257
258 match user_input.trim().parse::<u8>() {
259 Ok(1) => Ok(MoveType::Rock),
260 Ok(2) => Ok(MoveType::Paper),
261 Ok(3) => Ok(MoveType::Scissors),
262 _ => Err("Invalid input. Please enter 1, 2, or 3.".to_string()),
263 }
264 }
265}
266
267
268/// # PlayerMoves Struct
269///
270/// Represents the moves made by the user and the opponent in a single round of the game.
271///
272/// ## Fields
273///
274/// - `user_move`:
275/// - The move chosen by the user (of type `MoveType`).
276/// - Defaults to `MoveType::None` when uninitialized.
277/// - `enemy_move`:
278/// - The move chosen by the opponent (of type `MoveType`).
279/// - Defaults to `MoveType::None` when uninitialized.
280///
281/// ## Methods
282///
283/// - **`PlayerMoves::new()`**: Safely initializes a `PlayerMoves` instance with both moves set to `MoveType::None`.
284/// - **`PlayerMoves::build_from_input()`**: Builds a new `PlayerMoves` instance by getting the user's input and randomly generating the enemy's move.
285/// - **`PlayerMoves::check_who_wins_round()`**: Determines the winner of the round based on the moves.
286///
287/// ## Examples
288///
289/// ### Initialize PlayerMoves with Default Values
290/// ```rust
291/// use rock_paper_scissors::{PlayerMoves, MoveType};
292///
293/// let moves = PlayerMoves::new();
294/// assert_eq!(moves.user_move, MoveType::None);
295/// assert_eq!(moves.enemy_move, MoveType::None);
296/// ```
297///
298/// ### Create and Compare Moves
299/// ```rust
300/// use rock_paper_scissors::{PlayerMoves, MoveType, Winner};
301///
302/// let moves = PlayerMoves {
303/// user_move: MoveType::Rock,
304/// enemy_move: MoveType::Scissors,
305/// };
306///
307/// assert_eq!(moves.check_who_wins_round(), Winner::User);
308/// ```
309#[derive(Debug, PartialEq)]
310pub struct PlayerMoves {
311 pub user_move: MoveType,
312 pub enemy_move: MoveType,
313}
314
315impl PlayerMoves {
316
317 /// Creates a new `PlayerMoves` instance with both the `user_move` and `enemy_move` set to `MoveType::None`.
318 ///
319 /// This provides a safe starting state where moves can be explicitly set at a later stage.
320 /// It is particularly useful for initializing structures in cases where moves are assigned dynamically (e.g., during gameplay).
321 ///
322 /// ## Examples
323 ///
324 /// ### Default Initialization
325 /// ```rust
326 /// use rock_paper_scissors::{PlayerMoves, MoveType};
327 ///
328 /// let moves = PlayerMoves::new();
329 /// assert_eq!(moves.user_move, MoveType::None);
330 /// assert_eq!(moves.enemy_move, MoveType::None);
331 /// ```
332 pub fn new() -> PlayerMoves {
333 PlayerMoves {
334 user_move: MoveType::None,
335 enemy_move: MoveType::None,
336 }
337 }
338
339 /// Builds a new `PlayerMoves` instance with a random enemy move and the user's move provided via input.
340 ///
341 /// # Warning
342 ///
343 /// Requires `build_from_input` to be used for user input.
344 ///
345 /// # Examples
346 ///
347 /// ```rust
348 /// use rock_paper_scissors::{PlayerMoves, MoveType};
349 ///
350 /// // let player_moves = PlayerMoves::build_from_input();
351 ///
352 /// // the build_from_input function takes user input, so this is an example output.
353 ///
354 /// let player_moves = PlayerMoves {
355 /// user_move: MoveType::Rock,
356 /// enemy_move: MoveType::Scissors,
357 /// };
358 ///
359 /// assert_eq!(player_moves.user_move, MoveType::Rock);
360 /// assert_eq!(player_moves.enemy_move, MoveType::Scissors);
361 /// ```
362 pub fn build_from_input() -> PlayerMoves {
363 let user_move = loop {
364 match MoveType::from_user_input() {
365 Ok(m) => break m,
366 Err(e) => println!("{}", e),
367 }
368 };
369
370 PlayerMoves {
371 user_move,
372 enemy_move: MoveType::random_move(),
373 }
374 }
375
376 /// Determines the winner of the round based on the user's and enemy's moves.
377 ///
378 /// # Examples
379 ///
380 /// ```rust
381 /// use rock_paper_scissors::{PlayerMoves, MoveType, Winner};
382 ///
383 /// let player_moves = PlayerMoves {
384 /// user_move: MoveType::Paper,
385 /// enemy_move: MoveType::Rock,
386 /// };
387 ///
388 /// assert_eq!(player_moves.check_who_wins_round(), Winner::User);
389 /// ```
390 pub fn check_who_wins_round(&self) -> Winner {
391 match (&self.user_move, &self.enemy_move) {
392 (MoveType::Rock, MoveType::Rock) | (MoveType::Paper, MoveType::Paper) | (MoveType::Scissors, MoveType::Scissors) => Winner::Tie,
393 (MoveType::Rock, MoveType::Scissors) | (MoveType::Paper, MoveType::Rock) | (MoveType::Scissors, MoveType::Paper) => Winner::User,
394 _ => Winner::Enemy,
395 }
396 }
397}
398
399/// # Scores struct
400///
401/// Represents the current scores for both the user and the enemy in a game session.
402///
403/// - `user_wins`: Number of rounds won by the user.
404/// - `enemy_wins`: Number of rounds won by the enemy.
405///
406/// # Examples
407///
408/// ```rust
409/// use rock_paper_scissors::Scores;
410///
411/// let mut scores = Scores::new();
412/// scores.user_wins += 1;
413/// assert_eq!(scores.user_wins, 1);
414/// assert_eq!(scores.enemy_wins, 0);
415/// ```
416#[derive(Debug, PartialEq)]
417pub struct Scores {
418 pub user_wins: u8,
419 pub enemy_wins: u8,
420}
421
422impl Scores {
423 /// Creates a new `Scores` instance with zero scores.
424 ///
425 /// # Examples
426 ///
427 /// ```rust
428 /// use rock_paper_scissors::Scores;
429 ///
430 /// let scores = Scores::new();
431 /// assert_eq!(scores.user_wins, 0);
432 /// assert_eq!(scores.enemy_wins, 0);
433 /// ```
434 pub fn new() -> Scores {
435 Scores {
436 user_wins: 0,
437 enemy_wins: 0,
438 }
439 }
440
441 /// Checks if the game has a winner (first to however many wins).
442 ///
443 /// If either the user or the enemy has a certain number of specified wins, returns the winner as `Ok(Winner)`. Otherwise, returns an `Err` type.a
444 ///
445 /// # Examples
446 ///
447 /// ```rust
448 /// use rock_paper_scissors::{Scores, Winner, GameSettings};
449 ///
450 /// let game_settings: GameSettings = GameSettings::from_first_to(3);
451 ///
452 /// let scores = Scores {
453 /// user_wins: 3,
454 /// enemy_wins: 2,
455 /// };
456 ///
457 /// assert_eq!(scores.check_for_winner(&game_settings), Ok(Winner::User));
458 /// ```
459 pub fn check_for_winner(&self, game_settings: &GameSettings) -> Result<Winner, &str> {
460 if self.user_wins == game_settings.first_to {
461 Ok(Winner::User)
462 } else if self.enemy_wins == game_settings.first_to {
463 Ok(Winner::Enemy)
464 } else {
465 Err("rock-paper-scissors: err: No winner yet")
466 }
467 }
468
469 /// Resets the scores to zero.
470 ///
471 /// # Examples
472 ///
473 /// ```rust
474 /// use rock_paper_scissors::Scores;
475 ///
476 /// let mut scores = Scores {
477 /// user_wins: 2,
478 /// enemy_wins: 3,
479 /// };
480 ///
481 /// scores.reset();
482 /// assert_eq!(scores.user_wins, 0);
483 /// assert_eq!(scores.enemy_wins, 0);
484 /// ```
485 #[allow(dead_code)]
486 pub fn reset(&mut self) {
487 self.user_wins = 0;
488 self.enemy_wins = 0;
489 }
490}
491
492/// # GameSettings Struct
493///
494/// The `GameSettings` struct provides a simple yet flexible mechanism to configure the win conditions for a "Rock, Paper, Scissors" game session.
495/// It allows developers to define the number of wins required to declare an overall game winner.
496///
497/// ## Fields
498///
499/// - `from_first_to`
500/// - Specifies the number of round wins required for either the user or opponent to win the game.
501/// - This value defaults to `0` when initializing using `GameSettings::new()`.
502///
503/// ## Methods
504///
505/// ### `GameSettings::new()`
506/// Creates a new `GameSettings` instance with `from_first_to` set to `0`. This can act as a placeholder until specific settings are defined.
507///
508/// ```rust
509/// use rock_paper_scissors::GameSettings;
510///
511/// let game_settings = GameSettings::new();
512/// assert_eq!(game_settings.first_to, 1);
513/// ```
514///
515/// ### `GameSettings::first_to_3()`
516/// Provides a predefined configuration where the game is set to end after 3 wins from either the user or the opponent.
517///
518/// ```rust
519/// use rock_paper_scissors::GameSettings;
520///
521/// let game_settings = GameSettings::from_first_to(3);
522/// assert_eq!(game_settings.first_to, 3);
523/// ```
524///
525/// ## Examples
526///
527/// ### Using Custom Win Conditions
528/// Developers can define their own win conditions by directly instantiating the `GameSettings` struct:
529///
530/// ```rust
531/// use rock_paper_scissors::GameSettings;
532///
533/// let custom_game_settings = GameSettings {
534/// first_to: 5,
535/// };
536///
537/// assert_eq!(custom_game_settings.first_to, 5);
538/// ```
539///
540/// ### Combining with Scores
541/// The `GameSettings` struct is designed to work seamlessly with the `Scores` struct to determine if a game session has reached its end:
542///
543/// ```rust
544/// use rock_paper_scissors::{Scores, GameSettings, Winner};
545///
546/// let game_settings = GameSettings::from_first_to(3);
547/// let mut scores = Scores::new();
548///
549/// // Simulate some rounds
550/// scores.user_wins = 3;
551///
552/// // Check for game winner
553/// let winner = scores.check_for_winner(&game_settings);
554/// assert_eq!(winner, Ok(Winner::User));
555/// ```
556#[derive(Debug, PartialEq)]
557pub struct GameSettings {
558 pub first_to: u8,
559}
560
561impl GameSettings {
562 /// Creates a new game configuration with the default `from_first_to` value of `1`.
563 ///
564 /// ## Examples
565 /// ```rust
566 /// use rock_paper_scissors::GameSettings;
567 ///
568 /// let settings = GameSettings::new();
569 /// assert_eq!(settings.first_to, 1);
570 /// ```
571 pub fn new() -> GameSettings {
572 GameSettings { first_to: 1 }
573 }
574
575 /// The `from_user_input` method allows users to customize the game settings by
576 /// providing input for the `first_to` win condition. The number entered defines how many
577 /// victories are required to declare a winner in the game session.
578 ///
579 /// ## Description
580 ///
581 /// - This function prompts the user to input a number (representing the target win count).
582 /// - It validates the input to ensure it's a valid positive integer (`u8`).
583 /// - If the input is valid, it returns a `GameSettings` instance with the `first_to` property set
584 /// to the input value.
585 /// - In case of invalid input, such as non-numeric values or parsing errors, it returns an error
586 /// message.
587 ///
588 /// ## Behavior
589 ///
590 /// - Reads a line of input from the console.
591 /// - Tries to parse the trimmed input into a `u8` number.
592 /// - If parsing succeeds:
593 /// - The parsed number is assigned to the `first_to` field of the `GameSettings` struct.
594 /// - Returns `Ok(GameSettings)`.
595 /// - If parsing fails:
596 /// - Returns an error with a descriptive message (e.g., `"Invalid input. Please enter a number."`).
597 ///
598 /// ## Use Case
599 ///
600 /// This method is designed to make game initialization interactive by letting users define the win
601 /// condition directly from the console. For example, users can adjust how many game rounds they
602 /// need to win to end the game.
603 ///
604 /// ## Examples
605 ///
606 /// ### Correct Input
607 ///
608 /// When valid input is provided:
609 /// ```rust
610 /// use rock_paper_scissors::GameSettings;
611 ///
612 /// // Simulating valid user input:
613 /// // Let's say user enters "5" (first to 5 wins).
614 /// // let game_settings = GameSettings::from_user_input();
615 /// // returns the following:
616 /// let game_settings: Result<GameSettings, &'static str> = Ok(GameSettings {
617 /// first_to: 5,
618 /// });
619 /// match game_settings {
620 /// Ok(settings) => assert_eq!(settings.first_to, 5),
621 /// Err(_) => panic!("This should not happen for valid input"),
622 /// }
623 /// ```
624 ///
625 /// ### Invalid Input
626 ///
627 /// Example of invalid
628 pub fn from_user_input() -> Result<GameSettings, &'static str> {
629 let mut game_settings = GameSettings::new();
630 let mut user_input = String::new();
631
632 io::stdin()
633 .read_line(&mut user_input)
634 .expect("Failed to read line");
635
636 match user_input.trim().parse::<u8>() { // Result<u8, _>
637 Ok(first_to) => {
638 game_settings.first_to = first_to;
639 Ok(game_settings)
640 },
641 Err(_) => Err("Invalid input. Please enter a number.")
642 }
643 }
644
645 /// Prebuilt configuration where the first player to win 3 rounds is declared the winner.
646 ///
647 /// ## Examples
648 /// ```rust
649 /// use rock_paper_scissors::GameSettings;
650 ///
651 /// let settings = GameSettings::from_first_to(3);
652 /// assert_eq!(settings.first_to, 3);
653 /// ```
654 pub fn from_first_to(first_to: u8) -> GameSettings {
655 GameSettings {
656 first_to
657 }
658 }
659}