├── src ├── __init__.py ├── HighlightArea.py ├── MoveLog.py ├── Kuchtohai.py ├── chessmain.py ├── test.py ├── ChessAI.py └── chessengine.py ├── .DS_Store ├── images ├── bB.png ├── bK.png ├── bN.png ├── bQ.png ├── bR.png ├── bp.png ├── wB.png ├── wK.png ├── wN.png ├── wQ.png ├── wR.png ├── wp.png ├── .DS_Store ├── depth.png ├── alpha-beta.png ├── move-order.png ├── trans-table.jpg ├── positional-value-2.png └── positional-value.jpg ├── .github └── ISSUE_TEMPLATE │ └── feature_request.md ├── SECURITY.md ├── LICENSE ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md └── README.md /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EuclidStellar/Sepentia-ChessEngine/HEAD/.DS_Store -------------------------------------------------------------------------------- /images/bB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EuclidStellar/Sepentia-ChessEngine/HEAD/images/bB.png -------------------------------------------------------------------------------- /images/bK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EuclidStellar/Sepentia-ChessEngine/HEAD/images/bK.png -------------------------------------------------------------------------------- /images/bN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EuclidStellar/Sepentia-ChessEngine/HEAD/images/bN.png -------------------------------------------------------------------------------- /images/bQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EuclidStellar/Sepentia-ChessEngine/HEAD/images/bQ.png -------------------------------------------------------------------------------- /images/bR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EuclidStellar/Sepentia-ChessEngine/HEAD/images/bR.png -------------------------------------------------------------------------------- /images/bp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EuclidStellar/Sepentia-ChessEngine/HEAD/images/bp.png -------------------------------------------------------------------------------- /images/wB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EuclidStellar/Sepentia-ChessEngine/HEAD/images/wB.png -------------------------------------------------------------------------------- /images/wK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EuclidStellar/Sepentia-ChessEngine/HEAD/images/wK.png -------------------------------------------------------------------------------- /images/wN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EuclidStellar/Sepentia-ChessEngine/HEAD/images/wN.png -------------------------------------------------------------------------------- /images/wQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EuclidStellar/Sepentia-ChessEngine/HEAD/images/wQ.png -------------------------------------------------------------------------------- /images/wR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EuclidStellar/Sepentia-ChessEngine/HEAD/images/wR.png -------------------------------------------------------------------------------- /images/wp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EuclidStellar/Sepentia-ChessEngine/HEAD/images/wp.png -------------------------------------------------------------------------------- /images/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EuclidStellar/Sepentia-ChessEngine/HEAD/images/.DS_Store -------------------------------------------------------------------------------- /images/depth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EuclidStellar/Sepentia-ChessEngine/HEAD/images/depth.png -------------------------------------------------------------------------------- /images/alpha-beta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EuclidStellar/Sepentia-ChessEngine/HEAD/images/alpha-beta.png -------------------------------------------------------------------------------- /images/move-order.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EuclidStellar/Sepentia-ChessEngine/HEAD/images/move-order.png -------------------------------------------------------------------------------- /images/trans-table.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EuclidStellar/Sepentia-ChessEngine/HEAD/images/trans-table.jpg -------------------------------------------------------------------------------- /images/positional-value-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EuclidStellar/Sepentia-ChessEngine/HEAD/images/positional-value-2.png -------------------------------------------------------------------------------- /images/positional-value.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EuclidStellar/Sepentia-ChessEngine/HEAD/images/positional-value.jpg -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Gaurav Singh 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 | -------------------------------------------------------------------------------- /src/HighlightArea.py: -------------------------------------------------------------------------------- 1 | 2 | import pygame as p 3 | 4 | import chessmain as cm 5 | 6 | def highlightSquares(screen, game_state, valid_moves, square_selected): 7 | """ 8 | Highlight square selected and moves for piece selected. 9 | """ 10 | if (len(game_state.move_log)) > 0: 11 | last_move = game_state.move_log[-1] 12 | s = p.Surface((cm.SQUARE_SIZE, cm.SQUARE_SIZE)) 13 | s.set_alpha(100) 14 | s.fill(p.Color('green')) 15 | screen.blit(s, (last_move.end_col * cm.SQUARE_SIZE, last_move.end_row * cm.SQUARE_SIZE)) 16 | if square_selected != (): 17 | row, col = square_selected 18 | if game_state.board[row][col][0] == ( 19 | 'w' if game_state.white_to_move else 'b'): # square_selected is a piece that can be moved 20 | # highlight selected square 21 | s = p.Surface((cm.SQUARE_SIZE, cm.SQUARE_SIZE)) 22 | s.set_alpha(100) # transparency value 0 -> transparent, 255 -> opaque 23 | s.fill(p.Color('blue')) 24 | screen.blit(s, (col * cm.SQUARE_SIZE, row * cm.SQUARE_SIZE)) 25 | # highlight moves from that square 26 | s.fill(p.Color('yellow')) 27 | for move in valid_moves: 28 | if move.start_row == row and move.start_col == col: 29 | screen.blit(s, (move.end_col * cm.SQUARE_SIZE, move.end_row * cm.SQUARE_SIZE)) 30 | 31 | -------------------------------------------------------------------------------- /src/MoveLog.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import pygame as p 4 | import chessmain as cm 5 | 6 | 7 | def drawMoveLog(screen, game_state, font): 8 | """ 9 | Draws the move log. 10 | 11 | """ 12 | move_log_rect = p.Rect(cm.BOARD_WIDTH, 0, cm.MOVE_LOG_PANEL_WIDTH, cm.MOVE_LOG_PANEL_HEIGHT) 13 | p.draw.rect(screen, p.Color('gray'), move_log_rect) 14 | move_log = game_state.move_log 15 | move_texts = [] 16 | for i in range(0, len(move_log), 2): 17 | move_string = str(i // 2 + 1) + '. ' + str(move_log[i]) + " " 18 | if i + 1 < len(move_log): 19 | move_string += str(move_log[i + 1]) + " " 20 | move_texts.append(move_string) 21 | 22 | moves_per_row = 3 23 | padding = 5 24 | line_spacing = 2 25 | text_y = padding 26 | for i in range(0, len(move_texts), moves_per_row): 27 | text = "" 28 | for j in range(moves_per_row): 29 | if i + j < len(move_texts): 30 | text += move_texts[i + j] 31 | 32 | text_object = font.render(text, True, p.Color('black')) 33 | text_location = move_log_rect.move(padding, text_y) 34 | screen.blit(text_object, text_location) 35 | text_y += text_object.get_height() + line_spacing 36 | 37 | 38 | 39 | def drawEndGameText(screen, text): 40 | font = p.font.SysFont("Helvetica", 32, True, False) 41 | text_object = font.render(text, False, p.Color("gray")) 42 | text_location = p.Rect(0, 0, cm.BOARD_WIDTH, cm.BOARD_HEIGHT).move(cm.BOARD_WIDTH / 2 - text_object.get_width() / 2, 43 | cm.BOARD_HEIGHT / 2 - text_object.get_height() / 2) 44 | screen.blit(text_object, text_location) 45 | text_object = font.render(text, False, p.Color('black')) 46 | screen.blit(text_object, text_location.move(2, 2)) -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Sepantia 2 | 3 | Welcome to the Sepantia-Chess Engine project! We're glad you're interested in contributing. Please take a moment to read through this document to understand how you can contribute to the project. 4 | 5 | ## How to Contribute 6 | 7 | There are several ways you can contribute to the Python Chess Engine project: 8 | 9 | ### 1. Reporting Issues 10 | 11 | If you encounter any bugs, errors, or have suggestions for improvements, please [open an issue](https://github.com/euclidstellar/chess-engine/issues) on our GitHub repository. When reporting issues, please include as much detail as possible, including steps to reproduce the issue and your operating system/environment. 12 | 13 | ### 2. Fixing Issues 14 | 15 | You can contribute by fixing existing issues or implementing new features. Here's how: 16 | 17 | - Fork the repository to your GitHub account. 18 | - Create a new branch for your changes: `git checkout -b fix-issue-#` (where # is the issue number). 19 | - Make your changes. 20 | - Test your changes thoroughly. 21 | - Commit your changes: `git commit -am "Fix #"` 22 | - Push your changes to your fork: `git push origin fix-issue-#` 23 | - [Open a pull request](https://github.com/yourusername/chess-engine/pulls) with a clear description of your changes and reference to the related issue. 24 | 25 | ### 3. Improving Documentation 26 | 27 | You can contribute by improving the project's documentation. This includes fixing typos, adding examples, or clarifying existing documentation. 28 | 29 | ### 4. Providing Feedback 30 | 31 | You can provide feedback on the project's direction, features, and implementation by participating in discussions on issues or pull requests. 32 | 33 | ## Code Style 34 | 35 | When contributing code, please follow these guidelines: 36 | 37 | - Follow [PEP 8](https://pep8.org/) for Python code style. 38 | - Use clear and descriptive variable/function names. 39 | - Write docstrings for functions and classes to explain their purpose and usage. 40 | 41 | ## License 42 | 43 | By contributing to the Python Chess Engine project, you agree that your contributions will be licensed under the project's [MIT License](LICENSE). 44 | 45 | ## Contact 46 | 47 | If you have any questions or need further assistance, feel free to contact us via [email](mailto:euclidstellar@example.com) or open an issue on GitHub. 48 | 49 | Thank you for your contributions! 50 | 51 | -------------------------------------------------------------------------------- /src/Kuchtohai.py: -------------------------------------------------------------------------------- 1 | # import pygame as p 2 | # import ChessMain as cm 3 | # import HighlightArea 4 | 5 | # def drawGameState(screen, game_state, valid_moves, square_selected): 6 | # """ 7 | # Responsible for all the graphics within current game state. 8 | # """ 9 | # drawBoard(screen) # draw squares on the board 10 | # HighlightArea.highlightSquares(screen, game_state, valid_moves, square_selected) 11 | # drawPieces(screen, game_state.board) # draw pieces on top of those squares 12 | 13 | 14 | # def drawBoard(screen): 15 | # """ 16 | # Draw the squares on the board. 17 | # The top left square is always light. 18 | # """ 19 | # global colors 20 | # colors = [p.Color("white"), p.Color("dark green")] 21 | # for row in range(cm.DIMENSION): 22 | # for column in range(cm.DIMENSION): 23 | # color = colors[((row + column) % 2)] 24 | # p.draw.rect(screen, color, p.Rect(column * cm.SQUARE_SIZE, row * cm.SQUARE_SIZE, cm.SQUARE_SIZE, cm.SQUARE_SIZE)) 25 | 26 | 27 | # def drawPieces(screen, board): 28 | # """ 29 | # Draw the pieces on the board using the current game_state.board 30 | # """ 31 | # for row in range(cm.DIMENSION): 32 | # for column in range(cm.DIMENSION): 33 | # piece = board[row][column] 34 | # if piece != "--": 35 | # screen.blit(cm.IMAGES[piece], p.Rect(column * cm.SQUARE_SIZE, row * cm.SQUARE_SIZE, cm.SQUARE_SIZE, cm.SQUARE_SIZE)) 36 | 37 | 38 | # def animateMove(move, screen, board, clock): 39 | # """ 40 | # Animating a move 41 | # """ 42 | # global colors 43 | # d_row = move.end_row - move.start_row 44 | # d_col = move.end_col - move.start_col 45 | # frames_per_square = 10 # frames to move one square 46 | # frame_count = (abs(d_row) + abs(d_col)) * frames_per_square 47 | # for frame in range(frame_count + 1): 48 | # row, col = (move.start_row + d_row * frame / frame_count, move.start_col + d_col * frame / frame_count) 49 | # drawBoard(screen) 50 | # drawPieces(screen, board) 51 | # # erase the piece moved from its ending square 52 | # color = colors[(move.end_row + move.end_col) % 2] 53 | # end_square = p.Rect(move.end_col * cm.SQUARE_SIZE, move.end_row * cm.SQUARE_SIZE, cm.SQUARE_SIZE, cm.SQUARE_SIZE) 54 | # p.draw.rect(screen, color, end_square) 55 | # # draw captured piece onto rectangle 56 | # if move.piece_captured != '--': 57 | # if move.is_enpassant_move: 58 | # enpassant_row = move.end_row + 1 if move.piece_captured[0] == 'b' else move.end_row - 1 59 | # end_square = p.Rect(move.end_col * cm.SQUARE_SIZE, enpassant_row * cm.SQUARE_SIZE, cm.SQUARE_SIZE, cm.SQUARE_SIZE) 60 | # screen.blit(cm.IMAGES[move.piece_captured], end_square) 61 | # # draw moving piece 62 | # screen.blit(cm.IMAGES[move.piece_moved], p.Rect(col * cm.SQUARE_SIZE, row * cm.SQUARE_SIZE, cm.SQUARE_SIZE, cm.SQUARE_SIZE)) 63 | # p.display.flip() 64 | # clock.tick(60) -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | Email. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /src/chessmain.py: -------------------------------------------------------------------------------- 1 | 2 | import pygame as p 3 | import chessengine, ChessAI , HighlightArea , MoveLog 4 | import sys 5 | from multiprocessing import Process, Queue 6 | 7 | BOARD_WIDTH = BOARD_HEIGHT = 620 8 | MOVE_LOG_PANEL_WIDTH = 256 9 | MOVE_LOG_PANEL_HEIGHT = BOARD_HEIGHT 10 | DIMENSION = 8 11 | SQUARE_SIZE = BOARD_HEIGHT // DIMENSION 12 | MAX_FPS = 15 13 | IMAGES = {} 14 | 15 | 16 | def loadImages(): 17 | elements = ['wp', 'wR', 'wN', 'wB', 'wK', 'wQ', 'bp', 'bR', 'bN', 'bB', 'bK', 'bQ'] 18 | for piece in elements: 19 | IMAGES[piece] = p.transform.scale(p.image.load("images/" + piece + ".png"), (SQUARE_SIZE, SQUARE_SIZE)) 20 | 21 | 22 | def main(): 23 | """ 24 | The main driver for our code. 25 | This will handle user input and updating the graphics. 26 | """ 27 | p.init() 28 | screen = p.display.set_mode((BOARD_WIDTH + MOVE_LOG_PANEL_WIDTH, BOARD_HEIGHT)) 29 | clock = p.time.Clock() 30 | screen.fill(p.Color("white")) 31 | game_state = chessengine.GameState() 32 | valid_moves = game_state.getValidMoves() 33 | move_made = False # flag variable for when a move is made 34 | animate = False # flag variable for when we should animate a move 35 | loadImages() # do this only once before while loop 36 | running = True 37 | square_selected = () # no square is selected initially, this will keep track of the last click of the user (tuple(row,col)) 38 | player_clicks = [] # this will keep track of player clicks (two tuples) 39 | game_over = False 40 | ai_thinking = False 41 | move_undone = False 42 | move_finder_process = None 43 | move_log_font = p.font.SysFont("Arial", 14, False, False) 44 | player_one = True# if a human is playing white, then this will be True, else False 45 | player_two = False # if a hyman is playing white, then this will be True, else False 46 | 47 | while running: 48 | human_turn = (game_state.white_to_move and player_one) or (not game_state.white_to_move and player_two) 49 | for e in p.event.get(): 50 | if e.type == p.QUIT: 51 | p.quit() 52 | sys.exit() 53 | # mouse handler 54 | elif e.type == p.MOUSEBUTTONDOWN: 55 | if not game_over: 56 | location = p.mouse.get_pos() # (x, y) location of the mouse 57 | col = location[0] // SQUARE_SIZE 58 | row = location[1] // SQUARE_SIZE 59 | if square_selected == (row, col) or col >= 8: # user clicked the same square twice 60 | square_selected = () # deselect 61 | player_clicks = [] # clear clicks 62 | else: 63 | square_selected = (row, col) 64 | player_clicks.append(square_selected) # append for both 1st and 2nd click 65 | if len(player_clicks) == 2 and human_turn: # after 2nd click 66 | move = chessengine.Move(player_clicks[0], player_clicks[1], game_state.board) 67 | for i in range(len(valid_moves)): 68 | if move == valid_moves[i]: 69 | game_state.makeMove(valid_moves[i]) 70 | move_made = True 71 | animate = True 72 | square_selected = () # reset user clicks 73 | player_clicks = [] 74 | if not move_made: 75 | player_clicks = [square_selected] 76 | 77 | # key handler 78 | elif e.type == p.KEYDOWN: 79 | if e.key == p.K_z: # undo when 'z' is pressed 80 | game_state.undoMove() 81 | move_made = True 82 | animate = False 83 | game_over = False 84 | if ai_thinking: 85 | move_finder_process.terminate() 86 | ai_thinking = False 87 | move_undone = True 88 | if e.key == p.K_r: # reset the game when 'r' is pressed 89 | game_state = chessengine.GameState() 90 | valid_moves = game_state.getValidMoves() 91 | square_selected = () 92 | player_clicks = [] 93 | move_made = False 94 | animate = False 95 | game_over = False 96 | if ai_thinking: 97 | move_finder_process.terminate() 98 | ai_thinking = False 99 | move_undone = True 100 | 101 | # AI move finder 102 | if not game_over and not human_turn and not move_undone: 103 | if not ai_thinking: 104 | ai_thinking = True 105 | return_queue = Queue() # used to pass data between threads 106 | move_finder_process = Process(target=ChessAI.findBestMove, args=(game_state, valid_moves, return_queue)) 107 | move_finder_process.start() 108 | 109 | if not move_finder_process.is_alive(): 110 | ai_move = return_queue.get() 111 | if ai_move is None: 112 | ai_move = ChessAI.findRandomMove(valid_moves) 113 | game_state.makeMove(ai_move) 114 | move_made = True 115 | animate = True 116 | ai_thinking = False 117 | 118 | if move_made: 119 | if animate: 120 | animateMove(game_state.move_log[-1], screen, game_state.board, clock) 121 | valid_moves = game_state.getValidMoves() 122 | move_made = False 123 | animate = False 124 | move_undone = False 125 | 126 | positionsInChess(screen, game_state, valid_moves, square_selected) 127 | 128 | if not game_over: 129 | MoveLog.drawMoveLog(screen, game_state, move_log_font) 130 | 131 | if game_state.checkmate: 132 | game_over = True 133 | if game_state.white_to_move: 134 | MoveLog.drawEndGameText(screen, "Black wins by checkmate") 135 | else: 136 | MoveLog.drawEndGameText(screen, "White wins by checkmate") 137 | 138 | elif game_state.stalemate: 139 | game_over = True 140 | MoveLog.drawEndGameText(screen, "Stalemate") 141 | 142 | clock.tick(MAX_FPS) 143 | p.display.flip() 144 | 145 | 146 | def positionsInChess(screen, game_state, valid_moves, square_selected): 147 | """ 148 | Responsible for all the graphics within current game state. 149 | """ 150 | drawBoard(screen) # draw squares on the board 151 | HighlightArea.highlightSquares(screen, game_state, valid_moves, square_selected) 152 | drawElements(screen, game_state.board) # draw pieces on top of those squares 153 | 154 | 155 | def drawBoard(screen): 156 | """ 157 | Draw the squares on the board. 158 | The top left square is always light. 159 | """ 160 | global colors 161 | colors = [p.Color("white"), p.Color("dark green")] 162 | for row in range(DIMENSION): 163 | for column in range(DIMENSION): 164 | color = colors[((row + column) % 2)] 165 | p.draw.rect(screen, color, p.Rect(column * SQUARE_SIZE, row * SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE)) 166 | 167 | 168 | def drawElements(screen, board): 169 | """ 170 | Draw the pieces on the board using the current game_state.board 171 | """ 172 | for row in range(DIMENSION): 173 | for column in range(DIMENSION): 174 | piece = board[row][column] 175 | if piece != "--": 176 | screen.blit(IMAGES[piece], p.Rect(column * SQUARE_SIZE, row * SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE)) 177 | 178 | 179 | def animateMove(move, screen, board, clock): 180 | """ 181 | Animating a move 182 | """ 183 | global colors 184 | d_row = move.end_row - move.start_row 185 | d_col = move.end_col - move.start_col 186 | frames_per_square = 10 # frames to move one square 187 | frame_count = (abs(d_row) + abs(d_col)) * frames_per_square 188 | for frame in range(frame_count + 1): 189 | row, col = (move.start_row + d_row * frame / frame_count, move.start_col + d_col * frame / frame_count) 190 | drawBoard(screen) 191 | drawElements(screen, board) 192 | # erase the piece moved from its ending square 193 | color = colors[(move.end_row + move.end_col) % 2] 194 | end_square = p.Rect(move.end_col * SQUARE_SIZE, move.end_row * SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE) 195 | p.draw.rect(screen, color, end_square) 196 | # draw captured piece onto rectangle 197 | if move.piece_captured != '--': 198 | if move.is_enpassant_move: 199 | enpassant_row = move.end_row + 1 if move.piece_captured[0] == 'b' else move.end_row - 1 200 | end_square = p.Rect(move.end_col * SQUARE_SIZE, enpassant_row * SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE) 201 | screen.blit(IMAGES[move.piece_captured], end_square) 202 | # draw moving piece 203 | screen.blit(IMAGES[move.piece_moved], p.Rect(col * SQUARE_SIZE, row * SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE)) 204 | p.display.flip() 205 | clock.tick(60) 206 | 207 | 208 | if __name__ == "__main__": 209 | main() 210 | -------------------------------------------------------------------------------- /src/test.py: -------------------------------------------------------------------------------- 1 | # """ 2 | # Handling the AI moves. 3 | # """ 4 | # import random 5 | # import threading 6 | # import queue 7 | 8 | # piece_score = {"K": 0, "Q": 9, "R": 5, "B": 3, "N": 3, "p": 1} 9 | 10 | # knight_scores = [[0.0, 0.1, 0.2, 0.2, 0.2, 0.2, 0.1, 0.0], 11 | # [0.1, 0.3, 0.5, 0.5, 0.5, 0.5, 0.3, 0.1], 12 | # [0.2, 0.5, 0.6, 0.65, 0.65, 0.6, 0.5, 0.2], 13 | # [0.2, 0.55, 0.65, 0.7, 0.7, 0.65, 0.55, 0.2], 14 | # [0.2, 0.5, 0.65, 0.7, 0.7, 0.65, 0.5, 0.2], 15 | # [0.2, 0.55, 0.6, 0.65, 0.65, 0.6, 0.55, 0.2], 16 | # [0.1, 0.3, 0.5, 0.55, 0.55, 0.5, 0.3, 0.1], 17 | # [0.0, 0.1, 0.2, 0.2, 0.2, 0.2, 0.1, 0.0]] 18 | 19 | # bishop_scores = [[0.0, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.0], 20 | # [0.2, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.2], 21 | # [0.2, 0.4, 0.5, 0.6, 0.6, 0.5, 0.4, 0.2], 22 | # [0.2, 0.5, 0.5, 0.6, 0.6, 0.5, 0.5, 0.2], 23 | # [0.2, 0.4, 0.6, 0.6, 0.6, 0.6, 0.4, 0.2], 24 | # [0.2, 0.6, 0.6, 0.6, 0.6, 0.6, 0.6, 0.2], 25 | # [0.2, 0.5, 0.4, 0.4, 0.4, 0.4, 0.5, 0.2], 26 | # [0.0, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.0]] 27 | 28 | # rook_scores = [[0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25], 29 | # [0.5, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.5], 30 | # [0.0, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.0], 31 | # [0.0, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.0], 32 | # [0.0, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.0], 33 | # [0.0, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.0], 34 | # [0.0, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.0], 35 | # [0.25, 0.25, 0.25, 0.5, 0.5, 0.25, 0.25, 0.25]] 36 | 37 | # queen_scores = [[0.0, 0.2, 0.2, 0.3, 0.3, 0.2, 0.2, 0.0], 38 | # [0.2, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.2], 39 | # [0.2, 0.4, 0.5, 0.5, 0.5, 0.5, 0.4, 0.2], 40 | # [0.3, 0.4, 0.5, 0.5, 0.5, 0.5, 0.4, 0.3], 41 | # [0.4, 0.4, 0.5, 0.5, 0.5, 0.5, 0.4, 0.3], 42 | # [0.2, 0.5, 0.5, 0.5, 0.5, 0.5, 0.4, 0.2], 43 | # [0.2, 0.4, 0.5, 0.4, 0.4, 0.4, 0.4, 0.2], 44 | # [0.0, 0.2, 0.2, 0.3, 0.3, 0.2, 0.2, 0.0]] 45 | 46 | # pawn_scores = [[0.8, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8], 47 | # [0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7], 48 | # [0.3, 0.3, 0.4, 0.5, 0.5, 0.4, 0.3, 0.3], 49 | # [0.25, 0.25, 0.3, 0.45, 0.45, 0.3, 0.25, 0.25], 50 | # [0.2, 0.2, 0.2, 0.4, 0.4, 0.2, 0.2, 0.2], 51 | # [0.25, 0.15, 0.1, 0.2, 0.2, 0.1, 0.15, 0.25], 52 | # [0.25, 0.3, 0.3, 0.0, 0.0, 0.3, 0.3, 0.25], 53 | # [0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2]] 54 | 55 | # piece_position_scores = {"wN": knight_scores, 56 | # "bN": knight_scores[::-1], 57 | # "wB": bishop_scores, 58 | # "bB": bishop_scores[::-1], 59 | # "wQ": queen_scores, 60 | # "bQ": queen_scores[::-1], 61 | # "wR": rook_scores, 62 | # "bR": rook_scores[::-1], 63 | # "wp": pawn_scores, 64 | # "bp": pawn_scores[::-1]} 65 | 66 | # CHECKMATE = 10000 67 | # STALEMATE = 0 68 | # DEPTH = 5 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | # def findBestMoveThread(game_state, valid_moves, return_queue): 77 | # findMoveNegaMaxAlphaBeta(game_state, valid_moves, DEPTH, -CHECKMATE, CHECKMATE, 78 | # 1 if game_state.white_to_move else -1) 79 | # return_queue.put(next_move) 80 | 81 | # def findMoveConcurrently(game_state, valid_moves , return_queue): 82 | # threads = [] 83 | # return_queue = queue.Queue() 84 | 85 | # for move in valid_moves: 86 | # thread = threading.Thread(target=findBestMoveThread, args=(game_state.copy(), [move], return_queue)) 87 | # thread.start() 88 | # threads.append(thread) 89 | 90 | # for thread in threads: 91 | # thread.join() 92 | 93 | # best_move = None 94 | # best_score = -CHECKMATE 95 | 96 | # while not return_queue.empty(): 97 | # move = return_queue.get() 98 | # score = scoreBoard(game_state.makeMove(move)) 99 | # if score > best_score: 100 | # best_move = move 101 | # best_score = score 102 | 103 | # return best_move 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | # def orderMoves(moves, game_state): 113 | # # Order moves based on captures and high-value targets 114 | # capture_moves = [move for move in moves if game_state.board[move.end_row][move.end_col] != "--"] 115 | # quiet_moves = [move for move in moves if move not in capture_moves] 116 | 117 | # # Sort capture moves by their value 118 | # #what will be benifit if this ? benifit is that we will capture the high value pieces first 119 | # capture_moves.sort(key=lambda move: piece_score[game_state.board[move.end_row][move.end_col][1]], reverse=True) 120 | 121 | # # Sort quiet moves by their positional score 122 | # #what will be benifit if this ? benifit is that we will move the pieces to the best position 123 | # quiet_moves.sort(key=lambda move: piece_position_scores.get(game_state.board[move.start_row][move.start_col], [[0]*8]*8)[move.end_row][move.end_col], reverse=True) 124 | 125 | # ordered_moves = capture_moves + quiet_moves 126 | 127 | # return ordered_moves 128 | 129 | 130 | # def findMoveNegaMaxAlphaBeta(game_state, valid_moves, depth, alpha, beta, turn_multiplier): 131 | # global next_move 132 | # if depth == 0 or game_state.stalemate or game_state.checkmate: 133 | # return turn_multiplier * scoreBoard(game_state) 134 | 135 | # max_score = -CHECKMATE 136 | # # Move Ordering: Sort moves based on some heuristic 137 | # ordered_moves = orderMoves(valid_moves, game_state) 138 | # for move in ordered_moves: 139 | # game_state.makeMove(move) 140 | # next_moves = game_state.getValidMoves() 141 | # score = -findMoveNegaMaxAlphaBeta(game_state, next_moves, depth - 1, -beta, -alpha, -turn_multiplier) 142 | # game_state.undoMove() 143 | # if score > max_score: 144 | # max_score = score 145 | # if depth == DEPTH: 146 | # next_move = move 147 | # alpha = max(alpha, score) 148 | # if alpha >= beta: 149 | # break 150 | # return max_score 151 | 152 | # # Transposition Table 153 | # transposition_table = {} 154 | 155 | # def findMove(game_state, valid_moves, return_queue): 156 | # global next_move, transposition_table 157 | # next_move = None 158 | # for depth in range(1, DEPTH + 1): 159 | # findMoveNegaMaxAlphaBetaTT(game_state, valid_moves, depth, -CHECKMATE, CHECKMATE, 1 if game_state.white_to_move else -1) 160 | # # clear transposition table after each depth iteration 161 | # transposition_table = {} 162 | # if next_move is not None: 163 | # break 164 | # return_queue.put(next_move) 165 | 166 | # def findMoveNegaMaxAlphaBetaTT(game_state, valid_moves, depth, alpha, beta, turn_multiplier): 167 | # global next_move, transposition_table 168 | # hash_key = game_state.getHashKey() 169 | # if hash_key in transposition_table: 170 | # return transposition_table[hash_key] 171 | 172 | # if depth == 0 or game_state.stalemate or game_state.checkmate: 173 | # return turn_multiplier * scoreBoard(game_state) 174 | 175 | # max_score = -CHECKMATE 176 | # ordered_moves = orderMoves(valid_moves, game_state) 177 | # for move in ordered_moves: 178 | # game_state.makeMove(move) 179 | # next_moves = game_state.getValidMoves() 180 | # score = -findMoveNegaMaxAlphaBetaTT(game_state, next_moves, depth - 1, -beta, -alpha, -turn_multiplier) 181 | # game_state.undoMove() 182 | # if score > max_score: 183 | # max_score = score 184 | # if depth == DEPTH: 185 | # next_move = move 186 | # alpha = max(alpha, score) 187 | # if alpha >= beta: 188 | # break 189 | 190 | # transposition_table[hash_key] = max_score 191 | # return max_score 192 | 193 | # def controlCenterMoves(game_state, valid_moves): 194 | # """ 195 | # Prioritize moves that control the center. 196 | # """ 197 | # center_moves = [] 198 | # for move in valid_moves: 199 | # if move.start_row in [1, 6] and abs(move.start_col - move.end_col) <= 1: 200 | # center_moves.append(move) 201 | # elif move.start_col == 4 or move.end_col == 4: 202 | # center_moves.append(move) 203 | # return center_moves 204 | 205 | # def openingMoves(game_state, valid_moves): 206 | # """ 207 | # Integrate famous opening moves. 208 | # """ 209 | # # Define opening moves (e.g., for white) 210 | # opening_moves = {"Italian Game": ["e2e4", "e7e5", "g1f3", "b8c6", "f1c4"], 211 | # "Sicilian Defense": ["e2e4", "c7c5"]} 212 | 213 | # # Check if current moves match any opening moves 214 | # current_moves = [move.getChessNotation() for move in game_state.move_log] 215 | # for opening in opening_moves.values(): 216 | # if current_moves[:len(opening)] == opening: 217 | # return valid_moves[len(opening):] # Return moves after opening moves 218 | 219 | # return valid_moves 220 | 221 | # def findBestMove(game_state, valid_moves, return_queue): 222 | # global next_move 223 | # next_move = None 224 | # #random.shuffle(valid_moves) 225 | # findMoveNegaMaxAlphaBeta(game_state, valid_moves, DEPTH, -CHECKMATE, CHECKMATE, 226 | # 1 if game_state.white_to_move else -1) 227 | # return_queue.put(next_move) 228 | 229 | # def scoreBoard(game_state): 230 | # """ 231 | # Score the board. A positive score is good for white, a negative score is good for black. 232 | # """ 233 | # if game_state.checkmate: 234 | # if game_state.white_to_move: 235 | # return -CHECKMATE # black wins 236 | # else: 237 | # return CHECKMATE # white wins 238 | # elif game_state.stalemate: 239 | # return STALEMATE 240 | # score = 0 241 | # for row in range(len(game_state.board)): 242 | # for col in range(len(game_state.board[row])): 243 | # piece = game_state.board[row][col] 244 | # if piece != "--": 245 | # piece_position_score = 0 246 | # if piece[1] != "K": 247 | # piece_position_score = piece_position_scores[piece][row][col] 248 | # if piece[0] == "w": 249 | # score += piece_score[piece[1]] + piece_position_score 250 | # if piece[0] == "b": 251 | # score -= piece_score[piece[1]] + piece_position_score 252 | 253 | # return score 254 | 255 | 256 | # def findRandomMove(valid_moves): 257 | # """ 258 | # Picks and returns a random valid move. 259 | # """ 260 | # return random.choice(valid_moves) 261 | 262 | 263 | 264 | 265 | 266 | -------------------------------------------------------------------------------- /src/ChessAI.py: -------------------------------------------------------------------------------- 1 | """ 2 | Handling the AI moves. 3 | """ 4 | import random 5 | import threading 6 | import queue 7 | 8 | piece_score = {"K": 6000, "Q": 929, "R": 512, "B": 320, "N": 280, "p": 100} 9 | 10 | 11 | pawn_scores = [[ 0, 0, 0, 0, 0, 0, 0, 0,], 12 | [78, 83, 86, 73, 102, 82, 85, 90], 13 | [ 7, 29, 21, 44, 40, 31, 44, 7], 14 | [-17, 16, -2, 15, 14, 0, 15, -13], 15 | [-26, 3, 10, 9, 6, 1, 0, -23], 16 | [-22, 9, 5, -11, -10, -2, 3, -19], 17 | [-31, 8, -7, -37, -36, -14, 3, -31], 18 | [0.0, 0.1, 0.2, 0.2, 0.2, 0.2, 0.1, 0.0]] 19 | 20 | bishop_scores = [[-59, -78, -82, -76, -23, -107, -37, -50], 21 | [-11, 20, 35, -42, -39, 31, 2, -22], 22 | [-9, 39, -32, 41, 52, -10, 28, -14], 23 | [25, 17, 20, 34, 26, 25, 15, 10], 24 | [13, 10, 17, 23, 17, 16, 0, 7], 25 | [14, 25, 24, 15, 8, 25, 20, 15], 26 | [19, 20, 11, 6, 7, 6, 20, 16], 27 | [-7, 2, -15, -12, -14, -15, -10, -10]] 28 | 29 | rook_scores = [[35, 29, 33, 4, 37, 33, 56, 50], 30 | [55, 29, 56, 67, 55, 62, 34, 60], 31 | [19, 35, 28, 33, 45, 27, 25, 15], 32 | [0, 5, 16, 13, 18, -4, -9, -6], 33 | [-28, -35, -16, -21, -13, -29, -46, -30], 34 | [-42, -28, -42, -25, -25, -35, -26, -46], 35 | [-53, -38, -31, -26, -29, -43, -44, -53], 36 | [-30, -24, -18, 5, -2, -18, -31, -32]] 37 | 38 | queen_scores = [[ 6, 1, -8, -104, 69, 24, 88, 26], 39 | [ 14, 32, 60, -10, 20, 76, 57, 24], 40 | [ -2, 43, 32, 60, 72, 63, 43, 2], 41 | [ 1, -16, 22, 17, 25, 20, -13, -6], 42 | [ -14, -15, -2, -5, -1, -10, -20, -22], 43 | [ -30, -6, -13, -11, -16, -11, -16, -27], 44 | [ -36, -18, 0, -19, -15, -15, -21, -38], 45 | [ -39, -30, -31, -13, -31, -36, -34, -42]] 46 | 47 | 48 | knight_scores = [[-66, -53, -75, -75, -10, -55, -58, -70], 49 | [-3, -6, 100, -36, 4, 62, -4, -14], 50 | [10, 67, 1, 74, 73, 27, 62, -2], 51 | [24, 24, 45, 37, 33, 41, 25, 17], 52 | [-1, 5, 31, 21, 22, 35, 2, 0], 53 | [-18, 10, 13, 22, 18, 15, 11, -14], 54 | [-23, -15, 2, 0, 2, 0, -23, -20], 55 | [-66, -53, -75, -75, -10, -55, -58, -70]] 56 | 57 | piece_position_scores = {"wN": knight_scores, 58 | "bN": knight_scores[::-1], 59 | "wB": bishop_scores, 60 | "bB": bishop_scores[::-1], 61 | "wQ": queen_scores, 62 | "bQ": queen_scores[::-1], 63 | "wR": rook_scores, 64 | "bR": rook_scores[::-1], 65 | "wp": pawn_scores, 66 | "bp": pawn_scores[::-1]} 67 | 68 | CHECKMATE = 10000 69 | STALEMATE = 0 70 | DEPTH = 4 71 | 72 | 73 | def orderMoves(moves, game_state): 74 | # Order moves based on captures and high-value targets 75 | capture_moves = [move for move in moves if game_state.board[move.end_row][move.end_col] != "--"] 76 | quiet_moves = [move for move in moves if move not in capture_moves] 77 | 78 | #what will be benifit if this ? benifit is that we will capture the high value pieces first 79 | capture_moves.sort(key=lambda move: piece_score[game_state.board[move.end_row][move.end_col][1]], reverse=True) 80 | 81 | #what will be benifit if this ? benifit is that we will move the pieces to the best position 82 | quiet_moves.sort(key=lambda move: piece_position_scores.get(game_state.board[move.start_row][move.start_col], [[0]*8]*8)[move.end_row][move.end_col], reverse=True) 83 | 84 | ordered_moves = capture_moves + quiet_moves 85 | 86 | return ordered_moves 87 | 88 | 89 | def findMoveNegaMaxAlphaBeta(game_state, valid_moves, depth, alpha, beta, turn_multiplier): 90 | global next_move 91 | if depth == 0 or game_state.stalemate or game_state.checkmate: 92 | return turn_multiplier * scoreBoard(game_state) 93 | 94 | max_score = -CHECKMATE 95 | # Move Ordering: Sorting moves based on some heuristic 96 | ordered_moves = orderMoves(valid_moves, game_state) 97 | for move in ordered_moves: 98 | game_state.makeMove(move) 99 | next_moves = game_state.getValidMoves() 100 | score = -findMoveNegaMaxAlphaBeta(game_state, next_moves, depth - 1, -beta, -alpha, -turn_multiplier) 101 | game_state.undoMove() 102 | if score > max_score: 103 | max_score = score 104 | if depth == DEPTH: 105 | next_move = move 106 | alpha = max(alpha, score) 107 | if alpha >= beta: 108 | break 109 | return max_score 110 | 111 | # Transposition Table 112 | transposition_table = {} #used to not re-evaluate position that is already evaluated 113 | 114 | def findMove(game_state, valid_moves, return_queue): 115 | global next_move, transposition_table 116 | next_move = None 117 | for depth in range(1, DEPTH + 1): 118 | findMoveNegaMaxAlphaBetaTT(game_state, valid_moves, depth, -CHECKMATE, CHECKMATE, 1 if game_state.white_to_move else -1) 119 | transposition_table = {} 120 | if next_move is not None: 121 | break 122 | return_queue.put(next_move) 123 | 124 | 125 | def findMoveNegaMaxAlphaBetaTT(game_state, valid_moves, depth, alpha, beta, turn_multiplier): 126 | global next_move, transposition_table 127 | hash_key = game_state.getHashKey() 128 | if hash_key in transposition_table: 129 | return transposition_table[hash_key] 130 | 131 | if depth == 0 or game_state.stalemate or game_state.checkmate: 132 | return turn_multiplier * scoreBoard(game_state) 133 | 134 | max_score = -CHECKMATE 135 | ordered_moves = orderMoves(valid_moves, game_state) 136 | for move in ordered_moves: 137 | game_state.makeMove(move) 138 | next_moves = game_state.getValidMoves() 139 | score = -findMoveNegaMaxAlphaBetaTT(game_state, next_moves, depth - 1, -beta, -alpha, -turn_multiplier) 140 | game_state.undoMove() 141 | if score > max_score: 142 | max_score = score 143 | if depth == DEPTH: 144 | next_move = move 145 | alpha = max(alpha, score) 146 | if alpha >= beta: 147 | break 148 | 149 | transposition_table[hash_key] = max_score 150 | return max_score 151 | 152 | def controlCenterMoves(game_state, valid_moves): 153 | """ 154 | Prioritize moves that control the center. 155 | """ 156 | center_moves = [] 157 | for move in valid_moves: 158 | if move.start_row in [1, 6] and abs(move.start_col - move.end_col) <= 1: 159 | center_moves.append(move) 160 | elif move.start_col == 4 or move.end_col == 4: 161 | center_moves.append(move) 162 | return center_moves 163 | 164 | def openingMoves(game_state, valid_moves): 165 | """ 166 | Integrate famous opening moves. 167 | """ 168 | opening_moves = {"Italian Game": ["e2e4", "e7e5", "g1f3", "b8c6", "f1c4"], 169 | "Sicilian Defense": ["e2e4", "c7c5"]} 170 | 171 | current_moves = [move.getChessNotation() for move in game_state.move_log] 172 | for opening in opening_moves.values(): 173 | if current_moves[:len(opening)] == opening: 174 | return valid_moves[len(opening):] 175 | 176 | return valid_moves 177 | 178 | def findBestMove(game_state, valid_moves, return_queue): 179 | global next_move 180 | next_move = None 181 | #random.shuffle(valid_moves) 182 | findMoveNegaMaxAlphaBeta(game_state, valid_moves, DEPTH, -CHECKMATE, CHECKMATE, 183 | 1 if game_state.white_to_move else -1) 184 | return_queue.put(next_move) 185 | 186 | def scoreBoard(game_state): 187 | """ 188 | Score the board. A positive score is good for white, a negative score is good for black. 189 | """ 190 | if game_state.checkmate: 191 | if game_state.white_to_move: 192 | return -CHECKMATE # black wins 193 | else: 194 | return CHECKMATE # white wins 195 | elif game_state.stalemate: 196 | return STALEMATE 197 | score = 0 198 | for row in range(len(game_state.board)): 199 | for col in range(len(game_state.board[row])): 200 | piece = game_state.board[row][col] 201 | if piece != "--": 202 | piece_position_score = 0 203 | if piece[1] != "K": 204 | piece_position_score = piece_position_scores[piece][row][col] 205 | if piece[0] == "w": 206 | score += piece_score[piece[1]] + piece_position_score 207 | if piece[0] == "b": 208 | score -= piece_score[piece[1]] + piece_position_score 209 | 210 | return score 211 | 212 | 213 | def findRandomMove(valid_moves): 214 | """ 215 | Picks and returns a random valid move. 216 | """ 217 | return random.choice(valid_moves) 218 | 219 | 220 | 221 | 222 | 223 | 224 | # knight_scores = [[0.0, 0.1, 0.2, 0.2, 0.2, 0.2, 0.1, 0.0], 225 | # [0.1, 0.3, 0.5, 0.5, 0.5, 0.5, 0.3, 0.1], 226 | # [0.2, 0.5, 0.6, 0.65, 0.65, 0.6, 0.5, 0.2], 227 | # [0.2, 0.55, 0.65, 0.7, 0.7, 0.65, 0.55, 0.2], 228 | # [0.2, 0.5, 0.65, 0.7, 0.7, 0.65, 0.5, 0.2], 229 | # [0.2, 0.55, 0.6, 0.65, 0.65, 0.6, 0.55, 0.2], 230 | # [0.1, 0.3, 0.5, 0.55, 0.55, 0.5, 0.3, 0.1], 231 | # [0.0, 0.1, 0.2, 0.2, 0.2, 0.2, 0.1, 0.0]] 232 | 233 | # bishop_scores = [[0.0, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.0], 234 | # [0.2, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.2], 235 | # [0.2, 0.4, 0.5, 0.6, 0.6, 0.5, 0.4, 0.2], 236 | # [0.2, 0.5, 0.5, 0.6, 0.6, 0.5, 0.5, 0.2], 237 | # [0.2, 0.4, 0.6, 0.6, 0.6, 0.6, 0.4, 0.2], 238 | # [0.2, 0.6, 0.6, 0.6, 0.6, 0.6, 0.6, 0.2], 239 | # [0.2, 0.5, 0.4, 0.4, 0.4, 0.4, 0.5, 0.2], 240 | # [0.0, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.0]] 241 | 242 | # rook_scores = [[0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25], 243 | # [0.5, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.5], 244 | # [0.0, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.0], 245 | # [0.0, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.0], 246 | # [0.0, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.0], 247 | # [0.0, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.0], 248 | # [0.0, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.0], 249 | # [0.25, 0.25, 0.25, 0.5, 0.5, 0.25, 0.25, 0.25]] 250 | 251 | # queen_scores = [[0.0, 0.2, 0.2, 0.3, 0.3, 0.2, 0.2, 0.0], 252 | # [0.2, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.2], 253 | # [0.2, 0.4, 0.5, 0.5, 0.5, 0.5, 0.4, 0.2], 254 | # [0.3, 0.4, 0.5, 0.5, 0.5, 0.5, 0.4, 0.3], 255 | # [0.4, 0.4, 0.5, 0.5, 0.5, 0.5, 0.4, 0.3], 256 | # [0.2, 0.5, 0.5, 0.5, 0.5, 0.5, 0.4, 0.2], 257 | # [0.2, 0.4, 0.5, 0.4, 0.4, 0.4, 0.4, 0.2], 258 | # [0.0, 0.2, 0.2, 0.3, 0.3, 0.2, 0.2, 0.0]] 259 | 260 | # pawn_scores = [[0.8, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8], 261 | # [0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7], 262 | # [0.3, 0.3, 0.4, 0.5, 0.5, 0.4, 0.3, 0.3], 263 | # [0.25, 0.25, 0.3, 0.45, 0.45, 0.3, 0.25, 0.25], 264 | # [0.2, 0.2, 0.2, 0.4, 0.4, 0.2, 0.2, 0.2], 265 | # [0.25, 0.15, 0.1, 0.2, 0.2, 0.1, 0.15, 0.25], 266 | # [0.25, 0.3, 0.3, 0.0, 0.0, 0.3, 0.3, 0.25], 267 | # [0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2]] 268 | 269 | 270 | # test data of positional statistics 271 | 272 | 273 | 274 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Sepentia - Chess Engine 3 | 4 | ## Prerequisites 5 | 6 | Before you start, ensure you have the following installed on your system: 7 | 8 | - [Python](https://www.python.org/downloads/) (version 3.6 or higher) 9 | - [Git](https://git-scm.com/downloads) (optional, if you want to clone the repository) 10 | 11 | ## Installation Steps 12 | 13 | 1. **Clone the Repository**: 14 | 15 | ```bash 16 | git clone https://github.com/EuclidStellar/Sepentia-ChessEngine.git 17 | 18 | ``` 19 | 2. **Install Pygame**: 20 | ```bash 21 | pip3 install pygame 22 | ``` 23 | 4. **Open Sepentia in Code Editor (VS code)**: 24 | ```bash 25 | python3 chessmain.py 26 | ``` 27 | 28 | ## Heurestia 29 | 30 | [![Medium](https://github-readme-medium.vercel.app/?username=euclidstellar_57634)](https://medium.com/@euclidstellar_57634) 31 | 32 | As AI is a buzzword nowadays, let's talk mathematics and implement some real positional statistics and some algorithms that you have used in cp but never in real life. Ever thought about how Samay Raina's chess-dotcom bot plays against you at 1800 ElO? As Most of your answers will be: we will collect all his game data on how he plays and then train a model that replicates his moves but I mean how you can train something that has 121 million possibilities maybe Chess.com personalise some of his blunders in his bot but that's not how it works 33 | 34 | So, when I was watching Nakamura vs Pragg ( 98.8 vs 98.3 ) I just out of curiosity got a question how these accuracies are calculated 35 | ans: chess engine ( Stockfish ) OK how do they work? 36 | Chess Engine uses a very simple and standard algorithm minimax ( https://leetcode.com/problems/can-i-win/description/ ) just in case you are a leetcode grinder here's a task for you & you know the drill 37 | moreover, minimax is not an optimal algorithm to calculate the best move on the square board of 64 blocks where positional statistics changes after every move. 38 | 39 | So here's my experience of coding a Chess Engine of 1400 ElO at a depth of 5 40 | for non-chess players: ELO is a rating stat, and depth is the number of moves you calculate ahead in the game tree to evaluate possible moves 41 | 42 | ![Depth](/images/depth.png) 43 | 44 | First, we will check all the valid moves available on the chessboard then we use the minimax algorithm to explore each level of the possible game tree where 45 | each level represents a player's turn, and each node represents a possible board position after a move. The main objective of the minimax algorithm is to evaluate maximising moves for your turn and evaluating minimising moves for the opponent ( for now, consider this for material on the board algo will always try to have your maximum material on the chessboard for you and minimum for the opponent ) 46 | The main problem with the minimax algorithm is its computational complexity, especially where the branching factor (number of possible moves) is high 47 | Minimax explores the entire game tree to find the best move which is not optimal because you are lazy(human) and you cannot travel each node 48 | 49 | here comes the alpha-beta pruning algorithm: 50 | When a maximising player finds a move with a value greater than or equal to beta, it means that the opponent has a better move elsewhere. Therefore, the maximising doesn't need to explore further down this branch. It cuts off the exploration and returns the current best move with value beta. 51 | 52 | OK now we know how a game tree works and how we search inside a game tree 53 | but the problem is in chess there's a huge role of positional scores, pawn structures, centre control, forking and pins all these parameters make a chess engine. 54 | how are we gonna tackle all this? 55 | well well we have some desired data on the internet through which we can make this possible as each piece has a different value as a material and different value on different positions of the chessboard 56 | Example: 
king = 0 ( you cannot capture king ), Queen = 9, Rook = 5, Bishop = 3, Knight= 3, pawn=1 [yhese al are material value ] 57 | positional scores: every piece on the chessboard will have a different position score on 64 blocks based on studies ex: The value 0.0 represents the lowest score, meaning the worst position for a knight. The values increase as we move towards the centre of the board, indicating better positions example: a knight placed in the centre (rows 2 to 5, columns 2 to 5) would have higher scores compared to those placed on the edges with the highest score, 0.7, is assigned to the centre-most squares (row 3, column 3 to 6) the score gradually decreases as we move away from the centre.
 58 | The role of these scores in the code is to help evaluate the relative strength of different positions on the board for each type of piece during the algorithm’s decision-making process, these scores are used to prioritize moves that lead to positions with higher scores
 59 | 60 | ![Relative-Positional-value](/images/positional-value.jpg) 61 | 62 | example : The positional score of a piece reflects its strength based on its position on the board and in chess, certain positions are more advantageous for pieces than others example, a knight in the center of the board has more mobility and control over squares than a knight stuck in a corner. Similarly, a rook on an open file has more attacking potential than a rook blocked by pawns by incorporating positional scores into the evaluation function of a chess engine, the engine can make smarter decisions about which moves to prioritize. For instance, the engine might prefer moving a piece to a square where it has better control over the board or can support other pieces more effectively. 63 | 64 | ![Positional-value](/images/positional-value-2.png) 65 | 66 | Now let's understand how ordering of move is done and why ordering of move is very important for a chess engine to make decision ? 67 | By ordering moves, the engine reduces the search space, focusing on the most promising moves first this pruning of less promising moves makes the search more efficient, allowing the engine to explore deeper and find better moves within a given time frame. 68 | 69 | I have divided ordering move in 2 steps: capture moves and quiet moves 70 | Capture moves are those moves where a piece captures an opponent's piece and Quiet moves are those moves where a piece moves to a square without capturing any opponent's piece. Capture moves are sorted based on the value of the piece being captured higher-valued pieces are prioritized for capture this is implemented using a lambda function that retrieves the piece value from the piece_score dictionary and sorts the capture moves in descending order of piece value. 71 | Quiet moves are sorted based on the value of the target square each square has a positional score that indicates how good it is for a piece to occupy that square the piece_position_scores dictionary stores these positional scores for each piece type on each square of the board and quiet moves are sorted in descending order of the positional score of the target square and evaluating quiet moves based on positional scores helps the engine identify moves that improve the overall position of its pieces this ensures that the engine doesn't just capture pieces but also aims to improve its position on the board, leading to better long-term prospects and then I'm returning the summision of both the operations. 72 | 73 | When evaluating the chessboard, we consider two main factors: intrinsic value and positional value. Intrinsic value is like the importance of a department in a company; it's how valuable the piece is in isolation. Positional value is akin to the location of the department within the company's offices. A sales department located in a prime location (like controlling the center of the board) is more valuable than one in a less desirable area (like trapped in a corner). 74 | Checkmate in chess is like one company buying out another. If one company decisively takes over, its value rises while the other company's value becomes zero. Stalemate, on the other hand, is like a merger negotiation that stalls. No company wins or loses; the value remains static. 75 | The total score of the chessboard, calculated by summing up the intrinsic and positional values of all pieces, gives an overall appraisal of the game state. This evaluation is essential for a chess engine's decision-making process. It helps the engine decide which moves are better than others, formulate long-term strategies, and even determine the order in which moves should be explored during the search process 76 | 77 | ![Move-order](/images/move-order.png) 78 | 79 | Ok now we have understanding of how engine works let's understand how to make it effecient ? 80 | 81 | Imagine you're playing chess and you have to decide which move to make where each move you can make leads to another set of possible moves for your opponent, and those moves lead to even more moves, forming a tree-like structure this structure is called a game tree. 82 | 83 | Let's say you're considering three possible moves, labeled A, B, and C. Each of these moves leads to two possible responses from your opponent, and each of those responses leads to another two possible moves from you, and so on. 84 | ```bash 85 | A 86 | / \ 87 | Opp1 Opp2 88 | / \ / \ 89 | B1 B2 C1 C2 90 | / \ / \ / \ / \ 91 | ... ... ... ... 92 | ``` 93 | the game tree is much larger and deeper, but for this example, we'll keep it simple. 94 | Alpha-Beta Pruning: Imagine you have to explore this entire game tree to find the best move. It would take a lot of time and computational power. This is where alpha-beta pruning comes in. 95 | Alpha-beta pruning is a way to reduce the number of nodes explored in the game tree by eliminating branches that can't possibly lead to a better outcome. 96 | Alpha: Represents the best (highest) value found so far by any means along the path for the maximizing player (e.g., white). 97 | Beta: Represents the best (lowest) value found so far by any means along the path for the minimizing player (e.g., black). 98 | If the algorithm finds a move that is better than the current alpha (for the maximizing player) or beta (for the minimizing player), it updates the alpha or beta accordingly. 99 | If, during the exploration of the subtree, the algorithm finds a node where the beta of the minimizing player becomes less than or equal to the alpha of the maximizing player, it knows that the maximizing player won't choose this path because the minimizing player has a better option elsewhere. So, the algorithm prunes (cuts off) this branch and doesn't explore it further. 100 | 101 | 102 | ![Depth](/images/alpha-beta.png) 103 | 104 | In the context of game trees, depth refers to how many moves ahead the algorithm is exploring. Each level of depth represents a move by one player (e.g., white), followed by a response by the opponent (e.g., black). 105 | 106 | ```bash 107 | Depth 0 (Initial Board) 108 | / \ 109 | Depth 1 Depth 1 110 | (White) (Black) 111 | / \ / \ 112 | Depth 2 Depth 2 Depth 2 Depth 2 113 | (White) (Black) (White) (Black) 114 | ... ... ... ... 115 | 116 | In this tree, the root node represents the initial board position. 117 | Each subsequent level of nodes represents the possible moves by each player. 118 | The depth of the tree indicates how many moves ahead the algorithm is exploring. 119 | 120 | ``` 121 | How we are achieving depth ? 122 | Using recursion ( yeah , you got it if there's a recursion there must be a problem with time and memory hold on we will sort this later) 123 | for now let's understand recursive calls in this engine 124 | The depth of the game tree corresponds to the recursion depth of the algorithm and recursive calls continue until a stopping condition is met, such as reaching the desired depth or encountering a terminal node (checkmate) and more importantly recursive calls allow the algorithm to evaluate positions at different depths, considering the moves of both players and after exploring a branch of the tree, recursive calls backtrack to explore other branches this backtracking process effectively explores the entire tree up to the specified depth. 125 | 126 | Now you must say "hehe recursive calls hi to hai badha do depth badh jayegi aur engine acha ho jayega ?" 127 | Lemme ask you something "paise hi to hai government se bolo aur chaap de gareebi khatam ho jayegi ?" 128 | Let's understand why NOT? 129 | As the depth of the game tree increases, the number of nodes explored and the computational power required also increase exponentially this has significant effects on both memory and CPU power 130 | When we explore additional level of depth, the number of nodes in the tree grows exponentially and more memory is required to store information about each node, including board positions, scores, and move sequences and deeper searches require more memory to hold the additional nodes, potentially leading to memory overflow or excessive memory consumption which ultimately leads to more time. 131 | 132 | here this line comes in " yeah , you got it if there's a recursion there must be a problem with time and memory hold on we will sort this later" 133 | 134 | now understand how I'm handling memory and computational resources to make my chess engine more fast and accurate 135 | Using Transposition table is the most crucial step I choose to tackle this problem : 136 | A transposition table is essentially a cache that stores previously computed positions and their evaluations it's like a memory bank that remembers positions encountered during the search. 137 | Let's understand how it works and how it is helping my algo: 138 | Each position in the game is hashed to create a unique identifier and this hash serves as the key to store and retrieve information about the position in the transposition table when a position is evaluated during the search, its evaluation value, depth, and other relevant information are stored in the transposition table whereas this information is associated with the hashed position 139 | 140 | Before evaluating a position, the algorithm checks if it has already been evaluated and stored in the transposition table and if the position is found in the table and its depth is sufficient, the algorithm can use the stored evaluation value instead of re-evaluating the position and if the stored depth is less than the current depth, the information may not be reliable, and the position may need to be re-evaluated. 141 | During deep searches, many positions are revisited due to branching in the game tree and the transposition table helps avoid re-evaluating these positions. With transposition table, the algorithm can safely increase the depth of the search without running out of memory even with limited memory resources, the table ensures that previously explored positions are available for reference. 142 | 143 | OK Gaurav We tackled the memory but how we are managing the time you may have saved memory but what about time ? 144 | ok let's understanding it with an analogy : 145 | Imagine you're building a puzzle, and each friend helps with a different section. The depth of the task represents its complexity, like adding more layers to the puzzle. Recursive calls break down complex tasks into smaller parts, just as you might divide a puzzle into smaller sections to solve them. As depth increases, so does the demand for memory and processing power. Here multiprocessing helps by distributing the workload across multiple CPU cores, allowing tasks to be completed faster and more efficiently. 146 | 147 | ![Depth](/images/trans-table.jpg) 148 | 149 | By far you have got an answer of using multiprocessing let's understand how it gonna help sepentia: 150 | In Sepentia, the search process involves exploring possible moves and counter-moves where this search can be broken down into independent tasks that can be executed simultaneously. Multiprocessing allows the chess engine to perform these tasks concurrently across multiple CPU cores, drastically speeding up the search process where each core can explore a different branch of the game tree simultaneously, effectively increasing the search depth within the same amount of time. While one process explores a particular branch, other processes can generate moves for subsequent branches, thus overlapping computation and improving efficiency which helps to process more positions per second. 151 | 152 | I also tried to use GPU for achieving parallelism but failed to fight with the architecture of my mac but here's a intution how GPU can help to make sepentia more better : 153 | GPUs are highly parallel processors that excel at performing many computations simultaneously by offloading certain computational tasks to GPUs, Sepentia can achieve even greater performance gains, especially in tasks like evaluating positions and performing deep searches. 154 | 155 | 156 | That's the whole intution and logic behind this If you read the complete stuff I have written I sincerely thank you for your time and 157 | If you have any idea to Improve this and want to give a suggestion drop a mail at euclidstellar@gmail.com I will be more than happy to have your suggestions. 158 | 159 | ```bash 160 | Sepentia, 161 | Gaurav 162 | @euclidstellar 163 | ``` 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 
 177 | -------------------------------------------------------------------------------- /src/chessengine.py: -------------------------------------------------------------------------------- 1 | class GameState: 2 | def __init__(self): 3 | self.board = [ 4 | ["bR", "bN", "bB", "bQ", "bK", "bB", "bN", "bR"], 5 | ["bp", "bp", "bp", "bp", "bp", "bp", "bp", "bp"], 6 | ["--", "--", "--", "--", "--", "--", "--", "--"], 7 | ["--", "--", "--", "--", "--", "--", "--", "--"], 8 | ["--", "--", "--", "--", "--", "--", "--", "--"], 9 | ["--", "--", "--", "--", "--", "--", "--", "--"], 10 | ["wp", "wp", "wp", "wp", "wp", "wp", "wp", "wp"], 11 | ["wR", "wN", "wB", "wQ", "wK", "wB", "wN", "wR"]] 12 | self.moveFunctions = {"p": self.getPawnMoves, "R": self.getRookMoves, "N": self.getKnightMoves, 13 | "B": self.getBishopMoves, "Q": self.getQueenMoves, "K": self.getKingMoves} 14 | self.white_to_move = True 15 | self.move_log = [] 16 | self.white_king_location = (7, 4) 17 | self.black_king_location = (0, 4) 18 | self.checkmate = False 19 | self.stalemate = False 20 | self.in_check = False 21 | self.pins = [] 22 | self.checks = [] 23 | self.move_counter = 0 24 | self.enpassant_possible = () # coordinates for the square where en-passant capture is possible 25 | self.enpassant_possible_log = [self.enpassant_possible] 26 | self.current_castling_rights = CastleRights(True, True, True, True) 27 | self.castle_rights_log = [CastleRights(self.current_castling_rights.wks, self.current_castling_rights.bks, 28 | self.current_castling_rights.wqs, self.current_castling_rights.bqs)] 29 | 30 | def makeMove(self, move): 31 | """ 32 | Takes a Move as a parameter and executes it. 33 | (this will not work for castling, pawn promotion and en-passant) 34 | """ 35 | self.board[move.start_row][move.start_col] = "--" 36 | self.board[move.end_row][move.end_col] = move.piece_moved 37 | self.move_log.append(move) # log the move so we can undo it later 38 | self.white_to_move = not self.white_to_move # switch players 39 | self.move_counter = self.move_counter + 1 40 | # update king's location if moved 41 | if move.piece_moved == "wK": 42 | self.white_king_location = (move.end_row, move.end_col) 43 | elif move.piece_moved == "bK": 44 | self.black_king_location = (move.end_row, move.end_col) 45 | 46 | # pawn promotion 47 | if move.is_pawn_promotion: 48 | # if not is_AI: 49 | # promoted_piece = input("Promote to Q, R, B, or N:") #take this to UI later 50 | # self.board[move.end_row][move.end_col] = move.piece_moved[0] + promoted_piece 51 | # else: 52 | self.board[move.end_row][move.end_col] = move.piece_moved[0] + "Q" 53 | 54 | # enpassant move 55 | if move.is_enpassant_move: 56 | self.board[move.start_row][move.end_col] = "--" # capturing the pawn 57 | 58 | # update enpassant_possible variable 59 | if move.piece_moved[1] == "p" and abs(move.start_row - move.end_row) == 2: # only on 2 square pawn advance 60 | self.enpassant_possible = ((move.start_row + move.end_row) // 2, move.start_col) 61 | else: 62 | self.enpassant_possible = () 63 | 64 | # castle move 65 | if move.is_castle_move: 66 | if move.end_col - move.start_col == 2: # king-side castle move 67 | self.board[move.end_row][move.end_col - 1] = self.board[move.end_row][ 68 | move.end_col + 1] # moves the rook to its new square 69 | self.board[move.end_row][move.end_col + 1] = '--' # erase old rook 70 | else: # queen-side castle move 71 | self.board[move.end_row][move.end_col + 1] = self.board[move.end_row][ 72 | move.end_col - 2] # moves the rook to its new square 73 | self.board[move.end_row][move.end_col - 2] = '--' # erase old rook 74 | 75 | self.enpassant_possible_log.append(self.enpassant_possible) 76 | 77 | # update castling rights - whenever it is a rook or king move 78 | self.updateCastleRights(move) 79 | self.castle_rights_log.append(CastleRights(self.current_castling_rights.wks, self.current_castling_rights.bks, 80 | self.current_castling_rights.wqs, self.current_castling_rights.bqs)) 81 | def getHashKey(self): 82 | """ 83 | Returns a unique identifier for the current board state. 84 | """ 85 | 86 | return str(self.board) 87 | def undoMove(self): 88 | """ 89 | Undo the last move 90 | """ 91 | if len(self.move_log) != 0: # make sure that there is a move to undo 92 | move = self.move_log.pop() 93 | self.board[move.start_row][move.start_col] = move.piece_moved 94 | self.board[move.end_row][move.end_col] = move.piece_captured 95 | self.white_to_move = not self.white_to_move # swap players 96 | # update the king's position if needed 97 | if move.piece_moved == "wK": 98 | self.white_king_location = (move.start_row, move.start_col) 99 | elif move.piece_moved == "bK": 100 | self.black_king_location = (move.start_row, move.start_col) 101 | # undo en passant move 102 | if move.is_enpassant_move: 103 | self.board[move.end_row][move.end_col] = "--" # leave landing square blank 104 | self.board[move.start_row][move.end_col] = move.piece_captured 105 | 106 | self.enpassant_possible_log.pop() 107 | self.enpassant_possible = self.enpassant_possible_log[-1] 108 | 109 | # undo castle rights 110 | self.castle_rights_log.pop() # get rid of the new castle rights from the move we are undoing 111 | self.current_castling_rights = self.castle_rights_log[ 112 | -1] # set the current castle rights to the last one in the list 113 | # undo the castle move 114 | if move.is_castle_move: 115 | if move.end_col - move.start_col == 2: # king-side 116 | self.board[move.end_row][move.end_col + 1] = self.board[move.end_row][move.end_col - 1] 117 | self.board[move.end_row][move.end_col - 1] = '--' 118 | else: # queen-side 119 | self.board[move.end_row][move.end_col - 2] = self.board[move.end_row][move.end_col + 1] 120 | self.board[move.end_row][move.end_col + 1] = '--' 121 | self.checkmate = False 122 | self.stalemate = False 123 | 124 | def updateCastleRights(self, move): 125 | """ 126 | Update the castle rights given the move 127 | """ 128 | if move.piece_captured == "wR": 129 | if move.end_col == 0: # left rook 130 | self.current_castling_rights.wqs = False 131 | elif move.end_col == 7: # right rook 132 | self.current_castling_rights.wks = False 133 | elif move.piece_captured == "bR": 134 | if move.end_col == 0: # left rook 135 | self.current_castling_rights.bqs = False 136 | elif move.end_col == 7: # right rook 137 | self.current_castling_rights.bks = False 138 | 139 | if move.piece_moved == 'wK': 140 | self.current_castling_rights.wqs = False 141 | self.current_castling_rights.wks = False 142 | elif move.piece_moved == 'bK': 143 | self.current_castling_rights.bqs = False 144 | self.current_castling_rights.bks = False 145 | elif move.piece_moved == 'wR': 146 | if move.start_row == 7: 147 | if move.start_col == 0: # left rook 148 | self.current_castling_rights.wqs = False 149 | elif move.start_col == 7: # right rook 150 | self.current_castling_rights.wks = False 151 | elif move.piece_moved == 'bR': 152 | if move.start_row == 0: 153 | if move.start_col == 0: # left rook 154 | self.current_castling_rights.bqs = False 155 | elif move.start_col == 7: # right rook 156 | self.current_castling_rights.bks = False 157 | 158 | def getValidMoves(self): 159 | """ 160 | All moves considering checks. 161 | """ 162 | temp_castle_rights = CastleRights(self.current_castling_rights.wks, self.current_castling_rights.bks, 163 | self.current_castling_rights.wqs, self.current_castling_rights.bqs) 164 | # advanced algorithm 165 | moves = [] 166 | self.in_check, self.pins, self.checks = self.checkForPinsAndChecks() 167 | 168 | if self.white_to_move: 169 | king_row = self.white_king_location[0] 170 | king_col = self.white_king_location[1] 171 | else: 172 | king_row = self.black_king_location[0] 173 | king_col = self.black_king_location[1] 174 | if self.in_check: 175 | if len(self.checks) == 1: # only 1 check, block the check or move the king 176 | moves = self.getAllPossibleMoves() 177 | # to block the check you must put a piece into one of the squares between the enemy piece and your king 178 | check = self.checks[0] # checking information that will be used multiple times 179 | check_row = check[0] 180 | check_col = check[1] 181 | piece_checking = self.board[check_row][check_col] 182 | valid_squares = [] # squares that pieces can move to 183 | # if knight, must capture the knight or move your king, other pieces can be blocked 184 | if piece_checking[1] == "N": 185 | valid_squares = [(check_row, check_col)] 186 | else: 187 | for i in range(1, 8): 188 | valid_square = (king_row + check[2] * i, 189 | king_col + check[3] * i) # check[2] and check[3] are the check directions 190 | valid_squares.append(valid_square) 191 | if valid_square[0] == check_row and valid_square[ 192 | 1] == check_col: # once you get to piece and check 193 | break 194 | # get rid of any moves that don't block check or move king 195 | for i in range(len(moves) - 1, -1, -1): # iterate through the list backwards when removing elements 196 | if moves[i].piece_moved[1] != "K": # move doesn't move king so it must block or capture 197 | if not (moves[i].end_row, 198 | moves[i].end_col) in valid_squares: # move doesn't block or capture piece 199 | moves.remove(moves[i]) 200 | else: # double check, king has to move 201 | self.getKingMoves(king_row, king_col, moves) 202 | else: # not in check - all moves are fine 203 | moves = self.getAllPossibleMoves() 204 | if self.white_to_move: 205 | self.getCastleMoves(self.white_king_location[0], self.white_king_location[1], moves) 206 | else: 207 | self.getCastleMoves(self.black_king_location[0], self.black_king_location[1], moves) 208 | 209 | if len(moves) == 0: 210 | if self.inCheck(): 211 | self.checkmate = True 212 | else: 213 | 214 | self.stalemate = True 215 | else: 216 | self.checkmate = False 217 | self.stalemate = False 218 | 219 | self.current_castling_rights = temp_castle_rights 220 | return moves 221 | 222 | def inCheck(self): 223 | """ 224 | Determine if a current player is in check 225 | """ 226 | if self.white_to_move: 227 | return self.squareUnderAttack(self.white_king_location[0], self.white_king_location[1]) 228 | else: 229 | return self.squareUnderAttack(self.black_king_location[0], self.black_king_location[1]) 230 | 231 | def squareUnderAttack(self, row, col): 232 | """ 233 | Determine if enemy can attack the square row col 234 | """ 235 | self.white_to_move = not self.white_to_move # switch to opponent's point of view 236 | opponents_moves = self.getAllPossibleMoves() 237 | self.white_to_move = not self.white_to_move 238 | for move in opponents_moves: 239 | if move.end_row == row and move.end_col == col: # square is under attack 240 | return True 241 | return False 242 | 243 | def getAllPossibleMoves(self): 244 | """ 245 | All moves without considering checks. 246 | """ 247 | moves = [] 248 | for row in range(len(self.board)): 249 | for col in range(len(self.board[row])): 250 | turn = self.board[row][col][0] 251 | if (turn == "w" and self.white_to_move) or (turn == "b" and not self.white_to_move): 252 | piece = self.board[row][col][1] 253 | self.moveFunctions[piece](row, col, moves) # calls appropriate move function based on piece type 254 | return moves 255 | 256 | def checkForPinsAndChecks(self): 257 | pins = [] # squares pinned and the direction its pinned from 258 | checks = [] # squares where enemy is applying a check 259 | in_check = False 260 | if self.white_to_move: 261 | enemy_color = "b" 262 | ally_color = "w" 263 | start_row = self.white_king_location[0] 264 | start_col = self.white_king_location[1] 265 | else: 266 | enemy_color = "w" 267 | ally_color = "b" 268 | start_row = self.black_king_location[0] 269 | start_col = self.black_king_location[1] 270 | # check outwards from king for pins and checks, keep track of pins 271 | directions = ((-1, 0), (0, -1), (1, 0), (0, 1), (-1, -1), (-1, 1), (1, -1), (1, 1)) 272 | for j in range(len(directions)): 273 | direction = directions[j] 274 | possible_pin = () # reset possible pins 275 | for i in range(1, 8): 276 | end_row = start_row + direction[0] * i 277 | end_col = start_col + direction[1] * i 278 | if 0 <= end_row <= 7 and 0 <= end_col <= 7: 279 | end_piece = self.board[end_row][end_col] 280 | if end_piece[0] == ally_color and end_piece[1] != "K": 281 | if possible_pin == (): # first allied piece could be pinned 282 | possible_pin = (end_row, end_col, direction[0], direction[1]) 283 | else: # 2nd allied piece - no check or pin from this direction 284 | break 285 | elif end_piece[0] == enemy_color: 286 | enemy_type = end_piece[1] 287 | 288 | 289 | # for better understanding of the code, refer to the comments in the next line 290 | 291 | # 5 possibilities in this complex conditional 292 | # 1.) orthogonally away from king and piece is a rook 293 | # 2.) diagonally away from king and piece is a bishop 294 | # 3.) 1 square away diagonally from king and piece is a pawn 295 | # 4.) any direction and piece is a queen 296 | # 5.) any direction 1 square away and piece is a king 297 | 298 | 299 | if (0 <= j <= 3 and enemy_type == "R") or (4 <= j <= 7 and enemy_type == "B") or ( 300 | i == 1 and enemy_type == "p" and ( 301 | (enemy_color == "w" and 6 <= j <= 7) or (enemy_color == "b" and 4 <= j <= 5))) or ( 302 | enemy_type == "Q") or (i == 1 and enemy_type == "K"): 303 | if possible_pin == (): # no piece blocking, so check 304 | in_check = True 305 | checks.append((end_row, end_col, direction[0], direction[1])) 306 | break 307 | else: # piece blocking so pin 308 | pins.append(possible_pin) 309 | break 310 | else: # enemy piece not applying checks 311 | break 312 | else: 313 | break # off board 314 | # check for knight checks 315 | knight_moves = ((-2, -1), (-2, 1), (-1, 2), (1, 2), (2, -1), (2, 1), (-1, -2), (1, -2)) 316 | for move in knight_moves: 317 | end_row = start_row + move[0] 318 | end_col = start_col + move[1] 319 | if 0 <= end_row <= 7 and 0 <= end_col <= 7: 320 | end_piece = self.board[end_row][end_col] 321 | if end_piece[0] == enemy_color and end_piece[1] == "N": # enemy knight attacking a king 322 | in_check = True 323 | checks.append((end_row, end_col, move[0], move[1])) 324 | return in_check, pins, checks 325 | 326 | def getPawnMoves(self, row, col, moves): 327 | """ 328 | Get all the pawn moves for the pawn located at row, col and add the moves to the list. 329 | """ 330 | piece_pinned = False 331 | pin_direction = () 332 | for i in range(len(self.pins) - 1, -1, -1): 333 | if self.pins[i][0] == row and self.pins[i][1] == col: 334 | piece_pinned = True 335 | pin_direction = (self.pins[i][2], self.pins[i][3]) 336 | self.pins.remove(self.pins[i]) 337 | break 338 | 339 | if self.white_to_move: 340 | move_amount = -1 341 | start_row = 6 342 | enemy_color = "b" 343 | king_row, king_col = self.white_king_location 344 | else: 345 | move_amount = 1 346 | start_row = 1 347 | enemy_color = "w" 348 | king_row, king_col = self.black_king_location 349 | 350 | if self.board[row + move_amount][col] == "--": # 1 square pawn advance 351 | if not piece_pinned or pin_direction == (move_amount, 0): 352 | moves.append(Move((row, col), (row + move_amount, col), self.board)) 353 | if row == start_row and self.board[row + 2 * move_amount][col] == "--": # 2 square pawn advance 354 | moves.append(Move((row, col), (row + 2 * move_amount, col), self.board)) 355 | if col - 1 >= 0: # capture to the left 356 | if not piece_pinned or pin_direction == (move_amount, -1): 357 | if self.board[row + move_amount][col - 1][0] == enemy_color: 358 | moves.append(Move((row, col), (row + move_amount, col - 1), self.board)) 359 | if (row + move_amount, col - 1) == self.enpassant_possible: 360 | attacking_piece = blocking_piece = False 361 | if king_row == row: 362 | if king_col < col: # king is left of the pawn 363 | # inside: between king and the pawn; 364 | # outside: between pawn and border; 365 | # inside and outside are used to check if there is a piece blocking the check or not 366 | inside_range = range(king_col + 1, col - 1) 367 | outside_range = range(col + 1, 8) 368 | else: # king right of the pawn 369 | inside_range = range(king_col - 1, col, -1) 370 | outside_range = range(col - 2, -1, -1) 371 | for i in inside_range: 372 | if self.board[row][i] != "--": # some piece beside en-passant pawn blocks 373 | blocking_piece = True 374 | for i in outside_range: 375 | square = self.board[row][i] 376 | if square[0] == enemy_color and (square[1] == "R" or square[1] == "Q"): 377 | attacking_piece = True 378 | elif square != "--": 379 | blocking_piece = True 380 | if not attacking_piece or blocking_piece: 381 | moves.append(Move((row, col), (row + move_amount, col - 1), self.board, is_enpassant_move=True)) 382 | if col + 1 <= 7: # capture to the right 383 | if not piece_pinned or pin_direction == (move_amount, +1): 384 | if self.board[row + move_amount][col + 1][0] == enemy_color: 385 | moves.append(Move((row, col), (row + move_amount, col + 1), self.board)) 386 | if (row + move_amount, col + 1) == self.enpassant_possible: 387 | attacking_piece = blocking_piece = False 388 | if king_row == row: 389 | if king_col < col: # king is left of the pawn 390 | # inside: between king and the pawn; 391 | # outside: between pawn and border; 392 | inside_range = range(king_col + 1, col) 393 | outside_range = range(col + 2, 8) 394 | else: # king right of the pawn 395 | inside_range = range(king_col - 1, col + 1, -1) 396 | outside_range = range(col - 1, -1, -1) 397 | for i in inside_range: 398 | if self.board[row][i] != "--": # some piece beside en-passant pawn blocks 399 | blocking_piece = True 400 | for i in outside_range: 401 | square = self.board[row][i] 402 | if square[0] == enemy_color and (square[1] == "R" or square[1] == "Q"): 403 | attacking_piece = True 404 | elif square != "--": 405 | blocking_piece = True 406 | if not attacking_piece or blocking_piece: 407 | moves.append(Move((row, col), (row + move_amount, col + 1), self.board, is_enpassant_move=True)) 408 | 409 | def getRookMoves(self, row, col, moves): 410 | """ 411 | Get all the rook moves for the rook located at row, col and add the moves to the list. 412 | """ 413 | piece_pinned = False 414 | pin_direction = () 415 | for i in range(len(self.pins) - 1, -1, -1): 416 | if self.pins[i][0] == row and self.pins[i][1] == col: 417 | piece_pinned = True 418 | pin_direction = (self.pins[i][2], self.pins[i][3]) 419 | if self.board[row][col][ 420 | 1] != "Q": # can't remove queen from pin on rook moves, only remove it on bishop moves 421 | self.pins.remove(self.pins[i]) 422 | break 423 | 424 | directions = ((-1, 0), (0, -1), (1, 0), (0, 1)) # up, left, down, right 425 | enemy_color = "b" if self.white_to_move else "w" 426 | for direction in directions: 427 | for i in range(1, 8): 428 | end_row = row + direction[0] * i 429 | end_col = col + direction[1] * i 430 | if 0 <= end_row <= 7 and 0 <= end_col <= 7: # check for possible moves only in boundaries of the board 431 | if not piece_pinned or pin_direction == direction or pin_direction == ( 432 | -direction[0], -direction[1]): 433 | end_piece = self.board[end_row][end_col] 434 | if end_piece == "--": # empty space is valid 435 | moves.append(Move((row, col), (end_row, end_col), self.board)) 436 | elif end_piece[0] == enemy_color: # capture enemy piece 437 | moves.append(Move((row, col), (end_row, end_col), self.board)) 438 | break 439 | else: # friendly piece 440 | break 441 | else: # off board 442 | break 443 | 444 | def getKnightMoves(self, row, col, moves): 445 | """ 446 | Get all the knight moves for the knight located at row col and add the moves to the list. 447 | """ 448 | piece_pinned = False 449 | for i in range(len(self.pins) - 1, -1, -1): 450 | if self.pins[i][0] == row and self.pins[i][1] == col: 451 | piece_pinned = True 452 | self.pins.remove(self.pins[i]) 453 | break 454 | 455 | knight_moves = ((-2, -1), (-2, 1), (-1, 2), (1, 2), (2, -1), (2, 1), (-1, -2), 456 | (1, -2)) # up/left up/right right/up right/down down/left down/right left/up left/down 457 | ally_color = "w" if self.white_to_move else "b" 458 | for move in knight_moves: 459 | end_row = row + move[0] 460 | end_col = col + move[1] 461 | if 0 <= end_row <= 7 and 0 <= end_col <= 7: 462 | if not piece_pinned: 463 | end_piece = self.board[end_row][end_col] 464 | if end_piece[0] != ally_color: # not an ally piece - empty or enemy 465 | moves.append(Move((row, col), (end_row, end_col), self.board)) 466 | 467 | def getBishopMoves(self, row, col, moves): 468 | """ 469 | Get all the bishop moves for the bishop located at row col and add the moves to the list. 470 | """ 471 | piece_pinned = False 472 | pin_direction = () 473 | for i in range(len(self.pins) - 1, -1, -1): 474 | if self.pins[i][0] == row and self.pins[i][1] == col: 475 | piece_pinned = True 476 | pin_direction = (self.pins[i][2], self.pins[i][3]) 477 | self.pins.remove(self.pins[i]) 478 | break 479 | 480 | directions = ((-1, -1), (-1, 1), (1, 1), (1, -1)) # diagonals: up/left up/right down/right down/left 481 | enemy_color = "b" if self.white_to_move else "w" 482 | for direction in directions: 483 | for i in range(1, 8): 484 | end_row = row + direction[0] * i 485 | end_col = col + direction[1] * i 486 | if 0 <= end_row <= 7 and 0 <= end_col <= 7: # check if the move is on board 487 | if not piece_pinned or pin_direction == direction or pin_direction == ( 488 | -direction[0], -direction[1]): 489 | end_piece = self.board[end_row][end_col] 490 | if end_piece == "--": # empty space is valid 491 | moves.append(Move((row, col), (end_row, end_col), self.board)) 492 | elif end_piece[0] == enemy_color: # capture enemy piece 493 | moves.append(Move((row, col), (end_row, end_col), self.board)) 494 | break 495 | else: # friendly piece 496 | break 497 | else: # off board 498 | break 499 | 500 | def getQueenMoves(self, row, col, moves): 501 | """ 502 | Get all the queen moves for the queen located at row col and add the moves to the list. 503 | """ 504 | self.getBishopMoves(row, col, moves) 505 | self.getRookMoves(row, col, moves) 506 | 507 | def getKingMoves(self, row, col, moves): 508 | """ 509 | Get all the king moves for the king located at row col and add the moves to the list. 510 | """ 511 | row_moves = (-1, -1, -1, 0, 0, 1, 1, 1) 512 | col_moves = (-1, 0, 1, -1, 1, -1, 0, 1) 513 | ally_color = "w" if self.white_to_move else "b" 514 | for i in range(8): 515 | end_row = row + row_moves[i] 516 | end_col = col + col_moves[i] 517 | if 0 <= end_row <= 7 and 0 <= end_col <= 7: 518 | end_piece = self.board[end_row][end_col] 519 | if end_piece[0] != ally_color: # not an ally piece - empty or enemy 520 | # place king on end square and check for checks 521 | if ally_color == "w": 522 | self.white_king_location = (end_row, end_col) 523 | else: 524 | self.black_king_location = (end_row, end_col) 525 | in_check, pins, checks = self.checkForPinsAndChecks() 526 | if not in_check: 527 | moves.append(Move((row, col), (end_row, end_col), self.board)) 528 | # place king back on original location 529 | if ally_color == "w": 530 | self.white_king_location = (row, col) 531 | else: 532 | self.black_king_location = (row, col) 533 | 534 | def getCastleMoves(self, row, col, moves): 535 | """ 536 | Generate all valid castle moves for the king at (row, col) and add them to the list of moves. 537 | """ 538 | if self.squareUnderAttack(row, col): 539 | return # can't castle while in check 540 | if (self.white_to_move and self.current_castling_rights.wks) or ( 541 | not self.white_to_move and self.current_castling_rights.bks): 542 | self.getKingsideCastleMoves(row, col, moves) 543 | if (self.white_to_move and self.current_castling_rights.wqs) or ( 544 | not self.white_to_move and self.current_castling_rights.bqs): 545 | self.getQueensideCastleMoves(row, col, moves) 546 | 547 | def getKingsideCastleMoves(self, row, col, moves): 548 | if self.board[row][col + 1] == '--' and self.board[row][col + 2] == '--': 549 | if not self.squareUnderAttack(row, col + 1) and not self.squareUnderAttack(row, col + 2): 550 | moves.append(Move((row, col), (row, col + 2), self.board, is_castle_move=True)) 551 | 552 | def getQueensideCastleMoves(self, row, col, moves): 553 | if self.board[row][col - 1] == '--' and self.board[row][col - 2] == '--' and self.board[row][col - 3] == '--': 554 | if not self.squareUnderAttack(row, col - 1) and not self.squareUnderAttack(row, col - 2): 555 | moves.append(Move((row, col), (row, col - 2), self.board, is_castle_move=True)) 556 | 557 | 558 | class CastleRights: 559 | def __init__(self, wks, bks, wqs, bqs): 560 | self.wks = wks 561 | self.bks = bks 562 | self.wqs = wqs 563 | self.bqs = bqs 564 | 565 | 566 | class Move: 567 | 568 | ranks_to_rows = {"1": 7, "2": 6, "3": 5, "4": 4, 569 | "5": 3, "6": 2, "7": 1, "8": 0} 570 | rows_to_ranks = {v: k for k, v in ranks_to_rows.items()} 571 | files_to_cols = {"a": 0, "b": 1, "c": 2, "d": 3, 572 | "e": 4, "f": 5, "g": 6, "h": 7} 573 | cols_to_files = {v: k for k, v in files_to_cols.items()} 574 | 575 | def __init__(self, start_square, end_square, board, is_enpassant_move=False, is_castle_move=False): 576 | self.start_row = start_square[0] 577 | self.start_col = start_square[1] 578 | self.end_row = end_square[0] 579 | self.end_col = end_square[1] 580 | self.piece_moved = board[self.start_row][self.start_col] 581 | self.piece_captured = board[self.end_row][self.end_col] 582 | # pawn promotion 583 | self.is_pawn_promotion = (self.piece_moved == "wp" and self.end_row == 0) or ( 584 | self.piece_moved == "bp" and self.end_row == 7) 585 | # en passant 586 | self.is_enpassant_move = is_enpassant_move 587 | if self.is_enpassant_move: 588 | self.piece_captured = "wp" if self.piece_moved == "bp" else "bp" 589 | # castle move 590 | self.is_castle_move = is_castle_move 591 | 592 | self.is_capture = self.piece_captured != "--" 593 | self.moveID = self.start_row * 1000 + self.start_col * 100 + self.end_row * 10 + self.end_col 594 | 595 | def __eq__(self, other): 596 | """ 597 | Overriding the equals method. 598 | """ 599 | if isinstance(other, Move): 600 | return self.moveID == other.moveID 601 | return False 602 | 603 | def getChessNotation(self): 604 | if self.is_pawn_promotion: 605 | return self.getRankFile(self.end_row, self.end_col) + "Q" 606 | if self.is_castle_move: 607 | if self.end_col == 1: 608 | return "0-0-0" 609 | else: 610 | return "0-0" 611 | if self.is_enpassant_move: 612 | return self.getRankFile(self.start_row, self.start_col)[0] + "x" + self.getRankFile(self.end_row, 613 | self.end_col) + " e.p." 614 | if self.piece_captured != "--": 615 | if self.piece_moved[1] == "p": 616 | return self.getRankFile(self.start_row, self.start_col)[0] + "x" + self.getRankFile(self.end_row, 617 | self.end_col) 618 | else: 619 | return self.piece_moved[1] + "x" + self.getRankFile(self.end_row, self.end_col) 620 | else: 621 | if self.piece_moved[1] == "p": 622 | return self.getRankFile(self.end_row, self.end_col) 623 | else: 624 | return self.piece_moved[1] + self.getRankFile(self.end_row, self.end_col) 625 | 626 | 627 | def getRankFile(self, row, col): 628 | return self.cols_to_files[col] + self.rows_to_ranks[row] 629 | 630 | def __str__(self): 631 | if self.is_castle_move: 632 | return "0-0" if self.end_col == 6 else "0-0-0" 633 | 634 | end_square = self.getRankFile(self.end_row, self.end_col) 635 | 636 | if self.piece_moved[1] == "p": 637 | if self.is_capture: 638 | return self.cols_to_files[self.start_col] + "x" + end_square 639 | else: 640 | return end_square + "Q" if self.is_pawn_promotion else end_square 641 | 642 | move_string = self.piece_moved[1] 643 | if self.is_capture: 644 | move_string += "x" 645 | return move_string + end_square 646 | --------------------------------------------------------------------------------