Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
453890c776 |
@@ -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
@@ -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"
|
||||
},
|
||||
|
||||
+86
-27
@@ -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 full = r.unwrap();
|
||||
if !full { return Ok(()); }
|
||||
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);
|
||||
|
||||
let r_text = results_text(&game, &ctx.data().lang);
|
||||
let game_clone = game.clone();
|
||||
game.clear();
|
||||
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<ChannelId> = Vec::new();
|
||||
let full = r.unwrap();
|
||||
if !full { return Ok(()); }
|
||||
|
||||
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 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
@@ -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
@@ -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
@@ -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;
|
||||
@@ -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()); }
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user