pikadick/commands/
tic_tac_toe.rs

1mod board;
2mod concede;
3mod play;
4mod renderer;
5mod scoreboard;
6mod stats;
7
8use self::renderer::Renderer;
9pub use self::{
10    board::BOARD_COMMAND,
11    concede::CONCEDE_COMMAND,
12    play::PLAY_COMMAND,
13    scoreboard::SCOREBOARD_COMMAND,
14    stats::STATS_COMMAND,
15};
16use crate::{
17    checks::ENABLED_CHECK,
18    database::{
19        model::TicTacToePlayer,
20        TicTacToeTryMoveError,
21        TicTacToeTryMoveResponse,
22    },
23    ClientDataKey,
24};
25use serenity::{
26    builder::{
27        CreateAttachment,
28        CreateMessage,
29    },
30    client::Context,
31    framework::standard::{
32        macros::command,
33        Args,
34        CommandResult,
35    },
36    model::{
37        channel::Message,
38        prelude::*,
39    },
40};
41use std::sync::Arc;
42use tracing::error;
43
44/// Data pertaining to running tic_tac_toe games
45#[derive(Clone)]
46pub struct TicTacToeData {
47    renderer: Arc<Renderer>,
48}
49
50impl TicTacToeData {
51    /// Make a new [`TicTacToeData`].
52    pub fn new() -> Self {
53        let renderer = Renderer::new().expect("failed to init renderer");
54
55        Self {
56            renderer: Arc::new(renderer),
57        }
58    }
59}
60
61impl std::fmt::Debug for TicTacToeData {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        f.debug_struct("TicTacToeData").finish()
64    }
65}
66
67impl Default for TicTacToeData {
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73impl TicTacToePlayer {
74    /// Get the "mention" for a user.
75    ///
76    /// Computer is "computer" and users are mentioned.
77    pub fn mention(self) -> GamePlayerMention {
78        GamePlayerMention(self)
79    }
80}
81
82#[derive(Debug, Copy, Clone)]
83pub struct GamePlayerMention(TicTacToePlayer);
84
85impl std::fmt::Display for GamePlayerMention {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        match self.0 {
88            TicTacToePlayer::Computer => "computer".fmt(f),
89            TicTacToePlayer::User(user_id) => user_id.mention().fmt(f),
90        }
91    }
92}
93
94#[command("tic-tac-toe")]
95#[aliases("ttt")]
96#[sub_commands("play", "concede", "board", "stats", "scoreboard")]
97#[description("Play a game of Tic-Tac-Toe")]
98#[usage("<move #>")]
99#[example("0")]
100#[min_args(1)]
101#[max_args(1)]
102#[checks(Enabled)]
103#[bucket("default")]
104pub async fn tic_tac_toe(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
105    let data_lock = ctx.data.read().await;
106    let client_data = data_lock
107        .get::<ClientDataKey>()
108        .expect("missing client data");
109    let tic_tac_toe_data = client_data.tic_tac_toe_data.clone();
110    let db = client_data.db.clone();
111    drop(data_lock);
112
113    let guild_id = msg.guild_id;
114    let author_id = msg.author.id;
115
116    let mut move_index = match args.trimmed().single::<u8>() {
117        Ok(num) => num,
118        Err(error) => {
119            let response = format!("That move is not a number: {error}\nUse `tic-tac-toe play <computer/@user> <X/O> to start a game.`");
120            msg.channel_id.say(&ctx.http, response).await?;
121            return Ok(());
122        }
123    };
124
125    if !(1..=9).contains(&move_index) {
126        let response = format!(
127            "Your move number must be between 1 and 9 {}",
128            author_id.mention()
129        );
130        msg.channel_id.say(&ctx.http, response).await?;
131        return Ok(());
132    }
133
134    move_index -= 1;
135
136    match db
137        .try_tic_tac_toe_move(guild_id.into(), author_id.into(), move_index)
138        .await
139    {
140        Ok(TicTacToeTryMoveResponse::Winner {
141            game,
142            winner,
143            loser,
144        }) => {
145            let file = match tic_tac_toe_data
146                .renderer
147                .render_board_async(game.board)
148                .await
149            {
150                Ok(file) => {
151                    CreateAttachment::bytes(file, format!("ttt-{}.png", game.board.encode_u16()))
152                }
153                Err(error) => {
154                    error!("Failed to render Tic-Tac-Toe board: {error}");
155                    msg.channel_id
156                        .say(
157                            &ctx.http,
158                            format!("Failed to render Tic-Tac-Toe board: {error}"),
159                        )
160                        .await?;
161                    return Ok(());
162                }
163            };
164            let content = format!(
165                "{} has triumphed over {} in Tic-Tac-Toe",
166                winner.mention(),
167                loser.mention(),
168            );
169            let message_builder = CreateMessage::new().content(content).add_file(file);
170            msg.channel_id
171                .send_message(&ctx.http, message_builder)
172                .await?;
173        }
174        Ok(TicTacToeTryMoveResponse::Tie { game }) => {
175            let file = match tic_tac_toe_data
176                .renderer
177                .render_board_async(game.board)
178                .await
179            {
180                Ok(file) => {
181                    CreateAttachment::bytes(file, format!("ttt-{}.png", game.board.encode_u16()))
182                }
183                Err(error) => {
184                    error!("Failed to render Tic-Tac-Toe board: {error}");
185                    msg.channel_id
186                        .say(
187                            &ctx.http,
188                            format!("Failed to render Tic-Tac-Toe board: {error}"),
189                        )
190                        .await?;
191                    return Ok(());
192                }
193            };
194            let content = format!(
195                "{} has tied with {} in Tic-Tac-Toe",
196                game.get_player(tic_tac_toe::Team::X).mention(),
197                game.get_player(tic_tac_toe::Team::O).mention(),
198            );
199            let message_builder = CreateMessage::new().content(content).add_file(file);
200            msg.channel_id
201                .send_message(&ctx.http, message_builder)
202                .await?;
203        }
204        Ok(TicTacToeTryMoveResponse::NextTurn { game }) => {
205            let file = match tic_tac_toe_data
206                .renderer
207                .render_board_async(game.board)
208                .await
209            {
210                Ok(file) => {
211                    CreateAttachment::bytes(file, format!("ttt-{}.png", game.board.encode_u16()))
212                }
213                Err(error) => {
214                    error!("Failed to render Tic-Tac-Toe board: {error}");
215                    msg.channel_id
216                        .say(
217                            &ctx.http,
218                            format!("Failed to render Tic-Tac-Toe board: {error}"),
219                        )
220                        .await?;
221                    return Ok(());
222                }
223            };
224            let content = format!("Your turn {}", game.get_player_turn().mention());
225            let message_builder = CreateMessage::new().content(content).add_file(file);
226            msg.channel_id
227                .send_message(&ctx.http, message_builder)
228                .await?;
229        }
230        Err(TicTacToeTryMoveError::InvalidTurn) => {
231            let response = "It is not your turn. Please wait for your opponent to finish.";
232            msg.channel_id.say(&ctx.http, response).await?;
233        }
234        Err(TicTacToeTryMoveError::InvalidMove) => {
235            let response = format!(
236                "Invalid move {}. Please choose one of the available squares.\n",
237                author_id.mention(),
238            );
239            msg.channel_id.say(&ctx.http, response).await?;
240        }
241        Err(TicTacToeTryMoveError::NotInAGame) => {
242            let response =
243                "No games in progress. Make one with `tic-tac-toe play <computer/@user> <X/O>`.";
244            msg.channel_id.say(&ctx.http, response).await?;
245        }
246        Err(TicTacToeTryMoveError::Database(error)) => {
247            error!("{error:?}");
248            msg.channel_id.say(&ctx.http, "database error").await?;
249        }
250    }
251
252    Ok(())
253}