use std::collections::HashMap;
use std::io::{self, BufRead, BufReader, Seek, SeekFrom};
use std::fs::File;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Puzzle {
pub id: String,
pub fen: String,
pub moves: Vec<String>,
pub rating: u16,
pub themes: Vec<String>,
}
impl Puzzle {
pub fn solution_moves(&self) -> &[String] {
if self.moves.len() > 1 {
&self.moves[1..]
} else {
&[]
}
}
fn from_csv_line(line: &str) -> Option<Self> {
let mut fields = CsvFieldIter::new(line);
let id = fields.next()?;
let fen = fields.next()?;
let moves_str = fields.next()?;
let rating_str = fields.next()?;
fields.next()?;
fields.next()?;
fields.next()?;
let themes_str = fields.next()?;
let rating: u16 = rating_str.parse().ok()?;
let moves: Vec<String> = moves_str.split_whitespace().map(String::from).collect();
let themes: Vec<String> = themes_str.split_whitespace().map(String::from).collect();
Some(Puzzle {
id: id.to_string(),
fen: fen.to_string(),
moves,
rating,
themes,
})
}
}
struct CsvFieldIter<'a> {
remaining: &'a str,
}
impl<'a> CsvFieldIter<'a> {
fn new(line: &'a str) -> Self {
Self { remaining: line }
}
fn next(&mut self) -> Option<&'a str> {
if self.remaining.is_empty() {
return None;
}
match self.remaining.find(',') {
Some(pos) => {
let field = &self.remaining[..pos];
self.remaining = &self.remaining[pos + 1..];
Some(field)
}
None => {
let field = self.remaining;
self.remaining = "";
Some(field)
}
}
}
}
pub struct PuzzleIndex {
path: PathBuf,
theme_offsets: HashMap<String, Vec<u64>>,
pub theme_counts: Vec<(String, String, usize)>, pub total: usize,
}
impl PuzzleIndex {
pub fn build(path: &Path) -> io::Result<Self> {
let file = File::open(path)?;
let mut reader = BufReader::with_capacity(256 * 1024, file);
let mut theme_offsets: HashMap<String, Vec<u64>> = HashMap::new();
let mut line = String::new();
let mut offset: u64 = 0;
let mut total: usize = 0;
loop {
line.clear();
let bytes_read = reader.read_line(&mut line)?;
if bytes_read == 0 {
break;
}
let trimmed = line.trim_end();
if let Some(themes_field) = nth_csv_field(trimmed, 7) {
for theme in themes_field.split_whitespace() {
theme_offsets
.entry(theme.to_string())
.or_default()
.push(offset);
}
}
offset += bytes_read as u64;
total += 1;
}
let theme_counts: Vec<(String, String, usize)> = TACTIC_THEMES
.iter()
.map(|&(tag, name)| {
let count = theme_offsets.get(tag).map_or(0, |v| v.len());
(tag.to_string(), name.to_string(), count)
})
.collect();
Ok(Self {
path: path.to_path_buf(),
theme_offsets,
theme_counts,
total,
})
}
pub fn load_theme(
&self,
theme: &str,
max_rating: Option<u16>,
limit: usize,
) -> io::Result<Vec<Puzzle>> {
let offsets = match self.theme_offsets.get(theme) {
Some(o) => o,
None => return Ok(Vec::new()),
};
let mut file = File::open(&self.path)?;
let mut puzzles = Vec::with_capacity(limit.min(offsets.len()));
let mut line_buf = String::new();
for &off in offsets {
if puzzles.len() >= limit {
break;
}
file.seek(SeekFrom::Start(off))?;
line_buf.clear();
let mut reader = BufReader::new(&file);
reader.read_line(&mut line_buf)?;
if let Some(puzzle) = Puzzle::from_csv_line(line_buf.trim_end()) {
if max_rating.map_or(true, |max| puzzle.rating <= max) {
puzzles.push(puzzle);
}
}
}
Ok(puzzles)
}
#[allow(dead_code)]
pub fn load_theme_with_offset(
&self,
theme: &str,
max_rating: Option<u16>,
limit: usize,
offset: usize,
) -> io::Result<Vec<Puzzle>> {
let offsets = match self.theme_offsets.get(theme) {
Some(o) => o,
None => return Ok(Vec::new()),
};
let mut file = File::open(&self.path)?;
let mut puzzles = Vec::with_capacity(limit.min(offsets.len()));
let mut line_buf = String::new();
let mut skipped = 0usize;
for &off in offsets {
if puzzles.len() >= limit {
break;
}
file.seek(SeekFrom::Start(off))?;
line_buf.clear();
let mut reader = BufReader::new(&file);
reader.read_line(&mut line_buf)?;
if let Some(puzzle) = Puzzle::from_csv_line(line_buf.trim_end()) {
if max_rating.map_or(true, |max| puzzle.rating <= max) {
if skipped < offset {
skipped += 1;
continue;
}
puzzles.push(puzzle);
}
}
}
Ok(puzzles)
}
}
fn nth_csv_field(line: &str, n: usize) -> Option<&str> {
let mut start = 0;
for _ in 0..n {
start = line[start..].find(',')? + start + 1;
}
let end = line[start..].find(',').map_or(line.len(), |p| start + p);
Some(&line[start..end])
}
pub const TACTIC_THEMES: &[(&str, &str)] = &[
("fork", "Fork / Double Attack"),
("pin", "Pin"),
("skewer", "Skewer"),
("discoveredAttack", "Discovered Attack"),
("mateIn1", "Mate in 1"),
("mateIn2", "Mate in 2"),
("mateIn3", "Mate in 3+"),
("backRankMate", "Back Rank Mate"),
("smotheredMate", "Smothered Mate"),
("hangingPiece", "Hanging Piece"),
("trappedPiece", "Trapped Piece"),
("deflection", "Deflection"),
("decoy", "Decoy"),
("overloading", "Overloading"),
("interference", "Interference"),
("sacrifice", "Sacrifice"),
("clearance", "Clearance"),
("quietMove", "Quiet Move"),
("xRayAttack", "X-Ray Attack"),
("zugzwang", "Zugzwang"),
("promotion", "Pawn Promotion"),
("underPromotion", "Underpromotion"),
("castling", "Castling"),
("enPassant", "En Passant"),
("exposedKing", "Exposed King"),
("kingsideAttack", "Kingside Attack"),
("queensideAttack", "Queenside Attack"),
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_csv_field_iter() {
let line = "abc,def,ghi,jkl";
let mut iter = CsvFieldIter::new(line);
assert_eq!(iter.next(), Some("abc"));
assert_eq!(iter.next(), Some("def"));
assert_eq!(iter.next(), Some("ghi"));
assert_eq!(iter.next(), Some("jkl"));
assert_eq!(iter.next(), None);
}
#[test]
fn test_nth_csv_field() {
let line = "00sHx,fen_here,e8d7 a2e6,1760,80,83,72,mate mateIn2,url,opening";
assert_eq!(nth_csv_field(line, 0), Some("00sHx"));
assert_eq!(nth_csv_field(line, 3), Some("1760"));
assert_eq!(nth_csv_field(line, 7), Some("mate mateIn2"));
}
#[test]
fn test_puzzle_from_csv_line() {
let line = "abc,r1bqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1,e2e4 d7d5,1200,80,90,50,fork short,url,opening";
let p = Puzzle::from_csv_line(line).unwrap();
assert_eq!(p.id, "abc");
assert_eq!(p.rating, 1200);
assert_eq!(p.solution_moves(), &["d7d5"]);
assert_eq!(p.themes, vec!["fork", "short"]);
}
}