Author SHA1 Message Date
ByteDice 453890c776 i cant get this to work its too spaghetti 2026-04-28 21:04:05 +02:00
10 changed files with 281 additions and 117 deletions
+6 -1
View File
@@ -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
}
}
}
+1 -1
View File
@@ -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"
},
+76 -17
View File
@@ -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 game.is_full_lobby() { return Ok(()) }
if let Err(_) = r
{ send_msg(ctx, ctx.data().lang.get("dc.wwrps.already_submitted", &[]), true, true).await; return Ok(()); }
let data_lock = &ctx.data().discord_data.lock().await["users"];
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 full = r.unwrap();
if !full { return Ok(()); }
let r_text = results_text(&game, &ctx.data().lang);
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<ChannelId> = 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<i8>,
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}
]);
}
@@ -95,3 +142,15 @@ async fn get_wwrps_channel(ctx: Context<'_>) -> Option<u64> {
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);
}
+1 -1
View File
@@ -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 {
+5 -1
View File
@@ -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();
+2 -79
View File
@@ -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<RPSPlayer>; 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<bool, Error> {
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<i8> {
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()); }
}
pub mod game;
pub mod ranks;
+82
View File
@@ -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<RPSPlayer>; 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<bool, Error> {
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<i8> {
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()); }
}
+91
View File
@@ -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"),
}
}
}
+1 -1
View File
@@ -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};
+4 -4
View File
@@ -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;
}
}