Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add personal card ratings panel and minor improvements #470

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
293 changes: 140 additions & 153 deletions src-tauri/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ use specta::Type;
use std::{
fs::{remove_file, File, OpenOptions},
path::PathBuf,
sync::atomic::{AtomicI32, AtomicUsize, Ordering},
sync::atomic::{AtomicUsize, Ordering},
time::{Duration, Instant},
};
use std::{
Expand Down Expand Up @@ -1123,27 +1123,52 @@ pub async fn get_tournaments(

#[derive(Debug, Clone, Serialize, Type, Default)]
pub struct PlayerGameInfo {
pub won: i32,
pub lost: i32,
pub draw: i32,
pub data_per_month: Vec<(String, MonthData)>,
pub white_openings: Vec<(String, Results)>,
pub black_openings: Vec<(String, Results)>,
pub site_stats_data: Vec<SiteStatsData>,
}

#[derive(Debug, Clone, Serialize, Type, Default, Eq, Ord, PartialEq, PartialOrd)]
pub struct Results {
pub won: i32,
pub lost: i32,
pub draw: i32,
#[derive(Debug, Clone, Serialize, Deserialize, Default, Type)]
#[repr(u8)] // Ensure minimal memory usage (as u8)
pub enum GameOutcome {
#[default]
Won = 0,
Drawn = 1,
Lost = 2,
}

impl GameOutcome {
pub fn from_str(result_str: &str, is_white: bool) -> Option<Self> {
match result_str {
"1-0" => Some(if is_white {
GameOutcome::Won
} else {
GameOutcome::Lost
}),
"1/2-1/2" => Some(GameOutcome::Drawn),
"0-1" => Some(if is_white {
GameOutcome::Lost
} else {
GameOutcome::Won
}),
_ => None,
}
}
}

#[derive(Debug, Clone, Serialize, Type, Default)]
pub struct SiteStatsData {
pub site: String,
pub player: String,
pub data: Vec<StatsData>,
}

#[derive(Debug, Clone, Serialize, Type, Default)]
pub struct MonthData {
pub count: i32,
pub avg_elo: i32,
#[serde(skip)]
avg_count: i32,
pub struct StatsData {
pub date: String,
pub is_player_white: bool,
pub player_elo: i32,
pub result: GameOutcome,
pub time_control: String,
pub opening: String,
}

#[derive(Serialize, Debug, Clone, Type, tauri_specta::Event)]
Expand All @@ -1164,6 +1189,8 @@ pub async fn get_players_game_info(
let timer = Instant::now();

let sql_query = games::table
.inner_join(sites::table.on(games::site_id.eq(sites::id)))
.inner_join(players::table.on(players::id.eq(id)))
.select((
games::white_id,
games::black_id,
Expand All @@ -1172,6 +1199,9 @@ pub async fn get_players_game_info(
games::moves,
games::white_elo,
games::black_elo,
games::time_control,
sites::name,
players::name,
))
.filter(games::white_id.eq(id).or(games::black_id.eq(id)))
.filter(games::fen.is_null());
Expand All @@ -1184,159 +1214,116 @@ pub async fn get_players_game_info(
Vec<u8>,
Option<i32>,
Option<i32>,
Option<String>,
Option<String>,
Option<String>,
);
let info: Vec<GameInfo> = sql_query.load(db)?;

let mut game_info = PlayerGameInfo::default();
let white_openings = DashMap::new();
let black_openings = DashMap::new();
let won = AtomicI32::new(0);
let lost = AtomicI32::new(0);
let draw = AtomicI32::new(0);
let data_per_month = DashMap::new();
let progress = AtomicUsize::new(0);

info.par_iter().for_each(
|(white_id, black_id, outcome, date, moves, white_elo, black_elo)| {
let is_white = *white_id == id;
assert!(is_white || *black_id == id);

let mut setups = vec![];
let mut chess = Chess::default();
for (i, byte) in moves.iter().enumerate() {
if i > 54 {
// max length of opening in data
break;
game_info.site_stats_data = info
.par_iter()
.filter_map(
|(
white_id,
black_id,
outcome,
date,
moves,
white_elo,
black_elo,
time_control,
site,
player,
)| {
let is_white = *white_id == id;
let is_black = *black_id == id;
let result = GameOutcome::from_str(outcome.as_deref()?, is_white);

if !is_white && !is_black
|| is_white && white_elo.is_none()
|| is_black && black_elo.is_none()
|| result.is_none()
|| date.is_none()
|| site.is_none()
|| player.is_none()
{
return None;
}
let m = decode_move(*byte, &chess).unwrap();
chess.play_unchecked(&m);
setups.push(chess.clone().into_setup(EnPassantMode::Legal));
}

setups.reverse();
for setup in setups {
if let Ok(opening) = get_opening_from_setup(setup) {
let openings = if is_white {
&white_openings
let site = site.as_deref().map(|s| {
if s.starts_with("https://lichess.org/") {
"Lichess".to_string()
} else {
&black_openings
};
if outcome.as_deref() == Some("1-0") {
openings
.entry(opening)
.and_modify(|e: &mut Results| {
if is_white {
e.won += 1;
} else {
e.lost += 1;
}
})
.or_insert(Results {
won: 1,
lost: 0,
draw: 0,
});
} else if outcome.as_deref() == Some("0-1") {
openings
.entry(opening)
.and_modify(|e| {
if is_white {
e.lost += 1;
} else {
e.won += 1;
}
})
.or_insert(Results {
won: 0,
lost: 1,
draw: 0,
});
} else if outcome.as_deref() == Some("1/2-1/2") {
openings
.entry(opening)
.and_modify(|e| {
e.draw += 1;
})
.or_insert(Results {
won: 0,
lost: 0,
draw: 1,
});
s.to_string()
}

break;
})?;

let mut setups = vec![];
let mut chess = Chess::default();
for (i, byte) in moves.iter().enumerate() {
if i > 54 {
// max length of opening in data
break;
}
let m = decode_move(*byte, &chess).unwrap();
chess.play_unchecked(&m);
setups.push(chess.clone().into_setup(EnPassantMode::Legal));
}
}

if let Some(date) = date {
let date = match NaiveDate::parse_from_str(date, "%Y.%m.%d") {
Ok(date) => date,
Err(_) => return,
};
let month = date.format("%Y-%m").to_string();

// update count and avg elo
let mut month_data = data_per_month.entry(month).or_insert(MonthData::default());
month_data.count += 1;
let elo = if is_white { white_elo } else { black_elo };
if let Some(elo) = elo {
month_data.avg_elo += elo;
month_data.avg_count += 1;
setups.reverse();
let opening = setups
.iter()
.find_map(|setup| get_opening_from_setup(setup.clone()).ok())
.unwrap_or_default();

let p = progress.fetch_add(1, Ordering::Relaxed);
if p % 1000 == 0 || p == info.len() - 1 {
let _ = DatabaseProgress {
id: id.to_string(),
progress: (p as f64 / info.len() as f64) * 100_f64,
}
.emit(&app);
}
}
match outcome.as_deref() {
Some("1-0") => match is_white {
true => won.fetch_add(1, Ordering::Relaxed),
false => lost.fetch_add(1, Ordering::Relaxed),
},
Some("0-1") => match is_white {
true => lost.fetch_add(1, Ordering::Relaxed),
false => won.fetch_add(1, Ordering::Relaxed),
},
Some("1/2-1/2") => draw.fetch_add(1, Ordering::Relaxed),
_ => 0,
};

let p = progress.fetch_add(1, Ordering::Relaxed);
if p % 1000 == 0 || p == info.len() - 1 {
let _ = DatabaseProgress {
id: id.to_string(),
progress: (p as f64 / info.len() as f64) * 100_f64,
Some(SiteStatsData {
site: site.clone(),
player: player.clone().unwrap(),
data: vec![StatsData {
date: date.clone().unwrap(),
is_player_white: is_white,
player_elo: if is_white {
white_elo.unwrap()
} else {
black_elo.unwrap()
},
result: result.unwrap(),
time_control: time_control.clone().unwrap_or_default(),
opening,
}],
})
},
)
.fold(|| DashMap::new(), |acc, data| {
acc.entry((data.site.clone(), data.player.clone()))
.or_insert_with(Vec::new)
.extend(data.data);
acc
})
.reduce(|| DashMap::new(), |acc1, acc2| {
for ((site, player), data) in acc2 {
acc1.entry((site, player))
.or_insert_with(Vec::new)
.extend(data);
}
.emit(&app);
}
},
);
game_info.white_openings = white_openings.into_iter().collect();
game_info.black_openings = black_openings.into_iter().collect();
game_info.won = won.into_inner();
game_info.lost = lost.into_inner();
game_info.draw = draw.into_inner();
game_info.data_per_month = data_per_month.into_iter().collect();
game_info.data_per_month = game_info
.data_per_month
acc1
},
)
.into_iter()
.map(|(month, data)| {
let avg_elo = if data.avg_count == 0 {
0
} else {
data.avg_elo / data.avg_count
};
(
month,
MonthData {
count: data.count,
avg_elo,
avg_count: data.avg_count,
},
)
})
.map(|((site, player), data)| SiteStatsData { site, player, data })
.collect();

// sort openings by count
game_info.white_openings.sort_by(|(_, a), (_, b)| b.cmp(a));
game_info.black_openings.sort_by(|(_, a), (_, b)| b.cmp(a));

println!("get_players_game_info {:?}: {:?}", file, timer.elapsed());

Ok(game_info)
Expand Down
7 changes: 4 additions & 3 deletions src/bindings/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,16 +386,16 @@ export type EngineOptions = { fen: string; moves: string[]; extraOptions: Engine
export type Event = { id: number; name: string | null }
export type FidePlayer = { fideid: number; name: string; country: string; sex: string; title: string | null; w_title: string | null; o_title: string | null; foa_title: string | null; rating: number | null; games: number | null; k: number | null; rapid_rating: number | null; rapid_games: number | null; rapid_k: number | null; blitz_rating: number | null; blitz_games: number | null; blitz_k: number | null; birthday: number | null; flag: string | null }
export type FileMetadata = { last_modified: number }
export type GameOutcome = "Won" | "Drawn" | "Lost"
export type GameQueryJs = { options?: QueryOptions<GameSort> | null; player1?: number | null; player2?: number | null; tournament_id?: number | null; start_date?: string | null; end_date?: string | null; range1?: [number, number] | null; range2?: [number, number] | null; sides?: Sides | null; outcome?: string | null; position?: PositionQueryJs | null }
export type GameSort = "id" | "date" | "whiteElo" | "blackElo" | "ply_count"
export type GoMode = { t: "PlayersTime"; c: PlayersTime } | { t: "Depth"; c: number } | { t: "Time"; c: number } | { t: "Nodes"; c: number } | { t: "Infinite" }
export type MonthData = { count: number; avg_elo: number }
export type MoveAnalysis = { best: BestMoves[]; novelty: boolean; is_sacrifice: boolean }
export type NormalizedGame = { id: number; fen: string; event: string; event_id: number; site: string; site_id: number; date?: string | null; time?: string | null; round?: string | null; white: string; white_id: number; white_elo?: number | null; black: string; black_id: number; black_elo?: number | null; result: Outcome; time_control?: string | null; eco?: string | null; ply_count?: number | null; moves: string }
export type OutOpening = { name: string; fen: string }
export type Outcome = "1-0" | "0-1" | "1/2-1/2" | "*"
export type Player = { id: number; name: string | null; elo: number | null }
export type PlayerGameInfo = { won: number; lost: number; draw: number; data_per_month: ([string, MonthData])[]; white_openings: ([string, Results])[]; black_openings: ([string, Results])[] }
export type PlayerGameInfo = { site_stats_data: SiteStatsData[] }
export type PlayerQuery = { options: QueryOptions<PlayerSort>; name?: string | null; range?: [number, number] | null }
export type PlayerSort = "id" | "name" | "elo"
export type PlayersTime = { white: number; black: number; winc: number; binc: number }
Expand All @@ -406,7 +406,6 @@ export type PuzzleDatabaseInfo = { title: string; description: string; puzzleCou
export type QueryOptions<SortT> = { skipCount: boolean; page?: number | null; pageSize?: number | null; sort: SortT; direction: SortDirection }
export type QueryResponse<T> = { data: T; count: number | null }
export type ReportProgress = { progress: number; id: string; finished: boolean }
export type Results = { won: number; lost: number; draw: number }
export type Score = { value: ScoreValue;
/**
* The probability of each result (win, draw, loss).
Expand All @@ -422,7 +421,9 @@ export type ScoreValue =
*/
{ type: "mate"; value: number }
export type Sides = "BlackWhite" | "WhiteBlack" | "Any"
export type SiteStatsData = { site: string; player: string; data: StatsData[] }
export type SortDirection = "asc" | "desc"
export type StatsData = { date: string; is_player_white: boolean; player_elo: number; result: GameOutcome; time_control: string; opening: string }
export type Token = { type: "ParenOpen" } | { type: "ParenClose" } | { type: "Comment"; value: string } | { type: "San"; value: string } | { type: "Header"; value: { tag: string; value: string } } | { type: "Nag"; value: string } | { type: "Outcome"; value: string }
export type TournamentQuery = { options: QueryOptions<TournamentSort>; name: string | null }
export type TournamentSort = "id" | "name"
Expand Down
Loading