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
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 errorRecords 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 errorThe 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 errorA 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 errorThe 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 errorTakes 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.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/leaderboard.qwer$ curl -O https://quillolang.org/plugins/files/leaderboard.qweThen, in your program, all of it or only what you call:
use leaderboard.qweruse leaderboard_score, leaderboard_top from leaderboard // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
Example
// 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
//! 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, } })}Related
ratings.qwer · 1.0.0Player and team ratings from results: Elo with each side's chance, Glicko-2 with uncertainty and volatility, TrueSkill for two teams with draws, and a season's games run in order into a table.8 views · 10 downloadsAutocomplete trie.qwer · 1.0.0A prefix index for autocomplete kept in memory by name: entries with weights and data, the best completions of what was typed, at a word's start too.8 views · 11 downloadsMemory cache memory_cache.qwer · 1.0.0An in-memory cache shared by all requests, with a lifetime per entry, a size limit that drops the least used first, counters for rate limits, and hit statistics.8 views · 8 downloadsPercentiles quantiles.qwer · 1.0.0Streaming percentiles kept in memory by name: p50, p95 and p99 of response times or order values with a t-digest that stays small, and the rank of any value.7 views · 8 downloadsSketches sketches.qwer · 1.0.0Counting at scale in little memory, kept by name for all requests: unique visitors by HyperLogLog, merged across days, and top pages or search terms by a count-min sketch.7 views · 7 downloadsThread 0 posts
On Leaderboards: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.