atagen

jelq

public

an http based roguelike rpg about jelqing

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