├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── __init__.py ├── app.py ├── board.py ├── chess_ai.py ├── game.py ├── heuristics.py ├── minimax.py ├── moves.py ├── moves_cache.json ├── multi_chess_ai.py ├── node.py ├── requirements.txt ├── runtime.txt ├── screenshots └── chessai.png ├── static ├── LICENSE.txt ├── css │ ├── chessboard-0.3.0.css │ └── chessboard-0.3.0.min.css ├── img │ └── chesspieces │ │ └── wikipedia │ │ ├── bB.png │ │ ├── bK.png │ │ ├── bN.png │ │ ├── bP.png │ │ ├── bQ.png │ │ ├── bR.png │ │ ├── wB.png │ │ ├── wK.png │ │ ├── wN.png │ │ ├── wP.png │ │ ├── wQ.png │ │ └── wR.png └── js │ ├── chessboard-0.3.0.js │ ├── chessboard-0.3.0.min.js │ └── script.js ├── templates └── index.html └── test_helpers.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | env/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | 89 | # Rope project settings 90 | .ropeproject 91 | 92 | *.prof 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 James Lim 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 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn app:app 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chess AI 2 | 3 | This project focuses on computer science concepts such as data structures and algorithms. Chessnut is the chess engine we are using for all the moves and chess logic. We are utilizing a tree to generate the possible chessboards 3 levels deep and depth first search, minimax, and alpha-beta pruning to find the best move based on the following heuristics: 4 | 5 | * material (total piece count for each player) 6 | * number of possible legal moves with emphasis on center squares 7 | * check/checkmate status 8 | * pawn structure 9 | 10 | Currently trying to implement multiprocessing as our recursive function uses a lot of computing power so calculating heuristics on board states more than 4 levels deep takes a lot of time. With a depth of 3 leves, our AI makes pretty good moves but also makes a lot of ill-advised ones as well. The AI's chess intelligence is estimated to be at a level 3 out of 9. 11 | 12 | ![Chess AI Terminal Screenshot](https://github.com/jameslim1021/Chess-AI/blob/master/screenshots/chessai.png) 13 | 14 | ## Getting Started 15 | 16 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. 17 | 18 | ### Prerequisities 19 | 20 | 1. [Python 2](https://www.python.org/downloads/) 21 | 2. [Virtualenv](https://virtualenv.pypa.io/en/stable/installation/) 22 | 3. [PyPy](http://pypy.org/download.html) (Python2.7 compatible) 23 | 24 | ### Installing 25 | 26 | After installing the prerequisites and cloning this repo, go into the repo and create a virtual env: 27 | 28 | ``` 29 | virtualenv env 30 | ``` 31 | 32 | Activate the env: 33 | 34 | ``` 35 | source env/bin/activate 36 | ``` 37 | 38 | Install the dependencies: 39 | 40 | ``` 41 | pip install -r requirements.txt 42 | ``` 43 | 44 | Run the game with: 45 | 46 | ``` 47 | python chess_ai.py 48 | ``` 49 | 50 | It is HIGHLY recommended that you run ```chess_ai.py``` with PyPy to greatly reduce computation time. 51 | 52 | ## Minimax Algorithm 53 | 54 | Borrowing from Wikipedia's concise definition, the [minimax algorithm](https://en.wikipedia.org/wiki/Minimax) is "a decision rule used ... for minimizing the possible loss for a worst case (maximum loss) scenario." With respect to chess, the player to act is the maximizer, whose move would be met with an adversarial response from the opponent (minimizer). The minimax algorithm assumes that the opponent is competent and would respond by minimizing the value (determined by some heuristic) of the maximizer. 55 | 56 | ![Minimax Diagram](https://upload.wikimedia.org/wikipedia/commons/thumb/6/6f/Minimax.svg/701px-Minimax.svg.png "Minimax Diagram") 57 | 58 | This simplified tree represents a game where each depth represents a player's turn. Starting at the bottom of the tree (the deeper into the tree, the further into the game), the leaf nodes' values are passed back up to determine the maximizer's current best move. In an actual chess game, the each depth would have many more branches with each branch representing a possible move by a chess piece. 59 | 60 | ## Alpha-Beta Pruning 61 | 62 | Because of the number of board states possible in chess (estimated to be [10^120](https://en.wikipedia.org/wiki/Shannon_number)), minimax can be improved with a layer of [alpha-beta pruning](https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning). By keeping track of alpha (the highest value guaranteed to the maximizer) and beta (the lowest value guaranteed to the minimizer), it is possible to avoid calculating the heuristics of certain board states that cannot improve the situation for the current player. 63 | 64 | ![Alpha-Beta Pruning Diagram](https://upload.wikimedia.org/wikipedia/commons/thumb/9/91/AB_pruning.svg/1212px-AB_pruning.svg.png "Alpha-Beta Pruning Diagram") 65 | 66 | The grayed-out leaf node with a heuristic of 5 is never explored because the maximizer, at that point, is guaranteed a 5 by going left and can do no better than 4 by going right. That is, if the value of the grayed-out leaf node is greater than 4, the minimizer would choose the 4. If it were less than 4, the minimizer would choose it instead. From the maximizer's perspective, there is no reason to investigate that leaf node. 67 | 68 | For more information on the history of chess, minimax, and alpha-beta pruning, check out Patrick Winston's [lecture](https://www.youtube.com/watch?v=STjW3eH0Cik). 69 | 70 | ## Heuristics 71 | 72 | There are many factors when calculating the heuristics of a chessboard. As we developed our heuristic formula to consider more factors, the computations required to calculate the best move increased exponentially. At the moment, the AI considers the following 4 aspects of a board in its heuristic function. 73 | 74 | ### Material 75 | The material heuristic compares the value of one's pieces with the opponent's pieces. It encourages the AI to capture pieces and make favorable trades. We used the standard values for each piece: 76 | * Pawn: 1 77 | * Bishop/Knight: 3 78 | * Rook: 5 79 | * Queen: 9 80 | 81 | ### Number of Moves 82 | This heuristic calculates the number of legal moves a player can make. It encourages the AI to develop its pieces to exert more control over the board. It prioritizes controlling the center where pieces will have more options to influence the game. For example, a queen near the center of the board can move in 8 directions and thus control more squares, whereas a queen on a corner square can only move in 3 directions. 83 | 84 | ### Pawn Structure 85 | The pawn structure heuristic gives a score based on the number of pawns supported by other pawns. It encourages the AI to develop its pawns in a way that allows pawns moving forward to be defended by other pawns from behind. 86 | 87 | ### Check Status 88 | This heuristic checks if a player is in check or checkmate status. It encourages the AI to make moves that would put the opponent in check while avoiding moves that would put itself in check. It also detects if a move would put opponent in checkmate, which would be prioritized over all other heuristics. 89 | 90 | ### Other Heuristics 91 | The heuristics we used don't come close to representing all the depth involved in chess. Creating a heuristic that would encourage the AI to employ more complex strategies and tactics is not only conceptually difficult. It is also computationally demanding. As we optimize our engine, we would like to tweak our heuristics to better match the complexities of chess. 92 | 93 | For more ideas about chess heuristics, check out this [article](https://www.quora.com/What-are-some-heuristics-for-quickly-evaluating-chess-positions). 94 | 95 | ## Authors 96 | 97 | * **Ian Jabour** - [l4nk332](https://github.com/l4nk332) 98 | * **James Lim** - [lamesjim](https://github.com/lamesjim) 99 | * **Dai Nguyen** - [dnguyen87](https://github.com/dnguyen87) 100 | 101 | ## License 102 | 103 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 104 | 105 | ## Acknowledgments 106 | 107 | * cgearheart's [Chessnut](https://github.com/cgearhart/Chessnut) 108 | * Aleks Kamko's [Alpha-Beta Pruning Practice](http://inst.eecs.berkeley.edu/~cs61b/fa14/ta-materials/apps/ab_tree_practice/) 109 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Chessnut is a simple chess board model written in Python. Chessnut is 3 | *not* a chess engine -- it has no AI to play games, and it has no GUI. It is 4 | a simple package that can import/export games in Forsyth-Edwards Notation 5 | (FEN), generate a list of legal moves for the current board position, 6 | intelligently validate & apply moves (including en passant, castling, etc.), 7 | and keep track of the game with a history of both moves and corresponding 8 | FEN representation. 9 | 10 | Chessnut is not written for speed, it is written for simplicity (there are 11 | only two real classes, and only about 200 lines of code). By adding a custom 12 | move evaluation function, Chessnut could be used as a chess engine. The 13 | simplicity of the model lends itself well to studying the construction of a 14 | chess engine without worrying about implementing a chess model, or to easily 15 | find the set of legal moves for either player on a particular chess board for 16 | use in conjunction with another chess application. 17 | 18 | To use Chessnut, import the Game class from the module: 19 | 20 | from Chessnut import Game 21 | 22 | chessgame = Game() # Initialize a game in the standard opening position 23 | 24 | chessgame.get_moves() # List of the 20 legal opening moves for white 25 | 26 | chessgame.apply_move('e2e4') # Advance the pawn from e2 to e4 27 | 28 | chessgame.apply_move('e2e4') # raise InvalidMove (no piece on e2) 29 | """ 30 | 31 | # import module functions and promote into the package namespace 32 | from game import Game 33 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask, render_template, request, redirect, url_for, send_from_directory, jsonify 3 | from chess_ai import Game_Engine 4 | 5 | app = Flask(__name__, static_url_path='/static') 6 | 7 | 8 | @app.route('/') 9 | def index(): 10 | return render_template("index.html") 11 | 12 | @app.route('/board', methods=['POST']) 13 | def send_board(): 14 | game_engine = Game_Engine(request.form['fen']) 15 | game = game_engine.game 16 | ai = game_engine.computer 17 | if game.status < 2: 18 | ai_algebraic_move = ai.ab_make_move(str(game)) 19 | game.apply_move(ai_algebraic_move) 20 | return jsonify(move = ai_algebraic_move, fen = str(game)) 21 | return "Game over" 22 | 23 | 24 | if __name__ == "__main__": 25 | port = int(os.environ.get('PORT')) or 5000 26 | app.run(port) 27 | -------------------------------------------------------------------------------- /board.py: -------------------------------------------------------------------------------- 1 | """ 2 | The board class manages the position of pieces, and conversion to and from 3 | Forsyth-Edwards Notation (FEN). This class is only used internally by the 4 | `Game` class. 5 | """ 6 | 7 | 8 | class Board(object): 9 | """ 10 | This class manages the position of all pieces in a chess game. The 11 | position is stored as a list of single-character strings. 12 | """ 13 | 14 | def __init__(self, position=' ' * 64): 15 | self._position = [] 16 | self.set_position(position) 17 | 18 | def __str__(self): 19 | """ 20 | Convert the piece placement array to a FEN string. 21 | """ 22 | pos = [] 23 | for idx, piece in enumerate(self._position): 24 | 25 | # add a '/' at the end of each row 26 | if idx > 0 and idx % 8 == 0: 27 | pos.append('/') 28 | 29 | # blank spaces must be converted to numbers in the final FEN 30 | if not piece.isspace(): 31 | pos.append(piece) 32 | elif pos and pos[-1].isdigit(): 33 | pos[-1] = str(int(pos[-1]) + 1) 34 | else: 35 | pos.append('1') 36 | return ''.join(pos) 37 | 38 | def set_position(self, position): 39 | """ 40 | Convert a FEN position string into a piece placement array. 41 | """ 42 | self._position = [] 43 | for char in position: 44 | if char == '/': # skip row separator character 45 | continue 46 | elif char.isdigit(): 47 | # replace numbers characters with that number of spaces 48 | self._position.extend([' '] * int(char)) 49 | else: 50 | self._position.append(char) 51 | 52 | def get_piece(self, index): 53 | """Get the piece at the given index in the position array.""" 54 | return self._position[index] 55 | 56 | def get_owner(self, index): 57 | """ 58 | Get the owner of the piece at the given index in the position array. 59 | """ 60 | piece = self.get_piece(index) 61 | if not piece.isspace(): 62 | return 'w' if piece.isupper() else 'b' 63 | return None 64 | 65 | def move_piece(self, start, end, piece): 66 | """ 67 | Move a piece by removing it from the starting position and adding it 68 | to the end position. If a different piece is provided, that piece will 69 | be placed at the end index instead. 70 | """ 71 | self._position[end] = piece 72 | self._position[start] = ' ' 73 | 74 | def find_piece(self, symbol): 75 | """ 76 | Find the index of the specified piece on the board, returns -1 if the 77 | piece is not on the board. 78 | """ 79 | return ''.join(self._position).find(symbol) 80 | -------------------------------------------------------------------------------- /chess_ai.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from subprocess import call 3 | from time import sleep 4 | from game import Game 5 | from test_helpers import heuristic_gen, get_successors 6 | from node import Node 7 | import heuristics 8 | import random 9 | import time 10 | import json 11 | 12 | # open JSON file to read cached oves 13 | with open("./moves_cache.json", "r") as f: 14 | try: 15 | cache_moves = json.load(f) 16 | # if the file is empty the ValueError will be thrown 17 | except ValueError: 18 | cache_moves = {'even': {}, 'odd': {}} 19 | 20 | even_moves = cache_moves['even'] 21 | odd_moves = cache_moves['odd'] 22 | 23 | # Magenta = '\033[95m' 24 | # Blue = '\033[94m' 25 | # Green = '\033[92m' 26 | # Yellow = '\033[93m' 27 | # Red = '\033[91m' 28 | # Clear = '\033[0m' 29 | # Bold = '\033[1m' 30 | # Underline = '\033[4m' 31 | 32 | class Game_Engine(): 33 | def __init__(self, board_state): 34 | self.game = Game(board_state) 35 | self.computer = AI(self.game, 5) 36 | 37 | def prompt_user(self): 38 | print("\033[94m\033[1m===================================================================") 39 | print ("\033[93m ______________ \n" 40 | " __ ____/__ /_____________________\n" 41 | " _ / __ __ \ _ \_ ___/_ ___/\n" 42 | " / /___ _ / / / __/(__ )_(__ ) \n" 43 | " \____/ /_/ /_/\___//____/ /____/ \n" 44 | " ") 45 | print("\033[94m===================================================================\033[0m\033[22m") 46 | print("\nWelcome! To play, enter a command, e.g. '\033[95me2e4\033[0m'. To quit, type '\033[91mff\033[0m'.") 47 | self.computer.print_board(str(self.game)) 48 | try: 49 | while self.game.status < 2: 50 | user_move = raw_input("\nMake a move: \033[95m") 51 | print("\033[0m") 52 | while user_move not in self.game.get_moves() and user_move != "ff": 53 | user_move = raw_input("Please enter a valid move: ") 54 | if user_move == "ff": 55 | print("You surrendered.") 56 | break; 57 | self.game.apply_move(user_move) 58 | captured = self.captured_pieces(str(self.game)) 59 | start_time = time.time() 60 | self.computer.print_board(str(self.game), captured) 61 | print("\nCalculating...\n") 62 | if self.game.status < 2: 63 | current_state = str(self.game) 64 | computer_move = self.computer.ab_make_move(current_state) 65 | PIECE_NAME = {'p': 'pawn', 'b': 'bishop', 'n': 'knight', 'r': 'rook', 'q': 'queen', 'k': 'king'} 66 | start = computer_move[:2] 67 | end = computer_move[2:4] 68 | piece = PIECE_NAME[self.game.board.get_piece(self.game.xy2i(computer_move[:2]))] 69 | captured_piece = self.game.board.get_piece(self.game.xy2i(computer_move[2:4])) 70 | if captured_piece != " ": 71 | captured_piece = PIECE_NAME[captured_piece.lower()] 72 | print("---------------------------------") 73 | print("Computer's \033[92m{piece}\033[0m at \033[92m{start}\033[0m captured \033[91m{captured_piece}\033[0m at \033[91m{end}\033[0m.").format(piece = piece, start = start, captured_piece = captured_piece, end = end) 74 | print("---------------------------------") 75 | else: 76 | print("---------------------------------") 77 | print("Computer moved \033[92m{piece}\033[0m at \033[92m{start}\033[0m to \033[92m{end}\033[0m.".format(piece = piece, start = start, end = end)) 78 | print("---------------------------------") 79 | print("\033[1mNodes visited:\033[0m \033[93m{}\033[0m".format(self.computer.node_count)) 80 | print("\033[1mNodes cached:\033[0m \033[93m{}\033[0m".format(len(self.computer.cache))) 81 | print("\033[1mNodes found in cache:\033[0m \033[93m{}\033[0m".format(self.computer.found_in_cache)) 82 | print("\033[1mElapsed time in sec:\033[0m \033[93m{time}\033[0m".format(time=time.time() - start_time)) 83 | self.game.apply_move(computer_move) 84 | captured = self.captured_pieces(str(self.game)) 85 | self.computer.print_board(str(self.game), captured) 86 | user_move = raw_input("Game over. Play again? y/n: ") 87 | if user_move.lower() == "y": 88 | self.game = Game() 89 | self.computer.game = self.game 90 | self.prompt_user() 91 | # cache moves into JSON file 92 | with open("./moves_cache.json", "w") as f: 93 | if self.computer.max_depth % 2 == 0: 94 | for key in self.computer.cache: 95 | cache_moves["even"][key] = self.computer.cache[key] 96 | json.dump(cache_moves, f) 97 | else: 98 | for key in self.computer.cache: 99 | cache_moves["odd"][key] = self.computer.cache[key] 100 | json.dump(cache_moves, f) 101 | except KeyboardInterrupt: 102 | with open("./moves_cache.json", "w") as f: 103 | if self.computer.max_depth % 2 == 0: 104 | for key in self.computer.cache: 105 | cache_moves["even"][key] = self.computer.cache[key] 106 | json.dump(cache_moves, f) 107 | else: 108 | for key in self.computer.cache: 109 | cache_moves["odd"][key] = self.computer.cache[key] 110 | json.dump(cache_moves, f) 111 | print("\nYou quitter!") 112 | 113 | # def write_to_cache(self): 114 | 115 | 116 | def captured_pieces(self, board_state): 117 | piece_tracker = {'P': 8, 'B': 2, 'N': 2, 'R': 2, 'Q': 1, 'K': 1, 'p': 8, 'b': 2, 'n': 2, 'r': 2, 'q': 1, 'k': 1} 118 | captured = { 119 | "w": [], 120 | "b": [] 121 | } 122 | for char in board_state.split()[0]: 123 | if char in piece_tracker: 124 | piece_tracker[char] -= 1 125 | for piece in piece_tracker: 126 | if piece_tracker[piece] > 0: 127 | if piece.isupper(): 128 | captured['w'] += piece_tracker[piece] * piece 129 | else: 130 | captured['b'] += piece_tracker[piece] * piece 131 | piece_tracker[piece] = 0 132 | return captured 133 | 134 | class AI(): 135 | def __init__(self, game, max_depth=4, leaf_nodes=[], node_count=0): 136 | self.max_depth = max_depth 137 | self.leaf_nodes = heuristic_gen(leaf_nodes) 138 | self.game = game 139 | self.node_count = node_count 140 | if self.max_depth % 2 == 0: 141 | self.cache = cache_moves['even'] 142 | else: 143 | self.cache = cache_moves['odd'] 144 | self.found_in_cache = 0 145 | 146 | def print_board(self, board_state, captured={"w": [], "b": []}): 147 | PIECE_SYMBOLS = {'P': '♟', 148 | 'B': '♝', 149 | 'N': '♞', 150 | 'R': '♜', 151 | 'Q': '♛', 152 | 'K': '♚', 153 | 'p': '\033[36m\033[1m♙\033[0m', 154 | 'b': '\033[36m\033[1m♗\033[0m', 155 | 'n': '\033[36m\033[1m♘\033[0m', 156 | 'r': '\033[36m\033[1m♖\033[0m', 157 | 'q': '\033[36m\033[1m♕\033[0m', 158 | 'k': '\033[36m\033[1m♔\033[0m'} 159 | board_state = board_state.split()[0].split("/") 160 | board_state_str = "\n" 161 | white_captured = " ".join(PIECE_SYMBOLS[piece] for piece in captured['w']) 162 | black_captured = " ".join(PIECE_SYMBOLS[piece] for piece in captured['b']) 163 | for i, row in enumerate(board_state): 164 | board_state_str += str(8-i) 165 | for char in row: 166 | if char.isdigit(): 167 | board_state_str += " ♢" * int(char) 168 | else: 169 | board_state_str += " " + PIECE_SYMBOLS[char] 170 | if i == 0: 171 | board_state_str += " Captured:" if len(white_captured) > 0 else "" 172 | if i == 1: 173 | board_state_str += " " + white_captured 174 | if i == 6: 175 | board_state_str += " Captured:" if len(black_captured) > 0 else "" 176 | if i == 7: 177 | board_state_str += " " + black_captured 178 | board_state_str += "\n" 179 | board_state_str += " A B C D E F G H" 180 | self.found_in_cache = 0 181 | self.node_count = 0 182 | print(board_state_str) 183 | 184 | def get_moves(self, board_state=None): 185 | if board_state == None: 186 | board_state = str(self.game) 187 | possible_moves = [] 188 | for move in Game(board_state).get_moves(): 189 | if (len(move) < 5 or move[4] == "q"): 190 | clone = Game(board_state) 191 | clone.apply_move(move) 192 | node = Node(str(clone)) 193 | node.algebraic_move = move 194 | possible_moves.append(node) 195 | return possible_moves 196 | 197 | def get_heuristic(self, board_state=None): 198 | cache_parse = board_state.split(" ")[0] + " " + board_state.split(" ")[1] 199 | if board_state == None: 200 | board_state = str(self.game) 201 | if cache_parse in self.cache: 202 | self.found_in_cache += 1 203 | return self.cache[cache_parse] 204 | clone = Game(board_state) 205 | total_points = 0 206 | # total piece count 207 | total_points += heuristics.material(board_state, 100) 208 | total_points += heuristics.piece_moves(clone, 50) 209 | total_points += heuristics.in_check(clone, 1) 210 | total_points += heuristics.pawn_structure(board_state, 1) 211 | self.cache[cache_parse] = total_points 212 | return total_points 213 | 214 | def minimax(self, node, current_depth=0): 215 | current_depth += 1 216 | if current_depth == self.max_depth: 217 | # get heuristic of each node 218 | node.value = self.get_heuristic(node.board_state) 219 | return node.value 220 | if current_depth % 2 == 0: 221 | # min player's turn 222 | self.is_turn = False 223 | return min([self.minimax(child_node, current_depth) for child_node in self.get_moves(node.board_state, self.is_turn)]) 224 | else: 225 | # max player's turn 226 | self.is_turn = True 227 | return max([self.minimax(child_node, current_depth) for child_node in self.get_moves(node.board_state, self.is_turn)]) 228 | 229 | def make_move(self, node): 230 | self.is_turn = True 231 | possible_moves = self.get_moves(node.board_state, self.is_turn) 232 | for move in possible_moves: 233 | move.value = self.minimax(move, 1) 234 | best_move = possible_moves[0] 235 | for move in possible_moves: 236 | if move.value > best_move.value: 237 | best_move = move 238 | # best_move at this point stores the move with the highest heuristic 239 | return best_move 240 | def ab_make_move(self, board_state): 241 | possible_moves = self.get_moves(board_state) 242 | alpha = float("-inf") 243 | beta = float("inf") 244 | best_move = possible_moves[0] 245 | for move in possible_moves: 246 | board_value = self.ab_minimax(move, alpha, beta, 1) 247 | if alpha < board_value: 248 | alpha = board_value 249 | best_move = move 250 | best_move.value = alpha 251 | # best_move at this point stores the move with the highest heuristic 252 | return best_move.algebraic_move 253 | 254 | def ab_minimax(self, node, alpha, beta, current_depth=0): 255 | current_depth += 1 256 | if current_depth == self.max_depth: 257 | board_value = self.get_heuristic(node.board_state) 258 | if current_depth % 2 == 0: 259 | # pick largest number, where root is black and even depth 260 | if (alpha < board_value): 261 | alpha = board_value 262 | self.node_count += 1 263 | return alpha 264 | else: 265 | # pick smallest number, where root is black and odd depth 266 | if (beta > board_value): 267 | beta = board_value 268 | self.node_count += 1 269 | return beta 270 | if current_depth % 2 == 0: 271 | # min player's turn 272 | for child_node in self.get_moves(node.board_state): 273 | if alpha < beta: 274 | board_value = self.ab_minimax(child_node,alpha, beta, current_depth) 275 | if beta > board_value: 276 | beta = board_value 277 | return beta 278 | else: 279 | # max player's turn 280 | for child_node in self.get_moves(node.board_state): 281 | if alpha < beta: 282 | board_value = self.ab_minimax(child_node,alpha, beta, current_depth) 283 | if alpha < board_value: 284 | alpha = board_value 285 | return alpha 286 | 287 | # if __name__ == "__main__": 288 | # import unittest 289 | # class Test_AI(unittest.TestCase): 290 | # # def test_minimax(self): 291 | # # data_set_1 = [8, 12, -13, 4, 1, 1, 20, 17, -5, 292 | # # -1, -15, -12, -11, -1, 1, 17, -3, 12, 293 | # # -7, 14, 9, 18, 4, -15, 8, 0, -6] 294 | # # first_test_AI = AI(4, 3, data_set_1) 295 | # # self.assertEqual(first_test_AI.minimax(Node()), 8, "Should return correct minimax when given b = 3 and d = 3") 296 | # # data_set_2 = [-4, -17, 6, 10, -6, -1, 16, 12, 297 | # # -12, 16, -18, -18, -20, -15, -18, -8, 298 | # # 8, 0, 11, -14, 11, -20, 8, -2, 299 | # # -17, -18, -11, 10, -8, -14, 7, -17] 300 | # # second_test_AI = AI(6, 2, data_set_2) 301 | # # self.assertEqual(second_test_AI.minimax(Node()), -8, "Should return correct minimax when given b = 2 and d = 5") 302 | # # data_set_3 = [-7, 14, -11, -16, -3, -19, 17, 0, 15, 303 | # # 5, -12, 18, -12, 17, 11, 12, 5, -4, 304 | # # 13, -12, 9, 0, 12, 12, -10, 1, -19, 305 | # # 20, 6, 13, 9, 14, 7, -3, 4, 11, 306 | # # -14, -10, -13, -18, 17, -6, 0, -8, -1, 307 | # # 3, 14, 6, -1, -7, 3, 8, 2, 10, 308 | # # 6, -19, 15, -4, -10, -1, -19, -2, 6, 309 | # # -4, 14, -3, -9, -20, 11, -18, 15, -1, 310 | # # -9, -10, 15, 0, 8, -4, -12, 4, -17] 311 | # # third_test_AI = AI(5, 3, data_set_3) 312 | # # self.assertEqual(third_test_AI.minimax(Node()), -4, "Should return correct minimax when given b = 3 and d = 4") 313 | # # 314 | # # def test_make_move(self): 315 | # # data_set_1 = [-4, -17, 6, 10, -6, -1, 16, 12, 316 | # # -12, 16, -18, -18, -20, -15, -18, -8, 317 | # # 8, 0, 11, -14, 11, -20, 8, -2, 318 | # # -17, -18, -11, 10, -8, -14, 7, -17] 319 | # # first_test_AI = AI(6, 2, data_set_1) 320 | # # self.assertEqual(first_test_AI.make_move(Node()).value, -8, "Should return best move given node w/ current board state") 321 | # # data_set_2 = [-7, 14, -11, -16, -3, -19, 17, 0, 15, 322 | # # 5, -12, 18, -12, 17, 11, 12, 5, -4, 323 | # # 13, -12, 9, 0, 12, 12, -10, 1, -19, 324 | # # 20, 6, 13, 9, 14, 7, -3, 4, 11, 325 | # # -14, -10, -13, -18, 17, -6, 0, -8, -1, 326 | # # 3, 14, 6, -1, -7, 3, 8, 2, 10, 327 | # # 6, -19, 15, -4, -10, -1, -19, -2, 6, 328 | # # -4, 14, -3, -9, -20, 11, -18, 15, -1, 329 | # # -9, -10, 15, 0, 8, -4, -12, 4, -17] 330 | # # second_test_AI = AI(5, 3, data_set_2) 331 | # # self.assertEqual(second_test_AI.make_move(Node()).value, -4, "Should return best move when many moves are possible") 332 | # # 333 | # # def test_ab(self): 334 | # # data_set_1_prune = [8, 12, -13, 4, 1, 1, 20, 335 | # # -1, -15, -12, 336 | # # -7, 14, 9, 18, 8, 0, -6] 337 | # # data_set_1_unprune = [-4, -17, 6, 10, -6, -1, 16, 12, 338 | # # -12, 16, -18, -18, -20, -15, -18, -8, 339 | # # 8, 0, 11, -14, 11, -20, 8, -2, 340 | # # -17, -18, -11, 10, -8, -14, 7, -17] 341 | # # first_prune_test_ab_AI = AI(4, 3, data_set_1_prune) 342 | # # first_unprune_test_ab_AI = AI(4, 3, data_set_1_unprune) 343 | # # self.assertEqual(first_prune_test_ab_AI.ab_make_move(Node()).value, 8, "Should return correct number with pruning when given b = 3 and d = 3") 344 | # # self.assertEqual(first_unprune_test_ab_AI.ab_make_move(Node()).value == 8, False, "Should fail for unpruned dataset") 345 | # 346 | # # def test_get_moves(self): 347 | # # new_game = Game() 348 | # # first_test_AI = AI(new_game, 4, 0) 349 | # # # White move 350 | # # self.assertEqual(len(first_test_AI.get_moves()), 20, "Should get all initial moves for white") 351 | # # current_turn = str(new_game).split(" ")[1] 352 | # # self.assertEqual(current_turn, "w", "Should start as white's turn") 353 | # # new_game.apply_move("a2a4") 354 | # # # Black move 355 | # # current_turn = str(new_game).split(" ")[1] 356 | # # self.assertEqual(current_turn, "b", "Should switch to black's turn") 357 | # # self.assertEqual(len(first_test_AI.get_moves()), 20, "Should get all initial moves for black") 358 | # # new_game.apply_move("b8a6") 359 | # # # White move 360 | # # current_turn = str(new_game).split(" ")[1] 361 | # # self.assertEqual(current_turn, "w", "Should start as white's turn") 362 | # # self.assertEqual(len(first_test_AI.get_moves()), 21, "Should get all moves for white 3rd turn") 363 | # def test_make_move(self): 364 | # new_game = Game() 365 | # first_test_AI = AI(new_game, 2, 0) 366 | # first_test_AI.print_board(str(new_game)) 367 | # new_game.apply_move("a2a3") 368 | # first_test_AI.print_board(str(new_game)) 369 | # new_game.apply_move(first_test_AI.ab_make_move(str(new_game))) 370 | # first_test_AI.print_board(str(new_game)) 371 | # 372 | # unittest.main() 373 | 374 | if __name__ == '__main__': 375 | new_test = Game_Engine('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1') 376 | new_test.prompt_user() 377 | -------------------------------------------------------------------------------- /game.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | The game module implements core Chessnut class, `Game`, to control a chess 4 | game. 5 | 6 | Two additional classes are defined: `InvalidMove` -- a subclass of the base 7 | `Exception` class, and `State` -- a namedtuple for handling game state 8 | information. 9 | 10 | Chessnut has neither an *engine*, nor a *GUI*, and it cannot currently 11 | handle any chess variants (e.g., Chess960) that are not equivalent to standard 12 | chess rules. 13 | """ 14 | 15 | from collections import namedtuple 16 | 17 | from board import Board 18 | from moves import MOVES 19 | 20 | # Define a named tuple with FEN field names to hold game state information 21 | State = namedtuple('State', ['player', 'rights', 'en_passant', 'ply', 'turn']) 22 | 23 | 24 | class InvalidMove(Exception): 25 | """ 26 | Subclass base `Exception` so that exception handling doesn't have to 27 | be generic. 28 | """ 29 | pass 30 | 31 | 32 | class Game(object): 33 | """ 34 | This class manages a chess game instance -- it stores an internal 35 | representation of the position of each piece on the board in an instance 36 | of the `Board` class, and the additional state information in an instance 37 | of the `State` namedtuple class. 38 | """ 39 | 40 | NORMAL = 0 41 | CHECK = 1 42 | CHECKMATE = 2 43 | STALEMATE = 3 44 | 45 | default_fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1' 46 | 47 | def __init__(self, fen=default_fen, validate=True): 48 | """ 49 | Initialize the game board to the supplied FEN state (or the default 50 | starting state if none is supplied), and determine whether to check 51 | the validity of moves returned by `get_moves()`. 52 | """ 53 | self.board = Board() 54 | self.state = State(' ', ' ', ' ', ' ', ' ') 55 | self.move_history = [] 56 | self.fen_history = [] 57 | self.validate = validate 58 | self.set_fen(fen=fen) 59 | 60 | def __str__(self): 61 | """Return the current FEN representation of the game.""" 62 | return ' '.join(str(x) for x in [self.board] + list(self.state)) 63 | 64 | @staticmethod 65 | def i2xy(pos_idx): 66 | """ 67 | Convert a board index to algebraic notation. 68 | """ 69 | return chr(97 + pos_idx % 8) + str(8 - pos_idx // 8) 70 | 71 | @staticmethod 72 | def xy2i(pos_xy): 73 | """ 74 | Convert algebraic notation to board index. 75 | """ 76 | return (8 - int(pos_xy[1])) * 8 + (ord(pos_xy[0]) - ord('a')) 77 | 78 | def get_fen(self): 79 | """ 80 | Get the latest FEN string of the current game. 81 | """ 82 | return ' '.join(str(x) for x in [self.board] + list(self.state)) 83 | 84 | def set_fen(self, fen): 85 | """ 86 | Parse a FEN string into components and store in the `board` and `state` 87 | properties, and append the FEN string to the game history *without* 88 | clearing it first. 89 | """ 90 | self.fen_history.append(fen) 91 | fields = fen.split(' ') 92 | fields[4] = int(fields[4]) 93 | fields[5] = int(fields[5]) 94 | self.state = State(*fields[1:]) 95 | self.board.set_position(fields[0]) 96 | 97 | def reset(self, fen=default_fen): 98 | """ 99 | Clear the game history and set the board to the default starting 100 | position. 101 | """ 102 | self.move_history = [] 103 | self.fen_history = [] 104 | self.set_fen(fen) 105 | 106 | # def _translate(self, move): 107 | # """ 108 | # Translate FEN castling notation to simple algebraic move notation. 109 | # """ 110 | # if move == 'O-O': 111 | # move = 'e1g1' if self.state.player == 'w' else 'e8g8' 112 | # elif move == 'O-O-O': 113 | # move = 'e1c1' if self.state.player == 'w' else 'e8c8' 114 | # return move 115 | 116 | def apply_move(self, move): 117 | """ 118 | Take a move in simple algebraic notation and apply it to the game. 119 | Note that simple algebraic notation differs from FEN move notation 120 | in that castling is not given any special notation, and pawn promotion 121 | piece is always lowercase. 122 | 123 | Update the state information (player, castling rights, en passant 124 | target, ply, and turn), apply the move to the game board, and 125 | update the game history. 126 | """ 127 | 128 | # declare the status fields using default parameters 129 | fields = ['w', 'KQkq', '-', 0, 1] 130 | # move = self._translate(move) 131 | 132 | # gracefully handle empty or incomplete moves 133 | if move is None or move == '' or len(move) < 4: 134 | raise InvalidMove("\nIllegal move: {}\nfen: {}".format(move, 135 | str(self))) 136 | 137 | # convert to lower case to avoid casing issues 138 | move = move.lower() 139 | 140 | start = Game.xy2i(move[:2]) 141 | end = Game.xy2i(move[2:4]) 142 | piece = self.board.get_piece(start) 143 | target = self.board.get_piece(end) 144 | 145 | if self.validate and move not in self.get_moves(idx_list=[start]): 146 | raise InvalidMove("\nIllegal move: {}\nfen: {}".format(move, 147 | str(self))) 148 | 149 | # toggle the active player 150 | fields[0] = {'w': 'b', 'b': 'w'}[self.state.player] 151 | 152 | # modify castling rights - the set of castling rights that *might* 153 | # be voided by a move is uniquely determined by the starting index 154 | # of the move - regardless of what piece moves from that position 155 | # (excluding chess variants like chess960). 156 | rights_map = {0: 'q', 4: 'kq', 7: 'k', 157 | 56: 'Q', 60: 'KQ', 63: 'K'} 158 | void_set = ''.join([rights_map.get(start, ''), 159 | rights_map.get(end, '')]) 160 | new_rights = [r for r in self.state.rights if r not in void_set] 161 | fields[1] = ''.join(new_rights) or '-' 162 | 163 | # set en passant target square when a pawn advances two spaces 164 | if piece.lower() == 'p' and abs(start - end) == 16: 165 | fields[2] = Game.i2xy((start + end) // 2) 166 | 167 | # reset the half move counter when a pawn moves or is captured 168 | fields[3] = self.state.ply + 1 169 | if piece.lower() == 'p' or target.lower() != ' ': 170 | fields[3] = 0 171 | 172 | # Increment the turn counter when the next move is from white, i.e., 173 | # the current player is black 174 | fields[4] = self.state.turn 175 | if self.state.player == 'b': 176 | fields[4] = self.state.turn + 1 177 | 178 | # check for pawn promotion 179 | if len(move) == 5: 180 | piece = move[4] 181 | if self.state.player == 'w': 182 | piece = piece.upper() 183 | 184 | # record the move in the game history and apply it to the board 185 | self.move_history.append(move) 186 | self.board.move_piece(start, end, piece) 187 | 188 | # move the rook to the other side of the king in case of castling 189 | c_type = {62: 'K', 58: 'Q', 6: 'k', 2: 'q'}.get(end, None) 190 | if piece.lower() == 'k' and c_type and c_type in self.state.rights: 191 | coords = {'K': (63, 61), 'Q': (56, 59), 192 | 'k': (7, 5), 'q': (0, 3)}[c_type] 193 | r_piece = self.board.get_piece(coords[0]) 194 | self.board.move_piece(coords[0], coords[1], r_piece) 195 | 196 | # in en passant remove the piece that is captured 197 | if piece.lower() == 'p' and self.state.en_passant != '-' \ 198 | and Game.xy2i(self.state.en_passant) == end: 199 | ep_tgt = Game.xy2i(self.state.en_passant) 200 | if ep_tgt < 24: 201 | self.board.move_piece(end + 8, end + 8, ' ') 202 | elif ep_tgt > 32: 203 | self.board.move_piece(end - 8, end - 8, ' ') 204 | 205 | # state update must happen after castling 206 | self.set_fen(' '.join(str(x) for x in [self.board] + list(fields))) 207 | 208 | def get_moves(self, player=None, idx_list=range(64)): 209 | """ 210 | Get a list containing the legal moves for pieces owned by the 211 | specified player that are located at positions included in the 212 | idx_list. By default, it compiles the list for the active player 213 | (i.e., self.state.player) by filtering the list of _all_moves() to 214 | eliminate any that would expose the player's king to check. 215 | """ 216 | if not self.validate: 217 | return self._all_moves(player=player, idx_list=idx_list) 218 | 219 | if not player: 220 | player = self.state.player 221 | 222 | res_moves = [] 223 | 224 | # This is the most inefficient part of the model - there is no cache 225 | # for previously computed move lists, so creating a new test board 226 | # each time and applying the moves incurs high overhead, and throws 227 | # away the result at the end of each pass through the loop 228 | test_board = Game(fen=str(self), validate=False) 229 | for move in self._all_moves(player=player, idx_list=idx_list): 230 | 231 | test_board.reset(fen=str(self)) 232 | 233 | # Don't allow castling out of or through the king in check 234 | k_sym, opp = {'w': ('K', 'b'), 'b': ('k', 'w')}.get(player) 235 | kdx = self.board.find_piece(k_sym) 236 | k_loc = Game.i2xy(kdx) 237 | dx = abs(kdx - Game.xy2i(move[2:4])) 238 | 239 | if move[0:2] == k_loc and dx == 2: 240 | 241 | op_moves = set([m[2:4] for m in test_board.get_moves(player=opp)]) 242 | castle_gap = {'e1g1': 'e1f1', 'e1c1': 'e1d1', 243 | 'e8g8': 'e8f8', 'e8c8': 'e8d8'}.get(move, '') 244 | 245 | # testing for castle gap in the move list depends on _all_moves() 246 | # returning moves in order radiating away from each piece, so that 247 | # king castling moves are always considered after verifying the king 248 | # can legally move to the gap cell. 249 | if (k_loc in op_moves or castle_gap and castle_gap not in res_moves): 250 | continue 251 | 252 | # Apply the move to the test board to ensure that the king does 253 | # not end up in check 254 | test_board.apply_move(move) 255 | tgts = set([m[2:4] for m in test_board.get_moves()]) 256 | 257 | if Game.i2xy(test_board.board.find_piece(k_sym)) not in tgts: 258 | res_moves.append(move) 259 | 260 | return res_moves 261 | 262 | def _all_moves(self, player=None, idx_list=range(64)): 263 | """ 264 | Get a list containing all reachable moves for pieces owned by the 265 | specified player (including moves that would expose the player's king 266 | to check) that are located at positions included in the idx_list. By 267 | default, it compiles the list for the active player (i.e., 268 | self.state.player) by checking every square on the board. 269 | """ 270 | player = player or self.state.player 271 | res_moves = [] 272 | for start in idx_list: 273 | if self.board.get_owner(start) != player: 274 | continue 275 | 276 | # MOVES contains the list of all possible moves for a piece of 277 | # the specified type on an empty chess board. 278 | piece = self.board.get_piece(start) 279 | rays = MOVES.get(piece, [''] * 64) 280 | 281 | for ray in rays[start]: 282 | # Trace each of the 8 (or fewer) possible directions that a 283 | # piece at the given starting index could move 284 | 285 | new_moves = self._trace_ray(start, piece, ray, player) 286 | res_moves.extend(new_moves) 287 | 288 | return res_moves 289 | 290 | def _trace_ray(self, start, piece, ray, player): 291 | """ 292 | Return a list of moves by filtering the supplied ray (a list of 293 | indices corresponding to end points that lie on a common line from 294 | the starting index) based on the state of the chess board (e.g., 295 | castling, capturing, en passant, etc.). Moves are in simple algebraic 296 | notation, e.g., 'a2a4', 'g7h8q', etc. 297 | 298 | Each ray should be an element from Chessnut.MOVES, representing all 299 | the moves that a piece could make from the starting square on an 300 | otherwise blank chessboard. This function filters the moves in a ray 301 | by enforcing the rules of chess for the legality of capturing pieces, 302 | castling, en passant, and pawn promotion. 303 | """ 304 | res_moves = [] 305 | 306 | for end in ray: 307 | 308 | sym = piece.lower() 309 | del_x = abs(end - start) % 8 310 | move = [Game.i2xy(start) + Game.i2xy(end)] 311 | tgt_owner = self.board.get_owner(end) 312 | 313 | # Abort if the current player owns the piece at the end point 314 | if tgt_owner == player: 315 | break 316 | 317 | # Test castling exception for king 318 | if sym == 'k' and del_x == 2: 319 | gap_owner = self.board.get_owner((start + end) // 2) 320 | out_owner = self.board.get_owner(end - 1) 321 | rights = {62: 'K', 58: 'Q', 6: 'k', 2: 'q'}.get(end, ' ') 322 | if (tgt_owner or gap_owner or rights not in self.state.rights or 323 | (rights.lower() == 'q' and out_owner)): 324 | # Abort castling because missing castling rights 325 | # or piece in the way 326 | break 327 | 328 | if sym == 'p': 329 | # Pawns cannot move forward to a non-empty square 330 | if del_x == 0 and tgt_owner: 331 | break 332 | 333 | # Test en passant exception for pawn 334 | elif del_x != 0 and not tgt_owner: 335 | ep_coords = self.state.en_passant 336 | if ep_coords == '-' or end != Game.xy2i(ep_coords): 337 | break 338 | 339 | # Pawn promotions should list all possible promotions 340 | if (end < 8 or end > 55): 341 | move = [move[0] + s for s in ['b', 'n', 'r', 'q']] 342 | 343 | res_moves.extend(move) 344 | 345 | # break after capturing an enemy piece 346 | if tgt_owner: 347 | break 348 | 349 | return res_moves 350 | 351 | @property 352 | def status(self): 353 | 354 | k_sym, opp = {'w': ('K', 'b'), 'b': ('k', 'w')}.get(self.state.player) 355 | k_loc = Game.i2xy(self.board.find_piece(k_sym)) 356 | can_move = len(self.get_moves()) 357 | is_exposed = [m[2:] for m in self._all_moves(player=opp) 358 | if m[2:] == k_loc] 359 | 360 | status = Game.NORMAL 361 | if is_exposed: 362 | status = Game.CHECK 363 | if not can_move: 364 | status = Game.CHECKMATE 365 | elif not can_move: 366 | status = Game.STALEMATE 367 | 368 | return status 369 | -------------------------------------------------------------------------------- /heuristics.py: -------------------------------------------------------------------------------- 1 | from game import Game 2 | 3 | def material(board_state, weight): 4 | black_points = 0 5 | board_state = board_state.split()[0] 6 | piece_values = {'p': 1, 'b': 3, 'n': 3, 'r': 5, 'q': 9, 'k': 0} 7 | for piece in board_state: 8 | if piece.islower(): 9 | black_points += piece_values[piece] 10 | elif piece.isupper(): 11 | black_points -= piece_values[piece.lower()] 12 | return black_points * weight 13 | 14 | def piece_moves(game, weight): 15 | black_points = 0 16 | turn = str(game).split()[1] 17 | square_values = {"e4": 1, "e5": 1, "d4": 1, "d5": 1, "c6": 0.5, "d6": 0.5, "e6": 0.5, "f6": 0.5, 18 | "c3": 0.5, "d3": 0.5, "e3": 0.5, "f3": 0.5, "c4": 0.5, "c5": 0.5, "f4": 0.5, "f5": 0.5} 19 | possible_moves = game.get_moves() 20 | for move in possible_moves: 21 | if turn == "b": 22 | if move[2:4] in square_values: 23 | black_points += square_values[move[2:4]] 24 | else: 25 | if move[2:4] in square_values: 26 | black_points -= square_values[move[2:4]] 27 | # piece_values = {'p': 1, 'b': 4, 'n': 4, 'r': 3, 'q': 3, 'k': 0} 28 | # for move in game.get_moves(): 29 | # current_piece = game.board.get_piece(game.xy2i(move[:2])) 30 | # if current_piece.islower(): 31 | # black_points += piece_values[current_piece] 32 | return black_points 33 | 34 | def pawn_structure(board_state, weight): 35 | black_points = 0 36 | board_state, current_player = [segment for segment in board_state.split()[:2]] 37 | board_state = board_state.split("/") 38 | 39 | # convert fen into matrix: 40 | board_state_arr = [] 41 | for row in board_state: 42 | row_arr = [] 43 | for char in row: 44 | if char.isdigit(): 45 | for i in range(int(char)): 46 | row_arr.append(" ") 47 | else: 48 | row_arr.append(char) 49 | board_state_arr.append(row_arr) 50 | 51 | # determine pawn to search for based on whose turn it is 52 | for i, row in enumerate(board_state_arr): 53 | for j in range(len(row)): 54 | if board_state_arr[i][j] == "p": 55 | tl = i-1, j-1 56 | tr = i-1, j+1 57 | if tl[0] >= 0 and tl[0] <= 7 and tl[1] >= 0 and tl[1] <= 7: 58 | if board_state_arr[tl[0]][tl[1]] == "p": 59 | black_points += 1 60 | if tr[0] >= 0 and tr[0] <= 7 and tr[1] >= 0 and tr[1] <= 7: 61 | if board_state_arr[tr[0]][tr[1]] == "p": 62 | black_points += 1 63 | return black_points * weight 64 | 65 | def in_check(game, weight): 66 | black_points = 0 67 | current_status = game.status 68 | # Turn should be 'w' or 'b' 69 | turn = str(game).split(" ")[1] 70 | # Check or Checkmate situations 71 | if turn == "w": 72 | if current_status == 1: 73 | black_points += 1 * weight 74 | elif current_status == 2: 75 | black_points += float("inf") 76 | else: 77 | if current_status == 1: 78 | black_points -= 1 * weight 79 | elif current_status == 2: 80 | black_points += float("-inf") 81 | return black_points 82 | 83 | # def center_squares(game, weight): 84 | # black_points = 0 85 | # # inner center squares - e4, e5, d4, d5 86 | # inner = [game.board.get_piece(game.xy2i("e4")), 87 | # game.board.get_piece(game.xy2i("e5")), 88 | # game.board.get_piece(game.xy2i("d4")), 89 | # game.board.get_piece(game.xy2i("d5"))] 90 | # for square in inner: 91 | # if square.islower(): 92 | # black_points += 3 93 | # # outer center squares - c3, d3, e3, f3, c6, d6, e6, f6, f4, f5, c4, c5 94 | # outer = [game.board.get_piece(game.xy2i("c3")), 95 | # game.board.get_piece(game.xy2i("d3")), 96 | # game.board.get_piece(game.xy2i("e3")), 97 | # game.board.get_piece(game.xy2i("f3")), 98 | # game.board.get_piece(game.xy2i("c6")), 99 | # game.board.get_piece(game.xy2i("d6")), 100 | # game.board.get_piece(game.xy2i("e6")), 101 | # game.board.get_piece(game.xy2i("f6")), 102 | # game.board.get_piece(game.xy2i("f4")), 103 | # game.board.get_piece(game.xy2i("f5")), 104 | # game.board.get_piece(game.xy2i("c4")), 105 | # game.board.get_piece(game.xy2i("c5"))] 106 | # for square in outer: 107 | # if square.islower(): 108 | # black_points += 1 109 | # return black_points * weight 110 | 111 | if __name__ == "__main__": 112 | import unittest 113 | class Test_material_heuristic(unittest.TestCase): 114 | def test_material_heuristic(self): 115 | player_points_1 = {'w': 0, 'b': 0} 116 | self.assertEqual(material('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', player_points_1, 1)['w'], 45, "Should return white player's total sum of piece values") 117 | player_points_2 = {'w': 0, 'b': 0} 118 | self.assertEqual(material('1nbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', player_points_2, 1)['b'], 40, "Should return black player's total sum of piece values minus one rook") 119 | 120 | def test_piece_moves_heuristics(self): 121 | player_points_1 = {'w': 0, 'b': 0} 122 | new_game = Game() 123 | self.assertEqual(piece_moves(new_game, player_points_1, 0.50)['w'], 16, "Should return white player's sum of total weighted legal moves") 124 | player_points_2 = {'w': 0, 'b': 0} 125 | new_game.apply_move("d2d3") 126 | new_game.apply_move("e7e6") 127 | self.assertEqual(piece_moves(new_game, player_points_2, 0.50)['w'], 29, "Should return white player's sum of total weighted legal moves after pawn moves d2d3") 128 | 129 | def test_in_check(self): 130 | player_points = {'w': 0, 'b': 0} 131 | # Initialized Board 132 | situation_a = Game() 133 | in_check(situation_a, player_points, 1) 134 | self.assertEqual(player_points['b'], 0, "Should not increment player_points when opponent not in check or checkmate") 135 | self.assertEqual(player_points['w'], 0, "Should not increment player_points when opponent not in check or checkmate") 136 | # Check situation 137 | situation_b = Game("rnbqkbnr/ppp2ppp/8/1B1pp3/3PP3/8/PPP2PPP/RNBQK1NR b KQkq - 1 3") 138 | in_check(situation_b, player_points, 1) 139 | self.assertEqual(player_points['b'], 0, "Should not increment player_points when opponent not in check or checkmate") 140 | self.assertEqual(player_points['w'], 1, "Should increment player_points when opponent is in check") 141 | 142 | # Checkmate situation 143 | situation_c = Game("r1bqkbnr/p1pppB1p/1pn2p2/6p1/8/1QP1P3/PP1P1PPP/RNB1K1NR b KQkq - 1 5") 144 | in_check(situation_c, player_points, 1) 145 | self.assertEqual(player_points['b'], 0, "Should not increment player_points when opponent not in check or checkmate") 146 | self.assertEqual(player_points['w'], float("inf"), "Should set player_points to infinity when opponent in checkmate") 147 | 148 | def test_center_squares(self): 149 | player_points = {'w': 0, 'b': 0} 150 | #Initialized board 151 | situation_a = Game() 152 | center_squares(situation_a, player_points, 1) 153 | self.assertEqual(player_points['b'], 0, "Should not have value since no piece is in any of the center squares") 154 | self.assertEqual(player_points['w'], 0, "Should not have value since no piece is in any of the center squares") 155 | situation_b = Game("r1bqkb1r/ppp1pppp/2n2n2/3p4/3PP3/2PQ4/PP3PPP/RNB1KBNR b KQkq e3 0 4") 156 | center_squares(situation_b, player_points, 1) 157 | self.assertEqual(player_points['b'], 5, "Should have points for 2 pieces in the outer square and 1 in the inner (5)") 158 | self.assertEqual(player_points['w'], 8, "Should have points for 2 pieces in the outer square and 2 in the inner (8)") 159 | 160 | def test_pawn_structure_heuristic(self): 161 | player_points_1 = {'w': 0, 'b': 0} 162 | situation_1 = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" 163 | self.assertEqual(pawn_structure(situation_1, player_points_1, 1)['w'], 0, "Should return value of white's pawn structure") 164 | player_points_2 = {'w': 0, 'b': 0} 165 | situation_2 = "rnbqkbnr/8/p1p1p1p1/1p1p1p1p/1P1P1P1P/P1P1P1P1/8/RNBQKBNR w KQkq - 0 9" 166 | self.assertEqual(pawn_structure(situation_2, player_points_2, 1)['w'], 7, "Should return value of white's pawn structure") 167 | player_points_3 = {'w': 0, 'b': 0} 168 | situation_3 = "rnbqkbnr/pppp4/4pp2/P3P1pp/1P1P4/2P5/5PPP/RNBQKBNR b KQkq - 0 7" 169 | pawn_structure(situation_3, player_points_3, 1) 170 | self.assertEqual(player_points_3['w'], 4, "Should return value of white's pawn structure") 171 | self.assertEqual(player_points_3['b'], 2, "Should return value of black's pawn structure") 172 | unittest.main() 173 | -------------------------------------------------------------------------------- /minimax.py: -------------------------------------------------------------------------------- 1 | from test_helpers import heuristic_gen, get_successors 2 | 3 | def minimax(board_state, heuristic, next_moves, current_depth=0, max_depth=4): 4 | current_depth += 1 5 | if current_depth == max_depth: 6 | # get heuristic of each node 7 | return next(heuristic) 8 | if current_depth % 2 == 0: 9 | # min player's turn 10 | return min([minimax(node, heuristic, current_depth, max_depth) for node in next_moves()]) 11 | else: 12 | # max player's turn 13 | return max([minimax(node, heuristic, current_depth, max_depth) for node in next_moves()]) 14 | 15 | if __name__ == "__main__": 16 | import unittest 17 | class Test_minimax(unittest.TestCase): 18 | def test_minimax(self): 19 | heuristics = heuristic_gen([8, 12, -13, 4, 1, 1, 20, 17, -5, 20 | -1, -15, -12, -11, -1, 1, 17, -3, 12, 21 | -7, 14, 9, 18, 4, -15, 8, 0, -6]) 22 | heuristics2 = heuristic_gen([-4, -17, 6, 10, -6, -1, 16, 12, -12, 23 | 16, -18, -18, -20, -15, -18, -8, 8, 0, 24 | 11, -14, 11, -20, 8, -2, -17, -18, 25 | -11, 10, -8, -14, 7, -17]) 26 | self.assertEqual(minimax(None, heuristics), 8, "Should return 8") 27 | self.assertEqual(minimax(None, heuristics2, 0, 6), -8, "Should return -8") 28 | unittest.main() 29 | -------------------------------------------------------------------------------- /moves.py: -------------------------------------------------------------------------------- 1 | """ 2 | Generates and returns a dictionary containing the superset of (i.e., all) 3 | legal moves on a chessboard. 4 | 5 | The structure returned is a dictionary using single-character symbols for keys 6 | (representing each type of chess piece, e.g., 'k', 'Q', 'N', 'r', etc. -- with 7 | lowercase letters for black pieces and uppercase for white), and whose values 8 | are 64-element lists. 9 | 10 | The list indices correspond to a raster-style index of a chessboard (i.e., 11 | 'a8'=0, 'h8'=8, 'a7'=8,...'h1'=63) representing the starting square of the 12 | piece. Each element of the list is another list that contains 8 or fewer 13 | elements that represent vectors for the 8 possible directions ("rays") that a 14 | chesspiece could move. Each vector is a list containing integers that 15 | represent the ending index of a legal move, sorted by increasing distance 16 | from the starting point. Empty vectors are removed from the list. 17 | 18 | For example: A queen on 'h8' (idx = 7) can move to the left (West) to each 19 | of the indices 0, 1, 2, 3, 4, 5, 6, and cannot move right (East), right/up 20 | (Northeast), up (North), up/left (Northwest), or down/right (Southeast) 21 | because the piece is at the corner of the board. Thus, 22 | 23 | len(MOVES['q'][7]) == 3 # West, Southwest, & South 24 | 25 | - and - 26 | 27 | MOVES['q'][7][0] = [6, 5, 4, 3, 2, 1, 0] # sorted by distance from idx = 7 28 | 29 | Which says that a black queen at 'h8' can move in a line to 'g8', 'f8',...'a8'. 30 | 31 | Generalizing: 32 | 33 | MOVES[][][] = [list of moves] 34 | 35 | This list of moves assumes that there are no other pieces on the board, so the 36 | actual set of legal moves for a particular board will be a subset of those 37 | returned here. Organizing the moves this way allows implicit validation when 38 | searching for legal moves on a particular board because any illegal move 39 | (e.g., blocked position) will short-circuit testing the remainder of the ray. 40 | It isn't a significant computational savings, but it simplifies the logic for 41 | determining legal moves. 42 | """ 43 | 44 | from math import atan2 45 | from copy import deepcopy 46 | 47 | 48 | # Precalculate angles for index pairs that form legal moves - straight lines 49 | # (up, down, left, right, diagonal) and the 8 directions a knight can move; 50 | # the index of the angle within this list will be used as the ray index to 51 | # group moves that lie in the same direction. 52 | DIRECTIONS = [(1, 0), (1, 1), (0, 1), (-1, 1), # straight lines 53 | (-1, 0), (-1, -1), (0, -1), (1, -1), 54 | (2, 1), (1, 2), (-1, 2), (-2, 1), # knights 55 | (-2, -1), (-1, -2), (1, -2), (2, -1), 56 | ] 57 | RAYS = [atan2(d[1], d[0]) for d in DIRECTIONS] 58 | 59 | 60 | # These keys are chess piece names, and the values are functions that verify 61 | # legality of moving in a particular direction for that type of piece. Symbols 62 | # for the white piece ('K', 'Q', 'N', 'B', and 'R') do not need to be 63 | # explicitly tested because their moves are the same as the black pieces for 64 | # all pieces *except* pawns, which differ because they are the only pieces 65 | # that cannot move backwards. 66 | PIECES = {'k': lambda y, dx, dy: abs(dx) <= 1 and abs(dy) <= 1, 67 | 'q': lambda y, dx, dy: dx == 0 or dy == 0 or abs(dx) == abs(dy), 68 | 'n': lambda y, dx, dy: (abs(dx) >= 1 and 69 | abs(dy) >= 1 and 70 | abs(dx) + abs(dy) == 3), 71 | 'b': lambda y, dx, dy: abs(dx) == abs(dy), 72 | 'r': lambda y, dx, dy: dx == 0 or dy == 0, 73 | 'p': lambda y, dx, dy: (y < 8 and abs(dx) <= 1 and dy == -1), 74 | 'P': lambda y, dx, dy: (y > 1 and abs(dx) <= 1 and dy == 1), 75 | } 76 | 77 | MOVES = dict() 78 | 79 | for sym, is_legal in PIECES.items(): 80 | 81 | MOVES[sym] = list() 82 | 83 | for idx in range(64): 84 | 85 | # Initialize arrays for each of the 8 possible directions that a 86 | # piece could be moved; some of these will be empty and 87 | # removed later 88 | MOVES[sym].append([list() for _ in range(8)]) 89 | 90 | # Sorting the list of end points by distance from the starting 91 | # point ensures that the ouptut order is properly sorted 92 | for end in sorted(range(64), key=lambda x: abs(x - idx)): 93 | 94 | # Determine the row, change in column, and change in row 95 | # of the start/end point pair for move validation 96 | y = 8 - idx // 8 97 | dx = (end % 8) - (idx % 8) 98 | dy = (8 - end // 8) - y 99 | 100 | if idx == end or not is_legal(y, dx, dy): 101 | continue 102 | 103 | angle = atan2(dy, dx) 104 | if angle in RAYS: 105 | 106 | # Mod by 8 to shift the ray index of knight moves down 107 | # by 8 from the index found in DIRECTIONS; the ray index of 108 | # all other pieces will be unchanged 109 | ray_num = RAYS.index(angle) % 8 110 | MOVES[sym][idx][ray_num].append(end) 111 | 112 | # Remove unused (empty) lists 113 | MOVES[sym][idx] = [r for r in MOVES[sym][idx] if r] 114 | 115 | # Create references to remaining pieces - the original set is only 116 | # minimally covering; Pawns are already included. 117 | for sym in ['K', 'Q', 'N', 'B', 'R']: 118 | MOVES[sym] = deepcopy(MOVES[sym.lower()]) 119 | 120 | # Directly add castling for kings 121 | MOVES['k'][4][0].append(6) 122 | MOVES['k'][4][1].append(2) 123 | MOVES['K'][60][0].append(62) 124 | MOVES['K'][60][4].append(58) 125 | 126 | # Directly add double-space pawn opening moves 127 | IDX = 0 128 | for i in range(8): 129 | MOVES['p'][8 + i][IDX].append(24 + i) 130 | MOVES['P'][55 - i][IDX].append(39 - i) 131 | IDX = 1 132 | -------------------------------------------------------------------------------- /multi_chess_ai.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from subprocess import call 3 | from time import sleep 4 | from game import Game 5 | from test_helpers import heuristic_gen, get_successors 6 | from node import Node 7 | import heuristics 8 | import random 9 | import time 10 | from multiprocessing import Pool 11 | import math 12 | 13 | cache = {} 14 | found_in_cache = 0 15 | 16 | class Test_Engine(): 17 | def __init__(self): 18 | self.game = Game() 19 | self.computer = AI(self.game, 4) 20 | 21 | def prompt_user(self): 22 | self.computer.print_board() 23 | while self.game.status < 2: 24 | user_move = raw_input("Make a move: ") 25 | while user_move not in self.game.get_moves() and user_move != "ff": 26 | user_move = raw_input("Please enter a valid move: ") 27 | if user_move == "ff": 28 | break; 29 | self.game.apply_move(user_move) 30 | start_time = time.time() 31 | self.computer.print_board() 32 | print("\nCalculating...\n") 33 | if self.game.status < 2: 34 | current_state = str(self.game) 35 | computer_move = self.computer.ab_make_move(current_state) 36 | PIECE_NAME = {'p': 'Pawn', 'b': 'Bishop', 'n': 'Knight', 'r': 'Rook', 'q': 'Queen', 'k': 'King'} 37 | print("Computer moved " + PIECE_NAME[self.game.board.get_piece(self.game.xy2i(computer_move[:2]))] + " at " + computer_move[:2] + " to " + computer_move[2:]) 38 | self.game.apply_move(computer_move) 39 | self.computer.print_board() 40 | print("Elapsed time in sec: {time}".format(time=time.time() - start_time)) 41 | user_move = raw_input("Game over. Play again? y/n: ") 42 | if user_move.lower() == "y": 43 | self.game = Game() 44 | self.computer.game = self.game 45 | self.prompt_user() 46 | 47 | class AI(): 48 | def __init__(self, game, max_depth=4, node_count=0): 49 | self.max_depth = max_depth 50 | self.game = game 51 | self.node_count = node_count 52 | 53 | def print_board(self, board_state=None): 54 | global cache 55 | global found_in_cache 56 | PIECE_SYMBOLS = {'P': '♟', 'B': '♝', 'N': '♞', 'R': '♜', 'Q': '♛', 'K': '♚', 'p': '♙', 'b': '♗', 'n': '♘', 'r': '♖', 'q': '♕', 'k': '♔'} 57 | if board_state == None: 58 | board_state = str(self.game) 59 | board_state = board_state.split()[0].split("/") 60 | board_state_str = "\n" 61 | for i, row in enumerate(board_state): 62 | board_state_str += str(8-i) 63 | for char in row: 64 | if char.isdigit(): 65 | board_state_str += " ♢" * int(char) 66 | else: 67 | board_state_str += " " + PIECE_SYMBOLS[char] 68 | board_state_str += "\n" 69 | board_state_str += " A B C D E F G H" 70 | 71 | print("Node Count: {}".format(self.node_count)) 72 | print("Cache size: {}".format(len(cache))) 73 | print("Found in Cache: {}".format(found_in_cache)) 74 | found_in_cache = 0 75 | self.node_count = 0 76 | print(board_state_str) 77 | 78 | def get_moves(self, board_state=None): 79 | if board_state == None: 80 | board_state = str(self.game) 81 | possible_moves = [] 82 | for move in Game(board_state).get_moves(): 83 | if (len(move) < 5 or move[4] == "q"): 84 | clone = Game(board_state) 85 | clone.apply_move(move) 86 | node = Node(str(clone)) 87 | node.algebraic_move = move 88 | possible_moves.append(node) 89 | return possible_moves 90 | 91 | def get_heuristic(self, board_state=None): 92 | global cache 93 | global found_in_cache 94 | 95 | cache_parse = board_state.split(" ")[0] + " " + board_state.split(" ")[1] 96 | if board_state == None: 97 | board_state = str(self.game) 98 | if cache_parse in cache: 99 | found_in_cache += 1 100 | return cache[cache_parse] 101 | clone = Game(board_state) 102 | total_points = 0 103 | # total piece count 104 | total_points += heuristics.material(board_state, 0.6) 105 | total_points += heuristics.piece_moves(clone, 0.15) 106 | total_points += heuristics.in_check(clone, 0.1) 107 | # total_points += heuristics.center_squares(clone, 0.2) 108 | total_points += heuristics.pawn_structure(board_state, 0.15) 109 | cache[cache_parse] = total_points 110 | return total_points 111 | 112 | # def minimax(self, node, current_depth=0): 113 | # current_depth += 1 114 | # if current_depth == self.max_depth: 115 | # # get heuristic of each node 116 | # node.value = self.get_heuristic(node.board_state) 117 | # return node.value 118 | # if current_depth % 2 == 0: 119 | # # min player's turn 120 | # self.is_turn = False 121 | # return min([self.minimax(child_node, current_depth) for child_node in self.get_moves(node.board_state, self.is_turn)]) 122 | # else: 123 | # # max player's turn 124 | # self.is_turn = True 125 | # return max([self.minimax(child_node, current_depth) for child_node in self.get_moves(node.board_state, self.is_turn)]) 126 | 127 | # def make_move(self, node): 128 | # self.is_turn = True 129 | # possible_moves = self.get_moves(node.board_state, self.is_turn) 130 | # for move in possible_moves: 131 | # move.value = self.minimax(move, 1) 132 | # best_move = possible_moves[0] 133 | # for move in possible_moves: 134 | # if move.value > best_move.value: 135 | # best_move = move 136 | # # best_move at this point stores the move with the highest heuristic 137 | # return best_move 138 | 139 | def ab_make_move(self, board_state): 140 | possible_moves = self.get_moves(board_state) 141 | # print possible_moves 142 | best_move = None 143 | 144 | p = Pool(4) 145 | result = p.map(map_partial, possible_moves, ) 146 | for node in result: 147 | if best_move == None: 148 | best_move = node 149 | if best_move.value < node.value: 150 | best_move = node 151 | p.close() 152 | p.join() 153 | return best_move.algebraic_move 154 | 155 | def ab_minimax(self, node, alpha=float("-inf"), beta=float("inf"), current_depth=1): 156 | current_depth += 1 157 | if current_depth == self.max_depth: 158 | board_value = self.get_heuristic(node.board_state) 159 | if current_depth % 2 == 0: 160 | # pick largest number, where root is black and even depth 161 | if (alpha < board_value): 162 | alpha = board_value 163 | self.node_count += 1 164 | return alpha 165 | else: 166 | # pick smallest number, where root is black and odd depth 167 | if (beta > board_value): 168 | beta = board_value 169 | self.node_count += 1 170 | return beta 171 | if current_depth % 2 == 0: 172 | # min player's turn 173 | for child_node in self.get_moves(node.board_state): 174 | if alpha < beta: 175 | board_value = self.ab_minimax(child_node,alpha, beta, current_depth) 176 | if beta > board_value: 177 | beta = board_value 178 | return beta 179 | else: 180 | # max player's turn 181 | for child_node in self.get_moves(node.board_state): 182 | if alpha < beta: 183 | board_value = self.ab_minimax(child_node,alpha, beta, current_depth) 184 | if alpha < board_value: 185 | alpha = board_value 186 | return alpha 187 | 188 | # if __name__ == "__main__": 189 | # import unittest 190 | # class Test_AI(unittest.TestCase): 191 | # # def test_minimax(self): 192 | # # data_set_1 = [8, 12, -13, 4, 1, 1, 20, 17, -5, 193 | # # -1, -15, -12, -11, -1, 1, 17, -3, 12, 194 | # # -7, 14, 9, 18, 4, -15, 8, 0, -6] 195 | # # first_test_AI = AI(4, 3, data_set_1) 196 | # # self.assertEqual(first_test_AI.minimax(Node()), 8, "Should return correct minimax when given b = 3 and d = 3") 197 | # # data_set_2 = [-4, -17, 6, 10, -6, -1, 16, 12, 198 | # # -12, 16, -18, -18, -20, -15, -18, -8, 199 | # # 8, 0, 11, -14, 11, -20, 8, -2, 200 | # # -17, -18, -11, 10, -8, -14, 7, -17] 201 | # # second_test_AI = AI(6, 2, data_set_2) 202 | # # self.assertEqual(second_test_AI.minimax(Node()), -8, "Should return correct minimax when given b = 2 and d = 5") 203 | # # data_set_3 = [-7, 14, -11, -16, -3, -19, 17, 0, 15, 204 | # # 5, -12, 18, -12, 17, 11, 12, 5, -4, 205 | # # 13, -12, 9, 0, 12, 12, -10, 1, -19, 206 | # # 20, 6, 13, 9, 14, 7, -3, 4, 11, 207 | # # -14, -10, -13, -18, 17, -6, 0, -8, -1, 208 | # # 3, 14, 6, -1, -7, 3, 8, 2, 10, 209 | # # 6, -19, 15, -4, -10, -1, -19, -2, 6, 210 | # # -4, 14, -3, -9, -20, 11, -18, 15, -1, 211 | # # -9, -10, 15, 0, 8, -4, -12, 4, -17] 212 | # # third_test_AI = AI(5, 3, data_set_3) 213 | # # self.assertEqual(third_test_AI.minimax(Node()), -4, "Should return correct minimax when given b = 3 and d = 4") 214 | # # 215 | # # def test_make_move(self): 216 | # # data_set_1 = [-4, -17, 6, 10, -6, -1, 16, 12, 217 | # # -12, 16, -18, -18, -20, -15, -18, -8, 218 | # # 8, 0, 11, -14, 11, -20, 8, -2, 219 | # # -17, -18, -11, 10, -8, -14, 7, -17] 220 | # # first_test_AI = AI(6, 2, data_set_1) 221 | # # self.assertEqual(first_test_AI.make_move(Node()).value, -8, "Should return best move given node w/ current board state") 222 | # # data_set_2 = [-7, 14, -11, -16, -3, -19, 17, 0, 15, 223 | # # 5, -12, 18, -12, 17, 11, 12, 5, -4, 224 | # # 13, -12, 9, 0, 12, 12, -10, 1, -19, 225 | # # 20, 6, 13, 9, 14, 7, -3, 4, 11, 226 | # # -14, -10, -13, -18, 17, -6, 0, -8, -1, 227 | # # 3, 14, 6, -1, -7, 3, 8, 2, 10, 228 | # # 6, -19, 15, -4, -10, -1, -19, -2, 6, 229 | # # -4, 14, -3, -9, -20, 11, -18, 15, -1, 230 | # # -9, -10, 15, 0, 8, -4, -12, 4, -17] 231 | # # second_test_AI = AI(5, 3, data_set_2) 232 | # # self.assertEqual(second_test_AI.make_move(Node()).value, -4, "Should return best move when many moves are possible") 233 | # # 234 | # # def test_ab(self): 235 | # # data_set_1_prune = [8, 12, -13, 4, 1, 1, 20, 236 | # # -1, -15, -12, 237 | # # -7, 14, 9, 18, 8, 0, -6] 238 | # # data_set_1_unprune = [-4, -17, 6, 10, -6, -1, 16, 12, 239 | # # -12, 16, -18, -18, -20, -15, -18, -8, 240 | # # 8, 0, 11, -14, 11, -20, 8, -2, 241 | # # -17, -18, -11, 10, -8, -14, 7, -17] 242 | # # first_prune_test_ab_AI = AI(4, 3, data_set_1_prune) 243 | # # first_unprune_test_ab_AI = AI(4, 3, data_set_1_unprune) 244 | # # self.assertEqual(first_prune_test_ab_AI.ab_make_move(Node()).value, 8, "Should return correct number with pruning when given b = 3 and d = 3") 245 | # # self.assertEqual(first_unprune_test_ab_AI.ab_make_move(Node()).value == 8, False, "Should fail for unpruned dataset") 246 | # 247 | # # def test_get_moves(self): 248 | # # new_game = Game() 249 | # # first_test_AI = AI(new_game, 4, 0) 250 | # # # White move 251 | # # self.assertEqual(len(first_test_AI.get_moves()), 20, "Should get all initial moves for white") 252 | # # current_turn = str(new_game).split(" ")[1] 253 | # # self.assertEqual(current_turn, "w", "Should start as white's turn") 254 | # # new_game.apply_move("a2a4") 255 | # # # Black move 256 | # # current_turn = str(new_game).split(" ")[1] 257 | # # self.assertEqual(current_turn, "b", "Should switch to black's turn") 258 | # # self.assertEqual(len(first_test_AI.get_moves()), 20, "Should get all initial moves for black") 259 | # # new_game.apply_move("b8a6") 260 | # # # White move 261 | # # current_turn = str(new_game).split(" ")[1] 262 | # # self.assertEqual(current_turn, "w", "Should start as white's turn") 263 | # # self.assertEqual(len(first_test_AI.get_moves()), 21, "Should get all moves for white 3rd turn") 264 | # def test_make_move(self): 265 | # new_game = Game() 266 | # first_test_AI = AI(new_game, 2, 0) 267 | # first_test_AI.print_board(str(new_game)) 268 | # new_game.apply_move("a2a3") 269 | # first_test_AI.print_board(str(new_game)) 270 | # new_game.apply_move(first_test_AI.ab_make_move(str(new_game))) 271 | # first_test_AI.print_board(str(new_game)) 272 | # 273 | # unittest.main() 274 | 275 | new_test = Test_Engine() 276 | 277 | def map_partial(move_node, AI=new_test.computer): 278 | move_node.value = AI.ab_minimax(move_node) 279 | return move_node 280 | 281 | new_test.prompt_user() 282 | -------------------------------------------------------------------------------- /node.py: -------------------------------------------------------------------------------- 1 | class Node(): 2 | def __init__(self, board_state=None, algebraic_move=None, value=None): 3 | self.board_state = board_state 4 | self.algebraic_move = algebraic_move 5 | self.value = value 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click==6.6 2 | Flask==0.11.1 3 | gunicorn==19.6.0 4 | itsdangerous==0.24 5 | Jinja2==2.8 6 | MarkupSafe==0.23 7 | Werkzeug==0.11.10 8 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | pypy-4.0.0 2 | -------------------------------------------------------------------------------- /screenshots/chessai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lamesjim/Chess-AI/1c734cb008ab98df02b08ae1bf48f222ae2662f2/screenshots/chessai.png -------------------------------------------------------------------------------- /static/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2013 Chris Oakman 2 | http://chessboardjs.com/ 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /static/css/chessboard-0.3.0.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * chessboard.js v0.3.0 3 | * 4 | * Copyright 2013 Chris Oakman 5 | * Released under the MIT license 6 | * https://github.com/oakmac/chessboardjs/blob/master/LICENSE 7 | * 8 | * Date: 10 Aug 2013 9 | */ 10 | 11 | /* clearfix */ 12 | .clearfix-7da63 { 13 | clear: both; 14 | } 15 | 16 | /* board */ 17 | .board-b72b1 { 18 | border: 2px solid #404040; 19 | -moz-box-sizing: content-box; 20 | box-sizing: content-box; 21 | } 22 | 23 | /* square */ 24 | .square-55d63 { 25 | float: left; 26 | position: relative; 27 | 28 | /* disable any native browser highlighting */ 29 | -webkit-touch-callout: none; 30 | -webkit-user-select: none; 31 | -khtml-user-select: none; 32 | -moz-user-select: none; 33 | -ms-user-select: none; 34 | user-select: none; 35 | } 36 | 37 | /* white square */ 38 | .white-1e1d7 { 39 | background-color: #f0d9b5; 40 | color: #b58863; 41 | } 42 | 43 | /* black square */ 44 | .black-3c85d { 45 | background-color: #b58863; 46 | color: #f0d9b5; 47 | } 48 | 49 | /* highlighted square */ 50 | .highlight1-32417, .highlight2-9c5d2 { 51 | -webkit-box-shadow: inset 0 0 3px 3px yellow; 52 | -moz-box-shadow: inset 0 0 3px 3px yellow; 53 | box-shadow: inset 0 0 3px 3px yellow; 54 | } 55 | 56 | /* notation */ 57 | .notation-322f9 { 58 | cursor: default; 59 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 60 | font-size: 14px; 61 | position: absolute; 62 | } 63 | .alpha-d2270 { 64 | bottom: 1px; 65 | right: 3px; 66 | } 67 | .numeric-fc462 { 68 | top: 2px; 69 | left: 2px; 70 | } -------------------------------------------------------------------------------- /static/css/chessboard-0.3.0.min.css: -------------------------------------------------------------------------------- 1 | /*! chessboard.js v0.3.0 | (c) 2013 Chris Oakman | MIT License chessboardjs.com/license */ 2 | .clearfix-7da63{clear:both}.board-b72b1{border:2px solid #404040;-moz-box-sizing:content-box;box-sizing:content-box}.square-55d63{float:left;position:relative;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.white-1e1d7{background-color:#f0d9b5;color:#b58863}.black-3c85d{background-color:#b58863;color:#f0d9b5}.highlight1-32417,.highlight2-9c5d2{-webkit-box-shadow:inset 0 0 3px 3px yellow;-moz-box-shadow:inset 0 0 3px 3px yellow;box-shadow:inset 0 0 3px 3px yellow}.notation-322f9{cursor:default;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;position:absolute}.alpha-d2270{bottom:1px;right:3px}.numeric-fc462{top:2px;left:2px} -------------------------------------------------------------------------------- /static/img/chesspieces/wikipedia/bB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lamesjim/Chess-AI/1c734cb008ab98df02b08ae1bf48f222ae2662f2/static/img/chesspieces/wikipedia/bB.png -------------------------------------------------------------------------------- /static/img/chesspieces/wikipedia/bK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lamesjim/Chess-AI/1c734cb008ab98df02b08ae1bf48f222ae2662f2/static/img/chesspieces/wikipedia/bK.png -------------------------------------------------------------------------------- /static/img/chesspieces/wikipedia/bN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lamesjim/Chess-AI/1c734cb008ab98df02b08ae1bf48f222ae2662f2/static/img/chesspieces/wikipedia/bN.png -------------------------------------------------------------------------------- /static/img/chesspieces/wikipedia/bP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lamesjim/Chess-AI/1c734cb008ab98df02b08ae1bf48f222ae2662f2/static/img/chesspieces/wikipedia/bP.png -------------------------------------------------------------------------------- /static/img/chesspieces/wikipedia/bQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lamesjim/Chess-AI/1c734cb008ab98df02b08ae1bf48f222ae2662f2/static/img/chesspieces/wikipedia/bQ.png -------------------------------------------------------------------------------- /static/img/chesspieces/wikipedia/bR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lamesjim/Chess-AI/1c734cb008ab98df02b08ae1bf48f222ae2662f2/static/img/chesspieces/wikipedia/bR.png -------------------------------------------------------------------------------- /static/img/chesspieces/wikipedia/wB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lamesjim/Chess-AI/1c734cb008ab98df02b08ae1bf48f222ae2662f2/static/img/chesspieces/wikipedia/wB.png -------------------------------------------------------------------------------- /static/img/chesspieces/wikipedia/wK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lamesjim/Chess-AI/1c734cb008ab98df02b08ae1bf48f222ae2662f2/static/img/chesspieces/wikipedia/wK.png -------------------------------------------------------------------------------- /static/img/chesspieces/wikipedia/wN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lamesjim/Chess-AI/1c734cb008ab98df02b08ae1bf48f222ae2662f2/static/img/chesspieces/wikipedia/wN.png -------------------------------------------------------------------------------- /static/img/chesspieces/wikipedia/wP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lamesjim/Chess-AI/1c734cb008ab98df02b08ae1bf48f222ae2662f2/static/img/chesspieces/wikipedia/wP.png -------------------------------------------------------------------------------- /static/img/chesspieces/wikipedia/wQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lamesjim/Chess-AI/1c734cb008ab98df02b08ae1bf48f222ae2662f2/static/img/chesspieces/wikipedia/wQ.png -------------------------------------------------------------------------------- /static/img/chesspieces/wikipedia/wR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lamesjim/Chess-AI/1c734cb008ab98df02b08ae1bf48f222ae2662f2/static/img/chesspieces/wikipedia/wR.png -------------------------------------------------------------------------------- /static/js/chessboard-0.3.0.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * chessboard.js v0.3.0 3 | * 4 | * Copyright 2013 Chris Oakman 5 | * Released under the MIT license 6 | * http://chessboardjs.com/license 7 | * 8 | * Date: 10 Aug 2013 9 | */ 10 | 11 | // start anonymous scope 12 | ;(function() { 13 | 'use strict'; 14 | 15 | //------------------------------------------------------------------------------ 16 | // Chess Util Functions 17 | //------------------------------------------------------------------------------ 18 | var COLUMNS = 'abcdefgh'.split(''); 19 | 20 | function validMove(move) { 21 | // move should be a string 22 | if (typeof move !== 'string') return false; 23 | 24 | // move should be in the form of "e2-e4", "f6-d5" 25 | var tmp = move.split('-'); 26 | if (tmp.length !== 2) return false; 27 | 28 | return (validSquare(tmp[0]) === true && validSquare(tmp[1]) === true); 29 | } 30 | 31 | function validSquare(square) { 32 | if (typeof square !== 'string') return false; 33 | return (square.search(/^[a-h][1-8]$/) !== -1); 34 | } 35 | 36 | function validPieceCode(code) { 37 | if (typeof code !== 'string') return false; 38 | return (code.search(/^[bw][KQRNBP]$/) !== -1); 39 | } 40 | 41 | // TODO: this whole function could probably be replaced with a single regex 42 | function validFen(fen) { 43 | if (typeof fen !== 'string') return false; 44 | 45 | // cut off any move, castling, etc info from the end 46 | // we're only interested in position information 47 | fen = fen.replace(/ .+$/, ''); 48 | 49 | // FEN should be 8 sections separated by slashes 50 | var chunks = fen.split('/'); 51 | if (chunks.length !== 8) return false; 52 | 53 | // check the piece sections 54 | for (var i = 0; i < 8; i++) { 55 | if (chunks[i] === '' || 56 | chunks[i].length > 8 || 57 | chunks[i].search(/[^kqrbnpKQRNBP1-8]/) !== -1) { 58 | return false; 59 | } 60 | } 61 | 62 | return true; 63 | } 64 | 65 | function validPositionObject(pos) { 66 | if (typeof pos !== 'object') return false; 67 | 68 | for (var i in pos) { 69 | if (pos.hasOwnProperty(i) !== true) continue; 70 | 71 | if (validSquare(i) !== true || validPieceCode(pos[i]) !== true) { 72 | return false; 73 | } 74 | } 75 | 76 | return true; 77 | } 78 | 79 | // convert FEN piece code to bP, wK, etc 80 | function fenToPieceCode(piece) { 81 | // black piece 82 | if (piece.toLowerCase() === piece) { 83 | return 'b' + piece.toUpperCase(); 84 | } 85 | 86 | // white piece 87 | return 'w' + piece.toUpperCase(); 88 | } 89 | 90 | // convert bP, wK, etc code to FEN structure 91 | function pieceCodeToFen(piece) { 92 | var tmp = piece.split(''); 93 | 94 | // white piece 95 | if (tmp[0] === 'w') { 96 | return tmp[1].toUpperCase(); 97 | } 98 | 99 | // black piece 100 | return tmp[1].toLowerCase(); 101 | } 102 | 103 | // convert FEN string to position object 104 | // returns false if the FEN string is invalid 105 | function fenToObj(fen) { 106 | if (validFen(fen) !== true) { 107 | return false; 108 | } 109 | 110 | // cut off any move, castling, etc info from the end 111 | // we're only interested in position information 112 | fen = fen.replace(/ .+$/, ''); 113 | 114 | var rows = fen.split('/'); 115 | var position = {}; 116 | 117 | var currentRow = 8; 118 | for (var i = 0; i < 8; i++) { 119 | var row = rows[i].split(''); 120 | var colIndex = 0; 121 | 122 | // loop through each character in the FEN section 123 | for (var j = 0; j < row.length; j++) { 124 | // number / empty squares 125 | if (row[j].search(/[1-8]/) !== -1) { 126 | var emptySquares = parseInt(row[j], 10); 127 | colIndex += emptySquares; 128 | } 129 | // piece 130 | else { 131 | var square = COLUMNS[colIndex] + currentRow; 132 | position[square] = fenToPieceCode(row[j]); 133 | colIndex++; 134 | } 135 | } 136 | 137 | currentRow--; 138 | } 139 | 140 | return position; 141 | } 142 | 143 | // position object to FEN string 144 | // returns false if the obj is not a valid position object 145 | function objToFen(obj) { 146 | if (validPositionObject(obj) !== true) { 147 | return false; 148 | } 149 | 150 | var fen = ''; 151 | 152 | var currentRow = 8; 153 | for (var i = 0; i < 8; i++) { 154 | for (var j = 0; j < 8; j++) { 155 | var square = COLUMNS[j] + currentRow; 156 | 157 | // piece exists 158 | if (obj.hasOwnProperty(square) === true) { 159 | fen += pieceCodeToFen(obj[square]); 160 | } 161 | 162 | // empty space 163 | else { 164 | fen += '1'; 165 | } 166 | } 167 | 168 | if (i !== 7) { 169 | fen += '/'; 170 | } 171 | 172 | currentRow--; 173 | } 174 | 175 | // squeeze the numbers together 176 | // haha, I love this solution... 177 | fen = fen.replace(/11111111/g, '8'); 178 | fen = fen.replace(/1111111/g, '7'); 179 | fen = fen.replace(/111111/g, '6'); 180 | fen = fen.replace(/11111/g, '5'); 181 | fen = fen.replace(/1111/g, '4'); 182 | fen = fen.replace(/111/g, '3'); 183 | fen = fen.replace(/11/g, '2'); 184 | 185 | return fen; 186 | } 187 | 188 | window['ChessBoard'] = window['ChessBoard'] || function(containerElOrId, cfg) { 189 | 'use strict'; 190 | 191 | cfg = cfg || {}; 192 | 193 | //------------------------------------------------------------------------------ 194 | // Constants 195 | //------------------------------------------------------------------------------ 196 | 197 | var MINIMUM_JQUERY_VERSION = '1.7.0', 198 | START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', 199 | START_POSITION = fenToObj(START_FEN); 200 | 201 | // use unique class names to prevent clashing with anything else on the page 202 | // and simplify selectors 203 | var CSS = { 204 | alpha: 'alpha-d2270', 205 | black: 'black-3c85d', 206 | board: 'board-b72b1', 207 | chessboard: 'chessboard-63f37', 208 | clearfix: 'clearfix-7da63', 209 | highlight1: 'highlight1-32417', 210 | highlight2: 'highlight2-9c5d2', 211 | notation: 'notation-322f9', 212 | numeric: 'numeric-fc462', 213 | piece: 'piece-417db', 214 | row: 'row-5277c', 215 | sparePieces: 'spare-pieces-7492f', 216 | sparePiecesBottom: 'spare-pieces-bottom-ae20f', 217 | sparePiecesTop: 'spare-pieces-top-4028b', 218 | square: 'square-55d63', 219 | white: 'white-1e1d7' 220 | }; 221 | 222 | //------------------------------------------------------------------------------ 223 | // Module Scope Variables 224 | //------------------------------------------------------------------------------ 225 | 226 | // DOM elements 227 | var containerEl, 228 | boardEl, 229 | draggedPieceEl, 230 | sparePiecesTopEl, 231 | sparePiecesBottomEl; 232 | 233 | // constructor return object 234 | var widget = {}; 235 | 236 | //------------------------------------------------------------------------------ 237 | // Stateful 238 | //------------------------------------------------------------------------------ 239 | 240 | var ANIMATION_HAPPENING = false, 241 | BOARD_BORDER_SIZE = 2, 242 | CURRENT_ORIENTATION = 'white', 243 | CURRENT_POSITION = {}, 244 | SQUARE_SIZE, 245 | DRAGGED_PIECE, 246 | DRAGGED_PIECE_LOCATION, 247 | DRAGGED_PIECE_SOURCE, 248 | DRAGGING_A_PIECE = false, 249 | SPARE_PIECE_ELS_IDS = {}, 250 | SQUARE_ELS_IDS = {}, 251 | SQUARE_ELS_OFFSETS; 252 | 253 | //------------------------------------------------------------------------------ 254 | // JS Util Functions 255 | //------------------------------------------------------------------------------ 256 | 257 | // http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript 258 | function createId() { 259 | return 'xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx'.replace(/x/g, function(c) { 260 | var r = Math.random() * 16 | 0; 261 | return r.toString(16); 262 | }); 263 | } 264 | 265 | function deepCopy(thing) { 266 | return JSON.parse(JSON.stringify(thing)); 267 | } 268 | 269 | function parseSemVer(version) { 270 | var tmp = version.split('.'); 271 | return { 272 | major: parseInt(tmp[0], 10), 273 | minor: parseInt(tmp[1], 10), 274 | patch: parseInt(tmp[2], 10) 275 | }; 276 | } 277 | 278 | // returns true if version is >= minimum 279 | function compareSemVer(version, minimum) { 280 | version = parseSemVer(version); 281 | minimum = parseSemVer(minimum); 282 | 283 | var versionNum = (version.major * 10000 * 10000) + 284 | (version.minor * 10000) + version.patch; 285 | var minimumNum = (minimum.major * 10000 * 10000) + 286 | (minimum.minor * 10000) + minimum.patch; 287 | 288 | return (versionNum >= minimumNum); 289 | } 290 | 291 | //------------------------------------------------------------------------------ 292 | // Validation / Errors 293 | //------------------------------------------------------------------------------ 294 | 295 | function error(code, msg, obj) { 296 | // do nothing if showErrors is not set 297 | if (cfg.hasOwnProperty('showErrors') !== true || 298 | cfg.showErrors === false) { 299 | return; 300 | } 301 | 302 | var errorText = 'ChessBoard Error ' + code + ': ' + msg; 303 | 304 | // print to console 305 | if (cfg.showErrors === 'console' && 306 | typeof console === 'object' && 307 | typeof console.log === 'function') { 308 | console.log(errorText); 309 | if (arguments.length >= 2) { 310 | console.log(obj); 311 | } 312 | return; 313 | } 314 | 315 | // alert errors 316 | if (cfg.showErrors === 'alert') { 317 | if (obj) { 318 | errorText += '\n\n' + JSON.stringify(obj); 319 | } 320 | window.alert(errorText); 321 | return; 322 | } 323 | 324 | // custom function 325 | if (typeof cfg.showErrors === 'function') { 326 | cfg.showErrors(code, msg, obj); 327 | } 328 | } 329 | 330 | // check dependencies 331 | function checkDeps() { 332 | // if containerId is a string, it must be the ID of a DOM node 333 | if (typeof containerElOrId === 'string') { 334 | // cannot be empty 335 | if (containerElOrId === '') { 336 | window.alert('ChessBoard Error 1001: ' + 337 | 'The first argument to ChessBoard() cannot be an empty string.' + 338 | '\n\nExiting...'); 339 | return false; 340 | } 341 | 342 | // make sure the container element exists in the DOM 343 | var el = document.getElementById(containerElOrId); 344 | if (! el) { 345 | window.alert('ChessBoard Error 1002: Element with id "' + 346 | containerElOrId + '" does not exist in the DOM.' + 347 | '\n\nExiting...'); 348 | return false; 349 | } 350 | 351 | // set the containerEl 352 | containerEl = $(el); 353 | } 354 | 355 | // else it must be something that becomes a jQuery collection 356 | // with size 1 357 | // ie: a single DOM node or jQuery object 358 | else { 359 | containerEl = $(containerElOrId); 360 | 361 | if (containerEl.length !== 1) { 362 | window.alert('ChessBoard Error 1003: The first argument to ' + 363 | 'ChessBoard() must be an ID or a single DOM node.' + 364 | '\n\nExiting...'); 365 | return false; 366 | } 367 | } 368 | 369 | // JSON must exist 370 | if (! window.JSON || 371 | typeof JSON.stringify !== 'function' || 372 | typeof JSON.parse !== 'function') { 373 | window.alert('ChessBoard Error 1004: JSON does not exist. ' + 374 | 'Please include a JSON polyfill.\n\nExiting...'); 375 | return false; 376 | } 377 | 378 | // check for a compatible version of jQuery 379 | if (! (typeof window.$ && $.fn && $.fn.jquery && 380 | compareSemVer($.fn.jquery, MINIMUM_JQUERY_VERSION) === true)) { 381 | window.alert('ChessBoard Error 1005: Unable to find a valid version ' + 382 | 'of jQuery. Please include jQuery ' + MINIMUM_JQUERY_VERSION + ' or ' + 383 | 'higher on the page.\n\nExiting...'); 384 | return false; 385 | } 386 | 387 | return true; 388 | } 389 | 390 | function validAnimationSpeed(speed) { 391 | if (speed === 'fast' || speed === 'slow') { 392 | return true; 393 | } 394 | 395 | if ((parseInt(speed, 10) + '') !== (speed + '')) { 396 | return false; 397 | } 398 | 399 | return (speed >= 0); 400 | } 401 | 402 | // validate config / set default options 403 | function expandConfig() { 404 | if (typeof cfg === 'string' || validPositionObject(cfg) === true) { 405 | cfg = { 406 | position: cfg 407 | }; 408 | } 409 | 410 | // default for orientation is white 411 | if (cfg.orientation !== 'black') { 412 | cfg.orientation = 'white'; 413 | } 414 | CURRENT_ORIENTATION = cfg.orientation; 415 | 416 | // default for showNotation is true 417 | if (cfg.showNotation !== false) { 418 | cfg.showNotation = true; 419 | } 420 | 421 | // default for draggable is false 422 | if (cfg.draggable !== true) { 423 | cfg.draggable = false; 424 | } 425 | 426 | // default for dropOffBoard is 'snapback' 427 | if (cfg.dropOffBoard !== 'trash') { 428 | cfg.dropOffBoard = 'snapback'; 429 | } 430 | 431 | // default for sparePieces is false 432 | if (cfg.sparePieces !== true) { 433 | cfg.sparePieces = false; 434 | } 435 | 436 | // draggable must be true if sparePieces is enabled 437 | if (cfg.sparePieces === true) { 438 | cfg.draggable = true; 439 | } 440 | 441 | // default piece theme is wikipedia 442 | if (cfg.hasOwnProperty('pieceTheme') !== true || 443 | (typeof cfg.pieceTheme !== 'string' && 444 | typeof cfg.pieceTheme !== 'function')) { 445 | cfg.pieceTheme = '/static/img/chesspieces/wikipedia/{piece}.png'; 446 | } 447 | 448 | // animation speeds 449 | if (cfg.hasOwnProperty('appearSpeed') !== true || 450 | validAnimationSpeed(cfg.appearSpeed) !== true) { 451 | cfg.appearSpeed = 200; 452 | } 453 | if (cfg.hasOwnProperty('moveSpeed') !== true || 454 | validAnimationSpeed(cfg.moveSpeed) !== true) { 455 | cfg.moveSpeed = 200; 456 | } 457 | if (cfg.hasOwnProperty('snapbackSpeed') !== true || 458 | validAnimationSpeed(cfg.snapbackSpeed) !== true) { 459 | cfg.snapbackSpeed = 50; 460 | } 461 | if (cfg.hasOwnProperty('snapSpeed') !== true || 462 | validAnimationSpeed(cfg.snapSpeed) !== true) { 463 | cfg.snapSpeed = 25; 464 | } 465 | if (cfg.hasOwnProperty('trashSpeed') !== true || 466 | validAnimationSpeed(cfg.trashSpeed) !== true) { 467 | cfg.trashSpeed = 100; 468 | } 469 | 470 | // make sure position is valid 471 | if (cfg.hasOwnProperty('position') === true) { 472 | if (cfg.position === 'start') { 473 | CURRENT_POSITION = deepCopy(START_POSITION); 474 | } 475 | 476 | else if (validFen(cfg.position) === true) { 477 | CURRENT_POSITION = fenToObj(cfg.position); 478 | } 479 | 480 | else if (validPositionObject(cfg.position) === true) { 481 | CURRENT_POSITION = deepCopy(cfg.position); 482 | } 483 | 484 | else { 485 | error(7263, 'Invalid value passed to config.position.', cfg.position); 486 | } 487 | } 488 | 489 | return true; 490 | } 491 | 492 | //------------------------------------------------------------------------------ 493 | // DOM Misc 494 | //------------------------------------------------------------------------------ 495 | 496 | // calculates square size based on the width of the container 497 | // got a little CSS black magic here, so let me explain: 498 | // get the width of the container element (could be anything), reduce by 1 for 499 | // fudge factor, and then keep reducing until we find an exact mod 8 for 500 | // our square size 501 | function calculateSquareSize() { 502 | var containerWidth = parseInt(containerEl.css('width'), 10); 503 | 504 | // defensive, prevent infinite loop 505 | if (! containerWidth || containerWidth <= 0) { 506 | return 0; 507 | } 508 | 509 | // pad one pixel 510 | var boardWidth = containerWidth - 1; 511 | 512 | while (boardWidth % 8 !== 0 && boardWidth > 0) { 513 | boardWidth--; 514 | } 515 | 516 | return (boardWidth / 8); 517 | } 518 | 519 | // create random IDs for elements 520 | function createElIds() { 521 | // squares on the board 522 | for (var i = 0; i < COLUMNS.length; i++) { 523 | for (var j = 1; j <= 8; j++) { 524 | var square = COLUMNS[i] + j; 525 | SQUARE_ELS_IDS[square] = square + '-' + createId(); 526 | } 527 | } 528 | 529 | // spare pieces 530 | var pieces = 'KQRBNP'.split(''); 531 | for (var i = 0; i < pieces.length; i++) { 532 | var whitePiece = 'w' + pieces[i]; 533 | var blackPiece = 'b' + pieces[i]; 534 | SPARE_PIECE_ELS_IDS[whitePiece] = whitePiece + '-' + createId(); 535 | SPARE_PIECE_ELS_IDS[blackPiece] = blackPiece + '-' + createId(); 536 | } 537 | } 538 | 539 | //------------------------------------------------------------------------------ 540 | // Markup Building 541 | //------------------------------------------------------------------------------ 542 | 543 | function buildBoardContainer() { 544 | var html = '
'; 545 | 546 | if (cfg.sparePieces === true) { 547 | html += '
'; 549 | } 550 | 551 | html += '
'; 552 | 553 | if (cfg.sparePieces === true) { 554 | html += '
'; 556 | } 557 | 558 | html += '
'; 559 | 560 | return html; 561 | } 562 | 563 | /* 564 | var buildSquare = function(color, size, id) { 565 | var html = '
'; 568 | 569 | if (cfg.showNotation === true) { 570 | 571 | } 572 | 573 | html += '
'; 574 | 575 | return html; 576 | }; 577 | */ 578 | 579 | function buildBoard(orientation) { 580 | if (orientation !== 'black') { 581 | orientation = 'white'; 582 | } 583 | 584 | var html = ''; 585 | 586 | // algebraic notation / orientation 587 | var alpha = deepCopy(COLUMNS); 588 | var row = 8; 589 | if (orientation === 'black') { 590 | alpha.reverse(); 591 | row = 1; 592 | } 593 | 594 | var squareColor = 'white'; 595 | for (var i = 0; i < 8; i++) { 596 | html += '
'; 597 | for (var j = 0; j < 8; j++) { 598 | var square = alpha[j] + row; 599 | 600 | html += '
'; 605 | 606 | if (cfg.showNotation === true) { 607 | // alpha notation 608 | if ((orientation === 'white' && row === 1) || 609 | (orientation === 'black' && row === 8)) { 610 | html += '
' + 611 | alpha[j] + '
'; 612 | } 613 | 614 | // numeric notation 615 | if (j === 0) { 616 | html += '
' + 617 | row + '
'; 618 | } 619 | } 620 | 621 | html += '
'; // end .square 622 | 623 | squareColor = (squareColor === 'white' ? 'black' : 'white'); 624 | } 625 | html += '
'; 626 | 627 | squareColor = (squareColor === 'white' ? 'black' : 'white'); 628 | 629 | if (orientation === 'white') { 630 | row--; 631 | } 632 | else { 633 | row++; 634 | } 635 | } 636 | 637 | return html; 638 | } 639 | 640 | function buildPieceImgSrc(piece) { 641 | if (typeof cfg.pieceTheme === 'function') { 642 | return cfg.pieceTheme(piece); 643 | } 644 | 645 | if (typeof cfg.pieceTheme === 'string') { 646 | return cfg.pieceTheme.replace(/{piece}/g, piece); 647 | } 648 | 649 | // NOTE: this should never happen 650 | error(8272, 'Unable to build image source for cfg.pieceTheme.'); 651 | return ''; 652 | } 653 | 654 | function buildPiece(piece, hidden, id) { 655 | var html = ''; 668 | 669 | return html; 670 | } 671 | 672 | function buildSparePieces(color) { 673 | var pieces = ['wK', 'wQ', 'wR', 'wB', 'wN', 'wP']; 674 | if (color === 'black') { 675 | pieces = ['bK', 'bQ', 'bR', 'bB', 'bN', 'bP']; 676 | } 677 | 678 | var html = ''; 679 | for (var i = 0; i < pieces.length; i++) { 680 | html += buildPiece(pieces[i], false, SPARE_PIECE_ELS_IDS[pieces[i]]); 681 | } 682 | 683 | return html; 684 | } 685 | 686 | //------------------------------------------------------------------------------ 687 | // Animations 688 | //------------------------------------------------------------------------------ 689 | 690 | function animateSquareToSquare(src, dest, piece, completeFn) { 691 | // get information about the source and destination squares 692 | var srcSquareEl = $('#' + SQUARE_ELS_IDS[src]); 693 | var srcSquarePosition = srcSquareEl.offset(); 694 | var destSquareEl = $('#' + SQUARE_ELS_IDS[dest]); 695 | var destSquarePosition = destSquareEl.offset(); 696 | 697 | // create the animated piece and absolutely position it 698 | // over the source square 699 | var animatedPieceId = createId(); 700 | $('body').append(buildPiece(piece, true, animatedPieceId)); 701 | var animatedPieceEl = $('#' + animatedPieceId); 702 | animatedPieceEl.css({ 703 | display: '', 704 | position: 'absolute', 705 | top: srcSquarePosition.top, 706 | left: srcSquarePosition.left 707 | }); 708 | 709 | // remove original piece from source square 710 | srcSquareEl.find('.' + CSS.piece).remove(); 711 | 712 | // on complete 713 | var complete = function() { 714 | // add the "real" piece to the destination square 715 | destSquareEl.append(buildPiece(piece)); 716 | 717 | // remove the animated piece 718 | animatedPieceEl.remove(); 719 | 720 | // run complete function 721 | if (typeof completeFn === 'function') { 722 | completeFn(); 723 | } 724 | }; 725 | 726 | // animate the piece to the destination square 727 | var opts = { 728 | duration: cfg.moveSpeed, 729 | complete: complete 730 | }; 731 | animatedPieceEl.animate(destSquarePosition, opts); 732 | } 733 | 734 | function animateSparePieceToSquare(piece, dest, completeFn) { 735 | var srcOffset = $('#' + SPARE_PIECE_ELS_IDS[piece]).offset(); 736 | var destSquareEl = $('#' + SQUARE_ELS_IDS[dest]); 737 | var destOffset = destSquareEl.offset(); 738 | 739 | // create the animate piece 740 | var pieceId = createId(); 741 | $('body').append(buildPiece(piece, true, pieceId)); 742 | var animatedPieceEl = $('#' + pieceId); 743 | animatedPieceEl.css({ 744 | display: '', 745 | position: 'absolute', 746 | left: srcOffset.left, 747 | top: srcOffset.top 748 | }); 749 | 750 | // on complete 751 | var complete = function() { 752 | // add the "real" piece to the destination square 753 | destSquareEl.find('.' + CSS.piece).remove(); 754 | destSquareEl.append(buildPiece(piece)); 755 | 756 | // remove the animated piece 757 | animatedPieceEl.remove(); 758 | 759 | // run complete function 760 | if (typeof completeFn === 'function') { 761 | completeFn(); 762 | } 763 | }; 764 | 765 | // animate the piece to the destination square 766 | var opts = { 767 | duration: cfg.moveSpeed, 768 | complete: complete 769 | }; 770 | animatedPieceEl.animate(destOffset, opts); 771 | } 772 | 773 | // execute an array of animations 774 | function doAnimations(a, oldPos, newPos) { 775 | ANIMATION_HAPPENING = true; 776 | 777 | var numFinished = 0; 778 | function onFinish() { 779 | numFinished++; 780 | 781 | // exit if all the animations aren't finished 782 | if (numFinished !== a.length) return; 783 | 784 | drawPositionInstant(); 785 | ANIMATION_HAPPENING = false; 786 | 787 | // run their onMoveEnd function 788 | if (cfg.hasOwnProperty('onMoveEnd') === true && 789 | typeof cfg.onMoveEnd === 'function') { 790 | cfg.onMoveEnd(deepCopy(oldPos), deepCopy(newPos)); 791 | } 792 | } 793 | 794 | for (var i = 0; i < a.length; i++) { 795 | // clear a piece 796 | if (a[i].type === 'clear') { 797 | $('#' + SQUARE_ELS_IDS[a[i].square] + ' .' + CSS.piece) 798 | .fadeOut(cfg.trashSpeed, onFinish); 799 | } 800 | 801 | // add a piece (no spare pieces) 802 | if (a[i].type === 'add' && cfg.sparePieces !== true) { 803 | $('#' + SQUARE_ELS_IDS[a[i].square]) 804 | .append(buildPiece(a[i].piece, true)) 805 | .find('.' + CSS.piece) 806 | .fadeIn(cfg.appearSpeed, onFinish); 807 | } 808 | 809 | // add a piece from a spare piece 810 | if (a[i].type === 'add' && cfg.sparePieces === true) { 811 | animateSparePieceToSquare(a[i].piece, a[i].square, onFinish); 812 | } 813 | 814 | // move a piece 815 | if (a[i].type === 'move') { 816 | animateSquareToSquare(a[i].source, a[i].destination, a[i].piece, 817 | onFinish); 818 | } 819 | } 820 | } 821 | 822 | // returns the distance between two squares 823 | function squareDistance(s1, s2) { 824 | s1 = s1.split(''); 825 | var s1x = COLUMNS.indexOf(s1[0]) + 1; 826 | var s1y = parseInt(s1[1], 10); 827 | 828 | s2 = s2.split(''); 829 | var s2x = COLUMNS.indexOf(s2[0]) + 1; 830 | var s2y = parseInt(s2[1], 10); 831 | 832 | var xDelta = Math.abs(s1x - s2x); 833 | var yDelta = Math.abs(s1y - s2y); 834 | 835 | if (xDelta >= yDelta) return xDelta; 836 | return yDelta; 837 | } 838 | 839 | // returns an array of closest squares from square 840 | function createRadius(square) { 841 | var squares = []; 842 | 843 | // calculate distance of all squares 844 | for (var i = 0; i < 8; i++) { 845 | for (var j = 0; j < 8; j++) { 846 | var s = COLUMNS[i] + (j + 1); 847 | 848 | // skip the square we're starting from 849 | if (square === s) continue; 850 | 851 | squares.push({ 852 | square: s, 853 | distance: squareDistance(square, s) 854 | }); 855 | } 856 | } 857 | 858 | // sort by distance 859 | squares.sort(function(a, b) { 860 | return a.distance - b.distance; 861 | }); 862 | 863 | // just return the square code 864 | var squares2 = []; 865 | for (var i = 0; i < squares.length; i++) { 866 | squares2.push(squares[i].square); 867 | } 868 | 869 | return squares2; 870 | } 871 | 872 | // returns the square of the closest instance of piece 873 | // returns false if no instance of piece is found in position 874 | function findClosestPiece(position, piece, square) { 875 | // create array of closest squares from square 876 | var closestSquares = createRadius(square); 877 | 878 | // search through the position in order of distance for the piece 879 | for (var i = 0; i < closestSquares.length; i++) { 880 | var s = closestSquares[i]; 881 | 882 | if (position.hasOwnProperty(s) === true && position[s] === piece) { 883 | return s; 884 | } 885 | } 886 | 887 | return false; 888 | } 889 | 890 | // calculate an array of animations that need to happen in order to get 891 | // from pos1 to pos2 892 | function calculateAnimations(pos1, pos2) { 893 | // make copies of both 894 | pos1 = deepCopy(pos1); 895 | pos2 = deepCopy(pos2); 896 | 897 | var animations = []; 898 | var squaresMovedTo = {}; 899 | 900 | // remove pieces that are the same in both positions 901 | for (var i in pos2) { 902 | if (pos2.hasOwnProperty(i) !== true) continue; 903 | 904 | if (pos1.hasOwnProperty(i) === true && pos1[i] === pos2[i]) { 905 | delete pos1[i]; 906 | delete pos2[i]; 907 | } 908 | } 909 | 910 | // find all the "move" animations 911 | for (var i in pos2) { 912 | if (pos2.hasOwnProperty(i) !== true) continue; 913 | 914 | var closestPiece = findClosestPiece(pos1, pos2[i], i); 915 | if (closestPiece !== false) { 916 | animations.push({ 917 | type: 'move', 918 | source: closestPiece, 919 | destination: i, 920 | piece: pos2[i] 921 | }); 922 | 923 | delete pos1[closestPiece]; 924 | delete pos2[i]; 925 | squaresMovedTo[i] = true; 926 | } 927 | } 928 | 929 | // add pieces to pos2 930 | for (var i in pos2) { 931 | if (pos2.hasOwnProperty(i) !== true) continue; 932 | 933 | animations.push({ 934 | type: 'add', 935 | square: i, 936 | piece: pos2[i] 937 | }) 938 | 939 | delete pos2[i]; 940 | } 941 | 942 | // clear pieces from pos1 943 | for (var i in pos1) { 944 | if (pos1.hasOwnProperty(i) !== true) continue; 945 | 946 | // do not clear a piece if it is on a square that is the result 947 | // of a "move", ie: a piece capture 948 | if (squaresMovedTo.hasOwnProperty(i) === true) continue; 949 | 950 | animations.push({ 951 | type: 'clear', 952 | square: i, 953 | piece: pos1[i] 954 | }); 955 | 956 | delete pos1[i]; 957 | } 958 | 959 | return animations; 960 | } 961 | 962 | //------------------------------------------------------------------------------ 963 | // Control Flow 964 | //------------------------------------------------------------------------------ 965 | 966 | function drawPositionInstant() { 967 | // clear the board 968 | boardEl.find('.' + CSS.piece).remove(); 969 | 970 | // add the pieces 971 | for (var i in CURRENT_POSITION) { 972 | if (CURRENT_POSITION.hasOwnProperty(i) !== true) continue; 973 | 974 | $('#' + SQUARE_ELS_IDS[i]).append(buildPiece(CURRENT_POSITION[i])); 975 | } 976 | } 977 | 978 | function drawBoard() { 979 | boardEl.html(buildBoard(CURRENT_ORIENTATION)); 980 | drawPositionInstant(); 981 | 982 | if (cfg.sparePieces === true) { 983 | if (CURRENT_ORIENTATION === 'white') { 984 | sparePiecesTopEl.html(buildSparePieces('black')); 985 | sparePiecesBottomEl.html(buildSparePieces('white')); 986 | } 987 | else { 988 | sparePiecesTopEl.html(buildSparePieces('white')); 989 | sparePiecesBottomEl.html(buildSparePieces('black')); 990 | } 991 | } 992 | } 993 | 994 | // given a position and a set of moves, return a new position 995 | // with the moves executed 996 | function calculatePositionFromMoves(position, moves) { 997 | position = deepCopy(position); 998 | 999 | for (var i in moves) { 1000 | if (moves.hasOwnProperty(i) !== true) continue; 1001 | 1002 | // skip the move if the position doesn't have a piece on the source square 1003 | if (position.hasOwnProperty(i) !== true) continue; 1004 | 1005 | var piece = position[i]; 1006 | delete position[i]; 1007 | position[moves[i]] = piece; 1008 | } 1009 | 1010 | return position; 1011 | } 1012 | 1013 | function setCurrentPosition(position) { 1014 | var oldPos = deepCopy(CURRENT_POSITION); 1015 | var newPos = deepCopy(position); 1016 | var oldFen = objToFen(oldPos); 1017 | var newFen = objToFen(newPos); 1018 | 1019 | // do nothing if no change in position 1020 | if (oldFen === newFen) return; 1021 | 1022 | // run their onChange function 1023 | if (cfg.hasOwnProperty('onChange') === true && 1024 | typeof cfg.onChange === 'function') { 1025 | cfg.onChange(oldPos, newPos); 1026 | } 1027 | 1028 | // update state 1029 | CURRENT_POSITION = position; 1030 | } 1031 | 1032 | function isXYOnSquare(x, y) { 1033 | for (var i in SQUARE_ELS_OFFSETS) { 1034 | if (SQUARE_ELS_OFFSETS.hasOwnProperty(i) !== true) continue; 1035 | 1036 | var s = SQUARE_ELS_OFFSETS[i]; 1037 | if (x >= s.left && x < s.left + SQUARE_SIZE && 1038 | y >= s.top && y < s.top + SQUARE_SIZE) { 1039 | return i; 1040 | } 1041 | } 1042 | 1043 | return 'offboard'; 1044 | } 1045 | 1046 | // records the XY coords of every square into memory 1047 | function captureSquareOffsets() { 1048 | SQUARE_ELS_OFFSETS = {}; 1049 | 1050 | for (var i in SQUARE_ELS_IDS) { 1051 | if (SQUARE_ELS_IDS.hasOwnProperty(i) !== true) continue; 1052 | 1053 | SQUARE_ELS_OFFSETS[i] = $('#' + SQUARE_ELS_IDS[i]).offset(); 1054 | } 1055 | } 1056 | 1057 | function removeSquareHighlights() { 1058 | boardEl.find('.' + CSS.square) 1059 | .removeClass(CSS.highlight1 + ' ' + CSS.highlight2); 1060 | } 1061 | 1062 | function snapbackDraggedPiece() { 1063 | // there is no "snapback" for spare pieces 1064 | if (DRAGGED_PIECE_SOURCE === 'spare') { 1065 | trashDraggedPiece(); 1066 | return; 1067 | } 1068 | 1069 | removeSquareHighlights(); 1070 | 1071 | // animation complete 1072 | function complete() { 1073 | drawPositionInstant(); 1074 | draggedPieceEl.css('display', 'none'); 1075 | 1076 | // run their onSnapbackEnd function 1077 | if (cfg.hasOwnProperty('onSnapbackEnd') === true && 1078 | typeof cfg.onSnapbackEnd === 'function') { 1079 | cfg.onSnapbackEnd(DRAGGED_PIECE, DRAGGED_PIECE_SOURCE, 1080 | deepCopy(CURRENT_POSITION), CURRENT_ORIENTATION); 1081 | } 1082 | } 1083 | 1084 | // get source square position 1085 | var sourceSquarePosition = 1086 | $('#' + SQUARE_ELS_IDS[DRAGGED_PIECE_SOURCE]).offset(); 1087 | 1088 | // animate the piece to the target square 1089 | var opts = { 1090 | duration: cfg.snapbackSpeed, 1091 | complete: complete 1092 | }; 1093 | draggedPieceEl.animate(sourceSquarePosition, opts); 1094 | 1095 | // set state 1096 | DRAGGING_A_PIECE = false; 1097 | } 1098 | 1099 | function trashDraggedPiece() { 1100 | removeSquareHighlights(); 1101 | 1102 | // remove the source piece 1103 | var newPosition = deepCopy(CURRENT_POSITION); 1104 | delete newPosition[DRAGGED_PIECE_SOURCE]; 1105 | setCurrentPosition(newPosition); 1106 | 1107 | // redraw the position 1108 | drawPositionInstant(); 1109 | 1110 | // hide the dragged piece 1111 | draggedPieceEl.fadeOut(cfg.trashSpeed); 1112 | 1113 | // set state 1114 | DRAGGING_A_PIECE = false; 1115 | } 1116 | 1117 | function dropDraggedPieceOnSquare(square) { 1118 | removeSquareHighlights(); 1119 | 1120 | // update position 1121 | var newPosition = deepCopy(CURRENT_POSITION); 1122 | delete newPosition[DRAGGED_PIECE_SOURCE]; 1123 | newPosition[square] = DRAGGED_PIECE; 1124 | setCurrentPosition(newPosition); 1125 | 1126 | // get target square information 1127 | var targetSquarePosition = $('#' + SQUARE_ELS_IDS[square]).offset(); 1128 | 1129 | // animation complete 1130 | var complete = function() { 1131 | drawPositionInstant(); 1132 | draggedPieceEl.css('display', 'none'); 1133 | 1134 | // execute their onSnapEnd function 1135 | if (cfg.hasOwnProperty('onSnapEnd') === true && 1136 | typeof cfg.onSnapEnd === 'function') { 1137 | cfg.onSnapEnd(DRAGGED_PIECE_SOURCE, square, DRAGGED_PIECE); 1138 | } 1139 | }; 1140 | 1141 | // snap the piece to the target square 1142 | var opts = { 1143 | duration: cfg.snapSpeed, 1144 | complete: complete 1145 | }; 1146 | draggedPieceEl.animate(targetSquarePosition, opts); 1147 | 1148 | // set state 1149 | DRAGGING_A_PIECE = false; 1150 | } 1151 | 1152 | function beginDraggingPiece(source, piece, x, y) { 1153 | // run their custom onDragStart function 1154 | // their custom onDragStart function can cancel drag start 1155 | if (typeof cfg.onDragStart === 'function' && 1156 | cfg.onDragStart(source, piece, 1157 | deepCopy(CURRENT_POSITION), CURRENT_ORIENTATION) === false) { 1158 | return; 1159 | } 1160 | 1161 | // set state 1162 | DRAGGING_A_PIECE = true; 1163 | DRAGGED_PIECE = piece; 1164 | DRAGGED_PIECE_SOURCE = source; 1165 | 1166 | // if the piece came from spare pieces, location is offboard 1167 | if (source === 'spare') { 1168 | DRAGGED_PIECE_LOCATION = 'offboard'; 1169 | } 1170 | else { 1171 | DRAGGED_PIECE_LOCATION = source; 1172 | } 1173 | 1174 | // capture the x, y coords of all squares in memory 1175 | captureSquareOffsets(); 1176 | 1177 | // create the dragged piece 1178 | draggedPieceEl.attr('src', buildPieceImgSrc(piece)) 1179 | .css({ 1180 | display: '', 1181 | position: 'absolute', 1182 | left: x - (SQUARE_SIZE / 2), 1183 | top: y - (SQUARE_SIZE / 2) 1184 | }); 1185 | 1186 | if (source !== 'spare') { 1187 | // highlight the source square and hide the piece 1188 | $('#' + SQUARE_ELS_IDS[source]).addClass(CSS.highlight1) 1189 | .find('.' + CSS.piece).css('display', 'none'); 1190 | } 1191 | } 1192 | 1193 | function updateDraggedPiece(x, y) { 1194 | // put the dragged piece over the mouse cursor 1195 | draggedPieceEl.css({ 1196 | left: x - (SQUARE_SIZE / 2), 1197 | top: y - (SQUARE_SIZE / 2) 1198 | }); 1199 | 1200 | // get location 1201 | var location = isXYOnSquare(x, y); 1202 | 1203 | // do nothing if the location has not changed 1204 | if (location === DRAGGED_PIECE_LOCATION) return; 1205 | 1206 | // remove highlight from previous square 1207 | if (validSquare(DRAGGED_PIECE_LOCATION) === true) { 1208 | $('#' + SQUARE_ELS_IDS[DRAGGED_PIECE_LOCATION]) 1209 | .removeClass(CSS.highlight2); 1210 | } 1211 | 1212 | // add highlight to new square 1213 | if (validSquare(location) === true) { 1214 | $('#' + SQUARE_ELS_IDS[location]).addClass(CSS.highlight2); 1215 | } 1216 | 1217 | // run onDragMove 1218 | if (typeof cfg.onDragMove === 'function') { 1219 | cfg.onDragMove(location, DRAGGED_PIECE_LOCATION, 1220 | DRAGGED_PIECE_SOURCE, DRAGGED_PIECE, 1221 | deepCopy(CURRENT_POSITION), CURRENT_ORIENTATION); 1222 | } 1223 | 1224 | // update state 1225 | DRAGGED_PIECE_LOCATION = location; 1226 | } 1227 | 1228 | function stopDraggedPiece(location) { 1229 | // determine what the action should be 1230 | var action = 'drop'; 1231 | if (location === 'offboard' && cfg.dropOffBoard === 'snapback') { 1232 | action = 'snapback'; 1233 | } 1234 | if (location === 'offboard' && cfg.dropOffBoard === 'trash') { 1235 | action = 'trash'; 1236 | } 1237 | 1238 | // run their onDrop function, which can potentially change the drop action 1239 | if (cfg.hasOwnProperty('onDrop') === true && 1240 | typeof cfg.onDrop === 'function') { 1241 | var newPosition = deepCopy(CURRENT_POSITION); 1242 | 1243 | // source piece is a spare piece and position is off the board 1244 | //if (DRAGGED_PIECE_SOURCE === 'spare' && location === 'offboard') {...} 1245 | // position has not changed; do nothing 1246 | 1247 | // source piece is a spare piece and position is on the board 1248 | if (DRAGGED_PIECE_SOURCE === 'spare' && validSquare(location) === true) { 1249 | // add the piece to the board 1250 | newPosition[location] = DRAGGED_PIECE; 1251 | } 1252 | 1253 | // source piece was on the board and position is off the board 1254 | if (validSquare(DRAGGED_PIECE_SOURCE) === true && location === 'offboard') { 1255 | // remove the piece from the board 1256 | delete newPosition[DRAGGED_PIECE_SOURCE]; 1257 | } 1258 | 1259 | // source piece was on the board and position is on the board 1260 | if (validSquare(DRAGGED_PIECE_SOURCE) === true && 1261 | validSquare(location) === true) { 1262 | // move the piece 1263 | delete newPosition[DRAGGED_PIECE_SOURCE]; 1264 | newPosition[location] = DRAGGED_PIECE; 1265 | } 1266 | 1267 | var oldPosition = deepCopy(CURRENT_POSITION); 1268 | 1269 | var result = cfg.onDrop(DRAGGED_PIECE_SOURCE, location, DRAGGED_PIECE, 1270 | newPosition, oldPosition, CURRENT_ORIENTATION); 1271 | if (result === 'snapback' || result === 'trash') { 1272 | action = result; 1273 | } 1274 | } 1275 | 1276 | // do it! 1277 | if (action === 'snapback') { 1278 | snapbackDraggedPiece(); 1279 | } 1280 | else if (action === 'trash') { 1281 | trashDraggedPiece(); 1282 | } 1283 | else if (action === 'drop') { 1284 | dropDraggedPieceOnSquare(location); 1285 | } 1286 | } 1287 | 1288 | //------------------------------------------------------------------------------ 1289 | // Public Methods 1290 | //------------------------------------------------------------------------------ 1291 | 1292 | // clear the board 1293 | widget.clear = function(useAnimation) { 1294 | widget.position({}, useAnimation); 1295 | }; 1296 | 1297 | /* 1298 | // get or set config properties 1299 | // TODO: write this, GitHub Issue #1 1300 | widget.config = function(arg1, arg2) { 1301 | // get the current config 1302 | if (arguments.length === 0) { 1303 | return deepCopy(cfg); 1304 | } 1305 | }; 1306 | */ 1307 | 1308 | // remove the widget from the page 1309 | widget.destroy = function() { 1310 | // remove markup 1311 | containerEl.html(''); 1312 | draggedPieceEl.remove(); 1313 | 1314 | // remove event handlers 1315 | containerEl.unbind(); 1316 | }; 1317 | 1318 | // shorthand method to get the current FEN 1319 | widget.fen = function() { 1320 | return widget.position('fen'); 1321 | }; 1322 | 1323 | // flip orientation 1324 | widget.flip = function() { 1325 | widget.orientation('flip'); 1326 | }; 1327 | 1328 | /* 1329 | // TODO: write this, GitHub Issue #5 1330 | widget.highlight = function() { 1331 | 1332 | }; 1333 | */ 1334 | 1335 | // move pieces 1336 | widget.move = function() { 1337 | // no need to throw an error here; just do nothing 1338 | if (arguments.length === 0) return; 1339 | 1340 | var useAnimation = true; 1341 | 1342 | // collect the moves into an object 1343 | var moves = {}; 1344 | for (var i = 0; i < arguments.length; i++) { 1345 | // any "false" to this function means no animations 1346 | if (arguments[i] === false) { 1347 | useAnimation = false; 1348 | continue; 1349 | } 1350 | 1351 | // skip invalid arguments 1352 | if (validMove(arguments[i]) !== true) { 1353 | error(2826, 'Invalid move passed to the move method.', arguments[i]); 1354 | continue; 1355 | } 1356 | 1357 | var tmp = arguments[i].split('-'); 1358 | moves[tmp[0]] = tmp[1]; 1359 | } 1360 | 1361 | // calculate position from moves 1362 | var newPos = calculatePositionFromMoves(CURRENT_POSITION, moves); 1363 | 1364 | // update the board 1365 | widget.position(newPos, useAnimation); 1366 | 1367 | // return the new position object 1368 | return newPos; 1369 | }; 1370 | 1371 | widget.orientation = function(arg) { 1372 | // no arguments, return the current orientation 1373 | if (arguments.length === 0) { 1374 | return CURRENT_ORIENTATION; 1375 | } 1376 | 1377 | // set to white or black 1378 | if (arg === 'white' || arg === 'black') { 1379 | CURRENT_ORIENTATION = arg; 1380 | drawBoard(); 1381 | return; 1382 | } 1383 | 1384 | // flip orientation 1385 | if (arg === 'flip') { 1386 | CURRENT_ORIENTATION = (CURRENT_ORIENTATION === 'white') ? 'black' : 'white'; 1387 | drawBoard(); 1388 | return; 1389 | } 1390 | 1391 | error(5482, 'Invalid value passed to the orientation method.', arg); 1392 | }; 1393 | 1394 | widget.position = function(position, useAnimation) { 1395 | // no arguments, return the current position 1396 | if (arguments.length === 0) { 1397 | return deepCopy(CURRENT_POSITION); 1398 | } 1399 | 1400 | // get position as FEN 1401 | if (typeof position === 'string' && position.toLowerCase() === 'fen') { 1402 | return objToFen(CURRENT_POSITION); 1403 | } 1404 | 1405 | // default for useAnimations is true 1406 | if (useAnimation !== false) { 1407 | useAnimation = true; 1408 | } 1409 | 1410 | // start position 1411 | if (typeof position === 'string' && position.toLowerCase() === 'start') { 1412 | position = deepCopy(START_POSITION); 1413 | } 1414 | 1415 | // convert FEN to position object 1416 | if (validFen(position) === true) { 1417 | position = fenToObj(position); 1418 | } 1419 | 1420 | // validate position object 1421 | if (validPositionObject(position) !== true) { 1422 | error(6482, 'Invalid value passed to the position method.', position); 1423 | return; 1424 | } 1425 | 1426 | if (useAnimation === true) { 1427 | // start the animations 1428 | doAnimations(calculateAnimations(CURRENT_POSITION, position), 1429 | CURRENT_POSITION, position); 1430 | 1431 | // set the new position 1432 | setCurrentPosition(position); 1433 | } 1434 | // instant update 1435 | else { 1436 | setCurrentPosition(position); 1437 | drawPositionInstant(); 1438 | } 1439 | }; 1440 | 1441 | widget.resize = function() { 1442 | // calulate the new square size 1443 | SQUARE_SIZE = calculateSquareSize(); 1444 | 1445 | // set board width 1446 | boardEl.css('width', (SQUARE_SIZE * 8) + 'px'); 1447 | 1448 | // set drag piece size 1449 | draggedPieceEl.css({ 1450 | height: SQUARE_SIZE, 1451 | width: SQUARE_SIZE 1452 | }); 1453 | 1454 | // spare pieces 1455 | if (cfg.sparePieces === true) { 1456 | containerEl.find('.' + CSS.sparePieces) 1457 | .css('paddingLeft', (SQUARE_SIZE + BOARD_BORDER_SIZE) + 'px'); 1458 | } 1459 | 1460 | // redraw the board 1461 | drawBoard(); 1462 | }; 1463 | 1464 | // set the starting position 1465 | widget.start = function(useAnimation) { 1466 | widget.position('start', useAnimation); 1467 | }; 1468 | 1469 | //------------------------------------------------------------------------------ 1470 | // Browser Events 1471 | //------------------------------------------------------------------------------ 1472 | 1473 | function isTouchDevice() { 1474 | return ('ontouchstart' in document.documentElement); 1475 | } 1476 | 1477 | // reference: http://www.quirksmode.org/js/detect.html 1478 | function isMSIE() { 1479 | return (navigator && navigator.userAgent && 1480 | navigator.userAgent.search(/MSIE/) !== -1); 1481 | } 1482 | 1483 | function stopDefault(e) { 1484 | e.preventDefault(); 1485 | } 1486 | 1487 | function mousedownSquare(e) { 1488 | // do nothing if we're not draggable 1489 | if (cfg.draggable !== true) return; 1490 | 1491 | var square = $(this).attr('data-square'); 1492 | 1493 | // no piece on this square 1494 | if (validSquare(square) !== true || 1495 | CURRENT_POSITION.hasOwnProperty(square) !== true) { 1496 | return; 1497 | } 1498 | 1499 | beginDraggingPiece(square, CURRENT_POSITION[square], e.pageX, e.pageY); 1500 | } 1501 | 1502 | function touchstartSquare(e) { 1503 | // do nothing if we're not draggable 1504 | if (cfg.draggable !== true) return; 1505 | 1506 | var square = $(this).attr('data-square'); 1507 | 1508 | // no piece on this square 1509 | if (validSquare(square) !== true || 1510 | CURRENT_POSITION.hasOwnProperty(square) !== true) { 1511 | return; 1512 | } 1513 | 1514 | e = e.originalEvent; 1515 | beginDraggingPiece(square, CURRENT_POSITION[square], 1516 | e.changedTouches[0].pageX, e.changedTouches[0].pageY); 1517 | } 1518 | 1519 | function mousedownSparePiece(e) { 1520 | // do nothing if sparePieces is not enabled 1521 | if (cfg.sparePieces !== true) return; 1522 | 1523 | var piece = $(this).attr('data-piece'); 1524 | 1525 | beginDraggingPiece('spare', piece, e.pageX, e.pageY); 1526 | } 1527 | 1528 | function touchstartSparePiece(e) { 1529 | // do nothing if sparePieces is not enabled 1530 | if (cfg.sparePieces !== true) return; 1531 | 1532 | var piece = $(this).attr('data-piece'); 1533 | 1534 | e = e.originalEvent; 1535 | beginDraggingPiece('spare', piece, 1536 | e.changedTouches[0].pageX, e.changedTouches[0].pageY); 1537 | } 1538 | 1539 | function mousemoveWindow(e) { 1540 | // do nothing if we are not dragging a piece 1541 | if (DRAGGING_A_PIECE !== true) return; 1542 | 1543 | updateDraggedPiece(e.pageX, e.pageY); 1544 | } 1545 | 1546 | function touchmoveWindow(e) { 1547 | // do nothing if we are not dragging a piece 1548 | if (DRAGGING_A_PIECE !== true) return; 1549 | 1550 | // prevent screen from scrolling 1551 | e.preventDefault(); 1552 | 1553 | updateDraggedPiece(e.originalEvent.changedTouches[0].pageX, 1554 | e.originalEvent.changedTouches[0].pageY); 1555 | } 1556 | 1557 | function mouseupWindow(e) { 1558 | // do nothing if we are not dragging a piece 1559 | if (DRAGGING_A_PIECE !== true) return; 1560 | 1561 | // get the location 1562 | var location = isXYOnSquare(e.pageX, e.pageY); 1563 | 1564 | stopDraggedPiece(location); 1565 | } 1566 | 1567 | function touchendWindow(e) { 1568 | // do nothing if we are not dragging a piece 1569 | if (DRAGGING_A_PIECE !== true) return; 1570 | 1571 | // get the location 1572 | var location = isXYOnSquare(e.originalEvent.changedTouches[0].pageX, 1573 | e.originalEvent.changedTouches[0].pageY); 1574 | 1575 | stopDraggedPiece(location); 1576 | } 1577 | 1578 | function mouseenterSquare(e) { 1579 | // do not fire this event if we are dragging a piece 1580 | // NOTE: this should never happen, but it's a safeguard 1581 | if (DRAGGING_A_PIECE !== false) return; 1582 | 1583 | if (cfg.hasOwnProperty('onMouseoverSquare') !== true || 1584 | typeof cfg.onMouseoverSquare !== 'function') return; 1585 | 1586 | // get the square 1587 | var square = $(e.currentTarget).attr('data-square'); 1588 | 1589 | // NOTE: this should never happen; defensive 1590 | if (validSquare(square) !== true) return; 1591 | 1592 | // get the piece on this square 1593 | var piece = false; 1594 | if (CURRENT_POSITION.hasOwnProperty(square) === true) { 1595 | piece = CURRENT_POSITION[square]; 1596 | } 1597 | 1598 | // execute their function 1599 | cfg.onMouseoverSquare(square, piece, deepCopy(CURRENT_POSITION), 1600 | CURRENT_ORIENTATION); 1601 | } 1602 | 1603 | function mouseleaveSquare(e) { 1604 | // do not fire this event if we are dragging a piece 1605 | // NOTE: this should never happen, but it's a safeguard 1606 | if (DRAGGING_A_PIECE !== false) return; 1607 | 1608 | if (cfg.hasOwnProperty('onMouseoutSquare') !== true || 1609 | typeof cfg.onMouseoutSquare !== 'function') return; 1610 | 1611 | // get the square 1612 | var square = $(e.currentTarget).attr('data-square'); 1613 | 1614 | // NOTE: this should never happen; defensive 1615 | if (validSquare(square) !== true) return; 1616 | 1617 | // get the piece on this square 1618 | var piece = false; 1619 | if (CURRENT_POSITION.hasOwnProperty(square) === true) { 1620 | piece = CURRENT_POSITION[square]; 1621 | } 1622 | 1623 | // execute their function 1624 | cfg.onMouseoutSquare(square, piece, deepCopy(CURRENT_POSITION), 1625 | CURRENT_ORIENTATION); 1626 | } 1627 | 1628 | //------------------------------------------------------------------------------ 1629 | // Initialization 1630 | //------------------------------------------------------------------------------ 1631 | 1632 | function addEvents() { 1633 | // prevent browser "image drag" 1634 | $('body').on('mousedown mousemove', '.' + CSS.piece, stopDefault); 1635 | 1636 | // mouse drag pieces 1637 | boardEl.on('mousedown', '.' + CSS.square, mousedownSquare); 1638 | containerEl.on('mousedown', '.' + CSS.sparePieces + ' .' + CSS.piece, 1639 | mousedownSparePiece); 1640 | 1641 | // mouse enter / leave square 1642 | boardEl.on('mouseenter', '.' + CSS.square, mouseenterSquare); 1643 | boardEl.on('mouseleave', '.' + CSS.square, mouseleaveSquare); 1644 | 1645 | // IE doesn't like the events on the window object, but other browsers 1646 | // perform better that way 1647 | if (isMSIE() === true) { 1648 | // IE-specific prevent browser "image drag" 1649 | document.ondragstart = function() { return false; }; 1650 | 1651 | $('body').on('mousemove', mousemoveWindow); 1652 | $('body').on('mouseup', mouseupWindow); 1653 | } 1654 | else { 1655 | $(window).on('mousemove', mousemoveWindow); 1656 | $(window).on('mouseup', mouseupWindow); 1657 | } 1658 | 1659 | // touch drag pieces 1660 | if (isTouchDevice() === true) { 1661 | boardEl.on('touchstart', '.' + CSS.square, touchstartSquare); 1662 | containerEl.on('touchstart', '.' + CSS.sparePieces + ' .' + CSS.piece, 1663 | touchstartSparePiece); 1664 | $(window).on('touchmove', touchmoveWindow); 1665 | $(window).on('touchend', touchendWindow); 1666 | } 1667 | } 1668 | 1669 | function initDom() { 1670 | // build board and save it in memory 1671 | containerEl.html(buildBoardContainer()); 1672 | boardEl = containerEl.find('.' + CSS.board); 1673 | 1674 | if (cfg.sparePieces === true) { 1675 | sparePiecesTopEl = containerEl.find('.' + CSS.sparePiecesTop); 1676 | sparePiecesBottomEl = containerEl.find('.' + CSS.sparePiecesBottom); 1677 | } 1678 | 1679 | // create the drag piece 1680 | var draggedPieceId = createId(); 1681 | $('body').append(buildPiece('wP', true, draggedPieceId)); 1682 | draggedPieceEl = $('#' + draggedPieceId); 1683 | 1684 | // get the border size 1685 | BOARD_BORDER_SIZE = parseInt(boardEl.css('borderLeftWidth'), 10); 1686 | 1687 | // set the size and draw the board 1688 | widget.resize(); 1689 | } 1690 | 1691 | function init() { 1692 | if (checkDeps() !== true || 1693 | expandConfig() !== true) return; 1694 | 1695 | // create unique IDs for all the elements we will create 1696 | createElIds(); 1697 | 1698 | initDom(); 1699 | addEvents(); 1700 | } 1701 | 1702 | // go time 1703 | init(); 1704 | 1705 | // return the widget object 1706 | return widget; 1707 | 1708 | }; // end window.ChessBoard 1709 | 1710 | // expose util functions 1711 | window.ChessBoard.fenToObj = fenToObj; 1712 | window.ChessBoard.objToFen = objToFen; 1713 | 1714 | })(); // end anonymous wrapper 1715 | -------------------------------------------------------------------------------- /static/js/chessboard-0.3.0.min.js: -------------------------------------------------------------------------------- 1 | /*! chessboard.js v0.3.0 | (c) 2013 Chris Oakman | MIT License chessboardjs.com/license */ 2 | (function(){function l(f){return"string"!==typeof f?!1:-1!==f.search(/^[a-h][1-8]$/)}function Q(f){if("string"!==typeof f)return!1;f=f.replace(/ .+$/,"");f=f.split("/");if(8!==f.length)return!1;for(var b=0;8>b;b++)if(""===f[b]||8m;m++){for(var l=f[m].split(""),r=0,w=0;wm;m++){for(var l=0;8>l;l++){var r=B[l]+n;!0===f.hasOwnProperty(r)?(r=f[r].split(""),r="w"===r[0]?r[1].toUpperCase(): 4 | r[1].toLowerCase(),b+=r):b+="1"}7!==m&&(b+="/");n--}b=b.replace(/11111111/g,"8");b=b.replace(/1111111/g,"7");b=b.replace(/111111/g,"6");b=b.replace(/11111/g,"5");b=b.replace(/1111/g,"4");b=b.replace(/111/g,"3");return b=b.replace(/11/g,"2")}var B="abcdefgh".split("");window.ChessBoard=window.ChessBoard||function(f,b){function n(){return"xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx".replace(/x/g,function(a){return(16*Math.random()|0).toString(16)})}function m(a){return JSON.parse(JSON.stringify(a))}function X(a){a= 5 | a.split(".");return{major:parseInt(a[0],10),minor:parseInt(a[1],10),patch:parseInt(a[2],10)}}function r(a,e,c){if(!0===b.hasOwnProperty("showErrors")&&!1!==b.showErrors){var d="ChessBoard Error "+a+": "+e;"console"===b.showErrors&&"object"===typeof console&&"function"===typeof console.log?(console.log(d),2<=arguments.length&&console.log(c)):"alert"===b.showErrors?(c&&(d+="\n\n"+JSON.stringify(c)),window.alert(d)):"function"===typeof b.showErrors&&b.showErrors(a,e,c)}}function w(a){return"fast"=== 6 | a||"slow"===a?!0:parseInt(a,10)+""!==a+""?!1:0<=a}function I(){for(var a=0;a=b;b++){var c=B[a]+b;s[c]=c+"-"+n()}b="KQRBNP".split("");for(a=0;a';!0===b.sparePieces&&(a+='
');a+='
';!0===b.sparePieces&&(a+='
'); 7 | return a+""}function A(a){"black"!==a&&(a="white");var e="",c=m(B),d=8;"black"===a&&(c.reverse(),d=1);for(var C="white",f=0;8>f;f++){for(var e=e+('
'),k=0;8>k;k++){var g=c[k]+d,e=e+('
');if(!0===b.showNotation){if("white"===a&&1===d||"black"===a&&8===d)e+='
'+c[k]+"
";0===k&&(e+='
'+d+"
")}e+="
";C="white"===C?"black":"white"}e+='
';C="white"===C?"black":"white";"white"===a?d--:d++}return e}function Y(a){if("function"===typeof b.pieceTheme)return b.pieceTheme(a);if("string"===typeof b.pieceTheme)return b.pieceTheme.replace(/{piece}/g,a);r(8272,"Unable to build image source for cfg.pieceTheme.");return""}function D(a,b,c){var d=''}function N(a){var b="wK wQ wR wB wN wP".split(" ");"black"===a&&(b="bK bQ bR bB bN bP".split(" "));a="";for(var c=0;c=d?c:d}function la(a){for(var b=[],c=0;8>c;c++)for(var d=0;8>d;d++){var g=B[c]+(d+1);a!==g&&b.push({square:g,distance:ka(a,g)})}b.sort(function(a,b){return a.distance-b.distance});a=[];for(c=0;c=d.left&&a=d.top&&b=a)p=0;else{for(a-=1;0!==a%8&&0=1E8*b.major+1E4*b.minor+b.patch;return a?!0:(window.alert("ChessBoard Error 1005: Unable to find a valid version of jQuery. Please include jQuery 1.7.0 or higher on the page.\n\nExiting..."), 29 | !1)}()){if("string"===typeof b||!0===F(b))b={position:b};"black"!==b.orientation&&(b.orientation="white");u=b.orientation;!1!==b.showNotation&&(b.showNotation=!0);!0!==b.draggable&&(b.draggable=!1);"trash"!==b.dropOffBoard&&(b.dropOffBoard="snapback");!0!==b.sparePieces&&(b.sparePieces=!1);!0===b.sparePieces&&(b.draggable=!0);if(!0!==b.hasOwnProperty("pieceTheme")||"string"!==typeof b.pieceTheme&&"function"!==typeof b.pieceTheme)b.pieceTheme="img/chesspieces/wikipedia/{piece}.png";if(!0!==b.hasOwnProperty("appearSpeed")|| 30 | !0!==w(b.appearSpeed))b.appearSpeed=200;if(!0!==b.hasOwnProperty("moveSpeed")||!0!==w(b.moveSpeed))b.moveSpeed=200;if(!0!==b.hasOwnProperty("snapbackSpeed")||!0!==w(b.snapbackSpeed))b.snapbackSpeed=50;if(!0!==b.hasOwnProperty("snapSpeed")||!0!==w(b.snapSpeed))b.snapSpeed=25;if(!0!==b.hasOwnProperty("trashSpeed")||!0!==w(b.trashSpeed))b.trashSpeed=100;!0===b.hasOwnProperty("position")&&("start"===b.position?g=m(fa):!0===Q(b.position)?g=K(b.position):!0===F(b.position)?g=m(b.position):r(7263,"Invalid value passed to config.position.", 31 | b.position));W=!0}W&&(I(),ya(),xa());return q};window.ChessBoard.fenToObj=K;window.ChessBoard.objToFen=L})(); -------------------------------------------------------------------------------- /static/js/script.js: -------------------------------------------------------------------------------- 1 | // $(function() { 2 | var board, 3 | game = new Chess(), 4 | statusEl = $('#status'), 5 | fenEl = $('#fen'), 6 | pgnEl = $('#pgn'); 7 | 8 | // do not pick up pieces if the game is over 9 | // only pick up pieces for the side to move 10 | var onDragStart = function(source, piece, position, orientation) { 11 | if (game.game_over() === true || 12 | (game.turn() === 'w' && piece.search(/^b/) !== -1) || 13 | (game.turn() === 'b' && piece.search(/^w/) === -1)) { 14 | return false; 15 | } 16 | }; 17 | 18 | var onDrop = function(source, target) { 19 | // see if the move is legal 20 | var move = game.move({ 21 | from: source, 22 | to: target, 23 | promotion: 'q' // NOTE: always promote to a queen for example simplicity 24 | }); 25 | 26 | // illegal move 27 | if (move === null) return 'snapback'; 28 | 29 | updateStatus(); 30 | 31 | if(game.game_over()){ 32 | $("body").prepend("

GAME OVER

"); 33 | } 34 | 35 | $.ajax({ 36 | method: "POST", 37 | url: "/board", 38 | data: {fen: game.fen()}, 39 | success: function(data){ 40 | var cpuMove = data.move.slice(0,2) + '-' + data.move.slice(2,4); 41 | board.move(cpuMove); 42 | game.load(data.fen); 43 | } 44 | }); 45 | 46 | }; 47 | 48 | // update the board position after the piece snap 49 | // for castling, en passant, pawn promotion 50 | var onSnapEnd = function() { 51 | board.position(game.fen()); 52 | }; 53 | 54 | var updateStatus = function() { 55 | var status = ''; 56 | 57 | var moveColor = 'White'; 58 | if (game.turn() === 'b') { 59 | moveColor = 'Black'; 60 | } 61 | 62 | // checkmate? 63 | if (game.in_checkmate() === true) { 64 | status = 'Game over, ' + moveColor + ' is in checkmate.'; 65 | } 66 | 67 | // draw? 68 | else if (game.in_draw() === true) { 69 | status = 'Game over, drawn position'; 70 | } 71 | 72 | // game still on 73 | else { 74 | status = moveColor + ' to move'; 75 | 76 | // check? 77 | if (game.in_check() === true) { 78 | status += ', ' + moveColor + ' is in check'; 79 | } 80 | } 81 | 82 | statusEl.html(status); 83 | fenEl.html(game.fen()); 84 | pgnEl.html(game.pgn()); 85 | }; 86 | 87 | var cfg = { 88 | draggable: true, 89 | position: 'start', 90 | onDragStart: onDragStart, 91 | onDrop: onDrop, 92 | onSnapEnd: onSnapEnd 93 | }; 94 | board = ChessBoard('board', cfg); 95 | 96 | updateStatus(); 97 | // }); 98 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chess-AI 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test_helpers.py: -------------------------------------------------------------------------------- 1 | from node import Node 2 | 3 | def heuristic_gen(list): 4 | for num in list: 5 | yield num 6 | yield "Done generating" 7 | 8 | def get_successors(n=3): 9 | successors = [] 10 | for i in range(0, n): 11 | successors.append(Node()) 12 | return successors 13 | 14 | if __name__ == "__main__": 15 | import unittest 16 | class Test_heuristic(unittest.TestCase): 17 | 18 | def test_heuristic(self): 19 | list_a = [1,2,3] 20 | x = heuristic_gen(list_a) 21 | self.assertEqual(next(x), 1) 22 | self.assertEqual(next(x), 2) 23 | self.assertEqual(next(x), 3) 24 | self.assertEqual(next(x), "Done generating") 25 | 26 | class Test_get_successors(unittest.TestCase): 27 | 28 | def test_get_successors(self): 29 | node_check = all(isinstance(successor, Node) for successor in get_successors(5)) 30 | self.assertEqual(len(get_successors()), 3, "Should handle defaults") 31 | self.assertEqual(len(get_successors(0)), 0, "Should return 0") 32 | self.assertEqual(node_check, True, "Should print a list of 5 nodes") 33 | 34 | unittest.main() 35 | --------------------------------------------------------------------------------