From 1f066de806dbb044724cfe7c04eae4d42e649dd6 Mon Sep 17 00:00:00 2001 From: Baptiste Fouques Date: Thu, 23 Feb 2023 22:35:33 +0100 Subject: [PATCH 1/2] Add chess feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Provide fully playable chess environment linked with uci compatible chess engine (stockfish…) --- COPYING.md | 27 + crates/core/Cargo.toml | 10 + crates/core/build.rs | 8 + crates/core/rustfmt.toml | 3 + crates/core/src/color.rs | 4 + crates/core/src/document/mupdf_sys.rs | 3 + crates/core/src/emulator.rs | 7 + crates/core/src/font/mod.rs | 1 - crates/core/src/settings/mod.rs | 23 + crates/core/src/view/clock.rs | 20 +- crates/core/src/view/common.rs | 3 + crates/core/src/view/icon.rs | 5 +- crates/core/src/view/label.rs | 9 +- crates/core/src/view/mod.rs | 58 ++ .../core/src/view/plato_chess/bottom_bar.rs | 231 ++++++ .../core/src/view/plato_chess/chess_board.rs | 157 ++++ .../core/src/view/plato_chess/chess_cell.rs | 160 ++++ .../core/src/view/plato_chess/chess_moves.rs | 323 ++++++++ crates/core/src/view/plato_chess/mod.rs | 765 ++++++++++++++++++ .../src/view/plato_chess/sliders_dialog.rs | 274 +++++++ crates/core/src/view/slider.rs | 4 + crates/core/src/view/top_bar.rs | 84 +- crates/emulator/Cargo.toml | 7 + crates/emulator/src/main.rs | 11 + crates/plato/Cargo.toml | 5 + crates/plato/src/app.rs | 5 + crates/plato/src/main.rs | 7 + icons/bchess.svg | 70 ++ icons/chess/bB.svg | 52 ++ icons/chess/bK.svg | 66 ++ icons/chess/bN.svg | 63 ++ icons/chess/bP.svg | 1 + icons/chess/bQ.svg | 86 ++ icons/chess/bR.svg | 58 ++ icons/chess/wK.svg | 66 ++ icons/engine-chess.svg | 760 +++++++++++++++++ icons/human-chess.svg | 761 +++++++++++++++++ icons/wchess.svg | 71 ++ 38 files changed, 4213 insertions(+), 55 deletions(-) create mode 100644 COPYING.md create mode 100644 crates/core/rustfmt.toml create mode 100644 crates/core/src/view/plato_chess/bottom_bar.rs create mode 100644 crates/core/src/view/plato_chess/chess_board.rs create mode 100644 crates/core/src/view/plato_chess/chess_cell.rs create mode 100644 crates/core/src/view/plato_chess/chess_moves.rs create mode 100644 crates/core/src/view/plato_chess/mod.rs create mode 100644 crates/core/src/view/plato_chess/sliders_dialog.rs create mode 100644 icons/bchess.svg create mode 100644 icons/chess/bB.svg create mode 100644 icons/chess/bK.svg create mode 100644 icons/chess/bN.svg create mode 100644 icons/chess/bP.svg create mode 100644 icons/chess/bQ.svg create mode 100644 icons/chess/bR.svg create mode 100644 icons/chess/wK.svg create mode 100644 icons/engine-chess.svg create mode 100644 icons/human-chess.svg create mode 100644 icons/wchess.svg diff --git a/COPYING.md b/COPYING.md new file mode 100644 index 00000000..9f4438db --- /dev/null +++ b/COPYING.md @@ -0,0 +1,27 @@ + +Any file in this project that does not state otherwise and is not listed as an +exception below is part of *plato* and copyright (c) 2017 Bastien Dejean +and plato contributors. + +For a list of the authors see the commit log or +https://github.com/baskerville/plato/graphs/contributors/ + +Plato is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +See the LICENSE file for a copy of the *GNU General Public License*. + + +Exceptions (free) +----------------- + +Files | Author(s) | License +--- | --- | --- +icons/chess|[Colin M.L. Burnett](https://en.wikipedia.org/wiki/User:Cburnett) | [GPLv2+](https://www.gnu.org/licenses/gpl-2.0.txt) diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 1ed74f81..dd0054d5 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -40,3 +40,13 @@ rand_core = "0.6.4" rand_xoshiro = "0.6.0" percent-encoding = "2.2.0" chrono = { version = "0.4.23", features = ["serde"] } +log = "0.4.17" + +chess_uci = { git = "https://git.hadoly.fr/bateast/chess_uci.git", optional = true } +chess = { version = "3.2.0", optional = true } +env_logger = { version = "0.10.0", optional = true } + + +[features] +chess = ["dep:chess_uci", "dep:chess"] +devel = ["dep:env_logger"] diff --git a/crates/core/build.rs b/crates/core/build.rs index e7729546..13351d8b 100644 --- a/crates/core/build.rs +++ b/crates/core/build.rs @@ -23,6 +23,14 @@ fn main() { "linux" => { println!("cargo:rustc-link-search=target/mupdf_wrapper/Linux"); println!("cargo:rustc-link-lib=dylib=stdc++"); + println!("cargo:rustc-link-lib=dylib=stdc++"); + println!("cargo:rustc-link-lib=z"); + println!("cargo:rustc-link-lib=bz2"); + println!("cargo:rustc-link-lib=jpeg"); + println!("cargo:rustc-link-lib=png16"); + println!("cargo:rustc-link-lib=gumbo"); + println!("cargo:rustc-link-lib=openjp2"); + println!("cargo:rustc-link-lib=jbig2dec"); }, "macos" => { println!("cargo:rustc-link-search=target/mupdf_wrapper/Darwin"); diff --git a/crates/core/rustfmt.toml b/crates/core/rustfmt.toml new file mode 100644 index 00000000..22026e83 --- /dev/null +++ b/crates/core/rustfmt.toml @@ -0,0 +1,3 @@ +max_width=120 +fn_args_layout="Compressed" +short_array_element_width_threshold=25 diff --git a/crates/core/src/color.rs b/crates/core/src/color.rs index 09bf244f..865147a1 100644 --- a/crates/core/src/color.rs +++ b/crates/core/src/color.rs @@ -27,6 +27,10 @@ pub const TEXT_BUMP_LARGE: [u8; 3] = [GRAY11, BLACK, BLACK]; pub const TEXT_INVERTED_SOFT: [u8; 3] = [GRAY05, WHITE, WHITE]; pub const TEXT_INVERTED_HARD: [u8; 3] = [BLACK, WHITE, GRAY06]; +pub const CHESSBOARD_BG: u8 = KEYBOARD_BG; +pub const CELL_BLACK: [u8; 2] = [GRAY05, GRAY07]; +pub const CELL_WHITE: [u8; 2] = [GRAY10, GRAY07]; + pub const SEPARATOR_NORMAL: u8 = GRAY10; pub const SEPARATOR_STRONG: u8 = GRAY07; diff --git a/crates/core/src/document/mupdf_sys.rs b/crates/core/src/document/mupdf_sys.rs index 4cdd5fd2..173795f6 100644 --- a/crates/core/src/document/mupdf_sys.rs +++ b/crates/core/src/document/mupdf_sys.rs @@ -3,6 +3,9 @@ use std::mem; pub const FZ_MAX_COLORS: usize = 32; +#[cfg(target_arch = "x86_64")] +pub const FZ_VERSION: &str = "1.21.1"; +#[cfg(target_arch = "arm")] pub const FZ_VERSION: &str = "1.20.0"; pub const FZ_META_INFO_AUTHOR: &str = "info:Author"; diff --git a/crates/core/src/emulator.rs b/crates/core/src/emulator.rs index d5ac330a..88b21f50 100644 --- a/crates/core/src/emulator.rs +++ b/crates/core/src/emulator.rs @@ -67,6 +67,9 @@ use crate::library::Library; use crate::font::Fonts; use crate::app::Context; +#[cfg(feature = "chess")] +use crate::view::plato_chess::PlatoChess; + pub const APP_NAME: &str = "Plato"; const DEFAULT_ROTATION: i8 = 1; @@ -413,6 +416,10 @@ fn main() -> Result<(), Error> { AppCmd::Sketch => { Box::new(Sketch::new(context.fb.rect(), &mut rq, &mut context)) }, + #[cfg(feature = "chess")] + AppCmd::Chess => { + Box::new(PlatoChess::new(context.fb.rect(), &mut rq, &mut context)) + }, AppCmd::Calculator => { Box::new(Calculator::new(context.fb.rect(), &tx, &mut rq, &mut context)?) }, diff --git a/crates/core/src/font/mod.rs b/crates/core/src/font/mod.rs index afefcd93..f4b42789 100644 --- a/crates/core/src/font/mod.rs +++ b/crates/core/src/font/mod.rs @@ -763,7 +763,6 @@ unsafe fn font_data_from_script(script: HbScript) -> &'static [libc::c_uchar] { HB_SCRIPT_BOPOMOFO | HB_SCRIPT_HAN => &_binary_resources_fonts_droid_DroidSansFallback_ttf_start, - HB_SCRIPT_ARABIC => &_binary_resources_fonts_noto_NotoNaskhArabic_Regular_ttf_start, HB_SCRIPT_SYRIAC => &_binary_resources_fonts_noto_NotoSansSyriac_Regular_otf_start, HB_SCRIPT_MEROITIC_CURSIVE | HB_SCRIPT_MEROITIC_HIEROGLYPHS => &_binary_resources_fonts_noto_NotoSansMeroitic_Regular_otf_start, diff --git a/crates/core/src/settings/mod.rs b/crates/core/src/settings/mod.rs index 0bd1819f..f7c9bf33 100644 --- a/crates/core/src/settings/mod.rs +++ b/crates/core/src/settings/mod.rs @@ -121,6 +121,8 @@ pub struct Settings { pub reader: ReaderSettings, pub import: ImportSettings, pub dictionary: DictionarySettings, + #[cfg(feature = "chess")] + pub chess_engine_settings: ChessEngineSettings, pub sketch: SketchSettings, pub calculator: CalculatorSettings, pub battery: BatterySettings, @@ -173,6 +175,25 @@ pub struct ImportSettings { pub allowed_kinds: FxHashSet, } +#[cfg(feature = "chess")] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default, rename_all = "kebab-case")] +pub struct ChessEngineSettings { + pub elo: u32, + pub slow_motion: u32, + pub path: PathBuf, + pub font_size: f32, + pub margin_width: i32, +} + +#[cfg(feature = "chess")] +impl Default for ChessEngineSettings { + fn default() -> Self { + ChessEngineSettings { elo: 1500, slow_motion: 100, path: PathBuf::from("bin/stockfish"), + font_size: 8.0, margin_width: 2} + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default, rename_all = "kebab-case")] pub struct DictionarySettings { @@ -535,6 +556,8 @@ impl Default for Settings { reader: ReaderSettings::default(), import: ImportSettings::default(), dictionary: DictionarySettings::default(), + #[cfg(feature = "chess")] + chess_engine_settings: ChessEngineSettings::default(), sketch: SketchSettings::default(), calculator: CalculatorSettings::default(), battery: BatterySettings::default(), diff --git a/crates/core/src/view/clock.rs b/crates/core/src/view/clock.rs index 55eefb88..33614845 100644 --- a/crates/core/src/view/clock.rs +++ b/crates/core/src/view/clock.rs @@ -17,21 +17,23 @@ pub struct Clock { } impl Clock { - pub fn new(rect: &mut Rectangle, context: &mut Context) -> Clock { - let time = Local::now(); - let format = context.settings.time_format.clone(); - let font = font_from_style(&mut context.fonts, &NORMAL_STYLE, CURRENT_DEVICE.dpi); - let width = font.plan(&time.format(&format).to_string(), None, None).width + font.em() as i32; - rect.min.x = rect.max.x - width; + pub fn new(rect: Rectangle, context: &mut Context) -> Clock { Clock { id: ID_FEEDER.next(), - rect: *rect, + rect, children: Vec::new(), - format, - time, + format: context.settings.time_format.clone(), + time: Local::now(), } } + pub fn compute_width(context: &mut Context) -> i32 { + let time = Local::now(); + let format = context.settings.time_format.clone(); + let font = font_from_style(&mut context.fonts, &NORMAL_STYLE, CURRENT_DEVICE.dpi); + font.plan(&time.format(&format).to_string(), None, None).width + font.em() as i32 + } + pub fn update(&mut self, rq: &mut RenderQueue) { self.time = Local::now(); rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui)); diff --git a/crates/core/src/view/common.rs b/crates/core/src/view/common.rs index e4a5a696..0417c7b5 100644 --- a/crates/core/src/view/common.rs +++ b/crates/core/src/view/common.rs @@ -85,6 +85,9 @@ pub fn toggle_main_menu(view: &mut dyn View, rect: Rectangle, enable: Option, hold_event: Option, + font_size: u32, } impl Label { @@ -27,6 +28,7 @@ impl Label { align, event: None, hold_event: None, + font_size: NORMAL_STYLE.size, } } @@ -46,6 +48,10 @@ impl Label { rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui)); } } + + pub fn set_font_size(&mut self, font_size: f32) { + self.font_size = (64.0 * font_size) as u32; + } } impl View for Label { @@ -72,7 +78,8 @@ impl View for Label { fb.draw_rectangle(&self.rect, TEXT_NORMAL[0]); - let font = font_from_style(fonts, &NORMAL_STYLE, dpi); + let mut style = NORMAL_STYLE; style.size = self.font_size; + let font = font_from_style(fonts, &style, dpi); let x_height = font.x_heights.0 as i32; let padding = font.em() as i32; let max_width = self.rect.width() as i32 - padding; diff --git a/crates/core/src/view/mod.rs b/crates/core/src/view/mod.rs index 4efb369f..570aa369 100644 --- a/crates/core/src/view/mod.rs +++ b/crates/core/src/view/mod.rs @@ -42,6 +42,13 @@ pub mod calculator; pub mod sketch; pub mod touch_events; pub mod rotation_values; +#[cfg(feature = "chess")] +pub mod plato_chess; + +#[cfg(feature = "devel")] +use { + crate::device::CURRENT_DEVICE, +}; use std::ops::{Deref, DerefMut}; use std::time::{Instant, Duration}; @@ -63,6 +70,12 @@ use crate::gesture::GestureEvent; use self::calculator::LineOrigin; use self::key::KeyKind; use crate::context::Context; +#[cfg(feature = "chess")] +use { + chess::Square, + chess_uci::uci_command::EngineCommand, +}; +use log::{debug, warn}; // Border thicknesses in pixels, at 300 DPI. pub const THICKNESS_SMALL: f32 = 1.0; @@ -294,6 +307,12 @@ pub enum Event { Gesture(GestureEvent), Keyboard(KeyboardEvent), Key(KeyKind), + #[cfg(feature = "chess")] + ChessCommand(EngineCommand), + #[cfg(feature = "chess")] + ChessGo(bool), + #[cfg(feature = "chess")] + ChessCell(Square, bool), Open(Box), OpenHtml(String, Option), LoadPixmap(usize), @@ -350,6 +369,12 @@ pub enum Event { Scroll(i32), Save, Guess, + #[cfg(feature = "chess")] + Reset, + #[cfg(feature = "chess")] + UpdateValue(f32), + #[cfg(feature = "chess")] + SaveAll(ViewId, Vec), CheckBattery, SetWifi(bool), MightSuspend, @@ -368,6 +393,8 @@ pub enum Event { #[derive(Debug, Clone, Eq, PartialEq)] pub enum AppCmd { Sketch, + #[cfg(feature = "chess")] + Chess, Calculator, Dictionary { query: String, @@ -408,6 +435,14 @@ pub enum ViewId { MarginCropperMenu, SearchMenu, SketchMenu, + #[cfg(feature = "chess")] + ChessMenu, + #[cfg(feature = "chess")] + ChessSettings, + #[cfg(feature = "chess")] + ChessPromotion, + #[cfg(feature = "chess")] + ChessMoves, RenameDocument, RenameDocumentInput, GoToPage, @@ -444,6 +479,10 @@ pub enum SliderId { LightWarmth, ContrastExponent, ContrastGray, + #[cfg(feature = "chess")] + ChessElo, + #[cfg(feature = "chess")] + ChessSlow, } impl SliderId { @@ -454,6 +493,10 @@ impl SliderId { SliderId::FontSize => "Font Size".to_string(), SliderId::ContrastExponent => "Contrast Exponent".to_string(), SliderId::ContrastGray => "Contrast Gray".to_string(), + #[cfg(feature = "chess")] + SliderId::ChessElo => "Elo level calibration".to_string(), + #[cfg(feature = "chess")] + SliderId::ChessSlow => "Slow motion".to_string(), } } } @@ -521,6 +564,14 @@ pub enum EntryId { CopyTo(PathBuf, usize), MoveTo(PathBuf, usize), AddDirectory(PathBuf), + #[cfg(feature = "chess")] + ChessPlayer(chess::Color, chess_uci::Player), + #[cfg(feature = "chess")] + ChessPiece(chess::ChessMove), + #[cfg(feature = "chess")] + ChessEngineSettings, + #[cfg(feature = "chess")] + ChessTimeControl(u64, u64), SelectDirectory(PathBuf), ToggleSelectDirectory(PathBuf), SetStatus(PathBuf, SimpleStatus), @@ -612,6 +663,7 @@ impl EntryKind { } } +#[derive(Debug)] pub struct RenderData { pub id: Option, pub rect: Rectangle, @@ -671,6 +723,12 @@ impl RenderQueue { } pub fn add(&mut self, data: RenderData) { + debug!("adding to render queue {:?}", data); + #[cfg(feature = "devel")] + if (data.rect.max.x as u32, data.rect.max.y as u32) > CURRENT_DEVICE.dims { + warn!("rect ({:?})out of screen limits {:?}", data.rect, CURRENT_DEVICE.dims); + } + self.entry((data.mode, data.wait)).or_insert_with(|| { Vec::new() }).push((data.id, data.rect)); diff --git a/crates/core/src/view/plato_chess/bottom_bar.rs b/crates/core/src/view/plato_chess/bottom_bar.rs new file mode 100644 index 00000000..a920031c --- /dev/null +++ b/crates/core/src/view/plato_chess/bottom_bar.rs @@ -0,0 +1,231 @@ +use crate::view::icon::Icon; +use crate::view::key::KeyKind; +use crate::view::label::Label; +use crate::view::{Align, Bus, Event, Hub, Id, RenderData, RenderQueue, View, ID_FEEDER}; + +use crate::context::Context; +use crate::device::CURRENT_DEVICE; +use crate::font::{font_from_style, Fonts, NORMAL_STYLE}; +use crate::framebuffer::{Framebuffer, UpdateMode}; +use crate::geom::Rectangle; +use crate::gesture::GestureEvent; +use crate::input::DeviceEvent; + +use log::debug; + +use chess::Color; + +use std::time::Duration; + +#[derive(Debug)] +pub struct BottomBar { + id: Id, + rect: Rectangle, + children: Vec>, + color: Color, + human: bool, + clocks: [Duration; 2], +} + +impl BottomBar { + pub fn new( + rect: Rectangle, name: &str, color: Color, human: bool, context: &mut Context, + ) -> BottomBar { + let id = ID_FEEDER.next(); + let mut children = Vec::new(); + + let sizes = Self::compute_sizes(rect, context); + + let icon = Icon::new( + match color { + Color::White => "wchess", + Color::Black => "bchess", + }, + sizes[0], + Event::Key(KeyKind::Alternate), + ); + children.push(Box::new(icon) as Box); + + let icon = Icon::new("arrow-left", sizes[1], Event::Cancel); + children.push(Box::new(icon) as Box); + + let name_label = Label::new(sizes[2], name.to_string(), Align::Center); + children.push(Box::new(name_label) as Box); + + let mut white_time_label = Label::new(sizes[3], "––:––:––".to_string(), Align::Left(0)); + white_time_label.set_font_size(context.settings.chess_engine_settings.font_size); + children.push(Box::new(white_time_label) as Box); + let mut black_time_label = Label::new(sizes[4], "––:––:––".to_string(), Align::Right(0)); + black_time_label.set_font_size(context.settings.chess_engine_settings.font_size); + children.push(Box::new(black_time_label) as Box); + + let icon = Icon::new( + if human { "human-chess" } else { "engine-chess" }, + sizes[5], + Event::ChessGo(human), + ); + children.push(Box::new(icon) as Box); + + BottomBar { + id, + rect, + children, + color, + human, + clocks: [Duration::new(0, 0), Duration::new(0, 0)], + } + } + + fn compute_sizes(rect: Rectangle, context: &mut Context) -> [Rectangle; 6] { + let side = rect.height() as i32; + + let font = font_from_style(&mut context.fonts, &NORMAL_STYLE, CURRENT_DEVICE.dpi); + let plan = font.plan("––:––:–– ←", None, None); + let time_width = 15 * plan.width / 10; + + [ + rect![rect.min, rect.min + side], + // undo_icon + rect![ + pt!(rect.min.x + side, rect.min.y), + pt!(rect.min.x + 2 * side, rect.min.y + side) + ], + // labels + rect![ + pt!(rect.min.x + 2 * side, rect.min.y), + pt!(rect.max.x - side - time_width, rect.max.y) + ], + rect![ + pt!(rect.max.x - side - time_width, rect.min.y), + pt!(rect.max.x - side, rect.max.y - rect.height() as i32 / 2) + ], + rect![ + pt!(rect.max.x - side - time_width, rect.min.y + rect.height() as i32 / 2), + pt!(rect.max.x - side, rect.max.y) + ], + rect![rect.max - side, rect.max], + ] + } + + pub fn update_color(&mut self, color: Color, rq: &mut RenderQueue) { + if self.color != color { + let index = 0; + let color_rect = *self.child(index).rect(); + + let icon = Icon::new( + match color { + Color::White => "wchess", + Color::Black => "bchess", + }, + color_rect, + Event::Key(KeyKind::Alternate), + ); + rq.add(RenderData::new(icon.id(), color_rect, UpdateMode::Gui)); + self.children[index] = Box::new(icon) as Box; + } + self.color = color; + + self.update_clocks(self.clocks[0], self.clocks[1], rq) + } + + pub fn update_player(&mut self, human: bool, rq: &mut RenderQueue) { + if human != self.human { + let index = self.len() - 1; + let rect = *self.child(index).rect(); + + let icon = Icon::new( + if human { "human-chess" } else { "engine-chess" }, + rect, + Event::ChessGo(human), + ); + rq.add(RenderData::new(icon.id(), rect, UpdateMode::Gui)); + self.children[index] = Box::new(icon) as Box; + } + self.human = human; + } + + pub fn update_name(&mut self, text: &str, rq: &mut RenderQueue) { + let name_label = self.child_mut(2).downcast_mut::