├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── documentation_request.md │ ├── feature_request.md │ └── refactor_request.md ├── pull_request_template.md └── workflows │ ├── build_and_test.yml │ ├── deploy-website.yml │ ├── docker_push.yml │ ├── flow_test_build_push.yml │ ├── lint.yml │ ├── release_cargo.yml │ └── test-deploy-website.yml ├── .gitignore ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── cargo-generate.toml ├── examples ├── demo-two-player.gif ├── demo-two-player.tape ├── helper.gif ├── helper.tape ├── play_against_black_bot.gif ├── play_against_black_bot.tape ├── play_against_white_bot.gif └── play_against_white_bot.tape ├── render_demos.sh ├── src ├── app.rs ├── constants.rs ├── event.rs ├── game_logic │ ├── board.rs │ ├── bot.rs │ ├── coord.rs │ ├── game.rs │ ├── game_board.rs │ ├── mod.rs │ ├── opponent.rs │ └── ui.rs ├── handler.rs ├── lib.rs ├── logging.rs ├── main.rs ├── pieces │ ├── bishop.rs │ ├── king.rs │ ├── knight.rs │ ├── mod.rs │ ├── pawn.rs │ ├── queen.rs │ └── rook.rs ├── server │ ├── game_server.rs │ └── mod.rs ├── ui │ ├── main_ui.rs │ ├── mod.rs │ ├── popups.rs │ ├── prompt.rs │ └── tui.rs └── utils.rs ├── tests ├── checkmates.rs ├── draws.rs ├── fen.rs ├── lib.rs ├── pieces │ ├── bishop.rs │ ├── king.rs │ ├── knight.rs │ ├── mod.rs │ ├── pawn.rs │ ├── queen.rs │ └── rook.rs ├── promotions.rs └── utils.rs └── website ├── .gitignore ├── README.md ├── blog └── wip.md ├── docs ├── Code Architecture │ ├── Game.md │ ├── intro.md │ └── pieces.md ├── Configuration │ ├── display.md │ ├── engine.md │ ├── intro.md │ └── logging.md ├── Installation │ ├── Arch Linux.md │ ├── Build from source.md │ ├── Cargo.md │ ├── Cargod.md │ ├── Docker.md │ ├── Logging.md │ ├── NetBSD.md │ ├── NixOS.md │ └── Packaging status.md ├── Multiplayer │ ├── Local multiplayer.md │ ├── Online multiplayer.md │ └── bore-port.png └── intro.mdx ├── docusaurus.config.ts ├── package-lock.json ├── package.json ├── sidebars.ts ├── src ├── components │ └── HomepageFeatures │ │ ├── index.tsx │ │ └── styles.module.css ├── css │ └── custom.css └── pages │ ├── index.module.css │ └── index.tsx ├── static ├── .nojekyll ├── gif │ ├── demo-two-player.gif │ ├── helper.gif │ ├── multiplayer.gif │ ├── play_against_black_bot.gif │ └── play_against_white_bot.gif └── img │ ├── ascii-display-mode-board.png │ ├── default-display-mode-board.png │ ├── favicon.ico │ ├── logo.png │ ├── social-card.png │ ├── undraw_docusaurus_mountain.svg │ ├── undraw_docusaurus_react.svg │ └── undraw_docusaurus_tree.svg ├── tsconfig.json └── yarn.lock /.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 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/workflows/build_and_test.yml: -------------------------------------------------------------------------------- 1 | name: Cargo Build & Test 2 | 3 | on: 4 | pull_request: 5 | workflow_call: 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | build_and_test: 12 | name: Rust project - latest 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | toolchain: 17 | - stable 18 | - beta 19 | - nightly 20 | steps: 21 | - uses: actions/checkout@v4 22 | - run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }} 23 | continue-on-error: true 24 | - run: cargo build --verbose 25 | continue-on-error: true 26 | - run: cargo test --verbose 27 | continue-on-error: true 28 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/workflows/docker_push.yml: -------------------------------------------------------------------------------- 1 | name: Build and push docker image 2 | 3 | on: 4 | workflow_call: 5 | 6 | env: 7 | REGISTRY: ghcr.io 8 | IMAGE_NAME: ${{ github.repository }} 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | packages: write 15 | contents: read 16 | steps: 17 | - name: Setup Docker buildx 18 | uses: docker/setup-buildx-action@v3 19 | - name: Log in to the Container registry 20 | uses: docker/login-action@v3 21 | with: 22 | registry: ghcr.io 23 | username: ${{ github.actor }} 24 | password: ${{ secrets.GITHUB_TOKEN }} 25 | - uses: docker/metadata-action@v5 26 | name: Extract metadata (tags, labels) for Docker 27 | id: meta 28 | with: 29 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 30 | tags: ${{ inputs.image-tags }} 31 | flavor: latest=false 32 | - name: Build and push Docker image 33 | uses: docker/build-push-action@v5 34 | with: 35 | push: 'true' 36 | tags: ${{ steps.meta.outputs.tags }} 37 | labels: ${{ steps.meta.outputs.labels }} 38 | cache-from: type=gha 39 | platforms: linux/amd64, linux/arm64, linux/arm/v7 40 | cache-to: type=gha,mode=max 41 | 42 | -------------------------------------------------------------------------------- /.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 | docker_push: 23 | name: Build & push Docker image 24 | needs: build_and_test 25 | uses: ./.github/workflows/docker_push.yml 26 | 27 | release_crate: 28 | name: Release new crate 29 | needs: build_and_test 30 | uses: ./.github/workflows/release_cargo.yml 31 | secrets: inherit 32 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Cargo Build & Test 2 | 3 | on: 4 | pull_request: 5 | workflow_call: 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | lint: 12 | name: Rust project - latest 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | name: Checkout project 17 | 18 | - uses: dtolnay/rust-toolchain@stable 19 | name: Install the Rust toolchain 20 | 21 | - uses: Swatinem/rust-cache@v2 22 | name: Use cached dependencies and artifacts 23 | 24 | - name: Check formatting 25 | run: cargo fmt --check 26 | 27 | - name: Check linting 28 | run: cargo clippy -- -D warnings -------------------------------------------------------------------------------- /.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 | - uses: katyo/publish-crates@v2 17 | with: 18 | registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} 19 | ignore-unpublished-changes: true 20 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | ``` -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "chess-tui" 3 | version = "1.6.1" 4 | authors = ["Thomas Mauran"] 5 | license = "MIT" 6 | edition = "2021" 7 | description = "A rusty chess game in your terminal 🦀" 8 | keywords = ["tui", "graphics", "chess", "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 = "0.28.1" 16 | uci = "0.2.1" 17 | toml = "0.5.8" 18 | log = "0.4.25" 19 | simplelog = "0.12.2" 20 | chrono = "0.4.39" 21 | 22 | [features] 23 | chess-tui = [] 24 | default = ["chess-tui"] 25 | 26 | [profile.release] 27 | lto = true 28 | codegen-units = 1 29 | opt-level = "z" 30 | strip = true 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 1. This tells docker to use the Rust official image 2 | FROM rust:1.83 as builder 3 | 4 | # 2. Copy the files in your machine to the Docker image 5 | COPY ./ ./ 6 | 7 | # Build your program for release 8 | RUN cargo build --release 9 | 10 | FROM debian:bookworm-slim AS runner 11 | COPY --from=builder /target/release/chess-tui /usr/bin/chess-tui 12 | 13 | ENTRYPOINT [ "/usr/bin/chess-tui" ] -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

chess-tui

3 | A rusty chess game in your terminal 🦀 4 | 5 | ![board](./examples/play_against_white_bot.gif) 6 | 7 |
8 | 9 | ![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 and playing against any UCI compatible chess engine. 16 | 17 | ### Quick install 18 | 19 | ```bash 20 | cargo install chess-tui 21 | chess-tui 22 | ``` 23 | 24 | If you want to install the game with your favorite package manager, you can find the installation guide [here](https://thomas-mauran.github.io/chess-tui/docs/Installation/Packaging%20status). 25 | 26 | ### Available on 27 | [![Packaging status](https://repology.org/badge/vertical-allrepos/chess-tui.svg)](https://repology.org/project/chess-tui/versions) 28 | 29 | ### Features 30 | 31 |
32 | Helper menu 33 | Helper menu 34 |
35 |
36 | Local 2 player mode 37 | Local 2 players 38 |
39 |
40 | Online multiplayer 41 | Online multiplayer 42 |
43 |
44 | Draws 45 | 50 |
51 |
52 | Piece Promotion 53 | no demo available yet 54 |
55 |
56 | Play against any UCI chess engine as black or white 57 |

Play the white pieces

58 | Play against a chess engine as white 59 | 60 |

Play the black pieces

61 | Play against a chess engine as black 62 |
63 | 64 | ### Connect a chess engine 65 | 66 | You can play chess-tui with any UCI compatible chess engines. To do so you will need to use the -e command to give the chess engine binary path. 67 | 68 | Example: 69 | 70 | ```bash 71 | chess-tui -e /your/bin/path 72 | ``` 73 | 74 | Here I installed stockfish using homebrew and gave chess-tui the path the the engine binary. 75 | This command will store in your home directory the chess engine path so you don't have to relink it everytime ! 76 | 77 | ### Configuration 78 | 79 | Chess-tui uses a TOML configuration file located at `~/.config/chess-tui/config.toml`. Here are the available configuration options: 80 | 81 | ```toml 82 | # Path to the chess engine binary 83 | engine_path = "/path/to/engine" 84 | 85 | # Display mode: "DEFAULT" or "ASCII" 86 | display_mode = "DEFAULT" 87 | 88 | # Logging level: "Off", "Error", "Warn", "Info", "Debug", "Trace" 89 | log_level = "Off" 90 | ``` 91 | 92 | #### Configuration Options: 93 | 94 | - **engine_path**: Path to your UCI-compatible chess engine binary 95 | - **display_mode**: 96 | - `DEFAULT`: Uses unicode chess pieces 97 | - `ASCII`: Uses ASCII characters for pieces 98 | - **log_level**: Controls the verbosity of logging 99 | - `Off`: No logging (default) 100 | - `Error`: Only errors 101 | - `Warn`: Warnings and errors 102 | - `Info`: General information, warnings and errors 103 | - `Debug`: Debugging information 104 | - `Trace`: Very verbose debugging information 105 | 106 | The config file is automatically created when you first run chess-tui. You can manually edit it to customize your experience. 107 | 108 | All logs are stored in `~/.config/chess-tui/logs`. 109 | 110 | Base config: 111 | ```toml 112 | # no engine path 113 | display_mode = "DEFAULT" 114 | log_level = "Off" 115 | ``` 116 | 117 | ### Documentation 118 | 119 | You can find the documentation of the project [here](https://thomas-mauran.github.io/chess-tui/docs/intro) 120 | 121 | ### Roadmap 122 | 123 | You can find the roadmap of the project [here](https://github.com/users/thomas-mauran/projects/4) if you want to contribute. 124 | 125 | ### Crates.io 126 | 127 | The project is also available on crates.io [here](https://crates.io/crates/chess-tui) 128 | -------------------------------------------------------------------------------- /cargo-generate.toml: -------------------------------------------------------------------------------- 1 | # configuration for https://cargo-generate.github.io/cargo-generate/ 2 | 3 | [template] 4 | ignore = ["README.md", ".github/"] 5 | -------------------------------------------------------------------------------- /examples/demo-two-player.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/589ebd9fb1ebebd9ee02277e6564619d0a8092f8/examples/demo-two-player.gif -------------------------------------------------------------------------------- /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 1920 10 | Set Height 1080 11 | 12 | Set WindowBar Colorful 13 | Set WindowBarSize 40 14 | 15 | Type "cargo run" Sleep 500ms Enter 16 | 17 | Sleep 0.5s 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 1.5s 26 | 27 | Down @0.3s 28 | Down @0.3s 29 | Space @0.3s 30 | Sleep 0.8s 31 | Space @0.3s 32 | 33 | Sleep 0.8s 34 | Left @0.3s 35 | Space @0.3s 36 | Sleep 0.8s 37 | Space @0.3s 38 | 39 | Down @0.3s 40 | Space @0.3s 41 | Sleep 0.8s 42 | Space @0.3s 43 | Sleep 0.8s 44 | 45 | Right @0.3s 46 | Right @0.3s 47 | Right @0.3s 48 | Space @0.3s 49 | Sleep 0.8s 50 | Space @0.3s 51 | Sleep 0.8s 52 | 53 | Left @0.3s 54 | Space @0.3s 55 | Sleep 0.8s 56 | Right @0.3s 57 | Right @0.3s 58 | Space @0.3s 59 | Sleep 0.8s 60 | 61 | Up @0.3s 62 | Right @0.3s 63 | Right @0.3s 64 | Space @0.3s 65 | Sleep 0.8s 66 | Space @0.3s 67 | Sleep 0.8s 68 | 69 | 70 | Up @0.3s 71 | Up @0.3s 72 | Up @0.3s 73 | Space @0.3s 74 | Sleep 0.8s 75 | Space @0.3s 76 | Sleep 0.8s 77 | -------------------------------------------------------------------------------- /examples/helper.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/589ebd9fb1ebebd9ee02277e6564619d0a8092f8/examples/helper.gif -------------------------------------------------------------------------------- /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 1920 10 | Set Height 1080 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 | Space @0.3s 23 | Sleep 1s 24 | 25 | Type h 26 | 27 | Sleep 5s 28 | -------------------------------------------------------------------------------- /examples/play_against_black_bot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/589ebd9fb1ebebd9ee02277e6564619d0a8092f8/examples/play_against_black_bot.gif -------------------------------------------------------------------------------- /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 1920 10 | Set Height 1080 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 | Space @0.3s 21 | Sleep 1s 22 | 23 | Right @0.3s 24 | Space @0.3s 25 | Sleep 0.3s 26 | 27 | Down @0.3s 28 | Down @0.3s 29 | Space @0.3s 30 | Sleep 0.3s 31 | Space @0.3s 32 | 33 | Right @0.3s 34 | Space @0.3s 35 | Sleep 0.3s 36 | Space @0.3s 37 | 38 | Right @0.3s 39 | Space @0.3s 40 | Sleep 0.3s 41 | Space @0.3s 42 | 43 | Left @0.3s 44 | Left @0.3s 45 | Left @0.3s 46 | Left @0.3s 47 | Left @0.3s 48 | Space @0.3s 49 | Sleep 0.8s 50 | Space @0.3s 51 | Sleep 0.8s 52 | 53 | -------------------------------------------------------------------------------- /examples/play_against_white_bot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/589ebd9fb1ebebd9ee02277e6564619d0a8092f8/examples/play_against_white_bot.gif -------------------------------------------------------------------------------- /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 1920 10 | Set Height 1080 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 | Space @0.3s 21 | Sleep 0.5s 22 | Space @0.3s 23 | Sleep 0.5s 24 | 25 | Down @0.3s 26 | Down @0.3s 27 | Space @0.3s 28 | Space @0.3s 29 | 30 | Down @0.3s 31 | Space @0.3s 32 | Space @0.3s 33 | 34 | Up @0.3s 35 | Space @0.3s 36 | Space @0.3s 37 | 38 | Up @0.3s 39 | Left @0.3s 40 | Space @0.3s 41 | Space @0.3s 42 | 43 | Up @0.3s 44 | Left @0.3s 45 | Space @0.3s 46 | Right @0.3s 47 | Space @0.3s 48 | 49 | Left @0.3s 50 | Down @0.3s 51 | Space @0.3s 52 | Sleep 0.5s 53 | Space @0.3s 54 | 55 | Sleep 5s -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use dirs::home_dir; 2 | use log::LevelFilter; 3 | use toml::Value; 4 | 5 | use crate::{ 6 | constants::{DisplayMode, Pages, Popups}, 7 | game_logic::{bot::Bot, game::Game, opponent::Opponent}, 8 | pieces::PieceColor, 9 | server::game_server::GameServer, 10 | }; 11 | use std::{ 12 | error, 13 | fs::{self, File}, 14 | io::Write, 15 | net::{IpAddr, UdpSocket}, 16 | thread::sleep, 17 | time::Duration, 18 | }; 19 | 20 | /// Application result type. 21 | pub type AppResult = std::result::Result>; 22 | 23 | /// Application. 24 | pub struct App { 25 | /// Is the application running? 26 | pub running: bool, 27 | /// Game 28 | pub game: Game, 29 | /// Current page to render 30 | pub current_page: Pages, 31 | /// Current popup to render 32 | pub current_popup: Option, 33 | // Selected color when playing against the bot 34 | pub selected_color: Option, 35 | /// Hosting 36 | pub hosting: Option, 37 | /// Host Ip 38 | pub host_ip: Option, 39 | /// menu current cursor 40 | pub menu_cursor: u8, 41 | /// path of the chess engine 42 | pub chess_engine_path: Option, 43 | pub log_level: LevelFilter, 44 | } 45 | 46 | impl Default for App { 47 | fn default() -> Self { 48 | Self { 49 | running: true, 50 | game: Game::default(), 51 | current_page: Pages::Home, 52 | current_popup: None, 53 | selected_color: None, 54 | hosting: None, 55 | host_ip: None, 56 | menu_cursor: 0, 57 | chess_engine_path: None, 58 | log_level: LevelFilter::Off, 59 | } 60 | } 61 | } 62 | 63 | impl App { 64 | pub fn toggle_help_popup(&mut self) { 65 | if self.current_popup == Some(Popups::Help) { 66 | self.current_popup = None; 67 | } else { 68 | self.current_popup = Some(Popups::Help); 69 | } 70 | } 71 | pub fn toggle_credit_popup(&mut self) { 72 | if self.current_page == Pages::Home { 73 | self.current_page = Pages::Credit; 74 | } else { 75 | self.current_page = Pages::Home; 76 | } 77 | } 78 | 79 | pub fn setup_game_server(&mut self, host_color: PieceColor) { 80 | let is_host_white = host_color == PieceColor::White; 81 | 82 | log::info!("Starting game server with host color: {:?}", host_color); 83 | 84 | std::thread::spawn(move || { 85 | let game_server = GameServer::new(is_host_white); 86 | log::info!("Game server created, starting server..."); 87 | game_server.run(); 88 | }); 89 | 90 | sleep(Duration::from_millis(100)); 91 | } 92 | 93 | pub fn create_opponent(&mut self) { 94 | let other_player_color = if self.selected_color.is_some() { 95 | Some(self.selected_color.unwrap().opposite()) 96 | } else { 97 | None 98 | }; 99 | 100 | if self.hosting.unwrap() { 101 | log::info!("Setting up host with color: {:?}", self.selected_color); 102 | self.current_popup = Some(Popups::WaitingForOpponentToJoin); 103 | self.host_ip = Some(format!("{}:2308", self.get_host_ip())); 104 | } 105 | 106 | let addr = self.host_ip.as_ref().unwrap().to_string(); 107 | let addr_with_port = addr.to_string(); 108 | log::info!("Attempting to connect to: {}", addr_with_port); 109 | 110 | // ping the server to see if it's up 111 | let s = UdpSocket::bind(addr_with_port.clone()); 112 | if s.is_err() { 113 | log::error!( 114 | "Server is unreachable at {}: {}", 115 | addr_with_port, 116 | s.err().unwrap() 117 | ); 118 | self.host_ip = None; 119 | return; 120 | } 121 | 122 | log::info!("Creating opponent with color: {:?}", other_player_color); 123 | self.game.opponent = Some(Opponent::new(addr_with_port, other_player_color)); 124 | 125 | if !self.hosting.unwrap() { 126 | log::info!("Setting up client (non-host) player"); 127 | self.selected_color = Some(self.game.opponent.as_mut().unwrap().color.opposite()); 128 | self.game.opponent.as_mut().unwrap().game_started = true; 129 | } 130 | 131 | if self.selected_color.unwrap() == PieceColor::Black { 132 | log::debug!("Flipping board for black player"); 133 | self.game.game_board.flip_the_board(); 134 | } 135 | } 136 | 137 | pub fn go_to_home(&mut self) { 138 | self.current_page = Pages::Home; 139 | self.restart(); 140 | } 141 | 142 | pub fn get_host_ip(&self) -> IpAddr { 143 | let socket = UdpSocket::bind("0.0.0.0:0").unwrap(); 144 | socket.connect("8.8.8.8:80").unwrap(); // Use an external IP to identify the default route 145 | 146 | socket.local_addr().unwrap().ip() 147 | } 148 | 149 | /// Handles the tick event of the terminal. 150 | pub fn tick(&self) {} 151 | 152 | /// Set running to false to quit the application. 153 | pub fn quit(&mut self) { 154 | self.running = false; 155 | } 156 | 157 | pub fn menu_cursor_up(&mut self, l: u8) { 158 | if self.menu_cursor > 0 { 159 | self.menu_cursor -= 1; 160 | } else { 161 | self.menu_cursor = l - 1; 162 | } 163 | } 164 | pub fn menu_cursor_right(&mut self, l: u8) { 165 | if self.menu_cursor < l - 1 { 166 | self.menu_cursor += 1; 167 | } else { 168 | self.menu_cursor = 0; 169 | } 170 | } 171 | pub fn menu_cursor_left(&mut self, l: u8) { 172 | if self.menu_cursor > 0 { 173 | self.menu_cursor -= 1; 174 | } else { 175 | self.menu_cursor = l - 1; 176 | } 177 | } 178 | pub fn menu_cursor_down(&mut self, l: u8) { 179 | if self.menu_cursor < l - 1 { 180 | self.menu_cursor += 1; 181 | } else { 182 | self.menu_cursor = 0; 183 | } 184 | } 185 | 186 | pub fn color_selection(&mut self) { 187 | self.current_popup = None; 188 | let color = match self.menu_cursor { 189 | 0 => PieceColor::White, 190 | 1 => PieceColor::Black, 191 | _ => unreachable!("Invalid color selection"), 192 | }; 193 | self.selected_color = Some(color); 194 | } 195 | 196 | pub fn bot_setup(&mut self) { 197 | let empty = "".to_string(); 198 | let path = match self.chess_engine_path.as_ref() { 199 | Some(engine_path) => engine_path, 200 | None => &empty, 201 | }; 202 | 203 | // if the selected Color is Black, we need to switch the Game 204 | if let Some(color) = self.selected_color { 205 | if color == PieceColor::Black { 206 | self.game.bot = Some(Bot::new(path, true)); 207 | 208 | self.game.execute_bot_move(); 209 | self.game.player_turn = PieceColor::Black; 210 | } 211 | } 212 | } 213 | 214 | pub fn hosting_selection(&mut self) { 215 | let choice = self.menu_cursor == 0; 216 | self.hosting = Some(choice); 217 | self.current_popup = None; 218 | } 219 | 220 | pub fn restart(&mut self) { 221 | let bot = self.game.bot.clone(); 222 | let opponent = self.game.opponent.clone(); 223 | self.game = Game::default(); 224 | 225 | self.game.bot = bot; 226 | self.game.opponent = opponent; 227 | self.current_popup = None; 228 | 229 | if self.game.bot.as_ref().is_some() 230 | && self 231 | .game 232 | .bot 233 | .as_ref() 234 | .is_some_and(|bot| bot.is_bot_starting) 235 | { 236 | self.game.execute_bot_move(); 237 | self.game.player_turn = PieceColor::Black; 238 | } 239 | } 240 | 241 | pub fn menu_select(&mut self) { 242 | match self.menu_cursor { 243 | 0 => self.current_page = Pages::Solo, 244 | 1 => { 245 | self.menu_cursor = 0; 246 | self.current_page = Pages::Multiplayer 247 | } 248 | 2 => { 249 | self.menu_cursor = 0; 250 | self.current_page = Pages::Bot 251 | } 252 | 3 => { 253 | self.game.ui.display_mode = match self.game.ui.display_mode { 254 | DisplayMode::ASCII => DisplayMode::DEFAULT, 255 | DisplayMode::DEFAULT => DisplayMode::ASCII, 256 | }; 257 | self.update_config(); 258 | } 259 | 4 => self.toggle_help_popup(), 260 | 5 => self.current_page = Pages::Credit, 261 | _ => {} 262 | } 263 | } 264 | 265 | pub fn update_config(&self) { 266 | let home_dir = home_dir().expect("Could not get home directory"); 267 | let config_path = home_dir.join(".config/chess-tui/config.toml"); 268 | let mut config = match fs::read_to_string(config_path.clone()) { 269 | Ok(content) => content 270 | .parse::() 271 | .unwrap_or_else(|_| Value::Table(Default::default())), 272 | Err(_) => Value::Table(Default::default()), 273 | }; 274 | 275 | if let Some(table) = config.as_table_mut() { 276 | table.insert( 277 | "display_mode".to_string(), 278 | Value::String(self.game.ui.display_mode.to_string()), 279 | ); 280 | table.insert( 281 | "log_level".to_string(), 282 | Value::String(self.log_level.to_string().to_string()), 283 | ); 284 | } 285 | 286 | let mut file = File::create(config_path.clone()).unwrap(); 287 | file.write_all(config.to_string().as_bytes()).unwrap(); 288 | } 289 | 290 | pub fn reset(&mut self) { 291 | self.game = Game::default(); 292 | self.current_popup = None; 293 | self.selected_color = None; 294 | self.hosting = None; 295 | self.host_ip = None; 296 | self.menu_cursor = 0; 297 | self.chess_engine_path = None; 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /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 | pub const TITLE: &str = r" 11 | ██████╗██╗ ██╗███████╗███████╗███████╗ ████████╗██╗ ██╗██╗ 12 | ██╔════╝██║ ██║██╔════╝██╔════╝██╔════╝ ╚══██╔══╝██║ ██║██║ 13 | ██║ ███████║█████╗ ███████╗███████╗█████╗██║ ██║ ██║██║ 14 | ██║ ██╔══██║██╔══╝ ╚════██║╚════██║╚════╝██║ ██║ ██║██║ 15 | ╚██████╗██║ ██║███████╗███████║███████║ ██║ ╚██████╔╝██║ 16 | ╚═════╝╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═════╝ ╚═╝ 17 | "; 18 | 19 | #[derive(Debug, Clone, Copy)] 20 | pub enum DisplayMode { 21 | DEFAULT, 22 | ASCII, 23 | } 24 | 25 | impl fmt::Display for DisplayMode { 26 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 27 | match *self { 28 | DisplayMode::ASCII => write!(f, "ASCII"), 29 | DisplayMode::DEFAULT => write!(f, "DEFAULT"), 30 | } 31 | } 32 | } 33 | 34 | pub fn home_dir() -> Result { 35 | match dirs::home_dir() { 36 | Some(dir) => Ok(dir), 37 | None => Err("Could not get home directory"), 38 | } 39 | } 40 | 41 | #[derive(Debug, PartialEq, Clone)] 42 | pub enum Pages { 43 | Home, 44 | Solo, 45 | Multiplayer, 46 | Bot, 47 | Credit, 48 | } 49 | impl Pages { 50 | pub fn variant_count() -> usize { 51 | 6 52 | } 53 | } 54 | 55 | #[derive(Debug, PartialEq, Clone)] 56 | pub enum Popups { 57 | ColorSelection, 58 | MultiplayerSelection, 59 | EnterHostIP, 60 | WaitingForOpponentToJoin, 61 | EnginePathError, 62 | Help, 63 | } 64 | -------------------------------------------------------------------------------- /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/game_logic/board.rs: -------------------------------------------------------------------------------- 1 | use super::coord::Coord; 2 | use crate::pieces::{PieceColor, PieceType}; 3 | 4 | pub type Board = [[Option<(PieceType, PieceColor)>; 8]; 8]; 5 | 6 | impl std::ops::Index<&Coord> for Board { 7 | type Output = Option<(PieceType, PieceColor)>; 8 | 9 | fn index(&self, index: &Coord) -> &Self::Output { 10 | &self[index.row as usize][index.col as usize] 11 | } 12 | } 13 | 14 | impl std::ops::IndexMut<&Coord> for Board { 15 | fn index_mut(&mut self, index: &Coord) -> &mut Self::Output { 16 | &mut self[index.row as usize][index.col as usize] 17 | } 18 | } 19 | 20 | pub fn init_board() -> Board { 21 | [ 22 | [ 23 | Some((PieceType::Rook, PieceColor::Black)), 24 | Some((PieceType::Knight, PieceColor::Black)), 25 | Some((PieceType::Bishop, PieceColor::Black)), 26 | Some((PieceType::Queen, PieceColor::Black)), 27 | Some((PieceType::King, PieceColor::Black)), 28 | Some((PieceType::Bishop, PieceColor::Black)), 29 | Some((PieceType::Knight, PieceColor::Black)), 30 | Some((PieceType::Rook, PieceColor::Black)), 31 | ], 32 | [ 33 | Some((PieceType::Pawn, PieceColor::Black)), 34 | Some((PieceType::Pawn, PieceColor::Black)), 35 | Some((PieceType::Pawn, PieceColor::Black)), 36 | Some((PieceType::Pawn, PieceColor::Black)), 37 | Some((PieceType::Pawn, PieceColor::Black)), 38 | Some((PieceType::Pawn, PieceColor::Black)), 39 | Some((PieceType::Pawn, PieceColor::Black)), 40 | Some((PieceType::Pawn, PieceColor::Black)), 41 | ], 42 | [None, None, None, None, None, None, None, None], 43 | [None, None, None, None, None, None, None, None], 44 | [None, None, None, None, None, None, None, None], 45 | [None, None, None, None, None, None, None, None], 46 | [ 47 | Some((PieceType::Pawn, PieceColor::White)), 48 | Some((PieceType::Pawn, PieceColor::White)), 49 | Some((PieceType::Pawn, PieceColor::White)), 50 | Some((PieceType::Pawn, PieceColor::White)), 51 | Some((PieceType::Pawn, PieceColor::White)), 52 | Some((PieceType::Pawn, PieceColor::White)), 53 | Some((PieceType::Pawn, PieceColor::White)), 54 | Some((PieceType::Pawn, PieceColor::White)), 55 | ], 56 | [ 57 | Some((PieceType::Rook, PieceColor::White)), 58 | Some((PieceType::Knight, PieceColor::White)), 59 | Some((PieceType::Bishop, PieceColor::White)), 60 | Some((PieceType::Queen, PieceColor::White)), 61 | Some((PieceType::King, PieceColor::White)), 62 | Some((PieceType::Bishop, PieceColor::White)), 63 | Some((PieceType::Knight, PieceColor::White)), 64 | Some((PieceType::Rook, PieceColor::White)), 65 | ], 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /src/game_logic/bot.rs: -------------------------------------------------------------------------------- 1 | use uci::Engine; 2 | 3 | use crate::utils::convert_notation_into_position; 4 | 5 | #[derive(Clone)] 6 | pub struct Bot { 7 | // the chess engine 8 | pub engine: Engine, 9 | /// Used to indicate if a bot move is following 10 | pub bot_will_move: bool, 11 | // if the bot is starting, meaning the player is black 12 | pub is_bot_starting: bool, 13 | } 14 | 15 | // Custom Default implementation 16 | impl Default for Bot { 17 | fn default() -> Self { 18 | Bot { 19 | engine: Engine::new("path_to_engine").expect("Failed to load engine"), // Specify the default engine path 20 | bot_will_move: false, 21 | is_bot_starting: false, 22 | } 23 | } 24 | } 25 | 26 | impl Bot { 27 | pub fn new(engine_path: &str, is_bot_starting: bool) -> Bot { 28 | let engine = Bot::create_engine(engine_path); 29 | 30 | Self { 31 | engine, 32 | bot_will_move: false, 33 | is_bot_starting, 34 | } 35 | } 36 | 37 | /// Allows you so set a 38 | pub fn set_engine(&mut self, engine_path: &str) { 39 | self.engine = Bot::create_engine(engine_path) 40 | } 41 | 42 | pub fn create_engine(engine_path: &str) -> Engine { 43 | match Engine::new(engine_path) { 44 | Ok(engine) => engine, 45 | Err(e) => { 46 | panic!( 47 | "Failed to initialize the engine at path: {}. Error: {:?}", 48 | engine_path, e 49 | ); 50 | } 51 | } 52 | } 53 | /* Method to make a move for the bot 54 | We use the UCI protocol to communicate with the chess engine 55 | */ 56 | pub fn get_bot_move(&mut self, fen_position: String) -> String { 57 | self.engine.set_position(&(fen_position as String)).unwrap(); 58 | let best_move = self.engine.bestmove(); 59 | let Ok(movement) = best_move else { 60 | panic!("An error has occured") 61 | }; 62 | 63 | convert_notation_into_position(&movement) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/game_logic/coord.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::UNDEFINED_POSITION; 2 | 3 | #[derive(PartialEq, Clone, Debug, Eq, PartialOrd, Ord, Copy)] 4 | pub struct Coord { 5 | /// rank, horizontal row, line, y axis 6 | pub row: u8, 7 | /// file, vertical column, x axis 8 | pub col: u8, 9 | } 10 | impl Coord { 11 | pub fn new, U2: Into>(row: U1, col: U2) -> Self { 12 | Coord { 13 | row: row.into(), 14 | col: col.into(), 15 | } 16 | } 17 | /// optional new: try making a valid [`Coord`], if can't, return [`None`] 18 | pub fn opt_new, U2: TryInto>(row: U1, col: U2) -> Option { 19 | let row: u8 = row.try_into().ok()?; 20 | let col: u8 = col.try_into().ok()?; 21 | 22 | let ret = Coord { row, col }; 23 | if ret.is_valid() { 24 | Some(ret) 25 | } else { 26 | None 27 | } 28 | } 29 | /// not yet set position, has to later be set and only used afterwards 30 | pub fn undefined() -> Self { 31 | Coord { 32 | row: UNDEFINED_POSITION, 33 | col: UNDEFINED_POSITION, 34 | } 35 | } 36 | /// checks whether `self` is valid as a chess board coordinate 37 | pub fn is_valid(&self) -> bool { 38 | (0..8).contains(&self.col) && (0..8).contains(&self.row) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/game_logic/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod board; 2 | pub mod bot; 3 | pub mod coord; 4 | pub mod game; 5 | pub mod game_board; 6 | pub mod opponent; 7 | pub mod ui; 8 | -------------------------------------------------------------------------------- /src/game_logic/opponent.rs: -------------------------------------------------------------------------------- 1 | use crate::pieces::{PieceColor, PieceMove}; 2 | use log; 3 | use std::{ 4 | io::{Read, Write}, 5 | net::TcpStream, 6 | panic, 7 | }; 8 | 9 | pub struct Opponent { 10 | // The stream to communicate with the engine 11 | pub stream: Option, 12 | /// Used to indicate if a Opponent move is following 13 | pub opponent_will_move: bool, 14 | // The color of the Opponent 15 | pub color: PieceColor, 16 | /// Is Game started 17 | pub game_started: bool, 18 | } 19 | 20 | // Custom Default implementation 21 | impl Default for Opponent { 22 | fn default() -> Self { 23 | Opponent { 24 | stream: None, 25 | opponent_will_move: false, 26 | color: PieceColor::Black, 27 | game_started: false, 28 | } 29 | } 30 | } 31 | 32 | impl Clone for Opponent { 33 | fn clone(&self) -> Self { 34 | Opponent { 35 | stream: self.stream.as_ref().and_then(|s| s.try_clone().ok()), // Custom handling for TcpStream 36 | opponent_will_move: self.opponent_will_move, 37 | color: self.color, 38 | game_started: self.game_started, 39 | } 40 | } 41 | } 42 | 43 | impl Opponent { 44 | pub fn copy(&self) -> Self { 45 | Opponent { 46 | stream: None, 47 | opponent_will_move: self.opponent_will_move, 48 | color: self.color, 49 | game_started: self.game_started, 50 | } 51 | } 52 | 53 | pub fn new(addr: String, color: Option) -> Opponent { 54 | log::info!( 55 | "Creating new opponent with addr: {} and color: {:?}", 56 | addr, 57 | color 58 | ); 59 | 60 | // Attempt to connect 5 times to the provided address 61 | let mut stream: Option = None; 62 | for attempt in 1..=5 { 63 | log::debug!("Connection attempt {} to {}", attempt, addr); 64 | match TcpStream::connect(addr.clone()) { 65 | Ok(s) => { 66 | log::info!("Successfully connected to server"); 67 | stream = Some(s); 68 | break; 69 | } 70 | Err(e) => { 71 | log::error!("Failed connection attempt {} to {}: {}", attempt, addr, e); 72 | } 73 | } 74 | } 75 | 76 | if let Some(stream) = stream { 77 | let color = match color { 78 | Some(color) => { 79 | log::info!("Using provided color: {:?}", color); 80 | color 81 | } 82 | None => { 83 | log::info!("Getting color from stream"); 84 | get_color_from_stream(&stream) 85 | } 86 | }; 87 | 88 | let opponent_will_move = match color { 89 | PieceColor::White => true, 90 | PieceColor::Black => false, 91 | }; 92 | log::info!( 93 | "Created opponent with color {:?}, will_move: {}", 94 | color, 95 | opponent_will_move 96 | ); 97 | 98 | Opponent { 99 | stream: Some(stream), 100 | opponent_will_move, 101 | color, 102 | game_started: false, 103 | } 104 | } else { 105 | log::error!("Failed to connect after 5 attempts to {}", addr); 106 | panic!( 107 | "Failed to connect to the server after 5 attempts to the following address {}", 108 | addr 109 | ); 110 | } 111 | } 112 | 113 | pub fn start_stream(&mut self, addr: &str) { 114 | match TcpStream::connect(addr) { 115 | Ok(stream) => { 116 | self.stream = Some(stream); 117 | } 118 | Err(e) => { 119 | panic!("Failed to connect: {}", e); 120 | } 121 | } 122 | } 123 | 124 | pub fn send_end_game_to_server(&mut self) { 125 | if let Some(game_stream) = self.stream.as_mut() { 126 | if let Err(e) = game_stream.write_all("ended".as_bytes()) { 127 | eprintln!("Failed to send end game: {}", e); 128 | } 129 | } 130 | } 131 | 132 | pub fn send_move_to_server( 133 | &mut self, 134 | move_to_send: &PieceMove, 135 | promotion_type: Option, 136 | ) { 137 | if let Some(game_stream) = self.stream.as_mut() { 138 | let move_str = format!( 139 | "{}{}{}{}{}", 140 | move_to_send.from.row, 141 | move_to_send.from.col, 142 | move_to_send.to.row, 143 | move_to_send.to.col, 144 | match promotion_type { 145 | Some(promotion) => promotion, 146 | None => "".to_string(), 147 | } 148 | ); 149 | if let Err(e) = game_stream.write_all(move_str.as_bytes()) { 150 | eprintln!("Failed to send move: {}", e); 151 | } 152 | } 153 | } 154 | 155 | pub fn read_stream(&mut self) -> String { 156 | if let Some(game_stream) = self.stream.as_mut() { 157 | let mut buffer = vec![0; 5]; 158 | match game_stream.read(&mut buffer) { 159 | Ok(bytes_read) => { 160 | if bytes_read == 0 { 161 | return String::new(); 162 | } 163 | let response = String::from_utf8_lossy(&buffer[..bytes_read]); 164 | if response.trim() == "ended" || response.trim() == "" { 165 | log::error!("Game ended by the other opponent"); 166 | panic!("Game ended by the other opponent"); 167 | } 168 | response.to_string() 169 | } 170 | Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { 171 | // This is expected for non-blocking sockets 172 | log::debug!("Socket not ready, would block"); 173 | String::new() 174 | } 175 | Err(e) => { 176 | log::error!("Failed to read from stream: {}", e); 177 | String::new() 178 | } 179 | } 180 | } else { 181 | String::new() 182 | } 183 | } 184 | } 185 | 186 | pub fn get_color_from_stream(mut stream: &TcpStream) -> PieceColor { 187 | let mut buffer = [0; 5]; 188 | let bytes_read = stream.read(&mut buffer).unwrap(); // Number of bytes read 189 | let color = String::from_utf8_lossy(&buffer[..bytes_read]).to_string(); 190 | 191 | match color.as_str() { 192 | "w" => PieceColor::White, 193 | "b" => PieceColor::Black, 194 | _ => panic!("Failed to get color from stream"), 195 | } 196 | } 197 | 198 | pub fn wait_for_game_start(mut stream: &TcpStream) { 199 | let mut buffer = [0; 5]; 200 | let bytes_read = stream.read(&mut buffer).unwrap(); // Number of bytes read 201 | let response = String::from_utf8_lossy(&buffer[..bytes_read]).to_string(); 202 | 203 | match response.as_str() { 204 | "s" => (), 205 | _ => panic!("Failed to get color from stream"), 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /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 | // Chess pieces structures 16 | pub mod pieces; 17 | 18 | // Game related structures 19 | pub mod game_logic; 20 | 21 | // Constats for the app 22 | pub mod constants; 23 | 24 | // Utils methods for the board 25 | pub mod utils; 26 | 27 | // Logging 28 | pub mod logging; 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "chess-tui")] 2 | extern crate chess_tui; 3 | 4 | use chess_tui::app::{App, AppResult}; 5 | use chess_tui::constants::{home_dir, DisplayMode}; 6 | use chess_tui::event::{Event, EventHandler}; 7 | use chess_tui::game_logic::game::GameState; 8 | use chess_tui::game_logic::opponent::wait_for_game_start; 9 | use chess_tui::handler::{handle_key_events, handle_mouse_events}; 10 | use chess_tui::logging; 11 | use chess_tui::ui::tui::Tui; 12 | use clap::Parser; 13 | use log::LevelFilter; 14 | use std::fs::{self, File}; 15 | use std::io::Write; 16 | use std::panic; 17 | use std::path::Path; 18 | use toml::Value; 19 | 20 | /// Simple program to greet a person 21 | #[derive(Parser, Debug)] 22 | #[command(author, version, about, long_about = None)] 23 | struct Args { 24 | /// Path for the chess engine 25 | #[arg(short, long, default_value = "")] 26 | engine_path: String, 27 | } 28 | 29 | fn main() -> AppResult<()> { 30 | // Used to enable mouse capture 31 | ratatui::crossterm::execute!( 32 | std::io::stdout(), 33 | ratatui::crossterm::event::EnableMouseCapture 34 | )?; 35 | // Parse the cli arguments 36 | let args = Args::parse(); 37 | 38 | let home_dir = home_dir()?; 39 | let folder_path = home_dir.join(".config/chess-tui"); 40 | let config_path = home_dir.join(".config/chess-tui/config.toml"); 41 | 42 | // Create the configuration file 43 | config_create(&args, &folder_path, &config_path)?; 44 | 45 | // Create an application. 46 | let mut app = App::default(); 47 | 48 | // We store the chess engine path if there is one 49 | if let Ok(content) = fs::read_to_string(config_path) { 50 | if content.trim().is_empty() { 51 | app.chess_engine_path = None; 52 | } else { 53 | let config = content.parse::().unwrap(); 54 | if let Some(engine_path) = config.get("engine_path") { 55 | app.chess_engine_path = Some(engine_path.as_str().unwrap().to_string()); 56 | } 57 | // Set the display mode based on the configuration file 58 | if let Some(display_mode) = config.get("display_mode") { 59 | app.game.ui.display_mode = match display_mode.as_str() { 60 | Some("ASCII") => DisplayMode::ASCII, 61 | _ => DisplayMode::DEFAULT, 62 | }; 63 | } 64 | // Add log level handling 65 | if let Some(log_level) = config.get("log_level") { 66 | app.log_level = log_level 67 | .as_str() 68 | .and_then(|s| s.parse().ok()) 69 | .unwrap_or(LevelFilter::Off); 70 | } 71 | } 72 | } else { 73 | println!("Error reading the file or the file does not exist"); 74 | } 75 | 76 | // Setup logging 77 | if let Err(e) = logging::setup_logging(&folder_path, &app.log_level) { 78 | eprintln!("Failed to initialize logging: {}", e); 79 | } 80 | 81 | // Initialize the terminal user interface. 82 | let terminal = ratatui::try_init()?; 83 | let events = EventHandler::new(250); 84 | let mut tui = Tui::new(terminal, events); 85 | 86 | let default_panic = std::panic::take_hook(); 87 | panic::set_hook(Box::new(move |info| { 88 | ratatui::restore(); 89 | ratatui::crossterm::execute!( 90 | std::io::stdout(), 91 | ratatui::crossterm::event::DisableMouseCapture 92 | ) 93 | .unwrap(); 94 | default_panic(info); 95 | })); 96 | 97 | // Start the main loop. 98 | while app.running { 99 | // Render the user interface. 100 | tui.draw(&mut app)?; 101 | // Handle events. 102 | match tui.events.next()? { 103 | Event::Tick => app.tick(), 104 | Event::Key(key_event) => handle_key_events(key_event, &mut app)?, 105 | Event::Mouse(mouse_event) => handle_mouse_events(mouse_event, &mut app)?, 106 | Event::Resize(_, _) => {} 107 | } 108 | if app.game.bot.is_some() && app.game.bot.as_ref().is_some_and(|bot| bot.bot_will_move) { 109 | app.game.execute_bot_move(); 110 | app.game.switch_player_turn(); 111 | if let Some(bot) = app.game.bot.as_mut() { 112 | bot.bot_will_move = false; 113 | } 114 | // need to be centralised 115 | if app.game.game_board.is_checkmate(app.game.player_turn) { 116 | app.game.game_state = GameState::Checkmate; 117 | } else if app.game.game_board.is_draw(app.game.player_turn) { 118 | app.game.game_state = GameState::Draw; 119 | } 120 | tui.draw(&mut app)?; 121 | } 122 | 123 | if app.game.opponent.is_some() 124 | && app 125 | .game 126 | .opponent 127 | .as_ref() 128 | .is_some_and(|opponent| !opponent.game_started) 129 | { 130 | let opponent = app.game.opponent.as_mut().unwrap(); 131 | wait_for_game_start(opponent.stream.as_ref().unwrap()); 132 | opponent.game_started = true; 133 | app.current_popup = None; 134 | } 135 | 136 | // If it's the opponent turn, wait for the opponent to move 137 | if app.game.opponent.is_some() 138 | && app 139 | .game 140 | .opponent 141 | .as_ref() 142 | .is_some_and(|opponent| opponent.opponent_will_move) 143 | { 144 | tui.draw(&mut app)?; 145 | 146 | if !app.game.game_board.is_checkmate(app.game.player_turn) 147 | && !app.game.game_board.is_draw(app.game.player_turn) 148 | { 149 | app.game.execute_opponent_move(); 150 | app.game.switch_player_turn(); 151 | } 152 | 153 | // need to be centralised 154 | if app.game.game_board.is_checkmate(app.game.player_turn) { 155 | app.game.game_state = GameState::Checkmate; 156 | } else if app.game.game_board.is_draw(app.game.player_turn) { 157 | app.game.game_state = GameState::Draw; 158 | } 159 | tui.draw(&mut app)?; 160 | } 161 | } 162 | 163 | // Exit the user interface. 164 | ratatui::try_restore()?; 165 | // Free up the mouse, otherwise it will remain linked to the terminal 166 | ratatui::crossterm::execute!( 167 | std::io::stdout(), 168 | ratatui::crossterm::event::DisableMouseCapture 169 | )?; 170 | 171 | Ok(()) 172 | } 173 | 174 | fn config_create(args: &Args, folder_path: &Path, config_path: &Path) -> AppResult<()> { 175 | std::fs::create_dir_all(folder_path)?; 176 | 177 | if !config_path.exists() { 178 | //write to console 179 | File::create(config_path)?; 180 | } 181 | 182 | // Attempt to read the configuration file and parse it as a TOML Value. 183 | // If we encounter any issues (like the file not being readable or not being valid TOML), we start with a new, empty TOML table instead. 184 | let mut config = match fs::read_to_string(config_path) { 185 | Ok(content) => content 186 | .parse::() 187 | .unwrap_or_else(|_| Value::Table(Default::default())), 188 | Err(_) => Value::Table(Default::default()), 189 | }; 190 | 191 | // We update the configuration with the engine_path and display_mode. 192 | // If these keys are already in the configuration, we leave them as they are. 193 | // If they're not, we add them with default values. 194 | if let Some(table) = config.as_table_mut() { 195 | // Only update the engine_path in the configuration if it's not empty 196 | if args.engine_path.is_empty() { 197 | table 198 | .entry("engine_path".to_string()) 199 | .or_insert(Value::String(String::new())); 200 | } else { 201 | table.insert( 202 | "engine_path".to_string(), 203 | Value::String(args.engine_path.clone()), 204 | ); 205 | } 206 | table 207 | .entry("display_mode".to_string()) 208 | .or_insert(Value::String("DEFAULT".to_string())); 209 | table 210 | .entry("log_level".to_string()) 211 | .or_insert(Value::String(LevelFilter::Off.to_string())); 212 | } 213 | 214 | let mut file = File::create(config_path)?; 215 | file.write_all(config.to_string().as_bytes())?; 216 | 217 | Ok(()) 218 | } 219 | 220 | #[cfg(test)] 221 | mod tests { 222 | use super::*; 223 | use std::fs; 224 | use toml::Value; 225 | 226 | #[test] 227 | fn test_config_create() { 228 | let args = Args { 229 | engine_path: "test_engine_path".to_string(), 230 | }; 231 | 232 | let home_dir = home_dir().expect("Failed to get home directory"); 233 | let folder_path = home_dir.join(".test/chess-tui"); 234 | let config_path = home_dir.join(".test/chess-tui/config.toml"); 235 | 236 | let result = config_create(&args, &folder_path, &config_path); 237 | 238 | assert!(result.is_ok()); 239 | assert!(config_path.exists()); 240 | 241 | let content = fs::read_to_string(config_path).unwrap(); 242 | let config: Value = content.parse().unwrap(); 243 | let table = config.as_table().unwrap(); 244 | 245 | assert_eq!( 246 | table.get("engine_path").unwrap().as_str().unwrap(), 247 | "test_engine_path" 248 | ); 249 | assert_eq!( 250 | table.get("display_mode").unwrap().as_str().unwrap(), 251 | "DEFAULT" 252 | ); 253 | let removed = fs::remove_dir_all(home_dir.join(".test")); 254 | assert!(removed.is_ok()); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/pieces/bishop.rs: -------------------------------------------------------------------------------- 1 | use super::{Movable, PieceColor, Position}; 2 | use crate::constants::DisplayMode; 3 | use crate::game_logic::coord::Coord; 4 | use crate::game_logic::game_board::GameBoard; 5 | use crate::utils::{cleaned_positions, is_cell_color_ally, is_piece_opposite_king}; 6 | pub struct Bishop; 7 | 8 | impl Movable for Bishop { 9 | fn piece_move( 10 | coordinates: &Coord, 11 | color: PieceColor, 12 | game_board: &GameBoard, 13 | allow_move_on_ally_positions: bool, 14 | ) -> Vec { 15 | let mut positions: Vec = vec![]; 16 | 17 | let y = coordinates.row; 18 | let x = coordinates.col; 19 | 20 | // for diagonal from piece to top left 21 | for i in 1..8u8 { 22 | let new_x: i8 = x as i8 - i as i8; 23 | let new_y: i8 = y as i8 - i as i8; 24 | let Some(new_coordinates) = Coord::opt_new(new_y, new_x) else { 25 | break; 26 | }; 27 | 28 | // Empty cell 29 | if game_board.get_piece_color(&new_coordinates).is_none() { 30 | positions.push(new_coordinates); 31 | continue; 32 | } 33 | // Ally cell 34 | if is_cell_color_ally(game_board, &new_coordinates, color) { 35 | if !allow_move_on_ally_positions { 36 | break; 37 | } 38 | positions.push(new_coordinates); 39 | break; 40 | } 41 | 42 | // Enemy cell 43 | positions.push(new_coordinates); 44 | if !allow_move_on_ally_positions 45 | || !is_piece_opposite_king(game_board.board[new_y as usize][new_x as usize], color) 46 | { 47 | break; 48 | } 49 | } 50 | 51 | // for diagonal from piece to bottom right 52 | for i in 1..8u8 { 53 | let new_x = x + i; 54 | let new_y = y + i; 55 | 56 | let new_coordinates = Coord::new(new_y, new_x); 57 | 58 | // Invalid coords 59 | if !new_coordinates.is_valid() { 60 | break; 61 | } 62 | 63 | // Empty cell 64 | if game_board.get_piece_color(&new_coordinates).is_none() { 65 | positions.push(new_coordinates); 66 | continue; 67 | } 68 | // Ally cell 69 | if is_cell_color_ally(game_board, &new_coordinates, color) { 70 | if !allow_move_on_ally_positions { 71 | break; 72 | } 73 | positions.push(new_coordinates); 74 | break; 75 | } 76 | 77 | // Enemy cell 78 | positions.push(new_coordinates); 79 | if !allow_move_on_ally_positions 80 | || !is_piece_opposite_king(game_board.board[new_y as usize][new_x as usize], color) 81 | { 82 | break; 83 | } 84 | } 85 | 86 | // for diagonal from piece to bottom left 87 | for i in 1..8u8 { 88 | let new_x: i8 = x as i8 - i as i8; 89 | let new_y: i8 = y as i8 + i as i8; 90 | let Some(new_coordinates) = Coord::opt_new(new_y, new_x) else { 91 | break; 92 | }; 93 | 94 | // Invalid coords 95 | if !new_coordinates.is_valid() { 96 | break; 97 | } 98 | 99 | // Empty cell 100 | if game_board.get_piece_color(&new_coordinates).is_none() { 101 | positions.push(new_coordinates); 102 | continue; 103 | } 104 | // Ally cell 105 | if is_cell_color_ally(game_board, &new_coordinates, color) { 106 | if !allow_move_on_ally_positions { 107 | break; 108 | } 109 | positions.push(new_coordinates); 110 | break; 111 | } 112 | 113 | // Enemy cell 114 | positions.push(new_coordinates); 115 | if !allow_move_on_ally_positions 116 | || !is_piece_opposite_king(game_board.board[new_y as usize][new_x as usize], color) 117 | { 118 | break; 119 | } 120 | } 121 | 122 | // for diagonal from piece to top right 123 | for i in 1..8u8 { 124 | let new_x = x as i8 + i as i8; 125 | let new_y = y as i8 - i as i8; 126 | let Some(new_coordinates) = Coord::opt_new(new_y, new_x) else { 127 | break; 128 | }; 129 | 130 | // Empty cell 131 | if game_board.get_piece_color(&new_coordinates).is_none() { 132 | positions.push(new_coordinates); 133 | continue; 134 | } 135 | // Ally cell 136 | if is_cell_color_ally(game_board, &new_coordinates, color) { 137 | if !allow_move_on_ally_positions { 138 | break; 139 | } 140 | positions.push(new_coordinates); 141 | break; 142 | } 143 | 144 | // Enemy cell 145 | positions.push(new_coordinates); 146 | if !allow_move_on_ally_positions 147 | || !is_piece_opposite_king( 148 | game_board.board[new_coordinates.row as usize][new_coordinates.col as usize], 149 | color, 150 | ) 151 | { 152 | break; 153 | } 154 | } 155 | cleaned_positions(&positions) 156 | } 157 | } 158 | 159 | impl Position for Bishop { 160 | fn authorized_positions( 161 | coordinates: &Coord, 162 | color: PieceColor, 163 | game_board: &GameBoard, 164 | _is_king_checked: bool, 165 | ) -> Vec { 166 | // if the king is checked we clean all the position not resolving the check 167 | game_board.impossible_positions_king_checked( 168 | coordinates, 169 | Self::piece_move(coordinates, color, game_board, false), 170 | color, 171 | ) 172 | } 173 | fn protected_positions( 174 | coordinates: &Coord, 175 | color: PieceColor, 176 | game_board: &GameBoard, 177 | ) -> Vec { 178 | Self::piece_move(coordinates, color, game_board, true) 179 | } 180 | } 181 | 182 | impl Bishop { 183 | pub fn to_string(display_mode: &DisplayMode) -> &'static str { 184 | match display_mode { 185 | DisplayMode::DEFAULT => { 186 | "\ 187 | \n\ 188 | ⭘\n\ 189 | █✝█\n\ 190 | ███\n\ 191 | ▗█████▖\n\ 192 | " 193 | } 194 | DisplayMode::ASCII => "B", 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/pieces/king.rs: -------------------------------------------------------------------------------- 1 | use super::{Movable, PieceColor, PieceType, Position}; 2 | use crate::constants::DisplayMode; 3 | use crate::game_logic::coord::Coord; 4 | use crate::game_logic::game_board::GameBoard; 5 | use crate::utils::{cleaned_positions, is_cell_color_ally}; 6 | pub struct King; 7 | 8 | impl Movable for King { 9 | fn piece_move( 10 | coordinates: &Coord, 11 | color: PieceColor, 12 | game_board: &GameBoard, 13 | allow_move_on_ally_positions: bool, 14 | ) -> Vec { 15 | let mut positions: Vec = vec![]; 16 | let y = coordinates.row; 17 | let x = coordinates.col; 18 | 19 | // can move on a complete row 20 | // Generate positions in all eight possible directions 21 | for &dy in &[-1i8, 0, 1] { 22 | for &dx in &[-1i8, 0, 1] { 23 | // Skip the case where both dx and dy are zero (the current position) 24 | let new_x = x as i8 + dx; 25 | let new_y = y as i8 + dy; 26 | 27 | let new_coordinates = Coord::new(new_y as u8, new_x as u8); 28 | if new_coordinates.is_valid() 29 | && (!is_cell_color_ally(game_board, &new_coordinates, color) 30 | || allow_move_on_ally_positions) 31 | { 32 | positions.push(new_coordinates); 33 | } 34 | } 35 | } 36 | 37 | cleaned_positions(&positions) 38 | } 39 | } 40 | 41 | impl Position for King { 42 | fn authorized_positions( 43 | coordinates: &Coord, 44 | color: PieceColor, 45 | game_board: &GameBoard, 46 | is_king_checked: bool, 47 | ) -> Vec { 48 | let mut positions: Vec = vec![]; 49 | let checked_cells = game_board.get_all_protected_cells(color); 50 | 51 | let rook_big_castle_x = 0; 52 | let rook_small_castle_x = 7; 53 | let king_row = 7; 54 | let king_col = if color == PieceColor::White { 4 } else { 3 }; 55 | 56 | // We check the condition for small and big castling 57 | if !game_board.did_piece_already_move(( 58 | Some(PieceType::King), 59 | Some(color), 60 | Coord::new(king_row, king_col), 61 | )) && !is_king_checked 62 | { 63 | // We check if there is no pieces between tower and king 64 | // Big castle check 65 | if !game_board.did_piece_already_move(( 66 | Some(PieceType::Rook), 67 | Some(color), 68 | Coord::new(king_row, rook_big_castle_x), 69 | )) && King::check_castling_condition( 70 | game_board, 71 | color, 72 | 0, 73 | king_col as i8 - 1, 74 | &checked_cells, 75 | ) { 76 | positions.push(Coord::new(king_row, 0)); 77 | } 78 | // Small castle check 79 | if !game_board.did_piece_already_move(( 80 | Some(PieceType::Rook), 81 | Some(color), 82 | Coord::new(king_row, rook_small_castle_x), 83 | )) && King::check_castling_condition( 84 | game_board, 85 | color, 86 | king_col as i8 + 1, 87 | 7, 88 | &checked_cells, 89 | ) { 90 | positions.push(Coord::new(king_row, 7)); 91 | } 92 | } 93 | 94 | // Here we only want king positions that are not in impossible (already checked) 95 | let king_cells = King::piece_move(coordinates, color, game_board, false); 96 | 97 | for king_position in king_cells.clone() { 98 | if !checked_cells.contains(&king_position) { 99 | positions.push(king_position); 100 | } 101 | } 102 | 103 | positions 104 | } 105 | 106 | // This method is used to calculated the cells the king is actually covering and is used when the other king authorized position is called 107 | fn protected_positions( 108 | coordinates: &Coord, 109 | color: PieceColor, 110 | game_board: &GameBoard, 111 | ) -> Vec { 112 | Self::piece_move(coordinates, color, game_board, true) 113 | } 114 | } 115 | 116 | impl King { 117 | pub fn to_string(display_mode: &DisplayMode) -> &'static str { 118 | match display_mode { 119 | DisplayMode::DEFAULT => { 120 | "\ 121 | ✚\n\ 122 | ▞▀▄▀▚\n\ 123 | ▙▄█▄▟\n\ 124 | ▐███▌\n\ 125 | ▗█████▖\n\ 126 | " 127 | } 128 | DisplayMode::ASCII => "K", 129 | } 130 | } 131 | 132 | // Check if nothing is in between the king and a rook and if none of those cells are getting checked 133 | pub fn check_castling_condition( 134 | game_board: &GameBoard, 135 | color: PieceColor, 136 | start: i8, 137 | end: i8, 138 | checked_cells: &[Coord], 139 | ) -> bool { 140 | let king_row = 7; 141 | 142 | let mut valid_for_castling = true; 143 | 144 | for i in start..=end { 145 | let new_coordinates = Coord::new(king_row, i as u8); 146 | 147 | if checked_cells.contains(&new_coordinates) { 148 | valid_for_castling = false; 149 | } 150 | if (i == 7 || i == 0) 151 | && (game_board.get_piece_type(&new_coordinates) != Some(PieceType::Rook) 152 | || !is_cell_color_ally(game_board, &new_coordinates, color)) 153 | || (i != 7 && i != 0 && game_board.get_piece_type(&new_coordinates).is_some()) 154 | { 155 | valid_for_castling = false; 156 | } 157 | } 158 | 159 | valid_for_castling 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/pieces/knight.rs: -------------------------------------------------------------------------------- 1 | use super::{Movable, PieceColor, Position}; 2 | use crate::constants::DisplayMode; 3 | use crate::game_logic::coord::Coord; 4 | use crate::game_logic::game_board::GameBoard; 5 | use crate::utils::{cleaned_positions, is_cell_color_ally}; 6 | pub struct Knight; 7 | 8 | impl Movable for Knight { 9 | fn piece_move( 10 | coordinates: &Coord, 11 | color: PieceColor, 12 | game_board: &GameBoard, 13 | allow_move_on_ally_positions: bool, 14 | ) -> Vec { 15 | let mut positions: Vec = Vec::new(); 16 | 17 | // Generate knight positions in all eight possible L-shaped moves 18 | let piece_move: [(i8, i8); 8] = [ 19 | (-2, -1), 20 | (-2, 1), 21 | (-1, -2), 22 | (-1, 2), 23 | (1, -2), 24 | (1, 2), 25 | (2, -1), 26 | (2, 1), 27 | ]; 28 | 29 | for &(dy, dx) in &piece_move { 30 | let Some(new_coordinates) = 31 | Coord::opt_new(coordinates.row as i8 + dy, coordinates.col as i8 + dx) 32 | else { 33 | continue; 34 | }; 35 | 36 | if is_cell_color_ally(game_board, &new_coordinates, color) 37 | && !allow_move_on_ally_positions 38 | { 39 | continue; 40 | } 41 | 42 | positions.push(new_coordinates); 43 | } 44 | 45 | cleaned_positions(&positions) 46 | } 47 | } 48 | 49 | impl Position for Knight { 50 | fn authorized_positions( 51 | coordinates: &Coord, 52 | color: PieceColor, 53 | game_board: &GameBoard, 54 | _is_king_checked: bool, 55 | ) -> Vec { 56 | game_board.impossible_positions_king_checked( 57 | coordinates, 58 | Self::piece_move(coordinates, color, game_board, false), 59 | color, 60 | ) 61 | } 62 | 63 | fn protected_positions( 64 | coordinates: &Coord, 65 | color: PieceColor, 66 | game_board: &GameBoard, 67 | ) -> Vec { 68 | Self::piece_move(coordinates, color, game_board, true) 69 | } 70 | } 71 | 72 | impl Knight { 73 | pub fn to_string(display_mode: &DisplayMode) -> &'static str { 74 | match display_mode { 75 | DisplayMode::DEFAULT => { 76 | "\ 77 | \n\ 78 | ▟▛██▙\n\ 79 | ▟█████\n\ 80 | ▀▀▟██▌\n\ 81 | ▟████\n\ 82 | " 83 | } 84 | DisplayMode::ASCII => "N", 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/pieces/mod.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | 3 | use self::{bishop::Bishop, king::King, knight::Knight, pawn::Pawn, queen::Queen, rook::Rook}; 4 | use super::constants::DisplayMode; 5 | use crate::game_logic::{coord::Coord, game_board::GameBoard}; 6 | 7 | pub mod bishop; 8 | pub mod king; 9 | pub mod knight; 10 | pub mod pawn; 11 | pub mod queen; 12 | pub mod rook; 13 | 14 | /// The different type of pieces in the game 15 | #[derive(Debug, Copy, Clone, PartialEq, Hash)] 16 | pub enum PieceType { 17 | Pawn, 18 | Rook, 19 | Bishop, 20 | Queen, 21 | King, 22 | Knight, 23 | } 24 | 25 | impl PieceType { 26 | /// The authorized position for a piece at a certain coordinate 27 | pub fn authorized_positions( 28 | self, 29 | coordinates: &Coord, 30 | color: PieceColor, 31 | game_board: &GameBoard, 32 | is_king_checked: bool, 33 | ) -> Vec { 34 | match self { 35 | PieceType::Pawn => { 36 | Pawn::authorized_positions(coordinates, color, game_board, is_king_checked) 37 | } 38 | PieceType::Rook => { 39 | Rook::authorized_positions(coordinates, color, game_board, is_king_checked) 40 | } 41 | PieceType::Bishop => { 42 | Bishop::authorized_positions(coordinates, color, game_board, is_king_checked) 43 | } 44 | PieceType::Queen => { 45 | Queen::authorized_positions(coordinates, color, game_board, is_king_checked) 46 | } 47 | PieceType::King => { 48 | King::authorized_positions(coordinates, color, game_board, is_king_checked) 49 | } 50 | PieceType::Knight => { 51 | Knight::authorized_positions(coordinates, color, game_board, is_king_checked) 52 | } 53 | } 54 | } 55 | 56 | /// The cells a given piece is protecting 57 | pub fn protected_positions( 58 | selected_coordinates: &Coord, 59 | piece_type: PieceType, 60 | color: PieceColor, 61 | game_board: &GameBoard, 62 | ) -> Vec { 63 | match piece_type { 64 | PieceType::Pawn => Pawn::protected_positions(selected_coordinates, color, game_board), 65 | PieceType::Rook => Rook::protected_positions(selected_coordinates, color, game_board), 66 | PieceType::Bishop => { 67 | Bishop::protected_positions(selected_coordinates, color, game_board) 68 | } 69 | PieceType::Queen => Queen::protected_positions(selected_coordinates, color, game_board), 70 | PieceType::King => King::protected_positions(selected_coordinates, color, game_board), 71 | PieceType::Knight => { 72 | Knight::protected_positions(selected_coordinates, color, game_board) 73 | } 74 | } 75 | } 76 | 77 | /// Convert a PieceType to a symbol 78 | pub fn piece_to_utf_enum( 79 | piece_type: &PieceType, 80 | piece_color: Option, 81 | ) -> &'static str { 82 | match (piece_type, piece_color) { 83 | (PieceType::Queen, Some(PieceColor::Black)) => "♕", 84 | (PieceType::Queen, Some(PieceColor::White)) => "♛", 85 | (PieceType::King, Some(PieceColor::Black)) => "♔", 86 | (PieceType::King, Some(PieceColor::White)) => "♚", 87 | (PieceType::Rook, Some(PieceColor::Black)) => "♖", 88 | (PieceType::Rook, Some(PieceColor::White)) => "♜", 89 | (PieceType::Bishop, Some(PieceColor::Black)) => "♗", 90 | (PieceType::Bishop, Some(PieceColor::White)) => "♝", 91 | (PieceType::Knight, Some(PieceColor::Black)) => "♘", 92 | (PieceType::Knight, Some(PieceColor::White)) => "♞", 93 | (PieceType::Pawn, Some(PieceColor::Black)) => "♙", 94 | (PieceType::Pawn, Some(PieceColor::White)) => "♟", 95 | _ => "NONE", 96 | } 97 | } 98 | 99 | /// Convert a PieceType fo a conform fen character 100 | pub fn piece_to_fen_enum( 101 | piece_type: Option, 102 | piece_color: Option, 103 | ) -> &'static str { 104 | match (piece_type, piece_color) { 105 | (Some(PieceType::Queen), Some(PieceColor::Black)) => "q", 106 | (Some(PieceType::Queen), Some(PieceColor::White)) => "Q", 107 | (Some(PieceType::King), Some(PieceColor::Black)) => "k", 108 | (Some(PieceType::King), Some(PieceColor::White)) => "K", 109 | (Some(PieceType::Rook), Some(PieceColor::Black)) => "r", 110 | (Some(PieceType::Rook), Some(PieceColor::White)) => "R", 111 | (Some(PieceType::Bishop), Some(PieceColor::Black)) => "b", 112 | (Some(PieceType::Bishop), Some(PieceColor::White)) => "B", 113 | (Some(PieceType::Knight), Some(PieceColor::Black)) => "n", 114 | (Some(PieceType::Knight), Some(PieceColor::White)) => "N", 115 | (Some(PieceType::Pawn), Some(PieceColor::Black)) => "p", 116 | (Some(PieceType::Pawn), Some(PieceColor::White)) => "P", 117 | (None, None) => "", 118 | _ => unreachable!("Undefined piece and piece color tuple"), 119 | } 120 | } 121 | 122 | pub fn piece_type_to_string_enum( 123 | piece_type: Option, 124 | display_mode: &DisplayMode, 125 | ) -> &'static str { 126 | match piece_type { 127 | Some(PieceType::Queen) => Queen::to_string(display_mode), 128 | Some(PieceType::King) => King::to_string(display_mode), 129 | Some(PieceType::Rook) => Rook::to_string(display_mode), 130 | Some(PieceType::Bishop) => Bishop::to_string(display_mode), 131 | Some(PieceType::Knight) => Knight::to_string(display_mode), 132 | Some(PieceType::Pawn) => Pawn::to_string(display_mode), 133 | None => " ", 134 | } 135 | } 136 | } 137 | 138 | /// Implementing the PartialOrd trait for PieceType to allow comparison between PieceType 139 | #[allow(clippy::non_canonical_partial_ord_impl)] 140 | impl PartialOrd for PieceType { 141 | fn partial_cmp(&self, other: &Self) -> Option { 142 | if self == other { 143 | return Some(Ordering::Equal); 144 | } 145 | match (self, other) { 146 | (PieceType::Pawn, _) => Some(Ordering::Less), 147 | (PieceType::Queen, _) => Some(Ordering::Greater), 148 | (_, PieceType::Pawn) => Some(Ordering::Greater), 149 | (_, PieceType::Queen) => Some(Ordering::Less), 150 | (PieceType::Rook, PieceType::Bishop) => Some(Ordering::Greater), 151 | (PieceType::Rook, PieceType::Knight) => Some(Ordering::Greater), 152 | (PieceType::Bishop, PieceType::Knight) => Some(Ordering::Greater), // just for visual purpose 153 | (PieceType::Bishop, PieceType::Rook) => Some(Ordering::Less), 154 | (PieceType::Knight, PieceType::Rook) => Some(Ordering::Less), 155 | (PieceType::Knight, PieceType::Bishop) => Some(Ordering::Less), // just for visual purpose 156 | _ => Some(Ordering::Equal), 157 | } 158 | } 159 | } 160 | impl Ord for PieceType { 161 | fn cmp(&self, other: &Self) -> Ordering { 162 | self.partial_cmp(other).unwrap() 163 | } 164 | } 165 | 166 | impl Eq for PieceType {} 167 | 168 | #[derive(Debug, Copy, Clone, PartialEq)] 169 | pub struct PieceMove { 170 | pub piece_type: PieceType, 171 | pub piece_color: PieceColor, 172 | pub from: Coord, 173 | pub to: Coord, 174 | } 175 | 176 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] 177 | pub enum PieceColor { 178 | White = 0, 179 | Black = 1, 180 | } 181 | 182 | impl PieceColor { 183 | pub fn opposite(self) -> Self { 184 | match self { 185 | Self::Black => Self::White, 186 | Self::White => Self::Black, 187 | } 188 | } 189 | } 190 | 191 | pub trait Movable { 192 | fn piece_move( 193 | coordinates: &Coord, 194 | color: PieceColor, 195 | game_board: &GameBoard, 196 | allow_move_on_ally_positions: bool, 197 | ) -> Vec; 198 | } 199 | 200 | pub trait Position { 201 | fn authorized_positions( 202 | coordinates: &Coord, 203 | color: PieceColor, 204 | game_board: &GameBoard, 205 | is_king_checked: bool, 206 | ) -> Vec; 207 | 208 | fn protected_positions( 209 | coordinates: &Coord, 210 | color: PieceColor, 211 | game_board: &GameBoard, 212 | ) -> Vec; 213 | } 214 | -------------------------------------------------------------------------------- /src/pieces/pawn.rs: -------------------------------------------------------------------------------- 1 | use super::{Movable, PieceColor, PieceMove, PieceType, Position}; 2 | use crate::constants::DisplayMode; 3 | use crate::game_logic::coord::Coord; 4 | use crate::game_logic::game_board::GameBoard; 5 | use crate::utils::{cleaned_positions, invert_position, is_cell_color_ally}; 6 | 7 | pub struct Pawn; 8 | 9 | impl Movable for Pawn { 10 | fn piece_move( 11 | coordinates: &Coord, 12 | color: PieceColor, 13 | game_board: &GameBoard, 14 | allow_move_on_ally_positions: bool, 15 | ) -> Vec { 16 | // Pawns can only move in one direction depending of their color 17 | // -1 we go up 18 | let direction: i8 = if allow_move_on_ally_positions { 1 } else { -1 }; 19 | 20 | let mut positions: Vec = vec![]; 21 | 22 | let (y, x) = (coordinates.row, coordinates.col); 23 | 24 | // move one in front 25 | let new_x_front_one = x; 26 | let new_y_front_one = y as i8 + direction; 27 | let new_coordinates_front_one = Coord::new(new_y_front_one as u8, new_x_front_one); 28 | 29 | if new_coordinates_front_one.is_valid() 30 | && !allow_move_on_ally_positions 31 | && game_board 32 | .get_piece_color(&new_coordinates_front_one) 33 | .is_none() 34 | { 35 | // Empty cell 36 | positions.push(new_coordinates_front_one); 37 | 38 | // move front a second cell 39 | let new_x_front_two = x; 40 | let new_y_front_two = y as i8 + direction * 2; 41 | let new_coordinates_front_two = Coord::new(new_y_front_two as u8, new_x_front_two); 42 | 43 | if new_coordinates_front_two.is_valid() 44 | && game_board 45 | .get_piece_color(&new_coordinates_front_two) 46 | .is_none() 47 | && (y == 6) 48 | { 49 | positions.push(new_coordinates_front_two); 50 | } 51 | } 52 | 53 | // check for enemy piece on the right 54 | let new_x_right = x + 1; 55 | let new_y_right = y as i8 + direction; 56 | let new_coordinates_right = 57 | if let Some(new_coord) = Coord::opt_new(new_y_right, new_x_right) { 58 | new_coord 59 | } else { 60 | Coord::undefined() 61 | }; 62 | 63 | // check for enemy piece on the left 64 | let new_x_left = x as i8 - 1; 65 | let new_y_left = y as i8 + direction; 66 | let new_coordinates_left = if let Some(new_coord) = Coord::opt_new(new_y_left, new_x_left) { 67 | new_coord 68 | } else { 69 | Coord::undefined() 70 | }; 71 | 72 | // If we allow on ally position we push it anyway 73 | 74 | if allow_move_on_ally_positions { 75 | if new_coordinates_right.is_valid() { 76 | positions.push(new_coordinates_right); 77 | }; 78 | if new_coordinates_left.is_valid() { 79 | positions.push(new_coordinates_left); 80 | }; 81 | } else { 82 | // else we check if it's an ally piece 83 | if new_coordinates_right.is_valid() 84 | && game_board.get_piece_color(&new_coordinates_right).is_some() 85 | && !is_cell_color_ally(game_board, &new_coordinates_right, color) 86 | { 87 | positions.push(new_coordinates_right); 88 | } 89 | if new_coordinates_left.is_valid() 90 | && game_board.get_piece_color(&new_coordinates_left).is_some() 91 | && !is_cell_color_ally(game_board, &new_coordinates_left, color) 92 | { 93 | positions.push(new_coordinates_left); 94 | } 95 | } 96 | 97 | // We check for en passant 98 | if let Some(latest_move) = game_board.move_history.last() { 99 | let number_of_cells_move = latest_move.to.row as i8 - latest_move.from.row as i8; 100 | 101 | let last_coords = invert_position(&Coord::new(latest_move.to.row, latest_move.to.col)); 102 | // We check if the latest move was on the right start cell 103 | // if it moved 2 cells 104 | // and if the current pawn is next to this pawn latest position 105 | if latest_move.piece_type == PieceType::Pawn 106 | && number_of_cells_move == -2 107 | && y == last_coords.row 108 | && (x as i8 == (last_coords.col as i8) - 1 || x == last_coords.col + 1) 109 | { 110 | let new_y = y - 1; 111 | let new_x = last_coords.col; 112 | positions.push(Coord::new(new_y, new_x)); 113 | } 114 | } 115 | cleaned_positions(&positions) 116 | } 117 | } 118 | 119 | impl Position for Pawn { 120 | fn authorized_positions( 121 | coordinates: &Coord, 122 | color: PieceColor, 123 | game_board: &GameBoard, 124 | _is_king_checked: bool, 125 | ) -> Vec { 126 | // If the king is not checked we get then normal moves 127 | // if the king is checked we clean all the position not resolving the check 128 | game_board.impossible_positions_king_checked( 129 | coordinates, 130 | Self::piece_move(coordinates, color, game_board, false), 131 | color, 132 | ) 133 | } 134 | 135 | fn protected_positions( 136 | coordinates: &Coord, 137 | color: PieceColor, 138 | game_board: &GameBoard, 139 | ) -> Vec { 140 | Self::piece_move(coordinates, color, game_board, true) 141 | } 142 | } 143 | 144 | impl Pawn { 145 | pub fn to_string(display_mode: &DisplayMode) -> &'static str { 146 | match display_mode { 147 | DisplayMode::DEFAULT => { 148 | "\ 149 | \n\ 150 | \n\ 151 | ▟█▙\n\ 152 | ▜█▛\n\ 153 | ▟███▙\n\ 154 | " 155 | } 156 | DisplayMode::ASCII => "P", 157 | } 158 | } 159 | 160 | // Check if the pawn moved two cells (used for en passant) 161 | pub fn did_pawn_move_two_cells(last_move: Option<&PieceMove>) -> bool { 162 | match last_move { 163 | Some(last_move) => { 164 | let distance = (last_move.to.row as i8 - last_move.from.row as i8).abs(); 165 | 166 | if last_move.piece_type == PieceType::Pawn && distance == 2 { 167 | return true; 168 | } 169 | false 170 | } 171 | _ => false, 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/pieces/queen.rs: -------------------------------------------------------------------------------- 1 | use super::rook::Rook; 2 | use super::{Movable, PieceColor, Position}; 3 | use crate::constants::DisplayMode; 4 | use crate::game_logic::coord::Coord; 5 | use crate::game_logic::game_board::GameBoard; 6 | use crate::pieces::bishop::Bishop; 7 | use crate::utils::cleaned_positions; 8 | 9 | pub struct Queen; 10 | 11 | impl Movable for Queen { 12 | fn piece_move( 13 | coordinates: &Coord, 14 | color: PieceColor, 15 | game_board: &GameBoard, 16 | allow_move_on_ally_positions: bool, 17 | ) -> Vec { 18 | let mut positions = vec![]; 19 | 20 | // Queen = bishop concat rook 21 | positions.extend(Bishop::piece_move( 22 | coordinates, 23 | color, 24 | game_board, 25 | allow_move_on_ally_positions, 26 | )); 27 | positions.extend(Rook::piece_move( 28 | coordinates, 29 | color, 30 | game_board, 31 | allow_move_on_ally_positions, 32 | )); 33 | 34 | cleaned_positions(&positions) 35 | } 36 | } 37 | 38 | impl Position for Queen { 39 | fn authorized_positions( 40 | coordinates: &Coord, 41 | color: PieceColor, 42 | game_board: &GameBoard, 43 | _is_king_checked: bool, 44 | ) -> Vec { 45 | game_board.impossible_positions_king_checked( 46 | coordinates, 47 | Self::piece_move(coordinates, color, game_board, false), 48 | color, 49 | ) 50 | } 51 | fn protected_positions( 52 | coordinates: &Coord, 53 | color: PieceColor, 54 | game_board: &GameBoard, 55 | ) -> Vec { 56 | Self::piece_move(coordinates, color, game_board, true) 57 | } 58 | } 59 | 60 | impl Queen { 61 | pub fn to_string(display_mode: &DisplayMode) -> &'static str { 62 | match display_mode { 63 | DisplayMode::DEFAULT => { 64 | "\ 65 | \n\ 66 | ◀█▟█▙█▶\n\ 67 | ◥█◈█◤\n\ 68 | ███\n\ 69 | ▗█████▖\n\ 70 | " 71 | } 72 | DisplayMode::ASCII => "Q", 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/pieces/rook.rs: -------------------------------------------------------------------------------- 1 | use super::{Movable, PieceColor, Position}; 2 | use crate::constants::DisplayMode; 3 | use crate::game_logic::coord::Coord; 4 | use crate::game_logic::game_board::GameBoard; 5 | use crate::utils::{cleaned_positions, is_cell_color_ally, is_piece_opposite_king}; 6 | pub struct Rook; 7 | 8 | impl Movable for Rook { 9 | fn piece_move( 10 | coordinates: &Coord, 11 | color: PieceColor, 12 | game_board: &GameBoard, 13 | allow_move_on_ally_positions: bool, 14 | ) -> Vec { 15 | // Pawns can only move in one direction depending on their color 16 | let mut positions = vec![]; 17 | 18 | let (y, x) = (coordinates.row, coordinates.col); 19 | 20 | // RIGHT ROW 21 | for i in 1..8u8 { 22 | let new_x = x + i; 23 | let new_y = y; 24 | let new_coordinates = Coord::new(new_y, new_x); 25 | 26 | // Invalid coords 27 | if !new_coordinates.is_valid() { 28 | break; 29 | } 30 | 31 | // Empty cell 32 | if game_board.get_piece_color(&new_coordinates).is_none() { 33 | positions.push(new_coordinates); 34 | continue; 35 | } 36 | // Ally cell 37 | if is_cell_color_ally(game_board, &new_coordinates, color) { 38 | if !allow_move_on_ally_positions { 39 | break; 40 | } 41 | positions.push(new_coordinates); 42 | break; 43 | } 44 | 45 | // Enemy cell 46 | positions.push(new_coordinates); 47 | if !allow_move_on_ally_positions 48 | || !is_piece_opposite_king(game_board.board[new_y as usize][new_x as usize], color) 49 | { 50 | break; 51 | } 52 | } 53 | 54 | // LEFT ROW 55 | for i in 1..=8 { 56 | let new_x: i8 = x as i8 - i as i8; 57 | let new_y: i8 = y as i8; 58 | let Some(new_coordinates) = Coord::opt_new(new_y, new_x) else { 59 | break; 60 | }; 61 | 62 | // Empty piece 63 | if game_board.get_piece_color(&new_coordinates).is_none() { 64 | positions.push(new_coordinates); 65 | continue; 66 | } 67 | 68 | // Ally piece 69 | if is_cell_color_ally(game_board, &new_coordinates, color) { 70 | if !allow_move_on_ally_positions { 71 | break; 72 | } 73 | positions.push(new_coordinates); 74 | break; 75 | } 76 | 77 | // Enemy cell 78 | positions.push(new_coordinates); 79 | if !allow_move_on_ally_positions 80 | || !is_piece_opposite_king(game_board.board[new_y as usize][new_x as usize], color) 81 | { 82 | break; 83 | } 84 | } 85 | 86 | // BOTTOM ROW 87 | for i in 1..8u8 { 88 | let new_x = x; 89 | let new_y = y + i; 90 | let new_coordinates = Coord::new(new_y, new_x); 91 | 92 | // Invalid coords 93 | if !new_coordinates.is_valid() { 94 | break; 95 | } 96 | 97 | // Empty cell 98 | if game_board.get_piece_color(&new_coordinates).is_none() { 99 | positions.push(new_coordinates); 100 | continue; 101 | } 102 | // Ally cell 103 | if is_cell_color_ally(game_board, &new_coordinates, color) { 104 | if !allow_move_on_ally_positions { 105 | break; 106 | } 107 | positions.push(new_coordinates); 108 | break; 109 | } 110 | 111 | // Enemy cell 112 | positions.push(new_coordinates); 113 | 114 | if !allow_move_on_ally_positions 115 | || !is_piece_opposite_king(game_board.board[new_y as usize][new_x as usize], color) 116 | { 117 | break; 118 | } 119 | } 120 | 121 | // UP ROW 122 | for i in 1..8u8 { 123 | let new_x = x as i8; 124 | let new_y = y as i8 - i as i8; 125 | let Some(new_coordinates) = Coord::opt_new(new_y, new_x) else { 126 | break; 127 | }; 128 | 129 | // Empty cell 130 | if game_board.get_piece_color(&new_coordinates).is_none() { 131 | positions.push(new_coordinates); 132 | continue; 133 | } 134 | // Ally cell 135 | if is_cell_color_ally(game_board, &new_coordinates, color) { 136 | if !allow_move_on_ally_positions { 137 | break; 138 | } 139 | positions.push(new_coordinates); 140 | break; 141 | } 142 | // Enemy cell 143 | positions.push(new_coordinates); 144 | 145 | if !allow_move_on_ally_positions 146 | || !is_piece_opposite_king(game_board.board[new_y as usize][new_x as usize], color) 147 | { 148 | break; 149 | } 150 | } 151 | 152 | cleaned_positions(&positions) 153 | } 154 | } 155 | 156 | impl Position for Rook { 157 | fn authorized_positions( 158 | coordinates: &Coord, 159 | color: PieceColor, 160 | game_board: &GameBoard, 161 | _is_king_checked: bool, 162 | ) -> Vec { 163 | // If the king is not checked we get then normal moves 164 | // if the king is checked we clean all the position not resolving the check 165 | game_board.impossible_positions_king_checked( 166 | coordinates, 167 | Self::piece_move(coordinates, color, game_board, false), 168 | color, 169 | ) 170 | } 171 | 172 | fn protected_positions( 173 | coordinates: &Coord, 174 | color: PieceColor, 175 | game_board: &GameBoard, 176 | ) -> Vec { 177 | Self::piece_move(coordinates, color, game_board, true) 178 | } 179 | } 180 | 181 | impl Rook { 182 | pub fn to_string(display_mode: &DisplayMode) -> &'static str { 183 | match display_mode { 184 | DisplayMode::DEFAULT => { 185 | "\ 186 | \n\ 187 | █▟█▙█\n\ 188 | ▜███▛\n\ 189 | ▐███▌\n\ 190 | ▗█████▖\n\ 191 | " 192 | } 193 | DisplayMode::ASCII => "R", 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/server/game_server.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{Read, Write}, 3 | net::{TcpListener, TcpStream}, 4 | sync::{ 5 | atomic::{AtomicBool, Ordering}, 6 | mpsc, Arc, Mutex, 7 | }, 8 | thread, 9 | }; 10 | 11 | use log; 12 | 13 | #[derive(Debug)] 14 | pub struct Client { 15 | addr: String, 16 | stream: TcpStream, 17 | } 18 | 19 | #[derive(Clone)] 20 | pub struct GameServer { 21 | pub clients: Arc>>, 22 | pub client_id: usize, 23 | pub is_host_white: bool, 24 | pub stop_signal: Arc, 25 | } 26 | 27 | impl GameServer { 28 | pub fn new(is_host_white: bool) -> Self { 29 | Self { 30 | clients: Arc::new(Mutex::new(vec![])), 31 | client_id: 0, 32 | is_host_white, 33 | stop_signal: Arc::new(AtomicBool::new(false)), 34 | } 35 | } 36 | 37 | pub fn run(&self) { 38 | log::info!("Starting game server on 0.0.0.0:2308"); 39 | let listener = TcpListener::bind("0.0.0.0:2308").expect("Failed to create listener"); 40 | listener 41 | .set_nonblocking(true) 42 | .expect("Failed to set listener to non-blocking"); 43 | 44 | let state = self.clients.clone(); 45 | let stop_signal = self.stop_signal.clone(); 46 | let (shutdown_tx, shutdown_rx) = mpsc::channel(); 47 | 48 | // Spawn a thread to watch for the stop signal 49 | let stop_signal_clone = stop_signal.clone(); 50 | thread::spawn(move || { 51 | while !stop_signal_clone.load(Ordering::SeqCst) { 52 | thread::sleep(std::time::Duration::from_millis(100)); 53 | } 54 | let _ = shutdown_tx.send(()); 55 | }); 56 | 57 | loop { 58 | // Check for shutdown signal 59 | if shutdown_rx.try_recv().is_ok() { 60 | log::info!("Received shutdown signal, stopping server"); 61 | break; 62 | } 63 | 64 | // Handle incoming connections 65 | match listener.accept() { 66 | Ok((mut stream, addr)) => { 67 | log::info!("New connection from: {}", addr); 68 | let state = Arc::clone(&state); 69 | let stop_signal = Arc::clone(&stop_signal); 70 | let color = if self.is_host_white { "w" } else { "b" }; 71 | 72 | thread::spawn(move || { 73 | { 74 | let mut state_lock = state.lock().unwrap(); 75 | // There is already one player (host who choose the color) we will need to send the color to the joining player and inform the host of the game start 76 | if state_lock.len() == 1 { 77 | stream.write_all(color.as_bytes()).unwrap(); 78 | let other_player = state_lock.last().unwrap(); 79 | let mut other_player_stream = 80 | other_player.stream.try_clone().unwrap(); 81 | other_player_stream.write_all("s".as_bytes()).unwrap(); 82 | } else if state_lock.len() >= 2 { 83 | stream.write_all("Game is already full".as_bytes()).unwrap(); 84 | return; 85 | } 86 | 87 | state_lock.push(Client { 88 | addr: stream.peer_addr().unwrap().to_string(), 89 | stream: stream.try_clone().unwrap(), 90 | }); 91 | } 92 | handle_client(state, stop_signal, stream); 93 | }); 94 | } 95 | Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { 96 | thread::sleep(std::time::Duration::from_millis(100)); 97 | } 98 | Err(e) => { 99 | log::error!("Failed to accept connection: {}", e); 100 | } 101 | } 102 | } 103 | } 104 | } 105 | 106 | fn handle_client( 107 | state: Arc>>, 108 | stop_signal: Arc, 109 | mut stream: TcpStream, 110 | ) { 111 | let addr = stream.peer_addr().unwrap().to_string(); 112 | log::info!("Starting client handler for: {}", addr); 113 | 114 | // Set socket to non-blocking mode 115 | if let Err(e) = stream.set_nonblocking(true) { 116 | log::error!("Failed to set non-blocking mode for client {}: {}", addr, e); 117 | return; 118 | } 119 | 120 | loop { 121 | let mut buffer = [0; 5]; 122 | match stream.read(&mut buffer) { 123 | Ok(0) => { 124 | log::info!("Client {} disconnected", addr); 125 | broadcast_message(state.clone(), "ended".to_string(), &addr); 126 | remove_client(&state, &addr); 127 | stop_signal.store(true, Ordering::SeqCst); 128 | break; 129 | } 130 | Ok(bytes_read) => { 131 | let request = String::from_utf8_lossy(&buffer[..bytes_read]); 132 | log::debug!("Received message from {}: {}", addr, request.trim()); 133 | broadcast_message(state.clone(), format!("{}", request), &addr); 134 | 135 | if request.trim() == "ended" { 136 | log::info!("Client {} sent end signal", addr); 137 | remove_client(&state, &addr); 138 | stop_signal.store(true, Ordering::SeqCst); 139 | break; 140 | } 141 | } 142 | Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { 143 | // This is normal for non-blocking sockets 144 | std::thread::sleep(std::time::Duration::from_millis(50)); 145 | continue; 146 | } 147 | Err(e) => { 148 | log::error!("Error reading from client {}: {}", addr, e); 149 | break; 150 | } 151 | } 152 | } 153 | } 154 | 155 | fn broadcast_message(state: Arc>>, message: String, sender_addr: &String) { 156 | let state = state.lock().unwrap(); 157 | for client in state.iter() { 158 | if &client.addr == sender_addr { 159 | continue; 160 | } 161 | let mut client_stream = client.stream.try_clone().unwrap(); 162 | client_stream.write_all(message.as_bytes()).unwrap(); 163 | } 164 | } 165 | 166 | fn remove_client(state: &Arc>>, addr: &str) { 167 | let mut state_lock = state.lock().unwrap(); 168 | if let Some(index) = state_lock.iter().position(|client| client.addr == addr) { 169 | state_lock.remove(index); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/server/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod game_server; 2 | -------------------------------------------------------------------------------- /src/ui/main_ui.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | layout::{Constraint, Direction, Layout}, 3 | prelude::{Alignment, Rect}, 4 | style::{Color, Modifier, Style, Stylize}, 5 | text::Line, 6 | widgets::{Block, Paragraph}, 7 | Frame, 8 | }; 9 | 10 | use crate::{ 11 | constants::Popups, 12 | game_logic::{bot::Bot, game::GameState}, 13 | ui::popups::{ 14 | render_color_selection_popup, render_credit_popup, render_end_popup, 15 | render_engine_path_error_popup, render_help_popup, render_promotion_popup, 16 | }, 17 | }; 18 | 19 | use super::popups::{ 20 | render_enter_multiplayer_ip, render_multiplayer_selection_popup, render_wait_for_other_player, 21 | }; 22 | use crate::{ 23 | app::App, 24 | constants::{DisplayMode, Pages, TITLE}, 25 | pieces::PieceColor, 26 | }; 27 | 28 | /// Renders the user interface widgets. 29 | pub fn render(app: &mut App, frame: &mut Frame<'_>) { 30 | let main_area = frame.area(); 31 | 32 | // Solo game 33 | if app.current_page == Pages::Solo { 34 | render_game_ui(frame, app, main_area); 35 | } 36 | // Multiplayer game 37 | else if app.current_page == Pages::Multiplayer { 38 | if app.hosting.is_none() { 39 | app.current_popup = Some(Popups::MultiplayerSelection); 40 | } else if app.selected_color.is_none() && app.hosting.unwrap() { 41 | app.current_popup = Some(Popups::ColorSelection); 42 | } else if app.game.opponent.is_none() { 43 | if app.host_ip.is_none() { 44 | if app.hosting.is_some() && app.hosting.unwrap() { 45 | app.setup_game_server(app.selected_color.unwrap()); 46 | app.host_ip = Some("127.0.0.1".to_string()); 47 | } else { 48 | app.current_popup = Some(Popups::EnterHostIP); 49 | } 50 | } else { 51 | app.create_opponent(); 52 | } 53 | } else if app.game.opponent.as_mut().unwrap().game_started { 54 | render_game_ui(frame, app, main_area); 55 | } 56 | } 57 | // Play against bot 58 | else if app.current_page == Pages::Bot { 59 | if app.chess_engine_path.is_none() || app.chess_engine_path.as_ref().unwrap().is_empty() { 60 | render_engine_path_error_popup(frame); 61 | } else if app.selected_color.is_none() { 62 | app.current_popup = Some(Popups::ColorSelection); 63 | } else if app.game.bot.is_none() { 64 | let engine_path = app.chess_engine_path.clone().unwrap(); 65 | let is_bot_starting = app.selected_color.unwrap() == PieceColor::Black; 66 | app.game.bot = Some(Bot::new(engine_path.as_str(), is_bot_starting)); 67 | } else { 68 | render_game_ui(frame, app, main_area); 69 | } 70 | } 71 | // Render menu 72 | else { 73 | render_menu_ui(frame, app, main_area); 74 | } 75 | 76 | if app.current_page == Pages::Credit { 77 | render_credit_popup(frame); 78 | } 79 | 80 | // Render popups 81 | match app.current_popup { 82 | Some(Popups::ColorSelection) => { 83 | render_color_selection_popup(frame, app); 84 | } 85 | Some(Popups::MultiplayerSelection) => { 86 | render_multiplayer_selection_popup(frame, app); 87 | } 88 | Some(Popups::EnterHostIP) => { 89 | render_enter_multiplayer_ip(frame, &app.game.ui.prompt); 90 | } 91 | Some(Popups::WaitingForOpponentToJoin) => { 92 | render_wait_for_other_player(frame, app.get_host_ip()); 93 | } 94 | Some(Popups::Help) => { 95 | render_help_popup(frame); 96 | } 97 | _ => {} 98 | } 99 | } 100 | 101 | /// Helper function to create a centered rect using up certain percentage of the available rect `r` 102 | pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { 103 | let popup_layout = Layout::default() 104 | .direction(Direction::Vertical) 105 | .constraints([ 106 | Constraint::Percentage((100 - percent_y) / 2), 107 | Constraint::Percentage(percent_y), 108 | Constraint::Percentage((100 - percent_y) / 2), 109 | ]) 110 | .split(r); 111 | 112 | Layout::default() 113 | .direction(Direction::Horizontal) 114 | .constraints([ 115 | Constraint::Percentage((100 - percent_x) / 2), 116 | Constraint::Percentage(percent_x), 117 | Constraint::Percentage((100 - percent_x) / 2), 118 | ]) 119 | .split(popup_layout[1])[1] 120 | } 121 | 122 | pub fn render_cell(frame: &mut Frame, square: Rect, color: Color, modifier: Option) { 123 | let mut cell = Block::default().bg(color); 124 | if let Some(modifier) = modifier { 125 | cell = cell.add_modifier(modifier); 126 | } 127 | frame.render_widget(cell, square); 128 | } 129 | 130 | // Method to render the home menu and the options 131 | pub fn render_menu_ui(frame: &mut Frame, app: &App, main_area: Rect) { 132 | let main_layout_horizontal = Layout::default() 133 | .direction(Direction::Vertical) 134 | .constraints( 135 | [ 136 | Constraint::Ratio(1, 5), 137 | Constraint::Ratio(1, 5), 138 | Constraint::Ratio(3, 5), 139 | ] 140 | .as_ref(), 141 | ) 142 | .split(main_area); 143 | 144 | // Title 145 | let title_paragraph = Paragraph::new(TITLE) 146 | .alignment(Alignment::Center) 147 | .block(Block::default()); 148 | frame.render_widget(title_paragraph, main_layout_horizontal[0]); 149 | 150 | // Board block representing the full board div 151 | let text: Vec> = vec![Line::from(""), Line::from("A chess game made in 🦀")]; 152 | let sub_title = Paragraph::new(text) 153 | .alignment(Alignment::Center) 154 | .block(Block::default()); 155 | frame.render_widget(sub_title, main_layout_horizontal[1]); 156 | 157 | // Determine the "display mode" text 158 | let display_mode_menu = { 159 | let display_mode = match app.game.ui.display_mode { 160 | DisplayMode::DEFAULT => "Default", 161 | DisplayMode::ASCII => "ASCII", 162 | }; 163 | format!("Display mode: {display_mode}") 164 | }; 165 | 166 | // Board block representing the full board div 167 | let menu_items = [ 168 | "Normal game", 169 | "Multiplayer", 170 | "Play against a bot", 171 | &display_mode_menu, 172 | "Help", 173 | "Credits", 174 | ]; 175 | let mut menu_body: Vec> = vec![]; 176 | 177 | for (i, menu_item) in menu_items.iter().enumerate() { 178 | menu_body.push(Line::from("")); 179 | let mut text = if app.menu_cursor == i as u8 { 180 | "> ".to_string() 181 | } else { 182 | String::new() 183 | }; 184 | text.push_str(menu_item); 185 | menu_body.push(Line::from(text)); 186 | } 187 | 188 | let sub_title = Paragraph::new(menu_body) 189 | .bold() 190 | .alignment(Alignment::Center) 191 | .block(Block::default()); 192 | frame.render_widget(sub_title, main_layout_horizontal[2]); 193 | } 194 | 195 | // Method to render the game board and handle game popups 196 | pub fn render_game_ui(frame: &mut Frame<'_>, app: &mut App, main_area: Rect) { 197 | let main_layout_horizontal = Layout::default() 198 | .direction(Direction::Vertical) 199 | .constraints( 200 | [ 201 | Constraint::Ratio(1, 18), 202 | Constraint::Ratio(16, 18), 203 | Constraint::Ratio(1, 18), 204 | ] 205 | .as_ref(), 206 | ) 207 | .split(main_area); 208 | 209 | let main_layout_vertical = Layout::default() 210 | .direction(Direction::Horizontal) 211 | .constraints( 212 | [ 213 | Constraint::Ratio(2, 17), 214 | Constraint::Ratio(9, 17), 215 | Constraint::Ratio(1, 17), 216 | Constraint::Ratio(5, 17), 217 | ] 218 | .as_ref(), 219 | ) 220 | .split(main_layout_horizontal[1]); 221 | 222 | let right_box_layout = Layout::default() 223 | .direction(Direction::Vertical) 224 | .constraints( 225 | [ 226 | Constraint::Ratio(2, 15), 227 | Constraint::Ratio(11, 15), 228 | Constraint::Ratio(2, 15), 229 | ] 230 | .as_ref(), 231 | ) 232 | .split(main_layout_vertical[3]); 233 | // Board block representing the full board div 234 | let board_block = Block::default().style(Style::default()); 235 | 236 | // We render the board_block in the center layout made above 237 | frame.render_widget(board_block.clone(), main_layout_vertical[1]); 238 | 239 | let game_clone = app.game.clone(); 240 | app.game.ui.board_render( 241 | board_block.inner(main_layout_vertical[1]), 242 | frame, 243 | &game_clone, 244 | ); // Mutable borrow now allowed 245 | 246 | //top box for white material 247 | app.game.ui.black_material_render( 248 | board_block.inner(right_box_layout[0]), 249 | frame, 250 | &app.game.game_board.black_taken_pieces, 251 | ); 252 | 253 | // We make the inside of the board 254 | app.game 255 | .ui 256 | .history_render(board_block.inner(right_box_layout[1]), frame, &app.game); 257 | 258 | //bottom box for black matetrial 259 | app.game.ui.white_material_render( 260 | board_block.inner(right_box_layout[2]), 261 | frame, 262 | &app.game.game_board.white_taken_pieces, 263 | ); 264 | 265 | if app.game.game_state == GameState::Promotion { 266 | render_promotion_popup(frame, app); 267 | } 268 | 269 | if app.game.game_state == GameState::Checkmate { 270 | let victorious_player = app.game.player_turn.opposite(); 271 | 272 | let string_color = match victorious_player { 273 | PieceColor::White => "White", 274 | PieceColor::Black => "Black", 275 | }; 276 | 277 | render_end_popup( 278 | frame, 279 | &format!("{string_color} Won !!!"), 280 | app.game.opponent.is_some(), 281 | ); 282 | } 283 | 284 | if app.game.game_state == GameState::Draw { 285 | render_end_popup(frame, "That's a draw", app.game.opponent.is_some()); 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/ui/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod main_ui; 2 | pub mod popups; 3 | pub mod prompt; 4 | pub mod tui; 5 | -------------------------------------------------------------------------------- /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 submit_message(&mut self) { 41 | self.message = self.input.clone(); 42 | self.input.clear(); 43 | self.reset_cursor(); 44 | } 45 | 46 | pub fn enter_char(&mut self, new_char: char) { 47 | let index = self.byte_index(); 48 | if index < 40 { 49 | self.input.insert(index, new_char); 50 | self.move_cursor_right(); 51 | } 52 | } 53 | 54 | /// Returns the byte index based on the character position. 55 | /// 56 | /// Since each character in a string can be contain multiple bytes, it's necessary to calculate 57 | /// the byte index based on the index of the character. 58 | pub fn byte_index(&self) -> usize { 59 | self.input 60 | .char_indices() 61 | .map(|(i, _)| i) 62 | .nth(self.character_index) 63 | .unwrap_or(self.input.len()) 64 | } 65 | 66 | pub fn delete_char(&mut self) { 67 | let is_not_cursor_leftmost = self.character_index != 0; 68 | if is_not_cursor_leftmost { 69 | // Method "remove" is not used on the saved text for deleting the selected char. 70 | // Reason: Using remove on String works on bytes instead of the chars. 71 | // Using remove would require special care because of char boundaries. 72 | 73 | let current_index = self.character_index; 74 | let from_left_to_current_index = current_index - 1; 75 | 76 | // Getting all characters before the selected character. 77 | let before_char_to_delete = self.input.chars().take(from_left_to_current_index); 78 | // Getting all characters after selected character. 79 | let after_char_to_delete = self.input.chars().skip(current_index); 80 | 81 | // Put all characters together except the selected one. 82 | // By leaving the selected one out, it is forgotten and therefore deleted. 83 | self.input = before_char_to_delete.chain(after_char_to_delete).collect(); 84 | self.move_cursor_left(); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::game_logic::coord::Coord; 2 | use crate::game_logic::game::Game; 3 | use crate::game_logic::game_board::GameBoard; 4 | use crate::{ 5 | constants::{DisplayMode, UNDEFINED_POSITION}, 6 | pieces::{PieceColor, PieceType}, 7 | }; 8 | use ratatui::{ 9 | layout::{Alignment, Rect}, 10 | style::{Color, Stylize}, 11 | widgets::{Block, Padding, Paragraph}, 12 | }; 13 | 14 | /// method to clean `positions`: remove impossible positions 15 | pub fn cleaned_positions(positions: &[Coord]) -> Vec { 16 | positions 17 | .iter() 18 | .filter(|position| position.is_valid()) 19 | .copied() 20 | .collect() 21 | } 22 | 23 | /// Return true for ally cell color; false for enemy 24 | pub fn is_cell_color_ally(game_board: &GameBoard, coordinates: &Coord, color: PieceColor) -> bool { 25 | match game_board.get_piece_color(coordinates) { 26 | Some(cell_color) => cell_color == color, 27 | None => false, // Treat empty cell as ally 28 | } 29 | } 30 | 31 | pub fn col_to_letter(col: u8) -> String { 32 | match col { 33 | 0 => "a".to_string(), 34 | 1 => "b".to_string(), 35 | 2 => "c".to_string(), 36 | 3 => "d".to_string(), 37 | 4 => "e".to_string(), 38 | 5 => "f".to_string(), 39 | 6 => "g".to_string(), 40 | 7 => "h".to_string(), 41 | _ => unreachable!("Col out of bound {}", col), 42 | } 43 | } 44 | 45 | pub fn letter_to_col(col: Option) -> i8 { 46 | match col { 47 | Some('a') => 0, 48 | Some('b') => 1, 49 | Some('c') => 2, 50 | Some('d') => 3, 51 | Some('e') => 4, 52 | Some('f') => 5, 53 | Some('g') => 6, 54 | Some('h') => 7, 55 | _ => unreachable!("Col out of bound"), 56 | } 57 | } 58 | 59 | pub fn convert_position_into_notation(position: &str) -> String { 60 | let from_y = get_int_from_char(position.chars().next()); 61 | let from_x = get_int_from_char(position.chars().nth(1)); 62 | let to_y = get_int_from_char(position.chars().nth(2)); 63 | let to_x = get_int_from_char(position.chars().nth(3)); 64 | 65 | let from_x = col_to_letter(from_x); 66 | let to_x = col_to_letter(to_x); 67 | 68 | format!("{from_x}{}-{to_x}{}", 8 - from_y, 8 - to_y) 69 | } 70 | 71 | pub fn convert_notation_into_position(notation: &str) -> String { 72 | let from_x = &letter_to_col(notation.chars().next()); 73 | let from_y = (get_int_from_char(notation.chars().nth(1)) as i8 - 8).abs(); 74 | 75 | let to_x = &letter_to_col(notation.chars().nth(2)); 76 | let to_y = (get_int_from_char(notation.chars().nth(3)) as i8 - 8).abs(); 77 | 78 | format!("{from_y}{from_x}{to_y}{to_x}") 79 | } 80 | 81 | pub fn get_int_from_char(ch: Option) -> u8 { 82 | match ch { 83 | Some(ch) => ch.to_digit(10).unwrap() as u8, 84 | _ => UNDEFINED_POSITION, 85 | } 86 | } 87 | 88 | pub fn is_piece_opposite_king(piece: Option<(PieceType, PieceColor)>, color: PieceColor) -> bool { 89 | match piece { 90 | Some((piece_type, piece_color)) => { 91 | piece_type == PieceType::King && piece_color == color.opposite() 92 | } 93 | _ => false, 94 | } 95 | } 96 | 97 | pub fn color_to_ratatui_enum(piece_color: Option) -> Color { 98 | match piece_color { 99 | Some(PieceColor::Black) => Color::Black, 100 | Some(PieceColor::White) => Color::White, 101 | None => Color::Red, 102 | } 103 | } 104 | 105 | pub fn get_cell_paragraph<'a>( 106 | game: &'a Game, 107 | cell_coordinates: &'a Coord, 108 | bounding_rect: Rect, 109 | ) -> Paragraph<'a> { 110 | // Get piece and color 111 | let piece_color = game.game_board.get_piece_color(cell_coordinates); 112 | let piece_type = game.game_board.get_piece_type(cell_coordinates); 113 | let piece_enum = PieceType::piece_type_to_string_enum(piece_type, &game.ui.display_mode); 114 | 115 | let paragraph = match game.ui.display_mode { 116 | DisplayMode::DEFAULT => { 117 | let color_enum = color_to_ratatui_enum(piece_color); 118 | 119 | // Place the pieces on the board 120 | Paragraph::new(piece_enum).fg(color_enum) 121 | } 122 | DisplayMode::ASCII => { 123 | // Determine piece letter case 124 | let paragraph = match piece_color { 125 | // pieces belonging to the player on top will be lower case 126 | Some(PieceColor::Black) => Paragraph::new(piece_enum.to_lowercase()), 127 | // pieces belonging to the player on bottom will be upper case 128 | Some(PieceColor::White) => Paragraph::new(piece_enum.to_uppercase().underlined()), 129 | // Pass through original value 130 | None => Paragraph::new(piece_enum), 131 | }; 132 | 133 | // Place the pieces on the board 134 | paragraph.block(Block::new().padding(Padding::vertical(bounding_rect.height / 2))) 135 | } 136 | }; 137 | 138 | paragraph.alignment(Alignment::Center) 139 | } 140 | 141 | pub fn invert_position(coord: &Coord) -> Coord { 142 | Coord::new(7 - coord.row, 7 - coord.col) 143 | } 144 | -------------------------------------------------------------------------------- /tests/draws.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use chess_tui::game_logic::coord::Coord; 4 | use chess_tui::game_logic::game::Game; 5 | use chess_tui::game_logic::game_board::GameBoard; 6 | use chess_tui::pieces::{PieceColor, PieceMove, PieceType}; 7 | #[test] 8 | fn is_draw_true() { 9 | let custom_board = [ 10 | [ 11 | Some((PieceType::King, PieceColor::White)), 12 | None, 13 | None, 14 | None, 15 | None, 16 | None, 17 | None, 18 | None, 19 | ], 20 | [ 21 | None, 22 | None, 23 | Some((PieceType::Queen, PieceColor::Black)), 24 | None, 25 | None, 26 | None, 27 | None, 28 | None, 29 | ], 30 | [ 31 | None, 32 | Some((PieceType::Rook, PieceColor::Black)), 33 | None, 34 | None, 35 | None, 36 | None, 37 | None, 38 | None, 39 | ], 40 | [None, None, None, None, None, None, None, None], 41 | [None, None, None, None, None, None, None, None], 42 | [None, None, None, None, None, None, None, None], 43 | [None, None, None, None, None, None, None, None], 44 | [None, None, None, None, None, None, None, None], 45 | ]; 46 | 47 | let game_board = GameBoard::new(custom_board, vec![], vec![]); 48 | let mut game = Game::new(game_board, PieceColor::White); 49 | game.game_board.board = custom_board; 50 | 51 | assert!(game.game_board.is_draw(game.player_turn)); 52 | } 53 | 54 | #[test] 55 | fn is_draw_false() { 56 | let custom_board = [ 57 | [ 58 | Some((PieceType::King, PieceColor::White)), 59 | None, 60 | None, 61 | None, 62 | None, 63 | None, 64 | None, 65 | None, 66 | ], 67 | [ 68 | None, 69 | None, 70 | None, 71 | None, 72 | Some((PieceType::Queen, PieceColor::Black)), 73 | None, 74 | None, 75 | None, 76 | ], 77 | [ 78 | None, 79 | None, 80 | Some((PieceType::Rook, PieceColor::Black)), 81 | None, 82 | None, 83 | None, 84 | None, 85 | None, 86 | ], 87 | [None, None, None, None, None, None, None, None], 88 | [None, None, None, None, None, None, None, None], 89 | [None, None, None, None, None, None, None, None], 90 | [None, None, None, None, None, None, None, None], 91 | [None, None, None, None, None, None, None, None], 92 | ]; 93 | 94 | let game_board = GameBoard::new(custom_board, vec![], vec![]); 95 | let mut game = Game::new(game_board, PieceColor::White); 96 | game.game_board.board = custom_board; 97 | 98 | assert!(!game.game_board.is_draw(game.player_turn)); 99 | } 100 | 101 | #[test] 102 | fn fifty_moves_draw() { 103 | let custom_board = [ 104 | [None, None, None, None, None, None, None, None], 105 | [ 106 | None, 107 | None, 108 | Some((PieceType::King, PieceColor::White)), 109 | None, 110 | None, 111 | None, 112 | Some((PieceType::King, PieceColor::Black)), 113 | None, 114 | ], 115 | [None, None, None, None, None, None, None, None], 116 | [None, None, None, None, None, None, None, None], 117 | [None, None, None, None, None, None, None, None], 118 | [None, None, None, None, None, None, None, None], 119 | [None, None, None, None, None, None, None, None], 120 | [None, None, None, None, None, None, None, None], 121 | ]; 122 | // We setup the game 123 | 124 | let game_board = GameBoard::new(custom_board, vec![], vec![]); 125 | let mut game = Game::new(game_board, PieceColor::White); 126 | game.game_board.board = custom_board; 127 | 128 | game.game_board.set_consecutive_non_pawn_or_capture(49); 129 | assert!(!game.game_board.is_draw(game.player_turn)); 130 | 131 | // Move the pawn to a make the 50th move 132 | game.execute_move(&Coord::new(1, 6), &Coord::new(1, 5)); 133 | assert!(game.game_board.is_draw(game.player_turn)); 134 | } 135 | 136 | #[test] 137 | fn consecutive_position_draw() { 138 | let custom_board = [ 139 | [ 140 | None, 141 | None, 142 | Some((PieceType::King, PieceColor::White)), 143 | None, 144 | None, 145 | None, 146 | Some((PieceType::King, PieceColor::Black)), 147 | None, 148 | ], 149 | [None, None, None, None, None, None, None, None], 150 | [None, None, None, None, None, None, None, None], 151 | [None, None, None, None, None, None, None, None], 152 | [None, None, None, None, None, None, None, None], 153 | [None, None, None, None, None, None, None, None], 154 | [None, None, None, None, None, None, None, None], 155 | [None, None, None, None, None, None, None, None], 156 | ]; 157 | 158 | // We setup the game 159 | 160 | let game_board = GameBoard::new( 161 | custom_board, 162 | vec![ 163 | (PieceMove { 164 | piece_type: PieceType::King, 165 | piece_color: PieceColor::White, 166 | from: Coord::new(0, 2), 167 | to: Coord::new(0, 1), 168 | }), 169 | (PieceMove { 170 | piece_type: PieceType::King, 171 | piece_color: PieceColor::Black, 172 | from: Coord::new(0, 6), 173 | to: Coord::new(0, 5), 174 | }), 175 | (PieceMove { 176 | piece_type: PieceType::King, 177 | piece_color: PieceColor::White, 178 | from: Coord::new(0, 1), 179 | to: Coord::new(0, 2), 180 | }), 181 | (PieceMove { 182 | piece_type: PieceType::King, 183 | piece_color: PieceColor::Black, 184 | from: Coord::new(0, 5), 185 | to: Coord::new(0, 6), 186 | }), 187 | (PieceMove { 188 | piece_type: PieceType::King, 189 | piece_color: PieceColor::White, 190 | from: Coord::new(0, 2), 191 | to: Coord::new(0, 1), 192 | }), 193 | (PieceMove { 194 | piece_type: PieceType::King, 195 | piece_color: PieceColor::Black, 196 | from: Coord::new(0, 6), 197 | to: Coord::new(0, 5), 198 | }), 199 | (PieceMove { 200 | piece_type: PieceType::King, 201 | piece_color: PieceColor::White, 202 | from: Coord::new(0, 1), 203 | to: Coord::new(0, 2), 204 | }), 205 | (PieceMove { 206 | piece_type: PieceType::King, 207 | piece_color: PieceColor::Black, 208 | from: Coord::new(0, 5), 209 | to: Coord::new(0, 6), 210 | }), 211 | ], 212 | vec![], 213 | ); 214 | let mut game = Game::new(game_board, PieceColor::White); 215 | game.game_board.board = custom_board; 216 | 217 | let mut copy_move_history = game.game_board.move_history.clone(); 218 | 219 | for piece_move in copy_move_history.iter_mut() { 220 | game.execute_move(&piece_move.from, &piece_move.to); 221 | 222 | // In a chess game, board.is_draw() is called after every move 223 | assert!(!game.game_board.is_draw(game.player_turn)); 224 | } 225 | 226 | // Move the king to replicate a third time the same position 227 | game.execute_move(&Coord::new(0, 2), &Coord::new(0, 1)); 228 | assert!(game.game_board.is_draw(game.player_turn)); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /tests/fen.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use chess_tui::game_logic::coord::Coord; 4 | use chess_tui::game_logic::game::Game; 5 | use chess_tui::game_logic::game_board::GameBoard; 6 | use chess_tui::pieces::{PieceColor, PieceMove, PieceType}; 7 | 8 | #[test] 9 | fn fen_converter_1() { 10 | let custom_board = [ 11 | [ 12 | None, 13 | None, 14 | Some((PieceType::King, PieceColor::Black)), 15 | None, 16 | None, 17 | None, 18 | None, 19 | Some((PieceType::Rook, PieceColor::White)), 20 | ], 21 | [None, None, None, None, None, None, None, None], 22 | [ 23 | None, 24 | None, 25 | None, 26 | None, 27 | Some((PieceType::King, PieceColor::White)), 28 | None, 29 | None, 30 | None, 31 | ], 32 | [None, None, None, None, None, None, None, None], 33 | [None, None, None, None, None, None, None, None], 34 | [None, None, None, None, None, None, None, None], 35 | [None, None, None, None, None, None, None, None], 36 | [None, None, None, None, None, None, None, None], 37 | ]; 38 | // We setup the game 39 | let game_board = GameBoard::new(custom_board, vec![], vec![]); 40 | let mut game = Game::new(game_board, PieceColor::White); 41 | game.game_board.board = custom_board; 42 | 43 | // Move the king to replicate a third time the same position 44 | assert_eq!( 45 | game.game_board.fen_position(false, game.player_turn), 46 | "2k4R/8/4K3/8/8/8/8/8 b - - 0 1" 47 | ); 48 | } 49 | 50 | #[test] 51 | fn fen_converter_en_passant() { 52 | let custom_board = [ 53 | [ 54 | None, 55 | None, 56 | Some((PieceType::King, PieceColor::Black)), 57 | None, 58 | None, 59 | None, 60 | None, 61 | Some((PieceType::Rook, PieceColor::White)), 62 | ], 63 | [None, None, None, None, None, None, None, None], 64 | [ 65 | None, 66 | None, 67 | None, 68 | None, 69 | Some((PieceType::King, PieceColor::White)), 70 | None, 71 | None, 72 | None, 73 | ], 74 | [None, None, None, None, None, None, None, None], 75 | [ 76 | None, 77 | None, 78 | Some((PieceType::Pawn, PieceColor::White)), 79 | None, 80 | None, 81 | None, 82 | None, 83 | None, 84 | ], 85 | [None, None, None, None, None, None, None, None], 86 | [None, None, None, None, None, None, None, None], 87 | [None, None, None, None, None, None, None, None], 88 | ]; 89 | // We setup the game 90 | let game_board = GameBoard::new( 91 | custom_board, 92 | vec![ 93 | (PieceMove { 94 | piece_type: PieceType::Pawn, 95 | piece_color: PieceColor::White, 96 | from: Coord::new(6, 2), 97 | to: Coord::new(4, 2), 98 | }), 99 | ], 100 | vec![], 101 | ); 102 | let mut game = Game::new(game_board, PieceColor::White); 103 | game.game_board.board = custom_board; 104 | 105 | // Move the king to replicate a third time the same position 106 | assert_eq!( 107 | game.game_board.fen_position(false, game.player_turn), 108 | "2k4R/8/4K3/8/2P5/8/8/8 b - c3 0 1" 109 | ); 110 | } 111 | #[test] 112 | fn fen_converter_castling() { 113 | let custom_board = [ 114 | [ 115 | Some((PieceType::Rook, PieceColor::Black)), 116 | Some((PieceType::Knight, PieceColor::Black)), 117 | Some((PieceType::Bishop, PieceColor::Black)), 118 | Some((PieceType::Queen, PieceColor::Black)), 119 | Some((PieceType::King, PieceColor::Black)), 120 | Some((PieceType::Bishop, PieceColor::Black)), 121 | Some((PieceType::Knight, PieceColor::Black)), 122 | Some((PieceType::Rook, PieceColor::Black)), 123 | ], 124 | [ 125 | Some((PieceType::Pawn, PieceColor::Black)), 126 | Some((PieceType::Pawn, PieceColor::Black)), 127 | Some((PieceType::Pawn, PieceColor::Black)), 128 | Some((PieceType::Pawn, PieceColor::Black)), 129 | Some((PieceType::Pawn, PieceColor::Black)), 130 | Some((PieceType::Pawn, PieceColor::Black)), 131 | Some((PieceType::Pawn, PieceColor::Black)), 132 | Some((PieceType::Pawn, PieceColor::Black)), 133 | ], 134 | [None, None, None, None, None, None, None, None], 135 | [None, None, None, None, None, None, None, None], 136 | [None, None, None, None, None, None, None, None], 137 | [None, None, None, None, None, None, None, None], 138 | [ 139 | Some((PieceType::Pawn, PieceColor::White)), 140 | Some((PieceType::Pawn, PieceColor::White)), 141 | Some((PieceType::Pawn, PieceColor::White)), 142 | Some((PieceType::Pawn, PieceColor::White)), 143 | Some((PieceType::Pawn, PieceColor::White)), 144 | Some((PieceType::Pawn, PieceColor::White)), 145 | Some((PieceType::Pawn, PieceColor::White)), 146 | Some((PieceType::Pawn, PieceColor::White)), 147 | ], 148 | [ 149 | Some((PieceType::Rook, PieceColor::White)), 150 | Some((PieceType::Knight, PieceColor::White)), 151 | Some((PieceType::Bishop, PieceColor::White)), 152 | Some((PieceType::Queen, PieceColor::White)), 153 | Some((PieceType::King, PieceColor::White)), 154 | Some((PieceType::Bishop, PieceColor::White)), 155 | Some((PieceType::Knight, PieceColor::White)), 156 | Some((PieceType::Rook, PieceColor::White)), 157 | ], 158 | ]; 159 | // We setup the game 160 | let game_board = GameBoard::new(custom_board, vec![], vec![]); 161 | let mut game = Game::new(game_board, PieceColor::White); 162 | game.game_board.board = custom_board; 163 | 164 | // Move the king to replicate a third time the same position 165 | assert_eq!( 166 | game.game_board.fen_position(false, game.player_turn), 167 | "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1" 168 | ); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /tests/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod pieces; 2 | -------------------------------------------------------------------------------- /tests/pieces/knight.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use chess_tui::game_logic::coord::Coord; 4 | use chess_tui::game_logic::game::Game; 5 | use chess_tui::game_logic::game_board::GameBoard; 6 | use chess_tui::pieces::knight::Knight; 7 | use chess_tui::pieces::{PieceColor, PieceType, Position}; 8 | 9 | #[test] 10 | fn no_enemies() { 11 | let custom_board = [ 12 | [None, None, None, None, None, None, None, None], 13 | [None, None, None, None, None, None, None, None], 14 | [None, None, None, None, None, None, None, None], 15 | [None, None, None, None, None, None, None, None], 16 | [ 17 | None, 18 | None, 19 | None, 20 | None, 21 | Some((PieceType::Knight, PieceColor::White)), 22 | None, 23 | None, 24 | None, 25 | ], 26 | [None, None, None, None, None, None, None, None], 27 | [None, None, None, None, None, None, None, None], 28 | [None, None, None, None, None, None, None, None], 29 | ]; 30 | let mut game = Game::default(); 31 | game.game_board.board = custom_board; 32 | 33 | let mut right_positions = vec![ 34 | Coord::new(2, 3), 35 | Coord::new(2, 5), 36 | Coord::new(3, 2), 37 | Coord::new(3, 6), 38 | Coord::new(5, 2), 39 | Coord::new(5, 6), 40 | Coord::new(6, 3), 41 | Coord::new(6, 5), 42 | ]; 43 | right_positions.sort(); 44 | 45 | let mut positions = Knight::authorized_positions( 46 | &Coord::new(4, 4), 47 | PieceColor::White, 48 | &game.game_board, 49 | false, 50 | ); 51 | positions.sort(); 52 | 53 | assert_eq!(right_positions, positions); 54 | } 55 | 56 | #[test] 57 | fn enemy_and_ally() { 58 | let custom_board = [ 59 | [None, None, None, None, None, None, None, None], 60 | [None, None, None, None, None, None, None, None], 61 | [None, None, None, None, None, None, None, None], 62 | [None, None, None, None, None, None, None, None], 63 | [None, None, None, None, None, None, None, None], 64 | [ 65 | None, 66 | None, 67 | None, 68 | None, 69 | None, 70 | None, 71 | Some((PieceType::Pawn, PieceColor::White)), 72 | None, 73 | ], 74 | [ 75 | None, 76 | None, 77 | None, 78 | None, 79 | None, 80 | Some((PieceType::Pawn, PieceColor::Black)), 81 | None, 82 | None, 83 | ], 84 | [ 85 | None, 86 | None, 87 | None, 88 | None, 89 | None, 90 | None, 91 | None, 92 | Some((PieceType::Knight, PieceColor::White)), 93 | ], 94 | ]; 95 | let mut game = Game::default(); 96 | game.game_board.board = custom_board; 97 | 98 | let mut right_positions = vec![Coord::new(6, 5)]; 99 | right_positions.sort(); 100 | 101 | let mut positions = Knight::authorized_positions( 102 | &Coord::new(7, 7), 103 | PieceColor::White, 104 | &game.game_board, 105 | false, 106 | ); 107 | positions.sort(); 108 | 109 | assert_eq!(right_positions, positions); 110 | } 111 | 112 | #[test] 113 | fn king_checked_can_resolve() { 114 | let custom_board = [ 115 | [None, None, None, None, None, None, None, None], 116 | [None, None, None, None, None, None, None, None], 117 | [None, None, None, None, None, None, None, None], 118 | [None, None, None, None, None, None, None, None], 119 | [None, None, None, None, None, None, None, None], 120 | [ 121 | None, 122 | None, 123 | None, 124 | None, 125 | None, 126 | None, 127 | Some((PieceType::King, PieceColor::White)), 128 | None, 129 | ], 130 | [ 131 | None, 132 | None, 133 | None, 134 | None, 135 | None, 136 | Some((PieceType::Knight, PieceColor::White)), 137 | None, 138 | None, 139 | ], 140 | [ 141 | None, 142 | None, 143 | None, 144 | None, 145 | None, 146 | None, 147 | None, 148 | Some((PieceType::Knight, PieceColor::Black)), 149 | ], 150 | ]; 151 | let game_board = GameBoard::new(custom_board, vec![], vec![]); 152 | let mut game = Game::new(game_board, PieceColor::White); 153 | game.game_board.board = custom_board; 154 | 155 | let is_king_checked = game 156 | .game_board 157 | .is_getting_checked(game.game_board.board, game.player_turn); 158 | 159 | let mut right_positions = vec![Coord::new(7, 7)]; 160 | right_positions.sort(); 161 | 162 | let mut positions = Knight::authorized_positions( 163 | &Coord::new(6, 5), 164 | PieceColor::White, 165 | &game.game_board, 166 | is_king_checked, 167 | ); 168 | positions.sort(); 169 | 170 | assert_eq!(right_positions, positions); 171 | } 172 | 173 | #[test] 174 | fn king_checked_cant_resolve() { 175 | let custom_board = [ 176 | [None, None, None, None, None, None, None, None], 177 | [None, None, None, None, None, None, None, None], 178 | [None, None, None, None, None, None, None, None], 179 | [None, None, None, None, None, None, None, None], 180 | [None, None, None, None, None, None, None, None], 181 | [ 182 | None, 183 | None, 184 | None, 185 | None, 186 | None, 187 | None, 188 | Some((PieceType::King, PieceColor::White)), 189 | None, 190 | ], 191 | [ 192 | None, 193 | None, 194 | None, 195 | None, 196 | Some((PieceType::Knight, PieceColor::White)), 197 | None, 198 | None, 199 | None, 200 | ], 201 | [ 202 | None, 203 | None, 204 | None, 205 | None, 206 | None, 207 | None, 208 | None, 209 | Some((PieceType::Knight, PieceColor::Black)), 210 | ], 211 | ]; 212 | let game_board = GameBoard::new(custom_board, vec![], vec![]); 213 | let mut game = Game::new(game_board, PieceColor::White); 214 | game.game_board.board = custom_board; 215 | 216 | let is_king_checked = game 217 | .game_board 218 | .is_getting_checked(game.game_board.board, game.player_turn); 219 | 220 | let mut right_positions: Vec = vec![]; 221 | right_positions.sort(); 222 | 223 | let mut positions = Knight::authorized_positions( 224 | &Coord::new(6, 4), 225 | PieceColor::White, 226 | &game.game_board, 227 | is_king_checked, 228 | ); 229 | positions.sort(); 230 | 231 | assert_eq!(right_positions, positions); 232 | } 233 | #[test] 234 | fn nailing() { 235 | let custom_board = [ 236 | [ 237 | None, 238 | None, 239 | None, 240 | None, 241 | Some((PieceType::King, PieceColor::Black)), 242 | None, 243 | None, 244 | None, 245 | ], 246 | [ 247 | None, 248 | None, 249 | None, 250 | None, 251 | Some((PieceType::Knight, PieceColor::Black)), 252 | None, 253 | None, 254 | None, 255 | ], 256 | [None, None, None, None, None, None, None, None], 257 | [ 258 | None, 259 | None, 260 | None, 261 | None, 262 | Some((PieceType::Queen, PieceColor::White)), 263 | None, 264 | None, 265 | None, 266 | ], 267 | [None, None, None, None, None, None, None, None], 268 | [None, None, None, None, None, None, None, None], 269 | [None, None, None, None, None, None, None, None], 270 | [None, None, None, None, None, None, None, None], 271 | ]; 272 | 273 | let game_board = GameBoard::new(custom_board, vec![], vec![]); 274 | let mut game = Game::new(game_board, PieceColor::Black); 275 | game.game_board.board = custom_board; 276 | 277 | let is_king_checked = game 278 | .game_board 279 | .is_getting_checked(game.game_board.board, game.player_turn); 280 | 281 | let mut right_positions: Vec = vec![]; 282 | right_positions.sort(); 283 | 284 | let mut positions = Knight::authorized_positions( 285 | &Coord::new(1, 4), 286 | PieceColor::Black, 287 | &game.game_board, 288 | is_king_checked, 289 | ); 290 | positions.sort(); 291 | 292 | assert_eq!(right_positions, positions); 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /tests/pieces/mod.rs: -------------------------------------------------------------------------------- 1 | mod bishop; 2 | mod king; 3 | mod knight; 4 | mod pawn; 5 | mod queen; 6 | mod rook; 7 | -------------------------------------------------------------------------------- /tests/promotions.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use chess_tui::game_logic::coord::Coord; 4 | use chess_tui::game_logic::game::Game; 5 | use chess_tui::game_logic::game_board::GameBoard; 6 | use chess_tui::pieces::{PieceColor, PieceMove, PieceType}; 7 | #[test] 8 | fn is_promote_true() { 9 | let custom_board = [ 10 | [ 11 | None, 12 | None, 13 | None, 14 | None, 15 | Some((PieceType::Pawn, PieceColor::White)), 16 | None, 17 | None, 18 | Some((PieceType::King, PieceColor::Black)), 19 | ], 20 | [ 21 | None, 22 | None, 23 | None, 24 | Some((PieceType::Rook, PieceColor::White)), 25 | None, 26 | None, 27 | None, 28 | None, 29 | ], 30 | [None, None, None, None, None, None, None, None], 31 | [None, None, None, None, None, None, None, None], 32 | [None, None, None, None, None, None, None, None], 33 | [None, None, None, None, None, None, None, None], 34 | [None, None, None, None, None, None, None, None], 35 | [ 36 | None, 37 | Some((PieceType::King, PieceColor::White)), 38 | None, 39 | None, 40 | None, 41 | None, 42 | None, 43 | None, 44 | ], 45 | ]; 46 | let game_board = GameBoard::new( 47 | custom_board, 48 | vec![ 49 | (PieceMove { 50 | piece_type: PieceType::Pawn, 51 | piece_color: PieceColor::White, 52 | from: Coord::new(1, 4), 53 | to: Coord::new(0, 4), 54 | }), 55 | ], 56 | vec![], 57 | ); 58 | let mut game = Game::new(game_board, PieceColor::Black); 59 | game.game_board.board = custom_board; 60 | 61 | assert!(game.game_board.is_latest_move_promotion()); 62 | } 63 | #[test] 64 | fn is_promote_false() { 65 | let custom_board = [ 66 | [ 67 | None, 68 | None, 69 | None, 70 | None, 71 | None, 72 | None, 73 | None, 74 | Some((PieceType::King, PieceColor::Black)), 75 | ], 76 | [ 77 | None, 78 | None, 79 | None, 80 | Some((PieceType::Rook, PieceColor::White)), 81 | None, 82 | None, 83 | None, 84 | None, 85 | ], 86 | [None, None, None, None, None, None, None, None], 87 | [None, None, None, None, None, None, None, None], 88 | [None, None, None, None, None, None, None, None], 89 | [None, None, None, None, None, None, None, None], 90 | [ 91 | None, 92 | None, 93 | None, 94 | Some((PieceType::Pawn, PieceColor::White)), 95 | None, 96 | None, 97 | None, 98 | None, 99 | ], 100 | [ 101 | None, 102 | Some((PieceType::King, PieceColor::White)), 103 | None, 104 | None, 105 | None, 106 | None, 107 | None, 108 | None, 109 | ], 110 | ]; 111 | let game_board = GameBoard::new( 112 | custom_board, 113 | vec![ 114 | (PieceMove { 115 | piece_type: PieceType::Pawn, 116 | piece_color: PieceColor::White, 117 | from: Coord::new(7, 3), 118 | to: Coord::new(6, 3), 119 | }), 120 | ], 121 | vec![], 122 | ); 123 | let mut game = Game::new(game_board, PieceColor::Black); 124 | game.game_board.board = custom_board; 125 | 126 | assert!(!game.game_board.is_latest_move_promotion()); 127 | } 128 | 129 | #[test] 130 | fn promote_and_checkmate() { 131 | let custom_board = [ 132 | [ 133 | None, 134 | None, 135 | None, 136 | None, 137 | None, 138 | None, 139 | None, 140 | Some((PieceType::King, PieceColor::Black)), 141 | ], 142 | [ 143 | None, 144 | None, 145 | None, 146 | Some((PieceType::Rook, PieceColor::White)), 147 | Some((PieceType::Pawn, PieceColor::White)), 148 | None, 149 | None, 150 | None, 151 | ], 152 | [None, None, None, None, None, None, None, None], 153 | [None, None, None, None, None, None, None, None], 154 | [None, None, None, None, None, None, None, None], 155 | [None, None, None, None, None, None, None, None], 156 | [None, None, None, None, None, None, None, None], 157 | [ 158 | None, 159 | Some((PieceType::King, PieceColor::White)), 160 | None, 161 | None, 162 | None, 163 | None, 164 | None, 165 | None, 166 | ], 167 | ]; 168 | // We setup the game 169 | let game_board = GameBoard::new(custom_board, vec![], vec![]); 170 | let mut game = Game::new(game_board, PieceColor::White); 171 | game.game_board.board = custom_board; 172 | 173 | // Move the pawn to a promote cell 174 | game.execute_move(&Coord::new(1, 4), &Coord::new(0, 4)); 175 | assert!(game.game_board.is_latest_move_promotion()); 176 | 177 | // Promote the pawn 178 | game.promote_piece(); 179 | 180 | // The black king gets checkmated 181 | game.player_turn = PieceColor::Black; 182 | assert!(game.game_board.is_checkmate(game.player_turn)); 183 | } 184 | 185 | #[test] 186 | fn is_promote_true_black() { 187 | let custom_board = [ 188 | [ 189 | None, 190 | None, 191 | None, 192 | None, 193 | Some((PieceType::Pawn, PieceColor::Black)), 194 | None, 195 | None, 196 | Some((PieceType::King, PieceColor::White)), 197 | ], 198 | [ 199 | None, 200 | None, 201 | None, 202 | Some((PieceType::Rook, PieceColor::Black)), 203 | None, 204 | None, 205 | None, 206 | None, 207 | ], 208 | [ 209 | None, 210 | Some((PieceType::King, PieceColor::Black)), 211 | None, 212 | None, 213 | None, 214 | None, 215 | None, 216 | None, 217 | ], 218 | [None, None, None, None, None, None, None, None], 219 | [None, None, None, None, None, None, None, None], 220 | [None, None, None, None, None, None, None, None], 221 | [None, None, None, None, None, None, None, None], 222 | [None, None, None, None, None, None, None, None], 223 | ]; 224 | let game_board = GameBoard::new( 225 | custom_board, 226 | vec![ 227 | (PieceMove { 228 | piece_type: PieceType::Pawn, 229 | piece_color: PieceColor::Black, 230 | from: Coord::new(1, 4), 231 | to: Coord::new(0, 4), 232 | }), 233 | ], 234 | vec![], 235 | ); 236 | let mut game = Game::new(game_board, PieceColor::Black); 237 | game.game_board.board = custom_board; 238 | 239 | assert!(game.game_board.is_latest_move_promotion()); 240 | } 241 | 242 | #[test] 243 | fn promote_and_draw() { 244 | let custom_board = [ 245 | [None, None, None, None, None, None, None, None], 246 | [ 247 | None, 248 | None, 249 | None, 250 | None, 251 | None, 252 | Some((PieceType::Pawn, PieceColor::Black)), 253 | None, 254 | Some((PieceType::King, PieceColor::White)), 255 | ], 256 | [ 257 | None, 258 | Some((PieceType::King, PieceColor::Black)), 259 | None, 260 | None, 261 | None, 262 | None, 263 | None, 264 | None, 265 | ], 266 | [None, None, None, None, None, None, None, None], 267 | [ 268 | None, 269 | None, 270 | None, 271 | None, 272 | None, 273 | None, 274 | Some((PieceType::Rook, PieceColor::Black)), 275 | None, 276 | ], 277 | [None, None, None, None, None, None, None, None], 278 | [None, None, None, None, None, None, None, None], 279 | [None, None, None, None, None, None, None, None], 280 | ]; 281 | // We setup the game 282 | let game_board = GameBoard::new(custom_board, vec![], vec![]); 283 | let mut game = Game::new(game_board, PieceColor::Black); 284 | game.game_board.board = custom_board; 285 | 286 | // Move the pawn to a promote cell 287 | game.execute_move(&Coord::new(1, 5), &Coord::new(0, 5)); 288 | assert!(game.game_board.is_latest_move_promotion()); 289 | 290 | // Promote the pawn 291 | game.promote_piece(); 292 | 293 | // The black king gets checkmated 294 | game.player_turn = PieceColor::White; 295 | assert!(game.game_board.is_draw(game.player_turn)); 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /tests/utils.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use chess_tui::utils::{convert_notation_into_position, convert_position_into_notation}; 4 | 5 | #[test] 6 | fn convert_position_into_notation_1() { 7 | assert_eq!(convert_position_into_notation("7152"), "b1-c3") 8 | } 9 | 10 | #[test] 11 | fn convert_position_into_notation_2() { 12 | assert_eq!(convert_position_into_notation("0257"), "c8-h3") 13 | } 14 | 15 | #[test] 16 | fn convert_notation_into_position_1() { 17 | assert_eq!(convert_notation_into_position("c8b7"), "0211") 18 | } 19 | #[test] 20 | fn convert_notation_into_position_2() { 21 | assert_eq!(convert_notation_into_position("g7h8"), "1607") 22 | } 23 | #[test] 24 | fn convert_notation_into_position_3() { 25 | assert_eq!(convert_notation_into_position("g1f3"), "7655") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /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/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/blog/wip.md: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | # This is a WIP -------------------------------------------------------------------------------- /website/docs/Code Architecture/Game.md: -------------------------------------------------------------------------------- 1 | ## Global architecture 2 | 3 | ```mermaid 4 | classDiagram 5 | 6 | class App { 7 | +bool running 8 | +Game game 9 | +Pages current_page 10 | +Option current_popup 11 | +Option selected_color 12 | +Option hosting 13 | +Option host_ip 14 | +u8 menu_cursor 15 | +Option chess_engine_path 16 | 17 | +toggle_help_popup() 18 | +toggle_credit_popup() 19 | +go_to_home() 20 | +tick() 21 | +quit() 22 | +menu_cursor_up(l: u8) 23 | +menu_cursor_right(l: u8) 24 | +menu_cursor_left(l: u8) 25 | +menu_cursor_down(l: u8) 26 | +color_selection() 27 | +restart() 28 | +menu_select() 29 | +update_config() 30 | +setup_game_server(host_color: PieceColor) 31 | +create_opponent() 32 | +hosting_selection() 33 | +bot_setup() 34 | +get_host_ip() IpAddr 35 | } 36 | class Game { 37 | +GameBoard game_board 38 | +UI ui 39 | +Option bot 40 | +Option opponent 41 | +PieceColor player_turn 42 | +GameState game_state 43 | 44 | +new(game_board: GameBoard, player_turn: PieceColor) 45 | +set_board(game_board: GameBoard) 46 | +set_player_turn(player_turn: PieceColor) 47 | +switch_player_turn() 48 | +select_cell() 49 | +execute_bot_move() 50 | +promote_piece() 51 | +execute_move(from: Coord, to: Coord) 52 | +handle_cell_click() 53 | +already_selected_cell_action() 54 | +handle_promotion() 55 | +execute_opponent_move() 56 | +handle_multiplayer_promotion() 57 | } 58 | 59 | class GameBoard { 60 | +board : Board 61 | +move_history: Vec 62 | +board_history: Vec 63 | +consecutive_non_pawn_or_capture: i32 64 | +white_taken_pieces: Vec 65 | +black_taken_pieces: Vec 66 | +new(board: Board, move_history: List, board_history: List) : GameBoard 67 | +get_authorized_positions(player_turn: PieceColor, coordinates: Coord) : List 68 | +add_piece_to_taken_pieces(from: Coord, to: Coord, player_turn: PieceColor) : void 69 | +reset() : void 70 | +flip_the_board() : void 71 | +is_checkmate(player_turn: PieceColor) : bool 72 | +is_draw() : bool 73 | +fen_position(is_bot_starting: bool, player_turn: PieceColor) : string 74 | } 75 | 76 | 77 | class UI { 78 | +cursor_coordinates : Coord 79 | +selected_coordinates : Coord 80 | +selected_piece_cursor : int 81 | +promotion_cursor : int 82 | +old_cursor_position : Coord 83 | +top_x : u16 84 | +top_y : u16 85 | +width : u16 86 | +height : u16 87 | +mouse_used : bool 88 | +display_mode : DisplayMode 89 | +reset() : void 90 | +is_cell_selected() : bool 91 | +move_selected_piece_cursor(first_time_moving: bool, direction: i8, authorized_positions: Vec) : void 92 | +cursor_up(authorized_positions: Vec) : void 93 | +cursor_down(authorized_positions: Vec) : void 94 | +cursor_left(authorized_positions: Vec) : void 95 | +cursor_left_promotion() : void 96 | +cursor_right(authorized_positions: Vec) : void 97 | +cursor_right_promotion() : void 98 | +unselect_cell() : void 99 | +history_render(area: Rect, frame: Frame, move_history: Vec) : void 100 | +white_material_render(area: Rect, frame: Frame, white_taken_pieces: Vec) : void 101 | +black_material_render(area: Rect, frame: Frame, black_taken_pieces: Vec) : void 102 | +board_render(area: Rect, frame: Frame, game: Game) : void 103 | } 104 | 105 | class Bot { 106 | +engine: Engine 107 | +bot_will_move: bool 108 | +is_bot_starting 109 | +set_engine(engine_path: &str) 110 | +create_engine(engine_path: &str): Engine 111 | +get_bot_move(fen_position: String): String 112 | } 113 | 114 | class Opponent { 115 | +Option stream 116 | +bool opponent_will_move 117 | +PieceColor color 118 | +bool game_started 119 | 120 | +copy() : Opponent 121 | +new(addr: String, color: Option) : Opponent 122 | +start_stream(addr: &str) : void 123 | +send_end_game_to_server() : void 124 | +send_move_to_server(move_to_send: &PieceMove, promotion_type: Option) : void 125 | +read_stream() : String 126 | } 127 | 128 | class Coord { 129 | +row: int 130 | +col: int 131 | +is_valid(): bool 132 | +new(row: int, col: int): Coord 133 | } 134 | 135 | class PieceMove { 136 | +piece_type: PieceType 137 | +piece_color: PieceColor 138 | +from: Coord 139 | +to: Coord 140 | } 141 | 142 | class PieceColor { 143 | <> 144 | +White 145 | +Black 146 | } 147 | 148 | class PieceType { 149 | <> 150 | +King 151 | +Queen 152 | +Rook 153 | +Bishop 154 | +Knight 155 | +Pawn 156 | } 157 | 158 | class GameState { 159 | <> 160 | +Checkmate 161 | +Draw 162 | +Playing 163 | +Promotion 164 | } 165 | 166 | App --> Game : "owns" 167 | Game "1" --> "1" GameBoard 168 | Game --> "1" GameState : type 169 | Game "1" --> "1" UI 170 | Game "1" --> "0..1" Bot 171 | GameBoard "1" --> "0..*" PieceMove 172 | GameBoard "1" --> "0..*" Coord 173 | UI "1" --> "1" Coord 174 | Bot "1" --> "1" PieceMove : generates 175 | PieceMove "1" --> "1" Coord : from_to 176 | Coord "1" --> "1" PieceColor : color 177 | Coord "1" --> "1" PieceType : type 178 | Game "1" --> "0..1" Opponent : "has" 179 | Opponent "1" --> "1" PieceColor : color 180 | ``` 181 | 182 | 183 | This Class diagram allows us to have a quick overview of the game architecture and how the different struct interact. 184 | 185 | ### App 186 | 187 | The App struct is the main struct of the game. It will be the one storing the game when we play it as well as the different pages we can navigate to. 188 | 189 | 190 | ### Game 191 | 192 | The Game struct represent a new game. As variable a game stores a GameBoard, a UI, a Bot (if it's again a bot), the player turn and the game state. 193 | 194 | 195 | ### GameBoard 196 | 197 | The GameBoard struct represent everything to the board, the table storing the pieces, the move history, the taken pieces, the consecutive non pawn or capture moves and the player turn. 198 | 199 | ### UI 200 | 201 | The UI struct represent the user interface. It stores the cursor position, the selected piece, the promotion cursor, the old cursor position, the top left corner of the board, the width and height of the board, if the mouse is used and the display mode. 202 | It also handles the rendering of the board, taken pieces and the rendering of the move history. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /website/docs/Code Architecture/pieces.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: Pieces 3 | title: Pieces 4 | sidebar_position: 2 5 | --- 6 | 7 | ## Project architecture 8 | 9 | Let's begin by looking at the pieces in the game. 10 | ## Class diagram 11 | 12 | ### Pieces 13 | 14 | ```mermaid 15 | classDiagram 16 | 17 | class PieceType{ 18 | +authorized_positions() 19 | +protected_positions() 20 | +piece_to_utf_enum() 21 | +piece_to_fen_enum() 22 | +piece_type_to_string_enum() 23 | +partial_cmp() 24 | +cmp() 25 | } 26 | 27 | class PieceColor { 28 | <> 29 | Black 30 | White 31 | +opposite() PieceColor 32 | } 33 | 34 | class Pawn { 35 | +authorized_positions() 36 | +protected_positions() 37 | +to_string() 38 | +piece_move() 39 | } 40 | 41 | class Rook { 42 | +authorized_positions() 43 | +protected_positions() 44 | +to_string() 45 | +piece_move() 46 | } 47 | 48 | class Knight { 49 | +authorized_positions() 50 | +protected_positions() 51 | +to_string() 52 | +piece_move() 53 | } 54 | 55 | class Bishop { 56 | +authorized_positions() 57 | +protected_positions() 58 | +to_string() 59 | +piece_move() 60 | } 61 | 62 | class Queen { 63 | +authorized_positions() 64 | +protected_positions() 65 | +to_string() 66 | +piece_move() 67 | } 68 | 69 | class King { 70 | +authorized_positions() 71 | +protected_positions() 72 | +to_string() 73 | +piece_move() 74 | } 75 | 76 | class Coord { 77 | <> 78 | +row: u8 79 | +col: u8 80 | } 81 | 82 | 83 | class PieceMove { 84 | +piece_type: PieceType 85 | +piece_color: PieceColor 86 | +from: Coord 87 | +to: Coord 88 | } 89 | 90 | class Movable { 91 | <> 92 | +piece_move() 93 | } 94 | 95 | 96 | class Position { 97 | <> 98 | +authorized_positions() 99 | +protected_positions() 100 | } 101 | 102 | PieceType <|-- Pawn 103 | PieceType <|-- Rook 104 | PieceType <|-- Bishop 105 | PieceType <|-- Knight 106 | PieceType <|-- King 107 | PieceType <|-- Queen 108 | 109 | PieceMove --> PieceType 110 | PieceMove --> PieceColor 111 | PieceMove --> Coord 112 | 113 | PieceType ..|> Movable 114 | PieceType ..|> Position 115 | 116 | Pawn --> Coord 117 | Rook --> Coord 118 | Knight --> Coord 119 | Bishop --> Coord 120 | Queen --> Coord 121 | King --> Coord 122 | 123 | Movable <|.. Pawn 124 | Movable <|.. Rook 125 | Movable <|.. Knight 126 | Movable <|.. Bishop 127 | Movable <|.. Queen 128 | Movable <|.. King 129 | 130 | Position <|.. Pawn 131 | Position <|.. Rook 132 | Position <|.. Knight 133 | Position <|.. Bishop 134 | Position <|.. Queen 135 | Position <|.. King 136 | ``` 137 | 138 | This schema can be a little bit overwhelming but let's break it apart. 139 | 140 | #### PieceType 141 | 142 | This class is basically the parent class of all the pieces. It contains the methods that are common to all the pieces. Such as authorized positions, protected positions, etc. 143 | 144 | #### PieceColor 145 | 146 | This is an enum that contains the two colors of the pieces. Black and White. 147 | 148 | #### Pawn, Rook, Knight, Bishop, Queen, King 149 | 150 | These are the classes that represent the pieces. They all inherit from PieceType and implement the methods that are specific to their type. 151 | 152 | #### Movable and Position 153 | 154 | These are rust traits that are implemented by the pieces. Movable is a trait that represents a piece that can move (piece_move method). Position is a trait that shows the authorized and protected positions of a piece. 155 | 156 | #### Coord 157 | 158 | This is a data structure that represents a position on the board. It contains a row and a column. 159 | 160 | #### PieceMove 161 | 162 | This is a data structure that represents a move of a piece. It contains the type of the piece, the color of the piece, the starting position, and the ending position. This is mainly used in the board structure that we will see later. 163 | 164 | -------------------------------------------------------------------------------- /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 | :::tip 39 | Use ASCII mode if you experience display issues with the default Unicode pieces in your terminal. 40 | ::: -------------------------------------------------------------------------------- /website/docs/Configuration/engine.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: engine 3 | title: Chess Engine 4 | sidebar_position: 4 5 | --- 6 | 7 | # Chess Engine Configuration 8 | 9 | To play against a computer opponent, you need to configure a UCI-compatible chess engine. 10 | 11 | ## Configuration 12 | 13 | Set the path to your chess engine in the configuration file: 14 | 15 | ```toml 16 | engine_path = "/path/to/your/engine" 17 | ``` 18 | 19 | ## Supported Engines 20 | 21 | Any UCI-compatible chess engine should work. Some popular options include: 22 | - Stockfish 23 | - Leela Chess Zero 24 | - Komodo 25 | 26 | :::note 27 | The engine path must point to a valid UCI-compatible chess engine executable. If not configured correctly, the bot play option will be disabled. 28 | ::: -------------------------------------------------------------------------------- /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/chess-tui/config.toml`. This section covers all available configuration options. 10 | 11 | ## Configuration File 12 | 13 | The configuration file is automatically created when you first run chess-tui. You can modify it manually to customize your experience: 14 | 15 | ```toml 16 | # ~/.config/chess-tui/config.toml 17 | 18 | # Display mode: "DEFAULT" or "ASCII" 19 | display_mode = "DEFAULT" 20 | 21 | # Chess engine path (optional) 22 | engine_path = "/path/to/your/engine" 23 | 24 | # Logging level: "OFF", "ERROR", "WARN", "INFO", "DEBUG", or "TRACE" 25 | log_level = "OFF" 26 | ``` -------------------------------------------------------------------------------- /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/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 | ### Available Log Levels 20 | 21 | - `OFF` - Logging disabled (default) 22 | - `ERROR` - Only error messages 23 | - `WARN` - Warning and error messages 24 | - `INFO` - Informational messages, warnings, and errors 25 | - `DEBUG` - Detailed debug information plus all above 26 | - `TRACE` - Most detailed logging level 27 | 28 | ## Log Files 29 | 30 | When logging is enabled, log files are stored in: 31 | ``` 32 | ~/.config/chess-tui/logs/ 33 | ``` 34 | 35 | Each log file is named with a timestamp: 36 | ``` 37 | chess-tui_YYYY-MM-DD_HH-MM-SS.log 38 | ``` 39 | 40 | For example: `chess-tui_2024-03-20_15-30-45.log` 41 | 42 | ## Usage 43 | 44 | Logs can be helpful when: 45 | - Debugging multiplayer connection issues 46 | - Understanding game state changes 47 | - Investigating unexpected behavior 48 | - Developing new features 49 | 50 | :::tip 51 | 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. 52 | ::: -------------------------------------------------------------------------------- /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/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/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: -------------------------------------------------------------------------------- /website/docs/Installation/Docker.md: -------------------------------------------------------------------------------- 1 | # Docker 2 | 3 | **Chess-tui** can be runned in a Docker container. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | docker run --rm -it ghcr.io/thomas-mauran/chess-tui:main 9 | ``` 10 | -------------------------------------------------------------------------------- /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/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 | ### Available Log Levels 14 | 15 | - `OFF` - Logging disabled (default) 16 | - `ERROR` - Only error messages 17 | - `WARN` - Warning and error messages 18 | - `INFO` - Informational messages, warnings, and errors 19 | - `DEBUG` - Detailed debug information plus all above 20 | - `TRACE` - Most detailed logging level 21 | 22 | ## Log Files 23 | 24 | When logging is enabled, log files are stored in: 25 | ``` 26 | ~/.config/chess-tui/logs/ 27 | ``` 28 | 29 | Each log file is named with a timestamp: 30 | ``` 31 | chess-tui_YYYY-MM-DD_HH-MM-SS.log 32 | ``` 33 | 34 | For example: `chess-tui_2024-03-20_15-30-45.log` 35 | 36 | ## Usage 37 | 38 | Logs can be helpful when: 39 | - Debugging multiplayer connection issues 40 | - Understanding game state changes 41 | - Investigating unexpected behavior 42 | - Developing new features 43 | 44 | :::tip 45 | 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. 46 | ::: -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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?channel=24.05&show=chess-tui&from=0&size=50&sort=relevance&type=packages&query=chess-tui). -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/Multiplayer/bore-port.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/589ebd9fb1ebebd9ee02277e6564619d0a8092f8/website/docs/Multiplayer/bore-port.png -------------------------------------------------------------------------------- /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 | ### Welcome to the chess-tui documentation! 12 | 13 | Chess TUI is a terminal chess game written in [Rust](https://www.rust-lang.org/) . It is a simple and easy-to-use chess game that you can play in your terminal. 14 | 15 | 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. 16 |
17 |
18 | logo 19 |
20 |
21 | 22 | 23 | 24 | :::warning Disclaimer 25 | This project is still in development and some features may not be available yet. 26 | ::: 27 | 28 | ## Quick install 29 | 30 | ```bash 31 | cargo install chess-tui 32 | ``` 33 | 34 | **Then run the game with:** 35 | ```bash 36 | chess-tui 37 | ``` 38 | 39 | You can find a more detailed installation guide with your favorite package manager in the [Installation](/docs/Installation/Cargo) section. -------------------------------------------------------------------------------- /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: 'A rusty chess game in your terminal 🦀', 10 | favicon: 'img/favicon.ico', 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 | }, 38 | blog: { 39 | showReadingTime: true, 40 | feedOptions: { 41 | type: ['rss', 'atom'], 42 | xslt: true, 43 | }, 44 | editUrl: 45 | 'https://github.com/thomas-mauran/chess-tui/', 46 | onInlineTags: 'warn', 47 | onInlineAuthors: 'warn', 48 | onUntruncatedBlogPosts: 'warn', 49 | }, 50 | theme: { 51 | customCss: './src/css/custom.css', 52 | }, 53 | } satisfies Preset.Options, 54 | ], 55 | ], 56 | 57 | themeConfig: { 58 | image: 'img/social-card.png', 59 | navbar: { 60 | title: 'Chess-tui', 61 | logo: { 62 | alt: 'Chess tui logo', 63 | src: 'img/logo.png', 64 | }, 65 | items: [ 66 | { 67 | type: 'docSidebar', 68 | sidebarId: 'tutorialSidebar', 69 | position: 'left', 70 | label: 'Documentation', 71 | }, 72 | { to: '/blog', label: 'Blog', position: 'left' }, 73 | { 74 | href: 'https://github.com/thomas-mauran/chess-tui', 75 | position: 'right', 76 | className: 'header-github-link', 77 | 'aria-label': 'GitHub repository', 78 | html: `GitHub Stars`, 79 | }, 80 | ], 81 | }, 82 | footer: { 83 | style: 'dark', 84 | links: [ 85 | { 86 | title: 'Docs', 87 | items: [ 88 | { 89 | label: 'Tutorial', 90 | to: '/docs/intro', 91 | }, 92 | ], 93 | }, 94 | { 95 | title: 'Community', 96 | items: [ 97 | { 98 | label: 'Github Discussions', 99 | href: 'https://github.com/thomas-mauran/chess-tui/discussions', 100 | }, 101 | ], 102 | }, 103 | { 104 | title: 'More', 105 | items: [ 106 | { 107 | label: 'Blog', 108 | to: '/blog', 109 | }, 110 | { 111 | label: 'GitHub', 112 | href: 'https://github.com/thomas-mauran/chess-tui/', 113 | }, 114 | ], 115 | }, 116 | ], 117 | copyright: `Copyright © ${new Date().getFullYear()} Thomas Mauran, Inc. Built with Docusaurus.`, 118 | }, 119 | } satisfies Preset.ThemeConfig, 120 | 121 | markdown: { 122 | mermaid: true, 123 | }, 124 | themes: ['@docusaurus/theme-mermaid'], 125 | }; 126 | 127 | export default config; 128 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/Cargo', 'Installation/Build from source', 'Installation/NetBSD', 'Installation/Arch Linux', 'Installation/NixOS', 'Installation/Docker'], 10 | }, 11 | { 12 | type: 'category', 13 | label: 'Configuration', 14 | items: [ 15 | 'Configuration/configuration-intro', 16 | 'Configuration/display', 17 | 'Configuration/logging', 18 | 'Configuration/engine', 19 | ], 20 | }, 21 | { 22 | type: 'category', 23 | label: 'Multiplayer', 24 | items: ['Multiplayer/Local multiplayer', 'Multiplayer/Online multiplayer'], 25 | }, 26 | { 27 | type: 'category', 28 | label: 'Code Architecture', 29 | items: ['Code Architecture/Intro', 'Code Architecture/Pieces', 'Code Architecture/Game'], 30 | }, 31 | ], 32 | }; 33 | 34 | export default sidebars; 35 | -------------------------------------------------------------------------------- /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 | Games over the network are a work in progress. 25 | 26 | ), 27 | }, 28 | { 29 | title: 'Lichess Integration 🌐', 30 | description: ( 31 | <> 32 | This is a work in progress. You will soon be able to play online on Lichess. 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 | -------------------------------------------------------------------------------- /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 | .featureGif { 9 | max-width: 100%; /* Ensure the image doesn't overflow its container */ 10 | height: auto; /* Maintain aspect ratio */ 11 | display: inline-block; /* Ensure proper alignment */ 12 | } 13 | 14 | -------------------------------------------------------------------------------- /website/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /* Root variables for the chess-inspired theme */ 2 | :root { 3 | --ifm-color-primary: #3e3e3e; /* Dark color for background or primary accents */ 4 | --ifm-color-primary-dark: #2b2b2b; /* Darker color for deeper contrasts */ 5 | --ifm-color-primary-darker: #1a1a1a; /* Even darker shade for dark accents */ 6 | --ifm-color-primary-darkest: #101010; /* Deepest shade for background */ 7 | --ifm-color-primary-light: #c4b79f; /* Light, neutral brown, akin to wooden chess pieces */ 8 | --ifm-color-primary-lighter: #d9c8a6; /* Lighter brown */ 9 | --ifm-color-primary-lightest: #f2e6d1; /* Lightest shade, cream-colored */ 10 | --ifm-code-font-size: 95%; 11 | --docusaurus-highlighted-code-line-bg: rgba(62, 62, 62, 0.1); /* Highlighted code line background */ 12 | } 13 | 14 | /* Dark mode specific colors */ 15 | [data-theme='dark'] { 16 | --ifm-color-primary: #3e3e3e; /* Dark color for background or primary accents */ 17 | --ifm-color-primary-dark: #2b2b2b; /* Darker color for deeper contrasts */ 18 | --ifm-color-primary-darker: #1a1a1a; /* Even darker shade for dark accents */ 19 | --ifm-color-primary-darkest: #101010; /* Deepest shade for background */ 20 | --ifm-color-primary-light: #a69b77; /* Lighter brown for dark mode */ 21 | --ifm-color-primary-lighter: #bfae85; /* A bit warmer brown */ 22 | --ifm-color-primary-lightest: #e1d0a0; /* Light cream shade for dark mode */ 23 | --docusaurus-highlighted-code-line-bg: rgba(62, 62, 62, 0.3); /* Highlighted code line background */ 24 | } 25 | 26 | a { 27 | color: var(--ifm-color-primary-light); /* Light color for links */ 28 | } 29 | 30 | a:hover { 31 | color: var(--ifm-color-primary-lighter); /* Slightly warmer color for hover state */ 32 | } 33 | 34 | .navbar__link--active { 35 | color: var(--ifm-color-primary-lighter); /* Slightly warmer color for focus state */ 36 | } 37 | 38 | .navbar__link:hover { 39 | color: var(--ifm-color-primary-lighter); /* Slightly warmer color for hover state */ 40 | } 41 | 42 | /* Apply background color and text color to footer */ 43 | footer.footer { 44 | background-color: var(--ifm-color-primary-darkest); /* Darkest background for footer */ 45 | color: var(--ifm-color-primary-light); /* Light text color for footer */ 46 | } 47 | 48 | /* Customize footer links */ 49 | footer.footer a { 50 | color: var(--ifm-color-primary-lightest); /* Light color for links */ 51 | } 52 | 53 | footer.footer a:hover { 54 | color: var(--ifm-color-primary-lighter); /* Slightly warmer color for hover state */ 55 | } 56 | 57 | /* Ensure the theme toggle button aligns properly */ 58 | .navbar__toggle { 59 | display: flex; 60 | align-items: center; 61 | justify-content: center; 62 | height: 100%; 63 | } 64 | 65 | /* Adjust spacing or alignment if necessary */ 66 | .navbar__toggle svg { 67 | vertical-align: middle; 68 | height: 1.2em; /* Adjust size to match GitHub icons */ 69 | margin-left: 8px; /* Fine-tune the spacing */ 70 | margin-right: 8px; /* Fine-tune the spacing */ 71 | } 72 | 73 | /* Adjust GitHub badge alignment */ 74 | .header-github-link img { 75 | vertical-align: middle; 76 | } 77 | -------------------------------------------------------------------------------- /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 | .hero__subtitle { 18 | font-size: 1.5rem; /* Adjust size as needed */ 19 | font-weight: bold; 20 | color: #ebedf0; 21 | text-align: center; 22 | } 23 | 24 | .heroBanner { 25 | padding: 4rem 0; 26 | text-align: center; 27 | position: relative; 28 | overflow: hidden; 29 | } 30 | 31 | @media screen and (max-width: 996px) { 32 | .heroBanner { 33 | padding: 2rem; 34 | } 35 | } 36 | 37 | .buttons { 38 | display: flex; 39 | align-items: center; 40 | justify-content: center; 41 | } 42 | 43 | .featureGif { 44 | border-radius: 5px; 45 | max-width: 70%; 46 | height: auto; 47 | display: inline-block; 48 | box-shadow: 0 10px 20px rgba(0, 0, 0, 0.8); 49 | } 50 | 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 | -------------------------------------------------------------------------------- /website/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/589ebd9fb1ebebd9ee02277e6564619d0a8092f8/website/static/.nojekyll -------------------------------------------------------------------------------- /website/static/gif/demo-two-player.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/589ebd9fb1ebebd9ee02277e6564619d0a8092f8/website/static/gif/demo-two-player.gif -------------------------------------------------------------------------------- /website/static/gif/helper.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/589ebd9fb1ebebd9ee02277e6564619d0a8092f8/website/static/gif/helper.gif -------------------------------------------------------------------------------- /website/static/gif/multiplayer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/589ebd9fb1ebebd9ee02277e6564619d0a8092f8/website/static/gif/multiplayer.gif -------------------------------------------------------------------------------- /website/static/gif/play_against_black_bot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/589ebd9fb1ebebd9ee02277e6564619d0a8092f8/website/static/gif/play_against_black_bot.gif -------------------------------------------------------------------------------- /website/static/gif/play_against_white_bot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/589ebd9fb1ebebd9ee02277e6564619d0a8092f8/website/static/gif/play_against_white_bot.gif -------------------------------------------------------------------------------- /website/static/img/ascii-display-mode-board.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/589ebd9fb1ebebd9ee02277e6564619d0a8092f8/website/static/img/ascii-display-mode-board.png -------------------------------------------------------------------------------- /website/static/img/default-display-mode-board.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/589ebd9fb1ebebd9ee02277e6564619d0a8092f8/website/static/img/default-display-mode-board.png -------------------------------------------------------------------------------- /website/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/589ebd9fb1ebebd9ee02277e6564619d0a8092f8/website/static/img/favicon.ico -------------------------------------------------------------------------------- /website/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/589ebd9fb1ebebd9ee02277e6564619d0a8092f8/website/static/img/logo.png -------------------------------------------------------------------------------- /website/static/img/social-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomas-mauran/chess-tui/589ebd9fb1ebebd9ee02277e6564619d0a8092f8/website/static/img/social-card.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 | --------------------------------------------------------------------------------