From 7cdc3a2755ec054b22057e164e4f92491f0061b5 Mon Sep 17 00:00:00 2001 From: prescientmoon Date: Tue, 20 Aug 2024 21:06:40 +0200 Subject: [PATCH] Add soring system help & sdf scoring --- schema.sql | 7 +--- src/arcaea/score.rs | 23 +++++++---- src/commands/mod.rs | 53 +++++++++++++++++++++++- src/commands/{utils.rs => utils/mod.rs} | 2 + src/commands/utils/two_columns.rs | 54 +++++++++++++++++++++++++ 5 files changed, 124 insertions(+), 15 deletions(-) rename src/commands/{utils.rs => utils/mod.rs} (97%) create mode 100644 src/commands/utils/two_columns.rs diff --git a/schema.sql b/schema.sql index 3b3d512..bf0c9dc 100644 --- a/schema.sql +++ b/schema.sql @@ -42,11 +42,6 @@ CREATE TABLE IF NOT EXISTS plays ( discord_attachment_id TEXT, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - creation_ptt INTEGER, - creation_zeta_ptt INTEGER, - - score INTEGER NOT NULL, - zeta_score INTEGER NOT NULL, max_recall INTEGER, far_notes INTEGER, @@ -62,7 +57,7 @@ CREATE TABLE IF NOT EXISTS scores ( score INTEGER NOT NULL, creation_ptt INTEGER, - scoring_system NOT NULL CHECK (scoring_system IN ('standard', 'ex')), + scoring_system NOT NULL CHECK (scoring_system IN ('standard', 'sdf', 'ex')), FOREIGN KEY (play_id) REFERENCES plays(id), UNIQUE(play_id, scoring_system) diff --git a/src/arcaea/score.rs b/src/arcaea/score.rs index ef9cd2d..74f303a 100644 --- a/src/arcaea/score.rs +++ b/src/arcaea/score.rs @@ -14,15 +14,21 @@ use super::{ pub enum ScoringSystem { Standard, - // Inspired by sdvx's EX-scoring + /// Forgives up to 9 missed shinies, then uses EX scoring. + /// PMs correspond to SDPMs. + SDF, + + /// Inspired by sdvx's EX-scoring. + /// PMs correspond to MPMs. EX, } impl ScoringSystem { - pub const SCORING_SYSTEMS: [Self; 2] = [Self::Standard, Self::EX]; + pub const SCORING_SYSTEMS: [Self; 3] = [Self::Standard, Self::SDF, Self::EX]; /// Values used inside sqlite - pub const SCORING_SYSTEM_DB_STRINGS: [&'static str; 2] = ["standard", "ex"]; + pub const SCORING_SYSTEM_DB_STRINGS: [&'static str; Self::SCORING_SYSTEMS.len()] = + ["standard", "sdf", "ex"]; #[inline] pub fn to_index(self) -> usize { @@ -95,11 +101,7 @@ impl Score { /// Remove the contribution made by shinies to a score. #[inline] pub fn forget_shinies(self, note_count: u32) -> Self { - Self( - (Self::increment(note_count) * Rational64::from_integer(self.units(note_count) as i64)) - .floor() - .to_integer() as u32, - ) + Self(self.0 - self.shinies(note_count)) } /// Compute a score without making a distinction between shinies and pures. That is, the given @@ -147,9 +149,14 @@ impl Score { pub fn convert_to(self, scoring_system: ScoringSystem, chart: &Chart) -> Self { match scoring_system { ScoringSystem::Standard => self, + ScoringSystem::SDF => { + let shinies = self.shinies(chart.note_count); + Self(self.0 + 9.min(chart.note_count - shinies)).to_zeta(chart.note_count) + } ScoringSystem::EX => self.to_zeta(chart.note_count), } } + // }}} // {{{ Score => Play rating #[inline] diff --git a/src/commands/mod.rs b/src/commands/mod.rs index e645b56..053c7a6 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -7,11 +7,12 @@ pub mod utils; // {{{ Help /// Show this help menu -#[poise::command(prefix_command, track_edits, slash_command)] +#[poise::command(prefix_command, slash_command, subcommands("scoring", "scoringz"))] pub async fn help( ctx: Context<'_>, #[description = "Specific command to show help about"] #[autocomplete = "poise::builtins::autocomplete_command"] + #[rest] command: Option, ) -> Result<(), Error> { poise::builtins::help( @@ -27,3 +28,53 @@ pub async fn help( Ok(()) } // }}} +// {{{ Scoring help +/// Explains the different scoring systems +#[poise::command(prefix_command, slash_command)] +async fn scoring(ctx: Context<'_>) -> Result<(), Error> { + static CONTENT: &'static str = " +## 1. Standard scoring (`standard`): +This is the base-game Arcaea scoring system we all know and love! Points are awarded for each note, with a `2:1` pure:far ratio. The score is then scaled up such that `10_000_000` is the maximum. Last but not least, the number of max pures is added to the total. + +## 2. ξ scoring (`ex`): +This is a stricter scoring method inspired by EX-scoring in sdvx. The scoring algorithm works almost the same as in standard scoring, except a `5:4:2` max-pure:pure:far ratio is used (the number of max pures is no longer added to the scaled up total). This means shinies (i.e. max pures) are worth 1.25x as much as non-max pures. + +Use this scoring method if you want to focus on shiny accuracy. ξ-scoring has the added property that ξ-PMs correspond to standard FPMs. + +## 3. Single-digit-forgiveness scoring (`sdf`): +This is a slightly more lax version of ξ-scoring which overlooks up to 9 non-max pures. SDF-scoring has the added property that SDF-PMs correspond to standard SDPMs. + + +Most commands take an optional parameter specifying what scoring system to use. For instance, `stats b30 ex` will produce a b30 image with scores computed using SDF scoring. This makes the system extremely versatile — for instance, all the standard PM related achievements suddenly gain an extra meaning while in other modes (namely, they refer to SDPMs and FPMs in SDF or ξ scoring respectively) + "; + + ctx.reply(CONTENT).await?; + + Ok(()) +} +// }}} +// {{{ Scoring gen-z help +/// Explains the different scoring systems using gen-z slang +#[poise::command(prefix_command, slash_command)] +async fn scoringz(ctx: Context<'_>) -> Result<(), Error> { + static CONTENT: &'static str = " +## 1. Standard scoring (`standard`): +Alright, fam, this is the OG Arcaea scoring setup that everyone vibes with! You hit notes, you get points — easy clap. The ratio is straight up `2:1` pure:far. The score then gets a glow-up, maxing out at `10 milly`. And hold up, you even get bonus points for those max pures at the end. No cap, this is the classic way to flex your skills. + +## 2. ξ scoring (`ex`): +Now, this one’s for the real Gs. ξ scoring is inspired by EX-scoring, for the SDVX-pilled of y'all, so you know it’s serious business. It’s like standard, but with more drip — a `5:4:2` max-pure:pure:far ratio. That means shinies are worth fat stacks — 1.25x more than Ohio pures. No bonus points here, so it’s all about that shiny flex. + +If you’re all about shinymaxxing, this is your go-to. Oh, and ξ-PMs? They line up with standard FPMs - if you can hit those, you're truly the CEO of rhythm. + +## 3. Skibidi-digit-forgiveness scoring (`sdf`): +For those who wanna chill a bit, while still on the acc grindset, we got SDF scoring. It’s like ξ scoring but with a bit of slack — up to 9 Ohio pures get a pass. SDF-PMs line up with standard SDPMs, so you’re still big-braining it. + + +Real ones can skip the yap and use this already, fr. But for the sussy NPCs among y'all who wanna like, see the best 30 Ws with ξ-scoring — just hit `stats b30 ex` and you’re golden. This makes the whole system hella versatile — like, standard PMs highkey get a whole new ass meaning depending on the achievement mode you’re mewing in. + "; + + ctx.reply(CONTENT).await?; + + Ok(()) +} +// }}} diff --git a/src/commands/utils.rs b/src/commands/utils/mod.rs similarity index 97% rename from src/commands/utils.rs rename to src/commands/utils/mod.rs index 886358a..599a60b 100644 --- a/src/commands/utils.rs +++ b/src/commands/utils/mod.rs @@ -1,3 +1,5 @@ +pub mod two_columns; + #[macro_export] macro_rules! edit_reply { ($ctx:expr, $handle:expr, $($arg:tt)*) => {{ diff --git a/src/commands/utils/two_columns.rs b/src/commands/utils/two_columns.rs new file mode 100644 index 0000000..07184d9 --- /dev/null +++ b/src/commands/utils/two_columns.rs @@ -0,0 +1,54 @@ +//! These functions have been copy-pasted from internal `poise` code. + +use std::fmt::Write as _; + +/// Convenience function to align descriptions behind commands +pub struct TwoColumnList(Vec<(String, Option)>); + +impl TwoColumnList { + /// Creates a new [`TwoColumnList`] + pub fn new() -> Self { + Self(Vec::new()) + } + + /// Add a line that needs the padding between the columns + pub fn push_two_colums(&mut self, command: String, description: String) { + self.0.push((command, Some(description))); + } + + /// Add a line that doesn't influence the first columns's width + pub fn push_heading(&mut self, category: &str) { + if !self.0.is_empty() { + self.0.push(("".to_string(), None)); + } + let mut category = category.to_string(); + category += ":"; + self.0.push((category, None)); + } + + /// Convert the list into a string with aligned descriptions + pub fn into_string(self) -> String { + let longest_command = self + .0 + .iter() + .filter_map(|(command, description)| { + if description.is_some() { + Some(command.len()) + } else { + None + } + }) + .max() + .unwrap_or(0); + let mut text = String::new(); + for (command, description) in self.0 { + if let Some(description) = description { + let padding = " ".repeat(longest_command - command.len() + 3); + writeln!(text, "{}{}{}", command, padding, description).unwrap(); + } else { + writeln!(text, "{}", command).unwrap(); + } + } + text + } +}