diff --git a/src/arcaea/chart.rs b/src/arcaea/chart.rs
index 81f86da..fd41a0f 100644
--- a/src/arcaea/chart.rs
+++ b/src/arcaea/chart.rs
@@ -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,
 			})
 		})?;
 
diff --git a/src/arcaea/jacket.rs b/src/arcaea/jacket.rs
index 1597070..cf06e0b 100644
--- a/src/arcaea/jacket.rs
+++ b/src/arcaea/jacket.rs
@@ -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;
 							}
 						}
 					}
diff --git a/src/arcaea/rating.rs b/src/arcaea/rating.rs
index 149d197..d0fb45a 100644
--- a/src/arcaea/rating.rs
+++ b/src/arcaea/rating.rs
@@ -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`.
diff --git a/src/bin/cli/commands/prepare_jackets.rs b/src/bin/cli/commands/prepare_jackets.rs
index 771aed3..17287c3 100644
--- a/src/bin/cli/commands/prepare_jackets.rs
+++ b/src/bin/cli/commands/prepare_jackets.rs
@@ -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(())
diff --git a/src/bin/discord-bot/main.rs b/src/bin/discord-bot/main.rs
index 3090402..b2d1b4a 100644
--- a/src/bin/discord-bot/main.rs
+++ b/src/bin/discord-bot/main.rs
@@ -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| {
diff --git a/src/bin/discord-presence/main.rs b/src/bin/discord-presence/main.rs
index 3be806a..4408949 100644
--- a/src/bin/discord-presence/main.rs
+++ b/src/bin/discord-presence/main.rs
@@ -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(())
 }
diff --git a/src/commands/calc.rs b/src/commands/calc.rs
new file mode 100644
index 0000000..663eb9d
--- /dev/null
+++ b/src/commands/calc.rs
@@ -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(())
+}
+// }}}
+// }}}
diff --git a/src/commands/chart.rs b/src/commands/chart.rs
index 8db25e3..5c982f0 100644
--- a/src/commands/chart.rs
+++ b/src/commands/chart.rs
@@ -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;
diff --git a/src/commands/discord.rs b/src/commands/discord.rs
index e00ffca..bf8a27b 100644
--- a/src/commands/discord.rs
+++ b/src/commands/discord.rs
@@ -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(())
 		}
 
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
index 0a49309..e14401a 100644
--- a/src/commands/mod.rs
+++ b/src/commands/mod.rs
@@ -5,6 +5,7 @@ pub mod discord;
 pub mod score;
 pub mod stats;
 pub mod utils;
+pub mod calc;
 
 // {{{ Help
 /// Show this help menu
diff --git a/src/context.rs b/src/context.rs
index 8f0c86b..3542ecf 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -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?;
 
diff --git a/src/lib.rs b/src/lib.rs
index 979f3f5..6470d20 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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;
diff --git a/src/recognition/fuzzy_song_name.rs b/src/recognition/fuzzy_song_name.rs
index e56efd0..a8fda98 100644
--- a/src/recognition/fuzzy_song_name.rs
+++ b/src/recognition/fuzzy_song_name.rs
@@ -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();
 
diff --git a/test/commands/calc/expected/basic_usage/0.toml b/test/commands/calc/expected/basic_usage/0.toml
new file mode 100644
index 0000000..22d9a00
--- /dev/null
+++ b/test/commands/calc/expected/basic_usage/0.toml
@@ -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 = []
diff --git a/test/commands/calc/rating/basic_usage/0.toml b/test/commands/calc/rating/basic_usage/0.toml
new file mode 100644
index 0000000..ed29156
--- /dev/null
+++ b/test/commands/calc/rating/basic_usage/0.toml
@@ -0,0 +1,4 @@
+reply = true
+content = "The score 9'349'070 on Arcana Eden [PRS] yields a rating of 8.20"
+embeds = []
+attachments = []
diff --git a/test/commands/commands/chart/info/no_suffix/0.toml b/test/commands/commands/chart/info/no_suffix/0.toml
new file mode 100644
index 0000000..4ee7b3d
--- /dev/null
+++ b/test/commands/commands/chart/info/no_suffix/0.toml
@@ -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"
diff --git a/test/commands/commands/chart/info/specify_difficulty/0.toml b/test/commands/commands/chart/info/specify_difficulty/0.toml
new file mode 100644
index 0000000..f3d63c0
--- /dev/null
+++ b/test/commands/commands/chart/info/specify_difficulty/0.toml
@@ -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"
diff --git a/test/score/magic/single_pic/0.toml b/test/score/magic/single_pic/0.toml
new file mode 100644
index 0000000..054680a
--- /dev/null
+++ b/test/score/magic/single_pic/0.toml
@@ -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"
diff --git a/test/score/magic/weird_kerning/0.toml b/test/score/magic/weird_kerning/0.toml
new file mode 100644
index 0000000..740214a
--- /dev/null
+++ b/test/score/magic/weird_kerning/0.toml
@@ -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"