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]
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 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
88fn 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#[derive(PartialEq, Props, Clone)]
133pub struct ChessboardProps {
134 is_interactive: Option<bool>,
138 color: PlayerColor,
140 position: Option<String>,
142 pieces_set: Option<PieceSet>,
144 action: Option<Action>,
146 uci_tx: Option<Coroutine<String>>,
148}
149
150static NEXT_ACTION: AtomicU32 = AtomicU32::new(0);
152
153static PROCESSED_ACTION: AtomicU32 = AtomicU32::new(1);
156
157#[derive(Debug, Clone, PartialEq)]
158pub struct Action {
159 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)]
181pub(crate) enum ActionInner {
183 MakeUciMove(String),
184 RevertMove,
185}
186
187struct CompleteChessboardProps {
189 is_interactive: bool,
190 color: PlayerColor,
191 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#[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}