├── tests ├── __init__.py ├── test_core │ ├── __init__.py │ ├── test_algebraic │ │ ├── __init__.py │ │ ├── test_move.py │ │ ├── test_location.py │ │ └── test_converter.py │ ├── test_color.py │ └── test_board.py └── test_pieces │ ├── __init__.py │ ├── test_knight.py │ ├── test_bishop.py │ ├── test_king.py │ └── test_pawn.py ├── setup.cfg ├── requirements.txt ├── chess_py ├── game │ ├── __init__.py │ ├── game_state.py │ ├── interface.py │ └── game.py ├── players │ ├── __init__.py │ ├── human.py │ └── player.py ├── core │ ├── algebraic │ │ ├── __init__.py │ │ ├── notation_const.py │ │ ├── move.py │ │ ├── location.py │ │ └── converter.py │ ├── __init__.py │ ├── color.py │ └── board.py ├── __init__.py └── pieces │ ├── __init__.py │ ├── queen.py │ ├── bishop.py │ ├── piece_const.py │ ├── rook.py │ ├── knight.py │ ├── piece.py │ ├── king.py │ └── pawn.py ├── docs ├── chess_py.rst ├── chess_py.players.rst ├── chess_py.core.rst ├── chess_py.game.rst ├── index.rst ├── chess_py.core.algebraic.rst ├── chess_py.pieces.rst ├── make.bat ├── Makefile ├── conf.py └── UCIprotocol.md ├── .gitignore ├── .travis.yml ├── main.py ├── setup.py ├── LICENSE └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_pieces/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /tests/test_core/test_algebraic/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | twine 3 | pypandoc 4 | pandoc 5 | -------------------------------------------------------------------------------- /chess_py/game/__init__.py: -------------------------------------------------------------------------------- 1 | from .game import Game 2 | from . import game_state 3 | 4 | __all__ = ['Game', 'game_state'] 5 | -------------------------------------------------------------------------------- /chess_py/players/__init__.py: -------------------------------------------------------------------------------- 1 | from chess_py.players.human import Human 2 | from chess_py.players.player import Player 3 | 4 | __all__ = ['Human', 'Player'] 5 | -------------------------------------------------------------------------------- /chess_py/core/algebraic/__init__.py: -------------------------------------------------------------------------------- 1 | from .location import Location, Direction 2 | from .move import Move 3 | 4 | __all__ = ['converter', 'Location', 'Move', 'notation_const'] 5 | -------------------------------------------------------------------------------- /docs/chess_py.rst: -------------------------------------------------------------------------------- 1 | chess_py package 2 | ================ 3 | 4 | .. toctree:: 5 | 6 | chess_py.core 7 | chess_py.game 8 | chess_py.pieces 9 | chess_py.players 10 | -------------------------------------------------------------------------------- /chess_py/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import * 2 | from .game import * 3 | from .pieces import * 4 | from .players import * 5 | 6 | __all__ = core.__all__ + game.__all__ + pieces.__all__ + players.__all__ 7 | -------------------------------------------------------------------------------- /chess_py/core/__init__.py: -------------------------------------------------------------------------------- 1 | from . import algebraic, color 2 | from .algebraic import Move, Location 3 | from .algebraic import converter, notation_const 4 | from .board import Board 5 | 6 | __all__ = ['Board', 'color'] + algebraic.__all__ 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | /build/ 3 | /chess_py.egg-info/ 4 | /dist/ 5 | /docs/_build/ 6 | .idea/ 7 | __pycache__/ 8 | docs/_static/ 9 | docs/_templates/ 10 | /gh-pages/ 11 | /venv/ 12 | *.pyc 13 | *.DS_Store 14 | .pytest_cache 15 | pandoc-*.* 16 | -------------------------------------------------------------------------------- /tests/test_core/test_color.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from chess_py import color 4 | 5 | 6 | class TestColor(TestCase): 7 | 8 | def test_opponent(self): 9 | self.assertEqual(-color.white, color.black) 10 | self.assertEqual(-color.black, color.white) 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.2" 5 | - "3.3" 6 | - "3.4" 7 | - "3.5" 8 | - "3.6" 9 | # command to install dependencies 10 | install: python setup.py install 11 | install: pip install -r requirements.txt 12 | 13 | # command to run tests 14 | script: 15 | - py.test 16 | 17 | branches: 18 | only: 19 | - master -------------------------------------------------------------------------------- /chess_py/pieces/__init__.py: -------------------------------------------------------------------------------- 1 | from .bishop import Bishop 2 | from .king import King 3 | from .knight import Knight 4 | from .pawn import Pawn 5 | from .piece import Piece 6 | from .queen import Queen 7 | from .rook import Rook 8 | from .piece_const import PieceValues 9 | 10 | __all__ = ['Bishop', 'King', 'Knight', 'Pawn', 'Piece', 'piece_const', 'Queen', 'Rook'] 11 | -------------------------------------------------------------------------------- /docs/chess_py.players.rst: -------------------------------------------------------------------------------- 1 | chess_py.players package 2 | ======================== 3 | 4 | chess_py.players.human module 5 | ----------------------------- 6 | 7 | .. automodule:: chess_py.players.human 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | chess_py.players.player module 13 | ------------------------------ 14 | 15 | .. automodule:: chess_py.players.player 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | -------------------------------------------------------------------------------- /chess_py/core/algebraic/notation_const.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Class stores integer values for various types of moves in algebraic notation. 5 | 6 | Copyright © 2016 Aubhro Sengupta. All rights reserved. 7 | """ 8 | 9 | 10 | MOVEMENT = 0 11 | 12 | CAPTURE = 1 13 | 14 | KING_SIDE_CASTLE = 2 15 | 16 | QUEEN_SIDE_CASTLE = 3 17 | 18 | EN_PASSANT = 4 19 | 20 | PROMOTE = 5 21 | 22 | CAPTURE_AND_PROMOTE = 6 23 | 24 | NOT_IMPLEMENTED = 7 25 | 26 | LONG_ALG = 8 27 | -------------------------------------------------------------------------------- /docs/chess_py.core.rst: -------------------------------------------------------------------------------- 1 | chess_py.core package 2 | ===================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | chess_py.core.algebraic 10 | 11 | chess_py.core.board module 12 | -------------------------- 13 | 14 | .. automodule:: chess_py.core.board 15 | :members: 16 | :undoc-members: 17 | :show-inheritance: 18 | 19 | chess_py.core.color module 20 | -------------------------- 21 | 22 | .. automodule:: chess_py.core.color 23 | :members: 24 | :undoc-members: 25 | :show-inheritance: 26 | 27 | -------------------------------------------------------------------------------- /tests/test_pieces/test_knight.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from chess_py import color, Location, Move, Board, Knight 3 | 4 | 5 | class TestKnight(TestCase): 6 | def setUp(self): 7 | self.empty_pos = Board([[None for _ in range(8)] for _ in range(8)]) 8 | 9 | def test_possible_moves(self): 10 | self.empty_pos.place_piece_at_square(Knight(color.white, Location.from_string("e4")), Location.from_string("e4")) 11 | knight = self.empty_pos.piece_at_square(Location.from_string("e4")) 12 | 13 | moves = knight.possible_moves(self.empty_pos) 14 | self.assertEqual(len(list(moves)), 8) 15 | -------------------------------------------------------------------------------- /docs/chess_py.game.rst: -------------------------------------------------------------------------------- 1 | chess_py.game package 2 | ===================== 3 | 4 | chess_py.game.game module 5 | ------------------------- 6 | 7 | .. automodule:: chess_py.game.game 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | chess_py.game.game_state module 13 | ------------------------------- 14 | 15 | .. automodule:: chess_py.game.game_state 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | chess_py.game.interface module 21 | ------------------------------ 22 | 23 | .. automodule:: chess_py.game.interface 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. chess_py documentation master file, created by 2 | sphinx-quickstart on Sat Sep 17 16:32:26 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to chess_py's documentation! 7 | ==================================== 8 | 9 | :author: Aubhro Sengupta 10 | :project: https://github.com/LordDarkula/chess_py 11 | :PyGotham Talk: https://2016.pygotham.org/talks/324/abstractions-and-building/ 12 | 13 | Contents: 14 | 15 | .. toctree:: 16 | :maxdepth: 4 17 | 18 | chess_py 19 | 20 | 21 | Indices and tables 22 | ================== 23 | 24 | * :ref:`genindex` 25 | * :ref:`modindex` 26 | * :ref:`search` 27 | 28 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Chess playing program 5 | Everything starts here 6 | 7 | 8 | 8 ║♜♞♝♛♚♝♞♜ 9 | 7 ║♟♟♟♟♟♟♟♟ 10 | 6 ║………………………………… 11 | 5 ║………………………………… 12 | 4 ║………………………………… 13 | 3 ║………………………………… 14 | 2 ║♙♙♙♙♙♙♙♙ 15 | 1 ║♖♘♗♕♔♗♘♖ 16 | --╚═══════════════ 17 | ——-a b c d e f g h 18 | 19 | Copyright © 2016 Aubhro Sengupta. All rights reserved. 20 | """ 21 | 22 | from chess_py import color, Human, Game 23 | 24 | 25 | def main(): 26 | """ 27 | Main method 28 | """ 29 | print("Creating a new game...") 30 | 31 | new_game = Game(Human(color.white), Human(color.black)) 32 | result = new_game.play() 33 | 34 | print("Result is ", result) 35 | 36 | if __name__ == "__main__": 37 | main() 38 | -------------------------------------------------------------------------------- /chess_py/game/game_state.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Static methods which check to see if 5 | game is over, and if a King is checkmated. 6 | 7 | Copyright © 2016 Aubhro Sengupta. All rights reserved. 8 | """ 9 | 10 | from ..core import color 11 | 12 | 13 | def no_moves(position): 14 | """ 15 | Finds if the game is over. 16 | 17 | :type: position: Board 18 | :rtype: bool 19 | """ 20 | return position.no_moves(color.white) \ 21 | or position.no_moves(color.black) 22 | 23 | 24 | def is_checkmate(position, input_color): 25 | """ 26 | Finds if particular King is checkmated. 27 | 28 | :type: position: Board 29 | :type: input_color: Color 30 | :rtype: bool 31 | """ 32 | return position.no_moves(input_color) and \ 33 | position.get_king(input_color).in_check(position) 34 | -------------------------------------------------------------------------------- /docs/chess_py.core.algebraic.rst: -------------------------------------------------------------------------------- 1 | chess_py.core.algebraic package 2 | =============================== 3 | 4 | chess_py.core.algebraic.converter module 5 | ---------------------------------------- 6 | 7 | .. automodule:: chess_py.core.algebraic.converter 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | chess_py.core.algebraic.location module 13 | --------------------------------------- 14 | 15 | .. automodule:: chess_py.core.algebraic.location 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | chess_py.core.algebraic.move module 21 | ----------------------------------- 22 | 23 | .. automodule:: chess_py.core.algebraic.move 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | chess_py.core.algebraic.notation_const module 29 | --------------------------------------------- 30 | 31 | .. automodule:: chess_py.core.algebraic.notation_const 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import setuptools 4 | from setuptools import setup 5 | try: 6 | import pypandoc 7 | # pypandoc.download_pandoc() 8 | read_md = lambda f: pypandoc.convert(f, 'rst') 9 | except ImportError: 10 | print("warning: pypandoc module not found, could not convert Markdown to RST") 11 | read_md = lambda f: open(f, 'r').read() 12 | 13 | 14 | setup( 15 | name='chess_py', 16 | version='3.3.0', 17 | description='Python chess client', 18 | long_description=read_md("README.md"), 19 | platforms='MacOS X, Windows, Linux', 20 | classifiers=[ 21 | 'Development Status :: 5 - Production/Stable', 22 | 'Intended Audience :: Developers', 23 | 'License :: OSI Approved :: MIT License', 24 | 'Programming Language :: Python :: 3.6', 25 | 'Programming Language :: Python :: 2.7', 26 | ], 27 | author='Aubhro Sengupta', 28 | author_email='aubhrosengupta@gmail.com', 29 | url='https://github.com/LordDarkula/chess_py', 30 | license='MIT', 31 | packages=setuptools.find_packages() 32 | ) 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright © 2016-2018 Aubhro Sengupta. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /chess_py/pieces/queen.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Class stores Queen on the board 5 | 6 | | rank 7 | | 7 8 ║♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜ 8 | | 6 7 ║♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟ 9 | | 5 6 ║… … … … … … … … 10 | | 4 5 ║… … … … … … … … 11 | | 3 4 ║… … … … … … … … 12 | | 2 3 ║… … … … … … … … 13 | | 1 2 ║♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙ 14 | | 0 1 ║♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖ 15 | | ----╚═══════════════ 16 | | ——---a b c d e f g h 17 | | -----0 1 2 3 4 5 6 7 18 | | ------file 19 | 20 | | Copyright © 2016 Aubhro Sengupta. All rights reserved. 21 | """ 22 | 23 | import itertools 24 | 25 | from .piece import Piece 26 | from .rook import Rook 27 | from .bishop import Bishop 28 | from ..core import color 29 | 30 | 31 | class Queen(Bishop, Piece): 32 | def __init__(self, input_color, location): 33 | Piece.__init__(self, input_color, location) 34 | 35 | def _symbols(self): 36 | return {color.white: "♛", color.black: "♕"} 37 | 38 | def __str__(self): 39 | return "Q" 40 | 41 | def possible_moves(self, position): 42 | for move in itertools.chain(Rook.possible_moves(self, position), 43 | Bishop.possible_moves(self, position)): 44 | yield move 45 | -------------------------------------------------------------------------------- /chess_py/players/human.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Included class for human interaction via console. 5 | Prints position and takes move written in algebraic notation as string input 6 | 7 | Copyright © 2016 Aubhro Sengupta. All rights reserved. 8 | """ 9 | 10 | from chess_py.core.algebraic import converter 11 | from chess_py.players.player import Player 12 | 13 | try: 14 | input = raw_input 15 | except NameError: 16 | pass 17 | 18 | 19 | class Human(Player): 20 | def __init__(self, input_color): 21 | """ 22 | Creates interface for human player. 23 | 24 | :type: input_color: Color 25 | """ 26 | super(Human, self).__init__(input_color) 27 | 28 | def generate_move(self, position): 29 | """ 30 | Returns valid and legal move given position 31 | 32 | :type: position: Board 33 | :rtype: Move 34 | """ 35 | while True: 36 | print(position) 37 | raw = input(str(self.color) + "\'s move \n") 38 | move = converter.short_alg(raw, self.color, position) 39 | 40 | if move is None: 41 | continue 42 | 43 | return move 44 | -------------------------------------------------------------------------------- /chess_py/pieces/bishop.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Class stores Bishop on the board 5 | 6 | | rank 7 | | 7 8 ║♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜ 8 | | 6 7 ║♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟ 9 | | 5 6 ║… … … … … … … … 10 | | 4 5 ║… … … … … … … … 11 | | 3 4 ║… … … … … … … … 12 | | 2 3 ║… … … … … … … … 13 | | 1 2 ║♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙ 14 | | 0 1 ║♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖ 15 | | ----╚═══════════════ 16 | | ——---a b c d e f g h 17 | | -----0 1 2 3 4 5 6 7 18 | | ------file 19 | 20 | | Copyright © 2016 Aubhro Sengupta. All rights reserved. 21 | """ 22 | 23 | import itertools 24 | 25 | from .piece import Piece 26 | from .rook import Rook 27 | from ..core import color 28 | 29 | 30 | class Bishop(Rook, Piece): 31 | def __init__(self, input_color, location): 32 | """ 33 | Creates Bishop object that can be compared to and return possible moves 34 | 35 | :type: input_color: Color 36 | """ 37 | Piece.__init__(self, input_color, location) 38 | 39 | def _symbols(self): 40 | return {color.white: "♝", color.black: "♗"} 41 | 42 | def __str__(self): 43 | return "B" 44 | 45 | def possible_moves(self, position): 46 | """ 47 | Returns all possible bishop moves. 48 | 49 | :type: position: Board 50 | :rtype: list 51 | """ 52 | 53 | for move in itertools.chain(*[self.moves_in_direction(fn, position) for fn in self.diag_fn]): 54 | yield move 55 | -------------------------------------------------------------------------------- /chess_py/players/player.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Parent class of chess engines built on chess_py. This class must be inherited 5 | and abstract method ``generate_move(self, position) must be implemented.``. 6 | """ 7 | 8 | from abc import ABCMeta, abstractmethod 9 | from pip._vendor.distlib.compat import raw_input 10 | import sys 11 | 12 | 13 | class Player: 14 | __metaclass__ = ABCMeta 15 | 16 | def __init__(self, input_color): 17 | """ 18 | Creates interface for base player. 19 | 20 | :type: input_color: Color 21 | """ 22 | self.color = input_color 23 | 24 | @abstractmethod 25 | def generate_move(self, position): 26 | """ 27 | Must be implemented by classes that extend ``Player``. 28 | Must return object of type ``Move``. 29 | 30 | :type: position: Board 31 | :rtype: Move 32 | """ 33 | pass 34 | 35 | @staticmethod 36 | def getUCI(): 37 | """ 38 | Internal method used by ``Interface`` 39 | to read UCI commands from external GUI. 40 | 41 | :rtype: str 42 | """ 43 | return sys.stdin.read() 44 | 45 | @staticmethod 46 | def setUCI(command): 47 | """ 48 | Internal method used by ``Interface`` 49 | to write UCI commands to the console so they 50 | can be read by external GUI. 51 | 52 | :type: command: str 53 | """ 54 | sys.stdout.write(command) 55 | 56 | -------------------------------------------------------------------------------- /docs/chess_py.pieces.rst: -------------------------------------------------------------------------------- 1 | chess_py.pieces package 2 | ======================= 3 | 4 | chess_py.pieces.bishop module 5 | ----------------------------- 6 | 7 | .. automodule:: chess_py.pieces.bishop 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | chess_py.pieces.king module 13 | --------------------------- 14 | 15 | .. automodule:: chess_py.pieces.king 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | chess_py.pieces.knight module 21 | ----------------------------- 22 | 23 | .. automodule:: chess_py.pieces.knight 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | chess_py.pieces.pawn module 29 | --------------------------- 30 | 31 | .. automodule:: chess_py.pieces.pawn 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | chess_py.pieces.piece module 37 | ---------------------------- 38 | 39 | .. automodule:: chess_py.pieces.piece 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | chess_py.pieces.piece_const module 45 | ---------------------------------- 46 | 47 | .. automodule:: chess_py.pieces.piece_const 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | chess_py.pieces.queen module 53 | ---------------------------- 54 | 55 | .. automodule:: chess_py.pieces.queen 56 | :members: 57 | :undoc-members: 58 | :show-inheritance: 59 | 60 | chess_py.pieces.rook module 61 | --------------------------- 62 | 63 | .. automodule:: chess_py.pieces.rook 64 | :members: 65 | :undoc-members: 66 | :show-inheritance: 67 | 68 | -------------------------------------------------------------------------------- /tests/test_pieces/test_bishop.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from chess_py import Board, Location, converter 4 | 5 | 6 | class TestBishop(TestCase): 7 | def setUp(self): 8 | self.board = Board.init_default() 9 | 10 | def test_no_possible_moves(self): 11 | self.assertEqual(len(list(self.board.piece_at_square(Location.from_string("c1")) 12 | .possible_moves(self.board))), 0) 13 | 14 | def test_left_diagonal(self): 15 | self.board.update(converter.long_alg("b2b3", self.board)) 16 | moves = list(self.board.piece_at_square(Location.from_string("c1")).possible_moves(self.board)) 17 | 18 | self.assertEqual(len(moves), 2) 19 | self.assertEqual(moves[0], converter.long_alg("c1b2", self.board)) 20 | self.assertEqual(moves[1], converter.long_alg("c1a3", self.board)) 21 | 22 | def test_capture(self): 23 | self.board.move_piece(Location.from_string("g1"), Location.from_string("g7")) 24 | moves = list(self.board.piece_at_square(Location.from_string("f8")).possible_moves(self.board)) 25 | 26 | self.assertEqual(len(moves), 1) 27 | self.assertEqual(moves[0], converter.long_alg("f8g7", self.board)) 28 | 29 | def test_possible_moves(self): 30 | self.board.move_piece(Location.from_string("c1"), Location.from_string("d4")) 31 | test_moves = self.board.piece_at_square(Location.from_string("d4")).possible_moves(self.board) 32 | real_moves = ["d4e5", "d4f6", "d4g7", "d4c5", "d4b6", "d4a7", "d4e3", "d4c3"] 33 | 34 | for i, move in enumerate(test_moves): 35 | self.assertEqual(str(move), real_moves[i]) 36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/test_core/test_algebraic/test_move.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from chess_py import Move, Location, notation_const, Pawn, color, Queen 4 | 5 | 6 | class TestMove(unittest.TestCase): 7 | def setUp(self): 8 | self.white_pawn = Pawn(color.white, Location(1, 0)) 9 | self.black_pawn = Pawn(color.black, Location(1, 0)) 10 | 11 | self.white_pawn_move = Move(Location(2, 0), 12 | piece=self.white_pawn, 13 | status=notation_const.MOVEMENT, 14 | start_loc=Location(1, 0)) 15 | 16 | self.start_specified = Move(Location(2, 0), 17 | piece=self.white_pawn, 18 | status=notation_const.MOVEMENT, 19 | start_loc=Location(3, 5)) 20 | 21 | def testStr(self): 22 | self.assertEqual(str(self.start_specified), "f4a3") 23 | 24 | self.white_pawn_move = Move(Location(7, 0), 25 | piece=self.white_pawn, 26 | status=notation_const.MOVEMENT, 27 | start_loc=Location(6, 0), 28 | promoted_to_piece=Queen(color.white, 29 | Location(7, 0))) 30 | 31 | def testEquals(self): 32 | self.assertEqual(self.white_pawn_move, Move(end_loc=Location(2, 0), 33 | piece=self.white_pawn, 34 | status=notation_const.MOVEMENT, 35 | start_loc=Location(1, 0))) 36 | 37 | 38 | if __name__ == '__main__': 39 | unittest.main() 40 | -------------------------------------------------------------------------------- /tests/test_core/test_algebraic/test_location.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from chess_py import Location 4 | 5 | 6 | class TestLocation(unittest.TestCase): 7 | 8 | def testEquals(self): 9 | self.assertEqual(Location(2, 3), Location(2, 3)) 10 | self.assertEqual(Location(7, 6), Location(7, 6)) 11 | self.assertNotEqual(Location(4, 5), Location(5, 4)) 12 | 13 | def testStr(self): 14 | self.assertEqual(str(Location(3, 4)), "e4") 15 | self.assertEqual(str(Location(0, 0)), "a1") 16 | self.assertEqual(str(Location(7, 7)), "h8") 17 | 18 | def testShiftUp(self): 19 | self.assertEqual(Location(3, 4).shift_up(), Location(4, 4)) 20 | self.assertEqual(Location(0, 2).shift_up(), Location(1, 2)) 21 | 22 | def testShiftDown(self): 23 | self.assertEqual(Location(3, 4).shift_down(), Location(2, 4)) 24 | self.assertEqual(Location(1, 2).shift_down(), Location(0, 2)) 25 | 26 | def testShiftRight(self): 27 | self.assertEqual(Location(3, 4).shift_right(), Location(3, 5)) 28 | self.assertEqual(Location(0, 2).shift_right(), Location(0, 3)) 29 | 30 | def testShiftLeft(self): 31 | self.assertEqual(Location(3, 4).shift_left(), Location(3, 3)) 32 | self.assertEqual(Location(0, 2).shift_left(), Location(0, 1)) 33 | 34 | def testShiftUpRight(self): 35 | self.assertEqual(Location(3, 4).shift_up_right(), Location(4, 5)) 36 | 37 | def testShiftUpLeft(self): 38 | self.assertEqual(Location(1, 2).shift_up_left(), Location(2, 1)) 39 | 40 | def testShiftDownRight(self): 41 | self.assertEqual(Location(5, 3).shift_down_right(), Location(4, 4)) 42 | 43 | def testShiftDownLeft(self): 44 | self.assertEqual(Location(1, 1).shift_down_left(), Location(0, 0)) 45 | 46 | if __name__ == '__main__': 47 | unittest.main() 48 | -------------------------------------------------------------------------------- /chess_py/core/color.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Easy way to access bool values for black and white without directly 5 | typing True or False. 6 | 7 | Copyright © 2016 Aubhro Sengupta. All rights reserved. 8 | """ 9 | 10 | 11 | class Color: 12 | 13 | _color_dict = { 14 | 'white': True, 15 | 'black': False, 16 | } 17 | 18 | def __init__(self, raw): 19 | """ 20 | Initializes new color using a boolean 21 | True is white and False is black 22 | 23 | :type: raw: bool 24 | """ 25 | self._bool = raw 26 | 27 | @classmethod 28 | def from_string(cls, string): 29 | """ 30 | Converts string "white" or "black" into 31 | corresponding color 32 | 33 | :type: string: str 34 | :rtype: Color 35 | """ 36 | return cls(cls._color_dict[string]) 37 | 38 | def __repr__(self): 39 | return "color.{}".format(str(self)) 40 | 41 | def __str__(self): 42 | if self._bool: 43 | return "white" 44 | else: 45 | return "black" 46 | 47 | def __bool__(self): 48 | return self._bool 49 | 50 | def __int__(self): 51 | if self._bool: 52 | return 1 53 | else: 54 | return -1 55 | 56 | def __key(self): 57 | return bool(self) 58 | 59 | def __hash__(self): 60 | return hash(self.__key()) 61 | 62 | def __neg__(self): 63 | return Color(not self._bool) 64 | 65 | def __eq__(self, other): 66 | """ 67 | Finds out this color is the same as another color. 68 | 69 | :type: other: Color 70 | :rtype: bool 71 | """ 72 | return bool(self) == bool(other) 73 | 74 | def __ne__(self, other): 75 | return not self.__eq__(other) 76 | 77 | 78 | white = Color(True) 79 | black = Color(False) 80 | -------------------------------------------------------------------------------- /chess_py/pieces/piece_const.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Constants for piece values in the game 5 | 6 | Copyright © 2016 Aubhro Sengupta. All rights reserved. 7 | """ 8 | 9 | from .bishop import Bishop 10 | from .pawn import Pawn 11 | from .queen import Queen 12 | from .rook import Rook 13 | from .knight import Knight 14 | from .king import King 15 | 16 | 17 | class PieceValues: 18 | def __init__(self): 19 | self.PAWN_VALUE = 1 20 | self.KNIGHT_VALUE = 3 21 | self.BISHOP_VALUE = 3.5 22 | self.ROOK_VALUE = 5 23 | self.QUEEN_VALUE = 9 24 | self.KING_VALUE = 999 25 | 26 | @classmethod 27 | def init_manual(cls, pawn_value, knight_value, bishop_value, rook_value, queen_value, king_value): 28 | """ 29 | Manual init method for external piece values 30 | 31 | :type: PAWN_VALUE: int 32 | :type: KNIGHT_VALUE: int 33 | :type: BISHOP_VALUE: int 34 | :type: ROOK_VALUE: int 35 | :type: QUEEN_VALUE: int 36 | """ 37 | piece_values = cls() 38 | piece_values.PAWN_VALUE = pawn_value 39 | piece_values.KNIGHT_VALUE = knight_value 40 | piece_values.BISHOP_VALUE = bishop_value 41 | piece_values.ROOK_VALUE = rook_value 42 | piece_values.QUEEN_VALUE = queen_value 43 | piece_values.KING_VALUE = king_value 44 | return piece_values 45 | 46 | def val(self, piece, ref_color): 47 | """ 48 | Finds value of ``Piece`` 49 | 50 | :type: piece: Piece 51 | :type: ref_color: Color 52 | :rtype: int 53 | """ 54 | if piece is None: 55 | return 0 56 | 57 | if ref_color == piece.color: 58 | const = 1 59 | else: 60 | const = -1 61 | 62 | if isinstance(piece, Pawn): 63 | return self.PAWN_VALUE * const 64 | elif isinstance(piece, Queen): 65 | return self.QUEEN_VALUE * const 66 | elif isinstance(piece, Bishop): 67 | return self.BISHOP_VALUE * const 68 | elif isinstance(piece, Rook): 69 | return self.ROOK_VALUE * const 70 | elif isinstance(piece, Knight): 71 | return self.KNIGHT_VALUE * const 72 | elif isinstance(piece, King): 73 | return self.KING_VALUE * const 74 | return 0 75 | -------------------------------------------------------------------------------- /chess_py/pieces/rook.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Class stores Rook on the board 5 | 6 | | rank 7 | | 7 8 ║♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜ 8 | | 6 7 ║♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟ 9 | | 5 6 ║… … … … … … … … 10 | | 4 5 ║… … … … … … … … 11 | | 3 4 ║… … … … … … … … 12 | | 2 3 ║… … … … … … … … 13 | | 1 2 ║♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙ 14 | | 0 1 ║♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖ 15 | | ----╚═══════════════ 16 | | ——---a b c d e f g h 17 | | -----0 1 2 3 4 5 6 7 18 | | ------file 19 | 20 | | Copyright © 2016 Aubhro Sengupta. All rights reserved. 21 | """ 22 | 23 | import itertools 24 | 25 | from .piece import Piece 26 | from ..core.algebraic import notation_const 27 | from ..core import color 28 | 29 | 30 | class Rook(Piece): 31 | def __init__(self, input_color, location): 32 | """ 33 | Initializes a rook that is capable of being compared to another rook, 34 | and returning a list of possible moves. 35 | 36 | :type: input_color: Color 37 | :type: location: Location 38 | """ 39 | super(Rook, self).__init__(input_color, location) 40 | self.has_moved = False 41 | 42 | def _symbols(self): 43 | return {color.white: "♜", color.black: "♖"} 44 | 45 | def __str__(self): 46 | return "R" 47 | 48 | def moves_in_direction(self, direction, position): 49 | """ 50 | Finds moves in a given direction 51 | 52 | :type: direction: lambda 53 | :type: position: Board 54 | :rtype: list 55 | """ 56 | current_square = self.location 57 | 58 | while True: 59 | try: 60 | current_square = direction(current_square) 61 | except IndexError: 62 | return 63 | 64 | if self.contains_opposite_color_piece(current_square, position): 65 | yield self.create_move(current_square, notation_const.CAPTURE) 66 | 67 | if not position.is_square_empty(current_square): 68 | return 69 | 70 | yield self.create_move(current_square, notation_const.MOVEMENT) 71 | 72 | def possible_moves(self, position): 73 | """ 74 | Returns all possible rook moves. 75 | 76 | :type: position: Board 77 | :rtype: list 78 | """ 79 | for move in itertools.chain(*[self.moves_in_direction(fn, position) for fn in self.cross_fn]): 80 | yield move 81 | -------------------------------------------------------------------------------- /chess_py/game/interface.py: -------------------------------------------------------------------------------- 1 | 2 | from multiprocessing import Process 3 | 4 | from ..core import Board 5 | 6 | 7 | class UCI: 8 | def __init__(self, player, engine_name, author): 9 | """ 10 | 11 | :type: player: Player 12 | """ 13 | self.player = player 14 | self.engine = engine_name 15 | self.author = author 16 | self.position = Board.init_default() 17 | self.running = True 18 | self.latest_input = "" 19 | 20 | def play(self): 21 | self.runInParallel(lambda: self.read(), lambda: self.set_up()) 22 | self.set_up() 23 | 24 | def set_up(self): 25 | self.wait_for("uci") 26 | 27 | self.write("id name " + self.engine) 28 | self.write("id author " + self.author) 29 | 30 | self.write("uciok") 31 | 32 | self.wait_for("ucinewgame") 33 | self.start_game() 34 | 35 | def start_game(self): 36 | if self.latest_input == "isready": 37 | self.write("readyok") 38 | 39 | self.wait_for("") 40 | 41 | def wait_for(self, command): 42 | """ 43 | Waits until ``self.latest_input`` is a certain command 44 | :type command: 45 | """ 46 | while self.latest_input != command: 47 | pass 48 | 49 | def read(self): 50 | """ 51 | Continuously reads from the console and updates 52 | ``self.latest_input`` with latest command. Runs 53 | as a side process at all times so main process 54 | has the most current information form the console 55 | accessed through ``self.latest_input``. 56 | """ 57 | while self.running: 58 | self.latest_input = self.player.getUCI() 59 | 60 | def write(self, command): 61 | """ 62 | Writes to the console given the command. 63 | Called by the main process when it needs to 64 | send commands to the console. 65 | 66 | :type: command: str 67 | """ 68 | self.player.setUCI(command) 69 | 70 | def runInParallel(*fns): 71 | """ 72 | Runs multiple processes in parallel. 73 | 74 | :type: fns: def 75 | """ 76 | proc = [] 77 | for fn in fns: 78 | p = Process(target=fn) 79 | p.start() 80 | proc.append(p) 81 | for p in proc: 82 | p.join() 83 | 84 | -------------------------------------------------------------------------------- /chess_py/pieces/knight.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Class stores Knight on the board 5 | 6 | | rank 7 | | 7 8 ║♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜ 8 | | 6 7 ║♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟ 9 | | 5 6 ║… … … … … … … … 10 | | 4 5 ║… … … … … … … … 11 | | 3 4 ║… … … … … … … … 12 | | 2 3 ║… … … … … … … … 13 | | 1 2 ║♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙ 14 | | 0 1 ║♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖ 15 | | ----╚═══════════════ 16 | | ——---a b c d e f g h 17 | | -----0 1 2 3 4 5 6 7 18 | | ------file 19 | 20 | | Copyright © 2016 Aubhro Sengupta. All rights reserved. 21 | """ 22 | 23 | from ..core.algebraic import notation_const 24 | from ..pieces.piece import Piece 25 | from ..core.algebraic.move import Move 26 | from ..core import color 27 | 28 | 29 | class Knight(Piece): 30 | def __init__(self, input_color, location): 31 | """ 32 | Initializes Knight 33 | :type: input_color: Color 34 | :type: location Location 35 | """ 36 | super(Knight, self).__init__(input_color, location) 37 | 38 | def _symbols(self): 39 | return {color.white: "♞", color.black: "♘"} 40 | 41 | def __str__(self): 42 | return "N" 43 | 44 | @staticmethod 45 | def _rotate_direction_ninety_degrees(direction): 46 | if direction == 3: 47 | return 0, 2 48 | right_angles = [direction - 1, direction + 1] 49 | for index, angle in enumerate(right_angles): 50 | if angle == -1: 51 | right_angles[index] = 3 52 | elif angle == 4: 53 | right_angles[index] = 0 54 | 55 | return right_angles 56 | 57 | def possible_moves(self, position): 58 | """ 59 | Finds all possible knight moves 60 | :type: position Board 61 | :rtype: list 62 | """ 63 | for direction in [0, 1, 2, 3]: 64 | angles = self._rotate_direction_ninety_degrees(direction) 65 | for angle in angles: 66 | try: 67 | end_loc = self.location.shift(angle).shift(direction).shift(direction) 68 | if position.is_square_empty(end_loc): 69 | status = notation_const.MOVEMENT 70 | elif not position.piece_at_square(end_loc).color == self.color: 71 | status = notation_const.CAPTURE 72 | else: 73 | continue 74 | 75 | yield Move(end_loc=end_loc, 76 | piece=self, 77 | status=status, 78 | start_loc=self.location) 79 | 80 | except IndexError: 81 | pass 82 | 83 | -------------------------------------------------------------------------------- /chess_py/pieces/piece.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Parent class for all pieces 5 | 6 | Copyright © 2016 Aubhro Sengupta. All rights reserved. 7 | """ 8 | 9 | from abc import ABCMeta, abstractmethod 10 | 11 | from ..core.algebraic.location import Location 12 | from ..core import color 13 | from ..core.color import Color 14 | 15 | 16 | class Piece: 17 | __metaclass__ = ABCMeta 18 | 19 | @abstractmethod 20 | def __init__(self, input_color, location): 21 | """ 22 | Initializes a piece that is capable of moving 23 | 24 | :type: input_color: Color 25 | :type: location: Location 26 | :type: white_symbol: str 27 | :type: black_symbol: str 28 | """ 29 | assert isinstance(input_color, Color) 30 | assert isinstance(location, Location) 31 | 32 | self.color = input_color 33 | self.location = location 34 | 35 | self.cross_fn = [lambda x: x.shift_up(), lambda x: x.shift_right(), 36 | lambda x: x.shift_down(), lambda x: x.shift_left()] 37 | 38 | self.diag_fn = [lambda x: x.shift_up_right(), lambda x: x.shift_up_left(), 39 | lambda x: x.shift_down_right(), lambda x: x.shift_down_left()] 40 | 41 | def __key(self): 42 | return self.color, self.location 43 | 44 | def __hash__(self): 45 | return hash(str(self)) 46 | 47 | def __eq__(self, other): 48 | """ 49 | Finds out if piece is the same type and color as self 50 | :type: other: Piece 51 | """ 52 | return type(other) is type(self) and other.color == self.color 53 | 54 | def __ne__(self, other): 55 | return not self.__eq__(other) 56 | 57 | def __repr__(self): 58 | return "{}({}, {})".format(self.__class__, self.color, self.location) 59 | 60 | @abstractmethod 61 | def __str__(self): 62 | pass 63 | 64 | @abstractmethod 65 | def _symbols(self): 66 | pass 67 | 68 | @abstractmethod 69 | def possible_moves(self, position): 70 | pass 71 | 72 | def __copy__(self): 73 | return self.__class__(self.color, self.location) 74 | 75 | @property 76 | def symbol(self): 77 | return self._symbols()[self.color] 78 | 79 | def contains_opposite_color_piece(self, square, position): 80 | """ 81 | Finds if square on the board is occupied by a ``Piece`` 82 | belonging to the opponent. 83 | 84 | :type: square: Location 85 | :type: position: Board 86 | :rtype: bool 87 | """ 88 | return not position.is_square_empty(square) and \ 89 | position.piece_at_square(square).color != self.color 90 | 91 | def create_move(self, end_loc, status): 92 | # TODO: fix circular imports 93 | from chess_py import Move 94 | return Move(end_loc=end_loc, 95 | piece=self, 96 | status=status, 97 | start_loc=self.location) 98 | -------------------------------------------------------------------------------- /chess_py/game/game.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Class that holds state of a game. 5 | 6 | Takes two subclasses of Player as defined 7 | in chess_py.players.player.Player, and calls 8 | ``generate_move(position)`` 9 | from each of the players and updates the board using 10 | each corresponding result. 11 | 12 | Start game using play(), which returns the result 13 | (1 - white wins, 0 - black wins, 0.5 - draw) 14 | when the game is finished. 15 | 16 | Copyright © 2016 Aubhro Sengupta. All rights reserved. 17 | """ 18 | 19 | import itertools 20 | 21 | from ..core.board import Board 22 | from ..core import color 23 | from . import game_state 24 | from ..core.algebraic.converter import make_legal 25 | 26 | 27 | class Game: 28 | def __init__(self, player_white, player_black): 29 | """ 30 | Creates new game given the players. 31 | 32 | :type: player_white: Player 33 | :type: player_black: Player 34 | """ 35 | self.player_white = player_white 36 | self.player_black = player_black 37 | self.position = Board.init_default() 38 | 39 | def play(self): 40 | """ 41 | Starts game and returns one of 3 results . 42 | Iterates between methods ``white_move()`` and 43 | ``black_move()`` until game ends. Each 44 | method calls the respective player's ``generate_move()`` 45 | method. 46 | 47 | :rtype: int 48 | """ 49 | colors = [lambda: self.white_move(), lambda: self.black_move()] 50 | colors = itertools.cycle(colors) 51 | 52 | while True: 53 | color_fn = next(colors) 54 | if game_state.no_moves(self.position): 55 | if self.position.get_king(color.white).in_check(self.position): 56 | return 1 57 | 58 | elif self.position.get_king(color.black).in_check(self.position): 59 | return 0 60 | 61 | else: 62 | return 0.5 63 | 64 | color_fn() 65 | 66 | def white_move(self): 67 | """ 68 | Calls the white player's ``generate_move()`` 69 | method and updates the board with the move returned. 70 | """ 71 | move = self.player_white.generate_move(self.position) 72 | move = make_legal(move, self.position) 73 | self.position.update(move) 74 | 75 | def black_move(self): 76 | """ 77 | Calls the black player's ``generate_move()`` 78 | method and updates the board with the move returned. 79 | """ 80 | move = self.player_black.generate_move(self.position) 81 | move = make_legal(move, self.position) 82 | self.position.update(move) 83 | 84 | def all_possible_moves(self, input_color): 85 | """ 86 | Finds all possible moves a particular player can 87 | play during a game. Calling this method is recommended over 88 | calling the ``all_possible_moves(input_color)`` 89 | from this ``Board`` directly. 90 | """ 91 | return self.position.all_possible_moves(input_color) 92 | -------------------------------------------------------------------------------- /chess_py/core/algebraic/move.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Class that stores chess moves. 5 | Destination, status and piece making move are required 6 | to initialize Move. 7 | 8 | :type: end_loc: Location 9 | :type: piece: Piece 10 | :type: status: int 11 | 12 | Copyright © 2016 Aubhro Sengupta. All rights reserved. 13 | """ 14 | 15 | from .location import Location 16 | 17 | 18 | class Move: 19 | def __init__(self, 20 | end_loc, 21 | piece, 22 | status, 23 | start_loc, 24 | promoted_to_piece=None): 25 | """ 26 | Constructor to create move using ``Location`` 27 | 28 | :type: end_loc: Location 29 | :type: piece: Piece 30 | :type: status: int 31 | """ 32 | self._end_loc = end_loc 33 | self._status = status 34 | self._piece = piece 35 | self._start_loc = start_loc 36 | self.color = piece.color 37 | self.promoted_to_piece = promoted_to_piece 38 | 39 | @property 40 | def end_loc(self): 41 | return self._end_loc 42 | 43 | @property 44 | def status(self): 45 | return self._status 46 | 47 | @property 48 | def piece(self): 49 | return self._piece 50 | 51 | def __key(self): 52 | return self.end_loc, \ 53 | self.piece, \ 54 | self.status, \ 55 | self.start_loc, \ 56 | self.promoted_to_piece 57 | 58 | def __hash__(self): 59 | return hash(self.__key()) 60 | 61 | def __eq__(self, other): 62 | """ 63 | Finds if move is same move as this one. 64 | :type: other: Move 65 | """ 66 | if not isinstance(other, self.__class__): 67 | raise TypeError("Cannot compare type {} with Move".format(type(other))) 68 | 69 | for index, item in enumerate(self.__key()): 70 | if not self._check_equals_or_none(item, other.__key()[index]): 71 | return False 72 | 73 | return True 74 | 75 | @staticmethod 76 | def _check_equals_or_none(var1, var2): 77 | """ 78 | If either is None then return True, 79 | otherwise compare them and return 80 | if they are equal. 81 | 82 | :type: var1: object 83 | :type: var2: object 84 | :rtype: bool 85 | """ 86 | if var1 is None or var2 is None: 87 | return True 88 | 89 | return var1 == var2 90 | 91 | def __ne__(self, other): 92 | return not self.__eq__(other) 93 | 94 | def __repr__(self): 95 | return "Move({})".format(self.__dict__) 96 | 97 | def __str__(self): 98 | """ 99 | Finds string representation in long algebraic notation 100 | 101 | :rtype: str 102 | """ 103 | move_str = str(self._start_loc) + str(self._end_loc) 104 | 105 | if self.promoted_to_piece is not None: 106 | move_str += str(self.promoted_to_piece) 107 | 108 | return move_str 109 | 110 | @property 111 | def start_loc(self): 112 | """ 113 | Finds start Location of move if specified. 114 | Otherwise throws an AttributeError 115 | 116 | :rtype: Location 117 | """ 118 | return self._start_loc 119 | 120 | def would_move_be_promotion(self): 121 | """ 122 | Finds if move from current location would be a promotion 123 | """ 124 | return (self._end_loc.rank == 0 and not self.color) or \ 125 | (self._end_loc.rank == 7 and self.color) 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chess_py 2 | 3 | ![Board](http://i.stack.imgur.com/yQaOq.png) 4 | 5 | [![Build Status](https://travis-ci.org/LordDarkula/chess_py.svg?branch=master)](https://travis-ci.org/LordDarkula/chess_py) 6 | [![Code Climate](https://codeclimate.com/github/LordDarkula/chess_py/badges/gpa.svg)](https://codeclimate.com/github/LordDarkula/chess_py) 7 | [![PyPI version](https://badge.fury.io/py/chess_py.svg)](https://pypi.python.org/pypi/chess_py) 8 | [![Python27](https://img.shields.io/badge/python-2.7-blue.svg)](https://www.python.org/download/releases/2.7/) 9 | [![Python35](https://img.shields.io/badge/python-3.5-blue.svg)](https://www.python.org/downloads/release/python-350/) 10 | [![License](https://img.shields.io/cocoapods/l/EasyQL.svg?style=flat)](https://github.com/LordDarkula/chess_py/blob/master/LICENSE) 11 | [![Twitter](https://img.shields.io/badge/twitter-@LordDarkula-blue.svg?style=flat)](http://twitter.com/LordDarkula) 12 | 13 | ## License 14 | chess_py is available under the MIT license. See the [LICENSE](https://github.com/LordDarkula/chess_py/blob/master/LICENSE) file for more info. 15 | Copyright © 2016 Aubhro Sengupta. All rights reserved. 16 | 17 | ## Talk @Pygotham 2016 18 | I gave a talk at PyGotham 2016 on this library. Abstract can be found [here](https://2016.pygotham.org/talks/324/abstractions-and-building/). 19 | 20 | ## Introduction 21 | 22 | Chess_py is an open source chess library written in Python designed to aid in the creation of chess engines. Handles the chess so you can focus on the engine. 23 | 24 | ## Installation 25 | 26 | To use as a immediately start up a game between two human players in the console, navigate inside the root directory of the package and run main.py. 27 | 28 | ```bash 29 | python main.py 30 | ``` 31 | 32 | To install package 33 | 34 | ### ``pip`` (*Recommended*) 35 | ```bash 36 | pip install chess_py 37 | ``` 38 | 39 | ### Or manually 40 | ```bash 41 | python setup.py install 42 | ``` 43 | ## Documentation 44 | 45 | View complete technical documentation [here](http://lorddarkula.github.io/chess_py/html/html/index.html). 46 | 47 | ## Great! How do you use it? (*An Example*) 48 | 49 | Chess_py has the capability of creating games between players, either human, or AI 50 | 51 | ```python 52 | import chess_py 53 | from chess_py import Game, Human, color 54 | 55 | """ Creates a Game with 2 humans. 56 | When run, this will open the console,""" 57 | new_game = Game(Human(color.white), Human(color.black)) 58 | 59 | """ After game is completed, outcome will be stored in result. 60 | The integer result will be one of three values. 61 | white wins - 0, black wins - 1, draw - 0.5 """ 62 | result = new_game.play() 63 | ``` 64 | 65 | To build a chess engine on with chess_py, inherit Player and implement generate_move() 66 | 67 | ```python 68 | import chess_py 69 | from chess_py import Game, Human, color 70 | 71 | # Engine which plays the move with the highest immediate material advantage 72 | class MyEngine(chess_py.Player): 73 | def __init__(self, input_color): 74 | 75 | # Creates piece value scheme to specify value of each piece. 76 | self.piece_values = chess_py.PieceValues.init_manual(PAWN_VALUE=1, 77 | KNIGHT_VALUE=3, 78 | BISHOP_VALUE=3, 79 | ROOK_VALUE=5, 80 | QUEEN_VALUE=9) 81 | 82 | # Super call to 83 | super(chess_py.Player, self).__init__(input_color) 84 | 85 | def generate_move(self, position): 86 | # position parameter is an object of type Board 87 | 88 | # Finds all possible moves I can play. 89 | moves = position.all_possible_moves(self.color) 90 | 91 | # Initalizes best move and advantage after it has been played to dummy values. 92 | best_move = None 93 | best_move_advantage = -99 94 | 95 | # Loops through possible moves 96 | for move in moves: 97 | """ advantage_as_result(move, piece_values) finds numerical advantage 98 | as specified by piece value scheme above. Returns negative values for 99 | positions of disadvantage. Returns +/-99 for checkmate. """ 100 | advantage = position.advantage_as_result(move, self.piece_values) 101 | 102 | # If this move is better than best move, it is the best move. 103 | if advantage >= best_move_advantage: 104 | best_move = move 105 | best_move_advantage = advantage 106 | 107 | return best_move 108 | 109 | # If file is run as script, a Game is set up between My_engine and Human and result is printed. 110 | if __name__ == "__main__": 111 | new_game = Game(MyEngine(color.white), Human(color.black)) 112 | 113 | # white wins - 0, black wins - 1, draw - 0.5 114 | print("Result: ", new_game.play()) 115 | ``` 116 | 117 | -------------------------------------------------------------------------------- /tests/test_pieces/test_king.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from chess_py import color, Location, Move, Board, King, converter, notation_const, Rook 3 | 4 | 5 | class TestKing(TestCase): 6 | def setUp(self): 7 | self.board = Board.init_default() 8 | 9 | def test_in_check_as_result(self): 10 | self.assertFalse(self.board.get_king(color.white).in_check_as_result(self.board, 11 | converter.long_alg("e2e4", self.board))) 12 | 13 | self.board.move_piece(Location.from_string("e1"), Location.from_string("e3")) 14 | self.board.move_piece(Location.from_string("e8"), Location.from_string("e5")) 15 | 16 | # self.assertTrue(self.board.get_king(color.white).in_check_as_result(self.board, converter.long_alg("e3e4", self.board))) 17 | 18 | def test_add(self): 19 | self.assertEqual( 20 | len(list(self.board.get_king(color.white).add(lambda x: x.shift_up(), self.board))), 21 | 0) 22 | 23 | self.board.update(converter.long_alg("e2e4", self.board)) 24 | 25 | # King should be able to move up 26 | self.assertEqual( 27 | len(list(self.board.get_king(color.white).add(lambda x: x.shift_up(), self.board))), 28 | 1) 29 | 30 | # King should not be able to move down 31 | self.assertEqual( 32 | len(list(self.board.get_king(color.white).add(lambda x: x.shift_down(), self.board))), 33 | 0) 34 | 35 | # King should not be able to move left 36 | self.assertEqual( 37 | len(list(self.board.get_king(color.white).add(lambda x: x.shift_left(), self.board))), 38 | 0) 39 | 40 | # King should not be able to move right 41 | self.assertEqual( 42 | len(list(self.board.get_king(color.white).add(lambda x: x.shift_right(), self.board))), 43 | 0) 44 | 45 | # King should not be able to move up left 46 | self.assertEqual( 47 | len(list(self.board.get_king(color.white).add(lambda x: x.shift_up_left(), self.board))), 48 | 0) 49 | 50 | # King should not be able to move down right 51 | self.assertEqual( 52 | len(list(self.board.get_king(color.white).add(lambda x: x.shift_down_right(), self.board))), 53 | 0) 54 | 55 | # King should not be able to move down left 56 | self.assertEqual( 57 | len(list(self.board.get_king(color.white).add(lambda x: x.shift_down_left(), self.board))), 58 | 0) 59 | 60 | # King should not be able to move up right 61 | self.assertEqual( 62 | len(list(self.board.get_king(color.white).add(lambda x: x.shift_up_right(), self.board))), 63 | 0) 64 | 65 | def test_kingside_castle(self): 66 | self.board.update(converter.short_alg("e4", color.white, self.board)) 67 | self.board.update(converter.short_alg("Nf3", color.white, self.board)) 68 | self.board.update(converter.short_alg("Be2", color.white, self.board)) 69 | 70 | castle_move = Move( 71 | end_loc=Location.from_string("g1"), 72 | piece=King(color.white, Location.from_string("g1")), 73 | status=notation_const.KING_SIDE_CASTLE, 74 | start_loc=Location.from_string("e1") 75 | ) 76 | 77 | self.assertEqual( 78 | list(self.board.get_king(color.white).add_castle(self.board))[0], castle_move) 79 | 80 | def test_queenside_castle(self): 81 | 82 | self.board.remove_piece_at_square(Location.from_string("b1")) 83 | self.board.remove_piece_at_square(Location.from_string("c1")) 84 | self.board.remove_piece_at_square(Location.from_string("d1")) 85 | 86 | castle_move = Move( 87 | end_loc=Location.from_string("c1"), 88 | piece=King(color.white, Location.from_string("c1")), 89 | status=notation_const.QUEEN_SIDE_CASTLE, 90 | start_loc=Location.from_string("e1") 91 | ) 92 | 93 | self.assertEqual( 94 | list(self.board.get_king(color.white).add_castle(self.board))[0], castle_move) 95 | 96 | def test_possible_moves(self): 97 | self.board = Board([[None for _ in range(8)] for _ in range(8)]) 98 | my_king = King(color.white, Location.from_string("f3")) 99 | self.board.place_piece_at_square(my_king, Location.from_string("f3")) 100 | moves = ['f3f4', 'f3g3', 'f3f2', 'f3e3', 'f3g4', 'f3e4', 'f3g2', 'f3e2'] 101 | 102 | for i, move in enumerate(my_king.possible_moves(self.board)): 103 | self.assertEqual(move, converter.long_alg(moves[i], self.board)) 104 | 105 | def test_in_check(self): 106 | self.board = Board([[None for _ in range(8)] for _ in range(8)]) 107 | my_king = King(color.white, Location.from_string("f3")) 108 | self.board.place_piece_at_square(my_king, Location.from_string("f3")) 109 | self.board.place_piece_at_square(Rook(color.black, Location.from_string("f1")), Location.from_string("f1")) 110 | 111 | print(self.board.piece_at_square(Location.from_string("f1")).color) 112 | 113 | print(self.board) 114 | print(my_king.color) 115 | print(color.white == color.black) 116 | self.assertTrue(my_king.in_check(self.board)) 117 | 118 | self.board = Board.init_default() 119 | self.board.update(converter.long_alg("f2f3", self.board)) 120 | self.board.move_piece(Location.from_string("d8"), Location.from_string("g3")) 121 | 122 | self.assertTrue(self.board.get_king(color.white).in_check(self.board)) 123 | -------------------------------------------------------------------------------- /tests/test_core/test_algebraic/test_converter.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from chess_py import converter, Board, Move, Location, color, notation_const 4 | from chess_py import Pawn, Knight, Queen, Rook 5 | 6 | 7 | class TestConverter(unittest.TestCase): 8 | def setUp(self): 9 | self.test_board = Board.init_default() 10 | self.e_four_move = Move(end_loc=Location.from_string("e4"), 11 | piece=Pawn(color.white, Location.from_string("e4")), 12 | status=notation_const.MOVEMENT, 13 | start_loc=Location.from_string("e2")) 14 | 15 | def test_short_alg(self): 16 | self.assertEqual(converter.short_alg("e4", color.white, self.test_board), self.e_four_move) 17 | 18 | def test_incomplete_alg_pawn_movement(self): 19 | self.assertEqual( 20 | converter.incomplete_alg("e4", color.white, self.test_board), 21 | Move( 22 | end_loc=Location.from_string("e4"), 23 | piece=Pawn(color.white, Location.from_string("e4")), 24 | status=notation_const.MOVEMENT, 25 | start_loc=Location.from_string("e2") 26 | ) 27 | ) 28 | 29 | def test_incomplete_alg_piece_movement(self): 30 | self.assertEqual( 31 | converter.incomplete_alg("Nf3", color.white, self.test_board), 32 | Move( 33 | end_loc=Location.from_string("f3"), 34 | piece=Knight(color.white, Location.from_string("f3")), 35 | status=notation_const.MOVEMENT, 36 | start_loc=Location.from_string("g1") 37 | ) 38 | ) 39 | 40 | def test_incomplete_alg_pawn_capture(self): 41 | self.test_board.update(converter.short_alg("e4", color.white, self.test_board)) 42 | self.test_board.update(converter.short_alg("d5", color.black, self.test_board)) 43 | self.assertEqual( 44 | converter.incomplete_alg("exd5", color.white, self.test_board), 45 | Move( 46 | end_loc=Location.from_string("d5"), 47 | piece=Pawn(color.white, Location.from_string("e4")), 48 | status=notation_const.CAPTURE, 49 | start_loc=Location.from_string("e4") 50 | ) 51 | ) 52 | 53 | def test_incomplete_alg_piece_capture(self): 54 | self.test_board.update(converter.short_alg("Nf3", color.white, self.test_board)) 55 | self.test_board.update(converter.short_alg("e5", color.black, self.test_board)) 56 | self.assertEqual( 57 | converter.incomplete_alg("Nxe5", color.white, self.test_board), 58 | Move( 59 | end_loc=Location.from_string("e5"), 60 | piece=Knight(color.white, Location.from_string("f3")), 61 | status=notation_const.CAPTURE, 62 | start_loc=Location.from_string("f3") 63 | ) 64 | ) 65 | 66 | def test_incomplete_alg_pawn_promotion(self): 67 | self.test_board.move_piece(Location.from_string("a2"), Location.from_string("a7")) 68 | self.test_board.remove_piece_at_square(Location.from_string("a8")) 69 | self.assertEqual( 70 | converter.incomplete_alg("a8=Q", color.white, self.test_board), 71 | Move( 72 | end_loc=Location.from_string("a8"), 73 | piece=Pawn(color.white, Location.from_string("e7")), 74 | status=notation_const.PROMOTE, 75 | promoted_to_piece=Queen, 76 | start_loc=Location.from_string("a7") 77 | ) 78 | ) 79 | 80 | def test_incomplete_alg_piece_movement_with_file_specified(self): 81 | self.assertEqual( 82 | converter.incomplete_alg("gNf3", color.white, self.test_board), 83 | Move( 84 | end_loc=Location.from_string("f3"), 85 | piece=Knight(color.white, Location.from_string("g1")), 86 | status=notation_const.MOVEMENT, 87 | start_loc=Location.from_string("g1") 88 | ) 89 | ) 90 | 91 | def test_incomplete_alg_piece_movement_with_file_specified_alt(self): 92 | self.assertEqual( 93 | converter.incomplete_alg("Ngf3", color.white, self.test_board), 94 | Move( 95 | end_loc=Location.from_string("f3"), 96 | piece=Knight(color.white, Location.from_string("g1")), 97 | status=notation_const.MOVEMENT, 98 | start_loc=Location.from_string("g1") 99 | ) 100 | ) 101 | 102 | def test_incomplete_alg_piece_movement_with_rank_and_file_specified(self): 103 | self.assertEqual( 104 | converter.incomplete_alg("e1Nf3", color.white, self.test_board), 105 | Move( 106 | end_loc=Location.from_string("f3"), 107 | piece=Knight(color.white, Location.from_string("e1")), 108 | status=notation_const.MOVEMENT, 109 | start_loc=Location.from_string("e1") 110 | ) 111 | ) 112 | 113 | def test_incomplete_alg_pawn_promotion_with_capture(self): 114 | self.test_board.move_piece(Location.from_string("a2"), Location.from_string("a7")) 115 | self.assertEqual( 116 | converter.incomplete_alg("axb8=R", color.white, self.test_board), 117 | Move( 118 | end_loc=Location.from_string("b8"), 119 | piece=Pawn(color.white, Location.from_string("a7")), 120 | status=notation_const.CAPTURE_AND_PROMOTE, 121 | promoted_to_piece=Rook, 122 | start_loc=Location.from_string("a7") 123 | ) 124 | ) 125 | -------------------------------------------------------------------------------- /tests/test_pieces/test_pawn.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from chess_py.core.algebraic import notation_const 4 | from chess_py.core import Board 5 | from chess_py.core.algebraic import Location, Move 6 | from chess_py.pieces import Queen, Rook, Bishop, Knight, Pawn 7 | from chess_py import color 8 | 9 | 10 | class TestPawn(TestCase): 11 | def setUp(self): 12 | self.position = Board.init_default() 13 | 14 | self.white_pawn = Pawn(color.white, Location.from_string("e2")) 15 | self.position.place_piece_at_square(self.white_pawn, Location.from_string("e2")) 16 | 17 | self.black_pawn = Pawn(color.black, Location.from_string("a7")) 18 | self.position.place_piece_at_square(self.black_pawn, Location.from_string("a7")) 19 | 20 | def test_square_in_front(self): 21 | self.assertEqual(self.white_pawn.square_in_front(self.white_pawn.location), Location.from_string("e3")) 22 | self.assertEqual(self.black_pawn.square_in_front(self.black_pawn.location), Location.from_string("a6")) 23 | 24 | def test_two_squares_in_front(self): 25 | self.assertEqual(self.white_pawn.two_squares_in_front(self.white_pawn.location), Location.from_string("e4")) 26 | self.assertEqual(self.black_pawn.two_squares_in_front(self.black_pawn.location), Location.from_string("a5")) 27 | 28 | def test_would_move_be_promotion(self): 29 | self.assertTrue(self.white_pawn.would_move_be_promotion(Location.from_string("e7"))) 30 | self.assertTrue(self.black_pawn.would_move_be_promotion(Location.from_string("a2"))) 31 | self.assertFalse(self.white_pawn.would_move_be_promotion(Location.from_string("e2"))) 32 | self.assertFalse(self.black_pawn.would_move_be_promotion(Location.from_string("a7"))) 33 | 34 | def test_create_promotion_moves(self): 35 | self.white_pawn.location = Location.from_string("e7") 36 | moves = list(self.white_pawn.create_promotion_moves(notation_const.CAPTURE, 37 | Location.from_string("e7"))) 38 | self.assertEqual(len(list(moves)), 4) 39 | self.assertEqual(moves[0].start_loc, Location.from_string("e7")) 40 | 41 | self.assertEqual(moves[0].promoted_to_piece, Queen) 42 | self.assertEqual(moves[1].promoted_to_piece, Rook) 43 | self.assertEqual(moves[2].promoted_to_piece, Bishop) 44 | self.assertEqual(moves[3].promoted_to_piece, Knight) 45 | 46 | def test_forward_moves(self): 47 | self.white_pawn.location = Location.from_string("e2") 48 | moves = list(self.white_pawn.forward_moves(self.position)) 49 | 50 | self.assertEqual(len(moves), 2) 51 | self.assertEqual(moves[0], Move(end_loc=self.white_pawn.square_in_front(self.white_pawn.location), 52 | piece=self.white_pawn, 53 | status=notation_const.MOVEMENT, 54 | start_loc=self.white_pawn.location)) 55 | 56 | self.assertEqual(moves[1], Move(end_loc=self.white_pawn.square_in_front(self.white_pawn.square_in_front(self.white_pawn.location)), 57 | piece=self.white_pawn, 58 | status=notation_const.MOVEMENT, 59 | start_loc=self.white_pawn.location)) 60 | 61 | moves = list(self.black_pawn.forward_moves(self.position)) 62 | self.assertEqual(len(moves), 2) 63 | 64 | self.assertEqual(moves[0], Move(end_loc=self.black_pawn.square_in_front(self.black_pawn.location), 65 | piece=self.black_pawn, 66 | status=notation_const.MOVEMENT, 67 | start_loc=self.black_pawn.location)) 68 | 69 | self.assertEqual(moves[1], Move(end_loc=self.black_pawn.square_in_front(self.black_pawn.square_in_front(self.black_pawn.location)), 70 | piece=self.black_pawn, 71 | status=notation_const.MOVEMENT, 72 | start_loc=self.black_pawn.location)) 73 | 74 | def test_capture_moves(self): 75 | self.position.move_piece(Location.from_string("d7"), Location.from_string("d5")) 76 | self.position.move_piece(Location.from_string("e2"), Location.from_string("e4")) 77 | 78 | black_pawn = self.position.piece_at_square(Location.from_string("d5")) 79 | move = list(self.white_pawn.capture_moves(self.position)) 80 | 81 | self.assertEqual(len(move), 1) 82 | 83 | self.assertEqual(move[0], Move(end_loc=black_pawn.location, 84 | piece=self.white_pawn, 85 | status=notation_const.CAPTURE, 86 | start_loc=self.white_pawn.location)) 87 | 88 | def test_en_passant_moves(self): 89 | self.position.move_piece(Location.from_string("d7"), Location.from_string("d4")) 90 | self.position.move_piece(Location.from_string("e2"), Location.from_string("e4")) 91 | 92 | black_pawn = self.position.piece_at_square(Location.from_string("d4")) 93 | self.position.piece_at_square(Location.from_string("e4")).just_moved_two_steps = True 94 | 95 | move = list(black_pawn.en_passant_moves(self.position)) 96 | 97 | self.assertEqual(len(move), 1) 98 | self.assertEqual(move[0], Move(end_loc=black_pawn.square_in_front(black_pawn.location.shift_right()), 99 | piece=black_pawn, 100 | status=notation_const.EN_PASSANT, 101 | start_loc=black_pawn.location)) 102 | 103 | def test_possible_moves(self): 104 | self.assertEqual(len(list(self.white_pawn.possible_moves(self.position))), 2) 105 | self.position.move_piece(Location.from_string("e2"), Location.from_string("e3")) 106 | self.assertEqual(len(list(self.white_pawn.possible_moves(self.position))), 1) 107 | 108 | -------------------------------------------------------------------------------- /chess_py/pieces/king.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Class stores King on the board 5 | 6 | | rank 7 | | 7 8 ║♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜ 8 | | 6 7 ║♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟ 9 | | 5 6 ║… … … … … … … … 10 | | 4 5 ║… … … … … … … … 11 | | 3 4 ║… … … … … … … … 12 | | 2 3 ║… … … … … … … … 13 | | 1 2 ║♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙ 14 | | 0 1 ║♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖ 15 | | ----╚═══════════════ 16 | | ——---a b c d e f g h 17 | | -----0 1 2 3 4 5 6 7 18 | | ------file 19 | 20 | | Copyright © 2016 Aubhro Sengupta. All rights reserved. 21 | """ 22 | 23 | import itertools 24 | from copy import copy as cp 25 | 26 | from .piece import Piece 27 | from .rook import Rook 28 | from ..core.algebraic import notation_const 29 | from ..core import color 30 | from ..core.algebraic.location import Location 31 | 32 | 33 | class King(Piece): 34 | def __init__(self, input_color, location): 35 | """ 36 | Creates a King. 37 | 38 | :type: input_color: Color 39 | :type: location: Location 40 | """ 41 | super(King, self).__init__(input_color, location) 42 | self.has_moved = False 43 | self.cardinal_directions = self.cross_fn + self.diag_fn 44 | 45 | def _symbols(self): 46 | return {color.white: "♚", color.black: "♔"} 47 | 48 | def __str__(self): 49 | return "K" 50 | 51 | def in_check_as_result(self, pos, move): 52 | """ 53 | Finds if playing my move would make both kings meet. 54 | 55 | :type: pos: Board 56 | :type: move: Move 57 | :rtype: bool 58 | """ 59 | test = cp(pos) 60 | test.update(move) 61 | test_king = test.get_king(move.color) 62 | 63 | return self.loc_adjacent_to_opponent_king(test_king.location, test) 64 | 65 | def loc_adjacent_to_opponent_king(self, location, position): 66 | """ 67 | Finds if 2 kings are touching given the position of one of the kings. 68 | 69 | :type: location: Location 70 | :type: position: Board 71 | :rtype: bool 72 | """ 73 | for fn in self.cardinal_directions: 74 | try: 75 | if isinstance(position.piece_at_square(fn(location)), King) and \ 76 | position.piece_at_square(fn(location)).color != self.color: 77 | return True 78 | 79 | except IndexError: 80 | pass 81 | 82 | return False 83 | 84 | def add(self, func, position): 85 | """ 86 | Adds all 8 cardinal directions as moves for the King if legal. 87 | 88 | :type: function: function 89 | :type: position: Board 90 | :rtype: gen 91 | """ 92 | try: 93 | if self.loc_adjacent_to_opponent_king(func(self.location), position): 94 | return 95 | except IndexError: 96 | return 97 | 98 | if position.is_square_empty(func(self.location)): 99 | yield self.create_move(func(self.location), notation_const.MOVEMENT) 100 | 101 | elif position.piece_at_square(func(self.location)).color != self.color: 102 | yield self.create_move(func(self.location), notation_const.CAPTURE) 103 | 104 | def _rook_legal_for_castle(self, rook): 105 | """ 106 | Decides if given rook exists, is of this color, and has not moved so it 107 | is eligible to castle. 108 | 109 | :type: rook: Rook 110 | :rtype: bool 111 | """ 112 | return rook is not None and \ 113 | type(rook) is Rook and \ 114 | rook.color == self.color and \ 115 | not rook.has_moved 116 | 117 | def _empty_not_in_check(self, position, direction): 118 | """ 119 | Checks if set of squares in between ``King`` and ``Rook`` are empty and safe 120 | for the king to castle. 121 | 122 | :type: position: Position 123 | :type: direction: function 124 | :type: times: int 125 | :rtype: bool 126 | """ 127 | def valid_square(square): 128 | return position.is_square_empty(square) and \ 129 | not self.in_check(position, square) 130 | 131 | return valid_square(direction(self.location, 1)) and \ 132 | valid_square(direction(self.location, 2)) 133 | 134 | def add_castle(self, position): 135 | """ 136 | Adds kingside and queenside castling moves if legal 137 | 138 | :type: position: Board 139 | """ 140 | if self.has_moved or self.in_check(position): 141 | return 142 | 143 | if self.color == color.white: 144 | rook_rank = 0 145 | else: 146 | rook_rank = 7 147 | 148 | castle_type = { 149 | notation_const.KING_SIDE_CASTLE: { 150 | "rook_file": 7, 151 | "direction": lambda king_square, times: king_square.shift_right(times) 152 | }, 153 | notation_const.QUEEN_SIDE_CASTLE: { 154 | "rook_file": 0, 155 | "direction": lambda king_square, times: king_square.shift_left(times) 156 | } 157 | } 158 | for castle_key in castle_type: 159 | castle_dict = castle_type[castle_key] 160 | castle_rook = position.piece_at_square(Location(rook_rank, castle_dict["rook_file"])) 161 | if self._rook_legal_for_castle(castle_rook) and \ 162 | self._empty_not_in_check(position, castle_dict["direction"]): 163 | yield self.create_move(castle_dict["direction"](self.location, 2), castle_key) 164 | 165 | def possible_moves(self, position): 166 | """ 167 | Generates list of possible moves 168 | 169 | :type: position: Board 170 | :rtype: list 171 | """ 172 | # Chain used to combine multiple generators 173 | for move in itertools.chain(*[self.add(fn, position) for fn in self.cardinal_directions]): 174 | yield move 175 | 176 | for move in self.add_castle(position): 177 | yield move 178 | 179 | def in_check(self, position, location=None): 180 | """ 181 | Finds if the king is in check or if both kings are touching. 182 | 183 | :type: position: Board 184 | :return: bool 185 | """ 186 | location = location or self.location 187 | for piece in position: 188 | 189 | if piece is not None and piece.color != self.color: 190 | if not isinstance(piece, King): 191 | for move in piece.possible_moves(position): 192 | 193 | if move.end_loc == location: 194 | return True 195 | else: 196 | if self.loc_adjacent_to_opponent_king(piece.location, position): 197 | return True 198 | 199 | return False 200 | -------------------------------------------------------------------------------- /chess_py/core/algebraic/location.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Class stores Locations on the Board in form ``Location(rank, file)``. 5 | 6 | | rank - y coordinate from 0 to 7 7 | | file - x coordinate from 0 to 7 8 | 9 | Can be initialized with coordinates not 10 | on the chess board, however, such locations 11 | will not work properly with other classes 12 | such as ``Board``. 13 | 14 | Location is immutable. 15 | 16 | Examples (shown on board below): 17 | 18 | | a = Location(0, 0) 19 | | b = Location(7, 7) 20 | | c = Location(3, 4) 21 | 22 | | rank 23 | | 7 8 ║… … … … … … … b 24 | | 6 7 ║… … … … … … … … 25 | | 5 6 ║… … … … … … … … 26 | | 4 5 ║… … … … … … … … 27 | | 3 4 ║… … … … c … … … 28 | | 2 3 ║… … … … … … … … 29 | | 1 2 ║… … … … … … … … 30 | | 0 1 ║a … … … … … … … 31 | | ----╚═══════════════ 32 | | ——---a b c d e f g h 33 | | -----0 1 2 3 4 5 6 7 34 | | ------file 35 | 36 | Copyright © 2016 Aubhro Sengupta. All rights reserved. 37 | """ 38 | 39 | from .. import color 40 | 41 | class Direction: 42 | UP = 0 43 | RIGHT = 1 44 | DOWN = 2 45 | LEFT = 3 46 | 47 | 48 | class Location: 49 | def __init__(self, rank, file): 50 | """ 51 | Creates a location on a chessboard given x and y coordinates. 52 | 53 | :type: rank: int 54 | :type: file: int 55 | """ 56 | if rank < 0 or rank > 7 or file < 0 or file > 7: 57 | raise IndexError("Location must be on the board") 58 | 59 | self._rank = rank 60 | self._file = file 61 | 62 | @classmethod 63 | def from_string(cls, alg_str): 64 | """ 65 | Creates a location from a two character string consisting of 66 | the file then rank written in algebraic notation. 67 | 68 | Examples: e4, b5, a7 69 | 70 | :type: alg_str: str 71 | :rtype: Location 72 | """ 73 | try: 74 | return cls(int(alg_str[1]) - 1, ord(alg_str[0]) - 97) 75 | except ValueError as e: 76 | raise ValueError("Location.from_string {} invalid: {}".format(alg_str, e)) 77 | 78 | @property 79 | def rank(self): 80 | return self._rank 81 | 82 | @property 83 | def file(self): 84 | return self._file 85 | 86 | def __key(self): 87 | return self.rank, self.file 88 | 89 | def __hash__(self): 90 | return hash(self.__key()) 91 | 92 | def __eq__(self, other): 93 | """ 94 | Tests to see if both locations 95 | are the same ie rank and file is 96 | the same. 97 | 98 | :type: other: Location 99 | """ 100 | if not isinstance(other, self.__class__): 101 | raise TypeError("Cannot compare other types with Location") 102 | 103 | return int(self.rank) == int(other.rank) and \ 104 | int(self.file) == int(other.file) 105 | 106 | def __ne__(self, other): 107 | return not self.__eq__(other) 108 | 109 | def __repr__(self): 110 | return "Location({}, {} ({}))".format(self._rank, self._file, str(self)) 111 | 112 | def __str__(self): 113 | """ 114 | Finds string representation of Location in algebraic form ie "e4" 115 | 116 | :rtype: str 117 | """ 118 | if self._rank is None: 119 | rank_str = "" 120 | else: 121 | rank_str = str(self._rank + 1) 122 | 123 | if self._file is None: 124 | file_str = "" 125 | else: 126 | file_str = chr(self._file + 97) 127 | 128 | return file_str + rank_str 129 | 130 | def on_board(self): 131 | """ 132 | Returns if the move is on the board or not. 133 | If the rank and file are both in between 134 | 0 and 7, this method will return True. 135 | 136 | :rtype: bool 137 | """ 138 | if -1 < self._rank < 8 and \ 139 | -1 < self._file < 8: 140 | return True 141 | 142 | return False 143 | 144 | def shift(self, direction): 145 | """ 146 | Shifts in direction provided by ``Direction`` enum. 147 | 148 | :type: direction: Direction 149 | :rtype: Location 150 | """ 151 | try: 152 | if direction == Direction.UP: 153 | return self.shift_up() 154 | elif direction == Direction.DOWN: 155 | return self.shift_down() 156 | elif direction == Direction.RIGHT: 157 | return self.shift_right() 158 | elif direction == Direction.LEFT: 159 | return self.shift_left() 160 | else: 161 | raise IndexError("Invalid direction {}".format(direction)) 162 | except IndexError as e: 163 | raise IndexError(e) 164 | 165 | def shift_up(self, times=1): 166 | """ 167 | Finds Location shifted up by 1 168 | 169 | :rtype: Location 170 | """ 171 | try: 172 | return Location(self._rank + times, self._file) 173 | except IndexError as e: 174 | raise IndexError(e) 175 | 176 | def shift_down(self, times=1): 177 | """ 178 | Finds Location shifted down by 1 179 | 180 | :rtype: Location 181 | """ 182 | try: 183 | return Location(self._rank - times, self._file) 184 | except IndexError as e: 185 | raise IndexError(e) 186 | 187 | def shift_forward(self, ref_color, times=1): 188 | direction = 1 if ref_color == color.white else -1 189 | try: 190 | return Location(self._rank + times*direction, self._file) 191 | except IndexError as e: 192 | raise IndexError(e) 193 | 194 | def shift_back(self, ref_color, times=1): 195 | direction = -1 if ref_color == color.white else 1 196 | try: 197 | return Location(self._rank + times*direction, self._file) 198 | except IndexError as e: 199 | raise IndexError(e) 200 | 201 | def shift_right(self, times=1): 202 | """ 203 | Finds Location shifted right by 1 204 | 205 | :rtype: Location 206 | """ 207 | try: 208 | return Location(self._rank, self._file + times) 209 | except IndexError as e: 210 | raise IndexError(e) 211 | 212 | def shift_left(self, times=1): 213 | """ 214 | Finds Location shifted left by 1 215 | 216 | :rtype: Location 217 | """ 218 | try: 219 | return Location(self._rank, self._file - times) 220 | except IndexError as e: 221 | raise IndexError(e) 222 | 223 | def shift_up_right(self, times=1): 224 | """ 225 | Finds Location shifted up right by 1 226 | 227 | :rtype: Location 228 | """ 229 | try: 230 | return Location(self._rank + times, self._file + times) 231 | except IndexError as e: 232 | raise IndexError(e) 233 | 234 | def shift_up_left(self, times=1): 235 | """ 236 | Finds Location shifted up left by 1 237 | 238 | :rtype: Location 239 | """ 240 | try: 241 | return Location(self._rank + times, self._file - times) 242 | except IndexError as e: 243 | raise IndexError(e) 244 | 245 | def shift_down_right(self, times=1): 246 | """ 247 | Finds Location shifted down right by 1 248 | 249 | :rtype: Location 250 | """ 251 | try: 252 | return Location(self._rank - times, self._file + times) 253 | except IndexError as e: 254 | raise IndexError(e) 255 | 256 | def shift_down_left(self, times=1): 257 | """ 258 | Finds Location shifted down left by 1 259 | 260 | :rtype: Location 261 | """ 262 | try: 263 | return Location(self._rank - times, self._file - times) 264 | except IndexError as e: 265 | raise IndexError(e) 266 | -------------------------------------------------------------------------------- /chess_py/pieces/pawn.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Class stores Pawn on the board 5 | 6 | | rank 7 | | 7 8 ║♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜ 8 | | 6 7 ║♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟ 9 | | 5 6 ║… … … … … … … … 10 | | 4 5 ║… … … … … … … … 11 | | 3 4 ║… … … … … … … … 12 | | 2 3 ║… … … … … … … … 13 | | 1 2 ║♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙ 14 | | 0 1 ║♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖ 15 | | ----╚═══════════════ 16 | | ——---a b c d e f g h 17 | | -----0 1 2 3 4 5 6 7 18 | | ------file 19 | 20 | | Copyright © 2016 Aubhro Sengupta. All rights reserved. 21 | """ 22 | import itertools 23 | 24 | from .bishop import Bishop 25 | from .piece import Piece 26 | from .queen import Queen 27 | from .rook import Rook 28 | from .knight import Knight 29 | from ..core import color 30 | from ..core.algebraic import notation_const 31 | from ..core.algebraic.move import Move 32 | 33 | 34 | class Pawn(Piece): 35 | def __init__(self, input_color, location): 36 | """ 37 | Initializes a Pawn that is capable of moving 38 | 39 | :type: input_color: Color 40 | :type: location: Location 41 | """ 42 | self.just_moved_two_steps = False 43 | super(Pawn, self).__init__(input_color, location) 44 | 45 | def _symbols(self): 46 | return {color.white: "♟", color.black: "♙"} 47 | 48 | def __str__(self): 49 | return "P" 50 | 51 | def on_home_row(self, location=None): 52 | """ 53 | Finds out if the piece is on the home row. 54 | 55 | :return: bool for whether piece is on home row or not 56 | """ 57 | location = location or self.location 58 | return (self.color == color.white and location.rank == 1) or \ 59 | (self.color == color.black and location.rank == 6) 60 | 61 | def would_move_be_promotion(self, location=None): 62 | """ 63 | Finds if move from current get_location would result in promotion 64 | 65 | :type: location: Location 66 | :rtype: bool 67 | """ 68 | location = location or self.location 69 | return (location.rank == 1 and self.color == color.black) or \ 70 | (location.rank == 6 and self.color == color.white) 71 | 72 | def square_in_front(self, location=None): 73 | """ 74 | Finds square directly in front of Pawn 75 | 76 | :type: location: Location 77 | :rtype: Location 78 | """ 79 | location = location or self.location 80 | return location.shift_up() if self.color == color.white else location.shift_down() 81 | 82 | def two_squares_in_front(self, location): 83 | """ 84 | Finds square two squares in front of Pawn 85 | 86 | :type: location: Location 87 | :rtype: get_location 88 | """ 89 | return self.square_in_front(self.square_in_front(location)) 90 | 91 | def create_promotion_moves(self, status, location=None): 92 | location = location or self.square_in_front() 93 | def create_each_move(piece): 94 | return Move(end_loc=location, 95 | piece=self, 96 | status=status, 97 | start_loc=self.location, 98 | promoted_to_piece=piece) 99 | 100 | yield create_each_move(Queen) 101 | yield create_each_move(Rook) 102 | yield create_each_move(Bishop) 103 | yield create_each_move(Knight) 104 | 105 | def forward_moves(self, position): 106 | """ 107 | Finds possible moves one step and two steps in front 108 | of Pawn. 109 | 110 | :type: position: Board 111 | :rtype: list 112 | """ 113 | if position.is_square_empty(self.square_in_front(self.location)): 114 | """ 115 | If square in front is empty add the move 116 | """ 117 | if self.would_move_be_promotion(): 118 | for move in self.create_promotion_moves(notation_const.PROMOTE): 119 | yield move 120 | else: 121 | yield self.create_move(end_loc=self.square_in_front(self.location), 122 | status=notation_const.MOVEMENT) 123 | 124 | if self.on_home_row() and \ 125 | position.is_square_empty(self.two_squares_in_front(self.location)): 126 | """ 127 | If pawn is on home row and two squares in front of the pawn is empty 128 | add the move 129 | """ 130 | yield self.create_move( 131 | end_loc=self.square_in_front(self.square_in_front(self.location)), 132 | status=notation_const.MOVEMENT 133 | ) 134 | 135 | def _one_diagonal_capture_square(self, capture_square, position): 136 | """ 137 | Adds specified diagonal as a capture move if it is one 138 | """ 139 | if self.contains_opposite_color_piece(capture_square, position): 140 | 141 | if self.would_move_be_promotion(): 142 | for move in self.create_promotion_moves(status=notation_const.CAPTURE_AND_PROMOTE, 143 | location=capture_square): 144 | yield move 145 | 146 | else: 147 | yield self.create_move(end_loc=capture_square, 148 | status=notation_const.CAPTURE) 149 | 150 | def capture_moves(self, position): 151 | """ 152 | Finds out all possible capture moves 153 | 154 | :rtype: list 155 | """ 156 | try: 157 | right_diagonal = self.square_in_front(self.location.shift_right()) 158 | for move in self._one_diagonal_capture_square(right_diagonal, position): 159 | yield move 160 | except IndexError: 161 | pass 162 | 163 | try: 164 | left_diagonal = self.square_in_front(self.location.shift_left()) 165 | for move in self._one_diagonal_capture_square(left_diagonal, position): 166 | yield move 167 | except IndexError: 168 | pass 169 | 170 | def on_en_passant_valid_location(self): 171 | """ 172 | Finds out if pawn is on enemy center rank. 173 | 174 | :rtype: bool 175 | """ 176 | return (self.color == color.white and self.location.rank == 4) or \ 177 | (self.color == color.black and self.location.rank == 3) 178 | 179 | def _is_en_passant_valid(self, opponent_pawn_location, position): 180 | """ 181 | Finds if their opponent's pawn is next to this pawn 182 | 183 | :rtype: bool 184 | """ 185 | try: 186 | pawn = position.piece_at_square(opponent_pawn_location) 187 | return pawn is not None and \ 188 | isinstance(pawn, Pawn) and \ 189 | pawn.color != self.color and \ 190 | position.piece_at_square(opponent_pawn_location).just_moved_two_steps 191 | except IndexError: 192 | return False 193 | 194 | def add_one_en_passant_move(self, direction, position): 195 | """ 196 | Yields en_passant moves in given direction if it is legal. 197 | 198 | :type: direction: function 199 | :type: position: Board 200 | :rtype: gen 201 | """ 202 | try: 203 | if self._is_en_passant_valid(direction(self.location), position): 204 | yield self.create_move( 205 | end_loc=self.square_in_front(direction(self.location)), 206 | status=notation_const.EN_PASSANT 207 | ) 208 | except IndexError: 209 | pass 210 | 211 | def en_passant_moves(self, position): 212 | """ 213 | Finds possible en passant moves. 214 | 215 | :rtype: list 216 | """ 217 | 218 | # if pawn is not on a valid en passant get_location then return None 219 | if self.on_en_passant_valid_location(): 220 | for move in itertools.chain(self.add_one_en_passant_move(lambda x: x.shift_right(), position), 221 | self.add_one_en_passant_move(lambda x: x.shift_left(), position)): 222 | yield move 223 | 224 | def possible_moves(self, position): 225 | """ 226 | Finds out the locations of possible moves given board.Board position. 227 | :pre get_location is on board and piece at specified get_location on position 228 | 229 | :type: position: Board 230 | :rtype: list 231 | """ 232 | for move in itertools.chain(self.forward_moves(position), 233 | self.capture_moves(position), 234 | self.en_passant_moves(position)): 235 | yield move 236 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | echo. dummy to check syntax errors of document sources 43 | goto end 44 | ) 45 | 46 | if "%1" == "clean" ( 47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 48 | del /q /s %BUILDDIR%\* 49 | goto end 50 | ) 51 | 52 | 53 | REM Check if sphinx-build is available and fallback to Python version if any 54 | %SPHINXBUILD% 1>NUL 2>NUL 55 | if errorlevel 9009 goto sphinx_python 56 | goto sphinx_ok 57 | 58 | :sphinx_python 59 | 60 | set SPHINXBUILD=python -m sphinx.__init__ 61 | %SPHINXBUILD% 2> nul 62 | if errorlevel 9009 ( 63 | echo. 64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 65 | echo.installed, then set the SPHINXBUILD environment variable to point 66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 67 | echo.may add the Sphinx directory to PATH. 68 | echo. 69 | echo.If you don't have Sphinx installed, grab it from 70 | echo.http://sphinx-doc.org/ 71 | exit /b 1 72 | ) 73 | 74 | :sphinx_ok 75 | 76 | 77 | if "%1" == "html" ( 78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 79 | if errorlevel 1 exit /b 1 80 | echo. 81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 82 | goto end 83 | ) 84 | 85 | if "%1" == "dirhtml" ( 86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 87 | if errorlevel 1 exit /b 1 88 | echo. 89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 90 | goto end 91 | ) 92 | 93 | if "%1" == "singlehtml" ( 94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 95 | if errorlevel 1 exit /b 1 96 | echo. 97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 98 | goto end 99 | ) 100 | 101 | if "%1" == "pickle" ( 102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 103 | if errorlevel 1 exit /b 1 104 | echo. 105 | echo.Build finished; now you can process the pickle files. 106 | goto end 107 | ) 108 | 109 | if "%1" == "json" ( 110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished; now you can process the JSON files. 114 | goto end 115 | ) 116 | 117 | if "%1" == "htmlhelp" ( 118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished; now you can run HTML Help Workshop with the ^ 122 | .hhp project file in %BUILDDIR%/htmlhelp. 123 | goto end 124 | ) 125 | 126 | if "%1" == "qthelp" ( 127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 128 | if errorlevel 1 exit /b 1 129 | echo. 130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 131 | .qhcp project file in %BUILDDIR%/qthelp, like this: 132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\chess_py.qhcp 133 | echo.To view the help file: 134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\chess_py.ghc 135 | goto end 136 | ) 137 | 138 | if "%1" == "devhelp" ( 139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished. 143 | goto end 144 | ) 145 | 146 | if "%1" == "epub" ( 147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 148 | if errorlevel 1 exit /b 1 149 | echo. 150 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 151 | goto end 152 | ) 153 | 154 | if "%1" == "epub3" ( 155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 156 | if errorlevel 1 exit /b 1 157 | echo. 158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 159 | goto end 160 | ) 161 | 162 | if "%1" == "latex" ( 163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 164 | if errorlevel 1 exit /b 1 165 | echo. 166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdf" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "latexpdfja" ( 181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 182 | cd %BUILDDIR%/latex 183 | make all-pdf-ja 184 | cd %~dp0 185 | echo. 186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 187 | goto end 188 | ) 189 | 190 | if "%1" == "text" ( 191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 192 | if errorlevel 1 exit /b 1 193 | echo. 194 | echo.Build finished. The text files are in %BUILDDIR%/text. 195 | goto end 196 | ) 197 | 198 | if "%1" == "man" ( 199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 200 | if errorlevel 1 exit /b 1 201 | echo. 202 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 203 | goto end 204 | ) 205 | 206 | if "%1" == "texinfo" ( 207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 208 | if errorlevel 1 exit /b 1 209 | echo. 210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 211 | goto end 212 | ) 213 | 214 | if "%1" == "gettext" ( 215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 216 | if errorlevel 1 exit /b 1 217 | echo. 218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 219 | goto end 220 | ) 221 | 222 | if "%1" == "changes" ( 223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 224 | if errorlevel 1 exit /b 1 225 | echo. 226 | echo.The overview file is in %BUILDDIR%/changes. 227 | goto end 228 | ) 229 | 230 | if "%1" == "linkcheck" ( 231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 232 | if errorlevel 1 exit /b 1 233 | echo. 234 | echo.Link check complete; look for any errors in the above output ^ 235 | or in %BUILDDIR%/linkcheck/output.txt. 236 | goto end 237 | ) 238 | 239 | if "%1" == "doctest" ( 240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 241 | if errorlevel 1 exit /b 1 242 | echo. 243 | echo.Testing of doctests in the sources finished, look at the ^ 244 | results in %BUILDDIR%/doctest/output.txt. 245 | goto end 246 | ) 247 | 248 | if "%1" == "coverage" ( 249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 250 | if errorlevel 1 exit /b 1 251 | echo. 252 | echo.Testing of coverage in the sources finished, look at the ^ 253 | results in %BUILDDIR%/coverage/python.txt. 254 | goto end 255 | ) 256 | 257 | if "%1" == "xml" ( 258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 259 | if errorlevel 1 exit /b 1 260 | echo. 261 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 262 | goto end 263 | ) 264 | 265 | if "%1" == "pseudoxml" ( 266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 267 | if errorlevel 1 exit /b 1 268 | echo. 269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 270 | goto end 271 | ) 272 | 273 | if "%1" == "dummy" ( 274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 275 | if errorlevel 1 exit /b 1 276 | echo. 277 | echo.Build finished. Dummy builder generates no files. 278 | goto end 279 | ) 280 | 281 | :end 282 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = ../gh-pages/html 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " epub3 to make an epub3" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | @echo " dummy to check syntax errors of document sources" 51 | 52 | .PHONY: clean 53 | clean: 54 | rm -rf $(BUILDDIR)/* 55 | 56 | .PHONY: html 57 | html: 58 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 61 | 62 | .PHONY: dirhtml 63 | dirhtml: 64 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 65 | @echo 66 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 67 | 68 | .PHONY: singlehtml 69 | singlehtml: 70 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 71 | @echo 72 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 73 | 74 | .PHONY: pickle 75 | pickle: 76 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 77 | @echo 78 | @echo "Build finished; now you can process the pickle files." 79 | 80 | .PHONY: json 81 | json: 82 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 83 | @echo 84 | @echo "Build finished; now you can process the JSON files." 85 | 86 | .PHONY: htmlhelp 87 | htmlhelp: 88 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 89 | @echo 90 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 91 | ".hhp project file in $(BUILDDIR)/htmlhelp." 92 | 93 | .PHONY: qthelp 94 | qthelp: 95 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 96 | @echo 97 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 98 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 99 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/chess_py.qhcp" 100 | @echo "To view the help file:" 101 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/chess_py.qhc" 102 | 103 | .PHONY: applehelp 104 | applehelp: 105 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 106 | @echo 107 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 108 | @echo "N.B. You won't be able to view it unless you put it in" \ 109 | "~/Library/Documentation/Help or install it in your application" \ 110 | "bundle." 111 | 112 | .PHONY: devhelp 113 | devhelp: 114 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 115 | @echo 116 | @echo "Build finished." 117 | @echo "To view the help file:" 118 | @echo "# mkdir -p $$HOME/.local/share/devhelp/chess_py" 119 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/chess_py" 120 | @echo "# devhelp" 121 | 122 | .PHONY: epub 123 | epub: 124 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 125 | @echo 126 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 127 | 128 | .PHONY: epub3 129 | epub3: 130 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 131 | @echo 132 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 133 | 134 | .PHONY: latex 135 | latex: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo 138 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 139 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 140 | "(use \`make latexpdf' here to do that automatically)." 141 | 142 | .PHONY: latexpdf 143 | latexpdf: 144 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 145 | @echo "Running LaTeX files through pdflatex..." 146 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 147 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 148 | 149 | .PHONY: latexpdfja 150 | latexpdfja: 151 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 152 | @echo "Running LaTeX files through platex and dvipdfmx..." 153 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 154 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 155 | 156 | .PHONY: text 157 | text: 158 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 159 | @echo 160 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 161 | 162 | .PHONY: man 163 | man: 164 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 165 | @echo 166 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 167 | 168 | .PHONY: texinfo 169 | texinfo: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo 172 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 173 | @echo "Run \`make' in that directory to run these through makeinfo" \ 174 | "(use \`make info' here to do that automatically)." 175 | 176 | .PHONY: info 177 | info: 178 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 179 | @echo "Running Texinfo files through makeinfo..." 180 | make -C $(BUILDDIR)/texinfo info 181 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 182 | 183 | .PHONY: gettext 184 | gettext: 185 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 186 | @echo 187 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 188 | 189 | .PHONY: changes 190 | changes: 191 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 192 | @echo 193 | @echo "The overview file is in $(BUILDDIR)/changes." 194 | 195 | .PHONY: linkcheck 196 | linkcheck: 197 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 198 | @echo 199 | @echo "Link check complete; look for any errors in the above output " \ 200 | "or in $(BUILDDIR)/linkcheck/output.txt." 201 | 202 | .PHONY: doctest 203 | doctest: 204 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 205 | @echo "Testing of doctests in the sources finished, look at the " \ 206 | "results in $(BUILDDIR)/doctest/output.txt." 207 | 208 | .PHONY: coverage 209 | coverage: 210 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 211 | @echo "Testing of coverage in the sources finished, look at the " \ 212 | "results in $(BUILDDIR)/coverage/python.txt." 213 | 214 | .PHONY: xml 215 | xml: 216 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 217 | @echo 218 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 219 | 220 | .PHONY: pseudoxml 221 | pseudoxml: 222 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 223 | @echo 224 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 225 | 226 | .PHONY: dummy 227 | dummy: 228 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 229 | @echo 230 | @echo "Build finished. Dummy builder generates no files." 231 | -------------------------------------------------------------------------------- /tests/test_core/test_board.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from chess_py import Board, color, Location 4 | from chess_py import Pawn, Knight, Bishop, Rook, Queen, King, piece_const, converter 5 | 6 | 7 | class TestBoard(TestCase): 8 | def setUp(self): 9 | self.board = Board.init_default() 10 | 11 | def test_init_default(self): 12 | white = color.white 13 | black = color.black 14 | test = Board([ 15 | 16 | # First rank 17 | [Rook(white, Location(0, 0)), Knight(white, Location(0, 1)), Bishop(white, Location(0, 2)), 18 | Queen(white, Location(0, 3)), King(white, Location(0, 4)), Bishop(white, Location(0, 5)), 19 | Knight(white, Location(0, 6)), Rook(white, Location(0, 7))], 20 | 21 | # Second rank 22 | [Pawn(white, Location(1, file)) for file in range(8)], 23 | 24 | 25 | # Third rank 26 | [None for _ in range(8)], 27 | 28 | # Fourth rank 29 | [None for _ in range(8)], 30 | 31 | # Fifth rank 32 | [None for _ in range(8)], 33 | 34 | # Sixth rank 35 | [None for _ in range(8)], 36 | 37 | # Seventh rank 38 | [Pawn(black, Location(6, file)) for file in range(8)], 39 | 40 | # Eighth rank 41 | [Rook(black, Location(7, 0)), Knight(black, Location(7, 1)), Bishop(black, Location(7, 2)), 42 | Queen(black, Location(7, 3)), King(black, Location(7, 4)), Bishop(black, Location(7, 5)), 43 | Knight(black, Location(7, 6)), Rook(black, Location(7, 7))]]) 44 | 45 | self.assertEqual(self.board, test) 46 | 47 | def test_copy(self): 48 | tester = Board.init_default() 49 | 50 | for num, row in enumerate(self.board.position): 51 | for index, piece in enumerate(row): 52 | self.assertEqual(piece, tester.position[num][index]) 53 | 54 | def test_piece_at_square(self): 55 | self.assertEqual(self.board.piece_at_square(Location(0, 0)), 56 | Rook(color.white, Location(0, 0))) 57 | 58 | self.assertEqual(self.board.piece_at_square(Location(1, 0)), 59 | Pawn(color.white, Location(1, 0))) 60 | 61 | self.assertEqual(self.board.piece_at_square(Location(0, 1)), 62 | Knight(color.white, Location(0, 1))) 63 | 64 | def test_is_square_empty(self): 65 | self.assertTrue(self.board.is_square_empty(Location(2, 0))) 66 | self.assertFalse(self.board.is_square_empty(Location(0, 3))) 67 | 68 | def test_material_advantage_parity(self): 69 | self.assertEqual(self.board.material_advantage(color.white, piece_const.PieceValues()), 0) 70 | self.assertEqual(self.board.material_advantage(color.black, piece_const.PieceValues()), 0) 71 | 72 | def test_material_advantage_black_advantage(self): 73 | self.board.position[0][0] = None 74 | 75 | self.assertEqual(self.board.material_advantage(color.white, piece_const.PieceValues()), -5) 76 | self.assertEqual(self.board.material_advantage(color.black, piece_const.PieceValues()), 5) 77 | 78 | self.board = Board.init_default() 79 | self.board.position[0][1] = None 80 | 81 | self.assertEqual(self.board.material_advantage(color.white, piece_const.PieceValues()), -3) 82 | self.assertEqual(self.board.material_advantage(color.black, piece_const.PieceValues()), 3) 83 | 84 | def test_material_advantage_white_advantage(self): 85 | self.board = Board.init_default() 86 | self.board.position[7][0] = None 87 | 88 | self.assertEqual(self.board.material_advantage(color.white, piece_const.PieceValues()), 5) 89 | self.assertEqual(self.board.material_advantage(color.black, piece_const.PieceValues()), -5) 90 | 91 | self.board = Board.init_default() 92 | self.board.position[7][3] = None 93 | 94 | self.assertEqual(self.board.material_advantage(color.white, piece_const.PieceValues()), 9) 95 | self.assertEqual(self.board.material_advantage(color.black, piece_const.PieceValues()), -9) 96 | 97 | self.board = Board.init_default() 98 | self.board.position[7][2] = None 99 | 100 | self.assertEqual(self.board.material_advantage(color.white, piece_const.PieceValues()), 3.5) 101 | self.assertEqual(self.board.material_advantage(color.black, piece_const.PieceValues()), -3.5) 102 | 103 | def test_advantage_as_result(self): 104 | self.assertEqual(self.board.advantage_as_result(converter.long_alg("e2e4", self.board), 105 | piece_const.PieceValues()), 0) 106 | 107 | self.board.position[1][3] = None 108 | self.assertEqual( 109 | self.board.advantage_as_result(converter.long_alg("d1d7", self.board), piece_const.PieceValues()), 0) 110 | 111 | self.board.update(converter.short_alg("e3", color.white, self.board)) 112 | 113 | self.assertEqual( 114 | self.board.advantage_as_result( 115 | converter.short_alg("Bd2", color.white, self.board), piece_const.PieceValues()), -1) 116 | 117 | def test_all_possible_moves_1(self): 118 | """ 119 | Print statement to easily get the list of moves in string form. 120 | Used for constructing tests. 121 | 122 | for move in self.board.all_possible_moves(color.white): 123 | print("\""+ str(move) + "\", ", end="") 124 | """ 125 | moves = {"b1c3", "b1a3", "g1h3", "g1f3", "a2a3", "a2a4", "b2b3", "b2b4", 126 | "c2c3", "c2c4", "d2d3", "d2d4", "e2e3", "e2e4", "f2f3", "f2f4", 127 | "g2g3", "g2g4", "h2h3", "h2h4"} 128 | 129 | self.assertEqual(moves, {str(move) for move in self.board.all_possible_moves(color.white)}) 130 | 131 | def test_all_possible_moves_2(self): 132 | self.board.update(converter.long_alg("e2e4", self.board)) 133 | 134 | moves = {"a7a6", "a7a5", "b7b6", "b7b5", "c7c6", "c7c5", "d7d6", "d7d5", 135 | "e7e6", "e7e5", "f7f6", "f7f5", "g7g6", "g7g5", "h7h6", "h7h5", 136 | "b8a6", "b8c6", "g8f6", "g8h6"} 137 | 138 | self.assertEqual(moves, {str(move) for move in self.board.all_possible_moves(color.black)}) 139 | 140 | def test_no_moves(self): 141 | self.assertFalse(self.board.no_moves(color.white)) 142 | self.assertFalse(self.board.no_moves(color.black)) 143 | 144 | # Scholar's Mate 145 | self.board.update(converter.short_alg("f4", color.white, self.board)) 146 | self.board.update(converter.short_alg("e5", color.black, self.board)) 147 | self.board.update(converter.short_alg("g4", color.white, self.board)) 148 | self.board.update(converter.short_alg("Qh4", color.black, self.board)) 149 | 150 | self.assertTrue(self.board.no_moves(color.white)) 151 | 152 | def test_find_piece(self): 153 | self.assertEqual(self.board.find_piece(Rook(color.white, Location(0, 0))), 154 | Location(0, 0)) 155 | 156 | self.assertEqual(self.board.find_piece(Rook(color.black, Location(7, 0))), 157 | Location(7, 0)) 158 | 159 | self.assertNotEqual(self.board.find_piece(Rook(color.black, Location(7, 0))), 160 | Location(3, 0)) 161 | 162 | self.assertEqual(self.board.find_piece(Pawn(color.white, Location(0, 0))), 163 | Location.from_string("a2")) 164 | 165 | self.assertEqual(self.board.find_piece(Knight(color.white, Location(0, 0))), 166 | Location.from_string("b1")) 167 | 168 | def test_find_king(self): 169 | self.assertEqual(self.board.find_king(color.white), 170 | Location(0, 4)) 171 | 172 | self.assertEqual(self.board.find_king(color.black), 173 | Location(7, 4)) 174 | 175 | def test_get_king(self): 176 | self.assertEqual(self.board.get_king(color.white), 177 | King(color.white, Location(0, 4))) 178 | 179 | self.assertEqual(self.board.get_king(color.black), 180 | King(color.black, Location(7, 4))) 181 | 182 | def test_remove_piece_at_square(self): 183 | test_board = Board.init_default() 184 | test_board.position[0][0] = None 185 | self.board.remove_piece_at_square(Location(0, 0)) 186 | self.assertEqual(self.board, test_board) 187 | 188 | def test_place_piece_at_square(self): 189 | test = Board.init_default() 190 | pawn = Pawn(color.white, Location.from_string("e3")) 191 | 192 | test.position[2][4] = pawn 193 | 194 | self.board.place_piece_at_square(pawn, Location.from_string("e3")) 195 | 196 | self.assertEqual(self.board, test) 197 | 198 | def test_move_piece(self): 199 | test = Board.init_default() 200 | pawn = test.position[1][4] 201 | test.position[1][4] = None 202 | test.position[3][4] = pawn 203 | 204 | self.board.move_piece(Location.from_string("e2"), Location.from_string("e4")) 205 | 206 | self.assertEqual(self.board, test) 207 | 208 | def test_update_pawn_moves_one_step(self): 209 | pawn = self.board.piece_at_square(Location.from_string("e2")) 210 | self.board.update(converter.long_alg("e2e3", self.board)) 211 | 212 | self.assertIsInstance(pawn, Pawn) 213 | self.assertEqual(self.board.piece_at_square(Location.from_string("e3")), pawn) 214 | self.assertIsNone(self.board.piece_at_square(Location.from_string("e2"))) 215 | self.assertFalse(pawn.just_moved_two_steps) 216 | 217 | def test_update_pawn_moves_two_steps(self): 218 | pawn = self.board.piece_at_square(Location.from_string("e2")) 219 | self.board.update(converter.long_alg("e2e4", self.board)) 220 | 221 | self.assertIsInstance(pawn, Pawn) 222 | self.assertEqual(self.board.piece_at_square(Location.from_string("e4")), pawn) 223 | self.assertIsNone(self.board.piece_at_square(Location.from_string("e2"))) 224 | self.assertIsNone(self.board.piece_at_square(Location.from_string("e3"))) 225 | self.assertTrue(pawn.just_moved_two_steps) 226 | 227 | self.board.update(converter.long_alg("d2d4", self.board)) 228 | 229 | self.assertFalse(pawn.just_moved_two_steps) 230 | 231 | def test_update_moves_king_side_castle(self): 232 | self.board.update(converter.short_alg("e4", color.white, self.board)) 233 | self.board.update(converter.short_alg("Nf3", color.white, self.board)) 234 | self.board.update(converter.short_alg("Be2", color.white, self.board)) 235 | self.board.update(converter.short_alg("o-o", color.white, self.board)) 236 | 237 | king = self.board.piece_at_square(Location.from_string("g1")) 238 | self.assertIsInstance(king, King) 239 | self.assertTrue(king.has_moved) 240 | 241 | rook = self.board.piece_at_square(Location.from_string("f1")) 242 | self.assertIsInstance(rook, Rook) 243 | self.assertTrue(rook.has_moved) 244 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # chess_py documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Sep 17 16:32:26 2016. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | sys.path.insert(0, os.path.abspath("../")) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [ 32 | 'sphinx.ext.autodoc', 33 | 'sphinx.ext.todo', 34 | 'sphinx.ext.viewcode', 35 | ] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ['_templates'] 39 | 40 | # The suffix(es) of source filenames. 41 | # You can specify multiple suffix as a list of string: 42 | # source_suffix = ['.rst', '.md'] 43 | source_suffix = '.rst' 44 | 45 | # The encoding of source files. 46 | #source_encoding = 'utf-8-sig' 47 | 48 | # The master toctree document. 49 | master_doc = 'index' 50 | 51 | # General information about the project. 52 | project = u'chess_py' 53 | copyright = u'2016, Aubhro Sengupta' 54 | author = u'Aubhro Sengupta' 55 | 56 | # The version info for the project you're documenting, acts as replacement for 57 | # |version| and |release|, also used in various other places throughout the 58 | # built documents. 59 | # 60 | # The short X.Y version. 61 | version = u'' 62 | # The full version, including alpha/beta/rc tags. 63 | release = u'' 64 | 65 | # The language for content autogenerated by Sphinx. Refer to documentation 66 | # for a list of supported languages. 67 | # 68 | # This is also used if you do content translation via gettext catalogs. 69 | # Usually you set "language" from the command line for these cases. 70 | language = 'en' 71 | 72 | # There are two options for replacing |today|: either, you set today to some 73 | # non-false value, then it is used: 74 | #today = '' 75 | # Else, today_fmt is used as the format for a strftime call. 76 | #today_fmt = '%B %d, %Y' 77 | 78 | # List of patterns, relative to source directory, that match files and 79 | # directories to ignore when looking for source files. 80 | # This patterns also effect to html_static_path and html_extra_path 81 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 82 | 83 | # The reST default role (used for this markup: `text`) to use for all 84 | # documents. 85 | #default_role = None 86 | 87 | # If true, '()' will be appended to :func: etc. cross-reference text. 88 | #add_function_parentheses = True 89 | 90 | # If true, the current module name will be prepended to all description 91 | # unit titles (such as .. function::). 92 | #add_module_names = True 93 | 94 | # If true, sectionauthor and moduleauthor directives will be shown in the 95 | # output. They are ignored by default. 96 | #show_authors = False 97 | 98 | # The name of the Pygments (syntax highlighting) style to use. 99 | pygments_style = 'sphinx' 100 | 101 | # A list of ignored prefixes for module index sorting. 102 | #modindex_common_prefix = [] 103 | 104 | # If true, keep warnings as "system message" paragraphs in the built documents. 105 | #keep_warnings = False 106 | 107 | # If true, `todo` and `todoList` produce output, else they produce nothing. 108 | todo_include_todos = True 109 | 110 | 111 | # -- Options for HTML output ---------------------------------------------- 112 | 113 | # The theme to use for HTML and HTML Help pages. See the documentation for 114 | # a list of builtin themes. 115 | html_theme = 'classic' 116 | 117 | # Theme options are theme-specific and customize the look and feel of a theme 118 | # further. For a list of options available for each theme, see the 119 | # documentation. 120 | #html_theme_options = {} 121 | 122 | # Add any paths that contain custom themes here, relative to this directory. 123 | #html_theme_path = [] 124 | 125 | # The name for this set of Sphinx documents. 126 | # " v documentation" by default. 127 | #html_title = u'chess_py v' 128 | 129 | # A shorter title for the navigation bar. Default is the same as html_title. 130 | #html_short_title = None 131 | 132 | # The name of an image file (relative to this directory) to place at the top 133 | # of the sidebar. 134 | #html_logo = None 135 | 136 | # The name of an image file (relative to this directory) to use as a favicon of 137 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 138 | # pixels large. 139 | #html_favicon = None 140 | 141 | # Add any paths that contain custom static files (such as style sheets) here, 142 | # relative to this directory. They are copied after the builtin static files, 143 | # so a file named "default.css" will overwrite the builtin "default.css". 144 | html_static_path = ['_static'] 145 | 146 | # Add any extra paths that contain custom files (such as robots.txt or 147 | # .htaccess) here, relative to this directory. These files are copied 148 | # directly to the root of the documentation. 149 | #html_extra_path = [] 150 | 151 | # If not None, a 'Last updated on:' timestamp is inserted at every page 152 | # bottom, using the given strftime format. 153 | # The empty string is equivalent to '%b %d, %Y'. 154 | #html_last_updated_fmt = None 155 | 156 | # If true, SmartyPants will be used to convert quotes and dashes to 157 | # typographically correct entities. 158 | #html_use_smartypants = True 159 | 160 | # Custom sidebar templates, maps document names to template names. 161 | #html_sidebars = {} 162 | 163 | # Additional templates that should be rendered to pages, maps page names to 164 | # template names. 165 | #html_additional_pages = {} 166 | 167 | # If false, no module index is generated. 168 | #html_domain_indices = True 169 | 170 | # If false, no index is generated. 171 | #html_use_index = True 172 | 173 | # If true, the index is split into individual pages for each letter. 174 | #html_split_index = False 175 | 176 | # If true, links to the reST sources are added to the pages. 177 | #html_show_sourcelink = True 178 | 179 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 180 | #html_show_sphinx = True 181 | 182 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 183 | #html_show_copyright = True 184 | 185 | # If true, an OpenSearch description file will be output, and all pages will 186 | # contain a tag referring to it. The value of this option must be the 187 | # base URL from which the finished HTML is served. 188 | #html_use_opensearch = '' 189 | 190 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 191 | #html_file_suffix = None 192 | 193 | # Language to be used for generating the HTML full-text search index. 194 | # Sphinx supports the following languages: 195 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 196 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' 197 | #html_search_language = 'en' 198 | 199 | # A dictionary with options for the search language support, empty by default. 200 | # 'ja' uses this config value. 201 | # 'zh' user can custom change `jieba` dictionary path. 202 | #html_search_options = {'type': 'default'} 203 | 204 | # The name of a javascript file (relative to the configuration directory) that 205 | # implements a search results scorer. If empty, the default will be used. 206 | #html_search_scorer = 'scorer.js' 207 | 208 | # Output file base name for HTML help builder. 209 | htmlhelp_basename = 'chess_pydoc' 210 | 211 | # -- Options for LaTeX output --------------------------------------------- 212 | 213 | latex_elements = { 214 | # The paper size ('letterpaper' or 'a4paper'). 215 | #'papersize': 'letterpaper', 216 | 217 | # The font size ('10pt', '11pt' or '12pt'). 218 | #'pointsize': '10pt', 219 | 220 | # Additional stuff for the LaTeX preamble. 221 | #'preamble': '', 222 | 223 | # Latex figure (float) alignment 224 | #'figure_align': 'htbp', 225 | } 226 | 227 | # Grouping the document tree into LaTeX files. List of tuples 228 | # (source start file, target name, title, 229 | # author, documentclass [howto, manual, or own class]). 230 | latex_documents = [ 231 | (master_doc, 'chess_py.tex', u'chess\\_py Documentation', 232 | u'Author', 'manual'), 233 | ] 234 | 235 | # The name of an image file (relative to this directory) to place at the top of 236 | # the title page. 237 | #latex_logo = None 238 | 239 | # For "manual" documents, if this is true, then toplevel headings are parts, 240 | # not chapters. 241 | #latex_use_parts = False 242 | 243 | # If true, show page references after internal links. 244 | #latex_show_pagerefs = False 245 | 246 | # If true, show URL addresses after external links. 247 | #latex_show_urls = False 248 | 249 | # Documents to append as an appendix to all manuals. 250 | #latex_appendices = [] 251 | 252 | # If false, no module index is generated. 253 | #latex_domain_indices = True 254 | 255 | 256 | # -- Options for manual page output --------------------------------------- 257 | 258 | # One entry per manual page. List of tuples 259 | # (source start file, name, description, authors, manual section). 260 | man_pages = [ 261 | (master_doc, 'chess_py', u'chess_py Documentation', 262 | [author], 1) 263 | ] 264 | 265 | # If true, show URL addresses after external links. 266 | #man_show_urls = False 267 | 268 | 269 | # -- Options for Texinfo output ------------------------------------------- 270 | 271 | # Grouping the document tree into Texinfo files. List of tuples 272 | # (source start file, target name, title, author, 273 | # dir menu entry, description, category) 274 | texinfo_documents = [ 275 | (master_doc, 'chess_py', u'chess_py Documentation', 276 | author, 'chess_py', 'One line description of project.', 277 | 'Miscellaneous'), 278 | ] 279 | 280 | # Documents to append as an appendix to all manuals. 281 | #texinfo_appendices = [] 282 | 283 | # If false, no module index is generated. 284 | #texinfo_domain_indices = True 285 | 286 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 287 | #texinfo_show_urls = 'footnote' 288 | 289 | # If true, do not generate a @detailmenu in the "Top" node's menu. 290 | #texinfo_no_detailmenu = False 291 | 292 | 293 | # -- Options for Epub output ---------------------------------------------- 294 | 295 | # Bibliographic Dublin Core info. 296 | epub_title = project 297 | epub_author = author 298 | epub_publisher = author 299 | epub_copyright = copyright 300 | 301 | # The basename for the epub file. It defaults to the project name. 302 | #epub_basename = project 303 | 304 | # The HTML theme for the epub output. Since the default themes are not 305 | # optimized for small screen space, using the same theme for HTML and epub 306 | # output is usually not wise. This defaults to 'epub', a theme designed to save 307 | # visual space. 308 | #epub_theme = 'epub' 309 | 310 | # The language of the text. It defaults to the language option 311 | # or 'en' if the language is not set. 312 | #epub_language = '' 313 | 314 | # The scheme of the identifier. Typical schemes are ISBN or URL. 315 | #epub_scheme = '' 316 | 317 | # The unique identifier of the text. This can be a ISBN number 318 | # or the project homepage. 319 | #epub_identifier = '' 320 | 321 | # A unique identification for the text. 322 | #epub_uid = '' 323 | 324 | # A tuple containing the cover image and cover page html template filenames. 325 | #epub_cover = () 326 | 327 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 328 | #epub_guide = () 329 | 330 | # HTML files that should be inserted before the pages created by sphinx. 331 | # The format is a list of tuples containing the path and title. 332 | #epub_pre_files = [] 333 | 334 | # HTML files that should be inserted after the pages created by sphinx. 335 | # The format is a list of tuples containing the path and title. 336 | #epub_post_files = [] 337 | 338 | # A list of files that should not be packed into the epub file. 339 | epub_exclude_files = ['search.html'] 340 | 341 | # The depth of the table of contents in toc.ncx. 342 | #epub_tocdepth = 3 343 | 344 | # Allow duplicate toc entries. 345 | #epub_tocdup = True 346 | 347 | # Choose between 'default' and 'includehidden'. 348 | #epub_tocscope = 'default' 349 | 350 | # Fix unsupported image types using the Pillow. 351 | #epub_fix_images = False 352 | 353 | # Scale large images. 354 | #epub_max_image_width = 0 355 | 356 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 357 | #epub_show_urls = 'inline' 358 | 359 | # If false, no index is generated. 360 | #epub_use_index = True 361 | -------------------------------------------------------------------------------- /chess_py/core/board.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Constructs board object which stores the get_location of all the pieces. 5 | 6 | Default Array 7 | 8 | | [[0th row 0th item, 0th row 1st item, 0th row 2nd item], 9 | | [1st row 0th item, 1st row 1st item, 1st row 2nd item], 10 | | [2nd row 0th item, 2nd row 1st item, 2nd row 2nd item]] 11 | 12 | | Default board 13 | | 8 ║♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜ Black pieces 14 | | 7 ║♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟ Black pawns 15 | | 6 ║a6… … … … … …h6 16 | | 5 ║… … … … … … … … 17 | | 4 ║… … … … … … … … 18 | | 3 ║a3… … … … … …h3 Algebraic 19 | | 2 ║♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙ White pawns 20 | | 1 ║♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖ White pieces 21 | | -—╚═══════════════ 22 | | ——-a b c d e f g h 23 | 24 | Pieces on the board are flipped in position array so white home row is at index 0 25 | and black home row is at index 7 26 | 27 | | Copyright © 2016 Aubhro Sengupta. All rights reserved. 28 | """ 29 | 30 | from __future__ import print_function 31 | 32 | import inspect 33 | from multiprocessing import Process 34 | from copy import copy as cp 35 | from math import fabs 36 | 37 | from .color import white, black 38 | from .algebraic import notation_const 39 | from .algebraic.location import Location 40 | from .algebraic.move import Move 41 | from ..pieces.piece import Piece 42 | from ..pieces.bishop import Bishop 43 | from ..pieces.king import King 44 | from ..pieces.pawn import Pawn 45 | from ..pieces.queen import Queen 46 | from ..pieces.rook import Rook 47 | from ..pieces.knight import Knight 48 | 49 | 50 | class Board: 51 | """ 52 | Standard starting position in a chess game. 53 | Initialized upon startup and is used when init_default constructor is used 54 | 55 | """ 56 | 57 | def __init__(self, position): 58 | """ 59 | Creates a ``Board`` given an array of ``Piece`` and ``None`` 60 | objects to represent the given position of the board. 61 | 62 | :type: position: list 63 | """ 64 | self.position = position 65 | self.possible_moves = dict() 66 | try: 67 | self.king_loc_dict = {white: self.find_king(white), 68 | black: self.find_king(black)} 69 | except ValueError: 70 | self.king_loc_dict = None 71 | 72 | @classmethod 73 | def init_default(cls): 74 | """ 75 | Creates a ``Board`` with the standard chess starting position. 76 | 77 | :rtype: Board 78 | """ 79 | return cls([ 80 | 81 | # First rank 82 | [Rook(white, Location(0, 0)), Knight(white, Location(0, 1)), Bishop(white, Location(0, 2)), 83 | Queen(white, Location(0, 3)), King(white, Location(0, 4)), Bishop(white, Location(0, 5)), 84 | Knight(white, Location(0, 6)), Rook(white, Location(0, 7))], 85 | 86 | # Second rank 87 | [Pawn(white, Location(1, file)) for file in range(8)], 88 | 89 | # Third rank 90 | [None for _ in range(8)], 91 | 92 | # Fourth rank 93 | [None for _ in range(8)], 94 | 95 | # Fifth rank 96 | [None for _ in range(8)], 97 | 98 | # Sixth rank 99 | [None for _ in range(8)], 100 | 101 | # Seventh rank 102 | [Pawn(black, Location(6, file)) for file in range(8)], 103 | 104 | # Eighth rank 105 | [Rook(black, Location(7, 0)), Knight(black, Location(7, 1)), Bishop(black, Location(7, 2)), 106 | Queen(black, Location(7, 3)), King(black, Location(7, 4)), Bishop(black, Location(7, 5)), 107 | Knight(black, Location(7, 6)), Rook(black, Location(7, 7))] 108 | ]) 109 | 110 | @property 111 | def position_tuple(self): 112 | return ((str(piece) for piece in self.position[index]) for index, row in enumerate(self.position)) 113 | 114 | def __key(self): 115 | return self.position 116 | 117 | def __hash__(self): 118 | return hash(tuple([hash(piece) for piece in self])) 119 | 120 | def __eq__(self, other): 121 | if not isinstance(other, self.__class__): 122 | raise TypeError("Cannot compare other type to Board") 123 | 124 | for i, row in enumerate(self.position): 125 | for j, piece in enumerate(row): 126 | 127 | if piece != other.position[i][j]: 128 | return False 129 | 130 | return True 131 | 132 | def __ne__(self, other): 133 | return not self.__eq__(other) 134 | 135 | def __str__(self): 136 | board_string = "" 137 | for i, row in enumerate(self.position): 138 | board_string += str(8 - i) + " " 139 | for j, square in enumerate(row): 140 | 141 | piece = self.piece_at_square(Location(7 - i, j)) 142 | if isinstance(piece, Piece): 143 | board_string += piece.symbol + " " 144 | else: 145 | board_string += "_ " 146 | 147 | board_string += "\n" 148 | 149 | board_string += " a b c d e f g h" 150 | return board_string 151 | 152 | def __iter__(self): 153 | for row in self.position: 154 | for square in row: 155 | yield square 156 | 157 | def __copy__(self): 158 | """ 159 | Copies the board faster than deepcopy 160 | 161 | :rtype: Board 162 | """ 163 | return Board([[cp(piece) or None 164 | for piece in self.position[index]] 165 | for index, row in enumerate(self.position)]) 166 | 167 | def piece_at_square(self, location): 168 | """ 169 | Finds the chess piece at a square of the position. 170 | 171 | :type: location: Location 172 | :rtype: Piece 173 | """ 174 | return self.position[location.rank][location.file] 175 | 176 | def is_square_empty(self, location): 177 | """ 178 | Finds whether a chess piece occupies a square of the position. 179 | 180 | :type: location: Location 181 | :rtype: bool 182 | """ 183 | return self.position[location.rank][location.file] is None 184 | 185 | def material_advantage(self, input_color, val_scheme): 186 | """ 187 | Finds the advantage a particular side possesses given a value scheme. 188 | 189 | :type: input_color: Color 190 | :type: val_scheme: PieceValues 191 | :rtype: double 192 | """ 193 | 194 | if self.get_king(input_color).in_check(self) and self.no_moves(input_color): 195 | return -100 196 | 197 | if self.get_king(-input_color).in_check(self) and self.no_moves(-input_color): 198 | return 100 199 | 200 | return sum([val_scheme.val(piece, input_color) for piece in self]) 201 | 202 | def advantage_as_result(self, move, val_scheme): 203 | """ 204 | Calculates advantage after move is played 205 | 206 | :type: move: Move 207 | :type: val_scheme: PieceValues 208 | :rtype: double 209 | """ 210 | test_board = cp(self) 211 | test_board.update(move) 212 | return test_board.material_advantage(move.color, val_scheme) 213 | 214 | def all_possible_moves(self, input_color): 215 | """ 216 | Checks if all the possible moves has already been calculated 217 | and is stored in `possible_moves` dictionary. If not, it is calculated 218 | with `_calc_all_possible_moves`. 219 | 220 | :type: input_color: Color 221 | :rtype: list 222 | """ 223 | position_tuple = self.position_tuple 224 | if position_tuple not in self.possible_moves: 225 | self.possible_moves[position_tuple] = tuple(self._calc_all_possible_moves(input_color)) 226 | 227 | return self.possible_moves[position_tuple] 228 | 229 | def _calc_all_possible_moves(self, input_color): 230 | """ 231 | Returns list of all possible moves 232 | 233 | :type: input_color: Color 234 | :rtype: list 235 | """ 236 | for piece in self: 237 | 238 | # Tests if square on the board is not empty 239 | if piece is not None and piece.color == input_color: 240 | 241 | for move in piece.possible_moves(self): 242 | 243 | test = cp(self) 244 | test_move = Move(end_loc=move.end_loc, 245 | piece=test.piece_at_square(move.start_loc), 246 | status=move.status, 247 | start_loc=move.start_loc, 248 | promoted_to_piece=move.promoted_to_piece) 249 | test.update(test_move) 250 | 251 | if self.king_loc_dict is None: 252 | yield move 253 | continue 254 | 255 | my_king = test.piece_at_square(self.king_loc_dict[input_color]) 256 | 257 | if my_king is None or \ 258 | not isinstance(my_king, King) or \ 259 | my_king.color != input_color: 260 | self.king_loc_dict[input_color] = test.find_king(input_color) 261 | my_king = test.piece_at_square(self.king_loc_dict[input_color]) 262 | 263 | if not my_king.in_check(test): 264 | yield move 265 | 266 | def runInParallel(*fns): 267 | """ 268 | Runs multiple processes in parallel. 269 | 270 | :type: fns: def 271 | """ 272 | proc = [] 273 | for fn in fns: 274 | p = Process(target=fn) 275 | p.start() 276 | proc.append(p) 277 | for p in proc: 278 | p.join() 279 | 280 | def no_moves(self, input_color): 281 | 282 | # Loops through columns 283 | for piece in self: 284 | 285 | # Tests if square on the board is not empty 286 | if piece is not None and piece.color == input_color: 287 | 288 | for move in piece.possible_moves(self): 289 | 290 | test = cp(self) 291 | test.update(move) 292 | 293 | if not test.get_king(input_color).in_check(test): 294 | return False 295 | 296 | return True 297 | 298 | def find_piece(self, piece): 299 | """ 300 | Finds Location of the first piece that matches piece. 301 | If none is found, Exception is raised. 302 | 303 | :type: piece: Piece 304 | :rtype: Location 305 | """ 306 | for i, _ in enumerate(self.position): 307 | for j, _ in enumerate(self.position): 308 | loc = Location(i, j) 309 | 310 | if not self.is_square_empty(loc) and \ 311 | self.piece_at_square(loc) == piece: 312 | return loc 313 | 314 | raise ValueError("{} \nPiece not found: {}".format(self, piece)) 315 | 316 | def get_piece(self, piece_type, input_color): 317 | """ 318 | Gets location of a piece on the board given the type and color. 319 | 320 | :type: piece_type: Piece 321 | :type: input_color: Color 322 | :rtype: Location 323 | """ 324 | for loc in self: 325 | piece = self.piece_at_square(loc) 326 | 327 | if not self.is_square_empty(loc) and \ 328 | isinstance(piece, piece_type) and \ 329 | piece.color == input_color: 330 | return loc 331 | 332 | raise Exception("{} \nPiece not found: {}".format(self, piece_type)) 333 | 334 | def find_king(self, input_color): 335 | """ 336 | Finds the Location of the King of input_color 337 | 338 | :type: input_color: Color 339 | :rtype: Location 340 | """ 341 | return self.find_piece(King(input_color, Location(0, 0))) 342 | 343 | def get_king(self, input_color): 344 | """ 345 | Returns King of input_color 346 | 347 | :type: input_color: Color 348 | :rtype: King 349 | """ 350 | return self.piece_at_square(self.find_king(input_color)) 351 | 352 | def remove_piece_at_square(self, location): 353 | """ 354 | Removes piece at square 355 | 356 | :type: location: Location 357 | """ 358 | self.position[location.rank][location.file] = None 359 | 360 | def place_piece_at_square(self, piece, location): 361 | """ 362 | Places piece at given get_location 363 | 364 | :type: piece: Piece 365 | :type: location: Location 366 | """ 367 | self.position[location.rank][location.file] = piece 368 | piece.location = location 369 | 370 | def move_piece(self, initial, final): 371 | """ 372 | Moves piece from one location to another 373 | 374 | :type: initial: Location 375 | :type: final: Location 376 | """ 377 | self.place_piece_at_square(self.piece_at_square(initial), final) 378 | self.remove_piece_at_square(initial) 379 | 380 | def update(self, move): 381 | """ 382 | Updates position by applying selected move 383 | 384 | :type: move: Move 385 | """ 386 | if move is None: 387 | raise TypeError("Move cannot be type None") 388 | 389 | if self.king_loc_dict is not None and isinstance(move.piece, King): 390 | self.king_loc_dict[move.color] = move.end_loc 391 | 392 | # Invalidates en-passant 393 | for square in self: 394 | pawn = square 395 | if isinstance(pawn, Pawn): 396 | pawn.just_moved_two_steps = False 397 | 398 | # Sets King and Rook has_moved property to True is piece has moved 399 | if type(move.piece) is King or type(move.piece) is Rook: 400 | move.piece.has_moved = True 401 | 402 | elif move.status == notation_const.MOVEMENT and \ 403 | isinstance(move.piece, Pawn) and \ 404 | fabs(move.end_loc.rank - move.start_loc.rank) == 2: 405 | move.piece.just_moved_two_steps = True 406 | 407 | if move.status == notation_const.KING_SIDE_CASTLE: 408 | self.move_piece(Location(move.end_loc.rank, 7), Location(move.end_loc.rank, 5)) 409 | self.piece_at_square(Location(move.end_loc.rank, 5)).has_moved = True 410 | 411 | elif move.status == notation_const.QUEEN_SIDE_CASTLE: 412 | self.move_piece(Location(move.end_loc.rank, 0), Location(move.end_loc.rank, 3)) 413 | self.piece_at_square(Location(move.end_loc.rank, 3)).has_moved = True 414 | 415 | elif move.status == notation_const.EN_PASSANT: 416 | self.remove_piece_at_square(Location(move.start_loc.rank, move.end_loc.file)) 417 | 418 | elif move.status == notation_const.PROMOTE or \ 419 | move.status == notation_const.CAPTURE_AND_PROMOTE: 420 | try: 421 | self.remove_piece_at_square(move.start_loc) 422 | self.place_piece_at_square(move.promoted_to_piece(move.color, move.end_loc), move.end_loc) 423 | except TypeError as e: 424 | raise ValueError("Promoted to piece cannot be None in Move {}\n{}".format(repr(move), e)) 425 | return 426 | 427 | self.move_piece(move.piece.location, move.end_loc) 428 | -------------------------------------------------------------------------------- /chess_py/core/algebraic/converter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Methods that take external input and attempt 5 | to turn them into usable commands. 6 | 7 | Copyright © 2016 Aubhro Sengupta. All rights reserved. 8 | """ 9 | from copy import copy as cp 10 | 11 | from .. import color 12 | from . import notation_const 13 | from .location import Location 14 | from .move import Move 15 | from ..board import Board 16 | from ...pieces.bishop import Bishop 17 | from ...pieces.king import King 18 | from ...pieces.pawn import Pawn 19 | from ...pieces.queen import Queen 20 | from ...pieces.rook import Rook 21 | from ...pieces.knight import Knight 22 | 23 | 24 | def _get_piece(string, index): 25 | """ 26 | Returns Piece subclass given index of piece. 27 | 28 | :type: index: int 29 | :type: loc Location 30 | 31 | :raise: KeyError 32 | """ 33 | piece = string[index].strip() 34 | piece = piece.upper() 35 | piece_dict = {'R': Rook, 36 | 'P': Pawn, 37 | 'B': Bishop, 38 | 'N': Knight, 39 | 'Q': Queen, 40 | 'K': King} 41 | try: 42 | return piece_dict[piece] 43 | except KeyError: 44 | raise ValueError("Piece {} is invalid".format(piece)) 45 | 46 | 47 | def _get_piece_start_location(end_location, 48 | input_color, 49 | piece_in_move, 50 | position, 51 | start_rank=None, 52 | start_file=None): 53 | try: 54 | start_rank = int(start_rank) - 1 55 | except TypeError: 56 | pass 57 | try: 58 | start_file = ord(start_file) - 97 59 | except TypeError: 60 | pass 61 | 62 | def _is_at_start_rank_and_file(potential_start): 63 | if start_rank is not None and start_file is not None: 64 | return start_rank == potential_start.rank and \ 65 | start_file == potential_start.file 66 | elif start_rank is not None: 67 | return start_rank == potential_start.rank 68 | elif start_file is not None: 69 | return start_file == potential_start.file 70 | else: 71 | return True 72 | 73 | def _is_valid_move_option(move): 74 | real_piece = position.piece_at_square(move.end_loc) 75 | return type(real_piece) is piece_in_move and \ 76 | real_piece.color == input_color and \ 77 | _is_at_start_rank_and_file(move.end_loc) 78 | 79 | test_piece = piece_in_move(input_color, end_location) 80 | empty_board = Board([[None for _ in range(8)] for _ in range(8)]) 81 | empty_board_valid_moves = [move for move in test_piece.possible_moves(empty_board) 82 | if _is_valid_move_option(move)] 83 | 84 | if len(empty_board_valid_moves) == 1: 85 | return position.piece_at_square(empty_board_valid_moves[0].end_loc), \ 86 | empty_board_valid_moves[0].end_loc 87 | else: 88 | for empty_board_move in empty_board_valid_moves: 89 | poss_piece = position.piece_at_square(empty_board_move.end_loc) 90 | for real_board_move in poss_piece.possible_moves(position): 91 | test_in_check_board = cp(position) 92 | test_move = Move(end_loc=real_board_move.end_loc, 93 | piece=test_in_check_board.piece_at_square(real_board_move.start_loc), 94 | status=real_board_move.status, 95 | start_loc=real_board_move.start_loc, 96 | promoted_to_piece=real_board_move.promoted_to_piece) 97 | test_in_check_board.update(test_move) 98 | if real_board_move.end_loc == end_location and \ 99 | not test_in_check_board.get_king(input_color).in_check(test_in_check_board): 100 | return poss_piece, real_board_move.start_loc 101 | 102 | raise ValueError("No valid piece move found") 103 | 104 | 105 | def incomplete_alg(alg_str, input_color, position): 106 | """ 107 | Converts a string written in short algebraic form into an incomplete move. 108 | These incomplete moves do not have the initial location specified and 109 | therefore cannot be used to update the board. IN order to fully utilize 110 | incomplete move, it must be run through ``make_legal()`` with 111 | the corresponding position. It is recommended to use 112 | ``short_alg()`` instead of this method because it returns a complete 113 | move. 114 | 115 | Examples: e4, Nf3, exd5, Qxf3, 00, 000, e8=Q 116 | 117 | :type: alg_str: str 118 | :type: input_color: Color 119 | """ 120 | edge_rank = 0 \ 121 | if input_color == color.white \ 122 | else 7 123 | 124 | if alg_str is None or len(alg_str) <= 1: 125 | raise ValueError("algebraic string {} is invalid".format(alg_str)) 126 | 127 | # King-side castle 128 | if alg_str in ["00", "oo", "OO", "0-0", "o-o", "O-O"]: 129 | return Move(end_loc=Location(edge_rank, 6), 130 | piece=King(input_color, Location(edge_rank, 4)), 131 | status=notation_const.KING_SIDE_CASTLE, 132 | start_loc=Location(edge_rank, 4)) 133 | 134 | # Queen-side castle 135 | if alg_str in ["000", "ooo", "OOO", "0-0-0", "o-o-o", "O-O-O"]: 136 | return Move(end_loc=Location(edge_rank, 2), 137 | piece=King(input_color, Location(edge_rank, 4)), 138 | status=notation_const.QUEEN_SIDE_CASTLE, 139 | start_loc=Location(edge_rank, 4)) 140 | try: 141 | end_location = Location.from_string(alg_str[-2:]) 142 | except ValueError: 143 | end_location = Location.from_string(alg_str[-4:-2]) 144 | 145 | # Pawn movement 146 | if len(alg_str) == 2: 147 | possible_pawn = position.piece_at_square(end_location.shift_back(input_color)) 148 | if type(possible_pawn) is Pawn and \ 149 | possible_pawn.color == input_color: 150 | start_location = end_location.shift_back(input_color) 151 | else: 152 | start_location = end_location.shift_back(input_color, times=2) 153 | return Move(end_loc=end_location, 154 | piece=position.piece_at_square(start_location), 155 | status=notation_const.MOVEMENT, 156 | start_loc=start_location) 157 | 158 | # Non-pawn Piece movement 159 | if len(alg_str) == 3: 160 | possible_piece, start_location = _get_piece_start_location(end_location, 161 | input_color, 162 | _get_piece(alg_str, 0), 163 | position) 164 | return Move(end_loc=end_location, 165 | piece=possible_piece, 166 | status=notation_const.MOVEMENT, 167 | start_loc=start_location) 168 | 169 | # Multiple options (Capture or Piece movement with file specified) 170 | if len(alg_str) == 4: 171 | 172 | # Capture 173 | if alg_str[1].upper() == "X": 174 | 175 | # Pawn capture 176 | if not alg_str[0].isupper(): 177 | pawn_location = Location(end_location.rank, ord(alg_str[0]) - 97).shift_back(input_color) 178 | possible_pawn = position.piece_at_square(pawn_location) 179 | if type(possible_pawn) is Pawn and \ 180 | possible_pawn.color == input_color: 181 | en_passant_pawn = position.piece_at_square(end_location.shift_back(input_color)) 182 | if type(en_passant_pawn) is Pawn and \ 183 | en_passant_pawn.color != input_color and \ 184 | position.is_square_empty(end_location): 185 | return Move(end_loc=end_location, 186 | piece=position.piece_at_square(pawn_location), 187 | status=notation_const.EN_PASSANT, 188 | start_loc=pawn_location) 189 | else: 190 | return Move(end_loc=end_location, 191 | piece=position.piece_at_square(pawn_location), 192 | status=notation_const.CAPTURE, 193 | start_loc=pawn_location) 194 | 195 | # Piece capture 196 | elif alg_str[0].isupper(): 197 | possible_piece, start_location = _get_piece_start_location(end_location, 198 | input_color, 199 | _get_piece(alg_str, 0), 200 | position) 201 | return Move(end_loc=end_location, 202 | piece=possible_piece, 203 | status=notation_const.CAPTURE, 204 | start_loc=start_location) 205 | 206 | # Pawn Promotion 207 | elif alg_str[2] == "=": 208 | promote_end_loc = Location.from_string(alg_str[:2]) 209 | if promote_end_loc.rank != 0 and promote_end_loc.rank != 7: 210 | raise ValueError("Promotion {} must be on the last rank".format(alg_str)) 211 | return Move(end_loc=promote_end_loc, 212 | piece=Pawn(input_color, promote_end_loc), 213 | status=notation_const.PROMOTE, 214 | promoted_to_piece=_get_piece(alg_str, 3), 215 | start_loc=promote_end_loc.shift_back(input_color)) 216 | 217 | # Non-pawn Piece movement with file specified (aRb7) 218 | elif alg_str[1].isupper() and not alg_str[0].isdigit(): 219 | possible_piece, start_location = _get_piece_start_location(end_location, 220 | input_color, 221 | _get_piece(alg_str, 1), 222 | position, 223 | start_file=alg_str[0]) 224 | return Move(end_loc=end_location, 225 | piece=possible_piece, 226 | status=notation_const.MOVEMENT, 227 | start_loc=start_location) 228 | 229 | # (alt) Non-pawn Piece movement with file specified (Rab7) 230 | elif alg_str[0].isupper() and not alg_str[1].isdigit(): 231 | possible_piece, start_location = _get_piece_start_location(end_location, 232 | input_color, 233 | _get_piece(alg_str, 0), 234 | position, 235 | start_file=alg_str[1]) 236 | return Move(end_loc=end_location, 237 | piece=possible_piece, 238 | status=notation_const.MOVEMENT, 239 | start_loc=start_location) 240 | 241 | # Non-pawn Piece movement with rank specified (R1b7) 242 | elif alg_str[0].isupper() and alg_str[1].isdigit(): 243 | possible_piece, start_location = _get_piece_start_location(end_location, 244 | input_color, 245 | _get_piece(alg_str, 0), 246 | position, 247 | start_rank=alg_str[1]) 248 | return Move(end_loc=end_location, 249 | piece=possible_piece, 250 | status=notation_const.MOVEMENT, 251 | start_loc=start_location) 252 | 253 | # Multiple options 254 | if len(alg_str) == 5: 255 | 256 | # Non-pawn Piece movement with rank and file specified (a2Ra1 257 | if not alg_str[0].isdigit() and \ 258 | alg_str[1].isdigit() and \ 259 | alg_str[2].isupper() and \ 260 | not alg_str[3].isdigit() and \ 261 | alg_str[4].isdigit: 262 | start_loc = Location.from_string(alg_str[:2]) 263 | return Move(end_loc=end_location, 264 | piece=_get_piece(alg_str, 2)(input_color, end_location), 265 | status=notation_const.MOVEMENT, 266 | start_loc=start_loc) 267 | 268 | # Multiple Piece capture options 269 | if alg_str[2].upper() == "X": 270 | 271 | # Piece capture with rank specified (R1xa1) 272 | if alg_str[1].isdigit(): 273 | possible_piece, start_location = _get_piece_start_location(end_location, 274 | input_color, 275 | _get_piece(alg_str, 0), 276 | position, 277 | start_rank=alg_str[1]) 278 | return Move(end_loc=end_location, 279 | piece=possible_piece, 280 | status=notation_const.CAPTURE, 281 | start_loc=start_location) 282 | 283 | # Piece capture with file specified (Rdxd7) 284 | else: 285 | possible_piece, start_location = _get_piece_start_location(end_location, 286 | input_color, 287 | _get_piece(alg_str, 0), 288 | position, 289 | start_file=alg_str[1]) 290 | return Move(end_loc=end_location, 291 | piece=possible_piece, 292 | status=notation_const.CAPTURE, 293 | start_loc=start_location) 294 | 295 | # Pawn promotion with capture 296 | if len(alg_str) == 6 and alg_str[4] == "=": 297 | start_file = ord(alg_str[0]) - 97 298 | promote_capture_end_loc = Location.from_string(alg_str[2:4]) 299 | return Move(end_loc=promote_capture_end_loc, 300 | piece=Pawn(input_color, promote_capture_end_loc), 301 | status=notation_const.CAPTURE_AND_PROMOTE, 302 | promoted_to_piece=_get_piece(alg_str, 5), 303 | start_loc=Location(end_location.shift_back(input_color).rank, start_file)) 304 | 305 | raise ValueError("algebraic string {} is invalid in \n{}".format(alg_str, position)) 306 | 307 | 308 | def make_legal(move, position): 309 | """ 310 | Converts an incomplete move (initial ``Location`` not specified) 311 | and the corresponding position into the a complete move 312 | with the most likely starting point specified. If no moves match, ``None`` 313 | is returned. 314 | 315 | :type: move: Move 316 | :type: position: Board 317 | :rtype: Move 318 | """ 319 | assert isinstance(move, Move) 320 | for legal_move in position.all_possible_moves(move.color): 321 | 322 | if move.status == notation_const.LONG_ALG: 323 | if move.end_loc == legal_move.end_loc and \ 324 | move.start_loc == legal_move.start_loc: 325 | return legal_move 326 | 327 | elif move == legal_move: 328 | return legal_move 329 | 330 | raise ValueError("Move {} not legal in \n{}".format(repr(move), position)) 331 | 332 | 333 | def short_alg(algebraic_string, input_color, position): 334 | """ 335 | Converts a string written in short algebraic form, the color 336 | of the side whose turn it is, and the corresponding position 337 | into a complete move that can be played. If no moves match, 338 | None is returned. 339 | 340 | Examples: e4, Nf3, exd5, Qxf3, 00, 000, e8=Q 341 | 342 | :type: algebraic_string: str 343 | :type: input_color: Color 344 | :type: position: Board 345 | """ 346 | return make_legal(incomplete_alg(algebraic_string, input_color, position), position) 347 | 348 | 349 | def long_alg(alg_str, position): 350 | """ 351 | Converts a string written in long algebraic form 352 | and the corresponding position into a complete move 353 | (initial location specified). Used primarily for 354 | UCI, but can be used for other purposes. 355 | 356 | :type: alg_str: str 357 | :type: position: Board 358 | :rtype: Move 359 | """ 360 | if alg_str is None or len(alg_str) < 4 or len(alg_str) > 6: 361 | raise ValueError("Invalid string input {}".format(alg_str)) 362 | 363 | end = Location.from_string(alg_str[2:]) 364 | start = Location.from_string(alg_str[:2]) 365 | piece = position.piece_at_square(start) 366 | 367 | if len(alg_str) == 4: 368 | return make_legal(Move(end_loc=end, 369 | piece=piece, 370 | status=notation_const.LONG_ALG, 371 | start_loc=start), position) 372 | 373 | promoted_to = _get_piece(alg_str, 4) 374 | if promoted_to is None or \ 375 | promoted_to is King or \ 376 | promoted_to is Pawn: 377 | raise Exception("Invalid move input") 378 | 379 | return make_legal(Move(end_loc=end, 380 | piece=piece, 381 | status=notation_const.LONG_ALG, 382 | start_loc=start, 383 | promoted_to_piece=promoted_to), position) 384 | -------------------------------------------------------------------------------- /docs/UCIprotocol.md: -------------------------------------------------------------------------------- 1 | > Written by Stefan-Meyer Kahlen. Taken from [this website](http://wbec-ridderkerk.nl/html/UCIProtocol.html) 2 | 3 | Description of the universal chess interface (UCI) April 2004 4 | ================================================================ 5 | 6 | * The specification is independent of the operating system. For Windows, 7 | the engine is a normal exe file, either a console or "real" windows application. 8 | 9 | * all communication is done via standard input and output with text commands, 10 | 11 | * The engine should boot and wait for input from the GUI, 12 | the engine should wait for the "isready" or "setoption" command to set up its internal parameters 13 | as the boot process should be as quick as possible. 14 | 15 | * the engine must always be able to process input from stdin, even while thinking. 16 | 17 | * all command strings the engine receives will end with '\n', 18 | also all commands the GUI receives should end with '\n', 19 | Note: '\n' can be 0x0c or 0x0a0c or any combination depending on your OS. 20 | If you use Engine und GUI in the same OS this should be no problem if you cummunicate in text mode, 21 | but be aware of this when for example running a Linux engine in a Windows GUI. 22 | 23 | * The engine will always be in forced mode which means it should never start calculating 24 | or pondering without receiving a "go" command first. 25 | 26 | * Before the engine is asked to search on a position, there will always be a position command 27 | to tell the engine about the current position. 28 | 29 | * by default all the opening book handling is done by the GUI, 30 | but there is an option for the engine to use its own book ("OwnBook" option, see below) 31 | 32 | * if the engine or the GUI receives an unknown command or token it should just ignore it and try to 33 | parse the rest of the string. 34 | 35 | * if the engine receives a command which is not supposed to come, for example "stop" when the engine is 36 | not calculating, it should also just ignore it. 37 | 38 | 39 | Move format: 40 | ------------ 41 | 42 | The move format is in long algebraic notation. 43 | A nullmove from the Engine to the GUI should be send as 0000. 44 | Examples: e2e4, e7e5, e1g1 (white short castling), e7e8q (for promotion) 45 | 46 | 47 | 48 | GUI to engine: 49 | -------------- 50 | 51 | These are all the command the engine gets from the interface. 52 | 53 | * uci 54 | tell engine to use the uci (universal chess interface), 55 | this will be send once as a first command after program boot 56 | to tell the engine to switch to uci mode. 57 | After receiving the uci command the engine must identify itself with the "id" command 58 | and sent the "option" commands to tell the GUI which engine settings the engine supports if any. 59 | After that the engine should sent "uciok" to acknowledge the uci mode. 60 | If no uciok is sent within a certain time period, the engine task will be killed by the GUI. 61 | 62 | * debug [ on | off ] 63 | switch the debug mode of the engine on and off. 64 | In debug mode the engine should sent additional infos to the GUI, e.g. with the "info string" command, 65 | to help debugging, e.g. the commands that the engine has received etc. 66 | This mode should be switched off by default and this command can be sent 67 | any time, also when the engine is thinking. 68 | 69 | * isready 70 | this is used to synchronize the engine with the GUI. When the GUI has sent a command or 71 | multiple commands that can take some time to complete, 72 | this command can be used to wait for the engine to be ready again or 73 | to ping the engine to find out if it is still alive. 74 | E.g. this should be sent after setting the path to the tablebases as this can take some time. 75 | This command is also required once before the engine is asked to do any search 76 | to wait for the engine to finish initializing. 77 | This command must always be answered with "readyok" and can be sent also when the engine is calculating 78 | in which case the engine should also immediately answer with "readyok" without stopping the search. 79 | 80 | * setoption name [value ] 81 | this is sent to the engine when the user wants to change the internal parameters 82 | of the engine. For the "button" type no value is needed. 83 | One string will be sent for each parameter and this will only be sent when the engine is waiting. 84 | The name of the option in should not be case sensitive and can inludes spaces like also the value. 85 | The substrings "value" and "name" should be avoided in and to allow unambiguous parsing, 86 | for example do not use = "draw value". 87 | Here are some strings for the example below: 88 | "setoption name Nullmove value true\n" 89 | "setoption name Selectivity value 3\n" 90 | "setoption name Style value Risky\n" 91 | "setoption name Clear Hash\n" 92 | "setoption name NalimovPath value c:\chess\tb\4;c:\chess\tb\5\n" 93 | 94 | * register 95 | this is the command to try to register an engine or to tell the engine that registration 96 | will be done later. This command should always be sent if the engine has send "registration error" 97 | at program startup. 98 | The following tokens are allowed: 99 | * later 100 | the user doesn't want to register the engine now. 101 | * name 102 | the engine should be registered with the name 103 | * code 104 | the engine should be registered with the code 105 | Example: 106 | "register later" 107 | "register name Stefan MK code 4359874324" 108 | 109 | * ucinewgame 110 | this is sent to the engine when the next search (started with "position" and "go") will be from 111 | a different game. This can be a new game the engine should play or a new game it should analyse but 112 | also the next position from a testsuite with positions only. 113 | If the GUI hasn't sent a "ucinewgame" before the first "position" command, the engine shouldn't 114 | expect any further ucinewgame commands as the GUI is probably not supporting the ucinewgame command. 115 | So the engine should not rely on this command even though all new GUIs should support it. 116 | As the engine's reaction to "ucinewgame" can take some time the GUI should always send "isready" 117 | after "ucinewgame" to wait for the engine to finish its operation. 118 | 119 | * position [fen | startpos ] moves .... 120 | set up the position described in fenstring on the internal board and 121 | play the moves on the internal chess board. 122 | if the game was played from the start position the string "startpos" will be sent 123 | Note: no "new" command is needed. However, if this position is from a different game than 124 | the last position sent to the engine, the GUI should have sent a "ucinewgame" inbetween. 125 | 126 | * go 127 | start calculating on the current position set up with the "position" command. 128 | There are a number of commands that can follow this command, all will be sent in the same string. 129 | If one command is not send its value should be interpreted as it would not influence the search. 130 | * searchmoves .... 131 | restrict search to this moves only 132 | Example: After "position startpos" and "go infinite searchmoves e2e4 d2d4" 133 | the engine should only search the two moves e2e4 and d2d4 in the initial position. 134 | * ponder 135 | start searching in pondering mode. 136 | Do not exit the search in ponder mode, even if it's mate! 137 | This means that the last move sent in in the position string is the ponder move. 138 | The engine can do what it wants to do, but after a "ponderhit" command 139 | it should execute the suggested move to ponder on. This means that the ponder move sent by 140 | the GUI can be interpreted as a recommendation about which move to ponder. However, if the 141 | engine decides to ponder on a different move, it should not display any mainlines as they are 142 | likely to be misinterpreted by the GUI because the GUI expects the engine to ponder 143 | on the suggested move. 144 | * wtime 145 | white has x msec left on the clock 146 | * btime 147 | black has x msec left on the clock 148 | * winc 149 | white increment per move in mseconds if x > 0 150 | * binc 151 | black increment per move in mseconds if x > 0 152 | * movestogo 153 | there are x moves to the next time control, 154 | this will only be sent if x > 0, 155 | if you don't get this and get the wtime and btime it's sudden death 156 | * depth 157 | search x plies only. 158 | * nodes 159 | search x nodes only, 160 | * mate 161 | search for a mate in x moves 162 | * movetime 163 | search exactly x mseconds 164 | * infinite 165 | search until the "stop" command. Do not exit the search without being told so in this mode! 166 | 167 | * stop 168 | stop calculating as soon as possible, 169 | don't forget the "bestmove" and possibly the "ponder" token when finishing the search 170 | 171 | * ponderhit 172 | the user has played the expected move. This will be sent if the engine was told to ponder on the same move 173 | the user has played. The engine should continue searching but switch from pondering to normal search. 174 | 175 | * quit 176 | quit the program as soon as possible 177 | 178 | 179 | Engine to GUI: 180 | -------------- 181 | 182 | * id 183 | * name 184 | this must be sent after receiving the "uci" command to identify the engine, 185 | e.g. "id name Shredder X.Y\n" 186 | * author 187 | this must be sent after receiving the "uci" command to identify the engine, 188 | e.g. "id author Stefan MK\n" 189 | 190 | * uciok 191 | Must be sent after the id and optional options to tell the GUI that the engine 192 | has sent all infos and is ready in uci mode. 193 | 194 | * readyok 195 | This must be sent when the engine has received an "isready" command and has 196 | processed all input and is ready to accept new commands now. 197 | It is usually sent after a command that can take some time to be able to wait for the engine, 198 | but it can be used anytime, even when the engine is searching, 199 | and must always be answered with "isready". 200 | 201 | * bestmove [ ponder ] 202 | the engine has stopped searching and found the move best in this position. 203 | the engine can send the move it likes to ponder on. The engine must not start pondering automatically. 204 | this command must always be sent if the engine stops searching, also in pondering mode if there is a 205 | "stop" command, so for every "go" command a "bestmove" command is needed! 206 | Directly before that the engine should send a final "info" command with the final search information, 207 | the the GUI has the complete statistics about the last search. 208 | 209 | * copyprotection 210 | this is needed for copyprotected engines. After the uciok command the engine can tell the GUI, 211 | that it will check the copy protection now. This is done by "copyprotection checking". 212 | If the check is ok the engine should sent "copyprotection ok", otherwise "copyprotection error". 213 | If there is an error the engine should not function properly but should not quit alone. 214 | If the engine reports "copyprotection error" the GUI should not use this engine 215 | and display an error message instead! 216 | The code in the engine can look like this 217 | TellGUI("copyprotection checking\n"); 218 | // ... check the copy protection here ... 219 | if(ok) 220 | TellGUI("copyprotection ok\n"); 221 | else 222 | TellGUI("copyprotection error\n"); 223 | 224 | * registration 225 | this is needed for engines that need a username and/or a code to function with all features. 226 | Analog to the "copyprotection" command the engine can send "registration checking" 227 | after the uciok command followed by either "registration ok" or "registration error". 228 | Also after every attempt to register the engine it should answer with "registration checking" 229 | and then either "registration ok" or "registration error". 230 | In contrast to the "copyprotection" command, the GUI can use the engine after the engine has 231 | reported an error, but should inform the user that the engine is not properly registered 232 | and might not use all its features. 233 | In addition the GUI should offer to open a dialog to 234 | enable registration of the engine. To try to register an engine the GUI can send 235 | the "register" command. 236 | The GUI has to always answer with the "register" command if the engine sends "registration error" 237 | at engine startup (this can also be done with "register later") 238 | and tell the user somehow that the engine is not registered. 239 | This way the engine knows that the GUI can deal with the registration procedure and the user 240 | will be informed that the engine is not properly registered. 241 | 242 | * info 243 | the engine wants to send infos to the GUI. This should be done whenever one of the info has changed. 244 | The engine can send only selected infos and multiple infos can be send with one info command, 245 | e.g. "info currmove e2e4 currmovenumber 1" or 246 | "info depth 12 nodes 123456 nps 100000". 247 | Also all infos belonging to the pv should be sent together 248 | e.g. "info depth 2 score cp 214 time 1242 nodes 2124 nps 34928 pv e2e4 e7e5 g1f3" 249 | I suggest to start sending "currmove", "currmovenumber", "currline" and "refutation" only after one second 250 | to avoid too much traffic. 251 | Additional info: 252 | * depth 253 | search depth in plies 254 | * seldepth 255 | selective search depth in plies, 256 | if the engine sends seldepth there must also a "depth" be present in the same string. 257 | * time 258 | the time searched in ms, this should be sent together with the pv. 259 | * nodes 260 | x nodes searched, the engine should send this info regularly 261 | * pv ... 262 | the best line found 263 | * multipv 264 | this for the multi pv mode. 265 | for the best move/pv add "multipv 1" in the string when you send the pv. 266 | in k-best mode always send all k variants in k strings together. 267 | * score 268 | * cp 269 | the score from the engine's point of view in centipawns. 270 | * mate 271 | mate in y moves, not plies. 272 | If the engine is getting mated use negativ values for y. 273 | * lowerbound 274 | the score is just a lower bound. 275 | * upperbound 276 | the score is just an upper bound. 277 | * currmove 278 | currently searching this move 279 | * currmovenumber 280 | currently searching move number x, for the first move x should be 1 not 0. 281 | * hashfull 282 | the hash is x permill full, the engine should send this info regularly 283 | * nps 284 | x nodes per second searched, the engine should send this info regularly 285 | * tbhits 286 | x positions where found in the endgame table bases 287 | * cpuload 288 | the cpu usage of the engine is x permill. 289 | * string 290 | any string str which will be displayed be the engine, 291 | if there is a string command the rest of the line will be interpreted as . 292 | * refutation ... 293 | move is refuted by the line ... , i can be any number >= 1. 294 | Example: after move d1h5 is searched, the engine can send 295 | "info refutation d1h5 g6h5" 296 | if g6h5 is the best answer after d1h5 or if g6h5 refutes the move d1h5. 297 | if there is norefutation for d1h5 found, the engine should just send 298 | "info refutation d1h5" 299 | The engine should only send this if the option "UCI_ShowRefutations" is set to true. 300 | * currline ... 301 | this is the current line the engine is calculating. is the number of the cpu if 302 | the engine is running on more than one cpu. = 1,2,3.... 303 | if the engine is just using one cpu, can be omitted. 304 | If is greater than 1, always send all k lines in k strings together. 305 | The engine should only send this if the option "UCI_ShowCurrLine" is set to true. 306 | 307 | 308 | * option 309 | This command tells the GUI which parameters can be changed in the engine. 310 | This should be sent once at engine startup after the "uci" and the "id" commands 311 | if any parameter can be changed in the engine. 312 | The GUI should parse this and build a dialog for the user to change the settings. 313 | Note that not every option needs to appear in this dialog as some options like 314 | "Ponder", "UCI_AnalyseMode", etc. are better handled elsewhere or are set automatically. 315 | If the user wants to change some settings, the GUI will send a "setoption" command to the engine. 316 | Note that the GUI need not send the setoption command when starting the engine for every option if 317 | it doesn't want to change the default value. 318 | For all allowed combinations see the example below, 319 | as some combinations of this tokens don't make sense. 320 | One string will be sent for each parameter. 321 | * name 322 | The option has the name id. 323 | Certain options have a fixed value for , which means that the semantics of this option is fixed. 324 | Usually those options should not be displayed in the normal engine options window of the GUI but 325 | get a special treatment. "Pondering" for example should be set automatically when pondering is 326 | enabled or disabled in the GUI options. The same for "UCI_AnalyseMode" which should also be set 327 | automatically by the GUI. All those certain options have the prefix "UCI_" except for the 328 | first 6 options below. If the GUI get an unknown Option with the prefix "UCI_", it should just 329 | ignore it and not display it in the engine's options dialog. 330 | * = Hash, type is spin 331 | the value in MB for memory for hash tables can be changed, 332 | this should be answered with the first "setoptions" command at program boot 333 | if the engine has sent the appropriate "option name Hash" command, 334 | which should be supported by all engines! 335 | So the engine should use a very small hash first as default. 336 | * = NalimovPath, type string 337 | this is the path on the hard disk to the Nalimov compressed format. 338 | Multiple directories can be concatenated with ";" 339 | * = NalimovCache, type spin 340 | this is the size in MB for the cache for the nalimov table bases 341 | These last two options should also be present in the initial options exchange dialog 342 | when the engine is booted if the engine supports it 343 | * = Ponder, type check 344 | this means that the engine is able to ponder. 345 | The GUI will send this whenever pondering is possible or not. 346 | Note: The engine should not start pondering on its own if this is enabled, this option is only 347 | needed because the engine might change its time management algorithm when pondering is allowed. 348 | * = OwnBook, type check 349 | this means that the engine has its own book which is accessed by the engine itself. 350 | if this is set, the engine takes care of the opening book and the GUI will never 351 | execute a move out of its book for the engine. If this is set to false by the GUI, 352 | the engine should not access its own book. 353 | * = MultiPV, type spin 354 | the engine supports multi best line or k-best mode. the default value is 1 355 | * = UCI_ShowCurrLine, type check, should be false by default, 356 | the engine can show the current line it is calculating. see "info currline" above. 357 | * = UCI_ShowRefutations, type check, should be false by default, 358 | the engine can show a move and its refutation in a line. see "info refutations" above. 359 | * = UCI_LimitStrength, type check, should be false by default, 360 | The engine is able to limit its strength to a specific Elo number, 361 | This should always be implemented together with "UCI_Elo". 362 | * = UCI_Elo, type spin 363 | The engine can limit its strength in Elo within this interval. 364 | If UCI_LimitStrength is set to false, this value should be ignored. 365 | If UCI_LimitStrength is set to true, the engine should play with this specific strength. 366 | This should always be implemented together with "UCI_LimitStrength". 367 | * = UCI_AnalyseMode, type check 368 | The engine wants to behave differently when analysing or playing a game. 369 | For example when playing it can use some kind of learning. 370 | This is set to false if the engine is playing a game, otherwise it is true. 371 | * = UCI_Opponent, type string 372 | With this command the GUI can send the name, title, elo and if the engine is playing a human 373 | or computer to the engine. 374 | The format of the string has to be [GM|IM|FM|WGM|WIM|none] [|none] [computer|human] 375 | Example: 376 | "setoption name UCI_Opponent value GM 2800 human Gary Kasparow" 377 | "setoption name UCI_Opponent value none none computer Shredder" 378 | 379 | 380 | * type 381 | The option has type t. 382 | There are 5 different types of options the engine can send 383 | * check 384 | a checkbox that can either be true or false 385 | * spin 386 | a spin wheel that can be an integer in a certain range 387 | * combo 388 | a combo box that can have different predefined strings as a value 389 | * button 390 | a button that can be pressed to send a command to the engine 391 | * string 392 | a text field that has a string as a value, 393 | an empty string has the value "" 394 | * default 395 | the default value of this parameter is x 396 | * min 397 | the minimum value of this parameter is x 398 | * max 399 | the maximum value of this parameter is x 400 | * var 401 | a predefined value of this parameter is x 402 | Example: 403 | Here are 5 strings for each of the 5 possible types of options 404 | "option name Nullmove type check default true\n" 405 | "option name Selectivity type spin default 2 min 0 max 4\n" 406 | "option name Style type combo default Normal var Solid var Normal var Risky\n" 407 | "option name NalimovPath type string default c:\\n" 408 | "option name Clear Hash type button\n" 409 | 410 | 411 | 412 | Example: 413 | -------- 414 | 415 | This is how the communication when the engine boots can look like: 416 | 417 | GUI engine 418 | ``` java 419 | // tell the engine to switch to UCI mode 420 | uci 421 | 422 | // engine identify 423 | id name Shredder 424 | id author Stefan MK 425 | 426 | // engine sends the options it can change 427 | // the engine can change the hash size from 1 to 128 MB 428 | option name Hash type spin default 1 min 1 max 128 429 | 430 | // the engine supports Nalimov endgame tablebases 431 | option name NalimovPath type string default 432 | option name NalimovCache type spin default 1 min 1 max 32 433 | 434 | // the engine can switch off Nullmove and set the playing style 435 | option name Nullmove type check default true 436 | option name Style type combo default Normal var Solid var Normal var Risky 437 | 438 | // the engine has sent all parameters and is ready 439 | uciok 440 | 441 | // Note: here the GUI can already send a "quit" command if it just wants to find out 442 | // details about the engine, so the engine should not initialize its internal 443 | // parameters before here. 444 | // now the GUI sets some values in the engine 445 | // set hash to 32 MB 446 | setoption name Hash value 32 447 | 448 | // init tbs 449 | setoption name NalimovCache value 1 450 | setoption name NalimovPath value d:\tb;c\tb 451 | 452 | // waiting for the engine to finish initializing 453 | // this command and the answer is required here! 454 | isready 455 | 456 | // engine has finished setting up the internal values 457 | readyok 458 | 459 | // now we are ready to go 460 | 461 | // if the GUI is supporting it, tell the engine that is is 462 | // searching on a game that is hasn't searched on before 463 | ucinewgame 464 | 465 | // if the engine supports the "UCI_AnalyseMode" option and the next search is supposted to 466 | // be an analysis, the GUI should set "UCI_AnalyseMode" to true if it is currently 467 | // set to false with this engine 468 | setoption name UCI_AnalyseMode value true 469 | 470 | // tell the engine to search infinite from the start position after 1.e4 e5 471 | position startpos moves e2e4 e7e5 472 | go infinite 473 | 474 | // the engine starts sending infos about the search to the GUI 475 | // (only some examples are given) 476 | 477 | 478 | info depth 1 seldepth 0 479 | info score cp 13 depth 1 nodes 13 time 15 pv f1b5 480 | info depth 2 seldepth 2 481 | info nps 15937 482 | info score cp 14 depth 2 nodes 255 time 15 pv f1c4 f8c5 483 | info depth 2 seldepth 7 nodes 255 484 | info depth 3 seldepth 7 485 | info nps 26437 486 | info score cp 20 depth 3 nodes 423 time 15 pv f1c4 g8f6 b1c3 487 | info nps 41562 488 | .... 489 | 490 | 491 | // here the user has seen enough and asks to stop the searching 492 | stop 493 | 494 | // the engine has finished searching and is sending the bestmove command 495 | // which is needed for every "go" command sent to tell the GUI 496 | // that the engine is ready again 497 | bestmove g1f3 ponder d8f6 498 | ``` 499 | --------------------------------------------------------------------------------