├── .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 | 
6 |
7 |
8 |
9 | [](https://opensource.org/licenses/MIT)[](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 | [](https://repology.org/project/chess-tui/versions)
28 |
29 | ### Features
30 |
31 |
32 | Helper menu
33 |
34 |
35 |
36 | Local 2 player mode
37 |
38 |
39 |
40 | Online multiplayer
41 |
42 |
43 |
44 | Draws
45 |
46 | - Stalemate
47 | - 50 moves rules
48 | - 3 time repetition of the same position
49 |
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 |
59 |
60 | Play the black pieces
61 |
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 |

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 |

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 | [](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 | 
--------------------------------------------------------------------------------
/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 | 
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 | 
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 |

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: `
`,
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 |
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 |
--------------------------------------------------------------------------------