dioxus_chessboard/
chessboard.rs

1use crate::files::Files;
2use crate::historical_board::HistoricalBoard;
3use crate::move_builder::MoveBuilder;
4use crate::promotion::Promotion;
5use crate::ranks::Ranks;
6use crate::square::Square;
7use crate::PieceSet;
8use dioxus::prelude::*;
9use owlchess::board::PrettyStyle;
10use owlchess::{Coord, File, Rank};
11use std::fmt::{Debug, Display};
12use std::sync::atomic::AtomicU32;
13use std::sync::atomic::Ordering::Relaxed;
14use tracing::{debug, info, warn};
15
16const CHESSBOARD_STYLES: Asset = asset!("/public/css/chessboard.css");
17
18/// Component rendering [Chessboard].
19#[component]
20pub fn Chessboard(props: ChessboardProps) -> Element {
21    let props = props.complete();
22    debug!("{props:?}");
23
24    use_context_provider(|| {
25        Signal::new(
26            HistoricalBoard::from_fen(&props.position)
27                .expect("Board must be constructible from a valid position"),
28        )
29    });
30
31    use_context_provider(|| Signal::new(MoveBuilder::new(props.uci_tx)));
32
33    let historical_board = use_context::<Signal<HistoricalBoard>>();
34    let mut move_builder = use_context::<Signal<MoveBuilder>>();
35
36    if let Some(action) = props.action {
37        maybe_update_board(
38            action,
39            props.is_interactive,
40            &historical_board,
41            &mut move_builder,
42        );
43    }
44
45    let (files, ranks) = match props.color {
46        PlayerColor::White => (
47            File::iter().collect::<Vec<_>>(),
48            Rank::iter().collect::<Vec<_>>(),
49        ),
50        PlayerColor::Black => (
51            File::iter().collect::<Vec<_>>().into_iter().rev().collect(),
52            Rank::iter().collect::<Vec<_>>().into_iter().rev().collect(),
53        ),
54    };
55
56    let mut chessboard_classes = vec!["chessboard"];
57
58    if move_builder.read().check_promotion().is_some() {
59        // Promotion is required.
60        chessboard_classes.push("opacity-25");
61    }
62
63    rsx! {
64        document::Link { rel: "stylesheet", href: CHESSBOARD_STYLES }
65
66        div { position: "relative",
67            div { class: chessboard_classes.join(" "),
68                for r in ranks.iter().cloned() {
69                    div { class: "row",
70                        for f in files.iter().cloned() {
71                            Square {
72                                is_interactive: props.is_interactive,
73                                coord: Coord::from_parts(f, r),
74                                color: props.color,
75                                pieces_set: props.pieces_set,
76                            }
77                        }
78                    }
79                }
80            }
81            Ranks { color: props.color }
82            Files { color: props.color }
83            Promotion { color: props.color, pieces_set: props.pieces_set }
84        }
85    }
86}
87
88/// Examine [Action] and apply respective changes if the action has not yet been processed.
89/// If the action was processed, does nothing.
90/// If the board is not interactive, mark the action as processed.
91fn maybe_update_board(
92    action: Action,
93    is_interactive: bool,
94    historical_board: &Signal<HistoricalBoard>,
95    move_builder: &mut Signal<MoveBuilder>,
96) {
97    let processed_action = PROCESSED_ACTION.load(Relaxed);
98    if processed_action == action.discriminator {
99        return;
100    }
101    PROCESSED_ACTION.store(action.discriminator, Relaxed);
102
103    if !is_interactive {
104        debug!("Chessboard is not interactive. Ignoring the request...");
105        return;
106    }
107
108    debug!("Applying action: {action:?}");
109
110    let board = historical_board.read();
111
112    match action.action {
113        ActionInner::MakeUciMove(uci) => {
114            if move_builder.write().apply_uci_move(&uci, &board).is_ok() {
115                info!("Injected move: {uci}");
116            } else {
117                warn!(
118                    "Injected move {uci} is not legal in the current position\n{}",
119                    board.pretty(PrettyStyle::Utf8)
120                );
121            }
122        }
123        ActionInner::RevertMove => {
124            if let Some(m) = board.last_move() {
125                move_builder.write().revert_move(m);
126            }
127        }
128    }
129}
130
131/// [Chessboard] properties.
132#[derive(PartialEq, Props, Clone)]
133pub struct ChessboardProps {
134    /// Is the board interactive?
135    /// If you only need to display a position, set this to false.
136    /// By default, the board will be interactive.
137    is_interactive: Option<bool>,
138    /// Color the player plays for, i.e., pieces at the bottom.
139    color: PlayerColor,
140    /// Starting position in FEN notation.
141    position: Option<String>,
142    /// Pieces set.
143    pieces_set: Option<PieceSet>,
144    /// Injected action.
145    action: Option<Action>,
146    /// Transmitter channel of moves made on the board.
147    uci_tx: Option<Coroutine<String>>,
148}
149
150/// Action counter to make every [ActionInner] unique, i.e., [UniqueAction].
151static NEXT_ACTION: AtomicU32 = AtomicU32::new(0);
152
153/// Keeps track which injected [UniqueAction]'s have been processed.
154/// At initialization, this value must be different from the one in [NEXT_ACTION].
155static PROCESSED_ACTION: AtomicU32 = AtomicU32::new(1);
156
157#[derive(Debug, Clone, PartialEq)]
158pub struct Action {
159    /// Value allowing to discriminate instances of this variant.
160    discriminator: u32,
161    action: ActionInner,
162}
163
164impl Action {
165    pub fn make_move(m: &str) -> Self {
166        Self {
167            discriminator: NEXT_ACTION.fetch_add(1, Relaxed),
168            action: ActionInner::MakeUciMove(m.to_string()),
169        }
170    }
171
172    pub fn revert_move() -> Action {
173        Self {
174            discriminator: NEXT_ACTION.fetch_add(1, Relaxed),
175            action: ActionInner::RevertMove,
176        }
177    }
178}
179
180#[derive(Debug, Clone, PartialEq)]
181/// List of action [Chessboard] can receive via its client.
182pub(crate) enum ActionInner {
183    MakeUciMove(String),
184    RevertMove,
185}
186
187/// Complete properties with absent optional values of [ChessboardProps] filled with default values.
188struct CompleteChessboardProps {
189    is_interactive: bool,
190    color: PlayerColor,
191    /// Starting position in FEN notation.
192    position: String,
193    pieces_set: PieceSet,
194    action: Option<Action>,
195    uci_tx: Option<Coroutine<String>>,
196}
197
198impl Debug for CompleteChessboardProps {
199    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200        f.debug_struct("CompleteChessboardProps")
201            .field("is_interactive", &self.is_interactive)
202            .field("color", &self.color)
203            .field("position", &self.position)
204            .field("pieces_set", &self.pieces_set)
205            .field("action", &self.action)
206            .finish()
207    }
208}
209
210impl ChessboardProps {
211    fn complete(self) -> CompleteChessboardProps {
212        CompleteChessboardProps {
213            is_interactive: self.is_interactive.unwrap_or(true),
214            color: self.color,
215            position: self
216                .position
217                .unwrap_or_else(|| Self::default_position().to_string()),
218            pieces_set: self.pieces_set.unwrap_or(PieceSet::Standard),
219            action: self.action,
220            uci_tx: self.uci_tx,
221        }
222    }
223    fn default_position() -> &'static str {
224        "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
225    }
226}
227
228/// Color of the player's pieces.
229#[derive(PartialEq, Clone, Copy, Debug)]
230pub enum PlayerColor {
231    White,
232    Black,
233}
234
235impl PlayerColor {
236    pub fn flip(&mut self) {
237        match self {
238            Self::White => *self = Self::Black,
239            Self::Black => *self = Self::White,
240        }
241    }
242}
243
244impl Display for PlayerColor {
245    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246        match self {
247            Self::White => write!(f, "White"),
248            Self::Black => write!(f, "Black"),
249        }
250    }
251}