rustacean_roulette/
lib.rs

1mod commands;
2mod constants;
3
4pub use commands::Commands;
5use frankenstein::{
6    client_reqwest::Bot, methods::{DeleteMyCommandsParams, SetMyCommandsParams, SetMyDefaultAdministratorRightsParams}, types::BotCommandScope, AsyncTelegramApi, Error
7};
8use rand::{Rng, seq::index::sample};
9use serde::Deserialize;
10use std::time::{SystemTime, UNIX_EPOCH};
11
12/// Configuration for the bot.
13#[derive(Deserialize)]
14pub struct Config {
15    /// The token for the bot.
16    pub token: String,
17    /// The configuration for the Russian Roulette game.
18    #[serde(default)]
19    pub game: RouletteConfig,
20    /// The override configuration for groups.
21    #[serde(default)]
22    pub groups: Vec<GroupConfig>,
23}
24
25/// Configuration for the Russian Roulette game.
26#[derive(Clone, Debug, Deserialize)]
27pub struct RouletteConfig {
28    /// Number of chambers in the revolver.
29    #[serde(default = "constants::chambers")]
30    chambers: usize,
31    /// Number of bullets in the revolver.
32    #[serde(default = "constants::bullets")]
33    bullets: usize,
34    /// Minimum time to mute in seconds.
35    #[serde(default = "constants::min_mute_time")]
36    min_mute_time: u32,
37    /// Maximum time to mute in seconds.
38    #[serde(default = "constants::max_mute_time")]
39    max_mute_time: u32,
40}
41
42impl RouletteConfig {
43    /// Starts a new game of Russian Roulette.
44    pub fn start(self) -> Result<Roulette, &'static str> {
45        // Sanity check
46        if self.chambers <= 0 {
47            return Err("Number of chambers must be greater than 0");
48        }
49        if self.bullets <= 0 {
50            return Err("Number of bullets must be greater than 0");
51        }
52        if self.bullets > self.chambers {
53            return Err("Number of bullets must be less than or equal to number of chambers");
54        }
55        if self.min_mute_time < 30 {
56            return Err("Minimum mute time must be greater than or equal to 30 seconds");
57        }
58        if self.max_mute_time > 3600 {
59            // FIXME: 365 days
60            return Err("Maximum mute time must be less than or equal to 3600 seconds");
61        }
62        if self.min_mute_time > self.max_mute_time {
63            return Err("Minimum mute time must be less than or equal to maximum mute time");
64        }
65
66        // Initialize the contents of the chambers
67        let contents = vec![false; self.chambers];
68        let mut roulette = Roulette {
69            config: self,
70            contents,
71            position: 0,
72        };
73        roulette.restart();
74
75        Ok(roulette)
76    }
77
78    /// Get the number of bullets and chambers.
79    pub fn info(&self) -> (usize, usize) {
80        (self.bullets, self.chambers)
81    }
82
83    /// Generate a random mute time and the time until which the user will be muted.
84    pub fn random_mute_until(&self) -> (u64, u64) {
85        // Generate a random mute time between min and max
86        let mut rng = rand::rng();
87        let duration: u64 = rng
88            .random_range(self.min_mute_time..=self.max_mute_time)
89            .into();
90        // Convert to seconds and add to current time
91        let now = SystemTime::now()
92            .duration_since(UNIX_EPOCH)
93            .expect("Time went backwards")
94            .as_secs();
95        (duration, now + duration)
96    }
97}
98
99impl Default for RouletteConfig {
100    fn default() -> Self {
101        Self {
102            chambers: constants::chambers(),
103            bullets: constants::bullets(),
104            min_mute_time: constants::min_mute_time(),
105            max_mute_time: constants::max_mute_time(),
106        }
107    }
108}
109
110/// A Russian Roulette game.
111#[derive(Clone, Debug)]
112pub struct Roulette {
113    /// Configuration for the game.
114    pub config: RouletteConfig,
115    /// An array of boolean values representing the contents of the chambers. `true` means the chamber is loaded with a bullet, `false` means it is empty.
116    contents: Vec<bool>,
117    /// The current chamber index.
118    position: usize,
119}
120
121impl Roulette {
122    /// Restart the Russian Roulette game.
123    pub fn restart(&mut self) {
124        self.position = 0;
125        self.contents.fill(false);
126
127        // Randomly choose `bullets` chambers to be loaded with bullets.
128        let mut rng = rand::rng();
129        let selected = sample(&mut rng, self.contents.len(), self.config.bullets);
130        for i in selected {
131            self.contents[i] = true;
132        }
133    }
134
135    /// Get the number of bullets and chambers.
136    pub fn info(&self) -> (usize, usize) {
137        self.config.info()
138    }
139
140    /// Generate a random mute time and the time until which the user will be muted.
141    pub fn random_mute_until(&self) -> (u64, u64) {
142        self.config.random_mute_until()
143    }
144
145    /// Try to fire the current chamber.
146    ///
147    /// - If the chamber is loaded with a bullet, return `Some(true)`
148    /// - If the chamber is empty, return `Some(false)`
149    /// - If we have fired all filled chambers, return `None`
150    pub fn fire(&mut self) -> Option<bool> {
151        if self.peek().0 == 0 {
152            // No filled chambers left
153            return None;
154        }
155
156        let result = self.contents[self.position];
157        self.position += 1;
158
159        Some(result)
160    }
161
162    /// Peek the left-over chambers, returning count of filled and left chambers.
163    pub fn peek(&self) -> (usize, usize) {
164        let filled = self
165            .contents
166            .iter()
167            .skip(self.position)
168            .filter(|&&x| x)
169            .count();
170        let left = self.contents.len() - self.position;
171        (filled, left)
172    }
173}
174
175/// Configuration for a group.
176#[derive(Debug, Deserialize)]
177pub struct GroupConfig {
178    /// The ID of the group.
179    pub id: i64,
180    /// Override number of chambers in the revolver.
181    chambers: Option<usize>,
182    /// Override number of bullets in the revolver.
183    bullets: Option<usize>,
184    /// Override minimum time to mute in seconds.
185    min_mute_time: Option<u32>,
186    /// Override maximum time to mute in seconds.
187    max_mute_time: Option<u32>,
188}
189
190impl GroupConfig {
191    /// Resolves to a [`RouletteConfig`].
192    pub fn resolve(&self, default: &RouletteConfig) -> RouletteConfig {
193        let Self {
194            chambers,
195            bullets,
196            min_mute_time,
197            max_mute_time,
198            ..
199        } = self;
200        let (chambers, bullets, min_mute_time, max_mute_time) = (
201            chambers.unwrap_or(default.chambers),
202            bullets.unwrap_or(default.bullets),
203            min_mute_time.unwrap_or(default.min_mute_time),
204            max_mute_time.unwrap_or(default.max_mute_time),
205        );
206        RouletteConfig {
207            chambers,
208            bullets,
209            min_mute_time,
210            max_mute_time,
211        }
212    }
213}
214
215/// Set commands and default admin rights for the bot.
216pub async fn init_commands_and_rights(bot: &Bot) -> Result<(), Error> {
217    let delete_param = DeleteMyCommandsParams::builder().build();
218    bot.delete_my_commands(&delete_param).await?;
219
220    let commands_param = SetMyCommandsParams::builder()
221        .commands(Commands::list())
222        .scope(BotCommandScope::AllGroupChats)
223        .build();
224    bot.set_my_commands(&commands_param).await?;
225
226    let rights_param = SetMyDefaultAdministratorRightsParams::builder()
227        .rights(constants::RECOMMENDED_ADMIN_RIGHTS)
228        .build();
229    bot.set_my_default_administrator_rights(&rights_param)
230        .await?;
231
232    Ok(())
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn test_fire() {
241        let config = RouletteConfig {
242            chambers: 3,
243            bullets: 1,
244            min_mute_time: 60,
245            max_mute_time: 600,
246        };
247        // let mut roulette = config.start().unwrap();
248        let mut roulette = Roulette {
249            config,
250            contents: vec![false, true, false],
251            position: 0,
252        };
253
254        assert_eq!(roulette.fire(), Some(false));
255        assert_eq!(roulette.fire(), Some(true));
256        assert_eq!(roulette.fire(), None);
257        assert_eq!(roulette.fire(), None);
258    }
259
260    #[test]
261    fn test_restart() {
262        let mut roulette = RouletteConfig::default().start().unwrap();
263
264        for _ in 0..10 {
265            roulette.restart();
266        }
267
268        assert_eq!(roulette.contents.len(), 6);
269        assert_eq!(roulette.peek().0, 2);
270        assert_eq!(roulette.position, 0);
271    }
272}