pgnparse/
parser.rs

1use log::{log_enabled, info, Level};
2
3use shakmaty::variants::{Antichess, Atomic, Chess, Crazyhouse, Horde, KingOfTheHill, RacingKings, ThreeCheck};
4use shakmaty::san::{San};
5use shakmaty::uci::{Uci};
6use shakmaty::fen;
7use shakmaty::fen::Fen;
8use shakmaty::Position;
9use pgn_reader::{Visitor, Skip, RawHeader, SanPlus, BufferedReader};
10use serde::{Deserialize, Serialize};
11use std::fs::File;
12use std::io::{self, BufRead};
13use std::path::Path;
14use rand::Rng;
15
16/// variant enum
17#[derive(Debug)]
18pub enum Variant{
19	VariantAntichess,
20	VariantAtomic,	
21	VariantChess960,
22	VariantCrazyhose,
23	VariantFromPosition,
24	VariantHorde,
25	VariantKingOfTheHill,
26	VariantRacingKings,
27	VariantStandard,	
28	VariantThreeCheck,
29}
30
31use Variant::*;
32
33/// san, uci, fen, epd for move
34#[derive(Debug, Serialize, Deserialize)]
35pub struct SanUciFenEpd {
36	pub san: String,
37	pub uci: String,
38	pub fen_before: String,
39	pub epd_before: String,
40	pub fen_after: String,
41	pub epd_after: String,
42}
43
44/// pgn headers and moves
45#[derive(Debug, Serialize, Deserialize)]
46pub struct PgnInfo {
47	pub headers: std::collections::HashMap<String, String>,
48	pub moves: Vec<SanUciFenEpd>,
49}
50
51/// implementation for PgnInfo
52impl PgnInfo {
53	/// create new pgn info
54	pub fn new() -> PgnInfo {
55		PgnInfo {
56			headers: std::collections::HashMap::new(),
57			moves: vec!(),
58		}
59	}
60	
61	/// push san uci fen epd
62	pub fn push(&mut self, san_uci_fen_epd: SanUciFenEpd) {
63		self.moves.push(san_uci_fen_epd);
64	}
65	
66	/// insert header
67	pub fn insert_header<K, V>(&mut self, key: K, value: V)
68	where K: core::fmt::Display, V: core::fmt::Display {
69		let key = format!("{}", key);
70		let value = format!("{}", value);
71
72		self.headers.insert(key, value);
73	}
74	
75	/// get header
76	pub fn get_header<T>(&mut self, key:T) -> String
77	where T: core::fmt::Display {
78		let key = format!("{}", key);
79
80		self.headers.get(&key).unwrap_or(&"?".to_string()).to_string()
81	}
82}
83
84/// parsing state
85struct ParsingState{
86	antichess_pos: Antichess,
87	atomic_pos: Atomic,
88	chess_pos: Chess,	
89	crazyhouse_pos: Crazyhouse,
90	horde_pos: Horde,
91	kingofthehill_pos: KingOfTheHill,
92	racingkings_pos: RacingKings,
93	three_check_pos: ThreeCheck,
94	variant: Variant,
95	check_custom_fen: bool,
96	pgn_info: PgnInfo,
97}
98
99impl ParsingState{
100	fn new() -> ParsingState{
101		ParsingState{
102			antichess_pos: Antichess::default(),
103			atomic_pos: Atomic::default(),
104			chess_pos: Chess::default(),			
105			crazyhouse_pos: Crazyhouse::default(),
106			horde_pos: Horde::default(),
107			kingofthehill_pos: KingOfTheHill::default(),
108			racingkings_pos: RacingKings::default(),
109			three_check_pos: ThreeCheck::default(),
110			variant: VariantStandard,
111			check_custom_fen: true,
112			pgn_info: PgnInfo::new(),
113		}
114	}
115}
116
117macro_rules! gen_make_move {
118    ($($variant:tt,$pos:ident,)+) => (
119        fn make_move(parsing_state: &mut ParsingState, san_plus: SanPlus) {
120            match parsing_state.variant {
121                $(
122                    $variant => {
123						let san_orig = san_plus.san;
124						let san_str = format!("{}", san_orig);        						
125						let san_result:std::result::Result<San, _> = san_str.parse();
126						
127						match san_result {
128							Ok(san) => {
129								let move_result = san.to_move(&parsing_state.$pos);
130
131								match move_result {
132									Ok(m) => {
133										let uci_str = match parsing_state.variant {
134											VariantChess960 => Uci::from_chess960(&m).to_string(),
135											_ => Uci::from_standard(&m).to_string()
136										};										
137										let fen_before_str = format!("{}", fen::fen(&parsing_state.$pos));
138										let epd_before_str = format!("{}", fen::epd(&parsing_state.$pos));
139										
140										parsing_state.$pos.play_unchecked(&m);
141										
142										let fen_after_str = format!("{}", fen::fen(&parsing_state.$pos));
143										let epd_after_str = format!("{}", fen::epd(&parsing_state.$pos));
144										
145										let san_uci_fen_epd = SanUciFenEpd{
146											san: san_str,
147											uci: uci_str,
148											fen_before: fen_before_str,
149											epd_before: epd_before_str,
150											fen_after: fen_after_str,
151											epd_after: epd_after_str,
152										};						
153										
154										parsing_state.pgn_info.push(san_uci_fen_epd);										
155									},
156									_ => println!("move error {:?}", move_result)
157								}				
158							},
159							Err(err) => {
160								println!("san parsing error {:?}", err)
161							}
162						}
163					},
164                )+
165            };
166        }
167    )
168}
169
170gen_make_move!(
171	VariantStandard, chess_pos,
172	VariantChess960, chess_pos,
173	VariantFromPosition, chess_pos,
174	VariantAtomic, atomic_pos,
175	VariantAntichess, antichess_pos,
176	VariantCrazyhose, crazyhouse_pos,
177	VariantHorde, horde_pos,
178	VariantRacingKings, racingkings_pos,
179	VariantKingOfTheHill, kingofthehill_pos,
180	VariantThreeCheck, three_check_pos,
181);
182
183/// variant name to variant
184fn variant_name_to_variant(variant: &str) -> Variant {
185	match variant.to_lowercase().as_str() {
186		"antichess" | "anti chess" | "giveaway" | "give away" => VariantAntichess,
187		"atomic" => VariantAtomic,
188		"chess960" | "chess 960" => VariantChess960,
189		"crazyhouse" | "crazy house" => VariantCrazyhose,
190		"fromposition" | "from position" => VariantFromPosition,								
191		"horde" => VariantHorde,
192		"kingofthehill" | "king of the hill" | "koth" => VariantKingOfTheHill,
193		"racingkings" | "racing kings" => VariantRacingKings,
194		"standard" => VariantStandard,
195		"threecheck" | "three check" | "3check" | "3 check" => VariantThreeCheck,
196		_ => VariantStandard,
197	}
198}
199
200/// implement visitor
201impl Visitor for ParsingState {
202    type Result = String;
203
204    fn header(&mut self, key: &[u8], value: RawHeader<'_>) {
205		let key_str_result = std::str::from_utf8(key);
206		match key_str_result {
207			Ok(key_str) => {
208				let value_str_result = std::str::from_utf8(value.as_bytes());
209				match value_str_result {
210					Ok(value_str) => {
211						self.pgn_info.insert_header(key_str.to_string(), value_str.to_string());
212						
213						if key_str == "Variant" {
214							self.variant = variant_name_to_variant(value_str);
215						}
216					},
217					Err(err) => println!("header value utf8 parse error {:?}", err)
218				}
219			}
220			Err(err) => println!("header key utf8 parse error {:?}", err)
221		}		
222    }
223
224    fn begin_variation(&mut self) -> Skip {
225        Skip(true) // stay in the mainline
226    }
227
228    fn san(&mut self, san_plus: SanPlus) {		
229		if self.check_custom_fen {
230			self.check_custom_fen = false;
231			
232			if let Some(fen) = self.pgn_info.headers.get("FEN") {
233				let castling_mode = match self.variant {
234					VariantChess960 => shakmaty::CastlingMode::Chess960,
235					_ => shakmaty::CastlingMode::Standard,
236				};
237				
238				match self.variant {
239					VariantAntichess => {
240						let pos = Fen::from_ascii(fen.as_bytes()).ok()
241                			.and_then(|f| f.position(castling_mode).ok());
242						
243						if let Some(pos) = pos {
244							self.antichess_pos = pos;
245						}
246						
247					},
248					VariantAtomic => {
249						let pos = Fen::from_ascii(fen.as_bytes()).ok()
250                			.and_then(|f| f.position(castling_mode).ok());
251						
252						if let Some(pos) = pos {
253							self.atomic_pos = pos;
254						}
255						
256					},
257					VariantCrazyhose => {
258						let pos = Fen::from_ascii(fen.as_bytes()).ok()
259                			.and_then(|f| f.position(castling_mode).ok());
260						
261						if let Some(pos) = pos {
262							self.crazyhouse_pos = pos;
263						}
264						
265					},
266					VariantHorde => {
267						let pos = Fen::from_ascii(fen.as_bytes()).ok()
268                			.and_then(|f| f.position(castling_mode).ok());
269						
270						if let Some(pos) = pos {
271							self.horde_pos = pos;
272						}
273						
274					},
275					VariantKingOfTheHill => {
276						let pos = Fen::from_ascii(fen.as_bytes()).ok()
277                			.and_then(|f| f.position(castling_mode).ok());
278						
279						if let Some(pos) = pos {
280							self.kingofthehill_pos = pos;
281						}
282						
283					},
284					VariantRacingKings => {
285						let pos = Fen::from_ascii(fen.as_bytes()).ok()
286                			.and_then(|f| f.position(castling_mode).ok());
287						
288						if let Some(pos) = pos {
289							self.racingkings_pos = pos;
290						}
291						
292					},
293					VariantThreeCheck => {
294						let pos = Fen::from_ascii(fen.as_bytes()).ok()
295                			.and_then(|f| f.position(castling_mode).ok());
296						
297						if let Some(pos) = pos {
298							self.three_check_pos = pos;
299						}
300						
301					},
302					_ => {
303						let pos = Fen::from_ascii(fen.as_bytes()).ok()
304                			.and_then(|f| f.position(castling_mode).ok());
305						
306						if let Some(pos) = pos {
307							self.chess_pos = pos;
308						}
309					}
310				}					
311			}
312		}
313		
314		make_move(self, san_plus);
315    }
316
317    fn end_game(&mut self) -> Self::Result {
318		let ser_result = serde_json::to_string(&self.pgn_info);
319		
320		match ser_result {
321			Ok(ser_str) => {
322				ser_str
323			},
324			Err(err) => {
325				println!("{:?}", err);
326				"".to_string()
327			}
328		}			
329    }
330}
331
332/// parse pgn to json string
333pub fn parse_pgn_to_json_string<T>(pgn_str: T) -> String
334where T: core::fmt::Display {
335	let pgn_str = pgn_str.to_string();
336	let pgn_bytes = pgn_str.as_bytes();
337		
338	let mut reader = BufferedReader::new_cursor(&pgn_bytes);
339
340	let mut visitor = ParsingState::new();
341	
342	match reader.read_game(&mut visitor) {
343		Ok(moves_opt) => moves_opt.unwrap_or("".to_string()),
344		Err(err) => {
345			println!("{:?}", err);
346			"".to_string()
347		}
348	}
349}
350
351/// parse pgn to rust struct
352pub fn parse_pgn_to_rust_struct<T>(pgn_str: T) -> PgnInfo 
353where T: core::fmt::Display {
354	let parse_result = parse_pgn_to_json_string(pgn_str.to_string());
355		
356	match serde_json::from_str::<PgnInfo>(&parse_result) {
357		Ok(pgn_info) => pgn_info,
358		_ => PgnInfo::new(),
359	}
360}
361
362/// read lines of file
363pub fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
364where P: AsRef<Path>, {
365    let file = File::open(filename)?;
366    Ok(io::BufReader::new(file).lines())
367}
368
369/// pgn iterator
370pub struct PgnIterator {	
371	pub lines: io::Lines<io::BufReader<File>>,
372}
373
374/// pgn iterator implementation
375impl PgnIterator {
376	pub fn new<T>(filename: T) -> Option<PgnIterator>
377	where T: AsRef<Path> {
378		if let Ok(file) = File::open(filename) {
379			return Some(PgnIterator{
380				lines: io::BufReader::new(file).lines()
381			})
382		}
383
384		None
385	}
386}
387
388/// pgn reading states
389enum PgnReadingState {
390	/// wait head
391	WaitHead,
392	/// read head
393	ReadHead,
394	/// wait body
395	WaitBody,
396	/// read body
397	ReadBody,
398}
399
400/// Iterator trait for PgnIterator implementation
401impl std::iter::Iterator for PgnIterator {
402	type Item = String;
403
404	/// next pgn
405	fn next(&mut self) -> Option<Self::Item> {
406		let mut state = PgnReadingState::WaitHead;
407
408		let mut accum = String::new();
409        
410        loop{
411        	if let Some(Ok(line)) = self.lines.next() {
412        		if line.len() == 0 {
413        			match state {
414        				PgnReadingState::ReadBody => return Some(accum),
415        				PgnReadingState::WaitHead => {},
416        				PgnReadingState::ReadHead => {
417        					accum = accum + &line + "\n";
418
419        					state = PgnReadingState::WaitBody;
420        				},
421        				_ => accum = accum + &line + "\n"
422        			}
423        		} else {
424        			match state {
425        				PgnReadingState::WaitHead => {
426        					if line.chars().next().unwrap() != '[' {
427        						// waiting for head but not receiving a header
428        						return None
429        					}
430
431        					accum = line;
432
433        					state = PgnReadingState::ReadHead
434        				},
435        				PgnReadingState::WaitBody => {
436        					accum = accum + &line + "\n";
437
438        					state = PgnReadingState::ReadBody
439        				},
440        				_=> accum = accum + &line + "\n"
441        			}
442        		}
443        	} else {
444        		match state {
445        			PgnReadingState::ReadBody => return Some(accum),
446        			_ => return None
447        		}
448        	}
449        }        
450    }
451}
452
453/// book move
454#[derive(Debug)]
455pub struct BookMove {
456	/// white wins
457	pub win: usize,
458	/// draw
459	pub draw: usize,
460	/// black wins
461	pub loss: usize,
462	/// uci
463	pub uci: String,
464	/// san
465	pub san: String,
466}
467
468/// book move implementation
469impl BookMove {
470	/// new book move
471	pub fn new<U, S>(uci: U, san: S) -> BookMove
472	where U: core::fmt::Display, S: core::fmt::Display {
473		BookMove {
474			win: 0,
475			draw: 0,
476			loss: 0,
477			uci: uci.to_string(),
478			san: san.to_string(),
479		}
480	}
481
482	/// plays
483	pub fn plays(&self) -> usize {
484		self.win + self.draw + self.loss
485	}
486
487	/// perf
488	pub fn perf(&self) -> usize {
489		let plays = self.plays();
490
491		if plays == 0 {
492			return 0
493		}
494
495		( ( ( 2 * self.win ) + self.draw ) * 50 ) / plays
496	}
497}
498
499/// book position
500#[derive(Debug)]
501pub struct BookPosition {
502	/// epd
503	pub epd: String,
504	/// moves
505	pub moves: std::collections::HashMap<String, BookMove>,
506}
507
508/// book position implmenetation
509impl BookPosition {
510	/// new book position
511	pub fn new<T>(epd: T) -> BookPosition 
512	where T: core::fmt::Display {
513		let epd = epd.to_string();
514
515		BookPosition {
516			epd: epd,
517			moves: std::collections::HashMap::new(),
518		}
519	}
520
521	/// total plays
522	pub fn total_plays(&self) -> usize {
523		let mut accum = 0;
524
525		for (_, m) in &self.moves {
526			accum += m.plays();
527		}
528
529		accum
530	}
531
532	/// total perf
533	pub fn total_perf(&self) -> usize {
534		let mut accum = 0;
535
536		for (_, m) in &self.moves {
537			accum += m.perf();
538		}
539
540		accum
541	}
542
543	/// get random weighted move
544	pub fn get_random_weighted_by_plays(&self) -> Option<&BookMove> {
545		let mut rng = rand::thread_rng();
546
547		let t = self.total_plays();
548
549		if t == 0 {
550			return None;
551		}
552
553		let r = rng.gen_range(0..t);
554
555		let mut accum = 0;
556
557		for (_, m) in &self.moves {
558			accum += m.plays();
559
560			if accum >= r {
561				return Some(m);
562			}
563		}
564
565		return None
566	}
567
568	/// get random weighted move
569	pub fn get_random_weighted_by_perf(&self) -> Option<&BookMove> {
570		let mut rng = rand::thread_rng();
571
572		let t = self.total_perf();
573
574		if t == 0 {
575			return None;
576		}
577
578		let r = rng.gen_range(0..t);
579
580		let mut accum = 0;
581
582		for (_, m) in &self.moves {
583			accum += m.perf();
584
585			if accum >= r {
586				return Some(m);
587			}
588		}
589
590		return None
591	}
592
593	/// get random move by mixed staretgy
594	pub fn get_random_mixed(&self, plays_weight: usize) -> Option<&BookMove> {
595		let mut rng = rand::thread_rng();
596		
597		let r = rng.gen_range(0..100);
598
599		if r <= plays_weight {
600			return self.get_random_weighted_by_plays();
601		}
602
603		self.get_random_weighted_by_perf()
604	}
605}
606
607/// book
608#[derive(Debug)]
609pub struct Book {
610	/// positions
611	pub positions: std::collections::HashMap<String, BookPosition>,
612	/// max depth
613	pub max_depth: usize,
614	/// me
615	pub me: Option<String>,
616}
617
618/// get turn of epd
619pub fn turn_white<T>(epd: T) -> bool
620where T: core::fmt::Display {
621	let epd = epd.to_string();
622
623	let parts:Vec<&str> = epd.split(" ").collect();
624
625	parts[1] == "w"
626}
627
628/// book implementation
629impl Book {
630	/// new book
631	pub fn new() -> Book {
632		Book {
633			positions: std::collections::HashMap::new(),
634			max_depth: 20,
635			me: None,
636		}
637	}
638
639	/// set me
640	pub fn me<T>(mut self, me: T) -> Book
641	where T: core::fmt::Display {
642		self.me = Some(me.to_string());
643
644		self
645	}
646
647	/// set max depth
648	pub fn max_depth<T>(mut self, max_depth: T) -> Book
649	where T: core::fmt::Display {
650		if let Ok(max_depth) = max_depth.to_string().parse() {
651			self.max_depth = max_depth;
652		}
653
654		self
655	}
656
657	/// parse file to book
658	pub fn parse<T>(&mut self, filename: T)
659	where T: AsRef<Path> + std::fmt::Display {		
660		let show_filename = filename.to_string();
661
662		if log_enabled!(Level::Info) {
663			info!("parsing {}", show_filename);
664		}
665
666		let iter = PgnIterator::new(filename);
667
668		let mut games = 0;
669		let mut moves = 0;
670		let mut parsed_moves = 0;
671		let mut me_white = 0;
672		let mut me_black = 0;
673
674		if let Some(iter) = iter {
675			for pgn in iter {
676				let mut parsed = parse_pgn_to_rust_struct(pgn);
677
678				if let Some(me) = self.me.to_owned() {
679					let white = parsed.get_header("White");
680					let black = parsed.get_header("Black");
681
682					if me == white {
683						me_white += 1;
684					}
685
686					if me == black {
687						me_black += 1;
688					}
689				}
690
691				let result = match parsed.get_header("Result").as_str() {
692					"1-0" => 2,
693					"0-1" => 0,
694					_ => 1,
695				};
696
697				let mut max_move = self.max_depth;
698
699				let len = parsed.moves.len();
700
701				if len < max_move {
702					max_move = len;
703				}
704
705				games += 1;
706				moves += len;
707				parsed_moves += max_move;
708
709				for i in 0..max_move {
710					let m = &parsed.moves[i];
711
712					let pos = self.positions.entry(m.epd_before.to_owned()).or_insert(BookPosition::new(m.epd_before.to_owned()));
713
714					let pm = pos.moves.entry(m.uci.to_owned()).or_insert(BookMove::new(m.uci.to_owned(), m.san.to_owned()));
715
716					let result_wrt = match turn_white(m.epd_before.to_owned()) {
717						true => result,
718						_ => 2 - result,
719					};
720
721					match result_wrt {
722						2 => pm.win += 1,
723						1 => pm.draw += 1,
724						_ => pm.loss += 1,
725					}
726				}
727			}
728		}
729
730		if log_enabled!(Level::Info) {
731			info!("parsing {} done, total games {}, total moves {}, parsed moves {}, me white {}, me black {}", show_filename, games, moves, parsed_moves, me_white, me_black);
732		}
733	}
734}