chess/
chess_gui.rs

1use std::cmp::max;
2
3use ggez::event::{KeyCode, KeyMods, MouseButton};
4use ggez::{event, graphics, Context, GameError, GameResult};
5use log::{debug, info};
6use unicode_segmentation::UnicodeSegmentation;
7
8use crate::{
9    Align, Button, Chess, GameState, Square, Theme, ALL_SQUARES, BOARD_CELL_PX_SIZE, BOARD_PX_SIZE,
10    BOARD_SIZE, INDEX_THEME, NUM_THEMES, SIDE_SCREEN_PX_SIZE, THEMES,
11};
12
13/// GUI for the [`Chess`] game.
14#[derive(Debug)]
15pub struct ChessGui {
16    pub(crate) chess: Chess,
17    theme: Theme,
18    buttons: Vec<Button>,
19}
20
21impl ChessGui {
22    /// Create a new instance of ChessGui.
23    pub fn new(chess: Chess, theme: Theme, buttons: Vec<Button>) -> Self {
24        ChessGui {
25            chess,
26            theme,
27            buttons,
28        }
29    }
30
31    /// Reset The chess game and buttons but not the theme.
32    pub fn reset(&mut self) {
33        self.chess.reset();
34        self.buttons.clear();
35        self.init_buttons();
36    }
37
38    /// Set the theme for the GUI.
39    ///
40    /// # Examples
41    ///
42    /// ```
43    /// use chess::{ChessGui, THEME_SANDCASTLE};
44    ///
45    /// let mut game = ChessGui::default();
46    /// game.set_theme(THEME_SANDCASTLE);
47    /// ```
48    pub fn set_theme(&mut self, theme: Theme) {
49        self.theme = theme;
50    }
51
52    /// Set the theme to the next one for the GUI.
53    ///
54    /// # Safety
55    ///
56    /// This function use/set a static variable.
57    pub unsafe fn next_theme(&mut self) {
58        INDEX_THEME = (INDEX_THEME + 1) % NUM_THEMES;
59        self.theme = THEMES[INDEX_THEME % 6];
60    }
61
62    /// Add a button in the GUI.
63    pub fn add_button(&mut self, button: Button) {
64        self.buttons.push(button);
65    }
66
67    /// Set all the buttons in the GUI.
68    fn init_buttons(&mut self) {
69        self.buttons.push(
70            Button::new(
71                "theme",
72                true,
73                graphics::Rect::new(
74                    BOARD_PX_SIZE.0 + SIDE_SCREEN_PX_SIZE.0 - 70.0,
75                    20.0,
76                    50.0,
77                    50.0,
78                ),
79                graphics::Color::new(1.0, 1.0, 1.0, 1.0),
80                "Theme",
81                Align::Center,
82                Some(|chess_gui| unsafe {
83                    chess_gui.next_theme();
84                }),
85            )
86            .set_image(self.theme.theme_icon_path),
87        );
88        self.buttons.push(Button::new(
89            "undo",
90            true,
91            graphics::Rect::new(
92                BOARD_PX_SIZE.0 + 20.0,
93                SIDE_SCREEN_PX_SIZE.1 - 210.0,
94                150.0,
95                50.0,
96            ),
97            graphics::Color::new(0.65, 0.44, 0.78, 1.0),
98            "Undo",
99            Align::Center,
100            Some(|chess_gui| {
101                chess_gui.chess.undo();
102            }),
103        ));
104        self.buttons.push(Button::new(
105            "declare-draw",
106            false,
107            graphics::Rect::new(
108                BOARD_PX_SIZE.0 + 190.0,
109                SIDE_SCREEN_PX_SIZE.1 - 210.0,
110                150.0,
111                50.0,
112            ),
113            graphics::Color::new(0.89, 0.8, 0.35, 1.0),
114            "Declare Draw",
115            Align::Center,
116            Some(|chess_gui| {
117                chess_gui.chess.declare_draw();
118            }),
119        ));
120        self.buttons.push(Button::new(
121            "offer-draw",
122            true,
123            graphics::Rect::new(
124                BOARD_PX_SIZE.0 + 20.0,
125                SIDE_SCREEN_PX_SIZE.1 - 140.0,
126                150.0,
127                50.0,
128            ),
129            graphics::Color::new(1.0, 0.64, 0.38, 1.0),
130            "Offer Draw",
131            Align::Center,
132            Some(|chess_gui| {
133                chess_gui.chess.offer_draw();
134            }),
135        ));
136        self.buttons.push(Button::new(
137            "accept-draw",
138            false,
139            graphics::Rect::new(
140                BOARD_PX_SIZE.0 + 190.0,
141                SIDE_SCREEN_PX_SIZE.1 - 140.0,
142                150.0,
143                50.0,
144            ),
145            graphics::Color::new(0.56, 0.78, 0.4, 1.0),
146            "Accept Draw",
147            Align::Center,
148            Some(|chess_gui| {
149                chess_gui.chess.accept_draw();
150            }),
151        ));
152        self.buttons.push(Button::new(
153            "reset",
154            true,
155            graphics::Rect::new(
156                BOARD_PX_SIZE.0 + 20.0,
157                SIDE_SCREEN_PX_SIZE.1 - 70.0,
158                150.0,
159                50.0,
160            ),
161            graphics::Color::new(0.65, 0.44, 0.78, 1.0),
162            "Reset",
163            Align::Center,
164            Some(|chess_gui| {
165                chess_gui.reset();
166            }),
167        ));
168        self.buttons.push(Button::new(
169            "resign",
170            true,
171            graphics::Rect::new(
172                BOARD_PX_SIZE.0 + 190.0,
173                SIDE_SCREEN_PX_SIZE.1 - 70.0,
174                150.0,
175                50.0,
176            ),
177            graphics::Color::new(0.98, 0.3, 0.3, 1.0),
178            "Resign",
179            Align::Center,
180            Some(|chess_gui| {
181                chess_gui.chess.resign(chess_gui.chess.board.side_to_move());
182            }),
183        ));
184    }
185
186    /// Base function to call when a user click on the screen.
187    pub fn click(&mut self, x: f32, y: f32) {
188        if x < BOARD_PX_SIZE.0 && self.chess.state.is_ongoing() {
189            self.click_on_board(x, y);
190        } else {
191            self.click_on_side(x, y);
192        }
193    }
194
195    /// React when the user click on the board screen.
196    ///
197    /// It is the callers responsibility to ensure the coordinate is in the board.
198    fn click_on_board(&mut self, x: f32, y: f32) {
199        let current_square = Square::from_screen(x, y);
200        debug!("Click at: ({x},{y}) -> on the square: {current_square}");
201        match self.chess.square_focused {
202            Some(square_selected) => self.chess.play(square_selected, current_square),
203            None => {
204                if self
205                    .chess
206                    .board
207                    .color_on_is(current_square, self.chess.board.side_to_move())
208                {
209                    self.chess.square_focused = Some(current_square);
210                }
211            }
212        }
213    }
214
215    /// React when the user click on the side screen.
216    ///
217    /// It is the callers responsibility to ensure the coordinate is in the side.
218    fn click_on_side(&mut self, x: f32, y: f32) {
219        info!("Click at: ({x},{y}) -> on the side screen");
220        let buttons = self.buttons.clone();
221        for button in buttons.iter() {
222            if button.contains(x, y) {
223                button.clicked(self);
224            }
225        }
226    }
227
228    /// Draw all of the board side.
229    fn draw_board(&self, ctx: &mut Context) -> GameResult {
230        self.draw_empty_board(ctx)?;
231        self.draw_legal_moves(ctx)?;
232        self.draw_pinned_piece(ctx)?;
233        self.draw_content_board(ctx)?;
234        Ok(())
235    }
236
237    /// Draw the empty chess board (without pieces).
238    fn draw_empty_board(&self, ctx: &mut Context) -> GameResult {
239        for y in 0..BOARD_SIZE.1 {
240            for x in 0..BOARD_SIZE.0 {
241                let color_index = if (x % 2 == 1 && y % 2 == 1) || (x % 2 == 0 && y % 2 == 0) {
242                    0
243                } else {
244                    1
245                };
246                let mesh = graphics::MeshBuilder::new()
247                    .rectangle(
248                        graphics::DrawMode::fill(),
249                        graphics::Rect::new(
250                            x as f32 * BOARD_CELL_PX_SIZE.0,
251                            y as f32 * BOARD_CELL_PX_SIZE.1,
252                            BOARD_CELL_PX_SIZE.0,
253                            BOARD_CELL_PX_SIZE.1,
254                        ),
255                        self.theme.board_color[color_index],
256                    )?
257                    .build(ctx)?;
258                graphics::draw(ctx, &mesh, graphics::DrawParam::default())?;
259            }
260        }
261        Ok(())
262    }
263
264    /// Draw pieces on the board.
265    fn draw_content_board(&self, ctx: &mut Context) -> GameResult {
266        let mut path;
267        let mut image;
268        for square in ALL_SQUARES {
269            if let Some((piece, color)) = self.chess.board.on(square) {
270                path = self.theme.piece_path[color.to_index()][piece.to_index()];
271                image = graphics::Image::new(ctx, path).expect("Image load error");
272                let (x, y) = square.to_screen();
273                let dest_point = [x, y];
274                let image_scale = [0.5, 0.5];
275                let dp = graphics::DrawParam::new()
276                    .dest(dest_point)
277                    .scale(image_scale);
278                graphics::draw(ctx, &image, dp)?;
279            }
280        }
281        Ok(())
282    }
283
284    /// Draw all the possible destination of the selected piece.
285    fn draw_legal_moves(&self, ctx: &mut Context) -> GameResult {
286        if self.theme.valid_moves_color.is_some() {
287            if let Some(square) = self.chess.square_focused {
288                for dest in self.chess.board.get_legal_moves(square) {
289                    let (x, y) = dest.to_screen();
290                    let mesh = graphics::MeshBuilder::new()
291                        .rectangle(
292                            graphics::DrawMode::fill(),
293                            graphics::Rect::new(x, y, BOARD_CELL_PX_SIZE.0, BOARD_CELL_PX_SIZE.1),
294                            self.theme.valid_moves_color.unwrap(),
295                        )?
296                        .build(ctx)?;
297                    graphics::draw(ctx, &mesh, graphics::DrawParam::default())?;
298                }
299            }
300        }
301        Ok(())
302    }
303
304    /// Draw a cross on [`Square`] that are pinned (i.e. can't move).
305    fn draw_pinned_piece(&self, ctx: &mut Context) -> GameResult {
306        if self.theme.piece_pinned_path.is_some() {
307            let mut path;
308            let mut image;
309            for square in self.chess.board.pinned() {
310                path = self.theme.piece_pinned_path.unwrap();
311                image = graphics::Image::new(ctx, path).expect("Image load error");
312                let (x, y) = square.to_screen();
313                let dest_point = [x, y];
314                // We set the scale at 1.0 because we want the same size
315                // for the image and a Board_cell
316                const SCALE: f32 = 1.0;
317                let image_scale = [
318                    SCALE * (BOARD_CELL_PX_SIZE.0 / image.width() as f32),
319                    SCALE * (BOARD_CELL_PX_SIZE.1 / image.height() as f32),
320                ];
321                let dp = graphics::DrawParam::new()
322                    .dest(dest_point)
323                    .scale(image_scale);
324                graphics::draw(ctx, &image, dp)?;
325            }
326        } else if self.theme.piece_pinned_color.is_some() {
327            for piece in self.chess.board.pinned() {
328                let (x, y) = piece.to_screen();
329                let mesh = graphics::MeshBuilder::new()
330                    .rectangle(
331                        graphics::DrawMode::fill(),
332                        graphics::Rect::new(x, y, BOARD_CELL_PX_SIZE.0, BOARD_CELL_PX_SIZE.1),
333                        self.theme.piece_pinned_color.unwrap(),
334                    )?
335                    .build(ctx)?;
336                graphics::draw(ctx, &mesh, graphics::DrawParam::default())?;
337            }
338        }
339        Ok(())
340    }
341
342    /// Draw all the side screen.
343    fn draw_side(&self, ctx: &mut Context) -> GameResult {
344        for button in self.buttons.iter() {
345            button.draw(ctx, self.theme.font_path, self.theme.font_scale)?;
346        }
347        self.draw_timers(ctx)?;
348        self.draw_winner(ctx)?;
349        Ok(())
350    }
351
352    /// Draw timers on the side screen.
353    fn draw_timers(&self, ctx: &mut Context) -> GameResult {
354        // Draw the rect background
355        let bounds_white = graphics::Rect::new(BOARD_PX_SIZE.0 + 20.0, 20.0, 115.0, 50.0);
356        let bounds_black = graphics::Rect::new(BOARD_PX_SIZE.0 + 155.0, 20.0, 115.0, 50.0);
357        let background_mesh_white = graphics::MeshBuilder::new()
358            .rectangle(
359                graphics::DrawMode::fill(),
360                bounds_white,
361                graphics::Color::new(0.5, 0.5, 0.5, 1.0),
362            )?
363            .build(ctx)?;
364        graphics::draw(ctx, &background_mesh_white, graphics::DrawParam::default())?;
365        let background_mesh_black = graphics::MeshBuilder::new()
366            .rectangle(
367                graphics::DrawMode::fill(),
368                bounds_black,
369                graphics::Color::new(0.5, 0.5, 0.5, 1.0),
370            )?
371            .build(ctx)?;
372        graphics::draw(ctx, &background_mesh_black, graphics::DrawParam::default())?;
373
374        // Draw the text
375        let text_white = format!("{}:{}", "--", "--");
376        let font = graphics::Font::new(ctx, self.theme.font_path)?;
377        let text_white = graphics::Text::new((text_white, font, self.theme.font_scale * 2.0));
378        let dest_point = [
379            bounds_white.x + (bounds_white.w - text_white.width(ctx)) / 2.0,
380            bounds_white.y + (bounds_white.h - text_white.height(ctx)) / 2.0,
381        ];
382        graphics::draw(ctx, &text_white, (dest_point,))?;
383        let text_black = format!("{}:{}", "--", "--");
384        let font = graphics::Font::new(ctx, self.theme.font_path)?;
385        let text_black = graphics::Text::new((text_black, font, self.theme.font_scale * 2.0));
386        let dest_point = [
387            bounds_black.x + (bounds_black.w - text_black.width(ctx)) / 2.0,
388            bounds_black.y + (bounds_black.h - text_black.height(ctx)) / 2.0,
389        ];
390        graphics::draw(ctx, &text_black, (dest_point, graphics::Color::BLACK))?;
391
392        Ok(())
393    }
394
395    /// Draw the winner on the side screen.
396    fn draw_winner(&self, ctx: &mut Context) -> GameResult {
397        // Draw the rect background
398        let bounds = graphics::Rect::new(
399            BOARD_PX_SIZE.0 + 20.0,
400            90.0,
401            320.0,
402            SIDE_SCREEN_PX_SIZE.1 - 250.0 - 70.0,
403        );
404        let background_mesh = graphics::MeshBuilder::new()
405            .rectangle(
406                graphics::DrawMode::stroke(3.0),
407                bounds,
408                graphics::Color::new(0.7, 0.7, 0.7, 1.0),
409            )?
410            .build(ctx)?;
411        graphics::draw(ctx, &background_mesh, graphics::DrawParam::default())?;
412
413        // Draw the text (9 caractères max avec une font_scale de 20.0)
414        let text = match self.chess.state {
415            GameState::Ongoing => {
416                let line1 = "Ongoing:".to_string();
417                let line2 = format!("{:?} turn", self.chess.board.side_to_move());
418                let line1_size = line1.graphemes(true).count();
419                let line2_size = line2.graphemes(true).count();
420                let max_size = max(line1_size, line2_size);
421                format!("{: ^max_size$}\n{}", line1, line2)
422            }
423            GameState::Checkmates(color) => {
424                format!("{:?} is checkmate\n\n    {:?} win !", color, !color)
425            }
426            GameState::Stalemate => "Draw: Stalemate".to_string(),
427            GameState::DrawAccepted => "Draw: Accepted".to_string(),
428            GameState::DrawDeclared => "Draw: Declared".to_string(),
429            GameState::Resigns(color) => format!("{:?} resigns\n\n {:?} win !", color, !color),
430        };
431        let font = graphics::Font::new(ctx, self.theme.font_path)?;
432        let text = graphics::Text::new((text, font, self.theme.font_scale * 2.0));
433        let dest_point = [
434            bounds.x + (bounds.w - text.width(ctx)) / 2.0,
435            bounds.y + (bounds.h - text.height(ctx)) / 2.0,
436        ];
437        graphics::draw(ctx, &text, (dest_point,))?;
438        Ok(())
439    }
440}
441
442impl event::EventHandler<GameError> for ChessGui {
443    /// Update will happen on every frame before it is drawn.
444    fn update(&mut self, _ctx: &mut Context) -> GameResult {
445        for button in self.buttons.iter_mut() {
446            match button.id {
447                "declare-draw" => {
448                    if self.chess.can_declare_draw() {
449                        button.enable();
450                    } else {
451                        button.disable();
452                    }
453                }
454                "accept-draw" => {
455                    if self.chess.offer_draw {
456                        button.enable();
457                    } else {
458                        button.disable();
459                    }
460                }
461                _ => {}
462            }
463        }
464        if self.chess.state.is_finish() {
465            for button in self.buttons.iter_mut() {
466                match button.id {
467                    "reset" | "theme" => {}
468                    _ => button.disable(),
469                }
470            }
471        }
472        Ok(())
473    }
474
475    /// Render the game's current state.
476    fn draw(&mut self, ctx: &mut Context) -> GameResult {
477        // First we clear the screen and set the background color
478        graphics::clear(ctx, self.theme.background_color);
479
480        // Draw the board and the side screen (that contains all button/info)
481        self.draw_board(ctx)?;
482        self.draw_side(ctx)?;
483
484        // Finally we call graphics::present to cycle the gpu's framebuffer and display
485        // the new frame we just drew.
486        graphics::present(ctx)?;
487
488        // And return success.
489        Ok(())
490    }
491
492    /// Called every time a mouse button gets pressed
493    fn mouse_button_down_event(&mut self, _ctx: &mut Context, button: MouseButton, x: f32, y: f32) {
494        if button == MouseButton::Left {
495            self.click(x, y);
496        }
497    }
498
499    /// Change the [`ggez::input::mouse::CursorIcon`] when the mouse is on a button.
500    fn mouse_motion_event(&mut self, ctx: &mut Context, x: f32, y: f32, _dx: f32, _dy: f32) {
501        if x > BOARD_PX_SIZE.0 {
502            let mut on_button = false;
503            for button in self.buttons.iter() {
504                if button.contains(x, y) {
505                    on_button = true;
506                    break;
507                }
508            }
509            if on_button {
510                ggez::input::mouse::set_cursor_type(ctx, ggez::input::mouse::CursorIcon::Hand);
511            } else {
512                ggez::input::mouse::set_cursor_type(ctx, ggez::input::mouse::CursorIcon::Default);
513            }
514        }
515    }
516
517    /// Called every time a key gets pressed.
518    ///
519    /// # Keys
520    ///
521    /// |  Keys  |          Actions           |
522    /// |--------|----------------------------|
523    /// | Escape | Quit the game              |
524    /// | R      | Reset the game and buttons |
525    /// | CTRL+Z | Undo                       |
526    fn key_down_event(
527        &mut self,
528        ctx: &mut Context,
529        keycode: KeyCode,
530        keymod: KeyMods,
531        _repeat: bool,
532    ) {
533        match keycode {
534            KeyCode::Escape => event::quit(ctx),
535            KeyCode::R => self.reset(),
536            KeyCode::Z if keymod == KeyMods::CTRL => self.chess.undo(),
537            _ => {}
538        };
539    }
540}
541
542impl Default for ChessGui {
543    fn default() -> Self {
544        let mut chess_gui = ChessGui::new(
545            Default::default(),
546            Default::default(),
547            Vec::with_capacity(7),
548        );
549        chess_gui.init_buttons();
550        chess_gui
551    }
552}