From ef1157bd53133683c4c83466a00a5fba49bd3b33 Mon Sep 17 00:00:00 2001 From: jasonyess Date: Sun, 23 Mar 2025 08:27:25 -0400 Subject: [PATCH 1/6] List players with 0 points in stats viewer --- .../20250323004031_list_scoreless_players.down.sql | 12 ++++++++++++ .../20250323004031_list_scoreless_players.up.sql | 14 ++++++++++++++ .../static/js/modules/statsviewer.js | 2 +- .../static/js/statsviewer/individual.js | 8 ++++++-- .../sql/paginate_player_ranking.sql | 3 ++- pointercrate-demonlist/src/player/paginate.rs | 6 +++++- 6 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 migrations/20250323004031_list_scoreless_players.down.sql create mode 100644 migrations/20250323004031_list_scoreless_players.up.sql diff --git a/migrations/20250323004031_list_scoreless_players.down.sql b/migrations/20250323004031_list_scoreless_players.down.sql new file mode 100644 index 000000000..e44b9d109 --- /dev/null +++ b/migrations/20250323004031_list_scoreless_players.down.sql @@ -0,0 +1,12 @@ +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; \ No newline at end of file diff --git a/migrations/20250323004031_list_scoreless_players.up.sql b/migrations/20250323004031_list_scoreless_players.up.sql new file mode 100644 index 000000000..d31883ff0 --- /dev/null +++ b/migrations/20250323004031_list_scoreless_players.up.sql @@ -0,0 +1,14 @@ +CREATE OR REPLACE VIEW ranked_players AS + SELECT + ROW_NUMBER() OVER(ORDER BY players.score DESC, id) AS index, + (CASE WHEN players.score = 0.0 THEN NULL + ELSE RANK() OVER(ORDER BY players.score DESC) + END) 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; \ No newline at end of file diff --git a/pointercrate-demonlist-pages/static/js/modules/statsviewer.js b/pointercrate-demonlist-pages/static/js/modules/statsviewer.js index 4041da583..b419061d9 100644 --- a/pointercrate-demonlist-pages/static/js/modules/statsviewer.js +++ b/pointercrate-demonlist-pages/static/js/modules/statsviewer.js @@ -153,7 +153,7 @@ export class StatsViewer extends FilteredPaginator { super.onReceive(response); // Using currentlySelected is O.K. here, as selection via clicking li-elements is the only possibility (well, not for the nation based one, but oh well)! - this._rank.innerText = this.currentlySelected.dataset.rank; + this._rank.innerText = this.currentlySelected.dataset.rank ?? "None"; this._score.innerHTML = this.currentlySelected.getElementsByTagName("i")[0].innerHTML; } diff --git a/pointercrate-demonlist-pages/static/js/statsviewer/individual.js b/pointercrate-demonlist-pages/static/js/statsviewer/individual.js index 8716c479c..f285bfd66 100644 --- a/pointercrate-demonlist-pages/static/js/statsviewer/individual.js +++ b/pointercrate-demonlist-pages/static/js/statsviewer/individual.js @@ -16,6 +16,7 @@ class IndividualStatsViewer extends StatsViewer { rankingEndpoint: "/api/v1/players/ranking/", entryGenerator: generateStatsViewerPlayer, }); + this.updateQueryData("scoreless", true); } onReceive(response) { @@ -198,9 +199,12 @@ function generateStatsViewerPlayer(player) { li.className = "white hover"; li.dataset.id = player.id; - li.dataset.rank = player.rank; + + if (player.rank) { + li.dataset.rank = player.rank; + b.appendChild(document.createTextNode("#" + player.rank + " ")); + } - b.appendChild(document.createTextNode("#" + player.rank + " ")); i.appendChild(document.createTextNode(player.score.toFixed(2))); if (player.nationality) { diff --git a/pointercrate-demonlist/sql/paginate_player_ranking.sql b/pointercrate-demonlist/sql/paginate_player_ranking.sql index 9fd2b94f9..60454c060 100644 --- a/pointercrate-demonlist/sql/paginate_player_ranking.sql +++ b/pointercrate-demonlist/sql/paginate_player_ranking.sql @@ -6,5 +6,6 @@ WHERE (index < $1 OR $1 IS NULL) AND (nation = $4 OR iso_country_code = $4 OR (nation IS NULL AND $5) OR ($4 IS NULL AND NOT $5)) AND (continent = CAST($6::TEXT AS continent) OR $6 IS NULL) AND (subdivision = $7 OR $7 IS NULL) + AND (score > 0.0 OR $8 = TRUE) ORDER BY rank {}, id -LIMIT $8 \ No newline at end of file +LIMIT $9 \ No newline at end of file diff --git a/pointercrate-demonlist/src/player/paginate.rs b/pointercrate-demonlist/src/player/paginate.rs index 3b3103796..1bb1cbf51 100644 --- a/pointercrate-demonlist/src/player/paginate.rs +++ b/pointercrate-demonlist/src/player/paginate.rs @@ -128,6 +128,9 @@ pub struct RankingPagination { #[serde(default, deserialize_with = "non_nullable")] name_contains: Option, + + #[serde(default, deserialize_with = "non_nullable")] + scoreless: Option, } impl PaginationQuery for RankingPagination { @@ -145,7 +148,7 @@ impl PaginationQuery for RankingPagination { #[derive(Debug, Serialize)] pub struct RankedPlayer { - rank: i64, + rank: Option, #[serde(skip)] index: i64, #[serde(flatten)] @@ -174,6 +177,7 @@ impl Paginatable for RankedPlayer { .bind(query.nation == Some(None)) .bind(query.continent.as_ref().map(|c| c.to_sql())) .bind(&query.subdivision) + .bind(query.scoreless) .bind(query.params.limit + 1) .fetch(connection); From b6de191c52565c15f30a19fc9f3879e56f5e649b Mon Sep 17 00:00:00 2001 From: jasonyess Date: Sun, 23 Mar 2025 10:33:39 -0400 Subject: [PATCH 2/6] only list players with at least one verification or approved record --- .../20250323004031_list_scoreless_players.up.sql | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/migrations/20250323004031_list_scoreless_players.up.sql b/migrations/20250323004031_list_scoreless_players.up.sql index d31883ff0..502bde5c3 100644 --- a/migrations/20250323004031_list_scoreless_players.up.sql +++ b/migrations/20250323004031_list_scoreless_players.up.sql @@ -11,4 +11,15 @@ CREATE OR REPLACE VIEW ranked_players AS FROM players LEFT OUTER JOIN nationalities ON players.nationality = nationalities.iso_country_code - WHERE NOT players.banned; \ No newline at end of file + WHERE NOT players.banned + AND ( + EXISTS ( -- check if player has at least one approved record + SELECT 1 FROM records + WHERE records.player = players.id + AND records.status_ = 'APPROVED' + ) + OR EXISTS ( -- check if player has verified at least one demon + SELECT 1 FROM demons + WHERE demons.verifier = players.id + ) + ); \ No newline at end of file From 6c3329f7adea67accf86015a5af050442120a2b2 Mon Sep 17 00:00:00 2001 From: jasonyess Date: Mon, 24 Mar 2025 09:30:01 -0400 Subject: [PATCH 3/6] revert EVERYTHING --- ...0323004031_list_scoreless_players.down.sql | 12 --------- ...250323004031_list_scoreless_players.up.sql | 25 ------------------- .../static/js/modules/statsviewer.js | 2 +- .../static/js/statsviewer/individual.js | 8 ++---- .../sql/paginate_player_ranking.sql | 3 +-- pointercrate-demonlist/src/player/paginate.rs | 6 +---- 6 files changed, 5 insertions(+), 51 deletions(-) delete mode 100644 migrations/20250323004031_list_scoreless_players.down.sql delete mode 100644 migrations/20250323004031_list_scoreless_players.up.sql diff --git a/migrations/20250323004031_list_scoreless_players.down.sql b/migrations/20250323004031_list_scoreless_players.down.sql deleted file mode 100644 index e44b9d109..000000000 --- a/migrations/20250323004031_list_scoreless_players.down.sql +++ /dev/null @@ -1,12 +0,0 @@ -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; \ No newline at end of file diff --git a/migrations/20250323004031_list_scoreless_players.up.sql b/migrations/20250323004031_list_scoreless_players.up.sql deleted file mode 100644 index 502bde5c3..000000000 --- a/migrations/20250323004031_list_scoreless_players.up.sql +++ /dev/null @@ -1,25 +0,0 @@ -CREATE OR REPLACE VIEW ranked_players AS - SELECT - ROW_NUMBER() OVER(ORDER BY players.score DESC, id) AS index, - (CASE WHEN players.score = 0.0 THEN NULL - ELSE RANK() OVER(ORDER BY players.score DESC) - END) 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 ( - EXISTS ( -- check if player has at least one approved record - SELECT 1 FROM records - WHERE records.player = players.id - AND records.status_ = 'APPROVED' - ) - OR EXISTS ( -- check if player has verified at least one demon - SELECT 1 FROM demons - WHERE demons.verifier = players.id - ) - ); \ No newline at end of file diff --git a/pointercrate-demonlist-pages/static/js/modules/statsviewer.js b/pointercrate-demonlist-pages/static/js/modules/statsviewer.js index b419061d9..4041da583 100644 --- a/pointercrate-demonlist-pages/static/js/modules/statsviewer.js +++ b/pointercrate-demonlist-pages/static/js/modules/statsviewer.js @@ -153,7 +153,7 @@ export class StatsViewer extends FilteredPaginator { super.onReceive(response); // Using currentlySelected is O.K. here, as selection via clicking li-elements is the only possibility (well, not for the nation based one, but oh well)! - this._rank.innerText = this.currentlySelected.dataset.rank ?? "None"; + this._rank.innerText = this.currentlySelected.dataset.rank; this._score.innerHTML = this.currentlySelected.getElementsByTagName("i")[0].innerHTML; } diff --git a/pointercrate-demonlist-pages/static/js/statsviewer/individual.js b/pointercrate-demonlist-pages/static/js/statsviewer/individual.js index f285bfd66..8716c479c 100644 --- a/pointercrate-demonlist-pages/static/js/statsviewer/individual.js +++ b/pointercrate-demonlist-pages/static/js/statsviewer/individual.js @@ -16,7 +16,6 @@ class IndividualStatsViewer extends StatsViewer { rankingEndpoint: "/api/v1/players/ranking/", entryGenerator: generateStatsViewerPlayer, }); - this.updateQueryData("scoreless", true); } onReceive(response) { @@ -199,12 +198,9 @@ function generateStatsViewerPlayer(player) { li.className = "white hover"; li.dataset.id = player.id; - - if (player.rank) { - li.dataset.rank = player.rank; - b.appendChild(document.createTextNode("#" + player.rank + " ")); - } + li.dataset.rank = player.rank; + b.appendChild(document.createTextNode("#" + player.rank + " ")); i.appendChild(document.createTextNode(player.score.toFixed(2))); if (player.nationality) { diff --git a/pointercrate-demonlist/sql/paginate_player_ranking.sql b/pointercrate-demonlist/sql/paginate_player_ranking.sql index 60454c060..9fd2b94f9 100644 --- a/pointercrate-demonlist/sql/paginate_player_ranking.sql +++ b/pointercrate-demonlist/sql/paginate_player_ranking.sql @@ -6,6 +6,5 @@ WHERE (index < $1 OR $1 IS NULL) AND (nation = $4 OR iso_country_code = $4 OR (nation IS NULL AND $5) OR ($4 IS NULL AND NOT $5)) AND (continent = CAST($6::TEXT AS continent) OR $6 IS NULL) AND (subdivision = $7 OR $7 IS NULL) - AND (score > 0.0 OR $8 = TRUE) ORDER BY rank {}, id -LIMIT $9 \ No newline at end of file +LIMIT $8 \ No newline at end of file diff --git a/pointercrate-demonlist/src/player/paginate.rs b/pointercrate-demonlist/src/player/paginate.rs index 1bb1cbf51..3b3103796 100644 --- a/pointercrate-demonlist/src/player/paginate.rs +++ b/pointercrate-demonlist/src/player/paginate.rs @@ -128,9 +128,6 @@ pub struct RankingPagination { #[serde(default, deserialize_with = "non_nullable")] name_contains: Option, - - #[serde(default, deserialize_with = "non_nullable")] - scoreless: Option, } impl PaginationQuery for RankingPagination { @@ -148,7 +145,7 @@ impl PaginationQuery for RankingPagination { #[derive(Debug, Serialize)] pub struct RankedPlayer { - rank: Option, + rank: i64, #[serde(skip)] index: i64, #[serde(flatten)] @@ -177,7 +174,6 @@ impl Paginatable for RankedPlayer { .bind(query.nation == Some(None)) .bind(query.continent.as_ref().map(|c| c.to_sql())) .bind(&query.subdivision) - .bind(query.scoreless) .bind(query.params.limit + 1) .fetch(connection); From 50128e96aed600c8954b3ac5eaa920109b343323 Mon Sep 17 00:00:00 2001 From: jasonyess Date: Thu, 27 Mar 2025 17:39:34 -0400 Subject: [PATCH 4/6] list players with 0 list points and accepted legacy records --- ...0324133910_list_scoreless_players.down.sql | 71 +++++++++++++++++++ ...250324133910_list_scoreless_players.up.sql | 69 ++++++++++++++++++ pointercrate-demonlist/src/player/mod.rs | 8 +-- 3 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 migrations/20250324133910_list_scoreless_players.down.sql create mode 100644 migrations/20250324133910_list_scoreless_players.up.sql diff --git a/migrations/20250324133910_list_scoreless_players.down.sql b/migrations/20250324133910_list_scoreless_players.down.sql new file mode 100644 index 000000000..d438b17d7 --- /dev/null +++ b/migrations/20250324133910_list_scoreless_players.down.sql @@ -0,0 +1,71 @@ +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; + +SELECT recompute_player_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 000000000..00f4eece2 --- /dev/null +++ b/migrations/20250324133910_list_scoreless_players.up.sql @@ -0,0 +1,69 @@ +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; + +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 35e8fc7fe..feb962235 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 nationality: Option, } @@ -67,7 +67,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); } } @@ -82,10 +82,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) From b1c343e2847b7e74e8ab570ef4fb703cdae2fa67 Mon Sep 17 00:00:00 2001 From: stadust <43299462+stadust@users.noreply.github.com> Date: Mon, 11 Aug 2025 23:21:25 +0100 Subject: [PATCH 5/6] Show players with approved legacy records/verifications on stats viewer Make "score" nullable, where score == 0 means "this player has approved records, verifications, but they do not give score anymore because they are legacy or progress records on extended demons", and score == NULL means "this player has no approved records or verifications whatsoever". Then have the stats viewer query all players that have non-NULL score, so that players with only legacy records (e.g. RioT) show up again. Signed-off-by: stadust <43299462+stadust@users.noreply.github.com> --- .../20250811220551_no_coalesce_score.down.sql | 68 ++++++++++++++++++ .../20250811220551_no_coalesce_score.up.sql | 69 +++++++++++++++++++ pointercrate-demonlist/src/player/mod.rs | 6 +- .../tests/demonlist/player/score.rs | 14 ++-- pointercrate-test/tests/demonlist/record.rs | 4 +- 5 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 migrations/20250811220551_no_coalesce_score.down.sql create mode 100644 migrations/20250811220551_no_coalesce_score.up.sql diff --git a/migrations/20250811220551_no_coalesce_score.down.sql b/migrations/20250811220551_no_coalesce_score.down.sql new file mode 100644 index 000000000..4e9d3a635 --- /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 000000000..454eabad1 --- /dev/null +++ b/migrations/20250811220551_no_coalesce_score.up.sql @@ -0,0 +1,69 @@ +-- 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 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 d21f7baad..6263b82b6 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,7 +83,7 @@ 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", diff --git a/pointercrate-test/tests/demonlist/player/score.rs b/pointercrate-test/tests/demonlist/player/score.rs index e8c9253b0..fb9a633a9 100644 --- a/pointercrate-test/tests/demonlist/player/score.rs +++ b/pointercrate-test/tests/demonlist/player/score.rs @@ -32,7 +32,7 @@ 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 +50,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 +66,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 +164,7 @@ 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 +207,7 @@ 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 +229,7 @@ pub async fn test_score_resets_if_last_record_removed(pool: Pool) { .await; assert_eq!( - player.player.score, 0.0f64, - "Removal of player's last record did not reset their score to 0" + player.player.score, None, + "Removal of player's last record did not reset their score to null" ); } diff --git a/pointercrate-test/tests/demonlist/record.rs b/pointercrate-test/tests/demonlist/record.rs index adb46d192..ccf31acfc 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"); } From 0832b303434dc02a84c87c70483953a871894a5a Mon Sep 17 00:00:00 2001 From: jasonyess Date: Sun, 9 Nov 2025 14:22:43 -0500 Subject: [PATCH 6/6] modify score_giving table & resolve test failures --- ...0324133910_list_scoreless_players.down.sql | 17 +++++++++++++++- ...250324133910_list_scoreless_players.up.sql | 17 +++++++++++++++- .../20250811220551_no_coalesce_score.up.sql | 1 + .../tests/demonlist/player/score.rs | 20 ++++++++++++++----- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/migrations/20250324133910_list_scoreless_players.down.sql b/migrations/20250324133910_list_scoreless_players.down.sql index d438b17d7..7dd0c5897 100644 --- a/migrations/20250324133910_list_scoreless_players.down.sql +++ b/migrations/20250324133910_list_scoreless_players.down.sql @@ -68,4 +68,19 @@ CREATE OR REPLACE VIEW ranked_players AS ON players.nationality = nationalities.iso_country_code WHERE NOT players.banned AND players.score > 0.0; -SELECT recompute_player_scores(); \ No newline at end of file +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 index 00f4eece2..a9eae2e9c 100644 --- a/migrations/20250324133910_list_scoreless_players.up.sql +++ b/migrations/20250324133910_list_scoreless_players.up.sql @@ -66,4 +66,19 @@ CREATE OR REPLACE VIEW ranked_players AS 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 +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.up.sql b/migrations/20250811220551_no_coalesce_score.up.sql index 454eabad1..f92d8dcfa 100644 --- a/migrations/20250811220551_no_coalesce_score.up.sql +++ b/migrations/20250811220551_no_coalesce_score.up.sql @@ -8,6 +8,7 @@ CREATE OR REPLACE FUNCTION record_score(progress FLOAT, demon FLOAT, list_size F $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 diff --git a/pointercrate-test/tests/demonlist/player/score.rs b/pointercrate-test/tests/demonlist/player/score.rs index fb9a633a9..3c34a5844 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.unwrap(), 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), @@ -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, Some(0.0f64), "Progress record on extended list demon is given score (or incorrectly setting score to null)"); + 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.unwrap(), 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, None, - "Removal of player's last record did not reset their score to null" + player.player.score, + Some(0.0f64), + "Removal of player's last record did not reset their score to 0" ); }