Implement a few math commands
This commit is contained in:
parent
f3edaf9e72
commit
2ac13510f2
|
@ -1,6 +1,5 @@
|
|||
use std::path::Path;
|
||||
// {{{ Imports
|
||||
use std::{fmt::Display, num::NonZeroU16, path::PathBuf};
|
||||
use std::{fmt::Display, num::NonZeroU16};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use image::{ImageBuffer, Rgb};
|
||||
|
@ -26,6 +25,8 @@ impl Difficulty {
|
|||
[Self::PST, Self::PRS, Self::FTR, Self::ETR, Self::BYD];
|
||||
|
||||
pub const DIFFICULTY_SHORTHANDS: [&'static str; 5] = ["PST", "PRS", "FTR", "ETR", "BYD"];
|
||||
pub const DIFFICULTY_SHORTHANDS_IN_BRACKETS: [&'static str; 5] =
|
||||
["[PST]", "[PRS]", "[FTR]", "[ETR]", "[BYD]"];
|
||||
pub const DIFFICULTY_STRINGS: [&'static str; 5] =
|
||||
["PAST", "PRESENT", "FUTURE", "ETERNAL", "BEYOND"];
|
||||
|
||||
|
@ -53,11 +54,7 @@ impl FromSql for Difficulty {
|
|||
|
||||
impl Display for Difficulty {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
Self::DIFFICULTY_SHORTHANDS[self.to_index()].to_lowercase()
|
||||
)
|
||||
write!(f, "{}", Self::DIFFICULTY_SHORTHANDS[self.to_index()])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -192,6 +189,24 @@ pub struct Song {
|
|||
pub pack: Option<String>,
|
||||
pub side: Side,
|
||||
}
|
||||
|
||||
impl Song {
|
||||
/// Returns true if multiple songs are known to exist with the given title.
|
||||
#[inline]
|
||||
pub fn ambigous_name(&self) -> bool {
|
||||
self.title == "Genesis" || self.title == "Quon"
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Song {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.ambigous_name() {
|
||||
write!(f, "{} ({})", self.title, self.artist)
|
||||
} else {
|
||||
write!(f, "{}", self.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
// {{{ Chart
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
|
@ -215,6 +230,10 @@ pub struct Chart {
|
|||
|
||||
#[serde(skip)]
|
||||
pub cached_jacket: Option<Jacket>,
|
||||
|
||||
/// If `None`, the default jacket is used.
|
||||
/// Otherwise, a difficulty-specific jacket exists.
|
||||
pub jacket_source: Option<Difficulty>,
|
||||
}
|
||||
// }}}
|
||||
// {{{ Cached song
|
||||
|
@ -374,6 +393,7 @@ impl SongCache {
|
|||
note_count: row.get("note_count")?,
|
||||
note_design: row.get("note_design")?,
|
||||
cached_jacket: None,
|
||||
jacket_source: None,
|
||||
})
|
||||
})?;
|
||||
|
||||
|
|
|
@ -106,6 +106,7 @@ impl JacketCache {
|
|||
|
||||
Vec::new()
|
||||
} else {
|
||||
let suffix = format!("_{BITMAP_IMAGE_SIZE}.jpg");
|
||||
let songs_dir = get_asset_dir().join("songs/by_id");
|
||||
let entries =
|
||||
fs::read_dir(songs_dir).with_context(|| "Couldn't read songs directory")?;
|
||||
|
@ -127,7 +128,12 @@ impl JacketCache {
|
|||
for entry in entries {
|
||||
let file = entry?;
|
||||
let raw_name = file.file_name();
|
||||
let name = raw_name.to_str().unwrap().strip_suffix(".jpg").unwrap();
|
||||
let name = raw_name.to_str().unwrap();
|
||||
if !name.ends_with(&suffix) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let name = name.strip_suffix(&suffix).unwrap();
|
||||
|
||||
let difficulty = Difficulty::DIFFICULTY_SHORTHANDS
|
||||
.iter()
|
||||
|
@ -146,6 +152,7 @@ impl JacketCache {
|
|||
let chart = song_cache
|
||||
.lookup_by_difficulty_mut(song_id, difficulty)
|
||||
.unwrap();
|
||||
chart.jacket_source = Some(difficulty);
|
||||
chart.cached_jacket = Some(Jacket {
|
||||
raw: contents,
|
||||
bitmap,
|
||||
|
@ -153,11 +160,12 @@ impl JacketCache {
|
|||
} else {
|
||||
for chart_id in song_cache.lookup_song(song_id)?.charts() {
|
||||
let chart = song_cache.lookup_chart_mut(chart_id)?;
|
||||
if chart.cached_jacket.is_none() {
|
||||
if chart.jacket_source.is_none() {
|
||||
chart.cached_jacket = Some(Jacket {
|
||||
raw: contents,
|
||||
bitmap,
|
||||
});
|
||||
chart.jacket_source = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use num::Rational32;
|
||||
use num::{Rational32, ToPrimitive};
|
||||
|
||||
pub type Rating = Rational32;
|
||||
|
||||
|
@ -13,7 +13,10 @@ pub fn rating_as_fixed(rating: Rating) -> i32 {
|
|||
/// Saves a rating rational as a float with precision 2.
|
||||
#[inline]
|
||||
pub fn rating_as_float(rating: Rating) -> f32 {
|
||||
rating_as_fixed(rating) as f32 / 100.0
|
||||
let hundred = Rational32::from_integer(100);
|
||||
let rounded = (rating * hundred).round();
|
||||
|
||||
(rounded / hundred).to_f32().unwrap()
|
||||
}
|
||||
|
||||
/// The pseudo-inverse of `rating_as_fixed`.
|
||||
|
|
|
@ -36,7 +36,7 @@ pub fn run() -> Result<(), Error> {
|
|||
let entries = fs::read_dir(&raw_songs_dir)
|
||||
.with_context(|| "Couldn't read songs directory")?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.with_context(|| format!("Could not read member of `songs/raw`"))?;
|
||||
.with_context(|| "Could not read member of `songs/raw`")?;
|
||||
|
||||
for (i, dir) in entries.iter().enumerate() {
|
||||
let raw_dir_name = dir.file_name();
|
||||
|
@ -48,10 +48,7 @@ pub fn run() -> Result<(), Error> {
|
|||
}
|
||||
|
||||
print!("{}/{}: {dir_name}", i, entries.len());
|
||||
|
||||
if i % 5 == 0 {
|
||||
stdout().flush()?;
|
||||
}
|
||||
stdout().flush()?;
|
||||
// }}}
|
||||
|
||||
let entries = fs::read_dir(dir.path())
|
||||
|
@ -126,13 +123,31 @@ pub fn run() -> Result<(), Error> {
|
|||
|
||||
jacket_vectors.push((song.id, ImageVec::from_image(&image)));
|
||||
|
||||
let image = image.resize(BITMAP_IMAGE_SIZE, BITMAP_IMAGE_SIZE, FilterType::Gaussian);
|
||||
let image_out_path =
|
||||
out_dir.join(format!("{difficulty_string}_{BITMAP_IMAGE_SIZE}.jpg"));
|
||||
image
|
||||
// .blur(27.5)
|
||||
.save(&image_out_path)
|
||||
.with_context(|| format!("Could not save image to {image_out_path:?}"))?;
|
||||
let small_image =
|
||||
image.resize(BITMAP_IMAGE_SIZE, BITMAP_IMAGE_SIZE, FilterType::Gaussian);
|
||||
|
||||
{
|
||||
let image_small_path =
|
||||
out_dir.join(format!("{difficulty_string}_{BITMAP_IMAGE_SIZE}.jpg"));
|
||||
small_image
|
||||
.save(&image_small_path)
|
||||
.with_context(|| format!("Could not save image to {image_small_path:?}"))?;
|
||||
}
|
||||
|
||||
{
|
||||
let image_full_path = out_dir.join(format!("{difficulty_string}_full.jpg"));
|
||||
image
|
||||
.save(&image_full_path)
|
||||
.with_context(|| format!("Could not save image to {image_full_path:?}"))?;
|
||||
}
|
||||
|
||||
{
|
||||
let blurred_out_path = out_dir.join(format!("{difficulty_string}_blurred.jpg"));
|
||||
small_image
|
||||
.blur(27.5)
|
||||
.save(&blurred_out_path)
|
||||
.with_context(|| format!("Could not save image to {blurred_out_path:?}"))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,9 +167,9 @@ pub fn run() -> Result<(), Error> {
|
|||
{
|
||||
println!("Encoded {} images", jacket_vectors.len());
|
||||
let bytes = postcard::to_allocvec(&jacket_vectors)
|
||||
.with_context(|| format!("Coult not encode jacket matrix"))?;
|
||||
.with_context(|| "Coult not encode jacket matrix")?;
|
||||
fs::write(songs_dir.join("recognition_matrix"), bytes)
|
||||
.with_context(|| format!("Could not write jacket matrix"))?;
|
||||
.with_context(|| "Could not write jacket matrix")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -21,6 +21,7 @@ async fn main() {
|
|||
commands::score::score(),
|
||||
commands::stats::stats(),
|
||||
commands::chart::chart(),
|
||||
commands::calc::calc(),
|
||||
],
|
||||
prefix_options: poise::PrefixFrameworkOptions {
|
||||
stripped_dynamic_prefix: Some(|_ctx, message, _user_ctx| {
|
||||
|
|
|
@ -21,7 +21,7 @@ async fn main() -> Result<(), Error> {
|
|||
ipc.connect().map_err(|e| anyhow!("{}", e))?;
|
||||
|
||||
println!("Starting presence loop...");
|
||||
for i in 0.. {
|
||||
loop {
|
||||
println!("Getting most recent score...");
|
||||
let res = reqwest::get(format!("{}/plays/latest", server_url)).await;
|
||||
|
||||
|
@ -42,7 +42,6 @@ async fn main() -> Result<(), Error> {
|
|||
"{}/jackets/by_chart_id/{}.png",
|
||||
server_url, &triplet.chart.id
|
||||
);
|
||||
let jacket_url = "https://static.wikia.nocookie.net/iowiro/images/c/c2/Fracture_Ray.jpg/revision/latest?cb=20230928061927";
|
||||
println!("Jacket url: {}", jacket_url);
|
||||
|
||||
let jacket_text = format!("{} — {}", &triplet.song.title, &triplet.song.artist);
|
||||
|
@ -68,6 +67,4 @@ async fn main() -> Result<(), Error> {
|
|||
ipc.set_activity(activity).map_err(|e| anyhow!("{}", e))?;
|
||||
tokio::time::sleep(Duration::from_secs(30)).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
184
src/commands/calc.rs
Normal file
184
src/commands/calc.rs
Normal file
|
@ -0,0 +1,184 @@
|
|||
// {{{ Imports
|
||||
use num::{FromPrimitive, Rational32};
|
||||
|
||||
use crate::arcaea::play::{compute_b30_ptt, get_best_plays};
|
||||
use crate::arcaea::rating::{rating_as_float, rating_from_fixed, Rating};
|
||||
use crate::context::{Context, Error, TaggedError};
|
||||
use crate::recognition::fuzzy_song_name::guess_song_and_chart;
|
||||
use crate::user::User;
|
||||
|
||||
use crate::arcaea::score::{Score, ScoringSystem};
|
||||
|
||||
use super::discord::MessageContext;
|
||||
// }}}
|
||||
|
||||
// {{{ Top command
|
||||
/// Compute different things
|
||||
#[poise::command(
|
||||
prefix_command,
|
||||
slash_command,
|
||||
subcommands("expected", "rating"),
|
||||
subcommand_required
|
||||
)]
|
||||
pub async fn calc(_ctx: Context<'_>) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
// }}}
|
||||
// {{{ Expected
|
||||
// {{{ Implementation
|
||||
async fn expected_impl(
|
||||
ctx: &mut impl MessageContext,
|
||||
ptt: Option<Rational32>,
|
||||
name: &str,
|
||||
) -> Result<Score, TaggedError> {
|
||||
let (song, chart) = guess_song_and_chart(ctx.data(), name)?;
|
||||
|
||||
let ptt = if let Some(ptt) = ptt {
|
||||
ptt
|
||||
} else {
|
||||
let user = User::from_context(ctx)?;
|
||||
compute_b30_ptt(
|
||||
ScoringSystem::Standard,
|
||||
&get_best_plays(ctx.data(), user.id, ScoringSystem::Standard, 30, 30, None)?,
|
||||
)
|
||||
};
|
||||
|
||||
let cc = rating_from_fixed(chart.chart_constant as i32);
|
||||
|
||||
let score = if ptt >= cc + 2 {
|
||||
Rational32::from_integer(chart.note_count as i32 + 10_000_000)
|
||||
} else if ptt >= cc + 1 {
|
||||
Rational32::from_integer(9_800_000)
|
||||
+ (ptt - cc - 1).reduced() * Rational32::from_integer(200_000)
|
||||
} else {
|
||||
Rational32::from_integer(9_500_000)
|
||||
+ (ptt - cc).reduced() * Rational32::from_integer(300_000)
|
||||
};
|
||||
let score = Score(score.to_integer().max(0) as u32);
|
||||
|
||||
ctx.reply(&format!(
|
||||
"The expected score for a player of potential {:.2} on {} [{}] is {}",
|
||||
rating_as_float(ptt),
|
||||
song,
|
||||
chart.difficulty,
|
||||
score
|
||||
))
|
||||
.await?;
|
||||
|
||||
Ok(score)
|
||||
}
|
||||
// }}}
|
||||
// {{{ Tests
|
||||
#[cfg(test)]
|
||||
mod expected_tests {
|
||||
use crate::{
|
||||
commands::discord::mock::MockContext, context::testing::get_mock_context, golden_test,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn consistent_with_rating() -> Result<(), Error> {
|
||||
let (mut ctx, _guard) = get_mock_context().await?;
|
||||
ctx.save_messages = false; // We don't want to waste time writing to a vec
|
||||
|
||||
for i in 0..1_000 {
|
||||
let score = Score(i * 10_000);
|
||||
let rating = score.play_rating(1140);
|
||||
let res = expected_impl(&mut ctx, Some(rating), "Pentiment [BYD]")
|
||||
.await
|
||||
.map_err(|e| e.error)?;
|
||||
assert_eq!(
|
||||
score, res,
|
||||
"Wrong expected score for starting score {score} and rating {rating}"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
golden_test!(basic_usage, "commands/calc/expected/basic_usage");
|
||||
async fn basic_usage(ctx: &mut MockContext) -> Result<(), TaggedError> {
|
||||
expected_impl(
|
||||
ctx,
|
||||
Some(Rational32::from_f32(12.27).unwrap()),
|
||||
"Vicious anti heorism",
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
// {{{ Discord wrapper
|
||||
/// Computes the expected score for a player of some potential on a given chart.
|
||||
#[poise::command(prefix_command, slash_command, user_cooldown = 1)]
|
||||
async fn expected(
|
||||
mut ctx: Context<'_>,
|
||||
#[description = "The potential to compute the expected score for"] ptt: Option<f32>,
|
||||
#[rest]
|
||||
#[description = "Name of chart (difficulty at the end)"]
|
||||
name: String,
|
||||
) -> Result<(), Error> {
|
||||
let res = expected_impl(&mut ctx, ptt.and_then(Rational32::from_f32), &name).await;
|
||||
ctx.handle_error(res).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
// }}}
|
||||
// }}}
|
||||
// {{{ Rating
|
||||
// {{{ Implementation
|
||||
async fn rating_impl(
|
||||
ctx: &mut impl MessageContext,
|
||||
score: Score,
|
||||
name: &str,
|
||||
) -> Result<Rating, TaggedError> {
|
||||
let (song, chart) = guess_song_and_chart(ctx.data(), name)?;
|
||||
|
||||
let rating = score.play_rating(chart.chart_constant);
|
||||
|
||||
ctx.reply(&format!(
|
||||
"The score {} on {} [{}] yields a rating of {:.2}",
|
||||
score,
|
||||
song,
|
||||
chart.difficulty,
|
||||
rating_as_float(rating),
|
||||
))
|
||||
.await?;
|
||||
|
||||
Ok(rating)
|
||||
}
|
||||
// }}}
|
||||
// {{{ Tests
|
||||
#[cfg(test)]
|
||||
mod rating_tests {
|
||||
use crate::{commands::discord::mock::MockContext, golden_test};
|
||||
|
||||
use super::*;
|
||||
|
||||
golden_test!(basic_usage, "commands/calc/rating/basic_usage");
|
||||
async fn basic_usage(ctx: &mut MockContext) -> Result<(), TaggedError> {
|
||||
rating_impl(ctx, Score(9_349_070), "Arcana Eden [PRS]").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
// {{{ Discord wrapper
|
||||
/// Computes the rating (potential) of a play on a given chart.
|
||||
#[poise::command(prefix_command, slash_command, user_cooldown = 1)]
|
||||
async fn rating(
|
||||
mut ctx: Context<'_>,
|
||||
score: u32,
|
||||
#[rest]
|
||||
#[description = "Name of chart (difficulty at the end)"]
|
||||
name: String,
|
||||
) -> Result<(), Error> {
|
||||
let res = rating_impl(&mut ctx, Score(score), &name).await;
|
||||
ctx.handle_error(res).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
// }}}
|
||||
// }}}
|
|
@ -1,5 +1,5 @@
|
|||
use anyhow::anyhow;
|
||||
// {{{ Imports
|
||||
use anyhow::anyhow;
|
||||
use poise::serenity_prelude::{CreateAttachment, CreateEmbed};
|
||||
|
||||
use crate::arcaea::{chart::Side, play::Play};
|
||||
|
@ -138,7 +138,7 @@ mod info_tests {
|
|||
async fn info(
|
||||
mut ctx: Context<'_>,
|
||||
#[rest]
|
||||
#[description = "Name of chart to show (difficulty at the end)"]
|
||||
#[description = "Name of chart (difficulty at the end)"]
|
||||
name: String,
|
||||
) -> Result<(), Error> {
|
||||
let res = info_impl(&mut ctx, &name).await;
|
||||
|
@ -251,7 +251,7 @@ mod best_tests {
|
|||
async fn best(
|
||||
mut ctx: Context<'_>,
|
||||
#[rest]
|
||||
#[description = "Name of chart to show (difficulty at the end)"]
|
||||
#[description = "Name of chart (difficulty at the end)"]
|
||||
name: String,
|
||||
) -> Result<(), Error> {
|
||||
let res = best_impl(&mut ctx, &name).await;
|
||||
|
@ -403,7 +403,7 @@ async fn plot(
|
|||
mut ctx: Context<'_>,
|
||||
scoring_system: Option<ScoringSystem>,
|
||||
#[rest]
|
||||
#[description = "Name of chart to show (difficulty at the end)"]
|
||||
#[description = "Name of chart (difficulty at the end)"]
|
||||
name: String,
|
||||
) -> Result<(), Error> {
|
||||
let res = plot_impl(&mut ctx, scoring_system, name).await;
|
||||
|
|
|
@ -199,6 +199,9 @@ pub mod mock {
|
|||
pub user_id: u64,
|
||||
pub data: UserContext,
|
||||
|
||||
/// If true, messages will be saved in a vec.
|
||||
pub save_messages: bool,
|
||||
|
||||
messages: Vec<ReplyEssence>,
|
||||
}
|
||||
|
||||
|
@ -207,6 +210,7 @@ pub mod mock {
|
|||
Self {
|
||||
data,
|
||||
user_id: 666,
|
||||
save_messages: true,
|
||||
messages: vec![],
|
||||
}
|
||||
}
|
||||
|
@ -274,7 +278,10 @@ pub mod mock {
|
|||
}
|
||||
|
||||
async fn send(&mut self, message: CreateReply) -> Result<(), Error> {
|
||||
self.messages.push(ReplyEssence::from_reply(message));
|
||||
if self.save_messages {
|
||||
self.messages.push(ReplyEssence::from_reply(message));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ pub mod discord;
|
|||
pub mod score;
|
||||
pub mod stats;
|
||||
pub mod utils;
|
||||
pub mod calc;
|
||||
|
||||
// {{{ Help
|
||||
/// Show this help menu
|
||||
|
|
|
@ -141,6 +141,10 @@ impl UserContext {
|
|||
// {{{ Testing helpers
|
||||
#[cfg(test)]
|
||||
pub mod testing {
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::commands::discord::mock::MockContext;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub async fn get_shared_context() -> &'static UserContext {
|
||||
|
@ -165,6 +169,16 @@ pub mod testing {
|
|||
);
|
||||
}
|
||||
|
||||
pub async fn get_mock_context() -> Result<(MockContext, TempDir), Error> {
|
||||
let mut data = (*get_shared_context().await).clone();
|
||||
let dir = tempfile::tempdir()?;
|
||||
data.db = connect_db(dir.path());
|
||||
import_songs_and_jackets_from(dir.path());
|
||||
|
||||
let ctx = MockContext::new(data);
|
||||
Ok((ctx, dir))
|
||||
}
|
||||
|
||||
// rustfmt fucks up the formatting here,
|
||||
// but the skip attribute doesn't seem to work well on macros 🤔
|
||||
#[macro_export]
|
||||
|
@ -184,12 +198,7 @@ pub mod testing {
|
|||
($test_path:expr, $f:expr) => {{
|
||||
use std::str::FromStr;
|
||||
|
||||
let mut data = (*$crate::context::testing::get_shared_context().await).clone();
|
||||
let dir = tempfile::tempdir()?;
|
||||
data.db = $crate::context::connect_db(dir.path());
|
||||
$crate::context::testing::import_songs_and_jackets_from(dir.path());
|
||||
|
||||
let mut ctx = $crate::commands::discord::mock::MockContext::new(data);
|
||||
let (mut ctx, _guard) = $crate::context::testing::get_mock_context().await?;
|
||||
let res = $crate::user::User::create_from_context(&ctx);
|
||||
ctx.handle_error(res).await?;
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
#![allow(async_fn_in_trait)]
|
||||
#![allow(clippy::needless_range_loop)]
|
||||
#![allow(clippy::redundant_closure)]
|
||||
// This sometimes triggers for rationals, where it doesn't make sense
|
||||
#![allow(clippy::int_plus_one)]
|
||||
|
||||
pub mod arcaea;
|
||||
pub mod assets;
|
||||
|
|
|
@ -32,22 +32,23 @@ pub fn guess_song_and_chart<'a>(
|
|||
ctx: &'a UserContext,
|
||||
name: &'a str,
|
||||
) -> Result<(&'a Song, &'a Chart), Error> {
|
||||
let name = name.trim();
|
||||
let (name, difficulty) = name
|
||||
.strip_suffix("PST")
|
||||
.zip(Some(Difficulty::PST))
|
||||
.or_else(|| strip_case_insensitive_suffix(name, "[PST]").zip(Some(Difficulty::PST)))
|
||||
.or_else(|| strip_case_insensitive_suffix(name, "PRS").zip(Some(Difficulty::PRS)))
|
||||
.or_else(|| strip_case_insensitive_suffix(name, "[PRS]").zip(Some(Difficulty::PRS)))
|
||||
.or_else(|| strip_case_insensitive_suffix(name, "FTR").zip(Some(Difficulty::FTR)))
|
||||
.or_else(|| strip_case_insensitive_suffix(name, "[FTR]").zip(Some(Difficulty::FTR)))
|
||||
.or_else(|| strip_case_insensitive_suffix(name, "ETR").zip(Some(Difficulty::ETR)))
|
||||
.or_else(|| strip_case_insensitive_suffix(name, "[ETR]").zip(Some(Difficulty::ETR)))
|
||||
.or_else(|| strip_case_insensitive_suffix(name, "BYD").zip(Some(Difficulty::BYD)))
|
||||
.or_else(|| strip_case_insensitive_suffix(name, "[BYD]").zip(Some(Difficulty::BYD)))
|
||||
.unwrap_or((name, Difficulty::FTR));
|
||||
let mut name = name.trim();
|
||||
let mut inferred_difficulty = None;
|
||||
|
||||
guess_chart_name(name, &ctx.song_cache, Some(difficulty), true)
|
||||
for difficulty in Difficulty::DIFFICULTIES {
|
||||
for shorthand in [
|
||||
Difficulty::DIFFICULTY_SHORTHANDS[difficulty.to_index()],
|
||||
Difficulty::DIFFICULTY_SHORTHANDS_IN_BRACKETS[difficulty.to_index()],
|
||||
] {
|
||||
if let Some(stripped) = strip_case_insensitive_suffix(name, shorthand) {
|
||||
inferred_difficulty = Some(difficulty);
|
||||
name = stripped;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guess_chart_name(name, &ctx.song_cache, inferred_difficulty, true)
|
||||
}
|
||||
// }}}
|
||||
// {{{ Guess chart by name
|
||||
|
@ -74,11 +75,20 @@ pub fn guess_chart_name<'a>(
|
|||
let mut close_enough: Vec<_> = cache
|
||||
.charts()
|
||||
.filter_map(|chart| {
|
||||
if difficulty.map_or(false, |d| d != chart.difficulty) {
|
||||
let cached_song = &cache.lookup_song(chart.song_id).ok()?;
|
||||
let song = &cached_song.song;
|
||||
let plausible_difficulty = match difficulty {
|
||||
Some(difficulty) => difficulty == chart.difficulty,
|
||||
None => {
|
||||
let chart_count = cached_song.charts().count();
|
||||
chart_count == 1 || chart.difficulty == Difficulty::FTR
|
||||
}
|
||||
};
|
||||
|
||||
if !plausible_difficulty {
|
||||
return None;
|
||||
}
|
||||
|
||||
let song = &cache.lookup_song(chart.song_id).ok()?.song;
|
||||
let song_title = &song.lowercase_title;
|
||||
distance_vec.clear();
|
||||
|
||||
|
|
4
test/commands/calc/expected/basic_usage/0.toml
Normal file
4
test/commands/calc/expected/basic_usage/0.toml
Normal file
|
@ -0,0 +1,4 @@
|
|||
reply = true
|
||||
content = "The expected score for a player of potential 12.27 on Vicious [ANTi] Heroism [BYD] is 9'834'000"
|
||||
embeds = []
|
||||
attachments = []
|
4
test/commands/calc/rating/basic_usage/0.toml
Normal file
4
test/commands/calc/rating/basic_usage/0.toml
Normal file
|
@ -0,0 +1,4 @@
|
|||
reply = true
|
||||
content = "The score 9'349'070 on Arcana Eden [PRS] yields a rating of 8.20"
|
||||
embeds = []
|
||||
attachments = []
|
52
test/commands/commands/chart/info/no_suffix/0.toml
Normal file
52
test/commands/commands/chart/info/no_suffix/0.toml
Normal file
|
@ -0,0 +1,52 @@
|
|||
reply = true
|
||||
|
||||
[[embeds]]
|
||||
title = "Pentiment [FTR 10]"
|
||||
type = "rich"
|
||||
|
||||
[embeds.thumbnail]
|
||||
url = "attachment://chart.png"
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Note count"
|
||||
value = "1345"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Chart constant"
|
||||
value = "10.3"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Total plays"
|
||||
value = "0"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "BPM"
|
||||
value = "200-222"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Side"
|
||||
value = "conflict"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Artist"
|
||||
value = "Pentiment"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Note design"
|
||||
value = "Paradox Blight"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Pack"
|
||||
value = "Final Verdict"
|
||||
inline = true
|
||||
|
||||
[[attachments]]
|
||||
filename = "chart.png"
|
||||
hash = "sha256_5ffda660ce1c6ddd7c60bbb7f34443a7772e608f930768a147e423cc62b7e25d"
|
52
test/commands/commands/chart/info/specify_difficulty/0.toml
Normal file
52
test/commands/commands/chart/info/specify_difficulty/0.toml
Normal file
|
@ -0,0 +1,52 @@
|
|||
reply = true
|
||||
|
||||
[[embeds]]
|
||||
title = "HELLOHELL [ETR 9]"
|
||||
type = "rich"
|
||||
|
||||
[embeds.thumbnail]
|
||||
url = "attachment://chart.png"
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Note count"
|
||||
value = "770"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Chart constant"
|
||||
value = "9.4"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Total plays"
|
||||
value = "0"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "BPM"
|
||||
value = "155"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Side"
|
||||
value = "conflict"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Artist"
|
||||
value = "HELLOHELL"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Note design"
|
||||
value = "eién"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Pack"
|
||||
value = "World Extend 3: Illusions"
|
||||
inline = true
|
||||
|
||||
[[attachments]]
|
||||
filename = "chart.png"
|
||||
hash = "sha256_e00a92ba1abbcf97c7b006447867914b9d95dc4dfb039e05260d71128b60eedb"
|
57
test/score/magic/single_pic/0.toml
Normal file
57
test/score/magic/single_pic/0.toml
Normal file
|
@ -0,0 +1,57 @@
|
|||
reply = true
|
||||
|
||||
[[embeds]]
|
||||
title = "ALTER EGO [FTR 10]"
|
||||
type = "rich"
|
||||
|
||||
[embeds.thumbnail]
|
||||
url = "attachment://416-9926250-0.png"
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Score"
|
||||
value = "9'926'250"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Rating"
|
||||
value = "12.13"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Grade"
|
||||
value = "EX+"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "ξ-Score"
|
||||
value = "9'693'042"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "ξ-Rating"
|
||||
value = "11.14"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "ξ-Grade"
|
||||
value = "AA"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Status"
|
||||
value = "C (-164/-12/-5)"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Max recall"
|
||||
value = "397"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "ID"
|
||||
value = "1"
|
||||
inline = true
|
||||
|
||||
[[attachments]]
|
||||
filename = "416-9926250-0.png"
|
||||
hash = "sha256_5f1febcdf44bc22bf7ef5bff0cee197a65a221277ebe9615d469002b4324ada3"
|
113
test/score/magic/weird_kerning/0.toml
Normal file
113
test/score/magic/weird_kerning/0.toml
Normal file
|
@ -0,0 +1,113 @@
|
|||
reply = true
|
||||
|
||||
[[embeds]]
|
||||
title = "Antithese [FTR 8+]"
|
||||
type = "rich"
|
||||
|
||||
[embeds.thumbnail]
|
||||
url = "attachment://116-9983744-0.png"
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Score"
|
||||
value = "9'983'744"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Rating"
|
||||
value = "10.72"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Grade"
|
||||
value = "EX+"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "ξ-Score"
|
||||
value = "9'920'182"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "ξ-Rating"
|
||||
value = "10.40"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "ξ-Grade"
|
||||
value = "EX+"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Status"
|
||||
value = "C (-27/-1/-1)"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Max recall"
|
||||
value = "479"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "ID"
|
||||
value = "1"
|
||||
inline = true
|
||||
|
||||
[[embeds]]
|
||||
title = "GENOCIDER [FTR 10+]"
|
||||
type = "rich"
|
||||
|
||||
[embeds.thumbnail]
|
||||
url = "attachment://243-9724775-1.png"
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Score"
|
||||
value = "9'724'775"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Rating"
|
||||
value = "11.45"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Grade"
|
||||
value = "AA"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "ξ-Score"
|
||||
value = "9'453'809"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "ξ-Rating"
|
||||
value = "10.55"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "ξ-Grade"
|
||||
value = "A"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Status"
|
||||
value = "C (-180/-40/-21)"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "Max recall"
|
||||
value = "347"
|
||||
inline = true
|
||||
|
||||
[[embeds.fields]]
|
||||
name = "ID"
|
||||
value = "2"
|
||||
inline = true
|
||||
|
||||
[[attachments]]
|
||||
filename = "116-9983744-0.png"
|
||||
hash = "sha256_75b03ac3392d4bcb9d377396a36708aea1298dd463c08d5d62ca1e1414bfeaef"
|
||||
|
||||
[[attachments]]
|
||||
filename = "243-9724775-1.png"
|
||||
hash = "sha256_4a8ad7af5482104b3ec7c9a50091bb6f01df10a0cc758837cf47dcf7d73fd59d"
|
Loading…
Reference in a new issue