pikadick/commands/
tic_tac_toe.rs1mod 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#[derive(Clone)]
46pub struct TicTacToeData {
47 renderer: Arc<Renderer>,
48}
49
50impl TicTacToeData {
51 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 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}