diff --git a/migrations/20250324133910_list_scoreless_players.down.sql b/migrations/20250324133910_list_scoreless_players.down.sql new file mode 100644 index 00000000..7dd0c589 --- /dev/null +++ b/migrations/20250324133910_list_scoreless_players.down.sql @@ -0,0 +1,86 @@ +UPDATE players + SET score = 0.0 + WHERE score IS NULL; + +ALTER TABLE players + ALTER COLUMN score + SET NOT NULL; + +CREATE OR REPLACE FUNCTION record_score(progress FLOAT, demon FLOAT, list_size FLOAT, requirement FLOAT) RETURNS FLOAT AS +$record_score$ +SELECT CASE + WHEN progress = 100 THEN + CASE + + WHEN 55 < demon AND demon <= 150 THEN + (56.191 * EXP(LN(2) * ((54.147 - (demon + 3.2)) * LN(50.0)) / 99.0)) + 6.273 + WHEN 35 < demon AND demon <= 55 THEN + 212.61 * (EXP(LN(1.036) * (1 - demon))) + 25.071 + WHEN 20 < demon AND demon <= 35 THEN + (250 - 83.389) * (EXP(LN(1.0099685) * (2 - demon))) - 31.152 + WHEN demon <= 20 THEN + (250 - 100.39) * (EXP(LN(1.168) * (1 - demon))) + 100.39 + + END + + WHEN progress < requirement THEN + 0.0 + ELSE + CASE + + WHEN 55 < demon AND demon <= 150 THEN + ((56.191 * EXP(LN(2) * ((54.147 - (demon + 3.2)) * LN(50.0)) / 99.0)) + 6.273) * (EXP(LN(5) * (progress - requirement) / (100 - requirement))) / 10 + WHEN 35 < demon AND demon <= 55 THEN + (212.61 * (EXP(LN(1.036) * (1 - demon))) + 25.071) * (EXP(LN(5) * (progress - requirement) / (100 - requirement))) / 10 + WHEN 20 < demon AND demon <= 35 THEN + ((250 - 83.389) * (EXP(LN(1.0099685) * (2 - demon))) - 31.152) * (EXP(LN(5) * (progress - requirement) / (100 - requirement))) / 10 + WHEN demon <= 20 THEN + ((250 - 100.39) * (EXP(LN(1.168) * (1 - demon))) + 100.39) * (EXP(LN(5) * (progress - requirement) / (100 - requirement))) / 10 + + END + END; +$record_score$ + LANGUAGE SQL IMMUTABLE; + +CREATE OR REPLACE FUNCTION recompute_player_scores() RETURNS void AS $$ + UPDATE players + SET score = coalesce(q.score, 0) + FROM players p + LEFT OUTER JOIN ( + SELECT player, SUM(record_score(progress, position, 150, requirement)) as score + FROM score_giving + GROUP BY player + ) q + ON q.player = p.id + WHERE players.id = p.id; +$$ LANGUAGE SQL; + +CREATE OR REPLACE VIEW ranked_players AS + SELECT + ROW_NUMBER() OVER(ORDER BY players.score DESC, id) AS index, + RANK() OVER(ORDER BY players.score DESC) AS rank, + id, name, players.score, subdivision, + nationalities.iso_country_code, + nationalities.nation, + nationalities.continent + FROM players + LEFT OUTER JOIN nationalities + ON players.nationality = nationalities.iso_country_code + WHERE NOT players.banned AND players.score > 0.0; + +CREATE OR REPLACE VIEW score_giving AS + SELECT records.progress, demons.position, demons.requirement, records.player + FROM records + INNER JOIN demons + ON demons.id = records.demon + WHERE records.status_ = 'APPROVED' AND (demons.position <= 75 OR records.progress = 100) + + UNION + + SELECT 100, demons.position, demons.requirement, demons.verifier + FROM demons; + + +SELECT recompute_player_scores(); +SELECT recompute_nation_scores(); +SELECT recompute_subdivision_scores(); \ No newline at end of file diff --git a/migrations/20250324133910_list_scoreless_players.up.sql b/migrations/20250324133910_list_scoreless_players.up.sql new file mode 100644 index 00000000..a9eae2e9 --- /dev/null +++ b/migrations/20250324133910_list_scoreless_players.up.sql @@ -0,0 +1,84 @@ +ALTER TABLE players + ALTER COLUMN score + DROP NOT NULL; + +CREATE OR REPLACE FUNCTION record_score(progress FLOAT, demon FLOAT, list_size FLOAT, requirement FLOAT) RETURNS FLOAT AS +$record_score$ +SELECT CASE + WHEN demon > 150 THEN 0.0 + + WHEN progress = 100 THEN + CASE + + WHEN 55 < demon AND demon <= 150 THEN + (56.191 * EXP(LN(2) * ((54.147 - (demon + 3.2)) * LN(50.0)) / 99.0)) + 6.273 + WHEN 35 < demon AND demon <= 55 THEN + 212.61 * (EXP(LN(1.036) * (1 - demon))) + 25.071 + WHEN 20 < demon AND demon <= 35 THEN + (250 - 83.389) * (EXP(LN(1.0099685) * (2 - demon))) - 31.152 + WHEN demon <= 20 THEN + (250 - 100.39) * (EXP(LN(1.168) * (1 - demon))) + 100.39 + + END + + WHEN progress < requirement THEN + 0.0 + ELSE + CASE + + WHEN 55 < demon AND demon <= 150 THEN + ((56.191 * EXP(LN(2) * ((54.147 - (demon + 3.2)) * LN(50.0)) / 99.0)) + 6.273) * (EXP(LN(5) * (progress - requirement) / (100 - requirement))) / 10 + WHEN 35 < demon AND demon <= 55 THEN + (212.61 * (EXP(LN(1.036) * (1 - demon))) + 25.071) * (EXP(LN(5) * (progress - requirement) / (100 - requirement))) / 10 + WHEN 20 < demon AND demon <= 35 THEN + ((250 - 83.389) * (EXP(LN(1.0099685) * (2 - demon))) - 31.152) * (EXP(LN(5) * (progress - requirement) / (100 - requirement))) / 10 + WHEN demon <= 20 THEN + ((250 - 100.39) * (EXP(LN(1.168) * (1 - demon))) + 100.39) * (EXP(LN(5) * (progress - requirement) / (100 - requirement))) / 10 + + END + END; +$record_score$ +LANGUAGE SQL IMMUTABLE; + +CREATE OR REPLACE FUNCTION recompute_player_scores() RETURNS void AS $$ + UPDATE players + SET score = q.score + FROM players p + LEFT OUTER JOIN ( + SELECT player, SUM(record_score(progress, position, 150, requirement)) as score + FROM score_giving + GROUP BY player + ) q + ON q.player = p.id + WHERE players.id = p.id; +$$ LANGUAGE SQL; + +CREATE OR REPLACE VIEW ranked_players AS + SELECT + ROW_NUMBER() OVER(ORDER BY players.score DESC, id) AS index, + RANK() OVER(ORDER BY players.score DESC) AS rank, + id, name, players.score, subdivision, + nationalities.iso_country_code, + nationalities.nation, + nationalities.continent + FROM players + LEFT OUTER JOIN nationalities + ON players.nationality = nationalities.iso_country_code + WHERE NOT players.banned AND players.score IS NOT NULL; + +CREATE OR REPLACE VIEW score_giving AS + SELECT records.progress, demons.position, demons.requirement, records.player + FROM records + INNER JOIN demons + ON demons.id = records.demon + WHERE records.status_ = 'APPROVED' + + UNION + + SELECT 100, demons.position, demons.requirement, demons.verifier + FROM demons; + + +SELECT recompute_player_scores(); +SELECT recompute_nation_scores(); +SELECT recompute_subdivision_scores(); \ No newline at end of file diff --git a/migrations/20250811220551_no_coalesce_score.down.sql b/migrations/20250811220551_no_coalesce_score.down.sql new file mode 100644 index 00000000..4e9d3a63 --- /dev/null +++ b/migrations/20250811220551_no_coalesce_score.down.sql @@ -0,0 +1,68 @@ +-- Add down migration script here +CREATE OR REPLACE FUNCTION record_score(progress FLOAT, demon FLOAT, list_size FLOAT, requirement FLOAT) RETURNS FLOAT AS +$record_score$ +SELECT CASE + WHEN progress = 100 THEN + CASE + + WHEN 55 < demon AND demon <= 150 THEN + (56.191 * EXP(LN(2) * ((54.147 - (demon + 3.2)) * LN(50.0)) / 99.0)) + 6.273 + WHEN 35 < demon AND demon <= 55 THEN + 212.61 * (EXP(LN(1.036) * (1 - demon))) + 25.071 + WHEN 20 < demon AND demon <= 35 THEN + (250 - 83.389) * (EXP(LN(1.0099685) * (2 - demon))) - 31.152 + WHEN demon <= 20 THEN + (250 - 100.39) * (EXP(LN(1.168) * (1 - demon))) + 100.39 + + END + + WHEN progress < requirement THEN + 0.0 + ELSE + CASE + + WHEN 55 < demon AND demon <= 150 THEN + ((56.191 * EXP(LN(2) * ((54.147 - (demon + 3.2)) * LN(50.0)) / 99.0)) + 6.273) * (EXP(LN(5) * (progress - requirement) / (100 - requirement))) / 10 + WHEN 35 < demon AND demon <= 55 THEN + (212.61 * (EXP(LN(1.036) * (1 - demon))) + 25.071) * (EXP(LN(5) * (progress - requirement) / (100 - requirement))) / 10 + WHEN 20 < demon AND demon <= 35 THEN + ((250 - 83.389) * (EXP(LN(1.0099685) * (2 - demon))) - 31.152) * (EXP(LN(5) * (progress - requirement) / (100 - requirement))) / 10 + WHEN demon <= 20 THEN + ((250 - 100.39) * (EXP(LN(1.168) * (1 - demon))) + 100.39) * (EXP(LN(5) * (progress - requirement) / (100 - requirement))) / 10 + + END + END; +$record_score$ +LANGUAGE SQL IMMUTABLE; + +CREATE OR REPLACE FUNCTION recompute_player_scores() RETURNS void AS $$ +UPDATE players +SET score = coalesce(q.score, 0) + FROM players p + LEFT OUTER JOIN ( + SELECT player, SUM(record_score(progress, position, 150, requirement)) as score + FROM score_giving + GROUP BY player + ) q +ON q.player = p.id +WHERE players.id = p.id; +$$ LANGUAGE SQL; + +CREATE OR REPLACE VIEW ranked_players AS +SELECT + ROW_NUMBER() OVER(ORDER BY players.score DESC, id) AS index, + RANK() OVER(ORDER BY players.score DESC) AS rank, + id, name, players.score, subdivision, + nationalities.iso_country_code, + nationalities.nation, + nationalities.continent +FROM players + LEFT OUTER JOIN nationalities + ON players.nationality = nationalities.iso_country_code +WHERE NOT players.banned AND players.score > 0.0; + +SELECT recompute_player_scores(); + +ALTER TABLE players + ALTER COLUMN score + SET NOT NULL; \ No newline at end of file diff --git a/migrations/20250811220551_no_coalesce_score.up.sql b/migrations/20250811220551_no_coalesce_score.up.sql new file mode 100644 index 00000000..f92d8dcf --- /dev/null +++ b/migrations/20250811220551_no_coalesce_score.up.sql @@ -0,0 +1,70 @@ +-- Add up migration script here + +ALTER TABLE players + ALTER COLUMN score + DROP NOT NULL; + +CREATE OR REPLACE FUNCTION record_score(progress FLOAT, demon FLOAT, list_size FLOAT, requirement FLOAT) RETURNS FLOAT AS +$record_score$ +SELECT CASE + WHEN demon > 150 THEN 0.0 + WHEN demon > 75 AND progress < 100 THEN 0.0 + WHEN progress < requirement THEN 0.0 + WHEN progress = 100 THEN + CASE + WHEN 55 < demon AND demon <= 150 THEN + (56.191 * EXP(LN(2) * ((54.147 - (demon + 3.2)) * LN(50.0)) / 99.0)) + 6.273 + WHEN 35 < demon AND demon <= 55 THEN + 212.61 * (EXP(LN(1.036) * (1 - demon))) + 25.071 + WHEN 20 < demon AND demon <= 35 THEN + (250 - 83.389) * (EXP(LN(1.0099685) * (2 - demon))) - 31.152 + WHEN demon <= 20 THEN + (250 - 100.39) * (EXP(LN(1.168) * (1 - demon))) + 100.39 + ELSE + 0.0 + END + ELSE + CASE + + WHEN 55 < demon AND demon <= 150 THEN + ((56.191 * EXP(LN(2) * ((54.147 - (demon + 3.2)) * LN(50.0)) / 99.0)) + 6.273) * (EXP(LN(5) * (progress - requirement) / (100 - requirement))) / 10 + WHEN 35 < demon AND demon <= 55 THEN + (212.61 * (EXP(LN(1.036) * (1 - demon))) + 25.071) * (EXP(LN(5) * (progress - requirement) / (100 - requirement))) / 10 + WHEN 20 < demon AND demon <= 35 THEN + ((250 - 83.389) * (EXP(LN(1.0099685) * (2 - demon))) - 31.152) * (EXP(LN(5) * (progress - requirement) / (100 - requirement))) / 10 + WHEN demon <= 20 THEN + ((250 - 100.39) * (EXP(LN(1.168) * (1 - demon))) + 100.39) * (EXP(LN(5) * (progress - requirement) / (100 - requirement))) / 10 + ELSE + 0.0 + END +END; +$record_score$ +LANGUAGE SQL IMMUTABLE; + +CREATE OR REPLACE FUNCTION recompute_player_scores() RETURNS void AS $$ +UPDATE players +SET score = q.score + FROM players p + LEFT OUTER JOIN ( + SELECT player, SUM(record_score(progress, position, 150, requirement)) as score + FROM score_giving + GROUP BY player + ) q +ON q.player = p.id +WHERE players.id = p.id; +$$ LANGUAGE SQL; + +CREATE OR REPLACE VIEW ranked_players AS +SELECT + ROW_NUMBER() OVER(ORDER BY players.score DESC, id) AS index, + RANK() OVER(ORDER BY players.score DESC) AS rank, + id, name, players.score, subdivision, + nationalities.iso_country_code, + nationalities.nation, + nationalities.continent +FROM players + LEFT OUTER JOIN nationalities + ON players.nationality = nationalities.iso_country_code +WHERE NOT players.banned AND players.score IS NOT NULL; + +SELECT recompute_player_scores(); \ No newline at end of file diff --git a/pointercrate-demonlist/src/player/mod.rs b/pointercrate-demonlist/src/player/mod.rs index d21f7baa..49b5513d 100644 --- a/pointercrate-demonlist/src/player/mod.rs +++ b/pointercrate-demonlist/src/player/mod.rs @@ -58,7 +58,7 @@ pub struct Player { /// - Player updates /// * Player banned /// * Player objects merged - pub score: f64, + pub score: Option, pub rank: Option, pub nationality: Option, } @@ -68,7 +68,7 @@ pub struct Player { impl Hash for Player { fn hash(&self, state: &mut H) { self.base.hash(state); - ((self.score * 100f64) as u64).hash(state); + ((self.score.unwrap_or(0.0) * 100f64) as u64).hash(state); self.nationality.hash(state); } } @@ -83,10 +83,10 @@ impl Taggable for FullPlayer { impl DatabasePlayer { /// Recomputes this player's score and updates it in the database. - pub async fn update_score(&self, connection: &mut PgConnection) -> Result { + pub async fn update_score(&self, connection: &mut PgConnection) -> Result, CoreError> { // No need to specially handle banned players - they have no approved records, so `score_of_player` will return 0 let new_score = sqlx::query!( - "UPDATE players SET score = coalesce(score_of_player($1), 0) WHERE id = $1 RETURNING score", + "UPDATE players SET score = score_of_player($1) WHERE id = $1 RETURNING score", self.id ) .fetch_one(&mut *connection) diff --git a/pointercrate-test/tests/demonlist/player/score.rs b/pointercrate-test/tests/demonlist/player/score.rs index e8c9253b..3c34a584 100644 --- a/pointercrate-test/tests/demonlist/player/score.rs +++ b/pointercrate-test/tests/demonlist/player/score.rs @@ -32,7 +32,11 @@ pub async fn test_score_update_on_record_update(pool: Pool) { .get_success_result() .await; - assert_ne!(player.player.score, 0.0f64, "Adding approved record failed to give player score"); + assert_ne!( + player.player.score.unwrap(), + 0.0f64, + "Adding approved record failed to give player score" + ); clnt.patch( format!("/api/v1/records/{}/", record.id), @@ -50,7 +54,7 @@ pub async fn test_score_update_on_record_update(pool: Pool) { .get_success_result() .await; - assert_eq!(player.player.score, 0.0f64, "Rejecting record failed to remove player score"); + assert_eq!(player.player.score, None, "Rejecting record failed to remove player score"); } #[sqlx::test(migrations = "../migrations")] @@ -66,7 +70,7 @@ pub async fn test_verifications_give_score(pool: Pool) { .get_success_result() .await; - assert_ne!(player.player.score, 0.0f64); + assert_ne!(player.player.score.unwrap(), 0.0f64); } async fn nationality_score(iso_country_code: &str, connection: &mut PgConnection) -> f64 { @@ -164,7 +168,11 @@ pub async fn test_extended_progress_records_give_no_score(pool: Pool) .get_success_result() .await; - assert_eq!(player.player.score, 0.0f64, "Progress record on extended list demon is given score"); + assert_eq!( + player.player.score, + Some(0.0f64), + "Progress record on extended list demon is given score (or incorrectly setting score to null)" + ); } #[sqlx::test(migrations = "../migrations")] @@ -207,7 +215,8 @@ pub async fn test_score_resets_if_last_record_removed(pool: Pool) { .await; assert_ne!( - player.player.score, 0.0f64, + player.player.score.unwrap(), + 0.0f64, "Progress record on final main list demon not giving score" ); @@ -229,7 +238,8 @@ pub async fn test_score_resets_if_last_record_removed(pool: Pool) { .await; assert_eq!( - player.player.score, 0.0f64, + player.player.score, + Some(0.0f64), "Removal of player's last record did not reset their score to 0" ); } diff --git a/pointercrate-test/tests/demonlist/record.rs b/pointercrate-test/tests/demonlist/record.rs index adb46d19..ccf31acf 100644 --- a/pointercrate-test/tests/demonlist/record.rs +++ b/pointercrate-test/tests/demonlist/record.rs @@ -252,7 +252,7 @@ async fn test_record_deletion_updates_player_score(pool: Pool) { .get_success_result() .await; - assert_ne!(player.player.score, 0.0f64, "Adding approved record failed to give player score"); + assert_ne!(player.player.score.unwrap(), 0.0f64, "Adding approved record failed to give player score"); clnt.delete(format!("/api/v1/records/{}/", record.id)) .authorize_as(&helper) @@ -267,5 +267,5 @@ async fn test_record_deletion_updates_player_score(pool: Pool) { .get_success_result() .await; - assert_eq!(player.player.score, 0.0f64, "Deleting approved record failed to lower player score"); + assert_eq!(player.player.score, None, "Deleting approved record failed to lower player score"); }