Skip to content

Commit b1dd810

Browse files
Merge pull request #170 from pleco-rs/copilot/add-bitmove-deserialization
Implement FromStr for BitMove and SQ
2 parents f54c0c2 + 7094b87 commit b1dd810

File tree

3 files changed

+188
-0
lines changed

3 files changed

+188
-0
lines changed

pleco/src/core/piece_move.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
6464
use std::cmp::{Ord, Ordering, PartialEq, PartialOrd};
6565
use std::fmt;
66+
use std::str::FromStr;
6667

6768
use super::sq::SQ;
6869
use super::*;
@@ -146,6 +147,77 @@ impl fmt::Display for BitMove {
146147
}
147148
}
148149

150+
impl FromStr for BitMove {
151+
type Err = BitMoveFromStrError;
152+
153+
/// Parses a `BitMove` from a UCI move string (e.g., "e2e4", "a7a8q").
154+
///
155+
/// The format is: source square, destination square, and an optional promotion piece
156+
/// character ('n', 'b', 'r', 'q').
157+
///
158+
/// Note: Without board context, only the source, destination, and promotion information
159+
/// can be determined. Flags such as capture, en passant, castle, or double pawn push
160+
/// cannot be inferred from the string alone.
161+
///
162+
/// # Examples
163+
///
164+
/// ```rust
165+
/// use pleco::BitMove;
166+
/// use std::str::FromStr;
167+
///
168+
/// let mv = BitMove::from_str("e2e4").unwrap();
169+
/// assert_eq!(mv.to_string(), "e2e4");
170+
///
171+
/// let promo = BitMove::from_str("a7a8q").unwrap();
172+
/// assert!(promo.is_promo());
173+
/// ```
174+
fn from_str(s: &str) -> Result<Self, Self::Err> {
175+
let len = s.len();
176+
if len < 4 || len > 5 {
177+
return Err(BitMoveFromStrError);
178+
}
179+
let src = s[0..2].parse::<SQ>().map_err(|_| BitMoveFromStrError)?;
180+
let dst = s[2..4].parse::<SQ>().map_err(|_| BitMoveFromStrError)?;
181+
182+
if len == 5 {
183+
let promo_char = s.as_bytes()[4];
184+
let prom = match promo_char {
185+
b'n' => PieceType::N,
186+
b'b' => PieceType::B,
187+
b'r' => PieceType::R,
188+
b'q' => PieceType::Q,
189+
_ => return Err(BitMoveFromStrError),
190+
};
191+
// Capture status cannot be determined without board context
192+
Ok(BitMove::init(PreMoveInfo {
193+
src,
194+
dst,
195+
flags: MoveFlag::Promotion {
196+
capture: false,
197+
prom,
198+
},
199+
}))
200+
} else {
201+
// Without board context, flags like capture, en passant, castle,
202+
// or double pawn push cannot be inferred from the string alone
203+
Ok(BitMove::make(BitMove::FLAG_QUIET, src, dst))
204+
}
205+
}
206+
}
207+
208+
/// Error type for parsing a `BitMove` from a string.
209+
#[derive(Clone, Debug, PartialEq, Eq)]
210+
pub struct BitMoveFromStrError;
211+
212+
impl fmt::Display for BitMoveFromStrError {
213+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
214+
write!(
215+
f,
216+
"invalid move string, expected format like 'e2e4' or 'a7a8q'"
217+
)
218+
}
219+
}
220+
149221
// https://chessprogramming.wikispaces.com/Encoding+Moves
150222
impl BitMove {
151223
pub const FLAG_QUIET: u16 = 0b0000;

pleco/src/core/sq.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ use super::*;
5959
use std::fmt;
6060
use std::mem::transmute;
6161
use std::ops::*;
62+
use std::str::FromStr;
6263

6364
// TODO: Investigate possibility of using an Enum instead
6465

@@ -392,3 +393,46 @@ impl fmt::Display for SQ {
392393
write!(f, "{}", SQ_DISPLAY[self.0 as usize])
393394
}
394395
}
396+
397+
impl FromStr for SQ {
398+
type Err = SQFromStrError;
399+
400+
/// Parses a `SQ` from a string in algebraic notation (e.g., "a1", "h8").
401+
///
402+
/// # Examples
403+
///
404+
/// ```rust
405+
/// use pleco::SQ;
406+
/// use std::str::FromStr;
407+
///
408+
/// let sq = SQ::from_str("e4").unwrap();
409+
/// assert_eq!(sq, SQ::E4);
410+
/// ```
411+
fn from_str(s: &str) -> Result<Self, Self::Err> {
412+
let bytes = s.as_bytes();
413+
if bytes.len() != 2 {
414+
return Err(SQFromStrError);
415+
}
416+
let file = bytes[0];
417+
let rank = bytes[1];
418+
if !(b'a'..=b'h').contains(&file) || !(b'1'..=b'8').contains(&rank) {
419+
return Err(SQFromStrError);
420+
}
421+
let file_idx = file - b'a';
422+
let rank_idx = rank - b'1';
423+
Ok(SQ(rank_idx * 8 + file_idx))
424+
}
425+
}
426+
427+
/// Error type for parsing a `SQ` from a string.
428+
#[derive(Clone, Debug, PartialEq, Eq)]
429+
pub struct SQFromStrError;
430+
431+
impl fmt::Display for SQFromStrError {
432+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
433+
write!(
434+
f,
435+
"invalid square string, expected format like 'a1' through 'h8'"
436+
)
437+
}
438+
}

pleco/tests/move_generating.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use pleco::board::{Board, RandBoard};
44
use pleco::core::piece_move::*;
55
use pleco::core::*;
66
use pleco::SQ;
7+
use std::str::FromStr;
78

89
#[test]
910
fn test_movegen_captures() {
@@ -333,3 +334,74 @@ fn all_move_flags() -> Vec<MoveFlag> {
333334
move_flags.push(MoveFlag::QuietMove);
334335
move_flags
335336
}
337+
338+
#[test]
339+
fn sq_from_str() {
340+
assert_eq!(SQ::from_str("a1").unwrap(), SQ::A1);
341+
assert_eq!(SQ::from_str("h8").unwrap(), SQ::H8);
342+
assert_eq!(SQ::from_str("e4").unwrap(), SQ::E4);
343+
assert_eq!(SQ::from_str("d7").unwrap(), SQ::D7);
344+
assert!(SQ::from_str("").is_err());
345+
assert!(SQ::from_str("a").is_err());
346+
assert!(SQ::from_str("a9").is_err());
347+
assert!(SQ::from_str("i1").is_err());
348+
assert!(SQ::from_str("a1b").is_err());
349+
}
350+
351+
#[test]
352+
fn sq_from_str_roundtrip() {
353+
for i in 0..64u8 {
354+
let sq = SQ(i);
355+
let s = sq.to_string();
356+
let parsed = SQ::from_str(&s).unwrap();
357+
assert_eq!(sq, parsed);
358+
}
359+
}
360+
361+
#[test]
362+
fn bitmove_from_str_quiet() {
363+
let mv = BitMove::from_str("e2e4").unwrap();
364+
assert_eq!(mv.get_src(), SQ::E2);
365+
assert_eq!(mv.get_dest(), SQ::E4);
366+
assert!(!mv.is_promo());
367+
assert_eq!(mv.to_string(), "e2e4");
368+
}
369+
370+
#[test]
371+
fn bitmove_from_str_promotion() {
372+
let mv = BitMove::from_str("a7a8q").unwrap();
373+
assert_eq!(mv.get_src(), SQ::A7);
374+
assert_eq!(mv.get_dest(), SQ::A8);
375+
assert!(mv.is_promo());
376+
assert_eq!(mv.promo_piece(), PieceType::Q);
377+
378+
let mv = BitMove::from_str("b7b8n").unwrap();
379+
assert!(mv.is_promo());
380+
assert_eq!(mv.promo_piece(), PieceType::N);
381+
382+
let mv = BitMove::from_str("c7c8b").unwrap();
383+
assert!(mv.is_promo());
384+
assert_eq!(mv.promo_piece(), PieceType::B);
385+
386+
let mv = BitMove::from_str("d7d8r").unwrap();
387+
assert!(mv.is_promo());
388+
assert_eq!(mv.promo_piece(), PieceType::R);
389+
}
390+
391+
#[test]
392+
fn bitmove_from_str_invalid() {
393+
assert!(BitMove::from_str("").is_err());
394+
assert!(BitMove::from_str("e2").is_err());
395+
assert!(BitMove::from_str("e2e4e6").is_err());
396+
assert!(BitMove::from_str("e2e9").is_err());
397+
assert!(BitMove::from_str("e2e4x").is_err());
398+
}
399+
400+
#[test]
401+
fn bitmove_from_str_roundtrip_quiet() {
402+
let original = BitMove::make_quiet(SQ::E2, SQ::E4);
403+
let s = original.to_string();
404+
let parsed = BitMove::from_str(&s).unwrap();
405+
assert_eq!(parsed.get_src(), original.get_src());
406+
assert_eq!(parsed.get_dest(), original.get_dest());
407+
}

0 commit comments

Comments
 (0)