use ggez::event::{KeyCode, KeyMods, MouseButton};
use ggez::{event, graphics, Context, GameError, GameResult};
use log::{debug, info};
use crate::{
Align, Button, Chess, GameState, Square, Theme, ALL_SQUARES, BOARD_CELL_PX_SIZE, BOARD_PX_SIZE,
BOARD_SIZE, INDEX_THEME, NUM_THEMES, SIDE_SCREEN_PX_SIZE, THEMES,
};
#[derive(Debug)]
pub struct ChessGui {
pub(crate) chess: Chess,
theme: Theme,
buttons: Vec<Button>,
}
impl ChessGui {
pub fn new(chess: Chess, theme: Theme, buttons: Vec<Button>) -> Self {
ChessGui {
chess,
theme,
buttons,
}
}
pub fn reset(&mut self) {
self.chess.reset();
self.buttons.clear();
self.init_buttons();
}
pub fn set_theme(&mut self, theme: Theme) {
self.theme = theme;
}
pub unsafe fn next_theme(&mut self) {
INDEX_THEME = (INDEX_THEME + 1) % NUM_THEMES;
self.theme = THEMES[INDEX_THEME % 6];
}
pub fn add_button(&mut self, button: Button) {
self.buttons.push(button);
}
fn init_buttons(&mut self) {
self.buttons.push(
Button::new(
"theme",
true,
graphics::Rect::new(
BOARD_PX_SIZE.0 + SIDE_SCREEN_PX_SIZE.0 - 70.0,
20.0,
50.0,
50.0,
),
graphics::Color::new(1.0, 1.0, 1.0, 1.0),
"Theme",
Align::Center,
Some(|chess_gui| unsafe {
chess_gui.next_theme();
}),
)
.set_image(self.theme.theme_icon_path),
);
self.buttons.push(Button::new(
"undo",
true,
graphics::Rect::new(
BOARD_PX_SIZE.0 + 20.0,
SIDE_SCREEN_PX_SIZE.1 - 210.0,
150.0,
50.0,
),
graphics::Color::new(0.65, 0.44, 0.78, 1.0),
"Undo",
Align::Center,
Some(|chess_gui| {
chess_gui.chess.undo();
}),
));
self.buttons.push(Button::new(
"declare-draw",
false,
graphics::Rect::new(
BOARD_PX_SIZE.0 + 190.0,
SIDE_SCREEN_PX_SIZE.1 - 210.0,
150.0,
50.0,
),
graphics::Color::new(0.89, 0.8, 0.35, 1.0),
"Declare Draw",
Align::Center,
Some(|chess_gui| {
chess_gui.chess.declare_draw();
}),
));
self.buttons.push(Button::new(
"offer-draw",
true,
graphics::Rect::new(
BOARD_PX_SIZE.0 + 20.0,
SIDE_SCREEN_PX_SIZE.1 - 140.0,
150.0,
50.0,
),
graphics::Color::new(1.0, 0.64, 0.38, 1.0),
"Offer Draw",
Align::Center,
Some(|chess_gui| {
chess_gui.chess.offer_draw();
}),
));
self.buttons.push(Button::new(
"accept-draw",
false,
graphics::Rect::new(
BOARD_PX_SIZE.0 + 190.0,
SIDE_SCREEN_PX_SIZE.1 - 140.0,
150.0,
50.0,
),
graphics::Color::new(0.56, 0.78, 0.4, 1.0),
"Accept Draw",
Align::Center,
Some(|chess_gui| {
chess_gui.chess.accept_draw();
}),
));
self.buttons.push(Button::new(
"reset",
true,
graphics::Rect::new(
BOARD_PX_SIZE.0 + 20.0,
SIDE_SCREEN_PX_SIZE.1 - 70.0,
150.0,
50.0,
),
graphics::Color::new(0.65, 0.44, 0.78, 1.0),
"Reset",
Align::Center,
Some(|chess_gui| {
chess_gui.reset();
}),
));
self.buttons.push(Button::new(
"resign",
true,
graphics::Rect::new(
BOARD_PX_SIZE.0 + 190.0,
SIDE_SCREEN_PX_SIZE.1 - 70.0,
150.0,
50.0,
),
graphics::Color::new(0.98, 0.3, 0.3, 1.0),
"Resign",
Align::Center,
Some(|chess_gui| {
chess_gui.chess.resign(chess_gui.chess.board.side_to_move());
}),
));
}
pub fn click(&mut self, x: f32, y: f32) {
if x < BOARD_PX_SIZE.0 && self.chess.state.is_ongoing() {
self.click_on_board(x, y);
} else {
self.click_on_side(x, y);
}
}
fn click_on_board(&mut self, x: f32, y: f32) {
let current_square = Square::from_screen(x, y);
debug!("Click at: ({x},{y}) -> on the square: {current_square}");
match self.chess.square_focused {
Some(square_selected) => self.chess.play(square_selected, current_square),
None => {
if self
.chess
.board
.color_on_is(current_square, self.chess.board.side_to_move())
{
self.chess.square_focused = Some(current_square);
}
}
}
}
fn click_on_side(&mut self, x: f32, y: f32) {
info!("Click at: ({x},{y}) -> on the side screen");
let buttons = self.buttons.clone();
for button in buttons.iter() {
if button.contains(x, y) {
button.clicked(self);
}
}
}
fn draw_board(&self, ctx: &mut Context) -> GameResult {
self.draw_empty_board(ctx)?;
self.draw_legal_moves(ctx)?;
self.draw_pinned_piece(ctx)?;
self.draw_content_board(ctx)?;
Ok(())
}
fn draw_empty_board(&self, ctx: &mut Context) -> GameResult {
for y in 0..BOARD_SIZE.1 {
for x in 0..BOARD_SIZE.0 {
let color_index = if (x % 2 == 1 && y % 2 == 1) || (x % 2 == 0 && y % 2 == 0) {
0
} else {
1
};
let mesh = graphics::MeshBuilder::new()
.rectangle(
graphics::DrawMode::fill(),
graphics::Rect::new(
x as f32 * BOARD_CELL_PX_SIZE.0,
y as f32 * BOARD_CELL_PX_SIZE.1,
BOARD_CELL_PX_SIZE.0,
BOARD_CELL_PX_SIZE.1,
),
self.theme.board_color[color_index],
)?
.build(ctx)?;
graphics::draw(ctx, &mesh, graphics::DrawParam::default())?;
}
}
Ok(())
}
fn draw_content_board(&self, ctx: &mut Context) -> GameResult {
let mut path;
let mut image;
for square in ALL_SQUARES {
if let Some((piece, color)) = self.chess.board.on(square) {
path = self.theme.piece_path[color.to_index()][piece.to_index()];
image = graphics::Image::new(ctx, path).expect("Image load error");
let (x, y) = square.to_screen();
let dest_point = [x, y];
let image_scale = [0.5, 0.5];
let dp = graphics::DrawParam::new()
.dest(dest_point)
.scale(image_scale);
graphics::draw(ctx, &image, dp)?;
}
}
Ok(())
}
fn draw_legal_moves(&self, ctx: &mut Context) -> GameResult {
if self.theme.valid_moves_color.is_some() {
if let Some(square) = self.chess.square_focused {
for dest in self.chess.board.get_legal_moves(square) {
let (x, y) = dest.to_screen();
let mesh = graphics::MeshBuilder::new()
.rectangle(
graphics::DrawMode::fill(),
graphics::Rect::new(x, y, BOARD_CELL_PX_SIZE.0, BOARD_CELL_PX_SIZE.1),
self.theme.valid_moves_color.unwrap(),
)?
.build(ctx)?;
graphics::draw(ctx, &mesh, graphics::DrawParam::default())?;
}
}
}
Ok(())
}
fn draw_pinned_piece(&self, ctx: &mut Context) -> GameResult {
if self.theme.piece_pinned_path.is_some() {
let mut path;
let mut image;
for square in self.chess.board.pinned() {
path = self.theme.piece_pinned_path.unwrap();
image = graphics::Image::new(ctx, path).expect("Image load error");
let (x, y) = square.to_screen();
let dest_point = [x, y];
const SCALE: f32 = 1.0;
let image_scale = [
SCALE * (BOARD_CELL_PX_SIZE.0 / image.width() as f32),
SCALE * (BOARD_CELL_PX_SIZE.1 / image.height() as f32),
];
let dp = graphics::DrawParam::new()
.dest(dest_point)
.scale(image_scale);
graphics::draw(ctx, &image, dp)?;
}
} else if self.theme.piece_pinned_color.is_some() {
for piece in self.chess.board.pinned() {
let (x, y) = piece.to_screen();
let mesh = graphics::MeshBuilder::new()
.rectangle(
graphics::DrawMode::fill(),
graphics::Rect::new(x, y, BOARD_CELL_PX_SIZE.0, BOARD_CELL_PX_SIZE.1),
self.theme.piece_pinned_color.unwrap(),
)?
.build(ctx)?;
graphics::draw(ctx, &mesh, graphics::DrawParam::default())?;
}
}
Ok(())
}
fn draw_side(&self, ctx: &mut Context) -> GameResult {
for button in self.buttons.iter() {
button.draw(ctx, self.theme.font_path, self.theme.font_scale)?;
}
self.draw_timers(ctx)?;
self.draw_winner(ctx)?;
Ok(())
}
fn draw_timers(&self, ctx: &mut Context) -> GameResult {
let bounds_white = graphics::Rect::new(BOARD_PX_SIZE.0 + 20.0, 20.0, 115.0, 50.0);
let bounds_black = graphics::Rect::new(BOARD_PX_SIZE.0 + 155.0, 20.0, 115.0, 50.0);
let background_mesh_white = graphics::MeshBuilder::new()
.rectangle(
graphics::DrawMode::fill(),
bounds_white,
graphics::Color::new(0.5, 0.5, 0.5, 1.0),
)?
.build(ctx)?;
graphics::draw(ctx, &background_mesh_white, graphics::DrawParam::default())?;
let background_mesh_black = graphics::MeshBuilder::new()
.rectangle(
graphics::DrawMode::fill(),
bounds_black,
graphics::Color::new(0.5, 0.5, 0.5, 1.0),
)?
.build(ctx)?;
graphics::draw(ctx, &background_mesh_black, graphics::DrawParam::default())?;
let text_white = format!("{}:{}", "--", "--");
let font = graphics::Font::new(ctx, self.theme.font_path)?;
let text_white = graphics::Text::new((text_white, font, self.theme.font_scale * 2.0));
let dest_point = [
bounds_white.x + (bounds_white.w - text_white.width(ctx)) / 2.0,
bounds_white.y + (bounds_white.h - text_white.height(ctx)) / 2.0,
];
graphics::draw(ctx, &text_white, (dest_point,))?;
let text_black = format!("{}:{}", "--", "--");
let font = graphics::Font::new(ctx, self.theme.font_path)?;
let text_black = graphics::Text::new((text_black, font, self.theme.font_scale * 2.0));
let dest_point = [
bounds_black.x + (bounds_black.w - text_black.width(ctx)) / 2.0,
bounds_black.y + (bounds_black.h - text_black.height(ctx)) / 2.0,
];
graphics::draw(ctx, &text_black, (dest_point, graphics::Color::BLACK))?;
Ok(())
}
fn draw_winner(&self, ctx: &mut Context) -> GameResult {
let bounds = graphics::Rect::new(
BOARD_PX_SIZE.0 + 20.0,
90.0,
320.0,
SIDE_SCREEN_PX_SIZE.1 - 250.0 - 70.0,
);
let background_mesh = graphics::MeshBuilder::new()
.rectangle(
graphics::DrawMode::stroke(3.0),
bounds,
graphics::Color::new(0.7, 0.7, 0.7, 1.0),
)?
.build(ctx)?;
graphics::draw(ctx, &background_mesh, graphics::DrawParam::default())?;
let text = match self.chess.state {
GameState::Ongoing => "Ongoing".to_string(),
GameState::Checkmates(color) => {
format!("{:?} is checkmate\n\n {:?} win !", color, !color)
}
GameState::Stalemate => "Draw: Stalemate".to_string(),
GameState::DrawAccepted => "Draw: Accepted".to_string(),
GameState::DrawDeclared => "Draw: Declared".to_string(),
GameState::Resigns(color) => format!("{:?} resigns\n\n {:?} win !", color, !color),
};
let font = graphics::Font::new(ctx, self.theme.font_path)?;
let text = graphics::Text::new((text, font, self.theme.font_scale * 2.0));
let dest_point = [
bounds.x + (bounds.w - text.width(ctx)) / 2.0,
bounds.y + (bounds.h - text.height(ctx)) / 2.0,
];
graphics::draw(ctx, &text, (dest_point,))?;
Ok(())
}
}
impl event::EventHandler<GameError> for ChessGui {
fn update(&mut self, _ctx: &mut Context) -> GameResult {
for button in self.buttons.iter_mut() {
match button.id {
"declare-draw" => {
if self.chess.can_declare_draw() {
button.enable();
} else {
button.disable();
}
}
"accept-draw" => {
if self.chess.offer_draw {
button.enable();
} else {
button.disable();
}
}
_ => {}
}
}
if self.chess.state.is_finish() {
for button in self.buttons.iter_mut() {
match button.id {
"reset" | "theme" => {}
_ => button.disable(),
}
}
}
Ok(())
}
fn draw(&mut self, ctx: &mut Context) -> GameResult {
graphics::clear(ctx, self.theme.background_color);
self.draw_board(ctx)?;
self.draw_side(ctx)?;
graphics::present(ctx)?;
Ok(())
}
fn mouse_button_down_event(&mut self, _ctx: &mut Context, button: MouseButton, x: f32, y: f32) {
if button == MouseButton::Left {
self.click(x, y);
}
}
fn mouse_motion_event(&mut self, ctx: &mut Context, x: f32, y: f32, _dx: f32, _dy: f32) {
if x > BOARD_PX_SIZE.0 {
let mut on_button = false;
for button in self.buttons.iter() {
if button.contains(x, y) {
on_button = true;
break;
}
}
if on_button {
ggez::input::mouse::set_cursor_type(ctx, ggez::input::mouse::CursorIcon::Hand);
} else {
ggez::input::mouse::set_cursor_type(ctx, ggez::input::mouse::CursorIcon::Default);
}
}
}
fn key_down_event(
&mut self,
ctx: &mut Context,
keycode: KeyCode,
keymod: KeyMods,
_repeat: bool,
) {
match keycode {
KeyCode::Escape => event::quit(ctx),
KeyCode::R => self.reset(),
KeyCode::Z if keymod == KeyMods::CTRL => self.chess.undo(),
_ => {}
};
}
}
impl Default for ChessGui {
fn default() -> Self {
let mut chess_gui = ChessGui::new(
Default::default(),
Default::default(),
Vec::with_capacity(7),
);
chess_gui.init_buttons();
chess_gui
}
}