ascii_hangman_backend/
lib.rs

1//! This module provides the backend API for the game logic
2
3mod ascii_art;
4mod dictionary;
5pub mod game;
6mod image;
7mod secret;
8use crate::dictionary::ConfigParseError;
9use crate::dictionary::Dict;
10use crate::game::Game;
11use crate::game::State;
12use crate::image::Image;
13
14pub const VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
15pub const AUTHOR: &str = "(c) Jens Getreu, 2016-2021.";
16
17/// Title line.
18pub const TITLE: &str = "ASCII-Hangman for Kids\n";
19
20/// The ASCII-art image should not change too often, to keep the
21/// children focused on the words rather then on the image.
22/// The disclosing image should be seen as an additional motivation
23/// that changes only from time to time. This value determines how many
24/// games should start with the same image until it changes.
25pub const CHANGE_IMAGE_MAX: usize = 5;
26
27/// Number of wrong guess allowed.
28pub const LIVES: u8 = 7;
29/// Fallback sample configuration when the desktop application finds no configuration.
30/// This is also place holder and default when the web application starts.
31pub const CONF_TEMPLATE: &str = "# Add own secrets here; one per line.\r
32\r
33secrets:\r
34- guess me\r
35- _good l_uck\r
36- \"_der Hund:_| the dog\"\r
37- _3*_7_=21_\r
38\r
39\r
40# Uncomment 3 lines to use an optional custom image:\r
41\r
42#image: |1\r
43#   ::\r
44# C|__|\r
45";
46
47/// Configuration template. This short version is used in the web application.
48pub const CONF_TEMPLATE_SHORT: &str = "# Replace the sample secrets with your own; one per line.\r
49\r
50secrets:\r
51- guess me\r
52- _good l_uck\r
53- \"_der Hund:_| the dog\"\r
54- _3*_7_=21_\r
55";
56
57/// State of the application.
58#[derive(Debug)]
59pub struct Backend {
60    dict: Dict,
61    game: Game,
62    image: Image,
63    change_image: Option<usize>,
64}
65
66/// API to interact with all game logic. This is used by the desktop frontend
67/// in `main.rs` or by the web-app frontend in `lib.rs`.
68pub trait HangmanBackend {
69    /// Initialize the application with config data and start the first game.
70    fn new(config: &str) -> Result<Self, ConfigParseError>
71    where
72        Self: std::marker::Sized;
73
74    /// The user_input is a key stroke. The meaning depends on the game's state:
75    fn process_user_input(&mut self, inp: &str);
76
77    /// Renders the image. Make sure it is up to date with `self.image.update()`.
78    fn render_image(&self) -> String;
79
80    /// Forward the private image dimension
81    fn get_image_dimension(&self) -> (u8, u8);
82
83    /// Renders the partly hidden secret.
84    fn render_secret(&self) -> String;
85
86    /// Informs about some game statistics: lifes
87    fn render_game_lifes(&self) -> String;
88
89    /// Informs about some game statistics: last guess
90    fn render_game_last_guess(&self) -> String;
91
92    /// Tells the user what to do next.
93    fn render_instructions(&self) -> String;
94
95    /// Forwards the game's state
96    fn get_state(&self) -> State;
97}
98
99impl HangmanBackend for Backend {
100    fn new(config: &str) -> Result<Self, ConfigParseError> {
101        let mut dict = Dict::from(config)?;
102        // A dictionary guaranties to have least one secret.
103        let secret = dict.get_random_secret().unwrap();
104        let game = Game::new(&secret, LIVES, dict.is_empty());
105        // We assume, that the configuration file comes with a custom image.
106        let mut change_image = None;
107        let mut image = Image::from_yaml(config).or_else(|_| {
108            // We use our built-in images (first game = 0).
109            change_image = Some(0);
110            Image::new()
111        })?;
112        image.update(&game);
113        Ok(Self {
114            dict,
115            game,
116            image,
117            change_image,
118        })
119    }
120
121    fn process_user_input(&mut self, inp: &str) {
122        match self.game.state {
123            State::Victory => {
124                // Start a new game. As long as we do not get a `State::VictoryGameOver`, we know
125                // that there is at least one secret left.
126                let secret = self.dict.get_random_secret().unwrap();
127                self.game = Game::new(&secret, LIVES, self.dict.is_empty());
128                // We change the image, when we have guessed a certain number of times.
129                if let Some(n) = self.change_image {
130                    if n == CHANGE_IMAGE_MAX - 1 {
131                        // Switch to the next image.
132                        if let Ok(new_image) = Image::new() {
133                            self.image = new_image;
134                        };
135                        self.change_image = Some(0);
136                    } else {
137                        self.change_image = Some(n + 1);
138                    };
139                };
140                self.image.update(&self.game);
141            }
142
143            State::VictoryGameOver => {}
144
145            State::Defeat | State::DefeatGameOver => {
146                // We will ask this secret again; this way we never end a game with a defeat.
147                self.dict.add((self.game.secret).to_raw_string());
148                // Start a new game. As we just added a secret, we know there is at least one.
149                let secret = self.dict.get_random_secret().unwrap();
150                self.game = Game::new(&secret, LIVES, self.dict.is_empty());
151                self.image.update(&self.game);
152            }
153            State::Ongoing => {
154                self.game.guess(inp.chars().next().unwrap_or(' '));
155                // `guess()` changes the game state:
156                self.image.update(&self.game);
157            }
158        }
159    }
160
161    fn render_image(&self) -> String {
162        format!("{}", self.image)
163    }
164
165    #[allow(dead_code)]
166    fn get_image_dimension(&self) -> (u8, u8) {
167        self.image.dimension
168    }
169
170    fn render_secret(&self) -> String {
171        format!("{}", self.game.secret)
172    }
173
174    fn render_game_lifes(&self) -> String {
175        format!("Lifes: {}", self.game.lifes)
176    }
177
178    fn render_game_last_guess(&self) -> String {
179        format!("Last guess: {}", self.game.last_guess)
180    }
181
182    fn render_instructions(&self) -> String {
183        match self.game.state {
184            State::Victory => String::from("Congratulations! You won!"),
185            State::VictoryGameOver => String::from("Congratulations! You won!"),
186            State::Defeat | State::DefeatGameOver => String::from("You lost."),
187            State::Ongoing => String::from("Type a letter, then press [Enter]:"),
188        }
189    }
190
191    fn get_state(&self) -> State {
192        self.game.state.clone()
193    }
194}