diff --git a/data/defaults/dc_data_preset.json b/data/defaults/dc_data_preset.json index a69c92a..a1e194d 100644 --- a/data/defaults/dc_data_preset.json +++ b/data/defaults/dc_data_preset.json @@ -1,9 +1,14 @@ { "servers": { - "SERVER ID": { + "SERVER_ID": { "re_posts_channel": 0, "re_disabled": false, "wwrps_channel": 0 } + }, + "users": { + "USER_ID": { + "wwrps_elo": 0 + } } } \ No newline at end of file diff --git a/data/lang/en.json b/data/lang/en.json index b58d534..07c30a3 100644 --- a/data/lang/en.json +++ b/data/lang/en.json @@ -75,7 +75,7 @@ "already_submitted": "You can't compete against yourself! Please wait until another player has submitted their RPS.", "anon": "[Anonymous]", "draw": "DRAW", - "match": "### {0} (P1) vs {1} (P2)...\n## {2}\n**P1:** {3}\n**P2:** {4}", + "match": "### {0} (P1) vs {1} (P2)...\n## {2}\n**P1:** {3} **[{4} +{5}] {6}**\n**P2:** {7} **[{8} +{9}] {10}**", "p1_win": "P1 WINS", "p2_win": "P2 WINS" }, diff --git a/src/cmds/generic/wwrps.rs b/src/cmds/generic/wwrps.rs index 84e04cc..9e84fec 100644 --- a/src/cmds/generic/wwrps.rs +++ b/src/cmds/generic/wwrps.rs @@ -1,7 +1,8 @@ use poise::serenity_prelude::{ChannelId, Mentionable}; +use serde_json::Value; use tokio::sync::MutexGuard; -use crate::{Context, Error, games::wwrps::{RPS, RPSGame, RPSPlayer}, lang::Lang, messages::{http_send_msg, send_msg}}; +use crate::{Context, Error, games::wwrps::{game::{RPS, RPSGame, RPSPlayer}, ranks::{RPSStats, Ranks}}, lang::Lang, messages::{http_send_msg, send_msg}}; #[poise::command( @@ -27,43 +28,80 @@ pub async fn cmd( let mut game = ctx.data().rps_game.lock().await; - if !game.is_full_lobby() { - let r = game.add_player( - RPSPlayer { selection, user: ctx.author().clone(), wwrps_channel: ChannelId::new(c), anonymous }); - - if let Err(_) = r - { send_msg(ctx, ctx.data().lang.get("dc.wwrps.already_submitted", &[]), true, true).await; return Ok(()); } + if game.is_full_lobby() { return Ok(()) } - let full = r.unwrap(); - if !full { return Ok(()); } + let data_lock = &ctx.data().discord_data.lock().await["users"]; - let r_text = results_text(&game, &ctx.data().lang); - let game_clone = game.clone(); - game.clear(); + let r = game.add_player(RPSPlayer { + selection, + user: ctx.author().clone(), + wwrps_channel: ChannelId::new(c), + anonymous, + stats: get_player_stats(ctx.author().id.get(), data_lock) + }, ctx.data().args.dev); + + if let Err(_) = r { + send_msg(ctx, ctx.data().lang.get("dc.wwrps.already_submitted", &[]), true, true).await; + return Ok(()); + } - let mut used_channels: Vec = Vec::new(); - - for player in &game_clone.players { - if let Some(p) = player { - if used_channels.contains(&p.wwrps_channel) { continue; } - used_channels.push(p.wwrps_channel); - http_send_msg(ctx.http(), p.wwrps_channel, r_text.clone()).await; - } + let full = r.unwrap(); + if !full { return Ok(()); } + + let winner = game.get_winner(); + + let p1 = game.players[0].as_ref().unwrap(); + let p2 = game.players[1].as_ref().unwrap(); + + let old_elos = [p1.stats.elo, p2.stats.elo]; + let expected = RPSStats::get_elo_expected(old_elos[0], old_elos[1]); + + if let Some(w) = winner { + // 0 means p1 wins, and 1 means p2 wins + // we flip it because ELO counts 0 as a loss + game.players[0].as_mut().unwrap().stats.update_elo(expected, (!w) as f32); + game.players[1].as_mut().unwrap().stats.update_elo(expected, w as f32); + } + + let r_text = results_text(&game, &ctx.data().lang, winner, old_elos); + + // clone and clear here to prevent race conditions while + // sending the results + let game_clone = game.clone(); + game.clear(); + + let mut used_channels: Vec = Vec::new(); + + // send the results + for player in &game_clone.players { + if let Some(p) = player { + if used_channels.contains(&p.wwrps_channel) { continue; } + + used_channels.push(p.wwrps_channel); + http_send_msg(ctx.http(), p.wwrps_channel, r_text.clone()).await; } } + return Ok(()); } -fn results_text(game: &MutexGuard<'_, RPSGame>, lang: &Lang) -> String { - let winner = game.get_winner(); +fn results_text( + game: &MutexGuard<'_, RPSGame>, + lang: &Lang, + winner: Option, + old_elos: [u16; 2] +) -> String +{ let winner_text: String; let p1 = game.players[0].as_ref().unwrap(); let p2 = game.players[1].as_ref().unwrap(); - let p1_n = if !p1.anonymous { p1.user.mention().to_string() } else { lang.get("dc.wwrps.anon", &[]) }; - let p2_n = if !p2.anonymous { p2.user.mention().to_string() } else { lang.get("dc.wwrps.anon", &[]) }; + let p1_n = if !p1.anonymous { p1.user.mention().to_string() } + else { lang.get("dc.wwrps.anon", &[]) }; + let p2_n = if !p2.anonymous { p2.user.mention().to_string() } + else { lang.get("dc.wwrps.anon", &[]) }; if let Some(w) = winner { winner_text = if w == 0 { lang.get("dc.wwrps.p1_win", &[]) } @@ -73,11 +111,20 @@ fn results_text(game: &MutexGuard<'_, RPSGame>, lang: &Lang) -> String { return lang.get( "dc.wwrps.match", &[ - p1.selection.to_string(), - p2.selection.to_string(), - winner_text, - p1_n, - p2_n + p1.selection.to_string(), // {0} + p2.selection.to_string(), // {1} + + winner_text, // {2} + + p1_n, // {3} + old_elos[0].to_string(), // {4} + (p1.stats.elo - old_elos[0]).to_string(), // {5} + Ranks::from_elo(p1.stats.elo).to_string(), // {6} + + p2_n, // {7} + old_elos[1].to_string(), // {8} + (p2.stats.elo - old_elos[1]).to_string(), // {9} + Ranks::from_elo(p2.stats.elo).to_string(), // {10} ]); } @@ -94,4 +141,16 @@ async fn get_wwrps_channel(ctx: Context<'_>) -> Option { let Some(c_id) = s.get("wwrps_channel") else { return None; }; return Some(c_id.as_u64().unwrap()); +} + + +fn get_player_stats(uid: u64, db: &Value) -> RPSStats { + if let Some(user) = db.get(uid.to_string()) { + if let Some(elo) = user.get("wwrps_elo") { + if let Some(elo_i64) = elo.as_i64() + { return RPSStats::from(uid, elo_i64 as u16); } + } + } + + return RPSStats::new(uid); } \ No newline at end of file diff --git a/src/db/bot_data.rs b/src/db/bot_data.rs index a28a3df..485733c 100644 --- a/src/db/bot_data.rs +++ b/src/db/bot_data.rs @@ -1,7 +1,7 @@ use serde_json::Value; use tokio::sync::Mutex; -use crate::{db::{env_vars::AssistantEnv, terminal_args::Args}, games::wwrps::RPSGame, lang::Lang}; +use crate::{db::{env_vars::AssistantEnv, terminal_args::Args}, games::wwrps::game::RPSGame, lang::Lang}; pub struct Data { diff --git a/src/db/discord.rs b/src/db/discord.rs index 762687d..be3292d 100644 --- a/src/db/discord.rs +++ b/src/db/discord.rs @@ -27,8 +27,12 @@ fn generate_data() { let preset_str = fs::read_to_string(PRESET_PATH).unwrap(); let mut preset_json: Value = serde_json::from_str(&preset_str).unwrap(); + // remove the examples if let Some(servers) = preset_json["servers"].as_object_mut() { - servers.remove("SERVER ID"); + servers.remove("SERVER_ID"); + } + if let Some(users) = preset_json["users"].as_object_mut() { + users.remove("USER_ID"); } let json_str = serde_json::to_string_pretty(&preset_json).unwrap(); diff --git a/src/games/wwrps.rs b/src/games/wwrps.rs index 855620c..ddfceb7 100644 --- a/src/games/wwrps.rs +++ b/src/games/wwrps.rs @@ -1,79 +1,2 @@ -use std::fmt::Display; - -use poise::serenity_prelude::{ChannelId, User}; - -use crate::Error; - - -#[derive(poise::ChoiceParameter, PartialEq, Clone, Debug)] -#[repr(u8)] -pub enum RPS { - Paper = 0, - Rock = 1, - Scissors = 2 -} - - -#[derive(Clone)] -pub struct RPSPlayer { - pub selection: RPS, - pub user: User, - pub wwrps_channel: ChannelId, - pub anonymous: bool -} -#[derive(Clone)] -pub struct RPSGame { - pub players: [Option; 2], -} - - -impl Display for RPS { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - return match self { - Self::Paper => write!(f, "Paper"), - Self::Rock => write!(f, "Rock"), - Self::Scissors => write!(f, "Scissors") - }; - } -} - - -impl RPSGame { - pub fn new() -> Self { - return RPSGame { - players: [None, None], - } - } - - /// Returns wether the lobby is filled or not - pub fn add_player(&mut self, player: RPSPlayer) -> Result { - if let Some(p1) = &self.players[0] - { if p1.user == player.user { return Err(Error::from("Cannot add player, it already exists!")); }} - - if self.players[0].is_none() { self.players[0] = Some(player); return Ok(false); } - else if self.players[1].is_none() { self.players[1] = Some(player); return Ok(true); } - else { return Err(Error::from("Cannot add player, list is full!")); } - } - - pub fn clear(&mut self) - { self.players = [None, None]; } - - - pub fn get_winner(&self) -> Option { - if self.players.iter().any(|i| i.is_none()) - { return None; } - - let Some(p1) = self.players[0].clone() else { return None; }; - let Some(p2) = self.players[1].clone() else { return None; }; - - let i1 = p1.selection as u8; - let i2 = p2.selection as u8; - - if i1 == i2 { return None; } - else if (i1 + 1) % 3 == i2 { return Some(0); } - else { return Some(1); } - } - - pub fn is_full_lobby(&self) -> bool - { return self.players.iter().all(|i| i.is_some()); } -} \ No newline at end of file +pub mod game; +pub mod ranks; \ No newline at end of file diff --git a/src/games/wwrps/game.rs b/src/games/wwrps/game.rs new file mode 100644 index 0000000..b7bdc37 --- /dev/null +++ b/src/games/wwrps/game.rs @@ -0,0 +1,82 @@ +use std::fmt::Display; + +use poise::serenity_prelude::{ChannelId, User}; + +use crate::{Error, games::wwrps::ranks::RPSStats}; + + +#[derive(poise::ChoiceParameter, PartialEq, Clone, Debug)] +#[repr(u8)] +pub enum RPS { + Paper = 0, + Rock = 1, + Scissors = 2 +} + + +#[derive(Clone)] +pub struct RPSPlayer { + pub selection: RPS, + pub user: User, + pub wwrps_channel: ChannelId, + pub anonymous: bool, + pub stats: RPSStats +} +#[derive(Clone)] +pub struct RPSGame { + pub players: [Option; 2], +} + + +impl Display for RPS { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return match self { + Self::Paper => write!(f, "Paper"), + Self::Rock => write!(f, "Rock"), + Self::Scissors => write!(f, "Scissors") + }; + } +} + + +impl RPSGame { + pub fn new() -> Self { + return RPSGame { + players: [None, None], + } + } + + /// Returns wether the lobby is filled or not + pub fn add_player(&mut self, player: RPSPlayer, allow_dupes: bool) -> Result { + if let Some(p1) = &self.players[0] { + if p1.user == player.user && !allow_dupes + { return Err(Error::from("Cannot add player, it already exists!")); } + } + + if self.players[0].is_none() { self.players[0] = Some(player); return Ok(false); } + else if self.players[1].is_none() { self.players[1] = Some(player); return Ok(true); } + else { return Err(Error::from("Cannot add player, list is full!")); } + } + + pub fn clear(&mut self) + { self.players = [None, None]; } + + + pub fn get_winner(&self) -> Option { + if self.players.iter().any(|i| i.is_none()) + { return None; } + + let Some(p1) = self.players[0].clone() else { return None; }; + let Some(p2) = self.players[1].clone() else { return None; }; + + let i1 = p1.selection as u8; + let i2 = p2.selection as u8; + + if i1 == i2 { return None; } + else if (i1 + 1) % 3 == i2 { return Some(0); } // calculate if p1 wins + else { return Some(1); } // otherwise p2 wins + } + + pub fn is_full_lobby(&self) -> bool + { return self.players.iter().all(|i| i.is_some()); } +} \ No newline at end of file diff --git a/src/games/wwrps/ranks.rs b/src/games/wwrps/ranks.rs new file mode 100644 index 0000000..8e31614 --- /dev/null +++ b/src/games/wwrps/ranks.rs @@ -0,0 +1,91 @@ +pub enum Ranks { + PlasticScissors, + PrinterPaper, + Pebble, + + ArkOfTheElements, + Origami, + Obsidian, + + Dwayne, + LiterallyBrokenTheSystem +} + + +#[derive(Clone)] +pub struct RPSStats { + pub uid: u64, + pub elo: u16 + // Win/loss history? +} + + +static SENSITIVITY: u16 = 32; +pub static START_ELO: u16 = 500; + + +impl RPSStats { + pub fn new(uid: u64) -> RPSStats + { return RPSStats { uid: uid, elo: START_ELO } } + + pub fn from(uid: u64, elo: u16) -> RPSStats + { return RPSStats { uid: uid, elo: elo } } + + pub fn update_elo(&mut self, expected: f32, score: f32) + { self.elo += SENSITIVITY * (score - expected) as u16; } + + pub fn get_elo_expected(elo_a: u16, elo_b: u16) -> f32 + { return 1.0 / (1.0 + f32::powf(10.0, (elo_b - elo_a) as f32 / 400.0)) } + + // TODO: figure out a way to add RAM-efficient win/loss history before doing this + /*pub fn to_b64(&self) -> String { + return String::new(); + } + + pub fn from_b64(b64: String, uid: u64) -> RPSStats { + return Self::new(); + }*/ +} + + +impl Ranks { + pub fn from_elo(elo: u16) -> Ranks { + // formula: start + x * inc + step * (x * (x - 1) / 2) + // start = 100 + // inc = 40 + // step = 40 + + return match elo { + n if n > 1500 => Ranks::Dwayne, + + n if n > 1180 => Ranks::Obsidian, + n if n > 900 => Ranks::Origami, + n if n > 660 => Ranks::ArkOfTheElements, + + n if n > 460 => Ranks::Pebble, + n if n > 300 => Ranks::PrinterPaper, + n if n <= 180 => Ranks::PlasticScissors, + + _ => Ranks::LiterallyBrokenTheSystem + } + } +} + + +impl ToString for Ranks { + fn to_string(&self) -> String { + return match self { + Ranks::PlasticScissors => String::from("Plastic Scissors"), + Ranks::PrinterPaper => String::from("Printer Paper"), + Ranks::Pebble => String::from("Pebble"), + + Ranks::ArkOfTheElements => String::from("Ark of the Elements"), + Ranks::Origami => String::from("Origami"), + Ranks::Obsidian => String::from("Obsidian"), + + Ranks::Dwayne => String::from("Dwayne"), + + Ranks::LiterallyBrokenTheSystem => String::from("YOU HAVE BROKEN THE GAME SOMEHOW"), + } + } +} \ No newline at end of file diff --git a/src/gen.rs b/src/gen.rs index 0fde4c0..f4566a8 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -9,7 +9,7 @@ use toml::Value; use crate::db::env_vars::AssistantEnv; use crate::db::{cfg, discord, reddit}; -use crate::games::wwrps::RPSGame; +use crate::games::wwrps::game::RPSGame; use crate::lang::Lang; use crate::{Args, Cmd, Data, cmds, events, rs_println}; diff --git a/src/lang.rs b/src/lang.rs index df16494..4ee4f24 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -8,8 +8,8 @@ use crate::{Error, rs_warnln}; #[derive(Debug)] enum LangErrorType { - Fallback, - ShortIndex, + InvalidArguments, + NotAnEndpoint, KeyNotFound } @@ -57,7 +57,7 @@ impl Lang { else { search = r; } } - rs_warnln!("LANG warning ({:?})! ({})", LangErrorType::ShortIndex, str_path); + rs_warnln!("LANG warning ({:?})! ({})", LangErrorType::NotAnEndpoint, str_path); return str_path; } @@ -68,7 +68,7 @@ impl Lang { if let Ok(ok) = cow { return ok.to_string(); } else { - rs_warnln!("LANG warning ({:?})! ({})", LangErrorType::Fallback, fallback); + rs_warnln!("LANG warning ({:?})! ({})", LangErrorType::InvalidArguments, fallback); return fallback; } }