Skip to main content

chess_tui/
utils.rs

1use crate::game_logic::coord::Coord;
2use ratatui::style::Color;
3use shakmaty::Square;
4
5#[must_use]
6pub fn color_to_ratatui_enum(piece_color: Option<shakmaty::Color>) -> Color {
7    match piece_color {
8        Some(shakmaty::Color::Black) => Color::Black,
9        Some(shakmaty::Color::White) => Color::White,
10        None => Color::Red,
11    }
12}
13
14#[must_use]
15pub fn flip_square_if_needed(square: Square, is_flipped: bool) -> Square {
16    if is_flipped {
17        Coord::from_square(square)
18            .reverse()
19            .to_square()
20            .unwrap_or(square)
21    } else {
22        square
23    }
24}
25
26#[must_use]
27pub fn get_square_from_coord(coord: Coord, is_flipped: bool) -> Option<Square> {
28    if is_flipped {
29        coord.reverse().to_square()
30    } else {
31        coord.to_square()
32    }
33}
34
35#[must_use]
36pub fn get_coord_from_square(square: Option<Square>, is_flipped: bool) -> Coord {
37    if let Some(s) = square {
38        if is_flipped {
39            Coord::from_square(s).reverse()
40        } else {
41            Coord::from_square(s)
42        }
43    } else {
44        Coord::undefined()
45    }
46}
47
48/// Convert a character to an integer for parsing UCI moves
49#[must_use]
50pub fn get_int_from_char(c: Option<char>) -> u8 {
51    match c {
52        Some('b' | '1') => 1,
53        Some('c' | '2') => 2,
54        Some('d' | '3') => 3,
55        Some('e' | '4') => 4,
56        Some('f' | '5') => 5,
57        Some('g' | '6') => 6,
58        Some('h' | '7') => 7,
59        _ => 0,
60    }
61}
62
63#[must_use]
64pub fn get_opposite_square(square: Option<Square>) -> Option<Square> {
65    square.and_then(|s| Coord::from_square(s).reverse().to_square())
66}
67
68/// Convert position format ("4644") to UCI notation (e.g., "e4e4")
69#[must_use]
70pub fn convert_position_into_notation(position: &str) -> String {
71    let chars: Vec<char> = position.chars().collect();
72    if chars.len() < 4 {
73        return String::new();
74    }
75
76    // Safe conversion: to_digit returns values 0-9, which fits in u8
77    let from_row = chars[0].to_digit(10).unwrap_or(0).min(255) as u8;
78    let from_col = chars[1].to_digit(10).unwrap_or(0).min(255) as u8;
79    let to_row = chars[2].to_digit(10).unwrap_or(0).min(255) as u8;
80    let to_col = chars[3].to_digit(10).unwrap_or(0).min(255) as u8;
81
82    // Convert from our internal format to chess notation
83    // Row is inverted: row 0 = rank 8, row 7 = rank 1
84    let from_rank = 7 - from_row;
85    let to_rank = 7 - to_row;
86
87    let files = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
88    let ranks = ['1', '2', '3', '4', '5', '6', '7', '8'];
89
90    format!(
91        "{}{}{}{}",
92        files[from_col as usize],
93        ranks[from_rank as usize],
94        files[to_col as usize],
95        ranks[to_rank as usize]
96    )
97}
98
99// normalize lower case sans into valid uppercase san
100#[must_use]
101pub fn normalize_lowercase_to_san(input: &str) -> String {
102    let s0 = input.trim();
103
104    if s0.is_empty() {
105        return String::new();
106    }
107
108    // ---- Castling normalization (accept common variants) ----
109    let lower = s0.to_ascii_lowercase();
110    match lower.as_str() {
111        "o-o" | "0-0" => return "O-O".to_string(),
112        "o-o-o" | "0-0-0" => return "O-O-O".to_string(),
113        _ => {}
114    }
115
116    let mut s = s0.to_string();
117
118    // ---- Uppercase leading piece letter (only first char) ----
119    if let Some(first) = s.chars().next() {
120        let up = match first {
121            'n' => Some('N'),
122            'b' => Some('B'),
123            'r' => Some('R'),
124            'q' => Some('Q'),
125            'k' => Some('K'),
126            _ => None, // pawn moves like "e4" should stay lowercase
127        };
128
129        if let Some(up) = up {
130            s.replace_range(0..first.len_utf8(), &up.to_string());
131        }
132    }
133
134    // ---- Uppercase promotion piece after '=' (e8=q -> e8=Q) ----
135    if let Some(eq) = s.find('=') {
136        // SAN promotion piece is a single ASCII letter right after '='
137        let bytes = s.as_bytes();
138        if eq + 1 < bytes.len() {
139            let promo = bytes[eq + 1] as char;
140            let up = match promo {
141                'q' => Some('Q'),
142                'r' => Some('R'),
143                'b' => Some('B'),
144                'n' => Some('N'),
145                _ => None,
146            };
147            if let Some(up) = up {
148                s.replace_range(eq + 1..eq + 2, &up.to_string());
149            }
150        }
151    }
152
153    s
154}