├── website ├── static │ ├── .nojekyll │ ├── img │ │ ├── logo.png │ │ ├── favicon.ico │ │ ├── social-card.png │ │ ├── ascii-display-mode-board.png │ │ ├── default-display-mode-board.png │ │ └── lichess-api-setup │ │ │ ├── lichess-token-page.png │ │ │ ├── chess-tui-main-menu.png │ │ │ ├── lichess-token-config.png │ │ │ ├── lichess-token-generated.png │ │ │ └── lichess-generate-token-button.png │ └── gif │ │ ├── helper.gif │ │ ├── ratatui.gif │ │ ├── multiplayer.gif │ │ ├── lichess-menu.gif │ │ ├── demo-two-player.gif │ │ ├── play_against_black_bot.gif │ │ └── play_against_white_bot.gif ├── docs │ ├── Multiplayer │ │ ├── bore-port.png │ │ ├── Local multiplayer.md │ │ └── Online multiplayer.md │ ├── Installation │ │ ├── Cargo.md │ │ ├── Cargod.md │ │ ├── Docker.mdx │ │ ├── Build from source.md │ │ ├── NetBSD.md │ │ ├── Arch Linux.md │ │ ├── NixOS.md │ │ ├── Binary.md │ │ ├── Packaging status.md │ │ ├── Debian Ubuntu.md │ │ └── Logging.md │ ├── Code Architecture │ │ ├── intro.md │ │ ├── Bot.md │ │ ├── App.md │ │ ├── GameLogic.md │ │ ├── pieces.md │ │ ├── Opponent.md │ │ ├── GameBoard.md │ │ └── UI.md │ ├── Configuration │ │ ├── display.md │ │ ├── logging.md │ │ ├── intro.md │ │ └── skins.md │ ├── intro.mdx │ └── Lichess │ │ └── features.md ├── blog │ ├── authors.yml │ ├── 2023-12-03-release-0.1.3.md │ ├── 2025-12-15-release-2.1.2.md │ ├── 2025-12-14-release-2.1.1.md │ ├── 2024-03-26-release-1.2.1.md │ ├── 2024-11-17-release-1.4.0.md │ ├── 2024-12-15-release-1.6.0.md │ ├── 2023-12-04-release-1.0.0.md │ ├── 2024-02-28-release-1.2.0.md │ ├── 2025-02-03-release-1.6.1.md │ ├── 2025-12-16-release-2.2.0.md │ ├── 2023-12-02-release-0.1.2.md │ ├── 2025-12-13-release-2.1.0.md │ ├── 2024-11-26-release-1.5.0.md │ ├── 2023-12-09-release-1.1.0.md │ ├── 2025-09-03-release-1.6.2.md │ ├── 2025-12-11-release-2.0.0.md │ └── 2024-11-15-release-1.3.0.md ├── tsconfig.json ├── .gitignore ├── README.md ├── src │ ├── components │ │ ├── HomepageFeatures │ │ │ ├── styles.module.css │ │ │ └── index.tsx │ │ └── LatestVersion │ │ │ └── index.tsx │ └── pages │ │ ├── index.tsx │ │ └── index.module.css ├── package.json ├── sidebars.ts └── docusaurus.config.ts ├── src ├── server │ └── mod.rs ├── ui │ ├── mod.rs │ ├── tui.rs │ ├── prompt.rs │ └── ongoing_games.rs ├── game_logic │ ├── mod.rs │ ├── bot.rs │ └── coord.rs ├── lib.rs ├── config.rs ├── logging.rs ├── pieces │ ├── queen.rs │ ├── knight.rs │ ├── pawn.rs │ ├── rook.rs │ ├── bishop.rs │ ├── king.rs │ └── mod.rs ├── constants.rs ├── event.rs ├── utils.rs ├── skin.rs ├── default_skins.json └── sound.rs ├── examples ├── helper.gif ├── ratatui.gif ├── lichess-menu.gif ├── demo-two-player.gif ├── play_against_black_bot.gif ├── play_against_white_bot.gif ├── helper.tape ├── play_against_black_bot.tape ├── lichess-menu.tape ├── demo-two-player.tape └── play_against_white_bot.tape ├── cargo-generate.toml ├── .github ├── workflows │ ├── release_docker.yml │ ├── flow_test_build_push.yml │ ├── release_cargo.yml │ ├── lint.yml │ ├── test-deploy-website.yml │ ├── docker_push.yml │ ├── deploy-website.yml │ ├── build_and_test.yml │ ├── release_binary.yml │ └── generate-blog-post.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── documentation_request.md │ ├── refactor_request.md │ └── bug_report.md └── pull_request_template.md ├── scripts ├── render_demos.sh ├── blog-scripts │ ├── fetch-and-generate-blog.js │ └── generate-blog-post.js └── install-stockfish.sh ├── .dockerignore ├── .gitignore ├── LICENSE ├── tests └── skin_tests.rs ├── Dockerfile ├── Cargo.toml ├── CONTRIBUTING.md └── README.md /website/static/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/server/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod game_server; 2 | -------------------------------------------------------------------------------- /examples/helper.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/HEAD/examples/helper.gif -------------------------------------------------------------------------------- /examples/ratatui.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/HEAD/examples/ratatui.gif -------------------------------------------------------------------------------- /examples/lichess-menu.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/HEAD/examples/lichess-menu.gif -------------------------------------------------------------------------------- /website/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/HEAD/website/static/img/logo.png -------------------------------------------------------------------------------- /examples/demo-two-player.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/HEAD/examples/demo-two-player.gif -------------------------------------------------------------------------------- /website/static/gif/helper.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/HEAD/website/static/gif/helper.gif -------------------------------------------------------------------------------- /website/static/gif/ratatui.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/HEAD/website/static/gif/ratatui.gif -------------------------------------------------------------------------------- /website/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/HEAD/website/static/img/favicon.ico -------------------------------------------------------------------------------- /website/static/gif/multiplayer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/HEAD/website/static/gif/multiplayer.gif -------------------------------------------------------------------------------- /website/static/img/social-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/HEAD/website/static/img/social-card.png -------------------------------------------------------------------------------- /examples/play_against_black_bot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/HEAD/examples/play_against_black_bot.gif -------------------------------------------------------------------------------- /examples/play_against_white_bot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/HEAD/examples/play_against_white_bot.gif -------------------------------------------------------------------------------- /website/static/gif/lichess-menu.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/HEAD/website/static/gif/lichess-menu.gif -------------------------------------------------------------------------------- /website/docs/Multiplayer/bore-port.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/HEAD/website/docs/Multiplayer/bore-port.png -------------------------------------------------------------------------------- /website/static/gif/demo-two-player.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/HEAD/website/static/gif/demo-two-player.gif -------------------------------------------------------------------------------- /src/ui/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod lichess_menu; 2 | pub mod main_ui; 3 | pub mod ongoing_games; 4 | pub mod popups; 5 | pub mod prompt; 6 | pub mod tui; 7 | -------------------------------------------------------------------------------- /cargo-generate.toml: -------------------------------------------------------------------------------- 1 | # configuration for https://cargo-generate.github.io/cargo-generate/ 2 | 3 | [template] 4 | ignore = ["README.md", ".github/"] 5 | -------------------------------------------------------------------------------- /website/static/gif/play_against_black_bot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/HEAD/website/static/gif/play_against_black_bot.gif -------------------------------------------------------------------------------- /website/static/gif/play_against_white_bot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/HEAD/website/static/gif/play_against_white_bot.gif -------------------------------------------------------------------------------- /website/static/img/ascii-display-mode-board.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/HEAD/website/static/img/ascii-display-mode-board.png -------------------------------------------------------------------------------- /src/game_logic/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bot; 2 | pub mod coord; 3 | pub mod game; 4 | pub mod game_board; 5 | pub mod opponent; 6 | pub mod puzzle; 7 | pub mod ui; 8 | -------------------------------------------------------------------------------- /website/static/img/default-display-mode-board.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/HEAD/website/static/img/default-display-mode-board.png -------------------------------------------------------------------------------- /website/static/img/lichess-api-setup/lichess-token-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/HEAD/website/static/img/lichess-api-setup/lichess-token-page.png -------------------------------------------------------------------------------- /website/static/img/lichess-api-setup/chess-tui-main-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/HEAD/website/static/img/lichess-api-setup/chess-tui-main-menu.png -------------------------------------------------------------------------------- /website/static/img/lichess-api-setup/lichess-token-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/HEAD/website/static/img/lichess-api-setup/lichess-token-config.png -------------------------------------------------------------------------------- /website/static/img/lichess-api-setup/lichess-token-generated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/HEAD/website/static/img/lichess-api-setup/lichess-token-generated.png -------------------------------------------------------------------------------- /website/blog/authors.yml: -------------------------------------------------------------------------------- 1 | thomas-mauran: 2 | name: Thomas Mauran 3 | title: Maintainer 4 | url: https://github.com/thomas-mauran 5 | image_url: https://github.com/thomas-mauran.png 6 | -------------------------------------------------------------------------------- /website/static/img/lichess-api-setup/lichess-generate-token-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/HEAD/website/static/img/lichess-api-setup/lichess-generate-token-button.png -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | }, 7 | "exclude": [".docusaurus", "build"] 8 | } 9 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /website/docs/Installation/Cargo.md: -------------------------------------------------------------------------------- 1 | # Cargo 2 | 3 | **Chess-tui** can be installed with cargo, the Rust package manager. 4 | 5 | 6 | ## Installation 7 | 8 | ```bash 9 | cargo install chess-tui 10 | ``` 11 | 12 | **Then run the game with:** 13 | ```bash 14 | chess-tui 15 | ``` 16 | 17 | The package is available on [crates.io](https://crates.io/crates/chess-tui) :tada: -------------------------------------------------------------------------------- /website/docs/Installation/Cargod.md: -------------------------------------------------------------------------------- 1 | # Cargo 2 | 3 | **Chess-tui** can be installed with cargo, the Rust package manager. 4 | 5 | 6 | ## Installation 7 | 8 | ```bash 9 | cargo install chess-tui 10 | ``` 11 | 12 | **Then run the game with:** 13 | ```bash 14 | chess-tui 15 | ``` 16 | 17 | The package is available on [crates.io](https://crates.io/crates/chess-tui) :tada: -------------------------------------------------------------------------------- /.github/workflows/release_docker.yml: -------------------------------------------------------------------------------- 1 | name: Build and push Docker image on release 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | 7 | jobs: 8 | docker_push: 9 | name: Build & push Docker image 10 | permissions: 11 | packages: write 12 | contents: read 13 | uses: ./.github/workflows/docker_push.yml 14 | with: 15 | image-tags: ${{ github.event.release.tag_name }} 16 | -------------------------------------------------------------------------------- /website/docs/Installation/Docker.mdx: -------------------------------------------------------------------------------- 1 | import { DockerCommands } from '@site/src/components/LatestVersion'; 2 | 3 | # Docker 4 | 5 | **Chess-tui** can be runned in a Docker container. 6 | 7 | ## Installation 8 | 9 | 10 | 11 | Replace the version tag with the version you want to use. Check the [GitHub releases](https://github.com/thomas-mauran/chess-tui/releases) for available versions. 12 | 13 | -------------------------------------------------------------------------------- /website/docs/Multiplayer/Local multiplayer.md: -------------------------------------------------------------------------------- 1 | # Local Multiplayer 2 | 3 | The local multiplayer feature is available in the `Normal game` menu option. You can play chess with your friends on the same computer using this feature. 4 | 5 | 6 | Each turn the board will turn allowing your opponent to play. The game will continue until one of the players wins or the game ends in a draw. 7 | 8 | ![Demo](../../static/gif/demo-two-player.gif) -------------------------------------------------------------------------------- /website/docs/Installation/Build from source.md: -------------------------------------------------------------------------------- 1 | # Build from source 2 | 3 | **Chess-tui** can be directly built from the source code. 4 | 5 | :::warning Make sure you have [Rust](https://www.rust-lang.org/tools/install) installed on your machine. 6 | ::: 7 | 8 | ## Installation 9 | 10 | ```bash 11 | git clone https://github.com/thomas-mauran/chess-tui 12 | cd chess-tui 13 | cargo build --release 14 | 15 | ./target/release/chess-tui 16 | ``` 17 | -------------------------------------------------------------------------------- /website/docs/Installation/NetBSD.md: -------------------------------------------------------------------------------- 1 | # NetBSD 2 | 3 | **Chess-tui** can be directly installed from pkgsource the NetBSD package manager. 4 | 5 | 6 | ## Installation 7 | 8 | ```bash 9 | pkgin install chess-tui 10 | ``` 11 | 12 | This package is available on the [official NetBSD repositories](https://pkgsrc.se/games/chess-tui). 13 | 14 | A big thank you to [0323pin](https://github.com/0323pin) who is taking care of the package maintenance on NetBSD 🎉 15 | -------------------------------------------------------------------------------- /examples/helper.tape: -------------------------------------------------------------------------------- 1 | Output examples/helper.gif 2 | 3 | Require echo 4 | 5 | Set BorderRadius 10 6 | Set Shell zsh 7 | 8 | Set FontSize 14 9 | Set Width 2000 10 | Set Height 1300 11 | 12 | Set WindowBar Colorful 13 | Set WindowBarSize 40 14 | 15 | Type "cargo run" Sleep 500ms Enter 16 | 17 | Sleep 2s 18 | Down @0.3s 19 | Down @0.3s 20 | Down @0.3s 21 | Down @0.3s 22 | Down @0.3s 23 | Down @0.3s 24 | Space @0.3s 25 | Sleep 1s 26 | 27 | Type h 28 | 29 | Sleep 5s 30 | -------------------------------------------------------------------------------- /website/docs/Installation/Arch Linux.md: -------------------------------------------------------------------------------- 1 | # Arch Linux 2 | 3 | **Chess-tui** can be directly built from the Arch Linux package manager. 4 | 5 | 6 | ## Installation 7 | 8 | ```bash 9 | pacman -S chess-tui 10 | ``` 11 | 12 | This package is available on the [official Arch Linux repositories](https://archlinux.org/packages/extra/x86_64/chess-tui/). 13 | 14 | A big thank you to [Orhun](https://github.com/orhun) who is taking care of the package maintenance on Arch Linux 🎉 -------------------------------------------------------------------------------- /website/docs/Installation/NixOS.md: -------------------------------------------------------------------------------- 1 | # NixOS 2 | 3 | **Chess-tui** can be directly installed from the NixOS package manager. 4 | 5 | 6 | ## Installation 7 | 8 | A nix-shell will temporarily modify your $PATH environment variable. This can be used to try a piece of software before deciding to permanently install it. 9 | 10 | ```bash 11 | nix-shell -p chess-tui 12 | ``` 13 | 14 | This package is available on the [official NixOS repositories](https://search.nixos.org/packages?show=chess-tui&query=chess+tui). -------------------------------------------------------------------------------- /scripts/render_demos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Ensure the vhs command is available 4 | command -v vhs >/dev/null 2>&1 || { echo >&2 "vhs command not found. Please install it."; exit 1; } 5 | 6 | # Check if the examples directory exists 7 | if [ ! -d "examples" ]; then 8 | echo "Examples directory not found." 9 | exit 1 10 | fi 11 | 12 | # Loop through all .tape files in the examples directory and run vhs for each file 13 | for file in examples/*.tape; do 14 | vhs "$file" 15 | done 16 | 17 | cp ./examples/*.gif ./website/static/gif -------------------------------------------------------------------------------- /website/blog/2023-12-03-release-0.1.3.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Release 0.1.3 - pawn promotion 3 | authors: 4 | - thomas-mauran 5 | tags: 6 | - release 7 | --- 8 | 9 | # Release 0.1.3 - pawn promotion 10 | 11 | **Released:** December 3, 2023 12 | 13 | - pawn promotion 14 | - refactors 15 | 16 | 17 | 18 | ## Full Changelog 19 | 20 | For the complete list of changes, see the [full changelog](https://github.com/thomas-mauran/chess-tui/releases/tag/0.1.3). 21 | 22 | --- 23 | 24 | [View on GitHub](https://github.com/thomas-mauran/chess-tui/releases/tag/0.1.3) 25 | -------------------------------------------------------------------------------- /website/blog/2025-12-15-release-2.1.2.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Release 2.1.2 - build on intel macos 3 | authors: 4 | - thomas-mauran 5 | tags: 6 | - release 7 | --- 8 | 9 | # Release 2.1.2 - build on intel macos 10 | 11 | **Released:** December 15, 2025 12 | 13 | - build on intel macos 14 | 15 | 16 | 17 | ## Full Changelog 18 | 19 | For the complete list of changes, see the [full changelog](https://github.com/thomas-mauran/chess-tui/releases/tag/2.1.2). 20 | 21 | --- 22 | 23 | [View on GitHub](https://github.com/thomas-mauran/chess-tui/releases/tag/2.1.2) 24 | -------------------------------------------------------------------------------- /.github/workflows/flow_test_build_push.yml: -------------------------------------------------------------------------------- 1 | name: Build and push docker image 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | lint: 14 | name: Lint 15 | uses: ./.github/workflows/lint.yml 16 | 17 | build_and_test: 18 | needs: lint 19 | name: Build and test 20 | uses: ./.github/workflows/build_and_test.yml 21 | 22 | release_crate: 23 | name: Release new crate 24 | needs: build_and_test 25 | uses: ./.github/workflows/release_cargo.yml 26 | secrets: inherit 27 | -------------------------------------------------------------------------------- /.github/workflows/release_cargo.yml: -------------------------------------------------------------------------------- 1 | name: Release the cargo crate 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | release: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | toolchain: stable 14 | override: true 15 | 16 | - name: Install ALSA development libraries 17 | run: sudo apt-get update && sudo apt-get install -y libasound2-dev 18 | 19 | - uses: katyo/publish-crates@v2 20 | with: 21 | registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} 22 | ignore-unpublished-changes: true 23 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /// Application. 2 | pub mod app; 3 | 4 | /// Terminal events handler. 5 | pub mod event; 6 | 7 | /// Widget renderer. 8 | pub mod ui; 9 | 10 | pub mod server; 11 | 12 | /// Event handler. 13 | pub mod handler; 14 | 15 | // Game related structures 16 | pub mod game_logic; 17 | 18 | // Constants for the app 19 | pub mod constants; 20 | 21 | // Utils methods for the board 22 | pub mod utils; 23 | 24 | // Logging 25 | pub mod logging; 26 | 27 | // Chess pieces 28 | pub mod pieces; 29 | 30 | // Configuration 31 | pub mod config; 32 | 33 | // Skin 34 | pub mod skin; 35 | 36 | // Lichess 37 | pub mod lichess; 38 | 39 | // Sound effects 40 | pub mod sound; 41 | -------------------------------------------------------------------------------- /website/docs/Code Architecture/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: Intro 3 | title: Introduction 4 | sidebar_position: 1 5 | --- 6 | 7 | # Introduction 8 | 9 | In this section we will go through the project architecture and the different classes that compose the project. 10 | 11 | The goal here is not to have a detailled description of each method and variable but rather a global overview of how things work since it would be too much work to keep this up to date. 12 | 13 | If you wanna have a really detailled code oriented documentation you can always check the code itself or the [Doc RS](https://docs.rs/chess-tui/1.4.0/chess_tui/) directly. 14 | 15 | Hopefully this will help you understand the project better and maybe even contribute to it ! 16 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Website directory - not needed for the Rust binary 2 | website/ 3 | 4 | # GitHub workflows and CI/CD files 5 | .github/ 6 | 7 | # Examples and demo files 8 | examples/ 9 | 10 | # Test files 11 | tests/ 12 | 13 | # Documentation 14 | *.md 15 | CONTRIBUTING.md 16 | LICENSE 17 | 18 | # Git files 19 | .git/ 20 | .gitignore 21 | 22 | # IDE and editor files 23 | .vscode/ 24 | .idea/ 25 | *.swp 26 | *.swo 27 | *~ 28 | 29 | # OS files 30 | .DS_Store 31 | Thumbs.db 32 | 33 | # Build artifacts (will be generated in container) 34 | target/ 35 | 36 | # Other project files not needed for build 37 | cargo-generate.toml 38 | render_demos.sh 39 | *.gif 40 | *.tape 41 | 42 | # Docker files themselves 43 | Dockerfile 44 | .dockerignore 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | #Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | .vscode/ 17 | 18 | .DS_Store 19 | 20 | # Debian packaging artifacts 21 | debian/.debhelper/ 22 | debian/files 23 | debian/substvars 24 | debian/*.substvars 25 | debian/*.log 26 | debian/chess-tui/ 27 | *.deb 28 | *.dsc 29 | *.changes 30 | *.buildinfo 31 | *.tar.gz 32 | *.tar.xz -------------------------------------------------------------------------------- /website/blog/2025-12-14-release-2.1.1.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Release 2.1.1 - fix style for the vertical alignment of the left side number 3 | authors: 4 | - thomas-mauran 5 | tags: 6 | - release 7 | --- 8 | 9 | # Release 2.1.1 - fix style for the vertical alignment of the left side number 10 | 11 | **Released:** December 14, 2025 12 | 13 | - fix style for the vertical alignment of the left side number 14 | - fix the piece style with different scaling 15 | 16 | 17 | 18 | ## Full Changelog 19 | 20 | For the complete list of changes, see the [full changelog](https://github.com/thomas-mauran/chess-tui/releases/tag/2.1.1). 21 | 22 | --- 23 | 24 | [View on GitHub](https://github.com/thomas-mauran/chess-tui/releases/tag/2.1.1) 25 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Cargo Build & Test 2 | 3 | on: 4 | pull_request: 5 | workflow_call: 6 | 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | lint: 13 | name: Rust project - latest 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | name: Checkout project 18 | 19 | - uses: dtolnay/rust-toolchain@stable 20 | name: Install the Rust toolchain 21 | 22 | - name: Install ALSA development libraries 23 | run: sudo apt-get update && sudo apt-get install -y libasound2-dev 24 | 25 | - uses: Swatinem/rust-cache@v2 26 | name: Use cached dependencies and artifacts 27 | 28 | - name: Check formatting 29 | run: cargo fmt --check 30 | 31 | - name: Check linting 32 | run: cargo clippy -- -D warnings 33 | -------------------------------------------------------------------------------- /.github/workflows/test-deploy-website.yml: -------------------------------------------------------------------------------- 1 | name: Test deployment 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | # Review gh actions docs if you want to further define triggers, paths, etc 8 | # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on 9 | 10 | jobs: 11 | test-deploy: 12 | name: Test deployment 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: 18 19 | cache: yarn 20 | cache-dependency-path: website/yarn.lock 21 | 22 | - name: Install dependencies 23 | run: yarn install --frozen-lockfile 24 | working-directory: website 25 | - name: Test build website 26 | run: yarn build 27 | working-directory: website 28 | -------------------------------------------------------------------------------- /website/docs/Installation/Binary.md: -------------------------------------------------------------------------------- 1 | # Binary 2 | 3 | **Chess-tui** is just a binary, so you can just download it through `wget` or `curl`. 4 | 5 | ## Installation 6 | 7 | You can get the latest release with: 8 | 9 | ```bash 10 | LATEST=$(curl -s "https://api.github.com/repos/thomas-mauran/chess-tui/releases" | jq -r '.[0].name') 11 | 12 | curl -LO https://github.com/thomas-mauran/chess-tui/releases/download/$LATEST/chess-tui-$LATEST-x86_64-unknown-linux-gnu.tar.gz 13 | ``` 14 | 15 | Then, extract the binary: 16 | 17 | ```bash 18 | tar xvf chess-tui-$LATEST-x86_64-unknown-linux-gnu.tar.gz 19 | ``` 20 | 21 | **And finally, run the game with:** 22 | 23 | ```bash 24 | ./chess-tui 25 | ``` 26 | 27 | You can find the latest release here [github.com/thomas-mauran/chess-tui/releases/latest](https://github.com/thomas-mauran/chess-tui/releases/latest) :tada: 28 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Serialize, Deserialize)] 4 | pub struct Config { 5 | pub engine_path: Option, 6 | pub display_mode: Option, 7 | pub log_level: Option, 8 | pub bot_depth: Option, 9 | pub selected_skin_name: Option, 10 | pub lichess_token: Option, 11 | pub sound_enabled: Option, 12 | } 13 | 14 | impl Default for Config { 15 | fn default() -> Self { 16 | Self { 17 | engine_path: None, 18 | display_mode: Some("DEFAULT".to_string()), 19 | log_level: Some("OFF".to_string()), 20 | bot_depth: Some(10), 21 | selected_skin_name: Some("Default".to_string()), 22 | lichess_token: None, 23 | sound_enabled: Some(true), 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: enhancement 6 | assignees: "" 7 | --- 8 | 9 | ## Problem 10 | 11 | 14 | 15 | ## Solution 16 | 17 | 24 | 25 | ## Alternatives 26 | 27 | 30 | 31 | ## Additional context 32 | 33 | 36 | -------------------------------------------------------------------------------- /examples/play_against_black_bot.tape: -------------------------------------------------------------------------------- 1 | Output examples/play_against_black_bot.gif 2 | 3 | Require echo 4 | 5 | Set BorderRadius 10 6 | Set Shell zsh 7 | 8 | Set FontSize 14 9 | Set Width 2000 10 | Set Height 1300 11 | 12 | Set WindowBar Colorful 13 | Set WindowBarSize 40 14 | 15 | Type "cargo run" Sleep 500ms Enter 16 | 17 | Sleep 2s 18 | Down @0.3s 19 | Down @0.3s 20 | Down @0.3s 21 | Space @0.3s 22 | Sleep 0.7s 23 | 24 | Right @0.3s 25 | Space @0.3s 26 | Sleep 0.7s 27 | 28 | Down @0.3s 29 | Down @0.3s 30 | Space @0.3s 31 | Sleep 0.7s 32 | Space @0.3s 33 | 34 | Sleep 0.7s 35 | Right @0.3s 36 | Space @0.3s 37 | Sleep 0.7s 38 | Space @0.3s 39 | 40 | Sleep 0.7s 41 | Right @0.3s 42 | Space @0.3s 43 | Sleep 0.7s 44 | Space @0.3s 45 | 46 | Sleep 0.7s 47 | Left @0.3s 48 | Left @0.3s 49 | Left @0.3s 50 | Left @0.3s 51 | Left @0.3s 52 | Space @0.3s 53 | Sleep 0.7s 54 | Space @0.3s 55 | Sleep 0.7s 56 | 57 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /website/docs/Installation/Packaging status.md: -------------------------------------------------------------------------------- 1 | # Packaging status 2 | 3 | Thanks to a few awesome people, `chess-tui` is available in a few package managers. Here is a list of the package managers and the current status of the packaging. 4 | 5 | [![Packaging status](https://repology.org/badge/vertical-allrepos/chess-tui.svg)](https://repology.org/project/chess-tui/versions) 6 | 7 | ## Direct Downloads 8 | 9 | **Debian/Ubuntu (.deb package):** 10 | A `.deb` package is available for each release. You can download and install it with: 11 | 12 | ```bash 13 | DEB_URL=$(curl -s "https://api.github.com/repos/thomas-mauran/chess-tui/releases/latest" | jq -r '.assets[] | select(.name | endswith(".deb")) | .browser_download_url') && curl -LO "$DEB_URL" && sudo dpkg -i "$(basename "$DEB_URL")" && sudo apt-get install -f 14 | ``` 15 | 16 | **Homebrew (unofficial tap):** 17 | An unofficial Homebrew tap is available: 18 | 19 | ```bash 20 | brew install thomas-mauran/tap/chess-tui 21 | ``` 22 | -------------------------------------------------------------------------------- /examples/lichess-menu.tape: -------------------------------------------------------------------------------- 1 | Output examples/lichess-menu.gif 2 | 3 | Require echo 4 | 5 | Set BorderRadius 10 6 | Set Shell zsh 7 | 8 | Set FontSize 14 9 | Set Width 2000 10 | Set Height 1300 11 | 12 | Set WindowBar Colorful 13 | Set WindowBarSize 40 14 | 15 | Type "cargo run" Sleep 500ms Enter 16 | 17 | Sleep 2s 18 | 19 | # Navigate to Lichess menu (option 2, index 2) 20 | Down @0.3s 21 | Down @0.3s 22 | Space @0.3s 23 | 24 | Sleep 2s 25 | 26 | # Navigate through Lichess menu options 27 | # Show "Seek Game" option 28 | Sleep 1s 29 | 30 | # Navigate to "Puzzle" option 31 | Down @0.3s 32 | Sleep 1s 33 | 34 | # Navigate to "My Ongoing Games" option 35 | Down @0.3s 36 | Sleep 1s 37 | 38 | # Navigate to "Join by Code" option 39 | Down @0.3s 40 | Sleep 1s 41 | 42 | # Go back up to show "Puzzle" option 43 | Up @0.3s 44 | Sleep 1s 45 | 46 | # Show the menu for a bit longer to see the stats panel 47 | Sleep 3s 48 | 49 | # Press Esc to go back to home menu 50 | Type Esc 51 | 52 | Sleep 2s 53 | -------------------------------------------------------------------------------- /website/blog/2024-03-26-release-1.2.1.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Release 1.2.1 - ASCII Mode and Key Event Fixes 3 | authors: 4 | - thomas-mauran 5 | tags: 6 | - release 7 | date: 2024-03-26 8 | --- 9 | 10 | # Release 1.2.1 11 | 12 | **Released:** March 26, 2024 13 | 14 | This release adds ASCII display mode and fixes key event handling. 15 | 16 | 17 | 18 | ## What's New 19 | 20 | ### ASCII Mode and Board Style 21 | Added ASCII display mode and customizable board styles for different visual preferences. 22 | 23 | ### Bug Fixes 24 | - Fixed key release and repeat event handling to ignore unnecessary events 25 | 26 | ## Contributors 27 | 28 | Thank you to: 29 | - @joshka 30 | - @psnehanshu (first contribution!) 31 | 32 | ## Full Changelog 33 | 34 | For the complete list of changes, see the [full changelog](https://github.com/thomas-mauran/chess-tui/compare/1.2.0...1.2.1). 35 | 36 | --- 37 | 38 | [View on GitHub](https://github.com/thomas-mauran/chess-tui/releases/tag/1.2.1) 39 | -------------------------------------------------------------------------------- /website/blog/2024-11-17-release-1.4.0.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Release 1.4.0 - Threefold Repetition Fix and Board Rotation 3 | authors: 4 | - thomas-mauran 5 | tags: 6 | - release 7 | date: 2024-11-17 8 | --- 9 | 10 | # Release 1.4.0 11 | 12 | **Released:** November 17, 2024 13 | 14 | This release includes an important rule fix and a new board rotation feature. 15 | 16 | 17 | 18 | ## What's New 19 | 20 | ### Turn Board Feature 21 | You can now rotate the board to view it from either player's perspective, making it easier to play from any angle. 22 | 23 | ### Bug Fixes 24 | - Fixed threefold repetition detection to properly handle draw scenarios 25 | 26 | ## Contributors 27 | 28 | Thank you to: 29 | - @TomaDumitrescu (first contribution!) 30 | - @thomas-mauran 31 | 32 | ## Full Changelog 33 | 34 | For the complete list of changes, see the [full changelog](https://github.com/thomas-mauran/chess-tui/compare/1.3.0...1.4.0). 35 | 36 | --- 37 | 38 | [View on GitHub](https://github.com/thomas-mauran/chess-tui/releases/tag/1.4.0) 39 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Title 2 | 3 | ## Description 4 | 5 | 6 | 7 | Fixes # (issue) 8 | 9 | ## How Has This Been Tested? 10 | 11 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 12 | 13 | - [ ] Test A 14 | - [ ] Test B 15 | 16 | ## Checklist: 17 | 18 | - [ ] My code follows the style guidelines of this project 19 | - [ ] I have performed a self-review of my code 20 | - [ ] I have commented my code, particularly in hard-to-understand areas 21 | - [ ] I have made corresponding changes to the documentation 22 | - [ ] My changes generate no new warnings 23 | - [ ] I have added tests that prove my fix is effective or that my feature works 24 | - [ ] New and existing unit tests pass locally with my changes 25 | - [ ] Any dependent changes have been merged and published in downstream modules 26 | -------------------------------------------------------------------------------- /src/logging.rs: -------------------------------------------------------------------------------- 1 | use chrono::Local; 2 | use log::LevelFilter; 3 | use simplelog::{CombinedLogger, Config, WriteLogger}; 4 | use std::fs; 5 | use std::path::Path; 6 | 7 | pub fn setup_logging( 8 | config_dir: &Path, 9 | log_level: &LevelFilter, 10 | ) -> Result<(), Box> { 11 | match log_level { 12 | LevelFilter::Off => Ok(()), // No logging setup needed 13 | level => { 14 | // Create logs directory 15 | let log_dir = config_dir.join("logs"); 16 | fs::create_dir_all(&log_dir)?; 17 | 18 | // Create log file with timestamp 19 | let timestamp = Local::now().format("%Y-%m-%d_%H-%M-%S"); 20 | let log_file = log_dir.join(format!("chess-tui_{}.log", timestamp)); 21 | 22 | CombinedLogger::init(vec![WriteLogger::new( 23 | *level, 24 | Config::default(), 25 | fs::File::create(log_file)?, 26 | )])?; 27 | 28 | log::info!("Logging initialized at {level} level"); 29 | Ok(()) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /website/blog/2024-12-15-release-1.6.0.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Release 1.6.0 - Online Multiplayer Support 3 | authors: 4 | - thomas-mauran 5 | tags: 6 | - release 7 | date: 2024-12-15 8 | --- 9 | 10 | # Release 1.6.0 11 | 12 | **Released:** December 15, 2024 13 | 14 | This release introduces online multiplayer functionality, allowing you to play chess with others over the network! 15 | 16 | 17 | 18 | ## What's New 19 | 20 | ### Online Multiplayer 21 | Play chess with friends and opponents online! This feature enables real-time multiplayer games over the network. 22 | 23 | ### Dependency Updates 24 | - Updated `lru` to v0.12.5 25 | 26 | ### Docker Improvements 27 | - Fixed Dockerfile for better containerization support 28 | 29 | ## Contributors 30 | 31 | Thank you to: 32 | - @Adamska1008 (first contribution!) 33 | - @thomas-mauran 34 | 35 | ## Full Changelog 36 | 37 | For the complete list of changes, see the [full changelog](https://github.com/thomas-mauran/chess-tui/compare/1.5.0...1.6.0). 38 | 39 | --- 40 | 41 | [View on GitHub](https://github.com/thomas-mauran/chess-tui/releases/tag/1.6.0) 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Thomas Mauran 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/skin_tests.rs: -------------------------------------------------------------------------------- 1 | use chess_tui::skin::Skin; 2 | use ratatui::style::Color; 3 | use std::io::Write; 4 | use tempfile::NamedTempFile; 5 | 6 | #[test] 7 | fn test_default_skin() { 8 | let skin = Skin::default(); 9 | assert_eq!(skin.name, "Default"); 10 | assert_eq!(skin.board_white_color, Color::Rgb(160, 160, 160)); 11 | assert_eq!(skin.piece_white_color, Color::White); 12 | } 13 | 14 | #[test] 15 | fn test_load_skin() { 16 | let json = r#"{ 17 | "name": "Test Skin", 18 | "board_white_color": "Red", 19 | "board_black_color": "Blue", 20 | "piece_white_color": "Green", 21 | "piece_black_color": "Yellow", 22 | "cursor_color": "LightBlue", 23 | "selection_color": "LightGreen", 24 | "last_move_color": "LightGreen" 25 | }"#; 26 | 27 | let mut file = NamedTempFile::new().unwrap(); 28 | write!(file, "{}", json).unwrap(); 29 | 30 | let skin = Skin::load_from_file(file.path()).unwrap(); 31 | 32 | assert_eq!(skin.name, "Test Skin"); 33 | assert_eq!(skin.board_white_color, Color::Red); 34 | assert_eq!(skin.board_black_color, Color::Blue); 35 | assert_eq!(skin.piece_white_color, Color::Green); 36 | } 37 | -------------------------------------------------------------------------------- /website/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | @media screen and (max-width: 996px) { 9 | .features { 10 | padding: 1.5rem 0; 11 | } 12 | 13 | /* Ensure features stack properly on mobile */ 14 | .features .container .row { 15 | margin: 0; 16 | } 17 | 18 | .features .container .row > div { 19 | margin-bottom: 2rem; 20 | } 21 | } 22 | 23 | @media screen and (max-width: 480px) { 24 | .features { 25 | padding: 1rem 0; 26 | } 27 | 28 | .features .container .row > div { 29 | margin-bottom: 1.5rem; 30 | padding: 0 1rem; /* Add horizontal padding */ 31 | } 32 | 33 | .features .container .row > div h3 { 34 | font-size: 1.2rem; /* Slightly smaller headings on mobile */ 35 | } 36 | 37 | .features .container .row > div p { 38 | font-size: 0.9rem; /* Slightly smaller text on mobile */ 39 | } 40 | } 41 | 42 | .featureGif { 43 | max-width: 100%; /* Ensure the image doesn't overflow its container */ 44 | height: auto; /* Maintain aspect ratio */ 45 | display: inline-block; /* Ensure proper alignment */ 46 | } 47 | 48 | -------------------------------------------------------------------------------- /examples/demo-two-player.tape: -------------------------------------------------------------------------------- 1 | Output examples/demo-two-player.gif 2 | 3 | Require echo 4 | 5 | Set BorderRadius 10 6 | Set Shell zsh 7 | 8 | Set FontSize 14 9 | Set Width 2000 10 | Set Height 1300 11 | 12 | Set WindowBar Colorful 13 | Set WindowBarSize 40 14 | 15 | Type "cargo run" Sleep 500ms Enter 16 | 17 | Sleep 0.7s 18 | Down @0.3s 19 | Down @0.3s 20 | Down @0.3s 21 | Down @0.3s 22 | Down @0.3s 23 | Down @0.3s 24 | Down @0.3s 25 | Down @0.3s 26 | Space @0.3s 27 | Sleep 1.5s 28 | 29 | Down @0.3s 30 | Down @0.3s 31 | Space @0.3s 32 | Sleep 0.7s 33 | Space @0.3s 34 | 35 | Sleep 0.7s 36 | Left @0.3s 37 | Space @0.3s 38 | Sleep 0.7s 39 | Space @0.3s 40 | 41 | Down @0.3s 42 | Space @0.3s 43 | Sleep 0.7s 44 | Space @0.3s 45 | Sleep 0.7s 46 | 47 | Right @0.3s 48 | Right @0.3s 49 | Right @0.3s 50 | Space @0.3s 51 | Sleep 0.7s 52 | Space @0.3s 53 | Sleep 0.7s 54 | 55 | Left @0.3s 56 | Space @0.3s 57 | Sleep 0.7s 58 | Right @0.3s 59 | Right @0.3s 60 | Space @0.3s 61 | Sleep 0.7s 62 | 63 | Up @0.3s 64 | Right @0.3s 65 | Right @0.3s 66 | Space @0.3s 67 | Sleep 0.7s 68 | Space @0.3s 69 | Sleep 0.7s 70 | 71 | 72 | Up @0.3s 73 | Up @0.3s 74 | Up @0.3s 75 | Space @0.3s 76 | Sleep 0.7s 77 | Space @0.3s 78 | Sleep 0.7s 79 | -------------------------------------------------------------------------------- /src/ui/tui.rs: -------------------------------------------------------------------------------- 1 | use crate::app::{App, AppResult}; 2 | use crate::event::EventHandler; 3 | use crate::ui::main_ui; 4 | use ratatui::backend::Backend; 5 | use ratatui::Terminal; 6 | 7 | /// Representation of a terminal user interface. 8 | /// 9 | /// It is responsible for setting up the terminal, 10 | /// initializing the interface and handling the draw events. 11 | #[derive(Debug)] 12 | pub struct Tui { 13 | /// Interface to the Terminal. 14 | terminal: Terminal, 15 | /// Terminal event handler. 16 | pub events: EventHandler, 17 | } 18 | 19 | impl Tui { 20 | /// Constructs a new instance of [`Tui`]. 21 | pub fn new(terminal: Terminal, events: EventHandler) -> Self { 22 | Self { terminal, events } 23 | } 24 | 25 | /// [`Draw`] the terminal interface by [`rendering`] the widgets. 26 | /// 27 | /// [`Draw`]: ratatui::Terminal::draw 28 | /// [`rendering`]: crate::ui:render 29 | // Créer une fonction async pour le rendu 30 | pub fn draw(&mut self, app: &mut App) -> AppResult<()> { 31 | // Passe une closure synchrone qui appelle la fonction async 32 | self.terminal.draw(|frame| main_ui::render(app, frame))?; 33 | Ok(()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /website/blog/2023-12-04-release-1.0.0.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Release 1.0.0 - Working game, Helper video demo + readme setup for features + render script 3 | authors: 4 | - thomas-mauran 5 | tags: 6 | - release 7 | --- 8 | 9 | # Release 1.0.0 - Working game, Helper video demo + readme setup for features + render script 10 | 11 | **Released:** December 4, 2023 12 | 13 | ## What's New 14 | 15 | 16 | * feat: working game :partying_face: 17 | * feat: helper video demo + readme setup for features + render script by [@thomas-mauran](https://github.com/thomas-mauran) in https://github.com/thomas-mauran/chess-tui/pull/29 18 | * feat(board): draw by 50 moves and 3 times same position by [@thomas-mauran](https://github.com/thomas-mauran) in https://github.com/thomas-mauran/chess-tui/pull/30 19 | * refactor: use & borrowing for move_history instead of clones by [@thomas-mauran](https://github.com/thomas-mauran) in https://github.com/thomas-mauran/chess-tui/pull/32 20 | 21 | 22 | 23 | ## Full Changelog 24 | 25 | For the complete list of changes, see the [full changelog](https://github.com/thomas-mauran/chess-tui/releases/tag/1.0.0). 26 | 27 | --- 28 | 29 | [View on GitHub](https://github.com/thomas-mauran/chess-tui/releases/tag/1.0.0) 30 | -------------------------------------------------------------------------------- /website/blog/2024-02-28-release-1.2.0.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Release 1.2.0 - Vim Shortcuts and UCI Engine Support 3 | authors: 4 | - thomas-mauran 5 | tags: 6 | - release 7 | date: 2024-02-28 8 | --- 9 | 10 | # Release 1.2.0 11 | 12 | **Released:** February 28, 2024 13 | 14 | This release introduces Vim keyboard shortcuts and UCI chess engine connection support. 15 | 16 | 17 | 18 | ## What's New 19 | 20 | ### Vim Keyboard Shortcuts 21 | Added Vim-style keyboard shortcuts for navigation and interaction, making the interface more familiar for Vim users. 22 | 23 | ### UCI and Chess Engine Connection 24 | Chess TUI now supports connecting to UCI-compatible chess engines, allowing you to play against stronger opponents and use different engines. 25 | 26 | ### Code Quality 27 | - Applied `cargo clippy` linting improvements throughout the codebase 28 | 29 | ## Contributors 30 | 31 | Thank you to: 32 | - @JeromeSchmied (first contribution!) 33 | - @thomas-mauran 34 | - @jarjk 35 | 36 | ## Full Changelog 37 | 38 | For the complete list of changes, see the [full changelog](https://github.com/thomas-mauran/chess-tui/compare/1.1.0...1.2.0). 39 | 40 | --- 41 | 42 | [View on GitHub](https://github.com/thomas-mauran/chess-tui/releases/tag/1.2.0) 43 | -------------------------------------------------------------------------------- /examples/play_against_white_bot.tape: -------------------------------------------------------------------------------- 1 | Output examples/play_against_white_bot.gif 2 | 3 | Require echo 4 | 5 | Set BorderRadius 10 6 | Set Shell zsh 7 | 8 | Set FontSize 14 9 | Set Width 2000 10 | Set Height 1300 11 | 12 | Set WindowBar Colorful 13 | Set WindowBarSize 40 14 | 15 | Type "cargo run" Sleep 0.7s Enter 16 | 17 | Sleep 2s 18 | Down @0.3s 19 | Down @0.3s 20 | Down @0.3s 21 | Space @0.3s 22 | Sleep 0.7s 23 | Space @0.3s 24 | Sleep 0.7s 25 | 26 | Down @0.3s 27 | Down @0.3s 28 | Space @0.3s 29 | Sleep 0.7s 30 | Space @0.3s 31 | Sleep 0.7s 32 | 33 | Down @0.3s 34 | Space @0.3s 35 | Sleep 0.7s 36 | Space @0.3s 37 | Sleep 0.7s 38 | 39 | Up @0.3s 40 | Space @0.3s 41 | Sleep 0.7s 42 | Space @0.3s 43 | Sleep 0.7s 44 | 45 | Up @0.3s 46 | Left @0.3s 47 | Space @0.3s 48 | Sleep 0.7s 49 | Space @0.3s 50 | Sleep 0.7s 51 | 52 | Up @0.3s 53 | Left @0.3s 54 | Space @0.3s 55 | Sleep 0.7s 56 | Right @0.3s 57 | Space @0.3s 58 | Sleep 0.7s 59 | 60 | Left @0.3s 61 | Down @0.3s 62 | Space @0.3s 63 | Sleep 0.7s 64 | Space @0.3s 65 | Sleep 0.7s 66 | 67 | Left @0.3s 68 | Space @0.3s 69 | Sleep 0.7s 70 | Space @0.3s 71 | Sleep 0.7s 72 | 73 | Left @0.3s 74 | Up @0.3s 75 | Space @0.3s 76 | Sleep 0.7s 77 | Space @0.3s 78 | Sleep 0.7s 79 | 80 | Sleep 1.5s 81 | Type "h" 82 | 83 | Sleep 5s -------------------------------------------------------------------------------- /src/pieces/queen.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::DisplayMode; 2 | use crate::pieces::PieceSize; 3 | use shakmaty::Color; 4 | 5 | pub struct Queen; 6 | 7 | impl Queen { 8 | pub fn to_string(display_mode: &DisplayMode, size: PieceSize, color: Option) -> String { 9 | match display_mode { 10 | DisplayMode::DEFAULT | DisplayMode::CUSTOM => match size { 11 | PieceSize::Small => match color { 12 | Some(Color::White) => "♕".to_string(), 13 | Some(Color::Black) => "♛".to_string(), 14 | None => " ".to_string(), 15 | }, 16 | PieceSize::Compact => { 17 | // Simple 2-line design for medium-sized cells 18 | "\n ◈\n ███".to_string() 19 | } 20 | PieceSize::Extended => { 21 | // Extended 3-4 line design - more solid and consistent 22 | " ◈\n █▄█\n ███\n ███".to_string() 23 | } 24 | PieceSize::Large => "\ 25 | \n\ 26 | ◀█▟█▙█▶\n\ 27 | ◥█◈█◤\n\ 28 | ███\n\ 29 | ▗█████▖\n\ 30 | " 31 | .to_string(), 32 | }, 33 | DisplayMode::ASCII => "Q".to_string(), 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/pieces/knight.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::DisplayMode; 2 | use crate::pieces::PieceSize; 3 | use shakmaty::Color; 4 | 5 | pub struct Knight; 6 | 7 | impl Knight { 8 | pub fn to_string(display_mode: &DisplayMode, size: PieceSize, color: Option) -> String { 9 | match display_mode { 10 | DisplayMode::DEFAULT | DisplayMode::CUSTOM => match size { 11 | PieceSize::Small => match color { 12 | Some(Color::White) => "♘".to_string(), 13 | Some(Color::Black) => "♞".to_string(), 14 | None => " ".to_string(), 15 | }, 16 | PieceSize::Compact => { 17 | // Simple 2-line design for medium-sized cells 18 | "\n▞█▙\n ██".to_string() 19 | } 20 | PieceSize::Extended => { 21 | // Extended 3-4 line design - more solid and consistent 22 | " \n ▞█▙\n▞███\n ███".to_string() 23 | } 24 | PieceSize::Large => "\ 25 | \n\ 26 | ▟▛██▙\n\ 27 | ▟█████\n\ 28 | ▀▀▟██▌\n\ 29 | ▟████\n\ 30 | " 31 | .to_string(), 32 | }, 33 | DisplayMode::ASCII => "N".to_string(), 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/pieces/pawn.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::DisplayMode; 2 | use crate::pieces::PieceSize; 3 | use shakmaty::Color; 4 | 5 | pub struct Pawn; 6 | 7 | impl Pawn { 8 | pub fn to_string(display_mode: &DisplayMode, size: PieceSize, color: Option) -> String { 9 | match display_mode { 10 | DisplayMode::DEFAULT | DisplayMode::CUSTOM => match size { 11 | PieceSize::Small => match color { 12 | Some(Color::White) => "♙".to_string(), 13 | Some(Color::Black) => "♟".to_string(), 14 | None => " ".to_string(), 15 | }, 16 | PieceSize::Compact => { 17 | // Simple 2-line design for medium-sized cells 18 | "\n ▟▙\n ██".to_string() 19 | } 20 | PieceSize::Extended => { 21 | // Extended 3-4 line design - more solid and consistent 22 | " \n \n ▟▙\n ██".to_string() 23 | } 24 | PieceSize::Large => "\ 25 | \n\ 26 | \n\ 27 | ▟█▙\n\ 28 | ▜█▛\n\ 29 | ▟███▙\n\ 30 | " 31 | .to_string(), 32 | }, 33 | DisplayMode::ASCII => "P".to_string(), 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/pieces/rook.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::DisplayMode; 2 | use crate::pieces::PieceSize; 3 | use shakmaty::Color; 4 | 5 | pub struct Rook; 6 | 7 | impl Rook { 8 | pub fn to_string(display_mode: &DisplayMode, size: PieceSize, color: Option) -> String { 9 | match display_mode { 10 | DisplayMode::DEFAULT | DisplayMode::CUSTOM => match size { 11 | PieceSize::Small => match color { 12 | Some(Color::White) => "♖".to_string(), 13 | Some(Color::Black) => "♜".to_string(), 14 | None => " ".to_string(), 15 | }, 16 | PieceSize::Compact => { 17 | // Simple 2-line design for medium-sized cells 18 | "\n ▙█▟\n ███".to_string() 19 | } 20 | PieceSize::Extended => { 21 | // Extended 3-4 line design - more solid and consistent 22 | " \n ▙█▟\n ███\n ███".to_string() 23 | } 24 | PieceSize::Large => "\ 25 | \n\ 26 | █▟█▙█\n\ 27 | ▜███▛\n\ 28 | ▐███▌\n\ 29 | ▗█████▖\n\ 30 | " 31 | .to_string(), 32 | }, 33 | DisplayMode::ASCII => "R".to_string(), 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/pieces/bishop.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::DisplayMode; 2 | use crate::pieces::PieceSize; 3 | use shakmaty::Color; 4 | 5 | pub struct Bishop; 6 | 7 | impl Bishop { 8 | pub fn to_string(display_mode: &DisplayMode, size: PieceSize, color: Option) -> String { 9 | match display_mode { 10 | DisplayMode::DEFAULT | DisplayMode::CUSTOM => match size { 11 | PieceSize::Small => match color { 12 | Some(Color::White) => "♗".to_string(), 13 | Some(Color::Black) => "♝".to_string(), 14 | None => " ".to_string(), 15 | }, 16 | PieceSize::Compact => { 17 | // Simple 2-line design for medium-sized cells 18 | "\n ⭘\n ███".to_string() 19 | } 20 | PieceSize::Extended => { 21 | // Extended 3-4 line design - more solid and consistent 22 | " ⭘\n █▄█\n ███\n ███".to_string() 23 | } 24 | PieceSize::Large => "\ 25 | \n\ 26 | ⭘\n\ 27 | █✝█\n\ 28 | ███\n\ 29 | ▗█████▖\n\ 30 | " 31 | .to_string(), 32 | }, 33 | DisplayMode::ASCII => "B".to_string(), 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /website/docs/Installation/Debian Ubuntu.md: -------------------------------------------------------------------------------- 1 | # Debian/Ubuntu 2 | 3 | **Chess-tui** can be installed on Debian-based systems using the `.deb` package from GitHub releases. 4 | 5 | ## Installation 6 | 7 | ### One-liner (recommended) 8 | 9 | ```bash 10 | DEB_URL=$(curl -s "https://api.github.com/repos/thomas-mauran/chess-tui/releases/latest" | jq -r '.assets[] | select(.name | endswith(".deb")) | .browser_download_url') && curl -LO "$DEB_URL" && sudo dpkg -i "$(basename "$DEB_URL")" && sudo apt-get install -f 11 | ``` 12 | 13 | ### Step-by-step 14 | 15 | ```bash 16 | # Get the latest release and find the .deb file 17 | DEB_URL=$(curl -s "https://api.github.com/repos/thomas-mauran/chess-tui/releases/latest" | jq -r '.assets[] | select(.name | endswith(".deb")) | .browser_download_url') 18 | 19 | # Download the .deb package 20 | curl -LO "$DEB_URL" 21 | 22 | # Get the filename from the URL 23 | DEB_FILE=$(basename "$DEB_URL") 24 | 25 | # Install the package 26 | sudo dpkg -i "$DEB_FILE" 27 | 28 | # Fix any missing dependencies (if needed) 29 | sudo apt-get install -f 30 | ``` 31 | 32 | **Then run the game with:** 33 | ```bash 34 | chess-tui 35 | ``` 36 | 37 | You can find the latest release here [github.com/thomas-mauran/chess-tui/releases/latest](https://github.com/thomas-mauran/chess-tui/releases/latest) :tada: 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation Request 3 | about: Propose or add documentation for the project 4 | title: "" 5 | labels: documentation 6 | assignees: "" 7 | 8 | --- 9 | 10 | ### Scope of Documentation 11 | 12 | 18 | 19 | --- 20 | 21 | ### Proposed Documentation Plan 22 | 23 | 30 | 31 | --- 32 | 33 | ### Expected Benefits 34 | 35 | 42 | 43 | --- 44 | 45 | ### Additional Context 46 | 47 | 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/refactor_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Refactor request 3 | about: Propose improvements to the code structure or architecture 4 | title: "" 5 | labels: refactor 6 | assignees: "" 7 | 8 | --- 9 | 10 | ### Problem 11 | 12 | 15 | 16 | --- 17 | 18 | ### Proposed Refactor Plan 19 | 20 | 28 | 29 | --- 30 | 31 | ### Expected Benefits 32 | 33 | 40 | 41 | --- 42 | 43 | ### Additional Context 44 | 45 | 50 | -------------------------------------------------------------------------------- /website/blog/2025-02-03-release-1.6.1.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Release 1.6.1 - Logging System and Multiplayer Fixes 3 | authors: 4 | - thomas-mauran 5 | tags: 6 | - release 7 | date: 2025-02-03 8 | --- 9 | 10 | # Release 1.6.1 11 | 12 | **Released:** February 3, 2025 13 | 14 | This release focuses on improving logging capabilities and fixing multiplayer connection issues. 15 | 16 | 17 | 18 | ## What's New 19 | 20 | ### Configurable Logging System 21 | Added a comprehensive, configurable logging system with full documentation. This makes debugging and monitoring much easier. 22 | 23 | ### Multiplayer Connection Improvements 24 | - Fixed connection issues with improved socket handling 25 | - Enhanced logging for multiplayer connections to help diagnose issues 26 | 27 | ### History Display Fix 28 | Corrected the moves history display to show moves accurately. 29 | 30 | ### Bug Fixes 31 | - Fixed ESC key bug when playing against a bot 32 | 33 | ## Contributors 34 | 35 | Thank you to: 36 | - @TomPlanche (first contribution!) 37 | - @thomas-mauran 38 | 39 | ## Full Changelog 40 | 41 | For the complete list of changes, see the [full changelog](https://github.com/thomas-mauran/chess-tui/compare/1.6.0...1.6.1). 42 | 43 | --- 44 | 45 | [View on GitHub](https://github.com/thomas-mauran/chess-tui/releases/tag/1.6.1) 46 | -------------------------------------------------------------------------------- /website/blog/2025-12-16-release-2.2.0.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Release 2.2.0 - Blinking selected cell cursor, Improve the color selection popup, Allow engine to have an argumen... 3 | authors: 4 | - thomas-mauran 5 | tags: 6 | - release 7 | --- 8 | 9 | # Release 2.2.0 - Blinking selected cell cursor, Improve the color selection popup, Allow engine to have an argumen... 10 | 11 | **Released:** December 16, 2025 12 | 13 | ## What's New 14 | 15 | 16 | * feat: blinking selected cell cursor by [@thomas-mauran](https://github.com/thomas-mauran) in https://github.com/thomas-mauran/chess-tui/pull/169 17 | * feat: improve the color selection popup by [@thomas-mauran](https://github.com/thomas-mauran) in https://github.com/thomas-mauran/chess-tui/pull/171 18 | * feat: allow engine to have an argument too by [@thomas-mauran](https://github.com/thomas-mauran) in https://github.com/thomas-mauran/chess-tui/pull/172 19 | * feat: deb build by [@thomas-mauran](https://github.com/thomas-mauran) in https://github.com/thomas-mauran/chess-tui/pull/173 20 | 21 | 22 | 23 | ## Full Changelog 24 | 25 | For the complete list of changes, see the [full changelog](https://github.com/thomas-mauran/chess-tui/releases/tag/2.2.0). 26 | 27 | --- 28 | 29 | [View on GitHub](https://github.com/thomas-mauran/chess-tui/releases/tag/2.2.0) 30 | -------------------------------------------------------------------------------- /website/blog/2023-12-02-release-0.1.2.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Release 0.1.2 - Remove explicitly set background on board blocks, Game fully working 3 | authors: 4 | - thomas-mauran 5 | tags: 6 | - release 7 | --- 8 | 9 | # Release 0.1.2 - Remove explicitly set background on board blocks, Game fully working 10 | 11 | **Released:** December 2, 2023 12 | 13 | ## What's New 14 | 15 | 16 | * pieces by [@joshka](https://github.com/joshka) in https://github.com/thomas-mauran/chess-tui/pull/2 17 | * fix: remove explicitly set background on board blocks by [@Kuruyia](https://github.com/Kuruyia) in https://github.com/thomas-mauran/chess-tui/pull/16 18 | * feat: game fully working by [@thomas-mauran](https://github.com/thomas-mauran) 19 | 20 | * [@joshka](https://github.com/joshka) made their first contribution in https://github.com/thomas-mauran/chess-tui/pull/2 21 | * [@Kuruyia](https://github.com/Kuruyia) made their first contribution in https://github.com/thomas-mauran/chess-tui/pull/16 22 | * [@thomas-mauran](https://github.com/thomas-mauran) made their first contribution in https://github.com/thomas-mauran/chess-tui/pull/19 23 | 24 | 25 | 26 | ## Full Changelog 27 | 28 | For the complete list of changes, see the [full changelog](https://github.com/thomas-mauran/chess-tui/releases/tag/0.1.2). 29 | 30 | --- 31 | 32 | [View on GitHub](https://github.com/thomas-mauran/chess-tui/releases/tag/0.1.2) 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create an issue about a bug you encountered 4 | title: "" 5 | labels: bug 6 | assignees: "" 7 | --- 8 | 9 | 14 | 15 | ## Description 16 | 17 | 20 | 21 | ## To Reproduce 22 | 23 | 27 | 28 | ## Expected behavior 29 | 30 | 33 | 34 | ## Screenshots 35 | 36 | 39 | 40 | ## Environment 41 | 42 | 50 | 51 | - OS: 52 | - Terminal Emulator: 53 | - Font: 54 | - Crate version: 55 | - Backend: 56 | 57 | ## Additional context 58 | 59 | 63 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 1. This tells docker to use the Rust official image 2 | FROM rust:1.87 as builder 3 | 4 | # Install ALSA development libraries for rodio 5 | RUN apt-get update && \ 6 | apt-get install -y --no-install-recommends \ 7 | libasound2-dev \ 8 | && rm -rf /var/lib/apt/lists/* 9 | 10 | # 2. Copy dependency files first for better layer caching 11 | COPY Cargo.toml Cargo.lock ./ 12 | 13 | # 3. Create a dummy src directory to cache dependencies 14 | RUN mkdir src && echo "fn main() {}" > src/main.rs && \ 15 | cargo build --release && \ 16 | rm -rf src 17 | 18 | # 4. Copy the actual source code 19 | COPY src/ ./src/ 20 | 21 | # 5. Build your program for release 22 | RUN cargo build --release 23 | 24 | FROM debian:bookworm-slim AS runner 25 | 26 | # Install SSL libraries required by reqwest and ALSA runtime libraries for rodio 27 | RUN apt-get update && \ 28 | apt-get install -y --no-install-recommends \ 29 | libssl3 \ 30 | libasound2 \ 31 | ca-certificates && \ 32 | rm -rf /var/lib/apt/lists/* 33 | 34 | # Configure ALSA to use a null device to suppress error messages when no audio hardware is available 35 | # This prevents ALSA from spamming stderr with errors in Docker containers 36 | RUN echo 'pcm.!default { type null }' > /etc/asound.conf && \ 37 | echo 'ctl.!default { type null }' >> /etc/asound.conf 38 | 39 | COPY --from=builder /target/release/chess-tui /usr/bin/chess-tui 40 | 41 | ENTRYPOINT [ "/usr/bin/chess-tui" ] -------------------------------------------------------------------------------- /website/blog/2025-12-13-release-2.1.0.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Release 2.1.0 - Add sounds, Lichess + doc + disconnect, Bot auto install script and debug popup 3 | authors: 4 | - thomas-mauran 5 | tags: 6 | - release 7 | --- 8 | 9 | # Release 2.1.0 - Add sounds, Lichess + doc + disconnect, Bot auto install script and debug popup 10 | 11 | **Released:** December 13, 2025 12 | 13 | ## What's New 14 | 15 | 16 | * ci: build for aarch64-macos, aarch64-linux and x64-windows as well by [@jarjk](https://github.com/jarjk) in https://github.com/thomas-mauran/chess-tui/pull/162 17 | * feat: add sounds by [@thomas-mauran](https://github.com/thomas-mauran) in https://github.com/thomas-mauran/chess-tui/pull/161 18 | * feat: lichess + doc + disconnect by [@thomas-mauran](https://github.com/thomas-mauran) in https://github.com/thomas-mauran/chess-tui/pull/165 19 | * feat: bot auto install script and debug popup by [@thomas-mauran](https://github.com/thomas-mauran) in https://github.com/thomas-mauran/chess-tui/pull/166 20 | * docs: blog posts + releases blog posts by [@thomas-mauran](https://github.com/thomas-mauran) in https://github.com/thomas-mauran/chess-tui/pull/167 21 | 22 | 23 | 24 | ## Full Changelog 25 | 26 | For the complete list of changes, see the [full changelog](https://github.com/thomas-mauran/chess-tui/releases/tag/2.1.0). 27 | 28 | --- 29 | 30 | [View on GitHub](https://github.com/thomas-mauran/chess-tui/releases/tag/2.1.0) 31 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "3.6.3", 19 | "@docusaurus/preset-classic": "3.6.3", 20 | "@docusaurus/theme-mermaid": "^3.6.3", 21 | "@mdx-js/react": "^3.0.0", 22 | "clsx": "^2.0.0", 23 | "prism-react-renderer": "^2.3.0", 24 | "react": "^18.0.0", 25 | "react-dom": "^18.0.0" 26 | }, 27 | "devDependencies": { 28 | "@docusaurus/module-type-aliases": "3.6.3", 29 | "@docusaurus/tsconfig": "3.6.3", 30 | "@docusaurus/types": "3.6.3", 31 | "typescript": "~5.6.2" 32 | }, 33 | "resolutions": { 34 | "dompurify": "3.1.6" 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.5%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 3 chrome version", 44 | "last 3 firefox version", 45 | "last 5 safari version" 46 | ] 47 | }, 48 | "engines": { 49 | "node": ">=18.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/docker_push.yml: -------------------------------------------------------------------------------- 1 | name: Build and push docker image 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | image-tags: 7 | required: true 8 | type: string 9 | 10 | env: 11 | REGISTRY: ghcr.io 12 | IMAGE_NAME: ${{ github.repository }} 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | permissions: 18 | packages: write 19 | contents: read 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v4 23 | - name: Setup Docker buildx 24 | uses: docker/setup-buildx-action@v3 25 | - name: Log in to the Container registry 26 | uses: docker/login-action@v3 27 | with: 28 | registry: ghcr.io 29 | username: ${{ github.actor }} 30 | password: ${{ secrets.GITHUB_TOKEN }} 31 | - uses: docker/metadata-action@v5 32 | name: Extract metadata (tags, labels) for Docker 33 | id: meta 34 | with: 35 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 36 | tags: | 37 | type=raw,value=${{ inputs.image-tags }} 38 | flavor: latest=false 39 | - name: Build and push Docker image 40 | uses: docker/build-push-action@v5 41 | with: 42 | push: 'true' 43 | tags: ${{ steps.meta.outputs.tags }} 44 | labels: ${{ steps.meta.outputs.labels }} 45 | cache-from: type=gha 46 | platforms: linux/amd64, linux/arm64, linux/arm/v7 47 | cache-to: type=gha,mode=max 48 | -------------------------------------------------------------------------------- /website/blog/2024-11-26-release-1.5.0.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Release 1.5.0 - Website, Mouse Support, and Major Refactoring 3 | authors: 4 | - thomas-mauran 5 | tags: 6 | - release 7 | date: 2024-11-26 8 | --- 9 | 10 | # Release 1.5.0 11 | 12 | **Released:** November 26, 2024 13 | 14 | This release brings significant improvements including mouse support, website documentation, and important bug fixes. 15 | 16 | 17 | 18 | ## What's New 19 | 20 | ### Mouse Click Support 21 | You can now interact with the chess board using your mouse! Click on pieces to select and move them. 22 | 23 | ### Website Documentation 24 | Created a comprehensive website with documentation, installation guides, and getting started tutorials. 25 | 26 | ### Bug Fixes 27 | - Fixed bot promotion using UCI standard 28 | - Fixed material difference calculation when castling 29 | - Fixed en passant to ensure the captured piece is properly removed 30 | 31 | ### Code Improvements 32 | - Refactored and split code for better maintainability 33 | - Moved tests to dedicated test files 34 | 35 | ## Contributors 36 | 37 | Thank you to all contributors: 38 | - @theonlytruealex (first contribution!) 39 | - @LucaSain (first contribution!) 40 | - @thomas-mauran 41 | 42 | ## Full Changelog 43 | 44 | For the complete list of changes, see the [full changelog](https://github.com/thomas-mauran/chess-tui/compare/1.4.0...1.5.0). 45 | 46 | --- 47 | 48 | [View on GitHub](https://github.com/thomas-mauran/chess-tui/releases/tag/1.5.0) 49 | -------------------------------------------------------------------------------- /src/pieces/king.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::DisplayMode; 2 | use crate::pieces::PieceSize; 3 | use shakmaty::Color; 4 | 5 | pub struct King; 6 | 7 | impl King { 8 | pub fn to_string(display_mode: &DisplayMode, size: PieceSize, color: Option) -> String { 9 | match display_mode { 10 | DisplayMode::DEFAULT | DisplayMode::CUSTOM => match size { 11 | PieceSize::Small => { 12 | // Use standard Unicode chess symbols for 1x1 13 | match color { 14 | Some(Color::White) => "♔".to_string(), 15 | Some(Color::Black) => "♚".to_string(), 16 | None => " ".to_string(), 17 | } 18 | } 19 | PieceSize::Compact => { 20 | // Simple 2-line design for medium-sized cells 21 | "\n ✚\n ███".to_string() 22 | } 23 | PieceSize::Extended => { 24 | // Extended 3-4 line design - more solid and consistent 25 | " ✚\n █▄█\n ███\n ███".to_string() 26 | } 27 | PieceSize::Large => { 28 | // Current multi-line art 29 | "\ 30 | ✚\n\ 31 | ▞▀▄▀▚\n\ 32 | ▙▄█▄▟\n\ 33 | ▐███▌\n\ 34 | ▗█████▖\n\ 35 | " 36 | .to_string() 37 | } 38 | }, 39 | DisplayMode::ASCII => "K".to_string(), 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /website/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type { SidebarsConfig } from "@docusaurus/plugin-content-docs"; 2 | 3 | const sidebars: SidebarsConfig = { 4 | tutorialSidebar: [ 5 | "intro", 6 | { 7 | type: "category", 8 | label: "Installation", 9 | items: ["Installation/Packaging status", "Installation/Binary", "Installation/Debian Ubuntu", "Installation/Cargo", "Installation/Build from source", "Installation/NetBSD", "Installation/Arch Linux", "Installation/NixOS", "Installation/Docker"], 10 | }, 11 | { 12 | type: "category", 13 | label: "Configuration", 14 | items: ["Configuration/configuration-intro", "Configuration/display", "Configuration/skins", "Configuration/logging", "Configuration/bot"], 15 | }, 16 | { 17 | type: "category", 18 | label: "Multiplayer", 19 | items: ["Multiplayer/Local multiplayer", "Multiplayer/Online multiplayer"], 20 | }, 21 | { 22 | type: "category", 23 | label: "Lichess", 24 | items: ["Lichess/features", "Lichess/setup"], 25 | }, 26 | { 27 | type: "category", 28 | label: "Code Architecture", 29 | items: [ 30 | "Code Architecture/Intro", 31 | "Code Architecture/Game", 32 | "Code Architecture/Pieces", 33 | "Code Architecture/App", 34 | "Code Architecture/GameLogic", 35 | "Code Architecture/GameBoard", 36 | "Code Architecture/UI", 37 | "Code Architecture/Bot", 38 | "Code Architecture/Opponent", 39 | ], 40 | }, 41 | ], 42 | }; 43 | 44 | export default sidebars; 45 | -------------------------------------------------------------------------------- /website/blog/2023-12-09-release-1.1.0.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Release 1.1.0 - Home Menu, Credits, and Platform Support 3 | authors: 4 | - thomas-mauran 5 | tags: 6 | - release 7 | date: 2023-12-09 8 | --- 9 | 10 | # Release 1.1.0 11 | 12 | **Released:** December 9, 2023 13 | 14 | This release adds a home menu, credits screen, and expands platform support. 15 | 16 | 17 | 18 | ## What's New 19 | 20 | ### Home Menu 21 | Added a home menu with improved navigation and user experience. 22 | 23 | ### Credits Screen 24 | Added a credits screen to acknowledge contributors and the project. 25 | 26 | ### Platform Support 27 | - Added NetBSD installation instructions 28 | - Added Arch Linux installation directions 29 | 30 | ### Docker Improvements 31 | - Created a much smaller final Docker image for better distribution 32 | 33 | ### Bug Fixes 34 | - Fixed history display on line 8 35 | - Fixed issue where selecting a chess piece with unauthorized moves was not properly prevented 36 | - Added "escape" event to helper menu 37 | 38 | ### Documentation 39 | - Added contributing guidelines 40 | 41 | ## Contributors 42 | 43 | Thank you to all contributors: 44 | - @0323pin (first contribution!) 45 | - @jhauris (first contribution!) 46 | - @charley04310 (first contribution!) 47 | - @Rustmilian (first contribution!) 48 | - @thomas-mauran 49 | 50 | ## Full Changelog 51 | 52 | For the complete list of changes, see the [full changelog](https://github.com/thomas-mauran/chess-tui/compare/1.0.0...1.1.0). 53 | 54 | --- 55 | 56 | [View on GitHub](https://github.com/thomas-mauran/chess-tui/releases/tag/1.1.0) 57 | -------------------------------------------------------------------------------- /website/docs/Installation/Logging.md: -------------------------------------------------------------------------------- 1 | # Logging 2 | 3 | Chess-tui includes a configurable logging system that can help with debugging and understanding the application's behavior. 4 | 5 | ## Configuration 6 | 7 | Logging can be configured in the `CONFIG_HOME/chess-tui/config.toml` file. The log level can be set using the `log_level` option: 8 | 9 | ```toml 10 | log_level = "INFO" # Default is "OFF" 11 | ``` 12 | 13 | CONFIG_HOME is typically: 14 | - Linux: $XDG_CONFIG_HOME or $HOME/.config 15 | - macOS: $HOME/Library/Application Support 16 | - Windows: `%APPDATA%` (Roaming AppData folder) 17 | 18 | ### Available Log Levels 19 | 20 | - `OFF` - Logging disabled (default) 21 | - `ERROR` - Only error messages 22 | - `WARN` - Warning and error messages 23 | - `INFO` - Informational messages, warnings, and errors 24 | - `DEBUG` - Detailed debug information plus all above 25 | - `TRACE` - Most detailed logging level 26 | 27 | ## Log Files 28 | 29 | When logging is enabled, log files are stored in: 30 | ``` 31 | CONFIG_HOME/chess-tui/logs/ 32 | ``` 33 | 34 | Each log file is named with a timestamp: 35 | ``` 36 | chess-tui_YYYY-MM-DD_HH-MM-SS.log 37 | ``` 38 | 39 | For example: `chess-tui_2024-03-20_15-30-45.log` 40 | 41 | ## Usage 42 | 43 | Logs can be helpful when: 44 | - Debugging multiplayer connection issues 45 | - Understanding game state changes 46 | - Investigating unexpected behavior 47 | - Developing new features 48 | 49 | :::tip 50 | For normal gameplay, you can leave logging set to `OFF`. Enable logging only when you need to troubleshoot issues or want to understand the application's behavior in detail. 51 | ::: -------------------------------------------------------------------------------- /website/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Link from '@docusaurus/Link'; 3 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 4 | import Layout from '@theme/Layout'; 5 | import HomepageFeatures from '@site/src/components/HomepageFeatures'; 6 | import Heading from '@theme/Heading'; 7 | 8 | import styles from './index.module.css'; 9 | 10 | function HomepageHeader() { 11 | const {siteConfig} = useDocusaurusContext(); 12 | return ( 13 |
14 |
15 | {/* 16 | {siteConfig.title} 17 | */} 18 | Demo 24 |

{siteConfig.tagline}

25 |
26 | 29 | Play now ♟️ 30 | 31 |
32 |
33 |
34 | ); 35 | } 36 | 37 | 38 | 39 | export default function Home(): JSX.Element { 40 | const {siteConfig} = useDocusaurusContext(); 41 | return ( 42 | 45 | 46 |
47 | 48 |
49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/deploy-website.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | # Review gh actions docs if you want to further define triggers, paths, etc 8 | # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on 9 | 10 | jobs: 11 | build: 12 | name: Build Docusaurus 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: 18 21 | cache: yarn 22 | cache-dependency-path: website/yarn.lock 23 | 24 | - name: Install dependencies 25 | run: yarn install --frozen-lockfile 26 | working-directory: website 27 | 28 | - name: Build website 29 | run: yarn build 30 | working-directory: website 31 | 32 | - name: Upload Build Artifact 33 | uses: actions/upload-pages-artifact@v3 34 | with: 35 | path: website/build 36 | 37 | deploy: 38 | name: Deploy to GitHub Pages 39 | needs: build 40 | 41 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 42 | permissions: 43 | pages: write # to deploy to Pages 44 | id-token: write # to verify the deployment originates from an appropriate source 45 | 46 | # Deploy to the github-pages environment 47 | environment: 48 | name: github-pages 49 | url: ${{ steps.deployment.outputs.page_url }} 50 | 51 | runs-on: ubuntu-latest 52 | steps: 53 | - name: Deploy to GitHub Pages 54 | id: deployment 55 | uses: actions/deploy-pages@v4 56 | -------------------------------------------------------------------------------- /website/docs/Configuration/display.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: display 3 | title: Display Mode 4 | sidebar_position: 2 5 | --- 6 | 7 | import DefaultBoard from '@site/static/img/default-display-mode-board.png'; 8 | import AsciiBoard from '@site/static/img/ascii-display-mode-board.png'; 9 | 10 | # Display Mode 11 | 12 | Chess-tui supports two display modes for rendering the chess pieces: 13 | 14 | ## Default Mode 15 | ```toml 16 | display_mode = "DEFAULT" 17 | ``` 18 | Uses Unicode chess pieces for a richer visual experience. 19 | 20 |
21 | Default display mode 22 |

Default mode with Unicode chess pieces

23 |
24 | 25 | ## ASCII Mode 26 | ```toml 27 | display_mode = "ASCII" 28 | ``` 29 | Uses ASCII characters for better compatibility with terminals that don't support Unicode. 30 | 31 |
32 | ASCII display mode 33 |

ASCII mode for better compatibility

34 |
35 | 36 | You can toggle between display modes in-game using the menu option or by editing the configuration file. 37 | 38 | ## Custom Skins 39 | 40 | When using custom color skins (see [Skins](/docs/Configuration/skins)), the display mode is automatically set to `CUSTOM`. Custom skins allow you to personalize the board colors, piece colors, and UI element colors while still using Unicode chess pieces. 41 | 42 | :::tip 43 | Use ASCII mode if you experience display issues with the default Unicode pieces in your terminal. 44 | ::: -------------------------------------------------------------------------------- /website/docs/Configuration/logging.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: logging 3 | title: Logging 4 | sidebar_position: 3 5 | --- 6 | 7 | # Logging 8 | 9 | Chess-tui includes a configurable logging system that can help with debugging and understanding the application's behavior. 10 | 11 | ## Configuration 12 | 13 | Logging can be configured in the `CONFIG_DIR/chess-tui/config.toml` file. The log level can be set using the `log_level` option: 14 | 15 | ```toml 16 | log_level = "INFO" # Default is "OFF" 17 | ``` 18 | 19 | CONFIG_DIR is typically: 20 | - Linux: $XDG_CONFIG_HOME or $HOME/.config 21 | - macOS: $HOME/Library/Application Support 22 | - Windows: `%APPDATA%` (Roaming AppData folder) 23 | 24 | ### Available Log Levels 25 | 26 | - `OFF` - Logging disabled (default) 27 | - `ERROR` - Only error messages 28 | - `WARN` - Warning and error messages 29 | - `INFO` - Informational messages, warnings, and errors 30 | - `DEBUG` - Detailed debug information plus all above 31 | - `TRACE` - Most detailed logging level 32 | 33 | ## Log Files 34 | 35 | When logging is enabled, log files are stored in: 36 | ``` 37 | CONFIG_DIR/chess-tui/logs/ 38 | ``` 39 | 40 | Each log file is named with a timestamp: 41 | ``` 42 | chess-tui_YYYY-MM-DD_HH-MM-SS.log 43 | ``` 44 | 45 | For example: `chess-tui_2024-03-20_15-30-45.log` 46 | 47 | ## Usage 48 | 49 | Logs can be helpful when: 50 | - Debugging multiplayer connection issues 51 | - Understanding game state changes 52 | - Investigating unexpected behavior 53 | - Developing new features 54 | 55 | :::tip 56 | For normal gameplay, you can leave logging set to `OFF`. Enable logging only when you need to troubleshoot issues or want to understand the application's behavior in detail. 57 | ::: -------------------------------------------------------------------------------- /website/blog/2025-09-03-release-1.6.2.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Release 1.6.2 - FEN Serialization, Ruci Migration, and Bot Improvements 3 | authors: 4 | - thomas-mauran 5 | tags: 6 | - release 7 | date: 2025-09-03 8 | --- 9 | 10 | # Release 1.6.2 11 | 12 | **Released:** September 3, 2025 13 | 14 | This release includes important fixes, migrations, and feature improvements. 15 | 16 | 17 | 18 | ## What's New 19 | 20 | ### FEN Serialization Fix 21 | FEN (Forsyth-Edwards Notation) serialization now follows the official specification correctly. 22 | 23 | ### Engine Migration 24 | Migrated to `ruci` for improved chess engine compatibility and performance. 25 | 26 | ### Handler Refactoring 27 | The handler system has been refactored to organize code by pages, improving maintainability and code organization. 28 | 29 | ### Bot Depth CLI Argument 30 | You can now specify bot depth directly from the command line, giving you more control over bot difficulty. 31 | 32 | ### UI Improvements 33 | - End screen can now be hidden 34 | - Improved styling throughout the application 35 | - Fixed black player selection issue 36 | 37 | ### CI/CD Updates 38 | - Chess TUI binary is now automatically uploaded to releases 39 | 40 | ## Contributors 41 | 42 | Thank you to all contributors: 43 | - @ccapitalK (first contribution!) 44 | - @tigerros 45 | - @RaoniSilvestre (first contribution!) 46 | - @mfernd (first contribution!) 47 | - @thomas-mauran 48 | 49 | ## Full Changelog 50 | 51 | For the complete list of changes, see the [full changelog](https://github.com/thomas-mauran/chess-tui/compare/1.6.1...1.6.2). 52 | 53 | --- 54 | 55 | [View on GitHub](https://github.com/thomas-mauran/chess-tui/releases/tag/1.6.2) 56 | -------------------------------------------------------------------------------- /website/blog/2025-12-11-release-2.0.0.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Release 2.0.0 - Major Update with Shakmaty Integration, Skins, and Lichess Support 3 | authors: 4 | - thomas-mauran 5 | tags: 6 | - release 7 | - major-update 8 | date: 2025-12-11 9 | --- 10 | 11 | # Release 2.0.0 12 | 13 | **Released:** December 11, 2025 14 | 15 | We're excited to announce the release of Chess TUI 2.0.0! This major update brings significant improvements and new features to enhance your chess playing experience. 16 | 17 | 18 | 19 | ## What's New 20 | 21 | ### Shakmaty Integration 22 | The game engine has been upgraded to use the `shakmaty` library, providing better chess logic and move validation. 23 | 24 | ### Skins Feature 25 | Customize the appearance of your chess board with the new skins feature! Choose from different visual styles to make your game experience more personalized. 26 | 27 | ### Move Through History 28 | Navigate through your game history more easily with improved history navigation controls. 29 | 30 | ### Lichess Integration 31 | Connect and play on Lichess directly from Chess TUI! This integration opens up new possibilities for online play and game management. 32 | 33 | ### Bug Fixes 34 | - Fixed an issue where exiting while hosting a game would cause problems 35 | 36 | ## Contributors 37 | 38 | A big thank you to all contributors who made this release possible: 39 | - @tigerros 40 | - @thomas-mauran 41 | - @Andrada42 (first contribution!) 42 | 43 | ## Full Changelog 44 | 45 | For the complete list of changes, see the [full changelog](https://github.com/thomas-mauran/chess-tui/compare/1.6.2...2.0.0). 46 | 47 | --- 48 | 49 | [View on GitHub](https://github.com/thomas-mauran/chess-tui/releases/tag/2.0.0) 50 | -------------------------------------------------------------------------------- /website/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Heading from '@theme/Heading'; 3 | import styles from './styles.module.css'; 4 | 5 | type FeatureItem = { 6 | title: string; 7 | description: JSX.Element; 8 | }; 9 | 10 | const FeatureList: FeatureItem[] = [ 11 | { 12 | title: 'Plug any Chess Engine 🤖', 13 | description: ( 14 | <> 15 | You can play locally against any UCI-compatible chess engine. 16 | 17 | ), 18 | }, 19 | { 20 | title: 'Challenge a Friend 🤼', 21 | description: ( 22 | <> 23 | Chess TUI allows you to play chess with a friend on the same computer. 24 | Play against your friends over the network. 25 | 26 | ), 27 | }, 28 | { 29 | title: 'Lichess Integration 🌐', 30 | description: ( 31 | <> 32 | Play against lichess players directly from your terminal. 33 | 34 | ), 35 | }, 36 | // Additional features can go here 37 | ]; 38 | 39 | function Feature({title, description}: FeatureItem) { 40 | return ( 41 |
42 |
43 |
44 |
45 | {title} 46 |

{description}

47 |
48 |
49 | ); 50 | } 51 | 52 | export default function HomepageFeatures(): JSX.Element { 53 | return ( 54 |
55 |
56 |
57 | {FeatureList.map((props, idx) => ( 58 | 59 | ))} 60 |
61 |
62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "chess-tui" 3 | version = "2.2.0" 4 | authors = ["Thomas Mauran"] 5 | license = "MIT" 6 | edition = "2021" 7 | description = "Play chess from your terminal 🦀" 8 | keywords = ["chess", "tui", "graphics", "game", "board"] 9 | homepage = "https://github.com/thomas-mauran/chess-tui" 10 | repository = "https://github.com/thomas-mauran/chess-tui" 11 | 12 | [dependencies] 13 | clap = { version = "4.4.11", features = ["derive"] } 14 | dirs = "5.0.1" 15 | ratatui = { version = "0.29.0", features = ["serde"] } 16 | ruci = { version = "2.1.0", features = ["engine-sync"] } 17 | toml = "0.5.8" 18 | log = "0.4.25" 19 | simplelog = "0.12.2" 20 | chrono = "0.4.39" 21 | shakmaty = "0.27.3" 22 | serde = { version = "1.0", features = ["derive"] } 23 | serde_json = "1.0" 24 | reqwest = { version = "0.11", features = ["blocking", "json"] } 25 | tokio = { version = "1", features = ["full"] } 26 | dotenv = "0.15" 27 | rodio = "0.18" 28 | 29 | [dev-dependencies] 30 | tempfile = "3.8" 31 | 32 | [features] 33 | chess-tui = [] 34 | default = ["chess-tui"] 35 | 36 | [profile.release] 37 | lto = true 38 | codegen-units = 1 39 | opt-level = "z" 40 | strip = true 41 | 42 | [package.metadata.deb] 43 | # Configuration for cargo-deb (alternative packaging method) 44 | # Install with: cargo install cargo-deb 45 | # Build with: cargo deb 46 | maintainer = "Thomas Mauran " 47 | copyright = "2023-2025, Thomas Mauran" 48 | license-file = ["LICENSE"] 49 | extended-description = """\ 50 | Chess-tui is a simple chess game you can play from your terminal. 51 | It supports: 52 | - Local 2 players mode 53 | - Online multiplayer 54 | - Playing against any UCI compatible chess engine 55 | - Lichess integration 56 | - Custom skins 57 | - And more! 58 | """ 59 | section = "games" 60 | priority = "optional" 61 | depends = "$auto" 62 | -------------------------------------------------------------------------------- /.github/workflows/build_and_test.yml: -------------------------------------------------------------------------------- 1 | name: Cargo Build & Test 2 | 3 | on: 4 | pull_request: 5 | workflow_call: 6 | workflow_dispatch: 7 | 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build_and_test: 14 | name: Rust project - latest 15 | strategy: 16 | matrix: 17 | toolchain: 18 | - stable 19 | - beta 20 | - nightly 21 | os: 22 | - macos-latest 23 | - macos-15-intel 24 | - ubuntu-latest 25 | - ubuntu-24.04-arm 26 | - windows-latest 27 | runs-on: ${{ matrix.os }} 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: Swatinem/rust-cache@v2 31 | - name: Install ALSA development libraries (Linux/Unix only) 32 | if: runner.os == 'Linux' || runner.os == 'Unix' 33 | run: | 34 | if command -v apt-get &> /dev/null; then 35 | sudo apt-get update && sudo apt-get install -y libasound2-dev 36 | elif command -v dnf &> /dev/null; then 37 | sudo dnf install -y alsa-lib-devel 38 | elif command -v yum &> /dev/null; then 39 | sudo yum install -y alsa-lib-devel 40 | elif command -v pacman &> /dev/null; then 41 | sudo pacman -S --noconfirm alsa-lib 42 | elif command -v zypper &> /dev/null; then 43 | sudo zypper install -y alsa-devel 44 | elif command -v pkg_add &> /dev/null; then 45 | sudo pkg_add alsa-lib || pkg_add alsa-lib 46 | else 47 | echo "Unknown package manager, skipping ALSA installation" 48 | fi 49 | - run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }} 50 | continue-on-error: true 51 | - run: cargo check --verbose 52 | continue-on-error: true 53 | - run: cargo test --verbose 54 | continue-on-error: true 55 | -------------------------------------------------------------------------------- /website/blog/2024-11-15-release-1.3.0.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Release 1.3.0 - Config File, History System, and Material Difference 3 | authors: 4 | - thomas-mauran 5 | tags: 6 | - release 7 | date: 2024-11-15 8 | --- 9 | 10 | # Release 1.3.0 11 | 12 | **Released:** November 15, 2024 13 | 14 | This release brings configuration file support, improved history system, and material difference tracking. 15 | 16 | 17 | 18 | ## What's New 19 | 20 | ### Configuration File 21 | Chess TUI now supports a TOML configuration file located at `.config/chess-tui/`, allowing you to customize settings persistently. 22 | 23 | ### Better History System 24 | Improved game history navigation and display for a better user experience. 25 | 26 | ### Material Difference 27 | Track the material difference between players during the game. 28 | 29 | ### Code Quality Improvements 30 | - Fixed various issues found with `cargo clippy -- --warn clippy::pedantic` 31 | - Improved coordinate storage 32 | - Keymap fixes 33 | - Dependency updates 34 | 35 | ### Performance Optimizations 36 | - Enabled Link-Time Optimization (LTO) and other size-related optimizations for smaller binaries 37 | 38 | ### CI/CD 39 | - Added clippy and fmt checks to CI pipeline 40 | 41 | ### Documentation 42 | - Updated Arch Linux installation instructions 43 | 44 | ## Contributors 45 | 46 | Thank you to all contributors: 47 | - @orhun (first contribution!) 48 | - @nicholasmello (first contribution!) 49 | - @damien-mathieu1 (first contribution!) 50 | - @zamazan4ik (first contribution!) 51 | - @infernosalex (first contribution!) 52 | - @thomas-mauran 53 | - @JeromeSchmied 54 | 55 | ## Full Changelog 56 | 57 | For the complete list of changes, see the [full changelog](https://github.com/thomas-mauran/chess-tui/compare/1.2.1...1.3.0). 58 | 59 | --- 60 | 61 | [View on GitHub](https://github.com/thomas-mauran/chess-tui/releases/tag/1.3.0) 62 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Chess-tui contribution guidelines 2 | 3 | Thank you for your interest in improving Chess-tui! We'd love to have your contribution. We expect all contributors to abide by the [Rust code of conduct], which you can find at that link. 4 | 5 | [Rust code of conduct]: https://www.rust-lang.org/policies/code-of-conduct 6 | 7 | ## License 8 | 9 | Chess-tui is MIT licensed project and so are all 10 | contributions. Please see the [`LICENSE-MIT`] files in 11 | this directory for more details. 12 | 13 | [`LICENSE-MIT`]: https://github.com/rust-lang/rust-by-example/blob/master/LICENSE-MIT 14 | 15 | 16 | ## Pull Requests 17 | 18 | To make changes to Chess-tui, please send in pull requests on GitHub to the `main` 19 | branch. We'll review them and either merge or request changes. Travis CI tests 20 | everything as well, so you may get feedback from it too. 21 | 22 | If you make additions or other changes to a pull request, feel free to either amend 23 | previous commits or only add new ones, however you prefer. At the end the commit will be squashed. 24 | 25 | ## Issue Tracker 26 | 27 | You can find the issue tracker [on 28 | GitHub](https://github.com/thomas-mauran/chess-tui/issues). If you've found a 29 | problem with Chess-tui, please open an issue there. 30 | 31 | We use the following labels: 32 | 33 | * `enhancement`: This is for any request for new sections or functionality. 34 | * `bug`: This is for anything that's in Chess-tui, but incorrect or not working. 35 | * `documentation`: This is for anything related to documentation. 36 | * `help wanted`: This is for issues that we'd like to fix, but don't have the time 37 | to do ourselves. If you'd like to work on one of these, please leave a comment 38 | saying so, so we can help you get started. 39 | * `good first issue`: This is for issues that are good for people who are new to the project or open-source community in general. 40 | 41 | ## Development workflow 42 | 43 | To build Chess-tui, [install Rust](https://www.rust-lang.org/tools/install), and then: 44 | 45 | ```bash 46 | $ git clone https://github.com/thomas-mauran/chess-tui 47 | $ cd chess-tui 48 | $ cargo build --release 49 | $ ./target/release/chess-tui 50 | ``` -------------------------------------------------------------------------------- /website/docs/Configuration/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: configuration-intro 3 | title: Configuration 4 | sidebar_position: 1 5 | --- 6 | 7 | # Configuration 8 | 9 | Chess-tui can be configured through the configuration file located at `CONFIG_DIR/chess-tui/config.toml`. Additionally, custom color skins can be configured in `CONFIG_DIR/chess-tui/skins.json`. This section covers all available configuration options. 10 | 11 | CONFIG_DIR is typically: 12 | - Linux: $XDG_CONFIG_HOME or $HOME/.config 13 | - macOS: $HOME/Library/Application Support 14 | - Windows: `%APPDATA%` (Roaming AppData folder) 15 | 16 | ## Command Line Options 17 | 18 | Some configuration options can also be set directly from the command line: 19 | 20 | ```bash 21 | # Set chess engine path 22 | chess-tui -e /path/to/engine 23 | 24 | # Set engine path with command-line arguments (e.g., GNU Chess) 25 | chess-tui -e "/opt/homebrew/bin/gnuchess --uci" 26 | 27 | # Set bot thinking depth 28 | chess-tui --depth 15 29 | 30 | # Combine both options 31 | chess-tui -e /path/to/engine --depth 15 32 | chess-tui -e "/opt/homebrew/bin/gnuchess --uci" --depth 15 33 | 34 | # Stockfish simple example 35 | chess-tui -e /opt/homebrew/opt/stockfish 36 | ``` 37 | 38 | Command line options take precedence over configuration file values. 39 | 40 | ## Configuration File 41 | 42 | The configuration file is automatically created when you first run chess-tui. You can modify it manually to customize your experience: 43 | 44 | ```toml 45 | # CONFIG_DIR/chess-tui/config.toml 46 | 47 | # Display mode: "DEFAULT" or "ASCII" 48 | display_mode = "DEFAULT" 49 | 50 | # Chess engine path (optional) 51 | # Can include command-line arguments for engines that require them 52 | engine_path = "/path/to/your/engine" 53 | # Example with GNU Chess: engine_path = "/opt/homebrew/bin/gnuchess --uci" 54 | 55 | # Logging level: "OFF", "ERROR", "WARN", "INFO", "DEBUG", or "TRACE" 56 | log_level = "OFF" 57 | 58 | # Bot thinking depth for chess engine (1-255, default: 10) 59 | bot_depth = 10 60 | ``` 61 | 62 | CONFIG_DIR is typically: 63 | - Linux: $XDG_CONFIG_HOME or $HOME/.config 64 | - macOS: $HOME/Library/Application Support 65 | - Windows: `%APPDATA%` (Roaming AppData folder) -------------------------------------------------------------------------------- /website/src/components/LatestVersion/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | export function LatestVersion(): JSX.Element { 4 | const [version, setVersion] = useState('latest'); 5 | const [loading, setLoading] = useState(true); 6 | 7 | useEffect(() => { 8 | fetch('https://api.github.com/repos/thomas-mauran/chess-tui/releases/latest') 9 | .then((response) => response.json()) 10 | .then((data) => { 11 | if (data.tag_name) { 12 | // Remove 'v' prefix if present 13 | const versionNumber = data.tag_name.replace(/^v/, ''); 14 | setVersion(versionNumber); 15 | } 16 | setLoading(false); 17 | }) 18 | .catch(() => { 19 | // Fallback to 'latest' if fetch fails 20 | setVersion('latest'); 21 | setLoading(false); 22 | }); 23 | }, []); 24 | 25 | if (loading) { 26 | return latest; 27 | } 28 | 29 | return {version}; 30 | } 31 | 32 | export function DockerCommands(): JSX.Element { 33 | const [version, setVersion] = useState('latest'); 34 | const [loading, setLoading] = useState(true); 35 | 36 | useEffect(() => { 37 | fetch('https://api.github.com/repos/thomas-mauran/chess-tui/releases/latest') 38 | .then((response) => response.json()) 39 | .then((data) => { 40 | if (data.tag_name) { 41 | // Remove 'v' prefix if present 42 | const versionNumber = data.tag_name.replace(/^v/, ''); 43 | setVersion(versionNumber); 44 | } 45 | setLoading(false); 46 | }) 47 | .catch(() => { 48 | // Fallback to 'latest' if fetch fails 49 | setVersion('latest'); 50 | setLoading(false); 51 | }); 52 | }, []); 53 | 54 | const tag = loading ? 'latest' : version; 55 | 56 | return ( 57 | <> 58 |

Pull and run a specific version:

59 |
60 |         {`docker pull ghcr.io/thomas-mauran/chess-tui:${tag}
61 | docker run --rm -it ghcr.io/thomas-mauran/chess-tui:${tag}`}
62 |       
63 |

Or run directly without pulling first:

64 |
65 |         {`docker run --rm -it ghcr.io/thomas-mauran/chess-tui:${tag}`}
66 |       
67 | 68 | ); 69 | } 70 | 71 | -------------------------------------------------------------------------------- /src/constants.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | use std::path::PathBuf; 3 | 4 | use ratatui::style::Color; 5 | 6 | pub const UNDEFINED_POSITION: u8 = u8::MAX; 7 | pub const WHITE: Color = Color::Rgb(160, 160, 160); 8 | pub const BLACK: Color = Color::Rgb(128, 95, 69); 9 | 10 | // Network constants 11 | pub const NETWORK_PORT: u16 = 2308; 12 | pub const NETWORK_BUFFER_SIZE: usize = 5; 13 | pub const SLEEP_DURATION_SHORT_MS: u64 = 50; 14 | pub const SLEEP_DURATION_LONG_MS: u64 = 100; 15 | 16 | pub const TITLE: &str = r" 17 | ██████╗██╗ ██╗███████╗███████╗███████╗ ████████╗██╗ ██╗██╗ 18 | ██╔════╝██║ ██║██╔════╝██╔════╝██╔════╝ ╚══██╔══╝██║ ██║██║ 19 | ██║ ███████║█████╗ ███████╗███████╗█████╗██║ ██║ ██║██║ 20 | ██║ ██╔══██║██╔══╝ ╚════██║╚════██║╚════╝██║ ██║ ██║██║ 21 | ╚██████╗██║ ██║███████╗███████║███████║ ██║ ╚██████╔╝██║ 22 | ╚═════╝╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═════╝ ╚═╝ 23 | "; 24 | 25 | #[derive(Debug, Clone, Copy, PartialEq)] 26 | pub enum DisplayMode { 27 | DEFAULT, 28 | ASCII, 29 | CUSTOM, 30 | } 31 | 32 | impl fmt::Display for DisplayMode { 33 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 34 | match *self { 35 | DisplayMode::ASCII => write!(f, "ASCII"), 36 | DisplayMode::DEFAULT => write!(f, "DEFAULT"), 37 | DisplayMode::CUSTOM => write!(f, "CUSTOM"), 38 | } 39 | } 40 | } 41 | 42 | pub fn config_dir() -> Result { 43 | match dirs::config_dir() { 44 | Some(dir) => Ok(dir), 45 | None => Err("Could not get config directory"), 46 | } 47 | } 48 | 49 | #[derive(Debug, PartialEq, Clone)] 50 | pub enum Pages { 51 | Home, 52 | Solo, 53 | Multiplayer, 54 | Lichess, 55 | LichessMenu, 56 | OngoingGames, 57 | Bot, 58 | Credit, 59 | } 60 | impl Pages { 61 | pub fn variant_count() -> usize { 62 | 7 63 | } 64 | } 65 | 66 | #[derive(Debug, PartialEq, Clone, Copy)] 67 | pub enum Popups { 68 | ColorSelection, 69 | MultiplayerSelection, 70 | EnterHostIP, 71 | WaitingForOpponentToJoin, 72 | EnginePathError, 73 | Help, 74 | EndScreen, 75 | PuzzleEndScreen, 76 | Error, 77 | Success, 78 | SeekingLichessGame, 79 | EnterGameCode, 80 | EnterLichessToken, 81 | ResignConfirmation, 82 | } 83 | -------------------------------------------------------------------------------- /src/game_logic/bot.rs: -------------------------------------------------------------------------------- 1 | use ruci::{Engine, Go}; 2 | use shakmaty::fen::Fen; 3 | use shakmaty::uci::UciMove; 4 | use std::borrow::Cow; 5 | use std::process::Command; 6 | use std::str::FromStr; 7 | 8 | #[derive(Clone)] 9 | pub struct Bot { 10 | pub engine_path: String, 11 | /// Used to indicate if a bot move is following 12 | pub bot_will_move: bool, 13 | // if the bot is starting, meaning the player is black 14 | pub is_bot_starting: bool, 15 | /// Bot thinking depth for chess engine 16 | pub depth: u8, 17 | } 18 | 19 | impl Bot { 20 | pub fn new(engine_path: &str, is_bot_starting: bool, depth: u8) -> Bot { 21 | Self { 22 | engine_path: engine_path.to_string(), 23 | bot_will_move: false, 24 | is_bot_starting, 25 | depth, 26 | } 27 | } 28 | 29 | pub fn get_move(&self, fen: &str) -> UciMove { 30 | // Parse engine_path to support command-line arguments 31 | // Split by spaces, treating first part as command and rest as args 32 | let parts: Vec<&str> = self.engine_path.split_whitespace().collect(); 33 | let (command, args) = if parts.is_empty() { 34 | (self.engine_path.as_str(), &[] as &[&str]) 35 | } else { 36 | (parts[0], &parts[1..]) 37 | }; 38 | 39 | let mut cmd = Command::new(command); 40 | if !args.is_empty() { 41 | cmd.args(args); 42 | } 43 | 44 | let mut process = cmd 45 | .stdin(std::process::Stdio::piped()) 46 | .stdout(std::process::Stdio::piped()) 47 | .spawn() 48 | .expect("Failed to spawn engine process"); 49 | 50 | let mut engine = 51 | Engine::from_process(&mut process, false).expect("Failed to initialize engine"); 52 | 53 | engine 54 | .send(ruci::Position::Fen { 55 | fen: Cow::Owned(Fen::from_str(fen).unwrap()), 56 | moves: Cow::Borrowed(&[]), 57 | }) 58 | .unwrap(); 59 | 60 | engine 61 | .go( 62 | &Go { 63 | depth: Some(self.depth as usize), 64 | ..Default::default() 65 | }, 66 | |_| {}, 67 | ) 68 | .unwrap() 69 | .take_normal() 70 | .unwrap() 71 | .r#move 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /website/docs/intro.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Getting Started 6 | 7 | import Logo from '/img/logo.png'; 8 | 9 |
10 |
11 | logo 12 |
13 |
14 | ### Welcome to the chess-tui documentation! 15 | 16 | Chess-tui is a simple chess game you can play from your terminal. It supports local 2 players mode, online multiplayer, playing against any UCI compatible chess engine, Lichess integration, custom skins and more! 17 | 18 | This documentation will guide you through the different features of the game, how to install it, and the code architecture of the project if you want to contribute. 19 |
20 |
21 | 22 | ## Quick Install 23 | 24 | **Homebrew:** 25 | ```bash 26 | brew install thomas-mauran/tap/chess-tui 27 | ``` 28 | 29 | **Debian/Ubuntu:** 30 | ```bash 31 | DEB_URL=$(curl -s "https://api.github.com/repos/thomas-mauran/chess-tui/releases/latest" | jq -r '.assets[] | select(.name | endswith(".deb")) | .browser_download_url') && curl -LO "$DEB_URL" && sudo dpkg -i "$(basename "$DEB_URL")" && sudo apt-get install -f 32 | ``` 33 | 34 | **Cargo:** 35 | ```bash 36 | cargo install chess-tui 37 | ``` 38 | 39 | For installation via package managers or other methods, see the [Installation Guide](/docs/Installation/Packaging%20status). 40 | 41 | ## Game Controls 42 | 43 | ### Basic Controls 44 | 45 | - **Arrow keys** or **h/j/k/l**: Move the cursor (you can also use the mouse) 46 | - **Space**: Select a piece 47 | - **Esc**: Deselect a piece or hide popups 48 | - **?**: Show help menu with all controls 49 | - **q**: Quit the game 50 | - **b**: Go to the home menu / reset the game 51 | - **s**: Cycle through available skins 52 | - **r**: Restart the current game (solo mode only) 53 | - **p/P**: Navigate to previous position in history (solo mode only, not in puzzle mode) 54 | - **n/N**: Navigate to next position in history (solo mode only, not in puzzle mode) 55 | - **t/T**: Show hint in puzzle mode (select the piece to move) 56 | - **Ctrl +** or **Ctrl -**: Zoom in or out to adjust piece sizes (may differ in certain terminals) 57 | 58 | ### Color Codes 59 | 60 | - **Blue cell**: Your cursor 61 | - **Green cell**: Selected piece 62 | - **Purple cell**: The king is getting checked 63 | - **Grey cell**: Available cells for the selected piece 64 | 65 | Press **?** during gameplay to see all available controls in the help menu. -------------------------------------------------------------------------------- /src/game_logic/coord.rs: -------------------------------------------------------------------------------- 1 | use shakmaty::Square; 2 | 3 | /// A coordinate on the chess board (row, col format) 4 | /// This wraps around shakmaty's Square type for compatibility with existing UI code 5 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 6 | pub struct Coord { 7 | pub row: u8, 8 | pub col: u8, 9 | } 10 | 11 | impl Coord { 12 | pub fn new(row: u8, col: u8) -> Self { 13 | Self { row, col } 14 | } 15 | 16 | pub fn undefined() -> Self { 17 | Self { row: 255, col: 255 } 18 | } 19 | 20 | pub fn is_valid(&self) -> bool { 21 | self.row < 8 && self.col < 8 22 | } 23 | 24 | pub fn reverse(&self) -> Self { 25 | Self { 26 | row: 7 - self.row, 27 | col: 7 - self.col, 28 | } 29 | } 30 | 31 | /// Convert to shakmaty Square 32 | /// Our board representation: row 0 is rank 8, row 7 is rank 1 33 | pub fn to_square(&self) -> Option { 34 | if !self.is_valid() { 35 | return None; 36 | } 37 | // Flip row: our row 0 = rank 8, row 7 = rank 1 38 | let rank = 7 - self.row; 39 | let file = self.col; 40 | Square::try_from(rank * 8 + file).ok() 41 | } 42 | 43 | /// Safe conversion to Square with validation 44 | pub fn try_to_square(&self) -> Option { 45 | self.to_square() 46 | } 47 | 48 | /// Convert from shakmaty Square 49 | pub fn from_square(square: Square) -> Self { 50 | let index = square as u8; 51 | let rank = index / 8; 52 | let file = index % 8; 53 | // Flip row back: rank 7 = our row 0, rank 0 = our row 7 54 | Self { 55 | row: 7 - rank, 56 | col: file, 57 | } 58 | } 59 | } 60 | 61 | impl PartialOrd for Coord { 62 | fn partial_cmp(&self, other: &Self) -> Option { 63 | Some(self.cmp(other)) 64 | } 65 | } 66 | 67 | impl Ord for Coord { 68 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 69 | match self.row.cmp(&other.row) { 70 | std::cmp::Ordering::Equal => self.col.cmp(&other.col), 71 | other => other, 72 | } 73 | } 74 | } 75 | 76 | #[cfg(test)] 77 | mod tests { 78 | use super::*; 79 | 80 | #[test] 81 | fn test_coord_square_conversion() { 82 | // Test e4 (row 4, col 4 in our system = rank 4, file e in chess) 83 | let coord = Coord::new(4, 4); 84 | let square = coord.to_square().unwrap(); 85 | let back = Coord::from_square(square); 86 | assert_eq!(coord, back); 87 | } 88 | 89 | #[test] 90 | fn test_undefined() { 91 | let undef = Coord::undefined(); 92 | assert!(!undef.is_valid()); 93 | assert_eq!(undef.to_square(), None); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/event.rs: -------------------------------------------------------------------------------- 1 | use crate::app::AppResult; 2 | use ratatui::crossterm::event::{self, Event as CrosstermEvent, KeyEvent, MouseEvent}; 3 | use std::sync::mpsc; 4 | use std::thread; 5 | use std::time::{Duration, Instant}; 6 | 7 | /// Terminal events. 8 | #[derive(Clone, Copy, Debug)] 9 | pub enum Event { 10 | /// Terminal tick. 11 | Tick, 12 | /// Key press. 13 | Key(KeyEvent), 14 | /// Mouse click/scroll. 15 | Mouse(MouseEvent), 16 | /// Terminal resize. 17 | Resize(u16, u16), 18 | } 19 | 20 | /// Terminal event handler. 21 | #[allow(dead_code)] 22 | #[derive(Debug)] 23 | pub struct EventHandler { 24 | /// Event sender channel. 25 | sender: mpsc::Sender, 26 | /// Event receiver channel. 27 | receiver: mpsc::Receiver, 28 | /// Event handler thread. 29 | handler: thread::JoinHandle<()>, 30 | } 31 | 32 | impl EventHandler { 33 | /// Constructs a new instance of [`EventHandler`]. 34 | pub fn new(tick_rate: u64) -> Self { 35 | let tick_rate = Duration::from_millis(tick_rate); 36 | let (sender, receiver) = mpsc::channel(); 37 | let handler = { 38 | let sender = sender.clone(); 39 | thread::spawn(move || { 40 | let mut last_tick = Instant::now(); 41 | loop { 42 | let timeout = tick_rate 43 | .checked_sub(last_tick.elapsed()) 44 | .unwrap_or(tick_rate); 45 | 46 | if event::poll(timeout).expect("no events available") { 47 | match event::read().expect("unable to read event") { 48 | CrosstermEvent::Key(e) => sender.send(Event::Key(e)), 49 | CrosstermEvent::Mouse(e) => sender.send(Event::Mouse(e)), 50 | CrosstermEvent::Resize(w, h) => sender.send(Event::Resize(w, h)), 51 | _ => unimplemented!(), 52 | } 53 | .expect("failed to send terminal event"); 54 | } 55 | 56 | if last_tick.elapsed() >= tick_rate { 57 | sender.send(Event::Tick).expect("failed to send tick event"); 58 | last_tick = Instant::now(); 59 | } 60 | } 61 | }) 62 | }; 63 | Self { 64 | sender, 65 | receiver, 66 | handler, 67 | } 68 | } 69 | 70 | /// Receive the next event from the handler thread. 71 | /// 72 | /// This function will always block the current thread if 73 | /// there is no data available and it's possible for more data to be sent. 74 | pub fn next(&self) -> AppResult { 75 | Ok(self.receiver.recv()?) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::game_logic::coord::Coord; 2 | use ratatui::style::Color; 3 | use shakmaty::Square; 4 | 5 | pub fn color_to_ratatui_enum(piece_color: Option) -> Color { 6 | match piece_color { 7 | Some(shakmaty::Color::Black) => Color::Black, 8 | Some(shakmaty::Color::White) => Color::White, 9 | None => Color::Red, 10 | } 11 | } 12 | 13 | pub fn flip_square_if_needed(square: Square, is_flipped: bool) -> Square { 14 | if is_flipped { 15 | Coord::from_square(square) 16 | .reverse() 17 | .to_square() 18 | .unwrap_or(square) 19 | } else { 20 | square 21 | } 22 | } 23 | 24 | pub fn get_square_from_coord(coord: Coord, is_flipped: bool) -> Option { 25 | if is_flipped { 26 | coord.reverse().to_square() 27 | } else { 28 | coord.to_square() 29 | } 30 | } 31 | 32 | pub fn get_coord_from_square(square: Option, is_flipped: bool) -> Coord { 33 | if let Some(s) = square { 34 | if is_flipped { 35 | Coord::from_square(s).reverse() 36 | } else { 37 | Coord::from_square(s) 38 | } 39 | } else { 40 | Coord::undefined() 41 | } 42 | } 43 | 44 | /// Convert a character to an integer for parsing UCI moves 45 | pub fn get_int_from_char(c: Option) -> u8 { 46 | match c { 47 | Some('a') | Some('0') => 0, 48 | Some('b') | Some('1') => 1, 49 | Some('c') | Some('2') => 2, 50 | Some('d') | Some('3') => 3, 51 | Some('e') | Some('4') => 4, 52 | Some('f') | Some('5') => 5, 53 | Some('g') | Some('6') => 6, 54 | Some('h') | Some('7') => 7, 55 | _ => 0, 56 | } 57 | } 58 | 59 | pub fn get_opposite_square(square: Option) -> Option { 60 | square.and_then(|s| Coord::from_square(s).reverse().to_square()) 61 | } 62 | 63 | /// Convert position format ("4644") to UCI notation (e.g., "e4e4") 64 | pub fn convert_position_into_notation(position: &str) -> String { 65 | let chars: Vec = position.chars().collect(); 66 | if chars.len() < 4 { 67 | return String::new(); 68 | } 69 | 70 | let from_row = chars[0].to_digit(10).unwrap_or(0) as u8; 71 | let from_col = chars[1].to_digit(10).unwrap_or(0) as u8; 72 | let to_row = chars[2].to_digit(10).unwrap_or(0) as u8; 73 | let to_col = chars[3].to_digit(10).unwrap_or(0) as u8; 74 | 75 | // Convert from our internal format to chess notation 76 | // Row is inverted: row 0 = rank 8, row 7 = rank 1 77 | let from_rank = 7 - from_row; 78 | let to_rank = 7 - to_row; 79 | 80 | let files = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']; 81 | let ranks = ['1', '2', '3', '4', '5', '6', '7', '8']; 82 | 83 | format!( 84 | "{}{}{}{}", 85 | files[from_col as usize], 86 | ranks[from_rank as usize], 87 | files[to_col as usize], 88 | ranks[to_rank as usize] 89 | ) 90 | } 91 | -------------------------------------------------------------------------------- /src/skin.rs: -------------------------------------------------------------------------------- 1 | use ratatui::style::Color; 2 | use serde::{Deserialize, Serialize}; 3 | use std::fs; 4 | use std::path::Path; 5 | 6 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 7 | pub struct Skin { 8 | pub name: String, 9 | pub board_white_color: Color, 10 | pub board_black_color: Color, 11 | pub piece_white_color: Color, 12 | pub piece_black_color: Color, 13 | pub cursor_color: Color, 14 | pub selection_color: Color, 15 | pub last_move_color: Color, 16 | } 17 | 18 | impl Default for Skin { 19 | fn default() -> Self { 20 | Self { 21 | name: "Default".to_string(), 22 | board_white_color: Color::Rgb(160, 160, 160), 23 | board_black_color: Color::Rgb(128, 95, 69), 24 | piece_white_color: Color::White, 25 | piece_black_color: Color::Black, 26 | cursor_color: Color::LightBlue, 27 | selection_color: Color::LightGreen, 28 | last_move_color: Color::LightGreen, 29 | } 30 | } 31 | } 32 | 33 | #[derive(Debug, Clone, Serialize, Deserialize)] 34 | pub struct SkinCollection { 35 | pub skins: Vec, 36 | } 37 | 38 | impl Skin { 39 | pub fn load_from_file>(path: P) -> Result> { 40 | let content = fs::read_to_string(path)?; 41 | let skin: Skin = serde_json::from_str(&content)?; 42 | Ok(skin) 43 | } 44 | 45 | pub fn load_all_skins>( 46 | path: P, 47 | ) -> Result, Box> { 48 | let content = fs::read_to_string(path)?; 49 | let collection: SkinCollection = serde_json::from_str(&content)?; 50 | Ok(collection.skins) 51 | } 52 | 53 | pub fn get_skin_by_name(skins: &[Skin], name: &str) -> Option { 54 | skins.iter().find(|s| s.name == name).cloned() 55 | } 56 | 57 | /// Creates a special "Default" display mode skin entry 58 | pub fn default_display_mode() -> Self { 59 | Self { 60 | name: "Default".to_string(), 61 | board_white_color: Color::Rgb(160, 160, 160), 62 | board_black_color: Color::Rgb(128, 95, 69), 63 | piece_white_color: Color::White, 64 | piece_black_color: Color::Black, 65 | cursor_color: Color::LightBlue, 66 | selection_color: Color::LightGreen, 67 | last_move_color: Color::LightGreen, 68 | } 69 | } 70 | 71 | /// Creates a special "ASCII" display mode skin entry 72 | pub fn ascii_display_mode() -> Self { 73 | Self { 74 | name: "ASCII".to_string(), 75 | board_white_color: Color::Rgb(160, 160, 160), 76 | board_black_color: Color::Rgb(128, 95, 69), 77 | piece_white_color: Color::White, 78 | piece_black_color: Color::Black, 79 | cursor_color: Color::LightBlue, 80 | selection_color: Color::LightGreen, 81 | last_move_color: Color::LightGreen, 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /website/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .hero__title { 7 | font-size: 3rem; /* Adjust size as needed */ 8 | font-weight: bold; 9 | background-color: #ffdcc6; 10 | background-image: linear-gradient(-45deg, #ffdcc6 0%, #df650e 70%, #df650e 100%); 11 | background-clip: text; 12 | -webkit-background-clip: text; 13 | color: transparent; 14 | text-align: center; 15 | } 16 | 17 | @media screen and (max-width: 996px) { 18 | .hero__title { 19 | font-size: 2rem; /* Smaller on mobile */ 20 | } 21 | } 22 | 23 | @media screen and (max-width: 480px) { 24 | .hero__title { 25 | font-size: 1.5rem; /* Even smaller on very small screens */ 26 | } 27 | } 28 | 29 | .hero__subtitle { 30 | font-size: 1.5rem; /* Adjust size as needed */ 31 | font-weight: bold; 32 | color: #ebedf0; 33 | text-align: center; 34 | } 35 | 36 | @media screen and (max-width: 996px) { 37 | .hero__subtitle { 38 | font-size: 1.2rem; /* Smaller on mobile */ 39 | } 40 | } 41 | 42 | @media screen and (max-width: 480px) { 43 | .hero__subtitle { 44 | font-size: 1rem; /* Even smaller on very small screens */ 45 | padding: 0 1rem; /* Add horizontal padding */ 46 | } 47 | } 48 | 49 | /* Light mode subtitle color */ 50 | html[data-theme='light'] .hero__subtitle { 51 | color: #1a1a1a; 52 | } 53 | 54 | .heroBanner { 55 | padding: 4rem 0; 56 | text-align: center; 57 | position: relative; 58 | overflow: hidden; 59 | } 60 | 61 | /* Ensure hero background is theme-aware */ 62 | html[data-theme='light'] .heroBanner { 63 | background-color: var(--ifm-background-color); 64 | } 65 | 66 | @media screen and (max-width: 996px) { 67 | .heroBanner { 68 | padding: 2rem 1rem; /* Less padding on mobile */ 69 | } 70 | } 71 | 72 | @media screen and (max-width: 480px) { 73 | .heroBanner { 74 | padding: 1.5rem 0.5rem; /* Even less padding on very small screens */ 75 | } 76 | } 77 | 78 | .buttons { 79 | display: flex; 80 | align-items: center; 81 | justify-content: center; 82 | padding: 0 1rem; /* Add horizontal padding on mobile */ 83 | } 84 | 85 | @media screen and (max-width: 480px) { 86 | .buttons { 87 | padding: 0 0.5rem; 88 | } 89 | 90 | .buttons .button { 91 | font-size: 0.9rem; /* Slightly smaller button text on mobile */ 92 | padding: 0.6rem 1.2rem; /* Adjust button padding */ 93 | } 94 | } 95 | 96 | .featureGif { 97 | border-radius: 5px; 98 | max-width: 60%; 99 | height: auto; 100 | display: inline-block; 101 | box-shadow: 0 10px 20px rgba(0, 0, 0, 0.8); 102 | } 103 | 104 | @media screen and (max-width: 996px) { 105 | .featureGif { 106 | max-width: 85%; /* Larger on tablet */ 107 | } 108 | } 109 | 110 | @media screen and (max-width: 480px) { 111 | .featureGif { 112 | max-width: 95%; /* Almost full width on mobile */ 113 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.6); /* Lighter shadow on mobile */ 114 | } 115 | } 116 | 117 | -------------------------------------------------------------------------------- /website/docs/Lichess/features.md: -------------------------------------------------------------------------------- 1 | # Features 2 | 3 | ![Lichess menu](../../static/gif/lichess-menu.gif) 4 | 5 | Once authenticated, you can access the Lichess menu from the main screen. Here are the available features: 6 | 7 | ## User Profile 8 | 9 | Upon entering the Lichess menu, you will see your profile information on the right side of the screen, including: 10 | * **Username** and online status. 11 | * **Ratings** for different time controls (Blitz, Rapid, Classical, Bullet, Puzzle). 12 | * **Game Statistics**: Total games, wins, losses, and draws. 13 | * **Visual Charts**: A bar chart visualizing your win/loss/draw ratio or rating distribution. 14 | 15 | ## Play Online 16 | 17 | ### Seek a Game (Quick Pairing) 18 | Select **Seek Game** to find an opponent for a correspondence game (3 days per move). The application will connect to Lichess and find a match for you. This is perfect for playing at your own pace without time pressure. 19 | 20 | ### Join by ID 21 | If you have a specific game ID (e.g., from a friend or a tournament), you can use **Join by Code** to enter the game ID and join directly. 22 | 23 | ### Ongoing Games 24 | The **My Ongoing Games** option lists all your currently active games on Lichess. Select one to jump right back into the action. This is perfect for correspondence games or reconnecting to a live game. 25 | 26 | ## Puzzles 27 | 28 | Select **Puzzle** to play rated chess puzzles from Lichess. 29 | * **Solve**: Make moves on the board to solve the puzzle. 30 | * **Feedback**: You'll get immediate feedback on whether your move was correct or incorrect. 31 | * **Rating**: Your puzzle rating will update automatically after each puzzle. 32 | 33 | ## Technical Details: Polling System 34 | 35 | Due to limitations in the Lichess API, `chess-tui` uses a polling system to ensure reliable move updates during games. 36 | 37 | ### Why Polling? 38 | 39 | The Lichess streaming API can have random delays ranging from 3 to 60 seconds when delivering move updates. This makes it unreliable for real-time gameplay, especially in faster time controls. To work around this limitation, `chess-tui` implements an intelligent polling system that: 40 | 41 | * **Polls every 3 seconds** to check for new moves and game state updates 42 | * **Skips polling when it's your turn** to avoid unnecessary API calls (since no new moves will arrive) 43 | * **Automatically resumes polling** when you make a move (to catch your opponent's response) 44 | * **Uses the public stream endpoint** to fetch the current game state with each poll 45 | 46 | ### How It Works 47 | 48 | 1. When you join a game, a background polling thread starts automatically 49 | 2. The thread polls the Lichess API every 3 seconds to get the current game state 50 | 3. If it detects a new move (by comparing turn counts or the last move), it immediately updates the board 51 | 4. When it's your turn, polling is paused to save resources 52 | 5. As soon as you make a move, polling resumes to catch your opponent's response 53 | 54 | This ensures you see moves as quickly as possible (within 3 seconds) regardless of stream delays, providing a smooth and responsive gameplay experience. 55 | -------------------------------------------------------------------------------- /scripts/blog-scripts/fetch-and-generate-blog.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Script to fetch the latest GitHub release and generate a blog post 5 | * Usage: node fetch-and-generate-blog.js 6 | */ 7 | 8 | const https = require('https'); 9 | const fs = require('fs'); 10 | const path = require('path'); 11 | 12 | const REPO = 'thomas-mauran/chess-tui'; 13 | const API_URL = `https://api.github.com/repos/${REPO}/releases/latest`; 14 | 15 | // Fetch latest release 16 | https.get(API_URL, { 17 | headers: { 18 | 'User-Agent': 'Node.js' 19 | } 20 | }, (res) => { 21 | let data = ''; 22 | 23 | res.on('data', (chunk) => { 24 | data += chunk; 25 | }); 26 | 27 | res.on('end', () => { 28 | try { 29 | const release = JSON.parse(data); 30 | 31 | // Extract information 32 | const version = release.tag_name.replace(/^v/, ''); 33 | const date = release.published_at.split('T')[0]; 34 | const body = release.body || ''; 35 | 36 | // Extract title with better fallback logic 37 | let title = release.name; 38 | 39 | // If name is empty, whitespace, or just the version number, try to extract from body 40 | if (!title || title.trim() === '' || title.trim() === version || title.trim() === `v${version}`) { 41 | // Try to extract title from release body (look for markdown headers) 42 | // Skip common section headers that aren't good titles 43 | const skipHeaders = ["What's Changed", "What's New", "Contributors", "New Contributors", "Full Changelog"]; 44 | const bodyLines = body.split('\n'); 45 | for (const line of bodyLines) { 46 | const trimmed = line.trim(); 47 | // Look for markdown headers (# Title or ## Title) 48 | if (trimmed.match(/^#+\s+.+$/)) { 49 | const extractedTitle = trimmed.replace(/^#+\s+/, '').trim(); 50 | // Skip if it's a common section header 51 | if (!skipHeaders.includes(extractedTitle)) { 52 | title = extractedTitle; 53 | break; 54 | } 55 | } 56 | } 57 | } 58 | 59 | // Final fallback 60 | if (!title || title.trim() === '' || title.trim() === version || title.trim() === `v${version}`) { 61 | title = `Release ${version}`; 62 | } else { 63 | title = title.trim(); 64 | } 65 | 66 | // Call the generate-blog-post script 67 | const generateScript = path.join(__dirname, 'generate-blog-post.js'); 68 | const { execSync } = require('child_process'); 69 | 70 | // Properly escape the body for shell execution 71 | const escapedBody = body.replace(/'/g, "'\"'\"'"); 72 | 73 | const command = `node "${generateScript}" "${version}" "${date}" "${title}" '${escapedBody}'`; 74 | 75 | console.log(`Generating blog post for release ${version}...`); 76 | execSync(command, { stdio: 'inherit' }); 77 | 78 | } catch (error) { 79 | console.error('Error processing release:', error.message); 80 | process.exit(1); 81 | } 82 | }); 83 | }).on('error', (error) => { 84 | console.error('Error fetching release:', error.message); 85 | process.exit(1); 86 | }); 87 | -------------------------------------------------------------------------------- /src/pieces/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bishop; 2 | pub mod king; 3 | pub mod knight; 4 | pub mod pawn; 5 | pub mod queen; 6 | pub mod rook; 7 | 8 | use crate::constants::DisplayMode; 9 | use shakmaty::{Color, Role}; 10 | 11 | /// Represents the available space for rendering a piece 12 | #[derive(Debug, Clone, Copy, PartialEq)] 13 | pub enum PieceSize { 14 | /// Single character (1x1) - use standard Unicode chess symbols 15 | Small, 16 | /// Simple 2-line design - reliable fallback for medium-sized cells 17 | Compact, 18 | /// Extended 3-4 line design - intermediate between compact and large 19 | Extended, 20 | /// Large multi-line art (current default) 21 | Large, 22 | } 23 | 24 | impl PieceSize { 25 | /// Determine the appropriate piece size based on cell dimensions 26 | pub fn from_dimensions(height: u16) -> Self { 27 | // If height is less than 3, use small (1x1) 28 | if height < 3 { 29 | return PieceSize::Small; 30 | } 31 | // If height is less than 4, use compact (simple 2-line) 32 | if height < 4 { 33 | return PieceSize::Compact; 34 | } 35 | // If height is less than 5, use extended (3-4 lines) 36 | if height < 5 { 37 | return PieceSize::Extended; 38 | } 39 | // If height is less than 7, use large multi-line art 40 | if height < 7 { 41 | return PieceSize::Large; 42 | } 43 | // Otherwise use large multi-line art 44 | PieceSize::Large 45 | } 46 | } 47 | 48 | /// Convert piece type to UTF-8 character 49 | pub fn role_to_utf_enum(role: &Role, color: Option) -> &'static str { 50 | match color { 51 | Some(Color::White) => match role { 52 | Role::King => "♔", 53 | Role::Queen => "♕", 54 | Role::Rook => "♖", 55 | Role::Bishop => "♗", 56 | Role::Knight => "♘", 57 | Role::Pawn => "♙", 58 | }, 59 | Some(Color::Black) => match role { 60 | Role::King => "♚", 61 | Role::Queen => "♛", 62 | Role::Rook => "♜", 63 | Role::Bishop => "♝", 64 | Role::Knight => "♞", 65 | Role::Pawn => "♟", 66 | }, 67 | None => " ", 68 | } 69 | } 70 | 71 | /// Convert piece type to string based on display mode 72 | /// Note: This is used for the board grid. For multi-line designs, see individual piece modules. 73 | pub fn role_to_string_enum(role: Option, display_mode: &DisplayMode) -> &'static str { 74 | match display_mode { 75 | DisplayMode::DEFAULT | DisplayMode::CUSTOM => match role { 76 | // Custom single-character designs using Unicode box drawing and symbols 77 | Some(Role::King) => "♚", // King with cross 78 | Some(Role::Queen) => "♛", // Queen with multiple points 79 | Some(Role::Rook) => "♜", // Rook/Castle tower 80 | Some(Role::Bishop) => "♝", // Bishop with pointed top 81 | Some(Role::Knight) => "♞", // Knight/Horse head 82 | Some(Role::Pawn) => "♟", // Simple pawn 83 | None => " ", 84 | }, 85 | 86 | DisplayMode::ASCII => match role { 87 | Some(Role::King) => "K", 88 | Some(Role::Queen) => "Q", 89 | Some(Role::Rook) => "R", 90 | Some(Role::Bishop) => "B", 91 | Some(Role::Knight) => "N", 92 | Some(Role::Pawn) => "P", 93 | None => " ", 94 | }, 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/ui/prompt.rs: -------------------------------------------------------------------------------- 1 | /// App holds the state of the application 2 | 3 | #[derive(Clone, Default)] 4 | pub struct Prompt { 5 | /// Current value of the input box 6 | pub input: String, 7 | /// Position of cursor in the editor area. 8 | pub character_index: usize, 9 | /// The prompt entry message 10 | pub message: String, 11 | } 12 | 13 | impl Prompt { 14 | pub fn new() -> Self { 15 | Self { 16 | input: "".to_string(), 17 | character_index: 0, 18 | message: String::new(), 19 | } 20 | } 21 | 22 | pub fn move_cursor_left(&mut self) { 23 | let cursor_moved_left = self.character_index.saturating_sub(1); 24 | self.character_index = self.clamp_cursor(cursor_moved_left); 25 | } 26 | 27 | pub fn move_cursor_right(&mut self) { 28 | let cursor_moved_right = self.character_index.saturating_add(1); 29 | self.character_index = self.clamp_cursor(cursor_moved_right); 30 | } 31 | 32 | pub fn clamp_cursor(&self, new_cursor_pos: usize) -> usize { 33 | new_cursor_pos.clamp(0, self.input.chars().count()) 34 | } 35 | 36 | pub fn reset_cursor(&mut self) { 37 | self.character_index = 0; 38 | } 39 | 40 | pub fn reset(&mut self) { 41 | self.input.clear(); 42 | self.message.clear(); 43 | self.reset_cursor(); 44 | } 45 | 46 | pub fn submit_message(&mut self) { 47 | self.message = self.input.clone(); 48 | self.input.clear(); 49 | self.reset_cursor(); 50 | } 51 | 52 | pub fn enter_char(&mut self, new_char: char) { 53 | let index = self.byte_index(); 54 | // Increased limit to 200 to accommodate long API tokens (Lichess tokens can be 100+ chars) 55 | if index < 200 { 56 | self.input.insert(index, new_char); 57 | self.move_cursor_right(); 58 | } 59 | } 60 | 61 | /// Returns the byte index based on the character position. 62 | /// 63 | /// Since each character in a string can be contain multiple bytes, it's necessary to calculate 64 | /// the byte index based on the index of the character. 65 | pub fn byte_index(&self) -> usize { 66 | self.input 67 | .char_indices() 68 | .map(|(i, _)| i) 69 | .nth(self.character_index) 70 | .unwrap_or(self.input.len()) 71 | } 72 | 73 | pub fn delete_char(&mut self) { 74 | let is_not_cursor_leftmost = self.character_index != 0; 75 | if is_not_cursor_leftmost { 76 | // Method "remove" is not used on the saved text for deleting the selected char. 77 | // Reason: Using remove on String works on bytes instead of the chars. 78 | // Using remove would require special care because of char boundaries. 79 | 80 | let current_index = self.character_index; 81 | let from_left_to_current_index = current_index - 1; 82 | 83 | // Getting all characters before the selected character. 84 | let before_char_to_delete = self.input.chars().take(from_left_to_current_index); 85 | // Getting all characters after selected character. 86 | let after_char_to_delete = self.input.chars().skip(current_index); 87 | 88 | // Put all characters together except the selected one. 89 | // By leaving the selected one out, it is forgotten and therefore deleted. 90 | self.input = before_char_to_delete.chain(after_char_to_delete).collect(); 91 | self.move_cursor_left(); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /website/docs/Multiplayer/Online multiplayer.md: -------------------------------------------------------------------------------- 1 | # Online Multiplayer 2 | 3 | You can now play chess with your friends online. The online multiplayer feature is available in the `Multiplayer` menu option. 4 | 5 | ![multiplayer gif demo](../../static/gif/multiplayer.gif) 6 | 7 | 8 | ## LAN 9 | 10 | If you are on the same network as your friend you don't have anything to worry about. One of the player need to choose `Host` and the other player need to choose `Join`. The player who is hosting the game will get it's ip displayed on the screen. The other player need to enter the `ip`:2308 and click on `Join`. 11 | 12 | By default the game will be hosted on port 2308, make sure you had :2308 at the end of the ip address. 13 | 14 | ## WLAN 15 | 16 | If you are not on the same network as your friend you need to do some port forwarding, but don't worry tools allows you to do that in one command ! 17 | 18 | For this we will use [Bore](https://github.com/ekzhang/bore) which is an open source rust written tool that allows you to expose your local server to the internet. 19 | 20 | First you need to install bore, you can do that by running the following command: 21 | 22 | ```bash 23 | cargo install bore 24 | ``` 25 | 26 | Then you need to create a tcp tunnel to your local server, you can do that by running the following command: 27 | 28 | ```bash 29 | bore local 2308 --to bore.pub 30 | ``` 31 | 32 | this will create a tunnel to your local server on port 2308 to bore.pub, once done you will see the following message: 33 | ![Bore port](bore-port.png) 34 | 35 | This means that you can access the game on bore.pub:12455 (the port will obviously be different). 36 | 37 | The other player then only need to enter bore.pub:port_given to join the game. 38 | 39 | Here for example it would be `bore.pub:12455` 40 | 41 | ### How does it work ? 42 | 43 | When you host a game a new thread will be created running a game_server instance that will listen on the port 2308. This Game Server will handle 2 clients at max and will simply forward the messages between the 2 clients. In the meantime the main thread creates a new Player instance which represent a connection to the game server. 44 | 45 | If you are joining a game you are not creating a game server but simply creating a Player instance that will connect to the game server address. 46 | 47 | ```mermaid 48 | graph TD 49 | A[Start] -->|Host Game| B[Main Thread Creates Game Server] 50 | B --> C[Game Server Listens on Port 2308] 51 | B --> F[Main Thread Creates Player Instance] 52 | F --> G[Player Instance Connects to Game Server] 53 | A -->|Join Game| H[Create Player Instance] 54 | H --> I[Player Connects to Game Server Address] 55 | G --> C 56 | I --> C 57 | ``` 58 | 59 | ### Message exchange 60 | 61 | The message exchange between the clients and the server is done using a simple protocol with the following terms: 62 | 63 | - `b` : Player will play with black pieces 64 | - `w` : Player will play with white pieces 65 | - `s` : The game has started 66 | - `ended` : The game has ended 67 | - `e4e5` : A move from e4 to e5 68 | - `e6e7q` : A move from e6 to e7 with a promotion to queen 69 | 70 | When we are hosting we choose a color and then wait for the `s` message to be sent to start the game. When we are joining we wait for the color `b` or `w` message then for the `s` message to start the game. 71 | 72 | When the game is started the server will send the `s` message to both clients and the game will start. The clients will then send the moves to the server and the server will forward the moves to the other client. 73 | 74 | When the game ends the server will send the `ended` message to both clients and the game will be over. 75 | -------------------------------------------------------------------------------- /website/docs/Code Architecture/Bot.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: Bot 3 | title: Bot 4 | sidebar_position: 7 5 | --- 6 | 7 | ## Bot 8 | 9 | The `Bot` struct interfaces with UCI-compatible chess engines using the **ruci** library. It manages the engine process and converts between FEN positions and UCI moves. 10 | 11 | ## Responsibilities 12 | 13 | The `Bot` manages: 14 | - **Engine process** - Spawning and managing the chess engine subprocess 15 | - **UCI protocol communication** - Sending positions and receiving moves 16 | - **Move computation** - Configurable thinking depth 17 | - **FEN conversion** - Converting board positions to FEN strings 18 | 19 | ## Key Fields 20 | 21 | ```rust 22 | pub struct Bot { 23 | pub engine_path: String, // Path to chess engine executable 24 | pub bot_will_move: bool, // Flag to trigger bot move 25 | pub is_bot_starting: bool, // Whether bot plays first 26 | pub depth: u8, // Thinking depth (1-255) 27 | } 28 | ``` 29 | 30 | ## Key Methods 31 | 32 | ### Initialization 33 | 34 | - **`new(engine_path: &str, is_bot_starting: bool, depth: u8) -> Bot`** 35 | - Creates a new Bot instance with configuration 36 | - Sets thinking depth and engine path 37 | - Does not spawn process until `get_move()` is called 38 | 39 | ### Move Computation 40 | 41 | - **`get_move(fen: &str) -> UciMove`** 42 | - Spawns a new engine process for each move 43 | - Sends FEN position to engine 44 | - Requests best move with configured depth 45 | - Returns UCI move (e.g., "e2e4") 46 | - Blocks until engine responds 47 | - Process is terminated after move is received 48 | 49 | ## UCI Protocol 50 | 51 | The bot communicates with engines using the Universal Chess Interface (UCI) protocol: 52 | 53 | 1. **Position command**: Sends current board state in FEN format 54 | 2. **Go command**: Requests engine to calculate best move 55 | 3. **Best move response**: Engine returns move in UCI notation 56 | 57 | The **ruci** library handles the protocol details, allowing the bot to focus on: 58 | - Converting game positions to FEN 59 | - Converting UCI moves to shakmaty Moves 60 | - Managing engine lifecycle 61 | 62 | ## Async Architecture 63 | 64 | Bot moves are computed asynchronously to keep the UI responsive: 65 | 66 | 1. **`App::start_bot_thinking()`** spawns a background thread 67 | 2. Thread creates a new `Bot` instance 68 | 3. Bot computes move in the background 69 | 4. Move is sent through a channel (`bot_move_receiver`) 70 | 5. **`App::check_bot_move()`** applies the move when ready 71 | 72 | This prevents the UI from freezing during engine computation, which can take several seconds for deep analysis. 73 | 74 | ## Engine Requirements 75 | 76 | The bot requires a UCI-compatible chess engine, such as: 77 | - **Stockfish** - Popular open-source engine 78 | - **Leela Chess Zero** - Neural network engine 79 | - Any engine supporting UCI protocol 80 | 81 | The engine path is configured via: 82 | - Command line argument: `--engine-path` 83 | - Configuration file: `CONFIG_DIR/chess-tui/config.toml` 84 | 85 | CONFIG_DIR is typically: 86 | - Linux: $XDG_CONFIG_HOME or $HOME/.config 87 | - macOS: $HOME/Library/Application Support 88 | - Windows: `%APPDATA%` (Roaming AppData folder) 89 | 90 | ## Configuration 91 | 92 | - **Depth**: Controls how many moves ahead the engine analyzes (default: 10) 93 | - Higher depth = stronger play but slower 94 | - Lower depth = faster but weaker play 95 | - **Engine path**: Path to the chess engine executable 96 | 97 | ## Implementation Details 98 | 99 | The bot creates a new engine process for each move computation. This ensures: 100 | - Clean state for each move calculation 101 | - No process lifecycle management complexity 102 | - Proper resource cleanup after each move 103 | 104 | The engine process is spawned, used to compute a move, and then terminated. This approach is simpler than maintaining a persistent engine connection. 105 | 106 | -------------------------------------------------------------------------------- /website/docusaurus.config.ts: -------------------------------------------------------------------------------- 1 | import { themes as prismThemes } from 'prism-react-renderer'; 2 | import type { Config } from '@docusaurus/types'; 3 | import type * as Preset from '@docusaurus/preset-classic'; 4 | 5 | // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) 6 | 7 | const config: Config = { 8 | title: 'Chess TUI', 9 | tagline: 'Play chess from your terminal 🦀', 10 | favicon: 'img/logo.png', 11 | 12 | // Set the production url of your site here 13 | url: 'https://thomas-mauran.github.io', 14 | baseUrl: '/chess-tui/', 15 | 16 | organizationName: 'thomas-mauran', 17 | projectName: 'chess-tui', 18 | deploymentBranch: 'gh-pages', 19 | trailingSlash: false, 20 | 21 | onBrokenLinks: 'throw', 22 | onBrokenMarkdownLinks: 'warn', 23 | 24 | i18n: { 25 | defaultLocale: 'en', 26 | locales: ['en'], 27 | }, 28 | 29 | presets: [ 30 | [ 31 | 'classic', 32 | { 33 | docs: { 34 | sidebarPath: './sidebars.ts', 35 | editUrl: 36 | 'https://github.com/thomas-mauran/chess-tui/', 37 | sidebarCollapsible: true, 38 | }, 39 | blog: { 40 | showReadingTime: true, 41 | blogSidebarCount: 'ALL', 42 | blogSidebarTitle: 'Recent posts', 43 | feedOptions: { 44 | type: ['rss', 'atom'], 45 | xslt: true, 46 | }, 47 | editUrl: 48 | 'https://github.com/thomas-mauran/chess-tui/', 49 | onInlineTags: 'warn', 50 | onInlineAuthors: 'warn', 51 | onUntruncatedBlogPosts: 'warn', 52 | }, 53 | theme: { 54 | customCss: './src/css/custom.css', 55 | }, 56 | } satisfies Preset.Options, 57 | ], 58 | ], 59 | 60 | themeConfig: { 61 | image: 'img/social-card.png', 62 | prism: { 63 | theme: prismThemes.github, 64 | darkTheme: prismThemes.dracula, 65 | additionalLanguages: ['bash', 'toml'], 66 | }, 67 | navbar: { 68 | title: 'Chess-tui', 69 | logo: { 70 | alt: 'Chess tui logo', 71 | src: 'img/logo.png', 72 | }, 73 | items: [ 74 | { 75 | type: 'docSidebar', 76 | sidebarId: 'tutorialSidebar', 77 | position: 'left', 78 | label: 'Documentation', 79 | }, 80 | { to: '/blog', label: 'Blog', position: 'left' }, 81 | { 82 | href: 'https://github.com/thomas-mauran/chess-tui', 83 | position: 'right', 84 | className: 'header-github-link', 85 | 'aria-label': 'GitHub repository', 86 | html: `GitHub Stars`, 87 | }, 88 | ], 89 | }, 90 | footer: { 91 | style: 'dark', 92 | links: [ 93 | { 94 | title: 'Docs', 95 | items: [ 96 | { 97 | label: 'Tutorial', 98 | to: '/docs/intro', 99 | }, 100 | ], 101 | }, 102 | { 103 | title: 'Community', 104 | items: [ 105 | { 106 | label: 'Github Discussions', 107 | href: 'https://github.com/thomas-mauran/chess-tui/discussions', 108 | }, 109 | ], 110 | }, 111 | { 112 | title: 'More', 113 | items: [ 114 | { 115 | label: 'Blog', 116 | to: '/blog', 117 | }, 118 | { 119 | label: 'GitHub', 120 | href: 'https://github.com/thomas-mauran/chess-tui/', 121 | }, 122 | ], 123 | }, 124 | ], 125 | copyright: `Copyright © ${new Date().getFullYear()} Thomas Mauran, Inc. Built with Docusaurus.`, 126 | }, 127 | } satisfies Preset.ThemeConfig, 128 | 129 | markdown: { 130 | mermaid: true, 131 | }, 132 | themes: ['@docusaurus/theme-mermaid'], 133 | }; 134 | 135 | export default config; 136 | -------------------------------------------------------------------------------- /website/docs/Code Architecture/App.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: App 3 | title: App 4 | sidebar_position: 3 5 | --- 6 | 7 | ## App 8 | 9 | The `App` struct is the main application container that orchestrates the entire chess-tui application. 10 | 11 | ## Responsibilities 12 | 13 | The `App` struct manages: 14 | - **Application lifecycle and state** - running flag, pages, popups 15 | - **Game configuration** - chess engine path, bot depth, display mode 16 | - **Bot move computation** - background threads for async bot thinking 17 | - **Multiplayer server setup** - opponent connections and game server 18 | - **Menu navigation** - user input handling and page transitions 19 | - **Configuration file management** - `CONFIG_DIR/chess-tui/config.toml` 20 | 21 | CONFIG_DIR is typically: 22 | - Linux: $XDG_CONFIG_HOME or $HOME/.config 23 | - macOS: $HOME/Library/Application Support 24 | - Windows: `%APPDATA%` (Roaming AppData folder) 25 | 26 | ## Key Fields 27 | 28 | ```rust 29 | pub struct App { 30 | pub running: bool, // Application running state 31 | pub game: Game, // The chess game instance 32 | pub current_page: Pages, // Current page (Home, Solo, Multiplayer, Bot, Credit) 33 | pub current_popup: Option, // Active popup (Help, ColorSelection, etc.) 34 | pub selected_color: Option, // Selected color when playing against bot 35 | pub hosting: Option, // Whether hosting a multiplayer game 36 | pub host_ip: Option, // Host IP address for multiplayer 37 | pub menu_cursor: u8, // Menu navigation cursor 38 | pub chess_engine_path: Option, // Path to UCI chess engine 39 | pub log_level: LevelFilter, // Logging level 40 | pub bot_depth: u8, // Bot thinking depth (1-255) 41 | pub bot_move_receiver: Option>, // Channel receiver for bot moves 42 | pub error_message: Option, // Error message for Error popup 43 | } 44 | ``` 45 | 46 | ## Key Methods 47 | 48 | ### Bot Management 49 | 50 | - **`start_bot_thinking()`** - Spawns a background thread to compute bot moves asynchronously 51 | - **`check_bot_move()`** - Checks if bot move is ready and applies it to the game 52 | - **`is_bot_thinking()`** - Returns whether bot is currently computing a move 53 | - **`apply_bot_move(Move)`** - Applies a computed bot move to the game board 54 | 55 | ### Game Setup 56 | 57 | - **`bot_setup()`** - Initializes bot with engine path and depth 58 | - **`create_opponent()`** - Sets up multiplayer opponent connection 59 | - **`setup_game_server(host_color: Color)`** - Starts game server for hosting 60 | - **`hosting_selection()`** - Handles host/client selection in multiplayer 61 | 62 | ### Navigation 63 | 64 | - **`go_to_home()`** - Returns to home page and restarts game 65 | - **`menu_select()`** - Handles menu item selection 66 | - **`menu_cursor_up/down/left/right(l: u8)`** - Navigates menu cursor 67 | 68 | ### Configuration 69 | 70 | - **`update_config()`** - Writes current settings to config file 71 | - **`get_host_ip()`** - Retrieves local IP address for multiplayer hosting 72 | 73 | ### Game Control 74 | 75 | - **`restart()`** - Resets game while preserving bot/opponent setup 76 | - **`reset()`** - Complete reset of all game state 77 | - **`quit()`** - Exits the application 78 | 79 | ## Bot Async Architecture 80 | 81 | Bot moves are computed asynchronously to keep the UI responsive: 82 | 83 | 1. **`start_bot_thinking()`** spawns a thread that: 84 | - Creates a new `Bot` instance in the thread 85 | - Gets the current FEN position 86 | - Computes the best move using the UCI engine 87 | - Sends the move through a channel (`bot_move_receiver`) 88 | 89 | 2. **`check_bot_move()`** is called each frame to: 90 | - Check if a move is ready in the channel 91 | - Apply the move if available 92 | - Clear the receiver 93 | 94 | 3. **`is_bot_thinking()`** prevents starting multiple bot threads simultaneously 95 | 96 | This architecture ensures the UI remains responsive even when the chess engine takes time to compute moves. 97 | 98 | -------------------------------------------------------------------------------- /.github/workflows/release_binary.yml: -------------------------------------------------------------------------------- 1 | name: Upload binary to published release 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | 7 | jobs: 8 | build-and-upload-binary: 9 | name: Build and Upload Binary to release 10 | strategy: 11 | matrix: 12 | include: 13 | - { os: "macos-latest", target: "aarch64-apple-darwin" } 14 | - { os: "macos-15-intel", target: "x86_64-apple-darwin" } 15 | - { os: "ubuntu-latest", target: "x86_64-unknown-linux-gnu" } 16 | - { os: "ubuntu-24.04-arm", target: "aarch64-unknown-linux-gnu" } 17 | - { os: "windows-latest", target: "x86_64-pc-windows-msvc", ext: ".exe" } 18 | runs-on: ${{ matrix.os }} 19 | permissions: 20 | contents: write 21 | steps: 22 | - uses: actions/checkout@v4 23 | name: Checkout project 24 | 25 | - uses: dtolnay/rust-toolchain@stable 26 | name: Install the Rust toolchain 27 | # note: would require setting `targets` if `matrix.target` wasn't host-default 28 | 29 | - name: Install ALSA development libraries (Linux/Unix only) 30 | if: runner.os == 'Linux' || runner.os == 'Unix' 31 | run: | 32 | if command -v apt-get &> /dev/null; then 33 | sudo apt-get update && sudo apt-get install -y libasound2-dev 34 | elif command -v dnf &> /dev/null; then 35 | sudo dnf install -y alsa-lib-devel 36 | elif command -v yum &> /dev/null; then 37 | sudo yum install -y alsa-lib-devel 38 | elif command -v pacman &> /dev/null; then 39 | sudo pacman -S --noconfirm alsa-lib 40 | elif command -v zypper &> /dev/null; then 41 | sudo zypper install -y alsa-devel 42 | elif command -v pkg_add &> /dev/null; then 43 | sudo pkg_add alsa-lib || pkg_add alsa-lib 44 | else 45 | echo "Unknown package manager, skipping ALSA installation" 46 | fi 47 | 48 | - uses: Swatinem/rust-cache@v2 49 | name: Use cached dependencies and artifacts 50 | 51 | - run: cargo build --release --target ${{ matrix.target }} 52 | name: Build release binary 53 | 54 | - run: | 55 | TAG=${{ github.event.release.tag_name }} 56 | TAR_NAME="chess-tui-$TAG-${{ matrix.target }}.tar.gz" 57 | echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV 58 | 59 | EXE_NAME="chess-tui${{ matrix.ext }}" 60 | cp "target/${{ matrix.target }}/release/$EXE_NAME" . 61 | tar -czvf "$TAR_NAME" ./chess-tui 62 | shell: bash # needed for windows 63 | name: Set TAR_NAME from tag and platform, create tar file containing the binary 64 | 65 | - uses: softprops/action-gh-release@v2 66 | name: Upload binary to release 67 | if: github.ref_type == 'tag' 68 | with: 69 | files: ${{ env.TAR_NAME }} 70 | env: 71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 72 | 73 | build-and-upload-deb: 74 | name: Build and Upload Debian package 75 | runs-on: ubuntu-latest 76 | needs: build-and-upload-binary 77 | permissions: 78 | contents: write 79 | steps: 80 | - uses: actions/checkout@v4 81 | name: Checkout project 82 | 83 | - uses: dtolnay/rust-toolchain@stable 84 | name: Install the Rust toolchain 85 | 86 | - name: Install ALSA development libraries 87 | run: sudo apt-get update && sudo apt-get install -y libasound2-dev 88 | 89 | - name: Install cargo-deb 90 | run: cargo install cargo-deb 91 | 92 | - uses: Swatinem/rust-cache@v2 93 | name: Use cached dependencies and artifacts 94 | 95 | - name: Build Debian package 96 | run: cargo deb 97 | 98 | - name: Find generated .deb 99 | id: find_deb 100 | run: | 101 | DEB_PATH=$(find target/debian -name "*.deb" | head -n 1) 102 | echo "deb_path=$DEB_PATH" >> "$GITHUB_OUTPUT" 103 | 104 | - uses: softprops/action-gh-release@v2 105 | name: Upload .deb to release 106 | if: github.ref_type == 'tag' 107 | with: 108 | files: ${{ steps.find_deb.outputs.deb_path }} 109 | env: 110 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

chess-tui

3 | Play chess from your terminal 🦀 4 | 5 | ![board](./examples/ratatui.gif) 6 | 7 |
8 | 9 | ![Stars](https://img.shields.io/github/stars/thomas-mauran/chess-tui?logo=github) ![Downloads](https://img.shields.io/crates/d/chess-tui?logo=rust) ![GitHub CI](https://github.com/thomas-mauran/chess-tui/actions/workflows/flow_test_build_push.yml/badge.svg) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![GitHub release](https://img.shields.io/github/v/release/thomas-mauran/chess-tui?color=success)](https://github.com/thomas-mauran/chess-tui/releases/latest) 10 |
11 |
12 | 13 | ### Description 14 | 15 | Chess-tui is a simple chess game you can play from your terminal. It supports local 2 players mode, online multiplayer, playing against any UCI compatible chess engine, Lichess integration, custom skins and more! 16 | 17 | ### Quick Install 18 | 19 | **Homebrew:** 20 | ```bash 21 | brew install thomas-mauran/tap/chess-tui 22 | chess-tui 23 | ``` 24 | 25 | **Debian/Ubuntu:** 26 | ```bash 27 | DEB_URL=$(curl -s "https://api.github.com/repos/thomas-mauran/chess-tui/releases/latest" | jq -r '.assets[] | select(.name | endswith(".deb")) | .browser_download_url') && curl -LO "$DEB_URL" && sudo dpkg -i "$(basename "$DEB_URL")" && sudo apt-get install -f 28 | chess-tui 29 | ``` 30 | 31 | **Cargo:** 32 | ```bash 33 | cargo install chess-tui 34 | chess-tui 35 | ``` 36 | 37 | **Available on:** [![Packaging status](https://repology.org/badge/vertical-allrepos/chess-tui.svg)](https://repology.org/project/chess-tui/versions) 38 | 39 | For installation via package managers or other methods, see the [Installation Guide](https://thomas-mauran.github.io/chess-tui/docs/Installation/Packaging%20status). 40 | 41 | ### Features 42 | 43 |
44 | Local 2 player mode 45 | Local 2 players 46 |
47 |
48 | Play against any UCI chess engine 49 | Play against a chess engine 50 |
51 |
52 | Lichess Integration 53 |

Play online on Lichess directly from your terminal!

54 | Lichess menu 55 |

See Lichess Features for details.

56 |
57 |
58 | Online multiplayer 59 | Online multiplayer 60 |
61 |
62 | Helper menu 63 | Helper menu 64 |
65 | 66 | ### Quick Start 67 | 68 | **Connect a chess engine:** 69 | ```bash 70 | # Simple engine path 71 | chess-tui -e /path/to/engine 72 | 73 | # Engine with command-line arguments (e.g., GNU Chess with UCI mode) 74 | chess-tui -e "/opt/homebrew/bin/gnuchess --uci" 75 | 76 | # Stockfish example 77 | chess-tui -e /opt/homebrew/bin/stockfish 78 | ``` 79 | See [Bot Configuration](https://thomas-mauran.github.io/chess-tui/docs/Configuration/bot) for details. 80 | 81 | **Configure Lichess:** 82 | ```bash 83 | chess-tui -l YOUR_LICHESS_TOKEN_HERE 84 | ``` 85 | See [Lichess Setup](https://thomas-mauran.github.io/chess-tui/docs/Lichess/setup) for details. 86 | 87 | ### Documentation 88 | 89 | 📚 **[Full Documentation](https://thomas-mauran.github.io/chess-tui/docs/intro)** 90 | 91 | - [Installation Guide](https://thomas-mauran.github.io/chess-tui/docs/Installation/Packaging%20status) 92 | - [Configuration](https://thomas-mauran.github.io/chess-tui/docs/Configuration/configuration-intro) 93 | - [Lichess Features](https://thomas-mauran.github.io/chess-tui/docs/Lichess/features) 94 | - [Multiplayer Guide](https://thomas-mauran.github.io/chess-tui/docs/Multiplayer/Local%20multiplayer) 95 | 96 | ### Links 97 | 98 | - 📦 [Crates.io](https://crates.io/crates/chess-tui) 99 | - 🗺️ [Roadmap](https://github.com/users/thomas-mauran/projects/4) 100 | - 🐛 [Report Issues](https://github.com/thomas-mauran/chess-tui/issues) 101 | -------------------------------------------------------------------------------- /src/default_skins.json: -------------------------------------------------------------------------------- 1 | { 2 | "skins": [ 3 | { 4 | "name": "Default", 5 | "board_white_color": {"Rgb": [160, 160, 160]}, 6 | "board_black_color": {"Rgb": [128, 95, 69]}, 7 | "piece_white_color": "White", 8 | "piece_black_color": "Black", 9 | "cursor_color": "LightBlue", 10 | "selection_color": "LightGreen", 11 | "last_move_color": "LightGreen" 12 | }, 13 | { 14 | "name": "Matrix", 15 | "board_white_color": {"Rgb": [60, 60, 60]}, 16 | "board_black_color": {"Rgb": [30, 30, 30]}, 17 | "piece_white_color": {"Rgb": [150, 255, 255]}, 18 | "piece_black_color": {"Rgb": [100, 200, 100]}, 19 | "cursor_color": {"Rgb": [120, 220, 220]}, 20 | "selection_color": {"Rgb": [100, 200, 200]}, 21 | "last_move_color": {"Rgb": [80, 180, 180]} 22 | }, 23 | { 24 | "name": "Ocean", 25 | "board_white_color": {"Rgb": [200, 220, 235]}, 26 | "board_black_color": {"Rgb": [100, 140, 170]}, 27 | "piece_white_color": {"Rgb": [245, 250, 255]}, 28 | "piece_black_color": {"Rgb": [50, 80, 120]}, 29 | "cursor_color": {"Rgb": [150, 200, 230]}, 30 | "selection_color": {"Rgb": [120, 180, 220]}, 31 | "last_move_color": {"Rgb": [100, 160, 200]} 32 | }, 33 | { 34 | "name": "Forest", 35 | "board_white_color": {"Rgb": [200, 230, 200]}, 36 | "board_black_color": {"Rgb": [100, 150, 100]}, 37 | "piece_white_color": {"Rgb": [250, 255, 250]}, 38 | "piece_black_color": {"Rgb": [60, 100, 60]}, 39 | "cursor_color": {"Rgb": [150, 220, 150]}, 40 | "selection_color": {"Rgb": [120, 200, 120]}, 41 | "last_move_color": {"Rgb": [200, 220, 120]} 42 | }, 43 | { 44 | "name": "Sunset", 45 | "board_white_color": {"Rgb": [255, 235, 210]}, 46 | "board_black_color": {"Rgb": [200, 140, 120]}, 47 | "piece_white_color": {"Rgb": [255, 250, 245]}, 48 | "piece_black_color": {"Rgb": [150, 80, 60]}, 49 | "cursor_color": {"Rgb": [255, 220, 150]}, 50 | "selection_color": {"Rgb": [255, 200, 120]}, 51 | "last_move_color": {"Rgb": [255, 180, 100]} 52 | }, 53 | { 54 | "name": "Midnight", 55 | "board_white_color": {"Rgb": [100, 100, 120]}, 56 | "board_black_color": {"Rgb": [50, 50, 70]}, 57 | "piece_white_color": {"Rgb": [220, 220, 240]}, 58 | "piece_black_color": {"Rgb": [140, 140, 180]}, 59 | "cursor_color": {"Rgb": [180, 160, 220]}, 60 | "selection_color": {"Rgb": [160, 140, 200]}, 61 | "last_move_color": {"Rgb": [140, 120, 200]} 62 | }, 63 | { 64 | "name": "Classic", 65 | "board_white_color": {"Rgb": [240, 217, 181]}, 66 | "board_black_color": {"Rgb": [181, 136, 99]}, 67 | "piece_white_color": {"Rgb": [250, 250, 250]}, 68 | "piece_black_color": {"Rgb": [40, 40, 40]}, 69 | "cursor_color": {"Rgb": [150, 200, 255]}, 70 | "selection_color": {"Rgb": [120, 220, 120]}, 71 | "last_move_color": {"Rgb": [100, 200, 100]} 72 | }, 73 | { 74 | "name": "Neon", 75 | "board_white_color": {"Rgb": [40, 40, 40]}, 76 | "board_black_color": {"Rgb": [20, 20, 20]}, 77 | "piece_white_color": {"Rgb": [255, 150, 255]}, 78 | "piece_black_color": {"Rgb": [150, 255, 255]}, 79 | "cursor_color": {"Rgb": [255, 255, 150]}, 80 | "selection_color": {"Rgb": [220, 150, 220]}, 81 | "last_move_color": {"Rgb": [150, 255, 150]} 82 | }, 83 | { 84 | "name": "Retro", 85 | "board_white_color": "White", 86 | "board_black_color": "Black", 87 | "piece_white_color": {"Rgb": [100, 150, 255]}, 88 | "piece_black_color": {"Rgb": [200, 80, 80]}, 89 | "cursor_color": {"Rgb": [255, 200, 0]}, 90 | "selection_color": {"Rgb": [100, 200, 100]}, 91 | "last_move_color": {"Rgb": [150, 200, 150]} 92 | } 93 | ] 94 | } 95 | 96 | -------------------------------------------------------------------------------- /website/docs/Code Architecture/GameLogic.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: GameLogic 3 | title: GameLogic 4 | sidebar_position: 4 5 | --- 6 | 7 | ## GameLogic 8 | 9 | The `GameLogic` struct encapsulates all game logic, separating it from UI concerns. This separation was introduced in a recent refactor to improve code maintainability and reduce borrow checker issues. 10 | 11 | ## Responsibilities 12 | 13 | The `GameLogic` struct manages: 14 | - **Board state management** - via `GameBoard` 15 | - **Move execution and validation** - using shakmaty library 16 | - **Bot and opponent handling** - coordinating external players 17 | - **Game state tracking** - checkmate, draw, promotion states 18 | 19 | ## Key Fields 20 | 21 | ```rust 22 | pub struct GameLogic { 23 | pub game_board: GameBoard, // Chess board state 24 | pub bot: Option, // Bot player (if playing against bot) 25 | pub opponent: Option, // Opponent player (if multiplayer) 26 | pub player_turn: Color, // Current player's turn 27 | pub game_state: GameState, // Current game state 28 | } 29 | ``` 30 | 31 | ## Key Methods 32 | 33 | ### Move Execution 34 | 35 | - **`execute_move(from: Square, to: Square)`** - Executes a player move 36 | - Validates the move using shakmaty 37 | - Updates move history and position history 38 | - Tracks captured pieces 39 | - Increments draw counter for 50-move rule 40 | 41 | - **`execute_bot_move()`** - Executes a bot move 42 | - Gets FEN position from game board 43 | - Retrieves move from bot via UCI protocol 44 | - Converts UCI move to shakmaty Move (with error handling) 45 | - Applies move to board (with error handling) 46 | - Handles errors gracefully without panicking 47 | 48 | - **`execute_opponent_move() -> bool`** - Executes opponent move from network 49 | - Reads move string from TCP stream 50 | - Uses `parse_opponent_move_string()` helper to parse chess notation (e.g., "e2e4") 51 | - Handles promotion piece selection 52 | - Returns true if move was successfully executed, false on error 53 | 54 | - **`parse_opponent_move_string(move_str: &str) -> Option<(Square, Square, Option)>`** (private helper) 55 | - Parses move string in chess notation (e.g., "e2e4" or "e7e8q") 56 | - Returns (from_square, to_square, promotion_piece) or None if invalid 57 | 58 | ### Promotion 59 | 60 | - **`promote_piece(promotion_cursor: u8)`** - Handles pawn promotion 61 | - Removes last move from history 62 | - Re-executes move with selected promotion piece 63 | - Updates game state back to Playing 64 | 65 | - **`handle_multiplayer_promotion()`** - Sends promotion choice to opponent 66 | 67 | ### Game State 68 | 69 | - **`switch_player_turn()`** - Alternates between White and Black 70 | - **`update_game_state()`** - Updates game state based on board conditions 71 | - Checks for checkmate 72 | - Checks for draw conditions 73 | - Checks for promotion requirement 74 | 75 | ### Helper Methods (Internal) 76 | 77 | - **`update_game_state_after_move()`** - Updates game state after a move (checkmate, draw, promotion) 78 | - **`handle_after_move_bot_logic()`** - Handles bot-specific logic after a move 79 | - **`handle_after_move_opponent_logic()`** - Handles opponent-specific logic after a move 80 | - **`handle_after_move_board_flip()`** - Handles board flipping logic after a move (only in single-player mode) 81 | 82 | ## Integration with Game 83 | 84 | The `GameLogic` is owned by the `Game` struct, which also contains the `UI` struct: 85 | 86 | ```rust 87 | pub struct Game { 88 | pub logic: GameLogic, 89 | pub ui: UI, 90 | } 91 | ``` 92 | 93 | This separation allows: 94 | - UI to remain responsive while game logic executes 95 | - Clear separation of concerns 96 | - Easier testing of game logic independently 97 | - Reduced borrow checker conflicts 98 | 99 | ## Game State Flow 100 | 101 | 1. **Playing** - Normal game state, players making moves 102 | 2. **Promotion** - Pawn reached promotion square, waiting for piece selection 103 | 3. **Checkmate** - Game ended, one player is in checkmate 104 | 4. **Draw** - Game ended in a draw (stalemate, 50-move rule, repetition, insufficient material) 105 | 106 | The game state is automatically updated after each move via `update_game_state()`. 107 | 108 | -------------------------------------------------------------------------------- /.github/workflows/generate-blog-post.yml: -------------------------------------------------------------------------------- 1 | name: Generate Blog Post from Release 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | 7 | jobs: 8 | generate-blog-post: 9 | name: Generate Blog Post 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | pull-requests: write 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 18 25 | 26 | - name: Extract release information 27 | id: release-info 28 | run: | 29 | TAG_NAME="${{ github.event.release.tag_name }}" 30 | RELEASE_DATE="${{ github.event.release.published_at }}" 31 | RELEASE_BODY="${{ github.event.release.body }}" 32 | 33 | # Extract version number from tag (remove 'v' prefix if present) 34 | VERSION="${TAG_NAME#v}" 35 | 36 | # Format date as YYYY-MM-DD 37 | DATE=$(echo "$RELEASE_DATE" | cut -d'T' -f1) 38 | 39 | # Extract title from release body (first non-empty line that looks like a title) 40 | # Or use tag name as fallback 41 | TITLE=$(echo "$RELEASE_BODY" | grep -E "^#+\s+" | head -n 1 | sed 's/^#\+\s*//' || echo "Release $VERSION") 42 | 43 | # If title is empty or just whitespace, use tag name 44 | if [ -z "$TITLE" ] || [ "$TITLE" = "" ]; then 45 | TITLE="Release $VERSION" 46 | fi 47 | 48 | echo "version=$VERSION" >> $GITHUB_OUTPUT 49 | echo "date=$DATE" >> $GITHUB_OUTPUT 50 | echo "title=$TITLE" >> $GITHUB_OUTPUT 51 | echo "body<> $GITHUB_OUTPUT 52 | echo "$RELEASE_BODY" >> $GITHUB_OUTPUT 53 | echo "EOF" >> $GITHUB_OUTPUT 54 | 55 | - name: Generate blog post 56 | run: | 57 | node scripts/blog-scripts/generate-blog-post.js \ 58 | "${{ steps.release-info.outputs.version }}" \ 59 | "${{ steps.release-info.outputs.date }}" \ 60 | "${{ steps.release-info.outputs.title }}" \ 61 | "${{ steps.release-info.outputs.body }}" 62 | 63 | - name: Configure Git 64 | run: | 65 | git config --global user.name 'github-actions[bot]' 66 | git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com' 67 | 68 | - name: Create branch and commit blog post 69 | id: commit 70 | run: | 71 | # Ensure we're on main and up to date 72 | git checkout main 73 | git pull origin main 74 | 75 | BRANCH_NAME="docs/release-${{ steps.release-info.outputs.version }}-blog-post" 76 | git checkout -b "$BRANCH_NAME" 77 | 78 | # Only add blog post files, explicitly avoid workflow files 79 | git add website/blog/*.md 80 | 81 | # Check if there are any staged changes 82 | if git diff --staged --quiet; then 83 | echo "No changes to commit" 84 | echo "has_changes=false" >> $GITHUB_OUTPUT 85 | else 86 | # Verify we're only committing blog files 87 | STAGED_FILES=$(git diff --staged --name-only) 88 | echo "Staged files: $STAGED_FILES" 89 | 90 | git commit -m "docs: add blog post for release ${{ steps.release-info.outputs.version }}" 91 | git push -u origin "$BRANCH_NAME" 92 | echo "has_changes=true" >> $GITHUB_OUTPUT 93 | fi 94 | 95 | - name: Create Pull Request 96 | if: steps.commit.outputs.has_changes == 'true' 97 | uses: peter-evans/create-pull-request@v6 98 | with: 99 | token: ${{ secrets.GITHUB_TOKEN }} 100 | branch: docs/release-${{ steps.release-info.outputs.version }}-blog-post 101 | base: main 102 | title: "docs: Add blog post for release ${{ steps.release-info.outputs.version }}" 103 | body: | 104 | This PR was automatically created by the release workflow. 105 | 106 | It adds a blog post for release **${{ steps.release-info.outputs.version }}**. 107 | 108 | Please review and merge when ready. 109 | labels: | 110 | documentation 111 | automated 112 | delete-branch: true 113 | -------------------------------------------------------------------------------- /website/docs/Code Architecture/pieces.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: Pieces 3 | title: Pieces 4 | sidebar_position: 2 5 | --- 6 | 7 | ## Pieces Architecture 8 | 9 | The pieces module in chess-tui is responsible for **rendering and display** of chess pieces. The actual chess logic (move validation, legal moves, game rules) is handled by the [`shakmaty`](https://docs.rs/shakmaty/) library. 10 | 11 | ## Architecture Overview 12 | 13 | ```mermaid 14 | classDiagram 15 | 16 | class Role { 17 | <> 18 | King 19 | Queen 20 | Rook 21 | Bishop 22 | Knight 23 | Pawn 24 | } 25 | 26 | class Color { 27 | <> 28 | White 29 | Black 30 | } 31 | 32 | class DisplayMode { 33 | <> 34 | DEFAULT 35 | ASCII 36 | } 37 | 38 | class PiecesModule { 39 | +role_to_utf_enum(role: Role, color: Option~Color~) str 40 | +role_to_string_enum(role: Option~Role~, display_mode: DisplayMode) str 41 | } 42 | 43 | class PieceDisplayStructs { 44 | <> 45 | Pawn, Rook, Knight, Bishop, Queen, King 46 | Each with to_string(display_mode) method 47 | } 48 | 49 | Role <|.. PiecesModule : "uses" 50 | Color <|.. PiecesModule : "uses" 51 | DisplayMode <|.. PiecesModule : "uses" 52 | DisplayMode <|.. PieceDisplayStructs : "uses" 53 | ``` 54 | 55 | ## Key Components 56 | 57 | ### Piece Representation 58 | 59 | Chess pieces are represented using types from the **shakmaty** library: 60 | - **`shakmaty::Role`** - Enum representing piece types (King, Queen, Rook, Bishop, Knight, Pawn) 61 | - **`shakmaty::Color`** - Enum representing piece colors (White, Black) 62 | 63 | The actual chess logic (legal moves, check detection, etc.) is handled by shakmaty's `Position` and `Chess` types in the `GameBoard` struct. 64 | 65 | ### Piece Display Structs 66 | 67 | Each piece type has a simple struct with a `to_string()` method for rendering: 68 | 69 | - **`Pawn`** - Renders pawn pieces 70 | - **`Rook`** - Renders rook pieces 71 | - **`Knight`** - Renders knight pieces 72 | - **`Bishop`** - Renders bishop pieces 73 | - **`Queen`** - Renders queen pieces 74 | - **`King`** - Renders king pieces 75 | 76 | Each struct implements: 77 | ```rust 78 | pub fn to_string(display_mode: &DisplayMode) -> &'static str 79 | ``` 80 | 81 | This method returns different visual representations based on the display mode: 82 | - **DEFAULT mode**: Multi-line Unicode art using box-drawing characters 83 | - **ASCII mode**: Single character representation (P, R, N, B, Q, K) 84 | 85 | ### Utility Functions 86 | 87 | The `pieces` module provides utility functions for converting between shakmaty types and display strings: 88 | 89 | #### `role_to_utf_enum(role: &Role, color: Option) -> &'static str` 90 | 91 | Converts a shakmaty `Role` and optional `Color` to a UTF-8 chess piece symbol: 92 | - White pieces: ♔ ♕ ♖ ♗ ♘ ♙ 93 | - Black pieces: ♚ ♛ ♜ ♝ ♞ ♟ 94 | 95 | Used for rendering pieces in the move history and captured pieces displays. 96 | 97 | #### `role_to_string_enum(role: Option, display_mode: &DisplayMode) -> &'static str` 98 | 99 | Converts a shakmaty `Role` to a string representation based on display mode: 100 | - **DEFAULT**: Returns UTF-8 chess symbols (♚ ♛ ♜ ♝ ♞ ♟) 101 | - **ASCII**: Returns single letters (K Q R B N P) 102 | 103 | ## Integration with Game Logic 104 | 105 | The pieces module is **purely for display purposes**. All chess logic is handled by: 106 | 107 | 1. **`GameBoard`** - Uses `shakmaty::Chess` for position management 108 | 2. **`shakmaty::Position`** - Provides legal moves, check detection, etc. 109 | 3. **`shakmaty::Move`** - Represents moves in the game 110 | 111 | When rendering the board, the UI layer: 112 | 1. Gets piece information from `GameBoard.get_role_at_square()` and `get_piece_color_at_square()` 113 | 2. Uses the pieces module utilities to convert to display strings 114 | 3. Renders using the appropriate display mode 115 | 116 | ## Example Usage 117 | 118 | ```rust 119 | use shakmaty::{Role, Color}; 120 | use crate::pieces::{role_to_utf_enum, King}; 121 | 122 | // Get UTF symbol for a white king 123 | let symbol = role_to_utf_enum(&Role::King, Some(Color::White)); // "♔" 124 | 125 | // Get display string for king in ASCII mode 126 | let ascii = King::to_string(&DisplayMode::ASCII); // "K" 127 | ``` 128 | -------------------------------------------------------------------------------- /src/ui/ongoing_games.rs: -------------------------------------------------------------------------------- 1 | use crate::app::App; 2 | use ratatui::{ 3 | layout::{Alignment, Constraint, Direction, Layout}, 4 | style::{Color, Modifier, Style}, 5 | text::{Line, Span}, 6 | widgets::{Block, BorderType, Borders, Paragraph}, 7 | Frame, 8 | }; 9 | 10 | pub fn render_ongoing_games(frame: &mut Frame, app: &App) { 11 | let area = frame.area(); 12 | 13 | // Create layout 14 | let chunks = Layout::default() 15 | .direction(Direction::Vertical) 16 | .constraints([ 17 | Constraint::Length(3), // Title 18 | Constraint::Min(10), // Games list 19 | Constraint::Length(3), // Footer 20 | ]) 21 | .split(area); 22 | 23 | // Title 24 | let title = Paragraph::new("My Ongoing Lichess Games") 25 | .style( 26 | Style::default() 27 | .fg(Color::Yellow) 28 | .add_modifier(Modifier::BOLD), 29 | ) 30 | .alignment(Alignment::Center) 31 | .block( 32 | Block::default() 33 | .borders(Borders::ALL) 34 | .border_type(BorderType::Rounded), 35 | ); 36 | frame.render_widget(title, chunks[0]); 37 | 38 | // Games list 39 | let games = &app.ongoing_games; 40 | let mut game_lines = vec![Line::from("")]; 41 | 42 | if games.is_empty() { 43 | game_lines 44 | .push(Line::from("No ongoing games found.").style(Style::default().fg(Color::Gray))); 45 | game_lines.push(Line::from("")); 46 | game_lines.push(Line::from("Use 'Seek Game' to start a new game.")); 47 | } else { 48 | for (idx, game) in games.iter().enumerate() { 49 | let is_selected = app.menu_cursor == idx as u8; 50 | 51 | let style = if is_selected { 52 | Style::default() 53 | .fg(Color::Black) 54 | .bg(Color::White) 55 | .add_modifier(Modifier::BOLD) 56 | } else { 57 | Style::default().fg(Color::White) 58 | }; 59 | 60 | let prefix = if is_selected { "► " } else { " " }; 61 | 62 | let opponent_name = &game.opponent.username; 63 | let rating = game 64 | .opponent 65 | .rating 66 | .map(|r| format!(" ({})", r)) 67 | .unwrap_or_default(); 68 | let turn_indicator = if game.is_my_turn { 69 | " ⏰ Your turn" 70 | } else { 71 | "" 72 | }; 73 | 74 | game_lines.push(Line::from(vec![ 75 | Span::styled(prefix, style), 76 | Span::styled( 77 | format!("vs {} {}{}", opponent_name, rating, turn_indicator), 78 | style, 79 | ), 80 | ])); 81 | 82 | game_lines.push(Line::from(vec![ 83 | Span::raw(" "), 84 | Span::styled( 85 | format!("Game ID: {} | Color: {}", game.game_id, game.color), 86 | Style::default().fg(Color::Gray), 87 | ), 88 | ])); 89 | 90 | game_lines.push(Line::from("")); 91 | } 92 | } 93 | 94 | let games_widget = Paragraph::new(game_lines) 95 | .block( 96 | Block::default() 97 | .borders(Borders::ALL) 98 | .border_type(BorderType::Rounded) 99 | .title(format!("{} game(s)", games.len())), 100 | ) 101 | .alignment(Alignment::Left); 102 | frame.render_widget(games_widget, chunks[1]); 103 | 104 | // Footer 105 | let footer = Paragraph::new(vec![Line::from(vec![ 106 | Span::styled("↑/↓", Style::default().fg(Color::Cyan)), 107 | Span::raw(" Navigate "), 108 | Span::styled("Enter", Style::default().fg(Color::Cyan)), 109 | Span::raw(" Join "), 110 | Span::styled("R", Style::default().fg(Color::Cyan)), 111 | Span::raw(" Resign "), 112 | Span::styled("Esc", Style::default().fg(Color::Cyan)), 113 | Span::raw(" Back"), 114 | ])]) 115 | .alignment(Alignment::Center) 116 | .block( 117 | Block::default() 118 | .borders(Borders::ALL) 119 | .border_type(BorderType::Rounded), 120 | ); 121 | frame.render_widget(footer, chunks[2]); 122 | } 123 | -------------------------------------------------------------------------------- /website/docs/Code Architecture/Opponent.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: Opponent 3 | title: Opponent 4 | sidebar_position: 8 5 | --- 6 | 7 | ## Opponent 8 | 9 | The `Opponent` struct handles online multiplayer via TCP streams. It manages network communication, move synchronization, and color assignment between players. 10 | 11 | ## Responsibilities 12 | 13 | The `Opponent` manages: 14 | - **TCP stream communication** - Network connection to other player 15 | - **Move encoding/decoding** - Converting moves to/from network format 16 | - **Color assignment** - Determining which color each player plays 17 | - **Game start coordination** - Synchronizing game initialization 18 | 19 | ## Key Fields 20 | 21 | ```rust 22 | pub struct Opponent { 23 | pub stream: Option, // TCP connection to opponent 24 | pub opponent_will_move: bool, // Flag indicating opponent's turn 25 | pub color: Color, // This player's color 26 | pub game_started: bool, // Whether game has started 27 | } 28 | ``` 29 | 30 | ## Key Methods 31 | 32 | ### Initialization 33 | 34 | - **`new(addr: String, color: Option) -> Opponent`** 35 | - Attempts connection to server (up to 5 retries) 36 | - Gets color from server if not provided 37 | - Sets `opponent_will_move` based on color (White moves first) 38 | - Panics if connection fails after all attempts 39 | 40 | - **`start_stream(addr: &str)`** - Establishes TCP connection to address 41 | 42 | ### Move Communication 43 | 44 | - **`send_move_to_server(move: &Move, promotion: Option)`** 45 | - Encodes move in standard chess notation (e.g., "e2e4") 46 | - Appends promotion piece if applicable (q, r, b, n) 47 | - Sends move string over TCP stream 48 | 49 | - **`read_stream() -> String`** 50 | - Reads move string from TCP stream (non-blocking) 51 | - Returns empty string if no data available 52 | - Handles "ended" message (game termination) 53 | - Returns move in format "e2e4" or "e7e8q" (with promotion) 54 | 55 | ### Game Control 56 | 57 | - **`send_end_game_to_server()`** - Sends "ended" message to signal game end 58 | 59 | ### Utility 60 | 61 | - **`copy() -> Opponent`** - Creates copy without stream (for cloning) 62 | - **`clone()`** - Custom clone implementation that attempts to clone TCP stream 63 | 64 | ## Move Encoding 65 | 66 | Moves are encoded as 4-5 character strings: 67 | - **Format**: `{from}{to}[promotion]` 68 | - **Examples**: 69 | - `"e2e4"` - Pawn from e2 to e4 70 | - `"e7e8q"` - Pawn promotion to queen 71 | - `"g1f3"` - Knight from g1 to f3 72 | 73 | Promotion pieces: 74 | - `q` - Queen 75 | - `r` - Rook 76 | - `b` - Bishop 77 | - `n` - Knight 78 | 79 | ## Color Assignment 80 | 81 | When connecting to a server: 82 | 1. **Host** chooses their color first 83 | 2. **Client** receives opposite color from server 84 | 3. Server sends: 85 | - `"w"` for White 86 | - `"b"` for Black 87 | 88 | The `opponent_will_move` flag is set based on color: 89 | - **White** → `opponent_will_move = true` (moves first) 90 | - **Black** → `opponent_will_move = false` (waits for opponent) 91 | 92 | ## Game Start Coordination 93 | 94 | The server sends `"s"` (start) message when: 95 | - Both players are connected 96 | - Colors are assigned 97 | - Game is ready to begin 98 | 99 | The `wait_for_game_start()` function blocks until this message is received. 100 | 101 | ## Network Protocol 102 | 103 | ### Connection Flow 104 | 105 | 1. **Host** starts game server via `App::setup_game_server()` 106 | 2. **Client** connects via `Opponent::new()` 107 | 3. Server assigns colors and sends start signal 108 | 4. Players exchange moves via TCP streams 109 | 110 | ### Move Exchange 111 | 112 | - Moves are sent immediately after execution 113 | - Non-blocking reads prevent UI freezing 114 | - Empty reads indicate no move available yet 115 | - "ended" message terminates the game 116 | 117 | ## Error Handling 118 | 119 | - **Connection failures**: Retries up to 5 times before panicking 120 | - **Network errors**: Returns empty string, allowing retry 121 | - **Game end**: Panics on "ended" message (expected behavior) 122 | - **Stream cloning**: Handles cases where stream cannot be cloned 123 | 124 | ## Integration with Game 125 | 126 | The opponent is managed by `GameLogic`: 127 | 128 | ```rust 129 | pub struct GameLogic { 130 | pub opponent: Option, 131 | // ... 132 | } 133 | ``` 134 | 135 | When `opponent_will_move` is true: 136 | 1. `App` main loop checks for opponent moves 137 | 2. Calls `GameLogic::execute_opponent_move()` 138 | 3. Reads move from stream 139 | 4. Applies move to board 140 | 5. Switches player turn 141 | 142 | This keeps the UI responsive while waiting for network moves. 143 | 144 | -------------------------------------------------------------------------------- /website/docs/Code Architecture/GameBoard.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: GameBoard 3 | title: GameBoard 4 | sidebar_position: 5 5 | --- 6 | 7 | ## GameBoard 8 | 9 | The `GameBoard` struct manages the chess board state using the **shakmaty** chess library. It maintains position history, move history, and handles all chess rule validation. 10 | 11 | ## Responsibilities 12 | 13 | The `GameBoard` manages: 14 | - **Position history** - Complete history of board positions 15 | - **Move history** - All moves made in the game 16 | - **Draw detection** - 50-move rule, threefold repetition, stalemate, insufficient material 17 | - **Move validation** - Using shakmaty's legal move system 18 | - **Coordinate conversion** - Handling flipped board orientation 19 | 20 | ## Key Fields 21 | 22 | ```rust 23 | pub struct GameBoard { 24 | pub move_history: Vec, // History of all moves 25 | pub position_history: Vec, // History of board positions (last = current) 26 | pub consecutive_non_pawn_or_capture: i32, // Counter for 50-move rule 27 | pub taken_pieces: Vec, // Captured pieces (with color) 28 | pub is_flipped: bool, // Board orientation for display 29 | } 30 | ``` 31 | 32 | ## Key Methods 33 | 34 | ### Position Access 35 | 36 | - **`position_ref() -> &Chess`** - Gets a read-only reference to the last position in the history (panics if empty) 37 | - **`current_position() -> Option<&Chess>`** - Safe access to current position, returns None if history is empty 38 | 39 | ### Move Execution 40 | 41 | - **`execute_move(from: Square, to: Square, promotion: Option) -> Option`** 42 | - Validates move using shakmaty's legal move system 43 | - Tracks captured pieces 44 | - Updates position and move history 45 | - Returns executed Move if successful, None if illegal 46 | 47 | - **`execute_shakmaty_move(from: Square, to: Square) -> bool`** 48 | - Convenience method for moves without promotion 49 | - Returns true if move was executed 50 | 51 | - **`execute_standard_move(from: Square, to: Square, promotion: Option) -> Option`** 52 | - Executes moves from standard (non-flipped) coordinates 53 | - Used for bot and opponent moves which come in standard notation 54 | 55 | - **`execute_shakmaty_move_with_promotion(from: Square, to: Square, promotion: Option) -> bool`** 56 | - Executes move with explicit promotion piece 57 | 58 | ### Move Information 59 | 60 | - **`move_to_san(index: usize) -> String`** - Converts move to Standard Algebraic Notation 61 | - Uses shakmaty's SAN conversion 62 | - Examples: "e4", "Nf3", "O-O", "Qxd5+" 63 | 64 | - **`is_latest_move_promotion() -> bool`** - Checks if last move was a promotion 65 | 66 | ### Piece Information 67 | 68 | - **`get_role_at_square(square: &Square) -> Option`** - Gets piece type at square 69 | - **`get_piece_color_at_square(square: &Square) -> Option`** - Gets piece color at square 70 | - **`is_square_occupied(square: &Square) -> bool`** - Checks if square has a piece 71 | 72 | ### Legal Moves 73 | 74 | - **`get_authorized_positions(player_turn: Color, square: &Square) -> Vec`** 75 | - Returns all legal destination squares for a piece 76 | - Uses shakmaty's `legal_moves()` method 77 | - Filters moves by source square 78 | 79 | ### Game End Detection 80 | 81 | - **`is_checkmate() -> bool`** - Checks if current position is checkmate 82 | - **`is_draw() -> bool`** - Checks for draw conditions: 83 | - Stalemate 84 | - 50-move rule (`consecutive_non_pawn_or_capture == 50`) 85 | - Threefold repetition (`is_draw_by_repetition()`) 86 | - Insufficient material 87 | - **`is_draw_by_repetition() -> bool`** - Checks if position repeated 3+ times 88 | - **`is_getting_checked(player_turn: Color) -> bool`** - Checks if player is in check 89 | 90 | ### Taken Pieces 91 | 92 | - **`white_taken_pieces() -> Vec`** - Returns captured white pieces 93 | - **`black_taken_pieces() -> Vec`** - Returns captured black pieces 94 | 95 | ### Board Orientation 96 | 97 | - **`flip_the_board()`** - Toggles board orientation (for perspective switching) 98 | - **`is_flipped: bool`** - Tracks current orientation 99 | 100 | ### FEN Position 101 | 102 | - **`fen_position() -> String`** 103 | - Generates FEN string for UCI engine communication 104 | - Uses shakmaty's FEN encoding 105 | - Automatically uses current board position 106 | 107 | ### Utility 108 | 109 | - **`reset()`** - Resets board to starting position 110 | - **`increment_consecutive_non_pawn_or_capture(role_from: Role, role_to: Option)`** 111 | - Updates 50-move rule counter 112 | - Resets on pawn moves or captures 113 | 114 | ## Shakmaty Integration 115 | 116 | The `GameBoard` uses shakmaty's `Chess` type for position management: 117 | 118 | - **`Vec`** - Position history, each element is a complete board state 119 | - **`Move`** - Move representation with promotion, castling, en passant support 120 | - **Legal move validation** - All moves are validated using `chess.legal_moves()` 121 | - **SAN conversion** - Uses `San::from_move()` for algebraic notation 122 | 123 | This ensures all chess rules are correctly enforced by the shakmaty library. 124 | 125 | -------------------------------------------------------------------------------- /scripts/install-stockfish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Install Stockfish chess engine 4 | # This script detects the operating system and installs Stockfish accordingly 5 | # and automatically configures it in chess-tui's config file 6 | 7 | set -e 8 | 9 | echo "Installing Stockfish chess engine..." 10 | 11 | # Function to get config directory based on OS 12 | get_config_dir() { 13 | if [[ "$OSTYPE" == "darwin"* ]]; then 14 | # macOS 15 | echo "$HOME/Library/Application Support" 16 | elif [[ "$OSTYPE" == "linux-gnu"* ]]; then 17 | # Linux - check XDG_CONFIG_HOME first, fallback to ~/.config 18 | if [[ -n "$XDG_CONFIG_HOME" ]]; then 19 | echo "$XDG_CONFIG_HOME" 20 | else 21 | echo "$HOME/.config" 22 | fi 23 | else 24 | # Fallback (Windows would be %APPDATA% but bash script won't work there anyway) 25 | echo "$HOME/.config" 26 | fi 27 | } 28 | 29 | # Function to update config.toml with engine_path 30 | update_config_file() { 31 | local config_file="$1" 32 | local engine_path="$2" 33 | 34 | # Create config directory if it doesn't exist 35 | local config_dir=$(dirname "$config_file") 36 | mkdir -p "$config_dir" 37 | 38 | if [ -f "$config_file" ] && [ -s "$config_file" ]; then 39 | # Config file exists and is not empty 40 | # Check if engine_path already exists in file 41 | if grep -q "^engine_path" "$config_file"; then 42 | # Update existing engine_path line 43 | # Use | as delimiter to avoid issues with forward slashes in paths 44 | if [[ "$OSTYPE" == "darwin"* ]]; then 45 | # macOS uses BSD sed 46 | sed -i '' "s|^engine_path = .*|engine_path = \"$engine_path\"|" "$config_file" 47 | else 48 | # Linux uses GNU sed 49 | sed -i "s|^engine_path = .*|engine_path = \"$engine_path\"|" "$config_file" 50 | fi 51 | else 52 | # Append engine_path to existing file 53 | # Add a blank line before appending for better formatting 54 | echo "" >> "$config_file" 55 | echo "engine_path = \"$engine_path\"" >> "$config_file" 56 | fi 57 | else 58 | # Create new config file with default values 59 | cat > "$config_file" << EOF 60 | engine_path = "$engine_path" 61 | display_mode = "DEFAULT" 62 | log_level = "OFF" 63 | bot_depth = 10 64 | selected_skin_name = "Default" 65 | sound_enabled = true 66 | EOF 67 | fi 68 | } 69 | 70 | # Detect OS 71 | if [[ "$OSTYPE" == "darwin"* ]]; then 72 | # macOS 73 | if command -v brew &> /dev/null; then 74 | echo "Detected macOS with Homebrew" 75 | echo "Installing Stockfish via Homebrew..." 76 | brew install stockfish 77 | STOCKFISH_PATH=$(brew --prefix)/bin/stockfish 78 | 79 | # Update config file 80 | CONFIG_DIR=$(get_config_dir) 81 | CONFIG_FILE="$CONFIG_DIR/chess-tui/config.toml" 82 | echo "" 83 | echo "Configuring chess-tui..." 84 | update_config_file "$CONFIG_FILE" "$STOCKFISH_PATH" 85 | 86 | echo "" 87 | echo "✓ Stockfish installed and configured successfully!" 88 | echo "" 89 | echo "The engine path has been set in: $CONFIG_FILE" 90 | echo " engine_path = \"$STOCKFISH_PATH\"" 91 | else 92 | echo "Error: Homebrew is required for macOS installation." 93 | echo "Install Homebrew from: https://brew.sh" 94 | exit 1 95 | fi 96 | elif [[ "$OSTYPE" == "linux-gnu"* ]]; then 97 | # Linux 98 | if command -v apt-get &> /dev/null; then 99 | # Debian/Ubuntu 100 | echo "Detected Debian/Ubuntu Linux" 101 | echo "Installing Stockfish via apt..." 102 | sudo apt-get update 103 | sudo apt-get install -y stockfish 104 | STOCKFISH_PATH="/usr/bin/stockfish" 105 | elif command -v dnf &> /dev/null; then 106 | # Fedora 107 | echo "Detected Fedora Linux" 108 | echo "Installing Stockfish via dnf..." 109 | sudo dnf install -y stockfish 110 | STOCKFISH_PATH="/usr/bin/stockfish" 111 | elif command -v pacman &> /dev/null; then 112 | # Arch Linux 113 | echo "Detected Arch Linux" 114 | echo "Installing Stockfish via pacman..." 115 | sudo pacman -S --noconfirm stockfish 116 | STOCKFISH_PATH="/usr/bin/stockfish" 117 | else 118 | echo "Error: Unsupported Linux distribution." 119 | echo "Please install Stockfish manually from: https://stockfishchess.org/download/" 120 | exit 1 121 | fi 122 | 123 | # Update config file 124 | CONFIG_DIR=$(get_config_dir) 125 | CONFIG_FILE="$CONFIG_DIR/chess-tui/config.toml" 126 | echo "" 127 | echo "Configuring chess-tui..." 128 | update_config_file "$CONFIG_FILE" "$STOCKFISH_PATH" 129 | 130 | echo "" 131 | echo "✓ Stockfish installed and configured successfully!" 132 | echo "" 133 | echo "The engine path has been set in: $CONFIG_FILE" 134 | echo " engine_path = \"$STOCKFISH_PATH\"" 135 | else 136 | echo "Error: Unsupported operating system: $OSTYPE" 137 | echo "Please install Stockfish manually from: https://stockfishchess.org/download/" 138 | exit 1 139 | fi 140 | 141 | -------------------------------------------------------------------------------- /scripts/blog-scripts/generate-blog-post.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Script to generate a Docusaurus blog post from GitHub release information 5 | * Usage: node generate-blog-post.js <release-body> 6 | */ 7 | 8 | const fs = require('fs'); 9 | const path = require('path'); 10 | 11 | const [version, date, title, releaseBody] = process.argv.slice(2); 12 | 13 | if (!version || !date || !title) { 14 | console.error('Usage: node generate-blog-post.js <version> <date> <title> <release-body>'); 15 | process.exit(1); 16 | } 17 | 18 | // Format date for display 19 | const dateObj = new Date(date); 20 | const formattedDate = dateObj.toLocaleDateString('en-US', { 21 | year: 'numeric', 22 | month: 'long', 23 | day: 'numeric' 24 | }); 25 | 26 | // Generate filename using absolute path 27 | const blogDir = path.join(__dirname, '..', '..', 'website', 'blog'); 28 | const filename = path.join(blogDir, `${date}-release-${version}.md`); 29 | 30 | // Parse release body and convert to blog format 31 | function parseReleaseBody(body) { 32 | if (!body) return ''; 33 | 34 | const lines = body.split('\n'); 35 | const sections = []; 36 | let currentSection = []; 37 | let inWhatChanged = false; 38 | let inContributors = false; 39 | let inChangelog = false; 40 | let skipNextEmpty = false; 41 | 42 | for (let i = 0; i < lines.length; i++) { 43 | let line = lines[i]; 44 | const trimmed = line.trim(); 45 | 46 | // Skip empty lines at the very start 47 | if (sections.length === 0 && !trimmed) continue; 48 | 49 | // Skip "Full Changelog" section (we'll add our own) 50 | if (trimmed.includes('Full Changelog') || trimmed.includes('**Full Changelog**')) { 51 | inChangelog = true; 52 | continue; 53 | } 54 | 55 | if (inChangelog && (trimmed.startsWith('---') || trimmed === '')) { 56 | inChangelog = false; 57 | continue; 58 | } 59 | if (inChangelog) continue; 60 | 61 | // Convert "What's Changed" to "What's New" 62 | if (trimmed.includes("What's Changed")) { 63 | if (currentSection.length > 0) { 64 | sections.push(currentSection.join('\n')); 65 | currentSection = []; 66 | } 67 | sections.push('## What\'s New\n'); 68 | inWhatChanged = true; 69 | skipNextEmpty = true; 70 | continue; 71 | } 72 | 73 | // Handle contributors section 74 | if (trimmed.includes('Contributors') && !trimmed.includes('New Contributors')) { 75 | if (currentSection.length > 0) { 76 | sections.push(currentSection.join('\n')); 77 | currentSection = []; 78 | } 79 | sections.push('## Contributors\n'); 80 | inContributors = true; 81 | inWhatChanged = false; 82 | skipNextEmpty = true; 83 | continue; 84 | } 85 | 86 | // Skip "New Contributors" header but keep the content 87 | if (trimmed.includes('New Contributors')) { 88 | skipNextEmpty = true; 89 | continue; 90 | } 91 | 92 | // Skip empty line after section headers 93 | if (skipNextEmpty && !trimmed) { 94 | skipNextEmpty = false; 95 | continue; 96 | } 97 | skipNextEmpty = false; 98 | 99 | // Convert PR references to markdown links (but preserve existing links) 100 | let processedLine = line.replace(/#(\d+)/g, (match, prNum) => { 101 | // Check if it's already a link by looking at surrounding context 102 | const before = line.substring(0, line.indexOf(match)); 103 | const after = line.substring(line.indexOf(match) + match.length); 104 | if (before.includes('[') || after.includes(']')) { 105 | return match; 106 | } 107 | return `[#${prNum}](https://github.com/thomas-mauran/chess-tui/pull/${prNum})`; 108 | }); 109 | 110 | // Convert @username to GitHub links (but not if already a link) 111 | // Match @ followed by word characters and hyphens, but not if inside a markdown link 112 | processedLine = processedLine.replace(/@([a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)/g, (match, username, offset) => { 113 | const before = processedLine.substring(0, offset); 114 | const after = processedLine.substring(offset + match.length); 115 | // Don't convert if already inside a markdown link 116 | if (before.includes('[') && !before.includes(']') || after.includes(']') && !after.includes('[')) { 117 | return match; 118 | } 119 | // Don't convert if it's part of a URL 120 | if (before.match(/https?:\/\/[^\s]*$/) || after.match(/^[^\s]*github\.com/)) { 121 | return match; 122 | } 123 | return `[@${username}](https://github.com/${username})`; 124 | }); 125 | 126 | // Add line to current section 127 | currentSection.push(processedLine); 128 | } 129 | 130 | // Add remaining section 131 | if (currentSection.length > 0) { 132 | sections.push(currentSection.join('\n')); 133 | } 134 | 135 | return sections.join('\n\n'); 136 | } 137 | 138 | // Extract H1 heading (just "Release X.X.X" without description) 139 | const h1Heading = title.includes(' - ') 140 | ? title.substring(0, title.indexOf(' - ')) 141 | : title; 142 | 143 | // Generate blog post content 144 | const blogContent = `--- 145 | title: ${title} 146 | authors: 147 | - name: Thomas Mauran 148 | url: https://github.com/thomas-mauran 149 | tags: 150 | - release 151 | --- 152 | 153 | # ${h1Heading} 154 | 155 | **Released:** ${formattedDate} 156 | 157 | ${parseReleaseBody(releaseBody)} 158 | 159 | ## Full Changelog 160 | 161 | For the complete list of changes, see the [full changelog](https://github.com/thomas-mauran/chess-tui/releases/tag/${version}). 162 | 163 | --- 164 | 165 | [View on GitHub](https://github.com/thomas-mauran/chess-tui/releases/tag/${version}) 166 | `; 167 | 168 | // Ensure blog directory exists 169 | if (!fs.existsSync(blogDir)) { 170 | fs.mkdirSync(blogDir, { recursive: true }); 171 | } 172 | 173 | // Write file 174 | fs.writeFileSync(filename, blogContent, 'utf8'); 175 | console.log(`✅ Created blog post: ${filename}`); 176 | -------------------------------------------------------------------------------- /website/docs/Configuration/skins.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: skins 3 | title: Skins 4 | sidebar_position: 5 5 | --- 6 | 7 | # Skins 8 | 9 | Chess-tui supports custom color skins that allow you to personalize the appearance of the chess board, pieces, and UI elements. 10 | 11 | ## File Location 12 | 13 | Skins are configured in a JSON file located at `CONFIG_DIR/chess-tui/skins.json`. This file contains an array of skin definitions. 14 | 15 | CONFIG_DIR is typically: 16 | - Linux: $XDG_CONFIG_HOME or $HOME/.config 17 | - macOS: $HOME/Library/Application Support 18 | - Windows: `%APPDATA%` (Roaming AppData folder) 19 | 20 | 21 | ## Using Skins 22 | 23 | ### Cycling Through Skins 24 | 25 | You can cycle through available skins in two ways: 26 | 27 | 1. **From the Home Menu**: Navigate to the "Skin" menu item (use arrow keys) and press: 28 | - **Space** or **Enter** to cycle forward 29 | - **Left Arrow** (or 'h') to cycle backward 30 | - **Right Arrow** (or 'l') to cycle forward 31 | 32 | 2. **During Gameplay**: Press **'s'** to cycle through skins at any time 33 | 34 | ### Built-in Display Modes 35 | 36 | Chess-tui includes two built-in display modes that are always available: 37 | 38 | - **Default**: Uses Unicode chess pieces with default colors 39 | - **ASCII**: Uses ASCII characters for better terminal compatibility 40 | 41 | These are always the first two options when cycling through skins. 42 | 43 | ## Skin Configuration Format 44 | 45 | The `skins.json` file follows this structure: 46 | 47 | ```json 48 | { 49 | "skins": [ 50 | { 51 | "name": "Skin Name", 52 | "board_white_color": "color", 53 | "board_black_color": "color", 54 | "piece_white_color": "color", 55 | "piece_black_color": "color", 56 | "cursor_color": "color", 57 | "selection_color": "color", 58 | "last_move_color": "color" 59 | } 60 | ] 61 | } 62 | ``` 63 | 64 | ## Field Descriptions 65 | 66 | | Field | Type | Description | 67 | |-------|------|-------------| 68 | | `name` | string | Display name of the skin (shown in UI) | 69 | | `board_white_color` | color | Color for light squares on the board | 70 | | `board_black_color` | color | Color for dark squares on the board | 71 | | `piece_white_color` | color | Color for white pieces | 72 | | `piece_black_color` | color | Color for black pieces | 73 | | `cursor_color` | color | Color for the cursor highlight | 74 | | `selection_color` | color | Color for selected piece highlight | 75 | | `last_move_color` | color | Color for last move highlight | 76 | 77 | ## Color Formats 78 | 79 | Colors can be specified in two ways: 80 | 81 | ### 1. Named Colors (String) 82 | 83 | Use standard terminal color names: 84 | 85 | - Basic colors: `"Black"`, `"Red"`, `"Green"`, `"Yellow"`, `"Blue"`, `"Magenta"`, `"Cyan"`, `"White"` 86 | - Gray shades: `"Gray"`, `"DarkGray"` 87 | - Light variants: `"LightRed"`, `"LightGreen"`, `"LightYellow"`, `"LightBlue"`, `"LightMagenta"`, `"LightCyan"` 88 | 89 | Example: 90 | ```json 91 | "board_white_color": "LightGray" 92 | ``` 93 | 94 | ### 2. RGB Values (Object) 95 | 96 | Use RGB values (0-255 for each component): 97 | 98 | ```json 99 | "board_white_color": {"Rgb": [240, 217, 181]} 100 | ``` 101 | 102 | ## Complete Example 103 | 104 | Here's a complete example with multiple skins: 105 | 106 | ```json 107 | { 108 | "skins": [ 109 | { 110 | "name": "Ocean", 111 | "board_white_color": {"Rgb": [200, 220, 235]}, 112 | "board_black_color": {"Rgb": [100, 140, 170]}, 113 | "piece_white_color": {"Rgb": [245, 250, 255]}, 114 | "piece_black_color": {"Rgb": [50, 80, 120]}, 115 | "cursor_color": {"Rgb": [150, 200, 230]}, 116 | "selection_color": {"Rgb": [120, 180, 220]}, 117 | "last_move_color": {"Rgb": [100, 160, 200]} 118 | }, 119 | { 120 | "name": "Forest", 121 | "board_white_color": {"Rgb": [200, 230, 200]}, 122 | "board_black_color": {"Rgb": [100, 150, 100]}, 123 | "piece_white_color": {"Rgb": [250, 255, 250]}, 124 | "piece_black_color": {"Rgb": [60, 100, 60]}, 125 | "cursor_color": {"Rgb": [150, 220, 150]}, 126 | "selection_color": {"Rgb": [120, 200, 120]}, 127 | "last_move_color": {"Rgb": [200, 220, 120]} 128 | }, 129 | { 130 | "name": "Retro", 131 | "board_white_color": "White", 132 | "board_black_color": "Black", 133 | "piece_white_color": {"Rgb": [100, 150, 255]}, 134 | "piece_black_color": {"Rgb": [200, 80, 80]}, 135 | "cursor_color": {"Rgb": [255, 200, 0]}, 136 | "selection_color": {"Rgb": [100, 200, 100]}, 137 | "last_move_color": {"Rgb": [150, 200, 150]} 138 | } 139 | ] 140 | } 141 | ``` 142 | 143 | ## Tips for Creating Skins 144 | 145 | - **Contrast**: Ensure good contrast between board squares and piece colors for visibility 146 | - **Readability**: Make sure pieces are clearly visible on both light and dark squares 147 | - **Cursor Visibility**: Choose cursor and selection colors that stand out on both square types 148 | - **Color Harmony**: Use a cohesive color palette for a pleasing aesthetic 149 | - **Testing**: Test your colors in different terminal emulators as color rendering can vary 150 | 151 | ## Default Skins 152 | 153 | Chess-tui comes with several pre-configured skins: 154 | 155 | - **Default**: Classic beige and brown board 156 | - **Matrix**: Dark theme with cyan and green accents 157 | - **Ocean**: Calming blue tones 158 | - **Forest**: Nature-inspired green palette 159 | - **Sunset**: Warm orange and peach colors 160 | - **Midnight**: Dark theme with purple tones 161 | - **Classic**: Traditional wooden chess board colors 162 | - **Neon**: Vibrant colors on dark background 163 | - **Retro**: Black and white board with colored pieces 164 | 165 | These skins are included in the `skins.json` file in the repository. You can modify them or add your own custom skins to the file. 166 | 167 | -------------------------------------------------------------------------------- /src/sound.rs: -------------------------------------------------------------------------------- 1 | use rodio::{OutputStream, Sink}; 2 | use std::sync::atomic::{AtomicBool, Ordering}; 3 | 4 | // Global sound enabled state 5 | static SOUND_ENABLED: AtomicBool = AtomicBool::new(true); 6 | // Track if audio is actually available (checked at startup) 7 | static AUDIO_AVAILABLE: AtomicBool = AtomicBool::new(true); 8 | 9 | /// Check if audio is available and update the availability state 10 | /// This should be called at startup to detect if we're in an environment without audio (e.g., Docker) 11 | pub fn check_audio_availability() -> bool { 12 | // Try to create an output stream 13 | // Note: ALSA may print errors to stderr, but we handle the failure gracefully 14 | let available = OutputStream::try_default().is_ok(); 15 | AUDIO_AVAILABLE.store(available, Ordering::Relaxed); 16 | available 17 | } 18 | 19 | /// Set whether sounds are enabled 20 | pub fn set_sound_enabled(enabled: bool) { 21 | SOUND_ENABLED.store(enabled, Ordering::Relaxed); 22 | } 23 | 24 | /// Get whether sounds are enabled 25 | pub fn is_sound_enabled() -> bool { 26 | SOUND_ENABLED.load(Ordering::Relaxed) && AUDIO_AVAILABLE.load(Ordering::Relaxed) 27 | } 28 | 29 | /// Plays a move sound when a chess piece is moved. 30 | /// This generates a pleasant, wood-like "click" sound using multiple harmonics. 31 | pub fn play_move_sound() { 32 | if !is_sound_enabled() { 33 | return; 34 | } 35 | // Spawn in a separate thread to avoid blocking the main game loop 36 | std::thread::spawn(|| { 37 | // Try to get an output stream, but don't fail if audio isn't available 38 | let Ok((_stream, stream_handle)) = OutputStream::try_default() else { 39 | return; 40 | }; 41 | 42 | // Create a sink to play the sound 43 | let Ok(sink) = Sink::try_new(&stream_handle) else { 44 | return; 45 | }; 46 | 47 | // Generate a pleasant wood-like click sound 48 | // Using a lower fundamental frequency with harmonics for a richer sound 49 | let sample_rate = 44100; 50 | let duration = 0.08; // 80 milliseconds - slightly longer for better perception 51 | let fundamental = 200.0; // Lower frequency for a more pleasant, less harsh sound 52 | 53 | let num_samples = (sample_rate as f64 * duration) as usize; 54 | let mut samples = Vec::with_capacity(num_samples); 55 | 56 | for i in 0..num_samples { 57 | let t = i as f64 / sample_rate as f64; 58 | 59 | // Create a more sophisticated envelope with exponential decay 60 | // Quick attack, smooth decay - like a wood piece being placed 61 | let envelope = if t < duration * 0.1 { 62 | // Quick attack (10% of duration) 63 | (t / (duration * 0.1)).powf(0.5) 64 | } else { 65 | // Exponential decay 66 | let decay_start = duration * 0.1; 67 | let decay_time = t - decay_start; 68 | let decay_duration = duration - decay_start; 69 | (-decay_time * 8.0 / decay_duration).exp() 70 | }; 71 | 72 | // Generate a richer sound with harmonics 73 | // Fundamental + 2nd harmonic (octave) + 3rd harmonic (fifth) 74 | let fundamental_wave = (t * fundamental * 2.0 * std::f64::consts::PI).sin(); 75 | let harmonic2 = (t * fundamental * 2.0 * 2.0 * std::f64::consts::PI).sin() * 0.3; 76 | let harmonic3 = (t * fundamental * 2.0 * 3.0 * std::f64::consts::PI).sin() * 0.15; 77 | 78 | // Combine harmonics with envelope 79 | let sample = (fundamental_wave + harmonic2 + harmonic3) * envelope * 0.25; 80 | 81 | // Convert to i16 sample 82 | samples.push((sample * i16::MAX as f64).clamp(i16::MIN as f64, i16::MAX as f64) as i16); 83 | } 84 | 85 | // Convert to a source that rodio can play 86 | let source = rodio::buffer::SamplesBuffer::new(1, sample_rate, samples); 87 | sink.append(source); 88 | sink.sleep_until_end(); 89 | }); 90 | } 91 | 92 | /// Plays a light navigation sound when moving through menu items. 93 | /// This generates a subtle, high-pitched "tick" sound for menu navigation. 94 | pub fn play_menu_nav_sound() { 95 | if !is_sound_enabled() { 96 | return; 97 | } 98 | // Spawn in a separate thread to avoid blocking the main game loop 99 | std::thread::spawn(|| { 100 | // Try to get an output stream, but don't fail if audio isn't available 101 | let Ok((_stream, stream_handle)) = OutputStream::try_default() else { 102 | return; 103 | }; 104 | 105 | // Create a sink to play the sound 106 | let Ok(sink) = Sink::try_new(&stream_handle) else { 107 | return; 108 | }; 109 | 110 | // Generate a light, high-pitched tick sound for menu navigation 111 | let sample_rate = 44100; 112 | let duration = 0.04; 113 | let frequency = 600.0; 114 | 115 | let num_samples = (sample_rate as f64 * duration) as usize; 116 | let mut samples = Vec::with_capacity(num_samples); 117 | 118 | for i in 0..num_samples { 119 | let t = i as f64 / sample_rate as f64; 120 | 121 | let envelope = if t < duration * 0.2 { 122 | (t / (duration * 0.2)).powf(0.3) 123 | } else { 124 | let decay_start = duration * 0.2; 125 | let decay_time = t - decay_start; 126 | let decay_duration = duration - decay_start; 127 | (-decay_time * 12.0 / decay_duration).exp() 128 | }; 129 | 130 | let sample = (t * frequency * 2.0 * std::f64::consts::PI).sin() * envelope * 0.3; 131 | 132 | samples.push((sample * i16::MAX as f64).clamp(i16::MIN as f64, i16::MAX as f64) as i16); 133 | } 134 | 135 | // Convert to a source that rodio can play 136 | let source = rodio::buffer::SamplesBuffer::new(1, sample_rate, samples); 137 | sink.append(source); 138 | sink.sleep_until_end(); 139 | }); 140 | } 141 | -------------------------------------------------------------------------------- /website/docs/Code Architecture/UI.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: UI 3 | title: UI 4 | sidebar_position: 6 5 | --- 6 | 7 | ## UI 8 | 9 | The `UI` struct handles all rendering and user interaction for the chess game. It manages cursor position, piece selection, and renders the board, move history, and captured pieces. 10 | 11 | ## Responsibilities 12 | 13 | The `UI` struct manages: 14 | - **Board rendering** - Chess board with pieces, highlights, and colors 15 | - **Move history display** - Algebraic notation move list 16 | - **Captured pieces display** - Shows taken pieces for each side 17 | - **Cursor and selection** - Keyboard and mouse interaction 18 | - **Display modes** - DEFAULT (Unicode) and ASCII rendering 19 | 20 | ## Key Fields 21 | 22 | ```rust 23 | pub struct UI { 24 | pub cursor_coordinates: Coord, // Current cursor position 25 | pub selected_square: Option<Square>, // Currently selected piece square 26 | pub selected_piece_cursor: i8, // Cursor for authorized moves 27 | pub promotion_cursor: i8, // Cursor for promotion selection 28 | pub old_cursor_position: Coord, // Previous cursor (for unselect) 29 | pub top_x: u16, // Board top-left X coordinate 30 | pub top_y: u16, // Board top-left Y coordinate 31 | pub width: u16, // Cell width 32 | pub height: u16, // Cell height 33 | pub mouse_used: bool, // Whether last action was mouse 34 | pub display_mode: DisplayMode, // DEFAULT or ASCII 35 | pub prompt: Prompt, // Text input prompt 36 | } 37 | ``` 38 | 39 | ## Key Methods 40 | 41 | ### Cursor Management 42 | 43 | - **`cursor_up/down/left/right(authorized_positions: Vec<Coord>)`** 44 | - Moves cursor in specified direction 45 | - If piece is selected, moves through authorized positions only 46 | - Otherwise, moves freely on the board 47 | 48 | - **`move_selected_piece_cursor(first_time_moving: bool, direction: i8, authorized_positions: Vec<Coord>)`** 49 | - Moves cursor through legal moves when piece is selected 50 | - Cycles through available destination squares 51 | 52 | - **`cursor_left_promotion()` / `cursor_right_promotion()`** 53 | - Navigates promotion piece selection (Queen, Rook, Bishop, Knight) 54 | 55 | ### Selection 56 | 57 | - **`is_cell_selected() -> bool`** - Checks if a piece is currently selected 58 | - **`unselect_cell()`** - Deselects piece and restores cursor position 59 | 60 | ### Rendering Methods 61 | 62 | #### Board Rendering 63 | 64 | - **`board_render(area: Rect, frame: &mut Frame, logic: &GameLogic)`** 65 | - Main board rendering method 66 | - Handles board flipping for perspective 67 | - Renders pieces using Unicode or ASCII 68 | - Highlights: 69 | - **Blue** - Current cursor position 70 | - **Green** - Selected piece or last move squares 71 | - **Grey** - Available move destinations 72 | - **Magenta** - King in check (with blinking) 73 | 74 | #### Label Rendering 75 | 76 | - **`render_rank_labels(frame: &mut Frame, area: Rect, is_flipped: bool)`** 77 | - Renders rank labels (1-8) on the left side of the board 78 | - Order adjusts based on board flip state 79 | 80 | - **`render_file_labels(frame: &mut Frame, area: Rect, is_flipped: bool)`** 81 | - Renders file labels (A-H) below the board 82 | - Order adjusts based on board flip state 83 | 84 | #### History Rendering 85 | 86 | - **`history_render(area: Rect, frame: &mut Frame, game: &Game)`** 87 | - Displays move history in Standard Algebraic Notation 88 | - Shows move number, piece symbol, and move (e.g., "1. ♙ e4 ♟ e5") 89 | - Formats moves in pairs (white, black) 90 | - Uses fixed-width formatting to ensure consistent column alignment: 91 | - Line numbers: Right-aligned in 3 characters + ". " (5 chars total) 92 | - White moves: Icon + space + move notation in 8 characters (left-aligned) 93 | - Black moves: Icon + space + move notation in 8 characters (left-aligned) 94 | - Maintains alignment regardless of move length or piece type 95 | 96 | #### Material Rendering 97 | 98 | - **`white_material_render(area: Rect, frame: &mut Frame, white_taken_pieces: &[Role])`** 99 | - Displays captured white pieces 100 | - Shows piece symbols for taken pieces 101 | - Displays "Press ? for help" at the bottom 102 | 103 | - **`black_material_render(area: Rect, frame: &mut Frame, black_taken_pieces: &[Role])`** 104 | - Displays captured black pieces 105 | - Shows piece symbols for taken pieces 106 | 107 | ### Piece Rendering 108 | 109 | - **`render_piece_paragraph(piece_type: Option<Role>, piece_color: Option<Color>, square: Rect) -> Paragraph`** 110 | - Renders individual piece based on display mode 111 | - **DEFAULT mode**: Multi-line Unicode art 112 | - **ASCII mode**: Single character (K, Q, R, B, N, P) 113 | 114 | ### Utility 115 | 116 | - **`reset()`** - Resets all UI state to defaults 117 | 118 | ## Display Modes 119 | 120 | ### DEFAULT Mode 121 | - Uses Unicode chess piece symbols (♔ ♕ ♖ ♗ ♘ ♙) 122 | - Multi-line piece art for board rendering 123 | - Color-coded pieces (white/black) 124 | 125 | ### ASCII Mode 126 | - Single character representation (K, Q, R, B, N, P) 127 | - Uppercase for white, lowercase for black 128 | - Underlined for white pieces 129 | 130 | ## Coordinate System 131 | 132 | The UI handles coordinate conversion between: 133 | - **Visual coordinates** - What the user sees (may be flipped) 134 | - **Standard coordinates** - Internal shakmaty coordinates (a1-h8) 135 | 136 | The `is_flipped` flag in `GameBoard` determines if coordinates need conversion when: 137 | - Selecting pieces 138 | - Moving pieces 139 | - Rendering the board 140 | 141 | ## Integration with Game 142 | 143 | The `UI` is owned by the `Game` struct alongside `GameLogic`: 144 | 145 | ```rust 146 | pub struct Game { 147 | pub logic: GameLogic, 148 | pub ui: UI, 149 | } 150 | ``` 151 | 152 | The UI reads game state from `GameLogic` but doesn't modify it directly. All game state changes go through `Game` methods which coordinate between UI and logic. 153 | 154 | --------------------------------------------------------------------------------