Skip to main content

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}