diff --git a/.editorconfig b/.editorconfig index df0b5dd62..b5391e3e7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,3 +1,5 @@ +# https://editorconfig.org/ + root = true [*] diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..7e1e93611 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: niklasf diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..0d23a8f03 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: '/' + schedule: + interval: monthly diff --git a/.github/workflows/setup-ubuntu-latest.sh b/.github/workflows/setup-ubuntu-latest.sh new file mode 100755 index 000000000..10b8d1115 --- /dev/null +++ b/.github/workflows/setup-ubuntu-latest.sh @@ -0,0 +1,17 @@ +#!/bin/sh -e + +# Stockfish +sudo apt-get install -y stockfish + +# Crafty +sudo apt-get install -y crafty + +# Fairy-stockfish +sudo apt-get install -y fairy-stockfish + +# Gaviota libgtb +git clone https://github.com/michiguel/Gaviota-Tablebases.git --depth 1 +cd Gaviota-Tablebases +make +echo "LD_LIBRARY_PATH=`pwd`:${LD_LIBRARY_PATH}" >> $GITHUB_ENV +cd .. diff --git a/.github/workflows/setup-windows-latest.sh b/.github/workflows/setup-windows-latest.sh new file mode 100755 index 000000000..48037cbef --- /dev/null +++ b/.github/workflows/setup-windows-latest.sh @@ -0,0 +1,16 @@ +#!/bin/sh -e + +echo Download stockfish ... +choco install wget +wget https://github.com/official-stockfish/Stockfish/releases/download/sf_16/stockfish-windows-x86-64-avx2.zip + +echo Unzip .. +7z e stockfish-windows-x86-64-avx2.zip stockfish/stockfish-windows-x86-64-avx2.exe + +echo Setup path ... +mv stockfish-windows-x86-64-avx2.exe stockfish.exe +pwd >> $GITHUB_PATH + +echo Download fairy-stockfish ... +wget https://github.com/fairy-stockfish/Fairy-Stockfish/releases/latest/download/fairy-stockfish-largeboard_x86-64.exe +mv fairy-stockfish-largeboard_x86-64.exe fairy-stockfish.exe diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..9d0880bb4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,72 @@ +name: Test + +on: [push, pull_request] + +permissions: + contents: read + +jobs: + test: + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + fail-fast: false + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + - run: .github/workflows/setup-${{ matrix.os }}.sh + shell: bash + - run: pip install -e . + - run: python test.py -v + perft: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: "3.14" + - run: pip install -e . + - run: python examples/perft/perft.py -t 1 examples/perft/random.perft --max-nodes 10000 + - run: python examples/perft/perft.py -t 1 examples/perft/chess960.perft --max-nodes 100000 + - run: python examples/perft/perft.py -t 1 examples/perft/tricky.perft + - run: python examples/perft/perft.py -t 1 --variant giveaway examples/perft/giveaway.perft + - run: python examples/perft/perft.py -t 1 --variant atomic examples/perft/atomic.perft + - run: python examples/perft/perft.py -t 1 --variant racingkings examples/perft/racingkings.perft + - run: python examples/perft/perft.py -t 1 --variant horde examples/perft/horde.perft + - run: python examples/perft/perft.py -t 1 --variant crazyhouse examples/perft/crazyhouse.perft + - run: python examples/perft/perft.py -t 1 --variant 3check examples/perft/3check.perft + typing: + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + fail-fast: false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + - run: pip install -e . + - run: pip install mypy + - run: python -m mypy --strict chess + - run: python -m mypy --strict examples/**/*.py + - run: pip install pyright + - run: python -m pyright chess + - run: python -m pyright examples/**/*.py + readme: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: "3.14" + - run: sudo apt-get update && sudo apt-get install -y docutils-common + - run: python setup.py --long-description | rst2html --strict --no-raw > /dev/null + - run: pip install -e . + - run: .github/workflows/setup-ubuntu-latest.sh + - run: python -m doctest README.rst diff --git a/.gitignore b/.gitignore index 80d4fd61a..076e4fcff 100644 --- a/.gitignore +++ b/.gitignore @@ -6,10 +6,11 @@ venv/ .coveralls.yml nosetests.xml .tox +.mypy_cache dist/ build/ -python_chess.egg-info/ +*.egg-info/ docs/_build/ data/gaviota/*.gtb.cp4 @@ -18,4 +19,6 @@ data/syzygy/suicide/*.stb[wz] data/syzygy/atomic/*.atb[wz] data/syzygy/giveaway/*.[gs]tb[wz] +fuzz/corpus + release-v*.txt diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 000000000..dbe71809a --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,15 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.10" + +sphinx: + configuration: docs/conf.py + +python: + install: + - method: pip + path: . + - requirements: docs/requirements.txt diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 27730b944..000000000 --- a/.travis.yml +++ /dev/null @@ -1,68 +0,0 @@ -language: python -dist: trusty -sudo: false -python: - - "2.6" - - "2.7" - - "3.3" - - "3.4" - - "3.5" - - "3.6" - - "3.7-dev" - - "pypy" - - "pypy3" -matrix: - include: - - python: "3.7-dev" - env: PERFT=1 -addons: - apt: - packages: - - liblzma-dev -cache: - directories: - - data/gaviota - - data/syzygy/suicide -before_install: - - # Stockfish - - wget https://stockfish.s3.amazonaws.com/stockfish-8-linux.zip - - unzip stockfish-8-linux.zip - - mkdir -p bin - - cp stockfish-8-linux/Linux/stockfish_8_x64 bin/stockfish - - export PATH="`pwd`/bin:${PATH}" - - which stockfish || (echo $PATH && false) - - # Crafty - - git clone https://github.com/lazydroid/crafty-chess - - cd crafty-chess - - make unix-gcc - - export PATH="`pwd`:${PATH}" - - cd .. - - # Gaviota libgtb - - git clone https://github.com/michiguel/Gaviota-Tablebases.git --depth 1 - - cd Gaviota-Tablebases - - make - - export LD_LIBRARY_PATH="`pwd`:${LD_LIBRARY_PATH}" - - cd .. - - # Gaviota tablebases - - cd data/gaviota - - wget --no-verbose --no-check-certificate --no-clobber --input-file TEST-SOURCE.txt - - cd ../.. - - # Suicide syzygy bases - - cd data/syzygy/suicide - - wget --no-verbose --no-check-certificate --no-clobber --input-file TEST-SOURCE.txt - - cd ../../.. -install: - - pip install coverage coveralls - - pip install -e .[test] -script: - - if [[ $PERFT -ne 1 ]]; then coverage erase; fi - - if [[ $PERFT -ne 1 ]]; then coverage run --source chess test.py -vv; fi - - if [[ $PERFT -ne 1 ]]; then coverage run --source chess --append -m doctest README.rst --verbose; fi - - if [[ $PERFT -ne 1 ]]; then coveralls || [[ $? -eq 139 ]]; fi - - if [[ $PERFT -eq 1 ]]; then python examples/perft/perft.py -t 1 examples/perft/random.perft --max-nodes 10000; fi - - if [[ $PERFT -eq 1 ]]; then python examples/perft/perft.py -t 1 examples/perft/tricky.perft; fi - - if [[ $PERFT -eq 1 ]]; then python examples/perft/perft.py -t 1 --variant giveaway examples/perft/giveaway.perft; fi - - if [[ $PERFT -eq 1 ]]; then python examples/perft/perft.py -t 1 --variant atomic examples/perft/atomic.perft; fi - - if [[ $PERFT -eq 1 ]]; then python examples/perft/perft.py -t 1 --variant racingkings examples/perft/racingkings.perft; fi - - if [[ $PERFT -eq 1 ]]; then python examples/perft/perft.py -t 1 --variant horde examples/perft/horde.perft; fi - - if [[ $PERFT -eq 1 ]]; then python examples/perft/perft.py -t 1 --variant crazyhouse examples/perft/crazyhouse.perft; fi diff --git a/CHANGELOG-OLD.rst b/CHANGELOG-OLD.rst new file mode 100644 index 000000000..bebcba4d0 --- /dev/null +++ b/CHANGELOG-OLD.rst @@ -0,0 +1,1887 @@ +Old Changelog for python-chess up to 1.0.0 +========================================== + +New in v1.0.0 (24th Sep 2020) +----------------------------- + +Changes: + +* Now requires Python 3.7+. +* `chess.engine` will now cut off illegal principal variations at the first + illegal move instead of discarding them entirely. +* `chess.engine.EngineProtocol` renamed to `chess.engine.Protocol`. +* `chess.engine.Option` is no longer a named tuple. +* Renamed `chess.gaviota` internals. +* Relaxed type annotations of `chess.pgn.GameNode.variation()` and related + methods. +* Changed default colors of `chess.svg.Arrow` and + `chess.pgn.GameNode.arrows()`. These can be overriden with the new + `chess.svg.board(..., colors)` feature. +* Documentation improvements. Will now show type aliases like `chess.Square` + instead of `int`. + +Bugfixes: + +* Fix insufficient material with same-color bishops on both sides. +* Clarify that `chess.Board.can_claim_draw()` and related methods refer to + claims by the player to move. Three-fold repetition could already be claimed + before making the final repeating move. `chess.Board.can_claim_fifty_moves()` + now also allows a claim before the final repeating move. The previous + behavior is `chess.Board.is_fifty_moves()`. +* Fix parsing of green arrows/circles in `chess.pgn.GameNode.arrows()`. +* Fix overloaded type signature of `chess.engine.Protocol.engine()`. + +New features: + +* Added `chess.parse_square()`, to be used instead of + `chess.SQUARE_NAMES.index()`. +* Added `chess.Board.apply_mirror()`. +* Added `chess.svg.board(..., colors)`, to allow overriding the default theme. + +New in v0.31.4 (9th Aug 2020) +----------------------------- + +Bugfixes: + +* Fix inconsistency where `board.is_legal()` was not accepting castling moves + in Chess960 notation (when board is in standard mode), while all other + methods did. +* Fix `chess.pgn.GameNode.set_clock()` with negative or floating point values. +* Avoid leading and trailing spaces in PGN comments when setting annotations. + +New features: + +* Finish typing and declare support for mypy. + +New in v0.31.3 (18th Jul 2020) +------------------------------ + +Bugfixes: + +* Custom castling rights assigned to `board.castling_rights` or castling rights + left over after `Board.set_board_fen()` were not correctly cleaned after + the first move. + +Changes: + +* Ignore up to one consecutive empty line between PGN headers. +* Added PGN Variant `From Position` as an alias for standard chess. +* `chess.pgn.FileExporter.result()` now returns the number of written + characters. +* `chess.engine` now avoids sending 0 for search limits, which some engines + misunderstand as no limit. +* `chess.engine` better handles null moves sent to the engine. +* `chess.engine` now gracefully handles `NULL` ponder moves and uppercase + moves received from UCI engines, which is technically invalid. + +New features: + +* Added `chess.pgn.GameNode.{clock, set_clock}()` to read and write + `[%clk ...]` **PGN annotations**. +* Added `chess.pgn.GameNode.{arrows, set_arrows}()` to read and write + `[%csl ...]` and `[%cal ...]` PGN annotations. +* Added `chess.pgn.GameNode.{eval, set_eval}()` to read and write + `[%eval ...]` PGN annotations. +* Added `SquareSet.ray(a, b)` and `SquareSet.between(a, b)`. + +New in v0.31.2 (2nd Jun 2020) +----------------------------- + +Bugfixes: + +* Fix rejected/accepted in `chess.engine.XBoardProtocol`. +* Misc typing fixes. + +Changes: + +* Deprecated `chess.syzygy.is_table_name()`. Replaced with + `chess.syzygy.is_tablename()` which has additional parameters and defaults to + `one_king`. +* Take advantage of `int.bit_count()` coming in Python 3.10. + +New in v0.31.1 (5th May 2020) +----------------------------- + +Bugfixes: + +* `RacingKingsBoard.is_variant_win()` no longer incorrectly returns `True` + for drawn positions. +* Multiple moves for EPD opcodes *am* and *bm* are now sorted as required by + the specification. +* Coordinates of SVG boards are now properly aligned, even when rendered as + SVG Tiny. + +Changes: + +* SVG boards now have a background color for the coordinate margin, making + coordinates readable on dark backgrounds. +* Added *[Variant "Illegal"]* as an alias for standard chess + (used by Chessbase). + +Features: + +* Added `Board.find_move()`, useful for finding moves that match human input. + +New in v0.31.0 (21st Apr 2020) +------------------------------ + +Changes: + +* Replaced lookup table `chess.BB_BETWEEN[a][b]` with a function + `chess.between(a, b)`. Improves initialization and runtime performance. +* `chess.pgn.BaseVisitor.result()` is now an abstract method, forcing + subclasses to implement it. +* Removed helper attributes from `chess.engine.InfoDict`. Instead it is now + a `TypedDict`. +* `chess.engine.PovScore` equality is now semantic instead of structural: + Scores compare equal to the negative score from the opposite point of view. + +Bugfixes: + +* `chess.Board.is_irreversible()` now considers ceding legal en passant + captures as irreversible. Also documented that false-negatives due to forced + lines are by design. +* Fixed stack overflow in `chess.pgn` when exporting, visiting or getting the + final board of a very long game. +* Clarified documentation regarding board validity. +* `chess.pgn.GameNode.__repr__()` no longer errors if the root node has invalid + FEN or Variant headers. +* Carriage returns are no longer allowed in PGN header values, fixing + reparsability. +* Fixed type error when XBoard name or egt features have a value that looks + like an integer. +* `chess.engine` is now passing type checks with mypy. +* `chess.gaviota` is now passing type checks with mypy. + +Features: + +* Added `chess.Board.gives_check()`. +* `chess.engine.AnalysisResult.wait()` now returns `chess.engine.BestMove`. +* Added `empty_square` parameter for `chess.Board.unicode()` with better + aligned default (⭘). + +New in v0.30.1 (18th Jan 2020) +------------------------------ + +Changes: + +* Positions with more than two checkers are considered invalid and + `board.status()` returns `chess.STATUS_TOO_MANY_CHECKERS`. +* Pawns drops in Crazyhouse are considered zeroing and reset + `board.halfmove_clock` when played. +* Now validating file sizes when opening Syzygy tables and Polyglot opening + books. +* Explicitly warn about untrusted tablebase files and chess engines. + +Bugfixes: + +* Fix Racing Kings game end detection: Black cannot catch up if their own + pieces block the goal. White would win on the next turn, so this did not + impact the game theoretical outcome of the game. +* Fix bugs discovered by fuzzing the EPD parser: Fixed serialization of + empty strings, reparsability of empty move lists, handling of non-finite + floats, and handling of whitespace in opcodes. + +Features: + +* Added `board.checkers()`, returning a set of squares with the pieces giving + check. + +New in v0.30.0 (1st Jan 2020) +----------------------------- + +Changes: + +* **Dropped support for Python 3.5.** +* Remove explicit loop arguments in `chess.engine` module, following + https://bugs.python.org/issue36373. + +Bugfixes: + +* `chess.engine.EngineProtocol.returncode` is no longer poisoned when + `EngineProtocol.quit()` times out. +* `chess.engine.PlayResult.info` was not always of type + `chess.engine.InfoDict`. + +Features: + +* The background thread spawned by `chess.engine.SimpleEngine` is now named + for improved debuggability, revealing the PID of the engine process. +* `chess.engine.EventLoopPolicy` now supports `asyncio.PidfdChildWatcher` + when running on Python 3.9+ and Linux 5.3+. +* Add `chess.Board.san_and_push()`. + +New in v0.29.0 (2nd Dec 2019) +----------------------------- + +Changes: + +* `chess.variant.GiveawayBoard` **now starts with castling rights**. + `chess.variant.AntichessBoard` is the same variant without castling rights. +* UCI info parser no longer reports errors when encountering unknown tokens. +* Performance improvements for repetition detection. +* Since Python 3.8: `chess.syzygy`/`chess.polyglot` use `madvise(MADV_RANDOM)` + to prepare table/book files for random access. + +Bugfixes: + +* Fix syntax error in type annotation of `chess.engine.run_in_background()`. +* Fix castling rights when king is exploded in Atomic. Mitigated by the fact + that the game is over and that it did not affect FEN. +* Fix insufficient material with underpromoted pieces in Crazyhouse. Mitigated + by the fact that affected positions are unreachable in Crazyhouse. + +Features: + +* Support `wdl` in UCI info (usually activated with `UCI_ShowWDL`). + +New in v0.28.3 (3rd Sep 2019) +----------------------------- + +Bugfixes: + +* Follow FICS rules in Atomic castling edge cases. +* Handle self-reported errors by XBoard engines "Error: ..." or + "Illegal move: ...". + +New in v0.28.2 (25th Jul 2019) +------------------------------ + +Bugfixes: + +* Fixed exception propagation, when a UCI engine sends an invalid `bestmove`. + Thanks @fsmosca. + +Changes: + +* `chess.Move.from_uci()` no longer accepts moves from and to the same square, + for example `a1a1`. `0000` is now the only valid null move notation. + +New in v0.28.1 (25th May 2019) +------------------------------ + +Bugfixes: + +* The minimum Python version is 3.5.3 (instead of 3.5.0). +* Fix `board.is_irreversible()` when capturing a rook that had castling rights. + +Changes: + +* `is_en_passant()`, `is_capture()`, `is_zeroing()`, `is_irreversible()`, + `is_castling()`, `is_kingside_castling()` and `is_queenside_castling()` + now consistently return `False` for null moves. +* Added `chess.engine.InfoDict` class with typed shorthands for common keys. +* Support `[Variant "3-check"]` (from chess.com PGNs). + +New in v0.28.0 (20th May 2019) +------------------------------ + +Changes: + +* Dropped support for Python 3.4 (end of life reached). +* `chess.polyglot.Entry.move` **is now a property instead of a method**. + The raw move is now always decoded in the context of the position (relevant + for castling moves). +* `Piece`, `Move`, `BaseBoard` and `Board` comparisons no longer support + duck typing. +* FENs sent to engines now always include potential en passant squares, even if + no legal en passant capture exists. +* Circular SVG arrows now have a `circle` CSS class. +* Superfluous dashes (-) in EPDs are no longer treated as opcodes. +* Removed `GameCreator`, `HeaderCreator` and `BoardCreator` aliases for + `{Game,Headers,Board}Builder`. + +Bugfixes: + +* Notation like `Kh1` is no longer accepted for castling moves. +* Remove stale files from wheels published on PyPI. +* Parsing Three-Check EPDs with moves was always failing. +* Some methods in `chess.variant` were returning bool-ish integers, when they + should have returned `bool`. +* `chess.engine`: Fix line decoding when Windows line-endings arrive seperately + in stdout buffer. +* `chess.engine`: Survive timeout in analysis. +* `chess.engine`: Survive unexpected `bestmove` sent by misbehaving UCI engines. + +New features: + +* **Experimental type signatures for almost all public APIs** (`typing`). + Some modules do not yet internally pass typechecking. +* Added `Board.color_at(square)`. +* Added `chess.engine.AnalysisResult.get()` and `empty()`. +* `chess.engine`: The `UCI_AnalyseMode` option is still automatically managed, + but can now be overwritten. +* `chess.engine.EngineProtocol` and constructors now optionally take + an explicit `loop` argument. + +New in v0.27.3 (21st Mar 2019) +------------------------------ + +Changes: + +* `XBoardProtocol` will no longer raise an exception when the engine resigned. + Instead it sets a new flag `PlayResult.resigned`. `resigned` and + `draw_offered` are keyword-only arguments. +* Renamed `chess.pgn.{Game,Header,Board}Creator` to + `{Game,Headers,Board}Builder`. Aliases kept in place. + +Bugfixes: + +* Make `XBoardProtocol` robust against engines that send a move after claiming + a draw or resigning. Thanks @pascalgeo. +* `XBoardProtocol` no longer ignores `Hint:` sent by the engine. +* Fix handling of illegal moves in `XBoardProtocol`. +* Fix exception when engine is shut down while pondering. +* Fix unhandled internal exception and file descriptor leak when engine + initialization fails. +* Fix `HordeBoard.status()` when black pieces are on the first rank. + Thanks @Wisling. + +New features: + +* Added `chess.pgn.Game.builder()`, `chess.pgn.Headers.builder()` and + `chess.pgn.GameNode.dangling_node()` to simplify subclassing `GameNode`. +* `EngineProtocol.communicate()` is now also available in the synchronous API. + +New in v0.27.2 (16th Mar 2019) +------------------------------ + +Bugfixes: + +* `chess.engine.XBoardProtocol.play()` was searching 100 times longer than + intended when using `chess.engine.Limit.time`, and searching 100 times more + nodes than intended when using `chess.engine.Limit.nodes`. Thanks @pascalgeo. + +New in v0.27.1 (15th Mar 2019) +------------------------------ + +Bugfixes: + +* `chess.engine.XBoardProtocol.play()` was raising `KeyError` when using time + controls with increment or remaining moves. Thanks @pascalgeo. + +New in v0.27.0 (14th Mar 2019) +------------------------------ + +This is the second **release candidate for python-chess 1.0**. If you see the +need for breaking changes, please speak up now! + +Bugfixes: + +* `EngineProtocol.analyse(*, multipv)` was not passing this argument to the + engine and therefore only returned the first principal variation. + Thanks @svangordon. +* `chess.svg.board(*, squares)`: The X symbol on selected squares is now more + visible when it overlaps pieces. + +Changes: + +* **FEN/EPD parsing is now more relaxed**: Incomplete FENs and EPDs are + completed with reasonable defaults (`w - - 0 1`). The EPD parser accepts + fields with moves in UCI notation (for example the technically invalid + `bm g1f3` instead of `bm Nf3`). +* The PGN parser now skips games with invalid FEN headers and variations after + an illegal move (after handling the error as usual). + +New features: + +* Added `Board.is_repetition(count=3)`. +* Document that `chess.engine.EngineProtocol` is compatible with + AsyncSSH 1.16.0. + +New in v0.26.0 (19th Feb 2019) +------------------------------ + +This is the first **release candidate for python-chess 1.0**. If you see the +need for breaking changes, please speak up now! + +Changes: + +* `chess.engine` **is now stable and replaces** + `chess.uci` **and** `chess.xboard`. +* Advanced: `EngineProtocol.initialize()` is now public for use with custom + transports. +* Removed `__ne__` implementations (not required since Python 3). +* Assorted documentation and coding-style improvements. + +New features: + +* Check insufficient material for a specific side: + `board.has_insufficient_material(color)`. +* Copy boards with limited stack depth: `board.copy(stack=depth)`. + +Bugfixes: + +* Properly handle delayed engine errors, for example unsupported options. + +New in v0.25.1 (24th Jan 2019) +------------------------------ + +Bugfixes: + +* `chess.engine` did not correctly handle Windows-style line endings. + Thanks @Bstylestuff. + +New in v0.25.0 (18th Jan 2019) +------------------------------ + +New features: + +* This release introduces a new **experimental API for chess engine + communication**, `chess.engine`, based on `asyncio`. It is intended to + eventually replace `chess.uci` and `chess.xboard`. + +Bugfixes: + +* Fixed race condition in LRU-cache of open Syzygy tables. The LRU-cache is + enabled by default (*max_fds*). +* Fix deprecation warning and unclosed file in setup.py. + Thanks Mickaël Schoentgen. + +Changes: + +* `chess.pgn.read_game()` now ignores BOM at the start of the stream. +* Removed deprecated items. + +New in v0.24.2 (5th Jan 2019) +----------------------------- + +Bugfixes: + +* `CrazyhouseBoard.root()` and `ThreeCheckBoard.root()` were not returning the + correct pockets and number of remaining checks, respectively. Thanks @gbtami. +* `chess.pgn.skip_game()` now correctly skips PGN comments that contain + line-breaks and PGN header tag notation. + +Changes: + +* Renamed `chess.pgn.GameModelCreator` to `GameCreator`. Alias kept in place + and will be removed in a future release. +* Renamed `chess.engine` to `chess._engine`. Use re-exports from `chess.uci` + or `chess.xboard`. +* Renamed `Board.stack` to `Board._stack`. Do not use this directly. +* Improved memory usage: `Board.legal_moves` and `Board.pseudo_legal_moves` + no longer create reference cycles. PGN visitors can manage headers + themselves. +* Removed previously deprecated items. + +Features: + +* Added `chess.pgn.BaseVisitor.visit_board()` and `chess.pgn.BoardCreator`. + +New in v0.24.1, v0.23.11 (7th Dec 2018) +--------------------------------------- + +Bugfixes: + +* Fix `chess.Board.set_epd()` and `chess.Board.from_epd()` with semicolon + in string operand. Thanks @jdart1. +* `chess.pgn.GameNode.uci()` was always raising an exception. + Also included in v0.24.0. + +New in v0.24.0 (3rd Dec 2018) +----------------------------- + +This release **drops support for Python 2**. The *0.23.x* branch will be +maintained for one more month. + +Changes: + +* **Require Python 3.4.** Thanks @hugovk. +* No longer using extra pip features: + `pip install python-chess[engine,gaviota]` is now `pip install python-chess`. +* Various keyword arguments can now be used as **keyword arguments only**. +* `chess.pgn.GameNode.accept()` now + **also visits the move leading to that node**. +* `chess.pgn.GameModelCreator` now requires that `begin_game()` be called. +* `chess.pgn.scan_headers()` and `chess.pgn.scan_offsets()` have been removed. + Instead the new functions `chess.pgn.read_headers()` and + `chess.pgn.skip_game()` can be used for a similar purpose. +* `chess.syzygy`: Invalid magic headers now raise `IOError`. Previously they + were only checked in an assertion. + `type(board).{tbw_magic,tbz_magic,pawnless_tbw_magic,pawnless_tbz_magic}` + are now byte literals. +* `board.status()` constants (`STATUS_`) are now typed using `enum.IntFlag`. + Values remain unchanged. +* `chess.svg.Arrow` is no longer a `namedtuple`. +* `chess.PIECE_SYMBOLS[0]` and `chess.PIECE_NAMES[0]` are now `None` instead + of empty strings. +* Performance optimizations: + + * `chess.pgn.Game.from_board()`, + * `chess.square_name()` + * Replace `collections.deque` with lists almost everywhere. + +* Renamed symbols (aliases will be removed in the next release): + + * `chess.BB_VOID` -> `BB_EMPTY` + * `chess.bswap()` -> `flip_vertical()` + * `chess.pgn.GameNode.main_line()` -> `mainline_moves()` + * `chess.pgn.GameNode.is_main_line()` -> `is_mainline()` + * `chess.variant.BB_HILL` -> `chess.BB_CENTER` + * `chess.syzygy.open_tablebases()` -> `open_tablebase()` + * `chess.syzygy.Tablebases` -> `Tablebase` + * `chess.syzygy.Tablebase.open_directory()` -> `add_directory()` + * `chess.gaviota.open_tablebases()` -> `open_tablebase()` + * `chess.gaviota.open_tablebases_native()` -> `open_tablebase_native()` + * `chess.gaviota.NativeTablebases` -> `NativeTablebase` + * `chess.gaviota.PythonTablebases` -> `PythonTablebase` + * `chess.gaviota.NativeTablebase.open_directory()` -> `add_directory()` + * `chess.gaviota.PythonTablebase.open_directory()` -> `add_directory()` + +Bugfixes: + +* The PGN parser now gives the visitor a chance to handle unknown chess + variants and continue parsing. +* `chess.pgn.GameNode.uci()` was always raising an exception. + +New features: + +* `chess.SquareSet` now extends `collections.abc.MutableSet` and can be + initialized from iterables. +* `board.apply_transform(f)` and `board.transform(f)` can apply bitboard + transformations to a position. Examples: + `chess.flip_{vertical,horizontal,diagonal,anti_diagonal}`. +* `chess.pgn.GameNode.mainline()` iterates over nodes of the mainline. + Can also be used with `reversed()`. Reversal is now also supported for + `chess.pgn.GameNode.mainline_moves()`. +* `chess.svg.Arrow(tail, head, color="#888")` gained an optional *color* + argument. +* `chess.pgn.BaseVisitor.parse_san(board, san)` is used by parsers and can + be overwritten to deal with non-standard input formats. +* `chess.pgn`: Visitors can advise the parser to skip games or variations by + returning the special value `chess.pgn.SKIP` from `begin_game()`, + `end_headers()` or `begin_variation()`. This is only a hint. + The corresponding `end_game()` or `end_variation()` will still be called. +* Added `chess.svg.MARGIN`. + +New in v0.23.10 (31st Oct 2018) +------------------------------- + +Bugfixes: + +* `chess.SquareSet` now correctly handles negative masks. Thanks @hasnul. +* `chess.pgn` now accepts `[Variant "chess 960"]` (with the space). + +New in v0.23.9 (4th Jul 2018) +----------------------------- + +Changes: + +* Updated `Board.is_fivefold_repetition()`. FIDE rules have changed and the + repetition no longer needs to occur on consecutive alternating moves. + Thanks @LegionMammal978. + +New in v0.23.8 (1st Jul 2018) +----------------------------- + +Bugfixes: + +* `chess.syzygy`: Correctly initialize wide DTZ map for experimental 7 piece + table KRBBPvKQ. + +New in v0.23.7 (26th Jun 2018) +------------------------------ + +Bugfixes: + +* Fixed `ThreeCheckBoard.mirror()` and `CrazyhouseBoard.mirror()`, which + were previously resetting remaining checks and pockets respectively. + Thanks @QueensGambit. + +Changes: + +* `Board.move_stack` is now guaranteed to be UCI compatible with respect to + the representation of castling moves and `board.chess960`. +* Drop support for Python 3.3, which is long past end of life. +* `chess.uci`: The `position` command now manages `UCI_Chess960` and + `UCI_Variant` automatically. +* `chess.uci`: The `position` command will now always send the entire history + of moves from the root position. +* Various coding style fixes and improvements. Thanks @hugovk. + +New features: + +* Added `Board.root()`. + +New in v0.23.6 (25th May 2018) +------------------------------ + +Bugfixes: + +* Gaviota: Fix Python based Gaviota tablebase probing when there are multiple + en passant captures. Thanks @bjoernholzhauer. +* Syzygy: Fix DTZ for some mate in 1 positions. Similarly to the fix from + v0.23.1 this is mostly cosmetic. +* Syzygy: Fix DTZ off-by-one in some 6 piece antichess positions with moves + that threaten to force a capture. This is mostly cosmetic. + +Changes: + +* Let `uci.Engine.position()` send history of at least 8 moves if available. + Previously it sent only moves that were relevant for repetition detection. + This is mostly useful for Lc0. Once performance issues are solved, a future + version will always send the entire history. Thanks @SashaMN and @Mk-Chan. +* Various documentation fixes and improvements. + +New features: + +* Added `polyglot.MemoryMappedReader.get(board, default=None)`. + +New in v0.23.5 (11th May 2018) +------------------------------ + +Bugfixes: + +* Atomic chess: KNvKN is not insufficient material. +* Crazyhouse: Detect insufficient material. This can not happen unless the + game was started with insufficient material. + +Changes: + +* Better error messages when parsing info from UCI engine fails. +* Better error message for `b.set_board_fen(b.fen())`. + +New in v0.23.4 (29th Apr 2018) +------------------------------ + +New features: + +* XBoard: Support pondering. Thanks Manik Charan. +* UCI: Support unofficial `info ebf`. + +Bugfixes: + +* Implement 16 bit DTZ mapping, which is required for some of the longest + 7 piece endgames. + +New in v0.23.3 (21st Apr 2018) +------------------------------ + +New features: + +* XBoard: Support `variant`. Thanks gbtami. + +New in v0.23.2 (20th Apr 2018) +------------------------------ + +Bugfixes: + +* XBoard: Handle multiple features and features with spaces. Thanks gbtami. +* XBoard: Ignore debug output prefixed with `#`. Thanks Dan Ravensloft and + Manik Charan. + +New in v0.23.1 (13th Apr 2018) +------------------------------ + +Bugfixes: + +* Fix DTZ in case of mate in 1. This is a cosmetic fix, as the previous + behavior was only off by one (which is allowed by design). + +New in v0.23.0 (8th Apr 2018) +----------------------------- + +New features: + +* Experimental support for 7 piece Syzygy tablebases. + +Changes: + +* `chess.syzygy.filenames()` was renamed to `tablenames()` and + gained an optional `piece_count=6` argument. +* `chess.syzygy.normalize_filename()` was renamed to `normalize_tablename()`. +* The undocumented constructors of `chess.syzygy.WdlTable` and + `chess.syzygy.DtzTable` have been changed. + +New in v0.22.2 (15th Mar 2018) +------------------------------ + +Bugfixes: + +* In standard chess promoted pieces were incorrectly considered as + distinguishable from normal pieces with regard to position equality + and threefold repetition. Thanks to kn-sq-tb for reporting. + +Changes: + +* The PGN `game.headers` are now a custom mutable mapping that validates the + validity of tag names. +* Basic attack and pin methods moved to `BaseBoard`. +* Documentation fixes and improvements. + +New features: + +* Added `Board.lan()` for long algebraic notation. + +New in v0.22.1 (1st Jan 2018) +----------------------------- + +New features: + +* Added `Board.mirror()`, `SquareSet.mirror()` and `bswap()`. +* Added `chess.pgn.GameNode.accept_subgame()`. +* XBoard: Added `resign`, `analyze`, `exit`, `name`, `rating`, `computer`, + `egtpath`, `pause`, `resume`. Completed option parsing. + +Changes: + +* `chess.pgn`: Accept FICS wilds without warning. +* XBoard: Inform engine about game results. + +Bugfixes: + +* `chess.pgn`: Allow games without movetext. +* XBoard: Fixed draw handling. + +New in v0.22.0 (20th Nov 2017) +------------------------------ + +Changes: + +* `len(board.legal_moves)` **replaced by** `board.legal_moves.count()`. + Previously `list(board.legal_moves)` was generating moves twice, resulting in + a considerable slowdown. Thanks to Martin C. Doege for reporting. +* **Dropped Python 2.6 support.** +* XBoard: `offer_draw` renamed to `draw`. + +New features: + +* XBoard: Added `DrawHandler`. + +New in v0.21.2 (17th Nov 2017) +------------------------------ + +Changes: + +* `chess.svg` is now fully SVG Tiny 1.2 compatible. Removed + `chess.svg.DEFAULT_STYLE` which would from now on be always empty. + +New in v0.21.1 (14th Nov 2017) +------------------------------ + +Bugfixes: + +* `Board.set_piece_at()` no longer shadows optional `promoted` + argument from `BaseBoard`. +* Fixed `ThreeCheckBoard.is_irreversible()` and + `ThreeCheckBoard._transposition_key()`. + +New features: + +* Added `Game.without_tag_roster()`. `chess.pgn.StringExporter()` can now + handle games without any headers. +* XBoard: `white`, `black`, `random`, `nps`, `otim`, `undo`, `remove`. Thanks + to Manik Charan. + +Changes: + +* Documentation fixes and tweaks by Boštjan Mejak. +* Changed unicode character for empty squares in `Board.unicode()`. + +New in v0.21.0 (13th Nov 2017) +------------------------------ + +Release yanked. + +New in v0.20.1 (16th Oct 2017) +------------------------------ + +Bugfixes: + +* Fix arrow positioning on SVG boards. +* Documentation fixes and improvements, making most doctests runnable. + +New in v0.20.0 (13th Oct 2017) +------------------------------ + +Bugfixes: + +* Some XBoard commands were not returning futures. +* Support semicolon comments in PGNs. + +Changes: + +* Changed FEN and EPD formatting options. It is now possible to include en + passant squares in FEN and X-FEN style, or to include only strictly relevant + en passant squares. +* Relax en passant square validation in `Board.set_fen()`. +* Ensure `is_en_passant()`, `is_capture()`, `is_zeroing()` and + `is_irreversible()` strictly return bools. +* Accept `Z0` as a null move in PGNs. + +New features: + +* XBoard: Add `memory`, `core`, `stop` and `movenow` commands. + Abstract `post`/`nopost`. Initial `FeatureMap` support. Support `usermove`. +* Added `Board.has_pseudo_legal_en_passant()`. +* Added `Board.piece_map()`. +* Added `SquareSet.carry_rippler()`. +* Factored out some (unstable) low level APIs: `BB_CORNERS`, + `_carry_rippler()`, `_edges()`. + +New in v0.19.0 (27th Jul 2017) +------------------------------ + +New features: + +* **Experimental XBoard engine support.** Thanks to Manik Charan and + Cash Costello. Expect breaking changes in future releases. +* Added an undocumented `chess.polyglot.ZobristHasher` to make Zobrist hashing + easier to extend. + +Bugfixes: + +* Merely pseudo-legal en passant does no longer count for repetitions. +* Fixed repetition detection in Three-Check and Crazyhouse. (Previously + check counters and pockets were ignored.) +* Checking moves in Three-Check are now considered as irreversible by + `ThreeCheckBoard.is_irreversible()`. +* `chess.Move.from_uci("")` was raising `IndexError` instead of `ValueError`. + Thanks Jonny Balls. + +Changes: + +* `chess.syzygy.Tablebases` constructor no longer supports directly opening + a directory. Use `chess.syzygy.open_tablebases()`. +* `chess.gaviota.PythonTablebases` and `NativeTablebases` constructors + no longer support directly opening a directory. + Use `chess.gaviota.open_tablebases()`. +* `chess.Board` instances are now compared by the position they represent, + not by exact match of the internal data structures (or even move history). +* Relaxed castling right validation in Chess960: Kings/rooks of opposing sites + are no longer required to be on the same file. +* Removed misnamed `Piece.__unicode__()` and `BaseBoard.__unicode__()`. Use + `Piece.unicode_symbol()` and `BaseBoard.unicode()` instead. +* Changed `chess.SquareSet.__repr__()`. +* Support `[Variant "normal"]` in PGNs. +* `pip install python-chess[engine]` instead of `python-chess[uci]` (since + the extra dependencies are required for both UCI and XBoard engines). +* Mixed documentation fixes and improvements. + +New in v0.18.4 (27th Jul 2017) +------------------------------ + +Changes: + +* Support `[Variant "fischerandom"]` in PGNs for Cutechess compatibility. + Thanks to Steve Maughan for reporting. + +New in v0.18.3 (28th Jun 2017) +------------------------------ + +Bugfixes: + +* `chess.gaviota.NativeTablebases.get_dtm()` and `get_wdl()` were missing. + +New in v0.18.2 (1st Jun 2017) +----------------------------- + +Bugfixes: + +* Fixed castling in atomic chess when there is a rank attack. +* The halfmove clock in Crazyhouse is no longer incremented unconditionally. + `CrazyhouseBoard.is_zeroing(move)` now considers pawn moves and captures as + zeroing. Added `Board.is_irreversible(move)` that can be used instead. +* Fixed an inconsistency where the `chess.pgn` tokenizer accepts long algebraic + notation but `Board.parse_san()` did not. + +Changes: + +* Added more NAG constants in `chess.pgn`. + +New in v0.18.1 (1st May 2017) +----------------------------- + +Bugfixes: + +* Crazyhouse drops were accepted as pseudo-legal (and legal) even if the + respective piece was not in the pocket. +* `CrazyhouseBoard.pop()` was failing to undo en passant moves. +* `CrazyhouseBoard.pop()` was always returning `None`. +* `Move.__copy__()` was failing to copy Crazyhouse drops. +* Fix ~ order (marker for promoted pieces) in FENs. +* Promoted pieces in Crazyhouse were not communicated with UCI engines. + +Changes: + +* `ThreeCheckBoard.uci_variant` changed from `threecheck` to `3check`. + +New in v0.18.0 (20th Apr 2017) +------------------------------ + +Bugfixes: + +* Fixed `Board.parse_uci()` for crazyhouse drops. Thanks to Ryan Delaney. +* Fixed `AtomicBoard.is_insufficient_material()`. +* Fixed signature of `SuicideBoard.was_into_check()`. +* Explicitly close input and output streams when a `chess.uci.PopenProcess` + terminates. +* The documentation of `Board.attackers()` was wrongly stating that en passant + capturable pawns are considered attacked. + +Changes: + +* `chess.SquareSet` is no longer hashable (since it is mutable). +* Removed functions and constants deprecated in v0.17.0. +* Dropped `gmpy2` and `gmpy` as optional dependencies. They were no longer + improving performance. +* Various tweaks and optimizations for 5% improvement in PGN parsing and perft + speed. (Signature of `_is_safe` and `_ep_skewered` changed). +* Rewritten `chess.svg.board()` using `xml.etree`. No longer supports *pre* and + *post*. Use an XML parser if you need to modify the SVG. Now only inserts + actually used piece defintions. +* Untangled UCI process and engine instanciation, changing signatures of + constructors and allowing arbitrary arguments to `subprocess.Popen`. +* Coding style and documentation improvements. + +New features: + +* `chess.svg.board()` now supports arrows. Thanks to @rheber for implementing + this feature. +* Let `chess.uci.PopenEngine` consistently handle Ctrl+C across platforms + and Python versions. `chess.uci.popen_engine()` now supports a `setpgrp` + keyword argument to start the engine process in a new process group. + Thanks to @dubiousjim. +* Added `board.king(color)` to find the (royal) king of a given side. +* SVGs now have `viewBox` and `chess.svg.board(size=None)` supports and + defaults to `None` (i.e. scaling to the size of the container). + +New in v0.17.0 (6th Mar 2017) +----------------------------- + +Changes: + +* Rewritten move generator, various performance tweaks, code simplications + (500 lines removed) amounting to **doubled PGN parsing and perft speed**. +* Removed `board.generate_evasions()` and `board.generate_non_evasions()`. +* Removed `board.transpositions`. Transpositions are now counted on demand. +* `file_index()`, `rank_index()`, and `pop_count()` have been renamed to + `square_file()`, `square_rank()` and `popcount()` respectively. Aliases will + be removed in some future release. +* `STATUS_ILLEGAL_CHECK` has been renamed to `STATUS_RACE_CHECK`. The alias + will be removed in a future release. +* Removed `DIAG_ATTACKS_NE`, `DIAG_ATTACKS_NW`, `RANK_ATTACKS` and + `FILE_ATTACKS` as well as the corresponding masks. New attack tables + `BB_DIAG_ATTACKS` (combined both diagonal tables), `BB_RANK_ATTACKS` and + `BB_FILE_ATTACKS` are indexed by square instead of mask. +* `board.push()` no longer requires pseudo-legality. +* Documentation improvements. + +Bugfixes: + +* **Positions in variant end are now guaranteed to have no legal moves.** + `board.is_variant_end()` has been added to test for special variant end + conditions. Thanks to salvador-dali. +* `chess.svg`: Fixed a typo in the class names of black queens. Fixed fill + color for black rooks and queens. Added SVG Tiny support. These combined + changes fix display in a number of applications, including + Jupyter Qt Console. Thanks to Alexander Meshcheryakov. +* `board.ep_square` was not consistently `None` instead of `0`. +* Detect invalid racing kings positions: `STATUS_RACE_OVER`, + `STATUS_RACE_MATERIAL`. +* `SAN_REGEX`, `FEN_CASTLING_REGEX` and `TAG_REGEX` now try to match the + entire string and no longer accept newlines. +* Fixed `Move.__hash__()` for drops. + +New features: + +* `board.remove_piece_at()` now returns the removed piece. +* Added `square_distance()` and `square_mirror()`. +* Added `msb()`, `lsb()`, `scan_reversed()` and `scan_forward()`. +* Added `BB_RAYS` and `BB_BETWEEN`. + +New in v0.16.2 (15th Jan 2017) +------------------------------ + +Changes: + +* `board.move_stack` now contains the exact move objects added with + `Board.push()` (instead of normalized copies for castling moves). + This ensures they can be used with `Board.variation_san()` amongst others. +* `board.ep_square` is now `None` instead of `0` for no en passant square. +* `chess.svg`: Better vector graphics for knights. Thanks to ProgramFox. +* Documentation improvements. + +New in v0.16.1 (12th Dec 2016) +------------------------------ + +Bugfixes: + +* Explosions in atomic chess were not destroying castling rights. Thanks to + ProgramFOX for finding this issue. + +New in v0.16.0 (11th Dec 2016) +------------------------------ + +Bugfixes: + +* `pin_mask()`, `pin()` and `is_pinned()` make more sense when already + in check. Thanks to Ferdinand Mosca. + +New features: + +* **Variant support: Suicide, Giveaway, Atomic, King of the Hill, Racing Kings, + Horde, Three-check, Crazyhouse.** `chess.Move` now supports drops. +* More fine grained dependencies. Use *pip install python-chess[uci,gaviota]* to + install dependencies for the full feature set. +* Added `chess.STATUS_EMPTY` and `chess.STATUS_ILLEGAL_CHECK`. +* The `board.promoted` mask keeps track of promoted pieces. +* Optionally copy boards without the move stack: `board.copy(stack=False)`. +* `examples/bratko_kopec` now supports avoid move (am), variants and + displays fractional scores immidiately. Thanks to Daniel Dugovic. +* `perft.py` rewritten with multi-threading support and moved to + `examples/perft`. +* `chess.syzygy.dependencies()`, `chess.syzygy.all_dependencies()` to generate + Syzygy tablebase dependencies. + +Changes: + +* **Endgame tablebase probing (Syzygy, Gaviota):** `probe_wdl()` **,** + `probe_dtz()` **and** `probe_dtm()` **now raise** `KeyError` **or** + `MissingTableError` **instead of returning** *None*. If you prefer getting + `None` in case of an error use `get_wdl()`, `get_dtz()` and `get_dtm()`. +* `chess.pgn.BaseVisitor.result()` returns `True` by default and is no longer + used by `chess.pgn.read_game()` if no game was found. +* Non-fast-forward update of the Git repository to reduce size (old binary + test assets removed). +* `board.pop()` now uses a boardstate stack to undo moves. +* `uci.engine.position()` will send the move history only until the latest + zeroing move. +* Optimize `board.clean_castling_rights()` and micro-optimizations improving + PGN parser performance by around 20%. +* Syzygy tables now directly use the endgame name as hash keys. +* Improve test performance (especially on Travis CI). +* Documentation updates and improvements. + +New in v0.15.4 +-------------- + +New features: + +* Highlight last move and checks when rendering board SVGs. + +New in v0.15.3 (21st Sep 2016) +------------------------------ + +Bugfixes: + +* `pgn.Game.errors` was not populated as documented. Thanks to Ryan Delaney + for reporting. + +New features: + +* Added `pgn.GameNode.add_line()` and `pgn.GameNode.main_line()` which make + it easier to work with lists of moves as variations. + +New in v0.15.2 +-------------- + +Bugfixes: + +* Fix a bug where `shift_right()` and `shift_2_right()` were producing + integers larger than 64bit when shifting squares off the board. This is + very similar to the bug fixed in v0.15.1. Thanks to piccoloprogrammatore + for reporting. + +New in v0.15.1 (12th Sep 2016) +------------------------------ + +Bugfixes: + +* Fix a bug where `shift_up_right()` and `shift_up_left()` were producing + integers larger than 64bit when shifting squares off the board. + +New features: + +* Replaced `__html__` with experimental SVG rendering for IPython. + +New in v0.15.0 (11th Aug 2016) +------------------------------ + +Changes: + +* `chess.uci.Score` **no longer has** `upperbound` **and** `lowerbound` + **attributes**. Previously these were always *False*. + +* Significant improvements of move generation speed, around **2.3x faster + PGN parsing**. Removed the following internal attributes and methods of + the `Board` class: `attacks_valid`, `attacks_to`, `attacks_from`, + `_pinned()`, `attacks_valid_stack`, `attacks_from_stack`, `attacks_to_stack`, + `generate_attacks()`. + +* UCI: Do not send *isready* directly after *go*. Though allowed by the UCI + protocol specification it is just not nescessary and many engines were having + trouble with this. + +* Polyglot: Use less memory for uniform random choices from big opening books + (reservoir sampling). + +* Documentation improvements. + +Bugfixes: + +* Allow underscores in PGN header tags. Found and fixed by Bajusz Tamás. + +New features: + +* Added `Board.chess960_pos()` to identify the Chess960 starting position + number of positions. + +* Added `chess.BB_BACKRANKS` and `chess.BB_PAWN_ATTACKS`. + +New in v0.14.1 (7th Jun 2016) +----------------------------- + +Bugfixes: + +* Backport Bugfix for Syzygy DTZ related to en passant. + See official-stockfish/Stockfish@6e2ca97d93812b2. + +Changes: + +* Added optional argument *max_fds=128* to `chess.syzygy.open_tablebases()`. + An LRU cache is used to keep at most *max_fds* files open. This allows using + many tables without running out of file descriptors. + Previously all tables were opened at once. + +* Syzygy and Gaviota now store absolute tablebase paths, in case you change + the working directory of the process. + +* The default implementation of `chess.uci.InfoHandler.score()` will no longer + store score bounds in `info["score"]`, only real scores. + +* Added `Board.set_chess960_pos()`. + +* Documentation improvements. + +New in v0.14.0 (7th Apr 2016) +----------------------------- + +Changes: + +* `Board.attacker_mask()` **has been renamed to** `Board.attackers_mask()` for + consistency. + +* **The signature of** `Board.generate_legal_moves()` **and** + `Board.generate_pseudo_legal_moves()` **has been changed.** Previously it + was possible to select piece types for selective move generation: + + `Board.generate_legal_moves(castling=True, pawns=True, knights=True, bishops=True, rooks=True, queens=True, king=True)` + + Now it is possible to select arbitrary sets of origin and target squares. + `to_mask` uses the corresponding rook squares for castling moves. + + `Board.generate_legal_moves(from_mask=BB_ALL, to_mask=BB)` + + To generate all knight and queen moves do: + + `board.generate_legal_moves(board.knights | board.queens)` + + To generate only castling moves use: + + `Board.generate_castling_moves(from_mask=BB_ALL, to_mask=BB_ALL)` + +* Additional hardening has been added on top of the bugfix from v0.13.3. + Diagonal skewers on the last double pawn move are now handled correctly, + even though such positions can not be reached with a sequence of legal moves. + +* `chess.syzygy` now uses the more efficient selective move generation. + +New features: + +* The following move generation methods have been added: + `Board.generate_pseudo_legal_ep(from_mask=BB_ALL, to_mask=BB_ALL)`, + `Board.generate_legal_ep(from_mask=BB_ALL, to_mask=BB_ALL)`, + `Board.generate_pseudo_legal_captures(from_mask=BB_ALL, to_mask=BB_ALL)`, + `Board.generate_legal_captures(from_mask=BB_ALL, to_mask=BB_ALL)`. + + +New in v0.13.3 (7th Apr 2016) +----------------------------- + +**This is a bugfix release for a move generation bug.** Other than the bugfix +itself there are only minimal fully backwardscompatible changes. +You should update immediately. + +Bugfixes: + +* When capturing en passant, both the capturer and the captured pawn disappear + from the fourth or fifth rank. If those pawns were covering a horizontal + attack on the king, then capturing en passant should not have been legal. + + `Board.generate_legal_moves()` and `Board.is_into_check()` have been fixed. + + The same principle applies for diagonal skewers, but nothing has been done + in this release: If the last double pawn move covers a diagonal attack, then + the king would have already been in check. + + v0.14.0 adds additional hardening for all cases. It is recommended you + upgrade to v0.14.0 as soon as you can deal with the + non-backwards compatible changes. + +Changes: + +* `chess.uci` now uses `subprocess32` if applicable (and available). + Additionally a lock is used to work around a race condition in Python 2, that + can occur when spawning engines from multiple threads at the same time. + +* Consistently handle tabs in UCI engine output. + +New in v0.13.2 (19th Jan 2016) +------------------------------ + +Changes: + +* `chess.syzygy.open_tablebases()` now raises if the given directory + does not exist. + +* Allow visitors to handle invalid `FEN` tags in PGNs. + +* Gaviota tablebase probing fails faster for piece counts > 5. + +Minor new features: + +* Added `chess.pgn.Game.from_board()`. + +New in v0.13.1 (20th Dec 2015) +------------------------------ + +Changes: + +* Missing *SetUp* tags in PGNs are ignored. + +* Incompatible comparisons on `chess.Piece`, `chess.Move`, `chess.Board` + and `chess.SquareSet` now return *NotImplemented* instead of *False*. + +Minor new features: + +* Factored out basic board operations to `chess.BaseBoard`. This is inherited + by `chess.Board` and extended with the usual move generation features. + +* Added optional *claim_draw* argument to `chess.Base.is_game_over()`. + +* Added `chess.Board.result(claim_draw=False)`. + +* Allow `chess.Board.set_piece_at(square, None)`. + +* Added `chess.SquareSet.from_square(square)`. + +New in v0.13.0 (10th Nov 2015) +------------------------------ + +* `chess.pgn.Game.export()` and `chess.pgn.GameNode.export()` have been + removed and replaced with a new visitor concept. + +* `chess.pgn.read_game()` no longer takes an `error_handler` argument. Errors + are now logged. Use the new visitor concept to change this behaviour. + +New in v0.12.5 (18th Oct 2015) +------------------------------ + +Bugfixes: + +* Context manager support for pure Python Gaviota probing code. Various + documentation fixes for Gaviota probing. Thanks to Jürgen Précour for + reporting. + +* PGN variation start comments for variations on the very first move were + assigned to the game. Thanks to Norbert Räcke for reporting. + +New in v0.12.4 (13th Oct 2015) +------------------------------ + +Bugfixes: + +* Another en passant related Bugfix for pure Python Gaviota tablebase probing. + +New features: + +* Added `pgn.GameNode.is_end()`. + +Changes: + +* Big speedup for `pgn` module. Boards are cached less agressively. Board + move stacks are copied faster. + +* Added tox.ini to specify test suite and flake8 options. + +New in v0.12.3 (9th Oct 2015) +----------------------------- + +Bugfixes: + +* Some invalid castling rights were silently ignored by `Board.set_fen()`. Now + it is ensured information is stored for retrieval using `Board.status()`. + +New in v0.12.2 (7th Oct 2015) +----------------------------- + +Bugfixes: + +* Some Gaviota probe results were incorrect for positions where black could + capture en passant. + +New in v0.12.1 (7th Oct 2015) +----------------------------- + +Changes: + +* Robust handling of invalid castling rights. You can also use the new + method `Board.clean_castling_rights()` to get the subset of strictly valid + castling rights. + +New in v0.12.0 (3rd Oct 2015) +----------------------------- + +New features: + +* Python 2.6 support. Patch by vdbergh. + +* Pure Python Gaviota tablebase probing. Thanks to Jean-Noël Avila. + +New in v0.11.1 (7th Sep 2015) +----------------------------- + +Bugfixes: + +* `syzygy.Tablebases.probe_dtz()` has was giving wrong results for some + positions with possible en passant capturing. This was found and fixed + upstream: https://github.com/official-stockfish/Stockfish/issues/394. + +* Ignore extra spaces in UCI `info` lines, as for example sent by the + Hakkapeliitta engine. Thanks to Jürgen Précour for reporting. + +New in v0.11.0 (6th Sep 2015) +----------------------------- + +Changes: + +* **Chess960** support and the **representation of castling moves** has been + changed. + + The constructor of board has a new `chess960` argument, defaulting to + `False`: `Board(fen=STARTING_FEN, chess960=False)`. That property is + available as `Board.chess960`. + + In Chess960 mode the behaviour is as in the previous release. Castling moves + are represented as a king move to the corresponding rook square. + + In the default standard chess mode castling moves are represented with + the standard UCI notation, e.g. `e1g1` for king-side castling. + + `Board.uci(move, chess960=None)` creates UCI representations for moves. + Unlike `Move.uci()` it can convert them in the context of the current + position. + + `Board.has_chess960_castling_rights()` has been added to test for castling + rights that are impossible in standard chess. + + The modules `chess.polyglot`, `chess.pgn` and `chess.uci` will transparently + handle both modes. + +* In a previous release `Board.fen()` has been changed to only display an + en passant square if a legal en passant move is indeed possible. This has + now also been adapted for `Board.shredder_fen()` and `Board.epd()`. + +New features: + +* Get individual FEN components: `Board.board_fen()`, `Board.castling_xfen()`, + `Board.castling_shredder_fen()`. + +* Use `Board.has_legal_en_passant()` to test if a position has a legal + en passant move. + +* Make `repr(board.legal_moves)` human readable. + +New in v0.10.1 (30th Aug 2015) +------------------------------ + +Bugfixes: + +* Fix use-after-free in Gaviota tablebase initialization. + +New in v0.10.0 (28th Aug 2015) +------------------------------ + +New dependencies: + +* If you are using Python < 3.2 you have to install `futures` in order to + use the `chess.uci` module. + +Changes: + +* There are big changes in the UCI module. Most notably in async mode multiple + commands can be executed at the same time (e.g. `go infinite` and then + `stop` or `go ponder` and then `ponderhit`). + + `go infinite` and `go ponder` will now wait for a result, i.e. you may have + to call `stop` or `ponderhit` from a different thread or run the commands + asynchronously. + + `stop` and `ponderhit` no longer have a result. + +* The values of the color constants `chess.WHITE` and `chess.BLACK` have been + changed. Previously `WHITE` was `0`, `BLACK` was `1`. Now `WHITE` is `True`, + `BLACK` is `False`. The recommended way to invert `color` is using + `not color`. + +* The pseudo piece type `chess.NONE` has been removed in favor of just using + `None`. + +* Changed the `Board(fen)` constructor. If the optional `fen` argument is not + given behavior did not change. However if `None` is passed explicitly an + empty board is created. Previously the starting position would have been + set up. + +* `Board.fen()` will now only show completely legal en passant squares. + +* `Board.set_piece_at()` and `Board.remove_piece_at()` will now clear the + move stack, because the old moves may not be valid in the changed position. + +* `Board.parse_uci()` and `Board.push_uci()` will now accept null moves. + +* Changed shebangs from `#!/usr/bin/python` to `#!/usr/bin/env python` for + better virtualenv support. + +* Removed unused game data files from repository. + +Bugfixes: + +* PGN: Prefer the game result from the game termination marker over `*` in the + header. These should be identical in standard compliant PGNs. Thanks to + Skyler Dawson for reporting this. + +* Polyglot: `minimum_weight` for `find()`, `find_all()` and `choice()` was + not respected. + +* Polyglot: Negative indexing of opening books was raising `IndexError`. + +* Various documentation fixes and improvements. + +New features: + +* Experimental probing of Gaviota tablebases via libgtb. + +* New methods to construct boards: + + .. code:: python + + >>> chess.Board.empty() + Board('8/8/8/8/8/8/8/8 w - - 0 1') + + >>> board, ops = chess.Board.from_epd("4k3/8/8/8/8/8/8/4K3 b - - fmvn 17; hmvc 13") + >>> board + Board('4k3/8/8/8/8/8/8/4K3 b - - 13 17') + >>> ops + {'fmvn': 17, 'hmvc': 13} + +* Added `Board.copy()` and hooks to let the copy module to the right thing. + +* Added `Board.has_castling_rights(color)`, + `Board.has_kingside_castling_rights(color)` and + `Board.has_queenside_castling_rights(color)`. + +* Added `Board.clear_stack()`. + +* Support common set operations on `chess.SquareSet()`. + +New in v0.9.1 (15th Jul 2015) +----------------------------- + +Bugfixes: + +* UCI module could not handle castling ponder moves. Thanks to Marco Belli for + reporting. +* The initial move number in PGNs was missing, if black was to move in the + starting position. Thanks to Jürgen Précour for reporting. +* Detect more impossible en passant squares in `Board.status()`. There already + was a requirement for a pawn on the fifth rank. Now the sixth and seventh + rank must be empty, additionally. We do not do further retrograde analysis, + because these are the only cases affecting move generation. + +New in v0.8.3 (15th Jul 2015) +----------------------------- + +Bugfixes: + +* The initial move number in PGNs was missing, if black was to move in the + starting position. Thanks to Jürgen Précour for reporting. +* Detect more impossible en passant squares in `Board.status()`. There already + was a requirement for a pawn on the fifth rank. Now the sixth and seventh + rank must be empty, additionally. We do not do further retrograde analysis, + because these are the only cases affecting move generation. + +New in v0.9.0 (24th Jun 2015) +----------------------------- + +**This is a big update with quite a few breaking changes. Carefully review +the changes before upgrading. It's no problem if you can not update right now. +The 0.8.x branch still gets bugfixes.** + +Incompatible changes: + +* Removed castling right constants. Castling rights are now represented as a + bitmask of the rook square. For example: + + .. code:: python + + >>> board = chess.Board() + + >>> # Standard castling rights. + >>> board.castling_rights == chess.BB_A1 | chess.BB_H1 | chess.BB_A8 | chess.BB_H8 + True + + >>> # Check for the presence of a specific castling right. + >>> can_white_castle_queenside = chess.BB_A1 & board.castling_rights + + Castling moves were previously encoded as the corresponding king movement in + UCI, e.g. `e1f1` for white kingside castling. **Now castling moves are + encoded as a move to the corresponding rook square** (`UCI_Chess960`-style), + e.g. `e1a1`. + + You may use the new methods `Board.uci(move, chess960=True)`, + `Board.parse_uci(uci)` and `Board.push_uci(uci)` to handle this + transparently. + + The `uci` module takes care of converting moves when communicating with an + engine that is not in `UCI_Chess960` mode. + +* The `get_entries_for_position(board)` method of polyglot opening book readers + has been changed to `find_all(board, minimum_weight=1)`. By default entries + with weight 0 are excluded. + +* The `Board.pieces` lookup list has been removed. + +* In 0.8.1 the spelling of repetition (was repitition) was fixed. + `can_claim_threefold_repetition()` and `is_fivefold_repetition()` are the + affected method names. Aliases are now removed. + +* `Board.set_epd()` will now interpret `bm`, `am` as a list of moves for the + current position and `pv` as a variation (represented by a list of moves). + Thanks to Jordan Bray for reporting this. + +* Removed `uci.InfoHandler.pre_bestmove()` and + `uci.InfoHandler.post_bestmove()`. + +* `uci.InfoHandler().info["score"]` is now relative to multipv. Use + + .. code:: python + + >>> with info_handler as info: + ... if 1 in info["score"]: + ... cp = info["score"][1].cp + + where you were previously using + + .. code:: python + + >>> with info_handler as info: + ... if "score" in info: + ... cp = info["score"].cp + +* Clear `uci.InfoHandler()` dictionary at the start of new searches + (new `on_go()`), not at the end of searches. + +* Renamed `PseudoLegalMoveGenerator.bitboard` and `LegalMoveGenerator.bitboard` + to `PseudoLegalMoveGenerator.board` and `LegalMoveGenerator.board`, + respectively. + +* Scripts removed. + +* Python 3.2 compatibility dropped. Use Python 3.3 or higher. Python 2.7 + support is not affected. + +New features: + +* **Introduced Chess960 support.** `Board(fen)` and `Board.set_fen(fen)` now + support X-FENs. Added `Board.shredder_fen()`. + `Board.status(allow_chess960=True)` has an optional argument allowing to + insist on standard chess castling rules. + Added `Board.is_valid(allow_chess960=True)`. + +* **Improved move generation using** `Shatranj-style direct lookup + `_. **Removed rotated bitboards. Perft + speed has been more than doubled.** + +* Added `choice(board)` and `weighted_choice(board)` for polyglot opening book + readers. + +* Added `Board.attacks(square)` to determine attacks *from* a given square. + There already was `Board.attackers(color, square)` returning attacks *to* + a square. + +* Added `Board.is_en_passant(move)`, `Board.is_capture(move)` and + `Board.is_castling(move)`. + +* Added `Board.pin(color, square)` and `Board.is_pinned(color, square)`. + +* There is a new method `Board.pieces(piece_type, color)` to get a set of + squares with the specified pieces. + +* Do expensive Syzygy table initialization on demand. + +* Allow promotions like `e8Q` (usually `e8=Q`) in `Board.parse_san()` and + PGN files. + +* Patch by Richard C. Gerkin: Added `Board.__unicode__()` just like + `Board.__str__()` but with unicode pieces. +* Patch by Richard C. Gerkin: Added `Board.__html__()`. + +New in v0.8.2 (21st Jun 2015) +----------------------------- + +Bugfixes: + +* `pgn.Game.setup()` with the standard starting position was failing when the + standard starting position was already set. Thanks to Jordan Bray for + reporting this. + +Optimizations: + +* Remove `bswap()` from Syzygy decompression hot path. Directly read integers + with the correct endianness. + +New in v0.8.1 (29th May 2015) +----------------------------- + +* Fixed pondering mode in uci module. For example `ponderhit()` was blocking + indefinitely. Thanks to Valeriy Huz for reporting this. + +* Patch by Richard C. Gerkin: Moved searchmoves to the end of the UCI go + command, where it will not cause other command parameters to be ignored. + +* Added missing check or checkmate suffix to castling SANs, e.g. `O-O-O#`. + +* Fixed off-by-one error in polyglot opening book binary search. This would + not have caused problems for real opening books. + +* Fixed Python 3 support for reverse polyglot opening book iteration. + +* Bestmoves may be literally `(none)` in UCI protocol, for example in + checkmate positions. Fix parser and return `None` as the bestmove in this + case. + +* Fixed spelling of repetition (was repitition). + `can_claim_threefold_repetition()` and `is_fivefold_repetition()` are the + affected method names. Aliases are there for now, but will be removed in the + next release. Thanks to Jimmy Patrick for reporting this. + +* Added `SquareSet.__reversed__()`. + +* Use containerized tests on Travis CI, test against Stockfish 6, improved + test coverage amd various minor clean-ups. + +New in v0.8.0 (25th Mar 2015) +----------------------------- + +* **Implement Syzygy endgame tablebase probing.** + `https://syzygy-tables.info `_ + is an example project that provides a public API using the new features. + +* The interface for aynchronous UCI command has changed to mimic + `concurrent.futures`. `is_done()` is now just `done()`. Callbacks will + receive the command object as a single argument instead of the result. + The `result` property and `wait()` have been removed in favor of a + synchronously waiting `result()` method. + +* The result of the `stop` and `go` UCI commands are now named tuples (instead + of just normal tuples). + +* Add alias `Board` for `Bitboard`. + +* Fixed race condition during UCI engine startup. Lines received during engine + startup sometimes needed to be processed before the Engine object was fully + initialized. + +New in v0.7.0 (21st Feb 2015) +----------------------------- + +* **Implement UCI engine communication.** + +* Patch by Matthew Lai: `Add caching for gameNode.board()`. + +New in v0.6.0 (8th Nov 2014) +---------------------------- + +* If there are comments in a game before the first move, these are now assigned + to `Game.comment` instead of `Game.starting_comment`. `Game.starting_comment` + is ignored from now on. `Game.starts_variation()` is no longer true. + The first child node of a game can no longer have a starting comment. + It is possible to have a game with `Game.comment` set, that is otherwise + completely empty. + +* Fix export of games with variations. Previously the moves were exported in + an unusual (i.e. wrong) order. + +* Install `gmpy2` or `gmpy` if you want to use slightly faster binary + operations. + +* Ignore superfluous variation opening brackets in PGN files. + +* Add `GameNode.san()`. + +* Remove `sparse_pop_count()`. Just use `pop_count()`. + +* Remove `next_bit()`. Now use `bit_scan()`. + +New in v0.5.0 (28th Oct 2014) +----------------------------- + +* PGN parsing is now more robust: `read_game()` ignores invalid tokens. + Still exceptions are going to be thrown on illegal or ambiguous moves, but + this behaviour can be changed by passing an `error_handler` argument. + + .. code:: python + + >>> # Raises ValueError: + >>> game = chess.pgn.read_game(file_with_illegal_moves) + + .. code:: python + + >>> # Silently ignores errors and continues parsing: + >>> game = chess.pgn.read_game(file_with_illegal_moves, None) + + .. code:: python + + >>> # Logs the error, continues parsing: + >>> game = chess.pgn.read_game(file_with_illegal_moves, logger.exception) + + If there are too many closing brackets this is now ignored. + + Castling moves like 0-0 (with zeros) are now accepted in PGNs. + The `Bitboard.parse_san()` method remains strict as always, though. + + Previously the parser was strictly following the PGN spefification in that + empty lines terminate a game. So a game like + + :: + + [Event "?"] + + { Starting comment block } + + 1. e4 e5 2. Nf3 Nf6 * + + would have ended directly after the starting comment. To avoid this, the + parser will now look ahead until it finds at least one move or a termination + marker like `*`, `1-0`, `1/2-1/2` or `0-1`. + +* Introduce a new function `scan_headers()` to quickly scan a PGN file for + headers without having to parse the full games. + +* Minor testcoverage improvements. + +New in v0.4.2 (11th Oct 2014) +----------------------------- + +* Fix bug where `pawn_moves_from()` and consequently `is_legal()` weren't + handling en passant correctly. Thanks to Norbert Naskov for reporting. + +New in v0.4.1 (26th Aug 2014) +----------------------------- + +* Fix `is_fivefold_repitition()`: The new fivefold repetition rule requires + the repetitions to occur on *alternating consecutive* moves. + +* Minor testing related improvements: Close PGN files, allow running via + setuptools. + +* Add recently introduced features to README. + +New in v0.4.0 (19th Aug 2014) +----------------------------- + +* Introduce `can_claim_draw()`, `can_claim_fifty_moves()` and + `can_claim_threefold_repitition()`. + +* Since the first of July 2014 a game is also over (even without claim by one + of the players) if there were 75 moves without a pawn move or capture or + a fivefold repetition. Let `is_game_over()` respect that. Introduce + `is_seventyfive_moves()` and `is_fivefold_repitition()`. Other means of + ending a game take precedence. + +* Threefold repetition checking requires efficient hashing of positions + to build the table. So performance improvements were needed there. The + default polyglot compatible zobrist hashes are now built incrementally. + +* Fix low level rotation operations `l90()`, `l45()` and `r45()`. There was + no problem in core because correct versions of the functions were inlined. + +* Fix equality and inequality operators for `Bitboard`, `Move` and `Piece`. + Also make them robust against comparisons with incompatible types. + +* Provide equality and inequality operators for `SquareSet` and + `polyglot.Entry`. + +* Fix return values of incremental arithmetical operations for `SquareSet`. + +* Make `polyglot.Entry` a `collections.namedtuple`. + +* Determine and improve test coverage. + +* Minor coding style fixes. + +New in v0.3.1 (15th Aug 2014) +----------------------------- + +* `Bitboard.status()` now correctly detects `STATUS_INVALID_EP_SQUARE`, + instead of errors or false reports. + +* Polyglot opening book reader now correctly handles knight underpromotions. + +* Minor coding style fixes, including removal of unused imports. + +New in v0.3.0 (13th Aug 2014) +----------------------------- + +* Rename property `half_moves` of `Bitboard` to `halfmove_clock`. + +* Rename property `ply` of `Bitboard` to `fullmove_number`. + +* Let PGN parser handle symbols like `!`, `?`, `!?` and so on by converting + them to NAGs. + +* Add a human readable string representation for Bitboards. + + .. code:: python + + >>> print(chess.Bitboard()) + r n b q k b n r + p p p p p p p p + . . . . . . . . + . . . . . . . . + . . . . . . . . + . . . . . . . . + P P P P P P P P + R N B Q K B N R + +* Various documentation improvements. + +New in v0.2.0 +------------- + +* **Implement PGN parsing and writing.** +* Hugely improve test coverage and use Travis CI for continuous integration and + testing. +* Create an API documentation. +* Improve Polyglot opening-book handling. + +New in v0.1.0 +------------- + +Apply the lessons learned from the previous releases, redesign the API and +implement it in pure Python. + +New in v0.0.4 +------------- + +Implement the basics in C++ and provide bindings for Python. Obviously +performance was a lot better - but at the expense of having to compile +code for the target platform. + +Pre v0.0.4 +---------- + +First experiments with a way too slow pure Python API, creating way too many +objects for basic operations. diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 22848e436..03a9555d1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,1068 +1,408 @@ Changelog for python-chess ========================== -New in v0.19.0 --------------- - -New features: - -* **Experimental XBoard engine support.** Thanks to Manik Charan and - Cash Costello. Expect breaking changes in future releases. -* Added an undocumented `chess.polyglot.ZobristHasher` to make Zobrist hashing - easier to extend. +New in v1.11.2 (25th Feb 2025) +------------------------------ Bugfixes: -* Merely pseudo-legal en passant does no longer count for repetitions. -* Fixed repetition detection in Three-Check and Crazyhouse. (Previously - check counters and pockets were ignored.) -* Checking moves in Three-Check are now considered as irreversible by - `ThreeCheckBoard.is_irreversible()`. -* `chess.Move.from_uci("")` was raising `IndexError` instead of `ValueError`. - Thanks Jonny Balls. - -Changes: - -* `chess.syzygy.Tablebases` constructor no longer supports directly opening - a directory. Use `chess.syzygy.open_tablebases()`. -* `chess.gaviota.PythonTablebases` and `NativeTablebases` constructors - no longer support directly opening a directory. - Use `chess.gaviota.open_tablebases()`. -* `chess.Board` instances are now compared by the position they represent, - not by exact match of the internal data structures (or even move history). -* Relaxed castling right validation in Chess960: Kings/rooks of opposing sites - are no longer required to be on the same file. -* Removed misnamed `Piece.__unicode__()` and `BaseBoard.__unicode__()`. Use - `Piece.unicode_symbol()` and `BaseBoard.unicode()` instead. -* Changed `chess.SquareSet.__repr__()`. -* Support `[Variant "normal"]` in PGNs. -* `pip install python-chess[engine]` instead of `python-chess[uci]` (since - the extra dependencies are required for both UCI and XBoard engines). -* Mixed documentation fixes and improvements. - -New in v0.18.4 --------------- - -Changes: - -* Support `[Variant "fischerandom"]` in PGNs for Cutechess compability. - Thanks to Steve Maughan for reporting. +* Fix ``chess.gaviota.PythonTablebase`` does not properly resolve positions + where en passant captures are the best move. -New in v0.18.3 --------------- +New in v1.11.1 (9th Oct 2024) +----------------------------- Bugfixes: -* `chess.gaviota.NativeTablebases.get_dtm()` and `get_wdl()` were missing. - -New in v0.18.2 --------------- - -Bugfixes: +* ``chess.engine``: Fix parsing of UCI options containing containing ``name``, + ``type``, ``default``, ``min``, or ``max``, e.g., ``mini``. -* Fixed castling in atomic chess when there is a rank attack. -* The halfmove clock in Crazyhouse is no longer incremented unconditionally. - `CrazyhouseBoard.is_zeroing(move)` now considers pawn moves and captures as - zeroing. Added `Board.is_irreversible(move)` that can be used instead. -* Fixed an inconsistency where the `chess.pgn` tokenizer accepts long algebraic - notation but `Board.parse_san()` did not. +New in v1.11.0 (4th Oct 2024) +----------------------------- Changes: -* Added more NAG constants in `chess.pgn`. - -New in v0.18.1 --------------- - -Bugfixes: - -* Crazyhouse drops were accepted as pseudo legal (and legal) even if the - respective piece was not in the pocket. -* `CrazyhouseBoard.pop()` was failing to undo en passant moves. -* `CrazyhouseBoard.pop()` was always returning `None`. -* `Move.__copy__()` was failing to copy Crazyhouse drops. -* Fix ~ order (marker for promoted pieces) in FENs. -* Promoted pieces in Crazyhouse were not communicated with UCI engines. +* Drop support for Python 3.7, which has reached its end of life. +* ``chess.engine.EventLoopPolicy`` is no longer needed and now merely an alias + for the default event loop policy. +* If available and requested via ``setpgrp``, use ``process_group`` support + from Python 3.11 for engine processes. +* No longer eagerly reject 8 piece positions in ``chess.syzygy``, so that + some 8 piece positions with decisive captures can be probed successfully. +* The string wrapper returned by ``chess.svg`` functions now also implements + ``_repr_html_``. +* Significant changes to ``chess.engine`` internals: + ``chess.engine.BaseCommand`` methods other than the constructor no longer + receive ``engine: Protocol``. +* Significant changes to board state internals: Subclasses of ``chess.Board`` + can no longer hook into board state recording/restoration and need to + override relevant methods instead (``clear_stack``, ``copy``, ``root``, + ``push``, ``pop``). -Changes: - -* `ThreeCheckBoard.uci_variant` changed from `threecheck` to `3check`. +New features: -New in v0.18.0 --------------- +* Add ``chess.pgn.Game.time_control()`` and related data models. +* Add model ``sf16.1`` for ``chess.engine.Score.wdl()``, the new default. Bugfixes: -* Fixed `Board.parse_uci()` for crazyhouse drops. Thanks to Ryan Delaney. -* Fixed `AtomicBoard.is_insufficient_material()`. -* Fixed signature of `SuicideBoard.was_into_check()`. -* Explicitly close input and output streams when a `chess.uci.PopenProcess` - terminates. -* The documentation of `Board.attackers()` was wrongly stating that en passant - capturable pawns are considered attacked. - -Changes: +* Fix unsolicited engine output may cause assertion errors with regard to + command states. +* Fix handling of whitespace in UCI engine communication. +* For ``chess.Board.epd()`` and ``chess.Board.set_epd()``, require that EPD + opcodes start with a letter. -* `chess.SquareSet` is no longer hashable (since it is mutable). -* Removed functions and constants deprecated in v0.17.0. -* Dropped `gmpy2` and `gmpy` as optional dependencies. They were no longer - improving performance. -* Various tweaks and optimizations for 5% improvement in PGN parsing and perft - speed. (Signature of `_is_safe` and `_ep_skewered` changed). -* Rewritten `chess.svg.board()` using `xml.etree`. No longer supports *pre* and - *post*. Use an XML parser if you need to modify the SVG. Now only inserts - actually used piece defintions. -* Untangled UCI process and engine instanciation, changing signatures of - constructors and allowing arbitrary arguments to `subprocess.Popen`. -* Coding style and documentation improvements. +New in v1.10.0 (27th Jul 2023) +------------------------------ New features: -* `chess.svg.board()` now supports arrows. Thanks to @rheber for implementing - this feature. -* Let `chess.uci.PopenEngine` consistently handle Ctrl+C across platforms - and Python versions. `chess.uci.popen_engine()` now supports a `setpgrp` - keyword argument to start the engine process in a new process group. - Thanks to @dubiousjim. -* Added `board.king(color)` to find the (royal) king of a given side. -* SVGs now have `viewBox` and `chess.svg.board(size=None)` supports and - defaults to `None` (i.e. scaling to the size of the container). - -New in v0.17.0 --------------- - -Changes: - -* Rewritten move generator, various performance tweaks, code simplications - (500 lines removed) amounting to **doubled PGN parsing and perft speed**. -* Removed `board.generate_evasions()` and `board.generate_non_evasions()`. -* Removed `board.transpositions`. Transpositions are now counted on demand. -* `file_index()`, `rank_index()`, and `pop_count()` have been renamed to - `square_file()`, `square_rank()` and `popcount()` respectively. Aliases will - be removed in some future release. -* `STATUS_ILLEGAL_CHECK` has been renamed to `STATUS_RACE_CHECK`. The alias - will be removed in a future release. -* Removed `DIAG_ATTACKS_NE`, `DIAG_ATTACKS_NW`, `RANK_ATTACKS` and - `FILE_ATTACKS` as well as the corresponding masks. New attack tables - `BB_DIAG_ATTACKS` (combined both diagonal tables), `BB_RANK_ATTACKS` and - `BB_FILE_ATTACKS` are indexed by square instead of mask. -* `board.push()` no longer requires pseudo-legality. -* Documentation improvements. +* Use ``chess.engine.Opponent`` to send opponent information to engines. +* Inform engines about the game result using + ``chess.engine.Protocol.send_game_result()``. +* Add ``chess.engine.Limit.clock_id``. +* Add ``chess.svg.board(..., borders=True)``. +* Avoid rendering background behind SVG boards to better support transparency. +* Add ``chess.pgn.BaseVisitor.begin_parse_san()``. +* Introduce new distance metrics ``chess.square_manhattan_distance()`` and + ``chess.square_knight_distance()``. Bugfixes: -* **Positions in variant end are now guaranteed to have no legal moves.** - `board.is_variant_end()` has been added to test for special variant end - conditions. Thanks to salvador-dali. -* `chess.svg`: Fixed a typo in the class names of black queens. Fixed fill - color for black rooks and queens. Added SVG Tiny support. These combined - changes fix display in a number of applications, including - Jupyter Qt Console. Thanks to Alexander Meshcheryakov. -* `board.ep_square` was not consistently `None` instead of `0`. -* Detect invalid racing kings positions: `STATUS_RACE_OVER`, - `STATUS_RACE_MATERIAL`. -* `SAN_REGEX`, `FEN_CASTLING_REGEX` and `TAG_REGEX` now try to match the - entire string and no longer accept newlines. -* Fixed `Move.__hash__()` for drops. - -New features: - -* `board.remove_piece_at()` now returns the removed piece. -* Added `square_distance()` and `square_mirror()`. -* Added `msb()`, `lsb()`, `scan_reversed()` and `scan_forward()`. -* Added `BB_RAYS` and `BB_BETWEEN`. - -New in v0.16.2 --------------- +* Fix ``chess.pgn.GameNode.eval()`` sometimes off by one centipawn. +* Fix handling of additional spaces between UCI option tokens. +* Handle implicit XBoard engine resignation via output like + ``0-1 {White resigns}``. Changes: -* `board.move_stack` now contains the exact move objects added with - `Board.push()` (instead of normalized copies for castling moves). - This ensures they can be used with `Board.variation_san()` amongst others. -* `board.ep_square` is now `None` instead of `0` for no en passant square. -* `chess.svg`: Better vector graphics for knights. Thanks to ProgramFox. -* Documentation improvements. +* Add model ``sf16`` for ``chess.engine.Score.wdl()``, the new default. +* Update ``lichess`` WDL model. +* Keep PGN headers that do not belong to the Seven Tag Roster in insertion + order. +* Halve the number of open file descriptors maintained by tablebases + and opening books. +* Reduce verbosity of logged ``chess.pgn`` errors. -New in v0.16.1 --------------- +New in v1.9.4 (22nd Dec 2022) +----------------------------- Bugfixes: -* Explosions in atomic chess were not destroying castling rights. Thanks to - ProgramFOX for finding this issue. +* Fix ``PovScore.wdl()`` ignored ``model`` and ``ply`` parameters. +* ``chess.syzygy``: Check that board matches tablebase variant. -New in v0.16.0 --------------- +New features: -Bugfixes: +* Add model ``sf15.1`` for ``chess.engine.Score.wdl()``. +* Raise more specific exceptions: ``chess.IllegalMoveError``, + ``chess.AmbiguousMoveError``, and ``chess.InvalidMoveError``. -* `pin_mask()`, `pin()` and `is_pinned()` make more sense when already - in check. Thanks to Ferdinand Mosca. +New in v1.9.3 (16th Sep 2022) +----------------------------- -New features: +Bugfixes: -* **Variant support: Suicide, Giveaway, Atomic, King of the Hill, Racing Kings, - Horde, Three-check, Crazyhouse.** `chess.Move` now supports drops. -* More fine grained dependencies. Use *pip install python-chess[uci,gaviota]* to - install dependencies for the full feature set. -* Added `chess.STATUS_EMPTY` and `chess.STATUS_ILLEGAL_CHECK`. -* The `board.promoted` mask keeps track of promoted pieces. -* Optionally copy boards without the move stack: `board.copy(stack=False)`. -* `examples/bratko_kopec` now supports avoid move (am), variants and - displays fractional scores immidiately. Thanks to Daniel Dugovic. -* `perft.py` rewritten with multi-threading support and moved to - `examples/perft`. -* `chess.syzygy.dependencies()`, `chess.syzygy.all_dependencies()` to generate - Syzygy tablebase dependencies. +* Fix some valid characters were not accepted in PGN tag names. Changes: -* **Endgame tablebase probing (Syzygy, Gaviota):** `probe_wdl()` **,** - `probe_dtz()` **and** `probe_dtm()` **now raise** `KeyError` **or** - `MissingTableError` **instead of returning** *None*. If you prefer getting - `None` in case of an error use `get_wdl()`, `get_dtz()` and `get_dtm()`. -* `chess.pgn.BaseVisitor.result()` returns `True` by default and is no longer - used by `chess.pgn.read_game()` if no game was found. -* Non-fast-forward update of the Git repository to reduce size (old binary - test assets removed). -* `board.pop()` now uses a boardstate stack to undo moves. -* `uci.engine.position()` will send the move history only until the latest - zeroing move. -* Optimize `board.clean_castling_rights()` and micro-optimizations improving - PGN parser performance by around 20%. -* Syzygy tables now directly use the endgame name as hash keys. -* Improve test performance (especially on Travis CI). -* Documentation updates and improvements. - -New in v0.15.4 --------------- +* Skip over syntactically invalid PGN tags. +* Detect Antichess insufficient material with two opposing knights. New features: -* Highlight last move and checks when rendering board SVGs. +* Add ``chess.Board.unicode(..., orientation=chess.WHITE)``. -New in v0.15.3 --------------- +New in v1.9.2 (17th Jun 2022) +----------------------------- Bugfixes: -* `pgn.Game.errors` was not populated as documented. Thanks to Ryan Delaney - for reporting. +* Fix recursive Crazyhouse move generation sometimes failing with + with ``RuntimeError``. +* Fix rendering of black pawn SVG on dark background. New features: -* Added `pgn.GameNode.add_line()` and `pgn.GameNode.main_line()` which make - it easier to work with lists of moves as variations. - -New in v0.15.2 --------------- - -Bugfixes: - -* Fix a bug where `shift_right()` and `shift_2_right()` were producing - integers larger than 64bit when shifting squares off the board. This is - very similar to the bug fixed in v0.15.1. Thanks to piccoloprogrammatore - for reporting. +* Add ``chess.engine.AnalysisResult.would_block()``. -New in v0.15.1 --------------- +New in v1.9.1 (28th May 2022) +----------------------------- Bugfixes: -* Fix a bug where `shift_up_right()` and `shift_up_left()` were producing - integers larger than 64bit when shifting squares off the board. - -New features: - -* Replaced __html__ with experimental SVG rendering for IPython. - -New in v0.15.0 --------------- +* Reject pawn capture SAN if the original file is not specified, e.g., + ``d5`` will no longer match ``cxd5``. Changes: -* `chess.uci.Score` **no longer has** `upperbound` **and** `lowerbound` - **attributes**. Previously these were always *False*. - -* Significant improvements of move generation speed, around **2.3x faster - PGN parsing**. Removed the following internal attributes and methods of - the `Board` class: `attacks_valid`, `attacks_to`, `attacks_from`, - `_pinned()`, `attacks_valid_stack`, `attacks_from_stack`, `attacks_to_stack`, - `generate_attacks()`. +* Tweak handling of whitespace in PGN comments: When parsing, any leading + and trailing whitespace (beyond one space) is preserved. When joining + multiple PGN comments, they are now separated with a space instead of a + newline character. When removing annotations from comments, leftover + whitespace is avoided. -* UCI: Do not send *isready* directly after *go*. Though allowed by the UCI - protocol specification it is just not nescessary and many engines were having - trouble with this. +New features: -* Polyglot: Use less memory for uniform random choices from big opening books - (reservoir sampling). +* Add model ``sf15`` for ``chess.engine.Score.wdl()``. -* Documentation improvements. +New in v1.9.0 (18th Mar 2022) +----------------------------- Bugfixes: -* Allow underscores in PGN header tags. Found and fixed by Bajusz Tamás. +* Expand position validation to detect check conflicting with en passant + square. New features: -* Added `Board.chess960_pos()` to identify the Chess960 starting position - number of positions. - -* Added `chess.BB_BACKRANKS` and `chess.BB_PAWN_ATTACKS`. +* Add ``chess.svg.board(..., fill=...)``. +* Let ``chess.svg.board()`` add ASCII board as description of SVG. +* Add hint when engine process dies due to illegal instruction. -New in v0.14.1 --------------- +New in v1.8.0 (23rd Dec 2021) +----------------------------- Bugfixes: -* Backport Bugfix for Syzygy DTZ related to en-passant. - See official-stockfish/Stockfish@6e2ca97d93812b2. - -Changes: - -* Added optional argument *max_fds=128* to `chess.syzygy.open_tablebases()`. - An LRU cache is used to keep at most *max_fds* files open. This allows using - many tables without running out of file descriptors. - Previously all tables were opened at once. - -* Syzygy and Gaviota now store absolute tablebase paths, in case you change - the working directory of the process. - -* The default implementation of `chess.uci.InfoHandler.score()` will no longer - store score bounds in `info["score"]`, only real scores. - -* Added `Board.set_chess960_pos()`. - -* Documentation improvements. - -New in v0.14.0 --------------- - -Changes: - -* `Board.attacker_mask()` **has been renamed to** `Board.attackers_mask()` for - consistency. - -* **The signature of** `Board.generate_legal_moves()` **and** - `Board.generate_pseudo_legal_moves()` **has been changed.** Previously it - was possible to select piece types for selective move generation: - - `Board.generate_legal_moves(castling=True, pawns=True, knights=True, bishops=True, rooks=True, queens=True, king=True)` - - Now it is possible to select arbitrary sets of origin and target squares. - `to_mask` uses the corresponding rook squares for castling moves. - - `Board.generate_legal_moves(from_mask=BB_ALL, to_mask=BB)` - - To generate all knight and queen moves do: - - `board.generate_legal_moves(board.knights | board.queens)` - - To generate only castling moves use: - - `Board.generate_castling_moves(from_mask=BB_ALL, to_mask=BB_ALL)` - -* Additional hardening has been added on top of the bugfix from v0.13.3. - Diagonal skewers on the last double pawn move are now handled correctly, - even though such positions can not be reached with a sequence of legal moves. - -* `chess.syzygy` now uses the more efficient selective move generation. +* Fix ``SquareSet.issuperset()`` and ``SquareSet.issubset()`` by swapping + their respective implementations. New features: -* The following move generation methods have been added: - `Board.generate_pseudo_legal_ep(from_mask=BB_ALL, to_mask=BB_ALL)`, - `Board.generate_legal_ep(from_mask=BB_ALL, to_mask=BB_ALL)`, - `Board.generate_pseudo_legal_captures(from_mask=BB_ALL, to_mask=BB_ALL)`, - `Board.generate_legal_captures(from_mask=BB_ALL, to_mask=BB_ALL)`. +* Read and write PGN comments like ``[%emt 0:05:21]``. +New in v1.7.0 (7th Oct 2021) +---------------------------- -New in v0.13.3 --------------- +New features: -**This is a bugfix release for a move generation bug.** Other than the bugfix -itself there are only minimal fully backwardscompatible changes. -You should update immediately. +* Add new models for ``chess.engine.Score.wdl()``: ``sf`` (the new default) + and ``sf14``. +* Add ``chess.Board.piece_map()``. Bugfixes: -* When capturing en passant, both the capturer and the captured pawn disappear - from the fourth or fifth rank. If those pawns were covering a horizontal - attack on the king, then capturing en passant should not have been legal. - - `Board.generate_legal_moves()` and `Board.is_into_check()` have been fixed. - - The same principle applies for diagonal skewers, but nothing has been done - in this release: If the last double pawn move covers a diagonal attack, then - the king would have already been in check. - - v0.14.0 adds additional hardening for all cases. It is recommended you - upgrade to v0.14.0 as soon as you can deal with the - non-backwards compatible changes. - -Changes: - -* `chess.uci` now uses `subprocess32` if applicable (and available). - Additionally a lock is used to work around a race condition in Python 2, that - can occur when spawning engines from multiple threads at the same time. - -* Consistently handle tabs in UCI engine output. - -New in v0.13.2 --------------- - -Changes: +* ``chess.pgn``: Fix skipping with nested variations. +* ``chess.svg``: Make check gradient compatible with QtSvg. -* `chess.syzygy.open_tablebases()` now raises if the given directory - does not exist. +New in v1.6.1 (12th Jun 2021) +----------------------------- -* Allow visitors to handle invalid `FEN` tags in PGNs. +Bugfixes: -* Gaviota tablebase probing fails faster for piece counts > 5. +* Make ``chess.engine.SimpleEngine.play(..., draw_offered=True)`` available. + Previously only added for ``chess.engine.Protocol``. -Minor new features: +New in v1.6.0 (11th Jun 2021) +----------------------------- -* Added `chess.pgn.Game.from_board()`. +New features: -New in v0.13.1 --------------- +* Allow offering a draw to XBoard engines using + ``chess.engine.Protocol.play(..., draw_offered=True)``. +* Now detects insufficient material in Horde. Thanks @stevepapazis! Changes: -* Missing *SetUp* tags in PGNs are ignored. - -* Incompatible comparisons on `chess.Piece`, `chess.Move`, `chess.Board` - and `chess.SquareSet` now return *NotImplemented* instead of *False*. - -Minor new features: - -* Factored out basic board operations to `chess.BaseBoard`. This is inherited - by `chess.Board` and extended with the usual move generation features. - -* Added optional *claim_draw* argument to `chess.Base.is_game_over()`. - -* Added `chess.Board.result(claim_draw=False)`. - -* Allow `chess.Board.set_piece_at(square, None)`. - -* Added `chess.SquareSet.from_square(square)`. - -New in v0.13.0 --------------- - -* `chess.pgn.Game.export()` and `chess.pgn.GameNode.export()` have been - removed and replaced with a new visitor concept. - -* `chess.pgn.read_game()` no longer takes an `error_handler` argument. Errors - are now logged. Use the new visitor concept to change this behaviour. - -New in v0.12.5 --------------- +* ``chess.engine.popen_engine(..., setpgrp=True)`` on Windows now merges + ``CREATE_NEW_PROCESS_GROUP`` into ``creationflags`` instead of overriding. + On Unix it now uses ``start_new_session`` instead of calling ``setpgrp`` in + ``preexec_fn``. +* Declare that ``chess.svg`` produces SVG Tiny 1.2, and prepare SVG 2 forwards + compatibility. Bugfixes: -* Context manager support for pure Python Gaviota probing code. Various - documentation fixes for Gaviota probing. Thanks to Jürgen Précour for - reporting. +* Fix slightly off-center pawns in ``chess.svg``. +* Fix typing error in Python 3.10 (due to added ``int.bit_count``). -* PGN variation start comments for variations on the very first move were - assigned to the game. Thanks to Norbert Räcke for reporting. - -New in v0.12.4 --------------- +New in v1.5.0 (7th Apr 2021) +---------------------------- Bugfixes: -* Another en passant related Bugfix for pure Python Gaviota tablebase probing. +* Fixed typing of ``chess.pgn.Mainline.__reversed__()``. It is now a generator, + and ``chess.pgn.ReverseMainline`` has been **removed**. + This is a breaking change but a required bugfix. +* Implement UCI **ponderhit** for consecutive calls to + ``chess.engine.Protocol.play(..., ponder=True)``. Previously, the pondering + search was always stopped and restarted. +* Provide the full move stack, not just the position, for UCI pondering. +* Fixed XBoard level in sudden death games. +* Ignore trailing space after ponder move sent by UCI engine. + Previously, such a move would be rejected. +* Prevent cancelling engine commands after they have already been cancelled or + completed. Some internals (``chess.engine.BaseCommand``) have been changed to + accomplish this. New features: -* Added `pgn.GameNode.is_end()`. +* Added ``chess.Board.outcome()``. +* Implement and accept usermove feature for XBoard engines. -Changes: +Special thanks to @MarkZH for many of the engine related changes in this +release! -* Big speedup for `pgn` module. Boards are cached less agressively. Board - move stacks are copied faster. +New in v1.4.0 (25th Jan 2021) +----------------------------- -* Added tox.ini to specify test suite and flake8 options. +New features: -New in v0.12.3 --------------- +* Let ``chess.pgn.GameNode.eval()`` accept PGN comments like + ``[%eval 2.5,11]``, meaning 250 centipawns at depth 11. + Use ``chess.pgn.GameNode.eval_depth()`` and + ``chess.pgn.GameNode.set_eval(..., depth)`` to get and set the depth. +* Read and write PGN comments with millisecond precision like + ``[%clk 1:23:45.678]``. -Bugfixes: +Changes: -* Some invalid castling rights were silently ignored by `Board.set_fen()`. Now - it is ensured information is stored for retrieval using `Board.status()`. +* Recover from invalid UTF-8 sent by an UCI engine, by ignoring that + (and only that) line. -New in v0.12.2 --------------- +New in v1.3.3 (27th Dec 2020) +----------------------------- Bugfixes: -* Some Gaviota probe results were incorrect for positions where black could - capture en passant. - -New in v0.12.1 --------------- +* Fixed unintended collisions and optimized ``chess.Piece.__hash__()``. +* Fixed false-positive ``chess.STATUS_IMPOSSIBLE_CHECK`` if checkers are + aligned with other king. Changes: -* Robust handling of invalid castling rights. You can also use the new - method `Board.clean_castling_rights()` to get the subset of strictly valid - castling rights. - -New in v0.12.0 --------------- +* Also detect ``chess.STATUS_IMPOSSIBLE_CHECK`` if checker is aligned with + en passant square and king. New features: -* Python 2.6 support. Patch by vdbergh. +* Implemented Lichess winning chance model for ``chess.engine.Score``: + ``score.wdl(model="lichess")``. -* Pure Python Gaviota tablebase probing. Thanks to Jean-Noël Avila. - -New in v0.11.1 --------------- +New in v1.3.2 (12th Dec 2020) +----------------------------- Bugfixes: -* `syzygy.Tablebases.probe_dtz()` has was giving wrong results for some - positions with possible en passant capturing. This was found and fixed - upstream: https://github.com/official-stockfish/Stockfish/issues/394. - -* Ignore extra spaces in UCI `info` lines, as for example sent by the - Hakkapeliitta engine. Thanks to Jürgen Précour for reporting. - -New in v0.11.0 --------------- - -Changes: - -* **Chess960** support and the **representation of castling moves** has been - changed. +* Added a new reason for ``board.status()`` to be invalid: + ``chess.STATUS_IMPOSSIBLE_CHECK``. This detects positions where two sliding + pieces are giving check while also being aligned with the king + on the same rank, file, or diagonal. Such positions are impossible to reach, + break Stockfish, and maybe other engines. - The constructor of board has a new `chess960` argument, defaulting to - `False`: `Board(fen=STARTING_FEN, chess960=False)`. That property is - available as `Board.chess960`. - - In Chess960 mode the behaviour is as in the previous release. Castling moves - are represented as a king move to the corresponding rook square. - - In the default standard chess mode castling moves are represented with - the standard UCI notation, e.g. `e1g1` for king-side castling. - - `Board.uci(move, chess960=None)` creates UCI representations for moves. - Unlike `Move.uci()` it can convert them in the context of the current - position. - - `Board.has_chess960_castling_rights()` has been added to test for castling - rights that are impossible in standard chess. - - The modules `chess.polyglot`, `chess.pgn` and `chess.uci` will transparently - handle both modes. - -* In a previous release `Board.fen()` has been changed to only display an - en passant square if a legal en passant move is indeed possible. This has - now also been adapted for `Board.shredder_fen()` and `Board.epd()`. - -New features: - -* Get individual FEN components: `Board.board_fen()`, `Board.castling_xfen()`, - `Board.castling_shredder_fen()`. - -* Use `Board.has_legal_en_passant()` to test if a position has a legal - en passant move. - -* Make `repr(board.legal_moves)` human readable. - -New in v0.10.1 --------------- +New in v1.3.1 (6th Dec 2020) +---------------------------- Bugfixes: -* Fix use-after-free in Gaviota tablebase initialization. +* ``chess.pgn.read_game()`` now properly detects variant games with Chess960 + castling rights (as well as mislabeled Standard Chess960 games). Previously, + all castling moves in such games were rejected. -New in v0.10.0 --------------- - -New dependencies: - -* If you are using Python < 3.2 you have to install `futures` in order to - use the `chess.uci` module. +New in v1.3.0 (6th Nov 2020) +---------------------------- Changes: -* There are big changes in the UCI module. Most notably in async mode multiple - commands can be executed at the same time (e.g. `go infinite` and then - `stop` or `go ponder` and then `ponderhit`). - - `go infinite` and `go ponder` will now wait for a result, i.e. you may have - to call `stop` or `ponderhit` from a different thread or run the commands - asynchronously. - - `stop` and `ponderhit` no longer have a result. - -* The values of the color constants `chess.WHITE` and `chess.BLACK` have been - changed. Previously `WHITE` was `0`, `BLACK` was `1`. Now `WHITE` is `True`, - `BLACK` is `False`. The recommended way to invert `color` is using - `not color`. - -* The pseudo piece type `chess.NONE` has been removed in favor of just using - `None`. - -* Changed the `Board(fen)` constructor. If the optional `fen` argument is not - given behavior did not change. However if `None` is passed explicitly an - empty board is created. Previously the starting position would have been - set up. - -* `Board.fen()` will now only show completely legal en passant squares. - -* `Board.set_piece_at()` and `Board.remove_piece_at()` will now clear the - move stack, because the old moves may not be valid in the changed position. - -* `Board.parse_uci()` and `Board.push_uci()` will now accept null moves. - -* Changed shebangs from `#!/usr/bin/python` to `#!/usr/bin/env python` for - better virtualenv support. +* Introduced ``chess.pgn.ChildNode``, a subclass of ``chess.pgn.GameNode`` + for all nodes other than the root node, and converted ``chess.pgn.GameNode`` + to an abstract base class. This improves ergonomics in typed code. -* Removed unused game data files from repository. + The change is backwards compatible if using only documented features. + However, a notable undocumented feature is the ability to create dangling + nodes. This is no longer possible. If you have been using this for + subclassing, override ``GameNode.add_variation()`` instead of + ``GameNode.dangling_node()``. It is now the only method that creates child + nodes. Bugfixes: -* PGN: Prefer the game result from the game termination marker over `*` in the - header. These should be identical in standard compliant PGNs. Thanks to - Skyler Dawson for reporting this. - -* Polyglot: `minimum_weight` for `find()`, `find_all()` and `choice()` was - not respected. - -* Polyglot: Negative indexing of opening books was raising `IndexError`. - -* Various documentation fixes and improvements. +* Removed broken ``weakref``-based caching in ``chess.pgn.GameNode.board()``. New features: -* Experimental probing of Gaviota tablebases via libgtb. - -* New methods to construct boards: - - .. code:: python - - >>> chess.Board.empty() - Board('8/8/8/8/8/8/8/8 w - - 0 1') - - >>> board, ops = chess.Board.from_epd("4k3/8/8/8/8/8/8/4K3 b - - fmvn 17; hmvc 13") - >>> board - Board('4k3/8/8/8/8/8/8/4K3 b - - 13 17') - >>> ops - {'fmvn': 17, 'hmvc': 13} - -* Added `Board.copy()` and hooks to let the copy module to the right thing. +* Added ``chess.pgn.GameNode.next()``. -* Added `Board.has_castling_rights(color)`, - `Board.has_kingside_castling_rights(color)` and - `Board.has_queenside_castling_rights(color)`. - -* Added `Board.clear_stack()`. - -* Support common set operations on `chess.SquareSet()`. - -New in v0.9.1 -------------- - -Bugfixes: - -* UCI module could not handle castling ponder moves. Thanks to Marco Belli for - reporting. -* The initial move number in PGNs was missing, if black was to move in the - starting position. Thanks to Jürgen Précour for reporting. -* Detect more impossible en passant squares in `Board.status()`. There already - was a requirement for a pawn on the fifth rank. Now the sixth and seventh - rank must be empty, additionally. We do not do further retrograde analysis, - because these are the only cases affecting move generation. - -New in v0.8.3 -------------- +New in v1.2.2 (29th Oct 2020) +----------------------------- Bugfixes: -* The initial move number in PGNs was missing, if black was to move in the - starting position. Thanks to Jürgen Précour for reporting. -* Detect more impossible en passant squares in `Board.status()`. There already - was a requirement for a pawn on the fifth rank. Now the sixth and seventh - rank must be empty, additionally. We do not do further retrograde analysis, - because these are the only cases affecting move generation. - -New in v0.9.0 -------------- - -**This is a big update with quite a few breaking changes. Carefully review -the changes before upgrading. It's no problem if you can not update right now. -The 0.8.x branch still gets bugfixes.** - -Incompatible changes: - -* Removed castling right constants. Castling rights are now represented as a - bitmask of the rook square. For example: - - .. code:: python - - >>> board = chess.Board() - - >>> # Standard castling rights. - >>> board.castling_rights == chess.BB_A1 | chess.BB_H1 | chess.BB_A8 | chess.BB_H8 - True - - >>> # Check for the presence of a specific castling right. - >>> can_white_castle_queenside = chess.BB_A1 & board.castling_rights - - Castling moves were previously encoded as the corresponding king movement in - UCI, e.g. `e1f1` for white kingside castling. **Now castling moves are - encoded as a move to the corresponding rook square** (`UCI_Chess960`-style), - e.g. `e1a1`. - - You may use the new methods `Board.uci(move, chess960=True)`, - `Board.parse_uci(uci)` and `Board.push_uci(uci)` to handle this - transparently. - - The `uci` module takes care of converting moves when communicating with an - engine that is not in `UCI_Chess960` mode. - -* The `get_entries_for_position(board)` method of polyglot opening book readers - has been changed to `find_all(board, minimum_weight=1)`. By default entries - with weight 0 are excluded. - -* The `Board.pieces` lookup list has been removed. - -* In 0.8.1 the spelling of repetition (was repitition) was fixed. - `can_claim_threefold_repetition()` and `is_fivefold_repetition()` are the - affected method names. Aliases are now removed. - -* `Board.set_epd()` will now interpret `bm`, `am` as a list of moves for the - current position and `pv` as a variation (represented by a list of moves). - Thanks to Jordan Bray for reporting this. - -* Removed `uci.InfoHandler.pre_bestmove()` and - `uci.InfoHandler.post_bestmove()`. +* Fixed regression where releases were uploaded without the ``py.typed`` + marker. -* `uci.InfoHandler().info["score"]` is now relative to multipv. Use +New in v1.2.1 (26th Oct 2020) +----------------------------- - .. code:: python - - >>> with info_handler as info: - ... if 1 in info["score"]: - ... cp = info["score"][1].cp - - where you were previously using - - .. code:: python - - >>> with info_handler as info: - ... if "score" in info: - ... cp = info["score"].cp +Changes: -* Clear `uci.InfoHandler()` dictionary at the start of new searches - (new `on_go()`), not at the end of searches. +* The primary location for the published package is now + https://pypi.org/project/chess/. Thanks to + `Kristian Glass `_ for transferring the + namespace. -* Renamed `PseudoLegalMoveGenerator.bitboard` and `LegalMoveGenerator.bitboard` - to `PseudoLegalMoveGenerator.board` and `LegalMoveGenerator.board`, - respectively. + The old https://pypi.org/project/python-chess/ will remain an alias that + installs the package from the new location as a dependency (as recommended by + `PEP423 `_). -* Scripts removed. + ``ModuleNotFoundError: No module named 'chess'`` after upgrading from + previous versions? Run ``pip install --force-reinstall chess`` + (due to https://github.com/niklasf/python-chess/issues/680). -* Python 3.2 compability dropped. Use Python 3.3 or higher. Python 2.7 support - is not affected. +New in v1.2.0 (22nd Oct 2020) +----------------------------- New features: -* **Introduced Chess960 support.** `Board(fen)` and `Board.set_fen(fen)` now - support X-FENs. Added `Board.shredder_fen()`. - `Board.status(allow_chess960=True)` has an optional argument allowing to - insist on standard chess castling rules. - Added `Board.is_valid(allow_chess960=True)`. - -* **Improved move generation using** `Shatranj-style direct lookup - `_. **Removed rotated bitboards. Perft - speed has been more than doubled.** - -* Added `choice(board)` and `weighted_choice(board)` for polyglot opening book - readers. - -* Added `Board.attacks(square)` to determine attacks *from* a given square. - There already was `Board.attackers(color, square)` returning attacks *to* - a square. +* Added ``chess.Board.ply()``. +* Added ``chess.pgn.GameNode.ply()`` and ``chess.pgn.GameNode.turn()``. +* Added ``chess.engine.PovWdl``, ``chess.engine.Wdl``, and conversions from + scores: ``chess.engine.PovScore.wdl()``, ``chess.engine.Score.wdl()``. +* Added ``chess.engine.Score.score(*, mate_score: int) -> int`` overload. -* Added `Board.is_en_passant(move)`, `Board.is_capture(move)` and - `Board.is_castling(move)`. +Changes: -* Added `Board.pin(color, square)` and `Board.is_pinned(color, square)`. +* The ``PovScore`` returned by ``chess.pgn.GameNode.eval()`` is now always + relative to the side to move. The ambiguity around ``[%eval #0]`` has been + resolved to ``Mate(-0)``. This makes sense, given that the authors of the + specification probably had standard chess in mind (where a game-ending move + is always a loss for the opponent). Previously, this would be parsed as + ``None``. +* Typed ``chess.engine.InfoDict["wdl"]`` as the new ``chess.engine.PovWdl``, + rather than ``Tuple[int, int, int]``. The new type is backwards compatible, + but it is recommended to use its documented fields and methods instead. +* Removed ``chess.engine.PovScore.__str__()``. String representation falls back + to ``__repr__``. +* The ``en_passant`` parameter of ``chess.Board.fen()`` and + ``chess.Board.epd()`` is now typed as ``Literal["legal", "fen", "xfen"]`` + rather than ``str``. + +New in v1.1.0 (4th Oct 2020) +---------------------------- -* There is a new method `Board.pieces(piece_type, color)` to get a set of - squares with the specified pieces. +New features: -* Do expensive Syzygy table initialization on demand. +* Added ``chess.svg.board(..., orientation)``. This is a more idiomatic way to + set the board orientation than ``flipped``. +* Added ``chess.svg.Arrow.pgn()`` and ``chess.svg.Arrow.from_pgn()``. -* Allow promotions like `e8Q` (usually `e8=Q`) in `Board.parse_san()` and - PGN files. +Changes: -* Patch by Richard C. Gerkin: Added `Board.__unicode__()` just like - `Board.__str__()` but with unicode pieces. -* Patch by Richard C. Gerkin: Added `Board.__html__()`. +* Further relaxed ``chess.Board.parse_san()``. Now accepts fully specified moves + like ``e2e4``, even if that is not a pawn move, castling notation with zeros, + null moves in UCI notation, and null moves in XBoard notation. -New in v0.8.2 -------------- +New in v1.0.1 (24th Sep 2020) +----------------------------- Bugfixes: -* `pgn.Game.setup()` with the standard starting position was failing when the - standard starting position was already set. Thanks to Jordan Bray for - reporting this. - -Optimizations: - -* Remove `bswap()` from Syzygy decompression hot path. Directly read integers - with the correct endianness. - -New in v0.8.1 -------------- - -* Fixed pondering mode in uci module. For example `ponderhit()` was blocking - indefinitely. Thanks to Valeriy Huz for reporting this. - -* Patch by Richard C. Gerkin: Moved searchmoves to the end of the UCI go - command, where it will not cause other command parameters to be ignored. - -* Added missing check or checkmate suffix to castling SANs, e.g. `O-O-O#`. - -* Fixed off-by-one error in polyglot opening book binary search. This would - not have caused problems for real opening books. - -* Fixed Python 3 support for reverse polyglot opening book iteration. - -* Bestmoves may be literally `(none)` in UCI protocol, for example in - checkmate positions. Fix parser and return `None` as the bestmove in this - case. - -* Fixed spelling of repetition (was repitition). - `can_claim_threefold_repetition()` and `is_fivefold_repetition()` are the - affected method names. Aliases are there for now, but will be removed in the - next release. Thanks to Jimmy Patrick for reporting this. - -* Added `SquareSet.__reversed__()`. - -* Use containerized tests on Travis CI, test against Stockfish 6, improved - test coverage amd various minor clean-ups. - -New in v0.8.0 -------------- - -* **Implement Syzygy endgame tablebase probing.** - `https://syzygy-tables.info `_ - is an example project that provides a public API using the new features. - -* The interface for aynchronous UCI command has changed to mimic - `concurrent.futures`. `is_done()` is now just `done()`. Callbacks will - receive the command object as a single argument instead of the result. - The `result` property and `wait()` have been removed in favor of a - synchronously waiting `result()` method. - -* The result of the `stop` and `go` UCI commands are now named tuples (instead - of just normal tuples). - -* Add alias `Board` for `Bitboard`. - -* Fixed race condition during UCI engine startup. Lines received during engine - startup sometimes needed to be processed before the Engine object was fully - initialized. - -New in v0.7.0 -------------- - -* **Implement UCI engine communication.** - -* Patch by Matthew Lai: `Add caching for gameNode.board()`. - -New in v0.6.0 -------------- - -* If there are comments in a game before the first move, these are now assigned - to `Game.comment` instead of `Game.starting_comment`. `Game.starting_comment` - is ignored from now on. `Game.starts_variation()` is no longer true. - The first child node of a game can no longer have a starting comment. - It is possible to have a game with `Game.comment` set, that is otherwise - completely empty. - -* Fix export of games with variations. Previously the moves were exported in - an unusual (i.e. wrong) order. - -* Install `gmpy2` or `gmpy` if you want to use slightly faster binary - operations. - -* Ignore superfluous variation opening brackets in PGN files. - -* Add `GameNode.san()`. - -* Remove `sparse_pop_count()`. Just use `pop_count()`. - -* Remove `next_bit()`. Now use `bit_scan()`. - -New in v0.5.0 -------------- - -* PGN parsing is now more robust: `read_game()` ignores invalid tokens. - Still exceptions are going to be thrown on illegal or ambiguous moves, but - this behaviour can be changed by passing an `error_handler` argument. - - .. code:: python - - >>> # Raises ValueError: - >>> game = chess.pgn.read_game(file_with_illegal_moves) - - .. code:: python - - >>> # Silently ignores errors and continues parsing: - >>> game = chess.pgn.read_game(file_with_illegal_moves, None) - - .. code:: python - - >>> # Logs the error, continues parsing: - >>> game = chess.pgn.read_game(file_with_illegal_moves, logger.exception) - - If there are too many closing brackets this is now ignored. - - Castling moves like 0-0 (with zeros) are now accepted in PGNs. - The `Bitboard.parse_san()` method remains strict as always, though. - - Previously the parser was strictly following the PGN spefification in that - empty lines terminate a game. So a game like - - :: - - [Event "?"] - - { Starting comment block } - - 1. e4 e5 2. Nf3 Nf6 * - - would have ended directly after the starting comment. To avoid this, the - parser will now look ahead until it finds at least one move or a termination - marker like `*`, `1-0`, `1/2-1/2` or `0-1`. - -* Introduce a new function `scan_headers()` to quickly scan a PGN file for - headers without having to parse the full games. - -* Minor testcoverage improvements. - -New in v0.4.2 -------------- - -* Fix bug where `pawn_moves_from()` and consequently `is_legal()` weren't - handling en passant correctly. Thanks to Norbert Naskov for reporting. - -New in v0.4.1 -------------- - -* Fix `is_fivefold_repitition()`: The new fivefold repetition rule requires - the repetitions to occur on *alternating consecutive* moves. - -* Minor testing related improvements: Close PGN files, allow running via - setuptools. - -* Add recently introduced features to README. - -New in v0.4.0 -------------- - -* Introduce `can_claim_draw()`, `can_claim_fifty_moves()` and - `can_claim_threefold_repitition()`. - -* Since the first of July 2014 a game is also over (even without claim by one - of the players) if there were 75 moves without a pawn move or capture or - a fivefold repetition. Let `is_game_over()` respect that. Introduce - `is_seventyfive_moves()` and `is_fivefold_repitition()`. Other means of - ending a game take precedence. - -* Threefold repetition checking requires efficient hashing of positions - to build the table. So performance improvements were needed there. The - default polyglot compatible zobrist hashes are now built incrementally. - -* Fix low level rotation operations `l90()`, `l45()` and `r45()`. There was - no problem in core because correct versions of the functions were inlined. - -* Fix equality and inequality operators for `Bitboard`, `Move` and `Piece`. - Also make them robust against comparisons with incompatible types. - -* Provide equality and inequality operators for `SquareSet` and - `polyglot.Entry`. - -* Fix return values of incremental arithmetical operations for `SquareSet`. - -* Make `polyglot.Entry` a `collections.namedtuple`. - -* Determine and improve test coverage. - -* Minor coding style fixes. - -New in v0.3.1 -------------- - -* `Bitboard.status()` now correctly detects `STATUS_INVALID_EP_SQUARE`, - instead of errors or false reports. - -* Polyglot opening book reader now correctly handles knight underpromotions. - -* Minor coding style fixes, including removal of unused imports. - -New in v0.3.0 -------------- - -* Rename property `half_moves` of `Bitboard` to `halfmove_clock`. - -* Rename property `ply` of `Bitboard` to `fullmove_number`. - -* Let PGN parser handle symbols like `!`, `?`, `!?` and so on by converting - them to NAGs. - -* Add a human readable string representation for Bitboards. - - .. code:: python - - >>> print(chess.Bitboard()) - r n b q k b n r - p p p p p p p p - . . . . . . . . - . . . . . . . . - . . . . . . . . - . . . . . . . . - P P P P P P P P - R N B Q K B N R - -* Various documentation improvements. - -New in v0.2.0 -------------- - -* **Implement PGN parsing and writing.** -* Hugely improve test coverage and use Travis CI for continuous integration and - testing. -* Create an API documentation. -* Improve Polyglot opening-book handling. - -New in v0.1.0 -------------- - -Apply the lessons learned from the previous releases, redesign the API and -implement it in pure Python. - -New in v0.0.4 -------------- - -Implement the basics in C++ and provide bindings for Python. Obviously -performance was a lot better - but at the expense of having to compile -code for the target platform. +* ``chess.svg``: Restored SVG Tiny compatibility by splitting colors like + ``#rrggbbaa`` into a solid color and opacity. -Pre v0.0.4 ----------- +New in v1.0.0 (24th Sep 2020) +----------------------------- -First experiments with a way too slow pure Python API, creating way too many -objects for basic operations. +See ``CHANGELOG-OLD.rst`` for changes up to v1.0.0. diff --git a/MANIFEST.in b/MANIFEST.in index 0265f5ea9..77ac05205 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,2 @@ -include README.rst include CHANGELOG.rst include LICENSE.txt diff --git a/README.rst b/README.rst index 713c568ef..d4d51a128 100644 --- a/README.rst +++ b/README.rst @@ -1,23 +1,28 @@ -python-chess: a pure Python chess library -========================================= +python-chess: a chess library for Python +======================================== -.. image:: https://travis-ci.org/niklasf/python-chess.svg?branch=master - :target: https://travis-ci.org/niklasf/python-chess +.. image:: https://github.com/niklasf/python-chess/workflows/Test/badge.svg + :target: https://github.com/niklasf/python-chess/actions + :alt: Test status -.. image:: https://coveralls.io/repos/github/niklasf/python-chess/badge.svg?branch=master - :target: https://coveralls.io/github/niklasf/python-chess?branch=master - -.. image:: https://badge.fury.io/py/python-chess.svg - :target: https://pypi.python.org/pypi/python-chess +.. image:: https://badge.fury.io/py/chess.svg + :target: https://pypi.python.org/pypi/chess + :alt: PyPI package .. image:: https://readthedocs.org/projects/python-chess/badge/?version=latest :target: https://python-chess.readthedocs.io/en/latest/ + :alt: Docs + +.. image:: https://badges.gitter.im/python-chess/community.svg + :target: https://gitter.im/python-chess/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge + :alt: Chat on Gitter Introduction ------------ -python-chess is a pure Python chess library with move generation and validation -and handling of common formats. This is the scholars mate in python-chess: +python-chess is a chess library for Python, with move generation, +move validation, and support for common formats. This is the Scholar's mate in +python-chess: .. code:: python @@ -51,24 +56,34 @@ and handling of common formats. This is the scholars mate in python-chess: >>> board Board('r1bqkb1r/pppp1Qpp/2n2n2/4p3/2B1P3/8/PPPP1PPP/RNB1K1NR b KQkq - 0 4') +Installing +---------- + +Requires Python 3.8+. Download and install the latest release: + +:: + + pip install chess + + `Documentation `__ -------------------------------------------------------------------- +-------------------------------------------------------------------- * `Core `_ * `PGN parsing and writing `_ * `Polyglot opening book reading `_ * `Gaviota endgame tablebase probing `_ * `Syzygy endgame tablebase probing `_ -* `UCI engine communication `_ +* `UCI/XBoard engine communication `_ * `Variants `_ * `Changelog `_ Features -------- -* Supports Python 2.6+, Python 3.3+ and PyPy. +* Includes mypy typings. -* IPython notebook integration. +* IPython/Jupyter Notebook integration. `SVG rendering docs `_. .. code:: python @@ -76,6 +91,7 @@ Features >>> board # doctest: +SKIP .. image:: https://backscattering.de/web-boardimage/board.png?fen=r1bqkb1r/pppp1Qpp/2n2n2/4p3/2B1P3/8/PPPP1PPP/RNB1K1NR&lastmove=h5f7&check=e8 + :alt: r1bqkb1r/pppp1Qpp/2n2n2/4p3/2B1P3/8/PPPP1PPP/RNB1K1NR * Chess variants: Standard, Chess960, Suicide, Giveaway, Atomic, King of the Hill, Racing Kings, Horde, Three-check, Crazyhouse. @@ -114,10 +130,10 @@ Features False >>> board.is_insufficient_material() False - >>> board.is_game_over() - True + >>> board.outcome() + Outcome(termination=, winner=True) -* Detects repetitions. Has a half move clock. +* Detects repetitions. Has a half-move clock. .. code:: python @@ -130,9 +146,9 @@ Features >>> board.can_claim_draw() False - With the new rules from July 2014 a game ends drawn (even without a claim) - once a fivefold repetition occurs or if there are 75 moves without a pawn - push or capture. Other ways of ending a game take precedence. + With the new rules from July 2014, a game ends as a draw (even without a + claim) once a fivefold repetition occurs or if there are 75 moves without + a pawn push or capture. Other ways of ending a game take precedence. .. code:: python @@ -152,7 +168,7 @@ Features >>> attackers = board.attackers(chess.WHITE, chess.F3) >>> attackers - SquareSet(0x0000000000004040) + SquareSet(0x0000_0000_0000_4040) >>> chess.G2 in attackers True >>> print(attackers) @@ -201,9 +217,9 @@ Features >>> ops == {'bm': [chess.Move.from_uci('d6d1')], 'id': 'BK.01'} True -* Detects absolute `pins and their directions `_. +* Detects `absolute pins and their directions `_. -* Read Polyglot opening books. +* Reads Polyglot opening books. `Docs `__. .. code:: python @@ -214,16 +230,14 @@ Features >>> board = chess.Board() >>> main_entry = book.find(board) - >>> main_entry.move() + >>> main_entry.move Move.from_uci('e2e4') >>> main_entry.weight 1 - >>> main_entry.learn - 0 >>> book.close() -* Read and write PGNs. Supports headers, comments, NAGs and a tree of +* Reads and writes PGNs. Supports headers, comments, NAGs and a tree of variations. `Docs `__. @@ -239,27 +253,8 @@ Features >>> first_game.headers["Black"] 'Bordais' - >>> # Get the mainline as a list of moves. - >>> moves = first_game.main_line() - >>> first_game.board().variation_san(moves) - '1. e4 c5 2. c4 Nc6 3. Ne2 Nf6 4. Nbc3 Nb4 5. g3 Nd3#' - - >>> # Iterate through the mainline of this embarrasingly short game. - >>> node = first_game - >>> while not node.is_end(): - ... next_node = node.variations[0] - ... print(node.board().san(next_node.move)) - ... node = next_node - e4 - c5 - c4 - Nc6 - Ne2 - Nf6 - Nbc3 - Nb4 - g3 - Nd3# + >>> first_game.mainline() # doctest: +ELLIPSIS + >>> first_game.headers["Result"] '0-1' @@ -274,78 +269,95 @@ Features >>> import chess.syzygy - >>> tablebases = chess.syzygy.open_tablebases("data/syzygy/regular") + >>> tablebase = chess.syzygy.open_tablebase("data/syzygy/regular") >>> # Black to move is losing in 53 half moves (distance to zero) in this >>> # KNBvK endgame. >>> board = chess.Board("8/2K5/4B3/3N4/8/8/4k3/8 b - - 0 1") - >>> tablebases.probe_dtz(board) + >>> tablebase.probe_dtz(board) -53 - >>> tablebases.close() + >>> tablebase.close() -* Communicate with an UCI engine. - `Docs `__. +* Communicate with UCI/XBoard engines. Based on ``asyncio``. + `Docs `__. .. code:: python - >>> import chess.uci + >>> import chess.engine - >>> engine = chess.uci.popen_engine("stockfish") - >>> engine.uci() - >>> engine.author # doctest: +SKIP - 'Tord Romstad, Marco Costalba and Joona Kiiski' + >>> engine = chess.engine.SimpleEngine.popen_uci("stockfish") - >>> # Synchronous mode. >>> board = chess.Board("1k1r4/pp1b1R2/3q2pp/4p3/2B5/4Q3/PPP2B2/2K5 b - - 0 1") - >>> engine.position(board) - >>> engine.go(movetime=2000) # Gets tuple of bestmove and ponder move. - BestMove(bestmove=Move.from_uci('d6d1'), ponder=Move.from_uci('c1d1')) - - >>> # Asynchronous mode. - >>> def callback(command): - ... bestmove, ponder = command.result() - ... assert bestmove == chess.Move.from_uci('d6d1') - ... - >>> command = engine.go(movetime=2000, async_callback=callback) - >>> command.done() - False - >>> command.result() - BestMove(bestmove=Move.from_uci('d6d1'), ponder=Move.from_uci('c1d1')) - >>> command.done() - True + >>> limit = chess.engine.Limit(time=2.0) + >>> engine.play(board, limit) # doctest: +ELLIPSIS + - >>> # Quit. >>> engine.quit() - 0 - -Installing ----------- - -Download and install the latest release: - -:: - - pip install python-chess[engine,gaviota] - -Selected use cases ------------------- - -If you like, let me know if you are creating something intresting with -python-chess, for example: - -* a stand alone chess computer based on DGT board - http://www.picochess.org/ -* a website to probe Syzygy endgame tablebases - https://syzygy-tables.info/ -* a GUI to play against UCI chess engines - http://johncheetham.com/projects/jcchess/ -* an HTTP microservice to render board images - https://github.com/niklasf/web-boardimage -* a bot to play chess on Telegram - https://github.com/cxjdavin/tgchessbot -* a tool to build `Anki `_ decks from a PGN opening repertoire - https://github.com/asdfjkl/pgn2anki -Acknowledgements ----------------- +Selected projects +----------------- + +If you like, share interesting things you are using python-chess for, for example: + ++------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------------+ +| .. image:: https://github.com/niklasf/python-chess/blob/master/docs/images/syzygy.png?raw=true | https://syzygy-tables.info/ | +| :height: 64 | | +| :width: 64 | | +| :target: https://syzygy-tables.info/ | A website to probe Syzygy endgame tablebases | ++------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------------+ +| .. image:: https://github.com/niklasf/python-chess/blob/master/docs/images/maia.png?raw=true | https://maiachess.com/ | +| :height: 64 | | +| :width: 64 | | +| :target: https://maiachess.com/ | A human-like neural network chess engine | ++------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------------+ +| .. image:: https://github.com/niklasf/python-chess/blob/master/docs/images/clente-chess.png?raw=true | `clente/chess `_ | +| :height: 64 | | +| :width: 64 | | +| :target: https://github.com/clente/chess | Opinionated wrapper to use python-chess from the R programming language | ++------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------------+ +| .. image:: https://github.com/niklasf/python-chess/blob/master/docs/images/crazyara.png?raw=true | https://crazyara.org/ | +| :height: 64 | | +| :width: 64 | | +| :target: https://crazyara.org/ | Deep learning for Crazyhouse | ++------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------------+ +| .. image:: https://github.com/niklasf/python-chess/blob/master/docs/images/jcchess.png?raw=true | `http://johncheetham.com `_ | +| :height: 64 | | +| :width: 64 | | +| :target: http://johncheetham.com/projects/jcchess/ | A GUI to play against UCI chess engines | ++------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------------+ +| .. image:: https://github.com/niklasf/python-chess/blob/master/docs/images/pettingzoo.png?raw=true | `https://pettingzoo.farama.org `_ | +| :width: 64 | | +| :height: 64 | | +| :target: https://pettingzoo.farama.org/environments/classic/chess/ | A multi-agent reinforcement learning environment | ++------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------------+ +| .. image:: https://github.com/niklasf/python-chess/blob/master/docs/images/cli-chess.png?raw=true | `cli-chess `_ | +| :width: 64 | | +| :height: 64 | | +| :target: https://github.com/trevorbayless/cli-chess | A highly customizable way to play chess in your terminal | ++------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------------+ + +* extensions to build engines (search and evaluation) – https://github.com/Mk-Chan/python-chess-engine-extensions +* a stand-alone chess computer based on DGT board – https://picochess.com/ +* a bridge between Lichess API and chess engines – https://github.com/lichess-bot-devs/lichess-bot +* a command-line PGN annotator – https://github.com/rpdelaney/python-chess-annotator +* an HTTP microservice to render board images – https://github.com/niklasf/web-boardimage +* building a toy chess engine with alpha-beta pruning, piece-square tables, and move ordering – https://healeycodes.com/building-my-own-chess-engine/ +* a JIT compiled chess engine – https://github.com/SamRagusa/Batch-First +* teaching Cognitive Science – `https://jupyter.brynmawr.edu `_ +* an `Alexa skill to play blindfold chess `_ – https://github.com/laynr/blindfold-chess +* a chessboard widget for PySide2 – https://github.com/H-a-y-k/hichesslib +* Django Rest Framework API for multiplayer chess – https://github.com/WorkShoft/capablanca-api +* a `browser based PGN viewer `_ written in PyScript – https://github.com/nmstoker/ChessMatchViewer +* an accessible chessboard that allows blind and visually impaired players to play chess against Stockfish – https://github.com/blindpandas/chessmart +* a web-based chess vision exercise – https://github.com/3d12/rookognition + + +Prior art +--------- Thanks to the Stockfish authors and thanks to Sam Tannous for publishing his -approach to `avoid rotated bitboards with direct lookup (pdf) `_ +approach to `avoid rotated bitboards with direct lookup (PDF) `_ alongside his GPL2+ engine `Shatranj `_. Some move generation ideas are taken from these sources. @@ -353,12 +365,11 @@ Thanks to Ronald de Man for his `Syzygy endgame tablebases `_. The probing code in python-chess is very directly ported from his C probing code. -Thanks to Miguel A. Ballicora for his -`Gaviota tablebases `_. -(I wish the generating code was free software). +Thanks to `Kristian Glass `_ for +transferring the namespace ``chess`` on PyPI. License ------- python-chess is licensed under the GPL 3 (or any later version at your option). -Check out LICENSE.txt for the full text. +Check out ``LICENSE.txt`` for the full text. diff --git a/chess/__init__.py b/chess/__init__.py index 62a2a6722..9ea44f36e 100644 --- a/chess/__init__.py +++ b/chess/__init__.py @@ -1,264 +1,545 @@ -# -*- coding: utf-8 -*- -# -# This file is part of the python-chess library. -# Copyright (C) 2012-2017 Niklas Fiekas -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - """ -A pure Python chess library with move generation and validation, Polyglot -opening book probing, PGN reading and writing, Gaviota tablebase probing, -Syzygy tablebase probing and XBoard/UCI engine communication. +A chess library with move generation and validation, +Polyglot opening book probing, PGN reading and writing, +Gaviota tablebase probing, +Syzygy tablebase probing, and XBoard/UCI engine communication. """ +from __future__ import annotations + __author__ = "Niklas Fiekas" __email__ = "niklas.fiekas@backscattering.de" -__version__ = "0.19.0" +__version__ = "1.11.2" +import collections import copy +import dataclasses +import enum +import math import re import itertools +import typing + +from typing import ClassVar, Callable, Counter, Dict, Hashable, Iterable, Iterator, List, Literal, Mapping, Optional, SupportsInt, Tuple, Type, TypeVar, Union + +if typing.TYPE_CHECKING: + from typing_extensions import Self, TypeAlias + + +EnPassantSpec = Literal["legal", "fen", "xfen"] + + +Color: TypeAlias = bool +WHITE: Color = True +BLACK: Color = False +COLORS: List[Color] = [WHITE, BLACK] +ColorName = Literal["white", "black"] +COLOR_NAMES: List[ColorName] = ["black", "white"] -try: - import backport_collections as collections -except ImportError: - import collections +PieceType: TypeAlias = int +PAWN: PieceType = 1 +KNIGHT: PieceType = 2 +BISHOP: PieceType = 3 +ROOK: PieceType = 4 +QUEEN: PieceType = 5 +KING: PieceType = 6 +PIECE_TYPES: List[PieceType] = [PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING] +PIECE_SYMBOLS = [None, "p", "n", "b", "r", "q", "k"] +PIECE_NAMES = [None, "pawn", "knight", "bishop", "rook", "queen", "king"] -COLORS = [WHITE, BLACK] = [True, False] -COLOR_NAMES = ["black", "white"] +def piece_symbol(piece_type: PieceType) -> str: + return typing.cast(str, PIECE_SYMBOLS[piece_type]) -PIECE_TYPES = [PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING] = range(1, 7) -PIECE_SYMBOLS = ["", "p", "n", "b", "r", "q", "k"] -PIECE_NAMES = ["", "pawn", "knight", "bishop", "rook", "queen", "king"] +def piece_name(piece_type: PieceType) -> str: + return typing.cast(str, PIECE_NAMES[piece_type]) UNICODE_PIECE_SYMBOLS = { - "R": u"♖", "r": u"♜", - "N": u"♘", "n": u"♞", - "B": u"♗", "b": u"♝", - "Q": u"♕", "q": u"♛", - "K": u"♔", "k": u"♚", - "P": u"♙", "p": u"♟", + "R": "♖", "r": "♜", + "N": "♘", "n": "♞", + "B": "♗", "b": "♝", + "Q": "♕", "q": "♛", + "K": "♔", "k": "♚", + "P": "♙", "p": "♟", } +File: TypeAlias = int +FILE_A: File = 0 +FILE_B: File = 1 +FILE_C: File = 2 +FILE_D: File = 3 +FILE_E: File = 4 +FILE_F: File = 5 +FILE_G: File = 6 +FILE_H: File = 7 +FILES = [FILE_A, FILE_B, FILE_C, FILE_D, FILE_E, FILE_F, FILE_G, FILE_H] FILE_NAMES = ["a", "b", "c", "d", "e", "f", "g", "h"] +Rank: TypeAlias = int +RANK_1: Rank = 0 +RANK_2: Rank = 1 +RANK_3: Rank = 2 +RANK_4: Rank = 3 +RANK_5: Rank = 4 +RANK_6: Rank = 5 +RANK_7: Rank = 6 +RANK_8: Rank = 7 +RANKS = [RANK_1, RANK_2, RANK_3, RANK_4, RANK_5, RANK_6, RANK_7, RANK_8] RANK_NAMES = ["1", "2", "3", "4", "5", "6", "7", "8"] STARTING_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" -"""The FEN of the standard chess starting position.""" +"""The FEN for the standard chess starting position.""" STARTING_BOARD_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR" -"""The board part of the FEN of the standard chess starting position.""" - -STATUS_VALID = 0 -STATUS_NO_WHITE_KING = 1 -STATUS_NO_BLACK_KING = 2 -STATUS_TOO_MANY_KINGS = 4 -STATUS_TOO_MANY_WHITE_PAWNS = 8 -STATUS_TOO_MANY_BLACK_PAWNS = 16 -STATUS_PAWNS_ON_BACKRANK = 32 -STATUS_TOO_MANY_WHITE_PIECES = 64 -STATUS_TOO_MANY_BLACK_PIECES = 128 -STATUS_BAD_CASTLING_RIGHTS = 256 -STATUS_INVALID_EP_SQUARE = 512 -STATUS_OPPOSITE_CHECK = 1024 -STATUS_EMPTY = 2048 -STATUS_RACE_CHECK = 4096 -STATUS_RACE_OVER = 8192 -STATUS_RACE_MATERIAL = 16384 - - -SQUARES = [ - A1, B1, C1, D1, E1, F1, G1, H1, - A2, B2, C2, D2, E2, F2, G2, H2, - A3, B3, C3, D3, E3, F3, G3, H3, - A4, B4, C4, D4, E4, F4, G4, H4, - A5, B5, C5, D5, E5, F5, G5, H5, - A6, B6, C6, D6, E6, F6, G6, H6, - A7, B7, C7, D7, E7, F7, G7, H7, - A8, B8, C8, D8, E8, F8, G8, H8] = range(64) - -def square(file_index, rank_index): +"""The board part of the FEN for the standard chess starting position.""" + + +class Status(enum.IntFlag): + VALID = 0 + NO_WHITE_KING = 1 << 0 + NO_BLACK_KING = 1 << 1 + TOO_MANY_KINGS = 1 << 2 + TOO_MANY_WHITE_PAWNS = 1 << 3 + TOO_MANY_BLACK_PAWNS = 1 << 4 + PAWNS_ON_BACKRANK = 1 << 5 + TOO_MANY_WHITE_PIECES = 1 << 6 + TOO_MANY_BLACK_PIECES = 1 << 7 + BAD_CASTLING_RIGHTS = 1 << 8 + INVALID_EP_SQUARE = 1 << 9 + OPPOSITE_CHECK = 1 << 10 + EMPTY = 1 << 11 + RACE_CHECK = 1 << 12 + RACE_OVER = 1 << 13 + RACE_MATERIAL = 1 << 14 + TOO_MANY_CHECKERS = 1 << 15 + IMPOSSIBLE_CHECK = 1 << 16 + +STATUS_VALID = Status.VALID +STATUS_NO_WHITE_KING = Status.NO_WHITE_KING +STATUS_NO_BLACK_KING = Status.NO_BLACK_KING +STATUS_TOO_MANY_KINGS = Status.TOO_MANY_KINGS +STATUS_TOO_MANY_WHITE_PAWNS = Status.TOO_MANY_WHITE_PAWNS +STATUS_TOO_MANY_BLACK_PAWNS = Status.TOO_MANY_BLACK_PAWNS +STATUS_PAWNS_ON_BACKRANK = Status.PAWNS_ON_BACKRANK +STATUS_TOO_MANY_WHITE_PIECES = Status.TOO_MANY_WHITE_PIECES +STATUS_TOO_MANY_BLACK_PIECES = Status.TOO_MANY_BLACK_PIECES +STATUS_BAD_CASTLING_RIGHTS = Status.BAD_CASTLING_RIGHTS +STATUS_INVALID_EP_SQUARE = Status.INVALID_EP_SQUARE +STATUS_OPPOSITE_CHECK = Status.OPPOSITE_CHECK +STATUS_EMPTY = Status.EMPTY +STATUS_RACE_CHECK = Status.RACE_CHECK +STATUS_RACE_OVER = Status.RACE_OVER +STATUS_RACE_MATERIAL = Status.RACE_MATERIAL +STATUS_TOO_MANY_CHECKERS = Status.TOO_MANY_CHECKERS +STATUS_IMPOSSIBLE_CHECK = Status.IMPOSSIBLE_CHECK + + +class Termination(enum.Enum): + """Enum with reasons for a game to be over.""" + + CHECKMATE = enum.auto() + """See :func:`chess.Board.is_checkmate()`.""" + STALEMATE = enum.auto() + """See :func:`chess.Board.is_stalemate()`.""" + INSUFFICIENT_MATERIAL = enum.auto() + """See :func:`chess.Board.is_insufficient_material()`.""" + SEVENTYFIVE_MOVES = enum.auto() + """See :func:`chess.Board.is_seventyfive_moves()`.""" + FIVEFOLD_REPETITION = enum.auto() + """See :func:`chess.Board.is_fivefold_repetition()`.""" + FIFTY_MOVES = enum.auto() + """See :func:`chess.Board.can_claim_fifty_moves()`.""" + THREEFOLD_REPETITION = enum.auto() + """See :func:`chess.Board.can_claim_threefold_repetition()`.""" + VARIANT_WIN = enum.auto() + """See :func:`chess.Board.is_variant_win()`.""" + VARIANT_LOSS = enum.auto() + """See :func:`chess.Board.is_variant_loss()`.""" + VARIANT_DRAW = enum.auto() + """See :func:`chess.Board.is_variant_draw()`.""" + +@dataclasses.dataclass +class Outcome: + """ + Information about the outcome of an ended game, usually obtained from + :func:`chess.Board.outcome()`. + """ + + termination: Termination + """The reason for the game to have ended.""" + + winner: Optional[Color] + """The winning color or ``None`` if drawn.""" + + def result(self) -> str: + """Returns ``1-0``, ``0-1`` or ``1/2-1/2``.""" + return "1/2-1/2" if self.winner is None else ("1-0" if self.winner else "0-1") + + +class InvalidMoveError(ValueError): + """Raised when move notation is not syntactically valid""" + + +class IllegalMoveError(ValueError): + """Raised when the attempted move is illegal in the current position""" + + +class AmbiguousMoveError(ValueError): + """Raised when the attempted move is ambiguous in the current position""" + + +Square: TypeAlias = int +A1: Square = 0 +B1: Square = 1 +C1: Square = 2 +D1: Square = 3 +E1: Square = 4 +F1: Square = 5 +G1: Square = 6 +H1: Square = 7 +A2: Square = 8 +B2: Square = 9 +C2: Square = 10 +D2: Square = 11 +E2: Square = 12 +F2: Square = 13 +G2: Square = 14 +H2: Square = 15 +A3: Square = 16 +B3: Square = 17 +C3: Square = 18 +D3: Square = 19 +E3: Square = 20 +F3: Square = 21 +G3: Square = 22 +H3: Square = 23 +A4: Square = 24 +B4: Square = 25 +C4: Square = 26 +D4: Square = 27 +E4: Square = 28 +F4: Square = 29 +G4: Square = 30 +H4: Square = 31 +A5: Square = 32 +B5: Square = 33 +C5: Square = 34 +D5: Square = 35 +E5: Square = 36 +F5: Square = 37 +G5: Square = 38 +H5: Square = 39 +A6: Square = 40 +B6: Square = 41 +C6: Square = 42 +D6: Square = 43 +E6: Square = 44 +F6: Square = 45 +G6: Square = 46 +H6: Square = 47 +A7: Square = 48 +B7: Square = 49 +C7: Square = 50 +D7: Square = 51 +E7: Square = 52 +F7: Square = 53 +G7: Square = 54 +H7: Square = 55 +A8: Square = 56 +B8: Square = 57 +C8: Square = 58 +D8: Square = 59 +E8: Square = 60 +F8: Square = 61 +G8: Square = 62 +H8: Square = 63 +SQUARES: List[Square] = list(range(64)) + +SQUARE_NAMES = [f + r for r in RANK_NAMES for f in FILE_NAMES] + +def parse_square(name: str) -> Square: + """ + Gets the square index for the given square *name* + (e.g., ``a1`` returns ``0``). + + :raises: :exc:`ValueError` if the square name is invalid. + """ + return SQUARE_NAMES.index(name) + +def square_name(square: Square) -> str: + """Gets the name of the square, like ``a3``.""" + return SQUARE_NAMES[square] + +def square(file_index: File, rank_index: Rank) -> Square: """Gets a square number by file and rank index.""" return rank_index * 8 + file_index -def square_file(square): - """Gets the file index of the square where ``0`` is the a file.""" +def parse_file(name: str) -> File: + """ + Gets the file index for the given file *name* + (e.g., ``a`` returns ``0``). + + :raises: :exc:`ValueError` if the file name is invalid. + """ + return FILE_NAMES.index(name) + +def file_name(file: File) -> str: + """Gets the name of the file, like ``a``.""" + return FILE_NAMES[file] + +def parse_rank(name: str) -> File: + """ + Gets the rank index for the given rank *name* + (e.g., ``1`` returns ``0``). + + :raises: :exc:`ValueError` if the rank name is invalid. + """ + return FILE_NAMES.index(name) + +def rank_name(rank: Rank) -> str: + """Gets the name of the rank, like ``1``.""" + return FILE_NAMES[rank] + +def square_file(square: Square) -> File: + """Gets the file index of the square where ``0`` is the a-file.""" return square & 7 -def square_rank(square): +def square_rank(square: Square) -> Rank: """Gets the rank index of the square where ``0`` is the first rank.""" return square >> 3 -def square_name(square): - """Gets the name of the square, e.g. ``a3``.""" - return FILE_NAMES[square_file(square)] + RANK_NAMES[square_rank(square)] - -def square_distance(a, b): +def square_distance(a: Square, b: Square) -> int: """ - Gets the distance (i.e. the number of king steps) from square *a* to *b*. + Gets the Chebyshev distance (i.e., the number of king steps) from square *a* to *b*. """ return max(abs(square_file(a) - square_file(b)), abs(square_rank(a) - square_rank(b))) -def square_mirror(square): +def square_manhattan_distance(a: Square, b: Square) -> int: + """ + Gets the Manhattan/Taxicab distance (i.e., the number of orthogonal king steps) from square *a* to *b*. + """ + return abs(square_file(a) - square_file(b)) + abs(square_rank(a) - square_rank(b)) + +def square_knight_distance(a: Square, b: Square) -> int: + """ + Gets the Knight distance (i.e., the number of knight moves) from square *a* to *b*. + """ + dx = abs(square_file(a) - square_file(b)) + dy = abs(square_rank(a) - square_rank(b)) + + if dx + dy == 1: + return 3 + elif dx == dy == 2: + return 4 + elif dx == dy == 1: + if BB_SQUARES[a] & BB_CORNERS or BB_SQUARES[b] & BB_CORNERS: # Special case only for corner squares + return 4 + + m = math.ceil(max(dx / 2, dy / 2, (dx + dy) / 3)) + return m + ((m + dx + dy) % 2) + +def square_mirror(square: Square) -> Square: """Mirrors the square vertically.""" return square ^ 0x38 -SQUARES_180 = [square_mirror(sq) for sq in SQUARES] -SQUARE_NAMES = [square_name(sq) for sq in SQUARES] - - -BB_VOID = 0 -BB_ALL = 0xffffffffffffffff - -BB_SQUARES = [ - BB_A1, BB_B1, BB_C1, BB_D1, BB_E1, BB_F1, BB_G1, BB_H1, - BB_A2, BB_B2, BB_C2, BB_D2, BB_E2, BB_F2, BB_G2, BB_H2, - BB_A3, BB_B3, BB_C3, BB_D3, BB_E3, BB_F3, BB_G3, BB_H3, - BB_A4, BB_B4, BB_C4, BB_D4, BB_E4, BB_F4, BB_G4, BB_H4, - BB_A5, BB_B5, BB_C5, BB_D5, BB_E5, BB_F5, BB_G5, BB_H5, - BB_A6, BB_B6, BB_C6, BB_D6, BB_E6, BB_F6, BB_G6, BB_H6, - BB_A7, BB_B7, BB_C7, BB_D7, BB_E7, BB_F7, BB_G7, BB_H7, - BB_A8, BB_B8, BB_C8, BB_D8, BB_E8, BB_F8, BB_G8, BB_H8 -] = [1 << sq for sq in SQUARES] - -BB_LIGHT_SQUARES = 0x55aa55aa55aa55aa -BB_DARK_SQUARES = 0xaa55aa55aa55aa55 - -BB_FILES = [ - BB_FILE_A, - BB_FILE_B, - BB_FILE_C, - BB_FILE_D, - BB_FILE_E, - BB_FILE_F, - BB_FILE_G, - BB_FILE_H -] = [0x0101010101010101 << i for i in range(8)] - -BB_RANKS = [ - BB_RANK_1, - BB_RANK_2, - BB_RANK_3, - BB_RANK_4, - BB_RANK_5, - BB_RANK_6, - BB_RANK_7, - BB_RANK_8 -] = [0xff << (8 * i) for i in range(8)] - -BB_BACKRANKS = BB_RANK_1 | BB_RANK_8 - - -try: - # Added in Python 2.7 and 3.1 respectively. - int.bit_length -except AttributeError: - def _lsb_table(): - table = [0 for _ in range(64)] - for square, bb in enumerate(BB_SQUARES): - index = (((bb ^ (bb - 1)) * 0x3f79d71b4cb0a89) & 0xffffffffffffffff) >> 58 - table[index] = square - return table - - def lsb(bb, _table=_lsb_table()): - return _table[(((bb ^ (bb - 1)) * 0x3f79d71b4cb0a89) & 0xffffffffffffffff) >> 58] - - def scan_forward(bb, _bin=bin, _len=len): - string = _bin(bb) - l = _len(string) - r = string.rfind("1") - while r != -1: - yield l - r - 1 - r = string.rfind("1", 0, r) - - def msb(bb, _bin=bin, _len=len): - string = _bin(bb) - return _len(string) - 1 - string.find("1") - - def scan_reversed(bb, _bin=bin, _len=len): - string = _bin(bb) - l = _len(string) - r = string.find("1") - while r != -1: - yield l - r - 1 - r = string.find("1", r + 1) -else: - def lsb(bb): - return (bb & -bb).bit_length() - 1 - - def scan_forward(bb): - while bb: - r = bb & -bb - yield r.bit_length() - 1 - bb ^= r - - def msb(bb): - return bb.bit_length() - 1 - - def scan_reversed(bb, _BB_SQUARES=BB_SQUARES): - while bb: - r = bb.bit_length() - 1 - yield r - bb ^= _BB_SQUARES[r] - - -def popcount(b, _bin=bin): - return _bin(b).count("1") - - -def shift_down(b): +SQUARES_180: List[Square] = [square_mirror(sq) for sq in SQUARES] + + +Bitboard: TypeAlias = int +BB_EMPTY: Bitboard = 0 +BB_ALL: Bitboard = 0xffff_ffff_ffff_ffff + +BB_A1: Bitboard = 1 << A1 +BB_B1: Bitboard = 1 << B1 +BB_C1: Bitboard = 1 << C1 +BB_D1: Bitboard = 1 << D1 +BB_E1: Bitboard = 1 << E1 +BB_F1: Bitboard = 1 << F1 +BB_G1: Bitboard = 1 << G1 +BB_H1: Bitboard = 1 << H1 +BB_A2: Bitboard = 1 << A2 +BB_B2: Bitboard = 1 << B2 +BB_C2: Bitboard = 1 << C2 +BB_D2: Bitboard = 1 << D2 +BB_E2: Bitboard = 1 << E2 +BB_F2: Bitboard = 1 << F2 +BB_G2: Bitboard = 1 << G2 +BB_H2: Bitboard = 1 << H2 +BB_A3: Bitboard = 1 << A3 +BB_B3: Bitboard = 1 << B3 +BB_C3: Bitboard = 1 << C3 +BB_D3: Bitboard = 1 << D3 +BB_E3: Bitboard = 1 << E3 +BB_F3: Bitboard = 1 << F3 +BB_G3: Bitboard = 1 << G3 +BB_H3: Bitboard = 1 << H3 +BB_A4: Bitboard = 1 << A4 +BB_B4: Bitboard = 1 << B4 +BB_C4: Bitboard = 1 << C4 +BB_D4: Bitboard = 1 << D4 +BB_E4: Bitboard = 1 << E4 +BB_F4: Bitboard = 1 << F4 +BB_G4: Bitboard = 1 << G4 +BB_H4: Bitboard = 1 << H4 +BB_A5: Bitboard = 1 << A5 +BB_B5: Bitboard = 1 << B5 +BB_C5: Bitboard = 1 << C5 +BB_D5: Bitboard = 1 << D5 +BB_E5: Bitboard = 1 << E5 +BB_F5: Bitboard = 1 << F5 +BB_G5: Bitboard = 1 << G5 +BB_H5: Bitboard = 1 << H5 +BB_A6: Bitboard = 1 << A6 +BB_B6: Bitboard = 1 << B6 +BB_C6: Bitboard = 1 << C6 +BB_D6: Bitboard = 1 << D6 +BB_E6: Bitboard = 1 << E6 +BB_F6: Bitboard = 1 << F6 +BB_G6: Bitboard = 1 << G6 +BB_H6: Bitboard = 1 << H6 +BB_A7: Bitboard = 1 << A7 +BB_B7: Bitboard = 1 << B7 +BB_C7: Bitboard = 1 << C7 +BB_D7: Bitboard = 1 << D7 +BB_E7: Bitboard = 1 << E7 +BB_F7: Bitboard = 1 << F7 +BB_G7: Bitboard = 1 << G7 +BB_H7: Bitboard = 1 << H7 +BB_A8: Bitboard = 1 << A8 +BB_B8: Bitboard = 1 << B8 +BB_C8: Bitboard = 1 << C8 +BB_D8: Bitboard = 1 << D8 +BB_E8: Bitboard = 1 << E8 +BB_F8: Bitboard = 1 << F8 +BB_G8: Bitboard = 1 << G8 +BB_H8: Bitboard = 1 << H8 +BB_SQUARES: List[Bitboard] = [1 << sq for sq in SQUARES] + +BB_CORNERS: Bitboard = BB_A1 | BB_H1 | BB_A8 | BB_H8 +BB_CENTER: Bitboard = BB_D4 | BB_E4 | BB_D5 | BB_E5 + +BB_LIGHT_SQUARES: Bitboard = 0x55aa_55aa_55aa_55aa +BB_DARK_SQUARES: Bitboard = 0xaa55_aa55_aa55_aa55 + +BB_FILE_A: Bitboard = 0x0101_0101_0101_0101 << FILE_A +BB_FILE_B: Bitboard = 0x0101_0101_0101_0101 << FILE_B +BB_FILE_C: Bitboard = 0x0101_0101_0101_0101 << FILE_C +BB_FILE_D: Bitboard = 0x0101_0101_0101_0101 << FILE_D +BB_FILE_E: Bitboard = 0x0101_0101_0101_0101 << FILE_E +BB_FILE_F: Bitboard = 0x0101_0101_0101_0101 << FILE_F +BB_FILE_G: Bitboard = 0x0101_0101_0101_0101 << FILE_G +BB_FILE_H: Bitboard = 0x0101_0101_0101_0101 << FILE_H +BB_FILES: List[Bitboard] = [BB_FILE_A, BB_FILE_B, BB_FILE_C, BB_FILE_D, BB_FILE_E, BB_FILE_F, BB_FILE_G, BB_FILE_H] + +BB_RANK_1: Bitboard = 0xff << (8 * RANK_1) +BB_RANK_2: Bitboard = 0xff << (8 * RANK_2) +BB_RANK_3: Bitboard = 0xff << (8 * RANK_3) +BB_RANK_4: Bitboard = 0xff << (8 * RANK_4) +BB_RANK_5: Bitboard = 0xff << (8 * RANK_5) +BB_RANK_6: Bitboard = 0xff << (8 * RANK_6) +BB_RANK_7: Bitboard = 0xff << (8 * RANK_7) +BB_RANK_8: Bitboard = 0xff << (8 * RANK_8) +BB_RANKS: List[Bitboard] = [BB_RANK_1, BB_RANK_2, BB_RANK_3, BB_RANK_4, BB_RANK_5, BB_RANK_6, BB_RANK_7, BB_RANK_8] + +BB_BACKRANKS: Bitboard = BB_RANK_1 | BB_RANK_8 + + +def lsb(bb: Bitboard) -> int: + return (bb & -bb).bit_length() - 1 + +def scan_forward(bb: Bitboard) -> Iterator[Square]: + while bb: + r = bb & -bb + yield r.bit_length() - 1 + bb ^= r + +def msb(bb: Bitboard) -> int: + return bb.bit_length() - 1 + +def scan_reversed(bb: Bitboard) -> Iterator[Square]: + while bb: + r = bb.bit_length() - 1 + yield r + bb ^= BB_SQUARES[r] + +# Python 3.10 or fallback. +popcount: Callable[[Bitboard], int] = getattr(int, "bit_count", lambda bb: bin(bb).count("1")) + +def flip_vertical(bb: Bitboard) -> Bitboard: + # https://www.chessprogramming.org/Flipping_Mirroring_and_Rotating#FlipVertically + bb = ((bb >> 8) & 0x00ff_00ff_00ff_00ff) | ((bb & 0x00ff_00ff_00ff_00ff) << 8) + bb = ((bb >> 16) & 0x0000_ffff_0000_ffff) | ((bb & 0x0000_ffff_0000_ffff) << 16) + bb = (bb >> 32) | ((bb & 0x0000_0000_ffff_ffff) << 32) + return bb + +def flip_horizontal(bb: Bitboard) -> Bitboard: + # https://www.chessprogramming.org/Flipping_Mirroring_and_Rotating#MirrorHorizontally + bb = ((bb >> 1) & 0x5555_5555_5555_5555) | ((bb & 0x5555_5555_5555_5555) << 1) + bb = ((bb >> 2) & 0x3333_3333_3333_3333) | ((bb & 0x3333_3333_3333_3333) << 2) + bb = ((bb >> 4) & 0x0f0f_0f0f_0f0f_0f0f) | ((bb & 0x0f0f_0f0f_0f0f_0f0f) << 4) + return bb + +def flip_diagonal(bb: Bitboard) -> Bitboard: + # https://www.chessprogramming.org/Flipping_Mirroring_and_Rotating#FlipabouttheDiagonal + t = (bb ^ (bb << 28)) & 0x0f0f_0f0f_0000_0000 + bb = bb ^ t ^ (t >> 28) + t = (bb ^ (bb << 14)) & 0x3333_0000_3333_0000 + bb = bb ^ t ^ (t >> 14) + t = (bb ^ (bb << 7)) & 0x5500_5500_5500_5500 + bb = bb ^ t ^ (t >> 7) + return bb + +def flip_anti_diagonal(bb: Bitboard) -> Bitboard: + # https://www.chessprogramming.org/Flipping_Mirroring_and_Rotating#FlipabouttheAntidiagonal + t = bb ^ (bb << 36) + bb = bb ^ ((t ^ (bb >> 36)) & 0xf0f0_f0f0_0f0f_0f0f) + t = (bb ^ (bb << 18)) & 0xcccc_0000_cccc_0000 + bb = bb ^ t ^ (t >> 18) + t = (bb ^ (bb << 9)) & 0xaa00_aa00_aa00_aa00 + bb = bb ^ t ^ (t >> 9) + return bb + + +def shift_down(b: Bitboard) -> Bitboard: return b >> 8 -def shift_2_down(b): +def shift_2_down(b: Bitboard) -> Bitboard: return b >> 16 -def shift_up(b): +def shift_up(b: Bitboard) -> Bitboard: return (b << 8) & BB_ALL -def shift_2_up(b): +def shift_2_up(b: Bitboard) -> Bitboard: return (b << 16) & BB_ALL -def shift_right(b): +def shift_right(b: Bitboard) -> Bitboard: return (b << 1) & ~BB_FILE_A & BB_ALL -def shift_2_right(b): +def shift_2_right(b: Bitboard) -> Bitboard: return (b << 2) & ~BB_FILE_A & ~BB_FILE_B & BB_ALL -def shift_left(b): +def shift_left(b: Bitboard) -> Bitboard: return (b >> 1) & ~BB_FILE_H -def shift_2_left(b): +def shift_2_left(b: Bitboard) -> Bitboard: return (b >> 2) & ~BB_FILE_G & ~BB_FILE_H -def shift_up_left(b): +def shift_up_left(b: Bitboard) -> Bitboard: return (b << 7) & ~BB_FILE_H & BB_ALL -def shift_up_right(b): +def shift_up_right(b: Bitboard) -> Bitboard: return (b << 9) & ~BB_FILE_A & BB_ALL -def shift_down_left(b): +def shift_down_left(b: Bitboard) -> Bitboard: return (b >> 9) & ~BB_FILE_H -def shift_down_right(b): +def shift_down_right(b: Bitboard) -> Bitboard: return (b >> 7) & ~BB_FILE_A -def _sliding_attacks(square, occupied, deltas): - attacks = 0 +def _sliding_attacks(square: Square, occupied: Bitboard, deltas: Iterable[int]) -> Bitboard: + attacks = BB_EMPTY for delta in deltas: sq = square @@ -275,31 +556,37 @@ def _sliding_attacks(square, occupied, deltas): return attacks -BB_KNIGHT_ATTACKS = [_sliding_attacks(sq, BB_ALL, [17, 15, 10, 6, -17, -15, -10, -6]) for sq in SQUARES] -BB_KING_ATTACKS = [_sliding_attacks(sq, BB_ALL, [9, 8, 7, 1, -9, -8, -7, -1]) for sq in SQUARES] -BB_PAWN_ATTACKS = [[_sliding_attacks(sq, BB_ALL, deltas) for sq in SQUARES] for deltas in [[-7, -9], [7, 9]]] +def _step_attacks(square: Square, deltas: Iterable[int]) -> Bitboard: + return _sliding_attacks(square, BB_ALL, deltas) +BB_KNIGHT_ATTACKS: List[Bitboard] = [_step_attacks(sq, [17, 15, 10, 6, -17, -15, -10, -6]) for sq in SQUARES] +BB_KING_ATTACKS: List[Bitboard] = [_step_attacks(sq, [9, 8, 7, 1, -9, -8, -7, -1]) for sq in SQUARES] +BB_PAWN_ATTACKS: List[List[Bitboard]] = [[_step_attacks(sq, deltas) for sq in SQUARES] for deltas in [[-7, -9], [7, 9]]] -def _attack_table(deltas): - mask_table = [] - attack_table = [] - for square, bb in enumerate(BB_SQUARES): - attacks = {} +def _edges(square: Square) -> Bitboard: + return (((BB_RANK_1 | BB_RANK_8) & ~BB_RANKS[square_rank(square)]) | + ((BB_FILE_A | BB_FILE_H) & ~BB_FILES[square_file(square)])) - edges = (((BB_RANK_1 | BB_RANK_8) & ~BB_RANKS[square_rank(square)]) | - ((BB_FILE_A | BB_FILE_H) & ~BB_FILES[square_file(square)])) +def _carry_rippler(mask: Bitboard) -> Iterator[Bitboard]: + # Carry-Rippler trick to iterate subsets of mask. + subset = BB_EMPTY + while True: + yield subset + subset = (subset - mask) & mask + if not subset: + break - mask = _sliding_attacks(square, 0, deltas) & ~edges +def _attack_table(deltas: List[int]) -> Tuple[List[Bitboard], List[Dict[Bitboard, Bitboard]]]: + mask_table: List[Bitboard] = [] + attack_table: List[Dict[Bitboard, Bitboard]] = [] - # Carry-Rippler trick to iterate subsets of mask. - subset = 0 - while True: - attacks[subset] = _sliding_attacks(square, subset, deltas) + for square in SQUARES: + attacks = {} - subset = (subset - mask) & mask - if not subset: - break + mask = _sliding_attacks(square, 0, deltas) & ~_edges(square) + for subset in _carry_rippler(mask): + attacks[subset] = _sliding_attacks(square, subset, deltas) attack_table.append(attacks) mask_table.append(mask) @@ -311,105 +598,87 @@ def _attack_table(deltas): BB_RANK_MASKS, BB_RANK_ATTACKS = _attack_table([-1, 1]) -def _rays(): - rays = [] - between = [] +def _rays() -> List[List[Bitboard]]: + rays: List[List[Bitboard]] = [] for a, bb_a in enumerate(BB_SQUARES): - rays_row = [] - between_row = [] + rays_row: List[Bitboard] = [] for b, bb_b in enumerate(BB_SQUARES): if BB_DIAG_ATTACKS[a][0] & bb_b: rays_row.append((BB_DIAG_ATTACKS[a][0] & BB_DIAG_ATTACKS[b][0]) | bb_a | bb_b) - between_row.append(BB_DIAG_ATTACKS[a][BB_DIAG_MASKS[a] & bb_b] & BB_DIAG_ATTACKS[b][BB_DIAG_MASKS[b] & bb_a]) elif BB_RANK_ATTACKS[a][0] & bb_b: rays_row.append(BB_RANK_ATTACKS[a][0] | bb_a) - between_row.append(BB_RANK_ATTACKS[a][BB_RANK_MASKS[a] & bb_b] & BB_RANK_ATTACKS[b][BB_RANK_MASKS[b] & bb_a]) elif BB_FILE_ATTACKS[a][0] & bb_b: rays_row.append(BB_FILE_ATTACKS[a][0] | bb_a) - between_row.append(BB_FILE_ATTACKS[a][BB_FILE_MASKS[a] & bb_b] & BB_FILE_ATTACKS[b][BB_FILE_MASKS[b] & bb_a]) else: - rays_row.append(0) - between_row.append(0) + rays_row.append(BB_EMPTY) rays.append(rays_row) - between.append(between_row) - return rays, between + return rays + +BB_RAYS = _rays() -BB_RAYS, BB_BETWEEN = _rays() +def ray(a: Square, b: Square) -> Bitboard: + return BB_RAYS[a][b] +def between(a: Square, b: Square) -> Bitboard: + bb = BB_RAYS[a][b] & ((BB_ALL << a) ^ (BB_ALL << b)) + return bb & (bb - 1) -SAN_REGEX = re.compile(r"^([NBKRQ])?([a-h])?([1-8])?[\-x]?([a-h][1-8])(=?[nbrqkNBRQK])?(\+|#)?\Z") + +SAN_REGEX = re.compile(r"^([NBKRQ])?([a-h])?([1-8])?[\-x]?([a-h][1-8])(=?[nbrqkNBRQK])?[\+#]?\Z") FEN_CASTLING_REGEX = re.compile(r"^(?:-|[KQABCDEFGH]{0,2}[kqabcdefgh]{0,2})\Z") -class Piece(object): +@dataclasses.dataclass +class Piece: """A piece with type and color.""" - def __init__(self, piece_type, color): - self.piece_type = piece_type - self.color = color + piece_type: PieceType + """The piece type.""" + + color: Color + """The piece color.""" - def symbol(self): + def symbol(self) -> str: """ Gets the symbol ``P``, ``N``, ``B``, ``R``, ``Q`` or ``K`` for white pieces or the lower-case variants for the black pieces. """ - if self.color == WHITE: - return PIECE_SYMBOLS[self.piece_type].upper() - else: - return PIECE_SYMBOLS[self.piece_type] + symbol = piece_symbol(self.piece_type) + return symbol.upper() if self.color else symbol - def unicode_symbol(self, invert_color=False): + def unicode_symbol(self, *, invert_color: bool = False) -> str: """ - Gets the unicode character for the piece. + Gets the Unicode character for the piece. """ - if not invert_color: - return UNICODE_PIECE_SYMBOLS[self.symbol()] - else: - return UNICODE_PIECE_SYMBOLS[self.symbol().swapcase()] + symbol = self.symbol().swapcase() if invert_color else self.symbol() + return UNICODE_PIECE_SYMBOLS[symbol] - def __hash__(self): - return hash(self.piece_type * (self.color + 1)) + def __hash__(self) -> int: + return self.piece_type + (-1 if self.color else 5) - def __repr__(self): - return "Piece.from_symbol('{0}')".format(self.symbol()) + def __repr__(self) -> str: + return f"Piece.from_symbol({self.symbol()!r})" - def __str__(self): + def __str__(self) -> str: return self.symbol() - def _repr_svg_(self): + def _repr_svg_(self) -> str: import chess.svg return chess.svg.piece(self, size=45) - def __eq__(self, other): - ne = self.__ne__(other) - return NotImplemented if ne is NotImplemented else not ne - - def __ne__(self, other): - try: - if self.piece_type != other.piece_type: - return True - elif self.color != other.color: - return True - else: - return False - except AttributeError: - return NotImplemented - @classmethod - def from_symbol(cls, symbol): + def from_symbol(cls, symbol: str) -> Piece: """ - Creates a piece instance from a piece symbol. + Creates a :class:`~chess.Piece` instance from a piece symbol. :raises: :exc:`ValueError` if the symbol is invalid. """ - if symbol.islower(): - return cls(PIECE_SYMBOLS.index(symbol), BLACK) - else: - return cls(PIECE_SYMBOLS.index(symbol.lower()), WHITE) + return cls(PIECE_SYMBOLS.index(symbol.lower()), symbol.isupper()) -class Move(object): +@dataclasses.dataclass(unsafe_hash=True) +class Move: """ Represents a move from a square to a square and possibly the promotion piece type. @@ -417,94 +686,79 @@ class Move(object): Drops and null moves are supported. """ - def __init__(self, from_square, to_square, promotion=None, drop=None): - self.from_square = from_square - self.to_square = to_square - self.promotion = promotion - self.drop = drop + from_square: Square + """The source square.""" - def uci(self): + to_square: Square + """The target square.""" + + promotion: Optional[PieceType] = None + """The promotion piece type or ``None``.""" + + drop: Optional[PieceType] = None + """The drop piece type or ``None``.""" + + def uci(self) -> str: """ Gets a UCI string for the move. - For example a move from A7 to A8 would be ``a7a8`` or ``a7a8q`` if it - is a promotion to a queen. + For example, a move from a7 to a8 would be ``a7a8`` or ``a7a8q`` + (if the latter is a promotion to a queen). - The UCI representatin of null moves is ``0000``. + The UCI representation of a null move is ``0000``. """ if self.drop: - return PIECE_SYMBOLS[self.drop].upper() + "@" + SQUARE_NAMES[self.to_square] + return piece_symbol(self.drop).upper() + "@" + SQUARE_NAMES[self.to_square] elif self.promotion: - return SQUARE_NAMES[self.from_square] + SQUARE_NAMES[self.to_square] + PIECE_SYMBOLS[self.promotion] + return SQUARE_NAMES[self.from_square] + SQUARE_NAMES[self.to_square] + piece_symbol(self.promotion) elif self: return SQUARE_NAMES[self.from_square] + SQUARE_NAMES[self.to_square] else: return "0000" - def __bool__(self): - return bool(self.from_square or self.to_square or self.promotion or self.drop) - - __nonzero__ = __bool__ - - def __eq__(self, other): - ne = self.__ne__(other) - return NotImplemented if ne is NotImplemented else not ne + def xboard(self) -> str: + return self.uci() if self else "@@@@" - def __ne__(self, other): - try: - if self.from_square != other.from_square: - return True - elif self.to_square != other.to_square: - return True - elif self.promotion != other.promotion: - return True - elif self.drop != other.drop: - return True - else: - return False - except AttributeError: - return NotImplemented + def __bool__(self) -> bool: + return bool(self.from_square or self.to_square or self.promotion or self.drop) - def __repr__(self): - return "Move.from_uci('{0}')".format(self.uci()) + def __repr__(self) -> str: + return f"Move.from_uci({self.uci()!r})" - def __str__(self): + def __str__(self) -> str: return self.uci() - def __hash__(self): - return hash((self.to_square, self.from_square, self.promotion, self.drop)) - - def __copy__(self): - return type(self)(self.from_square, self.to_square, self.promotion, self.drop) - - def __deepcopy__(self, memo): - move = self.__copy__() - memo[id(self)] = move - return move - @classmethod - def from_uci(cls, uci): + def from_uci(cls, uci: str) -> Move: """ Parses a UCI string. - :raises: :exc:`ValueError` if the UCI string is invalid. + :raises: :exc:`InvalidMoveError` if the UCI string is invalid. """ if uci == "0000": return cls.null() elif len(uci) == 4 and "@" == uci[1]: - drop = PIECE_SYMBOLS.index(uci[0].lower()) - square = SQUARE_NAMES.index(uci[2:]) + try: + drop = PIECE_SYMBOLS.index(uci[0].lower()) + square = SQUARE_NAMES.index(uci[2:]) + except ValueError: + raise InvalidMoveError(f"invalid uci: {uci!r}") return cls(square, square, drop=drop) - elif len(uci) == 4: - return cls(SQUARE_NAMES.index(uci[0:2]), SQUARE_NAMES.index(uci[2:4])) - elif len(uci) == 5: - promotion = PIECE_SYMBOLS.index(uci[4]) - return cls(SQUARE_NAMES.index(uci[0:2]), SQUARE_NAMES.index(uci[2:4]), promotion=promotion) + elif 4 <= len(uci) <= 5: + try: + from_square = SQUARE_NAMES.index(uci[0:2]) + to_square = SQUARE_NAMES.index(uci[2:4]) + promotion = PIECE_SYMBOLS.index(uci[4]) if len(uci) == 5 else None + except ValueError: + raise InvalidMoveError(f"invalid uci: {uci!r}") + if from_square == to_square and from_square != A1: + raise InvalidMoveError(f"invalid uci (use 0000 for null moves): {uci!r}") + return cls(from_square, to_square, promotion=promotion) else: - raise ValueError("expected uci string to be of length 4 or 5: {0}".format(repr(uci))) + raise InvalidMoveError(f"expected uci string to be of length 4 or 5: {uci!r}") @classmethod - def null(cls): + def null(cls) -> Move: """ Gets a null move. @@ -512,24 +766,28 @@ def null(cls): forfeits en passant capturing). Null moves evaluate to ``False`` in boolean contexts. + >>> import chess + >>> >>> bool(chess.Move.null()) False """ return cls(0, 0) -class BaseBoard(object): +BaseBoardT = TypeVar("BaseBoardT", bound="BaseBoard") + +class BaseBoard: """ A board representing the position of chess pieces. See :class:`~chess.Board` for a full board with move generation. - The board is initialized to the standard chess starting position, unless + The board is initialized with the standard chess starting position, unless otherwise specified in the optional *board_fen* argument. If *board_fen* - is ``None`` an empty board is created. + is ``None``, an empty board is created. """ - def __init__(self, board_fen=STARTING_BOARD_FEN): - self.occupied_co = [BB_VOID, BB_VOID] + def __init__(self, board_fen: Optional[str] = STARTING_BOARD_FEN) -> None: + self.occupied_co = [BB_EMPTY, BB_EMPTY] if board_fen is None: self._clear_board() @@ -538,42 +796,61 @@ def __init__(self, board_fen=STARTING_BOARD_FEN): else: self._set_board_fen(board_fen) - def _reset_board(self): + def _reset_board(self) -> None: self.pawns = BB_RANK_2 | BB_RANK_7 self.knights = BB_B1 | BB_G1 | BB_B8 | BB_G8 self.bishops = BB_C1 | BB_F1 | BB_C8 | BB_F8 - self.rooks = BB_A1 | BB_H1 | BB_A8 | BB_H8 + self.rooks = BB_CORNERS self.queens = BB_D1 | BB_D8 self.kings = BB_E1 | BB_E8 - self.promoted = BB_VOID + self.promoted = BB_EMPTY self.occupied_co[WHITE] = BB_RANK_1 | BB_RANK_2 self.occupied_co[BLACK] = BB_RANK_7 | BB_RANK_8 self.occupied = BB_RANK_1 | BB_RANK_2 | BB_RANK_7 | BB_RANK_8 - def reset_board(self): + def reset_board(self) -> None: + """ + Resets pieces to the starting position. + + :class:`~chess.Board` also resets the move stack, but not turn, + castling rights and move counters. Use :func:`chess.Board.reset()` to + fully restore the starting position. + """ self._reset_board() - def _clear_board(self): - self.pawns = BB_VOID - self.knights = BB_VOID - self.bishops = BB_VOID - self.rooks = BB_VOID - self.queens = BB_VOID - self.kings = BB_VOID + def _clear_board(self) -> None: + self.pawns = BB_EMPTY + self.knights = BB_EMPTY + self.bishops = BB_EMPTY + self.rooks = BB_EMPTY + self.queens = BB_EMPTY + self.kings = BB_EMPTY - self.promoted = BB_VOID + self.promoted = BB_EMPTY - self.occupied_co[WHITE] = BB_VOID - self.occupied_co[BLACK] = BB_VOID - self.occupied = BB_VOID + self.occupied_co[WHITE] = BB_EMPTY + self.occupied_co[BLACK] = BB_EMPTY + self.occupied = BB_EMPTY - def clear_board(self): - """Clears the board.""" + def clear_board(self) -> None: + """ + Clears the board. + + :class:`~chess.Board` also clears the move stack. + """ self._clear_board() - def pieces_mask(self, piece_type, color): + def piece_count(self) -> int: + """ + Gets the number of pieces on the board. + + Does not include Crazyhouse pockets. + """ + return popcount(self.occupied) + + def pieces_mask(self, piece_type: PieceType, color: Color) -> Bitboard: if piece_type == PAWN: bb = self.pawns elif piece_type == KNIGHT: @@ -586,10 +863,12 @@ def pieces_mask(self, piece_type, color): bb = self.queens elif piece_type == KING: bb = self.kings + else: + assert False, f"expected PieceType, got {piece_type!r}" return bb & self.occupied_co[color] - def pieces(self, piece_type, color): + def pieces(self, piece_type: PieceType, color: Color) -> SquareSet: """ Gets pieces of the given type and color. @@ -597,20 +876,22 @@ def pieces(self, piece_type, color): """ return SquareSet(self.pieces_mask(piece_type, color)) - def piece_at(self, square): + def piece_at(self, square: Square) -> Optional[Piece]: """Gets the :class:`piece ` at the given square.""" piece_type = self.piece_type_at(square) if piece_type: mask = BB_SQUARES[square] color = bool(self.occupied_co[WHITE] & mask) return Piece(piece_type, color) + else: + return None - def piece_type_at(self, square): + def piece_type_at(self, square: Square) -> Optional[PieceType]: """Gets the piece type at the given square.""" mask = BB_SQUARES[square] if not self.occupied & mask: - return None + return None # Early return elif self.pawns & mask: return PAWN elif self.knights & mask: @@ -621,22 +902,173 @@ def piece_type_at(self, square): return ROOK elif self.queens & mask: return QUEEN - elif self.kings & mask: + else: return KING - def king(self, color): + def color_at(self, square: Square) -> Optional[Color]: + """Gets the color of the piece at the given square.""" + mask = BB_SQUARES[square] + if self.occupied_co[WHITE] & mask: + return WHITE + elif self.occupied_co[BLACK] & mask: + return BLACK + else: + return None + + def _effective_promoted(self) -> Bitboard: + return BB_EMPTY + + def king(self, color: Color) -> Optional[Square]: """ - Finds the king square of the given side. Returns ``None`` if there - is no king of that color. + Finds the unique king square of the given side. Returns ``None`` if + there is no king or multiple kings of that color. - In variants with king promotions only non-promoted kings are + In variants with king promotions, only non-promoted kings are considered. """ - king_mask = self.occupied_co[color] & self.kings & ~self.promoted - if king_mask: - return msb(king_mask) + king_mask = self.occupied_co[color] & self.kings & ~self._effective_promoted() + return msb(king_mask) if king_mask and not king_mask & (king_mask - 1) else None + + def attacks_mask(self, square: Square) -> Bitboard: + bb_square = BB_SQUARES[square] + + if bb_square & self.pawns: + color = bool(bb_square & self.occupied_co[WHITE]) + return BB_PAWN_ATTACKS[color][square] + elif bb_square & self.knights: + return BB_KNIGHT_ATTACKS[square] + elif bb_square & self.kings: + return BB_KING_ATTACKS[square] + else: + attacks = 0 + if bb_square & self.bishops or bb_square & self.queens: + attacks = BB_DIAG_ATTACKS[square][BB_DIAG_MASKS[square] & self.occupied] + if bb_square & self.rooks or bb_square & self.queens: + attacks |= (BB_RANK_ATTACKS[square][BB_RANK_MASKS[square] & self.occupied] | + BB_FILE_ATTACKS[square][BB_FILE_MASKS[square] & self.occupied]) + return attacks + + def attacks(self, square: Square) -> SquareSet: + """ + Gets the set of attacked squares from the given square. + + There will be no attacks if the square is empty. Pinned pieces are + still attacking other squares. + + Returns a :class:`set of squares `. + """ + return SquareSet(self.attacks_mask(square)) + + def attackers_mask(self, color: Color, square: Square, occupied: Optional[Bitboard] = None) -> Bitboard: + occupied = self.occupied if occupied is None else occupied + + rank_pieces = BB_RANK_MASKS[square] & occupied + file_pieces = BB_FILE_MASKS[square] & occupied + diag_pieces = BB_DIAG_MASKS[square] & occupied + + queens_and_rooks = self.queens | self.rooks + queens_and_bishops = self.queens | self.bishops + + attackers = ( + (BB_KING_ATTACKS[square] & self.kings) | + (BB_KNIGHT_ATTACKS[square] & self.knights) | + (BB_RANK_ATTACKS[square][rank_pieces] & queens_and_rooks) | + (BB_FILE_ATTACKS[square][file_pieces] & queens_and_rooks) | + (BB_DIAG_ATTACKS[square][diag_pieces] & queens_and_bishops) | + (BB_PAWN_ATTACKS[not color][square] & self.pawns)) + + return attackers & self.occupied_co[color] + + def is_attacked_by(self, color: Color, square: Square, occupied: Optional[IntoSquareSet] = None) -> bool: + """ + Checks if the given side attacks the given square. + + Pinned pieces still count as attackers. Pawns that can be captured + en passant are **not** considered attacked. + + *occupied* determines which squares are considered to block attacks. + For example, + ``board.occupied ^ board.pieces_mask(chess.KING, board.turn)`` can be + used to consider X-ray attacks through the king. + Defaults to ``board.occupied`` (all pieces including the king, + no X-ray attacks). + """ + return bool(self.attackers_mask(color, square, None if occupied is None else SquareSet(occupied).mask)) + + def attackers(self, color: Color, square: Square, occupied: Optional[IntoSquareSet] = None) -> SquareSet: + """ + Gets the set of attackers of the given color for the given square. + + Pinned pieces still count as attackers. + + *occupied* determines which squares are considered to block attacks. + For example, + ``board.occupied ^ board.pieces_mask(chess.KING, board.turn)`` can be + used to consider X-ray attacks through the king. + Defaults to ``board.occupied`` (all pieces including the king, + no X-ray attacks). + + Returns a :class:`set of squares `. + """ + return SquareSet(self.attackers_mask(color, square, None if occupied is None else SquareSet(occupied).mask)) + + def pin_mask(self, color: Color, square: Square) -> Bitboard: + king = self.king(color) + if king is None: + return BB_ALL + + square_mask = BB_SQUARES[square] + + for attacks, sliders in [(BB_FILE_ATTACKS, self.rooks | self.queens), + (BB_RANK_ATTACKS, self.rooks | self.queens), + (BB_DIAG_ATTACKS, self.bishops | self.queens)]: + rays = attacks[king][0] + if rays & square_mask: + snipers = rays & sliders & self.occupied_co[not color] + for sniper in scan_reversed(snipers): + if between(sniper, king) & (self.occupied | square_mask) == square_mask: + return ray(king, sniper) + + break + + return BB_ALL + + def pin(self, color: Color, square: Square) -> SquareSet: + """ + Detects an absolute pin (and its direction) of the given square to + the king of the given color. + + >>> import chess + >>> + >>> board = chess.Board("rnb1k2r/ppp2ppp/5n2/3q4/1b1P4/2N5/PP3PPP/R1BQKBNR w KQkq - 3 7") + >>> board.is_pinned(chess.WHITE, chess.C3) + True + >>> direction = board.pin(chess.WHITE, chess.C3) + >>> direction + SquareSet(0x0000_0001_0204_0810) + >>> print(direction) + . . . . . . . . + . . . . . . . . + . . . . . . . . + 1 . . . . . . . + . 1 . . . . . . + . . 1 . . . . . + . . . 1 . . . . + . . . . 1 . . . + + Returns a :class:`set of squares ` that mask the rank, + file or diagonal of the pin. If there is no pin, then a mask of the + entire board is returned. + """ + return SquareSet(self.pin_mask(color, square)) + + def is_pinned(self, color: Color, square: Square) -> bool: + """ + Detects if the given square is pinned to the king of the given color. + """ + return self.pin_mask(color, square) != BB_ALL - def _remove_piece_at(self, square): + def _remove_piece_at(self, square: Square) -> Optional[PieceType]: piece_type = self.piece_type_at(square) mask = BB_SQUARES[square] @@ -653,7 +1085,7 @@ def _remove_piece_at(self, square): elif piece_type == KING: self.kings ^= mask else: - return + return None self.occupied ^= mask self.occupied_co[WHITE] &= ~mask @@ -663,17 +1095,18 @@ def _remove_piece_at(self, square): return piece_type - def remove_piece_at(self, square): + def remove_piece_at(self, square: Square) -> Optional[Piece]: """ Removes the piece from the given square. Returns the :class:`~chess.Piece` or ``None`` if the square was already empty. + + :class:`~chess.Board` also clears the move stack. """ color = bool(self.occupied_co[WHITE] & BB_SQUARES[square]) piece_type = self._remove_piece_at(square) - if piece_type: - return Piece(piece_type, color) + return Piece(piece_type, color) if piece_type else None - def _set_piece_at(self, square, piece_type, color, promoted=False): + def _set_piece_at(self, square: Square, piece_type: PieceType, color: Color, promoted: bool = False) -> None: self._remove_piece_at(square) mask = BB_SQUARES[square] @@ -690,6 +1123,8 @@ def _set_piece_at(self, square, piece_type, color, promoted=False): self.queens |= mask elif piece_type == KING: self.kings |= mask + else: + return self.occupied ^= mask self.occupied_co[color] ^= mask @@ -697,23 +1132,26 @@ def _set_piece_at(self, square, piece_type, color, promoted=False): if promoted: self.promoted ^= mask - def set_piece_at(self, square, piece, promoted=False): + def set_piece_at(self, square: Square, piece: Optional[Piece], promoted: bool = False) -> None: """ Sets a piece at the given square. An existing piece is replaced. Setting *piece* to ``None`` is equivalent to :func:`~chess.Board.remove_piece_at()`. + + :class:`~chess.Board` also clears the move stack. """ if piece is None: self._remove_piece_at(square) else: self._set_piece_at(square, piece.piece_type, piece.color, promoted) - def board_fen(self, promoted=False): + def board_fen(self, *, promoted: Optional[bool] = None) -> str: """ - Gets the board FEN. + Gets the board FEN (e.g., + ``rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR``). """ - builder = [] + builder: List[str] = [] empty = 0 for square in SQUARES_180: @@ -726,7 +1164,14 @@ def board_fen(self, promoted=False): builder.append(str(empty)) empty = 0 builder.append(piece.symbol()) - if promoted and BB_SQUARES[square] & self.promoted: + + if promoted is None: + promoted_mask = self._effective_promoted() + elif promoted: + promoted_mask = self.promoted + else: + promoted_mask = BB_EMPTY + if BB_SQUARES[square] & promoted_mask: builder.append("~") if BB_SQUARES[square] & BB_FILE_H: @@ -739,11 +1184,16 @@ def board_fen(self, promoted=False): return "".join(builder) - def _set_board_fen(self, fen): + def _set_board_fen(self, fen: str) -> None: + # Compatibility with set_fen(). + fen = fen.strip() + if " " in fen: + raise ValueError(f"expected position part of fen, got multiple parts: {fen!r}") + # Ensure the FEN is valid. rows = fen.split("/") if len(rows) != 8: - raise ValueError("expected 8 rows in position part of fen: {0}".format(repr(fen))) + raise ValueError(f"expected 8 rows in position part of fen: {fen!r}") # Validate each row. for row in rows: @@ -754,24 +1204,24 @@ def _set_board_fen(self, fen): for c in row: if c in ["1", "2", "3", "4", "5", "6", "7", "8"]: if previous_was_digit: - raise ValueError("two subsequent digits in position part of fen: {0}".format(repr(fen))) + raise ValueError(f"two subsequent digits in position part of fen: {fen!r}") field_sum += int(c) previous_was_digit = True previous_was_piece = False elif c == "~": if not previous_was_piece: - raise ValueError("~ not after piece in position part of fen: {0}".format(repr(fen))) + raise ValueError(f"'~' not after piece in position part of fen: {fen!r}") previous_was_digit = False previous_was_piece = False - elif c.lower() in ["p", "n", "b", "r", "q", "k"]: + elif c.lower() in PIECE_SYMBOLS: field_sum += 1 previous_was_digit = False previous_was_piece = True else: - raise ValueError("invalid character in position part of fen: {0}".format(repr(fen))) + raise ValueError(f"invalid character in position part of fen: {fen!r}") if field_sum != 8: - raise ValueError("expected 8 columns per row in position part of fen: {0}".format(repr(fen))) + raise ValueError(f"expected 8 columns per row in position part of fen: {fen!r}") # Clear the board. self._clear_board() @@ -781,31 +1231,59 @@ def _set_board_fen(self, fen): for c in fen: if c in ["1", "2", "3", "4", "5", "6", "7", "8"]: square_index += int(c) - elif c.lower() in ["p", "n", "b", "r", "q", "k"]: + elif c.lower() in PIECE_SYMBOLS: piece = Piece.from_symbol(c) self._set_piece_at(SQUARES_180[square_index], piece.piece_type, piece.color) square_index += 1 elif c == "~": self.promoted |= BB_SQUARES[SQUARES_180[square_index - 1]] - def set_board_fen(self, fen): + def set_board_fen(self, fen: str) -> None: """ - Parses a FEN and sets the board from it. + Parses *fen* and sets up the board, where *fen* is the board part of + a FEN. + + :class:`~chess.Board` also clears the move stack. - :raises: :exc:`ValueError` if the FEN string is invalid. + :raises: :exc:`ValueError` if syntactically invalid. """ self._set_board_fen(fen) - def _set_chess960_pos(self, sharnagl): - if not 0 <= sharnagl <= 959: - raise ValueError("chess960 position index not 0 <= {0} <= 959".format(repr(sharnagl))) + def piece_map(self, *, mask: Bitboard = BB_ALL) -> Dict[Square, Piece]: + """ + Gets a dictionary of :class:`pieces ` by square index. + """ + result: Dict[Square, Piece] = {} + for square in scan_reversed(self.occupied & mask): + result[square] = typing.cast(Piece, self.piece_at(square)) + return result + + def _set_piece_map(self, pieces: Mapping[Square, Piece]) -> None: + self._clear_board() + for square, piece in pieces.items(): + self._set_piece_at(square, piece.piece_type, piece.color) + + def set_piece_map(self, pieces: Mapping[Square, Piece]) -> None: + """ + Sets up the board from a dictionary of :class:`pieces ` + by square index. + + :class:`~chess.Board` also clears the move stack. + """ + self._set_piece_map(pieces) + + def _set_chess960_pos(self, scharnagl: int) -> None: + if not 0 <= scharnagl <= 959: + raise ValueError(f"chess960 position index not 0 <= {scharnagl!r} <= 959") # See http://www.russellcottrell.com/Chess/Chess960.htm for # a description of the algorithm. - n, bw = divmod(sharnagl, 4) + n, bw = divmod(scharnagl, 4) n, bb = divmod(n, 4) n, q = divmod(n, 6) + n1 = 0 + n2 = 0 for n1 in range(0, 4): n2 = n + (3 - n1) * (4 - n1) // 2 - 5 if n1 < n2 and 1 <= n2 <= 4: @@ -825,7 +1303,7 @@ def _set_chess960_pos(self, sharnagl): used = [bw_file, bb_file, q_file] # Knights. - self.knights = BB_VOID + self.knights = BB_EMPTY for i in range(0, 8): if i not in used: if n1 == 0 or n2 == 0: @@ -855,19 +1333,18 @@ def _set_chess960_pos(self, sharnagl): self.occupied_co[WHITE] = BB_RANK_1 | BB_RANK_2 self.occupied_co[BLACK] = BB_RANK_7 | BB_RANK_8 self.occupied = BB_RANK_1 | BB_RANK_2 | BB_RANK_7 | BB_RANK_8 - self.promoted = BB_VOID + self.promoted = BB_EMPTY - def set_chess960_pos(self, sharnagl): + def set_chess960_pos(self, scharnagl: int) -> None: """ Sets up a Chess960 starting position given its index between 0 and 959. - - >>> board.set_chess960_pos(random.randint(0, 959)) + Also see :func:`~chess.BaseBoard.from_chess960_pos()`. """ - self._set_chess960_pos(sharnagl) + self._set_chess960_pos(scharnagl) - def chess960_pos(self): + def chess960_pos(self) -> Optional[int]: """ - Gets the Chess960 starting position index between 0 and 959 + Gets the Chess960 starting position index between 0 and 959, or ``None``. """ if self.occupied_co[WHITE] != BB_RANK_1 | BB_RANK_2: @@ -876,31 +1353,19 @@ def chess960_pos(self): return None if self.pawns != BB_RANK_2 | BB_RANK_7: return None - if self.promoted: + if self._effective_promoted(): return None - if popcount(self.bishops) != 4: - return None - if popcount(self.rooks) != 4: - return None - if popcount(self.knights) != 4: - return None - if popcount(self.queens) != 2: - return None - if popcount(self.kings) != 2: + # Piece counts. + brnqk = [self.bishops, self.rooks, self.knights, self.queens, self.kings] + if [popcount(pieces) for pieces in brnqk] != [4, 4, 4, 2, 2]: return None - if (BB_RANK_1 & self.knights) << 56 != BB_RANK_8 & self.knights: - return None - if (BB_RANK_1 & self.bishops) << 56 != BB_RANK_8 & self.bishops: - return None - if (BB_RANK_1 & self.rooks) << 56 != BB_RANK_8 & self.rooks: - return None - if (BB_RANK_1 & self.queens) << 56 != BB_RANK_8 & self.queens: - return None - if (BB_RANK_1 & self.kings) << 56 != BB_RANK_8 & self.kings: + # Symmetry. + if any((BB_RANK_1 & pieces) << 56 != BB_RANK_8 & pieces for pieces in brnqk): return None + # Algorithm from ChessX, src/database/bitboard.cpp, r2254. x = self.bishops & (2 + 8 + 32 + 128) if not x: return None @@ -912,7 +1377,6 @@ def chess960_pos(self): bs2 = lsb(x) * 2 cc_pos += bs2 - # Algorithm from ChessX src/database/bitboard.cpp r2254. q = 0 qf = False n0 = 0 @@ -956,11 +1420,11 @@ def chess960_pos(self): else: return None - def __repr__(self): - return "{0}('{1}')".format(type(self).__name__, self.board_fen()) + def __repr__(self) -> str: + return f"{type(self).__name__}({self.board_fen()!r})" - def __str__(self): - builder = [] + def __str__(self) -> str: + builder: List[str] = [] for square in SQUARES_180: piece = self.piece_at(square) @@ -978,9 +1442,16 @@ def __str__(self): return "".join(builder) - def unicode(self, invert_color=False, borders=False): - builder = [] - for rank_index in range(7, -1, -1): + def unicode(self, *, invert_color: bool = False, borders: bool = False, empty_square: str = "⭘", orientation: Color = WHITE) -> str: + """ + Returns a string representation of the board with Unicode pieces. + Useful for pretty-printing to a terminal. + + :param invert_color: Invert color of the Unicode pieces. + :param borders: Show borders and a coordinate margin. + """ + builder: List[str] = [] + for rank_index in (range(7, -1, -1) if orientation else range(8)): if borders: builder.append(" ") builder.append("-" * 17) @@ -989,12 +1460,12 @@ def unicode(self, invert_color=False, borders=False): builder.append(RANK_NAMES[rank_index]) builder.append(" ") - for file_index in range(8): + for i, file_index in enumerate(range(8) if orientation else range(7, -1, -1)): square_index = square(file_index, rank_index) if borders: builder.append("|") - elif file_index > 0: + elif i > 0: builder.append(" ") piece = self.piece_at(square_index) @@ -1002,56 +1473,93 @@ def unicode(self, invert_color=False, borders=False): if piece: builder.append(piece.unicode_symbol(invert_color=invert_color)) else: - builder.append(".") + builder.append(empty_square) if borders: builder.append("|") - if borders or rank_index > 0: + if borders or (rank_index > 0 if orientation else rank_index < 7): builder.append("\n") if borders: builder.append(" ") builder.append("-" * 17) builder.append("\n") - builder.append(" a b c d e f g h") + letters = "a b c d e f g h" if orientation else "h g f e d c b a" + builder.append(" " + letters) return "".join(builder) - def _repr_svg_(self): + def _repr_svg_(self) -> str: import chess.svg return chess.svg.board(board=self, size=400) - def __eq__(self, board): - ne = self.__ne__(board) - return NotImplemented if ne is NotImplemented else not ne - - def __ne__(self, board): - try: - if self.occupied != board.occupied: - return True - elif self.occupied_co[WHITE] != board.occupied_co[WHITE]: - return True - elif self.pawns != board.pawns: - return True - elif self.knights != board.knights: - return True - elif self.bishops != board.bishops: - return True - elif self.rooks != board.rooks: - return True - elif self.queens != board.queens: - return True - elif self.kings != board.kings: - return True - else: - return False - except AttributeError: + def __eq__(self, board: object) -> bool: + if isinstance(board, BaseBoard): + return ( + self.occupied == board.occupied and + self.occupied_co[WHITE] == board.occupied_co[WHITE] and + self.pawns == board.pawns and + self.knights == board.knights and + self.bishops == board.bishops and + self.rooks == board.rooks and + self.queens == board.queens and + self.kings == board.kings) + else: return NotImplemented - def copy(self): - """Creates a copy of the board.""" - board = type(self)(None) + def apply_transform(self, f: Callable[[Bitboard], Bitboard]) -> None: + self.pawns = f(self.pawns) + self.knights = f(self.knights) + self.bishops = f(self.bishops) + self.rooks = f(self.rooks) + self.queens = f(self.queens) + self.kings = f(self.kings) + + self.occupied_co[WHITE] = f(self.occupied_co[WHITE]) + self.occupied_co[BLACK] = f(self.occupied_co[BLACK]) + self.occupied = f(self.occupied) + self.promoted = f(self.promoted) + + def transform(self, f: Callable[[Bitboard], Bitboard]) -> Self: + """ + Returns a transformed copy of the board (without move stack) + by applying a bitboard transformation function. + + Available transformations include :func:`chess.flip_vertical()`, + :func:`chess.flip_horizontal()`, :func:`chess.flip_diagonal()`, + :func:`chess.flip_anti_diagonal()`, :func:`chess.shift_down()`, + :func:`chess.shift_up()`, :func:`chess.shift_left()`, and + :func:`chess.shift_right()`. + + Alternatively, :func:`~chess.BaseBoard.apply_transform()` can be used + to apply the transformation on the board. + """ + board = self.copy() + board.apply_transform(f) + return board + + def apply_mirror(self) -> None: + self.apply_transform(flip_vertical) + self.occupied_co[WHITE], self.occupied_co[BLACK] = self.occupied_co[BLACK], self.occupied_co[WHITE] + + def mirror(self) -> Self: + """ + Returns a mirrored copy of the board (without move stack). + + The board is mirrored vertically and piece colors are swapped, so that + the position is equivalent modulo color. + + Alternatively, :func:`~chess.BaseBoard.apply_mirror()` can be used + to mirror the board. + """ + board = self.copy() + board.apply_mirror() + return board + + def copy(self) -> Self: + """Creates a copy of the board.""" + board = type(self)(None) board.pawns = self.pawns board.knights = self.knights @@ -1067,16 +1575,16 @@ def copy(self): return board - def __copy__(self): + def __copy__(self) -> Self: return self.copy() - def __deepcopy__(self, memo): + def __deepcopy__(self, memo: Dict[int, object]) -> Self: board = self.copy() memo[id(self)] = board return board @classmethod - def empty(cls): + def empty(cls: Type[BaseBoardT]) -> BaseBoardT: """ Creates a new empty board. Also see :func:`~chess.BaseBoard.clear_board()`. @@ -1084,19 +1592,25 @@ def empty(cls): return cls(None) @classmethod - def from_chess960_pos(cls, sharnagl): + def from_chess960_pos(cls: Type[BaseBoardT], scharnagl: int) -> BaseBoardT: """ - Creates a new board, initialized to a Chess960 starting position. - Also see :func:`~chess.BaseBoard.set_chess960_pos()`. + Creates a new board, initialized with a Chess960 starting position. + + >>> import chess + >>> import random + >>> + >>> board = chess.Board.from_chess960_pos(random.randint(0, 959)) """ board = cls.empty() - board.set_chess960_pos(sharnagl) + board.set_chess960_pos(scharnagl) return board -class _BoardState(object): +BoardT = TypeVar("BoardT", bound="Board") - def __init__(self, board): +class _BoardState: + + def __init__(self, board: Board) -> None: self.pawns = board.pawns self.knights = board.knights self.bishops = board.bishops @@ -1116,50 +1630,144 @@ def __init__(self, board): self.halfmove_clock = board.halfmove_clock self.fullmove_number = board.fullmove_number + def restore(self, board: Board) -> None: + board.pawns = self.pawns + board.knights = self.knights + board.bishops = self.bishops + board.rooks = self.rooks + board.queens = self.queens + board.kings = self.kings + + board.occupied_co[WHITE] = self.occupied_w + board.occupied_co[BLACK] = self.occupied_b + board.occupied = self.occupied + + board.promoted = self.promoted + + board.turn = self.turn + board.castling_rights = self.castling_rights + board.ep_square = self.ep_square + board.halfmove_clock = self.halfmove_clock + board.fullmove_number = self.fullmove_number class Board(BaseBoard): """ - A :class:`~chess.BaseBoard` and additional information representing - a chess position. + A :class:`~chess.BaseBoard`, additional information representing + a chess position, and a :data:`move stack `. - Provides move generation, validation, parsing, attack generation, - game end detection, move counters and the capability to make and unmake - moves. + Provides :data:`move generation `, validation, + :func:`parsing `, attack generation, + :func:`game end detection `, + and the capability to :func:`make ` and + :func:`unmake ` moves. The board is initialized to the standard chess starting position, unless otherwise specified in the optional *fen* argument. - If *fen* is ``None`` an empty board is created. + If *fen* is ``None``, an empty board is created. - Optionally supports *chess960*. In Chess960 castling moves are encoded + Optionally supports *chess960*. In Chess960, castling moves are encoded by a king move to the corresponding rook square. Use :func:`chess.Board.from_chess960_pos()` to create a board with one of the Chess960 starting positions. + + It's safe to set :data:`~Board.turn`, :data:`~Board.castling_rights`, + :data:`~Board.ep_square`, :data:`~Board.halfmove_clock` and + :data:`~Board.fullmove_number` directly. + + .. warning:: + It is possible to set up and work with invalid positions. In this + case, :class:`~chess.Board` implements a kind of "pseudo-chess" + (useful to gracefully handle errors or to implement chess variants). + Use :func:`~chess.Board.is_valid()` to detect invalid positions. + """ + + aliases: ClassVar[List[str]] = ["Standard", "Chess", "Classical", "Normal", "Illegal", "From Position"] + uci_variant: ClassVar[Optional[str]] = "chess" + xboard_variant: ClassVar[Optional[str]] = "normal" + starting_fen: ClassVar[str] = STARTING_FEN + + tbw_suffix: ClassVar[Optional[str]] = ".rtbw" + tbz_suffix: ClassVar[Optional[str]] = ".rtbz" + tbw_magic: ClassVar[Optional[bytes]] = b"\x71\xe8\x23\x5d" + tbz_magic: ClassVar[Optional[bytes]] = b"\xd7\x66\x0c\xa5" + pawnless_tbw_suffix: ClassVar[Optional[str]] = None + pawnless_tbz_suffix: ClassVar[Optional[str]] = None + pawnless_tbw_magic: ClassVar[Optional[bytes]] = None + pawnless_tbz_magic: ClassVar[Optional[bytes]] = None + connected_kings: ClassVar[bool] = False + one_king: ClassVar[bool] = True + captures_compulsory: ClassVar[bool] = False + + turn: Color + """The side to move (``chess.WHITE`` or ``chess.BLACK``).""" + + castling_rights: Bitboard + """ + Bitmask of the rooks with castling rights. + + To test for specific squares: + + >>> import chess + >>> + >>> board = chess.Board() + >>> bool(board.castling_rights & chess.BB_H1) # White can castle with the h1 rook + True + + To add a specific square: + + >>> board.castling_rights |= chess.BB_A1 + + Use :func:`~chess.Board.set_castling_fen()` to set multiple castling + rights. Also see :func:`~chess.Board.has_castling_rights()`, + :func:`~chess.Board.has_kingside_castling_rights()`, + :func:`~chess.Board.has_queenside_castling_rights()`, + :func:`~chess.Board.has_chess960_castling_rights()`, + :func:`~chess.Board.clean_castling_rights()`. + """ + + ep_square: Optional[Square] + """ + The potential en passant square on the third or sixth rank or ``None``. + + Use :func:`~chess.Board.has_legal_en_passant()` to test if en passant + capturing would actually be possible on the next move. + """ + + fullmove_number: int + """ + Counts move pairs. Starts at `1` and is incremented after every move + of the black side. + """ + + halfmove_clock: int + """The number of half-moves since the last capture or pawn move.""" + + promoted: Bitboard + """A bitmask of pieces that have been promoted.""" + + chess960: bool + """ + Whether the board is in Chess960 mode. In Chess960 castling moves are + represented as king moves to the corresponding rook square. """ - aliases = ["Standard", "Chess", "Classical", "Normal"] - uci_variant = "chess" - starting_fen = STARTING_FEN - - tbw_suffix = ".rtbw" - tbz_suffix = ".rtbz" - tbw_magic = [0x71, 0xE8, 0x23, 0x5D] - tbz_magic = [0xD7, 0x66, 0x0C, 0xA5] - pawnless_tbw_suffix = pawnless_tbz_suffix = None - pawnless_tbw_magic = pawnless_tbz_magic = None - connected_kings = False - one_king = True - captures_compulsory = False - - def __init__(self, fen=STARTING_FEN, chess960=False): + move_stack: List[Move] + """ + The move stack. Use :func:`Board.push() `, + :func:`Board.pop() `, + :func:`Board.peek() ` and + :func:`Board.clear_stack() ` for + manipulation. + """ + + def __init__(self, fen: Optional[str] = STARTING_FEN, *, chess960: bool = False) -> None: BaseBoard.__init__(self, None) self.chess960 = chess960 - self.pseudo_legal_moves = PseudoLegalMoveGenerator(self) - self.legal_moves = LegalMoveGenerator(self) - + self.ep_square = None self.move_stack = [] - self.stack = [] + self._stack: List[_BoardState] = [] if fen is None: self.clear() @@ -1168,57 +1776,115 @@ def __init__(self, fen=STARTING_FEN, chess960=False): else: self.set_fen(fen) - def reset(self): + @property + def legal_moves(self) -> LegalMoveGenerator: + """ + A dynamic list of legal moves. + + >>> import chess + >>> + >>> board = chess.Board() + >>> board.legal_moves.count() + 20 + >>> bool(board.legal_moves) + True + >>> move = chess.Move.from_uci("g1f3") + >>> move in board.legal_moves + True + + Wraps :func:`~chess.Board.generate_legal_moves()` and + :func:`~chess.Board.is_legal()`. + """ + return LegalMoveGenerator(self) + + @property + def pseudo_legal_moves(self) -> PseudoLegalMoveGenerator: + """ + A dynamic list of pseudo-legal moves, much like the legal move list. + + Pseudo-legal moves might leave or put the king in check, but are + otherwise valid. Null moves are not pseudo-legal. Castling moves are + only included if they are completely legal. + + Wraps :func:`~chess.Board.generate_pseudo_legal_moves()` and + :func:`~chess.Board.is_pseudo_legal()`. + """ + return PseudoLegalMoveGenerator(self) + + def reset(self) -> None: """Restores the starting position.""" self.turn = WHITE - self.castling_rights = BB_A1 | BB_H1 | BB_A8 | BB_H8 + self.castling_rights = BB_CORNERS self.ep_square = None self.halfmove_clock = 0 self.fullmove_number = 1 self.reset_board() - def reset_board(self): - super(Board, self).reset_board() + def reset_board(self) -> None: + super().reset_board() self.clear_stack() - def clear(self): + def clear(self) -> None: """ Clears the board. - Resets move stacks and move counters. The side to move is white. There + Resets move stack and move counters. The side to move is white. There are no rooks or kings, so castling rights are removed. - In order to be in a valid :func:`~chess.Board.status()` at least kings + In order to be in a valid :func:`~chess.Board.status()`, at least kings need to be put on the board. """ self.turn = WHITE - self.castling_rights = BB_VOID + self.castling_rights = BB_EMPTY self.ep_square = None self.halfmove_clock = 0 self.fullmove_number = 1 self.clear_board() - def clear_board(self): - super(Board, self).clear_board() + def clear_board(self) -> None: + super().clear_board() self.clear_stack() - def clear_stack(self): + def clear_stack(self) -> None: """Clears the move stack.""" - del self.move_stack[:] - del self.stack[:] + self.move_stack.clear() + self._stack.clear() + + def root(self) -> Self: + """Returns a copy of the root position.""" + if self._stack: + board = type(self)(None, chess960=self.chess960) + self._stack[0].restore(board) + return board + else: + return self.copy(stack=False) + + def ply(self) -> int: + """ + Returns the number of half-moves since the start of the game, as + indicated by :data:`~chess.Board.fullmove_number` and + :data:`~chess.Board.turn`. + + If moves have been pushed from the beginning, this is usually equal to + ``len(board.move_stack)``. But note that a board can be set up with + arbitrary starting positions, and the stack can be cleared. + """ + return 2 * (self.fullmove_number - 1) + (self.turn == BLACK) - def remove_piece_at(self, square): - piece = super(Board, self).remove_piece_at(square) + def remove_piece_at(self, square: Square) -> Optional[Piece]: + """Remove a piece, if any, from the given square and return the removed piece.""" + piece = super().remove_piece_at(square) self.clear_stack() return piece - def set_piece_at(self, square, piece): - super(Board, self).set_piece_at(square, piece) + def set_piece_at(self, square: Square, piece: Optional[Piece], promoted: bool = False) -> None: + """Place a piece on a square.""" + super().set_piece_at(square, piece, promoted=promoted) self.clear_stack() - def generate_pseudo_legal_moves(self, from_mask=BB_ALL, to_mask=BB_ALL): + def generate_pseudo_legal_moves(self, from_mask: Bitboard = BB_ALL, to_mask: Bitboard = BB_ALL) -> Iterator[Move]: our_pieces = self.occupied_co[self.turn] # Generate piece moves. @@ -1230,8 +1896,7 @@ def generate_pseudo_legal_moves(self, from_mask=BB_ALL, to_mask=BB_ALL): # Generate castling moves. if from_mask & self.kings: - for move in self.generate_castling_moves(from_mask, to_mask): - yield move + yield from self.generate_castling_moves(from_mask, to_mask) # The remaining moves are all pawn moves. pawns = self.pawns & self.occupied_co[self.turn] & from_mask @@ -1246,7 +1911,7 @@ def generate_pseudo_legal_moves(self, from_mask=BB_ALL, to_mask=BB_ALL): self.occupied_co[not self.turn] & to_mask) for to_square in scan_reversed(targets): - if square_rank(to_square) in [0, 7]: + if square_rank(to_square) in [RANK_1, RANK_8]: yield Move(from_square, to_square, QUEEN) yield Move(from_square, to_square, ROOK) yield Move(from_square, to_square, BISHOP) @@ -1269,7 +1934,7 @@ def generate_pseudo_legal_moves(self, from_mask=BB_ALL, to_mask=BB_ALL): for to_square in scan_reversed(single_moves): from_square = to_square + (8 if self.turn == BLACK else -8) - if square_rank(to_square) in [0, 7]: + if square_rank(to_square) in [RANK_1, RANK_8]: yield Move(from_square, to_square, QUEEN) yield Move(from_square, to_square, ROOK) yield Move(from_square, to_square, BISHOP) @@ -1284,187 +1949,88 @@ def generate_pseudo_legal_moves(self, from_mask=BB_ALL, to_mask=BB_ALL): # Generate en passant captures. if self.ep_square: - for move in self.generate_pseudo_legal_ep(from_mask, to_mask): - yield move + yield from self.generate_pseudo_legal_ep(from_mask, to_mask) - def generate_pseudo_legal_ep(self, from_mask=BB_ALL, to_mask=BB_ALL): + def generate_pseudo_legal_ep(self, from_mask: Bitboard = BB_ALL, to_mask: Bitboard = BB_ALL) -> Iterator[Move]: if not self.ep_square or not BB_SQUARES[self.ep_square] & to_mask: return + if BB_SQUARES[self.ep_square] & self.occupied: + return + capturers = ( self.pawns & self.occupied_co[self.turn] & from_mask & - BB_PAWN_ATTACKS[not self.turn][self.ep_square]) + BB_PAWN_ATTACKS[not self.turn][self.ep_square] & + BB_RANKS[RANK_5 if self.turn else RANK_4]) for capturer in scan_reversed(capturers): yield Move(capturer, self.ep_square) - def generate_pseudo_legal_captures(self, from_mask=BB_ALL, to_mask=BB_ALL): + def generate_pseudo_legal_captures(self, from_mask: Bitboard = BB_ALL, to_mask: Bitboard = BB_ALL) -> Iterator[Move]: return itertools.chain( self.generate_pseudo_legal_moves(from_mask, to_mask & self.occupied_co[not self.turn]), self.generate_pseudo_legal_ep(from_mask, to_mask)) - def _attackers_mask(self, color, square, occupied): - rank_pieces = BB_RANK_MASKS[square] & occupied - file_pieces = BB_FILE_MASKS[square] & occupied - diag_pieces = BB_DIAG_MASKS[square] & occupied - - queens_and_rooks = self.queens | self.rooks - queens_and_bishops = self.queens | self.bishops - - attackers = ( - (BB_KING_ATTACKS[square] & self.kings) | - (BB_KNIGHT_ATTACKS[square] & self.knights) | - (BB_RANK_ATTACKS[square][rank_pieces] & queens_and_rooks) | - (BB_FILE_ATTACKS[square][file_pieces] & queens_and_rooks) | - (BB_DIAG_ATTACKS[square][diag_pieces] & queens_and_bishops) | - (BB_PAWN_ATTACKS[not color][square] & self.pawns)) - - return attackers & self.occupied_co[color] - - def attackers_mask(self, color, square): - return self._attackers_mask(color, square, self.occupied) - - def is_attacked_by(self, color, square): - """ - Checks if the given side attacks the given square. - - Pinned pieces still count as attackers. Pawns that can be captured - en passant are **not** considered attacked. - """ - return bool(self.attackers_mask(color, square)) + def checkers_mask(self) -> Bitboard: + king = self.king(self.turn) + return BB_EMPTY if king is None else self.attackers_mask(not self.turn, king) - def attackers(self, color, square): + def checkers(self) -> SquareSet: """ - Gets a set of attackers of the given color for the given square. - - Pinned pieces still count as attackers. + Gets the pieces currently giving check. Returns a :class:`set of squares `. """ - return SquareSet(self.attackers_mask(color, square)) - - def attacks_mask(self, square): - bb_square = BB_SQUARES[square] - - if bb_square & self.pawns: - if bb_square & self.occupied_co[WHITE]: - return BB_PAWN_ATTACKS[WHITE][square] - else: - return BB_PAWN_ATTACKS[BLACK][square] - elif bb_square & self.knights: - return BB_KNIGHT_ATTACKS[square] - elif bb_square & self.kings: - return BB_KING_ATTACKS[square] - else: - attacks = 0 - if bb_square & self.bishops or bb_square & self.queens: - attacks = BB_DIAG_ATTACKS[square][BB_DIAG_MASKS[square] & self.occupied] - if bb_square & self.rooks or bb_square & self.queens: - attacks |= (BB_RANK_ATTACKS[square][BB_RANK_MASKS[square] & self.occupied] | - BB_FILE_ATTACKS[square][BB_FILE_MASKS[square] & self.occupied]) - return attacks - - def attacks(self, square): - """ - Gets a set of attacked squares from a given square. + return SquareSet(self.checkers_mask()) - There will be no attacks if the square is empty. Pinned pieces are - still attacking other squares. + def is_check(self) -> bool: + """Tests if the current side to move is in check.""" + return bool(self.checkers_mask()) - Returns a :class:`set of squares `. + def gives_check(self, move: Move) -> bool: """ - return SquareSet(self.attacks_mask(square)) - - def pin_mask(self, color, square): - king = self.king(color) - if king is None: - return BB_ALL - - square_mask = BB_SQUARES[square] - - for attacks, sliders in [(BB_FILE_ATTACKS, self.rooks | self.queens), - (BB_RANK_ATTACKS, self.rooks | self.queens), - (BB_DIAG_ATTACKS, self.bishops | self.queens)]: - rays = attacks[king][0] - if rays & square_mask: - snipers = rays & sliders & self.occupied_co[not color] - for sniper in scan_reversed(snipers): - if BB_BETWEEN[sniper][king] & (self.occupied | square_mask) == square_mask: - return BB_RAYS[king][sniper] - - break - - return BB_ALL - - def pin(self, color, square): + Probes if the given move would put the opponent in check. The move + must be at least pseudo-legal. """ - Detects an absolute pin (and its direction) of the given square to - the king of the given color. - - >>> board = chess.Board("rnb1k2r/ppp2ppp/5n2/3q4/1b1P4/2N5/PP3PPP/R1BQKBNR w KQkq - 3 7") - >>> board.is_pinned(chess.WHITE, chess.C3) - True - >>> direction = board.pin(chess.WHITE, chess.C3) - >>> direction - SquareSet(0x0000000102040810) - >>> print(direction) - . . . . . . . . - . . . . . . . . - . . . . . . . . - 1 . . . . . . . - . 1 . . . . . . - . . 1 . . . . . - . . . 1 . . . . - . . . . 1 . . . - - Returns a :class:`set of squares ` that mask the rank, - file or diagonal of the pin. If there is no pin, then a mask of the - entire board is returned. - """ - return SquareSet(self.pin_mask(color, square)) + self.push(move) + try: + return self.is_check() + finally: + self.pop() - def is_pinned(self, color, square): + def gives_checkmate(self, move: Move) -> bool: """ - Detects if the given square is pinned to the king of the given color. + Probes if the given move would put the opponent in checkmate. The move + must be at least pseudo-legal. """ - return self.pin_mask(color, square) != BB_ALL - - def is_check(self): - """Returns if the current side to move is in check.""" - king = self.king(self.turn) - return king is not None and self.is_attacked_by(not self.turn, king) + self.push(move) + try: + return self.is_checkmate() + finally: + self.pop() - def is_into_check(self, move): - """ - Checks if the given move would leave the king in check or put it into - check. The move must be at least pseudo legal. - """ + def is_into_check(self, move: Move) -> bool: king = self.king(self.turn) if king is None: return False + # If already in check, look if it is an evasion. checkers = self.attackers_mask(not self.turn, king) - if checkers: - # If already in check, look if it is an evasion. - if move not in self._generate_evasions(king, checkers, BB_SQUARES[move.from_square], BB_SQUARES[move.to_square]): - return True + if checkers and move not in self._generate_evasions(king, checkers, BB_SQUARES[move.from_square], BB_SQUARES[move.to_square]): + return True return not self._is_safe(king, self._slider_blockers(king), move) - def was_into_check(self): - """ - Checks if the king of the other side is attacked. Such a position is not - valid and could only be reached by an illegal move. - """ + def was_into_check(self) -> bool: king = self.king(not self.turn) return king is not None and self.is_attacked_by(self.turn, king) - def is_pseudo_legal(self, move): - # Null moves are not pseudo legal. + def is_pseudo_legal(self, move: Move) -> bool: + # Null moves are not pseudo-legal. if not move: return False - # Drops are not pseudo legal. + # Drops are not pseudo-legal. if move.drop: return False @@ -1486,13 +2052,14 @@ def is_pseudo_legal(self, move): if piece != PAWN: return False - if self.turn == WHITE and square_rank(move.to_square) != 7: + if self.turn == WHITE and square_rank(move.to_square) != RANK_8: return False - elif self.turn == BLACK and square_rank(move.to_square) != 0: + elif self.turn == BLACK and square_rank(move.to_square) != RANK_1: return False # Handle castling. if piece == KING: + move = self._from_chess960(self.chess960, move.from_square, move.to_square) if move in self.generate_castling_moves(): return True @@ -1507,14 +2074,15 @@ def is_pseudo_legal(self, move): # Handle all other pieces. return bool(self.attacks_mask(move.from_square) & to_mask) - def is_legal(self, move): + def is_legal(self, move: Move) -> bool: + """Check if a move is legal in the current position.""" return not self.is_variant_end() and self.is_pseudo_legal(move) and not self.is_into_check(move) - def is_variant_end(self): + def is_variant_end(self) -> bool: """ Checks if the game is over due to a special variant end condition. - Note for example that stalemate is not considered a variant-specific + Note, for example, that stalemate is not considered a variant-specific end condition (this method will return ``False``), yet it can have a special **result** in suicide chess (any of :func:`~chess.Board.is_variant_loss()`, @@ -1523,104 +2091,111 @@ def is_variant_end(self): """ return False - def is_variant_loss(self): - """Checks if a special variant-specific loss condition is fulfilled.""" + def is_variant_loss(self) -> bool: + """ + Checks if the current side to move lost due to a variant-specific + condition. + """ return False - def is_variant_win(self): - """Checks if a special variant-specific win condition is fulfilled.""" + def is_variant_win(self) -> bool: + """ + Checks if the current side to move won due to a variant-specific + condition. + """ return False - def is_variant_draw(self): + def is_variant_draw(self) -> bool: """ - Checks if a special variant-specific drawing condition is fulfilled. + Checks if a variant-specific drawing condition is fulfilled. """ return False - def is_game_over(self, claim_draw=False): + def is_game_over(self, *, claim_draw: bool = False) -> bool: + """ + Check if the game is over by any rule. + + The game is not considered to be over by the + :func:`fifty-move rule ` or + :func:`threefold repetition `, + unless *claim_draw* is given. Note that checking the latter can be + slow. + """ + return self.outcome(claim_draw=claim_draw) is not None + + def result(self, *, claim_draw: bool = False) -> str: + """ + Return the result of a game: 1-0, 0-1, 1/2-1/2, or *. + + The game is not considered to be over by the + :func:`fifty-move rule ` or + :func:`threefold repetition `, + unless *claim_draw* is given. Note that checking the latter can be + slow. + """ + outcome = self.outcome(claim_draw=claim_draw) + return outcome.result() if outcome else "*" + + def outcome(self, *, claim_draw: bool = False) -> Optional[Outcome]: """ Checks if the game is over due to :func:`checkmate `, :func:`stalemate `, :func:`insufficient material `, the :func:`seventyfive-move rule `, - :func:`fivefold repetition ` + :func:`fivefold repetition `, or a :func:`variant end condition `. + Returns the :class:`chess.Outcome` if the game has ended, otherwise + ``None``. - The game is not considered to be over by - :func:`threefold repetition ` - or the :func:`fifty-move rule `, - unless *claim_draw* is given. - """ - # Seventyfive-move rule. - if self.is_seventyfive_moves(): - return True - - # Insufficient material. - if self.is_insufficient_material(): - return True - - # Stalemate or checkmate. - if not any(self.generate_legal_moves()): - return True - - # Fivefold repetition. - if self.is_fivefold_repetition(): - return True - - # Draw claim. - if claim_draw and self.can_claim_draw(): - return True + Alternatively, use :func:`~chess.Board.is_game_over()` if you are not + interested in who won the game and why. - return False - - def result(self, claim_draw=False): - """ - Gets the game result. - - ``1-0``, ``0-1`` or ``1/2-1/2`` if the - :func:`game is over `. Otherwise the result - is undetermined: ``*``. + The game is not considered to be over by the + :func:`fifty-move rule ` or + :func:`threefold repetition `, + unless *claim_draw* is given. Note that checking the latter can be + slow. """ - # Chess variant support + # Variant support. if self.is_variant_loss(): - return "0-1" if self.turn == WHITE else "1-0" - elif self.is_variant_win(): - return "1-0" if self.turn == WHITE else "0-1" - elif self.is_variant_draw(): - return "1/2-1/2" + return Outcome(Termination.VARIANT_LOSS, not self.turn) + if self.is_variant_win(): + return Outcome(Termination.VARIANT_WIN, self.turn) + if self.is_variant_draw(): + return Outcome(Termination.VARIANT_DRAW, None) - # Checkmate. + # Normal game end. if self.is_checkmate(): - return "0-1" if self.turn == WHITE else "1-0" - - # Draw claimed. - if claim_draw and self.can_claim_draw(): - return "1/2-1/2" - - # Seventyfive-move rule or fivefold repetition. - if self.is_seventyfive_moves() or self.is_fivefold_repetition(): - return "1/2-1/2" - - # Insufficient material. + return Outcome(Termination.CHECKMATE, not self.turn) if self.is_insufficient_material(): - return "1/2-1/2" - - # Stalemate. + return Outcome(Termination.INSUFFICIENT_MATERIAL, None) if not any(self.generate_legal_moves()): - return "1/2-1/2" + return Outcome(Termination.STALEMATE, None) + + # Automatic draws. + if self.is_seventyfive_moves(): + return Outcome(Termination.SEVENTYFIVE_MOVES, None) + if self.is_fivefold_repetition(): + return Outcome(Termination.FIVEFOLD_REPETITION, None) + + # Claimable draws. + if claim_draw: + if self.can_claim_fifty_moves(): + return Outcome(Termination.FIFTY_MOVES, None) + if self.can_claim_threefold_repetition(): + return Outcome(Termination.THREEFOLD_REPETITION, None) - # Undetermined. - return "*" + return None - def is_checkmate(self): + def is_checkmate(self) -> bool: """Checks if the current position is a checkmate.""" if not self.is_check(): return False return not any(self.generate_legal_moves()) - def is_stalemate(self): + def is_stalemate(self) -> bool: """Checks if the current position is a stalemate.""" if self.is_check(): return False @@ -1630,103 +2205,127 @@ def is_stalemate(self): return not any(self.generate_legal_moves()) - def is_insufficient_material(self): - """Checks for a draw due to insufficient mating material.""" - # Enough material to mate. - if self.pawns or self.rooks or self.queens: - return False - - # A single knight or a single bishop. - if popcount(self.occupied) <= 3: - return True - - # More than a single knight. - if self.knights: - return False - - # All bishops on the same color. - if self.bishops & BB_DARK_SQUARES == 0: - return True - elif self.bishops & BB_LIGHT_SQUARES == 0: - return True - else: - return False - - def is_seventyfive_moves(self): + def is_insufficient_material(self) -> bool: """ - Since the first of July 2014 a game is automatically drawn (without - a claim by one of the players) if the half move clock since a capture - or pawn move is equal to or grather than 150. Other means to end a game - take precedence. + Checks if neither side has sufficient winning material + (:func:`~chess.Board.has_insufficient_material()`). """ - if self.halfmove_clock >= 150: - if any(self.generate_legal_moves()): - return True + return all(self.has_insufficient_material(color) for color in COLORS) - return False - - def is_fivefold_repetition(self): - """ - Since the first of July 2014 a game is automatically drawn (without - a claim by one of the players) if a position occurs for the fifth time - on consecutive alternating moves. + def has_insufficient_material(self, color: Color) -> bool: """ - transposition_key = self._transposition_key() + Checks if *color* has insufficient winning material. - if len(self.move_stack) < 16: - return False + This is guaranteed to return ``False`` if *color* can still win the + game. - switchyard = collections.deque() + The converse does not necessarily hold: + The implementation only looks at the material, including the colors + of bishops, but not considering piece positions. So fortress + positions or positions with forced lines may return ``False``, even + though there is no possible winning line. + """ + if self.occupied_co[color] & (self.pawns | self.rooks | self.queens): + return False - for _ in range(4): - # Go back two full moves, each. - for _ in range(4): - switchyard.append(self.pop()) + # Knights are only insufficient material if: + # (1) We do not have any other pieces, including more than one knight. + # (2) The opponent does not have pawns, knights, bishops or rooks. + # These would allow selfmate. + if self.occupied_co[color] & self.knights: + return (popcount(self.occupied_co[color]) <= 2 and + not (self.occupied_co[not color] & ~self.kings & ~self.queens)) + + # Bishops are only insufficient material if: + # (1) We do not have any other pieces, including bishops of the + # opposite color. + # (2) The opponent does not have bishops of the opposite color, + # pawns or knights. These would allow selfmate. + if self.occupied_co[color] & self.bishops: + same_color = (not self.bishops & BB_DARK_SQUARES) or (not self.bishops & BB_LIGHT_SQUARES) + return same_color and not self.pawns and not self.knights - # Check the position was the same before. - if self._transposition_key() != transposition_key: - while switchyard: - self.push(switchyard.pop()) + return True - return False + def _is_halfmoves(self, n: int) -> bool: + return self.halfmove_clock >= n and any(self.generate_legal_moves()) - while switchyard: - self.push(switchyard.pop()) + def is_seventyfive_moves(self) -> bool: + """ + Since the 1st of July 2014, a game is automatically drawn (without + a claim by one of the players) if the half-move clock since a capture + or pawn move is equal to or greater than 150. Other means to end a game + take precedence. + """ + return self._is_halfmoves(150) - return True + def is_fivefold_repetition(self) -> bool: + """ + Since the 1st of July 2014 a game is automatically drawn (without + a claim by one of the players) if a position occurs for the fifth time. + Originally this had to occur on consecutive alternating moves, but + this has since been revised. + """ + return self.is_repetition(5) - def can_claim_draw(self): + def can_claim_draw(self) -> bool: """ - Checks if the side to move can claim a draw by the fifty-move rule or + Checks if the player to move can claim a draw by the fifty-move rule or by threefold repetition. + + Note that checking the latter can be slow. """ return self.can_claim_fifty_moves() or self.can_claim_threefold_repetition() - def can_claim_fifty_moves(self): + def is_fifty_moves(self) -> bool: """ - Draw by the fifty-move rule can be claimed once the clock of halfmoves - since the last capture or pawn move becomes equal or greater to 100 - and the side to move still has a legal move they can make. + Checks that the clock of halfmoves since the last capture or pawn move + is greater or equal to 100, and that no other means of ending the game + (like checkmate) take precedence. """ - # Fifty-move rule. - if self.halfmove_clock >= 100: - if any(self.generate_legal_moves()): - return True + return self._is_halfmoves(100) + + def can_claim_fifty_moves(self) -> bool: + """ + Checks if the player to move can claim a draw by the fifty-move rule. + + In addition to :func:`~chess.Board.is_fifty_moves()`, the fifty-move + rule can also be claimed if there is a legal move that achieves this + condition. + """ + if self.is_fifty_moves(): + return True + + if self.halfmove_clock >= 99: + for move in self.generate_legal_moves(): + if not self.is_zeroing(move): + self.push(move) + try: + if self.is_fifty_moves(): + return True + finally: + self.pop() return False - def can_claim_threefold_repetition(self): + def can_claim_threefold_repetition(self) -> bool: """ + Checks if the player to move can claim a draw by threefold repetition. + Draw by threefold repetition can be claimed if the position on the - board occured for the third time or if such a repetition is reached + board occurred for the third time or if such a repetition is reached with one of the possible legal moves. + + Note that checking this can be slow: In the worst case + scenario, every legal move has to be tested and the entire game has to + be replayed because there is no incremental transposition table. """ transposition_key = self._transposition_key() - transpositions = collections.Counter() + transpositions: Counter[Hashable] = collections.Counter() transpositions.update((transposition_key, )) # Count positions. - switchyard = collections.deque() + switchyard: List[Move] = [] while self.move_stack: move = self.pop() switchyard.append(move) @@ -1739,30 +2338,82 @@ def can_claim_threefold_repetition(self): while switchyard: self.push(switchyard.pop()) - # Threefold repetition occured. + # Threefold repetition occurred. if transpositions[transposition_key] >= 3: return True # The next legal move is a threefold repetition. for move in self.generate_legal_moves(): self.push(move) - - if transpositions[self._transposition_key()] >= 2: + try: + if transpositions[self._transposition_key()] >= 2: + return True + finally: self.pop() - return True - self.pop() + return False + + def is_repetition(self, count: int = 3) -> bool: + """ + Checks if the current position has repeated 3 (or a given number of) + times. + + Unlike :func:`~chess.Board.can_claim_threefold_repetition()`, + this does not consider a repetition that can be played on the next + move. + + Note that checking this can be slow: In the worst case, the entire + game has to be replayed because there is no incremental transposition + table. + """ + # Fast check, based on occupancy only. + maybe_repetitions = 1 + for state in reversed(self._stack): + if state.occupied == self.occupied: + maybe_repetitions += 1 + if maybe_repetitions >= count: + break + if maybe_repetitions < count: + return False + + # Check full replay. + transposition_key = self._transposition_key() + switchyard: List[Move] = [] + + try: + while True: + if count <= 1: + return True + + if len(self.move_stack) < count - 1: + break + + move = self.pop() + switchyard.append(move) + + if self.is_irreversible(move): + break + + if self._transposition_key() == transposition_key: + count -= 1 + finally: + while switchyard: + self.push(switchyard.pop()) return False - def _push_capture(self, move, capture_square, piece_type, was_promoted): + def _push_capture(self, move: Move, capture_square: Square, piece_type: PieceType, was_promoted: bool) -> None: pass - def push(self, move): + def push(self, move: Move) -> None: """ - Updates the position with the given move and puts it onto the + Updates the position with the given *move* and puts it onto the move stack. + >>> import chess + >>> + >>> board = chess.Board() + >>> >>> Nf3 = chess.Move.from_uci("g1f3") >>> board.push(Nf3) # Make the move @@ -1772,15 +2423,19 @@ def push(self, move): Null moves just increment the move counters, switch turns and forfeit en passant capturing. - :warning: Moves are not checked for legality. + .. warning:: + Moves are not checked for legality. It is the caller's + responsibility to ensure that the move is at least pseudo-legal or + a null move. """ - # Remember game state. - self.stack.append(_BoardState(self)) - self.move_stack.append(move) - + # Push move and remember board state. move = self._to_chess960(move) + board_state = _BoardState(self) + self.castling_rights = self.clean_castling_rights() # Before pushing stack + self.move_stack.append(self._from_chess960(self.chess960, move.from_square, move.to_square, move.promotion, move.drop)) + self._stack.append(board_state) - # Reset ep square. + # Reset en passant square. ep_square = self.ep_square self.ep_square = None @@ -1789,7 +2444,7 @@ def push(self, move): if self.turn == BLACK: self.fullmove_number += 1 - # On a null move simply swap turns and reset the en passant square. + # On a null move, simply swap turns and reset the en passant square. if not move: self.turn = not self.turn return @@ -1800,43 +2455,44 @@ def push(self, move): self.turn = not self.turn return - # Zero the half move clock. + # Zero the half-move clock. if self.is_zeroing(move): self.halfmove_clock = 0 from_bb = BB_SQUARES[move.from_square] to_bb = BB_SQUARES[move.to_square] - promoted = self.promoted & from_bb + promoted = bool(self.promoted & from_bb) piece_type = self._remove_piece_at(move.from_square) + assert piece_type is not None, f"push() expects move to be pseudo-legal, but got {move} in {self.board_fen()}" capture_square = move.to_square captured_piece_type = self.piece_type_at(capture_square) # Update castling rights. - self.castling_rights = self.clean_castling_rights() & ~to_bb & ~from_bb - if piece_type == KING and not promoted: + self.castling_rights &= ~to_bb & ~from_bb + if piece_type == KING and not self._effective_promoted() & from_bb: if self.turn == WHITE: self.castling_rights &= ~BB_RANK_1 else: self.castling_rights &= ~BB_RANK_8 - elif captured_piece_type == KING and not self.promoted & to_bb: - if self.turn == WHITE and square_rank(move.to_square) == 7: + elif captured_piece_type == KING and not self._effective_promoted() & to_bb: + if self.turn == WHITE and square_rank(move.to_square) == RANK_8: self.castling_rights &= ~BB_RANK_8 - elif self.turn == BLACK and square_rank(move.to_square) == 0: + elif self.turn == BLACK and square_rank(move.to_square) == RANK_1: self.castling_rights &= ~BB_RANK_1 # Handle special pawn moves. if piece_type == PAWN: diff = move.to_square - move.from_square - if diff == 16 and square_rank(move.from_square) == 1: + if diff == 16 and square_rank(move.from_square) == RANK_2: self.ep_square = move.from_square + 8 - elif diff == -16 and square_rank(move.from_square) == 6: + elif diff == -16 and square_rank(move.from_square) == RANK_7: self.ep_square = move.from_square - 8 elif move.to_square == ep_square and abs(diff) in [7, 9] and not captured_piece_type: # Remove pawns captured en passant. down = -8 if self.turn == WHITE else 8 - capture_square = ep_square + down + capture_square = move.to_square + down captured_piece_type = self._remove_piece_at(capture_square) # Promotion. @@ -1859,9 +2515,9 @@ def push(self, move): self._set_piece_at(G1 if self.turn == WHITE else G8, KING, self.turn) self._set_piece_at(F1 if self.turn == WHITE else F8, ROOK, self.turn) - # Put piece on target square. - if not castling and piece_type: - was_promoted = self.promoted & to_bb + # Put the piece on the target square. + if not castling: + was_promoted = bool(self.promoted & to_bb) self._set_piece_at(move.to_square, piece_type, self.turn, promoted) if captured_piece_type: @@ -1870,37 +2526,17 @@ def push(self, move): # Swap turn. self.turn = not self.turn - def pop(self): + def pop(self) -> Move: """ Restores the previous position and returns the last move from the stack. - :raises: :exc:`IndexError` if the stack is empty. + :raises: :exc:`IndexError` if the move stack is empty. """ move = self.move_stack.pop() - state = self.stack.pop() - - self.pawns = state.pawns - self.knights = state.knights - self.bishops = state.bishops - self.rooks = state.rooks - self.queens = state.queens - self.kings = state.kings - - self.occupied_co[WHITE] = state.occupied_w - self.occupied_co[BLACK] = state.occupied_b - self.occupied = state.occupied - - self.promoted = state.promoted - - self.turn = state.turn - self.castling_rights = state.castling_rights - self.ep_square = state.ep_square - self.halfmove_clock = state.halfmove_clock - self.fullmove_number = state.fullmove_number - + self._stack.pop().restore(self) return move - def peek(self): + def peek(self) -> Move: """ Gets the last move from the move stack. @@ -1908,12 +2544,34 @@ def peek(self): """ return self.move_stack[-1] - def castling_shredder_fen(self): + def find_move(self, from_square: Square, to_square: Square, promotion: Optional[PieceType] = None) -> Move: + """ + Finds a matching legal move for an origin square, a target square, and + an optional promotion piece type. + + For pawn moves to the backrank, the promotion piece type defaults to + :data:`chess.QUEEN`, unless otherwise specified. + + Castling moves are normalized to king moves by two steps, except in + Chess960. + + :raises: :exc:`IllegalMoveError` if no matching legal move is found. + """ + if promotion is None and self.pawns & BB_SQUARES[from_square] and BB_SQUARES[to_square] & BB_BACKRANKS: + promotion = QUEEN + + move = self._from_chess960(self.chess960, from_square, to_square, promotion) + if not self.is_legal(move): + raise IllegalMoveError(f"no matching legal move for {move.uci()} ({SQUARE_NAMES[from_square]} -> {SQUARE_NAMES[to_square]}) in {self.fen()}") + + return move + + def castling_shredder_fen(self) -> str: castling_rights = self.clean_castling_rights() if not castling_rights: return "-" - builder = [] + builder: List[str] = [] for square in scan_reversed(castling_rights & BB_RANK_1): builder.append(FILE_NAMES[square_file(square)].upper()) @@ -1923,8 +2581,8 @@ def castling_shredder_fen(self): return "".join(builder) - def castling_xfen(self): - builder = [] + def castling_xfen(self) -> str: + builder: List[str] = [] for color in COLORS: king = self.king(color) @@ -1952,121 +2610,156 @@ def castling_xfen(self): else: return "-" - def has_legal_en_passant(self): + def has_pseudo_legal_en_passant(self) -> bool: + """Checks if there is a pseudo-legal en passant capture.""" + return self.ep_square is not None and any(self.generate_pseudo_legal_ep()) + + def has_legal_en_passant(self) -> bool: """Checks if there is a legal en passant capture.""" return self.ep_square is not None and any(self.generate_legal_ep()) - def fen(self, promoted=None): + def fen(self, *, shredder: bool = False, en_passant: EnPassantSpec = "legal", promoted: Optional[bool] = None) -> str: """ - Gets the FEN representation of the position. + Gets a FEN representation of the position. - A FEN string (e.g. + A FEN string (e.g., ``rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1``) consists - of the position part (:func:`~chess.Board.board_fen()`), the turn, - the castling part (:func:`~chess.Board.castling_xfen()`), a relevant - en passant square (:data:`~chess.Board.ep_square`, - :func:`~chess.Board.has_legal_en_passant()`), the halfmove clock - and the fullmove number. - - Optionally designates *promoted* pieces with a ``~`` before - their symbol. + of the board part :func:`~chess.Board.board_fen()`, the + :data:`~chess.Board.turn`, the castling part + (:data:`~chess.Board.castling_rights`), + the en passant square (:data:`~chess.Board.ep_square`), + the :data:`~chess.Board.halfmove_clock` + and the :data:`~chess.Board.fullmove_number`. + + :param shredder: Use :func:`~chess.Board.castling_shredder_fen()` + and encode castling rights by the file of the rook + (like ``HAha``) instead of the default + :func:`~chess.Board.castling_xfen()` (like ``KQkq``). + :param en_passant: By default, only fully legal en passant squares + are included (:func:`~chess.Board.has_legal_en_passant()`). + Pass ``fen`` to strictly follow the FEN specification + (always include the en passant square after a two-step pawn move) + or ``xfen`` to follow the X-FEN specification + (:func:`~chess.Board.has_pseudo_legal_en_passant()`). + :param promoted: Mark promoted pieces like ``Q~``. By default, this is + only enabled in chess variants where this is relevant. """ return " ".join([ - self.epd(promoted=promoted), + self.epd(shredder=shredder, en_passant=en_passant, promoted=promoted), str(self.halfmove_clock), str(self.fullmove_number) ]) - def shredder_fen(self, promoted=None): - """ - Gets the Shredder FEN representation of the position. - - Castling rights are encoded by the file of the rook. The starting - castling rights in normal chess are HAha. - - Use :func:`~chess.Board.castling_shredder_fen()` to get just the - castling part. - - Optionally designates *promoted* pieces with a ``~`` before - their symbol. - """ + def shredder_fen(self, *, en_passant: EnPassantSpec = "legal", promoted: Optional[bool] = None) -> str: return " ".join([ - self.epd(shredder_fen=True, promoted=promoted), + self.epd(shredder=True, en_passant=en_passant, promoted=promoted), str(self.halfmove_clock), str(self.fullmove_number) ]) - def set_fen(self, fen): + def set_fen(self, fen: str) -> None: """ Parses a FEN and sets the position from it. - :raises: :exc:`ValueError` if the FEN string is invalid. + :raises: :exc:`ValueError` if syntactically invalid. Use + :func:`~chess.Board.is_valid()` to detect invalid positions. """ - # Ensure there are six parts. parts = fen.split() - if len(parts) != 6: - raise ValueError("fen string should consist of 6 parts: {0}".format(repr(fen))) - - # Check that the turn part is valid. - if not parts[1] in ["w", "b"]: - raise ValueError("expected 'w' or 'b' for turn part of fen: {0}".format(repr(fen))) - - # Check that the castling part is valid. - if not FEN_CASTLING_REGEX.match(parts[2]): - raise ValueError("invalid castling part in fen: {0}".format(repr(fen))) - - # Check that the en passant part is valid. - if parts[3] != "-": - if parts[1] == "w": - if square_rank(SQUARE_NAMES.index(parts[3])) != 5: - raise ValueError("expected ep square to be on sixth rank: {0}".format(repr(fen))) - else: - if square_rank(SQUARE_NAMES.index(parts[3])) != 2: - raise ValueError("expected ep square to be on third rank: {0}".format(repr(fen))) - # Check that the half move part is valid. - if int(parts[4]) < 0: - raise ValueError("halfmove clock can not be negative: {0}".format(repr(fen))) + # Board part. + try: + board_part = parts.pop(0) + except IndexError: + raise ValueError("empty fen") - # Check that the fullmove number part is valid. - # 0 is allowed for compability but later replaced with 1. - if int(parts[5]) < 0: - raise ValueError("fullmove number must be positive: {0}".format(repr(fen))) + # Turn. + try: + turn_part = parts.pop(0) + except IndexError: + turn = WHITE + else: + if turn_part == "w": + turn = WHITE + elif turn_part == "b": + turn = BLACK + else: + raise ValueError(f"expected 'w' or 'b' for turn part of fen: {fen!r}") - # Validate the board part and set it. - self._set_board_fen(parts[0]) + # Validate castling part. + try: + castling_part = parts.pop(0) + except IndexError: + castling_part = "-" + else: + if not FEN_CASTLING_REGEX.match(castling_part): + raise ValueError(f"invalid castling part in fen: {fen!r}") - # Set the turn. - if parts[1] == "w": - self.turn = WHITE + # En passant square. + try: + ep_part = parts.pop(0) + except IndexError: + ep_square = None + else: + try: + ep_square = None if ep_part == "-" else SQUARE_NAMES.index(ep_part) + except ValueError: + raise ValueError(f"invalid en passant part in fen: {fen!r}") + + # Check that the half-move part is valid. + try: + halfmove_part = parts.pop(0) + except IndexError: + halfmove_clock = 0 else: - self.turn = BLACK + try: + halfmove_clock = int(halfmove_part) + except ValueError: + raise ValueError(f"invalid half-move clock in fen: {fen!r}") - # Set castling flags. - self._set_castling_fen(parts[2]) + if halfmove_clock < 0: + raise ValueError(f"half-move clock cannot be negative: {fen!r}") - # Set the en passant square. - if parts[3] == "-": - self.ep_square = None + # Check that the full-move number part is valid. + # 0 is allowed for compatibility, but later replaced with 1. + try: + fullmove_part = parts.pop(0) + except IndexError: + fullmove_number = 1 else: - self.ep_square = SQUARE_NAMES.index(parts[3]) + try: + fullmove_number = int(fullmove_part) + except ValueError: + raise ValueError(f"invalid fullmove number in fen: {fen!r}") + + if fullmove_number < 0: + raise ValueError(f"fullmove number cannot be negative: {fen!r}") - # Set the mover counters. - self.halfmove_clock = int(parts[4]) - self.fullmove_number = int(parts[5]) or 1 + fullmove_number = max(fullmove_number, 1) - # Clear move stack. + # All parts should be consumed now. + if parts: + raise ValueError(f"fen string has more parts than expected: {fen!r}") + + # Validate the board part and set it. + self._set_board_fen(board_part) + + # Apply. + self.turn = turn + self._set_castling_fen(castling_part) + self.ep_square = ep_square + self.halfmove_clock = halfmove_clock + self.fullmove_number = fullmove_number self.clear_stack() - def _set_castling_fen(self, castling_fen): + def _set_castling_fen(self, castling_fen: str) -> None: if not castling_fen or castling_fen == "-": - self.castling_rights = BB_VOID + self.castling_rights = BB_EMPTY return if not FEN_CASTLING_REGEX.match(castling_fen): - raise ValueError("invalid castling fen: {0}".format(repr(castling_fen))) + raise ValueError(f"invalid castling fen: {castling_fen!r}") - self.castling_rights = BB_VOID + self.castling_rights = BB_EMPTY for flag in castling_fen: color = WHITE if flag.isupper() else BLACK @@ -2091,22 +2784,28 @@ def _set_castling_fen(self, castling_fen): else: self.castling_rights |= BB_FILES[FILE_NAMES.index(flag)] & backrank - def set_castling_fen(self, castling_fen): + def set_castling_fen(self, castling_fen: str) -> None: """ Sets castling rights from a string in FEN notation like ``Qqk``. + Also clears the move stack. + :raises: :exc:`ValueError` if the castling FEN is syntactically invalid. """ self._set_castling_fen(castling_fen) self.clear_stack() - def set_board_fen(self, fen): - super(Board, self).set_board_fen(fen) + def set_board_fen(self, fen: str) -> None: + super().set_board_fen(fen) + self.clear_stack() + + def set_piece_map(self, pieces: Mapping[Square, Piece]) -> None: + super().set_piece_map(pieces) self.clear_stack() - def set_chess960_pos(self, sharnagl): - super(Board, self).set_chess960_pos(sharnagl) + def set_chess960_pos(self, scharnagl: int) -> None: + super().set_chess960_pos(scharnagl) self.chess960 = True self.turn = WHITE self.castling_rights = self.rooks @@ -2116,13 +2815,13 @@ def set_chess960_pos(self, sharnagl): self.clear_stack() - def chess960_pos(self, ignore_turn=False, ignore_castling=False, ignore_counters=True): + def chess960_pos(self, *, ignore_turn: bool = False, ignore_castling: bool = False, ignore_counters: bool = True) -> Optional[int]: """ - Gets the Chess960 starting position index between 0 and 956 + Gets the Chess960 starting position index between 0 and 956, or ``None`` if the current position is not a Chess960 starting position. - By default white to move (**ignore_turn**) and full castling rights + By default, white to move (**ignore_turn**) and full castling rights (**ignore_castling**) are required, but move counters (**ignore_counters**) are ignored. """ @@ -2141,264 +2840,317 @@ def chess960_pos(self, ignore_turn=False, ignore_castling=False, ignore_counters if self.fullmove_number != 1 or self.halfmove_clock != 0: return None - return super(Board, self).chess960_pos() + return super().chess960_pos() - def _epd_operations(self, operations): - epd = [] + def _epd_operations(self, operations: Mapping[str, Union[None, str, int, float, Move, Iterable[Move]]]) -> str: + epd: List[str] = [] first_op = True for opcode, operand in operations.items(): + self._validate_epd_opcode(opcode) + if not first_op: epd.append(" ") first_op = False epd.append(opcode) - # Value is empty. if operand is None: epd.append(";") - continue - - # Value is a move. - if hasattr(operand, "from_square") and hasattr(operand, "to_square") and hasattr(operand, "promotion"): - # Append SAN for moves. + elif isinstance(operand, Move): epd.append(" ") epd.append(self.san(operand)) epd.append(";") - continue - - # Value is numeric. - if isinstance(operand, (int, float)): - # Append integer or float. - epd.append(" ") - epd.append(str(operand)) - epd.append(";") - continue - - # Value is a set of moves or a variation. - if hasattr(operand, "__iter__"): - position = Board(self.shredder_fen()) if opcode == "pv" else self - iterator = operand.__iter__() - first_move = next(iterator) - if hasattr(first_move, "from_square") and hasattr(first_move, "to_square") and hasattr(first_move, "promotion"): + elif isinstance(operand, int): + epd.append(f" {operand};") + elif isinstance(operand, float): + assert math.isfinite(operand), f"expected numeric epd operand to be finite, got: {operand}" + epd.append(f" {operand};") + elif opcode == "pv" and not isinstance(operand, str) and hasattr(operand, "__iter__"): + position = self.copy(stack=False) + for move in operand: + epd.append(" ") + epd.append(position.san_and_push(move)) + epd.append(";") + elif opcode in ["am", "bm"] and not isinstance(operand, str) and hasattr(operand, "__iter__"): + for san in sorted(self.san(move) for move in operand): epd.append(" ") - epd.append(position.san(first_move)) - if opcode == "pv": - position.push(first_move) - - for move in iterator: - epd.append(" ") - epd.append(position.san(move)) - if opcode == "pv": - position.push(move) - - epd.append(";") - continue - - # Append as escaped string. - epd.append(" \"") - epd.append(str(operand).replace("\r", "").replace("\n", " ").replace("\\", "\\\\").replace(";", "\\s")) - epd.append("\";") + epd.append(san) + epd.append(";") + else: + # Append as escaped string. + epd.append(" \"") + epd.append(str(operand).replace("\\", "\\\\").replace("\t", "\\t").replace("\r", "\\r").replace("\n", "\\n").replace("\"", "\\\"")) + epd.append("\";") return "".join(epd) - def epd(self, shredder_fen=False, promoted=None, **operations): + def epd(self, *, shredder: bool = False, en_passant: EnPassantSpec = "legal", promoted: Optional[bool] = None, **operations: Union[None, str, int, float, Move, Iterable[Move]]) -> str: """ Gets an EPD representation of the current position. + See :func:`~chess.Board.fen()` for FEN formatting options (*shredder*, + *ep_square* and *promoted*). + EPD operations can be given as keyword arguments. Supported operands - are strings, integers, floats and moves and lists of moves and None. - All other operands are converted to strings. + are strings, integers, finite floats, legal moves and ``None``. + Additionally, the operation ``pv`` accepts a legal variation as + a list of moves. The operations ``am`` and ``bm`` accept a list of + legal moves in the current position. - A list of moves for *pv* will be interpreted as a variation. All other - move lists are interpreted as a set of moves in the current position. + The name of the field cannot be a lone dash and cannot contain spaces, + newlines, carriage returns or tabs. - *hmvc* and *fmvc* are not included by default. You can use: + *hmvc* and *fmvn* are not included by default. You can use: - >>> board.epd(hmvc=board.halfmove_clock, fmvc=board.fullmove_number) - 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - hmvc 0; fmvc 1;' + >>> import chess + >>> + >>> board = chess.Board() + >>> board.epd(hmvc=board.halfmove_clock, fmvn=board.fullmove_number) + 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - hmvc 0; fmvn 1;' """ - epd = [] + if en_passant == "fen": + ep_square = self.ep_square + elif en_passant == "xfen": + ep_square = self.ep_square if self.has_pseudo_legal_en_passant() else None + else: + ep_square = self.ep_square if self.has_legal_en_passant() else None - epd.append(self.board_fen(promoted=promoted)) - epd.append("w" if self.turn == WHITE else "b") - epd.append(self.castling_shredder_fen() if shredder_fen else self.castling_xfen()) - epd.append(SQUARE_NAMES[self.ep_square] if self.has_legal_en_passant() else "-") + epd = [self.board_fen(promoted=promoted), + "w" if self.turn == WHITE else "b", + self.castling_shredder_fen() if shredder else self.castling_xfen(), + SQUARE_NAMES[ep_square] if ep_square is not None else "-"] if operations: epd.append(self._epd_operations(operations)) return " ".join(epd) - def _parse_epd_ops(self, operation_part, make_board): - operations = {} - - if not operation_part: - return operations - - operation_part += ";" - + def _validate_epd_opcode(self, opcode: str) -> None: + if not opcode: + raise ValueError("empty string is not a valid epd opcode") + if opcode == "-": + raise ValueError("dash (-) is not a valid epd opcode") + if not opcode[0].isalpha(): + raise ValueError(f"expected epd opcode to start with a letter, got: {opcode!r}") + for blacklisted in [" ", "\n", "\t", "\r"]: + if blacklisted in opcode: + raise ValueError(f"invalid character {blacklisted!r} in epd opcode: {opcode!r}") + + def _parse_epd_ops(self, operation_part: str, make_board: Callable[[], Self]) -> Dict[str, Union[None, str, int, float, Move, List[Move]]]: + operations: Dict[str, Union[None, str, int, float, Move, List[Move]]] = {} + state = "opcode" opcode = "" operand = "" - in_operand = False - in_quotes = False - escape = False - position = None - for c in operation_part: - if not in_operand: - if c == ";": - operations[opcode] = None - opcode = "" - elif c == " ": + for ch in itertools.chain(operation_part, [None]): + if state == "opcode": + if ch in [" ", "\t", "\r", "\n"]: + if opcode == "-": + opcode = "" + elif opcode: + self._validate_epd_opcode(opcode) + state = "after_opcode" + elif ch is None or ch == ";": + if opcode == "-": + opcode = "" + elif opcode: + operations[opcode] = [] if opcode in ["pv", "am", "bm"] else None + opcode = "" + else: + opcode += ch + elif state == "after_opcode": + if ch in [" ", "\t", "\r", "\n"]: + pass + elif ch == "\"": + state = "string" + elif ch is None or ch == ";": if opcode: - in_operand = True + operations[opcode] = [] if opcode in ["pv", "am", "bm"] else None + opcode = "" + state = "opcode" + elif ch in "+-.0123456789": + operand = ch + state = "numeric" else: - opcode += c - else: - if c == "\"": - if not operand and not in_quotes: - in_quotes = True - elif escape: - operand += c - elif c == "\\": - if escape: - operand += c + operand = ch + state = "san" + elif state == "numeric": + if ch is None or ch == ";": + if "." in operand or "e" in operand or "E" in operand: + parsed = float(operand) + if not math.isfinite(parsed): + raise ValueError(f"invalid numeric operand for epd operation {opcode!r}: {operand!r}") + operations[opcode] = parsed else: - escape = True - elif c == "s": - if escape: - operand += ";" - else: - operand += c - elif c == ";": - if escape: - operand += "\\" - - if in_quotes: - # A string operand. - operations[opcode] = operand + operations[opcode] = int(operand) + opcode = "" + operand = "" + state = "opcode" + else: + operand += ch + elif state == "string": + if ch is None or ch == "\"": + operations[opcode] = operand + opcode = "" + operand = "" + state = "opcode" + elif ch == "\\": + state = "string_escape" + else: + operand += ch + elif state == "string_escape": + if ch is None: + operations[opcode] = operand + opcode = "" + operand = "" + state = "opcode" + elif ch == "r": + operand += "\r" + state = "string" + elif ch == "n": + operand += "\n" + state = "string" + elif ch == "t": + operand += "\t" + state = "string" + else: + operand += ch + state = "string" + elif state == "san": + if ch is None or ch == ";": + if position is None: + position = make_board() + + if opcode == "pv": + # A variation. + variation: List[Move] = [] + for token in operand.split(): + move = position.parse_xboard(token) + variation.append(move) + position.push(move) + + # Reset the position. + while position.move_stack: + position.pop() + + operations[opcode] = variation + elif opcode in ["bm", "am"]: + # A set of moves. + operations[opcode] = [position.parse_xboard(token) for token in operand.split()] else: - try: - # An integer. - operations[opcode] = int(operand) - except ValueError: - try: - # A float. - operations[opcode] = float(operand) - except ValueError: - if position is None: - position = make_board() - if opcode == "pv": - # A variation. - operations[opcode] = [] - for token in operand.split(): - move = position.parse_san(token) - operations[opcode].append(move) - position.push(move) - - # Reset the position. - while position.move_stack: - position.pop() - elif opcode in ("bm", "am"): - # A set of moves. - operations[opcode] = [position.parse_san(token) for token in operand.split()] - else: - # A single move. - operations[opcode] = position.parse_san(operand) + # A single move. + operations[opcode] = position.parse_xboard(operand) opcode = "" operand = "" - in_operand = False - in_quotes = False - escape = False + state = "opcode" else: - operand += c + operand += ch + assert state == "opcode" return operations - def set_epd(self, epd): + def set_epd(self, epd: str) -> Dict[str, Union[None, str, int, float, Move, List[Move]]]: """ Parses the given EPD string and uses it to set the position. - If present the ``hmvc`` and the ``fmvn`` are used to set the half move - clock and the fullmove number. Otherwise ``0`` and ``1`` are used. + If present, ``hmvc`` and ``fmvn`` are used to set the half-move + clock and the full-move number. Otherwise, ``0`` and ``1`` are used. Returns a dictionary of parsed operations. Values can be strings, - integers, floats or move objects. + integers, floats, move objects, or lists of moves. :raises: :exc:`ValueError` if the EPD string is invalid. """ - # Split into 4 or 5 parts. parts = epd.strip().rstrip(";").split(None, 4) - if len(parts) < 4: - raise ValueError("epd should consist of at least 4 parts: {0}".format(repr(epd))) # Parse ops. if len(parts) > 4: operations = self._parse_epd_ops(parts.pop(), lambda: type(self)(" ".join(parts) + " 0 1")) + parts.append(str(operations["hmvc"]) if "hmvc" in operations else "0") + parts.append(str(operations["fmvn"]) if "fmvn" in operations else "1") + self.set_fen(" ".join(parts)) + return operations else: - operations = {} + self.set_fen(epd) + return {} - # Create a full FEN and parse it. - parts.append(str(operations["hmvc"]) if "hmvc" in operations else "0") - parts.append(str(operations["fmvn"]) if "fmvn" in operations else "1") - self.set_fen(" ".join(parts)) - - return operations + def san(self, move: Move) -> str: + """ + Gets the standard algebraic notation of the given move in the context + of the current position. + """ + return self._algebraic(move) - def san(self, move): + def lan(self, move: Move) -> str: """ - Gets the standard algebraic notation of the given move in the context of + Gets the long algebraic notation of the given move in the context of the current position. - - There is no validation. It is only guaranteed to work if the move is - legal or a null move. """ - if not move: - # Null move. - return "--" + return self._algebraic(move, long=True) + + def san_and_push(self, move: Move) -> str: + return self._algebraic_and_push(move) + + def _algebraic(self, move: Move, *, long: bool = False) -> str: + san = self._algebraic_and_push(move, long=long) + self.pop() + return san + + def _algebraic_and_push(self, move: Move, *, long: bool = False) -> str: + san = self._algebraic_without_suffix(move, long=long) # Look ahead for check or checkmate. self.push(move) is_check = self.is_check() is_checkmate = (is_check and self.is_checkmate()) or self.is_variant_loss() or self.is_variant_win() - self.pop() + + # Add check or checkmate suffix. + if is_checkmate and move: + return san + "#" + elif is_check and move: + return san + "+" + else: + return san + + def _algebraic_without_suffix(self, move: Move, *, long: bool = False) -> str: + # Null move. + if not move: + return "--" # Drops. if move.drop: san = "" if move.drop != PAWN: - san = PIECE_SYMBOLS[move.drop].upper() + san = piece_symbol(move.drop).upper() san += "@" + SQUARE_NAMES[move.to_square] + return san # Castling. if self.is_castling(move): if square_file(move.to_square) < square_file(move.from_square): - san = "O-O-O" + return "O-O-O" else: - san = "O-O" + return "O-O" - if move.drop or self.is_castling(move): - if is_checkmate: - return san + "#" - elif is_check: - return san + "+" - else: - return san - - piece = self.piece_type_at(move.from_square) + piece_type = self.piece_type_at(move.from_square) + assert piece_type, f"san() and lan() expect move to be legal or null, but got {move} in {self.fen()}" + capture = self.is_capture(move) - if piece == PAWN: + if piece_type == PAWN: san = "" else: - san = PIECE_SYMBOLS[piece].upper() + san = piece_symbol(piece_type).upper() + if long: + san += SQUARE_NAMES[move.from_square] + elif piece_type != PAWN: # Get ambiguous move candidates. - # Relevant candidates: Not excatly the current move, + # Relevant candidates: not exactly the current move, # but to the same square. others = 0 - from_mask = self.pieces_mask(piece, self.turn) + from_mask = self.pieces_mask(piece_type, self.turn) from_mask &= ~BB_SQUARES[move.from_square] to_mask = BB_SQUARES[move.to_square] for candidate in self.generate_legal_moves(from_mask, to_mask): @@ -2420,105 +3172,125 @@ def san(self, move): san += FILE_NAMES[square_file(move.from_square)] if row: san += RANK_NAMES[square_rank(move.from_square)] + elif capture: + san += FILE_NAMES[square_file(move.from_square)] # Captures. - if self.is_capture(move): - if piece == PAWN: - san += FILE_NAMES[square_file(move.from_square)] + if capture: san += "x" + elif long: + san += "-" # Destination square. san += SQUARE_NAMES[move.to_square] # Promotion. if move.promotion: - san += "=" + PIECE_SYMBOLS[move.promotion].upper() - - # Add check or checkmate suffix - if is_checkmate: - san += "#" - elif is_check: - san += "+" + san += "=" + piece_symbol(move.promotion).upper() return san - def variation_san(self, variation): + def variation_san(self, variation: Iterable[Move]) -> str: """ - Given a sequence of moves, return a string representing the sequence - in standard algebraic notation (e.g. ``1. e4 e5 2. Nf3 Nc6`` or + Given a sequence of moves, returns a string representing the sequence + in standard algebraic notation (e.g., ``1. e4 e5 2. Nf3 Nc6`` or ``37...Bg6 38. fxg6``). - This board will not be modified as a result of calling this. + The board will not be modified as a result of calling this. - :raises: :exc:`ValueError` if any moves in the sequence are illegal. + :raises: :exc:`IllegalMoveError` if any moves in the sequence are illegal. """ board = self.copy(stack=False) - san = [] + san: List[str] = [] for move in variation: if not board.is_legal(move): - raise ValueError("illegal move {0} in position {1}".format(move, board.fen())) + raise IllegalMoveError(f"illegal move {move} in position {board.fen()}") if board.turn == WHITE: - san.append("{0}. {1}".format(board.fullmove_number, board.san(move))) + san.append(f"{board.fullmove_number}. {board.san_and_push(move)}") elif not san: - san.append("{0}...{1}".format(board.fullmove_number, board.san(move))) + san.append(f"{board.fullmove_number}...{board.san_and_push(move)}") else: - san.append(board.san(move)) - - board.push(move) + san.append(board.san_and_push(move)) return " ".join(san) - def parse_san(self, san): + def parse_san(self, san: str) -> Move: """ Uses the current position as the context to parse a move in standard - algebraic notation and return the corresponding move object. + algebraic notation and returns the corresponding move object. + + Ambiguous moves are rejected. Overspecified moves (including long + algebraic notation) are accepted. Some common syntactical deviations + are also accepted. The returned move is guaranteed to be either legal or a null move. - :raises: :exc:`ValueError` if the SAN is invalid or ambiguous. + :raises: + :exc:`ValueError` (specifically an exception specified below) if the SAN is invalid, illegal or ambiguous. + + - :exc:`InvalidMoveError` if the SAN is syntactically invalid. + - :exc:`IllegalMoveError` if the SAN is illegal. + - :exc:`AmbiguousMoveError` if the SAN is ambiguous. """ # Castling. try: - if san in ["O-O", "O-O+", "O-O#"]: + if san in ["O-O", "O-O+", "O-O#", "0-0", "0-0+", "0-0#"]: return next(move for move in self.generate_castling_moves() if self.is_kingside_castling(move)) - elif san in ["O-O-O", "O-O-O+", "O-O-O#"]: + elif san in ["O-O-O", "O-O-O+", "O-O-O#", "0-0-0", "0-0-0+", "0-0-0#"]: return next(move for move in self.generate_castling_moves() if self.is_queenside_castling(move)) except StopIteration: - raise ValueError("illegal san: {0} in {1}".format(repr(san), self.fen())) + raise IllegalMoveError(f"illegal san: {san!r} in {self.fen()}") # Match normal moves. match = SAN_REGEX.match(san) if not match: # Null moves. - if san == "--": + if san in ["--", "Z0", "0000", "@@@@"]: return Move.null() + elif "," in san: + raise InvalidMoveError(f"unsupported multi-leg move: {san!r}") + else: + raise InvalidMoveError(f"invalid san: {san!r}") - raise ValueError("invalid san: {0}".format(repr(san))) - - # Get target square. + # Get target square. Mask our own pieces to exclude castling moves. to_square = SQUARE_NAMES.index(match.group(4)) - to_mask = BB_SQUARES[to_square] + to_mask = BB_SQUARES[to_square] & ~self.occupied_co[self.turn] - # Get the promotion type. + # Get the promotion piece type. p = match.group(5) - promotion = p and PIECE_SYMBOLS.index(p[-1].lower()) + promotion = PIECE_SYMBOLS.index(p[-1].lower()) if p else None + + # Filter by original square. + from_mask = BB_ALL + from_file = None + from_rank = None + if match.group(2): + from_file = FILE_NAMES.index(match.group(2)) + from_mask &= BB_FILES[from_file] + if match.group(3): + from_rank = int(match.group(3)) - 1 + from_mask &= BB_RANKS[from_rank] # Filter by piece type. if match.group(1): piece_type = PIECE_SYMBOLS.index(match.group(1).lower()) - from_mask = self.pieces_mask(piece_type, self.turn) + from_mask &= self.pieces_mask(piece_type, self.turn) + elif from_file is not None and from_rank is not None: + # Allow fully specified moves, even if they are not pawn moves, + # including castling moves. + move = self.find_move(square(from_file, from_rank), to_square, promotion) + if move.promotion == promotion: + return move + else: + raise IllegalMoveError(f"missing promotion piece type: {san!r} in {self.fen()}") else: - from_mask = self.pawns - - # Filter by source file. - if match.group(2): - from_mask &= BB_FILES[FILE_NAMES.index(match.group(2))] + from_mask &= self.pawns - # Filter by source rank. - if match.group(3): - from_mask &= BB_RANKS[int(match.group(3)) - 1] + # Do not allow pawn captures if file is not specified. + if from_file is None: + from_mask &= BB_FILES[square_file(to_square)] # Match legal moves. matched_move = None @@ -2527,34 +3299,39 @@ def parse_san(self, san): continue if matched_move: - raise ValueError("ambiguous san: {0} in {1}".format(repr(san), self.fen())) + raise AmbiguousMoveError(f"ambiguous san: {san!r} in {self.fen()}") matched_move = move if not matched_move: - raise ValueError("illegal san: {0} in {1}".format(repr(san), self.fen())) + raise IllegalMoveError(f"illegal san: {san!r} in {self.fen()}") return matched_move - def push_san(self, san): + def push_san(self, san: str) -> Move: """ Parses a move in standard algebraic notation, makes the move and puts - it on the the move stack. + it onto the move stack. Returns the move. - :raises: :exc:`ValueError` if neither legal nor a null move. + :raises: + :exc:`ValueError` (specifically an exception specified below) if neither legal nor a null move. + + - :exc:`InvalidMoveError` if the SAN is syntactically invalid. + - :exc:`IllegalMoveError` if the SAN is illegal. + - :exc:`AmbiguousMoveError` if the SAN is ambiguous. """ move = self.parse_san(san) self.push(move) return move - def uci(self, move, chess960=None): + def uci(self, move: Move, *, chess960: Optional[bool] = None) -> str: """ Gets the UCI notation of the move. *chess960* defaults to the mode of the board. Pass ``True`` to force - *Chess960* mode. + Chess960 mode. """ if chess960 is None: chess960 = self.chess960 @@ -2563,7 +3340,7 @@ def uci(self, move, chess960=None): move = self._from_chess960(chess960, move.from_square, move.to_square, move.promotion, move.drop) return move.uci() - def parse_uci(self, uci): + def parse_uci(self, uci: str) -> Move: """ Parses the given move in UCI notation. @@ -2571,8 +3348,12 @@ def parse_uci(self, uci): The returned move is guaranteed to be either legal or a null move. - :raises: :exc:`ValueError` if the move is invalid or illegal in the + :raises: + :exc:`ValueError` (specifically an exception specified below) if the move is invalid or illegal in the current position (but not a null move). + + - :exc:`InvalidMoveError` if the UCI is syntactically invalid. + - :exc:`IllegalMoveError` if the UCI is illegal. """ move = Move.from_uci(uci) @@ -2583,79 +3364,107 @@ def parse_uci(self, uci): move = self._from_chess960(self.chess960, move.from_square, move.to_square, move.promotion, move.drop) if not self.is_legal(move): - raise ValueError("illegal uci: {0} in {1}".format(repr(uci), self.fen())) + raise IllegalMoveError(f"illegal uci: {uci!r} in {self.fen()}") return move - def push_uci(self, uci): + def push_uci(self, uci: str) -> Move: """ Parses a move in UCI notation and puts it on the move stack. Returns the move. - :raises: :exc:`ValueError` if the move is invalid or illegal in the + :raises: + :exc:`ValueError` (specifically an exception specified below) if the move is invalid or illegal in the current position (but not a null move). + + - :exc:`InvalidMoveError` if the UCI is syntactically invalid. + - :exc:`IllegalMoveError` if the UCI is illegal. """ move = self.parse_uci(uci) self.push(move) return move - def is_en_passant(self, move): + def xboard(self, move: Move, chess960: Optional[bool] = None) -> str: + if chess960 is None: + chess960 = self.chess960 + + if not chess960 or not self.is_castling(move): + return move.xboard() + elif self.is_kingside_castling(move): + return "O-O" + else: + return "O-O-O" + + def parse_xboard(self, xboard: str) -> Move: + return self.parse_san(xboard) + + push_xboard = push_san + + def is_en_passant(self, move: Move) -> bool: """Checks if the given pseudo-legal move is an en passant capture.""" return (self.ep_square == move.to_square and - self.pawns & BB_SQUARES[move.from_square] and + bool(self.pawns & BB_SQUARES[move.from_square]) and abs(move.to_square - move.from_square) in [7, 9] and not self.occupied & BB_SQUARES[move.to_square]) - def is_capture(self, move): + def is_capture(self, move: Move) -> bool: """Checks if the given pseudo-legal move is a capture.""" - return BB_SQUARES[move.to_square] & self.occupied_co[not self.turn] or self.is_en_passant(move) + touched = BB_SQUARES[move.from_square] ^ BB_SQUARES[move.to_square] + return bool(touched & self.occupied_co[not self.turn]) or self.is_en_passant(move) - def is_zeroing(self, move): + def is_zeroing(self, move: Move) -> bool: """Checks if the given pseudo-legal move is a capture or pawn move.""" - return BB_SQUARES[move.from_square] & self.pawns or BB_SQUARES[move.to_square] & self.occupied_co[not self.turn] + touched = BB_SQUARES[move.from_square] ^ BB_SQUARES[move.to_square] + return bool(touched & self.pawns or touched & self.occupied_co[not self.turn] or move.drop == PAWN) - def is_irreversible(self, move): + def _reduces_castling_rights(self, move: Move) -> bool: + cr = self.clean_castling_rights() + touched = BB_SQUARES[move.from_square] ^ BB_SQUARES[move.to_square] + return bool(touched & cr or + cr & BB_RANK_1 and touched & self.kings & self.occupied_co[WHITE] & ~self._effective_promoted() or + cr & BB_RANK_8 and touched & self.kings & self.occupied_co[BLACK] & ~self._effective_promoted()) + + def is_irreversible(self, move: Move) -> bool: """ Checks if the given pseudo-legal move is irreversible. - In standard chess pawn moves, captures and moves that destroy castling - rights are irreversible. + In standard chess, pawn moves, captures, moves that destroy castling + rights and moves that cede en passant are irreversible. + + This method has false-negatives with forced lines. For example, a check + that will force the king to lose castling rights is not considered + irreversible. Only the actual king move is. """ - backrank = BB_RANK_1 if self.turn == WHITE else BB_RANK_8 - castling_rights = self.clean_castling_rights() & backrank - return (self.is_zeroing(move) or - castling_rights and BB_SQUARES[move.from_square] & self.kings & ~self.promoted or - castling_rights & BB_SQUARES[move.from_square] or - castling_rights & BB_SQUARES[move.to_square]) + return self.is_zeroing(move) or self._reduces_castling_rights(move) or self.has_legal_en_passant() - def is_castling(self, move): + def is_castling(self, move: Move) -> bool: """Checks if the given pseudo-legal move is a castling move.""" if self.kings & BB_SQUARES[move.from_square]: diff = square_file(move.from_square) - square_file(move.to_square) - return abs(diff) > 1 or self.rooks & self.occupied_co[self.turn] & BB_SQUARES[move.to_square] + return abs(diff) > 1 or bool(self.rooks & self.occupied_co[self.turn] & BB_SQUARES[move.to_square]) return False - def is_kingside_castling(self, move): + def is_kingside_castling(self, move: Move) -> bool: """ Checks if the given pseudo-legal move is a kingside castling move. """ return self.is_castling(move) and square_file(move.to_square) > square_file(move.from_square) - def is_queenside_castling(self, move): + def is_queenside_castling(self, move: Move) -> bool: """ Checks if the given pseudo-legal move is a queenside castling move. """ return self.is_castling(move) and square_file(move.to_square) < square_file(move.from_square) - def clean_castling_rights(self): + def clean_castling_rights(self) -> Bitboard: """ Returns valid castling rights filtered from :data:`~chess.Board.castling_rights`. """ - if self.stack: - # Castling rights do not change in a game, so we can assume them to - # be filtered already. + if self._stack: + # No new castling rights are assigned in a game, so we can assume + # they were filtered already. return self.castling_rights castling = self.castling_rights & self.rooks @@ -2668,16 +3477,16 @@ def clean_castling_rights(self): black_castling &= (BB_A8 | BB_H8) # The kings must be on e1 or e8. - if not self.occupied_co[WHITE] & self.kings & ~self.promoted & BB_E1: + if not self.occupied_co[WHITE] & self.kings & ~self._effective_promoted() & BB_E1: white_castling = 0 - if not self.occupied_co[BLACK] & self.kings & ~self.promoted & BB_E8: + if not self.occupied_co[BLACK] & self.kings & ~self._effective_promoted() & BB_E8: black_castling = 0 return white_castling | black_castling else: - # The kings must be on the backrank. - white_king_mask = self.occupied_co[WHITE] & self.kings & BB_RANK_1 & ~self.promoted - black_king_mask = self.occupied_co[BLACK] & self.kings & BB_RANK_8 & ~self.promoted + # The kings must be on the back rank. + white_king_mask = self.occupied_co[WHITE] & self.kings & BB_RANK_1 & ~self._effective_promoted() + black_king_mask = self.occupied_co[BLACK] & self.kings & BB_RANK_8 & ~self._effective_promoted() if not white_king_mask: white_castling = 0 if not black_king_mask: @@ -2693,8 +3502,8 @@ def clean_castling_rights(self): if white_h_side and msb(white_h_side) < msb(white_king_mask): white_h_side = 0 - black_a_side = (black_castling & -black_castling) - black_h_side = BB_SQUARES[msb(black_castling)] if black_castling else BB_VOID + black_a_side = black_castling & -black_castling + black_h_side = BB_SQUARES[msb(black_castling)] if black_castling else BB_EMPTY if black_a_side and msb(black_a_side) > msb(black_king_mask): black_a_side = 0 @@ -2704,18 +3513,18 @@ def clean_castling_rights(self): # Done. return black_a_side | black_h_side | white_a_side | white_h_side - def has_castling_rights(self, color): + def has_castling_rights(self, color: Color) -> bool: """Checks if the given side has castling rights.""" backrank = BB_RANK_1 if color == WHITE else BB_RANK_8 return bool(self.clean_castling_rights() & backrank) - def has_kingside_castling_rights(self, color): + def has_kingside_castling_rights(self, color: Color) -> bool: """ Checks if the given side has kingside (that is h-side in Chess960) castling rights. """ backrank = BB_RANK_1 if color == WHITE else BB_RANK_8 - king_mask = self.kings & self.occupied_co[color] & backrank & ~self.promoted + king_mask = self.kings & self.occupied_co[color] & backrank & ~self._effective_promoted() if not king_mask: return False @@ -2726,17 +3535,17 @@ def has_kingside_castling_rights(self, color): if rook > king_mask: return True - castling_rights = castling_rights & (castling_rights - 1) + castling_rights &= castling_rights - 1 return False - def has_queenside_castling_rights(self, color): + def has_queenside_castling_rights(self, color: Color) -> bool: """ Checks if the given side has queenside (that is a-side in Chess960) castling rights. """ backrank = BB_RANK_1 if color == WHITE else BB_RANK_8 - king_mask = self.kings & self.occupied_co[color] & backrank & ~self.promoted + king_mask = self.kings & self.occupied_co[color] & backrank & ~self._effective_promoted() if not king_mask: return False @@ -2747,11 +3556,11 @@ def has_queenside_castling_rights(self, color): if rook < king_mask: return True - castling_rights = castling_rights & (castling_rights - 1) + castling_rights &= castling_rights - 1 return False - def has_chess960_castling_rights(self): + def has_chess960_castling_rights(self) -> bool: """ Checks if there are castling rights that are only possible in Chess960. """ @@ -2763,7 +3572,7 @@ def has_chess960_castling_rights(self): # Standard chess castling rights can only be on the standard # starting rook squares. - if castling_rights & ~(BB_A1 | BB_A8 | BB_H1 | BB_H8): + if castling_rights & ~BB_CORNERS: return True # If there are any castling rights in standard chess, the king must be @@ -2775,16 +3584,15 @@ def has_chess960_castling_rights(self): return False - def status(self): + def status(self) -> Status: """ Gets a bitmask of possible problems with the position. - Move making, generation and validation are only guaranteed to work on - a completely valid board. + :data:`~chess.STATUS_VALID` if all basic validity requirements are met. + This does not imply that the position is actually reachable with a + series of legal moves from the starting position. - :data:`~chess.STATUS_VALID` for a completely valid board. - - Otherwise bitwise combinations of: + Otherwise, bitwise combinations of: :data:`~chess.STATUS_NO_WHITE_KING`, :data:`~chess.STATUS_NO_BLACK_KING`, :data:`~chess.STATUS_TOO_MANY_KINGS`, @@ -2799,7 +3607,9 @@ def status(self): :data:`~chess.STATUS_EMPTY`, :data:`~chess.STATUS_RACE_CHECK`, :data:`~chess.STATUS_RACE_OVER`, - :data:`~chess.STATUS_RACE_MATERIAL`. + :data:`~chess.STATUS_RACE_MATERIAL`, + :data:`~chess.STATUS_TOO_MANY_CHECKERS`, + :data:`~chess.STATUS_IMPOSSIBLE_CHECK`. """ errors = STATUS_VALID @@ -2808,11 +3618,11 @@ def status(self): errors |= STATUS_EMPTY # There must be exactly one king of each color. - if not self.occupied_co[WHITE] & self.kings: + if not self.occupied_co[WHITE] & self.kings & ~self._effective_promoted(): errors |= STATUS_NO_WHITE_KING - if not self.occupied_co[BLACK] & self.kings: + if not self.occupied_co[BLACK] & self.kings & ~self._effective_promoted(): errors |= STATUS_NO_BLACK_KING - if popcount(self.occupied & self.kings) > 2: + if popcount(self.occupied & self.kings & ~self._effective_promoted()) > 2: errors |= STATUS_TOO_MANY_KINGS # There can not be more than 16 pieces of any color. @@ -2821,13 +3631,13 @@ def status(self): if popcount(self.occupied_co[BLACK]) > 16: errors |= STATUS_TOO_MANY_BLACK_PIECES - # There can not be more than eight pawns of any color. + # There can not be more than 8 pawns of any color. if popcount(self.occupied_co[WHITE] & self.pawns) > 8: errors |= STATUS_TOO_MANY_WHITE_PAWNS if popcount(self.occupied_co[BLACK] & self.pawns) > 8: errors |= STATUS_TOO_MANY_BLACK_PAWNS - # Pawns can not be on the backrank. + # Pawns can not be on the back rank. if self.pawns & BB_BACKRANKS: errors |= STATUS_PAWNS_ON_BACKRANK @@ -2836,56 +3646,82 @@ def status(self): errors |= STATUS_BAD_CASTLING_RIGHTS # En passant. - if self.ep_square: - if self.turn == WHITE: - ep_rank = 5 - pawn_mask = shift_down(BB_SQUARES[self.ep_square]) - seventh_rank_mask = shift_up(BB_SQUARES[self.ep_square]) + valid_ep_square = self._valid_ep_square() + if self.ep_square != valid_ep_square: + errors |= STATUS_INVALID_EP_SQUARE + + # Side to move giving check. + if self.was_into_check(): + errors |= STATUS_OPPOSITE_CHECK + + # More than the maximum number of possible checkers in the variant. + checkers = self.checkers_mask() + our_kings = self.kings & self.occupied_co[self.turn] & ~self._effective_promoted() + if checkers: + if popcount(checkers) > 2: + errors |= STATUS_TOO_MANY_CHECKERS + + if valid_ep_square is not None: + pushed_to = valid_ep_square ^ A2 + pushed_from = valid_ep_square ^ A4 + occupied_before = (self.occupied & ~BB_SQUARES[pushed_to]) | BB_SQUARES[pushed_from] + if popcount(checkers) > 1 or ( + msb(checkers) != pushed_to and + self._attacked_for_king(our_kings, occupied_before)): + errors |= STATUS_IMPOSSIBLE_CHECK else: - ep_rank = 2 - pawn_mask = shift_up(BB_SQUARES[self.ep_square]) - seventh_rank_mask = shift_down(BB_SQUARES[self.ep_square]) + if popcount(checkers) > 2 or (popcount(checkers) == 2 and ray(lsb(checkers), msb(checkers)) & our_kings): + errors |= STATUS_IMPOSSIBLE_CHECK + + return errors - # The en passant square must be on the third or sixth rank. - if square_rank(self.ep_square) != ep_rank: - errors |= STATUS_INVALID_EP_SQUARE + def _valid_ep_square(self) -> Optional[Square]: + if not self.ep_square: + return None - # The last move must have been a double pawn push, so there must - # be a pawn of the correct color on the fourth or fifth rank. - if not self.pawns & self.occupied_co[not self.turn] & pawn_mask: - errors |= STATUS_INVALID_EP_SQUARE + if self.turn == WHITE: + ep_rank = RANK_6 + pawn_mask = shift_down(BB_SQUARES[self.ep_square]) + seventh_rank_mask = shift_up(BB_SQUARES[self.ep_square]) + else: + ep_rank = RANK_3 + pawn_mask = shift_up(BB_SQUARES[self.ep_square]) + seventh_rank_mask = shift_down(BB_SQUARES[self.ep_square]) - # And the en passant square must be empty. - if self.occupied & BB_SQUARES[self.ep_square]: - errors |= STATUS_INVALID_EP_SQUARE + # The en passant square must be on the third or sixth rank. + if square_rank(self.ep_square) != ep_rank: + return None - # And the second rank must be empty. - if self.occupied & seventh_rank_mask: - errors |= STATUS_INVALID_EP_SQUARE + # The last move must have been a double pawn push, so there must + # be a pawn of the correct color on the fourth or fifth rank. + if not self.pawns & self.occupied_co[not self.turn] & pawn_mask: + return None - # Side to move giving check. - if self.was_into_check(): - errors |= STATUS_OPPOSITE_CHECK + # And the en passant square must be empty. + if self.occupied & BB_SQUARES[self.ep_square]: + return None - return errors + # And the second rank must be empty. + if self.occupied & seventh_rank_mask: + return None - def is_valid(self): - """ - Checks if the board is valid. + return self.ep_square - Move making, generation and validation are only guaranteed to work on - a completely valid board. + def is_valid(self) -> bool: + """ + Checks some basic validity requirements. See :func:`~chess.Board.status()` for details. """ return self.status() == STATUS_VALID - def _ep_skewered(self, king, capturer): - # Handle the special case where the king would be in check, if the + def _ep_skewered(self, king: Square, capturer: Square) -> bool: + # Handle the special case where the king would be in check if the # pawn and its capturer disappear from the rank. # Vertical skewers of the captured pawn are not possible. (Pins on # the capturer are not handled here.) + assert self.ep_square is not None last_double = self.ep_square + (-8 if self.turn == WHITE else 8) @@ -2906,7 +3742,7 @@ def _ep_skewered(self, king, capturer): return False - def _slider_blockers(self, king): + def _slider_blockers(self, king: Square) -> Bitboard: rooks_and_queens = self.rooks | self.queens bishops_and_queens = self.bishops | self.queens @@ -2917,33 +3753,33 @@ def _slider_blockers(self, king): blockers = 0 for sniper in scan_reversed(snipers & self.occupied_co[not self.turn]): - b = BB_BETWEEN[king][sniper] & self.occupied + b = between(king, sniper) & self.occupied - # Add to blockers if exactly one piece in between. + # Add to blockers if exactly one piece in-between. if b and BB_SQUARES[msb(b)] == b: blockers |= b return blockers & self.occupied_co[self.turn] - def _is_safe(self, king, blockers, move): + def _is_safe(self, king: Square, blockers: Bitboard, move: Move) -> bool: if move.from_square == king: if self.is_castling(move): return True else: return not self.is_attacked_by(not self.turn, move.to_square) elif self.is_en_passant(move): - return (self.pin_mask(self.turn, move.from_square) & BB_SQUARES[move.to_square] and - not self._ep_skewered(king, move.from_square)) + return bool(self.pin_mask(self.turn, move.from_square) & BB_SQUARES[move.to_square] and + not self._ep_skewered(king, move.from_square)) else: - return (not blockers & BB_SQUARES[move.from_square] or - BB_RAYS[move.from_square][move.to_square] & BB_SQUARES[king]) + return bool(not blockers & BB_SQUARES[move.from_square] or + ray(move.from_square, move.to_square) & BB_SQUARES[king]) - def _generate_evasions(self, king, checkers, from_mask=BB_ALL, to_mask=BB_ALL): + def _generate_evasions(self, king: Square, checkers: Bitboard, from_mask: Bitboard = BB_ALL, to_mask: Bitboard = BB_ALL) -> Iterator[Move]: sliders = checkers & (self.bishops | self.rooks | self.queens) attacked = 0 for checker in scan_reversed(sliders): - attacked |= BB_RAYS[king][checker] & ~BB_SQUARES[checker] + attacked |= ray(king, checker) & ~BB_SQUARES[checker] if BB_SQUARES[king] & from_mask: for to_square in scan_reversed(BB_KING_ATTACKS[king] & ~self.occupied_co[self.turn] & ~attacked & to_mask): @@ -2952,20 +3788,18 @@ def _generate_evasions(self, king, checkers, from_mask=BB_ALL, to_mask=BB_ALL): checker = msb(checkers) if BB_SQUARES[checker] == checkers: # Capture or block a single checker. - target = BB_BETWEEN[king][checker] | checkers + target = between(king, checker) | checkers - for move in self.generate_pseudo_legal_moves(~self.kings & from_mask, target & to_mask): - yield move + yield from self.generate_pseudo_legal_moves(~self.kings & from_mask, target & to_mask) # Capture the checking pawn en passant (but avoid yielding # duplicate moves). if self.ep_square and not BB_SQUARES[self.ep_square] & target: last_double = self.ep_square + (-8 if self.turn == WHITE else 8) if last_double == checker: - for move in self.generate_pseudo_legal_ep(from_mask, to_mask): - yield move + yield from self.generate_pseudo_legal_ep(from_mask, to_mask) - def generate_legal_moves(self, from_mask=BB_ALL, to_mask=BB_ALL): + def generate_legal_moves(self, from_mask: Bitboard = BB_ALL, to_mask: Bitboard = BB_ALL) -> Iterator[Move]: if self.is_variant_end(): return @@ -2983,10 +3817,9 @@ def generate_legal_moves(self, from_mask=BB_ALL, to_mask=BB_ALL): if self._is_safe(king, blockers, move): yield move else: - for move in self.generate_pseudo_legal_moves(from_mask, to_mask): - yield move + yield from self.generate_pseudo_legal_moves(from_mask, to_mask) - def generate_legal_ep(self, from_mask=BB_ALL, to_mask=BB_ALL): + def generate_legal_ep(self, from_mask: Bitboard = BB_ALL, to_mask: Bitboard = BB_ALL) -> Iterator[Move]: if self.is_variant_end(): return @@ -2994,29 +3827,22 @@ def generate_legal_ep(self, from_mask=BB_ALL, to_mask=BB_ALL): if not self.is_into_check(move): yield move - def generate_legal_captures(self, from_mask=BB_ALL, to_mask=BB_ALL): + def generate_legal_captures(self, from_mask: Bitboard = BB_ALL, to_mask: Bitboard = BB_ALL) -> Iterator[Move]: return itertools.chain( self.generate_legal_moves(from_mask, to_mask & self.occupied_co[not self.turn]), self.generate_legal_ep(from_mask, to_mask)) - def _attacked_for_king(self, path, occupied): - return any(self._attackers_mask(not self.turn, sq, occupied) for sq in scan_reversed(path)) - - def _castling_uncovers_rank_attack(self, rook_bb, king_to): - # Test the special case where we castle and our rook shielded us from - # an attack, so castling would be into check. - rank_pieces = BB_RANK_MASKS[king_to] & (self.occupied ^ rook_bb) - sliders = (self.queens | self.rooks) & self.occupied_co[not self.turn] - return BB_RANK_ATTACKS[king_to][rank_pieces] & sliders + def _attacked_for_king(self, path: Bitboard, occupied: Bitboard) -> bool: + return any(self.attackers_mask(not self.turn, sq, occupied) for sq in scan_reversed(path)) - def generate_castling_moves(self, from_mask=BB_ALL, to_mask=BB_ALL): + def generate_castling_moves(self, from_mask: Bitboard = BB_ALL, to_mask: Bitboard = BB_ALL) -> Iterator[Move]: if self.is_variant_end(): return backrank = BB_RANK_1 if self.turn == WHITE else BB_RANK_8 - king = self.occupied_co[self.turn] & self.kings & ~self.promoted & backrank & from_mask - king = king & -king - if not king or self._attacked_for_king(king, self.occupied): + king = self.occupied_co[self.turn] & self.kings & ~self._effective_promoted() & backrank & from_mask + king &= -king + if not king: return bb_c = BB_FILE_C & backrank @@ -3028,30 +3854,19 @@ def generate_castling_moves(self, from_mask=BB_ALL, to_mask=BB_ALL): rook = BB_SQUARES[candidate] a_side = rook < king + king_to = bb_c if a_side else bb_g + rook_to = bb_d if a_side else bb_f - empty_for_rook = 0 - empty_for_king = 0 + king_path = between(msb(king), msb(king_to)) + rook_path = between(candidate, msb(rook_to)) - if a_side: - king_to = msb(bb_c) - if not rook & bb_d: - empty_for_rook = BB_BETWEEN[candidate][msb(bb_d)] | bb_d - if not king & bb_c: - empty_for_king = BB_BETWEEN[msb(king)][king_to] | bb_c - else: - king_to = msb(bb_g) - if not rook & bb_f: - empty_for_rook = BB_BETWEEN[candidate][msb(bb_f)] | bb_f - if not king & bb_g: - empty_for_king = BB_BETWEEN[msb(king)][king_to] | bb_g - - if not ((self.occupied ^ king ^ rook) & (empty_for_king | empty_for_rook) or - self._attacked_for_king(empty_for_king, self.occupied ^ king) or - self._castling_uncovers_rank_attack(rook, king_to)): + if not ((self.occupied ^ king ^ rook) & (king_path | rook_path | king_to | rook_to) or + self._attacked_for_king(king_path | king, self.occupied ^ king) or + self._attacked_for_king(king_to, self.occupied ^ king ^ rook ^ rook_to)): yield self._from_chess960(self.chess960, msb(king), candidate) - def _from_chess960(self, chess960, from_square, to_square, promotion=None, drop=None): - if not chess960 and drop is None: + def _from_chess960(self, chess960: bool, from_square: Square, to_square: Square, promotion: Optional[PieceType] = None, drop: Optional[PieceType] = None) -> Move: + if not chess960 and promotion is None and drop is None: if from_square == E1 and self.kings & BB_E1: if to_square == H1: return Move(E1, G1) @@ -3065,7 +3880,7 @@ def _from_chess960(self, chess960, from_square, to_square, promotion=None, drop= return Move(from_square, to_square, promotion, drop) - def _to_chess960(self, move): + def _to_chess960(self, move: Move) -> Move: if move.from_square == E1 and self.kings & BB_E1: if move.to_square == G1 and not self.rooks & BB_G1: return Move(E1, H1) @@ -3079,44 +3894,76 @@ def _to_chess960(self, move): return move - def _transposition_key(self): + def _transposition_key(self) -> Hashable: return (self.pawns, self.knights, self.bishops, self.rooks, self.queens, self.kings, - self.occupied_co[WHITE], self.occupied_co[BLACK], self.promoted, + self._effective_promoted(), + self.occupied_co[WHITE], self.occupied_co[BLACK], self.turn, self.clean_castling_rights(), self.ep_square if self.has_legal_en_passant() else None) - def __repr__(self): + def __repr__(self) -> str: if not self.chess960: - return "{0}('{1}')".format(type(self).__name__, self.fen()) + return f"{type(self).__name__}({self.fen()!r})" else: - return "{0}('{1}', chess960=True)".format(type(self).__name__, self.fen()) + return f"{type(self).__name__}({self.fen()!r}, chess960=True)" - def _repr_svg_(self): + def _repr_svg_(self) -> str: import chess.svg - lastmove = self.peek() if self.move_stack else None - check = self.king(self.turn) if self.is_check() else None - return chess.svg.board(board=self, lastmove=lastmove, check=check, size=400) + return chess.svg.board( + board=self, + size=390, + lastmove=self.peek() if self.move_stack else None, + check=self.king(self.turn) if self.is_check() else None) + + def __eq__(self, board: object) -> bool: + if isinstance(board, Board): + return ( + self.halfmove_clock == board.halfmove_clock and + self.fullmove_number == board.fullmove_number and + type(self).uci_variant == type(board).uci_variant and + self._transposition_key() == board._transposition_key()) + else: + return NotImplemented - def __ne__(self, board): - # Compare positions (including move counters), but excluding history. - try: - if self.halfmove_clock != board.halfmove_clock: - return True - if self.fullmove_number != board.fullmove_number: - return True + def apply_transform(self, f: Callable[[Bitboard], Bitboard]) -> None: + super().apply_transform(f) + self.clear_stack() + self.ep_square = None if self.ep_square is None else msb(f(BB_SQUARES[self.ep_square])) + self.castling_rights = f(self.castling_rights) - if type(self).uci_variant != type(board).uci_variant: - return True - if self._transposition_key() != board._transposition_key(): - return True - except AttributeError: - return NotImplemented - else: - return False + def transform(self, f: Callable[[Bitboard], Bitboard]) -> Self: + board = self.copy(stack=False) + board.apply_transform(f) + return board + + def apply_mirror(self) -> None: + super().apply_mirror() + self.turn = not self.turn + + def mirror(self) -> Self: + """ + Returns a mirrored copy of the board. + + The board is mirrored vertically and piece colors are swapped, so that + the position is equivalent modulo color. Also swap the "en passant" + square, castling rights and turn. + + Alternatively, :func:`~chess.Board.apply_mirror()` can be used + to mirror the board. + """ + board = self.copy() + board.apply_mirror() + return board + + def copy(self, *, stack: Union[bool, int] = True) -> Self: + """ + Creates a copy of the board. - def copy(self, stack=True): - board = super(Board, self).copy() + Defaults to copying the entire move stack. Alternatively, *stack* can + be ``False``, or an integer to copy a limited number of moves. + """ + board = super().copy() board.chess960 = self.chess960 @@ -3127,18 +3974,19 @@ def copy(self, stack=True): board.halfmove_clock = self.halfmove_clock if stack: - board.move_stack = copy.deepcopy(self.move_stack) - board.stack = copy.copy(self.stack) + stack = len(self.move_stack) if stack is True else stack + board.move_stack = [copy.copy(move) for move in self.move_stack[-stack:]] + board._stack = self._stack[-stack:] return board @classmethod - def empty(cls, chess960=False): + def empty(cls: Type[BoardT], *, chess960: bool = False) -> BoardT: """Creates a new empty board. Also see :func:`~chess.Board.clear()`.""" return cls(None, chess960=chess960) @classmethod - def from_epd(cls, epd, chess960=False): + def from_epd(cls: Type[BoardT], epd: str, *, chess960: bool = False) -> Tuple[BoardT, Dict[str, Union[None, str, int, float, Move, List[Move]]]]: """ Creates a new board from an EPD string. See :func:`~chess.Board.set_epd()`. @@ -3149,33 +3997,32 @@ def from_epd(cls, epd, chess960=False): return board, board.set_epd(epd) @classmethod - def from_chess960_pos(cls, sharnagl): + def from_chess960_pos(cls: Type[BoardT], scharnagl: int) -> BoardT: board = cls.empty(chess960=True) - board.set_chess960_pos(sharnagl) + board.set_chess960_pos(scharnagl) return board -class PseudoLegalMoveGenerator(object): +class PseudoLegalMoveGenerator: - def __init__(self, board): + def __init__(self, board: Board) -> None: self.board = board - def __bool__(self): + def __bool__(self) -> bool: return any(self.board.generate_pseudo_legal_moves()) - __nonzero__ = __bool__ + def count(self) -> int: + # List conversion is faster than iterating. + return len(list(self)) - def __len__(self): - return sum(1 for _ in self.board.generate_pseudo_legal_moves()) - - def __iter__(self): + def __iter__(self) -> Iterator[Move]: return self.board.generate_pseudo_legal_moves() - def __contains__(self, move): + def __contains__(self, move: Move) -> bool: return self.board.is_pseudo_legal(move) - def __repr__(self): - builder = [] + def __repr__(self) -> str: + builder: List[str] = [] for move in self: if self.board.is_legal(move): @@ -3184,53 +4031,60 @@ def __repr__(self): builder.append(self.board.uci(move)) sans = ", ".join(builder) - - return "".format(hex(id(self)), sans) + return f"" -class LegalMoveGenerator(object): +class LegalMoveGenerator: - def __init__(self, board): + def __init__(self, board: Board) -> None: self.board = board - def __bool__(self): + def __bool__(self) -> bool: return any(self.board.generate_legal_moves()) - __nonzero__ = __bool__ + def count(self) -> int: + # List conversion is faster than iterating. + return len(list(self)) - def __len__(self): - return sum(1 for _ in self.board.generate_legal_moves()) - - def __iter__(self): + def __iter__(self) -> Iterator[Move]: return self.board.generate_legal_moves() - def __contains__(self, move): + def __contains__(self, move: Move) -> bool: return self.board.is_legal(move) - def __repr__(self): + def __repr__(self) -> str: sans = ", ".join(self.board.san(move) for move in self) - return "".format(hex(id(self)), sans) + return f"" + +IntoSquareSet: TypeAlias = Union[SupportsInt, Iterable[Square]] -class SquareSet(object): +class SquareSet: """ A set of squares. - >>> chess.SquareSet(chess.BB_A8 | chess.BB_RANK_1) - SquareSet(0x01000000000000ff) + >>> import chess + >>> + >>> squares = chess.SquareSet([chess.A8, chess.A1]) + >>> squares + SquareSet(0x0100_0000_0000_0001) + + >>> squares = chess.SquareSet(chess.BB_A8 | chess.BB_RANK_1) + >>> squares + SquareSet(0x0100_0000_0000_00ff) >>> print(squares) + 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . - . . . . . . . . - . 1 . . . . 1 . + 1 1 1 1 1 1 1 1 >>> len(squares) - 2 + 9 >>> bool(squares) True @@ -3239,22 +4093,36 @@ class SquareSet(object): True >>> for square in squares: + ... # 0 -- chess.A1 ... # 1 -- chess.B1 + ... # 2 -- chess.C1 + ... # 3 -- chess.D1 + ... # 4 -- chess.E1 + ... # 5 -- chess.F1 ... # 6 -- chess.G1 + ... # 7 -- chess.H1 + ... # 56 -- chess.A8 ... print(square) ... + 0 1 + 2 + 3 + 4 + 5 6 + 7 + 56 >>> list(squares) - [1, 6] + [0, 1, 2, 3, 4, 5, 6, 7, 56] - Square sets are internally represented by 64 bit integer masks of the + Square sets are internally represented by 64-bit integer masks of the included squares. Bitwise operations can be used to compute unions, intersections and shifts. >>> int(squares) - 66 + 72057594037928191 Also supports common set operations like :func:`~chess.SquareSet.issubset()`, :func:`~chess.SquareSet.issuperset()`, @@ -3269,51 +4137,128 @@ class SquareSet(object): :func:`~chess.SquareSet.clear()`. """ - def __init__(self, mask=BB_VOID): - self.mask = mask + def __init__(self, squares: IntoSquareSet = BB_EMPTY) -> None: + try: + self.mask: Bitboard = squares.__int__() & BB_ALL # type: ignore + return + except AttributeError: + self.mask = 0 + + # Try squares as an iterable. Not under except clause for nicer + # backtraces. + for square in squares: # type: ignore + self.add(square) - def issubset(self, other): - return not bool(~self & other) + # Set + + def __contains__(self, square: Square) -> bool: + return bool(BB_SQUARES[square] & self.mask) + + def __iter__(self) -> Iterator[Square]: + return scan_forward(self.mask) + + def __reversed__(self) -> Iterator[Square]: + return scan_reversed(self.mask) + + def __len__(self) -> int: + return popcount(self.mask) + + # MutableSet + + def add(self, square: Square) -> None: + """Adds a square to the set.""" + self.mask |= BB_SQUARES[square] + + def discard(self, square: Square) -> None: + """Discards a square from the set.""" + self.mask &= ~BB_SQUARES[square] + + # frozenset + + def isdisjoint(self, other: IntoSquareSet) -> bool: + """Tests if the square sets are disjoint.""" + return not bool(self & other) + + def issubset(self, other: IntoSquareSet) -> bool: + """Tests if this square set is a subset of another.""" + return not bool(self & ~SquareSet(other)) - def issuperset(self, other): - return not bool(self & ~other) + def issuperset(self, other: IntoSquareSet) -> bool: + """Tests if this square set is a superset of another.""" + return not bool(~self & other) - def union(self, other): + def union(self, other: IntoSquareSet) -> SquareSet: return self | other - def intersection(self, other): + def __or__(self, other: IntoSquareSet) -> SquareSet: + r = SquareSet(other) + r.mask |= self.mask + return r + + def intersection(self, other: IntoSquareSet) -> SquareSet: return self & other - def difference(self, other): - return self & ~other + def __and__(self, other: IntoSquareSet) -> SquareSet: + r = SquareSet(other) + r.mask &= self.mask + return r + + def difference(self, other: IntoSquareSet) -> SquareSet: + return self - other + + def __sub__(self, other: IntoSquareSet) -> SquareSet: + r = SquareSet(other) + r.mask = self.mask & ~r.mask + return r - def symmetric_difference(self, other): + def symmetric_difference(self, other: IntoSquareSet) -> SquareSet: return self ^ other - def update(self, other): - self |= other + def __xor__(self, other: IntoSquareSet) -> SquareSet: + r = SquareSet(other) + r.mask ^= self.mask + return r - def intersection_update(self, other): - self &= other + def copy(self) -> SquareSet: + return SquareSet(self.mask) - def difference_update(self, other): - self &= ~other + # set - def symmetric_difference_update(self, other): - self ^= other + def update(self, *others: IntoSquareSet) -> None: + for other in others: + self |= other + + def __ior__(self, other: IntoSquareSet) -> SquareSet: + self.mask |= SquareSet(other).mask + return self - def copy(self): - return type(self)(self.mask) + def intersection_update(self, *others: IntoSquareSet) -> None: + for other in others: + self &= other - def add(self, square): - """Add a square to the set.""" - self.mask |= BB_SQUARES[square] + def __iand__(self, other: IntoSquareSet) -> SquareSet: + self.mask &= SquareSet(other).mask + return self + + def difference_update(self, other: IntoSquareSet) -> None: + self -= other + + def __isub__(self, other: IntoSquareSet) -> SquareSet: + self.mask &= ~SquareSet(other).mask + return self - def remove(self, square): + def symmetric_difference_update(self, other: IntoSquareSet) -> None: + self ^= other + + def __ixor__(self, other: IntoSquareSet) -> SquareSet: + self.mask ^= SquareSet(other).mask + return self + + def remove(self, square: Square) -> None: """ - Remove a square from the set. + Removes a square from the set. - :raises: :exc:`KeyError` if the given square was not in the set. + :raises: :exc:`KeyError` if the given *square* was not in the set. """ mask = BB_SQUARES[square] if self.mask & mask: @@ -3321,153 +4266,141 @@ def remove(self, square): else: raise KeyError(square) - def discard(self, square): - """Discards a square from the set.""" - self.mask &= ~BB_SQUARES[square] - - def pop(self): + def pop(self) -> Square: """ - Removes a square from the set and returns it. + Removes and returns a square from the set. - :raises: :exc:`KeyError` on an empty set. + :raises: :exc:`KeyError` if the set is empty. """ if not self.mask: - raise KeyError("pop from empty set") + raise KeyError("pop from empty SquareSet") square = lsb(self.mask) self.mask &= (self.mask - 1) return square - def clear(self): - self.mask = BB_VOID - - def __bool__(self): - return bool(self.mask) - - __nonzero__ = __bool__ - - def __eq__(self, other): - ne = self.__ne__(other) - return NotImplemented if ne is NotImplemented else not ne - - def __ne__(self, other): - try: - return self.mask != int(other) - except ValueError: - return NotImplemented - - def __len__(self): - return popcount(self.mask) + def clear(self) -> None: + """Removes all elements from this set.""" + self.mask = BB_EMPTY - def __iter__(self): - return scan_forward(self.mask) + # SquareSet - def __reversed__(self): - return scan_reversed(self.mask) + def carry_rippler(self) -> Iterator[Bitboard]: + """Iterator over the subsets of this set.""" + return _carry_rippler(self.mask) - def __contains__(self, square): - return bool(BB_SQUARES[square] & self.mask) + def mirror(self) -> SquareSet: + """Returns a vertically mirrored copy of this square set.""" + return SquareSet(flip_vertical(self.mask)) - def __lshift__(self, shift): - return type(self)((self.mask << shift) & BB_ALL) + def tolist(self) -> List[bool]: + """Converts the set to a list of 64 bools.""" + result = [False] * 64 + for square in self: + result[square] = True + return result - def __rshift__(self, shift): - return type(self)(self.mask >> shift) + def __bool__(self) -> bool: + return bool(self.mask) - def __and__(self, other): + def __eq__(self, other: object) -> bool: try: - return type(self)(self.mask & other.mask) - except AttributeError: - return type(self)(self.mask & other) + return self.mask == SquareSet(other).mask # type: ignore + except (TypeError, ValueError): + return NotImplemented - def __xor__(self, other): - try: - return type(self)((self.mask ^ other.mask) & BB_ALL) - except AttributeError: - return type(self)((self.mask ^ other) & BB_ALL) + def __lshift__(self, shift: int) -> SquareSet: + return SquareSet((self.mask << shift) & BB_ALL) - def __or__(self, other): - try: - return type(self)((self.mask | other.mask) & BB_ALL) - except AttributeError: - return type(self)((self.mask | other) & BB_ALL) + def __rshift__(self, shift: int) -> SquareSet: + return SquareSet(self.mask >> shift) - def __ilshift__(self, shift): - self.mask = (self.mask << shift & BB_ALL) + def __ilshift__(self, shift: int) -> SquareSet: + self.mask = (self.mask << shift) & BB_ALL return self - def __irshift__(self, shift): + def __irshift__(self, shift: int) -> SquareSet: self.mask >>= shift return self - def __iand__(self, other): - try: - self.mask &= other.mask - except AttributeError: - self.mask &= other - return self - - def __ixor__(self, other): - try: - self.mask = (self.mask ^ other.mask) & BB_ALL - except AttributeError: - self.mask = (self.mask ^ other) & BB_ALL - return self - - def __ior__(self, other): - try: - self.mask = (self.mask | other.mask) & BB_ALL - except AttributeError: - self.mask = (self.mask | other) & BB_ALL - return self - - def __invert__(self): - return type(self)(~self.mask & BB_ALL) - - def __oct__(self): - return oct(self.mask) - - def __hex__(self): - return hex(self.mask) + def __invert__(self) -> SquareSet: + return SquareSet(~self.mask & BB_ALL) - def __int__(self): + def __int__(self) -> int: return self.mask - def __index__(self): + def __index__(self) -> int: return self.mask - def __repr__(self): - return "SquareSet({0:#018x})".format(self.mask) + def __repr__(self) -> str: + return f"SquareSet({self.mask:#021_x})" - def __str__(self): - builder = [] + def __str__(self) -> str: + builder: List[str] = [] for square in SQUARES_180: mask = BB_SQUARES[square] + builder.append("1" if self.mask & mask else ".") - if self.mask & mask: - builder.append("1") - else: - builder.append(".") - - if mask & BB_FILE_H: - if square != H1: - builder.append("\n") - else: + if not mask & BB_FILE_H: builder.append(" ") + elif square != H1: + builder.append("\n") return "".join(builder) - def _repr_svg_(self): + def _repr_svg_(self) -> str: import chess.svg - return chess.svg.board(squares=self, size=400) + return chess.svg.board(squares=self, size=390) + + @classmethod + def ray(cls, a: Square, b: Square) -> SquareSet: + """ + All squares on the rank, file or diagonal with the two squares, if they + are aligned. + + >>> import chess + >>> + >>> print(chess.SquareSet.ray(chess.E2, chess.B5)) + . . . . . . . . + . . . . . . . . + 1 . . . . . . . + . 1 . . . . . . + . . 1 . . . . . + . . . 1 . . . . + . . . . 1 . . . + . . . . . 1 . . + """ + return cls(ray(a, b)) + + @classmethod + def between(cls, a: Square, b: Square) -> SquareSet: + """ + All squares on the rank, file or diagonal between the two squares + (bounds not included), if they are aligned. + + >>> import chess + >>> + >>> print(chess.SquareSet.between(chess.E2, chess.B5)) + . . . . . . . . + . . . . . . . . + . . . . . . . . + . . . . . . . . + . . 1 . . . . . + . . . 1 . . . . + . . . . . . . . + . . . . . . . . + """ + return cls(between(a, b)) @classmethod - def from_square(cls, square): + def from_square(cls, square: Square) -> SquareSet: """ - Creates a SquareSet from a single square. + Creates a :class:`~chess.SquareSet` from a single square. - >>> chess.SquareSet.from_square(chess.A1) == chess.BB_A1: + >>> import chess + >>> + >>> chess.SquareSet.from_square(chess.A1) == chess.BB_A1 True """ return cls(BB_SQUARES[square]) diff --git a/chess/engine.py b/chess/engine.py index b1f22dcd6..c66bc0c45 100644 --- a/chess/engine.py +++ b/chess/engine.py @@ -1,282 +1,3114 @@ -# -*- coding: utf-8 -*- -# -# This file is part of the python-chess library. -# Copyright (C) 2012-2017 Niklas Fiekas -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - +from __future__ import annotations + +import abc +import asyncio +import collections +import concurrent.futures +import contextlib +import copy +import dataclasses +import enum +import inspect import logging -import threading -import os +import math +import shlex +import subprocess import sys -import signal -import platform +import threading +import time +import typing +import re -try: - import backport_collections as collections -except ImportError: - import collections +import chess -try: - import queue -except ImportError: - import Queue as queue +from chess import Color +from types import TracebackType +from typing import Any, Callable, Coroutine, Deque, Dict, Generator, Generic, Iterable, Iterator, List, Literal, Mapping, MutableMapping, Optional, Tuple, Type, TypedDict, TypeVar, Union -if os.name == "posix" and sys.version_info[0] < 3: - try: - import subprocess32 as subprocess - except ImportError: - import subprocess +if typing.TYPE_CHECKING: + from typing_extensions import override else: - import subprocess + F = typing.TypeVar("F", bound=Callable[..., Any]) + def override(fn: F, /) -> F: + return fn + +if typing.TYPE_CHECKING: + from typing_extensions import Self + +WdlModel = Literal["sf", "sf16.1", "sf16", "sf15.1", "sf15", "sf14", "sf12", "lichess"] + + +T = TypeVar("T") +ProtocolT = TypeVar("ProtocolT", bound="Protocol") +ConfigValue = Union[str, int, bool, None] +ConfigMapping = Mapping[str, ConfigValue] -FUTURE_POLL_TIMEOUT = 0.1 if platform.system() == "Windows" else 60 LOGGER = logging.getLogger(__name__) -class EngineTerminatedException(Exception): - """The engine has been terminated.""" - pass +MANAGED_OPTIONS = ["uci_chess960", "uci_variant", "multipv", "ponder"] -class EngineStateException(Exception): - """Unexpected engine state.""" - pass +def run_in_background(coroutine: Callable[[concurrent.futures.Future[T]], Coroutine[Any, Any, None]], *, name: Optional[str] = None, debug: Optional[bool] = None) -> T: + """ + Runs ``coroutine(future)`` in a new event loop on a background thread. + Blocks on *future* and returns the result as soon as it is resolved. + The coroutine and all remaining tasks continue running in the background + until complete. + """ + assert inspect.iscoroutinefunction(coroutine) -class MockProcess(object): - def __init__(self, engine): - self.engine = engine - self._expectations = collections.deque() - self._is_dead = threading.Event() - self._std_streams_closed = False + future: concurrent.futures.Future[T] = concurrent.futures.Future() - self.engine.on_process_spawned(self) + def background() -> None: + try: + asyncio.run(coroutine(future), debug=debug) + future.cancel() + except Exception as exc: + future.set_exception(exc) - self._send_queue = queue.Queue() - self._send_thread = threading.Thread(target=self._send_thread_target) - self._send_thread.daemon = True - self._send_thread.start() + threading.Thread(target=background, name=name).start() + return future.result() - def _send_thread_target(self): - while not self._is_dead.is_set(): - line = self._send_queue.get() - if line is not None: - self.engine.on_line_received(line) - self._send_queue.task_done() - def expect(self, expectation, responses=()): - self._expectations.append((expectation, responses)) +class EngineError(RuntimeError): + """Runtime error caused by a misbehaving engine or incorrect usage.""" - def assert_done(self): - assert not self._expectations, "pending expectations: {0}".format(self._expectations) - def assert_terminated(self): - self.assert_done() - assert self._is_dead.is_set() +class EngineTerminatedError(EngineError): + """The engine process exited unexpectedly.""" - def is_alive(self): - return not self._is_dead.is_set() - def terminate(self): - self._is_dead.set() - self._send_queue.put(None) - self.engine.on_terminated() +class AnalysisComplete(Exception): + """ + Raised when analysis is complete, all information has been consumed, but + further information was requested. + """ - def kill(self): - self._is_dead.set() - self._send_queue.put(None) - self.engine.on_terminated() - def send_line(self, string): - assert self.is_alive() +@dataclasses.dataclass(frozen=True) +class Option: + """Information about an available engine option.""" - assert self._expectations, "unexpected: {0}".format(string) - expectation, responses = self._expectations.popleft() - assert expectation == string, "expected: {0}, got {1}".format(expectation, string) + name: str + """The name of the option.""" - for response in responses: - self._send_queue.put(response) + type: str + """ + The type of the option. + + +--------+-----+------+------------------------------------------------+ + | type | UCI | CECP | value | + +========+=====+======+================================================+ + | check | X | X | ``True`` or ``False`` | + +--------+-----+------+------------------------------------------------+ + | spin | X | X | integer, between *min* and *max* | + +--------+-----+------+------------------------------------------------+ + | combo | X | X | string, one of *var* | + +--------+-----+------+------------------------------------------------+ + | button | X | X | ``None`` | + +--------+-----+------+------------------------------------------------+ + | reset | | X | ``None`` | + +--------+-----+------+------------------------------------------------+ + | save | | X | ``None`` | + +--------+-----+------+------------------------------------------------+ + | string | X | X | string without line breaks | + +--------+-----+------+------------------------------------------------+ + | file | | X | string, interpreted as the path to a file | + +--------+-----+------+------------------------------------------------+ + | path | | X | string, interpreted as the path to a directory | + +--------+-----+------+------------------------------------------------+ + """ - def wait_for_return_code(self): - self._is_dead.wait() - return 0 + default: ConfigValue + """The default value of the option.""" + + min: Optional[int] + """The minimum integer value of a *spin* option.""" + + max: Optional[int] + """The maximum integer value of a *spin* option.""" + + var: Optional[List[str]] + """A list of allowed string values for a *combo* option.""" + + def parse(self, value: ConfigValue) -> ConfigValue: + if self.type == "check": + return value and value != "false" + elif self.type == "spin": + try: + value = int(value) # type: ignore + except ValueError: + raise EngineError(f"expected integer for spin option {self.name!r}, got: {value!r}") + if self.min is not None and value < self.min: + raise EngineError(f"expected value for option {self.name!r} to be at least {self.min}, got: {value}") + if self.max is not None and self.max < value: + raise EngineError(f"expected value for option {self.name!r} to be at most {self.max}, got: {value}") + return value + elif self.type == "combo": + value = str(value) + if value not in (self.var or []): + raise EngineError("invalid value for combo option {!r}, got: {} (available: {})".format(self.name, value, ", ".join(self.var) if self.var else "-")) + return value + elif self.type in ["button", "reset", "save"]: + return None + elif self.type in ["string", "file", "path"]: + value = str(value) + if "\n" in value or "\r" in value: + raise EngineError(f"invalid line-break in string option {self.name!r}: {value!r}") + return value + else: + raise EngineError(f"unknown option type: {self.type!r}") + + def is_managed(self) -> bool: + """ + Some options are managed automatically: ``UCI_Chess960``, + ``UCI_Variant``, ``MultiPV``, ``Ponder``. + """ + return self.name.lower() in MANAGED_OPTIONS + + +@dataclasses.dataclass +class Limit: + """Search-termination condition.""" + + time: Optional[float] = None + """Search exactly *time* seconds.""" + + depth: Optional[int] = None + """Search *depth* ply only.""" + + nodes: Optional[int] = None + """Search only a limited number of *nodes*.""" + + mate: Optional[int] = None + """Search for a mate in *mate* moves.""" + + white_clock: Optional[float] = None + """Time in seconds remaining for White.""" + + black_clock: Optional[float] = None + """Time in seconds remaining for Black.""" + + white_inc: Optional[float] = None + """Fisher increment for White, in seconds.""" + + black_inc: Optional[float] = None + """Fisher increment for Black, in seconds.""" + + remaining_moves: Optional[int] = None + """ + Number of moves to the next time control. If this is not set, but + *white_clock* and *black_clock* are, then it is sudden death. + """ + + clock_id: object = None + """ + An identifier to use with XBoard engines to signal that the time + control has changed. When this field changes, Xboard engines are + sent level or st commands as appropriate. Otherwise, only time + and otim commands are sent to update the engine's clock. + """ + + def __repr__(self) -> str: + # Like default __repr__, but without None values. + return "{}({})".format( + type(self).__name__, + ", ".join("{}={!r}".format(attr, getattr(self, attr)) + for attr in ["time", "depth", "nodes", "mate", "white_clock", "black_clock", "white_inc", "black_inc", "remaining_moves"] + if getattr(self, attr) is not None)) + + +class InfoDict(TypedDict, total=False): + """ + Dictionary of aggregated information sent by the engine. + + Commonly used keys are: ``score`` (a :class:`~chess.engine.PovScore`), + ``pv`` (a list of :class:`~chess.Move` objects), ``depth``, + ``seldepth``, ``time`` (in seconds), ``nodes``, ``nps``, ``multipv`` + (``1`` for the mainline). + + Others: ``tbhits``, ``currmove``, ``currmovenumber``, ``hashfull``, + ``cpuload``, ``refutation``, ``currline``, ``ebf`` (effective branching factor), + ``wdl`` (a :class:`~chess.engine.PovWdl`), and ``string``. + """ + score: PovScore + pv: List[chess.Move] + depth: int + seldepth: int + time: float + nodes: int + nps: int + tbhits: int + multipv: int + currmove: chess.Move + currmovenumber: int + hashfull: int + cpuload: int + refutation: Dict[chess.Move, List[chess.Move]] + currline: Dict[int, List[chess.Move]] + ebf: float + wdl: PovWdl + string: str + + +class PlayResult: + """Returned by :func:`chess.engine.Protocol.play()`.""" + + move: Optional[chess.Move] + """The best move according to the engine, or ``None``.""" + + ponder: Optional[chess.Move] + """The response that the engine expects after *move*, or ``None``.""" + + info: InfoDict + """ + A dictionary of extra :class:`information ` + sent by the engine, if selected with the *info* argument of + :func:`~chess.engine.Protocol.play()`. + """ + + draw_offered: bool + """Whether the engine offered a draw before moving.""" + + resigned: bool + """Whether the engine resigned.""" + + def __init__(self, + move: Optional[chess.Move], + ponder: Optional[chess.Move], + info: Optional[InfoDict] = None, + *, + draw_offered: bool = False, + resigned: bool = False) -> None: + self.move = move + self.ponder = ponder + self.info = info or {} + self.draw_offered = draw_offered + self.resigned = resigned + + def __repr__(self) -> str: + return "<{} at {:#x} (move={}, ponder={}, info={}, draw_offered={}, resigned={})>".format( + type(self).__name__, id(self), self.move, self.ponder, self.info, + self.draw_offered, self.resigned) + + +class Info(enum.IntFlag): + """Used to filter information sent by the chess engine.""" + NONE = 0 + BASIC = 1 + SCORE = 2 + PV = 4 + REFUTATION = 8 + CURRLINE = 16 + ALL = BASIC | SCORE | PV | REFUTATION | CURRLINE + +INFO_NONE = Info.NONE +INFO_BASIC = Info.BASIC +INFO_SCORE = Info.SCORE +INFO_PV = Info.PV +INFO_REFUTATION = Info.REFUTATION +INFO_CURRLINE = Info.CURRLINE +INFO_ALL = Info.ALL + + +@dataclasses.dataclass +class Opponent: + """Used to store information about an engine's opponent.""" + + name: Optional[str] + """The name of the opponent.""" + + title: Optional[str] + """The opponent's title--for example, GM, IM, or BOT.""" + + rating: Optional[int] + """The opponent's ELO rating.""" + + is_engine: Optional[bool] + """Whether the opponent is a chess engine/computer program.""" + + +class PovScore: + """A relative :class:`~chess.engine.Score` and the point of view.""" + + relative: Score + """The relative :class:`~chess.engine.Score`.""" + + turn: Color + """The point of view (``chess.WHITE`` or ``chess.BLACK``).""" + + def __init__(self, relative: Score, turn: Color) -> None: + self.relative = relative + self.turn = turn + + def white(self) -> Score: + """Gets the score from White's point of view.""" + return self.pov(chess.WHITE) + + def black(self) -> Score: + """Gets the score from Black's point of view.""" + return self.pov(chess.BLACK) - def pid(self): + def pov(self, color: Color) -> Score: + """Gets the score from the point of view of the given *color*.""" + return self.relative if self.turn == color else -self.relative + + def is_mate(self) -> bool: + """Tests if this is a mate score.""" + return self.relative.is_mate() + + def wdl(self, *, model: WdlModel = "sf", ply: int = 30) -> PovWdl: + """See :func:`~chess.engine.Score.wdl()`.""" + return PovWdl(self.relative.wdl(model=model, ply=ply), self.turn) + + def __repr__(self) -> str: + return "PovScore({!r}, {})".format(self.relative, "WHITE" if self.turn else "BLACK") + + def __eq__(self, other: object) -> bool: + if isinstance(other, PovScore): + return self.white() == other.white() + else: + return NotImplemented + + +class Score(abc.ABC): + """ + Evaluation of a position. + + The score can be :class:`~chess.engine.Cp` (centi-pawns), + :class:`~chess.engine.Mate` or :py:data:`~chess.engine.MateGiven`. + A positive value indicates an advantage. + + There is a total order defined on centi-pawn and mate scores. + + >>> from chess.engine import Cp, Mate, MateGiven + >>> + >>> Mate(-0) < Mate(-1) < Cp(-50) < Cp(200) < Mate(4) < Mate(1) < MateGiven + True + + Scores can be negated to change the point of view: + + >>> -Cp(20) + Cp(-20) + + >>> -Mate(-4) + Mate(+4) + + >>> -Mate(0) + MateGiven + """ + + @typing.overload + @abc.abstractmethod + def score(self, *, mate_score: int) -> int: ... + @typing.overload + @abc.abstractmethod + def score(self, *, mate_score: Optional[int] = None) -> Optional[int]: ... + @abc.abstractmethod + def score(self, *, mate_score: Optional[int] = None) -> Optional[int]: + """ + Returns the centi-pawn score as an integer or ``None``. + + You can optionally pass a large value to convert mate scores to + centi-pawn scores. + + >>> Cp(-300).score() + -300 + >>> Mate(5).score() is None + True + >>> Mate(5).score(mate_score=100000) + 99995 + """ + + @abc.abstractmethod + def mate(self) -> Optional[int]: + """ + Returns the number of plies to mate, negative if we are getting + mated, or ``None``. + + .. warning:: + This conflates ``Mate(0)`` (we lost) and ``MateGiven`` + (we won) to ``0``. + """ + + def is_mate(self) -> bool: + """Tests if this is a mate score.""" + return self.mate() is not None + + @abc.abstractmethod + def wdl(self, *, model: WdlModel = "sf", ply: int = 30) -> Wdl: + """ + Returns statistics for the expected outcome of this game, based on + a *model*, given that this score is reached at *ply*. + + Scores have a total order, but it makes little sense to compute + the difference between two scores. For example, going from + ``Cp(-100)`` to ``Cp(+100)`` is much more significant than going + from ``Cp(+300)`` to ``Cp(+500)``. It is better to compute differences + of the expectation values for the outcome of the game (based on winning + chances and drawing chances). + + >>> Cp(100).wdl().expectation() - Cp(-100).wdl().expectation() # doctest: +ELLIPSIS + 0.379... + + >>> Cp(500).wdl().expectation() - Cp(300).wdl().expectation() # doctest: +ELLIPSIS + 0.015... + + :param model: + * ``sf``, the WDL model used by the latest Stockfish + (currently ``sf16``). + * ``sf16``, the WDL model used by Stockfish 16. + * ``sf15.1``, the WDL model used by Stockfish 15.1. + * ``sf15``, the WDL model used by Stockfish 15. + * ``sf14``, the WDL model used by Stockfish 14. + * ``sf12``, the WDL model used by Stockfish 12. + * ``lichess``, the win rate model used by Lichess. + Does not use *ply*, and does not consider drawing chances. + :param ply: The number of half-moves played since the starting + position. Models may scale scores slightly differently based on + this. Defaults to middle game. + """ + + @abc.abstractmethod + def __neg__(self) -> Score: + ... + + @abc.abstractmethod + def __pos__(self) -> Score: + ... + + @abc.abstractmethod + def __abs__(self) -> Score: + ... + + def _score_tuple(self) -> Tuple[bool, bool, bool, int, Optional[int]]: + mate = self.mate() + return ( + isinstance(self, MateGivenType), + mate is not None and mate > 0, + mate is None, + -(mate or 0), + self.score(), + ) + + def __eq__(self, other: object) -> bool: + if isinstance(other, Score): + return self._score_tuple() == other._score_tuple() + else: + return NotImplemented + + def __lt__(self, other: object) -> bool: + if isinstance(other, Score): + return self._score_tuple() < other._score_tuple() + else: + return NotImplemented + + def __le__(self, other: object) -> bool: + if isinstance(other, Score): + return self._score_tuple() <= other._score_tuple() + else: + return NotImplemented + + def __gt__(self, other: object) -> bool: + if isinstance(other, Score): + return self._score_tuple() > other._score_tuple() + else: + return NotImplemented + + def __ge__(self, other: object) -> bool: + if isinstance(other, Score): + return self._score_tuple() >= other._score_tuple() + else: + return NotImplemented + +def _sf16_1_wins(cp: int, *, ply: int) -> int: + # https://github.com/official-stockfish/Stockfish/blob/sf_16.1/src/uci.cpp#L48 + NormalizeToPawnValue = 356 + # https://github.com/official-stockfish/Stockfish/blob/sf_16.1/src/uci.cpp#L383-L384 + m = min(120, max(8, ply / 2 + 1)) / 32 + a = (((-1.06249702 * m + 7.42016937) * m + 0.89425629) * m) + 348.60356174 + b = (((-5.33122190 * m + 39.57831533) * m + -90.84473771) * m) + 123.40620748 + x = min(4000, max(cp * NormalizeToPawnValue / 100, -4000)) + return int(0.5 + 1000 / (1 + math.exp((a - x) / b))) + +def _sf16_wins(cp: int, *, ply: int) -> int: + # https://github.com/official-stockfish/Stockfish/blob/sf_16/src/uci.h#L38 + NormalizeToPawnValue = 328 + # https://github.com/official-stockfish/Stockfish/blob/sf_16/src/uci.cpp#L200-L224 + m = min(240, max(ply, 0)) / 64 + a = (((0.38036525 * m + -2.82015070) * m + 23.17882135) * m) + 307.36768407 + b = (((-2.29434733 * m + 13.27689788) * m + -14.26828904) * m) + 63.45318330 + x = min(4000, max(cp * NormalizeToPawnValue / 100, -4000)) + return int(0.5 + 1000 / (1 + math.exp((a - x) / b))) + +def _sf15_1_wins(cp: int, *, ply: int) -> int: + # https://github.com/official-stockfish/Stockfish/blob/sf_15.1/src/uci.h#L38 + NormalizeToPawnValue = 361 + # https://github.com/official-stockfish/Stockfish/blob/sf_15.1/src/uci.cpp#L200-L224 + m = min(240, max(ply, 0)) / 64 + a = (((-0.58270499 * m + 2.68512549) * m + 15.24638015) * m) + 344.49745382 + b = (((-2.65734562 * m + 15.96509799) * m + -20.69040836) * m) + 73.61029937 + x = min(4000, max(cp * NormalizeToPawnValue / 100, -4000)) + return int(0.5 + 1000 / (1 + math.exp((a - x) / b))) + +def _sf15_wins(cp: int, *, ply: int) -> int: + # https://github.com/official-stockfish/Stockfish/blob/sf_15/src/uci.cpp#L200-L220 + m = min(240, max(ply, 0)) / 64 + a = (((-1.17202460e-1 * m + 5.94729104e-1) * m + 1.12065546e+1) * m) + 1.22606222e+2 + b = (((-1.79066759 * m + 11.30759193) * m + -17.43677612) * m) + 36.47147479 + x = min(2000, max(cp, -2000)) + return int(0.5 + 1000 / (1 + math.exp((a - x) / b))) + +def _sf14_wins(cp: int, *, ply: int) -> int: + # https://github.com/official-stockfish/Stockfish/blob/sf_14/src/uci.cpp#L200-L220 + m = min(240, max(ply, 0)) / 64 + a = (((-3.68389304 * m + 30.07065921) * m + -60.52878723) * m) + 149.53378557 + b = (((-2.01818570 * m + 15.85685038) * m + -29.83452023) * m) + 47.59078827 + x = min(2000, max(cp, -2000)) + return int(0.5 + 1000 / (1 + math.exp((a - x) / b))) + +def _sf12_wins(cp: int, *, ply: int) -> int: + # https://github.com/official-stockfish/Stockfish/blob/sf_12/src/uci.cpp#L198-L218 + m = min(240, max(ply, 0)) / 64 + a = (((-8.24404295 * m + 64.23892342) * m + -95.73056462) * m) + 153.86478679 + b = (((-3.37154371 * m + 28.44489198) * m + -56.67657741) * m) + 72.05858751 + x = min(1000, max(cp, -1000)) + return int(0.5 + 1000 / (1 + math.exp((a - x) / b))) + +def _lichess_raw_wins(cp: int) -> int: + # https://github.com/lichess-org/lila/pull/11148 + # https://github.com/lichess-org/lila/blob/2242b0a08faa06e7be5508d338ede7bb09049777/modules/analyse/src/main/WinPercent.scala#L26-L30 + return round(1000 / (1 + math.exp(-0.00368208 * cp))) + + +class Cp(Score): + """Centi-pawn score.""" + + def __init__(self, cp: int) -> None: + self.cp = cp + + def mate(self) -> None: return None - def __repr__(self): - return "".format(hex(id(self))) + def score(self, *, mate_score: Optional[int] = None) -> int: + return self.cp + + def wdl(self, *, model: WdlModel = "sf", ply: int = 30) -> Wdl: + if model == "lichess": + wins = _lichess_raw_wins(max(-1000, min(self.cp, 1000))) + losses = 1000 - wins + elif model == "sf12": + wins = _sf12_wins(self.cp, ply=ply) + losses = _sf12_wins(-self.cp, ply=ply) + elif model == "sf14": + wins = _sf14_wins(self.cp, ply=ply) + losses = _sf14_wins(-self.cp, ply=ply) + elif model == "sf15": + wins = _sf15_wins(self.cp, ply=ply) + losses = _sf15_wins(-self.cp, ply=ply) + elif model == "sf15.1": + wins = _sf15_1_wins(self.cp, ply=ply) + losses = _sf15_1_wins(-self.cp, ply=ply) + elif model == "sf16": + wins = _sf16_wins(self.cp, ply=ply) + losses = _sf16_wins(-self.cp, ply=ply) + else: + wins = _sf16_1_wins(self.cp, ply=ply) + losses = _sf16_1_wins(-self.cp, ply=ply) + draws = 1000 - wins - losses + return Wdl(wins, draws, losses) + + def __str__(self) -> str: + return f"+{self.cp:d}" if self.cp > 0 else str(self.cp) + + def __repr__(self) -> str: + return f"Cp({self})" + + def __neg__(self) -> Cp: + return Cp(-self.cp) + + def __pos__(self) -> Cp: + return Cp(self.cp) + + def __abs__(self) -> Cp: + return Cp(abs(self.cp)) + + +class Mate(Score): + """Mate score.""" + + def __init__(self, moves: int) -> None: + self.moves = moves + + def mate(self) -> int: + return self.moves + + @typing.overload + def score(self, *, mate_score: int) -> int: ... + @typing.overload + def score(self, *, mate_score: Optional[int] = None) -> Optional[int]: ... + def score(self, *, mate_score: Optional[int] = None) -> Optional[int]: + if mate_score is None: + return None + elif self.moves > 0: + return mate_score - self.moves + else: + return -mate_score - self.moves + + def wdl(self, *, model: WdlModel = "sf", ply: int = 30) -> Wdl: + if model == "lichess": + cp = (21 - min(10, abs(self.moves))) * 100 + wins = _lichess_raw_wins(cp) + return Wdl(wins, 0, 1000 - wins) if self.moves > 0 else Wdl(1000 - wins, 0, wins) + else: + return Wdl(1000, 0, 0) if self.moves > 0 else Wdl(0, 0, 1000) + + def __str__(self) -> str: + return f"#+{self.moves}" if self.moves > 0 else f"#-{abs(self.moves)}" + + def __repr__(self) -> str: + return "Mate({})".format(str(self).lstrip("#")) + + def __neg__(self) -> Union[MateGivenType, Mate]: + return MateGiven if not self.moves else Mate(-self.moves) + + def __pos__(self) -> Mate: + return Mate(self.moves) + + def __abs__(self) -> Union[MateGivenType, Mate]: + return MateGiven if not self.moves else Mate(abs(self.moves)) + + +class MateGivenType(Score): + """Winning mate score, equivalent to ``-Mate(0)``.""" + + def mate(self) -> int: + return 0 + + @typing.overload + def score(self, *, mate_score: int) -> int: ... + @typing.overload + def score(self, *, mate_score: Optional[int] = None) -> Optional[int]: ... + def score(self, *, mate_score: Optional[int] = None) -> Optional[int]: + return mate_score + + def wdl(self, *, model: WdlModel = "sf", ply: int = 30) -> Wdl: + return Wdl(1000, 0, 0) + + def __neg__(self) -> Mate: + return Mate(0) + + def __pos__(self) -> MateGivenType: + return self + + def __abs__(self) -> MateGivenType: + return self + + def __repr__(self) -> str: + return "MateGiven" + + def __str__(self) -> str: + return "#+0" + +MateGiven = MateGivenType() + + +@dataclasses.dataclass +class PovWdl: + """ + Relative :class:`win/draw/loss statistics ` and the point + of view. + """ + + relative: Wdl + """The relative :class:`~chess.engine.Wdl`.""" + turn: Color + """The point of view (``chess.WHITE`` or ``chess.BLACK``).""" -class PopenProcess(object): - def __init__(self, engine, command, **kwargs): - self.engine = engine + def white(self) -> Wdl: + """Gets the :class:`~chess.engine.Wdl` from White's point of view.""" + return self.pov(chess.WHITE) - self._receiving_thread = threading.Thread(target=self._receiving_thread_target) - self._receiving_thread.daemon = True - self._stdin_lock = threading.Lock() + def black(self) -> Wdl: + """Gets the :class:`~chess.engine.Wdl` from Black's point of view.""" + return self.pov(chess.BLACK) - self.engine.on_process_spawned(self) + def pov(self, color: Color) -> Wdl: + """ + Gets the :class:`~chess.engine.Wdl` from the point of view of the given + *color*. + """ + return self.relative if self.turn == color else -self.relative - popen_args = { - "stdout": subprocess.PIPE, - "stdin": subprocess.PIPE, - "bufsize": 1, # Line buffering - "universal_newlines": True, + def __bool__(self) -> bool: + return bool(self.relative) + + def __repr__(self) -> str: + return "PovWdl({!r}, {})".format(self.relative, "WHITE" if self.turn else "BLACK") + + +@dataclasses.dataclass +class Wdl: + """Win/draw/loss statistics.""" + + wins: int + """The number of wins.""" + + draws: int + """The number of draws.""" + + losses: int + """The number of losses.""" + + def total(self) -> int: + """ + Returns the total number of games. Usually, ``wdl`` reported by engines + is scaled to 1000 games. + """ + return self.wins + self.draws + self.losses + + def winning_chance(self) -> float: + """Returns the relative frequency of wins.""" + return self.wins / self.total() + + def drawing_chance(self) -> float: + """Returns the relative frequency of draws.""" + return self.draws / self.total() + + def losing_chance(self) -> float: + """Returns the relative frequency of losses.""" + return self.losses / self.total() + + def expectation(self) -> float: + """ + Returns the expectation value, where a win is valued 1, a draw is + valued 0.5, and a loss is valued 0. + """ + return (self.wins + 0.5 * self.draws) / self.total() + + def __bool__(self) -> bool: + return bool(self.total()) + + def __pos__(self) -> Wdl: + return self + + def __neg__(self) -> Wdl: + return Wdl(self.losses, self.draws, self.wins) + + +class MockTransport(asyncio.SubprocessTransport, asyncio.WriteTransport): + def __init__(self, protocol: Protocol) -> None: + super().__init__() + self.protocol = protocol + self.expectations: Deque[Tuple[str, List[str]]] = collections.deque() + self.expected_pings = 0 + self.stdin_buffer = bytearray() + self.protocol.connection_made(self) + + def expect(self, expectation: str, responses: List[str] = []) -> None: + self.expectations.append((expectation, responses)) + + def expect_ping(self) -> None: + self.expected_pings += 1 + + def assert_done(self) -> None: + assert not self.expectations, f"pending expectations: {self.expectations}" + + def get_pipe_transport(self, fd: int) -> Optional[asyncio.BaseTransport]: + assert fd == 0, f"expected 0 for stdin, got {fd}" + return self + + def write(self, data: bytes | bytearray | memoryview) -> None: + self.stdin_buffer.extend(data) + while b"\n" in self.stdin_buffer: + line_bytes, self.stdin_buffer = self.stdin_buffer.split(b"\n", 1) + line = line_bytes.decode("utf-8") + + if line.startswith("ping ") and self.expected_pings: + self.expected_pings -= 1 + self.protocol.pipe_data_received(1, (line.replace("ping ", "pong ") + "\n").encode("utf-8")) + else: + assert self.expectations, f"unexpected: {line!r}" + expectation, responses = self.expectations.popleft() + assert expectation == line, f"expected {expectation}, got: {line}" + if responses: + self.protocol.loop.call_soon(self.protocol.pipe_data_received, 1, "\n".join(responses + [""]).encode("utf-8")) + + def get_pid(self) -> int: + return id(self) + + def get_returncode(self) -> Optional[int]: + return None if self.expectations else 0 + + +class Protocol(asyncio.SubprocessProtocol, metaclass=abc.ABCMeta): + """Protocol for communicating with a chess engine process.""" + + id: Dict[str, str] + """ + Dictionary of information about the engine. Common keys are ``name`` + and ``author``. + """ + + returncode: asyncio.Future[int] + """Future: Exit code of the process.""" + + def __init__(self) -> None: + self.loop = asyncio.get_running_loop() + self.transport: Optional[asyncio.SubprocessTransport] = None + + self.buffer = { + 1: bytearray(), # stdout + 2: bytearray(), # stderr } - popen_args.update(kwargs) - self.process = subprocess.Popen(command, **popen_args) - self._receiving_thread.start() + self.command: Optional[BaseCommand[Any]] = None + self.next_command: Optional[BaseCommand[Any]] = None + + self.initialized = False + self.returncode: asyncio.Future[int] = asyncio.Future() + + def connection_made(self, transport: asyncio.BaseTransport) -> None: + # SubprocessTransport expected, but not checked to allow duck typing. + self.transport = transport # type: ignore + LOGGER.debug("%s: Connection made", self) + + def connection_lost(self, exc: Optional[Exception]) -> None: + assert self.transport is not None + code = self.transport.get_returncode() + assert code is not None, "connect lost, but got no returncode" + LOGGER.debug("%s: Connection lost (exit code: %d, error: %s)", self, code, exc) + + # Terminate commands. + command, self.command = self.command, None + next_command, self.next_command = self.next_command, None + if command: + command._engine_terminated(code) + if next_command: + next_command._engine_terminated(code) + + self.returncode.set_result(code) + + def process_exited(self) -> None: + LOGGER.debug("%s: Process exited", self) + + def send_line(self, line: str) -> None: + LOGGER.debug("%s: << %s", self, line) + assert self.transport is not None, "cannot send line before connection is made" + stdin = self.transport.get_pipe_transport(0) + # WriteTransport expected, but not checked to allow duck typing. + stdin.write((line + "\n").encode("utf-8")) # type: ignore + + def pipe_data_received(self, fd: int, data: Union[bytes, str]) -> None: + self.buffer[fd].extend(data) # type: ignore + while b"\n" in self.buffer[fd]: + line_bytes, self.buffer[fd] = self.buffer[fd].split(b"\n", 1) + if line_bytes.endswith(b"\r"): + line_bytes = line_bytes[:-1] + try: + line = line_bytes.decode("utf-8") + except UnicodeDecodeError as err: + LOGGER.warning("%s: >> %r (%s)", self, bytes(line_bytes), err) + else: + if fd == 1: + self._line_received(line) + else: + self.error_line_received(line) + + def error_line_received(self, line: str) -> None: + LOGGER.warning("%s: stderr >> %s", self, line) + + def _line_received(self, line: str) -> None: + LOGGER.debug("%s: >> %s", self, line) + + self.line_received(line) + + if self.command: + self.command._line_received(line) + + def line_received(self, line: str) -> None: + pass + + async def communicate(self, command_factory: Callable[[Self], BaseCommand[T]]) -> T: + command = command_factory(self) + + if self.returncode.done(): + raise EngineTerminatedError(f"engine process dead (exit code: {self.returncode.result()})") + + assert command.state == CommandState.NEW, command.state + + if self.next_command is not None: + self.next_command.result.cancel() + self.next_command.finished.cancel() + self.next_command.set_finished() + + self.next_command = command + + def previous_command_finished() -> None: + self.command, self.next_command = self.next_command, None + if self.command is not None: + cmd = self.command + + def cancel_if_cancelled(result: asyncio.Future[T]) -> None: + if result.cancelled(): + cmd._cancel() + + cmd.result.add_done_callback(cancel_if_cancelled) + cmd._start() + cmd.add_finished_callback(previous_command_finished) + + if self.command is None: + previous_command_finished() + elif not self.command.result.done(): + self.command.result.cancel() + elif not self.command.result.cancelled(): + self.command._cancel() + + return await command.result + + def __repr__(self) -> str: + pid = self.transport.get_pid() if self.transport is not None else "?" + return f"<{type(self).__name__} (pid={pid})>" + + @property + @abc.abstractmethod + def options(self) -> MutableMapping[str, Option]: + """Dictionary of available options.""" + + @abc.abstractmethod + async def initialize(self) -> None: + """Initializes the engine.""" + + @abc.abstractmethod + async def ping(self) -> None: + """ + Pings the engine and waits for a response. Used to ensure the engine + is still alive and idle. + """ + + @abc.abstractmethod + async def configure(self, options: ConfigMapping) -> None: + """ + Configures global engine options. + + :param options: A dictionary of engine options where the keys are + names of :data:`~chess.engine.Protocol.options`. Do not set options + that are managed automatically + (:func:`chess.engine.Option.is_managed()`). + """ + + @abc.abstractmethod + async def send_opponent_information(self, *, opponent: Optional[Opponent] = None, engine_rating: Optional[int] = None) -> None: + """ + Sends the engine information about its opponent. The information will + be sent after a new game is announced and before the first move. This + method should be called before the first move of a game--i.e., the + first call to :func:`chess.engine.Protocol.play()`. + + :param opponent: Optional. An instance of :class:`chess.engine.Opponent` that has the opponent's information. + :param engine_rating: Optional. This engine's own rating. Only used by XBoard engines. + """ + + @abc.abstractmethod + async def play(self, board: chess.Board, limit: Limit, *, game: object = None, info: Info = INFO_NONE, ponder: bool = False, draw_offered: bool = False, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}, opponent: Optional[Opponent] = None) -> PlayResult: + """ + Plays a position. + + :param board: The position. The entire move stack will be sent to the + engine. + :param limit: An instance of :class:`chess.engine.Limit` that + determines when to stop thinking. + :param game: Optional. An arbitrary object that identifies the game. + Will automatically inform the engine if the object is not equal + to the previous game (e.g., ``ucinewgame``, ``new``). + :param info: Selects which additional information to retrieve from the + engine. ``INFO_NONE``, ``INFO_BASIC`` (basic information that is + trivial to obtain), ``INFO_SCORE``, ``INFO_PV``, + ``INFO_REFUTATION``, ``INFO_CURRLINE``, ``INFO_ALL`` or any + bitwise combination. Some overhead is associated with parsing + extra information. + :param ponder: Whether the engine should keep analysing in the + background even after the result has been returned. + :param draw_offered: Whether the engine's opponent has offered a draw. + Ignored by UCI engines. + :param root_moves: Optional. Consider only root moves from this list. + :param options: Optional. A dictionary of engine options for the + analysis. The previous configuration will be restored after the + analysis is complete. You can permanently apply a configuration + with :func:`~chess.engine.Protocol.configure()`. + :param opponent: Optional. Information about a new opponent. Information + about the original opponent will be restored once the move is + complete. New opponent information can be made permanent with + :func:`~chess.engine.Protocol.send_opponent_information()`. + """ + + @typing.overload + async def analyse(self, board: chess.Board, limit: Limit, *, game: object = None, info: Info = INFO_ALL, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> InfoDict: ... + @typing.overload + async def analyse(self, board: chess.Board, limit: Limit, *, multipv: int, game: object = None, info: Info = INFO_ALL, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> List[InfoDict]: ... + @typing.overload + async def analyse(self, board: chess.Board, limit: Limit, *, multipv: Optional[int] = None, game: object = None, info: Info = INFO_ALL, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> Union[List[InfoDict], InfoDict]: ... + async def analyse(self, board: chess.Board, limit: Limit, *, multipv: Optional[int] = None, game: object = None, info: Info = INFO_ALL, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> Union[List[InfoDict], InfoDict]: + """ + Analyses a position and returns a dictionary of + :class:`information `. + + :param board: The position to analyse. The entire move stack will be + sent to the engine. + :param limit: An instance of :class:`chess.engine.Limit` that + determines when to stop the analysis. + :param multipv: Optional. Analyse multiple root moves. Will return + a list of at most *multipv* dictionaries rather than just a single + info dictionary. + :param game: Optional. An arbitrary object that identifies the game. + Will automatically inform the engine if the object is not equal + to the previous game (e.g., ``ucinewgame``, ``new``). + :param info: Selects which information to retrieve from the + engine. ``INFO_NONE``, ``INFO_BASIC`` (basic information that is + trivial to obtain), ``INFO_SCORE``, ``INFO_PV``, + ``INFO_REFUTATION``, ``INFO_CURRLINE``, ``INFO_ALL`` or any + bitwise combination. Some overhead is associated with parsing + extra information. + :param root_moves: Optional. Limit analysis to a list of root moves. + :param options: Optional. A dictionary of engine options for the + analysis. The previous configuration will be restored after the + analysis is complete. You can permanently apply a configuration + with :func:`~chess.engine.Protocol.configure()`. + """ + analysis = await self.analysis(board, limit, multipv=multipv, game=game, info=info, root_moves=root_moves, options=options) + + with analysis: + await analysis.wait() + + return analysis.info if multipv is None else analysis.multipv + + @abc.abstractmethod + async def analysis(self, board: chess.Board, limit: Optional[Limit] = None, *, multipv: Optional[int] = None, game: object = None, info: Info = INFO_ALL, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> AnalysisResult: + """ + Starts analysing a position. + + :param board: The position to analyse. The entire move stack will be + sent to the engine. + :param limit: Optional. An instance of :class:`chess.engine.Limit` + that determines when to stop the analysis. Analysis is infinite + by default. + :param multipv: Optional. Analyse multiple root moves. + :param game: Optional. An arbitrary object that identifies the game. + Will automatically inform the engine if the object is not equal + to the previous game (e.g., ``ucinewgame``, ``new``). + :param info: Selects which information to retrieve from the + engine. ``INFO_NONE``, ``INFO_BASIC`` (basic information that is + trivial to obtain), ``INFO_SCORE``, ``INFO_PV``, + ``INFO_REFUTATION``, ``INFO_CURRLINE``, ``INFO_ALL`` or any + bitwise combination. Some overhead is associated with parsing + extra information. + :param root_moves: Optional. Limit analysis to a list of root moves. + :param options: Optional. A dictionary of engine options for the + analysis. The previous configuration will be restored after the + analysis is complete. You can permanently apply a configuration + with :func:`~chess.engine.Protocol.configure()`. + + Returns :class:`~chess.engine.AnalysisResult`, a handle that allows + asynchronously iterating over the information sent by the engine + and stopping the analysis at any time. + """ + + @abc.abstractmethod + async def send_game_result(self, board: chess.Board, winner: Optional[Color] = None, game_ending: Optional[str] = None, game_complete: bool = True) -> None: + """ + Sends the engine the result of the game. + + XBoard engines receive the final moves and a line of the form + ``result {}``. The ```` field is one of ``1-0``, + ``0-1``, ``1/2-1/2``, or ``*`` to indicate white won, black won, draw, + or adjournment, respectively. The ```` field is a description + of the specific reason for the end of the game: "White mates", + "Time forfeiture", "Stalemate", etc. + + UCI engines do not expect end-of-game information and so are not + sent anything. + + :param board: The final state of the board. + :param winner: Optional. Specify the winner of the game. This is useful + if the result of the game is not evident from the board--e.g., time + forfeiture or draw by agreement. If not ``None``, this parameter + overrides any winner derivable from the board. + :param game_ending: Optional. Text describing the reason for the game + ending. Similarly to the winner parameter, this overrides any game + result derivable from the board. + :param game_complete: Optional. Whether the game reached completion. + """ + + @abc.abstractmethod + async def quit(self) -> None: + """Asks the engine to shut down.""" + + @classmethod + async def popen(cls: Type[ProtocolT], command: Union[str, List[str]], *, setpgrp: bool = False, **popen_args: Any) -> Tuple[asyncio.SubprocessTransport, ProtocolT]: + if not isinstance(command, list): + command = [command] + + if setpgrp: + try: + # Windows. + popen_args["creationflags"] = popen_args.get("creationflags", 0) | subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore + except AttributeError: + # Unix. + if sys.version_info >= (3, 11): + popen_args["process_group"] = 0 + else: + # Before Python 3.11 + popen_args["start_new_session"] = True + + return await asyncio.get_running_loop().subprocess_exec(cls, *command, **popen_args) + + +class CommandState(enum.Enum): + NEW = enum.auto() + ACTIVE = enum.auto() + CANCELLING = enum.auto() + DONE = enum.auto() + + +class BaseCommand(Generic[T]): + def __init__(self, engine: Protocol) -> None: + self._engine = engine + + self.state = CommandState.NEW + + self.result: asyncio.Future[T] = asyncio.Future() + self.finished: asyncio.Future[None] = asyncio.Future() + + self._finished_callbacks: List[Callable[[], None]] = [] + + def add_finished_callback(self, callback: Callable[[], None]) -> None: + self._finished_callbacks.append(callback) + self._dispatch_finished() + + def _dispatch_finished(self) -> None: + if self.finished.done(): + while self._finished_callbacks: + self._finished_callbacks.pop()() + + def _engine_terminated(self, code: int) -> None: + hint = ", binary not compatible with cpu?" if code in [-4, 0xc000001d] else "" + exc = EngineTerminatedError(f"engine process died unexpectedly (exit code: {code}{hint})") + if self.state == CommandState.ACTIVE: + self.engine_terminated(exc) + elif self.state == CommandState.CANCELLING: + self.finished.set_result(None) + self._dispatch_finished() + elif self.state == CommandState.NEW: + self._handle_exception(exc) + + def _handle_exception(self, exc: Exception) -> None: + if not self.result.done(): + self.result.set_exception(exc) + else: + self._engine.loop.call_exception_handler({ # XXX + "message": f"{type(self).__name__} failed after returning preliminary result ({self.result!r})", + "exception": exc, + "protocol": self._engine, + "transport": self._engine.transport, + }) + + if not self.finished.done(): + self.finished.set_result(None) + self._dispatch_finished() + + def set_finished(self) -> None: + assert self.state in [CommandState.ACTIVE, CommandState.CANCELLING], self.state + if not self.result.done(): + self.result.set_exception(EngineError(f"engine command finished before returning result: {self!r}")) + self.state = CommandState.DONE + self.finished.set_result(None) + self._dispatch_finished() + + def _cancel(self) -> None: + if self.state != CommandState.CANCELLING and self.state != CommandState.DONE: + assert self.state == CommandState.ACTIVE, self.state + self.state = CommandState.CANCELLING + self.cancel() + + def _start(self) -> None: + assert self.state == CommandState.NEW, self.state + self.state = CommandState.ACTIVE + try: + self.check_initialized() + self.start() + except EngineError as err: + self._handle_exception(err) + + def _line_received(self, line: str) -> None: + assert self.state in [CommandState.ACTIVE, CommandState.CANCELLING], self.state + try: + self.line_received(line) + except EngineError as err: + self._handle_exception(err) + + def cancel(self) -> None: + pass + + def check_initialized(self) -> None: + if not self._engine.initialized: + raise EngineError("tried to run command, but engine is not initialized") + + def start(self) -> None: + raise NotImplementedError + + def line_received(self, line: str) -> None: + pass + + def engine_terminated(self, exc: Exception) -> None: + self._handle_exception(exc) + + def __repr__(self) -> str: + return "<{} at {:#x} (state={}, result={}, finished={}>".format(type(self).__name__, id(self), self.state, self.result, self.finished) + + +class UciProtocol(Protocol): + """ + An implementation of the + `Universal Chess Interface `_ + protocol. + """ + + def __init__(self) -> None: + super().__init__() + self._options: UciOptionMap[Option] = UciOptionMap() + self.config: UciOptionMap[ConfigValue] = UciOptionMap() + self.target_config: UciOptionMap[ConfigValue] = UciOptionMap() + self.id = {} + self.board = chess.Board() + self.game: object = None + self.first_game = True + self.may_ponderhit: Optional[chess.Board] = None + self.ponderhit = False + + @property + @override + def options(self) -> UciOptionMap[Option]: + return self._options + + async def initialize(self) -> None: + class UciInitializeCommand(BaseCommand[None]): + def __init__(self, engine: UciProtocol): + super().__init__(engine) + self.engine = engine + + @override + def check_initialized(self) -> None: + if self.engine.initialized: + raise EngineError("engine already initialized") + + @override + def start(self) -> None: + self.engine.send_line("uci") + + @override + def line_received(self, line: str) -> None: + token, remaining = _next_token(line) + if line.strip() == "uciok" and not self.result.done(): + self.engine.initialized = True + self.result.set_result(None) + self.set_finished() + elif token == "option": + self._option(remaining) + elif token == "id": + self._id(remaining) + + def _option(self, arg: str) -> None: + current_parameter = None + option_parts: dict[str, str] = {k: "" for k in ["name", "type", "default", "min", "max"]} + var = [] + + parameters = list(option_parts.keys()) + ['var'] + inner_regex = '|'.join([fr"\b{parameter}\b" for parameter in parameters]) + option_regex = fr"\s*({inner_regex})\s*" + for token in re.split(option_regex, arg.strip()): + if token == "var" or (token in option_parts and not option_parts[token]): + current_parameter = token + elif current_parameter == "var": + var.append(token) + elif current_parameter: + option_parts[current_parameter] = token + + def parse_min_max_value(option_parts: dict[str, str], which: Literal["min", "max"]) -> Optional[int]: + try: + number = option_parts[which] + return int(number) if number else None + except ValueError: + LOGGER.exception(f"Exception parsing option {which}") + return None + + name = option_parts["name"] + type = option_parts["type"] + default = option_parts["default"] + min = parse_min_max_value(option_parts, "min") + max = parse_min_max_value(option_parts, "max") + + without_default = Option(name, type, None, min, max, var) + option = Option(without_default.name, without_default.type, without_default.parse(default), min, max, var) + self.engine.options[option.name] = option + + if option.default is not None: + self.engine.config[option.name] = option.default + if option.default is not None and not option.is_managed() and option.name.lower() != "uci_analysemode": + self.engine.target_config[option.name] = option.default + + def _id(self, arg: str) -> None: + key, value = _next_token(arg) + self.engine.id[key] = value.strip() + + return await self.communicate(UciInitializeCommand) + + def _isready(self) -> None: + self.send_line("isready") + + def _opponent_info(self) -> None: + opponent_info = self.config.get("UCI_Opponent") or self.target_config.get("UCI_Opponent") + if opponent_info: + self.send_line(f"setoption name UCI_Opponent value {opponent_info}") + + def _ucinewgame(self) -> None: + self.send_line("ucinewgame") + self._opponent_info() + self.first_game = False + self.ponderhit = False + + def debug(self, on: bool = True) -> None: + """ + Switches debug mode of the engine on or off. This does not interrupt + other ongoing operations. + """ + if on: + self.send_line("debug on") + else: + self.send_line("debug off") + + async def ping(self) -> None: + class UciPingCommand(BaseCommand[None]): + def __init__(self, engine: UciProtocol) -> None: + super().__init__(engine) + self.engine = engine + + def start(self) -> None: + self.engine._isready() + + @override + def line_received(self, line: str) -> None: + if line.strip() == "readyok": + self.result.set_result(None) + self.set_finished() + else: + LOGGER.warning("%s: Unexpected engine output: %r", self.engine, line) + + return await self.communicate(UciPingCommand) + + def _changed_options(self, options: ConfigMapping) -> bool: + return any(value is None or value != self.config.get(name) for name, value in _chain_config(options, self.target_config)) + + def _setoption(self, name: str, value: ConfigValue) -> None: + try: + value = self.options[name].parse(value) + except KeyError: + raise EngineError("engine does not support option {} (available options: {})".format(name, ", ".join(self.options))) + + if value is None or value != self.config.get(name): + builder = ["setoption name", name] + if value is False: + builder.append("value false") + elif value is True: + builder.append("value true") + elif value is not None: + builder.append("value") + builder.append(str(value)) + + if name != "UCI_Opponent": # sent after ucinewgame + self.send_line(" ".join(builder)) + self.config[name] = value + + def _configure(self, options: ConfigMapping) -> None: + for name, value in _chain_config(options, self.target_config): + if name.lower() in MANAGED_OPTIONS: + raise EngineError("cannot set {} which is automatically managed".format(name)) + self._setoption(name, value) + + async def configure(self, options: ConfigMapping) -> None: + class UciConfigureCommand(BaseCommand[None]): + def __init__(self, engine: UciProtocol): + super().__init__(engine) + self.engine = engine + + def start(self) -> None: + self.engine._configure(options) + self.engine.target_config.update({name: value for name, value in options.items() if value is not None}) + self.result.set_result(None) + self.set_finished() + + return await self.communicate(UciConfigureCommand) + + def _opponent_configuration(self, *, opponent: Optional[Opponent] = None) -> ConfigMapping: + if opponent and opponent.name and "UCI_Opponent" in self.options: + rating = opponent.rating or "none" + title = opponent.title or "none" + player_type = "computer" if opponent.is_engine else "human" + return {"UCI_Opponent": f"{title} {rating} {player_type} {opponent.name}"} + else: + return {} + + async def send_opponent_information(self, *, opponent: Optional[Opponent] = None, engine_rating: Optional[int] = None) -> None: + return await self.configure(self._opponent_configuration(opponent=opponent)) + + def _position(self, board: chess.Board) -> None: + # Select UCI_Variant and UCI_Chess960. + uci_variant = type(board).uci_variant + if "UCI_Variant" in self.options: + self._setoption("UCI_Variant", uci_variant) + elif uci_variant != "chess": + raise EngineError("engine does not support UCI_Variant") + + if "UCI_Chess960" in self.options: + self._setoption("UCI_Chess960", board.chess960) + elif board.chess960: + raise EngineError("engine does not support UCI_Chess960") + + # Send starting position. + builder = ["position"] + safe_history = all(board.move_stack) + root = board.root() if safe_history else board + fen = root.fen(shredder=board.chess960, en_passant="fen") + if uci_variant == "chess" and fen == chess.STARTING_FEN: + builder.append("startpos") + else: + builder.append("fen") + builder.append(fen) + + # Send moves. + if not safe_history: + LOGGER.warning("Not transmitting history with null moves to UCI engine") + elif board.move_stack: + builder.append("moves") + builder.extend(move.uci() for move in board.move_stack) + + self.send_line(" ".join(builder)) + self.board = board.copy(stack=False) + + def _go(self, limit: Limit, *, root_moves: Optional[Iterable[chess.Move]] = None, ponder: bool = False, infinite: bool = False) -> None: + builder = ["go"] + if ponder: + builder.append("ponder") + if limit.white_clock is not None: + builder.append("wtime") + builder.append(str(max(1, round(limit.white_clock * 1000)))) + if limit.black_clock is not None: + builder.append("btime") + builder.append(str(max(1, round(limit.black_clock * 1000)))) + if limit.white_inc is not None: + builder.append("winc") + builder.append(str(round(limit.white_inc * 1000))) + if limit.black_inc is not None: + builder.append("binc") + builder.append(str(round(limit.black_inc * 1000))) + if limit.remaining_moves is not None and int(limit.remaining_moves) > 0: + builder.append("movestogo") + builder.append(str(int(limit.remaining_moves))) + if limit.depth is not None: + builder.append("depth") + builder.append(str(max(1, int(limit.depth)))) + if limit.nodes is not None: + builder.append("nodes") + builder.append(str(max(1, int(limit.nodes)))) + if limit.mate is not None: + builder.append("mate") + builder.append(str(max(1, int(limit.mate)))) + if limit.time is not None: + builder.append("movetime") + builder.append(str(max(1, round(limit.time * 1000)))) + if infinite: + builder.append("infinite") + if root_moves is not None: + builder.append("searchmoves") + if root_moves: + builder.extend(move.uci() for move in root_moves) + else: + # Work around searchmoves followed by nothing. + builder.append("0000") + self.send_line(" ".join(builder)) + + async def play(self, board: chess.Board, limit: Limit, *, game: object = None, info: Info = INFO_NONE, ponder: bool = False, draw_offered: bool = False, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}, opponent: Optional[Opponent] = None) -> PlayResult: + new_options: Dict[str, ConfigValue] = {} + for name, value in options.items(): + new_options[name] = value + new_options.update(self._opponent_configuration(opponent=opponent)) + + engine = self + + class UciPlayCommand(BaseCommand[PlayResult]): + def __init__(self, engine: UciProtocol): + super().__init__(engine) + self.engine = engine + + # May ponderhit only in the same game and with unchanged target + # options. The managed options UCI_AnalyseMode, Ponder, and + # MultiPV never change between pondering play commands. + engine.may_ponderhit = board if ponder and not engine.first_game and game == engine.game and not engine._changed_options(new_options) else None + + @override + def start(self) -> None: + self.info: InfoDict = {} + self.pondering: Optional[chess.Board] = None + self.sent_isready = False + self.start_time = time.perf_counter() + + if self.engine.ponderhit: + self.engine.ponderhit = False + self.engine.send_line("ponderhit") + return + + if "UCI_AnalyseMode" in self.engine.options and "UCI_AnalyseMode" not in self.engine.target_config and all(name.lower() != "uci_analysemode" for name in new_options): + self.engine._setoption("UCI_AnalyseMode", False) + if "Ponder" in self.engine.options: + self.engine._setoption("Ponder", ponder) + if "MultiPV" in self.engine.options: + self.engine._setoption("MultiPV", self.engine.options["MultiPV"].default) + + new_opponent = new_options.get("UCI_Opponent") or self.engine.target_config.get("UCI_Opponent") + opponent_changed = new_opponent != self.engine.config.get("UCI_Opponent") + self.engine._configure(new_options) + + if self.engine.first_game or self.engine.game != game or opponent_changed: + self.engine.game = game + self.engine._ucinewgame() + self.sent_isready = True + self.engine._isready() + else: + self._readyok() + + @override + def line_received(self, line: str) -> None: + token, remaining = _next_token(line) + if token == "info": + self._info(remaining) + elif token == "bestmove": + self._bestmove(remaining) + elif line.strip() == "readyok" and self.sent_isready: + self._readyok() + else: + LOGGER.warning("%s: Unexpected engine output: %r", self.engine, line) + + def _readyok(self) -> None: + self.sent_isready = False + engine._position(board) + engine._go(limit, root_moves=root_moves) + + def _info(self, arg: str) -> None: + if not self.pondering: + self.info.update(_parse_uci_info(arg, self.engine.board, info)) + + def _bestmove(self, arg: str) -> None: + if self.pondering: + self.pondering = None + elif not self.result.cancelled(): + best = _parse_uci_bestmove(self.engine.board, arg) + self.result.set_result(PlayResult(best.move, best.ponder, self.info)) + + if ponder and best.move and best.ponder: + self.pondering = board.copy() + self.pondering.push(best.move) + self.pondering.push(best.ponder) + self.engine._position(self.pondering) + + # Adjust clocks for pondering. + time_used = time.perf_counter() - self.start_time + ponder_limit = copy.copy(limit) + if ponder_limit.white_clock is not None: + ponder_limit.white_clock += (ponder_limit.white_inc or 0.0) + if self.pondering.turn == chess.WHITE: + ponder_limit.white_clock -= time_used + if ponder_limit.black_clock is not None: + ponder_limit.black_clock += (ponder_limit.black_inc or 0.0) + if self.pondering.turn == chess.BLACK: + ponder_limit.black_clock -= time_used + if ponder_limit.remaining_moves: + ponder_limit.remaining_moves -= 1 + + self.engine._go(ponder_limit, ponder=True) + + if not self.pondering: + self.end() + + def end(self) -> None: + engine.may_ponderhit = None + self.set_finished() + + @override + def cancel(self) -> None: + if self.engine.may_ponderhit and self.pondering and self.engine.may_ponderhit.move_stack == self.pondering.move_stack and self.engine.may_ponderhit == self.pondering: + self.engine.ponderhit = True + self.end() + else: + self.engine.send_line("stop") + + @override + def engine_terminated(self, exc: Exception) -> None: + # Allow terminating engine while pondering. + if not self.result.done(): + super().engine_terminated(exc) + + return await self.communicate(UciPlayCommand) + + async def analysis(self, board: chess.Board, limit: Optional[Limit] = None, *, multipv: Optional[int] = None, game: object = None, info: Info = INFO_ALL, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> AnalysisResult: + class UciAnalysisCommand(BaseCommand[AnalysisResult]): + def __init__(self, engine: UciProtocol): + super().__init__(engine) + self.engine = engine + + def start(self) -> None: + self.analysis = AnalysisResult(stop=lambda: self.cancel()) + self.sent_isready = False + + if "Ponder" in self.engine.options: + self.engine._setoption("Ponder", False) + if "UCI_AnalyseMode" in self.engine.options and "UCI_AnalyseMode" not in self.engine.target_config and all(name.lower() != "uci_analysemode" for name in options): + self.engine._setoption("UCI_AnalyseMode", True) + if "MultiPV" in self.engine.options or (multipv and multipv > 1): + self.engine._setoption("MultiPV", 1 if multipv is None else multipv) + + self.engine._configure(options) + + if self.engine.first_game or self.engine.game != game: + self.engine.game = game + self.engine._ucinewgame() + self.sent_isready = True + self.engine._isready() + else: + self._readyok() + + @override + def line_received(self, line: str) -> None: + token, remaining = _next_token(line) + if token == "info": + self._info(remaining) + elif token == "bestmove": + self._bestmove(remaining) + elif line.strip() == "readyok" and self.sent_isready: + self._readyok() + else: + LOGGER.warning("%s: Unexpected engine output: %r", self.engine, line) + + def _readyok(self) -> None: + self.sent_isready = False + self.engine._position(board) + + if limit: + self.engine._go(limit, root_moves=root_moves) + else: + self.engine._go(Limit(), root_moves=root_moves, infinite=True) + + self.result.set_result(self.analysis) + + def _info(self, arg: str) -> None: + self.analysis.post(_parse_uci_info(arg, self.engine.board, info)) + + def _bestmove(self, arg: str) -> None: + if not self.result.done(): + raise EngineError("was not searching, but engine sent bestmove") + best = _parse_uci_bestmove(self.engine.board, arg) + self.set_finished() + self.analysis.set_finished(best) + + @override + def cancel(self) -> None: + self.engine.send_line("stop") + + @override + def engine_terminated(self, exc: Exception) -> None: + LOGGER.debug("%s: Closing analysis because engine has been terminated (error: %s)", self.engine, exc) + self.analysis.set_exception(exc) + + return await self.communicate(UciAnalysisCommand) + + async def send_game_result(self, board: chess.Board, winner: Optional[Color] = None, game_ending: Optional[str] = None, game_complete: bool = True) -> None: + pass + + async def quit(self) -> None: + self.send_line("quit") + await asyncio.shield(self.returncode) + + +UCI_REGEX = re.compile(r"^[a-h][1-8][a-h][1-8][pnbrqk]?|[PNBRQK]@[a-h][1-8]|0000\Z") + +def _create_variation_line(root_board: chess.Board, line: str) -> tuple[list[chess.Move], str]: + board = root_board.copy(stack=False) + currline: list[chess.Move] = [] + while True: + next_move, remaining_line_after_move = _next_token(line) + if UCI_REGEX.match(next_move): + currline.append(board.push_uci(next_move)) + line = remaining_line_after_move + else: + return currline, line + + +def _parse_uci_info(arg: str, root_board: chess.Board, selector: Info = INFO_ALL) -> InfoDict: + info: InfoDict = {} + if not selector: + return info + + remaining_line = arg + while remaining_line: + parameter, remaining_line = _next_token(remaining_line) + + if parameter == "string": + info["string"] = remaining_line + break + elif parameter in ["depth", "seldepth", "nodes", "multipv", "currmovenumber", + "hashfull", "nps", "tbhits", "cpuload", "movesleft"]: + try: + number, remaining_line = _next_token(remaining_line) + info[parameter] = int(number) # type: ignore + except (ValueError, IndexError): + LOGGER.error("Exception parsing %s from info: %r", parameter, arg) + elif parameter == "time": + try: + time_ms, remaining_line = _next_token(remaining_line) + info["time"] = int(time_ms) / 1000.0 + except (ValueError, IndexError): + LOGGER.error("Exception parsing %s from info: %r", parameter, arg) + elif parameter == "ebf": + try: + number, remaining_line = _next_token(remaining_line) + info["ebf"] = float(number) + except (ValueError, IndexError): + LOGGER.error("Exception parsing %s from info: %r", parameter, arg) + elif parameter == "score" and selector & INFO_SCORE: + try: + kind, remaining_line = _next_token(remaining_line) + value, remaining_line = _next_token(remaining_line) + token, remaining_after_token = _next_token(remaining_line) + if token in ["lowerbound", "upperbound"]: + info[token] = True # type: ignore + remaining_line = remaining_after_token + if kind == "cp": + info["score"] = PovScore(Cp(int(value)), root_board.turn) + elif kind == "mate": + info["score"] = PovScore(Mate(int(value)), root_board.turn) + else: + LOGGER.error("Unknown score kind %r in info (expected cp or mate): %r", kind, arg) + except (ValueError, IndexError): + LOGGER.error("Exception parsing score from info: %r", arg) + elif parameter == "currmove": + try: + current_move, remaining_line = _next_token(remaining_line) + info["currmove"] = chess.Move.from_uci(current_move) + except (ValueError, IndexError): + LOGGER.error("Exception parsing currmove from info: %r", arg) + elif parameter == "currline" and selector & INFO_CURRLINE: + try: + if "currline" not in info: + info["currline"] = {} + + cpunr_text, remaining_line = _next_token(remaining_line) + cpunr = int(cpunr_text) + currline, remaining_line = _create_variation_line(root_board, remaining_line) + info["currline"][cpunr] = currline + except (ValueError, IndexError): + LOGGER.error("Exception parsing currline from info: %r, position at root: %s", arg, root_board.fen()) + elif parameter == "refutation" and selector & INFO_REFUTATION: + try: + if "refutation" not in info: + info["refutation"] = {} + + board = root_board.copy(stack=False) + refuted_text, remaining_line = _next_token(remaining_line) + refuted = board.push_uci(refuted_text) + + refuted_by, remaining_line = _create_variation_line(board, remaining_line) + info["refutation"][refuted] = refuted_by + except (ValueError, IndexError): + LOGGER.error("Exception parsing refutation from info: %r, position at root: %s", arg, root_board.fen()) + elif parameter == "pv" and selector & INFO_PV: + try: + pv, remaining_line = _create_variation_line(root_board, remaining_line) + info["pv"] = pv + except (ValueError, IndexError): + LOGGER.error("Exception parsing pv from info: %r, position at root: %s", arg, root_board.fen()) + elif parameter == "wdl": + try: + wins, remaining_line = _next_token(remaining_line) + draws, remaining_line = _next_token(remaining_line) + losses, remaining_line = _next_token(remaining_line) + info["wdl"] = PovWdl(Wdl(int(wins), int(draws), int(losses)), root_board.turn) + except (ValueError, IndexError): + LOGGER.error("Exception parsing wdl from info: %r", arg) + + return info + +def _parse_uci_bestmove(board: chess.Board, args: str) -> BestMove: + tokens = args.split() + + move = None + ponder = None + + if tokens and tokens[0] not in ["(none)", "NULL"]: + try: + # AnMon 5.75 uses uppercase letters to denote promotion types. + move = board.push_uci(tokens[0].lower()) + except ValueError as err: + raise EngineError(err) + + try: + # Houdini 1.5 sends NULL instead of skipping the token. + if len(tokens) >= 3 and tokens[1] == "ponder" and tokens[2] not in ["(none)", "NULL"]: + ponder = board.parse_uci(tokens[2].lower()) + except ValueError: + LOGGER.exception("Engine sent invalid ponder move") + finally: + board.pop() + + return BestMove(move, ponder) - def _receiving_thread_target(self): - while True: - line = self.process.stdout.readline() - if not line: - # Stream closed. - break - self.engine.on_line_received(line.rstrip()) +def _chain_config(a: ConfigMapping, b: ConfigMapping) -> Iterable[Tuple[str, ConfigValue]]: + merged = dict(a) + for k, v in b.items(): + merged.setdefault(k, v) + if "Hash" in merged and "Threads" in merged: + # Move Hash after Threads, as recommended by Stockfish. + hash_val = merged["Hash"] + del merged["Hash"] + merged["Hash"] = hash_val + return merged.items() - # Close file descriptors. - self.process.stdout.close() - with self._stdin_lock: - self.process.stdin.close() - # Ensure the process is terminated (not just the in/out streams). - if self.is_alive(): - self.terminate() - self.wait_for_return_code() +class UciOptionMap(MutableMapping[str, T]): + """Dictionary with case-insensitive keys.""" - self.engine.on_terminated() + def __init__(self, data: Optional[Iterable[Tuple[str, T]]] = None, **kwargs: T) -> None: + self._store: Dict[str, Tuple[str, T]] = {} + if data is None: + data = {} + self.update(data, **kwargs) - def is_alive(self): - return self.process.poll() is None + def __setitem__(self, key: str, value: T) -> None: + self._store[key.lower()] = (key, value) - def terminate(self): - self.process.terminate() + def __getitem__(self, key: str) -> T: + return self._store[key.lower()][1] - def kill(self): - self.process.kill() + def __delitem__(self, key: str) -> None: + del self._store[key.lower()] - def send_line(self, string): - with self._stdin_lock: - self.process.stdin.write(string + "\n") - self.process.stdin.flush() + def __iter__(self) -> Iterator[str]: + return (casedkey for casedkey, _ in self._store.values()) - def wait_for_return_code(self): - self.process.wait() - return self.process.returncode + def __len__(self) -> int: + return len(self._store) - def pid(self): - return self.process.pid + def __eq__(self, other: object) -> bool: + try: + for key, value in self.items(): + if key not in other or other[key] != value: # type: ignore + return False + + for key, value in other.items(): # type: ignore + if key not in self or self[key] != value: + return False + + return True + except (TypeError, AttributeError): + return NotImplemented + + def copy(self) -> UciOptionMap[T]: + return type(self)(self._store.values()) - def __repr__(self): - return "".format(hex(id(self)), self.pid()) + def __copy__(self) -> UciOptionMap[T]: + return self.copy() + def __repr__(self) -> str: + return f"{type(self).__name__}({dict(self.items())!r})" -class SpurProcess(object): - def __init__(self, engine, shell, command): - self.engine = engine - self.shell = shell - self._stdout_buffer = [] +XBOARD_ERROR_REGEX = re.compile(r"^\s*(Error|Illegal move)(\s*\([^()]+\))?\s*:") + + +class XBoardProtocol(Protocol): + """ + An implementation of the + `XBoard protocol `__ (CECP). + """ - self._result = None + def __init__(self) -> None: + super().__init__() + self.features: Dict[str, Union[int, str]] = {} + self.id = {} + self._options = { + "random": Option("random", "check", False, None, None, None), + "computer": Option("computer", "check", False, None, None, None), + "name": Option("name", "string", "", None, None, None), + "engine_rating": Option("engine_rating", "spin", 0, None, None, None), + "opponent_rating": Option("opponent_rating", "spin", 0, None, None, None) + } + self.config: Dict[str, ConfigValue] = {} + self.target_config: Dict[str, ConfigValue] = {} + self.board = chess.Board() + self.game: object = None + self.clock_id: object = None + self.first_game = True + + @property + @override + def options(self) -> Dict[str, Option]: + return self._options + + async def initialize(self) -> None: + class XBoardInitializeCommand(BaseCommand[None]): + def __init__(self, engine: XBoardProtocol): + super().__init__(engine) + self.engine = engine + + @override + def check_initialized(self) -> None: + if self.engine.initialized: + raise EngineError("engine already initialized") + + @override + def start(self) -> None: + self.engine.send_line("xboard") + self.engine.send_line("protover 2") + self.timeout_handle = self.engine.loop.call_later(2.0, lambda: self.timeout()) + + def timeout(self) -> None: + LOGGER.error("%s: Timeout during initialization", self.engine) + self.end() + + @override + def line_received(self, line: str) -> None: + token, remaining = _next_token(line) + if token.startswith("#"): + pass + elif token == "feature": + self._feature(remaining) + elif XBOARD_ERROR_REGEX.match(line): + raise EngineError(line) + + def _feature(self, arg: str) -> None: + for feature in shlex.split(arg): + key, value = feature.split("=", 1) + if key == "option": + option = _parse_xboard_option(value) + if option.name not in ["random", "computer", "cores", "memory"]: + self.engine.options[option.name] = option + else: + try: + self.engine.features[key] = int(value) + except ValueError: + self.engine.features[key] = value + + if "done" in self.engine.features: + self.timeout_handle.cancel() + if self.engine.features.get("done"): + self.end() + + def end(self) -> None: + if not self.engine.features.get("ping", 0): + self.result.set_exception(EngineError("xboard engine did not declare required feature: ping")) + self.set_finished() + return + if not self.engine.features.get("setboard", 0): + self.result.set_exception(EngineError("xboard engine did not declare required feature: setboard")) + self.set_finished() + return + + if not self.engine.features.get("reuse", 1): + LOGGER.warning("%s: Rejecting feature reuse=0", self.engine) + self.engine.send_line("rejected reuse") + if not self.engine.features.get("sigterm", 1): + LOGGER.warning("%s: Rejecting feature sigterm=0", self.engine) + self.engine.send_line("rejected sigterm") + if self.engine.features.get("san", 0): + LOGGER.warning("%s: Rejecting feature san=1", self.engine) + self.engine.send_line("rejected san") + + if "myname" in self.engine.features: + self.engine.id["name"] = str(self.engine.features["myname"]) + + if self.engine.features.get("memory", 0): + self.engine.options["memory"] = Option("memory", "spin", 16, 1, None, None) + self.engine.send_line("accepted memory") + if self.engine.features.get("smp", 0): + self.engine.options["cores"] = Option("cores", "spin", 1, 1, None, None) + self.engine.send_line("accepted smp") + if self.engine.features.get("egt"): + for egt in str(self.engine.features["egt"]).split(","): + name = f"egtpath {egt}" + self.engine.options[name] = Option(name, "path", None, None, None, None) + self.engine.send_line("accepted egt") + + for option in self.engine.options.values(): + if option.default is not None: + self.engine.config[option.name] = option.default + if option.default is not None and not option.is_managed(): + self.engine.target_config[option.name] = option.default + + self.engine.initialized = True + self.result.set_result(None) + self.set_finished() + + return await self.communicate(XBoardInitializeCommand) + + def _ping(self, n: int) -> None: + self.send_line(f"ping {n}") + + def _variant(self, variant: Optional[str]) -> None: + variants = str(self.features.get("variants", "")).split(",") + if not variant or variant not in variants: + raise EngineError("unsupported xboard variant: {} (available: {})".format(variant, ", ".join(variants))) + + self.send_line(f"variant {variant}") + + def _new(self, board: chess.Board, game: object, options: ConfigMapping, opponent: Optional[Opponent] = None) -> None: + self._configure(options) + self._configure(self._opponent_configuration(opponent=opponent)) + + # Set up starting position. + root = board.root() + new_options = any(param in options for param in ("random", "computer")) + new_game = self.first_game or self.game != game or new_options or opponent or root != self.board.root() + self.game = game + self.first_game = False + if new_game: + self.board = root + self.send_line("new") + + variant = type(board).xboard_variant + if variant == "normal" and board.chess960: + self._variant("fischerandom") + elif variant != "normal": + self._variant(variant) + + if self.config.get("random"): + self.send_line("random") + + opponent_name = self.config.get("name") + if opponent_name and self.features.get("name", True): + self.send_line(f"name {opponent_name}") + + opponent_rating = self.config.get("opponent_rating") + engine_rating = self.config.get("engine_rating") + if engine_rating or opponent_rating: + self.send_line(f"rating {engine_rating or 0} {opponent_rating or 0}") + + if self.config.get("computer"): + self.send_line("computer") + + self.send_line("force") + + fen = root.fen(shredder=board.chess960, en_passant="fen") + if variant != "normal" or fen != chess.STARTING_FEN or board.chess960: + self.send_line(f"setboard {fen}") + else: + self.send_line("force") + + # Undo moves until common position. + common_stack_len = 0 + if not new_game: + for left, right in zip(self.board.move_stack, board.move_stack): + if left == right: + common_stack_len += 1 + else: + break + + while len(self.board.move_stack) > common_stack_len + 1: + self.send_line("remove") + self.board.pop() + self.board.pop() + + while len(self.board.move_stack) > common_stack_len: + self.send_line("undo") + self.board.pop() + + # Play moves from board stack. + for move in board.move_stack[common_stack_len:]: + if not move: + LOGGER.warning("Null move (in %s) may not be supported by all XBoard engines", self.board.fen()) + prefix = "usermove " if self.features.get("usermove", 0) else "" + self.send_line(prefix + self.board.xboard(move)) + self.board.push(move) + + async def ping(self) -> None: + class XBoardPingCommand(BaseCommand[None]): + def __init__(self, engine: XBoardProtocol): + super().__init__(engine) + self.engine = engine + + @override + def start(self) -> None: + n = id(self) & 0xffff + self.pong = f"pong {n}" + self.engine._ping(n) + + @override + def line_received(self, line: str) -> None: + if line == self.pong: + self.result.set_result(None) + self.set_finished() + elif not line.startswith("#"): + LOGGER.warning("%s: Unexpected engine output: %r", self.engine, line) + elif XBOARD_ERROR_REGEX.match(line): + raise EngineError(line) + + return await self.communicate(XBoardPingCommand) + + async def play(self, board: chess.Board, limit: Limit, *, game: object = None, info: Info = INFO_NONE, ponder: bool = False, draw_offered: bool = False, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}, opponent: Optional[Opponent] = None) -> PlayResult: + if root_moves is not None: + raise EngineError("play with root_moves, but xboard supports 'include' only in analysis mode") + + class XBoardPlayCommand(BaseCommand[PlayResult]): + def __init__(self, engine: XBoardProtocol): + super().__init__(engine) + self.engine = engine + + @override + def start(self) -> None: + self.play_result = PlayResult(None, None) + self.stopped = False + self.pong_after_move: Optional[str] = None + self.pong_after_ponder: Optional[str] = None + + # Set game, position and configure. + self.engine._new(board, game, options, opponent) + + # Limit or time control. + clock = limit.white_clock if board.turn else limit.black_clock + increment = limit.white_inc if board.turn else limit.black_inc + if limit.clock_id is None or limit.clock_id != self.engine.clock_id: + self._send_time_control(clock, increment) + self.engine.clock_id = limit.clock_id + if limit.nodes is not None: + if limit.time is not None or limit.white_clock is not None or limit.black_clock is not None or increment is not None: + raise EngineError("xboard does not support mixing node limits with time limits") + + if "nps" not in self.engine.features: + LOGGER.warning("%s: Engine did not explicitly declare support for node limits (feature nps=?)") + elif not self.engine.features["nps"]: + raise EngineError("xboard engine does not support node limits (feature nps=0)") + + self.engine.send_line("nps 1") + self.engine.send_line(f"st {max(1, int(limit.nodes))}") + if limit.depth is not None: + self.engine.send_line(f"sd {max(1, int(limit.depth))}") + if limit.white_clock is not None: + self.engine.send_line("{} {}".format("time" if board.turn else "otim", max(1, round(limit.white_clock * 100)))) + if limit.black_clock is not None: + self.engine.send_line("{} {}".format("otim" if board.turn else "time", max(1, round(limit.black_clock * 100)))) + + if draw_offered and self.engine.features.get("draw", 1): + self.engine.send_line("draw") + + # Start thinking. + self.engine.send_line("post" if info else "nopost") + self.engine.send_line("hard" if ponder else "easy") + self.engine.send_line("go") + + @override + def line_received(self, line: str) -> None: + token, remaining = _next_token(line) + if token == "move": + self._move(remaining.strip()) + elif token == "Hint:": + self._hint(remaining.strip()) + elif token == "pong": + pong_line = f"{token} {remaining.strip()}" + if pong_line == self.pong_after_move: + if not self.result.done(): + self.result.set_result(self.play_result) + if not ponder: + self.set_finished() + elif pong_line == self.pong_after_ponder: + if not self.result.done(): + self.result.set_result(self.play_result) + self.set_finished() + elif f"{token} {remaining.strip()}" == "offer draw": + if not self.result.done(): + self.play_result.draw_offered = True + self._ping_after_move() + elif line.strip() == "resign": + if not self.result.done(): + self.play_result.resigned = True + self._ping_after_move() + elif token in ["1-0", "0-1", "1/2-1/2"]: + if "resign" in line and not self.result.done(): + self.play_result.resigned = True + self._ping_after_move() + elif token.startswith("#"): + pass + elif XBOARD_ERROR_REGEX.match(line): + self.engine.first_game = True # Board state might no longer be in sync + raise EngineError(line) + elif len(line.split()) >= 4 and line.lstrip()[0].isdigit(): + self._post(line) + else: + LOGGER.warning("%s: Unexpected engine output: %r", self.engine, line) + + def _send_time_control(self, clock: Optional[float], increment: Optional[float]) -> None: + if limit.remaining_moves or clock is not None or increment is not None: + base_mins, base_secs = divmod(int(clock or 0), 60) + self.engine.send_line(f"level {limit.remaining_moves or 0} {base_mins}:{base_secs:02d} {increment or 0}") + if limit.time is not None: + self.engine.send_line(f"st {max(0.01, limit.time)}") + + def _post(self, line: str) -> None: + if not self.result.done(): + self.play_result.info = _parse_xboard_post(line, self.engine.board, info) + + def _move(self, arg: str) -> None: + if not self.result.done() and self.play_result.move is None: + try: + self.play_result.move = self.engine.board.push_xboard(arg) + except ValueError as err: + self.result.set_exception(EngineError(err)) + else: + self._ping_after_move() + else: + try: + self.engine.board.push_xboard(arg) + except ValueError: + LOGGER.exception("Exception playing unexpected move") + + def _hint(self, arg: str) -> None: + if not self.result.done() and self.play_result.move is not None and self.play_result.ponder is None: + try: + self.play_result.ponder = self.engine.board.parse_xboard(arg) + except ValueError: + LOGGER.exception("Exception parsing hint") + else: + LOGGER.warning("Unexpected hint: %r", arg) + + def _ping_after_move(self) -> None: + if self.pong_after_move is None: + n = id(self) & 0xffff + self.pong_after_move = f"pong {n}" + self.engine._ping(n) + + @override + def cancel(self) -> None: + if self.stopped: + return + self.stopped = True + + if self.result.cancelled(): + self.engine.send_line("?") + + if ponder: + self.engine.send_line("easy") + + n = (id(self) + 1) & 0xffff + self.pong_after_ponder = f"pong {n}" + self.engine._ping(n) + + @override + def engine_terminated(self, exc: Exception) -> None: + # Allow terminating engine while pondering. + if not self.result.done(): + super().engine_terminated(exc) + + return await self.communicate(XBoardPlayCommand) + + async def analysis(self, board: chess.Board, limit: Optional[Limit] = None, *, multipv: Optional[int] = None, game: object = None, info: Info = INFO_ALL, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> AnalysisResult: + if multipv is not None: + raise EngineError("xboard engine does not support multipv") + + if limit is not None and (limit.white_clock is not None or limit.black_clock is not None): + raise EngineError("xboard analysis does not support clock limits") + + class XBoardAnalysisCommand(BaseCommand[AnalysisResult]): + def __init__(self, engine: XBoardProtocol): + super().__init__(engine) + self.engine = engine + + @override + def start(self) -> None: + self.stopped = False + self.best_move: Optional[chess.Move] = None + self.analysis = AnalysisResult(stop=lambda: self.cancel()) + self.final_pong: Optional[str] = None + + self.engine._new(board, game, options) + + if root_moves is not None: + if not self.engine.features.get("exclude", 0): + raise EngineError("xboard engine does not support root_moves (feature exclude=0)") + + self.engine.send_line("exclude all") + for move in root_moves: + self.engine.send_line(f"include {self.engine.board.xboard(move)}") + + self.engine.send_line("post") + self.engine.send_line("analyze") + + self.result.set_result(self.analysis) + + if limit is not None and limit.time is not None: + self.time_limit_handle: Optional[asyncio.Handle] = self.engine.loop.call_later(limit.time, lambda: self.cancel()) + else: + self.time_limit_handle = None + + @override + def line_received(self, line: str) -> None: + token, remaining = _next_token(line) + if token.startswith("#"): + pass + elif len(line.split()) >= 4 and line.lstrip()[0].isdigit(): + self._post(line) + elif f"{token} {remaining.strip()}" == self.final_pong: + self.end() + elif XBOARD_ERROR_REGEX.match(line): + self.engine.first_game = True # Board state might no longer be in sync + raise EngineError(line) + else: + LOGGER.warning("%s: Unexpected engine output: %r", self.engine, line) + + def _post(self, line: str) -> None: + post_info = _parse_xboard_post(line, self.engine.board, info) + self.analysis.post(post_info) + + pv = post_info.get("pv") + if pv: + self.best_move = pv[0] + + if limit is not None: + if limit.time is not None and post_info.get("time", 0) >= limit.time: + self.cancel() + elif limit.nodes is not None and post_info.get("nodes", 0) >= limit.nodes: + self.cancel() + elif limit.depth is not None and post_info.get("depth", 0) >= limit.depth: + self.cancel() + elif limit.mate is not None and "score" in post_info: + if post_info["score"].relative >= Mate(limit.mate): + self.cancel() + + def end(self) -> None: + if self.time_limit_handle: + self.time_limit_handle.cancel() + + self.set_finished() + self.analysis.set_finished(BestMove(self.best_move, None)) + + @override + def cancel(self) -> None: + if self.stopped: + return + self.stopped = True + + self.engine.send_line(".") + self.engine.send_line("exit") + + n = id(self) & 0xffff + self.final_pong = f"pong {n}" + self.engine._ping(n) + + @override + def engine_terminated(self, exc: Exception) -> None: + LOGGER.debug("%s: Closing analysis because engine has been terminated (error: %s)", self.engine, exc) + + if self.time_limit_handle: + self.time_limit_handle.cancel() + + self.analysis.set_exception(exc) + + return await self.communicate(XBoardAnalysisCommand) + + def _setoption(self, name: str, value: ConfigValue) -> None: + if value is not None and value == self.config.get(name): + return - self._waiting_thread = threading.Thread(target=self._waiting_thread_target) - self._waiting_thread.daemon = True + try: + option = self.options[name] + except KeyError: + raise EngineError(f"unsupported xboard option or command: {name}") - self.engine.on_process_spawned(self) - self.process = self.shell.spawn(command, store_pid=True, allow_error=True, stdout=self) - self._waiting_thread.start() + self.config[name] = value = option.parse(value) - def write(self, byte): - # Interally called whenever a byte is received. - if byte == b"\r": + if name in ["random", "computer", "name", "engine_rating", "opponent_rating"]: + # Applied in _new. pass - elif byte == b"\n": - self.engine.on_line_received(b"".join(self._stdout_buffer).decode("utf-8")) - del self._stdout_buffer[:] + elif name in ["memory", "cores"] or name.startswith("egtpath "): + self.send_line(f"{name} {value}") + elif value is None: + self.send_line(f"option {name}") + elif value is True: + self.send_line(f"option {name}=1") + elif value is False: + self.send_line(f"option {name}=0") + else: + self.send_line(f"option {name}={value}") + + def _configure(self, options: ConfigMapping) -> None: + for name, value in _chain_config(options, self.target_config): + if name.lower() in MANAGED_OPTIONS: + raise EngineError(f"cannot set {name} which is automatically managed") + self._setoption(name, value) + + async def configure(self, options: ConfigMapping) -> None: + class XBoardConfigureCommand(BaseCommand[None]): + def __init__(self, engine: XBoardProtocol): + super().__init__(engine) + self.engine = engine + + @override + def start(self) -> None: + self.engine._configure(options) + self.engine.target_config.update({name: value for name, value in options.items() if value is not None}) + self.result.set_result(None) + self.set_finished() + + return await self.communicate(XBoardConfigureCommand) + + def _opponent_configuration(self, *, opponent: Optional[Opponent] = None, engine_rating: Optional[int] = None) -> ConfigMapping: + if opponent is None: + return {} + + opponent_info: Dict[str, Union[int, bool, str]] = {"engine_rating": engine_rating or self.target_config.get("engine_rating") or 0, + "opponent_rating": opponent.rating or 0, + "computer": opponent.is_engine or False} + + if opponent.name and self.features.get("name", True): + opponent_info["name"] = f"{opponent.title or ''} {opponent.name}".strip() + + return opponent_info + + async def send_opponent_information(self, *, opponent: Optional[Opponent] = None, engine_rating: Optional[int] = None) -> None: + return await self.configure(self._opponent_configuration(opponent=opponent, engine_rating=engine_rating)) + + async def send_game_result(self, board: chess.Board, winner: Optional[Color] = None, game_ending: Optional[str] = None, game_complete: bool = True) -> None: + class XBoardGameResultCommand(BaseCommand[None]): + def __init__(self, engine: XBoardProtocol): + super().__init__(engine) + self.engine = engine + + @override + def start(self) -> None: + if game_ending and any(c in game_ending for c in "{}\n\r"): + raise EngineError(f"invalid line break or curly braces in game ending message: {game_ending!r}") + + self.engine._new(board, self.engine.game, {}) # Send final moves to engine. + + outcome = board.outcome(claim_draw=True) + + if not game_complete: + result = "*" + ending = game_ending or "" + elif winner is not None or game_ending: + result = "1-0" if winner == chess.WHITE else "0-1" if winner == chess.BLACK else "1/2-1/2" + ending = game_ending or "" + elif outcome is not None and outcome.winner is not None: + result = outcome.result() + winning_color = "White" if outcome.winner == chess.WHITE else "Black" + is_checkmate = outcome.termination == chess.Termination.CHECKMATE + ending = f"{winning_color} {'mates' if is_checkmate else 'variant win'}" + elif outcome is not None: + result = outcome.result() + ending = outcome.termination.name.capitalize().replace("_", " ") + else: + result = "*" + ending = "" + + ending_text = f"{{{ending}}}" if ending else "" + self.engine.send_line(f"result {result} {ending_text}".strip()) + self.result.set_result(None) + self.set_finished() + + return await self.communicate(XBoardGameResultCommand) + + async def quit(self) -> None: + self.send_line("quit") + await asyncio.shield(self.returncode) + + +def _parse_xboard_option(feature: str) -> Option: + params = feature.split() + + name = params[0] + type = params[1][1:] + default: Optional[ConfigValue] = None + min = None + max = None + var = None + + if type == "combo": + var = [] + choices = params[2:] + for choice in choices: + if choice == "///": + continue + elif choice[0] == "*": + default = choice[1:] + var.append(choice[1:]) + else: + var.append(choice) + elif type == "check": + default = int(params[2]) + elif type in ["string", "file", "path"]: + if len(params) > 2: + default = params[2] else: - self._stdout_buffer.append(byte) + default = "" + elif type == "spin": + default = int(params[2]) + min = int(params[3]) + max = int(params[4]) + + return Option(name, type, default, min, max, var) + + +def _parse_xboard_post(line: str, root_board: chess.Board, selector: Info = INFO_ALL) -> InfoDict: + # Format: depth score time nodes [seldepth [nps [tbhits]]] pv + info: InfoDict = {} + + # Split leading integer tokens from pv. + pv_tokens = line.split() + integer_tokens = [] + while pv_tokens: + token = pv_tokens.pop(0) + try: + integer_tokens.append(int(token)) + except ValueError: + pv_tokens.insert(0, token) + break + + if len(integer_tokens) < 4: + return info + + # Required integer tokens. + info["depth"] = integer_tokens.pop(0) + cp = integer_tokens.pop(0) + info["time"] = int(integer_tokens.pop(0)) / 100 + info["nodes"] = int(integer_tokens.pop(0)) + + # Score. + if cp <= -100000: + score: Score = Mate(cp + 100000) + elif cp == 100000: + score = MateGiven + elif cp >= 100000: + score = Mate(cp - 100000) + else: + score = Cp(cp) + info["score"] = PovScore(score, root_board.turn) + + # Optional integer tokens. + if integer_tokens: + info["seldepth"] = integer_tokens.pop(0) + if integer_tokens: + info["nps"] = integer_tokens.pop(0) + + while len(integer_tokens) > 1: + # Reserved for future extensions. + integer_tokens.pop(0) + + if integer_tokens: + info["tbhits"] = integer_tokens.pop(0) + + # Principal variation. + pv = [] + board = root_board.copy(stack=False) + for token in pv_tokens: + if token.rstrip(".").isdigit(): + continue + + try: + pv.append(board.push_xboard(token)) + except ValueError: + break + + if not (selector & INFO_PV): + break + info["pv"] = pv + + return info + + +def _next_token(line: str) -> tuple[str, str]: + """ + Get the next token in a whitespace-delimited line of text. + + The result is returned as a 2-part tuple of strings. + + If the input line is empty or all whitespace, then the result is two + empty strings. + + If the input line is not empty and not completely whitespace, then + the first element of the returned tuple is a single word with + leading and trailing whitespace removed. The second element is the + unchanged rest of the line. + """ + parts = line.split(maxsplit=1) + return parts[0] if parts else "", parts[1] if len(parts) == 2 else "" + + +class BestMove: + """Returned by :func:`chess.engine.AnalysisResult.wait()`.""" + + move: Optional[chess.Move] + """The best move according to the engine, or ``None``.""" - def _waiting_thread_target(self): - self._result = self.process.wait_for_result() - self.engine.on_terminated() + ponder: Optional[chess.Move] + """The response that the engine expects after *move*, or ``None``.""" - def is_alive(self): - return self.process.is_running() + def __init__(self, move: Optional[chess.Move], ponder: Optional[chess.Move]): + self.move = move + self.ponder = ponder - def terminate(self): - self.process.send_signal(signal.SIGTERM) + def __repr__(self) -> str: + return "<{} at {:#x} (move={}, ponder={}>".format( + type(self).__name__, id(self), self.move, self.ponder) - def kill(self): - self.process.send_signal(signal.SIGKILL) - def send_line(self, string): - self.process.stdin_write(string.encode("utf-8")) - self.process.stdin_write(b"\n") +class AnalysisResult: + """ + Handle to ongoing engine analysis. + Returned by :func:`chess.engine.Protocol.analysis()`. + + Can be used to asynchronously iterate over information sent by the engine. + + Automatically stops the analysis when used as a context manager. + """ + + multipv: List[InfoDict] + """ + A list of dictionaries with aggregated information sent by the engine. + One item for each root move. + """ + + def __init__(self, stop: Optional[Callable[[], None]] = None): + self._stop = stop + self._queue: asyncio.Queue[InfoDict] = asyncio.Queue() + self._posted_kork = False + self._seen_kork = False + self._finished: asyncio.Future[BestMove] = asyncio.Future() + self.multipv = [{}] + + def post(self, info: InfoDict) -> None: + # Empty dictionary reserved for kork. + if not info: + return + + multipv = info.get("multipv", 1) + while len(self.multipv) < multipv: + self.multipv.append({}) + self.multipv[multipv - 1].update(info) + + self._queue.put_nowait(info) + + def _kork(self) -> None: + if not self._posted_kork: + self._posted_kork = True + self._queue.put_nowait({}) + + def set_finished(self, best: BestMove) -> None: + if not self._finished.done(): + self._finished.set_result(best) + self._kork() + + def set_exception(self, exc: Exception) -> None: + self._finished.set_exception(exc) + self._kork() + + @property + def info(self) -> InfoDict: + """ + A dictionary of aggregated information sent by the engine. This is + actually an alias for ``multipv[0]``. + """ + return self.multipv[0] + + def stop(self) -> None: + """Stops the analysis as soon as possible.""" + if self._stop and not self._posted_kork: + self._stop() + self._stop = None + + async def wait(self) -> BestMove: + """Waits until the analysis is finished.""" + return await self._finished + + async def get(self) -> InfoDict: + """ + Waits for the next dictionary of information from the engine and + returns it. + + It might be more convenient to use ``async for info in analysis: ...``. + + :raises: :exc:`chess.engine.AnalysisComplete` if the analysis is + complete (or has been stopped) and all information has been + consumed. Use :func:`~chess.engine.AnalysisResult.next()` if you + prefer to get ``None`` instead of an exception. + """ + if self._seen_kork: + raise AnalysisComplete() + + info = await self._queue.get() + if not info: + # Empty dictionary marks end. + self._seen_kork = True + await self._finished + raise AnalysisComplete() + + return info + + def would_block(self) -> bool: + """ + Checks if calling :func:`~chess.engine.AnalysisResult.get()`, + calling :func:`~chess.engine.AnalysisResult.next()`, + or advancing the iterator one step would require waiting for the + engine. + + These functions would return immediately if information is + pending (queue is not + :func:`empty `) or if the search + is finished. + """ + return not self._seen_kork and self._queue.empty() + + def empty(self) -> bool: + """ + Checks if all current information has been consumed. + + If the queue is empty, but the analysis is still ongoing, then further + information can become available in the future. + """ + return self._seen_kork or self._queue.qsize() <= self._posted_kork + + async def next(self) -> Optional[InfoDict]: + try: + return await self.get() + except AnalysisComplete: + return None + + def __aiter__(self) -> AnalysisResult: + return self - def wait_for_return_code(self): - return self.process.wait_for_result().return_code + async def __anext__(self) -> InfoDict: + try: + return await self.get() + except AnalysisComplete: + raise StopAsyncIteration - def pid(self): - return self.process.pid + def __enter__(self) -> AnalysisResult: + return self - def __repr__(self): - return "".format(hex(id(self)), self.pid()) + def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]) -> None: + self.stop() -def _popen_engine(command, engine_cls, setpgrp=False, _popen_lock=threading.Lock(), **kwargs): +async def popen_uci(command: Union[str, List[str]], *, setpgrp: bool = False, **popen_args: Any) -> Tuple[asyncio.SubprocessTransport, UciProtocol]: """ - Opens a local chess engine process. + Spawns and initializes a UCI engine. - :param engine_cls: Engine class + :param command: Path of the engine executable, or a list including the + path and arguments. :param setpgrp: Open the engine process in a new process group. This will - stop signals (such as keyboards interrupts) from propagating from the + stop signals (such as keyboard interrupts) from propagating from the parent process. Defaults to ``False``. + :param popen_args: Additional arguments for + `popen `_. + Do not set ``stdin``, ``stdout``, ``bufsize`` or + ``universal_newlines``. + + Returns a subprocess transport and engine protocol pair. """ - engine = engine_cls() + transport, protocol = await UciProtocol.popen(command, setpgrp=setpgrp, **popen_args) + try: + await protocol.initialize() + except: + transport.close() + raise + return transport, protocol - popen_args = {} - if setpgrp: - try: - # Windows - popen_args["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP - except AttributeError: - # Unix - popen_args["preexec_fn"] = os.setpgrp - popen_args.update(kwargs) - # Work around possible race condition in Python 2 subprocess module, - # that can occur when concurrently opening processes. - with _popen_lock: - PopenProcess(engine, command, **popen_args) +async def popen_xboard(command: Union[str, List[str]], *, setpgrp: bool = False, **popen_args: Any) -> Tuple[asyncio.SubprocessTransport, XBoardProtocol]: + """ + Spawns and initializes an XBoard engine. - return engine + :param command: Path of the engine executable, or a list including the + path and arguments. + :param setpgrp: Open the engine process in a new process group. This will + stop signals (such as keyboard interrupts) from propagating from the + parent process. Defaults to ``False``. + :param popen_args: Additional arguments for + `popen `_. + Do not set ``stdin``, ``stdout``, ``bufsize`` or + ``universal_newlines``. + + Returns a subprocess transport and engine protocol pair. + """ + transport, protocol = await XBoardProtocol.popen(command, setpgrp=setpgrp, **popen_args) + try: + await protocol.initialize() + except: + transport.close() + raise + return transport, protocol -def _spur_spawn_engine(shell, command, engine_cls): +async def _async(sync: Callable[[], T]) -> T: + return sync() + + +class SimpleEngine: """ - Spawns a remote engine using a `Spur`_ shell. + Synchronous wrapper around a transport and engine protocol pair. Provides + the same methods and attributes as :class:`chess.engine.Protocol` + with blocking functions instead of coroutines. + + You may not concurrently modify objects passed to any of the methods. Other + than that, :class:`~chess.engine.SimpleEngine` is thread-safe. When sending + a new command to the engine, any previous running command will be cancelled + as soon as possible. + + Methods will raise :class:`asyncio.TimeoutError` if an operation takes + *timeout* seconds longer than expected (unless *timeout* is ``None``). - .. _Spur: https://pypi.python.org/pypi/spur + Automatically closes the transport when used as a context manager. """ - engine = engine_cls() - SpurProcess(engine, shell, command) - return engine + + def __init__(self, transport: asyncio.SubprocessTransport, protocol: Protocol, *, timeout: Optional[float] = 10.0) -> None: + self.transport = transport + self.protocol = protocol + self.timeout = timeout + + self._shutdown_lock = threading.Lock() + self._shutdown = False + self.shutdown_event = asyncio.Event() + + self.returncode: concurrent.futures.Future[int] = concurrent.futures.Future() + + def _timeout_for(self, limit: Optional[Limit]) -> Optional[float]: + if self.timeout is None or limit is None or limit.time is None: + return None + return self.timeout + limit.time + + @contextlib.contextmanager + def _not_shut_down(self) -> Generator[None, None, None]: + with self._shutdown_lock: + if self._shutdown: + raise EngineTerminatedError("engine event loop dead") + yield + + @property + def options(self) -> MutableMapping[str, Option]: + with self._not_shut_down(): + coro = _async(lambda: copy.copy(self.protocol.options)) + future = asyncio.run_coroutine_threadsafe(coro, self.protocol.loop) + return future.result() + + @property + def id(self) -> Mapping[str, str]: + with self._not_shut_down(): + coro = _async(lambda: self.protocol.id.copy()) + future = asyncio.run_coroutine_threadsafe(coro, self.protocol.loop) + return future.result() + + def communicate(self, command_factory: Callable[[Protocol], BaseCommand[T]]) -> T: + with self._not_shut_down(): + coro = self.protocol.communicate(command_factory) + future = asyncio.run_coroutine_threadsafe(coro, self.protocol.loop) + return future.result() + + def configure(self, options: ConfigMapping) -> None: + with self._not_shut_down(): + coro = asyncio.wait_for(self.protocol.configure(options), self.timeout) + future = asyncio.run_coroutine_threadsafe(coro, self.protocol.loop) + return future.result() + + def send_opponent_information(self, *, opponent: Optional[Opponent] = None, engine_rating: Optional[int] = None) -> None: + with self._not_shut_down(): + coro = asyncio.wait_for( + self.protocol.send_opponent_information(opponent=opponent, engine_rating=engine_rating), + self.timeout) + future = asyncio.run_coroutine_threadsafe(coro, self.protocol.loop) + return future.result() + + def ping(self) -> None: + with self._not_shut_down(): + coro = asyncio.wait_for(self.protocol.ping(), self.timeout) + future = asyncio.run_coroutine_threadsafe(coro, self.protocol.loop) + return future.result() + + def play(self, board: chess.Board, limit: Limit, *, game: object = None, info: Info = INFO_NONE, ponder: bool = False, draw_offered: bool = False, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}, opponent: Optional[Opponent] = None) -> PlayResult: + with self._not_shut_down(): + coro = asyncio.wait_for( + self.protocol.play(board, limit, game=game, info=info, ponder=ponder, draw_offered=draw_offered, root_moves=root_moves, options=options, opponent=opponent), + self._timeout_for(limit)) + future = asyncio.run_coroutine_threadsafe(coro, self.protocol.loop) + return future.result() + + @typing.overload + def analyse(self, board: chess.Board, limit: Limit, *, game: object = None, info: Info = INFO_ALL, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> InfoDict: ... + @typing.overload + def analyse(self, board: chess.Board, limit: Limit, *, multipv: int, game: object = None, info: Info = INFO_ALL, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> List[InfoDict]: ... + @typing.overload + def analyse(self, board: chess.Board, limit: Limit, *, multipv: Optional[int] = None, game: object = None, info: Info = INFO_ALL, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> Union[InfoDict, List[InfoDict]]: ... + def analyse(self, board: chess.Board, limit: Limit, *, multipv: Optional[int] = None, game: object = None, info: Info = INFO_ALL, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> Union[InfoDict, List[InfoDict]]: + with self._not_shut_down(): + coro = asyncio.wait_for( + self.protocol.analyse(board, limit, multipv=multipv, game=game, info=info, root_moves=root_moves, options=options), + self._timeout_for(limit)) + future = asyncio.run_coroutine_threadsafe(coro, self.protocol.loop) + return future.result() + + def analysis(self, board: chess.Board, limit: Optional[Limit] = None, *, multipv: Optional[int] = None, game: object = None, info: Info = INFO_ALL, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> SimpleAnalysisResult: + with self._not_shut_down(): + coro = asyncio.wait_for( + self.protocol.analysis(board, limit, multipv=multipv, game=game, info=info, root_moves=root_moves, options=options), + self.timeout) # Timeout until analysis is *started* + future = asyncio.run_coroutine_threadsafe(coro, self.protocol.loop) + return SimpleAnalysisResult(self, future.result()) + + def send_game_result(self, board: chess.Board, winner: Optional[Color] = None, game_ending: Optional[str] = None, game_complete: bool = True) -> None: + with self._not_shut_down(): + coro = asyncio.wait_for(self.protocol.send_game_result(board, winner, game_ending, game_complete), self.timeout) + future = asyncio.run_coroutine_threadsafe(coro, self.protocol.loop) + return future.result() + + def quit(self) -> None: + with self._not_shut_down(): + coro = asyncio.wait_for(self.protocol.quit(), self.timeout) + future = asyncio.run_coroutine_threadsafe(coro, self.protocol.loop) + return future.result() + + def close(self) -> None: + """ + Closes the transport and the background event loop as soon as possible. + """ + def _shutdown() -> None: + self.transport.close() + self.shutdown_event.set() + + with self._shutdown_lock: + if not self._shutdown: + self._shutdown = True + self.protocol.loop.call_soon_threadsafe(_shutdown) + + @classmethod + def popen(cls, Protocol: Type[Protocol], command: Union[str, List[str]], *, timeout: Optional[float] = 10.0, debug: Optional[bool] = None, setpgrp: bool = False, **popen_args: Any) -> SimpleEngine: + async def background(future: concurrent.futures.Future[SimpleEngine]) -> None: + transport, protocol = await Protocol.popen(command, setpgrp=setpgrp, **popen_args) + threading.current_thread().name = f"{cls.__name__} (pid={transport.get_pid()})" + simple_engine = cls(transport, protocol, timeout=timeout) + try: + await asyncio.wait_for(protocol.initialize(), timeout) + future.set_result(simple_engine) + returncode = await protocol.returncode + simple_engine.returncode.set_result(returncode) + finally: + simple_engine.close() + await simple_engine.shutdown_event.wait() + + return run_in_background(background, name=f"{cls.__name__} (command={command!r})", debug=debug) + + @classmethod + def popen_uci(cls, command: Union[str, List[str]], *, timeout: Optional[float] = 10.0, debug: Optional[bool] = None, setpgrp: bool = False, **popen_args: Any) -> SimpleEngine: + """ + Spawns and initializes a UCI engine. + Returns a :class:`~chess.engine.SimpleEngine` instance. + """ + return cls.popen(UciProtocol, command, timeout=timeout, debug=debug, setpgrp=setpgrp, **popen_args) + + @classmethod + def popen_xboard(cls, command: Union[str, List[str]], *, timeout: Optional[float] = 10.0, debug: Optional[bool] = None, setpgrp: bool = False, **popen_args: Any) -> SimpleEngine: + """ + Spawns and initializes an XBoard engine. + Returns a :class:`~chess.engine.SimpleEngine` instance. + """ + return cls.popen(XBoardProtocol, command, timeout=timeout, debug=debug, setpgrp=setpgrp, **popen_args) + + def __enter__(self) -> SimpleEngine: + return self + + def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]) -> None: + self.close() + + def __repr__(self) -> str: + pid = self.transport.get_pid() # This happens to be thread-safe + return f"<{type(self).__name__} (pid={pid})>" + + +class SimpleAnalysisResult: + """ + Synchronous wrapper around :class:`~chess.engine.AnalysisResult`. Returned + by :func:`chess.engine.SimpleEngine.analysis()`. + """ + + def __init__(self, simple_engine: SimpleEngine, inner: AnalysisResult) -> None: + self.simple_engine = simple_engine + self.inner = inner + + @property + def info(self) -> InfoDict: + with self.simple_engine._not_shut_down(): + coro = _async(lambda: self.inner.info.copy()) + future = asyncio.run_coroutine_threadsafe(coro, self.simple_engine.protocol.loop) + return future.result() + + @property + def multipv(self) -> List[InfoDict]: + with self.simple_engine._not_shut_down(): + coro = _async(lambda: [info.copy() for info in self.inner.multipv]) + future = asyncio.run_coroutine_threadsafe(coro, self.simple_engine.protocol.loop) + return future.result() + + def stop(self) -> None: + with self.simple_engine._not_shut_down(): + self.simple_engine.protocol.loop.call_soon_threadsafe(self.inner.stop) + + def wait(self) -> BestMove: + with self.simple_engine._not_shut_down(): + future = asyncio.run_coroutine_threadsafe(self.inner.wait(), self.simple_engine.protocol.loop) + return future.result() + + def would_block(self) -> bool: + with self.simple_engine._not_shut_down(): + future = asyncio.run_coroutine_threadsafe(_async(self.inner.would_block), self.simple_engine.protocol.loop) + return future.result() + + def empty(self) -> bool: + with self.simple_engine._not_shut_down(): + future = asyncio.run_coroutine_threadsafe(_async(self.inner.empty), self.simple_engine.protocol.loop) + return future.result() + + def get(self) -> InfoDict: + with self.simple_engine._not_shut_down(): + future = asyncio.run_coroutine_threadsafe(self.inner.get(), self.simple_engine.protocol.loop) + return future.result() + + def next(self) -> Optional[InfoDict]: + with self.simple_engine._not_shut_down(): + future = asyncio.run_coroutine_threadsafe(self.inner.next(), self.simple_engine.protocol.loop) + return future.result() + + def __iter__(self) -> Iterator[InfoDict]: + with self.simple_engine._not_shut_down(): + self.simple_engine.protocol.loop.call_soon_threadsafe(self.inner.__aiter__) + return self + + def __next__(self) -> InfoDict: + try: + with self.simple_engine._not_shut_down(): + future = asyncio.run_coroutine_threadsafe(self.inner.__anext__(), self.simple_engine.protocol.loop) + return future.result() + except StopAsyncIteration: + raise StopIteration + + def __enter__(self) -> SimpleAnalysisResult: + return self + + def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]) -> None: + self.stop() diff --git a/chess/gaviota.py b/chess/gaviota.py index f498f762a..7152a18f0 100644 --- a/chess/gaviota.py +++ b/chess/gaviota.py @@ -1,35 +1,20 @@ -# -*- coding: utf-8 -*- -# -# This file is part of the python-chess library. -# Copyright (C) 2015 Jean-Noël Avila -# Copyright (C) 2015-2017 Niklas Fiekas -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . +from __future__ import annotations import ctypes import ctypes.util +import dataclasses import fnmatch +import logging +import lzma import os import os.path -import logging import struct +import typing + import chess -try: - import backport_collections as collections -except ImportError: - import collections +from types import TracebackType +from typing import BinaryIO, Callable, Dict, List, Optional, Tuple, Type, Union LOGGER = logging.getLogger(__name__) @@ -99,38 +84,38 @@ EGTB_MAXBLOCKSIZE = 65536 -def map24_b(s): - s = s - 8 +def map24_b(s: int) -> int: + s -= 8 return ((s & 3) + s) >> 1 -def map88(x): +def map88(x: int) -> int: return x + (x & 56) -def in_queenside(x): +def in_queenside(x: int) -> int: return (x & (1 << 2)) == 0 -def flip_we(x): +def flip_we(x: int) -> int: return x ^ 7 -def flip_ns(x): +def flip_ns(x: int) -> int: return x ^ 56 -def flip_nw_se(x): +def flip_nw_se(x: int) -> int: return ((x & 7) << 3) | (x >> 3) -def idx_is_empty(x): +def idx_is_empty(x: int) -> int: return x == -1 -def flip_type(x, y): +def flip_type(x: chess.Square, y: chess.Square) -> int: ret = 0 - if chess.square_file(x) > 3: + if chess.square_file(x) > chess.FILE_D: x = flip_we(x) y = flip_we(y) ret |= 1 - if chess.square_rank(x) > 3: + if chess.square_rank(x) > chess.RANK_4: x = flip_ns(x) y = flip_ns(y) ret |= 2 @@ -152,17 +137,16 @@ def flip_type(x, y): return ret -def init_flipt(): +def init_flipt() -> List[List[int]]: return [[flip_type(j, i) for i in range(64)] for j in range(64)] FLIPT = init_flipt() -def init_pp48_idx(): - MAX_I = 48 - MAX_J = 48 +def init_pp48_idx() -> Tuple[List[List[int]], List[int], List[int]]: + MAX_I = MAX_J = 48 idx = 0 - pp48_idx = [[-1] * MAX_J for i in range(MAX_I)] + pp48_idx = [[-1] * MAX_J for _ in range(MAX_I)] pp48_sq_x = [NOSQUARE] * MAX_PP48_INDEX pp48_sq_y = [NOSQUARE] * MAX_PP48_INDEX @@ -184,11 +168,9 @@ def init_pp48_idx(): PP48_IDX, PP48_SQ_X, PP48_SQ_Y = init_pp48_idx() -def init_ppp48_idx(): - MAX_I = 48 - MAX_J = 48 - MAX_K = 48 - ppp48_idx = [[[-1] * MAX_I for j in range(MAX_J)] for k in range(MAX_K)] +def init_ppp48_idx() -> Tuple[List[List[List[int]]], List[int], List[int], List[int]]: + MAX_I = MAX_J = MAX_K = 48 + ppp48_idx = [[[-1] * MAX_I for _ in range(MAX_J)] for _ in range(MAX_K)] ppp48_sq_x = [NOSQUARE] * MAX_PPP48_INDEX ppp48_sq_y = [NOSQUARE] * MAX_PPP48_INDEX ppp48_sq_z = [NOSQUARE] * MAX_PPP48_INDEX @@ -217,15 +199,15 @@ def init_ppp48_idx(): ppp48_sq_x[idx] = i ppp48_sq_y[idx] = j ppp48_sq_z[idx] = k - idx = idx + 1 + idx += 1 return ppp48_idx, ppp48_sq_x, ppp48_sq_y, ppp48_sq_z PPP48_IDX, PPP48_SQ_X, PPP48_SQ_Y, PPP48_SQ_Z = init_ppp48_idx() -def init_aaidx(): - aaidx = [[-1] * 64 for y in range(64)] +def init_aaidx() -> Tuple[List[int], List[List[int]]]: + aaidx = [[-1] * 64 for _ in range(64)] aabase = [0] * MAX_AAINDEX idx = 0 @@ -244,18 +226,18 @@ def init_aaidx(): AABASE, AAIDX = init_aaidx() -def init_aaa(): +def init_aaa() -> Tuple[List[int], List[List[int]]]: # Get aaa_base. comb = [a * (a - 1) // 2 for a in range(64)] accum = 0 aaa_base = [0] * 64 for a in range(64 - 1): - accum = accum + comb[a] + accum += comb[a] aaa_base[a + 1] = accum # Get aaa_xyz. - aaa_xyz = [[-1] * 3 for idx in range(MAX_AAAINDEX)] + aaa_xyz = [[-1] * 3 for _ in range(MAX_AAAINDEX)] idx = 0 for z in range(64): @@ -271,7 +253,7 @@ def init_aaa(): AAA_BASE, AAA_XYZ = init_aaa() -def pp_putanchorfirst(a, b): +def pp_putanchorfirst(a: int, b: int) -> Tuple[int, int]: row_b = b & 56 row_a = a & 56 @@ -315,26 +297,26 @@ def pp_putanchorfirst(a, b): return anchor, loosen -def wsq_to_pidx24(pawn): +def wsq_to_pidx24(pawn: int) -> int: sq = pawn sq = flip_ns(sq) - sq -= 8 # Down one row. + sq -= 8 # Down one row idx24 = (sq + (sq & 3)) >> 1 return idx24 -def wsq_to_pidx48(pawn): +def wsq_to_pidx48(pawn: int) -> int: sq = pawn sq = flip_ns(sq) - sq -= 8 # Down one row. + sq -= 8 # Down one row idx48 = sq return idx48 -def init_ppidx(): - ppidx = [[-1] * 48 for i in range(24)] +def init_ppidx() -> Tuple[List[List[int]], List[int], List[int]]: + ppidx = [[-1] * 48 for _ in range(24)] pp_hi24 = [-1] * MAX_PPINDEX pp_lo48 = [-1] * MAX_PPINDEX @@ -350,7 +332,7 @@ def init_ppidx(): anchor, loosen = pp_putanchorfirst(a, b) if (anchor & 7) > 3: - # Square in the kingside. + # Square on the kingside. anchor = flip_we(anchor) loosen = flip_we(loosen) @@ -368,19 +350,19 @@ def init_ppidx(): PPIDX, PP_HI24, PP_LO48 = init_ppidx() -def norm_kkindex(x, y): - if chess.square_file(x) > 3: +def norm_kkindex(x: chess.Square, y: chess.Square) -> Tuple[int, int]: + if chess.square_file(x) > chess.FILE_D: x = flip_we(x) y = flip_we(y) - if chess.square_rank(x) > 3: + if chess.square_rank(x) > chess.RANK_4: x = flip_ns(x) y = flip_ns(y) rowx = chess.square_rank(x) colx = chess.square_file(x) - if (rowx > colx): + if rowx > colx: x = flip_nw_se(x) y = flip_nw_se(y) @@ -393,8 +375,8 @@ def norm_kkindex(x, y): return x, y -def init_kkidx(): - kkidx = [[-1] * 64 for x in range(64)] +def init_kkidx() -> Tuple[List[List[int]], List[int], List[int]]: + kkidx = [[-1] * 64 for _ in range(64)] bksq = [-1] * MAX_KKINDEX wksq = [-1] * MAX_KKINDEX idx = 0 @@ -417,7 +399,7 @@ def init_kkidx(): KKIDX, WKSQ, BKSQ = init_kkidx() -def kxk_pctoindex(c): +def kxk_pctoindex(c: Request) -> int: BLOCK_Ax = 64 ft = flip_type(c.black_piece_squares[0], c.white_piece_squares[0]) @@ -444,7 +426,7 @@ def kxk_pctoindex(c): return ki * BLOCK_Ax + ws[1] -def kapkb_pctoindex(c): +def kapkb_pctoindex(c: Request) -> int: BLOCK_A = 64 * 64 * 64 * 64 BLOCK_B = 64 * 64 * 64 BLOCK_C = 64 * 64 @@ -460,7 +442,7 @@ def kapkb_pctoindex(c): return NOINDEX if (pawn & 7) > 3: - # Column is more than 3, i.e. e, f, g or h. + # Column is more than 3, i.e., e, f, g or h. pawn = flip_we(pawn) wk = flip_we(wk) bk = flip_we(bk) @@ -469,12 +451,12 @@ def kapkb_pctoindex(c): sq = pawn sq ^= 56 # flip_ns - sq -= 8 # down one row + sq -= 8 # Down one row pslice = (sq + (sq & 3)) >> 1 return pslice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + wa * BLOCK_D + ba -def kabpk_pctoindex(c): +def kabpk_pctoindex(c: Request) -> int: BLOCK_A = 64 * 64 * 64 * 64 BLOCK_B = 64 * 64 * 64 BLOCK_C = 64 * 64 @@ -487,7 +469,7 @@ def kabpk_pctoindex(c): bk = c.black_piece_squares[0] if (pawn & 7) > 3: - # Column is more than 3, i.e. e, f, g or h. + # Column is more than 3, i.e., e, f, g or h. pawn = flip_we(pawn) wk = flip_we(wk) bk = flip_we(bk) @@ -498,7 +480,7 @@ def kabpk_pctoindex(c): return pslice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + wa * BLOCK_D + wb -def kabkp_pctoindex(c): +def kabkp_pctoindex(c: Request) -> int: BLOCK_A = 64 * 64 * 64 * 64 BLOCK_B = 64 * 64 * 64 BLOCK_C = 64 * 64 @@ -514,7 +496,7 @@ def kabkp_pctoindex(c): return NOINDEX if (pawn & 7) > 3: - # Column is more than 3, i.e. e, f, g or h. + # Column is more than 3, i.e., e, f, g or h. pawn = flip_we(pawn) wk = flip_we(wk) bk = flip_we(bk) @@ -522,12 +504,12 @@ def kabkp_pctoindex(c): wb = flip_we(wb) sq = pawn - sq -= 8 # down one row + sq -= 8 # Down one row pslice = (sq + (sq & 3)) >> 1 return pslice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + wa * BLOCK_D + wb -def kaapk_pctoindex(c): +def kaapk_pctoindex(c: Request) -> int: BLOCK_C = MAX_AAINDEX BLOCK_B = 64 * BLOCK_C BLOCK_A = 64 * BLOCK_B @@ -539,7 +521,7 @@ def kaapk_pctoindex(c): bk = c.black_piece_squares[0] if (pawn & 7) > 3: - # Column is more than 3, i.e. e, f, g or h. + # Column is more than 3, i.e., e, f, g or h. pawn = flip_we(pawn) wk = flip_we(wk) bk = flip_we(bk) @@ -555,7 +537,7 @@ def kaapk_pctoindex(c): return pslice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + aa_combo -def kaakp_pctoindex(c): +def kaakp_pctoindex(c: Request) -> int: BLOCK_C = MAX_AAINDEX BLOCK_B = 64 * BLOCK_C BLOCK_A = 64 * BLOCK_B @@ -567,7 +549,7 @@ def kaakp_pctoindex(c): pawn = c.black_piece_squares[1] if (pawn & 7) > 3: - # Column is more than 3, i.e. e, f, g or h. + # Column is more than 3, i.e., e, f, g or h. pawn = flip_we(pawn) wk = flip_we(wk) bk = flip_we(bk) @@ -584,7 +566,7 @@ def kaakp_pctoindex(c): return pslice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + aa_combo -def kapkp_pctoindex(c): +def kapkp_pctoindex(c: Request) -> int: BLOCK_A = 64 * 64 * 64 BLOCK_B = 64 * 64 BLOCK_C = 64 @@ -599,7 +581,7 @@ def kapkp_pctoindex(c): loosen = pawn_b if (anchor & 7) > 3: - # Column is more than 3, i.e. e, f, g or h. + # Column is more than 3, i.e., e, f, g or h. anchor = flip_we(anchor) loosen = flip_we(loosen) wk = flip_we(wk) @@ -615,7 +597,7 @@ def kapkp_pctoindex(c): return pp_slice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + wa -def kappk_pctoindex(c): +def kappk_pctoindex(c: Request) -> int: BLOCK_A = 64 * 64 * 64 BLOCK_B = 64 * 64 BLOCK_C = 64 @@ -629,7 +611,7 @@ def kappk_pctoindex(c): anchor, loosen = pp_putanchorfirst(pawn_a, pawn_b) if (anchor & 7) > 3: - # Column is more than 3, i.e. e, f, g or h. + # Column is more than 3, i.e., e, f, g or h. anchor = flip_we(anchor) loosen = flip_we(loosen) wk = flip_we(wk) @@ -646,7 +628,7 @@ def kappk_pctoindex(c): return pp_slice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + wa -def kppka_pctoindex(c): +def kppka_pctoindex(c: Request) -> int: BLOCK_A = 64 * 64 * 64 BLOCK_B = 64 * 64 BLOCK_C = 64 @@ -676,7 +658,7 @@ def kppka_pctoindex(c): return pp_slice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + ba -def kabck_pctoindex(c): +def kabck_pctoindex(c: Request) -> int: N_WHITE = 4 N_BLACK = 1 BLOCK_A = 64 * 64 * 64 @@ -707,7 +689,7 @@ def kabck_pctoindex(c): return ki * BLOCK_A + ws[1] * BLOCK_B + ws[2] * BLOCK_C + ws[3] -def kabbk_pctoindex(c): +def kabbk_pctoindex(c: Request) -> int: N_WHITE = 4 N_BLACK = 1 BLOCK_Bx = 64 @@ -738,7 +720,7 @@ def kabbk_pctoindex(c): return ki * BLOCK_Ax + ai * BLOCK_Bx + ws[1] -def kaabk_pctoindex(c): +def kaabk_pctoindex(c: Request) -> int: N_WHITE = 4 N_BLACK = 1 BLOCK_Bx = 64 @@ -749,15 +731,15 @@ def kaabk_pctoindex(c): ws = c.white_piece_squares[:N_WHITE] bs = c.black_piece_squares[:N_BLACK] - if ((ft & WE_FLAG) != 0): + if (ft & WE_FLAG) != 0: ws = [flip_we(i) for i in ws] bs = [flip_we(i) for i in bs] - if ((ft & NS_FLAG) != 0): + if (ft & NS_FLAG) != 0: ws = [flip_ns(i) for i in ws] bs = [flip_ns(i) for i in bs] - if ((ft & NW_SE_FLAG) != 0): + if (ft & NW_SE_FLAG) != 0: ws = [flip_nw_se(i) for i in ws] bs = [flip_nw_se(i) for i in bs] @@ -769,12 +751,12 @@ def kaabk_pctoindex(c): return ki * BLOCK_Ax + ai * BLOCK_Bx + ws[3] -def aaa_getsubi(x, y, z): +def aaa_getsubi(x: int, y: int, z: int) -> int: bse = AAA_BASE[z] calc_idx = x + (y - 1) * y // 2 + bse return calc_idx -def kaaak_pctoindex(c): +def kaaak_pctoindex(c: Request) -> int: N_WHITE = 4 N_BLACK = 1 BLOCK_Ax = MAX_AAAINDEX @@ -821,7 +803,7 @@ def kaaak_pctoindex(c): return ki * BLOCK_Ax + ai -def kppkp_pctoindex(c): +def kppkp_pctoindex(c: Request) -> int: BLOCK_Ax = MAX_PP48_INDEX * 64 * 64 BLOCK_Bx = 64 * 64 BLOCK_Cx = 64 @@ -842,7 +824,7 @@ def kppkp_pctoindex(c): i = flip_we(flip_ns(pawn_a)) - 8 j = flip_we(flip_ns(pawn_b)) - 8 - # Black pawn, so low indexes mean more advanced. + # Black pawn, so low indexes are more advanced. k = map24_b(pawn_c) pp48_slice = PP48_IDX[i][j] @@ -852,7 +834,7 @@ def kppkp_pctoindex(c): return k * BLOCK_Ax + pp48_slice * BLOCK_Bx + wk * BLOCK_Cx + bk -def kaakb_pctoindex(c): +def kaakb_pctoindex(c: Request) -> int: N_WHITE = 3 N_BLACK = 2 BLOCK_Bx = 64 @@ -883,7 +865,7 @@ def kaakb_pctoindex(c): return ki * BLOCK_Ax + ai * BLOCK_Bx + bs[1] -def kabkc_pctoindex(c): +def kabkc_pctoindex(c: Request) -> int: N_WHITE = 3 N_BLACK = 2 @@ -915,7 +897,7 @@ def kabkc_pctoindex(c): return ki * BLOCK_Ax + ws[1] * BLOCK_Bx + ws[2] * BLOCK_Cx + bs[1] -def kpkp_pctoindex(c): +def kpkp_pctoindex(c: Request) -> int: BLOCK_Ax = 64 * 64 BLOCK_Bx = 64 @@ -943,7 +925,7 @@ def kpkp_pctoindex(c): return pp_slice * BLOCK_Ax + wk * BLOCK_Bx + bk -def kppk_pctoindex(c): +def kppk_pctoindex(c: Request) -> int: BLOCK_Ax = 64 * 64 BLOCK_Bx = 64 wk = c.white_piece_squares[0] @@ -969,7 +951,7 @@ def kppk_pctoindex(c): return pp_slice * BLOCK_Ax + wk * BLOCK_Bx + bk -def kapk_pctoindex(c): +def kapk_pctoindex(c: Request) -> int: BLOCK_Ax = 64 * 64 * 64 BLOCK_Bx = 64 * 64 BLOCK_Cx = 64 @@ -990,12 +972,12 @@ def kapk_pctoindex(c): sq = pawn sq ^= 56 # flip_ns - sq -= 8 # down one row + sq -= 8 # Down one row pslice = ((sq + (sq & 3)) >> 1) return pslice * BLOCK_Ax + wk * BLOCK_Bx + bk * BLOCK_Cx + wa -def kabk_pctoindex(c): +def kabk_pctoindex(c: Request) -> int: BLOCK_Ax = 64 * 64 BLOCK_Bx = 64 @@ -1023,7 +1005,7 @@ def kabk_pctoindex(c): return ki * BLOCK_Ax + ws[1] * BLOCK_Bx + ws[2] -def kakp_pctoindex(c): +def kakp_pctoindex(c: Request) -> int: BLOCK_Ax = 64 * 64 * 64 BLOCK_Bx = 64 * 64 BLOCK_Cx = 64 @@ -1043,12 +1025,12 @@ def kakp_pctoindex(c): wa = flip_we(wa) sq = pawn - sq -= 8 # down one row + sq -= 8 # Down one row pslice = (sq + (sq & 3)) >> 1 return pslice * BLOCK_Ax + wk * BLOCK_Bx + bk * BLOCK_Cx + wa -def kaak_pctoindex(c): +def kaak_pctoindex(c: Request) -> int: N_WHITE = 3 N_BLACK = 1 BLOCK_Ax = MAX_AAINDEX @@ -1078,7 +1060,7 @@ def kaak_pctoindex(c): return ki * BLOCK_Ax + ai -def kakb_pctoindex(c): +def kakb_pctoindex(c: Request) -> int: BLOCK_Ax = 64 * 64 BLOCK_Bx = 64 @@ -1112,7 +1094,7 @@ def kakb_pctoindex(c): return ki * BLOCK_Ax + ws[1] * BLOCK_Bx + bs[1] -def kpk_pctoindex(c): +def kpk_pctoindex(c: Request) -> int: BLOCK_A = 64 * 64 BLOCK_B = 64 @@ -1130,13 +1112,13 @@ def kpk_pctoindex(c): sq = pawn sq ^= 56 # flip_ns - sq -= 8 # down one row + sq -= 8 # Down one row pslice = ((sq + (sq & 3)) >> 1) res = pslice * BLOCK_A + wk * BLOCK_B + bk return res -def kpppk_pctoindex(c): +def kpppk_pctoindex(c: Request) -> int: BLOCK_A = 64 * 64 BLOCK_B = 64 @@ -1172,189 +1154,190 @@ def kpppk_pctoindex(c): return ppp48_slice * BLOCK_A + wk * BLOCK_B + bk -Endgamekey = collections.namedtuple("Endgamekey", ["maxindex", "slice_n", "pctoi"]) +class EndgameKey: + def __init__(self, maxindex: int, slice_n: int, pctoi: Callable[[Request], int]): + self.maxindex = maxindex + self.slice_n = slice_n + self.pctoi = pctoi EGKEY = { - "kqk": Endgamekey(MAX_KXK, 1, kxk_pctoindex), - "krk": Endgamekey(MAX_KXK, 1, kxk_pctoindex), - "kbk": Endgamekey(MAX_KXK, 1, kxk_pctoindex), - "knk": Endgamekey(MAX_KXK, 1, kxk_pctoindex), - "kpk": Endgamekey(MAX_kpk, 24, kpk_pctoindex), - - "kqkq": Endgamekey(MAX_kakb, 1, kakb_pctoindex), - "kqkr": Endgamekey(MAX_kakb, 1, kakb_pctoindex), - "kqkb": Endgamekey(MAX_kakb, 1, kakb_pctoindex), - "kqkn": Endgamekey(MAX_kakb, 1, kakb_pctoindex), - - "krkr": Endgamekey(MAX_kakb, 1, kakb_pctoindex), - "krkb": Endgamekey(MAX_kakb, 1, kakb_pctoindex), - "krkn": Endgamekey(MAX_kakb, 1, kakb_pctoindex), - - "kbkb": Endgamekey(MAX_kakb, 1, kakb_pctoindex), - "kbkn": Endgamekey(MAX_kakb, 1, kakb_pctoindex), - - "knkn": Endgamekey(MAX_kakb, 1, kakb_pctoindex), - - "kqqk": Endgamekey(MAX_kaak, 1, kaak_pctoindex), - "kqrk": Endgamekey(MAX_kabk, 1, kabk_pctoindex), - "kqbk": Endgamekey(MAX_kabk, 1, kabk_pctoindex), - "kqnk": Endgamekey(MAX_kabk, 1, kabk_pctoindex), - - "krrk": Endgamekey(MAX_kaak, 1, kaak_pctoindex), - "krbk": Endgamekey(MAX_kabk, 1, kabk_pctoindex), - "krnk": Endgamekey(MAX_kabk, 1, kabk_pctoindex), - - "kbbk": Endgamekey(MAX_kaak, 1, kaak_pctoindex), - "kbnk": Endgamekey(MAX_kabk, 1, kabk_pctoindex), - - "knnk": Endgamekey(MAX_kaak, 1, kaak_pctoindex), - "kqkp": Endgamekey(MAX_kakp, 24, kakp_pctoindex), - "krkp": Endgamekey(MAX_kakp, 24, kakp_pctoindex), - "kbkp": Endgamekey(MAX_kakp, 24, kakp_pctoindex), - "knkp": Endgamekey(MAX_kakp, 24, kakp_pctoindex), - - "kqpk": Endgamekey(MAX_kapk, 24, kapk_pctoindex), - "krpk": Endgamekey(MAX_kapk, 24, kapk_pctoindex), - "kbpk": Endgamekey(MAX_kapk, 24, kapk_pctoindex), - "knpk": Endgamekey(MAX_kapk, 24, kapk_pctoindex), - - "kppk": Endgamekey(MAX_kppk, MAX_PPINDEX, kppk_pctoindex), - - "kpkp": Endgamekey(MAX_kpkp, MAX_PpINDEX, kpkp_pctoindex), - - "kppkp": Endgamekey(MAX_kppkp, 24 * MAX_PP48_INDEX, kppkp_pctoindex), - - "kbbkr": Endgamekey(MAX_kaakb, 1, kaakb_pctoindex), - "kbbkb": Endgamekey(MAX_kaakb, 1, kaakb_pctoindex), - "knnkb": Endgamekey(MAX_kaakb, 1, kaakb_pctoindex), - "knnkn": Endgamekey(MAX_kaakb, 1, kaakb_pctoindex), - - "kqqqk": Endgamekey(MAX_kaaak, 1, kaaak_pctoindex), - "kqqrk": Endgamekey(MAX_kaabk, 1, kaabk_pctoindex), - "kqqbk": Endgamekey(MAX_kaabk, 1, kaabk_pctoindex), - "kqqnk": Endgamekey(MAX_kaabk, 1, kaabk_pctoindex), - "kqrrk": Endgamekey(MAX_kabbk, 1, kabbk_pctoindex), - "kqrbk": Endgamekey(MAX_kabck, 1, kabck_pctoindex), - "kqrnk": Endgamekey(MAX_kabck, 1, kabck_pctoindex), - "kqbbk": Endgamekey(MAX_kabbk, 1, kabbk_pctoindex), - "kqbnk": Endgamekey(MAX_kabck, 1, kabck_pctoindex), - "kqnnk": Endgamekey(MAX_kabbk, 1, kabbk_pctoindex), - "krrrk": Endgamekey(MAX_kaaak, 1, kaaak_pctoindex), - "krrbk": Endgamekey(MAX_kaabk, 1, kaabk_pctoindex), - "krrnk": Endgamekey(MAX_kaabk, 1, kaabk_pctoindex), - "krbbk": Endgamekey(MAX_kabbk, 1, kabbk_pctoindex), - "krbnk": Endgamekey(MAX_kabck, 1, kabck_pctoindex), - "krnnk": Endgamekey(MAX_kabbk, 1, kabbk_pctoindex), - "kbbbk": Endgamekey(MAX_kaaak, 1, kaaak_pctoindex), - "kbbnk": Endgamekey(MAX_kaabk, 1, kaabk_pctoindex), - "kbnnk": Endgamekey(MAX_kabbk, 1, kabbk_pctoindex), - "knnnk": Endgamekey(MAX_kaaak, 1, kaaak_pctoindex), - "kqqkq": Endgamekey(MAX_kaakb, 1, kaakb_pctoindex), - "kqqkr": Endgamekey(MAX_kaakb, 1, kaakb_pctoindex), - "kqqkb": Endgamekey(MAX_kaakb, 1, kaakb_pctoindex), - "kqqkn": Endgamekey(MAX_kaakb, 1, kaakb_pctoindex), - "kqrkq": Endgamekey(MAX_kabkc, 1, kabkc_pctoindex), - "kqrkr": Endgamekey(MAX_kabkc, 1, kabkc_pctoindex), - "kqrkb": Endgamekey(MAX_kabkc, 1, kabkc_pctoindex), - "kqrkn": Endgamekey(MAX_kabkc, 1, kabkc_pctoindex), - "kqbkq": Endgamekey(MAX_kabkc, 1, kabkc_pctoindex), - "kqbkr": Endgamekey(MAX_kabkc, 1, kabkc_pctoindex), - "kqbkb": Endgamekey(MAX_kabkc, 1, kabkc_pctoindex), - "kqbkn": Endgamekey(MAX_kabkc, 1, kabkc_pctoindex), - "kqnkq": Endgamekey(MAX_kabkc, 1, kabkc_pctoindex), - "kqnkr": Endgamekey(MAX_kabkc, 1, kabkc_pctoindex), - "kqnkb": Endgamekey(MAX_kabkc, 1, kabkc_pctoindex), - "kqnkn": Endgamekey(MAX_kabkc, 1, kabkc_pctoindex), - "krrkq": Endgamekey(MAX_kaakb, 1, kaakb_pctoindex), - "krrkr": Endgamekey(MAX_kaakb, 1, kaakb_pctoindex), - "krrkb": Endgamekey(MAX_kaakb, 1, kaakb_pctoindex), - "krrkn": Endgamekey(MAX_kaakb, 1, kaakb_pctoindex), - "krbkq": Endgamekey(MAX_kabkc, 1, kabkc_pctoindex), - "krbkr": Endgamekey(MAX_kabkc, 1, kabkc_pctoindex), - "krbkb": Endgamekey(MAX_kabkc, 1, kabkc_pctoindex), - "krbkn": Endgamekey(MAX_kabkc, 1, kabkc_pctoindex), - "krnkq": Endgamekey(MAX_kabkc, 1, kabkc_pctoindex), - "krnkr": Endgamekey(MAX_kabkc, 1, kabkc_pctoindex), - "krnkb": Endgamekey(MAX_kabkc, 1, kabkc_pctoindex), - "krnkn": Endgamekey(MAX_kabkc, 1, kabkc_pctoindex), - "kbbkq": Endgamekey(MAX_kaakb, 1, kaakb_pctoindex), - "kbbkn": Endgamekey(MAX_kaakb, 1, kaakb_pctoindex), - "kbnkq": Endgamekey(MAX_kabkc, 1, kabkc_pctoindex), - "kbnkr": Endgamekey(MAX_kabkc, 1, kabkc_pctoindex), - "kbnkb": Endgamekey(MAX_kabkc, 1, kabkc_pctoindex), - "kbnkn": Endgamekey(MAX_kabkc, 1, kabkc_pctoindex), - "knnkq": Endgamekey(MAX_kaakb, 1, kaakb_pctoindex), - "knnkr": Endgamekey(MAX_kaakb, 1, kaakb_pctoindex), - - "kqqpk": Endgamekey(MAX_kaapk, 24, kaapk_pctoindex), - "kqrpk": Endgamekey(MAX_kabpk, 24, kabpk_pctoindex), - "kqbpk": Endgamekey(MAX_kabpk, 24, kabpk_pctoindex), - "kqnpk": Endgamekey(MAX_kabpk, 24, kabpk_pctoindex), - "krrpk": Endgamekey(MAX_kaapk, 24, kaapk_pctoindex), - "krbpk": Endgamekey(MAX_kabpk, 24, kabpk_pctoindex), - "krnpk": Endgamekey(MAX_kabpk, 24, kabpk_pctoindex), - "kbbpk": Endgamekey(MAX_kaapk, 24, kaapk_pctoindex), - "kbnpk": Endgamekey(MAX_kabpk, 24, kabpk_pctoindex), - "knnpk": Endgamekey(MAX_kaapk, 24, kaapk_pctoindex), - - "kqppk": Endgamekey(MAX_kappk, MAX_PPINDEX, kappk_pctoindex), - "krppk": Endgamekey(MAX_kappk, MAX_PPINDEX, kappk_pctoindex), - "kbppk": Endgamekey(MAX_kappk, MAX_PPINDEX, kappk_pctoindex), - "knppk": Endgamekey(MAX_kappk, MAX_PPINDEX, kappk_pctoindex), - - "kqpkq": Endgamekey(MAX_kapkb, 24, kapkb_pctoindex), - "kqpkr": Endgamekey(MAX_kapkb, 24, kapkb_pctoindex), - "kqpkb": Endgamekey(MAX_kapkb, 24, kapkb_pctoindex), - "kqpkn": Endgamekey(MAX_kapkb, 24, kapkb_pctoindex), - "krpkq": Endgamekey(MAX_kapkb, 24, kapkb_pctoindex), - "krpkr": Endgamekey(MAX_kapkb, 24, kapkb_pctoindex), - "krpkb": Endgamekey(MAX_kapkb, 24, kapkb_pctoindex), - "krpkn": Endgamekey(MAX_kapkb, 24, kapkb_pctoindex), - "kbpkq": Endgamekey(MAX_kapkb, 24, kapkb_pctoindex), - "kbpkr": Endgamekey(MAX_kapkb, 24, kapkb_pctoindex), - "kbpkb": Endgamekey(MAX_kapkb, 24, kapkb_pctoindex), - "kbpkn": Endgamekey(MAX_kapkb, 24, kapkb_pctoindex), - "knpkq": Endgamekey(MAX_kapkb, 24, kapkb_pctoindex), - "knpkr": Endgamekey(MAX_kapkb, 24, kapkb_pctoindex), - "knpkb": Endgamekey(MAX_kapkb, 24, kapkb_pctoindex), - "knpkn": Endgamekey(MAX_kapkb, 24, kapkb_pctoindex), - "kppkq": Endgamekey(MAX_kppka, MAX_PPINDEX, kppka_pctoindex), - "kppkr": Endgamekey(MAX_kppka, MAX_PPINDEX, kppka_pctoindex), - "kppkb": Endgamekey(MAX_kppka, MAX_PPINDEX, kppka_pctoindex), - "kppkn": Endgamekey(MAX_kppka, MAX_PPINDEX, kppka_pctoindex), - - "kqqkp": Endgamekey(MAX_kaakp, 24, kaakp_pctoindex), - "kqrkp": Endgamekey(MAX_kabkp, 24, kabkp_pctoindex), - "kqbkp": Endgamekey(MAX_kabkp, 24, kabkp_pctoindex), - "kqnkp": Endgamekey(MAX_kabkp, 24, kabkp_pctoindex), - "krrkp": Endgamekey(MAX_kaakp, 24, kaakp_pctoindex), - "krbkp": Endgamekey(MAX_kabkp, 24, kabkp_pctoindex), - "krnkp": Endgamekey(MAX_kabkp, 24, kabkp_pctoindex), - "kbbkp": Endgamekey(MAX_kaakp, 24, kaakp_pctoindex), - "kbnkp": Endgamekey(MAX_kabkp, 24, kabkp_pctoindex), - "knnkp": Endgamekey(MAX_kaakp, 24, kaakp_pctoindex), - - "kqpkp": Endgamekey(MAX_kapkp, MAX_PpINDEX, kapkp_pctoindex), - "krpkp": Endgamekey(MAX_kapkp, MAX_PpINDEX, kapkp_pctoindex), - "kbpkp": Endgamekey(MAX_kapkp, MAX_PpINDEX, kapkp_pctoindex), - "knpkp": Endgamekey(MAX_kapkp, MAX_PpINDEX, kapkp_pctoindex), - - "kpppk": Endgamekey(MAX_kpppk, MAX_PPP48_INDEX, kpppk_pctoindex), + "kqk": EndgameKey(MAX_KXK, 1, kxk_pctoindex), + "krk": EndgameKey(MAX_KXK, 1, kxk_pctoindex), + "kbk": EndgameKey(MAX_KXK, 1, kxk_pctoindex), + "knk": EndgameKey(MAX_KXK, 1, kxk_pctoindex), + "kpk": EndgameKey(MAX_kpk, 24, kpk_pctoindex), + + "kqkq": EndgameKey(MAX_kakb, 1, kakb_pctoindex), + "kqkr": EndgameKey(MAX_kakb, 1, kakb_pctoindex), + "kqkb": EndgameKey(MAX_kakb, 1, kakb_pctoindex), + "kqkn": EndgameKey(MAX_kakb, 1, kakb_pctoindex), + + "krkr": EndgameKey(MAX_kakb, 1, kakb_pctoindex), + "krkb": EndgameKey(MAX_kakb, 1, kakb_pctoindex), + "krkn": EndgameKey(MAX_kakb, 1, kakb_pctoindex), + + "kbkb": EndgameKey(MAX_kakb, 1, kakb_pctoindex), + "kbkn": EndgameKey(MAX_kakb, 1, kakb_pctoindex), + + "knkn": EndgameKey(MAX_kakb, 1, kakb_pctoindex), + + "kqqk": EndgameKey(MAX_kaak, 1, kaak_pctoindex), + "kqrk": EndgameKey(MAX_kabk, 1, kabk_pctoindex), + "kqbk": EndgameKey(MAX_kabk, 1, kabk_pctoindex), + "kqnk": EndgameKey(MAX_kabk, 1, kabk_pctoindex), + + "krrk": EndgameKey(MAX_kaak, 1, kaak_pctoindex), + "krbk": EndgameKey(MAX_kabk, 1, kabk_pctoindex), + "krnk": EndgameKey(MAX_kabk, 1, kabk_pctoindex), + + "kbbk": EndgameKey(MAX_kaak, 1, kaak_pctoindex), + "kbnk": EndgameKey(MAX_kabk, 1, kabk_pctoindex), + + "knnk": EndgameKey(MAX_kaak, 1, kaak_pctoindex), + "kqkp": EndgameKey(MAX_kakp, 24, kakp_pctoindex), + "krkp": EndgameKey(MAX_kakp, 24, kakp_pctoindex), + "kbkp": EndgameKey(MAX_kakp, 24, kakp_pctoindex), + "knkp": EndgameKey(MAX_kakp, 24, kakp_pctoindex), + + "kqpk": EndgameKey(MAX_kapk, 24, kapk_pctoindex), + "krpk": EndgameKey(MAX_kapk, 24, kapk_pctoindex), + "kbpk": EndgameKey(MAX_kapk, 24, kapk_pctoindex), + "knpk": EndgameKey(MAX_kapk, 24, kapk_pctoindex), + + "kppk": EndgameKey(MAX_kppk, MAX_PPINDEX, kppk_pctoindex), + + "kpkp": EndgameKey(MAX_kpkp, MAX_PpINDEX, kpkp_pctoindex), + + "kppkp": EndgameKey(MAX_kppkp, 24 * MAX_PP48_INDEX, kppkp_pctoindex), + + "kbbkr": EndgameKey(MAX_kaakb, 1, kaakb_pctoindex), + "kbbkb": EndgameKey(MAX_kaakb, 1, kaakb_pctoindex), + "knnkb": EndgameKey(MAX_kaakb, 1, kaakb_pctoindex), + "knnkn": EndgameKey(MAX_kaakb, 1, kaakb_pctoindex), + + "kqqqk": EndgameKey(MAX_kaaak, 1, kaaak_pctoindex), + "kqqrk": EndgameKey(MAX_kaabk, 1, kaabk_pctoindex), + "kqqbk": EndgameKey(MAX_kaabk, 1, kaabk_pctoindex), + "kqqnk": EndgameKey(MAX_kaabk, 1, kaabk_pctoindex), + "kqrrk": EndgameKey(MAX_kabbk, 1, kabbk_pctoindex), + "kqrbk": EndgameKey(MAX_kabck, 1, kabck_pctoindex), + "kqrnk": EndgameKey(MAX_kabck, 1, kabck_pctoindex), + "kqbbk": EndgameKey(MAX_kabbk, 1, kabbk_pctoindex), + "kqbnk": EndgameKey(MAX_kabck, 1, kabck_pctoindex), + "kqnnk": EndgameKey(MAX_kabbk, 1, kabbk_pctoindex), + "krrrk": EndgameKey(MAX_kaaak, 1, kaaak_pctoindex), + "krrbk": EndgameKey(MAX_kaabk, 1, kaabk_pctoindex), + "krrnk": EndgameKey(MAX_kaabk, 1, kaabk_pctoindex), + "krbbk": EndgameKey(MAX_kabbk, 1, kabbk_pctoindex), + "krbnk": EndgameKey(MAX_kabck, 1, kabck_pctoindex), + "krnnk": EndgameKey(MAX_kabbk, 1, kabbk_pctoindex), + "kbbbk": EndgameKey(MAX_kaaak, 1, kaaak_pctoindex), + "kbbnk": EndgameKey(MAX_kaabk, 1, kaabk_pctoindex), + "kbnnk": EndgameKey(MAX_kabbk, 1, kabbk_pctoindex), + "knnnk": EndgameKey(MAX_kaaak, 1, kaaak_pctoindex), + "kqqkq": EndgameKey(MAX_kaakb, 1, kaakb_pctoindex), + "kqqkr": EndgameKey(MAX_kaakb, 1, kaakb_pctoindex), + "kqqkb": EndgameKey(MAX_kaakb, 1, kaakb_pctoindex), + "kqqkn": EndgameKey(MAX_kaakb, 1, kaakb_pctoindex), + "kqrkq": EndgameKey(MAX_kabkc, 1, kabkc_pctoindex), + "kqrkr": EndgameKey(MAX_kabkc, 1, kabkc_pctoindex), + "kqrkb": EndgameKey(MAX_kabkc, 1, kabkc_pctoindex), + "kqrkn": EndgameKey(MAX_kabkc, 1, kabkc_pctoindex), + "kqbkq": EndgameKey(MAX_kabkc, 1, kabkc_pctoindex), + "kqbkr": EndgameKey(MAX_kabkc, 1, kabkc_pctoindex), + "kqbkb": EndgameKey(MAX_kabkc, 1, kabkc_pctoindex), + "kqbkn": EndgameKey(MAX_kabkc, 1, kabkc_pctoindex), + "kqnkq": EndgameKey(MAX_kabkc, 1, kabkc_pctoindex), + "kqnkr": EndgameKey(MAX_kabkc, 1, kabkc_pctoindex), + "kqnkb": EndgameKey(MAX_kabkc, 1, kabkc_pctoindex), + "kqnkn": EndgameKey(MAX_kabkc, 1, kabkc_pctoindex), + "krrkq": EndgameKey(MAX_kaakb, 1, kaakb_pctoindex), + "krrkr": EndgameKey(MAX_kaakb, 1, kaakb_pctoindex), + "krrkb": EndgameKey(MAX_kaakb, 1, kaakb_pctoindex), + "krrkn": EndgameKey(MAX_kaakb, 1, kaakb_pctoindex), + "krbkq": EndgameKey(MAX_kabkc, 1, kabkc_pctoindex), + "krbkr": EndgameKey(MAX_kabkc, 1, kabkc_pctoindex), + "krbkb": EndgameKey(MAX_kabkc, 1, kabkc_pctoindex), + "krbkn": EndgameKey(MAX_kabkc, 1, kabkc_pctoindex), + "krnkq": EndgameKey(MAX_kabkc, 1, kabkc_pctoindex), + "krnkr": EndgameKey(MAX_kabkc, 1, kabkc_pctoindex), + "krnkb": EndgameKey(MAX_kabkc, 1, kabkc_pctoindex), + "krnkn": EndgameKey(MAX_kabkc, 1, kabkc_pctoindex), + "kbbkq": EndgameKey(MAX_kaakb, 1, kaakb_pctoindex), + "kbbkn": EndgameKey(MAX_kaakb, 1, kaakb_pctoindex), + "kbnkq": EndgameKey(MAX_kabkc, 1, kabkc_pctoindex), + "kbnkr": EndgameKey(MAX_kabkc, 1, kabkc_pctoindex), + "kbnkb": EndgameKey(MAX_kabkc, 1, kabkc_pctoindex), + "kbnkn": EndgameKey(MAX_kabkc, 1, kabkc_pctoindex), + "knnkq": EndgameKey(MAX_kaakb, 1, kaakb_pctoindex), + "knnkr": EndgameKey(MAX_kaakb, 1, kaakb_pctoindex), + + "kqqpk": EndgameKey(MAX_kaapk, 24, kaapk_pctoindex), + "kqrpk": EndgameKey(MAX_kabpk, 24, kabpk_pctoindex), + "kqbpk": EndgameKey(MAX_kabpk, 24, kabpk_pctoindex), + "kqnpk": EndgameKey(MAX_kabpk, 24, kabpk_pctoindex), + "krrpk": EndgameKey(MAX_kaapk, 24, kaapk_pctoindex), + "krbpk": EndgameKey(MAX_kabpk, 24, kabpk_pctoindex), + "krnpk": EndgameKey(MAX_kabpk, 24, kabpk_pctoindex), + "kbbpk": EndgameKey(MAX_kaapk, 24, kaapk_pctoindex), + "kbnpk": EndgameKey(MAX_kabpk, 24, kabpk_pctoindex), + "knnpk": EndgameKey(MAX_kaapk, 24, kaapk_pctoindex), + + "kqppk": EndgameKey(MAX_kappk, MAX_PPINDEX, kappk_pctoindex), + "krppk": EndgameKey(MAX_kappk, MAX_PPINDEX, kappk_pctoindex), + "kbppk": EndgameKey(MAX_kappk, MAX_PPINDEX, kappk_pctoindex), + "knppk": EndgameKey(MAX_kappk, MAX_PPINDEX, kappk_pctoindex), + + "kqpkq": EndgameKey(MAX_kapkb, 24, kapkb_pctoindex), + "kqpkr": EndgameKey(MAX_kapkb, 24, kapkb_pctoindex), + "kqpkb": EndgameKey(MAX_kapkb, 24, kapkb_pctoindex), + "kqpkn": EndgameKey(MAX_kapkb, 24, kapkb_pctoindex), + "krpkq": EndgameKey(MAX_kapkb, 24, kapkb_pctoindex), + "krpkr": EndgameKey(MAX_kapkb, 24, kapkb_pctoindex), + "krpkb": EndgameKey(MAX_kapkb, 24, kapkb_pctoindex), + "krpkn": EndgameKey(MAX_kapkb, 24, kapkb_pctoindex), + "kbpkq": EndgameKey(MAX_kapkb, 24, kapkb_pctoindex), + "kbpkr": EndgameKey(MAX_kapkb, 24, kapkb_pctoindex), + "kbpkb": EndgameKey(MAX_kapkb, 24, kapkb_pctoindex), + "kbpkn": EndgameKey(MAX_kapkb, 24, kapkb_pctoindex), + "knpkq": EndgameKey(MAX_kapkb, 24, kapkb_pctoindex), + "knpkr": EndgameKey(MAX_kapkb, 24, kapkb_pctoindex), + "knpkb": EndgameKey(MAX_kapkb, 24, kapkb_pctoindex), + "knpkn": EndgameKey(MAX_kapkb, 24, kapkb_pctoindex), + "kppkq": EndgameKey(MAX_kppka, MAX_PPINDEX, kppka_pctoindex), + "kppkr": EndgameKey(MAX_kppka, MAX_PPINDEX, kppka_pctoindex), + "kppkb": EndgameKey(MAX_kppka, MAX_PPINDEX, kppka_pctoindex), + "kppkn": EndgameKey(MAX_kppka, MAX_PPINDEX, kppka_pctoindex), + + "kqqkp": EndgameKey(MAX_kaakp, 24, kaakp_pctoindex), + "kqrkp": EndgameKey(MAX_kabkp, 24, kabkp_pctoindex), + "kqbkp": EndgameKey(MAX_kabkp, 24, kabkp_pctoindex), + "kqnkp": EndgameKey(MAX_kabkp, 24, kabkp_pctoindex), + "krrkp": EndgameKey(MAX_kaakp, 24, kaakp_pctoindex), + "krbkp": EndgameKey(MAX_kabkp, 24, kabkp_pctoindex), + "krnkp": EndgameKey(MAX_kabkp, 24, kabkp_pctoindex), + "kbbkp": EndgameKey(MAX_kaakp, 24, kaakp_pctoindex), + "kbnkp": EndgameKey(MAX_kabkp, 24, kabkp_pctoindex), + "knnkp": EndgameKey(MAX_kaakp, 24, kaakp_pctoindex), + + "kqpkp": EndgameKey(MAX_kapkp, MAX_PpINDEX, kapkp_pctoindex), + "krpkp": EndgameKey(MAX_kapkp, MAX_PpINDEX, kapkp_pctoindex), + "kbpkp": EndgameKey(MAX_kapkp, MAX_PpINDEX, kapkp_pctoindex), + "knpkp": EndgameKey(MAX_kapkp, MAX_PpINDEX, kapkp_pctoindex), + + "kpppk": EndgameKey(MAX_kpppk, MAX_PPP48_INDEX, kpppk_pctoindex), } -def sortlists(ws, wp): +def sortlists(ws: List[int], wp: List[int]) -> Tuple[List[int], List[int]]: z = sorted(zip(wp, ws), key=lambda x: x[0], reverse=True) wp2, ws2 = zip(*z) return list(ws2), list(wp2) -def egtb_block_unpack(side, n, bp): - try: - return [dtm_unpack(side, i) for i in bp[:n]] - except TypeError: - return [dtm_unpack(side, ord(i)) for i in bp[:n]] +def egtb_block_unpack(side: int, n: int, bp: bytes) -> List[int]: + return [dtm_unpack(side, i) for i in bp[:n]] -def split_index(i): +def split_index(i: int) -> Tuple[int, int]: return divmod(i, ENTRIES_PER_BLOCK) @@ -1373,55 +1356,13 @@ def split_index(i): iBMATEt = tb_BMATE | 4 -def removepiece(ys, yp, j): - del ys[j] - del yp[j] - -def opp(side): +def opp(side: int) -> int: return 1 if side == 0 else 0 -def adjust_up(dist): - udist = dist - sw = udist & INFOMASK - - if sw in [iWMATE, iWMATEt, iBMATE, iBMATEt]: - udist += (1 << PLYSHIFT) - - return udist - -def bestx(side, a, b): - # 0 = selectfirst - # 1 = selectlowest - # 2 = selecthighest - # 3 = selectsecond - comparison = [ - # draw, wmate, bmate, forbid - [0, 3, 0, 0], # draw - [0, 1, 0, 0], # wmate - [3, 3, 2, 0], # bmate - [3, 3, 3, 0], # forbid - ] - - xorkey = [0, 3] - - if (a == iFORBID): - return b - if (b == iFORBID): - return a - - retu = [a, a, b, b] - - if (b < a): - retu[1] = b - retu[2] = a - - key = comparison[a & 3][b & 3] ^ xorkey[side] - return retu[key] - -def unpackdist(d): +def unpackdist(d: int) -> Tuple[int, int]: return d >> PLYSHIFT, d & INFOMASK -def dtm_unpack(stm, packed): +def dtm_unpack(stm: int, packed: int) -> int: p = packed if p in [iDRAW, iFORBID]: @@ -1491,107 +1432,134 @@ class MissingTableError(KeyError): """Can not probe position because a required table is missing.""" -class TableBlock(object): - def __init__(self, egkey, side, offset, age): +class TableBlock: + pcache: List[int] + + def __init__(self, egkey: str, side: int, offset: int, age: int): self.egkey = egkey self.side = side self.offset = offset self.age = age - self.pcache = None -class Request(object): - def __init__(self, white_squares, white_types, black_squares, black_types, side, epsq): +class Request: + egkey: str + white_piece_squares: List[int] + white_piece_types: List[int] + black_piece_squares: List[int] + black_piece_types: List[int] + is_reversed: bool + + def __init__(self, white_squares: List[int], white_types: List[chess.PieceType], black_squares: List[int], black_types: List[chess.PieceType], side: int): self.white_squares, self.white_types = sortlists(white_squares, white_types) self.black_squares, self.black_types = sortlists(black_squares, black_types) self.realside = side self.side = side - self.epsq = epsq - self.egkey = None - self.white_piece_squares = None - self.white_piece_types = None - self.black_piece_squares = None - self.black_piece_types = None - self.is_reversed = None - self.white_piece_squares = None +@dataclasses.dataclass +class ZipInfo: + extraoffset: int + totalblocks: int + blockindex: List[int] -Zipinfo = collections.namedtuple("Zipinfo", ["extraoffset", "totalblocks", "blockindex"]) - -class PythonTablebases(object): +class PythonTablebase: """Provides access to Gaviota tablebases using pure Python code.""" - def __init__(self, lzma): - self.lzma = lzma - - self.available_tables = {} + def __init__(self) -> None: + self.available_tables: Dict[str, str] = {} - self.streams = {} - self.zipinfo = {} + self.streams: Dict[str, BinaryIO] = {} + self.zipinfo: Dict[str, ZipInfo] = {} - self.block_cache = {} + self.block_cache: Dict[Tuple[str, int, int], TableBlock] = {} self.block_age = 0 - def open_directory(self, directory): - """Loads *.gtb.cp4* tables from a directory.""" + def add_directory(self, directory: str) -> None: + """ + Adds *.gtb.cp4* tables from a directory. The relevant files are lazily + opened when the tablebase is actually probed. + """ directory = os.path.abspath(directory) if not os.path.isdir(directory): - raise IOError("not a directory: {0}".format(repr(directory))) + raise IOError(f"not a directory: {directory!r}") for tbfile in fnmatch.filter(os.listdir(directory), "*.gtb.cp4"): self.available_tables[os.path.basename(tbfile).replace(".gtb.cp4", "")] = os.path.join(directory, tbfile) - def probe_dtm(self, board): + def probe_dtm(self, board: chess.Board) -> int: """ Probes for depth to mate information. - The absolute value is the number of half moves until forced mate - (or ``0`` in drawn positions). The value is positive if the - side to move is winning, otherwise it is negative. + The absolute value is the number of half-moves until forced mate + (or ``0`` in drawn or checkmated positions). The value is positive if + the side to move is winning, otherwise it is negative or 0. - In the example position white to move will get mated in 10 half moves: + In the example position, white to move will get mated in 10 half-moves: - >>> with chess.gaviota.open_tablebases("data/gaviota") as tablebases: - ... tablebases.probe_dtm(chess.Board("8/8/8/8/8/8/8/K2kr3 w - - 0 1")) + >>> import chess + >>> import chess.gaviota + >>> + >>> with chess.gaviota.open_tablebase("data/gaviota") as tablebase: + ... board = chess.Board("8/8/8/8/8/8/8/K2kr3 w - - 0 1") + ... print(tablebase.probe_dtm(board)) ... -10 :raises: :exc:`KeyError` (or specifically :exc:`chess.gaviota.MissingTableError`) if the probe fails. Use - :func:`~chess.gaviota.PythonTablebases.get_dtm()` if you prefer + :func:`~chess.gaviota.PythonTablebase.get_dtm()` if you prefer to get ``None`` instead of an exception. + + Note that probing a corrupted table file is undefined behavior. """ # Can not probe positions with castling rights. if board.castling_rights: - raise KeyError("gaviota tables do not contain positions with castling rights: {0}".format(board.fen())) + raise KeyError(f"gaviota tables do not contain positions with castling rights: {board.fen()}") - # Prepare the tablebase request. - white = [(square, board.piece_type_at(square)) for square in chess.SquareSet(board.occupied_co[chess.WHITE])] - black = [(square, board.piece_type_at(square)) for square in chess.SquareSet(board.occupied_co[chess.BLACK])] - white_squares, white_types = zip(*white) - black_squares, black_types = zip(*black) - side = 0 if (board.turn == chess.WHITE) else 1 - epsq = board.ep_square if board.ep_square else NOSQUARE - req = Request(white_squares, white_types, black_squares, black_types, side, epsq) + # Supports only up to 5 pieces. + if board.piece_count() > 5: + raise KeyError(f"gaviota tables support up to 5 pieces, not {board.piece_count()}: {board.fen()}") # KvK is a draw. - if len(white_squares) == 1 and len(black_squares) == 1: + if board.occupied == board.kings: return 0 - # Only up to 5-men tablebases. - if len(white_squares) + len(black_squares) > 5: - raise KeyError("gaviota tables support up to 5 pieces, not {0}: {1}".format(chess.popcount(board.occupied), board.fen())) + # Resolve en passant. + dtm = self._probe_dtm_no_ep(board) + for move in board.generate_legal_ep(): + try: + board.push(move) + + if board.is_checkmate(): + child_dtm = 1 + else: + child_dtm = -self._probe_dtm_no_ep(board) + if child_dtm > 0: + child_dtm += 1 + elif child_dtm < 0: + child_dtm -= 1 + + dtm = min(dtm, child_dtm) if dtm * child_dtm > 0 else max(dtm, child_dtm) + finally: + board.pop() + return dtm + + def _probe_dtm_no_ep(self, board: chess.Board) -> int: + # Prepare the tablebase request. + white_squares = list(chess.SquareSet(board.occupied_co[chess.WHITE])) + white_types = [typing.cast(chess.PieceType, board.piece_type_at(sq)) for sq in white_squares] + black_squares = list(chess.SquareSet(board.occupied_co[chess.BLACK])) + black_types = [typing.cast(chess.PieceType, board.piece_type_at(sq)) for sq in black_squares] + side = 0 if (board.turn == chess.WHITE) else 1 + req = Request(white_squares, white_types, black_squares, black_types, side) # Probe. - dtm = self.egtb_get_dtm(req) + dtm = self._tb_probe(req) ply, res = unpackdist(dtm) - if res == iDRAW: - # Draw. - return 0 - elif res == iWMATE: + if res == iWMATE: # White mates in the stored position. if req.realside == 1: if req.is_reversed: @@ -1615,29 +1583,38 @@ def probe_dtm(self, board): return -ply else: return ply + else: + # Draw. + return 0 - def get_dtm(self, board, default=None): + def get_dtm(self, board: chess.Board, default: Optional[int] = None) -> Optional[int]: try: return self.probe_dtm(board) except KeyError: return default - def probe_wdl(self, board): + def probe_wdl(self, board: chess.Board) -> int: """ - Probes for win/draw/loss-information. + Probes for win/draw/loss information. Returns ``1`` if the side to move is winning, ``0`` if it is a draw, and ``-1`` if the side to move is losing. - >>> with chess.gaviota.open_tablebases("data/gaviota") as tablebases: - ... tablebases.probe_wdl(chess.Board("8/4k3/8/B7/8/8/8/4K3 w - - 0 1")) + >>> import chess + >>> import chess.gaviota + >>> + >>> with chess.gaviota.open_tablebase("data/gaviota") as tablebase: + ... board = chess.Board("8/4k3/8/B7/8/8/8/4K3 w - - 0 1") + ... print(tablebase.probe_wdl(board)) ... 0 :raises: :exc:`KeyError` (or specifically :exc:`chess.gaviota.MissingTableError`) if the probe fails. Use - :func:`~chess.gaviota.PythonTablebases.get_wdl()` if you prefer + :func:`~chess.gaviota.PythonTablebase.get_wdl()` if you prefer to get ``None`` instead of an exception. + + Note that probing a corrupted table file is undefined behavior. """ dtm = self.probe_dtm(board) @@ -1648,18 +1625,18 @@ def probe_wdl(self, board): return 0 elif dtm > 0: return 1 - elif dtm < 0: + else: return -1 - def get_wdl(self, board, default=None): + def get_wdl(self, board: chess.Board, default: Optional[int] = None) -> Optional[int]: try: return self.probe_wdl(board) except KeyError: return default - def _setup_tablebase(self, req): - white_letters = "".join([chess.PIECE_SYMBOLS[i] for i in req.white_types]) - black_letters = "".join([chess.PIECE_SYMBOLS[i] for i in req.black_types]) + def _setup_tablebase(self, req: Request) -> BinaryIO: + white_letters = "".join(chess.piece_symbol(i) for i in req.white_types) + black_letters = "".join(chess.piece_symbol(i) for i in req.black_types) if (white_letters + black_letters) in self.available_tables: req.is_reversed = False @@ -1675,27 +1652,24 @@ def _setup_tablebase(self, req): req.white_piece_types = req.black_types req.black_piece_squares = [flip_ns(s) for s in req.white_squares] req.black_piece_types = req.white_types - req.side = opp(req.side) - if req.epsq != NOSQUARE: - req.epsq = flip_ns(req.epsq) else: - raise MissingTableError("no gaviota table available for: {0}v{1}".format(white_letters.upper(), black_letters.upper())) + raise MissingTableError(f"no gaviota table available for: {white_letters.upper()}v{black_letters.upper()}") return self._open_tablebase(req) - def _open_tablebase(self, req): + def _open_tablebase(self, req: Request) -> BinaryIO: stream = self.streams.get(req.egkey) if stream is None: path = self.available_tables[req.egkey] - stream = open(path, "rb+") + stream = open(path, "rb") self.egtb_loadindexes(req.egkey, stream) self.streams[req.egkey] = stream return stream - def close(self): + def close(self) -> None: """Closes all loaded tables.""" self.available_tables.clear() @@ -1708,72 +1682,7 @@ def close(self): _, stream = self.streams.popitem() stream.close() - def egtb_get_dtm(self, req): - dtm = self._tb_probe(req) - - if req.epsq != NOSQUARE: - capturer_a = 0 - capturer_b = 0 - xed = 0 - - # Flip for move generation. - if req.side == 0: - xs = list(req.white_piece_squares) - xp = list(req.white_piece_types) - ys = list(req.black_piece_squares) - yp = list(req.black_piece_types) - else: - xs = list(req.black_piece_squares) - xp = list(req.black_piece_types) - ys = list(req.white_piece_squares) - yp = list(req.white_piece_types) - - # Captured pawn trick: from ep square to captured. - xed = req.epsq ^ (1 << 3) - - # Find captured index (j). - try: - j = ys.index(xed) - except ValueError: - j = -1 - - # Try first possible ep capture. - if 0 == (0x88 & (map88(xed) + 1)): - capturer_a = xed + 1 - - # Try second possible ep capture - if 0 == (0x88 & (map88(xed) - 1)): - capturer_b = xed - 1 - - if (j > -1) and (ys[j] == xed): - # Find capturers (i). - for i in range(len(xs)): - if xp[i] == chess.PAWN and (xs[i] == capturer_a or xs[i] == capturer_b): - epscore = iFORBID - - # Execute capture. - xs[i] = req.epsq - removepiece(ys, yp, j) - - # Flip back. - if req.side == 1: - xs, ys = ys, xs - xp, yp = yp, xp - - # Make subrequest. - subreq = Request(xs, xp, ys, yp, opp(req.side), NOSQUARE) - try: - epscore = self._tb_probe(subreq) - epscore = adjust_up(epscore) - - # Chooses to ep or not. - dtm = bestx(req.side, epscore, dtm) - except IndexError: - break - - return dtm - - def egtb_block_getnumber(self, req, idx): + def egtb_block_getnumber(self, req: Request, idx: int) -> int: maxindex = EGKEY[req.egkey].maxindex blocks_per_side = 1 + (maxindex - 1) // ENTRIES_PER_BLOCK @@ -1781,18 +1690,18 @@ def egtb_block_getnumber(self, req, idx): return req.side * blocks_per_side + block_in_side - def egtb_block_getsize(self, req, idx): + def egtb_block_getsize(self, req: Request, idx: int) -> int: blocksz = ENTRIES_PER_BLOCK maxindex = EGKEY[req.egkey].maxindex block = idx // blocksz offset = block * blocksz if (offset + blocksz) > maxindex: - return maxindex - offset # last block size + return maxindex - offset # Last block size else: - return blocksz # size of a normal block + return blocksz # Size of a normal block - def _tb_probe(self, req): + def _tb_probe(self, req: Request) -> int: stream = self._setup_tablebase(req) idx = EGKEY[req.egkey].pctoi(req) offset, remainder = split_index(idx) @@ -1807,13 +1716,13 @@ def _tb_probe(self, req): z = self.egtb_block_getsize_zipped(req.egkey, block) self.egtb_block_park(req.egkey, block, stream) - buffer_zipped = stream.read(z) + buffer_zipped: bytearray | bytes = stream.read(z) if buffer_zipped[0] == 0: # If flag is zero, plain LZMA is following. buffer_zipped = buffer_zipped[2:] else: - # Else LZMA86. We have to build a fake header. + # Else LZMA86. Build a fake header. DICTIONARY_SIZE = 4096 POS_STATE_BITS = 2 NUM_LITERAL_POS_STATE_BITS = 0 @@ -1828,19 +1737,14 @@ def _tb_probe(self, req): # Concatenate the fake header with the true LZMA stream. buffer_zipped = properties + buffer_zipped[15:] - buffer_packed = self.lzma.LZMADecompressor().decompress(buffer_zipped) + buffer_packed = lzma.LZMADecompressor().decompress(buffer_zipped) t.pcache = egtb_block_unpack(req.side, n, buffer_packed) # Update LRU block cache. self.block_cache[(t.egkey, t.offset, t.side)] = t if len(self.block_cache) > 128: - lru_cache_key, lru_age = None, None - for cache_key, cache_entry in self.block_cache.items(): - if lru_age is None or cache_entry.age < lru_age: - lru_cache_key = cache_key - lru_age = cache_entry.age - + lru_cache_key = min(self.block_cache, key=lambda cache_key: self.block_cache[cache_key].age) del self.block_cache[lru_cache_key] else: t.age = self.block_age @@ -1850,7 +1754,7 @@ def _tb_probe(self, req): return dtm - def egtb_loadindexes(self, egkey, stream): + def egtb_loadindexes(self, egkey: str, stream: BinaryIO) -> ZipInfo: zipinfo = self.zipinfo.get(egkey) if zipinfo is None: @@ -1864,39 +1768,39 @@ def egtb_loadindexes(self, egkey, stream): n_idx = blocks + 1 IndexStruct = struct.Struct("<" + "I" * n_idx) - p = IndexStruct.unpack(stream.read(IndexStruct.size)) + p = list(IndexStruct.unpack(stream.read(IndexStruct.size))) - zipinfo = Zipinfo(extraoffset=0, totalblocks=n_idx, blockindex=p) + zipinfo = ZipInfo(extraoffset=0, totalblocks=n_idx, blockindex=p) self.zipinfo[egkey] = zipinfo return zipinfo - def egtb_block_getsize_zipped(self, egkey, block): + def egtb_block_getsize_zipped(self, egkey: str, block: int) -> int: i = self.zipinfo[egkey].blockindex[block] j = self.zipinfo[egkey].blockindex[block + 1] return j - i - def egtb_block_park(self, egkey, block, stream): + def egtb_block_park(self, egkey: str, block: int, stream: BinaryIO) -> int: i = self.zipinfo[egkey].blockindex[block] i += self.zipinfo[egkey].extraoffset stream.seek(i) return i - def __enter__(self): + def __enter__(self) -> PythonTablebase: return self - def __exit__(self, exc_type, exc_value, traceback): + def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]) -> None: self.close() -class NativeTablebases(object): +class NativeTablebase: """ Provides access to Gaviota tablebases via the shared library libgtb. - Has the same interface as :class:`~chess.gaviota.PythonTablebases`. + Has the same interface as :class:`~chess.gaviota.PythonTablebase`. """ - def __init__(self, libgtb): - self.paths = [] + def __init__(self, libgtb: ctypes.CDLL) -> None: + self.paths: List[str] = [] self.libgtb = libgtb self.libgtb.tb_init.restype = ctypes.c_char_p @@ -1919,14 +1823,14 @@ def __init__(self, libgtb): self._tbcache_restart(1024 * 1024, 50) - def open_directory(self, directory): + def add_directory(self, directory: str) -> None: if not os.path.isdir(directory): - raise IOError("not a directory: {0}".format(repr(directory))) + raise IOError(f"not a directory: {directory!r}") self.paths.append(directory) self._tb_restart() - def _tb_restart(self): + def _tb_restart(self) -> None: self.c_paths = (ctypes.c_char_p * len(self.paths))() self.c_paths[:] = [path.encode("utf-8") for path in self.paths] @@ -1941,48 +1845,48 @@ def _tb_restart(self): av = self.libgtb.tb_availability() if av & 1: - LOGGER.debug("Some 3 piece tablebases available") + LOGGER.debug("Some 3-piece tables available") if av & 2: - LOGGER.debug("All 3 piece tablebases complete") + LOGGER.debug("All 3-piece tables complete") if av & 4: - LOGGER.debug("Some 4 piece tablebases available") + LOGGER.debug("Some 4-piece tables available") if av & 8: - LOGGER.debug("All 4 piece tablebases complete") + LOGGER.debug("All 4-piece tables complete") if av & 16: - LOGGER.debug("Some 5 piece tablebases available") + LOGGER.debug("Some 5-piece tables available") if av & 32: - LOGGER.debug("All 5 piece tablebases complete") + LOGGER.debug("All 5-piece tables complete") - def _tbcache_restart(self, cache_mem, wdl_fraction): + def _tbcache_restart(self, cache_mem: int, wdl_fraction: int) -> None: self.libgtb.tbcache_restart(ctypes.c_size_t(cache_mem), ctypes.c_int(wdl_fraction)) - def probe_dtm(self, board): + def probe_dtm(self, board: chess.Board) -> int: return self._probe_hard(board) - def probe_wdl(self, board): + def probe_wdl(self, board: chess.Board) -> int: return self._probe_hard(board, wdl_only=True) - def get_dtm(self, board, default=None): + def get_dtm(self, board: chess.Board, default: Optional[int] = None) -> Optional[int]: try: return self.probe_dtm(board) except KeyError: return default - def get_wdl(self, board, default=None): + def get_wdl(self, board: chess.Board, default: Optional[int] = None) -> Optional[int]: try: return self.probe_wdl(board) except KeyError: return default - def _probe_hard(self, board, wdl_only=False): + def _probe_hard(self, board: chess.Board, wdl_only: bool = False) -> int: if board.is_insufficient_material(): return 0 if board.castling_rights: - raise KeyError("gaviota tables do not contain positions with castling rights: {0}".format(board.fen())) + raise KeyError(f"gaviota tables do not contain positions with castling rights: {board.fen()}") - if chess.popcount(board.occupied) > 5: - raise KeyError("gaviota tables support up to 5 pieces, not {0}: {1}".format(chess.popcount(board.occupied), board.fen())) + if board.piece_count() > 5: + raise KeyError(f"gaviota tables support up to 5 pieces, not {board.piece_count()}: {board.fen()}") stm = ctypes.c_uint(0 if board.turn == chess.WHITE else 1) ep_square = ctypes.c_uint(board.ep_square if board.ep_square else 64) @@ -1994,7 +1898,7 @@ def _probe_hard(self, board, wdl_only=False): i = -1 for i, square in enumerate(chess.SquareSet(board.occupied_co[chess.WHITE])): c_ws[i] = square - c_wp[i] = board.piece_type_at(square) + c_wp[i] = typing.cast(chess.PieceType, board.piece_type_at(square)) c_ws[i + 1] = 64 c_wp[i + 1] = 0 @@ -2005,7 +1909,7 @@ def _probe_hard(self, board, wdl_only=False): i = -1 for i, square in enumerate(chess.SquareSet(board.occupied_co[chess.BLACK])): c_bs[i] = square - c_bp[i] = board.piece_type_at(square) + c_bp[i] = typing.cast(chess.PieceType, board.piece_type_at(square)) c_bs[i + 1] = 64 c_bp[i + 1] = 0 @@ -2022,79 +1926,69 @@ def _probe_hard(self, board, wdl_only=False): # Probe forbidden. if info.value == 3: - raise MissingTableError("gaviota table for {0} not available".format(board.fen())) - - # Probe failed or unknown. - if not ret or info.value == 7: - raise KeyError("gaviota probe failed for {0}".format(board.fen())) + raise MissingTableError(f"gaviota table for {board.fen()} not available") # Draw. - if info.value == 0: + if ret and info.value == 0: return 0 # White mates. - if info.value == 1: + if ret and info.value == 1: return dtm if board.turn == chess.WHITE else -dtm # Black mates. - if info.value == 2: + if ret and info.value == 2: return dtm if board.turn == chess.BLACK else -dtm - def close(self): + raise KeyError(f"gaviota probe failed for {board.fen()}") + + def close(self) -> None: self.paths = [] if self.libgtb.tb_is_initialized(): self.libgtb.tbcache_done() self.libgtb.tb_done() - def __enter__(self): + def __enter__(self) -> NativeTablebase: return self - def __exit__(self, exc_type, exc_value, traceback): + def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]) -> None: self.close() -def open_tablebases_native(directory, libgtb=None, LibraryLoader=ctypes.cdll): +def open_tablebase_native(directory: str, *, libgtb: Optional[str] = None, LibraryLoader: ctypes.LibraryLoader[ctypes.CDLL] = ctypes.cdll) -> NativeTablebase: """ - Opens a collection of tablebases for probing using libgtb. + Opens a collection of tables for probing using libgtb. - In most cases :func:`~chess.gaviota.open_tablebases()` should be used. + In most cases :func:`~chess.gaviota.open_tablebase()` should be used. Use this function only if you do not want to downgrade to pure Python tablebase probing. :raises: :exc:`RuntimeError` or :exc:`OSError` when libgtb can not be used. """ libgtb = libgtb or ctypes.util.find_library("gtb") or "libgtb.so.1.0.1" - tables = NativeTablebases(LibraryLoader.LoadLibrary(libgtb)) - tables.open_directory(directory) + tables = NativeTablebase(LibraryLoader.LoadLibrary(libgtb)) + tables.add_directory(directory) return tables -def open_tablebases(directory, libgtb=None, LibraryLoader=ctypes.cdll): +def open_tablebase(directory: str, *, libgtb: Optional[str] = None, LibraryLoader: ctypes.LibraryLoader[ctypes.CDLL] = ctypes.cdll) -> Union[NativeTablebase, PythonTablebase]: """ - Opens a collection of tablebases for probing. + Opens a collection of tables for probing. First native access via the shared library libgtb is tried. You can optionally provide a specific library name or a library loader. The shared library has global state and caches, so only one instance can be open at a time. - Second pure Python probing code is tried. + Second, pure Python probing code is tried. """ try: if LibraryLoader: - return open_tablebases_native(directory, libgtb, LibraryLoader) + return open_tablebase_native(directory, libgtb=libgtb, LibraryLoader=LibraryLoader) except (OSError, RuntimeError) as err: - LOGGER.info("Falling back to pure Python tablebases: %r", err) - - try: - import lzma - except ImportError: - try: - from backports import lzma - except ImportError: - raise ImportError("chess.gaviota requires backports.lzma or libgtb") + LOGGER.info("Falling back to pure Python tablebase: %r", err) - tables = PythonTablebases(lzma) - tables.open_directory(directory) + tables = PythonTablebase() + tables.add_directory(directory) return tables diff --git a/chess/pgn.py b/chess/pgn.py index e50eec86c..5ae5b43b0 100644 --- a/chess/pgn.py +++ b/chess/pgn.py @@ -1,36 +1,32 @@ -# -*- coding: utf-8 -*- -# -# This file is part of the python-chess library. -# Copyright (C) 2012-2017 Niklas Fiekas -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . +from __future__ import annotations -import chess +import abc +import dataclasses +import enum import itertools -import re import logging +import re +import typing -try: - import backport_collections as collections -except ImportError: - import collections +import chess +import chess.engine +import chess.svg + +from typing import Any, Callable, Dict, Generic, Iterable, Iterator, List, Literal, Mapping, MutableMapping, Set, TextIO, Tuple, Type, TypeVar, Optional, Union +from chess import Color, Square + +if typing.TYPE_CHECKING: + from typing_extensions import Self, override +else: + F = typing.TypeVar("F", bound=Callable[..., Any]) + def override(fn: F, /) -> F: + return fn LOGGER = logging.getLogger(__name__) -# Reference of Numeric Annotation Glyphs: +# Reference of Numeric Annotation Glyphs (NAGs): # https://en.wikipedia.org/wiki/Numeric_Annotation_Glyphs NAG_NULL = 0 @@ -82,17 +78,23 @@ NAG_NOVELTY = 146 -TAG_REGEX = re.compile(r"^\[([A-Za-z0-9_]+)\s+\"(.*)\"\]\s*$") +TAG_REGEX = re.compile(r"^\[([A-Za-z0-9][A-Za-z0-9_+#=:-]*)\s+\"([^\r]*)\"\]\s*$") + +TAG_NAME_REGEX = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_+#=:-]*\Z") MOVETEXT_REGEX = re.compile(r""" ( [NBKRQ]?[a-h]?[1-8]?[\-x]?[a-h][1-8](?:=?[nbrqkNBRQK])? |[PNBRQK]?@[a-h][1-8] |-- + |Z0 + |0000 + |@@@@ |O-O(?:-O)? |0-0(?:-0)? ) |(\{.*) + |(;.*) |(\$[0-9]+) |(\() |(\)) @@ -100,66 +102,191 @@ |([\?!]{1,2}) """, re.DOTALL | re.VERBOSE) +SKIP_MOVETEXT_REGEX = re.compile(r""";|\{|\}""") + + +CLOCK_REGEX = re.compile(r"""(?P\s?)\[%clk\s(?P\d+):(?P\d+):(?P\d+(?:\.\d*)?)\](?P\s?)""") +EMT_REGEX = re.compile(r"""(?P\s?)\[%emt\s(?P\d+):(?P\d+):(?P\d+(?:\.\d*)?)\](?P\s?)""") + +EVAL_REGEX = re.compile(r""" + (?P\s?) + \[%eval\s(?: + \#(?P[+-]?\d+) + |(?P[+-]?(?:\d{0,10}\.\d{1,2}|\d{1,10}\.?)) + )(?: + ,(?P\d+) + )?\] + (?P\s?) + """, re.VERBOSE) + +ARROWS_REGEX = re.compile(r""" + (?P\s?) + \[%(?:csl|cal)\s(?P + [RGYB][a-h][1-8](?:[a-h][1-8])? + (?:,[RGYB][a-h][1-8](?:[a-h][1-8])?)* + )\] + (?P\s?) + """, re.VERBOSE) + +def _condense_affix(infix: str) -> Callable[[typing.Match[str]], str]: + def repl(match: typing.Match[str]) -> str: + if infix: + return match.group("prefix") + infix + match.group("suffix") + else: + return match.group("prefix") and match.group("suffix") + return repl -class GameNode(object): - def __init__(self): - self.parent = None - self.move = None - self.nags = set() - self.starting_comment = "" - self.comment = "" +def _standardize_comments(comment: Union[str, list[str]]) -> list[str]: + return [] if not comment else [comment] if isinstance(comment, str) else comment + + +TAG_ROSTER = ["Event", "Site", "Date", "Round", "White", "Black", "Result"] + + +class SkipType(enum.Enum): + SKIP = None + +SKIP = SkipType.SKIP + + +ResultT = TypeVar("ResultT", covariant=True) + + +class TimeControlType(enum.Enum): + UNKNOWN = 0 + UNLIMITED = 1 + STANDARD = 2 + RAPID = 3 + BLITZ = 4 + BULLET = 5 + + +@dataclasses.dataclass +class TimeControlPart: + moves: int = 0 + time: int = 0 + increment: float = 0 + delay: float = 0 + + +@dataclasses.dataclass +class TimeControl: + """ + PGN TimeControl Parser + Spec: http://www.saremba.de/chessgml/standards/pgn/pgn-complete.htm#c9.6 + + Not Yet Implemented: + - Hourglass/Sandclock ('*' prefix) + - Differentiating between Bronstein and Simple Delay (Not part of the PGN Spec) + - More Info: https://en.wikipedia.org/wiki/Chess_clock#Timing_methods + """ + + parts: list[TimeControlPart] = dataclasses.field(default_factory=list) + type: TimeControlType = TimeControlType.UNKNOWN + + +class _AcceptFrame: + def __init__(self, node: ChildNode, *, is_variation: bool = False, sidelines: bool = True): + self.state = "pre" + self.node = node + self.is_variation = is_variation + self.variations = iter(itertools.islice(node.parent.variations, 1, None) if sidelines else []) + self.in_variation = False + + +class GameNode(abc.ABC): + variations: List[ChildNode] + """A list of child nodes.""" + + comments: list[str] + """ + A comment that goes behind the move leading to this node. Comments + that occur before any moves are assigned to the root node. + """ + + starting_comments: list[str] + + nags: Set[int] + + def __init__(self, *, comment: Union[str, list[str]] = "") -> None: self.variations = [] + self.comments = _standardize_comments(comment) + + # Deprecated: These should be properties of ChildNode, but need to + # remain here for backwards compatibility. + self.starting_comments = [] + self.nags = set() - self.board_cached = None + @property + @abc.abstractmethod + def parent(self) -> Optional[GameNode]: + """The parent node or ``None`` if this is the root node of the game.""" - def board(self, _cache=True): + @property + @abc.abstractmethod + def move(self) -> Optional[chess.Move]: + """ + The move leading to this node or ``None`` if this is the root node of + the game. """ - Gets a board with the position of the node. - It's a copy, so modifying the board will not alter the game. + @abc.abstractmethod + def board(self) -> chess.Board: """ - if self.board_cached: - return self.board_cached.copy() + Gets a board with the position of the node. - board = self.parent.board(_cache=False) - board.push(self.move) + For the root node, this is the default starting position (for the + ``Variant``) unless the ``FEN`` header tag is set. - if _cache: - self.board_cached = board - return board.copy() - else: - return board + It's a copy, so modifying the board will not alter the game. - def san(self): + Complexity is `O(n)`. """ - Gets the standard algebraic notation of the move leading to this node. - See :func:`chess.Board.san()`. - Do not call this on the root node. + @abc.abstractmethod + def ply(self) -> int: """ - return self.parent.board().san(self.move) + Returns the number of half-moves up to this node, as indicated by + fullmove number and turn of the position. + See :func:`chess.Board.ply()`. - def uci(self, chess960=None): + Usually this is equal to the number of parent nodes, but it may be + more if the game was started from a custom position. + + Complexity is `O(n)`. """ - Gets the UCI notation of the move leading to this node. - See :func:`chess.Board.uci()`. - Do not call this on the root node. + def turn(self) -> Color: """ - return self.parent.board().san(self.move, chess960=chess960) + Gets the color to move at this node. See :data:`chess.Board.turn`. - def root(self): - """Gets the root node, i.e. the game.""" - node = self + Complexity is `O(n)`. + """ + return self.ply() % 2 == 0 + def root(self) -> GameNode: + node = self while node.parent: node = node.parent - return node - def end(self): - """Follows the main variation to the end and returns the last node.""" + def game(self) -> Game: + """ + Gets the root node, i.e., the game. + + Complexity is `O(n)`. + """ + root = self.root() + assert isinstance(root, Game), "GameNode not rooted in Game" + return root + + def end(self) -> GameNode: + """ + Follows the main variation to the end and returns the last node. + + Complexity is `O(n)`. + """ node = self while node.variations: @@ -167,23 +294,36 @@ def end(self): return node - def is_end(self): - """Checks if this node is the last node in the current variation.""" + def is_end(self) -> bool: + """ + Checks if this node is the last node in the current variation. + + Complexity is `O(1)`. + """ return not self.variations - def starts_variation(self): + def starts_variation(self) -> bool: """ Checks if this node starts a variation (and can thus have a starting comment). The root node does not start a variation and can have no starting comment. + + For example, in ``1. e4 e5 (1... c5 2. Nf3) 2. Nf3``, the node holding + 1... c5 starts a variation. + + Complexity is `O(1)`. """ if not self.parent or not self.parent.variations: return False return self.parent.variations[0] != self - def is_main_line(self): - """Checks if the node is in the main line of the game.""" + def is_mainline(self) -> bool: + """ + Checks if the node is in the mainline of the game. + + Complexity is `O(n)`. + """ node = self while node.parent: @@ -196,83 +336,102 @@ def is_main_line(self): return True - def is_main_variation(self): + def is_main_variation(self) -> bool: """ Checks if this node is the first variation from the point of view of its - parent. The root node also is in the main variation. + parent. The root node is also in the main variation. + + Complexity is `O(1)`. """ if not self.parent: return True return not self.parent.variations or self.parent.variations[0] == self - def variation(self, move): + def __getitem__(self, move: Union[int, chess.Move, GameNode]) -> ChildNode: + try: + return self.variations[move] # type: ignore + except TypeError: + for variation in self.variations: + if variation.move == move or variation == move: + return variation + + raise KeyError(move) + + def __contains__(self, move: Union[int, chess.Move, GameNode]) -> bool: + try: + self[move] + except KeyError: + return False + else: + return True + + def variation(self, move: Union[int, chess.Move, GameNode]) -> ChildNode: """ - Gets a child node by move or index. + Gets a child node by either the move or the variation index. """ - for index, variation in enumerate(self.variations): - if move == variation.move or index == move or move == variation: - return variation - - raise KeyError("variation not found") + return self[move] - def has_variation(self, move): - """Checks if the given move appears as a variation.""" - return move in (variation.move for variation in self.variations) + def has_variation(self, move: Union[int, chess.Move, GameNode]) -> bool: + """Checks if this node has the given variation.""" + return move in self - def promote_to_main(self, move): - """Promotes the given move to the main variation.""" - variation = self.variation(move) + def promote_to_main(self, move: Union[int, chess.Move, GameNode]) -> None: + """Promotes the given *move* to the main variation.""" + variation = self[move] self.variations.remove(variation) self.variations.insert(0, variation) - def promote(self, move): - """Moves the given variation one up in the list of variations.""" - variation = self.variation(move) + def promote(self, move: Union[int, chess.Move, GameNode]) -> None: + """Moves a variation one up in the list of variations.""" + variation = self[move] i = self.variations.index(variation) if i > 0: self.variations[i - 1], self.variations[i] = self.variations[i], self.variations[i - 1] - def demote(self, move): - """Moves the given variation one down in the list of variations.""" - variation = self.variation(move) + def demote(self, move: Union[int, chess.Move, GameNode]) -> None: + """Moves a variation one down in the list of variations.""" + variation = self[move] i = self.variations.index(variation) if i < len(self.variations) - 1: self.variations[i + 1], self.variations[i] = self.variations[i], self.variations[i + 1] - def remove_variation(self, move): - """Removes a variation by move.""" + def remove_variation(self, move: Union[int, chess.Move, GameNode]) -> None: + """Removes a variation.""" self.variations.remove(self.variation(move)) - def add_variation(self, move, comment="", starting_comment="", nags=()): + def add_variation(self, move: chess.Move, *, comment: Union[str, list[str]] = "", starting_comment: Union[str, list[str]] = "", nags: Iterable[int] = []) -> ChildNode: """Creates a child node with the given attributes.""" - node = GameNode() - node.move = move - node.nags = set(nags) - node.parent = self - node.comment = comment - node.starting_comment = starting_comment - self.variations.append(node) - return node + # Instantiate ChildNode only in this method. + return ChildNode(self, move, comment=comment, starting_comment=starting_comment, nags=nags) - def add_main_variation(self, move, comment=""): + def add_main_variation(self, move: chess.Move, *, comment: str = "", nags: Iterable[int] = []) -> ChildNode: """ Creates a child node with the given attributes and promotes it to the main variation. """ - node = self.add_variation(move, comment=comment) - self.variations.remove(node) - self.variations.insert(0, node) + node = self.add_variation(move, comment=comment, nags=nags) + self.variations.insert(0, self.variations.pop()) return node - def main_line(self): - """Yields the moves of the main line starting in this node.""" - node = self - while node.variations: - node = node.variations[0] - yield node.move + def next(self) -> Optional[ChildNode]: + """ + Returns the first node of the mainline after this node, or ``None`` if + this node does not have any children. + + Complexity is `O(1)`. + """ + return self.variations[0] if self.variations else None + + def mainline(self) -> Mainline[ChildNode]: + """Returns an iterable over the mainline starting after this node.""" + return Mainline(self, lambda node: node) + + def mainline_moves(self) -> Mainline[chess.Move]: + """Returns an iterable over the main moves after this node.""" + return Mainline(self, lambda node: node.move) - def add_line(self, moves, comment="", starting_comment="", nags=()): + def add_line(self, moves: Iterable[chess.Move], *, comment: Union[str, list[str]] = "", starting_comment: Union[str, list[str]] = "", nags: Iterable[int] = []) -> GameNode: """ Creates a sequence of child nodes for the given list of moves. Adds *comment* and *nags* to the last node of the line and returns it. @@ -285,242 +444,696 @@ def add_line(self, moves, comment="", starting_comment="", nags=()): starting_comment = "" # Merge comment and NAGs. - if node.comment: - node.comment += " " + comment - else: - node.comment = comment - + comments = _standardize_comments(comment) + node.comments.extend(comments) node.nags.update(nags) return node - def accept(self, visitor, _board=None): + def eval(self) -> Optional[chess.engine.PovScore]: """ - Traverse game nodes in PGN order using the given *visitor*. Returns - the visitor result. + Parses the first valid ``[%eval ...]`` annotation in the comment of + this node, if any. + + Complexity is `O(n)`. """ - board = self.board() if _board is None else _board + match = EVAL_REGEX.search(" ".join(self.comments)) + if not match: + return None + + turn = self.turn() + + if match.group("mate"): + mate = int(match.group("mate")) + score: chess.engine.Score = chess.engine.Mate(mate) + if mate == 0: + # Resolve this ambiguity in the specification in favor of + # standard chess: The player to move after mate is the player + # who has been mated. + return chess.engine.PovScore(score, turn) + else: + score = chess.engine.Cp(round(float(match.group("cp")) * 100)) - # The mainline move goes first. - if self.variations: - main_variation = self.variations[0] - visitor.visit_move(board, main_variation.move) + return chess.engine.PovScore(score if turn else -score, turn) + + def eval_depth(self) -> Optional[int]: + """ + Parses the first valid ``[%eval ...]`` annotation in the comment of + this node and returns the corresponding depth, if any. + + Complexity is `O(1)`. + """ + match = EVAL_REGEX.search(" ".join(self.comments)) + return int(match.group("depth")) if match and match.group("depth") else None + + def set_eval(self, score: Optional[chess.engine.PovScore], depth: Optional[int] = None) -> None: + """ + Replaces the first valid ``[%eval ...]`` annotation in the comment of + this node or adds a new one. + """ + eval = "" + if score is not None: + depth_suffix = "" if depth is None else f",{max(depth, 0):d}" + cp = score.white().score() + if cp is not None: + eval = f"[%eval {float(cp) / 100:.2f}{depth_suffix}]" + elif score.white().mate(): + eval = f"[%eval #{score.white().mate()}{depth_suffix}]" + + self._replace_or_add_annotation(eval, EVAL_REGEX) + + def arrows(self) -> List[chess.svg.Arrow]: + """ + Parses all ``[%csl ...]`` and ``[%cal ...]`` annotations in the comment + of this node. - # Visit NAGs. - for nag in sorted(main_variation.nags): - visitor.visit_nag(nag) + Returns a list of :class:`arrows `. + """ + arrows = [] + for match in ARROWS_REGEX.finditer(" ".join(self.comments)): + for group in match.group("arrows").split(","): + arrows.append(chess.svg.Arrow.from_pgn(group)) - # Visit the comment. - if main_variation.comment: - visitor.visit_comment(main_variation.comment) + return arrows - # Then visit sidelines. - for variation in itertools.islice(self.variations, 1, None): - # Start variation. - visitor.begin_variation() + def set_arrows(self, arrows: Iterable[Union[chess.svg.Arrow, Tuple[Square, Square]]]) -> None: + """ + Replaces all valid ``[%csl ...]`` and ``[%cal ...]`` annotations in + the comment of this node or adds new ones. + """ + csl: List[str] = [] + cal: List[str] = [] - # Append starting comment. - if variation.starting_comment: - visitor.visit_comment(variation.starting_comment) + for arrow in arrows: + try: + tail, head = arrow # type: ignore + arrow = chess.svg.Arrow(tail, head) + except TypeError: + pass + (csl if arrow.tail == arrow.head else cal).append(arrow.pgn()) # type: ignore - # Visit move. - visitor.visit_move(board, variation.move) + for index in range(len(self.comments)): + self.comments[index] = ARROWS_REGEX.sub(_condense_affix(""), self.comments[index]) - # Visit NAGs. - for nag in sorted(variation.nags): - visitor.visit_nag(nag) + self.comments = list(filter(None, self.comments)) - # Visit comment. - if variation.comment: - visitor.visit_comment(variation.comment) + prefix = "" + if csl: + prefix += f"[%csl {','.join(csl)}]" + if cal: + prefix += f"[%cal {','.join(cal)}]" - # Recursively append the next moves. - board.push(variation.move) - variation.accept(visitor, _board=board) - board.pop() + if prefix: + self.comments.insert(0, prefix) - # End variation. - visitor.end_variation() + def clock(self) -> Optional[float]: + """ + Parses the first valid ``[%clk ...]`` annotation in the comment of + this node, if any. - # The mainline is continued last. - if self.variations: - main_variation = self.variations[0] + Returns the player's remaining time to the next time control after this + move, in seconds. + """ + match = CLOCK_REGEX.search(" ".join(self.comments)) + if match is None: + return None + return int(match.group("hours")) * 3600 + int(match.group("minutes")) * 60 + float(match.group("seconds")) + + def set_clock(self, seconds: Optional[float]) -> None: + """ + Replaces the first valid ``[%clk ...]`` annotation in the comment of + this node or adds a new one. + """ + clk = "" + if seconds is not None: + seconds = max(0, seconds) + hours = int(seconds // 3600) + minutes = int(seconds % 3600 // 60) + seconds = seconds % 3600 % 60 + seconds_part = f"{seconds:06.3f}".rstrip("0").rstrip(".") + clk = f"[%clk {hours:d}:{minutes:02d}:{seconds_part}]" + + self._replace_or_add_annotation(clk, CLOCK_REGEX) + + def emt(self) -> Optional[float]: + """ + Parses the first valid ``[%emt ...]`` annotation in the comment of + this node, if any. + + Returns the player's elapsed move time use for the comment of this + move, in seconds. + """ + match = EMT_REGEX.search(" ".join(self.comments)) + if match is None: + return None + return int(match.group("hours")) * 3600 + int(match.group("minutes")) * 60 + float(match.group("seconds")) + + def set_emt(self, seconds: Optional[float]) -> None: + """ + Replaces the first valid ``[%emt ...]`` annotation in the comment of + this node or adds a new one. + """ + emt = "" + if seconds is not None: + seconds = max(0, seconds) + hours = int(seconds // 3600) + minutes = int(seconds % 3600 // 60) + seconds = seconds % 3600 % 60 + seconds_part = f"{seconds:06.3f}".rstrip("0").rstrip(".") + emt = f"[%emt {hours:d}:{minutes:02d}:{seconds_part}]" + + self._replace_or_add_annotation(emt, EMT_REGEX) + + def _replace_or_add_annotation(self, text: str, regex: re.Pattern[str]) -> None: + found = 0 + for index in range(len(self.comments)): + self.comments[index], found = regex.subn(_condense_affix(text), self.comments[index], count=1) + if found: + break + + self.comments = list(filter(None, self.comments)) + + if not found and text: + self.comments.append(text) + + @abc.abstractmethod + def accept(self, visitor: BaseVisitor[ResultT]) -> ResultT: + """ + Traverses game nodes in PGN order using the given *visitor*. Starts with + the move leading to this node. Returns the *visitor* result. + """ + + def accept_subgame(self, visitor: BaseVisitor[ResultT]) -> ResultT: + """ + Traverses headers and game nodes in PGN order, as if the game was + starting after this node. Returns the *visitor* result. + """ + if visitor.begin_game() is not SKIP: + game = self.game() + board = self.board() + + dummy_game = Game.without_tag_roster() + dummy_game.setup(board) + + visitor.begin_headers() + + for tagname, tagvalue in game.headers.items(): + if tagname not in dummy_game.headers: + visitor.visit_header(tagname, tagvalue) + for tagname, tagvalue in dummy_game.headers.items(): + visitor.visit_header(tagname, tagvalue) - # Recursively append the next moves. - board.push(main_variation.move) - main_variation.accept(visitor, _board=board) - board.pop() + if visitor.end_headers() is not SKIP: + visitor.visit_board(board) - # Get the result if not called recursively. - if _board is None: - return visitor.result() + if self.variations: + self.variations[0]._accept(board, visitor) - def __str__(self): + visitor.visit_result(game.headers.get("Result", "*")) + + visitor.end_game() + return visitor.result() + + def __str__(self) -> str: return self.accept(StringExporter(columns=None)) -class Game(GameNode): +class ChildNode(GameNode): + """ + A child node of a game, with the move leading to it. + Extends :class:`~chess.pgn.GameNode`. """ - The root node of a game with extra information such as headers and the - starting position. - By default the following 7 headers are provided in an ordered dictionary: + starting_comments: list[str] + """ + A comment for the start of a variation. Only nodes that + actually start a variation (:func:`~chess.pgn.GameNode.starts_variation()` + checks this) can have a starting comment. The root node can not have + a starting comment. + """ - >>> game = chess.pgn.Game() - >>> game.headers["Event"] - '?' - >>> game.headers["Site"] - '?' - >>> game.headers["Date"] - '????.??.??' - >>> game.headers["Round"] - '?' - >>> game.headers["White"] - '?' - >>> game.headers["Black"] - '?' - >>> game.headers["Result"] - '*' - - Also has all the other properties and methods of - :class:`~chess.pgn.GameNode`. + nags: Set[int] + """ + A set of NAGs as integers. NAGs always go behind a move, so the root + node of the game will never have NAGs. """ - def __init__(self): - super(Game, self).__init__() + def __init__(self, parent: GameNode, move: chess.Move, *, comment: Union[str, list[str]] = "", starting_comment: Union[str, list[str]] = "", nags: Iterable[int] = []) -> None: + super().__init__(comment=comment) + self._parent = parent + self._move = move + self.parent.variations.append(self) + + self.nags.update(nags) + self.starting_comments = _standardize_comments(starting_comment) + + @property + @override + def parent(self) -> GameNode: + """The parent node.""" + return self._parent + + @property + @override + def move(self) -> chess.Move: + """The move leading to this node.""" + return self._move + + @override + def board(self) -> chess.Board: + stack: List[chess.Move] = [] + node: GameNode = self + + while node.move is not None and node.parent is not None: + stack.append(node.move) + node = node.parent - self.headers = collections.OrderedDict() - self.headers["Event"] = "?" - self.headers["Site"] = "?" - self.headers["Date"] = "????.??.??" - self.headers["Round"] = "?" - self.headers["White"] = "?" - self.headers["Black"] = "?" - self.headers["Result"] = "*" + board = node.game().board() - self.errors = [] + while stack: + board.push(stack.pop()) + + return board - def board(self, _cache=False): + @override + def ply(self) -> int: + ply = 0 + node: GameNode = self + while node.parent is not None: + ply += 1 + node = node.parent + return node.game().ply() + ply + + def san(self) -> str: """ - Gets the starting position of the game. + Gets the standard algebraic notation of the move leading to this node. + See :func:`chess.Board.san()`. + + Do not call this on the root node. - Unless the ``FEN`` header tag is set this is the default starting - position (for the ``Variant``). + Complexity is `O(n)`. """ - chess960 = self.headers.get("Variant", "").lower() in [ - "chess960", - "fischerandom", # Cutechess - "fischerrandom"] + return self.parent.board().san(self.move) - if chess960 or "Variant" not in self.headers: - VariantBoard = chess.Board + def uci(self, *, chess960: Optional[bool] = None) -> str: + """ + Gets the UCI notation of the move leading to this node. + See :func:`chess.Board.uci()`. + + Do not call this on the root node. + + Complexity is `O(n)`. + """ + return self.parent.board().uci(self.move, chess960=chess960) + + @override + def end(self) -> ChildNode: + """ + Follows the main variation to the end and returns the last node. + + Complexity is `O(n)`. + """ + return typing.cast(ChildNode, super().end()) + + def _accept_node(self, parent_board: chess.Board, visitor: BaseVisitor[ResultT]) -> None: + if self.starting_comments: + visitor.visit_comment(self.starting_comments) + + visitor.visit_move(parent_board, self.move) + + parent_board.push(self.move) + visitor.visit_board(parent_board) + parent_board.pop() + + for nag in sorted(self.nags): + visitor.visit_nag(nag) + + if self.comments: + visitor.visit_comment(self.comments) + + def _accept(self, parent_board: chess.Board, visitor: BaseVisitor[ResultT], *, sidelines: bool = True) -> None: + stack = [_AcceptFrame(self, sidelines=sidelines)] + + while stack: + top = stack[-1] + + if top.in_variation: + top.in_variation = False + visitor.end_variation() + + if top.state == "pre": + top.node._accept_node(parent_board, visitor) + top.state = "variations" + elif top.state == "variations": + try: + variation = next(top.variations) + except StopIteration: + if top.node.variations: + parent_board.push(top.node.move) + stack.append(_AcceptFrame(top.node.variations[0], sidelines=True)) + top.state = "post" + else: + top.state = "end" + else: + if visitor.begin_variation() is not SKIP: + stack.append(_AcceptFrame(variation, sidelines=False, is_variation=True)) + top.in_variation = True + elif top.state == "post": + parent_board.pop() + top.state = "end" + else: + stack.pop() + + @override + def accept(self, visitor: BaseVisitor[ResultT]) -> ResultT: + self._accept(self.parent.board(), visitor, sidelines=False) + return visitor.result() + + def __repr__(self) -> str: + try: + parent_board = self.parent.board() + except ValueError: + return f"<{type(self).__name__} at {id(self):#x} (dangling: {self.move})>" else: - from chess.variant import find_variant - VariantBoard = find_variant(self.headers["Variant"]) + return "<{} at {:#x} ({}{} {} ...)>".format( + type(self).__name__, + id(self), + parent_board.fullmove_number, + "." if parent_board.turn == chess.WHITE else "...", + parent_board.san(self.move)) - fen = self.headers.get("FEN", VariantBoard.starting_fen) - board = VariantBoard(fen, chess960=chess960) - board.chess960 = board.chess960 or board.has_chess960_castling_rights() - return board - def setup(self, board): +GameT = TypeVar("GameT", bound="Game") + +class Game(GameNode): + """ + The root node of a game with extra information such as headers and the + starting position. Extends :class:`~chess.pgn.GameNode`. + """ + + headers: Headers + """ + A mapping of headers. By default, the following 7 headers are provided + (Seven Tag Roster): + + >>> import chess.pgn + >>> + >>> game = chess.pgn.Game() + >>> game.headers + Headers(Event='?', Site='?', Date='????.??.??', Round='?', White='?', Black='?', Result='*') + """ + + errors: List[Exception] + """ + A list of errors (such as illegal or ambiguous moves) encountered while + parsing the game. + """ + + def __init__(self, headers: Optional[Union[Mapping[str, str], Iterable[Tuple[str, str]]]] = None) -> None: + super().__init__() + self.headers = Headers(headers) + self.errors = [] + + @property + @override + def parent(self) -> None: + return None + + @property + @override + def move(self) -> None: + return None + + @override + def board(self) -> chess.Board: + return self.headers.board() + + @override + def ply(self) -> int: + # Optimization: Parse FEN only for custom starting positions. + return self.board().ply() if "FEN" in self.headers else 0 + + def setup(self, board: Union[chess.Board, str]) -> None: """ - Setup a specific starting position. This sets (or resets) the + Sets up a specific starting position. This sets (or resets) the ``FEN``, ``SetUp``, and ``Variant`` header tags. """ try: - fen = board.fen() + fen = board.fen() # type: ignore + setup = typing.cast(chess.Board, board) except AttributeError: - board = chess.Board(board) - board.chess960 = board.has_chess960_castling_rights() - fen = board.fen() + setup = chess.Board(board) # type: ignore + setup.chess960 = setup.has_chess960_castling_rights() + fen = setup.fen() - if fen == type(board).starting_fen: - self.headers.pop("SetUp", None) + if fen == type(setup).starting_fen: self.headers.pop("FEN", None) + self.headers.pop("SetUp", None) else: - self.headers["SetUp"] = "1" self.headers["FEN"] = fen + self.headers["SetUp"] = "1" - if type(board).aliases[0] == "Standard" and board.chess960: + if type(setup).aliases[0] == "Standard" and setup.chess960: self.headers["Variant"] = "Chess960" - elif type(board).aliases[0] != "Standard": - self.headers["Variant"] = type(board).aliases[0] - self.headers["FEN"] = board.fen() + elif type(setup).aliases[0] != "Standard": + self.headers["Variant"] = type(setup).aliases[0] + self.headers["FEN"] = fen else: self.headers.pop("Variant", None) - def accept(self, visitor): + @override + def accept(self, visitor: BaseVisitor[ResultT]) -> ResultT: """ Traverses the game in PGN order using the given *visitor*. Returns - the visitor result. + the *visitor* result. """ - visitor.begin_game() + if visitor.begin_game() is not SKIP: + for tagname, tagvalue in self.headers.items(): + visitor.visit_header(tagname, tagvalue) + if visitor.end_headers() is not SKIP: + board = self.board() + visitor.visit_board(board) - visitor.begin_headers() - for tagname, tagvalue in self.headers.items(): - visitor.visit_header(tagname, tagvalue) - visitor.end_headers() + if self.comments: + visitor.visit_comment(self.comments) - if self.comment: - visitor.visit_comment(self.comment) + if self.variations: + self.variations[0]._accept(board, visitor) - super(Game, self).accept(visitor, _board=self.board()) + visitor.visit_result(self.headers.get("Result", "*")) - visitor.visit_result(self.headers.get("Result", "*")) visitor.end_game() return visitor.result() + def time_control(self) -> TimeControl: + """ + Returns the time control of the game. If the game has no time control + information, the default time control ('UNKNOWN') is returned. + """ + time_control_header = self.headers.get("TimeControl", "") + return parse_time_control(time_control_header) + @classmethod - def from_board(cls, board): + def from_board(cls: Type[GameT], board: chess.Board) -> GameT: """Creates a game from the move stack of a :class:`~chess.Board()`.""" - # Undo all moves. - switchyard = collections.deque() - while board.move_stack: - switchyard.append(board.pop()) - - # Setup initial position. + # Setup the initial position. game = cls() - game.setup(board) - node = game + game.setup(board.root()) + node: GameNode = game # Replay all moves. - while switchyard: - move = switchyard.pop() + for move in board.move_stack: node = node.add_variation(move) - board.push(move) game.headers["Result"] = board.result() return game + @classmethod + def without_tag_roster(cls: Type[GameT]) -> GameT: + """Creates an empty game without the default Seven Tag Roster.""" + return cls(headers={}) + + @classmethod + def builder(cls: Type[GameT]) -> GameBuilder[GameT]: + return GameBuilder(Game=cls) + + def __repr__(self) -> str: + return "<{} at {:#x} ({!r} vs. {!r}, {!r} at {!r}{})>".format( + type(self).__name__, + id(self), + self.headers.get("White", "?"), + self.headers.get("Black", "?"), + self.headers.get("Date", "????.??.??"), + self.headers.get("Site", "?"), + f", {len(self.errors)} errors" if self.errors else "") + + +HeadersT = TypeVar("HeadersT", bound="Headers") + +class Headers(MutableMapping[str, str]): + def __init__(self, data: Optional[Union[Mapping[str, str], Iterable[Tuple[str, str]]]] = None, **kwargs: str) -> None: + self._tag_roster: Dict[str, str] = {} + self._others: Dict[str, str] = {} + + if data is None: + data = { + "Event": "?", + "Site": "?", + "Date": "????.??.??", + "Round": "?", + "White": "?", + "Black": "?", + "Result": "*" + } + + self.update(data, **kwargs) + + def is_chess960(self) -> bool: + return self.get("Variant", "").lower() in [ + "chess960", + "chess 960", + "fischerandom", # Cute Chess + "fischerrandom", + "fischer random", + ] + + def is_wild(self) -> bool: + # http://www.freechess.org/Help/HelpFiles/wild.html + return self.get("Variant", "").lower() in [ + "wild/0", "wild/1", "wild/2", "wild/3", "wild/4", "wild/5", + "wild/6", "wild/7", "wild/8", "wild/8a"] + + def variant(self) -> Type[chess.Board]: + if "Variant" not in self or self.is_chess960() or self.is_wild(): + return chess.Board + else: + from chess.variant import find_variant + return find_variant(self["Variant"]) + + def board(self) -> chess.Board: + VariantBoard = self.variant() + fen = self.get("FEN", VariantBoard.starting_fen) + board = VariantBoard(fen, chess960=self.is_chess960()) + board.chess960 = board.chess960 or board.has_chess960_castling_rights() + return board + + def __setitem__(self, key: str, value: str) -> None: + if key in TAG_ROSTER: + self._tag_roster[key] = value + elif not TAG_NAME_REGEX.match(key): + raise ValueError(f"invalid pgn header tag: {key!r}") + elif "\n" in value or "\r" in value: + raise ValueError(f"line break in pgn header {key}: {value!r}") + else: + self._others[key] = value + + def __getitem__(self, key: str) -> str: + return self._tag_roster[key] if key in TAG_ROSTER else self._others[key] + + def __delitem__(self, key: str) -> None: + if key in TAG_ROSTER: + del self._tag_roster[key] + else: + del self._others[key] + + def __iter__(self) -> Iterator[str]: + for key in TAG_ROSTER: + if key in self._tag_roster: + yield key -class BaseVisitor(object): + yield from self._others + + def __len__(self) -> int: + return len(self._tag_roster) + len(self._others) + + def copy(self) -> Self: + return type(self)(self) + + def __copy__(self) -> Self: + return self.copy() + + def __repr__(self) -> str: + return "{}({})".format( + type(self).__name__, + ", ".join("{}={!r}".format(key, value) for key, value in self.items())) + + @classmethod + def builder(cls: Type[HeadersT]) -> HeadersBuilder[HeadersT]: + return HeadersBuilder(Headers=cls) + + +MainlineMapT = TypeVar("MainlineMapT") + +class Mainline(Generic[MainlineMapT]): + def __init__(self, start: GameNode, f: Callable[[ChildNode], MainlineMapT]) -> None: + self.start = start + self.f = f + + def __bool__(self) -> bool: + return bool(self.start.variations) + + def __iter__(self) -> Iterator[MainlineMapT]: + node = self.start + while node.variations: + node = node.variations[0] + yield self.f(node) + + def __reversed__(self) -> Iterator[MainlineMapT]: + node = self.start.end() + while node.parent and node != self.start: + yield self.f(typing.cast(ChildNode, node)) + node = node.parent + + def accept(self, visitor: BaseVisitor[ResultT]) -> ResultT: + node = self.start + board = self.start.board() + while node.variations: + node = node.variations[0] + node._accept_node(board, visitor) + board.push(node.move) + return visitor.result() + + def __str__(self) -> str: + return self.accept(StringExporter(columns=None)) + + def __repr__(self) -> str: + return f"" + + +class BaseVisitor(abc.ABC, Generic[ResultT]): """ Base class for visitors. Use with :func:`chess.pgn.Game.accept()` or - :func:`chess.pgn.GameNode.accept()`. + :func:`chess.pgn.GameNode.accept()` or :func:`chess.pgn.read_game()`. - Methods are called in PGN order. + The methods are called in PGN order. """ - def begin_game(self): + def begin_game(self) -> Optional[SkipType]: """Called at the start of a game.""" pass - def begin_headers(self): - """Called at the start of the game headers.""" + def begin_headers(self) -> Optional[Headers]: + """Called before visiting game headers.""" pass - def visit_header(self, tagname, tagvalue): + def visit_header(self, tagname: str, tagvalue: str) -> None: """Called for each game header.""" pass - def end_headers(self): - """Called at the end of the game headers.""" + def end_headers(self) -> Optional[SkipType]: + """Called after visiting game headers.""" + pass + + def begin_parse_san(self, board: chess.Board, san: str) -> Optional[SkipType]: + """ + When the visitor is used by a parser, this is called at the start of + each standard algebraic notation detailing a move. + """ pass - def visit_move(self, board, move): + def visit_move(self, board: chess.Board, move: chess.Move) -> None: """ Called for each move. @@ -529,179 +1142,316 @@ def visit_move(self, board, move): """ pass - def visit_comment(self, comment): + def visit_board(self, board: chess.Board) -> None: + """ + Called for the starting position of the game and after each move. + + The board state must be restored before the traversal continues. + """ + pass + + def visit_comment(self, comment: list[str]) -> None: """Called for each comment.""" pass - def visit_nag(self, nag): + def visit_nag(self, nag: int) -> None: """Called for each NAG.""" pass - def begin_variation(self): + def begin_variation(self) -> Optional[SkipType]: """ Called at the start of a new variation. It is not called for the mainline of the game. """ pass - def end_variation(self): + def end_variation(self) -> None: """Concludes a variation.""" pass - def visit_result(self, result): - """Called at the end of the game with the *Result*-header.""" + def visit_result(self, result: str) -> None: + """ + Called at the end of a game with the value from the ``Result`` header. + """ pass - def end_game(self): + def end_game(self) -> None: """Called at the end of a game.""" pass - def result(self): - """Called to get the result of the visitor. Defaults to ``True``.""" - return True + @abc.abstractmethod + def result(self) -> ResultT: + """Called to get the result of the visitor.""" - def handle_error(self, error): - """Called for errors encountered. Defaults to raising an exception.""" + def handle_error(self, error: Exception) -> None: + """Called for encountered errors. Defaults to raising an exception.""" raise error -class GameModelCreator(BaseVisitor): +class GameBuilder(BaseVisitor[GameT]): """ Creates a game model. Default visitor for :func:`~chess.pgn.read_game()`. """ - def __init__(self): - self.game = Game() + @typing.overload + def __init__(self: GameBuilder[Game]) -> None: ... + @typing.overload + def __init__(self, *, Game: Type[GameT]) -> None: ... + def __init__(self, *, Game: Any = Game) -> None: + self.Game = Game - self.variation_stack = [self.game] - self.starting_comment = "" + @override + def begin_game(self) -> None: + self.game: GameT = self.Game() + + self.variation_stack: List[GameNode] = [self.game] + self.starting_comments: list[str] = [] self.in_variation = False - def visit_header(self, tagname, tagvalue): + @override + def begin_headers(self) -> Headers: + return self.game.headers + + @override + def visit_header(self, tagname: str, tagvalue: str) -> None: self.game.headers[tagname] = tagvalue - def visit_nag(self, nag): + @override + def visit_nag(self, nag: int) -> None: self.variation_stack[-1].nags.add(nag) - def begin_variation(self): - self.variation_stack.append(self.variation_stack[-1].parent) + @override + def begin_variation(self) -> None: + parent = self.variation_stack[-1].parent + assert parent is not None, "begin_variation called, but root node on top of stack" + self.variation_stack.append(parent) self.in_variation = False - def end_variation(self): + @override + def end_variation(self) -> None: self.variation_stack.pop() - def visit_result(self, result): + @override + def visit_result(self, result: str) -> None: if self.game.headers.get("Result", "*") == "*": self.game.headers["Result"] = result - def visit_comment(self, comment): + @override + def visit_comment(self, comment: Union[str, list[str]]) -> None: + comments = _standardize_comments(comment) if self.in_variation or (self.variation_stack[-1].parent is None and self.variation_stack[-1].is_end()): # Add as a comment for the current node if in the middle of - # a variation. Add as a comment for the game, if the comment + # a variation. Add as a comment for the game if the comment # starts before any move. - new_comment = [self.variation_stack[-1].comment, comment] - self.variation_stack[-1].comment = "\n".join(new_comment).strip() + self.variation_stack[-1].comments.extend(comments) + self.variation_stack[-1].comments = list(filter(None, self.variation_stack[-1].comments)) else: - # Otherwise it is a starting comment. - new_comment = [self.starting_comment, comment] - self.starting_comment = "\n".join(new_comment).strip() + # Otherwise, it is a starting comment. + self.starting_comments.extend(comments) + self.starting_comments = list(filter(None, self.starting_comments)) - def visit_move(self, board, move): + @override + def visit_move(self, board: chess.Board, move: chess.Move) -> None: self.variation_stack[-1] = self.variation_stack[-1].add_variation(move) - self.variation_stack[-1].starting_comment = self.starting_comment - self.starting_comment = "" + self.variation_stack[-1].starting_comments = self.starting_comments + self.starting_comments = [] self.in_variation = True - def handle_error(self, error): + @override + def handle_error(self, error: Exception) -> None: """ Populates :data:`chess.pgn.Game.errors` with encountered errors and logs them. + + You can silence the log and handle errors yourself after parsing: + + >>> import chess.pgn + >>> import logging + >>> + >>> logging.getLogger("chess.pgn").setLevel(logging.CRITICAL) + >>> + >>> pgn = open("data/pgn/kasparov-deep-blue-1997.pgn") + >>> + >>> game = chess.pgn.read_game(pgn) + >>> game.errors # List of exceptions + [] + + You can also override this method to hook into error handling: + + >>> import chess.pgn + >>> + >>> class MyGameBuilder(chess.pgn.GameBuilder): + >>> def handle_error(self, error: Exception) -> None: + >>> pass # Ignore error + >>> + >>> pgn = open("data/pgn/kasparov-deep-blue-1997.pgn") + >>> + >>> game = chess.pgn.read_game(pgn, Visitor=MyGameBuilder) """ - LOGGER.exception("error during pgn parsing") + LOGGER.error("%s while parsing %r", error, self.game) self.game.errors.append(error) - def result(self): + @override + def result(self) -> GameT: """ Returns the visited :class:`~chess.pgn.Game()`. """ return self.game -class StringExporter(BaseVisitor): - """ - Allows exporting a game as a string. +class HeadersBuilder(BaseVisitor[HeadersT]): + """Collects headers into a dictionary.""" - >>> exporter = chess.pgn.StringExporter(headers=True, variations=True, comments=True) - >>> pgn_string = game.accept(exporter) + @typing.overload + def __init__(self: HeadersBuilder[Headers]) -> None: ... + @typing.overload + def __init__(self, *, Headers: Type[HeadersT]) -> None: ... + def __init__(self, *, Headers: Any = Headers) -> None: + self.Headers = Headers - Only *columns* characters are written per line. If *columns* is ``None`` - then the entire movetext will be on a single line. This does not affect - header tags and comments. + @override + def begin_headers(self) -> HeadersT: + self.headers: HeadersT = self.Headers({}) + return self.headers + + @override + def visit_header(self, tagname: str, tagvalue: str) -> None: + self.headers[tagname] = tagvalue - There will be no newlines at the end of the string. + @override + def end_headers(self) -> SkipType: + return SKIP + + @override + def result(self) -> HeadersT: + return self.headers + + +class BoardBuilder(BaseVisitor[chess.Board]): """ + Returns the final position of the game. The mainline of the game is + on the move stack. + """ + + @override + def begin_game(self) -> None: + self.skip_variation_depth = 0 + + @override + def begin_variation(self) -> SkipType: + self.skip_variation_depth += 1 + return SKIP - def __init__(self, columns=80, headers=True, comments=True, variations=True): + @override + def end_variation(self) -> None: + self.skip_variation_depth = max(self.skip_variation_depth - 1, 0) + + @override + def visit_board(self, board: chess.Board) -> None: + if not self.skip_variation_depth: + self.board = board + + @override + def result(self) -> chess.Board: + return self.board + + +class SkipVisitor(BaseVisitor[Literal[True]]): + """Skips a game.""" + + @override + def begin_game(self) -> SkipType: + return SKIP + + @override + def end_headers(self) -> SkipType: + return SKIP + + @override + def begin_variation(self) -> SkipType: + return SKIP + + @override + def result(self) -> Literal[True]: + return True + + +class StringExporterMixin: + def __init__(self, *, columns: Optional[int] = 80, headers: bool = True, comments: bool = True, variations: bool = True): self.columns = columns self.headers = headers self.comments = comments self.variations = variations + self.found_headers = False + self.force_movenumber = True - self.lines = [] + self.lines: List[str] = [] self.current_line = "" self.variation_depth = 0 - def flush_current_line(self): + def flush_current_line(self) -> None: if self.current_line: self.lines.append(self.current_line.rstrip()) self.current_line = "" - def write_token(self, token): + def write_token(self, token: str) -> None: if self.columns is not None and self.columns - len(self.current_line) < len(token): self.flush_current_line() self.current_line += token - def write_line(self, line=""): + def write_line(self, line: str = "") -> None: self.flush_current_line() self.lines.append(line.rstrip()) - def end_game(self): + def end_game(self) -> None: self.write_line() - def visit_header(self, tagname, tagvalue): - if self.headers: - self.write_line("[{0} \"{1}\"]".format(tagname, tagvalue)) + def begin_headers(self) -> None: + self.found_headers = False - def end_headers(self): + def visit_header(self, tagname: str, tagvalue: str) -> None: if self.headers: + self.found_headers = True + self.write_line(f"[{tagname} \"{tagvalue}\"]") + + def end_headers(self) -> None: + if self.found_headers: self.write_line() - def begin_variation(self): + def begin_variation(self) -> Optional[SkipType]: self.variation_depth += 1 if self.variations: self.write_token("( ") self.force_movenumber = True + return None + else: + return SKIP - def end_variation(self): + def end_variation(self) -> None: self.variation_depth -= 1 if self.variations: self.write_token(") ") self.force_movenumber = True - def visit_comment(self, comment): + def visit_comment(self, comment: Union[str, list[str]]) -> None: if self.comments and (self.variations or not self.variation_depth): - self.write_token("{ " + comment.replace("}", "").strip() + " } ") + def pgn_format(comments: list[str]) -> str: + edit = map(lambda s: s.replace("{", "").replace("}", ""), comments) + return " ".join(f"{{ {comment} }}" for comment in edit if comment) + + comments = _standardize_comments(comment) + self.write_token(pgn_format(comments) + " ") self.force_movenumber = True - def visit_nag(self, nag): + def visit_nag(self, nag: int) -> None: if self.comments and (self.variations or not self.variation_depth): self.write_token("$" + str(nag) + " ") - def visit_move(self, board, move): + def visit_move(self, board: chess.Board, move: chess.Move) -> None: if self.variations or not self.variation_depth: # Write the move number. if board.turn == chess.WHITE: @@ -714,195 +1464,316 @@ def visit_move(self, board, move): self.force_movenumber = False - def visit_result(self, result): + def visit_result(self, result: str) -> None: self.write_token(result + " ") - def result(self): + +class StringExporter(StringExporterMixin, BaseVisitor[str]): + """ + Allows exporting a game as a string. + + >>> import chess.pgn + >>> + >>> game = chess.pgn.Game() + >>> + >>> exporter = chess.pgn.StringExporter(headers=True, variations=True, comments=True) + >>> pgn_string = game.accept(exporter) + + Only *columns* characters are written per line. If *columns* is ``None``, + then the entire movetext will be on a single line. This does not affect + header tags and comments. + + There will be no newline characters at the end of the string. + """ + + @override + def result(self) -> str: if self.current_line: return "\n".join(itertools.chain(self.lines, [self.current_line.rstrip()])).rstrip() else: return "\n".join(self.lines).rstrip() - def __str__(self): + def __str__(self) -> str: return self.result() -class FileExporter(StringExporter): +class FileExporter(StringExporterMixin, BaseVisitor[int]): """ - Like a :class:`~chess.pgn.StringExporter`, but games are written directly - to a text file. + Acts like a :class:`~chess.pgn.StringExporter`, but games are written + directly into a text file. There will always be a blank line after each game. Handling encodings is up to the caller. - >>> new_pgn = open("new.pgn", "w", encoding="utf-8") + >>> import chess.pgn + >>> + >>> game = chess.pgn.Game() + >>> + >>> new_pgn = open("/dev/null", "w", encoding="utf-8") >>> exporter = chess.pgn.FileExporter(new_pgn) >>> game.accept(exporter) """ - def __init__(self, handle, columns=80, headers=True, comments=True, variations=True): - super(FileExporter, self).__init__(columns=columns, headers=headers, comments=comments, variations=variations) + def __init__(self, handle: TextIO, *, columns: Optional[int] = 80, headers: bool = True, comments: bool = True, variations: bool = True): + super().__init__(columns=columns, headers=headers, comments=comments, variations=variations) self.handle = handle - def flush_current_line(self): + @override + def begin_game(self) -> None: + self.written: int = 0 + super().begin_game() + + def flush_current_line(self) -> None: if self.current_line: - self.handle.write(self.current_line.rstrip()) - self.handle.write("\n") + self.written += self.handle.write(self.current_line.rstrip()) + self.written += self.handle.write("\n") self.current_line = "" - def write_line(self, line=""): + def write_line(self, line: str = "") -> None: self.flush_current_line() - self.handle.write(line.rstrip()) - self.handle.write("\n") + self.written += self.handle.write(line.rstrip()) + self.written += self.handle.write("\n") - def result(self): - return None + @override + def result(self) -> int: + return self.written - def __repr__(self): - return "".format(hex(id(self))) + def __repr__(self) -> str: + return f"" - def __str__(self): + def __str__(self) -> str: return self.__repr__() -def read_game(handle, Visitor=GameModelCreator): +@typing.overload +def read_game(handle: TextIO) -> Optional[Game]: ... +@typing.overload +def read_game(handle: TextIO, *, Visitor: Callable[[], BaseVisitor[ResultT]]) -> Optional[ResultT]: ... +def read_game(handle: TextIO, *, Visitor: Any = GameBuilder) -> Any: """ Reads a game from a file opened in text mode. + >>> import chess.pgn + >>> >>> pgn = open("data/pgn/kasparov-deep-blue-1997.pgn") + >>> >>> first_game = chess.pgn.read_game(pgn) >>> second_game = chess.pgn.read_game(pgn) >>> >>> first_game.headers["Event"] 'IBM Man-Machine, New York USA' - - By using text mode the parser does not need to handle encodings. It is the - callers responsibility to open the file with the correct encoding. - PGN files are ASCII or UTF-8 most of the time. So the following should - cover most relevant cases (ASCII, UTF-8, UTF-8 with BOM). - - >>> pgn = open("data/pgn/kasparov-deep-blue-1997.pgn", encoding="utf-8-sig") + >>> + >>> # Iterate through all moves and play them on a board. + >>> board = first_game.board() + >>> for move in first_game.mainline_moves(): + ... board.push(move) + ... + >>> board + Board('4r3/6P1/2p2P1k/1p6/pP2p1R1/P1B5/2P2K2/3r4 b - - 0 45') + + By using text mode, the parser does not need to handle encodings. It is the + caller's responsibility to open the file with the correct encoding. + PGN files are usually ASCII or UTF-8 encoded, sometimes with BOM (which + this parser automatically ignores). See :func:`open` for options to + deal with encoding errors. + + >>> pgn = open("data/pgn/kasparov-deep-blue-1997.pgn", encoding="utf-8") Use :class:`~io.StringIO` to parse games from a string. - >>> pgn_string = "1. e4 e5 2. Nf3 *" + >>> import io >>> - >>> try: - >>> from StringIO import StringIO # Python 2 - >>> except ImportError: - >>> from io import StringIO # Python 3 - >>> - >>> pgn = StringIO(pgn_string) + >>> pgn = io.StringIO("1. e4 e5 2. Nf3 *") >>> game = chess.pgn.read_game(pgn) The end of a game is determined by a completely blank line or the end of - the file. (Of course blank lines in comments are possible.) + the file. (Of course, blank lines in comments are possible). - According to the standard at least the usual 7 header tags are required - for a valid game. This parser also handles games without any headers just - fine. + According to the PGN standard, at least the usual seven header tags are + required for a valid game. This parser also handles games without any + headers just fine. The parser is relatively forgiving when it comes to errors. It skips over - tokens it can not parse. Any exceptions are logged. + tokens it can not parse. By default, any exceptions are logged and + collected in :data:`Game.errors `. This behavior can + be :func:`overridden `. - Returns the parsed game or ``None`` if the EOF is reached. + Returns the parsed game or ``None`` if the end of file is reached. """ visitor = Visitor() - dummy_game = Game() found_game = False - found_content = False - - line = handle.readline() + skipping_game = False + managed_headers: Optional[Headers] = None + unmanaged_headers: Optional[Headers] = None + board_stack: List[chess.Board] = [] + + # Ignore leading empty lines and comments. + line = handle.readline().lstrip("\ufeff") + while line.isspace() or line.startswith("%") or line.startswith(";"): + line = handle.readline() # Parse game headers. + consecutive_empty_lines = 0 while line: - # Skip empty lines and comments. - if line.isspace() or line.startswith("%"): + # Ignore comments. + if line.startswith("%") or line.startswith(";"): line = handle.readline() continue + # Ignore up to one consecutive empty line between headers. + if consecutive_empty_lines < 1 and line.isspace(): + consecutive_empty_lines += 1 + line = handle.readline() + continue + + # First token of the game. if not found_game: - visitor.begin_game() - visitor.begin_headers() found_game = True + skipping_game = visitor.begin_game() is SKIP + if not skipping_game: + managed_headers = visitor.begin_headers() + if not isinstance(managed_headers, Headers): + unmanaged_headers = Headers({}) - # Read header tags. - tag_match = TAG_REGEX.match(line) - if tag_match: - dummy_game.headers[tag_match.group(1)] = tag_match.group(2) - visitor.visit_header(tag_match.group(1), tag_match.group(2)) - else: + if not line.startswith("["): break - line = handle.readline() + consecutive_empty_lines = 0 - if found_game: - visitor.end_headers() + if not skipping_game: + tag_match = TAG_REGEX.match(line) + if tag_match: + visitor.visit_header(tag_match.group(1), tag_match.group(2)) + if unmanaged_headers is not None: + unmanaged_headers[tag_match.group(1)] = tag_match.group(2) + else: + # Ignore invalid or malformed headers. + line = handle.readline() + continue - # Get the next non-empty line. - while line.isspace(): line = handle.readline() - # Movetext parser state. - try: - board_stack = [dummy_game.board()] - except ValueError as error: - visitor.handle_error(error) - board_stack = [chess.Board()] + if not found_game: + return None - # Parse movetext. - while line: - read_next_line = True + if not skipping_game: + skipping_game = visitor.end_headers() is SKIP + + if not skipping_game: + # Chess variant. + headers = managed_headers if unmanaged_headers is None else unmanaged_headers + assert headers is not None, "got neither managed nor unmanaged headers" + try: + VariantBoard = headers.variant() + except ValueError as error: + visitor.handle_error(error) + VariantBoard = chess.Board + + # Initial position. + fen = headers.get("FEN", VariantBoard.starting_fen) + try: + board = VariantBoard(fen, chess960=headers.is_chess960()) + except ValueError as error: + visitor.handle_error(error) + skipping_game = True + else: + board.chess960 = board.chess960 or board.has_chess960_castling_rights() + board_stack = [board] + visitor.visit_board(board) + + # Fast path: Skip entire game. + if skipping_game: + in_comment = False + + while line: + if not in_comment: + if line.isspace(): + break + elif line.startswith("%"): + line = handle.readline() + continue + + for match in SKIP_MOVETEXT_REGEX.finditer(line): + token = match.group(0) + if token == "{": + in_comment = True + elif not in_comment and token == ";": + break + elif token == "}": + in_comment = False - if line.startswith("%"): - # Ignore comments. line = handle.readline() - continue - # An empty line is the end of a game. - if found_content and line.isspace(): - if found_game: + visitor.end_game() + return visitor.result() + + # Parse movetext. + skip_variation_depth = 0 + fresh_line = True + while line: + if fresh_line: + # Ignore comments. + if line.startswith("%") or line.startswith(";"): + line = handle.readline() + continue + # An empty line means the end of a game. + if line.isspace(): visitor.end_game() return visitor.result() - else: - return + fresh_line = True for match in MOVETEXT_REGEX.finditer(line): token = match.group(0) - if not found_game: - found_game = True - visitor.begin_game() - if token.startswith("{"): # Consume until the end of the comment. - line = token[1:] + start_index = 2 if token.startswith("{ ") else 1 + line = token[start_index:] + comment_lines = [] while line and "}" not in line: - comment_lines.append(line.rstrip()) + comment_lines.append(line) line = handle.readline() - end_index = line.find("}") - comment_lines.append(line[:end_index]) - if "}" in line: - line = line[end_index:] - else: - line = "" - visitor.visit_comment("\n".join(comment_lines).strip()) - - # Continue with the current or the next line. if line: - read_next_line = False + close_index = line.find("}") + end_index = close_index - 1 if close_index > 0 and line[close_index - 1] == " " else close_index + comment_lines.append(line[:end_index]) + line = line[close_index + 1:] + + if not skip_variation_depth: + visitor.visit_comment("".join(comment_lines)) + + # Continue with the current line. + fresh_line = False + break + elif token == "(": + if skip_variation_depth: + skip_variation_depth += 1 + elif board_stack[-1].move_stack: + if visitor.begin_variation() is SKIP: + skip_variation_depth = 1 + else: + board = board_stack[-1].copy() + board.pop() + board_stack.append(board) + elif token == ")": + if skip_variation_depth == 1: + skip_variation_depth = 0 + visitor.end_variation() + elif skip_variation_depth: + skip_variation_depth -= 1 + elif len(board_stack) > 1: + visitor.end_variation() + board_stack.pop() + elif skip_variation_depth: + continue + elif token.startswith(";"): break elif token.startswith("$"): # Found a NAG. - try: - nag = int(token[1:]) - except ValueError as error: - visitor.handle_error(error) - else: - visitor.visit_nag(nag) + visitor.visit_nag(int(token[1:])) elif token == "?": visitor.visit_nag(NAG_MISTAKE) elif token == "??": @@ -915,159 +1786,130 @@ def read_game(handle, Visitor=GameModelCreator): visitor.visit_nag(NAG_SPECULATIVE_MOVE) elif token == "?!": visitor.visit_nag(NAG_DUBIOUS_MOVE) - elif token == "(" and board_stack[-1].move_stack: - visitor.begin_variation() - - board = board_stack[-1].copy() - board.pop() - board_stack.append(board) - elif token == ")" and len(board_stack) > 1: - # Always leave at least the root node on the stack. - visitor.end_variation() - board_stack.pop() elif token in ["1-0", "0-1", "1/2-1/2", "*"] and len(board_stack) == 1: - # Found a result token. - found_content = True visitor.visit_result(token) else: - # Found a SAN token. - found_content = True - - # Replace zeros castling notation. - if token == "0-0": - token = "O-O" - elif token == "0-0-0": - token = "O-O-O" - - # Parse the SAN. - try: - move = board_stack[-1].parse_san(token) - except ValueError as error: - visitor.handle_error(error) - else: - visitor.visit_move(board_stack[-1], move) - board_stack[-1].push(move) - - if read_next_line: + # Parse SAN tokens. + if visitor.begin_parse_san(board_stack[-1], token) is not SKIP: + try: + move = board_stack[-1].parse_san(token) + except ValueError as error: + visitor.handle_error(error) + skip_variation_depth = 1 + else: + visitor.visit_move(board_stack[-1], move) + board_stack[-1].push(move) + visitor.visit_board(board_stack[-1]) + + if fresh_line: line = handle.readline() - if found_game: - visitor.end_game() - return visitor.result() + visitor.end_game() + return visitor.result() -def scan_headers(handle): +def read_headers(handle: TextIO) -> Optional[Headers]: """ - Scan a PGN file opened in text mode for game offsets and headers. - - Yields a tuple for each game. The first element is the offset. The second - element is an ordered dictionary of game headers. + Reads game headers from a PGN file opened in text mode. Skips the rest of + the game. Since actually parsing many games from a big file is relatively expensive, - this is a better way to look only for specific games and seek and parse - them later. + this is a better way to look only for specific games and then seek and + parse them later. This example scans for the first game with Kasparov as the white player. - >>> pgn = open("mega.pgn") - >>> for offset, headers in chess.pgn.scan_headers(pgn): - ... if "Kasparov" in headers["White"]: - ... kasparov_offset = offset + >>> import chess.pgn + >>> + >>> pgn = open("data/pgn/kasparov-deep-blue-1997.pgn") + >>> + >>> kasparov_offsets = [] + >>> + >>> while True: + ... offset = pgn.tell() + ... + ... headers = chess.pgn.read_headers(pgn) + ... if headers is None: ... break - - Then it can later be seeked an parsed. - - >>> pgn.seek(kasparov_offset) - >>> game = chess.pgn.read_game(pgn) - - This also works nicely with generators, scanning lazily only when the next - offset is required. - - >>> white_win_offsets = (offset for offset, headers in chess.pgn.scan_headers(pgn) - ... if headers["Result"] == "1-0") - >>> first_white_win = next(white_win_offsets) - >>> second_white_win = next(white_win_offsets) - - :warning: Be careful when seeking a game in the file while more offsets are - being generated. + ... + ... if "Kasparov" in headers.get("White", "?"): + ... kasparov_offsets.append(offset) + + Then it can later be seeked and parsed. + + >>> for offset in kasparov_offsets: + ... pgn.seek(offset) + ... chess.pgn.read_game(pgn) # doctest: +ELLIPSIS + 0 + + 1436 + + 3067 + """ - in_comment = False - - game_headers = None - game_pos = None - - last_pos = handle.tell() - line = handle.readline() + return read_game(handle, Visitor=HeadersBuilder) - while line: - # Skip single line comments. - if line.startswith("%"): - last_pos = handle.tell() - line = handle.readline() - continue - # Reading a header tag. Parse it and add it to the current headers. - if not in_comment and line.startswith("["): - tag_match = TAG_REGEX.match(line) - if tag_match: - if game_pos is None: - game_headers = collections.OrderedDict() - game_headers["Event"] = "?" - game_headers["Site"] = "?" - game_headers["Date"] = "????.??.??" - game_headers["Round"] = "?" - game_headers["White"] = "?" - game_headers["Black"] = "?" - game_headers["Result"] = "*" - - game_pos = last_pos - - game_headers[tag_match.group(1)] = tag_match.group(2) - - last_pos = handle.tell() - line = handle.readline() - continue - - # Reading movetext. Update parser state in_comment in order to skip - # comments that look like header tags. - if (not in_comment and "{" in line) or (in_comment and "}" in line): - in_comment = line.rfind("{") > line.rfind("}") - - # Reading movetext. If there were headers, previously, those are now - # complete and can be yielded. - if game_pos is not None: - yield game_pos, game_headers - game_pos = None +def skip_game(handle: TextIO) -> bool: + """ + Skips a game. Returns ``True`` if a game was found and skipped. + """ + return bool(read_game(handle, Visitor=SkipVisitor)) - last_pos = handle.tell() - line = handle.readline() - # Yield the headers of the last game. - if game_pos is not None: - yield game_pos, game_headers +def parse_time_control(time_control: str) -> TimeControl: + tc = TimeControl() + if not time_control: + return tc -def scan_offsets(handle): - """ - Scan a PGN file opened in text mode for game offsets. + if time_control.startswith("?"): + return tc - Yields the starting offsets of all the games, so that they can be seeked - later. This is just like :func:`~chess.pgn.scan_headers()` but more - efficient if you do not actually need the header information. + if time_control.startswith("-"): + tc.type = TimeControlType.UNLIMITED + return tc - The PGN standard requires each game to start with an ``Event`` tag. So does - this scanner. - """ - in_comment = False + def _parse_part(part: str) -> TimeControlPart: + tcp = TimeControlPart() - last_pos = handle.tell() - line = handle.readline() + moves_time, *bonus = part.split("+") - while line: - if not in_comment and line.startswith("[Event \""): - yield last_pos - elif (not in_comment and "{" in line) or (in_comment and "}" in line): - in_comment = line.rfind("{") > line.rfind("}") + if bonus: + _bonus = bonus[0] + if _bonus.lower().endswith("d"): + tcp.delay = float(_bonus[:-1]) + else: + tcp.increment = float(_bonus) - last_pos = handle.tell() - line = handle.readline() + moves, *time = moves_time.split("/") + if time: + tcp.moves = int(moves) + tcp.time = int(time[0]) + else: + tcp.moves = 0 + tcp.time = int(moves) + + return tcp + + tc.parts = [_parse_part(part) for part in time_control.split(":")] + + if len(tc.parts) > 1: + for part in tc.parts[:-1]: + if part.moves == 0: + raise ValueError("Only last part can be 'sudden death'.") + + # Classification according to https://www.fide.com/FIDE/handbook/LawsOfChess.pdf + # (Bullet added) + base_time = tc.parts[0].time + increment = tc.parts[0].increment + if (base_time + 60 * increment) < 3 * 60: + tc.type = TimeControlType.BULLET + elif (base_time + 60 * increment) < 15 * 60: + tc.type = TimeControlType.BLITZ + elif (base_time + 60 * increment) < 60 * 60: + tc.type = TimeControlType.RAPID + else: + tc.type = TimeControlType.STANDARD + + return tc diff --git a/chess/polyglot.py b/chess/polyglot.py index 360ab74bc..a7d6807c4 100644 --- a/chess/polyglot.py +++ b/chess/polyglot.py @@ -1,31 +1,18 @@ -# -*- coding: utf-8 -*- -# -# This file is part of the python-chess library. -# Copyright (C) 2012-2017 Niklas Fiekas -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . +from __future__ import annotations import chess import struct import os import mmap import random +import typing + +from types import TracebackType +from typing import Callable, Container, Iterator, List, NamedTuple, Optional, Type, Union + + +StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] -try: - import backport_collections as collections -except ImportError: - import collections ENTRY_STRUCT = struct.Struct(">QHHI") @@ -230,22 +217,22 @@ ] -class ZobristHasher(object): - def __init__(self, array): +class ZobristHasher: + def __init__(self, array: List[int]) -> None: assert len(array) >= 781 self.array = array - def hash_board(self, board): + def hash_board(self, board: chess.BaseBoard) -> int: zobrist_hash = 0 for pivot, squares in enumerate(board.occupied_co): for square in chess.scan_reversed(squares): - piece_index = (board.piece_type_at(square) - 1) * 2 + pivot + piece_index = (typing.cast(chess.PieceType, board.piece_type_at(square)) - 1) * 2 + pivot zobrist_hash ^= self.array[64 * piece_index + square] return zobrist_hash - def hash_castling(self, board): + def hash_castling(self, board: chess.Board) -> int: zobrist_hash = 0 # Hash in the castling flags. @@ -260,10 +247,10 @@ def hash_castling(self, board): return zobrist_hash - def hash_ep_square(self, board): + def hash_ep_square(self, board: chess.Board) -> int: # Hash in the en passant file. if board.ep_square: - # But only if theres actually a pawn ready to capture it. Legality + # But only if there's actually a pawn ready to capture it. Legality # of the potential capture is irrelevant. if board.turn == chess.WHITE: ep_mask = chess.shift_down(chess.BB_SQUARES[board.ep_square]) @@ -275,20 +262,20 @@ def hash_ep_square(self, board): return self.array[772 + chess.square_file(board.ep_square)] return 0 - def hash_turn(self, board): + def hash_turn(self, board: chess.Board) -> int: # Hash in the turn. return self.array[780] if board.turn == chess.WHITE else 0 - def __call__(self, board): + def __call__(self, board: chess.Board) -> int: return (self.hash_board(board) ^ self.hash_castling(board) ^ self.hash_ep_square(board) ^ self.hash_turn(board)) -def zobrist_hash(board, _hasher=ZobristHasher(POLYGLOT_RANDOM_ARRAY)): +def zobrist_hash(board: chess.Board, *, _hasher: Callable[[chess.Board], int] = ZobristHasher(POLYGLOT_RANDOM_ARRAY)) -> int: """ Calculates the Polyglot Zobrist hash of the position. - A zobrist hash is an XOR of pseudo random values picked from + A Zobrist hash is an XOR of pseudo-random values picked from an array. Which values are picked is decided by features of the position, such as piece positions, castling rights and en passant squares. @@ -296,84 +283,105 @@ def zobrist_hash(board, _hasher=ZobristHasher(POLYGLOT_RANDOM_ARRAY)): return _hasher(board) -class Entry(collections.namedtuple("Entry", ["key", "raw_move", "weight", "learn"])): - """An entry from a polyglot opening book.""" +class Entry(NamedTuple): + """An entry from a Polyglot opening book.""" - def move(self, chess960=False): - """Gets the move (as a :class:`~chess.Move` object).""" - # Extract source and target square. - to_square = self.raw_move & 0x3f - from_square = (self.raw_move >> 6) & 0x3f + key: int + """The Zobrist hash of the position.""" - # Extract the promotion type. - promotion_part = (self.raw_move >> 12) & 0x7 - promotion = promotion_part + 1 if promotion_part else None + raw_move: int + """ + The raw binary representation of the move. Use + :data:`~chess.polyglot.Entry.move` instead. + """ - # Convert castling moves. - if not chess960 and not promotion: - if from_square == chess.E1: - if to_square == chess.H1: - return chess.Move(chess.E1, chess.G1) - elif to_square == chess.A1: - return chess.Move(chess.E1, chess.C1) - elif from_square == chess.E8: - if to_square == chess.H8: - return chess.Move(chess.E8, chess.G8) - elif to_square == chess.A8: - return chess.Move(chess.E8, chess.C8) - - if promotion and from_square == to_square: - return chess.Move(from_square, to_square, drop=promotion) - else: - return chess.Move(from_square, to_square, promotion) + weight: int + """An integer value that can be used as the weight for this entry.""" + learn: int + """Another integer value that can be used for extra information.""" -class MemoryMappedReader(object): - """Maps a polyglot opening book to memory.""" + move: chess.Move + """The :class:`~chess.Move`.""" - def __init__(self, filename): - self.fd = os.open(filename, os.O_RDONLY | os.O_BINARY if hasattr(os, "O_BINARY") else os.O_RDONLY) +class _EmptyMmap(bytearray): + def size(self) -> int: + return 0 + + def close(self) -> None: + pass + + def madvise(self, option: int) -> None: + pass + + +def _randint(rng: Optional[random.Random], a: int, b: int) -> int: + return random.randint(a, b) if rng is None else rng.randint(a, b) + + +class MemoryMappedReader: + """Maps a Polyglot opening book to memory.""" + + def __init__(self, filename: StrOrBytesPath) -> None: + fd = os.open(filename, os.O_RDONLY | getattr(os, "O_BINARY", 0)) try: - self.mmap = mmap.mmap(self.fd, 0, access=mmap.ACCESS_READ) - except (ValueError, mmap.error): - # Can not memory map empty opening books. - self.mmap = None + self.mmap: Union[mmap.mmap, _EmptyMmap] = mmap.mmap(fd, 0, access=mmap.ACCESS_READ) + except (ValueError, OSError): + self.mmap = _EmptyMmap() # Workaround for empty opening books. + finally: + os.close(fd) + + if self.mmap.size() % ENTRY_STRUCT.size != 0: + raise IOError(f"invalid file size: ensure {filename!r} is a valid polyglot opening book") + + try: + # Unix + self.mmap.madvise(mmap.MADV_RANDOM) + except AttributeError: + pass - def __enter__(self): + def __enter__(self) -> MemoryMappedReader: return self - def __exit__(self, exc_type, exc_value, traceback): + def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]) -> None: return self.close() - def __len__(self): - if self.mmap is None: - return 0 - else: - return self.mmap.size() // ENTRY_STRUCT.size - - def __getitem__(self, key): - if self.mmap is None: - raise IndexError() + def __len__(self) -> int: + return self.mmap.size() // ENTRY_STRUCT.size - if key < 0: - key = len(self) + key + def __getitem__(self, index: int) -> Entry: + if index < 0: + index = len(self) + index try: - key, raw_move, weight, learn = ENTRY_STRUCT.unpack_from(self.mmap, key * ENTRY_STRUCT.size) + key, raw_move, weight, learn = ENTRY_STRUCT.unpack_from(self.mmap, index * ENTRY_STRUCT.size) except struct.error: raise IndexError() - return Entry(key, raw_move, weight, learn) + # Extract source and target square. + to_square = raw_move & 0x3f + from_square = (raw_move >> 6) & 0x3f - def __iter__(self): - i = 0 - size = len(self) - while i < size: + # Extract the promotion type. + promotion_part = (raw_move >> 12) & 0x7 + promotion = promotion_part + 1 if promotion_part else None + + # Piece drop. + if from_square == to_square: + promotion, drop = None, promotion + else: + drop = None + + # Entry with move (not normalized). + move = chess.Move(from_square, to_square, promotion, drop) + return Entry(key, raw_move, weight, learn, move) + + def __iter__(self) -> Iterator[Entry]: + for i in range(len(self)): yield self[i] - i += 1 - def bisect_key_left(self, key): + def bisect_key_left(self, key: int) -> int: lo = 0 hi = len(self) @@ -387,16 +395,17 @@ def bisect_key_left(self, key): return lo - def __contains__(self, entry): - return any(current == entry for current in self.find_all(entry.key, entry.weight)) + def __contains__(self, entry: Entry) -> bool: + return any(current == entry for current in self.find_all(entry.key, minimum_weight=entry.weight)) - def find_all(self, board, minimum_weight=1, exclude_moves=()): + def find_all(self, board: Union[chess.Board, int], *, minimum_weight: int = 1, exclude_moves: Container[chess.Move] = []) -> Iterator[Entry]: """Seeks a specific position and yields corresponding entries.""" try: - key = int(board) - board = None + key = int(board) # type: ignore + context: Optional[chess.Board] = None except (TypeError, ValueError): - key = zobrist_hash(board) + context = typing.cast(chess.Board, board) + key = zobrist_hash(context) i = self.bisect_key_left(key) size = len(self) @@ -411,37 +420,44 @@ def find_all(self, board, minimum_weight=1, exclude_moves=()): if entry.weight < minimum_weight: continue - if board: - move = entry.move(chess960=board.chess960) - elif exclude_moves: - move = entry.move() + if context: + move = context._from_chess960(context.chess960, entry.move.from_square, entry.move.to_square, entry.move.promotion, entry.move.drop) + entry = Entry(entry.key, entry.raw_move, entry.weight, entry.learn, move) - if exclude_moves and move in exclude_moves: + if exclude_moves and entry.move in exclude_moves: continue - if board and not board.is_legal(move): + if context and not context.is_legal(entry.move): continue yield entry - def find(self, board, minimum_weight=1, exclude_moves=()): + def find(self, board: Union[chess.Board, int], *, minimum_weight: int = 1, exclude_moves: Container[chess.Move] = []) -> Entry: """ - Finds the main entry for the given position or zobrist hash. + Finds the main entry for the given position or Zobrist hash. - The main entry is the first entry with the highest weight. + The main entry is the (first) entry with the highest weight. - By default entries with weight ``0`` are excluded. This is a common way - to delete entries from an opening book without compacting it. Pass + By default, entries with weight ``0`` are excluded. This is a common + way to delete entries from an opening book without compacting it. Pass *minimum_weight* ``0`` to select all entries. - :raises: :exc:`IndexError` if no entries are found. + :raises: :exc:`IndexError` if no entries are found. Use + :func:`~chess.polyglot.MemoryMappedReader.get()` if you prefer to + get ``None`` instead of an exception. """ try: - return max(self.find_all(board, minimum_weight, exclude_moves), key=lambda entry: entry.weight) + return max(self.find_all(board, minimum_weight=minimum_weight, exclude_moves=exclude_moves), key=lambda entry: entry.weight) except ValueError: raise IndexError() - def choice(self, board, minimum_weight=1, exclude_moves=(), random=random): + def get(self, board: Union[chess.Board, int], default: Optional[Entry] = None, *, minimum_weight: int = 1, exclude_moves: Container[chess.Move] = []) -> Optional[Entry]: + try: + return self.find(board, minimum_weight=minimum_weight, exclude_moves=exclude_moves) + except IndexError: + return default + + def choice(self, board: Union[chess.Board, int], *, minimum_weight: int = 1, exclude_moves: Container[chess.Move] = [], random: Optional[random.Random] = None) -> Entry: """ Uniformly selects a random entry for the given position. @@ -449,8 +465,8 @@ def choice(self, board, minimum_weight=1, exclude_moves=(), random=random): """ chosen_entry = None - for i, entry in enumerate(self.find_all(board, minimum_weight, exclude_moves)): - if chosen_entry is None or random.randint(0, i) == i: + for i, entry in enumerate(self.find_all(board, minimum_weight=minimum_weight, exclude_moves=exclude_moves)): + if chosen_entry is None or _randint(random, 0, i) == i: chosen_entry = entry if chosen_entry is None: @@ -458,7 +474,7 @@ def choice(self, board, minimum_weight=1, exclude_moves=(), random=random): return chosen_entry - def weighted_choice(self, board, exclude_moves=(), random=random): + def weighted_choice(self, board: Union[chess.Board, int], *, exclude_moves: Container[chess.Move] = [], random: Optional[random.Random] = None) -> Entry: """ Selects a random entry for the given position, distributed by the weights of the entries. @@ -469,7 +485,7 @@ def weighted_choice(self, board, exclude_moves=(), random=random): if not total_weights: raise IndexError() - choice = random.randint(0, total_weights - 1) + choice = _randint(random, 0, total_weights - 1) current_sum = 0 for entry in self.find_all(board, exclude_moves=exclude_moves): @@ -479,24 +495,26 @@ def weighted_choice(self, board, exclude_moves=(), random=random): assert False - def close(self): + def close(self) -> None: """Closes the reader.""" - if self.mmap is not None: - self.mmap.close() + self.mmap.close() - try: - os.close(self.fd) - except OSError: - pass - -def open_reader(path): +def open_reader(path: StrOrBytesPath) -> MemoryMappedReader: """ Creates a reader for the file at the given path. + The following example opens a book to find all entries for the start + position: + + >>> import chess + >>> import chess.polyglot + >>> + >>> board = chess.Board() + >>> >>> with chess.polyglot.open_reader("data/polyglot/performance.bin") as reader: ... for entry in reader.find_all(board): - ... print(entry.move(), entry.weight, entry.learn) + ... print(entry.move, entry.weight, entry.learn) e2e4 1 0 d2d4 1 0 c2c4 1 0 diff --git a/data/polyglot/empty.bin b/chess/py.typed similarity index 100% rename from data/polyglot/empty.bin rename to chess/py.typed diff --git a/chess/svg.py b/chess/svg.py index 0ddd28153..7e8facf99 100644 --- a/chess/svg.py +++ b/chess/svg.py @@ -1,86 +1,158 @@ -# -*- coding: utf-8 -*- -# -# This file is part of the python-chess library. -# Copyright (C) 2016-2017 Niklas Fiekas -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# Piece vector graphics are copyright (C) Colin M.L. Burnett -# and also licensed under the -# GNU General Public License. - -from __future__ import division +from __future__ import annotations -import chess import math - import xml.etree.ElementTree as ET -try: - import backport_collections as collections -except ImportError: - import collections +import chess + +from typing import Dict, Iterable, Optional, Tuple, Union +from chess import Color, IntoSquareSet, Square SQUARE_SIZE = 45 +MARGIN = 20 PIECES = { - "b": """""", - "k": """""", - "n": """""", - "p": """""", - "q": """""", - "r": """""", - "B": """""", - "K": """""", - "N": """""", - "P": """""", - "Q": """""", - "R": """""" + "b": """""", # noqa: E501 + "k": """""", # noqa: E501 + "n": """""", # noqa: E501 + "p": """""", # noqa: E501 + "q": """""", # noqa: E501 + "r": """""", # noqa: E501 + "B": """""", # noqa: E501 + "K": """""", # noqa: E501 + "N": """""", # noqa: E501 + "P": """""", # noqa: E501 + "Q": """""", # noqa: E501 + "R": """""", # noqa: E501 } -XX = """""" +COORDS = { + "1": """""", # noqa: E501 + "2": """""", # noqa: E501 + "3": """""", # noqa: E501 + "4": """""", # noqa: E501 + "5": """""", # noqa: E501 + "6": """""", # noqa: E501 + "7": """""", # noqa: E501 + "8": """""", # noqa: E501 + "a": """""", # noqa: E501 + "b": """""", # noqa: E501 + "c": """""", # noqa: E501 + "d": """""", # noqa: E501 + "e": """""", # noqa: E501 + "f": """""", # noqa: E501 + "g": """""", # noqa: E501 + "h": """""", # noqa: E501 +} -ARROWHEAD = """""" +XX = """""" # noqa: E501 -CHECK_GRADIENT = """""" +CHECK_GRADIENT = """""" # noqa: E501 DEFAULT_COLORS = { "square light": "#ffce9e", "square dark": "#d18b47", "square dark lastmove": "#aaa23b", "square light lastmove": "#cdd16a", + "margin": "#212121", + "inner border": "#111", + "outer border": "#111", + "coord": "#e5e5e5", + "arrow green": "#15781B80", + "arrow red": "#88202080", + "arrow yellow": "#e68f00b3", + "arrow blue": "#00308880", } -DEFAULT_STYLE = """\ -.check { - fill: url(#check_gradient); -} -""" - -class Arrow(collections.namedtuple("Arrow", ["tail", "head"])): +class Arrow: """Details of an arrow to be drawn.""" - pass + + tail: Square + """Start square of the arrow.""" + + head: Square + """End square of the arrow.""" + + color: str + """Arrow color.""" + + def __init__(self, tail: Square, head: Square, *, color: str = "green") -> None: + self.tail = tail + self.head = head + self.color = color + + def pgn(self) -> str: + """ + Returns the arrow in the format used by ``[%csl ...]`` and + ``[%cal ...]`` PGN annotations, e.g., ``Ga1`` or ``Ya2h2``. + + Colors other than ``red``, ``yellow``, and ``blue`` default to green. + """ + if self.color == "red": + color = "R" + elif self.color == "yellow": + color = "Y" + elif self.color == "blue": + color = "B" + else: + color = "G" + + if self.tail == self.head: + return f"{color}{chess.SQUARE_NAMES[self.tail]}" + else: + return f"{color}{chess.SQUARE_NAMES[self.tail]}{chess.SQUARE_NAMES[self.head]}" + + def __str__(self) -> str: + return self.pgn() + + def __repr__(self) -> str: + return f"Arrow({chess.SQUARE_NAMES[self.tail].upper()}, {chess.SQUARE_NAMES[self.head].upper()}, color={self.color!r})" + + @classmethod + def from_pgn(cls, pgn: str) -> Arrow: + """ + Parses an arrow from the format used by ``[%csl ...]`` and + ``[%cal ...]`` PGN annotations, e.g., ``Ga1`` or ``Ya2h2``. + + Also allows skipping the color prefix, defaulting to green. + + :raises: :exc:`ValueError` if the format is invalid. + """ + if pgn.startswith("G"): + color = "green" + pgn = pgn[1:] + elif pgn.startswith("R"): + color = "red" + pgn = pgn[1:] + elif pgn.startswith("Y"): + color = "yellow" + pgn = pgn[1:] + elif pgn.startswith("B"): + color = "blue" + pgn = pgn[1:] + else: + color = "green" + + tail = chess.parse_square(pgn[:2]) + head = chess.parse_square(pgn[2:]) if len(pgn) > 2 else tail + return cls(tail, head, color=color) -def _svg(viewbox, size): +class SvgWrapper(str): + def _repr_svg_(self) -> SvgWrapper: + return self + + def _repr_html_(self) -> SvgWrapper: + return self + + +def _svg(viewbox: int, size: Optional[int]) -> ET.Element: svg = ET.Element("svg", { "xmlns": "http://www.w3.org/2000/svg", - "version": "1.1", "xmlns:xlink": "http://www.w3.org/1999/xlink", - "viewBox": "0 0 %d %d" % (viewbox, viewbox), + "viewBox": f"0 0 {viewbox:d} {viewbox:d}", }) if size is not None: @@ -90,173 +162,354 @@ def _svg(viewbox, size): return svg -def _text(content, x, y, width, height): - t = ET.Element("text", { - "x": str(x + width // 2), - "y": str(y + height // 2), - "font-size": str(max(1, int(min(width, height) * 0.7))), - "text-anchor": "middle", - "alignment-baseline": "middle", - }) - t.text = content +def _attrs(attrs: Dict[str, Union[str, int, float, None]]) -> Dict[str, str]: + return {k: str(v) for k, v in attrs.items() if v is not None} + + +def _select_color(colors: Dict[str, str], color: str) -> Tuple[str, float]: + return _color(colors.get(color, DEFAULT_COLORS[color])) + + +def _color(color: str) -> Tuple[str, float]: + if color.startswith("#"): + try: + if len(color) == 5: + return color[:4], int(color[4], 16) / 0xf + elif len(color) == 9: + return color[:7], int(color[7:], 16) / 0xff + except ValueError: + pass # Ignore invalid hex value + return color, 1.0 + + +def _coord(text: str, x: int, y: int, width: int, height: int, horizontal: bool, margin: int, *, color: str, opacity: float) -> ET.Element: + scale = margin / MARGIN + + if horizontal: + x += int(width - scale * width) // 2 + else: + y += int(height - scale * height) // 2 + + t = ET.Element("g", _attrs({ + "transform": f"translate({x}, {y}) scale({scale}, {scale})", + "fill": color, + "stroke": color, + "opacity": opacity if opacity < 1.0 else None, + })) + t.append(ET.fromstring(COORDS[text])) return t -def piece(piece, size=None): +def piece(piece: chess.Piece, size: Optional[int] = None) -> str: """ - Renders the given :class:`chess.Piece` as an SVG. + Renders the given :class:`chess.Piece` as an SVG image. - >>> chess.svg.piece(chess.Piece.from_symbol("R")) + >>> import chess + >>> import chess.svg + >>> + >>> chess.svg.piece(chess.Piece.from_symbol("R")) # doctest: +SKIP .. image:: ../docs/wR.svg + :alt: R """ svg = _svg(SQUARE_SIZE, size) svg.append(ET.fromstring(PIECES[piece.symbol()])) - return ET.tostring(svg).decode("utf-8") - - -def board(board=None, squares=None, flipped=False, coordinates=True, lastmove=None, check=None, arrows=(), size=None, style=None): + return SvgWrapper(ET.tostring(svg).decode("utf-8")) + + +def board(board: Optional[chess.BaseBoard] = None, *, + orientation: Color = chess.WHITE, + lastmove: Optional[chess.Move] = None, + check: Optional[Square] = None, + arrows: Iterable[Union[Arrow, Tuple[Square, Square]]] = [], + fill: Dict[Square, str] = {}, + squares: Optional[IntoSquareSet] = None, + size: Optional[int] = None, + coordinates: bool = True, + colors: Dict[str, str] = {}, + borders: bool = False, + style: Optional[str] = None) -> str: """ - Renders a board with pieces and/or selected squares as an SVG. + Renders a board with pieces and/or selected squares as an SVG image. - :param board: A :class:`chess.BaseBoard` with pieces or ``None``. - :param squares: A :class:`chess.SquareSet` with selected squares. - :param flipped: Pass ``True`` to flip the board. - :param coordinates: Pass ``False`` to disable coordinates in the margin. + :param board: A :class:`chess.BaseBoard` for a chessboard with pieces, or + ``None`` (the default) for a chessboard without pieces. + :param orientation: The point of view, defaulting to ``chess.WHITE``. :param lastmove: A :class:`chess.Move` to be highlighted. - :param check: A square to be marked as check. - :param arrows: A list of :class:`~chess.svg.Arrow` objects or tuples - like ``(chess.E2, chess.E4)``. - :param size: Limit the SVG size by setting width and height of the image. - :param style: CSS to use instead of the default stylesheet. - + :param check: A square to be marked indicating a check. + :param arrows: A list of :class:`~chess.svg.Arrow` objects, like + ``[chess.svg.Arrow(chess.E2, chess.E4)]``, or a list of tuples, like + ``[(chess.E2, chess.E4)]``. An arrow from a square pointing to the same + square is drawn as a circle, like ``[(chess.E2, chess.E2)]``. + :param fill: A dictionary mapping squares to a colors that they should be + filled with. + :param squares: A :class:`chess.SquareSet` with selected squares to mark + with an X. + :param size: The size of the image in pixels (e.g., ``400`` for a 400 by + 400 board), or ``None`` (the default) for no size limit. + :param coordinates: Pass ``False`` to disable the coordinate margin. + :param colors: A dictionary to override default colors. Possible keys are + ``square light``, ``square dark``, ``square light lastmove``, + ``square dark lastmove``, ``margin``, ``coord``, ``inner border``, + ``outer border``, ``arrow green``, ``arrow blue``, ``arrow red``, + and ``arrow yellow``. Values should look like ``#ffce9e`` (opaque), + or ``#15781B80`` (transparent). + :param borders: Pass ``True`` to enable a border around the board and, + (if *coordinates* is enabled) the coordinate margin. + :param style: A CSS stylesheet to include in the SVG image. + + >>> import chess + >>> import chess.svg + >>> >>> board = chess.Board("8/8/8/8/4N3/8/8/8 w - - 0 1") - >>> squares = board.attacks(chess.E4) - >>> chess.svg.board(board=board, squares=squares) + >>> + >>> chess.svg.board( + ... board, + ... fill=dict.fromkeys(board.attacks(chess.E4), "#cc0000cc"), + ... arrows=[chess.svg.Arrow(chess.E4, chess.F6, color="#0000cccc")], + ... squares=chess.SquareSet(chess.BB_DARK_SQUARES & chess.BB_FILE_B), + ... size=350, + ... ) # doctest: +SKIP .. image:: ../docs/Ne4.svg + :alt: 8/8/8/8/4N3/8/8/8 """ - margin = 20 if coordinates else 0 - svg = _svg(8 * SQUARE_SIZE + 2 * margin, size) + inner_border = 1 if borders and coordinates else 0 + outer_border = 1 if borders else 0 + margin = 15 if coordinates else 0 + board_offset = inner_border + margin + outer_border + full_size = 2 * outer_border + 2 * margin + 2 * inner_border + 8 * SQUARE_SIZE + svg = _svg(full_size, size) - ET.SubElement(svg, "style").text = DEFAULT_STYLE if style is None else style + if style: + ET.SubElement(svg, "style").text = style + + if board: + desc = ET.SubElement(svg, "desc") + asciiboard = ET.SubElement(desc, "pre") + asciiboard.text = str(board) defs = ET.SubElement(svg, "defs") if board: - for color in chess.COLORS: + for piece_color in chess.COLORS: for piece_type in chess.PIECE_TYPES: - if board.pieces_mask(piece_type, color): - defs.append(ET.fromstring(PIECES[chess.Piece(piece_type, color).symbol()])) + if board.pieces_mask(piece_type, piece_color): + defs.append(ET.fromstring(PIECES[chess.Piece(piece_type, piece_color).symbol()])) + + squares = chess.SquareSet(squares) if squares else chess.SquareSet() if squares: defs.append(ET.fromstring(XX)) - if arrows: - defs.append(ET.fromstring(ARROWHEAD)) + if check is not None: defs.append(ET.fromstring(CHECK_GRADIENT)) + if outer_border: + outer_border_color, outer_border_opacity = _select_color(colors, "outer border") + ET.SubElement(svg, "rect", _attrs({ + "x": outer_border / 2, + "y": outer_border / 2, + "width": full_size - outer_border, + "height": full_size - outer_border, + "fill": "none", + "stroke": outer_border_color, + "stroke-width": outer_border, + "opacity": outer_border_opacity if outer_border_opacity < 1.0 else None, + })) + + if margin: + margin_color, margin_opacity = _select_color(colors, "margin") + ET.SubElement(svg, "rect", _attrs({ + "x": outer_border + margin / 2, + "y": outer_border + margin / 2, + "width": full_size - 2 * outer_border - margin, + "height": full_size - 2 * outer_border - margin, + "fill": "none", + "stroke": margin_color, + "stroke-width": margin, + "opacity": margin_opacity if margin_opacity < 1.0 else None, + })) + + if inner_border: + inner_border_color, inner_border_opacity = _select_color(colors, "inner border") + ET.SubElement(svg, "rect", _attrs({ + "x": outer_border + margin + inner_border / 2, + "y": outer_border + margin + inner_border / 2, + "width": full_size - 2 * outer_border - 2 * margin - inner_border, + "height": full_size - 2 * outer_border - 2 * margin - inner_border, + "fill": "none", + "stroke": inner_border_color, + "stroke-width": inner_border, + "opacity": inner_border_opacity if inner_border_opacity < 1.0 else None, + })) + + # Render coordinates. + if coordinates: + coord_color, coord_opacity = _select_color(colors, "coord") + for file_index, file_name in enumerate(chess.FILE_NAMES): + x = (file_index if orientation else 7 - file_index) * SQUARE_SIZE + board_offset + # Keep some padding here to separate the ascender from the border + svg.append(_coord(file_name, x, 1, SQUARE_SIZE, margin, True, margin, color=coord_color, opacity=coord_opacity)) + svg.append(_coord(file_name, x, full_size - outer_border - margin, SQUARE_SIZE, margin, True, margin, color=coord_color, opacity=coord_opacity)) + for rank_index, rank_name in enumerate(chess.RANK_NAMES): + y = (7 - rank_index if orientation else rank_index) * SQUARE_SIZE + board_offset + svg.append(_coord(rank_name, 0, y, margin, SQUARE_SIZE, False, margin, color=coord_color, opacity=coord_opacity)) + svg.append(_coord(rank_name, full_size - outer_border - margin, y, margin, SQUARE_SIZE, False, margin, color=coord_color, opacity=coord_opacity)) + + # Render board. for square, bb in enumerate(chess.BB_SQUARES): file_index = chess.square_file(square) rank_index = chess.square_rank(square) - x = (file_index if not flipped else 7 - file_index) * SQUARE_SIZE + margin - y = (7 - rank_index if not flipped else rank_index) * SQUARE_SIZE + margin + x = (file_index if orientation else 7 - file_index) * SQUARE_SIZE + board_offset + y = (7 - rank_index if orientation else rank_index) * SQUARE_SIZE + board_offset cls = ["square", "light" if chess.BB_LIGHT_SQUARES & bb else "dark"] if lastmove and square in [lastmove.from_square, lastmove.to_square]: cls.append("lastmove") - fill_color = DEFAULT_COLORS[" ".join(cls)] + square_color, square_opacity = _select_color(colors, " ".join(cls)) + cls.append(chess.SQUARE_NAMES[square]) - # Fill color is required for renderers without aabbccddeeffgghh1122334455667788 +
. . . . . . . .
+. . . . . . . .
+. . . . . . . .
+. . . . . . . .
+. . . . N . . .
+. . . . . . . .
+. . . . . . . .
+. . . . . . . .
diff --git a/docs/conf.py b/docs/conf.py index 6ec37ff79..98e926efa 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,15 +1,30 @@ -# -*- coding: utf-8 -*- - import sys import os # Import the chess module. -sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath("..")) import chess +# Do not resolve these. +autodoc_type_aliases = { + "Square": "chess.Square", + "Color": "chess.Color", + "PieceType": "chess.PieceType", + "Bitboard": "chess.Bitboard", + "IntoSquareSet": "chess.IntoSquareSet", +} + # Autodoc. -extensions = ["sphinx.ext.autodoc"] -autodoc_member_order = 'bysource' +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinx.ext.intersphinx", + "sphinxcontrib.jquery" +] +autodoc_member_order = "bysource" +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), +} # The suffix of source filenames. source_suffix = ".rst" @@ -19,7 +34,7 @@ # General information about the project. project = "python-chess" -copyright = "2014-2017, Niklas Fiekas" +copyright = "2014–2024, Niklas Fiekas" # The version. version = chess.__version__ @@ -33,5 +48,5 @@ pygments_style = "sphinx" # The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = "default" +# a list of built-in themes. +html_theme = "sphinx_rtd_theme" diff --git a/docs/core.rst b/docs/core.rst index 6344a67f3..8bb045bbb 100644 --- a/docs/core.rst +++ b/docs/core.rst @@ -7,55 +7,75 @@ Colors Constants for the side to move or the color of a piece. .. py:data:: chess.WHITE - :annotation: = True + :type: chess.Color + :value: True .. py:data:: chess.BLACK - :annotation: = False + :type: chess.Color + :value: False -You can get the opposite color using `not color`. +You can get the opposite *color* using ``not color``. Piece types ----------- .. py:data:: chess.PAWN - :annotation: = 1 + :type: chess.PieceType + :value: 1 .. py:data:: chess.KNIGHT - :annotation: = 2 + :type: chess.PieceType + :value: 2 .. py:data:: chess.BISHOP - :annotation: = 3 + :type: chess.PieceType + :value: 3 .. py:data:: chess.ROOK - :annotation: = 4 + :type: chess.PieceType + :value: 4 .. py:data:: chess.QUEEN - :annotation: = 5 + :type: chess.PieceType + :value: 5 .. py:data:: chess.KING - :annotation: = 6 + :type: chess.PieceType + :value: 6 + +.. autofunction:: chess.piece_symbol + +.. autofunction:: chess.piece_name Squares ------- .. py:data:: chess.A1 - :annotation: = 0 + :type: chess.Square + :value: 0 .. py:data:: chess.B1 - :annotation: = 1 + :type: chess.Square + :value: 1 and so on to .. py:data:: chess.G8 - :annotation: = 62 + :type: chess.Square + :value: 62 .. py:data:: chess.H8 - :annotation: = 63 + :type: chess.Square + :value: 63 .. py:data:: chess.SQUARES - :annotation: = [chess.A1, chess.B1, ..., chess.G8, chess.H8] + :value: [chess.A1, chess.B1, ..., chess.G8, chess.H8] .. py:data:: chess.SQUARE_NAMES - :annotation: = ['a1', 'b1', ..., 'g8', 'h8'] + :value: ['a1', 'b1', ..., 'g8', 'h8'] .. py:data:: chess.FILE_NAMES - :annotation: = ['a', 'b', ..., 'g', 'h'] + :value: ['a', 'b', ..., 'g', 'h'] .. py:data:: chess.RANK_NAMES - :annotation: = ['1', '2', ..., '7', '8'] + :value: ['1', '2', ..., '7', '8'] + +.. autofunction:: chess.parse_square + +.. autofunction:: chess.square_name .. autofunction:: chess.square @@ -65,6 +85,10 @@ and so on to .. autofunction:: chess.square_distance +.. autofunction:: chess.square_manhattan_distance + +.. autofunction:: chess.square_knight_distance + .. autofunction:: chess.square_mirror Pieces @@ -73,36 +97,12 @@ Pieces .. autoclass:: chess.Piece :members: - .. py:attribute:: piece_type - - The piece type. - - .. py:attribute:: color - - The piece color. - Moves ----- .. autoclass:: chess.Move :members: - .. py:attribute:: from_square - - The source square. - - .. py:attribute:: to_square - - The target square. - - .. py:attribute:: promotion - - The promotion piece type or ``Ǹone``. - - .. py:attribute:: drop - - The drop piece type or ``None``. - Board ----- @@ -112,86 +112,18 @@ Board .. autoclass:: chess.Board :members: + :exclude-members: set_piece_at, remove_piece_at, reset_board, set_board_fen, set_piece_map, set_chess960_pos, apply_transform - .. py:attribute:: turn - - The side to move. - - .. py:attribute:: castling_rights - - Bitmask of the rooks with castling rights. - - >>> white_castle_kingside = board.castling_rights & chess.BB_H1 - - Also see :func:`~chess.Board.has_castling_rights()`, - :func:`~chess.Board.has_kingside_castling_rights()`, - :func:`~chess.Board.has_queenside_castling_rights()`, - :func:`~chess.Board.has_chess960_castling_rights()`, - :func:`~chess.Board.clean_castling_rights()` and - :func:`~chess.Board.set_castling_fen()`. - - .. py:attribute:: ep_square - - The potential en passant square on the third or sixth rank or ``None``. - - Use :func:`~chess.Board.has_legal_en_passant()` to test if en passant - capturing would actually be possible on the next move. - - .. py:attribute:: fullmove_number - - Counts move pairs. Starts at `1` and is incremented after every move - of the black side. - - .. py:attribute:: halfmove_clock - - The number of half moves since the last capture or pawn move. - - .. py:attribute:: chess960 - - Whether the board is in Chess960 mode. In Chess960 castling moves are - represented as king moves to the corresponding rook square. - - .. py:attribute:: legal_moves - :annotation: = LegalMoveGenerator(self) - - A dynamic list of legal moves. - - The following operations do not just generate everything but map to - more efficient methods. - - >>> len(board.legal_moves) - 20 - - >>> bool(board.legal_moves) - True - - >>> move in board.legal_moves - True - - Wraps :func:`~chess.Board.generate_legal_moves()` and - :func:`~chess.Board.is_legal()`. - - .. py:attribute:: pseudo_legal_moves - :annotation: = PseudoLegalMoveGenerator(self) - - A dynamic list of pseudo legal moves, much like the legal move list. - - Pseudo legal moves might leave or put the king in check, but are - otherwise valid. Null moves are not pseudo legal. Castling moves are - only included if they are completely legal. - - Wraps :func:`~chess.Board.generate_pseudo_legal_moves()` and - :func:`~chess.Board.is_pseudo_legal()`. +.. autoclass:: chess.BaseBoard + :members: - .. py:attribute:: move_stack +Outcome +------- - The move stack. Use :func:`Board.push() `, - :func:`Board.pop() `, - :func:`Board.peek() ` and - :func:`Board.clear_stack() ` for - manipulation. +.. autoclass:: chess.Outcome + :members: -.. autoclass:: chess.BaseBoard +.. autoclass:: chess.Termination :members: Square sets @@ -202,31 +134,40 @@ Square sets Common integer masks are: -.. py:data:: chess.BB_VOID - :annotation: = 0 +.. py:data:: chess.BB_EMPTY + :type: chess.Bitboard + :value: 0 .. py:data:: chess.BB_ALL - :annotation: = 0xFFFFFFFFFFFFFFFF + :type: chess.Bitboard + :value: 0xFFFF_FFFF_FFFF_FFFF Single squares: .. py:data:: chess.BB_SQUARES - :annotation: = [chess.BB_A1, chess.BB_B1, ..., chess.BB_G8, chess.BB_H8] + :value: [chess.BB_A1, chess.BB_B1, ..., chess.BB_G8, chess.BB_H8] Ranks and files: .. py:data:: chess.BB_RANKS - :annotation: = [chess.BB_RANK_1, ..., chess.BB_RANK_8] + :value: [chess.BB_RANK_1, ..., chess.BB_RANK_8] .. py:data:: chess.BB_FILES - :annotation: = [chess.BB_FILE_A, ..., chess.BB_FILE_H] + :value: [chess.BB_FILE_A, ..., chess.BB_FILE_H] Other masks: .. py:data:: chess.BB_LIGHT_SQUARES - :annotation: = 0x55AA55AA55AA55AA + :type: chess.Bitboard + :value: 0x55AA_55AA_55AA_55AA .. py:data:: chess.BB_DARK_SQUARES - :annotation: = 0xAA55AA55AA55AA55 + :type: chess.Bitboard + :value: 0xAA55_AA55_AA55_AA55 .. py:data:: chess.BB_BACKRANKS - :annotation: = chess.BB_RANK_1 | chess.BB_RANK_8 + :value: chess.BB_RANK_1 | chess.BB_RANK_8 + +.. py:data:: chess.BB_CORNERS + :value: chess.BB_A1 | chess.BB_H1 | chess.BB_A8 | chess.BB_H8 +.. py:data:: chess.BB_CENTER + :value: chess.BB_D4 | chess.BB_E4 | chess.BB_D5 | chess.BB_E5 diff --git a/docs/engine.rst b/docs/engine.rst new file mode 100644 index 000000000..a326430e0 --- /dev/null +++ b/docs/engine.rst @@ -0,0 +1,313 @@ +UCI/XBoard engine communication +=============================== + +The `Universal chess interface (UCI) `_ +and `XBoard protocol `_ +are standards for communicating with chess engines. This module +implements an abstraction for playing moves and analysing positions with +both kinds of engines. + +.. warning:: + Many popular chess engines make no guarantees, not even memory + safety, when parameters and positions are not completely + :func:`valid `. This module tries to deal with + benign misbehaving engines, but ultimately they are executables running + on your system. + +The preferred way to use the API is with an +`asyncio `_ event loop. +The examples also show a synchronous wrapper +:class:`~chess.engine.SimpleEngine` that automatically spawns an event loop +in the background. +:class:`~chess.engine.SimpleEngine` methods block until there is a result. + +Playing +------- + +Example: Let Stockfish play against itself, 100 milliseconds per move. + +.. code-block:: python + :caption: Using synchronous :class:`~chess.engine.SimpleEngine` + + import chess + import chess.engine + + engine = chess.engine.SimpleEngine.popen_uci(r"C:\Users\xxxxx\Downloads\stockfish_14_win_x64\stockfish_14_win_x64_avx2.exe") + + board = chess.Board() + while not board.is_game_over(): + result = engine.play(board, chess.engine.Limit(time=0.1)) + board.push(result.move) + + engine.quit() + +.. code-block:: python + :caption: Using asyncio + + import asyncio + import chess + import chess.engine + + async def main() -> None: + transport, engine = await chess.engine.popen_uci(r"C:\Users\xxxxx\Downloads\stockfish_14_win_x64\stockfish_14_win_x64_avx2.exe") + + board = chess.Board() + while not board.is_game_over(): + result = await engine.play(board, chess.engine.Limit(time=0.1)) + board.push(result.move) + + await engine.quit() + + asyncio.run(main()) + +.. autoclass:: chess.engine.Protocol + :members: play + +.. autoclass:: chess.engine.Limit + :members: + +.. autoclass:: chess.engine.PlayResult + :members: + +.. autoclass:: chess.engine.Protocol + :members: send_opponent_information + +.. autoclass:: chess.engine.Opponent + :members: + +.. autoclass:: chess.engine.Protocol + :members: send_game_result + +Analysing and evaluating a position +----------------------------------- + +Example: + +.. code-block:: python + :caption: Using synchronous :class:`~chess.engine.SimpleEngine` + + import chess + import chess.engine + + engine = chess.engine.SimpleEngine.popen_uci("/usr/bin/stockfish") + + board = chess.Board() + info = engine.analyse(board, chess.engine.Limit(time=0.1)) + print("Score:", info["score"]) + # Score: PovScore(Cp(+20), WHITE) + + board = chess.Board("r1bqkbnr/p1pp1ppp/1pn5/4p3/2B1P3/5Q2/PPPP1PPP/RNB1K1NR w KQkq - 2 4") + info = engine.analyse(board, chess.engine.Limit(depth=20)) + print("Score:", info["score"]) + # Score: PovScore(Mate(+1), WHITE) + + engine.quit() + +.. code-block:: python + :caption: Using asyncio + + import asyncio + import chess + import chess.engine + + async def main() -> None: + transport, engine = await chess.engine.popen_uci("/usr/bin/stockfish") + + board = chess.Board() + info = await engine.analyse(board, chess.engine.Limit(time=0.1)) + print(info["score"]) + # Score: PovScore(Cp(+20), WHITE) + + board = chess.Board("r1bqkbnr/p1pp1ppp/1pn5/4p3/2B1P3/5Q2/PPPP1PPP/RNB1K1NR w KQkq - 2 4") + info = await engine.analyse(board, chess.engine.Limit(depth=20)) + print(info["score"]) + # Score: PovScore(Mate(+1), WHITE) + + await engine.quit() + + asyncio.run(main()) + +.. autoclass:: chess.engine.Protocol + :members: analyse + +.. autoclass:: chess.engine.InfoDict + +.. autoclass:: chess.engine.PovScore + :members: + +.. autoclass:: chess.engine.Score + :members: + +.. autoclass:: chess.engine.PovWdl + :members: + +.. autoclass:: chess.engine.Wdl + :members: + +Indefinite or infinite analysis +------------------------------- + +Example: Stream information from the engine and stop on an arbitrary condition. + +.. code-block:: python + :caption: Using synchronous :class:`~chess.engine.SimpleEngine` + + import chess + import chess.engine + + engine = chess.engine.SimpleEngine.popen_uci("/usr/bin/stockfish") + + with engine.analysis(chess.Board()) as analysis: + for info in analysis: + print(info.get("score"), info.get("pv")) + + # Arbitrary stop condition. + if info.get("seldepth", 0) > 20: + break + + engine.quit() + +.. code-block:: python + :caption: Using asyncio + + import asyncio + import chess + import chess.engine + + async def main() -> None: + transport, engine = await chess.engine.popen_uci("/usr/bin/stockfish") + + with await engine.analysis(chess.Board()) as analysis: + async for info in analysis: + print(info.get("score"), info.get("pv")) + + # Arbitrary stop condition. + if info.get("seldepth", 0) > 20: + break + + await engine.quit() + + asyncio.run(main()) + +.. autoclass:: chess.engine.Protocol + :members: analysis + +.. autoclass:: chess.engine.AnalysisResult + :members: + +.. autoclass:: chess.engine.BestMove + :members: + +Options +------- + +:func:`~chess.Protocol.configure()`, +:func:`~chess.Protocol.play()`, +:func:`~chess.Protocol.analyse()` and +:func:`~chess.Protocol.analysis()` accept a dictionary of options. + +.. code-block:: python + :caption: Using synchronous :class:`~chess.engine.SimpleEngine` + + import chess.engine + + engine = chess.engine.SimpleEngine.popen_uci("/usr/bin/stockfish") + + # Check available options. + engine.options["Hash"] + # Option(name='Hash', type='spin', default=16, min=1, max=131072, var=[]) + + # Set an option. + engine.configure({"Hash": 32}) + + # [...] + +.. code-block:: python + :caption: Using asyncio + + import asyncio + import chess.engine + + async def main() -> None: + transport, engine = await chess.engine.popen_uci("/usr/bin/stockfish") + + # Check available options. + print(engine.options["Hash"]) + # Option(name='Hash', type='spin', default=16, min=1, max=131072, var=[]) + + # Set an option. + await engine.configure({"Hash": 32}) + + # [...] + + asyncio.run(main()) + +.. autoclass:: chess.engine.Protocol + :members: options, configure + +.. autoclass:: chess.engine.Option + :members: + +Logging +------- + +Communication is logged with debug level on a logger named ``chess.engine``. +Debug logs are useful while troubleshooting. Please also provide them +when submitting bug reports. + +.. code:: python + + import logging + + # Enable debug logging. + logging.basicConfig(level=logging.DEBUG) + +AsyncSSH +-------- + +:class:`chess.engine.Protocol` can also be used with +`AsyncSSH `_ (since 1.16.0) +to communicate with an engine on a remote computer. + +.. code:: python + + import asyncio + import asyncssh + import chess + import chess.engine + + async def main() -> None: + async with asyncssh.connect("localhost") as conn: + channel, engine = await conn.create_subprocess(chess.engine.UciProtocol, "/usr/bin/stockfish") + await engine.initialize() + + # Play, analyse, ... + await engine.ping() + + asyncio.run(main()) + +Reference +--------- + +.. autoclass:: chess.engine.EngineError + +.. autoclass:: chess.engine.EngineTerminatedError + +.. autoclass:: chess.engine.AnalysisComplete + +.. autofunction:: chess.engine.popen_uci + +.. autofunction:: chess.engine.popen_xboard + +.. autoclass:: chess.engine.Protocol + :members: id, returncode, initialize, ping, quit + +.. autoclass:: chess.engine.UciProtocol + +.. autoclass:: chess.engine.XBoardProtocol + +.. autoclass:: chess.engine.SimpleEngine + :members: + +.. autoclass:: chess.engine.SimpleAnalysisResult + :members: diff --git a/docs/gaviota.rst b/docs/gaviota.rst index a6ca1d97a..2bcb68b05 100644 --- a/docs/gaviota.rst +++ b/docs/gaviota.rst @@ -5,21 +5,16 @@ Gaviota tablebases provide **WDL** (win/draw/loss) and **DTM** (depth to mate) information for all endgame positions with up to 5 pieces. Positions with castling rights are not included. -.. autofunction:: chess.gaviota.open_tablebases +.. warning:: + Ensure tablebase files match the known checksums. Maliciously crafted + tablebase files may cause denial of service with + :class:`~chess.gaviota.PythonTablebase` and memory unsafety with + :class:`~chess.gaviota.NativeTablebase`. -.. autoclass:: chess.gaviota.PythonTablebases - :members: - -backports.lzma --------------- - -For Python versions before 3.3 you have to install ``backports.lzma`` in order -to use the pure Python probing code. +.. autofunction:: chess.gaviota.open_tablebase -.. code-block:: shell - - sudo apt-get install liblzma-dev libpython2.7-dev - pip install backports.lzma +.. autoclass:: chess.gaviota.PythonTablebase + :members: libgtb ------ @@ -36,7 +31,7 @@ Otherwise the pure Python probing code is used. sudo make install -.. autofunction:: chess.gaviota.open_tablebases_native +.. autofunction:: chess.gaviota.open_tablebase_native -.. autoclass:: chess.gaviota.NativeTablebases +.. autoclass:: chess.gaviota.NativeTablebase :members: diff --git a/docs/images/clente-chess.png b/docs/images/clente-chess.png new file mode 100644 index 000000000..4eb6ba16d Binary files /dev/null and b/docs/images/clente-chess.png differ diff --git a/docs/images/cli-chess.png b/docs/images/cli-chess.png new file mode 100644 index 000000000..eaa7ff306 Binary files /dev/null and b/docs/images/cli-chess.png differ diff --git a/docs/images/crazyara.png b/docs/images/crazyara.png new file mode 100644 index 000000000..0b1a35d29 Binary files /dev/null and b/docs/images/crazyara.png differ diff --git a/docs/images/jcchess.png b/docs/images/jcchess.png new file mode 100644 index 000000000..4e3a0b529 Binary files /dev/null and b/docs/images/jcchess.png differ diff --git a/docs/images/maia.png b/docs/images/maia.png new file mode 100644 index 000000000..9ced0aace Binary files /dev/null and b/docs/images/maia.png differ diff --git a/docs/images/pettingzoo.png b/docs/images/pettingzoo.png new file mode 100644 index 000000000..3cc3829c8 Binary files /dev/null and b/docs/images/pettingzoo.png differ diff --git a/docs/images/syzygy.png b/docs/images/syzygy.png new file mode 100644 index 000000000..ab189833e Binary files /dev/null and b/docs/images/syzygy.png differ diff --git a/docs/index.rst b/docs/index.rst index 1bc779728..da8558da6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,9 +11,13 @@ Contents polyglot gaviota syzygy - uci + engine svg variant + +.. toctree:: + :maxdepth: 1 + changelog Indices and tables diff --git a/docs/pgn.rst b/docs/pgn.rst index 6989b9a06..dd5a6fa47 100644 --- a/docs/pgn.rst +++ b/docs/pgn.rst @@ -1,78 +1,28 @@ PGN parsing and writing ======================= -Game model ----------- - -Games are represented as a tree of moves. Each `GameNode` can have extra -information such as comments. The root node of a game -(`Game` extends `GameNode`) also holds general information, such as game -headers. - -.. autoclass:: chess.pgn.Game - :members: - - .. py:attribute:: headers - - A `collections.OrderedDict()` of game headers. - - .. py:attribute:: errors - - A list of illegal or ambiguous move errors encountered while parsing - the game. - -.. autoclass:: chess.pgn.GameNode - :members: - - .. py:attribute:: parent - - The parent node or `None` if this is the root node of the game. - - .. py:attribute:: move - - The move leading to this node or `None` if this is the root node of the - game. - - .. py:attribute:: nags - :annotation: = set() - - A set of NAGs as integers. NAGs always go behind a move, so the root - node of the game can have none. - - .. py:attribute:: comment - :annotation: = '' - - A comment that goes behind the move leading to this node. Comments - that occur before any move are assigned to the root node. - - .. py:attribute:: starting_comment - :annotation: = '' - - A comment for the start of a variation. Only nodes that - actually start a variation (`starts_variation()`) can have a starting - comment. The root node can not have a starting comment. - - .. py:attribute:: variations - - A list of child nodes. - Parsing ------- .. autofunction:: chess.pgn.read_game -.. autofunction:: chess.pgn.scan_headers - -.. autofunction:: chess.pgn.scan_offsets - Writing ------- -If you want to export your game game with all headers, comments and variations -you can use: - +If you want to export your game with all headers, comments and variations, +you can do it like this: + +>>> import chess +>>> import chess.pgn +>>> +>>> game = chess.pgn.Game() +>>> game.headers["Event"] = "Example" +>>> node = game.add_variation(chess.Move.from_uci("e2e4")) +>>> node = node.add_variation(chess.Move.from_uci("e7e5")) +>>> node.comment = "Comment" +>>> >>> print(game) -[Event "?"] +[Event "Example"] [Site "?"] [Date "????.??.??"] [Round "?"] @@ -84,11 +34,33 @@ you can use: Remember that games in files should be separated with extra blank lines. ->>> print(game, file=handle, end="\n\n") +>>> print(game, file=open("/dev/null", "w"), end="\n\n") Use the :class:`~chess.pgn.StringExporter()` or :class:`~chess.pgn.FileExporter()` visitors if you need more control. +Game model +---------- + +Games are represented as a tree of moves. Conceptually each node represents a +position of the game. The tree consists of one root node +(:class:`~chess.pgn.Game`, also holding game headers) and many child +nodes (:class:`~chess.pgn.ChildNode`). +Both extend :class:`~chess.pgn.GameNode`. + +.. note:: Some basic methods have complexity `O(n)` for a game with n moves. + When following a variation, it is often more efficient to use visitors + or incrementally update state (like board, ply counter, or turn). + +.. autoclass:: chess.pgn.GameNode + :members: + +.. autoclass:: chess.pgn.Game + :members: headers, errors, setup, accept, from_board, without_tag_roster + +.. autoclass:: chess.pgn.ChildNode + :members: parent, move, starting_comment, nags, san, uci, end + Visitors -------- @@ -99,21 +71,25 @@ Visitors are an advanced concept for game tree traversal. The following visitors are readily available. -.. autoclass:: chess.pgn.GameModelCreator - :members: +.. autoclass:: chess.pgn.GameBuilder + :members: handle_error, result + +.. autoclass:: chess.pgn.HeadersBuilder + +.. autoclass:: chess.pgn.BoardBuilder + +.. autoclass:: chess.pgn.SkipVisitor .. autoclass:: chess.pgn.StringExporter - :members: .. autoclass:: chess.pgn.FileExporter - :members: NAGs ---- Numeric anotation glyphs describe moves and positions using standardized codes that are understood by many chess programs. During PGN parsing, annotations -like ``!``, ``?``, ``!!``, etc. are also converted to NAGs. +like ``!``, ``?``, ``!!``, etc., are also converted to NAGs. .. autodata:: chess.pgn.NAG_GOOD_MOVE .. autodata:: chess.pgn.NAG_MISTAKE @@ -121,3 +97,12 @@ like ``!``, ``?``, ``!!``, etc. are also converted to NAGs. .. autodata:: chess.pgn.NAG_BLUNDER .. autodata:: chess.pgn.NAG_SPECULATIVE_MOVE .. autodata:: chess.pgn.NAG_DUBIOUS_MOVE + +Skimming +-------- + +These functions allow for quickly skimming games without fully parsing them. + +.. autofunction:: chess.pgn.read_headers + +.. autofunction:: chess.pgn.skip_game diff --git a/docs/polyglot.rst b/docs/polyglot.rst index 4f010d43c..18c94c426 100644 --- a/docs/polyglot.rst +++ b/docs/polyglot.rst @@ -6,29 +6,12 @@ Polyglot opening book reading .. autoclass:: chess.polyglot.Entry :members: - .. py:attribute:: key - - The Zobrist hash of the position. - - .. py:attribute:: raw_move - - The raw binary representation of the move. Use the - :func:`~chess.polyglot.Entry.move()` method to extract a move object - from this. - - .. py:attribute:: weight - - An integer value that can be used as the weight for this entry. - - .. py:attribute:: learn - - Another integer value that can be used for extra information. .. autoclass:: chess.polyglot.MemoryMappedReader :members: .. py:data:: chess.polyglot.POLYGLOT_RANDOM_ARRAY - :annotation: = [0x9D39247E33776D41, ..., 0xF8D626AAAF278509] + :value: [0x9D39247E33776D41, ..., 0xF8D626AAAF278509] Array of 781 polyglot compatible pseudo random values for Zobrist hashing. diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 000000000..c04e3554a --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +Sphinx==8.1.2 +sphinxcontrib-jquery==4.1 +sphinx-rtd-theme==3.0.1 diff --git a/docs/svg.rst b/docs/svg.rst index 8647c69a9..2893bed63 100644 --- a/docs/svg.rst +++ b/docs/svg.rst @@ -1,12 +1,15 @@ SVG rendering ============= -This module renders SVGs (mostly for IPython integration). Piece images -are copyright -`Colin M.L. Burnett `_ and -triple licensed under GFDL & BSD & GPL. - +The :mod:`chess.svg` module renders SVG Tiny 1.2 images +(mostly for IPython/Jupyter Notebook integration). +The piece images by +`Colin M.L. Burnett `_ are triple +licensed under the GFDL, BSD and GPL. .. autofunction:: chess.svg.piece .. autofunction:: chess.svg.board + +.. autoclass:: chess.svg.Arrow + :members: diff --git a/docs/syzygy.rst b/docs/syzygy.rst index f23f21970..d2ce4edb6 100644 --- a/docs/syzygy.rst +++ b/docs/syzygy.rst @@ -1,11 +1,15 @@ Syzygy endgame tablebase probing ================================ -Syzygy tablebases provide **WDL** (win/draw/loss) and **DTZ** (distance to -zero) information for all endgame positions with up to 6 pieces. Positions -with castling rights are not included. +Syzygy tablebases provide WDL\ :sub:`50` (win/draw/loss under the 50-move rule) and +DTZ\ :sub:`50`'' (distance to zeroing) information with rounding for all endgame +positions with up to 7 pieces. Positions with castling rights are not included. -.. autofunction:: chess.syzygy.open_tablebases +.. warning:: + Ensure tablebase files match the known checksums. Maliciously crafted + tablebase files may cause denial of service. -.. autoclass:: chess.syzygy.Tablebases +.. autofunction:: chess.syzygy.open_tablebase + +.. autoclass:: chess.syzygy.Tablebase :members: diff --git a/docs/uci.rst b/docs/uci.rst deleted file mode 100644 index 349e5e730..000000000 --- a/docs/uci.rst +++ /dev/null @@ -1,185 +0,0 @@ -UCI engine communication -======================== - -The `Universal Chess Interface`_ is a protocol for communicating with engines. - -.. autofunction:: chess.uci.popen_engine - -.. autofunction:: chess.uci.spur_spawn_engine - -.. autoclass:: chess.uci.Engine - :members: terminate, kill, is_alive - - .. py:attribute:: process - - The underlying operating system process. - - .. py:attribute:: name - - The name of the engine. Conforming engines should send this as - *id name* when they receive the initial *uci* command. - - .. py:attribute:: author - - The author, as sent via *id author*. Just like the name. - - .. py:attribute:: options - - A case insensitive dictionary of :ref:`options`. The engine should send - available options when it receives the initial *uci* command. - - .. py:attribute:: uciok - - :class:`threading.Event()` that will be set as soon as *uciok* was - received. By then :data:`~chess.uci.Engine.name`, - :data:`~chess.uci.Engine.author` and :data:`~chess.uci.Engine.options` - should be available. - - .. py:attribute:: return_code - - The return code of the operating system process. - - .. py:attribute:: terminated - - :class:`threading.Event()` that will be set as soon as the underyling - operating system process is terminated and the - :data:`~chess.uci.Engine.return_code` is available. - -UCI commands ------------- - -.. autoclass:: chess.uci.Engine - :members: uci, debug, isready, setoption, ucinewgame, position, go, stop, - ponderhit, quit - -:exc:`~chess.uci.EngineTerminatedException` is raised if the engine process is -no longer alive. - -Asynchronous communication --------------------------- - -By default all operations are executed synchronously and their result is -returned. For example - ->>> engine.go(movetime=2000) -BestMove(bestmove=Move.from_uci('e2e4'), ponder=None) - -will take about 2000 milliseconds. All UCI commands have an optional -*async_callback* argument. They will then immediately return a `Future`_ -and continue. - ->>> command = engine.go(movetime=2000, async_callback=True) ->>> command.done() -False ->>> command.result() # Synchronously wait for the command to finish -BestMove(bestmove=Move.from_uci('e2e4'), ponder=None) ->>> command.done() -True - -Instead of just passing *async_callback=True* a callback function may be -passed. It will be invoked **possibly on a different thread** as soon as the -command is completed. It takes the command future as a single argument. - ->>> def on_go_finished(command): -... # Will likely be executed on a different thread. -... bestmove, ponder = command.result() -... ->>> command = engine.go(movetime=2000, async_callback=on_go_finished) - -Note about castling moves -------------------------- - -There are different ways castling moves may be encoded. The normal way to do it -is ``e1g1`` for short castling. The same move would be ``e1h1`` in -*UCI_Chess960* mode. - -This is abstracted away by the UCI module, but if the engine supports it, it -is recommended to enable enable *UCI_Chess960* mode. - ->>> engine.setoption({"UCI_Chess960": True}) - -Info handler ------------- - -.. autoclass:: chess.uci.Score - :members: - - .. py:attribute:: cp - - Evaluation in centipawns or ``None``. - - .. py:attribute:: mate - - Mate in x or ``None``. Negative if the engine thinks it is going to be - mated. - - .. py:attribute:: lowerbound - - If the score is not exact but only a lowerbound. - - .. py:attribute:: upperbound - - If the score is only an upperbound. - -.. autoclass:: chess.uci.InfoHandler - :members: - - .. py:attribute:: info - - The default implementation stores all received information in this - dictionary. To get a consistent snapshot use the object as if it were - a :class:`threading.Lock()`. - - >>> # Start thinking. - >>> engine.go(infinite=True, async_callback=True) - - >>> # Wait a moment, then access a consistent snapshot. - >>> time.sleep(3) - >>> with info_handler: - ... if 1 in info_handler.info["score"]: - ... print("Score: ", info_handler.info["score"][1].cp) - ... print("Mate: ", info_handler.info["score"][1].mate) - Score: 34 - Mate: None - -.. _options: - -Options -------- - -.. autoclass:: chess.uci.Option - - .. py:attribute:: name - - The name of the option. - - .. py:attribute:: type - - The type of the option. - - Officially documented types are ``check`` for a boolean value, ``spin`` - for an integer value between a minimum and a maximum, ``combo`` for an - enumeration of predefined string values (one of which can be selected), - ``button`` for an action and ``string`` for a textfield. - - .. py:attribute:: default - - The default value of the option. - - There is no need to send a *setoption* command with the defaut value. - - .. py:attribute:: min - - The minimum integer value of a *spin* option. - - .. py:attribute:: max - - The maximum integer value of a *spin* option. - - .. py:attribute:: var - - A list of allows string values for a *combo* option. - - -.. _Universal Chess Interface: https://chessprogramming.wikispaces.com/UCI -.. _Future: https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Future diff --git a/docs/variant.rst b/docs/variant.rst index 86907244e..0a5515873 100644 --- a/docs/variant.rst +++ b/docs/variant.rst @@ -4,24 +4,24 @@ Variants python-chess supports several chess variants. >>> import chess.variant +>>> >>> board = chess.variant.GiveawayBoard() ->>> # General information about the variants +>>> # General information about the variants. >>> type(board).uci_variant 'giveaway' +>>> type(board).xboard_variant +'giveaway' >>> type(board).starting_fen 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 0 1' -See :func:`chess.Board.is_variant_end()`, :func:`~chess.Board.is_variant_win()` -:func:`~chess.Board.is_variant_draw()` :func:`~chess.Board.is_variant_loss()` -for special variant end conditions and results. - ================ ========================================= ============= ============ -Variant Board class UCI Syzygy +Variant Board class UCI/XBoard Syzygy ================ ========================================= ============= ============ -Standard :class:`chess.Board` chess .rtbw, .rtbz +Standard :class:`chess.Board` chess/normal .rtbw, .rtbz Suicide :class:`chess.variant.SuicideBoard` suicide .stbw, .stbz Giveaway :class:`chess.variant.GiveawayBoard` giveaway .gtbw, .gtbz +Antichess :class:`chess.variant.AntichessBoard` antichess .gtbw, .gtbz Atomic :class:`chess.variant.AtomicBoard` atomic .atbw, .atbz King of the Hill :class:`chess.variant.KingOfTheHillBoard` kingofthehill Racing Kings :class:`chess.variant.RacingKingsBoard` racingkings @@ -32,6 +32,20 @@ Crazyhouse :class:`chess.variant.CrazyhouseBoard` crazyhouse .. autofunction:: chess.variant.find_variant +Game end +-------- + +See :func:`chess.Board.is_variant_end()`, :func:`~chess.Board.is_variant_win()`, +:func:`~chess.Board.is_variant_draw()`, +or :func:`~chess.Board.is_variant_loss()` for special variant end conditions +and results. + +Note that if all of them return ``False``, the game may still be over and +decided by standard conditions like :func:`~chess.Board.is_checkmate()`, +:func:`~chess.Board.is_stalemate()`, +:func:`~chess.Board.is_insufficient_material()`, move counters, repetitions, +and legitimate claims. + Chess960 -------- @@ -45,22 +59,55 @@ See :func:`chess.BaseBoard.set_chess960_pos()`, :func:`~chess.BaseBoard.from_chess960_pos()` for dealing with Chess960 starting positions. -UCI ---- +Crazyhouse +---------- + +.. autoclass:: chess.variant.CrazyhousePocket + :members: + +.. autoclass:: chess.variant.CrazyhouseBoard + :members: legal_drop_squares + + .. py:attribute:: pockets + :value: [chess.variant.CrazyhousePocket(), chess.variant.CrazyhousePocket()] -Stockfish and other engines allow you to switch variants by setting the -``UCI_Variant`` option. + Pockets for each color. For example, ``board.pockets[chess.WHITE]`` + are the pocket pieces available to White. +Three-check +----------- + +.. autoclass:: chess.variant.ThreeCheckBoard + + .. py:attribute:: remaining_checks + :value: [3, 3] + + Remaining checks until victory for each color. For example, + ``board.remaining_checks[chess.WHITE] == 0`` implies that White has won. + +UCI/XBoard +---------- + +`Multi-Variant Stockfish`_ and other engines have an ``UCI_Variant`` option. +XBoard engines may declare support for ``variants``. +This is automatically managed. + +>>> import chess.engine +>>> +>>> engine = chess.engine.SimpleEngine.popen_uci("stockfish-mv") +>>> >>> board = chess.variant.RacingKingsBoard() ->>> engine.setoption({ -... "UCI_Variant": type(board).uci_variant, -... "UCI_Chess960": board.chess960 -... }) ->>> engine.position(board) +>>> result = engine.play(board, chess.engine.Limit(time=1.0)) Syzygy ------ Syzygy tablebases are available for suicide, giveaway and atomic chess. ->>> tables = chess.syzygy.open_tablebases("data/syzygy", VariantBoard=chess.variant.AtomicBoard) +>>> import chess.syzygy +>>> import chess.variant +>>> +>>> tables = chess.syzygy.open_tablebase("data/syzygy", VariantBoard=chess.variant.AtomicBoard) + + +.. _Multi-Variant Stockfish: https://github.com/ddugovic/Stockfish diff --git a/examples/bratko_kopec/bratko_kopec.py b/examples/bratko_kopec/bratko_kopec.py index 175633c39..2553f4399 100755 --- a/examples/bratko_kopec/bratko_kopec.py +++ b/examples/bratko_kopec/bratko_kopec.py @@ -1,131 +1,116 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- +#!/usr/bin/env python3 -"""Run an EPD test suite with an UCI engine.""" +"""Run an EPD test suite with a UCI engine.""" -from __future__ import print_function - -import chess -import chess.uci -import chess.variant -import time +import asyncio import argparse import itertools import logging import sys +import typing +from typing import List, Tuple, Type -def test_epd(engine, epd, VariantBoard, threads, movetime): - position = VariantBoard() - epd_info = position.set_epd(epd) - epd_string = "%s" % epd_info.get("id", position.fen()) - if "am" in epd_info: - epd_string = "%s (avoid %s)" % (epd_string, " and ".join(position.san(am) for am in epd_info["am"])) - if "bm" in epd_info: - epd_string = "%s (expect %s)" % (epd_string, " or ".join(position.san(bm) for bm in epd_info["bm"])) +import chess +import chess.engine +import chess.variant - engine.ucinewgame() - engine.setoption({ - "UCI_Variant": VariantBoard.uci_variant, - "Threads": threads - }) - engine.position(position) - enginemove, pondermove = engine.go(movetime=movetime) +def parse_epd(epd: str, VariantBoard: Type[chess.Board]) -> Tuple[chess.Board, str, List[chess.Move], List[chess.Move]]: + board, epd_info = VariantBoard.from_epd(epd) - if "am" in epd_info and enginemove in epd_info["am"]: - print("%s: %s | +0" % (epd_string, position.san(enginemove))) - return 0.0 - elif "bm" in epd_info and enginemove not in epd_info["bm"]: - print("%s: %s | +0" % (epd_string, position.san(enginemove))) - return 0.0 + description = str(epd_info.get("id", board.fen())) + + if "am" in epd_info: + am = typing.cast(List[chess.Move], epd_info["am"]) + description = "{} (avoid {})".format(description, " and ".join(board.san(m) for m in am)) else: - print("%s: %s | +1" % (epd_string, position.san(enginemove))) - return 1.0 + am = [] + if "bm" in epd_info: + bm = typing.cast(List[chess.Move], epd_info["bm"]) + description = "{} (expect {})".format(description, " or ".join(board.san(m) for m in bm)) + else: + bm = [] -def test_epd_with_fractional_scores(engine, epd, VariantBoard, threads, movetime): - info_handler = chess.uci.InfoHandler() - engine.info_handlers.append(info_handler) + return board, description, am, bm - position = VariantBoard() - epd_info = position.set_epd(epd) - epd_string = "%s" % epd_info.get("id", position.fen()) - if "am" in epd_info: - epd_string = "%s (avoid %s)" % (epd_string, " and ".join(position.san(am) for am in epd_info["am"])) - if "bm" in epd_info: - epd_string = "%s (expect %s)" % (epd_string, " or ".join(position.san(bm) for bm in epd_info["bm"])) - engine.ucinewgame() - engine.setoption({ - "UCI_Variant": VariantBoard.uci_variant, - "Threads": threads - }) - engine.position(position) +async def test_epd(engine: chess.engine.Protocol, epd: str, VariantBoard: Type[chess.Board], movetime: float) -> float: + board, description, am, bm = parse_epd(epd, VariantBoard) - # Search in background - search = engine.go(infinite=True, async_callback=True) + limit = chess.engine.Limit(time=movetime) + result = await engine.play(board, limit, game=object()) - score = 0.0 + if not result.move: + print(f"{description}: -- | +0") + return 0.0 + elif result.move in am: + print(f"{description}: {board.san(result.move)} | +0") + return 0.0 + elif bm and result.move not in bm: + print(f"{description}: {board.san(result.move)} | +0") + return 0.0 + else: + print(f"{description}: {board.san(result.move)} | +1") + return 1.0 - print("%s:" % epd_string, end=" ") - sys.stdout.flush() - for step in range(0, 3): - time.sleep(movetime / 4000.0) +async def test_epd_with_fractional_scores(engine: chess.engine.Protocol, epd: str, VariantBoard: Type[chess.Board], movetime: float) -> float: + board, description, am, bm = parse_epd(epd, VariantBoard) - # Assess the current principal variation. - with info_handler as info: - if 1 in info["pv"] and len(info["pv"][1]) >= 1: - move = info["pv"][1][0] - print("(%s)" % position.san(move), end=" ") - sys.stdout.flush() - if "am" in epd_info and move in epd_info["am"]: + # Start analysis. + score = 0.0 + print(f"{description}:", end=" ", flush=True) + analysis = await engine.analysis(board, game=object()) + + with analysis: + for step in range(0, 4): + await asyncio.sleep(movetime / 4) + + # Assess the current principal variation. + if "pv" in analysis.info and len(analysis.info["pv"]) >= 1: + move = analysis.info["pv"][0] + print(board.san(move), end=" ", flush=True) + if move in am: continue # fail - elif "bm" in epd_info and move not in epd_info["bm"]: + elif bm and move not in bm: continue # fail else: score = 1.0 / (4 - step) else: - print("(no pv)", end=" ") - sys.stdout.flush() - - # Assess the final best move by the engine. - time.sleep(movetime / 4000.0) - engine.stop() - enginemove, pondermove = search.result() - if "am" in epd_info and enginemove in epd_info["am"]: - pass # fail - elif "bm" in epd_info and enginemove not in epd_info["bm"]: - pass # fail - else: - score = 1.0 - - print("%s | +%g" % (position.san(enginemove), score)) + print("(no pv)", end=" ", flush=True) - engine.info_handlers.remove(info_handler) + # Done. + print(f"| +{score}") return score -if __name__ == "__main__": +async def main() -> None: # Parse command line arguments. parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("-e", "--engine", required=True, + + engine_group = parser.add_mutually_exclusive_group(required=True) + engine_group.add_argument("-u", "--uci", help="The UCI engine under test.") + engine_group.add_argument("-x", "--xboard", + help="The XBoard engine under test.") + parser.add_argument("epd", nargs="+", type=argparse.FileType("r"), help="EPD test suite(s).") parser.add_argument("-v", "--variant", default="standard", help="Use a non-standard chess variant.") parser.add_argument("-t", "--threads", default=1, type=int, help="Threads for use by the UCI engine.") - parser.add_argument("-m", "--movetime", default=1000, type=int, - help="Time to move in milliseconds.") + parser.add_argument("-m", "--movetime", default=1.0, type=float, + help="Time to move in seconds.") parser.add_argument("-s", "--simple", dest="test_epd", action="store_const", default=test_epd_with_fractional_scores, const=test_epd, help="Run in simple mode without fractional scores.") parser.add_argument("-d", "--debug", action="store_true", help="Show debug logs.") + args = parser.parse_args() # Configure logger. @@ -134,9 +119,16 @@ def test_epd_with_fractional_scores(engine, epd, VariantBoard, threads, movetime # Find variant. VariantBoard = chess.variant.find_variant(args.variant) - # Open engine. - engine = chess.uci.popen_engine(args.engine) - engine.uci() + # Open and configure engine. + engine: chess.engine.Protocol + if args.uci: + _, engine = await chess.engine.popen_uci(args.uci) + if args.threads > 1: + await engine.configure({"Threads": args.threads}) + else: + _, engine = await chess.engine.popen_xboard(args.xboard) + if args.threads > 1: + await engine.configure({"cores": args.threads}) # Run each test line. score = 0.0 @@ -150,10 +142,14 @@ def test_epd_with_fractional_scores(engine, epd, VariantBoard, threads, movetime continue # Run the actual test. - score += args.test_epd(engine, epd, VariantBoard, args.threads, args.movetime) + score += await args.test_epd(engine, epd, VariantBoard, args.movetime) count += 1 - engine.quit() + await engine.quit() print("-------------------------------") - print("%g / %d" % (score, count)) + print(f"{score} / {count}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/chess960_pos_list.py b/examples/chess960_pos_list.py index 5e3a605be..88fe23696 100755 --- a/examples/chess960_pos_list.py +++ b/examples/chess960_pos_list.py @@ -1,23 +1,20 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- +#!/usr/bin/env python3 """List all Chess960 starting positions.""" -from __future__ import print_function - import sys import timeit import chess -def main(bench_only=False): +def main(bench_only: bool = False) -> None: board = chess.Board.empty(chess960=True) - for sharnagl in range(0, 960): - board.set_chess960_pos(sharnagl) + for scharnagl in range(0, 960): + board.set_chess960_pos(scharnagl) if not bench_only: - print(str(sharnagl).rjust(3), board.fen()) + print(str(scharnagl).rjust(3), board.fen()) if __name__ == "__main__": diff --git a/examples/perft/3check.perft b/examples/perft/3check.perft new file mode 100644 index 000000000..291a67865 --- /dev/null +++ b/examples/perft/3check.perft @@ -0,0 +1,11 @@ +id 3check-kiwipete +epd r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 1+1 +perft 1 48 +perft 2 2039 +perft 3 97848 + +id 3check-castling +epd r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 1+1 +perft 1 26 +perft 2 562 +perft 3 13410 diff --git a/examples/perft/atomic.perft b/examples/perft/atomic.perft index ba1fbe66f..c34570d5e 100644 --- a/examples/perft/atomic.perft +++ b/examples/perft/atomic.perft @@ -31,9 +31,17 @@ id atomic960-castle-2 epd r3k1rR/5K2/8/8/8/8/8/8 b kq - perft 1 25 perft 2 282 -perft 3 6746 -perft 4 98644 -perft 5 2585160 +perft 3 6753 +perft 4 98729 +perft 5 2587730 + +id atomic960-castle-3 +epd Rr2k1rR/3K4/3p4/8/8/8/7P/8 w kq - +perft 1 21 +perft 2 465 +perft 3 10631 +perft 4 241478 +perft 5 5800275 id shakmaty-bench epd rn2kb1r/1pp1p2p/p2q1pp1/3P4/2P3b1/4PN2/PP3PPP/R2QKB1R b KQkq - diff --git a/examples/perft/chess960.perft b/examples/perft/chess960.perft new file mode 100644 index 000000000..12276f256 --- /dev/null +++ b/examples/perft/chess960.perft @@ -0,0 +1,8642 @@ +# https://www.chessprogramming.org/Chess960_Perft_Results +# https://github.com/AndyGrant/Ethereal/blob/master/src/perft/fischer.epd + +id 0 +epd bqnb1rkr/pp3ppp/3ppn2/2p5/5P2/P2P4/NPP1P1PP/BQ1BNRKR w HFhf - +perft 1 21 +perft 2 528 +perft 3 12189 +perft 4 326672 +perft 5 8146062 +perft 6 227689589 + +id 1 +epd 2nnrbkr/p1qppppp/8/1ppb4/6PP/3PP3/PPP2P2/BQNNRBKR w HEhe - +perft 1 21 +perft 2 807 +perft 3 18002 +perft 4 667366 +perft 5 16253601 +perft 6 590751109 + +id 2 +epd b1q1rrkb/pppppppp/3nn3/8/P7/1PPP4/4PPPP/BQNNRKRB w GE - +perft 1 20 +perft 2 479 +perft 3 10471 +perft 4 273318 +perft 5 6417013 +perft 6 177654692 + +id 3 +epd qbbnnrkr/2pp2pp/p7/1p2pp2/8/P3PP2/1PPP1KPP/QBBNNR1R w hf - +perft 1 22 +perft 2 593 +perft 3 13440 +perft 4 382958 +perft 5 9183776 +perft 6 274103539 + +id 4 +epd 1nbbnrkr/p1p1ppp1/3p4/1p3P1p/3Pq2P/8/PPP1P1P1/QNBBNRKR w HFhf - +perft 1 28 +perft 2 1120 +perft 3 31058 +perft 4 1171749 +perft 5 34030312 +perft 6 1250970898 + +id 5 +epd qnbnr1kr/ppp1b1pp/4p3/3p1p2/8/2NPP3/PPP1BPPP/QNB1R1KR w HEhe - +perft 1 29 +perft 2 899 +perft 3 26578 +perft 4 824055 +perft 5 24851983 +perft 6 775718317 + +id 6 +epd q1bnrkr1/ppppp2p/2n2p2/4b1p1/2NP4/8/PPP1PPPP/QNB1RRKB w ge - +perft 1 30 +perft 2 860 +perft 3 24566 +perft 4 732757 +perft 5 21093346 +perft 6 649209803 + +id 7 +epd qbn1brkr/ppp1p1p1/2n4p/3p1p2/P7/6PP/QPPPPP2/1BNNBRKR w HFhf - +perft 1 25 +perft 2 635 +perft 3 17054 +perft 4 465806 +perft 5 13203304 +perft 6 377184252 + +id 8 +epd qnnbbrkr/1p2ppp1/2pp3p/p7/1P5P/2NP4/P1P1PPP1/Q1NBBRKR w HFhf - +perft 1 24 +perft 2 572 +perft 3 15243 +perft 4 384260 +perft 5 11110203 +perft 6 293989890 + +id 9 +epd qn1rbbkr/ppp2p1p/1n1pp1p1/8/3P4/P6P/1PP1PPPK/QNNRBB1R w hd - +perft 1 28 +perft 2 811 +perft 3 23175 +perft 4 679699 +perft 5 19836606 +perft 6 594527992 + +id 10 +epd qnr1bkrb/pppp2pp/3np3/5p2/8/P2P2P1/NPP1PP1P/QN1RBKRB w GDg - +perft 1 33 +perft 2 823 +perft 3 26895 +perft 4 713420 +perft 5 23114629 +perft 6 646390782 + +id 11 +epd qb1nrkbr/1pppp1p1/1n3p2/p1B4p/8/3P1P1P/PPP1P1P1/QBNNRK1R w HEhe - +perft 1 31 +perft 2 855 +perft 3 25620 +perft 4 735703 +perft 5 21796206 +perft 6 651054626 + +id 12 +epd qnnbrk1r/1p1ppbpp/2p5/p4p2/2NP3P/8/PPP1PPP1/Q1NBRKBR w HEhe - +perft 1 26 +perft 2 790 +perft 3 21238 +perft 4 642367 +perft 5 17819770 +perft 6 544866674 + +id 13 +epd 1qnrkbbr/1pppppp1/p1n4p/8/P7/1P1N1P2/2PPP1PP/QN1RKBBR w HDhd - +perft 1 37 +perft 2 883 +perft 3 32187 +perft 4 815535 +perft 5 29370838 +perft 6 783201510 + +id 14 +epd qn1rkrbb/pp1p1ppp/2p1p3/3n4/4P2P/2NP4/PPP2PP1/Q1NRKRBB w FDfd - +perft 1 24 +perft 2 585 +perft 3 14769 +perft 4 356950 +perft 5 9482310 +perft 6 233468620 + +id 15 +epd bb1qnrkr/pp1p1pp1/1np1p3/4N2p/8/1P4P1/P1PPPP1P/BBNQ1RKR w HFhf - +perft 1 29 +perft 2 864 +perft 3 25747 +perft 4 799727 +perft 5 24219627 +perft 6 776836316 + +id 16 +epd bnqbnr1r/p1p1ppkp/3p4/1p4p1/P7/3NP2P/1PPP1PP1/BNQB1RKR w HF - +perft 1 26 +perft 2 889 +perft 3 24353 +perft 4 832956 +perft 5 23701014 +perft 6 809194268 + +id 17 +epd bnqnrbkr/1pp2pp1/p7/3pP2p/4P1P1/8/PPPP3P/BNQNRBKR w HEhe d6 +perft 1 31 +perft 2 984 +perft 3 28677 +perft 4 962591 +perft 5 29032175 +perft 6 1008880643 + +id 18 +epd b1qnrrkb/ppp1pp1p/n2p1Pp1/8/8/P7/1PPPP1PP/BNQNRKRB w GE - +perft 1 20 +perft 2 484 +perft 3 10532 +perft 4 281606 +perft 5 6718715 +perft 6 193594729 + +id 19 +epd n1bqnrkr/pp1ppp1p/2p5/6p1/2P2b2/PN6/1PNPPPPP/1BBQ1RKR w HFhf - +perft 1 23 +perft 2 732 +perft 3 17746 +perft 4 558191 +perft 5 14481581 +perft 6 457140569 + +id 20 +epd n1bb1rkr/qpnppppp/2p5/p7/P1P5/5P2/1P1PPRPP/NQBBN1KR w Hhf - +perft 1 27 +perft 2 697 +perft 3 18724 +perft 4 505089 +perft 5 14226907 +perft 6 400942568 + +id 21 +epd nqb1rbkr/pppppp1p/4n3/6p1/4P3/1NP4P/PP1P1PP1/1QBNRBKR w HEhe - +perft 1 28 +perft 2 641 +perft 3 18811 +perft 4 456916 +perft 5 13780398 +perft 6 354122358 + +id 22 +epd n1bnrrkb/pp1pp2p/2p2p2/6p1/5B2/3P4/PPP1PPPP/NQ1NRKRB w GE - +perft 1 28 +perft 2 606 +perft 3 16883 +perft 4 381646 +perft 5 10815324 +perft 6 254026570 + +id 23 +epd nbqnbrkr/2ppp1p1/pp3p1p/8/4N2P/1N6/PPPPPPP1/1BQ1BRKR w HFhf - +perft 1 26 +perft 2 626 +perft 3 17268 +perft 4 437525 +perft 5 12719546 +perft 6 339132046 + +id 24 +epd nq1bbrkr/pp2nppp/2pp4/4p3/1PP1P3/1B6/P2P1PPP/NQN1BRKR w HFhf - +perft 1 21 +perft 2 504 +perft 3 11812 +perft 4 302230 +perft 5 7697880 +perft 6 207028745 + +id 25 +epd nqnrb1kr/2pp1ppp/1p1bp3/p1B5/5P2/3N4/PPPPP1PP/NQ1R1BKR w HDhd - +perft 1 30 +perft 2 672 +perft 3 19307 +perft 4 465317 +perft 5 13454573 +perft 6 345445468 + +id 26 +epd nqn2krb/p1prpppp/1pbp4/7P/5P2/8/PPPPPKP1/NQNRB1RB w g - +perft 1 21 +perft 2 461 +perft 3 10608 +perft 4 248069 +perft 5 6194124 +perft 6 152861936 + +id 27 +epd nb1n1kbr/ppp1rppp/3pq3/P3p3/8/4P3/1PPPRPPP/NBQN1KBR w Hh - +perft 1 19 +perft 2 566 +perft 3 11786 +perft 4 358337 +perft 5 8047916 +perft 6 249171636 + +id 28 +epd nqnbrkbr/1ppppp1p/p7/6p1/6P1/P6P/1PPPPP2/NQNBRKBR w HEhe - +perft 1 20 +perft 2 382 +perft 3 8694 +perft 4 187263 +perft 5 4708975 +perft 6 112278808 + +id 29 +epd nq1rkb1r/pp1pp1pp/1n2bp1B/2p5/8/5P1P/PPPPP1P1/NQNRKB1R w HDhd - +perft 1 24 +perft 2 809 +perft 3 20090 +perft 4 673811 +perft 5 17647882 +perft 6 593457788 + +id 30 +epd nqnrkrb1/pppppp2/7p/4b1p1/8/PN1NP3/1PPP1PPP/1Q1RKRBB w FDfd - +perft 1 26 +perft 2 683 +perft 3 18102 +perft 4 473911 +perft 5 13055173 +perft 6 352398011 + +id 31 +epd bb1nqrkr/1pp1ppp1/pn5p/3p4/8/P2NNP2/1PPPP1PP/BB2QRKR w HFhf - +perft 1 29 +perft 2 695 +perft 3 21193 +perft 4 552634 +perft 5 17454857 +perft 6 483785639 + +id 32 +epd bnn1qrkr/pp1ppp1p/2p5/b3Q1p1/8/5P1P/PPPPP1P1/BNNB1RKR w HFhf - +perft 1 44 +perft 2 920 +perft 3 35830 +perft 4 795317 +perft 5 29742670 +perft 6 702867204 + +id 33 +epd bnnqrbkr/pp1p2p1/2p1p2p/5p2/1P5P/1R6/P1PPPPP1/BNNQRBK1 w Ehe - +perft 1 33 +perft 2 1022 +perft 3 32724 +perft 4 1024721 +perft 5 32898113 +perft 6 1047360456 + +id 34 +epd b1nqrkrb/2pppppp/p7/1P6/1n6/P4P2/1P1PP1PP/BNNQRKRB w GEge - +perft 1 23 +perft 2 638 +perft 3 15744 +perft 4 446539 +perft 5 11735969 +perft 6 344211589 + +id 35 +epd n1bnqrkr/3ppppp/1p6/pNp1b3/2P3P1/8/PP1PPP1P/NBB1QRKR w HFhf - +perft 1 29 +perft 2 728 +perft 3 20768 +perft 4 532084 +perft 5 15621236 +perft 6 415766465 + +id 36 +epd n2bqrkr/p1p1pppp/1pn5/3p1b2/P6P/1NP5/1P1PPPP1/1NBBQRKR w HFhf - +perft 1 20 +perft 2 533 +perft 3 12152 +perft 4 325059 +perft 5 8088751 +perft 6 223068417 + +id 37 +epd nnbqrbkr/1pp1p1p1/p2p4/5p1p/2P1P3/N7/PPQP1PPP/N1B1RBKR w HEhe - +perft 1 27 +perft 2 619 +perft 3 18098 +perft 4 444421 +perft 5 13755384 +perft 6 357222394 + +id 38 +epd nnbqrkr1/pp1pp2p/2p2b2/5pp1/1P5P/4P1P1/P1PP1P2/NNBQRKRB w GEge - +perft 1 32 +perft 2 1046 +perft 3 33721 +perft 4 1111186 +perft 5 36218182 +perft 6 1202830851 + +id 39 +epd nb1qbrkr/p1pppp2/1p1n2pp/8/1P6/2PN3P/P2PPPP1/NB1QBRKR w HFhf - +perft 1 25 +perft 2 521 +perft 3 14021 +perft 4 306427 +perft 5 8697700 +perft 6 201455191 + +id 40 +epd nnq1brkr/pp1pppp1/8/2p4P/8/5K2/PPPbPP1P/NNQBBR1R w hf - +perft 1 23 +perft 2 724 +perft 3 18263 +perft 4 571072 +perft 5 15338230 +perft 6 484638597 + +id 41 +epd nnqrbb1r/pppppk2/5pp1/7p/1P6/3P2PP/P1P1PP2/NNQRBBKR w HD - +perft 1 30 +perft 2 717 +perft 3 21945 +perft 4 547145 +perft 5 17166700 +perft 6 450069742 + +id 42 +epd nnqr1krb/p1p1pppp/2bp4/8/1p1P4/4P3/PPP2PPP/NNQRBKRB w GDgd - +perft 1 25 +perft 2 873 +perft 3 20796 +perft 4 728628 +perft 5 18162741 +perft 6 641708630 + +id 43 +epd nbnqrkbr/p2ppp2/1p4p1/2p4p/3P3P/3N4/PPP1PPPR/NB1QRKB1 w Ehe - +perft 1 24 +perft 2 589 +perft 3 15190 +perft 4 382317 +perft 5 10630667 +perft 6 279474189 + +id 44 +epd n1qbrkbr/p1ppp2p/2n2pp1/1p6/1P6/2P3P1/P2PPP1P/NNQBRKBR w HEhe - +perft 1 22 +perft 2 592 +perft 3 14269 +perft 4 401976 +perft 5 10356818 +perft 6 301583306 + +id 45 +epd 2qrkbbr/ppn1pppp/n1p5/3p4/5P2/P1PP4/1P2P1PP/NNQRKBBR w HDhd - +perft 1 27 +perft 2 750 +perft 3 20584 +perft 4 605458 +perft 5 16819085 +perft 6 516796736 + +id 46 +epd 1nqr1rbb/pppkp1pp/1n3p2/3p4/1P6/5P1P/P1PPPKP1/NNQR1RBB w - - +perft 1 24 +perft 2 623 +perft 3 15921 +perft 4 429446 +perft 5 11594634 +perft 6 322745925 + +id 47 +epd bbn1rqkr/pp1pp2p/4npp1/2p5/1P6/2BPP3/P1P2PPP/1BNNRQKR w HEhe - +perft 1 23 +perft 2 730 +perft 3 17743 +perft 4 565340 +perft 5 14496370 +perft 6 468608864 + +id 48 +epd bn1brqkr/pppp2p1/3npp2/7p/PPP5/8/3PPPPP/BNNBRQKR w HEhe - +perft 1 25 +perft 2 673 +perft 3 17835 +perft 4 513696 +perft 5 14284338 +perft 6 434008567 + +id 49 +epd bn1rqbkr/ppp1ppp1/1n6/2p4p/7P/3P4/PPP1PPP1/BN1RQBKR w HDhd - +perft 1 25 +perft 2 776 +perft 3 20562 +perft 4 660217 +perft 5 18486027 +perft 6 616653869 + +id 50 +epd bnnr1krb/ppp2ppp/3p4/3Bp3/q1P3PP/8/PP1PPP2/BNNRQKR1 w GDgd - +perft 1 29 +perft 2 1040 +perft 3 30772 +perft 4 1053113 +perft 5 31801525 +perft 6 1075147725 + +id 51 +epd 1bbnrqkr/pp1ppppp/8/2p5/n7/3PNPP1/PPP1P2P/NBB1RQKR w HEhe - +perft 1 24 +perft 2 598 +perft 3 15673 +perft 4 409766 +perft 5 11394778 +perft 6 310589129 + +id 52 +epd nnbbrqkr/p2ppp1p/1pp5/8/6p1/N1P5/PPBPPPPP/N1B1RQKR w HEhe - +perft 1 26 +perft 2 530 +perft 3 14031 +perft 4 326312 +perft 5 8846766 +perft 6 229270702 + +id 53 +epd nnbrqbkr/2p1p1pp/p4p2/1p1p4/8/NP6/P1PPPPPP/N1BRQBKR w HDhd - +perft 1 17 +perft 2 496 +perft 3 10220 +perft 4 303310 +perft 5 7103549 +perft 6 217108001 + +id 54 +epd nnbrqk1b/pp2pprp/2pp2p1/8/3PP1P1/8/PPP2P1P/NNBRQRKB w d - +perft 1 33 +perft 2 820 +perft 3 27856 +perft 4 706784 +perft 5 24714401 +perft 6 645835197 + +id 55 +epd 1bnrbqkr/ppnpp1p1/2p2p1p/8/1P6/4PPP1/P1PP3P/NBNRBQKR w HDhd - +perft 1 27 +perft 2 705 +perft 3 19760 +perft 4 548680 +perft 5 15964771 +perft 6 464662032 + +id 56 +epd n1rbbqkr/pp1pppp1/7p/P1p5/1n6/2PP4/1P2PPPP/NNRBBQKR w HChc - +perft 1 22 +perft 2 631 +perft 3 14978 +perft 4 431801 +perft 5 10911545 +perft 6 320838556 + +id 57 +epd n1rqb1kr/p1pppp1p/1pn4b/3P2p1/P7/1P6/2P1PPPP/NNRQBBKR w HChc - +perft 1 24 +perft 2 477 +perft 3 12506 +perft 4 263189 +perft 5 7419372 +perft 6 165945904 + +id 58 +epd nnrqbkrb/pppp1pp1/7p/4p3/6P1/2N2B2/PPPPPP1P/NR1QBKR1 w Ggc - +perft 1 29 +perft 2 658 +perft 3 19364 +perft 4 476620 +perft 5 14233587 +perft 6 373744834 + +id 59 +epd n1nrqkbr/ppb2ppp/3pp3/2p5/2P3P1/5P2/PP1PPB1P/NBNRQK1R w HDhd - +perft 1 32 +perft 2 801 +perft 3 25861 +perft 4 681428 +perft 5 22318948 +perft 6 619857455 + +id 60 +epd 2rbqkbr/p1pppppp/1nn5/1p6/7P/P4P2/1PPPP1PB/NNRBQK1R w HChc - +perft 1 27 +perft 2 647 +perft 3 18030 +perft 4 458057 +perft 5 13189156 +perft 6 354689323 + +id 61 +epd nn1qkbbr/pp2ppp1/2rp4/2p4p/P2P4/1N5P/1PP1PPP1/1NRQKBBR w HCh - +perft 1 24 +perft 2 738 +perft 3 18916 +perft 4 586009 +perft 5 16420659 +perft 6 519075930 + +id 62 +epd nnrqk1bb/p1ppp2p/5rp1/1p3p2/1P4P1/5P1P/P1PPP3/NNRQKRBB w FCc - +perft 1 25 +perft 2 795 +perft 3 20510 +perft 4 648945 +perft 5 17342527 +perft 6 556144017 + +id 63 +epd bb1nrkqr/ppppn2p/4ppp1/8/1P4P1/4P3/P1PPKP1P/BBNNR1QR w he - +perft 1 29 +perft 2 664 +perft 3 20024 +perft 4 498376 +perft 5 15373803 +perft 6 406016364 + +id 64 +epd bnnbrkqr/1p1ppp2/8/p1p3pp/1P6/N4P2/PBPPP1PP/2NBRKQR w HEhe - +perft 1 31 +perft 2 770 +perft 3 24850 +perft 4 677212 +perft 5 22562080 +perft 6 662029574 + +id 65 +epd 1nnrkbqr/p1pp1ppp/4p3/1p6/1Pb1P3/6PB/P1PP1P1P/BNNRK1QR w HDhd - +perft 1 27 +perft 2 776 +perft 3 22133 +perft 4 641002 +perft 5 19153245 +perft 6 562738257 + +id 66 +epd bnr1kqrb/pppp1pp1/1n5p/4p3/P3P3/3P2P1/1PP2P1P/BNNRKQRB w GDg - +perft 1 26 +perft 2 624 +perft 3 16411 +perft 4 435426 +perft 5 11906515 +perft 6 338092952 + +id 67 +epd nbbnrkqr/p1ppp1pp/1p3p2/8/2P5/4P3/PP1P1PPP/NBBNRKQR w HEhe - +perft 1 25 +perft 2 624 +perft 3 15561 +perft 4 419635 +perft 5 10817378 +perft 6 311138112 + +id 68 +epd nn1brkqr/pp1bpppp/8/2pp4/P4P2/1PN5/2PPP1PP/N1BBRKQR w HEhe - +perft 1 23 +perft 2 659 +perft 3 16958 +perft 4 476567 +perft 5 13242252 +perft 6 373557073 + +id 69 +epd n1brkbqr/ppp1pp1p/6pB/3p4/2Pn4/8/PP2PPPP/NN1RKBQR w HDhd - +perft 1 32 +perft 2 1026 +perft 3 30360 +perft 4 978278 +perft 5 29436320 +perft 6 957904151 + +id 70 +epd nnbrkqrb/p2ppp2/Q5pp/1pp5/4PP2/2N5/PPPP2PP/N1BRK1RB w GDgd - +perft 1 36 +perft 2 843 +perft 3 29017 +perft 4 715537 +perft 5 24321197 +perft 6 630396940 + +id 71 +epd nbnrbk1r/pppppppq/8/7p/8/1N2QPP1/PPPPP2P/NB1RBK1R w HDhd - +perft 1 36 +perft 2 973 +perft 3 35403 +perft 4 1018054 +perft 5 37143354 +perft 6 1124883780 + +id 72 +epd nnrbbkqr/2pppp1p/p7/6p1/1p2P3/4QPP1/PPPP3P/NNRBBK1R w HChc - +perft 1 36 +perft 2 649 +perft 3 22524 +perft 4 489526 +perft 5 16836636 +perft 6 416139320 + +id 73 +epd nnrkbbqr/1p2pppp/p2p4/2p5/8/1N2P1P1/PPPP1P1P/1NKRBBQR w hc - +perft 1 26 +perft 2 672 +perft 3 18136 +perft 4 477801 +perft 5 13342771 +perft 6 363074681 + +id 74 +epd n1rkbqrb/pp1ppp2/2n3p1/2p4p/P5PP/1P6/2PPPP2/NNRKBQRB w GCgc - +perft 1 24 +perft 2 804 +perft 3 20712 +perft 4 684001 +perft 5 18761475 +perft 6 617932151 + +id 75 +epd nbkr1qbr/1pp1pppp/pn1p4/8/3P2P1/5R2/PPP1PP1P/NBN1KQBR w H - +perft 1 30 +perft 2 627 +perft 3 18669 +perft 4 423329 +perft 5 12815016 +perft 6 312798696 + +id 76 +epd nnr1kqbr/pp1pp1p1/2p5/b4p1p/P7/1PNP4/2P1PPPP/N1RBKQBR w HChc - +perft 1 12 +perft 2 421 +perft 3 6530 +perft 4 227044 +perft 5 4266410 +perft 6 149176979 + +id 77 +epd n1rkqbbr/p1pp1pp1/np2p2p/8/8/N4PP1/PPPPP1BP/N1RKQ1BR w HChc - +perft 1 27 +perft 2 670 +perft 3 19119 +perft 4 494690 +perft 5 14708490 +perft 6 397268628 + +id 78 +epd nnr1qrbb/p2kpppp/1p1p4/2p5/6P1/PP1P4/2P1PP1P/NNRKQRBB w FC - +perft 1 27 +perft 2 604 +perft 3 17043 +perft 4 409665 +perft 5 11993332 +perft 6 308518181 + +id 79 +epd bbnnrkrq/ppp1pp2/6p1/3p4/7p/7P/PPPPPPP1/BBNNRRKQ w ge - +perft 1 20 +perft 2 559 +perft 3 12242 +perft 4 355326 +perft 5 8427161 +perft 6 252274233 + +id 80 +epd bnnbrkr1/ppp2p1p/5q2/3pp1p1/4P3/1N4P1/PPPPRP1P/BN1B1KRQ w Gge - +perft 1 26 +perft 2 1036 +perft 3 27228 +perft 4 1028084 +perft 5 28286576 +perft 6 1042120495 + +id 81 +epd bn1rkbrq/1pppppp1/p6p/1n6/3P4/6PP/PPPRPP2/BNN1KBRQ w Ggd - +perft 1 29 +perft 2 633 +perft 3 19278 +perft 4 455476 +perft 5 14333034 +perft 6 361900466 + +id 82 +epd b1nrkrqb/1p1npppp/p2p4/2p5/5P2/4P2P/PPPP1RP1/BNNRK1QB w Dfd - +perft 1 25 +perft 2 475 +perft 3 12603 +perft 4 270909 +perft 5 7545536 +perft 6 179579818 + +id 83 +epd 1bbnrkrq/ppppppp1/8/7p/1n4P1/1PN5/P1PPPP1P/NBBR1KRQ w Gge - +perft 1 30 +perft 2 803 +perft 3 25473 +perft 4 709716 +perft 5 23443854 +perft 6 686365049 + +id 84 +epd nnbbrkrq/2pp1pp1/1p5p/pP2p3/7P/N7/P1PPPPP1/N1BBRKRQ w GEge - +perft 1 18 +perft 2 432 +perft 3 9638 +perft 4 242350 +perft 5 6131124 +perft 6 160393505 + +id 85 +epd nnbrkbrq/1pppp1p1/p7/7p/1P2Pp2/BN6/P1PP1PPP/1N1RKBRQ w GDgd - +perft 1 27 +perft 2 482 +perft 3 13441 +perft 4 282259 +perft 5 8084701 +perft 6 193484216 + +id 86 +epd n1brkrqb/pppp3p/n3pp2/6p1/3P1P2/N1P5/PP2P1PP/N1BRKRQB w FDfd - +perft 1 28 +perft 2 642 +perft 3 19005 +perft 4 471729 +perft 5 14529434 +perft 6 384837696 + +id 87 +epd nbnrbk2/p1pppp1p/1p3qr1/6p1/1B1P4/1N6/PPP1PPPP/1BNR1RKQ w d - +perft 1 30 +perft 2 796 +perft 3 22780 +perft 4 687302 +perft 5 20120565 +perft 6 641832725 + +id 88 +epd nnrbbrkq/1pp2ppp/3p4/p3p3/3P1P2/1P2P3/P1P3PP/NNRBBKRQ w GC - +perft 1 31 +perft 2 827 +perft 3 24538 +perft 4 663082 +perft 5 19979594 +perft 6 549437308 + +id 89 +epd nnrkbbrq/1pp2p1p/p2pp1p1/2P5/8/8/PP1PPPPP/NNRKBBRQ w Ggc - +perft 1 24 +perft 2 762 +perft 3 19283 +perft 4 624598 +perft 5 16838099 +perft 6 555230555 + +id 90 +epd nnr1brqb/1ppkp1pp/8/p2p1p2/1P1P4/N1P5/P3PPPP/N1RKBRQB w FC - +perft 1 23 +perft 2 640 +perft 3 15471 +perft 4 444905 +perft 5 11343507 +perft 6 334123513 + +id 91 +epd nbnrkrbq/2ppp2p/p4p2/1P4p1/4PP2/8/1PPP2PP/NBNRKRBQ w FDfd - +perft 1 31 +perft 2 826 +perft 3 26137 +perft 4 732175 +perft 5 23555139 +perft 6 686250413 + +id 92 +epd 1nrbkr1q/1pppp1pp/1n6/p4p2/N1b4P/8/PPPPPPPB/N1RBKR1Q w FCfc - +perft 1 27 +perft 2 862 +perft 3 24141 +perft 4 755171 +perft 5 22027695 +perft 6 696353497 + +id 93 +epd nnrkrbbq/pppp2pp/8/4pp2/4P3/P7/1PPPBPPP/NNKRR1BQ w c - +perft 1 25 +perft 2 792 +perft 3 19883 +perft 4 636041 +perft 5 16473376 +perft 6 532214177 + +id 94 +epd n1rk1qbb/pppprpp1/2n4p/4p3/2PP3P/8/PP2PPP1/NNRKRQBB w ECc - +perft 1 25 +perft 2 622 +perft 3 16031 +perft 4 425247 +perft 5 11420973 +perft 6 321855685 + +id 95 +epd bbq1rnkr/pnp1pp1p/1p1p4/6p1/2P5/2Q1P2P/PP1P1PP1/BB1NRNKR w HEhe - +perft 1 36 +perft 2 870 +perft 3 30516 +perft 4 811047 +perft 5 28127620 +perft 6 799738334 + +id 96 +epd bq1brnkr/1p1ppp1p/1np5/p5p1/8/1N5P/PPPPPPP1/BQ1BRNKR w HEhe - +perft 1 22 +perft 2 588 +perft 3 13524 +perft 4 380068 +perft 5 9359618 +perft 6 273795898 + +id 97 +epd bq1rn1kr/1pppppbp/Nn4p1/8/8/P7/1PPPPPPP/BQ1RNBKR w HDhd - +perft 1 24 +perft 2 711 +perft 3 18197 +perft 4 542570 +perft 5 14692779 +perft 6 445827351 + +id 98 +epd bqnr1kr1/pppppp1p/6p1/5n2/4B3/3N2PP/PbPPPP2/BQNR1KR1 w GDgd - +perft 1 31 +perft 2 1132 +perft 3 36559 +perft 4 1261476 +perft 5 43256823 +perft 6 1456721391 + +id 99 +epd qbb1rnkr/ppp3pp/4n3/3ppp2/1P3PP1/8/P1PPPN1P/QBB1RNKR w HEhe - +perft 1 28 +perft 2 696 +perft 3 20502 +perft 4 541886 +perft 5 16492398 +perft 6 456983120 + +id 100 +epd qnbbr1kr/pp1ppp1p/4n3/6p1/2p3P1/2PP1P2/PP2P2P/QNBBRNKR w HEhe - +perft 1 25 +perft 2 655 +perft 3 16520 +perft 4 450189 +perft 5 11767038 +perft 6 335414976 + +id 101 +epd 1nbrnbkr/p1ppp1pp/1p6/5p2/4q1PP/3P4/PPP1PP2/QNBRNBKR w HDhd - +perft 1 30 +perft 2 1162 +perft 3 33199 +perft 4 1217278 +perft 5 36048727 +perft 6 1290346802 + +id 102 +epd q1brnkrb/p1pppppp/n7/1p6/P7/3P1P2/QPP1P1PP/1NBRNKRB w GDgd - +perft 1 32 +perft 2 827 +perft 3 26106 +perft 4 718243 +perft 5 23143989 +perft 6 673147648 + +id 103 +epd qbnrb1kr/ppp1pp1p/3p4/2n3p1/1P6/6N1/P1PPPPPP/QBNRB1KR w HDhd - +perft 1 29 +perft 2 751 +perft 3 23132 +perft 4 610397 +perft 5 19555214 +perft 6 530475036 + +id 104 +epd q1rbbnkr/pppp1p2/2n3pp/2P1p3/3P4/8/PP1NPPPP/Q1RBBNKR w HChc - +perft 1 29 +perft 2 806 +perft 3 24540 +perft 4 687251 +perft 5 21694330 +perft 6 619907316 + +id 105 +epd q1r1bbkr/pnpp1ppp/2n1p3/1p6/2P2P2/2N1N3/PP1PP1PP/Q1R1BBKR w HChc - +perft 1 32 +perft 2 1017 +perft 3 32098 +perft 4 986028 +perft 5 31204371 +perft 6 958455898 + +id 106 +epd 2rnbkrb/pqppppp1/1pn5/7p/2P5/P1R5/QP1PPPPP/1N1NBKRB w Ggc - +perft 1 26 +perft 2 625 +perft 3 16506 +perft 4 434635 +perft 5 11856964 +perft 6 336672890 + +id 107 +epd qbnr1kbr/p2ppppp/2p5/1p6/4n2P/P4N2/1PPP1PP1/QBNR1KBR w HDhd - +perft 1 27 +perft 2 885 +perft 3 23828 +perft 4 767273 +perft 5 21855658 +perft 6 706272554 + +id 108 +epd qnrbnk1r/pp1pp2p/5p2/2pbP1p1/3P4/1P6/P1P2PPP/QNRBNKBR w HChc - +perft 1 26 +perft 2 954 +perft 3 24832 +perft 4 892456 +perft 5 24415089 +perft 6 866744329 + +id 109 +epd qnrnk1br/p1p2ppp/8/1pbpp3/8/PP2N3/1QPPPPPP/1NR1KBBR w HChc - +perft 1 26 +perft 2 783 +perft 3 20828 +perft 4 634267 +perft 5 17477825 +perft 6 539674275 + +id 110 +epd qnrnkrbb/Bpppp2p/6p1/5p2/5P2/3PP3/PPP3PP/QNRNKR1B w FCfc - +perft 1 28 +perft 2 908 +perft 3 25730 +perft 4 861240 +perft 5 25251641 +perft 6 869525254 + +id 111 +epd bbnqrn1r/ppppp2k/5p2/6pp/7P/1QP5/PP1PPPP1/B1N1RNKR w HE - +perft 1 33 +perft 2 643 +perft 3 21790 +perft 4 487109 +perft 5 16693640 +perft 6 410115900 + +id 112 +epd b1qbrnkr/ppp1pp2/2np4/6pp/4P3/2N4P/PPPP1PP1/BQ1BRNKR w HEhe - +perft 1 28 +perft 2 837 +perft 3 24253 +perft 4 745617 +perft 5 22197063 +perft 6 696399065 + +id 113 +epd bnqr1bkr/pp1ppppp/2p5/4N3/5P2/P7/1PPPPnPP/BNQR1BKR w HDhd - +perft 1 25 +perft 2 579 +perft 3 13909 +perft 4 341444 +perft 5 8601011 +perft 6 225530258 + +id 114 +epd b1qr1krb/pp1ppppp/n2n4/8/2p5/2P3P1/PP1PPP1P/BNQRNKRB w GDgd - +perft 1 28 +perft 2 707 +perft 3 19721 +perft 4 549506 +perft 5 15583376 +perft 6 468399900 + +id 115 +epd nbbqr1kr/1pppp1pp/8/p1n2p2/4P3/PN6/1PPPQPPP/1BB1RNKR w HEhe - +perft 1 30 +perft 2 745 +perft 3 23416 +perft 4 597858 +perft 5 19478789 +perft 6 515473678 + +id 116 +epd nqbbrn1r/p1pppp1k/1p4p1/7p/4P3/1R3B2/PPPP1PPP/NQB2NKR w H - +perft 1 24 +perft 2 504 +perft 3 13512 +perft 4 317355 +perft 5 9002073 +perft 6 228726497 + +id 117 +epd nqbr1bkr/p1p1ppp1/1p1n4/3pN2p/1P6/8/P1PPPPPP/NQBR1BKR w HDhd - +perft 1 29 +perft 2 898 +perft 3 26532 +perft 4 809605 +perft 5 24703467 +perft 6 757166494 + +id 118 +epd nqbrn1rb/pppp1kp1/5p1p/4p3/P4B2/3P2P1/1PP1PP1P/NQ1RNKRB w GD - +perft 1 34 +perft 2 671 +perft 3 22332 +perft 4 473110 +perft 5 15556806 +perft 6 353235120 + +id 119 +epd nb1r1nkr/ppp1ppp1/2bp4/7p/3P2qP/P6R/1PP1PPP1/NBQRBNK1 w Dhd - +perft 1 38 +perft 2 1691 +perft 3 60060 +perft 4 2526992 +perft 5 88557078 +perft 6 3589649998 + +id 120 +epd n1rbbnkr/1p1pp1pp/p7/2p1qp2/1B3P2/3P4/PPP1P1PP/NQRB1NKR w HChc - +perft 1 24 +perft 2 913 +perft 3 21595 +perft 4 807544 +perft 5 19866918 +perft 6 737239330 + +id 121 +epd nqrnbbkr/p2p1p1p/1pp5/1B2p1p1/1P3P2/4P3/P1PP2PP/NQRNB1KR w HChc - +perft 1 33 +perft 2 913 +perft 3 30159 +perft 4 843874 +perft 5 28053260 +perft 6 804687975 + +id 122 +epd nqr1bkrb/ppp1pp2/2np2p1/P6p/8/2P4P/1P1PPPP1/NQRNBKRB w GCgc - +perft 1 24 +perft 2 623 +perft 3 16569 +perft 4 442531 +perft 5 12681936 +perft 6 351623879 + +id 123 +epd nb1rnkbr/pqppppp1/1p5p/8/1PP4P/8/P2PPPP1/NBQRNKBR w HDhd - +perft 1 31 +perft 2 798 +perft 3 24862 +perft 4 694386 +perft 5 22616076 +perft 6 666227466 + +id 124 +epd nqrbnkbr/2p1p1pp/3p4/pp3p2/6PP/3P1N2/PPP1PP2/NQRB1KBR w HChc - +perft 1 24 +perft 2 590 +perft 3 14409 +perft 4 383690 +perft 5 9698432 +perft 6 274064911 + +id 125 +epd nqrnkbbr/pp1p1p1p/4p1p1/1p6/8/5P1P/P1PPP1P1/NQRNKBBR w HChc - +perft 1 30 +perft 2 1032 +perft 3 31481 +perft 4 1098116 +perft 5 34914919 +perft 6 1233362066 + +id 126 +epd nqrnkrbb/p2ppppp/1p6/2p5/2P3P1/5P2/PP1PPN1P/NQR1KRBB w FCfc - +perft 1 30 +perft 2 775 +perft 3 23958 +perft 4 668000 +perft 5 21141738 +perft 6 621142773 + +id 127 +epd bbnrqrk1/pp2pppp/4n3/2pp4/P7/1N5P/BPPPPPP1/B2RQNKR w HD - +perft 1 23 +perft 2 708 +perft 3 17164 +perft 4 554089 +perft 5 14343443 +perft 6 481405144 + +id 128 +epd bnr1qnkr/p1pp1p1p/1p4p1/4p1b1/2P1P3/1P6/PB1P1PPP/1NRBQNKR w HChc - +perft 1 30 +perft 2 931 +perft 3 29249 +perft 4 921746 +perft 5 30026687 +perft 6 968109774 + +id 129 +epd b1rqnbkr/ppp1ppp1/3p3p/2n5/P3P3/2NP4/1PP2PPP/B1RQNBKR w HChc - +perft 1 24 +perft 2 596 +perft 3 15533 +perft 4 396123 +perft 5 11099382 +perft 6 294180723 + +id 130 +epd bnrqnr1b/pp1pkppp/2p1p3/P7/2P5/7P/1P1PPPP1/BNRQNKRB w GC - +perft 1 24 +perft 2 572 +perft 3 15293 +perft 4 390903 +perft 5 11208688 +perft 6 302955778 + +id 131 +epd n1brq1kr/bppppppp/p7/8/4P1Pn/8/PPPP1P2/NBBRQNKR w HDhd - +perft 1 20 +perft 2 570 +perft 3 13139 +perft 4 371247 +perft 5 9919113 +perft 6 284592289 + +id 132 +epd 1rbbqnkr/ppn1ppp1/3p3p/2p5/3P4/1N4P1/PPPBPP1P/1R1BQNKR w HBhb - +perft 1 29 +perft 2 1009 +perft 3 29547 +perft 4 1040816 +perft 5 31059587 +perft 6 1111986835 + +id 133 +epd nrbq2kr/ppppppb1/5n1p/5Pp1/8/P5P1/1PPPP2P/NRBQNBKR w HBhb - +perft 1 20 +perft 2 520 +perft 3 11745 +perft 4 316332 +perft 5 7809837 +perft 6 216997152 + +id 134 +epd nrb1nkrb/pp3ppp/1qBpp3/2p5/8/P5P1/1PPPPP1P/NRBQNKR1 w GBgb - +perft 1 32 +perft 2 850 +perft 3 25642 +perft 4 734088 +perft 5 21981567 +perft 6 664886187 + +id 135 +epd 1br1bnkr/ppqppp1p/1np3p1/8/1PP4P/4N3/P2PPPP1/NBRQB1KR w HChc - +perft 1 32 +perft 2 798 +perft 3 24765 +perft 4 691488 +perft 5 22076141 +perft 6 670296871 + +id 136 +epd nrqbb1kr/1p1pp1pp/2p3n1/p4p2/3PP3/P5N1/1PP2PPP/NRQBB1KR w HBhb - +perft 1 32 +perft 2 791 +perft 3 26213 +perft 4 684890 +perft 5 23239122 +perft 6 634260266 + +id 137 +epd nrqn1bkr/ppppp1pp/4b3/8/4P1p1/5P2/PPPP3P/NRQNBBKR w HBhb - +perft 1 29 +perft 2 687 +perft 3 20223 +perft 4 506088 +perft 5 15236287 +perft 6 398759980 + +id 138 +epd nrqnbrkb/pppp1p2/4p2p/3B2p1/8/1P4P1/PQPPPP1P/NR1NBKR1 w GB - +perft 1 37 +perft 2 764 +perft 3 27073 +perft 4 610950 +perft 5 21284835 +perft 6 514864869 + +id 139 +epd nbrq1kbr/Bp3ppp/2pnp3/3p4/5P2/2P4P/PP1PP1P1/NBRQNK1R w HChc - +perft 1 40 +perft 2 1271 +perft 3 48022 +perft 4 1547741 +perft 5 56588117 +perft 6 1850696281 + +id 140 +epd nrqbnkbr/1p2ppp1/p1p4p/3p4/1P6/8/PQPPPPPP/1RNBNKBR w HBhb - +perft 1 28 +perft 2 757 +perft 3 23135 +perft 4 668025 +perft 5 21427496 +perft 6 650939962 + +id 141 +epd nrqn1bbr/2ppkppp/4p3/pB6/8/2P1P3/PP1P1PPP/NRQNK1BR w HB - +perft 1 27 +perft 2 642 +perft 3 17096 +perft 4 442653 +perft 5 11872805 +perft 6 327545120 + +id 142 +epd nrqnkrb1/p1ppp2p/1p4p1/4bp2/4PP1P/4N3/PPPP2P1/NRQ1KRBB w FBfb - +perft 1 27 +perft 2 958 +perft 3 27397 +perft 4 960350 +perft 5 28520172 +perft 6 995356563 + +id 143 +epd 1bnrnqkr/pbpp2pp/8/1p2pp2/P6P/3P1N2/1PP1PPP1/BBNR1QKR w HDhd - +perft 1 27 +perft 2 859 +perft 3 23475 +perft 4 773232 +perft 5 21581178 +perft 6 732696327 + +id 144 +epd b1rbnqkr/1pp1ppp1/2n4p/p2p4/5P2/1PBP4/P1P1P1PP/1NRBNQKR w HChc - +perft 1 26 +perft 2 545 +perft 3 14817 +perft 4 336470 +perft 5 9537260 +perft 6 233549184 + +id 145 +epd 1nrnqbkr/p1pppppp/1p6/8/2b2P2/P1N5/1PP1P1PP/BNR1QBKR w HChc - +perft 1 24 +perft 2 668 +perft 3 17716 +perft 4 494866 +perft 5 14216070 +perft 6 406225409 + +id 146 +epd 1nrnqkrb/2ppp1pp/p7/1p3p2/5P2/N5K1/PPPPP2P/B1RNQ1RB w gc - +perft 1 33 +perft 2 725 +perft 3 23572 +perft 4 559823 +perft 5 18547476 +perft 6 471443091 + +id 147 +epd nbbr1qkr/p1pppppp/8/1p1n4/3P4/1N3PP1/PPP1P2P/1BBRNQKR w HDhd - +perft 1 28 +perft 2 698 +perft 3 20527 +perft 4 539625 +perft 5 16555068 +perft 6 458045505 + +id 148 +epd 1rbbnqkr/1pnppp1p/p5p1/2p5/2P4P/5P2/PP1PP1PR/NRBBNQK1 w Bhb - +perft 1 24 +perft 2 554 +perft 3 14221 +perft 4 362516 +perft 5 9863080 +perft 6 269284081 + +id 149 +epd nrb1qbkr/2pppppp/2n5/p7/2p5/4P3/PPNP1PPP/1RBNQBKR w HBhb - +perft 1 23 +perft 2 618 +perft 3 15572 +perft 4 443718 +perft 5 12044358 +perft 6 360311412 + +id 150 +epd nrb1qkrb/2ppppp1/p3n3/1p1B3p/2P5/6P1/PP1PPPRP/NRBNQK2 w Bgb - +perft 1 27 +perft 2 593 +perft 3 16770 +perft 4 401967 +perft 5 11806808 +perft 6 303338935 + +id 151 +epd nbrn1qkr/ppp1pp2/3p2p1/3Q3P/b7/8/PPPPPP1P/NBRNB1KR w HChc - +perft 1 39 +perft 2 1056 +perft 3 40157 +perft 4 1133446 +perft 5 42201531 +perft 6 1239888683 + +id 152 +epd nr1bbqkr/pp1pp2p/1n3pp1/2p5/8/1P4P1/P1PPPPQP/NRNBBK1R w hb - +perft 1 25 +perft 2 585 +perft 3 15719 +perft 4 406544 +perft 5 11582539 +perft 6 320997679 + +id 153 +epd nr2bbkr/ppp1pppp/1n1p4/8/6PP/1NP4q/PP1PPP2/1RNQBBKR w HBhb - +perft 1 22 +perft 2 742 +perft 3 15984 +perft 4 545231 +perft 5 13287051 +perft 6 457010195 + +id 154 +epd 1rnqbkrb/ppp1p1p1/1n3p2/3p3p/P6P/4P3/1PPP1PP1/NRNQBRKB w gb - +perft 1 22 +perft 2 574 +perft 3 14044 +perft 4 379648 +perft 5 9968830 +perft 6 281344367 + +id 155 +epd nb1rqkbr/1pppp1pp/4n3/p4p2/6PP/5P2/PPPPPN2/NBR1QKBR w HCh - +perft 1 25 +perft 2 621 +perft 3 16789 +perft 4 462600 +perft 5 13378840 +perft 6 396575613 + +id 156 +epd nrnbqkbr/2pp2pp/4pp2/pp6/8/1P3P2/P1PPPBPP/NRNBQ1KR w hb - +perft 1 25 +perft 2 656 +perft 3 16951 +perft 4 466493 +perft 5 12525939 +perft 6 358763789 + +id 157 +epd nrnqkbbr/ppppp1p1/7p/5p2/8/P4PP1/NPPPP2P/NR1QKBBR w HBhb - +perft 1 28 +perft 2 723 +perft 3 20621 +perft 4 547522 +perft 5 15952533 +perft 6 439046803 + +id 158 +epd 1rnqkr1b/ppppp2p/1n3pp1/8/2P3P1/Pb1N4/1P1PPP1P/NR1QKRBB w FBfb - +perft 1 26 +perft 2 713 +perft 3 19671 +perft 4 548875 +perft 5 15865528 +perft 6 454532806 + +id 159 +epd bbnrnkqr/1pppp1pp/5p2/p7/7P/1P6/PBPPPPPR/1BNRNKQ1 w D - +perft 1 26 +perft 2 649 +perft 3 17834 +perft 4 502279 +perft 5 14375839 +perft 6 435585252 + +id 160 +epd bnrbk1qr/1ppp1ppp/p2np3/8/P7/2N2P2/1PPPP1PP/B1RBNKQR w HC - +perft 1 26 +perft 2 621 +perft 3 17569 +perft 4 451452 +perft 5 13514201 +perft 6 364421088 + +id 161 +epd br1nkbqr/ppppppp1/8/n6p/8/N1P2PP1/PP1PP2P/B1RNKBQR w HCh - +perft 1 29 +perft 2 664 +perft 3 20182 +perft 4 512316 +perft 5 16125924 +perft 6 442508159 + +id 162 +epd bnr1kqrb/pp1pppp1/2n5/2p5/1P4Pp/4N3/P1PPPP1P/BNKR1QRB w gc - +perft 1 36 +perft 2 888 +perft 3 31630 +perft 4 789863 +perft 5 27792175 +perft 6 719015345 + +id 163 +epd 1bbrnkqr/pp1p1ppp/2p1p3/1n6/5P2/3Q4/PPPPP1PP/NBBRNK1R w HDhd - +perft 1 36 +perft 2 891 +perft 3 31075 +perft 4 781792 +perft 5 26998966 +perft 6 702903862 + +id 164 +epd nrbbnk1r/pp2pppq/8/2pp3p/3P2P1/1N6/PPP1PP1P/1RBBNKQR w HBhb - +perft 1 29 +perft 2 1036 +perft 3 31344 +perft 4 1139166 +perft 5 35627310 +perft 6 1310683359 + +id 165 +epd nr1nkbqr/ppp3pp/5p2/3pp3/6b1/3PP3/PPP2PPP/NRBNKBQR w hb - +perft 1 18 +perft 2 664 +perft 3 13306 +perft 4 483892 +perft 5 10658989 +perft 6 386307449 + +id 166 +epd nrbnk1rb/ppp1pq1p/3p4/5pp1/2P1P3/1N6/PP1PKPPP/1RBN1QRB w gb - +perft 1 25 +perft 2 966 +perft 3 24026 +perft 4 920345 +perft 5 23957242 +perft 6 913710194 + +id 167 +epd 1brnbkqr/pppppp2/6p1/7p/1Pn5/P1NP4/2P1PPPP/NBR1BKQR w HChc - +perft 1 22 +perft 2 627 +perft 3 13760 +perft 4 395829 +perft 5 9627826 +perft 6 285900573 + +id 168 +epd nrnbbk1r/p1pppppq/8/7p/1p6/P5PP/1PPPPPQ1/NRNBBK1R w HBhb - +perft 1 29 +perft 2 888 +perft 3 26742 +perft 4 874270 +perft 5 27229468 +perft 6 930799376 + +id 169 +epd n1nkb1qr/prppppbp/6p1/1p6/2P2P2/P7/1P1PP1PP/NRNKBBQR w HBh - +perft 1 29 +perft 2 804 +perft 3 24701 +perft 4 688520 +perft 5 21952444 +perft 6 623156747 + +id 170 +epd nr2bqrb/ppkpp1pp/1np5/5p1P/5P2/2P5/PP1PP1P1/NRNKBQRB w GB - +perft 1 22 +perft 2 530 +perft 3 13055 +perft 4 347657 +perft 5 9244693 +perft 6 264088392 + +id 171 +epd nbr1kqbr/p3pppp/2ppn3/1p4P1/4P3/1P6/P1PP1P1P/NBRNKQBR w HChc - +perft 1 23 +perft 2 555 +perft 3 14291 +perft 4 350917 +perft 5 9692630 +perft 6 247479180 + +id 172 +epd nr1bkqbr/1p1pp1pp/pnp2p2/8/6P1/P1PP4/1P2PP1P/NRNBKQBR w HBhb - +perft 1 22 +perft 2 565 +perft 3 13343 +perft 4 365663 +perft 5 9305533 +perft 6 268612479 + +id 173 +epd nr1kqbbr/np2pppp/p1p5/1B1p1P2/8/4P3/PPPP2PP/NRNKQ1BR w HBhb - +perft 1 32 +perft 2 730 +perft 3 23391 +perft 4 556995 +perft 5 18103280 +perft 6 454569900 + +id 174 +epd nrnk1rbb/p1p2ppp/3pq3/Qp2p3/1P1P4/8/P1P1PPPP/NRN1KRBB w fb - +perft 1 28 +perft 2 873 +perft 3 25683 +perft 4 791823 +perft 5 23868737 +perft 6 747991356 + +id 175 +epd bbnrnkrq/pp1ppp1p/6p1/2p5/6P1/P5RP/1PPPPP2/BBNRNK1Q w Dgd - +perft 1 37 +perft 2 1260 +perft 3 45060 +perft 4 1542086 +perft 5 54843403 +perft 6 1898432768 + +id 176 +epd bnrb1rkq/ppnpppp1/3Q4/2p4p/7P/N7/PPPPPPP1/B1RBNKR1 w GC - +perft 1 38 +perft 2 878 +perft 3 31944 +perft 4 800440 +perft 5 28784300 +perft 6 784569826 + +id 177 +epd bnrnkbrq/p1ppppp1/1p5p/8/P2PP3/5P2/1PP3PP/BNRNKBRQ w GCgc - +perft 1 26 +perft 2 617 +perft 3 16992 +perft 4 419099 +perft 5 11965544 +perft 6 311309576 + +id 178 +epd bnrnkrqb/pp2p2p/2pp1pp1/8/P7/2PP1P2/1P2P1PP/BNRNKRQB w FCfc - +perft 1 26 +perft 2 721 +perft 3 19726 +perft 4 560824 +perft 5 15966934 +perft 6 467132503 + +id 179 +epd nbbrnkr1/1pppp1p1/p6q/P4p1p/8/5P2/1PPPP1PP/NBBRNRKQ w gd - +perft 1 18 +perft 2 556 +perft 3 10484 +perft 4 316634 +perft 5 6629293 +perft 6 202528241 + +id 180 +epd nrb1nkrq/2pp1ppp/p4b2/1p2p3/P4B2/3P4/1PP1PPPP/NR1BNRKQ w gb - +perft 1 24 +perft 2 562 +perft 3 14017 +perft 4 355433 +perft 5 9227883 +perft 6 247634489 + +id 181 +epd nrbnkbrq/p3p1pp/1p6/2pp1P2/8/3PP3/PPP2P1P/NRBNKBRQ w GBgb - +perft 1 31 +perft 2 746 +perft 3 24819 +perft 4 608523 +perft 5 21019301 +perft 6 542954168 + +id 182 +epd nrbnkrqb/pppp1p1p/4p1p1/8/7P/2P1P3/PPNP1PP1/1RBNKRQB w FBfb - +perft 1 20 +perft 2 459 +perft 3 9998 +perft 4 242762 +perft 5 5760165 +perft 6 146614723 + +id 183 +epd nbrn1krq/ppp1p2p/6b1/3p1pp1/8/4N1PP/PPPPPP2/NBR1BRKQ w gc - +perft 1 27 +perft 2 835 +perft 3 23632 +perft 4 766397 +perft 5 22667987 +perft 6 760795567 + +id 184 +epd nrnbbkrq/p1pp2pp/5p2/1p6/2P1pP1B/1P6/P2PP1PP/NRNB1KRQ w GBgb - +perft 1 24 +perft 2 646 +perft 3 16102 +perft 4 444472 +perft 5 11489727 +perft 6 324948755 + +id 185 +epd nrn1bbrq/1ppkppp1/p2p3p/8/1P3N2/4P3/P1PP1PPP/NR1KBBRQ w GB - +perft 1 32 +perft 2 591 +perft 3 18722 +perft 4 381683 +perft 5 12069159 +perft 6 269922838 + +id 186 +epd n1krbrqb/1ppppppp/p7/8/4n3/P4P1P/1PPPPQP1/NRNKBR1B w FB - +perft 1 26 +perft 2 639 +perft 3 16988 +perft 4 417190 +perft 5 12167153 +perft 6 312633873 + +id 187 +epd n1rnkrbq/1p1ppp1p/8/p1p1b1p1/3PQ1P1/4N3/PPP1PP1P/NBR1KRB1 w FCfc - +perft 1 35 +perft 2 1027 +perft 3 35731 +perft 4 1040417 +perft 5 35738410 +perft 6 1060661628 + +id 188 +epd nrnbkrbq/2pp1pp1/pp6/4p2p/P7/5PPP/1PPPP3/NRNBKRBQ w FBfb - +perft 1 26 +perft 2 628 +perft 3 16731 +perft 4 436075 +perft 5 11920087 +perft 6 331498921 + +id 189 +epd 1rnkrbbq/pp1p2pp/1n3p2/1Bp1p3/1P6/1N2P3/P1PP1PPP/1RNKR1BQ w EBeb - +perft 1 33 +perft 2 992 +perft 3 32244 +perft 4 983481 +perft 5 31703749 +perft 6 980306735 + +id 190 +epd nr1krqbb/p1ppppp1/8/1p5p/1Pn5/5P2/P1PPP1PP/NRNKRQBB w EBeb - +perft 1 24 +perft 2 670 +perft 3 15985 +perft 4 445492 +perft 5 11371067 +perft 6 325556465 + +id 191 +epd bbq1rkr1/1ppppppp/p1n2n2/8/2P2P2/1P6/PQ1PP1PP/BB1NRKNR w HEe - +perft 1 32 +perft 2 794 +perft 3 26846 +perft 4 689334 +perft 5 24085223 +perft 6 645633370 + +id 192 +epd b1nbrknr/1qppp1pp/p4p2/1p6/6P1/P2NP3/1PPP1P1P/BQ1BRKNR w HEhe - +perft 1 25 +perft 2 663 +perft 3 17138 +perft 4 482994 +perft 5 13157826 +perft 6 389603029 + +id 193 +epd bqnrk1nr/pp2ppbp/6p1/2pp4/2P5/5P2/PPQPP1PP/B1NRKBNR w HDhd - +perft 1 26 +perft 2 850 +perft 3 22876 +perft 4 759768 +perft 5 21341087 +perft 6 719712622 + +id 194 +epd bqnrknrb/1ppp1p1p/p7/6p1/1P2p3/P1PN4/3PPPPP/BQ1RKNRB w GDgd - +perft 1 25 +perft 2 721 +perft 3 19290 +perft 4 581913 +perft 5 16391601 +perft 6 511725087 + +id 195 +epd q1b1rknr/pp1pppp1/4n2p/2p1b3/1PP5/4P3/PQ1P1PPP/1BBNRKNR w HEhe - +perft 1 32 +perft 2 975 +perft 3 32566 +perft 4 955493 +perft 5 32649943 +perft 6 962536105 + +id 196 +epd qnbbrknr/1p1ppppp/8/p1p5/5P2/PP1P4/2P1P1PP/QNBBRKNR w HEhe - +perft 1 27 +perft 2 573 +perft 3 16331 +perft 4 391656 +perft 5 11562434 +perft 6 301166330 + +id 197 +epd q1brkb1r/p1pppppp/np3B2/8/6n1/1P5N/P1PPPPPP/QN1RKB1R w HDhd - +perft 1 32 +perft 2 984 +perft 3 31549 +perft 4 1007217 +perft 5 32597704 +perft 6 1075429389 + +id 198 +epd qn1rk1rb/p1pppppp/1p2n3/8/2b5/4NPP1/PPPPP1RP/QNBRK2B w Dgd - +perft 1 22 +perft 2 802 +perft 3 19156 +perft 4 697722 +perft 5 17761431 +perft 6 650603534 + +id 199 +epd qbnrbknr/ppp2p1p/8/3pp1p1/1PP1B3/5N2/P2PPPPP/Q1NRBK1R w HDhd - +perft 1 34 +perft 2 943 +perft 3 32506 +perft 4 930619 +perft 5 32523099 +perft 6 955802240 + +id 200 +epd qnrbb1nr/pp1p1ppp/2p2k2/4p3/4P3/5PPP/PPPP4/QNRBBKNR w HC - +perft 1 20 +perft 2 460 +perft 3 10287 +perft 4 241640 +perft 5 5846781 +perft 6 140714047 + +id 201 +epd qnr1bbnr/ppk1p1pp/3p4/2p2p2/8/2P5/PP1PPPPP/QNKRBBNR w - - +perft 1 19 +perft 2 572 +perft 3 11834 +perft 4 357340 +perft 5 7994547 +perft 6 243724815 + +id 202 +epd qnrkbnrb/1p1p1ppp/2p5/4p3/p7/N1BP4/PPP1PPPP/Q1R1KNRB w gc - +perft 1 27 +perft 2 579 +perft 3 16233 +perft 4 375168 +perft 5 10845146 +perft 6 268229097 + +id 203 +epd qbnrkn1r/1pppp1p1/p3bp2/2BN3p/8/5P2/PPPPP1PP/QBNRK2R w HDhd - +perft 1 40 +perft 2 1027 +perft 3 38728 +perft 4 1059229 +perft 5 38511307 +perft 6 1104094381 + +id 204 +epd qnrbknbr/1pp2ppp/4p3/p6N/2p5/8/PPPPPPPP/Q1RBK1BR w HChc - +perft 1 22 +perft 2 510 +perft 3 11844 +perft 4 300180 +perft 5 7403327 +perft 6 200581103 + +id 205 +epd 1qkrnbbr/p1pppppp/2n5/1p6/8/5NP1/PPPPPP1P/QNRK1BBR w HC - +perft 1 24 +perft 2 549 +perft 3 13987 +perft 4 352037 +perft 5 9396521 +perft 6 255676649 + +id 206 +epd q1rknr1b/1ppppppb/2n5/p2B3p/8/1PN3P1/P1PPPP1P/Q1RKNRB1 w FCfc - +perft 1 31 +perft 2 924 +perft 3 28520 +perft 4 861944 +perft 5 27463479 +perft 6 847726572 + +id 207 +epd bbnqrk1r/pp1pppp1/2p4p/8/6n1/1N1P1P2/PPP1P1PP/BBQ1RKNR w HEhe - +perft 1 24 +perft 2 804 +perft 3 20147 +perft 4 666341 +perft 5 18024195 +perft 6 595947631 + +id 208 +epd bn1brknr/ppp1p1pp/5p2/3p4/6qQ/3P3P/PPP1PPP1/BN1BRKNR w HEhe - +perft 1 25 +perft 2 854 +perft 3 22991 +perft 4 704173 +perft 5 20290974 +perft 6 600195008 + +id 209 +epd 1nqrkbnr/2pp1ppp/pp2p3/3b4/2P5/N7/PP1PPPPP/B1QRKBNR w HDhd - +perft 1 22 +perft 2 651 +perft 3 16173 +perft 4 479152 +perft 5 13133439 +perft 6 390886040 + +id 210 +epd bnqrk1rb/1pp1pppp/p2p4/4n3/2PPP3/8/PP3PPP/BNQRKNRB w GDgd - +perft 1 30 +perft 2 950 +perft 3 28169 +perft 4 889687 +perft 5 27610213 +perft 6 880739164 + +id 211 +epd nbb1rknr/1ppq1ppp/3p4/p3p3/4P3/1N2R3/PPPP1PPP/1BBQ1KNR w Hhe - +perft 1 33 +perft 2 988 +perft 3 31293 +perft 4 967575 +perft 5 30894863 +perft 6 985384035 + +id 212 +epd nqbbrknr/2ppp2p/pp4p1/5p2/7P/3P1P2/PPPBP1P1/NQ1BRKNR w HEhe - +perft 1 27 +perft 2 492 +perft 3 13266 +perft 4 276569 +perft 5 7583292 +perft 6 175376176 + +id 213 +epd 1qbrkb1r/pppppppp/8/3n4/4P1n1/PN6/1PPP1P1P/1QBRKBNR w HDhd - +perft 1 28 +perft 2 800 +perft 3 21982 +perft 4 630374 +perft 5 17313279 +perft 6 507140861 + +id 214 +epd 1qbrknrb/1p1ppppp/1np5/8/p4P1P/4P1N1/PPPP2P1/NQBRK1RB w GDgd - +perft 1 21 +perft 2 482 +perft 3 10581 +perft 4 267935 +perft 5 6218644 +perft 6 168704845 + +id 215 +epd nbqrbkr1/ppp1pppp/8/3p4/6n1/2P2PPN/PP1PP2P/NBQRBK1R w HDd - +perft 1 29 +perft 2 921 +perft 3 25748 +perft 4 840262 +perft 5 24138518 +perft 6 806554650 + +id 216 +epd nqrb1knr/1ppbpp1p/p7/3p2p1/2P3P1/5P1P/PP1PP3/NQRBBKNR w HChc - +perft 1 31 +perft 2 803 +perft 3 25857 +perft 4 665799 +perft 5 21998733 +perft 6 583349773 + +id 217 +epd 1qrkbbr1/pppp1ppp/1n3n2/4p3/5P2/1N6/PPPPP1PP/1QRKBBNR w HCc - +perft 1 25 +perft 2 715 +perft 3 19118 +perft 4 556325 +perft 5 15514933 +perft 6 459533767 + +id 218 +epd nqrkb1rb/pp2pppp/2p1n3/3p4/3PP1N1/8/PPP2PPP/NQRKB1RB w GCgc - +perft 1 26 +perft 2 795 +perft 3 21752 +perft 4 679387 +perft 5 19185851 +perft 6 616508881 + +id 219 +epd nb1rknbr/pp2ppp1/8/2Bp3p/6P1/2P2P1q/PP1PP2P/NBQRKN1R w HDhd - +perft 1 35 +perft 2 1391 +perft 3 43025 +perft 4 1726888 +perft 5 53033675 +perft 6 2139267832 + +id 220 +epd nqrbkn1r/pp1pp1pp/8/2p2p2/5P2/P3B2P/1PbPP1P1/NQRBKN1R w HChc - +perft 1 23 +perft 2 758 +perft 3 19439 +perft 4 653854 +perft 5 18296195 +perft 6 628403401 + +id 221 +epd nqrknbbr/pp1pppp1/7p/2p5/7P/1P1N4/P1PPPPPB/NQRK1B1R w HChc - +perft 1 29 +perft 2 824 +perft 3 23137 +perft 4 683686 +perft 5 19429491 +perft 6 595493802 + +id 222 +epd 1qrknrbb/B1p1pppp/8/1p1p4/2n2P2/1P6/P1PPP1PP/NQRKNR1B w FCfc - +perft 1 28 +perft 2 771 +perft 3 20237 +perft 4 581721 +perft 5 16065378 +perft 6 483037840 + +id 223 +epd bbnrqk1r/1ppppppp/8/7n/1p6/P6P/1BPPPPP1/1BNRQKNR w HDhd - +perft 1 25 +perft 2 601 +perft 3 15471 +perft 4 396661 +perft 5 10697065 +perft 6 289472497 + +id 224 +epd bnrbqknr/ppp3p1/3ppp1Q/7p/3P4/1P6/P1P1PPPP/BNRB1KNR w HChc - +perft 1 32 +perft 2 845 +perft 3 26876 +perft 4 742888 +perft 5 23717883 +perft 6 682154649 + +id 225 +epd bn1qkb1r/pprppppp/8/2p5/2PPP1n1/8/PPR2PPP/BN1QKBNR w Hh - +perft 1 32 +perft 2 856 +perft 3 27829 +perft 4 768595 +perft 5 25245957 +perft 6 727424329 + +id 226 +epd 1nrqknrb/p1pp1ppp/1p2p3/3N4/5P1P/5b2/PPPPP3/B1RQKNRB w GCgc - +perft 1 33 +perft 2 873 +perft 3 27685 +perft 4 779473 +perft 5 25128076 +perft 6 745401024 + +id 227 +epd nbbrqrk1/pppppppp/8/2N1n3/P7/6P1/1PPPPP1P/1BBRQKNR w HD - +perft 1 25 +perft 2 555 +perft 3 14339 +perft 4 342296 +perft 5 9153089 +perft 6 234841945 + +id 228 +epd 1rbbqknr/1ppp1pp1/1n2p3/p6p/4P1P1/P6N/1PPP1P1P/NRBBQK1R w HBhb - +perft 1 25 +perft 2 693 +perft 3 18652 +perft 4 528070 +perft 5 15133381 +perft 6 439344945 + +id 229 +epd nrq1kbnr/p1pbpppp/3p4/1p6/6P1/1N3N2/PPPPPP1P/1RBQKB1R w HBhb - +perft 1 24 +perft 2 648 +perft 3 16640 +perft 4 471192 +perft 5 12871967 +perft 6 380436777 + +id 230 +epd nr1qknr1/p1pppp1p/b5p1/1p6/8/P4PP1/1bPPP1RP/NRBQKN1B w Bgb - +perft 1 18 +perft 2 533 +perft 3 11215 +perft 4 331243 +perft 5 7777833 +perft 6 234905172 + +id 231 +epd nbrqbknr/1ppp2pp/8/4pp2/p2PP1P1/7N/PPP2P1P/NBRQBK1R w HChc - +perft 1 29 +perft 2 803 +perft 3 24416 +perft 4 706648 +perft 5 22305910 +perft 6 672322762 + +id 232 +epd nr1b1k1r/ppp1pppp/2bp1n2/6P1/2P3q1/5P2/PP1PP2P/NRQBBKNR w HBhb - +perft 1 27 +perft 2 1199 +perft 3 30908 +perft 4 1296241 +perft 5 35121759 +perft 6 1418677099 + +id 233 +epd nrqkbbnr/2pppp1p/p7/1p6/2P1Pp2/8/PPNP2PP/1RQKBBNR w HBhb - +perft 1 28 +perft 2 613 +perft 3 17874 +perft 4 432750 +perft 5 13097064 +perft 6 345294379 + +id 234 +epd 1rqkbnrb/pp1ppp1p/1n4p1/B1p5/3PP3/4N3/PPP2PPP/NRQK2RB w GBgb - +perft 1 33 +perft 2 723 +perft 3 23991 +perft 4 590970 +perft 5 19715083 +perft 6 535650233 + +id 235 +epd nbrqkn1r/1pppp2p/5pp1/p2b4/5P2/P2PN3/1PP1P1PP/NBRQK1BR w HChc - +perft 1 23 +perft 2 607 +perft 3 15482 +perft 4 400970 +perft 5 11026383 +perft 6 290708878 + +id 236 +epd nrqbknbr/pp1pppp1/8/2p4p/P3PP2/8/1PPP2PP/NRQBKNBR w HBhb - +perft 1 26 +perft 2 700 +perft 3 19371 +perft 4 556026 +perft 5 16058815 +perft 6 485460242 + +id 237 +epd nrqknbbr/p2pppp1/1pp5/6Qp/3P4/1P3P2/P1P1P1PP/NR1KNBBR w HBhb - +perft 1 40 +perft 2 905 +perft 3 32932 +perft 4 829746 +perft 5 29263502 +perft 6 791963709 + +id 238 +epd nrqknrbb/1p3ppp/p2p4/2p1p3/1P6/3PP1P1/P1P2P1P/NRQKNRBB w FBfb - +perft 1 29 +perft 2 780 +perft 3 22643 +perft 4 654495 +perft 5 19532077 +perft 6 593181101 + +id 239 +epd 1bnrkqnr/p1pppp2/7p/1p4p1/4b3/7N/PPPP1PPP/BBNRKQ1R w HDhd - +perft 1 25 +perft 2 725 +perft 3 19808 +perft 4 565006 +perft 5 16661676 +perft 6 487354613 + +id 240 +epd bnrbkq1r/pp2p1pp/5n2/2pp1p2/P7/N1PP4/1P2PPPP/B1RBKQNR w HChc - +perft 1 24 +perft 2 745 +perft 3 18494 +perft 4 584015 +perft 5 15079602 +perft 6 488924040 + +id 241 +epd 2rkqbnr/p1pppppp/2b5/1pn5/1P3P1Q/2B5/P1PPP1PP/1NRK1BNR w HChc - +perft 1 33 +perft 2 904 +perft 3 30111 +perft 4 840025 +perft 5 28194726 +perft 6 801757709 + +id 242 +epd bnrkqnrb/2pppp2/8/pp4pp/1P5P/6P1/P1PPPPB1/BNRKQNR1 w GCgc - +perft 1 34 +perft 2 1059 +perft 3 34090 +perft 4 1054311 +perft 5 33195397 +perft 6 1036498304 + +id 243 +epd 1bbrkq1r/pppp2pp/1n2pp1n/8/2PP4/1N4P1/PP2PP1P/1BBRKQNR w HDhd - +perft 1 33 +perft 2 891 +perft 3 28907 +perft 4 814247 +perft 5 26970098 +perft 6 788040469 + +id 244 +epd nrbbkqnr/1p2pp1p/p1p3p1/3p4/8/1PP5/P2PPPPP/NRBBKQNR w HBhb - +perft 1 21 +perft 2 567 +perft 3 13212 +perft 4 376487 +perft 5 9539687 +perft 6 284426039 + +id 245 +epd 1rbkqbr1/ppp1pppp/1n5n/3p4/3P4/1PP3P1/P3PP1P/NRBKQBNR w HBb - +perft 1 27 +perft 2 752 +perft 3 20686 +perft 4 606783 +perft 5 16986290 +perft 6 521817800 + +id 246 +epd nrbkq1rb/1ppp1pp1/4p1n1/p6p/2PP4/5P2/PPK1P1PP/NRB1QNRB w gb - +perft 1 35 +perft 2 697 +perft 3 23678 +perft 4 505836 +perft 5 16906409 +perft 6 390324794 + +id 247 +epd nbrkbqnr/p2pp1p1/5p2/1pp4p/7P/3P2P1/PPP1PP2/NBKRBQNR w hc - +perft 1 25 +perft 2 679 +perft 3 17223 +perft 4 484921 +perft 5 12879258 +perft 6 376652259 + +id 248 +epd nrkb1qnr/ppppp1p1/6bp/5p2/1PP1P1P1/8/P2P1P1P/NRKBBQNR w HBhb - +perft 1 32 +perft 2 761 +perft 3 24586 +perft 4 632916 +perft 5 20671433 +perft 6 568524724 + +id 249 +epd nrk1bbnr/p1q1pppp/1ppp4/8/3P3P/4K3/PPP1PPP1/NR1QBBNR w hb - +perft 1 30 +perft 2 719 +perft 3 21683 +perft 4 541389 +perft 5 16278120 +perft 6 423649784 + +id 250 +epd nrkqbr1b/1pppp1pp/5pn1/p6N/1P3P2/8/P1PPP1PP/NRKQB1RB w GBb - +perft 1 26 +perft 2 494 +perft 3 13815 +perft 4 296170 +perft 5 8763742 +perft 6 206993496 + +id 251 +epd nbrkq2r/pppp1bpp/4p1n1/5p2/7P/2P3N1/PP1PPPP1/NBKRQ1BR w hc - +perft 1 27 +perft 2 701 +perft 3 19536 +perft 4 535052 +perft 5 15394667 +perft 6 443506342 + +id 252 +epd nrkbqnbr/2ppp2p/pp6/5pp1/P1P5/8/1P1PPPPP/NRKBQNBR w HBhb - +perft 1 21 +perft 2 487 +perft 3 11341 +perft 4 285387 +perft 5 7218486 +perft 6 193586674 + +id 253 +epd nr1qnbbr/pk1pppp1/1pp4p/8/3P4/5P1P/PPP1P1P1/NRKQNBBR w HB - +perft 1 22 +perft 2 546 +perft 3 13615 +perft 4 352855 +perft 5 9587439 +perft 6 259830255 + +id 254 +epd nrkq1rbb/pp1ppp1p/2pn4/8/PP3Pp1/7P/2PPP1P1/NRKQNRBB w FBfb - +perft 1 26 +perft 2 839 +perft 3 22075 +perft 4 723845 +perft 5 19867117 +perft 6 658535326 + +id 255 +epd b2rknqr/pp1ppppp/8/2P5/n7/P7/1PPNPPPb/BBNRK1QR w HDhd - +perft 1 24 +perft 2 699 +perft 3 19523 +perft 4 575172 +perft 5 17734818 +perft 6 535094237 + +id 256 +epd bnrbknqr/pp2p2p/2p3p1/3p1p2/8/3P4/PPPNPPPP/B1RBKNQR w HChc - +perft 1 23 +perft 2 580 +perft 3 14320 +perft 4 385917 +perft 5 10133092 +perft 6 288041554 + +id 257 +epd bnrknb1r/pppp2pp/8/4pp2/6P1/3P3P/qPP1PPQ1/BNRKNB1R w HChc - +perft 1 28 +perft 2 1100 +perft 3 31813 +perft 4 1217514 +perft 5 36142423 +perft 6 1361341249 + +id 258 +epd b1rknqrb/ppp1p1p1/2np1p1p/8/4N3/6PQ/PPPPPP1P/B1RKN1RB w GCgc - +perft 1 36 +perft 2 629 +perft 3 23082 +perft 4 453064 +perft 5 16897544 +perft 6 367503974 + +id 259 +epd nb1rknqr/pbppp2p/6p1/1p3p2/5P2/3KP3/PPPP2PP/NBBR1NQR w hd - +perft 1 18 +perft 2 557 +perft 3 9779 +perft 4 300744 +perft 5 5822387 +perft 6 180936551 + +id 260 +epd nr1bknqr/1ppb1ppp/p7/3pp3/B7/2P3NP/PP1PPPP1/NRB1K1QR w HBhb - +perft 1 28 +perft 2 688 +perft 3 19541 +perft 4 519785 +perft 5 15153092 +perft 6 425149249 + +id 261 +epd nrbkn2r/pppp1pqp/4p1p1/8/3P2P1/P3B3/P1P1PP1P/NR1KNBQR w HBhb - +perft 1 32 +perft 2 808 +perft 3 25578 +perft 4 676525 +perft 5 22094260 +perft 6 609377239 + +id 262 +epd nrbknqrb/2p1ppp1/1p6/p2p2Bp/1P6/3P1P2/P1P1P1PP/NR1KNQRB w GBgb - +perft 1 30 +perft 2 625 +perft 3 18288 +perft 4 418895 +perft 5 12225742 +perft 6 301834282 + +id 263 +epd nbr1knqr/1pp1p1pp/3p1pb1/8/7P/5P2/PPPPPQP1/NBRKBN1R w HC - +perft 1 29 +perft 2 863 +perft 3 25767 +perft 4 800239 +perft 5 24965592 +perft 6 799182442 + +id 264 +epd n1kbbnqr/prp2ppp/1p1p4/4p3/1P2P3/3P1B2/P1P2PPP/NRK1BNQR w HBh - +perft 1 26 +perft 2 653 +perft 3 17020 +perft 4 449719 +perft 5 12187583 +perft 6 336872952 + +id 265 +epd nrknbbqr/pp3p1p/B3p1p1/2pp4/4P3/2N3P1/PPPP1P1P/NRK1B1QR w HBhb - +perft 1 29 +perft 2 683 +perft 3 19755 +perft 4 501807 +perft 5 14684565 +perft 6 394951291 + +id 266 +epd n1knbqrb/pr1p1ppp/Qp6/2p1p3/4P3/6P1/PPPP1P1P/NRKNB1RB w GBg - +perft 1 31 +perft 2 552 +perft 3 17197 +perft 4 371343 +perft 5 11663330 +perft 6 283583340 + +id 267 +epd nbrknqbr/p3p1pp/1p1p1p2/2p5/2Q1PP2/8/PPPP2PP/NBRKN1BR w HChc - +perft 1 37 +perft 2 913 +perft 3 32470 +perft 4 825748 +perft 5 28899548 +perft 6 759875563 + +id 268 +epd nrkb1qbr/pp1pppp1/5n2/7p/2p5/1N1NPP2/PPPP2PP/1RKB1QBR w HBhb - +perft 1 25 +perft 2 712 +perft 3 18813 +perft 4 543870 +perft 5 15045589 +perft 6 445074372 + +id 269 +epd nrk2bbr/pppqpppp/3p4/8/1P3nP1/3P4/P1P1PP1P/NRKNQBBR w HBhb - +perft 1 24 +perft 2 814 +perft 3 19954 +perft 4 670162 +perft 5 17603960 +perft 6 592121050 + +id 270 +epd nrknqrbb/1p2ppp1/2pp4/Q6p/P2P3P/8/1PP1PPP1/NRKN1RBB w FBfb - +perft 1 34 +perft 2 513 +perft 3 16111 +perft 4 303908 +perft 5 9569590 +perft 6 206509331 + +id 271 +epd bbnrk1rq/pp2p1pp/2ppn3/5p2/8/3NNP1P/PPPPP1P1/BB1RK1RQ w GDgd - +perft 1 28 +perft 2 697 +perft 3 20141 +perft 4 517917 +perft 5 15301879 +perft 6 410843713 + +id 272 +epd bnrbknrq/ppppp2p/6p1/5p2/4QPP1/8/PPPPP2P/BNRBKNR1 w GCgc - +perft 1 37 +perft 2 901 +perft 3 32612 +perft 4 877372 +perft 5 31385912 +perft 6 903831981 + +id 273 +epd bnkrnbrq/ppppp1p1/B6p/5p2/8/4P3/PPPP1PPP/BNKRN1RQ w - - +perft 1 26 +perft 2 417 +perft 3 11124 +perft 4 217095 +perft 5 5980981 +perft 6 133080499 + +id 274 +epd bnrk1rqb/2pppp1p/3n4/pp4p1/3Q1P2/2N3P1/PPPPP2P/B1RKNR1B w FCfc - +perft 1 49 +perft 2 1655 +perft 3 74590 +perft 4 2512003 +perft 5 107234294 +perft 6 3651608327 + +id 275 +epd nbbrk1rq/pp2pppp/2pp4/8/2P2n2/6N1/PP1PP1PP/NBBRKR1Q w Dgd - +perft 1 28 +perft 2 960 +perft 3 26841 +perft 4 884237 +perft 5 26083252 +perft 6 846682836 + +id 276 +epd nrbb2rq/pppk1ppp/4p1n1/3p4/6P1/1BP5/PP1PPPQP/NRB1KNR1 w GB - +perft 1 28 +perft 2 735 +perft 3 22048 +perft 4 593839 +perft 5 18588316 +perft 6 512048946 + +id 277 +epd nrbk1brq/p1ppppp1/7p/1p6/4P1nP/P7/1PPP1PP1/NRBKNBRQ w GBgb - +perft 1 22 +perft 2 572 +perft 3 12739 +perft 4 351494 +perft 5 8525056 +perft 6 247615348 + +id 278 +epd nrbk1rqb/1pp2ppp/5n2/p2pp3/5B2/1N1P2P1/PPP1PP1P/1R1KNRQB w FBfb - +perft 1 35 +perft 2 927 +perft 3 31559 +perft 4 849932 +perft 5 28465693 +perft 6 783048748 + +id 279 +epd nbrkb1rq/p1pp1ppp/4n3/4p3/Pp6/6N1/1PPPPPPP/NBRKBRQ1 w Cgc - +perft 1 20 +perft 2 456 +perft 3 10271 +perft 4 247733 +perft 5 6124625 +perft 6 154766108 + +id 280 +epd nrkb1nrq/p2pp1pp/1pp2p2/7b/6PP/5P2/PPPPP2N/NRKBB1RQ w GBgb - +perft 1 21 +perft 2 479 +perft 3 11152 +perft 4 264493 +perft 5 6696458 +perft 6 165253524 + +id 281 +epd nr1nbbr1/pppkpp1p/6p1/3p4/P6P/1P6/1RPPPPP1/N1KNBBRQ w G - +perft 1 20 +perft 2 498 +perft 3 11304 +perft 4 288813 +perft 5 7197322 +perft 6 188021682 + +id 282 +epd nrknbrqb/3p1ppp/ppN1p3/8/6P1/8/PPPPPP1P/1RKNBRQB w FBfb - +perft 1 32 +perft 2 526 +perft 3 17267 +perft 4 319836 +perft 5 10755190 +perft 6 220058991 + +id 283 +epd nbrkn1bq/p1pppr1p/1p6/5pp1/8/1N2PP2/PPPP2PP/1BKRNRBQ w c - +perft 1 19 +perft 2 491 +perft 3 10090 +perft 4 277313 +perft 5 6230616 +perft 6 180748649 + +id 284 +epd nrkbnrbq/ppppppp1/8/8/7p/PP3P2/2PPPRPP/NRKBN1BQ w Bfb - +perft 1 16 +perft 2 353 +perft 3 6189 +perft 4 156002 +perft 5 3008668 +perft 6 82706705 + +id 285 +epd nrknrbbq/p4ppp/2p1p3/1p1p4/1P2P3/2P5/P1NP1PPP/1RKNRBBQ w EBeb - +perft 1 29 +perft 2 728 +perft 3 21915 +perft 4 587668 +perft 5 18231199 +perft 6 511686397 + +id 286 +epd nrknr1bb/pppp1p2/7p/2qPp1p1/8/1P5P/P1P1PPP1/NRKNRQBB w EBeb - +perft 1 20 +perft 2 714 +perft 3 14336 +perft 4 500458 +perft 5 11132758 +perft 6 386064577 + +id 287 +epd bbqnrrkn/ppp2p1p/3pp1p1/8/1PP5/2Q5/P1BPPPPP/B2NRKRN w GE - +perft 1 39 +perft 2 593 +perft 3 23446 +perft 4 424799 +perft 5 16764576 +perft 6 346185058 + +id 288 +epd bqn1rkrn/p1p2ppp/1p1p4/4p3/3PP2b/8/PPP2PPP/BQNBRKRN w GEge - +perft 1 25 +perft 2 773 +perft 3 20042 +perft 4 616817 +perft 5 16632403 +perft 6 515838333 + +id 289 +epd bqnrkb1n/p1p1pprp/3p4/1p2P1p1/2PP4/8/PP3PPP/BQNRKBRN w GDd - +perft 1 31 +perft 2 860 +perft 3 28102 +perft 4 810379 +perft 5 27233018 +perft 6 813751250 + +id 290 +epd bqr1krnb/ppppppp1/7p/3n4/1P4P1/P4N2/2PPPP1P/BQNRKR1B w FDf - +perft 1 31 +perft 2 709 +perft 3 22936 +perft 4 559830 +perft 5 18608857 +perft 6 480498340 + +id 291 +epd qbbn1krn/pp3ppp/4r3/2ppp3/P1P4P/8/1P1PPPP1/QBBNRKRN w GEg - +perft 1 26 +perft 2 775 +perft 3 21100 +perft 4 649673 +perft 5 18476807 +perft 6 582542257 + +id 292 +epd qnbbrkrn/1p1pp2p/p7/2p2pp1/8/4P2P/PPPP1PPK/QNBBRR1N w ge - +perft 1 25 +perft 2 599 +perft 3 15139 +perft 4 389104 +perft 5 10260500 +perft 6 279222412 + +id 293 +epd qnbrkbrn/1ppp2p1/p3p2p/5p2/P4P2/1P6/2PPP1PP/QNBRKBRN w GDgd - +perft 1 27 +perft 2 588 +perft 3 16735 +perft 4 394829 +perft 5 11640416 +perft 6 293541380 + +id 294 +epd 1nbrkrnb/p1pppp1p/1pq3p1/8/4P3/P1P4N/1P1P1PPP/QNBRKR1B w FDfd - +perft 1 18 +perft 2 609 +perft 3 11789 +perft 4 406831 +perft 5 8604788 +perft 6 299491047 + +id 295 +epd qb1r1krn/pppp2pp/1n2ppb1/4P3/7P/8/PPPP1PP1/QBNRBKRN w GDgd - +perft 1 20 +perft 2 578 +perft 3 12205 +perft 4 349453 +perft 5 7939483 +perft 6 229142178 + +id 296 +epd qnr1bkrn/p3pppp/1bpp4/1p6/2P2PP1/8/PP1PPN1P/QNRBBKR1 w GCgc - +perft 1 30 +perft 2 865 +perft 3 26617 +perft 4 771705 +perft 5 24475596 +perft 6 719842237 + +id 297 +epd 1nkrbbrn/qppppppp/8/8/p2P4/1P5P/P1P1PPP1/QNKRBBRN w - - +perft 1 27 +perft 2 672 +perft 3 18371 +perft 4 505278 +perft 5 14065717 +perft 6 410130412 + +id 298 +epd 1qrkbrnb/ppp1p1pp/n2p4/5p2/4N3/8/PPPPPPPP/Q1RKBRNB w Ffc - +perft 1 25 +perft 2 718 +perft 3 18573 +perft 4 536771 +perft 5 14404324 +perft 6 424279467 + +id 299 +epd q1nrkrbn/pp1pppp1/2p4p/8/P7/5Pb1/BPPPPNPP/Q1NRKRB1 w FDfd - +perft 1 22 +perft 2 558 +perft 3 12911 +perft 4 336042 +perft 5 8516966 +perft 6 228074630 + +id 300 +epd qnrbkrbn/1p1p1pp1/p1p5/4p2p/8/3P1P2/PPP1P1PP/QNRBKRBN w FCfc - +perft 1 28 +perft 2 669 +perft 3 17713 +perft 4 440930 +perft 5 12055174 +perft 6 313276304 + +id 301 +epd qnrkr1bn/p1pp1ppp/8/1p2p3/3P1P2/bP4P1/P1P1P2P/QNRKRBBN w ECec - +perft 1 23 +perft 2 845 +perft 3 20973 +perft 4 759778 +perft 5 19939053 +perft 6 718075943 + +id 302 +epd q1krrnbb/p1p1pppp/2np4/1pB5/5P2/8/PPPPP1PP/QNRKRN1B w EC - +perft 1 29 +perft 2 776 +perft 3 21966 +perft 4 631941 +perft 5 18110831 +perft 6 549019739 + +id 303 +epd bbn1rkrn/pp1p1ppp/8/2p1p1q1/6P1/P7/BPPPPP1P/B1NQRKRN w GEge - +perft 1 26 +perft 2 936 +perft 3 25177 +perft 4 906801 +perft 5 24984621 +perft 6 901444251 + +id 304 +epd bn1brkrn/pp1qpp1p/2p3p1/3p4/1PPP4/P7/4PPPP/BNQBRKRN w GEge - +perft 1 29 +perft 2 755 +perft 3 22858 +perft 4 645963 +perft 5 20128587 +perft 6 600207069 + +id 305 +epd b2rkbrn/p1pppppp/qp6/8/1n6/2B2P2/P1PPP1PP/1NQRKBRN w GDgd - +perft 1 24 +perft 2 878 +perft 3 21440 +perft 4 791007 +perft 5 20840078 +perft 6 775795187 + +id 306 +epd b2rkrnb/pqp1pppp/n7/1p1p4/P7/N1P2N2/1P1PPPPP/B1QRKR1B w FDfd - +perft 1 26 +perft 2 724 +perft 3 19558 +perft 4 571891 +perft 5 16109522 +perft 6 492933398 + +id 307 +epd 1bbqrkrn/ppppp1p1/8/5p1p/P1n3P1/3P4/1PP1PP1P/NBBQRRKN w ge - +perft 1 25 +perft 2 678 +perft 3 17351 +perft 4 461211 +perft 5 12173245 +perft 6 329661421 + +id 308 +epd nqb1rrkn/ppp1bppp/3pp3/8/3P4/1P6/PQP1PPPP/N1BBRRKN w - - +perft 1 23 +perft 2 503 +perft 3 12465 +perft 4 290341 +perft 5 7626054 +perft 6 188215608 + +id 309 +epd nqbrkbr1/p1pppppp/1p6/2N2n2/2P5/5P2/PP1PP1PP/1QBRKBRN w GDgd - +perft 1 29 +perft 2 688 +perft 3 20289 +perft 4 506302 +perft 5 15167248 +perft 6 399015237 + +id 310 +epd nqbrkrn1/1ppppp2/6pp/p7/1P6/2Q5/P1PPPPPP/N1BRKRNB w FDfd - +perft 1 36 +perft 2 602 +perft 3 20985 +perft 4 397340 +perft 5 13706856 +perft 6 291708797 + +id 311 +epd nbqrbrkn/pp1p1pp1/2p5/4p2p/2P3P1/1P3P2/P2PP2P/NBQRBKRN w GD - +perft 1 34 +perft 2 655 +perft 3 22581 +perft 4 474396 +perft 5 16613630 +perft 6 379344541 + +id 312 +epd nqrbbrkn/1p1pppp1/8/p1p4p/4P2P/1N4P1/PPPP1P2/1QRBBKRN w GC - +perft 1 23 +perft 2 597 +perft 3 14468 +perft 4 400357 +perft 5 10096863 +perft 6 294900903 + +id 313 +epd nqrkbbrn/2p1p1pp/pp1p1p2/8/P2N4/2P5/1P1PPPPP/1QRKBBRN w GCgc - +perft 1 32 +perft 2 744 +perft 3 23310 +perft 4 550728 +perft 5 17597164 +perft 6 428786656 + +id 314 +epd n1krbrnb/q1pppppp/p7/1p6/3Q4/2P2P2/PP1PP1PP/N1RKBRNB w FC - +perft 1 43 +perft 2 1038 +perft 3 41327 +perft 4 1074450 +perft 5 40918952 +perft 6 1126603824 + +id 315 +epd nb1rkrbn/p1pp1p1p/qp6/4p1p1/5PP1/P7/1PPPPB1P/NBQRKR1N w FDfd - +perft 1 26 +perft 2 645 +perft 3 16463 +perft 4 445464 +perft 5 11911314 +perft 6 342563372 + +id 316 +epd nqr1krbn/pppp1ppp/8/8/3pP3/5P2/PPPb1NPP/NQRBKRB1 w FCfc - +perft 1 2 +perft 2 51 +perft 3 1047 +perft 4 27743 +perft 5 612305 +perft 6 17040200 + +id 317 +epd n1rkrbbn/pqppppp1/7p/1p6/8/1NPP4/PP1KPPPP/1QR1RBBN w ec - +perft 1 25 +perft 2 674 +perft 3 17553 +perft 4 505337 +perft 5 13421727 +perft 6 403551903 + +id 318 +epd 1qrkrnbb/1p1p1ppp/pnp1p3/8/3PP3/P6P/1PP2PP1/NQRKRNBB w ECec - +perft 1 24 +perft 2 688 +perft 3 17342 +perft 4 511444 +perft 5 13322502 +perft 6 403441498 + +id 319 +epd 1bnrqkrn/2ppppp1/p7/1p1b3p/3PP1P1/8/PPPQ1P1P/BBNR1KRN w GDgd - +perft 1 35 +perft 2 925 +perft 3 32238 +perft 4 857060 +perft 5 30458921 +perft 6 824344087 + +id 320 +epd bnrbqkr1/ppp2pp1/6n1/3pp2p/1P6/2N3N1/P1PPPPPP/B1RBQRK1 w gc - +perft 1 23 +perft 2 704 +perft 3 17345 +perft 4 539587 +perft 5 14154852 +perft 6 450893738 + +id 321 +epd 1nrqkbrn/p1pppppp/8/1p1b4/P6P/5P2/1PPPP1P1/BNRQKBRN w GCgc - +perft 1 19 +perft 2 505 +perft 3 10619 +perft 4 281422 +perft 5 6450025 +perft 6 175593967 + +id 322 +epd b1rqkrnb/ppppppp1/8/6p1/3n4/NP6/P1PPPP1P/B1RQKRNB w FCfc - +perft 1 25 +perft 2 614 +perft 3 15578 +perft 4 377660 +perft 5 10391021 +perft 6 259629603 + +id 323 +epd nbbrqkrn/ppp3p1/3pp3/5p1p/1P2P3/P7/2PPQPPP/NBBR1KRN w GDgd - +perft 1 30 +perft 2 833 +perft 3 25719 +perft 4 717713 +perft 5 22873901 +perft 6 649556666 + +id 324 +epd nr1bqrk1/ppp1pppp/6n1/3pP3/8/5PQb/PPPP2PP/NRBB1KRN w GB - +perft 1 26 +perft 2 734 +perft 3 20161 +perft 4 582591 +perft 5 17199594 +perft 6 512134836 + +id 325 +epd 1rbqkbr1/ppppp1pp/1n6/4np2/3P1P2/6P1/PPPQP2P/NRB1KBRN w GBgb - +perft 1 27 +perft 2 662 +perft 3 17897 +perft 4 447464 +perft 5 13038519 +perft 6 338365642 + +id 326 +epd nr1qkr1b/ppp1pp1p/4bn2/3p2p1/4P3/1Q6/PPPP1PPP/NRB1KRNB w FBfb - +perft 1 33 +perft 2 939 +perft 3 30923 +perft 4 942138 +perft 5 30995969 +perft 6 991509814 + +id 327 +epd nb1qbkrn/pprp1pp1/7p/2p1pB2/Q1PP4/8/PP2PPPP/N1R1BKRN w GCg - +perft 1 47 +perft 2 1128 +perft 3 50723 +perft 4 1306753 +perft 5 56747878 +perft 6 1560584212 + +id 328 +epd nrqb1rkn/pp2pppp/2bp4/2p5/6P1/2P3N1/PP1PPP1P/NRQBBRK1 w - - +perft 1 24 +perft 2 828 +perft 3 21148 +perft 4 723705 +perft 5 19506135 +perft 6 668969549 + +id 329 +epd nrq1bbrn/ppkpp2p/2p3p1/P4p2/8/4P1N1/1PPP1PPP/NRQKBBR1 w GB - +perft 1 25 +perft 2 525 +perft 3 13533 +perft 4 309994 +perft 5 8250997 +perft 6 201795680 + +id 330 +epd Br1kbrn1/pqpppp2/8/6pp/3b2P1/1N6/PPPPPP1P/1RQKBRN1 w FBfb - +perft 1 20 +perft 2 790 +perft 3 18175 +perft 4 695905 +perft 5 17735648 +perft 6 669854148 + +id 331 +epd nbrqkrbn/2p1p1pp/p7/1p1p1p2/4P1P1/5P2/PPPP3P/NBRQKRBN w FCfc - +perft 1 29 +perft 2 771 +perft 3 22489 +perft 4 647106 +perft 5 19192982 +perft 6 591335970 + +id 332 +epd 1rqbkrbn/1ppppp1p/1n6/p1N3p1/8/2P4P/PP1PPPP1/1RQBKRBN w FBfb - +perft 1 29 +perft 2 502 +perft 3 14569 +perft 4 287739 +perft 5 8652810 +perft 6 191762235 + +id 333 +epd 1rqkrbbn/ppnpp1pp/8/2p5/6p1/3P4/PPP1PPPP/NRK1RBBN w eb - +perft 1 19 +perft 2 531 +perft 3 10812 +perft 4 300384 +perft 5 6506674 +perft 6 184309316 + +id 334 +epd nrqkrnbb/p1pp2pp/5p2/4P3/2p5/4N3/PP1PP1PP/NRQKR1BB w EBeb - +perft 1 26 +perft 2 800 +perft 3 23256 +perft 4 756695 +perft 5 23952941 +perft 6 809841274 + +id 335 +epd bbnrkqrn/pp3pp1/4p2p/2pp4/4P1P1/1PB5/P1PP1P1P/1BNRKQRN w GDgd - +perft 1 33 +perft 2 915 +perft 3 30536 +perft 4 878648 +perft 5 29602610 +perft 6 881898159 + +id 336 +epd bnrbkqr1/1p2pppp/6n1/p1pp4/7P/P3P3/1PPPKPP1/BNRB1QRN w gc - +perft 1 19 +perft 2 457 +perft 3 9332 +perft 4 238944 +perft 5 5356253 +perft 6 144653627 + +id 337 +epd b1rkqbrn/pp1p2pp/2n1p3/2p2p2/3P2PP/8/PPP1PP2/BNKRQBRN w gc - +perft 1 30 +perft 2 985 +perft 3 30831 +perft 4 1011700 +perft 5 32684185 +perft 6 1080607773 + +id 338 +epd b1rkqrnb/2ppppp1/np6/p6p/1P6/P2P3P/2P1PPP1/BNRKQRNB w FCfc - +perft 1 26 +perft 2 692 +perft 3 18732 +perft 4 517703 +perft 5 14561181 +perft 6 413226841 + +id 339 +epd nbbrkqrn/1ppp1p2/p6p/4p1p1/5P2/1P5P/P1PPPNP1/NBBRKQR1 w GDgd - +perft 1 22 +perft 2 561 +perft 3 13222 +perft 4 367487 +perft 5 9307003 +perft 6 273928315 + +id 340 +epd nrbbkqrn/p1pppppp/8/1p6/4P3/7Q/PPPP1PPP/NRBBK1RN w GBgb - +perft 1 38 +perft 2 769 +perft 3 28418 +perft 4 632310 +perft 5 23091070 +perft 6 560139600 + +id 341 +epd nrbkqbrn/1pppp2p/8/p4pp1/P4PQ1/8/1PPPP1PP/NRBK1BRN w GBgb - +perft 1 23 +perft 2 507 +perft 3 13067 +perft 4 321423 +perft 5 8887567 +perft 6 237475184 + +id 342 +epd nr1kqr1b/pp2pppp/5n2/2pp4/P5b1/5P2/1PPPPRPP/NRBK1QNB w Bfb - +perft 1 18 +perft 2 626 +perft 3 12386 +perft 4 434138 +perft 5 9465555 +perft 6 335004239 + +id 343 +epd nbkrbqrn/1pppppp1/8/4P2p/pP6/P7/2PP1PPP/NBRKBQRN w GC - +perft 1 22 +perft 2 329 +perft 3 8475 +perft 4 148351 +perft 5 4160034 +perft 6 82875306 + +id 344 +epd nrkb1qrn/pp1pp1pp/8/5p1b/P1p4P/6N1/1PPPPPP1/NRKBBQR1 w GBgb - +perft 1 16 +perft 2 479 +perft 3 9037 +perft 4 275354 +perft 5 5862341 +perft 6 184959796 + +id 345 +epd 1rkq1brn/ppppp1pp/1n6/3b1p2/3N3P/5P2/PPPPP1P1/1RKQBBRN w GBgb - +perft 1 23 +perft 2 614 +perft 3 15324 +perft 4 418395 +perft 5 11090645 +perft 6 313526088 + +id 346 +epd nrk1brnb/pp1ppppp/2p5/3q4/5P2/PP6/1KPPP1PP/NR1QBRNB w fb - +perft 1 25 +perft 2 942 +perft 3 21765 +perft 4 792179 +perft 5 19318837 +perft 6 685549171 + +id 347 +epd nbrkqr1n/1pppp2p/p4pp1/2Bb4/5P2/6P1/PPPPP2P/NBRKQ1RN w Cfc - +perft 1 30 +perft 2 841 +perft 3 24775 +perft 4 677876 +perft 5 20145765 +perft 6 557578726 + +id 348 +epd n1kbqrbn/2p1pppp/1r6/pp1p4/P7/3P4/1PP1PPPP/NRKBQRBN w FBf - +perft 1 21 +perft 2 591 +perft 3 14101 +perft 4 394289 +perft 5 10295086 +perft 6 292131422 + +id 349 +epd nrkqrbb1/ppp1pppp/3p4/8/4P3/2Pn1P2/PP4PP/NRKQRBBN w EBeb - +perft 1 4 +perft 2 88 +perft 3 3090 +perft 4 73414 +perft 5 2640555 +perft 6 66958031 + +id 350 +epd nrkqrnbb/ppppp1p1/7p/1P3p2/3P4/2P5/P3PPPP/NRKQRNBB w EBeb - +perft 1 29 +perft 2 689 +perft 3 21091 +perft 4 508789 +perft 5 16226660 +perft 6 408570219 + +id 351 +epd bbnr1rqn/pp2pkpp/2pp1p2/8/4P1P1/8/PPPP1P1P/BBNRKRQN w FD - +perft 1 21 +perft 2 463 +perft 3 11135 +perft 4 256244 +perft 5 6826249 +perft 6 165025370 + +id 352 +epd bnrbk1qn/1pppprpp/8/p4p1P/6P1/3P4/PPP1PP2/BNRBKRQN w FCc - +perft 1 22 +perft 2 459 +perft 3 11447 +perft 4 268157 +perft 5 7371098 +perft 6 190583454 + +id 353 +epd 1nrkrbqn/p1pp1ppp/4p3/1p6/1PP5/6PB/P2PPPbP/BNRKR1QN w ECec - +perft 1 30 +perft 2 931 +perft 3 29012 +perft 4 887414 +perft 5 28412902 +perft 6 869228014 + +id 354 +epd b1rkr1nb/pppppqp1/n4B2/7p/8/1P4P1/P1PPPP1P/1NKRRQNB w ec - +perft 1 36 +perft 2 934 +perft 3 31790 +perft 4 930926 +perft 5 30392925 +perft 6 952871799 + +id 355 +epd nbbrkrqn/p1ppp1p1/8/1p3p1p/2P3PP/8/PP1PPPQ1/NBBRKR1N w FDfd - +perft 1 34 +perft 2 938 +perft 3 31848 +perft 4 921716 +perft 5 31185844 +perft 6 944483246 + +id 356 +epd 1rbbkrqn/ppp1pp2/1n1p2p1/7p/P3P1P1/3P4/1PP2P1P/NRBBKRQN w FBfb - +perft 1 26 +perft 2 646 +perft 3 18083 +perft 4 472744 +perft 5 14006203 +perft 6 384101783 + +id 357 +epd nrbkrbq1/Qpppp1pp/2n5/5p2/P4P2/6N1/1PPPP1PP/NRBKRB2 w EBeb - +perft 1 27 +perft 2 619 +perft 3 16713 +perft 4 421845 +perft 5 11718463 +perft 6 313794027 + +id 358 +epd 1rbkr1nb/pppp1qpp/1n6/4pp2/1PP1P3/8/PB1P1PPP/NR1KRQNB w EBeb - +perft 1 32 +perft 2 1029 +perft 3 32970 +perft 4 1080977 +perft 5 35483796 +perft 6 1181835398 + +id 359 +epd nbrk1rqn/p1ppp2p/1p6/5ppb/8/1N2P2P/PPPP1PP1/1BKRBRQN w fc - +perft 1 18 +perft 2 594 +perft 3 12350 +perft 4 408544 +perft 5 9329122 +perft 6 315021712 + +id 360 +epd nrkbbrqn/3pppp1/7p/ppp5/P7/1N5P/1PPPPPP1/1RKBBRQN w FBfb - +perft 1 19 +perft 2 417 +perft 3 9026 +perft 4 218513 +perft 5 5236331 +perft 6 137024458 + +id 361 +epd nrkr1bqn/ppp1pppp/3p4/1b6/7P/P7/1PPPPPP1/NRKRBBQN w DBdb - +perft 1 17 +perft 2 457 +perft 3 9083 +perft 4 243872 +perft 5 5503579 +perft 6 150091997 + +id 362 +epd nrkrbqnb/p4ppp/1p2p3/2pp4/6P1/2P2N2/PPNPPP1P/1RKRBQ1B w DBdb - +perft 1 27 +perft 2 755 +perft 3 21012 +perft 4 620093 +perft 5 17883987 +perft 6 547233320 + +id 363 +epd nbkrr1bn/ppB2ppp/4p3/2qp4/4P3/5P2/PPPP2PP/NBRKRQ1N w EC - +perft 1 37 +perft 2 1473 +perft 3 51939 +perft 4 1956521 +perft 5 68070015 +perft 6 2490912491 + +id 364 +epd n1kbrqbn/p1pp1pp1/4p2p/2B5/1r3P2/8/PPPPP1PP/NRKBRQ1N w EBe - +perft 1 30 +perft 2 1029 +perft 3 30874 +perft 4 1053163 +perft 5 32318550 +perft 6 1106487743 + +id 365 +epd nrkrqbbn/2pppp1p/8/pp6/1P1P2p1/P5P1/2P1PP1P/NRKRQBBN w DBdb - +perft 1 22 +perft 2 421 +perft 3 10034 +perft 4 221927 +perft 5 5754555 +perft 6 141245633 + +id 366 +epd nrkr1nbb/1ppp2pp/p3q3/4pp2/2P5/P3P3/1PKP1PPP/NR1RQNBB w db - +perft 1 22 +perft 2 619 +perft 3 13953 +perft 4 411392 +perft 5 9905109 +perft 6 301403003 + +id 367 +epd bbnrkrnq/1pp1p2p/6p1/p2p1p2/8/1P2P3/P1PP1PPP/BBNRKRNQ w FDfd - +perft 1 27 +perft 2 805 +perft 3 21915 +perft 4 688224 +perft 5 19133881 +perft 6 620749189 + +id 368 +epd bnrbkrn1/pp1ppp2/2p3pp/8/2Pq4/P4PP1/1P1PP2P/BNRBKRNQ w FCfc - +perft 1 20 +perft 2 770 +perft 3 16593 +perft 4 577980 +perft 5 13581691 +perft 6 456736500 + +id 369 +epd b1rkrbnq/1pp1pppp/2np4/p5N1/8/1P2P3/P1PP1PPP/BNRKRB1Q w ECec - +perft 1 37 +perft 2 740 +perft 3 27073 +perft 4 581744 +perft 5 21156664 +perft 6 485803600 + +id 370 +epd b1krrnqb/pp1ppp1p/n1p3p1/2N5/6P1/8/PPPPPP1P/B1RKRNQB w EC - +perft 1 34 +perft 2 850 +perft 3 28494 +perft 4 752350 +perft 5 25360295 +perft 6 698159474 + +id 371 +epd 1bbr1rnq/ppppkppp/8/3np3/4P3/3P4/PPP1KPPP/NBBRR1NQ w - - +perft 1 27 +perft 2 704 +perft 3 18290 +perft 4 480474 +perft 5 12817011 +perft 6 341026662 + +id 372 +epd nrbbk1nq/p1p1prpp/1p6/N2p1p2/P7/8/1PPPPPPP/R1BBKRNQ w Fb - +perft 1 23 +perft 2 552 +perft 3 13710 +perft 4 348593 +perft 5 9236564 +perft 6 248469879 + +id 373 +epd 1rbkrb1q/1pppp1pp/1n5n/p4p2/P3P3/1P6/2PPNPPP/NRBKRB1Q w EBeb - +perft 1 22 +perft 2 415 +perft 3 10198 +perft 4 217224 +perft 5 5735644 +perft 6 135295774 + +id 374 +epd nrbkr1qb/1pp1pppp/6n1/p2p4/2P1P3/1N4N1/PP1P1PPP/1RBKR1QB w EBeb - +perft 1 27 +perft 2 709 +perft 3 19126 +perft 4 506214 +perft 5 14192779 +perft 6 380516508 + +id 375 +epd nbrkbrnq/p3p1pp/1pp2p2/3p4/1PP5/4P3/P1KP1PPP/NBR1BRNQ w fc - +perft 1 24 +perft 2 715 +perft 3 18009 +perft 4 535054 +perft 5 14322279 +perft 6 427269976 + +id 376 +epd nrk1brnq/pp1p1pp1/7p/b1p1p3/1P6/6P1/P1PPPPQP/NRKBBRN1 w FBfb - +perft 1 29 +perft 2 675 +perft 3 20352 +perft 4 492124 +perft 5 15316285 +perft 6 389051744 + +id 377 +epd nrkr1bnq/1p2pppp/p2p4/1bp5/PP6/1R5N/2PPPPPP/N1KRBB1Q w Ddb - +perft 1 27 +perft 2 744 +perft 3 20494 +perft 4 571209 +perft 5 16188945 +perft 6 458900901 + +id 378 +epd nrk1b1qb/pppn1ppp/3rp3/3p4/2P3P1/3P4/PPN1PP1P/1RKRBNQB w DBb - +perft 1 35 +perft 2 941 +perft 3 33203 +perft 4 935791 +perft 5 33150360 +perft 6 968024386 + +id 379 +epd nb1rrnbq/ppkp1ppp/8/2p1p3/P7/1N2P3/1PPP1PPP/1BKRRNBQ w - - +perft 1 19 +perft 2 451 +perft 3 9655 +perft 4 235472 +perft 5 5506897 +perft 6 139436165 + +id 380 +epd nrkbrnbq/4pppp/1ppp4/p7/2P1P3/3P2N1/PP3PPP/NRKBR1BQ w EBeb - +perft 1 29 +perft 2 591 +perft 3 17132 +perft 4 384358 +perft 5 11245508 +perft 6 270967202 + +id 381 +epd nrkrnbbq/3p1ppp/1p6/p1p1p3/3P2P1/P4Q2/1PP1PP1P/NRKRNBB1 w DBdb - +perft 1 38 +perft 2 792 +perft 3 28597 +perft 4 640961 +perft 5 22654797 +perft 6 540864616 + +id 382 +epd nr1rnqbb/ppp1pp1p/3k2p1/3p4/1P5P/3P1N2/P1P1PPP1/NRKR1QBB w DB - +perft 1 25 +perft 2 758 +perft 3 18547 +perft 4 543643 +perft 5 13890077 +perft 6 402109399 + +id 383 +epd bbqrnnkr/1ppp1p1p/5p2/p5p1/P7/1P4P1/2PPPP1P/1BQRNNKR w HDhd - +perft 1 20 +perft 2 322 +perft 3 7224 +perft 4 145818 +perft 5 3588435 +perft 6 82754650 + +id 384 +epd bqrb2k1/pppppppr/5nnp/8/3P1P2/4P1N1/PPP3PP/BQRBN1KR w HCc - +perft 1 25 +perft 2 597 +perft 3 15872 +perft 4 397970 +perft 5 11162476 +perft 6 295682250 + +id 385 +epd bqrnn1kr/1pppbppp/8/4p3/1p6/2P1N2P/P2PPPP1/BQR1NBKR w HChc - +perft 1 34 +perft 2 921 +perft 3 31695 +perft 4 864023 +perft 5 30126510 +perft 6 850296236 + +id 386 +epd bqr1nkr1/pppppp2/2n3p1/7p/1P1b1P2/8/PQP1P1PP/B1RNNKRB w GCgc - +perft 1 23 +perft 2 788 +perft 3 21539 +perft 4 686795 +perft 5 20849374 +perft 6 645694580 + +id 387 +epd qbbrnn1r/1pppp1pk/p7/5p1p/P2P3P/3N4/1PP1PPP1/QBBR1NKR w HD - +perft 1 34 +perft 2 713 +perft 3 24475 +perft 4 562189 +perft 5 19494094 +perft 6 482645160 + +id 388 +epd qrbb2kr/p1pppppp/1p1n4/8/1P3n2/P7/Q1PPP1PP/1RBBNNKR w HBhb - +perft 1 28 +perft 2 977 +perft 3 26955 +perft 4 949925 +perft 5 27802999 +perft 6 992109168 + +id 389 +epd qrb2bkr/1pp1pppp/2np1n2/pN6/3P4/4B3/PPP1PPPP/QR2NBKR w HBhb - +perft 1 27 +perft 2 730 +perft 3 20534 +perft 4 585091 +perft 5 17005916 +perft 6 507008968 + +id 390 +epd qrbnnkrb/pp2pp1p/8/2pp2p1/7P/P1P5/QP1PPPP1/1RBNNKRB w GBgb - +perft 1 24 +perft 2 813 +perft 3 21142 +perft 4 707925 +perft 5 19615756 +perft 6 655850285 + +id 391 +epd 1brnb1kr/p1pppppp/1p6/8/4q2n/1P2P1P1/PNPP1P1P/QBR1BNKR w HChc - +perft 1 17 +perft 2 734 +perft 3 13462 +perft 4 530809 +perft 5 11032633 +perft 6 416356876 + +id 392 +epd 1rnbbnkr/1pp1pppp/1q1p4/p7/4P3/5PN1/PPPP1BPP/QRNB2KR w HBhb - +perft 1 26 +perft 2 809 +perft 3 21764 +perft 4 706677 +perft 5 20292750 +perft 6 675408811 + +id 393 +epd qrnnbb1Q/ppp1pk1p/3p2p1/5p2/PP6/5P2/2PPP1PP/1RNNBBKR w HB - +perft 1 37 +perft 2 751 +perft 3 27902 +perft 4 603931 +perft 5 22443036 +perft 6 515122176 + +id 394 +epd qrnnbkrb/p3p1pp/3p1p2/1pp5/PP2P3/8/2PP1PPP/QRNNBRKB w gb - +perft 1 30 +perft 2 906 +perft 3 27955 +perft 4 872526 +perft 5 27658191 +perft 6 890966633 + +id 395 +epd qbrnnkbr/1p2pp1p/p1p3p1/3p4/6P1/P1N4P/1PPPPP2/QBR1NKBR w HChc - +perft 1 26 +perft 2 701 +perft 3 18930 +perft 4 521377 +perft 5 14733245 +perft 6 416881799 + +id 396 +epd qr1b1kbr/1p1ppppp/1n1n4/p1p5/4P3/5NPP/PPPP1P2/QRNB1KBR w HBhb - +perft 1 26 +perft 2 649 +perft 3 17235 +perft 4 451997 +perft 5 12367604 +perft 6 342165821 + +id 397 +epd qrnnkb1r/1pppppp1/7p/p4b2/4P3/5P1P/PPPP2PR/QRNNKBB1 w Bhb - +perft 1 34 +perft 2 941 +perft 3 31720 +perft 4 901240 +perft 5 30307554 +perft 6 888709821 + +id 398 +epd qr1nkrbb/p2ppppp/1pp5/8/3Pn3/1NP3P1/PP2PP1P/QR1NKRBB w FBfb - +perft 1 19 +perft 2 505 +perft 3 11107 +perft 4 294251 +perft 5 7046501 +perft 6 190414579 + +id 399 +epd bbrqn1kr/1pppp1pp/4n3/5p2/p5P1/3P4/PPP1PPKP/BBRQNN1R w hc - +perft 1 24 +perft 2 573 +perft 3 12963 +perft 4 335845 +perft 5 8191054 +perft 6 227555387 + +id 400 +epd brqb1nkr/pppppp1p/8/4N1pn/5P2/6P1/PPPPP2P/BRQB1NKR w HBhb - +perft 1 26 +perft 2 550 +perft 3 14338 +perft 4 331666 +perft 5 8903754 +perft 6 223437427 + +id 401 +epd brqnn1kr/pp3ppp/2pbp3/3p4/8/2NPP3/PPP1BPPP/BRQ1N1KR w HBhb - +perft 1 27 +perft 2 780 +perft 3 20760 +perft 4 589328 +perft 5 16243731 +perft 6 463883447 + +id 402 +epd brq1nkrb/ppp2ppp/8/n2pp2P/P7/4P3/1PPP1PP1/BRQNNKRB w GBgb - +perft 1 17 +perft 2 426 +perft 3 8295 +perft 4 235162 +perft 5 5048497 +perft 6 153986034 + +id 403 +epd rbbqn1kr/pp2p1pp/6n1/2pp1p2/2P4P/P7/BP1PPPP1/R1BQNNKR w HAha - +perft 1 27 +perft 2 916 +perft 3 25798 +perft 4 890435 +perft 5 26302461 +perft 6 924181432 + +id 404 +epd 1qbbn1kr/1ppppppp/r3n3/8/p1P5/P7/1P1PPPPP/RQBBNNKR w HAh - +perft 1 29 +perft 2 817 +perft 3 24530 +perft 4 720277 +perft 5 22147642 +perft 6 670707652 + +id 405 +epd rqbnnbkr/ppp1ppp1/7p/3p4/PP6/7P/1NPPPPP1/RQB1NBKR w HAa - +perft 1 23 +perft 2 572 +perft 3 14509 +perft 4 381474 +perft 5 10416981 +perft 6 288064942 + +id 406 +epd r1bnnkrb/q1ppp1pp/p7/1p3pB1/2P1P3/3P4/PP3PPP/RQ1NNKRB w GAga - +perft 1 31 +perft 2 925 +perft 3 27776 +perft 4 860969 +perft 5 26316355 +perft 6 843078864 + +id 407 +epd rbqnb1kr/ppppp1pp/5p2/5N2/7P/1n3P2/PPPPP1P1/RBQNB1KR w HAha - +perft 1 32 +perft 2 864 +perft 3 27633 +perft 4 766551 +perft 5 24738875 +perft 6 707188107 + +id 408 +epd rqnbbn1r/ppppppp1/6k1/8/6Pp/2PN4/PP1PPPKP/RQ1BBN1R w - - +perft 1 27 +perft 2 566 +perft 3 15367 +perft 4 347059 +perft 5 9714509 +perft 6 234622128 + +id 409 +epd rqnnbbkr/p1p2pp1/1p1p3p/4p3/4NP2/6P1/PPPPP2P/RQN1BBKR w HAha - +perft 1 27 +perft 2 631 +perft 3 17923 +perft 4 452734 +perft 5 13307890 +perft 6 356279813 + +id 410 +epd 1qnnbrkb/rppp1ppp/p3p3/8/4P3/2PP1P2/PP4PP/RQNNBKRB w GA - +perft 1 24 +perft 2 479 +perft 3 12135 +perft 4 271469 +perft 5 7204345 +perft 6 175460841 + +id 411 +epd rbqnn1br/p1pppk1p/1p4p1/5p2/8/P1P2P2/1PBPP1PP/R1QNNKBR w HA - +perft 1 31 +perft 2 756 +perft 3 23877 +perft 4 625194 +perft 5 20036784 +perft 6 554292502 + +id 412 +epd rqnbnkbr/1ppppp2/p5p1/8/1P4p1/4PP2/P1PP3P/RQNBNKBR w HAha - +perft 1 24 +perft 2 715 +perft 3 18536 +perft 4 575589 +perft 5 16013189 +perft 6 515078271 + +id 413 +epd rq1nkbbr/1p2pppp/p2n4/2pp4/1P4P1/P2N4/2PPPP1P/RQ1NKBBR w HAha - +perft 1 27 +perft 2 694 +perft 3 19840 +perft 4 552904 +perft 5 16685687 +perft 6 494574415 + +id 414 +epd r1nnkrbb/pp1pppp1/2p3q1/7p/8/1PPP3P/P3PPP1/RQNNKRBB w FAfa - +perft 1 18 +perft 2 520 +perft 3 10808 +perft 4 329085 +perft 5 7508201 +perft 6 235103697 + +id 415 +epd bbrnqk1r/pppp3p/6p1/4pp2/3P2P1/8/PPP1PP1P/BBRN1NKR w HC - +perft 1 22 +perft 2 566 +perft 3 12965 +perft 4 362624 +perft 5 8721079 +perft 6 259069471 + +id 416 +epd brnb1nkr/pppqpp2/3p2pp/8/3PP3/1P6/PBP2PPP/1RNBQNKR w HBhb - +perft 1 32 +perft 2 859 +perft 3 28517 +perft 4 817464 +perft 5 27734108 +perft 6 829785474 + +id 417 +epd brnq1b1r/ppp1ppkp/3p1np1/8/8/5P1P/PPPPPKPR/BRNQNB2 w - - +perft 1 21 +perft 2 511 +perft 3 10951 +perft 4 273756 +perft 5 6372681 +perft 6 167139732 + +id 418 +epd brnq1rkb/1pppppp1/3n3p/p7/8/P4NP1/1PPPPPRP/BRNQ1K1B w B - +perft 1 25 +perft 2 548 +perft 3 14049 +perft 4 341208 +perft 5 9015901 +perft 6 235249649 + +id 419 +epd rbb1qnkr/p1ppp1pp/1p3p2/6n1/8/1PN1P2P/P1PP1PP1/RBB1QNKR w HAha - +perft 1 25 +perft 2 673 +perft 3 16412 +perft 4 467660 +perft 5 12099119 +perft 6 361714466 + +id 420 +epd rnbb1nkr/1ppp1ppp/4p3/p5q1/6P1/1PP5/PB1PPP1P/RN1BQNKR w HAha - +perft 1 19 +perft 2 663 +perft 3 14149 +perft 4 489653 +perft 5 11491355 +perft 6 399135495 + +id 421 +epd rnbqnbkr/1pp1p2p/3p1p2/p5p1/5PP1/2P5/PPNPP2P/RNBQ1BKR w HAha - +perft 1 24 +perft 2 647 +perft 3 16679 +perft 4 461931 +perft 5 12649636 +perft 6 361157611 + +id 422 +epd rnb2krb/pppqppnp/8/3p2p1/1P4P1/7P/P1PPPPB1/RNBQNKR1 w GAga - +perft 1 24 +perft 2 722 +perft 3 18749 +perft 4 605229 +perft 5 16609220 +perft 6 563558512 + +id 423 +epd rbnqb1kr/pppn1pp1/3p3p/4p3/1P6/P7/R1PPPPPP/1BNQBNKR w Hha - +perft 1 20 +perft 2 538 +perft 3 12277 +perft 4 345704 +perft 5 8687621 +perft 6 255304141 + +id 424 +epd rnqb1nkr/p1pbp1pp/8/1pPp1p2/P2P4/8/1P2PPPP/RNQBBNKR w HAha - +perft 1 35 +perft 2 764 +perft 3 26952 +perft 4 632796 +perft 5 22592380 +perft 6 564255328 + +id 425 +epd rnq1bbkr/1p1ppp1p/4n3/p1p3p1/P1PP4/8/RP2PPPP/1NQNBBKR w Hha - +perft 1 29 +perft 2 709 +perft 3 21296 +perft 4 570580 +perft 5 17597398 +perft 6 506140370 + +id 426 +epd 1nqnbkrb/1pppp2p/r7/p4pp1/3P4/8/PPPBPPPP/RNQNK1RB w g - +perft 1 27 +perft 2 1028 +perft 3 28534 +perft 4 1050834 +perft 5 30251988 +perft 6 1096869832 + +id 427 +epd rbnqnkbr/p1pp1p1p/8/1p2p3/3P2pP/2P5/PP2PPP1/RBNQNKBR w HAha - +perft 1 32 +perft 2 832 +perft 3 27120 +perft 4 750336 +perft 5 24945574 +perft 6 724171581 + +id 428 +epd rnq1nkbr/1p1p1ppp/2p1pb2/p7/7P/2P5/PPNPPPPB/RNQB1K1R w HAha - +perft 1 31 +perft 2 779 +perft 3 24010 +perft 4 638640 +perft 5 19919434 +perft 6 551494771 + +id 429 +epd rnqnk1br/p1ppp1bp/1p3p2/6p1/4N3/P5P1/1PPPPP1P/R1QNKBBR w HAha - +perft 1 25 +perft 2 717 +perft 3 19396 +perft 4 576577 +perft 5 16525239 +perft 6 507175842 + +id 430 +epd rnq1krbb/p1p1pppp/8/1p1p4/1n5B/2N2P2/PPPPP1PP/RNQ1KR1B w FAfa - +perft 1 28 +perft 2 867 +perft 3 24029 +perft 4 735686 +perft 5 21112751 +perft 6 654808184 + +id 431 +epd bbrnnqkr/1pp1pppp/3p4/p7/P3P3/7P/1PPP1PP1/BBRNNQKR w HChc - +perft 1 24 +perft 2 405 +perft 3 11025 +perft 4 210557 +perft 5 6196438 +perft 6 131401224 + +id 432 +epd brnbnqkr/p1ppp3/1p5p/5Pp1/5P2/3N4/PPPPP2P/BRNB1QKR w HBhb g6 +perft 1 25 +perft 2 785 +perft 3 21402 +perft 4 698331 +perft 5 20687969 +perft 6 695850727 + +id 433 +epd br1nqbkr/1ppppp2/pn6/6pp/2PP4/1N4P1/PP2PP1P/BR1NQBKR w HBhb - +perft 1 25 +perft 2 596 +perft 3 16220 +perft 4 421882 +perft 5 12185361 +perft 6 337805606 + +id 434 +epd 1rnnqkrb/p2ppp1p/1pp5/2N3p1/8/1P6/P1PPPPKP/BR1NQ1RB w gb - +perft 1 38 +perft 2 960 +perft 3 34831 +perft 4 913665 +perft 5 32490040 +perft 6 880403591 + +id 435 +epd rbbnnqkr/pp3pp1/2p1p3/3p3p/3P3P/1PP5/P3PPP1/RBBNNQKR w HAha - +perft 1 30 +perft 2 785 +perft 3 23079 +perft 4 656618 +perft 5 19885037 +perft 6 599219582 + +id 436 +epd rn1bnqkr/p1ppppp1/8/1p5p/P4P1P/3N4/1PPPP1b1/RNBB1QKR w HAha - +perft 1 27 +perft 2 752 +perft 3 21735 +perft 4 613194 +perft 5 18862234 +perft 6 547415271 + +id 437 +epd 1nbnqbkr/1p1p1ppp/r3p3/p1p5/P3P3/3Q4/1PPP1PPP/RNBN1BKR w HAh - +perft 1 33 +perft 2 721 +perft 3 24278 +perft 4 572535 +perft 5 19648535 +perft 6 496023732 + +id 438 +epd rnbnqkrb/2pppppp/1p6/p7/1PP5/4N2P/P2PPPP1/RNB1QKRB w GAg - +perft 1 23 +perft 2 570 +perft 3 14225 +perft 4 374196 +perft 5 10022614 +perft 6 279545007 + +id 439 +epd rbnnbq1r/ppppppkp/6p1/N7/4P3/P7/1PPP1PPP/RB1NBQKR w HA - +perft 1 27 +perft 2 620 +perft 3 18371 +perft 4 440594 +perft 5 13909432 +perft 6 349478320 + +id 440 +epd r1nbbqkr/pppppp1p/8/8/1n3Pp1/3N1QP1/PPPPP2P/RN1BB1KR w HAha - +perft 1 31 +perft 2 791 +perft 3 25431 +perft 4 682579 +perft 5 22408813 +perft 6 636779732 + +id 441 +epd rnq1bbkr/pp1p1ppp/2pnp3/8/7P/1QP5/PP1PPPPR/RNN1BBK1 w Aha - +perft 1 28 +perft 2 559 +perft 3 16838 +perft 4 390887 +perft 5 12242780 +perft 6 315431511 + +id 442 +epd rnnqbrkb/2ppppp1/1p1N4/p6p/4P3/8/PPPP1PPP/R1NQBKRB w GA - +perft 1 32 +perft 2 638 +perft 3 20591 +perft 4 438792 +perft 5 14395828 +perft 6 331782223 + +id 443 +epd rbnnq1br/pppp1kp1/4pp2/7p/PP6/2PP4/4PPPP/RBNNQKBR w HA - +perft 1 21 +perft 2 521 +perft 3 12201 +perft 4 320429 +perft 5 8239159 +perft 6 227346638 + +id 444 +epd rnnbqkbr/p2ppp2/7p/1pp3p1/2P2N2/8/PP1PPPPP/RN1BQKBR w HAha - +perft 1 25 +perft 2 528 +perft 3 13896 +perft 4 326094 +perft 5 9079829 +perft 6 232750602 + +id 445 +epd rnn1kbbr/ppppqp2/6p1/2N1p2p/P7/2P5/1P1PPPPP/RN1QKBBR w HAha - +perft 1 27 +perft 2 801 +perft 3 22088 +perft 4 707078 +perft 5 20334071 +perft 6 682580976 + +id 446 +epd rnnqkrbb/p1p1p1pp/1p3p2/8/3p2Q1/P1P1P3/1P1P1PPP/RNN1KRBB w FAfa - +perft 1 37 +perft 2 1014 +perft 3 34735 +perft 4 998999 +perft 5 32921537 +perft 6 988770109 + +id 447 +epd bbrnk1qr/1pppppp1/p4n1p/8/P2P2N1/8/1PP1PPPP/BBR1NKQR w HC - +perft 1 21 +perft 2 481 +perft 3 11213 +perft 4 279993 +perft 5 7015419 +perft 6 187564853 + +id 448 +epd brnbnkqr/1pp1p1p1/p2p1p2/7p/1P4PP/8/PBPPPP2/1RNBNKQR w HBhb - +perft 1 31 +perft 2 743 +perft 3 24260 +perft 4 660177 +perft 5 22391185 +perft 6 653721389 + +id 449 +epd br2kbqr/ppppp1pp/3n1p2/3P4/3n3P/3N4/PPP1PPP1/BR1NKBQR w HBhb - +perft 1 25 +perft 2 872 +perft 3 22039 +perft 4 748726 +perft 5 20281962 +perft 6 685749952 + +id 450 +epd br1nkqrb/ppppppp1/8/7p/4P3/n1P2PP1/PP1P3P/BRNNKQRB w GBgb - +perft 1 28 +perft 2 607 +perft 3 16934 +perft 4 396483 +perft 5 11607818 +perft 6 294181806 + +id 451 +epd rbbn1kqr/pp1pp1p1/2pn3p/5p2/5P2/1P1N4/PNPPP1PP/RBB2KQR w HAha - +perft 1 27 +perft 2 725 +perft 3 21543 +perft 4 616082 +perft 5 19239812 +perft 6 581716972 + +id 452 +epd rnbbnk1r/pp1ppp1p/6q1/2p5/PP4p1/4P3/2PP1PPP/RNBBNKQR w HAha - +perft 1 25 +perft 2 1072 +perft 3 26898 +perft 4 1088978 +perft 5 28469879 +perft 6 1122703887 + +id 453 +epd rnbnkbqr/1pp3pp/3p4/p3pp2/3P2P1/2N1N3/PPP1PP1P/R1B1KBQR w HAha - +perft 1 31 +perft 2 1028 +perft 3 32907 +perft 4 1095472 +perft 5 36025223 +perft 6 1211187800 + +id 454 +epd r1bnkqrb/1ppppppp/p3n3/8/6P1/4N3/PPPPPPRP/RNB1KQ1B w Aga - +perft 1 23 +perft 2 457 +perft 3 11416 +perft 4 250551 +perft 5 6666787 +perft 6 159759052 + +id 455 +epd rbn1bkqr/p1pp1pp1/1pn5/4p2p/7P/1PBP4/P1P1PPP1/RBNN1KQR w HAha - +perft 1 23 +perft 2 470 +perft 3 11649 +perft 4 264274 +perft 5 6963287 +perft 6 172833738 + +id 456 +epd rnnbbkqr/3ppppp/p7/1pp5/P6P/6P1/1PPPPP2/RNNBBKQR w HAha - +perft 1 26 +perft 2 569 +perft 3 15733 +perft 4 375556 +perft 5 11008114 +perft 6 284485303 + +id 457 +epd r1nk1bqr/1pppp1pp/2n5/p4p1b/5P2/1N4B1/PPPPP1PP/RN1K1BQR w HAha - +perft 1 25 +perft 2 824 +perft 3 21983 +perft 4 738366 +perft 5 20904119 +perft 6 716170771 + +id 458 +epd r1nkbqrb/p2pppp1/npp4p/8/4PP2/2N4P/PPPP2P1/R1NKBQRB w GAga - +perft 1 31 +perft 2 548 +perft 3 17480 +perft 4 349633 +perft 5 11469548 +perft 6 255067638 + +id 459 +epd rbnnkqbr/ppppp2p/5p2/6p1/2P1B3/P6P/1P1PPPP1/R1NNKQBR w HAha - +perft 1 31 +perft 2 809 +perft 3 24956 +perft 4 680747 +perft 5 21247414 +perft 6 606221516 + +id 460 +epd 1r1bkqbr/pppp1ppp/2nnp3/8/2P5/N4P2/PP1PP1PP/1RNBKQBR w Hh - +perft 1 28 +perft 2 810 +perft 3 22844 +perft 4 694599 +perft 5 20188622 +perft 6 636748147 + +id 461 +epd rn1kqbbr/p1pppp1p/1p4p1/1n6/1P2P3/4Q2P/P1PP1PP1/RNNK1BBR w HAha - +perft 1 39 +perft 2 848 +perft 3 30100 +perft 4 724426 +perft 5 25594662 +perft 6 659615710 + +id 462 +epd rn1kqrbb/pppppppp/8/8/2nP2P1/1P2P3/P1P2P1P/RNNKQRBB w FAfa - +perft 1 29 +perft 2 766 +perft 3 21701 +perft 4 567971 +perft 5 16944425 +perft 6 456898648 + +id 463 +epd b1rnnkrq/bpppppp1/7p/8/1p6/2B5/PNPPPPPP/1BR1NKRQ w GCgc - +perft 1 25 +perft 2 667 +perft 3 17253 +perft 4 472678 +perft 5 12865247 +perft 6 365621294 + +id 464 +epd brnb1krq/pppppppp/8/5P2/2P1n2P/8/PP1PP1P1/BRNBNKRQ w GBgb - +perft 1 23 +perft 2 620 +perft 3 14882 +perft 4 402561 +perft 5 10776855 +perft 6 300125003 + +id 465 +epd b1nnkbrq/pr1pppp1/1p5p/2p5/P2N1P2/8/1PPPP1PP/BR1NKBRQ w GBg - +perft 1 24 +perft 2 472 +perft 3 12181 +perft 4 267398 +perft 5 7370758 +perft 6 178605165 + +id 466 +epd br1nkrqb/p1p1p1pp/3n4/1p1p1p2/5N1P/4P3/PPPP1PP1/BR1NKRQB w FBfb - +perft 1 24 +perft 2 775 +perft 3 19398 +perft 4 624309 +perft 5 16429837 +perft 6 539767605 + +id 467 +epd rbbnnkrq/p2pp1pp/2p5/5p2/1pPP1B2/P7/1P2PPPP/RB1NNKRQ w GAga - +perft 1 34 +perft 2 921 +perft 3 30474 +perft 4 849933 +perft 5 28095833 +perft 6 806446436 + +id 468 +epd rnbbnkr1/1p1ppp1p/2p3p1/p7/2Pq4/1P1P4/P2BPPPP/RN1BNKRQ w GAga - +perft 1 26 +perft 2 1139 +perft 3 29847 +perft 4 1204863 +perft 5 32825932 +perft 6 1281760240 + +id 469 +epd 1rbnkbrq/pppppp2/n5pp/2P5/P7/4N3/1P1PPPPP/RNB1KBRQ w GAg - +perft 1 23 +perft 2 574 +perft 3 14146 +perft 4 391413 +perft 5 10203438 +perft 6 301874034 + +id 470 +epd 1nbnkr1b/rppppppq/p7/7p/1P5P/3P2P1/P1P1PP2/RNBNKRQB w FAf - +perft 1 33 +perft 2 823 +perft 3 26696 +perft 4 724828 +perft 5 23266182 +perft 6 672294132 + +id 471 +epd rbn1bkrq/ppppp3/4n2p/5pp1/1PN5/2P5/P2PPPPP/RBN1BKRQ w GAga - +perft 1 27 +perft 2 859 +perft 3 24090 +perft 4 796482 +perft 5 23075785 +perft 6 789152120 + +id 472 +epd r1nbbkrq/1ppp2pp/2n2p2/p3p3/5P2/1N4BP/PPPPP1P1/RN1B1KRQ w GAga - +perft 1 25 +perft 2 774 +perft 3 20141 +perft 4 618805 +perft 5 16718577 +perft 6 515864053 + +id 473 +epd rnnkbbrq/1pppp1p1/5p2/7p/p6P/3N1P2/PPPPP1PQ/RN1KBBR1 w GAga - +perft 1 29 +perft 2 673 +perft 3 20098 +perft 4 504715 +perft 5 15545590 +perft 6 416359581 + +id 474 +epd r1nkbrqb/pppp1p2/n3p1p1/7p/2P2P2/1P6/P2PPQPP/RNNKBR1B w FAfa - +perft 1 27 +perft 2 722 +perft 3 21397 +perft 4 593762 +perft 5 18742426 +perft 6 537750982 + +id 475 +epd rbnnkr1q/1ppp2pp/p4p2/P2bp3/4P2P/8/1PPP1PP1/RBNNKRBQ w FAfa - +perft 1 26 +perft 2 848 +perft 3 23387 +perft 4 741674 +perft 5 21591790 +perft 6 675163653 + +id 476 +epd rn1bkrb1/1ppppp1p/pn4p1/8/P2q3P/3P4/NPP1PPP1/RN1BKRBQ w FAfa - +perft 1 22 +perft 2 803 +perft 3 18322 +perft 4 632920 +perft 5 15847763 +perft 6 536419559 + +id 477 +epd rn1krbbq/pppp1npp/4pp2/8/4P2P/3P2P1/PPP2P2/RNNKRBBQ w EAea - +perft 1 29 +perft 2 810 +perft 3 23968 +perft 4 670500 +perft 5 20361517 +perft 6 575069358 + +id 478 +epd rnn1rqbb/ppkp1pp1/2p1p2p/2P5/8/3P1P2/PP2P1PP/RNNKRQBB w EA - +perft 1 22 +perft 2 506 +perft 3 11973 +perft 4 292344 +perft 5 7287368 +perft 6 189865944 + +id 479 +epd bbqr1knr/pppppp1p/8/4n1p1/2P1P3/6P1/PPQP1P1P/BB1RNKNR w HDhd - +perft 1 26 +perft 2 650 +perft 3 18253 +perft 4 481200 +perft 5 14301029 +perft 6 394943978 + +id 480 +epd bq1bnknr/pprppp1p/8/2p3p1/4PPP1/8/PPPP3P/BQRBNKNR w HCh - +perft 1 24 +perft 2 548 +perft 3 14021 +perft 4 347611 +perft 5 9374021 +perft 6 250988458 + +id 481 +epd bqrnkb1r/1p2pppp/p1pp3n/5Q2/2P4P/5N2/PP1PPPP1/B1RNKB1R w HChc - +perft 1 46 +perft 2 823 +perft 3 33347 +perft 4 673905 +perft 5 26130444 +perft 6 582880996 + +id 482 +epd bq1rknrb/pppppp1p/4n3/6p1/4P1P1/3P1P2/PPP4P/BQRNKNRB w GCg - +perft 1 23 +perft 2 618 +perft 3 14815 +perft 4 419474 +perft 5 10606831 +perft 6 315124518 + +id 483 +epd q1brnknr/pp1pp1p1/8/2p2p1p/5b2/P4N2/1PPPP1PP/QBBRK1NR w hd - +perft 1 22 +perft 2 675 +perft 3 15778 +perft 4 473994 +perft 5 12077228 +perft 6 368479752 + +id 484 +epd qrbbnknr/1p1ppp1p/p1p5/8/1P2P1p1/3P1B2/P1P2PPP/QRB1NKNR w HBhb - +perft 1 32 +perft 2 722 +perft 3 24049 +perft 4 569905 +perft 5 19584539 +perft 6 484814878 + +id 485 +epd qrb1kbnr/p3pppp/2n5/1ppp4/7P/3P1P2/PPP1P1PR/QRBNKBN1 w Bhb - +perft 1 26 +perft 2 831 +perft 3 22606 +perft 4 724505 +perft 5 20500804 +perft 6 662608969 + +id 486 +epd qrbnknrb/ppp1pp2/6p1/7p/PPNp4/8/2PPPPPP/QRB1KNRB w GBgb - +perft 1 31 +perft 2 840 +perft 3 26762 +perft 4 742772 +perft 5 24422614 +perft 6 701363800 + +id 487 +epd qbrnbknr/pp1pp1pp/8/2p2p2/3Q4/PP6/2PPPPPP/1BRNBKNR w HChc - +perft 1 38 +perft 2 1121 +perft 3 39472 +perft 4 1198438 +perft 5 41108769 +perft 6 1285503872 + +id 488 +epd qr1bbk1r/pppppp1p/1n6/5np1/4B3/1PP5/P2PPPPP/QRN1BKNR w HBhb - +perft 1 25 +perft 2 694 +perft 3 16938 +perft 4 472950 +perft 5 12164609 +perft 6 345122090 + +id 489 +epd qrnkbbnr/1p1pp2p/p7/2p1Npp1/6P1/7P/PPPPPP2/QR1KBBNR w HBhb - +perft 1 27 +perft 2 586 +perft 3 16348 +perft 4 393391 +perft 5 11409633 +perft 6 298054792 + +id 490 +epd qrnkbnrb/pp1p1p2/2p1p1pp/4N3/P4P2/8/1PPPP1PP/QR1KBNRB w GBgb - +perft 1 32 +perft 2 645 +perft 3 20737 +perft 4 460319 +perft 5 15037464 +perft 6 358531599 + +id 491 +epd qbrnknbr/1pppppp1/p6p/8/1P6/3PP3/PQP2PPP/1BRNKNBR w HChc - +perft 1 26 +perft 2 595 +perft 3 16755 +perft 4 415022 +perft 5 12214768 +perft 6 323518628 + +id 492 +epd qrnbk1br/1ppppp1p/p5p1/8/4Pn2/4K1P1/PPPP1P1P/QRNB1NBR w hb - +perft 1 24 +perft 2 609 +perft 3 13776 +perft 4 359415 +perft 5 8538539 +perft 6 230364479 + +id 493 +epd qrnk1bbr/1pnp1ppp/p1p1p3/8/3Q4/1P1N3P/P1PPPPP1/1RNK1BBR w HBhb - +perft 1 43 +perft 2 1106 +perft 3 42898 +perft 4 1123080 +perft 5 41695761 +perft 6 1113836402 + +id 494 +epd qrnknrb1/pppppp2/8/6pp/4P2P/3P1P2/PbP3P1/QRNKNRBB w FBfb - +perft 1 24 +perft 2 658 +perft 3 17965 +perft 4 488373 +perft 5 14457245 +perft 6 400971226 + +id 495 +epd bbrqnrk1/ppp2ppp/7n/3pp3/8/P4N1N/1PPPPPPP/BBRQ1RK1 w - - +perft 1 22 +perft 2 503 +perft 3 12078 +perft 4 310760 +perft 5 8080951 +perft 6 224960353 + +id 496 +epd brqbnk1r/1ppp1ppp/8/p3pn2/8/2PP1P2/PP2PKPP/BRQBN1NR w hb - +perft 1 25 +perft 2 745 +perft 3 19387 +perft 4 570459 +perft 5 15520298 +perft 6 460840861 + +id 497 +epd brqnkbnr/pp2pp1p/3p4/2p5/5p2/3P3P/PPP1PPP1/B1RNKBNR w Hhb - +perft 1 19 +perft 2 516 +perft 3 10755 +perft 4 312996 +perft 5 6995034 +perft 6 214340699 + +id 498 +epd brq1kn1b/1ppppprp/2n3p1/p7/P1N5/6P1/1PPPPP1P/BRQNK1RB w GBb - +perft 1 29 +perft 2 557 +perft 3 16739 +perft 4 352277 +perft 5 10840256 +perft 6 249999654 + +id 499 +epd rbbq1k1r/ppp1pppp/7n/1n1p4/5P2/P2P4/1PPBP1PP/RB1QNKNR w HAha - +perft 1 25 +perft 2 769 +perft 3 20110 +perft 4 638340 +perft 5 17438715 +perft 6 570893953 + +id 500 +epd r1bbnk1r/qpp1pppp/p6n/3p4/1P6/5N1P/P1PPPPP1/RQBBK1NR w ha - +perft 1 23 +perft 2 728 +perft 3 18209 +perft 4 587364 +perft 5 16053564 +perft 6 529082811 + +id 501 +epd rqbnkbnr/1pp2p1p/3p4/p3p1p1/8/2P2P2/PP1PPNPP/RQBNKB1R w HAha - +perft 1 26 +perft 2 772 +perft 3 21903 +perft 4 653704 +perft 5 19571559 +perft 6 593915677 + +id 502 +epd r1bnknrb/pqppp1p1/1p5p/5p2/7P/3P2N1/PPP1PPP1/RQBNK1RB w GAga - +perft 1 27 +perft 2 748 +perft 3 20291 +perft 4 597105 +perft 5 16324542 +perft 6 506453626 + +id 503 +epd rbqnbknr/pp1pppp1/8/2p5/3P3p/5N1P/PPP1PPPR/RBQNBK2 w Aha - +perft 1 30 +perft 2 859 +perft 3 26785 +perft 4 819631 +perft 5 26363334 +perft 6 842796987 + +id 504 +epd rqnbbrk1/ppppppp1/8/5n1p/3P3P/2B3P1/PPP1PP2/RQNB1KNR w HA - +perft 1 22 +perft 2 505 +perft 3 11452 +perft 4 283464 +perft 5 7055215 +perft 6 186760784 + +id 505 +epd rqnkbbnr/pp2p1p1/8/2pp1p1p/3PPP2/8/PPP1N1PP/RQNKBB1R w HAha - +perft 1 28 +perft 2 832 +perft 3 23142 +perft 4 722857 +perft 5 20429246 +perft 6 663183060 + +id 506 +epd rqnkbnr1/pppp2bp/6p1/4pp2/1P2P3/3NN3/P1PP1PPP/RQ1KB1RB w GAga - +perft 1 28 +perft 2 641 +perft 3 18835 +perft 4 459993 +perft 5 14038570 +perft 6 364210162 + +id 507 +epd rbq2kbr/pppppppp/2n5/P7/3P1n2/2P5/1P2PPPP/RBQNKNBR w HA - +perft 1 31 +perft 2 889 +perft 3 27028 +perft 4 766181 +perft 5 24299415 +perft 6 692180754 + +id 508 +epd rq1bkn1r/ppppp2p/3n4/5pp1/2b3P1/1N1P1P2/PPP1P2P/RQ1BKNBR w HAha - +perft 1 28 +perft 2 810 +perft 3 22667 +perft 4 657520 +perft 5 18719949 +perft 6 556282676 + +id 509 +epd r1nknbbr/p2ppp1p/1pp3p1/8/1P6/4P3/P1PPNPPq/R1QKNBBR w HAha - +perft 1 24 +perft 2 797 +perft 3 22144 +perft 4 719069 +perft 5 21862776 +perft 6 716521139 + +id 510 +epd rqnknrbb/ppp1p3/5ppp/2Np4/2P5/4P3/PP1P1PPP/RQNK1RBB w FAfa - +perft 1 34 +perft 2 686 +perft 3 23277 +perft 4 515541 +perft 5 17664543 +perft 6 423574794 + +id 511 +epd 1brnqknr/2p1pppp/p2p4/1P6/6P1/4Nb2/PP1PPP1P/BBR1QKNR w HChc - +perft 1 34 +perft 2 1019 +perft 3 32982 +perft 4 1003103 +perft 5 33322477 +perft 6 1043293394 + +id 512 +epd brn1qknr/1p1pppp1/pb5p/Q1p5/3P3P/8/PPP1PPPR/BRNB1KN1 w Bhb - +perft 1 32 +perft 2 642 +perft 3 20952 +perft 4 464895 +perft 5 15454749 +perft 6 371861782 + +id 513 +epd brnqkbnr/pppppp2/8/6pp/6P1/P2P1P2/1PP1P2P/BRNQKBNR w HBhb - +perft 1 20 +perft 2 441 +perft 3 9782 +perft 4 240220 +perft 5 5770284 +perft 6 153051835 + +id 514 +epd 2nqknrb/1rpppppp/5B2/pp6/1PP1b3/3P4/P3PPPP/1RNQKNRB w GBg - +perft 1 35 +perft 2 1042 +perft 3 36238 +perft 4 1101159 +perft 5 38505058 +perft 6 1202668717 + +id 515 +epd rb1nqknr/1pp1pppp/8/3p4/p2P4/6PN/PPPQPP1P/RBBN1K1R w HAha - +perft 1 29 +perft 2 692 +perft 3 21237 +perft 4 555018 +perft 5 17820605 +perft 6 497251206 + +id 516 +epd rnbbqknr/pppp4/5p2/4p1pp/P7/2N2PP1/1PPPP2P/R1BBQKNR w HAha - +perft 1 23 +perft 2 595 +perft 3 14651 +perft 4 415772 +perft 5 10881112 +perft 6 329010121 + +id 517 +epd rn1qkbnr/p1p1pp1p/bp4p1/3p4/1P6/4P3/P1PP1PPP/RNBQKBNR w HAha - +perft 1 30 +perft 2 794 +perft 3 24319 +perft 4 690811 +perft 5 21657601 +perft 6 647745807 + +id 518 +epd r1bqk1rb/pppnpppp/5n2/3p4/2P3PP/2N5/PP1PPP2/R1BQKNRB w GAga - +perft 1 32 +perft 2 821 +perft 3 27121 +perft 4 733155 +perft 5 24923473 +perft 6 710765657 + +id 519 +epd rbnqbknr/1p1ppp1p/6p1/p1p5/7P/3P4/PPP1PPP1/RBNQBKNR w HAha - +perft 1 24 +perft 2 720 +perft 3 18842 +perft 4 575027 +perft 5 15992882 +perft 6 501093456 + +id 520 +epd r1qbbk1r/pp1ppppp/n1p5/5n2/B1P3P1/8/PP1PPP1P/RNQ1BKNR w HAha - +perft 1 27 +perft 2 831 +perft 3 22293 +perft 4 698986 +perft 5 19948650 +perft 6 637973209 + +id 521 +epd rnqkbb1r/p1pppppp/8/8/1p4n1/PP4PP/2PPPP2/RNQKBBNR w HAha - +perft 1 18 +perft 2 463 +perft 3 9519 +perft 4 256152 +perft 5 6065231 +perft 6 172734380 + +id 522 +epd rnqk1nrb/pppbpp2/7p/3p2p1/4B3/2N1N1P1/PPPPPP1P/R1QKB1R1 w GAga - +perft 1 34 +perft 2 1171 +perft 3 38128 +perft 4 1318217 +perft 5 42109356 +perft 6 1465473753 + +id 523 +epd rbnqknbr/1pp1ppp1/3p4/7p/p2P2PP/2P5/PP2PP2/RBNQKNBR w HAha - +perft 1 32 +perft 2 867 +perft 3 28342 +perft 4 798722 +perft 5 26632459 +perft 6 781067145 + +id 524 +epd rn1bknbr/pq2pppp/1p6/2pp4/P7/1P1P4/2PNPPPP/RNQBK1BR w HAha - +perft 1 24 +perft 2 627 +perft 3 16652 +perft 4 462942 +perft 5 13200921 +perft 6 385193532 + +id 525 +epd r1qk1bbr/ppp1pp1p/2np1n2/6p1/2PP4/3BP3/PP3PPP/RNQKN1BR w HAha - +perft 1 31 +perft 2 992 +perft 3 30213 +perft 4 986631 +perft 5 30397368 +perft 6 1011631987 + +id 526 +epd r1qknrbb/pppp1p2/2n3p1/4p2p/8/QPP5/P1NPPPPP/RN1K1RBB w FAfa - +perft 1 30 +perft 2 702 +perft 3 21563 +perft 4 532939 +perft 5 16813114 +perft 6 438096194 + +id 527 +epd bbkr1qnr/2pppppp/2n5/pp6/8/PPN5/1BPPPPPP/1BR1KQNR w HC - +perft 1 25 +perft 2 573 +perft 3 15183 +perft 4 380910 +perft 5 10554668 +perft 6 283975400 + +id 528 +epd 1rnbkqnr/1bpppppp/1p6/7P/p2P4/5P2/PPP1P1P1/BRNBKQNR w HBhb - +perft 1 21 +perft 2 503 +perft 3 11790 +perft 4 301084 +perft 5 7679979 +perft 6 207799378 + +id 529 +epd brnkqbnr/2p1pppp/1p6/3p4/1pP5/P6P/3PPPP1/BRNKQBNR w HBhb - +perft 1 28 +perft 2 743 +perft 3 21054 +perft 4 587192 +perft 5 17354516 +perft 6 507176753 + +id 530 +epd br1kqnrb/npp1pppp/8/3p4/p4N2/PP6/2PPPPPP/BR1KQNRB w GBgb - +perft 1 31 +perft 2 808 +perft 3 25585 +perft 4 698475 +perft 5 22376575 +perft 6 640362920 + +id 531 +epd rbbnkq1r/pppppp1p/7n/6p1/P5P1/2P2N2/1P1PPP1P/RBBNKQ1R w HAha - +perft 1 29 +perft 2 580 +perft 3 17585 +perft 4 404831 +perft 5 12730970 +perft 6 325226128 + +id 532 +epd rnbbk1nr/pp2qppp/2ppp3/8/3P4/P1N4N/1PP1PPPP/R1BBKQ1R w HAha - +perft 1 29 +perft 2 838 +perft 3 24197 +perft 4 721884 +perft 5 21100580 +perft 6 646624429 + +id 533 +epd rnbk1b1r/ppppn1pp/4pp2/7q/7P/P5PB/1PPPPP2/RNBKQ1NR w HAha - +perft 1 20 +perft 2 729 +perft 3 16633 +perft 4 576199 +perft 5 14507076 +perft 6 498621813 + +id 534 +epd r2kqnrb/pbppppp1/np5p/8/4Q1P1/3P4/PPP1PP1P/RNBK1NRB w GAga - +perft 1 47 +perft 2 1219 +perft 3 55009 +perft 4 1486353 +perft 5 65239153 +perft 6 1834391369 + +id 535 +epd rbnkbq1r/p1p2ppp/1p2pn2/3p4/P3P3/3P4/1PP1KPPP/RBN1BQNR w ha - +perft 1 29 +perft 2 923 +perft 3 27179 +perft 4 883866 +perft 5 26202752 +perft 6 868565895 + +id 536 +epd rk1bb1nr/ppppqppp/n7/1N2p3/6P1/7N/PPPPPP1P/R1KBBQ1R w HA - +perft 1 27 +perft 2 703 +perft 3 19478 +perft 4 559525 +perft 5 16049807 +perft 6 492966455 + +id 537 +epd rnkqbbnr/p1ppp2p/1p4p1/8/1B3p1P/2NP4/PPP1PPP1/R1KQ1BNR w HAha - +perft 1 29 +perft 2 610 +perft 3 18855 +perft 4 438277 +perft 5 14020041 +perft 6 355083962 + +id 538 +epd rnkqb1rb/pp1p1ppp/4p3/2P3n1/8/1PP5/P3PPPP/RNKQBNRB w GAga - +perft 1 29 +perft 2 675 +perft 3 20699 +perft 4 535821 +perft 5 17000613 +perft 6 476598337 + +id 539 +epd rb1kqnbr/pp1pp1p1/1np2p2/7p/P1P3PP/8/1P1PPP2/RBNKQNBR w HAha - +perft 1 31 +perft 2 1077 +perft 3 33661 +perft 4 1183381 +perft 5 37415304 +perft 6 1328374620 + +id 540 +epd rnkbq1br/ppp2ppp/3p4/Q3p1n1/5P2/3P2P1/PPP1P2P/RNKB1NBR w HAha - +perft 1 41 +perft 2 1201 +perft 3 46472 +perft 4 1420367 +perft 5 52991625 +perft 6 1675608008 + +id 541 +epd rn1qnbbr/pp2pppp/2ppk3/8/2PP4/3Q1N2/PP2PPPP/RNK2BBR w HA - +perft 1 34 +perft 2 666 +perft 3 22474 +perft 4 472299 +perft 5 15860369 +perft 6 353831792 + +id 542 +epd rnkqnr1b/ppppp1pp/5p2/8/Q1P2P2/8/PP1P2PP/RbK1NRBB w FAfa - +perft 1 36 +perft 2 876 +perft 3 31987 +perft 4 788580 +perft 5 29022529 +perft 6 736717252 + +id 543 +epd bbrn1nqr/ppp1k1pp/5p2/3pp3/7P/3PN3/PPP1PPP1/BBRK1NQR w - - +perft 1 24 +perft 2 583 +perft 3 15063 +perft 4 383532 +perft 5 10522064 +perft 6 280707118 + +id 544 +epd brnbkn1r/1pppp1p1/4q3/p4p1p/7P/1N3P2/PPPPP1PQ/BR1BKN1R w HBhb - +perft 1 27 +perft 2 935 +perft 3 26120 +perft 4 885699 +perft 5 26000648 +perft 6 873063158 + +id 545 +epd br1knbqr/pp2p1pp/1n6/2pp1p2/6P1/2P4B/PP1PPPQP/BRNKN2R w HBhb - +perft 1 27 +perft 2 681 +perft 3 19202 +perft 4 510687 +perft 5 14954779 +perft 6 415624943 + +id 546 +epd brnk1qrb/p1ppppp1/1p5p/8/P3n3/1N4P1/1PPPPPRP/BR1KNQ1B w Bgb - +perft 1 22 +perft 2 638 +perft 3 13991 +perft 4 412346 +perft 5 9760752 +perft 6 293499724 + +id 547 +epd rbbnknqr/pppp3p/5pp1/8/1P1pP3/7P/P1P2PP1/RBBNKNQR w HAha - +perft 1 29 +perft 2 756 +perft 3 21616 +perft 4 614074 +perft 5 17602252 +perft 6 528140595 + +id 548 +epd 1nbbknqr/rpp1ppp1/1Q1p3p/p7/2P2PP1/8/PP1PP2P/RNBBKN1R w HAh - +perft 1 37 +perft 2 977 +perft 3 34977 +perft 4 944867 +perft 5 33695089 +perft 6 940198007 + +id 549 +epd rnb2bqr/ppkpppp1/3n3p/2p5/6PP/2N2P2/PPPPP3/R1BKNBQR w HA - +perft 1 30 +perft 2 647 +perft 3 20365 +perft 4 467780 +perft 5 15115531 +perft 6 369257622 + +id 550 +epd rn1k1qrb/p1pppppp/bp6/8/4n3/P4BPP/1PPPPP2/RNBKNQR1 w GAga - +perft 1 22 +perft 2 670 +perft 3 14998 +perft 4 451517 +perft 5 11199653 +perft 6 339919682 + +id 551 +epd rb2bnqr/nppkpppp/3p4/p7/1P6/P2N2P1/2PPPP1P/RB1KBNQR w HA - +perft 1 22 +perft 2 479 +perft 3 11475 +perft 4 264739 +perft 5 6831555 +perft 6 167329117 + +id 552 +epd r1kbb1qr/2pppppp/np2n3/p7/2P3P1/8/PP1PPPQP/RNKBBN1R w HAha - +perft 1 32 +perft 2 723 +perft 3 23953 +perft 4 581832 +perft 5 19472074 +perft 6 504622114 + +id 553 +epd rnknbb1r/p1ppp1pp/8/1p1P1p1q/8/P1P5/1P2PPPP/RNKNBBQR w HAha - +perft 1 19 +perft 2 607 +perft 3 12733 +perft 4 417451 +perft 5 9753617 +perft 6 325177085 + +id 554 +epd rnkn1qrb/pp1bp1pp/2p5/1N1p1p2/8/2P5/PPKPPPPP/R2NBQRB w ga - +perft 1 27 +perft 2 533 +perft 3 14549 +perft 4 330747 +perft 5 9206957 +perft 6 232664675 + +id 555 +epd r1nknqbr/pp2p1pp/2p2p2/3p4/6P1/PP1P4/2P1PP1b/RBNKNQBR w HAha - +perft 1 20 +perft 2 582 +perft 3 13777 +perft 4 409166 +perft 5 10708639 +perft 6 326565393 + +id 556 +epd rnkb1qbr/p1pp1p1p/1p2pn2/1Q4p1/4P3/N4P2/PPPP2PP/R1KBN1BR w HAha - +perft 1 40 +perft 2 1038 +perft 3 39356 +perft 4 1051441 +perft 5 39145902 +perft 6 1079612614 + +id 557 +epd rn2qbbr/1pkppp1p/p3n1p1/8/8/2P2P2/PP1PP1PP/RNKN1BBR w HA - +perft 1 24 +perft 2 605 +perft 3 14888 +perft 4 385964 +perft 5 9687507 +perft 6 260874068 + +id 558 +epd rn1nqrbb/p1kppp1p/8/1pp3p1/1P6/2N1P3/P1PP1PPP/RK1NQRBB w - - +perft 1 21 +perft 2 540 +perft 3 12489 +perft 4 337997 +perft 5 8436136 +perft 6 237525904 + +id 559 +epd bbrnknrq/1pp3pp/p2p1p2/4p3/P7/1P2N3/2PPPPPP/BBRN1RKQ w gc - +perft 1 24 +perft 2 527 +perft 3 13900 +perft 4 326175 +perft 5 9139962 +perft 6 226253685 + +id 560 +epd brnb1nrq/pppp1kpp/4p3/8/5p1P/P1P3P1/1P1PPP2/BRNBKNRQ w GB - +perft 1 29 +perft 2 773 +perft 3 23904 +perft 4 638768 +perft 5 20503775 +perft 6 560338709 + +id 561 +epd br1k1brq/ppppp2p/1n1n1pp1/8/P1P5/3P2P1/1P2PP1P/BRNKNBRQ w GBgb - +perft 1 28 +perft 2 811 +perft 3 23550 +perft 4 664880 +perft 5 19913758 +perft 6 565143976 + +id 562 +epd 1r1knrqb/n1pppppp/p1b5/1p6/8/3N1P2/PPPPP1PP/BRNK1RQB w fb - +perft 1 29 +perft 2 753 +perft 3 23210 +perft 4 620019 +perft 5 20044474 +perft 6 558383603 + +id 563 +epd rbbnk1rq/pppppppp/8/3Pn3/8/4P1P1/PPP2P1P/RBBNKNRQ w GAga - +perft 1 22 +perft 2 551 +perft 3 12619 +perft 4 324608 +perft 5 8204171 +perft 6 217689974 + +id 564 +epd rnbbk1rq/2pppp1p/p3n1p1/1p6/P3N3/8/1PPPPPPP/RNBB1KRQ w ga - +perft 1 26 +perft 2 742 +perft 3 20061 +perft 4 599527 +perft 5 16787080 +perft 6 525678162 + +id 565 +epd rnbkn1rq/ppppppb1/6p1/7p/2B2P2/1P2P3/P1PP2PP/RNBKN1RQ w GAga - +perft 1 28 +perft 2 799 +perft 3 23210 +perft 4 689436 +perft 5 20755098 +perft 6 639632905 + +id 566 +epd rn1knrqb/p2pppp1/b1p5/1p5p/2P2P2/1P6/P2PP1PP/RNBKNRQB w FAfa - +perft 1 30 +perft 2 579 +perft 3 18481 +perft 4 397545 +perft 5 13257198 +perft 6 311282465 + +id 567 +epd rbnkbnrq/pp2p1Np/2p2p2/8/3p4/8/PPPPPPPP/RBNKBR1Q w Aga - +perft 1 23 +perft 2 670 +perft 3 16435 +perft 4 501883 +perft 5 13012378 +perft 6 411860744 + +id 568 +epd rk1bbnrq/ppp1pppp/n7/3p4/5P2/3P2NP/PPP1P1P1/RNKBB1RQ w GA - +perft 1 26 +perft 2 597 +perft 3 16238 +perft 4 402506 +perft 5 11269462 +perft 6 296701249 + +id 569 +epd r1knbbrq/pppp2p1/2n1p2p/5p2/4P3/P1PP4/1P3PPP/RNKNBBRQ w GAga - +perft 1 20 +perft 2 596 +perft 3 13091 +perft 4 399069 +perft 5 9416862 +perft 6 293659781 + +id 570 +epd rnknbrqb/p1p1pp1p/3p4/1p1N2p1/8/N7/PPPPPPPP/1RK1BRQB w Ffa - +perft 1 26 +perft 2 724 +perft 3 18942 +perft 4 552040 +perft 5 15257204 +perft 6 461293885 + +id 571 +epd rbnknrb1/1p1ppp1p/p1p3p1/8/1P3P2/1R6/PqPPP1PP/RBNKN1BQ w Afa - +perft 1 31 +perft 2 1183 +perft 3 34723 +perft 4 1289502 +perft 5 38722152 +perft 6 1421492227 + +id 572 +epd rnkbnrbq/2p1ppp1/p7/1p1p3p/3P4/1P4P1/P1P1PP1P/RNKBNRBQ w FAfa - +perft 1 24 +perft 2 506 +perft 3 12748 +perft 4 301464 +perft 5 8086100 +perft 6 207129256 + +id 573 +epd r1knrbbq/pp1ppppp/2p1n3/8/2P3P1/P7/1PKPPP1P/RN1NRBBQ w ea - +perft 1 28 +perft 2 570 +perft 3 16037 +perft 4 352471 +perft 5 10278695 +perft 6 242592363 + +id 574 +epd rnknrq1b/ppp1p1p1/4b3/3p1p1p/6P1/P4P2/1PPPPQ1P/RNKNR1BB w EAea - +perft 1 30 +perft 2 739 +perft 3 23124 +perft 4 594962 +perft 5 19252739 +perft 6 521629794 + +id 575 +epd bbqr1krn/pppp1p1p/5n2/4p1p1/3P4/P3QP2/1PP1P1PP/BB1RNKRN w GDgd - +perft 1 31 +perft 2 799 +perft 3 25627 +perft 4 674913 +perft 5 22172123 +perft 6 609277274 + +id 576 +epd bq1b1krn/pp1ppppp/3n4/2r5/3p3N/6N1/PPP1PPPP/BQRB1KR1 w GCg - +perft 1 21 +perft 2 798 +perft 3 18571 +perft 4 688429 +perft 5 17546069 +perft 6 647165916 + +id 577 +epd bqrnkbrn/2pp1pp1/p7/1p2p2p/1P6/4N3/P1PPPPPP/BQR1KBRN w GCgc - +perft 1 27 +perft 2 783 +perft 3 22327 +perft 4 670798 +perft 5 20059741 +perft 6 624462073 + +id 578 +epd bqr1krnb/1np1pppp/8/pp1p4/8/2P2N2/PP1PPPPP/BQRNKR1B w FCfc - +perft 1 28 +perft 2 636 +perft 3 18874 +perft 4 461104 +perft 5 14237097 +perft 6 372181570 + +id 579 +epd qbb1rkrn/1ppppppp/p7/7n/8/P2P4/1PP1PPPP/QBBRNKRN w Gg - +perft 1 25 +perft 2 547 +perft 3 13837 +perft 4 332918 +perft 5 8849383 +perft 6 229112926 + +id 580 +epd 1rbbnkrn/p1p1pp1p/2q5/1p1p2p1/8/2P3P1/PP1PPP1P/QRBBNKRN w GBgb - +perft 1 24 +perft 2 1010 +perft 3 24370 +perft 4 983770 +perft 5 24328258 +perft 6 961371180 + +id 581 +epd qrb1kbrn/ppp1p2p/4npp1/3p4/8/1PP4P/PR1PPPP1/Q1BNKBRN w Ggb - +perft 1 18 +perft 2 451 +perft 3 9291 +perft 4 247310 +perft 5 5568106 +perft 6 155744022 + +id 582 +epd qr2krnb/p1p1pppp/b1np4/1p6/3NP3/7P/PPPP1PP1/QRBNKR1B w FBfb - +perft 1 25 +perft 2 667 +perft 3 17081 +perft 4 476030 +perft 5 12458875 +perft 6 361495148 + +id 583 +epd qbrnbkrn/ppp3pp/3p4/5p2/2P1pP2/6PP/PP1PP3/QBRNBKRN w GCgc - +perft 1 24 +perft 2 650 +perft 3 16835 +perft 4 445263 +perft 5 12187382 +perft 6 326834539 + +id 584 +epd qrnb1krn/ppp1p1pp/5p2/2Np4/b2P4/2P5/PP2PPPP/QR1BBKRN w GBgb - +perft 1 27 +perft 2 641 +perft 3 17490 +perft 4 432041 +perft 5 12103076 +perft 6 310695797 + +id 585 +epd qrnkbbrn/pp2pp2/8/2pp2pp/6PP/3P4/PPPKPP2/QRN1BBRN w gb - +perft 1 22 +perft 2 554 +perft 3 13116 +perft 4 357404 +perft 5 9014737 +perft 6 258925091 + +id 586 +epd qrnkbrnb/p1p1ppp1/1p6/3p4/3P3p/5N1P/PPP1PPP1/QRNKBR1B w FBfb - +perft 1 24 +perft 2 529 +perft 3 13205 +perft 4 318722 +perft 5 8295874 +perft 6 213856651 + +id 587 +epd qbr1krbn/1pppp1pp/p7/5pn1/2PP4/8/PPB1PPPP/Q1RNKRBN w FCfc - +perft 1 26 +perft 2 831 +perft 3 21651 +perft 4 696830 +perft 5 18961456 +perft 6 621884383 + +id 588 +epd 1rnbkrbn/1qp1pppp/3p4/pp6/4P3/1NP4P/PP1P1PP1/QR1BKRBN w FBfb - +perft 1 24 +perft 2 597 +perft 3 15089 +perft 4 404761 +perft 5 10832084 +perft 6 307793179 + +id 589 +epd q1rkrbbn/ppp1pppp/8/3p4/1PnP4/P7/1RP1PPPP/Q1NKRBBN w Ee - +perft 1 20 +perft 2 520 +perft 3 10769 +perft 4 278067 +perft 5 6452205 +perft 6 170268300 + +id 590 +epd qrnkrn1b/ppppp1pp/4b3/7P/6p1/P7/1PPPPP2/QRNKRNBB w EBeb - +perft 1 26 +perft 2 566 +perft 3 15623 +perft 4 381312 +perft 5 10940750 +perft 6 287987207 + +id 591 +epd bbr1nkrn/ppp1pppp/3q4/3p4/8/P7/1PPPPPPP/BBRQNRKN w gc - +perft 1 19 +perft 2 661 +perft 3 13895 +perft 4 460396 +perft 5 10870247 +perft 6 356399665 + +id 592 +epd brqbnkrn/pp1pp2p/5pp1/2p5/4P3/P2P1N2/1PP2PPP/BRQB1KRN w GBgb - +perft 1 27 +perft 2 679 +perft 3 19916 +perft 4 527306 +perft 5 16391730 +perft 6 455940859 + +id 593 +epd 2qnkbrn/p1pppppp/8/1r6/1p2bP2/7N/PPPPP1PP/BR1QKBRN w GBg - +perft 1 18 +perft 2 774 +perft 3 15713 +perft 4 635461 +perft 5 14371755 +perft 6 559579332 + +id 594 +epd r1qnkr1b/p1pppppp/7n/1p6/8/1P3b1N/PRPPPPPP/B1QNK1RB w f - +perft 1 21 +perft 2 677 +perft 3 15437 +perft 4 501520 +perft 5 12463801 +perft 6 410795298 + +id 595 +epd rbbqn1rn/pppp1pp1/3k4/4p2Q/2PPP3/8/PP3PPP/RBB1NKRN w GA - +perft 1 40 +perft 2 742 +perft 3 28757 +perft 4 579833 +perft 5 21852196 +perft 6 471452088 + +id 596 +epd rqbbnkrn/3pppp1/p1p4p/1p6/5P2/P2N4/1PPPP1PP/RQBBK1RN w ga - +perft 1 23 +perft 2 665 +perft 3 16400 +perft 4 492544 +perft 5 12794736 +perft 6 396640086 + +id 597 +epd r2nkbrn/pp2pppp/8/2ppqb2/2P3P1/5P2/PP1PPN1P/RQB1KBRN w GAga - +perft 1 28 +perft 2 1108 +perft 3 31164 +perft 4 1194581 +perft 5 34780853 +perft 6 1292405738 + +id 598 +epd rqbnk1nb/p1pppr1p/5p2/1p4p1/1PP1P3/8/P2P1PPP/RQBNKRNB w FAa - +perft 1 26 +perft 2 650 +perft 3 18208 +perft 4 491403 +perft 5 14565370 +perft 6 416833400 + +id 599 +epd rbqnb1rn/p1pp1kpp/1p2pp2/8/4P2P/P5P1/1PPP1P2/RBQNBKRN w GA - +perft 1 20 +perft 2 437 +perft 3 9423 +perft 4 222154 +perft 5 5282124 +perft 6 132309824 + +id 600 +epd rqnbbkrn/p1p1pppp/3p4/1p5B/8/1P1NP3/P1PP1PPP/RQ2BKRN w GAga - +perft 1 30 +perft 2 606 +perft 3 18382 +perft 4 422491 +perft 5 12989786 +perft 6 326601372 + +id 601 +epd rqnkbbr1/ppppp1pp/5p2/7n/8/2PNP2P/PP1P1PP1/RQ1KBBRN w GAga - +perft 1 23 +perft 2 482 +perft 3 12506 +perft 4 297869 +perft 5 8430874 +perft 6 217797292 + +id 602 +epd r1nkbrnb/2ppppp1/1q6/pp5p/1P6/P3P3/2PPKPPP/RQN1BRNB w fa - +perft 1 25 +perft 2 827 +perft 3 21518 +perft 4 701071 +perft 5 19290675 +perft 6 632892337 + +id 603 +epd rbqnkrbn/p1ppppp1/7p/1p6/7P/2N1P3/PPPP1PPB/RBQ1KR1N w FAfa - +perft 1 30 +perft 2 627 +perft 3 18566 +perft 4 440217 +perft 5 12976682 +perft 6 337377291 + +id 604 +epd r1nbkrbn/p1qp1ppp/8/1pp1p3/2P1P3/6P1/PP1PBP1P/RQN1KRBN w FAfa - +perft 1 22 +perft 2 616 +perft 3 14503 +perft 4 431199 +perft 5 10850952 +perft 6 335943324 + +id 605 +epd rqnkr1bn/ppp1ppb1/3p2pp/8/P7/2P2P2/1PKPP1PP/RQN1RBBN w ea - +perft 1 31 +perft 2 679 +perft 3 21365 +perft 4 493500 +perft 5 15661072 +perft 6 379844460 + +id 606 +epd r2krnbb/qppp1ppp/1n6/p3p3/PP6/4N3/N1PPPPPP/RQ1KR1BB w EAea - +perft 1 24 +perft 2 645 +perft 3 17054 +perft 4 487028 +perft 5 13837270 +perft 6 416239106 + +id 607 +epd bbr1qk1n/1ppppp1p/2n5/p7/P7/1P2P3/2PP1PrP/1BRNQKRN w GCc - +perft 1 18 +perft 2 520 +perft 3 10680 +perft 4 304462 +perft 5 7215306 +perft 6 207612575 + +id 608 +epd brnbq1rn/2ppppkp/p5p1/1p6/8/1BP3P1/PP1PPP1P/BRN1QRKN w - - +perft 1 21 +perft 2 625 +perft 3 13989 +perft 4 419667 +perft 5 9929336 +perft 6 300902534 + +id 609 +epd brn1kbrn/pp2p1pp/3p4/q1p2p2/2P4P/6P1/PP1PPP2/BRNQKBRN w GBgb - +perft 1 18 +perft 2 477 +perft 3 10205 +perft 4 273925 +perft 5 6720181 +perft 6 187205941 + +id 610 +epd brn1krnb/p3pppp/1qpp4/1p6/2P3P1/1P6/P2PPP1P/BRNQKRNB w FBfb - +perft 1 30 +perft 2 835 +perft 3 24761 +perft 4 716151 +perft 5 21806428 +perft 6 654487872 + +id 611 +epd r1b1qkrn/1p1ppppp/p1p1n3/8/4P3/1PN5/P1PPQPPb/RBB2KRN w GAga - +perft 1 28 +perft 2 825 +perft 3 24536 +perft 4 716585 +perft 5 22079005 +perft 6 647939781 + +id 612 +epd r1bbqk1n/p1pppprp/n7/1p4p1/5P2/2N3N1/PPPPP1PP/1RBBQKR1 w Ga - +perft 1 25 +perft 2 545 +perft 3 14657 +perft 4 358854 +perft 5 10271111 +perft 6 273864588 + +id 613 +epd rnbqkbrn/p1pp1pp1/4p3/7p/2p4P/2P5/PP1PPPP1/R1BQKBRN w GAga - +perft 1 17 +perft 2 445 +perft 3 9076 +perft 4 255098 +perft 5 5918310 +perft 6 174733195 + +id 614 +epd rnbqkrnb/1p1pp1p1/2p4p/p4p2/3P2P1/7N/PPPBPP1P/RN1QKR1B w FAfa - +perft 1 34 +perft 2 746 +perft 3 25319 +perft 4 623133 +perft 5 21285553 +perft 6 569141201 + +id 615 +epd rbnqbkr1/1ppppp2/p5n1/6pp/4P3/1N6/PPPP1PPP/RBQ1BRKN w ga - +perft 1 18 +perft 2 466 +perft 3 9683 +perft 4 260864 +perft 5 6051500 +perft 6 170135726 + +id 616 +epd rnqb1krn/ppppp1p1/7p/7b/P1P2pPP/8/1P1PPP2/RNQBBKRN w GAga - +perft 1 24 +perft 2 575 +perft 3 15400 +perft 4 385825 +perft 5 11039042 +perft 6 291243811 + +id 617 +epd rnqkbbr1/p1pp1ppp/4p3/1p6/P3P2n/5P2/1PPP1NPP/RNQKBBR1 w GAga - +perft 1 27 +perft 2 803 +perft 3 22883 +perft 4 694449 +perft 5 20666099 +perft 6 638696065 + +id 618 +epd rn1kbrnb/1qppp1pp/1p6/p4p2/1B1P4/1P5N/P1P1PPPP/RNQK1R1B w FAfa - +perft 1 37 +perft 2 1209 +perft 3 43015 +perft 4 1425600 +perft 5 49748034 +perft 6 1671593862 + +id 619 +epd rbnqkrbn/Bppp1p2/p5pp/4p3/5P2/6PP/PPPPP3/RBNQKR1N w FAfa - +perft 1 29 +perft 2 720 +perft 3 20434 +perft 4 534148 +perft 5 15384362 +perft 6 421343249 + +id 620 +epd rnqbkr1n/1p1ppbpp/3p1p2/p7/8/1P6/P1PPPPPP/R1QBKRBN w FAfa - +perft 1 20 +perft 2 657 +perft 3 14424 +perft 4 492678 +perft 5 11843134 +perft 6 413965054 + +id 621 +epd rnqkrb1n/ppppp3/6p1/5p1p/2b2P2/P1N5/1PPPP1PP/RQ1KRBBN w EAea - +perft 1 28 +perft 2 749 +perft 3 20684 +perft 4 543151 +perft 5 15379233 +perft 6 417191461 + +id 622 +epd rnqk1nbb/1pp2ppp/3pr3/p3p3/3P1P2/2N3N1/PPP1P1PP/R1QKR1BB w EAa - +perft 1 29 +perft 2 883 +perft 3 26412 +perft 4 815098 +perft 5 25144295 +perft 6 789705382 + +id 623 +epd bbr1kqrn/p1p1ppp1/1p2n2p/3p4/1P1P4/2N5/P1P1PPPP/BBR1KQRN w GCgc - +perft 1 22 +perft 2 485 +perft 3 11475 +perft 4 271271 +perft 5 6825123 +perft 6 171793012 + +id 624 +epd brnbkq1n/ppp1ppr1/7p/3p2p1/2P3PP/8/PPBPPP2/BRN1KQRN w GBb - +perft 1 30 +perft 2 634 +perft 3 19017 +perft 4 442537 +perft 5 13674310 +perft 6 345386924 + +id 625 +epd brnkqbr1/1pppp1pp/5p2/p7/P1P1P2n/8/1P1P1PP1/BRNKQBRN w GBgb - +perft 1 21 +perft 2 504 +perft 3 11672 +perft 4 305184 +perft 5 7778289 +perft 6 217596497 + +id 626 +epd b1rkqrnb/p1ppp1pp/1p1n4/5p2/5P2/PN5P/1PPPP1P1/BR1KQRNB w FBf - +perft 1 23 +perft 2 688 +perft 3 17259 +perft 4 531592 +perft 5 14228372 +perft 6 451842354 + +id 627 +epd 1bbnkqrn/rppppp2/p5p1/7p/7P/P1P1P3/1P1P1PP1/RBBNKQRN w GAg - +perft 1 25 +perft 2 450 +perft 3 12391 +perft 4 263946 +perft 5 7752404 +perft 6 185393913 + +id 628 +epd rnbbkqr1/1pppppp1/7p/p3n3/PP5P/8/1BPPPPP1/RN1BKQRN w GAga - +perft 1 23 +perft 2 543 +perft 3 12224 +perft 4 305812 +perft 5 7549008 +perft 6 199883770 + +id 629 +epd r1bkqbrn/ppppp1pp/8/5p2/3nPP2/1P4N1/P1PP2PP/RNBKQBR1 w GAga - +perft 1 27 +perft 2 751 +perft 3 21158 +perft 4 600417 +perft 5 17989920 +perft 6 527273615 + +id 630 +epd rnbkqr1b/1p1pp1pp/p4p1n/2p5/1P5P/N4P2/P1PPP1P1/R1BKQRNB w FAfa - +perft 1 21 +perft 2 498 +perft 3 11738 +perft 4 302278 +perft 5 7808375 +perft 6 216224115 + +id 631 +epd rbnkbqrn/p1p3pp/1p1p4/B3pp2/3P2P1/6N1/PPP1PP1P/RBNK1QR1 w GAga - +perft 1 34 +perft 2 977 +perft 3 33464 +perft 4 961128 +perft 5 33318567 +perft 6 978991050 + +id 632 +epd r1kbbqrn/ppp3pp/2np1p2/1P2p3/3P1P2/8/P1P1P1PP/RNKBBQRN w GAga - +perft 1 32 +perft 2 920 +perft 3 28916 +perft 4 844881 +perft 5 26763259 +perft 6 797524786 + +id 633 +epd rk1qbbrn/p2npppp/1p6/2p4Q/8/4P3/PPPP1PPP/RNK1B1RN w GA - +perft 1 35 +perft 2 657 +perft 3 22359 +perft 4 495406 +perft 5 16662477 +perft 6 419496845 + +id 634 +epd rnk1brnb/pp1p1pp1/8/q1p1p2p/5P2/NP6/P1PPP1PP/R1KQBRNB w FAfa - +perft 1 26 +perft 2 774 +perft 3 20215 +perft 4 610661 +perft 5 16987110 +perft 6 523437649 + +id 635 +epd rb1kqrbn/npp1ppp1/p7/3P3p/2PP4/8/PP3PPP/RBNKQRBN w FAfa - +perft 1 35 +perft 2 775 +perft 3 27395 +perft 4 661118 +perft 5 23983464 +perft 6 625669222 + +id 636 +epd rnkb1rbn/pp1p2pp/8/2p1pp1q/P6P/1PN5/2PPPPP1/R1KBQRBN w FAfa - +perft 1 22 +perft 2 899 +perft 3 21188 +perft 4 850597 +perft 5 21518343 +perft 6 857951339 + +id 637 +epd rnkqrbbn/1pppp1p1/8/p2N1p1p/2P4P/8/PP1PPPP1/R1KQRBBN w EAea - +perft 1 29 +perft 2 585 +perft 3 17571 +perft 4 393221 +perft 5 12238776 +perft 6 299752383 + +id 638 +epd rnk1r1bb/pp1ppppp/1q4n1/2p5/5P1P/3PP3/PPP3P1/RNKQRNBB w EAea - +perft 1 27 +perft 2 884 +perft 3 24613 +perft 4 811915 +perft 5 23698701 +perft 6 790239502 + +id 639 +epd bbrnkrqn/1ppp1p2/6pp/p3p3/5PP1/2PB4/PP1PP2P/B1RNKRQN w FCfc - +perft 1 37 +perft 2 693 +perft 3 25425 +perft 4 550527 +perft 5 20138432 +perft 6 481498664 + +id 640 +epd b1rbkrqn/ppp2ppp/1n2p3/3p4/6P1/2PP4/PP2PP1P/BRNBKRQN w FBf - +perft 1 21 +perft 2 463 +perft 3 10610 +perft 4 253204 +perft 5 6307276 +perft 6 159025909 + +id 641 +epd brnkrb1n/1pp1p1pp/3p4/p1Nq1p2/2P5/8/PP1PPPPP/BRK1RBQN w eb - +perft 1 27 +perft 2 725 +perft 3 17842 +perft 4 496072 +perft 5 12604078 +perft 6 362747791 + +id 642 +epd brn1r1nb/ppppkppp/4p3/8/2PP1P2/8/PP1KP1PP/BRN1RQNB w - - +perft 1 25 +perft 2 623 +perft 3 16874 +perft 4 426659 +perft 5 12290985 +perft 6 317097424 + +id 643 +epd rbb1krqn/1pp1pp1p/p3n1p1/3pP3/8/1PN5/P1PP1PPP/RBB1KRQN w FAfa d6 +perft 1 23 +perft 2 529 +perft 3 12641 +perft 4 310277 +perft 5 7861413 +perft 6 202594556 + +id 644 +epd r1bbkrqn/p1pppppp/8/4n3/1p5P/P2P2P1/1PP1PP2/RNBBKRQN w FAfa - +perft 1 23 +perft 2 571 +perft 3 13133 +perft 4 346793 +perft 5 8699448 +perft 6 243460643 + +id 645 +epd rnbkrbqn/p1pp1ppp/4p3/1p6/8/BPN3P1/P1PPPP1P/R2KRBQN w EAea - +perft 1 29 +perft 2 692 +perft 3 20014 +perft 4 500375 +perft 5 14904192 +perft 6 386694739 + +id 646 +epd rnbkrqn1/pppppp2/8/1Q2b1pp/P3P3/5P2/1PPP2PP/RNBKR1NB w EAea - +perft 1 37 +perft 2 1001 +perft 3 36440 +perft 4 987842 +perft 5 35626426 +perft 6 993747544 + +id 647 +epd rbnkbrqn/p1pppp2/7p/1p4pP/3P1P2/8/PPP1P1P1/RBNKBRQN w FAfa - +perft 1 30 +perft 2 564 +perft 3 17143 +perft 4 381364 +perft 5 11859538 +perft 6 293703269 + +id 648 +epd 1nkbbrqn/3ppppp/r1p5/pp6/8/4PP2/PPPPN1PP/RNKBBRQ1 w FAf - +perft 1 26 +perft 2 546 +perft 3 14641 +perft 4 344592 +perft 5 9556962 +perft 6 245137199 + +id 649 +epd rnkrbbq1/pppppnp1/7p/8/1B1Q1p2/3P1P2/PPP1P1PP/RNKR1B1N w DAda - +perft 1 43 +perft 2 887 +perft 3 36240 +perft 4 846858 +perft 5 33185346 +perft 6 851927292 + +id 650 +epd 1rkrbqnb/pppppp2/2n3p1/7p/3P3P/P4N2/1PP1PPP1/RNKRBQ1B w DAd - +perft 1 26 +perft 2 622 +perft 3 16049 +perft 4 403921 +perft 5 10786140 +perft 6 285233838 + +id 651 +epd rbnkr1bn/pp1pqp1p/2p1p3/6p1/3P4/7P/PPP1PPP1/RBNKRQBN w EAea - +perft 1 19 +perft 2 566 +perft 3 12257 +perft 4 381197 +perft 5 9107175 +perft 6 293397389 + +id 652 +epd r1kbrqb1/pppp2pp/2n1p1n1/5p1B/4PP2/P7/1PPP2PP/RNK1RQBN w EAea - +perft 1 39 +perft 2 1359 +perft 3 53626 +perft 4 1876028 +perft 5 73871486 +perft 6 2633945690 + +id 653 +epd rnkrqbbn/p1p3pp/1p1ppp2/8/1P6/3P2P1/PKP1PP1P/RN1RQBBN w da - +perft 1 26 +perft 2 776 +perft 3 20735 +perft 4 611907 +perft 5 16884013 +perft 6 503561996 + +id 654 +epd rnkrqnbb/ppp2p1p/3p4/4p1p1/3P3P/N1Q5/PPP1PPP1/R1KR1NBB w DAda - +perft 1 40 +perft 2 1175 +perft 3 45637 +perft 4 1375884 +perft 5 52620163 +perft 6 1633655838 + +id 655 +epd bbrnkrn1/p1pppp2/1p6/6pp/3q4/1P3QP1/P1PPPP1P/BBRNKRN1 w FCfc - +perft 1 34 +perft 2 1398 +perft 3 45749 +perft 4 1712950 +perft 5 57268492 +perft 6 2059942014 + +id 656 +epd br1bkrnq/1p2pppp/pnp5/3p4/P1P5/5P2/1P1PPKPP/BRNB1RNQ w fb - +perft 1 24 +perft 2 501 +perft 3 12237 +perft 4 284936 +perft 5 7049659 +perft 6 177940764 + +id 657 +epd brnkrbn1/pppppp1q/B6p/6p1/8/1P2PP2/P1PP2PP/BRNKR1NQ w EBeb - +perft 1 34 +perft 2 815 +perft 3 25868 +perft 4 700970 +perft 5 22006883 +perft 6 639803952 + +id 658 +epd br1krnqb/pppppp1p/1n4p1/8/8/P2NN3/2PPPPPP/BR1K1RQB w Beb - +perft 1 37 +perft 2 1029 +perft 3 36748 +perft 4 1025712 +perft 5 36214583 +perft 6 1026195877 + +id 659 +epd rbbnkr1q/p1p2ppp/1p1ppn2/8/1PP4P/8/P2PPPP1/RBBNKRNQ w FAfa - +perft 1 28 +perft 2 755 +perft 3 22623 +perft 4 605106 +perft 5 18972778 +perft 6 513486101 + +id 660 +epd r1b1krnq/pp2pppp/1bn5/2pp4/4N3/5P2/PPPPPRPP/R1BBK1NQ w Afa - +perft 1 24 +perft 2 705 +perft 3 17427 +perft 4 532521 +perft 5 13532966 +perft 6 426443376 + +id 661 +epd 1nbkrbn1/rpppppqp/p7/6p1/4P3/3P2P1/PPP1KP1P/RNB1RBNQ w e - +perft 1 31 +perft 2 800 +perft 3 24748 +perft 4 693366 +perft 5 21193292 +perft 6 625757852 + +id 662 +epd r1bkrnqb/pp3ppp/n1ppp3/8/1P5P/P7/R1PPPPP1/1NBKRNQB w Eea - +perft 1 21 +perft 2 482 +perft 3 11417 +perft 4 275339 +perft 5 7112890 +perft 6 180378139 + +id 663 +epd rbnkbrnq/ppp1p2p/5p2/3p2p1/1B1P4/1N4P1/PPP1PP1P/RB1K1RNQ w FAfa - +perft 1 33 +perft 2 780 +perft 3 25532 +perft 4 628945 +perft 5 20756770 +perft 6 535497008 + +id 664 +epd rnk1brnq/pp1ppppp/2p5/b7/8/1P2P2P/P1PP1PPQ/RNKBBRN1 w FAfa - +perft 1 29 +perft 2 648 +perft 3 19043 +perft 4 449637 +perft 5 13722785 +perft 6 341389148 + +id 665 +epd rnkrbbnq/p1p3pp/5p2/1p1pp3/P7/1PN2P2/2PPP1PP/R1KRBBNQ w DAda - +perft 1 26 +perft 2 827 +perft 3 21865 +perft 4 683167 +perft 5 18916370 +perft 6 589161126 + +id 666 +epd r1krbnqb/p1pp1ppp/2n1p3/8/1p4P1/PPP5/3PPP1P/RNKRBNQB w DAda - +perft 1 25 +perft 2 540 +perft 3 14709 +perft 4 331332 +perft 5 9491817 +perft 6 225389422 + +id 667 +epd rbnkrnbq/ppp1pp2/3p2p1/2N5/P6p/2P5/1P1PPPPP/RB1KRNBQ w EAea - +perft 1 32 +perft 2 790 +perft 3 25107 +perft 4 661207 +perft 5 20906017 +perft 6 578332225 + +id 668 +epd rnkbrn1q/1ppppppb/8/p4N1p/8/P1N5/1PPPPPPP/R1KBR1BQ w EAea - +perft 1 31 +perft 2 691 +perft 3 20813 +perft 4 510665 +perft 5 15308408 +perft 6 404129987 + +id 669 +epd rnkrnbbq/p1p2ppp/3pp3/1p6/6P1/4PQ1B/PPPP1P1P/RNKRN1B1 w DAda - +perft 1 29 +perft 2 558 +perft 3 16800 +perft 4 352887 +perft 5 10825379 +perft 6 246965507 + +id 670 +epd rnkrnqbb/pp2p1p1/3p3p/2p2p2/5P2/1P1N4/P1PPPQPP/RNKR2BB w DAda - +perft 1 29 +perft 2 762 +perft 3 23210 +perft 4 644936 +perft 5 20522675 +perft 6 596067005 + +id 671 +epd bb1rknnr/ppqppppp/8/2p5/3P1N2/1P6/P1P1PPPP/BBQRKN1R w HDhd - +perft 1 33 +perft 2 963 +perft 3 32279 +perft 4 1000890 +perft 5 34552118 +perft 6 1124738493 + +id 672 +epd bqrbknnr/ppp1p2p/8/3p1p2/5p2/P3N2P/1PPPP1P1/BQRBK1NR w HChc - +perft 1 20 +perft 2 398 +perft 3 9009 +perft 4 194859 +perft 5 4834319 +perft 6 113660536 + +id 673 +epd b1rk1bnr/qpp1pppp/p4n2/3p4/3PPP2/7N/PPP3PP/BQRKNB1R w HChc - +perft 1 25 +perft 2 648 +perft 3 16587 +perft 4 455720 +perft 5 12200870 +perft 6 351766307 + +id 674 +epd bqkrnnrb/pppp2p1/4pp2/4P2p/6P1/7P/PPPP1P2/BQRKNNRB w GC - +perft 1 30 +perft 2 493 +perft 3 15118 +perft 4 280726 +perft 5 8786998 +perft 6 181492621 + +id 675 +epd q1brknnr/1p1ppppp/p7/2p5/8/1PPP4/P2RPPPP/QBB1KNNR w Hhd - +perft 1 25 +perft 2 501 +perft 3 13206 +perft 4 290463 +perft 5 7982978 +perft 6 192717198 + +id 676 +epd qrb1k1nr/ppppb1pp/6n1/4ppN1/3P4/4N3/PPP1PPPP/QRBBK2R w HBhb - +perft 1 31 +perft 2 872 +perft 3 26191 +perft 4 739276 +perft 5 22493014 +perft 6 646855304 + +id 677 +epd 1rbknbnr/1ppp1pp1/q6p/p3p3/5P2/2PPB3/PP2P1PP/QR1KNBNR w HBhb - +perft 1 28 +perft 2 1020 +perft 3 28147 +perft 4 984000 +perft 5 27484692 +perft 6 947786800 + +id 678 +epd qrbk2rb/1ppp1ppp/5nn1/p3p3/1N6/P7/1PPPPPPP/QRB1KNRB w gb - +perft 1 23 +perft 2 592 +perft 3 14398 +perft 4 395716 +perft 5 10098215 +perft 6 293988585 + +id 679 +epd qbrk1nnr/1pp1pppp/2b5/p2p4/P2P2P1/8/1PP1PP1P/QBKRBNNR w hc - +perft 1 26 +perft 2 654 +perft 3 18103 +perft 4 471653 +perft 5 13740891 +perft 6 373081138 + +id 680 +epd qrkbbnnr/ppp2p1p/4p3/3p2p1/P7/2PP4/1P2PPPP/QRKBBNNR w HBhb - +perft 1 25 +perft 2 626 +perft 3 16616 +perft 4 431634 +perft 5 12079406 +perft 6 324006164 + +id 681 +epd qr1kbbnr/ppp1pp1p/4n1p1/2Pp4/6P1/4N3/PP1PPP1P/QRK1BBNR w HB d6 +perft 1 26 +perft 2 699 +perft 3 18068 +perft 4 497152 +perft 5 13353359 +perft 6 375702908 + +id 682 +epd qrk1b1rb/p1pppppp/3nnQ2/1p6/1P3P2/3P4/P1P1P1PP/1RKNBNRB w GBgb - +perft 1 43 +perft 2 1369 +perft 3 55463 +perft 4 1831200 +perft 5 71514365 +perft 6 2427477375 + +id 683 +epd qbrk1nbr/pppp3p/5n2/4ppp1/3P1P2/4N3/PPP1P1PP/QBKRN1BR w hc - +perft 1 25 +perft 2 752 +perft 3 20165 +perft 4 615263 +perft 5 17493373 +perft 6 543180234 + +id 684 +epd qrkb1nbr/1pppppQp/3n4/p7/5p2/1P1N4/P1PPP1PP/1RKB1NBR w HBhb - +perft 1 45 +perft 2 946 +perft 3 40100 +perft 4 966903 +perft 5 39736157 +perft 6 1051910977 + +id 685 +epd qrk1nbbr/ppp1p1p1/4n2p/3p1p2/1P5P/3P2P1/P1P1PP2/QRKNNBBR w HBhb - +perft 1 32 +perft 2 770 +perft 3 25367 +perft 4 646977 +perft 5 21717615 +perft 6 577979364 + +id 686 +epd qrkn1rbb/pp2pppp/2p5/3p4/P2Qn1P1/1P6/2PPPP1P/1RKNNRBB w FBfb - +perft 1 38 +perft 2 943 +perft 3 35335 +perft 4 868165 +perft 5 31909835 +perft 6 798405123 + +id 687 +epd bbrqknnr/ppp4p/3pp3/5pp1/4PP2/5Q2/PPPP2PP/BBR1KNNR w HChc - +perft 1 36 +perft 2 843 +perft 3 29974 +perft 4 758528 +perft 5 26828059 +perft 6 723306114 + +id 688 +epd 1rqbkn1r/p1p1pppp/1p5n/P2p4/3Pb1P1/8/1PP1PP1P/BRQBKNNR w HBhb - +perft 1 23 +perft 2 778 +perft 3 19482 +perft 4 649789 +perft 5 17337683 +perft 6 579112676 + +id 689 +epd br1knbnr/1qp1pppp/pp1p4/8/8/PP6/2PPPPPP/BRQKNBNR w HBhb - +perft 1 26 +perft 2 697 +perft 3 18835 +perft 4 546622 +perft 5 15280079 +perft 6 473071890 + +id 690 +epd brqk2rb/ppppp1pp/4np2/8/2n5/3P1Q2/PP2PPPP/BR1KNNRB w GBgb - +perft 1 32 +perft 2 948 +perft 3 30434 +perft 4 885713 +perft 5 29821322 +perft 6 874251866 + +id 691 +epd r1bqknnr/pp1pp1p1/5p1p/2p1b2N/2P5/8/PPQPPPPP/RBB1K1NR w HAha - +perft 1 31 +perft 2 785 +perft 3 25549 +perft 4 659952 +perft 5 22244193 +perft 6 592797491 + +id 692 +epd rqbbknnr/ppppp2p/5pp1/8/8/1P3PP1/PQPPP2P/R1BBKNNR w HAha - +perft 1 23 +perft 2 391 +perft 3 10163 +perft 4 198450 +perft 5 5576671 +perft 6 121267576 + +id 693 +epd rqbknbnr/1pp1p2p/p7/3p1pp1/7N/1PP5/P2PPPPP/RQBK1BNR w HAha - +perft 1 27 +perft 2 676 +perft 3 19606 +perft 4 522428 +perft 5 15955388 +perft 6 448477218 + +id 694 +epd rqb1nnrb/2ppkppp/1p2p3/p7/2PPP3/1P6/P4PPP/RQBKNNRB w GA - +perft 1 31 +perft 2 727 +perft 3 22895 +perft 4 570647 +perft 5 18361051 +perft 6 483248153 + +id 695 +epd rb1kbn1r/p1ppppp1/qp5n/7p/P7/RPP5/3PPPPP/1BQKBNNR w Hha - +perft 1 29 +perft 2 837 +perft 3 23815 +perft 4 730083 +perft 5 21279560 +perft 6 682863811 + +id 696 +epd rqkbb1nr/p1p2ppp/1p1p2n1/3Np3/4P3/5N2/PPPP1PPP/RQKBB2R w HAha - +perft 1 28 +perft 2 717 +perft 3 20663 +perft 4 550987 +perft 5 16347343 +perft 6 453153783 + +id 697 +epd rqknbbr1/p1pppp1p/1p3np1/8/4P3/2P2P1P/PP1P2P1/RQKNBBNR w HAa - +perft 1 27 +perft 2 650 +perft 3 18231 +perft 4 475303 +perft 5 13847463 +perft 6 383256006 + +id 698 +epd r1k1bnrb/1qpppppp/1p2n3/p7/1P5P/6P1/P1PPPP2/RQKNBNR1 w GAga - +perft 1 24 +perft 2 806 +perft 3 20693 +perft 4 713220 +perft 5 19382263 +perft 6 686009788 + +id 699 +epd rb1knnbr/1pp1ppp1/p2p3p/5q2/3B2P1/3P1P2/PPP1P2P/RBQKNN1R w HAha - +perft 1 34 +perft 2 1360 +perft 3 44096 +perft 4 1605706 +perft 5 51973672 +perft 6 1837704407 + +id 700 +epd rqkb1nbr/p1p1ppp1/1p3n1p/2Qp4/8/2P5/PP1PPPPP/R1KBNNBR w HAha - +perft 1 39 +perft 2 983 +perft 3 38218 +perft 4 940989 +perft 5 36347815 +perft 6 918801645 + +id 701 +epd rqknnbbr/2pppp2/pp5p/6p1/1P1P4/4PP2/P1P3PP/RQKNNBBR w HAha - +perft 1 26 +perft 2 628 +perft 3 17638 +perft 4 464924 +perft 5 13787303 +perft 6 386125234 + +id 702 +epd rqkn1rbb/1pp1pppp/p7/3p4/3Pn3/2P1PP2/PP4PP/RQKNNRBB w FAfa - +perft 1 20 +perft 2 527 +perft 3 12216 +perft 4 321533 +perft 5 8082183 +perft 6 219311659 + +id 703 +epd bbrkqn1r/1pppppp1/5n2/p7/1PP2P1p/7N/P2PP1PP/BBRKQN1R w HChc - +perft 1 36 +perft 2 963 +perft 3 35291 +perft 4 973839 +perft 5 35907489 +perft 6 1034223364 + +id 704 +epd brkbqn1r/p2ppppp/7n/1p6/P1p3PP/8/1PPPPP1N/BRKBQ1NR w HBhb - +perft 1 18 +perft 2 583 +perft 3 11790 +perft 4 394603 +perft 5 8858385 +perft 6 304339862 + +id 705 +epd brkq1bnr/pp1ppp1p/8/2p2np1/P7/8/1PPPPPPP/BRKQNBNR w HBhb - +perft 1 19 +perft 2 552 +perft 3 11811 +perft 4 354260 +perft 5 8432183 +perft 6 262293169 + +id 706 +epd brkqnnrb/1ppppppp/8/8/p3P3/5N2/PPPP1PPP/BRKQ1NRB w GBgb - +perft 1 21 +perft 2 397 +perft 3 9653 +perft 4 204350 +perft 5 5489836 +perft 6 128389738 + +id 707 +epd rbbkq1nr/1p2pppp/p1p3nB/3p4/1Q1P4/6N1/PPP1PPPP/RB1K2NR w HAha - +perft 1 40 +perft 2 1132 +perft 3 43404 +perft 4 1260470 +perft 5 47425783 +perft 6 1415578783 + +id 708 +epd rkbbq1nr/1pppp1p1/4np2/p6p/8/PP3P2/1KPPP1PP/R1BBQNNR w ha - +perft 1 24 +perft 2 596 +perft 3 15220 +perft 4 402121 +perft 5 10822049 +perft 6 302056813 + +id 709 +epd r1bqn1nr/pkpppp1p/1p4pb/8/PN6/R7/1PPPPPPP/1KBQ1BNR w H - +perft 1 33 +perft 2 794 +perft 3 25450 +perft 4 649150 +perft 5 20919309 +perft 6 561073410 + +id 710 +epd rkb1nnrb/1pppq1pp/p4p2/4p3/5P2/1P1PB3/P1P1P1PP/RK1QNNRB w GAga - +perft 1 26 +perft 2 625 +perft 3 17050 +perft 4 442036 +perft 5 12515042 +perft 6 342967558 + +id 711 +epd rbkqbn1r/pppp1p1p/2n1p1p1/8/8/1P1PP1N1/P1P2PPP/RBKQB1NR w HAha - +perft 1 30 +perft 2 660 +perft 3 20308 +perft 4 492714 +perft 5 15348335 +perft 6 403323883 + +id 712 +epd rkqbb1n1/pppppppr/8/6np/5P2/8/PPPPP1PP/RKQBBNNR w HAa - +perft 1 23 +perft 2 500 +perft 3 12154 +perft 4 292936 +perft 5 7519117 +perft 6 196524441 + +id 713 +epd rkqnbbnr/ppppppp1/8/7p/3N4/6PP/PPPPPP2/RKQNBB1R w HAa - +perft 1 24 +perft 2 484 +perft 3 12495 +perft 4 284570 +perft 5 7775173 +perft 6 193947530 + +id 714 +epd rkqnb1rb/p1p1pppp/1p1p4/2n5/3P4/2P1N1N1/PP2PPPP/RKQ1B1RB w GAga - +perft 1 28 +perft 2 1020 +perft 3 29124 +perft 4 1027904 +perft 5 30515456 +perft 6 1073711823 + +id 715 +epd rbk1nnbr/1ppq1ppp/p2p4/4p3/P3B2P/2P5/1P1PPPP1/R1KQNNBR w HAha - +perft 1 38 +perft 2 998 +perft 3 37265 +perft 4 1047592 +perft 5 38552638 +perft 6 1139322479 + +id 716 +epd r1qbn1br/k1pppppp/6n1/pp6/5P1P/P7/1PPPP1PB/RKQBNN1R w HA - +perft 1 22 +perft 2 549 +perft 3 12867 +perft 4 348574 +perft 5 8725809 +perft 6 251613569 + +id 717 +epd rkqnn1br/pppp3p/4p1pb/5p2/P2P4/7P/1PP1PPPB/RKQNNB1R w HAha - +perft 1 32 +perft 2 659 +perft 3 21249 +perft 4 469701 +perft 5 15434721 +perft 6 365761521 + +id 718 +epd rk1nnrbb/p1p1pppp/1p6/3p1q2/P3P3/2NN4/1PPP1PPP/RKQ2RBB w FAfa - +perft 1 29 +perft 2 989 +perft 3 29087 +perft 4 980477 +perft 5 29643404 +perft 6 998848556 + +id 719 +epd bbrk1q1r/ppppppp1/3n4/7p/3Pn3/6PN/PPP1PPNP/BBRK1Q1R w HChc - +perft 1 23 +perft 2 712 +perft 3 16551 +perft 4 516177 +perft 5 12995202 +perft 6 411077508 + +id 720 +epd brkbnq1r/p1ppp2p/5ppn/1p6/5P2/1P1P2P1/P1P1P2P/BRKBNQNR w HBhb - +perft 1 28 +perft 2 856 +perft 3 24984 +perft 4 780503 +perft 5 23529352 +perft 6 754501112 + +id 721 +epd br1k1bnr/ppppp1pp/4np2/1B2P2q/3P4/8/PPP2PPP/BRKNQ1NR w HB - +perft 1 36 +perft 2 1214 +perft 3 40615 +perft 4 1328331 +perft 5 45096834 +perft 6 1470987023 + +id 722 +epd brk1qnrb/pnppp1p1/1p6/5p1p/8/5PPP/PPPPP1R1/BRKNQN1B w Bgb - +perft 1 22 +perft 2 551 +perft 3 13111 +perft 4 353317 +perft 5 9040545 +perft 6 259643605 + +id 723 +epd rbbkn1nr/1ppp2pp/p3p3/2q2p2/3P4/6P1/PPPBPP1P/RB1KNQNR w HAha - +perft 1 31 +perft 2 1060 +perft 3 31332 +perft 4 1015099 +perft 5 30314172 +perft 6 976268967 + +id 724 +epd rkbbn1nr/ppppp1pp/8/6N1/5p2/1q6/P1PPPPPP/RKBBN1QR w HAha - +perft 1 3 +perft 2 72 +perft 3 1919 +perft 4 50827 +perft 5 1400832 +perft 6 39654253 + +id 725 +epd rkb2bnr/pp2pppp/2p1n3/3p4/q2P4/5NP1/PPP1PP1P/RKBNQBR1 w Aha - +perft 1 29 +perft 2 861 +perft 3 24504 +perft 4 763454 +perft 5 22763215 +perft 6 731511256 + +id 726 +epd rkbq1nrb/ppppppp1/7p/8/1P1n4/P4P1P/2PPP1P1/RKBNQNRB w GAga - +perft 1 25 +perft 2 672 +perft 3 17631 +perft 4 473864 +perft 5 12954224 +perft 6 361237536 + +id 727 +epd rbknb1nr/ppp1qp1p/6p1/3pp3/3P3P/2B1P3/PPP2PP1/RBKN1QNR w HAha - +perft 1 27 +perft 2 857 +perft 3 24688 +perft 4 792538 +perft 5 23790033 +perft 6 768247869 + +id 728 +epd rknbbq1r/p1pppppp/1p2N3/8/3n4/2P5/PP1PPPPP/RK1BBQNR w HAha - +perft 1 29 +perft 2 763 +perft 3 22138 +perft 4 574054 +perft 5 16926075 +perft 6 447896703 + +id 729 +epd r1nqbbnr/1pppp1pp/1k6/p4p2/8/4P3/PPPP1PPP/RKN1BBNR w HA - +perft 1 26 +perft 2 658 +perft 3 17302 +perft 4 464039 +perft 5 12380488 +perft 6 349047256 + +id 730 +epd rkn2qrb/ppp1pppp/6n1/1b1p4/1P6/4PPB1/P1PP2PP/RKNQ1NRB w GAga - +perft 1 23 +perft 2 574 +perft 3 14070 +perft 4 370324 +perft 5 9501401 +perft 6 263870337 + +id 731 +epd rbkn2br/ppppp1p1/4np1p/1P5q/8/2P1N3/P2PPPPP/RBK1QNBR w HAha - +perft 1 29 +perft 2 992 +perft 3 29506 +perft 4 999564 +perft 5 30148787 +perft 6 1045942540 + +id 732 +epd 1knbqnbr/1ppppp1p/r5p1/p7/7P/2PN2P1/PP1PPP2/RK1BQNBR w HAh - +perft 1 26 +perft 2 698 +perft 3 19395 +perft 4 512023 +perft 5 14848229 +perft 6 402599313 + +id 733 +epd rk1qnbbr/pnpppp1p/6p1/1p6/3P4/1P6/P1P1PPPP/RKNQNBBR w HAha - +perft 1 20 +perft 2 480 +perft 3 11159 +perft 4 287539 +perft 5 7425917 +perft 6 203194521 + +id 734 +epd rknqnrbb/pp1p2p1/5p1p/2p1p3/2P1P3/P2P4/1P3PPP/RKNQNRBB w FAfa - +perft 1 26 +perft 2 679 +perft 3 18116 +perft 4 494953 +perft 5 13790137 +perft 6 392629571 + +id 735 +epd bbrk2qr/pp1p1ppp/3n2n1/2p1p3/3P1P2/6N1/PPP1P1PP/BBRKN1QR w HChc - +perft 1 26 +perft 2 790 +perft 3 21521 +perft 4 673269 +perft 5 19259490 +perft 6 617563700 + +id 736 +epd b1krnnqr/1p1ppppp/p1p5/b6B/P7/4P1N1/1PPP1PPP/BRK1N1QR w HB - +perft 1 26 +perft 2 625 +perft 3 16451 +perft 4 415452 +perft 5 11490615 +perft 6 304805107 + +id 737 +epd 1rknnbqr/3ppppp/p7/1pp5/4b2P/P4P2/1PPPP1PR/BRKNNBQ1 w Bhb - +perft 1 24 +perft 2 757 +perft 3 19746 +perft 4 618777 +perft 5 17275100 +perft 6 544309489 + +id 738 +epd br1nn1rb/pppkpqpp/3p1p2/8/PP6/4N3/1KPPPPPP/BR2NQRB w - - +perft 1 24 +perft 2 682 +perft 3 17129 +perft 4 482711 +perft 5 13057308 +perft 6 375033550 + +id 739 +epd rbbkn1qr/pppp2p1/6np/4pp2/7N/7P/PPPPPPPR/RBBK1NQ1 w Aha - +perft 1 22 +perft 2 586 +perft 3 14158 +perft 4 409891 +perft 5 10607781 +perft 6 324452612 + +id 740 +epd rk1bn1qr/pppbpppp/4n3/4p3/4P3/5P2/PPPP2PP/RKBB1NQR w HAha - +perft 1 22 +perft 2 530 +perft 3 13440 +perft 4 348004 +perft 5 9514787 +perft 6 259898748 + +id 741 +epd rkbnnbqr/1ppp1ppp/p7/4p3/8/QP3P2/P1PPP1PP/RKBNNB1R w HAha - +perft 1 29 +perft 2 705 +perft 3 21511 +perft 4 551042 +perft 5 17524731 +perft 6 472356665 + +id 742 +epd 1kbnnqrb/1pp1p1pp/r4p2/p2p4/N4P2/3P4/PPP1P1PP/RKB1NQRB w GAg - +perft 1 21 +perft 2 623 +perft 3 14979 +perft 4 437554 +perft 5 11601134 +perft 6 343214006 + +id 743 +epd rbknbn1r/pppp1p1p/4p1q1/8/P1P3Pp/8/1P1PPP2/RBKNBNQR w HAha - +perft 1 30 +perft 2 813 +perft 3 24959 +perft 4 708454 +perft 5 23379040 +perft 6 692576573 + +id 744 +epd rk1bb1qr/2pppppp/p2nn3/1p4P1/6QP/8/PPPPPP2/RKNBBN1R w HAha - +perft 1 36 +perft 2 857 +perft 3 30124 +perft 4 757524 +perft 5 26485812 +perft 6 696999449 + +id 745 +epd rkn1bbqr/p2ppppp/2p1n3/1p6/4PP2/6PP/PPPP4/RKNNBBQR w HAha - +perft 1 33 +perft 2 687 +perft 3 22744 +perft 4 511018 +perft 5 17101732 +perft 6 412778368 + +id 746 +epd rkn1bqrb/pnp1pppp/3p4/8/Pp6/1N2NP2/1PPPP1PP/RK2BQRB w GAga - +perft 1 28 +perft 2 591 +perft 3 17174 +perft 4 406025 +perft 5 12182448 +perft 6 312575205 + +id 747 +epd rbk1n1br/ppp1ppqp/2n5/2Np2p1/8/2P5/PPBPPPPP/R1KN1QBR w HAha - +perft 1 35 +perft 2 930 +perft 3 30663 +perft 4 844433 +perft 5 27160490 +perft 6 780616047 + +id 748 +epd rknbn1br/1ppp1ppp/p3p3/8/1q6/2P2N1P/P2PPPP1/RKNB1QBR w HAha - +perft 1 4 +perft 2 157 +perft 3 3697 +perft 4 138102 +perft 5 3454704 +perft 6 125373395 + +id 749 +epd rkn1qbbr/pp3ppp/4n3/2ppp3/4P1P1/P2P4/1PP2P1P/RKNNQBBR w HAha - +perft 1 28 +perft 2 840 +perft 3 24437 +perft 4 771328 +perft 5 23200961 +perft 6 756489357 + +id 750 +epd rkn1qrbb/pp1ppp2/2p1n1p1/7p/2P2P1P/6P1/PP1PP3/RKNNQRBB w FAfa - +perft 1 32 +perft 2 867 +perft 3 27595 +perft 4 757836 +perft 5 24485663 +perft 6 688115847 + +id 751 +epd b1rknnrq/bpppp1p1/p6p/5p1P/6P1/4N3/PPPPPP2/BBRKN1RQ w GCgc - +perft 1 33 +perft 2 851 +perft 3 28888 +perft 4 763967 +perft 5 26686205 +perft 6 731944177 + +id 752 +epd brkb1nr1/pppppp2/3n2pp/3B4/1P6/4P3/PqPP1PPP/BRK1NNRQ w GBgb - +perft 1 4 +perft 2 98 +perft 3 2965 +perft 4 76143 +perft 5 2352530 +perft 6 64251468 + +id 753 +epd brk1nbrq/1ppppn1p/6p1/p4p2/P5P1/5R2/1PPPPP1P/BRKNNB1Q w Bgb - +perft 1 29 +perft 2 922 +perft 3 27709 +perft 4 879527 +perft 5 27463717 +perft 6 888881062 + +id 754 +epd brkn1rqb/1p1ppppp/3n4/p1p5/1P3P2/8/PNPPP1PP/BR1KNRQB w fb - +perft 1 29 +perft 2 633 +perft 3 19399 +perft 4 469818 +perft 5 15076198 +perft 6 396737074 + +id 755 +epd rb1k1nrq/pbp1pppp/1p1p1n2/8/5P2/4NN1P/PPPPP1P1/RBBK2RQ w GAga - +perft 1 28 +perft 2 841 +perft 3 24056 +perft 4 710751 +perft 5 20772996 +perft 6 613798447 + +id 756 +epd rkbbnnrq/p1pp3p/4p1p1/1p3p2/P6P/1P6/1BPPPPP1/RK1BNNRQ w GAga - +perft 1 33 +perft 2 957 +perft 3 30668 +perft 4 907217 +perft 5 29735654 +perft 6 903933626 + +id 757 +epd rk2nbrq/p1ppppp1/bpn5/7p/6P1/2N2P2/PPPPP1QP/RKB1NBR1 w GAga - +perft 1 24 +perft 2 687 +perft 3 18206 +perft 4 544627 +perft 5 15518417 +perft 6 484217179 + +id 758 +epd rkbn1r1b/pp1pppnp/6q1/2p3p1/5P1P/4N3/PPPPP1P1/RKB1NRQB w FAfa - +perft 1 23 +perft 2 831 +perft 3 21254 +perft 4 754622 +perft 5 21126103 +perft 6 744755212 + +id 759 +epd rbknb1rq/ppp1p1p1/3pnp1p/8/6PP/2PP4/PP2PP2/RBKNBNRQ w GAga - +perft 1 31 +perft 2 838 +perft 3 26800 +perft 4 736910 +perft 5 24008129 +perft 6 677776408 + +id 760 +epd rknbb1rq/p1pn1ppp/4p3/1p1p4/2P5/1P2N1P1/P2PPP1P/RKNBB1RQ w GAga - +perft 1 29 +perft 2 830 +perft 3 24798 +perft 4 721630 +perft 5 22243832 +perft 6 660040360 + +id 761 +epd rk1nbbrq/pp1p1ppp/3n4/P3p3/2p4P/8/1PPPPPP1/RKNNBBRQ w GAga - +perft 1 24 +perft 2 484 +perft 3 12776 +perft 4 297419 +perft 5 8379748 +perft 6 214004367 + +id 762 +epd rknnbr1b/ppp2pqp/3p4/4p1p1/7P/3P1P2/PPP1P1P1/RKNNBRQB w FAfa - +perft 1 32 +perft 2 838 +perft 3 26408 +perft 4 740701 +perft 5 23472124 +perft 6 699211365 + +id 763 +epd rb1k1rbq/ppppN1pp/2nn4/5p2/7P/8/PPPPPPP1/RBK1NRBQ w FA - +perft 1 27 +perft 2 800 +perft 3 22785 +perft 4 701742 +perft 5 20804424 +perft 6 660917073 + +id 764 +epd r1nbnrbq/kppppp1p/6p1/8/p1PP1P2/4P3/PP4PP/RKNBNRBQ w FA - +perft 1 28 +perft 2 757 +perft 3 21198 +perft 4 602699 +perft 5 17180857 +perft 6 507618340 + +id 765 +epd rkn1rbbq/p1pppppp/2n5/1pP5/8/1N2P3/PP1P1PPP/RK1NRBBQ w EAea - +perft 1 22 +perft 2 483 +perft 3 11890 +perft 4 283679 +perft 5 7497674 +perft 6 191130942 + +id 766 +epd rknnrqbb/2pppppp/8/p7/Np3P2/3P4/PPP1P1PP/RKN1RQBB w EAea - +perft 1 25 +perft 2 536 +perft 3 14456 +perft 4 339180 +perft 5 9694947 +perft 6 245669668 + +id 767 +epd bb1rknrn/1qppppp1/1p4B1/p6N/8/2P5/PP1PPPPP/B1QRK1RN w GDgd - +perft 1 32 +perft 2 715 +perft 3 22421 +perft 4 575008 +perft 5 17860156 +perft 6 502410909 + +id 768 +epd b1rbknrn/qpp1ppp1/p6p/3p4/2P5/1P1P1P2/P3P1PP/BQRBKNRN w GCgc - +perft 1 30 +perft 2 818 +perft 3 24421 +perft 4 688711 +perft 5 20981488 +perft 6 611986786 + +id 769 +epd bqkrnbrn/1pp1pp1p/p7/1B1p2p1/4P3/7P/PPPP1PP1/BQKRN1RN w - - +perft 1 28 +perft 2 676 +perft 3 18366 +perft 4 478054 +perft 5 13126287 +perft 6 363765666 + +id 770 +epd bqrknrnb/1p2ppp1/p1pp3p/8/3P1P2/1PP5/P3P1PP/BQRKNRNB w FCfc - +perft 1 31 +perft 2 646 +perft 3 20686 +perft 4 455607 +perft 5 14984618 +perft 6 349082278 + +id 771 +epd qbbrkn1r/pppppp1p/8/6p1/2P1Pn1P/6N1/PP1P1PP1/QBBRKNR1 w GDd - +perft 1 20 +perft 2 532 +perft 3 11581 +perft 4 303586 +perft 5 7512432 +perft 6 202967948 + +id 772 +epd 1rbbknr1/p1ppp1pp/1pq2pn1/8/3P4/P3P3/QPP2PPP/1RBBKNRN w GBgb - +perft 1 31 +perft 2 1002 +perft 3 30581 +perft 4 999607 +perft 5 30642468 +perft 6 1009228283 + +id 773 +epd qrbkn1rn/pppp1ppp/8/6b1/P1P1Pp2/8/1P1P2PP/QRBKNBRN w GBgb - +perft 1 22 +perft 2 505 +perft 3 12447 +perft 4 304863 +perft 5 8192621 +perft 6 214730959 + +id 774 +epd qrbk1rnb/p2ppp1p/5n2/1pp3p1/8/7P/PPPPPPPN/QRBKR1NB w Bfb - +perft 1 20 +perft 2 619 +perft 3 13448 +perft 4 449630 +perft 5 10571176 +perft 6 369603424 + +id 775 +epd qbrkb1r1/ppp2ppp/3pn1n1/P3p3/4P3/3P4/1PP2PPP/QBRKBNRN w GCgc - +perft 1 26 +perft 2 755 +perft 3 20596 +perft 4 604483 +perft 5 17164382 +perft 6 510878835 + +id 776 +epd qrkbb1r1/ppp1pnpp/3p2n1/5p2/1P3P2/2Q3N1/P1PPP1PP/1RKBB1RN w GBgb - +perft 1 35 +perft 2 918 +perft 3 32244 +perft 4 870888 +perft 5 30933394 +perft 6 867833733 + +id 777 +epd qrknbbrn/ppp1ppp1/8/7p/2Bp4/4PPP1/PPPP3P/QRKNB1RN w GBgb - +perft 1 27 +perft 2 593 +perft 3 16168 +perft 4 376808 +perft 5 10422676 +perft 6 258348640 + +id 778 +epd qrk1brnb/ppppp3/4n2p/5pp1/2PP4/2N4P/PP2PPP1/QRK1BRNB w FBfb - +perft 1 24 +perft 2 672 +perft 3 17447 +perft 4 506189 +perft 5 13765777 +perft 6 414930519 + +id 779 +epd qbrknrb1/p2ppppp/2p3n1/8/p4P2/6PP/1PPPP3/QBRKNRBN w FCfc - +perft 1 29 +perft 2 759 +perft 3 23235 +perft 4 634493 +perft 5 20416668 +perft 6 584870558 + +id 780 +epd 1rkb1rbn/p1pp1ppp/3np3/1p6/4qP2/3NB3/PPPPPRPP/QRKB3N w Bfb - +perft 1 22 +perft 2 923 +perft 3 22585 +perft 4 914106 +perft 5 24049880 +perft 6 957218571 + +id 781 +epd 1rknrbbn/p1pp1p1p/8/1p2p1p1/4qPP1/2P5/PP1PP1BP/QRKNR1BN w EBeb - +perft 1 28 +perft 2 1309 +perft 3 36355 +perft 4 1568968 +perft 5 44576409 +perft 6 1846382333 + +id 782 +epd qrk1rn1b/ppppp2p/4n3/3b1pp1/4P2P/5BP1/PPPP1P2/QRKNRNB1 w EBeb - +perft 1 26 +perft 2 839 +perft 3 22189 +perft 4 726354 +perft 5 19978260 +perft 6 661207281 + +id 783 +epd bbrqk1rn/pp1ppppp/8/2p5/2P1P3/5n1P/PPBP1PP1/B1RQKNRN w GCgc - +perft 1 3 +perft 2 95 +perft 3 2690 +perft 4 85038 +perft 5 2518864 +perft 6 80775549 + +id 784 +epd brqbk2n/pppppprp/8/6p1/1P3n2/5P2/P1PPP1PP/R1QBKNRN w Gb - +perft 1 22 +perft 2 593 +perft 3 13255 +perft 4 362760 +perft 5 8922397 +perft 6 253271592 + +id 785 +epd brqknbr1/pp3ppp/3p2n1/2p1p3/2P5/5P2/PPKPP1PP/BRQ1NBRN w gb - +perft 1 21 +perft 2 590 +perft 3 13190 +perft 4 397355 +perft 5 9581695 +perft 6 304103516 + +id 786 +epd 1rqknrnb/2pp1ppp/p3p3/1p6/P2P4/5bP1/1PP1PP1P/BRQKNRNB w FBfb - +perft 1 24 +perft 2 737 +perft 3 20052 +perft 4 598439 +perft 5 17948681 +perft 6 536330341 + +id 787 +epd rbb1k1rn/p1pqpppp/6n1/1p1p4/5P2/3PP3/PPP1K1PP/RBBQ1NRN w ga - +perft 1 24 +perft 2 694 +perft 3 16773 +perft 4 513782 +perft 5 13094823 +perft 6 419402704 + +id 788 +epd rqbbknr1/1ppp2pp/p5n1/4pp2/P7/1PP5/1Q1PPPPP/R1BBKNRN w GAga - +perft 1 24 +perft 2 600 +perft 3 15347 +perft 4 408207 +perft 5 11029596 +perft 6 308553169 + +id 789 +epd rqbknbrn/2pppppp/6Q1/pp6/8/2P5/PP1PPPPP/R1BKNBRN w GAga - +perft 1 40 +perft 2 949 +perft 3 34100 +perft 4 889887 +perft 5 31296485 +perft 6 881529007 + +id 790 +epd rqbknr1b/pp1ppp2/2p2n1p/6p1/8/3P1PPP/PPP1P3/RQBKNRNB w FAfa - +perft 1 20 +perft 2 560 +perft 3 12275 +perft 4 373921 +perft 5 8687544 +perft 6 277906201 + +id 791 +epd rbqkbnrn/p3pppp/1p6/3p4/P1p3P1/1P6/1QPPPP1P/RB1KBNRN w GAga - +perft 1 30 +perft 2 1155 +perft 3 35865 +perft 4 1351455 +perft 5 43092716 +perft 6 1614019629 + +id 792 +epd rqkbb1rn/p1p1pppn/1p1p4/7p/4PP2/7P/PPPPB1P1/RQK1BNRN w GAga - +perft 1 30 +perft 2 701 +perft 3 20804 +perft 4 515942 +perft 5 15450970 +perft 6 401499189 + +id 793 +epd rqknbbrn/1p2pp1p/3p2p1/p1p5/P2P4/1P6/1KP1PPPP/RQ1NBBRN w ga - +perft 1 28 +perft 2 756 +perft 3 21655 +perft 4 610320 +perft 5 17989811 +perft 6 525585996 + +id 794 +epd rqknbrnb/1pp3pp/5p2/p2pp3/P7/3PPN2/1PP2PPP/RQKNBR1B w FAfa - +perft 1 26 +perft 2 731 +perft 3 19509 +perft 4 550395 +perft 5 15209404 +perft 6 439767476 + +id 795 +epd rbqkr1bn/p1pppp1p/1p1n4/6p1/7P/3P1PP1/PPP1P3/RBQKNRBN w FAa - +perft 1 27 +perft 2 586 +perft 3 16282 +perft 4 381604 +perft 5 10905865 +perft 6 274364342 + +id 796 +epd rqk1nrb1/ppbp1ppp/4p1n1/2p5/7P/1PP5/P2PPPP1/RQKBNRBN w FAfa - +perft 1 27 +perft 2 749 +perft 3 21480 +perft 4 602318 +perft 5 18084787 +perft 6 520547029 + +id 797 +epd rqknrbbn/pp1p1ppp/4p3/2p5/3P2P1/7P/PPP1PP2/RQKNRBBN w EAa - +perft 1 20 +perft 2 533 +perft 3 11829 +perft 4 336248 +perft 5 8230417 +perft 6 245871540 + +id 798 +epd rqknrnbb/pp1ppp1p/2p3p1/8/8/1P2P1NP/P1PP1PP1/RQKNR1BB w EAea - +perft 1 22 +perft 2 633 +perft 3 14480 +perft 4 441877 +perft 5 10827868 +perft 6 343525739 + +id 799 +epd 1brkq1rn/2pppppp/1p2n3/p2bN3/8/7P/PPPPPPP1/BBRKQ1RN w GCgc - +perft 1 27 +perft 2 748 +perft 3 20134 +perft 4 580054 +perft 5 16010135 +perft 6 475206624 + +id 800 +epd brkbqnrn/2pp1ppp/8/1p2p3/Pp2N3/8/2PPPPPP/BRKBQNR1 w GBgb - +perft 1 30 +perft 2 827 +perft 3 25308 +perft 4 757837 +perft 5 23746165 +perft 6 751690068 + +id 801 +epd brk1nbrn/pp1ppppp/2p5/7P/5P2/q2P4/PPP1P1P1/BRKQNBRN w GBgb - +perft 1 15 +perft 2 471 +perft 3 8716 +perft 4 276424 +perft 5 5960901 +perft 6 190316951 + +id 802 +epd brkqnrnb/1p1pp1p1/p4p2/2p4p/8/P2PP3/1PP1QPPP/BRK1NRNB w FBfb - +perft 1 24 +perft 2 479 +perft 3 12584 +perft 4 280081 +perft 5 7830230 +perft 6 190419716 + +id 803 +epd rbbkqnrn/2ppp2p/pp3p2/6p1/P6P/8/RPPPPPP1/1BBKQNRN w Gga - +perft 1 21 +perft 2 523 +perft 3 12125 +perft 4 328733 +perft 5 8322614 +perft 6 242240658 + +id 804 +epd rkbbqr1n/1ppppppn/7p/p7/4P3/2P2P2/PP1PB1PP/RKB1QNRN w GAa - +perft 1 27 +perft 2 563 +perft 3 16026 +perft 4 372148 +perft 5 11105151 +perft 6 283211800 + +id 805 +epd rkbqnbrn/ppppp3/8/5ppp/2P3P1/7P/PPQPPP2/RKB1NBRN w GAga - +perft 1 28 +perft 2 639 +perft 3 19250 +perft 4 469250 +perft 5 14872172 +perft 6 384663405 + +id 806 +epd rkb1nrnb/pppp1pp1/5q1p/8/P3p3/4R1P1/1PPPPP1P/1KBQNRNB w Ffa - +perft 1 28 +perft 2 873 +perft 3 23690 +perft 4 720814 +perft 5 20209424 +perft 6 625281937 + +id 807 +epd rbkqb1rn/1p1ppppp/4n3/p1p5/8/3PBP2/PPP1P1PP/RBKQ1NRN w GAga - +perft 1 26 +perft 2 798 +perft 3 21416 +perft 4 667496 +perft 5 18475618 +perft 6 591681956 + +id 808 +epd rk1qbnrn/1p1ppppp/1b6/p1p5/P7/2P3NP/1P1PPPP1/RKQBB1RN w GAga - +perft 1 22 +perft 2 506 +perft 3 12313 +perft 4 301029 +perft 5 7891676 +perft 6 205739580 + +id 809 +epd rk1nbbrn/ppp1ppp1/8/3p3p/1P1P2q1/5PB1/P1P1P1PP/RKQN1BRN w GAga - +perft 1 31 +perft 2 956 +perft 3 29219 +perft 4 903799 +perft 5 27827461 +perft 6 876341492 + +id 810 +epd rkqnbr1b/pp1pppp1/7p/2p2n2/P2P4/7N/RPP1PPPP/1KQNBR1B w Ffa - +perft 1 31 +perft 2 750 +perft 3 24267 +perft 4 646252 +perft 5 21639104 +perft 6 617064197 + +id 811 +epd rbkq1rbn/2p1pppp/pp3n2/3p4/5P2/3N2N1/PPPPP1PP/RBKQR1B1 w Afa - +perft 1 26 +perft 2 647 +perft 3 18027 +perft 4 465119 +perft 5 13643783 +perft 6 369702807 + +id 812 +epd rkqbr1bn/p2ppppp/1pp2n2/8/5P2/3P1N2/PPP1PRPP/RKQB2BN w Aa - +perft 1 24 +perft 2 574 +perft 3 14593 +perft 4 371597 +perft 5 10066892 +perft 6 271121237 + +id 813 +epd rk1qrbbn/p1ppp1pp/1p2n3/5p2/1P6/K3N3/P1PPPPPP/R1Q1RBBN w ea - +perft 1 25 +perft 2 548 +perft 3 14069 +perft 4 340734 +perft 5 9043111 +perft 6 235545764 + +id 814 +epd rkqnrnbb/pp1pp3/2p5/5ppp/8/PP4NP/2PPPPP1/RKQNR1BB w EAea - +perft 1 23 +perft 2 727 +perft 3 18228 +perft 4 566572 +perft 5 15078056 +perft 6 471296844 + +id 815 +epd bbrknq1r/ppppppp1/8/7p/5n2/3P4/PPP1PNPP/BBKRNQR1 w c - +perft 1 21 +perft 2 610 +perft 3 13300 +perft 4 394705 +perft 5 9605845 +perft 6 293532398 + +id 816 +epd brkbnqr1/2pppnpp/pp3p2/8/4PPPP/8/PPPP4/BRKBNQRN w GBgb - +perft 1 30 +perft 2 757 +perft 3 23908 +perft 4 621332 +perft 5 20360394 +perft 6 548380577 + +id 817 +epd brk1qb1n/ppppppr1/2n3pp/8/2P3P1/2N5/PP1PPP1P/BR1KQBRN w b - +perft 1 26 +perft 2 570 +perft 3 15537 +perft 4 352883 +perft 5 10081351 +perft 6 242864559 + +id 818 +epd brknq1nb/pp2prpp/8/2pP1p2/6P1/2N5/PPPP1P1P/BRK1QRNB w FBb - +perft 1 33 +perft 2 830 +perft 3 27897 +perft 4 764915 +perft 5 26262884 +perft 6 765831403 + +id 819 +epd rbbk1qrn/ppp1p1pp/5p2/3p1n2/7N/P7/1PPPPPPP/RBB1KQRN w ga - +perft 1 21 +perft 2 562 +perft 3 13060 +perft 4 378883 +perft 5 9520963 +perft 6 290579255 + +id 820 +epd rk1b1qrn/ppp1pppp/5n2/3pN3/P6P/7b/1PPPPPP1/RKBB1QRN w GAga - +perft 1 28 +perft 2 677 +perft 3 19235 +perft 4 488740 +perft 5 14354779 +perft 6 383207197 + +id 821 +epd rkbnqbrn/pp1ppp1p/2p5/6p1/P7/4P3/KPPPQPPP/R1BN1BRN w - - +perft 1 28 +perft 2 585 +perft 3 17443 +perft 4 401483 +perft 5 12574541 +perft 6 310495538 + +id 822 +epd rk1nqrnb/pbpppp2/1p4p1/7p/P7/5NP1/1PPPPPBP/RKBNQR2 w FAfa - +perft 1 26 +perft 2 774 +perft 3 21626 +perft 4 645200 +perft 5 19093408 +perft 6 576325868 + +id 823 +epd rbknb1rn/p1pp2pp/1p6/4pp2/1q3P1B/2N5/PPPPPNPP/RBK2QR1 w GAga - +perft 1 31 +perft 2 1206 +perft 3 36940 +perft 4 1374158 +perft 5 42849564 +perft 6 1555711209 + +id 824 +epd rk1bbqrn/pp1pp1pp/3n4/5p2/3p4/1PP5/PK2PPPP/R1NBBQRN w ga - +perft 1 21 +perft 2 629 +perft 3 14059 +perft 4 429667 +perft 5 10587910 +perft 6 332632033 + +id 825 +epd rknqbbr1/p1pp1pp1/1p4n1/4p2p/4P1P1/6RB/PPPP1P1P/RKNQB2N w Aga - +perft 1 27 +perft 2 753 +perft 3 20918 +perft 4 593155 +perft 5 17318772 +perft 6 507563675 + +id 826 +epd rknqbr1b/pppp1ppp/4p2n/8/1P3P2/4P3/P1PPN1PP/RKNQBR1B w FAfa - +perft 1 26 +perft 2 623 +perft 3 17177 +perft 4 460663 +perft 5 13389799 +perft 6 383508368 + +id 827 +epd r2kqrbn/bppppppp/2n5/p4B2/5P2/2P5/PP1PP1PP/1RKNQRBN w F - +perft 1 39 +perft 2 1026 +perft 3 37800 +perft 4 1011922 +perft 5 35946987 +perft 6 992756232 + +id 828 +epd rk1bqrb1/ppppppp1/1n6/7p/2P2P1n/4P1Q1/PP1P2PP/RKNB1RBN w FAfa - +perft 1 35 +perft 2 760 +perft 3 25817 +perft 4 610557 +perft 5 21014787 +perft 6 536852043 + +id 829 +epd rkq1rb1n/ppppp1pp/1n6/5p2/PPb2P2/8/1KPPP1PP/R1NQRBBN w ea - +perft 1 27 +perft 2 754 +perft 3 21009 +perft 4 568788 +perft 5 16461795 +perft 6 448313956 + +id 830 +epd rknqr2b/pppnp1pp/3p4/3b1p2/8/1N1P2N1/PPP1PPPP/RKQ1R1BB w EAea - +perft 1 27 +perft 2 803 +perft 3 23708 +perft 4 700453 +perft 5 21875031 +perft 6 654754840 + +id 831 +epd bbrknrqn/ppppp1pB/8/2P2p1p/8/5N2/PP1PPPPP/B1RK1RQN w FCfc - +perft 1 30 +perft 2 799 +perft 3 23923 +perft 4 671112 +perft 5 20532790 +perft 6 603059376 + +id 832 +epd brkbnrq1/1pppp1p1/6np/p4p2/4P3/1PP5/P1KP1PPP/BR1BNRQN w fb - +perft 1 27 +perft 2 726 +perft 3 19329 +perft 4 555622 +perft 5 15156662 +perft 6 457601127 + +id 833 +epd brknrbq1/1p1p1ppp/p3p1n1/2p5/8/1P1BPP2/P1PP2PP/BRKNR1QN w EBeb - +perft 1 36 +perft 2 786 +perft 3 27868 +perft 4 655019 +perft 5 22852433 +perft 6 577223409 + +id 834 +epd brknrqnb/p2ppp1p/2p5/1p6/3P2p1/P1P1N3/1P2PPPP/BRK1RQNB w EBeb - +perft 1 23 +perft 2 649 +perft 3 15169 +perft 4 440504 +perft 5 10687843 +perft 6 320881984 + +id 835 +epd rbbk1rqn/1ppppppp/3n4/p7/2P5/3N4/PP1PPPPP/RBB1KRQN w fa - +perft 1 20 +perft 2 478 +perft 3 11094 +perft 4 275250 +perft 5 7094988 +perft 6 185488058 + +id 836 +epd rkbbnrqn/p2p1ppp/1p2p3/8/P1p1P3/1BP5/1P1P1PPP/RKB1NRQN w FAfa - +perft 1 22 +perft 2 570 +perft 3 13295 +perft 4 346811 +perft 5 8671852 +perft 6 229898448 + +id 837 +epd rkb1rb1n/ppppppqp/8/2n3p1/2P1P1P1/8/PP1P1P1P/RKBNRBQN w EAea - +perft 1 23 +perft 2 663 +perft 3 16212 +perft 4 490748 +perft 5 12900485 +perft 6 404944553 + +id 838 +epd rkb1rqnb/pppp3p/2n3p1/4pp2/P2P3P/2P5/1P2PPP1/RKBNRQNB w EAea - +perft 1 25 +perft 2 845 +perft 3 22188 +perft 4 741972 +perft 5 20276176 +perft 6 683290790 + +id 839 +epd rbk1brqn/ppp1pppp/8/3p4/7P/1P4P1/2PPPP2/RBKNBRQN w FAfa - +perft 1 24 +perft 2 526 +perft 3 13862 +perft 4 322175 +perft 5 9054028 +perft 6 222704171 + +id 840 +epd rknbbrqn/pp3pp1/4p3/2pp3p/2P5/8/PPBPPPPP/RKN1BRQN w FAfa - +perft 1 26 +perft 2 756 +perft 3 19280 +perft 4 559186 +perft 5 14697705 +perft 6 433719427 + +id 841 +epd 1knrbbqn/rp1p1ppp/p3p3/2p5/8/5P1P/PPPPP1P1/RKNRBBQN w DAd - +perft 1 26 +perft 2 539 +perft 3 15194 +perft 4 345070 +perft 5 10223443 +perft 6 248715580 + +id 842 +epd rknr1qnb/ppp1p1pp/3p2b1/8/4p3/1P3P1P/P1PP2P1/RKNRBQNB w DAda - +perft 1 25 +perft 2 701 +perft 3 18969 +perft 4 561369 +perft 5 16047041 +perft 6 496340789 + +id 843 +epd rbk1r1bn/ppppp1pp/4n3/5p2/1P3P2/4N2P/PqPPP1P1/RBK1RQBN w EAea - +perft 1 2 +perft 2 60 +perft 3 1319 +perft 4 41765 +perft 5 1017864 +perft 6 33183408 + +id 844 +epd r1nbrqbn/k1ppp1pp/1p6/p4p2/2P5/6PQ/PP1PPP1P/RKNBR1BN w EA - +perft 1 27 +perft 2 699 +perft 3 20436 +perft 4 561765 +perft 5 17192121 +perft 6 499247248 + +id 845 +epd rknrqbbn/1pp1pp2/p5p1/3p3p/6P1/PN5P/1PPPPP2/RK1RQBBN w DAda - +perft 1 23 +perft 2 611 +perft 3 15515 +perft 4 435927 +perft 5 11917036 +perft 6 352885930 + +id 846 +epd rknrqn1b/p1pp1ppb/8/1p2p1Qp/3P4/3N4/PPP1PPPP/RK1R1NBB w DAda - +perft 1 45 +perft 2 1170 +perft 3 48283 +perft 4 1320341 +perft 5 52213677 +perft 6 1500007485 + +id 847 +epd bbkrnrnq/p2p1ppp/2p1p3/1p6/1P2Q3/6P1/P1PPPP1P/BBKRNRN1 w - - +perft 1 41 +perft 2 1035 +perft 3 39895 +perft 4 1035610 +perft 5 38555608 +perft 6 1037686769 + +id 848 +epd brkbnr2/1ppppp1p/7n/p5N1/P2q4/8/1PPPPPPP/BRKBNRQ1 w FBfb - +perft 1 22 +perft 2 869 +perft 3 19234 +perft 4 679754 +perft 5 16453359 +perft 6 567287944 + +id 849 +epd brknrbnq/p1ppppp1/1p6/7p/2PP4/5P2/PPK1P1PP/BR1NRBNQ w eb - +perft 1 23 +perft 2 641 +perft 3 14748 +perft 4 422240 +perft 5 10192718 +perft 6 302864305 + +id 850 +epd brk1r1qb/pp1ppnpp/2p2pn1/8/6N1/2N3P1/PPPPPP1P/BRK1R1QB w EBeb - +perft 1 32 +perft 2 863 +perft 3 28379 +perft 4 773191 +perft 5 25848794 +perft 6 720443112 + +id 851 +epd rbbk1rnq/pppp1pp1/4p2p/8/3P2n1/4BN1P/PPP1PPP1/RB1K1RNQ w FAfa - +perft 1 26 +perft 2 628 +perft 3 16151 +perft 4 411995 +perft 5 11237919 +perft 6 300314373 + +id 852 +epd rkbbnr1q/p1pppppp/5n2/1p5B/PP6/4P3/2PP1PPP/RKB1NRNQ w FAfa - +perft 1 30 +perft 2 692 +perft 3 21036 +perft 4 519283 +perft 5 16025428 +perft 6 420887328 + +id 853 +epd rkb1rbnq/1pppp1pp/5p2/p7/5n1P/1PN3P1/P1PPPP2/RKB1RBNQ w EAea - +perft 1 32 +perft 2 825 +perft 3 27130 +perft 4 697251 +perft 5 23593363 +perft 6 622249676 + +id 854 +epd rkbnrnqb/1ppp1p1p/p5p1/4p3/4P3/2N2P2/PPPP2PP/RKBR1NQB w Aea - +perft 1 24 +perft 2 487 +perft 3 13300 +perft 4 301989 +perft 5 8782713 +perft 6 215787079 + +id 855 +epd rbknbr1q/pppp2pp/4p3/5p1n/1P2P2N/8/P1PP1PPP/RBKNBR1Q w FAfa - +perft 1 23 +perft 2 571 +perft 3 13799 +perft 4 365272 +perft 5 9224232 +perft 6 257288920 + +id 856 +epd rknbb1nq/pppppr2/5pp1/7p/8/1N4P1/PPPPPP1P/RK1BBRNQ w FAa - +perft 1 26 +perft 2 548 +perft 3 15618 +perft 4 350173 +perft 5 10587626 +perft 6 253006082 + +id 857 +epd rknr1bnq/p2pp1pp/1p3p2/2p4b/6PP/2P2N2/PP1PPP2/RKNRBB1Q w DAda - +perft 1 25 +perft 2 502 +perft 3 13150 +perft 4 279098 +perft 5 7824941 +perft 6 175766730 + +id 858 +epd rknrb1qb/ppp1pppp/3p4/8/4P1nP/2P5/PPKP1PP1/R1NRBNQB w da - +perft 1 23 +perft 2 643 +perft 3 14849 +perft 4 426616 +perft 5 10507328 +perft 6 312096061 + +id 859 +epd rbk1rnbq/pppp1npp/4p3/5p2/4P1P1/7P/PPPP1P1N/RBKNR1BQ w EAea - +perft 1 24 +perft 2 591 +perft 3 15178 +perft 4 376988 +perft 5 10251465 +perft 6 263574861 + +id 860 +epd rknbrnb1/p1pppp1p/1p6/3N2p1/P3q1P1/8/1PPPPP1P/RKNBR1BQ w EAea - +perft 1 28 +perft 2 948 +perft 3 27343 +perft 4 864588 +perft 5 26241141 +perft 6 812343987 + +id 861 +epd rknrn1b1/ppppppqp/8/6p1/2P5/2P1BP2/PP2P1PP/RKNRNB1Q w DAda - +perft 1 31 +perft 2 807 +perft 3 24360 +perft 4 672973 +perft 5 20455205 +perft 6 588518645 + +id 862 +epd 1k1rnqbb/npppppp1/r7/p2B3p/5P2/1N4P1/PPPPP2P/RK1RNQB1 w DAd - +perft 1 40 +perft 2 1122 +perft 3 44297 +perft 4 1249989 +perft 5 48711073 +perft 6 1412437357 + +id 863 +epd bbqr1rkn/pp1ppppp/8/2p5/1P2P1n1/7N/P1PP1P1P/BBQRKR1N w FD - +perft 1 26 +perft 2 841 +perft 3 22986 +perft 4 746711 +perft 5 21328001 +perft 6 705170410 + +id 864 +epd bqkr1rnn/1ppp1ppp/p4b2/4p3/P7/3PP2N/1PP2PPP/BQRBKR1N w FC - +perft 1 24 +perft 2 500 +perft 3 12802 +perft 4 293824 +perft 5 7928916 +perft 6 197806842 + +id 865 +epd bqrkrbnn/1pp1ppp1/8/p6p/3p4/P3P2P/QPPP1PP1/B1RKRBNN w ECec - +perft 1 31 +perft 2 592 +perft 3 18585 +perft 4 396423 +perft 5 12607528 +perft 6 298629240 + +id 866 +epd bqkrrnnb/2p1pppp/p7/1P1p4/8/2R3P1/PP1PPP1P/BQ1KRNNB w E - +perft 1 42 +perft 2 1124 +perft 3 45187 +perft 4 1276664 +perft 5 50052573 +perft 6 1483524894 + +id 867 +epd qbbrkrn1/p1pppn1p/8/1p3Pp1/2P5/8/PP1PPP1P/QBBRKRNN w FDfd - +perft 1 21 +perft 2 577 +perft 3 13244 +perft 4 392131 +perft 5 9683808 +perft 6 300294295 + +id 868 +epd qrbbkrnn/pp1p2pp/4p3/5p2/2p2P1P/2P5/PP1PP1P1/QRBBKRNN w FBfb - +perft 1 21 +perft 2 571 +perft 3 12736 +perft 4 345681 +perft 5 8239872 +perft 6 228837930 + +id 869 +epd qrbkrbn1/1pp1pppp/p2p4/8/5PPn/2P5/PP1PP3/QRBKRBNN w EBeb - +perft 1 18 +perft 2 466 +perft 3 9443 +perft 4 257776 +perft 5 5679073 +perft 6 162883949 + +id 870 +epd qrb1rnnb/pp1p1ppp/2pk4/4p3/1P2P3/1R6/P1PP1PPP/Q1BKRNNB w E - +perft 1 37 +perft 2 760 +perft 3 26863 +perft 4 562201 +perft 5 19486022 +perft 6 421740856 + +id 871 +epd qbrkbrn1/p1pppp1p/6n1/1p4p1/1P6/5P2/P1PPPBPP/QBRK1RNN w FCfc - +perft 1 33 +perft 2 824 +perft 3 27385 +perft 4 750924 +perft 5 25176664 +perft 6 734656217 + +id 872 +epd qrkbbr2/2pppppp/5nn1/pp1Q4/P7/3P4/1PP1PPPP/1RKBBRNN w FBfb - +perft 1 42 +perft 2 1147 +perft 3 44012 +perft 4 1311247 +perft 5 48216013 +perft 6 1522548864 + +id 873 +epd qrkrbbnn/pp2pp2/2pp2pp/1B6/P7/4P3/1PPP1PPP/QRKRB1NN w DBdb - +perft 1 26 +perft 2 464 +perft 3 12653 +perft 4 242892 +perft 5 6928220 +perft 6 142507795 + +id 874 +epd qrkrbnnb/p1pp1pp1/1p5p/4p3/1P6/6PN/PKPPPP1P/QR1RBN1B w db - +perft 1 29 +perft 2 705 +perft 3 20000 +perft 4 529810 +perft 5 15055365 +perft 6 419552571 + +id 875 +epd qbrkr1bn/p1p1pp1p/1p1p2n1/6p1/3P1P2/4P3/PPP3PP/QBKRRNBN w ec - +perft 1 23 +perft 2 613 +perft 3 14835 +perft 4 426484 +perft 5 10747407 +perft 6 323905533 + +id 876 +epd qrk1rnb1/p1pp1ppp/1p2Bbn1/8/4P3/6P1/PPPP1P1P/QRK1RNBN w EBeb - +perft 1 28 +perft 2 927 +perft 3 24887 +perft 4 846839 +perft 5 23063284 +perft 6 807913585 + +id 877 +epd 1qkrnbbn/1rpppppp/pp6/5N2/P4P2/8/1PPPP1PP/QRKRNBB1 w DBd - +perft 1 30 +perft 2 542 +perft 3 16646 +perft 4 345172 +perft 5 10976745 +perft 6 251694423 + +id 878 +epd qrkr2bb/pppppppp/8/1n2n3/1N5P/1P6/P1PPPPP1/QRKR1NBB w DBdb - +perft 1 28 +perft 2 719 +perft 3 21048 +perft 4 562015 +perft 5 17351761 +perft 6 479400272 + +id 879 +epd bbrqkrnn/3ppppp/8/ppp5/6P1/4P2N/PPPPKP1P/BBRQ1R1N w fc - +perft 1 21 +perft 2 704 +perft 3 16119 +perft 4 546215 +perft 5 13676371 +perft 6 470796854 + +id 880 +epd brqbkrnn/1pp2p1p/3pp1p1/p5N1/8/1P6/P1PPPPPP/BRQBK1RN w Bfb - +perft 1 34 +perft 2 688 +perft 3 22827 +perft 4 505618 +perft 5 16639723 +perft 6 402140795 + +id 881 +epd br1krb1n/2qppppp/pp3n2/8/1P4P1/8/P1PPPP1P/1RQKRBNN w EBeb - +perft 1 24 +perft 2 945 +perft 3 23943 +perft 4 926427 +perft 5 25019636 +perft 6 959651619 + +id 882 +epd brqkr1nb/2ppp1pp/1p2np2/p7/2P1PN2/8/PP1P1PPP/BRQKRN1B w EBeb - +perft 1 28 +perft 2 675 +perft 3 19728 +perft 4 504128 +perft 5 15516491 +perft 6 417396563 + +id 883 +epd rbbqkrnn/3pppp1/p7/1pp4p/2P1P2P/8/PP1P1PP1/RBBQKRNN w FAfa - +perft 1 26 +perft 2 671 +perft 3 18164 +perft 4 496806 +perft 5 14072641 +perft 6 404960259 + +id 884 +epd rqbbkr1n/pp1p1p1p/4pn2/2p3p1/4P1P1/3P3P/PPP2P2/RQBBKRNN w FAfa - +perft 1 22 +perft 2 633 +perft 3 14629 +perft 4 441809 +perft 5 10776416 +perft 6 335689685 + +id 885 +epd rqbkrbnn/p1ppp3/1p3pp1/7p/3P4/P1P5/1PQ1PPPP/R1BKRBNN w EAea - +perft 1 32 +perft 2 607 +perft 3 20339 +perft 4 454319 +perft 5 15586203 +perft 6 383515709 + +id 886 +epd rqbkrnn1/pp2ppbp/3p4/2p3p1/2P5/1P3N1P/P2PPPP1/RQBKRN1B w EAea - +perft 1 29 +perft 2 943 +perft 3 28732 +perft 4 908740 +perft 5 28761841 +perft 6 907579129 + +id 887 +epd rbqkb1nn/1ppppr1p/p5p1/5p2/1P6/2P4P/P1KPPPP1/RBQ1BRNN w a - +perft 1 22 +perft 2 441 +perft 3 10403 +perft 4 231273 +perft 5 5784206 +perft 6 140934555 + +id 888 +epd rqkb1rnn/1pp1pp1p/p5p1/1b1p4/3P4/P5P1/RPP1PP1P/1QKBBRNN w Ffa - +perft 1 21 +perft 2 505 +perft 3 11592 +perft 4 290897 +perft 5 7147063 +perft 6 188559137 + +id 889 +epd rq1rbbnn/pkp1ppp1/3p3p/1p2N1P1/8/8/PPPPPP1P/RQKRBB1N w DA - +perft 1 27 +perft 2 608 +perft 3 16419 +perft 4 387751 +perft 5 10808908 +perft 6 268393274 + +id 890 +epd rqkrb2b/p2ppppp/2p3nn/1p6/5P2/PP1P4/2P1P1PP/RQKRBNNB w DAda - +perft 1 30 +perft 2 749 +perft 3 21563 +perft 4 581531 +perft 5 16916813 +perft 6 485406712 + +id 891 +epd rbqkr1bn/pp1ppp2/2p1n2p/6p1/8/4BPNP/PPPPP1P1/RBQKRN2 w EAea - +perft 1 23 +perft 2 600 +perft 3 15082 +perft 4 410057 +perft 5 11041820 +perft 6 314327867 + +id 892 +epd rqkbrnb1/2ppp1pp/pp3pn1/8/5P2/B2P4/PPP1P1PP/RQKBRN1N w EAea - +perft 1 22 +perft 2 569 +perft 3 13541 +perft 4 371471 +perft 5 9395816 +perft 6 269460607 + +id 893 +epd rqkrnbb1/p1p1pppp/1p4n1/3p4/7P/P3P3/1PPPBPP1/RQKRN1BN w DAda - +perft 1 27 +perft 2 579 +perft 3 15565 +perft 4 373079 +perft 5 10238486 +perft 6 266047417 + +id 894 +epd rqkrn1bb/p1ppp1pp/4n3/1p6/6p1/4N3/PPPPPPPP/RQKR2BB w DAda - +perft 1 20 +perft 2 462 +perft 3 10234 +perft 4 274162 +perft 5 6563859 +perft 6 193376359 + +id 895 +epd bbrkqr2/pppp1ppp/6nn/8/2P1p3/3PP2N/PP3PPP/BBRKQR1N w FCfc - +perft 1 28 +perft 2 724 +perft 3 21688 +perft 4 619064 +perft 5 19318355 +perft 6 593204629 + +id 896 +epd brk1qrnn/1pppbppp/4p3/8/1p6/P1P4P/3PPPP1/BRKBQRNN w FBfb - +perft 1 24 +perft 2 662 +perft 3 16920 +perft 4 468215 +perft 5 12610387 +perft 6 355969349 + +id 897 +epd 1r1qrbnn/p1pkpppp/1p1p4/8/3P1PP1/P4b2/1PP1P2P/BRKQRBNN w EB - +perft 1 22 +perft 2 696 +perft 3 17021 +perft 4 510247 +perft 5 13697382 +perft 6 401903030 + +id 898 +epd 1rkqrnnb/p1p1p1pp/1p1p4/3b1p1N/4P3/5N2/PPPP1PPP/BRKQR2B w EBeb - +perft 1 29 +perft 2 887 +perft 3 27035 +perft 4 816176 +perft 5 26051242 +perft 6 791718847 + +id 899 +epd rbbkq1rn/pppppppp/7n/8/P7/3P3P/1PPKPPP1/RBB1QRNN w a - +perft 1 22 +perft 2 417 +perft 3 9900 +perft 4 216855 +perft 5 5505063 +perft 6 134818483 + +id 900 +epd rkbbqr1n/1p1pppp1/2p2n2/p4NBp/8/3P4/PPP1PPPP/RK1BQRN1 w FAfa - +perft 1 37 +perft 2 832 +perft 3 30533 +perft 4 728154 +perft 5 26676373 +perft 6 673756141 + +id 901 +epd rkbqrb1n/3pBppp/ppp2n2/8/8/P2P4/1PP1PPPP/RK1QRBNN w EAea - +perft 1 28 +perft 2 685 +perft 3 19718 +perft 4 543069 +perft 5 16033316 +perft 6 482288814 + +id 902 +epd rkb1rn1b/ppppqppp/4p3/8/1P2n1P1/5Q2/P1PP1P1P/RKB1RNNB w EAea - +perft 1 37 +perft 2 1158 +perft 3 40114 +perft 4 1234768 +perft 5 44672979 +perft 6 1389312729 + +id 903 +epd r1kqbrnn/pp1pp1p1/7p/2P2p2/5b2/3P4/P1P1P1PP/RBKQBRNN w FAfa - +perft 1 5 +perft 2 161 +perft 3 4745 +perft 4 154885 +perft 5 4734999 +perft 6 157499039 + +id 904 +epd rkqbbr1n/ppp1ppp1/8/Q2p3p/4n3/3P1P2/PPP1P1PP/RK1BBRNN w FAfa - +perft 1 38 +perft 2 1144 +perft 3 40433 +perft 4 1236877 +perft 5 43832975 +perft 6 1366087771 + +id 905 +epd rkqrbbn1/p1ppppp1/Bp5p/8/P6n/2P1P3/1P1P1PPP/RKQRB1NN w DAda - +perft 1 28 +perft 2 551 +perft 3 15488 +perft 4 350861 +perft 5 9944107 +perft 6 251179183 + +id 906 +epd rkqrb1nb/1ppp1ppp/p7/4p3/5n2/3P2N1/PPPQPPPP/RK1RB1NB w DAda - +perft 1 26 +perft 2 690 +perft 3 19877 +perft 4 513628 +perft 5 15965907 +perft 6 418191735 + +id 907 +epd rbkqrnbn/pppp1p2/4p1p1/7p/7P/P2P4/BPP1PPP1/R1KQRNBN w EAea - +perft 1 27 +perft 2 515 +perft 3 13992 +perft 4 309727 +perft 5 8792550 +perft 6 218658292 + +id 908 +epd rkqbrnbn/pp1ppp2/8/2p3p1/P1P4p/5P2/1PKPP1PP/R1QBRNBN w ea - +perft 1 27 +perft 2 627 +perft 3 16843 +perft 4 431101 +perft 5 11978698 +perft 6 328434174 + +id 909 +epd rkqrnbbn/1p2pp1p/3p2p1/p1p5/P5PP/3N4/1PPPPP2/RKQR1BBN w DAda - +perft 1 23 +perft 2 624 +perft 3 15512 +perft 4 451860 +perft 5 11960861 +perft 6 367311176 + +id 910 +epd rk2rnbb/ppqppppp/2pn4/8/1P3P2/6P1/P1PPP1NP/RKQR1NBB w DAa - +perft 1 27 +perft 2 727 +perft 3 20206 +perft 4 581003 +perft 5 16633696 +perft 6 505212747 + +id 911 +epd b1krrqnn/pp1ppp1p/2p3p1/8/P3Pb1P/1P6/2PP1PP1/BBRKRQNN w EC - +perft 1 32 +perft 2 943 +perft 3 30759 +perft 4 865229 +perft 5 28672582 +perft 6 800922511 + +id 912 +epd 1rkbrqnn/p1pp1ppp/1p6/8/P2Pp3/8/1PPKPPQP/BR1BR1NN w eb - +perft 1 28 +perft 2 916 +perft 3 24892 +perft 4 817624 +perft 5 22840279 +perft 6 759318058 + +id 913 +epd brkrqb1n/1pppp1pp/p7/3n1p2/P5P1/3PP3/1PP2P1P/BRKRQBNN w DBdb - +perft 1 27 +perft 2 669 +perft 3 18682 +perft 4 484259 +perft 5 13956472 +perft 6 380267099 + +id 914 +epd brkrqnnb/3pppp1/1p6/p1p4p/2P3P1/6N1/PP1PPP1P/BRKRQ1NB w DBdb - +perft 1 29 +perft 2 699 +perft 3 20042 +perft 4 512639 +perft 5 15093909 +perft 6 406594531 + +id 915 +epd r1bkrq1n/pp2pppp/3b1n2/2pp2B1/6P1/3P1P2/PPP1P2P/RB1KRQNN w EAea - +perft 1 27 +perft 2 835 +perft 3 22848 +perft 4 713550 +perft 5 19867800 +perft 6 631209313 + +id 916 +epd rk1brq1n/p1p1pppp/3p1n2/1p3b2/4P3/2NQ4/PPPP1PPP/RKBBR2N w EAea - +perft 1 36 +perft 2 1004 +perft 3 35774 +perft 4 979608 +perft 5 35143142 +perft 6 966310885 + +id 917 +epd rkbrqbnn/1p2ppp1/B1p5/p2p3p/4P2P/8/PPPP1PP1/RKBRQ1NN w DAda - +perft 1 27 +perft 2 748 +perft 3 21005 +perft 4 597819 +perft 5 17597073 +perft 6 515304215 + +id 918 +epd rkbrqn1b/pp1pp1pp/2p2p2/5n2/8/2P2P2/PP1PP1PP/RKBRQ1NB w DAda - +perft 1 20 +perft 2 479 +perft 3 10485 +perft 4 266446 +perft 5 6253775 +perft 6 167767913 + +id 919 +epd rbkrbnn1/ppppp1pp/5q2/5p2/5P2/P3P2N/1PPP2PP/RBKRBQ1N w DAda - +perft 1 28 +perft 2 947 +perft 3 26900 +perft 4 876068 +perft 5 26007841 +perft 6 838704143 + +id 920 +epd rkr1bqnn/1ppp1p1p/p5p1/4p3/3PP2b/2P2P2/PP4PP/RKRBBQNN w CAca - +perft 1 31 +perft 2 1004 +perft 3 32006 +perft 4 1006830 +perft 5 32688124 +perft 6 1024529879 + +id 921 +epd rkrqbbnn/pppp3p/8/4ppp1/1PP4P/8/P2PPPP1/RKRQBBNN w CAca - +perft 1 24 +perft 2 717 +perft 3 18834 +perft 4 564137 +perft 5 15844525 +perft 6 484884485 + +id 922 +epd rkrqbn1b/pppp2pp/8/4pp2/1P1P2n1/5N2/P1P1PP1P/RKRQBN1B w CAca - +perft 1 25 +perft 2 718 +perft 3 19654 +perft 4 587666 +perft 5 17257753 +perft 6 537354146 + +id 923 +epd rbkrqnbn/p1p1ppp1/1p1p4/8/3PP2p/2PB4/PP3PPP/R1KRQNBN w DAda - +perft 1 30 +perft 2 754 +perft 3 23298 +perft 4 611322 +perft 5 19338246 +perft 6 532603566 + +id 924 +epd 1krbqnbn/1p2pppp/r1pp4/p7/8/1P1P2PP/P1P1PP2/RKRBQNBN w CAc - +perft 1 21 +perft 2 566 +perft 3 13519 +perft 4 375128 +perft 5 9700847 +perft 6 279864836 + +id 925 +epd rkrq1b2/pppppppb/3n2np/2N5/4P3/7P/PPPP1PP1/RKRQ1BBN w CAca - +perft 1 33 +perft 2 654 +perft 3 21708 +perft 4 479678 +perft 5 15990307 +perft 6 382218272 + +id 926 +epd rkr1nnbb/ppp2p1p/3p1qp1/4p3/P5P1/3PN3/1PP1PP1P/RKRQN1BB w CAca - +perft 1 28 +perft 2 715 +perft 3 20361 +perft 4 555328 +perft 5 16303092 +perft 6 468666425 + +id 927 +epd bbrkrnqn/1p1ppppp/8/8/p2pP3/PP6/2P2PPP/BBRKRNQN w ECec - +perft 1 24 +perft 2 757 +perft 3 19067 +perft 4 603231 +perft 5 15957628 +perft 6 509307623 + +id 928 +epd brkbrnqn/ppp2p2/4p3/P2p2pp/6P1/5P2/1PPPP2P/BRKBRNQN w EBeb - +perft 1 25 +perft 2 548 +perft 3 14563 +perft 4 348259 +perft 5 9688526 +perft 6 247750144 + +id 929 +epd brkr1bqn/1pppppp1/3n3p/1p6/P7/4P1P1/1PPP1P1P/BRKRN1QN w DBdb - +perft 1 19 +perft 2 359 +perft 3 7430 +perft 4 157099 +perft 5 3521652 +perft 6 81787718 + +id 930 +epd brkr1qnb/pppp2pp/2B1p3/5p2/2n5/6PP/PPPPPPN1/BRKR1QN1 w DBdb - +perft 1 27 +perft 2 854 +perft 3 23303 +perft 4 741626 +perft 5 20558538 +perft 6 667089231 + +id 931 +epd rbbkrnqn/p1p1p1pp/8/1p1p4/1P1Pp3/6N1/P1P2PPP/RBBKRNQ1 w EAea - +perft 1 28 +perft 2 723 +perft 3 19844 +perft 4 514440 +perft 5 14621108 +perft 6 397454100 + +id 932 +epd rkbbrn1n/pppppp2/5q1p/6p1/3P3P/4P3/PPP2PP1/RKBBRNQN w EAea - +perft 1 25 +perft 2 741 +perft 3 19224 +perft 4 585198 +perft 5 15605840 +perft 6 485037906 + +id 933 +epd rkbr1bq1/ppnppppp/6n1/2p5/2P1N2P/8/PP1PPPP1/RKBRNBQ1 w DAda - +perft 1 24 +perft 2 547 +perft 3 14359 +perft 4 339497 +perft 5 9410221 +perft 6 234041078 + +id 934 +epd 1kbrnqnb/r1ppppp1/8/pp5p/8/1P1NP3/P1PP1PPP/RKB1RQNB w Ad - +perft 1 26 +perft 2 618 +perft 3 17305 +perft 4 442643 +perft 5 13112297 +perft 6 357030697 + +id 935 +epd rbkrb1qn/1pp1ppp1/3pn2p/pP6/8/4N1P1/P1PPPP1P/RBKRB1QN w DAda - +perft 1 21 +perft 2 544 +perft 3 12492 +perft 4 338832 +perft 5 8381483 +perft 6 236013157 + +id 936 +epd rkrbbnqn/ppppp3/5p2/6pp/5PBP/4P3/PPPP2P1/RKR1BNQN w CAca - +perft 1 30 +perft 2 891 +perft 3 25435 +perft 4 764356 +perft 5 21894752 +perft 6 669256602 + +id 937 +epd rkr1bb1n/ppppp1pp/5p2/4n3/3QP3/5P2/RPPP2PP/1KRNBB1N w Cca - +perft 1 45 +perft 2 1172 +perft 3 51766 +perft 4 1332060 +perft 5 57856784 +perft 6 1501852662 + +id 938 +epd rkr1bqnb/pp1ppppp/8/2pN4/1P6/5N2/P1PPnPPP/RKR1BQ1B w CAca - +perft 1 28 +perft 2 730 +perft 3 20511 +perft 4 559167 +perft 5 16323242 +perft 6 463032124 + +id 939 +epd rbkrnqb1/2ppppp1/p5np/1p6/8/3N4/PPPPPPPP/RBKRQNB1 w DAda - +perft 1 20 +perft 2 417 +perft 3 9159 +perft 4 217390 +perft 5 5180716 +perft 6 133936564 + +id 940 +epd rkrbnqb1/p1pppnpp/5p2/1p6/2P5/1P1P1N2/P3PPPP/RKRB1QBN w CAca - +perft 1 25 +perft 2 546 +perft 3 14039 +perft 4 330316 +perft 5 8813781 +perft 6 222026485 + +id 941 +epd rkr1qbbn/ppppppp1/4n3/7p/8/P7/KPPPPPPP/R1RNQBBN w ca - +perft 1 22 +perft 2 484 +perft 3 11458 +perft 4 267495 +perft 5 6633319 +perft 6 163291279 + +id 942 +epd rkrnqnb1/1ppppp2/p5p1/7p/8/P1bPP3/1PP1QPPP/RKRN1NBB w CAca - +perft 1 22 +perft 2 636 +perft 3 15526 +perft 4 441001 +perft 5 11614241 +perft 6 331083405 + +id 943 +epd b2krn1q/p1rppppp/1Q3n2/2p1b3/1P4P1/8/P1PPPP1P/BBRKRNN1 w ECe - +perft 1 36 +perft 2 1192 +perft 3 42945 +perft 4 1406795 +perft 5 50382104 +perft 6 1650202838 + +id 944 +epd brkbrnn1/pp1pppp1/7q/2p5/6Pp/4P1NP/PPPP1P2/BRKBR1NQ w EBeb - +perft 1 30 +perft 2 978 +perft 3 29593 +perft 4 942398 +perft 5 29205057 +perft 6 936568065 + +id 945 +epd brkrnb1q/pp1p1ppp/2p1p3/5n2/1P6/5N1N/P1PPPPPP/BRKR1B1Q w DBdb - +perft 1 31 +perft 2 897 +perft 3 27830 +perft 4 810187 +perft 5 25423729 +perft 6 755334868 + +id 946 +epd brkr1nqb/pp1p1pp1/2pn3p/P3p3/4P3/6P1/1PPP1P1P/BRKRNNQB w DBdb - +perft 1 19 +perft 2 382 +perft 3 8052 +perft 4 182292 +perft 5 4232274 +perft 6 103537333 + +id 947 +epd r1bkrn1q/ppbppppp/5n2/2p5/3P4/P6N/1PP1PPPP/RBBKRNQ1 w EAea - +perft 1 27 +perft 2 822 +perft 3 22551 +perft 4 678880 +perft 5 19115128 +perft 6 578210135 + +id 948 +epd rkbbrnnq/pp2pppp/8/2pp4/P1P5/1P3P2/3PP1PP/RKBBRNNQ w EAea - +perft 1 23 +perft 2 643 +perft 3 15410 +perft 4 442070 +perft 5 11170489 +perft 6 329615708 + +id 949 +epd rkbr1b1q/p1pppppp/1p1n4/7n/5QP1/3N4/PPPPPP1P/RKBR1BN1 w DAda - +perft 1 37 +perft 2 943 +perft 3 34382 +perft 4 880474 +perft 5 31568111 +perft 6 842265141 + +id 950 +epd rkbr1nqb/pppp2np/8/4ppp1/1P6/6N1/P1PPPPPP/RKBRN1QB w DAda - +perft 1 23 +perft 2 574 +perft 3 13260 +perft 4 362306 +perft 5 9020291 +perft 6 261247606 + +id 951 +epd rbkr1nnq/p1p1pp1p/1p4p1/3p4/b3P3/4N3/PPPPNPPP/RBKRB1Q1 w DAda - +perft 1 26 +perft 2 900 +perft 3 23414 +perft 4 805006 +perft 5 21653203 +perft 6 745802405 + +id 952 +epd rkrbb1nq/p2pppp1/1p4n1/2p4p/3N4/4P1P1/PPPP1P1P/RKRBBN1Q w CAca - +perft 1 32 +perft 2 697 +perft 3 22231 +perft 4 531121 +perft 5 17150175 +perft 6 441578567 + +id 953 +epd rkrnbb1q/pp2pp1p/6pn/2pp4/2B1P2P/8/PPPP1PP1/RKRNB1NQ w CAca - +perft 1 28 +perft 2 854 +perft 3 23853 +perft 4 755990 +perft 5 21823412 +perft 6 712787248 + +id 954 +epd rk2bnqb/pprpppp1/4n2p/2p5/P7/3P2NP/1PP1PPP1/RKRNB1QB w CAa - +perft 1 26 +perft 2 596 +perft 3 16251 +perft 4 414862 +perft 5 11758184 +perft 6 323043654 + +id 955 +epd r1krnnbq/pp1ppp1p/6p1/2p5/2P5/P3P3/Rb1P1PPP/1BKRNNBQ w Dda - +perft 1 2 +perft 2 61 +perft 3 1312 +perft 4 40072 +perft 5 937188 +perft 6 28753562 + +id 956 +epd 1krbnnbq/1pp1p1pp/r7/p2p1p2/3PP3/2P3P1/PP3P1P/RKRBNNBQ w CAc - +perft 1 30 +perft 2 953 +perft 3 28033 +perft 4 860530 +perft 5 25531358 +perft 6 787205262 + +id 957 +epd rkr1nbbq/2ppp1pp/1pn5/p4p2/P6P/3P4/1PP1PPPB/RKRNNB1Q w CAca - +perft 1 24 +perft 2 645 +perft 3 15689 +perft 4 446423 +perft 5 11484012 +perft 6 341262639 + +id 958 +epd rkrnnqbb/p1ppp2p/Qp6/4Pp2/5p2/8/PPPP2PP/RKRNN1BB w CAca - +perft 1 35 +perft 2 929 +perft 3 32020 +perft 4 896130 +perft 5 31272517 +perft 6 915268405 + +id 959 +epd bbq1nr1r/pppppk1p/2n2p2/6p1/P4P2/4P1P1/1PPP3P/BBQNNRKR w HF - +perft 1 23 +perft 2 589 +perft 3 14744 +perft 4 387556 +perft 5 10316716 +perft 6 280056112 diff --git a/examples/perft/crazyhouse.perft b/examples/perft/crazyhouse.perft index e693ff5df..ef07eac04 100644 --- a/examples/perft/crazyhouse.perft +++ b/examples/perft/crazyhouse.perft @@ -16,3 +16,17 @@ perft 1 42 perft 2 1347 perft 3 58057 perft 4 2083382 + +id zh-promoted +epd 4k3/1Q~6/8/8/4b3/8/Kpp5/8/ b - - +perft 1 20 +perft 2 360 +perft 3 5445 +perft 4 132758 + +id zh-midgame +epd 2rn1b1r/1pp2n1p/B1PBk1pP/5p1P/P5p1/2P2RP1/RP1QNP2/1NBK4[QPp] w - - +perft 1 99 +perft 2 3932 +perft 3 314782 +perft 4 10118606 diff --git a/examples/perft/giveaway.perft b/examples/perft/giveaway.perft index fdfaff51c..a54b31554 100644 --- a/examples/perft/giveaway.perft +++ b/examples/perft/giveaway.perft @@ -1,6 +1,31 @@ -id giveaway-start +id antichess-start epd rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - perft 1 20 perft 2 400 perft 3 8067 perft 4 153299 + +id a-pawn-vs-b-pawn +epd 8/1p6/8/8/8/8/P7/8 w - - +perft 1 2 +perft 2 4 +perft 3 4 +perft 4 3 +perft 5 1 +perft 6 0 + +id a-pawn-vs-c-pawn +epd 8/2p5/8/8/8/8/P7/8 w - - +perft 1 2 +perft 2 4 +perft 3 4 +perft 4 4 +perft 5 4 +perft 6 4 +perft 7 4 +perft 8 4 +perft 9 12 +perft 10 36 +perft 11 312 +perft 12 2557 +perft 13 30873 diff --git a/examples/perft/horde.perft b/examples/perft/horde.perft index 1bd26fb97..87bffeda3 100644 --- a/examples/perft/horde.perft +++ b/examples/perft/horde.perft @@ -4,3 +4,17 @@ perft 1 8 perft 2 128 perft 3 1274 perft 4 23310 + +id horde-open-flank +epd 4k3/pp4q1/3P2p1/8/P3PP2/PPP2r2/PPP5/PPPP4 b - - +perft 1 30 +perft 2 241 +perft 3 6633 +perft 4 56539 + +id horde-en-passant +epd k7/5p2/4p2P/3p2P1/2p2P2/1p2P2P/p2P2P1/2P2P2 w - - +perft 1 13 +perft 2 172 +perft 3 2205 +perft 4 33781 diff --git a/examples/perft/perft.py b/examples/perft/perft.py index 6a703814a..2c70cd2ed 100755 --- a/examples/perft/perft.py +++ b/examples/perft/perft.py @@ -1,25 +1,25 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- +#!/usr/bin/env python3 """ Run perft test to check correctness and speed of the legal move generator. """ -from __future__ import division -from __future__ import print_function - -import chess -import chess.variant import multiprocessing +import multiprocessing.pool import functools import time import argparse import sys +from typing import Callable, Iterator, Optional, TextIO, Type -def perft(depth, board): - if depth < 1: - return 1 +import chess +import chess.variant + + +def perft(depth: int, board: chess.Board) -> int: + if depth == 1: + return board.legal_moves.count() elif depth > 1: count = 0 @@ -29,42 +29,38 @@ def perft(depth, board): board.pop() return count - elif depth == 1: - return len(board.legal_moves) else: return 1 -def parallel_perft(pool, depth, board): - if depth < 1: - return 1 +def parallel_perft(pool: multiprocessing.pool.Pool, depth: int, board: chess.Board) -> int: + if depth == 1: + return board.legal_moves.count() elif depth > 1: - def successors(board): + def successors(board: chess.Board) -> Iterator[chess.Board]: for move in board.legal_moves: board_after = board.copy(stack=False) board_after.push(move) yield board_after return sum(pool.imap_unordered(functools.partial(perft, depth - 1), successors(board))) - elif depth == 1: - return len(board.legal_moves) else: return 1 -def sdiv(a, b): +def sdiv(a: float, b: float) -> float: try: return a / b except ZeroDivisionError: return float("Inf") -def main(perft_file, VariantBoard, perft_f, max_depth, max_nodes): +def main(perft_file: TextIO, VariantBoard: Type[chess.Board], perft_f: Callable[[int, chess.Board], int], max_depth: Optional[int], max_nodes: Optional[int]) -> None: current_id = None board = VariantBoard(chess960=True) column = 0 total_nodes = 0 - start_time = time.time() + start_time = time.perf_counter() for line in perft_file: # Skip comments and empty lines. @@ -86,33 +82,32 @@ def main(perft_file, VariantBoard, perft_f, max_depth, max_nodes): if nodes != perft_nodes: print() print() - print(" !!! Failure in", current_id or "") - print(" epd", board.epd()) - print(" perft", depth, nodes, "(got %d instead)" % perft_nodes) + print(f" !!! Failure in {current_id or ''}") + print(f" epd {board.epd()}") + print(f" perft {depth} {nodes} (got {perft_nodes} instead)") print() print(board) print() for move in sorted(board.legal_moves, key=lambda m: m.uci()): board.push(move) - print("%s: %d" % (move, perft_f(depth - 1, board))) + print(f"{move}: {perft_f(depth - 1, board)}") board.pop() sys.exit(1) total_nodes += perft_nodes - sys.stdout.write(".") - sys.stdout.flush() + print(".", end="", flush=True) column += 1 if column >= 40: column = 0 - sys.stdout.write(" nodes %d nps %.0f\n" % (total_nodes, sdiv(total_nodes, time.time() - start_time))) + sys.stdout.write(f" nodes {total_nodes} nps {sdiv(total_nodes, time.perf_counter() - start_time):.0f}\n") else: print() print("Unknown command:", cmd, arg) sys.exit(2) if column: - sys.stdout.write(" nodes %d nps %.0f\n" % (total_nodes, sdiv(total_nodes, time.time() - start_time))) + sys.stdout.write(f" nodes {total_nodes} nps {sdiv(total_nodes, time.perf_counter() - start_time):.0f}\n") if __name__ == "__main__": diff --git a/examples/perft/racingkings.perft b/examples/perft/racingkings.perft index aa7cb6d11..7b293ebb8 100644 --- a/examples/perft/racingkings.perft +++ b/examples/perft/racingkings.perft @@ -4,3 +4,12 @@ perft 1 21 perft 2 421 perft 3 11264 perft 4 296242 + +id occupied-goal +epd 4brn1/2K2k2/8/8/8/8/8/8 w - - +perft 1 6 +perft 2 33 +perft 3 178 +perft 4 3151 +perft 5 12981 +perft 6 265932 diff --git a/examples/perft/tricky.perft b/examples/perft/tricky.perft index 9254e2e8a..2510a9aea 100644 --- a/examples/perft/tricky.perft +++ b/examples/perft/tricky.perft @@ -77,3 +77,73 @@ perft 1 19 perft 2 628 perft 3 12858 perft 4 405636 + +# +# Source: http://talkchess.com/forum3/viewtopic.php?f=7&t=71379 +# + +id gotta-love-perft-1 +epd 8/ppp3p1/8/8/3p4/5Q2/1ppp2K1/brk4n w - - +perft 1 27 +perft 2 390 +perft 3 9354 +perft 4 134167 + +id gotta-love-perft-2 +epd 8/6kR/8/8/8/bq6/1rqqqqqq/K1nqnbrq b - - +perft 1 7 +perft 2 52 +perft 3 4593 +perft 4 50268 + +# +# Impossible checker alignment. +# + +id align-diag-1 +epd 3R4/8/q4k2/2B5/1NK5/3b4/8/8 w - - +perft 1 4 +perft 2 125 +perft 3 2854 + +id align-diag-2 +epd 2Nq4/2K5/1b6/8/7R/3k4/7P/8 w - - +perft 1 3 +perft 2 81 +perft 3 1217 + +id align-horizontal +epd 5R2/2P5/8/4k3/8/3rK2r/8/8 w - - +perft 1 2 +perft 2 56 +perft 3 1030 + +id align-ep +epd 8/8/8/1k6/3Pp3/8/8/4KQ2 b - d3 +perft 1 6 +perft 2 121 +perft 3 711 + +id align-ep-pinned +epd 1b1k4/8/8/1rPpK3/8/8/8/8 w - d6 +perft 1 5 +perft 2 100 +perft 3 555 + +id ep-unrelated-check +epd rnbqk1nr/bb3p1p/1q2r3/2pPp3/3P4/7P/1PP1NpPP/R1BQKBNR w KQkq c6 +perft 1 2 +perft 2 92 +perft 3 2528 + +# +# Impossible castling rights +# + +id asymmetrical-and-king-on-h +epd r2r3k/p7/3p4/8/8/P6P/8/R3K2R b KQq - +perft 1 14 +perft 2 206 +perft 3 3672 +perft 4 64639 +perft 5 1320962 diff --git a/examples/polyglot_tree.py b/examples/polyglot_tree.py new file mode 100755 index 000000000..fc4504861 --- /dev/null +++ b/examples/polyglot_tree.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 + +"""Print a Polyglot opening book in tree form.""" + +import argparse + +from typing import Set + +import chess +import chess.polyglot + + +def print_tree(args: argparse.Namespace, visited: Set[int], level: int = 0) -> None: + if level >= args.depth: + return + + zobrist_hash = chess.polyglot.zobrist_hash(args.board) + if zobrist_hash in visited: + return + + visited.add(zobrist_hash) + + for entry in args.book.find_all(zobrist_hash): + print("{}├─ \033[1m{}\033[0m (weight: {}, learn: {})".format( + "| " * level, + args.board.san(entry.move), + entry.weight, + entry.learn)) + + args.board.push(entry.move) + print_tree(args, visited, level + 1) + args.board.pop() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("book", type=chess.polyglot.open_reader) + parser.add_argument("--depth", type=int, default=5) + parser.add_argument("--fen", type=chess.Board, default=chess.Board(), dest="board") + args = parser.parse_args() + print_tree(args, visited=set()) diff --git a/examples/push_san.py b/examples/push_san.py index 484a41d5b..6c6e2a091 100755 --- a/examples/push_san.py +++ b/examples/push_san.py @@ -1,106 +1,103 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- +#!/usr/bin/env python3 -"""Play the immortal game using Board.push_san().""" - -from __future__ import print_function +"""Play the immortal game using push_san() from chess.Board().""" import chess import timeit -def play_immortal_game(): +def play_immortal_game() -> None: board = chess.Board() - # 1.e4 e5 + # 1. e4 e5 board.push_san("e4") board.push_san("e5") - # 2.f4 exf4 + # 2. f4 exf4 board.push_san("f4") board.push_san("exf4") - # 3.Bc4 Qh4+ + # 3. Bc4 Qh4+ board.push_san("Bc4") board.push_san("Qh4+") - # 4.Kf1 b5?! + # 4. Kf1 b5?! board.push_san("Kf1") board.push_san("b5") - # 5.Bxb5 Nf6 + # 5. Bxb5 Nf6 board.push_san("Bxb5") board.push_san("Nf6") - # 6.Nf3 Qh6 + # 6. Nf3 Qh6 board.push_san("Nf3") board.push_san("Qh6") - # 7.d3 Nh5 + # 7. d3 Nh5 board.push_san("d3") board.push_san("Nh5") - # 8.Nh4 Qg5 + # 8. Nh4 Qg5 board.push_san("Nh4") board.push_san("Qg5") - # 9.Nf5 c6 + # 9. Nf5 c6 board.push_san("Nf5") board.push_san("c6") - # 10.g4 Nf6 + # 10. g4 Nf6 board.push_san("g4") board.push_san("Nf6") - # 11.Rg1! cxb5? + # 11. Rg1! cxb5? board.push_san("Rg1") board.push_san("cxb5") - # 12.h4! Qg6 + # 12. h4! Qg6 board.push_san("h4") board.push_san("Qg6") - # 13.h5 Qg5 + # 13. h5 Qg5 board.push_san("h5") board.push_san("Qg5") - # 14.Qf3 Ng8 + # 14. Qf3 Ng8 board.push_san("Qf3") board.push_san("Ng8") - # 15.Bxf4 Qf6 + # 15. Bxf4 Qf6 board.push_san("Bxf4") board.push_san("Qf6") - # 16.Nc3 Bc5 + # 16. Nc3 Bc5 board.push_san("Nc3") board.push_san("Bc5") - # 17.Nd5 Qxb2 + # 17. Nd5 Qxb2 board.push_san("Nd5") board.push_san("Qxb2") - # 18.Bd6! Bxg1? + # 18. Bd6! Bxg1? board.push_san("Bd6") board.push_san("Bxg1") - # 19.e5! Qxa1+ + # 19. e5! Qxa1+ board.push_san("e5") board.push_san("Qxa1+") - # 20.Ke2 Na6 + # 20. Ke2 Na6 board.push_san("Ke2") board.push_san("Na6") - # 21.Nxg7+ Kd8 + # 21. Nxg7+ Kd8 board.push_san("Nxg7+") board.push_san("Kd8") - # 22.Qf6+! Nxf6 + # 22. Qf6+! Nxf6 board.push_san("Qf6+") board.push_san("Nxf6") - # 23.Be7# 1-0 + # 23. Be7# 1-0 board.push_san("Be7#") assert board.is_checkmate() diff --git a/examples/xray_attacks.py b/examples/xray_attacks.py new file mode 100755 index 000000000..9935e0779 --- /dev/null +++ b/examples/xray_attacks.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +"""Compute X-ray attacks through more valuable pieces.""" + +import chess + + +def xray_rook_attackers(board: chess.Board, color: chess.Color, square: chess.Square) -> chess.SquareSet: + occupied = board.occupied + rank_pieces = chess.BB_RANK_MASKS[square] & occupied + file_pieces = chess.BB_FILE_MASKS[square] & occupied + + # Find the closest piece for each direction. These may block attacks. + blockers = chess.BB_RANK_ATTACKS[square][rank_pieces] | chess.BB_FILE_ATTACKS[square][file_pieces] + + # Only consider blocking pieces of the victim that are more valuable + # than rooks. + blockers &= board.occupied_co[not color] & (board.queens | board.kings) + + # Now just ignore those blocking pieces. + occupied ^= blockers + + # And compute rook attacks. + rank_pieces = chess.BB_RANK_MASKS[square] & occupied + file_pieces = chess.BB_FILE_MASKS[square] & occupied + return chess.SquareSet(board.occupied_co[color] & board.rooks & ( + chess.BB_RANK_ATTACKS[square][rank_pieces] | + chess.BB_FILE_ATTACKS[square][file_pieces])) + + +def xray_bishop_attackers(board: chess.Board, color: chess.Color, square: chess.Square) -> chess.SquareSet: + occupied = board.occupied + diag_pieces = chess.BB_DIAG_MASKS[square] & occupied + + # Find the closest piece for each direction. These may block attacks. + blockers = chess.BB_DIAG_ATTACKS[square][diag_pieces] + + # Only consider blocking pieces of the victim that are more valuable + # than bishops. + blockers &= board.occupied_co[not color] & (board.rooks | board.queens | board.kings) + + # Now just ignore those blocking pieces. + occupied ^= blockers + + # And compute bishop attacks. + diag_pieces = chess.BB_DIAG_MASKS[square] & occupied + return chess.SquareSet(board.occupied_co[color] & board.bishops & chess.BB_DIAG_ATTACKS[square][diag_pieces]) + + +def example() -> None: + board = chess.Board("r3k2r/pp3p2/4p2Q/4q1p1/4P3/P2PK3/6PP/R3R3 w q - 1 2") + print("rook x-ray, black, h3:") + print(xray_rook_attackers(board, chess.BLACK, chess.H3)) + + board = chess.Board("r1b1r1k1/pp1n1pbp/1qp3p1/B2p4/3P4/Q3PN2/PP2BPPP/R4RK1 b - - 0 1") + print("bishop x-ray, white, d8:") + print(xray_bishop_attackers(board, chess.WHITE, chess.D8)) + + +if __name__ == "__main__": + example() diff --git a/fuzz/corpus/engine/stockfish-10 b/fuzz/corpus/engine/stockfish-10 new file mode 100644 index 000000000..cb3dac248 --- /dev/null +++ b/fuzz/corpus/engine/stockfish-10 @@ -0,0 +1,33 @@ +Stockfish 10 64 POPCNT by T. Romstad, M. Costalba, J. Kiiski, G. Linscott +id name Stockfish 10 64 POPCNT +id author T. Romstad, M. Costalba, J. Kiiski, G. Linscott + +option name Debug Log File type string default +option name Contempt type spin default 24 min -100 max 100 +option name Analysis Contempt type combo default Both var Off var White var Black var Both +option name Threads type spin default 1 min 1 max 512 +option name Hash type spin default 16 min 1 max 131072 +option name Clear Hash type button +option name Ponder type check default false +option name MultiPV type spin default 1 min 1 max 500 +option name Skill Level type spin default 20 min 0 max 20 +option name Move Overhead type spin default 30 min 0 max 5000 +option name Minimum Thinking Time type spin default 20 min 0 max 5000 +option name Slow Mover type spin default 84 min 10 max 1000 +option name nodestime type spin default 0 min 0 max 10000 +option name UCI_Chess960 type check default false +option name UCI_AnalyseMode type check default false +option name SyzygyPath type string default +option name SyzygyProbeDepth type spin default 1 min 1 max 100 +option name Syzygy50MoveRule type check default true +option name SyzygyProbeLimit type spin default 7 min 0 max 7 +uciok +! +readyok +! +! +! +info depth 1 seldepth 1 multipv 1 score cp 116 nodes 20 nps 20000 tbhits 0 time 1 pv e2e4 +info depth 2 seldepth 1 multipv 1 score cp 116 nodes 21 nps 21000 tbhits 0 time 1 pv e2e4 +bestmove e2e4 +! diff --git a/fuzz/corpus/engine/stockfish-mv-11 b/fuzz/corpus/engine/stockfish-mv-11 new file mode 100644 index 000000000..b7aa4cfec --- /dev/null +++ b/fuzz/corpus/engine/stockfish-mv-11 @@ -0,0 +1,44 @@ +Stockfish 11 64 BMI2 Multi-Variant by D. Dugovic, F. Fichter et al. +id name Stockfish 11 64 BMI2 Multi-Variant +id author D. Dugovic, F. Fichter et al. + +option name Debug Log File type string default +option name Contempt type spin default 24 min -100 max 100 +option name Analysis Contempt type combo default Both var Both var Off var White var Black +option name Threads type spin default 1 min 1 max 512 +option name Hash type spin default 16 min 1 max 131072 +option name Clear Hash type button +option name Ponder type check default false +option name MultiPV type spin default 1 min 1 max 500 +option name Skill Level type spin default 20 min 0 max 20 +option name Move Overhead type spin default 30 min 0 max 5000 +option name Minimum Thinking Time type spin default 20 min 0 max 5000 +option name Slow Mover type spin default 84 min 10 max 1000 +option name nodestime type spin default 0 min 0 max 10000 +option name UCI_Chess960 type check default false +option name UCI_Variant type combo default chess var chess var antichess var atomic var crazyhouse var horde var kingofthehill var racingkings var 3check var giveaway +option name UCI_AnalyseMode type check default false +option name UCI_LimitStrength type check default false +option name UCI_Elo type spin default 1350 min 1350 max 2850 +option name SyzygyPath type string default +option name SyzygyProbeDepth type spin default 1 min 1 max 100 +option name Syzygy50MoveRule type check default true +option name SyzygyProbeLimit type spin default 7 min 0 max 7 +uciok +! +readyok +! +! +! +info depth 1 seldepth 1 multipv 1 score cp 114 nodes 20 nps 10000 tbhits 0 time 2 pv e2e3 +info depth 2 seldepth 2 multipv 1 score cp 126 nodes 54 nps 27000 tbhits 0 time 2 pv e2e3 b7b6 +info depth 3 seldepth 3 multipv 1 score cp 124 nodes 147 nps 73500 tbhits 0 time 2 pv e2e3 b7b6 f1c4 +info depth 4 seldepth 4 multipv 1 score cp 75 nodes 574 nps 191333 tbhits 0 time 3 pv d2d4 e7e6 e2e3 b8c6 +info depth 5 seldepth 5 multipv 1 score cp 86 nodes 782 nps 195500 tbhits 0 time 4 pv c2c3 b8c6 d2d4 d7d6 +info depth 6 seldepth 6 multipv 1 score cp 56 nodes 2734 nps 341750 tbhits 0 time 8 pv d2d4 c7c6 g1f3 d7d5 c2c3 g8f6 +info depth 7 seldepth 8 multipv 1 score cp 43 nodes 5780 nps 525454 tbhits 0 time 11 pv c2c3 b8c6 d2d4 d7d5 e2e3 g8f6 f1e2 +info depth 8 seldepth 10 multipv 1 score cp 46 nodes 9568 nps 598000 tbhits 0 time 16 pv b1c3 d7d5 e2e4 d5e4 c3e4 c7c6 g1f3 g8f6 +info depth 9 seldepth 13 multipv 1 score cp 48 nodes 14134 nps 673047 tbhits 0 time 21 pv e2e4 d7d5 e4d5 d8d5 d2d4 d5e6 c1e3 g8f6 g1e2 b8c6 +info depth 10 seldepth 14 multipv 1 score cp 105 nodes 18613 nps 744520 tbhits 0 time 25 pv e2e4 e7e6 b1c3 b8c6 d2d4 d7d5 e4d5 e6d5 g1f3 +bestmove e2e4 ponder e7e6 +! diff --git a/fuzz/corpus/engine/uciok-readyok b/fuzz/corpus/engine/uciok-readyok new file mode 100644 index 000000000..bc966c9b4 --- /dev/null +++ b/fuzz/corpus/engine/uciok-readyok @@ -0,0 +1,5 @@ +Fake engine +uciok +! +readyok +! diff --git a/fuzz/corpus/epd/bratko-kopec.epd b/fuzz/corpus/epd/bratko-kopec.epd new file mode 100644 index 000000000..fed4bdd75 --- /dev/null +++ b/fuzz/corpus/epd/bratko-kopec.epd @@ -0,0 +1 @@ +1k1r4/pp1b1R2/3q2pp/4p3/2B5/4Q3/PPP2B2/2K5 b - - bm Qd1+; id "BK.01"; diff --git a/fuzz/corpus/epd/giveaway-puzzles.epd b/fuzz/corpus/epd/giveaway-puzzles.epd new file mode 100644 index 000000000..fbd3cae1e --- /dev/null +++ b/fuzz/corpus/epd/giveaway-puzzles.epd @@ -0,0 +1 @@ +2b5/K1p1pk2/6pb/8/8/8/8/8 b - - bm Bb7; diff --git a/fuzz/corpus/epd/pv.epd b/fuzz/corpus/epd/pv.epd new file mode 100644 index 000000000..4cf957965 --- /dev/null +++ b/fuzz/corpus/epd/pv.epd @@ -0,0 +1 @@ +rq2r1k1/5pp1/p7/4bNP1/1p2P2P/5Q2/PP4K1/5R1R w - - bm Nxg7; id "BT-2630.1"; ce 182; acs 0; acn 106652; pv Nxg7 Kxg7 Qxf7+ Kh8 Qh7+ Kxh7 Rf7+ Kg6 Rf6+ Bxf6 h5+ Kxg5; c0 8 diff --git a/fuzz/corpus/epd/string-escaping.epd b/fuzz/corpus/epd/string-escaping.epd new file mode 100644 index 000000000..7658302a3 --- /dev/null +++ b/fuzz/corpus/epd/string-escaping.epd @@ -0,0 +1 @@ +4k3/8/8/8/8/8/8/4K3 w - - a "foo\"bar";; ; b "foo\\\\"; diff --git a/fuzz/corpus/fen/empty b/fuzz/corpus/fen/empty new file mode 100644 index 000000000..78f0c1155 --- /dev/null +++ b/fuzz/corpus/fen/empty @@ -0,0 +1 @@ +8/8/8/8/8/8/8/8 w - - 0 1 diff --git a/fuzz/corpus/fen/endgame b/fuzz/corpus/fen/endgame new file mode 100644 index 000000000..169f062c8 --- /dev/null +++ b/fuzz/corpus/fen/endgame @@ -0,0 +1 @@ +1Q6/p3R3/6rp/P1K2k2/8/8/6pP/8 w - - 0 46 diff --git a/fuzz/corpus/fen/horde b/fuzz/corpus/fen/horde new file mode 100644 index 000000000..f9a2f7da3 --- /dev/null +++ b/fuzz/corpus/fen/horde @@ -0,0 +1 @@ +rnbqkb1r/p1p1nppp/2Pp4/3P1PP1/PPPPPP1P/PPP1PPPP/PPPPPPPP/PPPPPPPP w kq - 1 6 diff --git a/fuzz/corpus/fen/initial b/fuzz/corpus/fen/initial new file mode 100644 index 000000000..f9c9ce27d --- /dev/null +++ b/fuzz/corpus/fen/initial @@ -0,0 +1 @@ +rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 diff --git a/fuzz/corpus/fen/scid-3check b/fuzz/corpus/fen/scid-3check new file mode 100644 index 000000000..3d9e8a0ae --- /dev/null +++ b/fuzz/corpus/fen/scid-3check @@ -0,0 +1 @@ +r1b2rk1/ppp2p1p/4p1p1/4Bn2/1b2N3/3B4/PPP3PP/R2Q1R1K b - - 0 15 +0+0 diff --git a/fuzz/corpus/fen/scid-zh b/fuzz/corpus/fen/scid-zh new file mode 100644 index 000000000..2f043e42b --- /dev/null +++ b/fuzz/corpus/fen/scid-zh @@ -0,0 +1 @@ +1r3rkQ/pPp2p1p/1pp2Bp1/8/3P4/4P3/PP3PPP/R2Q1RK1/BPNBnbnn b - - 39 20 diff --git a/fuzz/corpus/fen/winboard-3check b/fuzz/corpus/fen/winboard-3check new file mode 100644 index 000000000..d6129a306 --- /dev/null +++ b/fuzz/corpus/fen/winboard-3check @@ -0,0 +1 @@ +r1b2rk1/ppp2p1p/4p1p1/4Bn2/1b2N3/3B4/PPP3PP/R2Q1R1K b - - 3+3 0 15 diff --git a/fuzz/corpus/fen/winboard-zh b/fuzz/corpus/fen/winboard-zh new file mode 100644 index 000000000..b0e689089 --- /dev/null +++ b/fuzz/corpus/fen/winboard-zh @@ -0,0 +1 @@ +r4rk1/p2bppbp/6p1/2qpP3/5P2/2NBPQPp/PPP4P/R1B3K1[NNNrp] b - - 39 20 diff --git a/fuzz/corpus/pgn/anastasian-lewis.pgn b/fuzz/corpus/pgn/anastasian-lewis.pgn new file mode 100644 index 000000000..04faad1e2 --- /dev/null +++ b/fuzz/corpus/pgn/anastasian-lewis.pgn @@ -0,0 +1,20 @@ +[Event "World Senior Teams +50"] +[Site "Radebeul GER"] +[Date "2016.07.03"] +[Round "8.2"] +[White "Anastasian, A."] +[Black "Lewis, An"] +[Result "1-0"] +[ECO "E90"] +[WhiteElo "2532"] +[BlackElo "2269"] +[PlyCount "84"] +[EventDate "2016.06.26"] + +1. d4 Nf6 2. c4 g6 3. Nc3 Bg7 4. e4 d6 5. Nf3 O-O 6. h3 e5 7. d5 Na6 8. Be3 Nh5 +9. Nh2 Qe8 10. Be2 Nf4 11. Bf3 f5 12. a3 Nc5 13. Bxc5 dxc5 14. O-O Qe7 15. Re1 +a6 16. Ne2 Qd6 17. Nf1 Bd7 18. Rb1 b6 19. Nd2 Bh6 20. Nxf4 Bxf4 21. b4 Rae8 22. +Qc2 Rf6 23. Qc3 Qf8 24. Nb3 cxb4 25. axb4 Bg5 26. Rb2 Rf7 27. Nc1 Qh6 28. Nd3 +fxe4 29. Bxe4 Bxh3 30. gxh3 Qxh3 31. Bg2 Qh4 32. Re4 Qh5 33. Rbe2 Ref8 34. c5 +Bf4 35. Nxe5 Qh2+ 36. Kf1 Rf5 37. Nf3 Qh5 38. Re7 Bh6 39. R2e5 bxc5 40. bxc5 +Rxf3 41. Bxf3 Z0 42. Ke1 Qh1+ 1-0 diff --git a/fuzz/corpus/pgn/antichess-programfox.pgn b/fuzz/corpus/pgn/antichess-programfox.pgn new file mode 100644 index 000000000..d4e9cce91 --- /dev/null +++ b/fuzz/corpus/pgn/antichess-programfox.pgn @@ -0,0 +1,7 @@ +[Variant "Antichess"] + +1. g3 Nh6 2. g4 Nxg4 3. b3 Nxh2 4. Rxh2 g5 5. Rxh7 Rxh7 6. Bh3 Rxh3 7. Nxh3 Na6 8. Nxg5 Nb4 9. Nxf7 Nxc2 10. Qxc2 Kxf7 11. Qxc7 Qxc7 12. a4 Qxc1 13. Ra3 Qxa3 14. Nxa3 b5 15. Nxb5 Rb8 16. Nxa7 Rxb3 17. Nxc8 Rg3 18. Nxe7 Bxe7 19. fxg3 Bh4 20. gxh4 d5 21. e4 dxe4 22. d3 exd3 23. Kf1 d2 24. Kg1 Kf6 25. a5 Ke6 26. a6 Kd7 27. a7 Kc7 28. h5 d1=B 29. a8=B Bxh5 30. Bf3 Bxf3 31. Kg2 Bxg2# 1-0 + +[Variant "Antichess"] + +1. Nh3 b6 2. Ng5 g6 3. Nxf7 Kxf7 4. g4 h5 5. gxh5 Rxh5 6. Bh3 Rxh3 7. Rg1 Rxh2 8. Rxg6 Rxf2 9. Rxg8 Kxg8 10. Kxf2 Na6 11. b4 Nxb4 12. a3 Nxc2 13. Qxc2 e6 14. Qxc7 Bxa3 15. Rxa3 Qxc7 16. Rxa7 Qxc1 17. Rxd7 Qxd2 18. Nxd2 Bxd7 19. Ne4 e5 20. Nc5 bxc5 21. e4 Bf5 22. exf5 c4 23. f6 Kh7 24. f7 Re8 25. fxe8=K c3 26. Kd7 e4 27. Kc6 e3 28. Kxe3 c2 29. Kc5 c1=K 30. Kd2 Kxd2 31. Kd5 Kg7 32. Kd6 Kd3 1/2-1/2 diff --git a/fuzz/corpus/pgn/cutechess-fischerrandom.pgn b/fuzz/corpus/pgn/cutechess-fischerrandom.pgn new file mode 100644 index 000000000..2fdbd09fe --- /dev/null +++ b/fuzz/corpus/pgn/cutechess-fischerrandom.pgn @@ -0,0 +1,190 @@ +[Event "?"] +[Site "?"] +[Date "2016.07.28"] +[Round "1"] +[White "Maverick 1.5"] +[Black "Stockfish 7"] +[Result "0-1"] +[FEN "nbbrknrq/pppppppp/8/8/8/8/PPPPPPPP/NBBRKNRQ w KQkq - 0 1"] +[PlyCount "120"] +[SetUp "1"] +[TimeControl "10+0.05"] +[Variant "fischerandom"] + +1. d4 {+0.13/12 0.29s} d5 {-0.17/15 0.30s} 2. c3 {+0.22/13 0.28s} +Nb6 {-0.17/14 0.14s} 3. e4 {+0.21/13 0.95s} dxe4 {+0.14/13 0.29s} +4. Bxe4 {+0.25/12 0.22s} g6 {-0.03/15 0.69s} 5. g4 {+0.31/11 0.30s} +c6 {+0.26/13 0.19s} 6. Qf3 {+0.29/11 0.30s} Ne6 {+0.25/15 0.67s} +7. Nb3 {+0.22/10 0.35s} Qf6 {+0.23/15 0.14s} 8. Qxf6 {+0.34/12 0.27s} +exf6 {+0.25/15 0.13s} 9. h4 {+0.32/12 0.39s} O-O {+0.20/15 0.073s} +10. Be3 {+0.15/11 0.33s} Nf4 {+0.68/14 0.60s} 11. Nc5 {+0.21/10 0.17s} +Rfe8 {+0.77/15 0.34s} 12. Nd2 {0.00/10 0.25s} Nbd5 {+1.24/14 0.27s} +13. c4 {0.00/10 0.16s} b6 {+1.51/14 0.19s} 14. cxd5 {-0.26/10 0.15s} +cxd5 {+1.69/12 0.030s} 15. Bc2 {-1.01/10 0.23s} bxc5 {+2.00/14 0.29s} +16. Nb3 {-1.29/11 0.17s} c4 {+1.82/15 0.50s} 17. Nc5 {-1.25/13 0.22s} +Bd6 {+2.05/15 0.19s} 18. Ba4 {-1.31/10 0.20s} Re7 {+1.91/15 0.27s} +19. Bc2 {-1.46/10 0.15s} h5 {+2.11/16 0.82s} 20. gxh5 {-1.62/12 0.20s} +Nxh5 {+1.89/16 0.43s} 21. O-O-O {-1.50/11 0.22s} Nf4 {+2.04/15 0.36s} +22. Rde1 {-1.74/10 0.13s} Kg7 {+2.14/15 0.20s} 23. h5 {-1.65/11 0.15s} +Nxh5 {+2.23/12 0.030s} 24. Kd2 {-1.92/10 0.19s} f5 {+2.43/14 0.36s} +25. Bg5 {-1.74/10 0.15s} Rxe1 {+2.37/15 0.22s} 26. Rxe1 {-2.13/11 0.17s} +f6 {+3.39/15 0.20s} 27. Bh4 {-2.74/13 0.14s} g5 {+3.22/13 0.13s} +28. Rg1 {-2.88/13 0.15s} Rh8 {+4.22/15 0.44s} 29. Bd1 {-2.88/10 0.15s} +Kf8 {+4.10/16 0.24s} 30. Bg3 {-3.61/12 0.14s} f4 {+4.29/16 0.25s} +31. Bh2 {-3.70/13 0.15s} Ng7 {+4.40/14 0.038s} 32. Rh1 {-4.06/14 0.23s} +Nf5 {+4.48/16 0.17s} 33. Bg4 {-4.56/13 0.11s} Ke7 {+4.60/14 0.021s} +34. Bxf5 {-4.14/11 0.13s} Bxf5 {+4.70/16 0.16s} 35. f3 {-4.34/12 0.25s} +Rh3 {+4.84/15 0.092s} 36. Ke2 {-5.89/12 0.32s} g4 {+4.83/13 0.025s} +37. fxg4 {-4.74/11 0.13s} Bg6 {+4.91/15 0.079s} 38. Ke1 {-4.95/10 0.12s} +Bxc5 {+5.01/13 0.038s} 39. dxc5 {-5.95/12 0.14s} Be4 {+5.22/14 0.11s} +40. a4 {-6.42/12 0.38s} Bxh1 {+5.82/13 0.079s} 41. Bxf4 {-7.92/12 0.12s} +d4 {+5.91/13 0.033s} 42. Kd2 {-6.92/10 0.083s} Kd7 {+6.10/14 0.082s} +43. g5 {-7.26/10 0.11s} fxg5 {+6.57/15 0.11s} 44. Bxg5 {-8.34/10 0.091s} +Kc6 {+6.81/13 0.026s} 45. Bf4 {-8.56/11 0.20s} Kxc5 {+7.90/15 0.14s} +46. a5 {-8.94/11 0.17s} Be4 {+11.14/16 0.22s} 47. a6 {-9.64/10 0.10s} +Rb3 {+15.84/15 0.094s} 48. Kc1 {-11.49/13 0.12s} c3 {+48.50/24 0.078s} +49. bxc3 {-10.88/13 0.088s} dxc3 {+48.54/23 0.018s} 50. Kd1 {-11.10/13 0.17s} +c2+ {+M35/23 0.14s} 51. Ke2 {-10.72/12 0.16s} Rb1 {+M31/23 0.024s} +52. Bd2 {-11.36/12 0.080s} c1=Q {+M21/24 0.076s} 53. Bxc1 {-12.14/14 0.11s} +Rxc1 {+M19/23 0.025s} 54. Ke3 {-12.35/13 0.072s} Kd5 {+M15/23 0.10s} +55. Kf2 {-M18/13 0.14s} Kd4 {+M11/24 0.070s} 56. Kg3 {-M10/12 0.026s} +Ke3 {+M9/24 0.016s} 57. Kg4 {-M8/8 0.001s} Rg1+ {+M7/40 0.063s} +58. Kh5 {-M6/6 0.001s} Kf4 {+M5/91 0.015s} 59. Kh6 {-M4/4 0.002s} +Rg2 {+M3/127 0.004s} 60. Kh5 {-M2/2 0.001s} Rh2# {+M1/127 0.003s, Black mates} +0-1 + +[Event "?"] +[Site "?"] +[Date "2016.07.28"] +[Round "1"] +[White "Stockfish 7"] +[Black "Maverick 1.5"] +[Result "1-0"] +[FEN "rqkrbnnb/pppppppp/8/8/8/8/PPPPPPPP/RQKRBNNB w KQkq - 0 1"] +[PlyCount "99"] +[SetUp "1"] +[TimeControl "10+0.05"] +[Variant "fischerandom"] + +1. g3 {+0.14/14 0.54s} g6 {-0.11/13 0.39s} 2. e4 {+0.12/14 0.31s} +Ne6 {+0.01/12 0.22s} 3. c3 {+0.32/15 0.54s} d6 {+0.31/12 0.25s} +4. d4 {+0.64/15 0.64s} Nf6 {+0.15/12 0.27s} 5. Ne3 {+0.55/14 0.29s} +Bg7 {+0.02/12 0.39s} 6. f4 {+0.72/15 1.1s} h5 {-0.25/11 0.32s} +7. Bf3 {+0.87/14 0.27s} Ba4 {-0.57/11 0.38s} 8. b3 {+0.94/13 0.18s} +Bd7 {-0.67/13 0.25s} 9. Kb2 {+0.91/14 0.65s} Nf8 {-0.44/11 0.31s} +10. h3 {+1.22/12 0.10s} e5 {-0.29/12 0.46s} 11. dxe5 {+1.53/13 0.27s} +dxe5 {-0.44/13 0.19s} 12. fxe5 {+1.49/14 0.51s} Ng8 {-0.40/12 0.30s} +13. g4 {+1.65/13 0.16s} hxg4 {-0.58/11 0.27s} 14. Bxg4 {+1.86/14 0.33s} +Bxe5 {-0.50/12 0.19s} 15. Nf3 {+2.20/14 0.23s} Bf4 {-0.79/12 0.19s} +16. Bh4 {+2.73/14 0.17s} Re8 {-0.87/12 0.16s} 17. Qd3 {+4.34/14 0.17s} +f5 {-2.56/13 0.17s} 18. Nxf5 {+4.28/15 0.30s} gxf5 {-2.69/14 0.17s} +19. Bxf5 {+4.68/15 0.061s} Bd6 {-3.08/14 0.22s} 20. e5 {+5.62/14 0.23s} +Nh6 {-3.56/13 0.37s} 21. Bxd7+ {+6.66/14 0.15s} Nxd7 {-3.81/12 0.13s} +22. Qg6 {+7.05/14 0.060s} Rf8 {-4.75/11 0.17s} 23. e6 {+8.47/15 0.36s} +b5 {-5.56/12 0.46s} 24. exd7+ {+9.50/15 0.28s} Kxd7 {-6.15/12 0.24s} +25. Ne5+ {+10.51/18 0.10s} Kc8 {-6.52/11 0.036s} 26. Qe6+ {+10.58/15 0.021s} +Kb7 {-5.07/8 0.021s} 27. Rxd6 {+11.11/19 0.24s} cxd6 {-10.56/11 0.22s} +28. Qd5+ {+11.15/16 0.062s} Kc8 {-11.09/15 0.24s} 29. Qc6+ {+11.23/17 0.13s} +Qc7 {-11.23/15 0.034s} 30. Qxa8+ {+11.31/15 0.066s} Qb8 {-11.23/15 0.038s} +31. Qxb8+ {+11.41/16 0.14s} Kxb8 {-11.23/13 0.030s} 32. Nd7+ {+11.39/15 0.034s} +Kb7 {-11.74/14 0.51s} 33. Nxf8 {+11.46/16 0.12s} Nf5 {-11.71/13 0.25s} +34. Bg5 {+11.51/15 0.032s} Kc6 {-11.69/11 0.26s} 35. h4 {+11.75/15 0.095s} +Ng3 {-12.05/11 0.12s} 36. Ne6 {+12.06/14 0.31s} Ne4 {-12.04/10 0.17s} +37. Nd4+ {+12.31/13 0.090s} Kb6 {-12.39/11 0.14s} 38. Bf4 {+12.75/15 0.22s} +a6 {-11.93/9 0.13s} 39. h5 {+15.20/16 0.20s} Kc5 {-13.49/11 0.26s} +40. h6 {+21.53/15 0.24s} Nf6 {-14.72/12 0.12s} 41. Re1 {+22.26/13 0.053s} +b4 {-14.64/11 0.13s} 42. Bxd6+ {+M21/19 0.20s} Kxd6 {-17.58/12 0.10s} +43. Re6+ {+M17/21 0.095s} Kd7 {-17.90/12 0.12s} 44. Rxf6 {+M15/21 0.047s} +Ke7 {-M18/11 0.25s} 45. Rf5 {+M13/19 0.069s} Kd6 {-M12/12 0.12s} +46. h7 {+M11/20 0.085s} a5 {-M10/10 0.010s} 47. h8=Q {+M9/20 0.076s} +Kc7 {-M8/8 0.007s} 48. Qg7+ {+M5/127 0.031s} Kd6 {-M4/4 0.001s} +49. Nb5+ {+M3/127 0.003s} Kc6 {-M2/2 0s} 50. Qc7# {+M1/127 0.003s, White mates} +1-0 + +[Event "?"] +[Site "?"] +[Date "2016.07.28"] +[Round "1"] +[White "Maverick 1.5"] +[Black "Stockfish 7"] +[Result "0-1"] +[FEN "rnkqnbbr/pppppppp/8/8/8/8/PPPPPPPP/RNKQNBBR w KQkq - 0 1"] +[PlyCount "70"] +[SetUp "1"] +[TimeControl "10+0.05"] +[Variant "fischerandom"] + +1. f4 {+0.30/11 0.23s} d5 {-0.09/13 0.15s} 2. Nc3 {+0.50/11 0.35s} +Nf6 {+0.03/14 0.27s} 3. Nf3 {+0.41/13 0.40s} e6 {+0.08/13 0.18s} +4. Bd4 {+0.33/12 0.42s} h6 {+0.32/13 0.29s} 5. e3 {+0.30/11 0.23s} +Ng4 {+0.24/13 0.16s} 6. Qe1 {+0.26/12 0.35s} c5 {+0.61/14 0.11s} +7. Be5 {+0.37/13 0.23s} Nc6 {+0.60/14 0.30s} 8. Bb5 {+0.23/13 0.54s} +Ngxe5 {+0.66/14 0.30s} 9. fxe5 {+0.10/13 0.23s} Bh7 {+0.55/13 0.069s} +10. Bxc6 {+0.28/11 0.20s} bxc6 {+0.76/14 0.35s} 11. Rf1 {+0.08/11 0.58s} +Rb8 {+1.28/13 0.22s} 12. b3 {0.00/10 0.32s} Rb7 {+1.54/13 0.20s} +13. e4 {-0.03/9 0.17s} c4 {+1.73/15 0.53s} 14. Qe3 {-0.31/9 0.19s} +Qa5 {+1.85/14 0.76s} 15. Nd4 {-0.57/8 0.19s} c5 {+3.82/13 0.34s} +16. Nxe6 {-1.50/10 0.18s} cxb3 {+3.82/15 0.45s} 17. cxb3 {-1.25/10 0.23s} +fxe6 {+4.14/13 0.15s} 18. exd5 {-1.72/10 0.17s} exd5 {+4.05/13 0.23s} +19. Qh3+ {-1.57/10 0.39s} Kb8 {+4.59/14 0.20s} 20. Nxd5 {-1.62/10 0.14s} +c4 {+8.22/14 0.32s} 21. Qe6 {-4.82/10 0.34s} Bc5 {+11.06/14 0.26s} +22. Kd1 {-6.91/10 0.48s} cxb3 {+12.77/14 0.20s} 23. d4 {-8.16/10 0.24s} +b2 {+21.67/14 0.23s} 24. Rb1 {-14.22/11 0.36s} Qa4+ {+23.31/14 0.24s} +25. Ke1 {-14.18/11 0.14s} Qxd4 {+30.39/15 0.80s} 26. Rf3 {-18.19/9 0.11s} +Qe4+ {+30.58/13 0.026s} 27. Ne3 {-21.85/11 0.16s} Qxb1+ {+40.76/14 0.84s} +28. Kf2 {-24.98/12 0.26s} Qe4 {+41.45/13 0.047s} 29. Kg3 {-24.92/12 0.56s} +b1=Q {+M21/13 0.18s} 30. Qg4 {-42.56/12 0.49s} Bxe3 {+M11/17 0.16s} +31. Qxe4 {-M18/10 0.11s} Qxe4 {+M9/20 0.022s} 32. a3 {-M8/11 0.32s} +g5 {+M7/30 0.094s} 33. h3 {-M6/6 0.005s} Qxe5+ {+M5/46 0.021s} +34. Kg4 {-M8/8 0.003s} h5+ {+M3/127 0.004s} 35. Kxh5 {-M2/2 0s} +Bf5# {+M1/127 0.003s, Black mates} 0-1 + +[Event "?"] +[Site "?"] +[Date "2016.07.28"] +[Round "1"] +[White "Stockfish 7"] +[Black "Maverick 1.5"] +[Result "1-0"] +[FEN "nbrnbkqr/pppppppp/8/8/8/8/PPPPPPPP/NBRNBKQR w KQkq - 0 1"] +[PlyCount "107"] +[SetUp "1"] +[TimeControl "10+0.05"] +[Variant "fischerandom"] + +1. c4 {+0.13/15 0.38s} c5 {-0.41/13 0.34s} 2. f3 {+0.17/14 0.36s} +Be5 {-0.27/13 0.29s} 3. Nb3 {+0.26/14 0.20s} d6 {-0.30/14 0.34s} +4. e3 {+0.11/15 0.67s} Nb6 {+0.08/12 0.32s} 5. d4 {0.00/14 0.60s} +cxd4 {+0.02/13 0.22s} 6. exd4 {+0.05/15 0.22s} Bf4 {-0.07/13 0.25s} +7. Bd2 {-0.05/12 0.029s} Bxd2 {-0.08/13 0.25s} 8. Nxd2 {+0.50/13 0.16s} +Bd7 {-0.12/12 0.23s} 9. b3 {+0.69/13 0.15s} Nc6 {-0.23/11 0.37s} +10. Nc3 {+0.85/14 0.35s} f6 {-0.33/11 0.23s} 11. d5 {+1.24/14 0.39s} +Ne5 {-0.83/11 0.19s} 12. f4 {+1.51/15 0.57s} Ng6 {-0.85/11 0.28s} +13. Qf2 {+1.67/16 0.36s} Qf7 {-0.67/11 0.17s} 14. a4 {+1.74/15 0.36s} +Rc7 {-0.99/11 0.29s} 15. Bd3 {+1.89/16 0.43s} O-O {-1.09/11 0.39s} +16. O-O {+1.80/15 0.077s} f5 {-1.07/11 0.19s} 17. g3 {+1.96/15 0.25s} +Nc8 {-1.09/10 0.18s} 18. Nf3 {+1.87/14 0.33s} a5 {-1.18/11 0.60s} +19. Ng5 {+2.46/14 0.24s} Qf6 {-1.35/14 0.19s} 20. Nb5 {+2.37/13 0.040s} +Bxb5 {-2.07/14 0.16s} 21. axb5 {+3.17/15 0.41s} Rd7 {-2.19/13 0.16s} +22. Ne6 {+3.39/14 0.11s} Re8 {-2.69/13 0.34s} 23. Qc2 {+3.75/15 0.34s} +Nb6 {-2.58/12 0.17s} 24. Bxf5 {+4.00/14 0.26s} Rc8 {-3.29/11 0.21s} +25. Ng5 {+4.98/13 0.099s} Kh8 {-4.05/11 0.20s} 26. Be6 {+4.99/12 0.024s} +Qd4+ {-4.27/11 0.12s} 27. Kh1 {+5.51/15 0.19s} Nf8 {-4.33/11 0.15s} +28. Rfe1 {+5.56/14 0.031s} a4 {-5.63/11 0.37s} 29. bxa4 {+5.70/15 0.16s} +g6 {-4.90/10 0.11s} 30. Re4 {+6.24/14 0.13s} Qf6 {-5.64/13 0.17s} +31. a5 {+6.28/13 0.031s} Nxd5 {-5.63/12 0.38s} 32. Bxd5 {+6.81/14 0.37s} +h6 {-6.27/12 0.60s} 33. Ne6 {+7.60/15 0.15s} Nh7 {-6.31/11 0.10s} +34. a6 {+8.36/16 0.23s} b6 {-6.96/12 0.20s} 35. Qa2 {+9.66/16 0.24s} +Ra7 {-7.23/12 0.13s} 36. Qf2 {+9.66/16 0.18s} Rb8 {-8.02/12 0.17s} +37. Nd4 {+9.86/15 0.029s} Ng5 {-9.02/12 0.19s} 38. Nc6 {+10.75/15 0.12s} +Nxe4 {-8.81/12 0.15s} 39. Bxe4 {+10.89/13 0.029s} d5 {-9.34/13 0.16s} +40. Bxd5 {+11.75/15 0.15s} Rba8 {-10.57/13 0.21s} 41. Nxa7 {+13.36/17 0.31s} +Rxa7 {-10.97/12 0.13s} 42. c5 {+13.46/14 0.023s} Rc7 {-10.67/11 0.13s} +43. Bc6 {+16.80/14 0.17s} Rxc6 {-11.13/9 0.094s} 44. bxc6 {+18.21/13 0.11s} +Qxc6+ {-14.47/9 0.17s} 45. Qg2 {+21.11/14 0.31s} Qa4 {-17.24/10 0.17s} +46. cxb6 {+27.63/15 0.41s} Qxa6 {-17.80/9 0.10s} 47. Qb2+ {+56.80/14 0.085s} +Kh7 {-19.85/10 0.19s} 48. b7 {+M19/15 0.045s} Qd6 {-17.43/11 0.31s} +49. b8=Q {+M13/17 0.060s} Qxb8 {-M12/13 0.25s} 50. Qxb8 {+M9/28 0.063s} +g5 {-M8/8 0.001s} 51. Qe8 {+M7/31 0.058s} Kg7 {-M6/6 0.001s} +52. Rc6 {+M5/77 0.014s} g4 {-M4/4 0.001s} 53. Qxe7+ {+M3/127 0.002s} +Kg8 {-M2/2 0.001s} 54. Rc8# {+M1/127 0.002s, White mates} 1-0 diff --git a/fuzz/corpus/pgn/kasparov-deep-blue-1997.pgn b/fuzz/corpus/pgn/kasparov-deep-blue-1997.pgn new file mode 100644 index 000000000..7b5fcf0b0 --- /dev/null +++ b/fuzz/corpus/pgn/kasparov-deep-blue-1997.pgn @@ -0,0 +1,135 @@ +[Event "IBM Man-Machine, New York USA"] +[Site "01"] +[Date "1997.??.??"] +[EventDate "?"] +[Round "?"] +[Result "1-0"] +[White "Garry Kasparov"] +[Black "Deep Blue (Computer)"] +[ECO "A06"] +[WhiteElo "?"] +[BlackElo "?"] +[PlyCount "89"] + +1.Nf3 d5 2.g3 Bg4 3.b3 Nd7 4.Bb2 e6 5.Bg2 Ngf6 6.O-O c6 7.d3 +Bd6 8.Nbd2 O-O 9.h3 Bh5 10.e3 h6 11.Qe1 Qa5 12.a3 Bc7 13.Nh4 +g5 14.Nhf3 e5 15.e4 Rfe8 16.Nh2 Qb6 17.Qc1 a5 18.Re1 Bd6 +19.Ndf1 dxe4 20.dxe4 Bc5 21.Ne3 Rad8 22.Nhf1 g4 23.hxg4 Nxg4 +24.f3 Nxe3 25.Nxe3 Be7 26.Kh1 Bg5 27.Re2 a4 28.b4 f5 29.exf5 +e4 30.f4 Bxe2 31.fxg5 Ne5 32.g6 Bf3 33.Bc3 Qb5 34.Qf1 Qxf1+ +35.Rxf1 h5 36.Kg1 Kf8 37.Bh3 b5 38.Kf2 Kg7 39.g4 Kh6 40.Rg1 +hxg4 41.Bxg4 Bxg4 42.Nxg4+ Nxg4+ 43.Rxg4 Rd5 44.f6 Rd1 45.g7 +1-0 + +[Event "IBM Man-Machine, New York USA"] +[Site "02"] +[Date "1997.??.??"] +[EventDate "?"] +[Round "?"] +[Result "1-0"] +[White "Deep Blue (Computer)"] +[Black "Garry Kasparov"] +[ECO "C93"] +[WhiteElo "?"] +[BlackElo "?"] +[PlyCount "89"] + +1.e4 e5 2.Nf3 Nc6 3.Bb5 a6 4.Ba4 Nf6 5.O-O Be7 6.Re1 b5 7.Bb3 +d6 8.c3 O-O 9.h3 h6 10.d4 Re8 11.Nbd2 Bf8 12.Nf1 Bd7 13.Ng3 +Na5 14.Bc2 c5 15.b3 Nc6 16.d5 Ne7 17.Be3 Ng6 18.Qd2 Nh7 19.a4 +Nh4 20.Nxh4 Qxh4 21.Qe2 Qd8 22.b4 Qc7 23.Rec1 c4 24.Ra3 Rec8 +25.Rca1 Qd8 26.f4 Nf6 27.fxe5 dxe5 28.Qf1 Ne8 29.Qf2 Nd6 +30.Bb6 Qe8 31.R3a2 Be7 32.Bc5 Bf8 33.Nf5 Bxf5 34.exf5 f6 +35.Bxd6 Bxd6 36.axb5 axb5 37.Be4 Rxa2 38.Qxa2 Qd7 39.Qa7 Rc7 +40.Qb6 Rb7 41.Ra8+ Kf7 42.Qa6 Qc7 43.Qc6 Qb6+ 44.Kf1 Rb8 +45.Ra6 1-0 + +[Event "IBM Man-Machine, New York USA"] +[Site "03"] +[Date "1997.??.??"] +[EventDate "?"] +[Round "?"] +[Result "1/2-1/2"] +[White "Garry Kasparov"] +[Black "Deep Blue (Computer)"] +[ECO "A00"] +[WhiteElo "?"] +[BlackElo "?"] +[PlyCount "95"] + +1.d3 e5 2.Nf3 Nc6 3.c4 Nf6 4.a3 d6 5.Nc3 Be7 6.g3 O-O 7.Bg2 +Be6 8.O-O Qd7 9.Ng5 Bf5 10.e4 Bg4 11.f3 Bh5 12.Nh3 Nd4 13.Nf2 +h6 14.Be3 c5 15.b4 b6 16.Rb1 Kh8 17.Rb2 a6 18.bxc5 bxc5 19.Bh3 +Qc7 20.Bg4 Bg6 21.f4 exf4 22.gxf4 Qa5 23.Bd2 Qxa3 24.Ra2 Qb3 +25.f5 Qxd1 26.Bxd1 Bh7 27.Nh3 Rfb8 28.Nf4 Bd8 29.Nfd5 Nc6 +30.Bf4 Ne5 31.Ba4 Nxd5 32.Nxd5 a5 33.Bb5 Ra7 34.Kg2 g5 +35.Bxe5+ dxe5 36.f6 Bg6 37.h4 gxh4 38.Kh3 Kg8 39.Kxh4 Kh7 +40.Kg4 Bc7 41.Nxc7 Rxc7 42.Rxa5 Rd8 43.Rf3 Kh8 44.Kh4 Kg8 +45.Ra3 Kh8 46.Ra6 Kh7 47.Ra3 Kh8 48.Ra6 1/2-1/2 + +[Event "IBM Man-Machine, New York USA"] +[Site "04"] +[Date "1997.??.??"] +[EventDate "?"] +[Round "?"] +[Result "1/2-1/2"] +[White "Deep Blue (Computer)"] +[Black "Garry Kasparov"] +[ECO "B10"] +[WhiteElo "?"] +[BlackElo "?"] +[PlyCount "111"] + +1.e4 c6 2.d4 d6 3.Nf3 Nf6 4.Nc3 Bg4 5.h3 Bh5 6.Bd3 e6 7.Qe2 d5 +8.Bg5 Be7 9.e5 Nfd7 10.Bxe7 Qxe7 11.g4 Bg6 12.Bxg6 hxg6 13.h4 +Na6 14.O-O-O O-O-O 15.Rdg1 Nc7 16.Kb1 f6 17.exf6 Qxf6 18.Rg3 +Rde8 19.Re1 Rhf8 20.Nd1 e5 21.dxe5 Qf4 22.a3 Ne6 23.Nc3 Ndc5 +24.b4 Nd7 25.Qd3 Qf7 26.b5 Ndc5 27.Qe3 Qf4 28.bxc6 bxc6 29.Rd1 +Kc7 30.Ka1 Qxe3 31.fxe3 Rf7 32.Rh3 Ref8 33.Nd4 Rf2 34.Rb1 Rg2 +35.Nce2 Rxg4 36.Nxe6+ Nxe6 37.Nd4 Nxd4 38.exd4 Rxd4 39.Rg1 Rc4 +40.Rxg6 Rxc2 41.Rxg7+ Kb6 42.Rb3+ Kc5 43.Rxa7 Rf1+ 44.Rb1 Rff2 +45.Rb4 Rc1+ 46.Rb1 Rcc2 47.Rb4 Rc1+ 48.Rb1 Rxb1+ 49.Kxb1 Re2 +50.Re7 Rh2 51.Rh7 Kc4 52.Rc7 c5 53.e6 Rxh4 54.e7 Re4 55.a4 Kb3 +56.Kc1 1/2-1/2 + +[Event "IBM Man-Machine, New York USA"] +[Site "05"] +[Date "1997.??.??"] +[EventDate "?"] +[Round "?"] +[Result "1/2-1/2"] +[White "Garry Kasparov"] +[Black "Deep Blue (Computer)"] +[ECO "A07"] +[WhiteElo "?"] +[BlackElo "?"] +[PlyCount "98"] + +1.Nf3 d5 2.g3 Bg4 3.Bg2 Nd7 4.h3 Bxf3 5.Bxf3 c6 6.d3 e6 7.e4 +Ne5 8.Bg2 dxe4 9.Bxe4 Nf6 10.Bg2 Bb4+ 11.Nd2 h5 12.Qe2 Qc7 +13.c3 Be7 14.d4 Ng6 15.h4 e5 16.Nf3 exd4 17.Nxd4 O-O-O 18.Bg5 +Ng4 19.O-O-O Rhe8 20.Qc2 Kb8 21.Kb1 Bxg5 22.hxg5 N6e5 23.Rhe1 +c5 24.Nf3 Rxd1+ 25.Rxd1 Nc4 26.Qa4 Rd8 27.Re1 Nb6 28.Qc2 Qd6 +29.c4 Qg6 30.Qxg6 fxg6 31.b3 Nxf2 32.Re6 Kc7 33.Rxg6 Rd7 +34.Nh4 Nc8 35.Bd5 Nd6 36.Re6 Nb5 37.cxb5 Rxd5 38.Rg6 Rd7 +39.Nf5 Ne4 40.Nxg7 Rd1+ 41.Kc2 Rd2+ 42.Kc1 Rxa2 43.Nxh5 Nd2 +44.Nf4 Nxb3+ 45.Kb1 Rd2 46.Re6 c4 47.Re3 Kb6 48.g6 Kxb5 49.g7 +Kb4 1/2-1/2 + +[Event "IBM Man-Machine, New York USA"] +[Site "06"] +[Date "1997.??.??"] +[EventDate "?"] +[Round "?"] +[Result "1-0"] +[White "Deep Blue (Computer)"] +[Black "Garry Kasparov"] +[ECO "B17"] +[WhiteElo "?"] +[BlackElo "?"] +[PlyCount "37"] + +1.e4 c6 2.d4 d5 3.Nc3 dxe4 4.Nxe4 Nd7 5.Ng5 Ngf6 6.Bd3 e6 +7.N1f3 h6 8.Nxe6 Qe7 9.O-O fxe6 10.Bg6+ Kd8 11.Bf4 b5 12.a4 +Bb7 13.Re1 Nd5 14.Bg3 Kc8 15.axb5 cxb5 16.Qd3 Bc6 17.Bf5 exf5 +18.Rxe7 Bxe7 19.c4 1-0 diff --git a/fuzz/corpus/pgn/knightvuillaume-jannlee-zh-lichess.pgn b/fuzz/corpus/pgn/knightvuillaume-jannlee-zh-lichess.pgn new file mode 100644 index 000000000..905501c39 --- /dev/null +++ b/fuzz/corpus/pgn/knightvuillaume-jannlee-zh-lichess.pgn @@ -0,0 +1,17 @@ +[Event "Crazyhouse Bullet Arena"] +[Site "https://lichess.org/R4GSdrpV"] +[Date "2017.04.25"] +[White "knightvuillaume"] +[Black "JannLee"] +[Result "0-1"] +[WhiteElo "1608"] +[BlackElo "2774"] +[PlyCount "48"] +[Variant "Crazyhouse"] +[TimeControl "60+0"] +[ECO "?"] +[Opening "?"] +[Termination "Normal"] +[Annotator "lichess.org"] + +1. d4 { [%clk 0:01:00] } d5 { [%clk 0:01:00] } 2. Nc3 { [%clk 0:01:00] } Bf5 { [%clk 0:01:00] } 3. e3 { [%clk 0:00:59] } e6 { [%clk 0:00:59] } 4. Bd3 { [%clk 0:00:59] } Bg6?! { (0.00 → 0.75) Inaccuracy. Best move was Nf6. } { [%clk 0:00:59] } (4... Nf6) 5. Nf3 { [%clk 0:00:57] } Bd6? { (0.70 → 1.99) Mistake. Best move was Nf6. } { [%clk 0:00:59] } (5... Nf6 6. O-O Bd6 7. Bxg6 hxg6 8. Ne5 O-O 9. B@f4 B@h4 10. Nf3 Bxf4 11. exf4 B@g4 12. B@g5) 6. O-O? { (1.99 → 0.50) Mistake. Best move was e4. } { [%clk 0:00:56] } (6. e4 dxe4 7. Nxe4 Nc6 8. O-O P@g4 9. Nxd6+ cxd6 10. Bxg6 hxg6 11. Ng5 B@f6 12. Qxg4 Nxd4) 6... Ne7? { (0.50 → 1.56) Mistake. Best move was Nf6. } { [%clk 0:00:59] } (6... Nf6 7. Bxg6 hxg6 8. B@e5 B@g4 9. Bxd6 cxd6 10. B@e2 O-O 11. Nd2 Bxe2 12. Nxe2 B@g4 13. f3) 7. g3? { (1.56 → -0.90) Mistake. Best move was e4. } { [%clk 0:00:55] } (7. e4 O-O) 7... Nbc6?! { (-0.90 → -0.34) Inaccuracy. Best move was Bxd3. } { [%clk 0:00:58] } (7... Bxd3 8. cxd3 B@g4 9. B@g2 O-O 10. e4 Nbc6 11. Be3 dxe4 12. dxe4 P@h3 13. Bxh3 Bxh3 14. P@g2) 8. Re1? { (-0.34 → -1.67) Mistake. Best move was e4. } { [%clk 0:00:53] } (8. e4 O-O 9. exd5 Nxd5 10. Bxg6 hxg6 11. B@h4 Nxc3 12. bxc3 N@h3+ 13. Kg2 f6 14. Kxh3 P@g4+) 8... O-O { [%clk 0:00:58] } 9. Ne2? { (-1.54 → -4.23) Mistake. Best move was Bxg6. } { [%clk 0:00:52] } (9. Bxg6) 9... e5? { (-4.23 → -2.33) Mistake. Best move was Nb4. } { [%clk 0:00:56] } (9... Nb4) 10. dxe5?! { (-2.33 → -2.87) Inaccuracy. Best move was Bxg6. } { [%clk 0:00:51] } (10. Bxg6) 10... Nxe5 { [%clk 0:00:56] } 11. Nxe5 { [%clk 0:00:51] } Bxe5 { [%clk 0:00:56] } 12. f4?? { (-2.17 → -8.28) Blunder. Best move was P@g2. } { [%clk 0:00:50] } (12. P@g2 P@e4 13. N@g4 Bd6 14. Nf4 exd3 15. cxd3 B@g5 16. Nxg6 Nxg6 17. B@c3 h5 18. P@f5 hxg4) 12... N@f3+ { [%clk 0:00:53] } 13. Kg2? { (-7.69 → -9.81) Mistake. Best move was Kf2. } { [%clk 0:00:48] } (13. Kf2 Nxh2 14. Ng1 P@f3 15. fxe5 Ng4+ 16. Kxf3 P@h2 17. B@h3 h1=Q+ 18. P@g2 Qxh3 19. gxh3 B@e4+) 13... Nxe1+ { [%clk 0:00:52] } 14. Qxe1 { [%clk 0:00:48] } Bd6?? { (-9.90 → -4.96) Blunder. Best move was Be4+. } { [%clk 0:00:49] } (14... Be4+ 15. P@f3) 15. P@f3?? { (-4.96 → -9.72) Blunder. Best move was Ng1. } { [%clk 0:00:45] } (15. Ng1 Bxd3) 15... P@e4 { [%clk 0:00:48] } 16. fxe4?! { (-9.12 → -10.91) Inaccuracy. Best move was N@h6+. } { [%clk 0:00:44] } (16. N@h6+ Kh8) 16... dxe4 { [%clk 0:00:48] } 17. Bc4 { [%clk 0:00:43] } P@f3+ { [%clk 0:00:47] } 18. Kf2 { [%clk 0:00:42] } fxe2 { [%clk 0:00:46] } 19. Qxe2 { [%clk 0:00:42] } N@h3+ { [%clk 0:00:45] } 20. Kg2 { [%clk 0:00:41] } R@f2+ { [%clk 0:00:43] } 21. Qxf2 { [%clk 0:00:40] } Nxf2 { [%clk 0:00:43] } 22. Kxf2 { [%clk 0:00:40] } Q@f3+ { [%clk 0:00:43] } 23. Ke1?! { (-24.75 → Mate in 8) Checkmate is now unavoidable. Best move was Kg1. } { [%clk 0:00:39] } (23. Kg1 Bxf4) 23... Bxf4?! { (Mate in 8 → -23.58) Lost forced checkmate sequence. Best move was Bb4+. } { [%clk 0:00:43] } (23... Bb4+ 24. P@d2 Bxd2+ 25. Bxd2 P@f2+ 26. Kf1 Qxd2 27. N@h6+ Kh8 28. Nxf7+ Rxf7 29. R@f8+ Raxf8 30. Bxf7) 24. gxf4?! { (-23.58 → Mate in 1) Checkmate is now unavoidable. Best move was P@d3. } { [%clk 0:00:37] } (24. P@d3 exd3) 24... Qdd1# { [%clk 0:00:42] } { Black wins by checkmate. } 0-1 diff --git a/fuzz/corpus/pgn/molinari-bordais-1979.pgn b/fuzz/corpus/pgn/molinari-bordais-1979.pgn new file mode 100644 index 000000000..d5dee8611 --- /dev/null +++ b/fuzz/corpus/pgn/molinari-bordais-1979.pgn @@ -0,0 +1,14 @@ +[Event "cr"] +[Site "cr"] +[Date "1979.??.??"] +[EventDate "?"] +[Round "?"] +[Result "0-1"] +[White "Molinari"] +[Black "Bordais"] +[ECO "B20"] +[WhiteElo "?"] +[BlackElo "?"] +[PlyCount "10"] + +1. e4 c5 2. c4 Nc6 3. Ne2 Nf6 4. Nbc3 Nb4 5. g3 Nd3# 0-1 diff --git a/fuzz/corpus/pgn/saturs-jannlee-zh-lichess.pgn b/fuzz/corpus/pgn/saturs-jannlee-zh-lichess.pgn new file mode 100644 index 000000000..f03040a28 --- /dev/null +++ b/fuzz/corpus/pgn/saturs-jannlee-zh-lichess.pgn @@ -0,0 +1,17 @@ +[Event "Rated game"] +[Site "https://lichess.org/l95HJ3fQ"] +[Date "2016.11.27"] +[White "saturos"] +[Black "JannLee"] +[Result "0-1"] +[WhiteElo "2612"] +[BlackElo "2789"] +[PlyCount "122"] +[Variant "Crazyhouse"] +[TimeControl "60+1"] +[ECO "?"] +[Opening "?"] +[Termination "Normal"] +[Annotator "lichess.org"] + +1. e4 e5 2. Nc3 Nc6 3. Nf3 Bc5 4. Bc4 Nf6 5. d3 O-O 6. O-O d6 7. Nd5 h6 8. Be3 Bg4 9. c3 Bxe3 10. fxe3 Nxd5 11. Bxd5 B@b6 12. d4 exd4 13. cxd4 P@e6 14. Bb3 Bxf3 15. Qxf3 Nxd4 16. exd4 Bxd4+ 17. Kh1 N@g5 18. Qh5 P@g6 19. Qe2 P@h3 20. gxh3 Nxh3 21. B@g3 P@h4 22. B@g2 hxg3 23. Bxh3 N@f2+ 24. Rxf2 gxf2 25. P@e3 R@g1+ 26. Rxg1 fxg1=Q+ 27. Kxg1 Bxe3+ 28. Qxe3 B@b6 29. P@d4 P@e5 30. P@c3 R@g5+ 31. B@g2 exd4 32. cxd4 e5 33. P@f2 Bxd4 34. Qd3 Qf6 35. R@f1 P@g4 36. N@d5 Qxf2+ 37. Rxf2 Bxf2+ 38. Kxf2 R@f4+ 39. Nxf4 exf4 40. B@d4 P@e5 41. N@e7+ Kh7 42. N@d5 P@f6 43. Bxg4 Rxg4 44. Qe2 B@h4+ 45. Kf1 Rxg2 46. Qxg2 N@g5 47. P@f5 B@h3 48. Qxh3 Nxh3 49. B@f2 Bxf2 50. Bxf2 B@d3+ 51. B@e2 Bxe2+ 52. Kxe2 f3+ 53. Kxf3 Ng5+ 54. Ke2 B@f3+ 55. Kd2 Q@e2+ 56. Kc3 Qxf2 57. Nxg6 Nxe4+ 58. Kd3 Qd2+ 59. Kc4 Qd4+ 60. Kb5 P@a6+ 61. Ka5 B@b6+ { White resigns } 0-1 diff --git a/fuzz/corpus/pgn/stockfish-learning.pgn b/fuzz/corpus/pgn/stockfish-learning.pgn new file mode 100644 index 000000000..6504c8593 --- /dev/null +++ b/fuzz/corpus/pgn/stockfish-learning.pgn @@ -0,0 +1,66 @@ +[Event "?"] +[Site "?"] +[Date "2019.01.14"] +[Round "1"] +[White "Stockfish-reference"] +[Black "Stockfish-learning"] +[Result "1-0"] +[ECO "B00"] +[Opening "St. George (Baker) defense"] +[GameDuration "00:40:54"] +[GameEndTime "2019-01-14T07:27:30.802 PST"] +[GameStartTime "2019-01-14T06:46:35.929 PST"] +[PlyCount "154"] +[Termination "adjudication"] +[TimeControl "900+5"] + +1. e4 {book} a6 {book} 2. c4 {book} e5 {0.00/31 48s} 3. Nf3 {+0.69/29 36s} +Nc6 {0.00/31 12s} 4. Nc3 {+0.56/28 14s} Nf6 {0.00/30 9.3s} 5. a3 {+0.50/28 29s} +d6 {+0.12/28 21s} 6. Be2 {+0.43/26 16s} Bg4 {+0.14/25 8.8s} 7. d3 {+0.43/28 34s} +g6 {0.00/28 14s} 8. O-O {+0.30/28 20s} Bg7 {+0.15/31 47s} 9. b4 {+0.24/27 13s} +O-O {0.00/26 14s} 10. Rb1 {+0.45/30 24s} Nd7 {-0.03/29 35s} +11. Bg5 {+0.68/25 6.8s} Bf6 {0.00/27 7.9s} 12. Be3 {+0.55/29 16s} +Bg7 {-0.05/30 44s} 13. Bg5 {+0.61/29 26s} Bf6 {0.00/32 8.7s} +14. Be3 {+0.51/29 11s} Bg7 {0.00/34 16s} 15. Qd2 {+0.50/30 22s} a5 {0.00/32 67s} +16. b5 {+0.45/29 7.9s} Bxf3 {0.00/32 6.0s} 17. Bxf3 {+0.51/32 9.4s} +Nd4 {0.00/34 8.9s} 18. Bd1 {+0.66/31 9.5s} Nc5 {-0.09/35 18s} +19. Bxd4 {+0.65/31 12s} exd4 {-0.32/34 37s} 20. Na4 {+0.83/32 29s} +Nd7 {-0.27/32 28s} 21. f4 {+0.78/32 27s} h5 {-0.35/32 21s} +22. Bf3 {+0.85/29 12s} Rb8 {-0.33/29 24s} 23. g3 {+0.81/30 30s} +b6 {-0.48/29 21s} 24. Bg2 {+0.88/29 21s} Nf6 {-0.58/33 46s} +25. Nb2 {+1.03/34 9.1s} Ng4 {-0.58/36 19s} 26. Nd1 {+1.03/34 11s} +Qe7 {-0.58/38 16s} 27. Bf3 {+1.03/37 13s} Nf6 {-0.58/38 7.6s} +28. Nf2 {+1.03/38 12s} Nd7 {-0.58/39 26s} 29. Bg2 {+1.03/38 17s} +Nc5 {-0.58/38 11s} 30. h4 {+1.03/38 15s} Rbe8 {-0.67/38 71s} +31. Kh2 {+1.03/39 15s} Qd8 {-0.67/34 18s} 32. Rbe1 {+1.03/35 12s} +Nd7 {-0.77/38 125s} 33. Qd1 {+1.03/36 16s} Nc5 {-0.87/34 30s} +34. Nh3 {+1.03/37 13s} Kh8 {-0.77/30 14s} 35. Qe2 {+1.03/37 25s} +Nd7 {-0.77/33 5.2s} 36. Ng5 {+1.13/35 44s} Bh6 {-0.77/33 7.8s} +37. Qf2 {+1.24/34 15s} Bg7 {-0.77/34 7.1s} 38. Rd1 {+1.42/31 14s} +Nc5 {-0.87/34 46s} 39. Kh1 {+1.33/32 24s} Kg8 {-0.77/32 9.1s} +40. Nf3 {+1.38/30 9.7s} Nb3 {-0.77/30 4.8s} 41. Bh3 {+1.68/28 14s} +Kh8 {-1.10/31 53s} 42. Rg1 {+1.86/32 46s} Nc5 {-1.01/27 5.0s} +43. g4 {+1.73/30 12s} Qf6 {-1.04/28 18s} 44. Ng5 {+1.78/31 21s} +hxg4 {-1.53/28 8.1s} 45. Rxg4 {+1.87/29 9.6s} Bh6 {-1.45/26 6.1s} +46. Qg3 {+1.86/29 15s} Rg8 {-1.50/29 15s} 47. Rg1 {+1.71/32 44s} +Rg7 {-1.32/29 4.7s} 48. h5 {+1.86/29 6.2s} Kg8 {-1.41/28 6.2s} +49. hxg6 {+1.46/32 27s} fxg6 {-1.25/28 7.8s} 50. Qh4 {+1.61/33 20s} +Bxg5 {-1.42/29 6.6s} 51. Rxg5 {+1.61/35 7.0s} Ne6 {-1.19/28 4.8s} +52. Bxe6+ {+1.61/38 10.0s} Rxe6 {-1.39/32 13s} 53. Kg2 {+1.61/39 15s} +Rh7 {-1.49/29 8.5s} 54. Qg4 {+1.61/41 9.0s} Kf8 {-2.00/31 19s} +55. Rh1 {+2.24/32 7.8s} Rxh1 {-2.01/28 2.5s} 56. Kxh1 {+2.36/35 12s} +Qh8+ {-1.93/29 5.9s} 57. Kg2 {+2.41/36 9.8s} Kf7 {-1.57/29 1.9s} +58. Qf3 {+2.46/37 14s} Rf6 {-2.27/33 13s} 59. e5 {+2.50/33 9.1s} +dxe5 {-2.25/30 2.5s} 60. Qd5+ {+2.87/33 18s} Kg7 {-2.43/30 4.3s} +61. Qd7+ {+2.92/33 7.1s} Rf7 {-2.55/31 5.7s} 62. Qe6 {+3.02/32 8.4s} +Qh6 {-2.48/28 3.7s} 63. Qxe5+ {+3.14/32 7.6s} Kh7 {-2.24/27 1.7s} +64. Qe4 {+3.13/35 13s} Qg7 {-2.39/30 8.2s} 65. Re5 {+3.45/32 6.9s} +Qf8 {-2.65/30 10s} 66. Re8 {+3.78/31 9.5s} Qc5 {-2.65/30 5.5s} +67. Kg3 {+3.99/31 7.7s} a4 {-2.92/31 5.2s} 68. Rd8 {+4.10/31 7.1s} +Re7 {-3.27/29 5.0s} 69. Qxd4 {+4.37/32 9.1s} Qxd4 {-3.28/27 2.5s} +70. Rxd4 {+4.41/29 7.3s} Kg7 {-3.62/31 7.5s} 71. Re4 {+5.72/31 7.3s} +Rd7 {-4.53/29 5.0s} 72. d4 {+6.17/33 8.0s} Rd8 {-5.41/32 5.0s} +73. Kg4 {+6.31/39 6.0s} Kf7 {-5.92/29 5.0s} 74. d5 {+7.67/39 36s} +Rh8 {-6.63/29 5.0s} 75. Re6 {+7.97/28 6.8s} Rh1 {-7.12/26 5.0s} +76. Rc6 {+8.52/27 8.6s} Rg1+ {-7.80/24 5.0s} 77. Kf3 {+8.86/27 6.4s} +Rf1+ {-8.11/24 5.0s, White wins by adjudication} 1-0 diff --git a/fuzz/engine.py b/fuzz/engine.py new file mode 100644 index 000000000..ad0cb9fe9 --- /dev/null +++ b/fuzz/engine.py @@ -0,0 +1,53 @@ +import asyncio +import logging + +import chess.engine + +from pythonfuzz.main import PythonFuzz + + +logging.getLogger("chess.engine").setLevel(logging.CRITICAL) + + +@PythonFuzz +def fuzz(buf): + lines = buf.split(b"!") + + class FuzzTransport: + def __init__(self, protocol): + self.protocol = protocol + self.protocol.connection_made(self) + + def get_pipe_transport(self, fd): + assert fd == 0, f"expected 0 for stdin, got {fd}" + return self + + def write(self, data): + if lines: + self.protocol.pipe_data_received(1, lines.pop(0)) + + def get_pid(self) -> int: + return id(self) + + def get_returncode(self): + return 0 + + async def main(): + protocol = chess.engine.UciProtocol() + transport = FuzzTransport(protocol) + await asyncio.wait_for(protocol.initialize(), 0.1) + await asyncio.wait_for(protocol.ping(), 0.1) + await asyncio.wait_for(protocol.analysis(chess.Board(), chess.engine.Limit(nodes=1)), 0.1) + + try: + asyncio.run(main()) + except asyncio.TimeoutError: + pass + except UnicodeDecodeError: + pass + except chess.engine.EngineError: + pass + + +if __name__ == "__main__": + fuzz() diff --git a/fuzz/epd.py b/fuzz/epd.py new file mode 100644 index 000000000..93bc01f99 --- /dev/null +++ b/fuzz/epd.py @@ -0,0 +1,25 @@ +import chess + +from pythonfuzz.main import PythonFuzz + + +@PythonFuzz +def fuzz(buf): + try: + epd = buf.decode("utf-8") + except UnicodeDecodeError: + pass + else: + try: + board, ops = chess.Board.from_epd(epd) + except ValueError as err: + pass + else: + sanitized_epd = board.epd(**ops) + sanitized_board, sanitized_ops = chess.Board.from_epd(sanitized_epd) + assert board == sanitized_board + assert ops == sanitized_ops, (ops, sanitized_ops) + + +if __name__ == "__main__": + fuzz() diff --git a/fuzz/fen.py b/fuzz/fen.py new file mode 100644 index 000000000..72a7a5f40 --- /dev/null +++ b/fuzz/fen.py @@ -0,0 +1,26 @@ +import chess + +from pythonfuzz.main import PythonFuzz + + +@PythonFuzz +def fuzz(buf): + try: + fen = buf.decode("utf-8") + except UnicodeDecodeError: + pass + else: + try: + board = chess.Board(fen) + except ValueError: + pass + else: + sanitized_fen = board.fen() + board.status() + list(board.legal_moves) + sanitized_board = chess.Board(sanitized_fen) + assert sanitized_board.fen() == sanitized_fen + + +if __name__ == "__main__": + fuzz() diff --git a/fuzz/pgn.py b/fuzz/pgn.py new file mode 100644 index 000000000..5bd1ccb8a --- /dev/null +++ b/fuzz/pgn.py @@ -0,0 +1,31 @@ +import io +import logging + +import chess.pgn + +from pythonfuzz.main import PythonFuzz + + +# The default parser logs errors for syntax errors. +logging.getLogger("chess.pgn").setLevel(logging.CRITICAL) + + +@PythonFuzz +def fuzz(buf): + try: + pgn = io.StringIO(buf.decode("utf-8")) + except UnicodeDecodeError: + pass + else: + while True: + game = chess.pgn.read_game(pgn) + if game is None: + break + + repr(game) + if not game.errors: + str(game) + + +if __name__ == "__main__": + fuzz() diff --git a/python-chess-stub/README.rst b/python-chess-stub/README.rst new file mode 100644 index 000000000..f672aeb9a --- /dev/null +++ b/python-chess-stub/README.rst @@ -0,0 +1,10 @@ +python-chess +============ + +Package moved +------------- + +This package has been moved to https://pypi.org/project/chess/. + +Thanks to `Kristian Glass `_ for making +the package name available. diff --git a/python-chess-stub/setup.py b/python-chess-stub/setup.py new file mode 100755 index 000000000..a0c4ef0d6 --- /dev/null +++ b/python-chess-stub/setup.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import setuptools + +setuptools.setup( + name="python-chess", + version="1.999", + author="Niklas Fiekas", + author_email="niklas.fiekas@backscattering.de", + description="A chess library with move generation, move validation, and support for common formats.", + long_description=open(os.path.join(os.path.dirname(__file__), "README.rst")).read(), + long_description_content_type="text/x-rst", + license="GPL-3.0+", + keywords="chess fen epd pgn polyglot syzygy gaviota uci xboard", + url="https://github.com/niklasf/python-chess", + packages=[], + install_requires=["chess>=1,<2"], + classifiers=[ + "Development Status :: 7 - Inactive", + ], + project_urls={ + "Documentation": "https://python-chess.readthedocs.io", + }, +) diff --git a/release.py b/release.py index 35f5d7a23..4057ab785 100755 --- a/release.py +++ b/release.py @@ -4,11 +4,7 @@ import os import chess import sys -import zipfile -import textwrap -import configparser -import requests -import bs4 +import subprocess def system(command): @@ -23,6 +19,12 @@ def check_git(): system("git diff --exit-code") system("git diff --cached --exit-code") + system("git fetch origin") + behind = int(subprocess.check_output(["git", "rev-list", "--count", "master..origin/master"])) + if behind > 0: + print(f"master is {behind} commit(s) behind origin/master") + sys.exit(1) + def test(): print("--- TEST ---------------------------------------------------------") @@ -34,13 +36,13 @@ def check_changelog(): with open("CHANGELOG.rst", "r") as changelog_file: changelog = changelog_file.read() - if "Upcoming in the next release" in changelog: - print("Found: Upcoming in the next release") + if "Upcoming in" in changelog: + print("Found: Upcoming in") sys.exit(1) - tagname = "v{0}".format(chess.__version__) + tagname = f"v{chess.__version__}" if tagname not in changelog: - print("Not found: {0}".format(tagname)) + print(f"Not found: {tagname}") sys.exit(1) @@ -51,15 +53,15 @@ def check_docs(): def tag_and_push(): print("--- TAG AND PUSH -------------------------------------------------") - tagname = "v{0}".format(chess.__version__) - release_filename = "release-{0}.txt".format(tagname) + tagname = f"v{chess.__version__}" + release_filename = f"release-{tagname}.txt" if not os.path.exists(release_filename): - print(">>> Creating {0} ...".format(release_filename)) + print(f">>> Creating {release_filename} ...") first_section = False prev_line = None with open(release_filename, "w") as release_txt, open("CHANGELOG.rst", "r") as changelog_file: - headline = "python-chess {0}".format(tagname) + headline = f"python-chess {tagname}" release_txt.write(headline + os.linesep) for line in changelog_file: @@ -84,35 +86,32 @@ def tag_and_push(): guessed_tagname = input(">>> Sure? Confirm tagname: ") if guessed_tagname != tagname: - print("Actual tagname is: {0}".format(tagname)) + print(f"Actual tagname is: {tagname}") sys.exit(1) - system("git tag {0} -s -F {1}".format(tagname, release_filename)) - system("git push origin master {0}".format(tagname)) + system(f"git tag {tagname} -s -F {release_filename}") + system(f"git push --atomic origin master {tagname}") return tagname -def update_rtd(): - print("--- UPDATE RTD ---------------------------------------------------") - system("curl -X POST http://readthedocs.org/build/python-chess") - - def pypi(): print("--- PYPI ---------------------------------------------------------") - system("python3 setup.py sdist upload") + system("rm -rf build") + system("python3 setup.py sdist") + system("twine check dist/*") + system("twine upload --skip-existing dist/*") def github_release(tagname): print("--- GITHUB RELEASE -----------------------------------------------") - print("https://github.com/niklasf/python-chess/releases/new?tag={0}".format(tagname)) + print(f"https://github.com/niklasf/python-chess/releases/new?tag={tagname}") if __name__ == "__main__": check_docs() test() - check_changelog() check_git() + check_changelog() tagname = tag_and_push() - update_rtd() pypi() github_release(tagname) diff --git a/setup.py b/setup.py index 2aa9ba610..5815947cd 100755 --- a/setup.py +++ b/setup.py @@ -1,27 +1,32 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- -# -# This file is part of the python-chess library. -# Copyright (C) 2012-2017 Niklas Fiekas -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -import chess import os -import setuptools -import sys import platform +import re +import sys +import textwrap + +import setuptools + + +if sys.version_info < (3, ): + raise ImportError(textwrap.dedent("""\ + You are trying to install python-chess on Python 2. + + The last compatible branch was 0.23.x, which was supported until the + end of 2018. Consider upgrading to Python 3. + """)) + +if sys.version_info < (3, 8): + raise ImportError(textwrap.dedent("""\ + You are trying to install python-chess. + + Since version 1.11.0, python-chess requires Python 3.8 or later. + Since version 1.0.0, python-chess requires Python 3.7 or later. + """)) + +import chess def read_description(): @@ -29,92 +34,64 @@ def read_description(): Reads the description from README.rst and substitutes mentions of the latest version with a concrete version number. """ - description = open(os.path.join(os.path.dirname(__file__), "README.rst")).read() + with open(os.path.join(os.path.dirname(__file__), "README.rst"), encoding="utf-8") as f: + description = f.read() # Link to the documentation of the specific version. description = description.replace( "//python-chess.readthedocs.io/en/latest/", - "//python-chess.readthedocs.io/en/v{0}/".format(chess.__version__)) + "//python-chess.readthedocs.io/en/v{}/".format(chess.__version__)) # Use documentation badge for the specific version. description = description.replace( "//readthedocs.org/projects/python-chess/badge/?version=latest", - "//readthedocs.org/projects/python-chess/badge/?version=v{0}".format(chess.__version__)) + "//readthedocs.org/projects/python-chess/badge/?version=v{}".format(chess.__version__)) - # Show Travis CI build status of the concrete version. - description = description.replace( - "//travis-ci.org/niklasf/python-chess.svg?branch=master", - "//travis-ci.org/niklasf/python-chess.svg?branch=v{0}".format(chess.__version__)) + # Remove doctest comments. + description = re.sub(r"\s*# doctest:.*", "", description) return description -def dependencies(): - deps = [] - - if sys.version_info < (2, 7): - deps.append("backport_collections") - - return deps - - -def extra_dependencies(): - extras = {} - - if sys.version_info < (3, 2): - extras["engine"] = ["futures"] - else: - extras["engine"] = [] - - if platform.python_implementation() == "CPython": - if sys.version_info < (3, 3): - extras["gaviota"] = ["backports.lzma"] - else: - extras["gaviota"] = [] - - extras["test"] = extras["engine"] + extras.get("gaviota", []) - - if sys.version_info < (2, 7): - extras["test"].append("unittest2") - - if platform.python_implementation() == "CPython": - extras["test"].append("spur") - - return extras - - setuptools.setup( - name="python-chess", + name="chess", version=chess.__version__, author=chess.__author__, author_email=chess.__email__, description=chess.__doc__.replace("\n", " ").strip(), long_description=read_description(), - license="GPL3", - keywords="chess fen pgn polyglot syzygy gaviota uci xboard", + long_description_content_type="text/x-rst", + license="GPL-3.0+", + keywords="chess fen epd pgn polyglot syzygy gaviota uci xboard", url="https://github.com/niklasf/python-chess", packages=["chess"], test_suite="test", - python_requires=">=2.6,!=3.0.*,!=3.1.*,!=3.2.*", - install_requires=dependencies(), - extras_require=extra_dependencies(), - tests_require=extra_dependencies().get("test"), + zip_safe=False, # For mypy + package_data={ + "chess": ["py.typed"], + }, + python_requires=">=3.8", classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.6", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Games/Entertainment :: Board Games", + "Topic :: Games/Entertainment :: Turn Based Strategy", "Topic :: Software Development :: Libraries :: Python Modules", + "Typing :: Typed", ], + project_urls={ + "Documentation": "https://python-chess.readthedocs.io", + }, + obsoletes=["python_chess"], ) diff --git a/test.py b/test.py index 3ac3dc009..228c62f3f 100755 --- a/test.py +++ b/test.py @@ -1,59 +1,30 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# This file is part of the python-chess library. -# Copyright (C) 2012-2017 Niklas Fiekas -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . +#!/usr/bin/env python3 -import chess -import chess.polyglot -import chess.pgn -import chess.uci -import chess.xboard -import chess.syzygy -import chess.gaviota -import chess.variant +import asyncio import copy +import logging import os import os.path -import textwrap -import sys -import time -import threading -import logging import platform +import sys +import tempfile +import textwrap +import unittest +import io -try: - from collections import OrderedDict -except ImportError: - from backport_collections import OrderedDict # Python 2.6 - -try: - import unittest2 as unittest # Python 2.6 -except ImportError: - import unittest - -try: - from StringIO import StringIO # Python 2 -except ImportError: - from io import StringIO # Python 3 +import chess +import chess.gaviota +import chess.engine +import chess.pgn +import chess.polyglot +import chess.svg +import chess.syzygy +import chess.variant class RaiseLogHandler(logging.StreamHandler): def handle(self, record): - super(RaiseLogHandler, self).handle(record) + super().handle(record) raise RuntimeError("was expecting no log messages") @@ -74,7 +45,7 @@ def test_square(self): for square in chess.SQUARES: file_index = chess.square_file(square) rank_index = chess.square_rank(square) - self.assertEqual(chess.square(file_index, rank_index), square, chess.SQUARE_NAMES[square]) + self.assertEqual(chess.square(file_index, rank_index), square, chess.square_name(square)) def test_shifts(self): shifts = [ @@ -99,6 +70,38 @@ def test_shifts(self): self.assertLessEqual(c, 1) self.assertEqual(c, chess.popcount(shifted & chess.BB_ALL)) + def test_parse_square(self): + self.assertEqual(chess.parse_square("a1"), 0) + with self.assertRaises(ValueError): + self.assertEqual(chess.parse_square("A1")) + with self.assertRaises(ValueError): + self.assertEqual(chess.parse_square("a0")) + + def test_square_distance(self): + self.assertEqual(chess.square_distance(chess.A1, chess.A1), 0) + self.assertEqual(chess.square_distance(chess.A1, chess.H8), 7) + self.assertEqual(chess.square_distance(chess.E1, chess.E8), 7) + self.assertEqual(chess.square_distance(chess.A4, chess.H4), 7) + self.assertEqual(chess.square_distance(chess.D4, chess.E5), 1) + + def test_square_manhattan_distance(self): + self.assertEqual(chess.square_manhattan_distance(chess.A1, chess.A1), 0) + self.assertEqual(chess.square_manhattan_distance(chess.A1, chess.H8), 14) + self.assertEqual(chess.square_manhattan_distance(chess.E1, chess.E8), 7) + self.assertEqual(chess.square_manhattan_distance(chess.A4, chess.H4), 7) + self.assertEqual(chess.square_manhattan_distance(chess.D4, chess.E5), 2) + + def test_square_knight_distance(self): + self.assertEqual(chess.square_knight_distance(chess.A1, chess.A1), 0) + self.assertEqual(chess.square_knight_distance(chess.A1, chess.H8), 6) + self.assertEqual(chess.square_knight_distance(chess.G1, chess.F3), 1) + self.assertEqual(chess.square_knight_distance(chess.E1, chess.E8), 5) + self.assertEqual(chess.square_knight_distance(chess.A4, chess.H4), 5) + self.assertEqual(chess.square_knight_distance(chess.A1, chess.B1), 3) + self.assertEqual(chess.square_knight_distance(chess.A1, chess.C3), 4) + self.assertEqual(chess.square_knight_distance(chess.A1, chess.B2), 4) + self.assertEqual(chess.square_knight_distance(chess.C1, chess.B2), 2) + class MoveTestCase(unittest.TestCase): @@ -123,20 +126,28 @@ def test_uci_parsing(self): self.assertEqual(chess.Move.from_uci("e7e8q").uci(), "e7e8q") self.assertEqual(chess.Move.from_uci("P@e4").uci(), "P@e4") self.assertEqual(chess.Move.from_uci("B@f4").uci(), "B@f4") + self.assertEqual(chess.Move.from_uci("0000").uci(), "0000") def test_invalid_uci(self): - with self.assertRaises(ValueError): + with self.assertRaises(chess.InvalidMoveError): chess.Move.from_uci("") - with self.assertRaises(ValueError): + with self.assertRaises(chess.InvalidMoveError): chess.Move.from_uci("N") - with self.assertRaises(ValueError): + with self.assertRaises(chess.InvalidMoveError): chess.Move.from_uci("z1g3") - with self.assertRaises(ValueError): + with self.assertRaises(chess.InvalidMoveError): chess.Move.from_uci("Q@g9") + def test_xboard_move(self): + self.assertEqual(chess.Move.from_uci("b5c7").xboard(), "b5c7") + self.assertEqual(chess.Move.from_uci("e7e8q").xboard(), "e7e8q") + self.assertEqual(chess.Move.from_uci("P@e4").xboard(), "P@e4") + self.assertEqual(chess.Move.from_uci("B@f4").xboard(), "B@f4") + self.assertEqual(chess.Move.from_uci("0000").xboard(), "@@@@") + def test_copy(self): a = chess.Move.from_uci("N@f3") b = chess.Move.from_uci("a1h8") @@ -155,6 +166,8 @@ def test_equality(self): d1 = chess.Piece(chess.BISHOP, chess.WHITE) d2 = chess.Piece(chess.BISHOP, chess.WHITE) + self.assertEqual(len(set([a, b, c, d1, d2])), 3) + self.assertEqual(a, d1) self.assertEqual(d1, a) self.assertEqual(d1, d2) @@ -178,12 +191,20 @@ def test_from_symbol(self): self.assertEqual(white_knight.color, chess.WHITE) self.assertEqual(white_knight.piece_type, chess.KNIGHT) self.assertEqual(white_knight.symbol(), "N") + self.assertEqual(str(white_knight), "N") black_queen = chess.Piece.from_symbol("q") self.assertEqual(black_queen.color, chess.BLACK) self.assertEqual(black_queen.piece_type, chess.QUEEN) self.assertEqual(black_queen.symbol(), "q") + self.assertEqual(str(black_queen), "q") + + def test_hash(self): + pieces = {chess.Piece.from_symbol(symbol) for symbol in "pnbrqkPNBRQK"} + self.assertEqual(len(pieces), 12) + hashes = {hash(piece) for piece in pieces} + self.assertEqual(hashes, set(range(12))) class BoardTestCase(unittest.TestCase): @@ -199,6 +220,18 @@ def test_empty(self): self.assertEqual(board.fen(), "8/8/8/8/8/8/8/8 w - - 0 1") self.assertEqual(board, chess.Board(None)) + def test_ply(self): + board = chess.Board() + self.assertEqual(board.ply(), 0) + board.push_san("d4") + self.assertEqual(board.ply(), 1) + board.push_san("d5") + self.assertEqual(board.ply(), 2) + board.clear_stack() + self.assertEqual(board.ply(), 2) + board.push_san("Nf3") + self.assertEqual(board.ply(), 3) + def test_from_epd(self): base_epd = "rnbqkb1r/ppp1pppp/5n2/3P4/8/8/PPPP1PPP/RNBQKBNR w KQkq -" board, ops = chess.Board.from_epd(base_epd + " ce 55;") @@ -249,11 +282,17 @@ def test_xfen(self): board = chess.Board(fen) self.assertEqual(board.fen(), fen) - # Illegal en passant square in illegal board. + # Illegal en passant square on illegal board. fen = "1r6/8/8/pP6/8/8/8/1K6 w - a6 0 1" board = chess.Board(fen) self.assertEqual(board.fen(), "1r6/8/8/pP6/8/8/8/1K6 w - - 0 1") + def test_fen_en_passant(self): + board = chess.Board() + board.push_san("e4") + self.assertEqual(board.fen(en_passant="fen"), "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1") + self.assertEqual(board.fen(en_passant="xfen"), "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1") + def test_get_set(self): board = chess.Board() self.assertEqual(board.piece_at(chess.B1), chess.Piece.from_symbol("N")) @@ -267,10 +306,23 @@ def test_get_set(self): board.set_piece_at(chess.F1, None) self.assertEqual(board.piece_at(chess.F1), None) + board.set_piece_at(chess.H7, chess.Piece.from_symbol("Q"), promoted=True) + self.assertEqual(board.promoted, chess.BB_H7) + + board.set_piece_at(chess.H7, None) + self.assertEqual(board.promoted, chess.BB_EMPTY) + self.assertEqual(board.piece_at(chess.H7), None) + + def test_color_at(self): + board = chess.Board() + self.assertEqual(board.color_at(chess.A1), chess.WHITE) + self.assertEqual(board.color_at(chess.G7), chess.BLACK) + self.assertEqual(board.color_at(chess.E4), None) + def test_pawn_captures(self): board = chess.Board() - # Kings gambit. + # King's Gambit. board.push(chess.Move.from_uci("e2e4")) board.push(chess.Move.from_uci("e7e5")) board.push(chess.Move.from_uci("f2f4")) @@ -299,15 +351,17 @@ def test_castling(self): board = chess.Board("r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 1 1") # Let white castle short. - move = board.parse_san("O-O") + move = board.parse_xboard("O-O") self.assertEqual(move, chess.Move.from_uci("e1g1")) self.assertEqual(board.san(move), "O-O") + self.assertEqual(board.xboard(move), "e1g1") self.assertIn(move, board.legal_moves) board.push(move) # Let black castle long. - move = board.parse_san("O-O-O") + move = board.parse_xboard("O-O-O") self.assertEqual(board.san(move), "O-O-O") + self.assertEqual(board.xboard(move), "e8c8") self.assertIn(move, board.legal_moves) board.push(move) self.assertEqual(board.fen(), "2kr3r/8/8/8/8/8/8/R4RK1 w - - 3 2") @@ -335,13 +389,22 @@ def test_castling(self): board.pop() self.assertEqual(board.fen(), "r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 1 1") + def test_castling_san(self): + board = chess.Board("4k3/8/8/8/8/8/8/4K2R w K - 0 1") + self.assertEqual(board.parse_san("O-O"), chess.Move.from_uci("e1g1")) + with self.assertRaises(chess.IllegalMoveError): + board.parse_san("Kg1") + with self.assertRaises(chess.IllegalMoveError): + board.parse_san("Kh1") + def test_ninesixty_castling(self): fen = "3r1k1r/4pp2/8/8/8/8/8/4RKR1 w Gd - 1 1" board = chess.Board(fen, chess960=True) - # Let white do the king side swap. + # Let white do the kingside swap. move = board.parse_san("O-O") self.assertEqual(board.san(move), "O-O") + self.assertEqual(board.xboard(move), "O-O") self.assertEqual(move.from_square, chess.F1) self.assertEqual(move.to_square, chess.G1) self.assertIn(move, board.legal_moves) @@ -354,6 +417,7 @@ def test_ninesixty_castling(self): # Let black castle queenside. move = board.parse_san("O-O-O") self.assertEqual(board.san(move), "O-O-O") + self.assertEqual(board.xboard(move), "O-O-O") self.assertEqual(move.from_square, chess.F8) self.assertEqual(move.to_square, chess.D8) self.assertIn(move, board.legal_moves) @@ -384,18 +448,26 @@ def test_ninesixty_castling(self): board.pop() self.assertEqual(board.shredder_fen(), fen) + def test_hside_rook_blocks_aside_castling(self): + board = chess.Board("4rrk1/pbbp2p1/1ppnp3/3n1pqp/3N1PQP/1PPNP3/PBBP2P1/4RRK1 w Ff - 10 18", chess960=True) + self.assertNotIn(chess.Move.from_uci("g1f1"), board.legal_moves) + self.assertNotIn(chess.Move.from_uci("g1e1"), board.legal_moves) + self.assertNotIn(chess.Move.from_uci("g1c1"), board.legal_moves) + self.assertNotIn(chess.Move.from_uci("g1a1"), board.legal_moves) + self.assertIn(chess.Move.from_uci("g1h1"), board.legal_moves) # Kh1 + def test_selective_castling(self): board = chess.Board("r3k2r/pppppppp/8/8/8/8/PPPPPPPP/R3K2R w KQkq - 0 1") - # King not selected + # King not selected. self.assertFalse(any(board.generate_castling_moves(chess.BB_ALL & ~board.kings))) - # Rook on h1 not selected + # Rook on h1 not selected. moves = board.generate_castling_moves(chess.BB_ALL, chess.BB_ALL & ~chess.BB_H1) self.assertEqual(len(list(moves)), 1) def test_castling_right_not_destroyed_bug(self): - # A rook move from H8 to H1 was only taking whites possible castling + # A rook move from h8 to h1 was only taking white's possible castling # rights away. board = chess.Board("2r1k2r/2qbbpp1/p2pp3/1p3PP1/Pn2P3/1PN1B3/1P3QB1/1K1R3R b k - 0 22") board.push_san("Rxh1") @@ -420,9 +492,9 @@ def test_invalid_castling_rights(self): self.assertEqual(board.fen(), "1r2k3/8/1p6/8/8/5P2/8/1R2KR2 w KQq - 0 1") def test_ninesixty_different_king_and_rook_file(self): - # Theoretically this position (with castling rights) can not be reached + # Theoretically, this position (with castling rights) can not be reached # with a series of legal moves from one of the 960 starting positions. - # Decision: We don't care. Neither does Stockfish or lichess.org. + # Decision: We don't care, neither do Stockfish or lichess.org. fen = "1r1k1r2/5p2/8/8/8/8/3N4/R5KR b KQkq - 0 1" board = chess.Board(fen, chess960=True) self.assertEqual(board.fen(), fen) @@ -431,18 +503,92 @@ def test_ninesixty_prevented_castle(self): board = chess.Board("4k3/8/8/1b6/8/8/8/5RKR w KQ - 0 1", chess960=True) self.assertFalse(board.is_legal(chess.Move.from_uci("g1f1"))) - def test_insufficient_material(self): - # Starting position. - board = chess.Board() - self.assertFalse(board.is_insufficient_material()) + def test_find_move(self): + board = chess.Board("4k3/1P6/8/8/8/8/3P4/4K2R w K - 0 1") - # King vs. King + 2 bishops of the same color. - board = chess.Board("k1K1B1B1/8/8/8/8/8/8/8 w - - 7 32") - self.assertTrue(board.is_insufficient_material()) + # Pawn moves. + self.assertEqual(board.find_move(chess.D2, chess.D4), chess.Move.from_uci("d2d4")) + self.assertEqual(board.find_move(chess.B7, chess.B8), chess.Move.from_uci("b7b8q")) + self.assertEqual(board.find_move(chess.B7, chess.B8, chess.KNIGHT), chess.Move.from_uci("b7b8n")) - # Add bishop of opposite color for the weaker side. - board.set_piece_at(chess.B8, chess.Piece.from_symbol("b")) - self.assertFalse(board.is_insufficient_material()) + # Illegal moves. + with self.assertRaises(chess.IllegalMoveError): + board.find_move(chess.D2, chess.D8) + with self.assertRaises(chess.IllegalMoveError): + board.find_move(chess.E1, chess.A1) + + # Castling. + self.assertEqual(board.find_move(chess.E1, chess.G1), chess.Move.from_uci("e1g1")) + self.assertEqual(board.find_move(chess.E1, chess.H1), chess.Move.from_uci("e1g1")) + board.chess960 = True + self.assertEqual(board.find_move(chess.E1, chess.H1), chess.Move.from_uci("e1h1")) + + def test_clean_castling_rights(self): + board = chess.Board() + board.set_board_fen("k6K/8/8/pppppppp/8/8/8/QqQq4") + self.assertEqual(board.clean_castling_rights(), chess.BB_EMPTY) + self.assertEqual(board.fen(), "k6K/8/8/pppppppp/8/8/8/QqQq4 w - - 0 1") + board.push_san("Qxc5") + self.assertEqual(board.clean_castling_rights(), chess.BB_EMPTY) + self.assertEqual(board.fen(), "k6K/8/8/ppQppppp/8/8/8/Qq1q4 b - - 0 1") + + def test_insufficient_material(self): + def _check(board, white, black): + self.assertEqual(board.has_insufficient_material(chess.WHITE), white) + self.assertEqual(board.has_insufficient_material(chess.BLACK), black) + self.assertEqual(board.is_insufficient_material(), white and black) + + # Imperfect implementation. + false_negative = False + + _check(chess.Board(), False, False) + _check(chess.Board("k1K1B1B1/8/8/8/8/8/8/8 w - - 7 32"), True, True) + _check(chess.Board("kbK1B1B1/8/8/8/8/8/8/8 w - - 7 32"), False, False) + _check(chess.Board("8/5k2/8/8/8/8/3K4/8 w - - 0 1"), True, True) + _check(chess.Board("8/3k4/8/8/2N5/8/3K4/8 b - - 0 1"), True, True) + _check(chess.Board("8/4rk2/8/8/8/8/3K4/8 w - - 0 1"), True, False) + _check(chess.Board("8/4qk2/8/8/8/8/3K4/8 w - - 0 1"), True, False) + _check(chess.Board("8/4bk2/8/8/8/8/3KB3/8 w - - 0 1"), False, False) + _check(chess.Board("8/8/3Q4/2bK4/B7/8/1k6/8 w - - 1 68"), False, False) + _check(chess.Board("8/5k2/8/8/8/4B3/3K1B2/8 w - - 0 1"), True, True) + _check(chess.Board("5K2/8/8/1B6/8/k7/6b1/8 w - - 0 39"), True, True) + _check(chess.Board("8/8/8/4k3/5b2/3K4/8/2B5 w - - 0 33"), True, True) + _check(chess.Board("3b4/8/8/6b1/8/8/R7/K1k5 w - - 0 1"), False, True) + + _check(chess.variant.AtomicBoard("8/3k4/8/8/2N5/8/3K4/8 b - - 0 1"), True, True) + _check(chess.variant.AtomicBoard("8/4rk2/8/8/8/8/3K4/8 w - - 0 1"), True, True) + _check(chess.variant.AtomicBoard("8/4qk2/8/8/8/8/3K4/8 w - - 0 1"), True, False) + _check(chess.variant.AtomicBoard("8/1k6/8/2n5/8/3NK3/8/8 b - - 0 1"), False, False) + _check(chess.variant.AtomicBoard("8/4bk2/8/8/8/8/3KB3/8 w - - 0 1"), True, True) + _check(chess.variant.AtomicBoard("4b3/5k2/8/8/8/8/3KB3/8 w - - 0 1"), False, False) + _check(chess.variant.AtomicBoard("3Q4/5kKB/8/8/8/8/8/8 b - - 0 1"), False, True) + _check(chess.variant.AtomicBoard("8/5k2/8/8/8/8/5K2/4bb2 w - - 0 1"), True, False) + _check(chess.variant.AtomicBoard("8/5k2/8/8/8/8/5K2/4nb2 w - - 0 1"), True, False) + + _check(chess.variant.GiveawayBoard("8/4bk2/8/8/8/8/3KB3/8 w - - 0 1"), False, False) + _check(chess.variant.GiveawayBoard("4b3/5k2/8/8/8/8/3KB3/8 w - - 0 1"), False, False) + _check(chess.variant.GiveawayBoard("8/8/8/6b1/8/3B4/4B3/5B2 w - - 0 1"), True, True) + _check(chess.variant.GiveawayBoard("8/8/5b2/8/8/3B4/3B4/8 w - - 0 1"), True, False) + _check(chess.variant.SuicideBoard("8/5p2/5P2/8/3B4/1bB5/8/8 b - - 0 1"), false_negative, false_negative) + _check(chess.variant.AntichessBoard("8/8/8/1n2N3/8/8/8/8 w - - 0 32"), True, False) + _check(chess.variant.AntichessBoard("8/3N4/8/1n6/8/8/8/8 b - - 1 32"), True, False) + _check(chess.variant.AntichessBoard("6n1/8/8/4N3/8/8/8/8 b - - 0 27"), False, True) + _check(chess.variant.AntichessBoard("8/8/5n2/4N3/8/8/8/8 w - - 1 28"), False, True) + _check(chess.variant.AntichessBoard("8/3n4/8/8/8/8/8/8 w - - 0 29"), False, True) + + _check(chess.variant.KingOfTheHillBoard("8/5k2/8/8/8/8/3K4/8 w - - 0 1"), False, False) + + _check(chess.variant.RacingKingsBoard("8/5k2/8/8/8/8/3K4/8 w - - 0 1"), False, False) + + _check(chess.variant.ThreeCheckBoard("8/5k2/8/8/8/8/3K4/8 w - - 3+3 0 1"), True, True) + _check(chess.variant.ThreeCheckBoard("8/5k2/8/8/8/8/3K2N1/8 w - - 3+3 0 1"), False, True) + + _check(chess.variant.CrazyhouseBoard("8/5k2/8/8/8/8/3K2N1/8[] w - - 0 1"), True, True) + _check(chess.variant.CrazyhouseBoard("8/5k2/8/8/8/5B2/3KB3/8[] w - - 0 1"), False, False) + _check(chess.variant.CrazyhouseBoard("8/8/8/8/3k4/3N~4/3K4/8 w - - 0 1"), False, False) + + _check(chess.variant.HordeBoard("8/5k2/8/8/8/4NN2/8/8 w - - 0 1"), True, False) + _check(chess.variant.HordeBoard("8/1b5r/1P6/1Pk3q1/1PP5/r1P5/P1P5/2P5 b - - 0 52"), False, False) def test_promotion_with_check(self): board = chess.Board("8/6P1/2p5/1Pqk4/6P1/2P1RKP1/4P1P1/8 w - - 0 1") @@ -454,6 +600,13 @@ def test_promotion_with_check(self): board.push_san("d1=Q+") self.assertEqual(board.fen(), "8/8/8/3R1P2/8/2k2K2/8/r2q4 w - - 0 83") + def test_ambiguous_move(self): + board = chess.Board("8/8/1n6/3R1P2/1n6/2k2K2/3p4/r6r b - - 0 82") + with self.assertRaises(chess.AmbiguousMoveError): + board.parse_san("Rf1") + with self.assertRaises(chess.AmbiguousMoveError): + board.parse_san("Nd5") + def test_scholars_mate(self): board = chess.Board() @@ -539,7 +692,7 @@ def test_san(self): self.assertEqual(board.san(fxe6_mate_ep), "exd6#") self.assertEqual(board.fen(), fen) - # Test ambiguation. + # Test disambiguation. fen = "N3k2N/8/8/3N4/N4N1N/2R5/1R6/4K3 w - - 0 1" board = chess.Board(fen) self.assertEqual(board.san(chess.Move.from_uci("e1f1")), "Kf1") @@ -550,6 +703,11 @@ def test_san(self): self.assertEqual(board.san(chess.Move.from_uci("h4g6")), "Nh4g6") self.assertEqual(board.fen(), fen) + # Test a bug where shakmaty used overly specific disambiguation. + fen = "8/2KN1p2/5p2/3N1B1k/5PNp/7P/7P/8 w - -" + board = chess.Board(fen) + self.assertEqual(board.san(chess.Move.from_uci("d5f6")), "N5xf6#") + # Do not disambiguate illegal alternatives. fen = "8/8/8/R2nkn2/8/8/2K5/8 b - - 0 1" board = chess.Board(fen) @@ -563,16 +721,43 @@ def test_san(self): self.assertEqual(board.san(chess.Move.from_uci("f2f1n")), "f1=N+") self.assertEqual(board.fen(), fen) - def test_san_newline(self): - fen = "rnbqk2r/ppppppbp/5np1/8/8/5NP1/PPPPPPBP/RNBQK2R w KQkq - 2 4" + def test_lan(self): + # Normal moves always with origin square. + fen = "N3k2N/8/8/3N4/N4N1N/2R5/1R6/4K3 w - - 0 1" board = chess.Board(fen) + self.assertEqual(board.lan(chess.Move.from_uci("e1f1")), "Ke1-f1") + self.assertEqual(board.lan(chess.Move.from_uci("c3c2")), "Rc3-c2") + self.assertEqual(board.lan(chess.Move.from_uci("a4c5")), "Na4-c5") + self.assertEqual(board.fen(), fen) - with self.assertRaises(ValueError): - board.parse_san("O-O\n") + # Normal capture. + fen = "rnbq1rk1/ppp1bpp1/4pn1p/3p2B1/2PP4/2N1PN2/PP3PPP/R2QKB1R w KQ - 0 7" + board = chess.Board(fen) + self.assertEqual(board.lan(chess.Move.from_uci("g5f6")), "Bg5xf6") + self.assertEqual(board.fen(), fen) - with self.assertRaises(ValueError): + # Pawn captures and moves. + fen = "6bk/7b/8/3pP3/8/8/8/Q3K3 w - d6 0 2" + board = chess.Board(fen) + self.assertEqual(board.lan(chess.Move.from_uci("e5d6")), "e5xd6#") + self.assertEqual(board.lan(chess.Move.from_uci("e5e6")), "e5-e6+") + self.assertEqual(board.fen(), fen) + + def test_san_newline(self): + board = chess.Board("rnbqk2r/ppppppbp/5np1/8/8/5NP1/PPPPPPBP/RNBQK2R w KQkq - 2 4") + with self.assertRaises(chess.InvalidMoveError): + board.parse_san("O-O\n") + with self.assertRaises(chess.InvalidMoveError): board.parse_san("Nc3\n") + def test_pawn_capture_san_without_file(self): + board = chess.Board("2rq1rk1/pb2bppp/1p2p3/n1ppPn2/2PP4/PP3N2/1B1NQPPP/RB3RK1 b - - 4 13") + with self.assertRaises(chess.IllegalMoveError): + board.parse_san("c4") + board = chess.Board("4k3/8/8/4Pp2/8/8/8/4K3 w - f6 0 2") + with self.assertRaises(chess.IllegalMoveError): + board.parse_san("f6") + def test_variation_san(self): board = chess.Board() self.assertEqual('1. e4 e5 2. Nf3', @@ -600,13 +785,13 @@ def test_variation_san(self): illegal_variation = ['d3h7', 'g8h7', 'f3h6', 'h7g8'] board = chess.Board(fen) - with self.assertRaises(ValueError) as err: + with self.assertRaises(chess.IllegalMoveError) as err: board.variation_san([chess.Move.from_uci(m) for m in illegal_variation]) message = str(err.exception) self.assertIn('illegal move', message.lower(), - msg="Error [{0}] mentions illegal move".format(message)) + msg=f"Error [{message}] mentions illegal move") self.assertIn('f3h6', message, - msg="Illegal move f3h6 appears in message [{0}]".format(message)) + msg=f"Illegal move f3h6 appears in message [{message}]") def test_move_stack_usage(self): board = chess.Board() @@ -637,16 +822,22 @@ def test_is_legal_move(self): # Missing promotion. self.assertNotIn(chess.Move.from_uci("g7g8"), board.legal_moves) + # Promote to pawn or king. + self.assertFalse(board.is_legal(chess.Move.from_uci("g7g8p"))) + self.assertFalse(board.is_pseudo_legal(chess.Move.from_uci("g7g8p"))) + self.assertFalse(board.is_legal(chess.Move.from_uci("g7g8k"))) + self.assertFalse(board.is_pseudo_legal(chess.Move.from_uci("g7g8k"))) + self.assertEqual(board.fen(), fen) def test_move_count(self): board = chess.Board("1N2k3/P7/8/8/3n4/8/2PP4/R3K2R w KQ - 0 1") - self.assertEqual(len(board.pseudo_legal_moves), 8 + 4 + 3 + 2 + 1 + 6 + 9) + self.assertEqual(board.pseudo_legal_moves.count(), 8 + 4 + 3 + 2 + 1 + 6 + 9) def test_polyglot(self): - # Test polyglot compability using test data from + # Test Polyglot compatibility using test data from # http://hardy.uhasselt.be/Toga/book_format.html. Forfeiting castling - # rights should not reset the half move counter, though. + # rights should not reset the half-move counter, though. board = chess.Board() self.assertEqual(board.fen(), "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") @@ -713,7 +904,7 @@ def test_castling_move_generation_bug(self): # Unmake the move. board.pop() - # Generate all pseudo legal moves, two moves deep. + # Generate all pseudo-legal moves, two moves deep. for move in board.pseudo_legal_moves: board.push(move) for move in board.pseudo_legal_moves: @@ -744,7 +935,7 @@ def test_move_generation_bug(self): self.assertNotIn(illegal_move, board.legal_moves) self.assertNotIn(illegal_move, board.generate_legal_moves()) - # Generate all pseudo legal moves. + # Generate all pseudo-legal moves. for a in board.pseudo_legal_moves: board.push(a) board.pop() @@ -821,18 +1012,48 @@ def test_status(self): board = chess.Board("bbqnnrkr/pppppppp/8/8/8/8/PPPPPPPP/BBQNNRKR w KQkq - 0 1", chess960=False) self.assertEqual(board.status(), chess.STATUS_BAD_CASTLING_RIGHTS) + # Opposite check. + board = chess.Board("4k3/8/8/8/8/8/4Q3/4K3 w - - 0 1") + self.assertEqual(board.status(), chess.STATUS_OPPOSITE_CHECK) + + # Empty board. + board = chess.Board(None) + self.assertEqual(board.status(), chess.STATUS_EMPTY | chess.STATUS_NO_WHITE_KING | chess.STATUS_NO_BLACK_KING) + + # Too many kings. + board = chess.Board("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBKKBNR w KQkq - 0 1") + self.assertEqual(board.status(), chess.STATUS_TOO_MANY_KINGS) + + # Triple check. + board = chess.Board("4k3/5P2/3N4/8/8/8/4R3/4K3 b - - 0 1") + self.assertEqual(board.status(), chess.STATUS_TOO_MANY_CHECKERS | chess.STATUS_IMPOSSIBLE_CHECK) + + # Impossible checker alignment. + board = chess.Board("3R4/8/q4k2/2B5/1NK5/3b4/8/8 w - - 0 1") + self.assertEqual(board.status(), chess.STATUS_IMPOSSIBLE_CHECK) + board = chess.Board("2Nq4/2K5/1b6/8/7R/3k4/7P/8 w - - 0 1") + self.assertEqual(board.status(), chess.STATUS_IMPOSSIBLE_CHECK) + board = chess.Board("5R2/2P5/8/4k3/8/3rK2r/8/8 w - - 0 1") + self.assertEqual(board.status(), chess.STATUS_IMPOSSIBLE_CHECK) + board = chess.Board("8/8/8/1k6/3Pp3/8/8/4KQ2 b - d3 0 1") + self.assertEqual(board.status(), chess.STATUS_IMPOSSIBLE_CHECK) + + # Checkers aligned with opponent king are fine. + board = chess.Board("8/8/5k2/p1q5/PP1rp1P1/3P1N2/2RK1r2/5nN1 w - - 0 3") + self.assertEqual(board.status(), chess.STATUS_VALID) + def test_one_king_movegen(self): board = chess.Board.empty() board.set_piece_at(chess.A1, chess.Piece(chess.KING, chess.WHITE)) self.assertFalse(board.is_valid()) - self.assertEqual(len(board.legal_moves), 3) - self.assertEqual(len(board.pseudo_legal_moves), 3) + self.assertEqual(board.legal_moves.count(), 3) + self.assertEqual(board.pseudo_legal_moves.count(), 3) board.push_san("Kb1") - self.assertEqual(len(board.legal_moves), 0) - self.assertEqual(len(board.pseudo_legal_moves), 0) + self.assertEqual(board.legal_moves.count(), 0) + self.assertEqual(board.pseudo_legal_moves.count(), 0) board.push_san("--") - self.assertEqual(len(board.legal_moves), 5) - self.assertEqual(len(board.pseudo_legal_moves), 5) + self.assertEqual(board.legal_moves.count(), 5) + self.assertEqual(board.pseudo_legal_moves.count(), 5) def test_epd(self): # Create an EPD with a move and a string. @@ -846,6 +1067,9 @@ def test_epd(self): board = chess.Board("4k3/8/8/8/8/8/8/4K3 w - - 0 1") self.assertEqual(board.epd(noop=None), "4k3/8/8/8/8/8/8/4K3 w - - noop;") + # Create an EPD with numbers. + self.assertEqual(board.epd(pi=3.14), "4k3/8/8/8/8/8/8/4K3 w - - pi 3.14;") + # Create an EPD with a variation. board = chess.Board("k7/8/8/8/8/8/4PPPP/4K1NR w K - 0 1") epd = board.epd(pv=[ @@ -861,7 +1085,7 @@ def test_epd(self): chess.Move.from_uci("e5e6"), # Ke6 chess.Move.from_uci("e5e4"), # Ke4 ]) - self.assertEqual(epd, "8/8/8/4k3/8/1K6/8/8 b - - bm Ke6 Ke4;") + self.assertEqual(epd, "8/8/8/4k3/8/1K6/8/8 b - - bm Ke4 Ke6;") # Test loading an EPD. board = chess.Board() @@ -870,7 +1094,7 @@ def test_epd(self): self.assertIn(chess.Move(chess.F2, chess.F4), operations["bm"]) self.assertEqual(operations["id"], "BK.24") - # Test loading an EPD with half counter operations. + # Test loading an EPD with half-move counter operations. board = chess.Board() operations = board.set_epd("4k3/8/8/8/8/8/8/4K3 b - - fmvn 17; hmvc 13") self.assertEqual(board.fen(), "4k3/8/8/8/8/8/8/4K3 b - - 13 17") @@ -898,6 +1122,37 @@ def test_epd(self): self.assertEqual(operations["pv"][1], chess.Move.from_uci("g8f8")) self.assertEqual(operations["pv"][2], chess.Move.from_uci("h7f7")) + # Test EPD with semicolon. + board = chess.Board() + operations = board.set_epd("r2qk2r/ppp1b1pp/2n1p3/3pP1n1/3P2b1/2PB1NN1/PP4PP/R1BQK2R w KQkq - bm Nxg5; c0 \"ERET.095; Queen sacrifice\";") + self.assertEqual(operations["bm"], [chess.Move.from_uci("f3g5")]) + self.assertEqual(operations["c0"], "ERET.095; Queen sacrifice") + + # Test EPD with string escaping. + board = chess.Board() + operations = board.set_epd(r"""4k3/8/8/8/8/8/8/4K3 w - - a "foo\"bar";; ; b "foo\\\\";""") + self.assertEqual(operations["a"], "foo\"bar") + self.assertEqual(operations["b"], "foo\\\\") + + # Test EPD with unmatched trailing quotes. + board = chess.Board() + operations = board.set_epd("1k1r4/pp1b1R2/3q2pp/4p3/2B5/4Q3/PPP2B2/2K5 b - - bm Qd1+; id \"") + self.assertEqual(operations["bm"], [chess.Move.from_uci("d6d1")]) + self.assertEqual(operations["id"], "") + self.assertEqual(board.epd(**operations), "1k1r4/pp1b1R2/3q2pp/4p3/2B5/4Q3/PPP2B2/2K5 b - - bm Qd1+; id \"\";") + + def test_eret_epd(self): + # Too many dashes. + epd = """r1bqk1r1/1p1p1n2/p1n2pN1/2p1b2Q/2P1Pp2/1PN5/PB4PP/R4RK1 w q - - bm Rxf4; id "ERET 001 - Entlastung";""" + board, ops = chess.Board.from_epd(epd) + self.assertEqual(ops["id"], "ERET 001 - Entlastung") + self.assertEqual(ops["bm"], [chess.Move.from_uci("f1f4")]) + + def test_set_fen_as_epd(self): + board = chess.Board() + with self.assertRaises(ValueError): + board.set_epd(board.fen()) # Move numbers are not valid opcodes + def test_null_moves(self): self.assertEqual(str(chess.Move.null()), "0000") self.assertEqual(chess.Move.null().uci(), "0000") @@ -934,7 +1189,7 @@ def test_en_passant_attackers(self): # Not attacking the pawn directly. attackers = board.attackers(chess.BLACK, chess.F4) - self.assertEqual(attackers, chess.BB_VOID) + self.assertEqual(attackers, chess.BB_EMPTY) def test_attacks(self): board = chess.Board("5rk1/p5pp/2p3p1/1p1pR3/3P2P1/2N5/PP3n2/2KB4 w - - 1 26") @@ -948,6 +1203,10 @@ def test_attacks(self): self.assertNotIn(chess.C5, attacks) self.assertNotIn(chess.F4, attacks) + pawn_attacks = board.attacks(chess.B2) + self.assertIn(chess.A3, pawn_attacks) + self.assertNotIn(chess.B3, pawn_attacks) + self.assertFalse(board.attacks(chess.G1)) def test_clear(self): @@ -957,45 +1216,55 @@ def test_clear(self): self.assertEqual(board.turn, chess.WHITE) self.assertEqual(board.fullmove_number, 1) self.assertEqual(board.halfmove_clock, 0) - self.assertEqual(board.castling_rights, chess.BB_VOID) + self.assertEqual(board.castling_rights, chess.BB_EMPTY) self.assertFalse(board.ep_square) self.assertFalse(board.piece_at(chess.E1)) - self.assertEqual(chess.popcount(board.occupied), 0) + self.assertEqual(board.piece_count(), 0) def test_threefold_repetition(self): board = chess.Board() - # Go back and forth with the nights to reach the starting position + # Go back and forth with the knights to reach the starting position # for a second time. self.assertFalse(board.can_claim_threefold_repetition()) + self.assertFalse(board.is_repetition()) board.push_san("Nf3") self.assertFalse(board.can_claim_threefold_repetition()) + self.assertFalse(board.is_repetition()) board.push_san("Nf6") self.assertFalse(board.can_claim_threefold_repetition()) + self.assertFalse(board.is_repetition()) board.push_san("Ng1") self.assertFalse(board.can_claim_threefold_repetition()) + self.assertFalse(board.is_repetition()) board.push_san("Ng8") # Once more. self.assertFalse(board.can_claim_threefold_repetition()) + self.assertFalse(board.is_repetition()) board.push_san("Nf3") self.assertFalse(board.can_claim_threefold_repetition()) + self.assertFalse(board.is_repetition()) board.push_san("Nf6") self.assertFalse(board.can_claim_threefold_repetition()) + self.assertFalse(board.is_repetition()) board.push_san("Ng1") # Now black can go back to the starting position (thus reaching it a - # third time.) + # third time). self.assertTrue(board.can_claim_threefold_repetition()) + self.assertFalse(board.is_repetition()) board.push_san("Ng8") - # They indee do it. Also white can now claim. + # They indeed do it. Also, white can now claim. self.assertTrue(board.can_claim_threefold_repetition()) + self.assertTrue(board.is_repetition()) # But not after a different move. board.push_san("e4") self.assertFalse(board.can_claim_threefold_repetition()) + self.assertFalse(board.is_repetition()) # Undo moves and check if everything works backwards. board.pop() # e4 @@ -1023,7 +1292,7 @@ def test_fivefold_repetition(self): self.assertFalse(board.is_fivefold_repetition()) self.assertFalse(board.is_game_over()) - # Repeat it once more. Now it is a five-fold repetition. + # Repeat it once more. Now it is a fivefold repetition. board.push_san("Be2") self.assertFalse(board.is_fivefold_repetition()) board.push_san("Ne4") @@ -1055,43 +1324,62 @@ def test_fivefold_repetition(self): self.assertTrue(board.can_claim_threefold_repetition()) self.assertTrue(board.is_game_over(claim_draw=True)) - # Do in fact repeat. + # Do, in fact, repeat. self.assertFalse(board.is_fivefold_repetition()) board.push_san("Qd8") - # This is a threefold repetition but not a fivefold repetition, because - # consecutive moves are required for that. + # This is a threefold repetition, and also a fivefold repetition since + # it no longer has to occur on consecutive moves. self.assertTrue(board.can_claim_threefold_repetition()) - self.assertFalse(board.is_fivefold_repetition()) + self.assertTrue(board.is_fivefold_repetition()) self.assertEqual(board.fen().split()[0], fen.split()[0]) + def test_trivial_is_repetition(self): + self.assertTrue(chess.Board().is_repetition(1)) + def test_fifty_moves(self): - # Test positions from Timman - Lutz (1995). + # Test positions from Jan Timman vs. Christopher Lutz (1995). board = chess.Board() + self.assertFalse(board.is_fifty_moves()) self.assertFalse(board.can_claim_fifty_moves()) board = chess.Board("8/5R2/8/r2KB3/6k1/8/8/8 w - - 19 79") + self.assertFalse(board.is_fifty_moves()) self.assertFalse(board.can_claim_fifty_moves()) board = chess.Board("8/8/6r1/4B3/8/4K2k/5R2/8 b - - 68 103") + self.assertFalse(board.is_fifty_moves()) self.assertFalse(board.can_claim_fifty_moves()) board = chess.Board("6R1/7k/8/8/1r3B2/5K2/8/8 w - - 99 119") - self.assertFalse(board.can_claim_fifty_moves()) + self.assertFalse(board.is_fifty_moves()) + self.assertTrue(board.can_claim_fifty_moves()) board = chess.Board("8/7k/8/6R1/1r3B2/5K2/8/8 b - - 100 119") + self.assertTrue(board.is_fifty_moves()) self.assertTrue(board.can_claim_fifty_moves()) board = chess.Board("8/7k/8/1r3KR1/5B2/8/8/8 w - - 105 122") + self.assertTrue(board.is_fifty_moves()) self.assertTrue(board.can_claim_fifty_moves()) - # Once checkmated it is too late to claim. + # Once checkmated, it is too late to claim. board = chess.Board("k7/8/NKB5/8/8/8/8/8 b - - 105 176") + self.assertFalse(board.is_fifty_moves()) self.assertFalse(board.can_claim_fifty_moves()) # A stalemate is a draw, but you can not and do not need to claim it by - # the fifty move rule. + # the fifty-move rule. board = chess.Board("k7/3N4/1K6/1B6/8/8/8/8 b - - 99 1") self.assertTrue(board.is_stalemate()) self.assertTrue(board.is_game_over()) + self.assertFalse(board.is_fifty_moves()) self.assertFalse(board.can_claim_fifty_moves()) self.assertFalse(board.can_claim_draw()) + def test_promoted_comparison(self): + board = chess.Board() + board.set_fen("5R2/3P4/8/8/7r/7r/7k/K7 w - - 0 1") + board.push_san("d8=R") + + same_board = chess.Board(board.fen()) + self.assertEqual(board, same_board) + def test_ep_legality(self): move = chess.Move.from_uci("h5g6") board = chess.Board("rnbqkbnr/pppppp2/7p/6pP/8/8/PPPPPPP1/RNBQKBNR w KQkq g6 0 3") @@ -1139,13 +1427,13 @@ def test_pseudo_legality(self): pseudo_legal_moves = list(board.generate_pseudo_legal_moves()) - # Ensure that all moves generated as pseudo legal pass the pseudo- - # legality check. + # Ensure that all moves generated as pseudo-legal pass the + # pseudo-legality check. for move in pseudo_legal_moves: self.assertTrue(board.is_pseudo_legal(move)) - # Check that moves not generated as pseudo legal do not pass the - # pseudo legality check. + # Check that moves not generated as pseudo-legal do not pass the + # pseudo-legality check. for move in sample_moves: if move not in pseudo_legal_moves: self.assertFalse(board.is_pseudo_legal(move)) @@ -1175,7 +1463,7 @@ def test_pieces(self): def test_string_conversion(self): board = chess.Board("7k/1p1qn1b1/pB1p1n2/3Pp3/4Pp1p/2QN1B2/PP4PP/6K1 w - - 0 28") - self.assertEqual(str(board), textwrap.dedent(u"""\ + self.assertEqual(str(board), textwrap.dedent("""\ . . . . . . . k . p . q n . b . p B . p . n . . @@ -1185,38 +1473,58 @@ def test_string_conversion(self): P P . . . . P P . . . . . . K .""")) - self.assertEqual(board.unicode(), textwrap.dedent(u"""\ - . . . . . . . ♚ - . ♟ . ♛ ♞ . ♝ . - ♟ ♗ . ♟ . ♞ . . - . . . ♙ ♟ . . . - . . . . ♙ ♟ . ♟ - . . ♕ ♘ . ♗ . . - ♙ ♙ . . . . ♙ ♙ - . . . . . . ♔ .""")) + self.assertEqual(board.unicode(empty_square="·"), textwrap.dedent("""\ + · · · · · · · ♚ + · ♟ · ♛ ♞ · ♝ · + ♟ ♗ · ♟ · ♞ · · + · · · ♙ ♟ · · · + · · · · ♙ ♟ · ♟ + · · ♕ ♘ · ♗ · · + ♙ ♙ · · · · ♙ ♙ + · · · · · · ♔ ·""")) + + self.assertEqual(board.unicode(invert_color=True, borders=True, empty_square="·"), textwrap.dedent("""\ + ----------------- + 8 |·|·|·|·|·|·|·|♔| + ----------------- + 7 |·|♙|·|♕|♘|·|♗|·| + ----------------- + 6 |♙|♝|·|♙|·|♘|·|·| + ----------------- + 5 |·|·|·|♟|♙|·|·|·| + ----------------- + 4 |·|·|·|·|♟|♙|·|♙| + ----------------- + 3 |·|·|♛|♞|·|♝|·|·| + ----------------- + 2 |♟|♟|·|·|·|·|♟|♟| + ----------------- + 1 |·|·|·|·|·|·|♚|·| + ----------------- + a b c d e f g h""")) def test_move_info(self): board = chess.Board("r1bqkb1r/p3np2/2n1p2p/1p4pP/2pP4/4PQ1N/1P2BPP1/RNB1K2R w KQkq g6 0 11") - self.assertTrue(board.is_capture(board.parse_san("Qxf7+"))) - self.assertFalse(board.is_en_passant(board.parse_san("Qxf7+"))) - self.assertFalse(board.is_castling(board.parse_san("Qxf7+"))) + self.assertTrue(board.is_capture(board.parse_xboard("Qxf7+"))) + self.assertFalse(board.is_en_passant(board.parse_xboard("Qxf7+"))) + self.assertFalse(board.is_castling(board.parse_xboard("Qxf7+"))) - self.assertTrue(board.is_capture(board.parse_san("hxg6"))) - self.assertTrue(board.is_en_passant(board.parse_san("hxg6"))) - self.assertFalse(board.is_castling(board.parse_san("hxg6"))) + self.assertTrue(board.is_capture(board.parse_xboard("hxg6"))) + self.assertTrue(board.is_en_passant(board.parse_xboard("hxg6"))) + self.assertFalse(board.is_castling(board.parse_xboard("hxg6"))) - self.assertFalse(board.is_capture(board.parse_san("b3"))) - self.assertFalse(board.is_en_passant(board.parse_san("b3"))) - self.assertFalse(board.is_castling(board.parse_san("b3"))) + self.assertFalse(board.is_capture(board.parse_xboard("b3"))) + self.assertFalse(board.is_en_passant(board.parse_xboard("b3"))) + self.assertFalse(board.is_castling(board.parse_xboard("b3"))) - self.assertFalse(board.is_capture(board.parse_san("Ra6"))) - self.assertFalse(board.is_en_passant(board.parse_san("Ra6"))) - self.assertFalse(board.is_castling(board.parse_san("Ra6"))) + self.assertFalse(board.is_capture(board.parse_xboard("Ra6"))) + self.assertFalse(board.is_en_passant(board.parse_xboard("Ra6"))) + self.assertFalse(board.is_castling(board.parse_xboard("Ra6"))) - self.assertFalse(board.is_capture(board.parse_san("O-O"))) - self.assertFalse(board.is_en_passant(board.parse_san("O-O"))) - self.assertTrue(board.is_castling(board.parse_san("O-O"))) + self.assertFalse(board.is_capture(board.parse_xboard("O-O"))) + self.assertFalse(board.is_en_passant(board.parse_xboard("O-O"))) + self.assertTrue(board.is_castling(board.parse_xboard("O-O"))) def test_pin(self): board = chess.Board("rnb1k1nr/2pppppp/3P4/8/1b5q/8/PPPNPBPP/RNBQKB1R w KQkq - 0 1") @@ -1232,6 +1540,8 @@ def test_pin(self): self.assertEqual(board.pin(chess.WHITE, chess.D2), chess.BB_E1 | chess.BB_D2 | chess.BB_C3 | chess.BB_B4 | chess.BB_A5) + self.assertEqual(chess.Board(None).pin(chess.WHITE, chess.F7), chess.BB_ALL) + def test_pin_in_check(self): # The knight on the eighth rank is on the outer side of the rank attack. board = chess.Board("1n1R2k1/2b1qpp1/p3p2p/1p6/1P2Q2P/4PNP1/P4PB1/6K1 b - - 0 1") @@ -1254,7 +1564,7 @@ def test_impossible_en_passant(self): self.assertTrue(board.status() & chess.STATUS_INVALID_EP_SQUARE) def test_horizontally_skewered_en_passant(self): - # Horizontal pin. Non evasion. + # Horizontal pin. Non-evasion. board = chess.Board("8/8/8/r2Pp2K/8/8/4k3/8 w - e6 0 1") move = chess.Move.from_uci("d5e6") self.assertEqual(board.status(), chess.STATUS_VALID) @@ -1277,7 +1587,7 @@ def test_diagonally_skewered_en_passant(self): # of legal moves. The last move must have been a double pawn move, # but then the king would have been in check already. - # Diagonal attack uncovered. Evasion try. + # Diagonal attack uncovered. Evasion attempt. board = chess.Board("8/8/8/5k2/4Pp2/8/2B5/4K3 b - e3 0 1") move = chess.Move.from_uci("f4e3") self.assertTrue(board.is_pseudo_legal(move)) @@ -1324,23 +1634,96 @@ def test_capture_generation(self): # Fully legal captures. lc = list(board.generate_legal_captures()) self.assertIn(board.parse_san("Qxd1"), lc) - self.assertIn(board.parse_san("exf6"), lc) # En passant. + self.assertIn(board.parse_san("exf6"), lc) # En passant self.assertIn(board.parse_san("Bxd3"), lc) self.assertEqual(len(lc), 3) plc = list(board.generate_pseudo_legal_captures()) self.assertIn(board.parse_san("Qxd1"), plc) - self.assertIn(board.parse_san("exf6"), plc) # En passant. + self.assertIn(board.parse_san("exf6"), plc) # En passant self.assertIn(board.parse_san("Bxd3"), plc) self.assertIn(chess.Move.from_uci("c2c7"), plc) self.assertIn(chess.Move.from_uci("c2d3"), plc) self.assertEqual(len(plc), 5) + def test_castling_is_legal(self): + board = chess.Board("rnbqkbnr/5p2/1pp3pp/p2P4/6P1/2NPpN2/PPP1Q1BP/R3K2R w Qq - 0 11") + self.assertFalse(board.is_legal(chess.Move.from_uci("e1g1"))) + self.assertFalse(board.is_legal(chess.Move.from_uci("e1h1"))) + + board.castling_rights |= chess.BB_H1 + self.assertTrue(board.is_legal(chess.Move.from_uci("e1g1"))) + self.assertTrue(board.is_legal(chess.Move.from_uci("e1h1"))) + def test_from_chess960_pos(self): board = chess.Board.from_chess960_pos(909) self.assertTrue(board.chess960) self.assertEqual(board.fen(), "rkqbrnbn/pppppppp/8/8/8/8/PPPPPPPP/RKQBRNBN w KQkq - 0 1") + def test_mirror(self): + board = chess.Board("r1bq1r2/pp2n3/4N2k/3pPppP/1b1n2Q1/2N5/PP3PP1/R1B1K2R w KQ g6 0 15") + mirrored = chess.Board("r1b1k2r/pp3pp1/2n5/1B1N2q1/3PpPPp/4n2K/PP2N3/R1BQ1R2 b kq g3 0 15") + self.assertEqual(board.mirror(), mirrored) + board.apply_mirror() + self.assertEqual(board, mirrored) + + def test_chess960_pos(self): + board = chess.Board() + + board.set_chess960_pos(0) + self.assertEqual(board.board_fen(), "bbqnnrkr/pppppppp/8/8/8/8/PPPPPPPP/BBQNNRKR") + self.assertEqual(board.chess960_pos(), 0) + + board.set_chess960_pos(631) + self.assertEqual(board.board_fen(), "rnbkqrnb/pppppppp/8/8/8/8/PPPPPPPP/RNBKQRNB") + self.assertEqual(board.chess960_pos(), 631) + + board.set_chess960_pos(518) + self.assertEqual(board.board_fen(), chess.STARTING_BOARD_FEN) + self.assertEqual(board.chess960_pos(), 518) + + board.set_chess960_pos(959) + self.assertEqual(board.board_fen(), "rkrnnqbb/pppppppp/8/8/8/8/PPPPPPPP/RKRNNQBB") + self.assertEqual(board.chess960_pos(), 959) + + def test_is_irreversible(self): + board = chess.Board("r3k2r/8/8/8/8/8/8/R3K2R w Qkq - 0 1") + self.assertTrue(board.is_irreversible(board.parse_san("Ra2"))) + self.assertTrue(board.is_irreversible(board.parse_san("O-O-O"))) + self.assertTrue(board.is_irreversible(board.parse_san("Kd1"))) + self.assertTrue(board.is_irreversible(board.parse_san("Rxa8"))) + self.assertTrue(board.is_irreversible(board.parse_san("Rxh8"))) + self.assertFalse(board.is_irreversible(board.parse_san("Rf1"))) + self.assertFalse(board.is_irreversible(chess.Move.null())) + + board.set_castling_fen("kq") + self.assertFalse(board.is_irreversible(board.parse_san("Ra2"))) + self.assertFalse(board.is_irreversible(board.parse_san("Kd1"))) + self.assertTrue(board.is_irreversible(board.parse_san("Rxa8"))) + self.assertTrue(board.is_irreversible(board.parse_san("Rxh8"))) + self.assertFalse(board.is_irreversible(board.parse_san("Rf1"))) + self.assertFalse(board.is_irreversible(chess.Move.null())) + + def test_king_captures_unmoved_rook(self): + board = chess.Board("8/8/8/B2p3Q/2qPp1P1/b7/2P2PkP/4K2R b K - 0 1") + move = board.parse_uci("g2h1") + self.assertFalse(board.is_castling(move)) + self.assertEqual(board.san(move), "Kxh1") + board.push(move) + self.assertEqual(board.fen(), "8/8/8/B2p3Q/2qPp1P1/b7/2P2P1P/4K2k w - - 0 2") + + def test_impossible_check_due_to_en_passant(self): + board = chess.Board("rnbqk1nr/bb3p1p/1q2r3/2pPp3/3P4/7P/1PP1NpPP/R1BQKBNR w KQkq c6") + self.assertEqual(board.status(), chess.STATUS_IMPOSSIBLE_CHECK) + self.assertEqual(board.ep_square, chess.C6) + self.assertTrue(board.has_pseudo_legal_en_passant()) + self.assertFalse(board.has_legal_en_passant()) + self.assertEqual(len(list(board.legal_moves)), 2) + + def test_multiple_kings(self): + board = chess.Board("KKKK1kkk/8/8/8/8/8/8/8 w - - 0 1") + self.assertEqual(board.king(chess.WHITE), None) + class LegalMoveGeneratorTestCase(unittest.TestCase): @@ -1349,9 +1732,11 @@ def test_list_conversion(self): def test_nonzero(self): self.assertTrue(chess.Board().legal_moves) + self.assertTrue(chess.Board().pseudo_legal_moves) caro_kann_mate = chess.Board("r1bqkb1r/pp1npppp/2pN1n2/8/3P4/8/PPP1QPPP/R1B1KBNR b KQkq - 4 6") self.assertFalse(caro_kann_mate.legal_moves) + self.assertTrue(caro_kann_mate.pseudo_legal_moves) def test_string_conversion(self): board = chess.Board("r3k1nr/ppq1pp1p/2p3p1/8/1PPR4/2N5/P3QPPP/5RK1 b kq b3 0 16") @@ -1364,27 +1749,31 @@ def test_string_conversion(self): self.assertIn("e8d7", str(board.pseudo_legal_moves)) self.assertIn("e8d7", repr(board.pseudo_legal_moves)) + def test_traverse_once(self): + class MockBoard: + def __init__(self): + self.traversals = 0 -class BaseBoardTestCase(unittest.TestCase): + def generate_legal_moves(self): + self.traversals += 1 + return + yield - def test_set_chess960_pos(self): - board = chess.BaseBoard() + board = MockBoard() + gen = chess.LegalMoveGenerator(board) + list(gen) + self.assertEqual(board.traversals, 1) - board.set_chess960_pos(0) - self.assertEqual(board.board_fen(), "bbqnnrkr/pppppppp/8/8/8/8/PPPPPPPP/BBQNNRKR") - self.assertEqual(board.chess960_pos(), 0) - - board.set_chess960_pos(631) - self.assertEqual(board.board_fen(), "rnbkqrnb/pppppppp/8/8/8/8/PPPPPPPP/RNBKQRNB") - self.assertEqual(board.chess960_pos(), 631) - board.set_chess960_pos(518) - self.assertEqual(board.board_fen(), chess.STARTING_BOARD_FEN) - self.assertEqual(board.chess960_pos(), 518) +class BaseBoardTestCase(unittest.TestCase): - board.set_chess960_pos(959) - self.assertEqual(board.board_fen(), "rkrnnqbb/pppppppp/8/8/8/8/PPPPPPPP/RKRNNQBB") - self.assertEqual(board.chess960_pos(), 959) + def test_set_piece_map(self): + a = chess.BaseBoard.empty() + b = chess.BaseBoard() + a.set_piece_map(b.piece_map()) + self.assertEqual(a, b) + a.set_piece_map({}) + self.assertNotEqual(a, b) class SquareSetTestCase(unittest.TestCase): @@ -1408,6 +1797,9 @@ def test_equality(self): self.assertEqual(chess.SquareSet(chess.BB_ALL), chess.BB_ALL) self.assertEqual(chess.BB_ALL, chess.SquareSet(chess.BB_ALL)) + self.assertEqual(int(chess.SquareSet(chess.SquareSet(999))), 999) + self.assertEqual(chess.SquareSet([chess.B8]), chess.BB_B8) + def test_string_conversion(self): expected = textwrap.dedent("""\ . . . . . . . 1 @@ -1432,10 +1824,10 @@ def test_reversed(self): def test_arithmetic(self): self.assertEqual(chess.SquareSet(chess.BB_RANK_2) & chess.BB_FILE_D, chess.BB_D2) - self.assertEqual(chess.SquareSet(chess.BB_ALL) ^ chess.BB_VOID, chess.BB_ALL) + self.assertEqual(chess.SquareSet(chess.BB_ALL) ^ chess.BB_EMPTY, chess.BB_ALL) self.assertEqual(chess.SquareSet(chess.BB_C1) | chess.BB_FILE_C, chess.BB_FILE_C) - bb = chess.SquareSet(chess.BB_VOID) + bb = chess.SquareSet(chess.BB_EMPTY) bb ^= chess.BB_ALL self.assertEqual(bb, chess.BB_ALL) bb &= chess.BB_E4 @@ -1453,21 +1845,30 @@ def test_arithmetic(self): self.assertEqual(bb, chess.BB_C1) def test_immutable_set_operations(self): - self.assertFalse(chess.SquareSet(chess.BB_A1).issubset(chess.BB_RANK_1)) - self.assertTrue(chess.SquareSet(chess.BB_RANK_1).issubset(chess.BB_A1)) - - self.assertTrue(chess.SquareSet(chess.BB_A1).issuperset(chess.BB_RANK_1)) - self.assertFalse(chess.SquareSet(chess.BB_RANK_1).issuperset(chess.BB_A1)) - - self.assertEqual(chess.SquareSet(chess.BB_A1).union(chess.BB_FILE_A), chess.BB_FILE_A) - - self.assertEqual(chess.SquareSet(chess.BB_A1).intersection(chess.BB_A2), chess.BB_VOID) - - self.assertEqual(chess.SquareSet(chess.BB_A1).difference(chess.BB_A2), chess.BB_A1) - - self.assertEqual(chess.SquareSet(chess.BB_A1).symmetric_difference(chess.BB_A2), chess.BB_A1 | chess.BB_A2) + examples = [ + chess.BB_EMPTY, + chess.BB_A1, + chess.BB_A2, + chess.BB_RANK_1, + chess.BB_RANK_2, + chess.BB_FILE_A, + chess.BB_FILE_E, + ] - self.assertEqual(chess.SquareSet(chess.BB_C5).copy(), chess.BB_C5) + for a in examples: + self.assertEqual(chess.SquareSet(a).copy(), a) + + for a in examples: + a = chess.SquareSet(a) + for b in examples: + b = chess.SquareSet(b) + self.assertEqual(set(a).isdisjoint(set(b)), a.isdisjoint(b)) + self.assertEqual(set(a).issubset(set(b)), a.issubset(b)) + self.assertEqual(set(a).issuperset(set(b)), a.issuperset(b)) + self.assertEqual(set(a).union(set(b)), set(a.union(b))) + self.assertEqual(set(a).intersection(set(b)), set(a.intersection(b))) + self.assertEqual(set(a).difference(set(b)), set(a.difference(b))) + self.assertEqual(set(a).symmetric_difference(set(b)), set(a.symmetric_difference(b))) def test_mutable_set_operations(self): squares = chess.SquareSet(chess.BB_A1) @@ -1498,19 +1899,61 @@ def test_mutable_set_operations(self): self.assertEqual(squares, chess.BB_A3) squares.clear() - self.assertEqual(squares, chess.BB_VOID) + self.assertEqual(squares, chess.BB_EMPTY) with self.assertRaises(KeyError): squares.pop() squares.add(chess.C7) self.assertEqual(squares.pop(), chess.C7) - self.assertEqual(squares, chess.BB_VOID) + self.assertEqual(squares, chess.BB_EMPTY) def test_from_square(self): self.assertEqual(chess.SquareSet.from_square(chess.H5), chess.BB_H5) self.assertEqual(chess.SquareSet.from_square(chess.C2), chess.BB_C2) + def test_carry_rippler(self): + self.assertEqual(sum(1 for _ in chess.SquareSet(chess.BB_D1).carry_rippler()), 2 ** 1) + self.assertEqual(sum(1 for _ in chess.SquareSet(chess.BB_FILE_B).carry_rippler()), 2 ** 8) + + def test_mirror(self): + self.assertEqual(chess.SquareSet(0x00a2_0900_0004_a600).mirror(), 0x00a6_0400_0009_a200) + self.assertEqual(chess.SquareSet(0x1e22_2212_0e0a_1222).mirror(), 0x2212_0a0e_1222_221e) + + def test_flip(self): + self.assertEqual(chess.flip_vertical(chess.BB_ALL), chess.BB_ALL) + self.assertEqual(chess.flip_horizontal(chess.BB_ALL), chess.BB_ALL) + self.assertEqual(chess.flip_diagonal(chess.BB_ALL), chess.BB_ALL) + self.assertEqual(chess.flip_anti_diagonal(chess.BB_ALL), chess.BB_ALL) + + s = chess.SquareSet(0x1e22_2212_0e0a_1222) # Letter R + self.assertEqual(chess.flip_vertical(s), 0x2212_0a0e_1222_221e) + self.assertEqual(chess.flip_horizontal(s), 0x7844_4448_7050_4844) + self.assertEqual(chess.flip_diagonal(s), 0x0000_6192_8c88_ff00) + self.assertEqual(chess.flip_anti_diagonal(s), 0x00ff_1131_4986_0000) + + def test_len_of_complenent(self): + squares = chess.SquareSet(~chess.BB_ALL) + self.assertEqual(len(squares), 0) + + squares = ~chess.SquareSet(chess.BB_BACKRANKS) + self.assertEqual(len(squares), 48) + + def test_int_conversion(self): + self.assertEqual(int(chess.SquareSet(chess.BB_CENTER)), 0x0000_0018_1800_0000) + self.assertEqual(hex(chess.SquareSet(chess.BB_CENTER)), "0x1818000000") + self.assertEqual(bin(chess.SquareSet(chess.BB_CENTER)), "0b1100000011000000000000000000000000000") + + def test_tolist(self): + self.assertEqual(chess.SquareSet(chess.BB_LIGHT_SQUARES).tolist().count(True), 32) + + def test_flip_ducktyping(self): + bb = 0x1e22_2212_0e0a_1222 + squares = chess.SquareSet(bb) + for f in [chess.flip_vertical, chess.flip_horizontal, chess.flip_diagonal, chess.flip_anti_diagonal]: + self.assertEqual(int(f(squares)), f(bb)) + self.assertEqual(int(squares), bb) # Not mutated + class PolyglotTestCase(unittest.TestCase): @@ -1519,24 +1962,23 @@ def test_performance_bin(self): pos = chess.Board() e4 = next(book.find_all(pos)) - self.assertEqual(e4.move(), pos.parse_san("e4")) - pos.push(e4.move()) + self.assertEqual(e4.move, pos.parse_san("e4")) + pos.push(e4.move) e5 = next(book.find_all(pos)) - self.assertEqual(e5.move(), pos.parse_san("e5")) - pos.push(e5.move()) + self.assertEqual(e5.move, pos.parse_san("e5")) + pos.push(e5.move) def test_mainline(self): with chess.polyglot.open_reader("data/polyglot/performance.bin") as book: board = chess.Board() while True: - try: - entry = book.find(board) - except IndexError: + entry = book.get(board) + if entry is None: break - else: - board.push(entry.move()) + + board.push(entry.move) self.assertEqual(board.fen(), "r2q1rk1/4bppp/p2p1n2/np5b/3BP1P1/5N1P/PPB2P2/RN1QR1K1 b - - 0 15") @@ -1544,29 +1986,34 @@ def test_lasker_trap(self): with chess.polyglot.open_reader("data/polyglot/lasker-trap.bin") as book: board = chess.Board("rnbqk1nr/ppp2ppp/8/4P3/1BP5/8/PP2KpPP/RN1Q1BNR b kq - 1 7") entry = book.find(board) - cute_underpromotion = entry.move() + cute_underpromotion = entry.move self.assertEqual(cute_underpromotion, board.parse_san("fxg1=N+")) def test_castling(self): with chess.polyglot.open_reader("data/polyglot/performance.bin") as book: # White decides between short castling and long castling at this - # turning point in the Queens Gambit Exchange. + # turning point in the Queen's Gambit Declined, Exchange Variation. pos = chess.Board("r1bqr1k1/pp1nbppp/2p2n2/3p2B1/3P4/2NBP3/PPQ1NPPP/R3K2R w KQ - 5 10") - moves = set(entry.move() for entry in book.find_all(pos)) + moves = set(entry.move for entry in book.find_all(pos)) self.assertIn(pos.parse_san("O-O"), moves) self.assertIn(pos.parse_san("O-O-O"), moves) self.assertIn(pos.parse_san("h3"), moves) self.assertEqual(len(moves), 3) - # Black usually castles long at this point in the Ruy Lopez - # Exchange. + # Black usually castles long at this point in the Ruy Lopez, + # Exchange Variation. pos = chess.Board("r3k1nr/1pp1q1pp/p1pb1p2/4p3/3PP1b1/2P1BN2/PP1N1PPP/R2Q1RK1 b kq - 4 9") - moves = set(entry.move() for entry in book.find_all(pos)) + moves = set(entry.move for entry in book.find_all(pos)) self.assertIn(pos.parse_san("O-O-O"), moves) self.assertEqual(len(moves), 1) + # Not a castling move. + pos = chess.Board("1r1qr1k1/1b2bp1n/p2p2pB/1pnPp2p/P1p1P3/R1P2NNP/1PBQ1PP1/4R1K1 w - - 0 1") + entry = book.find(pos) + self.assertEqual(entry.move, chess.Move.from_uci("e1a1")) + def test_empty_book(self): - with chess.polyglot.open_reader("data/polyglot/empty.bin") as book: + with chess.polyglot.open_reader(os.devnull) as book: self.assertEqual(len(book), 0) entries = book.find_all(chess.Board()) @@ -1583,13 +2030,13 @@ def test_reversed(self): self.assertEqual(book[0], last) def test_random_choice(self): - class FirstMockRandom(object): + class FirstMockRandom: @staticmethod def randint(first, last): assert first <= last return first - class LastMockRandom(object): + class LastMockRandom: @staticmethod def randint(first, last): assert first <= last @@ -1598,32 +2045,31 @@ def randint(first, last): with chess.polyglot.open_reader("data/polyglot/performance.bin") as book: # Uniform choice. entry = book.choice(chess.Board(), random=FirstMockRandom()) - self.assertEqual(entry.move(), chess.Move.from_uci("e2e4")) + self.assertEqual(entry.move, chess.Move.from_uci("e2e4")) entry = book.choice(chess.Board(), random=LastMockRandom()) - self.assertEqual(entry.move(), chess.Move.from_uci("c2c4")) + self.assertEqual(entry.move, chess.Move.from_uci("c2c4")) # Weighted choice. entry = book.weighted_choice(chess.Board(), random=FirstMockRandom()) - self.assertEqual(entry.move(), chess.Move.from_uci("e2e4")) + self.assertEqual(entry.move, chess.Move.from_uci("e2e4")) entry = book.weighted_choice(chess.Board(), random=LastMockRandom()) - self.assertEqual(entry.move(), chess.Move.from_uci("c2c4")) + self.assertEqual(entry.move, chess.Move.from_uci("c2c4")) # Weighted choice with excluded move. - entry = book.weighted_choice(chess.Board(), - exclude_moves=[chess.Move.from_uci("e2e4")], random=FirstMockRandom()) - self.assertEqual(entry.move(), chess.Move.from_uci("d2d4")) + entry = book.weighted_choice(chess.Board(), exclude_moves=[chess.Move.from_uci("e2e4")], random=FirstMockRandom()) + self.assertEqual(entry.move, chess.Move.from_uci("d2d4")) def test_find(self): with chess.polyglot.open_reader("data/polyglot/performance.bin") as book: entry = book.find(chess.Board()) - self.assertEqual(entry.move(), chess.Move.from_uci("e2e4")) + self.assertEqual(entry.move, chess.Move.from_uci("e2e4")) def test_exclude_moves(self): with chess.polyglot.open_reader("data/polyglot/performance.bin") as book: entry = book.find(chess.Board(), exclude_moves=[chess.Move.from_uci("e2e4")]) - self.assertEqual(entry.move(), chess.Move.from_uci("d2d4")) + self.assertEqual(entry.move, chess.Move.from_uci("d2d4")) def test_contains(self): with chess.polyglot.open_reader("data/polyglot/performance.bin") as book: @@ -1646,28 +2092,29 @@ class PgnTestCase(unittest.TestCase): def test_exporter(self): game = chess.pgn.Game() - game.comment = "Test game:" + game.comments = ["Test game:"] game.headers["Result"] = "*" + game.headers["VeryLongHeader"] = "This is a very long header, much wider than the 80 columns that PGNs are formatted with by default" e4 = game.add_variation(game.board().parse_san("e4")) - e4.comment = "Scandinavian defense:" + e4.comments = ["Scandinavian Defense:"] e4_d5 = e4.add_variation(e4.board().parse_san("d5")) e4_h5 = e4.add_variation(e4.board().parse_san("h5")) e4_h5.nags.add(chess.pgn.NAG_MISTAKE) - e4_h5.starting_comment = "This" - e4_h5.comment = "is nonesense" + e4_h5.starting_comments = ["This"] + e4_h5.comments = ["is nonsense"] e4_e5 = e4.add_variation(e4.board().parse_san("e5")) e4_e5_Qf3 = e4_e5.add_variation(e4_e5.board().parse_san("Qf3")) e4_e5_Qf3.nags.add(chess.pgn.NAG_MISTAKE) e4_c5 = e4.add_variation(e4.board().parse_san("c5")) - e4_c5.comment = "Sicilian" + e4_c5.comments = ["Sicilian"] e4_d5_exd5 = e4_d5.add_main_variation(e4_d5.board().parse_san("exd5")) - e4_d5_exd5.comment = "Best" + e4_d5_exd5.comments = ["Best", "and the end of this {example}"] # Test string exporter with various options. exporter = chess.pgn.StringExporter(headers=False, comments=False, variations=False) @@ -1688,18 +2135,23 @@ def test_exporter(self): [White "?"] [Black "?"] [Result "*"] + [VeryLongHeader "This is a very long header, much wider than the 80 columns that PGNs are formatted with by default"] - { Test game: } 1. e4 { Scandinavian defense: } 1... d5 ( { This } 1... h5 $2 - { is nonesense } ) ( 1... e5 2. Qf3 $2 ) ( 1... c5 { Sicilian } ) 2. exd5 - { Best } *""") + { Test game: } 1. e4 { Scandinavian Defense: } 1... d5 ( { This } 1... h5 $2 + { is nonsense } ) ( 1... e5 2. Qf3 $2 ) ( 1... c5 { Sicilian } ) 2. exd5 + { Best } { and the end of this example } *""") self.assertEqual(str(exporter), pgn) # Test file exporter. - virtual_file = StringIO() + virtual_file = io.StringIO() exporter = chess.pgn.FileExporter(virtual_file) game.accept(exporter) self.assertEqual(virtual_file.getvalue(), pgn + "\n\n") + def test_game_without_tag_roster(self): + game = chess.pgn.Game.without_tag_roster() + self.assertEqual(str(game), "*") + def test_setup(self): game = chess.pgn.Game() self.assertEqual(game.board(), chess.Board()) @@ -1729,7 +2181,7 @@ def test_setup(self): self.assertEqual(game.headers["SetUp"], "1") self.assertNotIn("Variant", game.headers) - # Chess960 starting position 283. + # Chess960 starting position #283. fen = "rkbqrnnb/pppppppp/8/8/8/8/PPPPPPPP/RKBQRNNB w KQkq - 0 1" game.setup(fen) self.assertEqual(game.headers["FEN"], fen) @@ -1777,26 +2229,34 @@ def test_read_game(self): self.assertEqual(sixth_game.headers["White"], "Deep Blue (Computer)") self.assertEqual(sixth_game.headers["Result"], "1-0") + def test_read_game_with_multicomment_move(self): + pgn = io.StringIO("1. e4 {A common opening} 1... e5 {A common response} {An uncommon comment}") + game = chess.pgn.read_game(pgn) + first_move = game.variation(0) + self.assertEqual(first_move.comments, ["A common opening"]) + second_move = first_move.variation(0) + self.assertEqual(second_move.comments, ["A common response", "An uncommon comment"]) + def test_comment_at_eol(self): - pgn = StringIO(textwrap.dedent("""\ + pgn = io.StringIO(textwrap.dedent("""\ 1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 4. c3 Nf6 5. d3 d6 6. Nbd2 a6 $6 (6... Bb6 $5 { - /\ Ne7, c6}) *""")) + /\\ Ne7, c6}) *""")) game = chess.pgn.read_game(pgn) # Seek the node after 6.Nbd2 and before 6...a6. node = game while node.variations and not node.has_variation(chess.Move.from_uci("a7a6")): - node = node.variation(0) + node = node[0] # Make sure the comment for the second variation is there. - self.assertIn(5, node.variation(1).nags) - self.assertEqual(node.variation(1).comment, "/\\ Ne7, c6") + self.assertIn(5, node[1].nags) + self.assertEqual(node[1].comments, ["\n/\\ Ne7, c6"]) def test_promotion_without_equals(self): # Example game from https://github.com/rozim/ChessData as originally # reported. - pgn = StringIO(textwrap.dedent("""\ + pgn = io.StringIO(textwrap.dedent("""\ [Event "It (open)"] [Site "Aschach (Austria)"] [Date "2011.12.26"] @@ -1830,81 +2290,99 @@ def test_promotion_without_equals(self): last_node = game.end() self.assertEqual(last_node.move.uci(), "b2b1q") + def test_header_with_paren(self): + with open("data/pgn/stockfish-learning.pgn") as pgn: + game = chess.pgn.read_game(pgn) + self.assertEqual(game.headers["Opening"], "St. George (Baker) defense") + self.assertEqual(game.end().board(), chess.Board("8/2p2k2/1pR3p1/1P1P4/p1P2P2/P4K2/8/5r2 w - - 7 78")) + + def test_special_tag_names(self): + pgn = io.StringIO("""[BlackType: "program"]""") + game = chess.pgn.read_game(pgn) + self.assertEqual(game.headers["BlackType:"], "program") + + with self.assertRaises(ValueError): + game.headers["~"] = "foo" + + game.headers["Equals="] = "bar" + def test_chess960_without_fen(self): - pgn = StringIO(textwrap.dedent("""\ + pgn = io.StringIO(textwrap.dedent("""\ [Variant "Chess960"] 1. e4 * """)) game = chess.pgn.read_game(pgn) - self.assertEqual(game.variation(0).move, chess.Move.from_uci("e2e4")) + self.assertEqual(game[0].move, chess.Move.from_uci("e2e4")) def test_variation_stack(self): # Survive superfluous closing brackets. - pgn = StringIO("1. e4 (1. d4))) !? *") + pgn = io.StringIO("1. e4 (1. d4))) !? *") logging.disable(logging.ERROR) game = chess.pgn.read_game(pgn) logging.disable(logging.NOTSET) - self.assertEqual(game.variation(0).san(), "e4") - self.assertEqual(game.variation(1).san(), "d4") - self.assertEqual(len(game.errors), 2) + self.assertEqual(game[0].san(), "e4") + self.assertEqual(game[0].uci(), "e2e4") + self.assertEqual(game[1].san(), "d4") + self.assertEqual(game[1].uci(), "d2d4") + self.assertEqual(len(game.errors), 0) # Survive superfluous opening brackets. - pgn = StringIO("((( 1. c4 *") + pgn = io.StringIO("((( 1. c4 *") logging.disable(logging.ERROR) game = chess.pgn.read_game(pgn) logging.disable(logging.NOTSET) - self.assertEqual(game.variation(0).san(), "c4") - self.assertEqual(len(game.errors), 3) + self.assertEqual(game[0].san(), "c4") + self.assertEqual(len(game.errors), 0) def test_game_starting_comment(self): - pgn = StringIO("{ Game starting comment } 1. d3") + pgn = io.StringIO("{ Game starting comment } 1. d3") game = chess.pgn.read_game(pgn) - self.assertEqual(game.comment, "Game starting comment") - self.assertEqual(game.variation(0).san(), "d3") + self.assertEqual(game.comments, ["Game starting comment"]) + self.assertEqual(game[0].san(), "d3") - pgn = StringIO("{ Empty game, but has a comment }") + pgn = io.StringIO("{ Empty game, but has a comment }") game = chess.pgn.read_game(pgn) - self.assertEqual(game.comment, "Empty game, but has a comment") + self.assertEqual(game.comments, ["Empty game, but has a comment"]) def test_game_starting_variation(self): - pgn = StringIO(textwrap.dedent("""\ + pgn = io.StringIO(textwrap.dedent("""\ {Start of game} 1. e4 ({Start of variation} 1. d4) 1... e5 """)) game = chess.pgn.read_game(pgn) - self.assertEqual(game.comment, "Start of game") + self.assertEqual(game.comments, ["Start of game"]) - node = game.variation(0) + node = game[0] self.assertEqual(node.move, chess.Move.from_uci("e2e4")) - self.assertFalse(node.comment) - self.assertFalse(node.starting_comment) + self.assertFalse(node.comments) + self.assertFalse(node.starting_comments) - node = game.variation(1) + node = game[1] self.assertEqual(node.move, chess.Move.from_uci("d2d4")) - self.assertFalse(node.comment) - self.assertEqual(node.starting_comment, "Start of variation") + self.assertFalse(node.comments) + self.assertEqual(node.starting_comments, ["Start of variation"]) def test_annotation_symbols(self): - pgn = StringIO("1. b4?! g6 2. Bb2 Nc6? 3. Bxh8!!") + pgn = io.StringIO("1. b4?! g6 2. Bb2 Nc6? 3. Bxh8!!") game = chess.pgn.read_game(pgn) - node = game.variation(0) + node = game.variation(chess.Move.from_uci("b2b4")) self.assertIn(chess.pgn.NAG_DUBIOUS_MOVE, node.nags) self.assertEqual(len(node.nags), 1) - node = node.variation(0) + node = node[0] self.assertEqual(len(node.nags), 0) - node = node.variation(0) + node = node[0] self.assertEqual(len(node.nags), 0) - node = node.variation(0) + node = node[0] self.assertIn(chess.pgn.NAG_MISTAKE, node.nags) self.assertEqual(len(node.nags), 1) - node = node.variation(0) + node = node[0] self.assertIn(chess.pgn.NAG_BRILLIANT_MOVE, node.nags) self.assertEqual(len(node.nags), 1) @@ -1924,10 +2402,10 @@ def test_tree_traversal(self): self.assertEqual(end_node.end(), end_node) self.assertEqual(alternative_node.end(), alternative_node) - self.assertTrue(game.is_main_line()) - self.assertTrue(node.is_main_line()) - self.assertTrue(end_node.is_main_line()) - self.assertFalse(alternative_node.is_main_line()) + self.assertTrue(game.is_mainline()) + self.assertTrue(node.is_mainline()) + self.assertTrue(end_node.is_mainline()) + self.assertFalse(alternative_node.is_mainline()) self.assertFalse(game.starts_variation()) self.assertFalse(node.starts_variation()) @@ -1946,14 +2424,14 @@ def test_promote_demote(self): self.assertTrue(a.is_main_variation()) self.assertFalse(b.is_main_variation()) - self.assertEqual(game.variation(0), a) - self.assertEqual(game.variation(1), b) + self.assertEqual(game[0], a) + self.assertEqual(game[1], b) game.promote(b) self.assertTrue(b.is_main_variation()) self.assertFalse(a.is_main_variation()) - self.assertEqual(game.variation(0), b) - self.assertEqual(game.variation(1), a) + self.assertEqual(game[0], b) + self.assertEqual(game[1], a) game.demote(b) self.assertTrue(a.is_main_variation()) @@ -1962,13 +2440,19 @@ def test_promote_demote(self): self.assertTrue(c.is_main_variation()) self.assertFalse(a.is_main_variation()) self.assertFalse(b.is_main_variation()) - self.assertEqual(game.variation(0), c) - self.assertEqual(game.variation(1), a) - self.assertEqual(game.variation(2), b) + self.assertEqual(game[0], c) + self.assertEqual(game[1], a) + self.assertEqual(game[2], b) - def test_scan_offsets(self): + def test_skip_game(self): with open("data/pgn/kasparov-deep-blue-1997.pgn") as pgn: - offsets = list(chess.pgn.scan_offsets(pgn)) + offsets = [] + while True: + offset = pgn.tell() + if chess.pgn.skip_game(pgn): + offsets.append(offset) + else: + break self.assertEqual(len(offsets), 6) pgn.seek(offsets[0]) @@ -1981,16 +2465,146 @@ def test_scan_offsets(self): self.assertEqual(sixth_game.headers["Event"], "IBM Man-Machine, New York USA") self.assertEqual(sixth_game.headers["Site"], "06") - def test_scan_headers(self): + def test_tricky_skip_game(self): + raw_pgn = textwrap.dedent(""" + 1. a3 ; { ; } + + 1. b3 { ; + % { + 1... g6 ; { + + 1. c3 { } + % { + 1... f6 ; { } {{{ + + 1. d3""") + pgn = io.StringIO(raw_pgn) + + offsets = [] + while True: + offset = pgn.tell() + if chess.pgn.skip_game(pgn): + offsets.append(offset) + else: + break + + self.assertEqual(len(offsets), 3) + + pgn.seek(offsets[0]) + self.assertEqual(chess.pgn.read_game(pgn).next().move, chess.Move.from_uci("a2a3")) + pgn.seek(offsets[1]) + self.assertEqual(chess.pgn.read_game(pgn).next().move, chess.Move.from_uci("b2b3")) + pgn.seek(offsets[2]) + self.assertEqual(chess.pgn.read_game(pgn).next().move, chess.Move.from_uci("d2d3")) + self.assertEqual(chess.pgn.read_game(pgn), None) + + def test_read_headers(self): with open("data/pgn/kasparov-deep-blue-1997.pgn") as pgn: - offsets = (offset for offset, headers in chess.pgn.scan_headers(pgn) - if headers["Result"] == "1/2-1/2") + offsets = [] + + while True: + offset = pgn.tell() + headers = chess.pgn.read_headers(pgn) + if headers is None: + break + elif headers.get("Result", "*") == "1/2-1/2": + offsets.append(offset) - first_drawn_game_offset = next(offsets) - pgn.seek(first_drawn_game_offset) + pgn.seek(offsets[0]) first_drawn_game = chess.pgn.read_game(pgn) self.assertEqual(first_drawn_game.headers["Site"], "03") - self.assertEqual(first_drawn_game.variation(0).move, chess.Move.from_uci("d2d3")) + self.assertEqual(first_drawn_game[0].move, chess.Move.from_uci("d2d3")) + + def test_parse_time_control(self): + with open("data/pgn/nepomniachtchi-liren-game1.pgn") as pgn: + game = chess.pgn.read_game(pgn) + tc = game.time_control() + + self.assertEqual(tc, chess.pgn.parse_time_control(game.headers["TimeControl"])) + + self.assertEqual(tc.type, chess.pgn.TimeControlType.STANDARD) + self.assertEqual(len(tc.parts), 3) + + tcp1, tcp2, tcp3 = tc.parts + + self.assertEqual(tcp1, chess.pgn.TimeControlPart(40, 7200)) + self.assertEqual(tcp2, chess.pgn.TimeControlPart(20, 3600)) + self.assertEqual(tcp3, chess.pgn.TimeControlPart(0, 900, 30)) + + self.assertEqual(chess.pgn.TimeControlType.BULLET, chess.pgn.parse_time_control("60").type) + self.assertEqual(chess.pgn.TimeControlType.BULLET, chess.pgn.parse_time_control("60+1").type) + + self.assertEqual(chess.pgn.TimeControlType.BLITZ, chess.pgn.parse_time_control("60+2").type) + self.assertEqual(chess.pgn.TimeControlType.BLITZ, chess.pgn.parse_time_control("300").type) + self.assertEqual(chess.pgn.TimeControlType.BLITZ, chess.pgn.parse_time_control("300+3").type) + + self.assertEqual(chess.pgn.TimeControlType.RAPID, chess.pgn.parse_time_control("300+10").type) + self.assertEqual(chess.pgn.TimeControlType.RAPID, chess.pgn.parse_time_control("1800").type) + self.assertEqual(chess.pgn.TimeControlType.RAPID, chess.pgn.parse_time_control("1800+10").type) + + self.assertEqual(chess.pgn.TimeControlType.STANDARD, chess.pgn.parse_time_control("1800+30").type) + self.assertEqual(chess.pgn.TimeControlType.STANDARD, chess.pgn.parse_time_control("5400").type) + self.assertEqual(chess.pgn.TimeControlType.STANDARD, chess.pgn.parse_time_control("5400+30").type) + + with self.assertRaises(ValueError): + chess.pgn.parse_time_control("300+a") + + with self.assertRaises(ValueError): + chess.pgn.parse_time_control("300+ad") + + with self.assertRaises(ValueError): + chess.pgn.parse_time_control("600:20/180") + + with self.assertRaises(ValueError): + chess.pgn.parse_time_control("abc") + + with self.assertRaises(ValueError): + chess.pgn.parse_time_control("40/abc") + + + def test_visit_board(self): + class TraceVisitor(chess.pgn.BaseVisitor): + def __init__(self): + self.trace = [] + + def visit_board(self, board): + self.trace.append(board.fen()) + + def visit_move(self, board, move): + self.trace.append(board.san(move)) + + def result(self): + return self.trace + + pgn = io.StringIO(textwrap.dedent("""\ + [FEN "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1"] + + 1... e5 (1... d5 2. exd5) (1... c5) 2. Nf3 Nc6 + """)) + + trace = [ + "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", + "e5", + "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2", + "d5", + "rnbqkbnr/ppp1pppp/8/3p4/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2", + "exd5", + "rnbqkbnr/ppp1pppp/8/3P4/8/8/PPPP1PPP/RNBQKBNR b KQkq - 0 2", + "c5", + "rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2", + "Nf3", + "rnbqkbnr/pppp1ppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2", + "Nc6", + "r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3", + ] + + self.assertEqual(trace, chess.pgn.read_game(pgn, Visitor=TraceVisitor)) + + pgn.seek(0) + self.assertEqual(trace, chess.pgn.read_game(pgn).accept(TraceVisitor())) + + pgn.seek(0) + self.assertEqual(chess.Board(trace[-1]), chess.pgn.read_game(pgn, Visitor=chess.pgn.BoardBuilder)) def test_black_to_move(self): game = chess.pgn.Game() @@ -2008,20 +2622,20 @@ def test_black_to_move(self): [White "?"] [Black "?"] [Result "*"] - [SetUp "1"] [FEN "8/8/4k3/8/4P3/4K3/8/8 b - - 0 17"] + [SetUp "1"] 17... Kd6 18. Kd4 Ke6 *""") self.assertEqual(str(game), expected) def test_result_termination_marker(self): - pgn = StringIO("1. d4 1-0") + pgn = io.StringIO("1. d4 1-0") game = chess.pgn.read_game(pgn) self.assertEqual(game.headers["Result"], "1-0") def test_missing_setup_tag(self): - pgn = StringIO(textwrap.dedent("""\ + pgn = io.StringIO(textwrap.dedent("""\ [Event "Test position"] [Site "Black to move "] [Date "1997.10.26"] @@ -2046,6 +2660,15 @@ def test_missing_setup_tag(self): board = chess.Board("rbb1N1k1/pp1n1ppp/8/2Pp4/3P4/4P3/P1Q2PPq/R1BR1K2 b - - 0 1") self.assertEqual(game.board(), board) + def test_chessbase_empty_line(self): + with open("data/pgn/chessbase-empty-line.pgn") as pgn: + game = chess.pgn.read_game(pgn) + self.assertEqual(game.headers["Event"], "AlphaZero vs. Stockfish") + self.assertEqual(game.headers["Round"], "1") + self.assertEqual(game.next().move, chess.Move.from_uci("e2e4")) + + self.assertTrue(chess.pgn.read_game(pgn) is None) + def test_game_from_board(self): setup = "3k4/8/4K3/8/8/8/8/2R5 b - - 0 1" board = chess.Board(setup) @@ -2062,11 +2685,18 @@ def test_game_from_board(self): self.assertEqual(game.headers["Result"], "1-0") def test_errors(self): - pgn = StringIO("1. e4 Qa1 e5 2. Qxf8") + pgn = io.StringIO(""" + 1. e4 Qa1 e5 2. Qxf8 + + 1. a3""") logging.disable(logging.ERROR) game = chess.pgn.read_game(pgn) logging.disable(logging.NOTSET) - self.assertEqual(len(game.errors), 2) + self.assertEqual(len(game.errors), 1) + self.assertEqual(game.end().board().fen(), "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1") + + game = chess.pgn.read_game(pgn) + self.assertEqual(game.end().board().fen(), "rnbqkbnr/pppppppp/8/8/8/P7/1PPPPPPP/RNBQKBNR b KQkq - 0 1") def test_add_line(self): game = chess.pgn.Game() @@ -2077,29 +2707,32 @@ def test_add_line(self): tail = game.add_line(moves, starting_comment="start", comment="end", nags=(17, 42)) self.assertEqual(tail.parent.move, chess.Move.from_uci("g1f3")) - self.assertEqual(tail.parent.starting_comment, "start") - self.assertEqual(tail.parent.comment, "") + self.assertEqual(tail.parent.starting_comments, ["start"]) + self.assertEqual(tail.parent.comments, []) self.assertEqual(len(tail.parent.nags), 0) self.assertEqual(tail.move, chess.Move.from_uci("d7d5")) - self.assertEqual(tail.comment, "end") + self.assertEqual(tail.comments, ["end"]) self.assertIn(42, tail.nags) - def test_main_line(self): + def test_mainline(self): moves = [chess.Move.from_uci(uci) for uci in ["d2d3", "g8f6", "e2e4"]] game = chess.pgn.Game() game.add_line(moves) - self.assertEqual(list(game.main_line()), moves) + self.assertEqual(list(game.mainline_moves()), moves) + self.assertTrue(game.mainline_moves()) + self.assertEqual(list(reversed(game.mainline_moves())), list(reversed(moves))) + self.assertEqual(str(game.mainline_moves()), "1. d3 Nf6 2. e4") def test_lan(self): - pgn = StringIO("1. e2-e4") + pgn = io.StringIO("1. e2-e4") game = chess.pgn.read_game(pgn) self.assertEqual(game.end().move, chess.Move.from_uci("e2e4")) def test_variants(self): - pgn = StringIO(textwrap.dedent("""\ + pgn = io.StringIO(textwrap.dedent("""\ [Variant "Atomic"] [FEN "8/8/1b6/8/3Nk3/4K3/8/8 w - - 0 1"] @@ -2122,544 +2755,1269 @@ def test_cutechess_fischerrandom(self): self.assertTrue(board.chess960) self.assertEqual(board.fen(), "nbbrknrq/pppppppp/8/8/8/8/PPPPPPPP/NBBRKNRQ w KQkq - 0 1") + def test_z0(self): + with open("data/pgn/anastasian-lewis.pgn") as pgn: + game = chess.pgn.read_game(pgn) + board = game.end().board() + self.assertEqual(board.fen(), "5rk1/2p1R2p/p5pb/2PPR3/8/2Q2B2/5P2/4K2q w - - 3 43") -class CraftyTestCase(unittest.TestCase): - - def setUp(self): - try: - self.engine = chess.xboard.popen_engine("crafty") - except OSError: - self.skipTest("need crafty") - - self.engine.xboard() - self.engine.send_line("log off") - - def tearDown(self): - self.engine.quit() - - def test_st(self): - self.engine.new() - self.engine.st(1) - self.engine.go() - - def test_sd(self): - self.engine.new() - self.engine.sd(5) - self.engine.go() - - def test_level(self): - self.engine.new() - self.engine.level(1, 0, 1, 0) - self.engine.go() - - def test_time(self): - self.engine.new() - self.engine.level(0, 1, 0, 0) - self.engine.time(100) - self.engine.go() - - def test_mate_search(self): - board = chess.Board() - board.set_fen("4r1k1/pQ3pp1/7p/4q3/4r3/P7/1P2nPPP/2BR1R1K b - - 0 1") # Mate in 2 - self.engine.setboard(board) - self.engine.sd(15) # Just to be safe - post_handler = chess.xboard.PostHandler() - self.engine.post_handlers.append(post_handler) - self.engine.go() - - for move in post_handler.post["pv"]: - board.push(move) - self.assertTrue(board.is_checkmate(), True) + def test_uci_moves(self): + with open("data/pgn/uci-moves.pgn") as pgn: + game = chess.pgn.read_game(pgn) + board = game.end().board() + self.assertEqual(board.fen(), "8/8/2B5/4k3/4Pp2/1b6/1P3K2/8 b - - 0 57") - def test_usermove(self): - board = chess.Board() - board.set_fen("4r1k1/pQ3pp1/7p/4q3/4r3/P7/1P2nPPP/2BR1R1K b - - 0 1") # Mate in 2 - self.engine.setboard(board) - self.engine.usermove(chess.Move.from_uci("e5h2")) - self.engine.usermove(chess.Move.from_uci("h1h2")) - self.engine.sd(15) - post_handler = chess.xboard.PostHandler() - self.engine.post_handlers.append(post_handler) - self.engine.go() - - board.push(chess.Move.from_uci("e5h2")) - board.push(chess.Move.from_uci("h1h2")) - for move in post_handler.post["pv"]: - board.push(move) - self.assertTrue(board.is_checkmate(), True) + def test_wierd_header(self): + pgn = io.StringIO(r"""[Black "[=0040.34h5a4]"]""") + game = chess.pgn.read_game(pgn) + self.assertEqual(game.headers["Black"], "[=0040.34h5a4]") - def test_playother(self): - board = chess.Board() - board.set_fen("4r1k1/pQ3pp1/7p/4q3/4r3/P7/1P2nPPP/2BR1R1K b - - 0 1") # Mate in 2 - self.engine.setboard(board) - board.push(chess.Move.from_uci("e5h2")) - self.engine.playother() + def test_semicolon_comment(self): + pgn = io.StringIO("1. e4 ; e5") + game = chess.pgn.read_game(pgn) + node = game.next() + self.assertEqual(node.move, chess.Move.from_uci("e2e4")) + self.assertTrue(node.is_end()) - self.engine.sd(15) - post_handler = chess.xboard.PostHandler() - self.engine.post_handlers.append(post_handler) - self.engine.usermove(chess.Move.from_uci("e5h2")) + def test_empty_game(self): + pgn = io.StringIO(" \n\n ") + game = chess.pgn.read_game(pgn) + self.assertTrue(game is None) - for move in post_handler.post["pv"]: - board.push(move) - self.assertTrue(board.is_checkmate(), True) + def test_no_movetext(self): + pgn = io.StringIO(textwrap.dedent(""" + [Event "A"] - def test_terminate(self): - try: - engine = chess.xboard.popen_engine("crafty") - except OSError: - self.skipTest("need crafty") - engine.xboard() - engine.st(100) - engine.sd(32) - engine.go(async_callback=True) - time.sleep(0.1) + [Event "B"] + """)) - engine.terminate() - self.assertFalse(engine.is_alive()) + game = chess.pgn.read_game(pgn) + self.assertEqual(game.headers["Event"], "A") + game = chess.pgn.read_game(pgn) + self.assertEqual(game.headers["Event"], "B") - def test_kill(self): - try: - engine = chess.xboard.popen_engine("crafty") - except OSError: - self.skipTest("need crafty") - engine.xboard() - engine.st(100) - engine.sd(32) - engine.go(async_callback=True) + self.assertTrue(chess.pgn.read_game(pgn) is None) - time.sleep(0.1) + def test_subgame(self): + pgn = io.StringIO("1. d4 d5 (1... Nf6 2. c4 (2. Nf3 g6 3. g3))") + game = chess.pgn.read_game(pgn) + node = game.next().variations[1] + subgame = node.accept_subgame(chess.pgn.GameBuilder()) + self.assertEqual(subgame.headers["FEN"], "rnbqkb1r/pppppppp/5n2/8/3P4/8/PPP1PPPP/RNBQKBNR w KQkq - 1 2") + self.assertEqual(subgame.next().move, chess.Move.from_uci("c2c4")) + self.assertEqual(subgame.variations[1].move, chess.Move.from_uci("g1f3")) + + def test_is_wild(self): + headers = chess.pgn.Headers() + headers["Variant"] = "wild/1" + self.assertTrue(headers.is_wild()) + + def test_my_game_node(self): + class MyGameNode(chess.pgn.GameNode): + def add_variation(self, move, *, comment="", starting_comment="", nags=[]): + return MyChildNode(self, move, comment=comment, starting_comment=starting_comment, nags=nags) + + class MyChildNode(chess.pgn.ChildNode, MyGameNode): + pass - engine.kill() - self.assertFalse(engine.is_alive()) + class MyGame(chess.pgn.Game, MyGameNode): + pass + pgn = io.StringIO("1. e4") + game = chess.pgn.read_game(pgn, Visitor=MyGame.builder) + self.assertTrue(isinstance(game, MyGame)) + node = game.variation(chess.Move.from_uci("e2e4")) + self.assertTrue(isinstance(node, MyGameNode)) -class StockfishTestCase(unittest.TestCase): + def test_recursion(self): + board = chess.Board("4k3/8/8/8/8/8/8/4K3 w - - 0 1") + for _ in range(1000): + board.push(chess.Move(chess.E1, chess.E2)) + board.push(chess.Move(chess.E8, chess.E7)) + board.push(chess.Move(chess.E2, chess.E1)) + board.push(chess.Move(chess.E7, chess.E8)) + game = chess.pgn.Game.from_board(board) + self.assertTrue(str(game).endswith("2000. Ke1 Ke8 1/2-1/2")) - def setUp(self): - try: - self.engine = chess.uci.popen_engine("stockfish") - except OSError: - self.skipTest("need stockfish") + def test_annotations(self): + game = chess.pgn.Game() + game.comments = ["foo [%bar] baz"] + + self.assertTrue(game.clock() is None) + clock = 12345 + game.set_clock(clock) + self.assertEqual(game.comments, ["foo [%bar] baz", "[%clk 3:25:45]"]) + self.assertEqual(game.clock(), clock) + + self.assertTrue(game.eval() is None) + game.set_eval(chess.engine.PovScore(chess.engine.Cp(-80), chess.WHITE)) + self.assertEqual(game.comments, ["foo [%bar] baz", "[%clk 3:25:45]", "[%eval -0.80]"]) + self.assertEqual(game.eval().white().score(), -80) + self.assertEqual(game.eval_depth(), None) + game.set_eval(chess.engine.PovScore(chess.engine.Mate(1), chess.WHITE), 5) + self.assertEqual(game.comments, ["foo [%bar] baz", "[%clk 3:25:45]", "[%eval #1,5]"]) + self.assertEqual(game.eval().white().mate(), 1) + self.assertEqual(game.eval_depth(), 5) + + self.assertEqual(game.arrows(), []) + game.set_arrows([(chess.A1, chess.A1), chess.svg.Arrow(chess.A1, chess.H1, color="red"), chess.svg.Arrow(chess.B1, chess.B8)]) + self.assertEqual(game.comments, ["[%csl Ga1][%cal Ra1h1,Gb1b8]", "foo [%bar] baz", "[%clk 3:25:45]", "[%eval #1,5]"]) + arrows = game.arrows() + self.assertEqual(len(arrows), 3) + self.assertEqual(arrows[0].color, "green") + self.assertEqual(arrows[1].color, "red") + self.assertEqual(arrows[2].color, "green") + + self.assertTrue(game.emt() is None) + emt = 321 + game.set_emt(emt) + self.assertEqual(game.comments, ["[%csl Ga1][%cal Ra1h1,Gb1b8]", "foo [%bar] baz", "[%clk 3:25:45]", "[%eval #1,5]", "[%emt 0:05:21]"]) + self.assertEqual(game.emt(), emt) + + game.set_eval(None) + self.assertEqual(game.comments, ["[%csl Ga1][%cal Ra1h1,Gb1b8]", "foo [%bar] baz", "[%clk 3:25:45]", "[%emt 0:05:21]"]) + + game.set_emt(None) + self.assertEqual(game.comments, ["[%csl Ga1][%cal Ra1h1,Gb1b8]", "foo [%bar] baz", "[%clk 3:25:45]"]) + + game.set_clock(None) + game.set_arrows([]) + self.assertEqual(game.comments, ["foo [%bar] baz"]) + + def test_eval(self): + game = chess.pgn.Game() + for cp in range(199, 220): + game.set_eval(chess.engine.PovScore(chess.engine.Cp(cp), chess.WHITE)) + self.assertEqual(game.eval().white().cp, cp) - self.engine.uci() + def test_float_emt(self): + game = chess.pgn.Game() + game.comments = ["[%emt 0:00:01.234]"] + self.assertEqual(game.emt(), 1.234) - def tearDown(self): - self.engine.quit() + game.set_emt(6.54321) + self.assertEqual(game.comments, ["[%emt 0:00:06.543]"]) + self.assertEqual(game.emt(), 6.543) - def test_forced_mates(self): - epds = [ - "1k1r4/pp1b1R2/3q2pp/4p3/2B5/4Q3/PPP2B2/2K5 b - - bm Qd1+; id \"BK.01\";", - "6k1/N1p3pp/2p5/3n1P2/4K3/1P5P/P1Pr1r2/R1R5 b - - bm Rf4+; id \"Clausthal 2014\";", - ] + game.set_emt(-70) + self.assertEqual(game.comments, ["[%emt 0:00:00]"]) # Clamped + self.assertEqual(game.emt(), 0) - board = chess.Board() + def test_float_clk(self): + game = chess.pgn.Game() + game.comments = ["[%clk 0:00:01.234]"] + self.assertEqual(game.clock(), 1.234) - for epd in epds: - operations = board.set_epd(epd) - self.engine.ucinewgame() - self.engine.position(board) - result = self.engine.go(mate=5) - self.assertIn(result[0], operations["bm"], operations["id"]) + game.set_clock(6.54321) + self.assertEqual(game.comments, ["[%clk 0:00:06.543]"]) + self.assertEqual(game.clock(), 6.543) - def test_async_callback(self): - self.async_callback_called = threading.Event() + game.set_clock(-70) + self.assertEqual(game.comments, ["[%clk 0:00:00]"]) # Clamped + self.assertEqual(game.clock(), 0) - def async_callback(command): - self.async_callback_called.set() + def test_node_turn(self): + game = chess.pgn.Game() + self.assertEqual(game.turn(), chess.WHITE) + node = game.add_variation(chess.Move.from_uci("a2a3")) + self.assertEqual(node.turn(), chess.BLACK) + node = node.add_variation(chess.Move.from_uci("a7a6")) + self.assertEqual(node.turn(), chess.WHITE) - command = self.engine.isready(async_callback=async_callback) + game = chess.pgn.Game() + game.setup("4k3/8/8/8/8/8/8/4K3 b - - 7 6") + self.assertEqual(game.turn(), chess.BLACK) + node = game.add_variation(chess.Move.from_uci("e8e7")) + self.assertEqual(node.turn(), chess.WHITE) + node = node.add_variation(chess.Move.from_uci("e1e2")) + self.assertEqual(node.turn(), chess.BLACK) + + def test_skip_inner_variation(self): + class BlackVariationsOnly(chess.pgn.GameBuilder): + def begin_variation(self): + self.skipping = self.variation_stack[-1].turn() != chess.WHITE + if self.skipping: + return chess.pgn.SKIP + else: + return super().begin_variation() - # Wait for the command to be executed. - command.result() - self.assertTrue(command.done()) + def end_variation(self): + if self.skipping: + self.skipping = False + else: + return super().end_variation() - self.async_callback_called.wait() - self.assertTrue(self.async_callback_called.is_set()) + pgn = "1. e4 e5 ( 1... d5 2. exd5 Qxd5 3. Nc3 ( 3. c4 ) 3... Qa5 ) *" + expected_pgn = "1. e4 e5 ( 1... d5 2. exd5 Qxd5 3. Nc3 Qa5 ) *" - def test_initialization(self): - self.assertIn("Stockfish", self.engine.name) - self.assertEqual(self.engine.options["UCI_Chess960"].name, "UCI_Chess960") - self.assertEqual(self.engine.options["uci_Chess960"].type, "check") - self.assertEqual(self.engine.options["UCI_CHESS960"].default, False) + # Driven by parser. + game = chess.pgn.read_game(io.StringIO(pgn), Visitor=BlackVariationsOnly) + self.assertEqual(game.accept(chess.pgn.StringExporter(headers=False)), expected_pgn) - def test_terminate(self): - self.engine.go(infinite=True, async_callback=True) + # Driven by game tree traversal. + game = chess.pgn.read_game(io.StringIO(pgn)).accept(BlackVariationsOnly()) + self.assertEqual(game.accept(chess.pgn.StringExporter(headers=False)), expected_pgn) + def test_utf8_bom(self): + not_utf8_sig = "utf-8" + with open("data/pgn/utf8-bom.pgn", encoding=not_utf8_sig) as pgn: + game = chess.pgn.read_game(pgn) + self.assertEqual(game.headers["Event"], "A") -class SpurEngineTestCase(unittest.TestCase): + game = chess.pgn.read_game(pgn) + self.assertEqual(game.headers["Event"], "B") - def setUp(self): - try: - import spur - self.shell = spur.LocalShell() - except ImportError: - self.skipTest("need spur library") + game = chess.pgn.read_game(pgn) + self.assertEqual(game, None) - try: - self.engine = chess.uci.spur_spawn_engine(self.shell, ["stockfish"]) - except OSError: - self.skipTest("need stockfish") - def test_local_shell(self): - self.engine.uci() +@unittest.skipIf(sys.platform == "win32" and (3, 8, 0) <= sys.version_info < (3, 8, 1), "https://bugs.python.org/issue34679") +class EngineTestCase(unittest.TestCase): - self.engine.ucinewgame() + def test_uci_option_map_equality(self): + a = chess.engine.UciOptionMap() + b = chess.engine.UciOptionMap() + c = chess.engine.UciOptionMap() + self.assertEqual(a, b) - # Find fools mate. - board = chess.Board() - board.push_san("g4") - board.push_san("e5") - board.push_san("f4") - self.engine.position(board) - bestmove, pondermove = self.engine.go(mate=1, movetime=2000) - self.assertEqual(board.san(bestmove), "Qh4#") + a["fOO"] = "bAr" + b["foo"] = "bAr" + c["fOo"] = "bar" + self.assertEqual(a, b) + self.assertEqual(b, a) + self.assertNotEqual(a, c) + self.assertNotEqual(c, a) + self.assertNotEqual(b, c) - self.engine.quit() + b["hello"] = "world" + self.assertNotEqual(a, b) + self.assertNotEqual(b, a) - def test_terminate(self): - self.engine.uci() - self.engine.go(infinite=True, async_callback=True) + def test_uci_option_map_len(self): + a = chess.engine.UciOptionMap() + self.assertEqual(len(a), 0) - time.sleep(0.1) + a["key"] = "value" + self.assertEqual(len(a), 1) - self.engine.terminate() - self.assertFalse(self.engine.is_alive()) + del a["key"] + self.assertEqual(len(a), 0) - def test_kill(self): - self.engine.uci() - self.engine.go(infinite=True, async_callback=True) + def test_score_ordering(self): + order = [ + chess.engine.Mate(-0), + chess.engine.Mate(-1), + chess.engine.Mate(-99), + chess.engine.Cp(-123), + chess.engine.Cp(-50), + chess.engine.Cp(0), + chess.engine.Cp(+30), + chess.engine.Cp(+800), + chess.engine.Mate(+77), + chess.engine.Mate(+1), + chess.engine.MateGiven, + ] - time.sleep(0.1) + for i, a in enumerate(order): + for j, b in enumerate(order): + self.assertEqual(i < j, a < b, f"{a!r} < {b!r}") + self.assertEqual(i == j, a == b, f"{a!r} == {b!r}") + self.assertEqual(i <= j, a <= b) + self.assertEqual(i != j, a != b) + self.assertEqual(i > j, a > b) + self.assertEqual(i >= j, a >= b) + self.assertEqual(i < j, a.score(mate_score=100000) < b.score(mate_score=100000)) + + for model in ["sf12", "sf14", "sf15", "sf15.1", "sf16", "sf16.1"]: + self.assertTrue(not (i < j) or a.wdl(model=model).expectation() <= b.wdl(model=model).expectation()) + self.assertTrue(not (i < j) or a.wdl(model=model).winning_chance() <= b.wdl(model=model).winning_chance()) + self.assertTrue(not (i < j) or a.wdl(model=model).losing_chance() >= b.wdl(model=model).losing_chance()) + + def test_score(self): + # Negation. + self.assertEqual(-chess.engine.Cp(+20), chess.engine.Cp(-20)) + self.assertEqual(-chess.engine.Mate(+4), chess.engine.Mate(-4)) + self.assertEqual(-chess.engine.Mate(-0), chess.engine.MateGiven) + self.assertEqual(-chess.engine.MateGiven, chess.engine.Mate(-0)) + + # Score. + self.assertEqual(chess.engine.Cp(-300).score(), -300) + self.assertEqual(chess.engine.Mate(+5).score(), None) + self.assertEqual(chess.engine.Mate(+5).score(mate_score=100000), 99995) + self.assertEqual(chess.engine.Mate(-7).score(mate_score=100000), -99993) + + # Mate. + self.assertEqual(chess.engine.Cp(-300).mate(), None) + self.assertEqual(chess.engine.Mate(+5).mate(), 5) + + # Wdl. + self.assertEqual(chess.engine.MateGiven.wdl().expectation(), 1) + self.assertEqual(chess.engine.Mate(0).wdl().expectation(), 0) + self.assertEqual(chess.engine.Cp(0).wdl().expectation(), 0.5) + + for cp in map(chess.engine.Cp, range(-1050, 1100, 50)): + wdl = cp.wdl() + self.assertTrue(wdl) + self.assertAlmostEqual(wdl.winning_chance() + wdl.drawing_chance() + wdl.losing_chance(), 1) + + self.assertFalse(chess.engine.Wdl(0, 0, 0)) + + def test_wdl_model(self): + self.assertEqual(chess.engine.Cp(131).wdl(model="sf12", ply=25), chess.engine.Wdl(524, 467, 9)) + self.assertEqual(chess.engine.Cp(146).wdl(model="sf14", ply=25), chess.engine.Wdl(601, 398, 1)) + self.assertEqual(chess.engine.Cp(40).wdl(model="sf15", ply=25), chess.engine.Wdl(58, 937, 5)) + self.assertEqual(chess.engine.Cp(100).wdl(model="sf15.1", ply=64), chess.engine.Wdl(497, 503, 0)) + self.assertEqual(chess.engine.Cp(-52).wdl(model="sf16", ply=63), chess.engine.Wdl(0, 932, 68)) + self.assertEqual(chess.engine.Cp(51).wdl(model="sf16.1", ply=158), chess.engine.Wdl(36, 964, 0)) + + @catchAndSkip(FileNotFoundError, "need stockfish") + def test_sf_forced_mates(self): + with chess.engine.SimpleEngine.popen_uci("stockfish", debug=True) as engine: + epds = [ + "1k1r4/pp1b1R2/3q2pp/4p3/2B5/4Q3/PPP2B2/2K5 b - - bm Qd1+; id \"BK.01\";", + "6k1/N1p3pp/2p5/3n1P2/4K3/1P5P/P1Pr1r2/R1R5 b - - bm Rf4+; id \"Clausthal 2014\";", + ] - self.engine.kill() - self.assertFalse(self.engine.is_alive()) + board = chess.Board() - def test_async_terminate(self): - command = self.engine.terminate(async_callback=True) - command.result() - self.assertTrue(command.done()) + for epd in epds: + operations = board.set_epd(epd) + result = engine.play(board, chess.engine.Limit(mate=3), game=object()) + self.assertIn(result.move, operations["bm"], operations["id"]) + + @catchAndSkip(FileNotFoundError, "need stockfish") + def test_sf_options(self): + with chess.engine.SimpleEngine.popen_uci("stockfish", debug=True) as engine: + self.assertEqual(engine.options["UCI_Chess960"].name, "UCI_Chess960") + self.assertEqual(engine.options["uci_Chess960"].type, "check") + self.assertEqual(engine.options["UCI_CHESS960"].default, False) + + @catchAndSkip(FileNotFoundError, "need stockfish") + def test_sf_analysis(self): + with chess.engine.SimpleEngine.popen_uci("stockfish", setpgrp=True, debug=True) as engine: + board = chess.Board("8/6K1/1p1B1RB1/8/2Q5/2n1kP1N/3b4/4n3 w - - 0 1") + limit = chess.engine.Limit(depth=40) + analysis = engine.analysis(board, limit) + with analysis: + for info in iter(analysis.next, None): + if "score" in info and info["score"].is_mate(): + break + else: + self.fail("never found a mate score") + + for info in analysis: + if "score" in info and info["score"].white() >= chess.engine.Mate(+2): + break + + analysis.wait() + self.assertFalse(analysis.would_block()) + + self.assertEqual(analysis.info["score"].relative, chess.engine.Mate(+2)) + self.assertEqual(analysis.multipv[0]["score"].black(), chess.engine.Mate(-2)) + + # Exhaust remaining information. + was_empty = analysis.empty() + was_really_empty = True + for info in analysis: + was_really_empty = False + self.assertEqual(was_really_empty, was_empty) + self.assertTrue(analysis.empty()) + self.assertFalse(analysis.would_block()) + for info in analysis: + self.fail("all info should have been consumed") + + @catchAndSkip(FileNotFoundError, "need stockfish") + def test_sf_multipv(self): + with chess.engine.SimpleEngine.popen_uci("stockfish", debug=True) as engine: + board = chess.Board("r2qr1k1/pb2npp1/1pn1p2p/8/3P4/P1PQ1N2/B4PPP/R1B1R1K1 w - - 2 15") + result = engine.analyse(board, chess.engine.Limit(depth=1), multipv=3) + self.assertEqual(len(result), 3) + self.assertTrue(result[0]["score"].relative >= result[1]["score"].relative) + self.assertTrue(result[1]["score"].relative >= result[2]["score"].relative) + + @catchAndSkip(FileNotFoundError, "need stockfish") + def test_sf_quit(self): + engine = chess.engine.SimpleEngine.popen_uci("stockfish", setpgrp=True, debug=True) + + with engine: + engine.quit() + + with self.assertRaises(chess.engine.EngineTerminatedError), engine: + engine.ping() + + @catchAndSkip(FileNotFoundError, "need fairy-stockfish") + def test_fairy_sf_initialize(self): + with chess.engine.SimpleEngine.popen_uci("fairy-stockfish", setpgrp=True, debug=True): + pass + def test_uci_option_parse(self): + async def main(): + protocol = chess.engine.UciProtocol() + mock = chess.engine.MockTransport(protocol) -class UciEngineTestCase(unittest.TestCase): + mock.expect("uci", ["option name UCI_Variant type combo default chess var bughouse var chess var mini var minishogi var threekings", "uciok"]) + await protocol.initialize() + mock.assert_done() - def setUp(self): - self.engine = chess.uci.Engine() - self.mock = chess.uci.MockProcess(self.engine) + mock.expect("isready", ["readyok"]) + await protocol.ping() + mock.assert_done() - self.mock.expect("uci", ("uciok", )) - self.engine.uci() - self.mock.assert_done() + asyncio.run(main()) - def tearDown(self): - self.engine.terminate() - self.mock.assert_terminated() - - def test_debug(self): - self.mock.expect("debug on") - self.engine.debug(True) - self.mock.assert_done() - - self.mock.expect("debug off") - self.engine.debug(False) - self.mock.assert_done() - - def test_ponderhit(self): - self.mock.expect("go ponder") - ponder_command = self.engine.go(ponder=True, async_callback=True) - self.mock.expect("ponderhit", ("bestmove e2e4", )) - self.engine.ponderhit() - self.assertEqual(ponder_command.result().bestmove, chess.Move.from_uci("e2e4")) - self.mock.assert_done() - - def test_kill(self): - self.engine.kill() - self.mock.assert_terminated() - - def test_go(self): - self.mock.expect("go infinite searchmoves e2e4 d2d4") - go_command = self.engine.go(searchmoves=[chess.Move.from_uci("e2e4"), chess.Move.from_uci("d2d4")], infinite=True, async_callback=True) - self.assertFalse(go_command.done()) - - self.mock.expect("stop", ("bestmove e2e4", )) - self.engine.stop() - bestmove, pondermove = go_command.result() - self.assertTrue(go_command.done()) - self.mock.assert_done() - self.assertEqual(bestmove, chess.Move.from_uci("e2e4")) - self.assertTrue(pondermove is None) - - self.mock.expect("go wtime 1 btime 2 winc 3 binc 4 movestogo 5 depth 6 nodes 7 mate 8 movetime 9", ( - "bestmove d2d4 ponder d7d5", - )) - self.engine.go(wtime=1, btime=2, winc=3, binc=4, movestogo=5, depth=6, nodes=7, mate=8, movetime=9) - self.mock.assert_done() - - self.mock.expect("go movetime 3333", ( - "bestmove (none) ponder (none)", - )) - bestmove, pondermove = self.engine.go(movetime=3333) - self.assertTrue(bestmove is None) - self.assertTrue(pondermove is None) - self.mock.assert_done() - - self.mock.expect("go mate 2", ( - "bestmove (none)", - )) - bestmove, pondermove = self.engine.go(mate=2) - self.assertTrue(bestmove is None) - self.assertTrue(pondermove is None) - self.mock.assert_done() - - def test_info_refutation(self): - handler = chess.uci.InfoHandler() - self.engine.info_handlers.append(handler) - - # Set a position where d1h5 g6h5 would be a legal sequence of moves. - fen = "8/8/6k1/8/8/8/1K6/3B4 w - - 0 1" - self.mock.expect("position fen " + fen) - self.engine.position(chess.Board(fen)) - - self.engine.on_line_received("info refutation d1h5 g6h5") - - d1h5 = chess.Move.from_uci("d1h5") - g6h5 = chess.Move.from_uci("g6h5") - - with handler as info: - self.assertEqual(len(info["refutation"][d1h5]), 1) - self.assertEqual(info["refutation"][d1h5][0], g6h5) - - self.engine.on_line_received("info refutation d1h5") - with handler as info: - self.assertTrue(info["refutation"][d1h5] is None) - - def test_info_string(self): - handler = chess.uci.InfoHandler() - self.engine.info_handlers.append(handler) - - self.engine.on_line_received("info string goes to end no matter score cp 4 what") - with handler as info: - self.assertEqual(info["string"], "goes to end no matter score cp 4 what") - self.assertNotIn(1, info["score"]) - - def test_info_currline(self): - handler = chess.uci.InfoHandler() - self.engine.info_handlers.append(handler) - - self.engine.on_line_received("info currline 0 e2e4 e7e5") - with handler as info: - self.assertEqual(info["currline"][0], [ - chess.Move.from_uci("e2e4"), - chess.Move.from_uci("e7e5"), + @catchAndSkip(FileNotFoundError, "need crafty") + def test_crafty_play_to_mate(self): + logging.disable(logging.WARNING) + try: + with tempfile.TemporaryDirectory(prefix="crafty") as tmpdir: + with chess.engine.SimpleEngine.popen_xboard("crafty", setpgrp=True, debug=True, cwd=tmpdir) as engine: + board = chess.Board("2bqkbn1/2pppp2/np2N3/r3P1p1/p2N2B1/5Q2/PPPPKPP1/RNB2r2 w KQkq - 0 1") + limit = chess.engine.Limit(depth=10) + while not board.is_game_over() and len(board.move_stack) < 5: + result = engine.play(board, limit, ponder=True) + board.push(result.move) + self.assertTrue(board.is_checkmate()) + engine.quit() + finally: + logging.disable(logging.NOTSET) + + @catchAndSkip(FileNotFoundError, "need crafty") + def test_crafty_analyse(self): + logging.disable(logging.WARNING) + try: + with tempfile.TemporaryDirectory(prefix="crafty") as tmpdir: + with chess.engine.SimpleEngine.popen_xboard("crafty", debug=True, cwd=tmpdir) as engine: + board = chess.Board("2bqkbn1/2pppp2/np2N3/r3P1p1/p2N2B1/5Q2/PPPPKPP1/RNB2r2 w KQkq - 0 1") + limit = chess.engine.Limit(depth=7, time=2.0) + info = engine.analyse(board, limit) + self.assertTrue(info["score"].relative > chess.engine.Cp(1000)) + engine.quit() + finally: + logging.disable(logging.NOTSET) + + @catchAndSkip(FileNotFoundError, "need crafty") + def test_crafty_ping(self): + with tempfile.TemporaryDirectory(prefix="crafty") as tmpdir: + with chess.engine.SimpleEngine.popen_xboard("crafty", debug=True, cwd=tmpdir) as engine: + engine.ping() + engine.quit() + + def test_uci_ping(self): + async def main(): + protocol = chess.engine.UciProtocol() + mock = chess.engine.MockTransport(protocol) + + mock.expect("uci", ["uciok"]) + await protocol.initialize() + mock.assert_done() + + mock.expect("isready", ["readyok"]) + await protocol.ping() + mock.assert_done() + + asyncio.run(main()) + + def test_uci_debug(self): + async def main(): + protocol = chess.engine.UciProtocol() + mock = chess.engine.MockTransport(protocol) + + mock.expect("debug on", []) + protocol.debug() + mock.assert_done() + + mock.expect("debug off", []) + protocol.debug(False) + mock.assert_done() + + asyncio.run(main()) + + def test_uci_go(self): + async def main(): + protocol = chess.engine.UciProtocol() + mock = chess.engine.MockTransport(protocol) + + # Initialize. + mock.expect("uci", ["uciok"]) + await protocol.initialize() + + # Pondering. + mock.expect("ucinewgame") + mock.expect("isready", ["readyok"]) + mock.expect("position startpos") + mock.expect("go movetime 123 searchmoves e2e4 d2d4", ["info string searching ...", "bestmove d2d4 ponder d7d5"]) + mock.expect("position startpos moves d2d4 d7d5") + mock.expect("go ponder movetime 123") + board = chess.Board() + result = await protocol.play(board, chess.engine.Limit(time=0.123), + root_moves=[board.parse_san("e4"), board.parse_san("d4")], + ponder=True, + info=chess.engine.INFO_ALL) + self.assertEqual(result.move, chess.Move.from_uci("d2d4")) + self.assertEqual(result.ponder, chess.Move.from_uci("d7d5")) + self.assertEqual(result.info["string"], "searching ...") + mock.assert_done() + + mock.expect("stop", ["bestmove c2c4"]) + + # Limits. + mock.expect("position startpos") + mock.expect("go wtime 1 btime 2 winc 3 binc 4 movestogo 5 depth 6 nodes 7 mate 8 movetime 9", ["bestmove d2d4"]) + limit = chess.engine.Limit(white_clock=0.001, black_clock=0.002, + white_inc=0.003, black_inc=0.004, + remaining_moves=5, depth=6, nodes=7, + mate=8, time=0.009) + result = await protocol.play(board, limit) + self.assertEqual(result.move, chess.Move.from_uci("d2d4")) + self.assertEqual(result.ponder, None) + mock.assert_done() + + asyncio.run(main()) + + def test_iota_log(self): + async def main(): + protocol = chess.engine.UciProtocol() + mock = chess.engine.MockTransport(protocol) + + # Initialize. + mock.expect("uci", ["uciok"]) + await protocol.initialize() + + # Iota writes invalid \0 character in old version. + mock.expect("ucinewgame") + mock.expect("isready", ["readyok"]) + mock.expect("position startpos moves d2d4") + mock.expect("go movetime 5000", ["bestmove e7e6\0"]) + board = chess.Board() + board.push_uci("d2d4") + with self.assertRaises(chess.engine.EngineError): + await protocol.play(board, chess.engine.Limit(time=5.0)) + mock.assert_done() + + asyncio.run(main()) + + def test_uci_analyse_mode(self): + async def main(): + protocol = chess.engine.UciProtocol() + mock = chess.engine.MockTransport(protocol) + + # Initialize. + mock.expect("uci", [ + "option name UCI_AnalyseMode type check default false", + "uciok", ]) + await protocol.initialize() + + # Analyse. + mock.expect("setoption name UCI_AnalyseMode value true") + mock.expect("ucinewgame") + mock.expect("isready", ["readyok"]) + mock.expect("position startpos") + mock.expect("go infinite") + mock.expect("stop", ["bestmove e2e4"]) + result = await protocol.analysis(chess.Board()) + self.assertTrue(result.would_block()) + result.stop() + best = await result.wait() + self.assertFalse(result.would_block()) + self.assertEqual(best.move, chess.Move.from_uci("e2e4")) + self.assertTrue(best.ponder is None) + mock.assert_done() + + # Explicitly disable. + mock.expect("setoption name UCI_AnalyseMode value false") + await protocol.configure({"UCI_AnalyseMode": False}) + mock.assert_done() + + # Analyse again. + mock.expect("position startpos") + mock.expect("go infinite") + mock.expect("stop", ["bestmove e2e4 ponder e7e5"]) + result = await protocol.analysis(chess.Board()) + result.stop() + best = await result.wait() + self.assertEqual(best.move, chess.Move.from_uci("e2e4")) + self.assertEqual(best.ponder, chess.Move.from_uci("e7e5")) + mock.assert_done() + + asyncio.run(main()) + + def test_uci_play_after_analyse(self): + async def main(): + protocol = chess.engine.UciProtocol() + mock = chess.engine.MockTransport(protocol) + + # Initialize. + mock.expect("uci", ["uciok"]) + await protocol.initialize() + + # Ponder. + board = chess.Board() + mock.expect("ucinewgame") + mock.expect("isready", ["readyok"]) + mock.expect("position startpos") + mock.expect("go depth 20", ["bestmove a2a4 ponder a7a5"]) + info = await protocol.analyse(board, chess.engine.Limit(depth=20)) + self.assertEqual(info, {}) + + # Play. + mock.expect("position startpos") + mock.expect("go movetime 3000", ["bestmove a2a4 ponder a7a5"]) + await protocol.play(board, chess.engine.Limit(time=3)) + + mock.assert_done() + + asyncio.run(main()) + + def test_uci_ponderhit(self): + async def main(): + protocol = chess.engine.UciProtocol() + mock = chess.engine.MockTransport(protocol) + + # Initialize. + mock.expect("uci", [ + "option name Hash type spin default 16 min 1 max 33554432", + "option name Ponder type check default false", + "option name UCI_Opponent type string", + "uciok", + ]) + await protocol.initialize() + + primary_opponent = chess.engine.Opponent("Eliza", None, 3500, True) + await protocol.send_opponent_information(opponent=primary_opponent) + + # First search. + mock.expect("setoption name Ponder value true") + mock.expect("ucinewgame") + mock.expect("setoption name UCI_Opponent value none 3500 computer Eliza") + mock.expect("isready", ["readyok"]) + mock.expect("position startpos") + mock.expect("go movetime 1000", ["bestmove d2d4 ponder g8f6"]) + mock.expect("position startpos moves d2d4 g8f6") + mock.expect("go ponder movetime 1000") + board = chess.Board() + result = await protocol.play(board, chess.engine.Limit(time=1), ponder=True) + self.assertEqual(result.move, chess.Move.from_uci("d2d4")) + self.assertEqual(result.ponder, chess.Move.from_uci("g8f6")) + + # Ponderhit. + board.push(result.move) + board.push(result.ponder) + mock.expect("ponderhit", ["bestmove c2c4 ponder e7e6"]) + mock.expect("position startpos moves d2d4 g8f6 c2c4 e7e6") + mock.expect("go ponder movetime 2000") + result = await protocol.play(board, chess.engine.Limit(time=2), ponder=True) + self.assertEqual(result.move, chess.Move.from_uci("c2c4")) + self.assertEqual(result.ponder, chess.Move.from_uci("e7e6")) + + # Ponderhit prevented by changed option. + board.push(result.move) + board.push(result.ponder) + mock.expect("stop", ["bestmove g2g3 ponder f8b4"]) + mock.expect("setoption name Hash value 32") + mock.expect("position startpos moves d2d4 g8f6 c2c4 e7e6") + mock.expect("go movetime 3000", ["bestmove b1c3 ponder f8b4"]) + mock.expect("position startpos moves d2d4 g8f6 c2c4 e7e6 b1c3 f8b4") + mock.expect("go ponder movetime 3000") + result = await protocol.play(board, chess.engine.Limit(time=3), ponder=True, options={"Hash": 32}) + self.assertEqual(result.move, chess.Move.from_uci("b1c3")) + self.assertEqual(result.ponder, chess.Move.from_uci("f8b4")) + + # Ponderhit prevented by reverted option. + board.push(result.move) + board.push(result.ponder) + mock.expect("stop", ["bestmove e2e3 ponder e8g8"]) + mock.expect("setoption name Hash value 16") + mock.expect("position startpos moves d2d4 g8f6 c2c4 e7e6 b1c3 f8b4") + mock.expect("go movetime 3000", ["bestmove d1c2 ponder d7d5"]) + mock.expect("position startpos moves d2d4 g8f6 c2c4 e7e6 b1c3 f8b4 d1c2 d7d5") + mock.expect("go ponder movetime 3000") + result = await protocol.play(board, chess.engine.Limit(time=3), ponder=True) + self.assertEqual(result.move, chess.Move.from_uci("d1c2")) + self.assertEqual(result.ponder, chess.Move.from_uci("d7d5")) + + # Interject analysis. + board.push(result.move) + board.push(result.ponder) + mock.expect("stop", ["bestmove c4d5 ponder e6d5"]) + mock.expect("setoption name Ponder value false") + mock.expect("position startpos moves d2d4 g8f6 c2c4 e7e6 b1c3 f8b4 d1c2 d7d5") + mock.expect("go movetime 4000", ["bestmove c4d5 ponder e6d5"]) + await protocol.analyse(board, chess.engine.Limit(time=4)) + + # Interjected analysis prevents ponderhit. + mock.expect("setoption name Ponder value true") + mock.expect("position startpos moves d2d4 g8f6 c2c4 e7e6 b1c3 f8b4 d1c2 d7d5") + mock.expect("go movetime 5000", ["bestmove c4d5 ponder e6d5"]) + mock.expect("position startpos moves d2d4 g8f6 c2c4 e7e6 b1c3 f8b4 d1c2 d7d5 c4d5 e6d5") + mock.expect("go ponder movetime 5000") + await protocol.play(board, chess.engine.Limit(time=5), ponder=True) + + # Ponderhit prevented by new opponent, which starts a new game. + board.push(chess.Move.from_uci("c4d5")) + board.push(chess.Move.from_uci("e6d5")) + mock.expect("stop", ["bestmove c1g5 ponder h7h6"]) + mock.expect("ucinewgame") + mock.expect("setoption name UCI_Opponent value GM 3000 human Guy Chapman") + mock.expect("isready", ["readyok"]) + mock.expect("position startpos moves d2d4 g8f6 c2c4 e7e6 b1c3 f8b4 d1c2 d7d5 c4d5 e6d5") + mock.expect("go movetime 5000", ["bestmove c1g5 ponder h7h6"]) + mock.expect("position startpos moves d2d4 g8f6 c2c4 e7e6 b1c3 f8b4 d1c2 d7d5 c4d5 e6d5 c1g5 h7h6") + mock.expect("go ponder movetime 5000") + opponent = chess.engine.Opponent("Guy Chapman", "GM", 3000, False) + await protocol.play(board, chess.engine.Limit(time=5), ponder=True, opponent=opponent) + + # Ponderhit prevented by restoration of previous opponent, which again starts a new game. + board.push(chess.Move.from_uci("c1g5")) + board.push(chess.Move.from_uci("h7h6")) + mock.expect("stop", ["bestmove g5h4 ponder b8c6"]) + mock.expect("ucinewgame") + mock.expect("setoption name UCI_Opponent value none 3500 computer Eliza") + mock.expect("isready", ["readyok"]) + mock.expect("position startpos moves d2d4 g8f6 c2c4 e7e6 b1c3 f8b4 d1c2 d7d5 c4d5 e6d5 c1g5 h7h6") + mock.expect("go movetime 5000", ["bestmove g5h4 ponder b8c6"]) + mock.expect("position startpos moves d2d4 g8f6 c2c4 e7e6 b1c3 f8b4 d1c2 d7d5 c4d5 e6d5 c1g5 h7h6 g5h4 b8c6") + mock.expect("go ponder movetime 5000") + await protocol.play(board, chess.engine.Limit(time=5), ponder=True) + + mock.assert_done() + + asyncio.run(main()) + + def test_uci_info(self): + # Info: refutation. + board = chess.Board("8/8/6k1/8/8/8/1K6/3B4 w - - 0 1") + info = chess.engine._parse_uci_info("refutation d1h5 g6h5", board) + self.assertEqual(info["refutation"][chess.Move.from_uci("d1h5")], [chess.Move.from_uci("g6h5")]) + + info = chess.engine._parse_uci_info("refutation d1h5", board) + self.assertEqual(info["refutation"][chess.Move.from_uci("d1h5")], []) + + # Info: string. + info = chess.engine._parse_uci_info("string goes to end no matter score cp 4 what", board) + self.assertEqual(info["string"], "goes to end no matter score cp 4 what") + + # Info: currline. + info = chess.engine._parse_uci_info("currline 0 e2e4 e7e5", chess.Board()) + self.assertEqual(info["currline"][0], [chess.Move.from_uci("e2e4"), chess.Move.from_uci("e7e5")]) + + # Info: ebf. + info = chess.engine._parse_uci_info("ebf 0.42", board) + self.assertEqual(info["ebf"], 0.42) + + # Info: depth, seldepth, score mate. + info = chess.engine._parse_uci_info("depth 7 seldepth 8 score mate 3", board) + self.assertEqual(info["depth"], 7) + self.assertEqual(info["seldepth"], 8) + self.assertEqual(info["score"], chess.engine.PovScore(chess.engine.Mate(+3), chess.WHITE)) + + # Info: tbhits, cpuload, hashfull, time, nodes, nps, movesleft. + info = chess.engine._parse_uci_info("tbhits 123 cpuload 456 hashfull 789 movesleft 42 time 987 nodes 654 nps 321", board) + self.assertEqual(info["tbhits"], 123) + self.assertEqual(info["cpuload"], 456) + self.assertEqual(info["hashfull"], 789) + self.assertEqual(info["time"], 0.987) + self.assertEqual(info["nodes"], 654) + self.assertEqual(info["nps"], 321) + self.assertEqual(info["movesleft"], 42) + + # Hakkapeliitta double spaces. + info = chess.engine._parse_uci_info("depth 10 seldepth 9 score cp 22 time 17 nodes 48299 nps 2683000 tbhits 0", board) + self.assertEqual(info["depth"], 10) + self.assertEqual(info["seldepth"], 9) + self.assertEqual(info["score"], chess.engine.PovScore(chess.engine.Cp(22), chess.WHITE)) + self.assertEqual(info["time"], 0.017) + self.assertEqual(info["nodes"], 48299) + self.assertEqual(info["nps"], 2683000) + self.assertEqual(info["tbhits"], 0) + + # Unknown tokens. + board = chess.Board() + info = chess.engine._parse_uci_info("depth 1 unkown1 seldepth 2 unknown2 time 16 nodes 1 score cp 72 unknown3 wdl 249 747 4 multipv 1 uknown4 pv g1f3 g8f6 unknown5", board) + self.assertEqual(info["depth"], 1) + self.assertEqual(info["seldepth"], 2) + self.assertEqual(info["time"], 0.016) + self.assertEqual(info["nodes"], 1) + self.assertEqual(info["score"], chess.engine.PovScore(chess.engine.Cp(72), chess.WHITE)) + self.assertEqual(info["multipv"], 1) + self.assertEqual(info["pv"], [chess.Move.from_uci("g1f3"), chess.Move.from_uci("g8f6")]) + + # WDL (activated with UCI_ShowWDL). + info = chess.engine._parse_uci_info("depth 1 seldepth 2 time 16 nodes 1 score cp 72 wdl 249 747 4 hashfull 0 nps 400 tbhits 0 multipv 1", board) + self.assertEqual(info["wdl"].white(), chess.engine.Wdl(249, 747, 4)) + + def test_uci_result(self): + async def main(): + protocol = chess.engine.UciProtocol() + mock = chess.engine.MockTransport(protocol) + + mock.expect("uci", ["uciok"]) + await protocol.initialize() + mock.assert_done() + + limit = chess.engine.Limit(time=5) + checkmate_board = chess.Board("k7/7R/6R1/8/8/8/8/K7 w - - 0 1") + + mock.expect("ucinewgame") + mock.expect("isready", ["readyok"]) + mock.expect("position fen k7/7R/6R1/8/8/8/8/K7 w - - 0 1") + mock.expect("go movetime 5000", ["bestmove g6g8"]) + result = await protocol.play(checkmate_board, limit, game="checkmate") + self.assertEqual(result.move, checkmate_board.parse_uci("g6g8")) + checkmate_board.push(result.move) + self.assertTrue(checkmate_board.is_checkmate()) + await protocol.send_game_result(checkmate_board) + mock.assert_done() + + asyncio.run(main()) + + def test_uci_output_after_command(self): + async def main(): + protocol = chess.engine.UciProtocol() + mock = chess.engine.MockTransport(protocol) + + mock.expect("uci", [ + "Arasan v24.0.0-10-g367aa9f Copyright 1994-2023 by Jon Dart.", + "All rights reserved.", + "id name Arasan v24.0.0-10-g367aa9f", + "uciok", + "info string out of do_all_pending, list size=0" + ]) + await protocol.initialize() - self.engine.on_line_received("info currline 1 string eol") - with handler as info: - self.assertEqual(info["currline"][1], []) - - def test_mate_score(self): - handler = chess.uci.InfoHandler() - self.engine.info_handlers.append(handler) - - self.engine.on_line_received("info depth 7 seldepth 8 score mate 3") - with handler as info: - self.assertEqual(info["score"][1].mate, 3) - self.assertEqual(info["score"][1].cp, None) - - def test_info(self): - handler = chess.uci.InfoHandler() - self.engine.info_handlers.append(handler) - - self.mock.expect("go", ("bestmove d2d4", )) - self.engine.go() - - self.engine.on_line_received("info tbhits 123 cpuload 456 hashfull 789") - with handler as info: - self.assertEqual(info["tbhits"], 123) - self.assertEqual(info["cpuload"], 456) - self.assertEqual(info["hashfull"], 789) - - self.mock.expect("go", ("bestmove e2e4", )) - self.engine.go() - - self.assertNotIn("tbhits", handler.info) - self.assertNotIn("cpuload", handler.info) - self.assertNotIn("hashfull", handler.info) - - self.engine.on_line_received("info time 987 nodes 654 nps 321") - with handler as info: - self.assertEqual(info["time"], 987) - self.assertEqual(info["nodes"], 654) - self.assertEqual(info["nps"], 321) - - self.mock.assert_done() - - def test_combo_option(self): - self.engine.on_line_received("option name MyEnum type combo var Abc def var g h") - self.assertEqual(self.engine.options["MyEnum"].type, "combo") - self.assertEqual(self.engine.options["MyEnum"].var, ["Abc def", "g h"]) - - def test_set_option(self): - self.mock.expect("setoption name Yes value true") - self.mock.expect("setoption name No value false") - self.mock.expect("setoption name Null option value none") - self.mock.expect("setoption name String option value value value") - self.mock.expect("isready", ("readyok", )) - self.engine.setoption(OrderedDict([ - ("Yes", True), - ("No", False), - ("Null option", None), - ("String option", "value value"), - ])) - self.mock.assert_done() - - def test_multi_pv(self): - handler = chess.uci.InfoHandler() - self.engine.info_handlers.append(handler) - - self.engine.on_line_received("info score cp 777 multipv 13 pv e2e4") - self.engine.on_line_received("info score cp 888 pv d2d4") - with handler as info: - # Principal variations. - self.assertEqual(info["pv"][13][0], chess.Move.from_uci("e2e4")) - self.assertEqual(info["pv"][1][0], chess.Move.from_uci("d2d4")) - - # Score is relative to multipv as well. - self.assertEqual(info["score"][13].cp, 777) - self.assertEqual(info["score"][1].cp, 888) - - def test_castling_move_conversion(self): - # Setup a position where white can castle on the next move. - fen = "rnbqkbnr/pppppppp/8/8/8/4PN2/PPPPBPPP/RNBQK2R w KQkq - 1 1" - board = chess.Board(fen) - self.mock.expect("position fen " + fen) - self.engine.position(board) - self.mock.assert_done() - - # Expect the standard castling move notation e1g1 and respond with it. - self.mock.expect("go movetime 70 searchmoves a2a3 e1g1", ( - "bestmove e1g1", - )) - bestmove, pondermove = self.engine.go(movetime=70, searchmoves=[ - board.parse_san("a3"), - board.parse_san("O-O"), - ]) - self.assertTrue(bestmove.from_square, chess.E1) - self.assertTrue(bestmove.to_square, chess.H1) - self.mock.assert_done() - - # Assert that we can change to UCI_Chess960 mode. - self.assertFalse(self.engine.uci_chess960) - self.mock.expect("setoption name uCi_CheSS960 value true") - self.mock.expect("isready", ("readyok", )) - self.engine.setoption({"uCi_CheSS960": True}) - self.assertTrue(self.engine.uci_chess960) - self.mock.assert_done() - - # Expect a Shredder FEN during for the position command. - self.mock.expect("position fen rnbqkbnr/pppppppp/8/8/8/4PN2/PPPPBPPP/RNBQK2R w HAha - 1 1") - self.engine.position(board) - self.mock.assert_done() - - # Check that castling move conversion is now disabled. - self.mock.expect("go movetime 70 searchmoves a2a3 e1h1", ( - "bestmove e1h1", - )) - bestmove, pondermove = self.engine.go(movetime=70, searchmoves=[ - board.parse_san("a3"), - board.parse_san("O-O"), - ]) - self.assertTrue(bestmove.from_square, chess.E1) - self.assertTrue(bestmove.to_square, chess.H1) - self.mock.assert_done() - - def test_castling_ponder(self): - # Setup position. - fen = "rnbqkb1r/pp1ppppp/5n2/2p5/4P3/5N2/PPPPBPPP/RNBQK2R b KQkq - 3 3" - board = chess.Board(fen, chess960=True) - self.mock.expect("position fen " + fen) - self.engine.position(board) - - # Test castling moves as ponder moves. - self.mock.expect("go depth 15", ("bestmove f6e4 ponder e1g1", )) - bestmove, ponder = self.engine.go(depth=15) - self.assertEqual(bestmove, chess.Move.from_uci("f6e4")) - self.assertEqual(ponder, chess.Move.from_uci("e1h1")) - - self.mock.assert_done() - - def test_invalid_castling_rights(self): - fen = "3qk3/4pp2/5r2/8/8/8/3PP1P1/4K1R1 b G - 0 1" - board = chess.Board(fen, chess960=True) - board.push_san("Rf5") - - # White can castle with the G-side rook, which is not possible in - # standard chess. The UCI module should just send the final FEN, - # show a warning and hope for the best. - self.mock.expect("position fen 3qk3/4pp2/8/5r2/8/8/3PP1P1/4K1R1 w K - 1 2") - logging.disable(logging.ERROR) - self.engine.position(board) - logging.disable(logging.NOTSET) - self.mock.assert_done() + mock.assert_done() - # Activate Chess960 mode. - self.mock.expect("setoption name UCI_Chess960 value true") - self.mock.expect("isready", ("readyok", )) - self.engine.setoption({"UCI_Chess960": True}) + asyncio.run(main()) - # Then those castling rights should work fine. - self.mock.expect("position fen " + fen + " moves f6f5") - self.engine.position(board) - self.mock.assert_done() + def test_hiarcs_bestmove(self): + async def main(): + protocol = chess.engine.UciProtocol() + mock = chess.engine.MockTransport(protocol) - def test_hakkapeliitta_double_spaces(self): - handler = chess.uci.InfoHandler() - self.engine.info_handlers.append(handler) + mock.expect("uci", ["uciok"]) + await protocol.initialize() - self.engine.on_line_received("info depth 10 seldepth 9 score cp 22 time 17 nodes 48299 nps 2683000 tbhits 0") + mock.expect("ucinewgame") + mock.expect("isready", ["readyok"]) + mock.expect("position fen QN4n1/6r1/3k4/8/b2K4/8/8/8 b - - 0 1") + mock.expect("go", [ + "info depth 1 seldepth 4 time 793 nodes 187 nps 235 score cp -40 pv g7g4 d4c3 string keep double space", + "bestmove g7g4 ponder d4c3 ", + ]) + result = await protocol.play(chess.Board("QN4n1/6r1/3k4/8/b2K4/8/8/8 b - - 0 1"), chess.engine.Limit(), info=chess.engine.INFO_ALL) + self.assertEqual(result.move, chess.Move.from_uci("g7g4")) + self.assertEqual(result.ponder, chess.Move.from_uci("d4c3")) + self.assertEqual(result.info["pv"], [chess.Move.from_uci("g7g4"), chess.Move.from_uci("d4c3")]) + self.assertEqual(result.info["string"], "keep double space") + mock.assert_done() + + asyncio.run(main()) + + def test_xboard_options(self): + async def main(): + protocol = chess.engine.XBoardProtocol() + mock = chess.engine.MockTransport(protocol) + + mock.expect("xboard") + mock.expect("protover 2", [ + "feature egt=syzygy,gaviota", + "feature option=\"spinvar -spin 50 0 100\"", + "feature option=\"combovar -combo HI /// HELLO /// BYE\"", + "feature option=\"checkvar -check 0\"", + "feature option=\"stringvar -string \"\"\"", + "feature option=\"filevar -file \"\"\"", + "feature option=\"pathvar -path \"\"\"", + "feature option=\"buttonvar -button\"", + "feature option=\"resetvar -reset\"", + "feature option=\"savevar -save\"", + "feature ping=1 setboard=1 done=1", + ]) + mock.expect("accepted egt") + await protocol.initialize() + mock.assert_done() + + self.assertEqual(protocol.options["egtpath syzygy"].type, "path") + self.assertEqual(protocol.options["egtpath gaviota"].name, "egtpath gaviota") + self.assertEqual(protocol.options["spinvar"].type, "spin") + self.assertEqual(protocol.options["spinvar"].default, 50) + self.assertEqual(protocol.options["spinvar"].min, 0) + self.assertEqual(protocol.options["spinvar"].max, 100) + self.assertEqual(protocol.options["combovar"].type, "combo") + self.assertEqual(protocol.options["combovar"].var, ["HI", "HELLO", "BYE"]) + self.assertEqual(protocol.options["checkvar"].type, "check") + self.assertEqual(protocol.options["checkvar"].default, False) + self.assertEqual(protocol.options["stringvar"].type, "string") + self.assertEqual(protocol.options["filevar"].type, "file") + self.assertEqual(protocol.options["pathvar"].type, "path") + self.assertEqual(protocol.options["buttonvar"].type, "button") + self.assertEqual(protocol.options["resetvar"].type, "reset") + self.assertEqual(protocol.options["savevar"].type, "save") + + mock.expect("option combovar=HI") + await protocol.configure({"combovar": "HI"}) + mock.assert_done() + + mock.expect("option spinvar=42") + await protocol.configure({"spinvar": 42}) + mock.assert_done() + + mock.expect("option checkvar=1") + await protocol.configure({"checkvar": True}) + mock.assert_done() + + mock.expect("option pathvar=.") + await protocol.configure({"pathvar": "."}) + mock.assert_done() + + mock.expect("option buttonvar") + await protocol.configure({"buttonvar": None}) + mock.assert_done() + + asyncio.run(main()) + + def test_xboard_replay(self): + async def main(): + protocol = chess.engine.XBoardProtocol() + mock = chess.engine.MockTransport(protocol) + + mock.expect("xboard") + mock.expect("protover 2", ["feature ping=1 setboard=1 done=1"]) + await protocol.initialize() + mock.assert_done() + + limit = chess.engine.Limit(time=1.5, depth=17) + board = chess.Board() + board.push_san("d4") + board.push_san("Nf6") + board.push_san("c4") + + mock.expect("new") + mock.expect("force") + mock.expect("d2d4") + mock.expect("g8f6") + mock.expect("c2c4") + mock.expect("st 1.5") + mock.expect("sd 17") + mock.expect("nopost") + mock.expect("easy") + mock.expect("go", ["move e7e6"]) + mock.expect_ping() + result = await protocol.play(board, limit, game="game") + self.assertEqual(result.move, board.parse_san("e6")) + mock.assert_done() - with handler as info: - self.assertEqual(info["depth"], 10) - self.assertEqual(info["seldepth"], 9) - self.assertEqual(info["score"][1].cp, 22) - self.assertEqual(info["score"][1].mate, None) - self.assertEqual(info["time"], 17) - self.assertEqual(info["nodes"], 48299) - self.assertEqual(info["nps"], 2683000) - self.assertEqual(info["tbhits"], 0) + board.pop() + mock.expect("force") + mock.expect("remove") + mock.expect("st 1.5") + mock.expect("sd 17") + mock.expect("nopost") + mock.expect("easy") + mock.expect("go", ["move c2c4"]) + mock.expect_ping() + result = await protocol.play(board, limit, game="game") + self.assertEqual(result.move, board.parse_san("c4")) + mock.assert_done() + board.pop() + board.pop() + mock.expect("force") + mock.expect("remove") + mock.expect("undo") + mock.expect("st 1.5") + mock.expect("sd 17") + mock.expect("nopost") + mock.expect("easy") + mock.expect("go", ["move d2d4"]) + mock.expect_ping() + result = await protocol.play(board, limit, game="game") + self.assertEqual(result.move, board.parse_san("d4")) + mock.assert_done() + + asyncio.run(main()) + + def test_xboard_opponent(self): + async def main(): + protocol = chess.engine.XBoardProtocol() + mock = chess.engine.MockTransport(protocol) + + mock.expect("xboard") + mock.expect("protover 2", ["feature ping=1 setboard=1 name=1 done=1"]) + await protocol.initialize() + mock.assert_done() + + limit = chess.engine.Limit(time=5) + board = chess.Board() + opponent = chess.engine.Opponent("Turk", "Mechanical", 2100, True) + await protocol.send_opponent_information(opponent=opponent, engine_rating=3600) + + mock.expect("new") + mock.expect("name Mechanical Turk") + mock.expect("rating 3600 2100") + mock.expect("computer") + mock.expect("force") + mock.expect("st 5") + mock.expect("nopost") + mock.expect("easy") + mock.expect("go", ["move e2e4"]) + mock.expect_ping() + result = await protocol.play(board, limit, game="game") + self.assertEqual(result.move, board.parse_san("e4")) + mock.assert_done() + + new_opponent = chess.engine.Opponent("Turochamp", None, 800, True) + board.push(result.move) + mock.expect("new") + mock.expect("name Turochamp") + mock.expect("rating 3600 800") + mock.expect("computer") + mock.expect("force") + mock.expect("e2e4") + mock.expect("st 5") + mock.expect("nopost") + mock.expect("easy") + mock.expect("go", ["move e7e5"]) + mock.expect_ping() + result = await protocol.play(board, limit, game="game", opponent=new_opponent) + self.assertEqual(result.move, board.parse_san("e5")) + mock.assert_done() + + bad_opponent = chess.engine.Opponent("New\nLine", "GM", 1, False) + with self.assertRaises(chess.engine.EngineError): + await protocol.send_opponent_information(opponent=bad_opponent) + mock.assert_done() + + with self.assertRaises(chess.engine.EngineError): + result = await protocol.play(board, limit, game="bad game", opponent=bad_opponent) + mock.assert_done() + + asyncio.run(main()) + + def test_xboard_result(self): + async def main(): + protocol = chess.engine.XBoardProtocol() + mock = chess.engine.MockTransport(protocol) + + mock.expect("xboard") + mock.expect("protover 2", ["feature ping=1 setboard=1 done=1"]) + await protocol.initialize() + mock.assert_done() + + limit = chess.engine.Limit(time=5) + checkmate_board = chess.Board("k7/7R/6R1/8/8/8/8/K7 w - - 0 1") + + mock.expect("new") + mock.expect("force") + mock.expect("setboard k7/7R/6R1/8/8/8/8/K7 w - - 0 1") + mock.expect("st 5") + mock.expect("nopost") + mock.expect("easy") + mock.expect("go", ["move g6g8"]) + mock.expect_ping() + mock.expect("force") + mock.expect("result 1-0 {White mates}") + result = await protocol.play(checkmate_board, limit, game="checkmate") + self.assertEqual(result.move, checkmate_board.parse_uci("g6g8")) + checkmate_board.push(result.move) + self.assertTrue(checkmate_board.is_checkmate()) + await protocol.send_game_result(checkmate_board) + mock.assert_done() + + unfinished_board = chess.Board() + mock.expect("new") + mock.expect("force") + mock.expect("st 5") + mock.expect("nopost") + mock.expect("easy") + mock.expect("go", ["move e2e4"]) + mock.expect_ping() + mock.expect("force") + mock.expect("result *") + result = await protocol.play(unfinished_board, limit, game="unfinished") + self.assertEqual(result.move, unfinished_board.parse_uci("e2e4")) + unfinished_board.push(result.move) + await protocol.send_game_result(unfinished_board, game_complete=False) + mock.assert_done() + + timeout_board = chess.Board() + mock.expect("new") + mock.expect("force") + mock.expect("st 5") + mock.expect("nopost") + mock.expect("easy") + mock.expect("go", ["move e2e4"]) + mock.expect_ping() + mock.expect("force") + mock.expect("result 0-1 {Time forfeiture}") + result = await protocol.play(timeout_board, limit, game="timeout") + self.assertEqual(result.move, timeout_board.parse_uci("e2e4")) + timeout_board.push(result.move) + await protocol.send_game_result(timeout_board, chess.BLACK, "Time forfeiture") + mock.assert_done() + + error_board = chess.Board() + mock.expect("new") + mock.expect("force") + mock.expect("st 5") + mock.expect("nopost") + mock.expect("easy") + mock.expect("go", ["move e2e4"]) + mock.expect_ping() + result = await protocol.play(error_board, limit, game="error") + self.assertEqual(result.move, error_board.parse_uci("e2e4")) + error_board.push(result.move) + for c in "\n\r{}": + with self.assertRaises(chess.engine.EngineError): + await protocol.send_game_result(error_board, chess.BLACK, f"Time{c}forfeiture") + mock.assert_done() + + material_board = chess.Board("k7/8/8/8/8/8/8/K7 b - - 0 1") + self.assertTrue(material_board.is_insufficient_material()) + mock.expect("new") + mock.expect("force") + mock.expect("setboard k7/8/8/8/8/8/8/K7 b - - 0 1") + mock.expect("result 1/2-1/2 {Insufficient material}") + await protocol.send_game_result(material_board) + mock.assert_done() + + asyncio.run(main()) + + def test_xboard_analyse(self): + async def main(): + protocol = chess.engine.XBoardProtocol() + mock = chess.engine.MockTransport(protocol) + + mock.expect("xboard") + mock.expect("protover 2", [ + "feature done=0 ping=1 setboard=1", + "feature exclude=1", + "feature variants=\"normal,atomic\" done=1", + ]) + await protocol.initialize() + mock.assert_done() + + board = chess.variant.AtomicBoard("rnbqkbnr/pppppppp/8/8/8/5N2/PPPPPPPP/RNBQKB1R b KQkq - 1 1") + limit = chess.engine.Limit(depth=1) + mock.expect("new") + mock.expect("variant atomic") + mock.expect("force") + mock.expect("setboard rnbqkbnr/pppppppp/8/8/8/5N2/PPPPPPPP/RNBQKB1R b KQkq - 1 1") + mock.expect("exclude all") + mock.expect("include f7f6") + mock.expect("post") + mock.expect("analyze", ["4 116 23 2252 1... f6 2. e4 e6"]) + mock.expect(".") + mock.expect("exit") + mock.expect_ping() + info = await protocol.analyse(board, limit, root_moves=[board.parse_san("f6")]) + self.assertEqual(info["depth"], 4) + self.assertEqual(info["score"], chess.engine.PovScore(chess.engine.Cp(116), chess.BLACK)) + self.assertEqual(info["time"], 0.23) + self.assertEqual(info["nodes"], 2252) + self.assertEqual(info["pv"], [chess.Move.from_uci(move) for move in ["f7f6", "e2e4", "e7e6"]]) + mock.assert_done() + + asyncio.run(main()) + + def test_xboard_level(self): + async def main(): + protocol = chess.engine.XBoardProtocol() + mock = chess.engine.MockTransport(protocol) + + mock.expect("xboard") + mock.expect("protover 2", ["feature ping=1 setboard=1 done=1"]) + await protocol.initialize() + mock.assert_done() + + limit = chess.engine.Limit(black_clock=65, white_clock=100, + black_inc=4, white_inc=8, + clock_id="xboard level") + board = chess.Board() + mock.expect("new") + mock.expect("force") + mock.expect("level 0 1:40 8") + mock.expect("time 10000") + mock.expect("otim 6500") + mock.expect("nopost") + mock.expect("easy") + mock.expect("go", ["move e2e4"]) + mock.expect_ping() + result = await protocol.play(board, limit) + self.assertEqual(result.move, chess.Move.from_uci("e2e4")) + mock.assert_done() + + board.push(result.move) + board.push_uci("e7e5") + + mock.expect("force") + mock.expect("e7e5") + mock.expect("time 10000") + mock.expect("otim 6500") + mock.expect("nopost") + mock.expect("easy") + mock.expect("go", ["move d2d4"]) + mock.expect_ping() + result = await protocol.play(board, limit) + self.assertEqual(result.move, chess.Move.from_uci("d2d4")) + mock.assert_done() + + asyncio.run(main()) + + def test_xboard_error(self): + async def main(): + protocol = chess.engine.XBoardProtocol() + mock = chess.engine.MockTransport(protocol) + + mock.expect("xboard") + mock.expect("protover 2", ["Error (failed to initialize): Too bad!"]) + with self.assertRaises(chess.engine.EngineError): + await protocol.initialize() + + with self.assertRaises(chess.engine.EngineError): + # Trying to use the engine, but it was not successfully initialized. + await protocol.ping() + + mock.assert_done() + + asyncio.run(main()) + + @catchAndSkip(FileNotFoundError, "need /bin/bash") + def test_transport_close_with_pending(self): + async def main(): + transport, protocol = await chess.engine.popen_uci(["/bin/bash", "-c", "read && echo uciok && sleep 86400"]) + protocol.loop.call_later(0.01, transport.close) + results = await asyncio.gather(protocol.ping(), protocol.ping(), return_exceptions=True) + self.assertNotEqual(results[0], None) + self.assertNotEqual(results[1], None) + + asyncio.run(main()) + + @catchAndSkip(FileNotFoundError, "need /bin/bash") + def test_quit_timeout(self): + with chess.engine.SimpleEngine.popen_uci(["/bin/bash", "-c", "read && echo uciok && sleep 86400"], debug=True) as engine: + engine.timeout = 0.01 + with self.assertRaises(asyncio.TimeoutError): + engine.quit() + + def test_run_in_background(self): + class ExpectedError(Exception): + pass -class UciOptionMapTestCase(unittest.TestCase): + async def raise_expected_error(future): + await asyncio.sleep(0.001) + raise ExpectedError - def test_equality(self): - a = chess.uci.OptionMap() - b = chess.uci.OptionMap() - c = chess.uci.OptionMap() - self.assertEqual(a, b) + with self.assertRaises(ExpectedError): + chess.engine.run_in_background(raise_expected_error) - a["fOO"] = "bAr" - b["foo"] = "bAr" - c["fOo"] = "bar" - self.assertEqual(a, b) - self.assertEqual(b, a) - self.assertNotEqual(a, c) - self.assertNotEqual(c, a) - self.assertNotEqual(b, c) + async def resolve(future): + await asyncio.sleep(0.001) + future.set_result("resolved") + await asyncio.sleep(0.001) - b["hello"] = "world" - self.assertNotEqual(a, b) - self.assertNotEqual(b, a) - - def test_len(self): - a = chess.uci.OptionMap() - self.assertEqual(len(a), 0) - - a["key"] = "value" - self.assertEqual(len(a), 1) - - del a["key"] - self.assertEqual(len(a), 0) + result = chess.engine.run_in_background(resolve) + self.assertEqual(result, "resolved") class SyzygyTestCase(unittest.TestCase): @@ -2667,40 +4025,48 @@ class SyzygyTestCase(unittest.TestCase): def test_calc_key(self): board = chess.Board("8/8/8/5N2/5K2/2kB4/8/8 b - - 0 1") key_from_board = chess.syzygy.calc_key(board) - key_from_filename = chess.syzygy.normalize_filename("KBNvK") + key_from_filename = chess.syzygy.normalize_tablename("KBNvK") self.assertEqual(key_from_board, key_from_filename) - def test_filenames(self): - self.assertIn("KPPvKN", chess.syzygy.filenames()) - self.assertIn("KNNPvKN", chess.syzygy.filenames()) - self.assertIn("KQRNvKR", chess.syzygy.filenames()) - self.assertIn("KRRRvKR", chess.syzygy.filenames()) - self.assertIn("KRRvKRR", chess.syzygy.filenames()) - self.assertIn("KRNvKRP", chess.syzygy.filenames()) - self.assertIn("KRPvKP", chess.syzygy.filenames()) - - def test_suicide_filenames(self): - # Test number of 6 piece filenames. - self.assertEqual(sum(1 for eg in chess.syzygy.filenames(one_king=False) if len(eg) == 7), 5754) - - def test_normalize_filename(self): - names = set(chess.syzygy.filenames()) + def test_tablenames(self): + self.assertIn("KPPvKN", chess.syzygy.tablenames()) + self.assertIn("KNNPvKN", chess.syzygy.tablenames()) + self.assertIn("KQRNvKR", chess.syzygy.tablenames()) + self.assertIn("KRRRvKR", chess.syzygy.tablenames()) + self.assertIn("KRRvKRR", chess.syzygy.tablenames()) + self.assertIn("KRNvKRP", chess.syzygy.tablenames()) + self.assertIn("KRPvKP", chess.syzygy.tablenames()) + + def test_suicide_tablenames(self): + # Test the number of 6-piece tables. + self.assertEqual(sum(1 for eg in chess.syzygy.tablenames(one_king=False) if len(eg) == 7), 5754) + + def test_normalize_tablename(self): + names = set(chess.syzygy.tablenames()) for name in names: self.assertTrue( - chess.syzygy.normalize_filename(name) in names, - "Already normalized {0}".format(name)) + chess.syzygy.normalize_tablename(name) in names, + f"Already normalized {name}") w, b = name.split("v", 1) swapped = b + "v" + w self.assertTrue( - chess.syzygy.normalize_filename(swapped) in names, - "Normalized {0}".format(swapped)) + chess.syzygy.normalize_tablename(swapped) in names, + f"Normalized {swapped}") def test_normalize_nnvbb(self): - self.assertEqual(chess.syzygy.normalize_filename("KNNvKBB"), "KBBvKNN") + self.assertEqual(chess.syzygy.normalize_tablename("KNNvKBB"), "KBBvKNN") + + def test_dependencies(self): + self.assertEqual(set(chess.syzygy.dependencies("KBNvK")), set(["KBvK", "KNvK"])) + + def test_get_wdl_get_dtz(self): + with chess.syzygy.Tablebase() as tables: + board = chess.Board() + self.assertEqual(tables.get_dtz(board, tables.get_wdl(board)), None) def test_probe_pawnless_wdl_table(self): - wdl = chess.syzygy.WdlTable("data/syzygy/regular", "KBNvK") + wdl = chess.syzygy.WdlTable("data/syzygy/regular/KBNvK.rtbw") wdl.init_table_wdl() board = chess.Board("8/8/8/5N2/5K2/2kB4/8/8 b - - 0 1") @@ -2721,7 +4087,7 @@ def test_probe_pawnless_wdl_table(self): wdl.close() def test_probe_wdl_table(self): - wdl = chess.syzygy.WdlTable("data/syzygy/regular", "KRvKP") + wdl = chess.syzygy.WdlTable("data/syzygy/regular/KRvKP.rtbw") wdl.init_table_wdl() board = chess.Board("8/8/2K5/4P3/8/8/8/3r3k b - - 1 1") @@ -2733,7 +4099,7 @@ def test_probe_wdl_table(self): wdl.close() def test_probe_dtz_table_piece(self): - dtz = chess.syzygy.DtzTable("data/syzygy/regular", "KRvKN") + dtz = chess.syzygy.DtzTable("data/syzygy/regular/KRvKN.rtbz") dtz.init_table_dtz() # Pawnless position with white to move. @@ -2747,7 +4113,7 @@ def test_probe_dtz_table_piece(self): dtz.close() def test_probe_dtz_table_pawn(self): - dtz = chess.syzygy.DtzTable("data/syzygy/regular", "KNvKP") + dtz = chess.syzygy.DtzTable("data/syzygy/regular/KNvKP.rtbz") dtz.init_table_dtz() board = chess.Board("8/1K6/1P6/8/8/8/6n1/7k w - - 0 1") @@ -2756,8 +4122,8 @@ def test_probe_dtz_table_pawn(self): dtz.close() def test_probe_wdl_tablebase(self): - with chess.syzygy.Tablebases(max_fds=2) as tables: - self.assertGreaterEqual(tables.open_directory("data/syzygy/regular"), 70) + with chess.syzygy.Tablebase(max_fds=2) as tables: + self.assertGreaterEqual(tables.add_directory("data/syzygy/regular"), 70) # Winning KRvKB. board = chess.Board("7k/6b1/6K1/8/8/8/8/3R4 b - - 12 7") @@ -2772,109 +4138,116 @@ def test_probe_wdl_tablebase(self): self.assertEqual(tables.probe_wdl_table(board), 2) def test_wdl_ep(self): - with chess.syzygy.open_tablebases("data/syzygy/regular") as tables: + with chess.syzygy.open_tablebase("data/syzygy/regular") as tables: # Winning KPvKP because of en passant. board = chess.Board("8/8/8/k2Pp3/8/8/8/4K3 w - e6 0 2") - # If there was no en passant this would be a draw. + # If there was no en passant, this would be a draw. self.assertEqual(tables.probe_wdl_table(board), 0) # But it is a win. self.assertEqual(tables.probe_wdl(board), 2) def test_dtz_ep(self): - with chess.syzygy.open_tablebases("data/syzygy/regular") as tables: + with chess.syzygy.open_tablebase("data/syzygy/regular") as tables: board = chess.Board("8/8/8/8/2pP4/2K5/4k3/8 b - d3 0 1") self.assertEqual(tables.probe_dtz_no_ep(board), -1) self.assertEqual(tables.probe_dtz(board), 1) def test_testsuite(self): - with chess.syzygy.open_tablebases("data/syzygy/regular") as tables: - with open("data/endgame.epd") as epds: - board = chess.Board() + with chess.syzygy.open_tablebase("data/syzygy/regular") as tables, open("data/endgame.epd") as epds: + board = chess.Board() - for line, epd in enumerate(epds): - extra = board.set_epd(epd) + for line, epd in enumerate(epds): + extra = board.set_epd(epd) - wdl_table = tables.probe_wdl_table(board) - self.assertEqual( - wdl_table, extra["wdl_table"], - "Expecting wdl_table {0} for {1}, got {2} (at line {3})".format(extra["wdl_table"], board.fen(), wdl_table, line + 1)) + wdl_table = tables.probe_wdl_table(board) + self.assertEqual( + wdl_table, extra["wdl_table"], + f"Expecting wdl_table {extra['wdl_table']} for {board.fen()}, got {wdl_table} (at line {line + 1})") - wdl = tables.probe_wdl(board) - self.assertEqual( - wdl, extra["wdl"], - "Expecting wdl {0} for {1}, got {2} (at line {3})".format(extra["wdl"], board.fen(), wdl, line + 1)) + wdl = tables.probe_wdl(board) + self.assertEqual( + wdl, extra["wdl"], + f"Expecting wdl {extra['wdl']} for {board.fen()}, got {wdl} (at line {line + 1})") - dtz = tables.probe_dtz(board) - self.assertEqual( - dtz, extra["dtz"], - "Expecting dtz {0} for {1}, got {2} (at line {3})".format(extra["dtz"], board.fen(), dtz, line + 1)) + dtz = tables.probe_dtz(board) + self.assertEqual( + dtz, extra["dtz"], + f"Expecting dtz {extra['dtz']} for {board.fen()}, got {dtz} (at line {line + 1})") @catchAndSkip(chess.syzygy.MissingTableError) def test_stockfish_dtz_bug(self): - with chess.syzygy.open_tablebases("data/syzygy/regular") as tables: + with chess.syzygy.open_tablebase("data/syzygy/regular") as tables: board = chess.Board("3K4/8/3k4/8/4p3/4B3/5P2/8 w - - 0 5") self.assertEqual(tables.probe_dtz(board), 15) @catchAndSkip(chess.syzygy.MissingTableError) def test_issue_93(self): - with chess.syzygy.open_tablebases("data/syzygy/regular") as tables: + with chess.syzygy.open_tablebase("data/syzygy/regular") as tables: board = chess.Board("4r1K1/6PP/3k4/8/8/8/8/8 w - - 1 64") self.assertEqual(tables.probe_wdl(board), 2) self.assertEqual(tables.probe_dtz(board), 4) @catchAndSkip(chess.syzygy.MissingTableError) def test_suicide_dtm(self): - with chess.syzygy.open_tablebases("data/syzygy/suicide", VariantBoard=chess.variant.SuicideBoard) as tables: - with open("data/suicide-dtm.epd") as epds: - for epd in epds: - epd = epd.strip() + with chess.syzygy.open_tablebase("data/syzygy/suicide", VariantBoard=chess.variant.SuicideBoard) as tables, open("data/suicide-dtm.epd") as epds: + for epd in epds: + epd = epd.strip() - board, solution = chess.variant.SuicideBoard.from_epd(epd) + board, solution = chess.variant.SuicideBoard.from_epd(epd) - wdl = tables.probe_wdl(board) + wdl = tables.probe_wdl(board) - expected_wdl = ((solution["max_dtm"] > 0) - (solution["max_dtm"] < 0)) * 2 - self.assertEqual(wdl, expected_wdl, "Expecting wdl {0}, got {1} (in {2})".format(expected_wdl, wdl, epd)) + expected_wdl = ((solution["max_dtm"] > 0) - (solution["max_dtm"] < 0)) * 2 + self.assertEqual(wdl, expected_wdl, f"Expecting wdl {expected_wdl}, got {wdl} (in {epd})") - dtz = tables.probe_dtz(board) + dtz = tables.probe_dtz(board) - if wdl > 0: - self.assertGreaterEqual(dtz, chess.syzygy.dtz_before_zeroing(wdl)) - self.assertLessEqual(dtz, 2 * solution["max_dtm"]) - elif wdl == 0: - self.assertEqual(dtz, 0) - else: - self.assertLessEqual(dtz, chess.syzygy.dtz_before_zeroing(wdl)) - self.assertGreaterEqual(dtz, 2 * solution["max_dtm"]) + if wdl > 0: + self.assertGreaterEqual(dtz, chess.syzygy.dtz_before_zeroing(wdl)) + self.assertLessEqual(dtz, 2 * solution["max_dtm"]) + elif wdl == 0: + self.assertEqual(dtz, 0) + else: + self.assertLessEqual(dtz, chess.syzygy.dtz_before_zeroing(wdl)) + self.assertGreaterEqual(dtz, 2 * solution["max_dtm"]) @catchAndSkip(chess.syzygy.MissingTableError) def test_suicide_dtz(self): - with chess.syzygy.open_tablebases("data/syzygy/suicide", VariantBoard=chess.variant.SuicideBoard) as tables: - with open("data/suicide-dtz.epd") as epds: - for epd in epds: - epd = epd.strip() - if epd.startswith("%") or epd.startswith("#"): - continue + with chess.syzygy.open_tablebase("data/syzygy/suicide", VariantBoard=chess.variant.SuicideBoard) as tables, open("data/suicide-dtz.epd") as epds: + for epd in epds: + epd = epd.strip() + if epd.startswith("%") or epd.startswith("#"): + continue - board, solution = chess.variant.SuicideBoard.from_epd(epd) + board, solution = chess.variant.SuicideBoard.from_epd(epd) - dtz = tables.probe_dtz(board) - self.assertEqual(dtz, solution["dtz"], "Expecting dtz {0}, got {1} (in {2})".format(solution["dtz"], dtz, epd)) + dtz = tables.probe_dtz(board) + self.assertEqual(dtz, solution["dtz"], f"Expecting dtz {solution['dtz']}, got {dtz} (in {epd})") - @unittest.skipIf(os.environ.get("TRAVIS_PYTHON_VERSION", "").startswith("pypy"), "travis pypy is very slow") @catchAndSkip(chess.syzygy.MissingTableError) def test_suicide_stats(self): board = chess.variant.SuicideBoard() - with chess.syzygy.open_tablebases("data/syzygy/suicide", VariantBoard=type(board)) as tables: - with open("data/suicide-stats.epd") as epds: - for l, epd in enumerate(epds): - solution = board.set_epd(epd) + with chess.syzygy.open_tablebase("data/syzygy/suicide", VariantBoard=type(board)) as tables, open("data/suicide-stats.epd") as epds: + for l, epd in enumerate(epds): + solution = board.set_epd(epd) - dtz = tables.probe_dtz(board) - self.assertAlmostEqual(dtz, solution["dtz"], delta=1, msg="Expected dtz {0}, got {1} (in l. {2}, fen: {3})".format(solution["dtz"], dtz, l + 1, board.fen())) + dtz = tables.probe_dtz(board) + self.assertAlmostEqual(dtz, solution["dtz"], delta=1, + msg=f"Expected dtz {solution['dtz']}, got {dtz} (in l. {l + 1}, fen: {board.fen()})") + + def test_antichess_kvk(self): + kvk = chess.variant.AntichessBoard("4k3/8/8/8/8/8/8/4K3 w - - 0 1") + + tables = chess.syzygy.Tablebase() + with self.assertRaises(KeyError): + tables.probe_dtz(kvk) + + tables = chess.syzygy.Tablebase(VariantBoard=chess.variant.AntichessBoard) + with self.assertRaises(chess.syzygy.MissingTableError): + tables.probe_dtz(kvk) class NativeGaviotaTestCase(unittest.TestCase): @@ -2882,34 +4255,39 @@ class NativeGaviotaTestCase(unittest.TestCase): @unittest.skipUnless(platform.python_implementation() == "CPython", "need CPython for native Gaviota") @catchAndSkip((OSError, RuntimeError), "need libgtb") def setUp(self): - self.tablebases = chess.gaviota.open_tablebases_native("data/gaviota") + self.tablebase = chess.gaviota.open_tablebase_native("data/gaviota") def tearDown(self): - self.tablebases.close() + self.tablebase.close() def test_native_probe_dtm(self): board = chess.Board("6K1/8/8/8/4Q3/8/6k1/8 b - - 0 1") - self.assertEqual(self.tablebases.probe_dtm(board), -14) + self.assertEqual(self.tablebase.probe_dtm(board), -14) board = chess.Board("8/3K4/8/8/8/4r3/4k3/8 b - - 0 1") - self.assertEqual(self.tablebases.get_dtm(board), 21) + self.assertEqual(self.tablebase.get_dtm(board), 21) def test_native_probe_wdl(self): board = chess.Board("8/8/4K3/2n5/8/3k4/8/8 w - - 0 1") - self.assertEqual(self.tablebases.probe_wdl(board), 0) + self.assertEqual(self.tablebase.probe_wdl(board), 0) board = chess.Board("8/8/1p2K3/8/8/3k4/8/8 b - - 0 1") - self.assertEqual(self.tablebases.get_wdl(board), 1) + self.assertEqual(self.tablebase.get_wdl(board), 1) + + @catchAndSkip(chess.gaviota.MissingTableError, "need KPPvKP.gtb.cp4") + def test_two_ep(self): + board = chess.Board("8/8/8/8/5pPp/8/5K1k/8 b - g3 0 61") + self.assertEqual(self.tablebase.probe_dtm(board), 19) class GaviotaTestCase(unittest.TestCase): @catchAndSkip(ImportError) def setUp(self): - self.tablebases = chess.gaviota.open_tablebases("data/gaviota", LibraryLoader=None) + self.tablebase = chess.gaviota.open_tablebase("data/gaviota", LibraryLoader=None) def tearDown(self): - self.tablebases.close() + self.tablebase.close() @catchAndSkip(chess.gaviota.MissingTableError) def test_dm_4(self): @@ -2928,9 +4306,8 @@ def test_dm_4(self): expected = extra["dm"] * 2 - 1 else: expected = extra["dm"] * 2 - dtm = self.tablebases.probe_dtm(board) - self.assertEqual(dtm, expected, - "Expecting dtm {0} for {1}, got {2} (at line {3})".format(expected, board.fen(), dtm, line + 1)) + dtm = self.tablebase.probe_dtm(board) + self.assertEqual(dtm, expected, f"Expecting dtm {expected} for {board.fen()}, got {dtm} (at line {line + 1})") @catchAndSkip(chess.gaviota.MissingTableError) def test_dm_5(self): @@ -2949,24 +4326,46 @@ def test_dm_5(self): expected = extra["dm"] * 2 - 1 else: expected = extra["dm"] * 2 - dtm = self.tablebases.probe_dtm(board) - self.assertEqual(dtm, expected, - "Expecting dtm {0} for {1}, got {2} (at line {3})".format(expected, board.fen(), dtm, line + 1)) + dtm = self.tablebase.probe_dtm(board) + self.assertEqual(dtm, expected, f"Expecting dtm {expected} for {board.fen()}, got {dtm} (at line {line + 1})") def test_wdl(self): board = chess.Board("8/8/4K3/2n5/8/3k4/8/8 w - - 0 1") - self.assertEqual(self.tablebases.probe_wdl(board), 0) + self.assertEqual(self.tablebase.probe_wdl(board), 0) board = chess.Board("8/8/1p2K3/8/8/3k4/8/8 b - - 0 1") - self.assertEqual(self.tablebases.probe_wdl(board), 1) + self.assertEqual(self.tablebase.probe_wdl(board), 1) def test_context_manager(self): - self.assertTrue(self.tablebases.available_tables) + self.assertTrue(self.tablebase.available_tables) - with self.tablebases: + with self.tablebase: pass - self.assertFalse(self.tablebases.available_tables) + self.assertFalse(self.tablebase.available_tables) + + @catchAndSkip(chess.gaviota.MissingTableError, "need KPPvKP.gtb.cp4") + def test_two_ep(self): + board = chess.Board("8/8/8/8/5pPp/8/5K1k/8 b - g3 0 61") + self.assertEqual(self.tablebase.probe_dtm(board), 19) + + board = chess.Board("K7/8/8/6k1/5pPp/8/8/8 b - g3 0 61") + self.assertEqual(self.tablebase.probe_dtm(board), 17) + + @catchAndSkip(chess.gaviota.MissingTableError, "need KPvKP.gtb.cp4") + def test_ep_is_best(self): + board = chess.Board("8/8/7k/8/1pP5/7K/8/8 b - c3 0 1") + self.assertEqual(self.tablebase.probe_dtm(board), 19) + + @catchAndSkip(chess.gaviota.MissingTableError, "need KQPvKP.gtb.cp4") + def test_ep_is_mate(self): + # The resulting mate. + board = chess.Board("5Q2/7k/6P1/5K2/8/8/8/8 b - - 0 1") + self.assertEqual(self.tablebase.probe_dtm(board), 0) + + # Ep leads to the previously tested mate position. + board = chess.Board("5Q2/7k/8/5KpP/8/8/8/8 w - g6 0 1") + self.assertEqual(self.tablebase.probe_dtm(board), 1) class SvgTestCase(unittest.TestCase): @@ -2976,6 +4375,19 @@ def test_svg_board(self): self.assertIn("white bishop", svg) self.assertNotIn("black queen", svg) + def test_svg_arrows(self): + svg = chess.svg.board(arrows=[(chess.A1, chess.A1)]) + self.assertIn("