Skip to content

Commit

Permalink
Add win probability to H2H page and add colored links
Browse files Browse the repository at this point in the history
  • Loading branch information
aviguptatx committed May 12, 2024
1 parent 7f1ff73 commit 6831066
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 5 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ chrono = "0.4.33"
plotly = "0.8.4"
reqwest = "0.11.24"
thiserror = "1.0.60"
skillratings = "0.26.0"

[profile.release]
lto = "fat"
Expand Down
36 changes: 35 additions & 1 deletion src/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,38 @@ pub async fn fetch_leaderboard_from_db(
Ok(leaderboard_data)
}

/// Fetches the trueskill mu and sigma for a given user from the database.
///
/// # Arguments
///
/// * `db_name` - A string representing the name of the database table to query.
/// * `username` - A reference to the username as a string.
/// * `client` - A reference to the Postgrest client.
///
/// # Returns
///
/// A `Result` containing a `LeaderboardEntry` struct, or an error if the database query fails.
pub async fn fetch_user_trueskill_from_db(
username: &str,
client: &Postgrest,
) -> Result<(f64, f64), Box<dyn Error>> {
let body = client
.from("all_rust")
.select("*")
.eq("username", username)
.execute()
.await?
.text()
.await?;

let user_data: LeaderboardEntry = serde_json::from_str::<Vec<LeaderboardEntry>>(&body)?
.first()
.cloned()
.ok_or("Couldn't find user in database")?;

Ok((user_data.mu, user_data.sigma))
}

/// Fetches the head-to-head data for two users from the database.
///
/// # Arguments
Expand Down Expand Up @@ -228,11 +260,13 @@ pub async fn fetch_h2h_data(
};

let time_diff_description = format!(
"On average, {} is {:.1} seconds {} than {}.",
"<a class=\"user1\" href=\"/user/{}\">{}</a> is {:.1} seconds {} than <a class=\"user2\" href=\"/user/{}\">{}</a> on average.",
user1,
user1,
h2h_data.avg_time_difference.abs(),
speed_verb,
user2,
user2,
);

Ok(HeadToHeadData {
Expand Down
13 changes: 13 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use askama::Template;
use chrono::Duration;
use database::fetch_user_trueskill_from_db;
use postgrest::Postgrest;
use util::compute_win_probability;
use worker::{event, Context, Env, Request, Response, Result, RouteContext, Router};

mod database;
Expand Down Expand Up @@ -185,6 +187,16 @@ async fn handle_h2h<T>(ctx: &RouteContext<T>, client: &Postgrest) -> Result<Resp
String::from("Need more times before we can generate scatter plot!")
});

let (user1_mu, user1_sigma) = fetch_user_trueskill_from_db(&user1, client)
.await
.map_err(|e| format!("Couldn't fetch user1 trueskill from database: {e}"))?;

let (user2_mu, user2_sigma) = fetch_user_trueskill_from_db(&user2, client)
.await
.map_err(|e| format!("Couldn't fetch user2 trueskill from database: {e}"))?;

let win_probability = compute_win_probability((user1_mu, user1_sigma), (user2_mu, user2_sigma));

let data = fetch_h2h_data(user1, user2, client).await.ok();

Response::from_html(
Expand All @@ -193,6 +205,7 @@ async fn handle_h2h<T>(ctx: &RouteContext<T>, client: &Postgrest) -> Result<Resp
data,
box_plot_html,
scatter_plot_html,
win_probability,
}
.render()
.unwrap(),
Expand Down
4 changes: 3 additions & 1 deletion src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ pub struct ResultEntry {
pub rank: i32,
}

#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Clone)]
pub struct LeaderboardEntry {
pub username: String,
pub mu: f64,
pub sigma: f64,
pub average_time: f64,
pub num_wins: i32,
pub num_played: i32,
Expand Down
11 changes: 11 additions & 0 deletions src/templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ mod filters {
Ok(f.round() as i32)
}

pub fn convert_decimal_to_percentage(decimal: &f64) -> ::askama::Result<String> {
Ok(format!("{:.2}%", decimal * 100.0))
}

pub fn unpack_time(score: &Option<crate::models::NytScore>) -> ::askama::Result<String> {
score.as_ref().map_or_else(
|| Ok(String::from("--")),
Expand Down Expand Up @@ -68,6 +72,7 @@ pub struct HeadToHeadTemplate {
pub data: Option<HeadToHeadData>,
pub box_plot_html: String,
pub scatter_plot_html: String,
pub win_probability: f64,
}

pub const CSS_STYLES: &str = "
Expand Down Expand Up @@ -130,4 +135,10 @@ pub const CSS_STYLES: &str = "
.podium-item.bronze {
background-color: #CD7F32;
}
a.user1 {
color: #1f77b4;
}
a.user2 {
color: #ff7f0e;
}
";
20 changes: 20 additions & 0 deletions src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use plotly::color::Rgb;
use plotly::common::{Line, Marker, Mode, Title};
use plotly::layout::{Axis, RangeSelector, RangeSlider, SelectorButton, SelectorStep, StepMode};
use plotly::{BoxPlot, Layout, Plot, Scatter};
use skillratings::trueskill::{expected_score, TrueSkillConfig, TrueSkillRating};
use std::cmp::{max, min};
use std::error::Error;

Expand Down Expand Up @@ -231,6 +232,25 @@ pub fn generate_box_plot_html(
Ok(plot.to_inline_html(Some("box-plot")))
}

pub fn compute_win_probability(user1: (f64, f64), user2: (f64, f64)) -> f64 {
expected_score(
&TrueSkillRating {
rating: user1.0,
uncertainty: user1.1,
},
&TrueSkillRating {
rating: user2.0,
uncertainty: user2.1,
},
&TrueSkillConfig {
draw_probability: 0.05,
beta: 4.167,
default_dynamics: 0.0833,
},
)
.0
}

/// Fetches the live leaderboard data from the New York Times API.
///
/// # Arguments
Expand Down
7 changes: 4 additions & 3 deletions templates/h2h.html
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,10 @@ <h2 class="text-center">Head to Head Stats</h2>
{% if data.user1 == data.user2 %}
<p>Try selecting different players, silly.</p>
{% else %}
<p><span id="totalMatches"><strong>{{ data.user1 }}</strong> and <strong>{{ data.user2 }}</strong> have played <strong>{{ data.total_matches }}</strong> crossword(s) together.</span></p>
<p><span id="wins"></span> <strong>{{ data.user1 }}</strong> has <strong>{{ data.wins_user1 }}</strong> win(s) while <strong>{{ data.user2 }}</strong> has <strong>{{ data.wins_user2 }}</strong> win(s). They have <strong>{{ data.ties }}</strong> tie(s).</p>
<p><span id="avgTimeDiff"><strong>{{ data.time_diff_description }}</strong></span></p>
<p><span id="totalMatches"><a class="user1" href="/user/{{data.user1}}">{{data.user1}}</a> and <a class="user2" href="/user/{{data.user2}}">{{data.user2}}</a> have played {{ data.total_matches }} crossword(s) together.</span></p>
<p><span id="wins"></span> <a class="user1" href="/user/{{data.user1}}">{{data.user1}}</a> has {{ data.wins_user1 }} win(s) while <a class="user2" href="/user/{{data.user2}}">{{data.user2}}</a> has {{ data.wins_user2 }} win(s). They have {{ data.ties }} tie(s).</p>
<p><span id="avgTimeDiff">{{ data.time_diff_description|safe }}</span></p>
<p><span id="winProbability"><a class="user1" href="/user/{{data.user1}}">{{data.user1}}</a> has a {{win_probability|convert_decimal_to_percentage}} chance of beating <a class="user2" href="/user/{{data.user2}}">{{data.user2}}</a>, according to ELO.</span></p>
{% endif %}
</div>
</div>
Expand Down

0 comments on commit 6831066

Please sign in to comment.