feat: initial implementation
This commit is contained in:
9 files changed
+624
No files matched your search
+15
@@ -0,0 +1,15 @@
|
|||||||
|
#--------------------------------------------------#
|
||||||
|
# The following was generated with gitignore.nvim: #
|
||||||
|
#--------------------------------------------------#
|
||||||
|
# Gitignore for the following technologies: Rust
|
||||||
|
|
||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
debug/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
||||||
Generated
+222
@@ -0,0 +1,222 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "assert_approx_eq"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c07dab4369547dbe5114677b33fbbf724971019f3818172d59a97a61c774ffd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-deque"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"crossbeam-epoch",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-epoch"
|
||||||
|
version = "0.9.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"cfg-if",
|
||||||
|
"crossbeam-utils",
|
||||||
|
"memoffset",
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hikaru"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"assert_approx_eq",
|
||||||
|
"indoc",
|
||||||
|
"lazy_static",
|
||||||
|
"rand",
|
||||||
|
"rayon",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indoc"
|
||||||
|
version = "2.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.150"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memoffset"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon"
|
||||||
|
version = "1.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"rayon-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon-core"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-deque",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.0+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "hikaru"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
assert_approx_eq = "1.1.0"
|
||||||
|
indoc = "2.0.4"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
rand = "0.8.5"
|
||||||
|
rayon = "1.8.0"
|
||||||
|
regex = "1.10.2"
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Stephen Waits <steve@waits.net>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice (including the next
|
||||||
|
paragraph) shall be included in all copies or substantial portions of the
|
||||||
|
Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
This is a hacked together Monte Carlo simulation of Hikaru's chess.com games.
|
||||||
|
|
||||||
|
For details, please see the [accompanying blog post](https://swaits.com/hikaru-winning-streaks/).
|
||||||
|
|
||||||
|
This repo does not include data. To fetch it, you'll need to grab it from
|
||||||
|
chess.com. I did it like this:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fetch the list of game archive URLs for the player 'Hikaru' from Chess.com
|
||||||
|
archive_urls=$(curl -Ls https://api.chess.com/pub/player/Hikaru/games/archives | jq -rc ".archives[]")
|
||||||
|
|
||||||
|
# Iterate over each archive URL
|
||||||
|
for url in $archive_urls; do
|
||||||
|
# Fetch the games from each archive and extract the PGN
|
||||||
|
curl -Ls "$url" | jq -rc ".games[].pgn"
|
||||||
|
done >> games.pgn # Append all PGNs to the 'games.pgn' file
|
||||||
|
```
|
||||||
|
|
||||||
|
Then just:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.sr.ht/~swaits/hikaru
|
||||||
|
cd hikaru
|
||||||
|
cargo run --release
|
||||||
|
```
|
||||||
|
|
||||||
|
If you find mistakes, please let me know. I'll be very grateful.
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use indoc::indoc;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
use crate::elo::Rating;
|
||||||
|
|
||||||
|
// a chess player - the minimum data we care about, just name and rating
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Player {
|
||||||
|
pub name: String,
|
||||||
|
pub rating: Rating,
|
||||||
|
}
|
||||||
|
|
||||||
|
// a single chess game, white/black players and the outcome (actual or simulated)
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Game {
|
||||||
|
pub white: Player,
|
||||||
|
pub black: Player,
|
||||||
|
pub outcome: Outcome,
|
||||||
|
}
|
||||||
|
|
||||||
|
// possible outocmes of a chess game
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Outcome {
|
||||||
|
WhiteWin,
|
||||||
|
BlackWin,
|
||||||
|
Draw,
|
||||||
|
}
|
||||||
|
|
||||||
|
// this regex captures all the possible fields in a PGN as exported from chess.com's API
|
||||||
|
lazy_static! {
|
||||||
|
static ref RE: Regex = Regex::new(indoc! {r#"
|
||||||
|
\[Event\s+"(?P<Event>.+?)"\]\s*
|
||||||
|
\[Site\s+"(?P<Site>.+?)"\]\s*
|
||||||
|
\[Date\s+"(?P<Date>.+?)"\]\s*
|
||||||
|
\[Round\s+"(?P<Round>.+?)"\]\s*
|
||||||
|
\[White\s+"(?P<White>.+?)"\]\s*
|
||||||
|
\[Black\s+"(?P<Black>.+?)"\]\s*
|
||||||
|
\[Result\s+"(?P<Result>.+?)"\]\s*
|
||||||
|
\[CurrentPosition\s+"(?P<CurrentPosition>.+?)"\]\s*
|
||||||
|
\[Timezone\s+"(?P<Timezone>.+?)"\]\s*
|
||||||
|
\[ECO\s+"(?P<ECO>.+?)"\]\s*
|
||||||
|
\[ECOUrl\s+"(?P<ECOUrl>.+?)"\]\s*
|
||||||
|
\[UTCDate\s+"(?P<UTCDate>.+?)"\]\s*
|
||||||
|
\[UTCTime\s+"(?P<UTCTime>.+?)"\]\s*
|
||||||
|
\[WhiteElo\s+"(?P<WhiteElo>.+?)"\]\s*
|
||||||
|
\[BlackElo\s+"(?P<BlackElo>.+?)"\]\s*
|
||||||
|
\[TimeControl\s+"(?P<TimeControl>.+?)"\]\s*
|
||||||
|
\[Termination\s+"(?P<Termination>.+?)"\]\s*
|
||||||
|
\[StartTime\s+"(?P<StartTime>.+?)"\]\s*
|
||||||
|
\[EndDate\s+"(?P<EndDate>.+?)"\]\s*
|
||||||
|
\[EndTime\s+"(?P<EndTime>.+?)"\]\s*
|
||||||
|
\[Link\s+"(?P<Link>.+?)"\]\s*
|
||||||
|
(?P<PGN>.+?)\n
|
||||||
|
"#})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// given a sting (expected to be a dump of chess.com's API), parse and return a list of games
|
||||||
|
pub fn parse_games(input: &str) -> Vec<Game> {
|
||||||
|
let mut games = Vec::new();
|
||||||
|
|
||||||
|
for caps in RE.captures_iter(input) {
|
||||||
|
let white_player = Player {
|
||||||
|
name: caps["White"].to_string(),
|
||||||
|
rating: Rating::from_str(caps["WhiteElo"].to_string().as_str()).unwrap(),
|
||||||
|
};
|
||||||
|
let black_player = Player {
|
||||||
|
name: caps["Black"].to_string(),
|
||||||
|
// rating: caps["BlackElo"].to_string().as_str().into(),
|
||||||
|
rating: Rating::from_str(caps["BlackElo"].to_string().as_str()).unwrap(),
|
||||||
|
};
|
||||||
|
let outcome = match &caps["Result"] {
|
||||||
|
"1-0" => Outcome::WhiteWin,
|
||||||
|
"0-1" => Outcome::BlackWin,
|
||||||
|
"1/2-1/2" => Outcome::Draw,
|
||||||
|
_ => continue, // or handle unknown result
|
||||||
|
};
|
||||||
|
|
||||||
|
games.push(Game {
|
||||||
|
white: white_player,
|
||||||
|
black: black_player,
|
||||||
|
outcome,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
games
|
||||||
|
}
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
// standard imports
|
||||||
|
use std::{num::ParseIntError, str::FromStr};
|
||||||
|
|
||||||
|
// crate imports
|
||||||
|
use assert_approx_eq::assert_approx_eq;
|
||||||
|
|
||||||
|
// probabilities of white or black winning or drawing
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ExpectedOutcome {
|
||||||
|
pub white: f64,
|
||||||
|
pub black: f64,
|
||||||
|
pub draw: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
// a new type for an Elo rating
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Rating(pub u32);
|
||||||
|
|
||||||
|
// create a `Rating` from a string (used during loading/parsing)
|
||||||
|
impl FromStr for Rating {
|
||||||
|
type Err = ParseIntError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let num = s.parse::<u32>()?; // Or whatever the primitive type should be
|
||||||
|
Ok(Self(num)) // Replace with the correct method to construct a Rating
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// estimate probability of draw given two Elo ratings
|
||||||
|
fn probability_of_draw(white_elo: &Rating, black_elo: &Rating) -> f64 {
|
||||||
|
// Constants for draw probability estimation
|
||||||
|
let max_draw_probability = 0.18; // Upper limit of draw probability
|
||||||
|
let rating_difference_sensitivity = 0.05; // Adjusts sensitivity to rating difference
|
||||||
|
let rating_diff = (black_elo.0 as f64 - white_elo.0 as f64).abs();
|
||||||
|
max_draw_probability / (1.0 + rating_difference_sensitivity * rating_diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// estimate probability of white winning, given two Elo ratings
|
||||||
|
fn probability_of_white_win(white_elo: &Rating, black_elo: &Rating) -> f64 {
|
||||||
|
// Expected outcomes for players A and B
|
||||||
|
// E_a = 1 / (1 + 10^((b - a) / 400)
|
||||||
|
// E_b = 1 - E_a
|
||||||
|
1.0 / (1.0 + 10.0f64.powf((black_elo.0 as f64 - white_elo.0 as f64) / 400.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
// estimate probability of black winning given white's probability of winning
|
||||||
|
fn probability_of_black_win(probability_of_white_win: f64) -> f64 {
|
||||||
|
1.0 - probability_of_white_win
|
||||||
|
}
|
||||||
|
|
||||||
|
// given two ratings, compute probabilities of win/loss/draw and return in `ExpectedOutcome`
|
||||||
|
pub fn calculate_expected_outcome(white_elo: &Rating, black_elo: &Rating) -> ExpectedOutcome {
|
||||||
|
// Estimate win probabilities
|
||||||
|
let mut p_white = probability_of_white_win(white_elo, black_elo);
|
||||||
|
let mut p_black = probability_of_black_win(p_white);
|
||||||
|
|
||||||
|
// Estimate draw probability based on rating difference
|
||||||
|
let p_draw = probability_of_draw(white_elo, black_elo);
|
||||||
|
|
||||||
|
// Adjust win probabilities to account for draw probability
|
||||||
|
p_white *= 1.0 - p_draw;
|
||||||
|
p_black *= 1.0 - p_draw;
|
||||||
|
|
||||||
|
// Sanity check
|
||||||
|
assert_approx_eq!((p_white + p_black + p_draw), 1.0);
|
||||||
|
|
||||||
|
ExpectedOutcome {
|
||||||
|
// Estimate win probabilities
|
||||||
|
white: p_white,
|
||||||
|
black: p_black,
|
||||||
|
draw: p_draw,
|
||||||
|
}
|
||||||
|
}
|
||||||
+124
@@ -0,0 +1,124 @@
|
|||||||
|
// project modules
|
||||||
|
mod chess;
|
||||||
|
mod elo;
|
||||||
|
mod stochastic;
|
||||||
|
|
||||||
|
// standard imports
|
||||||
|
use std::{collections::HashMap, fs, sync::Arc};
|
||||||
|
|
||||||
|
// crate imports
|
||||||
|
use rayon::prelude::*;
|
||||||
|
|
||||||
|
// project imports
|
||||||
|
use chess::{parse_games, Game, Outcome};
|
||||||
|
use stochastic::simulate_games;
|
||||||
|
|
||||||
|
// load games into `Vec<Game>` from the chess.com API output
|
||||||
|
fn load_games() -> Vec<Game> {
|
||||||
|
let filename = "games.pgn";
|
||||||
|
|
||||||
|
// Read the contents of the file into a string
|
||||||
|
let contents = fs::read_to_string(filename).unwrap();
|
||||||
|
|
||||||
|
// Parse the contents into a Vec<Game>
|
||||||
|
parse_games(&contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
// find all the win streaks in a `Vec<Game>` for players matching `player_name`
|
||||||
|
fn generate_win_streak_histogram(player_name: &str, games: &[Game]) -> HashMap<usize, usize> {
|
||||||
|
let mut histogram = HashMap::new();
|
||||||
|
let mut current_streak = 0;
|
||||||
|
|
||||||
|
for game in games {
|
||||||
|
let is_player_white = game.white.name == player_name;
|
||||||
|
let is_player_black = game.black.name == player_name;
|
||||||
|
|
||||||
|
assert!(is_player_white || is_player_black);
|
||||||
|
if !(is_player_white || is_player_black) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let won_as_white = is_player_white && matches!(game.outcome, Outcome::WhiteWin);
|
||||||
|
let won_as_black = is_player_black && matches!(game.outcome, Outcome::BlackWin);
|
||||||
|
|
||||||
|
if won_as_white || won_as_black {
|
||||||
|
// our player won
|
||||||
|
current_streak += 1;
|
||||||
|
} else if current_streak > 0 {
|
||||||
|
// our playe lost or drew, record streak and reset it
|
||||||
|
*histogram.entry(current_streak).or_insert(0) += 1;
|
||||||
|
current_streak = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the last streak if the games list ends on a winning streak
|
||||||
|
if current_streak > 0 {
|
||||||
|
*histogram.entry(current_streak).or_insert(0) += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
histogram
|
||||||
|
}
|
||||||
|
|
||||||
|
// dump the histogram to stdout
|
||||||
|
fn print_histogram(histogram: &HashMap<usize, usize>, divisor: Option<u32>) {
|
||||||
|
let max_streak = histogram.keys().max().cloned().unwrap_or(0);
|
||||||
|
|
||||||
|
println!("Win Streak Histogram:");
|
||||||
|
for streak_length in 1..=max_streak {
|
||||||
|
let count = histogram.get(&streak_length).cloned().unwrap_or(0);
|
||||||
|
|
||||||
|
match divisor {
|
||||||
|
Some(div) => {
|
||||||
|
let expected_count = (count as f64) / (div as f64);
|
||||||
|
let probability = expected_count.min(1.0) * 100.0;
|
||||||
|
println!(
|
||||||
|
"Streak Length: {:>3}, Expected Count: {:>5.0}, Probability: {:>6.2}%",
|
||||||
|
streak_length, expected_count, probability
|
||||||
|
);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
println!("Streak Length: {:>3}, Count: {}", streak_length, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// load actual games
|
||||||
|
println!("Loading...");
|
||||||
|
let games = Arc::new(load_games()); // Arc because of rayon use below
|
||||||
|
println!("Loaded {} games", games.len());
|
||||||
|
println!();
|
||||||
|
|
||||||
|
println!("Actual Results Histogram:");
|
||||||
|
let histogram = generate_win_streak_histogram("Hikaru", &games);
|
||||||
|
print_histogram(&histogram, None);
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// Run the simulations in parallel and collect the histograms
|
||||||
|
println!("Simulating...");
|
||||||
|
println!();
|
||||||
|
let iterations = 10_000;
|
||||||
|
let histograms: Vec<HashMap<usize, usize>> = (0..iterations)
|
||||||
|
.into_par_iter()
|
||||||
|
.map(|_| {
|
||||||
|
let simulated_games = simulate_games(&games);
|
||||||
|
generate_win_streak_histogram("Hikaru", &simulated_games)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Reduce the histograms into a single histogram
|
||||||
|
let merged_histogram = histograms
|
||||||
|
.into_iter()
|
||||||
|
.reduce(|mut acc, h| {
|
||||||
|
for (k, v) in h {
|
||||||
|
*acc.entry(k).or_insert(0) += v;
|
||||||
|
}
|
||||||
|
acc
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// And print it!
|
||||||
|
println!("Simulated Results ({} games):", iterations);
|
||||||
|
print_histogram(&merged_histogram, Some(iterations));
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
chess::{Game, Outcome},
|
||||||
|
elo::{calculate_expected_outcome, Rating},
|
||||||
|
};
|
||||||
|
|
||||||
|
// simulates a single game
|
||||||
|
fn simulate_game(white_elo: &Rating, black_elo: &Rating) -> Outcome {
|
||||||
|
// Get expected outcomes
|
||||||
|
let e = calculate_expected_outcome(white_elo, black_elo);
|
||||||
|
|
||||||
|
// Roll the dice
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let random_value = rng.gen::<f64>();
|
||||||
|
|
||||||
|
// Determine and return outcome
|
||||||
|
if random_value <= e.white {
|
||||||
|
Outcome::WhiteWin
|
||||||
|
} else if random_value <= (e.white + e.black) {
|
||||||
|
Outcome::BlackWin
|
||||||
|
} else {
|
||||||
|
Outcome::Draw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulate a series of games and return a new simulated `Vec<Game>` series
|
||||||
|
pub fn simulate_games(actual: &[Game]) -> Vec<Game> {
|
||||||
|
actual
|
||||||
|
.iter()
|
||||||
|
.map(|g| Game {
|
||||||
|
white: g.white.to_owned(),
|
||||||
|
black: g.black.to_owned(),
|
||||||
|
outcome: simulate_game(&g.white.rating, &g.black.rating),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
Reference in new issue
Block a user