Skip to main content

quantik_core/
qfen.rs

1use crate::bitboard::Bitboard;
2use crate::constants::SHAPE_LETTERS;
3
4/// Encode a bitboard as a QFEN string.
5///
6/// Format: 4 slash-separated ranks of 4 characters each.
7/// Uppercase A-D for player 0, lowercase a-d for player 1, `.` for empty.
8pub fn bb_to_qfen(bb: &Bitboard) -> String {
9    let mut ranks = [String::new(), String::new(), String::new(), String::new()];
10
11    for (r, rank) in ranks.iter_mut().enumerate() {
12        for c in 0..4 {
13            let pos = r * 4 + c;
14            let mut ch = '.';
15            for color in 0..2u8 {
16                for s in 0..4u8 {
17                    if (bb.planes[Bitboard::plane_index(color, s)] >> pos) & 1 == 1 {
18                        let letter = SHAPE_LETTERS[s as usize];
19                        ch = if color == 0 {
20                            letter
21                        } else {
22                            letter.to_ascii_lowercase()
23                        };
24                    }
25                }
26            }
27            rank.push(ch);
28        }
29    }
30    ranks.join("/")
31}
32
33/// Decode a QFEN string into a bitboard.
34pub fn bb_from_qfen(qfen: &str) -> Result<Bitboard, String> {
35    let parts: Vec<&str> = qfen.split('/').collect();
36    if parts.len() != 4 {
37        return Err(format!("QFEN must have 4 ranks, got {}", parts.len()));
38    }
39    let mut planes = [0u16; 8];
40
41    for (r, rank) in parts.iter().enumerate() {
42        let chars: Vec<char> = rank.chars().collect();
43        if chars.len() != 4 {
44            return Err(format!("Rank {} must have 4 chars, got {}", r, chars.len()));
45        }
46        for (c, &ch) in chars.iter().enumerate() {
47            if ch == '.' {
48                continue;
49            }
50            let upper = ch.to_ascii_uppercase();
51            let shape = match upper {
52                'A' => 0u8,
53                'B' => 1,
54                'C' => 2,
55                'D' => 3,
56                _ => return Err(format!("Invalid char '{}' in QFEN", ch)),
57            };
58            let color: u8 = if ch.is_uppercase() { 0 } else { 1 };
59            let pos = r * 4 + c;
60            planes[Bitboard::plane_index(color, shape)] |= 1u16 << pos;
61        }
62    }
63    Ok(Bitboard::new(planes))
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn empty_board_qfen() {
72        let bb = Bitboard::EMPTY;
73        assert_eq!(bb_to_qfen(&bb), "..../..../..../....");
74    }
75
76    #[test]
77    fn roundtrip_qfen() {
78        let qfen = "AbCd/..../..../....";
79        let bb = bb_from_qfen(qfen).unwrap();
80        assert_eq!(bb_to_qfen(&bb), qfen);
81    }
82
83    #[test]
84    fn mixed_position() {
85        let qfen = "A.bC/..../d..B/...a";
86        let bb = bb_from_qfen(qfen).unwrap();
87        assert_eq!(bb_to_qfen(&bb), qfen);
88    }
89
90    #[test]
91    fn invalid_qfen_rank_count() {
92        assert!(bb_from_qfen("..../....").is_err());
93    }
94
95    #[test]
96    fn invalid_qfen_char() {
97        assert!(bb_from_qfen("X.../..../..../....").is_err());
98    }
99}