Compare commits

...

10 commits

7 changed files with 352 additions and 89 deletions

142
Cargo.lock generated
View file

@ -29,6 +29,21 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.5.0" version = "0.5.0"
@ -155,6 +170,20 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-targets 0.48.5",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.4.5" version = "4.4.5"
@ -594,6 +623,29 @@ dependencies = [
"tokio-native-tls", "tokio-native-tls",
] ]
[[package]]
name = "iana-time-zone"
version = "0.1.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "ident_case" name = "ident_case"
version = "1.0.1" version = "1.0.1"
@ -1154,6 +1206,7 @@ dependencies = [
name = "startrnr" name = "startrnr"
version = "0.2.0" version = "0.2.0"
dependencies = [ dependencies = [
"chrono",
"clap", "clap",
"cynic", "cynic",
"cynic-codegen", "cynic-codegen",
@ -1163,7 +1216,6 @@ dependencies = [
"schema", "schema",
"serde", "serde",
"sqlite", "sqlite",
"time-format",
] ]
[[package]] [[package]]
@ -1233,12 +1285,6 @@ dependencies = [
"syn 2.0.29", "syn 2.0.29",
] ]
[[package]]
name = "time-format"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42baec394ad2773a90e037d7b6dfc7518d1f121b9dbaeebad42e19568c39f196"
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.6.0" version = "1.6.0"
@ -1504,13 +1550,22 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets 0.52.0",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [ dependencies = [
"windows-targets", "windows-targets 0.48.5",
] ]
[[package]] [[package]]
@ -1519,13 +1574,28 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [ dependencies = [
"windows_aarch64_gnullvm", "windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc", "windows_aarch64_msvc 0.48.5",
"windows_i686_gnu", "windows_i686_gnu 0.48.5",
"windows_i686_msvc", "windows_i686_msvc 0.48.5",
"windows_x86_64_gnu", "windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm", "windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc", "windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
dependencies = [
"windows_aarch64_gnullvm 0.52.0",
"windows_aarch64_msvc 0.52.0",
"windows_i686_gnu 0.52.0",
"windows_i686_msvc 0.52.0",
"windows_x86_64_gnu 0.52.0",
"windows_x86_64_gnullvm 0.52.0",
"windows_x86_64_msvc 0.52.0",
] ]
[[package]] [[package]]
@ -1534,42 +1604,84 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.48.5" version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]] [[package]]
name = "winreg" name = "winreg"
version = "0.50.0" version = "0.50.0"

View file

@ -12,7 +12,7 @@ path = "src/main.rs"
[dependencies] [dependencies]
# CLI # CLI
clap = { version = "4.4", features = ["derive"] } clap = { version = "4.4", features = ["derive"] }
time-format = "1.1" chrono = "0.4"
# GraphQL schema # GraphQL schema
schema.path = "schema" schema.path = "schema"

View file

@ -12,11 +12,11 @@ rankings automatically.
**All of these features work for any game, in any region, without restriction.** **All of these features work for any game, in any region, without restriction.**
> **Warning**<br> > [!WARNING]
> StartRNR is unstable and under active development. The design and user > StartRNR is unstable and under active development. The design and user
> interface of this program is experimental and may be subject to change. > interface of this program is experimental and may be subject to change.
> >
> Currently, only generating datasets has been implemented. > Currently, the power ranking and seeding features have not been implemented.
## Installation ## Installation
@ -34,6 +34,30 @@ Alternatively, if you use Nix:
nix profile install github:kiana-S/StartRNR nix profile install github:kiana-S/StartRNR
``` ```
## Usage
Once StartRNR is installed, run:
``` sh
startrnr sync
```
The program will walk you through creating a dataset, then run its rating
algorithm. **This may take up to a few hours to finish running!**
Once the rating data has been generated, these commands can be used to access it:
``` sh
# Access a player's data
startrnr player info <player>
# Analyze matchup of two players
startrnr player matchup <player1> <player2>
```
A player can be specified by their tag or by their
[discriminator](https://help.start.gg/en/articles/4855957-discriminators-on-start-gg).
## Configuration ## Configuration
StartRNR stores its rating databases in its config directory, which is located at: StartRNR stores its rating databases in its config directory, which is located at:

View file

@ -4,6 +4,8 @@ use std::fs::{self, OpenOptions};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
pub struct DatasetMetadata { pub struct DatasetMetadata {
pub start: Timestamp,
pub end: Option<Timestamp>,
pub last_sync: Timestamp, pub last_sync: Timestamp,
pub game_id: VideogameId, pub game_id: VideogameId,
@ -38,10 +40,11 @@ fn datasets_path(config_dir: &Path) -> std::io::Result<PathBuf> {
pub fn open_datasets(config_dir: &Path) -> sqlite::Result<Connection> { pub fn open_datasets(config_dir: &Path) -> sqlite::Result<Connection> {
let path = datasets_path(config_dir).unwrap(); let path = datasets_path(config_dir).unwrap();
let query = "PRAGMA foreign_keys = ON; let query = "
CREATE TABLE IF NOT EXISTS datasets ( CREATE TABLE IF NOT EXISTS datasets (
name TEXT UNIQUE NOT NULL, name TEXT UNIQUE NOT NULL,
start INTEGER NOT NULL,
end INTEGER,
last_sync INTEGER NOT NULL, last_sync INTEGER NOT NULL,
game_id INTEGER NOT NULL, game_id INTEGER NOT NULL,
game_name TEXT NOT NULL, game_name TEXT NOT NULL,
@ -68,10 +71,9 @@ CREATE TABLE IF NOT EXISTS events (
) STRICT; ) STRICT;
CREATE TABLE IF NOT EXISTS sets ( CREATE TABLE IF NOT EXISTS sets (
id TEXT UNIQUE NOT NULL, id TEXT PRIMARY KEY,
event INTEGER NOT NULL, event INTEGER NOT NULL REFERENCES events
FOREIGN KEY(event) REFERENCES events ) STRICT, WITHOUT ROWID;
) STRICT;
"; ";
let connection = sqlite::open(path)?; let connection = sqlite::open(path)?;
@ -102,6 +104,10 @@ pub fn list_datasets(connection: &Connection) -> sqlite::Result<Vec<(String, Dat
Ok(( Ok((
r_.read::<&str, _>("name").to_owned(), r_.read::<&str, _>("name").to_owned(),
DatasetMetadata { DatasetMetadata {
start: Timestamp(r_.read::<i64, _>("start") as u64),
end: r_
.read::<Option<i64>, _>("end")
.map(|x| Timestamp(x as u64)),
last_sync: Timestamp(r_.read::<i64, _>("last_sync") as u64), last_sync: Timestamp(r_.read::<i64, _>("last_sync") as u64),
game_id: VideogameId(r_.read::<i64, _>("game_id") as u64), game_id: VideogameId(r_.read::<i64, _>("game_id") as u64),
game_name: r_.read::<&str, _>("game_name").to_owned(), game_name: r_.read::<&str, _>("game_name").to_owned(),
@ -139,8 +145,6 @@ pub fn rename_dataset(connection: &Connection, old: &str, new: &str) -> sqlite::
r#"UPDATE datasets SET name = '{1}' WHERE name = '{0}'; r#"UPDATE datasets SET name = '{1}' WHERE name = '{0}';
ALTER TABLE "{0}_players" RENAME TO "{1}_players"; ALTER TABLE "{0}_players" RENAME TO "{1}_players";
ALTER TABLE "{0}_network" RENAME TO "{1}_network"; ALTER TABLE "{0}_network" RENAME TO "{1}_network";
DROP INDEX "{0}_network_A";
CREATE INDEX "{1}_network_A" ON "{1}_network" (player_A);
DROP INDEX "{0}_network_B"; DROP INDEX "{0}_network_B";
CREATE INDEX "{1}_network_B" ON "{1}_network" (player_B);"#, CREATE INDEX "{1}_network_B" ON "{1}_network" (player_B);"#,
old, new old, new
@ -154,10 +158,10 @@ 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, id INTEGER PRIMARY KEY REFERENCES players,
last_played INTEGER NOT NULL, last_played INTEGER NOT NULL,
deviation REAL NOT NULL, deviation REAL NOT NULL,
volatility REAL NOT NULL, volatility REAL NOT NULL,
@ -182,14 +186,13 @@ CREATE TABLE "{0}_network" (
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),
UNIQUE (player_A, player_B), PRIMARY KEY (player_A, player_B),
CHECK (player_A < player_B), CHECK (player_A < player_B),
FOREIGN KEY(player_A) REFERENCES "{0}_players" FOREIGN KEY(player_A) REFERENCES "{0}_players"
ON DELETE CASCADE, ON DELETE CASCADE,
FOREIGN KEY(player_B) REFERENCES "{0}_players" FOREIGN KEY(player_B) REFERENCES "{0}_players"
ON DELETE CASCADE ON DELETE CASCADE
) STRICT; ) STRICT;
CREATE INDEX "{0}_network_A" ON "{0}_network" (player_A);
CREATE INDEX "{0}_network_B" ON "{0}_network" (player_B);"#, CREATE INDEX "{0}_network_B" ON "{0}_network" (player_B);"#,
dataset dataset
); );
@ -198,17 +201,19 @@ CREATE INDEX "{0}_network_B" ON "{0}_network" (player_B);"#,
.prepare(query1)? .prepare(query1)?
.into_iter() .into_iter()
.bind((1, dataset))? .bind((1, dataset))?
.bind((2, metadata.last_sync.0 as i64))? .bind((2, metadata.start.0 as i64))?
.bind((3, metadata.game_id.0 as i64))? .bind((3, metadata.end.map(|x| x.0 as i64)))?
.bind((4, &metadata.game_name[..]))? .bind((4, metadata.last_sync.0 as i64))?
.bind((5, &metadata.game_slug[..]))? .bind((5, metadata.game_id.0 as i64))?
.bind((6, metadata.country.as_deref()))? .bind((6, &metadata.game_name[..]))?
.bind((7, metadata.state.as_deref()))? .bind((7, &metadata.game_slug[..]))?
.bind((8, metadata.set_limit as i64))? .bind((8, metadata.country.as_deref()))?
.bind((9, metadata.decay_rate))? .bind((9, metadata.state.as_deref()))?
.bind((10, metadata.adj_decay_rate))? .bind((10, metadata.set_limit as i64))?
.bind((11, metadata.period))? .bind((11, metadata.decay_rate))?
.bind((12, metadata.tau))? .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)
@ -228,6 +233,10 @@ pub fn get_metadata(
.map(|r| { .map(|r| {
let r_ = r?; let r_ = r?;
Ok(DatasetMetadata { Ok(DatasetMetadata {
start: Timestamp(r_.read::<i64, _>("start") as u64),
end: r_
.read::<Option<i64>, _>("end")
.map(|x| Timestamp(x as u64)),
last_sync: Timestamp(r_.read::<i64, _>("last_sync") as u64), last_sync: Timestamp(r_.read::<i64, _>("last_sync") as u64),
game_id: VideogameId(r_.read::<i64, _>("game_id") as u64), game_id: VideogameId(r_.read::<i64, _>("game_id") as u64),
game_name: r_.read::<&str, _>("game_name").to_owned(), game_name: r_.read::<&str, _>("game_name").to_owned(),
@ -612,7 +621,7 @@ pub fn hypothetical_advantage(
decay_rate: f64, decay_rate: f64,
adj_decay_rate: f64, adj_decay_rate: f64,
) -> sqlite::Result<f64> { ) -> sqlite::Result<f64> {
use std::collections::{HashMap, HashSet}; use std::collections::{HashSet, VecDeque};
// Check trivial cases // Check trivial cases
if player1 == player2 || either_isolated(connection, dataset, player1, player2)? { if player1 == player2 || either_isolated(connection, dataset, player1, player2)? {
@ -620,49 +629,63 @@ pub fn hypothetical_advantage(
} }
let mut visited: HashSet<PlayerId> = HashSet::new(); let mut visited: HashSet<PlayerId> = HashSet::new();
let mut queue: HashMap<PlayerId, (f64, f64)> = HashMap::from([(player1, (0.0, 1.0))]); let mut queue: VecDeque<(PlayerId, Vec<(f64, f64)>)> =
VecDeque::from([(player1, Vec::from([(0.0, 1.0)]))]);
let mut final_paths = Vec::new();
while !queue.is_empty() && final_paths.len() < 100 {
let (visiting, paths) = queue.pop_front().unwrap();
while !queue.is_empty() {
let visiting = *queue
.iter()
.max_by(|a, b| a.1 .1.partial_cmp(&b.1 .1).unwrap())
.unwrap()
.0;
let (adv_v, decay_v) = queue.remove(&visiting).unwrap();
let connections = get_edges(connection, dataset, visiting)?; let connections = get_edges(connection, dataset, visiting)?;
for (id, adv, sets) in connections for (id, adv, sets) in connections
.into_iter() .into_iter()
.filter(|(id, _, _)| !visited.contains(id)) .filter(|(id, _, _)| !visited.contains(id))
{ {
let advantage = adv_v + adv; let rf = if id == player2 {
&mut final_paths
} else if let Some(r) = queue.iter_mut().find(|(id_, _)| id == *id_) {
&mut r.1
} else {
queue.push_back((id, Vec::new()));
&mut queue.back_mut().unwrap().1
};
if id == player2 { if rf.len() < 100 {
return Ok(advantage * decay_v); let decay = if sets >= set_limit {
}
let decay = decay_v
* if sets >= set_limit {
decay_rate decay_rate
} else { } else {
adj_decay_rate adj_decay_rate
}; };
let iter = paths.iter().map(|(a, d)| (a + adv, d * decay));
if queue rf.extend(iter);
.get(&id) rf.truncate(100);
.map(|(_, decay_old)| *decay_old < decay)
.unwrap_or(true)
{
queue.insert(id, (advantage, decay));
} }
} }
visited.insert(visiting); visited.insert(visiting);
} }
// No path found let max_decay = final_paths
.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
Ok(0.0) Ok(0.0)
} }
}
pub fn initialize_edge( pub fn initialize_edge(
connection: &Connection, connection: &Connection,
@ -739,6 +762,8 @@ CREATE TABLE IF NOT EXISTS sets (
pub fn metadata() -> DatasetMetadata { pub fn metadata() -> DatasetMetadata {
DatasetMetadata { DatasetMetadata {
start: Timestamp(1),
end: None,
last_sync: Timestamp(1), last_sync: Timestamp(1),
game_id: VideogameId(0), game_id: VideogameId(0),
game_name: String::from("Test Game"), game_name: String::from("Test Game"),

View file

@ -1,10 +1,10 @@
#![feature(iterator_try_collect)] #![feature(iterator_try_collect)]
#![feature(extend_one)] #![feature(extend_one)]
use chrono::{Local, TimeZone, Utc};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use sqlite::*; use sqlite::*;
use std::path::PathBuf; use std::{cmp::min, path::PathBuf};
use time_format::strftime_utc;
mod queries; mod queries;
use queries::*; use queries::*;
@ -117,7 +117,6 @@ fn main() {
let connection = let connection =
open_datasets(&config_dir).unwrap_or_else(|_| error("Could not open datasets file", 2)); open_datasets(&config_dir).unwrap_or_else(|_| error("Could not open datasets file", 2));
#[allow(unreachable_patterns)]
match cli.subcommand { match cli.subcommand {
Subcommands::Dataset { Subcommands::Dataset {
subcommand: DatasetSC::List, subcommand: DatasetSC::List,
@ -145,7 +144,12 @@ fn main() {
sync(&connection, get_auth_token(&config_dir), datasets, all) sync(&connection, get_auth_token(&config_dir), datasets, all)
} }
_ => println!("This feature is currently unimplemented."), Subcommands::Ranking {
subcommand: RankingSC::Create,
dataset,
} => ranking_create(&connection, dataset),
_ => eprintln!("This feature is currently unimplemented."),
} }
} }
@ -156,7 +160,7 @@ fn dataset_list(connection: &Connection) {
for (name, metadata) in datasets { for (name, metadata) in datasets {
print!( print!(
"\n· \x1b[1m\x1b[34m{}\x1b[0m "· \x1b[1m\x1b[34m{}\x1b[0m
\x1b[4m\x1b]8;;https://www.start.gg/{}\x1b\\{}\x1b]8;;\x1b\\\x1b[0m ", \x1b[4m\x1b]8;;https://www.start.gg/{}\x1b\\{}\x1b]8;;\x1b\\\x1b[0m ",
name, metadata.game_slug, metadata.game_name name, metadata.game_slug, metadata.game_name
); );
@ -171,15 +175,42 @@ fn dataset_list(connection: &Connection) {
println!("(Global)"); println!("(Global)");
} }
if metadata.last_sync.0 == 1 { 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"); print!("\x1b[1m\x1b[91mUnsynced\x1b[0m");
} else if Some(metadata.last_sync) == metadata.end {
print!("\x1b[1m\x1b[92mComplete\x1b[0m");
} else { } else {
print!( print!(
"\x1b[1mLast synced:\x1b[0m {}", "\x1b[1mLast synced:\x1b[0m {}",
strftime_utc("%b %e, %Y %I:%M %p", metadata.last_sync.0 as i64).unwrap() 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 { if current_time().0 - metadata.last_sync.0 > SECS_IN_WEEK
&& Some(metadata.last_sync) != metadata.end
{
if name == "default" { if name == "default" {
print!(" - \x1b[33mRun 'startrnr sync' to update!\x1b[0m"); print!(" - \x1b[33mRun 'startrnr sync' to update!\x1b[0m");
} else { } else {
@ -204,7 +235,7 @@ fn dataset_list(connection: &Connection) {
"\x1b[1mRating Period:\x1b[0m {} days", "\x1b[1mRating Period:\x1b[0m {} days",
metadata.period / SECS_IN_DAY as f64 metadata.period / SECS_IN_DAY as f64
); );
println!("\x1b[1mTau Constant:\x1b[0m {}", metadata.tau); println!("\x1b[1mTau Constant:\x1b[0m {}\n", metadata.tau);
} }
} }
@ -300,6 +331,70 @@ State/province to track ratings for (leave empty for none): "
None 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);
}
};
// Set Limit // Set Limit
let mut set_limit = 0; let mut set_limit = 0;
@ -426,7 +521,9 @@ Tau constant (default 0.4): "
connection, connection,
&name, &name,
DatasetMetadata { DatasetMetadata {
last_sync: Timestamp(1), start,
end,
last_sync: start,
game_id, game_id,
game_name, game_name,
game_slug, game_slug,
@ -496,7 +593,6 @@ fn player_info(connection: &Connection, dataset: Option<String>, player: String)
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));
println!();
if let Some(pre) = prefix { if let Some(pre) = prefix {
print!("\x1b[2m{}\x1b[22m ", pre); print!("\x1b[2m{}\x1b[22m ", pre);
} }
@ -518,7 +614,6 @@ fn player_info(connection: &Connection, dataset: Option<String>, player: String)
println!("\x1b[1mVolatility:\x1b[0m {}", volatility); println!("\x1b[1mVolatility:\x1b[0m {}", volatility);
} }
// TODO: Finish
fn player_matchup( fn player_matchup(
connection: &Connection, connection: &Connection,
dataset: Option<String>, dataset: Option<String>,
@ -583,7 +678,6 @@ fn player_matchup(
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();
println!();
if let Some(pre) = prefix1 { if let Some(pre) = prefix1 {
print!("\x1b[2m{}\x1b[22m ", pre); print!("\x1b[2m{}\x1b[22m ", pre);
} }
@ -663,14 +757,22 @@ fn sync(connection: &Connection, auth: String, datasets: Vec<String>, all: bool)
let current_time = current_time(); let current_time = current_time();
for dataset in datasets { for dataset in datasets {
let dataset_config = get_metadata(connection, &dataset) let dataset_metadata = get_metadata(connection, &dataset)
.expect("Error communicating with SQLite") .expect("Error communicating with SQLite")
.unwrap_or_else(|| error(&format!("Dataset {} does not exist!", dataset), 1)); .unwrap_or_else(|| error(&format!("Dataset {} does not exist!", dataset), 1));
sync_dataset(connection, &dataset, dataset_config, current_time, &auth) 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"); .expect("Error communicating with SQLite");
update_last_sync(connection, &dataset, current_time) update_last_sync(connection, &dataset, before).expect("Error communicating with SQLite");
.expect("Error communicating with SQLite");
} }
} }
fn ranking_create(connection: &Connection, dataset: Option<String>) {
let dataset = dataset.unwrap_or_else(|| String::from("default"));
}

View file

@ -125,7 +125,7 @@ fn get_event_sets(event: EventId, auth: &str) -> Option<Vec<SetData>> {
fn get_tournament_events( fn get_tournament_events(
metadata: &DatasetMetadata, metadata: &DatasetMetadata,
current_time: Timestamp, before: Timestamp,
auth: &str, auth: &str,
) -> Option<Vec<EventData>> { ) -> Option<Vec<EventData>> {
println!("Accessing tournaments..."); println!("Accessing tournaments...");
@ -135,7 +135,7 @@ fn get_tournament_events(
let tour_response = run_query::<TournamentEvents, _>( let tour_response = run_query::<TournamentEvents, _>(
TournamentEventsVars { TournamentEventsVars {
after_date: after, after_date: after,
before_date: current_time, before_date: before,
game_id: metadata.game_id, game_id: metadata.game_id,
country: metadata.country.as_deref(), country: metadata.country.as_deref(),
state: metadata.state.as_deref(), state: metadata.state.as_deref(),
@ -160,7 +160,7 @@ fn get_tournament_events(
let next_response = run_query::<TournamentEvents, _>( let next_response = run_query::<TournamentEvents, _>(
TournamentEventsVars { TournamentEventsVars {
after_date: after, after_date: after,
before_date: current_time, before_date: before,
game_id: metadata.game_id, game_id: metadata.game_id,
country: metadata.country.as_deref(), country: metadata.country.as_deref(),
state: metadata.state.as_deref(), state: metadata.state.as_deref(),
@ -304,10 +304,10 @@ pub fn sync_dataset(
connection: &Connection, connection: &Connection,
dataset: &str, dataset: &str,
metadata: DatasetMetadata, metadata: DatasetMetadata,
current_time: Timestamp, before: Timestamp,
auth: &str, auth: &str,
) -> sqlite::Result<()> { ) -> sqlite::Result<()> {
let events = get_tournament_events(&metadata, current_time, auth) let events = get_tournament_events(&metadata, before, auth)
.unwrap_or_else(|| error("Could not access start.gg", 1)); .unwrap_or_else(|| error("Could not access start.gg", 1));
connection.execute("BEGIN;")?; connection.execute("BEGIN;")?;

View file

@ -10,12 +10,12 @@ 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 fn error(msg: &str, code: i32) -> ! { pub fn error(msg: &str, code: i32) -> ! {
println!("\nERROR: {}", msg); eprintln!("\nERROR: {}", msg);
exit(code) exit(code)
} }
pub fn issue(msg: &str, code: i32) -> ! { pub fn issue(msg: &str, code: i32) -> ! {
println!("\n{}", msg); eprintln!("\n{}", msg);
exit(code) exit(code)
} }