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 7f018c159..076e4fcff 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ nosetests.xml dist/ build/ -python_chess.egg-info/ +*.egg-info/ docs/_build/ data/gaviota/*.gtb.cp4 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 143a00eae..000000000 --- a/.travis.yml +++ /dev/null @@ -1,96 +0,0 @@ -dist: xenial -language: python -sudo: false -python: - - "3.6" - - "3.7" -matrix: - include: - - python: "3.8" - env: LATEST=1 -cache: - directories: - - data/gaviota - - data/syzygy/suicide -before_cache: - - rm -f data/gaviota/travis_wait_*.log || true - - rm -f data/syzygy/suicide/travis_wait_*.log || true -before_install: - - | # Stockfish - wget https://stockfish.s3.amazonaws.com/stockfish-10-linux.zip - unzip stockfish-10-linux.zip - mkdir -p bin - cp stockfish-10-linux/Linux/stockfish_10_x64_modern bin/stockfish - chmod +x 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 - travis_wait wget --no-verbose --no-check-certificate --no-clobber --input-file TEST-SOURCE.txt - cd ../.. - - | # Suicide syzygy bases - cd data/syzygy/suicide - travis_wait wget --no-verbose --no-check-certificate --no-clobber --input-file TEST-SOURCE.txt - cd ../../.. -install: - - pip install --upgrade pip wheel - - pip install --upgrade setuptools - - pip install --upgrade coveralls - - if [[ $LATEST -eq 1 ]]; then pip install --upgrade mypy; fi - - pip install -e . -script: - - # Unit tests - - coverage erase - - coverage run --source chess test.py -vv SquareTestCase - - coverage run --source chess --append test.py -vv MoveTestCase - - coverage run --source chess --append test.py -vv PieceTestCase - - coverage run --source chess --append test.py -vv BoardTestCase - - coverage run --source chess --append test.py -vv LegalMoveGeneratorTestCase - - coverage run --source chess --append test.py -vv BaseBoardTestCase - - coverage run --source chess --append test.py -vv SquareSetTestCase - - coverage run --source chess --append test.py -vv PolyglotTestCase - - coverage run --source chess --append test.py -vv PgnTestCase - - coverage run --source chess --append test.py -vv SyzygyTestCase - - coverage run --source chess --append test.py -vv NativeGaviotaTestCase - - coverage run --source chess --append test.py -vv GaviotaTestCase - - coverage run --source chess --append test.py -vv SvgTestCase - - coverage run --source chess --append test.py -vv SuicideTestCase - - coverage run --source chess --append test.py -vv AtomicTestCase - - coverage run --source chess --append test.py -vv RacingKingsTestCase - - coverage run --source chess --append test.py -vv HordeTestCase - - coverage run --source chess --append test.py -vv ThreeCheckTestCase - - coverage run --source chess --append test.py -vv CrazyhouseTestCase - - coverage run --source chess --append test.py -vv GiveawayTestCase - - coverage run --source chess --append test.py -vv EngineTestCase - - coverage run --source chess --append -m doctest README.rst --verbose - - echo Unit tests complete - - coveralls || [[ $? -eq 139 ]] - - # Typing - - if [[ $LATEST -eq 1 ]]; then python -m mypy chess/__init__.py; fi - - if [[ $LATEST -eq 1 ]]; then python -m mypy chess/engine.py || true; fi - - if [[ $LATEST -eq 1 ]]; then python -m mypy chess/gaviota.py || true; fi - - if [[ $LATEST -eq 1 ]]; then python -m mypy chess/pgn.py; fi - - if [[ $LATEST -eq 1 ]]; then python -m mypy chess/polyglot.py; fi - - if [[ $LATEST -eq 1 ]]; then python -m mypy chess/svg.py; fi - - if [[ $LATEST -eq 1 ]]; then python -m mypy chess/syzygy.py || true; fi - - if [[ $LATEST -eq 1 ]]; then python -m mypy chess/variant.py; fi - - # Perft tests - - if [[ $LATEST -eq 1 ]]; then python examples/perft/perft.py -t 1 examples/perft/random.perft --max-nodes 10000; fi - - if [[ $LATEST -eq 1 ]]; then python examples/perft/perft.py -t 1 examples/perft/tricky.perft; fi - - if [[ $LATEST -eq 1 ]]; then python examples/perft/perft.py -t 1 --variant giveaway examples/perft/giveaway.perft; fi - - if [[ $LATEST -eq 1 ]]; then python examples/perft/perft.py -t 1 --variant atomic examples/perft/atomic.perft; fi - - if [[ $LATEST -eq 1 ]]; then python examples/perft/perft.py -t 1 --variant racingkings examples/perft/racingkings.perft; fi - - if [[ $LATEST -eq 1 ]]; then python examples/perft/perft.py -t 1 --variant horde examples/perft/horde.perft; fi - - if [[ $LATEST -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 cdb934e25..03a9555d1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,1728 +1,408 @@ Changelog for python-chess ========================== -New in v0.30.1 --------------- - -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 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 --------------- - -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 --------------- - -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 --------------- - -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 --------------- +New in v1.11.2 (25th Feb 2025) +------------------------------ Bugfixes: -* Fixed exception propagation, when an 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. +* Fix ``chess.gaviota.PythonTablebase`` does not properly resolve positions + where en passant captures are the best move. -New in v0.28.1 --------------- +New in v1.11.1 (9th Oct 2024) +----------------------------- 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 --------------- - -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 - ducktyping. -* 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. +* ``chess.engine``: Fix parsing of UCI options containing containing ``name``, + ``type``, ``default``, ``min``, or ``max``, e.g., ``mini``. -New in v0.27.3 --------------- +New in v1.11.0 (4th Oct 2024) +----------------------------- 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. +* 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``). 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 --------------- +* Add ``chess.pgn.Game.time_control()`` and related data models. +* Add model ``sf16.1`` for ``chess.engine.Score.wdl()``, the new default. 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. +* 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. -New in v0.27.1 --------------- - -Bugfixes: - -* `chess.engine.XBoardProtocol.play()` was raising `KeyError` when using time - controls with increment or remaining moves. Thanks @pascalgeo. - -New in v0.27.0 --------------- - -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 --------------- - -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 --------------- - -Bugfixes: - -* `chess.engine` did not correctly handle Windows-style line endings. - Thanks @Bstylestuff. - -New in v0.25.0 --------------- +New in v1.10.0 (27th Jul 2023) +------------------------------ 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`. +* 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: -* 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. +* 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: -* `chess.pgn.read_game()` now ignores BOM at the start of the stream. -* Removed deprecated items. - -New in v0.24.2 --------------- - -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`. +* 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.24.1, v0.23.11 ------------------------- +New in v1.9.4 (22nd Dec 2022) +----------------------------- 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 --------------- - -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. +* Fix ``PovScore.wdl()`` ignored ``model`` and ``ply`` parameters. +* ``chess.syzygy``: Check that board matches tablebase variant. 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 ---------------- - -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 --------------- - -Changes: - -* Updated `Board.is_fivefold_repetition()`. FIDE rules have changed and the - repetition no longer needs to occur on consecutive alternating moves. - Thanks @LegionMammal978. +* Add model ``sf15.1`` for ``chess.engine.Score.wdl()``. +* Raise more specific exceptions: ``chess.IllegalMoveError``, + ``chess.AmbiguousMoveError``, and ``chess.InvalidMoveError``. -New in v0.23.8 --------------- +New in v1.9.3 (16th Sep 2022) +----------------------------- Bugfixes: -* `chess.syzygy`: Correctly initialize wide DTZ map for experimental 7 piece - table KRBBPvKQ. - -New in v0.23.7 --------------- - -Bugfixes: - -* Fixed `ThreeCheckBoard.mirror()` and `CrazyhouseBoard.mirror()`, which - were previously resetting remaining checks and pockets respectively. - Thanks @QueensGambit. +* Fix some valid characters were not accepted in PGN tag names. 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. +* Skip over syntactically invalid PGN tags. +* Detect Antichess insufficient material with two opposing knights. New features: -* Added `Board.root()`. +* Add ``chess.Board.unicode(..., orientation=chess.WHITE)``. -New in v0.23.6 --------------- +New in v1.9.2 (17th Jun 2022) +----------------------------- 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. +* Fix recursive Crazyhouse move generation sometimes failing with + with ``RuntimeError``. +* Fix rendering of black pawn SVG on dark background. New features: -* Added `polyglot.MemoryMappedReader.get(board, default=None)`. +* Add ``chess.engine.AnalysisResult.would_block()``. -New in v0.23.5 --------------- +New in v1.9.1 (28th May 2022) +----------------------------- Bugfixes: -* Atomic chess: KNvKN is not insufficient material. -* Crazyhouse: Detect insufficient material. This can not happen unless the - game was started with insufficient material. +* Reject pawn capture SAN if the original file is not specified, e.g., + ``d5`` will no longer match ``cxd5``. 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 --------------- - -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 --------------- +* 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. New features: -* XBoard: Support `variant`. Thanks gbtami. - -New in v0.23.2 --------------- - -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 --------------- - -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 --------------- - -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. +* Add model ``sf15`` for ``chess.engine.Score.wdl()``. -New in v0.22.2 --------------- +New in v1.9.0 (18th Mar 2022) +----------------------------- 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. +* Expand position validation to detect check conflicting with en passant + square. New features: -* Added `Board.lan()` for long algebraic notation. +* 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.22.1 --------------- - -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. +New in v1.8.0 (23rd Dec 2021) +----------------------------- Bugfixes: -* `chess.pgn`: Allow games without movetext. -* XBoard: Fixed draw handling. - -New in v0.22.0 --------------- - -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`. +* Fix ``SquareSet.issuperset()`` and ``SquareSet.issubset()`` by swapping + their respective implementations. New features: -* XBoard: Added `DrawHandler`. - -New in v0.21.2 --------------- +* Read and write PGN comments like ``[%emt 0:05:21]``. -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 --------------- - -Bugfixes: - -* `Board.set_piece_at()` no longer shadows optional `promoted` - argument from `BaseBoard`. -* Fixed `ThreeCheckBoard.is_irreversible()` and - `ThreeCheckBoard._transposition_key()`. +New in v1.7.0 (7th Oct 2021) +---------------------------- 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 --------------- - -Release yanked. - -New in v0.20.1 --------------- +* Add new models for ``chess.engine.Score.wdl()``: ``sf`` (the new default) + and ``sf14``. +* Add ``chess.Board.piece_map()``. Bugfixes: -* Fix arrow positioning on SVG boards. -* Documentation fixes and improvements, making most doctests runnable. +* ``chess.pgn``: Fix skipping with nested variations. +* ``chess.svg``: Make check gradient compatible with QtSvg. -New in v0.20.0 --------------- +New in v1.6.1 (12th Jun 2021) +----------------------------- 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. +* Make ``chess.engine.SimpleEngine.play(..., draw_offered=True)`` available. + Previously only added for ``chess.engine.Protocol``. -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 --------------- +New in v1.6.0 (11th Jun 2021) +----------------------------- 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. +* Allow offering a draw to XBoard engines using + ``chess.engine.Protocol.play(..., draw_offered=True)``. +* Now detects insufficient material in Horde. Thanks @stevepapazis! 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. - -New in v0.18.3 --------------- +* ``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: -* `chess.gaviota.NativeTablebases.get_dtm()` and `get_wdl()` were missing. +* Fix slightly off-center pawns in ``chess.svg``. +* Fix typing error in Python 3.10 (due to added ``int.bit_count``). -New in v0.18.2 --------------- +New in v1.5.0 (7th Apr 2021) +---------------------------- 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 --------------- - -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 --------------- - -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. +* 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: -* `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. +* Added ``chess.Board.outcome()``. +* Implement and accept usermove feature for XBoard engines. -Bugfixes: +Special thanks to @MarkZH for many of the engine related changes in this +release! -* **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 in v1.4.0 (25th Jan 2021) +----------------------------- 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 --------------- +* 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]``. 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 --------------- +* Recover from invalid UTF-8 sent by an UCI engine, by ignoring that + (and only that) line. -Bugfixes: - -* Explosions in atomic chess were not destroying castling rights. Thanks to - ProgramFOX for finding this issue. - -New in v0.16.0 --------------- +New in v1.3.3 (27th Dec 2020) +----------------------------- 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. +* Fixed unintended collisions and optimized ``chess.Piece.__hash__()``. +* Fixed false-positive ``chess.STATUS_IMPOSSIBLE_CHECK`` if checkers are + aligned with other king. 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 --------------- +* Also detect ``chess.STATUS_IMPOSSIBLE_CHECK`` if checker is aligned with + en passant square and king. New features: -* Highlight last move and checks when rendering board SVGs. +* Implemented Lichess winning chance model for ``chess.engine.Score``: + ``score.wdl(model="lichess")``. -New in v0.15.3 --------------- +New in v1.3.2 (12th Dec 2020) +----------------------------- Bugfixes: -* `pgn.Game.errors` was not populated as documented. Thanks to Ryan Delaney - for reporting. - -New features: +* 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. -* 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 --------------- +New in v1.3.1 (6th Dec 2020) +---------------------------- 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 --------------- +* ``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. -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 --------------- +New in v1.3.0 (6th Nov 2020) +---------------------------- 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). +* 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. -* Documentation improvements. + 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: -* 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 --------------- - -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. +* Removed broken ``weakref``-based caching in ``chess.pgn.GameNode.board()``. 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)`. +* Added ``chess.pgn.GameNode.next()``. - -New in v0.13.3 --------------- - -**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. +New in v1.2.2 (29th Oct 2020) +----------------------------- 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. +* Fixed regression where releases were uploaded without the ``py.typed`` + marker. -New in v0.13.2 --------------- +New in v1.2.1 (26th Oct 2020) +----------------------------- 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 --------------- - -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)`. +* The primary location for the published package is now + https://pypi.org/project/chess/. Thanks to + `Kristian Glass `_ for transferring the + namespace. -* Allow `chess.Board.set_piece_at(square, None)`. + 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 `_). -* Added `chess.SquareSet.from_square(square)`. + ``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). -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 --------------- - -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 --------------- - -Bugfixes: - -* Another en passant related Bugfix for pure Python Gaviota tablebase probing. +New in v1.2.0 (22nd Oct 2020) +----------------------------- 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 --------------- - -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 --------------- - -Bugfixes: - -* Some Gaviota probe results were incorrect for positions where black could - capture en passant. - -New in v0.12.1 --------------- +* 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. 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. +* 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 v0.12.0 --------------- +New in v1.1.0 (4th Oct 2020) +---------------------------- 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 --------------- - -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. - - 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 --------------- - -Bugfixes: - -* Fix use-after-free in Gaviota tablebase initialization. - -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. +* 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()``. 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 -------------- - -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 -------------- - -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 -------------- +* 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. -**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 compability 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 -------------- +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 1daff3a14..d4d51a128 100644 --- a/README.rst +++ b/README.rst @@ -1,29 +1,27 @@ -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://ci.appveyor.com/api/projects/status/y9k3hdbm0f0nbum9/branch/master?svg=true - :target: https://ci.appveyor.com/project/niklasf/python-chess - -.. 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, move -validation and support for common formats. This is the Scholar's mate in +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 @@ -58,6 +56,16 @@ 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 `__ -------------------------------------------------------------------- @@ -73,7 +81,7 @@ python-chess: Features -------- -* Supports Python 3.6+ and PyPy3. +* Includes mypy typings. * IPython/Jupyter Notebook integration. `SVG rendering docs `_. @@ -83,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. @@ -121,8 +130,8 @@ 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. @@ -278,8 +287,6 @@ Features >>> import chess.engine >>> engine = chess.engine.SimpleEngine.popen_uci("stockfish") - >>> engine.id.get("name") - 'Stockfish 10 64 POPCNT' >>> board = chess.Board("1k1r4/pp1b1R2/3q2pp/4p3/2B5/4Q3/PPP2B2/2K5 b - - 0 1") >>> limit = chess.engine.Limit(time=2.0) @@ -288,34 +295,66 @@ Features >>> engine.quit() -Installing ----------- - -Download and install the latest release: - -:: - - pip install python-chess - -Selected use cases ------------------- - -If you like, let me know if you are creating something interesting 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/ -* deep learning for Crazyhouse – https://github.com/QueensGambit/CrazyAra -* a bridge between Lichess API and chess engines – https://github.com/careless25/lichess-bot +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 -* a GUI to play against UCI chess engines – http://johncheetham.com/projects/jcchess/ * 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 -Acknowledgements ----------------- + +Prior art +--------- Thanks to the Stockfish authors and thanks to Sam Tannous for publishing his approach to `avoid rotated bitboards with direct lookup (PDF) `_ @@ -326,8 +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 `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/appveyor.yml b/appveyor.yml deleted file mode 100644 index ee7719536..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,17 +0,0 @@ -environment: - matrix: - - PYTHON: "C:\\Python36" - - PYTHON: "C:\\Python37" - - PYTHON: "C:\\Python38" - -build: off - -install: - # Stockfish - - ps: Start-FileDownload "https://stockfishchess.org/files/stockfish-10-win.zip" - - 7z e stockfish-10-win.zip stockfish-10-win/Windows/*.exe - - ren stockfish_10_x64_popcnt.exe stockfish.exe - - set PATH=%cd%;%PATH% - -test_script: - - "%PYTHON%\\python.exe test.py -vv" diff --git a/chess/__init__.py b/chess/__init__.py index b610c081f..9ea44f36e 100644 --- a/chess/__init__.py +++ b/chess/__init__.py @@ -1,68 +1,91 @@ -# This file is part of the python-chess library. -# Copyright (C) 2012-2020 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.30.1" +__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, Generic, Hashable, Iterable, Iterator, List, Mapping, Optional, SupportsInt, Tuple, Type, TypeVar, Union +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 = bool -COLORS = [WHITE, BLACK] = [True, False] -COLOR_NAMES = ["black", "white"] +Color: TypeAlias = bool +WHITE: Color = True +BLACK: Color = False +COLORS: List[Color] = [WHITE, BLACK] +ColorName = Literal["white", "black"] +COLOR_NAMES: List[ColorName] = ["black", "white"] -PieceType = int -PIECE_TYPES = [PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING] = range(1, 7) +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"] -def piece_symbol(piece_type: PieceType, _PIECE_SYMBOLS: List[Optional[str]] = PIECE_SYMBOLS) -> str: - return typing.cast(str, _PIECE_SYMBOLS[piece_type]) +def piece_symbol(piece_type: PieceType) -> str: + return typing.cast(str, PIECE_SYMBOLS[piece_type]) 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" @@ -74,22 +97,23 @@ def piece_name(piece_type: PieceType) -> str: class Status(enum.IntFlag): VALID = 0 - NO_WHITE_KING = 1 - NO_BLACK_KING = 2 - TOO_MANY_KINGS = 4 - TOO_MANY_WHITE_PAWNS = 8 - TOO_MANY_BLACK_PAWNS = 16 - PAWNS_ON_BACKRANK = 32 - TOO_MANY_WHITE_PIECES = 64 - TOO_MANY_BLACK_PIECES = 128 - BAD_CASTLING_RIGHTS = 256 - INVALID_EP_SQUARE = 512 - OPPOSITE_CHECK = 1024 - EMPTY = 2048 - RACE_CHECK = 4096 - RACE_OVER = 8192 - RACE_MATERIAL = 16384 - TOO_MANY_CHECKERS = 32768 + 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 @@ -108,95 +132,317 @@ class Status(enum.IntFlag): 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()`. + """ - -Square = int -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) + 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 square(file_index: int, rank_index: int) -> Square: +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: Square) -> int: +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: Square) -> int: +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: Square) -> str: - """Gets the name of the square, like ``a3``.""" - return SQUARE_NAMES[square] - 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_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] - - -Bitboard = int -BB_EMPTY = 0 -BB_ALL = 0xffff_ffff_ffff_ffff - -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_CORNERS = BB_A1 | BB_H1 | BB_A8 | BB_H8 -BB_CENTER = BB_D4 | BB_E4 | BB_D5 | BB_E5 - -BB_LIGHT_SQUARES = 0x55aa_55aa_55aa_55aa -BB_DARK_SQUARES = 0xaa55_aa55_aa55_aa55 - -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 -] = [0x0101_0101_0101_0101 << 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 +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: @@ -211,14 +457,14 @@ def scan_forward(bb: Bitboard) -> Iterator[Square]: def msb(bb: Bitboard) -> int: return bb.bit_length() - 1 -def scan_reversed(bb: Bitboard, *, _BB_SQUARES: List[Bitboard] = BB_SQUARES) -> Iterator[Square]: +def scan_reversed(bb: Bitboard) -> Iterator[Square]: while bb: r = bb.bit_length() - 1 yield r - bb ^= _BB_SQUARES[r] + bb ^= BB_SQUARES[r] -def popcount(bb: Bitboard, *, _bin: Callable[[int], str] = bin) -> int: - return _bin(bb).count("1") +# 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 @@ -237,11 +483,11 @@ def flip_horizontal(bb: Bitboard) -> Bitboard: 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)) + bb = bb ^ t ^ (t >> 28) t = (bb ^ (bb << 14)) & 0x3333_0000_3333_0000 - bb = bb ^ (t ^ (t >> 14)) + bb = bb ^ t ^ (t >> 14) t = (bb ^ (bb << 7)) & 0x5500_5500_5500_5500 - bb = bb ^ (t ^ (t >> 7)) + bb = bb ^ t ^ (t >> 7) return bb def flip_anti_diagonal(bb: Bitboard) -> Bitboard: @@ -249,9 +495,9 @@ def flip_anti_diagonal(bb: Bitboard) -> Bitboard: 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)) + bb = bb ^ t ^ (t >> 18) t = (bb ^ (bb << 9)) & 0xaa00_aa00_aa00_aa00 - bb = bb ^ (t ^ (t >> 9)) + bb = bb ^ t ^ (t >> 9) return bb @@ -313,9 +559,9 @@ def _sliding_attacks(square: Square, occupied: Bitboard, deltas: Iterable[int]) def _step_attacks(square: Square, deltas: Iterable[int]) -> Bitboard: return _sliding_attacks(square, BB_ALL, deltas) -BB_KNIGHT_ATTACKS = [_step_attacks(sq, [17, 15, 10, 6, -17, -15, -10, -6]) for sq in SQUARES] -BB_KING_ATTACKS = [_step_attacks(sq, [9, 8, 7, 1, -9, -8, -7, -1]) for sq in SQUARES] -BB_PAWN_ATTACKS = [[_step_attacks(sq, deltas) for sq in SQUARES] for deltas in [[-7, -9], [7, 9]]] +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 _edges(square: Square) -> Bitboard: @@ -332,8 +578,8 @@ def _carry_rippler(mask: Bitboard) -> Iterator[Bitboard]: break def _attack_table(deltas: List[int]) -> Tuple[List[Bitboard], List[Dict[Bitboard, Bitboard]]]: - mask_table = [] - attack_table = [] + mask_table: List[Bitboard] = [] + attack_table: List[Dict[Bitboard, Bitboard]] = [] for square in SQUARES: attacks = {} @@ -353,9 +599,9 @@ def _attack_table(deltas: List[int]) -> Tuple[List[Bitboard], List[Dict[Bitboard def _rays() -> List[List[Bitboard]]: - rays = [] + rays: List[List[Bitboard]] = [] for a, bb_a in enumerate(BB_SQUARES): - rays_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) @@ -370,22 +616,28 @@ def _rays() -> List[List[Bitboard]]: BB_RAYS = _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") +@dataclasses.dataclass class Piece: """A piece with type and color.""" - def __init__(self, piece_type: PieceType, color: Color) -> None: - self.piece_type = piece_type - self.color = color + piece_type: PieceType + """The piece type.""" + + color: Color + """The piece color.""" def symbol(self) -> str: """ @@ -403,7 +655,7 @@ def unicode_symbol(self, *, invert_color: bool = False) -> str: return UNICODE_PIECE_SYMBOLS[symbol] def __hash__(self) -> int: - return hash(self.piece_type * (self.color + 1)) + return self.piece_type + (-1 if self.color else 5) def __repr__(self) -> str: return f"Piece.from_symbol({self.symbol()!r})" @@ -415,14 +667,8 @@ def _repr_svg_(self) -> str: import chess.svg return chess.svg.piece(self, size=45) - def __eq__(self, other: object) -> bool: - if isinstance(other, Piece): - return (self.piece_type, self.color) == (other.piece_type, other.color) - else: - return NotImplemented - @classmethod - def from_symbol(cls, symbol: str) -> "Piece": + def from_symbol(cls, symbol: str) -> Piece: """ Creates a :class:`~chess.Piece` instance from a piece symbol. @@ -431,6 +677,7 @@ def from_symbol(cls, symbol: str) -> "Piece": return cls(PIECE_SYMBOLS.index(symbol.lower()), symbol.isupper()) +@dataclasses.dataclass(unsafe_hash=True) class Move: """ Represents a move from a square to a square and possibly the promotion @@ -439,15 +686,21 @@ class Move: Drops and null moves are supported. """ - def __init__(self, from_square: Square, to_square: Square, promotion: Optional[PieceType] = None, drop: Optional[PieceType] = None) -> None: - self.from_square = from_square - self.to_square = to_square - self.promotion = promotion - self.drop = drop + from_square: Square + """The source square.""" + + 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 an UCI string for the move. + Gets a UCI string for the move. For example, a move from a7 to a8 would be ``a7a8`` or ``a7a8q`` (if the latter is a promotion to a queen). @@ -469,50 +722,43 @@ def xboard(self) -> str: def __bool__(self) -> bool: return bool(self.from_square or self.to_square or self.promotion or self.drop) - def __eq__(self, other: object) -> bool: - if isinstance(other, Move): - return ( - self.from_square == other.from_square and - self.to_square == other.to_square and - self.promotion == other.promotion and - self.drop == other.drop) - else: - return NotImplemented - def __repr__(self) -> str: return f"Move.from_uci({self.uci()!r})" def __str__(self) -> str: return self.uci() - def __hash__(self) -> int: - return hash((self.to_square, self.from_square, self.promotion, self.drop)) - @classmethod - def from_uci(cls, uci: str) -> "Move": + def from_uci(cls, uci: str) -> Move: """ - Parses an UCI string. + 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 4 <= len(uci) <= 5: - 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 - if from_square == to_square: - raise ValueError(f"invalid uci (use 0000 for null moves): {uci!r}") + 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(f"expected uci string to be of length 4 or 5: {uci!r}") + raise InvalidMoveError(f"expected uci string to be of length 4 or 5: {uci!r}") @classmethod - def null(cls) -> "Move": + def null(cls) -> Move: """ Gets a null move. @@ -565,7 +811,13 @@ def _reset_board(self) -> None: self.occupied = BB_RANK_1 | BB_RANK_2 | BB_RANK_7 | BB_RANK_8 def reset_board(self) -> None: - """Resets piece positions to the starting position.""" + """ + 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) -> None: @@ -583,9 +835,21 @@ def _clear_board(self) -> None: self.occupied = BB_EMPTY def clear_board(self) -> None: - """Clears the board.""" + """ + Clears the board. + + :class:`~chess.Board` also clears the move stack. + """ self._clear_board() + 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 @@ -599,10 +863,12 @@ def pieces_mask(self, piece_type: PieceType, color: Color) -> Bitboard: 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: PieceType, color: Color) -> "SquareSet": + def pieces(self, piece_type: PieceType, color: Color) -> SquareSet: """ Gets pieces of the given type and color. @@ -649,16 +915,19 @@ def color_at(self, square: Square) -> Optional[Color]: 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 considered. """ - king_mask = self.occupied_co[color] & self.kings & ~self.promoted - return msb(king_mask) if king_mask else None + 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] @@ -679,7 +948,7 @@ def attacks_mask(self, square: Square) -> Bitboard: BB_FILE_ATTACKS[square][BB_FILE_MASKS[square] & self.occupied]) return attacks - def attacks(self, square: Square) -> "SquareSet": + def attacks(self, square: Square) -> SquareSet: """ Gets the set of attacked squares from the given square. @@ -690,7 +959,9 @@ def attacks(self, square: Square) -> "SquareSet": """ return SquareSet(self.attacks_mask(square)) - def _attackers_mask(self, color: Color, square: Square, occupied: Bitboard) -> Bitboard: + 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 @@ -708,27 +979,38 @@ def _attackers_mask(self, color: Color, square: Square, occupied: Bitboard) -> B return attackers & self.occupied_co[color] - def attackers_mask(self, color: Color, square: Square) -> Bitboard: - return self._attackers_mask(color, square, self.occupied) - - def is_attacked_by(self, color: Color, square: Square) -> bool: + 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)) + return bool(self.attackers_mask(color, square, None if occupied is None else SquareSet(occupied).mask)) - def attackers(self, color: Color, square: Square) -> "SquareSet": + 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)) + 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) @@ -745,13 +1027,13 @@ def pin_mask(self, color: Color, square: Square) -> Bitboard: 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 BB_RAYS[king][sniper] + return ray(king, sniper) break return BB_ALL - def pin(self, color: Color, square: Square) -> "SquareSet": + 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. @@ -817,6 +1099,8 @@ 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) @@ -854,17 +1138,20 @@ def set_piece_at(self, square: Square, piece: Optional[Piece], promoted: bool = 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: Optional[bool] = False) -> str: + 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: @@ -877,7 +1164,14 @@ def board_fen(self, *, promoted: Optional[bool] = False) -> str: 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: @@ -891,7 +1185,7 @@ def board_fen(self, *, promoted: Optional[bool] = False) -> str: return "".join(builder) def _set_board_fen(self, fen: str) -> None: - # Compability with set_fen(). + # Compatibility with set_fen(). fen = fen.strip() if " " in fen: raise ValueError(f"expected position part of fen, got multiple parts: {fen!r}") @@ -946,18 +1240,21 @@ def _set_board_fen(self, fen: str) -> None: 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. - :raises: :exc:`ValueError` if the FEN string is invalid. + :class:`~chess.Board` also clears the move stack. + + :raises: :exc:`ValueError` if syntactically invalid. """ self._set_board_fen(fen) - def piece_map(self) -> Dict[Square, Piece]: + def piece_map(self, *, mask: Bitboard = BB_ALL) -> Dict[Square, Piece]: """ Gets a dictionary of :class:`pieces ` by square index. """ - result = {} - for square in scan_reversed(self.occupied): + result: Dict[Square, Piece] = {} + for square in scan_reversed(self.occupied & mask): result[square] = typing.cast(Piece, self.piece_at(square)) return result @@ -970,19 +1267,23 @@ 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, sharnagl: int) -> None: - if not 0 <= sharnagl <= 959: - raise ValueError(f"chess960 position index not 0 <= {sharnagl!r} <= 959") + 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: @@ -1034,16 +1335,16 @@ def _set_chess960_pos(self, sharnagl: int) -> None: self.occupied = BB_RANK_1 | BB_RANK_2 | BB_RANK_7 | BB_RANK_8 self.promoted = BB_EMPTY - def set_chess960_pos(self, sharnagl: int) -> None: + def set_chess960_pos(self, scharnagl: int) -> None: """ Sets up a Chess960 starting position given its index between 0 and 959. Also see :func:`~chess.BaseBoard.from_chess960_pos()`. """ - self._set_chess960_pos(sharnagl) + self._set_chess960_pos(scharnagl) 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: @@ -1052,7 +1353,7 @@ def chess960_pos(self) -> Optional[int]: return None if self.pawns != BB_RANK_2 | BB_RANK_7: return None - if self.promoted: + if self._effective_promoted(): return None # Piece counts. @@ -1123,7 +1424,7 @@ def __repr__(self) -> str: return f"{type(self).__name__}({self.board_fen()!r})" def __str__(self) -> str: - builder = [] + builder: List[str] = [] for square in SQUARES_180: piece = self.piece_at(square) @@ -1141,7 +1442,7 @@ def __str__(self) -> str: return "".join(builder) - def unicode(self, *, invert_color: bool = False, borders: bool = False) -> str: + 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. @@ -1149,8 +1450,8 @@ def unicode(self, *, invert_color: bool = False, borders: bool = False) -> str: :param invert_color: Invert color of the Unicode pieces. :param borders: Show borders and a coordinate margin. """ - builder = [] - for rank_index in range(7, -1, -1): + builder: List[str] = [] + for rank_index in (range(7, -1, -1) if orientation else range(8)): if borders: builder.append(" ") builder.append("-" * 17) @@ -1159,12 +1460,12 @@ def unicode(self, *, invert_color: bool = False, borders: bool = False) -> str: 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) @@ -1172,19 +1473,20 @@ def unicode(self, *, invert_color: bool = False, borders: bool = False) -> str: if piece: builder.append(piece.unicode_symbol(invert_color=invert_color)) else: - builder.append(u"·") + 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) @@ -1219,10 +1521,10 @@ def apply_transform(self, f: Callable[[Bitboard], Bitboard]) -> None: self.occupied = f(self.occupied) self.promoted = f(self.promoted) - def transform(self: BaseBoardT, f: Callable[[Bitboard], Bitboard]) -> BaseBoardT: + def transform(self, f: Callable[[Bitboard], Bitboard]) -> Self: """ - Returns a transformed copy of the board by applying a bitboard - transformation function. + 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()`, @@ -1231,24 +1533,31 @@ def transform(self: BaseBoardT, f: Callable[[Bitboard], Bitboard]) -> BaseBoardT :func:`chess.shift_right()`. Alternatively, :func:`~chess.BaseBoard.apply_transform()` can be used - to apply the transformation in place. + to apply the transformation on the board. """ board = self.copy() board.apply_transform(f) return board - def mirror(self: BaseBoardT) -> BaseBoardT: + 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. + 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.transform(flip_vertical) - board.occupied_co[WHITE], board.occupied_co[BLACK] = board.occupied_co[BLACK], board.occupied_co[WHITE] + board = self.copy() + board.apply_mirror() return board - def copy(self: BaseBoardT) -> BaseBoardT: + def copy(self) -> Self: """Creates a copy of the board.""" board = type(self)(None) @@ -1266,10 +1575,10 @@ def copy(self: BaseBoardT) -> BaseBoardT: return board - def __copy__(self: BaseBoardT) -> BaseBoardT: + def __copy__(self) -> Self: return self.copy() - def __deepcopy__(self: BaseBoardT, memo: Dict[int, object]) -> BaseBoardT: + def __deepcopy__(self, memo: Dict[int, object]) -> Self: board = self.copy() memo[id(self)] = board return board @@ -1283,7 +1592,7 @@ def empty(cls: Type[BaseBoardT]) -> BaseBoardT: return cls(None) @classmethod - def from_chess960_pos(cls: Type[BaseBoardT], sharnagl: int) -> BaseBoardT: + def from_chess960_pos(cls: Type[BaseBoardT], scharnagl: int) -> BaseBoardT: """ Creates a new board, initialized with a Chess960 starting position. @@ -1293,15 +1602,15 @@ def from_chess960_pos(cls: Type[BaseBoardT], sharnagl: int) -> BaseBoardT: >>> 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 BoardT = TypeVar("BoardT", bound="Board") -class _BoardState(Generic[BoardT]): +class _BoardState: - def __init__(self, board: BoardT) -> None: + def __init__(self, board: Board) -> None: self.pawns = board.pawns self.knights = board.knights self.bishops = board.bishops @@ -1321,7 +1630,7 @@ def __init__(self, board: BoardT) -> None: self.halfmove_clock = board.halfmove_clock self.fullmove_number = board.fullmove_number - def restore(self, board: BoardT) -> None: + def restore(self, board: Board) -> None: board.pawns = self.pawns board.knights = self.knights board.bishops = self.bishops @@ -1365,16 +1674,17 @@ class Board(BaseBoard): :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 for implementing chess variants). + .. 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 = ["Standard", "Chess", "Classical", "Normal"] + 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 = STARTING_FEN + starting_fen: ClassVar[str] = STARTING_FEN tbw_suffix: ClassVar[Optional[str]] = ".rtbw" tbz_suffix: ClassVar[Optional[str]] = ".rtbz" @@ -1384,18 +1694,80 @@ class Board(BaseBoard): pawnless_tbz_suffix: ClassVar[Optional[str]] = None pawnless_tbw_magic: ClassVar[Optional[bytes]] = None pawnless_tbz_magic: ClassVar[Optional[bytes]] = None - connected_kings = False - one_king = True - captures_compulsory = False + 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. + """ - def __init__(self: BoardT, fen: Optional[str] = STARTING_FEN, *, chess960: bool = False) -> None: + 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.ep_square: Optional[Square] = None - self.move_stack: List[Move] = [] - self._stack: List[_BoardState[BoardT]] = [] + self.ep_square = None + self.move_stack = [] + self._stack: List[_BoardState] = [] if fen is None: self.clear() @@ -1405,13 +1777,40 @@ def __init__(self: BoardT, fen: Optional[str] = STARTING_FEN, *, chess960: bool self.set_fen(fen) @property - def pseudo_legal_moves(self) -> "PseudoLegalMoveGenerator": - return PseudoLegalMoveGenerator(self) + def legal_moves(self) -> LegalMoveGenerator: + """ + A dynamic list of legal moves. - @property - def legal_moves(self) -> "LegalMoveGenerator": + >>> 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 @@ -1433,7 +1832,7 @@ def clear(self) -> None: 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 @@ -1450,10 +1849,10 @@ def clear_board(self) -> None: 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: BoardT) -> BoardT: + def root(self) -> Self: """Returns a copy of the root position.""" if self._stack: board = type(self)(None, chess960=self.chess960) @@ -1462,12 +1861,26 @@ def root(self: BoardT) -> BoardT: 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: 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: 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() @@ -1498,7 +1911,7 @@ def generate_pseudo_legal_moves(self, from_mask: Bitboard = BB_ALL, to_mask: Bit 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) @@ -1521,7 +1934,7 @@ def generate_pseudo_legal_moves(self, from_mask: Bitboard = BB_ALL, to_mask: Bit 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) @@ -1548,7 +1961,7 @@ def generate_pseudo_legal_ep(self, from_mask: Bitboard = BB_ALL, to_mask: Bitboa capturers = ( self.pawns & self.occupied_co[self.turn] & from_mask & BB_PAWN_ATTACKS[not self.turn][self.ep_square] & - BB_RANKS[4 if self.turn else 3]) + BB_RANKS[RANK_5 if self.turn else RANK_4]) for capturer in scan_reversed(capturers): yield Move(capturer, self.ep_square) @@ -1562,7 +1975,7 @@ 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 checkers(self) -> "SquareSet": + def checkers(self) -> SquareSet: """ Gets the pieces currently giving check. @@ -1571,14 +1984,32 @@ def checkers(self) -> "SquareSet": return SquareSet(self.checkers_mask()) def is_check(self) -> bool: - """Returns if the current side to move is in check.""" + """Tests if the current side to move is in check.""" return bool(self.checkers_mask()) - def is_into_check(self, move: Move) -> bool: + def gives_check(self, move: Move) -> bool: + """ + Probes if the given move would put the opponent in check. The move + must be at least pseudo-legal. """ - Checks if the given move would leave the king in check or put it into - check. The move must be at least pseudo legal. + self.push(move) + try: + return self.is_check() + finally: + self.pop() + + def gives_checkmate(self, move: Move) -> bool: + """ + Probes if the given move would put the opponent in checkmate. The move + must be at least pseudo-legal. """ + self.push(move) + try: + return self.is_checkmate() + finally: + self.pop() + + def is_into_check(self, move: Move) -> bool: king = self.king(self.turn) if king is None: return False @@ -1595,11 +2026,11 @@ def was_into_check(self) -> bool: return king is not None and self.is_attacked_by(self.turn, king) def is_pseudo_legal(self, move: Move) -> bool: - # Null moves are not pseudo legal. + # 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 @@ -1616,18 +2047,19 @@ def is_pseudo_legal(self, move: Move) -> bool: if not self.occupied_co[self.turn] & from_mask: return False - # Only pawns can promote and only on the back rank. + # Only pawns can promote and only on the backrank. if move.promotion: 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 @@ -1643,6 +2075,7 @@ def is_pseudo_legal(self, move: Move) -> bool: return bool(self.attacks_mask(move.from_square) & to_mask) 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) -> bool: @@ -1659,28 +2092,28 @@ def is_variant_end(self) -> bool: return False def is_variant_loss(self) -> bool: - """Checks if a special variant-specific loss condition is fulfilled.""" + """ + Checks if the current side to move lost due to a variant-specific + condition. + """ return False def is_variant_win(self) -> bool: - """Checks if a special variant-specific win condition is fulfilled.""" + """ + Checks if the current side to move won due to a variant-specific + condition. + """ return False 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: bool = False) -> bool: """ - Checks if the game is over due to - :func:`checkmate `, - :func:`stalemate `, - :func:`insufficient material `, - the :func:`seventyfive-move rule `, - :func:`fivefold repetition ` - or a :func:`variant end condition `. + Check if the game is over by any rule. The game is not considered to be over by the :func:`fifty-move rule ` or @@ -1688,63 +2121,72 @@ def is_game_over(self, *, claim_draw: bool = False) -> bool: unless *claim_draw* is given. Note that checking the latter can be slow. """ - # 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 - - if claim_draw: - # Claim draw, including by three-fold repetition. - return self.can_claim_draw() - else: - # Five-fold repetition. - return self.is_fivefold_repetition() + return self.outcome(claim_draw=claim_draw) is not None def result(self, *, claim_draw: bool = False) -> str: """ - Gets the game result. + Return the result of a game: 1-0, 0-1, 1/2-1/2, or *. - ``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. - 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" + outcome = self.outcome(claim_draw=claim_draw) + return outcome.result() if outcome else "*" - # Checkmate. - if self.is_checkmate(): - return "0-1" if self.turn == WHITE else "1-0" + 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 `, + or a :func:`variant end condition `. + Returns the :class:`chess.Outcome` if the game has ended, otherwise + ``None``. - # Draw claimed. - if claim_draw and self.can_claim_draw(): - return "1/2-1/2" + Alternatively, use :func:`~chess.Board.is_game_over()` if you are not + interested in who won the game and why. - # Seventyfive-move rule or fivefold repetition. - if self.is_seventyfive_moves() or self.is_fivefold_repetition(): - return "1/2-1/2" + 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. + """ + # Variant support. + if self.is_variant_loss(): + 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) - # Insufficient material. + # Normal game end. + if self.is_checkmate(): + 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) -> bool: """Checks if the current position is a checkmate.""" @@ -1789,22 +2231,25 @@ def has_insufficient_material(self, color: Color) -> bool: # 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 self mate. + # 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 no dot have any other pieces, including bishops of the + # (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 self mate. + # 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.occupied_co[not color] & ~self.kings & ~self.rooks & ~self.queens) + return same_color and not self.pawns and not self.knights return True + def _is_halfmoves(self, n: int) -> bool: + return self.halfmove_clock >= n and any(self.generate_legal_moves()) + def is_seventyfive_moves(self) -> bool: """ Since the 1st of July 2014, a game is automatically drawn (without @@ -1812,11 +2257,7 @@ def is_seventyfive_moves(self) -> bool: or pawn move is equal to or greater than 150. Other means to end a game take precedence. """ - if self.halfmove_clock >= 150: - if any(self.generate_legal_moves()): - return True - - return False + return self._is_halfmoves(150) def is_fivefold_repetition(self) -> bool: """ @@ -1829,34 +2270,54 @@ def is_fivefold_repetition(self) -> bool: 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 is_fifty_moves(self) -> bool: + """ + 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. + """ + return self._is_halfmoves(100) + def can_claim_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 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. """ - # Fifty-move rule. - if self.halfmove_clock >= 100: - if any(self.generate_legal_moves()): - return True + 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) -> 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 + 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() @@ -1864,7 +2325,7 @@ def can_claim_threefold_repetition(self) -> bool: transpositions.update((transposition_key, )) # Count positions. - switchyard = [] + switchyard: List[Move] = [] while self.move_stack: move = self.pop() switchyard.append(move) @@ -1877,7 +2338,7 @@ def can_claim_threefold_repetition(self) -> bool: while switchyard: self.push(switchyard.pop()) - # Threefold repetition occured. + # Threefold repetition occurred. if transpositions[transposition_key] >= 3: return True @@ -1901,7 +2362,7 @@ def is_repetition(self, count: int = 3) -> bool: 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 + 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. """ @@ -1917,7 +2378,7 @@ def is_repetition(self, count: int = 3) -> bool: # Check full replay. transposition_key = self._transposition_key() - switchyard = [] + switchyard: List[Move] = [] try: while True: @@ -1941,15 +2402,12 @@ def is_repetition(self, count: int = 3) -> bool: return False - def _board_state(self: BoardT) -> _BoardState[BoardT]: - return _BoardState(self) - def _push_capture(self, move: Move, capture_square: Square, piece_type: PieceType, was_promoted: bool) -> None: pass - def push(self: BoardT, move: Move) -> None: + 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 @@ -1965,14 +2423,17 @@ def push(self: BoardT, move: Move) -> None: Null moves just increment the move counters, switch turns and forfeit en passant capturing. - :warning: Moves are not checked for legality. It is the callers - responsibility to ensure the move is at least pseudo-legal or + .. 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. """ # 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(self._board_state()) + self._stack.append(board_state) # Reset en passant square. ep_square = self.ep_square @@ -2008,30 +2469,30 @@ def push(self: BoardT, move: Move) -> None: 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. @@ -2065,11 +2526,11 @@ def push(self: BoardT, move: Move) -> None: # Swap turn. self.turn = not self.turn - def pop(self: BoardT) -> Move: + 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() self._stack.pop().restore(self) @@ -2083,12 +2544,34 @@ def peek(self) -> Move: """ return self.move_stack[-1] + 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()) @@ -2099,7 +2582,7 @@ def castling_shredder_fen(self) -> str: return "".join(builder) def castling_xfen(self) -> str: - builder = [] + builder: List[str] = [] for color in COLORS: king = self.king(color) @@ -2135,13 +2618,13 @@ 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, *, shredder: bool = False, en_passant: str = "legal", promoted: Optional[bool] = None) -> str: + def fen(self, *, shredder: bool = False, en_passant: EnPassantSpec = "legal", promoted: Optional[bool] = None) -> str: """ Gets a FEN representation of the position. 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 + 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`), @@ -2167,7 +2650,7 @@ def fen(self, *, shredder: bool = False, en_passant: str = "legal", promoted: Op str(self.fullmove_number) ]) - def shredder_fen(self, *, en_passant: str = "legal", promoted: Optional[bool] = None) -> str: + def shredder_fen(self, *, en_passant: EnPassantSpec = "legal", promoted: Optional[bool] = None) -> str: return " ".join([ self.epd(shredder=True, en_passant=en_passant, promoted=promoted), str(self.halfmove_clock), @@ -2178,7 +2661,8 @@ 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. """ parts = fen.split() @@ -2236,7 +2720,7 @@ def set_fen(self, fen: str) -> None: raise ValueError(f"half-move clock cannot be negative: {fen!r}") # Check that the full-move number part is valid. - # 0 is allowed for compability, but later replaced with 1. + # 0 is allowed for compatibility, but later replaced with 1. try: fullmove_part = parts.pop(0) except IndexError: @@ -2304,6 +2788,8 @@ 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. """ @@ -2318,8 +2804,8 @@ def set_piece_map(self, pieces: Mapping[Square, Piece]) -> None: super().set_piece_map(pieces) self.clear_stack() - def set_chess960_pos(self, sharnagl: int) -> None: - super().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 @@ -2331,11 +2817,11 @@ def set_chess960_pos(self, sharnagl: int) -> None: 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. """ @@ -2357,13 +2843,11 @@ def chess960_pos(self, *, ignore_turn: bool = False, ignore_castling: bool = Fal return super().chess960_pos() def _epd_operations(self, operations: Mapping[str, Union[None, str, int, float, Move, Iterable[Move]]]) -> str: - epd = [] + epd: List[str] = [] first_op = True for opcode, operand in operations.items(): - assert opcode != "-", "dash (-) is not a valid epd opcode" - for blacklisted in [" ", "\n", "\t", "\r"]: - assert blacklisted not in opcode, f"invalid character {blacklisted!r} in epd opcode: {opcode!r}" + self._validate_epd_opcode(opcode) if not first_op: epd.append(" ") @@ -2381,16 +2865,16 @@ def _epd_operations(self, operations: Mapping[str, Union[None, str, int, float, elif isinstance(operand, float): assert math.isfinite(operand), f"expected numeric epd operand to be finite, got: {operand}" epd.append(f" {operand};") - elif opcode in ["pv", "am", "bm"] and not isinstance(operand, str) and hasattr(operand, "__iter__"): - # Value is a set of moves or a variation. - position = Board(self.shredder_fen()) if opcode == "pv" else self + elif opcode == "pv" and not isinstance(operand, str) and hasattr(operand, "__iter__"): + position = self.copy(stack=False) for move in operand: - assert isinstance(move, Move), f"expected epd operand {opcode} to yield moves, got: {move!r}" epd.append(" ") - epd.append(position.san(move)) - if opcode == "pv": - position.push(move) - + 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(san) epd.append(";") else: # Append as escaped string. @@ -2400,7 +2884,7 @@ def _epd_operations(self, operations: Mapping[str, Union[None, str, int, float, return "".join(epd) - def epd(self, *, shredder: bool = False, en_passant: str = "legal", promoted: Optional[bool] = None, **operations: Union[None, str, int, float, Move, Iterable[Move]]) -> str: + 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. @@ -2409,20 +2893,20 @@ def epd(self, *, shredder: bool = False, en_passant: str = "legal", promoted: Op EPD operations can be given as keyword arguments. Supported operands are strings, integers, finite floats, legal moves and ``None``. - Aditionally, the operation ``pv`` also accepts a legal variation as - a list of moves. The operations ``bm`` and ``bm`` also accept a list of + 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. 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: >>> import chess >>> >>> board = chess.Board() - >>> board.epd(hmvc=board.halfmove_clock, fmvc=board.fullmove_number) - 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - hmvc 0; fmvc 1;' + >>> board.epd(hmvc=board.halfmove_clock, fmvn=board.fullmove_number) + 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - hmvc 0; fmvn 1;' """ if en_passant == "fen": ep_square = self.ep_square @@ -2441,7 +2925,18 @@ def epd(self, *, shredder: bool = False, en_passant: str = "legal", promoted: Op return " ".join(epd) - def _parse_epd_ops(self: BoardT, operation_part: str, make_board: Callable[[], BoardT]) -> Dict[str, Union[None, str, int, float, Move, List[Move]]]: + 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 = "" @@ -2454,6 +2949,7 @@ def _parse_epd_ops(self: BoardT, operation_part: str, make_board: Callable[[], B if opcode == "-": opcode = "" elif opcode: + self._validate_epd_opcode(opcode) state = "after_opcode" elif ch is None or ch == ";": if opcode == "-": @@ -2528,7 +3024,7 @@ def _parse_epd_ops(self: BoardT, operation_part: str, make_board: Callable[[], B if opcode == "pv": # A variation. - variation = [] + variation: List[Move] = [] for token in operand.split(): move = position.parse_xboard(token) variation.append(move) @@ -2702,14 +3198,14 @@ def variation_san(self, variation: Iterable[Move]) -> str: 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(f"illegal move {move} in position {board.fen()}") + raise IllegalMoveError(f"illegal move {move} in position {board.fen()}") if board.turn == WHITE: san.append(f"{board.fullmove_number}. {board.san_and_push(move)}") @@ -2725,50 +3221,76 @@ def parse_san(self, san: str) -> Move: Uses the current position as the context to parse a move in standard 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(f"illegal san: {san!r} in {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 in ["--", "Z0"]: + 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(f"invalid san: {san!r}") - - # 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] & ~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 + from_mask &= self.pawns - # Filter by source file. - if match.group(2): - from_mask &= BB_FILES[FILE_NAMES.index(match.group(2))] - - # 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 @@ -2777,23 +3299,28 @@ def parse_san(self, san: str) -> Move: continue if matched_move: - raise ValueError(f"ambiguous san: {san!r} in {self.fen()}") + raise AmbiguousMoveError(f"ambiguous san: {san!r} in {self.fen()}") matched_move = move if not matched_move: - raise ValueError(f"illegal san: {san!r} in {self.fen()}") + raise IllegalMoveError(f"illegal san: {san!r} in {self.fen()}") return matched_move 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) @@ -2821,8 +3348,12 @@ def parse_uci(self, uci: str) -> Move: 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) @@ -2833,7 +3364,7 @@ def parse_uci(self, uci: str) -> Move: move = self._from_chess960(self.chess960, move.from_square, move.to_square, move.promotion, move.drop) if not self.is_legal(move): - raise ValueError(f"illegal uci: {uci!r} in {self.fen()}") + raise IllegalMoveError(f"illegal uci: {uci!r} in {self.fen()}") return move @@ -2843,8 +3374,12 @@ def push_uci(self, uci: str) -> Move: 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) @@ -2862,30 +3397,9 @@ def xboard(self, move: Move, chess960: Optional[bool] = None) -> str: return "O-O-O" def parse_xboard(self, xboard: str) -> Move: - if xboard == "@@@@": - return Move.null() - elif "," in xboard: - raise ValueError(f"unsupported multi-leg xboard move: {xboard!r}") + return self.parse_san(xboard) - try: - move = Move.from_uci(xboard) - move = self._to_chess960(move) - move = self._from_chess960(self.chess960, move.from_square, move.to_square, move.promotion, move.drop) - if not self.is_legal(move): - raise ValueError(f"illegal xboard move: {xboard!r} in {self.fen()}") - return move - except ValueError: - pass - - try: - return self.parse_san(xboard) - except ValueError: - raise ValueError(f"invalid or illegal xboard move: {xboard!r} in {self.fen()}") - - def push_xboard(self, xboard: str) -> Move: - move = self.parse_xboard(xboard) - self.push(move) - return move + push_xboard = push_san def is_en_passant(self, move: Move) -> bool: """Checks if the given pseudo-legal move is an en passant capture.""" @@ -2908,17 +3422,21 @@ 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.promoted or - cr & BB_RANK_8 and touched & self.kings & self.occupied_co[BLACK] & ~self.promoted) + 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. """ - return self.is_zeroing(move) or self._reduces_castling_rights(move) + return self.is_zeroing(move) or self._reduces_castling_rights(move) or self.has_legal_en_passant() def is_castling(self, move: Move) -> bool: """Checks if the given pseudo-legal move is a castling move.""" @@ -2959,16 +3477,16 @@ def clean_castling_rights(self) -> Bitboard: 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 back rank. - 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 + 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: @@ -2984,7 +3502,7 @@ def clean_castling_rights(self) -> Bitboard: 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_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): @@ -3006,7 +3524,7 @@ def has_kingside_castling_rights(self, color: Color) -> bool: 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 @@ -3017,7 +3535,7 @@ def has_kingside_castling_rights(self, color: Color) -> bool: if rook > king_mask: return True - castling_rights = castling_rights & (castling_rights - 1) + castling_rights &= castling_rights - 1 return False @@ -3027,7 +3545,7 @@ def has_queenside_castling_rights(self, color: Color) -> bool: 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 @@ -3038,7 +3556,7 @@ def has_queenside_castling_rights(self, color: Color) -> bool: if rook < king_mask: return True - castling_rights = castling_rights & (castling_rights - 1) + castling_rights &= castling_rights - 1 return False @@ -3070,9 +3588,6 @@ 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 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. @@ -3093,7 +3608,8 @@ def status(self) -> Status: :data:`~chess.STATUS_RACE_CHECK`, :data:`~chess.STATUS_RACE_OVER`, :data:`~chess.STATUS_RACE_MATERIAL`, - :data:`~chess.STATUS_TOO_MANY_CHECKERS`. + :data:`~chess.STATUS_TOO_MANY_CHECKERS`, + :data:`~chess.STATUS_IMPOSSIBLE_CHECK`. """ errors = STATUS_VALID @@ -3102,11 +3618,11 @@ def status(self) -> Status: 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. @@ -3130,7 +3646,8 @@ def status(self) -> Status: errors |= STATUS_BAD_CASTLING_RIGHTS # En passant. - if self.ep_square != self._valid_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. @@ -3138,8 +3655,23 @@ def status(self) -> Status: errors |= STATUS_OPPOSITE_CHECK # More than the maximum number of possible checkers in the variant. - if popcount(self.checkers_mask()) > 2: - errors |= STATUS_TOO_MANY_CHECKERS + 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: + if popcount(checkers) > 2 or (popcount(checkers) == 2 and ray(lsb(checkers), msb(checkers)) & our_kings): + errors |= STATUS_IMPOSSIBLE_CHECK return errors @@ -3148,11 +3680,11 @@ def _valid_ep_square(self) -> Optional[Square]: return None if self.turn == WHITE: - ep_rank = 5 + 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 = 2 + ep_rank = RANK_3 pawn_mask = shift_up(BB_SQUARES[self.ep_square]) seventh_rank_mask = shift_down(BB_SQUARES[self.ep_square]) @@ -3179,9 +3711,6 @@ def is_valid(self) -> bool: """ Checks some basic validity requirements. - Move making, generation and validation are only guaranteed to work on - a valid board. - See :func:`~chess.Board.status()` for details. """ return self.status() == STATUS_VALID @@ -3243,14 +3772,14 @@ def _is_safe(self, king: Square, blockers: Bitboard, move: Move) -> bool: not self._ep_skewered(king, move.from_square)) else: return bool(not blockers & BB_SQUARES[move.from_square] or - BB_RAYS[move.from_square][move.to_square] & BB_SQUARES[king]) + ray(move.from_square, move.to_square) & BB_SQUARES[king]) 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): @@ -3304,15 +3833,15 @@ def generate_legal_captures(self, from_mask: Bitboard = BB_ALL, to_mask: Bitboar self.generate_legal_ep(from_mask, to_mask)) 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)) + return any(self.attackers_mask(not self.turn, sq, occupied) for sq in scan_reversed(path)) 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 + king = self.occupied_co[self.turn] & self.kings & ~self._effective_promoted() & backrank & from_mask + king &= -king if not king: return @@ -3368,6 +3897,7 @@ def _to_chess960(self, move: Move) -> Move: def _transposition_key(self) -> Hashable: return (self.pawns, self.knights, self.bishops, self.rooks, self.queens, self.kings, + 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) @@ -3382,7 +3912,7 @@ def _repr_svg_(self) -> str: import chess.svg return chess.svg.board( board=self, - size=400, + size=390, lastmove=self.peek() if self.move_stack else None, check=self.king(self.turn) if self.is_check() else None) @@ -3399,20 +3929,34 @@ def __eq__(self, board: object) -> bool: 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) - def transform(self: BoardT, f: Callable[[Bitboard], Bitboard]) -> BoardT: + def transform(self, f: Callable[[Bitboard], Bitboard]) -> Self: board = self.copy(stack=False) board.apply_transform(f) - board.ep_square = None if self.ep_square is None else msb(f(BB_SQUARES[self.ep_square])) - board.castling_rights = f(self.castling_rights) return board - def mirror(self: BoardT) -> BoardT: - board = super().mirror() - board.turn = not self.turn + 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: BoardT, *, stack: Union[bool, int] = True) -> BoardT: + def copy(self, *, stack: Union[bool, int] = True) -> Self: """ Creates a copy of the board. @@ -3453,9 +3997,9 @@ def from_epd(cls: Type[BoardT], epd: str, *, chess960: bool = False) -> Tuple[Bo return board, board.set_epd(epd) @classmethod - def from_chess960_pos(cls: Type[BoardT], sharnagl: int) -> BoardT: + 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 @@ -3478,7 +4022,7 @@ def __contains__(self, move: Move) -> bool: return self.board.is_pseudo_legal(move) def __repr__(self) -> str: - builder = [] + builder: List[str] = [] for move in self: if self.board.is_legal(move): @@ -3513,7 +4057,7 @@ def __repr__(self) -> str: return f"" -IntoSquareSet = Union[SupportsInt, Iterable[Square]] +IntoSquareSet: TypeAlias = Union[SupportsInt, Iterable[Square]] class SquareSet: """ @@ -3595,7 +4139,7 @@ class SquareSet: def __init__(self, squares: IntoSquareSet = BB_EMPTY) -> None: try: - self.mask = squares.__int__() & BB_ALL # type: ignore + self.mask: Bitboard = squares.__int__() & BB_ALL # type: ignore return except AttributeError: self.mask = 0 @@ -3632,50 +4176,50 @@ def discard(self, square: Square) -> None: # frozenset def isdisjoint(self, other: IntoSquareSet) -> bool: - """Test if the square sets are disjoint.""" + """Tests if the square sets are disjoint.""" return not bool(self & other) def issubset(self, other: IntoSquareSet) -> bool: - """Test if this square set is a subset of another.""" - return not bool(~self & other) + """Tests if this square set is a subset of another.""" + return not bool(self & ~SquareSet(other)) def issuperset(self, other: IntoSquareSet) -> bool: - """Test if this square set is a superset of another.""" - return not bool(self & ~SquareSet(other)) + """Tests if this square set is a superset of another.""" + return not bool(~self & other) - def union(self, other: IntoSquareSet) -> "SquareSet": + def union(self, other: IntoSquareSet) -> SquareSet: return self | other - def __or__(self, other: IntoSquareSet) -> "SquareSet": + def __or__(self, other: IntoSquareSet) -> SquareSet: r = SquareSet(other) r.mask |= self.mask return r - def intersection(self, other: IntoSquareSet) -> "SquareSet": + def intersection(self, other: IntoSquareSet) -> SquareSet: return self & other - def __and__(self, other: IntoSquareSet) -> "SquareSet": + def __and__(self, other: IntoSquareSet) -> SquareSet: r = SquareSet(other) r.mask &= self.mask return r - def difference(self, other: IntoSquareSet) -> "SquareSet": + def difference(self, other: IntoSquareSet) -> SquareSet: return self - other - def __sub__(self, other: IntoSquareSet) -> "SquareSet": + def __sub__(self, other: IntoSquareSet) -> SquareSet: r = SquareSet(other) r.mask = self.mask & ~r.mask return r - def symmetric_difference(self, other: IntoSquareSet) -> "SquareSet": + def symmetric_difference(self, other: IntoSquareSet) -> SquareSet: return self ^ other - def __xor__(self, other: IntoSquareSet) -> "SquareSet": + def __xor__(self, other: IntoSquareSet) -> SquareSet: r = SquareSet(other) r.mask ^= self.mask return r - def copy(self) -> "SquareSet": + def copy(self) -> SquareSet: return SquareSet(self.mask) # set @@ -3684,7 +4228,7 @@ def update(self, *others: IntoSquareSet) -> None: for other in others: self |= other - def __ior__(self, other: IntoSquareSet) -> "SquareSet": + def __ior__(self, other: IntoSquareSet) -> SquareSet: self.mask |= SquareSet(other).mask return self @@ -3692,21 +4236,21 @@ def intersection_update(self, *others: IntoSquareSet) -> None: for other in others: self &= other - def __iand__(self, other: IntoSquareSet) -> "SquareSet": + 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": + def __isub__(self, other: IntoSquareSet) -> SquareSet: self.mask &= ~SquareSet(other).mask return self def symmetric_difference_update(self, other: IntoSquareSet) -> None: self ^= other - def __ixor__(self, other: IntoSquareSet) -> "SquareSet": + def __ixor__(self, other: IntoSquareSet) -> SquareSet: self.mask ^= SquareSet(other).mask return self @@ -3714,7 +4258,7 @@ def remove(self, square: Square) -> None: """ 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: @@ -3724,9 +4268,9 @@ def remove(self, square: Square) -> None: 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 SquareSet") @@ -3736,7 +4280,7 @@ def pop(self) -> Square: return square def clear(self) -> None: - """Remove all elements from this set.""" + """Removes all elements from this set.""" self.mask = BB_EMPTY # SquareSet @@ -3745,12 +4289,12 @@ def carry_rippler(self) -> Iterator[Bitboard]: """Iterator over the subsets of this set.""" return _carry_rippler(self.mask) - def mirror(self) -> "SquareSet": + def mirror(self) -> SquareSet: """Returns a vertically mirrored copy of this square set.""" return SquareSet(flip_vertical(self.mask)) def tolist(self) -> List[bool]: - """Convert the set to a list of 64 bools.""" + """Converts the set to a list of 64 bools.""" result = [False] * 64 for square in self: result[square] = True @@ -3765,21 +4309,21 @@ def __eq__(self, other: object) -> bool: except (TypeError, ValueError): return NotImplemented - def __lshift__(self, shift: int) -> "SquareSet": + def __lshift__(self, shift: int) -> SquareSet: return SquareSet((self.mask << shift) & BB_ALL) - def __rshift__(self, shift: int) -> "SquareSet": + def __rshift__(self, shift: int) -> SquareSet: return SquareSet(self.mask >> shift) - def __ilshift__(self, shift: int) -> "SquareSet": + def __ilshift__(self, shift: int) -> SquareSet: self.mask = (self.mask << shift) & BB_ALL return self - def __irshift__(self, shift: int) -> "SquareSet": + def __irshift__(self, shift: int) -> SquareSet: self.mask >>= shift return self - def __invert__(self) -> "SquareSet": + def __invert__(self) -> SquareSet: return SquareSet(~self.mask & BB_ALL) def __int__(self) -> int: @@ -3792,7 +4336,7 @@ def __repr__(self) -> str: return f"SquareSet({self.mask:#021_x})" def __str__(self) -> str: - builder = [] + builder: List[str] = [] for square in SQUARES_180: mask = BB_SQUARES[square] @@ -3807,10 +4351,50 @@ def __str__(self) -> str: 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: Square) -> "SquareSet": + def from_square(cls, square: Square) -> SquareSet: """ Creates a :class:`~chess.SquareSet` from a single square. diff --git a/chess/engine.py b/chess/engine.py index c310df35c..c66bc0c45 100644 --- a/chess/engine.py +++ b/chess/engine.py @@ -1,18 +1,4 @@ -# This file is part of the python-chess library. -# Copyright (C) 2012-2020 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 @@ -20,60 +6,40 @@ import concurrent.futures import contextlib import copy +import dataclasses import enum +import inspect import logging -import warnings +import math import shlex import subprocess import sys import threading +import time import typing -import os import re +import chess + +from chess import Color from types import TracebackType -from typing import Any, Awaitable, Callable, Coroutine, Deque, Dict, Generator, Generic, Iterable, Iterator, List, Mapping, MutableMapping, NamedTuple, Optional, Text, Tuple, Type, TypeVar, Union - -try: - # Python 3.7 - from asyncio import get_running_loop as _get_running_loop -except ImportError: - from asyncio import _get_running_loop - -try: - # Python 3.7 - from asyncio import all_tasks as _all_tasks -except ImportError: - _all_tasks = asyncio.Task.all_tasks - -try: - # Python 3.7 - from asyncio import run as _run -except ImportError: - _T = TypeVar("_T") - - def _run(main: Awaitable[_T], *, debug: bool = False) -> _T: - assert _get_running_loop() is None - assert asyncio.iscoroutine(main) - - loop = asyncio.new_event_loop() - try: - asyncio.set_event_loop(loop) - loop.set_debug(debug) - return loop.run_until_complete(main) - finally: - try: - loop.run_until_complete(asyncio.gather(*_all_tasks(loop), return_exceptions=True)) - loop.run_until_complete(loop.shutdown_asyncgens()) - finally: - asyncio.set_event_loop(None) - loop.close() +from typing import Any, Callable, Coroutine, Deque, Dict, Generator, Generic, Iterable, Iterator, List, Literal, Mapping, MutableMapping, Optional, Tuple, Type, TypedDict, TypeVar, Union -import chess +if typing.TYPE_CHECKING: + from typing_extensions import override +else: + 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") -EngineProtocolT = TypeVar("EngineProtocolT", bound="EngineProtocol") +ProtocolT = TypeVar("ProtocolT", bound="Protocol") ConfigValue = Union[str, int, bool, None] ConfigMapping = Mapping[str, ConfigValue] @@ -85,130 +51,21 @@ def _run(main: Awaitable[_T], *, debug: bool = False) -> _T: MANAGED_OPTIONS = ["uci_chess960", "uci_variant", "multipv", "ponder"] -class EventLoopPolicy(asyncio.AbstractEventLoopPolicy): - """ - An event loop policy for thread-local event loops and child watchers. - Ensures each event loop is capable of spawning and watching subprocesses, - even when not running on the main thread. - - Windows: Uses :class:`~asyncio.ProactorEventLoop`. - - Unix: Uses :class:`~asyncio.SelectorEventLoop`. If available, - :class:`~asyncio.PidfdChildWatcher` is used to detect subprocess - termination (Python 3.9+ on Linux 5.3+). Otherwise the default child - watcher is used on the main thread and relatively slow eager polling - is used on all other threads. - """ - class _Local(threading.local): - loop: Optional[asyncio.AbstractEventLoop] = None - set_called = False - watcher: "Optional[asyncio.AbstractChildWatcher]" = None - - def __init__(self) -> None: - self._local = self._Local() - - def get_event_loop(self): - if self._local.loop is None and not self._local.set_called and threading.current_thread() is threading.main_thread(): - self.set_event_loop(self.new_event_loop()) - if self._local.loop is None: - raise RuntimeError(f"no current event loop in thread {threading.current_thread().name!r}") - return self._local.loop - - def set_event_loop(self, loop): - assert loop is None or isinstance(loop, asyncio.AbstractEventLoop) - self._local.set_called = True - self._local.loop = loop - if self._local.watcher is not None: - self._local.watcher.attach_loop(loop) - - def new_event_loop(self): - return asyncio.ProactorEventLoop() if sys.platform == "win32" else asyncio.SelectorEventLoop() - - def get_child_watcher(self): - if self._local.watcher is None: - self._local.watcher = self._init_watcher() - self._local.watcher.attach_loop(self._local.loop) - return self._local.watcher - - def set_child_watcher(self, watcher): - assert watcher is None or isinstance(watcher, asyncio.AbstractChildWatcher) - if self._local.watcher is not None: - self._local.watcher.close() - self._local.watcher = watcher - - def _init_watcher(self): - if sys.platform == "win32": - raise NotImplementedError - - try: - os.close(os.pidfd_open(os.getpid())) - return asyncio.PidfdChildWatcher() - except (AttributeError, OSError): - # Before Python 3.9 or before Linux 5.3 or the syscall is not - # permitted. - pass - - if threading.current_thread() is threading.main_thread(): - try: - return asyncio.ThreadedChildWatcher() - except AttributeError: - # Before Python 3.8. - return asyncio.SafeChildWatcher() - - class PollingChildWatcher(asyncio.SafeChildWatcher): - _loop: Optional[asyncio.AbstractEventLoop] - _callbacks: Dict[int, Any] - - def __init__(self) -> None: - super().__init__() - self._poll_handle: Optional[asyncio.Handle] = None - self._poll_delay = 0.001 - - def attach_loop(self, loop: Optional[asyncio.AbstractEventLoop]) -> None: - assert loop is None or isinstance(loop, asyncio.AbstractEventLoop) - - if self._loop is not None and loop is None and self._callbacks: - warnings.warn("A loop is being detached from a child watcher with pending handlers", RuntimeWarning) - - if self._poll_handle is not None: - self._poll_handle.cancel() - - self._loop = loop - if self._loop is not None: - self._poll_handle = self._loop.call_soon(self._poll) - self._do_waitpid_all() # type: ignore - - def _poll(self) -> None: - if self._loop: - self._do_waitpid_all() # type: ignore - self._poll_delay = min(self._poll_delay * 2, 1.0) - self._poll_handle = self._loop.call_later(self._poll_delay, self._poll) - - return PollingChildWatcher() - - -def run_in_background(coroutine: "Callable[[concurrent.futures.Future[T]], Coroutine[Any, Any, None]]", *, name: Optional[str] = None, debug: bool = False, _policy_lock: threading.Lock = threading.Lock()) -> T: +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 and returns the *future* result as soon as it is resolved. + 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 it is complete. - - Note: This installs a :class:`chess.engine.EventLoopPolicy` for the entire - process. + until complete. """ - assert asyncio.iscoroutinefunction(coroutine) - - with _policy_lock: - if not isinstance(asyncio.get_event_loop_policy(), EventLoopPolicy): - asyncio.set_event_loop_policy(EventLoopPolicy()) + assert inspect.iscoroutinefunction(coroutine) future: concurrent.futures.Future[T] = concurrent.futures.Future() def background() -> None: try: - _run(coroutine(future)) + asyncio.run(coroutine(future), debug=debug) future.cancel() except Exception as exc: future.set_exception(exc) @@ -232,15 +89,51 @@ class AnalysisComplete(Exception): """ -class Option(NamedTuple): +@dataclasses.dataclass(frozen=True) +class Option: """Information about an available engine option.""" name: str + """The name of the option.""" + 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 | + +--------+-----+------+------------------------------------------------+ + """ + 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": @@ -268,7 +161,7 @@ def parse(self, value: ConfigValue) -> ConfigValue: raise EngineError(f"invalid line-break in string option {self.name!r}: {value!r}") return value else: - raise EngineError("unknown option type: {}", self.type) + raise EngineError(f"unknown option type: {self.type!r}") def is_managed(self) -> bool: """ @@ -278,30 +171,50 @@ def is_managed(self) -> bool: return self.name.lower() in MANAGED_OPTIONS +@dataclasses.dataclass class Limit: - """Search termination condition.""" - - def __init__(self, *, - time: Optional[float] = None, - depth: Optional[int] = None, - nodes: Optional[int] = None, - mate: Optional[int] = None, - white_clock: Optional[float] = None, - black_clock: Optional[float] = None, - white_inc: Optional[float] = None, - black_inc: Optional[float] = None, - remaining_moves: Optional[int] = None): - self.time = time - self.depth = depth - self.nodes = nodes - self.mate = mate - self.white_clock = white_clock - self.black_clock = black_clock - self.white_inc = white_inc - self.black_inc = black_inc - self.remaining_moves = remaining_moves + """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)) @@ -309,34 +222,60 @@ def __repr__(self) -> str: if getattr(self, attr) is not None)) -try: - class InfoDict(typing.TypedDict, total=False): - """Dictionary of extra information sent by the engine.""" - 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: Tuple[int, int, int] - string: str -except AttributeError: - # Before Python 3.8. - InfoDict = dict # type: ignore +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.EngineProtocol.play()`.""" + """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], @@ -347,7 +286,7 @@ def __init__(self, resigned: bool = False) -> None: self.move = move self.ponder = ponder - self.info: InfoDict = info or {} + self.info = info or {} self.draw_offered = draw_offered self.resigned = resigned @@ -376,22 +315,45 @@ class Info(enum.IntFlag): 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.""" - def __init__(self, relative: "Score", turn: chess.Color) -> None: + 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": + def white(self) -> Score: """Gets the score from White's point of view.""" return self.pov(chess.WHITE) - def black(self) -> "Score": + def black(self) -> Score: """Gets the score from Black's point of view.""" return self.pov(chess.BLACK) - def pov(self, color: chess.Color) -> "Score": + 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 @@ -399,12 +361,13 @@ 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 __str__(self) -> str: - return str(self.relative) - def __eq__(self, other: object) -> bool: if isinstance(other, PovScore): return self.white() == other.white() @@ -439,6 +402,12 @@ class Score(abc.ABC): 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]: """ @@ -461,7 +430,8 @@ 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`` + .. warning:: + This conflates ``Mate(0)`` (we lost) and ``MateGiven`` (we won) to ``0``. """ @@ -470,8 +440,50 @@ def is_mate(self) -> bool: return self.mate() is not None @abc.abstractmethod - def __neg__(self) -> "Score": - pass + 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() @@ -513,6 +525,65 @@ def __ge__(self, other: object) -> bool: 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.""" @@ -526,19 +597,44 @@ def mate(self) -> None: 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": + def __neg__(self) -> Cp: return Cp(-self.cp) - def __pos__(self) -> "Cp": + def __pos__(self) -> Cp: return Cp(self.cp) - def __abs__(self) -> "Cp": + def __abs__(self) -> Cp: return Cp(abs(self.cp)) @@ -551,6 +647,10 @@ def __init__(self, moves: int) -> None: 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 @@ -559,19 +659,27 @@ def score(self, *, mate_score: Optional[int] = None) -> Optional[int]: 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"]: + def __neg__(self) -> Union[MateGivenType, Mate]: return MateGiven if not self.moves else Mate(-self.moves) - def __pos__(self) -> "Mate": + def __pos__(self) -> Mate: return Mate(self.moves) - def __abs__(self) -> Union["MateGivenType", "Mate"]: + def __abs__(self) -> Union[MateGivenType, Mate]: return MateGiven if not self.moves else Mate(abs(self.moves)) @@ -581,16 +689,23 @@ class MateGivenType(Score): 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": + def __pos__(self) -> MateGivenType: return self - def __abs__(self) -> "MateGivenType": + def __abs__(self) -> MateGivenType: return self def __repr__(self) -> str: @@ -602,8 +717,92 @@ def __str__(self) -> str: MateGiven = MateGivenType() -class MockTransport(asyncio.SubprocessTransport): - def __init__(self, protocol: "EngineProtocol") -> None: +@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``).""" + + def white(self) -> Wdl: + """Gets the :class:`~chess.engine.Wdl` from White's point of view.""" + return self.pov(chess.WHITE) + + def black(self) -> Wdl: + """Gets the :class:`~chess.engine.Wdl` from Black's point of view.""" + return self.pov(chess.BLACK) + + 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 + + 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() @@ -620,11 +819,11 @@ def expect_ping(self) -> None: def assert_done(self) -> None: assert not self.expectations, f"pending expectations: {self.expectations}" - def get_pipe_transport(self, fd: int) -> "MockTransport": # type: ignore + 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) -> None: + 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) @@ -632,13 +831,13 @@ def write(self, data: bytes) -> None: if line.startswith("ping ") and self.expected_pings: self.expected_pings -= 1 - self.protocol.pipe_data_received(1, line.replace("ping ", "pong ").encode("utf-8") + b"\n") + self.protocol.pipe_data_received(1, (line.replace("ping ", "pong ") + "\n").encode("utf-8")) else: - assert self.expectations, f"unexpected: {line}" + assert self.expectations, f"unexpected: {line!r}" expectation, responses = self.expectations.popleft() assert expectation == line, f"expected {expectation}, got: {line}" if responses: - self.protocol.pipe_data_received(1, "\n".join(responses).encode("utf-8") + b"\n") + 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) @@ -647,14 +846,20 @@ def get_returncode(self) -> Optional[int]: return None if self.expectations else 0 -class EngineProtocol(asyncio.SubprocessProtocol, metaclass=abc.ABCMeta): +class Protocol(asyncio.SubprocessProtocol, metaclass=abc.ABCMeta): """Protocol for communicating with a chess engine process.""" id: Dict[str, str] - options: MutableMapping[str, Option] + """ + Dictionary of information about the engine. Common keys are ``name`` + and ``author``. + """ - def __init__(self: EngineProtocolT) -> None: - self.loop = _get_running_loop() + 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 = { @@ -662,30 +867,30 @@ def __init__(self: EngineProtocolT) -> None: 2: bytearray(), # stderr } - self.command: Optional[BaseCommand[EngineProtocolT, Any]] = None - self.next_command: Optional[BaseCommand[EngineProtocolT, Any]] = None + 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 ducktyping. + # SubprocessTransport expected, but not checked to allow duck typing. self.transport = transport # type: ignore LOGGER.debug("%s: Connection made", self) - def connection_lost(self: EngineProtocolT, exc: Optional[Exception]) -> None: + 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. - if self.command is not None: - self.command._engine_terminated(self, code) - self.command = None - if self.next_command is not None: - self.next_command._engine_terminated(self, code) - self.next_command = None + 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) @@ -696,9 +901,8 @@ 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) - assert stdin is not None, "no pipe for stdin" - stdin.write(line.encode("utf-8")) - stdin.write(b"\n") + # 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 @@ -706,63 +910,64 @@ def pipe_data_received(self, fd: int, data: Union[bytes, str]) -> None: line_bytes, self.buffer[fd] = self.buffer[fd].split(b"\n", 1) if line_bytes.endswith(b"\r"): line_bytes = line_bytes[:-1] - line = line_bytes.decode("utf-8") - if fd == 1: - self.loop.call_soon(self._line_received, line) + try: + line = line_bytes.decode("utf-8") + except UnicodeDecodeError as err: + LOGGER.warning("%s: >> %r (%s)", self, bytes(line_bytes), err) else: - self.loop.call_soon(self.error_line_received, line) + 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: EngineProtocolT, line: str) -> None: + def _line_received(self, line: str) -> None: LOGGER.debug("%s: >> %s", self, line) self.line_received(line) if self.command: - self.command._line_received(self, line) + self.command._line_received(line) def line_received(self, line: str) -> None: pass - async def communicate(self: EngineProtocolT, command_factory: Callable[[], "BaseCommand[EngineProtocolT, T]"]) -> T: - command = command_factory() + 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 + 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._done() + self.next_command.set_finished() self.next_command = command - def previous_command_finished(_: "Optional[asyncio.Future[None]]") -> None: - if self.command is not None: - self.command._done() - + 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): + def cancel_if_cancelled(result: asyncio.Future[T]) -> None: if result.cancelled(): - cmd._cancel(self) + cmd._cancel() cmd.result.add_done_callback(cancel_if_cancelled) - cmd.finished.add_done_callback(previous_command_finished) - cmd._start(self) + cmd._start() + cmd.add_finished_callback(previous_command_finished) if self.command is None: - previous_command_finished(None) + previous_command_finished() elif not self.command.result.done(): self.command.result.cancel() elif not self.command.result.cancelled(): - self.command._cancel(self) + self.command._cancel() return await command.result @@ -770,6 +975,11 @@ 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.""" @@ -786,15 +996,28 @@ async def configure(self, options: ConfigMapping) -> None: """ Configures global engine options. - :param options: A dictionary of engine options, where the keys are - names of :py:attr:`~options`. Do not set options that are - managed automatically (:func:`chess.engine.Option.is_managed()`). + :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 play(self, board: chess.Board, limit: Limit, *, game: object = None, info: Info = INFO_NONE, ponder: bool = False, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> PlayResult: + async def send_opponent_information(self, *, opponent: Optional[Opponent] = None, engine_rating: Optional[int] = None) -> None: """ - Play a position. + 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. @@ -802,45 +1025,51 @@ async def play(self, board: chess.Board, limit: Limit, *, game: object = None, i 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``). + to the previous game (e.g., ``ucinewgame``, ``new``). :param info: Selects which additional information to retrieve from the - engine. ``INFO_NONE``, ``INFO_BASE`` (basic information that is + 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.EngineProtocol.configure()`. + 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 = {}) -> InfoDict: ... + 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 - `information <#chess.engine.PlayResult.info>`_. + :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 + :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``). + to the previous game (e.g., ``ucinewgame``, ``new``). :param info: Selects which information to retrieve from the - engine. ``INFO_NONE``, ``INFO_BASE`` (basic information that is + 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 @@ -849,7 +1078,7 @@ async def analyse(self, board: chess.Board, limit: Limit, *, multipv: Optional[i :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.EngineProtocol.configure()`. + with :func:`~chess.engine.Protocol.configure()`. """ analysis = await self.analysis(board, limit, multipv=multipv, game=game, info=info, root_moves=root_moves, options=options) @@ -859,7 +1088,7 @@ async def analyse(self, board: chess.Board, limit: Limit, *, multipv: Optional[i 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": + 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. @@ -871,9 +1100,9 @@ async def analysis(self, board: chess.Board, limit: Optional[Limit] = None, *, m :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``). + to the previous game (e.g., ``ucinewgame``, ``new``). :param info: Selects which information to retrieve from the - engine. ``INFO_NONE``, ``INFO_BASE`` (basic information that is + 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 @@ -882,11 +1111,37 @@ async def analysis(self, board: chess.Board, limit: Optional[Limit] = None, *, m :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.EngineProtocol.configure()`. + 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 the analysis at any time. + 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 @@ -894,112 +1149,129 @@ async def quit(self) -> None: """Asks the engine to shut down.""" @classmethod - async def popen(cls: Type[EngineProtocolT], command: Union[str, List[str]], *, setpgrp: bool = False, **kwargs: Any) -> Tuple[asyncio.SubprocessTransport, EngineProtocolT]: + 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] - popen_args = {} if setpgrp: try: # Windows. - popen_args["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore + popen_args["creationflags"] = popen_args.get("creationflags", 0) | subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore except AttributeError: # Unix. - popen_args["preexec_fn"] = os.setpgrp - popen_args.update(kwargs) + if sys.version_info >= (3, 11): + popen_args["process_group"] = 0 + else: + # Before Python 3.11 + popen_args["start_new_session"] = True - return await _get_running_loop().subprocess_exec(cls, *command, **popen_args) # type: ignore + return await asyncio.get_running_loop().subprocess_exec(cls, *command, **popen_args) class CommandState(enum.Enum): - New = 1 - Active = 2 - Cancelling = 3 - Done = 4 + NEW = enum.auto() + ACTIVE = enum.auto() + CANCELLING = enum.auto() + DONE = enum.auto() -class BaseCommand(Generic[EngineProtocolT, T]): - def __init__(self) -> None: - self.state = CommandState.New +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() - def _engine_terminated(self, engine: EngineProtocolT, code: int) -> None: - exc = EngineTerminatedError(f"engine process died unexpectedly (exit code: {code})") - if self.state == CommandState.Active: - self.engine_terminated(engine, exc) - elif self.state == CommandState.Cancelling: + 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) - elif self.state == CommandState.New: - self._handle_exception(engine, exc) + self._dispatch_finished() + elif self.state == CommandState.NEW: + self._handle_exception(exc) - def _handle_exception(self, engine: EngineProtocolT, exc: Exception) -> None: + def _handle_exception(self, exc: Exception) -> None: if not self.result.done(): self.result.set_exception(exc) else: - engine.loop.call_exception_handler({ - "message": f"engine command failed after returning preliminary result ({self.result!r})", + self._engine.loop.call_exception_handler({ # XXX + "message": f"{type(self).__name__} failed after returning preliminary result ({self.result!r})", "exception": exc, - "protocol": engine, - "transport": engine.transport, + "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] + 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, engine: EngineProtocolT) -> None: - assert self.state == CommandState.Active - self.state = CommandState.Cancelling - self.cancel(engine) + 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, engine: EngineProtocolT) -> None: - assert self.state == CommandState.New - self.state = CommandState.Active + def _start(self) -> None: + assert self.state == CommandState.NEW, self.state + self.state = CommandState.ACTIVE try: - self.check_initialized(engine) - self.start(engine) + self.check_initialized() + self.start() except EngineError as err: - self._handle_exception(engine, err) - - def _done(self) -> None: - assert self.state != CommandState.Done - self.state = CommandState.Done + self._handle_exception(err) - def _line_received(self, engine: EngineProtocolT, line: str) -> None: - assert self.state in [CommandState.Active, CommandState.Cancelling] + def _line_received(self, line: str) -> None: + assert self.state in [CommandState.ACTIVE, CommandState.CANCELLING], self.state try: - self.line_received(engine, line) + self.line_received(line) except EngineError as err: - self._handle_exception(engine, err) + self._handle_exception(err) - def cancel(self, engine: EngineProtocolT) -> None: + def cancel(self) -> None: pass - def check_initialized(self, engine: EngineProtocolT) -> None: - if not engine.initialized: + def check_initialized(self) -> None: + if not self._engine.initialized: raise EngineError("tried to run command, but engine is not initialized") - def start(self, engine: EngineProtocolT) -> None: + def start(self) -> None: raise NotImplementedError - def line_received(self, engine: EngineProtocolT, line: str) -> None: + def line_received(self, line: str) -> None: pass - def engine_terminated(self, engine: EngineProtocolT, exc: Exception) -> None: - self._handle_exception(engine, exc) + 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(EngineProtocol): +class UciProtocol(Protocol): """ An implementation of the `Universal Chess Interface `_ @@ -1008,103 +1280,106 @@ class UciProtocol(EngineProtocol): def __init__(self) -> None: super().__init__() - self.options: UciOptionMap[Option] = UciOptionMap() + 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[UciProtocol, None]): - def check_initialized(self, engine: UciProtocol) -> None: - if engine.initialized: + 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") - def start(self, engine: UciProtocol) -> None: - engine.send_line("uci") + @override + def start(self) -> None: + self.engine.send_line("uci") - def line_received(self, engine: UciProtocol, line: str) -> None: - if line == "uciok": - engine.initialized = True + @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 line.startswith("option "): - self._option(engine, line.split(" ", 1)[1]) - elif line.startswith("id "): - self._id(engine, line.split(" ", 1)[1]) + elif token == "option": + self._option(remaining) + elif token == "id": + self._id(remaining) - def _option(self, engine: UciProtocol, arg: str) -> None: + def _option(self, arg: str) -> None: current_parameter = None - - name: List[str] = [] - type: List[str] = [] - default: List[str] = [] - min = None - max = None - current_var = None + option_parts: dict[str, str] = {k: "" for k in ["name", "type", "default", "min", "max"]} var = [] - for token in arg.split(" "): - if token == "name" and not name: - current_parameter = "name" - elif token == "type" and not type: - current_parameter = "type" - elif token == "default" and not default: - current_parameter = "default" - elif token == "min" and min is None: - current_parameter = "min" - elif token == "max" and max is None: - current_parameter = "max" - elif token == "var": - current_parameter = "var" - if current_var is not None: - var.append(" ".join(current_var)) - current_var = [] - elif current_parameter == "name": - name.append(token) - elif current_parameter == "type": - type.append(token) - elif current_parameter == "default": - default.append(token) + 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": - current_var.append(token) - elif current_parameter == "min": - try: - min = int(token) - except ValueError: - LOGGER.exception("exception parsing option min") - elif current_parameter == "max": - try: - max = int(token) - except ValueError: - LOGGER.exception("exception parsing option max") + var.append(token) + elif current_parameter: + option_parts[current_parameter] = token - if current_var is not None: - var.append(" ".join(current_var)) + 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(" ".join(name), " ".join(type), None, min, max, var) - option = Option(without_default.name, without_default.type, without_default.parse(" ".join(default)), min, max, var) - engine.options[option.name] = option + 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: - engine.config[option.name] = option.default + self.engine.config[option.name] = option.default if option.default is not None and not option.is_managed() and option.name.lower() != "uci_analysemode": - engine.target_config[option.name] = option.default + self.engine.target_config[option.name] = option.default - def _id(self, engine: UciProtocol, arg: str) -> None: - key, value = arg.split(" ", 1) - engine.id[key] = value + 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: """ @@ -1117,19 +1392,27 @@ def debug(self, on: bool = True) -> None: self.send_line("debug off") async def ping(self) -> None: - class UciPingCommand(BaseCommand[UciProtocol, None]): - def start(self, engine: UciProtocol) -> None: - engine._isready() + class UciPingCommand(BaseCommand[None]): + def __init__(self, engine: UciProtocol) -> None: + super().__init__(engine) + self.engine = engine + + def start(self) -> None: + self.engine._isready() - def line_received(self, engine: UciProtocol, line: str) -> None: - if line == "readyok": + @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: %s", engine, line) + 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) @@ -1146,25 +1429,42 @@ def _setoption(self, name: str, value: ConfigValue) -> None: builder.append("value") builder.append(str(value)) - self.send_line(" ".join(builder)) + 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 collections.ChainMap(options, self.target_config).items(): + 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[UciProtocol, None]): - def start(self, engine: UciProtocol) -> None: - engine._configure(options) - engine.target_config.update({name: value for name, value in options.items() if value is not 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 @@ -1180,7 +1480,8 @@ def _position(self, board: chess.Board) -> None: # Send starting position. builder = ["position"] - root = board.root() + 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") @@ -1189,7 +1490,9 @@ def _position(self, board: chess.Board) -> None: builder.append(fen) # Send moves. - if board.move_stack: + 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) @@ -1202,185 +1505,238 @@ def _go(self, limit: Limit, *, root_moves: Optional[Iterable[chess.Move]] = None builder.append("ponder") if limit.white_clock is not None: builder.append("wtime") - builder.append(str(int(limit.white_clock * 1000))) + builder.append(str(max(1, round(limit.white_clock * 1000)))) if limit.black_clock is not None: builder.append("btime") - builder.append(str(int(limit.black_clock * 1000))) + builder.append(str(max(1, round(limit.black_clock * 1000)))) if limit.white_inc is not None: builder.append("winc") - builder.append(str(int(limit.white_inc * 1000))) + builder.append(str(round(limit.white_inc * 1000))) if limit.black_inc is not None: builder.append("binc") - builder.append(str(int(limit.black_inc * 1000))) + 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(int(limit.depth))) + builder.append(str(max(1, int(limit.depth)))) if limit.nodes is not None: builder.append("nodes") - builder.append(str(int(limit.nodes))) + builder.append(str(max(1, int(limit.nodes)))) if limit.mate is not None: builder.append("mate") - builder.append(str(int(limit.mate))) + builder.append(str(max(1, int(limit.mate)))) if limit.time is not None: builder.append("movetime") - builder.append(str(int(limit.time * 1000))) + builder.append(str(max(1, round(limit.time * 1000)))) if infinite: builder.append("infinite") - if root_moves: + if root_moves is not None: builder.append("searchmoves") - builder.extend(move.uci() for move in root_moves) + 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, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> PlayResult: - class UciPlayCommand(BaseCommand[UciProtocol, PlayResult]): - def start(self, engine: UciProtocol) -> None: + 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 = False + self.pondering: Optional[chess.Board] = None self.sent_isready = False + self.start_time = time.perf_counter() - if "UCI_AnalyseMode" in engine.options and "UCI_AnalyseMode" not in engine.target_config and all(name.lower() != "uci_analysemode" for name in options): - engine._setoption("UCI_AnalyseMode", False) - if "Ponder" in engine.options: - engine._setoption("Ponder", ponder) - if "MultiPV" in engine.options: - engine._setoption("MultiPV", engine.options["MultiPV"].default) + if self.engine.ponderhit: + self.engine.ponderhit = False + self.engine.send_line("ponderhit") + return - engine._configure(options) + 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) - if engine.first_game or engine.game != game: - engine.game = game - engine._ucinewgame() + 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 - engine._isready() + self.engine._isready() else: - self._readyok(engine) - - def line_received(self, engine: UciProtocol, line: str) -> None: - if line.startswith("info "): - self._info(engine, line.split(" ", 1)[1]) - elif line.startswith("bestmove "): - self._bestmove(engine, line.split(" ", 1)[1]) - elif line == "readyok" and self.sent_isready: - self._readyok(engine) + 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: %s", engine, line) + LOGGER.warning("%s: Unexpected engine output: %r", self.engine, line) - def _readyok(self, engine: UciProtocol) -> None: + def _readyok(self) -> None: self.sent_isready = False engine._position(board) engine._go(limit, root_moves=root_moves) - def _info(self, engine: UciProtocol, arg: str) -> None: + def _info(self, arg: str) -> None: if not self.pondering: - self.info.update(_parse_uci_info(arg, engine.board, info)) + self.info.update(_parse_uci_info(arg, self.engine.board, info)) - def _bestmove(self, engine: UciProtocol, arg: str) -> None: + def _bestmove(self, arg: str) -> None: if self.pondering: - self.pondering = False + self.pondering = None elif not self.result.cancelled(): - tokens = arg.split(None, 2) - - bestmove = None - if tokens[0] != "(none)": - try: - bestmove = engine.board.parse_uci(tokens[0]) - except ValueError as err: - raise EngineError(err) - - pondermove = None - if bestmove is not None and len(tokens) >= 3 and tokens[1] == "ponder" and tokens[2] != "(none)": - engine.board.push(bestmove) - try: - pondermove = engine.board.push_uci(tokens[2]) - except ValueError: - LOGGER.exception("engine sent invalid ponder move") - - self.result.set_result(PlayResult(bestmove, pondermove, self.info)) - - if ponder and pondermove: - self.pondering = True - engine._position(engine.board) - engine._go(limit, ponder=True) + 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(engine) + self.end() - def end(self, engine: UciProtocol) -> None: + def end(self) -> None: + engine.may_ponderhit = None self.set_finished() - def cancel(self, engine: UciProtocol) -> None: - engine.send_line("stop") + @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") - def engine_terminated(self, engine: UciProtocol, exc: Exception) -> None: + @override + def engine_terminated(self, exc: Exception) -> None: # Allow terminating engine while pondering. if not self.result.done(): - super().engine_terminated(engine, exc) + 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[UciProtocol, AnalysisResult]): - def start(self, engine: UciProtocol) -> None: - self.analysis = AnalysisResult(stop=lambda: self.cancel(engine)) + 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 "UCI_AnalyseMode" in engine.options and "UCI_AnalyseMode" not in engine.target_config and all(name.lower() != "uci_analysemode" for name in options): - engine._setoption("UCI_AnalyseMode", True) - if "MultiPV" in engine.options or (multipv and multipv > 1): - engine._setoption("MultiPV", 1 if multipv is None else multipv) + 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) - engine._configure(options) + self.engine._configure(options) - if engine.first_game or engine.game != game: - engine.game = game - engine._ucinewgame() + if self.engine.first_game or self.engine.game != game: + self.engine.game = game + self.engine._ucinewgame() self.sent_isready = True - engine._isready() + self.engine._isready() else: - self._readyok(engine) - - def line_received(self, engine: UciProtocol, line: str) -> None: - if line.startswith("info "): - self._info(engine, line.split(" ", 1)[1]) - elif line.startswith("bestmove "): - self._bestmove(engine, line.split(" ", 1)[1]) - elif line == "readyok" and self.sent_isready: - self._readyok(engine) + 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: %s", engine, line) + LOGGER.warning("%s: Unexpected engine output: %r", self.engine, line) - def _readyok(self, engine: UciProtocol) -> None: + def _readyok(self) -> None: self.sent_isready = False - engine._position(board) + self.engine._position(board) if limit: - engine._go(limit, root_moves=root_moves) + self.engine._go(limit, root_moves=root_moves) else: - engine._go(Limit(), root_moves=root_moves, infinite=True) + self.engine._go(Limit(), root_moves=root_moves, infinite=True) self.result.set_result(self.analysis) - def _info(self, engine: UciProtocol, arg: str) -> None: - self.analysis.post(_parse_uci_info(arg, engine.board, info)) + def _info(self, arg: str) -> None: + self.analysis.post(_parse_uci_info(arg, self.engine.board, info)) - def _bestmove(self, engine: UciProtocol, arg: str) -> None: + 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() + self.analysis.set_finished(best) - def cancel(self, engine: UciProtocol) -> None: - engine.send_line("stop") + @override + def cancel(self) -> None: + self.engine.send_line("stop") - def engine_terminated(self, engine: UciProtocol, exc: Exception) -> None: - LOGGER.debug("%s: Closing analysis because engine has been terminated (error: %s)", engine, exc) + @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) @@ -1388,102 +1744,153 @@ async def quit(self) -> None: 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 - tokens = arg.split(" ") - while tokens: - parameter = tokens.pop(0) + remaining_line = arg + while remaining_line: + parameter, remaining_line = _next_token(remaining_line) if parameter == "string": - info["string"] = " ".join(tokens) + info["string"] = remaining_line break - elif parameter in ["depth", "seldepth", "nodes", "multipv", "currmovenumber", "hashfull", "nps", "tbhits", "cpuload"]: + elif parameter in ["depth", "seldepth", "nodes", "multipv", "currmovenumber", + "hashfull", "nps", "tbhits", "cpuload", "movesleft"]: try: - info[parameter] = int(tokens.pop(0)) # type: ignore + 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) + LOGGER.error("Exception parsing %s from info: %r", parameter, arg) elif parameter == "time": try: - info["time"] = int(tokens.pop(0)) / 1000.0 + 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) + LOGGER.error("Exception parsing %s from info: %r", parameter, arg) elif parameter == "ebf": try: - info["ebf"] = float(tokens.pop(0)) + number, remaining_line = _next_token(remaining_line) + info["ebf"] = float(number) except (ValueError, IndexError): - LOGGER.error("exception parsing %s from info: %r", parameter, arg) + LOGGER.error("Exception parsing %s from info: %r", parameter, arg) elif parameter == "score" and selector & INFO_SCORE: try: - kind = tokens.pop(0) - value = tokens.pop(0) - if tokens and tokens[0] in ["lowerbound", "upperbound"]: - info[tokens.pop(0)] = True # type: ignore + 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) + 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) + LOGGER.error("Exception parsing score from info: %r", arg) elif parameter == "currmove": try: - info["currmove"] = chess.Move.from_uci(tokens.pop(0)) + 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) + 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 = int(tokens.pop(0)) - currline: List[chess.Move] = [] - - board = root_board.copy(stack=False) - while tokens and UCI_REGEX.match(tokens[0]): - currline.append(board.push_uci(tokens.pop(0))) + 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()) + 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 = board.push_uci(tokens.pop(0)) - refuted_by: List[chess.Move] = [] + refuted_text, remaining_line = _next_token(remaining_line) + refuted = board.push_uci(refuted_text) - while tokens and UCI_REGEX.match(tokens[0]): - refuted_by.append(board.push_uci(tokens.pop(0))) + 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()) + 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: List[chess.Move] = [] - board = root_board.copy(stack=False) - while tokens and UCI_REGEX.match(tokens[0]): - pv.append(board.push_uci(tokens.pop(0))) + 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()) + LOGGER.error("Exception parsing pv from info: %r, position at root: %s", arg, root_board.fen()) elif parameter == "wdl": try: - info["wdl"] = int(tokens.pop(0)), int(tokens.pop(0)), int(tokens.pop(0)) + 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) + 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 _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() + class UciOptionMap(MutableMapping[str, T]): """Dictionary with case-insensitive keys.""" - def __init__(self, data: Optional[Union[Iterable[Tuple[str, T]]]] = None, **kwargs: T) -> None: + 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 = {} @@ -1499,7 +1906,7 @@ def __delitem__(self, key: str) -> None: del self._store[key.lower()] def __iter__(self) -> Iterator[str]: - return (casedkey for casedkey, mappedvalue in self._store.values()) + return (casedkey for casedkey, _ in self._store.values()) def __len__(self) -> int: return len(self._store) @@ -1518,10 +1925,10 @@ def __eq__(self, other: object) -> bool: except (TypeError, AttributeError): return NotImplemented - def copy(self) -> "UciOptionMap[T]": + def copy(self) -> UciOptionMap[T]: return type(self)(self._store.values()) - def __copy__(self) -> "UciOptionMap[T]": + def __copy__(self) -> UciOptionMap[T]: return self.copy() def __repr__(self) -> str: @@ -1531,112 +1938,126 @@ def __repr__(self) -> str: XBOARD_ERROR_REGEX = re.compile(r"^\s*(Error|Illegal move)(\s*\([^()]+\))?\s*:") -class XBoardProtocol(EngineProtocol): +class XBoardProtocol(Protocol): """ An implementation of the - `XBoard protocol `_ (CECP). + `XBoard protocol `__ (CECP). """ def __init__(self) -> None: super().__init__() self.features: Dict[str, Union[int, str]] = {} self.id = {} - self.options = { + 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[XBoardProtocol, None]): - def check_initialized(self, engine: XBoardProtocol) -> None: - if engine.initialized: + 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") - def start(self, engine: XBoardProtocol) -> None: - engine.send_line("xboard") - engine.send_line("protover 2") - self.timeout_handle = engine.loop.call_later(2.0, lambda: self.timeout(engine)) + @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, engine: XBoardProtocol) -> None: - LOGGER.error("%s: Timeout during initialization", engine) - self.end(engine) + def timeout(self) -> None: + LOGGER.error("%s: Timeout during initialization", self.engine) + self.end() - def line_received(self, engine: XBoardProtocol, line: str) -> None: - if line.startswith("#"): + @override + def line_received(self, line: str) -> None: + token, remaining = _next_token(line) + if token.startswith("#"): pass - elif line.startswith("feature "): - self._feature(engine, line.split(" ", 1)[1]) + elif token == "feature": + self._feature(remaining) elif XBOARD_ERROR_REGEX.match(line): raise EngineError(line) - def _feature(self, engine: XBoardProtocol, arg: str) -> None: + 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"]: - engine.options[option.name] = option + self.engine.options[option.name] = option else: try: - engine.features[key] = int(value) + self.engine.features[key] = int(value) except ValueError: - engine.features[key] = value + self.engine.features[key] = value - if "done" in engine.features: + if "done" in self.engine.features: self.timeout_handle.cancel() - if engine.features.get("done"): - self.end(engine) + if self.engine.features.get("done"): + self.end() - def end(self, engine: XBoardProtocol) -> None: - if not engine.features.get("ping", 0): + 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 engine.features.get("setboard", 0): + 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 engine.features.get("reuse", 1): - LOGGER.warning("%s: Rejecting feature reuse=0", engine) - engine.send_line("reject reuse") - if not engine.features.get("sigterm", 1): - LOGGER.warning("%s: Rejecting feature sigterm=0", engine) - engine.send_line("reject sigterm") - if engine.features.get("usermove", 0): - LOGGER.warning("%s: Rejecting feature usermove=1", engine) - engine.send_line("reject usermove") - if engine.features.get("san", 0): - LOGGER.warning("%s: Rejecting feature san=1", engine) - engine.send_line("reject san") - - if "myname" in engine.features: - engine.id["name"] = str(engine.features["myname"]) - - if engine.features.get("memory", 0): - engine.options["memory"] = Option("memory", "spin", 16, 1, None, None) - engine.send_line("accept memory") - if engine.features.get("smp", 0): - engine.options["cores"] = Option("cores", "spin", 1, 1, None, None) - engine.send_line("accept smp") - if engine.features.get("egt"): - for egt in str(engine.features["egt"]).split(","): + 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}" - engine.options[name] = Option(name, "path", None, None, None, None) - engine.send_line("accept egt") + self.engine.options[name] = Option(name, "path", None, None, None, None) + self.engine.send_line("accepted egt") - for option in engine.options.values(): + for option in self.engine.options.values(): if option.default is not None: - engine.config[option.name] = option.default + self.engine.config[option.name] = option.default if option.default is not None and not option.is_managed(): - engine.target_config[option.name] = option.default + self.engine.target_config[option.name] = option.default - engine.initialized = True + self.engine.initialized = True self.result.set_result(None) self.set_finished() @@ -1652,13 +2073,14 @@ def _variant(self, variant: Optional[str]) -> None: self.send_line(f"variant {variant}") - def _new(self, board: chess.Board, game: object, options: ConfigMapping) -> None: + 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)) - # Setup start position. + # Set up starting position. root = board.root() - new_options = "random" in options or "computer" in options - new_game = self.first_game or self.game != game or new_options or root != self.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: @@ -1673,15 +2095,26 @@ def _new(self, board: chess.Board, game: object, options: ConfigMapping) -> None 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") + self.send_line("force") - if new_game: 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 @@ -1703,243 +2136,287 @@ def _new(self, board: chess.Board, game: object, options: ConfigMapping) -> None # Play moves from board stack. for move in board.move_stack[common_stack_len:]: - self.send_line(self.board.xboard(move)) + 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[XBoardProtocol, None]): - def start(self, engine: XBoardProtocol) -> 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}" - engine._ping(n) + self.engine._ping(n) - def line_received(self, engine: XBoardProtocol, line: str) -> None: + @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: %s", engine, line) + 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, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> PlayResult: + 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[XBoardProtocol, PlayResult]): - def start(self, engine: XBoardProtocol) -> None: + 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. - engine._new(board, game, options) + 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.remaining_moves or increment: - base_mins, base_secs = divmod(int((limit.white_clock if board.turn else limit.black_clock) or 0), 60) - engine.send_line(f"level {limit.remaining_moves or 0} {base_mins}:{base_secs:02d} {increment}") - + 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 engine.features: - LOGGER.warning("%s: Engine did not declare explicit support for node limits (feature nps=?)") - elif not engine.features["nps"]: + 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)") - engine.send_line("nps 1") - engine.send_line(f"st {int(limit.nodes)}") + self.engine.send_line("nps 1") + self.engine.send_line(f"st {max(1, int(limit.nodes))}") if limit.depth is not None: - engine.send_line(f"sd {limit.depth}") - if limit.time is not None: - engine.send_line(f"st {limit.time}") + self.engine.send_line(f"sd {max(1, int(limit.depth))}") if limit.white_clock is not None: - engine.send_line("{} {}".format("time" if board.turn else "otim", int(limit.white_clock * 100))) + 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: - engine.send_line("{} {}".format("otim" if board.turn else "time", int(limit.black_clock * 100))) + 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. - engine.send_line("post" if info else "nopost") - engine.send_line("hard" if ponder else "easy") - engine.send_line("go") - - def line_received(self, engine: XBoardProtocol, line: str) -> None: - if line.startswith("move "): - self._move(engine, line.split(" ", 1)[1]) - elif line.startswith("Hint: "): - self._hint(engine, line.split(" ", 1)[1]) - elif line == self.pong_after_move: - if not self.result.done(): - self.result.set_result(self.play_result) - if not ponder: + 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 line == self.pong_after_ponder: - if not self.result.done(): - self.result.set_result(self.play_result) - self.set_finished() - elif line == "offer draw": + elif f"{token} {remaining.strip()}" == "offer draw": if not self.result.done(): self.play_result.draw_offered = True - self._ping_after_move(engine) - elif line == "resign": + self._ping_after_move() + elif line.strip() == "resign": if not self.result.done(): self.play_result.resigned = True - self._ping_after_move(engine) - elif line.startswith("1-0") or line.startswith("0-1") or line.startswith("1/2-1/2"): - self._ping_after_move(engine) - elif line.startswith("#"): + 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): - engine.first_game = True # Board state might no longer be in sync + 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(engine, line) + self._post(line) else: - LOGGER.warning("%s: Unexpected engine output: %s", engine, line) + 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, engine: XBoardProtocol, line: str) -> None: + def _post(self, line: str) -> None: if not self.result.done(): - self.play_result.info = _parse_xboard_post(line, engine.board, info) + self.play_result.info = _parse_xboard_post(line, self.engine.board, info) - def _move(self, engine: XBoardProtocol, arg: str) -> None: + def _move(self, arg: str) -> None: if not self.result.done() and self.play_result.move is None: try: - self.play_result.move = engine.board.push_xboard(arg) + 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(engine) + self._ping_after_move() else: try: - engine.board.push_xboard(arg) + self.engine.board.push_xboard(arg) except ValueError: - LOGGER.exception("exception playing unexpected move") + LOGGER.exception("Exception playing unexpected move") - def _hint(self, engine: XBoardProtocol, arg: str) -> None: + 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 = engine.board.parse_xboard(arg) + self.play_result.ponder = self.engine.board.parse_xboard(arg) except ValueError: - LOGGER.exception("exception parsing hint") + LOGGER.exception("Exception parsing hint") else: - LOGGER.warning("unexpected hint: %r", arg) + LOGGER.warning("Unexpected hint: %r", arg) - def _ping_after_move(self, engine: XBoardProtocol) -> None: + def _ping_after_move(self) -> None: if self.pong_after_move is None: n = id(self) & 0xffff self.pong_after_move = f"pong {n}" - engine._ping(n) + self.engine._ping(n) - def cancel(self, engine: XBoardProtocol) -> None: + @override + def cancel(self) -> None: if self.stopped: return self.stopped = True if self.result.cancelled(): - engine.send_line("?") + self.engine.send_line("?") if ponder: - engine.send_line("easy") + self.engine.send_line("easy") n = (id(self) + 1) & 0xffff self.pong_after_ponder = f"pong {n}" - engine._ping(n) + self.engine._ping(n) - def engine_terminated(self, engine: XBoardProtocol, exc: Exception) -> None: + @override + def engine_terminated(self, exc: Exception) -> None: # Allow terminating engine while pondering. if not self.result.done(): - super().engine_terminated(engine, exc) + 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": + 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[XBoardProtocol, AnalysisResult]): - def start(self, engine: XBoardProtocol) -> None: + class XBoardAnalysisCommand(BaseCommand[AnalysisResult]): + def __init__(self, engine: XBoardProtocol): + super().__init__(engine) + self.engine = engine + + @override + def start(self) -> None: self.stopped = False - self.analysis = AnalysisResult(stop=lambda: self.cancel(engine)) + self.best_move: Optional[chess.Move] = None + self.analysis = AnalysisResult(stop=lambda: self.cancel()) self.final_pong: Optional[str] = None - engine._new(board, game, options) + self.engine._new(board, game, options) if root_moves is not None: - if not engine.features.get("exclude", 0): + if not self.engine.features.get("exclude", 0): raise EngineError("xboard engine does not support root_moves (feature exclude=0)") - engine.send_line("exclude all") + self.engine.send_line("exclude all") for move in root_moves: - engine.send_line(f"include {engine.board.xboard(move)}") + self.engine.send_line(f"include {self.engine.board.xboard(move)}") - engine.send_line("post") - engine.send_line("analyze") + 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] = engine.loop.call_later(limit.time, lambda: self.cancel(engine)) + self.time_limit_handle: Optional[asyncio.Handle] = self.engine.loop.call_later(limit.time, lambda: self.cancel()) else: self.time_limit_handle = None - def line_received(self, engine: XBoardProtocol, line: str) -> None: - if line.startswith("#"): + @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(engine, line) - elif line == self.final_pong: - self.end(engine) + self._post(line) + elif f"{token} {remaining.strip()}" == self.final_pong: + self.end() elif XBOARD_ERROR_REGEX.match(line): - engine.first_game = True # Board state might no longer be in sync + self.engine.first_game = True # Board state might no longer be in sync raise EngineError(line) else: - LOGGER.warning("%s: Unexpected engine output: %s", engine, line) + LOGGER.warning("%s: Unexpected engine output: %r", self.engine, line) - def _post(self, engine: XBoardProtocol, line: str) -> None: - post_info = _parse_xboard_post(line, engine.board, info | INFO_BASIC) + 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 typing.cast(float, post_info.get("time", 0)) >= limit.time: - self.cancel(engine) - elif limit.nodes is not None and typing.cast(int, post_info.get("nodes", 0)) >= limit.nodes: - self.cancel(engine) - elif limit.depth is not None and typing.cast(int, post_info.get("depth", 0)) >= limit.depth: - self.cancel(engine) + 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 typing.cast(PovScore, post_info["score"]).relative >= Mate(limit.mate): - self.cancel(engine) + if post_info["score"].relative >= Mate(limit.mate): + self.cancel() - def end(self, engine: XBoardProtocol) -> None: + def end(self) -> None: if self.time_limit_handle: self.time_limit_handle.cancel() self.set_finished() - self.analysis.set_finished() + self.analysis.set_finished(BestMove(self.best_move, None)) - def cancel(self, engine: XBoardProtocol) -> None: + @override + def cancel(self) -> None: if self.stopped: return self.stopped = True - engine.send_line(".") - engine.send_line("exit") + self.engine.send_line(".") + self.engine.send_line("exit") n = id(self) & 0xffff self.final_pong = f"pong {n}" - engine._ping(n) + self.engine._ping(n) - def engine_terminated(self, engine: XBoardProtocol, exc: Exception) -> None: - LOGGER.debug("%s: Closing analysis because engine has been terminated (error: %s)", engine, exc) + @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() @@ -1959,7 +2436,7 @@ def _setoption(self, name: str, value: ConfigValue) -> None: self.config[name] = value = option.parse(value) - if name in ["random", "computer"]: + if name in ["random", "computer", "name", "engine_rating", "opponent_rating"]: # Applied in _new. pass elif name in ["memory", "cores"] or name.startswith("egtpath "): @@ -1974,21 +2451,82 @@ def _setoption(self, name: str, value: ConfigValue) -> None: self.send_line(f"option {name}={value}") def _configure(self, options: ConfigMapping) -> None: - for name, value in collections.ChainMap(options, self.target_config).items(): + 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[XBoardProtocol, None]): - def start(self, engine: XBoardProtocol) -> None: - engine._configure(options) - engine.target_config.update({name: value for name, value in options.items() if value is not 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) @@ -2045,7 +2583,7 @@ def _parse_xboard_post(line: str, root_board: chess.Board, selector: Info = INFO pv_tokens.insert(0, token) break - if len(integer_tokens) < 4 or not selector: + if len(integer_tokens) < 4: return info # Required integer tokens. @@ -2079,9 +2617,6 @@ def _parse_xboard_post(line: str, root_board: chess.Board, selector: Info = INFO info["tbhits"] = integer_tokens.pop(0) # Principal variation. - if not (selector & INFO_PV): - return info - pv = [] board = root_board.copy(stack=False) for token in pv_tokens: @@ -2092,49 +2627,94 @@ def _parse_xboard_post(line: str, root_board: chess.Board, selector: Info = INFO 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``.""" + + ponder: Optional[chess.Move] + """The response that the engine expects after *move*, or ``None``.""" + + def __init__(self, move: Optional[chess.Move], ponder: Optional[chess.Move]): + self.move = move + self.ponder = ponder + + def __repr__(self) -> str: + return "<{} at {:#x} (move={}, ponder={}>".format( + type(self).__name__, id(self), self.move, self.ponder) + + class AnalysisResult: """ Handle to ongoing engine analysis. - Returned by :func:`chess.engine.EngineProtocol.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[None] = asyncio.Future() - self.multipv: List[InfoDict] = [{}] + 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 = typing.cast(int, info.get("multipv", 1)) + 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): + def _kork(self) -> None: if not self._posted_kork: self._posted_kork = True self._queue.put_nowait({}) - def set_finished(self) -> None: + def set_finished(self, best: BestMove) -> None: if not self._finished.done(): - self._finished.set_result(None) + self._finished.set_result(best) self._kork() def set_exception(self, exc: Exception) -> None: @@ -2143,6 +2723,10 @@ def set_exception(self, exc: Exception) -> None: @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: @@ -2151,9 +2735,9 @@ def stop(self) -> None: self._stop() self._stop = None - async def wait(self) -> None: - """Waits until the analysis is complete (or stopped).""" - await self._finished + async def wait(self) -> BestMove: + """Waits until the analysis is finished.""" + return await self._finished async def get(self) -> InfoDict: """ @@ -2179,15 +2763,26 @@ async def get(self) -> InfoDict: 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 information has been consumed. + 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. - - If the queue is not empty, then the next call to - :func:`~chess.engine.AnalysisResult.get()` will return instantly. """ return self._seen_kork or self._queue.qsize() <= self._posted_kork @@ -2197,7 +2792,7 @@ async def next(self) -> Optional[InfoDict]: except AnalysisComplete: return None - def __aiter__(self) -> "AnalysisResult": + def __aiter__(self) -> AnalysisResult: return self async def __anext__(self) -> InfoDict: @@ -2206,7 +2801,7 @@ async def __anext__(self) -> InfoDict: except AnalysisComplete: raise StopAsyncIteration - def __enter__(self) -> "AnalysisResult": + def __enter__(self) -> AnalysisResult: return self def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]) -> None: @@ -2215,7 +2810,7 @@ def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[ async def popen_uci(command: Union[str, List[str]], *, setpgrp: bool = False, **popen_args: Any) -> Tuple[asyncio.SubprocessTransport, UciProtocol]: """ - Spawns and initializes an UCI engine. + Spawns and initializes a UCI engine. :param command: Path of the engine executable, or a list including the path and arguments. @@ -2263,14 +2858,18 @@ async def popen_xboard(command: Union[str, List[str]], *, setpgrp: bool = False, return transport, protocol +async def _async(sync: Callable[[], T]) -> T: + return sync() + + class SimpleEngine: """ Synchronous wrapper around a transport and engine protocol pair. Provides - the same methods and attributes as :class:`~chess.engine.EngineProtocol`, + 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 + 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. @@ -2280,7 +2879,7 @@ class SimpleEngine: Automatically closes the transport when used as a context manager. """ - def __init__(self, transport: asyncio.SubprocessTransport, protocol: EngineProtocol, *, timeout: Optional[float] = 10.0) -> None: + def __init__(self, transport: asyncio.SubprocessTransport, protocol: Protocol, *, timeout: Optional[float] = 10.0) -> None: self.transport = transport self.protocol = protocol self.timeout = timeout @@ -2305,23 +2904,19 @@ def _not_shut_down(self) -> Generator[None, None, None]: @property def options(self) -> MutableMapping[str, Option]: - async def _get() -> MutableMapping[str, Option]: - return copy.copy(self.protocol.options) - with self._not_shut_down(): - future = asyncio.run_coroutine_threadsafe(_get(), self.protocol.loop) + 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]: - async def _get() -> Mapping[str, str]: - return self.protocol.id.copy() - with self._not_shut_down(): - future = asyncio.run_coroutine_threadsafe(_get(), self.protocol.loop) + 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[[], BaseCommand[EngineProtocol, T]]) -> T: + 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) @@ -2333,16 +2928,24 @@ def configure(self, options: ConfigMapping) -> None: 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, root_moves: Optional[Iterable[chess.Move]] = None, options: ConfigMapping = {}) -> PlayResult: + 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, root_moves=root_moves, options=options), + 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() @@ -2361,14 +2964,20 @@ def analyse(self, board: chess.Board, limit: Limit, *, multipv: Optional[int] = 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": + 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) # Analyis should start immediately + 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) @@ -2389,8 +2998,8 @@ def _shutdown() -> None: self.protocol.loop.call_soon_threadsafe(_shutdown) @classmethod - def popen(cls, Protocol: Type[EngineProtocol], command: Union[str, List[str]], *, timeout: Optional[float] = 10.0, debug: bool = False, setpgrp: bool = False, **popen_args: Any) -> "SimpleEngine": - async def background(future: "concurrent.futures.Future[SimpleEngine]") -> None: + 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) @@ -2406,29 +3015,29 @@ async def background(future: "concurrent.futures.Future[SimpleEngine]") -> None: 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: bool = False, setpgrp: bool = False, **popen_args: Any) -> "SimpleEngine": + 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 an UCI engine. + 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: bool = False, setpgrp: bool = False, **popen_args: Any) -> "SimpleEngine": + 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": + 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. + pid = self.transport.get_pid() # This happens to be thread-safe return f"<{type(self).__name__} (pid={pid})>" @@ -2444,37 +3053,35 @@ def __init__(self, simple_engine: SimpleEngine, inner: AnalysisResult) -> None: @property def info(self) -> InfoDict: - async def _get() -> InfoDict: - return self.inner.info.copy() - with self.simple_engine._not_shut_down(): - future = asyncio.run_coroutine_threadsafe(_get(), self.simple_engine.protocol.loop) + 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]: - async def _get() -> List[InfoDict]: - return [info.copy() for info in self.inner.multipv] - with self.simple_engine._not_shut_down(): - future = asyncio.run_coroutine_threadsafe(_get(), self.simple_engine.protocol.loop) + 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) -> None: + 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 empty(self) -> bool: - async def _empty() -> bool: - return self.inner.empty() + 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(_empty(), self.simple_engine.protocol.loop) + future = asyncio.run_coroutine_threadsafe(_async(self.inner.empty), self.simple_engine.protocol.loop) return future.result() def get(self) -> InfoDict: @@ -2500,7 +3107,7 @@ def __next__(self) -> InfoDict: except StopAsyncIteration: raise StopIteration - def __enter__(self) -> "SimpleAnalysisResult": + def __enter__(self) -> SimpleAnalysisResult: return self def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]) -> None: diff --git a/chess/gaviota.py b/chess/gaviota.py index cd5f22cf5..7152a18f0 100644 --- a/chess/gaviota.py +++ b/chess/gaviota.py @@ -1,22 +1,8 @@ -# This file is part of the python-chess library. -# Copyright (C) 2015 Jean-Noël Avila -# Copyright (C) 2015-2020 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 @@ -99,7 +85,7 @@ def map24_b(s: int) -> int: - s = s - 8 + s -= 8 return ((s & 3) + s) >> 1 def map88(x: int) -> int: @@ -124,12 +110,12 @@ def idx_is_empty(x: int) -> int: 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 @@ -158,10 +144,9 @@ def init_flipt() -> List[List[int]]: def init_pp48_idx() -> Tuple[List[List[int]], List[int], List[int]]: - MAX_I = 48 - MAX_J = 48 + 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,10 +169,8 @@ def init_pp48_idx() -> Tuple[List[List[int]], List[int], List[int]]: def init_ppp48_idx() -> Tuple[List[List[List[int]]], List[int], List[int], List[int]]: - 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)] + 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 @@ -216,7 +199,7 @@ def init_ppp48_idx() -> Tuple[List[List[List[int]]], List[int], List[int], List[ 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 @@ -224,7 +207,7 @@ def init_ppp48_idx() -> Tuple[List[List[List[int]]], List[int], List[int], List[ def init_aaidx() -> Tuple[List[int], List[List[int]]]: - aaidx = [[-1] * 64 for y in range(64)] + aaidx = [[-1] * 64 for _ in range(64)] aabase = [0] * MAX_AAINDEX idx = 0 @@ -250,11 +233,11 @@ def init_aaa() -> Tuple[List[int], List[List[int]]]: 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): @@ -318,7 +301,7 @@ 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 @@ -327,13 +310,13 @@ 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() -> Tuple[List[List[int]], List[int], List[int]]: - ppidx = [[-1] * 48 for i in range(24)] + ppidx = [[-1] * 48 for _ in range(24)] pp_hi24 = [-1] * MAX_PPINDEX pp_lo48 = [-1] * MAX_PPINDEX @@ -349,7 +332,7 @@ def init_ppidx() -> Tuple[List[List[int]], List[int], List[int]]: 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,11 +351,11 @@ def init_ppidx() -> Tuple[List[List[int]], List[int], List[int]]: def norm_kkindex(x: chess.Square, y: chess.Square) -> Tuple[int, int]: - if chess.square_file(x) > 3: + 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) @@ -393,7 +376,7 @@ def norm_kkindex(x: chess.Square, y: chess.Square) -> Tuple[int, int]: return x, y def init_kkidx() -> Tuple[List[List[int]], List[int], List[int]]: - kkidx = [[-1] * 64 for x in range(64)] + kkidx = [[-1] * 64 for _ in range(64)] bksq = [-1] * MAX_KKINDEX wksq = [-1] * MAX_KKINDEX idx = 0 @@ -416,7 +399,7 @@ def init_kkidx() -> Tuple[List[List[int]], List[int], List[int]]: KKIDX, WKSQ, BKSQ = init_kkidx() -def kxk_pctoindex(c: "Request") -> int: +def kxk_pctoindex(c: Request) -> int: BLOCK_Ax = 64 ft = flip_type(c.black_piece_squares[0], c.white_piece_squares[0]) @@ -443,7 +426,7 @@ def kxk_pctoindex(c: "Request") -> int: return ki * BLOCK_Ax + ws[1] -def kapkb_pctoindex(c: "Request") -> int: +def kapkb_pctoindex(c: Request) -> int: BLOCK_A = 64 * 64 * 64 * 64 BLOCK_B = 64 * 64 * 64 BLOCK_C = 64 * 64 @@ -459,7 +442,7 @@ def kapkb_pctoindex(c: "Request") -> int: 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) @@ -468,12 +451,12 @@ def kapkb_pctoindex(c: "Request") -> int: 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: "Request") -> int: +def kabpk_pctoindex(c: Request) -> int: BLOCK_A = 64 * 64 * 64 * 64 BLOCK_B = 64 * 64 * 64 BLOCK_C = 64 * 64 @@ -486,7 +469,7 @@ def kabpk_pctoindex(c: "Request") -> int: 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) @@ -497,7 +480,7 @@ def kabpk_pctoindex(c: "Request") -> int: return pslice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + wa * BLOCK_D + wb -def kabkp_pctoindex(c: "Request") -> int: +def kabkp_pctoindex(c: Request) -> int: BLOCK_A = 64 * 64 * 64 * 64 BLOCK_B = 64 * 64 * 64 BLOCK_C = 64 * 64 @@ -513,7 +496,7 @@ def kabkp_pctoindex(c: "Request") -> int: 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) @@ -521,12 +504,12 @@ def kabkp_pctoindex(c: "Request") -> int: 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: "Request") -> int: +def kaapk_pctoindex(c: Request) -> int: BLOCK_C = MAX_AAINDEX BLOCK_B = 64 * BLOCK_C BLOCK_A = 64 * BLOCK_B @@ -538,7 +521,7 @@ def kaapk_pctoindex(c: "Request") -> int: 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) @@ -554,7 +537,7 @@ def kaapk_pctoindex(c: "Request") -> int: return pslice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + aa_combo -def kaakp_pctoindex(c: "Request") -> int: +def kaakp_pctoindex(c: Request) -> int: BLOCK_C = MAX_AAINDEX BLOCK_B = 64 * BLOCK_C BLOCK_A = 64 * BLOCK_B @@ -566,7 +549,7 @@ def kaakp_pctoindex(c: "Request") -> int: 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) @@ -583,7 +566,7 @@ def kaakp_pctoindex(c: "Request") -> int: return pslice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + aa_combo -def kapkp_pctoindex(c: "Request") -> int: +def kapkp_pctoindex(c: Request) -> int: BLOCK_A = 64 * 64 * 64 BLOCK_B = 64 * 64 BLOCK_C = 64 @@ -598,7 +581,7 @@ def kapkp_pctoindex(c: "Request") -> int: 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) @@ -614,7 +597,7 @@ def kapkp_pctoindex(c: "Request") -> int: return pp_slice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + wa -def kappk_pctoindex(c: "Request") -> int: +def kappk_pctoindex(c: Request) -> int: BLOCK_A = 64 * 64 * 64 BLOCK_B = 64 * 64 BLOCK_C = 64 @@ -628,7 +611,7 @@ def kappk_pctoindex(c: "Request") -> int: 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) @@ -645,7 +628,7 @@ def kappk_pctoindex(c: "Request") -> int: return pp_slice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + wa -def kppka_pctoindex(c: "Request") -> int: +def kppka_pctoindex(c: Request) -> int: BLOCK_A = 64 * 64 * 64 BLOCK_B = 64 * 64 BLOCK_C = 64 @@ -675,7 +658,7 @@ def kppka_pctoindex(c: "Request") -> int: return pp_slice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + ba -def kabck_pctoindex(c: "Request") -> int: +def kabck_pctoindex(c: Request) -> int: N_WHITE = 4 N_BLACK = 1 BLOCK_A = 64 * 64 * 64 @@ -706,7 +689,7 @@ def kabck_pctoindex(c: "Request") -> int: return ki * BLOCK_A + ws[1] * BLOCK_B + ws[2] * BLOCK_C + ws[3] -def kabbk_pctoindex(c: "Request") -> int: +def kabbk_pctoindex(c: Request) -> int: N_WHITE = 4 N_BLACK = 1 BLOCK_Bx = 64 @@ -737,7 +720,7 @@ def kabbk_pctoindex(c: "Request") -> int: return ki * BLOCK_Ax + ai * BLOCK_Bx + ws[1] -def kaabk_pctoindex(c: "Request") -> int: +def kaabk_pctoindex(c: Request) -> int: N_WHITE = 4 N_BLACK = 1 BLOCK_Bx = 64 @@ -773,7 +756,7 @@ def aaa_getsubi(x: int, y: int, z: int) -> int: calc_idx = x + (y - 1) * y // 2 + bse return calc_idx -def kaaak_pctoindex(c: "Request") -> int: +def kaaak_pctoindex(c: Request) -> int: N_WHITE = 4 N_BLACK = 1 BLOCK_Ax = MAX_AAAINDEX @@ -820,7 +803,7 @@ def kaaak_pctoindex(c: "Request") -> int: return ki * BLOCK_Ax + ai -def kppkp_pctoindex(c: "Request") -> int: +def kppkp_pctoindex(c: Request) -> int: BLOCK_Ax = MAX_PP48_INDEX * 64 * 64 BLOCK_Bx = 64 * 64 BLOCK_Cx = 64 @@ -841,7 +824,7 @@ def kppkp_pctoindex(c: "Request") -> int: 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] @@ -851,7 +834,7 @@ def kppkp_pctoindex(c: "Request") -> int: return k * BLOCK_Ax + pp48_slice * BLOCK_Bx + wk * BLOCK_Cx + bk -def kaakb_pctoindex(c: "Request") -> int: +def kaakb_pctoindex(c: Request) -> int: N_WHITE = 3 N_BLACK = 2 BLOCK_Bx = 64 @@ -882,7 +865,7 @@ def kaakb_pctoindex(c: "Request") -> int: return ki * BLOCK_Ax + ai * BLOCK_Bx + bs[1] -def kabkc_pctoindex(c: "Request") -> int: +def kabkc_pctoindex(c: Request) -> int: N_WHITE = 3 N_BLACK = 2 @@ -914,7 +897,7 @@ def kabkc_pctoindex(c: "Request") -> int: return ki * BLOCK_Ax + ws[1] * BLOCK_Bx + ws[2] * BLOCK_Cx + bs[1] -def kpkp_pctoindex(c: "Request") -> int: +def kpkp_pctoindex(c: Request) -> int: BLOCK_Ax = 64 * 64 BLOCK_Bx = 64 @@ -942,7 +925,7 @@ def kpkp_pctoindex(c: "Request") -> int: return pp_slice * BLOCK_Ax + wk * BLOCK_Bx + bk -def kppk_pctoindex(c: "Request") -> int: +def kppk_pctoindex(c: Request) -> int: BLOCK_Ax = 64 * 64 BLOCK_Bx = 64 wk = c.white_piece_squares[0] @@ -968,7 +951,7 @@ def kppk_pctoindex(c: "Request") -> int: return pp_slice * BLOCK_Ax + wk * BLOCK_Bx + bk -def kapk_pctoindex(c: "Request") -> int: +def kapk_pctoindex(c: Request) -> int: BLOCK_Ax = 64 * 64 * 64 BLOCK_Bx = 64 * 64 BLOCK_Cx = 64 @@ -989,12 +972,12 @@ def kapk_pctoindex(c: "Request") -> int: 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: "Request") -> int: +def kabk_pctoindex(c: Request) -> int: BLOCK_Ax = 64 * 64 BLOCK_Bx = 64 @@ -1022,7 +1005,7 @@ def kabk_pctoindex(c: "Request") -> int: return ki * BLOCK_Ax + ws[1] * BLOCK_Bx + ws[2] -def kakp_pctoindex(c: "Request") -> int: +def kakp_pctoindex(c: Request) -> int: BLOCK_Ax = 64 * 64 * 64 BLOCK_Bx = 64 * 64 BLOCK_Cx = 64 @@ -1042,12 +1025,12 @@ def kakp_pctoindex(c: "Request") -> int: 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: "Request") -> int: +def kaak_pctoindex(c: Request) -> int: N_WHITE = 3 N_BLACK = 1 BLOCK_Ax = MAX_AAINDEX @@ -1077,7 +1060,7 @@ def kaak_pctoindex(c: "Request") -> int: return ki * BLOCK_Ax + ai -def kakb_pctoindex(c: "Request") -> int: +def kakb_pctoindex(c: Request) -> int: BLOCK_Ax = 64 * 64 BLOCK_Bx = 64 @@ -1111,7 +1094,7 @@ def kakb_pctoindex(c: "Request") -> int: return ki * BLOCK_Ax + ws[1] * BLOCK_Bx + bs[1] -def kpk_pctoindex(c: "Request") -> int: +def kpk_pctoindex(c: Request) -> int: BLOCK_A = 64 * 64 BLOCK_B = 64 @@ -1129,13 +1112,13 @@ def kpk_pctoindex(c: "Request") -> int: 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: "Request") -> int: +def kpppk_pctoindex(c: Request) -> int: BLOCK_A = 64 * 64 BLOCK_B = 64 @@ -1171,178 +1154,178 @@ def kpppk_pctoindex(c: "Request") -> int: return ppp48_slice * BLOCK_A + wk * BLOCK_B + bk -class Endgamekey: - def __init__(self, maxindex: int, slice_n: int, pctoi: Callable[["Request"], int]) -> None: +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), } @@ -1373,51 +1356,9 @@ def split_index(i: int) -> Tuple[int, int]: iBMATEt = tb_BMATE | 4 -def removepiece(ys: List[int], yp: List[int], j: int) -> None: - del ys[j] - del yp[j] - def opp(side: int) -> int: return 1 if side == 0 else 0 -def adjust_up(dist: int) -> int: - udist = dist - sw = udist & INFOMASK - - if sw in [iWMATE, iWMATEt, iBMATE, iBMATEt]: - udist += (1 << PLYSHIFT) - - return udist - -def bestx(side: int, a: int, b: int) -> int: - # 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: int) -> Tuple[int, int]: return d >> PLYSHIFT, d & INFOMASK @@ -1492,35 +1433,35 @@ class MissingTableError(KeyError): 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: Optional[List[int]] = None class Request: - def __init__(self, white_squares: List[int], white_types: List[chess.PieceType], black_squares: List[int], black_types: List[chess.PieceType], side: int, epsq: int): + 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: str = None - self.white_piece_squares: List[int] = None - self.white_piece_types: List[int] = None - self.black_piece_squares: List[int] = None - self.black_piece_types: List[int] = None - self.is_reversed: bool = None - -class Zipinfo: - def __init__(self, extraoffset: int, totalblocks: int, blockindex: List[int]) -> None: - self.extraoffset = extraoffset - self.totalblocks = totalblocks - self.blockindex = blockindex +@dataclasses.dataclass +class ZipInfo: + extraoffset: int + totalblocks: int + blockindex: List[int] class PythonTablebase: @@ -1530,7 +1471,7 @@ def __init__(self) -> None: self.available_tables: Dict[str, str] = {} self.streams: Dict[str, BinaryIO] = {} - self.zipinfo: Dict[str, Zipinfo] = {} + self.zipinfo: Dict[str, ZipInfo] = {} self.block_cache: Dict[Tuple[str, int, int], TableBlock] = {} self.block_age = 0 @@ -1552,10 +1493,10 @@ 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. + (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: >>> import chess >>> import chess.gaviota @@ -1578,24 +1519,44 @@ def probe_dtm(self, board: chess.Board) -> int: raise KeyError(f"gaviota tables do not contain positions with castling rights: {board.fen()}") # Supports only up to 5 pieces. - if chess.popcount(board.occupied) > 5: - raise KeyError(f"gaviota tables support up to 5 pieces, not {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()}") # KvK is a draw. if board.occupied == board.kings: return 0 + # 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 - epsq = board.ep_square if board.ep_square else NOSQUARE - req = Request(white_squares, white_types, black_squares, black_types, side, epsq) + 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 == iWMATE: @@ -1634,7 +1595,7 @@ def get_dtm(self, board: chess.Board, default: Optional[int] = None) -> Optional 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. @@ -1691,10 +1652,7 @@ def _setup_tablebase(self, req: Request) -> BinaryIO: 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(f"no gaviota table available for: {white_letters.upper()}v{black_letters.upper()}") @@ -1705,7 +1663,7 @@ def _open_tablebase(self, req: Request) -> BinaryIO: 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 @@ -1724,77 +1682,6 @@ def close(self) -> None: _, stream = self.streams.popitem() stream.close() - def egtb_get_dtm(self, req: Request) -> int: - 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 - - # Copy position. - xs_after = xs[:] - ys_after = ys[:] - xp_after = xp[:] - yp_after = yp[:] - - # Execute capture. - xs_after[i] = req.epsq - removepiece(ys_after, yp_after, j) - - # Flip back. - if req.side == 1: - xs_after, ys_after = ys_after, xs_after - xp_after, yp_after = yp_after, xp_after - - # Make subrequest. - subreq = Request(xs_after, xp_after, ys_after, yp_after, 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: Request, idx: int) -> int: maxindex = EGKEY[req.egkey].maxindex @@ -1810,9 +1697,9 @@ def egtb_block_getsize(self, req: Request, idx: int) -> int: 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: Request) -> int: stream = self._setup_tablebase(req) @@ -1829,13 +1716,13 @@ def _tb_probe(self, req: Request) -> int: 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 @@ -1857,12 +1744,7 @@ def _tb_probe(self, req: Request) -> int: # 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 @@ -1872,7 +1754,7 @@ def _tb_probe(self, req: Request) -> int: return dtm - def egtb_loadindexes(self, egkey: str, stream: BinaryIO) -> Zipinfo: + def egtb_loadindexes(self, egkey: str, stream: BinaryIO) -> ZipInfo: zipinfo = self.zipinfo.get(egkey) if zipinfo is None: @@ -1888,7 +1770,7 @@ def egtb_loadindexes(self, egkey: str, stream: BinaryIO) -> Zipinfo: IndexStruct = struct.Struct("<" + "I" * n_idx) 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 @@ -1904,7 +1786,7 @@ def egtb_block_park(self, egkey: str, block: int, stream: BinaryIO) -> int: stream.seek(i) return i - def __enter__(self) -> "PythonTablebase": + def __enter__(self) -> PythonTablebase: return self def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]) -> None: @@ -1917,7 +1799,7 @@ class NativeTablebase: Has the same interface as :class:`~chess.gaviota.PythonTablebase`. """ - def __init__(self, libgtb) -> None: + def __init__(self, libgtb: ctypes.CDLL) -> None: self.paths: List[str] = [] self.libgtb = libgtb @@ -1963,17 +1845,17 @@ def _tb_restart(self) -> None: av = self.libgtb.tb_availability() if av & 1: - LOGGER.debug("Some 3 piece tables available") + LOGGER.debug("Some 3-piece tables available") if av & 2: - LOGGER.debug("All 3 piece tables complete") + LOGGER.debug("All 3-piece tables complete") if av & 4: - LOGGER.debug("Some 4 piece tables available") + LOGGER.debug("Some 4-piece tables available") if av & 8: - LOGGER.debug("All 4 piece tables complete") + LOGGER.debug("All 4-piece tables complete") if av & 16: - LOGGER.debug("Some 5 piece tables available") + LOGGER.debug("Some 5-piece tables available") if av & 32: - LOGGER.debug("All 5 piece tables complete") + LOGGER.debug("All 5-piece tables complete") 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)) @@ -2003,8 +1885,8 @@ def _probe_hard(self, board: chess.Board, wdl_only: bool = False) -> int: if board.castling_rights: raise KeyError(f"gaviota tables do not contain positions with castling rights: {board.fen()}") - if chess.popcount(board.occupied) > 5: - raise KeyError(f"gaviota tables support up to 5 pieces, not {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) @@ -2067,14 +1949,14 @@ def close(self) -> None: self.libgtb.tbcache_done() self.libgtb.tb_done() - def __enter__(self) -> "NativeTablebase": + def __enter__(self) -> NativeTablebase: return self def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]) -> None: self.close() -def open_tablebase_native(directory: str, *, libgtb=None, LibraryLoader=ctypes.cdll) -> NativeTablebase: +def open_tablebase_native(directory: str, *, libgtb: Optional[str] = None, LibraryLoader: ctypes.LibraryLoader[ctypes.CDLL] = ctypes.cdll) -> NativeTablebase: """ Opens a collection of tables for probing using libgtb. @@ -2090,7 +1972,7 @@ def open_tablebase_native(directory: str, *, libgtb=None, LibraryLoader=ctypes.c return tables -def open_tablebase(directory: str, *, libgtb=None, LibraryLoader=ctypes.cdll) -> Union[NativeTablebase, PythonTablebase]: +def open_tablebase(directory: str, *, libgtb: Optional[str] = None, LibraryLoader: ctypes.LibraryLoader[ctypes.CDLL] = ctypes.cdll) -> Union[NativeTablebase, PythonTablebase]: """ Opens a collection of tables for probing. diff --git a/chess/pgn.py b/chess/pgn.py index fe140aa64..5ae5b43b0 100644 --- a/chess/pgn.py +++ b/chess/pgn.py @@ -1,30 +1,26 @@ -# This file is part of the python-chess library. -# Copyright (C) 2012-2020 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 dataclasses import enum import itertools import logging import re -import weakref import typing import chess +import chess.engine +import chess.svg -from typing import Callable, Dict, Generic, Iterable, Iterator, List, Mapping, MutableMapping, Set, TextIO, Tuple, Type, TypeVar, Optional, Union +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__) @@ -82,9 +78,9 @@ NAG_NOVELTY = 146 -TAG_REGEX = re.compile(r"^\[([A-Za-z0-9_]+)\s+\"([^\r]*)\"\]\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_]+\Z") +TAG_NAME_REGEX = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_+#=:-]*\Z") MOVETEXT_REGEX = re.compile(r""" ( @@ -92,6 +88,8 @@ |[PNBRQK]?@[a-h][1-8] |-- |Z0 + |0000 + |@@@@ |O-O(?:-O)? |0-0(?:-0)? ) @@ -106,6 +104,43 @@ 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 + + +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"] @@ -118,82 +153,140 @@ class SkipType(enum.Enum): ResultT = TypeVar("ResultT", covariant=True) -class GameNode: - def __init__(self) -> None: - self.parent: Optional[GameNode] = None - self.variations: List[GameNode] = [] +class TimeControlType(enum.Enum): + UNKNOWN = 0 + UNLIMITED = 1 + STANDARD = 2 + RAPID = 3 + BLITZ = 4 + BULLET = 5 - self.move: Optional[chess.Move] = None - self.nags: Set[int] = set() - self.starting_comment = "" - self.comment = "" - self.board_cached: Optional[weakref.ref[chess.Board]] = None +@dataclasses.dataclass +class TimeControlPart: + moves: int = 0 + time: int = 0 + increment: float = 0 + delay: float = 0 - @classmethod - def dangling_node(cls) -> "GameNode": - return GameNode() - def board(self, *, _cache: bool = True) -> chess.Board: - """ - Gets a board with the position of the node. +@dataclasses.dataclass +class TimeControl: + """ + PGN TimeControl Parser + Spec: http://www.saremba.de/chessgml/standards/pgn/pgn-complete.htm#c9.6 - It's a copy, so modifying the board will not alter the game. + 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() + + @property + @abc.abstractmethod + def parent(self) -> Optional[GameNode]: + """The parent node or ``None`` if this is the root node of the game.""" + + @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. """ - assert self.parent is not None and self.move is not None, "cannot get board of dangling GameNode" - if self.board_cached is not None: - board = self.board_cached() - if board is not None: - return board.copy() + @abc.abstractmethod + def board(self) -> chess.Board: + """ + 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 = weakref.ref(board) - return board.copy() - else: - return board + It's a copy, so modifying the board will not alter the game. - def _move(self) -> chess.Move: - assert self.move is not None, "cannot get move of dangling GameNode" - return self.move + Complexity is `O(n)`. + """ - def san(self) -> str: + @abc.abstractmethod + def ply(self) -> int: """ - Gets the standard algebraic notation of the move leading to this node. - See :func:`chess.Board.san()`. + 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()`. - Do not call this on the root node. + 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)`. """ - assert self.parent is not None and self.move is not None, "cannot get san of dangling GameNode" - return self.parent.board().san(self.move) - def uci(self, *, chess960: Optional[bool] = None) -> str: + def turn(self) -> Color: """ - Gets the UCI notation of the move leading to this node. - See :func:`chess.Board.uci()`. + Gets the color to move at this node. See :data:`chess.Board.turn`. - Do not call this on the root node. + Complexity is `O(n)`. """ - assert self.parent is not None and self.move is not None, "cannot get uci of dangling GameNode" - return self.parent.board().uci(self.move, chess960=chess960) + return self.ply() % 2 == 0 - def root(self) -> "GameNode": + def root(self) -> GameNode: node = self while node.parent: node = node.parent return node - def game(self) -> "Game": - """Gets the root node, i.e., the game.""" + 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.""" + 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: @@ -202,7 +295,11 @@ def end(self) -> "GameNode": return node def is_end(self) -> bool: - """Checks if this node is the last node in the current variation.""" + """ + Checks if this node is the last node in the current variation. + + Complexity is `O(1)`. + """ return not self.variations def starts_variation(self) -> bool: @@ -213,6 +310,8 @@ def starts_variation(self) -> bool: 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 @@ -220,7 +319,11 @@ def starts_variation(self) -> bool: return self.parent.variations[0] != self def is_mainline(self) -> bool: - """Checks if the node is in the mainline of the game.""" + """ + Checks if the node is in the mainline of the game. + + Complexity is `O(n)`. + """ node = self while node.parent: @@ -237,13 +340,15 @@ 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 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 __getitem__(self, move: Union[int, chess.Move]) -> "GameNode": + def __getitem__(self, move: Union[int, chess.Move, GameNode]) -> ChildNode: try: return self.variations[move] # type: ignore except TypeError: @@ -253,71 +358,80 @@ def __getitem__(self, move: Union[int, chess.Move]) -> "GameNode": raise KeyError(move) - def variation(self, move: Union[int, chess.Move]) -> "GameNode": + 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 either the move or the variation index. """ return self[move] - def has_variation(self, move: chess.Move) -> bool: - """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: chess.Move) -> None: + 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: chess.Move) -> None: + 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: chess.Move) -> None: + 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: chess.Move) -> None: + 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: chess.Move, *, comment: str = "", starting_comment: str = "", nags: Iterable[int] = ()) -> "GameNode": + 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 = type(self).dangling_node() - node.move = move - node.nags = set(nags) - node.comment = comment - node.starting_comment = starting_comment - - node.parent = self - 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: chess.Move, *, comment: str = "") -> "GameNode": + 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 mainline(self) -> "Mainline[GameNode]": - """Returns an iterator over the mainline starting after this node.""" + 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 iterator over the main moves after this node.""" - return Mainline(self, lambda node: node._move()) + 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: Iterable[chess.Move], *, comment: str = "", starting_comment: str = "", nags: Iterable[int] = ()) -> "GameNode": + 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. @@ -330,62 +444,186 @@ def add_line(self, moves: Iterable[chess.Move], *, comment: str = "", starting_c 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_node(self, parent_board: chess.Board, visitor: "BaseVisitor[ResultT]") -> None: - assert self.move is not None, "cannot visit dangling GameNode" + def eval(self) -> Optional[chess.engine.PovScore]: + """ + Parses the first valid ``[%eval ...]`` annotation in the comment of + this node, if any. - if self.starting_comment: - visitor.visit_comment(self.starting_comment) + Complexity is `O(n)`. + """ + match = EVAL_REGEX.search(" ".join(self.comments)) + if not match: + return None - visitor.visit_move(parent_board, self.move) + turn = self.turn() - parent_board.push(self.move) - visitor.visit_board(parent_board) - parent_board.pop() + 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)) - for nag in sorted(self.nags): - visitor.visit_nag(nag) + 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 - if self.comment: - visitor.visit_comment(self.comment) + 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. - def _accept(self, parent_board: chess.Board, visitor: "BaseVisitor[ResultT]", *, sidelines: bool = True) -> None: - assert self.parent is not None and self.move is not None, "cannot visit dangling GameNode" + 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)) - # First, visit the move that leads to this node. - self._accept_node(parent_board, visitor) + return arrows - # Then visit sidelines. - if sidelines and self == self.parent.variations[0]: - for variation in itertools.islice(self.parent.variations, 1, None): - if visitor.begin_variation() is not SKIP: - variation._accept(parent_board, visitor) - visitor.end_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] = [] - # The mainline is continued last. - if self.variations: - parent_board.push(self.move) - self.variations[0]._accept(parent_board, visitor) - parent_board.pop() + 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 + + for index in range(len(self.comments)): + self.comments[index] = ARROWS_REGEX.sub(_condense_affix(""), self.comments[index]) + + self.comments = list(filter(None, self.comments)) + + prefix = "" + if csl: + prefix += f"[%csl {','.join(csl)}]" + if cal: + prefix += f"[%cal {','.join(cal)}]" + + if prefix: + self.comments.insert(0, prefix) + + def clock(self) -> Optional[float]: + """ + Parses the first valid ``[%clk ...]`` annotation in the comment of + this node, if any. + + 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 accept(self, visitor: "BaseVisitor[ResultT]") -> ResultT: + 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. """ - assert self.parent is not None, "cannot visit dangling GameNode" - self._accept(self.parent.board(), visitor, sidelines=False) - return visitor.result() - def accept_subgame(self, visitor: "BaseVisitor[ResultT]") -> ResultT: + 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. @@ -419,10 +657,159 @@ def accept_subgame(self, visitor: "BaseVisitor[ResultT]") -> ResultT: def __str__(self) -> str: return self.accept(StringExporter(columns=None)) - def __repr__(self) -> str: - if self.parent is None: - return f"<{type(self).__name__} at {id(self):#x} (dangling: {self.move})>" +class ChildNode(GameNode): + """ + A child node of a game, with the move leading to it. + Extends :class:`~chess.pgn.GameNode`. + """ + + 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. + """ + + 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, 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 + + board = node.game().board() + + while stack: + board.push(stack.pop()) + + return board + + @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 standard algebraic notation of the move leading to this node. + See :func:`chess.Board.san()`. + + Do not call this on the root node. + + Complexity is `O(n)`. + """ + return self.parent.board().san(self.move) + + 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: @@ -433,7 +820,7 @@ def __repr__(self) -> str: id(self), parent_board.fullmove_number, "." if parent_board.turn == chess.WHITE else "...", - self.san()) + parent_board.san(self.move)) GameT = TypeVar("GameT", bound="Game") @@ -441,24 +828,51 @@ def __repr__(self) -> str: class Game(GameNode): """ The root node of a game with extra information such as headers and the - starting position. Also has all the other properties and methods of - :class:`~chess.pgn.GameNode`. + 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: List[Exception] = [] + self.errors = [] - def board(self, *, _cache: bool = False) -> chess.Board: - """ - Gets the starting position of the game. + @property + @override + def parent(self) -> None: + return None - Unless the ``FEN`` header tag is set, this is the default starting - position (for the ``Variant``). - """ + @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: """ Sets up a specific starting position. This sets (or resets) the @@ -473,11 +887,11 @@ def setup(self, board: Union[chess.Board, str]) -> None: fen = setup.fen() if fen == type(setup).starting_fen: - self.headers.pop("SetUp", None) 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(setup).aliases[0] == "Standard" and setup.chess960: self.headers["Variant"] = "Chess960" @@ -487,7 +901,8 @@ def setup(self, board: Union[chess.Board, str]) -> None: else: self.headers.pop("Variant", None) - def accept(self, visitor: "BaseVisitor[ResultT]") -> ResultT: + @override + def accept(self, visitor: BaseVisitor[ResultT]) -> ResultT: """ Traverses the game in PGN order using the given *visitor*. Returns the *visitor* result. @@ -499,8 +914,8 @@ def accept(self, visitor: "BaseVisitor[ResultT]") -> ResultT: board = self.board() visitor.visit_board(board) - if self.comment: - visitor.visit_comment(self.comment) + if self.comments: + visitor.visit_comment(self.comments) if self.variations: self.variations[0]._accept(board, visitor) @@ -510,6 +925,14 @@ def accept(self, visitor: "BaseVisitor[ResultT]") -> ResultT: 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: Type[GameT], board: chess.Board) -> GameT: """Creates a game from the move stack of a :class:`~chess.Board()`.""" @@ -527,20 +950,21 @@ def from_board(cls: Type[GameT], board: chess.Board) -> GameT: @classmethod def without_tag_roster(cls: Type[GameT]) -> GameT: - """Creates an empty game without the default 7 tag roster.""" + """Creates an empty game without the default Seven Tag Roster.""" return cls(headers={}) @classmethod - def builder(cls: Type[GameT]) -> "GameBuilder[GameT]": + def builder(cls: Type[GameT]) -> GameBuilder[GameT]: return GameBuilder(Game=cls) def __repr__(self) -> str: - return "<{} at {:#x} ({!r} vs. {!r}, {!r}{})>".format( + 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 "") @@ -597,17 +1021,14 @@ 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"non-alphanumeric pgn header tag: {key!r}") + 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: - if key in TAG_ROSTER: - return self._tag_roster[key] - else: - return self._others[key] + 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: @@ -620,15 +1041,15 @@ def __iter__(self) -> Iterator[str]: if key in self._tag_roster: yield key - yield from sorted(self._others) + yield from self._others def __len__(self) -> int: return len(self._tag_roster) + len(self._others) - def copy(self: HeadersT) -> HeadersT: + def copy(self) -> Self: return type(self)(self) - def __copy__(self: HeadersT) -> HeadersT: + def __copy__(self) -> Self: return self.copy() def __repr__(self) -> str: @@ -637,14 +1058,14 @@ def __repr__(self) -> str: ", ".join("{}={!r}".format(key, value) for key, value in self.items())) @classmethod - def builder(cls: Type[HeadersT]) -> "HeadersBuilder[HeadersT]": + 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[[GameNode], MainlineMapT]) -> None: + def __init__(self, start: GameNode, f: Callable[[ChildNode], MainlineMapT]) -> None: self.start = start self.f = f @@ -657,16 +1078,19 @@ def __iter__(self) -> Iterator[MainlineMapT]: node = node.variations[0] yield self.f(node) - def __reversed__(self) -> "ReverseMainline[MainlineMapT]": - return ReverseMainline(self.start, self.f) + 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: + 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()) + board.push(node.move) return visitor.result() def __str__(self) -> str: @@ -676,36 +1100,6 @@ def __repr__(self) -> str: return f"" -class ReverseMainline(Generic[MainlineMapT]): - def __init__(self, stop: GameNode, f: Callable[[GameNode], MainlineMapT]) -> None: - self.stop = stop - self.f = f - - self.length = 0 - node = stop - while node.variations: - node = node.variations[0] - self.length += 1 - self.end = node - - def __len__(self) -> int: - return self.length - - def __iter__(self) -> Iterator[MainlineMapT]: - node = self.end - while node.parent and node != self.stop: - yield self.f(node) - node = node.parent - - def __reversed__(self) -> Mainline[MainlineMapT]: - return Mainline(self.stop, self.f) - - def __repr__(self) -> str: - return "".format( - id(self), - " ".join(ReverseMainline(self.stop, lambda node: node._move().uci()))) - - class BaseVisitor(abc.ABC, Generic[ResultT]): """ Base class for visitors. @@ -732,21 +1126,12 @@ def end_headers(self) -> Optional[SkipType]: """Called after visiting game headers.""" pass - def parse_san(self, board: chess.Board, san: str) -> chess.Move: + def begin_parse_san(self, board: chess.Board, san: str) -> Optional[SkipType]: """ - When the visitor is used by a parser, this is called to parse a move - in standard algebraic notation. - - You can override the default implementation to work around specific - quirks of your input format. + When the visitor is used by a parser, this is called at the start of + each standard algebraic notation detailing a move. """ - # Replace zeros with correct castling notation. - if san == "0-0": - san = "O-O" - elif san == "0-0-0": - san = "O-O-O" - - return board.parse_san(san) + pass def visit_move(self, board: chess.Board, move: chess.Move) -> None: """ @@ -765,7 +1150,7 @@ def visit_board(self, board: chess.Board) -> None: """ pass - def visit_comment(self, comment: str) -> None: + def visit_comment(self, comment: list[str]) -> None: """Called for each comment.""" pass @@ -809,65 +1194,76 @@ class GameBuilder(BaseVisitor[GameT]): """ @typing.overload - def __init__(self: "GameBuilder[Game]") -> None: ... + def __init__(self: GameBuilder[Game]) -> None: ... @typing.overload - def __init__(self: "GameBuilder[GameT]", *, Game: Type[GameT]) -> None: ... - def __init__(self, *, Game = Game) -> None: + def __init__(self, *, Game: Type[GameT]) -> None: ... + def __init__(self, *, Game: Any = Game) -> None: self.Game = Game + @override def begin_game(self) -> None: self.game: GameT = self.Game() self.variation_stack: List[GameNode] = [self.game] - self.starting_comment = "" + self.starting_comments: list[str] = [] self.in_variation = False + @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 + @override def visit_nag(self, nag: int) -> None: self.variation_stack[-1].nags.add(nag) + @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 + @override def end_variation(self) -> None: self.variation_stack.pop() + @override def visit_result(self, result: str) -> None: if self.game.headers.get("Result", "*") == "*": self.game.headers["Result"] = result - def visit_comment(self, comment: str) -> None: + @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 # 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() + self.starting_comments.extend(comments) + self.starting_comments = list(filter(None, self.starting_comments)) + @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 + @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: + You can silence the log and handle errors yourself after parsing: >>> import chess.pgn >>> import logging @@ -885,16 +1281,17 @@ def handle_error(self, error: Exception) -> None: >>> import chess.pgn >>> >>> class MyGameBuilder(chess.pgn.GameBuilder): - >>> def handle_error(self, error): + >>> 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) + @override def result(self) -> GameT: """ Returns the visited :class:`~chess.pgn.Game()`. @@ -906,22 +1303,26 @@ class HeadersBuilder(BaseVisitor[HeadersT]): """Collects headers into a dictionary.""" @typing.overload - def __init__(self: "HeadersBuilder[Headers]") -> None: ... + def __init__(self: HeadersBuilder[Headers]) -> None: ... @typing.overload - def __init__(self: "HeadersBuilder[HeadersT]", *, Headers: Type[Headers]) -> None: ... - def __init__(self, *, Headers = Headers) -> None: + def __init__(self, *, Headers: Type[HeadersT]) -> None: ... + def __init__(self, *, Headers: Any = Headers) -> None: self.Headers = Headers + @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 + @override def end_headers(self) -> SkipType: return SKIP + @override def result(self) -> HeadersT: return self.headers @@ -932,37 +1333,46 @@ class BoardBuilder(BaseVisitor[chess.Board]): 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 + @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["typing.Literal[True]"]): +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 - def result(self) -> "typing.Literal[True]": + @override + def result(self) -> Literal[True]: return True @@ -1027,9 +1437,14 @@ def end_variation(self) -> None: self.write_token(") ") self.force_movenumber = True - def visit_comment(self, comment: str) -> None: + 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: int) -> None: @@ -1071,6 +1486,7 @@ class StringExporter(StringExporterMixin, BaseVisitor[str]): 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() @@ -1081,7 +1497,7 @@ def __str__(self) -> str: return self.result() -class FileExporter(StringExporterMixin, BaseVisitor[None]): +class FileExporter(StringExporterMixin, BaseVisitor[int]): """ Acts like a :class:`~chess.pgn.StringExporter`, but games are written directly into a text file. @@ -1102,19 +1518,25 @@ def __init__(self, handle: TextIO, *, columns: Optional[int] = 80, headers: bool super().__init__(columns=columns, headers=headers, comments=comments, variations=variations) self.handle = handle + @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: 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) -> None: - return None + @override + def result(self) -> int: + return self.written def __repr__(self) -> str: return f"" @@ -1127,7 +1549,7 @@ def __str__(self) -> str: 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 = GameBuilder): +def read_game(handle: TextIO, *, Visitor: Any = GameBuilder) -> Any: """ Reads a game from a file opened in text mode. @@ -1151,10 +1573,11 @@ def read_game(handle: TextIO, *, Visitor = GameBuilder): 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. So, the following should - cover most relevant cases (ASCII, UTF-8, UTF-8 with BOM). + 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-sig") + >>> pgn = open("data/pgn/kasparov-deep-blue-1997.pgn", encoding="utf-8") Use :class:`~io.StringIO` to parse games from a string. @@ -1166,14 +1589,14 @@ def read_game(handle: TextIO, *, Visitor = GameBuilder): 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). - According to the PGN standard, at least the usual 7 header tags are + 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. By default, any exceptions are logged and collected in :data:`Game.errors `. This behavior can - be :func:`overriden `. + be :func:`overridden `. Returns the parsed game or ``None`` if the end of file is reached. """ @@ -1183,6 +1606,7 @@ def read_game(handle: TextIO, *, Visitor = GameBuilder): 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") @@ -1190,12 +1614,19 @@ def read_game(handle: TextIO, *, Visitor = GameBuilder): line = handle.readline() # Parse game headers. + consecutive_empty_lines = 0 while line: # 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: found_game = True @@ -1208,6 +1639,8 @@ def read_game(handle: TextIO, *, Visitor = GameBuilder): if not line.startswith("["): break + consecutive_empty_lines = 0 + if not skipping_game: tag_match = TAG_REGEX.match(line) if tag_match: @@ -1215,7 +1648,9 @@ def read_game(handle: TextIO, *, Visitor = GameBuilder): if unmanaged_headers is not None: unmanaged_headers[tag_match.group(1)] = tag_match.group(2) else: - break + # Ignore invalid or malformed headers. + line = handle.readline() + continue line = handle.readline() @@ -1225,10 +1660,6 @@ def read_game(handle: TextIO, *, Visitor = GameBuilder): if not skipping_game: skipping_game = visitor.end_headers() is SKIP - # Ignore single empty line after headers. - if line.isspace(): - line = handle.readline() - if not skipping_game: # Chess variant. headers = managed_headers if unmanaged_headers is None else unmanaged_headers @@ -1242,12 +1673,14 @@ def read_game(handle: TextIO, *, Visitor = GameBuilder): # Initial position. fen = headers.get("FEN", VariantBoard.starting_fen) try: - board_stack = [VariantBoard(fen, chess960=headers.is_chess960())] + board = VariantBoard(fen, chess960=headers.is_chess960()) except ValueError as error: visitor.handle_error(error) skipping_game = True else: - visitor.visit_board(board_stack[0]) + 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: @@ -1277,42 +1710,43 @@ def read_game(handle: TextIO, *, Visitor = GameBuilder): # Parse movetext. skip_variation_depth = 0 + fresh_line = True while line: - read_next_line = True - - # 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() + 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() + fresh_line = True for match in MOVETEXT_REGEX.finditer(line): token = match.group(0) 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 = "" + + if line: + 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("\n".join(comment_lines).strip()) + visitor.visit_comment("".join(comment_lines)) - # Continue with the current or the next line. - if line: - read_next_line = False + # Continue with the current line. + fresh_line = False break elif token == "(": if skip_variation_depth: @@ -1325,9 +1759,12 @@ def read_game(handle: TextIO, *, Visitor = GameBuilder): board.pop() board_stack.append(board) elif token == ")": - if skip_variation_depth: + if skip_variation_depth == 1: + skip_variation_depth = 0 + visitor.end_variation() + elif skip_variation_depth: skip_variation_depth -= 1 - if len(board_stack) > 1: + elif len(board_stack) > 1: visitor.end_variation() board_stack.pop() elif skip_variation_depth: @@ -1353,17 +1790,18 @@ def read_game(handle: TextIO, *, Visitor = GameBuilder): visitor.visit_result(token) else: # Parse SAN tokens. - try: - move = visitor.parse_san(board_stack[-1], 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) + 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 read_next_line: + if fresh_line: line = handle.readline() visitor.end_game() @@ -1372,7 +1810,8 @@ def read_game(handle: TextIO, *, Visitor = GameBuilder): def read_headers(handle: TextIO) -> Optional[Headers]: """ - Reads game headers from a PGN file opened in text mode. + 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 then seek and @@ -1396,7 +1835,7 @@ def read_headers(handle: TextIO) -> Optional[Headers]: ... if "Kasparov" in headers.get("White", "?"): ... kasparov_offsets.append(offset) - Then it can later be seeked an parsed. + Then it can later be seeked and parsed. >>> for offset in kasparov_offsets: ... pgn.seek(offset) @@ -1413,6 +1852,64 @@ def read_headers(handle: TextIO) -> Optional[Headers]: def skip_game(handle: TextIO) -> bool: """ - Skip a game. Returns ``True`` if a game was found and skipped. + Skips a game. Returns ``True`` if a game was found and skipped. """ return bool(read_game(handle, Visitor=SkipVisitor)) + + +def parse_time_control(time_control: str) -> TimeControl: + tc = TimeControl() + + if not time_control: + return tc + + if time_control.startswith("?"): + return tc + + if time_control.startswith("-"): + tc.type = TimeControlType.UNLIMITED + return tc + + def _parse_part(part: str) -> TimeControlPart: + tcp = TimeControlPart() + + moves_time, *bonus = part.split("+") + + if bonus: + _bonus = bonus[0] + if _bonus.lower().endswith("d"): + tcp.delay = float(_bonus[:-1]) + else: + tcp.increment = float(_bonus) + + 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 1566eac43..a7d6807c4 100644 --- a/chess/polyglot.py +++ b/chess/polyglot.py @@ -1,18 +1,4 @@ -# This file is part of the python-chess library. -# Copyright (C) 2012-2020 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 @@ -25,7 +11,7 @@ from typing import Callable, Container, Iterator, List, NamedTuple, Optional, Type, Union -PathLike = Union[str, bytes, os.PathLike] +StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] ENTRY_STRUCT = struct.Struct(">QHHI") @@ -301,10 +287,22 @@ class Entry(NamedTuple): """An entry from a Polyglot opening book.""" key: int + """The Zobrist hash of the position.""" + raw_move: int + """ + The raw binary representation of the move. Use + :data:`~chess.polyglot.Entry.move` instead. + """ + 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.""" + move: chess.Move + """The :class:`~chess.Move`.""" class _EmptyMmap(bytearray): @@ -314,28 +312,36 @@ def size(self) -> int: 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: PathLike) -> None: - self.fd = os.open(filename, os.O_RDONLY | os.O_BINARY if hasattr(os, "O_BINARY") else os.O_RDONLY) - + def __init__(self, filename: StrOrBytesPath) -> None: + fd = os.open(filename, os.O_RDONLY | getattr(os, "O_BINARY", 0)) try: - self.mmap: Union[mmap.mmap, _EmptyMmap] = mmap.mmap(self.fd, 0, access=mmap.ACCESS_READ) + 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: - # Python 3.8 - self.mmap.madvise(mmap.MADV_RANDOM) # type: ignore + # Unix + self.mmap.madvise(mmap.MADV_RANDOM) except AttributeError: pass - def __enter__(self) -> "MemoryMappedReader": + def __enter__(self) -> MemoryMappedReader: return self def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]) -> None: @@ -372,11 +378,8 @@ def __getitem__(self, index: int) -> Entry: return Entry(key, raw_move, weight, learn, move) def __iter__(self) -> Iterator[Entry]: - i = 0 - size = len(self) - while i < size: + for i in range(len(self)): yield self[i] - i += 1 def bisect_key_left(self, key: int) -> int: lo = 0 @@ -395,7 +398,7 @@ def bisect_key_left(self, key: int) -> int: 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: Union[chess.Board, int], *, minimum_weight: int = 1, exclude_moves: Container[chess.Move] = ()) -> Iterator[Entry]: + 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) # type: ignore @@ -429,7 +432,7 @@ def find_all(self, board: Union[chess.Board, int], *, minimum_weight: int = 1, e yield entry - def find(self, board: Union[chess.Board, int], *, minimum_weight: int = 1, exclude_moves: Container[chess.Move] = ()) -> Entry: + 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. @@ -448,13 +451,13 @@ def find(self, board: Union[chess.Board, int], *, minimum_weight: int = 1, exclu except ValueError: raise IndexError() - def get(self, board: Union[chess.Board, int], default: Optional[Entry] = None, *, minimum_weight: int = 1, exclude_moves: Container[chess.Move] = ()) -> Optional[Entry]: + 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=random) -> Entry: + 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. @@ -463,7 +466,7 @@ def choice(self, board: Union[chess.Board, int], *, minimum_weight: int = 1, exc chosen_entry = None for i, entry in enumerate(self.find_all(board, minimum_weight=minimum_weight, exclude_moves=exclude_moves)): - if chosen_entry is None or random.randint(0, i) == i: + if chosen_entry is None or _randint(random, 0, i) == i: chosen_entry = entry if chosen_entry is None: @@ -471,7 +474,7 @@ def choice(self, board: Union[chess.Board, int], *, minimum_weight: int = 1, exc return chosen_entry - def weighted_choice(self, board: Union[chess.Board, int], *, exclude_moves: Container[chess.Move] = (), random=random) -> Entry: + 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. @@ -482,7 +485,7 @@ def weighted_choice(self, board: Union[chess.Board, int], *, exclude_moves: Cont 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): @@ -496,13 +499,8 @@ def close(self) -> None: """Closes the reader.""" self.mmap.close() - try: - os.close(self.fd) - except OSError: - pass - -def open_reader(path: PathLike) -> MemoryMappedReader: +def open_reader(path: StrOrBytesPath) -> MemoryMappedReader: """ Creates a reader for the file at the given path. diff --git a/chess/py.typed b/chess/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/chess/svg.py b/chess/svg.py index 6bc33baee..7e8facf99 100644 --- a/chess/svg.py +++ b/chess/svg.py @@ -1,29 +1,12 @@ -# This file is part of the python-chess library. -# Copyright (C) 2016-2020 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 annotations -import chess import math - import xml.etree.ElementTree as ET -from typing import Iterable, Optional, Tuple, Union +import chess + +from typing import Dict, Iterable, Optional, Tuple, Union +from chess import Color, IntoSquareSet, Square SQUARE_SIZE = 45 @@ -33,47 +16,141 @@ "b": """""", # noqa: E501 "k": """""", # noqa: E501 "n": """""", # noqa: E501 - "p": """""", # noqa: E501 + "p": """""", # noqa: E501 "q": """""", # noqa: E501 "r": """""", # noqa: E501 "B": """""", # noqa: E501 "K": """""", # noqa: E501 "N": """""", # noqa: E501 - "P": """""", # noqa: E501 + "P": """""", # noqa: E501 "Q": """""", # noqa: E501 "R": """""", # noqa: E501 } +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 +} + XX = """""" # noqa: E501 -CHECK_GRADIENT = """""" # noqa: E501 +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", } class Arrow: """Details of an arrow to be drawn.""" - def __init__(self, tail: chess.Square, head: chess.Square, *, color: str = "#888") -> None: + 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) + class SvgWrapper(str): - def _repr_svg_(self) -> "SvgWrapper": + 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": f"0 0 {viewbox:d} {viewbox:d}", }) @@ -85,15 +162,41 @@ def _svg(viewbox: int, size: Optional[int]) -> ET.Element: return svg -def _text(content: str, x: int, y: int, width: int, height: int) -> ET.Element: - 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 @@ -107,6 +210,7 @@ def piece(piece: chess.Piece, size: Optional[int] = None) -> str: >>> 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()])) @@ -114,47 +218,77 @@ def piece(piece: chess.Piece, size: Optional[int] = None) -> str: def board(board: Optional[chess.BaseBoard] = None, *, - squares: Optional[chess.IntoSquareSet] = None, - flipped: bool = False, - coordinates: bool = True, + orientation: Color = chess.WHITE, lastmove: Optional[chess.Move] = None, - check: Optional[chess.Square] = None, - arrows: Iterable[Union[Arrow, Tuple[chess.Square, chess.Square]]] = (), + 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 image. - :param board: A :class:`chess.BaseBoard` for a chessboard with pieces or + :param board: A :class:`chess.BaseBoard` for a chessboard with pieces, or ``None`` (the default) for a chessboard without pieces. - :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 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 like - ``[chess.svg.Arrow(chess.E2, chess.E4)]`` or a list of tuples like + :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. + 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) # doctest: +SKIP + >>> + >>> 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 = MARGIN 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) 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 piece_color in chess.COLORS: @@ -169,95 +303,177 @@ def board(board: Optional[chess.BaseBoard] = None, *, 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]) - ET.SubElement(svg, "rect", { - "x": str(x), - "y": str(y), - "width": str(SQUARE_SIZE), - "height": str(SQUARE_SIZE), + ET.SubElement(svg, "rect", _attrs({ + "x": x, + "y": y, + "width": SQUARE_SIZE, + "height": SQUARE_SIZE, "class": " ".join(cls), "stroke": "none", - "fill": fill_color, - }) - - if square == check: - ET.SubElement(svg, "rect", { - "x": str(x), - "y": str(y), - "width": str(SQUARE_SIZE), - "height": str(SQUARE_SIZE), - "class": "check", - "fill": "url(#check_gradient)", - }) - - # Render pieces. + "fill": square_color, + "opacity": square_opacity if square_opacity < 1.0 else None, + })) + + try: + fill_color, fill_opacity = _color(fill[square]) + except KeyError: + pass + else: + ET.SubElement(svg, "rect", _attrs({ + "x": x, + "y": y, + "width": SQUARE_SIZE, + "height": SQUARE_SIZE, + "stroke": "none", + "fill": fill_color, + "opacity": fill_opacity if fill_opacity < 1.0 else None, + })) + + # Render check mark. + if check is not None: + file_index = chess.square_file(check) + rank_index = chess.square_rank(check) + + 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 + + ET.SubElement(svg, "rect", _attrs({ + "x": x, + "y": y, + "width": SQUARE_SIZE, + "height": SQUARE_SIZE, + "class": "check", + "fill": "url(#check_gradient)", + })) + + # Render pieces and selected squares. + for square, bb in enumerate(chess.BB_SQUARES): + file_index = chess.square_file(square) + rank_index = chess.square_rank(square) + + 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 + if board is not None: piece = board.piece_at(square) if piece: + href = f"#{chess.COLOR_NAMES[piece.color]}-{chess.PIECE_NAMES[piece.piece_type]}" ET.SubElement(svg, "use", { - "xlink:href": f"#{chess.COLOR_NAMES[piece.color]}-{chess.PIECE_NAMES[piece.piece_type]}", + "href": href, + "xlink:href": href, "transform": f"translate({x:d}, {y:d})", }) # Render selected squares. - if squares is not None and square in squares: - ET.SubElement(svg, "use", { + if square in squares: + ET.SubElement(svg, "use", _attrs({ + "href": "#xx", "xlink:href": "#xx", - "x": str(x), - "y": str(y), - }) - - if coordinates: - for file_index, file_name in enumerate(chess.FILE_NAMES): - x = (file_index if not flipped else 7 - file_index) * SQUARE_SIZE + margin - svg.append(_text(file_name, x, 0, SQUARE_SIZE, margin)) - svg.append(_text(file_name, x, margin + 8 * SQUARE_SIZE, SQUARE_SIZE, margin)) - for rank_index, rank_name in enumerate(chess.RANK_NAMES): - y = (7 - rank_index if not flipped else rank_index) * SQUARE_SIZE + margin - svg.append(_text(rank_name, 0, y, margin, SQUARE_SIZE)) - svg.append(_text(rank_name, margin + 8 * SQUARE_SIZE, y, margin, SQUARE_SIZE)) + "x": x, + "y": y, + })) + # Render arrows. for arrow in arrows: try: tail, head, color = arrow.tail, arrow.head, arrow.color # type: ignore except AttributeError: tail, head = arrow # type: ignore - color = "#888" + color = "green" + + try: + color, opacity = _select_color(colors, " ".join(["arrow", color])) + except KeyError: + opacity = 1.0 tail_file = chess.square_file(tail) tail_rank = chess.square_rank(tail) head_file = chess.square_file(head) head_rank = chess.square_rank(head) - xtail = margin + (tail_file + 0.5 if not flipped else 7.5 - tail_file) * SQUARE_SIZE - ytail = margin + (7.5 - tail_rank if not flipped else tail_rank + 0.5) * SQUARE_SIZE - xhead = margin + (head_file + 0.5 if not flipped else 7.5 - head_file) * SQUARE_SIZE - yhead = margin + (7.5 - head_rank if not flipped else head_rank + 0.5) * SQUARE_SIZE + xtail = board_offset + (tail_file + 0.5 if orientation else 7.5 - tail_file) * SQUARE_SIZE + ytail = board_offset + (7.5 - tail_rank if orientation else tail_rank + 0.5) * SQUARE_SIZE + xhead = board_offset + (head_file + 0.5 if orientation else 7.5 - head_file) * SQUARE_SIZE + yhead = board_offset + (7.5 - head_rank if orientation else head_rank + 0.5) * SQUARE_SIZE if (head_file, head_rank) == (tail_file, tail_rank): - ET.SubElement(svg, "circle", { - "cx": str(xhead), - "cy": str(yhead), - "r": str(SQUARE_SIZE * 0.9 / 2), - "stroke-width": str(SQUARE_SIZE * 0.1), + ET.SubElement(svg, "circle", _attrs({ + "cx": xhead, + "cy": yhead, + "r": SQUARE_SIZE * 0.9 / 2, + "stroke-width": SQUARE_SIZE * 0.1, "stroke": color, + "opacity": opacity if opacity < 1.0 else None, "fill": "none", - "opacity": "0.5", "class": "circle", - }) + })) else: marker_size = 0.75 * SQUARE_SIZE marker_margin = 0.1 * SQUARE_SIZE @@ -271,17 +487,17 @@ def board(board: Optional[chess.BaseBoard] = None, *, xtip = xhead - dx * marker_margin / hypot ytip = yhead - dy * marker_margin / hypot - ET.SubElement(svg, "line", { - "x1": str(xtail), - "y1": str(ytail), - "x2": str(shaft_x), - "y2": str(shaft_y), + ET.SubElement(svg, "line", _attrs({ + "x1": xtail, + "y1": ytail, + "x2": shaft_x, + "y2": shaft_y, "stroke": color, - "stroke-width": str(SQUARE_SIZE * 0.2), - "opacity": "0.5", + "opacity": opacity if opacity < 1.0 else None, + "stroke-width": SQUARE_SIZE * 0.2, "stroke-linecap": "butt", "class": "arrow", - }) + })) marker = [(xtip, ytip), (shaft_x + dy * 0.5 * marker_size / hypot, @@ -289,11 +505,11 @@ def board(board: Optional[chess.BaseBoard] = None, *, (shaft_x - dy * 0.5 * marker_size / hypot, shaft_y + dx * 0.5 * marker_size / hypot)] - ET.SubElement(svg, "polygon", { - "points": " ".join(str(x) + "," + str(y) for x, y in marker), + ET.SubElement(svg, "polygon", _attrs({ + "points": " ".join(f"{x},{y}" for x, y in marker), "fill": color, - "opacity": "0.5", + "opacity": opacity if opacity < 1.0 else None, "class": "arrow", - }) + })) return SvgWrapper(ET.tostring(svg).decode("utf-8")) diff --git a/chess/syzygy.py b/chess/syzygy.py index 16c915aac..2250db5b5 100644 --- a/chess/syzygy.py +++ b/chess/syzygy.py @@ -1,18 +1,4 @@ -# This file is part of the python-chess library. -# Copyright (C) 2012-2020 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 collections import math @@ -21,11 +7,15 @@ import re import struct import threading +import typing import chess from types import TracebackType -from typing import Deque, Iterable, Iterator, List, MutableMapping, Optional, Tuple, Type, Union +from typing import Deque, Dict, Iterable, Iterator, List, Optional, Set, Tuple, Type, TypeVar, Union + +if typing.TYPE_CHECKING: + from typing_extensions import Self UINT64_BE = struct.Struct(">Q") @@ -88,7 +78,7 @@ def flipdiag(square: chess.Square) -> chess.Square: ] PTWIST = [ - 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 47, 35, 23, 11, 10, 22, 34, 46, 45, 33, 21, 9, 8, 20, 32, 44, 43, 31, 19, 7, 6, 18, 30, 42, @@ -288,7 +278,7 @@ def flipdiag(square: chess.Square) -> chess.Square: -1, -1, -1, -1, 276, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1 + -1, -1, -1, -1, -1, -1, -1, -1, ]] def test45(sq: chess.Square) -> bool: @@ -370,14 +360,18 @@ def binom(x: int, y: int) -> int: TABLENAME_REGEX = re.compile(r"^[KQRBNP]+v[KQRBNP]+\Z") -def is_table_name(name: str) -> bool: - return len(name) <= 7 + 1 and bool(TABLENAME_REGEX.match(name)) and normalize_tablename(name) == name +def is_tablename(name: str, *, one_king: bool = True, piece_count: Optional[int] = TBPIECES, normalized: bool = True) -> bool: + return ( + (piece_count is None or len(name) <= piece_count + 1) and + bool(TABLENAME_REGEX.match(name)) and + (not normalized or normalize_tablename(name) == name) and + (not one_king or (name != "KvK" and name.startswith("K") and "vK" in name))) def tablenames(*, one_king: bool = True, piece_count: int = 6) -> Iterator[str]: first = "K" if one_king else "P" - targets = [] + targets: List[str] = [] white_pieces = piece_count - 2 black_pieces = 0 @@ -420,7 +414,7 @@ def _dependencies(target: str, *, one_king: bool = True) -> Iterator[str]: def dependencies(target: str, *, one_king: bool = True) -> Iterator[str]: - closed = set() + closed: Set[str] = set() if one_king: closed.add("KvK") @@ -431,7 +425,7 @@ def dependencies(target: str, *, one_king: bool = True) -> Iterator[str]: def all_dependencies(targets: Iterable[str], *, one_king: bool = True) -> Iterator[str]: - closed = set() + closed: Set[str] = set() if one_king: closed.add("KvK") @@ -514,36 +508,35 @@ class MissingTableError(KeyError): class PairsData: - def __init__(self) -> None: - self.indextable = None - self.sizetable = None - self.data = None - self.offset = None - self.symlen = None - self.sympat: int = None - self.blocksize = None - self.idxbits = None - self.min_len = None - self.base = None + indextable: int + sizetable: int + data: int + offset: int + symlen: List[int] + sympat: int + blocksize: int + idxbits: int + min_len: int + base: List[int] class PawnFileData: def __init__(self) -> None: - self.precomp = {} - self.factor = {} - self.pieces = {} - self.norm = {} + self.precomp: Dict[int, PairsData] = {} + self.factor: Dict[int, List[int]] = {} + self.pieces: Dict[int, List[int]] = {} + self.norm: Dict[int, List[int]] = {} class PawnFileDataDtz: - def __init__(self) -> None: - self.precomp = None - self.factor = None - self.pieces = None - self.norm = None + precomp: PairsData + factor: List[int] + pieces: List[int] + norm: List[int] class Table: + size: List[int] def __init__(self, path: str, *, variant: Type[chess.Board] = chess.Board) -> None: self.path = path @@ -551,8 +544,7 @@ def __init__(self, path: str, *, variant: Type[chess.Board] = chess.Board) -> No self.write_lock = threading.RLock() self.initialized = False - self.fd = None - self.data = None + self.data: Optional[mmap.mmap] = None self.read_condition = threading.Condition() self.read_count = 0 @@ -569,8 +561,7 @@ def __init__(self, path: str, *, variant: Type[chess.Board] = chess.Board) -> No black_part, white_part = tablename.split("v") if self.has_pawns: - self.pawns = {0: white_part.count("P"), - 1: black_part.count("P")} + self.pawns = [white_part.count("P"), black_part.count("P")] if self.pawns[1] > 0 and (self.pawns[0] == 0 or self.pawns[1] < self.pawns[0]): self.pawns[0], self.pawns[1] = self.pawns[1], self.pawns[0] else: @@ -595,30 +586,34 @@ def __init__(self, path: str, *, variant: Type[chess.Board] = chess.Board) -> No self.enc_type = 1 + j def init_mmap(self) -> None: - with self.write_lock: - # Open fd. - if self.fd is None: - self.fd = os.open(self.path, os.O_RDONLY | os.O_BINARY if hasattr(os, "O_BINARY") else os.O_RDONLY) + if self.data is None: + fd = os.open(self.path, os.O_RDONLY | getattr(os, "O_BINARY", 0)) + try: + data = mmap.mmap(fd, 0, access=mmap.ACCESS_READ) + finally: + os.close(fd) - # Open mmap. - if self.data is None: - data = mmap.mmap(self.fd, 0, access=mmap.ACCESS_READ) - if data.size() % 64 != 16: - raise IOError(f"invalid file size: ensure {self.path!r} is a valid syzygy tablebase file") - self.data = data + if data.size() % 64 != 16: + raise IOError(f"invalid file size: ensure {self.path!r} is a valid syzygy tablebase file") - try: - # Python 3.8 - self.data.madvise(mmap.MADV_RANDOM) - except AttributeError: - pass + try: + # Unix + data.madvise(mmap.MADV_RANDOM) + except AttributeError: + pass + + self.data = data + + def check_magic(self, magic: Optional[bytes], pawnless_magic: Optional[bytes]) -> None: + assert self.data - def check_magic(self, magic: Optional[bytes], pawnless_magic: Optional[bytes]) -> bool: valid_magics = [magic, self.has_pawns and pawnless_magic] if self.data[:min(4, len(self.data))] not in valid_magics: raise IOError(f"invalid magic header: ensure {self.path!r} is a valid syzygy tablebase file") def setup_pairs(self, data_ptr: int, tb_size: int, size_idx: int, wdl: int) -> PairsData: + assert self.data + d = PairsData() self._flags = self.data[data_ptr] @@ -673,7 +668,7 @@ def setup_pairs(self, data_ptr: int, tb_size: int, size_idx: int, wdl: int) -> P return d - def set_norm_piece(self, norm: List[int], pieces) -> None: + def set_norm_piece(self, norm: List[int], pieces: List[int]) -> None: if self.enc_type == 0: norm[0] = 3 elif self.enc_type == 2: @@ -690,10 +685,7 @@ def set_norm_piece(self, norm: List[int], pieces) -> None: i += norm[i] def calc_factors_piece(self, factor: List[int], order: int, norm: List[int]) -> int: - if not self.variant.connected_kings: - PIVFAC = [31332, 28056, 462] - else: - PIVFAC = [31332, 0, 518, 278] + PIVFAC = [31332, 0, 518, 278] if self.variant.connected_kings else [31332, 28056, 462] n = 64 - norm[0] @@ -716,7 +708,7 @@ def calc_factors_piece(self, factor: List[int], order: int, norm: List[int]) -> return f - def calc_factors_pawn(self, factor: int, order: int, order2: int, norm: List[int], f: int) -> int: + def calc_factors_pawn(self, factor: List[int], order: int, order2: int, norm: List[int], f: int) -> int: i = norm[0] if order2 < 0x0f: i += norm[i] @@ -740,7 +732,7 @@ def calc_factors_pawn(self, factor: int, order: int, order2: int, norm: List[int return fac - def set_norm_pawn(self, norm: List[int], pieces) -> None: + def set_norm_pawn(self, norm: List[int], pieces: List[int]) -> None: norm[0] = self.pawns[0] if self.pawns[1]: norm[self.pawns[0]] = self.pawns[1] @@ -754,6 +746,8 @@ def set_norm_pawn(self, norm: List[int], pieces) -> None: i += norm[i] def calc_symlen(self, d: PairsData, s: int, tmp: List[int]) -> None: + assert self.data + w = d.sympat + 3 * s s2 = (self.data[w + 2] << 4) | (self.data[w + 1] >> 4) if s2 == 0x0fff: @@ -767,14 +761,14 @@ def calc_symlen(self, d: PairsData, s: int, tmp: List[int]) -> None: d.symlen[s] = d.symlen[s1] + d.symlen[s2] + 1 tmp[s] = 1 - def pawn_file(self, pos: chess.Square) -> int: + def pawn_file(self, pos: List[chess.Square]) -> chess.File: for i in range(1, self.pawns[0]): if FLAP[pos[0]] > FLAP[pos[i]]: pos[0], pos[i] = pos[i], pos[0] return FILE_TO_FILE[pos[0] & 0x07] - def encode_piece(self, norm: List[int], pos: List[chess.Square], factor: int) -> int: + def encode_piece(self, norm: List[int], pos: List[chess.Square], factor: List[int]) -> int: n = self.num if self.enc_type < 3: @@ -786,6 +780,7 @@ def encode_piece(self, norm: List[int], pos: List[chess.Square], factor: int) -> for i in range(n): pos[i] ^= 0x38 + i = 0 for i in range(n): if offdiag(pos[i]): break @@ -887,7 +882,7 @@ def encode_piece(self, norm: List[int], pos: List[chess.Square], factor: int) -> return idx - def encode_pawn(self, norm: List[int], pos: chess.Square, factor: int) -> int: + def encode_pawn(self, norm: List[int], pos: List[chess.Square], factor: List[int]) -> int: n = self.num if pos[0] & 0x04: @@ -944,6 +939,8 @@ def encode_pawn(self, norm: List[int], pos: chess.Square, factor: int) -> int: return idx def decompress_pairs(self, d: PairsData, idx: int) -> int: + assert self.data + if not d.idxbits: return d.min_len @@ -1009,16 +1006,16 @@ def decompress_pairs(self, d: PairsData, idx: int) -> int: return self.data[w] def read_uint64_be(self, data_ptr: int) -> int: - return UINT64_BE.unpack_from(self.data, data_ptr)[0] + return UINT64_BE.unpack_from(self.data, data_ptr)[0] # type: ignore def read_uint32(self, data_ptr: int) -> int: - return UINT32.unpack_from(self.data, data_ptr)[0] + return UINT32.unpack_from(self.data, data_ptr)[0] # type: ignore def read_uint32_be(self, data_ptr: int) -> int: - return UINT32_BE.unpack_from(self.data, data_ptr)[0] + return UINT32_BE.unpack_from(self.data, data_ptr)[0] # type: ignore def read_uint16(self, data_ptr: int) -> int: - return UINT16.unpack_from(self.data, data_ptr)[0] + return UINT16.unpack_from(self.data, data_ptr)[0] # type: ignore def close(self) -> None: with self.write_lock: @@ -1030,11 +1027,7 @@ def close(self) -> None: self.data.close() self.data = None - if self.fd is not None: - os.close(self.fd) - self.fd = None - - def __enter__(self) -> None: + def __enter__(self) -> Self: return self def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]) -> None: @@ -1042,10 +1035,13 @@ def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[ class WdlTable(Table): + _next: int + _flags: int def init_table_wdl(self) -> None: with self.write_lock: self.init_mmap() + assert self.data if self.initialized: return @@ -1056,22 +1052,14 @@ def init_table_wdl(self) -> None: self.size = [0 for _ in range(8 * 3)] # Used if there are only pieces. - self.precomp = {} - self.pieces = {} - - self.factor = {0: [0 for _ in range(TBPIECES)], - 1: [0 for _ in range(TBPIECES)]} - - self.norm = {0: [0 for _ in range(self.num)], - 1: [0 for _ in range(self.num)]} + self.precomp: Dict[int, PairsData] = {} + self.pieces: Dict[int, List[int]] = {} + self.factor = [[0 for _ in range(TBPIECES)] for _ in range(2)] + self.norm = [[0 for _ in range(self.num)] for _ in range(2)] # Used if there are pawns. self.files = [PawnFileData() for _ in range(4)] - self._next = None - self._flags = None - self.flags = None - split = self.data[4] & 0x01 files = 4 if self.data[4] & 0x02 else 1 @@ -1087,8 +1075,6 @@ def init_table_wdl(self) -> None: if split: self.precomp[1] = self.setup_pairs(data_ptr, self.tb_size[1], 3, True) data_ptr = self._next - else: - self.precomp[1] = None self.precomp[0].indextable = data_ptr data_ptr += self.size[0] @@ -1124,8 +1110,6 @@ def init_table_wdl(self) -> None: if split: self.files[f].precomp[1] = self.setup_pairs(data_ptr, self.tb_size[2 * f + 1], 6 * f + 3, True) data_ptr = self._next - else: - self.files[f].precomp[1] = None for f in range(files): self.files[f].precomp[0].indextable = data_ptr @@ -1153,6 +1137,8 @@ def init_table_wdl(self) -> None: self.initialized = True def setup_pieces_pawn(self, p_data: int, p_tb_size: int, f: int) -> None: + assert self.data + j = 1 + int(self.pawns[1] > 0) order = self.data[p_data] & 0x0f order2 = self.data[p_data + 1] & 0x0f if self.pawns[1] else 0x0f @@ -1171,6 +1157,8 @@ def setup_pieces_pawn(self, p_data: int, p_tb_size: int, f: int) -> None: self.tb_size[p_tb_size + 1] = self.calc_factors_pawn(self.files[f].factor[1], order, order2, self.files[f].norm[1], f) def setup_pieces_piece(self, p_data: int) -> None: + assert self.data + self.pieces[0] = [self.data[p_data + i + 1] & 0x0f for i in range(self.num)] order = self.data[p_data] & 0x0f self.set_norm_piece(self.norm[0], self.pieces[0]) @@ -1257,6 +1245,7 @@ class DtzTable(Table): def init_table_dtz(self) -> None: with self.write_lock: self.init_mmap() + assert self.data if self.initialized: return @@ -1267,31 +1256,31 @@ def init_table_dtz(self) -> None: self.norm = [0 for _ in range(self.num)] self.tb_size = [0, 0, 0, 0] self.size = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - self.files = [PawnFileDataDtz() for f in range(4)] + self.files = [PawnFileDataDtz() for _ in range(4)] files = 4 if self.data[4] & 0x02 else 1 p_data = 5 if not self.has_pawns: - self.map_idx = [0, 0, 0, 0] + self.map_idx: List[List[int]] = [[0, 0, 0, 0]] self.setup_pieces_piece_dtz(p_data, 0) p_data += self.num + 1 p_data += p_data & 0x01 self.precomp = self.setup_pairs(p_data, self.tb_size[0], 0, False) - self.flags = self._flags + self.flags: Union[int, List[int]] = self._flags p_data = self._next self.p_map = p_data if self.flags & 2: if not self.flags & 16: for i in range(4): - self.map_idx[i] = p_data + 1 - self.p_map + self.map_idx[0][i] = p_data + 1 - self.p_map p_data += 1 + self.data[p_data] else: for i in range(4): - self.map_idx[i] = (p_data + 2 - self.p_map) // 2 + self.map_idx[0][i] = (p_data + 2 - self.p_map) // 2 p_data += 2 + 2 * self.read_uint16(p_data) p_data += p_data & 0x01 @@ -1363,6 +1352,7 @@ def probe_dtz_table(self, board: chess.Board, wdl: int) -> Tuple[int, int]: def _probe_dtz_table(self, board: chess.Board, wdl: int) -> Tuple[int, int]: self.init_table_dtz() + assert self.data key = calc_key(board) @@ -1380,6 +1370,8 @@ def _probe_dtz_table(self, board: chess.Board, wdl: int) -> Tuple[int, int]: bside = 0 if not self.has_pawns: + assert isinstance(self.flags, int) + if (self.flags & 1) != bside and not self.symmetric: return 0, -1 @@ -1400,13 +1392,15 @@ def _probe_dtz_table(self, board: chess.Board, wdl: int) -> Tuple[int, int]: if self.flags & 2: if not self.flags & 16: - res = self.data[self.p_map + self.map_idx[WDL_TO_MAP[wdl + 2]] + res] + res = self.data[self.p_map + self.map_idx[0][WDL_TO_MAP[wdl + 2]] + res] else: - res = self.read_uint16(self.p_map + 2 * (self.map_idx[WDL_TO_MAP[wdl + 2]] + res)) + res = self.read_uint16(self.p_map + 2 * (self.map_idx[0][WDL_TO_MAP[wdl + 2]] + res)) if (not (self.flags & PA_FLAGS[wdl + 2])) or (wdl & 1): res *= 2 else: + assert isinstance(self.flags, list) + k = self.files[0].pieces[0] ^ cmirror piece_type = k & 0x07 color = k >> 3 @@ -1446,12 +1440,16 @@ def _probe_dtz_table(self, board: chess.Board, wdl: int) -> Tuple[int, int]: return res, 1 def setup_pieces_piece_dtz(self, p_data: int, p_tb_size: int) -> None: + assert self.data + self.pieces = [self.data[p_data + i + 1] & 0x0f for i in range(self.num)] order = self.data[p_data] & 0x0f self.set_norm_piece(self.norm, self.pieces) self.tb_size[p_tb_size] = self.calc_factors_piece(self.factor, order, self.norm) def setup_pieces_pawn_dtz(self, p_data: int, p_tb_size: int, f: int) -> None: + assert self.data + j = 1 + int(self.pawns[1] > 0) order = self.data[p_data] & 0x0f order2 = self.data[p_data + 1] & 0x0f if self.pawns[1] else 0x0f @@ -1467,10 +1465,6 @@ def setup_pieces_pawn_dtz(self, p_data: int, p_tb_size: int, f: int) -> None: class Tablebase: """ Manages a collection of tablebase files for probing. - - If *max_fds* is not ``None``, will at most use *max_fds* open file - descriptors at any given time. The least recently used tables are closed, - if nescessary. """ def __init__(self, *, max_fds: Optional[int] = 128, VariantBoard: Type[chess.Board] = chess.Board) -> None: self.variant = VariantBoard @@ -1479,8 +1473,8 @@ def __init__(self, *, max_fds: Optional[int] = 128, VariantBoard: Type[chess.Boa self.lru: Deque[Table] = collections.deque() self.lru_lock = threading.Lock() - self.wdl: MutableMapping[str, WdlTable] = {} - self.dtz: MutableMapping[str, DtzTable] = {} + self.wdl: Dict[str, Table] = {} + self.dtz: Dict[str, Table] = {} def _bump_lru(self, table: Table) -> None: if self.max_fds is None: @@ -1496,7 +1490,7 @@ def _bump_lru(self, table: Table) -> None: if len(self.lru) > self.max_fds: self.lru.pop().close() - def _open_table(self, hashtable: MutableMapping[str, Table], Table: Type[Table], path: str) -> int: + def _open_table(self, hashtable: Dict[str, Table], Table: Type[Table], path: str) -> int: table = Table(path, variant=self.variant) if table.key in hashtable: @@ -1510,8 +1504,8 @@ def add_directory(self, directory: str, *, load_wdl: bool = True, load_dtz: bool """ Adds tables from a directory. - By default all available tables with the correct file names - (e.g. WDL files like ``KQvKN.rtbw`` and DTZ files like ``KRBvK.rtbz``) + By default, all available tables with the correct file names + (e.g., WDL files like ``KQvKN.rtbw`` and DTZ files like ``KRBvK.rtbz``) are added. The relevant files are lazily opened when the tablebase is actually @@ -1519,27 +1513,23 @@ def add_directory(self, directory: str, *, load_wdl: bool = True, load_dtz: bool Returns the number of table files that were found. """ - num = 0 directory = os.path.abspath(directory) - - for filename in os.listdir(directory): - path = os.path.join(directory, filename) - tablename, ext = os.path.splitext(filename) - - if is_table_name(tablename) and os.path.isfile(path): - if load_wdl: - if ext == self.variant.tbw_suffix: - num += self._open_table(self.wdl, WdlTable, path) - elif "P" not in tablename and ext == self.variant.pawnless_tbw_suffix: - num += self._open_table(self.wdl, WdlTable, path) - - if load_dtz: - if ext == self.variant.tbz_suffix: - num += self._open_table(self.dtz, DtzTable, path) - elif "P" not in tablename and ext == self.variant.pawnless_tbz_suffix: - num += self._open_table(self.dtz, DtzTable, path) - - return num + return sum(self.add_file(os.path.join(directory, filename), load_wdl=load_wdl, load_dtz=load_dtz) for filename in os.listdir(directory)) + + def add_file(self, path: str, *, load_wdl: bool = True, load_dtz: bool = True) -> int: + tablename, ext = os.path.splitext(os.path.basename(path)) + if is_tablename(tablename, one_king=self.variant.one_king) and os.path.isfile(path): + if load_wdl: + if ext == self.variant.tbw_suffix: + return self._open_table(self.wdl, WdlTable, path) + elif "P" not in tablename and ext == self.variant.pawnless_tbw_suffix: + return self._open_table(self.wdl, WdlTable, path) + if load_dtz: + if ext == self.variant.tbz_suffix: + return self._open_table(self.dtz, DtzTable, path) + elif "P" not in tablename and ext == self.variant.pawnless_tbz_suffix: + return self._open_table(self.dtz, DtzTable, path) + return 0 def probe_wdl_table(self, board: chess.Board) -> int: # Test for variant end. @@ -1556,8 +1546,10 @@ def probe_wdl_table(self, board: chess.Board) -> int: key = calc_key(board) try: - table = self.wdl[key] + table = typing.cast(WdlTable, self.wdl[key]) except KeyError: + if board.piece_count() > TBPIECES: + raise KeyError(f"syzygy tables support up to {TBPIECES} pieces, not {board.piece_count()}: {board.fen()}") raise MissingTableError(f"did not find wdl table {key}") self._bump_lru(table) @@ -1565,6 +1557,20 @@ def probe_wdl_table(self, board: chess.Board) -> int: return table.probe_wdl_table(board) def probe_ab(self, board: chess.Board, alpha: int, beta: int, threats: bool = False) -> Tuple[int, int]: + # Check preconditions. + if board.uci_variant != self.variant.uci_variant: + raise KeyError(f"tablebase has been opened for {self.variant.uci_variant}, probed with: {board.uci_variant}") + if board.castling_rights: + raise KeyError(f"syzygy tables do not contain positions with castling rights: {board.fen()}") + + # Probing resolves captures, so sometimes we can obtain results for + # positions that have more pieces than the maximum number of supported + # pieces. We artificially limit this to one additional level, to + # make sure search remains somewhat bounded. + if board.piece_count() > TBPIECES + 1: + raise KeyError(f"syzygy tables support up to {TBPIECES} pieces, not {board.piece_count()}: {board.fen()}") + + # Special case: Variant with compulsory captures. if self.variant.captures_compulsory: if board.is_variant_win(): return 2, 2 @@ -1607,7 +1613,7 @@ def sprobe_ab(self, board: chess.Board, alpha: int, beta: int, threats: bool = F threats_found = False - if threats or chess.popcount(board.occupied) >= 6: + if threats or board.piece_count() >= 6: for threat in board.generate_legal_moves(~board.pawns): board.push(threat) try: @@ -1650,7 +1656,9 @@ def sprobe_capts(self, board: chess.Board, alpha: int, beta: int) -> Tuple[int, def probe_wdl(self, board: chess.Board) -> int: """ - Probes WDL tables for win/draw/loss-information. + Probes WDL tables for win/draw/loss information under the 50-move rule, + assuming the position has been reached directly after a capture or + pawn move. Probing is thread-safe when done with different *board* objects and if *board* objects are not modified during probing. @@ -1679,14 +1687,6 @@ def probe_wdl(self, board: chess.Board) -> int: Note that probing corrupted table files is undefined behavior. """ - # Positions with castling rights are not in the tablebase. - if board.castling_rights: - raise KeyError(f"syzygy tables do not contain positions with castling rights: {board.fen()}") - - # Validate piece count. - if chess.popcount(board.occupied) > 7: - raise KeyError(f"syzygy tables support up to 6 (and experimentally 7) pieces, not {chess.popcount(board.occupied)}: {board.fen()}") - # Probe. v, _ = self.probe_ab(board, -2, 2) @@ -1729,7 +1729,7 @@ def get_wdl(self, board: chess.Board, default: Optional[int] = None) -> Optional def probe_dtz_table(self, board: chess.Board, wdl: int) -> Tuple[int, int]: key = calc_key(board) try: - table = self.dtz[key] + table = typing.cast(DtzTable, self.dtz[key]) except KeyError: raise MissingTableError(f"did not find dtz table {key}") @@ -1811,12 +1811,18 @@ def probe_dtz_no_ep(self, board: chess.Board) -> int: def probe_dtz(self, board: chess.Board) -> int: """ - Probes DTZ tables for distance to zero information. + Probes DTZ tables for + `DTZ50'' information with rounding `_. - Both DTZ and WDL tables are required in order to probe for DTZ. + Minmaxing the DTZ50'' values guarantees winning a won position + (and drawing a drawn position), because it makes progress keeping the + win in hand. + However, the lines are not always the most straightforward ways to win. + Engines like Stockfish calculate themselves, checking with DTZ, but + only play according to DTZ if they can not manage on their own. Returns a positive value if the side to move is winning, ``0`` if the - position is a draw and a negative value if the side to move is losing. + position is a draw, and a negative value if the side to move is losing. More precisely: +-----+------------------+--------------------------------------------+ @@ -1844,17 +1850,16 @@ def probe_dtz(self, board: chess.Board) -> int: +-----+------------------+--------------------------------------------+ The return value can be off by one: a return value -n can mean a - losing zeroing move in in n + 1 plies and a return value +n can mean a + losing zeroing move in n + 1 plies and a return value +n can mean a winning zeroing move in n + 1 plies. - This is guaranteed not to happen for positions exactly on the edge of - the 50-move rule, so that (with some care) this never impacts the - result of practical play. + This implies some primary tablebase lines may waste up to 1 ply. + Rounding is never used for endgame phases where it would change the + game theoretical outcome. - Minmaxing the DTZ values guarantees winning a won position (and drawing - a drawn position), because it makes progress keeping the win in hand. - However the lines are not always the most straightforward ways to win. - Engines like Stockfish calculate themselves, checking with DTZ, but - only play according to DTZ if they can not manage on their own. + This means users need to be careful in positions that are nearly drawn + under the 50-move rule! Carelessly wasting 1 more ply by not following + the tablebase recommendation, for a total of 2 wasted plies, may + change the outcome of the game. >>> import chess >>> import chess.syzygy @@ -1868,6 +1873,8 @@ def probe_dtz(self, board: chess.Board) -> int: Probing is thread-safe when done with different *board* objects and if *board* objects are not modified during probing. + Both DTZ and WDL tables are required in order to probe for DTZ. + :raises: :exc:`KeyError` (or specifically :exc:`chess.syzygy.MissingTableError`) if the position could not be found in the tablebase. Use @@ -1883,7 +1890,7 @@ def probe_dtz(self, board: chess.Board) -> int: v1 = -3 - # Generate all en-passant moves. + # Generate all en passant moves. for move in board.generate_legal_ep(): board.push(move) try: @@ -1935,7 +1942,7 @@ def close(self) -> None: self.lru.clear() - def __enter__(self) -> "Tablebase": + def __enter__(self) -> Tablebase: return self def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]) -> None: @@ -1950,11 +1957,16 @@ def open_tablebase(directory: str, *, load_wdl: bool = True, load_dtz: bool = Tr .. note:: Generally probing requires tablebase files for the specific - material composition, **as well as** tablebase files with less pieces. - This is important because 6-piece and 5-piece files are often - distributed seperately, but are both required for 6-piece positions. - Use :func:`~chess.syzygy.Tablebase.add_directory()` to load + material composition, **as well as** material compositions transitively + reachable by captures and promotions. + This is important because 6-piece and 5-piece (let alone 7-piece) files + are often distributed separately, but are both required for 6-piece + positions. Use :func:`~chess.syzygy.Tablebase.add_directory()` to load tables from additional directories. + + :param max_fds: If *max_fds* is not ``None``, will at most use *max_fds* + open file descriptors at any given time. The least recently used tables + are closed, if necessary. """ tables = Tablebase(max_fds=max_fds, VariantBoard=VariantBoard) tables.add_directory(directory, load_wdl=load_wdl, load_dtz=load_dtz) diff --git a/chess/variant.py b/chess/variant.py index 0cd2e7c64..ba4c0f1ce 100644 --- a/chess/variant.py +++ b/chess/variant.py @@ -1,25 +1,14 @@ -# This file is part of the python-chess library. -# Copyright (C) 2016-2020 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 copy import itertools +import typing from typing import Dict, Generic, Hashable, Iterable, Iterator, List, Optional, Type, TypeVar, Union +if typing.TYPE_CHECKING: + from typing_extensions import Self + class SuicideBoard(chess.Board): @@ -48,6 +37,9 @@ def _attacked_for_king(self, path: chess.Bitboard, occupied: chess.Bitboard) -> def checkers_mask(self) -> chess.Bitboard: return chess.BB_EMPTY + def gives_check(self, move: chess.Move) -> bool: + return False + def is_into_check(self, move: chess.Move) -> bool: return False @@ -80,16 +72,25 @@ def is_variant_draw(self) -> bool: return self.is_stalemate() and self._material_balance() == 0 def has_insufficient_material(self, color: chess.Color) -> bool: - if self.occupied != self.bishops: + if not self.occupied_co[color]: + return False + elif not self.occupied_co[not color]: + return True + elif self.occupied == self.bishops: + # In a position with only bishops, check if all our bishops can be + # captured. + we_some_on_light = bool(self.occupied_co[color] & chess.BB_LIGHT_SQUARES) + we_some_on_dark = bool(self.occupied_co[color] & chess.BB_DARK_SQUARES) + they_all_on_dark = not (self.occupied_co[not color] & chess.BB_LIGHT_SQUARES) + they_all_on_light = not (self.occupied_co[not color] & chess.BB_DARK_SQUARES) + return (we_some_on_light and they_all_on_dark) or (we_some_on_dark and they_all_on_light) + elif self.occupied == self.knights and chess.popcount(self.knights) == 2: + return ( + self.turn == color ^ + bool(self.occupied_co[chess.WHITE] & chess.BB_LIGHT_SQUARES) ^ + bool(self.occupied_co[chess.BLACK] & chess.BB_DARK_SQUARES)) + else: return False - - # In a position with only bishops, check if all our bishops can be - # captured. - we_some_on_light = bool(self.occupied_co[color] & chess.BB_LIGHT_SQUARES) - we_some_on_dark = bool(self.occupied_co[color] & chess.BB_DARK_SQUARES) - they_all_on_dark = not (self.occupied_co[not color] & chess.BB_LIGHT_SQUARES) - they_all_on_light = not (self.occupied_co[not color] & chess.BB_DARK_SQUARES) - return (we_some_on_light and they_all_on_dark) or (we_some_on_dark and they_all_on_light) def generate_pseudo_legal_moves(self, from_mask: chess.Bitboard = chess.BB_ALL, to_mask: chess.Bitboard = chess.BB_ALL) -> Iterator[chess.Move]: for move in super().generate_pseudo_legal_moves(from_mask, to_mask): @@ -126,16 +127,8 @@ def is_legal(self, move: chess.Move) -> bool: else: return not any(self.generate_pseudo_legal_captures()) - def _transposition_key(self) -> Hashable: - if self.has_chess960_castling_rights(): - return (super()._transposition_key(), self.kings & self.promoted) - else: - return super()._transposition_key() - - def board_fen(self, promoted: Optional[bool] = None) -> str: - if promoted is None: - promoted = self.has_chess960_castling_rights() - return super().board_fen(promoted=promoted) + def _effective_promoted(self) -> chess.Bitboard: + return self.kings & self.promoted if self.castling_rights else chess.BB_EMPTY def status(self) -> chess.Status: status = super().status() @@ -196,7 +189,6 @@ class AtomicBoard(chess.Board): tbw_magic = b"\x55\x8d\xa4\x49" tbz_magic = b"\x91\xa9\x5e\xeb" connected_kings = True - one_king = True def is_variant_end(self) -> bool: return not all(self.kings & side for side in self.occupied_co) @@ -261,9 +253,9 @@ def _push_capture(self, move: chess.Move, capture_square: chess.Square, piece_ty # Destroy castling rights. self.castling_rights &= ~explosion_radius - if explosion_radius & self.kings & self.occupied_co[chess.WHITE] & ~self.promoted: + if explosion_radius & self.kings & self.occupied_co[chess.WHITE] & ~self._effective_promoted(): self.castling_rights &= ~chess.BB_RANK_1 - if explosion_radius & self.kings & self.occupied_co[chess.BLACK] & ~self.promoted: + if explosion_radius & self.kings & self.occupied_co[chess.BLACK] & ~self._effective_promoted(): self.castling_rights &= ~chess.BB_RANK_8 # Explode the capturing piece. @@ -315,12 +307,14 @@ def status(self) -> chess.Status: status &= ~chess.STATUS_NO_BLACK_KING if chess.popcount(self.checkers_mask()) <= 14: status &= ~chess.STATUS_TOO_MANY_CHECKERS + if self._valid_ep_square() is None: + status &= ~chess.STATUS_IMPOSSIBLE_CHECK return status class KingOfTheHillBoard(chess.Board): - aliases = ["King of the Hill", "KOTH"] + aliases = ["King of the Hill", "KOTH", "kingOfTheHill"] uci_variant = "kingofthehill" xboard_variant = "kingofthehill" # Unofficial @@ -360,36 +354,26 @@ def __init__(self, fen: Optional[str] = starting_fen, chess960: bool = False) -> def reset(self) -> None: self.set_fen(type(self).starting_fen) - def _gives_check(self, move: chess.Move) -> bool: - self.push(move) - gives_check = self.is_check() - self.pop() - return gives_check - def is_legal(self, move: chess.Move) -> bool: - return super().is_legal(move) and not self._gives_check(move) + return super().is_legal(move) and not self.gives_check(move) def generate_legal_moves(self, from_mask: chess.Bitboard = chess.BB_ALL, to_mask: chess.Bitboard = chess.BB_ALL) -> Iterator[chess.Move]: for move in super().generate_legal_moves(from_mask, to_mask): - if not self._gives_check(move): + if not self.gives_check(move): yield move def is_variant_end(self) -> bool: if not self.kings & chess.BB_RANK_8: return False - if self.turn == chess.WHITE or self.kings & self.occupied_co[chess.BLACK] & chess.BB_RANK_8: - return True - black_kings = self.kings & self.occupied_co[chess.BLACK] - if not black_kings: + if self.turn == chess.WHITE or black_kings & chess.BB_RANK_8 or not black_kings: return True - black_king = chess.msb(black_kings) - # White has reached the backrank. The game is over if black can not # also reach the backrank on the next move. Check if there are any # safe squares for the king. + black_king = chess.msb(black_kings) targets = chess.BB_KING_ATTACKS[black_king] & chess.BB_RANK_8 & ~self.occupied_co[chess.BLACK] return all(self.attackers_mask(chess.WHITE, target) for target in chess.scan_forward(targets)) @@ -401,7 +385,11 @@ def is_variant_loss(self) -> bool: return self.is_variant_end() and not self.kings & self.occupied_co[self.turn] & chess.BB_RANK_8 def is_variant_win(self) -> bool: - return self.is_variant_end() and bool(self.kings & self.occupied_co[self.turn] & chess.BB_RANK_8) + in_goal = self.kings & chess.BB_RANK_8 + return ( + self.is_variant_end() and + bool(in_goal & self.occupied_co[self.turn]) and + not in_goal & self.occupied_co[not self.turn]) def has_insufficient_material(self, color: chess.Color) -> bool: return False @@ -409,7 +397,7 @@ def has_insufficient_material(self, color: chess.Color) -> bool: def status(self) -> chess.Status: status = super().status() if self.is_check(): - status |= chess.STATUS_RACE_CHECK | chess.STATUS_TOO_MANY_CHECKERS + status |= chess.STATUS_RACE_CHECK | chess.STATUS_TOO_MANY_CHECKERS | chess.STATUS_IMPOSSIBLE_CHECK if self.turn == chess.BLACK and all(self.occupied_co[co] & self.kings & chess.BB_RANK_8 for co in chess.COLORS): status |= chess.STATUS_RACE_OVER if self.pawns: @@ -457,8 +445,210 @@ def is_variant_win(self) -> bool: return bool(self.occupied) and not self.occupied_co[not self.turn] def has_insufficient_material(self, color: chess.Color) -> bool: - # TODO: Could detect some cases where the Horde can no longer mate. - return False + # The side with the king can always win by capturing the Horde. + if color == chess.BLACK: + return False + + # See https://github.com/stevepapazis/horde-insufficient-material-tests + # for how the following has been derived. + + white = self.occupied_co[chess.WHITE] + queens = chess.popcount(white & self.queens) + pawns = chess.popcount(white & self.pawns) + rooks = chess.popcount(white & self.rooks) + bishops = chess.popcount(white & self.bishops) + knights = chess.popcount(white & self.knights) + + # Two same color bishops suffice to cover all the light and dark + # squares around the enemy king. + horde_darkb = chess.popcount(chess.BB_DARK_SQUARES & white & self.bishops) + horde_lightb = chess.popcount(chess.BB_LIGHT_SQUARES & white & self.bishops) + horde_bishop_co = chess.WHITE if horde_lightb >= 1 else chess.BLACK + horde_num = ( + pawns + knights + rooks + queens + + (horde_darkb if horde_darkb <= 2 else 2) + + (horde_lightb if horde_lightb <= 2 else 2) + ) + + pieces = self.occupied_co[chess.BLACK] + pieces_pawns = chess.popcount(pieces & self.pawns) + pieces_bishops = chess.popcount(pieces & self.bishops) + pieces_knights = chess.popcount(pieces & self.knights) + pieces_rooks = chess.popcount(pieces & self.rooks) + pieces_queens = chess.popcount(pieces & self.queens) + pieces_darkb = chess.popcount(chess.BB_DARK_SQUARES & pieces & self.bishops) + pieces_lightb = chess.popcount(chess.BB_LIGHT_SQUARES & pieces & self.bishops) + pieces_num = chess.popcount(pieces) + + def pieces_oppositeb_of(square_color: chess.Color) -> int: + return pieces_darkb if square_color == chess.WHITE else pieces_lightb + + def pieces_sameb_as(square_color: chess.Color) -> int: + return pieces_lightb if square_color == chess.WHITE else pieces_darkb + + def pieces_of_type_not(piece: int) -> int: + return pieces_num - piece + + def has_bishop_pair(side: chess.Color) -> bool: + return (horde_lightb >= 1 and horde_darkb >= 1) if side == chess.WHITE else (pieces_lightb >= 1 and pieces_darkb >= 1) + + if horde_num == 0: + return True + if horde_num >= 4: + # Four or more white pieces can always deliver mate. + return False + if (pawns >= 1 or queens >= 1) and horde_num >= 2: + # Pawns/queens are never insufficient material when paired with any other + # piece (a pawn promotes to a queen and delivers mate). + return False + if rooks >= 1 and horde_num >= 2: + # A rook is insufficient material only when it is paired with a bishop + # against a lone king. The horde can mate in any other case. + # A rook on A1 and a bishop on C3 mate a king on B1 when there is a + # friendly pawn/opposite-color-bishop/rook/queen on C2. + # A rook on B8 and a bishop C3 mate a king on A1 when there is a friendly + # knight on A2. + if not (horde_num == 2 and rooks == 1 and bishops == 1 and pieces_of_type_not(pieces_sameb_as(horde_bishop_co)) == 1): + return False + + if horde_num == 1: + if pieces_num == 1: + # A lone piece cannot mate a lone king. + return True + elif queens == 1: + # The horde has a lone queen. + # A lone queen mates a king on A1 bounded by: + # - a pawn/rook on A2 + # - two same color bishops on A2, B1 + # We ignore every other mating case, since it can be reduced to + # the two previous cases (e.g. a black pawn on A2 and a black + # bishop on B1). + return not ( + pieces_pawns >= 1 or + pieces_rooks >= 1 or + pieces_lightb >= 2 or + pieces_darkb >= 2 + ) + elif pawns == 1: + # Promote the pawn to a queen or a knight and check whether + # white can mate. + pawn_square = chess.SquareSet(self.pawns & white).pop() + promote_to_queen = self.copy(stack=False) + promote_to_queen.set_piece_at(pawn_square, chess.Piece(chess.QUEEN, chess.WHITE)) + promote_to_knight = self.copy(stack=False) + promote_to_knight.set_piece_at(pawn_square, chess.Piece(chess.KNIGHT, chess.WHITE)) + return promote_to_queen.has_insufficient_material(chess.WHITE) and promote_to_knight.has_insufficient_material(chess.WHITE) + elif rooks == 1: + # A lone rook mates a king on A8 bounded by a pawn/rook on A7 and a + # pawn/knight on B7. We ignore every other case, since it can be + # reduced to the two previous cases. + # (e.g. three pawns on A7, B7, C7) + return not ( + pieces_pawns >= 2 or + (pieces_rooks >= 1 and pieces_pawns >= 1) or + (pieces_rooks >= 1 and pieces_knights >= 1) or + (pieces_pawns >= 1 and pieces_knights >= 1) + ) + elif bishops == 1: + # The horde has a lone bishop. + return not ( + # The king can be mated on A1 if there is a pawn/opposite-color-bishop + # on A2 and an opposite-color-bishop on B1. + # If black has two or more pawns, white gets the benefit of the doubt; + # there is an outside chance that white promotes its pawns to + # opposite-color-bishops and selfmates theirself. + # Every other case that the king is mated by the bishop requires that + # black has two pawns or two opposite-color-bishop or a pawn and an + # opposite-color-bishop. + # For example a king on A3 can be mated if there is + # a pawn/opposite-color-bishop on A4, a pawn/opposite-color-bishop on + # B3, a pawn/bishop/rook/queen on A2 and any other piece on B2. + pieces_oppositeb_of(horde_bishop_co) >= 2 or + (pieces_oppositeb_of(horde_bishop_co) >= 1 and pieces_pawns >= 1) or + pieces_pawns >= 2 + ) + elif knights == 1: + # The horde has a lone knight. + return not ( + # The king on A1 can be smother mated by a knight on C2 if there is + # a pawn/knight/bishop on B2, a knight/rook on B1 and any other piece + # on A2. + # Moreover, when black has four or more pieces and two of them are + # pawns, black can promote their pawns and selfmate theirself. + pieces_num >= 4 and ( + pieces_knights >= 2 or pieces_pawns >= 2 or + (pieces_rooks >= 1 and pieces_knights >= 1) or + (pieces_rooks >= 1 and pieces_bishops >= 1) or + (pieces_knights >= 1 and pieces_bishops >= 1) or + (pieces_rooks >= 1 and pieces_pawns >= 1) or + (pieces_knights >= 1 and pieces_pawns >= 1) or + (pieces_bishops >= 1 and pieces_pawns >= 1) or + (has_bishop_pair(chess.BLACK) and pieces_pawns >= 1) + ) and + (pieces_of_type_not(pieces_darkb) >= 3 if pieces_darkb >= 2 else True) and + (pieces_of_type_not(pieces_lightb) >= 3 if pieces_lightb >= 2 else True) + ) + elif horde_num == 2: # By this point, we only need to deal with white's minor pieces. + if pieces_num == 1: + # Two minor pieces cannot mate a lone king. + return True + elif knights == 2: + # A king on A1 is mated by two knights, if it is obstructed by a + # pawn/bishop/knight on B2. On the other hand, if black only has + # major pieces it is a draw. + return not (pieces_pawns + pieces_bishops + pieces_knights >= 1) + elif has_bishop_pair(chess.WHITE): + return not ( + # A king on A1 obstructed by a pawn/bishop on A2 is mated + # by the bishop pair. + pieces_pawns >= 1 or pieces_bishops >= 1 or + # A pawn/bishop/knight on B4, a pawn/bishop/rook/queen on + # A4 and the king on A3 enable Boden's mate by the bishop + # pair. In every other case white cannot win. + (pieces_knights >= 1 and pieces_rooks + pieces_queens >= 1) + ) + elif bishops >= 1 and knights >= 1: + # The horde has a bishop and a knight. + return not ( + # A king on A1 obstructed by a pawn/opposite-color-bishop on + # A2 is mated by a knight on D2 and a bishop on C3. + pieces_pawns >= 1 or pieces_oppositeb_of(horde_bishop_co) >= 1 or + # A king on A1 bounded by two friendly pieces on A2 and B1 is + # mated when the knight moves from D4 to C2 so that both the + # knight and the bishop deliver check. + pieces_of_type_not(pieces_sameb_as(horde_bishop_co)) >= 3 + ) + else: + # The horde has two or more bishops on the same color. + # White can only win if black has enough material to obstruct + # the squares of the opposite color around the king. + return not ( + # A king on A1 obstructed by a pawn/opposite-bishop/knight + # on A2 and a opposite-bishop/knight on B1 is mated by two + # bishops on B2 and C3. This position is theoretically + # achievable even when black has two pawns or when they + # have a pawn and an opposite color bishop. + (pieces_pawns >= 1 and pieces_oppositeb_of(horde_bishop_co) >= 1) or + (pieces_pawns >= 1 and pieces_knights >= 1) or + (pieces_oppositeb_of(horde_bishop_co) >= 1 and pieces_knights >= 1) or + (pieces_oppositeb_of(horde_bishop_co) >= 2) or + pieces_knights >= 2 or + pieces_pawns >= 2 + # In every other case, white can only draw. + ) + elif horde_num == 3: + # A king in the corner is mated by two knights and a bishop or three + # knights or the bishop pair and a knight/bishop. + if (knights == 2 and bishops == 1) or knights == 3 or has_bishop_pair(chess.WHITE): + return False + else: + # White has two same color bishops and a knight. + # A king on A1 is mated by a bishop on B2, a bishop on C1 and a + # knight on C3, as long as there is another black piece to waste + # a tempo. + return pieces_num == 1 + + return True def status(self) -> chess.Status: status = super().status() @@ -479,20 +669,18 @@ def status(self) -> chess.Status: ThreeCheckBoardT = TypeVar("ThreeCheckBoardT", bound="ThreeCheckBoard") -class _ThreeCheckBoardState(Generic[ThreeCheckBoardT], chess._BoardState["ThreeCheckBoardT"]): - def __init__(self, board: "ThreeCheckBoardT") -> None: - super().__init__(board) +class _ThreeCheckBoardState: + def __init__(self, board: ThreeCheckBoard) -> None: self.remaining_checks_w = board.remaining_checks[chess.WHITE] self.remaining_checks_b = board.remaining_checks[chess.BLACK] - def restore(self, board: "ThreeCheckBoardT") -> None: - super().restore(board) + def restore(self, board: ThreeCheckBoard) -> None: board.remaining_checks[chess.WHITE] = self.remaining_checks_w board.remaining_checks[chess.BLACK] = self.remaining_checks_b class ThreeCheckBoard(chess.Board): - aliases = ["Three-check", "Three check", "Threecheck", "Three check chess", "3-check"] + aliases = ["Three-check", "Three check", "Threecheck", "Three check chess", "3-check", "3 check", "3check"] uci_variant = "3check" xboard_variant = "3check" starting_fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 3+3 0 1" @@ -504,8 +692,13 @@ class ThreeCheckBoard(chess.Board): def __init__(self, fen: Optional[str] = starting_fen, chess960: bool = False) -> None: self.remaining_checks = [3, 3] + self._three_check_stack: List[_ThreeCheckBoardState] = [] super().__init__(fen, chess960=chess960) + def clear_stack(self) -> None: + super().clear_stack() + self._three_check_stack.clear() + def reset_board(self) -> None: super().reset_board() self.remaining_checks[chess.WHITE] = 3 @@ -516,14 +709,17 @@ def clear_board(self) -> None: self.remaining_checks[chess.WHITE] = 3 self.remaining_checks[chess.BLACK] = 3 - def _board_state(self: ThreeCheckBoardT) -> _ThreeCheckBoardState[ThreeCheckBoardT]: - return _ThreeCheckBoardState(self) - def push(self, move: chess.Move) -> None: + self._three_check_stack.append(_ThreeCheckBoardState(self)) super().push(move) if self.is_check(): self.remaining_checks[not self.turn] -= 1 + def pop(self) -> chess.Move: + move = super().pop() + self._three_check_stack.pop().restore(self) + return move + def has_insufficient_material(self, color: chess.Color) -> bool: # Any remaining piece can give check. return not (self.occupied_co[color] & ~self.kings) @@ -568,7 +764,7 @@ def set_fen(self, fen: str) -> None: self.remaining_checks[chess.WHITE] = wc self.remaining_checks[chess.BLACK] = bc - def epd(self, shredder: bool = False, en_passant: str = "legal", promoted: Optional[bool] = None, **operations: Union[None, str, int, float, chess.Move, Iterable[chess.Move]]) -> str: + def epd(self, shredder: bool = False, en_passant: chess.EnPassantSpec = "legal", promoted: Optional[bool] = None, **operations: Union[None, str, int, float, chess.Move, Iterable[chess.Move]]) -> str: epd = [super().epd(shredder=shredder, en_passant=en_passant, promoted=promoted), "{:d}+{:d}".format(max(self.remaining_checks[chess.WHITE], 0), max(self.remaining_checks[chess.BLACK], 0))] @@ -589,24 +785,29 @@ def is_variant_win(self) -> bool: return self.remaining_checks[self.turn] <= 0 < self.remaining_checks[not self.turn] def is_irreversible(self, move: chess.Move) -> bool: - if super().is_irreversible(move): - return True - - self.push(move) - gives_check = self.is_check() - self.pop() - return gives_check + return super().is_irreversible(move) or self.gives_check(move) def _transposition_key(self) -> Hashable: return (super()._transposition_key(), self.remaining_checks[chess.WHITE], self.remaining_checks[chess.BLACK]) - def copy(self: ThreeCheckBoardT, stack: Union[bool, int] = True) -> ThreeCheckBoardT: + def copy(self, *, stack: Union[bool, int] = True) -> Self: board = super().copy(stack=stack) board.remaining_checks = self.remaining_checks.copy() + if stack: + stack = len(self.move_stack) if stack is True else stack + board._three_check_stack = self._three_check_stack[-stack:] return board - def mirror(self: ThreeCheckBoardT) -> ThreeCheckBoardT: + def root(self) -> Self: + if self._three_check_stack: + board = super().root() + self._three_check_stack[0].restore(board) + return board + else: + return self.copy(stack=False) + + def mirror(self) -> Self: board = super().mirror() board.remaining_checks[chess.WHITE] = self.remaining_checks[chess.BLACK] board.remaining_checks[chess.BLACK] = self.remaining_checks[chess.WHITE] @@ -615,16 +816,14 @@ def mirror(self: ThreeCheckBoardT) -> ThreeCheckBoardT: CrazyhouseBoardT = TypeVar("CrazyhouseBoardT", bound="CrazyhouseBoard") -class _CrazyhouseBoardState(Generic[CrazyhouseBoardT], chess._BoardState["CrazyhouseBoardT"]): - def __init__(self, board: "CrazyhouseBoardT") -> None: - super().__init__(board) +class _CrazyhouseBoardState: + def __init__(self, board: CrazyhouseBoard) -> None: self.pockets_w = board.pockets[chess.WHITE].copy() self.pockets_b = board.pockets[chess.BLACK].copy() - def restore(self, board: "CrazyhouseBoardT") -> None: - super().restore(board) - board.pockets[chess.WHITE] = self.pockets_w.copy() - board.pockets[chess.BLACK] = self.pockets_b.copy() + def restore(self, board: CrazyhouseBoard) -> None: + board.pockets[chess.WHITE] = self.pockets_w + board.pockets[chess.BLACK] = self.pockets_b CrazyhousePocketT = TypeVar("CrazyhousePocketT", bound="CrazyhousePocket") @@ -632,39 +831,40 @@ class CrazyhousePocket: """A Crazyhouse pocket with a counter for each piece type.""" def __init__(self, symbols: Iterable[str] = "") -> None: - self.pieces: Dict[chess.PieceType, int] = {} + self.reset() for symbol in symbols: self.add(chess.PIECE_SYMBOLS.index(symbol)) + def reset(self) -> None: + """Clears the pocket.""" + self._pieces = [-1, 0, 0, 0, 0, 0, 0] + def add(self, piece_type: chess.PieceType) -> None: """Adds a piece of the given type to this pocket.""" - self.pieces[piece_type] = self.pieces.get(piece_type, 0) + 1 + self._pieces[piece_type] += 1 def remove(self, piece_type: chess.PieceType) -> None: """Removes a piece of the given type from this pocket.""" - self.pieces[piece_type] -= 1 + assert self._pieces[piece_type], f"cannot remove {chess.piece_symbol(piece_type)} from {self!r}" + self._pieces[piece_type] -= 1 def count(self, piece_type: chess.PieceType) -> int: """Returns the number of pieces of the given type in the pocket.""" - return self.pieces.get(piece_type, 0) - - def reset(self) -> None: - """Clears the pocket.""" - self.pieces.clear() + return self._pieces[piece_type] def __str__(self) -> str: return "".join(chess.piece_symbol(pt) * self.count(pt) for pt in reversed(chess.PIECE_TYPES)) def __len__(self) -> int: - return sum(self.pieces.values()) + return sum(self._pieces[1:]) def __repr__(self) -> str: return f"CrazyhousePocket('{self}')" - def copy(self: CrazyhousePocketT) -> CrazyhousePocketT: + def copy(self) -> Self: """Returns a copy of this pocket.""" pocket = type(self)() - pocket.pieces = copy.copy(self.pieces) + pocket._pieces = self._pieces[:] return pocket class CrazyhouseBoard(chess.Board): @@ -681,8 +881,13 @@ class CrazyhouseBoard(chess.Board): def __init__(self, fen: Optional[str] = starting_fen, chess960: bool = False) -> None: self.pockets = [CrazyhousePocket(), CrazyhousePocket()] + self._crazyhouse_stack: List[_CrazyhouseBoardState] = [] super().__init__(fen, chess960=chess960) + def clear_stack(self) -> None: + super().clear_stack() + self._crazyhouse_stack.clear() + def reset_board(self) -> None: super().reset_board() self.pockets[chess.WHITE].reset() @@ -693,10 +898,8 @@ def clear_board(self) -> None: self.pockets[chess.WHITE].reset() self.pockets[chess.BLACK].reset() - def _board_state(self: CrazyhouseBoardT) -> _CrazyhouseBoardState[CrazyhouseBoardT]: - return _CrazyhouseBoardState(self) - def push(self, move: chess.Move) -> None: + self._crazyhouse_stack.append(_CrazyhouseBoardState(self)) super().push(move) if move.drop: self.pockets[not self.turn].remove(move.drop) @@ -707,18 +910,23 @@ def _push_capture(self, move: chess.Move, capture_square: chess.Square, piece_ty else: self.pockets[self.turn].add(piece_type) - def can_claim_fifty_moves(self) -> bool: - return False + def pop(self) -> chess.Move: + move = super().pop() + self._crazyhouse_stack.pop().restore(self) + return move - def is_seventyfive_moves(self) -> bool: + def _is_halfmoves(self, n: int) -> bool: + # No draw by 50-move rule or 75-move rule. return False def is_irreversible(self, move: chess.Move) -> bool: return self._reduces_castling_rights(move) + def _effective_promoted(self) -> chess.Bitboard: + return self.promoted & ~self.kings & ~self.pawns + def _transposition_key(self) -> Hashable: return (super()._transposition_key(), - self.promoted, str(self.pockets[chess.WHITE]), str(self.pockets[chess.BLACK])) def legal_drop_squares_mask(self) -> chess.Bitboard: @@ -736,6 +944,15 @@ def legal_drop_squares_mask(self) -> chess.Bitboard: return chess.BB_EMPTY def legal_drop_squares(self) -> chess.SquareSet: + """ + Gets the squares where the side to move could legally drop a piece. + Does *not* check whether they actually have a suitable piece in their + pocket. + + It is legal to drop a checkmate. + + Returns a :class:`set of squares `. + """ return chess.SquareSet(self.legal_drop_squares_mask()) def is_pseudo_legal(self, move: chess.Move) -> bool: @@ -755,9 +972,9 @@ def is_legal(self, move: chess.Move) -> bool: return super().is_legal(move) def generate_pseudo_legal_drops(self, to_mask: chess.Bitboard = chess.BB_ALL) -> Iterator[chess.Move]: - for to_square in chess.scan_forward(to_mask & ~self.occupied): - for pt, count in self.pockets[self.turn].pieces.items(): - if count and (pt != chess.PAWN or not chess.BB_BACKRANKS & chess.BB_SQUARES[to_square]): + for pt in chess.PIECE_TYPES: + if self.pockets[self.turn].count(pt): + for to_square in chess.scan_forward(to_mask & ~self.occupied & (~chess.BB_BACKRANKS if pt == chess.PAWN else chess.BB_ALL)): yield chess.Move(to_square, to_square, drop=pt) def generate_legal_drops(self, to_mask: chess.Bitboard = chess.BB_ALL) -> Iterator[chess.Move]: @@ -770,23 +987,23 @@ def generate_legal_moves(self, from_mask: chess.Bitboard = chess.BB_ALL, to_mask def parse_san(self, san: str) -> chess.Move: if "@" in san: - uci = san.rstrip("+# ") + uci = san.rstrip("+#") if uci[0] == "@": uci = "P" + uci move = chess.Move.from_uci(uci) if not self.is_legal(move): - raise ValueError(f"illegal drop san: {san!r} in {self.fen()}") + raise chess.IllegalMoveError(f"illegal drop san: {san!r} in {self.fen()}") return move else: return super().parse_san(san) def has_insufficient_material(self, color: chess.Color) -> bool: - # In practise no material can leave the game, but this is easy to - # implement anyway. Note that bishops can be captured and put onto + # In practice, no material can leave the game, but this is easy to + # implement, anyway. Note that bishops can be captured and put onto # a different color complex. return ( chess.popcount(self.occupied) + sum(len(pocket) for pocket in self.pockets) <= 3 and - not self.promoted and + not self._effective_promoted() and not self.pawns and not self.rooks and not self.queens and @@ -818,23 +1035,29 @@ def set_fen(self, fen: str) -> None: self.pockets[chess.WHITE] = white_pocket self.pockets[chess.BLACK] = black_pocket - def board_fen(self, promoted: Optional[bool] = None) -> str: - if promoted is None: - promoted = True - return super().board_fen(promoted=promoted) - - def epd(self, shredder: bool = False, en_passant: str = "legal", promoted: Optional[bool] = None, **operations: Union[None, str, int, float, chess.Move, Iterable[chess.Move]]) -> str: + def epd(self, shredder: bool = False, en_passant: chess.EnPassantSpec = "legal", promoted: Optional[bool] = None, **operations: Union[None, str, int, float, chess.Move, Iterable[chess.Move]]) -> str: epd = super().epd(shredder=shredder, en_passant=en_passant, promoted=promoted) board_part, info_part = epd.split(" ", 1) return f"{board_part}[{str(self.pockets[chess.WHITE]).upper()}{self.pockets[chess.BLACK]}] {info_part}" - def copy(self: CrazyhouseBoardT, stack: Union[bool, int] = True) -> CrazyhouseBoardT: + def copy(self, *, stack: Union[bool, int] = True) -> Self: board = super().copy(stack=stack) board.pockets[chess.WHITE] = self.pockets[chess.WHITE].copy() board.pockets[chess.BLACK] = self.pockets[chess.BLACK].copy() + if stack: + stack = len(self.move_stack) if stack is True else stack + board._crazyhouse_stack = self._crazyhouse_stack[-stack:] return board - def mirror(self: CrazyhouseBoardT) -> CrazyhouseBoardT: + def root(self) -> Self: + if self._crazyhouse_stack: + board = super().root() + self._crazyhouse_stack[0].restore(board) + return board + else: + return self.copy(stack=False) + + def mirror(self) -> Self: board = super().mirror() board.pockets[chess.WHITE] = self.pockets[chess.BLACK].copy() board.pockets[chess.BLACK] = self.pockets[chess.WHITE].copy() @@ -867,7 +1090,10 @@ def status(self) -> chess.Status: def find_variant(name: str) -> Type[chess.Board]: - """Looks for a variant board class by variant name.""" + """ + Looks for a variant board class by variant name. Supports many common + aliases. + """ for variant in VARIANTS: if any(alias.lower() == name.lower() for alias in variant.aliases): return variant diff --git a/data/gaviota/SOURCE.txt b/data/gaviota/SOURCE.txt index dda9c83c7..706756322 100644 --- a/data/gaviota/SOURCE.txt +++ b/data/gaviota/SOURCE.txt @@ -1,145 +1,145 @@ -https://syzygy-tables.info/gaviota/kbk.gtb.cp4 -https://syzygy-tables.info/gaviota/knk.gtb.cp4 -https://syzygy-tables.info/gaviota/kpk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqk.gtb.cp4 -https://syzygy-tables.info/gaviota/krk.gtb.cp4 -https://syzygy-tables.info/gaviota/kbbk.gtb.cp4 -https://syzygy-tables.info/gaviota/kbkb.gtb.cp4 -https://syzygy-tables.info/gaviota/kbkn.gtb.cp4 -https://syzygy-tables.info/gaviota/kbkp.gtb.cp4 -https://syzygy-tables.info/gaviota/kbnk.gtb.cp4 -https://syzygy-tables.info/gaviota/kbpk.gtb.cp4 -https://syzygy-tables.info/gaviota/knkn.gtb.cp4 -https://syzygy-tables.info/gaviota/knkp.gtb.cp4 -https://syzygy-tables.info/gaviota/knnk.gtb.cp4 -https://syzygy-tables.info/gaviota/knpk.gtb.cp4 -https://syzygy-tables.info/gaviota/kpkp.gtb.cp4 -https://syzygy-tables.info/gaviota/kppk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqbk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqkb.gtb.cp4 -https://syzygy-tables.info/gaviota/kqkn.gtb.cp4 -https://syzygy-tables.info/gaviota/kqkp.gtb.cp4 -https://syzygy-tables.info/gaviota/kqkq.gtb.cp4 -https://syzygy-tables.info/gaviota/kqkr.gtb.cp4 -https://syzygy-tables.info/gaviota/kqnk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqpk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqqk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqrk.gtb.cp4 -https://syzygy-tables.info/gaviota/krbk.gtb.cp4 -https://syzygy-tables.info/gaviota/krkb.gtb.cp4 -https://syzygy-tables.info/gaviota/krkn.gtb.cp4 -https://syzygy-tables.info/gaviota/krkp.gtb.cp4 -https://syzygy-tables.info/gaviota/krkr.gtb.cp4 -https://syzygy-tables.info/gaviota/krnk.gtb.cp4 -https://syzygy-tables.info/gaviota/krpk.gtb.cp4 -https://syzygy-tables.info/gaviota/krrk.gtb.cp4 -https://syzygy-tables.info/gaviota/kbbbk.gtb.cp4 -https://syzygy-tables.info/gaviota/kbbkb.gtb.cp4 -https://syzygy-tables.info/gaviota/kbbkn.gtb.cp4 -https://syzygy-tables.info/gaviota/kbbkp.gtb.cp4 -https://syzygy-tables.info/gaviota/kbbkq.gtb.cp4 -https://syzygy-tables.info/gaviota/kbbkr.gtb.cp4 -https://syzygy-tables.info/gaviota/kbbnk.gtb.cp4 -https://syzygy-tables.info/gaviota/kbbpk.gtb.cp4 -https://syzygy-tables.info/gaviota/kbnkb.gtb.cp4 -https://syzygy-tables.info/gaviota/kbnkn.gtb.cp4 -https://syzygy-tables.info/gaviota/kbnkp.gtb.cp4 -https://syzygy-tables.info/gaviota/kbnkq.gtb.cp4 -https://syzygy-tables.info/gaviota/kbnkr.gtb.cp4 -https://syzygy-tables.info/gaviota/kbnnk.gtb.cp4 -https://syzygy-tables.info/gaviota/kbnpk.gtb.cp4 -https://syzygy-tables.info/gaviota/kbpkb.gtb.cp4 -https://syzygy-tables.info/gaviota/kbpkn.gtb.cp4 -https://syzygy-tables.info/gaviota/kbpkp.gtb.cp4 -https://syzygy-tables.info/gaviota/kbpkq.gtb.cp4 -https://syzygy-tables.info/gaviota/kbpkr.gtb.cp4 -https://syzygy-tables.info/gaviota/kbppk.gtb.cp4 -https://syzygy-tables.info/gaviota/knnkb.gtb.cp4 -https://syzygy-tables.info/gaviota/knnkn.gtb.cp4 -https://syzygy-tables.info/gaviota/knnkp.gtb.cp4 -https://syzygy-tables.info/gaviota/knnkq.gtb.cp4 -https://syzygy-tables.info/gaviota/knnkr.gtb.cp4 -https://syzygy-tables.info/gaviota/knnnk.gtb.cp4 -https://syzygy-tables.info/gaviota/knnpk.gtb.cp4 -https://syzygy-tables.info/gaviota/knpkb.gtb.cp4 -https://syzygy-tables.info/gaviota/knpkn.gtb.cp4 -https://syzygy-tables.info/gaviota/knpkp.gtb.cp4 -https://syzygy-tables.info/gaviota/knpkq.gtb.cp4 -https://syzygy-tables.info/gaviota/knpkr.gtb.cp4 -https://syzygy-tables.info/gaviota/knppk.gtb.cp4 -https://syzygy-tables.info/gaviota/kppkb.gtb.cp4 -https://syzygy-tables.info/gaviota/kppkn.gtb.cp4 -https://syzygy-tables.info/gaviota/kppkp.gtb.cp4 -https://syzygy-tables.info/gaviota/kppkq.gtb.cp4 -https://syzygy-tables.info/gaviota/kppkr.gtb.cp4 -https://syzygy-tables.info/gaviota/kpppk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqbbk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqbkb.gtb.cp4 -https://syzygy-tables.info/gaviota/kqbkn.gtb.cp4 -https://syzygy-tables.info/gaviota/kqbkp.gtb.cp4 -https://syzygy-tables.info/gaviota/kqbkq.gtb.cp4 -https://syzygy-tables.info/gaviota/kqbkr.gtb.cp4 -https://syzygy-tables.info/gaviota/kqbnk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqbpk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqnkb.gtb.cp4 -https://syzygy-tables.info/gaviota/kqnkn.gtb.cp4 -https://syzygy-tables.info/gaviota/kqnkp.gtb.cp4 -https://syzygy-tables.info/gaviota/kqnkq.gtb.cp4 -https://syzygy-tables.info/gaviota/kqnkr.gtb.cp4 -https://syzygy-tables.info/gaviota/kqnnk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqnpk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqpkb.gtb.cp4 -https://syzygy-tables.info/gaviota/kqpkn.gtb.cp4 -https://syzygy-tables.info/gaviota/kqpkp.gtb.cp4 -https://syzygy-tables.info/gaviota/kqpkq.gtb.cp4 -https://syzygy-tables.info/gaviota/kqpkr.gtb.cp4 -https://syzygy-tables.info/gaviota/kqppk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqqbk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqqkb.gtb.cp4 -https://syzygy-tables.info/gaviota/kqqkn.gtb.cp4 -https://syzygy-tables.info/gaviota/kqqkp.gtb.cp4 -https://syzygy-tables.info/gaviota/kqqkq.gtb.cp4 -https://syzygy-tables.info/gaviota/kqqkr.gtb.cp4 -https://syzygy-tables.info/gaviota/kqqnk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqqpk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqqqk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqqrk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqrbk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqrkb.gtb.cp4 -https://syzygy-tables.info/gaviota/kqrkn.gtb.cp4 -https://syzygy-tables.info/gaviota/kqrkp.gtb.cp4 -https://syzygy-tables.info/gaviota/kqrkq.gtb.cp4 -https://syzygy-tables.info/gaviota/kqrkr.gtb.cp4 -https://syzygy-tables.info/gaviota/kqrnk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqrpk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqrrk.gtb.cp4 -https://syzygy-tables.info/gaviota/krbbk.gtb.cp4 -https://syzygy-tables.info/gaviota/krbkb.gtb.cp4 -https://syzygy-tables.info/gaviota/krbkn.gtb.cp4 -https://syzygy-tables.info/gaviota/krbkp.gtb.cp4 -https://syzygy-tables.info/gaviota/krbkq.gtb.cp4 -https://syzygy-tables.info/gaviota/krbkr.gtb.cp4 -https://syzygy-tables.info/gaviota/krbnk.gtb.cp4 -https://syzygy-tables.info/gaviota/krbpk.gtb.cp4 -https://syzygy-tables.info/gaviota/krnkb.gtb.cp4 -https://syzygy-tables.info/gaviota/krnkn.gtb.cp4 -https://syzygy-tables.info/gaviota/krnkp.gtb.cp4 -https://syzygy-tables.info/gaviota/krnkq.gtb.cp4 -https://syzygy-tables.info/gaviota/krnkr.gtb.cp4 -https://syzygy-tables.info/gaviota/krnnk.gtb.cp4 -https://syzygy-tables.info/gaviota/krnpk.gtb.cp4 -https://syzygy-tables.info/gaviota/krpkb.gtb.cp4 -https://syzygy-tables.info/gaviota/krpkn.gtb.cp4 -https://syzygy-tables.info/gaviota/krpkp.gtb.cp4 -https://syzygy-tables.info/gaviota/krpkq.gtb.cp4 -https://syzygy-tables.info/gaviota/krpkr.gtb.cp4 -https://syzygy-tables.info/gaviota/krppk.gtb.cp4 -https://syzygy-tables.info/gaviota/krrbk.gtb.cp4 -https://syzygy-tables.info/gaviota/krrkb.gtb.cp4 -https://syzygy-tables.info/gaviota/krrkn.gtb.cp4 -https://syzygy-tables.info/gaviota/krrkp.gtb.cp4 -https://syzygy-tables.info/gaviota/krrkq.gtb.cp4 -https://syzygy-tables.info/gaviota/krrkr.gtb.cp4 -https://syzygy-tables.info/gaviota/krrnk.gtb.cp4 -https://syzygy-tables.info/gaviota/krrpk.gtb.cp4 -https://syzygy-tables.info/gaviota/krrrk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kpk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbbk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbkb.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbnk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbpk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knnk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knpk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kpkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kppk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqbk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqkb.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqkq.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqkr.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqnk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqpk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqqk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqrk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krbk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krkb.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krkr.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krnk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krpk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krrk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbbbk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbbkb.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbbkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbbkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbbkq.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbbkr.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbbnk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbbpk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbnkb.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbnkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbnkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbnkq.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbnkr.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbnnk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbnpk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbpkb.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbpkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbpkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbpkq.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbpkr.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbppk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knnkb.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knnkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knnkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knnkq.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knnkr.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knnnk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knnpk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knpkb.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knpkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knpkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knpkq.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knpkr.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knppk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kppkb.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kppkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kppkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kppkq.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kppkr.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kpppk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqbbk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqbkb.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqbkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqbkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqbkq.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqbkr.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqbnk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqbpk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqnkb.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqnkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqnkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqnkq.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqnkr.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqnnk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqnpk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqpkb.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqpkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqpkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqpkq.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqpkr.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqppk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqqbk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqqkb.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqqkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqqkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqqkq.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqqkr.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqqnk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqqpk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqqqk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqqrk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqrbk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqrkb.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqrkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqrkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqrkq.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqrkr.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqrnk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqrpk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqrrk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krbbk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krbkb.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krbkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krbkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krbkq.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krbkr.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krbnk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krbpk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krnkb.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krnkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krnkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krnkq.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krnkr.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krnnk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krnpk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krpkb.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krpkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krpkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krpkq.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krpkr.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krppk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krrbk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krrkb.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krrkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krrkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krrkq.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krrkr.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krrnk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krrpk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krrrk.gtb.cp4 diff --git a/data/gaviota/TEST-SOURCE.txt b/data/gaviota/TEST-SOURCE.txt index f7d29a546..36c83d39a 100644 --- a/data/gaviota/TEST-SOURCE.txt +++ b/data/gaviota/TEST-SOURCE.txt @@ -1,47 +1,47 @@ -https://syzygy-tables.info/gaviota/kbbk.gtb.cp4 -https://syzygy-tables.info/gaviota/kbkb.gtb.cp4 -https://syzygy-tables.info/gaviota/kbkn.gtb.cp4 -https://syzygy-tables.info/gaviota/kbkp.gtb.cp4 -https://syzygy-tables.info/gaviota/kbnk.gtb.cp4 -https://syzygy-tables.info/gaviota/kbpk.gtb.cp4 -https://syzygy-tables.info/gaviota/knkn.gtb.cp4 -https://syzygy-tables.info/gaviota/knkp.gtb.cp4 -https://syzygy-tables.info/gaviota/knnk.gtb.cp4 -https://syzygy-tables.info/gaviota/knpk.gtb.cp4 -https://syzygy-tables.info/gaviota/kpkp.gtb.cp4 -https://syzygy-tables.info/gaviota/kppk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqbk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqkb.gtb.cp4 -https://syzygy-tables.info/gaviota/kqkn.gtb.cp4 -https://syzygy-tables.info/gaviota/kqkp.gtb.cp4 -https://syzygy-tables.info/gaviota/kqkq.gtb.cp4 -https://syzygy-tables.info/gaviota/kqkr.gtb.cp4 -https://syzygy-tables.info/gaviota/kqnk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqpk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqqk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqrk.gtb.cp4 -https://syzygy-tables.info/gaviota/krbk.gtb.cp4 -https://syzygy-tables.info/gaviota/krkb.gtb.cp4 -https://syzygy-tables.info/gaviota/krkn.gtb.cp4 -https://syzygy-tables.info/gaviota/krkp.gtb.cp4 -https://syzygy-tables.info/gaviota/krkr.gtb.cp4 -https://syzygy-tables.info/gaviota/krnk.gtb.cp4 -https://syzygy-tables.info/gaviota/krpk.gtb.cp4 -https://syzygy-tables.info/gaviota/krrk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqrnk.gtb.cp4 -https://syzygy-tables.info/gaviota/krpkn.gtb.cp4 -https://syzygy-tables.info/gaviota/kbpkp.gtb.cp4 -https://syzygy-tables.info/gaviota/krrpk.gtb.cp4 -https://syzygy-tables.info/gaviota/knppk.gtb.cp4 -https://syzygy-tables.info/gaviota/kbnkb.gtb.cp4 -https://syzygy-tables.info/gaviota/knnkn.gtb.cp4 -https://syzygy-tables.info/gaviota/krrnk.gtb.cp4 -https://syzygy-tables.info/gaviota/kqbbk.gtb.cp4 -https://syzygy-tables.info/gaviota/kppkp.gtb.cp4 -https://syzygy-tables.info/gaviota/kppkq.gtb.cp4 -https://syzygy-tables.info/gaviota/krrrk.gtb.cp4 -https://syzygy-tables.info/gaviota/krnkp.gtb.cp4 -https://syzygy-tables.info/gaviota/kpppk.gtb.cp4 -https://syzygy-tables.info/gaviota/knnkp.gtb.cp4 -https://syzygy-tables.info/gaviota/krnpk.gtb.cp4 -https://syzygy-tables.info/gaviota/knpkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbbk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbkb.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbnk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbpk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knnk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knpk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kpkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kppk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqbk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqkb.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqkq.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqkr.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqnk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqpk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqqk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqrk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krbk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krkb.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krkr.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krnk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krpk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krrk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqrnk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krpkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbpkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krrpk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knppk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kbnkb.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knnkn.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krrnk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kqbbk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kppkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kppkq.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krrrk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krnkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/kpppk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knnkp.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/krnpk.gtb.cp4 +https://tablebase.lichess.ovh/tables/standard/Gaviota/knpkp.gtb.cp4 diff --git a/data/pgn/chessbase-empty-line.pgn b/data/pgn/chessbase-empty-line.pgn new file mode 100644 index 000000000..f343a3d47 --- /dev/null +++ b/data/pgn/chessbase-empty-line.pgn @@ -0,0 +1,35 @@ +[Event "AlphaZero vs. Stockfish"] + +[Date "2017.12.04"] +[Round "1"] +[White "Stockfish 8"] +[Black "AlphaZero"] +[Result "0-1"] +[Board "1"] +[WhiteCountry "NOR"] +[WhiteFideId "stockfish"] +[BlackCountry "ENG"] +[BlackFideId "deepmind"] + +1. e4 e5 2. Nf3 Nc6 3. Bb5 Nf6 4. d3 Bc5 + 5. Bxc6 dxc6 6. O-O Nd7 7. Nbd2 O-O 8. +Qe1 f6 9. Nc4 Rf7 10. a4 Bf8 11. Kh1 +Nc5 12. a5 Ne6 13. Ncxe5 + fxe5 14. Nxe5 Rf6 15. Ng4 Rf7 16. Ne5 +Re7 17. a6 c5 18. f4 Qe8 19. axb7 Bxb7 +20. Qa5 Nd4 21. Qc3 Re6 22. Be3 Rb6 23. Nc4 +Rb4 24. b3 a5 25. Rxa5 Rxa5 26. Nxa5 Ba6 +27. Bxd4 Rxd4 28. Nc4 Rd8 + 29. g3 h6 30. Qa5 Bc8 31. Qxc7 Bh3 32. +Rg1 Rd7 33. Qe5 Qxe5 34. Nxe5 Ra7 35. Nc4 +g5 36. Rc1 Bg7 37. Ne5 Ra8 38. Nf3 Bb2 39. +Rb1 Bc3 40. Ng1 Bd7 41. Ne2 Bd2 42. Rd1 +Be3 43. Kg2 Bg4 44. Re1 Bd2 45. Rf1 Ra2 46. +h3 Bxe2 47. Rf2 Bxf4 48. Rxe2 Be5 49. Rf2 +Kg7 50. g4 Bd4 51. Re2 Kf6 52. e5+ Bxe5 53. +Kf3 Ra1 54. Rf2 Re1 55. Kg2+ Bf4 56. c3 +Rc1 57. d4 Rxc3 58. dxc5 + Rxc5 59. b4 Rc3 60. h4 Ke5 61. hxg5 +hxg5 62. Re2+ Kf6 63. Kf2 + Be5 64. Ra2 Rc4 65. Ra6+ Ke7 66. Ra5 +Ke6 67. Ra6+ Bd6 0-1 diff --git a/data/pgn/nepomniachtchi-liren-game1.pgn b/data/pgn/nepomniachtchi-liren-game1.pgn new file mode 100644 index 000000000..ea3234154 --- /dev/null +++ b/data/pgn/nepomniachtchi-liren-game1.pgn @@ -0,0 +1,22 @@ +[Event "FIDE World Championship 2023"] +[Site "Astana KAZ"] +[Date "2023.04.09"] +[Round "1"] +[White "Nepomniachtchi, Ian"] +[Black "Liren, Ding"] +[Result "1/2-1/2"] +[TimeControl "40/7200:20/3600:900+30"] +[WhiteFideId "4168119"] +[BlackFideId "8603677"] +[WhiteElo "2795"] +[BlackElo "2788"] + +1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 6. Bxc6 dxc6 +7. Re1 Nd7 8. d4 exd4 9. Qxd4 O-O 10. Bf4 Nc5 11. Qe3 Bg4 12. Nd4 +Qd7 13. Nc3 Rad8 14. Nf5 Ne6 15. Nxe7+ Qxe7 16. Bg3 Bh5 17. f3 f6 +18. h3 h6 19. Kh2 Bf7 20. Rad1 b6 21. a3 a5 22. Ne2 Rxd1 23. Rxd1 +Rd8 24. Rd3 c5 25. Qd2 c6 26. Rxd8+ Nxd8 27. Qf4 b5 28. Qb8 Kh7 +29. Bd6 Qd7 30. Ng3 Ne6 31. f4 h5 32. c3 c4 33. h4 Qd8 34. Qb7 +Be8 35. Nf5 Qd7 36. Qb8 Qd8 37. Qxd8 Nxd8 38. Nd4 Nb7 39. e5 Kg8 +40. Kg3 Bd7 41. Bc7 Nc5 42. Bxa5 Kf7 43. Bb4 Nd3 44. e6+ Bxe6 +45. Nxc6 Bd7 46. Nd4 Nxb2 47. Kf3 Nd3 48. g3 Nc1 49. Ke3 1/2-1/2 diff --git a/data/pgn/uci-moves.pgn b/data/pgn/uci-moves.pgn new file mode 100644 index 000000000..32c70f235 --- /dev/null +++ b/data/pgn/uci-moves.pgn @@ -0,0 +1,9 @@ +[Event "CCRL 40/4"] +[Site "CCRL"] +[Date "2006.05.24"] +[Round "1"] +[White "Aristarch 4.50"] +[Black "Chess Tiger 15"] +[Result "1/2-1/2"] + +g1f3 c7c5 e2e4 d7d6 d2d4 c5d4 f3d4 g8f6 b1c3 e7e6 c1e3 a7a6 g2g4 h7h6 d1f3 b8d7 f1e2 d8c7 f3h3 d6d5 e4d5 f8b4 e3d2 c7b6 d5e6 b6d4 e6d7 c8d7 e1c1 d4b6 d2e3 b4c5 h3g3 c5e3 f2e3 e8g8 g4g5 h6g5 g3g5 f8e8 d1d3 f6h7 c3d5 h7g5 d5b6 d7c6 h1g1 g5h3 g1g3 h3f2 b6a8 f2d3 c2d3 e8a8 g3g5 g7g6 g5c5 g8g7 h2h4 a8h8 h4h5 f7f5 d3d4 g6h5 d4d5 c6e8 c5c7 e8f7 d5d6 g7f6 d6d7 h8d8 c7b7 f6e7 e2a6 f7d5 b7b6 d8d7 a6e2 d7c7 c1d2 d5a2 b6h6 c7d7 d2e1 a2b3 h6h7 e7e6 h7d7 e6d7 e2h5 d7e6 h5d1 b3a2 e1f2 e6e5 f2f3 a2f7 d1c2 f7d5 f3g3 d5c4 c2b1 c4d5 b1d3 d5b3 d3b5 e5e4 g3f2 f5f4 b5c6 e4e5 e3e4 1/2-1/2 diff --git a/data/pgn/utf8-bom.pgn b/data/pgn/utf8-bom.pgn new file mode 100644 index 000000000..665e7775a --- /dev/null +++ b/data/pgn/utf8-bom.pgn @@ -0,0 +1,26 @@ +[Event "A"] +[Site "?"] +[Date "2024.04.25"] +[Round "?"] +[White "White vs 1...c5"] +[Black "?"] +[Result "*"] +[ECO "A00"] +[PlyCount "0"] +[SourceVersionDate "2024.04.25"] + + * + +[Event "B"] +[Site "?"] +[Date "2024.04.25"] +[Round "?"] +[White "White vs 1...c5"] +[Black "?"] +[Result "*"] +[ECO "A00"] +[PlyCount "0"] +[SourceVersionDate "2024.04.25"] + + * + diff --git a/docs/Ne4.svg b/docs/Ne4.svg index 21011ac15..4d97635e2 100644 --- a/docs/Ne4.svg +++ b/docs/Ne4.svg @@ -1,7 +1,8 @@ -aabbccddeeffgghh1122334455667788 +
. . . . . . . .
+. . . . . . . .
+. . . . . . . .
+. . . . . . . .
+. . . . N . . .
+. . . . . . . .
+. . . . . . . .
+. . . . . . . .
diff --git a/docs/conf.py b/docs/conf.py index 023d6321c..98e926efa 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,18 +1,30 @@ -import asyncio import sys import os -import sphinx -import sphinx.ext.autodoc -import sphinx.domains.python - # 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", "sphinx.ext.viewcode"] -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" @@ -22,7 +34,7 @@ # General information about the project. project = "python-chess" -copyright = "2014–2020, Niklas Fiekas" +copyright = "2014–2024, Niklas Fiekas" # The version. version = chess.__version__ @@ -36,47 +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" - -# Mark coroutine functions and methods. -def setup(app): - app.add_directive_to_domain("py", "coroutine", PyCoroutineFunction) - app.add_directive_to_domain("py", "coroutinemethod", PyCoroutineMethod) - app.add_autodocumenter(FunctionDocumenter) - app.add_autodocumenter(MethodDocumenter) - - return { - "version": "1.0", - "parallel_read_safe": True, - } - -class PyCoroutineMixin: - def handle_signature(self, sig, signode): - ret = super().handle_signature(sig, signode) - signode.insert(0, sphinx.addnodes.desc_annotation("coroutine ", "coroutine ")) - return ret - -class PyCoroutineFunction(PyCoroutineMixin, sphinx.domains.python.PyModulelevel): - def run(self): - self.name = "py:function" - return super().run() - -class PyCoroutineMethod(PyCoroutineMixin, sphinx.domains.python.PyClassmember): - def run(self): - self.name = "py:method" - return super().run() - -class FunctionDocumenter(sphinx.ext.autodoc.FunctionDocumenter): - def import_object(self): - ret = super().import_object() - if ret and asyncio.iscoroutinefunction(self.parent.__dict__.get(self.object_name)): - self.directivetype = "coroutine" - return ret - -class MethodDocumenter(sphinx.ext.autodoc.MethodDocumenter): - def import_object(self): - ret = super().import_object() - if ret and asyncio.iscoroutinefunction(self.parent.__dict__.get(self.object_name)): - self.directivetype = "coroutinemethod" - return ret +# a list of built-in themes. +html_theme = "sphinx_rtd_theme" diff --git a/docs/core.rst b/docs/core.rst index af737420d..8bb045bbb 100644 --- a/docs/core.rst +++ b/docs/core.rst @@ -7,10 +7,12 @@ 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``. @@ -18,17 +20,23 @@ 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 @@ -38,28 +46,36 @@ 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 @@ -69,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 @@ -77,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 ----- @@ -116,99 +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 (``chess.WHITE`` or ``chess.BLACK``). - - .. py:attribute:: castling_rights - - 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()`. - - .. 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:: promoted - - A bitmask of pieces that have been promoted. - - .. 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. - - >>> 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()`. - - .. 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 @@ -220,35 +135,39 @@ Square sets Common integer masks are: .. py:data:: chess.BB_EMPTY - :annotation: = 0 + :type: chess.Bitboard + :value: 0 .. py:data:: chess.BB_ALL - :annotation: = 0xFFFF_FFFF_FFFF_FFFF + :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: = 0x55AA_55AA_55AA_55AA + :type: chess.Bitboard + :value: 0x55AA_55AA_55AA_55AA .. py:data:: chess.BB_DARK_SQUARES - :annotation: = 0xAA55_AA55_AA55_AA55 + :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 - :annotation: = chess.BB_A1 | chess.BB_H1 | chess.BB_A8 | chess.BB_H8 + :value: chess.BB_A1 | chess.BB_H1 | chess.BB_A8 | chess.BB_H8 .. py:data:: chess.BB_CENTER - :annotation: = chess.BB_D4 | chess.BB_E4 | chess.BB_D5 | chess.BB_E5 + :value: chess.BB_D4 | chess.BB_E4 | chess.BB_D5 | chess.BB_E5 diff --git a/docs/engine.rst b/docs/engine.rst index db27c0ffe..a326430e0 100644 --- a/docs/engine.rst +++ b/docs/engine.rst @@ -1,34 +1,38 @@ UCI/XBoard engine communication =============================== -UCI and XBoard are protocols for communicating with chess engines. This module +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 +.. 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 -(examples show usage with Python 3.7 or later). +`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:: python +.. code-block:: python + :caption: Using synchronous :class:`~chess.engine.SimpleEngine` import chess import chess.engine - engine = chess.engine.SimpleEngine.popen_uci("/usr/bin/stockfish") + 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(): @@ -37,14 +41,15 @@ Example: Let Stockfish play against itself, 100 milliseconds per move. engine.quit() -.. code:: python +.. code-block:: python + :caption: Using asyncio import asyncio import chess import chess.engine - async def main(): - transport, engine = await chess.engine.popen_uci("/usr/bin/stockfish") + 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(): @@ -53,88 +58,33 @@ Example: Let Stockfish play against itself, 100 milliseconds per move. await engine.quit() - asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy()) asyncio.run(main()) -.. autoclass:: chess.engine.EngineProtocol +.. autoclass:: chess.engine.Protocol :members: play .. autoclass:: chess.engine.Limit :members: - .. py:attribute:: time - - Search exactly *time* seconds. - - .. py:attribute:: depth - - Search *depth* ply only. - - .. py:attribute:: nodes - - Search only a limited number of *nodes*. - - .. py:attribute:: mate - - Search for a mate in *mate* moves. - - .. py:attribute:: white_clock - - Time in seconds remaining for White. - - .. py:attribute:: black_clock - - Time in seconds remaining for Black. - - .. py:attribute:: white_inc - - Fisher increment for White, in seconds. - - .. py:attribute:: black_inc - - Fisher increment for Black, in seconds. - - .. py:attribute:: remaining_moves - - 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. - .. autoclass:: chess.engine.PlayResult :members: - .. py:attribute:: move - - The best move accordig to the engine, or ``None``. - - .. py:attribute:: ponder - - The response that the engine expects after *move*, or ``None``. - - .. py:attribute:: info - - A dictionary of extra 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``, - ``tbhits``, ``multipv``. - - Others: ``currmove``, ``currmovenumber``, ``hashfull``, ``cpuload``, - ``refutation``, ``currline``, ``ebf``, ``wdl``, and ``string``. +.. autoclass:: chess.engine.Protocol + :members: send_opponent_information - .. py:attribute:: draw_offered - - Whether the engine offered a draw before moving. - - .. py:attribute:: resigned +.. autoclass:: chess.engine.Opponent + :members: - Whether the engine resigned. +.. autoclass:: chess.engine.Protocol + :members: send_game_result Analysing and evaluating a position ----------------------------------- Example: -.. code:: python +.. code-block:: python + :caption: Using synchronous :class:`~chess.engine.SimpleEngine` import chess import chess.engine @@ -144,54 +94,54 @@ Example: board = chess.Board() info = engine.analyse(board, chess.engine.Limit(time=0.1)) print("Score:", info["score"]) - # Score: +20 + # 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: #+1 + # Score: PovScore(Mate(+1), WHITE) engine.quit() -.. code:: python +.. code-block:: python + :caption: Using asyncio import asyncio import chess import chess.engine - async def main(): + 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: +20 + # 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: #+1 + # Score: PovScore(Mate(+1), WHITE) await engine.quit() - asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy()) asyncio.run(main()) -.. autoclass:: chess.engine.EngineProtocol +.. autoclass:: chess.engine.Protocol :members: analyse +.. autoclass:: chess.engine.InfoDict + .. autoclass:: chess.engine.PovScore :members: - .. py:attribute:: relative - - The relative :class:`~chess.engine.Score`. - - .. py:attribute:: turn +.. autoclass:: chess.engine.Score + :members: - The point of view (``chess.WHITE`` or ``chess.BLACK``). +.. autoclass:: chess.engine.PovWdl + :members: -.. autoclass:: chess.engine.Score +.. autoclass:: chess.engine.Wdl :members: Indefinite or infinite analysis @@ -199,7 +149,8 @@ Indefinite or infinite analysis Example: Stream information from the engine and stop on an arbitrary condition. -.. code:: python +.. code-block:: python + :caption: Using synchronous :class:`~chess.engine.SimpleEngine` import chess import chess.engine @@ -216,13 +167,14 @@ Example: Stream information from the engine and stop on an arbitrary condition. engine.quit() -.. code:: python +.. code-block:: python + :caption: Using asyncio import asyncio import chess import chess.engine - async def main(): + async def main() -> None: transport, engine = await chess.engine.popen_uci("/usr/bin/stockfish") with await engine.analysis(chess.Board()) as analysis: @@ -235,53 +187,49 @@ Example: Stream information from the engine and stop on an arbitrary condition. await engine.quit() - asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy()) asyncio.run(main()) -.. autoclass:: chess.engine.EngineProtocol +.. autoclass:: chess.engine.Protocol :members: analysis .. autoclass:: chess.engine.AnalysisResult :members: - .. py:attribute:: info +.. autoclass:: chess.engine.BestMove + :members: - A dictionary of aggregated information sent by the engine. This is - actually an alias for ``multipv[0]``. +Options +------- - .. py:attribute:: multipv +:func:`~chess.Protocol.configure()`, +:func:`~chess.Protocol.play()`, +:func:`~chess.Protocol.analyse()` and +:func:`~chess.Protocol.analysis()` accept a dictionary of options. - A list of dictionaries with aggregated information sent by the engine. - One item for each root move. +.. code-block:: python + :caption: Using synchronous :class:`~chess.engine.SimpleEngine` -Options -------- + import chess.engine -:func:`~chess.EngineProtocol.configure()`, -:func:`~chess.EngineProtocol.play()`, -:func:`~chess.EngineProtocol.analyse()` and -:func:`~chess.EngineProtocol.analysis()` accept a dictionary of options. - ->>> 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}) ->>> ->>> # [...] + engine = chess.engine.SimpleEngine.popen_uci("/usr/bin/stockfish") -.. code:: python + # 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(): - transport, protocol = await chess.engine.popen_uci("/usr/bin/stockfish") + async def main() -> None: + transport, engine = await chess.engine.popen_uci("/usr/bin/stockfish") # Check available options. print(engine.options["Hash"]) @@ -292,61 +240,14 @@ Option(name='Hash', type='spin', default=16, min=1, max=131072, var=[]) # [...] - asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy()) asyncio.run(main()) -.. autoclass:: chess.engine.EngineProtocol - :members: configure - - .. py:attribute:: options - - Dictionary of available options. +.. autoclass:: chess.engine.Protocol + :members: options, configure .. autoclass:: chess.engine.Option :members: - .. py:attribute:: name - - The name of the option. - - .. py:attribute:: type - - The type of the option. - - +--------+-----+------+------------------------------------------------+ - | type | UCI | CECP | value | - +========+=====+======+================================================+ - | check | X | X | ``True`` or ``False`` | - +--------+-----+------+------------------------------------------------+ - | 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 | - +--------+-----+------+------------------------------------------------+ - - .. py:attribute:: default - - The default value of the option. - - .. 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 allowed string values for a *combo* option. - Logging ------- @@ -364,7 +265,7 @@ when submitting bug reports. AsyncSSH -------- -:class:`~chess.engine.EngineProtocol` can also be used with +:class:`chess.engine.Protocol` can also be used with `AsyncSSH `_ (since 1.16.0) to communicate with an engine on a remote computer. @@ -375,7 +276,7 @@ to communicate with an engine on a remote computer. import chess import chess.engine - async def main(): + 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() @@ -398,17 +299,8 @@ Reference .. autofunction:: chess.engine.popen_xboard -.. autoclass:: chess.engine.EngineProtocol - :members: initialize, ping, quit - - .. py:attribute:: returncode - - Future: Exit code of the process. - - .. py:attribute:: id - - Dictionary of information about the engine. Common keys are ``name`` - and ``author``. +.. autoclass:: chess.engine.Protocol + :members: id, returncode, initialize, ping, quit .. autoclass:: chess.engine.UciProtocol @@ -419,5 +311,3 @@ Reference .. autoclass:: chess.engine.SimpleAnalysisResult :members: - -.. autofunction:: chess.engine.EventLoopPolicy diff --git a/docs/gaviota.rst b/docs/gaviota.rst index 345f999d8..2bcb68b05 100644 --- a/docs/gaviota.rst +++ b/docs/gaviota.rst @@ -5,7 +5,8 @@ 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. -:warning: Ensure tablebase files match the known checksums. Maliciously crafted +.. 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`. 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/pgn.rst b/docs/pgn.rst index 96a4f5a1b..dd5a6fa47 100644 --- a/docs/pgn.rst +++ b/docs/pgn.rst @@ -42,64 +42,24 @@ Use the :class:`~chess.pgn.StringExporter()` or Game model ---------- -Games are represented as a tree of moves. Each :class:`~chess.pgn.GameNode` can have extra -information, such as comments. The root node of a game -(:class:`~chess.pgn.Game` extends the :class:`~chess.pgn.GameNode`) also -holds general information, such as game headers. +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`. -.. autoclass:: chess.pgn.Game - :members: - - .. py:attribute:: headers - - A mapping of headers. By default, the following 7 headers are provided: - - >>> import chess.pgn - >>> - >>> game = chess.pgn.Game() - >>> game.headers - Headers(Event='?', Site='?', Date='????.??.??', Round='?', White='?', Black='?', Result='*') - - .. py:attribute:: errors - - A list of errors (such as illegal or ambiguous moves) encountered while - parsing the game. +.. 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: - .. 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 will never have NAGs. - - .. py:attribute:: comment - :annotation: = '' - - A comment that goes behind the move leading to this node. Comments - that occur before any moves 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 (:func:`~chess.pgn.GameNode.starts_variation()` - checks this) can have a starting comment. The root node can not have - a starting comment. - - .. py:attribute:: variations +.. autoclass:: chess.pgn.Game + :members: headers, errors, setup, accept, from_board, without_tag_roster - A list of child nodes. +.. autoclass:: chess.pgn.ChildNode + :members: parent, move, starting_comment, nags, san, uci, end Visitors -------- diff --git a/docs/polyglot.rst b/docs/polyglot.rst index ce0d60b22..18c94c426 100644 --- a/docs/polyglot.rst +++ b/docs/polyglot.rst @@ -6,32 +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 - :data:`~chess.polyglot.Entry.move` instead. - - .. 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. - - .. py:attribute:: move - - The :class:`~chess.Move`. .. 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 b4b1e311f..2893bed63 100644 --- a/docs/svg.rst +++ b/docs/svg.rst @@ -1,28 +1,15 @@ SVG rendering ============= -The :mod:`chess.svg` module renders SVG Tiny images (mostly for IPython/Jupyter -Notebook integration). The piece images by +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: - - .. py:attribute:: tail - - Start square of the arrow. - - .. py:attribute:: head - - End square of the arrow. - - .. py:attribute:: color - :annotation: = "#888" - - Arrow color. diff --git a/docs/syzygy.rst b/docs/syzygy.rst index b6de388a4..d2ce4edb6 100644 --- a/docs/syzygy.rst +++ b/docs/syzygy.rst @@ -1,11 +1,12 @@ 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 (and experimentally 7) -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. -:warning: Ensure tablebase files match the known checksums. Maliciously crafted +.. warning:: + Ensure tablebase files match the known checksums. Maliciously crafted tablebase files may cause denial of service. .. autofunction:: chess.syzygy.open_tablebase diff --git a/docs/variant.rst b/docs/variant.rst index 8f1c6861c..0a5515873 100644 --- a/docs/variant.rst +++ b/docs/variant.rst @@ -7,7 +7,7 @@ python-chess supports several chess variants. >>> >>> board = chess.variant.GiveawayBoard() ->>> # General information about the variants +>>> # General information about the variants. >>> type(board).uci_variant 'giveaway' >>> type(board).xboard_variant @@ -15,10 +15,6 @@ python-chess supports several chess variants. >>> 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/XBoard Syzygy ================ ========================================= ============= ============ @@ -36,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 -------- @@ -56,9 +66,13 @@ Crazyhouse :members: .. autoclass:: chess.variant.CrazyhouseBoard + :members: legal_drop_squares .. py:attribute:: pockets - :annotation: = [chess.variant.CrazyhousePocket(), chess.variant.CrazyhousePocket()] + :value: [chess.variant.CrazyhousePocket(), chess.variant.CrazyhousePocket()] + + Pockets for each color. For example, ``board.pockets[chess.WHITE]`` + are the pocket pieces available to White. Three-check ----------- @@ -66,7 +80,10 @@ Three-check .. autoclass:: chess.variant.ThreeCheckBoard .. py:attribute:: remaining_checks - :annotation: = [3, 3] + :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 ---------- diff --git a/examples/bratko_kopec/bratko_kopec.py b/examples/bratko_kopec/bratko_kopec.py index 3a4a5bda5..2553f4399 100755 --- a/examples/bratko_kopec/bratko_kopec.py +++ b/examples/bratko_kopec/bratko_kopec.py @@ -1,52 +1,67 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 -"""Run an EPD test suite with an UCI engine.""" +"""Run an EPD test suite with a UCI engine.""" import asyncio -import time import argparse import itertools import logging import sys +import typing + +from typing import List, Tuple, Type import chess import chess.engine import chess.variant -async def test_epd(engine, epd, VariantBoard, 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) - epd_string = epd_info.get("id", board.fen()) + + description = str(epd_info.get("id", board.fen())) + if "am" in epd_info: - epd_string = "{} (avoid {})".format(epd_string, " and ".join(board.san(am) for am in epd_info["am"])) + am = typing.cast(List[chess.Move], epd_info["am"]) + description = "{} (avoid {})".format(description, " and ".join(board.san(m) for m in am)) + else: + am = [] + if "bm" in epd_info: - epd_string = "{} (expect {})".format(epd_string, " or ".join(board.san(bm) for bm in epd_info["bm"])) + bm = typing.cast(List[chess.Move], epd_info["bm"]) + description = "{} (expect {})".format(description, " or ".join(board.san(m) for m in bm)) + else: + bm = [] + + return board, description, am, bm + + +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) limit = chess.engine.Limit(time=movetime) result = await engine.play(board, limit, game=object()) - if "am" in epd_info and result.move in epd_info["am"]: - print(f"{epd_string}: {board.san(result.move)} | +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" in epd_info and result.move not in epd_info["bm"]: - print(f"{epd_string}: {board.san(result.move)} | +0") + elif bm and result.move not in bm: + print(f"{description}: {board.san(result.move)} | +0") return 0.0 else: - print(f"{epd_string}: {board.san(result.move)} | +1") + print(f"{description}: {board.san(result.move)} | +1") return 1.0 -async def test_epd_with_fractional_scores(engine, epd, VariantBoard, movetime): - board, epd_info = VariantBoard.from_epd(epd) - epd_string = epd_info.get("id", board.fen()) - if "am" in epd_info: - epd_string = "{} (avoid {})".format(epd_string, " and ".join(board.san(am) for am in epd_info["am"])) - if "bm" in epd_info: - epd_string = "{} (expect {})".format(epd_string, " or ".join(board.san(bm) for bm in epd_info["bm"])) +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) # Start analysis. score = 0.0 - print(f"{epd_string}:", end=" ", flush=True) + print(f"{description}:", end=" ", flush=True) analysis = await engine.analysis(board, game=object()) with analysis: @@ -57,9 +72,9 @@ async def test_epd_with_fractional_scores(engine, epd, VariantBoard, movetime): if "pv" in analysis.info and len(analysis.info["pv"]) >= 1: move = analysis.info["pv"][0] print(board.san(move), end=" ", flush=True) - if "am" in epd_info and move in epd_info["am"]: + 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) @@ -71,7 +86,7 @@ async def test_epd_with_fractional_scores(engine, epd, VariantBoard, movetime): return score -async def main(): +async def main() -> None: # Parse command line arguments. parser = argparse.ArgumentParser(description=__doc__) @@ -88,7 +103,7 @@ async def main(): parser.add_argument("-t", "--threads", default=1, type=int, help="Threads for use by the UCI engine.") parser.add_argument("-m", "--movetime", default=1.0, type=float, - help="Time to move in milliseconds.") + 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, @@ -105,6 +120,7 @@ async def main(): VariantBoard = chess.variant.find_variant(args.variant) # Open and configure engine. + engine: chess.engine.Protocol if args.uci: _, engine = await chess.engine.popen_uci(args.uci) if args.threads > 1: @@ -136,5 +152,4 @@ async def main(): if __name__ == "__main__": - asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy()) asyncio.run(main()) diff --git a/examples/chess960_pos_list.py b/examples/chess960_pos_list.py index d417b5892..88fe23696 100755 --- a/examples/chess960_pos_list.py +++ b/examples/chess960_pos_list.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 """List all Chess960 starting positions.""" @@ -7,14 +7,14 @@ 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/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/perft.py b/examples/perft/perft.py index 90ce3ae33..2c70cd2ed 100755 --- a/examples/perft/perft.py +++ b/examples/perft/perft.py @@ -1,19 +1,23 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 """ Run perft test to check correctness and speed of the legal move generator. """ -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 + +import chess +import chess.variant + -def perft(depth, board): +def perft(depth: int, board: chess.Board) -> int: if depth == 1: return board.legal_moves.count() elif depth > 1: @@ -29,11 +33,11 @@ def perft(depth, board): return 1 -def parallel_perft(pool, depth, board): +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) @@ -44,19 +48,19 @@ def successors(board): 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. @@ -96,14 +100,14 @@ def main(perft_file, VariantBoard, perft_f, max_depth, max_nodes): if column >= 40: column = 0 - sys.stdout.write(f" nodes {total_nodes} nps {sdiv(total_nodes, time.time() - start_time):.0f}\n") + 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(f" nodes {total_nodes} nps {sdiv(total_nodes, time.time() - start_time):.0f}\n") + 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/tricky.perft b/examples/perft/tricky.perft index e46e9366d..2510a9aea 100644 --- a/examples/perft/tricky.perft +++ b/examples/perft/tricky.perft @@ -95,3 +95,55 @@ 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 index 81009601e..fc4504861 100755 --- a/examples/polyglot_tree.py +++ b/examples/polyglot_tree.py @@ -1,13 +1,16 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 -"""Print a polyglot opening book in tree form.""" +"""Print a Polyglot opening book in tree form.""" + +import argparse + +from typing import Set import chess import chess.polyglot -import argparse -def print_tree(args, visited, level=0): +def print_tree(args: argparse.Namespace, visited: Set[int], level: int = 0) -> None: if level >= args.depth: return @@ -20,11 +23,11 @@ def print_tree(args, visited, level=0): for entry in args.book.find_all(zobrist_hash): print("{}├─ \033[1m{}\033[0m (weight: {}, learn: {})".format( "| " * level, - args.board.san(entry.move()), + args.board.san(entry.move), entry.weight, entry.learn)) - args.board.push(entry.move()) + args.board.push(entry.move) print_tree(args, visited, level + 1) args.board.pop() diff --git a/examples/push_san.py b/examples/push_san.py index d5e9cde1d..6c6e2a091 100755 --- a/examples/push_san.py +++ b/examples/push_san.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 """Play the immortal game using push_san() from chess.Board().""" @@ -6,7 +6,7 @@ import timeit -def play_immortal_game(): +def play_immortal_game() -> None: board = chess.Board() # 1. e4 e5 diff --git a/examples/xray_attacks.py b/examples/xray_attacks.py index a911fc9c3..9935e0779 100755 --- a/examples/xray_attacks.py +++ b/examples/xray_attacks.py @@ -1,11 +1,11 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 -"""Compute X-Ray attacks through more valuable pieces.""" +"""Compute X-ray attacks through more valuable pieces.""" import chess -def xray_rook_attackers(board, color, square): +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 @@ -28,7 +28,7 @@ def xray_rook_attackers(board, color, square): chess.BB_FILE_ATTACKS[square][file_pieces])) -def xray_bishop_attackers(board, color, square): +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 @@ -47,7 +47,7 @@ def xray_bishop_attackers(board, color, square): return chess.SquareSet(board.occupied_co[color] & board.bishops & chess.BB_DIAG_ATTACKS[square][diag_pieces]) -def example(): +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)) diff --git a/fuzz/corpus/engine/stockfish b/fuzz/corpus/engine/stockfish-10 similarity index 100% rename from fuzz/corpus/engine/stockfish rename to fuzz/corpus/engine/stockfish-10 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/engine.py b/fuzz/engine.py index a08d08bf3..ad0cb9fe9 100644 --- a/fuzz/engine.py +++ b/fuzz/engine.py @@ -7,7 +7,6 @@ logging.getLogger("chess.engine").setLevel(logging.CRITICAL) -asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy()) @PythonFuzz 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 de5c1de29..4057ab785 100755 --- a/release.py +++ b/release.py @@ -25,6 +25,7 @@ def check_git(): print(f"master is {behind} commit(s) behind origin/master") sys.exit(1) + def test(): print("--- TEST ---------------------------------------------------------") system("tox --skip-missing-interpreters") @@ -35,8 +36,8 @@ 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 = f"v{chess.__version__}" @@ -96,9 +97,9 @@ def tag_and_push(): def pypi(): print("--- PYPI ---------------------------------------------------------") system("rm -rf build") - system("python3 setup.py sdist bdist_wheel") + system("python3 setup.py sdist") system("twine check dist/*") - system("twine upload --skip-existing --sign dist/*") + system("twine upload --skip-existing dist/*") def github_release(tagname): diff --git a/setup.py b/setup.py index 2bce5cffd..5815947cd 100755 --- a/setup.py +++ b/setup.py @@ -1,21 +1,5 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- -# -# This file is part of the python-chess library. -# Copyright (C) 2012-2020 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 os import platform @@ -34,6 +18,14 @@ 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 @@ -55,16 +47,6 @@ def read_description(): "//readthedocs.org/projects/python-chess/badge/?version=latest", "//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{}".format(chess.__version__)) - - # Show Appveyor build status of the concrete version. - description = description.replace( - "/y9k3hdbm0f0nbum9/branch/master", - "/y9k3hdbm0f0nbum9/branch/v{}".format(chess.__version__)) - # Remove doctest comments. description = re.sub(r"\s*# doctest:.*", "", description) @@ -72,7 +54,7 @@ def read_description(): setuptools.setup( - name="python-chess", + name="chess", version=chess.__version__, author=chess.__author__, author_email=chess.__email__, @@ -84,22 +66,32 @@ def read_description(): url="https://github.com/niklasf/python-chess", packages=["chess"], test_suite="test", - python_requires=">=3.6", + 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 :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "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 41a61eddb..228c62f3f 100755 --- a/test.py +++ b/test.py @@ -1,24 +1,7 @@ -#!/usr/bin/env python -# -# This file is part of the python-chess library. -# Copyright (C) 2012-2020 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 asyncio import copy -import contextlib import logging import os import os.path @@ -87,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): @@ -114,16 +129,16 @@ def test_uci_parsing(self): 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): @@ -185,6 +200,12 @@ def test_from_symbol(self): 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,7 +282,7 @@ 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") @@ -289,7 +322,7 @@ def test_color_at(self): 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")) @@ -359,16 +392,16 @@ def test_castling(self): 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(ValueError): + with self.assertRaises(chess.IllegalMoveError): board.parse_san("Kg1") - with self.assertRaises(ValueError): + 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") @@ -426,15 +459,15 @@ def test_hside_rook_blocks_aside_castling(self): 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") @@ -459,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) @@ -470,6 +503,35 @@ 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_find_move(self): + board = chess.Board("4k3/1P6/8/8/8/8/3P4/4K2R w K - 0 1") + + # 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")) + + # 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) @@ -489,6 +551,9 @@ def _check(board, white, black): _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) @@ -505,6 +570,11 @@ def _check(board, white, black): _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) @@ -517,7 +587,8 @@ def _check(board, white, black): _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"), false_negative, 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") @@ -529,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() @@ -625,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) @@ -661,15 +744,20 @@ def test_lan(self): self.assertEqual(board.fen(), fen) def test_san_newline(self): - fen = "rnbqk2r/ppppppbp/5np1/8/8/5NP1/PPPPPPBP/RNBQK2R w KQkq - 2 4" - board = chess.Board(fen) - - with self.assertRaises(ValueError): + 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(ValueError): + 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', @@ -697,7 +785,7 @@ 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(), @@ -747,7 +835,7 @@ def test_move_count(self): 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. @@ -816,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: @@ -847,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() @@ -938,7 +1026,21 @@ def test_status(self): # 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) + 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() @@ -983,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() @@ -992,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") @@ -1026,13 +1128,13 @@ def test_epd(self): self.assertEqual(operations["bm"], [chess.Move.from_uci("f3g5")]) self.assertEqual(operations["c0"], "ERET.095; Queen sacrifice") - # Test an EPD with string escaping. + # 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 an EPD with unmatched trailing quotes. + # 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")]) @@ -1046,6 +1148,11 @@ def test_eret_epd(self): 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") @@ -1113,12 +1220,12 @@ def test_clear(self): 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()) @@ -1145,12 +1252,12 @@ def test_threefold_repetition(self): 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 indeed 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()) @@ -1185,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") @@ -1217,7 +1324,7 @@ 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") @@ -1231,29 +1338,37 @@ 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()) @@ -1312,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)) @@ -1348,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 . . @@ -1358,7 +1473,7 @@ 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("""\ · · · · · · · ♚ · ♟ · ♛ ♞ · ♝ · ♟ ♗ · ♟ · ♞ · · @@ -1368,7 +1483,7 @@ def test_string_conversion(self): ♙ ♙ · · · · ♙ ♙ · · · · · · ♔ ·""")) - self.assertEqual(board.unicode(invert_color=True, borders=True), textwrap.dedent(u"""\ + self.assertEqual(board.unicode(invert_color=True, borders=True, empty_square="·"), textwrap.dedent("""\ ----------------- 8 |·|·|·|·|·|·|·|♔| ----------------- @@ -1449,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) @@ -1472,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)) @@ -1519,18 +1634,27 @@ 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) @@ -1540,6 +1664,8 @@ 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() @@ -1578,6 +1704,26 @@ def test_is_irreversible(self): 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): @@ -1590,7 +1736,7 @@ def test_nonzero(self): 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(chess.Board().pseudo_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") @@ -1699,24 +1845,30 @@ def test_arithmetic(self): self.assertEqual(bb, chess.BB_C1) def test_immutable_set_operations(self): - self.assertTrue(chess.SquareSet(chess.BB_RANK_1).isdisjoint(chess.BB_RANK_2)) - self.assertFalse(chess.SquareSet(chess.BB_RANK_2).isdisjoint(chess.BB_FILE_E)) - - 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_EMPTY) - - 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) @@ -1795,6 +1947,13 @@ def test_int_conversion(self): 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): @@ -1833,7 +1992,7 @@ def test_lasker_trap(self): 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)) self.assertIn(pos.parse_san("O-O"), moves) @@ -1841,8 +2000,8 @@ def test_castling(self): 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)) self.assertIn(pos.parse_san("O-O-O"), moves) @@ -1933,29 +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) @@ -1978,9 +2137,9 @@ def test_exporter(self): [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. @@ -2022,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) @@ -2070,10 +2229,18 @@ 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 = io.StringIO(textwrap.dedent(r"""\ + 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) @@ -2084,7 +2251,7 @@ def test_comment_at_eol(self): # Make sure the comment for the second variation is there. self.assertIn(5, node[1].nags) - self.assertEqual(node[1].comment, "/\\ Ne7, c6") + self.assertEqual(node[1].comments, ["\n/\\ Ne7, c6"]) def test_promotion_without_equals(self): # Example game from https://github.com/rozim/ChessData as originally @@ -2129,6 +2296,16 @@ def test_header_with_paren(self): 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 = io.StringIO(textwrap.dedent("""\ [Variant "Chess960"] @@ -2162,12 +2339,12 @@ def test_variation_stack(self): def test_game_starting_comment(self): pgn = io.StringIO("{ Game starting comment } 1. d3") game = chess.pgn.read_game(pgn) - self.assertEqual(game.comment, "Game starting comment") + self.assertEqual(game.comments, ["Game starting comment"]) self.assertEqual(game[0].san(), "d3") 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 = io.StringIO(textwrap.dedent("""\ @@ -2175,17 +2352,17 @@ def test_game_starting_variation(self): """)) game = chess.pgn.read_game(pgn) - self.assertEqual(game.comment, "Start of game") + self.assertEqual(game.comments, ["Start of game"]) 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[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 = io.StringIO("1. b4?! g6 2. Bb2 Nc6? 3. Bxh8!!") @@ -2314,11 +2491,11 @@ def test_tricky_skip_game(self): self.assertEqual(len(offsets), 3) pgn.seek(offsets[0]) - self.assertEqual(chess.pgn.read_game(pgn).variations[0].move, chess.Move.from_uci("a2a3")) + 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).variations[0].move, chess.Move.from_uci("b2b3")) + 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).variations[0].move, chess.Move.from_uci("d2d3")) + 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): @@ -2338,6 +2515,53 @@ def test_read_headers(self): self.assertEqual(first_drawn_game.headers["Site"], "03") 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): @@ -2436,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) @@ -2474,12 +2707,12 @@ 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_mainline(self): @@ -2491,7 +2724,6 @@ def test_mainline(self): self.assertEqual(list(game.mainline_moves()), moves) self.assertTrue(game.mainline_moves()) self.assertEqual(list(reversed(game.mainline_moves())), list(reversed(moves))) - self.assertEqual(len(reversed(game.mainline_moves())), 3) self.assertEqual(str(game.mainline_moves()), "1. d3 Nf6 2. e4") def test_lan(self): @@ -2529,6 +2761,12 @@ def test_z0(self): board = game.end().board() self.assertEqual(board.fen(), "5rk1/2p1R2p/p5pb/2PPR3/8/2Q2B2/5P2/4K2q w - - 3 43") + 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_wierd_header(self): pgn = io.StringIO(r"""[Black "[=0040.34h5a4]"]""") game = chess.pgn.read_game(pgn) @@ -2537,7 +2775,7 @@ def test_wierd_header(self): def test_semicolon_comment(self): pgn = io.StringIO("1. e4 ; e5") game = chess.pgn.read_game(pgn) - node = game.variations[0] + node = game.next() self.assertEqual(node.move, chess.Move.from_uci("e2e4")) self.assertTrue(node.is_end()) @@ -2564,10 +2802,10 @@ def test_no_movetext(self): 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.variations[0].variations[1] + 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.variations[0].move, chess.Move.from_uci("c2c4")) + 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): @@ -2577,9 +2815,11 @@ def test_is_wild(self): def test_my_game_node(self): class MyGameNode(chess.pgn.GameNode): - @classmethod - def dangling_node(cls): - return MyGameNode() + 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 class MyGame(chess.pgn.Game, MyGameNode): pass @@ -2590,6 +2830,147 @@ class MyGame(chess.pgn.Game, MyGameNode): node = game.variation(chess.Move.from_uci("e2e4")) self.assertTrue(isinstance(node, MyGameNode)) + 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 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) + + def test_float_emt(self): + game = chess.pgn.Game() + game.comments = ["[%emt 0:00:01.234]"] + self.assertEqual(game.emt(), 1.234) + + game.set_emt(6.54321) + self.assertEqual(game.comments, ["[%emt 0:00:06.543]"]) + self.assertEqual(game.emt(), 6.543) + + game.set_emt(-70) + self.assertEqual(game.comments, ["[%emt 0:00:00]"]) # Clamped + self.assertEqual(game.emt(), 0) + + def test_float_clk(self): + game = chess.pgn.Game() + game.comments = ["[%clk 0:00:01.234]"] + self.assertEqual(game.clock(), 1.234) + + game.set_clock(6.54321) + self.assertEqual(game.comments, ["[%clk 0:00:06.543]"]) + self.assertEqual(game.clock(), 6.543) + + game.set_clock(-70) + self.assertEqual(game.comments, ["[%clk 0:00:00]"]) # Clamped + self.assertEqual(game.clock(), 0) + + 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) + + 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() + + def end_variation(self): + if self.skipping: + self.skipping = False + else: + return super().end_variation() + + 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 ) *" + + # Driven by parser. + game = chess.pgn.read_game(io.StringIO(pgn), Visitor=BlackVariationsOnly) + self.assertEqual(game.accept(chess.pgn.StringExporter(headers=False)), expected_pgn) + + # 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") + + game = chess.pgn.read_game(pgn) + self.assertEqual(game.headers["Event"], "B") + + game = chess.pgn.read_game(pgn) + self.assertEqual(game, None) + @unittest.skipIf(sys.platform == "win32" and (3, 8, 0) <= sys.version_info < (3, 8, 1), "https://bugs.python.org/issue34679") class EngineTestCase(unittest.TestCase): @@ -2648,6 +3029,11 @@ def test_score_ordering(self): 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)) @@ -2665,11 +3051,29 @@ def test_score(self): 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: - engine.configure({"Contempt": 23}) - 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\";", @@ -2679,7 +3083,7 @@ def test_sf_forced_mates(self): for epd in epds: operations = board.set_epd(epd) - result = engine.play(board, chess.engine.Limit(mate=5), game=object()) + result = engine.play(board, chess.engine.Limit(mate=3), game=object()) self.assertIn(result.move, operations["bm"], operations["id"]) @catchAndSkip(FileNotFoundError, "need stockfish") @@ -2691,9 +3095,9 @@ def test_sf_options(self): @catchAndSkip(FileNotFoundError, "need stockfish") def test_sf_analysis(self): - with chess.engine.SimpleEngine.popen_uci("stockfish", debug=True) as engine: + 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=20) + limit = chess.engine.Limit(depth=40) analysis = engine.analysis(board, limit) with analysis: for info in iter(analysis.next, None): @@ -2703,11 +3107,14 @@ def test_sf_analysis(self): self.fail("never found a mate score") for info in analysis: - if "score" in info and info["score"].white() >= chess.engine.Mate(+3): + if "score" in info and info["score"].white() >= chess.engine.Mate(+2): break + analysis.wait() - self.assertEqual(analysis.info["score"].relative, chess.engine.Mate(+3)) - self.assertEqual(analysis.multipv[0]["score"].black(), chess.engine.Mate(-3)) + 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() @@ -2716,6 +3123,7 @@ def test_sf_analysis(self): 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") @@ -2730,7 +3138,7 @@ def test_sf_multipv(self): @catchAndSkip(FileNotFoundError, "need stockfish") def test_sf_quit(self): - engine = chess.engine.SimpleEngine.popen_uci("stockfish", debug=True) + engine = chess.engine.SimpleEngine.popen_uci("stockfish", setpgrp=True, debug=True) with engine: engine.quit() @@ -2738,12 +3146,32 @@ def test_sf_quit(self): 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) + + 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() + + mock.expect("isready", ["readyok"]) + await protocol.ping() + mock.assert_done() + + asyncio.run(main()) + @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", debug=True, cwd=tmpdir) as engine: + 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: @@ -2788,10 +3216,7 @@ async def main(): await protocol.ping() mock.assert_done() - asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy()) - with contextlib.closing(asyncio.get_event_loop()) as loop: - loop.set_debug(True) - loop.run_until_complete(main()) + asyncio.run(main()) def test_uci_debug(self): async def main(): @@ -2806,10 +3231,7 @@ async def main(): protocol.debug(False) mock.assert_done() - asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy()) - with contextlib.closing(asyncio.get_event_loop()) as loop: - loop.set_debug(True) - loop.run_until_complete(main()) + asyncio.run(main()) def test_uci_go(self): async def main(): @@ -2851,10 +3273,7 @@ async def main(): self.assertEqual(result.ponder, None) mock.assert_done() - asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy()) - with contextlib.closing(asyncio.get_event_loop()) as loop: - loop.set_debug(True) - loop.run_until_complete(main()) + asyncio.run(main()) def test_iota_log(self): async def main(): @@ -2876,10 +3295,7 @@ async def main(): await protocol.play(board, chess.engine.Limit(time=5.0)) mock.assert_done() - asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy()) - with contextlib.closing(asyncio.get_event_loop()) as loop: - loop.set_debug(True) - loop.run_until_complete(main()) + asyncio.run(main()) def test_uci_analyse_mode(self): async def main(): @@ -2901,11 +3317,15 @@ async def main(): mock.expect("go infinite") mock.expect("stop", ["bestmove e2e4"]) result = await protocol.analysis(chess.Board()) + self.assertTrue(result.would_block()) result.stop() - await result.wait() + 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() - # Disable explicitly. + # Explicitly disable. mock.expect("setoption name UCI_AnalyseMode value false") await protocol.configure({"UCI_AnalyseMode": False}) mock.assert_done() @@ -2913,16 +3333,157 @@ async def main(): # Analyse again. mock.expect("position startpos") mock.expect("go infinite") - mock.expect("stop", ["bestmove e2e4"]) + mock.expect("stop", ["bestmove e2e4 ponder e7e5"]) result = await protocol.analysis(chess.Board()) result.stop() - await result.wait() + 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.set_event_loop_policy(chess.engine.EventLoopPolicy()) - with contextlib.closing(asyncio.get_event_loop()) as loop: - loop.set_debug(True) - loop.run_until_complete(main()) + 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. @@ -2951,14 +3512,15 @@ def test_uci_info(self): 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. - info = chess.engine._parse_uci_info("tbhits 123 cpuload 456 hashfull 789 time 987 nodes 654 nps 321", board) + # 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) @@ -2983,7 +3545,74 @@ def test_uci_info(self): # 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"], (249, 747, 4)) + 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() + + mock.assert_done() + + asyncio.run(main()) + + def test_hiarcs_bestmove(self): + async def main(): + protocol = chess.engine.UciProtocol() + mock = chess.engine.MockTransport(protocol) + + mock.expect("uci", ["uciok"]) + await protocol.initialize() + + 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(): @@ -3004,7 +3633,7 @@ async def main(): "feature option=\"savevar -save\"", "feature ping=1 setboard=1 done=1", ]) - mock.expect("accept egt") + mock.expect("accepted egt") await protocol.initialize() mock.assert_done() @@ -3045,10 +3674,7 @@ async def main(): await protocol.configure({"buttonvar": None}) mock.assert_done() - asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy()) - with contextlib.closing(asyncio.get_event_loop()) as loop: - loop.set_debug(True) - loop.run_until_complete(main()) + asyncio.run(main()) def test_xboard_replay(self): async def main(): @@ -3071,8 +3697,8 @@ async def main(): mock.expect("d2d4") mock.expect("g8f6") mock.expect("c2c4") - mock.expect("sd 17") mock.expect("st 1.5") + mock.expect("sd 17") mock.expect("nopost") mock.expect("easy") mock.expect("go", ["move e7e6"]) @@ -3084,8 +3710,8 @@ async def main(): board.pop() mock.expect("force") mock.expect("remove") - mock.expect("sd 17") mock.expect("st 1.5") + mock.expect("sd 17") mock.expect("nopost") mock.expect("easy") mock.expect("go", ["move c2c4"]) @@ -3099,8 +3725,8 @@ async def main(): mock.expect("force") mock.expect("remove") mock.expect("undo") - mock.expect("sd 17") mock.expect("st 1.5") + mock.expect("sd 17") mock.expect("nopost") mock.expect("easy") mock.expect("go", ["move d2d4"]) @@ -3109,9 +3735,153 @@ async def main(): self.assertEqual(result.move, board.parse_san("d4")) mock.assert_done() - asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy()) - with contextlib.closing(asyncio.get_event_loop()) as loop: - loop.run_until_complete(main()) + 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(): @@ -3148,10 +3918,7 @@ async def main(): self.assertEqual(info["pv"], [chess.Move.from_uci(move) for move in ["f7f6", "e2e4", "e7e6"]]) mock.assert_done() - asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy()) - with contextlib.closing(asyncio.get_event_loop()) as loop: - loop.set_debug(True) - loop.run_until_complete(main()) + asyncio.run(main()) def test_xboard_level(self): async def main(): @@ -3164,7 +3931,9 @@ async def main(): mock.assert_done() limit = chess.engine.Limit(black_clock=65, white_clock=100, - black_inc=4, white_inc=8) + 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") @@ -3174,14 +3943,26 @@ async def main(): mock.expect("easy") mock.expect("go", ["move e2e4"]) mock.expect_ping() - result = await protocol.play(chess.Board(), limit) + result = await protocol.play(board, limit) self.assertEqual(result.move, chess.Move.from_uci("e2e4")) mock.assert_done() - asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy()) - with contextlib.closing(asyncio.get_event_loop()) as loop: - loop.set_debug(True) - loop.run_until_complete(main()) + 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(): @@ -3189,20 +3970,17 @@ async def main(): mock = chess.engine.MockTransport(protocol) mock.expect("xboard") - mock.expect("protover 2", ["Error (failed to initialize): Too bad ..."]) + 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 engine, but it was not successfully initialized. + # Trying to use the engine, but it was not successfully initialized. await protocol.ping() mock.assert_done() - asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy()) - with contextlib.closing(asyncio.get_event_loop()) as loop: - loop.set_debug(True) - loop.run_until_complete(main()) + asyncio.run(main()) @catchAndSkip(FileNotFoundError, "need /bin/bash") def test_transport_close_with_pending(self): @@ -3213,10 +3991,7 @@ async def main(): self.assertNotEqual(results[0], None) self.assertNotEqual(results[1], None) - asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy()) - with contextlib.closing(asyncio.get_event_loop()) as loop: - loop.set_debug(True) - loop.run_until_complete(main()) + asyncio.run(main()) @catchAndSkip(FileNotFoundError, "need /bin/bash") def test_quit_timeout(self): @@ -3263,7 +4038,7 @@ def test_tablenames(self): self.assertIn("KRPvKP", chess.syzygy.tablenames()) def test_suicide_tablenames(self): - # Test number of 6 piece tables. + # 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): @@ -3367,7 +4142,7 @@ def test_wdl_ep(self): # 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. @@ -3451,7 +4226,6 @@ def test_suicide_dtz(self): 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() @@ -3464,6 +4238,17 @@ def test_suicide_stats(self): 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): @@ -3567,6 +4352,21 @@ def test_two_ep(self): 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): @@ -3596,8 +4396,8 @@ def test_parse_san(self): board.push_san("e4") board.push_san("d5") - # Capture mandatory. - with self.assertRaises(ValueError): + # Capture is mandatory. + with self.assertRaises(chess.IllegalMoveError): board.push_san("Nf3") def test_is_legal(self): @@ -3614,11 +4414,11 @@ def test_suicide_insufficient_material(self): board = chess.variant.SuicideBoard("8/8/8/2k5/8/8/4K3/8 b - - 0 1") self.assertFalse(board.is_insufficient_material()) - # Bishops on same color. + # Bishops on the same color. board = chess.variant.SuicideBoard("8/8/8/5b2/2B5/1B6/8/8 b - - 0 1") self.assertFalse(board.is_insufficient_material()) - # Opposite color bishops. + # Opposite-color bishops. board = chess.variant.SuicideBoard("4b3/8/8/8/3B4/2B5/8/8 b - - 0 1") self.assertTrue(board.is_insufficient_material()) @@ -3626,7 +4426,7 @@ def test_suicide_insufficient_material(self): board = chess.variant.SuicideBoard("8/5b2/5P2/8/3B4/2B5/8/8 b - - 0 1") self.assertFalse(board.is_insufficient_material()) - # Pawns blocked but on wrong color. + # Pawns blocked, but on wrong color. board = chess.variant.SuicideBoard("8/5p2/5P2/8/8/8/3b4/8 b - - 0 1") self.assertFalse(board.is_insufficient_material()) @@ -3667,7 +4467,7 @@ def test_atomic_mate_legality(self): self.assertFalse(board.is_legal(Qa7)) self.assertNotIn(Qa7, board.generate_legal_moves()) - # Ignore check to explode the opponents king. + # Ignore check to explode the opponent's king. Qxe6 = board.parse_san("Qxe6#") self.assertTrue(board.is_legal(Qxe6)) self.assertIn(Qxe6, board.generate_legal_moves()) @@ -3680,7 +4480,7 @@ def test_atomic_mate_legality(self): self.assertNotIn(Nxc4, board.generate_legal_moves()) def test_atomic_en_passant(self): - # Real world position. + # Real-world position. board = chess.variant.AtomicBoard("rn2kb1r/2p1p2p/p2q1pp1/1pPP4/Q7/4P3/PP3P1P/R3K3 w Qkq b6 0 11") board.push_san("cxb6+") self.assertEqual(board.fen(), "rn2kb1r/2p1p2p/p2q1pp1/3P4/Q7/4P3/PP3P1P/R3K3 b Qkq - 0 11") @@ -3700,7 +4500,7 @@ def test_atomic_insufficient_material(self): board = chess.variant.AtomicBoard("8/3k4/8/8/4R3/4K3/8/8 w - - 0 1") self.assertTrue(board.is_insufficient_material()) - # Only bishops but no captures possible. + # Only bishops, but no captures possible. board = chess.variant.AtomicBoard("7k/4b3/8/8/8/3B4/2B5/K7 w - - 0 1") self.assertTrue(board.is_insufficient_material()) @@ -3738,15 +4538,15 @@ def test_atomic_castle_with_kings_touching(self): self.assertEqual(board.fen(), "8/8/8/8/8/8/4k3/2KR3q b - - 1 1") board = chess.variant.AtomicBoard("8/8/8/8/8/8/5k2/R3K2r w Q - 0 1") - with self.assertRaises(ValueError): + with self.assertRaises(chess.IllegalMoveError): board.push_san("O-O-O") board = chess.variant.AtomicBoard("8/8/8/8/8/8/6k1/R5Kr w Q - 0 1", chess960=True) - with self.assertRaises(ValueError): + with self.assertRaises(chess.IllegalMoveError): board.push_san("O-O-O") board = chess.variant.AtomicBoard("8/8/8/8/8/8/4k3/r2RK2r w D - 0 1", chess960=True) - with self.assertRaises(ValueError): + with self.assertRaises(chess.IllegalMoveError): board.push_san("O-O-O") def test_castling_rights_explode_with_king(self): @@ -3766,6 +4566,23 @@ def test_atomic_validity(self): board = chess.variant.AtomicBoard("3N1NB1/2N1Q1N1/3RkR2/2NP1PN1/3NKN2/8/8/n7 w - - 0 1") self.assertEqual(board.status(), chess.STATUS_VALID) + def test_atomic960(self): + pgn = io.StringIO(textwrap.dedent("""\ + [Variant "Atomic"] + [FEN "rkrbbnnq/pppppppp/8/8/8/8/PPPPPPPP/RKRBBNNQ w KQkq - 0 1"] + + 1. g3 d5 2. Nf3 e5 3. Ng5 Bxg5 4. Qf3 Ne6 5. Qa3 a5 6. d4 g6 7. c3 h5 8. h4 Qh6 9. Bd2 Qxd2 10. O-O-O * + """)) + game = chess.pgn.read_game(pgn) + self.assertTrue(game.board().chess960) + self.assertEqual(game.end().parent.board().fen(), "rkr1b1n1/1pp2p2/4n1p1/p2pp2p/3P3P/Q1P3P1/PP2PP2/RK3N2 w Qkq - 0 10") + self.assertEqual(game.end().board().fen(), "rkr1b1n1/1pp2p2/4n1p1/p2pp2p/3P3P/Q1P3P1/PP2PP2/2KR1N2 b kq - 1 10") + + def test_atomic_king_exploded(self): + board = chess.variant.AtomicBoard("rn5r/pp4pp/2p3Nn/5p2/1b2P1PP/8/PPP2P2/R1B1KB1R b KQ - 0 9") + self.assertEqual(board.outcome().winner, chess.WHITE) + self.assertEqual(board.status(), chess.STATUS_VALID) + class RacingKingsTestCase(unittest.TestCase): @@ -3797,6 +4614,46 @@ def test_variant_end(self): self.assertFalse(board.is_variant_win()) self.assertTrue(board.is_variant_loss()) + # White far away. + board = chess.variant.RacingKingsBoard("k1q1R2Q/3N4/8/8/5K2/6n1/1b6/1r6 w - - 4 19") + self.assertTrue(board.is_variant_end()) + self.assertTrue(board.is_variant_loss()) + self.assertFalse(board.is_variant_win()) + self.assertFalse(board.is_variant_draw()) + self.assertEqual(board.result(), "0-1") + + # Black near backrank, but cannot move there. + board = chess.variant.RacingKingsBoard("2KR4/k7/2Q5/4q3/8/8/8/2N5 b - - 0 1") + self.assertTrue(board.is_variant_end()) + self.assertTrue(board.is_variant_loss()) + self.assertFalse(board.is_variant_win()) + self.assertFalse(board.is_variant_draw()) + self.assertEqual(board.result(), "1-0") + + # Black two moves away. + board = chess.variant.RacingKingsBoard("1r4RK/6R1/k1r5/8/8/8/4N3/q2n1n2 b - - 0 1") + self.assertTrue(board.is_variant_end()) + self.assertTrue(board.is_variant_loss()) + self.assertFalse(board.is_variant_win()) + self.assertFalse(board.is_variant_draw()) + self.assertEqual(board.result(), "1-0") + + # Both sides already reached the backrank. + board = chess.variant.RacingKingsBoard("kr3NK1/1q2R3/8/8/8/5n2/2N5/1rb2B1R w - - 11 14") + self.assertTrue(board.is_variant_end()) + self.assertFalse(board.is_variant_loss()) + self.assertFalse(board.is_variant_win()) + self.assertTrue(board.is_variant_draw()) + self.assertEqual(board.result(), "1/2-1/2") + + # Another draw. + board = chess.variant.RacingKingsBoard("1knq1RK1/2n5/8/8/3N4/6N1/6B1/8 w - - 23 25") + self.assertTrue(board.is_variant_end()) + self.assertFalse(board.is_variant_loss()) + self.assertFalse(board.is_variant_win()) + self.assertTrue(board.is_variant_draw()) + self.assertEqual(board.result(), "1/2-1/2") + def test_stalemate(self): board = chess.variant.RacingKingsBoard("1Q4R1/5K2/4B3/8/8/3N4/8/k7 b - - 0 1") self.assertTrue(board.is_game_over()) @@ -3840,7 +4697,7 @@ def test_legal_moves_after_end(self): def test_racing_kings_status_with_check(self): board = chess.variant.RacingKingsBoard("8/8/8/8/R7/8/krbnNB1K/qrbnNBRQ b - - 1 1") self.assertFalse(board.is_valid()) - self.assertEqual(board.status(), chess.STATUS_RACE_CHECK | chess.STATUS_TOO_MANY_CHECKERS) + self.assertEqual(board.status(), chess.STATUS_RACE_CHECK | chess.STATUS_TOO_MANY_CHECKERS | chess.STATUS_IMPOSSIBLE_CHECK) class HordeTestCase(unittest.TestCase): @@ -3865,7 +4722,7 @@ def test_double_pawn_push(self): self.assertIn(chess.Move.from_uci("e1e3"), board.generate_legal_moves()) self.assertTrue(board.is_legal(board.parse_san("e3"))) - # En-passant not possible. + # En passant not possible. board.push_san("e3") self.assertFalse(any(board.generate_pseudo_legal_ep())) @@ -3923,12 +4780,12 @@ def test_check_is_irreversible(self): self.assertTrue(board.is_irreversible(move)) board.push(move) - # Loses castling rights. + # Lose castling rights. move = board.parse_san("Ke7") self.assertTrue(board.is_irreversible(move)) board.push(move) - # Gives a check. + # Give check. move = board.parse_san("Nc6+") self.assertTrue(board.is_irreversible(move)) @@ -4017,7 +4874,7 @@ def test_capture_with_promotion(self): def test_illegal_drop_uci(self): board = chess.variant.CrazyhouseBoard() - with self.assertRaises(ValueError): + with self.assertRaises(chess.IllegalMoveError): board.parse_uci("N@f3") def test_crazyhouse_fen(self): diff --git a/tox.ini b/tox.ini index 63dd38810..493a46feb 100644 --- a/tox.ini +++ b/tox.ini @@ -1,20 +1,38 @@ [tox] -envlist = py35,py36,py37,py38,pypy3 +envlist = py38,py39,py310,py311,py312,py313,py314 [testenv] passenv = LD_LIBRARY_PATH whitelist_externals = stockfish crafty + fairy-stockfish commands = python test.py --verbose python -m doctest README.rst --verbose +[testenv:{py38,py39}] +passenv = LD_LIBRARY_PATH +whitelist_externals = + stockfish + crafty +commands = + python test.py --verbose + [flake8] ignore = - E126 E131 # allow over indent and unaligned indent - E241 # allow indenting array elements - E302 E305 # allow grouping functions - E741 # allow "ambiguous" variable names - W504 # allow binary operators before eol + # Allow over-indent and unaligned indent + E126 E131 + # Allow indenting array elements + E241 + # Allow grouping functions + E302 E305 + # Allow "ambiguous" variable names + E741 + # Allow binary operators before EOL (end of line) + W504 + # Allow "def overloaded(): ..." + E704 E301 + # Bare except (with or without reraise) + E722 max-line-length = 160