chessnut_move/protocol/position.rs
1// Copyright 2026 Daymon Littrell-Reyes
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Canonical chess positions and piece identities.
16
17#[cfg(doc)]
18use crate::protocol;
19use crate::protocol::{SQUARE_COUNT, Square};
20
21/// Number of packed bytes used to encode all 64 board squares.
22pub const BOARD_STATE_LENGTH: usize = 32;
23
24/// Piece occupancy for all 64 squares.
25///
26/// The internal array uses canonical A1-to-H8 indexing. Use
27/// [`Position::piece_at`] and [`Position::set_piece`] when the square is known
28/// by file and rank.
29///
30/// Realtime board updates carry positions through
31/// [`BoardEvent::PositionChanged`][protocol::BoardEvent::PositionChanged].
32///
33/// # Examples
34///
35/// ```
36/// use chessnut_move::protocol::{
37/// Color, File, Piece, PieceKind, Position, Rank, SQUARE_COUNT, Square,
38/// };
39///
40/// let mut position = Position::new([None; SQUARE_COUNT]);
41/// let e4 = Square::new(File::E, Rank::Four);
42/// position.set_piece(
43/// e4,
44/// Some(Piece {
45/// color: Color::White,
46/// kind: PieceKind::Pawn,
47/// }),
48/// );
49/// assert!(position.piece_at(e4).is_some());
50/// ```
51#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
52pub struct Position {
53 squares: [Option<Piece>; SQUARE_COUNT],
54}
55
56impl Position {
57 /// Creates a position from squares in canonical A1-to-H8 order.
58 ///
59 /// Index zero represents A1 and index 63 represents H8.
60 ///
61 /// # Examples
62 ///
63 /// ```
64 /// use chessnut_move::protocol::{
65 /// Color, File, Piece, PieceKind, Position, Rank, SQUARE_COUNT, Square,
66 /// };
67 ///
68 /// let white_king = Piece {
69 /// color: Color::White,
70 /// kind: PieceKind::King,
71 /// };
72 /// let e1 = Square::new(File::E, Rank::One);
73 /// let mut squares = [None; SQUARE_COUNT];
74 /// squares[e1.index()] = Some(white_king);
75 ///
76 /// let position = Position::new(squares);
77 /// assert_eq!(position.piece_at(e1), Some(white_king));
78 /// ```
79 pub fn new(squares: [Option<Piece>; SQUARE_COUNT]) -> Self {
80 Self { squares }
81 }
82
83 /// Returns the piece occupying a square, or `None` when it is empty.
84 ///
85 /// # Examples
86 ///
87 /// ```
88 /// use chessnut_move::protocol::{
89 /// BoardEvent, File, PieceKind, Rank, Square,
90 /// };
91 ///
92 /// fn pawn_reached_e4(event: BoardEvent) -> bool {
93 /// let BoardEvent::PositionChanged(position) = event else {
94 /// return false;
95 /// };
96 ///
97 /// position
98 /// .piece_at(Square::new(File::E, Rank::Four))
99 /// .is_some_and(|piece| piece.kind == PieceKind::Pawn)
100 /// }
101 /// # let _ = pawn_reached_e4;
102 /// ```
103 pub const fn piece_at(&self, square: Square) -> Option<Piece> {
104 self.squares[square.index()]
105 }
106
107 /// Replaces the piece occupying a square.
108 ///
109 /// Pass `None` to empty the square.
110 ///
111 /// # Examples
112 ///
113 /// ```
114 /// use chessnut_move::protocol::{
115 /// AutoMoveMode, Command, File, Position, Rank, Square,
116 /// };
117 ///
118 /// fn move_e2_to_e4(mut observed: Position) -> Command {
119 /// let e2 = Square::new(File::E, Rank::Two);
120 /// let e4 = Square::new(File::E, Rank::Four);
121 /// let pawn = observed.piece_at(e2);
122 ///
123 /// observed.set_piece(e2, None);
124 /// observed.set_piece(e4, pawn);
125 /// Command::auto_move(observed, AutoMoveMode::Normal)
126 /// }
127 /// # let _ = move_e2_to_e4;
128 /// ```
129 pub fn set_piece(&mut self, square: Square, piece: Option<Piece>) {
130 self.squares[square.index()] = piece;
131 }
132
133 /// Returns canonical square storage for private wire encoders.
134 pub(crate) const fn squares(&self) -> &[Option<Piece>; SQUARE_COUNT] {
135 &self.squares
136 }
137}
138
139/// A chess piece described by its [color] and [kind].
140///
141/// # Examples
142///
143/// ```
144/// use chessnut_move::protocol::{Color, Piece, PieceKind};
145///
146/// let queen = Piece {
147/// color: Color::Black,
148/// kind: PieceKind::Queen,
149/// };
150/// ```
151///
152/// [color]: Color
153/// [kind]: PieceKind
154#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
155pub struct Piece {
156 /// Side to which the piece belongs.
157 pub color: Color,
158
159 /// Chess role of the piece.
160 pub kind: PieceKind,
161}
162
163/// Side to which a chess piece belongs.
164#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
165pub enum Color {
166 /// White side.
167 White,
168
169 /// Black side.
170 Black,
171}
172
173/// Chess role of a piece.
174#[derive(PartialEq, Eq, Ord, PartialOrd, Copy, Clone, Debug, Hash)]
175pub enum PieceKind {
176 /// The Pawn piece on a Chess board.
177 Pawn,
178
179 /// The Knight piece on a Chess board.
180 Knight,
181
182 /// The Bishop piece on a Chess board.
183 Bishop,
184
185 /// The Rook piece on a Chess board.
186 Rook,
187
188 /// The Queen piece on a Chess board.
189 Queen,
190
191 /// The King piece on a Chess board.
192 King,
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use crate::protocol::{File, Rank};
199
200 #[test]
201 fn pieces_are_accessed_by_square() {
202 let square = Square::new(File::E, Rank::Four);
203 let piece = Piece {
204 color: Color::White,
205 kind: PieceKind::Pawn,
206 };
207 let mut position = Position::new([None; SQUARE_COUNT]);
208
209 position.set_piece(square, Some(piece));
210
211 assert_eq!(position.piece_at(square), Some(piece));
212 }
213}