├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── README.md ├── requirements.txt ├── setup.py └── termichess ├── __init__.py ├── __main__.py ├── app.py ├── assets ├── __init__.py ├── pieces │ ├── asciiart │ │ ├── __init__.py │ │ ├── basic.py │ │ ├── char.py │ │ ├── computer_first.py │ │ ├── computer_second.py │ │ ├── computer_third.py │ │ ├── geometric.py │ │ ├── glyph.py │ │ ├── got.py │ │ ├── mahabharat.py │ │ ├── minimalistic.py │ │ ├── potter.py │ │ ├── rad.py │ │ ├── retro.py │ │ ├── scientist.py │ │ └── shaded.py │ └── v1 │ │ ├── black_bishop.png │ │ ├── black_king.png │ │ ├── black_knight.png │ │ ├── black_pawn.png │ │ ├── black_queen.png │ │ ├── black_rook.png │ │ ├── white_bishop.png │ │ ├── white_king.png │ │ ├── white_knight.png │ │ ├── white_pawn.png │ │ ├── white_queen.png │ │ └── white_rook.png └── sound │ ├── check.wav │ └── move.wav ├── chess_board.py ├── config_screen.py ├── piece_generator └── create_pieces.py ├── promotion.py ├── sidebar.py └── utils.py /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: "3.10" 23 | 24 | - name: Install system dependencies 25 | run: | 26 | sudo apt-get update 27 | sudo apt-get install -y libasound2-dev 28 | 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install setuptools wheel 33 | 34 | - name: Build package 35 | run: | 36 | python setup.py sdist bdist_wheel 37 | 38 | - name: Install built package 39 | run: | 40 | pip install dist/*.tar.gz 41 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | 16 | - name: Set up Python 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: "3.10" 20 | 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | 26 | - name: Build package 27 | run: | 28 | python setup.py sdist bdist_wheel 29 | 30 | - name: Publish to PyPI 31 | env: 32 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 33 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 34 | run: | 35 | twine upload dist/* 36 | docker: 37 | runs-on: ubuntu-latest 38 | 39 | steps: 40 | - name: Checkout code 41 | uses: actions/checkout@v2 42 | 43 | - name: Set up Docker Buildx 44 | uses: docker/setup-buildx-action@v2 45 | 46 | - name: Login to GitHub Container Registry 47 | uses: docker/login-action@v2 48 | with: 49 | registry: ghcr.io 50 | username: ${{ github.repository_owner }} 51 | password: ${{ secrets.GITHUB_TOKEN }} 52 | 53 | - name: Build and push Docker image (multi-arch) 54 | uses: docker/build-push-action@v3 55 | with: 56 | context: . 57 | push: true 58 | tags: | 59 | ghcr.io/${{ github.repository }}:latest 60 | platforms: linux/amd64,linux/arm64 61 | build-args: | 62 | DOCKER_CLI_EXPERIMENTAL=enabled 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | 7 | # Distribution / packaging 8 | .Python 9 | build/ 10 | develop-eggs/ 11 | dist/ 12 | downloads/ 13 | eggs/ 14 | .eggs/ 15 | lib/ 16 | lib64/ 17 | parts/ 18 | sdist/ 19 | var/ 20 | wheels/ 21 | share/python-wheels/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | *.manifest 28 | *.spec 29 | 30 | # Installer logs 31 | pip-log.txt 32 | pip-delete-this-directory.txt 33 | 34 | # Unit test / coverage reports 35 | htmlcov/ 36 | .tox/ 37 | .nox/ 38 | .coverage 39 | .coverage.* 40 | .cache 41 | nosetests.xml 42 | coverage.xml 43 | *.cover 44 | *.py,cover 45 | .hypothesis/ 46 | .pytest_cache/ 47 | 48 | # Jupyter Notebook 49 | .ipynb_checkpoints 50 | 51 | # IPython 52 | profile_default/ 53 | ipython_config.py 54 | 55 | # Environments 56 | .env 57 | .venv 58 | env/ 59 | venv/ 60 | ENV/ 61 | env.bak/ 62 | venv.bak/ 63 | 64 | # IDEs and editors 65 | .idea/ 66 | .vscode/ 67 | *.swp 68 | *.swo 69 | *~ 70 | 71 | # OS generated files 72 | .DS_Store 73 | .DS_Store? 74 | ._* 75 | .Spotlight-V100 76 | .Trashes 77 | ehthumbs.db 78 | Thumbs.db 79 | 80 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim 2 | ENV TERM=xterm-256color COLORTERM=truecolor 3 | 4 | WORKDIR /app 5 | 6 | COPY . . 7 | 8 | RUN apt-get update && apt-get install -y build-essential libasound2-dev stockfish 9 | RUN pip install --no-cache-dir -r requirements.txt 10 | 11 | RUN mv /usr/games/stockfish /usr/local/bin/ 12 | 13 | RUN pip install -e . 14 | 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | include MANIFEST.in 4 | include requirements.txt 5 | recursive-include termichess * -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Termichess 2 | 3 | A game of chess in your terminal. Built using the amazing TUI framework `Textual`. 4 | 5 | ## Demo 6 | 7 | https://github.com/user-attachments/assets/305f1e5b-9c76-474f-b30e-5bee12ac3be8 8 | 9 | ## Features 10 | 11 | - ♟️ Play chess right in your terminal 12 | - 🧠 Stockfish integration for chess engine 13 | - ✅ Move validation and legal move highlighting 14 | - 🎨 Change board themes 15 | - 🤓 Variety of geeky board pieces 16 | - ⚙️ Configuration menu for different options to change on the fly 17 | 18 | ## Installation 19 | 20 | 1. Please use Python 3.10 within your favourite venv. I have only tested currently only on `python 3.10` 21 | 22 | 2. Install Stockfish: 23 | 24 | - On Ubuntu or Debian: `sudo apt-get install stockfish` 25 | - On macOS with Homebrew: `brew install stockfish` 26 | - On Windows, download from [Stockfish's official website](https://stockfishchess.org/download/) and add it to your system PATH. 27 | 28 | 3. Install TermiChess: 29 | 30 | `pip install termichess` 31 | 32 | ## Running the Game 33 | 34 | After installation, you can start the game by simply running: 35 | 36 | `termichess` 37 | 38 | - To exit out at any time , Press `q` . 39 | - To restart the game click on the `Restart` button 40 | 41 | ## Running via Docker 42 | 43 | You can also run `termichess` via Docker if that's what you prefer. 44 | 45 | `docker run -it ghcr.io/whiletruelearn/termichess:latest termichess` 46 | 47 | Note : Please keep `sound` config to off while using `docker` 48 | 49 | ## Contributing 50 | 51 | Contributions are welcome! Please feel free to submit a Pull Request. 52 | 53 | ## License 54 | 55 | This project is licensed under the Apache License - see the LICENSE file for details. 56 | 57 | ## Acknowledgments 58 | 59 | - [Textual](https://github.com/Textualize/textual) for the TUI framework 60 | - [python-chess](https://github.com/niklasf/python-chess) for chess logic 61 | - [Stockfish](https://stockfishchess.org/) for the chess engine 62 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | textual==0.82.0 2 | rich-pixels 3 | pillow==10.3.0 4 | chess==1.11.0 5 | pillow==10.3.0 6 | rich-pixels==3.0.1 7 | simpleaudio==1.0.4 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md", "r", encoding="utf-8") as fh: 4 | long_description = fh.read() 5 | 6 | setup( 7 | name="termichess", 8 | version="0.1.4", 9 | author="Krishna Sangeeth KS", 10 | author_email="kskrishnasangeeth@gmail.com", 11 | description="A chess game on your terminal", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/whiletruelearn/termichess", 15 | packages=find_packages(), 16 | include_package_data=True, 17 | package_data={ 18 | "termichess": ["assets/*"], 19 | }, 20 | install_requires=[ 21 | "textual==0.82.0", 22 | "pillow==10.3.0", 23 | "chess==1.11.0", 24 | "rich-pixels==3.0.1", 25 | "simpleaudio==1.0.4" 26 | ], 27 | entry_points={ 28 | "console_scripts": [ 29 | "termichess=termichess.__main__:main", 30 | ], 31 | }, 32 | classifiers=[ 33 | "Programming Language :: Python :: 3", 34 | "License :: OSI Approved :: MIT License", 35 | "Operating System :: OS Independent", 36 | ], 37 | python_requires=">=3.10", 38 | ) 39 | -------------------------------------------------------------------------------- /termichess/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whiletruelearn/termichess/8fe834aec88e32d0e56dd9743df59ae33e6113b2/termichess/__init__.py -------------------------------------------------------------------------------- /termichess/__main__.py: -------------------------------------------------------------------------------- 1 | from .app import ChessApp 2 | 3 | def main(): 4 | app = ChessApp() 5 | app.run() 6 | 7 | if __name__ == "__main__": 8 | main() -------------------------------------------------------------------------------- /termichess/app.py: -------------------------------------------------------------------------------- 1 | from textual.app import App, ComposeResult 2 | from textual.containers import Container, Horizontal 3 | from textual.widgets import Button 4 | from textual import events 5 | from textual.reactive import reactive 6 | import simpleaudio as sa 7 | from random import choice, random 8 | import chess 9 | from chess.engine import SimpleEngine 10 | from termichess.utils import ASSETS_PATH, CONF 11 | from termichess.config_screen import ConfigScreen 12 | from termichess.sidebar import InfoPanel, MoveHistory 13 | from termichess.chess_board import ChessBoard, ChessSquare 14 | from termichess.promotion import PawnPromotion 15 | 16 | 17 | 18 | class ChessApp(App): 19 | 20 | CSS = """ 21 | Screen { 22 | layout: horizontal; 23 | } 24 | 25 | ChessBoard { 26 | width: 80%; 27 | height: 100%; 28 | } 29 | 30 | #board-grid { 31 | width: 100%; 32 | height: 100%; 33 | layout: grid; 34 | grid-size: 8 8; 35 | grid-gutter: 0; 36 | } 37 | 38 | ChessSquare { 39 | width: 100%; 40 | height: 1fr; 41 | min-width: 8; 42 | min-height: 8; 43 | content-align: center middle; 44 | } 45 | 46 | 47 | 48 | ChessSquare.highlight { 49 | background: #aaa23a; 50 | } 51 | 52 | ChessSquare.selected { 53 | background: #aaa23a; 54 | } 55 | 56 | ChessSquare.possible-move { 57 | background: #85139c; 58 | } 59 | 60 | ChessSquare.possible-move.light { 61 | background: #c896db; 62 | } 63 | 64 | ChessSquare.possible-move.dark { 65 | background: #8b4b8b; 66 | } 67 | 68 | #sidebar { 69 | width: 20%; 70 | height: 100%; 71 | layout: vertical; 72 | } 73 | 74 | InfoPanel { 75 | height: 20%; 76 | border: solid green; 77 | padding: 1; 78 | } 79 | 80 | MoveHistory { 81 | height: 70%; 82 | border: solid yellow; 83 | padding: 1; 84 | } 85 | 86 | .hidden { 87 | display: none; 88 | } 89 | 90 | #restart-button { 91 | height: 3; 92 | border: solid white; 93 | } 94 | 95 | #menu-button { 96 | height: 3; 97 | border: solid white; 98 | } 99 | 100 | 101 | #config-container { 102 | width: 100%; 103 | height: auto; 104 | align: center top; 105 | } 106 | 107 | RetroTitle { 108 | content-align: center middle; 109 | padding:2; 110 | width: 100%; 111 | margin-bottom: 1; 112 | } 113 | 114 | #options-grid { 115 | grid-size: 5; 116 | grid-gutter: 1 2; 117 | grid-columns: 1fr 1fr 1fr 1fr 1fr; 118 | margin-bottom: 1; 119 | } 120 | 121 | ConfigBox { 122 | height: auto; 123 | 124 | padding: 1; 125 | } 126 | 127 | .box-title { 128 | text-align: center; 129 | text-style: bold; 130 | margin-bottom: 1; 131 | } 132 | 133 | RadioSet { 134 | width: 100%; 135 | height: auto; 136 | } 137 | 138 | #start-game { 139 | margin-top: 2; 140 | width: auto; 141 | min-width: 16; 142 | } 143 | 144 | #credit-line { 145 | margin-top: 2; 146 | text-align: center; 147 | color: $text-muted; 148 | } 149 | """ 150 | 151 | selected_square = reactive(None) 152 | 153 | def __init__(self): 154 | super().__init__() 155 | self.engine = SimpleEngine.popen_uci("stockfish") 156 | self.move_sound = sa.WaveObject.from_wave_file(f"{ASSETS_PATH}/sound/move.wav") 157 | self.check_sound = sa.WaveObject.from_wave_file(f"{ASSETS_PATH}/sound/check.wav") 158 | self.player_color = "white" 159 | self.sound_enabled = True 160 | 161 | def compose(self) -> ComposeResult: 162 | yield ConfigScreen() 163 | yield ChessBoard(classes="hidden") 164 | with Container(id="sidebar",classes="hidden"): 165 | yield InfoPanel() 166 | yield MoveHistory() 167 | with Horizontal(): 168 | yield Button("Restart", id="restart-button", variant="primary") 169 | yield Button("Menu", id="menu-button", variant="primary") 170 | 171 | def on_mount(self): 172 | self.chess_board = self.query_one(ChessBoard) 173 | self.info_panel = self.query_one(InfoPanel) 174 | self.move_history = self.query_one(MoveHistory) 175 | self.restart_button = self.query_one("#restart-button") 176 | self.menu_button = self.query_one("#menu-button") 177 | self.config_screen = self.query_one(ConfigScreen) 178 | self.current_screen = "config" 179 | self.update_info() 180 | self.toggle_screen() 181 | 182 | def toggle_screen(self): 183 | 184 | if self.current_screen == "config": 185 | self.config_screen.remove_class("hidden") 186 | self.chess_board.add_class("hidden") 187 | self.query_one("#sidebar").add_class("hidden") 188 | else: 189 | self.config_screen.add_class("hidden") 190 | self.chess_board.remove_class("hidden") 191 | self.query_one("#sidebar").remove_class("hidden") 192 | 193 | def apply_config(self): 194 | 195 | ChessSquare.piece_set = CONF["piece-type"] 196 | self.player_color = CONF["player_color"] 197 | self.sound_enabled = (CONF["sound"] == "on") 198 | 199 | if self.player_color == "random": 200 | self.player_color = choice(["white","black"]) 201 | 202 | self.chess_board.is_flipped = (self.player_color == "black") 203 | 204 | difficulty_mapping = { 205 | "beginner": {"depth": 1, "randomness": 0.9}, 206 | "easy": {"depth": 1, "randomness": 0.1}, 207 | "medium": {"depth": 2, "randomness": 0}, 208 | "hard": {"depth": 3, "randomness": 0}, 209 | "super hard": {"depth": 5, "randomness": 0} 210 | } 211 | self.engine_settings = difficulty_mapping.get(CONF["difficulty"], {"depth": 1, "randomness": 0.3}) 212 | self.notify(f"Difficulty set to: {CONF['difficulty']}") 213 | self.notify(f"Player color: {self.player_color}") 214 | self.chess_board.render_board() 215 | 216 | 217 | def handle_click(self, square: ChessSquare) -> None: 218 | if self.selected_square is None: 219 | if square.piece and square.piece.color == (chess.BLACK if self.player_color == "black" else chess.WHITE): 220 | self.selected_square = square 221 | square.add_class("selected") 222 | CONF["selected_square"] = self.selected_square 223 | from_square = chess.parse_square(square.id) 224 | if self.chess_board.is_flipped: 225 | from_square = chess.square_mirror(from_square) 226 | self.chess_board.highlight_possible_moves(from_square) 227 | else: 228 | if square != self.selected_square: 229 | self.move_piece(self.selected_square, square) 230 | self.selected_square.remove_class("selected") 231 | self.selected_square = None 232 | self.chess_board.clear_possible_moves() 233 | 234 | def move_piece(self, from_square: ChessSquare, to_square: ChessSquare): 235 | move = chess.Move.from_uci(f"{from_square.id}{to_square.id}") 236 | if self.chess_board.is_flipped: 237 | move = chess.Move(chess.square_mirror(move.from_square), chess.square_mirror(move.to_square)) 238 | 239 | if self.chess_board.board.piece_at(move.from_square).piece_type == chess.PAWN: 240 | 241 | if move.to_square in chess.SquareSet(chess.BB_BACKRANKS) and move.from_square in chess.SquareSet(chess.BB_RANK_7): 242 | self.notify("Time for pawn promotion") 243 | self.handle_pawn_promotion(move) 244 | 245 | if move in self.chess_board.board.legal_moves: 246 | self.chess_board.board.push(move) 247 | self.chess_board.last_move = move 248 | self.move_history.add_move(f"Player: {move}") 249 | self.chess_board.render_board() 250 | self.play_move_sound() 251 | self.update_info() 252 | self.check_game_over() 253 | if not self.chess_board.board.is_game_over(): 254 | self.set_timer(1, self.make_computer_move) 255 | 256 | def make_computer_move(self): 257 | board = self.chess_board.board 258 | legal_moves = list(board.legal_moves) 259 | 260 | if self.engine_settings["randomness"] > 0 and random() < self.engine_settings["randomness"] and not board.is_check(): 261 | move = choice(legal_moves) 262 | else: 263 | result = self.engine.play(board, chess.engine.Limit(depth=self.engine_settings["depth"])) 264 | move = result.move 265 | 266 | self.chess_board.board.push(move) 267 | self.chess_board.last_move = move 268 | displayed_move = move 269 | if self.chess_board.is_flipped: 270 | displayed_move = chess.Move(chess.square_mirror(move.from_square), chess.square_mirror(move.to_square)) 271 | self.move_history.add_move(f"Computer ({('white' if self.player_color == 'black' else 'black')}): {displayed_move}") 272 | self.chess_board.render_board() 273 | 274 | self.update_info() 275 | self.check_game_over() 276 | 277 | def update_info(self): 278 | if self.chess_board.board.is_game_over(): 279 | result = self.chess_board.board.result() 280 | if result == "1-0": 281 | status = "Game Over - White wins!" 282 | elif result == "0-1": 283 | status = "Game Over - Black wins!" 284 | else: 285 | status = "Game Over - Draw!" 286 | turn = "Game Ended" 287 | else: 288 | current_turn = "White" if self.chess_board.board.turn == chess.WHITE else "Black" 289 | turn = f"{current_turn} ({'Player' if current_turn.lower() == self.player_color else 'Computer'})" 290 | status = "Check!" if self.chess_board.board.is_check() else "Normal" 291 | if status == "Check!": 292 | self.play_check_sound() 293 | else: 294 | self.play_move_sound() 295 | self.info_panel.update_info(turn, status, CONF["difficulty"]) 296 | 297 | def play_move_sound(self): 298 | if self.sound_enabled: 299 | self.move_sound.play() 300 | 301 | def play_check_sound(self): 302 | if self.sound_enabled: 303 | self.check_sound.play() 304 | 305 | def check_game_over(self): 306 | if self.chess_board.board.is_game_over(): 307 | self.update_info() 308 | 309 | 310 | def restart_game(self): 311 | self.chess_board.board.reset() 312 | self.chess_board.last_move = None 313 | self.chess_board.clear_possible_moves() 314 | self.chess_board.render_board() 315 | self.move_history.moves = [] 316 | self.move_history.update("") 317 | self.update_info() 318 | 319 | 320 | 321 | 322 | 323 | def start_game(self): 324 | self.current_screen = "game" 325 | self.toggle_screen() 326 | self.chess_board.render_board() 327 | self.update_info() 328 | 329 | if self.player_color == "black": 330 | self.set_timer(1,self.make_computer_move) 331 | 332 | 333 | def on_button_pressed(self, event: Button.Pressed) -> None: 334 | if event.button.id == "restart-button": 335 | self.restart_game() 336 | elif event.button.id == "menu-button": 337 | self.current_screen = "config" 338 | self.toggle_screen() 339 | 340 | 341 | def on_key(self, event: events.Key): 342 | if event.key == "q": 343 | self.exit() 344 | 345 | def handle_pawn_promotion(self, move): 346 | def on_promotion_selected(piece): 347 | self.handle_promotion(move, piece) 348 | self.pop_screen() 349 | 350 | promotion_screen = PawnPromotion(on_promotion_selected) 351 | self.push_screen(promotion_screen) 352 | 353 | def handle_promotion(self, move, promoted_piece): 354 | if promoted_piece: 355 | move.promotion = promoted_piece 356 | self.chess_board.board.push(move) 357 | self.chess_board.board.remove_piece_at(move.to_square) 358 | self.chess_board.board.set_piece_at(move.to_square, chess.Piece(promoted_piece, chess.WHITE), True) 359 | self.chess_board.render_board() 360 | self.move_history.add_move(f"Player: {move}") 361 | self.play_move_sound() 362 | self.update_info() 363 | self.check_game_over() 364 | if not self.chess_board.board.is_game_over(): 365 | self.set_timer(1, self.make_computer_move) 366 | 367 | 368 | 369 | 370 | if __name__ == "__main__": 371 | app = ChessApp() 372 | app.run() -------------------------------------------------------------------------------- /termichess/assets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whiletruelearn/termichess/8fe834aec88e32d0e56dd9743df59ae33e6113b2/termichess/assets/__init__.py -------------------------------------------------------------------------------- /termichess/assets/pieces/asciiart/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whiletruelearn/termichess/8fe834aec88e32d0e56dd9743df59ae33e6113b2/termichess/assets/pieces/asciiart/__init__.py -------------------------------------------------------------------------------- /termichess/assets/pieces/asciiart/basic.py: -------------------------------------------------------------------------------- 1 | PIECE_ASCII_ART = { 2 | 'P': """ 3 | (^) 4 | |=| 5 | /___\\ 6 | """, 7 | 'N': """ 8 | ,^, 9 | <[~] 10 | /___\\ 11 | """, 12 | 'B': """ 13 | /+\\ 14 | (/ \\) 15 | /___\\ 16 | """, 17 | 'R': """ 18 | |==| 19 | |[]| 20 | /___\\ 21 | """, 22 | 'Q': """ 23 | \\**/ 24 | |><| 25 | /___\\ 26 | """, 27 | 'K': """ 28 | ♔ 29 | /|\\ 30 | /___\\ 31 | """, 32 | 'p': """ 33 | (.) 34 | |=| 35 | \\___/ 36 | """, 37 | 'n': """ 38 | ,., 39 | <[~] 40 | \\___/ 41 | """, 42 | 'b': """ 43 | /+\\ 44 | (/ \\) 45 | \\___/ 46 | """, 47 | 'r': """ 48 | |==| 49 | |[]| 50 | \\___/ 51 | """, 52 | 'q': """ 53 | \\**/ 54 | |><| 55 | \\___/ 56 | """, 57 | 'k': """ 58 | ♚ 59 | /|\\ 60 | \\___/ 61 | """ 62 | } -------------------------------------------------------------------------------- /termichess/assets/pieces/asciiart/char.py: -------------------------------------------------------------------------------- 1 | PIECE_ASCII_ART = { 2 | 'P': """ 3 | (^) 4 | |H| 5 | /WWW\\ 6 | """, 7 | 'N': """ 8 | ,e, 9 | <[H] 10 | /WWW\\ 11 | """, 12 | 'B': """ 13 | /+\\ 14 | (HHH) 15 | /WWW\\ 16 | """, 17 | 'R': """ 18 | |==| 19 | |HH| 20 | /WWW\\ 21 | """, 22 | 'Q': """ 23 | \\@@/ 24 | |><| 25 | /WWW\\ 26 | """, 27 | 'K': """ 28 | ♔ 29 | /W\\ 30 | /WWW\\ 31 | """, 32 | 'p': """ 33 | (.) 34 | |h| 35 | \\bbb/ 36 | """, 37 | 'n': """ 38 | ,e, 39 | <[h] 40 | \\bbb/ 41 | """, 42 | 'b': """ 43 | /+\\ 44 | (hhh) 45 | \\bbb/ 46 | """, 47 | 'r': """ 48 | |==| 49 | |hh| 50 | \\bbb/ 51 | """, 52 | 'q': """ 53 | \\@@/ 54 | |><| 55 | \\bbb/ 56 | """, 57 | 'k': """ 58 | ♚ 59 | /b\\ 60 | \\bbb/ 61 | """ 62 | } -------------------------------------------------------------------------------- /termichess/assets/pieces/asciiart/computer_first.py: -------------------------------------------------------------------------------- 1 | PIECE_ASCII_ART = { 2 | 'P': """ 3 | 01 4 | /||\\ 5 | ▀▀▀▀ 6 | """, # Binary (pawn) 7 | 8 | 'N': """ 9 | λ 10 | /F\\ 11 | ▀▀▀▀ 12 | """, # Lambda/Function (knight) 13 | 14 | 'B': """ 15 | {} 16 | /A\\ 17 | ▀▀▀▀ 18 | """, # Algorithm (bishop) 19 | 20 | 'R': """ 21 | [DB] 22 | |S| 23 | ▀▀▀▀ 24 | """, # Database (rook) 25 | 26 | 'Q': """ 27 | ∞ 28 | /O\\ 29 | ▀▀▀▀ 30 | """, # Big O Notation (queen) 31 | 32 | 'K': """ 33 | # 34 | /C\\ 35 | ▀▀▀▀ 36 | """, # Compiler (king) 37 | 38 | 'p': """ 39 | 01 40 | /||\\ 41 | ▀▀▀▀ 42 | """, # Binary (pawn) 43 | 44 | 'n': """ 45 | λ 46 | /F\\ 47 | ▀▀▀▀ 48 | """, # Lambda/Function (knight) 49 | 50 | 'b': """ 51 | {} 52 | /A\\ 53 | ▀▀▀▀ 54 | """, # Algorithm (bishop) 55 | 56 | 'r': """ 57 | [DB] 58 | |S| 59 | ▀▀▀▀ 60 | """, # Database (rook) 61 | 62 | 'q': """ 63 | ∞ 64 | /O\\ 65 | ▀▀▀▀ 66 | """, # Big O Notation (queen) 67 | 68 | 'k': """ 69 | # 70 | /C\\ 71 | ▀▀▀▀ 72 | """ 73 | } -------------------------------------------------------------------------------- /termichess/assets/pieces/asciiart/computer_second.py: -------------------------------------------------------------------------------- 1 | PIECE_ASCII_ART = { 2 | 'P': """ 3 | 10 4 | /||\\ 5 | ▀▀▀▀ 6 | """, # Binary (pawn) 7 | 8 | 'N': """ 9 | 10 | R 11 | ▀▀▀▀ 12 | """, # Recursion (knight) 13 | 14 | 'B': """ 15 | [] 16 | /D\\ 17 | ▀▀▀▀ 18 | """, # Data Structure (bishop) 19 | 20 | 'R': """ 21 | [AP] 22 | |I| 23 | ▀▀▀▀ 24 | """, # API (rook) 25 | 26 | 'Q': """ 27 | @ 28 | /N\\ 29 | ▀▀▀▀ 30 | """, # Network (queen) 31 | 32 | 'K': """ 33 | $ 34 | /S\\ 35 | ▀▀▀▀ 36 | """, # Shell (king), 37 | 38 | 'p': """ 39 | 10 40 | /||\\ 41 | ▀▀▀▀ 42 | """, # Binary (pawn) 43 | 44 | 'n': """ 45 | 46 | R 47 | ▀▀▀▀ 48 | """, # Recursion (knight) 49 | 50 | 'b': """ 51 | [] 52 | /D\\ 53 | ▀▀▀▀ 54 | """, # Data Structure (bishop) 55 | 56 | 'r': """ 57 | [AP] 58 | |I| 59 | ▀▀▀▀ 60 | """, # API (rook) 61 | 62 | 'q': """ 63 | @ 64 | /N\\ 65 | ▀▀▀▀ 66 | """, # Network (queen) 67 | 68 | 'k': """ 69 | $ 70 | /S\\ 71 | ▀▀▀▀ 72 | """ # Shell (king) 73 | } -------------------------------------------------------------------------------- /termichess/assets/pieces/asciiart/computer_third.py: -------------------------------------------------------------------------------- 1 | PIECE_ASCII_ART = { 2 | 'P': """ 3 | 0 4 | /1\\ 5 | ▀▀▀ 6 | """, # Binary (pawn) 7 | 8 | 'N': """ 9 | λ 10 | /→\\ 11 | ▀▀▀ 12 | """, # Lambda function (knight) 13 | 14 | 'B': """ 15 | ∑ 16 | 17 | ▀▀▀ 18 | """, # Algorithm/Sum (bishop) 19 | 20 | 'R': """ 21 | [█] 22 | |S| 23 | ▀▀▀ 24 | """, # Stack (rook) 25 | 26 | 'Q': """ 27 | ∞ 28 | 29 | ▀▀▀ 30 | """, # Big O Notation (queen) 31 | 32 | 'K': """ 33 | #! 34 | |C| 35 | ▀▀▀ 36 | """, # Compiler/Shebang (king) 37 | 38 | 'p': """ 39 | 1 40 | /0\\ 41 | ▀▀▀ 42 | """, # Binary (pawn) 43 | 44 | 'n': """ 45 | <> 46 | || 47 | ▀▀▀ 48 | """, # Recursion (knight) 49 | 50 | 'b': """ 51 | {T} 52 | /\\ 53 | ▀▀▀ 54 | """, # Tree data structure (bishop) 55 | 56 | 'r': """ 57 | [Q] 58 | || 59 | ▀▀▀ 60 | """, # Queue (rook) 61 | 62 | 'q': """ 63 | @IP 64 | \\/ 65 | ▀▀▀ 66 | """, # Network/IP (queen) 67 | 68 | 'k': """ 69 | $ 70 | /\\ 71 | ▀▀▀ 72 | """ # Shell (king) 73 | } -------------------------------------------------------------------------------- /termichess/assets/pieces/asciiart/geometric.py: -------------------------------------------------------------------------------- 1 | PIECE_ASCII_ART = { 2 | # Variant 1: Geometric 3 | 'P': """ 4 | /\\ 5 | /##\\ 6 | ████ 7 | """, 8 | 'N': """ 9 | /| 10 | █▀▄ 11 | ████ 12 | """, 13 | 'B': """ 14 | /+\\ 15 | █▄█ 16 | ████ 17 | """, 18 | 'R': """ 19 | ███ 20 | █ █ 21 | ████ 22 | """, 23 | 'Q': """ 24 | \\▀/ 25 | █▄█ 26 | ████ 27 | """, 28 | 'K': """ 29 | ♔ 30 | █▄█ 31 | ████ 32 | """, 33 | 34 | 'p': """ 35 | /\\ 36 | /##\\ 37 | ████ 38 | """, 39 | 'n': """ 40 | /| 41 | █▀▄ 42 | ████ 43 | """, 44 | 'b': """ 45 | /+\\ 46 | █▄█ 47 | ████ 48 | """, 49 | 'r': """ 50 | ███ 51 | █ █ 52 | ████ 53 | """, 54 | 'q': """ 55 | \\▀/ 56 | █▄█ 57 | ████ 58 | """, 59 | 'k': """ 60 | ♔ 61 | █▄█ 62 | ████ 63 | """ 64 | } -------------------------------------------------------------------------------- /termichess/assets/pieces/asciiart/glyph.py: -------------------------------------------------------------------------------- 1 | PIECE_ASCII_ART = { 2 | 3 | 'P': """ 4 | ▄▄ 5 | ▐▀▀▌ 6 | ▀▄▄▀ 7 | """, 8 | 'N': """ 9 | ▐▀ 10 | ▐▄▀▘ 11 | ▀▀▀▀ 12 | """, 13 | 'B': """ 14 | ▄ 15 | ▐▀▀▌ 16 | ▀▄▄▀ 17 | """, 18 | 'R': """ 19 | ▐▄▄▌ 20 | ▐ ▌ 21 | ▀▀▀▀ 22 | """, 23 | 'Q': """ 24 | ▐▀▀▌ 25 | ▐▄▄▌ 26 | ▀▀▀▀ 27 | """, 28 | 'K': """ 29 | ▐♔▌ 30 | ▐██▌ 31 | ▀▀▀▀ 32 | """, 33 | 'p': """ 34 | ▄▄ 35 | ▐▀▀▌ 36 | ▀▄▄▀ 37 | """, 38 | 'n': """ 39 | ▐▀ 40 | ▐▄▀▘ 41 | ▀▀▀▀ 42 | """, 43 | 'b': """ 44 | ▄ 45 | ▐▀▀▌ 46 | ▀▄▄▀ 47 | """, 48 | 'r': """ 49 | ▐▄▄▌ 50 | ▐ ▌ 51 | ▀▀▀▀ 52 | """, 53 | 'q': """ 54 | ▐▀▀▌ 55 | ▐▄▄▌ 56 | ▀▀▀▀ 57 | """, 58 | 'k': """ 59 | ▐♔▌ 60 | ▐██▌ 61 | ▀▀▀▀ 62 | """ 63 | 64 | 65 | } -------------------------------------------------------------------------------- /termichess/assets/pieces/asciiart/got.py: -------------------------------------------------------------------------------- 1 | PIECE_ASCII_ART = { 2 | 'P': """ 3 | /\\ 4 | /||\\ 5 | ▀▀▀▀ 6 | """, # Stark soldier's helmet 7 | 8 | 'N': """ 9 | <§§> 10 | ][ 11 | ▀▀▀▀ 12 | """, # Direwolf 13 | 14 | 'B': """ 15 | {@@} 16 | || 17 | ▀▀▀▀ 18 | """, # Weirwood tree 19 | 20 | 'R': """ 21 | [WW] 22 | |==| 23 | ▀▀▀▀ 24 | """, # Winterfell tower 25 | 26 | 'Q': """ 27 | \\†/ 28 | |D| 29 | ▀▀▀▀ 30 | """, # Daenerys with dragon 31 | 32 | 'K': """ 33 | Ω 34 | /S\\ 35 | ▀▀▀▀ 36 | """, # Stark sigil (direwolf head) 37 | 38 | 'p': """ 39 | /\\ 40 | /||\\ 41 | ▄▄▄▄ 42 | """, # Lannister soldier's helmet 43 | 44 | 'n': """ 45 | <ΞΞ> 46 | ][ 47 | ▄▄▄▄ 48 | """, # Lion 49 | 50 | 'b': """ 51 | {$$} 52 | || 53 | ▄▄▄▄ 54 | """, # Lannister gold 55 | 56 | 'r': """ 57 | [KL] 58 | |==| 59 | ▄▄▄▄ 60 | """, # King's Landing tower 61 | 62 | 'q': """ 63 | \\†/ 64 | |C| 65 | ▄▄▄▄ 66 | """, # Cersei with crown 67 | 68 | 'k': """ 69 | Ψ 70 | /L\\ 71 | ▄▄▄▄ 72 | """ # Lannister sigil (lion) 73 | } -------------------------------------------------------------------------------- /termichess/assets/pieces/asciiart/mahabharat.py: -------------------------------------------------------------------------------- 1 | PIECE_ASCII_ART = { 2 | 'P': """ 3 | /\\ 4 | /||\\ 5 | ▀▀▀▀ 6 | """, # Pandava soldier 7 | 8 | 'N': """ 9 | 10 | ][ 11 | ▀▀▀▀ 12 | """, # Horse (Ashwatthama) 13 | 14 | 'B': """ 15 | {##} 16 | || 17 | ▀▀▀▀ 18 | """, # Chakra (discus) 19 | 20 | 'R': """ 21 | [II] 22 | |==| 23 | ▀▀▀▀ 24 | """, # Indraprastha palace 25 | 26 | 'Q': """ 27 | \\†/ 28 | |D| 29 | ▀▀▀▀ 30 | """, # Draupadi 31 | 32 | 'K': """ 33 | Ω 34 | /Y\\ 35 | ▀▀▀▀ 36 | """, # Yudhishthira (eldest Pandava) 37 | 38 | 'p': """ 39 | /\\ 40 | /||\\ 41 | ▀▀▀▀ 42 | """, # Kaurava soldier 43 | 44 | 'n': """ 45 | 46 | ][ 47 | ▀▀▀▀ 48 | """, # Horse (Ashwatthama) 49 | 50 | 'b': """ 51 | {##} 52 | || 53 | ▀▀▀▀ 54 | """, # Chakra (discus) 55 | 56 | 'r': """ 57 | [HH] 58 | |==| 59 | ▀▀▀▀ 60 | """, # Hastinapur palace 61 | 62 | 'q': """ 63 | \\†/ 64 | |G| 65 | ▀▀▀▀ 66 | """, # Gandhari 67 | 68 | 'k': """ 69 | Ω 70 | /D\\ 71 | ▀▀▀▀ 72 | """ # Duryodhana (eldest Kaurava) 73 | } -------------------------------------------------------------------------------- /termichess/assets/pieces/asciiart/minimalistic.py: -------------------------------------------------------------------------------- 1 | PIECE_ASCII_ART = { 2 | 'P': """ 3 | ▄▄ 4 | ▐██▌ 5 | ▀▀▀▀ 6 | """, 7 | 'N': """ 8 | ▐▀▄ 9 | ▐█▀ 10 | ▀▀▀ 11 | """, 12 | 'B': """ 13 | ▄▄ 14 | ▐▀▀▌ 15 | ▀▀▀▀ 16 | """, 17 | 'R': """ 18 | ▐▀▀▌ 19 | ▐▄▄▌ 20 | ▀▀▀▀ 21 | """, 22 | 'Q': """ 23 | ▐▀▀▌ 24 | ▐██▌ 25 | ▀▀▀▀ 26 | """, 27 | 'K': """ 28 | ▐♔▌ 29 | ▐█▌ 30 | ▀▀▀ 31 | """, 32 | 'p': """ 33 | ▄▄ 34 | ▐██▌ 35 | ▀▀▀▀ 36 | """, 37 | 'n': """ 38 | ▐▀▄ 39 | ▐█▀ 40 | ▀▀▀ 41 | """, 42 | 'b': """ 43 | ▄▄ 44 | ▐▀▀▌ 45 | ▀▀▀▀ 46 | """, 47 | 'r': """ 48 | ▐▀▀▌ 49 | ▐▄▄▌ 50 | ▀▀▀▀ 51 | """, 52 | 'q': """ 53 | ▐▀▀▌ 54 | ▐██▌ 55 | ▀▀▀▀ 56 | """, 57 | 'k': """ 58 | ▐♚▌ 59 | ▐█▌ 60 | ▀▀▀ 61 | """ 62 | } -------------------------------------------------------------------------------- /termichess/assets/pieces/asciiart/potter.py: -------------------------------------------------------------------------------- 1 | PIECE_ASCII_ART = { 2 | 'P': """ 3 | /\\ 4 | /WW\\ 5 | ▀▀▀▀ 6 | """, # Wizard's hat 7 | 8 | 'N': """ 9 | >==> 10 | ][ 11 | ▀▀▀▀ 12 | """, # Broomstick 13 | 14 | 'B': """ 15 | {B} 16 | |=| 17 | ▀▀▀▀ 18 | """, # Spellbook 19 | 20 | 'R': """ 21 | [T] 22 | |H| 23 | ▀▀▀▀ 24 | """, # Hogwarts tower 25 | 26 | 'Q': """ 27 | \\*/ 28 | |W| 29 | ▀▀▀▀ 30 | """, # Wand casting spell 31 | 32 | 'K': """ 33 | ϟ 34 | /H\\ 35 | ▀▀▀▀ 36 | """, # Harry with lightning scar 37 | 38 | 'p': """ 39 | /\\ 40 | /ww\\ 41 | ▄▄▄▄ 42 | """, # Wizard's hat (black) 43 | 44 | 'n': """ 45 | >==> 46 | ][ 47 | ▄▄▄▄ 48 | """, # Broomstick (black) 49 | 50 | 'b': """ 51 | {b} 52 | |=| 53 | ▄▄▄▄ 54 | """, # Spellbook (black) 55 | 56 | 'r': """ 57 | [t] 58 | |h| 59 | ▄▄▄▄ 60 | """, # Hogwarts tower (black) 61 | 62 | 'q': """ 63 | \\*/ 64 | |w| 65 | ▄▄▄▄ 66 | """, # Wand casting spell (black) 67 | 68 | 'k': """ 69 | ϟ 70 | /v\\ 71 | ▄▄▄▄ 72 | """ # Voldemort with lightning scar 73 | } -------------------------------------------------------------------------------- /termichess/assets/pieces/asciiart/rad.py: -------------------------------------------------------------------------------- 1 | PIECE_ASCII_ART = { 2 | 'P': """ 3 | ██ 4 | ████ 5 | ▄▄▄▄ 6 | """, 7 | 'N': """ 8 | ██ 9 | ███▄ 10 | ▄▄▄▄ 11 | """, 12 | 'B': """ 13 | ▲ 14 | ████ 15 | ▄▄▄▄ 16 | """, 17 | 'R': """ 18 | █ █ 19 | ███ 20 | ▄▄▄▄ 21 | """, 22 | 'Q': """ 23 | █▀█ 24 | ███ 25 | ▄▄▄▄ 26 | """, 27 | 'K': """ 28 | █♔█ 29 | ███ 30 | ▄▄▄▄ 31 | """, 32 | 33 | 'p': """ 34 | ██ 35 | ████ 36 | ▄▄▄▄ 37 | """, 38 | 'n': """ 39 | ██ 40 | ███▄ 41 | ▄▄▄▄ 42 | """, 43 | 'b': """ 44 | ▲ 45 | ████ 46 | ▄▄▄▄ 47 | """, 48 | 'r': """ 49 | █ █ 50 | ███ 51 | ▄▄▄▄ 52 | """, 53 | 'q': """ 54 | █▀█ 55 | ███ 56 | ▄▄▄▄ 57 | """, 58 | 'k': """ 59 | █♔█ 60 | ███ 61 | ▄▄▄▄ 62 | """ 63 | 64 | } -------------------------------------------------------------------------------- /termichess/assets/pieces/asciiart/retro.py: -------------------------------------------------------------------------------- 1 | PIECE_ASCII_ART = { 2 | 'P': """ 3 | ▄▄▄ 4 | ▟███▙ 5 | ▀▀▀▀▀ 6 | """, 7 | 'N': """ 8 | ▟▀▙▄ 9 | ▟██▀ 10 | ▀▀▀▀▀ 11 | """, 12 | 'B': """ 13 | ▄█▄ 14 | ▟▀▀▀▙ 15 | ▀▀▀▀▀ 16 | """, 17 | 'R': """ 18 | ▟▀▀▀▀▙ 19 | ▐▄▄▄▄▌ 20 | ▀▀▀▀▀▀ 21 | """, 22 | 'Q': """ 23 | ▄▀▀▀▄ 24 | ▟▀███▀▙ 25 | ▀▀▀▀▀▀▀ 26 | """, 27 | 'K': """ 28 | ▄▀♔▀▄ 29 | ▟▀███▀▙ 30 | ▀▀▀▀▀▀▀ 31 | """, 32 | 'p': """ 33 | ▄▄▄ 34 | ▟███▙ 35 | ▀▀▀▀▀ 36 | """, 37 | 'n': """ 38 | ▟▀▙▄ 39 | ▟██▀ 40 | ▀▀▀▀▀ 41 | """, 42 | 'b': """ 43 | ▄█▄ 44 | ▟▀▀▀▙ 45 | ▀▀▀▀▀ 46 | """, 47 | 'r': """ 48 | ▟▀▀▀▀▙ 49 | ▐▄▄▄▄▌ 50 | ▀▀▀▀▀▀ 51 | """, 52 | 'q': """ 53 | ▄▀▀▀▄ 54 | ▟▀███▀▙ 55 | ▀▀▀▀▀▀▀ 56 | """, 57 | 'k': """ 58 | ▄▀♚▀▄ 59 | ▟▀███▀▙ 60 | ▀▀▀▀▀▀▀ 61 | """ 62 | } -------------------------------------------------------------------------------- /termichess/assets/pieces/asciiart/scientist.py: -------------------------------------------------------------------------------- 1 | PIECE_ASCII_ART = { 2 | 'P': """ 3 | [] 4 | /||\\ 5 | ▀▀▀▀ 6 | """, # Researcher (pawn) 7 | 8 | 'N': """ 9 | ∞ 10 | /Σ\\ 11 | ▀▀▀▀ 12 | """, # Euler (knight) 13 | 14 | 'B': """ 15 | ☢ 16 | /E\\ 17 | ▀▀▀▀ 18 | """, # Einstein (bishop) 19 | 20 | 'R': """ 21 | [DN] 22 | |A| 23 | ▀▀▀▀ 24 | """, # Darwin (rook) 25 | 26 | 'Q': """ 27 | ☿ 28 | /M\\ 29 | ▀▀▀▀ 30 | """, # Marie Curie (queen) 31 | 32 | 'K': """ 33 | Ω 34 | /N\\ 35 | ▀▀▀▀ 36 | """, # Newton (king) 37 | 38 | 'p': """ 39 | [] 40 | /||\\ 41 | ▀▀▀▀ 42 | """, # Researcher (pawn) 43 | 44 | 'n': """ 45 | ∞ 46 | /Σ\\ 47 | ▀▀▀▀ 48 | """, # Euler (knight) 49 | 50 | 'b': """ 51 | ☢ 52 | /E\\ 53 | ▀▀▀▀ 54 | """, # Einstein (bishop) 55 | 56 | 'r': """ 57 | [DN] 58 | |A| 59 | ▀▀▀▀ 60 | """, # Darwin (rook) 61 | 62 | 'q': """ 63 | ☿ 64 | /M\\ 65 | ▀▀▀▀ 66 | """, # Marie Curie (queen) 67 | 68 | 'k': """ 69 | Ω 70 | /N\\ 71 | ▀▀▀▀ 72 | """ # Newton (king) 73 | 74 | } -------------------------------------------------------------------------------- /termichess/assets/pieces/asciiart/shaded.py: -------------------------------------------------------------------------------- 1 | PIECE_ASCII_ART = { 2 | 3 | 'P': """ 4 | ▓▓ 5 | ▓▓▓▓ 6 | ░░░░ 7 | """, 8 | 'N': """ 9 | ▓▓ 10 | ▓▓▓░ 11 | ░░░░ 12 | """, 13 | 'B': """ 14 | ▓ 15 | ▓▓▓▓ 16 | ░░░░ 17 | """, 18 | 'R': """ 19 | ▓▓▓▓ 20 | ▓ ▓ 21 | ░░░░ 22 | """, 23 | 'Q': """ 24 | ▓ ▓ 25 | ▓▓▓▓ 26 | ░░░░ 27 | """, 28 | 'K': """ 29 | ▓♔▓ 30 | ▓▓▓▓ 31 | ░░░░ 32 | """, 33 | 34 | 35 | 'p': """ 36 | ▓▓ 37 | ▓▓▓▓ 38 | ░░░░ 39 | """, 40 | 'n': """ 41 | ▓▓ 42 | ▓▓▓░ 43 | ░░░░ 44 | """, 45 | 'b': """ 46 | ▓ 47 | ▓▓▓▓ 48 | ░░░░ 49 | """, 50 | 'r': """ 51 | ▓▓▓▓ 52 | ▓ ▓ 53 | ░░░░ 54 | """, 55 | 'q': """ 56 | ▓ ▓ 57 | ▓▓▓▓ 58 | ░░░░ 59 | """, 60 | 'k': """ 61 | ▓♔▓ 62 | ▓▓▓▓ 63 | ░░░░ 64 | """, 65 | 66 | } -------------------------------------------------------------------------------- /termichess/assets/pieces/v1/black_bishop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whiletruelearn/termichess/8fe834aec88e32d0e56dd9743df59ae33e6113b2/termichess/assets/pieces/v1/black_bishop.png -------------------------------------------------------------------------------- /termichess/assets/pieces/v1/black_king.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whiletruelearn/termichess/8fe834aec88e32d0e56dd9743df59ae33e6113b2/termichess/assets/pieces/v1/black_king.png -------------------------------------------------------------------------------- /termichess/assets/pieces/v1/black_knight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whiletruelearn/termichess/8fe834aec88e32d0e56dd9743df59ae33e6113b2/termichess/assets/pieces/v1/black_knight.png -------------------------------------------------------------------------------- /termichess/assets/pieces/v1/black_pawn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whiletruelearn/termichess/8fe834aec88e32d0e56dd9743df59ae33e6113b2/termichess/assets/pieces/v1/black_pawn.png -------------------------------------------------------------------------------- /termichess/assets/pieces/v1/black_queen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whiletruelearn/termichess/8fe834aec88e32d0e56dd9743df59ae33e6113b2/termichess/assets/pieces/v1/black_queen.png -------------------------------------------------------------------------------- /termichess/assets/pieces/v1/black_rook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whiletruelearn/termichess/8fe834aec88e32d0e56dd9743df59ae33e6113b2/termichess/assets/pieces/v1/black_rook.png -------------------------------------------------------------------------------- /termichess/assets/pieces/v1/white_bishop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whiletruelearn/termichess/8fe834aec88e32d0e56dd9743df59ae33e6113b2/termichess/assets/pieces/v1/white_bishop.png -------------------------------------------------------------------------------- /termichess/assets/pieces/v1/white_king.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whiletruelearn/termichess/8fe834aec88e32d0e56dd9743df59ae33e6113b2/termichess/assets/pieces/v1/white_king.png -------------------------------------------------------------------------------- /termichess/assets/pieces/v1/white_knight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whiletruelearn/termichess/8fe834aec88e32d0e56dd9743df59ae33e6113b2/termichess/assets/pieces/v1/white_knight.png -------------------------------------------------------------------------------- /termichess/assets/pieces/v1/white_pawn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whiletruelearn/termichess/8fe834aec88e32d0e56dd9743df59ae33e6113b2/termichess/assets/pieces/v1/white_pawn.png -------------------------------------------------------------------------------- /termichess/assets/pieces/v1/white_queen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whiletruelearn/termichess/8fe834aec88e32d0e56dd9743df59ae33e6113b2/termichess/assets/pieces/v1/white_queen.png -------------------------------------------------------------------------------- /termichess/assets/pieces/v1/white_rook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whiletruelearn/termichess/8fe834aec88e32d0e56dd9743df59ae33e6113b2/termichess/assets/pieces/v1/white_rook.png -------------------------------------------------------------------------------- /termichess/assets/sound/check.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whiletruelearn/termichess/8fe834aec88e32d0e56dd9743df59ae33e6113b2/termichess/assets/sound/check.wav -------------------------------------------------------------------------------- /termichess/assets/sound/move.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whiletruelearn/termichess/8fe834aec88e32d0e56dd9743df59ae33e6113b2/termichess/assets/sound/move.wav -------------------------------------------------------------------------------- /termichess/chess_board.py: -------------------------------------------------------------------------------- 1 | from textual.widgets import Static 2 | from textual.containers import Grid 3 | import chess 4 | from termichess.utils import CONF, get_theme_colors, PIECE_ASCII_ART, ASSETS_PATH 5 | from rich_pixels import Pixels 6 | from PIL import Image 7 | 8 | 9 | class ChessSquare(Static): 10 | piece_set = "retro" 11 | 12 | def __init__(self, id: str): 13 | super().__init__(id=id) 14 | self.piece = None 15 | self.add_class("light" if (ord(id[0]) - ord('a') + int(id[1])) % 2 == 0 else "dark") 16 | 17 | 18 | 19 | def set_piece(self, piece: chess.Piece | None): 20 | 21 | 22 | 23 | self.piece = piece 24 | if piece: 25 | color = "white" if piece.color == chess.WHITE else "black" 26 | piece_symbol = piece.symbol().lower() 27 | piece_name = chess.piece_name(piece.piece_type) 28 | if ChessSquare.piece_set in PIECE_ASCII_ART: 29 | ascii_art = PIECE_ASCII_ART[ChessSquare.piece_set][piece_symbol] 30 | colored_ascii_art = f"[bold {color}]{ascii_art}[/bold {color}]" 31 | self.update(colored_ascii_art) 32 | 33 | if ChessSquare.piece_set == "png-v1": 34 | image_path = f"{ASSETS_PATH}/pieces/v1/{color}_{piece_name}.png" 35 | img = Image.open(image_path) 36 | pixels = Pixels.from_image(img) 37 | self.update(pixels) 38 | else: 39 | self.update("") 40 | 41 | 42 | def on_click(self) -> None: 43 | self.app.handle_click(self) 44 | 45 | class ChessBoard(Static): 46 | def __init__(self, classes:str = None): 47 | super().__init__(classes=classes) 48 | self.board = chess.Board() 49 | self.last_move = None 50 | self.possible_moves = set() 51 | self.is_flipped = False 52 | 53 | def compose(self): 54 | with Grid(id="board-grid"): 55 | for row in range(8): 56 | for col in range(8): 57 | square_id = f"{chr(97 + col)}{8 - row}" 58 | yield ChessSquare(square_id) 59 | 60 | 61 | 62 | 63 | 64 | def render_board(self): 65 | light_bg, dark_bg = get_theme_colors(CONF["board-theme"]) 66 | 67 | for square in chess.SQUARES: 68 | square_name = chess.SQUARE_NAMES[square] 69 | display_square = chess.square_mirror(square) if self.is_flipped else square 70 | piece = self.board.piece_at(display_square) 71 | square_widget = self.query_one(f"#{square_name}", ChessSquare) 72 | 73 | selected_square = CONF.get('selected_square') 74 | 75 | if display_square not in self.possible_moves: 76 | square_widget.add_class("light" if (ord(square_name[0]) - ord('a') + int(square_name[1])) % 2 == 0 else "dark") 77 | square_widget.styles.background = light_bg if square_widget.has_class("light") else dark_bg 78 | 79 | if selected_square and selected_square.id == square_widget.id: 80 | square_widget.add_class("selected") 81 | square_widget.styles.background = "#aaa23a" 82 | 83 | if self.last_move and display_square in [self.last_move.from_square, self.last_move.to_square]: 84 | square_widget.add_class("highlight") 85 | square_widget.styles.background = "#aaa23a" 86 | 87 | if display_square in self.possible_moves: 88 | square_widget.add_class("possible-move-light" if (ord(square_name[0]) - ord('a') + int(square_name[1])) % 2 == 0 else "possible-move-dark") 89 | square_widget.styles.background = "#c896db" if square_widget.has_class("possible-move-light") else "#8b4b8b" 90 | 91 | square_widget.set_piece(piece) 92 | 93 | def get_possible_moves(self, from_square): 94 | return {move.to_square for move in self.board.legal_moves if move.from_square == from_square} 95 | 96 | def highlight_possible_moves(self, from_square): 97 | self.possible_moves = self.get_possible_moves(from_square) 98 | self.render_board() 99 | 100 | def clear_possible_moves(self): 101 | self.possible_moves.clear() 102 | self.render_board() -------------------------------------------------------------------------------- /termichess/config_screen.py: -------------------------------------------------------------------------------- 1 | from textual.containers import Grid, Center, Vertical 2 | from textual.widgets import Static, Button, RadioButton, RadioSet 3 | from termichess.utils import CONF, THEME_COLORS, PIECE_ASCII_ART 4 | 5 | 6 | class RetroTitle(Static): 7 | def render(self): 8 | return """ 9 | ████████╗███████╗██████╗ ███╗ ███╗██╗ ██████╗██╗ ██╗███████╗███████╗███████╗ 10 | ╚══██╔══╝██╔════╝██╔══██╗████╗ ████║██║██╔════╝██║ ██║██╔════╝██╔════╝██╔════╝ 11 | ██║ █████╗ ██████╔╝██╔████╔██║██║██║ ███████║█████╗ ███████╗███████╗ 12 | ██║ ██╔══╝ ██╔══██╗██║╚██╔╝██║██║██║ ██╔══██║██╔══╝ ╚════██║╚════██║ 13 | ██║ ███████╗██║ ██║██║ ╚═╝ ██║██║╚██████╗██║ ██║███████╗███████║███████║ 14 | ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝ 15 | """ 16 | 17 | class ConfigBox(Static): 18 | def __init__(self, title: str, id: str, options: list[str]): 19 | super().__init__() 20 | self.title = title 21 | self.box_id = id 22 | self.options = options 23 | 24 | def compose(self): 25 | yield Static(self.title, classes="box-title") 26 | with RadioSet(id=self.box_id): 27 | for option in self.options: 28 | yield RadioButton(option, value=option.lower()) 29 | 30 | 31 | 32 | 33 | 34 | class ConfigScreen(Static): 35 | 36 | 37 | def compose(self): 38 | with Center(): 39 | with Vertical(id="config-container"): 40 | yield RetroTitle() 41 | 42 | with Grid(id="options-grid", classes="options"): 43 | yield ConfigBox("Piece Type", "piece-type", list(PIECE_ASCII_ART.keys()) + ['png-v1']) 44 | yield ConfigBox("Board Theme", "board-theme", THEME_COLORS.keys()) 45 | yield ConfigBox("Player Color", "player_color", ["white", "black", "random"]) 46 | yield ConfigBox("Difficulty Level", "difficulty", ["beginner","easy", "medium", "hard", "super hard"]) 47 | yield ConfigBox("Sound", "sound", ["on", "off"]) 48 | 49 | with Center(): 50 | yield Button("Start Game", id="start-game", variant="primary") 51 | 52 | yield Static("Built with ❤️ by @whiletruelearn", id="credit-line") 53 | 54 | def on_mount(self): 55 | 56 | self.set_radio_button("piece-type", "retro") 57 | self.set_radio_button("board-theme", "classic") 58 | self.set_radio_button("player_color", "white") 59 | self.set_radio_button("difficulty", "beginner") 60 | self.set_radio_button("sound", "on") 61 | 62 | def set_radio_button(self, radio_set_id: str, value: str): 63 | radio_set = self.query_one(f"#{radio_set_id}") 64 | for child in radio_set.children: 65 | if isinstance(child, RadioButton) and child.value == value: 66 | child.value = True 67 | break 68 | 69 | def on_radio_set_changed(self, event: RadioSet.Changed) -> None: 70 | radio_set = event.radio_set 71 | selected_button = radio_set.pressed_button 72 | if selected_button: 73 | CONF[radio_set.id] = selected_button.label._text[0] 74 | 75 | 76 | def on_button_pressed(self, event: Button.Pressed) -> None: 77 | if event.button.id == "start-game": 78 | self.app.apply_config() 79 | self.app.start_game() 80 | -------------------------------------------------------------------------------- /termichess/piece_generator/create_pieces.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw 2 | 3 | def create_piece(color, shape): 4 | img = Image.new('RGBA', (16, 16), color=(0,0,0,0)) 5 | draw = ImageDraw.Draw(img) 6 | 7 | if color == 'white': 8 | main_color = (240, 240, 240) 9 | outline_color = (180, 180, 180) 10 | else: 11 | main_color = (60, 60, 60) 12 | outline_color = (20, 20, 20) 13 | 14 | draw.rectangle([4, 13, 11, 15], fill=main_color, outline=outline_color) 15 | 16 | if shape == 'pawn': 17 | 18 | draw.ellipse([5, 2, 9, 6], fill=main_color, outline=outline_color) 19 | draw.polygon([(4, 6), (10, 6), (9, 12), (5, 12)], fill=main_color, outline=outline_color) 20 | 21 | elif shape == 'rook': 22 | draw.rectangle([5, 3, 10, 13], fill=main_color, outline=outline_color) 23 | draw.line([(5, 3), (10, 3)], fill=outline_color) 24 | draw.line([(5, 3), (5, 1)], fill=outline_color) 25 | draw.line([(10, 3), (10, 1)], fill=outline_color) 26 | elif shape == 'knight': 27 | 28 | draw.polygon([(7, 1), (4, 13), (11, 13)], fill=main_color, outline=outline_color) 29 | draw.line([(6, 3), (7, 1), (8, 3)], fill=outline_color) 30 | draw.ellipse([6, 4, 10, 8], fill=outline_color if color == 'white' else main_color) 31 | 32 | elif shape == 'bishop': 33 | draw.polygon([(5, 13), (10, 13), (10, 5), (8, 2), (6, 2), (5, 5)], fill=main_color, outline=outline_color) 34 | draw.point((8, 4), fill=main_color) 35 | draw.line([(6, 3), (5, 6)], fill=outline_color) 36 | 37 | elif shape == 'queen': 38 | draw.polygon([(7, 1), (3, 13), (12, 13)], fill=main_color, outline=outline_color) 39 | draw.line([(3, 13), (7, 1), (12, 13)], fill=outline_color) 40 | elif shape == 'king': 41 | draw.polygon([(7, 3), (3, 13), (12, 13)], fill=main_color, outline=outline_color) 42 | draw.line([(7, 1), (7, 5)], fill=outline_color) 43 | draw.line([(5, 3), (9, 3)], fill=outline_color) 44 | 45 | return img 46 | 47 | def save_pieces(): 48 | pieces = ['pawn', 'rook', 'knight', 'bishop', 'queen', 'king'] 49 | colors = ['white', 'black'] 50 | 51 | for color in colors: 52 | for piece in pieces: 53 | img = create_piece(color, piece) 54 | img.save(f"{color}_{piece}.png") 55 | 56 | if __name__ == "__main__": 57 | save_pieces() -------------------------------------------------------------------------------- /termichess/promotion.py: -------------------------------------------------------------------------------- 1 | from textual.screen import ModalScreen 2 | from textual.widgets import Label, Button 3 | import chess 4 | 5 | class PawnPromotion(ModalScreen): 6 | CSS = """ 7 | PawnPromotion { 8 | height: auto; 9 | width: 30%; 10 | background: $surface; 11 | border: tall $panel; 12 | padding: 1; 13 | layout: grid; 14 | grid-rows: auto 1fr; 15 | grid-columns: 1fr 1fr 1fr 1fr; 16 | align: center middle; 17 | } 18 | 19 | #title { 20 | dock: top; 21 | height: 3; 22 | background: $boost; 23 | padding: 0 1; 24 | column-span: 4; 25 | } 26 | 27 | Button { 28 | width: 50%; 29 | height: 3; 30 | } 31 | """ 32 | 33 | def __init__(self, on_promotion_selected): 34 | super().__init__("Pawn Promotion") 35 | self.on_promotion_selected = on_promotion_selected 36 | self.mapping = {"Queen" : chess.QUEEN, 37 | "Rook" : chess.ROOK, 38 | "Bishop" : chess.BISHOP, 39 | "Knight" : chess.KNIGHT} 40 | 41 | def compose(self): 42 | yield Label("Select Promotion Piece", id="title") 43 | yield Button("Queen", id="queen") 44 | yield Button("Rook", id="rook") 45 | yield Button("Bishop", id="bishop") 46 | yield Button("Knight", id="knight") 47 | 48 | 49 | def on_dismiss(self): 50 | self.remove_self() 51 | 52 | 53 | def get_promotion_piece(self): 54 | return self.promotion_piece 55 | 56 | 57 | def on_button_pressed(self, event: Button.Pressed): 58 | piece_id = event.button.id 59 | promotion_piece = self.mapping.get(piece_id.capitalize()) 60 | self.on_promotion_selected(promotion_piece) 61 | self.dismiss() -------------------------------------------------------------------------------- /termichess/sidebar.py: -------------------------------------------------------------------------------- 1 | from textual.widgets import Static 2 | from textual.containers import Container, Horizontal 3 | from textual.widgets import Button 4 | 5 | 6 | class InfoPanel(Static): 7 | def update_info(self, turn: str, status: str, difficulty_level : str): 8 | self.update(f"[bold]Turn:[/bold] {turn}\n[bold]Status:[/bold] {status}\n[bold]Difficulty level:[/bold] {difficulty_level}") 9 | 10 | class MoveHistory(Static): 11 | def on_mount(self): 12 | self.moves = [] 13 | 14 | def add_move(self, move: str): 15 | self.moves.append(move) 16 | self.update("\n".join(self.moves[-25:])) -------------------------------------------------------------------------------- /termichess/utils.py: -------------------------------------------------------------------------------- 1 | from termichess.assets.pieces.asciiart import computer_first, computer_second, computer_third, retro, geometric, minimalistic, char, glyph, got, mahabharat, potter, rad, scientist 2 | import os 3 | 4 | 5 | PIECE_ASCII_ART = { 6 | 7 | "retro": retro.PIECE_ASCII_ART, 8 | "geometric": geometric.PIECE_ASCII_ART, 9 | "minimalistic": minimalistic.PIECE_ASCII_ART, 10 | "char" : char.PIECE_ASCII_ART, 11 | "computer1" : computer_first.PIECE_ASCII_ART, 12 | "computer2" : computer_second.PIECE_ASCII_ART, 13 | "computer3" : computer_third.PIECE_ASCII_ART, 14 | "glyph" : glyph.PIECE_ASCII_ART, 15 | "got" : got.PIECE_ASCII_ART, 16 | "mahabharat" : mahabharat.PIECE_ASCII_ART, 17 | "potter" : potter.PIECE_ASCII_ART, 18 | "rad" : rad.PIECE_ASCII_ART, 19 | "scientist" : scientist.PIECE_ASCII_ART 20 | } 21 | 22 | CONF = {'board-theme' : "classic", 'difficulty' : 'beginner'} 23 | 24 | ASSETS_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'assets') 25 | 26 | THEME_COLORS = { 27 | "classic": ("#ebecd0", "#739552"), 28 | "forest": ("#e8e5c2", "#3e5f2f"), 29 | "ocean": ("#e3f2fd", "#0077b6"), 30 | "midnight": ("#d1d5db", "#4b5563"), 31 | "autumn": ("#fde8e0", "#ca8a04"), 32 | "lavender": ("#f3e8ff", "#7e22ce"), 33 | "moss": ("#e3f9a6", "#3f6212"), 34 | "marble": ("#f5f5f5", "#bfbfbf"), 35 | "crimson": ("#f8f0f7", "#9b1b30"), 36 | "emerald": ("#f0feeb", "#1b5e20"), 37 | "sakura": ("#ffedf6", "#d16ba5"), 38 | "royal": ("#f2f2ff", "#3f3f8f"), 39 | "coffee": ("#f5f5eb", "#6f4e37"), 40 | } 41 | 42 | def get_theme_colors(theme: str) : 43 | try: 44 | return THEME_COLORS[theme] 45 | except KeyError: 46 | raise ValueError(f"Invalid theme: {theme}") 47 | --------------------------------------------------------------------------------