refactor: separate command subroutines into submodules
This commit is contained in:
parent
e644f96648
commit
9cd8d77753
src
3
src/commands.rs
Normal file
3
src/commands.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod dataset;
|
||||
pub mod player;
|
||||
pub mod sync;
|
342
src/commands/dataset.rs
Normal file
342
src/commands/dataset.rs
Normal file
|
@ -0,0 +1,342 @@
|
|||
use crate::database::*;
|
||||
use crate::queries::*;
|
||||
use crate::util::*;
|
||||
use chrono::{Local, TimeZone, Utc};
|
||||
use sqlite::*;
|
||||
|
||||
pub fn list(connection: &Connection) {
|
||||
let datasets = list_datasets(&connection).expect("Error communicating with SQLite");
|
||||
|
||||
for (name, metadata) in datasets {
|
||||
print!(
|
||||
"· \x1b[1m\x1b[34m{}\x1b[0m
|
||||
\x1b[4m\x1b]8;;https://www.start.gg/{}\x1b\\{}\x1b]8;;\x1b\\\x1b[0m ",
|
||||
name, metadata.game_slug, metadata.game_name
|
||||
);
|
||||
|
||||
if let Some(country) = metadata.country {
|
||||
if let Some(state) = metadata.state {
|
||||
println!("(in {}, {})", country, state);
|
||||
} else {
|
||||
println!("(in {})", country);
|
||||
}
|
||||
} else {
|
||||
println!("(Global)");
|
||||
}
|
||||
|
||||
let start = if metadata.start.0 != 1 {
|
||||
Some(
|
||||
Utc.timestamp_opt(metadata.start.0 as i64, 0)
|
||||
.unwrap()
|
||||
.format("%m/%d/%Y"),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let end = metadata
|
||||
.end
|
||||
.map(|x| Utc.timestamp_opt(x.0 as i64, 0).unwrap().format("%m/%d/%Y"));
|
||||
|
||||
match (start, end) {
|
||||
(None, None) => (),
|
||||
(Some(s), None) => println!("after {}", s),
|
||||
(None, Some(e)) => println!("until {}", e),
|
||||
(Some(s), Some(e)) => println!("{} - {}", s, e),
|
||||
}
|
||||
|
||||
if metadata.last_sync == metadata.start {
|
||||
print!("\x1b[1m\x1b[91mUnsynced\x1b[0m");
|
||||
} else if Some(metadata.last_sync) == metadata.end {
|
||||
print!("\x1b[1m\x1b[92mComplete\x1b[0m");
|
||||
} else {
|
||||
print!(
|
||||
"\x1b[1mLast synced:\x1b[0m {}",
|
||||
Local
|
||||
.timestamp_opt(metadata.last_sync.0 as i64, 0)
|
||||
.unwrap()
|
||||
.format("%b %e, %Y %r")
|
||||
);
|
||||
}
|
||||
if current_time().0 - metadata.last_sync.0 > SECS_IN_WEEK
|
||||
&& Some(metadata.last_sync) != metadata.end
|
||||
{
|
||||
if name == "default" {
|
||||
print!(" - \x1b[33mRun 'startrnr sync' to update!\x1b[0m");
|
||||
} else {
|
||||
print!(
|
||||
" - \x1b[33mRun 'startrnr sync {:?}' to update!\x1b[0m",
|
||||
name
|
||||
);
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"\n\x1b[1mNetwork Decay Constant:\x1b[0m {}",
|
||||
metadata.decay_const
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(connection: &Connection, auth: String, name: Option<String>) {
|
||||
// Name
|
||||
|
||||
let name = name.unwrap_or_else(|| {
|
||||
print!("Name of new dataset: ");
|
||||
read_string()
|
||||
});
|
||||
|
||||
// Game
|
||||
|
||||
print!("Search games: ");
|
||||
let games = run_query::<VideogameSearch, _>(
|
||||
VideogameSearchVars {
|
||||
name: &read_string(),
|
||||
},
|
||||
&auth,
|
||||
)
|
||||
.unwrap_or_else(|| error("Could not access start.gg", 1));
|
||||
|
||||
if games.is_empty() {
|
||||
issue("No games found!", 0);
|
||||
}
|
||||
|
||||
println!("\nSearch results:");
|
||||
for (i, game) in games.iter().enumerate() {
|
||||
println!("{} - {}", i, game.name);
|
||||
}
|
||||
|
||||
print!("\nGame to track ratings for (0-{}): ", games.len() - 1);
|
||||
let index = read_string()
|
||||
.parse::<usize>()
|
||||
.unwrap_or_else(|_| error("Not an integer", 1));
|
||||
if index >= games.len() {
|
||||
error("Out of range!", 1);
|
||||
}
|
||||
|
||||
let VideogameData {
|
||||
id: game_id,
|
||||
name: game_name,
|
||||
slug: game_slug,
|
||||
} = games[index].clone();
|
||||
|
||||
// Location
|
||||
|
||||
print!(
|
||||
"
|
||||
\x1b[1mCountry\x1b[0m
|
||||
Enter the two-letter code for the country you want to track ratings in, e.g.
|
||||
\"US\" for the United States. See \x1b[4m\x1b]8;;https://www.ups.com/worldshiphelp/\
|
||||
WSA/ENU/AppHelp/mergedProjects/CORE/Codes/Country_Territory_and_Currency_Codes.htm\
|
||||
\x1b\\this site\x1b]8;;\x1b\\\x1b[0m for a list of these codes.
|
||||
If no code is entered, then the dataset will track all players globally.
|
||||
|
||||
Country to track ratings for (leave empty for none): "
|
||||
);
|
||||
let country = {
|
||||
let mut string = read_string();
|
||||
if string.is_empty() {
|
||||
None
|
||||
} else if string.len() == 2 && string.chars().all(|c| c.is_ascii_alphabetic()) {
|
||||
string.make_ascii_uppercase();
|
||||
Some(string)
|
||||
} else {
|
||||
error("Input is not a two-letter code", 1)
|
||||
}
|
||||
};
|
||||
|
||||
let state = if country.as_ref().is_some_and(|s| s == "US" || s == "CA") {
|
||||
print!(
|
||||
"
|
||||
\x1b[1mState/Province\x1b[0m
|
||||
Enter the two-letter code for the US state or Canadian province you want to track
|
||||
ratings in, e.g. \"CA\" for California. See \x1b[4m\x1b]8;;https://www.ups.com/worldshiphelp/\
|
||||
WSA/ENU/AppHelp/mergedProjects/CORE/Codes/State_Province_Codes.htm\x1b\\this site\
|
||||
\x1b]8;;\x1b\\\x1b[0m for a list of these codes.
|
||||
If no code is entered, then the dataset will track all players within the country.
|
||||
|
||||
State/province to track ratings for (leave empty for none): "
|
||||
);
|
||||
let mut string = read_string();
|
||||
if string.is_empty() {
|
||||
None
|
||||
} else if string.len() == 2 && string.chars().all(|c| c.is_ascii_alphabetic()) {
|
||||
string.make_ascii_uppercase();
|
||||
Some(string)
|
||||
} else {
|
||||
error("Input is not a two-letter code", 1);
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Interval
|
||||
|
||||
print!(
|
||||
"
|
||||
\x1b[1mStart Date\x1b[0m
|
||||
The rating system will process tournaments starting at this date. If only a year
|
||||
is entered, the date will be the start of that year.
|
||||
|
||||
Start date (year, m/y, or m/d/y): "
|
||||
);
|
||||
let start = {
|
||||
let string = read_string();
|
||||
if string.is_empty() {
|
||||
Timestamp(1)
|
||||
} else if string.chars().all(|c| c.is_ascii_digit() || c == '/') {
|
||||
if let Some((y, m, d)) = match string.split('/').collect::<Vec<_>>()[..] {
|
||||
[] => None,
|
||||
[y] => Some((y.parse().unwrap(), 1, 1)),
|
||||
[m, y] => Some((y.parse().unwrap(), m.parse().unwrap(), 1)),
|
||||
[m, d, y] => Some((y.parse().unwrap(), m.parse().unwrap(), d.parse().unwrap())),
|
||||
_ => error("Input is not a date", 1),
|
||||
} {
|
||||
Timestamp(Utc.with_ymd_and_hms(y, m, d, 0, 1, 1).unwrap().timestamp() as u64)
|
||||
} else {
|
||||
Timestamp(1)
|
||||
}
|
||||
} else {
|
||||
error("Input is not a date", 1);
|
||||
}
|
||||
};
|
||||
|
||||
print!(
|
||||
"
|
||||
\x1b[1mEnd Date\x1b[0m
|
||||
The rating system will stop processing tournaments when it reaches this date. If
|
||||
only a year is entered, the date will be the end of that year.
|
||||
|
||||
End date (year, m/y, or m/d/y): "
|
||||
);
|
||||
let end = {
|
||||
let string = read_string();
|
||||
if string.is_empty() {
|
||||
None
|
||||
} else if string.chars().all(|c| c.is_ascii_digit() || c == '/') {
|
||||
if let Some((y, m, d)) = match string.split('/').collect::<Vec<_>>()[..] {
|
||||
[] => None,
|
||||
[y] => Some((y.parse().unwrap(), 12, 31)),
|
||||
[m, y] => Some((y.parse().unwrap(), m.parse().unwrap(), 30)),
|
||||
[m, d, y] => Some((y.parse().unwrap(), m.parse().unwrap(), d.parse().unwrap())),
|
||||
_ => error("Input is not a date", 1),
|
||||
} {
|
||||
Some(Timestamp(
|
||||
Utc.with_ymd_and_hms(y, m, d, 11, 59, 59)
|
||||
.unwrap()
|
||||
.timestamp() as u64,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
error("Input is not a date", 1);
|
||||
}
|
||||
};
|
||||
|
||||
// Advanced Options
|
||||
|
||||
// Defaults
|
||||
let mut decay_const = 0.9;
|
||||
let mut var_rate = (10.0 - 0.04) / SECS_IN_YEAR as f64 / 3.0;
|
||||
|
||||
print!("\nConfigure advanced options? (y/n) ");
|
||||
if let Some('y') = read_string().chars().next() {
|
||||
// Decay Rate
|
||||
|
||||
print!(
|
||||
"
|
||||
\x1b[1mNetwork Decay Constant\x1b[0m
|
||||
The network decay constant is a number between 0 and 1 that controls how
|
||||
player wins and losses propagate throughout the network. If the decay
|
||||
constant is 1, then it is assumed that a player's skill against one
|
||||
opponent always carries over to all other opponents. If the decay
|
||||
constant is 0, then all player match-ups are assumed to be independent of
|
||||
each other.
|
||||
|
||||
Network decay constant (default 0.9): "
|
||||
);
|
||||
let decay_const_input = read_string();
|
||||
if !decay_const_input.is_empty() {
|
||||
decay_const = decay_const_input
|
||||
.parse::<f64>()
|
||||
.unwrap_or_else(|_| error("Input is not a number", 1));
|
||||
if decay_const < 0.0 || decay_const > 1.0 {
|
||||
error("Input is not between 0 and 1", 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Variance Constant
|
||||
|
||||
print!(
|
||||
"
|
||||
\x1b[1mVariance Rate\x1b[0m
|
||||
This constant determines how quickly a player's variance (the uncertainty
|
||||
of their rating) increases over time. See the end of \x1b[4m\x1b]8;;http:\
|
||||
//www.glicko.net/glicko/glicko.pdf\x1b\\this paper\x1b]8;;\x1b\\\x1b[0m for details
|
||||
on how to compute a good value, or you can leave it blank and a reasonable
|
||||
default will be chosen.
|
||||
|
||||
Variance rate: "
|
||||
);
|
||||
let var_rate_input = read_string();
|
||||
if !var_rate_input.is_empty() {
|
||||
var_rate = var_rate_input
|
||||
.parse::<f64>()
|
||||
.unwrap_or_else(|_| error("Input is not a number", 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Done configuring
|
||||
|
||||
new_dataset(
|
||||
connection,
|
||||
&name,
|
||||
DatasetMetadata {
|
||||
start,
|
||||
end,
|
||||
last_sync: start,
|
||||
game_id,
|
||||
game_name,
|
||||
game_slug,
|
||||
country,
|
||||
state,
|
||||
decay_const,
|
||||
var_rate,
|
||||
},
|
||||
)
|
||||
.expect("Error communicating with SQLite");
|
||||
|
||||
println!("\nCreated dataset {}", name);
|
||||
}
|
||||
|
||||
pub fn delete(connection: &Connection, name: Option<String>) {
|
||||
let name = name.unwrap_or_else(|| {
|
||||
print!("Dataset to delete: ");
|
||||
read_string()
|
||||
});
|
||||
|
||||
delete_dataset(connection, &name).unwrap_or_else(|_| error("That dataset does not exist!", 1));
|
||||
}
|
||||
|
||||
pub fn rename(connection: &Connection, old: Option<String>, new: Option<String>) {
|
||||
let old = old.unwrap_or_else(|| {
|
||||
print!("Dataset to rename: ");
|
||||
read_string()
|
||||
});
|
||||
let new = new.unwrap_or_else(|| {
|
||||
print!("Rename to: ");
|
||||
read_string()
|
||||
});
|
||||
|
||||
match rename_dataset(connection, &old, &new) {
|
||||
Ok(()) => (),
|
||||
Err(sqlite::Error {
|
||||
code: Some(1),
|
||||
message: _,
|
||||
}) => error(&format!("Dataset {:?} does not exist", &old), 1),
|
||||
Err(sqlite::Error {
|
||||
code: Some(19),
|
||||
message: _,
|
||||
}) => error(&format!("Dataset {:?} already exists", &new), 1),
|
||||
Err(_) => error("Unknown error occurred", 2),
|
||||
};
|
||||
}
|
145
src/commands/player.rs
Normal file
145
src/commands/player.rs
Normal file
|
@ -0,0 +1,145 @@
|
|||
use crate::database::*;
|
||||
use crate::queries::*;
|
||||
use crate::util::*;
|
||||
use sqlite::*;
|
||||
|
||||
pub fn list(connection: &Connection, dataset: Option<String>, all: bool) {
|
||||
if all {
|
||||
} else {
|
||||
let dataset = dataset.unwrap_or_else(|| String::from("default"));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn info(connection: &Connection, dataset: Option<String>, player: String) {
|
||||
let dataset = dataset.unwrap_or_else(|| String::from("default"));
|
||||
|
||||
let PlayerData {
|
||||
id,
|
||||
name,
|
||||
prefix,
|
||||
discrim,
|
||||
} = get_player_from_input(connection, player)
|
||||
.unwrap_or_else(|_| error("Could not find player", 1));
|
||||
|
||||
let (won, lost) = get_player_set_counts(connection, &dataset, id)
|
||||
.unwrap_or_else(|_| error("Could not find player", 1));
|
||||
|
||||
if let Some(pre) = prefix {
|
||||
print!("\x1b[2m{}\x1b[22m ", pre);
|
||||
}
|
||||
println!(
|
||||
"\x1b[4m\x1b]8;;https://www.start.gg/user/{1}\x1b\\\
|
||||
\x1b[1m{0}\x1b[22m\x1b]8;;\x1b\\\x1b[0m ({1})",
|
||||
name, discrim
|
||||
);
|
||||
println!("\x1b[1mID:\x1b[0m {}", id.0);
|
||||
|
||||
println!(
|
||||
"\n\x1b[1mSet Count:\x1b[0m {} - {} ({:.3}%)",
|
||||
won,
|
||||
lost,
|
||||
(won as f64 / (won + lost) as f64) * 100.0
|
||||
);
|
||||
}
|
||||
|
||||
pub fn matchup(connection: &Connection, dataset: Option<String>, player1: String, player2: String) {
|
||||
let dataset = dataset.unwrap_or_else(|| String::from("default"));
|
||||
|
||||
let PlayerData {
|
||||
id: player1,
|
||||
name: name1,
|
||||
prefix: prefix1,
|
||||
discrim: discrim1,
|
||||
} = get_player_from_input(connection, player1)
|
||||
.unwrap_or_else(|_| error("Could not find player", 1));
|
||||
|
||||
let PlayerData {
|
||||
id: player2,
|
||||
name: name2,
|
||||
prefix: prefix2,
|
||||
discrim: discrim2,
|
||||
} = get_player_from_input(connection, player2)
|
||||
.unwrap_or_else(|_| error("Could not find player", 1));
|
||||
|
||||
let (hypothetical, advantage, variance) =
|
||||
get_network_data(connection, &dataset, player1, player2)
|
||||
.expect("Error communicating with SQLite")
|
||||
.map(|(adv, var)| (false, adv, var))
|
||||
.unwrap_or_else(|| {
|
||||
let metadata = get_metadata(connection, &dataset)
|
||||
.expect("Error communicating with SQLite")
|
||||
.unwrap_or_else(|| error("Dataset not found", 1));
|
||||
let (adv, var) = hypothetical_advantage(
|
||||
connection,
|
||||
&dataset,
|
||||
player1,
|
||||
player2,
|
||||
metadata.decay_const,
|
||||
)
|
||||
.expect("Error communicating with SQLite");
|
||||
(true, adv, var)
|
||||
});
|
||||
|
||||
let probability = 1.0 / (1.0 + f64::exp(-advantage));
|
||||
|
||||
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 len2 = prefix2.as_deref().map(|s| s.len() + 1).unwrap_or(0) + name2.len();
|
||||
|
||||
// Prefix + name for each player
|
||||
|
||||
if let Some(pre) = prefix1 {
|
||||
print!("\x1b[2m{}\x1b[22m ", pre);
|
||||
}
|
||||
print!(
|
||||
"\x1b[4m\x1b]8;;https://www.start.gg/user/{}\x1b\\\
|
||||
\x1b[1m{}\x1b[22m\x1b]8;;\x1b\\\x1b[0m - ",
|
||||
discrim1, name1
|
||||
);
|
||||
if let Some(pre) = prefix2 {
|
||||
print!("\x1b[2m{}\x1b[22m ", pre);
|
||||
}
|
||||
println!(
|
||||
"\x1b[4m\x1b]8;;https://www.start.gg/user/{}\x1b\\\
|
||||
\x1b[1m{}\x1b[22m\x1b]8;;\x1b\\\x1b[0m",
|
||||
discrim2, name2
|
||||
);
|
||||
|
||||
// Probability breakdown
|
||||
|
||||
println!(
|
||||
"\x1b[1m\x1b[{4}m{0:>2$}\x1b[0m - \x1b[1m\x1b[{5}m{1:<3$}\x1b[0m",
|
||||
format!("{:.1}%", (1.0 - probability) * 100.0),
|
||||
format!("{:.1}%", probability * 100.0),
|
||||
len1,
|
||||
len2,
|
||||
other_color,
|
||||
color
|
||||
);
|
||||
|
||||
// Advantage + variance
|
||||
|
||||
println!(
|
||||
"\n\x1b[1m{0}Advantage: \x1b[{1}m{2:+.4}\x1b[39m\n{0}Variance: {3:.4}\x1b[0m",
|
||||
if hypothetical { "Hypothetical " } else { "" },
|
||||
color,
|
||||
advantage,
|
||||
variance
|
||||
);
|
||||
|
||||
if !hypothetical {
|
||||
// Set count
|
||||
|
||||
let (a, b) = get_matchup_set_counts(connection, &dataset, player1, player2)
|
||||
.expect("Error communicating with SQLite");
|
||||
|
||||
println!(
|
||||
"\n\x1b[1mSet Count:\x1b[0m {} - {} ({:.3}% - {:.3}%)",
|
||||
a,
|
||||
b,
|
||||
(a as f64 / (a + b) as f64) * 100.0,
|
||||
(b as f64 / (a + b) as f64) * 100.0
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::commands::*;
|
||||
use crate::database::*;
|
||||
use crate::error;
|
||||
use crate::queries::*;
|
||||
use crate::util::*;
|
||||
use itertools::Itertools;
|
||||
use sqlite::*;
|
||||
use std::cmp::min;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
// Extract set data
|
||||
|
||||
|
@ -222,3 +223,45 @@ pub fn sync_dataset(
|
|||
}
|
||||
connection.execute("COMMIT;")
|
||||
}
|
||||
|
||||
pub fn sync(connection: &Connection, auth: String, datasets: Vec<String>, all: bool) {
|
||||
let all_datasets = list_dataset_names(connection).unwrap();
|
||||
|
||||
let datasets = if all {
|
||||
all_datasets
|
||||
} else if datasets.is_empty() {
|
||||
if all_datasets.is_empty() {
|
||||
print!("No datasets exist; create one? (y/n) ");
|
||||
if let Some('y') = read_string().chars().next() {
|
||||
dataset::new(connection, auth.clone(), Some(String::from("default")));
|
||||
vec![String::from("default")]
|
||||
} else {
|
||||
error("No datasets specified and no default dataset", 1)
|
||||
}
|
||||
} else if all_datasets.iter().any(|x| x == "default") {
|
||||
vec![String::from("default")]
|
||||
} else {
|
||||
error("No datasets specified and no default dataset", 1);
|
||||
}
|
||||
} else {
|
||||
datasets
|
||||
};
|
||||
|
||||
let current_time = current_time();
|
||||
|
||||
for dataset in datasets {
|
||||
let dataset_metadata = get_metadata(connection, &dataset)
|
||||
.expect("Error communicating with SQLite")
|
||||
.unwrap_or_else(|| error(&format!("Dataset {} does not exist!", dataset), 1));
|
||||
|
||||
let before = dataset_metadata
|
||||
.end
|
||||
.map(|end| min(end, current_time))
|
||||
.unwrap_or(current_time);
|
||||
|
||||
sync_dataset(connection, &dataset, dataset_metadata, before, &auth)
|
||||
.expect("Error communicating with SQLite");
|
||||
|
||||
update_last_sync(connection, &dataset, before).expect("Error communicating with SQLite");
|
||||
}
|
||||
}
|
638
src/main.rs
638
src/main.rs
|
@ -1,17 +1,15 @@
|
|||
#![feature(iterator_try_collect)]
|
||||
#![feature(extend_one)]
|
||||
|
||||
use chrono::{Local, TimeZone, Utc};
|
||||
use clap::{Parser, Subcommand};
|
||||
use sqlite::*;
|
||||
use std::{cmp::min, path::PathBuf};
|
||||
use std::path::PathBuf;
|
||||
|
||||
mod queries;
|
||||
use queries::*;
|
||||
mod commands;
|
||||
use commands::*;
|
||||
mod database;
|
||||
use database::*;
|
||||
mod sync;
|
||||
use sync::*;
|
||||
use database::open_datasets;
|
||||
mod queries;
|
||||
use queries::get_auth_token;
|
||||
mod util;
|
||||
use util::*;
|
||||
|
||||
|
@ -81,12 +79,6 @@ created if it does not already exist."
|
|||
#[arg(short, long, global = true, help = "The dataset to access")]
|
||||
dataset: Option<String>,
|
||||
},
|
||||
Ranking {
|
||||
#[command(subcommand)]
|
||||
subcommand: RankingSC,
|
||||
#[arg(short, long, global = true, help = "The dataset to access")]
|
||||
dataset: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
|
@ -112,12 +104,6 @@ enum PlayerSC {
|
|||
Matchup { player1: String, player2: String },
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum RankingSC {
|
||||
#[command(about = "Create a new ranking")]
|
||||
Create,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
|
@ -137,628 +123,30 @@ fn main() {
|
|||
match cli.subcommand {
|
||||
Subcommands::Dataset {
|
||||
subcommand: DatasetSC::List,
|
||||
} => dataset_list(&connection),
|
||||
} => dataset::list(&connection),
|
||||
Subcommands::Dataset {
|
||||
subcommand: DatasetSC::New { name },
|
||||
} => dataset_new(&connection, get_auth_token(&config_dir), name),
|
||||
} => dataset::new(&connection, get_auth_token(&config_dir), name),
|
||||
Subcommands::Dataset {
|
||||
subcommand: DatasetSC::Delete { name },
|
||||
} => dataset_delete(&connection, name),
|
||||
} => dataset::delete(&connection, name),
|
||||
Subcommands::Dataset {
|
||||
subcommand: DatasetSC::Rename { old, new },
|
||||
} => dataset_rename(&connection, old, new),
|
||||
} => dataset::rename(&connection, old, new),
|
||||
|
||||
Subcommands::Player {
|
||||
subcommand: PlayerSC::Info { player },
|
||||
dataset,
|
||||
} => player_info(&connection, dataset, player),
|
||||
} => player::info(&connection, dataset, player),
|
||||
Subcommands::Player {
|
||||
subcommand: PlayerSC::Matchup { player1, player2 },
|
||||
dataset,
|
||||
} => player_matchup(&connection, dataset, player1, player2),
|
||||
} => player::matchup(&connection, dataset, player1, player2),
|
||||
|
||||
Subcommands::Sync { datasets, all } => {
|
||||
sync(&connection, get_auth_token(&config_dir), datasets, all)
|
||||
sync::sync(&connection, get_auth_token(&config_dir), datasets, all)
|
||||
}
|
||||
|
||||
Subcommands::Ranking {
|
||||
subcommand: RankingSC::Create,
|
||||
dataset,
|
||||
} => ranking_create(&connection, dataset),
|
||||
|
||||
_ => eprintln!("This feature is currently unimplemented."),
|
||||
}
|
||||
}
|
||||
|
||||
// Datasets
|
||||
|
||||
fn dataset_list(connection: &Connection) {
|
||||
let datasets = list_datasets(&connection).expect("Error communicating with SQLite");
|
||||
|
||||
for (name, metadata) in datasets {
|
||||
print!(
|
||||
"· \x1b[1m\x1b[34m{}\x1b[0m
|
||||
\x1b[4m\x1b]8;;https://www.start.gg/{}\x1b\\{}\x1b]8;;\x1b\\\x1b[0m ",
|
||||
name, metadata.game_slug, metadata.game_name
|
||||
);
|
||||
|
||||
if let Some(country) = metadata.country {
|
||||
if let Some(state) = metadata.state {
|
||||
println!("(in {}, {})", country, state);
|
||||
} else {
|
||||
println!("(in {})", country);
|
||||
}
|
||||
} else {
|
||||
println!("(Global)");
|
||||
}
|
||||
|
||||
let start = if metadata.start.0 != 1 {
|
||||
Some(
|
||||
Utc.timestamp_opt(metadata.start.0 as i64, 0)
|
||||
.unwrap()
|
||||
.format("%m/%d/%Y"),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let end = metadata
|
||||
.end
|
||||
.map(|x| Utc.timestamp_opt(x.0 as i64, 0).unwrap().format("%m/%d/%Y"));
|
||||
|
||||
match (start, end) {
|
||||
(None, None) => (),
|
||||
(Some(s), None) => println!("after {}", s),
|
||||
(None, Some(e)) => println!("until {}", e),
|
||||
(Some(s), Some(e)) => println!("{} - {}", s, e),
|
||||
}
|
||||
|
||||
if metadata.last_sync == metadata.start {
|
||||
print!("\x1b[1m\x1b[91mUnsynced\x1b[0m");
|
||||
} else if Some(metadata.last_sync) == metadata.end {
|
||||
print!("\x1b[1m\x1b[92mComplete\x1b[0m");
|
||||
} else {
|
||||
print!(
|
||||
"\x1b[1mLast synced:\x1b[0m {}",
|
||||
Local
|
||||
.timestamp_opt(metadata.last_sync.0 as i64, 0)
|
||||
.unwrap()
|
||||
.format("%b %e, %Y %r")
|
||||
);
|
||||
}
|
||||
if current_time().0 - metadata.last_sync.0 > SECS_IN_WEEK
|
||||
&& Some(metadata.last_sync) != metadata.end
|
||||
{
|
||||
if name == "default" {
|
||||
print!(" - \x1b[33mRun 'startrnr sync' to update!\x1b[0m");
|
||||
} else {
|
||||
print!(
|
||||
" - \x1b[33mRun 'startrnr sync {:?}' to update!\x1b[0m",
|
||||
name
|
||||
);
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"\n\x1b[1mNetwork Decay Constant:\x1b[0m {}",
|
||||
metadata.decay_const
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn dataset_new(connection: &Connection, auth: String, name: Option<String>) {
|
||||
// Name
|
||||
|
||||
let name = name.unwrap_or_else(|| {
|
||||
print!("Name of new dataset: ");
|
||||
read_string()
|
||||
});
|
||||
|
||||
// Game
|
||||
|
||||
print!("Search games: ");
|
||||
let games = run_query::<VideogameSearch, _>(
|
||||
VideogameSearchVars {
|
||||
name: &read_string(),
|
||||
},
|
||||
&auth,
|
||||
)
|
||||
.unwrap_or_else(|| error("Could not access start.gg", 1));
|
||||
|
||||
if games.is_empty() {
|
||||
issue("No games found!", 0);
|
||||
}
|
||||
|
||||
println!("\nSearch results:");
|
||||
for (i, game) in games.iter().enumerate() {
|
||||
println!("{} - {}", i, game.name);
|
||||
}
|
||||
|
||||
print!("\nGame to track ratings for (0-{}): ", games.len() - 1);
|
||||
let index = read_string()
|
||||
.parse::<usize>()
|
||||
.unwrap_or_else(|_| error("Not an integer", 1));
|
||||
if index >= games.len() {
|
||||
error("Out of range!", 1);
|
||||
}
|
||||
|
||||
let VideogameData {
|
||||
id: game_id,
|
||||
name: game_name,
|
||||
slug: game_slug,
|
||||
} = games[index].clone();
|
||||
|
||||
// Location
|
||||
|
||||
print!(
|
||||
"
|
||||
\x1b[1mCountry\x1b[0m
|
||||
Enter the two-letter code for the country you want to track ratings in, e.g.
|
||||
\"US\" for the United States. See \x1b[4m\x1b]8;;https://www.ups.com/worldshiphelp/\
|
||||
WSA/ENU/AppHelp/mergedProjects/CORE/Codes/Country_Territory_and_Currency_Codes.htm\
|
||||
\x1b\\this site\x1b]8;;\x1b\\\x1b[0m for a list of these codes.
|
||||
If no code is entered, then the dataset will track all players globally.
|
||||
|
||||
Country to track ratings for (leave empty for none): "
|
||||
);
|
||||
let country = {
|
||||
let mut string = read_string();
|
||||
if string.is_empty() {
|
||||
None
|
||||
} else if string.len() == 2 && string.chars().all(|c| c.is_ascii_alphabetic()) {
|
||||
string.make_ascii_uppercase();
|
||||
Some(string)
|
||||
} else {
|
||||
error("Input is not a two-letter code", 1);
|
||||
}
|
||||
};
|
||||
|
||||
let state = if country.as_ref().is_some_and(|s| s == "US" || s == "CA") {
|
||||
print!(
|
||||
"
|
||||
\x1b[1mState/Province\x1b[0m
|
||||
Enter the two-letter code for the US state or Canadian province you want to track
|
||||
ratings in, e.g. \"CA\" for California. See \x1b[4m\x1b]8;;https://www.ups.com/worldshiphelp/\
|
||||
WSA/ENU/AppHelp/mergedProjects/CORE/Codes/State_Province_Codes.htm\x1b\\this site\
|
||||
\x1b]8;;\x1b\\\x1b[0m for a list of these codes.
|
||||
If no code is entered, then the dataset will track all players within the country.
|
||||
|
||||
State/province to track ratings for (leave empty for none): "
|
||||
);
|
||||
let mut string = read_string();
|
||||
if string.is_empty() {
|
||||
None
|
||||
} else if string.len() == 2 && string.chars().all(|c| c.is_ascii_alphabetic()) {
|
||||
string.make_ascii_uppercase();
|
||||
Some(string)
|
||||
} else {
|
||||
error("Input is not a two-letter code", 1);
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Interval
|
||||
|
||||
print!(
|
||||
"
|
||||
\x1b[1mStart Date\x1b[0m
|
||||
The rating system will process tournaments starting at this date. If only a year
|
||||
is entered, the date will be the start of that year.
|
||||
|
||||
Start date (year, m/y, or m/d/y): "
|
||||
);
|
||||
let start = {
|
||||
let string = read_string();
|
||||
if string.is_empty() {
|
||||
Timestamp(1)
|
||||
} else if string.chars().all(|c| c.is_ascii_digit() || c == '/') {
|
||||
if let Some((y, m, d)) = match string.split('/').collect::<Vec<_>>()[..] {
|
||||
[] => None,
|
||||
[y] => Some((y.parse().unwrap(), 1, 1)),
|
||||
[m, y] => Some((y.parse().unwrap(), m.parse().unwrap(), 1)),
|
||||
[m, d, y] => Some((y.parse().unwrap(), m.parse().unwrap(), d.parse().unwrap())),
|
||||
_ => error("Input is not a date", 1),
|
||||
} {
|
||||
Timestamp(Utc.with_ymd_and_hms(y, m, d, 0, 1, 1).unwrap().timestamp() as u64)
|
||||
} else {
|
||||
Timestamp(1)
|
||||
}
|
||||
} else {
|
||||
error("Input is not a date", 1);
|
||||
}
|
||||
};
|
||||
|
||||
print!(
|
||||
"
|
||||
\x1b[1mEnd Date\x1b[0m
|
||||
The rating system will stop processing tournaments when it reaches this date. If
|
||||
only a year is entered, the date will be the end of that year.
|
||||
|
||||
End date (year, m/y, or m/d/y): "
|
||||
);
|
||||
let end = {
|
||||
let string = read_string();
|
||||
if string.is_empty() {
|
||||
None
|
||||
} else if string.chars().all(|c| c.is_ascii_digit() || c == '/') {
|
||||
if let Some((y, m, d)) = match string.split('/').collect::<Vec<_>>()[..] {
|
||||
[] => None,
|
||||
[y] => Some((y.parse().unwrap(), 12, 31)),
|
||||
[m, y] => Some((y.parse().unwrap(), m.parse().unwrap(), 30)),
|
||||
[m, d, y] => Some((y.parse().unwrap(), m.parse().unwrap(), d.parse().unwrap())),
|
||||
_ => error("Input is not a date", 1),
|
||||
} {
|
||||
Some(Timestamp(
|
||||
Utc.with_ymd_and_hms(y, m, d, 11, 59, 59)
|
||||
.unwrap()
|
||||
.timestamp() as u64,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
error("Input is not a date", 1);
|
||||
}
|
||||
};
|
||||
|
||||
// Advanced Options
|
||||
|
||||
// Defaults
|
||||
let mut decay_const = 0.9;
|
||||
let mut var_rate = (10.0 - 0.04) / SECS_IN_YEAR as f64 / 3.0;
|
||||
|
||||
print!("\nConfigure advanced options? (y/n) ");
|
||||
if let Some('y') = read_string().chars().next() {
|
||||
// Decay Rate
|
||||
|
||||
print!(
|
||||
"
|
||||
\x1b[1mNetwork Decay Constant\x1b[0m
|
||||
The network decay constant is a number between 0 and 1 that controls how
|
||||
player wins and losses propagate throughout the network. If the decay
|
||||
constant is 1, then it is assumed that a player's skill against one
|
||||
opponent always carries over to all other opponents. If the decay
|
||||
constant is 0, then all player match-ups are assumed to be independent of
|
||||
each other.
|
||||
|
||||
Network decay constant (default 0.9): "
|
||||
);
|
||||
let decay_const_input = read_string();
|
||||
if !decay_const_input.is_empty() {
|
||||
decay_const = decay_const_input
|
||||
.parse::<f64>()
|
||||
.unwrap_or_else(|_| error("Input is not a number", 1));
|
||||
if decay_const < 0.0 || decay_const > 1.0 {
|
||||
error("Input is not between 0 and 1", 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Variance Constant
|
||||
|
||||
print!(
|
||||
"
|
||||
\x1b[1mVariance Rate\x1b[0m
|
||||
This constant determines how quickly a player's variance (the uncertainty
|
||||
of their rating) increases over time. See the end of \x1b[4m\x1b]8;;http:\
|
||||
//www.glicko.net/glicko/glicko.pdf\x1b\\this paper\x1b]8;;\x1b\\\x1b[0m for details
|
||||
on how to compute a good value, or you can leave it blank and a reasonable
|
||||
default will be chosen.
|
||||
|
||||
Variance rate: "
|
||||
);
|
||||
let var_rate_input = read_string();
|
||||
if !var_rate_input.is_empty() {
|
||||
var_rate = var_rate_input
|
||||
.parse::<f64>()
|
||||
.unwrap_or_else(|_| error("Input is not a number", 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Done configuring
|
||||
|
||||
new_dataset(
|
||||
connection,
|
||||
&name,
|
||||
DatasetMetadata {
|
||||
start,
|
||||
end,
|
||||
last_sync: start,
|
||||
game_id,
|
||||
game_name,
|
||||
game_slug,
|
||||
country,
|
||||
state,
|
||||
decay_const,
|
||||
var_rate,
|
||||
},
|
||||
)
|
||||
.expect("Error communicating with SQLite");
|
||||
|
||||
println!("\nCreated dataset {}", name);
|
||||
}
|
||||
|
||||
fn dataset_delete(connection: &Connection, name: Option<String>) {
|
||||
let name = name.unwrap_or_else(|| {
|
||||
print!("Dataset to delete: ");
|
||||
read_string()
|
||||
});
|
||||
|
||||
delete_dataset(connection, &name).unwrap_or_else(|_| error("That dataset does not exist!", 1));
|
||||
}
|
||||
|
||||
fn dataset_rename(connection: &Connection, old: Option<String>, new: Option<String>) {
|
||||
let old = old.unwrap_or_else(|| {
|
||||
print!("Dataset to rename: ");
|
||||
read_string()
|
||||
});
|
||||
let new = new.unwrap_or_else(|| {
|
||||
print!("Rename to: ");
|
||||
read_string()
|
||||
});
|
||||
|
||||
match rename_dataset(connection, &old, &new) {
|
||||
Ok(()) => (),
|
||||
Err(sqlite::Error {
|
||||
code: Some(1),
|
||||
message: _,
|
||||
}) => error(&format!("Dataset {:?} does not exist", &old), 1),
|
||||
Err(sqlite::Error {
|
||||
code: Some(19),
|
||||
message: _,
|
||||
}) => error(&format!("Dataset {:?} already exists", &new), 1),
|
||||
Err(_) => error("Unknown error occurred", 2),
|
||||
};
|
||||
}
|
||||
|
||||
// Players
|
||||
|
||||
fn player_info(connection: &Connection, dataset: Option<String>, player: String) {
|
||||
let dataset = dataset.unwrap_or_else(|| String::from("default"));
|
||||
|
||||
let PlayerData {
|
||||
id,
|
||||
name,
|
||||
prefix,
|
||||
discrim,
|
||||
} = get_player_from_input(connection, player)
|
||||
.unwrap_or_else(|_| error("Could not find player", 1));
|
||||
|
||||
let (won, lost) = get_player_set_counts(connection, &dataset, id)
|
||||
.unwrap_or_else(|_| error("Could not find player", 1));
|
||||
|
||||
if let Some(pre) = prefix {
|
||||
print!("\x1b[2m{}\x1b[22m ", pre);
|
||||
}
|
||||
println!(
|
||||
"\x1b[4m\x1b]8;;https://www.start.gg/user/{1}\x1b\\\
|
||||
\x1b[1m{0}\x1b[22m\x1b]8;;\x1b\\\x1b[0m ({1})",
|
||||
name, discrim
|
||||
);
|
||||
println!("\x1b[1mID:\x1b[0m {}", id.0);
|
||||
|
||||
println!(
|
||||
"\n\x1b[1mSet Count:\x1b[0m {} - {} ({:.3}%)",
|
||||
won,
|
||||
lost,
|
||||
(won as f64 / (won + lost) as f64) * 100.0
|
||||
);
|
||||
}
|
||||
|
||||
fn player_matchup(
|
||||
connection: &Connection,
|
||||
dataset: Option<String>,
|
||||
player1: String,
|
||||
player2: String,
|
||||
) {
|
||||
let dataset = dataset.unwrap_or_else(|| String::from("default"));
|
||||
|
||||
let PlayerData {
|
||||
id: player1,
|
||||
name: name1,
|
||||
prefix: prefix1,
|
||||
discrim: discrim1,
|
||||
} = get_player_from_input(connection, player1)
|
||||
.unwrap_or_else(|_| error("Could not find player", 1));
|
||||
|
||||
let PlayerData {
|
||||
id: player2,
|
||||
name: name2,
|
||||
prefix: prefix2,
|
||||
discrim: discrim2,
|
||||
} = get_player_from_input(connection, player2)
|
||||
.unwrap_or_else(|_| error("Could not find player", 1));
|
||||
|
||||
let (hypothetical, advantage, variance) =
|
||||
get_network_data(connection, &dataset, player1, player2)
|
||||
.expect("Error communicating with SQLite")
|
||||
.map(|(adv, var)| (false, adv, var))
|
||||
.unwrap_or_else(|| {
|
||||
let metadata = get_metadata(connection, &dataset)
|
||||
.expect("Error communicating with SQLite")
|
||||
.unwrap_or_else(|| error("Dataset not found", 1));
|
||||
let (adv, var) = hypothetical_advantage(
|
||||
connection,
|
||||
&dataset,
|
||||
player1,
|
||||
player2,
|
||||
metadata.decay_const,
|
||||
)
|
||||
.expect("Error communicating with SQLite");
|
||||
(true, adv, var)
|
||||
});
|
||||
|
||||
let probability = 1.0 / (1.0 + f64::exp(-advantage));
|
||||
|
||||
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 len2 = prefix2.as_deref().map(|s| s.len() + 1).unwrap_or(0) + name2.len();
|
||||
|
||||
// Prefix + name for each player
|
||||
|
||||
if let Some(pre) = prefix1 {
|
||||
print!("\x1b[2m{}\x1b[22m ", pre);
|
||||
}
|
||||
print!(
|
||||
"\x1b[4m\x1b]8;;https://www.start.gg/user/{}\x1b\\\
|
||||
\x1b[1m{}\x1b[22m\x1b]8;;\x1b\\\x1b[0m - ",
|
||||
discrim1, name1
|
||||
);
|
||||
if let Some(pre) = prefix2 {
|
||||
print!("\x1b[2m{}\x1b[22m ", pre);
|
||||
}
|
||||
println!(
|
||||
"\x1b[4m\x1b]8;;https://www.start.gg/user/{}\x1b\\\
|
||||
\x1b[1m{}\x1b[22m\x1b]8;;\x1b\\\x1b[0m",
|
||||
discrim2, name2
|
||||
);
|
||||
|
||||
// Probability breakdown
|
||||
|
||||
println!(
|
||||
"\x1b[1m\x1b[{4}m{0:>2$}\x1b[0m - \x1b[1m\x1b[{5}m{1:<3$}\x1b[0m",
|
||||
format!("{:.1}%", (1.0 - probability) * 100.0),
|
||||
format!("{:.1}%", probability * 100.0),
|
||||
len1,
|
||||
len2,
|
||||
other_color,
|
||||
color
|
||||
);
|
||||
|
||||
// Advantage + variance
|
||||
|
||||
println!(
|
||||
"\n\x1b[1m{0}Advantage: \x1b[{1}m{2:+.4}\x1b[39m\n{0}Variance: {3:.4}\x1b[0m",
|
||||
if hypothetical { "Hypothetical " } else { "" },
|
||||
color,
|
||||
advantage,
|
||||
variance
|
||||
);
|
||||
|
||||
if !hypothetical {
|
||||
// Set count
|
||||
|
||||
let (a, b) = get_matchup_set_counts(connection, &dataset, player1, player2)
|
||||
.expect("Error communicating with SQLite");
|
||||
|
||||
println!(
|
||||
"\n\x1b[1mSet Count:\x1b[0m {} - {} ({:.3}% - {:.3}%)",
|
||||
a,
|
||||
b,
|
||||
(a as f64 / (a + b) as f64) * 100.0,
|
||||
(b as f64 / (a + b) as f64) * 100.0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Sync
|
||||
|
||||
fn sync(connection: &Connection, auth: String, datasets: Vec<String>, all: bool) {
|
||||
let all_datasets = list_dataset_names(connection).unwrap();
|
||||
|
||||
let datasets = if all {
|
||||
all_datasets
|
||||
} else if datasets.is_empty() {
|
||||
if all_datasets.is_empty() {
|
||||
print!("No datasets exist; create one? (y/n) ");
|
||||
if let Some('y') = read_string().chars().next() {
|
||||
dataset_new(connection, auth.clone(), Some(String::from("default")));
|
||||
vec![String::from("default")]
|
||||
} else {
|
||||
error("No datasets specified and no default dataset", 1)
|
||||
}
|
||||
} else if all_datasets.iter().any(|x| x == "default") {
|
||||
vec![String::from("default")]
|
||||
} else {
|
||||
error("No datasets specified and no default dataset", 1);
|
||||
}
|
||||
} else {
|
||||
datasets
|
||||
};
|
||||
|
||||
let current_time = current_time();
|
||||
|
||||
for dataset in datasets {
|
||||
let dataset_metadata = get_metadata(connection, &dataset)
|
||||
.expect("Error communicating with SQLite")
|
||||
.unwrap_or_else(|| error(&format!("Dataset {} does not exist!", dataset), 1));
|
||||
|
||||
let before = dataset_metadata
|
||||
.end
|
||||
.map(|end| min(end, current_time))
|
||||
.unwrap_or(current_time);
|
||||
|
||||
sync_dataset(connection, &dataset, dataset_metadata, before, &auth)
|
||||
.expect("Error communicating with SQLite");
|
||||
|
||||
update_last_sync(connection, &dataset, before).expect("Error communicating with SQLite");
|
||||
}
|
||||
}
|
||||
|
||||
fn ranking_create(connection: &Connection, dataset: Option<String>) {
|
||||
use std::collections::HashMap;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue