1use bitflags::bitflags;
2use rusqlite::{
3 types::{
4 FromSql,
5 FromSqlError,
6 FromSqlResult,
7 ToSqlOutput,
8 ValueRef,
9 },
10 ToSql,
11};
12use serenity::{
13 model::prelude::*,
14 utils::parse_user_mention,
15};
16use std::{
17 borrow::Cow,
18 num::NonZeroU64,
19 str::FromStr,
20};
21
22struct DatabaseUserId(UserId);
24
25impl FromSql for DatabaseUserId {
26 fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
27 #[allow(clippy::or_fun_call)]
29 let value = value
30 .as_i64()
31 .map(i64::to_ne_bytes)
32 .map(u64::from_ne_bytes)
33 .map(NonZeroU64::new)?
34 .ok_or(FromSqlError::OutOfRange(0))?;
35
36 Ok(Self(UserId::from(value)))
37 }
38}
39
40#[derive(Debug, Copy, Clone)]
42pub struct TicTacToeGame {
43 pub board: tic_tac_toe::Board,
45 pub x_player: TicTacToePlayer,
47 pub o_player: TicTacToePlayer,
49}
50
51impl TicTacToeGame {
52 pub(super) fn new(x_player: TicTacToePlayer, o_player: TicTacToePlayer) -> Self {
54 Self {
55 board: Default::default(),
56 x_player,
57 o_player,
58 }
59 }
60
61 pub fn get_team_turn(&self) -> tic_tac_toe::Team {
63 self.board.get_turn()
64 }
65
66 pub fn get_player(&self, team: tic_tac_toe::Team) -> TicTacToePlayer {
68 match team {
69 tic_tac_toe::Team::X => self.x_player,
70 tic_tac_toe::Team::O => self.o_player,
71 }
72 }
73
74 pub fn get_player_turn(&self) -> TicTacToePlayer {
76 self.get_player(self.get_team_turn())
77 }
78
79 pub fn try_move(&mut self, index: u8, team: tic_tac_toe::Team) -> bool {
84 if index >= tic_tac_toe::NUM_TILES || self.board.get(index).is_some() {
85 false
86 } else {
87 self.board = self.board.set(index, Some(team));
88 true
89 }
90 }
91
92 pub fn get_opponent(&self, player: TicTacToePlayer) -> Option<TicTacToePlayer> {
94 match (player == self.x_player, player == self.o_player) {
95 (false, false) => None,
96 (false, true) => Some(self.x_player),
97 (true, false) => Some(self.o_player),
98 (true, true) => Some(player), }
100 }
101
102 pub fn iter_players(&self) -> impl Iterator<Item = TicTacToePlayer> + '_ {
108 let mut count = 0;
109 std::iter::from_fn(move || {
110 let ret = match count {
111 0 => self.x_player,
112 1 => self.o_player,
113 _c => return None,
114 };
115 count += 1;
116 Some(ret)
117 })
118 }
119}
120
121#[derive(Debug, Clone)]
122pub struct TicTacToePlayerParseError(std::num::ParseIntError);
123
124impl std::fmt::Display for TicTacToePlayerParseError {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 "invalid player".fmt(f)
127 }
128}
129
130impl std::error::Error for TicTacToePlayerParseError {
131 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
132 Some(&self.0)
133 }
134}
135
136#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
138pub enum TicTacToePlayer {
139 Computer,
141
142 User(UserId),
144}
145
146impl TicTacToePlayer {
147 pub fn is_computer(self) -> bool {
149 matches!(self, Self::Computer)
150 }
151
152 pub fn is_user(self) -> bool {
154 matches!(self, Self::User(_))
155 }
156
157 pub fn get_user(self) -> Option<UserId> {
159 match self {
160 Self::Computer => None,
161 Self::User(user_id) => Some(user_id),
162 }
163 }
164}
165
166impl From<TicTacToePlayer> for Cow<'static, str> {
167 fn from(player: TicTacToePlayer) -> Self {
168 match player {
169 TicTacToePlayer::Computer => "computer".into(),
170 TicTacToePlayer::User(id) => id.to_string().into(),
171 }
172 }
173}
174
175impl FromStr for TicTacToePlayer {
176 type Err = TicTacToePlayerParseError;
177
178 fn from_str(input: &str) -> Result<Self, Self::Err> {
179 if input.eq_ignore_ascii_case("computer") {
180 Ok(Self::Computer)
181 } else if let Some(user_id) = parse_user_mention(input) {
182 Ok(Self::User(user_id))
183 } else {
184 let user_id: NonZeroU64 = input.parse().map_err(TicTacToePlayerParseError)?;
185 Ok(Self::User(UserId::from(user_id)))
186 }
187 }
188}
189
190impl From<UserId> for TicTacToePlayer {
191 fn from(user_id: UserId) -> Self {
192 Self::User(user_id)
193 }
194}
195
196impl ToSql for TicTacToePlayer {
197 fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
198 match self {
199 Self::Computer => Ok(ToSqlOutput::Borrowed(ValueRef::Null)),
200 Self::User(id) => Ok(ToSqlOutput::Borrowed(ValueRef::Integer(i64::from(*id)))),
201 }
202 }
203}
204
205impl FromSql for TicTacToePlayer {
206 fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
207 match value {
208 ValueRef::Integer(int) => {
209 let int_u64 = u64::from_ne_bytes(int.to_ne_bytes());
210 #[allow(clippy::or_fun_call)]
212 let user_id =
213 UserId::from(NonZeroU64::new(int_u64).ok_or(FromSqlError::OutOfRange(0))?);
214 Ok(Self::User(user_id))
215 }
216 ValueRef::Null => Ok(Self::Computer),
217 _ => Err(FromSqlError::InvalidType),
218 }
219 }
220}
221
222#[derive(Debug, Copy, Clone)]
226pub struct MaybeGuildString {
227 pub guild_id: Option<GuildId>,
228}
229
230impl From<Option<GuildId>> for MaybeGuildString {
231 fn from(guild_id: Option<GuildId>) -> Self {
232 Self { guild_id }
233 }
234}
235
236impl ToSql for MaybeGuildString {
237 fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
238 match self.guild_id {
239 Some(guild_id) => Ok(ToSqlOutput::from(guild_id.to_string())),
240 None => Ok(ToSqlOutput::Borrowed(ValueRef::Text(b"empty"))),
241 }
242 }
243}
244
245impl FromSql for MaybeGuildString {
246 fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
247 let text = value.as_str()?;
248 match text.parse::<NonZeroU64>() {
249 Ok(guild_id) => Ok(MaybeGuildString {
250 guild_id: Some(GuildId::from(guild_id)),
251 }),
252 Err(e) => {
253 if text == "empty" {
254 Ok(MaybeGuildString { guild_id: None })
255 } else {
256 Err(FromSqlError::Other(Box::new(e)))
257 }
258 }
259 }
260 }
261}
262
263#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
265pub struct TicTacToeScore {
266 pub wins: u64,
268 pub losses: u64,
270 pub ties: u64,
272 pub concedes: u64,
274}
275
276#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
278pub struct TicTacToeTopPlayerScore {
279 pub score: i64,
281 pub player: UserId,
283 pub wins: u64,
285 pub losses: u64,
287 pub ties: u64,
289 pub concedes: u64,
291}
292
293impl TicTacToeTopPlayerScore {
294 pub(crate) fn from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<Self> {
304 let score = row.get(0)?;
305
306 let player = row.get::<_, DatabaseUserId>(1)?.0;
307
308 let wins = row.get(2)?;
309
310 let losses = row.get(3)?;
311
312 let ties = row.get(4)?;
313
314 let concedes = row.get(5)?;
315
316 Ok(TicTacToeTopPlayerScore {
317 score,
318 player,
319 wins,
320 losses,
321 ties,
322 concedes,
323 })
324 }
325}
326
327bitflags! {
328 #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
330 pub struct TikTokEmbedFlags: u32 {
331 const ENABLED = 1 << 0;
333 const DELETE_LINK = 1 << 1;
335 }
336}
337
338impl ToSql for TikTokEmbedFlags {
339 fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
340 Ok(self.bits().into())
341 }
342}
343
344impl FromSql for TikTokEmbedFlags {
345 fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
346 let value = value.as_i64()?;
347 let value = u32::try_from(value).map_err(|_e| FromSqlError::OutOfRange(value))?;
348
349 Self::from_bits(value).ok_or_else(|| FromSqlError::OutOfRange(value.into()))
350 }
351}
352
353impl Default for TikTokEmbedFlags {
354 fn default() -> Self {
355 Self::empty()
356 }
357}