mirror of
https://github.com/arthur-pbty/shadowbot.git
synced 2026-06-03 15:07:37 +02:00
add many game
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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"][..],
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user