Compare commits
10 commits
d33aa89ff6
...
aed935e1ac
Author | SHA1 | Date | |
---|---|---|---|
Kiana Sheibani | aed935e1ac | ||
Kiana Sheibani | ff2ede0178 | ||
Kiana Sheibani | e05f117def | ||
Kiana Sheibani | 3368457096 | ||
Kiana Sheibani | d39c280310 | ||
Kiana Sheibani | 26c2813b09 | ||
Kiana Sheibani | 54696c1b0e | ||
Kiana Sheibani | 47da06e3d6 | ||
Kiana Sheibani | ebec5f086d | ||
Kiana Sheibani | 3e8c79b5f0 |
142
Cargo.lock
generated
142
Cargo.lock
generated
|
@ -29,6 +29,21 @@ version = "0.1.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "anstream"
|
||||
version = "0.5.0"
|
||||
|
@ -155,6 +170,20 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "clap"
|
||||
version = "4.4.5"
|
||||
|
@ -594,6 +623,29 @@ dependencies = [
|
|||
"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]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
|
@ -1154,6 +1206,7 @@ dependencies = [
|
|||
name = "startrnr"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"cynic",
|
||||
"cynic-codegen",
|
||||
|
@ -1163,7 +1216,6 @@ dependencies = [
|
|||
"schema",
|
||||
"serde",
|
||||
"sqlite",
|
||||
"time-format",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1233,12 +1285,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "tinyvec"
|
||||
version = "1.6.0"
|
||||
|
@ -1504,13 +1550,22 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1519,13 +1574,28 @@ version = "0.48.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
"windows_aarch64_gnullvm 0.48.5",
|
||||
"windows_aarch64_msvc 0.48.5",
|
||||
"windows_i686_gnu 0.48.5",
|
||||
"windows_i686_msvc 0.48.5",
|
||||
"windows_x86_64_gnu 0.48.5",
|
||||
"windows_x86_64_gnullvm 0.48.5",
|
||||
"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]]
|
||||
|
@ -1534,42 +1604,84 @@ version = "0.48.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
|
|
|
@ -12,7 +12,7 @@ path = "src/main.rs"
|
|||
[dependencies]
|
||||
# CLI
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
time-format = "1.1"
|
||||
chrono = "0.4"
|
||||
|
||||
# GraphQL schema
|
||||
schema.path = "schema"
|
||||
|
|
28
README.md
28
README.md
|
@ -12,11 +12,11 @@ rankings automatically.
|
|||
|
||||
**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
|
||||
> 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
|
||||
|
||||
|
@ -34,6 +34,30 @@ Alternatively, if you use Nix:
|
|||
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
|
||||
|
||||
StartRNR stores its rating databases in its config directory, which is located at:
|
||||
|
|
117
src/database.rs
117
src/database.rs
|
@ -4,6 +4,8 @@ use std::fs::{self, OpenOptions};
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub struct DatasetMetadata {
|
||||
pub start: Timestamp,
|
||||
pub end: Option<Timestamp>,
|
||||
pub last_sync: Timestamp,
|
||||
|
||||
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> {
|
||||
let path = datasets_path(config_dir).unwrap();
|
||||
|
||||
let query = "PRAGMA foreign_keys = ON;
|
||||
|
||||
let query = "
|
||||
CREATE TABLE IF NOT EXISTS datasets (
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
start INTEGER NOT NULL,
|
||||
end INTEGER,
|
||||
last_sync INTEGER NOT NULL,
|
||||
game_id INTEGER NOT NULL,
|
||||
game_name TEXT NOT NULL,
|
||||
|
@ -68,10 +71,9 @@ CREATE TABLE IF NOT EXISTS events (
|
|||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sets (
|
||||
id TEXT UNIQUE NOT NULL,
|
||||
event INTEGER NOT NULL,
|
||||
FOREIGN KEY(event) REFERENCES events
|
||||
) STRICT;
|
||||
id TEXT PRIMARY KEY,
|
||||
event INTEGER NOT NULL REFERENCES events
|
||||
) STRICT, WITHOUT ROWID;
|
||||
";
|
||||
|
||||
let connection = sqlite::open(path)?;
|
||||
|
@ -102,6 +104,10 @@ pub fn list_datasets(connection: &Connection) -> sqlite::Result<Vec<(String, Dat
|
|||
Ok((
|
||||
r_.read::<&str, _>("name").to_owned(),
|
||||
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),
|
||||
game_id: VideogameId(r_.read::<i64, _>("game_id") as u64),
|
||||
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}';
|
||||
ALTER TABLE "{0}_players" RENAME TO "{1}_players";
|
||||
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";
|
||||
CREATE INDEX "{1}_network_B" ON "{1}_network" (player_B);"#,
|
||||
old, new
|
||||
|
@ -154,10 +158,10 @@ pub fn new_dataset(
|
|||
dataset: &str,
|
||||
metadata: DatasetMetadata,
|
||||
) -> sqlite::Result<()> {
|
||||
let query1 = r#"INSERT INTO datasets VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#;
|
||||
let query1 = r#"INSERT INTO datasets VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#;
|
||||
let query2 = format!(
|
||||
r#"CREATE TABLE "{0}_players" (
|
||||
id INTEGER PRIMARY KEY,
|
||||
id INTEGER PRIMARY KEY REFERENCES players,
|
||||
last_played INTEGER NOT NULL,
|
||||
deviation REAL NOT NULL,
|
||||
volatility REAL NOT NULL,
|
||||
|
@ -182,14 +186,13 @@ CREATE TABLE "{0}_network" (
|
|||
sets TEXT AS (sets_A || sets_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),
|
||||
FOREIGN KEY(player_A) REFERENCES "{0}_players"
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY(player_B) REFERENCES "{0}_players"
|
||||
ON DELETE CASCADE
|
||||
) STRICT;
|
||||
CREATE INDEX "{0}_network_A" ON "{0}_network" (player_A);
|
||||
CREATE INDEX "{0}_network_B" ON "{0}_network" (player_B);"#,
|
||||
dataset
|
||||
);
|
||||
|
@ -198,17 +201,19 @@ CREATE INDEX "{0}_network_B" ON "{0}_network" (player_B);"#,
|
|||
.prepare(query1)?
|
||||
.into_iter()
|
||||
.bind((1, dataset))?
|
||||
.bind((2, metadata.last_sync.0 as i64))?
|
||||
.bind((3, metadata.game_id.0 as i64))?
|
||||
.bind((4, &metadata.game_name[..]))?
|
||||
.bind((5, &metadata.game_slug[..]))?
|
||||
.bind((6, metadata.country.as_deref()))?
|
||||
.bind((7, metadata.state.as_deref()))?
|
||||
.bind((8, metadata.set_limit as i64))?
|
||||
.bind((9, metadata.decay_rate))?
|
||||
.bind((10, metadata.adj_decay_rate))?
|
||||
.bind((11, metadata.period))?
|
||||
.bind((12, metadata.tau))?
|
||||
.bind((2, metadata.start.0 as i64))?
|
||||
.bind((3, metadata.end.map(|x| x.0 as i64)))?
|
||||
.bind((4, metadata.last_sync.0 as i64))?
|
||||
.bind((5, metadata.game_id.0 as i64))?
|
||||
.bind((6, &metadata.game_name[..]))?
|
||||
.bind((7, &metadata.game_slug[..]))?
|
||||
.bind((8, metadata.country.as_deref()))?
|
||||
.bind((9, metadata.state.as_deref()))?
|
||||
.bind((10, metadata.set_limit as i64))?
|
||||
.bind((11, metadata.decay_rate))?
|
||||
.bind((12, metadata.adj_decay_rate))?
|
||||
.bind((13, metadata.period))?
|
||||
.bind((14, metadata.tau))?
|
||||
.try_for_each(|x| x.map(|_| ()))?;
|
||||
|
||||
connection.execute(query2)
|
||||
|
@ -228,6 +233,10 @@ pub fn get_metadata(
|
|||
.map(|r| {
|
||||
let r_ = r?;
|
||||
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),
|
||||
game_id: VideogameId(r_.read::<i64, _>("game_id") as u64),
|
||||
game_name: r_.read::<&str, _>("game_name").to_owned(),
|
||||
|
@ -612,7 +621,7 @@ pub fn hypothetical_advantage(
|
|||
decay_rate: f64,
|
||||
adj_decay_rate: f64,
|
||||
) -> sqlite::Result<f64> {
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::{HashSet, VecDeque};
|
||||
|
||||
// Check trivial cases
|
||||
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 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)?;
|
||||
|
||||
for (id, adv, sets) in connections
|
||||
.into_iter()
|
||||
.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 {
|
||||
return Ok(advantage * decay_v);
|
||||
}
|
||||
|
||||
let decay = decay_v
|
||||
* if sets >= set_limit {
|
||||
if rf.len() < 100 {
|
||||
let decay = if sets >= set_limit {
|
||||
decay_rate
|
||||
} else {
|
||||
adj_decay_rate
|
||||
};
|
||||
let iter = paths.iter().map(|(a, d)| (a + adv, d * decay));
|
||||
|
||||
if queue
|
||||
.get(&id)
|
||||
.map(|(_, decay_old)| *decay_old < decay)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
queue.insert(id, (advantage, decay));
|
||||
rf.extend(iter);
|
||||
rf.truncate(100);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize_edge(
|
||||
connection: &Connection,
|
||||
|
@ -739,6 +762,8 @@ CREATE TABLE IF NOT EXISTS sets (
|
|||
|
||||
pub fn metadata() -> DatasetMetadata {
|
||||
DatasetMetadata {
|
||||
start: Timestamp(1),
|
||||
end: None,
|
||||
last_sync: Timestamp(1),
|
||||
game_id: VideogameId(0),
|
||||
game_name: String::from("Test Game"),
|
||||
|
|
136
src/main.rs
136
src/main.rs
|
@ -1,10 +1,10 @@
|
|||
#![feature(iterator_try_collect)]
|
||||
#![feature(extend_one)]
|
||||
|
||||
use chrono::{Local, TimeZone, Utc};
|
||||
use clap::{Parser, Subcommand};
|
||||
use sqlite::*;
|
||||
use std::path::PathBuf;
|
||||
use time_format::strftime_utc;
|
||||
use std::{cmp::min, path::PathBuf};
|
||||
|
||||
mod queries;
|
||||
use queries::*;
|
||||
|
@ -117,7 +117,6 @@ fn main() {
|
|||
let connection =
|
||||
open_datasets(&config_dir).unwrap_or_else(|_| error("Could not open datasets file", 2));
|
||||
|
||||
#[allow(unreachable_patterns)]
|
||||
match cli.subcommand {
|
||||
Subcommands::Dataset {
|
||||
subcommand: DatasetSC::List,
|
||||
|
@ -145,7 +144,12 @@ fn main() {
|
|||
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 {
|
||||
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 ",
|
||||
name, metadata.game_slug, metadata.game_name
|
||||
);
|
||||
|
@ -171,15 +175,42 @@ fn dataset_list(connection: &Connection) {
|
|||
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");
|
||||
} else if Some(metadata.last_sync) == metadata.end {
|
||||
print!("\x1b[1m\x1b[92mComplete\x1b[0m");
|
||||
} else {
|
||||
print!(
|
||||
"\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" {
|
||||
print!(" - \x1b[33mRun 'startrnr sync' to update!\x1b[0m");
|
||||
} else {
|
||||
|
@ -204,7 +235,7 @@ fn dataset_list(connection: &Connection) {
|
|||
"\x1b[1mRating Period:\x1b[0m {} days",
|
||||
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
|
||||
};
|
||||
|
||||
// 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
|
||||
|
||||
let mut set_limit = 0;
|
||||
|
@ -426,7 +521,9 @@ Tau constant (default 0.4): "
|
|||
connection,
|
||||
&name,
|
||||
DatasetMetadata {
|
||||
last_sync: Timestamp(1),
|
||||
start,
|
||||
end,
|
||||
last_sync: start,
|
||||
game_id,
|
||||
game_name,
|
||||
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)
|
||||
.unwrap_or_else(|_| error("Could not find player", 1));
|
||||
|
||||
println!();
|
||||
if let Some(pre) = prefix {
|
||||
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);
|
||||
}
|
||||
|
||||
// TODO: Finish
|
||||
fn player_matchup(
|
||||
connection: &Connection,
|
||||
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 len2 = prefix2.as_deref().map(|s| s.len() + 1).unwrap_or(0) + name2.len();
|
||||
|
||||
println!();
|
||||
if let Some(pre) = prefix1 {
|
||||
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();
|
||||
|
||||
for dataset in datasets {
|
||||
let dataset_config = get_metadata(connection, &dataset)
|
||||
let dataset_metadata = get_metadata(connection, &dataset)
|
||||
.expect("Error communicating with SQLite")
|
||||
.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");
|
||||
|
||||
update_last_sync(connection, &dataset, current_time)
|
||||
.expect("Error communicating with SQLite");
|
||||
update_last_sync(connection, &dataset, before).expect("Error communicating with SQLite");
|
||||
}
|
||||
}
|
||||
|
||||
fn ranking_create(connection: &Connection, dataset: Option<String>) {
|
||||
let dataset = dataset.unwrap_or_else(|| String::from("default"));
|
||||
}
|
||||
|
|
10
src/sync.rs
10
src/sync.rs
|
@ -125,7 +125,7 @@ fn get_event_sets(event: EventId, auth: &str) -> Option<Vec<SetData>> {
|
|||
|
||||
fn get_tournament_events(
|
||||
metadata: &DatasetMetadata,
|
||||
current_time: Timestamp,
|
||||
before: Timestamp,
|
||||
auth: &str,
|
||||
) -> Option<Vec<EventData>> {
|
||||
println!("Accessing tournaments...");
|
||||
|
@ -135,7 +135,7 @@ fn get_tournament_events(
|
|||
let tour_response = run_query::<TournamentEvents, _>(
|
||||
TournamentEventsVars {
|
||||
after_date: after,
|
||||
before_date: current_time,
|
||||
before_date: before,
|
||||
game_id: metadata.game_id,
|
||||
country: metadata.country.as_deref(),
|
||||
state: metadata.state.as_deref(),
|
||||
|
@ -160,7 +160,7 @@ fn get_tournament_events(
|
|||
let next_response = run_query::<TournamentEvents, _>(
|
||||
TournamentEventsVars {
|
||||
after_date: after,
|
||||
before_date: current_time,
|
||||
before_date: before,
|
||||
game_id: metadata.game_id,
|
||||
country: metadata.country.as_deref(),
|
||||
state: metadata.state.as_deref(),
|
||||
|
@ -304,10 +304,10 @@ pub fn sync_dataset(
|
|||
connection: &Connection,
|
||||
dataset: &str,
|
||||
metadata: DatasetMetadata,
|
||||
current_time: Timestamp,
|
||||
before: Timestamp,
|
||||
auth: &str,
|
||||
) -> 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));
|
||||
|
||||
connection.execute("BEGIN;")?;
|
||||
|
|
|
@ -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 fn error(msg: &str, code: i32) -> ! {
|
||||
println!("\nERROR: {}", msg);
|
||||
eprintln!("\nERROR: {}", msg);
|
||||
exit(code)
|
||||
}
|
||||
|
||||
pub fn issue(msg: &str, code: i32) -> ! {
|
||||
println!("\n{}", msg);
|
||||
eprintln!("\n{}", msg);
|
||||
exit(code)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue