add many game

This commit is contained in:
Puechberty Arthur
2026-04-10 20:53:09 +02:00
parent 9ea4914e42
commit 618e222759
28 changed files with 2784 additions and 17 deletions
+45
View File
@@ -0,0 +1,45 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{send_embed, theme_color, truncate_text};
pub async fn handle_catsay(ctx: &Context, msg: &Message, args: &[&str]) {
if args.is_empty() {
let embed = CreateEmbed::new()
.title("Catsay")
.description("Utilise `+catsay <message>`.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let text = truncate_text(&args.join(" "), 120);
let bubble = format!("< {} >", text);
let cat = format!("{}\n \\\n \\\n /\\_/\\\n ( o.o )\n > ^ <", bubble);
let embed = CreateEmbed::new()
.title("Catsay")
.description(format!("```\n{}\n```", cat))
.color(theme_color(ctx).await);
send_embed(ctx, msg, embed).await;
}
pub struct CatsayCommand;
pub static COMMAND_DESCRIPTOR: CatsayCommand = CatsayCommand;
impl crate::commands::command_contract::CommandSpec for CatsayCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "catsay",
category: "game",
params: "<message>",
description: "Faire parler les chat.",
examples: &["+catsay Bonjour"],
default_aliases: &["meow"],
allow_in_dm: true,
default_permission: 0,
}
}
}
+44
View File
@@ -0,0 +1,44 @@
use chrono::{Datelike, NaiveDate, Utc};
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{send_embed, theme_color};
pub async fn handle_christmas(ctx: &Context, msg: &Message, _args: &[&str]) {
let today = Utc::now().date_naive();
let mut year = today.year();
let mut target = NaiveDate::from_ymd_opt(year, 12, 25).unwrap_or(today);
if today > target {
year += 1;
target = NaiveDate::from_ymd_opt(year, 12, 25).unwrap_or(today);
}
let days = (target - today).num_days();
let embed = CreateEmbed::new()
.title("Christmas")
.description(format!("Il reste **{}** jour(s) avant Noel.", days))
.color(theme_color(ctx).await);
send_embed(ctx, msg, embed).await;
}
pub struct ChristmasCommand;
pub static COMMAND_DESCRIPTOR: ChristmasCommand = ChristmasCommand;
impl crate::commands::command_contract::CommandSpec for ChristmasCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "christmas",
category: "game",
params: "aucun",
description: "Calcule le nombre de jours jusqu'a Noel.",
examples: &["+christmas"],
default_aliases: &["xmas"],
allow_in_dm: true,
default_permission: 0,
}
}
}
+51
View File
@@ -0,0 +1,51 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{mention_user, send_embed, theme_color};
pub async fn handle_claque(ctx: &Context, msg: &Message, args: &[&str]) {
let target = msg
.mentions
.first()
.map(|user| mention_user(user.id))
.or_else(|| {
args.first().map(|raw| {
if raw.starts_with("<@") {
raw.to_string()
} else {
format!("**{}**", raw)
}
})
})
.unwrap_or_else(|| "le vide".to_string());
let embed = CreateEmbed::new()
.title("Claque")
.description(format!(
"{} met une claque a {}.",
mention_user(msg.author.id),
target
))
.color(theme_color(ctx).await);
send_embed(ctx, msg, embed).await;
}
pub struct ClaqueCommand;
pub static COMMAND_DESCRIPTOR: ClaqueCommand = ClaqueCommand;
impl crate::commands::command_contract::CommandSpec for ClaqueCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "claque",
category: "game",
params: "[@user]",
description: "Fait une claque a un utilisateur mentionne ou a un utilisateur.",
examples: &["+claque", "+claque @Pseudo"],
default_aliases: &["slap"],
allow_in_dm: true,
default_permission: 0,
}
}
}
+97
View File
@@ -0,0 +1,97 @@
use rand::seq::SliceRandom;
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{send_embed, theme_color};
const SIZE: usize = 5;
const MINES: usize = 5;
pub async fn handle_demineur(ctx: &Context, msg: &Message, _args: &[&str]) {
let mut mine_positions = (0..(SIZE * SIZE)).collect::<Vec<_>>();
{
let mut rng = rand::thread_rng();
mine_positions.shuffle(&mut rng);
}
mine_positions.truncate(MINES);
let mut board = vec![vec![0u8; SIZE]; SIZE];
for index in &mine_positions {
let row = index / SIZE;
let col = index % SIZE;
board[row][col] = 9;
}
for row in 0..SIZE {
for col in 0..SIZE {
if board[row][col] == 9 {
continue;
}
let mut count = 0u8;
for dr in -1isize..=1 {
for dc in -1isize..=1 {
if dr == 0 && dc == 0 {
continue;
}
let nr = row as isize + dr;
let nc = col as isize + dc;
if nr >= 0
&& nr < SIZE as isize
&& nc >= 0
&& nc < SIZE as isize
&& board[nr as usize][nc as usize] == 9
{
count += 1;
}
}
}
board[row][col] = count;
}
}
let rendered = board
.iter()
.map(|line| {
line.iter()
.map(|cell| {
if *cell == 9 {
"||*||".to_string()
} else {
format!("||{}||", cell)
}
})
.collect::<Vec<_>>()
.join(" ")
})
.collect::<Vec<_>>()
.join("\n");
let embed = CreateEmbed::new()
.title("Demineur")
.description(format!(
"Plateau genere. Clique les spoilers pour reveler les cases.\n\n{}",
rendered
))
.color(theme_color(ctx).await);
send_embed(ctx, msg, embed).await;
}
pub struct DemineurCommand;
pub static COMMAND_DESCRIPTOR: DemineurCommand = DemineurCommand;
impl crate::commands::command_contract::CommandSpec for DemineurCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "demineur",
category: "game",
params: "aucun",
description: "Jouer a un jeu demineur.",
examples: &["+demineur"],
default_aliases: &["mine"],
allow_in_dm: true,
default_permission: 0,
}
}
}
+60
View File
@@ -0,0 +1,60 @@
use rand::seq::SliceRandom;
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{send_embed, theme_color};
pub async fn handle_eightball(ctx: &Context, msg: &Message, args: &[&str]) {
if args.is_empty() {
let embed = CreateEmbed::new()
.title("8ball")
.description("Pose ta question avec `+8ball <question>`.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let answers = [
"Oui, clairement.",
"Probablement.",
"Je ne pense pas.",
"Reessaye plus tard.",
"C est certain.",
"Impossible a dire pour le moment.",
];
let answer = {
let mut rng = rand::thread_rng();
answers
.choose(&mut rng)
.copied()
.unwrap_or("Reessaye plus tard.")
};
let question = args.join(" ");
let embed = CreateEmbed::new()
.title("8ball")
.description(format!("Question: {}\nReponse: **{}**", question, answer))
.color(theme_color(ctx).await);
send_embed(ctx, msg, embed).await;
}
pub struct EightballCommand;
pub static COMMAND_DESCRIPTOR: EightballCommand = EightballCommand;
impl crate::commands::command_contract::CommandSpec for EightballCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "8ball",
category: "game",
params: "<question>",
description: "Posez une question a la boule magique 8.",
examples: &["+8ball Vais-je gagner ?"],
default_aliases: &["magic8"],
allow_in_dm: true,
default_permission: 0,
}
}
}
+51
View File
@@ -0,0 +1,51 @@
use rand::Rng;
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{send_embed, theme_color};
pub async fn handle_epicgamer(ctx: &Context, msg: &Message, _args: &[&str]) {
let percent = {
let mut rng = rand::thread_rng();
rng.gen_range(0..=100)
};
let rank = if percent >= 90 {
"Legendary"
} else if percent >= 70 {
"Epic"
} else if percent >= 40 {
"Casual"
} else {
"Noob"
};
let embed = CreateEmbed::new()
.title("Epic Gamer")
.description(format!(
"Ton pourcentage de gamer epique est **{}%**.\nRang: **{}**.",
percent, rank
))
.color(theme_color(ctx).await);
send_embed(ctx, msg, embed).await;
}
pub struct EpicgamerCommand;
pub static COMMAND_DESCRIPTOR: EpicgamerCommand = EpicgamerCommand;
impl crate::commands::command_contract::CommandSpec for EpicgamerCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "epicgamer",
category: "game",
params: "aucun",
description: "Evaluez votre pourcentage de gamer epique.",
examples: &["+epicgamer"],
default_aliases: &["gamer"],
allow_in_dm: true,
default_permission: 0,
}
}
}
+51
View File
@@ -0,0 +1,51 @@
use rand::seq::SliceRandom;
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{send_embed, theme_color};
pub async fn handle_fasttype(ctx: &Context, msg: &Message, _args: &[&str]) {
let challenges = [
"shadow bot est rapide",
"rust rend le bot solide",
"je tape plus vite que mon ombre",
"discord et serenite",
];
let sentence = {
let mut rng = rand::thread_rng();
challenges
.choose(&mut rng)
.copied()
.unwrap_or("shadow bot est rapide")
};
let embed = CreateEmbed::new()
.title("FastType")
.description(format!(
"Premier a retaper cette phrase gagne:\n\n`{}`",
sentence
))
.color(theme_color(ctx).await);
send_embed(ctx, msg, embed).await;
}
pub struct FasttypeCommand;
pub static COMMAND_DESCRIPTOR: FasttypeCommand = FasttypeCommand;
impl crate::commands::command_contract::CommandSpec for FasttypeCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "fasttype",
category: "game",
params: "aucun",
description: "Jouer a un jeu de vitesse de frappe.",
examples: &["+fasttype"],
default_aliases: &["type"],
allow_in_dm: true,
default_permission: 0,
}
}
}
+56
View File
@@ -0,0 +1,56 @@
use rand::seq::SliceRandom;
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{send_embed, theme_color};
pub async fn handle_findemoji(ctx: &Context, msg: &Message, _args: &[&str]) {
let packs = [
(":cat:", [":cat:", ":dog:", ":fox:", ":bear:"]),
(":star:", [":moon:", ":sunny:", ":star:", ":zap:"]),
(":pizza:", [":hamburger:", ":pizza:", ":fries:", ":hotdog:"]),
];
let (target, options) = {
let mut rng = rand::thread_rng();
packs
.choose(&mut rng)
.copied()
.unwrap_or((":cat:", [":cat:", ":dog:", ":fox:", ":bear:"]))
};
let embed = CreateEmbed::new()
.title("FindEmoji")
.description(format!(
"Trouve l emoji cible: **{}**\nOptions: {}",
target,
options.join(" ")
))
.field(
"Regle",
"Le premier qui identifie le bon emoji gagne le round.",
false,
)
.color(theme_color(ctx).await);
send_embed(ctx, msg, embed).await;
}
pub struct FindemojiCommand;
pub static COMMAND_DESCRIPTOR: FindemojiCommand = FindemojiCommand;
impl crate::commands::command_contract::CommandSpec for FindemojiCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "findemoji",
category: "game",
params: "aucun",
description: "Jouer au jeu Trouver l Emoji.",
examples: &["+findemoji"],
default_aliases: &["emojihunt"],
allow_in_dm: true,
default_permission: 0,
}
}
}
+53
View File
@@ -0,0 +1,53 @@
use rand::Rng;
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{send_embed, theme_color};
pub async fn handle_flood(ctx: &Context, msg: &Message, _args: &[&str]) {
let palette = ['R', 'V', 'B', 'J', 'M', 'C'];
let mut rows = Vec::new();
{
let mut rng = rand::thread_rng();
for _ in 0..6 {
let mut line = String::new();
for _ in 0..6 {
let color = palette[rng.gen_range(0..palette.len())];
line.push(color);
line.push(' ');
}
rows.push(line.trim_end().to_string());
}
}
let embed = CreateEmbed::new()
.title("Flood")
.description(format!(
"Objectif: inonder la grille avec une seule couleur en un minimum de coups.\n\n```\n{}\n```",
rows.join("\n")
))
.field("Couleurs", "R=rouge V=vert B=bleu J=jaune M=magenta C=cyan", false)
.color(theme_color(ctx).await);
send_embed(ctx, msg, embed).await;
}
pub struct FloodCommand;
pub static COMMAND_DESCRIPTOR: FloodCommand = FloodCommand;
impl crate::commands::command_contract::CommandSpec for FloodCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "flood",
category: "game",
params: "aucun",
description: "Jouer au jeu Flood.",
examples: &["+flood"],
default_aliases: &["floodit"],
allow_in_dm: true,
default_permission: 0,
}
}
}
+472
View File
@@ -0,0 +1,472 @@
use rand::Rng;
use rand::seq::SliceRandom;
use serde::{Deserialize, Serialize};
use serenity::all::{ButtonStyle, ComponentInteraction};
use serenity::builder::{
CreateActionRow, CreateButton, CreateEmbed, CreateInteractionResponse,
CreateInteractionResponseMessage, CreateMessage,
};
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::theme_color;
use crate::db;
const GAME_KIND: &str = "2048";
const GAME_PREFIX: &str = "game:2048";
const SIZE: usize = 4;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Game2048State {
board: Vec<u16>,
owner_id: i64,
score: u32,
over: bool,
}
async fn pool(ctx: &Context) -> Option<sqlx::PgPool> {
let data = ctx.data.read().await;
data.get::<db::DbPoolKey>().cloned()
}
fn parse_component_id(custom_id: &str) -> Option<(i64, String)> {
let mut parts = custom_id.split(':');
let scope = parts.next()?;
let game = parts.next()?;
let session_id = parts.next()?.parse::<i64>().ok()?;
let action = parts.next()?.to_string();
if scope != "game" || game != "2048" || parts.next().is_some() {
return None;
}
Some((session_id, action))
}
fn create_fresh_board() -> Vec<u16> {
let mut board = vec![0u16; SIZE * SIZE];
let _ = spawn_random_tile(&mut board);
let _ = spawn_random_tile(&mut board);
board
}
fn spawn_random_tile(board: &mut [u16]) -> bool {
let empties = board
.iter()
.enumerate()
.filter_map(|(idx, value)| if *value == 0 { Some(idx) } else { None })
.collect::<Vec<_>>();
if empties.is_empty() {
return false;
}
let picked = {
let mut rng = rand::thread_rng();
empties.choose(&mut rng).copied()
};
let Some(index) = picked else {
return false;
};
let value = {
let mut rng = rand::thread_rng();
if rng.gen_bool(0.9) { 2 } else { 4 }
};
board[index] = value;
true
}
fn slide_merge_line(input: [u16; SIZE]) -> ([u16; SIZE], u32, bool) {
let non_zero = input
.iter()
.copied()
.filter(|value| *value != 0)
.collect::<Vec<_>>();
let mut merged = Vec::new();
let mut score_gain = 0u32;
let mut index = 0usize;
while index < non_zero.len() {
if index + 1 < non_zero.len() && non_zero[index] == non_zero[index + 1] {
let value = non_zero[index] * 2;
merged.push(value);
score_gain = score_gain.saturating_add(value as u32);
index += 2;
} else {
merged.push(non_zero[index]);
index += 1;
}
}
while merged.len() < SIZE {
merged.push(0);
}
let output = [merged[0], merged[1], merged[2], merged[3]];
let changed = output != input;
(output, score_gain, changed)
}
fn move_left(board: &mut [u16]) -> (bool, u32) {
let mut changed = false;
let mut gain = 0u32;
for row in 0..SIZE {
let base = row * SIZE;
let line = [
board[base],
board[base + 1],
board[base + 2],
board[base + 3],
];
let (new_line, score, line_changed) = slide_merge_line(line);
board[base] = new_line[0];
board[base + 1] = new_line[1];
board[base + 2] = new_line[2];
board[base + 3] = new_line[3];
changed |= line_changed;
gain = gain.saturating_add(score);
}
(changed, gain)
}
fn move_right(board: &mut [u16]) -> (bool, u32) {
let mut changed = false;
let mut gain = 0u32;
for row in 0..SIZE {
let base = row * SIZE;
let line = [
board[base + 3],
board[base + 2],
board[base + 1],
board[base],
];
let (new_line, score, line_changed) = slide_merge_line(line);
board[base + 3] = new_line[0];
board[base + 2] = new_line[1];
board[base + 1] = new_line[2];
board[base] = new_line[3];
changed |= line_changed;
gain = gain.saturating_add(score);
}
(changed, gain)
}
fn move_up(board: &mut [u16]) -> (bool, u32) {
let mut changed = false;
let mut gain = 0u32;
for col in 0..SIZE {
let line = [
board[col],
board[SIZE + col],
board[(2 * SIZE) + col],
board[(3 * SIZE) + col],
];
let (new_line, score, line_changed) = slide_merge_line(line);
board[col] = new_line[0];
board[SIZE + col] = new_line[1];
board[(2 * SIZE) + col] = new_line[2];
board[(3 * SIZE) + col] = new_line[3];
changed |= line_changed;
gain = gain.saturating_add(score);
}
(changed, gain)
}
fn move_down(board: &mut [u16]) -> (bool, u32) {
let mut changed = false;
let mut gain = 0u32;
for col in 0..SIZE {
let line = [
board[(3 * SIZE) + col],
board[(2 * SIZE) + col],
board[SIZE + col],
board[col],
];
let (new_line, score, line_changed) = slide_merge_line(line);
board[(3 * SIZE) + col] = new_line[0];
board[(2 * SIZE) + col] = new_line[1];
board[SIZE + col] = new_line[2];
board[col] = new_line[3];
changed |= line_changed;
gain = gain.saturating_add(score);
}
(changed, gain)
}
fn has_possible_moves(board: &[u16]) -> bool {
if board.iter().any(|value| *value == 0) {
return true;
}
for row in 0..SIZE {
for col in 0..SIZE {
let current = board[(row * SIZE) + col];
if col + 1 < SIZE && board[(row * SIZE) + (col + 1)] == current {
return true;
}
if row + 1 < SIZE && board[((row + 1) * SIZE) + col] == current {
return true;
}
}
}
false
}
fn apply_action(state: &mut Game2048State, action: &str) -> Result<(), &'static str> {
if action == "reset" {
state.board = create_fresh_board();
state.score = 0;
state.over = false;
return Ok(());
}
if state.over {
return Err("La partie est terminee. Utilise le bouton Reset.");
}
let (changed, gain) = match action {
"left" => move_left(&mut state.board),
"right" => move_right(&mut state.board),
"up" => move_up(&mut state.board),
"down" => move_down(&mut state.board),
_ => return Err("Action inconnue."),
};
if !changed {
return Err("Coup impossible dans cette direction.");
}
state.score = state.score.saturating_add(gain);
let _ = spawn_random_tile(&mut state.board);
state.over = !has_possible_moves(&state.board);
Ok(())
}
fn board_text(board: &[u16]) -> String {
let mut lines = Vec::new();
for row in 0..SIZE {
let mut cells = Vec::new();
for col in 0..SIZE {
let value = board[(row * SIZE) + col];
if value == 0 {
cells.push(format!("{:>5}", "."));
} else {
cells.push(format!("{:>5}", value));
}
}
lines.push(cells.join(" "));
}
lines.join("\n")
}
fn game_components(session_id: i64, over: bool) -> Vec<CreateActionRow> {
vec![
CreateActionRow::Buttons(vec![
CreateButton::new(format!("{}:{}:{}", GAME_PREFIX, session_id, "up"))
.label("Haut")
.style(ButtonStyle::Primary)
.disabled(over),
CreateButton::new(format!("{}:{}:{}", GAME_PREFIX, session_id, "reset"))
.label("Reset")
.style(ButtonStyle::Danger),
]),
CreateActionRow::Buttons(vec![
CreateButton::new(format!("{}:{}:{}", GAME_PREFIX, session_id, "left"))
.label("Gauche")
.style(ButtonStyle::Primary)
.disabled(over),
CreateButton::new(format!("{}:{}:{}", GAME_PREFIX, session_id, "down"))
.label("Bas")
.style(ButtonStyle::Primary)
.disabled(over),
CreateButton::new(format!("{}:{}:{}", GAME_PREFIX, session_id, "right"))
.label("Droite")
.style(ButtonStyle::Primary)
.disabled(over),
]),
]
}
fn game_embed(session_id: i64, state: &Game2048State, color: u32) -> CreateEmbed {
CreateEmbed::new()
.title("2048 interactif")
.description(format!(
"Session `#{}`\n\n```\n{}\n```",
session_id,
board_text(&state.board)
))
.field("Score", state.score.to_string(), true)
.field(
"Etat",
if state.over { "Terminee" } else { "En cours" },
true,
)
.color(color)
}
async fn send_ephemeral(ctx: &Context, component: &ComponentInteraction, content: &str) {
let _ = component
.create_response(
&ctx.http,
CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new()
.content(content)
.ephemeral(true),
),
)
.await;
}
pub async fn handle_2048(ctx: &Context, msg: &Message, _args: &[&str]) {
let Some(pool) = pool(ctx).await else {
let _ = msg
.channel_id
.send_message(
&ctx.http,
CreateMessage::new().embed(
CreateEmbed::new()
.title("2048")
.description("Base de donnees indisponible, impossible de demarrer une session interactive.")
.color(0xED4245),
),
)
.await;
return;
};
let owner_id = msg.author.id.get() as i64;
let state = Game2048State {
board: create_fresh_board(),
owner_id,
score: 0,
over: false,
};
let participants_json =
serde_json::to_string(&vec![owner_id]).unwrap_or_else(|_| "[]".to_string());
let state_json = serde_json::to_string(&state).unwrap_or_else(|_| "{}".to_string());
let bot_id = ctx.cache.current_user().id.get() as i64;
let Ok(session) = db::create_game_session(
&pool,
bot_id,
msg.guild_id.map(|id| id.get() as i64),
msg.channel_id.get() as i64,
owner_id,
GAME_KIND,
&participants_json,
&state_json,
)
.await
else {
return;
};
let color = theme_color(ctx).await;
let sent = msg
.channel_id
.send_message(
&ctx.http,
CreateMessage::new()
.embed(game_embed(session.id, &state, color))
.components(game_components(session.id, state.over)),
)
.await;
if let Ok(message) = sent {
let _ = db::set_game_session_message(&pool, session.id, message.id.get() as i64).await;
}
}
pub async fn handle_component_interaction(ctx: &Context, component: &ComponentInteraction) -> bool {
if !component.data.custom_id.starts_with(GAME_PREFIX) {
return false;
}
let Some((session_id, action)) = parse_component_id(&component.data.custom_id) else {
return false;
};
let Some(pool) = pool(ctx).await else {
send_ephemeral(ctx, component, "Base de donnees indisponible.").await;
return true;
};
let Ok(Some(session)) = db::get_game_session(&pool, session_id).await else {
send_ephemeral(ctx, component, "Session introuvable.").await;
return true;
};
if session.game_type != GAME_KIND {
return false;
}
let Ok(mut state) = serde_json::from_str::<Game2048State>(&session.state_json) else {
send_ephemeral(ctx, component, "Etat de session invalide.").await;
return true;
};
let actor_id = component.user.id.get() as i64;
if actor_id != state.owner_id {
send_ephemeral(ctx, component, "Seul le createur de la partie peut jouer.").await;
return true;
}
if let Err(message) = apply_action(&mut state, &action) {
send_ephemeral(ctx, component, message).await;
return true;
}
let status = if state.over { "finished" } else { "active" };
let state_json = serde_json::to_string(&state).unwrap_or_else(|_| session.state_json.clone());
let _ = db::update_game_session_state(&pool, session.id, &state_json, status).await;
let color = theme_color(ctx).await;
let _ = component
.create_response(
&ctx.http,
CreateInteractionResponse::UpdateMessage(
CreateInteractionResponseMessage::new()
.embed(game_embed(session.id, &state, color))
.components(game_components(session.id, state.over)),
),
)
.await;
true
}
pub struct Game2048Command;
pub static COMMAND_DESCRIPTOR: Game2048Command = Game2048Command;
impl crate::commands::command_contract::CommandSpec for Game2048Command {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "2048",
category: "game",
params: "aucun",
description: "Jouer au jeu 2048.",
examples: &["+2048"],
default_aliases: &["g2048"],
allow_in_dm: true,
default_permission: 0,
}
}
}
+57
View File
@@ -0,0 +1,57 @@
use rand::seq::SliceRandom;
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{send_embed, theme_color};
pub async fn handle_guesspokemon(ctx: &Context, msg: &Message, _args: &[&str]) {
let pokemons = [
"pikachu",
"bulbizarre",
"salameche",
"carapuce",
"evoli",
"dracaufeu",
];
let name = {
let mut rng = rand::thread_rng();
pokemons.choose(&mut rng).copied().unwrap_or("pikachu")
};
let hint = format!(
"{}{}",
&name[0..1],
"_".repeat(name.chars().count().saturating_sub(1))
);
let embed = CreateEmbed::new()
.title("GuessPokemon")
.description(format!(
"Qui est ce pokemon ?\nIndice: **{}** ({} lettres)",
hint,
name.chars().count()
))
.field("Reponse", "Le premier a donner le bon nom gagne.", false)
.color(theme_color(ctx).await);
send_embed(ctx, msg, embed).await;
}
pub struct GuesspokemonCommand;
pub static COMMAND_DESCRIPTOR: GuesspokemonCommand = GuesspokemonCommand;
impl crate::commands::command_contract::CommandSpec for GuesspokemonCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "guesspokemon",
category: "game",
params: "aucun",
description: "Jouer au jeu trouver le pokemon.",
examples: &["+guesspokemon"],
default_aliases: &["pokemon"],
allow_in_dm: true,
default_permission: 0,
}
}
}
+44
View File
@@ -0,0 +1,44 @@
use chrono::{Datelike, NaiveDate, Utc};
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{send_embed, theme_color};
pub async fn handle_halloween(ctx: &Context, msg: &Message, _args: &[&str]) {
let today = Utc::now().date_naive();
let mut year = today.year();
let mut target = NaiveDate::from_ymd_opt(year, 10, 31).unwrap_or(today);
if today > target {
year += 1;
target = NaiveDate::from_ymd_opt(year, 10, 31).unwrap_or(today);
}
let days = (target - today).num_days();
let embed = CreateEmbed::new()
.title("Halloween")
.description(format!("Il reste **{}** jour(s) avant Halloween.", days))
.color(theme_color(ctx).await);
send_embed(ctx, msg, embed).await;
}
pub struct HalloweenCommand;
pub static COMMAND_DESCRIPTOR: HalloweenCommand = HalloweenCommand;
impl crate::commands::command_contract::CommandSpec for HalloweenCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "halloween",
category: "game",
params: "aucun",
description: "Calcule le nombre de jours jusqu'a Halloween.",
examples: &["+halloween"],
default_aliases: &["spooky"],
allow_in_dm: true,
default_permission: 0,
}
}
}
+51
View File
@@ -0,0 +1,51 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{mention_user, send_embed, theme_color};
pub async fn handle_kiss(ctx: &Context, msg: &Message, args: &[&str]) {
let target = msg
.mentions
.first()
.map(|user| mention_user(user.id))
.or_else(|| {
args.first().map(|raw| {
if raw.starts_with("<@") {
raw.to_string()
} else {
format!("**{}**", raw)
}
})
})
.unwrap_or_else(|| mention_user(msg.author.id));
let embed = CreateEmbed::new()
.title("Kiss")
.description(format!(
"{} fait un bisou a {}.",
mention_user(msg.author.id),
target
))
.color(theme_color(ctx).await);
send_embed(ctx, msg, embed).await;
}
pub struct KissCommand;
pub static COMMAND_DESCRIPTOR: KissCommand = KissCommand;
impl crate::commands::command_contract::CommandSpec for KissCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "kiss",
category: "game",
params: "[@user]",
description: "Fait un bisou a un utilisateur mentionne ou a un utilisateur.",
examples: &["+kiss", "+kiss @Pseudo"],
default_aliases: &["bisou"],
allow_in_dm: true,
default_permission: 0,
}
}
}
+59
View File
@@ -0,0 +1,59 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{mention_user, send_embed, theme_color};
pub async fn handle_marry(ctx: &Context, msg: &Message, args: &[&str]) {
let target = msg
.mentions
.first()
.map(|user| mention_user(user.id))
.or_else(|| {
args.first().map(|raw| {
if raw.starts_with("<@") {
raw.to_string()
} else {
format!("**{}**", raw)
}
})
});
let Some(target) = target else {
let embed = CreateEmbed::new()
.title("Marry")
.description("Utilise `+marry <@user>` pour faire une demande.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let embed = CreateEmbed::new()
.title("Marry")
.description(format!(
"{} propose en mariage a {}. Reponse attendue dans le chat.",
mention_user(msg.author.id),
target
))
.color(theme_color(ctx).await);
send_embed(ctx, msg, embed).await;
}
pub struct MarryCommand;
pub static COMMAND_DESCRIPTOR: MarryCommand = MarryCommand;
impl crate::commands::command_contract::CommandSpec for MarryCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "marry",
category: "game",
params: "<@user>",
description: "Proposez en mariage a un utilisateur.",
examples: &["+marry @Pseudo"],
default_aliases: &["proposal"],
allow_in_dm: true,
default_permission: 0,
}
}
}
+448
View File
@@ -0,0 +1,448 @@
use rand::seq::SliceRandom;
use serde::{Deserialize, Serialize};
use serenity::all::{ButtonStyle, ComponentInteraction};
use serenity::builder::{
CreateActionRow, CreateButton, CreateEmbed, CreateInteractionResponse,
CreateInteractionResponseMessage, CreateMessage,
};
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::theme_color;
use crate::db;
const GAME_KIND: &str = "morpion";
const GAME_PREFIX: &str = "game:morpion";
#[derive(Debug, Clone, Serialize, Deserialize)]
struct MorpionState {
board: Vec<u8>,
player_x: i64,
player_o: i64,
current_turn: i64,
winner: i64,
moves: u8,
vs_bot: bool,
}
async fn pool(ctx: &Context) -> Option<sqlx::PgPool> {
let data = ctx.data.read().await;
data.get::<db::DbPoolKey>().cloned()
}
fn parse_component_id(custom_id: &str) -> Option<(i64, usize)> {
let mut parts = custom_id.split(':');
let scope = parts.next()?;
let game = parts.next()?;
let session_id = parts.next()?.parse::<i64>().ok()?;
let cell = parts.next()?.parse::<usize>().ok()?;
if scope != "game" || game != "morpion" || cell >= 9 || parts.next().is_some() {
return None;
}
Some((session_id, cell))
}
fn player_name(id: i64) -> String {
if id == 0 {
"Bot".to_string()
} else {
format!("<@{}>", id)
}
}
fn render_board(state: &MorpionState) -> String {
let mut rows = Vec::new();
for row in 0..3 {
let mut values = Vec::new();
for col in 0..3 {
let index = row * 3 + col;
let value = state.board[index];
let label = match value {
1 => "X".to_string(),
2 => "O".to_string(),
_ => (index + 1).to_string(),
};
values.push(label);
}
rows.push(values.join(" | "));
}
rows.join("\n---------\n")
}
fn has_winner(board: &[u8], mark: u8) -> bool {
const PATTERNS: [[usize; 3]; 8] = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
PATTERNS
.iter()
.any(|pattern| pattern.iter().all(|index| board[*index] == mark))
}
fn available_cells(board: &[u8]) -> Vec<usize> {
board
.iter()
.enumerate()
.filter_map(|(index, value)| if *value == 0 { Some(index) } else { None })
.collect()
}
fn apply_player_move(
state: &mut MorpionState,
cell: usize,
actor_id: i64,
) -> Result<(), &'static str> {
if state.winner != 0 {
return Err("La partie est deja terminee.");
}
if state.current_turn != actor_id {
return Err("Ce n'est pas ton tour.");
}
if state.board.get(cell).copied().unwrap_or(9) != 0 {
return Err("Cette case est deja occupee.");
}
let mark = if actor_id == state.player_x {
1
} else if actor_id == state.player_o {
2
} else {
return Err("Tu n'es pas un joueur de cette partie.");
};
state.board[cell] = mark;
state.moves = state.moves.saturating_add(1);
if has_winner(&state.board, mark) {
state.winner = if state.vs_bot && mark == 2 {
-2
} else {
actor_id
};
state.current_turn = 0;
return Ok(());
}
if state.moves >= 9 {
state.winner = -1;
state.current_turn = 0;
return Ok(());
}
state.current_turn = if actor_id == state.player_x {
state.player_o
} else {
state.player_x
};
Ok(())
}
fn apply_bot_move(state: &mut MorpionState) {
if !state.vs_bot || state.winner != 0 || state.current_turn != 0 {
return;
}
let empties = available_cells(&state.board);
let bot_cell = {
let mut rng = rand::thread_rng();
empties.choose(&mut rng).copied()
};
let Some(cell) = bot_cell else {
state.winner = -1;
return;
};
state.board[cell] = 2;
state.moves = state.moves.saturating_add(1);
if has_winner(&state.board, 2) {
state.winner = -2;
state.current_turn = 0;
return;
}
if state.moves >= 9 {
state.winner = -1;
state.current_turn = 0;
return;
}
state.current_turn = state.player_x;
}
fn game_components(session_id: i64, state: &MorpionState) -> Vec<CreateActionRow> {
let mut rows = Vec::new();
for row in 0..3 {
let mut buttons = Vec::new();
for col in 0..3 {
let index = row * 3 + col;
let value = state.board[index];
let label = match value {
1 => "X".to_string(),
2 => "O".to_string(),
_ => (index + 1).to_string(),
};
let style = match value {
1 => ButtonStyle::Danger,
2 => ButtonStyle::Success,
_ => ButtonStyle::Secondary,
};
let disabled = state.winner != 0 || value != 0;
buttons.push(
CreateButton::new(format!("{}:{}:{}", GAME_PREFIX, session_id, index))
.label(label)
.style(style)
.disabled(disabled),
);
}
rows.push(CreateActionRow::Buttons(buttons));
}
rows
}
fn game_embed(session_id: i64, state: &MorpionState, color: u32) -> CreateEmbed {
let status = if state.winner == 0 {
format!("Tour de {}.", player_name(state.current_turn))
} else if state.winner == -1 {
"Match nul.".to_string()
} else if state.winner == -2 {
"Le bot gagne.".to_string()
} else {
format!("Victoire de {}.", player_name(state.winner))
};
CreateEmbed::new()
.title("Morpion interactif")
.description(format!(
"Session `#{}`\n\n```\n{}\n```",
session_id,
render_board(state)
))
.field("Joueur X", player_name(state.player_x), true)
.field("Joueur O", player_name(state.player_o), true)
.field("Etat", status, false)
.color(color)
}
async fn send_ephemeral(ctx: &Context, component: &ComponentInteraction, content: &str) {
let _ = component
.create_response(
&ctx.http,
CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new()
.content(content)
.ephemeral(true),
),
)
.await;
}
pub async fn handle_morpion(ctx: &Context, msg: &Message, _args: &[&str]) {
let Some(pool) = pool(ctx).await else {
let _ = msg
.channel_id
.send_message(
&ctx.http,
CreateMessage::new().embed(
CreateEmbed::new()
.title("Morpion")
.description("Base de donnees indisponible, impossible de demarrer une session interactive.")
.color(0xED4245),
),
)
.await;
return;
};
let player_x = msg.author.id.get() as i64;
let player_o = msg
.mentions
.first()
.filter(|user| user.id != msg.author.id)
.map(|user| user.id.get() as i64)
.unwrap_or(0);
let vs_bot = player_o == 0;
let state = MorpionState {
board: vec![0; 9],
player_x,
player_o,
current_turn: player_x,
winner: 0,
moves: 0,
vs_bot,
};
let participants = if vs_bot {
vec![player_x]
} else {
vec![player_x, player_o]
};
let participants_json =
serde_json::to_string(&participants).unwrap_or_else(|_| "[]".to_string());
let state_json = serde_json::to_string(&state).unwrap_or_else(|_| "{}".to_string());
let bot_id = ctx.cache.current_user().id.get() as i64;
let Ok(session) = db::create_game_session(
&pool,
bot_id,
msg.guild_id.map(|id| id.get() as i64),
msg.channel_id.get() as i64,
player_x,
GAME_KIND,
&participants_json,
&state_json,
)
.await
else {
return;
};
let color = theme_color(ctx).await;
let sent = msg
.channel_id
.send_message(
&ctx.http,
CreateMessage::new()
.embed(game_embed(session.id, &state, color))
.components(game_components(session.id, &state)),
)
.await;
if let Ok(message) = sent {
let _ = db::set_game_session_message(&pool, session.id, message.id.get() as i64).await;
}
}
pub async fn handle_component_interaction(ctx: &Context, component: &ComponentInteraction) -> bool {
if !component.data.custom_id.starts_with(GAME_PREFIX) {
return false;
}
let Some((session_id, cell)) = parse_component_id(&component.data.custom_id) else {
return false;
};
let Some(pool) = pool(ctx).await else {
send_ephemeral(ctx, component, "Base de donnees indisponible.").await;
return true;
};
let Ok(Some(session)) = db::get_game_session(&pool, session_id).await else {
send_ephemeral(ctx, component, "Session introuvable.").await;
return true;
};
if session.game_type != GAME_KIND {
return false;
}
let Ok(mut state) = serde_json::from_str::<MorpionState>(&session.state_json) else {
send_ephemeral(ctx, component, "Etat de session invalide.").await;
return true;
};
if session.status != "active" || state.winner != 0 {
let color = theme_color(ctx).await;
let _ = component
.create_response(
&ctx.http,
CreateInteractionResponse::UpdateMessage(
CreateInteractionResponseMessage::new()
.embed(game_embed(session.id, &state, color))
.components(game_components(session.id, &state)),
),
)
.await;
return true;
}
let actor_id = component.user.id.get() as i64;
if state.vs_bot {
if actor_id != state.player_x {
send_ephemeral(
ctx,
component,
"Seul le createur de la partie peut jouer contre le bot.",
)
.await;
return true;
}
} else if actor_id != state.current_turn {
send_ephemeral(ctx, component, "Ce n'est pas ton tour.").await;
return true;
}
if let Err(error) = apply_player_move(&mut state, cell, actor_id) {
send_ephemeral(ctx, component, error).await;
return true;
}
if state.vs_bot && state.winner == 0 {
state.current_turn = 0;
apply_bot_move(&mut state);
}
let status = if state.winner == 0 {
"active"
} else {
"finished"
};
let state_json = serde_json::to_string(&state).unwrap_or_else(|_| session.state_json.clone());
let _ = db::update_game_session_state(&pool, session.id, &state_json, status).await;
let color = theme_color(ctx).await;
let _ = component
.create_response(
&ctx.http,
CreateInteractionResponse::UpdateMessage(
CreateInteractionResponseMessage::new()
.embed(game_embed(session.id, &state, color))
.components(game_components(session.id, &state)),
),
)
.await;
true
}
pub struct MorpionCommand;
pub static COMMAND_DESCRIPTOR: MorpionCommand = MorpionCommand;
impl crate::commands::command_contract::CommandSpec for MorpionCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "morpion",
category: "game",
params: "aucun",
description: "Jouer a morpion.",
examples: &["+morpion"],
default_aliases: &["tic", "tactoe"],
allow_in_dm: true,
default_permission: 0,
}
}
}
+69
View File
@@ -0,0 +1,69 @@
use rand::seq::SliceRandom;
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{send_embed, theme_color};
pub async fn handle_pendu(ctx: &Context, msg: &Message, _args: &[&str]) {
let words = [
"discord",
"shadowbot",
"rust",
"moderation",
"serenity",
"serveur",
];
let word = {
let mut rng = rand::thread_rng();
words.choose(&mut rng).copied().unwrap_or("rust")
};
let mut chars = word.chars().collect::<Vec<_>>();
if chars.len() > 2 {
for index in 1..chars.len() - 1 {
chars[index] = '_';
}
}
let masked = chars
.iter()
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join(" ");
let embed = CreateEmbed::new()
.title("Pendu")
.description(format!(
"Mot mystere: **{}**\nIndice: {} lettres.",
masked,
word.chars().count()
))
.field(
"Astuce",
"Tu peux jouer en discutant les propositions dans le salon.",
false,
)
.color(theme_color(ctx).await);
send_embed(ctx, msg, embed).await;
}
pub struct PenduCommand;
pub static COMMAND_DESCRIPTOR: PenduCommand = PenduCommand;
impl crate::commands::command_contract::CommandSpec for PenduCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "pendu",
category: "game",
params: "aucun",
description: "Jouer au jeu du pendu.",
examples: &["+pendu"],
default_aliases: &["hangman"],
allow_in_dm: true,
default_permission: 0,
}
}
}
+73
View File
@@ -0,0 +1,73 @@
use rand::seq::SliceRandom;
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{send_embed, theme_color};
pub async fn handle_pfc(ctx: &Context, msg: &Message, args: &[&str]) {
let choices = ["pierre", "papier", "ciseaux"];
if args.is_empty() {
let embed = CreateEmbed::new()
.title("Pierre Papier Ciseaux")
.description("Utilise `+pfc <pierre|papier|ciseaux>` pour jouer.")
.color(theme_color(ctx).await);
send_embed(ctx, msg, embed).await;
return;
}
let player = args[0].to_lowercase();
if !choices.iter().any(|choice| player == *choice) {
let embed = CreateEmbed::new()
.title("PFC")
.description("Choix invalide. Valeurs attendues: pierre, papier ou ciseaux.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let bot = {
let mut rng = rand::thread_rng();
choices.choose(&mut rng).copied().unwrap_or("pierre")
};
let result = if player == bot {
"Egalite."
} else if (player == "pierre" && bot == "ciseaux")
|| (player == "papier" && bot == "pierre")
|| (player == "ciseaux" && bot == "papier")
{
"Tu gagnes."
} else {
"Tu perds."
};
let embed = CreateEmbed::new()
.title("PFC")
.description(format!(
"Ton choix: **{}**\nChoix du bot: **{}**\n\n{}",
player, bot, result
))
.color(theme_color(ctx).await);
send_embed(ctx, msg, embed).await;
}
pub struct PfcCommand;
pub static COMMAND_DESCRIPTOR: PfcCommand = PfcCommand;
impl crate::commands::command_contract::CommandSpec for PfcCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "pfc",
category: "game",
params: "<pierre|papier|ciseaux>",
description: "Jouer a pierre-papier-ciseaux.",
examples: &["+pfc pierre"],
default_aliases: &["rps"],
allow_in_dm: true,
default_permission: 0,
}
}
}
+468
View File
@@ -0,0 +1,468 @@
use rand::seq::SliceRandom;
use serde::{Deserialize, Serialize};
use serenity::all::{ButtonStyle, ComponentInteraction};
use serenity::builder::{
CreateActionRow, CreateButton, CreateEmbed, CreateInteractionResponse,
CreateInteractionResponseMessage, CreateMessage,
};
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::theme_color;
use crate::db;
const GAME_KIND: &str = "puissance4";
const GAME_PREFIX: &str = "game:p4";
const WIDTH: usize = 7;
const HEIGHT: usize = 6;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Puissance4State {
board: Vec<u8>,
player_red: i64,
player_yellow: i64,
current_turn: i64,
winner: i64,
moves: u8,
vs_bot: bool,
}
async fn pool(ctx: &Context) -> Option<sqlx::PgPool> {
let data = ctx.data.read().await;
data.get::<db::DbPoolKey>().cloned()
}
fn index(row: usize, col: usize) -> usize {
row * WIDTH + col
}
fn parse_component_id(custom_id: &str) -> Option<(i64, usize)> {
let mut parts = custom_id.split(':');
let scope = parts.next()?;
let game = parts.next()?;
let session_id = parts.next()?.parse::<i64>().ok()?;
let col = parts.next()?.parse::<usize>().ok()?;
if scope != "game" || game != "p4" || col >= WIDTH || parts.next().is_some() {
return None;
}
Some((session_id, col))
}
fn player_name(id: i64) -> String {
if id == 0 {
"Bot".to_string()
} else {
format!("<@{}>", id)
}
}
fn board_text(state: &Puissance4State) -> String {
let mut lines = Vec::new();
for row in 0..HEIGHT {
let mut values = Vec::new();
for col in 0..WIDTH {
let value = state.board[index(row, col)];
let cell = match value {
1 => "R",
2 => "Y",
_ => ".",
};
values.push(cell.to_string());
}
lines.push(format!("| {} |", values.join(" ")));
}
lines.push(" 1 2 3 4 5 6 7".to_string());
lines.join("\n")
}
fn drop_piece(board: &mut [u8], col: usize, mark: u8) -> Option<usize> {
for row in (0..HEIGHT).rev() {
let idx = index(row, col);
if board[idx] == 0 {
board[idx] = mark;
return Some(row);
}
}
None
}
fn check_winner_at(board: &[u8], row: usize, col: usize, mark: u8) -> bool {
let directions = [(1isize, 0isize), (0, 1), (1, 1), (1, -1)];
for (dr, dc) in directions {
let mut count = 1;
for sign in [-1isize, 1isize] {
let mut r = row as isize;
let mut c = col as isize;
loop {
r += dr * sign;
c += dc * sign;
if r < 0 || r >= HEIGHT as isize || c < 0 || c >= WIDTH as isize {
break;
}
if board[index(r as usize, c as usize)] != mark {
break;
}
count += 1;
}
}
if count >= 4 {
return true;
}
}
false
}
fn valid_columns(board: &[u8]) -> Vec<usize> {
(0..WIDTH)
.filter(|col| board[index(0, *col)] == 0)
.collect::<Vec<_>>()
}
fn apply_turn(state: &mut Puissance4State, col: usize, actor_id: i64) -> Result<(), &'static str> {
if state.winner != 0 {
return Err("La partie est deja terminee.");
}
if state.current_turn != actor_id {
return Err("Ce n'est pas ton tour.");
}
let mark = if actor_id == state.player_red {
1
} else if actor_id == state.player_yellow {
2
} else {
return Err("Tu n'es pas un joueur de cette partie.");
};
let Some(row) = drop_piece(&mut state.board, col, mark) else {
return Err("Cette colonne est pleine.");
};
state.moves = state.moves.saturating_add(1);
if check_winner_at(&state.board, row, col, mark) {
state.winner = if state.vs_bot && mark == 2 {
-2
} else {
actor_id
};
state.current_turn = 0;
return Ok(());
}
if state.moves as usize >= WIDTH * HEIGHT {
state.winner = -1;
state.current_turn = 0;
return Ok(());
}
state.current_turn = if actor_id == state.player_red {
state.player_yellow
} else {
state.player_red
};
Ok(())
}
fn apply_bot_turn(state: &mut Puissance4State) {
if !state.vs_bot || state.winner != 0 || state.current_turn != 0 {
return;
}
let valid = valid_columns(&state.board);
let col = {
let mut rng = rand::thread_rng();
valid.choose(&mut rng).copied()
};
let Some(col) = col else {
state.winner = -1;
return;
};
let Some(row) = drop_piece(&mut state.board, col, 2) else {
state.winner = -1;
return;
};
state.moves = state.moves.saturating_add(1);
if check_winner_at(&state.board, row, col, 2) {
state.winner = -2;
state.current_turn = 0;
return;
}
if state.moves as usize >= WIDTH * HEIGHT {
state.winner = -1;
state.current_turn = 0;
return;
}
state.current_turn = state.player_red;
}
fn game_components(session_id: i64, state: &Puissance4State) -> Vec<CreateActionRow> {
let valid = valid_columns(&state.board);
let mut first_row = Vec::new();
let mut second_row = Vec::new();
for col in 0..WIDTH {
let button = CreateButton::new(format!("{}:{}:{}", GAME_PREFIX, session_id, col))
.label((col + 1).to_string())
.style(ButtonStyle::Primary)
.disabled(state.winner != 0 || !valid.contains(&col));
if col < 4 {
first_row.push(button);
} else {
second_row.push(button);
}
}
vec![
CreateActionRow::Buttons(first_row),
CreateActionRow::Buttons(second_row),
]
}
fn game_embed(session_id: i64, state: &Puissance4State, color: u32) -> CreateEmbed {
let status = if state.winner == 0 {
format!("Tour de {}.", player_name(state.current_turn))
} else if state.winner == -1 {
"Match nul.".to_string()
} else if state.winner == -2 {
"Le bot gagne.".to_string()
} else {
format!("Victoire de {}.", player_name(state.winner))
};
CreateEmbed::new()
.title("Puissance4 interactif")
.description(format!(
"Session `#{}`\n\n```\n{}\n```",
session_id,
board_text(state)
))
.field("Rouge", player_name(state.player_red), true)
.field("Jaune", player_name(state.player_yellow), true)
.field("Etat", status, false)
.color(color)
}
async fn send_ephemeral(ctx: &Context, component: &ComponentInteraction, content: &str) {
let _ = component
.create_response(
&ctx.http,
CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new()
.content(content)
.ephemeral(true),
),
)
.await;
}
pub async fn handle_puissance4(ctx: &Context, msg: &Message, _args: &[&str]) {
let Some(pool) = pool(ctx).await else {
let _ = msg
.channel_id
.send_message(
&ctx.http,
CreateMessage::new().embed(
CreateEmbed::new()
.title("Puissance4")
.description("Base de donnees indisponible, impossible de demarrer une session interactive.")
.color(0xED4245),
),
)
.await;
return;
};
let player_red = msg.author.id.get() as i64;
let player_yellow = msg
.mentions
.first()
.filter(|user| user.id != msg.author.id)
.map(|user| user.id.get() as i64)
.unwrap_or(0);
let vs_bot = player_yellow == 0;
let state = Puissance4State {
board: vec![0; WIDTH * HEIGHT],
player_red,
player_yellow,
current_turn: player_red,
winner: 0,
moves: 0,
vs_bot,
};
let participants = if vs_bot {
vec![player_red]
} else {
vec![player_red, player_yellow]
};
let participants_json =
serde_json::to_string(&participants).unwrap_or_else(|_| "[]".to_string());
let state_json = serde_json::to_string(&state).unwrap_or_else(|_| "{}".to_string());
let bot_id = ctx.cache.current_user().id.get() as i64;
let Ok(session) = db::create_game_session(
&pool,
bot_id,
msg.guild_id.map(|id| id.get() as i64),
msg.channel_id.get() as i64,
player_red,
GAME_KIND,
&participants_json,
&state_json,
)
.await
else {
return;
};
let color = theme_color(ctx).await;
let sent = msg
.channel_id
.send_message(
&ctx.http,
CreateMessage::new()
.embed(game_embed(session.id, &state, color))
.components(game_components(session.id, &state)),
)
.await;
if let Ok(message) = sent {
let _ = db::set_game_session_message(&pool, session.id, message.id.get() as i64).await;
}
}
pub async fn handle_component_interaction(ctx: &Context, component: &ComponentInteraction) -> bool {
if !component.data.custom_id.starts_with(GAME_PREFIX) {
return false;
}
let Some((session_id, col)) = parse_component_id(&component.data.custom_id) else {
return false;
};
let Some(pool) = pool(ctx).await else {
send_ephemeral(ctx, component, "Base de donnees indisponible.").await;
return true;
};
let Ok(Some(session)) = db::get_game_session(&pool, session_id).await else {
send_ephemeral(ctx, component, "Session introuvable.").await;
return true;
};
if session.game_type != GAME_KIND {
return false;
}
let Ok(mut state) = serde_json::from_str::<Puissance4State>(&session.state_json) else {
send_ephemeral(ctx, component, "Etat de session invalide.").await;
return true;
};
if session.status != "active" || state.winner != 0 {
let color = theme_color(ctx).await;
let _ = component
.create_response(
&ctx.http,
CreateInteractionResponse::UpdateMessage(
CreateInteractionResponseMessage::new()
.embed(game_embed(session.id, &state, color))
.components(game_components(session.id, &state)),
),
)
.await;
return true;
}
let actor_id = component.user.id.get() as i64;
if state.vs_bot {
if actor_id != state.player_red {
send_ephemeral(
ctx,
component,
"Seul le createur de la partie peut jouer contre le bot.",
)
.await;
return true;
}
} else if actor_id != state.current_turn {
send_ephemeral(ctx, component, "Ce n'est pas ton tour.").await;
return true;
}
if let Err(error) = apply_turn(&mut state, col, actor_id) {
send_ephemeral(ctx, component, error).await;
return true;
}
if state.vs_bot && state.winner == 0 {
state.current_turn = 0;
apply_bot_turn(&mut state);
}
let status = if state.winner == 0 {
"active"
} else {
"finished"
};
let state_json = serde_json::to_string(&state).unwrap_or_else(|_| session.state_json.clone());
let _ = db::update_game_session_state(&pool, session.id, &state_json, status).await;
let color = theme_color(ctx).await;
let _ = component
.create_response(
&ctx.http,
CreateInteractionResponse::UpdateMessage(
CreateInteractionResponseMessage::new()
.embed(game_embed(session.id, &state, color))
.components(game_components(session.id, &state)),
),
)
.await;
true
}
pub struct Puissance4Command;
pub static COMMAND_DESCRIPTOR: Puissance4Command = Puissance4Command;
impl crate::commands::command_contract::CommandSpec for Puissance4Command {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "puissance4",
category: "game",
params: "aucun",
description: "Lancer une partie de puissance4.",
examples: &["+puissance4"],
default_aliases: &["connect4", "p4"],
allow_in_dm: true,
default_permission: 0,
}
}
}
+43
View File
@@ -0,0 +1,43 @@
use serenity::builder::{CreateActionRow, CreateButton, CreateEmbed, CreateMessage};
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::theme_color;
pub async fn handle_rickroll(ctx: &Context, msg: &Message, _args: &[&str]) {
let url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ";
let embed = CreateEmbed::new()
.title("Rickroll")
.description("Never gonna give you up.")
.color(theme_color(ctx).await);
let _ = msg
.channel_id
.send_message(
&ctx.http,
CreateMessage::new()
.embed(embed)
.components(vec![CreateActionRow::Buttons(vec![
CreateButton::new_link(url).label("Ouvrir la video"),
])]),
)
.await;
}
pub struct RickrollCommand;
pub static COMMAND_DESCRIPTOR: RickrollCommand = RickrollCommand;
impl crate::commands::command_contract::CommandSpec for RickrollCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "rickroll",
category: "game",
params: "aucun",
description: "Never gonna give you up.",
examples: &["+rickroll"],
default_aliases: &["rr"],
allow_in_dm: true,
default_permission: 0,
}
}
}
+54
View File
@@ -0,0 +1,54 @@
use rand::seq::SliceRandom;
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{send_embed, theme_color};
pub async fn handle_slot(ctx: &Context, msg: &Message, _args: &[&str]) {
let symbols = ["7", "BAR", "STAR", "BELL", "CHERRY"];
let (a, b, c) = {
let mut rng = rand::thread_rng();
(
symbols.choose(&mut rng).copied().unwrap_or("7"),
symbols.choose(&mut rng).copied().unwrap_or("7"),
symbols.choose(&mut rng).copied().unwrap_or("7"),
)
};
let result = if a == b && b == c {
"Jackpot"
} else if a == b || b == c || a == c {
"Presque"
} else {
"Perdu"
};
let embed = CreateEmbed::new()
.title("Slot")
.description(format!(
"[ {} | {} | {} ]\nResultat: **{}**",
a, b, c, result
))
.color(theme_color(ctx).await);
send_embed(ctx, msg, embed).await;
}
pub struct SlotCommand;
pub static COMMAND_DESCRIPTOR: SlotCommand = SlotCommand;
impl crate::commands::command_contract::CommandSpec for SlotCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "slot",
category: "game",
params: "aucun",
description: "Jouer au jeu Slot.",
examples: &["+slot"],
default_aliases: &["machine"],
allow_in_dm: true,
default_permission: 0,
}
}
}
+52
View File
@@ -0,0 +1,52 @@
use rand::seq::SliceRandom;
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{send_embed, theme_color};
pub async fn handle_snake(ctx: &Context, msg: &Message, _args: &[&str]) {
let directions = ["haut", "bas", "gauche", "droite"];
let foods = ["pomme", "banane", "fraise", "citron"];
let (direction, food) = {
let mut rng = rand::thread_rng();
(
directions.choose(&mut rng).copied().unwrap_or("haut"),
foods.choose(&mut rng).copied().unwrap_or("pomme"),
)
};
let embed = CreateEmbed::new()
.title("Snake")
.description(format!(
"Partie lancee. Direction conseillee: **{}**.\nObjectif courant: attraper une **{}**.",
direction, food
))
.field(
"Commande rapide",
"Relance `+snake` pour un nouveau round.",
false,
)
.color(theme_color(ctx).await);
send_embed(ctx, msg, embed).await;
}
pub struct SnakeCommand;
pub static COMMAND_DESCRIPTOR: SnakeCommand = SnakeCommand;
impl crate::commands::command_contract::CommandSpec for SnakeCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "snake",
category: "game",
params: "aucun",
description: "Lancer une partie de snake.",
examples: &["+snake"],
default_aliases: &["snk"],
allow_in_dm: true,
default_permission: 0,
}
}
}
+39
View File
@@ -0,0 +1,39 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{mention_user, send_embed, theme_color};
pub async fn handle_unmarry(ctx: &Context, msg: &Message, _args: &[&str]) {
let author = mention_user(msg.author.id);
let target = msg
.mentions
.first()
.map(|user| mention_user(user.id))
.unwrap_or_else(|| "ton partenaire imaginaire".to_string());
let embed = CreateEmbed::new()
.title("Unmarry")
.description(format!("{} a dissous le mariage avec {}.", author, target))
.color(theme_color(ctx).await);
send_embed(ctx, msg, embed).await;
}
pub struct UnmarryCommand;
pub static COMMAND_DESCRIPTOR: UnmarryCommand = UnmarryCommand;
impl crate::commands::command_contract::CommandSpec for UnmarryCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "unmarry",
category: "game",
params: "[@user]",
description: "Dissoudre un mariage.",
examples: &["+unmarry", "+unmarry @Pseudo"],
default_aliases: &["divorce"],
allow_in_dm: true,
default_permission: 0,
}
}
}
+44
View File
@@ -0,0 +1,44 @@
use rand::seq::SliceRandom;
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{send_embed, theme_color};
pub async fn handle_wordle(ctx: &Context, msg: &Message, _args: &[&str]) {
let words = ["pomme", "ombre", "salon", "banjo", "pixel", "vocal"];
let secret = {
let mut rng = rand::thread_rng();
words.choose(&mut rng).copied().unwrap_or("pomme")
};
let hint = format!("{}{}", &secret[0..1], "_ _ _ _");
let embed = CreateEmbed::new()
.title("Wordle")
.description(format!(
"Mot secret de 5 lettres initialise.\nIndice: **{}**\n\nPropose un mot avec `+wordle <mot>` (version libre).",
hint
))
.color(theme_color(ctx).await);
send_embed(ctx, msg, embed).await;
}
pub struct WordleCommand;
pub static COMMAND_DESCRIPTOR: WordleCommand = WordleCommand;
impl crate::commands::command_contract::CommandSpec for WordleCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
name: "wordle",
category: "game",
params: "aucun",
description: "Jouer a Wordle.",
examples: &["+wordle"],
default_aliases: &["wd"],
allow_in_dm: true,
default_permission: 0,
}
}
}
+69
View File
@@ -74,6 +74,8 @@ pub mod bringall;
pub mod button;
#[path = "fun/calc.rs"]
pub mod calc;
#[path = "game/catsay.rs"]
pub mod catsay;
#[path = "botconfig/change.rs"]
pub mod change;
#[path = "botconfig/changeall.rs"]
@@ -84,8 +86,12 @@ pub mod changereset;
pub mod channel;
#[path = "fun/choose.rs"]
pub mod choose;
#[path = "game/christmas.rs"]
pub mod christmas;
#[path = "ticket/claim.rs"]
pub mod claim;
#[path = "game/claque.rs"]
pub mod claque;
#[path = "mod/cleanup.rs"]
pub mod cleanup;
#[path = "mod/clearallsanctions.rs"]
@@ -124,12 +130,16 @@ pub mod del_sanction;
pub mod delperm;
#[path = "roles/delrole.rs"]
pub mod delrole;
#[path = "game/demineur.rs"]
pub mod demineur;
#[path = "roles/derank.rs"]
pub mod derank;
#[path = "owner/discussion.rs"]
pub mod discussion;
#[path = "botconfig/dnd.rs"]
pub mod dnd;
#[path = "game/eightball.rs"]
pub mod eightball;
#[path = "fun/embed.rs"]
pub mod embed;
#[path = "fun/emoji.rs"]
@@ -138,8 +148,22 @@ pub mod emoji;
pub mod end;
#[path = "event/endgiveaway.rs"]
pub mod endgiveaway;
#[path = "game/epicgamer.rs"]
pub mod epicgamer;
#[path = "game/fasttype.rs"]
pub mod fasttype;
#[path = "game/findemoji.rs"]
pub mod findemoji;
#[path = "game/flood.rs"]
pub mod flood;
#[path = "game/g2048.rs"]
pub mod g2048;
#[path = "event/giveaway.rs"]
pub mod giveaway;
#[path = "game/guesspokemon.rs"]
pub mod guesspokemon;
#[path = "game/halloween.rs"]
pub mod halloween;
#[path = "perms/help.rs"]
pub mod help;
#[path = "perms/helpsetting.rs"]
@@ -162,6 +186,8 @@ pub mod invitereset;
pub mod join;
#[path = "mod/kick.rs"]
pub mod kick;
#[path = "game/kiss.rs"]
pub mod kiss;
#[path = "owner/leave.rs"]
pub mod leave;
#[path = "config/leavesettings.rs"]
@@ -182,6 +208,8 @@ pub mod logs_command_helpers;
pub mod logs_service;
#[path = "botconfig/mainprefix.rs"]
pub mod mainprefix;
#[path = "game/marry.rs"]
pub mod marry;
#[path = "roles/massiverole.rs"]
pub mod massiverole;
#[path = "info/member.rs"]
@@ -196,6 +224,8 @@ pub mod moderation_sanction_helpers;
pub mod moderation_tools;
#[path = "config/modlog.rs"]
pub mod modlog;
#[path = "game/morpion.rs"]
pub mod morpion;
#[path = "owner/mp.rs"]
pub mod mp;
#[path = "owner/mpdelete.rs"]
@@ -224,12 +254,16 @@ pub mod nolog;
pub mod online;
#[path = "owner/owner.rs"]
pub mod owner;
#[path = "game/pendu.rs"]
pub mod pendu;
#[path = "perms/perms.rs"]
pub mod perms;
#[path = "../utils/perms_helpers.rs"]
pub mod perms_helpers;
#[path = "../utils/perms_service.rs"]
pub mod perms_service;
#[path = "game/pfc.rs"]
pub mod pfc;
#[path = "info/pic.rs"]
pub mod pic;
#[path = "automation/piconly.rs"]
@@ -246,6 +280,8 @@ pub mod playto;
pub mod prefix;
#[path = "channel/public.rs"]
pub mod public;
#[path = "game/puissance4.rs"]
pub mod puissance4;
#[path = "mod/punish.rs"]
pub mod punish;
#[path = "mod/punishadd.rs"]
@@ -268,6 +304,8 @@ pub mod renew;
pub mod reroll;
#[path = "security/resetantiraide.rs"]
pub mod resetantiraide;
#[path = "game/rickroll.rs"]
pub mod rickroll;
#[path = "info/role.rs"]
pub mod role;
#[path = "config/rolelog.rs"]
@@ -312,8 +350,12 @@ pub mod setprofil;
pub mod shadowbot;
#[path = "info/showpics.rs"]
pub mod showpics;
#[path = "game/slot.rs"]
pub mod slot;
#[path = "channel/slowmode.rs"]
pub mod slowmode;
#[path = "game/snake.rs"]
pub mod snake;
#[path = "fun/snipe.rs"]
pub mod snipe;
#[path = "security/spam.rs"]
@@ -368,6 +410,8 @@ pub mod unhideall;
pub mod unlock;
#[path = "channel/unlockall.rs"]
pub mod unlockall;
#[path = "game/unmarry.rs"]
pub mod unmarry;
#[path = "roles/unmassiverole.rs"]
pub mod unmassiverole;
#[path = "mod/unmute.rs"]
@@ -394,6 +438,8 @@ pub mod voicemove;
pub mod warn;
#[path = "botconfig/watch.rs"]
pub mod watch;
#[path = "game/wordle.rs"]
pub mod wordle;
pub fn all_command_metadata() -> Vec<CommandMetadata> {
vec![
@@ -443,6 +489,29 @@ pub fn all_command_metadata() -> Vec<CommandMetadata> {
end::COMMAND_DESCRIPTOR.metadata(),
reroll::COMMAND_DESCRIPTOR.metadata(),
choose::COMMAND_DESCRIPTOR.metadata(),
g2048::COMMAND_DESCRIPTOR.metadata(),
snake::COMMAND_DESCRIPTOR.metadata(),
unmarry::COMMAND_DESCRIPTOR.metadata(),
pendu::COMMAND_DESCRIPTOR.metadata(),
pfc::COMMAND_DESCRIPTOR.metadata(),
flood::COMMAND_DESCRIPTOR.metadata(),
puissance4::COMMAND_DESCRIPTOR.metadata(),
morpion::COMMAND_DESCRIPTOR.metadata(),
epicgamer::COMMAND_DESCRIPTOR.metadata(),
demineur::COMMAND_DESCRIPTOR.metadata(),
catsay::COMMAND_DESCRIPTOR.metadata(),
claque::COMMAND_DESCRIPTOR.metadata(),
slot::COMMAND_DESCRIPTOR.metadata(),
fasttype::COMMAND_DESCRIPTOR.metadata(),
rickroll::COMMAND_DESCRIPTOR.metadata(),
kiss::COMMAND_DESCRIPTOR.metadata(),
wordle::COMMAND_DESCRIPTOR.metadata(),
findemoji::COMMAND_DESCRIPTOR.metadata(),
marry::COMMAND_DESCRIPTOR.metadata(),
guesspokemon::COMMAND_DESCRIPTOR.metadata(),
eightball::COMMAND_DESCRIPTOR.metadata(),
halloween::COMMAND_DESCRIPTOR.metadata(),
christmas::COMMAND_DESCRIPTOR.metadata(),
embed::COMMAND_DESCRIPTOR.metadata(),
clear_messages::COMMAND_DESCRIPTOR.metadata(),
clear_limit::COMMAND_DESCRIPTOR.metadata(),
+7
View File
@@ -116,6 +116,11 @@ const HELP_PAGES: &[HelpPage] = &[
title: "Outils",
description: "Giveaways, utilitaires, embeds et automatisations de contenu.",
},
HelpPage {
key: "game",
title: "Game",
description: "Jeux et commandes ludiques.",
},
HelpPage {
key: "bot",
title: "Bot & Présence",
@@ -177,6 +182,7 @@ fn help_page_for_command(
"roles" => "roles",
"salons_vocal" => "salons_vocal",
"outils" => "outils",
"game" => "game",
"bot" => "bot",
"administration" => "administration",
"permissions" => "permissions",
@@ -238,6 +244,7 @@ fn help_page_matches_input(page: &HelpPage, input: &str) -> bool {
"roles" => &["role", "roles"][..],
"salons_vocal" => &["salon", "salons", "vocal", "voice", "channels"][..],
"outils" => &["utilitaires", "tools", "giveaway"][..],
"game" => &["jeu", "jeux", "games", "fun"][..],
"bot" => &["profil", "presence", "activite", "activity"][..],
"administration" => &["admin", "admins"][..],
"permissions" => &["permission", "perms", "aide", "help"][..],
+172
View File
@@ -250,6 +250,23 @@ pub struct PunishRule {
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, sqlx::FromRow)]
#[allow(dead_code)]
pub struct GameSession {
pub id: i64,
pub bot_id: i64,
pub guild_id: Option<i64>,
pub channel_id: i64,
pub message_id: Option<i64>,
pub game_type: String,
pub owner_id: i64,
pub participants_json: String,
pub state_json: String,
pub status: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
pub async fn create_pool(database_url: &str) -> Result<PgPool, sqlx::Error> {
PgPoolOptions::new()
.max_connections(10)
@@ -1263,6 +1280,36 @@ pub async fn init_schema(pool: &PgPool) -> Result<(), sqlx::Error> {
.execute(pool)
.await?;
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS bot_game_sessions (
id BIGSERIAL PRIMARY KEY,
bot_id BIGINT NOT NULL,
guild_id BIGINT NULL,
channel_id BIGINT NOT NULL,
message_id BIGINT NULL,
game_type TEXT NOT NULL,
owner_id BIGINT NOT NULL,
participants_json TEXT NOT NULL DEFAULT '[]',
state_json TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
"#,
)
.execute(pool)
.await?;
sqlx::query(
r#"
CREATE INDEX IF NOT EXISTS idx_bot_game_sessions_lookup
ON bot_game_sessions (bot_id, game_type, status, updated_at DESC);
"#,
)
.execute(pool)
.await?;
Ok(())
}
@@ -4606,3 +4653,128 @@ pub async fn upsert_last_punish_triggered_at(
Ok(())
}
// ========== GAME SESSIONS FUNCTIONS ==========
pub async fn create_game_session(
pool: &PgPool,
bot_id: i64,
guild_id: Option<i64>,
channel_id: i64,
owner_id: i64,
game_type: &str,
participants_json: &str,
state_json: &str,
) -> Result<GameSession, sqlx::Error> {
let session = sqlx::query_as::<_, GameSession>(
r#"
INSERT INTO bot_game_sessions (
bot_id,
guild_id,
channel_id,
owner_id,
game_type,
participants_json,
state_json,
status
)
VALUES ($1, $2, $3, $4, $5, $6, $7, 'active')
RETURNING *;
"#,
)
.bind(bot_id)
.bind(guild_id)
.bind(channel_id)
.bind(owner_id)
.bind(game_type)
.bind(participants_json)
.bind(state_json)
.fetch_one(pool)
.await?;
Ok(session)
}
pub async fn set_game_session_message(
pool: &PgPool,
session_id: i64,
message_id: i64,
) -> Result<(), sqlx::Error> {
sqlx::query(
r#"
UPDATE bot_game_sessions
SET message_id = $1, updated_at = NOW()
WHERE id = $2;
"#,
)
.bind(message_id)
.bind(session_id)
.execute(pool)
.await?;
Ok(())
}
pub async fn get_game_session(
pool: &PgPool,
session_id: i64,
) -> Result<Option<GameSession>, sqlx::Error> {
let session = sqlx::query_as::<_, GameSession>(
r#"
SELECT *
FROM bot_game_sessions
WHERE id = $1
LIMIT 1;
"#,
)
.bind(session_id)
.fetch_optional(pool)
.await?;
Ok(session)
}
pub async fn update_game_session_state(
pool: &PgPool,
session_id: i64,
state_json: &str,
status: &str,
) -> Result<Option<GameSession>, sqlx::Error> {
let session = sqlx::query_as::<_, GameSession>(
r#"
UPDATE bot_game_sessions
SET state_json = $1, status = $2, updated_at = NOW()
WHERE id = $3
RETURNING *;
"#,
)
.bind(state_json)
.bind(status)
.bind(session_id)
.fetch_optional(pool)
.await?;
Ok(session)
}
#[allow(dead_code)]
pub async fn update_game_session_participants(
pool: &PgPool,
session_id: i64,
participants_json: &str,
) -> Result<Option<GameSession>, sqlx::Error> {
let session = sqlx::query_as::<_, GameSession>(
r#"
UPDATE bot_game_sessions
SET participants_json = $1, updated_at = NOW()
WHERE id = $2
RETURNING *;
"#,
)
.bind(participants_json)
.bind(session_id)
.fetch_optional(pool)
.await?;
Ok(session)
}
+14 -2
View File
@@ -2,8 +2,8 @@ use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::{
advanced_tools, ancien, autoconfiglog, boostembed, help, helpsetting, mp, perms_service,
rolemenu, suggestion, tempvoc, ticket, viewlogs,
advanced_tools, ancien, autoconfiglog, boostembed, g2048, help, helpsetting, morpion, mp,
perms_service, puissance4, rolemenu, suggestion, tempvoc, ticket, viewlogs,
};
pub async fn handle_interaction_create(ctx: &Context, interaction: &Interaction) {
@@ -42,6 +42,18 @@ pub async fn handle_interaction_create(ctx: &Context, interaction: &Interaction)
return;
}
if morpion::handle_component_interaction(ctx, component).await {
return;
}
if puissance4::handle_component_interaction(ctx, component).await {
return;
}
if g2048::handle_component_interaction(ctx, component).await {
return;
}
if help::handle_help_component(ctx, component).await {
return;
}
+41 -15
View File
@@ -9,22 +9,25 @@ use crate::commands::{
addinvite, addrole, alias, ancien, antilink, antimassmention, antiraideautoconfig, antispam,
autobackup, autoconfiglog, autopublish, autopublishoff, autopublishon, autoreact, backup,
badwords, ban, banlist, banner, bl, blinfo, boostembed, boosters, boostlog, bringall, button,
calc, change, changeall, changereset, channel, choose, claim, cleanup, clear_all_sanctions,
clear_badwords, clear_bl, clear_limit, clear_messages, clear_owners, clear_perms,
clear_sanctions, close, cmute, compet, create, del_sanction, delperm, delrole, derank,
discussion, dnd, embed, emoji, end, endgiveaway, giveaway, help, helpsetting, hide, hideall,
idle, invisible, invite, inviteboard, invitereset, join, kick, leave, leave_settings, link,
listen, loading, lock, lockall, mainprefix, massiverole, member, messagelog, modlog, mp,
mpdelete, mpsent, mpsettings, mute, mutelist, muterole, newsticker, noderank, noderankadd,
noderankdel, nolog, online, owner, perms, pic, piconly, piconlyadd, piconlydel, ping, playto,
prefix, public, punish, punishadd, punishdel, punishsetup, raidlog, removeinvite, rename,
renew, reroll, resetantiraide, role, rolelog, rolemembers, rolemenu, sanctions, say,
calc, catsay, change, changeall, changereset, channel, choose, christmas, claim, claque,
cleanup, clear_all_sanctions, clear_badwords, clear_bl, clear_limit, clear_messages,
clear_owners, clear_perms, clear_sanctions, close, cmute, compet, create, del_sanction,
delperm, delrole, demineur, derank, discussion, dnd, eightball, embed, emoji, end, endgiveaway,
epicgamer, fasttype, findemoji, flood, g2048, giveaway, guesspokemon, halloween, help,
helpsetting, hide, hideall, idle, invisible, invite, inviteboard, invitereset, join, kick,
kiss, leave, leave_settings, link, listen, loading, lock, lockall, mainprefix, marry,
massiverole, member, messagelog, modlog, morpion, mp, mpdelete, mpsent, mpsettings, mute,
mutelist, muterole, newsticker, noderank, noderankadd, noderankdel, nolog, online, owner,
pendu, perms, pfc, pic, piconly, piconlyadd, piconlydel, ping, playto, prefix, public,
puissance4, punish, punishadd, punishdel, punishsetup, raidlog, removeinvite, rename, renew,
reroll, resetantiraide, rickroll, role, rolelog, rolemembers, rolemenu, sanctions, say,
serverbanner, serverinfo, serverlist, serverpic, set_boostembed, set_modlogs, set_muterole,
setbanner, setname, setperm, setpic, setprofil, shadowbot, showpics, slowmode, snipe, spam,
stream, strikes, suggestion, suggestionsettings, sync, tempban, tempcmute, tempmute, temprole,
tempvoc, tempvoc_cmd, theme, ticket, ticket_member, tickets, timeout, unalias, unban, unbanall,
unbl, uncmute, unhide, unhideall, unlock, unlockall, unmassiverole, unmute, unmuteall, unowner,
untemprole, user, viewlogs, vocinfo, voicekick, voicelog, voicemove, warn, watch,
setbanner, setname, setperm, setpic, setprofil, shadowbot, showpics, slot, slowmode, snake,
snipe, spam, stream, strikes, suggestion, suggestionsettings, sync, tempban, tempcmute,
tempmute, temprole, tempvoc, tempvoc_cmd, theme, ticket, ticket_member, tickets, timeout,
unalias, unban, unbanall, unbl, uncmute, unhide, unhideall, unlock, unlockall, unmarry,
unmassiverole, unmute, unmuteall, unowner, untemprole, user, viewlogs, vocinfo, voicekick,
voicelog, voicemove, warn, watch, wordle,
};
use crate::commands::{alladmins, allbots, allperms, botadmins};
use crate::db::{DbPoolKey, upsert_message_observed};
@@ -261,6 +264,29 @@ pub async fn handle_message(ctx: &Context, msg: &Message) {
"end" => end::handle_end(ctx, msg, &args).await,
"reroll" => reroll::handle_reroll(ctx, msg, &args).await,
"choose" => choose::handle_choose(ctx, msg, &args).await,
"2048" => g2048::handle_2048(ctx, msg, &args).await,
"snake" => snake::handle_snake(ctx, msg, &args).await,
"unmarry" => unmarry::handle_unmarry(ctx, msg, &args).await,
"pendu" => pendu::handle_pendu(ctx, msg, &args).await,
"pfc" => pfc::handle_pfc(ctx, msg, &args).await,
"flood" => flood::handle_flood(ctx, msg, &args).await,
"puissance4" => puissance4::handle_puissance4(ctx, msg, &args).await,
"morpion" => morpion::handle_morpion(ctx, msg, &args).await,
"epicgamer" => epicgamer::handle_epicgamer(ctx, msg, &args).await,
"demineur" => demineur::handle_demineur(ctx, msg, &args).await,
"catsay" => catsay::handle_catsay(ctx, msg, &args).await,
"claque" => claque::handle_claque(ctx, msg, &args).await,
"slot" => slot::handle_slot(ctx, msg, &args).await,
"fasttype" => fasttype::handle_fasttype(ctx, msg, &args).await,
"rickroll" => rickroll::handle_rickroll(ctx, msg, &args).await,
"kiss" => kiss::handle_kiss(ctx, msg, &args).await,
"wordle" => wordle::handle_wordle(ctx, msg, &args).await,
"findemoji" => findemoji::handle_findemoji(ctx, msg, &args).await,
"marry" => marry::handle_marry(ctx, msg, &args).await,
"guesspokemon" => guesspokemon::handle_guesspokemon(ctx, msg, &args).await,
"8ball" => eightball::handle_eightball(ctx, msg, &args).await,
"halloween" => halloween::handle_halloween(ctx, msg, &args).await,
"christmas" => christmas::handle_christmas(ctx, msg, &args).await,
"embed" => embed::handle_embed(ctx, msg, &args).await,
"backup" => backup::handle_backup(ctx, msg, &args).await,
"autobackup" => autobackup::handle_autobackup(ctx, msg, &args).await,