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 let move_coord_i = crate::Move::Coordinate { x: 8, y: 18 }; 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 let move_coord_j = Move::Coordinate('J', 1); assert_eq!(
155 crate::Move::from_gtp(move_coord_j, board_size).unwrap(),
156 crate::Move::Coordinate { x: 8, y: 18 }
157 );
158 }
159}