1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2pub enum Color {
3 White,
4 Black,
5}
6
7impl Color {
8 pub fn opposite(&self) -> Self {
9 match self {
10 Color::White => Color::Black,
11 Color::Black => Color::White,
12 }
13 }
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum PieceType {
18 Pawn,
19 Knight,
20 Bishop,
21 Rook,
22 Queen,
23 King,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum GameEndStatus {
28 Checkmate(Color), Resignation(Color), Stalemate,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct Piece {
35 pub color: Color,
36 pub piece_type: PieceType,
37}
38
39impl Piece {
40 pub fn from_char(c: char) -> Option<Self> {
41 let color = if c.is_uppercase() {
42 Color::White
43 } else {
44 Color::Black
45 };
46 let piece_type = match c.to_ascii_lowercase() {
47 'p' => PieceType::Pawn,
48 'n' => PieceType::Knight,
49 'b' => PieceType::Bishop,
50 'r' => PieceType::Rook,
51 'q' => PieceType::Queen,
52 'k' => PieceType::King,
53 _ => return None,
54 };
55 Some(Piece { color, piece_type })
56 }
57
58 pub fn to_char(&self) -> char {
59 let c = match self.piece_type {
60 PieceType::Pawn => 'p',
61 PieceType::Knight => 'n',
62 PieceType::Bishop => 'b',
63 PieceType::Rook => 'r',
64 PieceType::Queen => 'q',
65 PieceType::King => 'k',
66 };
67 if self.color == Color::White {
68 c.to_ascii_uppercase()
69 } else {
70 c
71 }
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct Square {
79 pub index: usize,
80}
81
82impl Square {
83 pub fn new(index: usize) -> Self {
84 Square { index }
85 }
86
87 pub fn from_file_rank(file: usize, rank: usize) -> Option<Self> {
88 if file > 7 || rank > 7 {
89 return None;
90 }
91 Some(Square {
93 index: rank * 8 + file,
94 })
95 }
96}
97
98pub type BoardMatrix = [Option<Piece>; 64];
99
100#[derive(Debug, Clone, PartialEq)]
101pub struct GameState {
102 pub board: BoardMatrix,
103 pub active_color: Color,
104 pub castling_rights: String, pub en_passant_target: Option<Square>,
106 pub halfmove_clock: u16,
107 pub fullmove_number: u16,
108 pub manual_terminal_status: Option<GameEndStatus>,
109}
110
111impl GameState {
112 pub fn new() -> Self {
114 Self::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1").unwrap()
115 }
116}
117
118impl Default for GameState {
119 fn default() -> Self {
120 Self::new()
121 }
122}
123
124impl GameState {
125 pub fn from_fen(fen: &str) -> Result<Self, String> {
127 let parts: Vec<&str> = fen.split_whitespace().collect();
128 if parts.len() != 6 {
129 return Err("Invalid FEN string: incorrect number of fields".to_string());
130 }
131
132 let mut board: BoardMatrix = [None; 64];
133 let mut index = 0;
134
135 for c in parts[0].chars() {
137 if c == '/' {
138 continue;
139 } else if c.is_ascii_digit() {
140 let empty_squares = c.to_digit(10).unwrap() as usize;
141 index += empty_squares;
142 } else {
143 if index >= 64 {
144 return Err("Invalid FEN string: too many pieces/squares".to_string());
145 }
146 board[index] = Piece::from_char(c);
147 index += 1;
148 }
149 }
150
151 let active_color = match parts[1] {
153 "w" => Color::White,
154 "b" => Color::Black,
155 _ => return Err("Invalid active color in FEN".to_string()),
156 };
157
158 let castling_rights = parts[2].to_string();
160
161 let en_passant_target = if parts[3] != "-" {
163 let files = "abcdefgh";
164 let f_char = parts[3].chars().nth(0).unwrap();
165 let r_char = parts[3].chars().nth(1).unwrap();
166 let file = files.find(f_char).unwrap();
167 let rank = 8 - r_char.to_digit(10).unwrap() as usize;
168 Square::from_file_rank(file, rank)
169 } else {
170 None
171 };
172
173 let halfmove_clock = parts[4].parse::<u16>().unwrap_or(0);
175
176 let fullmove_number = parts[5].parse::<u16>().unwrap_or(1);
178
179 Ok(GameState {
180 board,
181 active_color,
182 castling_rights,
183 en_passant_target,
184 halfmove_clock,
185 fullmove_number,
186 manual_terminal_status: None,
187 })
188 }
189
190 pub fn to_fen(&self) -> String {
192 let mut fen = String::new();
193
194 for rank in 0..8 {
196 let mut empty_count = 0;
197 for file in 0..8 {
198 let index = rank * 8 + file;
199 if let Some(piece) = self.board[index] {
200 if empty_count > 0 {
201 fen.push_str(&empty_count.to_string());
202 empty_count = 0;
203 }
204 fen.push(piece.to_char());
205 } else {
206 empty_count += 1;
207 }
208 }
209 if empty_count > 0 {
210 fen.push_str(&empty_count.to_string());
211 }
212 if rank < 7 {
213 fen.push('/');
214 }
215 }
216
217 fen.push(' ');
219 fen.push(if self.active_color == Color::White {
220 'w'
221 } else {
222 'b'
223 });
224
225 fen.push(' ');
227 fen.push_str(&self.castling_rights);
228
229 fen.push(' ');
231 if let Some(sq) = self.en_passant_target {
232 let file = (sq.index % 8) as u8;
233 let rank = 8 - (sq.index / 8) as u8;
234 let file_char = (b'a' + file) as char;
235 fen.push(file_char);
236 fen.push_str(&rank.to_string());
237 } else {
238 fen.push('-');
239 }
240
241 fen.push_str(&format!(
243 " {} {}",
244 self.halfmove_clock, self.fullmove_number
245 ));
246
247 fen
248 }
249
250 pub fn to_ascii(&self) -> String {
252 let mut ascii = String::from(" +------------------------+\n");
253 for rank in 0..8 {
254 ascii.push_str(&format!("{} |", 8 - rank));
255 for file in 0..8 {
256 let index = rank * 8 + file;
257 if let Some(piece) = self.board[index] {
258 ascii.push_str(&format!(" {} ", piece.to_char()));
259 } else {
260 ascii.push_str(" . ");
261 }
262 }
263 ascii.push_str("|\n");
264 }
265 ascii.push_str(" +------------------------+\n");
266 ascii.push_str(" a b c d e f g h\n");
267 ascii
268 }
269
270 pub fn generate_heat_map(&self) -> [[(u8, u8); 8]; 8] {
273 let mut heat_map = [[(0u8, 0u8); 8]; 8];
274
275 for rank in 0..8 {
276 for file in 0..8 {
277 let index = rank * 8 + file;
278 if let Some(piece) = self.board[index] {
279 let attacks = super::movement::get_pseudo_legal_attacks(self, index, piece);
281
282 for att_idx in attacks {
283 let att_r = att_idx / 8;
284 let att_f = att_idx % 8;
285
286 if piece.color == Color::White {
287 heat_map[att_r][att_f].0 += 1;
288 } else {
289 heat_map[att_r][att_f].1 += 1;
290 }
291 }
292 }
293 }
294 }
295 heat_map
296 }
297
298 pub fn generate_predictive_matrix(&self) -> [[(u8, u8); 8]; 8] {
301 let mut aggregate_heat = [[(0u8, 0u8); 8]; 8];
302
303 for index in 0..64 {
304 if let Some(p) = self.board[index] {
305 if p.color == self.active_color {
306 let legal_targets = super::movement::get_legal_moves(self, index, p);
307 for target in legal_targets {
308 let mut branched_state = self.clone();
310 branched_state.apply_move(index, target, Some(PieceType::Queen));
312
313 let branch_heat = branched_state.generate_heat_map();
314
315 for r in 0..8 {
316 for c in 0..8 {
317 aggregate_heat[r][c].0 =
318 aggregate_heat[r][c].0.saturating_add(branch_heat[r][c].0);
319 aggregate_heat[r][c].1 =
320 aggregate_heat[r][c].1.saturating_add(branch_heat[r][c].1);
321 }
322 }
323 }
324 }
325 }
326 }
327 aggregate_heat
328 }
329
330 #[allow(clippy::needless_range_loop)]
332 pub fn extract_hottest_predictive_squares(&self, matrix: &[[(u8, u8); 8]; 8]) -> Vec<String> {
333 let mut heatmap_scores = Vec::new();
334
335 for r in 0..8 {
336 for c in 0..8 {
337 let heat_w = matrix[r][c].0;
338 let heat_b = matrix[r][c].1;
339 let total_heat = heat_w.saturating_add(heat_b);
340
341 if total_heat > 0 {
342 let sq_idx = r * 8 + c;
343 let file_char = (b'a' + (sq_idx % 8) as u8) as char;
344 let rank_char = (b'1' + (7 - (sq_idx / 8)) as u8) as char;
345 let coord = format!("{}{}", file_char, rank_char);
346
347 heatmap_scores.push((coord, total_heat));
348 }
349 }
350 }
351
352 heatmap_scores.sort_by_key(|b| std::cmp::Reverse(b.1));
354
355 heatmap_scores
356 .into_iter()
357 .take(4)
358 .map(|(coord, heat)| format!("{} (Heat: {})", coord, heat))
359 .collect()
360 }
361
362 pub fn apply_move(&mut self, from: usize, to: usize, promotion_target: Option<PieceType>) {
364 self.manual_terminal_status = None;
366
367 let is_capture = self.board[to].is_some();
368 let mut piece = self.board[from].take();
369 let mut reset_halfmove = is_capture;
370
371 if let Some(mut p) = piece {
372 if p.piece_type == PieceType::Pawn {
373 reset_halfmove = true;
374 }
375 if p.piece_type == PieceType::King {
377 if p.color == Color::White {
379 self.castling_rights = self.castling_rights.replace("K", "").replace("Q", "");
380 } else {
381 self.castling_rights = self.castling_rights.replace("k", "").replace("q", "");
382 }
383
384 if from == 60 && to == 62 {
386 self.board[61] = self.board[63].take();
388 } else if from == 60 && to == 58 {
389 self.board[59] = self.board[56].take();
391 } else if from == 4 && to == 6 {
392 self.board[5] = self.board[7].take();
394 } else if from == 4 && to == 2 {
395 self.board[3] = self.board[0].take();
397 }
398 }
399
400 if p.piece_type == PieceType::Rook {
402 if from == 63 {
403 self.castling_rights = self.castling_rights.replace("K", "");
404 }
405 if from == 56 {
406 self.castling_rights = self.castling_rights.replace("Q", "");
407 }
408 if from == 7 {
409 self.castling_rights = self.castling_rights.replace("k", "");
410 }
411 if from == 0 {
412 self.castling_rights = self.castling_rights.replace("q", "");
413 }
414 }
415
416 if p.piece_type == PieceType::Pawn {
418 if let Some(ep_sq) = self.en_passant_target {
419 if to == ep_sq.index {
420 let capture_idx = if p.color == Color::White {
422 to + 8
423 } else {
424 to - 8
425 };
426 self.board[capture_idx] = None;
427 }
428 }
429
430 let to_rank = to / 8;
432 if to_rank == 0 || to_rank == 7 {
433 p.piece_type = promotion_target.unwrap_or(PieceType::Queen);
434 }
435 }
436
437 piece = Some(p); }
439
440 self.board[to] = piece;
442
443 self.en_passant_target = None;
445 if let Some(p) = piece {
446 if p.piece_type == PieceType::Pawn {
447 let diff = (to as i32 - from as i32).abs();
448 if diff == 16 {
449 let ep_idx = if p.color == Color::White {
450 from - 8
451 } else {
452 from + 8
453 };
454 self.en_passant_target = Some(Square::new(ep_idx));
455 }
456 }
457 }
458
459 if to == 63 {
461 self.castling_rights = self.castling_rights.replace("K", "");
462 }
463 if to == 56 {
464 self.castling_rights = self.castling_rights.replace("Q", "");
465 }
466 if to == 7 {
467 self.castling_rights = self.castling_rights.replace("k", "");
468 }
469 if to == 0 {
470 self.castling_rights = self.castling_rights.replace("q", "");
471 }
472
473 if self.castling_rights.is_empty() {
475 self.castling_rights = "-".to_string();
476 } else if self.castling_rights != "-" && self.castling_rights.contains('-') {
477 self.castling_rights = self.castling_rights.replace("-", "");
478 }
479
480 self.active_color = self.active_color.opposite();
482 if self.active_color == Color::White {
483 self.fullmove_number += 1;
484 }
485
486 if reset_halfmove {
488 self.halfmove_clock = 0;
489 } else {
490 self.halfmove_clock += 1;
491 }
492 }
493
494 pub fn evaluate_terminal_state(&self) -> Option<GameEndStatus> {
496 if self.manual_terminal_status.is_some() {
497 return self.manual_terminal_status;
498 }
499
500 let mut has_moves = false;
501
502 for sq_idx in 0..64 {
504 if let Some(piece) = self.board[sq_idx] {
505 if piece.color == self.active_color {
506 let moves = super::movement::get_legal_moves(self, sq_idx, piece);
507 if !moves.is_empty() {
508 has_moves = true;
509 break;
510 }
511 }
512 }
513 }
514
515 if !has_moves {
517 if let Some(king_idx) = super::movement::find_king(self, self.active_color) {
518 if super::movement::is_square_attacked(self, king_idx, self.active_color.opposite())
519 {
520 return Some(GameEndStatus::Checkmate(self.active_color.opposite()));
521 } else {
523 return Some(GameEndStatus::Stalemate); }
525 } else {
526 return Some(GameEndStatus::Stalemate); }
528 }
529
530 None
531 }
532}
533
534#[cfg(test)]
535mod tests {
536 use super::*;
537
538 #[test]
539 fn test_initial_fen_parsing() {
540 let start_fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
541 let state = GameState::from_fen(start_fen).expect("Failed to parse starting FEN");
542
543 assert_eq!(state.to_fen(), start_fen);
545 }
546
547 #[test]
548 fn test_ascii_generation() {
549 let state = GameState::new();
550 let ascii = state.to_ascii();
551 assert!(ascii.contains("P P P P P P P P "));
552 assert!(ascii.contains("p p p p p p p p "));
553 assert!(ascii.contains("a b c d e f g h"));
554 }
555
556 #[test]
557 fn test_evaluate_fools_mate() {
558 let fen = "rnbqkbnr/ppppp2p/5p2/6pQ/4P3/8/PPPP1PPP/RNB1KBNR b KQkq - 1 3";
561 let state = GameState::from_fen(fen).unwrap();
562
563 let terminal = state.evaluate_terminal_state();
564 assert_eq!(
565 terminal,
566 Some(GameEndStatus::Checkmate(Color::White)),
567 "Mathematically verifies White's victory!"
568 );
569 }
570
571 #[test]
572 fn test_apply_move_fen_output() {
573 let mut state =
574 GameState::from_fen("rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e6 0 2")
575 .unwrap();
576 state.apply_move(57, 42, None);
578 let fen = state.to_fen();
579 println!("Test Output FEN: {}", fen);
580
581 let state_recovered = GameState::from_fen(&fen).unwrap();
582 assert_eq!(
583 state_recovered.board[42].unwrap().piece_type,
584 PieceType::Knight
585 );
586 }
587}