Skip to main content

chaiss_core/engine/
models.rs

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),   // Represents the Winner naturally
29    Resignation(Color), // Represents the Winner manually
30    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/// A square index on the board, 0 to 63.
76/// 0 is a8 (top-left), 63 is h1 (bottom-right).
77#[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        // 0,0 as top-left (a8). file=x, rank=y (0=8th rank, 7=1st rank).
92        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, // e.g. "KQkq"
105    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    /// Create starting position
113    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    /// Parses a FEN string into a GameState
126    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        // 1. Piece placement
136        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        // 2. Active color
152        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        // 3. Castling rights
159        let castling_rights = parts[2].to_string();
160
161        // 4. En passant target
162        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        // 5. Halfmove clock
174        let halfmove_clock = parts[4].parse::<u16>().unwrap_or(0);
175
176        // 6. Fullmove number
177        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    /// Converts the current state to a FEN string
191    pub fn to_fen(&self) -> String {
192        let mut fen = String::new();
193
194        // 1. Board
195        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        // 2. Active Color
218        fen.push(' ');
219        fen.push(if self.active_color == Color::White {
220            'w'
221        } else {
222            'b'
223        });
224
225        // 3. Castling Rights
226        fen.push(' ');
227        fen.push_str(&self.castling_rights);
228
229        // 4. En Passant
230        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        // 5 & 6
242        fen.push_str(&format!(
243            " {} {}",
244            self.halfmove_clock, self.fullmove_number
245        ));
246
247        fen
248    }
249
250    /// Converts the game state into an ASCII representation suited for LLM structural context.
251    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    /// Generates a heat map of attacked squares
271    /// Consumes the raycasting logic to build an authentic alpha-blend array distinguishing White vs Black mathematically!
272    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                    // Fetch every square this piece exerts mathematical pressure on
280                    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    /// Synthesizes the Second-Order Predictive Matrix by geometrically forecasting every
299    /// legal 1-ply branch mathematically and overlaying bounds!
300    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                        // Mathematically fork the evaluation geometry
309                        let mut branched_state = self.clone();
310                        // Assume Queen promotion implicitly to evaluate maximal geometric consequences
311                        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    /// Dynamically isolates the Top 4 mathematically contested bounds for the AI Payload formatting!
331    #[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        // Sort explicitly by maximum absolute geometric density descending
353        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    /// Mutates the state structurally, transposing the Piece vector entirely!
363    pub fn apply_move(&mut self, from: usize, to: usize, promotion_target: Option<PieceType>) {
364        // A direct physical piece translation fundamentally shatters any explicit manual overrides organically!
365        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            // Handle Castling Geometry Transpositions
376            if p.piece_type == PieceType::King {
377                // Permanently disable castling rights
378                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                // Physical Jump Execution natively
385                if from == 60 && to == 62 {
386                    // White Kingside
387                    self.board[61] = self.board[63].take();
388                } else if from == 60 && to == 58 {
389                    // White Queenside
390                    self.board[59] = self.board[56].take();
391                } else if from == 4 && to == 6 {
392                    // Black Kingside
393                    self.board[5] = self.board[7].take();
394                } else if from == 4 && to == 2 {
395                    // Black Queenside
396                    self.board[3] = self.board[0].take();
397                }
398            }
399
400            // Handle Rook explicit movement degradation
401            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            // Handle physical en passant capture geometry
417            if p.piece_type == PieceType::Pawn {
418                if let Some(ep_sq) = self.en_passant_target {
419                    if to == ep_sq.index {
420                        // Wipe mathematically captured pawn behind!
421                        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                // Implement Auto-Queening Promotion
431                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); // Load back the modified Piece structurally
438        }
439
440        // Execute structural landing
441        self.board[to] = piece;
442
443        // Reset en_passant_target dynamically if this was a double pawn push landing!
444        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        // Castling explicitly ends if Rooks are mathematically captured by an enemy piece!
460        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        // Keep FEN perfectly stringified
474        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        // Toggle native color and turn tracking natively
481        self.active_color = self.active_color.opposite();
482        if self.active_color == Color::White {
483            self.fullmove_number += 1;
484        }
485
486        // 50-Move Draw Bounds: physically evaluate resets!
487        if reset_halfmove {
488            self.halfmove_clock = 0;
489        } else {
490            self.halfmove_clock += 1;
491        }
492    }
493
494    /// Evaluates if all geometric paths for the currently active color are violently exhausted natively!
495    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        // Loop purely to test mathematical bounds
503        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        // Exiting loops natively verifies completely locked boards algebraically!
516        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                // The attacking hostiles won!
522                } else {
523                    return Some(GameEndStatus::Stalemate); // Pinned but safe algebraically
524                }
525            } else {
526                return Some(GameEndStatus::Stalemate); // Failsafe for missing King strings
527            }
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        // Re-serialize and ensure it perfectly matches standard.
544        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        // e4 g5, d4 f6, Qh5#
559        // White to move? No, Black to move and is checkmated by White!
560        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        // Nc3 = 57 to 42
577        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}