atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
624fe9c09c8eefdb9679b36259b76f6439318edc
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git

Blame: src/http/leaderboard.rs

8b8e83329a1datagen2026-10-12 12:01 UTC1
//! The public listings, paged fifty at a time with RFC 8288 `next` and `prev` links.
8b8e83329a1datagen2026-10-12 12:01 UTC2
8b8e83329a1datagen2026-10-12 12:01 UTC3
use axum::{
8b8e83329a1datagen2026-10-12 12:01 UTC4
    Json,
8b8e83329a1datagen2026-10-12 12:01 UTC5
    http::{StatusCode, header},
8b8e83329a1datagen2026-10-12 12:01 UTC6
    response::{IntoResponse, Response},
8b8e83329a1datagen2026-10-12 12:01 UTC7
};
8b8e83329a1datagen2026-10-12 12:01 UTC8
use serde_json::{Value, json};
8b8e83329a1datagen2026-10-12 12:01 UTC9
4f54028cba69atagen2026-10-14 11:45 UTC10
use super::{
4f54028cba69atagen2026-10-14 11:45 UTC11
    Shared,
4f54028cba69atagen2026-10-14 11:45 UTC12
    ctx::Ctx,
4f54028cba69atagen2026-10-14 11:45 UTC13
    meta::{GRAVEYARD_PATH, LEADERBOARD_PATH},
4f54028cba69atagen2026-10-14 11:45 UTC14
    run::records_unavailable,
4f54028cba69atagen2026-10-14 11:45 UTC15
};
8b8e83329a1datagen2026-10-12 12:01 UTC16
use crate::{
8b8e83329a1datagen2026-10-12 12:01 UTC17
    game::{balance::MM_PER_INCH, volume},
4f54028cba69atagen2026-10-14 11:45 UTC18
    link::Link,
8b8e83329a1datagen2026-10-12 12:01 UTC19
    problem::{PROBLEM_BASE, Problem},
8b8e83329a1datagen2026-10-12 12:01 UTC20
    records::{Ranking, Record},
8b8e83329a1datagen2026-10-12 12:01 UTC21
};
8b8e83329a1datagen2026-10-12 12:01 UTC22
8b8e83329a1datagen2026-10-12 12:01 UTC23
const PAGE: usize = 50;
8b8e83329a1datagen2026-10-12 12:01 UTC24
8b8e83329a1datagen2026-10-12 12:01 UTC25
fn page(ctx: &Ctx) -> Result<usize, Problem> {
8b8e83329a1datagen2026-10-12 12:01 UTC26
    match ctx.query("page").map(str::parse::<usize>) {
8b8e83329a1datagen2026-10-12 12:01 UTC27
        None => Ok(1),
8b8e83329a1datagen2026-10-12 12:01 UTC28
        Some(Ok(page)) if page >= 1 => Ok(page),
8b8e83329a1datagen2026-10-12 12:01 UTC29
        Some(_) => Err(Problem::new(
8b8e83329a1datagen2026-10-12 12:01 UTC30
            StatusCode::BAD_REQUEST,
8b8e83329a1datagen2026-10-12 12:01 UTC31
            "bad-page",
8b8e83329a1datagen2026-10-12 12:01 UTC32
            "Pages are counted from 1.",
8b8e83329a1datagen2026-10-12 12:01 UTC33
        )),
8b8e83329a1datagen2026-10-12 12:01 UTC34
    }
8b8e83329a1datagen2026-10-12 12:01 UTC35
}
8b8e83329a1datagen2026-10-12 12:01 UTC36
8b8e83329a1datagen2026-10-12 12:01 UTC37
/// `query` is everything but the page, already encoded, so the links keep the caller's sort.
8b8e83329a1datagen2026-10-12 12:01 UTC38
fn paged(body: Value, path: &str, query: &str, page: usize, full: bool) -> Response {
8b8e83329a1datagen2026-10-12 12:01 UTC39
    let link = |page: usize, rel: &str| {
8b8e83329a1datagen2026-10-12 12:01 UTC40
        let separator = if query.is_empty() { "" } else { "&" };
4f54028cba69atagen2026-10-14 11:45 UTC41
        Link::registered(&format!("{path}?{query}{separator}page={page}"), rel).to_string()
8b8e83329a1datagen2026-10-12 12:01 UTC42
    };
8b8e83329a1datagen2026-10-12 12:01 UTC43
    let mut links = Vec::new();
8b8e83329a1datagen2026-10-12 12:01 UTC44
    if page > 1 {
8b8e83329a1datagen2026-10-12 12:01 UTC45
        links.push(link(page - 1, "prev"));
8b8e83329a1datagen2026-10-12 12:01 UTC46
    }
8b8e83329a1datagen2026-10-12 12:01 UTC47
    if full {
8b8e83329a1datagen2026-10-12 12:01 UTC48
        links.push(link(page + 1, "next"));
8b8e83329a1datagen2026-10-12 12:01 UTC49
    }
8b8e83329a1datagen2026-10-12 12:01 UTC50
    let mut response = Json(body).into_response();
8b8e83329a1datagen2026-10-12 12:01 UTC51
    if links.is_empty() {
8b8e83329a1datagen2026-10-12 12:01 UTC52
        return response;
8b8e83329a1datagen2026-10-12 12:01 UTC53
    }
8b8e83329a1datagen2026-10-12 12:01 UTC54
    if let Ok(value) = links.join(", ").parse() {
8b8e83329a1datagen2026-10-12 12:01 UTC55
        response.headers_mut().insert(header::LINK, value);
8b8e83329a1datagen2026-10-12 12:01 UTC56
    }
8b8e83329a1datagen2026-10-12 12:01 UTC57
    response
8b8e83329a1datagen2026-10-12 12:01 UTC58
}
8b8e83329a1datagen2026-10-12 12:01 UTC59
8b8e83329a1datagen2026-10-12 12:01 UTC60
pub fn graveyard(state: &mut Shared, ctx: &Ctx) -> Response {
8b8e83329a1datagen2026-10-12 12:01 UTC61
    let page = match page(ctx) {
8b8e83329a1datagen2026-10-12 12:01 UTC62
        Ok(page) => page,
8b8e83329a1datagen2026-10-12 12:01 UTC63
        Err(problem) => return problem.into_response(),
8b8e83329a1datagen2026-10-12 12:01 UTC64
    };
8b8e83329a1datagen2026-10-12 12:01 UTC65
    let dead = match state.records.dead((page - 1) * PAGE, PAGE) {
8b8e83329a1datagen2026-10-12 12:01 UTC66
        Ok(dead) => dead,
8b8e83329a1datagen2026-10-12 12:01 UTC67
        Err(error) => return records_unavailable(&error),
8b8e83329a1datagen2026-10-12 12:01 UTC68
    };
8b8e83329a1datagen2026-10-12 12:01 UTC69
    let full = dead.len() == PAGE;
8b8e83329a1datagen2026-10-12 12:01 UTC70
    let runs: Vec<Value> = dead
8b8e83329a1datagen2026-10-12 12:01 UTC71
        .into_iter()
8b8e83329a1datagen2026-10-12 12:01 UTC72
        .filter_map(|record| {
8b8e83329a1datagen2026-10-12 12:01 UTC73
            let cause = record.cause?;
8b8e83329a1datagen2026-10-12 12:01 UTC74
            Some(json!({
8b8e83329a1datagen2026-10-12 12:01 UTC75
                "public_id": record.public_id,
8b8e83329a1datagen2026-10-12 12:01 UTC76
                "cause": format!("{PROBLEM_BASE}{}", cause.slug()),
8b8e83329a1datagen2026-10-12 12:01 UTC77
                "detail": cause.detail(),
8b8e83329a1datagen2026-10-12 12:01 UTC78
                "turn": record.turn,
8b8e83329a1datagen2026-10-12 12:01 UTC79
            }))
8b8e83329a1datagen2026-10-12 12:01 UTC80
        })
8b8e83329a1datagen2026-10-12 12:01 UTC81
        .collect();
4f54028cba69atagen2026-10-14 11:45 UTC82
    paged(json!({ "runs": runs }), GRAVEYARD_PATH, "", page, full)
8b8e83329a1datagen2026-10-12 12:01 UTC83
}
8b8e83329a1datagen2026-10-12 12:01 UTC84
8b8e83329a1datagen2026-10-12 12:01 UTC85
fn ranking(state: &Shared, ctx: &Ctx) -> Result<Ranking, Problem> {
8b8e83329a1datagen2026-10-12 12:01 UTC86
    match ctx.query("sort") {
8b8e83329a1datagen2026-10-12 12:01 UTC87
        None | Some("self-reported-length") => Ok(Ranking::SelfReported),
8b8e83329a1datagen2026-10-12 12:01 UTC88
        Some("volume") => Ok(Ranking::Volume),
8b8e83329a1datagen2026-10-12 12:01 UTC89
        Some("content-length") => match ctx.cookie("ruler") {
8b8e83329a1datagen2026-10-12 12:01 UTC90
            Some(ruler) if state.config.secret.verify("ruler", ruler) == Some("1") => {
8b8e83329a1datagen2026-10-12 12:01 UTC91
                Ok(Ranking::ContentLength)
8b8e83329a1datagen2026-10-12 12:01 UTC92
            }
8b8e83329a1datagen2026-10-12 12:01 UTC93
            Some(_) => Err(Problem::new(
8b8e83329a1datagen2026-10-12 12:01 UTC94
                StatusCode::BAD_REQUEST,
8b8e83329a1datagen2026-10-12 12:01 UTC95
                "exaggeration",
8b8e83329a1datagen2026-10-12 12:01 UTC96
                "The server has measured. Lying is not a technique.",
8b8e83329a1datagen2026-10-12 12:01 UTC97
            )
8b8e83329a1datagen2026-10-12 12:01 UTC98
            .with("cookie", "ruler")),
8b8e83329a1datagen2026-10-12 12:01 UTC99
            None => Err(Problem::new(
8b8e83329a1datagen2026-10-12 12:01 UTC100
                StatusCode::FORBIDDEN,
8b8e83329a1datagen2026-10-12 12:01 UTC101
                "ruler-required",
8b8e83329a1datagen2026-10-12 12:01 UTC102
                "Sorting by the truth requires a Ruler.",
8b8e83329a1datagen2026-10-12 12:01 UTC103
            )),
8b8e83329a1datagen2026-10-12 12:01 UTC104
        },
8b8e83329a1datagen2026-10-12 12:01 UTC105
        Some(_) => Err(Problem::new(
8b8e83329a1datagen2026-10-12 12:01 UTC106
            StatusCode::BAD_REQUEST,
8b8e83329a1datagen2026-10-12 12:01 UTC107
            "unknown-sort",
8b8e83329a1datagen2026-10-12 12:01 UTC108
            "The leaderboard sorts by self-reported-length, volume or content-length.",
8b8e83329a1datagen2026-10-12 12:01 UTC109
        )),
8b8e83329a1datagen2026-10-12 12:01 UTC110
    }
8b8e83329a1datagen2026-10-12 12:01 UTC111
}
8b8e83329a1datagen2026-10-12 12:01 UTC112
8b8e83329a1datagen2026-10-12 12:01 UTC113
fn score(ranking: Ranking, record: &Record) -> f64 {
8b8e83329a1datagen2026-10-12 12:01 UTC114
    match ranking {
8b8e83329a1datagen2026-10-12 12:01 UTC115
        Ranking::SelfReported => record.self_reported,
8b8e83329a1datagen2026-10-12 12:01 UTC116
        Ranking::ContentLength => record.length,
8b8e83329a1datagen2026-10-12 12:01 UTC117
        Ranking::Volume => volume(record.self_reported, record.girth),
8b8e83329a1datagen2026-10-12 12:01 UTC118
    }
8b8e83329a1datagen2026-10-12 12:01 UTC119
}
8b8e83329a1datagen2026-10-12 12:01 UTC120
8b8e83329a1datagen2026-10-12 12:01 UTC121
fn entry(ranking: Ranking, record: &Record) -> Value {
8b8e83329a1datagen2026-10-12 12:01 UTC122
    let mut entry = json!({
8b8e83329a1datagen2026-10-12 12:01 UTC123
        "public_id": record.public_id,
8b8e83329a1datagen2026-10-12 12:01 UTC124
        "status": record.status.name(),
8b8e83329a1datagen2026-10-12 12:01 UTC125
        "floor": record.floor,
8b8e83329a1datagen2026-10-12 12:01 UTC126
        "self_reported_length": (record.self_reported * 100.0).round() / 100.0,
8b8e83329a1datagen2026-10-12 12:01 UTC127
        "ranked": !record.seeded,
8b8e83329a1datagen2026-10-12 12:01 UTC128
        "viral": record.viral,
8b8e83329a1datagen2026-10-12 12:01 UTC129
    });
8b8e83329a1datagen2026-10-12 12:01 UTC130
    match ranking {
8b8e83329a1datagen2026-10-12 12:01 UTC131
        Ranking::Volume => {
8b8e83329a1datagen2026-10-12 12:01 UTC132
            entry["volume"] = json!((score(ranking, record) * 100.0).round() / 100.0);
8b8e83329a1datagen2026-10-12 12:01 UTC133
        }
8b8e83329a1datagen2026-10-12 12:01 UTC134
        Ranking::ContentLength => {
8b8e83329a1datagen2026-10-12 12:01 UTC135
            entry["content_length"] = json!((record.length * MM_PER_INCH).round());
8b8e83329a1datagen2026-10-12 12:01 UTC136
        }
8b8e83329a1datagen2026-10-12 12:01 UTC137
        Ranking::SelfReported => {}
8b8e83329a1datagen2026-10-12 12:01 UTC138
    }
8b8e83329a1datagen2026-10-12 12:01 UTC139
    entry
8b8e83329a1datagen2026-10-12 12:01 UTC140
}
8b8e83329a1datagen2026-10-12 12:01 UTC141
8b8e83329a1datagen2026-10-12 12:01 UTC142
/// Live runs are ranked alongside the recorded ones, and a live run outranks its own stale record.
8b8e83329a1datagen2026-10-12 12:01 UTC143
pub fn leaderboard(state: &mut Shared, ctx: &Ctx) -> Response {
8b8e83329a1datagen2026-10-12 12:01 UTC144
    let (ranking, page) = match ranking(state, ctx).and_then(|r| Ok((r, page(ctx)?))) {
8b8e83329a1datagen2026-10-12 12:01 UTC145
        Ok(parsed) => parsed,
8b8e83329a1datagen2026-10-12 12:01 UTC146
        Err(problem) => return problem.into_response(),
8b8e83329a1datagen2026-10-12 12:01 UTC147
    };
8b8e83329a1datagen2026-10-12 12:01 UTC148
    let seeded = ctx.query("seeded") == Some("include");
8b8e83329a1datagen2026-10-12 12:01 UTC149
    let mut entries: Vec<Record> = state
8b8e83329a1datagen2026-10-12 12:01 UTC150
        .runs
8b8e83329a1datagen2026-10-12 12:01 UTC151
        .each(Record::of)
8b8e83329a1datagen2026-10-12 12:01 UTC152
        .into_iter()
8b8e83329a1datagen2026-10-12 12:01 UTC153
        .filter(|record| seeded || !record.seeded)
8b8e83329a1datagen2026-10-12 12:01 UTC154
        .collect();
8b8e83329a1datagen2026-10-12 12:01 UTC155
    let wanted = page * PAGE;
8b8e83329a1datagen2026-10-12 12:01 UTC156
    match state
8b8e83329a1datagen2026-10-12 12:01 UTC157
        .records
8b8e83329a1datagen2026-10-12 12:01 UTC158
        .top(ranking, seeded, 0, wanted + entries.len())
8b8e83329a1datagen2026-10-12 12:01 UTC159
    {
8b8e83329a1datagen2026-10-12 12:01 UTC160
        Ok(recorded) => entries.extend(
8b8e83329a1datagen2026-10-12 12:01 UTC161
            recorded
8b8e83329a1datagen2026-10-12 12:01 UTC162
                .into_iter()
8b8e83329a1datagen2026-10-12 12:01 UTC163
                .filter(|record| !state.runs.contains(&record.id)),
8b8e83329a1datagen2026-10-12 12:01 UTC164
        ),
8b8e83329a1datagen2026-10-12 12:01 UTC165
        Err(error) => return records_unavailable(&error),
8b8e83329a1datagen2026-10-12 12:01 UTC166
    }
8b8e83329a1datagen2026-10-12 12:01 UTC167
    entries.sort_by(|a, b| score(ranking, b).total_cmp(&score(ranking, a)));
8b8e83329a1datagen2026-10-12 12:01 UTC168
    let full = entries.len() > wanted;
8b8e83329a1datagen2026-10-12 12:01 UTC169
    let listed: Vec<Value> = entries
8b8e83329a1datagen2026-10-12 12:01 UTC170
        .iter()
8b8e83329a1datagen2026-10-12 12:01 UTC171
        .skip(wanted - PAGE)
8b8e83329a1datagen2026-10-12 12:01 UTC172
        .take(PAGE)
8b8e83329a1datagen2026-10-12 12:01 UTC173
        .map(|record| entry(ranking, record))
8b8e83329a1datagen2026-10-12 12:01 UTC174
        .collect();
8b8e83329a1datagen2026-10-12 12:01 UTC175
    let mut query = Vec::new();
8b8e83329a1datagen2026-10-12 12:01 UTC176
    if let Some(sort) = ctx.query("sort") {
8b8e83329a1datagen2026-10-12 12:01 UTC177
        query.push(format!("sort={sort}"));
8b8e83329a1datagen2026-10-12 12:01 UTC178
    }
8b8e83329a1datagen2026-10-12 12:01 UTC179
    if seeded {
8b8e83329a1datagen2026-10-12 12:01 UTC180
        query.push("seeded=include".to_owned());
8b8e83329a1datagen2026-10-12 12:01 UTC181
    }
8b8e83329a1datagen2026-10-12 12:01 UTC182
    paged(
8b8e83329a1datagen2026-10-12 12:01 UTC183
        json!({ "entries": listed }),
4f54028cba69atagen2026-10-14 11:45 UTC184
        LEADERBOARD_PATH,
8b8e83329a1datagen2026-10-12 12:01 UTC185
        &query.join("&"),
8b8e83329a1datagen2026-10-12 12:01 UTC186
        page,
8b8e83329a1datagen2026-10-12 12:01 UTC187
        full,
8b8e83329a1datagen2026-10-12 12:01 UTC188
    )
8b8e83329a1datagen2026-10-12 12:01 UTC189
}