123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259 |
- import bancho from './bancho.js';
- import db from './database.js';
- import Config from './util/config.js';
- import {capture_sentry_exception, gen_url} from './util/helpers.js';
- import {get_division_from_elo, get_rankup_progress, update_the_one} from './elo_cache.js';
- const get_rating_stmt = db.prepare(`SELECT * FROM rating WHERE user_id = ? AND mode = ?`);
- const update_rating_stmt = db.prepare(`
- UPDATE rating SET
- s3_scores = ?,
- total_scores = ?,
- elo = ?
- WHERE user_id = ? AND mode = ?`,
- );
- const insert_score_stmt = db.prepare(`INSERT INTO score (
- game_id, user_id, mode, accuracy, score, max_combo,
- count_50, count_100, count_300, count_miss, count_geki, count_katu,
- perfect, enabled_mods, created_at, beatmap_id, placement, dodged, elo_diff
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
- );
- const better_users_stmt = db.prepare(`
- SELECT COUNT(*) AS nb FROM rating r1, rating r2
- WHERE r1.s3_scores >= ? AND r1.mode = ?
- AND r2.s3_scores >= ? AND r2.mode = ?
- AND r1.elo > r2.elo AND r2.user_id = ?`,
- );
- const nb_active_users_stmt = db.prepare(`SELECT COUNT(*) AS nb FROM rating WHERE s3_scores >= ? AND mode = ?`);
- const nb_active_users = [
- nb_active_users_stmt.get(Config.games_needed_for_rank, 0).nb,
- nb_active_users_stmt.get(Config.games_needed_for_rank, 1).nb,
- nb_active_users_stmt.get(Config.games_needed_for_rank, 2).nb,
- nb_active_users_stmt.get(Config.games_needed_for_rank, 3).nb,
- ];
- const ratings_cache = [[], [], [], []];
- function get_rating(user_id, ruleset) {
- let rating = ratings_cache[ruleset][user_id];
- if (typeof rating === 'undefined') {
- rating = get_rating_stmt.get(user_id, ruleset);
- ratings_cache[ruleset][user_id] = rating;
- }
- return rating;
- }
- async function save_game_and_update_rating(lobby, game) {
- if (!game || !game.scores) return;
- if (game.scores.length < 2) return;
- const insert_game = db.prepare(`INSERT INTO
- game (game_id, match_id, start_time, end_time, beatmap_id, play_mode, scoring_type, team_type, mods)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
- );
- try {
- insert_game.run(
- game.id, lobby.id, Date.parse(game.start_time).valueOf(), Date.parse(game.end_time).valueOf(),
- game.beatmap.id, game.mode_int, game.scoring_type, game.team_type, JSON.stringify(game.mods),
- );
- } catch (err) {
- console.error(`Game #${game.id} already saved! Ignoring.`);
- return;
- }
- // Sort from highest to lowest score to get placements
- game.scores = game.scores.sort((a, b) => b.score - a.score);
- let placement = 1;
- let worst_score = game.scores[0];
- for (const score of game.scores) {
- if (score.score < worst_score) placement++;
- score.placement = placement;
- worst_score = score.score;
- }
- // Get current user ratings
- for (const score of game.scores) {
- // Must be set BEFORE saving scores because we cache ratings
- score.tms = Date.parse(score.created_at).valueOf();
- score.rating = get_rating(score.user_id, game.mode_int);
- score.expected = 0;
- score.actual = 0;
- score.new_elo = score.rating.elo;
- }
- const today = (new Date()).getDay();
- const is_weekend = (today == 0) || (today == 6);
- // Compute elo changes
- for (const player of game.scores) {
- for (const opponent of game.scores) {
- if (player == opponent) continue;
- let perf = 0.5;
- if (player.score > opponent.score) perf = 1;
- if (player.score < opponent.score) perf = 0;
- if (opponent.dodged) perf = 1;
- if (player.dodged) perf = 0;
- player.actual += perf;
- player.expected += 1 / (1 + Math.pow(10, (opponent.rating.elo - player.rating.elo) / 480));
- }
- let k_factor = 32;
- if (is_weekend || player.rating.s3_scores < Config.games_needed_for_rank) k_factor *= 1.25;
- if (player.rating.elo > 2100) k_factor *= 0.75;
- if (player.rating.elo > 2400) k_factor *= 0.75;
- player.new_elo = player.rating.elo + k_factor * (player.actual - player.expected);
- if (player.new_elo < 100) player.new_elo = 100;
- }
- // Save ratings and scores
- for (const score of game.scores) {
- insert_score_stmt.run(
- game.id, score.user_id, game.mode_int, score.accuracy, score.score, score.max_combo,
- score.statistics.count_50, score.statistics.count_100, score.statistics.count_300,
- score.statistics.count_miss, score.statistics.count_geki, score.statistics.count_katu,
- score.perfect ? 1 : 0, JSON.stringify(score.mods),
- Date.parse(score.created_at).valueOf(), game.beatmap.id, score.placement,
- score.dodged ? 1 : 0, score.new_elo - score.rating.elo,
- );
- // score.rating is cached, so updating it here
- if (bancho.connected) score.rating.s3_scores++;
- score.rating.total_scores++;
- score.rating.elo = score.new_elo;
- // We also cache the total number of active users for each ruleset :)
- if (score.rating.s3_scores == 10) {
- nb_active_users[score.rating.mode]++;
- }
- update_rating_stmt.run(
- score.rating.s3_scores,
- score.rating.total_scores,
- score.rating.elo,
- score.rating.user_id,
- score.rating.mode,
- );
- }
- // Usually, we're in a live lobby, but sometimes we're just recomputing
- // scores (like after updating the ranking algorithm), so we return early.
- if (!lobby || !bancho.connected) {
- return;
- }
- const RANK_DIVISIONS = [
- 'Unranked',
- 'Cardboard',
- 'Wood',
- 'Wood+',
- 'Bronze',
- 'Bronze+',
- 'Silver',
- 'Silver+',
- 'Gold',
- 'Gold+',
- 'Platinum',
- 'Platinum+',
- 'Diamond',
- 'Diamond+',
- 'Legendary',
- 'The One',
- ];
- const rank_changes = [];
- for (const score of game.scores) {
- const player = lobby.players.find((player) => player.user_id == score.user_id);
- if (player.ratings[game.mode_int].s3_scores < Config.games_needed_for_rank) continue;
- const old_rank_text = player.ratings[game.mode_int].division;
- const new_rank_text = get_division_from_elo(score.rating.elo, game.mode_int);
- if (old_rank_text != new_rank_text) {
- const user_page = gen_url(game.mode_int, `/u/${player.user_id}/`);
- const direction = (RANK_DIVISIONS.indexOf(new_rank_text) > RANK_DIVISIONS.indexOf(old_rank_text)) ? '▲' : '▼';
- rank_changes.push(`${player.username} [${user_page} ${direction} ${new_rank_text} ]`);
- db.prepare(`UPDATE rating SET division = ? WHERE user_id = ? AND mode = ?`).run(new_rank_text, player.user_id, game.mode_int);
- player.ratings[game.mode_int].division = new_rank_text;
- }
- }
- // Update "The One"
- for (const score of game.scores) {
- try {
- await update_the_one(score.user_id, score.rating.elo, game.mode_int);
- } catch (err) {
- console.error('Error while updating the one', err);
- capture_sentry_exception(err);
- }
- }
- if (rank_changes.length > 0) {
- // Max 8 rank updates per message - or else it starts getting truncated
- const MAX_UPDATES_PER_MSG = 6;
- for (let i = 0, j = rank_changes.length; i < j; i += MAX_UPDATES_PER_MSG) {
- const updates = rank_changes.slice(i, i + MAX_UPDATES_PER_MSG);
- if (i == 0) {
- await lobby.send('Rank updates: ' + updates.join(' | '));
- } else {
- await lobby.send(updates.join(' | '));
- }
- }
- }
- }
- function get_active_users(ruleset) {
- return nb_active_users[ruleset];
- }
- // TODO: optimize
- function get_user_rank(user_id, ruleset) {
- if (!user_id) return null;
- const rating = get_rating(user_id, ruleset);
- if (!rating) {
- // User not initialized yet; doesn't have a rank
- return null;
- }
- const better_users = better_users_stmt.get(
- Config.games_needed_for_rank, ruleset,
- Config.games_needed_for_rank, ruleset,
- user_id,
- );
- const is_ranked = rating.s3_scores >= Config.games_needed_for_rank;
- const info = {
- elo: '???',
- fancy_elo: '???',
- rank_nb: '???',
- text: 'Unranked',
- s1_division: rating.s1_division,
- s2_division: rating.s2_division,
- s3_scores: rating.s3_scores,
- total_scores: rating.total_scores,
- is_ranked: is_ranked,
- };
- if (is_ranked) {
- info.elo = rating.elo;
- info.fancy_elo = Math.round(rating.elo);
- info.rank_nb = (better_users.nb + 1);
- info.text = get_division_from_elo(rating.elo, ruleset);
- info.rankup = get_rankup_progress(rating.elo, ruleset);
- }
- return info;
- }
- export {save_game_and_update_rating, get_user_rank, get_active_users};
|