Data structures

Leaderboards

Leaderboards in memory shared by all requests: scores set, kept at their best or added to, a player's rank, the top N, and the players around one.

Version 1.0.0 · 5 functions · 0 crates

#games#ranking#memory#gamification

Functions

What use leaderboard.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use leaderboard_score, leaderboard_top from leaderboard (Quillo 0.6).

leaderboard_score(board: str, player: str, score: int, mode: str) → anyor an error

Records score for player on board (made on first use) and gives { rank, player, score }. mode is "set" (this is the score now), "best" (kept only when higher than the player's best: arcade high scores) or "add" (added to the player's total: points earned). Up to a million players a board.

leaderboard_top(board: str, n: int) → listor an error

The first n players of board (up to 1,000), best first, as [{ rank, player, score }]; ties are in name order and share their rank.

leaderboard_rank(board: str, player: str) → any?or an error

A player's { rank, player, score, players } on board, players being how many are on it; null when the player has no score.

leaderboard_around(board: str, player: str, n: int) → listor an error

The n players above player (up to 100), the player, and the n below, as [{ rank, player, score }]: "you are 1,204th, here are your neighbours". Empty when the player has no score.

leaderboard_reset(board: str, player: str) → intor an error

Takes player off board, or, when player is "", empties the whole board. Gives how many players were taken off.

Get it

$ quillo install leaderboard               # leaderboard.qwer into this folder$ quillo install leaderboard --example     # and its example, leaderboard.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/leaderboard.qwer$ curl -O https://quillolang.org/plugins/files/leaderboard.qwe

Then, in your program, all of it or only what you call:

main.qwe
use leaderboard.qweruse leaderboard_score, leaderboard_top from leaderboard      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]

Example

leaderboard.qwe
// A game's high-score table: best scores kept, the top 3, a player's rank and neighbours.////   quillo run leaderboard.qwe      then open http://localhost:8080/boarduse "leaderboard.qwer"route POST /score = leaderboard_score("arcade", request.json.player, int(request.json.score), "best")?route GET /board {    leaderboard_reset("demo", "")?    for p in [["ada", 900], ["bo", 750], ["cy", 750], ["di", 600], ["ed", 1200], ["fi", 300]] {        leaderboard_score("demo", p[0], p[1], "best")?    }    leaderboard_score("demo", "fi", 100, "best")?          // lower than fi's best: kept at 300    leaderboard_score("demo", "di", 200, "add")?           // 600 + 200    { top: leaderboard_top("demo", 3)?, di: leaderboard_rank("demo", "di")?, around_cy: leaderboard_around("demo", "cy", 1)? }}
$ quillo run leaderboard.qwe$ curl 'localhost:8080/board'

Source

leaderboard.qwer
//! Leaderboards in memory, shared by all requests: scores set, kept at their best or added to, a player's rank, the top N, and the players around one, fast for a million players.//! version: 1.0.0//!//! [dependencies]use serde_json::{json, Value};use std::cmp::Reverse;use std::collections::{BTreeSet, HashMap};use std::sync::Mutex;#[derive(Default)]struct Board {    scores: HashMap<String, i64>,    /// Highest first, then by name: the board in order.    order: BTreeSet<(Reverse<i64>, String)>,}impl Board {    /// Rank as in sports: players with the same score share it (1, 2, 2, 4).    fn rank(&self, score: i64) -> usize {        self.order.range(..(Reverse(score), String::new())).count() + 1    }    fn row(&self, p: &str, s: i64) -> Value {        json!({ "rank": self.rank(s), "player": p, "score": s })    }}static BOARDS: Mutex<Option<HashMap<String, Board>>> = Mutex::new(None);fn with<T>(board: &str, f: impl FnOnce(&mut Board) -> T) -> Result<T, String> {    let mut g = BOARDS.lock().unwrap_or_else(|e| e.into_inner());    let all = g.get_or_insert_with(HashMap::new);    if !all.contains_key(board) && all.len() >= 10_000 {        return Err("up to 10,000 boards: leaderboard_reset old ones".into());    }    Ok(f(all.entry(board.to_string()).or_default()))}/// Records `score` for `player` on `board` (made on first use) and gives `{ rank, player, score }`. `mode` is "set" (this is the score now), "best" (kept only when higher than the player's best: arcade high scores) or "add" (added to the player's total: points earned). Up to a million players a board.pub fn leaderboard_score(board: &str, player: &str, score: i64, mode: &str) -> Result<Value, String> {    if player.is_empty() || player.len() > 200 {        return Err("player names are 1 to 200 characters".into());    }    with(board, |b| {        let old = b.scores.get(player).copied();        let new = match (mode, old) {            ("set", _) | ("best", None) | ("add", None) => score,            ("best", Some(o)) => o.max(score),            ("add", Some(o)) => o.saturating_add(score),            (m, _) => return Err(format!("mode is \"set\", \"best\" or \"add\", not {:?}", m)),        };        if old.is_none() && b.scores.len() >= 1_000_000 {            return Err("up to a million players a board".into());        }        if let Some(o) = old {            b.order.remove(&(Reverse(o), player.to_string()));        }        b.scores.insert(player.to_string(), new);        b.order.insert((Reverse(new), player.to_string()));        Ok(b.row(player, new))    })?}/// The first `n` players of `board` (up to 1,000), best first, as `[{ rank, player, score }]`; ties are in name order and share their rank.pub fn leaderboard_top(board: &str, n: i64) -> Result<Vec<Value>, String> {    with(board, |b| {        let mut out: Vec<Value> = Vec::new();        let mut last: Option<(i64, usize)> = None;        for (i, (Reverse(s), p)) in b.order.iter().take(n.clamp(0, 1000) as usize).enumerate() {            let rank = match last { Some((ls, lr)) if ls == *s => lr, _ => i + 1 };            last = Some((*s, rank));            out.push(json!({ "rank": rank, "player": p, "score": s }));        }        out    })}/// A player's `{ rank, player, score, players }` on `board`, `players` being how many are on it; null when the player has no score.pub fn leaderboard_rank(board: &str, player: &str) -> Result<Option<Value>, String> {    with(board, |b| b.scores.get(player).map(|&s| {        let mut r = b.row(player, s);        r["players"] = json!(b.scores.len());        r    }))}/// The `n` players above `player` (up to 100), the player, and the `n` below, as `[{ rank, player, score }]`: "you are 1,204th, here are your neighbours". Empty when the player has no score.pub fn leaderboard_around(board: &str, player: &str, n: i64) -> Result<Vec<Value>, String> {    let n = n.clamp(0, 100) as usize;    with(board, |b| {        let Some(&s) = b.scores.get(player) else { return vec![] };        let key = (Reverse(s), player.to_string());        let mut above: Vec<&(Reverse<i64>, String)> = b.order.range(..key.clone()).rev().take(n).collect();        above.reverse();        let below = b.order.range(key..).take(n + 1);        above.into_iter().chain(below).map(|(Reverse(s), p)| b.row(p, *s)).collect()    })}/// Takes `player` off `board`, or, when `player` is "", empties the whole board. Gives how many players were taken off.pub fn leaderboard_reset(board: &str, player: &str) -> Result<i64, String> {    with(board, |b| {        if player.is_empty() {            let n = b.scores.len() as i64;            *b = Board::default();            return n;        }        match b.scores.remove(player) {            Some(s) => { b.order.remove(&(Reverse(s), player.to_string())); 1 }            None => 0,        }    })}
Thread
76Useful? Like it, or add to its thread below.

Thread 0 posts

On Leaderboards: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.