1
Fork 0

Generalize code to more scoring systems

This commit is contained in:
prescientmoon 2024-08-20 19:24:32 +02:00
parent 62949004f2
commit 4ed3fe276b
Signed by: prescientmoon
SSH key fingerprint: SHA256:UUF9JT2s8Xfyv76b8ZuVL7XrmimH4o49p4b+iexbVH4
10 changed files with 375 additions and 226 deletions

View file

@ -1,10 +1,10 @@
# {{{ users
# }}}
create table IF NOT EXISTS users (
id INTEGER NOT NULL PRIMARY KEY,
discord_id TEXT UNIQUE NOT NULL,
is_pookie BOOL NOT NULL DEFAULT 0
);
# }}}
# {{{ songs
CREATE TABLE IF NOT EXISTS songs (
id INTEGER NOT NULL PRIMARY KEY,
@ -55,5 +55,18 @@ CREATE TABLE IF NOT EXISTS plays (
FOREIGN KEY (user_id) REFERENCES users(id)
);
# }}}
# {{{ scores
CREATE TABLE IF NOT EXISTS scores (
id INTEGER NOT NULL PRIMARY KEY,
play_id INTEGER NOT NULL,
score INTEGER NOT NULL,
creation_ptt INTEGER,
scoring_system NOT NULL CHECK (scoring_system IN ('standard', 'ex')),
FOREIGN KEY (play_id) REFERENCES plays(id),
UNIQUE(play_id, scoring_system)
)
# }}}
insert into users(discord_id) values (385759924917108740);

View file

@ -125,10 +125,11 @@ impl GoalStats {
let plays = get_best_plays(
&ctx.db,
&ctx.song_cache,
user,
user.id,
scoring_system,
0,
usize::MAX,
None,
)
.await??;
@ -150,22 +151,20 @@ impl GoalStats {
.count as usize;
// }}}
// {{{ Peak ptt
let peak_ptt = {
let record = query!(
"
SELECT
max(creation_ptt) as standard,
max(creation_zeta_ptt) as ex
FROM plays
"
)
.fetch_one(&ctx.db)
.await?;
match scoring_system {
ScoringSystem::Standard => record.standard,
ScoringSystem::EX => record.ex,
}
}
let peak_ptt = query!(
"
SELECT s.creation_ptt
FROM plays p
JOIN scores s ON s.play_id = p.id
WHERE user_id = ?
AND scoring_system = ?
",
user.id,
ScoringSystem::SCORING_SYSTEM_DB_STRINGS[scoring_system.to_index()]
)
.fetch_one(&ctx.db)
.await?
.creation_ptt
.ok_or_else(|| "No ptt history data found")? as u32;
// }}}
// {{{ Peak PM relay

View file

@ -2,4 +2,5 @@ pub mod achievement;
pub mod chart;
pub mod jacket;
pub mod play;
pub mod rating;
pub mod score;

View file

@ -1,41 +1,42 @@
use std::str::FromStr;
use std::array;
use chrono::NaiveDateTime;
use chrono::Utc;
use num::traits::Euclid;
use num::CheckedDiv;
use num::Rational32;
use num::Zero;
use poise::serenity_prelude::{
Attachment, AttachmentId, CreateAttachment, CreateEmbed, CreateEmbedAuthor, Timestamp,
};
use sqlx::{query, query_as, SqlitePool};
use sqlx::query_as;
use sqlx::{query, SqlitePool};
use crate::arcaea::chart::{Chart, Song};
use crate::context::{Error, UserContext};
use crate::user::User;
use super::chart::SongCache;
use super::rating::{rating_as_fixed, rating_as_float};
use super::score::{Score, ScoringSystem};
// {{{ Create play
#[derive(Debug, Clone)]
pub struct CreatePlay {
chart_id: u32,
discord_attachment_id: Option<AttachmentId>,
// Actual score data
// Scoring details
score: Score,
zeta_score: Score,
// Optional score details
max_recall: Option<u32>,
far_notes: Option<u32>,
}
impl CreatePlay {
#[inline]
pub fn new(score: Score, chart: &Chart) -> Self {
pub fn new(score: Score) -> Self {
Self {
chart_id: chart.id,
discord_attachment_id: None,
score,
zeta_score: score.to_zeta(chart.note_count as u32),
max_recall: None,
far_notes: None,
}
@ -60,24 +61,22 @@ impl CreatePlay {
}
// {{{ Save
pub async fn save(self, ctx: &UserContext, user: &User) -> Result<Play, Error> {
pub async fn save(self, ctx: &UserContext, user: &User, chart: &Chart) -> Result<Play, Error> {
let attachment_id = self.discord_attachment_id.map(|i| i.get() as i64);
// {{{ Save current data to play
let play = sqlx::query!(
"
INSERT INTO plays(
user_id,chart_id,discord_attachment_id,
score,zeta_score,max_recall,far_notes
user_id,chart_id,discord_attachment_id,
max_recall,far_notes
)
VALUES(?,?,?,?,?,?,?)
VALUES(?,?,?,?,?)
RETURNING id, created_at
",
user.id,
self.chart_id,
chart.id,
attachment_id,
self.score.0,
self.zeta_score.0,
self.max_recall,
self.far_notes
)
@ -85,93 +84,88 @@ impl CreatePlay {
.await?;
// }}}
// {{{ Update creation ptt data
let creation_ptt = get_best_plays(
&ctx.db,
&ctx.song_cache,
user,
ScoringSystem::Standard,
30,
30,
)
.await?
.ok()
.map(|plays| compute_b30_ptt(ScoringSystem::Standard, &plays));
let creation_zeta_ptt =
get_best_plays(&ctx.db, &ctx.song_cache, user, ScoringSystem::EX, 30, 30)
let scores = ScoreCollection::from_standard_score(self.score, chart);
for system in ScoringSystem::SCORING_SYSTEMS {
let i = system.to_index();
let plays = get_best_plays(&ctx.db, &ctx.song_cache, user.id, system, 30, 30, None)
.await?
.ok()
.map(|plays| compute_b30_ptt(ScoringSystem::EX, &plays));
.ok();
let creation_ptt: Option<_> = try { rating_as_fixed(compute_b30_ptt(system, &plays?)) };
query!(
"
INSERT INTO scores(play_id, score, creation_ptt, scoring_system)
VALUES (?,?,?,?)
",
play.id,
scores.0[i].0,
creation_ptt,
ScoringSystem::SCORING_SYSTEM_DB_STRINGS[i]
)
.execute(&ctx.db)
.await?;
}
query!(
"
UPDATE plays
SET
creation_ptt=?,
creation_zeta_ptt=?
WHERE
id=?
",
creation_ptt,
creation_zeta_ptt,
play.id
)
.execute(&ctx.db)
.await?;
// }}}
Ok(Play {
id: play.id as u32,
created_at: play.created_at,
chart_id: self.chart_id,
chart_id: chart.id,
user_id: user.id,
discord_attachment_id: self.discord_attachment_id,
score: self.score,
zeta_score: self.zeta_score,
scores,
max_recall: self.max_recall,
far_notes: self.far_notes,
creation_ptt,
creation_zeta_ptt,
})
}
// }}}
}
// }}}
// {{{ DbPlay
/// Version of `Play` matching the format sqlx expects
#[derive(Debug, Clone, sqlx::FromRow)]
/// Construct a `Play` from a sqlite return record.
#[macro_export]
macro_rules! play_from_db_record {
($chart:expr, $record:expr) => {{
use crate::arcaea::play::{Play, ScoreCollection};
use crate::arcaea::score::Score;
Play {
id: $record.id as u32,
chart_id: $record.chart_id as u32,
user_id: $record.user_id as u32,
scores: ScoreCollection::from_standard_score(Score($record.score as u32), $chart),
max_recall: $record.max_recall.map(|r| r as u32),
far_notes: $record.far_notes.map(|r| r as u32),
created_at: $record.created_at,
}
}};
}
/// Typed version of the input to the macro above.
/// Useful when using the non-macro version of the sqlx functions.
#[derive(Debug, sqlx::FromRow)]
pub struct DbPlay {
pub id: i64,
pub chart_id: i64,
pub user_id: i64,
pub discord_attachment_id: Option<String>,
pub score: i64,
pub zeta_score: i64,
pub created_at: chrono::NaiveDateTime,
// Score details
pub max_recall: Option<i64>,
pub far_notes: Option<i64>,
pub created_at: chrono::NaiveDateTime,
pub creation_ptt: Option<i64>,
pub creation_zeta_ptt: Option<i64>,
pub score: i64,
}
impl DbPlay {
#[inline]
pub fn into_play(self) -> Play {
Play {
id: self.id as u32,
chart_id: self.chart_id as u32,
user_id: self.user_id as u32,
score: Score(self.score as u32),
zeta_score: Score(self.zeta_score as u32),
max_recall: self.max_recall.map(|r| r as u32),
far_notes: self.far_notes.map(|r| r as u32),
created_at: self.created_at,
discord_attachment_id: self
.discord_attachment_id
.and_then(|s| AttachmentId::from_str(&s).ok()),
creation_ptt: self.creation_ptt.map(|r| r as i32),
creation_zeta_ptt: self.creation_zeta_ptt.map(|r| r as i32),
}
// }}}
// {{{ Score data
#[derive(Debug, Clone, Copy)]
pub struct ScoreCollection([Score; ScoringSystem::SCORING_SYSTEMS.len()]);
impl ScoreCollection {
pub fn from_standard_score(score: Score, chart: &Chart) -> Self {
ScoreCollection(array::from_fn(|i| {
score.convert_to(ScoringSystem::SCORING_SYSTEMS[i], chart)
}))
}
}
// }}}
@ -179,48 +173,38 @@ impl DbPlay {
#[derive(Debug, Clone)]
pub struct Play {
pub id: u32,
#[allow(unused)]
pub chart_id: u32,
pub user_id: u32,
#[allow(unused)]
pub discord_attachment_id: Option<AttachmentId>,
// Actual score data
pub score: Score,
pub zeta_score: Score,
// Optional score details
pub max_recall: Option<u32>,
pub far_notes: Option<u32>,
// Creation data
pub created_at: chrono::NaiveDateTime,
#[allow(dead_code)]
pub creation_ptt: Option<i32>,
#[allow(dead_code)]
pub creation_zeta_ptt: Option<i32>,
// Score details
pub max_recall: Option<u32>,
pub far_notes: Option<u32>,
pub scores: ScoreCollection,
}
impl Play {
// {{{ Query the underlying score
#[inline]
pub fn score(&self, system: ScoringSystem) -> Score {
match system {
ScoringSystem::Standard => self.score,
ScoringSystem::EX => self.zeta_score,
}
self.scores.0[system.to_index()]
}
#[inline]
pub fn play_rating(&self, system: ScoringSystem, chart_constant: u32) -> i32 {
pub fn play_rating(&self, system: ScoringSystem, chart_constant: u32) -> Rational32 {
self.score(system).play_rating(chart_constant)
}
#[inline]
pub fn play_rating_f32(&self, system: ScoringSystem, chart_constant: u32) -> f32 {
rating_as_float(self.score(system).play_rating(chart_constant))
}
// }}}
// {{{ Play => distribution
pub fn distribution(&self, note_count: u32) -> Option<(u32, u32, u32, u32)> {
if let Some(fars) = self.far_notes {
let (_, shinies, units) = self.score.analyse(note_count);
let (_, shinies, units) = self.score(ScoringSystem::Standard).analyse(note_count);
let (pures, rem) = units.checked_sub(fars)?.div_rem_euclid(&2);
if rem == 1 {
println!("The impossible happened: got an invalid amount of far notes!");
@ -237,8 +221,8 @@ impl Play {
// }}}
// {{{ Play => status
#[inline]
pub fn status(&self, chart: &Chart) -> Option<String> {
let score = self.score.0;
pub fn status(&self, scoring_system: ScoringSystem, chart: &Chart) -> Option<String> {
let score = self.score(scoring_system).0;
if score >= 10_000_000 {
if score > chart.note_count + 10_000_000 {
return None;
@ -266,8 +250,8 @@ impl Play {
}
#[inline]
pub fn short_status(&self, chart: &Chart) -> Option<char> {
let score = self.score.0;
pub fn short_status(&self, scoring_system: ScoringSystem, chart: &Chart) -> Option<char> {
let score = self.score(scoring_system).0;
if score >= 10_000_000 {
let non_max_pures = (chart.note_count + 10_000_000).checked_sub(score)?;
if non_max_pures == 0 {
@ -298,14 +282,18 @@ impl Play {
author: Option<&poise::serenity_prelude::User>,
) -> Result<(CreateEmbed, Option<CreateAttachment>), Error> {
// {{{ Get previously best score
let prev_play = query_as!(
DbPlay,
let prev_play = query!(
"
SELECT * FROM plays
WHERE user_id=?
AND chart_id=?
AND created_at<?
ORDER BY score DESC
SELECT
p.id, p.chart_id, p.user_id, p.created_at,
p.max_recall, p.far_notes, s.score
FROM plays p
JOIN scores s ON s.play_id = p.id
WHERE s.scoring_system='standard'
AND p.user_id=?
AND p.chart_id=?
AND p.created_at<?
ORDER BY s.score DESC
LIMIT 1
",
user.id,
@ -320,13 +308,18 @@ impl Play {
song.title, chart.difficulty
)
})?
.map(|p| p.into_play());
.map(|p| play_from_db_record!(chart, p));
let prev_score = prev_play.as_ref().map(|p| p.score);
let prev_zeta_score = prev_play.as_ref().map(|p| p.zeta_score);
let prev_score = prev_play.as_ref().map(|p| p.score(ScoringSystem::Standard));
let prev_zeta_score = prev_play.as_ref().map(|p| p.score(ScoringSystem::EX));
// }}}
let attachement_name = format!("{:?}-{:?}-{:?}.png", song.id, self.score.0, index);
let attachement_name = format!(
"{:?}-{:?}-{:?}.png",
song.id,
self.score(ScoringSystem::Standard).0,
index
);
let icon_attachement = match chart.cached_jacket.as_ref() {
Some(jacket) => Some(CreateAttachment::bytes(jacket.raw, &attachement_name)),
None => None,
@ -337,30 +330,46 @@ impl Play {
"{} [{:?} {}]",
&song.title, chart.difficulty, chart.level
))
.field("Score", self.score.display_with_diff(prev_score)?, true)
.field(
"Rating",
self.score.display_play_rating(prev_score, chart)?,
"Score",
self.score(ScoringSystem::Standard)
.display_with_diff(prev_score)?,
true,
)
.field(
"Rating",
self.score(ScoringSystem::Standard)
.display_play_rating(prev_score, chart)?,
true,
)
.field(
"Grade",
format!("{}", self.score(ScoringSystem::Standard).grade()),
true,
)
.field("Grade", format!("{}", self.score.grade()), true)
.field(
"ξ-Score",
self.zeta_score.display_with_diff(prev_zeta_score)?,
self.score(ScoringSystem::EX)
.display_with_diff(prev_zeta_score)?,
true,
)
// {{{ ξ-Rating
.field(
"ξ-Rating",
self.zeta_score
self.score(ScoringSystem::EX)
.display_play_rating(prev_zeta_score, chart)?,
true,
)
// }}}
.field("ξ-Grade", format!("{}", self.zeta_score.grade()), true)
.field(
"ξ-Grade",
format!("{}", self.score(ScoringSystem::EX).grade()),
true,
)
.field(
"Status",
self.status(chart).unwrap_or("-".to_string()),
self.status(ScoringSystem::Standard, chart)
.unwrap_or("-".to_string()),
true,
)
.field(
@ -402,24 +411,33 @@ pub type PlayCollection<'a> = Vec<(Play, &'a Song, &'a Chart)>;
pub async fn get_best_plays<'a>(
db: &SqlitePool,
song_cache: &'a SongCache,
user: &User,
user_id: u32,
scoring_system: ScoringSystem,
min_amount: usize,
max_amount: usize,
before: Option<NaiveDateTime>,
) -> Result<Result<PlayCollection<'a>, String>, Error> {
// {{{ DB data fetching
let plays: Vec<DbPlay> = query_as(
"
SELECT id, chart_id, user_id,
created_at, MAX(score) as score, zeta_score,
creation_ptt, creation_zeta_ptt, far_notes, max_recall, discord_attachment_id
FROM plays p
WHERE user_id = ?
GROUP BY chart_id
ORDER BY score DESC
SELECT
p.id, p.chart_id, p.user_id, p.created_at,
p.max_recall, p.far_notes, s.score,
MAX(s.score) as _cscore
-- ^ This is only here to make sqlite pick the correct row for the bare columns
FROM plays p
JOIN scores s ON s.play_id = p.id
JOIN scores cs ON cs.play_id = p.id
WHERE s.scoring_system='standard'
AND cs.scoring_system=?
AND p.user_id=?
AND p.created_at<=?
GROUP BY p.chart_id
",
)
.bind(user.id)
.bind(ScoringSystem::SCORING_SYSTEM_DB_STRINGS[scoring_system.to_index()])
.bind(user_id)
.bind(before.unwrap_or_else(|| Utc::now().naive_utc()))
.fetch_all(db)
.await?;
// }}}
@ -437,8 +455,8 @@ pub async fn get_best_plays<'a>(
let mut plays: Vec<(Play, &Song, &Chart)> = plays
.into_iter()
.map(|play| {
let play = play.into_play();
let (song, chart) = song_cache.lookup_chart(play.chart_id)?;
let (song, chart) = song_cache.lookup_chart(play.chart_id as u32)?;
let play = play_from_db_record!(chart, play);
Ok((play, song, chart))
})
.collect::<Result<Vec<_>, Error>>()?;
@ -451,12 +469,78 @@ pub async fn get_best_plays<'a>(
}
#[inline]
pub fn compute_b30_ptt(scoring_system: ScoringSystem, plays: &PlayCollection<'_>) -> i32 {
pub fn compute_b30_ptt(scoring_system: ScoringSystem, plays: &PlayCollection<'_>) -> Rational32 {
plays
.iter()
.map(|(play, _, chart)| play.play_rating(scoring_system, chart.chart_constant))
.sum::<i32>()
.checked_div(plays.len() as i32)
.unwrap_or(0)
.sum::<Rational32>()
.checked_div(&Rational32::from_integer(plays.len() as i32))
.unwrap_or(Rational32::zero())
}
// }}}
// {{{ Maintenance functions
pub async fn generate_missing_scores(ctx: &UserContext) -> Result<(), Error> {
let plays = query!(
"
SELECT
p.id, p.chart_id, p.user_id, p.created_at,
p.max_recall, p.far_notes, s.score
FROM plays p
JOIN scores s ON s.play_id = p.id
WHERE s.scoring_system='standard'
ORDER BY p.created_at ASC
"
)
// Can't use the stream based version because of db locking...
.fetch_all(&ctx.db)
.await?;
let mut i = 0;
for play in plays {
let (_, chart) = ctx.song_cache.lookup_chart(play.chart_id as u32)?;
let play = play_from_db_record!(chart, play);
for system in ScoringSystem::SCORING_SYSTEMS {
let i = system.to_index();
let plays = get_best_plays(
&ctx.db,
&ctx.song_cache,
play.user_id,
system,
30,
30,
Some(play.created_at),
)
.await?
.ok();
let creation_ptt: Option<_> = try { rating_as_fixed(compute_b30_ptt(system, &plays?)) };
let raw_score = play.scores.0[i].0;
query!(
"
INSERT INTO scores(play_id, score, creation_ptt, scoring_system)
VALUES ($1, $2, $3, $4)
ON CONFLICT(play_id, scoring_system)
DO UPDATE SET
score=$2, creation_ptt=$3
WHERE play_id = $1
AND scoring_system = $4
",
play.id,
raw_score,
creation_ptt,
ScoringSystem::SCORING_SYSTEM_DB_STRINGS[i],
)
.execute(&ctx.db)
.await?;
}
i += 1;
println!("Processed {i} plays");
}
Ok(())
}
// }}}

23
src/arcaea/rating.rs Normal file
View file

@ -0,0 +1,23 @@
use num::Rational32;
pub type Rating = Rational32;
/// Saves a rating rational as an integer where it's multiplied by 100.
#[inline]
pub fn rating_as_fixed(rating: Rating) -> i32 {
(rating * Rational32::from_integer(100))
.round()
.to_integer()
}
/// 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
}
/// The pseudo-inverse of `rating_as_fixed`.
#[inline]
pub fn rating_from_fixed(fixed: i32) -> Rating {
Rating::new(fixed, 100)
}

View file

@ -1,10 +1,13 @@
use std::fmt::{Display, Write};
use num::Rational64;
use num::{Rational32, Rational64};
use crate::context::Error;
use super::chart::Chart;
use super::{
chart::Chart,
rating::{rating_as_float, rating_from_fixed, Rating},
};
// {{{ Scoring system
#[derive(Debug, Clone, Copy, poise::ChoiceParameter)]
@ -15,6 +18,18 @@ pub enum ScoringSystem {
EX,
}
impl ScoringSystem {
pub const SCORING_SYSTEMS: [Self; 2] = [Self::Standard, Self::EX];
/// Values used inside sqlite
pub const SCORING_SYSTEM_DB_STRINGS: [&'static str; 2] = ["standard", "ex"];
#[inline]
pub fn to_index(self) -> usize {
self as usize
}
}
impl Default for ScoringSystem {
fn default() -> Self {
Self::Standard
@ -125,32 +140,39 @@ impl Score {
)
}
// }}}
// {{{ Scoring system conversion
/// Convert a standard score to any other scoring system. The output might be
/// nonsense if the given score is not using the standard system.
#[inline]
pub fn convert_to(self, scoring_system: ScoringSystem, chart: &Chart) -> Self {
match scoring_system {
ScoringSystem::Standard => self,
ScoringSystem::EX => self.to_zeta(chart.note_count),
}
}
// }}}
// {{{ Score => Play rating
#[inline]
pub fn play_rating(self, chart_constant: u32) -> i32 {
chart_constant as i32
pub fn play_rating(self, chart_constant: u32) -> Rating {
rating_from_fixed(chart_constant as i32)
+ if self.0 >= 10_000_000 {
200
Rational32::from_integer(2)
} else if self.0 >= 9_800_000 {
100 + (self.0 as i32 - 9_800_000) / 2_000
Rational32::from_integer(1)
+ Rational32::new(self.0 as i32 - 9_800_000, 200_000).reduced()
} else {
(self.0 as i32 - 9_500_000) / 3_000
Rational32::new(self.0 as i32 - 9_500_000, 300_000).reduced()
}
}
#[inline]
pub fn play_rating_f32(self, chart_constant: u32) -> f32 {
(self.play_rating(chart_constant)) as f32 / 100.0
}
pub fn display_play_rating(self, prev: Option<Self>, chart: &Chart) -> Result<String, Error> {
let mut buffer = String::with_capacity(14);
let play_rating = self.play_rating_f32(chart.chart_constant);
let play_rating = rating_as_float(self.play_rating(chart.chart_constant));
write!(buffer, "{:.2}", play_rating)?;
if let Some(prev) = prev {
let prev_play_rating = prev.play_rating_f32(chart.chart_constant);
let prev_play_rating = rating_as_float(prev.play_rating(chart.chart_constant));
if play_rating >= prev_play_rating {
write!(buffer, " (+{:.2})", play_rating - prev_play_rating)?;

View file

@ -4,7 +4,7 @@ use sqlx::query;
use crate::{
arcaea::chart::Side,
context::{Context, Error},
get_user,
get_user, play_from_db_record,
recognition::fuzzy_song_name::guess_song_and_chart,
};
use std::io::Cursor;
@ -20,13 +20,9 @@ use plotters::{
style::{IntoFont, TextStyle, BLUE, WHITE},
};
use poise::CreateReply;
use sqlx::query_as;
use crate::{
arcaea::{
play::DbPlay,
score::{Score, ScoringSystem},
},
arcaea::score::{Score, ScoringSystem},
user::discord_it_to_discord_user,
};
@ -121,13 +117,18 @@ async fn best(
let user = get_user!(&ctx);
let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?;
let play = query_as!(
DbPlay,
let play = query!(
"
SELECT * FROM plays
WHERE user_id=?
AND chart_id=?
ORDER BY score DESC
SELECT
p.id, p.chart_id, p.user_id, p.created_at,
p.max_recall, p.far_notes, s.score
FROM plays p
JOIN scores s ON s.play_id = p.id
WHERE s.scoring_system='standard'
AND p.user_id=?
AND p.chart_id=?
ORDER BY s.score DESC
LIMIT 1
",
user.id,
chart.id
@ -139,8 +140,8 @@ async fn best(
"Could not find any scores for {} [{:?}]",
song.title, chart.difficulty
)
})?
.into_play();
})?;
let play = play_from_db_record!(chart, play);
let (embed, attachment) = play
.to_embed(
@ -176,13 +177,17 @@ async fn plot(
let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?;
// SAFETY: we limit the amount of plotted plays to 1000.
let plays = query_as!(
DbPlay,
let plays = query!(
"
SELECT * FROM plays
WHERE user_id=?
AND chart_id=?
ORDER BY created_at ASC
SELECT
p.id, p.chart_id, p.user_id, p.created_at,
p.max_recall, p.far_notes, s.score
FROM plays p
JOIN scores s ON s.play_id = p.id
WHERE s.scoring_system='standard'
AND p.user_id=?
AND p.chart_id=?
ORDER BY s.score DESC
LIMIT 1000
",
user.id,
@ -204,7 +209,7 @@ async fn plot(
let max_time = plays.iter().map(|p| p.created_at).max().unwrap();
let mut min_score = plays
.iter()
.map(|p| p.clone().into_play().score(scoring_system))
.map(|p| play_from_db_record!(chart, p).score(scoring_system))
.min()
.unwrap()
.0 as i64;
@ -228,7 +233,7 @@ async fn plot(
{
let root = BitMapBackend::with_buffer(&mut buffer, (width, height)).into_drawing_area();
let mut chart = ChartBuilder::on(&root)
let mut chart_buider = ChartBuilder::on(&root)
.margin(25)
.caption(
format!("{} [{:?}]", song.title, chart.difficulty),
@ -241,7 +246,7 @@ async fn plot(
min_score..max_score,
)?;
chart
chart_buider
.configure_mesh()
.light_line_style(WHITE)
.y_label_formatter(&|s| format!("{}", Score(*s as u32)))
@ -261,7 +266,7 @@ async fn plot(
.map(|play| {
(
play.created_at.and_utc().timestamp_millis(),
play.into_play().score(scoring_system),
play_from_db_record!(chart, play).score(scoring_system),
)
})
.collect();
@ -269,12 +274,12 @@ async fn plot(
points.sort();
points.dedup();
chart.draw_series(LineSeries::new(
chart_buider.draw_series(LineSeries::new(
points.iter().map(|(t, s)| (*t, s.0 as i64)),
&BLUE,
))?;
chart.draw_series(points.iter().map(|(t, s)| {
chart_buider.draw_series(points.iter().map(|(t, s)| {
Circle::new((*t, s.0 as i64), 3, plotters::style::Color::filled(&BLUE))
}))?;
root.present()?;

View file

@ -1,11 +1,11 @@
use std::time::Instant;
use crate::arcaea::play::{CreatePlay, Play};
use crate::arcaea::play::CreatePlay;
use crate::arcaea::score::Score;
use crate::context::{Context, Error};
use crate::recognition::recognize::{ImageAnalyzer, ScoreKind};
use crate::user::{discord_it_to_discord_user, User};
use crate::{edit_reply, get_user, timed};
use crate::{edit_reply, get_user, play_from_db_record, timed};
use image::DynamicImage;
use poise::serenity_prelude::futures::future::join_all;
use poise::serenity_prelude::CreateMessage;
@ -117,11 +117,11 @@ pub async fn magic(
let maybe_fars =
Score::resolve_distibution_ambiguities(score, note_distribution, chart.note_count);
let play = CreatePlay::new(score, &chart)
let play = CreatePlay::new(score)
.with_attachment(file)
.with_fars(maybe_fars)
.with_max_recall(max_recall)
.save(&ctx.data(), &user)
.save(&ctx.data(), &user, &chart)
.await?;
// }}}
// }}}
@ -220,12 +220,16 @@ pub async fn show(
let res = query!(
"
SELECT
p.id,p.chart_id,p.user_id,p.score,p.zeta_score,
p.max_recall,p.created_at,p.far_notes,
p.id, p.chart_id, p.user_id, p.created_at,
p.max_recall, p.far_notes, s.score,
u.discord_id
FROM plays p
FROM plays p
JOIN scores s ON s.play_id = p.id
JOIN users u ON p.user_id = u.id
WHERE p.id=?
WHERE s.scoring_system='standard'
AND p.id=?
ORDER BY s.score DESC
LIMIT 1
",
id
)
@ -233,24 +237,12 @@ pub async fn show(
.await
.map_err(|_| format!("Could not find play with id {}", id))?;
let play = Play {
id: res.id as u32,
chart_id: res.chart_id as u32,
user_id: res.user_id as u32,
score: Score(res.score as u32),
zeta_score: Score(res.zeta_score as u32),
max_recall: res.max_recall.map(|r| r as u32),
far_notes: res.far_notes.map(|r| r as u32),
created_at: res.created_at,
discord_attachment_id: None,
creation_ptt: None,
creation_zeta_ptt: None,
};
let (song, chart) = ctx.data().song_cache.lookup_chart(res.chart_id as u32)?;
let play = play_from_db_record!(chart, res);
let author = discord_it_to_discord_user(&ctx, &res.discord_id).await?;
let user = User::by_id(&ctx.data().db, play.user_id).await?;
let (song, chart) = ctx.data().song_cache.lookup_chart(play.chart_id)?;
let (embed, attachment) = play
.to_embed(&ctx.data().db, &user, song, chart, i, Some(&author))
.await?;

View file

@ -13,6 +13,7 @@ use crate::{
chart::Level,
jacket::BITMAP_IMAGE_SIZE,
play::{compute_b30_ptt, get_best_plays},
rating::rating_as_float,
score::ScoringSystem,
},
assert_is_pookie,
@ -55,14 +56,15 @@ async fn best_plays(
get_best_plays(
&user_ctx.db,
&user_ctx.song_cache,
&user,
user.id,
scoring_system,
if require_full {
grid_size.0 * grid_size.1
} else {
grid_size.0 * (grid_size.1.max(1) - 1) + 1
} as usize,
(grid_size.0 * grid_size.1) as usize
(grid_size.0 * grid_size.1) as usize,
None
)
.await?
);
@ -287,7 +289,7 @@ async fn best_plays(
// }}}
// {{{ Display status text
with_font(&EXO_FONT, |faces| {
let status = play.short_status(chart).ok_or_else(|| {
let status = play.short_status(scoring_system, chart).ok_or_else(|| {
format!(
"Could not get status for score {}",
play.score(scoring_system)
@ -379,7 +381,7 @@ async fn best_plays(
style,
&format!(
"{:.2}",
play.play_rating(scoring_system, chart.chart_constant) as f32 / 100.0
play.play_rating_f32(scoring_system, chart.chart_constant)
),
)?;
@ -415,7 +417,7 @@ async fn best_plays(
.attachment(CreateAttachment::bytes(out_buffer, "b30.png"))
.content(format!(
"Your ptt is {:.2}",
compute_b30_ptt(scoring_system, &plays) as f32 / 100.0
rating_as_float(compute_b30_ptt(scoring_system, &plays))
));
ctx.send(reply).await?;

View file

@ -5,6 +5,7 @@
#![feature(async_closure)]
#![feature(try_blocks)]
#![feature(thread_local)]
#![feature(generic_arg_infer)]
mod arcaea;
mod assets;
@ -18,6 +19,7 @@ mod time;
mod transform;
mod user;
use arcaea::play::generate_missing_scores;
use assets::get_data_dir;
use context::{Error, UserContext};
use poise::serenity_prelude::{self as serenity};
@ -89,6 +91,12 @@ async fn main() {
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
let ctx = UserContext::new(pool).await?;
if var("SHIMMERING_REGEN_SCORES").unwrap_or_default() == "1" {
timed!("generate_missing_scores", {
generate_missing_scores(&ctx).await?;
});
}
Ok(ctx)
})
})