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