feat!: overhaul the entire rating algorithm
I am far, FAR too lazy to split this into multiple commits, so here it is.
This commit is contained in:
parent
c2158f85f7
commit
4b44b82531
420
src/database.rs
420
src/database.rs
|
@ -1,6 +1,5 @@
|
||||||
use crate::queries::*;
|
use crate::queries::*;
|
||||||
use sqlite::*;
|
use sqlite::*;
|
||||||
use std::fs::{self, OpenOptions};
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
pub struct DatasetMetadata {
|
pub struct DatasetMetadata {
|
||||||
|
@ -14,15 +13,14 @@ pub struct DatasetMetadata {
|
||||||
pub country: Option<String>,
|
pub country: Option<String>,
|
||||||
pub state: Option<String>,
|
pub state: Option<String>,
|
||||||
|
|
||||||
pub set_limit: u64,
|
pub decay_const: f64,
|
||||||
pub decay_rate: f64,
|
pub var_const: f64,
|
||||||
pub adj_decay_rate: f64,
|
|
||||||
pub period: f64,
|
|
||||||
pub tau: f64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the path to the datasets file.
|
/// Return the path to the datasets file.
|
||||||
fn datasets_path(dir: &Path) -> std::io::Result<PathBuf> {
|
fn datasets_path(dir: &Path) -> std::io::Result<PathBuf> {
|
||||||
|
use std::fs::{self, OpenOptions};
|
||||||
|
|
||||||
let mut path = dir.to_owned();
|
let mut path = dir.to_owned();
|
||||||
|
|
||||||
// Create datasets path if it doesn't exist
|
// Create datasets path if it doesn't exist
|
||||||
|
@ -50,11 +48,8 @@ CREATE TABLE IF NOT EXISTS datasets (
|
||||||
game_slug TEXT NOT NULL,
|
game_slug TEXT NOT NULL,
|
||||||
country TEXT,
|
country TEXT,
|
||||||
state TEXT,
|
state TEXT,
|
||||||
set_limit INTEGER NOT NULL,
|
|
||||||
decay_rate REAL NOT NULL,
|
decay_rate REAL NOT NULL,
|
||||||
adj_decay_rate REAL NOT NULL,
|
var_const REAL NOT NULL
|
||||||
period REAL NOT NULL,
|
|
||||||
tau REAL NOT NULL
|
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS players (
|
CREATE TABLE IF NOT EXISTS players (
|
||||||
|
@ -113,11 +108,8 @@ pub fn list_datasets(connection: &Connection) -> sqlite::Result<Vec<(String, Dat
|
||||||
game_slug: r_.read::<&str, _>("game_slug").to_owned(),
|
game_slug: r_.read::<&str, _>("game_slug").to_owned(),
|
||||||
country: r_.read::<Option<&str>, _>("country").map(String::from),
|
country: r_.read::<Option<&str>, _>("country").map(String::from),
|
||||||
state: r_.read::<Option<&str>, _>("state").map(String::from),
|
state: r_.read::<Option<&str>, _>("state").map(String::from),
|
||||||
set_limit: r_.read::<i64, _>("set_limit") as u64,
|
decay_const: r_.read::<f64, _>("decay_rate"),
|
||||||
decay_rate: r_.read::<f64, _>("decay_rate"),
|
var_const: r_.read::<f64, _>("adj_decay_rate"),
|
||||||
adj_decay_rate: r_.read::<f64, _>("adj_decay_rate"),
|
|
||||||
period: r_.read::<f64, _>("period"),
|
|
||||||
tau: r_.read::<f64, _>("tau"),
|
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
|
@ -157,17 +149,14 @@ pub fn new_dataset(
|
||||||
dataset: &str,
|
dataset: &str,
|
||||||
metadata: DatasetMetadata,
|
metadata: DatasetMetadata,
|
||||||
) -> sqlite::Result<()> {
|
) -> sqlite::Result<()> {
|
||||||
let query1 = r#"INSERT INTO datasets VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#;
|
let query1 = r#"INSERT INTO datasets VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#;
|
||||||
let query2 = format!(
|
let query2 = format!(
|
||||||
r#"CREATE TABLE "{0}_players" (
|
r#"CREATE TABLE "{0}_players" (
|
||||||
id INTEGER PRIMARY KEY REFERENCES players,
|
id INTEGER PRIMARY KEY REFERENCES players,
|
||||||
last_played INTEGER NOT NULL,
|
|
||||||
deviation REAL NOT NULL,
|
|
||||||
volatility REAL NOT NULL,
|
|
||||||
|
|
||||||
sets_won TEXT NOT NULL,
|
sets_won TEXT NOT NULL DEFAULT '',
|
||||||
sets_count_won INTEGER AS (length(sets_won) - length(replace(sets_won, ';', ''))),
|
sets_count_won INTEGER AS (length(sets_won) - length(replace(sets_won, ';', ''))),
|
||||||
sets_lost TEXT NOT NULL,
|
sets_lost TEXT NOT NULL DEFAULT '',
|
||||||
sets_count_lost INTEGER AS (length(sets_lost) - length(replace(sets_lost, ';', ''))),
|
sets_count_lost INTEGER AS (length(sets_lost) - length(replace(sets_lost, ';', ''))),
|
||||||
sets TEXT AS (sets_won || sets_lost),
|
sets TEXT AS (sets_won || sets_lost),
|
||||||
sets_count INTEGER AS (sets_count_won + sets_count_lost)
|
sets_count INTEGER AS (sets_count_won + sets_count_lost)
|
||||||
|
@ -177,10 +166,12 @@ CREATE TABLE "{0}_network" (
|
||||||
player_A INTEGER NOT NULL,
|
player_A INTEGER NOT NULL,
|
||||||
player_B INTEGER NOT NULL,
|
player_B INTEGER NOT NULL,
|
||||||
advantage REAL NOT NULL,
|
advantage REAL NOT NULL,
|
||||||
|
variance REAL NOT NULL,
|
||||||
|
last_updated INTEGER NOT NULL,
|
||||||
|
|
||||||
sets_A TEXT NOT NULL,
|
sets_A TEXT NOT NULL DEFAULT '',
|
||||||
sets_count_A INTEGER AS (length(sets_A) - length(replace(sets_A, ';', ''))),
|
sets_count_A INTEGER AS (length(sets_A) - length(replace(sets_A, ';', ''))),
|
||||||
sets_B TEXT NOT NULL,
|
sets_B TEXT NOT NULL DEFAULT '',
|
||||||
sets_count_B INTEGER AS (length(sets_B) - length(replace(sets_B, ';', ''))),
|
sets_count_B INTEGER AS (length(sets_B) - length(replace(sets_B, ';', ''))),
|
||||||
sets TEXT AS (sets_A || sets_B),
|
sets TEXT AS (sets_A || sets_B),
|
||||||
sets_count INTEGER AS (sets_count_A + sets_count_B),
|
sets_count INTEGER AS (sets_count_A + sets_count_B),
|
||||||
|
@ -208,11 +199,8 @@ CREATE INDEX "{0}_network_B" ON "{0}_network" (player_B);"#,
|
||||||
.bind((7, &metadata.game_slug[..]))?
|
.bind((7, &metadata.game_slug[..]))?
|
||||||
.bind((8, metadata.country.as_deref()))?
|
.bind((8, metadata.country.as_deref()))?
|
||||||
.bind((9, metadata.state.as_deref()))?
|
.bind((9, metadata.state.as_deref()))?
|
||||||
.bind((10, metadata.set_limit as i64))?
|
.bind((10, metadata.decay_const))?
|
||||||
.bind((11, metadata.decay_rate))?
|
.bind((11, metadata.var_const))?
|
||||||
.bind((12, metadata.adj_decay_rate))?
|
|
||||||
.bind((13, metadata.period))?
|
|
||||||
.bind((14, metadata.tau))?
|
|
||||||
.try_for_each(|x| x.map(|_| ()))?;
|
.try_for_each(|x| x.map(|_| ()))?;
|
||||||
|
|
||||||
connection.execute(query2)
|
connection.execute(query2)
|
||||||
|
@ -242,11 +230,8 @@ pub fn get_metadata(
|
||||||
game_slug: r_.read::<&str, _>("game_slug").to_owned(),
|
game_slug: r_.read::<&str, _>("game_slug").to_owned(),
|
||||||
country: r_.read::<Option<&str>, _>("country").map(String::from),
|
country: r_.read::<Option<&str>, _>("country").map(String::from),
|
||||||
state: r_.read::<Option<&str>, _>("state").map(String::from),
|
state: r_.read::<Option<&str>, _>("state").map(String::from),
|
||||||
set_limit: r_.read::<i64, _>("set_limit") as u64,
|
decay_const: r_.read::<f64, _>("decay_rate"),
|
||||||
decay_rate: r_.read::<f64, _>("decay_rate"),
|
var_const: r_.read::<f64, _>("var_const"),
|
||||||
adj_decay_rate: r_.read::<f64, _>("adj_decay_rate"),
|
|
||||||
period: r_.read::<f64, _>("period"),
|
|
||||||
tau: r_.read::<f64, _>("tau"),
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.and_then(Result::ok))
|
.and_then(Result::ok))
|
||||||
|
@ -290,19 +275,15 @@ pub fn add_set(connection: &Connection, set: &SetId, event: EventId) -> sqlite::
|
||||||
pub fn add_players(
|
pub fn add_players(
|
||||||
connection: &Connection,
|
connection: &Connection,
|
||||||
dataset: &str,
|
dataset: &str,
|
||||||
teams: &Teams<PlayerData>,
|
players: &Vec<PlayerData>,
|
||||||
time: Timestamp,
|
|
||||||
) -> sqlite::Result<()> {
|
) -> sqlite::Result<()> {
|
||||||
let query1 = "INSERT OR IGNORE INTO players (id, discrim, name, prefix) VALUES (?, ?, ?, ?)";
|
let query1 = "INSERT OR IGNORE INTO players (id, discrim, name, prefix) VALUES (?, ?, ?, ?)";
|
||||||
let query2 = format!(
|
let query2 = format!(
|
||||||
r#"INSERT OR IGNORE INTO "{}_players"
|
r#"INSERT OR IGNORE INTO "{}_players" (id) VALUES (?)"#,
|
||||||
(id, last_played, deviation, volatility, sets_won, sets_lost)
|
|
||||||
VALUES (?, ?, 2.01, 0.06, '', '')"#,
|
|
||||||
dataset
|
dataset
|
||||||
);
|
);
|
||||||
|
|
||||||
teams.iter().try_for_each(|team| {
|
players.iter().try_for_each(
|
||||||
team.iter().try_for_each(
|
|
||||||
|PlayerData {
|
|PlayerData {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
|
@ -318,11 +299,22 @@ pub fn add_players(
|
||||||
|
|
||||||
statement = connection.prepare(&query2)?;
|
statement = connection.prepare(&query2)?;
|
||||||
statement.bind((1, id.0 as i64))?;
|
statement.bind((1, id.0 as i64))?;
|
||||||
statement.bind((2, time.0 as i64))?;
|
|
||||||
statement.into_iter().try_for_each(|x| x.map(|_| ()))
|
statement.into_iter().try_for_each(|x| x.map(|_| ()))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_players(connection: &Connection, dataset: &str) -> sqlite::Result<Vec<PlayerId>> {
|
||||||
|
let query = format!(r#"SELECT id FROM "{}_players""#, dataset,);
|
||||||
|
|
||||||
|
connection
|
||||||
|
.prepare(&query)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| {
|
||||||
|
let r_ = r?;
|
||||||
|
Ok(PlayerId(r_.read::<i64, _>("id") as u64))
|
||||||
})
|
})
|
||||||
|
.try_collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_player(connection: &Connection, player: PlayerId) -> sqlite::Result<PlayerData> {
|
pub fn get_player(connection: &Connection, player: PlayerId) -> sqlite::Result<PlayerData> {
|
||||||
|
@ -375,26 +367,6 @@ pub fn match_player_name(connection: &Connection, name: &str) -> sqlite::Result<
|
||||||
.try_collect()
|
.try_collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_player_rating_data(
|
|
||||||
connection: &Connection,
|
|
||||||
dataset: &str,
|
|
||||||
player: PlayerId,
|
|
||||||
) -> sqlite::Result<(f64, f64, Timestamp)> {
|
|
||||||
let query = format!(
|
|
||||||
r#"SELECT deviation, volatility, last_played FROM "{}_players" WHERE id = ?"#,
|
|
||||||
dataset
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut statement = connection.prepare(&query)?;
|
|
||||||
statement.bind((1, player.0 as i64))?;
|
|
||||||
statement.next()?;
|
|
||||||
Ok((
|
|
||||||
statement.read::<f64, _>("deviation")?,
|
|
||||||
statement.read::<f64, _>("volatility")?,
|
|
||||||
Timestamp(statement.read::<i64, _>("last_played")? as u64),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_player_set_counts(
|
pub fn get_player_set_counts(
|
||||||
connection: &Connection,
|
connection: &Connection,
|
||||||
dataset: &str,
|
dataset: &str,
|
||||||
|
@ -436,27 +408,21 @@ pub fn get_matchup_set_counts(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_player_data(
|
pub fn set_player_set_counts(
|
||||||
connection: &Connection,
|
connection: &Connection,
|
||||||
dataset: &str,
|
dataset: &str,
|
||||||
player: PlayerId,
|
player: PlayerId,
|
||||||
last_played: Timestamp,
|
|
||||||
deviation: f64,
|
|
||||||
volatility: f64,
|
|
||||||
won: bool,
|
won: bool,
|
||||||
set: &SetId,
|
set: &SetId,
|
||||||
) -> sqlite::Result<()> {
|
) -> sqlite::Result<()> {
|
||||||
let query = format!(
|
let query = format!(
|
||||||
r#"UPDATE "{}_players" SET deviation = :dev, volatility = :vol, last_played = :last,
|
r#"UPDATE "{}_players" SET
|
||||||
sets_won = iif(:won, sets_won || :set || ';', sets_won),
|
sets_won = iif(:won, sets_won || :set || ';', sets_won),
|
||||||
sets_lost = iif(:won, sets_lost, sets_lost || :set || ';') WHERE id = :id"#,
|
sets_lost = iif(:won, sets_lost, sets_lost || :set || ';') WHERE id = :id"#,
|
||||||
dataset
|
dataset
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut statement = connection.prepare(&query)?;
|
let mut statement = connection.prepare(&query)?;
|
||||||
statement.bind((":dev", deviation))?;
|
|
||||||
statement.bind((":vol", volatility))?;
|
|
||||||
statement.bind((":last", last_played.0 as i64))?;
|
|
||||||
statement.bind((":id", player.0 as i64))?;
|
statement.bind((":id", player.0 as i64))?;
|
||||||
statement.bind((":won", if won { 1 } else { 0 }))?;
|
statement.bind((":won", if won { 1 } else { 0 }))?;
|
||||||
statement.bind((":set", &set.0.to_string()[..]))?;
|
statement.bind((":set", &set.0.to_string()[..]))?;
|
||||||
|
@ -464,18 +430,18 @@ pub fn set_player_data(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_advantage(
|
pub fn get_network_data(
|
||||||
connection: &Connection,
|
connection: &Connection,
|
||||||
dataset: &str,
|
dataset: &str,
|
||||||
player1: PlayerId,
|
player1: PlayerId,
|
||||||
player2: PlayerId,
|
player2: PlayerId,
|
||||||
) -> sqlite::Result<Option<f64>> {
|
) -> sqlite::Result<Option<(f64, f64)>> {
|
||||||
if player1 == player2 {
|
if player1 == player2 {
|
||||||
return Ok(Some(0.0));
|
return Ok(Some((0.0, 0.0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let query = format!(
|
let query = format!(
|
||||||
r#"SELECT iif(:a > :b, -advantage, advantage) AS advantage FROM "{}_network"
|
r#"SELECT iif(:a > :b, -advantage, advantage) AS advantage, variance FROM "{}_network"
|
||||||
WHERE player_A = min(:a, :b) AND player_B = max(:a, :b)"#,
|
WHERE player_A = min(:a, :b) AND player_B = max(:a, :b)"#,
|
||||||
dataset
|
dataset
|
||||||
);
|
);
|
||||||
|
@ -484,20 +450,24 @@ pub fn get_advantage(
|
||||||
statement.bind((":a", player1.0 as i64))?;
|
statement.bind((":a", player1.0 as i64))?;
|
||||||
statement.bind((":b", player2.0 as i64))?;
|
statement.bind((":b", player2.0 as i64))?;
|
||||||
statement.next()?;
|
statement.next()?;
|
||||||
statement.read::<Option<f64>, _>("advantage")
|
Ok(statement
|
||||||
|
.read::<Option<f64>, _>("advantage")?
|
||||||
|
.zip(statement.read::<Option<f64>, _>("variance")?))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_advantage(
|
pub fn insert_network_data(
|
||||||
connection: &Connection,
|
connection: &Connection,
|
||||||
dataset: &str,
|
dataset: &str,
|
||||||
player1: PlayerId,
|
player1: PlayerId,
|
||||||
player2: PlayerId,
|
player2: PlayerId,
|
||||||
advantage: f64,
|
advantage: f64,
|
||||||
|
variance: f64,
|
||||||
|
time: Timestamp,
|
||||||
) -> sqlite::Result<()> {
|
) -> sqlite::Result<()> {
|
||||||
let query = format!(
|
let query = format!(
|
||||||
r#"INSERT INTO "{}_network"
|
r#"INSERT INTO "{}_network"
|
||||||
(player_A, player_B, advantage, sets_A, sets_B)
|
(player_A, player_B, advantage, variance, last_updated)
|
||||||
VALUES (min(:a, :b), max(:a, :b), iif(:a > :b, -:v, :v), '', '')"#,
|
VALUES (min(:a, :b), max(:a, :b), iif(:a > :b, -:v, :v), :d, :t)"#,
|
||||||
dataset
|
dataset
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -505,32 +475,67 @@ pub fn insert_advantage(
|
||||||
statement.bind((":a", player1.0 as i64))?;
|
statement.bind((":a", player1.0 as i64))?;
|
||||||
statement.bind((":b", player2.0 as i64))?;
|
statement.bind((":b", player2.0 as i64))?;
|
||||||
statement.bind((":v", advantage))?;
|
statement.bind((":v", advantage))?;
|
||||||
|
statement.bind((":d", variance))?;
|
||||||
|
statement.bind((":t", time.0 as i64))?;
|
||||||
statement.into_iter().try_for_each(|x| x.map(|_| ()))
|
statement.into_iter().try_for_each(|x| x.map(|_| ()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn adjust_advantages(
|
pub fn adjust_for_time(
|
||||||
connection: &Connection,
|
connection: &Connection,
|
||||||
dataset: &str,
|
dataset: &str,
|
||||||
set: SetId,
|
player: PlayerId,
|
||||||
|
var_const: f64,
|
||||||
|
time: Timestamp,
|
||||||
|
) -> sqlite::Result<()> {
|
||||||
|
let query = format!(
|
||||||
|
r#"UPDATE "{0}_network" SET
|
||||||
|
variance = min(variance + :c * (:t - last_updated), 5.0),
|
||||||
|
last_updated = :t
|
||||||
|
WHERE player_A = :i OR player_B = :i"#,
|
||||||
|
dataset
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut statement = connection.prepare(query)?;
|
||||||
|
statement.bind((":i", player.0 as i64))?;
|
||||||
|
statement.bind((":c", var_const))?;
|
||||||
|
statement.bind((":t", time.0 as i64))?;
|
||||||
|
statement.into_iter().try_for_each(|x| x.map(|_| ()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn glicko_adjust(
|
||||||
|
connection: &Connection,
|
||||||
|
dataset: &str,
|
||||||
|
set: &SetId,
|
||||||
player1: PlayerId,
|
player1: PlayerId,
|
||||||
player2: PlayerId,
|
player2: PlayerId,
|
||||||
|
advantage: f64,
|
||||||
|
variance: f64,
|
||||||
winner: usize,
|
winner: usize,
|
||||||
adjust1: f64,
|
|
||||||
adjust2: f64,
|
|
||||||
decay_rate: f64,
|
decay_rate: f64,
|
||||||
) -> sqlite::Result<()> {
|
) -> sqlite::Result<()> {
|
||||||
|
let score = if winner != 0 { 1.0 } else { 0.0 };
|
||||||
|
|
||||||
|
let exp_val = 1.0 / (1.0 + (-advantage).exp());
|
||||||
|
|
||||||
|
let like_var = 1.0 / exp_val / (1.0 - exp_val);
|
||||||
|
let var_new = 1.0 / (1.0 / variance + 1.0 / like_var);
|
||||||
|
let adjust = score - exp_val;
|
||||||
|
|
||||||
let query1 = format!(
|
let query1 = format!(
|
||||||
r#"UPDATE "{}_network"
|
r#"UPDATE "{}_network" SET
|
||||||
SET advantage = advantage + iif(:pl = player_A, -:v, :v) * :d
|
variance = 1.0 / (1.0 / variance + :d / :lv),
|
||||||
|
advantage = advantage + :d * iif(:pl = player_A, -:adj, :adj)
|
||||||
|
/ (1.0 / variance + :d / :lv)
|
||||||
WHERE (player_A = :pl AND player_B != :plo)
|
WHERE (player_A = :pl AND player_B != :plo)
|
||||||
OR (player_B = :pl AND player_A != :plo)"#,
|
OR (player_B = :pl AND player_A != :plo)"#,
|
||||||
dataset
|
dataset
|
||||||
);
|
);
|
||||||
let query2 = format!(
|
let query2 = format!(
|
||||||
r#"UPDATE "{}_network"
|
r#"UPDATE "{}_network" SET
|
||||||
SET advantage = advantage + iif(:a > :b, -:v, :v),
|
variance = :var,
|
||||||
sets_A = iif(:w = (:a > :b), sets_A || :set || ';', sets_A),
|
advantage = advantage + iif(:a > :b, -:adj, :adj) * :var,
|
||||||
sets_B = iif(:w = (:b > :a), sets_B || :set || ';', sets_B)
|
sets_A = iif(:w = (:a > :b), sets_A || :set || ';', sets_A),
|
||||||
|
sets_B = iif(:w = (:b > :a), sets_B || :set || ';', sets_B)
|
||||||
WHERE player_A = min(:a, :b) AND player_B = max(:a, :b)"#,
|
WHERE player_A = min(:a, :b) AND player_B = max(:a, :b)"#,
|
||||||
dataset
|
dataset
|
||||||
);
|
);
|
||||||
|
@ -538,21 +543,24 @@ WHERE player_A = min(:a, :b) AND player_B = max(:a, :b)"#,
|
||||||
let mut statement = connection.prepare(&query1)?;
|
let mut statement = connection.prepare(&query1)?;
|
||||||
statement.bind((":pl", player1.0 as i64))?;
|
statement.bind((":pl", player1.0 as i64))?;
|
||||||
statement.bind((":plo", player2.0 as i64))?;
|
statement.bind((":plo", player2.0 as i64))?;
|
||||||
statement.bind((":v", adjust1))?;
|
statement.bind((":adj", -0.5 * adjust))?;
|
||||||
statement.bind((":d", decay_rate))?;
|
statement.bind((":d", decay_rate))?;
|
||||||
|
statement.bind((":lv", like_var))?;
|
||||||
statement.into_iter().try_for_each(|x| x.map(|_| ()))?;
|
statement.into_iter().try_for_each(|x| x.map(|_| ()))?;
|
||||||
|
|
||||||
statement = connection.prepare(&query1)?;
|
statement = connection.prepare(&query1)?;
|
||||||
statement.bind((":pl", player2.0 as i64))?;
|
statement.bind((":pl", player2.0 as i64))?;
|
||||||
statement.bind((":plo", player1.0 as i64))?;
|
statement.bind((":plo", player1.0 as i64))?;
|
||||||
statement.bind((":v", adjust2))?;
|
statement.bind((":adj", 0.5 * adjust))?;
|
||||||
statement.bind((":d", decay_rate))?;
|
statement.bind((":d", decay_rate))?;
|
||||||
|
statement.bind((":lv", like_var))?;
|
||||||
statement.into_iter().try_for_each(|x| x.map(|_| ()))?;
|
statement.into_iter().try_for_each(|x| x.map(|_| ()))?;
|
||||||
|
|
||||||
statement = connection.prepare(&query2)?;
|
statement = connection.prepare(&query2)?;
|
||||||
statement.bind((":a", player1.0 as i64))?;
|
statement.bind((":a", player1.0 as i64))?;
|
||||||
statement.bind((":b", player2.0 as i64))?;
|
statement.bind((":b", player2.0 as i64))?;
|
||||||
statement.bind((":v", adjust2 - adjust1))?;
|
statement.bind((":adj", adjust))?;
|
||||||
|
statement.bind((":var", var_new))?;
|
||||||
statement.bind((":w", winner as i64))?;
|
statement.bind((":w", winner as i64))?;
|
||||||
statement.bind((":set", &set.0.to_string()[..]))?;
|
statement.bind((":set", &set.0.to_string()[..]))?;
|
||||||
statement.into_iter().try_for_each(|x| x.map(|_| ()))
|
statement.into_iter().try_for_each(|x| x.map(|_| ()))
|
||||||
|
@ -562,11 +570,11 @@ pub fn get_edges(
|
||||||
connection: &Connection,
|
connection: &Connection,
|
||||||
dataset: &str,
|
dataset: &str,
|
||||||
player: PlayerId,
|
player: PlayerId,
|
||||||
) -> sqlite::Result<Vec<(PlayerId, f64, u64)>> {
|
) -> sqlite::Result<Vec<(PlayerId, f64, f64)>> {
|
||||||
let query = format!(
|
let query = format!(
|
||||||
r#"SELECT
|
r#"SELECT
|
||||||
iif(:pl = player_B, player_A, player_B) AS id,
|
iif(:pl = player_B, player_A, player_B) AS id,
|
||||||
iif(:pl = player_B, -advantage, advantage) AS advantage, sets_count
|
iif(:pl = player_B, -advantage, advantage) AS advantage, variance
|
||||||
FROM "{}_network"
|
FROM "{}_network"
|
||||||
WHERE player_A = :pl OR player_B = :pl"#,
|
WHERE player_A = :pl OR player_B = :pl"#,
|
||||||
dataset
|
dataset
|
||||||
|
@ -581,7 +589,7 @@ pub fn get_edges(
|
||||||
Ok((
|
Ok((
|
||||||
PlayerId(r_.read::<i64, _>("id") as u64),
|
PlayerId(r_.read::<i64, _>("id") as u64),
|
||||||
r_.read::<f64, _>("advantage"),
|
r_.read::<f64, _>("advantage"),
|
||||||
r_.read::<i64, _>("sets_count") as u64,
|
r_.read::<f64, _>("variance"),
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.try_collect()
|
.try_collect()
|
||||||
|
@ -616,20 +624,20 @@ pub fn hypothetical_advantage(
|
||||||
dataset: &str,
|
dataset: &str,
|
||||||
player1: PlayerId,
|
player1: PlayerId,
|
||||||
player2: PlayerId,
|
player2: PlayerId,
|
||||||
set_limit: u64,
|
|
||||||
decay_rate: f64,
|
decay_rate: f64,
|
||||||
adj_decay_rate: f64,
|
) -> sqlite::Result<(f64, f64)> {
|
||||||
) -> sqlite::Result<f64> {
|
|
||||||
use std::collections::{HashSet, VecDeque};
|
use std::collections::{HashSet, VecDeque};
|
||||||
|
|
||||||
// Check trivial cases
|
// Check trivial cases
|
||||||
if player1 == player2 || either_isolated(connection, dataset, player1, player2)? {
|
if player1 == player2 {
|
||||||
return Ok(0.0);
|
return Ok((0.0, 0.0));
|
||||||
|
} else if decay_rate < 0.05 || either_isolated(connection, dataset, player1, player2)? {
|
||||||
|
return Ok((0.0, 5.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut visited: HashSet<PlayerId> = HashSet::new();
|
let mut visited: HashSet<PlayerId> = HashSet::new();
|
||||||
let mut queue: VecDeque<(PlayerId, Vec<(f64, f64)>)> =
|
let mut queue: VecDeque<(PlayerId, Vec<(f64, f64, f64)>)> =
|
||||||
VecDeque::from([(player1, Vec::from([(0.0, 1.0)]))]);
|
VecDeque::from([(player1, Vec::from([(0.0, 0.0, 1.0 / decay_rate)]))]);
|
||||||
|
|
||||||
let mut final_paths = Vec::new();
|
let mut final_paths = Vec::new();
|
||||||
|
|
||||||
|
@ -638,7 +646,7 @@ pub fn hypothetical_advantage(
|
||||||
|
|
||||||
let connections = get_edges(connection, dataset, visiting)?;
|
let connections = get_edges(connection, dataset, visiting)?;
|
||||||
|
|
||||||
for (id, adv, sets) in connections
|
for (id, adv, var) in connections
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|(id, _, _)| !visited.contains(id))
|
.filter(|(id, _, _)| !visited.contains(id))
|
||||||
{
|
{
|
||||||
|
@ -652,12 +660,9 @@ pub fn hypothetical_advantage(
|
||||||
};
|
};
|
||||||
|
|
||||||
if rf.len() < 100 {
|
if rf.len() < 100 {
|
||||||
let decay = if sets >= set_limit {
|
let iter = paths
|
||||||
decay_rate
|
.iter()
|
||||||
} else {
|
.map(|(av, vr, dec)| (av + adv, vr + var, dec * decay_rate));
|
||||||
adj_decay_rate
|
|
||||||
};
|
|
||||||
let iter = paths.iter().map(|(a, d)| (a + adv, d * decay));
|
|
||||||
|
|
||||||
rf.extend(iter);
|
rf.extend(iter);
|
||||||
rf.truncate(100);
|
rf.truncate(100);
|
||||||
|
@ -667,22 +672,23 @@ pub fn hypothetical_advantage(
|
||||||
visited.insert(visiting);
|
visited.insert(visiting);
|
||||||
}
|
}
|
||||||
|
|
||||||
let max_decay = final_paths
|
if final_paths.len() == 0 {
|
||||||
.iter()
|
|
||||||
.map(|x| x.1)
|
|
||||||
.max_by(|d1, d2| d1.partial_cmp(d2).unwrap());
|
|
||||||
|
|
||||||
if let Some(mdec) = max_decay {
|
|
||||||
let sum_decay = final_paths.iter().map(|x| x.1).sum::<f64>();
|
|
||||||
Ok(final_paths
|
|
||||||
.into_iter()
|
|
||||||
.map(|(adv, dec)| adv * dec)
|
|
||||||
.sum::<f64>()
|
|
||||||
/ sum_decay
|
|
||||||
* mdec)
|
|
||||||
} else {
|
|
||||||
// No paths found
|
// No paths found
|
||||||
Ok(0.0)
|
Ok((0.0, 5.0))
|
||||||
|
} else {
|
||||||
|
let sum_decay: f64 = final_paths.iter().map(|(_, _, dec)| dec).sum();
|
||||||
|
let (final_adv, final_var) = final_paths
|
||||||
|
.into_iter()
|
||||||
|
.fold((0.0, 0.0), |(av, vr), (adv, var, dec)| {
|
||||||
|
(av + adv * dec, vr + (var + adv * adv) * dec)
|
||||||
|
});
|
||||||
|
let mut final_adv = final_adv / sum_decay;
|
||||||
|
let mut final_var = final_var / sum_decay - final_adv * final_adv;
|
||||||
|
if final_var > 5.0 {
|
||||||
|
final_adv = final_adv * (5.0 / final_var).sqrt();
|
||||||
|
final_var = 5.0;
|
||||||
|
}
|
||||||
|
Ok((final_adv, final_var))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -691,21 +697,12 @@ pub fn initialize_edge(
|
||||||
dataset: &str,
|
dataset: &str,
|
||||||
player1: PlayerId,
|
player1: PlayerId,
|
||||||
player2: PlayerId,
|
player2: PlayerId,
|
||||||
set_limit: u64,
|
|
||||||
decay_rate: f64,
|
decay_rate: f64,
|
||||||
adj_decay_rate: f64,
|
time: Timestamp,
|
||||||
) -> sqlite::Result<f64> {
|
) -> sqlite::Result<(f64, f64)> {
|
||||||
let adv = hypothetical_advantage(
|
let (adv, var) = hypothetical_advantage(connection, dataset, player1, player2, decay_rate)?;
|
||||||
connection,
|
insert_network_data(connection, dataset, player1, player2, adv, var, time)?;
|
||||||
dataset,
|
Ok((adv, var))
|
||||||
player1,
|
|
||||||
player2,
|
|
||||||
set_limit,
|
|
||||||
decay_rate,
|
|
||||||
adj_decay_rate,
|
|
||||||
)?;
|
|
||||||
insert_advantage(connection, dataset, player1, player2, adv)?;
|
|
||||||
Ok(adv)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
|
@ -729,8 +726,7 @@ CREATE TABLE IF NOT EXISTS datasets (
|
||||||
set_limit INTEGER NOT NULL,
|
set_limit INTEGER NOT NULL,
|
||||||
decay_rate REAL NOT NULL,
|
decay_rate REAL NOT NULL,
|
||||||
adj_decay_rate REAL NOT NULL,
|
adj_decay_rate REAL NOT NULL,
|
||||||
period REAL NOT NULL,
|
var_const
|
||||||
tau REAL NOT NULL
|
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS players (
|
CREATE TABLE IF NOT EXISTS players (
|
||||||
|
@ -769,11 +765,8 @@ CREATE TABLE IF NOT EXISTS sets (
|
||||||
game_slug: String::from("test"),
|
game_slug: String::from("test"),
|
||||||
country: None,
|
country: None,
|
||||||
state: None,
|
state: None,
|
||||||
set_limit: 0,
|
decay_const: 0.5,
|
||||||
decay_rate: 0.5,
|
var_const: 0.00000001,
|
||||||
adj_decay_rate: 0.5,
|
|
||||||
period: (3600 * 24 * 30) as f64,
|
|
||||||
tau: 0.2,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -787,141 +780,4 @@ CREATE TABLE IF NOT EXISTS sets (
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sqlite_sanity_check() -> sqlite::Result<()> {
|
|
||||||
let test_value: i64 = 2;
|
|
||||||
|
|
||||||
let connection = sqlite::open(":memory:")?;
|
|
||||||
connection.execute(
|
|
||||||
r#"CREATE TABLE test (a INTEGER);
|
|
||||||
INSERT INTO test VALUES (1);
|
|
||||||
INSERT INTO test VALUES (2)"#,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut statement = connection.prepare("SELECT * FROM test WHERE a = ?")?;
|
|
||||||
statement.bind((1, test_value))?;
|
|
||||||
statement.next()?;
|
|
||||||
assert_eq!(statement.read::<i64, _>("a")?, test_value);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_players() -> sqlite::Result<()> {
|
|
||||||
let connection = mock_datasets()?;
|
|
||||||
new_dataset(&connection, "test", metadata())?;
|
|
||||||
|
|
||||||
add_players(&connection, "test", &vec![players(2)], Timestamp(0))?;
|
|
||||||
|
|
||||||
let mut statement = connection.prepare("SELECT * FROM players WHERE id = 1")?;
|
|
||||||
statement.next()?;
|
|
||||||
assert_eq!(statement.read::<i64, _>("id")?, 1);
|
|
||||||
assert_eq!(statement.read::<String, _>("name")?, "1");
|
|
||||||
assert_eq!(statement.read::<Option<String>, _>("prefix")?, None);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn edge_insert_get() -> sqlite::Result<()> {
|
|
||||||
let connection = mock_datasets()?;
|
|
||||||
new_dataset(&connection, "test", metadata())?;
|
|
||||||
add_players(&connection, "test", &vec![players(2)], Timestamp(0))?;
|
|
||||||
|
|
||||||
insert_advantage(&connection, "test", PlayerId(2), PlayerId(1), 1.0)?;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
get_advantage(&connection, "test", PlayerId(1), PlayerId(2))?,
|
|
||||||
Some(-1.0)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
get_advantage(&connection, "test", PlayerId(2), PlayerId(1))?,
|
|
||||||
Some(1.0)
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn player_all_edges() -> sqlite::Result<()> {
|
|
||||||
let connection = mock_datasets()?;
|
|
||||||
new_dataset(&connection, "test", metadata())?;
|
|
||||||
add_players(&connection, "test", &vec![players(3)], Timestamp(0))?;
|
|
||||||
|
|
||||||
insert_advantage(&connection, "test", PlayerId(2), PlayerId(1), 1.0)?;
|
|
||||||
insert_advantage(&connection, "test", PlayerId(1), PlayerId(3), 5.0)?;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
get_edges(&connection, "test", PlayerId(1))?,
|
|
||||||
[(PlayerId(2), -1.0, 0), (PlayerId(3), 5.0, 0)]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
get_edges(&connection, "test", PlayerId(2))?,
|
|
||||||
[(PlayerId(1), 1.0, 0)]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
get_edges(&connection, "test", PlayerId(3))?,
|
|
||||||
[(PlayerId(1), -5.0, 0)]
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn hypoth_adv_trivial() -> sqlite::Result<()> {
|
|
||||||
let num_players = 3;
|
|
||||||
|
|
||||||
let connection = mock_datasets()?;
|
|
||||||
new_dataset(&connection, "test", metadata())?;
|
|
||||||
add_players(
|
|
||||||
&connection,
|
|
||||||
"test",
|
|
||||||
&vec![players(num_players)],
|
|
||||||
Timestamp(0),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let metadata = metadata();
|
|
||||||
for i in 1..=num_players {
|
|
||||||
for j in 1..=num_players {
|
|
||||||
assert_eq!(
|
|
||||||
hypothetical_advantage(
|
|
||||||
&connection,
|
|
||||||
"test",
|
|
||||||
PlayerId(i),
|
|
||||||
PlayerId(j),
|
|
||||||
metadata.set_limit,
|
|
||||||
metadata.decay_rate,
|
|
||||||
metadata.adj_decay_rate
|
|
||||||
)?,
|
|
||||||
0.0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn hypoth_adv1() -> sqlite::Result<()> {
|
|
||||||
let connection = mock_datasets()?;
|
|
||||||
new_dataset(&connection, "test", metadata())?;
|
|
||||||
add_players(&connection, "test", &vec![players(2)], Timestamp(0))?;
|
|
||||||
|
|
||||||
insert_advantage(&connection, "test", PlayerId(1), PlayerId(2), 1.0)?;
|
|
||||||
|
|
||||||
let metadata = metadata();
|
|
||||||
assert_eq!(
|
|
||||||
hypothetical_advantage(
|
|
||||||
&connection,
|
|
||||||
"test",
|
|
||||||
PlayerId(1),
|
|
||||||
PlayerId(2),
|
|
||||||
metadata.set_limit,
|
|
||||||
metadata.decay_rate,
|
|
||||||
metadata.adj_decay_rate
|
|
||||||
)?,
|
|
||||||
1.0
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
285
src/main.rs
285
src/main.rs
|
@ -81,6 +81,12 @@ created if it does not already exist."
|
||||||
#[arg(short, long, global = true, help = "The dataset to access")]
|
#[arg(short, long, global = true, help = "The dataset to access")]
|
||||||
dataset: Option<String>,
|
dataset: Option<String>,
|
||||||
},
|
},
|
||||||
|
Ranking {
|
||||||
|
#[command(subcommand)]
|
||||||
|
subcommand: RankingSC,
|
||||||
|
#[arg(short, long, global = true, help = "The dataset to access")]
|
||||||
|
dataset: Option<String>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
|
@ -106,13 +112,20 @@ enum PlayerSC {
|
||||||
Matchup { player1: String, player2: String },
|
Matchup { player1: String, player2: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum RankingSC {
|
||||||
|
#[command(about = "Create a new ranking")]
|
||||||
|
Create,
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
let config_dir = cli
|
let config_dir = cli.config_dir.unwrap_or_else(|| {
|
||||||
.config_dir
|
let mut dir = dirs::config_dir().expect("Could not determine config directory");
|
||||||
.map(|mut s| { s.push("startrnr"); s })
|
dir.push("startrnr");
|
||||||
.unwrap_or_else(|| dirs::config_dir().expect("Could not determine config directory"));
|
dir
|
||||||
|
});
|
||||||
|
|
||||||
let mut data_dir = dirs::data_dir().expect("Could not determine user data directory");
|
let mut data_dir = dirs::data_dir().expect("Could not determine user data directory");
|
||||||
data_dir.push("startrnr");
|
data_dir.push("startrnr");
|
||||||
|
@ -224,22 +237,10 @@ fn dataset_list(connection: &Connection) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!();
|
|
||||||
|
|
||||||
if metadata.set_limit != 0 && metadata.decay_rate != metadata.adj_decay_rate {
|
|
||||||
println!("\x1b[1mSet Limit:\x1b[0m {}", metadata.set_limit);
|
|
||||||
println!(
|
println!(
|
||||||
"\x1b[1mNetwork Decay Rate:\x1b[0m {} (adjusted {})",
|
"\n\x1b[1mNetwork Decay Constant:\x1b[0m {}",
|
||||||
metadata.decay_rate, metadata.adj_decay_rate
|
metadata.decay_const
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
println!("\x1b[1mNetwork Decay Rate:\x1b[0m {}", metadata.decay_rate);
|
|
||||||
}
|
|
||||||
println!(
|
|
||||||
"\x1b[1mRating Period:\x1b[0m {} days",
|
|
||||||
metadata.period / SECS_IN_DAY as f64
|
|
||||||
);
|
|
||||||
println!("\x1b[1mTau Constant:\x1b[0m {}\n", metadata.tau);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -399,33 +400,11 @@ End date (year, m/y, or m/d/y): "
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set Limit
|
|
||||||
|
|
||||||
let mut set_limit = 0;
|
|
||||||
print!(
|
|
||||||
"
|
|
||||||
\x1b[1mSet Limit\x1b[0m
|
|
||||||
The set limit is an optional feature of the rating system that defines how many
|
|
||||||
sets must be played between two players for their rating data to be considered
|
|
||||||
trustworthy.
|
|
||||||
This value should be set low, i.e. not more than 5 or 6.
|
|
||||||
|
|
||||||
Set limit (default 0): "
|
|
||||||
);
|
|
||||||
let set_limit_input = read_string();
|
|
||||||
if !set_limit_input.is_empty() {
|
|
||||||
set_limit = set_limit_input
|
|
||||||
.parse::<u64>()
|
|
||||||
.unwrap_or_else(|_| error("Input is not an integer", 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advanced Options
|
// Advanced Options
|
||||||
|
|
||||||
// Defaults
|
// Defaults
|
||||||
let mut decay_rate = 0.8;
|
let mut decay_const = 0.9;
|
||||||
let mut adj_decay_rate = 0.6;
|
let mut var_const = (10.0 - 0.04) / SECS_IN_YEAR as f64 / 3.0;
|
||||||
let mut period_days = 40.0;
|
|
||||||
let mut tau = 0.4;
|
|
||||||
|
|
||||||
print!("\nConfigure advanced options? (y/n) ");
|
print!("\nConfigure advanced options? (y/n) ");
|
||||||
if let Some('y') = read_string().chars().next() {
|
if let Some('y') = read_string().chars().next() {
|
||||||
|
@ -433,87 +412,42 @@ Set limit (default 0): "
|
||||||
|
|
||||||
print!(
|
print!(
|
||||||
"
|
"
|
||||||
\x1b[1mNetwork Decay Rate\x1b[0m
|
\x1b[1mNetwork Decay Constant\x1b[0m
|
||||||
The network decay rate is a number between 0 and 1 that controls how the
|
The network decay constant is a number between 0 and 1 that controls how
|
||||||
advantage network reacts to player wins and losses. If the decay rate is 1,
|
player wins and losses propagate throughout the network. If the decay
|
||||||
then it is assumed that a player's skill against one opponent always carries
|
constant is 1, then it is assumed that a player's skill against one
|
||||||
over to all other opponents. If the decay rate is 0, then all player match-ups
|
opponent always carries over to all other opponents. If the decay
|
||||||
are assumed to be independent of each other.
|
constant is 0, then all player match-ups are assumed to be independent of
|
||||||
|
each other.
|
||||||
|
|
||||||
Network decay rate (default 0.8): "
|
Network decay constant (default 0.9): "
|
||||||
);
|
);
|
||||||
let decay_rate_input = read_string();
|
let decay_const_input = read_string();
|
||||||
if !decay_rate_input.is_empty() {
|
if !decay_const_input.is_empty() {
|
||||||
decay_rate = decay_rate_input
|
decay_const = decay_const_input
|
||||||
.parse::<f64>()
|
.parse::<f64>()
|
||||||
.unwrap_or_else(|_| error("Input is not a number", 1));
|
.unwrap_or_else(|_| error("Input is not a number", 1));
|
||||||
if decay_rate < 0.0 || decay_rate > 1.0 {
|
if decay_const < 0.0 || decay_const > 1.0 {
|
||||||
error("Input is not between 0 and 1", 1);
|
error("Input is not between 0 and 1", 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjusted Decay Rate
|
// Variance Constant
|
||||||
|
|
||||||
if set_limit != 0 {
|
|
||||||
print!(
|
|
||||||
"
|
|
||||||
\x1b[1mAdjusted Network Decay Rate\x1b[0m
|
|
||||||
If the number of sets played between two players is less than the set limit,
|
|
||||||
then this value is used instead of the regular network decay rate.
|
|
||||||
This value should be \x1b[1mlower\x1b[0m than the network decay rate.
|
|
||||||
|
|
||||||
Adjusted network decay rate (default 0.6): "
|
|
||||||
);
|
|
||||||
let adj_decay_rate_input = read_string();
|
|
||||||
if !adj_decay_rate_input.is_empty() {
|
|
||||||
adj_decay_rate = adj_decay_rate_input
|
|
||||||
.parse::<f64>()
|
|
||||||
.unwrap_or_else(|_| error("Input is not a number", 1));
|
|
||||||
if decay_rate < 0.0 || decay_rate > 1.0 {
|
|
||||||
error("Input is not between 0 and 1", 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rating Period
|
|
||||||
|
|
||||||
print!(
|
print!(
|
||||||
"
|
"
|
||||||
\x1b[1mRating Period\x1b[0m
|
\x1b[1mVariance Rate\x1b[0m
|
||||||
The rating period is an interval of time that dictates how player ratings change
|
This constant determines how quickly a player's variance (the uncertainty
|
||||||
during inactivity. Ideally the rating period should be somewhat long, long
|
of their rating) increases over time. See the end of \x1b[4m\x1b]8;;http:\
|
||||||
enough to expect almost every player in the dataset to have played at least a
|
//www.glicko.net/glicko/glicko.pdf\x1b\\this paper\x1b]8;;\x1b\\\x1b[0m for details
|
||||||
few sets.
|
on how to compute a good value, or you can leave it blank and a reasonable
|
||||||
|
default will be chosen.
|
||||||
|
|
||||||
Rating period (in days, default 40): "
|
Variance rate: "
|
||||||
);
|
);
|
||||||
let period_input = read_string();
|
let var_const_input = read_string();
|
||||||
if !period_input.is_empty() {
|
if !var_const_input.is_empty() {
|
||||||
period_days = period_input
|
var_const = var_const_input
|
||||||
.parse::<f64>()
|
|
||||||
.unwrap_or_else(|_| error("Input is not a number", 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tau coefficient
|
|
||||||
|
|
||||||
print!(
|
|
||||||
"
|
|
||||||
\x1b[1mTau Constant\x1b[0m
|
|
||||||
The tau constant is an internal system constant that roughly represents how
|
|
||||||
much random chance and luck play a role in game outcomes. In games where match
|
|
||||||
results are highly predictable, and a player's skill is the sole factor for
|
|
||||||
whether they will win, the tau constant should be high (0.9 - 1.2). In games
|
|
||||||
where luck matters, and more improbable victories can occur, the tau constant
|
|
||||||
should be low (0.2 - 0.4).
|
|
||||||
|
|
||||||
The tau constant is set low by default, since skill-based competitive video
|
|
||||||
games tend to be on the more luck-heavy side.
|
|
||||||
|
|
||||||
Tau constant (default 0.4): "
|
|
||||||
);
|
|
||||||
let tau_input = read_string();
|
|
||||||
if !tau_input.is_empty() {
|
|
||||||
tau = tau_input
|
|
||||||
.parse::<f64>()
|
.parse::<f64>()
|
||||||
.unwrap_or_else(|_| error("Input is not a number", 1));
|
.unwrap_or_else(|_| error("Input is not a number", 1));
|
||||||
}
|
}
|
||||||
|
@ -533,11 +467,8 @@ Tau constant (default 0.4): "
|
||||||
game_slug,
|
game_slug,
|
||||||
country,
|
country,
|
||||||
state,
|
state,
|
||||||
set_limit,
|
decay_const,
|
||||||
decay_rate,
|
var_const,
|
||||||
adj_decay_rate,
|
|
||||||
period: SECS_IN_DAY as f64 * period_days,
|
|
||||||
tau,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.expect("Error communicating with SQLite");
|
.expect("Error communicating with SQLite");
|
||||||
|
@ -591,9 +522,6 @@ fn player_info(connection: &Connection, dataset: Option<String>, player: String)
|
||||||
} = get_player_from_input(connection, player)
|
} = get_player_from_input(connection, player)
|
||||||
.unwrap_or_else(|_| error("Could not find player", 1));
|
.unwrap_or_else(|_| error("Could not find player", 1));
|
||||||
|
|
||||||
let (deviation, volatility, _) = get_player_rating_data(connection, &dataset, id)
|
|
||||||
.unwrap_or_else(|_| error("Could not find player", 1));
|
|
||||||
|
|
||||||
let (won, lost) = get_player_set_counts(connection, &dataset, id)
|
let (won, lost) = get_player_set_counts(connection, &dataset, id)
|
||||||
.unwrap_or_else(|_| error("Could not find player", 1));
|
.unwrap_or_else(|_| error("Could not find player", 1));
|
||||||
|
|
||||||
|
@ -613,9 +541,6 @@ fn player_info(connection: &Connection, dataset: Option<String>, player: String)
|
||||||
lost,
|
lost,
|
||||||
(won as f64 / (won + lost) as f64) * 100.0
|
(won as f64 / (won + lost) as f64) * 100.0
|
||||||
);
|
);
|
||||||
|
|
||||||
println!("\n\x1b[1mDeviation:\x1b[0m {}", deviation);
|
|
||||||
println!("\x1b[1mVolatility:\x1b[0m {}", volatility);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn player_matchup(
|
fn player_matchup(
|
||||||
|
@ -634,9 +559,6 @@ fn player_matchup(
|
||||||
} = get_player_from_input(connection, player1)
|
} = get_player_from_input(connection, player1)
|
||||||
.unwrap_or_else(|_| error("Could not find player", 1));
|
.unwrap_or_else(|_| error("Could not find player", 1));
|
||||||
|
|
||||||
let (deviation1, _, _) = get_player_rating_data(connection, &dataset, player1)
|
|
||||||
.unwrap_or_else(|_| error("Could not find player", 1));
|
|
||||||
|
|
||||||
let PlayerData {
|
let PlayerData {
|
||||||
id: player2,
|
id: player2,
|
||||||
name: name2,
|
name: name2,
|
||||||
|
@ -645,42 +567,34 @@ fn player_matchup(
|
||||||
} = get_player_from_input(connection, player2)
|
} = get_player_from_input(connection, player2)
|
||||||
.unwrap_or_else(|_| error("Could not find player", 1));
|
.unwrap_or_else(|_| error("Could not find player", 1));
|
||||||
|
|
||||||
let (deviation2, _, _) = get_player_rating_data(connection, &dataset, player2)
|
let (hypothetical, advantage, variance) =
|
||||||
.unwrap_or_else(|_| error("Could not find player", 1));
|
get_network_data(connection, &dataset, player1, player2)
|
||||||
|
|
||||||
let (hypothetical, advantage) = get_advantage(connection, &dataset, player1, player2)
|
|
||||||
.expect("Error communicating with SQLite")
|
.expect("Error communicating with SQLite")
|
||||||
.map(|x| (false, x))
|
.map(|(adv, var)| (false, adv, var))
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
let metadata = get_metadata(connection, &dataset)
|
let metadata = get_metadata(connection, &dataset)
|
||||||
.expect("Error communicating with SQLite")
|
.expect("Error communicating with SQLite")
|
||||||
.unwrap_or_else(|| error("Dataset not found", 1));
|
.unwrap_or_else(|| error("Dataset not found", 1));
|
||||||
(
|
let (adv, var) = hypothetical_advantage(
|
||||||
true,
|
|
||||||
hypothetical_advantage(
|
|
||||||
connection,
|
connection,
|
||||||
&dataset,
|
&dataset,
|
||||||
player1,
|
player1,
|
||||||
player2,
|
player2,
|
||||||
metadata.set_limit,
|
metadata.decay_const,
|
||||||
metadata.decay_rate,
|
|
||||||
metadata.adj_decay_rate,
|
|
||||||
)
|
|
||||||
.expect("Error communicating with SQLite"),
|
|
||||||
)
|
)
|
||||||
|
.expect("Error communicating with SQLite");
|
||||||
|
(true, adv, var)
|
||||||
});
|
});
|
||||||
|
|
||||||
let probability = 1.0
|
let probability = 1.0 / (1.0 + f64::exp(-advantage));
|
||||||
/ (1.0
|
|
||||||
+ f64::exp(
|
|
||||||
g_func((deviation1 * deviation1 + deviation2 * deviation2).sqrt()) * advantage,
|
|
||||||
));
|
|
||||||
|
|
||||||
let (color, other_color) = ansi_num_color(advantage, 0.2, 2.0);
|
let (color, other_color) = ansi_num_color(advantage, 0.2, 2.0);
|
||||||
|
|
||||||
let len1 = prefix1.as_deref().map(|s| s.len() + 1).unwrap_or(0) + name1.len();
|
let len1 = prefix1.as_deref().map(|s| s.len() + 1).unwrap_or(0) + name1.len();
|
||||||
let len2 = prefix2.as_deref().map(|s| s.len() + 1).unwrap_or(0) + name2.len();
|
let len2 = prefix2.as_deref().map(|s| s.len() + 1).unwrap_or(0) + name2.len();
|
||||||
|
|
||||||
|
// Prefix + name for each player
|
||||||
|
|
||||||
if let Some(pre) = prefix1 {
|
if let Some(pre) = prefix1 {
|
||||||
print!("\x1b[2m{}\x1b[22m ", pre);
|
print!("\x1b[2m{}\x1b[22m ", pre);
|
||||||
}
|
}
|
||||||
|
@ -698,27 +612,31 @@ fn player_matchup(
|
||||||
discrim2, name2
|
discrim2, name2
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Probability breakdown
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"\x1b[1m\x1b[{4}m{0:>2$}\x1b[0m - \x1b[1m\x1b[{5}m{1:<3$}\x1b[0m",
|
"\x1b[1m\x1b[{4}m{0:>2$}\x1b[0m - \x1b[1m\x1b[{5}m{1:<3$}\x1b[0m",
|
||||||
format!("{:.1}%", probability * 100.0),
|
|
||||||
format!("{:.1}%", (1.0 - probability) * 100.0),
|
format!("{:.1}%", (1.0 - probability) * 100.0),
|
||||||
|
format!("{:.1}%", probability * 100.0),
|
||||||
len1,
|
len1,
|
||||||
len2,
|
len2,
|
||||||
other_color,
|
other_color,
|
||||||
color
|
color
|
||||||
);
|
);
|
||||||
|
|
||||||
if hypothetical {
|
// Advantage + variance
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"\n\x1b[1mHypothetical Advantage: \x1b[{1}m{0:+.4}\x1b[0m",
|
"\n\x1b[1m{0}Advantage: \x1b[{1}m{2:+.4}\x1b[39m\n{0}Variance: {3:.4}\x1b[0m",
|
||||||
advantage, color
|
if hypothetical { "Hypothetical " } else { "" },
|
||||||
);
|
color,
|
||||||
} else {
|
advantage,
|
||||||
println!(
|
variance
|
||||||
"\n\x1b[1mAdvantage: \x1b[{1}m{0:+.4}\x1b[0m",
|
|
||||||
advantage, color
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if !hypothetical {
|
||||||
|
// Set count
|
||||||
|
|
||||||
let (a, b) = get_matchup_set_counts(connection, &dataset, player1, player2)
|
let (a, b) = get_matchup_set_counts(connection, &dataset, player1, player2)
|
||||||
.expect("Error communicating with SQLite");
|
.expect("Error communicating with SQLite");
|
||||||
|
|
||||||
|
@ -777,5 +695,70 @@ fn sync(connection: &Connection, auth: String, datasets: Vec<String>, all: bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ranking_create(connection: &Connection, dataset: Option<String>) {
|
fn ranking_create(connection: &Connection, dataset: Option<String>) {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
let dataset = dataset.unwrap_or_else(|| String::from("default"));
|
let dataset = dataset.unwrap_or_else(|| String::from("default"));
|
||||||
|
|
||||||
|
let metadata = get_metadata(connection, &dataset)
|
||||||
|
.expect("Error communicating with SQLite")
|
||||||
|
.unwrap_or_else(|| error("Dataset not found", 1));
|
||||||
|
|
||||||
|
let exp = read_string().parse::<f64>().unwrap();
|
||||||
|
|
||||||
|
let players = get_all_players(connection, &dataset).expect("Error communicating with SQLite");
|
||||||
|
let num_players = players.len();
|
||||||
|
let mut table = players
|
||||||
|
.into_iter()
|
||||||
|
.map(|id| (id, 1.0 / num_players as f64))
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
table.shrink_to_fit();
|
||||||
|
|
||||||
|
let mut diff: f64 = 1.0;
|
||||||
|
let mut iter = 0;
|
||||||
|
|
||||||
|
while diff > 1e-8 {
|
||||||
|
let mut new_table = HashMap::with_capacity(table.capacity());
|
||||||
|
|
||||||
|
for (&id, &last) in table.iter() {
|
||||||
|
let mut points = get_edges(connection, &dataset, id)
|
||||||
|
.expect("Error communicating with SQLite")
|
||||||
|
.into_iter()
|
||||||
|
.map(|(other, adv, _sets)| (other, exp.powf(adv)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
points.push((id, 1.0));
|
||||||
|
|
||||||
|
let sum_points = points.iter().map(|(_, val)| val).sum::<f64>();
|
||||||
|
|
||||||
|
points.into_iter().for_each(|(other, pts)| {
|
||||||
|
let pts_ = last * pts / sum_points;
|
||||||
|
new_table
|
||||||
|
.entry(other)
|
||||||
|
.and_modify(|v| *v += pts_)
|
||||||
|
.or_insert(pts_);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if iter % 10 == 0 {
|
||||||
|
diff = (table
|
||||||
|
.iter()
|
||||||
|
.map(|(id, &last)| (new_table[id] - last) * (new_table[id] - last))
|
||||||
|
.sum::<f64>()
|
||||||
|
/ num_players as f64)
|
||||||
|
.sqrt();
|
||||||
|
println!("{}", diff);
|
||||||
|
}
|
||||||
|
|
||||||
|
table = new_table;
|
||||||
|
iter += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut list = table.into_iter().collect::<Vec<_>>();
|
||||||
|
list.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap());
|
||||||
|
|
||||||
|
println!();
|
||||||
|
for (id, pts) in list.into_iter().take(20) {
|
||||||
|
let player = get_player(connection, id).unwrap();
|
||||||
|
println!("{} - {}", player.name, pts);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
218
src/sync.rs
218
src/sync.rs
|
@ -1,4 +1,3 @@
|
||||||
use std::f64::consts::PI;
|
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
@ -8,86 +7,6 @@ use crate::queries::*;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use sqlite::*;
|
use sqlite::*;
|
||||||
|
|
||||||
// Glicko-2 system calculation
|
|
||||||
|
|
||||||
pub fn g_func(dev: f64) -> f64 {
|
|
||||||
1.0 / (1.0 + 3.0 * dev * dev / PI / PI).sqrt()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn time_adjust(periods: f64, old_dev_sq: f64, volatility: f64) -> f64 {
|
|
||||||
(old_dev_sq + periods * volatility * volatility).sqrt()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn illinois_optimize(fun: impl Fn(f64) -> f64, mut a: f64, mut b: f64) -> f64 {
|
|
||||||
let mut f_a = fun(a);
|
|
||||||
let mut f_b = fun(b);
|
|
||||||
|
|
||||||
while (b - a).abs() > 1e-6 {
|
|
||||||
let c = a + (a - b) * f_a / (f_b - f_a);
|
|
||||||
let f_c = fun(c);
|
|
||||||
if f_c * f_b > 0.0 {
|
|
||||||
f_a = f_a / 2.0;
|
|
||||||
} else {
|
|
||||||
a = b;
|
|
||||||
f_a = f_b;
|
|
||||||
}
|
|
||||||
b = c;
|
|
||||||
f_b = f_c;
|
|
||||||
}
|
|
||||||
a
|
|
||||||
}
|
|
||||||
|
|
||||||
fn glicko_adjust(
|
|
||||||
advantage: f64,
|
|
||||||
deviation: f64,
|
|
||||||
volatility: f64,
|
|
||||||
other_deviation: f64,
|
|
||||||
won: bool,
|
|
||||||
time: u64,
|
|
||||||
metadata: &DatasetMetadata,
|
|
||||||
) -> (f64, f64, f64) {
|
|
||||||
let period = metadata.period;
|
|
||||||
let tau = metadata.tau;
|
|
||||||
|
|
||||||
let g_val = g_func(other_deviation);
|
|
||||||
let exp_val = 1.0 / (1.0 + f64::exp(-g_val * advantage));
|
|
||||||
|
|
||||||
let variance = 1.0 / (g_val * g_val * exp_val * (1.0 - exp_val));
|
|
||||||
|
|
||||||
let score = if won { 1.0 } else { 0.0 };
|
|
||||||
let delta = variance * g_val * (score - exp_val);
|
|
||||||
|
|
||||||
let delta_sq = delta * delta;
|
|
||||||
let dev_sq = deviation * deviation;
|
|
||||||
let a = (volatility * volatility).ln();
|
|
||||||
let vol_fn = |x| {
|
|
||||||
let ex = f64::exp(x);
|
|
||||||
let subf = dev_sq + variance + ex;
|
|
||||||
((ex * (delta_sq - dev_sq - variance - ex)) / 2.0 / subf / subf) - (x - a) / tau / tau
|
|
||||||
};
|
|
||||||
|
|
||||||
let initial_b = if delta_sq > dev_sq + variance {
|
|
||||||
(delta_sq - dev_sq - variance).ln()
|
|
||||||
} else {
|
|
||||||
(1..)
|
|
||||||
.map(|k| vol_fn(a - k as f64 * tau))
|
|
||||||
.inspect(|x| {
|
|
||||||
if x.is_nan() {
|
|
||||||
panic!();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.find(|x| x >= &0.0)
|
|
||||||
.unwrap()
|
|
||||||
};
|
|
||||||
let vol_new = f64::exp(illinois_optimize(vol_fn, a, initial_b) / 2.0);
|
|
||||||
|
|
||||||
let dev_time = time_adjust(time as f64 / period, dev_sq, vol_new);
|
|
||||||
let dev_new = 1.0 / (1.0 / dev_time / dev_time + 1.0 / variance).sqrt();
|
|
||||||
let adjust = dev_new * dev_new * g_val * (score - exp_val);
|
|
||||||
|
|
||||||
(adjust, dev_new, vol_new)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract set data
|
// Extract set data
|
||||||
|
|
||||||
fn get_event_sets(event: EventId, auth: &str) -> Option<Vec<SetData>> {
|
fn get_event_sets(event: EventId, auth: &str) -> Option<Vec<SetData>> {
|
||||||
|
@ -200,104 +119,67 @@ fn update_from_set(
|
||||||
event_time: Timestamp,
|
event_time: Timestamp,
|
||||||
results: SetData,
|
results: SetData,
|
||||||
) -> sqlite::Result<()> {
|
) -> sqlite::Result<()> {
|
||||||
let players_data = results.teams;
|
let teams = results.teams;
|
||||||
// Fall back to event time if set time is not recorded
|
|
||||||
let time = results.time.unwrap_or(event_time);
|
|
||||||
add_players(connection, dataset, &players_data, time)?;
|
|
||||||
|
|
||||||
// Non-singles matches are currently not supported
|
// Non-singles matches are currently not supported
|
||||||
if players_data.len() != 2 || players_data[0].len() != 1 || players_data[1].len() != 1 {
|
if teams.len() != 2 || teams[0].len() != 1 || teams[1].len() != 1 {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut it = players_data.into_iter();
|
let players = teams.into_iter().flatten().collect::<Vec<_>>();
|
||||||
let player1 = it.next().unwrap()[0].id;
|
add_players(connection, dataset, &players)?;
|
||||||
let player2 = it.next().unwrap()[0].id;
|
|
||||||
drop(it);
|
|
||||||
|
|
||||||
let (deviation1, volatility1, last_played1) =
|
let player1 = players[0].id;
|
||||||
get_player_rating_data(connection, dataset, player1)?;
|
let player2 = players[1].id;
|
||||||
let time1 = time.0.checked_sub(last_played1.0).unwrap_or(0);
|
|
||||||
|
|
||||||
let (deviation2, volatility2, last_played2) =
|
// Time-adjust all variances associated with each player
|
||||||
get_player_rating_data(connection, dataset, player1)?;
|
let time = results.time.unwrap_or(event_time);
|
||||||
let time2 = time.0.checked_sub(last_played2.0).unwrap_or(0);
|
adjust_for_time(connection, dataset, player1, metadata.var_const, time)?;
|
||||||
|
adjust_for_time(connection, dataset, player2, metadata.var_const, time)?;
|
||||||
|
|
||||||
let advantage = match get_advantage(connection, dataset, player1, player2) {
|
let (advantage, variance) = match get_network_data(connection, dataset, player1, player2) {
|
||||||
Err(e) => Err(e)?,
|
Err(e) => Err(e)?,
|
||||||
Ok(None) => initialize_edge(
|
Ok(None) => initialize_edge(
|
||||||
connection,
|
connection,
|
||||||
dataset,
|
dataset,
|
||||||
player1,
|
player1,
|
||||||
player2,
|
player2,
|
||||||
metadata.set_limit,
|
metadata.decay_const,
|
||||||
metadata.decay_rate,
|
time,
|
||||||
metadata.adj_decay_rate,
|
|
||||||
)?,
|
)?,
|
||||||
Ok(Some(adv)) => adv,
|
Ok(Some(adv)) => adv,
|
||||||
};
|
};
|
||||||
let (adjust1, dev_new1, vol_new1) = glicko_adjust(
|
|
||||||
-advantage,
|
// println!("{}, {} - {}, {}", player1.0, player2.0, advantage, variance);
|
||||||
deviation1,
|
|
||||||
volatility1,
|
glicko_adjust(
|
||||||
deviation2,
|
connection,
|
||||||
results.winner == 0,
|
dataset,
|
||||||
time1,
|
&results.id,
|
||||||
metadata,
|
player1,
|
||||||
);
|
player2,
|
||||||
let (adjust2, dev_new2, vol_new2) = glicko_adjust(
|
|
||||||
advantage,
|
advantage,
|
||||||
deviation2,
|
variance,
|
||||||
volatility2,
|
results.winner,
|
||||||
deviation1,
|
metadata.decay_const,
|
||||||
results.winner == 1,
|
)?;
|
||||||
time2,
|
|
||||||
metadata,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set minimum deviation level
|
set_player_set_counts(
|
||||||
let dev_new1 = f64::max(dev_new1, 0.2);
|
|
||||||
let dev_new2 = f64::max(dev_new2, 0.2);
|
|
||||||
|
|
||||||
set_player_data(
|
|
||||||
connection,
|
connection,
|
||||||
dataset,
|
dataset,
|
||||||
player1,
|
player1,
|
||||||
time,
|
|
||||||
dev_new1,
|
|
||||||
vol_new1,
|
|
||||||
results.winner == 0,
|
results.winner == 0,
|
||||||
&results.id,
|
&results.id,
|
||||||
)?;
|
)?;
|
||||||
set_player_data(
|
set_player_set_counts(
|
||||||
connection,
|
connection,
|
||||||
dataset,
|
dataset,
|
||||||
player2,
|
player2,
|
||||||
time,
|
|
||||||
dev_new2,
|
|
||||||
vol_new2,
|
|
||||||
results.winner == 1,
|
results.winner == 1,
|
||||||
&results.id,
|
&results.id,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let (sets1, sets2) = get_matchup_set_counts(connection, dataset, player1, player2)?;
|
Ok(())
|
||||||
let decay_rate = if sets1 + sets2 >= metadata.set_limit {
|
|
||||||
metadata.decay_rate
|
|
||||||
} else {
|
|
||||||
metadata.adj_decay_rate
|
|
||||||
};
|
|
||||||
|
|
||||||
adjust_advantages(
|
|
||||||
connection,
|
|
||||||
dataset,
|
|
||||||
results.id,
|
|
||||||
player1,
|
|
||||||
player2,
|
|
||||||
results.winner,
|
|
||||||
adjust1,
|
|
||||||
adjust2,
|
|
||||||
decay_rate,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sync_dataset(
|
pub fn sync_dataset(
|
||||||
|
@ -340,45 +222,3 @@ pub fn sync_dataset(
|
||||||
}
|
}
|
||||||
connection.execute("COMMIT;")
|
connection.execute("COMMIT;")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::database::tests::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn glicko_single() -> sqlite::Result<()> {
|
|
||||||
let connection = mock_datasets()?;
|
|
||||||
new_dataset(&connection, "test", metadata())?;
|
|
||||||
let players = players(2).into_iter().map(|x| vec![x]).collect();
|
|
||||||
add_players(&connection, "test", &players, Timestamp(0))?;
|
|
||||||
|
|
||||||
update_from_set(
|
|
||||||
&connection,
|
|
||||||
"test",
|
|
||||||
&metadata(),
|
|
||||||
Timestamp(0),
|
|
||||||
SetData {
|
|
||||||
id: SetId(StringOrInt::Int(0)),
|
|
||||||
time: None,
|
|
||||||
teams: players,
|
|
||||||
winner: 0,
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"{:?}",
|
|
||||||
get_advantage(&connection, "test", PlayerId(1), PlayerId(2))?.unwrap()
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
"{:?}",
|
|
||||||
get_player_rating_data(&connection, "test", PlayerId(1))
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
"{:?}",
|
|
||||||
get_player_rating_data(&connection, "test", PlayerId(2))
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ use crate::queries::{PlayerData, PlayerId, Timestamp};
|
||||||
pub const SECS_IN_HR: u64 = 3600;
|
pub const SECS_IN_HR: u64 = 3600;
|
||||||
pub const SECS_IN_DAY: u64 = SECS_IN_HR * 24;
|
pub const SECS_IN_DAY: u64 = SECS_IN_HR * 24;
|
||||||
pub const SECS_IN_WEEK: u64 = SECS_IN_DAY * 7;
|
pub const SECS_IN_WEEK: u64 = SECS_IN_DAY * 7;
|
||||||
|
pub const SECS_IN_YEAR: u64 = SECS_IN_DAY * 365 + SECS_IN_HR * 6;
|
||||||
|
|
||||||
pub fn error(msg: &str, code: i32) -> ! {
|
pub fn error(msg: &str, code: i32) -> ! {
|
||||||
eprintln!("\nERROR: {}", msg);
|
eprintln!("\nERROR: {}", msg);
|
||||||
|
|
Loading…
Reference in a new issue