carina_libgo/
gtp.rs

1use core::fmt;
2use std::str::FromStr;
3
4use serde::Serialize;
5
6use crate::ParseError;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum Move {
10    Pass,
11    Coordinate(char, u8),
12}
13
14impl fmt::Display for Move {
15    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16        match self {
17            Move::Pass => write!(f, "pass"),
18            Move::Coordinate(x, y) => write!(f, "{}{}", x, y),
19        }
20    }
21}
22
23impl FromStr for Move {
24    type Err = ParseError;
25
26    fn from_str(str: &str) -> Result<Self, Self::Err> {
27        if str.is_empty() {
28            return Err(Self::Err::EmptyString);
29        }
30        let str = str.to_uppercase();
31        if str == "PASS" {
32            return Ok(Move::Pass);
33        }
34        let c = str
35            .as_bytes()
36            .get(0)
37            .ok_or_else(|| ParseError::InvalidCoordinate)?;
38        let s = str
39            .get(1..)
40            .ok_or_else(|| ParseError::InvalidCoordinate)?
41            .parse::<u8>()
42            .map_err(|_| ParseError::InvalidCoordinate)?;
43
44        Ok(Move::Coordinate(*c as char, s))
45    }
46}
47
48impl crate::Move {
49    pub fn to_gtp(self, board_size: u8) -> Result<Move, ParseError> {
50        match self {
51            crate::Move::Pass => Ok(Move::Pass),
52            crate::Move::Coordinate { x, y } => {
53                if x >= board_size || y >= board_size {
54                    return Err(ParseError::InvalidCoordinate);
55                }
56                let x = if x > 7 { x + 1 + b'A' } else { x + b'A' };
57                let y = board_size - y;
58                Ok(Move::Coordinate(x as char, y))
59            }
60        }
61    }
62
63    pub fn from_gtp(m: Move, board_size: u8) -> Result<Self, ParseError> {
64        match m {
65            Move::Pass => Ok(crate::Move::Pass),
66            Move::Coordinate(x, y) => {
67                if !x.is_ascii_uppercase() || y == 0 || y > board_size {
68                    return Err(ParseError::InvalidCoordinate);
69                }
70                let x = x as u8;
71                let x = if x > b'I' { x - b'A' - 1 } else { x - b'A' };
72                let y = board_size - y;
73                Ok(crate::Move::Coordinate { x, y })
74            }
75        }
76    }
77}
78
79impl Serialize for Move {
80    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
81    where
82        S: serde::Serializer,
83    {
84        serializer.serialize_str(&self.to_string())
85    }
86}
87
88impl<'de> serde::Deserialize<'de> for Move {
89    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
90    where
91        D: serde::Deserializer<'de>,
92    {
93        let s = String::deserialize(deserializer)?;
94        Move::from_str(&s).map_err(serde::de::Error::custom)
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn test_move_to_string() {
104        assert_eq!(Move::Pass.to_string(), "pass");
105        assert_eq!(Move::Coordinate('A', 1).to_string(), "A1");
106        assert_eq!(Move::Coordinate('B', 2).to_string(), "B2");
107    }
108
109    #[test]
110    fn test_move_from_string() {
111        assert_eq!(Move::from_str("pass").unwrap(), Move::Pass);
112        assert_eq!(Move::from_str("A1").unwrap(), Move::Coordinate('A', 1));
113        assert_eq!(Move::from_str("B2").unwrap(), Move::Coordinate('B', 2));
114    }
115
116    #[test]
117    fn test_to_gtp() {
118        let board_size = 19;
119        let move_pass = crate::Move::Pass;
120        let move_coord = crate::Move::Coordinate { x: 0, y: 1 };
121        assert_eq!(move_pass.to_gtp(board_size).unwrap(), Move::Pass);
122        assert_eq!(
123            move_coord.to_gtp(board_size).unwrap(),
124            Move::Coordinate('A', 18)
125        );
126        // test "skip I"
127        let move_coord_i = crate::Move::Coordinate { x: 8, y: 18 }; // I9
128        assert_eq!(
129            move_coord_i.to_gtp(board_size).unwrap(),
130            Move::Coordinate('J', 1)
131        );
132        let move_coord_i = crate::Move::Coordinate { x: 18, y: 18 };
133        assert_eq!(
134            move_coord_i.to_gtp(board_size).unwrap(),
135            Move::Coordinate('T', 1)
136        );
137    }
138
139    #[test]
140    fn test_from_gtp() {
141        let board_size = 19;
142        let move_pass = Move::Pass;
143        let move_coord = Move::Coordinate('A', 18);
144        assert_eq!(
145            crate::Move::from_gtp(move_pass, board_size).unwrap(),
146            crate::Move::Pass
147        );
148        assert_eq!(
149            crate::Move::from_gtp(move_coord, board_size).unwrap(),
150            crate::Move::Coordinate { x: 0, y: 1 }
151        );
152        // test "skip I"
153        let move_coord_j = Move::Coordinate('J', 1); // I9
154        assert_eq!(
155            crate::Move::from_gtp(move_coord_j, board_size).unwrap(),
156            crate::Move::Coordinate { x: 8, y: 18 }
157        );
158    }
159}